🤖 第18章 迁移学习
📄 一句话概括

两类的形状一模一样,只有比例变了:训练集正类占两成,测试集占五成。 不加权的模型训练准确率 0.9300、测试准确率 0.8275; 按类别加权后训练掉到 0.9200,测试升到 0.8925。 变的其实只是一条边界:从 x 等于 +0.5654 挪到 -0.0781,而两类样本的实际中点恰好是 -0.0781。

📝 本节和前面几节的关系

上一节的权重 w(x) 是输入的函数,得先估密度比。这一节的权重只跟标签有关,跟 x 无关。 代价小了,可利用的信息也少了。 情形是这样的:输入的分布在训练和测试时完全一样,y 给定 x 的规律也一样, 唯独「各类样本在总体里占多少」变了。 比如训练集里坏样本只占两成(因为做过欠采样),而线上坏样本其实占一半。 这类变化在实际中极其常见,因为很多人为了让训练集平衡, 会主动重采样——于是训练比例和真实比例天生就对不上。 这一节说的就是怎么在预测时把比例改回来。

一、只有比例变了

把假设写清楚,和上一节对照着看:

量 训练时 测试时 变没变
p(x 给定 y) 两个高斯 同样两个高斯 没变
p(y) 正类 0.2 正类 0.5 变了
注意这里变的是 p(y),不是 p(x)。
因为 p(x) 是两个 p(x 给定 y) 按 p(y) 混合出来的,
所以 p(x) 其实也变了——但它的变化完全由 p(y) 造成,没有额外的信息。
这个区别决定了方法:如果变化全部来自 p(y),那么重要性权重就只依赖 y。
写成式子:
w(x, y) = p_te(x, y) / p_tr(x, y)
        = [ p_te(x 给定 y) * p_te(y) ] / [ p_tr(x 给定 y) * p_tr(y) ]
        = p_te(y) / p_tr(y)          (因为条件分布相同,前一项约掉)

x 从式子里消失了。 这是一个巨大的简化:不用估密度比,只要数一数两边的类别比例就行。 本节数据里,正类权重 0.5 除以 0.2 等于 2.500,负类权重 0.5 除以 0.8 等于 0.625。

二、加权之后,训练准确率反而降了

把这两个权重丢进加权最小二乘,结果有点反直觉:

做法 训练准确率 测试准确率
不加权 0.9300 0.8275
按类别加权 0.9200 0.8925
训练那一栏降了 0.01,测试那一栏涨了 0.065。
降是应该的:训练集的真实比例就是两成对八成,你按五成对五成去优化,在训练集上必然吃点亏。
涨也是应该的:测试集才是目标,权重就是照着测试集的比例设的。
这件事的教学价值在于:它把「训练集表现」和「目标分布上的表现」彻底分开了。
如果你拿训练集做模型选择,加权版永远选不上——可它才是上线后更好的那个。
顺带说一句,这也是为什么验证集必须按目标分布采样。
如果验证集沿用训练集的两成比例,你会得出「加权有害」的结论,然后选错模型。

三、变的其实只是一条边界

权重全为正类 2.5、负类 0.625,等价于把正类样本复制四份、负类复制一份。 样本复制不会改变两类各自的形状,只会把决策边界往多数类那边推。 实测:

量 数值
不加权时的边界 x = +0.5654
加权后的边界 x = -0.0781
两类样本的实际中心 +1.1284 和 -1.2847
两个中心的中点 -0.0781
加权后的边界和两类中心的中点,最后四位完全一样。
这不是巧合。两类协方差相同时,贝叶斯最优边界就是中点;
最小二乘分类器在这种对称数据上恰好能把它找出来。
不加权时的 +0.5654 偏了 0.64,偏的方向正是训练集里的少数类。
换句话说:加权没有改变模型的形状,只是把它的偏置挪回了正中间。
这也是为什么这一节只需要 ⭐⭐——机制简单,难的是想到「比例也会变」这件事。

四、先验相同时,它什么也不做

一个方法值不值得信,要看它在不需要它的时候会不会帮倒忙。 把训练先验也设成五成,重跑:

| 做法 | 测试准确率 | 边界 | |---:|---:| | 不加权 | 0.8925 | -0.0821 | | 加权 | 0.8925 | -0.0821 | 两行一个数字都不差。 因为这时正负两类的权重都是 2.5 和 0.625 的同一个倍数关系被归一化掉了,等价于全 1。 这是好性质:没有移位时它自动退化成原模型,不会引入额外风险。 顺便注意,这个 0.8925 和上一段加权后的 0.8925 完全一致。 因为两者算的是同一件事——都还原到了「先验各半」的情形。

