神经网络 入门

从一个神经元往上搭出一整个网络,页面上每一条主张都画成你能亲手驱动的东西。单个神经元在算什么、为什么少了那一折堆多少层都白搭、该挑哪一种折法、为什么一层就是一次矩阵乘法、宽度和深度各自买到什么 —— 以及把它们串起来的前向传播。

01

一个神经元到底在算什么

一个神经元收进几个数,送出一个数。它由三个选择构成:一个方向、一个偏移、一次折叠。

大家背的那一行是 output = act(W·x + b)。写成这样它就只是算术,而算术好背、难想象。两个输入已经够把它的每一部分都画出来,所以整个第 1 节都待在同一个平面上,输入就是这平面上的一个点。

先看乘法。输入是这个平面上的一个点,权重向量是一个方向,而 w·x 就是输入沿这个方向走了多远 —— 把输入拖到任何地方,盯着那段加粗的琥珀色线段看:

x₁ = 1.80, x₂ = 1.00. 在图上任意位置拖动即可移动输入;方向键每次移动 0.1,Home 键复位
沿 w 的投影为 1.33,w·x = 1.14

注意 w·x 没有在量什么。它量的不是输入有多大:让输入垂直于权重箭头移动,这个数一点都不变。点积是一个相似度评分,而权重就是这个神经元在找的那个模式。

光有评分还没有零点。偏置提供了零点:z = w·x + b,所有满足 z = 0 的输入构成一条直的边界。拖动偏置,看着边界离开原点:

b = 0.20,样本输入得分 1.34

边界从来不转 —— 它只是平移,平移的量正好是 −b/|w|,也就是那支紫色箭头量出来的长度。到 时,青色的样本点自己没动,却落到了沉默的那一侧。没有偏置,网络里每一条边界都必须穿过原点。

转动是权重的活。别碰偏置,转动权重向量:边界始终与它垂直,扫过整个平面,样本的分数每转一圈变号两次:

w 在 339°,样本输入得分 1.33

所以 n 个权重在 n 维空间里挑一个方向,那一个偏置挑边界沿这个方向坐在多远处。每个神经元 n + 1 个数,训练要学的正是这些。单个神经元能表达的一切,都在这两个选择里。

还差一块。让输入沿权重方向走一遍,把输出画出来 —— 激活前是一条直线,而整流器在边界处把它折了一次:

t = 1.33,z = 1.34,输出 = 1.34

盯住那个折点。一个整流神经元就是一个铰链:一侧是平的,另一侧是一条直上的斜坡,铰接点正好落在边界穿过的地方,即 t = −0.23。滑到 ,这个神经元什么都不输出。这一折就是全部的非线性 —— 下一节讲的正是:没有它的网络根本不算网络。

02

为什么那一折不能省

连着两个线性层,等于一个线性层。整流器是网络里唯一能改变「能表达什么」的部件。

中间什么都不放,堆一百个矩阵,你什么都没买到。矩阵乘法满足结合律,所以 W₃(W₂(W₁x)) 就是 (W₃W₂W₁)x ——一个同样形状的矩阵,表达能力和单层完全一样。

这里放两个。一格输入点先过 B 层、再过 A 层;那圈琥珀色轮廓是同一个方块被它们的乘积矩阵送到的地方。拖滑块走完两层:

走完两层的 0%

注意那格点每次都正好落在轮廓上 ——也一样。两个 2×2 矩阵花了 8 个数,做出一个 4 个数就能表达的映射。堆得再深也一样:线性映射的复合还是线性映射。

这个上限很容易亲手摸到。XOR 要求恰好一个输入为 1 时输出 1。转动并平移边界,试着把四个带标签的点都放到和标签相符的那一侧:

4 个点里对了 3 个

3 个。永远是 3 个 —— 把所有角度和所有偏移扫一遍也到不了 4,总有第四个点是错的。一个神经元只能把平面切成两半,而没有任何一刀能把对角的两个角和另外两个分开。这个反驳让神经网络在整个 1970 年代几乎停摆。

整流器的贡献在于:它不是一刀,而是一折。落在神经元边界沉默那一侧的一切,都被压平到边界上。拖滑块把这一折合上:

折叠完成 0%

看被裁掉的点去了哪。它们不是在平面上滑动,而是塌到边界上,因为它们的输出是 0,而无论你从哪里过来,0 都只是同一个数。这一层是故意在丢信息 —— 正是这一点让它不可逆,也让它有用。

两折就够 XOR 用了。两个隐藏神经元看的是同一个方向 x₁ + x₂,第二个再把远角那一块减回去。一个一个装上去:

什么都没装,4 个里对 0 个

