上一篇的线性回归预测一个数。把这个数过一个 sigmoid 函数压到 0 到 1 之间,它就变成了一个概率——这就是逻辑回归,最简单的分类器。它简单,但每一个神经网络的最后一层都是它:前面所有层在做特征提取,最后一层是一个逻辑回归(两类)或 softmax 回归(多类)。奖励模型也不例外:一个 8B 的 LLM 提特征,上面接一个 \(4096 \to 1\) 的线性头,Bradley-Terry 的 loss 就是逻辑回归的 loss 作用在两个回答的分差上。看清这一点,奖励模型的一切性质——需要多少数据、怎么过拟合、准确率上限在哪——都可以用逻辑回归的语言想。

全篇的核心问题是:

一个预测数的模型怎么变成预测概率的模型,loss 为什么要换成交叉熵?1 奖励模型和逻辑回归是什么关系?2 为什么它的准确率到 80% 就上不去了?3

一、总览

1. 从一条线到一个概率

本文按”模型怎么一步步变复杂”组织:先把线性回归改成两类分类(sigmoid + 交叉熵),再改成多类(softmax),再改成”比较两个东西”(Bradley-Terry)。三个模型是同一个骨架:

线性回归、逻辑回归、softmax 与 Bradley-Terry 的同一骨架
模型 线性部分 输出 loss 出现在
线性回归(上一篇) \(z = w^T x + b\) \(\hat y = z\) \((z - y)^2\) 回归头
逻辑回归 \(z = w^T x + b\) \(p = \sigma(z)\) \(-[y \log p + (1-y)\log(1-p)]\) 二分类头、质量分类器
softmax 回归 \(z = W^T x + b\)(\(K\) 个数) \(p = \text{softmax}(z)\) \(-\log p_y\) 语言模型输出层
Bradley-Terry 奖励模型 \(z = w^T (x_A - x_B)\),无偏置 \(P(A \succ B) = \sigma(z)\) \(-\log \sigma(z)\) RLHF 的奖励模型、DPO

三个模型的梯度长得一模一样:(预测 − 真实)× 特征。

2. 本文的章节安排

本文的章节安排
章 主题 内容
二 从数到概率
  • sigmoid 长什么样
  • 为什么不能直接用线性回归做分类
  • 决策边界
三 交叉熵
  • 为什么不用平方误差
  • loss 曲线
  • 梯度 \((p - y)x\) 的推导与数值验证
四 手写一个逻辑回归
  • 12 行梯度下降
  • 二维数据上的决策边界图
  • 与 scikit-learn 对数
五 真实数据集
  • 乳腺癌 30 个特征 96%
  • 系数是可读的
  • 标准化
六 softmax 回归
  • 多类
  • 手写数字 10 类 96%
  • 它就是语言模型的输出层
七 奖励模型 = 逻辑回归
  • Bradley-Terry 的推导
  • 用 3000 个偏好对拟合一个
  • 拟合分 vs 真实分
八 准确率上限是标注的一致性
  • 按分差分桶看标注噪声
  • 噪声 vs 上限曲线
  • 对真实 RM 的含义
九 案例:垃圾短信识别
  • 5,574 条真实短信,TF-IDF + 逻辑回归:12,122 个权重,准确率 98.3%、精确率 100%、召回率 87%
  • 阈值怎么定
  • 模型学到了哪些词
  • 错在哪
十 本文小结  
十一 自测 六道题

3. 来龙去脉:一条 S 形曲线的两百年

逻辑回归的来历
年 谁 当时的问题 留下的东西
1838 Verhulst 人口不能无限指数增长,会被资源顶住 logistic 曲线 \(\sigma(z) = 1/(1+e^{-z})\):先快后慢、有上限的 S 形,”logistic”这个名字就是他起的
1944 Berkson 生物实验里”剂量 → 死亡率”是 0/1 结果,怎么用线性模型拟合一个概率 logit \(\log \frac{p}{1-p} = w^T x + b\):把概率拉直成一条线,反过来就是 sigmoid(第二章)
1958 Cox 怎么系统地估计这个模型的参数、做统计检验 逻辑回归作为一个统计模型成型;最大似然估计——就是本篇第三章的交叉熵
1970s–1980s 信用评分(FICO 1989) 银行要一个能解释、能审计的”违约概率” 逻辑回归成为金融风控的默认模型,至今如此:每个系数是一条能写进监管报告的规则
1998– 文本分类(垃圾邮件、新闻分类) 几万个词做特征,样本几千条,要快、要稀疏 词袋 / TF-IDF + 逻辑回归(或线性 SVM,第五篇)成为文本分类的基线,第九章的案例
1986– 神经网络 多层网络的最后一层该长什么样 每个分类网络的输出层就是它(第五章);奖励模型是它作用在分差上(第七章)

它解决的问题只有一个:线性回归的输出是任意实数,而概率必须在 0 到 1 之间。Berkson 的 logit 是”把概率变成实数再做线性回归”,Cox 的最大似然是”用交叉熵而不是平方误差训它”——这两步做完,线性回归就变成了分类器。之后八十年它没有被替代,因为替代者(树、SVM、神经网络)赢的是精度,输的是它的三个长处:系数可读(第五章的乳腺癌:凹点多 → 恶性)、输出是校准的概率(第十篇:它天生比树和 SVM 校准得好)、训练一秒。什么时候不该用它:特征与类别的关系不是”加权和过一个阈值”能表达的——一张图片的像素、一段语音的波形,这时要么先用别的东西提特征(深度学习做的事),要么换第四、五、六篇的非线性模型。

