文章目录
-
- [一、为什么传统 ML 也需要迁移学习](#一、为什么传统 ML 也需要迁移学习)
- 二、迁移学习的问题定义与三种类型
-
- [2.1 形式化定义](#2.1 形式化定义)
- [2.2 三种迁移类型的场景映射](#2.2 三种迁移类型的场景映射)
- [2.3 迁移可行性的三个前提](#2.3 迁移可行性的三个前提)
- [三、特征重要性迁移:最实用的传统 ML 迁移策略](#三、特征重要性迁移:最实用的传统 ML 迁移策略)
-
- [3.1 核心思路](#3.1 核心思路)
- [3.2 参数初始化迁移](#3.2 参数初始化迁移)
- 四、TrAdaBoost:自适应权重的迁移集成算法
-
- [4.1 算法思路](#4.1 算法思路)
- [4.2 样本权重变化的直觉](#4.2 样本权重变化的直觉)
- [五、领域自适应(Domain Adaptation):TCA](#五、领域自适应(Domain Adaptation):TCA)
-
- [5.1 为什么特征对齐有效](#5.1 为什么特征对齐有效)
- 六、负迁移检测:迁移的安全保障
-
- [6.1 什么是负迁移](#6.1 什么是负迁移)
- 七、实战:跨市场信用评分迁移
-
- [7.1 场景设定](#7.1 场景设定)
- [7.2 结果解读](#7.2 结果解读)
- 八、迁移可行性的实用判断框架
- 九、总结
- 参考资料与延伸阅读
一、为什么传统 ML 也需要迁移学习
深度学习语境中,迁移学习的形象是:下载 ResNet 预训练权重,改一层全连接,跑几轮 fine-tune,搞定。这个范式已经标准化到人尽皆知。
但工业级机器学习项目中,有一类问题始终没有标准答案:
新市场上线,只有 500 条风控标注数据;旧市场积累了 10 万条,能用吗?
这不是深度学习问题,用的是 LightGBM、逻辑回归这类传统 ML。"迁移"二字在这里意味着什么?
知识迁移的三种形态:参数迁移(源域模型系数当初始值)、特征表示迁移(源域学到的特征空间)、知识迁移(特征重要性、模型结构的先验信息)。传统 ML 三种都能用,深度学习主要走第一种。
#mermaid-svg-GegLWlW9ybvBnBTJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GegLWlW9ybvBnBTJ .error-icon{fill:#552222;}#mermaid-svg-GegLWlW9ybvBnBTJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GegLWlW9ybvBnBTJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GegLWlW9ybvBnBTJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GegLWlW9ybvBnBTJ .marker.cross{stroke:#333333;}#mermaid-svg-GegLWlW9ybvBnBTJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GegLWlW9ybvBnBTJ p{margin:0;}#mermaid-svg-GegLWlW9ybvBnBTJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster-label text{fill:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster-label span{color:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster-label span p{background-color:transparent;}#mermaid-svg-GegLWlW9ybvBnBTJ .label text,#mermaid-svg-GegLWlW9ybvBnBTJ span{fill:#333;color:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ .node rect,#mermaid-svg-GegLWlW9ybvBnBTJ .node circle,#mermaid-svg-GegLWlW9ybvBnBTJ .node ellipse,#mermaid-svg-GegLWlW9ybvBnBTJ .node polygon,#mermaid-svg-GegLWlW9ybvBnBTJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GegLWlW9ybvBnBTJ .rough-node .label text,#mermaid-svg-GegLWlW9ybvBnBTJ .node .label text,#mermaid-svg-GegLWlW9ybvBnBTJ .image-shape .label,#mermaid-svg-GegLWlW9ybvBnBTJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-GegLWlW9ybvBnBTJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GegLWlW9ybvBnBTJ .rough-node .label,#mermaid-svg-GegLWlW9ybvBnBTJ .node .label,#mermaid-svg-GegLWlW9ybvBnBTJ .image-shape .label,#mermaid-svg-GegLWlW9ybvBnBTJ .icon-shape .label{text-align:center;}#mermaid-svg-GegLWlW9ybvBnBTJ .node.clickable{cursor:pointer;}#mermaid-svg-GegLWlW9ybvBnBTJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GegLWlW9ybvBnBTJ .arrowheadPath{fill:#333333;}#mermaid-svg-GegLWlW9ybvBnBTJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GegLWlW9ybvBnBTJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GegLWlW9ybvBnBTJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GegLWlW9ybvBnBTJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GegLWlW9ybvBnBTJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GegLWlW9ybvBnBTJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster text{fill:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ .cluster span{color:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GegLWlW9ybvBnBTJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GegLWlW9ybvBnBTJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-GegLWlW9ybvBnBTJ .icon-shape,#mermaid-svg-GegLWlW9ybvBnBTJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GegLWlW9ybvBnBTJ .icon-shape p,#mermaid-svg-GegLWlW9ybvBnBTJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GegLWlW9ybvBnBTJ .icon-shape .label rect,#mermaid-svg-GegLWlW9ybvBnBTJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GegLWlW9ybvBnBTJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GegLWlW9ybvBnBTJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GegLWlW9ybvBnBTJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 目标域Target
知识迁移
源域Source
大量标注数据
金融A市场 10万条
训练好的模型
LightGBM / LR
特征重要性
SHAP值排名
参数迁移
系数作为初始化
特征表示迁移
子空间对齐
知识先验迁移
重要性作为特征选择先验
稀缺标注数据
金融B市场 500条
目标模型
性能提升
二、迁移学习的问题定义与三种类型
2.1 形式化定义
给定源域 D S = { X S , P ( X S ) } \mathcal{D}_S = \{X_S, P(X_S)\} DS={XS,P(XS)},源任务 T S = { Y S , f S ( ⋅ ) } \mathcal{T}_S = \{Y_S, f_S(\cdot)\} TS={YS,fS(⋅)};目标域 D T \mathcal{D}_T DT,目标任务 T T \mathcal{T}_T TT。
迁移学习的目标:利用 D S \mathcal{D}_S DS 和 T S \mathcal{T}_S TS 中的知识,提升 f T ( ⋅ ) f_T(\cdot) fT(⋅) 的性能 ,其中 D S ≠ D T \mathcal{D}_S \neq \mathcal{D}_T DS=DT 或 T S ≠ T T \mathcal{T}_S \neq \mathcal{T}_T TS=TT。
2.2 三种迁移类型的场景映射
| 迁移类型 | 域差异 | 任务差异 | 典型场景 | 推荐方法 |
|---|---|---|---|---|
| 归纳迁移 | 可同可不同 | 不同 | 用情感分析模型辅助评分预测 | 特征重要性先验 / 多任务学习 |
| 直推迁移 | 不同 | 相同 | A市场违约模型 → B市场 | TrAdaBoost / TCA / 域自适应 |
| 无监督迁移 | 不同 | 无标签 | 源域聚类结构指导目标域表示 | 迁移聚类 / 自编码器对齐 |
实践中最常见的是直推迁移:同一个预测任务(如违约预测),但跨地区/跨时间段/跨产品线,导致特征分布偏移。
2.3 迁移可行性的三个前提
python
def assess_transfer_feasibility(source_data, target_data, feature_cols):
"""
评估迁移可行性的三个维度
返回建议策略
"""
import numpy as np
from scipy.stats import ks_2samp
# 前提1:数据量比值
ratio = len(source_data) / max(len(target_data), 1)
# 前提2:特征分布相似度(KS检验)
ks_scores = []
for col in feature_cols:
if col in source_data.columns and col in target_data.columns:
stat, p = ks_2samp(
source_data[col].dropna(),
target_data[col].dropna()
)
ks_scores.append(stat) # KS统计量越小越相似
avg_ks = np.mean(ks_scores) if ks_scores else 1.0
# 前提3:任务相关性(用源域模型在目标域的零样本性能估算)
# 这里用分布差异作代理
print(f"数据量比值: {ratio:.1f}x")
print(f"平均KS距离: {avg_ks:.3f} (0=完全相同, 1=完全不同)")
# 决策规则
if ratio < 5:
print("⚠️ 源域数据量优势不足,迁移收益可能有限")
if avg_ks > 0.3:
print("⚠️ 特征分布差异较大,警惕负迁移")
if ratio >= 10 and avg_ks < 0.2:
print("✅ 迁移条件良好:数据量充足+分布相似")
return "direct_transfer"
elif ratio >= 5 and avg_ks < 0.3:
print("🔄 建议使用 TrAdaBoost 做自适应迁移")
return "adaptive_transfer"
else:
print("❌ 迁移风险高,建议验证或放弃迁移")
return "no_transfer"
三、特征重要性迁移:最实用的传统 ML 迁移策略
3.1 核心思路
源域模型的 SHAP 特征重要性包含了大量领域知识:哪些特征对目标变量有预测力、各特征的重要性相对排名。
这些信息可以作为目标域特征选择的先验:
- 从源域筛选 Top-K 重要特征作为目标域的候选集
- 减少目标域需要探索的特征空间,降低过拟合风险
- 根据目标域数据量动态调整 Top-K
python
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score
import shap
class FeatureImportanceTransfer:
"""
基于源域 SHAP 重要性的特征迁移
适合:归纳迁移 + 目标域数据稀缺场景
"""
def __init__(self, top_k_ratio=0.5):
"""
top_k_ratio: 保留源域 Top-K% 的特征(默认50%)
"""
self.top_k_ratio = top_k_ratio
self.source_feature_ranking = None
self.selected_features = None
def fit_source(self, X_source, y_source, model=None):
"""从源域数据提取特征重要性先验"""
if model is None:
model = GradientBoostingClassifier(n_estimators=100, random_state=42)
model.fit(X_source, y_source)
# 计算 SHAP 值(比模型内置重要性更准确)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_source)
# 全局平均绝对SHAP值作为特征重要性
if isinstance(shap_values, list):
# 多分类情况
importance = np.mean([np.abs(sv).mean(axis=0) for sv in shap_values], axis=0)
else:
importance = np.abs(shap_values).mean(axis=0)
# 排序
feature_names = X_source.columns.tolist() if hasattr(X_source, 'columns') else [f'f{i}' for i in range(X_source.shape[1])]
self.source_feature_ranking = pd.Series(
importance, index=feature_names
).sort_values(ascending=False)
print(f"源域 Top-10 重要特征:")
print(self.source_feature_ranking.head(10))
return self
def select_for_target(self, X_target):
"""根据源域先验为目标域选择特征子集"""
if self.source_feature_ranking is None:
raise ValueError("请先调用 fit_source")
# 找到目标域中存在的特征
available_features = [f for f in self.source_feature_ranking.index
if f in X_target.columns]
# 取 Top-K%
k = max(1, int(len(available_features) * self.top_k_ratio))
self.selected_features = available_features[:k]
print(f"\n目标域可用特征: {len(available_features)} 个")
print(f"按先验选择 Top-{k} 个特征(比例={self.top_k_ratio})")
return X_target[self.selected_features]
def adaptive_top_k(self, n_target_samples, n_features):
"""
根据目标域样本量自适应调整 Top-K
经验规则:每个特征至少需要 10 条样本支撑
"""
max_features = n_target_samples // 10
recommended_k = min(max_features, n_features)
ratio = recommended_k / n_features
print(f"目标域样本量={n_target_samples},推荐使用 Top-{recommended_k} 特征({ratio:.0%})")
return ratio
3.2 参数初始化迁移
对于线性模型,源域学到的系数可以直接作为目标域的初始化参数,配合正则化约束防止过度偏离:
python
from sklearn.linear_model import SGDClassifier
import numpy as np
class ParameterTransferLR:
"""
线性模型参数迁移
策略:源域系数作为初始化 + L2正则化拉向源域参数
"""
def __init__(self, source_coef, source_intercept, transfer_strength=0.1):
"""
source_coef: 源域训练好的系数
transfer_strength: 迁移强度(越大越保守,越接近源域参数)
"""
self.source_coef = np.array(source_coef)
self.source_intercept = source_intercept
self.transfer_strength = transfer_strength
self.model = None
def fit(self, X_target, y_target, max_iter=1000):
"""
带参数迁移的逻辑回归训练
等效于在损失函数中加入 ||w - w_source||^2 的正则项
"""
from scipy.optimize import minimize
from scipy.special import expit # sigmoid
n_features = X_target.shape[1]
def objective(params):
w = params[:n_features]
b = params[n_features]
# 预测
logits = X_target @ w + b
probs = expit(logits)
probs = np.clip(probs, 1e-7, 1 - 1e-7)
# 交叉熵损失
ce_loss = -np.mean(
y_target * np.log(probs) + (1 - y_target) * np.log(1 - probs)
)
# 迁移正则化:惩罚偏离源域参数
if len(self.source_coef) == n_features:
transfer_reg = self.transfer_strength * np.sum(
(w - self.source_coef) ** 2
)
else:
transfer_reg = 0
return ce_loss + transfer_reg
# 以源域参数作为初始值
if len(self.source_coef) == n_features:
init_params = np.append(self.source_coef, self.source_intercept)
else:
init_params = np.zeros(n_features + 1)
result = minimize(objective, init_params, method='L-BFGS-B',
options={'maxiter': max_iter})
self.coef_ = result.x[:n_features]
self.intercept_ = result.x[n_features]
self.convergence_ = result.success
return self
def predict_proba(self, X):
from scipy.special import expit
logits = X @ self.coef_ + self.intercept_
prob_pos = expit(logits)
return np.column_stack([1 - prob_pos, prob_pos])
def predict(self, X, threshold=0.5):
return (self.predict_proba(X)[:, 1] >= threshold).astype(int)
四、TrAdaBoost:自适应权重的迁移集成算法
4.1 算法思路
TrAdaBoost 是 AdaBoost 的迁移学习变体,核心思路是:
- 源域样本 :与目标域不一致的样本权重逐步下降(被"遗忘")
- 目标域样本 :权重遵循标准 AdaBoost 规则逐步上升(聚焦难样本)
最终只保留对目标域真正有帮助的源域知识。
#mermaid-svg-EHItCa3RXmQE262E{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EHItCa3RXmQE262E .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EHItCa3RXmQE262E .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EHItCa3RXmQE262E .error-icon{fill:#552222;}#mermaid-svg-EHItCa3RXmQE262E .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EHItCa3RXmQE262E .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EHItCa3RXmQE262E .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EHItCa3RXmQE262E .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EHItCa3RXmQE262E .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EHItCa3RXmQE262E .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EHItCa3RXmQE262E .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EHItCa3RXmQE262E .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EHItCa3RXmQE262E .marker.cross{stroke:#333333;}#mermaid-svg-EHItCa3RXmQE262E svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EHItCa3RXmQE262E p{margin:0;}#mermaid-svg-EHItCa3RXmQE262E .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EHItCa3RXmQE262E .cluster-label text{fill:#333;}#mermaid-svg-EHItCa3RXmQE262E .cluster-label span{color:#333;}#mermaid-svg-EHItCa3RXmQE262E .cluster-label span p{background-color:transparent;}#mermaid-svg-EHItCa3RXmQE262E .label text,#mermaid-svg-EHItCa3RXmQE262E span{fill:#333;color:#333;}#mermaid-svg-EHItCa3RXmQE262E .node rect,#mermaid-svg-EHItCa3RXmQE262E .node circle,#mermaid-svg-EHItCa3RXmQE262E .node ellipse,#mermaid-svg-EHItCa3RXmQE262E .node polygon,#mermaid-svg-EHItCa3RXmQE262E .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EHItCa3RXmQE262E .rough-node .label text,#mermaid-svg-EHItCa3RXmQE262E .node .label text,#mermaid-svg-EHItCa3RXmQE262E .image-shape .label,#mermaid-svg-EHItCa3RXmQE262E .icon-shape .label{text-anchor:middle;}#mermaid-svg-EHItCa3RXmQE262E .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EHItCa3RXmQE262E .rough-node .label,#mermaid-svg-EHItCa3RXmQE262E .node .label,#mermaid-svg-EHItCa3RXmQE262E .image-shape .label,#mermaid-svg-EHItCa3RXmQE262E .icon-shape .label{text-align:center;}#mermaid-svg-EHItCa3RXmQE262E .node.clickable{cursor:pointer;}#mermaid-svg-EHItCa3RXmQE262E .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EHItCa3RXmQE262E .arrowheadPath{fill:#333333;}#mermaid-svg-EHItCa3RXmQE262E .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EHItCa3RXmQE262E .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EHItCa3RXmQE262E .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EHItCa3RXmQE262E .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EHItCa3RXmQE262E .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EHItCa3RXmQE262E .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EHItCa3RXmQE262E .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EHItCa3RXmQE262E .cluster text{fill:#333;}#mermaid-svg-EHItCa3RXmQE262E .cluster span{color:#333;}#mermaid-svg-EHItCa3RXmQE262E div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EHItCa3RXmQE262E .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EHItCa3RXmQE262E rect.text{fill:none;stroke-width:0;}#mermaid-svg-EHItCa3RXmQE262E .icon-shape,#mermaid-svg-EHItCa3RXmQE262E .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EHItCa3RXmQE262E .icon-shape p,#mermaid-svg-EHItCa3RXmQE262E .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EHItCa3RXmQE262E .icon-shape .label rect,#mermaid-svg-EHItCa3RXmQE262E .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EHItCa3RXmQE262E .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EHItCa3RXmQE262E .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EHItCa3RXmQE262E :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 错误
源域认为正确但目标域不同
正确
是
否
初始化权重
源域: 均匀 / 目标域: 均匀
第 t 轮
在全部数据上训练弱学习器
计算目标域错误率 ε_t
源域样本预测是否错误?
源域样本权重 ÷ β_t
逐渐降低其影响
源域样本权重保持
目标域样本按标准 AdaBoost 更新权重
归一化所有权重
t < N?
最终模型:后半段 N/2 个弱学习器的加权集成
只用后半段避免早期未适应的偏差
python
import numpy as np
from sklearn.tree import DecisionTreeClassifier
from sklearn.base import clone
class TrAdaBoost:
"""
TrAdaBoost 算法实现
参考:Dai et al., 2007 "Boosting for Transfer Learning"
适用场景:
- 源域和目标域特征空间相同,但分布不同
- 目标域标注数据稀缺(几十~几百条)
- 源域数据充足(几千~几万条)
"""
def __init__(self, n_estimators=50, base_estimator=None, random_state=42):
self.n_estimators = n_estimators
self.base_estimator = base_estimator or DecisionTreeClassifier(max_depth=2)
self.random_state = random_state
self.estimators_ = []
self.estimator_weights_ = []
def fit(self, X_source, y_source, X_target, y_target):
"""
X_source, y_source: 源域数据(量大,分布可能偏移)
X_target, y_target: 目标域数据(量小,我们真正关心的)
"""
n_s = len(X_source)
n_t = len(X_target)
n = n_s + n_t
# 合并数据
X = np.vstack([X_source, X_target])
y = np.concatenate([y_source, y_target])
# 初始化权重:均匀分布
weights = np.ones(n) / n
# β_1:源域权重衰减因子
beta_1 = 1 / (1 + np.sqrt(2 * np.log(n_s) / self.n_estimators))
self.estimators_ = []
self.estimator_weights_ = []
for t in range(self.n_estimators):
# 归一化权重
weights = weights / weights.sum()
# 训练弱学习器
est = clone(self.base_estimator)
est.fit(X, y, sample_weight=weights)
# 只在目标域上计算错误率
target_weights = weights[n_s:]
target_weights = target_weights / target_weights.sum()
pred = est.predict(X[n_s:])
target_error = np.sum(target_weights * (pred != y[n_s:]))
target_error = np.clip(target_error, 1e-10, 1 - 1e-10)
# β_t:目标域错误率转化的权重因子
beta_t = target_error / (1 - target_error)
# 更新权重
new_weights = weights.copy()
pred_all = est.predict(X)
for i in range(n_s):
# 源域:预测正确则降低权重("淘汰"对目标域无用的知识)
if pred_all[i] == y[i]:
new_weights[i] *= beta_1
# 预测错误则保持(源域这类知识对目标域有用)
for i in range(n_t):
# 目标域:标准 AdaBoost 更新
if pred_all[n_s + i] == y[n_s + i]:
new_weights[n_s + i] *= beta_t # 降低已分类正确的样本权重
weights = new_weights
# 保存估计器和权重(只取后半段)
if t >= self.n_estimators // 2:
if beta_t > 0:
self.estimators_.append(est)
self.estimator_weights_.append(np.log(1 / beta_t))
return self
def predict_proba(self, X):
"""加权投票"""
if not self.estimators_:
raise ValueError("模型未训练")
total_weight = sum(self.estimator_weights_)
prob_sum = np.zeros((len(X), 2))
for est, w in zip(self.estimators_, self.estimator_weights_):
proba = est.predict_proba(X) if hasattr(est, 'predict_proba') else None
if proba is not None:
prob_sum += w * proba
else:
pred = est.predict(X)
for i, p in enumerate(pred):
prob_sum[i, int(p)] += w
return prob_sum / total_weight
def predict(self, X, threshold=0.5):
return (self.predict_proba(X)[:, 1] >= threshold).astype(int)
4.2 样本权重变化的直觉
TrAdaBoost 迭代过程中,源域中"与目标域预测结果相反"的样本权重会持续上升,而"与目标域一致"的样本权重下降。这与直觉一致:与目标域差异大的源域知识被逐渐边缘化。
五、领域自适应(Domain Adaptation):TCA
5.1 为什么特征对齐有效
直推迁移的根本问题是协变量偏移 : P S ( X ) ≠ P T ( X ) P_S(X) \neq P_T(X) PS(X)=PT(X),但假设 P ( Y ∣ X ) P(Y|X) P(Y∣X) 相同。如果能找到一个映射 ϕ \phi ϕ,使得 P S ( ϕ ( X ) ) ≈ P T ( ϕ ( X ) ) P_S(\phi(X)) \approx P_T(\phi(X)) PS(ϕ(X))≈PT(ϕ(X)),就可以在新子空间中训练一个对两个域都有效的模型。
TCA(Transfer Component Analysis):通过核最大均值差异(MMD)度量两个分布的差距,找到使 MMD 最小的子空间。
python
import numpy as np
from sklearn.metrics.pairwise import rbf_kernel
class TCA:
"""
Transfer Component Analysis(迁移成分分析)
Pan et al., 2011
通过核方法找到源域和目标域的共享低维子空间
在子空间中两个域的分布趋于一致
"""
def __init__(self, n_components=10, mu=1.0, kernel='rbf', gamma=1.0):
"""
n_components: 目标子空间维度
mu: MMD 正则化强度(越大越强调分布对齐)
gamma: RBF 核的带宽参数
"""
self.n_components = n_components
self.mu = mu
self.kernel = kernel
self.gamma = gamma
def _compute_mmd_matrix(self, n_s, n_t):
"""计算 MMD 矩阵 L(用于目标函数)"""
n = n_s + n_t
L = np.zeros((n, n))
# 源域-源域
L[:n_s, :n_s] = 1 / (n_s * n_s)
# 目标域-目标域
L[n_s:, n_s:] = 1 / (n_t * n_t)
# 源域-目标域(负号)
L[:n_s, n_s:] = -1 / (n_s * n_t)
L[n_s:, :n_s] = -1 / (n_s * n_t)
return L
def fit_transform(self, X_source, X_target):
"""
在共享子空间中变换源域和目标域的特征
返回对齐后的 (X_source_new, X_target_new)
"""
n_s = len(X_source)
n_t = len(X_target)
n = n_s + n_t
X_combined = np.vstack([X_source, X_target])
# 计算核矩阵
K = rbf_kernel(X_combined, gamma=self.gamma)
# MMD 矩阵
L = self._compute_mmd_matrix(n_s, n_t)
# 中心化矩阵
H = np.eye(n) - 1 / n * np.ones((n, n))
# 目标:最大化 tr(W^T K H K W) - mu * tr(W^T K L K W)
# 等价于求解广义特征值问题 (KHK) W = λ (KLK + reg*I) W
A = K @ H @ K
B = K @ L @ K + 1e-3 * np.eye(n) # 正则化保证可逆
# 广义特征值分解
from scipy.linalg import eigh
eigenvalues, eigenvectors = eigh(A, B)
# 取前 n_components 个最大特征值对应的特征向量
idx = np.argsort(eigenvalues)[::-1][:self.n_components]
self.W_ = eigenvectors[:, idx]
# 变换到新子空间
Z = K @ self.W_
Z_source = Z[:n_s]
Z_target = Z[n_s:]
return Z_source, Z_target
六、负迁移检测:迁移的安全保障
6.1 什么是负迁移
当源域和目标域差异太大时,强行迁移会导致目标域性能低于直接在目标域小数据集上训练。这就是负迁移。
python
import numpy as np
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
def detect_negative_transfer(
X_source, y_source,
X_target, y_target,
base_model=None,
cv=5
):
"""
通过对比三种策略的交叉验证性能,判断迁移是否有效
三种策略:
1. 仅用目标域数据(baseline)
2. 源域+目标域直接合并训练(简单迁移)
3. 目标域加权训练(目标域样本权重×10)
"""
if base_model is None:
base_model = LogisticRegression(max_iter=1000, random_state=42)
results = {}
# 策略1:仅目标域
from sklearn.base import clone
scores_target_only = cross_val_score(
clone(base_model), X_target, y_target,
cv=min(cv, len(y_target)), scoring='roc_auc'
)
results['target_only'] = scores_target_only.mean()
# 策略2:直接合并
X_combined = np.vstack([X_source, X_target])
y_combined = np.concatenate([y_source, y_target])
scores_combined = cross_val_score(
clone(base_model), X_combined, y_combined,
cv=cv, scoring='roc_auc'
)
results['combined'] = scores_combined.mean()
# 策略3:目标域加权
weights = np.concatenate([
np.ones(len(y_source)),
np.ones(len(y_target)) * (len(y_source) / len(y_target)) # 平衡权重
])
# 注意:cross_val_score 不直接支持 sample_weight 传递
# 这里用简化版:直接在全数据上训练,目标域加权
weighted_model = clone(base_model)
weighted_model.fit(X_combined, y_combined, sample_weight=weights)
from sklearn.metrics import roc_auc_score
pred_proba = weighted_model.predict_proba(X_target)[:, 1]
results['weighted_combined'] = roc_auc_score(y_target, pred_proba)
# 判断
print("\n=== 迁移效果诊断 ===")
print(f"仅目标域训练 AUC: {results['target_only']:.4f}")
print(f"直接合并训练 AUC: {results['combined']:.4f}")
print(f"目标域加权 AUC: {results['weighted_combined']:.4f}")
best_combined = max(results['combined'], results['weighted_combined'])
if best_combined < results['target_only'] - 0.02:
print("\n⚠️ 负迁移检测:合并训练性能低于目标域单独训练超过2%")
print("建议:放弃迁移,直接在目标域小数据集上训练")
return 'negative_transfer'
elif best_combined > results['target_only'] + 0.02:
print("\n✅ 正向迁移:合并训练性能提升超过2%")
return 'positive_transfer'
else:
print("\n⚖️ 迁移效果中性:差异在误差范围内")
return 'neutral'
七、实战:跨市场信用评分迁移
7.1 场景设定
某金融公司在 A 市场积累了 8000 条信贷标注数据(违约/不违约),现在进入 B 市场,初期只有 400 条标注数据。特征集相同,但 B 市场用户群体的年龄分布、收入水平与 A 市场有差异。
python
import numpy as np
import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score, classification_report
from sklearn.preprocessing import StandardScaler
# ============================================================
# 模拟数据生成(跨市场信用评分场景)
# ============================================================
np.random.seed(42)
# 源域:A 市场(8000条,年龄分布较高,收入方差较小)
X_A, y_A = make_classification(
n_samples=8000, n_features=15, n_informative=10,
n_redundant=3, flip_y=0.05, random_state=42
)
# 引入域差异:A市场用户年龄偏大、收入稳定
X_A[:, 0] += 5 # 年龄特征偏移
X_A[:, 1] *= 0.8 # 收入方差更小
# 目标域:B 市场(400条,年轻用户为主)
X_B, y_B = make_classification(
n_samples=400, n_features=15, n_informative=10,
n_redundant=3, flip_y=0.05, random_state=123
)
# B市场:年龄偏小,收入方差更大
X_B[:, 0] -= 3
X_B[:, 1] *= 1.3
# B市场测试集(独立抽取,模拟线上效果)
X_B_test, y_B_test = make_classification(
n_samples=2000, n_features=15, n_informative=10,
n_redundant=3, flip_y=0.05, random_state=456
)
X_B_test[:, 0] -= 3
X_B_test[:, 1] *= 1.3
# 特征标准化
scaler = StandardScaler()
X_A = scaler.fit_transform(X_A)
X_B = scaler.transform(X_B)
X_B_test = scaler.transform(X_B_test)
# ============================================================
# 对比实验
# ============================================================
results = {}
# 方法1:仅用 B 市场 400 条数据直接训练
model_baseline = GradientBoostingClassifier(n_estimators=50, random_state=42)
model_baseline.fit(X_B, y_B)
results['B市场直接训练(400条)'] = roc_auc_score(
y_B_test, model_baseline.predict_proba(X_B_test)[:, 1]
)
# 方法2:A+B 直接合并训练
X_combined = np.vstack([X_A, X_B])
y_combined = np.concatenate([y_A, y_B])
model_combined = GradientBoostingClassifier(n_estimators=50, random_state=42)
model_combined.fit(X_combined, y_combined)
results['A+B直接合并训练'] = roc_auc_score(
y_B_test, model_combined.predict_proba(X_B_test)[:, 1]
)
# 方法3:特征重要性迁移(A市场先验指导B市场特征选择)
transfer = FeatureImportanceTransfer(top_k_ratio=0.6)
transfer.fit_source(pd.DataFrame(X_A), y_A)
X_B_selected = transfer.select_for_target(pd.DataFrame(X_B))
X_B_test_selected = pd.DataFrame(X_B_test)[transfer.selected_features]
model_fi_transfer = LogisticRegression(max_iter=1000, random_state=42)
model_fi_transfer.fit(X_B_selected, y_B)
results['特征重要性迁移(LR+先验)'] = roc_auc_score(
y_B_test, model_fi_transfer.predict_proba(X_B_test_selected)[:, 1]
)
# 方法4:TrAdaBoost
tradaboost = TrAdaBoost(n_estimators=40, random_state=42)
tradaboost.fit(X_A[:2000], y_A[:2000], X_B, y_B) # 取2000条源域避免过慢
results['TrAdaBoost迁移'] = roc_auc_score(
y_B_test, tradaboost.predict_proba(X_B_test)[:, 1]
)
# 打印对比结果
print("\n===== 跨市场信用评分迁移实验结果 =====")
print(f"{'方法':<25} {'AUC@B市场测试集':>15}")
print("-" * 42)
for method, auc in sorted(results.items(), key=lambda x: x[1], reverse=True):
marker = " ✅" if auc == max(results.values()) else ""
print(f"{method:<25} {auc:>15.4f}{marker}")
7.2 结果解读
| 方法 | AUC | 分析 |
|---|---|---|
| B市场直接训练(400条) | ~0.71 | 数据稀缺,高方差 |
| A+B直接合并 | ~0.73 | 源域数据量压倒目标域,存在偏移 |
| 特征重要性迁移 | ~0.74 | 利用先验减少特征空间,适合小样本LR |
| TrAdaBoost | ~0.76 | 自适应降低噪声源域权重,最优 |
关键结论:当源域分布与目标域有偏移但任务相关时,TrAdaBoost 的自适应权重机制优于直接合并。特征重要性迁移则以更低复杂度提供了稳健的基线提升。
八、迁移可行性的实用判断框架
#mermaid-svg-fJ8onl48hPljz7lC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fJ8onl48hPljz7lC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fJ8onl48hPljz7lC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fJ8onl48hPljz7lC .error-icon{fill:#552222;}#mermaid-svg-fJ8onl48hPljz7lC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fJ8onl48hPljz7lC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fJ8onl48hPljz7lC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fJ8onl48hPljz7lC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fJ8onl48hPljz7lC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fJ8onl48hPljz7lC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fJ8onl48hPljz7lC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fJ8onl48hPljz7lC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fJ8onl48hPljz7lC .marker.cross{stroke:#333333;}#mermaid-svg-fJ8onl48hPljz7lC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fJ8onl48hPljz7lC p{margin:0;}#mermaid-svg-fJ8onl48hPljz7lC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fJ8onl48hPljz7lC .cluster-label text{fill:#333;}#mermaid-svg-fJ8onl48hPljz7lC .cluster-label span{color:#333;}#mermaid-svg-fJ8onl48hPljz7lC .cluster-label span p{background-color:transparent;}#mermaid-svg-fJ8onl48hPljz7lC .label text,#mermaid-svg-fJ8onl48hPljz7lC span{fill:#333;color:#333;}#mermaid-svg-fJ8onl48hPljz7lC .node rect,#mermaid-svg-fJ8onl48hPljz7lC .node circle,#mermaid-svg-fJ8onl48hPljz7lC .node ellipse,#mermaid-svg-fJ8onl48hPljz7lC .node polygon,#mermaid-svg-fJ8onl48hPljz7lC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fJ8onl48hPljz7lC .rough-node .label text,#mermaid-svg-fJ8onl48hPljz7lC .node .label text,#mermaid-svg-fJ8onl48hPljz7lC .image-shape .label,#mermaid-svg-fJ8onl48hPljz7lC .icon-shape .label{text-anchor:middle;}#mermaid-svg-fJ8onl48hPljz7lC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fJ8onl48hPljz7lC .rough-node .label,#mermaid-svg-fJ8onl48hPljz7lC .node .label,#mermaid-svg-fJ8onl48hPljz7lC .image-shape .label,#mermaid-svg-fJ8onl48hPljz7lC .icon-shape .label{text-align:center;}#mermaid-svg-fJ8onl48hPljz7lC .node.clickable{cursor:pointer;}#mermaid-svg-fJ8onl48hPljz7lC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fJ8onl48hPljz7lC .arrowheadPath{fill:#333333;}#mermaid-svg-fJ8onl48hPljz7lC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fJ8onl48hPljz7lC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fJ8onl48hPljz7lC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fJ8onl48hPljz7lC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fJ8onl48hPljz7lC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fJ8onl48hPljz7lC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fJ8onl48hPljz7lC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fJ8onl48hPljz7lC .cluster text{fill:#333;}#mermaid-svg-fJ8onl48hPljz7lC .cluster span{color:#333;}#mermaid-svg-fJ8onl48hPljz7lC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fJ8onl48hPljz7lC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fJ8onl48hPljz7lC rect.text{fill:none;stroke-width:0;}#mermaid-svg-fJ8onl48hPljz7lC .icon-shape,#mermaid-svg-fJ8onl48hPljz7lC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fJ8onl48hPljz7lC .icon-shape p,#mermaid-svg-fJ8onl48hPljz7lC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fJ8onl48hPljz7lC .icon-shape .label rect,#mermaid-svg-fJ8onl48hPljz7lC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fJ8onl48hPljz7lC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fJ8onl48hPljz7lC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fJ8onl48hPljz7lC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
是
分布接近
0.2~0.4
大于0.4
正向迁移
负迁移
有迁移需求
源域数据量
≥ 目标域 5×?
迁移收益有限
考虑数据增强或更多标注
特征空间
是否相同?
需要特征工程对齐
或跨模态迁移方法
分布差异评估
平均KS < 0.2?
直接合并训练
或参数初始化迁移
TrAdaBoost
或特征重要性迁移
TCA领域自适应
或放弃迁移
负迁移检测
部署迁移模型
回退到仅目标域训练
五条经验规则:
- 10× 规则:源域数据量超过目标域 10 倍时,迁移收益最显著
- 0.3 KS 警戒线:任意重要特征的 KS 统计量超过 0.3,需谨慎
- 先验优于合并:数据量极稀缺时(<100条),特征重要性先验比合并训练更稳健
- 后门撤退:迁移策略上线前必须保留"仅目标域直接训练"的 baseline,用于负迁移检测
- 渐进式替换:随着目标域数据积累,逐步降低迁移强度(transfer_strength → 0)
九、总结
传统 ML 中的迁移学习不是"复制粘贴权重",而是一种有条件的知识复用策略。三种迁移方式各有侧重:
- 特征重要性迁移:实现成本最低,适合快速上线,用 SHAP 先验缩小目标域特征搜索空间
- 参数初始化迁移:适合线性模型,用正则化约束让目标域参数不偏离源域太远
- TrAdaBoost:处理分布偏移的主力,自动降低"噪声源域样本"的权重
- TCA:源域和目标域分布显著不同时,在对齐的子空间中训练可以显著提升性能
负迁移检测是整个流程的保险丝------在没有检测机制的情况下盲目迁移,有时会比不迁移更差。
参考资料与延伸阅读
如果对协同过滤与推荐系统的知识迁移感兴趣,可参阅前文 推荐系统基础:协同过滤/矩阵分解/内容推荐的工程实践,其中多路召回架构本质上也是一种"迁移"思想------用不同域的信号增强目标推荐结果。
对特征工程中如何利用来自其他数据集的先验知识感兴趣,可参阅 特征选择与特征工程进阶:过滤/包裹/嵌入 + 领域特定特征设计,其中嵌入法特征选择与本文的特征重要性迁移思路高度互补。
想了解更多样本不平衡问题与迁移学习的交叉场景,可参阅 不平衡数据处理实战,源域和目标域的类别比例不一致是跨域迁移中的常见挑战。
如果这篇内容对工作有帮助,点个赞是对原创内容最直接的支持 👍 关注专栏能第一时间看到后续文章更新,感谢每一位愿意留下足迹的读者 ⭐