2. 域适应中的特征分布对齐
任务说明
本案例面向Sim2Real视觉任务中的经典问题:在合成数据(源域)上训练的模型,直接部署到真实数据(目标域)时性能显著下降。目标是通过最大均值差异(Maximum Mean Discrepancy, MMD)在特征空间中对齐源域与目标域的分布,使模型在目标域上获得更好的分类效果。我们将构建一个包含特征提取器、分类器和MMD损失的最小可复现训练框架,并在两个分布偏移的合成数据集上验证对齐效果。
方法原理与实现
MMD是一种基于核函数的分布距离度量。给定源域特征 X_s 和目标域特征 X_t,其经验估计为:
MMD² = E[k(x_s, x_s')] - 2E[k(x_s, x_t)] + E[k(x_t, x_t')]
其中 k 为高斯核 k(x,y) = exp(-||x-y||²/(2σ²))。我们使用多核MMD(多个带宽取平均)以增强表达能力。训练时总损失为 L = L_cls + λ·MMD²,其中 L_cls 仅在源域有标签数据上计算。
为在有限篇幅内完整验证域适应效果,我们构造两个可复现的合成视觉特征数据集:源域特征来自以 [2,2] 为中心的高斯分布(类别0)和 [-2,-2] 为中心的高斯分布(类别1);目标域特征由源域特征施加线性变换 x_t = R·x_s + t 得到,模拟光照/视角变化导致的协变量偏移。模型为两层全连接网络,在源域训练后直接测试目标域(无适应基线),再使用MMD联合训练后测试(有适应)。
代码实现
import torch
import torch.nn as nn
import numpy as np
from torch.utils.data import TensorDataset, DataLoader
# ---------- 1. 数据生成(可复现) ----------
def generate_data(seed=42):
torch.manual_seed(seed)
np.random.seed(seed)
n_per_class = 500
# 源域:两个类别的高斯簇
X_s0 = torch.randn(n_per_class, 2) * 0.5 + torch.tensor([2.0, 2.0])
X_s1 = torch.randn(n_per_class, 2) * 0.5 + torch.tensor([-2.0, -2.0])
X_s = torch.cat([X_s0, X_s1], dim=0)
y_s = torch.cat([torch.zeros(n_per_class), torch.ones(n_per_class)]).long()
# 目标域:对源域做线性变换(旋转+平移),模拟域偏移
theta = np.radians(30)
R = torch.tensor([[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]], dtype=torch.float32)
t = torch.tensor([1.0, -0.5])
X_t = X_s @ R.T + t
# 目标域标签仅用于评估,不参与训练
y_t = y_s.clone()
return X_s, y_s, X_t, y_t
# ---------- 2. 模型定义 ----------
class FeatureExtractor(nn.Module):
def __init__(self, in_dim=2, feat_dim=8):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, 16),
nn.ReLU(),
nn.Linear(16, feat_dim)
)
def forward(self, x):
return self.net(x)
class Classifier(nn.Module):
def __init__(self, feat_dim=8, n_class=2):
super().__init__()
self.fc = nn.Linear(feat_dim, n_class)
def forward(self, feat):
return self.fc(feat)
# ---------- 3. MMD损失(多核高斯) ----------
def compute_mmd(X_s, X_t, bandwidths=[0.1, 1.0, 10.0]):
"""计算多核MMD²,输入为特征矩阵 (N, D)"""
def rbf_kernel(A, B, sigma):
# A: (N1, D), B: (N2, D) -> (N1, N2)
dist2 = torch.cdist(A, B) ** 2
return torch.exp(-dist2 / (2 * sigma ** 2))
mmd = 0.0
for bw in bandwidths:
K_ss = rbf_kernel(X_s, X_s, bw)
K_tt = rbf_kernel(X_t, X_t, bw)
K_st = rbf_kernel(X_s, X_t, bw)
n_s = X_s.size(0)
n_t = X_t.size(0)
# 无偏估计
term1 = K_ss.sum() / (n_s * (n_s - 1))
term2 = K_tt.sum() / (n_t * (n_t - 1))
term3 = 2 * K_st.sum() / (n_s * n_t)
mmd += (term1 + term2 - term3)
return mmd / len(bandwidths)
# ---------- 4. 训练与评估 ----------
def train_eval(use_mmd=True, epochs=100, lambda_mmd=1.0, seed=42):
X_s, y_s, X_t, y_t = generate_data(seed)
# 划分源域训练/验证(目标域全部用于测试)
n_train = int(0.8 * X_s.size(0))
perm = torch.randperm(X_s.size(0))
train_idx, val_idx = perm[:n_train], perm[n_train:]
train_loader = DataLoader(TensorDataset(X_s[train_idx], y_s[train_idx]),
batch_size=64, shuffle=True)
val_s = (X_s[val_idx], y_s[val_idx])
test_t = (X_t, y_t)
feat_net = FeatureExtractor()
clf = Classifier()
params = list(feat_net.parameters()) + list(clf.parameters())
opt = torch.optim.Adam(params, lr=1e-3)
loss_cls = nn.CrossEntropyLoss()
for epoch in range(epochs):
feat_net.train(); clf.train()
for xb, yb in train_loader:
opt.zero_grad()
feat_s = feat_net(xb)
out_s = clf(feat_s)
L_cls = loss_cls(out_s, yb)
if use_mmd:
# 从目标域采样与当前batch相同数量的样本
idx_t = torch.randint(0, X_t.size(0), (xb.size(0),))
feat_t = feat_net(X_t[idx_t])
L_mmd = compute_mmd(feat_s, feat_t)
loss = L_cls + lambda_mmd * L_mmd
else:
loss = L_cls
loss.backward()
opt.step()
# 评估
feat_net.eval(); clf.eval()
def accuracy(X, y):
with torch.no_grad():
feat = feat_net(X)
pred = clf(feat).argmax(dim=1)
return (pred == y).float().mean().item()
acc_s_val = accuracy(val_s[0], val_s[1])
acc_t = accuracy(test_t[0], test_t[1])
return acc_s_val, acc_t
# ---------- 5. 运行对比 ----------
if __name__ == "__main__":
print("=== 无域适应(仅源域训练) ===")
acc_s_no, acc_t_no = train_eval(use_mmd=False, seed=42)
print(f"源域验证准确率: {acc_s_no:.4f}")
print(f"目标域测试准确率: {acc_t_no:.4f}")
print("\n=== 有MMD域适应 ===")
acc_s_mmd, acc_t_mmd = train_eval(use_mmd=True, seed=42)
print(f"源域验证准确率: {acc_s_mmd:.4f}")
print(f"目标域测试准确率: {acc_t_mmd:.4f}")
运行与验证
依赖:Python 3.8+,PyTorch 1.9+,NumPy。无需额外数据集,代码自动生成合成数据。
运行方式:将上述代码保存为 mmd_da.py,在终端执行 python mmd_da.py。
判定标准:观察输出的两组准确率。若MMD域适应有效,则“有MMD”的目标域测试准确率应明显高于“无MMD”的目标域准确率(例如提升超过5个百分点),同时源域验证准确率保持基本稳定(不显著下降)。由于数据生成和训练过程固定随机种子,结果可复现。注意:由于目标域由源域线性变换而来,特征分布存在系统性偏移,MMD对齐应能有效缓解该偏移。
预期输出示例(基于固定随机种子,逻辑上可复现):无MMD时目标域准确率约0.75~0.85,有MMD时目标域准确率提升至0.90以上,源域验证准确率保持在0.95左右。实际数值以运行输出为准,重点观察相对提升趋势。