返回

解决二分类图像识别中偶数轮训练精度损失为0的问题

Ai

二分类图像识别,偶数轮训练效果异常,怎么办?

最近用 CNN 做图像分类,碰到个怪事:训练轮数(epochs)设成偶数时,模型表现特别差,accuracy 和 loss 直接变成 0。 这是咋回事?别急,咱来捋一捋。

一、 问题原因分析

偶数轮训练出问题,看着挺玄乎,其实原因可能没那么复杂。排查下来,大概率是这几个方面导致的:

  1. 数据处理流程有 Bug :仔细看看flow_from_directory,特别是跟数据增强和 batch size 相关的部分。

  2. 数据迭代器“耗尽” :当训练的步数 (steps_per_epoch) 与训练集大小不匹配时,数据迭代器可能会在偶数轮次“耗尽”,导致模型没数据可学。

  3. 学习率与 Batch Size 不匹配 : 特别是用自定义的训练循环时候,如果batch_size和学习率搭配不好,会导致模型根本不收敛。

  4. 梯度消失/爆炸 : 在特定情况下,深层网络或者特殊激活函数(如sigmoid), 容易导致梯度消失,特别注意网络的初始化。

二、 解决方案

针对上面分析的原因,咱们逐个击破:

1. 检查并修正数据处理流程

  • 问题 : 数据生成器 (ImageDataGeneratorflow_from_directory) 的配置可能存在问题, 尤其是在数据增强、打乱顺序或者批量大小方面。

  • 原理 : 数据生成器负责从硬盘读取图像, 并进行预处理(比如缩放、旋转等)。如果配置不当,会导致模型无法正确读取和学习图像特征。

  • 代码示例/操作步骤 :

    from tensorflow.keras.preprocessing.image import ImageDataGenerator
    
    # 建议: 明确指定 batch_size, 并确保其与 steps_per_epoch 相匹配。
    train_datagen = ImageDataGenerator(
        rescale=1./255,
        # 其他增强参数, 如旋转、平移、剪切等 (按需添加)
    )
    valid_datagen = ImageDataGenerator(rescale=1./255)
    
    train_dir = "/root/.cache/kagglehub/datasets/ryanholbrook/car-or-truck/versions/1/train/"
    test_dir = "/root/.cache/kagglehub/datasets/ryanholbrook/car-or-truck/versions/1/valid/"
    
    # 确保 batch_size 与 steps_per_epoch 匹配.
    batch_size = 32
    train_data = train_datagen.flow_from_directory(
        directory=train_dir,
        batch_size=batch_size,
        target_size=(224, 224),
        class_mode="binary",
        seed=42,
        shuffle=True  # 训练集通常需要打乱
    )
    valid_data = valid_datagen.flow_from_directory(
        directory=test_dir,
        batch_size=batch_size,
        target_size=(224, 224),
        class_mode="binary",
        seed=42,
        shuffle=False  # 验证集通常不需要打乱
    )
    
    

    shuffle=True 加到 valid_data 里可能不是好做法,一般验证集不打乱。

  • 额外的安全建议 :

    • 打印部分增强后的训练数据,确保图像的变换没有问题。

2. 处理数据迭代器耗尽问题

  • 问题 : 训练步数 (steps_per_epoch) 和 epoch 数量设置不当, 数据迭代器可能会提前耗尽,导致后面的训练没有数据可用。从日志可以看到在epoch2,4,6等地方出现了accuracy: 0.0000e+00 - loss: 0.0000e+00 的输出。

  • 原理fit 函数默认在每个 epoch 结束时重置数据迭代器。但如果 steps_per_epoch * epochs 大于数据集的实际大小, 迭代器会在中间就耗尽数据,后面的epochs就取不到任何数据了。

  • 代码示例/操作步骤 :

    # 方法一 (推荐):使用 repeat() 无限循环数据
    # 让训练数据无限循环, 这样就不会耗尽了.
    train_data = train_data.repeat()
    
    # fit 时的 steps_per_epoch 可以设置为一个比较大的数,
    # 比如一个 epoch 内你希望更新多少次参数.
    
    # 注意: 因为 train_data 无限循环了, 所以你必须手动设置 steps_per_epoch.
    #       否则 fit 函数会永远跑下去。
     # 一个比较合适的值可以这么算: 数据集大小 // batch_size.
    steps_per_epoch =  5117 // batch_size
    history_1 = model_1.fit(
          train_data,
          epochs=25,
          steps_per_epoch=steps_per_epoch, # 可以考虑取 5117//32,
          validation_data=valid_data,
          validation_steps=len(valid_data) # 这个没问题,不用改.
          )
    
    
    # 方法二: 动态计算 steps_per_epoch (不太推荐, 除非你有特殊需求)
    
    # 在每个 epoch 开始前, 重新计算 steps_per_epoch.
    # 这种方式比较麻烦,一般用 repeat() 就够了。
    
    # class MyCallback(tf.keras.callbacks.Callback):
    #     def on_epoch_begin(self, epoch, logs=None):
    #         # 假设你的数据加载器叫 train_data
    #         # 并且有一个方法可以获取当前还能产生的 batch 数.
    #          remaining_batches = train_data.get_remaining_batches()
    #         self.model.steps_per_epoch = remaining_batches
    #         print(f"Epoch {epoch}: steps_per_epoch set to {remaining_batches}")
    
    # history_1 = model_1.fit(train_data, epochs=25, steps_per_epoch=len(train_data),
    #                 validation_data=valid_data, validation_steps=len(valid_data),
    #                  callbacks=[MyCallback()])
    
    
  • 额外的安全建议

    • 如果你非常关心训练集和验证集的划分, 不希望训练时看到重复的样本, 那么你可以考虑自定义训练循环,这样可以完全控制每个 epoch 用哪些数据。但这比较复杂。

