动手学深度学习:多层感知机(MLP)与激活函数从入门到原理

原创2026-09-30 23:48:41561 阅读
文章标签:人工智能深度学习机器学习教程

动手学深度学习:多层感知机(MLP)与激活函数从入门到原理

导读

多层感知机(Multilayer Perceptron,MLP)是《动手学深度学习》(d2l-zh)从线性模型迈向深度神经网络的关键一步:通过在输入与输出之间插入一个或多个全连接隐藏层,并用非线性激活函数驱动,MLP 得以表达线性模型无法刻画的复杂函数关系。本篇以仓库 chapter_multilayer-perceptrons/mlp_origin.md(及其中文版 chapter_multilayer-perceptrons/mlp.md)为主体,完整讲解隐藏层的动机、MLP 的数学形式、三大经典激活函数(ReLU、sigmoid、tanh)的几何与导数性质,并给出 MXNet、PyTorch、TensorFlow 三种框架的可运行代码,以及仓库源码中的实现佐证。读完你将理解"为什么线性模型不够用""为什么纯仿射的隐藏层没有价值""激活函数如何赋予网络表达能力",并能独立绘制与验证常用激活函数及其梯度。

单隐藏层多层感知机示意:4 个输入、5 个隐藏单元、3 个输出,隐藏层与输出层均为全连接(来源:img/mlp.svg)

隐藏层:从线性模型到深度网络的桥梁

线性模型为什么会失效

上一章(线性网络)介绍的 softmax 回归,本质上是通过单个仿射变换(线性变换加偏置)把输入直接映射到输出,再接 softmax 操作。如果标签与输入数据确实满足仿射关系,这种方法就足够了;但仿射变换中的"线性"是一个很强的假设,它至少隐含了两个容易被忽略的约束:

  1. 单调性:任何特征的增大,要么总是导致输出增大(对应权重为正),要么总是导致输出减小(权重为负)。这在某些场景下合理——例如预测一个人是否会偿还贷款时,可以认为收入越高的申请人越可能还款。但这种关系只是单调的,并不线性:收入从 0 增加到 5 万,还款可能性提升幅度通常远大于收入从 100 万增加到 105 万。一种补救办法是预处理数据(如改用收入的对数作为特征)使线性假设更接近成立。
  2. 违反单调性的反例普遍存在:例如根据体温预测死亡风险,体温高于 37°C 时温度越高风险越大,而低于 37°C 时温度越高风险反而越低。这类问题也能通过巧妙预处理(用"与 37°C 的距离"作为特征)来化解。

但猫狗图像分类这类任务暴露了线性模型的根本困境:增加位置 (13, 17) 处像素的强度,是否总是提高"图像是狗"的概率?对线性模型的依赖,等于隐含假设"区分猫狗只需评估单个像素的亮度"——而图像反转后类别保持不变,足以证明这种假设注定失败。更关键的是,与体温例子不同,这里无法通过简单预处理解决:任何像素的重要性都复杂地依赖于其上下文(周围像素的值)。理论上可能存在某种数据表示,在其之上线性模型是合适的,但我们无法靠手工计算出这种表示。深度神经网络的价值正在于此:用观测数据联合学习隐藏层表示,以及作用于该表示的线性预测器。

在网络中加入隐藏层:MLP 的诞生

克服线性模型局限性的最直接方式,是在输入层与输出层之间插入一个或多个隐藏层(hidden layer):把多个全连接层逐层堆叠,每一层把输出馈送给上一层,直到生成最终输出。可以把前 L−1L-1 层看作"表示学习"部分,把最后一层看作"线性预测器"。这种架构就是多层感知机(MLP)。

以 img/mlp.svg 所示的网络为例:4 个输入、3 个输出,隐藏层含 5 个隐藏单元。由于输入层不参与任何计算,产生输出只需实现隐藏层与输出层的计算,因此该 MLP 的层数为 2。注意这两层都是全连接的:每个输入影响隐藏层每个神经元,每个隐藏神经元又影响输出层每个神经元。

需要提醒的是,全连接层的参数开销可能高得惊人。正如中文版 mlp.md 引用的观点,即便不改变输入输出规模,也需要在参数节约与模型有效性之间权衡。这正是后续章节介绍卷积、参数共享等结构的重要动机。

从线性到非线性:数学形式与关键证明