二、从数到概率

1. 为什么不能直接用线性回归做分类

分类任务的 \(y\) 是 0 或 1(垃圾邮件 / 正常邮件,恶性 / 良性)。最直接的想法是照搬上一篇:把 0/1 当成数,拟合一条直线 \(\hat y = wx + b\),大于 0.5 就判 1。举个例子:复习小时数 \(x\) 与是否通过考试 \(y\),复习 1、2、3 小时的三人没过(\(y = 0\)),复习 5、6、7 小时的三人过了(\(y = 1\))。最小二乘直线是 \(\hat y = 0.214x - 0.357\),恰好在 \(x = 4\) 处穿过 0.5,看起来能用。两个毛病:

  1. 输出不是概率。复习 10 小时预测 1.79,复习 0 小时预测 −0.36——”通过的概率是 179%”没有意义。
  2. 分对的点反而拖后腿。再加一个复习 20 小时、通过了的人:他的 \(y = 1\),原直线在 \(x = 20\) 处的预测却是 3.93,残差 −2.93,平方误差 8.6——这个最不该有争议的样本,成了对损失贡献最大的点。重新拟合,最小二乘为了迁就他把直线压平成 \(0.052x + 0.245\),穿过 0.5 的位置从 4 小时挪到 4.9 小时——一个明显分对的样本,把边界拉偏了。它本不该有任何”惩罚”。

要修的是输出的形状:无论 \(z = wx + b\) 多大,输出都要落在 0 到 1 之间,而且越远离边界越接近 0 或 1、变化越慢。

2. sigmoid

逻辑回归保留线性部分 \(z = w^T x + b\),再过一个 sigmoid 函数把它压到 \((0, 1)\):

\[P(y = 1 \mid x) = \sigma(z) = \frac{1}{1 + e^{-z}}, \qquad z = w^T x + b\]

左:sigmoid 曲线,z = 0 处取 0.5,z = ±4 处接近 0 和 1,是一条平滑的 S 形;右:交叉熵损失随预测概率 p 的变化——y = 1 时 −log p 在 p → 0 处飙向无穷,对比灰色虚线的平方误差 (p − 1)² 最多只到 1

sigmoid 在几个 z 上的取值
\(z\) −4 −2 0 2 4
\(\sigma(z)\) 0.018 0.119 0.500 0.881 0.982

算一个:\(z = 2\) 时 \(e^{-2} = 0.135\),\(\sigma(2) = 1 / 1.135 = 0.881\)。\(z\) 越大、\(e^{-z}\) 越接近 0、\(\sigma\) 越接近 1;\(z\) 越负、\(e^{-z}\) 越大、\(\sigma\) 越接近 0;\(z = 0\) 时 \(e^0 = 1\),恰好 0.5。它还有一个对称性:\(\sigma(-z) = 1 - \sigma(z)\)(\(\sigma(-2) = 0.119 = 1 - 0.881\)),所以”是正类的概率”与”是负类的概率”自动加起来等于 1。\(z\) 可以理解为”证据的强度”:正的证据支持类 1,负的支持类 0,sigmoid 把证据换算成概率——而且在 \(\lvert z \rvert > 4\) 之后曲线几乎平了,复习 20 小时与复习 10 小时的概率都接近 1,上一节那个”分对却被重罚”的问题就消失了。

3. 决策边界

\(z > 0\) 时概率大于 0.5、预测正类;\(z = 0\) 即 \(w^T x + b = 0\) 的那条线(高维是超平面)是决策边界。它是线性的——逻辑回归只能画直线把两类分开,第四章的图会看到。不能用一条直线分开的数据(XOR)它就无能为力,要么手工造新特征,要么用多层网络(L3)让模型自己造。

三、交叉熵

1. 为什么不用平方误差

有了概率 \(p\),怎么衡量它预测得好不好?直觉的办法是 \((p - y)^2\),但它有个毛病:\(y = 1\) 而模型自信地说 \(p = 0.01\),平方误差是 0.98——与 \(p = 0.1\) 的 0.81 差不多,惩罚不够重。交叉熵(cross-entropy)用对数:

\[\mathcal{L} = -\big[\, y \log p + (1 - y) \log (1 - p) \,\big]\]
  • \(y = 1\) 时只剩 \(-\log p\):\(p = 0.9\) 罚 0.105,\(p = 0.5\) 罚 0.69,\(p = 0.01\) 罚 4.6——越自信地错,罚得越重,趋向无穷;
  • \(y = 0\) 时只剩 \(-\log(1 - p)\),对称。

上图右边画了这两条曲线与平方误差的对比。三个样本算一遍:模型对它们给出 \(p = 0.9, 0.3, 0.6\),真实标签 \(y = 1, 0, 1\),每个样本的损失分别是 \(-\log 0.9 = 0.105\)、\(-\log(1 - 0.3) = 0.357\)、\(-\log 0.6 = 0.511\),平均 0.324。第一个预测得又对又自信,几乎不罚;第三个虽然也判对了(0.6 > 0.5)但不够自信,罚得比第二个还重——交叉熵奖励的是自信且正确。

