算法学习笔记 Day 1:迁移学习与域自适应(DANN/CORAL)
目录
1. 迁移学习简介
1.1 背景
在工业预测与健康管理(Prognostics and Health Management, PHM)领域,我们经常面临一个核心挑战:训练数据与测试数据分布不一致。
具体来说:
| 场景 | 训练数据 | 测试数据 | 问题 |
|---|---|---|---|
| 跨工况 | 正常转速运行的轴承数据 | 不同转速下的轴承数据 | 工况变化导致分布偏移 |
| 跨设备 | 设备A的历史故障数据 | 设备B的新故障数据 | 设备个体差异 |
| 跨时段 | 历史生产数据 | 当前生产数据 | 环境/材料老化 |
传统机器学习方法假设训练和测试数据独立同分布(i.i.d.),这一假设在实际PHM场景中往往不成立,导致模型泛化能力急剧下降。
1.2 迁移学习的核心思想
迁移学习(Transfer Learning) 的核心思想是:利用源域(Source Domain)学到的知识,帮助目标域(Target Domain)的学习。
源域:拥有丰富标签数据的领域(通常是容易获取数据的场景)
目标域:数据稀缺但我们真正关心的领域
形式化定义:
- 源域 : D s = { ( x s i , y s i ) } i = 1 n s D_s = \{(x_s^i, y_s^i)\}_{i=1}^{n_s} Ds={(xsi,ysi)}i=1ns,包含大量标注数据
- 目标域 : D t = { x t j } j = 1 n t D_t = \{x_t^j\}_{j=1}^{n_t} Dt={xtj}j=1nt,只有少量或没有标注数据
- 任务 :在目标域上学习预测函数 f : x → y f: x \rightarrow y f:x→y
核心目标:最小化域间分布差异,使知识从源域有效迁移到目标域。
1.3 在PHM故障诊断中的重要性
- 数据稀缺问题:真实故障样本获取困难,迁移学习可以充分利用相似工况/设备的数据
- 标注成本高:工业数据标注需要专家知识,迁移学习减少对目标域标注的依赖
- 模型泛化:提高模型在不同工作条件下的鲁棒性
- 冷启动问题:新设备上线时没有足够历史数据,迁移学习提供启动能力
2. 域自适应原理
2.1 域差异度量
域自适应的关键在于如何量化"域间差异"。常用的度量方法有:
2.1.1 最大均值差异(MMD)
MMD(Maximum Mean Discrepancy)是度量两个分布差异的经典方法,通过比较样本在再生核希尔伯特空间(RKHS)中的均值差异:
MMD 2 ( D s , D t ) = ∥ 1 n s ∑ i = 1 n s ϕ ( x s i ) − 1 n t ∑ j = 1 n t ϕ ( x t j ) ∥ H 2 \text{MMD}^2(D_s, D_t) = \left\| \frac{1}{n_s}\sum_{i=1}^{n_s}\phi(x_s^i) - \frac{1}{n_t}\sum_{j=1}^{n_t}\phi(x_t^j) \right\|^2_{\mathcal{H}} MMD2(Ds,Dt)= ns1i=1∑nsϕ(xsi)−nt1j=1∑ntϕ(xtj) H2
其中 ϕ ( ⋅ ) \phi(\cdot) ϕ(⋅) 是核函数映射。常用的核函数为高斯核:
k ( x , y ) = exp ( − ∥ x − y ∥ 2 2 σ 2 ) k(x, y) = \exp\left(-\frac{\|x - y\|^2}{2\sigma^2}\right) k(x,y)=exp(−2σ2∥x−y∥2)
PHM应用场景:将源域和目标域的特征都映射到RKHS,比较其均值嵌入向量的距离。
2.1.2 CORAL距离
CORAL(Correlation Alignment)是一种简单而有效的域差异度量方法,通过对齐源域和目标域的二阶统计量(协方差矩阵):
L CORAL = 1 4 d 2 ∥ C s − C t ∥ F 2 \mathcal{L}_{\text{CORAL}} = \frac{1}{4d^2}\|\mathbf{C}_s - \mathbf{C}_t\|_F^2 LCORAL=4d21∥Cs−Ct∥F2
其中:
- C s \mathbf{C}_s Cs:源域特征的协方差矩阵
- C t \mathbf{C}_t Ct:目标域特征的协方差矩阵
- ∥ ⋅ ∥ F \|\cdot\|_F ∥⋅∥F:Frobenius范数
优点:计算简单,无需额外参数,在PHM领域效果稳健。
python
def coral_loss(source_features, target_features):
"""
计算CORAL损失:对齐源域和目标域特征的协方差矩阵
"""
d = source_features.size(1)
# 去均值化
source_centered = source_features - source_features.mean(dim=0, keepdim=True)
target_centered = target_features - target_features.mean(dim=0, keepdim=True)
# 计算协方差矩阵
source_cov = (source_centered.T @ source_centered) / (source_features.size(0) - 1)
target_cov = (target_centered.T @ target_features) / (target_features.size(0) - 1)
# CORAL损失
loss = torch.norm(source_cov - target_cov, p='fro') ** 2 / (4 * d * d)
return loss
2.2 DANN(域对抗神经网络)数学推导
DANN(Domain-Adversarial Neural Network)是迁移学习领域里程碑式的工作,其核心思想是:**训练一个特征提取器,使得提取的特征"欺骗"域判别器",从而达到域不变的效果。
2.2.1 网络架构
DANN由三部分组成:
输入数据 → 特征提取器 G_f → 特征表示 F
↓
类别预测器 G_y → 类别标签 y
域判别器 G_d → 域标签 d
2.2.2 数学推导
特征提取器 : F = G f ( x ; θ f ) F = G_f(x; \theta_f) F=Gf(x;θf),将输入映射到特征空间
类别预测器 : y ^ = G y ( F ; θ y ) \hat{y} = G_y(F; \theta_y) y^=Gy(F;θy),预测样本类别
域判别器 : d ^ = G d ( F ; θ d ) \hat{d} = G_d(F; \theta_d) d^=Gd(F;θd),判断样本来自哪个域
损失函数设计:
-
源域分类损失 (有监督):
L y ( θ f , θ y ) = − E ( x s , y s ) ∼ D s ∑ k = 1 K 1 [ k = y s ] log G y k ( G f ( x s ; θ f ) ; θ y ) \mathcal{L}y(\theta_f, \theta_y) = -\mathbb{E}{(x_s, y_s)\sim D_s} \sum_{k=1}^{K} \mathbb{1}_{[k=y_s]} \log G_y^k(G_f(x_s; \theta_f); \theta_y) Ly(θf,θy)=−E(xs,ys)∼Dsk=1∑K1[k=ys]logGyk(Gf(xs;θf);θy) -
域判别损失 :
L d ( θ f , θ d ) = − E x s ∼ D s log G d ( G f ( x s ; θ f ) ; θ d ) − E x t ∼ D t log ( 1 − G d ( G f ( x t ; θ f ) ; θ d ) ) \mathcal{L}d(\theta_f, \theta_d) = -\mathbb{E}{x_s\sim D_s} \log G_d(G_f(x_s; \theta_f); \theta_d) - \mathbb{E}_{x_t\sim D_t} \log(1 - G_d(G_f(x_t; \theta_f); \theta_d)) Ld(θf,θd)=−Exs∼DslogGd(Gf(xs;θf);θd)−Ext∼Dtlog(1−Gd(Gf(xt;θf);θd)) -
梯度反转层:这是DANN的核心创新!
在反向传播时,梯度反转层将域判别器的梯度取反:
R λ ( x ) = x R_\lambda(x) = x Rλ(x)=x
∂ R λ ∂ x = − λ I \frac{\partial R_\lambda}{\partial x} = -\lambda \mathbf{I} ∂x∂Rλ=−λI
这意味着特征提取器被训练去最大化域判别器的损失,从而学习到域不变的表示。
2.2.3 总体目标
min θ f , θ y max θ d L y ( θ f , θ y ) − λ L d ( θ f , θ d ) \min_{\theta_f, \theta_y} \max_{\theta_d} \mathcal{L}_y(\theta_f, \theta_y) - \lambda \mathcal{L}_d(\theta_f, \theta_d) θf,θyminθdmaxLy(θf,θy)−λLd(θf,θd)
- 类别预测器:最小化分类误差
- 特征提取器:最小化分类误差 + 最大化域判别误差(通过梯度反转)
- 域判别器:最大化域判别准确率
2.3 损失函数设计
完整的DANN损失函数为:
python
def dann_loss(source_features, target_features, source_labels,
feature_extractor, class_classifier, domain_classifier,
lambda_param=1.0):
"""
计算DANN总损失
Args:
source_features: 源域特征
target_features: 目标域特征
source_labels: 源域标签
lambda_param: 域对齐权重
"""
# 1. 源域分类损失
source_preds = class_classifier(source_features)
cls_loss = F.cross_entropy(source_preds, source_labels)
# 2. 域判别损失
# 源域样本标签为0,目标域样本标签为1
source_domain_labels = torch.zeros(source_features.size(0)).long().to(source_features.device)
target_domain_labels = torch.ones(target_features.size(0)).long().to(target_features.device)
combined_features = torch.cat([source_features, target_features], dim=0)
combined_domain_labels = torch.cat([source_domain_labels, target_domain_labels], dim=0)
domain_preds = domain_classifier(combined_features)
domain_loss = F.cross_entropy(domain_preds, combined_domain_labels)
# 3. 总损失
total_loss = cls_loss - lambda_param * domain_loss
return total_loss, cls_loss, domain_loss
3. 代码实现
3.1 整体网络结构
python
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np
from sklearn.preprocessing import StandardScaler
class DANN(nn.Module):
"""
域对抗神经网络 (Domain-Adversarial Neural Network)
用于PHM领域的跨域故障诊断
网络结构:
输入 → 特征提取器 → 特征表示
↓
类别预测器 → 故障类别
域判别器 → 域标签
"""
def __init__(self, input_dim, num_classes, hidden_dims=[256, 128]):
super(DANN, self).__init__()
# ==================== 特征提取器 ====================
# 将原始传感器数据映射到域不变的特征空间
feature_layers = []
prev_dim = input_dim
for hidden_dim in hidden_dims:
feature_layers.extend([
nn.Linear(prev_dim, hidden_dim),
nn.BatchNorm1d(hidden_dim),
nn.ReLU(),
nn.Dropout(0.3)
])
prev_dim = hidden_dim
self.feature_extractor = nn.Sequential(*feature_layers)
self.feature_dim = prev_dim
# ==================== 类别预测器 ====================
# 预测故障类型(仅在源域有标签时训练)
self.class_classifier = nn.Sequential(
nn.Linear(self.feature_dim, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, num_classes)
)
# ==================== 域判别器 ====================
# 判断样本来自源域还是目标域
self.domain_classifier = nn.Sequential(
nn.Linear(self.feature_dim, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 2) # 二分类:源域(0) 或 目标域(1)
)
def forward(self, x, alpha=1.0):
"""
前向传播
Args:
x: 输入数据
alpha: 梯度反转强度参数
Returns:
class_output: 类别预测
domain_output: 域预测
features: 提取的特征
"""
# 提取特征
features = self.feature_extractor(x)
# 梯度反转层会在反向传播时反转梯度
# 前向传播时不变
features_grad = GradientReverseFunction.apply(features, alpha)
# 类别预测和域预测
class_output = self.class_classifier(features)
domain_output = self.domain_classifier(features_grad)
return class_output, domain_output, features
def extract_features(self, x):
"""提取特征(用于推理)"""
return self.feature_extractor(x)
class GradientReverseFunction(torch.autograd.Function):
"""
梯度反转层实现
前向传播:保持输入不变
反向传播:将梯度乘以 -alpha 后输出
这是DANN的核心组件,使得特征提取器学习域不变表示
"""
@staticmethod
def forward(ctx, x, alpha):
ctx.alpha = alpha
return x.view_as(x)
@staticmethod
def backward(ctx, grad_output):
# 反向传播时反转梯度
return grad_output.neg() * ctx.alpha, None
class GradientReverseLayer(nn.Module):
"""
梯度反转层的封装
"""
def __init__(self, alpha=1.0):
super(GradientReverseLayer, self).__init__()
self.alpha = alpha
def forward(self, x):
return GradientReverseFunction.apply(x, self.alpha)
def dann_loss(source_features, target_features, source_labels,
class_classifier, domain_classifier, lambda_param=1.0):
"""
计算DANN损失函数
损失组成:
1. 源域分类损失:使特征提取器学习判别性特征
2. 域判别损失(带负号):通过梯度反转,最大化域判别器的损失
总目标:最小化分类损失,最大化域判别损失
"""
# 源域分类损失
source_preds = class_classifier(source_features)
cls_loss = F.cross_entropy(source_preds, source_labels)
# 构造域标签:源域为0,目标域为1
source_domain_labels = torch.zeros(source_features.size(0)).long().to(source_features.device)
target_domain_labels = torch.ones(target_features.size(0)).long().to(source_features.device)
# 拼接源域和目标域特征
combined_features = torch.cat([source_features, target_features], dim=0)
combined_domain_labels = torch.cat([source_domain_labels, target_domain_labels], dim=0)
# 域判别损失
domain_preds = domain_classifier(combined_features)
domain_loss = F.cross_entropy(domain_preds, combined_domain_labels)
# 总损失:注意域损失前有负号(通过梯度反转实现)
total_loss = cls_loss - lambda_param * domain_loss
return total_loss, cls_loss, domain_loss
3.2 完整训练流程
python
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
class PHMDANNTrainer:
"""
PHM领域DANN模型训练器
支持:
- 跨工况故障诊断
- 跨设备迁移
- 域自适应训练
"""
def __init__(self, model, device='cuda' if torch.cuda.is_available() else 'cpu'):
self.model = model.to(device)
self.device = device
self.history = {
'cls_loss': [], 'domain_loss': [], 'total_loss': [],
'source_acc': [], 'target_acc': []
}
def train_epoch(self, source_loader, target_loader,
optimizer, lambda_param=1.0):
"""
训练一个epoch
Args:
source_loader: 源域数据加载器(有标签)
target_loader: 目标域数据加载器(无标签)
optimizer: 优化器
lambda_param: 域对齐参数
Returns:
metrics: 当前epoch的训练指标
"""
self.model.train()
total_cls_loss, total_domain_loss = 0, 0
correct_source, total_source = 0, 0
# 创建目标域迭代器
target_iter = iter(target_loader)
for source_data, source_labels in source_loader:
# 获取目标域数据
try:
target_data, _ = next(target_iter)
except StopIteration:
target_iter = iter(target_loader)
target_data, _ = next(target_iter)
# 移到设备
source_data = source_data.to(self.device)
source_labels = source_labels.to(self.device)
target_data = target_data.to(self.device)
# 前向传播
optimizer.zero_grad()
# 提取特征
source_features = self.model.feature_extractor(source_data)
target_features = self.model.feature_extractor(target_data)
# 计算损失
total_loss, cls_loss, domain_loss = dann_loss(
source_features, target_features, source_labels,
self.model.class_classifier,
self.model.domain_classifier,
lambda_param
)
# 反向传播
total_loss.backward()
optimizer.step()
# 统计
total_cls_loss += cls_loss.item()
total_domain_loss += domain_loss.item()
# 源域准确率
source_preds = self.model.class_classifier(source_features)
correct_source += (source_preds.argmax(1) == source_labels).sum().item()
total_source += source_labels.size(0)
# 计算平均值
num_batches = len(source_loader)
metrics = {
'cls_loss': total_cls_loss / num_batches,
'domain_loss': total_domain_loss / num_batches,
'source_acc': correct_source / total_source
}
return metrics
def evaluate(self, test_loader):
"""
评估模型性能
在目标域上进行测试
"""
self.model.eval()
all_preds, all_labels = [], []
with torch.no_grad():
for data, labels in test_loader:
data, labels = data.to(self.device), labels.to(self.device)
# 提取特征并预测
features = self.model.extract_features(data)
outputs = self.model.class_classifier(features)
preds = outputs.argmax(1)
all_preds.extend(preds.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
# 计算指标
accuracy = np.mean(np.array(all_preds) == np.array(all_labels))
return {
'accuracy': accuracy,
'predictions': all_preds,
'labels': all_labels
}
def plot_results(self):
"""可视化训练结果"""
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# 损失曲线
axes[0].plot(self.history['cls_loss'], label='分类损失')
axes[0].plot(self.history['domain_loss'], label='域损失')
axes[0].set_xlabel('Epoch')
axes[0].set_ylabel('Loss')
axes[0].set_title('训练损失曲线')
axes[0].legend()
axes[0].grid(True)
# 准确率曲线
axes[1].plot(self.history['source_acc'], label='源域准确率')
axes[1].plot(self.history['target_acc'], label='目标域准确率')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Accuracy')
axes[1].set_title('准确率曲线')
axes[1].legend()
axes[1].grid(True)
# 混淆矩阵
if self.history.get('confusion_matrix') is not None:
sns.heatmap(self.history['confusion_matrix'], annot=True,
fmt='d', ax=axes[2], cmap='Blues')
axes[2].set_title('混淆矩阵')
axes[2].set_xlabel('预测')
axes[2].set_ylabel('真实')
plt.tight_layout()
plt.savefig('./dann_training_results.png', dpi=150)
plt.show()
def run_complete_experiment():
"""
完整实验流程演示
模拟场景:跨转速工况的轴承故障诊断
- 源域:转速 1800 RPM 下的轴承振动数据
- 目标域:转速 1500 RPM 下的轴承振动数据
"""
print("=" * 60)
print("DANN 跨域故障诊断实验")
print("=" * 60)
# ==================== 数据生成 ====================
np.random.seed(42)
torch.manual_seed(42)
# 模拟参数
n_samples = 500 # 每类样本数
n_features = 64 # 特征维度(模拟传感器通道)
n_classes = 4 # 故障类别数(正常 + 3种故障)
n_epochs = 100
# 生成源域数据(工况A:正常转速)
print("\n[1] 生成模拟数据...")
def generate_fault_data(n_samples, n_features, fault_type, noise_level=0.5):
"""生成模拟故障数据"""
t = np.linspace(0, 10 * np.pi, n_features)
if fault_type == 0: # 正常
freq = 1.0
elif fault_type == 1: # 内圈故障
freq = 2.3 # 特定频率成分
elif fault_type == 2: # 外圈故障
freq = 3.1
else: # 滚动体故障
freq = 4.7
X = []
for _ in range(n_samples):
signal = np.sin(freq * t) + noise_level * np.random.randn(n_features)
X.append(signal)
return np.array(X)
# 生成源域数据(高噪声)
source_data, source_labels = [], []
for fault_type in range(n_classes):
data = generate_fault_data(n_samples, n_features, fault_type, noise_level=0.8)
source_data.append(data)
source_labels.append(np.full(n_samples, fault_type))
source_data = np.vstack(source_data)
source_labels = np.concatenate(source_labels)
# 生成目标域数据(低噪声,分布略有不同)
target_data, target_labels = [], []
for fault_type in range(n_classes):
data = generate_fault_data(n_samples, n_features, fault_type, noise_level=0.3)
target_data.append(data)
target_labels.append(np.full(n_samples, fault_type))
target_data = np.vstack(target_data)
target_labels = np.concatenate(target_labels)
# 数据标准化
scaler = StandardScaler()
source_data = scaler.fit_transform(source_data)
target_data = scaler.transform(target_data)
# 转换为张量
source_data = torch.FloatTensor(source_data)
source_labels = torch.LongTensor(source_labels)
target_data = torch.FloatTensor(target_data)
target_labels = torch.LongTensor(target_labels)
# 数据加载器
source_dataset = TensorDataset(source_data, source_labels)
target_dataset = TensorDataset(target_data, target_labels)
source_loader = DataLoader(source_dataset, batch_size=32, shuffle=True)
target_loader = DataLoader(target_dataset, batch_size=32, shuffle=True)
# 划分测试集
test_size = 200
test_target_dataset = TensorDataset(target_data[:test_size], target_labels[:test_size])
test_loader = DataLoader(test_target_dataset, batch_size=32)
print(f" 源域样本数: {len(source_data)}")
print(f" 目标域样本数: {len(target_data)}")
print(f" 特征维度: {n_features}")
print(f" 故障类别数: {n_classes}")
# ==================== 模型训练 ====================
print("\n[2] 初始化DANN模型...")
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = DANN(input_dim=n_features, num_classes=n_classes, hidden_dims=[128, 64])
trainer = PHMDANNTrainer(model, device)
optimizer = optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=30, gamma=0.5)
print(f" 设备: {device}")
print(f" 模型参数量: {sum(p.numel() for p in model.parameters()):,}")
# ==================== 训练循环 ====================
print("\n[3] 开始训练...")
print("-" * 60)
for epoch in range(n_epochs):
# 动态调整lambda参数(从0渐增到1)
lambda_param = min(1.0, epoch / 30)
# 训练一个epoch
metrics = trainer.train_epoch(source_loader, target_loader, optimizer, lambda_param)
# 记录历史
trainer.history['cls_loss'].append(metrics['cls_loss'])
trainer.history['domain_loss'].append(metrics['domain_loss'])
trainer.history['source_acc'].append(metrics['source_acc'])
# 每20个epoch评估一次
if (epoch + 1) % 20 == 0:
eval_results = trainer.evaluate(test_loader)
trainer.history['target_acc'].append(eval_results['accuracy'])
print(f"Epoch {epoch+1:3d} | "
f"分类损失: {metrics['cls_loss']:.4f} | "
f"域损失: {metrics['domain_loss']:.4f} | "
f"源域准确率: {metrics['source_acc']:.4f} | "
f"目标域准确率: {eval_results['accuracy']:.4f}")
# ==================== 最终评估 ====================
print("\n[4] 最终评估结果:")
print("-" * 60)
final_results = trainer.evaluate(test_loader)
print(f" 目标域测试准确率: {final_results['accuracy']:.4f}")
# 绘制结果
trainer.plot_results()
# ==================== 对比实验 ====================
print("\n[5] 对比实验:baseline(无域自适应):")
print("-" * 60)
# 训练一个没有域判别器的baseline模型
class BaselineModel(nn.Module):
def __init__(self, input_dim, num_classes):
super().__init__()
self.features = nn.Sequential(
nn.Linear(input_dim, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
nn.Linear(64, num_classes)
)
def forward(self, x):
return self.features(x)
baseline = BaselineModel(n_features, n_classes).to(device)
baseline_optimizer = optim.Adam(baseline.parameters(), lr=0.001)
# 仅使用源域数据训练
for epoch in range(n_epochs):
for data, labels in source_loader:
data, labels = data.to(device), labels.to(device)
baseline_optimizer.zero_grad()
outputs = baseline(data)
loss = F.cross_entropy(outputs, labels)
loss.backward()
baseline_optimizer.zero_grad()
# 在目标域上评估baseline
baseline.eval()
correct, total = 0, 0
with torch.no_grad():
for data, labels in test_loader:
data, labels = data.to(device), labels.to(device)
outputs = baseline(data)
preds = outputs.argmax(1)
correct += (preds == labels).sum().item()
total += labels.size(0)
baseline_acc = correct / total
print(f" Baseline目标域准确率: {baseline_acc:.4f}")
print(f" DANN提升: +{(final_results['accuracy'] - baseline_acc)*100:.2f}%")
print("\n" + "=" * 60)
print("实验完成!")
print("=" * 60)
return model, trainer, final_results
# 运行实验
if __name__ == "__main__":
model, trainer, results = run_complete_experiment()
3.3 使用CORAL的简化实现
如果觉得DANN过于复杂,可以考虑更简单的CORAL方法:
python
class CORALModel(nn.Module):
"""
CORAL (Correlation Alignment) 模型
原理:对齐源域和目标域特征的协方差矩阵
优点:
- 无需对抗训练,训练稳定
- 实现简单,计算高效
- 只需一个损失项
"""
def __init__(self, input_dim, num_classes, hidden_dims=[128, 64]):
super().__init__()
# 特征提取器
self.feature_extractor = nn.Sequential(
nn.Linear(input_dim, hidden_dims[0]),
nn.BatchNorm1d(hidden_dims[0]),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dims[0], hidden_dims[1]),
nn.BatchNorm1d(hidden_dims[1]),
nn.ReLU(),
nn.Dropout(0.3)
)
# 分类器
self.classifier = nn.Linear(hidden_dims[1], num_classes)
def coral_loss(self, source_features, target_features):
"""
CORAL损失:对齐协方差矩阵
数学公式:
L_coral = ||C_s - C_t||_F^2 / (4 * d^2)
其中 C_s 和 C_t 分别是源域和目标域的协方差矩阵
"""
d = source_features.size(1)
# 去均值
source_c = source_features - source_features.mean(dim=0, keepdim=True)
target_c = target_features - target_features.mean(dim=0, keepdim=True)
# 计算协方差矩阵
source_cov = (source_c.T @ source_c) / (source_features.size(0) - 1)
target_cov = (target_c.T @ target_c) / (target_features.size(0) - 1)
# CORAL损失
loss = torch.norm(source_cov - target_cov, p='fro') ** 2
loss = loss / (4 * d * d)
return loss
def forward(self, x):
features = self.feature_extractor(x)
logits = self.classifier(features)
return logits, features
def train_coral(source_loader, target_loader, model, optimizer, alpha=1.0):
"""
训练CORAL模型
总损失 = 分类损失 + alpha * CORAL损失
"""
model.train()
total_cls_loss, total_coral_loss = 0, 0
target_iter = iter(target_loader)
for source_data, source_labels in source_loader:
try:
target_data, _ = next(target_iter)
except StopIteration:
target_iter = iter(target_loader)
target_data, _ = next(target_iter)
source_data, source_labels = source_data.cuda(), source_labels.cuda()
target_data = target_data.cuda()
optimizer.zero_grad()
# 前向传播
source_logits, source_features = model(source_data)
_, target_features = model(target_data)
# 分类损失
cls_loss = F.cross_entropy(source_logits, source_labels)
# CORAL损失
coral_loss = model.coral_loss(source_features, target_features)
# 总损失
loss = cls_loss + alpha * coral_loss
loss.backward()
optimizer.step()
total_cls_loss += cls_loss.item()
total_coral_loss += coral_loss.item()
return total_cls_loss / len(source_loader), total_coral_loss / len(source_loader)
4. PHM应用场景
4.1 跨工况故障诊断
问题描述:同一设备在不同转速、负载、温度等工况下运行,数据分布存在显著差异。
解决方案:使用DANN/CORAL提取工况不变的特征表示。
场景示例:
┌─────────────────────────────────────────────────────────┐
│ 轴承跨转速迁移 │
│ │
│ 源域:1800 RPM轴承振动数据(含故障标签) │
│ ↓ │
│ 特征提取器学习:工况无关的故障特征 │
│ ↓ │
│ 目标域:1200 RPM轴承振动数据(无标签) │
│ ↓ │
│ 输出:1200 RPM下的故障诊断结果 │
└─────────────────────────────────────────────────────────┘
实际数据特征:
- 不同转速下,故障特征频率会发生缩放
- 负载变化影响振动信号的幅值分布
- 温度影响导致传感器漂移
4.2 跨设备迁移
问题描述:新设备上线时没有足够的历史故障数据,需要从相似设备迁移知识。
典型案例:
| 迁移场景 | 源域 | 目标域 | 挑战 |
|---|---|---|---|
| 同型号不同机体 | 机组A数据 | 机组B数据 | 制造公差 |
| 同型号不同时段 | 历史数据 | 当前数据 | 磨损老化 |
| 不同型号同原理 | 大型风机 | 小型风机 | 尺度差异 |
处理策略:
- 提取高层次的物理特征(如共振频率、包络谱特征)
- 使用领域对抗学习减小设备特异性
- 结合物理先验知识约束特征空间
4.3 实际案例分析
案例:风力发电机齿轮箱故障诊断
背景:
- 齿轮箱故障会导致重大停机事故
- 不同风场的风速条件差异大
- 故障样本极其稀缺
方法:
1. 数据收集
- 风场A:丰富的故障数据(源域)
- 风场B:正常数据为主(目标域)
2. 网络设计
- 特征提取器:1D-CNN处理振动信号
- 域判别器:判断数据来源
- 类别分类器:识别故障类型
3. 训练策略
- 第一阶段:源域分类训练
- 第二阶段:域对抗微调
- 第三阶段:目标域伪标签自训练
4. 结果
- 源域准确率:95.2%
- 目标域准确率:89.7%(无迁移:62.3%)
- 提升:+27.4%
案例:数控机床刀具磨损监测
背景:
- 刀具状态直接影响加工质量
- 不同工件材料、不同切削参数下监测难度大
迁移学习方法:
- 工况感知特征提取:提取与切削参数无关的特征
- MMD正则化:最小化域间分布差异
- 渐进式迁移:从相似工况逐步扩展
5. 注意事项与总结
5.1 负迁移问题
什么是负迁移?
当源域和目标域差异过大时,迁移学习可能损害目标域的性能,而不是提升。
正常迁移:
源域知识 → 正向迁移 → 目标域性能提升
负迁移:
源域知识 → 误导性迁移 → 目标域性能下降
如何避免负迁移?
-
域相似性预检
- 在迁移前计算域间相似度(MMD、CORAL距离)
- 相似度低于阈值时不进行迁移
-
渐进式迁移
python# 从与目标域最相似的源域开始 source_domains = sort_by_similarity(target_domain) for source in source_domains: if similarity(source, target) > threshold: perform_transfer(source, target) -
多源域融合
- 避免单一源域带来的偏差
- 多个源域的知识融合更稳健
5.2 参数选择技巧
λ参数(域对齐权重)
| λ值 | 效果 | 适用场景 |
|---|---|---|
| 0.0-0.3 | 弱域对齐,侧重分类 | 域差异较小 |
| 0.5-1.0 | 中等域对齐 | 一般场景 |
| 1.0-2.0 | 强域对齐 | 域差异较大 |
建议:从λ=1.0开始,根据目标域准确率调整
学习率
python
# 推荐设置
optimizer = optim.Adam([
{'params': model.feature_extractor.parameters(), 'lr': 0.001},
{'params': model.class_classifier.parameters(), 'lr': 0.01}, # 分类器学习率更高
{'params': model.domain_classifier.parameters(), 'lr': 0.01}
], weight_decay=1e-4)
网络容量
| 源域数据量 | 建议网络深度 | 建议dropout |
|---|---|---|
| >5000 | 深网络 | 0.5 |
| 1000-5000 | 中等深度 | 0.3-0.4 |
| <1000 | 浅网络 | 0.1-0.2 |
5.3 与其他方法对比
| 方法 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| DANN | 对抗学习 | 效果最好,理论上保证 | 训练不稳定,调参难 | 域差异大 |
| CORAL | 协方差对齐 | 简单稳定,计算快 | 只能对齐二阶统计量 | 域差异中等 |
| MMD | 核方法 | 理论基础好 | 计算量大 | 小规模数据 |
| 伪标签 | 自训练 | 无需域假设 | 错误累积 | 有少量目标域数据 |
| 微调 | 有监督学习 | 简单 | 需要目标域标签 | 标签可获取时 |
5.4 最佳实践建议
PHM迁移学习实战 checklist:
□ 1. 数据探索:可视化源域和目标域分布
□ 2. 域相似性评估:计算MMD/CORAL距离
□ 3. 选择基线:简单方法(CORAL)先试
□ 4. 网络设计:特征提取器足够深/宽
□ 5. 训练策略:渐进式λ调整
□ 6. 监控指标:同时监控源域和目标域准确率
□ 7. 早停机制:目标域准确率下降时停止
□ 8. 结果验证:多种评估指标
5.5 总结
核心要点:
- 迁移学习解决PHM中的数据分布不一致问题
- DANN通过对抗训练学习域不变表示
- CORAL通过协方差对齐简化域自适应
- 负迁移是需要警惕的重要问题