神经网络登场:激活函数、层与 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. 可带走的
- 神经元 = f(w·x+b):一个线性变换加一个固定的非线性弯折;和大脑无关。
- 没有激活函数,叠层无意义;有了它,几个单元两层就 能拼出双峰曲线。
- 看激活函数看敏感区/饱和区:落进饱和区的神经元梯度≈0,学不到东西。
- 非凸 = 没有唯一正确参数;参数只在集体里有意义,别试图解读单个权重。
- nn.Module 装着参数和子模块;
model.parameters()一句收全;nn.Sequential串糖葫芦。 - 输入第 0 维永远是批量;单个样本也要
unsqueeze出批量维。 - 换模型不动训练循环:模型、损失、优化器三件套各自可换——这个结构一路沿用到全书最后,第 16 章那个多卡训练循环里还是它。
- 调用走
model(x),别直接调forward——会绕过 hook,错得无声。
7. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 神经元定义与生物学距离 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:54(搜「Artificial neurons」) · :58(搜「Rosenblatt」) |
| tanh 三个数 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:86(搜「2 18 + 6 = 42」) |
| 多层堆叠公式 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:105(搜「x_1 = f(w_1 * x + b_1)」) |
| 隐藏层名字来历 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:725(搜「hidden layer」) |
| 非凸与 参数无个体含义 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:137(搜「convex error curve」) · :148(搜「individual interpretability」) |
| 激活两个角色 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:164(搜「slopes at different values」) |
| 通用逼近注记 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:175(搜「Universal Approximation Theorem」) |
| tanh 狗/熊/垃圾车 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:208(搜「math.tanh(-2.2)」) |
| 激活函数全家福与 ReLU | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:250(搜「ReLU」) |
| 敏感区/饱和区与误差传播 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:43(搜「Differentiable」) · :313(搜「saturated」) |
| 四神经元拼曲线 | ch6 | text/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/Sequential | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:433(搜「torch.nn」) · :755(搜「nn.Sequential」) |
| 参数形状与 named_parameters | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:761(搜「13, 1」) |
| 批量维 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:540(搜「B × N_in」) |
| 别直接调 forward | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:477(搜「Silent error」) |
| 温度计结果与过拟合 | ch6 | text/14-ch06-6-using-a-neural-network-to-fit-the-data.txt:885(搜「output」) · :925(搜「it tries to chase the measurements」) |