🧪 第16章 机器学习概要
📄 一句话概括

逻辑回归三步:线性打分、sigmoid 压成概率、交叉熵量误差。 梯度是「预测减真值,乘特征,取平均」——三千轮把损失从 ln2 的 0.693 降到 0.062。 学习率没有通用答案,它跟特征量级绑在一起;特征放大十倍,同一个学习率就卡死。

📝 本章和前面几章的关系

这一篇对应教材 16.6 到 16.6.6:以 Python 程式库实作逻辑迴归,含逻辑迴归、S 型函數的數學式、 交叉熵誤差的數學式、梯度下降法的數學式,以及 NumPy 版与 scikit-learn 版两种写法。 上一篇讲了记号,这一篇把它们串成一个完整的训练过程——这是全书唯一一次把模型的内部拆开看。

一、三步:打分、压概率、量误差

步 算什么 输出
1 线性打分 z = X @ w + b 任意实数
2 压成概率 p = sigmoid(z) 0 到 1 之间
3 量误差 -mean(y*ln p + (1-y)*ln(1-p)) 一个非负数

三步做完,梯度一求,就往回挪一步。挪几千次,参数就定了。 整个训练过程就这么点内容,剩下的都是工程细节。 数据是自造的两类点:300 个,每类 150 个,中心分别在 (2, 2) 和 (5, 5),各自带标准差 1 的散布。 选这份数据是因为它完全可分——一条直线就能把两类分开。 后面会看到,完全可分会引出一个反直觉的结论。

二、sigmoid:把任意实数压到 0 到 1

z sigmoid(z)
-6.0 0.002473
-3.0 0.047426
0.0 0.500000
+3.0 0.952574
+6.0 0.997527

0 对应 0.5,两头趋近 0 和 1 但永远到不了。 这个函数的用处只有一个:把「算出来的分数」翻译成「概率」。 为什么不用「大于 0.5 就算 1」这种阶跃函数:它到处导数为 0,梯度下降没法走。 sigmoid 光滑、处处可导,才让下一步成立。 这也是整个机器学习里反复出现的套路:要优化,就得先让函数可导。很多看起来奇怪的设计(softmax、 GELU、hinge 的变体),根子都在这里。

三、交叉熵:一开始错得有多离谱

参数 损失
w = (0, 0)、b = 0 0.6931
ln 2 0.6931

w 和 b 全是 0,模型对每条数据都输出 0.5,所以交叉熵正好是 ln2。 跟标签怎么分布无关——150 比 150 也好,110 比 90 也好,都是这个数。 这个数是个标尺。 训练之后损失降到多少,跟它比才有意义:光看「0.06」你不知道好坏,看「从 0.69 降到 0.06」就清楚了。 丢掉这个标尺,就会犯一个常见错误:看到损失 0.3 觉得「还行」, 其实比瞎猜还差——0.3 意味着模型给的判断跟真值反着走。

四、梯度下降:三千轮

轮次 损失 w1 w2 b
0 0.6931 0.000 0.000 0.000
500 0.0956 1.176 1.253 -8.305
1000 0.0768 1.461 1.543 -10.403
1500 0.0695 1.643 1.724 -11.718
2000 0.0656 1.778 1.856 -12.685
2500 0.0632 1.886 1.960 -13.448
3000 0.0615 1.975 2.045 -14.077

三千轮之后损失 0.0615,比开头的 0.6931 低了 91%。 梯度那个式子是推出来的,但它的形状很好记:(预测值减真值) 乘上特征,再取平均。猜高了就往低推, 猜低了就往高推,推多少由特征大小决定。 有个细节值得看:第 0 轮 b 方向的梯度正好是 0。因为两类各 150 个、预测又全是 0.5,正负刚好抵消。 换个不平衡的数据集,这一步就不会是 0。

还有一处:最后四行每 500 轮只降 0.001 到 0.004。 这就是「还没收敛」的样子——继续降,但每步的收益越来越小。 损失曲线的尾巴是不是平的,决定你还要不要接着跑。

