同一份三条螺旋的数据,同一个训练算法,只换模型的层数: 没有隐层 0.5556,一层 8 个隐单元 1.0000,两层各 16 个也是 1.0000。 差别不在算法,在模型能不能把输入「揉」成新的坐标。 没有隐层时模型只会画直线,加上隐层之后它先换坐标再画线。 所以深度学习不是机器学习之外的东西,它就是机器学习里「模型层数比较多」那一支。
先把两件事分清。 机器学习是一门学科:让计算机从数据里找出规律,而不是靠人一条条写规则。 深度学习是这门学科里的一种做法:用很多层叠起来的网络去表达那个规律。 从关系上说,深度学习是机器学习的子集。 它没有推翻前面的东西——梯度下降、最小二乘、正则化,这些在深度学习里一个都没少, 只是被搬到了一个更深的结构里。 这一节要回答的就是最基础的那个问题:多加几层,到底改变了什么。
一、先看一份线性模型做不动的数据
造三条互相缠绕的螺旋线。每条从中心往外绕,角度逐渐偏开。 三类各 150 个点,输入只有两维。 这份数据的特点是:任何一条直线都切不开三类。 你可以在纸上试——无论把直线摆成什么角度,一条螺旋线总会和另一条交错。 线性模型能做的事,就是在平面上画直线(多类时画几条)。 所以它必然失败,而且不是「训练不够」的失败,是「结构上做不到」的失败。
二、只换层数,别的一概不动
三种结构,同一个数据、同一个优化方法、同一批超参数:
| 结构 | 参数个数 | 训练准确率 |
|---|---|---|
| 没有隐层 | 9 | 0.5556 |
| 一层 8 个隐单元 | 51 | 1.0000 |
| 两层各 16 个 | 371 | 1.0000 |
| 第一行 0.5556。 | ||
| 三类瞎猜是 0.3333,所以它确实学到了一点东西,但也就到此为止——直线能做的就这么多。 | ||
| 第二行直接跳到 1.0000,只用了 51 个参数。 | ||
| 注意这个数字:51 个参数就够描述三条螺旋的分界, | ||
| 说明「一层隐层」的表达力已经远远超过线性模型。 | ||
| 第三行也是 1.0000,但参数从 51 涨到 371。 | ||
| 在这个任务上,第二层是多余的——数据太简单,用不上。 | ||
| 这是一个该早点建立的观念:层数不是越多越好,是「够用就好」。 | ||
| 后面第 6 章讲泛化能力时,这句话会反复出现。 |
三、隐层到底做了什么
把隐层想成一个「换坐标」的动作。 原来的坐标是 (x1, x2),也就是样本在平面上的位置。 隐层把每个点映射到一个新的坐标 (h1, h2, ..., h8)——这个新坐标不是人定的,是训练出来的。 然后输出层在这个新坐标里画直线。
没有隐层: y = W * x + b 在原始坐标里画直线
一层隐层: y = V * relu(W * x + b) + c 先换坐标,再画直线
两层隐层: y = U * relu(V * relu(W * x + b) + c) + d 换两次
关键在于 relu 那个非线性。 如果没有它,两层线性变换叠起来还是线性变换(矩阵乘矩阵还是矩阵),层数再多也等于一层。 所以「深度」之所以有意义,前提是每层之间都夹了一个非线性。 这也是深度学习最容易被误解的地方。 它不是「把线性模型堆很多层」,而是「线性 + 非线性」交替堆很多层。 少了非线性,堆多少层都是一场空。
四、那它和机器学习其它方法是什么关系
把机器学习的方法按「模型长什么样」分,大致三类:
| 类别 | 预测值的样子 | 典型代表 |
|---|---|---|
| 线性模型 | 输入的加权和 | 最小二乘、逻辑回归、支持向量机 |
| 核模型 | 训练样本的加权和 | 核回归、核支持向量机 |
| 层级模型 | 多层的复合 | 神经网络、深度学习 |
| 这三类不是互相取代的关系。 | ||
| 线性模型至今仍是很多场景的首选——它快、可解释、样本要求低。 | ||
| 深度学习胜出的地方是:输入是原始数据(像素、波形、文字),而人很难说清「该看哪些特征」。 | ||
| 所以更准确的说法是:深度学习把「设计特征」这一步也交给了模型。 | ||
| 这件事的代价是需要更多数据、更多算力,收益是省掉了最难的那部分人工。 |
五、动手 Demo:深度学习与机器学习:一样的数据,不一样的层数
这段代码造三条互相缠绕的螺旋线,然后只改网络的层数,其它一概不动,看训练准确率怎么变。 重点看两处。第二段三行 0.5556、1.0000、1.0000,以及各自的参数个数 9、51、371; 第三段关于 relu 的那句——没有非线性,层数再多也等于一层。
# -*- coding: utf-8 -*-
import numpy as np
def spiral(n=150, C=3, seed=1, noise=0.12):
"""C 条互相缠绕的螺旋线。"""
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 init_mlp(sizes, seed=0):
"""He 初始化:每层权重的标准差取 sqrt(2 / 输入个数)。"""
rg = np.random.default_rng(seed)
W, b = [], []
for i in range(len(sizes) - 1):
W.append(rg.normal(0.0, np.sqrt(2.0 / sizes[i]), (sizes[i], sizes[i + 1])))
b.append(np.zeros(sizes[i + 1]))
return {"W": W, "b": b}
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 np.maximum(z, 0.0)
A.append(h)
return (A, Z) if keep else h
def softmax(S):
S = S - S.max(axis=1, keepdims=True)
E = np.exp(S)
return E / E.sum(axis=1, keepdims=True)
def backward(p, X, T):
A, Z = forward(p, X, keep=True)
n = len(p["W"])
d = (softmax(A[-1]) - T) / 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:
d = (d @ p["W"][i].T) * (Z[i - 1] > 0.0)
return {"W": gW, "b": gb}
def train(p, X, T, lr=0.1, epochs=200, 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(np.argmax(forward(p, X), axis=1) == y))
X, y = spiral(150, 3, seed=1)
T = onehot(y, 3)
print('[1] 同一份数据:三条互相缠绕的螺旋线')
print(' 样本 %d 个,三类各 %d 个,输入 2 维' % (len(y), 150))
print(' 任何一条直线都切不开这三条线,这就是「线性不可分」')
print('')
print('[2] 换模型的层数,别的一概不动')
print(' 结构 参数个数 训练准确率')
for sizes, tag in (([2, 3], '没有隐层'), ([2, 8, 3], '一层 8 个'), ([2, 16, 16, 3], '两层各 16')):
p = init_mlp(sizes, seed=1)
npar = sum(w.size for w in p['W']) + sum(b.size for b in p['b'])
p = train(p, X, T, lr=0.3, epochs=800, batch=32)
print(' %-16s %4d %.4f' % (tag, npar, acc(p, X, y)))
print('')
print('[3] 那「深度学习」到底深在哪')
print(' 没有隐层时,模型就是输入的加权和,再怎么优化也只能画直线')
print(' 加上隐层之后,模型先把输入揉成新的坐标,再在坐标里画直线')
print(' 两层就是把这个动作做两遍')
print(' 所以深度学习不是一个新东西,它就是「层数更多的神经网络」,仍然属于机器学习')
实跑输出:
--------------------------------------------------------------------
深度学习与机器学习:一样的数据,不一样的层数
--------------------------------------------------------------------
[1] 同一份数据:三条互相缠绕的螺旋线
样本 450 个,三类各 150 个,输入 2 维
任何一条直线都切不开这三条线,这就是「线性不可分」
[2] 换模型的层数,别的一概不动
结构 参数个数 训练准确率
没有隐层 9 0.5556
一层 8 个 51 1.0000
两层各 16 371 1.0000
[3] 那「深度学习」到底深在哪
没有隐层时,模型就是输入的加权和,再怎么优化也只能画直线
加上隐层之后,模型先把输入揉成新的坐标,再在坐标里画直线
两层就是把这个动作做两遍
所以深度学习不是一个新东西,它就是「层数更多的神经网络」,仍然属于机器学习
第一段那句「任何一条直线都切不开」要先接受下来。
这不是数据太难,是模型族不够——换成能画曲线的模型,同一个数据立刻就能分。
区分「数据难」和「模型不够」是这本书里反复要用到的一个判断。
第二段第一行 0.5556 值得算一下:三类瞎猜 0.3333,它多出来 0.22。
说明线性模型不是完全没用,它至少抓住了一部分结构(螺旋的半径和角度有关系)。
但也就到这儿了。
第二段第三行「两层各 16 个」参数 371 而准确率没涨,这不是坏事。
它说明在这个任务上,一层已经足够表达,第二层纯属浪费。
参数多不等于能力强,只等于「能表达更复杂的东西」——而这里不需要更复杂的东西。
第三段那三行公式要连起来看。
第一行是线性,第二行多了一个 relu,第三行多了一层 relu。
如果去掉两个 relu,第二行和第三行都会退化成第一行——矩阵乘矩阵还是矩阵。
这是「深度」能成立的全部技术前提。
代码里 init_mlp 用的是 He 初始化(标准差取 sqrt(2 除以输入个数))。
这个细节在第 2 章会细讲,这里只要知道:初始化不是随便取,取错了深层网络根本训不起来。
30 秒实验
- 把
spiral的noise从 0.12 改成 0.5,重跑。 三条线会缠在一起分不清,一层隐层的准确率会明显掉。 这时加第二层开始有用——因为需要更复杂的边界。 - 把
spiral的C从 3 改成 5,重跑。五条螺旋更难分,看一层 8 个隐单元还够不够。 不够就说明隐单元的个数也是一种「容量」。 - 把
init_mlp里的np.maximum(z, 0.0)全部删掉(包括 backward 里的导数),重跑。 三层结构会退化成和一层一样的结果。 这是理解「非线性」最直接的一次实验。
常见误区
- 「深度学习是机器学习之外的新东西」。 它是子集,不是并列。 梯度下降、损失函数、正则化、训练集验证集划分,这些在深度学习里一个都没变。 变的只是模型的形状。
- 「层数越多效果越好」。 实测两层各 16 个(371 个参数)在这个任务上和一层 8 个(51 个参数)一样准。 层数是容量,容量要匹配任务的复杂度,多了就是浪费,还可能过拟合。
- 「神经网络就是个黑箱,不知道它在干什么」。 至少机制是清楚的:一层线性变换加一个非线性,一层层叠上去。 不清楚的是「学到的那些中间表示到底对应什么含义」——这是两件事。 前者是数学,后者是解释。
- 「不用非线性也一样,反正层数多」。 不行。 连续两个线性变换的复合仍然是线性变换,数学上等价于一层。 没有 relu、sigmoid、tanh 这些,深度在网络里等于零。
自测三问(点开对答案)
1. 深度学习和机器学习是什么关系? —— 深度学习是机器学习的子集,不是并列关系。 它用的是同一套训练思路(梯度下降、损失函数、正则化),区别只在模型: 用多层「线性 + 非线性」的复合代替单层线性。 2. 为什么加了隐层,线性模型做不到的事就能做到了? —— 因为隐层把输入映射到了一个新坐标,输出层在那个新坐标里画直线。 在原坐标里切不开的三条螺旋,换个坐标就可能被几条直线分开。 实测训练准确率从 0.5556 跳到 1.0000,只用了 51 个参数。 3. 如果去掉每层之间的非线性激活,多层网络会变成什么? —— 会退化成一层线性模型。因为连续两个线性变换的复合还是线性变换,矩阵相乘仍然是矩阵。 层数再多也等于一层。这是「深度」能成立的技术前提。「多加一层就能做到」这件事,今天听起来理所当然。 可它曾经把整个领域卡了十几年——而且卡住它的,只是一个四行的真值表。 下一节从那四行讲起。
- ☐ (本节完,下一节 1.2 深度学习的发展历程)