沿用本书记号:设小批量 X∈Rn×d\mathbf{X} \in \mathbb{R}^{n \times d}(nn 个样本、每个样本 dd 个特征),隐藏层有 hh 个隐藏单元,其输出记为 H∈Rn×h\mathbf{H} \in \mathbb{R}^{n \times h},即隐藏表示/隐藏层变量/隐藏变量。隐藏层与输出层均为全连接,于是有隐藏层权重 W(1)∈Rd×h\mathbf{W}^{(1)} \in \mathbb{R}^{d \times h}、偏置 b(1)∈R1×h\mathbf{b}^{(1)} \in \mathbb{R}^{1 \times h},以及输出层权重 W(2)∈Rh×q\mathbf{W}^{(2)} \in \mathbb{R}^{h \times q}、偏置 b(2)∈R1×q\mathbf{b}^{(2)} \in \mathbb{R}^{1 \times q}。单隐藏层 MLP 的数学定义为:

H=XW(1)+b(1),O=HW(2)+b(2).\begin{aligned} \mathbf{H} & = \mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}, \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}. \end{aligned}

初看之下,加入隐藏层让我们多跟踪了一组参数,似乎"有所得"。但在上述定义下我们什么也没得到!原因很直接:隐藏单元是输入的仿射函数,输出(softmax 前)又是隐藏单元的仿射函数,而仿射函数的仿射函数仍然是仿射函数——线性模型本就能表示任何仿射函数。形式化地,对任意权重取值,我们可以把隐藏层"折叠"掉,得到等价的单层模型 W=W(1)W(2)\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}、b=b(1)W(2)+b(2)\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}:

O=(XW(1)+b(1))W(2)+b(2)=XW(1)W(2)+b(1)W(2)+b(2)=XW+b.\mathbf{O} = (\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})\mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W}^{(1)}\mathbf{W}^{(2)} + \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)} = \mathbf{X} \mathbf{W} + \mathbf{b}.

要真正释放多层架构的潜力,还差一个关键要素:在仿射变换之后,对每个隐藏单元施加非线性的激活函数 σ\sigma(其输出称为活性值 activations)。有了激活函数,MLP 便无法再退化回线性模型:

H=σ(XW(1)+b(1)),O=HW(2)+b(2).\begin{aligned} \mathbf{H} & = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}), \\ \mathbf{O} & = \mathbf{H}\mathbf{W}^{(2)} + \mathbf{b}^{(2)}. \end{aligned}

由于 X\mathbf{X} 的每一行对应一个小批量中的样本,非线性 σ\sigma 按行(rowwise)作用于输入;而本节讨论的常见激活函数通常是**按元素(elementwise)**操作的——计算每个活性值时无需参考其他隐藏单元的取值。若要构建更通用的 MLP,可以继续堆叠隐藏层,例如 H(1)=σ1(XW(1)+b(1))\mathbf{H}^{(1)} = \sigma_1(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)})、H(2)=σ2(H(1)W(2)+b(2))\mathbf{H}^{(2)} = \sigma_2(\mathbf{H}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}),一层叠一层,获得表达能力更强的模型。

通用近似定理:MLP 能做什么

MLP 通过隐藏神经元捕捉输入之间复杂的相互作用。只要给定足够多的神经元(数量可能大得离谱)与正确的权重,即便只有单隐藏层,MLP 也能对任意函数建模——这就是通用近似定理的核心结论。一个贴切的比喻是 C 语言:它能表达任何可计算程序,但"写出一段符合你要求的程序"才是真正的难点,这与"学习出正确权重"的困难如出一辙。

同时要注意,单隐藏层"理论上"能逼近任何函数,绝不意味着应该用单隐藏层网络解决所有问题。事实上,更深(而非更宽)的网络往往能以更紧凑的方式逼近许多函数,这也是后续章节反复强调深度价值的原因。

激活函数:赋予网络非线性的引擎

激活函数通过计算加权和并加上偏置,决定神经元是否被"激活",本质上是把输入信号变换为输出的可微算子,其中大多数引入了非线性。由于激活函数是深度学习的基础设施,下面依次剖析三种最经典的激活函数。

ReLU 函数

修正线性单元(Rectified Linear Unit,ReLU) 是目前最流行的选择,它实现简单,且在各类预测任务上表现良好。其定义极为简洁:给定元素 xx,取它与 0 的最大值:

ReLU⁡(x)=max⁡(x,0).\operatorname{ReLU}(x) = \max(x, 0).