五、和 sklearn 对一遍

做法 w b
手写 3000 轮(无正则) [1.975 2.046] -14.078
sklearn 默认(带 L2) [2.067 2.135] -14.806
sklearn 关掉正则 [2.945 2.949] -20.757

手写和 sklearn 默认版差得不多(权重差 0.092 与 0.089),方向完全一致。 这一步就够证明手写的那条链路是对的。 但关掉正则之后,sklearn 的权重一下子涨到 [2.95 2.95],比手写的还大。 这不是 bug,是数学结论: 两类点完全分得开的时候,「分得越开越好」没有上限,权重可以一直变大。 正则项的作用就是给它一个上限。 所以三组数对不上,不是谁算错,是目标函数和迭代次数都不一样。 要比就得比同一个目标、同一个停止条件。

这条经验在调包时特别有用: 用别人写的模型之前,先知道它在优化什么。不然模型给出的结果和你期待的差很远, 你连往哪查都不知道。

六、准确率,以及它骗人的地方

版本 准确率
手写 3000 轮 0.9767
sklearn 默认 0.9767

两边完全一样。这一格数字比权重那一列更有说服力:参数可以有无数组合,分类结果只有一个。 但也要看清这个数是怎么来的:两类点分得这么开,随便画条线都到九十几。 它不能说明模型好,只能说明数据简单。 真实的分类问题里,0.85 就已经很不错了。 看准确率之前先问一句:这份数据本来有多难?没有基准的准确率是没有信息量的。

七、学习率与特征尺度

学习率 1 步后 50 步后 300 步后
0.001 0.6921 0.6534 0.6117
0.5 0.8614 0.2682 0.1164
5.0 7.2967 0.0562 0.0559

0.001 那个跑三百轮只降了一点,还得再跑几千轮; 5.0 那个第一步虽然跳到 7.30,但 50 步内就降回 0.0562。在这批数据上,大步长没出问题。 然后做一个实验:把特征乘 10,学习率还是 5.0。

情形 1 步后 2 步后 300 步后
原尺度 7.2967 — 0.0559
特征乘 10 13.8155 13.8155 13.8155

放大之后第一步就跳到 13.82,然后一直停在那个数上不动了。 停住不是收敛,是卡死了。 一步跳过头,模型把每条数据都判成 0 或 1,sigmoid 进了饱和区, 梯度变成 0——再跑三万步也是这个数。 原因很直接:梯度里带着特征,特征大十倍,梯度就大十倍,等效步长也大十倍。 所以调学习率之前先看特征的量级,或者干脆把特征标准化到同一量级。

「学习率设多少」没有通用答案,它跟数据尺度绑在一起。任何一本教材给你一个数, 都是在那本书的数据上试出来的——包括这一篇的 0.5。

八、这一篇之后

逻辑回归的核心就三样:sigmoid、交叉熵、梯度下降。手写下来不到二十行。 第 4 篇到这里把「模型是什么」讲完了。 第 17 章要把它做成 API: 你会看到,模型那部分代码只占整个项目的一小块,剩下的全是接口、校验和错误处理。 这也回答了很多人第一次做机器学习项目时的困惑:「为什么我照着教程跑通了模型, 却做不出一个能用的东西」。因为能跑通的模型和能用的服务之间,隔着第 12 到 15 章那一整套东西。

九、动手 Demo:逻辑回归:NumPy 手写一遍,再和 sklearn 对一遍

这个 Demo 从造点开始,一路手写 sigmoid、交叉熵、梯度下降,最后拿 sklearn 对三组数。 第七节最后那个实验最值得看:把特征乘十,同一个学习率的损失就再也不动了。

for step in range(3001):
    p = sigmoid(Xs @ w + b)
    gw = Xs.T @ (p - ys) / m
    gb = float(np.mean(p - ys))
    if step % 500 == 0:
        print(step, round(loss_of(w, b), 4))
    w = w - LR * gw
    b = b - LR * gb

实跑输出:

