如何在Keras多标签图像分类任务中正确纳入无标签图像进行训练?
在Keras中利用无标签图像训练多标签分类模型
你的问题很典型——flow_from_dataframe在处理多标签场景下的无标签样本时确实会有这种尴尬:空标签要么被误判成新类别,要么直接被过滤掉。要解决这个问题,我们可以通过自定义数据生成器结合半监督训练策略来实现,下面给你具体的方案:
一、先理清数据标记逻辑
首先,我们需要在DataFrame里明确区分有标签和无标签样本,方便后续处理:
import pandas as pd # 新增一列标记是否有标签 df['has_labels'] = df['labels'].apply(lambda x: len(x) > 0) # 拆分出有标签和无标签的DataFrame labeled_df = df[df['has_labels']].reset_index(drop=True) unlabeled_df = df[~df['has_labels']].reset_index(drop=True)
二、方案1:自训练(Self-Training)——用伪标签利用无标签数据
这是最容易上手的半监督方式,思路是先训练一个基础模型,再用它给无标签数据生成伪标签,最后合并数据一起训练:
步骤1:用有标签数据训练基础模型
先正常用有标签数据训练一个初始模型:
from tensorflow.keras.preprocessing.image import ImageDataGenerator from tensorflow.keras.applications import MobileNetV2 from tensorflow.keras.layers import Dense, GlobalAveragePooling2D from tensorflow.keras.models import Model # 定义数据增强 datagen = ImageDataGenerator( rescale=1./255, validation_split=0.2, rotation_range=20, width_shift_range=0.2, height_shift_range=0.2, horizontal_flip=True ) # 生成有标签数据的生成器 labeled_train_gen = datagen.flow_from_dataframe( labeled_df, directory=data_dir + "/images", x_col='filename', y_col='labels', class_mode='categorical', shuffle=True, target_size=(IMAGE_SIZE, IMAGE_SIZE), batch_size=BATCH_SIZE, subset='training', seed=42 ) labeled_val_gen = datagen.flow_from_dataframe( labeled_df, directory=data_dir + "/images", x_col='filename', y_col='labels', class_mode='categorical', shuffle=False, target_size=(IMAGE_SIZE, IMAGE_SIZE), batch_size=BATCH_SIZE, subset='validation', seed=42 ) # 构建多标签分类模型(示例结构) base_model = MobileNetV2(input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3), include_top=False, weights='imagenet') x = base_model.output x = GlobalAveragePooling2D()(x) # 多标签分类必须用sigmoid激活 predictions = Dense(len(labeled_train_gen.class_indices), activation='sigmoid')(x) model = Model(inputs=base_model.input, outputs=predictions) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) # 训练基础模型 model.fit( labeled_train_gen, validation_data=labeled_val_gen, epochs=10 )
步骤2:给无标签数据生成伪标签
用训练好的基础模型对无标签图像预测,生成伪标签(注意只保留置信度高的结果,避免引入噪声):
# 无标签数据生成器(不需要标签) unlabeled_gen = datagen.flow_from_dataframe( unlabeled_df, directory=data_dir + "/images", x_col='filename', y_col=None, class_mode=None, shuffle=False, target_size=(IMAGE_SIZE, IMAGE_SIZE), batch_size=BATCH_SIZE, seed=42 ) # 预测伪标签 pseudo_labels = model.predict(unlabeled_gen, verbose=1) # 过滤置信度低的结果,比如只保留每个类别置信度>0.8的标签 threshold = 0.8 pseudo_labels_list = [] class_names = list(labeled_train_gen.class_indices.keys()) for pred in pseudo_labels: current_classes = [class_names[idx] for idx, score in enumerate(pred) if score > threshold] pseudo_labels_list.append(current_classes) # 把伪标签加回无标签DataFrame unlabeled_df['labels'] = pseudo_labels_list # 合并有标签和带伪标签的数据 combined_df = pd.concat([labeled_df, unlabeled_df]).reset_index(drop=True)
步骤3:用合并后的数据重新训练
现在可以用合并后的DataFrame训练模型了:
combined_train_gen = datagen.flow_from_dataframe( combined_df, directory=data_dir + "/images", x_col='filename', y_col='labels', class_mode='categorical', shuffle=True, target_size=(IMAGE_SIZE, IMAGE_SIZE), batch_size=BATCH_SIZE, subset='training', seed=42 ) combined_val_gen = datagen.flow_from_dataframe( combined_df, directory=data_dir + "/images", x_col='filename', y_col='labels', class_mode='categorical', shuffle=False, target_size=(IMAGE_SIZE, IMAGE_SIZE), batch_size=BATCH_SIZE, subset='validation', seed=42 ) # 继续训练模型(可以解冻部分预训练层提升效果) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) model.fit( combined_train_gen, validation_data=combined_val_gen, epochs=15 )
三、方案2:自定义混合生成器——同时训练有标签和无标签样本(一致性损失)
如果想更高效地利用无标签数据,可以自定义生成器,在每个batch中混合有标签和无标签样本,对有标签样本计算分类损失,对无标签样本计算一致性损失(即图像经过微小变换后,模型输出要和原输出一致):
自定义生成器示例
import numpy as np from tensorflow.keras.utils import Sequence class MixedDataGenerator(Sequence): def __init__(self, labeled_df, unlabeled_df, data_dir, image_size, batch_size, datagen): # 每个batch一半有标签,一半无标签 self.labeled_gen = datagen.flow_from_dataframe( labeled_df, directory=data_dir + "/images", x_col='filename', y_col='labels', class_mode='categorical', shuffle=True, target_size=(image_size, image_size), batch_size=batch_size//2, seed=42 ) self.unlabeled_gen = datagen.flow_from_dataframe( unlabeled_df, directory=data_dir + "/images", x_col='filename', y_col=None, class_mode=None, shuffle=True, target_size=(image_size, image_size), batch_size=batch_size//2, seed=42 ) self.class_indices = self.labeled_gen.class_indices self.num_classes = len(self.class_indices) def __len__(self): return max(len(self.labeled_gen), len(self.unlabeled_gen)) def __getitem__(self, idx): # 获取有标签的batch labeled_imgs, labeled_labels = self.labeled_gen[idx % len(self.labeled_gen)] # 获取无标签的batch unlabeled_imgs = self.unlabeled_gen[idx % len(self.unlabeled_gen)] # 对无标签图像做微小变换(比如水平翻转) unlabeled_imgs_aug = np.array([np.fliplr(img) for img in unlabeled_imgs]) # 合并所有图像和标记 all_imgs = np.concatenate([labeled_imgs, unlabeled_imgs, unlabeled_imgs_aug], axis=0) # 标记样本类型:1=有标签,0=无标签原图像,2=无标签变换图像 is_labeled = np.array([1]*len(labeled_imgs) + [0]*len(unlabeled_imgs) + [2]*len(unlabeled_imgs_aug)) # 无标签样本的标签占位用全0 all_labels = np.concatenate([labeled_labels, np.zeros((len(unlabeled_imgs)*2, self.num_classes))], axis=0) return [all_imgs, is_labeled], all_labels
自定义损失函数
需要自定义损失函数,区分有标签和无标签样本的损失计算:
import tensorflow as tf from tensorflow.keras import backend as K def mixed_loss(y_true, y_pred, is_labeled): # 有标签样本:计算二元交叉熵损失 labeled_mask = K.cast(K.equal(is_labeled, 1), K.floatx()) labeled_loss = K.sum(labeled_mask * K.binary_crossentropy(y_true, y_pred)) / K.sum(labeled_mask) # 无标签样本:计算一致性损失(原图像和变换后图像的输出MSE) unlabeled_mask = K.cast(K.equal(is_labeled, 0), K.floatx()) # 取原图像和变换后图像的输出做对比 orig_pred = y_pred[:len(y_pred)//3 + len(y_pred)//3] aug_pred = y_pred[len(y_pred)//3 + len(y_pred)//3:] unlabeled_loss = K.sum(unlabeled_mask * K.mean(K.square(orig_pred - aug_pred), axis=1)) / K.sum(unlabeled_mask) # 调整一致性损失的权重,避免盖过分类损失 return labeled_loss + 0.1 * unlabeled_loss
训练模型
需要修改模型的输入输出,适配自定义生成器:
from tensorflow.keras.layers import Input # 新增输入标记样本类型 img_input = Input(shape=(IMAGE_SIZE, IMAGE_SIZE, 3)) is_labeled_input = Input(shape=(1,)) base_model = MobileNetV2(input_tensor=img_input, include_top=False, weights='imagenet') x = base_model.output x = GlobalAveragePooling2D()(x) predictions = Dense(len(labeled_train_gen.class_indices), activation='sigmoid')(x) model = Model(inputs=[img_input, is_labeled_input], outputs=predictions) optimizer = tf.keras.optimizers.Adam() # 自定义训练步骤 @tf.function def train_step(data): imgs, is_labeled = data[0] y_true = data[1] with tf.GradientTape() as tape: y_pred = model([imgs, is_labeled], training=True) loss = mixed_loss(y_true, y_pred, is_labeled) gradients = tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(gradients, model.trainable_variables)) return {'loss': loss} # 初始化生成器 mixed_gen = MixedDataGenerator(labeled_df, unlabeled_df, data_dir, IMAGE_SIZE, BATCH_SIZE, datagen) # 开始训练 for epoch in range(20): print(f"Epoch {epoch+1}/20") total_loss = 0.0 for step in range(len(mixed_gen)): data = mixed_gen[step] metrics = train_step(data) total_loss += metrics['loss'].numpy() if step % 50 == 0: print(f"Step {step}, Loss: {metrics['loss'].numpy():.4f}") print(f"Epoch Loss: {total_loss/len(mixed_gen):.4f}")
关键注意事项
- 多标签分类一定要用
sigmoid激活函数和binary_crossentropy损失,不要用softmax和categorical_crossentropy,因为每个标签是独立的。 - 伪标签的置信度阈值很重要,太高会减少可用数据,太低会引入噪声,建议根据你的数据集调整。
- 一致性损失的权重需要根据实验调整,避免无标签样本的损失盖过有标签样本的分类损失。
内容的提问来源于stack exchange,提问作者brad
相关产品推荐
相关产品推荐