通俗地讲,ReLU 只保留正元素,把负元素全部置 0。绘制曲线可以看到它是分段线性的。从导数看:输入为负时导数为 0,输入为正时导数为 1;输入恰好等于 0 时不可导,实践中默认取左侧导数(视为 0)——因为输入几乎不可能精确为 0,正如书中所引的谚语:"如果微妙的边界条件很重要,我们很可能是在研究数学而非工程。"

ReLU 备受青睐的根本原因在于求导行为极其良好:要么让梯度消失(负区间),要么让梯度原样通过(正区间)。这使优化过程更稳定,并缓解了困扰早期神经网络的梯度消失问题(后文会详述)。

ReLU 还有众多变体,例如参数化 ReLU(pReLU)(出自 He et al., 2015),它在负区间引入一个线性项 αmin⁡(0,x)\alpha \min(0, x),让部分信息即使在输入为负时也能通过:

pReLU⁡(x)=max⁡(0,x)+αmin⁡(0,x).\operatorname{pReLU}(x) = \max(0, x) + \alpha \min(0, x).

三种框架下绘制 ReLU 及其梯度的代码如下(输入 xx 从 -8 到 8、步长 0.1):

#@tab mxnet
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
    y = npx.relu(x)
d2l.plot(x, y, 'x', 'relu(x)', figsize=(5, 2.5))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
#@tab pytorch
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
#@tab tensorflow
x = tf.Variable(tf.range(-8.0, 8.0, 0.1), dtype=tf.float32)
y = tf.nn.relu(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'relu(x)', figsize=(5, 2.5))
with tf.GradientTape() as t:
    y = tf.nn.relu(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of relu', figsize=(5, 2.5))

sigmoid 函数

sigmoid 函数把定义域 R\mathbb{R} 上的输入压缩到区间 (0, 1),因此常被称为挤压函数(squashing function):

sigmoid⁡(x)=11+exp⁡(−x).\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.

其历史与生物神经元的建模紧密相关:人工神经元的先驱(可追溯至发明者麦卡洛克和皮茨)专注于阈值单元——输入低于阈值输出 0、超过阈值输出 1。当研究重心转向基于梯度的学习后,sigmoid 凭借"平滑、可微的阈值单元近似"成为自然选择。今天 sigmoid 仍广泛用于输出层:当希望把输出解读为二分类问题的概率时(sigmoid 可视作 softmax 的特例)。但在隐藏层中它基本已被更简单、更易训练的 ReLU 取代;后续循环神经网络章节中,sigmoid 将以"信息门控"的形式回归(控制跨时间的信息流)。

sigmoid 有一个值得注意的几何性质:当输入接近 0 时,它近似于线性变换。其导数形式优雅:

ddxsigmoid⁡(x)=sigmoid⁡(x)(1−sigmoid⁡(x)).\frac{d}{dx} \operatorname{sigmoid}(x) = \operatorname{sigmoid}(x)\left(1-\operatorname{sigmoid}(x)\right).

导数在输入为 0 处达到最大值 0.25;输入向任一方向远离 0 时,导数都趋近于 0——这正是 sigmoid 在深层网络中容易引发梯度消失的数学根源。绘制代码如下:

#@tab mxnet
with autograd.record():
    y = npx.sigmoid(x)
d2l.plot(x, y, 'x', 'sigmoid(x)', figsize=(5, 2.5))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
#@tab pytorch
y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
x.grad.data.zero_()
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))
#@tab tensorflow
y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'sigmoid(x)', figsize=(5, 2.5))
with tf.GradientTape() as t:
    y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of sigmoid', figsize=(5, 2.5))

tanh 函数

tanh(双曲正切)函数与 sigmoid 类似,同样压缩输入,但输出区间为 (-1, 1):

tanh⁡(x)=1−exp⁡(−2x)1+exp⁡(−2x).\operatorname{tanh}(x) = \frac{1 - \exp(-2x)}{1 + \exp(-2x)}.

当输入接近 0 时,tanh 同样近似线性变换;其形状虽与 sigmoid 相似,但关于坐标系原点中心对称(即 tanh(0)=0,输出以 0 为中心,这是其在实践中常优于 sigmoid 的一个原因)。其导数为:

ddxtanh⁡(x)=1−tanh⁡2(x).\frac{d}{dx} \operatorname{tanh}(x) = 1 - \operatorname{tanh}^2(x).

当输入接近 0 时,tanh 导数接近最大值 1(比 sigmoid 的 0.25 更大,因此梯度传导效率更高);输入远离 0 时导数同样趋近于 0。绘制代码如下:

#@tab mxnet
with autograd.record():
    y = np.tanh(x)