看分数,别看那两条线。什么都不装就白拿 4 个里的 2 个,装上 h₁ 变成 3 个,而 h₂ 在输出把它减掉之前根本不改变任何答案 ——,分数才是 4。网络回答 1 的那片区域是两折之间的那条带,任何单个神经元都表达不出来。

03

挑哪一种折法

你挑哪个非线性,决定了梯度能往回走多远,以及路上会死掉多少神经元。

真正在用的基本就四个:relu、sigmoid、tanh 和 gelu。对每一个,重要的不是曲线本身而是它的斜率,因为反向传播唯一要乘的就是斜率 —— 每往回一层乘一次。

琥珀色的曲线是激活函数,青绿色那条是它的斜率。沿 z 移动标记,再在四个之间切换;读数会给出你站的那一点上的两个值:

relu 在 z = 1.00:输出 1.00,斜率 1.00

看斜率,别看输出。 relu 只要开着,斜率就正好是 1,梯度原样穿过。sigmoid 在 处最大也才 0.25,两边往外都在掉。每层这个 4 倍的差距,决定了一叠深网络到底能不能训起来。

把 sigmoid 从零点往外跟一段,用它自己的坐标轴。纵轴是对数的 —— 往下一格就是十分之一 —— 那条玫红虚线标的是百分之一的斜率:

|z| = 0.0 时斜率是 2.50×10⁻¹,10 层这样的层就掉到 10⁻⁶

在最好的位置 z = 0,斜率也才四分之一,十层叠起来梯度就只剩百万分之一。推到 ,光一层就要付 400 倍的代价。这就是饱和,也是 1990 年代只敢用两个隐藏层的原因。

relu 的失败方式正好相反,而且是永久的。在沉默的那一侧它的斜率正好是 0,所以一个 b已经掉到整批输入之下的神经元,再也没有梯度可以爬回来:

b = 0.20,16 个输入里还有 7 个能产生梯度

拖到 ,十六根柱子全变成玫红:这个神经元对每一个样本都输出 0,所以对每一个样本梯度都是 0,所以再也不会被更新。它不是慢,是死了。leaky relu 和 gelu 存在的理由就是:斜率正好为 0,离「永久失去一个神经元」只差一步。

04

一层就是一个矩阵

不是一袋神经元,而是一次矩阵乘法 —— 而这个矩阵的形状,就是这一层的全部开销。

把 m 个神经元并排放,每个都读同样的 n 个输入,它们各自长度为 n 的权重向量就叠成一个 n × m 的矩阵。一次乘法同时算出全部 m 个激活前值 —— 这正是 GPU 能把一层当成一条指令流跑的原因。

先看图,再数数。矩阵把每一个输入方向送到某个输出方向 —— 拖动青绿色的输入绕单位圆走一圈,看琥珀色的输出画出它变成了什么:

30°. 在图上任意位置拖动即可转动输入向量;方向键每次转 4 度
输入在 30°,输出长度是它的 1.30 倍

注意那个圆永远只会变成椭圆,别的都不会:线性映射只会拉伸和旋转,仅此而已。输出的长度按方向不同在 0.94 到 1.30 倍之间,两者之比就是这一层的条件数 —— 也就是它把梯度扭曲的倍数。

接下来数数。每个神经元占一列琥珀色的权重,外加一个紫色的偏置。把神经元一个个拖进这一层,从你正在搭的这张网格上读出参数量:

1 个神经元,9 个参数

配 8 个输入,就是 8×12 + 12 = 108 个数,两项都重要:权重按 n·m 增长,偏置只按 m 增长,所以宽输入上的宽层几乎全是权重。宽度翻一倍,参数量和运算量一起翻一倍。

批处理改变的是运算量,不是权重。把 B 个输入按行叠起来,这一层就变成一次矩阵乘矩阵 —— 但前提是内维必须对上。把 W 的行数从 128 拖开看看:

批 8,形状对上了

两条内边是按同一把尺子画的,所以这件事是一幅画而不是一句断言:两边接不上,乘积就不存在。批处理还买到了算术强度 —— 批为 1 时,这一层每读 1 字节权重只做 0.5 次运算,批为 32 时是 16 次。一张 H100 SXM 的普通 fp32 大约 67 TFLOP/s,对上 3.35 TB/s 的 HBM3,所以低于大约每字节 20 次运算,它就是在等内存,而不是在算。

05

宽度对上深度

两者都在买直线段,而且买到的数量完全不同。宽度对参数是线性的,深度是指数的。

一个单输入单输出的 relu 网络,永远是一个分段线性函数 —— 不管权重取什么值。所以「这个架构有多强的表达力」这次有精确答案:它最多能画出多少段直线?

一个 k 个神经元的隐藏层,最多给 k + 1 段。加神经元,看网络怎么向目标曲线收拢;读数给出整个区间上最大的误差:

1 个神经元,2 段,最大误差 1.000

