跳到主要内容

神经网络登场:激活函数、层与 nn 模块

这一章讲三件事: 一个「神经元」到底是什么(提示:一点生物学都不剩);为什么叠线性层没用、必须夹非线性;以及 PyTorch 的 nn 模块系统怎么把「换模型」变成只改三行。 位置:第 04、05 章造好了训练机器,这一章把机器里「模型」那个零件从一条直线换成一座塔。

1. 一个神经元:一句话就能说完

忘掉大脑。书里说得干脆:1958 年 Rosenblatt 的感知机确实受神经科学启发,但今天的神经网络和大脑「只有一丝相似」——两者大概只是碰巧用了同一类「逼近复杂函数」的数学策略1

一个神经元就是:输出 = f(w·x + b)。 先做第 04 章那个线性变换(乘权重 w、加偏置 b),再过一个固定的非线性函数 f——激活函数(activation function)。「激活」这个词是神经科学的化石,今天它只是一个固定的数学弯折,没有任何「激活」的意思。书里给的三个具体数,看一眼就全懂了2:

输入 x 线性部分 2x+6 过 tanh 后
18 42 ≈ 1 (tanh 的上限)
-2.79 0.42 ≈ 0.397 (落在敏感区)
-10 -14 ≈ -1 (tanh 的下限)

图说:tanh 把任意大的输入压进 (−1, 1)。x=18 和 x=1000 出去几乎一样;
只有中间一小段(大约 −2 到 2)输入的变化才会引起输出的变化。

一个神经元管一个数;一排神经元(每个有自己的 w、b)共享同一个输入,合起来叫一层(layer);一层的输出喂给下一层,叠起来的塔就是神经网络。输入和输出层之间那些被夹在中间的层叫隐藏层——名字只是因为它不直接面对输入输出,没有更深的含义3

2. 为什么非叠非线性不可

这是本章的「一句话定理」4:

线性变换叠线性变换,结果还是一个线性变换。 w₂·(w₁·x+b₁)+b₂ 展开合并,等于另一个 w′·x+b′。叠一百层,还是条直线。

所以没有激活函数的多层网络毫无意义。而只要每两层之间夹一个非线性弯折,性质就变了:每个神经元负责「在某个输入区间里拱起一个包」,权重管包的位置、高度、宽窄;把许多个包加起来,就能拼出几乎任意形状的连续曲线——这就是「神经网络是通用逼近器」的直觉版(严格版叫通用逼近定理:够宽的此类网络能逼近任意连续函数)5

书里用四个 tanh 神经元做了演示:A、B、C、D 四条 S 形曲线,位置陡峭各不相同;A+B 相加得到一个「中间一正一负两个包」的新形状;再把「A+B」喂给 C、D(这就是第二层)再相加,出来一条带明显双峰双谷的曲线——四个神经元、两层,已经有了相当的表现力5

激活函数怎么选:看敏感区

主流激活函数的样子:tanh 和 sigmoid 是光滑 S 形(压到 −11 或 01);ReLU(rectified linear unit,整流线性单元——「单元」就是网络里一个最小的计算零件)最简单——负数归零、正数原样通过(x<0 得 0,x≥0 得 x),目前在多数任务里是默认选择6

挑激活函数看的是两件事7:

  • 敏感区:输入落在这里时,输出的变化明显——梯度才不为零,误差才传得回来,参数才学得到东西;
  • 饱和区:输入太大太小,输出被压平——这一侧的梯度≈0,落在这里的神经元等于「冻住了」,误差从它身上流过却几乎不改变它。

所以激活函数给训练提供了一个重要性质:误差主要更新「当前正处在敏感区」的神经元,其余的暂时不动——每个样本只教育网络的一部分,这就是几百万个参数能各就各位的原因7

换一个视角:误差面不再是碗

第 04 章的线性模型,损失面是凸的(一只碗),有唯一谷底。神经网络的损失面不是凸的——崇山峻岭、处处是洼地,没有「唯一正确答案」8。这带来一个反常识的事实:

神经网络的单个参数没有含义。 同一批数据训练两次,两组参数可能完全不同、效果一样好;参数只在「集体行动」里才有意义8

非凸是代价也是能力的来源:凸性保证了「能解」,但线性只能逼近线性;放弃凸性,换来逼近真实世界非线性关系的能力8

3. nn:把「换模型」变成三行的事

理论讲完,看 PyTorch 怎么装这些零件。核心抽象是 nn.Module:一个 Python 类,约定俗成地装着若干参数(nn.Parameter,就是默认打开 requires_grad 的张量)和若干子模块,并知道怎么把输入算成输出9

主走查:还是那支温度计,但模型换成真网络

把第 05 章的训练循环原样保留,只换三样东西10:

seq_model = nn.Sequential( # Sequential:把模块像串糖葫芦一样串起来
nn.Linear(1, 13), # 第一层:1 个数 → 13 个数(13 个神经元)
nn.Tanh(), # 激活:逐个过 tanh
nn.Linear(13, 1)) # 第二层:13 个数合成 1 个输出

