同一份两条螺旋的数据,三种模型: 1958 年的单层感知器 0.6000,1986 年的两层 S 型网络 1.0000, 2011 年的三层 ReLU 也是 1.0000。 训练流程几乎是同一套,差别只在激活函数和层数。 三次浪潮的卡点各不相同,但都不是「想法不对」,是「当时的训练手段配不上想法」。
上一章把深度学习放进机器学习的家族里,这一章开始拆零件。 第一个零件是最小的那个:一个神经元。 但在拆之前,先花一节看历史。 不是为了记年份,是因为神经网络的三次起落,每一次都卡在一个具体的技术困难上。 而那些困难今天还在——只是换了个名字:初始化、梯度消失、学习率。
一、三种模型,一份数据
造两条互相缠绕的螺旋线,每类 150 个点,输入两维。 然后跑三个模型:
| 年代 | 模型 | 激活函数 | 参数个数 | 训练准确率 |
|---|---|---|---|---|
| 1958 | 单层感知器 | 阶跃 | 3 | 0.6000 |
| 1986 | 两层网络 | S 型(tanh) | 33 | 1.0000 |
| 2011 | 三层网络 | ReLU | 337 | 1.0000 |
| 第一行 0.6000。 | ||||
| 两类瞎猜是 0.5000,所以它确实学到了一点东西,但也就到此为止——直线能做的就那么多。 | ||||
| 第二行只用 33 个参数就跳到 1.0000。 | ||||
| 参数只多了 30 个,能力却从「画直线」变成「画任意曲线」,因为中间多了一层非线性的变换。 | ||||
| 第三行也是 1.0000,参数翻了十倍。 | ||||
| 在这个任务上第三层是多余的——这一点第 1 章已经说过,这里再强调一次:层数是容量,容量要够用, | ||||
| 多出来的部分是浪费。 |
二、1958 年:感知器为什么会轰动
感知器的贡献不在「能分类」,而在「权重能自己学」。 在它之前,M-P 神经元的权重得人手定(下一节会看到,与门、或门的权重都是人挑的)。 感知器把这件事反过来了:给它一堆已经标好的样本,它自己调权重。 规则简单到一句话:分对了不动,分错了把权重往正确答案那边推。 当时的宣传确实过头了,但技术上的突破是真的。 「从数据里学参数」这件事,从此成了这个领域的基本动作。
三、1969 年:一句话让经费断掉
明斯基和佩珀特在《感知器》里证明:单层感知器连异或都做不了。 异或的两类在平面上是对角交叉的,而单层感知器的决策边界是一条直线。 一条直线切不开这个叉形,所以结构上就做不到——不是训练得不好。 这个结论完全正确,但它被推广成了「神经网络这条路走不通」。 推广是错的:加一层就能做。 问题在于当时没人知道怎么训练多层——隐层的权重没有「正确答案」可以对照。 这就是第一次低谷。 卡住的不是表达力,是可训练性。
四、1986 年:反向传播解决了「怎么训多层」
反向传播的思路:把输出层的误差,按权重的比例一层层往回分摊。 每一层只需要知道「我这个权重对最终误差负多大责任」,就能改自己。 这件事用链式法则就能算,数学上并不新鲜。 它解决的是「怎么算梯度」,不是「怎么算得准」。 层数一多,梯度往回传的时候会一层层乘上激活函数的导数。 S 型函数的导数最大只有 0.25,十层乘下来就是千万分之一。 这叫梯度消失,是第二次低谷的主因。 第 2.5 节会把这条链子一层层算给你看,包括「漏乘一个导数会差多少」——实测最多差到 1566 倍, 符号都能反掉。
五、2006 年与 2012 年:条件齐了
2006 年,深层信念网络用「逐层预训练」让深层网络重新可训。 先把每一层单独训好,再整体微调——相当于给梯度下降找了个好起点。 第 4、5 章讲的受限玻尔兹曼机和自编码器,当年主要就是干这个用的。 2012 年,AlexNet 在图像识别比赛上大幅领先。 它用的技术并不新:卷积、ReLU、Dropout、GPU。 新的是这三样同时到位:数据够多(ImageNet)、算力够强(GPU)、 方法够稳(ReLU 解决了梯度消失)。 所以深度学习不是某一天被「发明」出来的。 它是三个条件同时到位之后,水到渠成的结果。
六、这段历史该记住什么
第一,表达力和可训练性是两件事。 「存在一组参数能做到」和「梯度下降能不能找到那组参数」,隔着一整个优化问题。 第二,被否定的往往是当时的训练手段,不是想法本身。 1969 年的结论没错,错的是推广。这类误推在技术史上反复发生。 第三,很多「新技术」是老想法等到了配套条件。 反向传播的数学在 1970 年代就有,卷积网络的雏形在 1980 年代末就出现, 真正爆发是在数据和算力跟上之后。
七、动手 Demo:三个年代的三种模型,一起跑同一份数据
这段代码把「感知器、两层网络、三层网络」放在同一份数据、同一批超参数下跑, 看准确率和参数个数怎么变。 重点看三处。第一段那句「一条直线切不开两段螺旋」; 第二段三行的 0.6000、1.0000、1.0000 和对应的 3、33、337;第三段把三次浪潮的卡点各自归到一句话上。
# -*- coding: utf-8 -*-
import unicodedata
import numpy as np
def wd(s):
"""显示宽度:中文算 2。"""
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 spiral(n=150, C=2, seed=1, noise=0.12):
"""两条互相缠绕的螺旋线。"""
rg = np.random.default_rng(seed)
X, Y = [], []
for c in range(C):
t = np.linspace(0.4, 4.2, n)
ang = t + c * 2.0 * np.pi / C
X.append(np.column_stack([t * np.cos(ang), t * np.sin(ang)])
+ rg.normal(0.0, noise, (n, 2)))
Y.append(np.full(n, c))
return np.vstack(X), np.concatenate(Y)
def onehot(y, C):
T = np.zeros((len(y), C))
T[np.arange(len(y)), y] = 1.0
return T
def percep_train(X, y, lr=0.1, epochs=100):
"""1958 年的感知器学习规则:分错的样本把权重往它那边推。"""
w = np.zeros(X.shape[1])
b = 0.0
for _ in range(epochs):
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
d = lr * (2 * y[i] - 1)
w = w + d * X[i]
b = b + d
if bad == 0:
break
return w, b
def percep_acc(X, y, w, b):
pred = (X @ w + b > 0.0).astype(int)
return float(np.mean(pred == y))
def init_mlp(sizes, seed=0, act='relu'):
"""He 初始化(relu)或 Xavier(其它激活)。"""
rg = np.random.default_rng(seed)
W, b = [], []
for i in range(len(sizes) - 1):
sd = np.sqrt(2.0 / sizes[i]) if act == 'relu' else np.sqrt(1.0 / sizes[i])
W.append(rg.normal(0.0, sd, (sizes[i], sizes[i + 1])))
b.append(np.zeros(sizes[i + 1]))
return {'W': W, 'b': b, 'act': act}
def _f(z, act):
if act == 'relu':
return np.maximum(z, 0.0)
if act == 'tanh':
return np.tanh(z)
return 1.0 / (1.0 + np.exp(-np.clip(z, -60.0, 60.0)))
def forward(p, X, keep=False):
Z, A = [], [X]
h = X
n = len(p['W'])
for i in range(n):
z = h @ p['W'][i] + p['b'][i]
Z.append(z)
h = z if i == n - 1 else _f(z, p['act'])
A.append(h)
return (A, Z) if keep else h
def sigmoid(z):
return 1.0 / (1.0 + np.exp(-np.clip(z, -60.0, 60.0)))
def backward(p, X, T):
"""二分类:单个 logit 接 sigmoid,损失取交叉熵。"""
A, Z = forward(p, X, keep=True)
n = len(p['W'])
d = (sigmoid(A[-1].ravel()) - T.ravel())[:, None] / len(X)
gW = [None] * n
gb = [None] * n
for i in range(n - 1, -1, -1):
gW[i] = A[i].T @ d
gb[i] = d.sum(axis=0)
if i > 0:
a = A[i]
df = (Z[i - 1] > 0.0).astype(float) if p['act'] == 'relu' \
else (1.0 - a * a if p['act'] == 'tanh' else a * (1.0 - a))
d = (d @ p['W'][i].T) * df
return {'W': gW, 'b': gb}
def train(p, X, T, lr=0.1, epochs=600, batch=32, seed=0):
rg = np.random.default_rng(seed)
n = len(X)
for _ in range(epochs):
idx = rg.permutation(n)
for s in range(0, n, batch):
j = idx[s:s + batch]
g = backward(p, X[j], T[j])
for i in range(len(p['W'])):
p['W'][i] -= lr * g['W'][i]
p['b'][i] -= lr * g['b'][i]
return p
def acc(p, X, y):
return float(np.mean((sigmoid(forward(p, X).ravel()) > 0.5).astype(int) == y))
X, y = spiral(150, 2, seed=1)
T = y.astype(float)[:, None]
print('[1] 同一份数据:两条互相缠绕的螺旋线')
print(' 样本 %d 个,输入 2 维;两类各 150 个点,绕着同一个中心转' % len(y))
print(' 一条直线无论怎么摆,都不可能同时把两段螺旋切开——这就是线性不可分')
print('')
print('[2] 三个年代的三种模型,同一个数据、同一批超参数')
print(' ' + pad('模型', 30) + pad('参数个数', 10) + '训练准确率')
w, b = percep_train(X, y, lr=0.1, epochs=100)
print(' ' + pad('1958 单层感知器(阶跃)', 30) + pad('%d' % 3, 10)
+ '%.4f' % percep_acc(X, y, w, b))
p2 = init_mlp([2, 8, 1], seed=1, act='tanh')
n2 = sum(v.size for v in p2['W']) + sum(v.size for v in p2['b'])
p2 = train(p2, X, T, lr=0.2, epochs=600, batch=32)
print(' ' + pad('1986 两层 S 型(反向传播)', 30) + pad('%d' % n2, 10)
+ '%.4f' % acc(p2, X, y))
p3 = init_mlp([2, 16, 16, 1], seed=1, act='relu')
n3 = sum(v.size for v in p3['W']) + sum(v.size for v in p3['b'])
p3 = train(p3, X, T, lr=0.1, epochs=600, batch=32)
print(' ' + pad('2011 三层 ReLU(He 初始化)', 30) + pad('%d' % n3, 10)
+ '%.4f' % acc(p3, X, y))
print('')
print('[3] 三次浪潮卡在哪、又靠什么过去')
print(' 第一次浪潮(1958~1969):感知器只能画直线,异或都做不到,1969 年之后停摆')
print(' 第二次浪潮(1986~1995):反向传播解决了「怎么训多层」,但 S 型激活一层层衰减')
print(' 第三次浪潮(2006~ ):ReLU 让梯度不再衰减,加上数据与算力,层数才真正堆得起来')
print(' 这张表里三行代码几乎是同一套训练流程,差别只在激活函数和层数')
实跑输出:
--------------------------------------------------------------------
神经网络的历史:三个年代的三种模型,一起跑同一份数据
--------------------------------------------------------------------
[1] 同一份数据:两条互相缠绕的螺旋线
样本 300 个,输入 2 维;两类各 150 个点,绕着同一个中心转
一条直线无论怎么摆,都不可能同时把两段螺旋切开——这就是线性不可分
[2] 三个年代的三种模型,同一个数据、同一批超参数
模型 参数个数 训练准确率
1958 单层感知器(阶跃) 3 0.6000
1986 两层 S 型(反向传播) 33 1.0000
2011 三层 ReLU(He 初始化) 337 1.0000
[3] 三次浪潮卡在哪、又靠什么过去
第一次浪潮(1958~1969):感知器只能画直线,异或都做不到,1969 年之后停摆
第二次浪潮(1986~1995):反向传播解决了「怎么训多层」,但 S 型激活一层层衰减
第三次浪潮(2006~ ):ReLU 让梯度不再衰减,加上数据与算力,层数才真正堆得起来
这张表里三行代码几乎是同一套训练流程,差别只在激活函数和层数
第一段那句「一条直线无论怎么摆都切不开」要先接受下来。
这不是数据太难,是模型族不够——换成能画曲线的模型,同一个数据立刻就能分。
第二段第一行 0.6000 值得算一下:两类瞎猜 0.5000,它多出来 0.10。
说明线性模型不是完全没用,它至少抓住了一部分结构(螺旋的半径和角度有关系)。
但也就到这儿了。
第二段第二行只用 33 个参数就到 1.0000,这个数字要记住。
33 个参数描述两条螺旋的分界,说明「一层隐层」的表达力已经远远超过线性模型。
它靠的不是参数多,而是中间那次非线性变换。
第三行 337 个参数没有换来更好的结果,这不是坏事。
它说明在这个任务上,一层已经足够表达,多加的两层纯属浪费。
参数多不等于能力强,只等于「能表达更复杂的东西」。
代码里 percep_train 就是感知器学习规则,train 是反向传播加小批量梯度下降。
注意这两个训练的代码几乎一样长——感知器并不比反向传播简单多少,它输在模型族,不输在实现。
30 秒实验
- 把
spiral的C从 2 改成 3,重跑。 感知器那一行会掉到 0.33 附近(三类瞎猜),因为三类螺旋更不可能靠直线分开。 两层和三层仍然是 1.0000。 - 把
percep_train的epochs从 100 改成 2000,重跑。准确率不会变。 感知器不是训练不够,是它的决策边界只能是直线——加轮数改变不了这件事。 - 把 1986 那一行的
act从tanh改成relu,其它不动,重跑。 大概率也是 1.0000,但换个种子可能掉。 ReLU 的梯度不衰减,但它有「死单元」问题,初始化不好会有单元一直输出 0。
常见误区
- 「感知器是最早的神经网络,所以它很弱」。 它的弱在于「只有一层」,不在于「早」。 单层感知器今天仍然在用,就是线性分类器;换个损失函数就是逻辑回归。
- 「1969 年明斯基证明了神经网络不行」。 他证明的是「单层感知器表达不了异或」,这个结论完全正确。 被误传成「神经网络不行」,是后人的过度推广。
- 「2012 年是深度学习元年,之前都是白干的」。 反向传播 1986 年就普及,卷积网络 1989 年就用在邮编识别上, ReLU 和 Dropout 也都在 2012 年之前。 2012 年只是这些零件终于凑齐了数据与算力。
- 「历史只是故事,对动手没帮助」。 这段历史里的每个卡点今天都还在:梯度消失、初始化、学习率、过拟合。 知道它们曾经怎么卡住人,比记住年份有用得多。
自测三问(点开对答案)
1. 三种模型在同一份数据上的准确率分别是多少?差在哪里? —— 单层感知器 0.6000,两层 S 型网络 1.0000,三层 ReLU 也是 1.0000。 差别不在训练算法,在模型族:感知器只能画直线,两层网络能画曲线。 第三层没有带来提升,因为数据太简单,一层隐层已经够用。 2. 为什么「表达力」和「可训练性」要分开看? —— 表达力问的是「存不存在一组参数能做到」,可训练性问的是「梯度下降能不能找到那组参数」。 两个隐单元就能表达异或,但 20 个种子里只有 9 个训得出来。 隔在这两件事中间的,是一整个优化问题。 3. 三次浪潮分别卡在什么地方? —— 第一次卡在「多层网络不知道怎么训练」,反向传播解决了它。 第二次卡在「层一深梯度就消失」,2006 年的逐层预训练缓解了它。 第三次没有低谷,因为数据、算力、方法三样同时到位了。历史讲完了,回到最基础的那个零件。 一个神经元到底在算什么?它的权重和阈值各管什么?为什么换一组数就能从「与门」变成「或门」? 下一节从 1943 年那个式子讲起。
- ☐ (本节完,下一节 2.2 M-P 模型)