--------------------------------------------------------------------
逻辑回归:NumPy 手写一遍,再和 sklearn 对一遍
--------------------------------------------------------------------

  ① 造两类点
  --------------------------------------------------------------------
  两类各 150 个点,X 的形状 (300, 2)
  第一类中心 (2.0, 2.0)/第二类中心 (5.0, 5.0)

  ② sigmoid:把任意实数压到 0 到 1
  --------------------------------------------------------------------
  sigmoid(-6.0) -> 0.002473
  sigmoid(-3.0) -> 0.047426
  sigmoid(+0.0) -> 0.500000
  sigmoid(+3.0) -> 0.952574
  sigmoid(+6.0) -> 0.997527
  ! 0 对应 0.5,两头趋近 0 和 1 但永远到不了。
  这个函数的用处只有一个:把「算出来的分数」翻译成「概率」。
  ! 为什么不用「大于 0.5 就算 1」这种阶跃函数:它到处导数为 0,梯度下降没法走。
  sigmoid 光滑、处处可导,才让下面那一步成立。

  ③ 交叉熵:一开始错得有多离谱
  --------------------------------------------------------------------
  w = (0, 0)、b = 0 时的损失 -> 0.6931
  ln2 -> 0.6931
  ! w 和 b 全是 0,模型对每条数据都输出 0.5,所以交叉熵正好是 ln2。
  跟标签怎么分布无关——150 比 150 也好,110 比 90 也好,都是这个数。
  ! 这个数是个标尺:训练之后损失降到多少,跟它比才有意义。
  光看「0.08」你不知道好坏,看「从 0.69 降到 0.08」就清楚了。

  ④ 梯度下降:三千轮,每五百轮看一眼
  --------------------------------------------------------------------
  第 0 轮梯度 -> w 方向 [-0.753  -0.7293],b 方向 0.000000