五、把测试先验扫一遍

真正要看的是一张表:测试先验从 0.1 扫到 0.9。

测试先验 不加权 加权 加权后的边界
0.1 0.9350 0.9300 +1.4751
0.2 0.9100 0.9100 +0.5654
0.3 0.8725 0.8850 +0.2428
0.4 0.8450 0.8725 +0.0597
0.5 0.8275 0.8925 -0.0781
0.6 0.8050 0.9050 -0.2107
0.7 0.7850 0.8875 -0.3752
0.8 0.7575 0.9150 -0.6513
0.9 0.7325 0.9375 -1.4089
不加权那一列从 0.9350 单调掉到 0.7325。
原因很直白:边界固定在 +0.5654,测试集里正类越多,被这条边界误判成正类以外的样本就越多。
这条边界本身没错,只是假设了「正类只占两成」。
加权那一列在 0.8850 到 0.9375 之间波动,明显更平。
边界一行行往左移:+1.4751、+0.5654、+0.2428,一直到 -1.4089,跟着测试集的真实比例走。
有两处要老实说。
第一,0.2 那一行两者完全相等(0.9100),因为训练先验就是 0.2,权重全是 1。
这是方法的定义决定的,不是巧合。
第二,0.1 那一行加权反而略差(0.9300 对 0.9350)。
因为测试先验离训练先验太远,负类权重涨到 9 倍,少数几个样本主导了拟合。
和上一节一样:移得越远,越没有本钱。

六、动手 Demo:类别平衡变化:形状不变、比例变了,只需要挪一下边界

这段代码造两个形状完全相同的高斯类,训练集让正类只占两成、测试集占五成; 然后按类别先验比加权,比训练准确率、测试准确率,再扫一遍测试先验从 0.1 到 0.9。 重点看三处。第二段训练准确率 0.9300 掉到 0.9200 而测试从 0.8275 升到 0.8925; 第三段加权后边界 -0.0781 与两类中心中点 -0.0781 完全一致;第五段不加权那一列的单调下滑。

# -*- coding: utf-8 -*-
import numpy as np


def wls(X, y, w=None):
    """加权最小二乘,返回斜率与截距。"""
    n = len(X)
    if w is None:
        w = np.ones(n)
    A = np.column_stack([np.ones(n), X])
    W = np.diag(w / w.mean())
    coef = np.linalg.solve(A.T @ W @ A + 1e-12 * np.eye(2), A.T @ W @ y)
    return coef[1], coef[0]


def score(b, a, X):
    """最小二乘分类器的判别值,和 0 比得类别。"""
    return a + b * X


def acc(b, a, X, y):
    return float(np.mean((score(b, a, X) > 0.0).astype(float) * 2.0 - 1.0 == y))


def thresh(b, a):
    """判别值等于零的那个位置。"""
    return -a / b


def label_weight(y, prior_tr, prior_te):
    """按类别给权:测试先验除以训练先验。"""
    w = np.zeros(len(y))
    for c, p_tr in ((1.0, prior_tr), (-1.0, 1.0 - prior_tr)):
        p_te = prior_te if c > 0 else 1.0 - prior_te
        w[y == c] = p_te / max(p_tr, 1e-9)
    return w


def balance_data(n=400, seed=1, prior_tr=0.2, prior_te=0.5, sep=2.4, noise=1.0):
    """两类的高斯形状完全相同,只有先验比例不同。"""
    rg = np.random.default_rng(seed)

    def draw(n, prior):
        nl = int(round(n * prior))
        pos = rg.normal(sep / 2.0, noise, nl)
        neg = rg.normal(-sep / 2.0, noise, n - nl)
        x = np.concatenate([pos, neg])
        y = np.concatenate([np.ones(nl), -np.ones(n - nl)])
        idx = rg.permutation(n)
        return x[idx], y[idx]

    xtr, ytr = draw(n, prior_tr)
    xte, yte = draw(n, prior_te)
    return xtr, ytr, xte, yte


xtr, ytr, xte, yte = balance_data()
w = label_weight(ytr, 0.2, 0.5)
b0, a0 = wls(xtr, ytr)
b1, a1 = wls(xtr, ytr, w)

