第T4周:猴痘病识别

前言

  • 实验环境

    python 3.9.2
    tensorflow 2.10.0
    Jupyter Notebook: 7.4.5

代码实现

设置gpu

  • tf.config.experimental.set_memory_growth(gpu0, True)
    • 作用:启用显存按需分配。
    • 参数说明:
      • 第一个参数:要设置的 PhysicalDevice 对象(这里是 gpu0)。
      • 第二个参数:True 表示启用显存增长;False 表示禁用(默认行为是一次性分配全部显存)。
    • 为什么要这样设置
      • 默认情况下,TensorFlow 会在程序启动时锁定 GPU 的全部显存,即使你只用了一小部分。
      • 启用 memory_growth=True 后,显存会随着模型训练/推理的需求逐步增长,更节省资源,也允许多个进程共享同一块 GPU(只要总显存够用)。
  • tf.config.set_visible_devices([gpu0], "GPU")
    • 作用:限制 TensorFlow 只能"看到"指定的设备。
    • 参数说明:
      • 第一个参数:一个设备列表(这里只包含 gpu0)。
      • 第二个参数:设备类型(如 "GPU""CPU")。
    • 效果:
      • 即使系统有多个 GPU,TensorFlow 也只会使用 [gpu0] 这一块。
      • 其他 GPU 对 TensorFlow 完全不可见,不会被分配任务,也不会被占用显存。
python 复制代码
# 设置GPU
import tensorflow as tf

gpus = tf.config.list_physical_devices("GPU") # 列出tensorflow能识别到的所有的gpu设备,返回的是列表

if gpus:
    gpu0 = gpus[0] # 设置为如果有多个gpu,仅使用第0个gpu
    tf.config.experimental.set_memory_growth(gpu0, True) # 设置gpu显存用量按需使用
    tf.config.set_visible_devices([gpu0], "GPU") # 将其它gpu对tensorflow隐藏,确保只使用指定的gpu

gpus

导入数据

python 复制代码
import pathlib

# 定义数据目录路径
data_dir = "./data/"

# 将字符串路径转换为 pathlib.Path 对象,便于后续使用路径时可以直接使用/来拼接
data_dir = pathlib.Path(data_dir)

查看数据

python 复制代码
# 查看数据集图片数量
image_count = len(list(data_dir.glob('*/*.jpg')))
print("数据集中图片总数量为:", image_count)
python 复制代码
import PIL
# 打开某种图片查看图片情况
Monkeypox = list(data_dir.glob('Monkeypox/*.jpg'))
# 由于PIL.Image.open() 只接受字符串路径(或类文件对象),不接受 pathlib.Path 对象,因此还需要使用str来进行强制类型转换
PIL.Image.open(str(Monkeypox[0]))