这个公式不是拍脑袋的,它来自上一篇第八章同样的最大似然推理。模型说”\(y = 1\) 的概率是 \(p\)“,那么观察到标签 \(y\) 的概率是:\(y = 1\) 时 \(p\),\(y = 0\) 时 \(1 - p\),合写成一个式子 \(p^y (1 - p)^{1 - y}\)(\(y\) 只取 0 或 1,两个因子总有一个的指数是 0、等于 1)。这就是抛一枚”正面概率为 \(p\)“的硬币的分布,叫伯努利分布。让全部样本的概率乘积最大 → 取对数变成和 → 加负号变最小:\(-[y \log p + (1 - y) \log(1 - p)]\),正是交叉熵。上一篇的平方误差是”噪声是高斯”假设下的最大似然,交叉熵是”标签是抛硬币”假设下的最大似然——两个损失来自同一条推理、不同的分布假设。

2. 梯度:预测减真实,乘特征

训练要用梯度下降(上一篇第四章),所以要算交叉熵对 \(w\) 的导数。\(\mathcal{L}\) 依赖 \(p\),\(p\) 依赖 \(z\),\(z\) 依赖 \(w\)——一层套一层,用链式法则:外层对内层的导数逐层相乘(”\(\mathcal{L}\) 随 \(w\) 变多快” = “\(\mathcal{L}\) 随 \(p\) 变多快” × “\(p\) 随 \(z\) 变多快” × “\(z\) 随 \(w\) 变多快”)。三步:

\[\frac{\partial \mathcal{L}}{\partial p} = -\frac{y}{p} + \frac{1 - y}{1 - p} = \frac{p - y}{p(1 - p)}, \qquad \frac{\partial p}{\partial z} = \sigma(z)\big(1 - \sigma(z)\big) = p(1 - p), \qquad \frac{\partial z}{\partial w} = x\]

第一步:\(\log p\) 的导数是 \(1/p\),\(\log(1 - p)\) 的导数是 \(-1/(1 - p)\),两项通分得到中间那个式子。第二步是 sigmoid 自己的性质:\(\sigma(z) = (1 + e^{-z})^{-1}\),求导得 \(e^{-z} / (1 + e^{-z})^2 = \sigma(z) \cdot \frac{e^{-z}}{1 + e^{-z}} = \sigma(z)(1 - \sigma(z))\)——sigmoid 的斜率可以用它自己的值算出来,\(z = 1\) 处 \(0.731 \times 0.269 = 0.197\),中间最陡(\(z = 0\) 处 0.25)、两头趴平。第三步 \(z = w^T x + b\) 对 \(w\) 的导数就是 \(x\)。三项相乘,\(p(1-p)\) 恰好约掉:

\[\frac{\partial \mathcal{L}}{\partial w} = (p - y)\, x\]

预测概率减真实标签,乘特征——与上一篇线性回归的梯度 \((w^T x - y)\, x\) 是同一个形式。sigmoid 的导数与 \(\log\) 的导数互相抵消,这不是巧合:交叉熵就是为 sigmoid 配的 loss(用平方误差配 sigmoid,梯度里会多一个 \(p(1-p)\),\(p\) 接近 0 或 1 时梯度消失,学不动)。

推导可能出错,所以用有限差分验证——导数的定义就是”参数动一点点,函数值变多少”:把 \(w_j\) 加上一个很小的 \(h\)(如 \(10^{-6}\))算一次 loss、减去 \(h\) 再算一次,两者之差除以 \(2h\) 就是导数的数值近似。它慢(每个参数算两次 loss)但不会推错,是检查梯度公式的标准办法:

公式算的梯度   [ 0.037713 -0.181696 -0.060722  0.146158]
有限差分算的   [ 0.037713 -0.181696 -0.060722  0.146158]   最大差 4.3e-11

四、手写一个逻辑回归

1. 十二行

def sigmoid(z):
    return 1 / (1 + np.exp(-z))

def bce(p, y):                                          # 二元交叉熵,每样本平均
    eps = 1e-12
    return -np.mean(y * np.log(p + eps) + (1 - y) * np.log(1 - p + eps))

def fit_logistic(X, y, lr=0.1, steps=2000):             # X 已含一列 1(偏置)
    w = np.zeros(X.shape[1])
    for _ in range(steps):
        p = sigmoid(X @ w)                              # ① 预测概率
        grad = X.T @ (p - y) / len(y)                   # ② 梯度:(p − y) 乘特征,再平均
        w -= lr * grad                                  # ③ 走一步
    return w
  1. ① 全部样本的线性部分过 sigmoid,得到 \(n\) 个概率(X @ w 一次算出所有样本的 \(z\));
  2. ② 第三章的公式 \((p - y)x\),用矩阵一次算完所有样本再平均:p - y 是 \(n\) 个”预测减真实”,X.T @ 把每个样本的这个数乘上它的特征、按样本加起来;
  3. ③ 与上一篇一模一样的更新。没有闭式解(令梯度为零的方程里 \(w\) 藏在 \(e^{-w^T x}\) 里,解不出来),所以只能梯度下降。

bce 里的 eps = 1e-12 是防止 \(\log 0\):p 恰好是 0 或 1 时 \(\log\) 会得到负无穷,加一个极小的数避开它。

2. 二维数据上看它

200 个点、两类、每类一团。跑 500 步:

左:手写梯度下降的训练交叉熵从 0.69(= log 2,随机猜的 loss)在 50 步内降到 0.2 并稳定;右:二维平面上 P(y = 1 ∣ x) 的等高线从蓝(0)渐变到红(1),黑色直线是 p = 0.5 的决策边界,把两团点大致分开,重叠区里有少数分错的点

