📊 第4章 机器学习的基础知识
📄 一句话概括

有一类问题一条直线分不开,比如异或:(0,1) 和 (1,0) 是一类。 直线模型训练两万轮,四个输出全挤在 0.5 附近——不是没训好,是表达不出来。 加两个隐藏神经元,同样两万轮,四个全对(0.008 / 0.993 / 0.991 / 0.007)。

📝 本章和前面几章的关系

上一篇讲过拟合。这一篇讲一种「怎么调都学不会」的情况,以及它的解法。 教材 4.5 节:深度学习——引发机器学习热潮的火种。 这一节篇幅不长,但它解释了为什么 2012 年之后机器学习突然变得无所不能。 答案不是算法变聪明了,是层数加上了、数据和算力跟上了。

一、先看一个分不开的问题

四个输入组合,对应四个输出:00 得 0,01 得 1,10 得 1,11 得 0。 这个规律叫异或。它的特点是:单独看任何一个输入,都决定不了输出。 输入 a 是 1 的时候,输出可能是 1(b=0)也可能是 0(b=1)。必须两个一起看。

画在平面上:(0,1) 和 (1,0) 归一类,(0,0) 和 (1,1) 归另一类。你画不出任何一条直线把它们分开。 这就是「线性不可分」。上一节那个 y = wx + b 的模型,本质就是画一条直线,所以它必然失败。

二、直线模型试一次

演示真的训了两万轮。四个输出是 0.533、0.500、0.467、0.434。 全挤在 0.5 附近,等于什么都没判断出来。 这不是训练不充分,是模型的表达能力不够。 再训两百万轮也是这个结果。

这种情况最容易被误判成「数据不够」或者「学习率不对」,然后白折腾很久。 判断方法:如果模型在训练集上怎么调都上不去,那就是表达能力的问题,不是训练的问题。

三、加两个隐藏神经元

在输入和输出之间插一层,放两个神经元。每个神经元自己做一次加权求和再过一次 S 形函数。 结构变成:两个输入 → 两个隐藏 → 一个输出。参数从 3 个变成 9 个。 同样训两万轮,结果 0.008、0.993、0.991、0.007。四个全对。

为什么加了就行?因为隐藏层先把输入换了个表示。 原来四个点挤在一个平面上,一条线分不开。 隐藏层把它们映射到一个新空间,在那个空间里,两类点就分得开了。 这件事可以直观看:两个隐藏神经元相当于画了两条线, 输出层再对这两条线的结果做一次组合——两条线能围出一块区域,区域就能分开异或的两类。

四、「深」是什么意思,代价是什么

一层隐藏已经能分开异或。要认人脸、听懂一句话,就得几十上百层。 「深」就是层数多。 每多一层,能表达的形状复杂一档,参数也多一批。 代价有三个:需要更多数据来喂(参数多容易过拟合)、需要更多算力来训、更难解释为什么这么判断。

第三个代价最容易被忽略。一个线性模型你能指着 w=3 说「x 每多 1,y 多 3」; 一个一百层的网络,你说不清它为什么把这张图判成猫。 所以教材说 4.5 节是「火种」,不是「终点」。 深度学习打开了能力上限,但也带来了新的问题:可解释性、数据依赖、算力成本。 这也是为什么现在很多场景仍然用简单模型——能解释、够用、便宜,比「先进」重要。

五、动手 Demo:异或:直线学不会,加两个神经元就会了

先让一个线性模型学异或,训两万轮,看它卡在哪。 再加一层(两个隐藏神经元),同样训两万轮。 手写前向和反向传播,不用任何框架。

def sig(v):
    return 1.0 / (1.0 + math.exp(-v))

# 一层隐藏:2 个隐藏神经元 + 1 个输出
for _ in range(20000):
    for (a, c), y in zip(X, Y):
        h = [sig(W1[j][0]*a + W1[j][1]*c + B1[j]) for j in range(2)]
        o = sig(sum(W2[j]*h[j] for j in range(2)) + B2)
        do = (o - y) * o * (1 - o)          # 输出层误差
        for j in range(2):
            W2[j] -= lr * do * h[j]
            dh = do * W2[j] * h[j] * (1 - h[j])   # 往隐藏层回传
            W1[j][0] -= lr * dh * a
            W1[j][1] -= lr * dh * c
        B2 -= lr * do