数据加载

  • tf.keras.preprocessing.image_dataset_from_directory(...)

    • 作用:从按类别组织的图像文件夹结构中自动加载数据,生成一个可直接用于训练的 tf.data.Dataset 对象,每个批次包含 (images, labels)

    • 参数说明与设计目的:

    • directory

      • 作用:指定图像数据集的根目录路径。

      • 为什么要这样设置

        函数需要知道从哪里读取图像。要求目录下每个子文件夹代表一个类别(如 cloudy/, rainy/),这是监督学习中常见的"文件夹即标签"约定,无需额外标注文件。

    • validation_splitsubset

      • 作用:

        • validation_split:指定保留多少比例的数据作为验证集(如 0.2 表示 20%)。
        • subset:指明当前加载的是 "training" 还是 "validation" 子集。
      • 为什么要这样设置

        为了在不手动划分数据的前提下,自动将数据分为训练集和验证集,便于模型评估和防止过拟合。配合固定 seed 可确保划分结果可复现。

    • seed

      • 作用:随机种子,控制数据打乱和训练/验证划分的随机性。

      • 为什么要这样设置

        确保每次运行代码时,训练集和验证集的划分完全一致,提升实验的可重复性。若不设 seed,每次划分可能不同,导致结果不可比。

    • image_size=(height, width)

      • 作用:将所有输入图像统一调整为指定高度和宽度。

      • 为什么要这样设置

        深度学习模型(尤其是 CNN)要求输入尺寸固定。原始图像大小不一,必须通过 resize 统一维度,才能组成 batch(张量需形状一致)。

    • batch_size

      • 作用:每个批次包含的图像数量。

      • 为什么要这样设置

        批量处理能提升 GPU 利用率和训练稳定性。太小则训练慢、梯度噪声大;太大则显存可能溢出。32 是常用默认值,可根据 GPU 显存调整。

    • label_mode

      • 作用:控制标签的编码格式,可选 int(整数)、'categorical'(one-hot)、'binary'(二分类)或 None。默认值为int

      • 为什么要这样设置

        为了匹配损失函数的要求:

        • SparseCategoricalCrossentropy → 需 'int' 标签;
        • CategoricalCrossentropy → 需 'categorical'(one-hot)标签;
        • 自动适配避免手动转换标签格式。
    • color_mode

      • 作用:指定图像加载的颜色通道数,如 'rgb'(3通道)、'grayscale'(1通道)。默认值为rgb

      • 为什么要这样设置

        根据任务需求决定输入维度。例如医学图像常用灰度图(节省计算),而自然图像通常用 RGB。确保输入张量通道数与模型第一层匹配。

    • shuffle(默认 True

      • 作用:是否在每轮训练前打乱数据顺序。

      • 为什么要这样设置

        打乱数据可避免模型学习到"顺序依赖",提升泛化能力。但在验证/测试时通常设为 False 以保持结果稳定。

    • class_names(可选)

      • 作用:显式指定类别的顺序和名称列表。

      • 为什么要这样设置

        默认按子目录字母顺序排序类别,但有时希望固定类别索引(如保证 cloudy=0 始终不变)。

python 复制代码
# 基本参数设置
batch_size = 32
img_height = 224
img_width = 224

# 数据加载
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir, # 数据所在的根目录路径,每个子文件夹代表一个类别
    validation_split=0.2, # 20%的数据将被保留作为验证集
    subset="training", # 指定当前加载的是训练集部分
    seed=123, # 随机种子,确保训练集和验证集划分的一致性
    image_size=(img_height, img_width), # 所有图像都将调整到这个尺寸
    batch_size=batch_size # 每个批次包含的样本数量
)
python 复制代码
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split=0.2,
    subset="validation", # 明确当前加载的是验证子集
    seed=123,
    image_size=(img_height, img_width),
    batch_size=batch_size)

输出标签

python 复制代码
class_names = train_ds.class_names
print(class_names)

数据可视化

python 复制代码
import matplotlib.pyplot as plt

# 创建一个宽 20 英寸、高 10 英寸的绘图窗口(画布)
# 较大的尺寸便于清晰展示多张图像及其标签
plt.figure(figsize=(20, 10))

# 从训练数据集 train_ds 中取出 1 个 batch 的数据(包含 images 和 labels)
# .take(1) 返回一个只包含第一个 batch 的 Dataset 对象,用于可视化
for images, labels in train_ds.take(1):
    # 遍历该 batch 中的前 20 张图像(假设 batch_size ≥ 20)
    for i in range(20):
        # 在 5 行 10 列的网格中创建第 (i+1) 个子图(共最多 50 个位置,仅使用前 20 个)
        ax = plt.subplot(5, 10, i + 1)

        # 将第 i 张图像从 TensorFlow 张量转换为 NumPy 数组,并转为 uint8 类型(像素值范围 [0, 255])
        # 这是 matplotlib 显示图像所要求的数据格式
        plt.imshow(images[i].numpy().astype("uint8"))

        # 设置子图标题为对应的类别名称
        # labels[i] 是一个整数标签,通过 class_names[labels[i]] 映射为可读的字符串
        plt.title(class_names[labels[i]])

        # 关闭坐标轴(不显示刻度和边框)
        plt.axis("off")

