使用NPY文件时Keras ImageDataGenerator与自定义DataGenerator结果差异问题
我目前正在学习神经网络,尝试对RVL-CDIP公开数据集进行分类,使用适配16个类别的InceptionResNetV2模型。
使用Keras的ImageDataGenerator类构建训练集和验证集训练时,获得了约80%的不错效果。
现在我尝试使用.npy文件训练模型:我将约3万张图片导出为尺寸为(299,299)的np数组,基于Sequences构建了自定义DataGenerator。训练结果和第一次训练相近,但验证集准确率仅约1%。
看起来像是出现了过拟合,但存在以下矛盾点:
- 图像训练和.npy训练使用的是完全相同的数据集
- 我已检查过图像转.npy的导出过程,将部分数组还原为图片展示没有问题
我发现的主要差异是训练集和验证集的构建方式:第一次训练我使用flow_from_directory()方法,第二次使用的是包含数据集每个元素路径和类别信息的pandas.DataFrame。
相关代码
生成器构建代码
def get_custom_data_generator(df_dataset, batch_size, df_train=pd.DataFrame(), df_validation=pd.DataFrame()): global STEPS_PER_EPOCH_TRAIN global STEPS_PER_EPOCH_VALIDATION if len(df_dataset) > 0: config.class_names = df_dataset['CLASS'].unique() num_classes = len(config.class_names) # Shuffling the dataset : df_dataset = df_dataset.sample(frac=1) split_row = int(df_dataset.shape[0] * config.dataset_creation.split.train) df_splits = np.split(df_dataset, [split_row], axis=0) df_train = df_splits[0] df_validation = df_splits[1] STEPS_PER_EPOCH_TRAIN = df_train.shape[0] // batch_size STEPS_PER_EPOCH_VALIDATION = df_validation.shape[0] // batch_size if len(df_train) > 0 and len(df_validation) > 0: train_set_generator = DataGenerator(df_train, x_col = ['PATH'], y_col = ['CLASS'], batch_size = batch_size, num_classes = num_classes) validation_set_generator = DataGenerator(df_validation, x_col = ['PATH'], y_col = ['CLASS'], batch_size = batch_size, num_classes = num_classes) return train_set_generator, validation_set_generator return None, None
自定义DataGenerator类代码
class DataGenerator(tf.keras.utils.Sequence): def __init__(self, df, x_col, y_col=None, batch_size=32, num_classes=None, shuffle=True, width=299, height = 299): self.batch_size = batch_size self.df = df self.indices = self.df.index.tolist() self.num_classes = num_classes self.shuffle = shuffle self.x_col = x_col self.y_col = y_col self.width = width self.height = height self.dim = (width,height) self.n_channels = 1 self.map_classes = map_classes(df['CLASS'].unique()) self.on_epoch_end() def __len__(self): return len(self.indices) // (self.batch_size) def __getitem__(self, index): index = self.index[index * self.batch_size:(index + 1) * self.batch_size] batch = [self.indices[k] for k in index] X, y = self.__data_generation_npy(batch) return X, y def on_epoch_end(self): self.index = np.arange(len(self.indices)) if self.shuffle == True: np.random.shuffle(self.index) def __data_generation_npy(self, batch): ''' Create a batch by using the CurrentNPY directory instead of the Current directory ''' npydataset_path = "D:/dataset/RVL/NPY" df = self.df X = np.empty((self.batch_size, *self.dim, 3)) y = np.empty(self.batch_size, dtype=int) for i, id in enumerate(batch): doc_row = df.loc[id] path = str(doc_row['PATH']) path = os.path.join(npydataset_path, path) typologie = str(doc_row['CLASS']) path += ".npy" with open(path, 'rb') as f: npyfile = np.load(path, allow_pickle=True) try: npyfile = np.expand_dims(npyfile, axis=3) X[i,]=npyfile[0] except ValueError: print('Value Error : npyfile=', npyfile.shape) y[i] = self.map_classes[typologie] X = np.array(X) obj_to_file(X) Y = np.array(y) Y = tf.keras.utils.to_categorical(Y, num_classes = self.num_classes) return X, Y
最核心的问题:训练、验证集的类别映射不统一
你在初始化DataGenerator时,分别给训练集和验证集生成器传入了各自的df,且两个生成器内部都独立调用了map_classes(df['CLASS'].unique())生成类别到索引的映射。而pandas的unique()方法返回值的顺序不固定,这会直接导致训练集和验证集的标签映射规则完全不同:训练时模型学到的是训练集的标签对应关系,验证时模型输出的索引对应到完全不同的类别,自然准确率接近随机猜测的水平,和你遇到的1%左右准确率的现象完全吻合。
修复方法:在拆分数据集之前就基于全量数据集的类别生成全局统一的类别映射字典,之后将这个字典作为参数传入训练、验证两个DataGenerator,不要让两个生成器各自生成映射。
其他需要排查的点
- 输入数据归一化逻辑是否对齐:你之前用ImageDataGenerator训练时大概率做了像素值归一化(比如缩放到01或者-11区间),但自定义生成器读取npy文件时没有做对应的归一化操作,InceptionResNetV2对输入数值范围非常敏感,也可能导致训练异常。
- 检查
obj_to_file(X)函数是否修改了输入数组X的数值,导致输入不符合模型要求。
可快速定位问题的测试步骤
- 分别从训练生成器和验证生成器中取出同一个样本对应的数据和标签,确认同一张图的标签在两个生成器中是否一致
- 临时关闭数据集shuffle逻辑,固定拆分规则后跑小批量训练,观察验证集准确率是否恢复正常
- 统计验证集生成器输出的标签分布,确认和训练集的标签分布没有出现极端倾斜
内容的提问来源于stack exchange,提问作者Samuel Ducloux