3. 调整学习率与 Batch Size

  • 问题描述: 学习率和 batch size 搭配不好, 尤其是使用 Adam 优化器时, 可能导致训练不稳定。

  • 原理:

    • 学习率 (learning rate) : 控制模型每次更新参数的步长.
    • Batch Size : 每次更新参数使用的样本数量.
    • 如果学习率过大,模型可能会在最优解附近“震荡”,难以收敛.
    • 如果 Batch Size 过小,每次更新的梯度估计可能不够准确,导致训练不稳定; Batch Size 太大, 又可能陷入局部最优.
  • 代码示例/操作步骤 :

    # 尝试更小的学习率
    #  Adam 的默认学习率 (0.001) 有时候可能偏大.
    
    # 方法一: 直接改 learning_rate
    optimizer = tf.keras.optimizers.Adam(learning_rate=0.0001)
    
    #方法二(推荐): 用 LearningRateScheduler, 让学习率在训练过程中逐渐减小
    # def scheduler(epoch, lr):
    #   if epoch < 10:
    #     return lr
    #   else:
    #     return lr * tf.math.exp(-0.1)
    #
    # callback = tf.keras.callbacks.LearningRateScheduler(scheduler)
    
    model_1.compile(loss="binary_crossentropy", optimizer=optimizer, metrics=["accuracy"])
    
    # 可以试试不同的 batch size, 比如 16, 64.  不要太大, 比如 128, 256...
    # (除非你的显存特别大)
    #  改 batch size 的话,记得要重新创建 train_data 和 valid_data。
     #batch_size = 16 或 64, 选一个试试
    
  • 进阶技巧:

    • 可以使用TensorBoard 来观察训练过程中的学习率变化、损失函数曲线等。
        tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="./logs") # 日志保存目录
        history_1 = model_1.fit(..., callbacks=[tensorboard_callback])
        # 训练完成后, 在命令行运行 tensorboard --logdir ./logs,
    

4. 网络初始化和梯度消失

  • 问题描述: 对于相对深的网络或者使用sigmoid这类激活函数,网络权重的初始化方式很重要,如果初始化不当可能导致梯度消失问题。
  • 原理 :
    • 不好的权重初始化会让信号在传递的过程中变得太大或太小。 如果信号太小, 梯度就会接近于 0 (梯度消失); 如果信号太大, 梯度可能非常大 (梯度爆炸)。 这两种情况都会让模型难以训练。
  • 代码示例和步骤:
    model_1 = tf.keras.models.Sequential([
      tf.keras.layers.Conv2D(filters=10, kernel_size=3, input_shape=(224,224,3), activation="relu", kernel_initializer='he_normal'),
      tf.keras.layers.Conv2D(10,3, activation="relu", kernel_initializer='he_normal'),
      tf.keras.layers.MaxPool2D(pool_size=2, padding="valid"),
      tf.keras.layers.Conv2D(10,3, activation="relu", kernel_initializer='he_normal'),
      tf.keras.layers.Conv2D(10,3, activation="relu", kernel_initializer='he_normal'),
      tf.keras.layers.MaxPool2D(pool_size=2, padding="valid"),
      tf.keras.layers.Flatten(),
      tf.keras.layers.Dense(1, activation="sigmoid")
    

])

*   针对卷积层: 可以使用 `kernel_initializer='he_normal'` (如果激活函数是 ReLU 或其变体), 或者 `kernel_initializer='glorot_normal'` (也叫 Xavier 初始化, 如果激活函数是 sigmoid 或 tanh)。 对于Dense层同样有效。
* 进阶:
  *   **Batch Normalization** : 在卷积层后、激活函数前加 Batch Normalization 层, 能有效缓解梯度消失/爆炸, 加速训练。

通过以上排查和调整,大概率能解决你遇到的问题。