再次检查数据

python 复制代码
for image_batch, labels_batch in train_ds:
    print(image_batch.shape)
    print(labels_batch.shape)
    break

配置数据集

  • AUTOTUNE = tf.data.AUTOTUNE

    • 作用:

      • tf.data.AUTOTUNE 是一个特殊常量,告诉 TensorFlow 自动根据系统资源和运行时情况,动态选择最优的并行度或缓冲区大小。

      • 它可以作为 prefetch(), map(), interleave() 等函数的 num_parallel_callsbuffer_size 参数。

  • train_ds.cache()

    • 作用:

      • 将数据集缓存到内存或本地磁盘,避免每次 epoch 都重复从磁盘读取和解码图像。
    • 缓存位置

      • 默认(无参数):缓存到内存(适合中小型数据集)。

      • 指定路径:cache("/path/to/cache") → 缓存到磁盘(适合大型数据集,避免 OOM)。

  • train_ds.shuffle(1000)

    • 作用:

      • 在每个 epoch 开始前,打乱数据顺序,确保模型不会学习到"顺序依赖"。

      • 关键参数:buffer_size=1000

        • TensorFlow 采用蓄水池抽样实现 shuffle。

        • 它维护一个大小为 1000 的缓冲区:

          1. 先从数据集中读取前 1000 个样本放入缓冲区;
          2. 每次随机从缓冲区中取出一个样本输出,并从数据集补充一个新样本;
          3. 重复直到数据集耗尽。
  • .prefetch(buffer_size=AUTOTUNE)

    • 作用:
      • 预加载下一个 batch 的数据到内存,实现 "数据加载"与"模型训练"的并行化。
python 复制代码
AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

构建模型

  • 各层的作用:
    • 输入预处理层 (Rescaling):
      • 作用:对输入图像像素值进行归一化预处理。
      • 原理说明:原始图像像素值通常在 [0, 255] 范围内,而神经网络在 [0, 1][-1, 1] 范围内训练更稳定。Rescaling(1./255) 将每个像素除以 255,将其缩放到 [0, 1] 区间。
    • 卷积层 1 (Conv2D(16, (3, 3), activation='relu')):
      • 作用:提取输入图像的初级局部特征(如边缘、角点、简单纹理)。
      • 参数解析:
        • 16:使用 16 个 3×3 卷积核,输出 16 通道的特征图;
        • (3, 3):卷积核尺寸为 3×3;
        • activation='relu':引入非线性激活函数,缓解梯度消失问题。
      • 输入形状:由 input_shape=(img_height, img_width, 3) 指定,表示接收 224×224 的 RGB 彩色图像。
    • 池化层 1 (AveragePooling2D((2, 2))):
      • 作用:对特征图进行空间下采样,降低分辨率。
      • 工作方式:使用 2×2 的平均池化窗口,将每 2×2 区域的像素取平均值作为输出,使特征图的高和宽各缩小一半。
    • 卷积层 2 (Conv2D(32, (3, 3), activation='relu')):
      • 作用:在第一层提取的低级特征基础上,组合出更复杂的中级语义特征(如局部部件、重复纹理或光照变化模式)。
      • 参数解析:
        • 32:卷积核数量翻倍,提升特征表达能力;
        • 继续使用 3×3 卷积核和 ReLU 激活,保持结构一致性,同时逐步增加模型容量。
    • 池化层 2 (AveragePooling2D((2, 2))):
      • 作用:再次进行 2 倍下采样,进一步压缩空间维度,聚焦更高层次的抽象特征。
      • 效果:经过两次池化后,原始 180×180 图像的空间尺寸已显著缩小(默认无填充时约为 43×43),减少计算量并增强平移不变性。
    • Dropout 层 (Dropout(0.3)):
      • 作用:一种正则化技术,在训练过程中随机将 30% 的神经元输出置零。
      • 防止过拟合:强制网络不依赖于特定神经元,提升泛化能力。尤其在小数据集(如天气图像)上,可有效缓解训练准确率高但验证准确率低的问题。
      • 注意:仅在训练阶段生效,推理(预测)时自动关闭。
    • 卷积层 3 (Conv2D(64, (3, 3), activation='relu')):
      • 作用:提取高级语义特征,如整体场景结构、判别性强的模式(例如"是否有云层"、"是否出现强光"等二分类关键线索)。
      • 参数解析:
        • 64:继续增加通道数,使模型能学习更丰富的特征表示;
        • 此层输出的特征图具有较强判别性,为后续分类提供关键依据。
    • Dropout 层 (Dropout(0.3))(第二次):
      • 作用:在深层特征之后再次施加正则化,进一步抑制模型对训练数据的过度拟合。
    • Flatten 层 (Flatten()):
      • 作用:将多维卷积特征张量(如 [batch, H, W, C])展平为一维向量([batch, H×W×C]),作为全连接层的输入。
    • 全连接层 (Dense(128, activation='relu')):
      • 作用:整合来自卷积主干的所有空间和通道信息,进行全局特征融合与非线性变换。
      • 参数解析:
        • 128:使用 128 个神经元构建一个紧凑的高维嵌入空间;
        • ReLU 激活继续引入非线性,增强模型判别能力。
    • 输出层 (Dense(num_classes)):
      • 作用:生成最终的分类 logits(未归一化的原始分数)。
      • 结构说明:
        • num_classes = 2,因此输出 2 个数值,分别对应两个类别;
        • 无激活函数:配合损失函数 SparseCategoricalCrossentropy(from_logits=True) 使用,由损失函数内部完成数值稳定的 softmax 计算;
        • 最终预测类别通过 argmax(output) 获得。
