You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 19:28:10