为何冻结全层的ResNet50在Fashion MNIST上表现极差?求排查
首先可以明确:ResNet50并非完全不适用于Fashion MNIST,但你的当前操作存在几个关键问题,导致模型性能拉胯。咱们一步步拆解问题,再给出解决办法:
核心问题拆解
1. 输入数据与预训练特征的不匹配
Fashion MNIST是28x28的单通道灰度图,你把它转成32x32x3的RGB图,大概率是直接将单通道复制成三通道吧?但ResNet50的预训练权重是基于ImageNet的真实RGB图像训练的,这种“伪RGB”图的特征分布和预训练数据差异极大——冻结所有层时,模型只能用预训练好的特征提取器,而这些特征根本不适合灰度服饰图像,自然提取不到有效信息。
2. 完全冻结所有层的策略错误
ResNet50的底层特征(比如边缘、纹理)确实有一定通用性,但高层特征是专门针对ImageNet的物体分类(比如猫狗、汽车)优化的,和Fashion MNIST的服饰类(T恤、鞋子)差异巨大。完全冻结所有层意味着模型无法调整高层特征来适配新任务,只能靠顶层的小网络硬凑,泛化能力必然很差,这就是你测试准确率只有41%的核心原因。
3. 顶层结构的小问题
你用Flatten()直接展开ResNet50的输出,对于32x32的输入来说,ResNet50的输出是(1,1,2048)的张量,Flatten后得到2048维向量,直接接128维Dense层虽然没问题,但缺少Dropout等正则化手段,容易出现过拟合(训练准确率78%不算高,但测试差这么多也有泛化不足的问题)。
针对性解决方案
方案1:优化输入预处理,适配单通道输入
不要强行转成三通道,而是让ResNet50支持单通道输入,这样更贴合原始数据的特点:
- 修改ResNet50的第一层卷积层,把输入通道从3改成1,同时复用原预训练权重中的单通道参数(避免随机初始化)。
方案2:放弃完全冻结,采用分阶段训练
先训练顶层网络,让模型先适配Fashion MNIST的分类任务,再解冻部分高层ResNet层进行微调,让特征提取器适配新数据:
- 先冻结所有ResNet层,训练顶层的Dense层;
- 再解冻ResNet的后几层(比如前100层冻结,后面解冻),用更小的学习率微调整个模型。
方案3:调整顶层结构,增加正则化
用GlobalAveragePooling2D()代替Flatten(),减少参数数量,同时添加Dropout层防止过拟合。
修改后的代码示例
from keras import optimizers from keras.applications.resnet50 import ResNet50 from keras.datasets import fashion_mnist from keras.layers import Activation, GlobalAveragePooling2D, Dense, Dropout, Conv2D from keras.models import Model from keras.utils import to_categorical from sklearn.model_selection import train_test_split import cv2 import numpy as np # 加载并拆分数据 (x, y), (x_test, y_test) = fashion_mnist.load_data() dat_train, dat_val, train_lbs, val_lbs = train_test_split(x, y, test_size=10000, random_state=42) # 预处理:缩放至32x32,添加单通道,归一化到[0,1] def preprocess_data(data): resized = np.array([cv2.resize(img, (32, 32)) for img in data]) return np.expand_dims(resized, axis=-1) / 255.0 dat_train = preprocess_data(dat_train) dat_val = preprocess_data(dat_val) x_test = preprocess_data(x_test) # 独热编码标签 train_lbs = to_categorical(train_lbs, 10) val_lbs = to_categorical(val_lbs, 10) y_test = to_categorical(y_test, 10) # 加载ResNet50并修改第一层为单通道输入 resnet50_base = ResNet50(include_top=False, weights='imagenet', input_shape=(32, 32, 3)) # 替换第一层卷积层 original_conv1 = resnet50_base.layers[0] new_conv1 = Conv2D(64, (7,7), strides=(2,2), padding='same', input_shape=(32,32,1), name='conv1') # 复用原权重的第一个通道参数 new_conv1.set_weights([original_conv1.get_weights()[0][:,:,:1,:], original_conv1.get_weights()[1]]) # 重新构建基础模型 resnet50_base = Model(inputs=new_conv1.input, outputs=resnet50_base.layers[-1].output) # -------------------------- 第一阶段:训练顶层 -------------------------- # 冻结所有ResNet层 for layer in resnet50_base.layers: layer.trainable = False # 构建顶层结构 base_out = resnet50_base.output base_out = GlobalAveragePooling2D()(base_out) base_out = Dense(256, activation='relu')(base_out) base_out = Dropout(0.5)(base_out) preds = Dense(10, activation="softmax")(base_out) model = Model(inputs=resnet50_base.input, outputs=preds) model.compile(loss="categorical_crossentropy", optimizer=optimizers.Adam(lr=0.001), metrics=["accuracy"]) # 训练顶层 model.fit(dat_train, train_lbs, batch_size=64, epochs=5, validation_data=(dat_val, val_lbs)) # -------------------------- 第二阶段:微调高层 -------------------------- # 解冻ResNet的后40层(可根据实际情况调整) for layer in resnet50_base.layers[-40:]: layer.trainable = True # 用更小的学习率微调,避免破坏预训练特征 model.compile(loss="categorical_crossentropy", optimizer=optimizers.Adam(lr=0.0001), metrics=["accuracy"]) model.fit(dat_train, train_lbs, batch_size=64, epochs=10, validation_data=(dat_val, val_lbs)) # 评估测试集 test_loss, test_acc = model.evaluate(x_test, y_test) print(f"测试准确率: {test_acc:.4f}")
效果预期
按照上述方案调整后,测试准确率应该能提升到90%以上,和专门针对小图像设计的模型(比如CNN)差距不大——ResNet50的特征提取能力还是能发挥作用的,只是需要适配任务特点。
内容的提问来源于stack exchange,提问作者Nemo

