生成对抗网络在广义零样本学习中的实战应用:从理论到代码实现

当面对那些从未在训练集中出现过的类别时,传统深度学习模型往往会束手无策。想象一下,你训练了一个出色的鸟类分类器,能够准确识别200种常见鸟类,但当遇到一种全新的、从未见过的鸟类时,系统就会完全失效。这正是广义零样本学习(Generalized Zero-Shot Learning, GZSL)要解决的核心问题——如何让模型不仅能识别已知类别,还能推广到那些只有语义描述而没有任何训练样本的"未见类别"。

1. 广义零样本学习的核心挑战与生成式解决方案

广义零样本学习与传统机器学习范式的根本区别在于,它要求模型在测试阶段同时处理已见类别(seen classes)和未见类别(unseen classes)的样本。这种设定更贴近现实场景,因为在真实世界中,我们往往无法为所有可能的类别收集足够的标注数据。

1.1 传统方法的局限性

传统嵌入方法(embedding-based methods)通常学习一个从视觉空间到语义空间的映射函数,面临三个主要问题:

  1. 投影域偏移(Projection Domain Shift):在已见类别上学习的映射函数,往往无法很好地泛化到未见类别
  2. 枢纽点问题(Hubness Problem):在高维嵌入空间中,某些点会成为大量其他点的最近邻,导致预测偏差
  3. 可见类偏差(Seen Class Bias):模型倾向于将测试样本预测为已见类别,因为训练过程中只接触过这些类别
# 传统嵌入方法的典型流程示例
def traditional_embedding_method(feature, semantic_embedding):
    # 学习一个从视觉特征到语义空间的映射
    projection_matrix = learn_projection(train_features, train_semantics)
    
    # 测试时将特征投影到语义空间
    projected_feature = np.dot(feature, projection_matrix)
    
    # 在语义空间中找到最近邻的类别
    distances = np.linalg.norm(semantic_embedding - projected_feature, axis=1)
    predicted_class = np.argmin(distances)
    
    return predicted_class

1.2 生成式方法的优势

生成对抗网络(GAN)和变分自编码器(VAE)等生成式方法通过为未见类别合成伪训练数据,将GZSL问题转化为标准的监督学习问题。这种方法具有以下优势:

表1:嵌入方法与生成式方法对比

特性嵌入方法生成式方法
处理未见类别直接映射到语义空间生成伪训练样本
训练复杂度相对简单较高(需训练生成器)
枢纽点问题严重较轻
可见类偏差明显可通过数据平衡缓解
最终分类器最近邻搜索可选用任何监督学习模型

生成式方法的核心思想是:如果能为未见类别生成足够真实且多样的特征样本,那么任何现成的分类器都可以直接应用。这大大扩展了GZSL解决方案的设计空间。

2. f-CLSWGAN:为未见类别生成特征的实战框架

在众多生成式方法中,f-CLSWGAN(feature-synthesizing Conditional Least Squares WGAN)因其稳定性和效果成为广泛采用的基准模型。下面我们深入解析其实现细节。

2.1 网络架构设计

f-CLSWGAN基于Wasserstein GAN(WGAN)框架,主要包含三个关键组件:

  1. 生成器(Generator):输入噪声向量和类别语义描述,输出合成的视觉特征
  2. 判别器(Discriminator):判断输入特征是真实的还是生成的,同时验证其类别一致性
  3. 分类器(Classifier):预训练的特征分类器,用于提供额外的监督信号
import torch
import torch.nn as nn

class Generator(nn.Module):
    def __init__(self, noise_dim, semantic_dim, feature_dim):
        super(Generator, self).__init__()
        self.fc1 = nn.Linear(noise_dim + semantic_dim, 4096)
        self.fc2 = nn.Linear(4096, feature_dim)
        self.leaky_relu = nn.LeakyReLU(0.2)
        
    def forward(self, noise, semantic):
        x = torch.cat([noise, semantic], dim=1)
        x = self.leaky_relu(self.fc1(x))
        x = self.fc2(x)
        return x

class Discriminator(nn.Module):
    def __init__(self, feature_dim, semantic_dim):
        super(Discriminator, self).__init__()
        self.fc1 = nn.Linear(feature_dim + semantic_dim, 4096)
        self.fc2 = nn.Linear(4096, 1)
        self.leaky_relu = nn.LeakyReLU(0.2)
        
    def forward(self, feature, semantic):
        x = torch.cat([feature, semantic], dim=1)
        x = self.leaky_relu(self.fc1(x))
        x = self.fc2(x)
        return x

2.2 损失函数设计

f-CLSWGAN的损失函数包含三个关键部分:

  1. Wasserstein对抗损失:确保生成特征的分布与真实特征分布一致
  2. 分类损失:保证生成特征能够被正确分类到对应类别
  3. 循环一致性损失(可选):增强生成特征与语义描述之间的对应关系
