You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

MNIST分类CNN严重过拟合求助:常规抑过拟合手段无效

分析你的MNIST CNN过拟合问题及解决方案

看起来你遇到了挺头疼的过拟合问题,而且数据增强的表现还和预期相反——别担心,我们一步步拆解问题,找到根源:

一、为什么你的数据增强起反作用?

你用的随机90度旋转其实对MNIST任务来说是有害的噪声:MNIST里的手写数字都是正向的,旋转90/180/270度后,数字会变成模型从未见过的“异常样本”(比如旋转90度的2根本不像2),模型花了大量精力去学习这些无意义的特征,自然会影响正常任务的性能。

你应该换成适合MNIST的轻微数据增强,比如:

  • 小角度旋转(±15度以内)
  • 轻微平移(上下左右各1-2像素)
  • 随机缩放(0.9-1.1倍)
  • 轻微剪切变形

这样的增强才是在模拟真实手写数字的自然变化,而不是制造完全陌生的样本。

二、模型结构的核心问题

你的ConvBlock设计有几个明显的不合理之处,这是导致过拟合的关键:

1. 每个Conv层后直接加MaxPool,特征丢失过快

输入是28x28的图像,你的第一个ConvBlock设置了conv_deep=2,意味着每个Conv后都要做一次Pool:

  • 第一次Conv+Pool:28x28 → 14x14
  • 第二次Conv+Pool:14x14 →7x7
    再加上第二个ConvBlock的Pool,最终特征图缩小到3x3,这会丢失大量细粒度的手写特征,模型只能靠记住训练样本拟合,自然过拟合。

正确的做法是:多个Conv层共享一个Pool层,比如2个Conv后再做一次Pool,让模型充分提取特征后再缩小尺寸。

2. BatchNormalization和Dropout的顺序错误

你当前的层顺序是:Conv → Pool → BN → Dropout,这不符合最佳实践。正确的顺序应该是:
Conv2D → BatchNormalization → Activation → MaxPooling → Dropout
(现在主流是把BN放在激活函数之前,能避免激活函数导致的梯度消失)

另外,你手动在call里判断training来应用Dropout是多余的——Keras的Dropout层会自动根据训练/推理模式切换,不需要手动处理。

3. 优化器学习率过高

你用了Adadelta(7.0),Adadelta的默认学习率是1.0,7.0的学习率太大了,会导致模型训练不稳定,参数震荡,无法收敛到泛化性好的点,反而容易记住训练数据的噪声。

三、具体修改方案

1. 重构ConvBlock结构

class ConvBlock(tf.keras.layers.Layer):
    """修正后的卷积块:多Conv共享一个Pool,调整BN/Dropout顺序"""
    def __init__(self, conv_deep=1, kernels=32, kernel_size=3, pool_size=2, dropout_rate=0.4):
        super(ConvBlock, self).__init__()
        self.conv_layers = []
        self.bnorm_layers = []
        self.pooling_layer = tf.keras.layers.MaxPool2D(pool_size=pool_size)
        self.dropout_layer = tf.keras.layers.Dropout(dropout_rate)
        
        for _ in range(conv_deep):
            self.conv_layers.append(tf.keras.layers.Conv2D(filters=kernels, 
                                                           kernel_size=kernel_size, 
                                                           padding="same"))
            self.bnorm_layers.append(tf.keras.layers.BatchNormalization())

    def call(self, inputs, training=False):
        output = inputs
        for conv, bnorm in zip(self.conv_layers, self.bnorm_layers):
            output = conv(output)
            output = bnorm(output, training=training)
            output = tf.keras.activations.relu(output)
        output = self.pooling_layer(output)
        output = self.dropout_layer(output, training=training)
        return output

2. 修正数据增强函数

def transform_sample(digit, label):
    # 小角度旋转(±15度)
    angle = tf.random.uniform([], -15, 15, dtype=tf.float32)
    t_digit = tf.keras.layers.experimental.preprocessing.rotation(digit, angle)
    # 轻微平移(上下左右最多2像素)
    t_digit = tf.keras.layers.experimental.preprocessing.random_translation(t_digit, 0.07, 0.07)
    # 随机缩放(0.9-1.1倍)
    t_digit = tf.keras.layers.experimental.preprocessing.random_zoom(t_digit, (0.9, 1.1))
    return t_digit, label

3. 调整模型结构与优化器

class DigitsClassifier(tf.keras.Model):
    """优化后的分类器"""
    def __init__(self):
        super(DigitsClassifier, self).__init__()
        # 2个Conv层后再Pool,避免特征过早丢失
        self.conv_input = ConvBlock(conv_deep=2, kernels=32)
        self.conv_hiden = ConvBlock(conv_deep=2, kernels=64)  # 增加Conv深度,减少全连接参数
        # 用GlobalAveragePooling代替Flatten,大幅减少参数,避免过拟合
        self.global_pool = tf.keras.layers.GlobalAveragePooling2D()
        self.softmax = tf.keras.layers.Dense(10, "softmax")

    def call(self, inputs, training=False):
        output = self.conv_input(inputs, training=training)
        output = self.conv_hiden(output, training=training)
        output = self.global_pool(output)
        output = self.softmax(output)
        return output

# 优化器换成Adam,学习率用默认的0.001,比Adadelta更稳定
model = DigitsClassifier()
model.compile(tf.keras.optimizers.Adam(), 
              tf.keras.losses.SparseCategoricalCrossentropy(),
              metrics=["accuracy"])  # 加上准确率指标,方便监控
model.fit(train_ds, epochs=30, verbose=2, validation_data=eval_ds)

四、额外的抑过拟合建议

  • 减少全连接层的参数:用GlobalAveragePooling2D代替Flatten是非常有效的方式,能把参数从几万降到几十
  • 调整Dropout率:可以尝试把Dropout率调到0.2-0.3,过高的Dropout会让模型学习不到足够的特征
  • 早停法:加入EarlyStopping回调,当验证集损失不再下降时自动停止训练,避免过度训练
    early_stop = tf.keras.callbacks.EarlyStopping(patience=5, restore_best_weights=True)
    model.fit(..., callbacks=[early_stop])
    
  • 权重衰减:在优化器中加入L2正则化,限制参数的大小
    model.compile(tf.keras.optimizers.Adam(weight_decay=1e-4), ...)
    

内容的提问来源于stack exchange,提问作者ElPapi42

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 06:50:51