| 轮次 | 损失 | w1 | w2 | b |
|---:|---:|---:|---:|---:|
| 0 | 0.6931 | 0.000 | 0.000 | 0.000 |
| 500 | 0.0956 | 1.176 | 1.253 | -8.305 |
| 1000 | 0.0768 | 1.461 | 1.543 | -10.403 |
| 1500 | 0.0695 | 1.643 | 1.724 | -11.718 |
| 2000 | 0.0656 | 1.778 | 1.856 | -12.685 |
| 2500 | 0.0632 | 1.886 | 1.960 | -13.448 |
| 3000 | 0.0615 | 1.975 | 2.045 | -14.077 |

  三千轮之后损失 0.0615,比开头那个 0.6931 低了 91%
  ! 梯度那个式子是推出来的,但它的形状很好记:(预测值减真值) 乘上特征,再取平均。
  猜高了就往低推,猜低了就往高推,推多少由特征大小决定。
  ! 第 0 轮 b 方向的梯度正好是 0,因为两类各 150 个、预测又全是 0.5,正负刚好抵消。
  换个不平衡的数据集,这一步就不会是 0。

  ⑤ 和 sklearn 对一遍
  --------------------------------------------------------------------
  手写 3000 轮(无正则) -> w [1.975 2.046],b -14.078
  sklearn 默认(带 L2)  -> w [2.067 2.135],b -14.806
  sklearn 关掉正则       -> w [2.945 2.949],b -20.757
  ! 手写和 sklearn 默认版差得不多(权重差 0.092 与 0.089),方向完全一致。
  ! 上面那张表最后四行,每 500 轮只降 0.001 到 0.004——这就是「还没收敛」的样子。
  手写版跑三千轮,sklearn 的优化器走的步数不一样,所以对不齐。
  ! 再看关掉正则那一行:权重是 [2.95 2.95],比手写的还大。
  ! 这不是 bug,是数学结论:两类点完全分得开的时候,「分得越开越好」这件事没有上限,
  权重可以一直变大。正则项的作用就是给它一个上限。
  ! 所以三组数对不上不是谁算错,是目标函数和迭代次数都不一样。
  要比就得比同一个目标、同一个停止条件——这也是调包之前必须先知道它在优化什么的原因。

  ⑥ 准确率
  --------------------------------------------------------------------
  手写版准确率     -> 0.9767
  sklearn 默认版   -> 0.9767
  ! 两类点分得这么开,随便画条线都到九十几。这个数不能说明模型好,只能说明数据简单。
  真实的分类问题里,0.85 就已经很不错了。

  ⑦ 学习率与特征尺度
  --------------------------------------------------------------------
  学习率 0.001 -> 1 步后 0.6921,50 步后 0.6534,300 步后 0.6117
  学习率 0.500 -> 1 步后 0.8614,50 步后 0.2682,300 步后 0.1164
  学习率 5.000 -> 1 步后 7.2967,50 步后 0.0562,300 步后 0.0559
  ! 0.001 那个跑三百轮只降一点,还得再跑几千轮;5.0 那个反而更快。在这批数据上,大步长没出问题。
  把特征乘 10,还是 5.0 -> 1 步后 13.8155,2 步后 13.8155,300 步后 13.8155
  原尺度同一组参数第 300 轮的损失是 0.0559
  ! 把特征乘 10、还是 5.0:第一步就跳到 13.82,之后一直停在那儿。
  原尺度同一个学习率也跳(第一步 7.30),但它在 50 步内就降回 0.0562。
  ! 停住不是收敛,是卡死了。
  一步跳过头,模型把每条数据都判成 0 或 1,sigmoid 进了饱和区,梯度变成 0——再跑三万步也是这个数。
  ! 原因很直接:梯度里带着特征,特征大十倍,梯度就大十倍,等效步长也大十倍。
  所以调学习率之前先看特征的量级,或者干脆把特征标准化到同一量级——这是下一章真正会做的事。
  ! 「学习率设多少」没有通用答案,它跟数据尺度绑在一起。
  任何一本教材给你一个数,都是在那本书的数据上试出来的。

  ⑧ 这一篇之后
  --------------------------------------------------------------------
  逻辑回归的核心就三样:sigmoid、交叉熵、梯度下降
  手写三千轮的损失 0.0615,sklearn 默认版 0.0600
  ! 第 4 篇到这里把「模型是什么」讲完了。
  第 17 章要把它做成 API:你会看到,模型那部分代码只占整个项目的一小块,剩下的全是接口、
  校验和错误处理。

第四节那个表最右一列一直在变,说明还没收敛;损失曲线的尾巴平不平,就看这里。 第五节三组数对不上,差别来自正则和迭代次数,不是谁算错。 第七节把特征乘十,同一个学习率的结果是卡在 13.8155 不动,不是继续降。

30 秒实验

  1. 把第七节的 LR 改成 0.05,看三千轮之后损失还能到多少。
  2. 把两类点的标准差从 1.0 改成 2.5(两类开始重叠),看准确率掉到多少。
  3. 想一想:为什么两类完全可分的时候,权重会一直变大?

常见误区

  1. 忘了先 sigmoid 就套交叉熵:ln 里出现负数,结果全是 nan。
  2. 没有基准就判断损失好坏:0.3 比瞎猜还差,因为瞎猜是 0.69。
  3. 学习率照抄教程:它跟数据尺度绑着,换个量级就卡死或发散。
  4. 拿训练集准确率当结论:数据简单的时候,随便画条线都九十几。
  5. 以为权重必须和 sklearn 一致:目标函数不同,数值本来就不同。
自测三问(点开对答案) 1. 逻辑回归的三步是什么? —— 线性打分、sigmoid 压成概率、交叉熵量误差。 2. 梯度长什么样?为什么好记? —— 预测值减真值,乘特征,取平均;猜高了往低推,猜低了往高推。 3. 为什么特征放大十倍就会卡死? —— 梯度跟着放大,等效步长也放大,一步跳进 sigmoid 饱和区,梯度归零。

第 16 章到此结束。下一篇把这些装成一个真的服务:定接口、写校验、接模型。

相关笔记