手写 GD 500 步:w = [0.012 1.892 1.282],训练准确率 0.930,最终交叉熵 0.201
sklearn:       w = [0.012 1.892 1.283],训练准确率 0.930

怎么读左图:一开始 \(w = 0\),每个样本 \(z = 0\)、预测 0.5,不管标签是什么损失都是 \(-\log 0.5 = 0.693\)——这是”完全不知道”的基准;loss 降到 0.201 表示模型对多数样本已经很确定。系数与 scikit-learn 的 LogisticRegression 差 0.001——两个实现算的是同一个东西。怎么读右图:每个位置的颜色是模型在那里给出的 \(P(y = 1)\),等高线是概率相同的位置——因为 \(z = w^T x + b\) 是线性的,\(z\) 相同的点连成直线,所以等高线是一组平行直线,\(p = 0.5\) 那条(黑线)就是决策边界。离边界越远概率越接近 0 或 1、颜色越深。两团点有重叠,直线分不干净,93% 已是线性边界的上限。

五、真实数据集

1. 乳腺癌:30 个特征 → 良性 / 恶性

569 个肿瘤、30 个数值特征(半径、纹理、凹点数……)、良性 / 恶性:

398 训练 / 171 测试;准确率 0.959;测试集每样本交叉熵 0.085
手写 GD(同样的 L2 强度):准确率 0.959
|w| 最大的三个特征: ['radius error (-1.08)', 'worst texture (-1.07)', 'mean concave points (-1.01)']
一个样本: 线性部分 wᵀx+b = -13.15 → σ(·) = 0.000 → 预测 恶性,真实 恶性

30 个特征的线性组合过一个 sigmoid,准确率 96%。那个样本的 \(z = -13.15\),sigmoid 之后是 0.000——模型非常确定它是恶性。

2. 系数是可读的

30 个特征的系数横条图:凹点数、最差纹理、半径误差等 23 个特征系数为负(红,偏向恶性),紧致度误差、对称性误差等 7 个为正(绿,偏向良性);绝对值最大约 1.1

每个系数的符号与大小可读:凹点越多、纹理越差、半径越大,越可能恶性(负号对应”恶性 = 0”的编码)。这是线性模型相对深度网络的一个实际优点——出了问题能看出是哪个特征在起作用。前提是特征先标准化(减均值除标准差,上一篇第五章),否则量纲大的特征系数天然小,比不了。

3. 每个分类头的原型

任何神经网络分类器 = 前面所有层做特征提取 \(x = f_\theta(\text{输入})\) + 最后一层 \(\sigma(w^T x + b)\)。这里的 30 个特征是医生手工测量的;深度学习改变的是特征从哪来(学出来而不是手工设计),没有改变最后那一步。给 15T token 打质量分的 fastText 分类器(第六篇),就是把文本的词向量平均成特征再接一个逻辑回归。

六、softmax 回归:多类

1. 从 2 类到 \(K\) 类

手写数字有 10 类,不能只用一个”是 / 不是”的概率。\(K\) 类时,每类一个线性部分 \(z_k = w_k^T x + b_k\)——\(K\) 组系数、\(K\) 个”证据分”;再把 \(K\) 个数过 softmax 变成和为 1 的概率:

\[p_k = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}, \qquad \mathcal{L} = -\log p_y\]

算一个:三类的分数 \(z = (2, 1, 0)\),先各取 \(e^z\) 得 \((7.39, 2.72, 1.00)\),和是 11.11,各除以和得 \(p = (0.665, 0.245, 0.090)\)——三个正数、和为 1、分数最高的类概率最大,而且分差 1 对应概率比 \(e \approx 2.72\) 倍。\(K = 2\) 时 softmax 退化成 sigmoid(两个分数只有差值有用)。loss 只看真实类别 \(y\) 的那个概率,希望它大:真实类是第 0 类就罚 \(-\log 0.665 = 0.41\),是第 2 类就罚 \(-\log 0.090 = 2.4\)。梯度仍是 \((p - y_{\text{one-hot}})\, x\)——把标签写成 one-hot 向量(长度 \(K\),真实类的位置是 1、其余 0,如第 1 类写成 \((0, 1, 0)\)),预测减真实、乘特征。代码与二元版只差一个 softmax:

def softmax(Z):
    Z = Z - Z.max(1, keepdims=True)                     # 数值稳定:先减每行最大值(不改变结果,见下)
    E = np.exp(Z)
    return E / E.sum(1, keepdims=True)

def fit_softmax(X, y, n_classes, lr=0.5, steps=500):
    Y = np.eye(n_classes)[y]                            # one-hot [n, K]
    W = np.zeros((X.shape[1], n_classes))               # [d+1, K]:每类一列系数
    for _ in range(steps):
        P = softmax(X @ W)                              # [n, K]
        W -= lr * X.T @ (P - Y) / len(y)                # 梯度形式与二元完全相同
    return W

softmax 第一行减去每行最大值是为了防溢出:\(z = 1000\) 时 \(e^{1000}\) 超出浮点数范围变成无穷,但每个 \(z_k\) 同减一个常数 \(c\),分子分母同乘 \(e^{-c}\),结果不变——减最大值后最大的指数是 \(e^0 = 1\),永远不会溢出。语言模型的输出层每天都在做这个减法。np.eye(n_classes)[y] 用单位矩阵的第 \(y\) 行当 one-hot 向量,一行代码把整个标签数组变成 \([n, K]\) 的 0/1 矩阵。