print('[1] 两类的形状一模一样,只有比例变了')
print('  训练集正类占 %.0f%%  测试集正类占 %.0f%%' % (100 * (ytr > 0).mean(), 100 * (yte > 0).mean()))
print('  正类样本的权重 %.3f  负类样本的权重 %.3f' % (w[ytr > 0][0], w[ytr < 0][0]))

print('')
print('[2] 同一个模型,两种称重方式')
print('  不做任何处理  训练准确率 %.4f   测试准确率 %.4f' % (acc(b0, a0, xtr, ytr), acc(b0, a0, xte, yte)))
print('  按类别加权    训练准确率 %.4f   测试准确率 %.4f' % (acc(b1, a1, xtr, ytr), acc(b1, a1, xte, yte)))
print('  训练那一栏反而降了,测试那一栏才是我们要的')

print('')
print('[3] 变的其实只是一条边界')
print('  不加权的边界 x = %+.4f' % thresh(b0, a0))
print('  加权后的边界 x = %+.4f' % thresh(b1, a1))
print('  两类样本的实际中心 %+.4f 和 %+.4f,中点 %+.4f'
      % (xtr[ytr > 0].mean(), xtr[ytr < 0].mean(), (xtr[ytr > 0].mean() + xtr[ytr < 0].mean()) / 2.0))
print('  加权把边界推到了中点,不偏袒任何一类')

print('')
print('[4] 训练先验和测试先验一样时,加权什么也不做')
xa, ya, xb, yb = balance_data(prior_tr=0.5, prior_te=0.5)
wa = label_weight(ya, 0.5, 0.5)
u0, v0 = wls(xa, ya)
u1, v1 = wls(xa, ya, wa)
print('  不加权  测试 %.4f  边界 %+.4f' % (acc(u0, v0, xb, yb), thresh(u0, v0)))
print('  加权    测试 %.4f  边界 %+.4f' % (acc(u1, v1, xb, yb), thresh(u1, v1)))
print('  两组权重全是同一个数,等于没加权')

print('')
print('[5] 测试先验从 0.1 扫到 0.9')
print('  测试先验  不加权    加权   加权后的边界')
for pte in (0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9):
    xa, ya, xb, yb = balance_data(prior_tr=0.2, prior_te=pte)
    wa = label_weight(ya, 0.2, pte)
    u0, v0 = wls(xa, ya)
    u1, v1 = wls(xa, ya, wa)
    print('  %.1f      %.4f  %.4f  %+.4f'
          % (pte, acc(u0, v0, xb, yb), acc(u1, v1, xb, yb), thresh(u1, v1)))

实跑输出:

--------------------------------------------------------------------
类别平衡变化:形状不变、比例变了,只需要挪一下边界
--------------------------------------------------------------------
[1] 两类的形状一模一样,只有比例变了
  训练集正类占 20%  测试集正类占 50%
  正类样本的权重 2.500  负类样本的权重 0.625

[2] 同一个模型,两种称重方式
  不做任何处理  训练准确率 0.9300   测试准确率 0.8275
  按类别加权    训练准确率 0.9200   测试准确率 0.8925
  训练那一栏反而降了,测试那一栏才是我们要的

[3] 变的其实只是一条边界
  不加权的边界 x = +0.5654
  加权后的边界 x = -0.0781
  两类样本的实际中心 +1.1284 和 -1.2847,中点 -0.0781
  加权把边界推到了中点,不偏袒任何一类

[4] 训练先验和测试先验一样时,加权什么也不做
  不加权  测试 0.8925  边界 -0.0821
  加权    测试 0.8925  边界 -0.0821
  两组权重全是同一个数,等于没加权

[5] 测试先验从 0.1 扫到 0.9
  测试先验  不加权    加权   加权后的边界
  0.1      0.9350  0.9300  +1.4751
  0.2      0.9100  0.9100  +0.5654
  0.3      0.8725  0.8850  +0.2428
  0.4      0.8450  0.8725  +0.0597
  0.5      0.8275  0.8925  -0.0781
  0.6      0.8050  0.9050  -0.2107
  0.7      0.7850  0.8875  -0.3752
  0.8      0.7575  0.9150  -0.6513
  0.9      0.7325  0.9375  -1.4089