实跑输出:

--------------------------------------------------------------------
深度学习:加一层隐藏层,异或就学会了
--------------------------------------------------------------------

  ① 先看这个问题的形状
  --------------------------------------------------------------------

| 输入 a | 输入 b | 想要输出 |
|:---:|:---:|:---:|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |

  这叫异或。
  同样输入 1 的时候,输出要看另一个输入是几——这种「要看两个输入配合」的规律,一条直线分不开。
  画在平面上:把 (0,1) 和 (1,0) 归一类、(0,0) 和 (1,1) 归另一类,你画不出任何一条直线把它们分开。

  ② 直线模型试一次,学不会
  --------------------------------------------------------------------
  训练两万轮之后:
    (0,0) 想要 0,输出 0.533
    (0,1) 想要 1,输出 0.500
    (1,0) 想要 1,输出 0.467
    (1,1) 想要 0,输出 0.434
  四个输出全挤在 0.5 附近。它不是没训练好,是这个形状根本表达不出来。

  ③ 加两个隐藏神经元
  --------------------------------------------------------------------
  也是两万轮:
    (0,0) 想要 0,输出 0.008  对
    (0,1) 想要 1,输出 0.993  对
    (1,0) 想要 1,输出 0.991  对
    (1,1) 想要 0,输出 0.007  对
  四个全对。
  多出来的那两个神经元,干的活就是先把输入换个表示——原来分不开的两类,换到新坐标里就分得开了。

  ④ 深度学习的「深」是什么意思
  --------------------------------------------------------------------

| 层数 | 能表达的形状 | 参数个数(本例) |
|---|---|---:|
| 0 层(直接线性) | 一条直线 | 3 |
| 1 层隐藏(2 个神经元) | 能拐弯的分界 | 9 |
| 多层 | 任意复杂形状 | 成百上千 |

  每加一层,能表达的形状复杂一点,参数也多一批。所谓「深」,就是层数多;「学习」,还是调那些参数。
  代价也在这儿:参数一多,就需要更多数据来喂,更多算力来调。
  这就是教材 4.5 节说的「引发机器学习热潮」——不是算法突然变聪明了,是数据和算力终于跟得上了。

第一张表就是异或的真值表,四行。看着简单,但它是深度学习史上最著名的小例子。 第二段那四个 0.5 附近的输出,是「表达能力不足」的标准表现。记住这个症状:训练集上怎么调都上不去。 第三段加了两个神经元就全对。注意参数只从 3 个变成 9 个——不是靠更多参数硬堆出来的,是靠结构变了。 最后一段那段反向传播代码是整个深度学习的骨架:算输出误差,按权重回传给隐藏层,各自调整。 所有框架内部都长这样。

30 秒实验

  1. 把隐藏神经元从 2 个改成 1 个,看还能不能学会异或。
  2. 把学习率 lr 从 1.0 改成 0.1,看要多少轮才能学会。
  3. 把隐藏层加到 3 层(每层 2 个),看会不会更快收敛——或者反而更难训。

常见误区

  1. 「模型学不会就是数据不够。」先看是不是表达不了。线性模型永远学不会异或,给多少数据都没用。
  2. 「层数越多越好。」层多会带来过拟合、训练困难、不可解释。够用就行。
  3. 「深度学习取代了传统方法。 」在很多结构化数据的场景里,简单的线性模型或树模型效果一样好,还更便宜。
自测三问(点开对答案) 1. 为什么一条直线分不开异或的两类? —— 因为 (0,1) 和 (1,0) 在对角线上,(0,0) 和 (1,1) 在另一条对角线上。 任何一条直线都无法把这两组点分到两侧。 2. 加了隐藏层之后,为什么就能分开了? —— 隐藏层把输入映射到了一个新的空间。在原空间里线性不可分的问题,在新空间里变得线性可分了。 3. 「深」的代价是什么? —— 三个:需要更多数据、需要更多算力、更难解释。 参数多容易过拟合,训练贵,而且说不清它为什么这么判断。

下一篇讲工具与分工:为什么建模只占工程师两成的时间。

相关笔记