前言

  • 实验环境
python 3.9.2
tensorflow 2.10.0                    
Jupyter Notebook: 7.4.5

代码实现

设置gpu

import tensorflow as tf

gpus = tf.config.list_physical_devices("GPU")
  # 设置GPU显存用量按需使用
if gpus:
    tf.config.experimental.set_memory_growth(gpus[0], True)
    tf.config.set_visible_devices([gpus[0]],"GPU")

# 打印出检测到的 GPU 列表
print(gpus)

在这里插入图片描述

导入数据

import matplotlib.pyplot as plt
# 支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号

import pathlib
# 隐藏警告
import warnings
warnings.filterwarnings('ignore')

data_dir = "../../datasets/catdog"
data_dir = pathlib.Path(data_dir)

image_count = len(list(data_dir.glob('*/*')))

print("图片总数为:{}".format(image_count))

在这里插入图片描述

数据加载

batch_size = 8
img_height = 224
img_width = 224

# 数据加载
# 加载数据集,自动完成:调整尺寸、打乱数据、划分验证集
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split = 0.2,
    subset = "training",
    seed = 12,
    image_size = (img_height, img_width),
    batch_size = batch_size)

在这里插入图片描述

val_ds = tf.keras.preprocessing.image_dataset_from_directory(
    data_dir,
    validation_split = 0.2,
    subset = "validation",
    seed = 12,
    image_size = (img_height, img_width),
    batch_size = batch_size)

在这里插入图片描述

输出标签

class_names = train_ds.class_names
print(class_names)

在这里插入图片描述

再次检查数据

for image_batch, labels_batch in train_ds:
    print(image_batch.shape)
    print(labels_batch.shape)
    break

在这里插入图片描述

处理数据集以及优化数据加载效率

AUTOTUNE = tf.data.AUTOTUNE

# 将图像像素值从 [0, 255](通常是 uint8 类型)缩放到 [0, 1] 的浮点范围。
def preprocess_image(image,label):
    return (image/255.0,label)

# 归一化处理,.map(func)表示对数据集中的每个元素应用 func。num_parallel_calls=AUTOTUNE表示并行执行预处理操作。
train_ds = train_ds.map(preprocess_image, num_parallel_calls = AUTOTUNE)
val_ds = val_ds.map(preprocess_image, num_parallel_calls = AUTOTUNE)

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

数据可视化

plt.figure(figsize = (15, 10))  # 图形的宽为15高为10

for images, labels in train_ds.take(1):
    for i in range(8):
        
        ax = plt.subplot(5, 8, i + 1) 
        plt.imshow(images[i])
        plt.title(class_names[labels[i]])
        
        plt.axis("off")

在这里插入图片描述

数据增强

data_augmentation = tf.keras.Sequential([
  tf.keras.layers.experimental.preprocessing.RandomFlip("horizontal_and_vertical", input_shape = (img_height, img_width, 3)),
  tf.keras.layers.experimental.preprocessing.RandomRotation(0.2),
])
  • 各层功能详解

    • RandomFlip(“horizontal_and_vertical”, input_shape=…)

      • 作用:随机对图像进行 水平翻转(左右镜像) 和/或 垂直翻转(上下颠倒)。

      • 触发概率:
        每个方向独立以约 50% 概率翻转(也可能都不翻或都翻)。

      • 参数说明:

        • "horizontal":仅水平翻转
        • "vertical":仅垂直翻转
        • "horizontal_and_vertical":两者都启用
    • RandomRotation(0.2)

      • 作用:对图像进行 随机旋转。

      • 角度范围:factor=0.2 表示旋转角度在 [−0.2, +0.2] 弧度 之间(≈ ±11.5°)。

    • input_shape=(img_height, img_width, 3)

      • 必须在第一个层指定输入形状,否则模型无法知道输入维度会产生报错。

      • 常见值:

        • (224, 224, 3):VGG、ResNet 等标准输入
        • (32, 32, 3):CIFAR-10
        • (299, 299, 3):Inception 系列
        • 若未指定 input_shape,在将此 Sequential 嵌入主模型时会报错:
          ValueError: This model has not yet been built.