def compute_gradient_penalty(D, real_samples, fake_samples, semantic):
    """计算WGAN-GP中的梯度惩罚项"""
    alpha = torch.rand(real_samples.size(0), 1).to(device)
    interpolates = (alpha * real_samples + ((1 - alpha) * fake_samples)).requires_grad_(True)
    d_interpolates = D(interpolates, semantic)
    
    gradients = torch.autograd.grad(
        outputs=d_interpolates,
        inputs=interpolates,
        grad_outputs=torch.ones_like(d_interpolates),
        create_graph=True,
        retain_graph=True,
        only_inputs=True,
    )[0]
    
    gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
    return gradient_penalty

# 训练循环中的关键损失计算
d_loss = -torch.mean(d_real) + torch.mean(d_fake) + lambda_gp * gradient_penalty
g_loss = -torch.mean(d_fake) + lambda_cls * cls_loss

2.3 实现技巧与调参经验

在实际实现f-CLSWGAN时,以下几个技巧能显著提升模型性能:

  1. 梯度惩罚(Gradient Penalty):使用WGAN-GP代替权重裁剪,稳定训练过程
  2. 特征归一化:对视觉特征进行标准化处理(如L2归一化)
  3. 学习率调度:采用线性衰减的学习率策略
  4. 平衡采样:为已见和未见类别生成等量的伪样本
  5. 分类器预热:先预训练分类器,再固定其参数用于生成器训练

提示:在实际应用中,建议先在小型数据集(如CUB)上调试超参数,再迁移到更大规模数据集(如AWA2)。典型超参数范围:生成器/判别器学习率0.0001-0.0005,批大小64-256,梯度惩罚系数10,分类损失权重0.1-0.5。

3. 从生成到分类:构建完整GZSL系统

有了生成模型合成的伪特征后,我们需要构建完整的分类流程。本节将介绍端到端的实现方案。

3.1 数据准备与预处理

常用GZSL数据集及其特性:

表2:常用GZSL数据集统计信息

数据集类别数图像数特征维度语义类型
CUB20011,7882048 (ResNet)312-D属性
AWA25037,3222048 (ResNet)85-D属性
SUN71714,3402048 (ResNet)102-D属性
FLO1028,1892048 (ResNet)1024-D词向量

数据预处理的关键步骤:

def prepare_data(dataset_name):
    # 加载特征和标签
    features = load_feature(dataset_name)  # shape: (N, feature_dim)
    labels = load_label(dataset_name)      # shape: (N,)
    semantics = load_semantic(dataset_name) # shape: (C, semantic_dim)
    
    # 划分已见/未见类别
    seen_classes, unseen_classes = get_class_split(dataset_name)
    
    # 标准化特征
    scaler = StandardScaler()
    features = scaler.fit_transform(features)
    
    # L2归一化
    features = normalize(features, norm='l2')
    
    # 构建训练集(仅包含已见类别)
    train_mask = np.isin(labels, seen_classes)
    X_train = features[train_mask]
    y_train = labels[train_mask]
    
    return X_train, y_train, seen_classes, unseen_classes, semantics

3.2 特征生成流程

为未见类别生成伪特征的完整流程:

def generate_unseen_features(generator, unseen_classes, semantics, samples_per_class=500):
    generated_features = []
    generated_labels = []
    
    for class_id in unseen_classes:
        # 获取类别语义向量
        class_semantic = semantics[class_id]
        
        # 为每个类别生成指定数量的样本
        for _ in range(samples_per_class):
            # 生成随机噪声
            noise = torch.randn(1, noise_dim).to(device)
            
            # 扩展语义向量维度
            semantic_vec = torch.FloatTensor(class_semantic).unsqueeze(0).to(device)
            
            # 生成特征
            with torch.no_grad():
                fake_feature = generator(noise, semantic_vec)
            
            generated_features.append(fake_feature.cpu().numpy())
            generated_labels.append(class_id)
    
    return np.concatenate(generated_features), np.array(generated_labels)

3.3 分类器训练与评估

使用生成的伪特征训练分类器时,需要注意以下几点:

  1. 数据平衡:确保已见和未见类别的样本数量相当
  2. 分类器选择:线性分类器通常足够,复杂模型可能过拟合
  3. 评估指标:使用调和平均数(H)综合评价已见和未见类别的准确率