nn.Linear(1, 13) 就是「一层 13 个神经元的线性变换」:内部是一个 13×1 的权重矩阵加一个长度 13 的偏置向量,参数共 13+13 个;第二层 13→1 再带 13+1 个——全网络 28 个参数10。13 这个数是作者随手选的,只为了和别的形状区分开10

训练代码对比第 05 章,改动只有三处11:

旧: optimizer = optim.SGD([params], lr=...) # 手工参数
新: optimizer = optim.SGD(seq_model.parameters(), lr=...) # 问模块要参数

旧: t_p = model(t_u, *params) # 手工传参
新: t_p = seq_model(t_u_batch) # 模型自己装着参数

旧: def loss_fn(t_p, t_c): ... # 手写均方
新: loss_fn = nn.MSELoss() # 现成的

model.parameters() 这个方法值得钉住:它递归(一层层往下钻、直到最底层)地收集本模块和所有子模块的全部参数,交给优化器。哪怕将来网络嵌套十层,要参数永远是这一句12

结果:5000 轮后,验证集上输出 [−1.99, 20.87] 对真实答案 [−4, 21],验证损失 2.02,比之前的手工线性模型还好一点13。画图看,网络的拟合线在数据点之间轻微地扭——这个只有 28 个参数的玩具,已经在追逐噪声了:过拟合不需要大模型,只需要「容量比数据多太多」13。这直观演示了第 05 章那句话:参数要按「装得下训练集」到「不再过拟合」之间来找尺寸。

两个工程细节

批量维。 nn 模块一律要求输入的第 0 维是批量:输入形状是 B×特征数。所以 11 个读数要 unsqueeze(1) 变成 11×1。一批样本一起过模型,既是为了一次摊薄开销(GPU 几千个核,一个样本喂不饱),也是某些层(如第 09 章的 batch norm)要用整批的统计量14

别直接调 .forward() 模块被当函数一样用括号调起来时,走的其实是 __call__,它会在 forward 前后做一些家务(比如 hook——一种挂在前后向过程中、到时机自动被调用的函数,术语叫回调;多卡训练等机制依赖它)。自己直接写 model.forward(x) 会绕过这些,属于「能跑但错得无声」15

4. 作者的判断与证据

说法性质
「线性叠线性仍是线性」数学事实4
通用逼近(若干「包」相加拼任意曲线)直觉演示 + 引用了 Nielsen 的在线书第 4 章做严格版;定理本身有严格证明5
「参数没有个体可解释性」作者陈述 + 明显的事实支撑(换种子参数全变);属于领域共识8
「ReLU 是目前最好的通用激活」书写时的经验共识;Sigmoid 已退出主流——书里同样照实写6
「网络没有让结果更好(这题本来就是线性的)」作者主动坦白:换网络在这个问题上是「没用的倒退」,纯为教学13

5. 边界与局限

  • 全章只有一个输入一个输出;多输入多输出、图像输入,第 07、08 章才接上。
  • 「激活函数几乎无硬性要求」——书里明确说这些规律「在特定情况下都可能被轻易推翻」;别把它当教条6
  • 通用逼近定理说「存在一个网络能逼近」,没说「梯度下降能找到它」——这是两件不同的事,后者没有同等强度的保证。
  • 28 参数的玩具已经表现出过拟合倾向;「网络越大越好」的直觉在第 09 章会被系统地拆掉。

6. 可带走的

  1. 神经元 = f(w·x+b):一个线性变换加一个固定的非线性弯折;和大脑无关。
  2. 没有激活函数,叠层无意义;有了它,几个单元两层就能拼出双峰曲线。
  3. 看激活函数看敏感区/饱和区:落进饱和区的神经元梯度≈0,学不到东西。
  4. 非凸 = 没有唯一正确参数;参数只在集体里有意义,别试图解读单个权重。
  5. nn.Module 装着参数和子模块;model.parameters() 一句收全;nn.Sequential 串糖葫芦。
  6. 输入第 0 维永远是批量;单个样本也要 unsqueeze 出批量维。
  7. 换模型不动训练循环:模型、损失、优化器三件套各自可换——这个结构一路沿用到全书最后,第 16 章那个多卡训练循环里还是它。
  8. 调用走 model(x),别直接调 forward——会绕过 hook,错得无声。

7. 原文地图

主题原书章原文位置
神经元定义与生物学距离ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:54(搜「Artificial neurons」) · :58(搜「Rosenblatt」)
tanh 三个数ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:86(搜「2 18 + 6 = 42」)
多层堆叠公式ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:105(搜「x_1 = f(w_1 * x + b_1)」)
隐藏层名字来历ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:725(搜「hidden layer」)
非凸与参数无个体含义ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:137(搜「convex error curve」) · :148(搜「individual interpretability」)
激活两个角色ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:164(搜「slopes at different values」)
通用逼近注记ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:175(搜「Universal Approximation Theorem」)
tanh 狗/熊/垃圾车ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:208(搜「math.tanh(-2.2)」)
激活函数全家福与 ReLUch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:250(搜「ReLU」)
敏感区/饱和区与误差传播ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:43(搜「Differentiable」) · :313(搜「saturated」)
四神经元拼曲线ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:346(搜「TANH(-2 * X -1.25)」) · :386(搜「two-layer network」)
nn.Module/Linear/Sequentialch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:433(搜「torch.nn」) · :755(搜「nn.Sequential」)
参数形状与 named_parametersch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:761(搜「13, 1」)
批量维ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:540(搜「B × N_in」)
别直接调 forwardch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:477(搜「Silent error」)
温度计结果与过拟合ch6text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:885(搜「output」) · :925(搜「it tries to chase the measurements」)