2. 手写数字:10 类

1797 张 8×8 的手写数字,64 个像素当特征:

左:一张 8×8 的手写数字图,真实标签 1;右:softmax 输出的 10 个类的概率柱状图,类 1 最高 0.74(红),类 8 有 0.22,其余接近 0

W 的形状 (65, 10)(64 个像素 + 1 偏置 → 10 类);手写 softmax GD 500 步测试准确率 0.961;sklearn 0.970
一个测试样本:10 个类的概率 [0.   0.74 0.   0.01 0.02 0.   0.   0.01 0.22 0.  ] → argmax 1,真实 1

64 个像素的 10 组线性组合过一个 softmax,96%。那个样本模型给”1”0.74、给”8”0.22——它觉得这张图有点像 8,这种”犹豫”就是概率输出比硬分类多给的信息。

3. 它就是语言模型的输出层

语言模型最后一层 nn.Linear(d_model, vocab) + softmax,就是一个 \(K = \text{vocab}\)(几万到几十万)类的 softmax 回归:特征是前面几十层 Transformer 算出的 \(d_{\text{model}}\) 维向量,”类别”是词表里的每个 token,loss 是 \(-\log p_{\text{下一个 token}}\)——与上面手写数字的代码一行一行对应,只是 \(W\) 从 \(65 \times 10\) 变成了 \(4096 \times 128000\)。L1 第二篇那个小 Transformer 最后的 nn.Linear(d, vocab) 就是它。

七、奖励模型 = 逻辑回归

1. 从”比较”到逻辑回归

RLHF 里的奖励模型(reward model,RM)要给每个回答打一个分——但标注员给不出”这个回答 7.3 分”这种绝对分数(不同人的尺子不一样),能可靠给出的是比较:”A 比 B 好”。于是问题变成:从一堆”A 胜 B”的记录学出一个标量分 \(r(x, y)\)(\(x\) 是问题、\(y\) 是回答),使得分高的确实更常胜出。Bradley-Terry 模型(1952 年为给球队排名提出的)假设:A 胜过 B 的概率由两者分差决定,分差过 sigmoid:

\[P(A \succ B) = \sigma\big(r(A) - r(B)\big), \qquad \mathcal{L} = -\log \sigma\big(r(A) - r(B)\big)\]

分差 0 → 50%(抛硬币);分差 2 → 88%;分差 4 → 98%——就是第二章那张 sigmoid 表,横轴换成了分差。分差越大标注员越一致,分数接近时标注员自己也常常摇摆,模型把这种摇摆当成正常现象而不是错误。现在假设 \(r\) 是特征的线性函数——\(r(A) = w^T x_A\),\(x_A\) 是 LLM 对回答 A 算出的一个特征向量(实际做法:把”问题 + 回答”喂给一个 LLM,取它最后一层在最后一个 token 上的 hidden state;这就是第五章说的”前面所有层做特征提取”)——那么

\[r(A) - r(B) = w^T x_A - w^T x_B = w^T (x_A - x_B)\]

代回去:\(P(A \succ B) = \sigma\big(w^T (x_A - x_B)\big)\)。对照第二章:这就是一个逻辑回归,输入是两个回答的特征差 \(x_A - x_B\),标签是”A 是否更好”。只有一个区别:没有偏置项 \(b\)——因为交换 A、B 时标签反转,必须有 \(P(A \succ B) + P(B \succ A) = 1\),即 \(\sigma(z) + \sigma(-z) = 1\),加了 \(b\) 就不成立。

2. 拟合一个

造 300 个”回答”(各一个 16 维特征)、一个真实的”品味”向量 \(w^*\),标注员按 Bradley-Terry 概率 \(\sigma(w^{*T}(x_A - x_B))\) 随机给出偏好(所以标注本身有噪声——两个回答分数接近时标注员像抛硬币),共 3000 对:

Xdiff = feats[i] - feats[j]                                                 # ① 特征取差
clf = LogisticRegression(fit_intercept=False).fit(Xdiff[:ntr], y[:ntr])     # ② 无偏置的逻辑回归
w_hand = fit_logistic(Xdiff[:ntr], y[:ntr])                                 # ③ 或者用第四章那 12 行(不加偏置列)

2993 个偏好对,80% 训练;留出集准确率 0.915(用真实分数判断的上限 0.913——标注本身有噪声)
学到的 w 与真实 w 的相关系数 0.999
手写 GD(无偏置列):留出集准确率 0.915,与真实 w 相关 0.999

左:300 个回答的拟合分 wᵀx 对真实分 r*(x) 的散点,几乎落在一条直线上(相关 0.998);右:Bradley-Terry 的 sigmoid 曲线,横轴分差、纵轴 A 胜过 B 的概率

学到的 \(w\) 与真实 \(w^*\) 相关 0.999,300 个回答的拟合分与真实分几乎在一条直线上(左图)——模型把”品味”学得几乎完美。

八、准确率上限是标注的一致性

1. 那 8.5% 错在哪

\(w\) 几乎完美,留出集准确率却只有 91.5%,而且用真实分数判断也只有 91.3%。把留出集的偏好对按两个回答的真实分差分成四桶,看标注与真实分数一致的比例:

按真实分差分桶的标注一致率
两个回答的真实分差 标注与真实分数一致率 对数
0.00–1.61 0.75 150
1.61–3.31 0.91 150
3.31–6.06 0.99 150
6.06–17.44 1.00 150