def train_and_evaluate(X_seen, y_seen, X_unseen_gen, y_unseen_gen, unseen_classes, test_features, test_labels):
    # 合并真实已见类别和生成的未见类别数据
    X_train = np.concatenate([X_seen, X_unseen_gen])
    y_train = np.concatenate([y_seen, y_unseen_gen])
    
    # 训练分类器
    classifier = LinearSVC(C=0.1, multi_class='ovr')
    classifier.fit(X_train, y_train)
    
    # 评估测试集
    y_pred = classifier.predict(test_features)
    
    # 计算已见/未见类别的准确率
    seen_mask = np.isin(test_labels, seen_classes)
    unseen_mask = np.isin(test_labels, unseen_classes)
    
    acc_seen = accuracy_score(test_labels[seen_mask], y_pred[seen_mask])
    acc_unseen = accuracy_score(test_labels[unseen_mask], y_pred[unseen_mask])
    
    # 计算调和平均数
    H = 2 * acc_seen * acc_unseen / (acc_seen + acc_unseen + 1e-12)
    
    return acc_seen, acc_unseen, H

4. 前沿进展与实战优化策略

近年来,GZSL领域涌现出许多改进生成质量的新方法。本节介绍几种经实践证明有效的进阶技术。

4.1 语义一致性增强技术

原始f-CLSWGAN的一个局限是生成的视觉特征可能与语义描述不完全一致。为解决这个问题,可以引入:

  1. 循环一致性损失:确保生成的特征能够被映射回原始语义空间
  2. 注意力机制:聚焦于语义描述中最相关的部分
  3. 多模态对比学习:拉近视觉特征与对应语义描述的距离
class ImprovedGenerator(nn.Module):
    def __init__(self, noise_dim, semantic_dim, feature_dim):
        super(ImprovedGenerator, self).__init__()
        # 增加注意力模块
        self.attention = nn.Sequential(
            nn.Linear(semantic_dim, semantic_dim),
            nn.Softmax(dim=1)
        )
        
        # 主干网络
        self.fc1 = nn.Linear(noise_dim + semantic_dim, 4096)
        self.fc2 = nn.Linear(4096, feature_dim)
        
    def forward(self, noise, semantic):
        # 应用注意力权重
        attn_weights = self.attention(semantic)
        weighted_semantic = semantic * attn_weights
        
        x = torch.cat([noise, weighted_semantic], dim=1)
        x = F.leaky_relu(self.fc1(x), 0.2)
        x = self.fc2(x)
        return x

def cycle_consistency_loss(feature, semantic, reconstructor):
    """计算循环一致性损失"""
    reconstructed_semantic = reconstructor(feature)
    return F.mse_loss(reconstructed_semantic, semantic)

4.2 生成-分类联合优化

另一种思路是端到端地联合优化生成器和分类器,而不是分阶段训练。这种方法可以:

  1. 让生成器直接优化分类性能而不仅仅是特征分布
  2. 动态调整生成样本的难度
  3. 实现生成与分类的协同进化
class JointOptimizationModel(nn.Module):
    def __init__(self, generator, classifier):
        super(JointOptimizationModel, self).__init__()
        self.generator = generator
        self.classifier = classifier
        
    def forward(self, noise, semantic, labels):
        # 生成特征
        gen_features = self.generator(noise, semantic)
        
        # 分类预测
        preds = self.classifier(gen_features)
        
        # 计算分类损失
        cls_loss = F.cross_entropy(preds, labels)
        
        return gen_features, cls_loss

# 训练过程中交替优化生成器和分类器
for epoch in range(num_epochs):
    # 训练生成器
    gen_optimizer.zero_grad()
    fake_features, cls_loss = joint_model(noise, semantic, labels)
    d_fake = discriminator(fake_features, semantic)
    g_loss = -torch.mean(d_fake) + lambda_cls * cls_loss
    g_loss.backward()
    gen_optimizer.step()
    
    # 训练分类器
    cls_optimizer.zero_grad()
    with torch.no_grad():
        fake_features = generator(noise, semantic)
    preds = classifier(fake_features)
    cls_loss = F.cross_entropy(preds, labels)
    cls_loss.backward()
    cls_optimizer.step()

4.3 多模态特征融合

利用多种预训练模型提取的视觉特征可以增强生成样本的多样性:

  1. 同时使用ResNet和ViT特征
  2. 不同层次的特征融合(低层+高层)
  3. 跨模态对齐(图像+文本)
class MultiModalGenerator(nn.Module):
    def __init__(self, noise_dim, semantic_dim, resnet_dim, vit_dim):
        super(MultiModalGenerator, self).__init__()
        # ResNet分支
        self.resnet_fc = nn.Linear(noise_dim + semantic_dim, resnet_dim)
        
        # ViT分支
        self.vit_fc = nn.Linear(noise_dim + semantic_dim, vit_dim)
        
        # 融合层
        self.fusion = nn.Linear(resnet_dim + vit_dim, resnet_dim)
        
    def forward(self, noise, semantic):
        x = torch.cat([noise, semantic], dim=1)
        
        # 生成两种模态的特征
        resnet_feat = F.leaky_relu(self.resnet_fc(x), 0.2)
        vit_feat = F.leaky_relu(self.vit_fc(x), 0.2)
        
        # 特征融合
        fused = torch.cat([resnet_feat, vit_feat], dim=1)
        out = self.fusion(fused)
        
        return out