Footnotes

  1. 出处:「6 Using a neural network to fit the data」第 54 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:54,搜「Artificial neurons」)与第 58 段(:58,搜「Rosenblatt」)。原文:Rosenblatt 1958 的感知机受神经科学启发,但现代人工神经网络与大脑机制「只有一丝相似」;两类网络大概用了「大体相似的数学策略来逼近复杂函数,因为这一类策略就是好使」。

  2. 出处:「6 Using a neural network to fit the data」第 86 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:86,搜「2 18 + 6 = 42」)。图 6.2 的三行数:x=18 → 2·18+6=42 → tanh(42)≈1;x=−2.79 → 0.42 → 0.3969;x=−10 → −14 → ≈−1。

  3. 出处:「6 Using a neural network to fit the data」第 105 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:105,搜「x_1 = f(w_1 * x + b_1)」)与第 725 段(:725,搜「hidden layer」)。多层公式 x₁=f(w₁x+b₁), x₂=f(w₂x₁+b₂) …;「hidden」只是因为中间层的输出不被直接观察。

  4. 出处:「6 Using a neural network to fit the data」第 287 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:287,搜「affine linear」)。原文:「重复施加 (w·x+b) 而不加激活函数,得到的仍是同样(仿射线性)形式的函数」;非线性让整体能逼近更复杂的函数。 2

  5. 出处:「6 Using a neural network to fit the data」第 175 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:175,搜「Universal Approximation Theorem」)与第 386 段(:386,搜「two-layer network」)。注记:非线性激活经缩放平移变成小「包」,摆放并叠加这些包即可逼近区间上任意连续函数;四神经元两层的演示见同章图 6.6。 2 3

  6. 出处:「6 Using a neural network to fit the data」第 250 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:250,搜「ReLU」)。原文:「ReLU 目前被认为是最好的通用激活函数之一,许多 state-of-the-art 结果都用它」;Sigmoid「在早期深度学习工作中广泛使用,此后已不再常用」。 2 3

  7. 出处:「6 Using a neural network to fit the data」第 43 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:43,搜「Differentiable」)与第 313 段(:313,搜「saturated」)。激活函数需非线性、可微(个别尖点可容忍);输入在敏感区时误差能有效回传,饱和区梯度近零、几乎不更新。 2

  8. 出处:「6 Using a neural network to fit the data」第 137 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:137,搜「convex error curve」)与第 148 段(:148,搜「individual interpretability」)。原文:神经网络即使配平方损失也没有凸误差面;「参数本身没有个体可解释性——同一份数据训出的两个网络,内部参数可以不同而性能相近」。 2 3 4

  9. 出处:「6 Using a neural network to fit the data」第 433 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:433,搜「torch.nn」)。nn.Module 可含若干 nn.Parameter 属性(训练中要被优化的张量)和子模块;子模块必须挂在顶层属性,塞进 list/dict 里优化器会找不到(那要用 nn.ModuleList/ModuleDict)。

  10. 出处:「6 Using a neural network to fit the data」第 755 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:755,搜「nn.Sequential」)与第 787 段(:787,搜「13, 1」)。Sequential(Linear(1,13), Tanh, Linear(13,1));参数形状 [13,1]、[13]、[1,13]、[1];「13 是随手选的」。 2 3

  11. 出处:「6 Using a neural network to fit the data」第 613 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:613,搜「parameters()」)与第 670 段(:670,搜「nn.MSELoss」)。model.parameters() 取代手工参数列表;nn.MSELoss() 取代手写损失;其余训练循环不变。

  12. 出处:「6 Using a neural network to fit the data」第 635 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:635,搜「recurses into submodules」)。parameters() 递归子模块、返回扁平列表,直接交给优化器构造。

  13. 出处:「6 Using a neural network to fit the data」第 885 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:885,搜「output」)与第 925 段(:925,搜「it tries to chase the measurements」)。5000 轮后验证输出 [−1.99, 20.87] 对 [−4, 21];「连我们这个小网络都有太多参数,它在追噪声」。 2 3

  14. 出处:「6 Using a neural network to fit the data」第 540 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:540,搜「B × N_in」)与第 580 段(:580,搜「big enough to saturate the computing」)。nn 模块期望第 0 维是批量;批量喂 GPU 才能喂饱并行单元,批统计也更稳。

  15. 出处:「6 Using a neural network to fit the data」第 477 段(text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:477,搜「Silent error」)。原文把直接调 forward 标为「Silent error. Don't do it!」:hook 不会被执行,可能引发错误。