📗 第2章 神经网络
📄 一句话概括

感知器的学习规则只有一句话:分对了不动,分错了把权重往正确答案那边推。 数据线性可分时它一定停下来,实测余量从 0.02 放到 1.60,更新次数从 52 次降到 2 次。 意外的是学习率完全不影响它:0.01 和 1.00 的更新次数一模一样,都是 13 次。 数据不可分时它永不停下来——异或四个点跑满 200 轮,更新 797 次,从不收敛。

📝 本节和前面几节的关系

上一节的权重是人挑的。 要挑出与门那组数,人得先知道答案;数据一多就没法这么干了。 感知器的贡献就是把这个动作自动化:给样本、给标签,它自己调权重。 规则简单到可以写在一张卡片上,但它背后有一条收敛定理——只要数据线性可分,它就一定能停。 这一节要把这条定理的三件事测出来:什么条件下停、停得多快、什么时候永远不停。

一、一句话的规则

对每个样本,先算输出,再和正确答案比:

y = step( w*x + b )        # 网络的输出
if y != t:                 # 只有分错才动
    w = w + lr * (t - y) * x
    b = b + lr * (t - y)

t 是正确答案(0 或 1),y 是输出。 分对了 t 减 y 是 0,什么都不动; 分错了是正 1 或负 1,于是权重被推向「让这个样本更容易分对」的方向。 注意它和梯度下降的区别:这里没有损失函数,没有求导,也没有「步长沿着梯度」。 它只是「错了就改」,改的方向由错在哪一边决定。

二、它一定停下来

1958 年就证明了:如果数据线性可分,感知器在有限次更新内一定停。 而且更新次数的上界是 (R 除以余量) 的平方,R 是样本到原点的最大距离。 这句话的实用含义:数据越好分(余量越大),学得越快。 实测:

| 余量 | 学习率 | 更新次数 | 最终准确率 | 收敛 | |:---:|:---:|:---:|:---:|---|---| | 0.02 | 0.10 | 52 | 1.0000 | 是 | | 0.05 | 0.10 | 21 | 1.0000 | 是 | | 0.20 | 0.10 | 13 | 1.0000 | 是 | | 0.80 | 0.10 | 3 | 1.0000 | 是 | | 1.60 | 0.10 | 2 | 1.0000 | 是 | | 0.20 | 0.01 | 13 | 1.0000 | 是 | | 0.20 | 1.00 | 13 | 1.0000 | 是 | 余量从 0.02 涨到 1.60,更新次数从 52 次降到 2 次。 余量小的时候每一处都要小心修,余量大时一次就到位。

三、学习率完全不影响它

表里最后三行的余量都是 0.20,学习率分别是 0.01、0.10、1.00。 三行的更新次数一模一样,都是 13 次。 原因是尺度不变。 每次更新是 w 加 lr 乘方向,学习率只把所有更新按同一个比例放大。 分界线是 w 乘 x 加 b 等于 0——整条 w 和 b 一起放大,这条线根本不动。 既然线不动,那么「哪些样本分错」也不变,迭代路径就完全一致。 这和梯度下降很不一样。 梯度下降里学习率决定了「会不会跨过谷底」,是可以让训练失败的; 感知器里它只是个缩放系数,改不了结果。 这也解释了为什么早期的感知器论文很少讨论学习率——在那个框架里它确实无关紧要。

四、不可分时它永不停下来

异或只有四个点,跑满 200 轮,更新 797 次,一次都没收敛。 每轮分错的个数在 2、3、4 之间来回跳,永远不会是 0。 这不是「学得慢」,是「没有解」。 更新规则的前提是「存在一组权重能把所有样本分对」,线性不可分时这个前提不成立, 于是权重被不同的样本来回推,在几个状态之间循环。 要判断「到底是不收敛还是没解」,看的是错误数会不会变成 0,而不是损失曲线看上去有没有在降。 这也是后来引入「损失函数」的一个动因:有个连续的指标,才能判断进展。

五、它的历史位置

感知器是第一个「从数据学参数」的模型,这一点已经足够重要。 它的问题也很清楚:只有一层,只能画直线。 1969 年那次打击之后,人们转向了「加一层」, 却发现隐层的权重没有「正确答案」可以对照——输出层错了,可隐层错在哪并不知道。 这个僵局要等到反向传播才被打开,那是下一节和 2.5 节的事。

六、动手 Demo:感知器:学习规则、收敛定理,和它撞不过的那堵墙

这段代码按已知分界线造两类点,用「离分界线至少多远」当余量,然后测不同余量、 不同学习率下的更新次数;最后用异或验证不可分的情况。 重点看三处。第二段五行的更新次数 52、21、13、3、2;最后三行学习率变了但次数都是 13; 第四段异或跑满 200 轮仍然不收敛。

# -*- coding: utf-8 -*-
import unicodedata

