首页 / 资讯中心 / 文章详情

联合分布适配JDA实战:跨域迁移学习从原理到代码避坑

联合分布适配JDA实战:跨域迁移学习从原理到代码避坑 ★ FEATURED ARTICLE
简介这份资源是联合分布适配JDA算法的代码实现包面向从事机器学习、迁移学习与域适应研究的开发者及学生帮助解决源域与目标域分布不一致时模型难以迁移应用的问题。压缩包共28个文件约53.52MB以mat数据文件与m脚本为主另含少量说明文档其中mat文件提供MNIST、USPS、PIE、COIL、Office-Caltech等常用跨域数据集m脚本则对应JDA主算法及多个实验运行入口便于直接复现与对比。目前已有1945人学习下载。通过研读代码读者可掌握JDA从PCA降维、协方差矩阵对齐到优化求解的完整流程理解如何拉近源域与目标域的联合分布差异并借助现成数据集快速验证跨域分类效果为图像识别、情感分析等迁移任务提供可复用的实现参考。1. 从一次跨域实验翻车说起JDA 到底能解决什么问题去年帮某实验室做设备故障诊断的迁移实验源域数据是实验室台架跑出来的振动信号目标域是现场机器采的。模型在源域准确率 96%换到目标域直接掉到 58%排查了两天以为是特征工程的问题最后发现根因是两边数据分布压根不在一个空间里——源域特征方差大、目标域方差小分类面直接偏了。这就是典型的域偏移问题而联合分布适配Joint Distribution AdaptationJDA就是冲着这个场景来的。这份 JDA 代码资源核心做的事就一件把源域和目标域的边缘分布、条件分布同时对齐让在源域训好的分类器能直接迁移到目标域上用。适合谁做故障诊断、脑电信号、图像分类这些跨域任务的开发者尤其是手头有标注源域数据、目标域只有少量或无标注的情况。代码结构不复杂但里面几个矩阵运算和迭代逻辑第一次看容易绕进去下面按能跑起来、能改动的顺序拆一遍。2. 环境准备与数据组织把源域和目标域喂进去2.1 依赖安装与版本确认这份代码依赖比较干净主要是数值计算和流形学习的基础库。我一般会先建一个独立环境避免和现有项目的 numpy 版本打架。# 创建虚拟环境Python 3.8 以上都行 conda create -n jda_env python3.9 conda activate jda_env # 核心依赖numpy 建议 1.21 以上scipy 用于矩阵分解 pip install numpy scipy scikit-learn装完之后建议跑一句python -c import numpy; print(numpy.__version__)确认版本。血泪经验是 numpy 1.24 之后np.float被移除了如果代码里有老写法会直接报 AttributeError遇到就改成float或np.float64。2.2 数据目录结构与格式约定代码默认的数据组织方式是按域分文件夹每个域一个.mat或.csv特征和标签分开存。常见做法是长这样data/ ├── source/ │ ├── features.mat # 源域特征矩阵shape(n_s, d) │ └── labels.mat # 源域标签shape(n_s,) └── target/ ├── features.mat # 目标域特征矩阵shape(n_t, d) └── labels.mat # 目标域标签仅评估用训练时不参与特征矩阵的行是样本、列是特征维度源域和目标域的特征维度必须一致这是 JDA 的硬性前提。如果维度对不上要么用 PCA 统一降维要么检查是不是特征提取阶段出了问题。标签文件在目标域里只用于最后算准确率训练过程中不能碰否则就是标签泄漏实验数据会虚高得离谱。2.3 加载与预处理的代码实现数据加载这块代码不长但有几个参数直接影响后续适配效果逐段看一下。import numpy as np from scipy.io import loadmat from sklearn.preprocessing import StandardScaler def load_domain(path): 加载单个域的特征和标签 features loadmat(path /features.mat)[features] labels loadmat(path /labels.mat)[labels].ravel() return features, labels # 加载源域和目标域 Xs, ys load_domain(data/source) Xt, yt load_domain(data/target) # 标准化用源域的均值和方差去变换目标域避免目标域信息泄漏 scaler StandardScaler() Xs scaler.fit_transform(Xs) Xt scaler.transform(Xt) # 注意是 transform不是 fit_transform print(f源域样本: {Xs.shape}, 目标域样本: {Xt.shape})这里的关键点是标准化必须用源域的统计量去变换目标域。如果两边各自 fit等于把目标域的分布信息提前泄露了适配结果会偏乐观。fit_transform和transform这一字之差在迁移学习里是原则问题。另外如果特征维度超过 200建议先做一次 PCA 降到 100 维左右再送进 JDA维度太高时 MMD 矩阵计算量大且容易过拟合。3. 核心算法拆解边缘分布和条件分布怎么同时对齐3.1 JDA 的数学骨架与代码映射JDA 的目标是找一个变换矩阵 A把源域和目标域映射到同一个子空间在这个子空间里同时最小化边缘分布差异和条件分布差异。数学上写成min ||A^T X (M0 Σ Mc) X^T A|| s.t. A^T X H X^T A I其中 M0 是边缘分布的 MMD 矩阵Mc 是每个类别的条件分布 MMD 矩阵H 是中心化矩阵。代码里对应三个核心函数compute_mmd_matrix算 M0compute_conditional_mmd算 Mcsolve_eigen解广义特征值。下面逐个看。3.2 边缘分布 MMD 矩阵的构造def compute_mmd_matrix(ns, nt): 构造边缘分布的 MMD 矩阵 M0 n ns nt M np.zeros((n, n)) # 源域内部块 M[:ns, :ns] 1.0 / (ns * ns) # 目标域内部块 M[ns:, ns:] 1.0 / (nt * nt) # 跨域块 M[:ns, ns:] -1.0 / (ns * nt) M[ns:, :ns] -1.0 / (ns * nt) return M这个矩阵的物理含义很直白源域样本之间、目标域样本之间是正贡献跨域样本之间是负贡献。最小化A^T X M X^T A就是在拉近两个域的整体分布。参数ns和nt分别是源域和目标域样本数注意这里用的是样本数不是特征数别传错了。3.3 条件分布适配与伪标签迭代条件分布适配是 JDA 比 TCA 多出来的部分也是效果提升的关键。它需要目标域的伪标签所以整个算法是迭代进行的先假设一个初始变换用最近邻分类器给目标域打伪标签再根据伪标签算条件 MMD更新变换循环几轮。def compute_conditional_mmd(ns, nt, ys, yt_pseudo, n_classes): 按类别构造条件分布 MMD 矩阵 n ns nt Mc np.zeros((n, n)) for c in range(n_classes): # 找出源域和目标域中属于第 c 类的样本索引 idx_s np.where(ys c)[0] idx_t np.where(yt_pseudo c)[0] ns_c, nt_c len(idx_s), len(idx_t) if ns_c 0 or nt_c 0: continue # 源域类内块 Mc[np.ix_(idx_s, idx_s)] 1.0 / (ns_c * ns_c) # 目标域类内块 Mc[np.ix_(idx_t ns, idx_t ns)] 1.0 / (nt_c * nt_c) # 跨域类间块 Mc[np.ix_(idx_s, idx_t ns)] -1.0 / (ns_c * nt_c) Mc[np.ix_(idx_t ns, idx_s)] -1.0 / (ns_c * nt_c) return Mc参数yt_pseudo是目标域的伪标签第一轮迭代时通常用源域训一个基分类器预测得到。n_classes是类别数必须和实际标签种类一致少一类会导致那类的分布完全不对齐。这里有个坑如果某个类别在目标域伪标签里一个样本都没有continue会跳过那一轮该类就不参与适配迭代几轮后可能越来越偏。3.4 广义特征值求解与变换矩阵def solve_eigen(X, M, H, dim30): 求解广义特征值问题返回前 dim 个最小特征值对应的向量 # 正则化项防止矩阵奇异 A X M X.T 1e-6 * np.eye(X.shape[0]) B X H X.T 1e-6 * np.eye(X.shape[0]) eigvals, eigvecs np.linalg.eigh(np.linalg.inv(B) A) # 取最小的 dim 个特征值对应的特征向量 idx np.argsort(eigvals)[:dim] W eigvecs[:, idx] return Wdim是降维后的子空间维度一般取 30 到 100 之间。太小会丢信息太大适配效果不明显。1e-6的正则化是防止矩阵不可逆如果数据里有常量特征列不加这个会直接报奇异矩阵错误。np.linalg.eigh要求输入是对称矩阵这里 A 和 B 都是对称的没问题。3.5 完整迭代流程串起来def jda_fit(Xs, ys, Xt, dim30, n_iter10, n_classes2): ns, nt Xs.shape[0], Xt.shape[0] X np.vstack([Xs, Xt]).T # 拼在一起shape(d, n) H np.eye(ns nt) - 1.0/(nsnt) * np.ones((nsnt, nsnt)) M0 compute_mmd_matrix(ns, nt) # 初始变换只用边缘分布 W solve_eigen(X, M0, H, dim) yt_pseudo None for it in range(n_iter): # 用当前变换投影训练基分类器 Zs W.T Xs.T Zt W.T Xt.T clf KNeighborsClassifier(n_neighbors1).fit(Zs, ys) yt_pseudo clf.predict(Zt) # 更新条件 MMD重新求解 Mc compute_conditional_mmd(ns, nt, ys, yt_pseudo, n_classes) W solve_eigen(X, M0 Mc, H, dim) return W, yt_pseudo迭代次数n_iter一般 5 到 10 轮就收敛了再多收益很小。每轮都用最新的 W 重新投影、重新打伪标签、重新算 Mc这是 JDA 和 TCA 的本质区别。跑完返回的 W 就是最终的变换矩阵yt_pseudo是最后一轮的目标域预测标签。4. 避坑与常见问题排查4.1 准确率不升反降现象跑完 JDA 目标域准确率比直接用源域分类器还低。原因通常是伪标签第一轮质量太差错误标签在迭代中被放大。解决第一轮不要直接用 1-NN换成在源域上交叉验证选出的最优分类器或者降低迭代次数到 3 轮先看趋势。4.2 矩阵奇异报错现象LinAlgError: Singular matrix。原因特征矩阵存在线性相关的列或者某类样本数太少导致 MMD 矩阵秩亏。解决加正则化项代码里已有1e-6或者先做 PCA 降维去掉冗余特征。4.3 目标域某类完全消失现象迭代几轮后目标域伪标签里某个类别一个样本都没有。原因初始变换对该类区分度不够1-NN 全判成了另一类。解决用类别平衡的采样策略或者在条件 MMD 里给稀有类加权。4.4 维度选择靠猜现象dim设 30 和设 100 结果差很多不知道选哪个。原因子空间维度和数据内在维度不匹配。解决画一个 dim 从 10 到 150 的准确率曲线选拐点。我一般会先跑一遍这个扫描别嫌麻烦。4.5 源域目标域样本数悬殊现象源域 5000 样本目标域 50 样本适配后效果很差。原因MMD 矩阵里目标域块的权重被稀释。解决在 M0 和 Mc 里对目标域块乘一个补偿系数或者对源域做下采样。5. 二次开发与改造思路这份代码的骨架很清晰改造空间主要在三个地方基分类器、距离度量和迭代策略。基分类器现在用的是 1-NN换成 SVM 或随机森林通常能提升伪标签质量。改的时候注意jda_fit里clf的 fit 和 predict 接口保持一致就行。我试过把 1-NN 换成 SVM在某个跨域数据集上目标域准确率从 72% 提到了 79%但训练时间翻了 3 倍得权衡。距离度量方面MMD 对均值差异敏感如果两个域的方差差异更大可以换成 CORAL 损失或者二阶 MMD。改法是重写compute_mmd_matrix把一阶统计量换成协方差矩阵的 Frobenius 范数。迭代策略上现在是无条件迭代固定轮数。可以加一个早停每轮算一下目标域伪标签的置信度连续两轮变化小于阈值就停。这样既省时间又避免过拟合。验证改造效果时我习惯留一个 hold-out 的目标域标注样本不参与任何训练和适配只在最后评估用。每次改完跑三遍取平均单次结果波动太大不能信。从那以后我每次做迁移实验都强制先跑一遍源域直接测试的基线再跑 JDA两个数放一起看提升幅度。如果提升不到 5 个点先回头查数据预处理别急着调算法参数。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站