构建模型

  • 各层的作用:

    • 前五层卷积块(Block 1 - Block 5)

      • Block 1 & 2: 通过双卷积层堆叠提取基础纹理与中级形状。
      • Block 3, 4 & 5: 采用三卷积层堆叠,通道数由 256 提升至 512。随着层数加深,感受野扩大,模型开始理解复杂的语义信息(如猫的耳朵、狗的鼻子等)。
      • 最大池化层 (MaxPooling2D):在每个 Block 结束处进行下采样,在保留关键特征的同时,将空间维度从 224 × 224 224 \times 224 224×224 逐步压缩至 7 × 7 7 \times 7 7×7
    • 全连接分类层(针对二分类优化)

      • Flatten 层: 将末端卷积特征展平为 25,088 维向量。
      • 全连接层 1 & 2 (Dense 4096):进行全局特征的非线性组合与变换。
      • Dropout 层 (Dropout(0.5)) :
        • 作用:在训练过程中随机断开 50% 的神经元连接。
        • 原理:防止模型过度依赖某些特定特征。
      • 输出层 (Dense(2, activation=‘softmax’)) :
        • 作用:输出一个二维概率向量,分别表示输入图像属于“猫”和“狗”两个类别的预测概率。Softmax 激活函数确保这两个概率之和为 1,模型通过选择概率较高的类别作为最终预测结果。
    • 为什么引入 Dropout 层?

      • VGG16 是一个拥有超过 1.3 亿参数的巨大模型。而使用的数据集(几千张图片)相对模型容量来说太小了,就会导致模型找捷径记住图片,就好比一个老师给了学生四五百道题,而这个学生是个记忆天才可以记忆上千甚至上万道题,所以他就会直接去记住这些题而不是总结解题规律,这就导致遇到新题还是不会做。
num_classes = len(class_names)

