使用Data Generator类训练CNN模型时准确率低下的问题排查
我有两个模型架构:模型1使用自定义Data Generator类拟合数据,模型2直接使用训练集和测试集的numpy数组拟合数据。模型1的准确率极低,而模型2的准确率表现良好。需要排查Data Generator类导致的问题。模型2的数据增强层包含将图像resize至(160,160,3)的操作。
代码实现
自定义Data Generator类
class DataGenerator(keras.utils.Sequence): def __init__(self, images, labels=None, mode='fit', batch_size=64, dim=(160, 160), channels=3, n_classes=10, shuffle=True, augment=False): # 初始化生成器配置 self.images = images self.labels = labels self.mode = mode self.batch_size = batch_size self.dim = dim self.channels = channels self.n_classes = n_classes self.shuffle = shuffle self.augment = augment self.on_epoch_end() if augment: self.augmenter=albu.Compose([ albu.HorizontalFlip(0.5), albu.VerticalFlip(0.5), albu.RandomRotate90(0.2), albu.ElasticTransform(p=0.2), albu.Resize(height=dim[0], width=dim[1]) ]) else: self.augmenter=albu.Resize(height=dim[0],width=dim[1]) # 每个epoch结束后调用的方法 def on_epoch_end(self): self.indexes = np.arange(self.images.shape[0]) if self.shuffle == True: np.random.shuffle(self.indexes) # 返回一个epoch中的步数(即batch数量) def __len__(self): return int(np.floor(len(self.images) / self.batch_size)) # 根据batch索引获取对应批次的数据 def __getitem__(self, index): # 生成一个batch的索引 batch_indexes = self.indexes[index * self.batch_size:(index+1) * self.batch_size] # 生成X的mini-batch X = np.empty((self.batch_size, *self.dim, self.channels)) for i, ID in enumerate(batch_indexes): img = self.images[ID] # 图像归一化到0-1 img = img.astype(np.float32)/255.0 # 训练集数据增强 img = self.augmenter(image=img)['image'] X[i] = img # 生成y的mini-batch if self.mode == 'fit': y = self.labels[batch_indexes] return X, y elif self.mode == 'predict': return X else: raise AttributeError("The mode should be set to either 'fit' or 'predict'.")
模型1(使用Data Generator)
def model_1(): base_model = tf.keras.applications.MobileNetV2(input_shape=(160,160,3), include_top=False, weights='imagenet') base_model.trainable=False # 冻结预训练模型 inputs=tf.keras.Input(shape=((160,160,3))) x=preprocess_input(inputs) x=base_model(x,training=False)# 保持BN层处于推理模式 x=layers.GlobalAveragePooling2D()(x) x=layers.Dropout(0.5)(x) outputs=layers.Dense(units=10,activation='softmax')(x) model=Model(inputs,outputs) return model model_1=model_1() history1=model_1.fit(train_data_generator,validation_data=valid_data_generator,epochs=20)
模型2(直接使用numpy数组)
def model_2(): base_model = tf.keras.applications.MobileNetV2(input_shape=(160,160,3), include_top=False, weights='imagenet') base_model.trainable=False # 冻结预训练模型 inputs=tf.keras.Input(shape=((32,32,3))) x=data_augmentation(inputs) x=preprocess_input(x) x=base_model(x,training=False)# 保持BN层处于推理模式 x=layers.GlobalAveragePooling2D()(x) x=layers.Dropout(0.5)(x) outputs=layers.Dense(units=10,activation='softmax')(x) model=Model(inputs,outputs) return model model2=model_2() history2=model2.fit(X_train,y_train,batch_size=64,epochs=5,validation_data=(X_test,y_test))
问题排查关键点
预处理顺序错误(核心问题):
MobileNetV2的preprocess_input要求输入是0-255范围的RGB图像,会将其转换为[-1,1]的归一化范围。但Generator中先执行了img = img.astype(np.float32)/255.0,把图像提前归一化到0-1,再做增强,最后模型的preprocess_input处理的是不符合要求的数据,完全打乱了预训练模型的输入分布。而模型2的流程是原始图像(0-255)→ 数据增强→preprocess_input,顺序完全正确。标签格式不匹配:
检查train_data_generator传入的labels格式:如果模型用categorical_crossentropy损失函数,标签需要是one-hot编码;如果是sparse_categorical_crossentropy,标签可以是整数。若模型2的y_train是one-hot格式,而Generator返回的是原始整数标签,会导致损失计算错误,直接拉低准确率。训练样本丢失:
Generator的__len__用np.floor计算步数,会丢弃最后一个不足batch_size的样本。虽然不会直接导致准确率极低,但会减少训练数据量,影响模型收敛。输入尺寸匹配问题:
模型2输入是(32,32,3),通过data_augmentationresize到160x160。需确认Generator接收的images原始尺寸是否为32x32,若尺寸不匹配,resize后的图像会失真,影响模型识别。
修复建议
调整预处理顺序:去掉Generator中的提前归一化,让模型的
preprocess_input处理原始0-255图像:def __getitem__(self, index): batch_indexes = self.indexes[index * self.batch_size:(index+1) * self.batch_size] actual_batch_size = len(batch_indexes) X = np.empty((actual_batch_size, *self.dim, self.channels)) for i, ID in enumerate(batch_indexes): img = self.images[ID] # 先做增强,再交给模型预处理 img = self.augmenter(image=img)['image'] X[i] = img if self.mode == 'fit': y = self.labels[batch_indexes] return X, y统一标签格式:若模型用
categorical_crossentropy,在Generator中把标签转为one-hot:y = tf.keras.utils.to_categorical(self.labels[batch_indexes], self.n_classes)修正步数计算:避免丢失最后一批样本:
def __len__(self): return int(np.ceil(len(self.images) / self.batch_size))
内容的提问来源于stack exchange,提问作者Gourav Sen