import numpy as np


def wd(s):
    return sum(2 if unicodedata.east_asian_width(c) in ('W', 'F') else 1 for c in s)


def pad(s, n):
    return s + ' ' * max(0, n - wd(s))


def sep_data(margin=0.5, n=100, seed=1):
    """按已知分界线造两类点,并保证每个点离分界线至少 margin。

    这样数据一定线性可分,且 margin 就是「余量」:余量越大越好分。
    """
    rg = np.random.default_rng(seed)
    wstar = np.array([1.0, -0.5])
    bstar = 0.2
    X, y = [], []
    while len(y) < 2 * n:
        p = rg.normal(0.0, 1.5, 2)
        v = float(p @ wstar + bstar)
        if abs(v) < margin:
            continue
        X.append(p)
        y.append(1 if v > 0.0 else 0)
    X = np.array(X)
    y = np.array(y)
    idx = rg.permutation(len(y))
    return X[idx], y[idx]


def percep(X, y, lr=0.1, max_ep=300):
    """感知器学习规则,返回 (w, b, 更新次数, 是否收敛, 每轮错分数)。"""
    w = np.zeros(2)
    b = 0.0
    upd = 0
    bad_hist = []
    for _ in range(max_ep):
        bad = 0
        for i in range(len(y)):
            pred = 1 if X[i] @ w + b > 0.0 else 0
            if pred != y[i]:
                bad += 1
                upd += 1
                d = lr * (2.0 * y[i] - 1.0)
                w = w + d * X[i]
                b = b + d
        bad_hist.append(bad)
        if bad == 0:
            return w, b, upd, True, bad_hist
    return w, b, upd, False, bad_hist


print('[1] 感知器学习规则只有一句话')
print('  分对了什么都不做;分错了,把权重往「正确答案那一边」推这一点')
print('  w <- w + lr * (t - y) * x ,b <- b + lr * (t - y)')
print('  其中 t 是正确答案(0 或 1),y 是网络的输出')
print('')

print('[2] 数据线性可分时它一定停下来(1958 年的感知器收敛定理)')
print('  余量     学习率   更新次数   最终准确率   收敛')
for margin, lr in ((0.02, 0.1), (0.05, 0.1), (0.20, 0.1), (0.80, 0.1), (1.60, 0.1),
                   (0.20, 0.01), (0.20, 1.00)):
    X, y = sep_data(margin=margin, n=200, seed=1)
    w, b, upd, ok, hist = percep(X, y, lr=lr)
    acc = float(np.mean((X @ w + b > 0.0).astype(int) == y))
    print('  ' + pad('%.2f' % margin, 9) + pad('%.2f' % lr, 9)
          + pad('%d' % upd, 11) + pad('%.4f' % acc, 13)
          + ('是' if ok else '否'))
print('')

print('[3] 两个反直觉的地方')
print('  余量从 0.02 涨到 1.60,更新次数从 52 次降到 2 次——数据越好分,学得越快')
print('  而且余量越小涨得越快:收敛定理给的上界是 (R / 余量) 的平方')
print('  实测不严格是平方关系——每换一个余量就换了一份数据,只能看趋势')
print('  但学习率 0.01 和 1.00 的更新次数一模一样:整条 w 和 b 一起按比例缩放')
print('  分界线是 w*x + b = 0,等比例缩放之后这条线根本不动,所以感知器对学习率免疫')
print('')

print('[4] 线性不可分时,它永不停下来(异或是最小的例子)')
X = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])
y = np.array([0, 1, 1, 0])
w, b, upd, ok, hist = percep(X, y, lr=0.1, max_ep=200)
print('  异或四个点,跑满 200 轮:更新 %d 次,收敛 = %s' % (upd, '是' if ok else '否'))
print('  前 12 轮的「分错几个点」:%s' % ' '.join(str(x) for x in hist[:12]))
print('  错分数永远不会变成 0,它在几个数之间来回跳——权重不是学不会,是没地方可去')

实跑输出:

--------------------------------------------------------------------
感知器:学习规则、收敛定理,和它撞不过的那堵墙
--------------------------------------------------------------------
[1] 感知器学习规则只有一句话
  分对了什么都不做;分错了,把权重往「正确答案那一边」推这一点
  w <- w + lr * (t - y) * x ,b <- b + lr * (t - y)
  其中 t 是正确答案(0 或 1),y 是网络的输出

[2] 数据线性可分时它一定停下来(1958 年的感知器收敛定理)
  余量     学习率   更新次数   最终准确率   收敛
  0.02     0.10     52         1.0000       是
  0.05     0.10     21         1.0000       是
  0.20     0.10     13         1.0000       是
  0.80     0.10     3          1.0000       是
  1.60     0.10     2          1.0000       是
  0.20     0.01     13         1.0000       是
  0.20     1.00     13         1.0000       是