d2l.plot(x, y, 'x', 'tanh(x)', figsize=(5, 2.5))
y.backward()
d2l.plot(x, x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
#@tab pytorch
y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))
x.grad.data.zero_()
y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))
#@tab tensorflow
y = tf.nn.tanh(x)
d2l.plot(x.numpy(), y.numpy(), 'x', 'tanh(x)', figsize=(5, 2.5))
with tf.GradientTape() as t:
    y = tf.nn.tanh(x)
d2l.plot(x.numpy(), t.gradient(y, x).numpy(), 'x', 'grad of tanh', figsize=(5, 2.5))

三种激活函数速览

激活函数 公式 输出区间 0 点处导数 典型用途 主要局限
ReLU max⁡(x,0)\max(x, 0) [0,+∞)[0, +\infty) 0(默认左侧导数) 隐藏层默认选择 负区间梯度为 0,神经元可能"死亡"
sigmoid 11+exp⁡(−x)\frac{1}{1+\exp(-x)} (0, 1) 0.25 二分类输出概率、RNN 门控 饱和区导数趋 0,易梯度消失
tanh 1−exp⁡(−2x)1+exp⁡(−2x)\frac{1-\exp(-2x)}{1+\exp(-2x)} (-1, 1) 1 隐藏层(输出以 0 为中心) 饱和区导数趋 0

把三种激活函数的代码放在一起对比即可直观感受:ReLU 是"分段线性 + 非饱和",梯度要么 0 要么 1,优化最友好;sigmoid 与 tanh 都是"平滑挤压 + 两侧饱和",在深层网络中容易让梯度在反向传播时逐层衰减。

仓库源码中的实战佐证

本仓库并非只有理论,d2l 目录下的工具包(d2l/mxnet.py、d2l/torch.py、d2l/tensorflow.py 等)为上述代码提供了 plot 绘图函数,其签名支持 X, Y, xlabel, ylabel, xlim, ylim, xscale, yscale, legend, fmts, figsize 等参数(默认线型 ('-', 'm--', 'g-.', 'r:')、默认图幅 (3.5, 2.5)),激活函数曲线正是经由它绘制的。此外,在下一节 mlp-scratch.md 中,你会看到从零实现的 relu 函数——它刻意不直接调用框架内置 relu,而是用 np.maximum 等底层算子手写:

# 从零实现(见 chapter_multilayer-perceptrons/mlp-scratch.md)
def relu(X):
    return np.maximum(X, 0)   # MXNet 版本
    # return torch.max(X, 0)[0]  # PyTorch 版本(X@W1 + b1 后调用)

随后在 MLP 前向传播中按 H = relu(X@W1 + b1) 的方式接入隐藏层,与本节公式 H=σ(XW(1)+b(1))\mathbf{H} = \sigma(\mathbf{X} \mathbf{W}^{(1)} + \mathbf{b}^{(1)}) 一一对应。仓库中该文件的四个框架分支均体现了这一实现(如 MXNet 的 relu(np.dot(X, W1) + b1)、PyTorch 的 relu(X@W1 + b1))。

小结

  • MLP 在输出层与输入层之间增加一个或多个全连接隐藏层,并通过激活函数转换隐藏层的输出,从而表达任意复杂的函数关系。
  • 不加激活函数的"纯仿射堆叠"等价于单个线性模型,激活函数(非线性)是 MLP 表达能力的来源。
  • 常用激活函数包括 ReLU 函数、sigmoid 函数和 tanh 函数,各有明确的数学形式、输出区间与导数行为,应结合场景选择:隐藏层首选 ReLU 及其变体,输出层(二分类)常用 sigmoid,需要零中心输出时可考虑 tanh。

练习

  1. 计算 pReLU 激活函数的导数。
  2. 证明仅使用 ReLU(或 pReLU)的 MLP 构造的是一个连续的分段线性函数。
  3. 证明 tanh⁡(x)+1=2sigmoid⁡(2x)\operatorname{tanh}(x) + 1 = 2 \operatorname{sigmoid}(2x)。
  4. 假设有一个非线性单元,它一次作用于一个小批量的数据,而不按元素独立作用。这可能引发哪些问题?(提示:回想本节强调的"按元素操作"性质及其对计算独立性的意义。)

以上推导与代码均可在仓库 chapter_multilayer-perceptrons/mlp_origin.md 中找到原始出处;继续阅读 mlp-scratch.md 与 mlp-concise.md 可分别学习 MLP 的从零实现与高级 API 实现。

登录后查看全文
d2l-zh