ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于DANN的轴承跨工况故障诊断:Python实战与避坑指南

基于DANN的轴承跨工况故障诊断:Python实战与避坑指南 简介这份资源面向计算机、人工智能及相关专业的学生、教师与企业技术人员提供基于迁移学习DANN模型的轴承故障诊断完整实现方案用于解决不同工况下数据分布差异导致的诊断精度下降问题适合作为毕业设计、课程设计、项目演示或进阶学习的参考。压缩包共35个文件约23KB以18个Python源码为核心涵盖数据加载、模型定义、训练与测试脚本另含zbak备份、xml配置、yml与gitignore等工程文件以及README说明文档结构清晰便于快速上手。已有147人学习下载。读者可获得可直接运行的DANN迁移诊断代码、配套数据集与模型文件、数据归一化与图像变换等工具脚本以及分模块的目录组织方式便于理解域适应机制并在此基础上修改扩展实现其他工况或任务的故障诊断功能。1. 迁移学习DANN做轴承故障诊断为什么换个转速模型就集体翻车同一台电机转速从 1200 rpm 调到 1800 rpm振动信号里那些冲击成分的周期、幅值、频谱分布全变了。你在 1200 rpm 数据上把 CNN 训到 99% 准确率直接丢到 1800 rpm 的测试集上准确率掉到 60% 出头——这不是模型没学好是它学到的特征跟工况绑死了。轴承故障诊断落地时最头疼的就是这件事标注数据永远集中在少数几个工况而设备实际运行要覆盖一堆转速和负载组合重新标一遍代价极高。迁移学习里的 DANNDomain-Adversarial Neural Network域对抗神经网络就是冲着这个场景来的。它的思路不复杂让特征提取器学到的表示既能分辨故障类别又分辨不出数据来自哪个工况。前者靠标签分类损失保证后者靠一个域判别器和梯度反转层GRL来对抗。最终特征对工况失忆对故障敏感跨工况泛化就上来了。这套东西在轴承故障诊断里属于直推式迁移学习的典型用法——源域有标签、目标域无标签目标域数据在训练时就参与进来做域对齐。这篇面向的是手里有 CWRU 或类似轴承数据集、想用 Python 把 DANN 跑通并真正用起来的工程师。我会把数据怎么切、模型怎么搭、GRL 怎么实现、参数怎么调、跨工况实验怎么设计讲清楚源码结构、文档说明、数据集组织方式都会落到可复现的粒度。新手能照着搭出第一版熟手能直接看到边界条件和那些容易翻车的地方。2. DANN 的对抗逻辑与轴承数据的域划分2.1 域、任务、域偏移在轴承场景里到底指什么先把概念对齐不然后面参数全是玄学。在轴承故障诊断里一个域通常由工况定义转速 负载 采样位置。CWRU 数据集常见的就是 0/1/2/3 hp 负载配 1730~1797 rpm 这几个组合每个组合下又有内圈、外圈、滚动体、正常四类外圈还分 6 点、3 点、12 点位置。源域 有标签的那批工况数据目标域 想诊断但没标签的那批工况数据。域偏移domain shift体现在两个层面。一是边缘分布偏移不同转速下振动信号的幅值分布、频谱能量集中区不一样。二是条件分布偏移同一类故障在不同转速下的时频特征形态也有差异。DANN 主要对齐的是边缘分布通过域判别器对条件分布偏移只能间接缓解这是它的能力边界后面避坑章节会展开。任务task就是故障分类标签空间在源域和目标域是共享的——都是那几类故障。如果目标域出现了源域没有的故障类型那就是开集问题DANN 原版处理不了得换思路。2.2 为什么选 DANN 而不是 MMD、CORAL 或微调迁移学习做故障诊断常见几条路线基于统计距离的MMD、CORAL、基于对抗的DANN、CDAN、基于微调的fine-tune 预训练模型。选型要看你的约束。MMD 类方法实现简单直接最小化源域和目标域特征的分布距离但它对核函数和带宽敏感多类、多工况时对齐效果不稳定。CORAL 对齐二阶统计量计算便宜适合分布差异主要是协方差漂移的场景但表达能力有限。微调需要目标域有少量标签如果你的目标域真的一点标签都没有微调无从下手。DANN 的优势在于域判别器是学出来的不需要手工设计距离度量对复杂非线性偏移更鲁棒。代价是训练不稳定对抗训练本身就有博弈加上梯度反转层的系数要调。我一般会先跑一个 MMD 基线如果 MMD 效果够用就不上 DANN只有当 MMD 明显拉胯、且目标域确实无标签时才值得投入 DANN 的调参成本。2.3 用 CWRU 构造跨工况任务的划分脚本数据组织是复现的第一步。下面这段脚本把 CWRU 的 .mat 文件读进来按工况分组切窗构造源域/目标域对。假设你已经把 CWRU 的 mat 文件按负载_转速_故障类型命名放在data/raw下。import os import numpy as np import scipy.io as sio from sklearn.preprocessing import StandardScaler # 采样频率 12kHz切窗长度 1024重叠 50% WIN_LEN 1024 STEP 512 def load_signal(mat_path, key_candidates(DE_time, X)): 读取 CWRU mat优先取驱动端加速度信号 DE_time mat sio.loadmat(mat_path) for k in key_candidates: for real_key in mat: if k in real_key: return mat[real_key].ravel().astype(np.float32) raise KeyError(fno signal key found in {mat_path}) def slice_windows(sig, winWIN_LEN, stepSTEP): 滑动切窗返回 (N, win) n (len(sig) - win) // step 1 idx np.arange(0, n * step, step)[:n] return np.stack([sig[i:i win] for i in idx]) def build_domain(root, load, rpm, fault): 按工况故障类型聚合一个域的数据 folder os.path.join(root, f{load}_{rpm}_{fault}) windows [] for f in os.listdir(folder): if f.endswith(.mat): sig load_signal(os.path.join(folder, f)) windows.append(slice_windows(sig)) return np.concatenate(windows, axis0) # 源域0hp / 1730rpm目标域3hp / 1750rpm FAULTS [normal, inner, outer, ball] src np.concatenate([build_domain(data/raw, 0, 1730, f) for f in FAULTS]) y_src np.concatenate([np.full(len(build_domain(data/raw, 0, 1730, f)), i) for i, f in enumerate(FAULTS)]) tgt np.concatenate([build_domain(data/raw, 3, 1750, f) for f in FAULTS]) y_tgt np.concatenate([np.full(len(build_domain(data/raw, 3, 1750, f)), i) for i, f in enumerate(FAULTS)]) # 标准化只用源域统计量避免目标域信息泄漏 scaler StandardScaler().fit(src) src scaler.transform(src) tgt scaler.transform(tgt) np.savez(data/processed/src.npz, xsrc, yy_src) np.savez(data/processed/tgt.npz, xtgt, yy_tgt)逻辑说明load_signal兼容不同 mat 的键名CWRU 里驱动端信号键名通常带DE_time。slice_windows用 1024 点窗、512 步长这是轴承诊断里比较通用的设置窗长要覆盖至少几个冲击周期。build_domain把同一工况同一故障的多个文件拼起来。参数说明WIN_LEN和STEP是最关键的两个。窗太短比如 256频谱分辨率不够故障特征频率分不开窗太长比如 4096单窗内工况变化被平均掉且样本数骤减。1024 在 12kHz 采样下对应约 85ms能覆盖内圈故障特征频率的几个周期。标准化只用源域 fit这点很多人会写错——如果用全体数据 fit目标域信息就泄漏进训练了跨工况实验的结论会虚高。3. 搭 DANN特征提取器、域判别器和梯度反转层3.1 一维 CNN 特征提取器的结构选择轴承信号是一维时序特征提取器用 1D-CNN 比 2D-CNN 更自然也不用做时频变换。结构上我一般用三层卷积 全局平均池化卷积核从大到小比如 64、16、3这样第一层感受野大能抓到冲击的包络后面层抓细节。import torch import torch.nn as nn class FeatureExtractor(nn.Module): def __init__(self, in_len1024, feat_dim128): super().__init__() self.net nn.Sequential( nn.Conv1d(1, 32, kernel_size64, stride8, padding28), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size16, stride2, padding7), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(64, feat_dim, kernel_size3, stride1, padding1), nn.BatchNorm1d(feat_dim), nn.ReLU(), nn.AdaptiveAvgPool1d(1) # 全局平均池化输出 (B, feat_dim, 1) ) def forward(self, x): # x: (B, 1, L) return self.net(x).squeeze(-1) # (B, feat_dim)逻辑说明第一层大核 stride 8 快速降采样减少计算量同时保留冲击结构BN 层对幅值漂移有稳定作用跨工况时这点很重要。全局平均池化替代全连接参数量小、过拟合风险低特征维度固定为feat_dim。参数说明feat_dim是特征维度128 是常用起点太小欠拟合、太大对抗训练更难收敛。第一层kernel_size64, stride8意味着原始 1024 点被压到约 128 长度感受野约 64 点5ms 左右能覆盖高频冲击。如果你的采样率不是 12kHz这些数要按比例调。3.2 梯度反转层GRL的实现与反向传播细节GRL 是 DANN 的核心前向恒等反向把梯度取负并乘一个系数。实现上用一个自定义 autograd Function。from torch.autograd import Function class GradReverse(Function): staticmethod def forward(ctx, x, lambd): ctx.lambd lambd return x.view_as(x) staticmethod def backward(ctx, grad_output): # 反向时梯度取负并缩放 return -ctx.lambd * grad_output, None def grad_reverse(x, lambd1.0): return GradReverse.apply(x, lambd)逻辑说明forward原样返回backward返回-lambd * grad。这样域判别器正常最小化自己的分类损失但梯度传到特征提取器时符号翻转特征提取器就往让域判别器分不出的方向走形成对抗。参数说明lambd是反转强度。实践中不固定用动态调度lambd 2 / (1 exp(-10 * p)) - 1p 是训练进度 0→1。早期 lambd 小先让分类任务稳住后期 lambd 大加强域对齐。这个调度是 DANN 原论文的做法直接固定 lambd1 往往早期就崩。3.3 域判别器与整体训练循环域判别器就是个二分类器输入特征输出源域/目标域。整体模型把三部分串起来。class DomainDiscriminator(nn.Module): def __init__(self, feat_dim128, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(feat_dim, hidden), nn.ReLU(), nn.Linear(hidden, 2) # 二分类源域 / 目标域 ) def forward(self, x): return self.net(x) class DANN(nn.Module): def __init__(self, n_class4, feat_dim128): super().__init__() self.feat FeatureExtractor(feat_dimfeat_dim) self.cls nn.Linear(feat_dim, n_class) self.disc DomainDiscriminator(feat_dim) def forward(self, x, lambd1.0): f self.feat(x) logits self.cls(f) rev grad_reverse(f, lambd) d_logits self.disc(rev) return logits, d_logits def train_dann(src_loader, tgt_loader, epochs100, lr1e-3, devicecuda): model DANN().to(device) opt torch.optim.Adam(model.parameters(), lrlr, weight_decay1e-4) ce nn.CrossEntropyLoss() for ep in range(epochs): model.train() p ep / epochs lambd 2 / (1 np.exp(-10 * p)) - 1 # 动态反转系数 for (xs, ys), (xt, _) in zip(src_loader, tgt_loader): xs, ys, xt xs.to(device), ys.to(device), xt.to(device) xs xs.unsqueeze(1); xt xt.unsqueeze(1) opt.zero_grad() logits_s, d_s model(xs, lambd) _, d_t model(xt, lambd) loss_cls ce(logits_s, ys) # 域标签源域0目标域1 d_label torch.cat([torch.zeros(xs.size(0)), torch.ones(xt.size(0))]).long().to(device) d_pred torch.cat([d_s, d_t], dim0) loss_domain ce(d_pred, d_label) loss loss_cls loss_domain loss.backward() opt.step() return model逻辑说明每个 batch 同时取源域和目标域数据源域算分类损失源域目标域一起算域判别损失。注意域判别器的梯度经过 GRL 反转后才进特征提取器分类损失正常回传。两个损失直接相加权重都是 1这是原版做法。参数说明lr1e-3配 Adam 是起点对抗训练对学习率敏感太大容易震荡。weight_decay1e-4抑制过拟合。epochs100视数据量调整样本少时 50 就够。lambd调度里的 10 是陡峭度越大后期反转越猛一般 10 够用。域标签约定源域 0、目标域 1别搞反反了对抗方向就错了。4. 跨工况实验设计与参数调优的实操细节4.1 源域/目标域配对与评估协议跨工况实验最忌讳的是随机划分。正确做法是按工况整组划分源域一个工况、目标域另一个工况测试集就是目标域全部数据。常见配对0hp→3hp、1730rpm→1750rpm、负载 0→负载 3。我一般会跑多组配对取平均单组结果波动大容易得出错误结论。评估时报告三个数源域测试准确率看有没有欠拟合、目标域准确率核心指标、以及目标域上的混淆矩阵看哪几类容易混。只报一个目标域准确率不够混淆矩阵能告诉你域对齐是不是把某些类对齐糊了。def evaluate(model, loader, devicecuda): model.eval() correct, total 0, 0 preds, labels [], [] with torch.no_grad(): for x, y in loader: x x.to(device).unsqueeze(1) logits, _ model(x, lambd0.0) # 评估时不需要对抗 pred logits.argmax(1).cpu().numpy() preds.append(pred); labels.append(y.numpy()) correct (pred y.numpy()).sum() total len(y) return correct / total, np.concatenate(preds), np.concatenate(labels)逻辑说明评估时lambd0域判别分支不影响只取分类输出。返回准确率和预测/标签数组方便后面画混淆矩阵。参数说明评估必须用model.eval()固定 BN 的 running stats否则结果不稳定。目标域评估时不要做任何标签相关的操作保持无标签设定。4.2 学习率、batch size 与反转系数的联合调参这三个参数互相耦合单独调一个往往没用。我的经验顺序是先固定 lambd 调度调 lr 和 batch size 让源域分类先收敛再调 lambd 的陡峭度和上限让目标域准确率爬升。参数常用范围影响调整方向lr1e-4 ~ 3e-3太大对抗震荡太小对齐慢源域不收敛就调大目标域震荡就调小batch size32 ~ 128太小域判别噪声大太大对齐粗样本少用 32样本多用 64/128lambd 上限0.5 ~ 1.0太大特征被域对齐主导分类退化目标域不涨就调大源域掉就调小feat_dim64 ~ 256太小表达不足太大过拟合从 128 起调一个典型翻车场景lr1e-2 配 lambd 快速升到 1训练前 10 个 epoch 源域准确率就崩了。原因是反转梯度太强特征提取器被域判别器带跑。解决是把 lr 降到 1e-3lambd 调度陡峭度从 10 降到 5给分类任务留出稳定期。4.3 用 t-SNE 验证域对齐是否真的发生准确率涨了不代表域对齐做好了可能只是碰巧。用 t-SNE 把源域和目标域特征画出来看两类点是否混在一起。对齐好的话同一故障的源域和目标域样本会聚成一簇对齐差的话源域和目标域会分成两团。from sklearn.manifold import TSNE import matplotlib.pyplot as plt def plot_tsne(model, src_x, tgt_x, devicecuda): model.eval() with torch.no_grad(): fs model.feat(src_x.to(device).unsqueeze(1)).cpu().numpy() ft model.feat(tgt_x.to(device).unsqueeze(1)).cpu().numpy() all_f np.concatenate([fs, ft]) emb TSNE(n_components2, perplexity30, initpca).fit_transform(all_f) n len(fs) plt.scatter(emb[:n, 0], emb[:n, 1], ctab:blue, labelsource, alpha0.5) plt.scatter(emb[n:, 0], emb[n:, 1], ctab:red, labeltarget, alpha0.5) plt.legend(); plt.savefig(tsne.png, dpi150)逻辑说明取特征提取器输出源域目标域拼一起降维。颜色区分域如果红蓝点充分混合说明对齐有效。参数说明perplexity30适合几百到几千样本样本更多可以调到 50。initpca比随机初始化稳定。t-SNE 只用于定性判断别拿它当定量指标。5. 避坑与排查DANN 训练里那些让人怀疑人生的时刻5.1 目标域准确率不升反降现象训练几十个 epoch源域准确率 99%目标域一直在 50% 上下晃甚至比不做迁移还低。原因多半是 lambd 升太快特征提取器过早被域判别器主导把类别信息也一起抹掉了。或者域判别器太强隐藏层太大对抗一边倒。解决把 lambd 调度陡峭度从 10 降到 5上限压到 0.5域判别器隐藏层从 64 降到 32先冻结域判别器训 10 个 epoch 让分类稳住再开启对抗。我一般会监控源域准确率一旦它开始掉就说明对抗过头了。5.2 源域和目标域准确率一起崩现象训练 loss 不降两个域准确率都接近随机。原因学习率太大导致梯度爆炸或者标准化做错了比如用了全体数据 fit或者根本没标准化。轴承信号幅值差异大不标准化几乎必崩。解决lr 降到 1e-3 甚至 5e-4检查标准化是否只用源域统计量加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0)。另外确认输入维度对不对unsqueeze(1)别忘了1D-CNN 要 (B, 1, L)。5.3 换一组工况配对就完全失效现象0hp→3hp 效果不错换成 1hp→2hp 就崩。原因不同配对的域偏移程度不一样超参不是通用的。另外样本量差异也会影响某些工况数据少域判别器学不好。解决每组配对单独调 lambd 上限偏移大的配对用更强的对齐。数据少的工况可以做数据增强加噪声、时间平移、幅值缩放。别指望一套超参打天下跨工况实验本来就要按配对调。5.4 评估时忘了切 eval 模式现象训练时目标域准确率看着还行单独评估时掉一截。原因BN 层在 train 模式下用 batch 统计量eval 模式用 running stats两者不一致。对抗训练里 batch 组成特殊源目标混train 模式的 BN 统计量本身就偏。解决评估前一定model.eval()并且用torch.no_grad()。如果 running stats 和实际分布差太多考虑换 LayerNorm 或 GroupNorm对小 batch 更稳。5.5 把目标域标签泄漏进训练现象目标域准确率高得离谱换一批数据就原形毕露。原因标准化用了全体数据、或者划分时目标域样本混进了源域、或者调参时反复看目标域测试结果导致过拟合测试集。解决标准化严格只用源域 fit源域目标域文件物理隔离调参用验证集目标域测试集只在最后跑一次。这条是血泪经验泄漏一次整个实验结论就废了。6. 把 DANN 用扎实从单组实验到可复用的诊断流程跑通一组配对只是起点真正落地要把它变成可复用的流程。我现在的习惯是写一个配置文件驱动的主脚本把工况配对、超参、随机种子都外置每组实验独立跑、独立存结果最后统一汇总。这样换数据集、换配对不用改代码。import yaml, itertools, json def run_grid(cfg_path): cfg yaml.safe_load(open(cfg_path)) results [] for src_d, tgt_d in cfg[pairs]: for lr, lambd_max in itertools.product(cfg[lr], cfg[lambd_max]): for seed in cfg[seeds]: acc_s, acc_t, _, _ train_and_eval( src_d, tgt_d, lrlr, lambd_maxlambd_max, seedseed) results.append(dict(srcsrc_d, tgttgt_d, lrlr, lambdlambd_max, seedseed, acc_srcacc_s, acc_tgtacc_t)) # 按配对聚合报告均值和标准差 json.dump(results, open(results.json, w), indent2) return results逻辑说明外层遍历工况配对内层遍历超参和种子。每个组合独立训练评估结果落 JSON。种子至少跑 3 个报告均值±标准差单次结果不可信。参数说明seeds建议 [0, 1, 2]lr网格 [5e-4, 1e-3, 2e-3]lambd_max网格 [0.5, 1.0]。网格别铺太大先粗调再细调。验证方法上除了目标域准确率我会额外看两个东西一是特征的可分性用目标域特征训一个线性分类器不参与训练如果线性可分性高说明特征质量好二是对齐的稳定性不同种子下目标域准确率的标准差标准差大说明训练不稳定得回头调 lambd 调度。一个具体技巧训练后期最后 20 个 epoch把 lambd 逐渐降回 0让分类头在已经对齐的特征上再收敛一下。这招在几组配对上都能涨 1~2 个点原理是避免对抗噪声干扰最终分类边界。代码上就是在lambd调度后面乘一个衰减因子。最后说个我踩过的坑别一上来就追求 SOTA 数字。先把源域→目标域的单组实验做扎实把 t-SNE 画出来确认对齐真的发生再谈调参。我早期跳过验证直接调参结果数字好看但换个种子就崩白折腾两周。稳扎稳打每组实验都留证据比追指标靠谱得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表