[3] 两个反直觉的地方
  余量从 0.02 涨到 1.60,更新次数从 52 次降到 2 次——数据越好分,学得越快
  而且余量越小涨得越快:收敛定理给的上界是 (R / 余量) 的平方
  实测不严格是平方关系——每换一个余量就换了一份数据,只能看趋势
  但学习率 0.01 和 1.00 的更新次数一模一样:整条 w 和 b 一起按比例缩放
  分界线是 w*x + b = 0,等比例缩放之后这条线根本不动,所以感知器对学习率免疫

[4] 线性不可分时,它永不停下来(异或是最小的例子)
  异或四个点,跑满 200 轮:更新 797 次,收敛 = 否
  前 12 轮的「分错几个点」:2 3 4 4 4 4 4 4 4 4 4 4
  错分数永远不会变成 0,它在几个数之间来回跳——权重不是学不会,是没地方可去

数据是按已知分界线造出来的,所以「一定线性可分」是构造保证的,不需要事后验证。 余量就是每个点到那条线的距离下限,是这一节唯一在变的量。 第二段的数字不是单调的平方关系,这一点要如实说。 收敛定理给的是上界,而且每换一个余量就重新造了一份数据,所以只能看趋势, 不能拿两次的比值当规律。 最后三行是最值得盯的地方。 学习率从 0.01 到 1.00 差一百倍,更新次数一个数都不差。 这不是巧合,是尺度不变性的直接结果——分界线不动,分错的集合就不动。 第四段的 797 次不要读成「快收敛了」。 它跑满了 200 轮、每轮四个样本,总共 800 次判断里更新了 797 次,意思是几乎每次判断都是错的。 权重被四个互相矛盾的样本推来推去,一直在原地打转。 代码里 percep 返回了「每轮错分数」这个列表,就是为了让你能看出震荡。 如果没有这个列表,只看最终准确率,很容易误判成「再训训就好了」。

30 秒实验

  1. 把异或那段的 max_ep 从 200 改成 2000,重跑。仍然是「收敛 = 否」,更新次数会涨到八千左右。 加轮数救不了没有解的问题。
  2. 把 sep_data 里造点的 rg.normal(0.0, 1.5, 2) 改成 rg.normal(0.0, 0.3, 2),重跑。 点会更集中,余量固定时更新次数会变少。 收敛定理里的 R(样本到原点的最大距离)也在起作用,不只是余量。
  3. 把学习率改成 100,重跑。更新次数还是 13,但权重的数值会大 1000 倍。 数值太大时浮点精度会出问题,这是「尺度不变」在工程上的唯一代价。

常见误区

  1. 「感知器是最早的神经网络,已经被淘汰了」。 把阶跃换成 sigmoid、把更新规则换成梯度下降,它就是逻辑回归; 把损失换成 hinge loss,它就是线性 SVM。 它没有被淘汰,只是换了名字留在今天的工具箱里。
  2. 「学习率越大收敛越快」。 感知器里学习率完全不影响更新次数,实测 0.01 和 1.00 都是 13 次。 它只做等比例缩放,分界线不动,迭代路径就不动。
  3. 「不收敛就是学习率没调好」。 异或跑满 200 轮更新 797 次,换任何学习率都不收敛——因为没有解。 先判断数据可分不可分,再谈调参。
  4. 「感知器能收敛,所以它是个好的分类器」。 收敛的前提是线性可分,而真实数据很少线性可分。 更麻烦的是它对噪声极敏感:一个标错的样本就能把分界线拉偏很多。
自测三问(点开对答案) 1. 感知器的学习规则是什么?为什么它不需要损失函数? —— 对每个样本算输出,分对了不动,分错了把权重加上「学习率乘方向乘样本」。 它不需要损失函数,因为它不靠求导,只靠「错在哪一边」来决定改的方向。 这也意味着它没有连续的进度指标,只能看错误数变不变。 2. 为什么学习率完全不影响感知器的更新次数? —— 因为分界线是 w 乘 x 加 b 等于 0,学习率只把所有更新按同一比例缩放。 整条 w 和 b 一起放大,这条线不动,那么哪些样本分错也不变,迭代路径完全一致。 实测 0.01 和 1.00 的更新次数都是 13 次。 3. 异或不收敛,是训练不够还是根本没解?怎么判断? —— 是根本没解。异或线性不可分,不存在一组权重能把它分对。 判断方法是看每轮的分错个数会不会变成 0,而不是看损失有没有在降。 实测跑满 200 轮、更新 797 次,错分数一直在 2 到 4 之间跳。

感知器撞的那堵墙叫「只有一层」。 加一层就能分开异或,可加层之后,隐层的权重该往哪改?输出错了,隐层并不知道自己错在哪。 下一节先看「两层能做到什么」,把墙凿开;2.5 节再讲「怎么训练两层」。

相关笔记