5. 实际应用中的挑战与解决方案

尽管生成式GZSL方法在理论上很吸引人,但在实际部署时仍会面临诸多挑战。本节分享一些实战经验。

5.1 小语义空间问题

当语义描述维度较低时(如AWA2只有85维属性),生成器难以学习到丰富的类别区分信息。解决方案包括:

  1. 语义空间扩展:使用词向量补充人工定义的属性
  2. 层次化语义编码:利用类别间的层次关系
  3. 自监督增强:通过对比学习挖掘更多语义信息
def enhance_semantics(base_semantics, word_vectors, class_hierarchy):
    """
    增强语义表示:
    base_semantics: 原始语义向量 (C, K)
    word_vectors: 预训练词向量 (C, M)
    class_hierarchy: 类别层次关系 (C, L)
    """
    # 拼接基础属性和词向量
    enhanced = np.concatenate([base_semantics, word_vectors], axis=1)
    
    # 添加层次关系特征
    hierarchy_features = []
    for c in range(len(base_semantics)):
        # 计算当前类别的所有祖先类别的平均语义
        ancestors = get_ancestors(class_hierarchy, c)
        if len(ancestors) > 0:
            ancestor_feat = np.mean(base_semantics[ancestors], axis=0)
        else:
            ancestor_feat = np.zeros_like(base_semantics[0])
        hierarchy_features.append(ancestor_feat)
    
    hierarchy_features = np.array(hierarchy_features)
    enhanced = np.concatenate([enhanced, hierarchy_features], axis=1)
    
    return enhanced

5.2 长尾分布问题

现实世界中类别分布往往不平衡,某些罕见类别样本极少。针对这种情况可以:

  1. 自适应生成样本数量:根据类别频率调整生成样本数
  2. 困难样本挖掘:聚焦于难以生成的类别
  3. 课程学习:从简单类别逐步过渡到困难类别
def adaptive_sampling(generator, classes, semantics, base_samples=300):
    """根据类别难度自适应生成不同数量的样本"""
    samples_per_class = []
    
    # 预生成少量样本评估每个类别的生成难度
    with torch.no_grad():
        for class_id in classes:
            noise = torch.randn(10, noise_dim).to(device)
            semantic = torch.FloatTensor(semantics[class_id]).unsqueeze(0).repeat(10, 1).to(device)
            features = generator(noise, semantic)
            
            # 用分类器评估生成质量
            preds = classifier(features)
            accuracy = (preds.argmax(1) == class_id).float().mean()
            
            # 根据准确率调整样本数量
            scale = 1.5 - accuracy.item()  # 生成质量越低,生成越多样本
            samples = int(base_samples * scale)
            samples_per_class.append(max(samples, 100))
    
    # 实际生成
    all_features = []
    all_labels = []
    
    for i, class_id in enumerate(classes):
        noise = torch.randn(samples_per_class[i], noise_dim).to(device)
        semantic = torch.FloatTensor(semantics[class_id]).unsqueeze(0).repeat(samples_per_class[i], 1).to(device)
        
        features = generator(noise, semantic)
        all_features.append(features.cpu())
        all_labels.extend([class_id] * samples_per_class[i])
    
    return torch.cat(all_features), np.array(all_labels)

5.3 领域适应问题

当测试数据与训练数据存在分布差异时(如不同相机拍摄的图像),性能会显著下降。可以考虑:

  1. 领域对抗训练:添加领域判别器减少域差异
  2. 特征解耦:分离领域相关和类别相关特征
  3. 测试时适应:利用无标签测试数据微调生成器
class DomainAwareGenerator(nn.Module):
    def __init__(self, noise_dim, semantic_dim, feature_dim):
        super(DomainAwareGenerator, self).__init__()
        # 共享编码器
        self.encoder = nn.Sequential(
            nn.Linear(noise_dim + semantic_dim, 2048),
            nn.LeakyReLU(0.2)
        )
        
        # 领域特定生成头
        self.domain1_head = nn.Linear(2048, feature_dim)
        self.domain2_head = nn.Linear(2048, feature_dim)
        
    def forward(self, noise, semantic, domain_id):
        x = torch.cat([noise, semantic], dim=1)
        x = self.encoder(x)
        
        if domain_id == 0:
            return self.domain1_head(x)
        else:
            return self.domain2_head(x)

class DomainDiscriminator(nn.Module):
    def __init__(self, feature_dim):
        super(DomainDiscriminator, self).__init__()
        self.net = nn.Sequential(
            nn.Linear(feature_dim, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 1),
            nn.Sigmoid()
        )
        
    def forward(self, x):
        return self.net(x)
Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