差的那 8.5% 几乎全在第一桶:分数接近的两个回答,标注员自己也只是在抛硬币(一致率 0.75)。这些错不是模型的错,是标注的噪声——任何模型都不可能超过 91.3%。

2. 噪声越大,上限越低

把标注噪声从小到大调六档,每档重新生成偏好、重新训练:

横轴标注噪声(对数),纵轴准确率:灰线是标注与真实分数的一致率(上限),从 0.97 降到 0.62;红线是学到的奖励模型的留出集准确率,每一档都贴着灰线,最多低 0.01

标注噪声对一致率上限与模型准确率的影响
标注噪声 标注一致率(上限) 模型留出准确率 \(w\) 与真实相关
0.3 0.965 0.962 1.000
1.0 0.913 0.915 0.999
2.0 0.838 0.830 0.993
4.0 0.718 0.713 0.995
8.0 0.618 0.613 0.961

模型准确率始终贴着上限走,而 \(w\) 的相关系数一直在 0.96 以上——模型学到的东西几乎没变,变的只是标注能被预测的程度。噪声 4.0 那一档:模型准确率 71%,\(w\) 相关 0.995。看到 71% 就说”模型不行、再训”,是在读错数字。

3. 对真实奖励模型的含义

真实的奖励模型也是这样:留出集准确率典型值 65%–80%,看起来不高,因为标签本身有噪声。但这里要把两个”一致率”分开——上面实验里的灰线是标签与真值的一致率(标签的正确率),它才是学到真值的模型能达到的上限,而它在真实数据里不可观测;能观测的是标注员之间的一致率(70%–80%),它比标签正确率低:两个各自独立 80% 正确的标注员,互相一致只有 \(0.8^2 + 0.2^2 = 68\%\),而学到真值的模型对他们任何一人的标签仍能 80%。所以”RM 准确率不能超过人际一致率”是错的——人际一致率是标签噪声的线索,不是天花板;判断是否在拟合噪声要看训练 / 验证 gap 与按难度分桶的表现。提升奖励模型的路仍然不是”训得更久”,而是更一致的标注、更清晰的标注指南、以及在分数接近的对上不强求(或者按一致率给样本加权)。

看清”奖励模型 = 逻辑回归作用在分差上”还有一个用处:”pairwise loss”不神秘,它就是把二分类的 \(w^T x\) 换成了 \(w^T(x_A - x_B)\);它怎么评估(第十篇:准确率、校准)、需要多少数据、怎么过拟合(第一篇),都可以用逻辑回归的语言想。L5 后训练系列第二篇讲真实奖励模型的训练细节;DPO(L0 第六篇)则是把这个逻辑回归里的 \(r\) 换成了策略的对数比。

九、案例:垃圾短信识别——TF-IDF + 逻辑回归

第五章的乳腺癌有 30 个医生量出来的特征。真实业务里更常见的情形是特征得自己造——比如输入是一条短信。这一章用逻辑回归做一个垃圾短信过滤器:它是 1998 年以来文本分类的标准基线,也是第六篇要讲的 LLM 数据质量分类器的原型(同一个结构,特征从词袋换成词向量)。

1. 问题与数据

UCI SMS Spam Collection(Almeida & Hidalgo 2011):5,574 条英文短信,人工标好 ham(正常)/ spam(垃圾),747 条 spam 占 13.4%。两条例子:

  • ham:Nah I don’t think he goes to usf, he lives around here though
  • spam:Had your mobile 11 months or more? U R entitled to Update to the latest colour mobiles wit…

spam 平均 139 个字符,ham 71 个——垃圾短信更长、更”广告”。类别不平衡:全部判 ham 的准确率就有 86.6%,所以准确率不是这个问题的指标,要看精确率(判成 spam 的里面真是 spam 的比例——误杀正常短信是用户最恨的)和召回率(所有 spam 里抓住的比例)。

2. 解决思路

%% 图:垃圾短信过滤器——从一条短信到一个概率
flowchart LR
    T["一条短信<br/>'FREE entry, call now'"] --> V["TF-IDF 向量化<br/>词表 12,122 个 1/2-gram<br/>→ 12,122 维稀疏向量 x"]
    V --> L["逻辑回归<br/>z = wᵀx + b<br/>12,122 个权重"]
    L --> P["p = σ(z)<br/>0.97"]
    P --> D{"p ≥ 阈值?"}
    D -->|是| S[进垃圾箱]
    D -->|否| H[进收件箱]

三步:(一)词袋——把短信变成一个向量,维度 = 词表大小,每一格是这个词在这条短信里的权重。用 TF-IDF 而不是简单计数,让常见词自动降权;词表里同时放单词和相邻两个词的组合(”call now”、”free entry”),因为很多信号在词对上。(二)逻辑回归——12,122 个权重,每个词一个,”这个词出现让 spam 的对数几率涨多少”。(三)阈值——模型给的是概率,进不进垃圾箱要定一个阈值,这一步是业务决定不是模型决定。

3. 代码

完整脚本 case_03_sms_spam.py,模型本身四行:

