☰
动手学深度学习:多层感知机(MLP)与激活函数从入门到原理
2026/10/1 7:59:05 网站建设 项目流程
  • 人工智能
  • 深度学习
  • 机器学习
  • 教程

【免费下载链接】d2l-zh

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

导读

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

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

线性模型为什么会失效

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

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

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

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

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

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

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

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

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

$$ \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 前)又是隐藏单元的仿射函数,而仿射函数的仿射函数仍然是仿射函数——线性模型本就能表示任何仿射函数。形式化地,对任意权重取值,我们可以把隐藏层"折叠"掉,得到等价的单层模型 $\mathbf{W} = \mathbf{W}^{(1)}\mathbf{W}^{(2)}$、$\mathbf{b} = \mathbf{b}^{(1)} \mathbf{W}^{(2)} + \mathbf{b}^{(2)}$:

$$ \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 便无法再退化回线性模型:

$$ \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} $$

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

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

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

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

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

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

ReLU 函数

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

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

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

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

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

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

三种框架下绘制 ReLU 及其梯度的代码如下(输入 $x$ 从 -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 函数把定义域 $\mathbb{R}$ 上的输入压缩到区间 (0, 1),因此常被称为挤压函数(squashing function):

$$\operatorname{sigmoid}(x) = \frac{1}{1 + \exp(-x)}.$$

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

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

$$\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):

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

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

$$\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)$$[0, +\infty)$0(默认左侧导数)隐藏层默认选择负区间梯度为 0,神经元可能"死亡"
sigmoid$\frac{1}{1+\exp(-x)}$(0, 1)0.25二分类输出概率、RNN 门控饱和区导数趋 0,易梯度消失
tanh$\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)的方式接入隐藏层,与本节公式 $\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. 证明 $\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

《动手学深度学习》:面向中文读者、能运行、可讨论。中英文版被70多个国家的500多所大学用于教学。

项目地址:https://gitcode.com/GitHub_Trending/d2/d2l-zh
点击查看免费下载

相关推荐

上一篇:pkgr vs Omnibus vs Capistrano:Linux应用打包与部署方案终极对比(附选型清单)
下一篇:Volatility3多语言支持:Python扩展与C模块集成方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询