python 复制代码
from tensorflow.keras import layers, models

# 定义类别数量(此处为二分类任务)
num_classes = 2

# 使用 Sequential 模型按顺序堆叠神经网络层
model = models.Sequential([
    # 输入预处理层:像素值归一化
    layers.experimental.preprocessing.Rescaling(
        1./255, 
        input_shape=(img_height, img_width, 3)
    ),

    # 第一组卷积块:提取低级视觉特征(边缘、角点、纹理)
    layers.Conv2D(16, (3, 3), activation='relu'), # 卷积层1,使用16个3×3卷积核
    layers.AveragePooling2D((2, 2)), # 池化层1,2×2平均池化,空间尺寸减半

    # 第二组卷积块:组合低级特征,形成中级语义模式
    layers.Conv2D(32, (3, 3), activation='relu'), # 卷积层2,通道数翻倍至32,增强表达能力
    layers.AveragePooling2D((2, 2)), # 池化层2,再次下采样,聚焦更抽象区域

    # 正则化层:防止过拟合
    layers.Dropout(0.3),  

    # 第三组卷积块:提取高级语义特征(如整体结构、判别性模式)
    layers.Conv2D(64, (3, 3), activation='relu'), # 卷积层3,64个卷积核,捕捉复杂模式
    layers.Dropout(0.3), # 再次加入 Dropout,进一步抑制过拟合

    # 全连接分类头:从特征图到类别预测
    layers.Flatten(), # 将多维特征图展平为一维向量
    layers.Dense(128, activation='relu'), # 全连接隐藏层
    layers.Dense(num_classes) # 输出层,输出num_classes 个 logits(未归一化分数)
])

# 打印模型各层的名称、输出形状和参数量,用于验证结构是否符合预期
model.summary()

编译模型

python 复制代码
# 设置优化器
opt = tf.keras.optimizers.Adam(learning_rate=1e-4)