Xtr, Xte, ytr, yte = train_test_split(df.text, y, test_size=0.2, random_state=0, stratify=y)  # 分层:两边 spam 比例一样
model = make_pipeline(
    TfidfVectorizer(ngram_range=(1, 2), min_df=2, sublinear_tf=True),  # 词与词对;只出现过 1 次的词不要
    LogisticRegression(C=10, max_iter=2000),                            # C 是 L2 正则强度的倒数(上一篇 Ridge 的 1/α)
)
model.fit(Xtr, ytr)
prob = model.predict_proba(Xte)[:, 1]                                   # 每条测试短信是 spam 的概率
pred = (prob >= 0.5).astype(int)

w = model[-1].coef_[0]                                                  # 12,122 个权重,每个词一个
vocab = model[0].get_feature_names_out()
top = np.argsort(w)                                                     # 最负的推向 ham,最正的推向 spam

stratify=y 是第一篇第三章的分层切:不分层的话 1,115 条测试集里的 spam 数量会抖。min_df=2 把只出现过一次的词扔掉——它们只在一条短信里出现,学不到什么,还会让词表膨胀三倍。

4. 效果

垃圾短信识别——测试集 1,115 条
模型 准确率 精确率 召回率 F1
基线:全部判 ham 0.866 — 0.000 —
TF-IDF + 逻辑回归,阈值 0.5 0.983 1.000 0.872 0.932
同上,阈值 0.3 0.979 0.950 0.893 0.920
同上,阈值 0.2 0.978 0.925 0.913 0.919

测试集混淆矩阵:966 条 ham 全判对,149 条 spam 抓住 130 条、漏 19 条

阈值 0.5 时:966 条 ham 一条都没误杀(精确率 100%),149 条 spam 抓住 130 条、漏 19 条(召回 87%)。这个形态是逻辑回归在不平衡数据上的典型:spam 是少数类,模型对”判 spam”很谨慎。

阈值是业务决定。下图是阈值从 0 扫到 1 时精确率和召回率的走法——两条线此消彼长,没有一个阈值让两个都最高:

阈值从 0 到 1,精确率单调升、召回率单调降;0.2 处两者都在 0.92 左右

把阈值降到 0.2,召回从 87% 升到 91%,代价是开始误杀(精确率 100% → 92.5%,1,115 条里误杀 11 条正常短信)。运营商的选择通常是宁漏勿杀(一条重要短信进了垃圾箱比收到一条广告严重得多),所以会取高阈值;一个企业内部的钓鱼邮件过滤器则反过来。模型只负责给概率,阈值由”两种错各多贵”决定——第十篇第三章专门讲这个。

模型学到了什么:12,122 个权重里最正和最负的各 12 个——

权重最大与最小的词:上方推向 spam 的是 txt、call、text、free、reply、uk、www、claim、150p;下方推向 ham 的是 me、my、that、ok、at、can、later

推向 spam 的:txt(+8.8)、call(+8.2)、text、free、reply、uk、www、mobile、claim、150p(英国短信资费)——全是”叫你做一件事”的广告词。推向 ham 的:me、my、that、ok、at、later、he——全是人和人说话的词。这就是第五章”系数可读”在文本上的形态:不需要任何可解释性工具,把权重排个序就知道模型在看什么,也能立刻发现它有没有学到不该学的东西(比如把某个客户的名字当成 spam 信号)。

错在哪:漏掉的 19 条 spam 里,一条是 Bank of Granite issues Strong-Buy EXPLOSIVE PICK…(股票骚扰,词表里几乎没有这类词,\(p = 0.04\)),一条是 Hi ya babe x u 4goten bout me?…(伪装成熟人的口吻,\(p = 0.01\))。第一种是训练集里没有这类样本——加数据能修;第二种是词袋的根本限制:它只看有哪些词,看不出”一个陌生号码用熟人口吻”这种需要理解上下文的信号,要修得换特征(第六篇的词向量、或者一个小语言模型)。

5. 落地还差什么

(一)分布漂移:垃圾短信是对抗性的,发送者会换词(”free” 写成 “fr33”),今天 98% 的模型半年后可能只剩 90%,要持续收集用户”举报 / 移出垃圾箱”的反馈重训——这也是为什么这个数据集是 2011 年的、上面的高分不能直接当今天的预期;(二)中文:词袋要先分词,或者直接用字 n-gram;(三)规模:5,000 条训练一秒,5 亿条也只是线性增长——这正是逻辑回归还在生产系统里的原因之一。第六篇的 LLM 数据质量分类器(给 15T token 打分决定留哪些网页)就是这个案例的放大版:同一个逻辑回归,特征从 TF-IDF 换成 fastText 词向量的平均,训练样本从人工标注换成”维基百科 vs 随机网页”。