model = tf.keras.Sequential([
    data_augmentation,
    # VGG16 主干
    # Block 1
    tf.keras.layers.Conv2D(64, (3, 3), activation = 'relu', padding = 'same', name = 'block1_conv1'),
    tf.keras.layers.Conv2D(64, (3, 3), activation = 'relu', padding = 'same', name = 'block1_conv2'),
    tf.keras.layers.MaxPooling2D((2, 2), strides = 2, name = 'block1_pool'),

    # Block 2
    tf.keras.layers.Conv2D(128, (3, 3), activation = 'relu', padding = 'same', name = 'block2_conv1'),
    tf.keras.layers.Conv2D(128, (3, 3), activation = 'relu', padding = 'same', name = 'block2_conv2'),
    tf.keras.layers.MaxPooling2D((2, 2), strides = 2, name = 'block2_pool'),

    # Block 3
    tf.keras.layers.Conv2D(256, (3, 3), activation = 'relu', padding = 'same', name = 'block3_conv1'),
    tf.keras.layers.Conv2D(256, (3, 3), activation = 'relu', padding = 'same', name = 'block3_conv2'),
    tf.keras.layers.Conv2D(256, (3, 3), activation = 'relu', padding = 'same', name = 'block3_conv3'),
    tf.keras.layers.MaxPooling2D((2, 2), strides = 2, name = 'block3_pool'),

    # Block 4
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block4_conv1'),
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block4_conv2'),
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block4_conv3'),
    tf.keras.layers.MaxPooling2D((2, 2), strides = 2, name = 'block4_pool'),

    # Block 5
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block5_conv1'),
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block5_conv2'),
    tf.keras.layers.Conv2D(512, (3, 3), activation = 'relu', padding = 'same', name = 'block5_conv3'),
    tf.keras.layers.MaxPooling2D((2, 2), strides = 2, name = 'block5_pool'),

    # 全连接分类
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(4096, activation = 'relu', name = 'fc1'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(4096, activation = 'relu', name = 'fc2'),
    tf.keras.layers.Dropout(0.5),
    tf.keras.layers.Dense(num_classes, activation = 'softmax', name = 'predictions')
])

# 编译模型
model.compile(optimizer = "adam",
              loss = 'sparse_categorical_crossentropy',
              metrics = ['accuracy'])

# 打印结构
model.summary()

在这里插入图片描述
在这里插入图片描述

训练模型

  • model.train_on_batch(x, y) 是 对单个 batch 的数据执行一次前向传播、损失计算、反向传播和参数更新。相比 model.fit()(自动处理整个数据集、epoch、batch 等),train_on_batch 可以自定义训练循环。

  • tqdm是一个 Python 进度条库,能为可迭代对象添加进度条。

    • 参数说明:
      • total: 总步数(用于计算百分比)
      • desc: 前缀描述(如 “Epoch 1/10”)
      • mininterval: 最小更新间隔
      • ncols: 进度条总宽度(字符数)
      • set_postfix(): 在进度条末尾动态显示额外信息(如 loss、acc)
      • update(n): 手动推进 n 步
  • 学习率调整说明:

    • 我将参考文档里的学习率从 10 − 4 10^{-4} 104 下调至 10 − 5 10^{-5} 105,并将每个 Epoch 的衰减系数从 0.92 0.92 0.92 放宽至 0.98 0.98 0.98
    • 调整原因与效果:
      • 使用 10 − 5 10^{-5} 105 作为起步,是为了让模型在训练初期更加平稳,同时保证学习率降得慢一点,防止学习率衰减过快。
      • 原有的 0.92 0.92 0.92 衰减过快,导致模型很快就缩减了一半以上,开始原地踏步。调整为 0.98 0.98 0.98 后,衰减没那么快了,模型后续仍然能学到东西继续进步。
      • 调整后的策略成功让模型突破了 50% 准确率的瓶颈。训练集准确率一路上升至接近 1.0,训练集 Loss 也成功下降至接近 0.0,说明模型收敛成功。然而,验证集准确率在达到约 0.8 之后开始剧烈震荡,且验证集 Loss 呈现明显上升趋势,这表明模型对未学过的东西不能适应,出现过拟合现象。
  • 上周的BUG:

    • 由于model.train_on_batch(x, y) 是 对单个 batch 的数据执行一次前向传播、损失计算、反向传播和参数更新,而loss/accuracy是在循环结束后才记录的,这就导致每个epoch中的各个batch的记录会逐次被后一个batch的记录所覆盖,最终只记录了每个 epoch 的最后一个 batch 的值,而不是整个 epoch 的平均值。

    • 修正如下:

      • 通过累积每个 batch的指标,并在 epoch 结束时计算均值,确保记录的是整个训练集/验证集的平均 loss 与 accuracy。

        • 为每个 epoch 初始化空列表:
        train_loss = []
        train_accuracy = []
        val_loss = []
        val_accuracy = []
        
        • 将每个 batch 的结果追加到列表中:
        history = model.train_on_batch(image, label)
        train_loss.append(history[0])
        train_accuracy.append(history[1])
        
        • epoch 结束后取均值并存入全局历史记录:
        history_train_loss.append(np.mean(train_loss))
        history_train_accuracy.append(np.mean(train_accuracy))
        
from tqdm import tqdm
import tensorflow.keras.backend as K
import numpy as np

epochs = 30
lr = 1e-5

# 记录训练数据,方便后面的分析
history_train_loss = []
history_train_accuracy = []
history_val_loss = []
history_val_accuracy = []

for epoch in range(epochs):
    train_total = len(train_ds)
    val_total = len(val_ds)
    
    """
    total:预期的迭代数目
    ncols:控制进度条宽度
    mininterval:进度更新最小间隔,以秒为单位(默认值:0.1)
    """
    with tqdm(total = train_total, desc = f'Epoch {epoch + 1}/{epochs}',mininterval=1,ncols=100) as pbar:
        
        lr = lr*0.98
        K.set_value(model.optimizer.lr, lr)
        
        train_loss = []
        train_accuracy = []
        for image,label in train_ds:   
             # 这里生成的是每一个batch的acc与loss
            history = model.train_on_batch(image,label)
            
            train_loss.append(history[0])
            train_accuracy.append(history[1])
            
            pbar.set_postfix({"train_loss": "%.4f"%history[0],
                              "train_acc":"%.4f"%history[1],
                              "lr": K.get_value(model.optimizer.lr)})
            pbar.update(1)
            
        history_train_loss.append(np.mean(train_loss))
        history_train_accuracy.append(np.mean(train_accuracy))
            
    print('开始验证!')
    
    with tqdm(total=val_total, desc=f'Epoch {epoch + 1}/{epochs}',mininterval=0.3,ncols=100) as pbar:

        val_loss = []
        val_accuracy = []
        for image,label in val_ds:      
            # 这里生成的是每一个batch的acc与loss
            history = model.test_on_batch(image,label)
            
            val_loss.append(history[0])
            val_accuracy.append(history[1])
            
            pbar.set_postfix({"val_loss": "%.4f"%history[0],
                              "val_acc":"%.4f"%history[1]})
            pbar.update(1)
        history_val_loss.append(np.mean(val_loss))
        history_val_accuracy.append(np.mean(val_accuracy))
            
    print('结束验证!')
    print("验证loss为:%.4f"%np.mean(val_loss))
    print("验证准确率为:%.4f"%np.mean(val_accuracy))

在这里插入图片描述
在这里插入图片描述

模型评估

from datetime import datetime
current_time = datetime.now() # 获取当前时间

epochs_range = range(epochs)

plt.figure(figsize = (14, 4))
plt.subplot(1, 2, 1)

plt.plot(epochs_range, history_train_accuracy, label = 'Training Accuracy')
plt.plot(epochs_range, history_val_accuracy, 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, history_train_loss, label = 'Training Loss')
plt.plot(epochs_range, history_val_loss, label = 'Validation Loss')
plt.legend(loc = 'upper right')
plt.title('Training and Validation Loss')
plt.show()

在这里插入图片描述

预测

# 采用加载的模型(new_model)来看预测结果
plt.figure(figsize = (18, 3))  # 图形的宽为18高为5
plt.suptitle("预测结果展示")

for images, labels in val_ds.take(1):
    for i in range(8):
        ax = plt.subplot(1,8, i + 1)  
        
        # 显示图片
        plt.imshow(images[i].numpy())
        
        # 需要给图片增加一个维度
        img_array = tf.expand_dims(images[i], 0) 
        
        # 使用模型预测图片中的人物
        predictions = model.predict(img_array)
        plt.title(class_names[np.argmax(predictions)])

        plt.axis("off")

在这里插入图片描述

学习总结

  • 针对上周的bug: train_on_batch 返回的是当前这个 batch(一小撮图片)的 loss 和 accuracy。一个 epoch 包含 340 个 batches,每个 batch 的表现都忽高忽低。原代码像是在终点线只看最后一名选手的成绩,在循环结束后直接把最后一份数据存入 history。由于 train_loss 在循环中不断被新值“覆盖”,导致最终记录的仅仅是最后一个batch的数值,而非整场比赛的平均分。 指导文档给的解决方法是引入一个空列表,把每个 batch 的成绩都存进去,等一个 epoch 跑完后算一次平均分再记录。

  • 由于 VGG16 就像一个记忆力超强但容易死记硬背的学生。如果只给它看几千张固定的猫狗照片,它会通过“背诵”图片编号来作弊,导致遇到新照片(验证集)就抓瞎。因此进行了数据增强,引入 RandomFlip(翻转)和 RandomRotation(旋转),相当于在给学生看题时,故意把书倒着拿或者照镜子看。这样能让模型不能只记像素位置,而是得去观察“猫耳朵尖尖的”或者“狗鼻子黑黑的”等一些通用特征,从而达到不错的训练效果。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