乳腺X光图像二分类:训练与验证准确率均停滞在50%
核心问题分析与修复
1. 输出层连接错误
你的代码中,最终输出层直接连接到了Flatten后的特征x,完全跳过了中间定义的x_class全连接层和Dropout层,这导致模型分类能力被严重削弱,相当于只用了一个简单线性分类器,无法有效学习复杂特征。
修正:
将输出层改为从x_class连接:
output = Dense(1, activation='sigmoid')(x_class) # 原代码是(x),改为(x_class)
2. 损失函数参数不匹配
你使用了sigmoid激活函数,但损失函数设置了from_logits=True,这会导致损失计算逻辑错误——from_logits=True是针对未经过激活函数的原始输出设计的,而sigmoid已经将输出压缩到0-1区间,两者冲突会导致模型无法正确优化。
修正:
将损失函数的from_logits改为False:
model.compile( loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), # 修改为False optimizer=optimizer, metrics=["accuracy"], )
3. 输入图像未归一化
EfficientNetV2的预训练权重是基于归一化到0-1区间的图像训练的,但你的数据生成器注释掉了rescale=1./255,导致输入图像还是0-255的原始像素值,这会让预训练模型的特征提取模块无法正常工作,输出的特征毫无区分度。
修正:
在训练和验证数据生成器中启用归一化:
train_datagen = ImageDataGenerator( rescale=1./255, # 取消注释 rotation_range=20, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.2, horizontal_flip=True, vertical_flip=False, fill_mode='nearest' ) valid_datagen = ImageDataGenerator( rescale=1./255 # 仅做归一化,不做数据增强 )
4. 验证集不应使用数据增强
验证集的作用是评估模型的泛化能力,使用数据增强会导致验证数据分布与真实测试场景不符,同时增加不必要的计算开销。
修正:
验证集生成器仅保留归一化操作,移除所有数据增强参数(旋转、平移、缩放等)。
5. 学习率过低
1e-6的学习率太小,模型参数几乎无法更新,导致训练停滞。对于预训练模型的微调,初始学习率建议设置为1e-4或1e-5,后续可根据训练情况调整。
修正:
调整优化器的学习率:
optimizer = Adam(learning_rate=1e-4, beta_1=0.9, beta_2=0.999)
6. 预训练模型的训练策略优化
默认情况下,EfficientNetV2的所有层都是可训练的,直接训练可能导致梯度不稳定或预训练特征被破坏。建议先冻结底层特征提取层,仅训练顶层分类器,再解冻部分层进行微调。
示例:
# 冻结预训练模型的所有层 efficientnet_model.trainable = False # 先训练顶层分类器 history = model.fit(train_generator, epochs=10, validation_data=valid_generator) # 解冻最后10层进行微调 efficientnet_model.trainable = True fine_tune_at = len(efficientnet_model.layers) - 10 for layer in efficientnet_model.layers[:fine_tune_at]: layer.trainable = False # 微调时降低学习率 optimizer = Adam(learning_rate=1e-5) model.compile( loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer=optimizer, metrics=["accuracy"], ) history_fine = model.fit(train_generator, epochs=20, initial_epoch=history.epoch[-1], validation_data=valid_generator)
7. 训练轮数不足
10轮训练对于预训练模型的微调来说太少,尤其是在初始学习率较低的情况下,模型还未进入有效收敛阶段。建议先训练10-20轮顶层,再微调20-30轮。
完整修正后的代码
import tensorflow as tf from tensorflow import keras from tensorflow.keras.layers import Input, Flatten, Dense, Dropout from tensorflow.keras.models import Model from tensorflow.keras.optimizers import Adam # Loading the Data train_df = new_df.sample(frac=0.7, random_state=0) # 70% of the data for training valid_df = new_df.drop(train_df.index) # remaining 30% for validation and testing batch_size = 32 # Create a data generator for training data train_datagen = ImageDataGenerator( rescale=1./255, rotation_range=20, width_shift_range=0.1, height_shift_range=0.1, zoom_range=0.2, horizontal_flip=True, vertical_flip=False, # 乳腺X光通常不使用垂直翻转 fill_mode='nearest' ) train_generator = train_datagen.flow_from_dataframe( dataframe=train_df, x_col='images_filepath', y_col='labels', target_size=(224, 224), batch_size=batch_size, shuffle=True, class_mode='binary' ) # Create a data generator for validation data valid_datagen = ImageDataGenerator( rescale=1./255 # 仅做归一化,不做数据增强 ) valid_generator = valid_datagen.flow_from_dataframe( dataframe=valid_df, x_col='images_filepath', y_col='labels', target_size=(224, 224), batch_size=batch_size, shuffle=False, # 验证集不需要打乱 class_mode='binary' ) # Build the model efficientnet_model = keras.applications.EfficientNetV2S( include_top=False, weights="imagenet", input_tensor=Input(shape=(224, 224, 3)) ) # 先冻结预训练层 efficientnet_model.trainable = False x = Flatten()(efficientnet_model.output) x_class = Dense(256, activation='relu')(x) x_class = Dropout(0.5)(x_class) output = Dense(1, activation='sigmoid')(x_class) model = Model(inputs=efficientnet_model.input, outputs=output) # 编译并训练顶层分类器 optimizer = Adam(learning_rate=1e-4) model.compile( loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer=optimizer, metrics=["accuracy"], ) history = model.fit(train_generator, epochs=10, validation_data=valid_generator) # 解冻部分层进行微调 efficientnet_model.trainable = True fine_tune_at = len(efficientnet_model.layers) - 10 for layer in efficientnet_model.layers[:fine_tune_at]: layer.trainable = False # 微调时使用更小的学习率重新编译 optimizer = Adam(learning_rate=1e-5) model.compile( loss=tf.keras.losses.BinaryCrossentropy(from_logits=False), optimizer=optimizer, metrics=["accuracy"], ) # 继续训练 history_fine = model.fit( train_generator, epochs=30, initial_epoch=history.epoch[-1], validation_data=valid_generator )
内容的提问来源于stack exchange,提问作者Harshkumar Mahesh

