迁移学习与传统 ML:知识迁移在小样本与跨域场景的应用

文章目录

一、为什么传统 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 特征重要性包含了大量领域知识:哪些特征对目标变量有预测力、各特征的重要性相对排名。

这些信息可以作为目标域特征选择的先验

  1. 从源域筛选 Top-K 重要特征作为目标域的候选集
  2. 减少目标域需要探索的特征空间,降低过拟合风险
  3. 根据目标域数据量动态调整 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领域自适应

或放弃迁移
负迁移检测
部署迁移模型
回退到仅目标域训练

五条经验规则

  1. 10× 规则:源域数据量超过目标域 10 倍时,迁移收益最显著
  2. 0.3 KS 警戒线:任意重要特征的 KS 统计量超过 0.3,需谨慎
  3. 先验优于合并:数据量极稀缺时(<100条),特征重要性先验比合并训练更稳健
  4. 后门撤退:迁移策略上线前必须保留"仅目标域直接训练"的 baseline,用于负迁移检测
  5. 渐进式替换:随着目标域数据积累,逐步降低迁移强度(transfer_strength → 0)

九、总结

传统 ML 中的迁移学习不是"复制粘贴权重",而是一种有条件的知识复用策略。三种迁移方式各有侧重:

  • 特征重要性迁移:实现成本最低,适合快速上线,用 SHAP 先验缩小目标域特征搜索空间
  • 参数初始化迁移:适合线性模型,用正则化约束让目标域参数不偏离源域太远
  • TrAdaBoost:处理分布偏移的主力,自动降低"噪声源域样本"的权重
  • TCA:源域和目标域分布显著不同时,在对齐的子空间中训练可以显著提升性能

负迁移检测是整个流程的保险丝------在没有检测机制的情况下盲目迁移,有时会比不迁移更差。


参考资料与延伸阅读

如果对协同过滤与推荐系统的知识迁移感兴趣,可参阅前文 推荐系统基础:协同过滤/矩阵分解/内容推荐的工程实践,其中多路召回架构本质上也是一种"迁移"思想------用不同域的信号增强目标推荐结果。

对特征工程中如何利用来自其他数据集的先验知识感兴趣,可参阅 特征选择与特征工程进阶:过滤/包裹/嵌入 + 领域特定特征设计,其中嵌入法特征选择与本文的特征重要性迁移思路高度互补。

想了解更多样本不平衡问题与迁移学习的交叉场景,可参阅 不平衡数据处理实战,源域和目标域的类别比例不一致是跨域迁移中的常见挑战。


如果这篇内容对工作有帮助,点个赞是对原创内容最直接的支持 👍 关注专栏能第一时间看到后续文章更新,感谢每一位愿意留下足迹的读者 ⭐