model.compile(optimizer=opt,
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

训练模型

python 复制代码
from tensorflow.keras.callbacks import ModelCheckpoint

# 设置训练总轮数(epochs)为 50
epochs = 50

# 创建 ModelCheckpoint 回调函数,用于在训练过程中自动保存最优模型
checkpointer = ModelCheckpoint(
    'best_model.h5', # 保存路径:将最优模型权重保存为 best_model.h5 文件
    monitor='val_accuracy', # 监控指标:以验证集准确率(validation accuracy)作为评判标准
    verbose=1, # 日志级别:1 表示在保存时打印提示信息
    save_best_only=True, # 仅保存最佳模型:只有当 monitored 指标比之前更好时才保存
    save_weights_only=True # 仅保存权重:不保存完整模型结构(只存 .h5 权重文件,体积更小)
)

# 启动模型训练
history = model.fit(
    train_ds, # 训练数据集(tf.data.Dataset 对象)
    validation_data=val_ds, # 验证数据集,用于每轮结束后评估泛化性能
    epochs=epochs, # 总训练轮数
    callbacks=[checkpointer] # 注册回调函数列表,此处包含模型自动保存机制
)


模型评估

python 复制代码
from datetime import datetime
current_time = datetime.now() # 获取当前时间

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(epochs)

plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.xlabel(current_time)

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

指定图片预测

python 复制代码
# 加载效果最好的模型权重
model.load_weights('best_model.h5')
python 复制代码
import tensorflow as tf
from PIL import Image
import numpy as np

# 加载图像
img = Image.open("./data/Monkeypox/M06_01_04.jpg")

# 将 PIL 图像转换为 NumPy 数组 
img_np = np.array(img)

# 调整图像尺寸
image = tf.image.resize(img_np, [img_height, img_width])

# 添加 Batch 维度
img_array = tf.expand_dims(image, 0) 

# 使用模型预测
predictions = model.predict(img_array)
print("预测结果为:", class_names[np.argmax(predictions)])

学习总结

  • 更熟练掌握了 tf.keras.preprocessing.image_dataset_from_directory 的用法。该函数能自动从"文件夹即标签"的目录结构中加载图像数据,生成高效的 tf.data.Dataset 对象。通过设置 validation_splitsubset参数,不用手动划分数据集就可以得到训练集与验证集;固定 seed 保证了数据集划分的可复现。
  • 对cnn网络中各层的作用有了更清晰的认识:Rescaling 层完成像素归一化;三个 Conv2D 层逐级提取从边缘到语义的多层次特征;AveragePooling2D 进行平滑下采样;Dropout(0.3) 有效缓解小数据集上的过拟合问题;Flatten 层连接卷积与全连接部分;最后的 Dense 层输出 logits。
  • 学会了 通过 ModelCheckpoint 回调机制 "只保存验证集上表现最好的模型权重"。 避免训练结束时因过拟合导致的次优模型被误用,还保证了后续预测基于泛化能力最强的版本。
  • 知道了对数据预处理一致性的重要:必须将 PIL 图像先转换为 NumPy 数组,再经过与训练时相同的尺寸调整和归一化流程,才能进行预测。
相关推荐
2301_790300966 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
VCR__6 小时前
python第三次作业
开发语言·python
韩立学长6 小时前
【开题答辩实录分享】以《助农信息发布系统设计与实现》为例进行选题答辩实录分享
python·web
2401_838472517 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272717 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
工程师老罗7 小时前
优化器、反向传播、损失函数之间是什么关系,Pytorch中如何使用和设置?
人工智能·pytorch·python
Fleshy数模7 小时前
我的第一只Python爬虫:从Requests库到爬取整站新书
开发语言·爬虫·python
CoLiuRs7 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask
小鸡吃米…7 小时前
机器学习 —— 训练与测试
人工智能·python·机器学习
七夜zippoe7 小时前
Docker容器化Python应用最佳实践:从镜像优化到安全防护
python·docker·云原生·eureka·容器化