第一段两行权重 2.500 和 0.625,比值正好是 4。 这等于把正类样本复制四份——因为 0.5 除以 0.2 是 2.5,0.5 除以 0.8 是 0.625, 再除以两者公因子,就是 4 比 1。 第二段那两行要一起读。 训练准确率 0.9300 掉到 0.9200,测试 0.8275 升到 0.8925。 如果只报训练那一栏,加权是个坏主意;只报测试那一栏,加权是个好主意。 报哪一个,取决于你打算用哪个分布去评估。 第三段是本篇最漂亮的一处。 两类中心 +1.1284 和 -1.2847,中点算出来 -0.0781;加权后的边界也印出来 -0.0781。 这两个数不是凑的,是同一个量的两种算法。 不加权的 +0.5654 偏离中点 0.64,偏的方向就是训练集里的少数类。 第四段那个「一个数字都不差」的对照组不能省。 它证明了这个方法在没有移位时是恒等变换,不会平白引入风险。 很多所谓的「改进方法」恰恰缺这个性质——不需要它的时候它也在捣乱。 第五段 0.1 和 0.2 两行是诚实的边界。 0.2 行两者相等,是因为训练先验就是 0.2; 0.1 行加权略差(0.9300 对 0.9350),是因为外推得太远。 报方法效果时把这两行留着,比只报最好看的那几行可信。

30 秒实验

  1. 把 balance_data 的 sep 从 2.4 改成 1.2,让两类重叠得更厉害,重跑。 看加权带来的提升从 0.065 缩到多少。 两类越难分,边界位置的影响越小——反正怎么划都错一堆。
  2. 把 balance_data 的 prior_tr 从 0.2 改成 0.05,重跑。 少数类只有 20 个样本,加权到 10 倍之后,边界会开始抖。 这就是类别不平衡问题的真面目:不是权重算错了,是少数类的样本根本不够。
  3. 把 label_weight 里的 prior_te 故意传错(比如真实是 0.5,你按 0.3 算),重跑。 看测试准确率掉到多少。 这一节的权重完全依赖「测试先验是多少」这个外部知识——估错了,方法就失效。 实战里这个数往往得靠业务经验或者线上统计拿到。

常见误区

  1. 「训练准确率降了,说明加权是错的」。 训练集的真实比例是两成对八成,按五成对五成优化当然在训练集上吃亏。 实测 0.9300 掉到 0.9200,同时测试从 0.8275 升到 0.8925。 该看哪一栏,取决于部署时面对的是哪个分布。
  2. 「类别平衡变化也要估密度比」。 不用。 因为条件分布没变,密度比里的 p(x 给定 y) 上下约掉了, 只剩 p_te(y) 除以 p_tr(y)——一个只跟标签有关的常数。 这一节的权重是一张两行的查找表,不是一个函数。
  3. 「加权会改变模型的形状」。 实测加权前后的边界从 +0.5654 挪到 -0.0781,但两类中心没动,加权后的边界正好落在中点。 样本复制只挪偏置,不改形状。 如果两类的协方差本来就不同,光靠加权是补不回来的——那得换模型。
  4. 「测试先验可以随便估」。 整套方法的权重完全由 p_te(y) 决定。 估成 0.1 而实际是 0.5,边界会停在 +1.4751 附近,比不加权的 +0.5654 还偏。 这个数必须从业务或线上统计里拿准,不能拍脑袋。
自测三问(点开对答案) 1. 为什么这一节的权重里没有 x? —— 因为假设只有 p(y) 变了、p(x 给定 y) 没变。 把 p_te(x,y)/p_tr(x,y) 展开成条件分布乘先验,条件分布那一项上下相同直接约掉, 只剩 p_te(y)/p_tr(y)。所以权重是一张按类别查的表。 2. 加权之后训练准确率降了,为什么还要用它? —— 因为训练集的比例(两成对八成)不是部署时要面对的比例(五成对五成)。 加权是照着目标比例优化的,所以在训练集上必然吃亏。 实测训练 0.9300 降到 0.9200,测试 0.8275 升到 0.8925。 关键前提是验证集也得按目标分布采样,否则会选错模型。 3. 加权后的边界 -0.0781 为什么恰好等于两类中心的中点? —— 因为两类协方差相同,贝叶斯最优边界就是两类中心的中点, 而最小二乘分类器在这种对称数据上恰好能找出它。 不加权时的 +0.5654 偏了 0.64,偏向训练集里的多数类(负类)。 加权等价于把正类样本复制四份,把边界推回正中间。

第 18 章处理的是「一个任务,两个分布」。 还有一种情形:根本没有「一个任务」——手上同时有好几个相关但不同的任务,每个都有自己的数据, 而且都不多。 下一章要问的是:这些任务能不能互相借力。

相关笔记