每个神经元正好贡献一个折点。从 到 ,最大误差从 0.182 降到 0.059 —— 层宽翻倍换来约 3 倍,这就是宽度的常见回报。段是一段一段来的。

深度买法不一样。两个 relu 神经元能把区间对折一次;下一层再把折过的东西再折一次,于是段数是相乘而不是相加。加层看看:

0 层,1 段直线

、每层 2 个神经元 —— 总共 10 个神经元、31 个参数 —— 画出32 段直线。单层要拿到同样的段数得用 31 个神经元、94 个参数。深度是复合,宽度是拼接;这就是 Telgarsky 的分离定理,而这幅图就是它的构造。

把两者放到同一张图上。纵轴是对数的,每格十倍,滑块把同一份参数预算分两种花法 —— 全买宽度或全买深度:

31 个参数:走宽度买到 11 段,走深度买到 32 段

在 处,宽层能到 21 段,深栈能到 1,024 段。要看形状,不要看高度:在对数轴上,一条直线就是指数增长。它没说的是训练能不能找到这些段 —— 这是架构能表达什么的上界,不是梯度下降会搭出什么的保证。

深度是要还账的,账在回程上到。每多一层,梯度就多乘一个因子,乘积按同样的方式指数化。把层数推出安全带之外:

走过 10 层后,梯度是出发时的 1.07×10⁻¹

每层增益 0.8 时,后是 1.4×10⁻⁵, 100 层后是 2×10⁻¹⁰ —— 这一步更新已经是舍入误差了。增益 1.2 时,同样 50 层是 9,100,第一次大步就把损失变成 NaN。残差连接、归一化层、以及小心的初始化,存在的理由都是把每层这个增益按在 1 附近。

06

前向传播

没有新东西要发明了:第 4 节那个矩阵用三次,中间夹上第 2 节那一折,还有一个向量每一步都在改变形状。

拿一个 4 个数的输入和一个 4 → 6 → 4 → 3 的网络。每个中间值都是一个向量,把这些向量画成柱子而不是写成数字,真正要紧的东西就露出来了:每一层的输出有多少能活过整流器。

先只看第一层。上面一排是同一个输入的6 个激活前值,下面一排是整流器留下的部分。让输入沿一个方向扫过去:

t = 0.00,6 个神经元里有 3 个在零以上

看有多少根柱子不见了。在初始输入处,6 个神经元里有 3 个在零以上,也就是说这一层的输出有一半正好是 0 —— 不是很小,是0。relu 网络天生稀疏,而正是这份稀疏让下面一排携带的信息严格少于上面一排。

现在看整趟。7 个阶段,每个要么是一次矩阵乘法加偏置,要么是一次整流;按播放,或者拖动进度条,看那个向量怎么改变长度和形状:

第 1 步,共 7 步:输入 x

看宽度,别看数值:进来 4 个数,然后 6 个,然后 4 个,出去 3 个。每个阶段都只是两种操作之一,在这个规模上 60 次乘加加 10 次比较就产出了全部答案。而这些中间向量每一个都得存下来,这笔账随批大小增长,权重那笔不会:

真实模型就是同样的形状配更大的数字。这是每门课都从它开始的分类器,784 → h → h → 10,每个权重矩阵一根柱子。拖动隐藏层宽度:

h = 128,118,282 个参数

注意参数都在哪。h = 128 时模型有 118,282 个参数,其中 85% 坐在第一层:784 个输入实在太多要读了。每张图 236,032 次运算、fp32 下 462 KB 权重。批大小 128 时还要再加 525 KB 激活值 —— 比权重还多 —— 这就是对训练比对推理贵得多的原因。

07

输出层,以及那七行

最后一层给出的是 logits —— 没有刻度的数。 softmax 给它一个刻度。

logits 既无界也未归一:网络里没有任何东西有理由产出落在 [0, 1] 里的值。softmax 做的是取指数再相除,把任意向量变成一个分布,同时完全不动排序。

把第一个 logit往上拖,看另外三个概率怎么替它买单,再拖温度 —— 取指数之前把每个 logit 除以 T,是「自信」和「平坦」之间唯一的旋钮,到 时最大的那一类几乎吃掉全部:

T = 1 时,最大的一类占 0.554

注意概率永远加起来是 1,顺序也从不改变。整个前向传播就是七行:

def forward(x, layers):
    for W, b in layers[:-1]:
        x = np.maximum(0, x @ W + b)
    W, b = layers[-1]
    z = x @ W + b
    z -= z.max()
    return np.exp(z) / np.exp(z).sum()

z -= z.max() 不是装饰。softmax 是平移不变的,减掉最大值不改变任何答案 —— 不减则 exp(1000) 会溢出,整个向量回来就是 NaN,还不抛错。