感知器的学习规则只有一句话:分对了不动,分错了把权重往正确答案那边推。 数据线性可分时它一定停下来,实测余量从 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 秒实验
- 把异或那段的
max_ep从 200 改成 2000,重跑。仍然是「收敛 = 否」,更新次数会涨到八千左右。 加轮数救不了没有解的问题。 - 把
sep_data里造点的rg.normal(0.0, 1.5, 2)改成rg.normal(0.0, 0.3, 2),重跑。 点会更集中,余量固定时更新次数会变少。 收敛定理里的 R(样本到原点的最大距离)也在起作用,不只是余量。 - 把学习率改成 100,重跑。更新次数还是 13,但权重的数值会大 1000 倍。 数值太大时浮点精度会出问题,这是「尺度不变」在工程上的唯一代价。
常见误区
- 「感知器是最早的神经网络,已经被淘汰了」。 把阶跃换成 sigmoid、把更新规则换成梯度下降,它就是逻辑回归; 把损失换成 hinge loss,它就是线性 SVM。 它没有被淘汰,只是换了名字留在今天的工具箱里。
- 「学习率越大收敛越快」。 感知器里学习率完全不影响更新次数,实测 0.01 和 1.00 都是 13 次。 它只做等比例缩放,分界线不动,迭代路径就不动。
- 「不收敛就是学习率没调好」。 异或跑满 200 轮更新 797 次,换任何学习率都不收敛——因为没有解。 先判断数据可分不可分,再谈调参。
- 「感知器能收敛,所以它是个好的分类器」。 收敛的前提是线性可分,而真实数据很少线性可分。 更麻烦的是它对噪声极敏感:一个标错的样本就能把分界线拉偏很多。
自测三问(点开对答案)
1. 感知器的学习规则是什么?为什么它不需要损失函数? —— 对每个样本算输出,分对了不动,分错了把权重加上「学习率乘方向乘样本」。 它不需要损失函数,因为它不靠求导,只靠「错在哪一边」来决定改的方向。 这也意味着它没有连续的进度指标,只能看错误数变不变。 2. 为什么学习率完全不影响感知器的更新次数? —— 因为分界线是 w 乘 x 加 b 等于 0,学习率只把所有更新按同一比例缩放。 整条 w 和 b 一起放大,这条线不动,那么哪些样本分错也不变,迭代路径完全一致。 实测 0.01 和 1.00 的更新次数都是 13 次。 3. 异或不收敛,是训练不够还是根本没解?怎么判断? —— 是根本没解。异或线性不可分,不存在一组权重能把它分对。 判断方法是看每轮的分错个数会不会变成 0,而不是看损失有没有在降。 实测跑满 200 轮、更新 797 次,错分数一直在 2 到 4 之间跳。感知器撞的那堵墙叫「只有一层」。 加一层就能分开异或,可加层之后,隐层的权重该往哪改?输出错了,隐层并不知道自己错在哪。 下一节先看「两层能做到什么」,把墙凿开;2.5 节再讲「怎么训练两层」。
- ☐ (本节完,下一节 2.4 多层感知器)