十、本文小结

  • 逻辑回归 = 线性部分 \(z = w^T x + b\) 过 sigmoid 得概率;\(z = 0\) 是线性的决策边界。
  • 交叉熵 \(-[y \log p + (1-y) \log(1-p)]\) 是伯努利分布的最大似然,越自信地错罚得越重;梯度 \((p - y)\, x\)——预测减真实、乘特征,与线性回归同形(sigmoid 与 log 的导数互相抵消,有限差分验证差 \(10^{-11}\))。
  • 手写 12 行梯度下降:二维两团点 93%,与 scikit-learn 系数差 0.001;乳腺癌 30 个特征 96%,系数可读(凹点多、纹理差 → 恶性)。
  • softmax 回归:\(K\) 类各一个线性部分过 softmax,loss \(-\log p_y\),梯度同形;手写数字 64 像素 → 10 类 96%;语言模型的输出层就是一个 vocab 类的 softmax 回归,深度学习只改变了特征从哪来。
  • 奖励模型 = 逻辑回归作用在两个回答的特征差上、无偏置:\(P(A \succ B) = \sigma(w^T(x_A - x_B))\)。3000 对拟出的 \(w\) 与真实相关 0.999。
  • 准确率上限是标签与真值的一致率:留出集 91.5% 与用真实分数判断的 91.3% 持平,错的几乎全在分差最小的那 1/4 对上(标签正确率 0.75);噪声从 0.3 到 8.0,模型准确率始终贴着上限、\(w\) 相关始终 > 0.96。真实数据里这条线不可观测,能观测的人际一致率(70–80%)比它低(独立 80% 正确的两人一致 68%),不能当 RM 的上限。
  • 案例:5,574 条短信,TF-IDF 词袋 12,122 维 + 逻辑回归,准确率 98.3%、精确率 100%、召回率 87%;阈值由两种错各多贵决定;权重排序就是可解释性(txt / call / free → spam,me / my / ok → ham);漏掉的是没见过的类型与需要读懂上下文的伪装。

配套代码:本文全部数字与图由 classical-ml/03_logistic_regression_and_reward_model.py(sigmoid / gradient / boundary / cancer / softmax / reward / noise 七个子实验)与 case_03_sms_spam.py(第九章案例,首次运行下载 200 KB 数据)产生,CPU 上十几秒跑完。

十一、自测

  1. \(z = w^T x + b = 2\),模型预测正类的概率是多少?\(z = -2\) 呢?

    答案

    \(\sigma(2) = 0.881\);\(\sigma(-2) = 0.119\)(两者和为 1)。

  2. 逻辑回归为什么用交叉熵而不用平方误差?两个理由。

    答案

    一,交叉熵是伯努利分布的最大似然,自信地错罚趋向无穷、平方误差最多罚 1;二,交叉熵配 sigmoid 的梯度是 \((p - y)x\),平方误差配 sigmoid 会多一个 \(p(1-p)\),\(p\) 接近 0 或 1 时梯度消失。

  3. 逻辑回归的决策边界是什么形状?为什么 XOR 问题它解不了?

    答案

    超平面(线性);XOR 的两类不能用一条直线分开——需要特征变换或多层(L3)。

  4. 一个 128k 词表、\(d_{\text{model}} = 4096\) 的语言模型,输出层是什么模型?参数矩阵多大?

    答案

    一个 128k 类的 softmax 回归;\(W\) 是 \(4096 \times 128000 \approx 5.2\) 亿参数。

  5. 奖励模型的逻辑回归为什么不能有偏置项?

    答案

    交换 A、B 标签反转,概率必须满足 \(P(A \succ B) + P(B \succ A) = 1\),即 \(\sigma(z) + \sigma(-z) = 1\),只有无偏置时成立。

  6. 一个奖励模型在留出集上准确率 72%,标注员两两一致率 75%。还值得继续训吗?该做什么?

    答案

    已接近上限(75%),继续训只会拟合噪声;该改进标注一致性、清理分歧大的对、或按类别看哪里差。

下一篇

下一篇放下”线性边界”这个限制,看三个思路完全不同的分类器——朴素贝叶斯(算概率)、KNN(找邻居)、决策树(问问题)——各自怎么工作、边界长什么样,并亲手实现它们。

  1. 把线性部分 \(z = w^T x + b\) 过一个 sigmoid \(\sigma(z) = 1/(1 + e^{-z})\) 压到 \((0,1)\) 就是概率;loss 换成交叉熵 \(-[y \log p + (1-y)\log(1-p)]\)——它是伯努利分布的最大似然,越自信地错罚得越重,而且它配 sigmoid 的梯度恰好是 \((p - y)x\),与线性回归同形,不会梯度消失。详见第二章、第三章。 ↩

  2. 奖励模型 = 逻辑回归作用在两个回答的特征差上、无偏置——\(P(A \succ B) = \sigma(w^T(x_A - x_B))\),loss 是 \(-\log\sigma(r_A - r_B)\),与 \(\sigma(w^T x + b)\) + 交叉熵是同一个模型;深度学习只改变了特征 \(x\) 从哪来。详见第七章。 ↩

  3. 偏好标签本身有噪声。合成实验里 3000 对拟出的 \(w\) 与真实相关 0.999,准确率 91.5% 恰好等于标签与真值的一致率 91.3%,错的几乎全在分差最小的 1/4 对上;把噪声调大六档,模型准确率始终贴着这条线、\(w\) 相关始终 > 0.96。所以 RM 的准确率上限由标注质量决定,不是模型容量——但注意可观测的人际一致率(70–80%)低于标签正确率(两个独立 80% 正确的人互相一致只有 68%),不能拿它当 RM 的天花板。详见第八章。 ↩

这篇对你有用?

本文由 arganzheng 创作,采用 CC BY 4.0 许可协议。在保留原文作者、署名以及完整原文链接(https://arganzheng.life/linear-and-logistic-regression-the-skeleton-of-reward-models.html)的前提下,欢迎各种形式的转载、翻译或商业引用。


COMMENTS

评论存放在 GitHub Discussions, 用 GitHub 账号登录即可发表,支持 Markdown。 想针对正文某句话说?选中那段文字,点浮出的「评论」即可划线评论;觉得哪里写错了,发表时勾上「同时提交 Issue」。 有人回复你时 GitHub 会按你的通知设置发邮件,不用守在这里。

×