ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

人工智能基础:激活函数原理、梯度消失与选型指南

人工智能基础:激活函数原理、梯度消失与选型指南 1. 删掉激活函数之后那个让我印象深刻的实验刚入门那会儿我盯着代码里的relu(x)看了很久心想不就是把负数抹成 0 吗这点小动作能翻出什么浪后来我把自己写的一个两层全连接网络里的两个 ReLU 全删了重新跑了一遍测试准确率从 96% 左右掉到 51%基本等于抛硬币。更离谱的是训练损失几乎不下降曲线平得像一条水平线。那一刻我才真正意识到激活函数不是网络里的可选装饰它是整个模型的关节——关节锁死了肌肉再发达也动不了。这篇内容围绕人工智能基础中的一块基石展开激活函数的概念。它不是那种看一眼就懂的语法糖而是牵动前向表达能力和反向梯度传播的关键部件。如果你正在啃人工智能入门的教材、准备人工智能大作业、或者刚开始看人工智能导论里关于神经网络的那一章那这篇东西基本就是给你写的。有基础的人可以重点关注第 4、5 节的实测和选型清单纯新手从第 1 节顺序往后读也不会掉队。我打算这么讲先把没有激活函数会怎样这件事用数学和一个具体实验讲透再钻进单个神经元内部看每一步计算然后把 Sigmoid、Tanh、ReLU 这一大家子逐个拆开对比接着写几十行 numpy 代码把梯度消失从抽象概念变成屏幕上能看到的数字最后给出一份分层、分任务的选型清单和几个新手最容易栽的坑。不会只给结论每个选择背后为什么这么定我都会说清楚。1.1 两层线性网络展开之后只剩一层假设第一层的权重和偏置是 $W_1$、$b_1$第二层是 $W_2$、$b_2$。中间如果什么非线性都不加那么h W1 · x b1 y W2 · h b2 W2 · (W1 · x b1) b2 (W2 · W1) · x (W2 · b1 b2) W · x b注意最后一步$W_2W_1$ 乘出来还是一个矩阵记作 $W$括号里那一坨也还是个向量记作 $b$。也就是说两层线性层叠在一起数学上完全等价于一层线性层。堆三层、十层、一百层结论一样。这里有个生活化的类比你拿两把直尺首尾相接去画曲线接得再长也画不出弧度因为每一段都是直的。想要画出弯的中间必须有个能掰弯的东西激活函数干的就是这个活。1.2 异或问题单层感知机的天花板如果说上面的推导还停留在代数层面那异或XOR问题就是最直观的证据。四个样本x1x2输出000011101110把 (0,0) 和 (1,1) 归为 0 类(0,1) 和 (1,0) 归为 1 类这是一组对角线分布的数据。你在平面上画多少条直线都没法把这两组点干净地分开——这就是单层感知机的死穴也是上世纪神经网络研究一度沉寂的原因之一。但只要加一个隐藏层并且给隐藏层配上非线性激活函数问题立刻解决。隐藏层先把输入空间折叠一下把四个点映射到一个新的坐标系里原本线性不可分的数据在新空间里就变得线性可分了。输出层那一步依然是线性的真正完成空间变换的是隐藏层加激活函数这个组合。1.3 激活函数真正提供的两样东西很多人只知道激活函数提供非线性这话对但不完整。它实际提供了两样东西。第一样是表达能力。通用近似定理告诉我们一个带非线性激活的隐藏层只要神经元足够多就能以任意精度逼近定义在紧集上的连续函数。这里的前提是非线性激活换成恒等映射定理立刻失效。第二样是梯度的传播特性。这一点经常被忽略但对训练成败的影响甚至比表达能力更直接。激活函数的导数决定了误差信号从输出层往输入层回传时是被放大、保持还是被层层压缩。Sigmoid 的导数最大值只有 0.25十层网络连乘下来就是 $0.25^{10} \approx 9.5\times10^{-7}$梯度基本蒸发了ReLU 正半轴导数恒为 1这条通路就不存在衰减问题。后面第 4 节我会用代码把这个差异跑出来给你看。2. 一个神经元内部到底在算什么理解了为什么需要接下来得把单个神经元的计算流程掰开看。因为激活函数的位置、它和前后步骤的配合关系直接决定了你后面调参时该动哪里。2.1 加权求和、加偏置、过激活三步走一个标准的人工神经元做三件事加权求和把上一层的每个输出 $x_i$ 乘上对应的权重 $w_i$累加起来加偏置加上一个常数 $b$作用是平移决策边界让激活函数不必非得在原点附近工作过激活把结果塞进激活函数 $f$输出给下一层。写成公式就是z Σ(w_i · x_i) b a f(z)这里 $z$ 通常叫预激活值pre-activation$a$ 叫激活值。很多框架的报错信息里会出现 logits指的就是没有过激活函数的原始输出本质和 $z$ 是一回事。和生物神经元做个对应树突负责接收信号相当于加权求和细胞体做信号整合相当于加偏置轴突把信号传出去但只有当累积电位超过某个阈值时才放电——这个阈值 非线性放电的行为就是激活函数在数学上的抽象。当然人工神经元的激活函数比生物神经元的放电机制简化了非常多别把两者完全等同。2.2 为什么激活函数必须几乎处处可导反向传播的核心是链式法则。假设损失 $L$ 对第 $l$ 层预激活值的梯度是 $\delta^{(l)}$那么它和第 $l1$ 层的关系大致是δ(l) (W(l1))ᵀ · δ(l1) ⊙ f(z(l))那个 $\odot$ 是逐元素相乘$f(z^{(l)})$ 就是激活函数在第 $l$ 层的导数。如果 $f$ 在某段区间导数不存在梯度就没法从那里穿过去参数也就更新不了。ReLU 恰好在 $z0$ 处不可导——左边导数是 0右边导数是 1导数不连续。这在数学上是个瑕疵但工程上完全不影响使用原因有两个一是输入恰好等于 0 的概率极低二是所有主流框架都直接约定 $z0$ 时导数为 0或 1不同实现有差异但几乎不影响结果。所以严格来说激活函数需要的是几乎处处可导而不是处处可导。提示如果你在做梯度检查gradient check时发现某个参数的数值梯度和解析梯度有微小差异先别急着怀疑代码写错了先看看是不是有神经元落在了 ReLU 的拐点上。把测试输入换成随机浮点数、避开 0差异通常就消失了。2.3 饱和区、零中心化、单调性这几个术语到底在说什么面试和教材里经常出现这几个词但很多人只是背下来了并不理解它们在讲什么。我用大白话解释一遍。饱和区指的是输入端绝对值很大时函数输出几乎不再变化、导数趋近于 0 的那一段。Sigmoid 在 $z$ 很大或很小时都会进入饱和区梯度被憋死在那里。这是梯度消失最主要的来源。零中心化说的是函数输出值的均值是否为 0。Sigmoid 输出恒为正落在 $(0,1)$ 区间这就导致下一层所有神经元的输入都是同一个符号。反向传播时同一个神经元的所有权重梯度会共享同一个符号更新方向只能一起往左或一起往右优化路径就会走出锯齿状的之字形收敛变慢。这是 Sigmoid 相比 Tanh 的主要劣势之一。单调性指函数是否单调递增。Sigmoid、Tanh、ReLU 都是单调的而 GELU、Swish 是非单调的在小负值区域会有一个浅浅的下凹。传统观点认为单调性有利于优化但实践证明非单调激活在很多任务上表现更好所以它并不是硬性要求。计算成本也得算进去。Sigmoid 和 Tanh 都要算指数函数exp在大规模网络上开销不小ReLU 只是一次比较和一次取最大快得多。这也是 ReLU 能从 2012 年之后迅速统治隐藏层的原因之一。把这几个维度记住后面看每种激活函数的优缺点时你就能自己对号入座而不是死记硬背。3. 逐个拆开从 Sigmoid 到 GELU 的家族谱系这一节我把常见的激活函数摆在一起对比。先上一张总表再逐个说细节。激活函数表达式导数值域最突出的问题Sigmoid$1/(1e^{-z})$$f(1-f)$(0, 1)饱和严重导数最大仅 0.25非零中心Tanh$(e^z-e^{-z})/(e^ze^{-z})$$1-f^2$(-1, 1)仍会饱和导数最大 1ReLU$\max(0, z)$1 或 0[0, ∞)死神经元非零中心输出无上界Leaky ReLU$\max(\alpha z, z)$1 或 $\alpha$(-∞, ∞)负半轴斜率 $\alpha$ 需要调ELU$z0$ 时为 $z$否则 $\alpha(e^z-1)$1 或 $f\alpha$(-α, ∞)含指数运算略慢GELU$z\cdot\Phi(z)$含正态分布项约 (-0.17, ∞)计算较贵实现有近似版本Swish/SiLU$z\cdot\sigma(\beta z)$复合求导约 (-0.28, ∞)多一个可调超参 $\beta$3.1 Sigmoid数学上最漂亮训练上最难受Sigmoid 把任意实数压缩到 $(0,1)$形状像一条平滑的 S 形曲线。它的导数有个非常优雅的性质f(z) f(z) · (1 - f(z))也就是说算出了前向输出导数顺手就能得到不用额外算指数。但它的两个问题都很致命。第一个是导数上限只有 0.25而且在 $z0$ 处才取到输入稍微偏离中心就迅速衰减。多层连乘梯度呈指数级消失。第二个是输出非零中心前面讲过会让优化路径变成锯齿形。它的主战场其实已经收缩到输出层了二分类任务里用它把 logits 映射成概率这没问题因为输出层不需要再往回传多少层。隐藏层里现在基本看不到它的身影了除非你在做门控结构比如 LSTM 里的门那里的 Sigmoid 是用来做开关的语义完全不同。3.2 Tanh零中心化带来的改善Tanh 可以看作 Sigmoid 的平移缩放版本tanh(z) 2·sigmoid(2z) - 1它的输出落在 $(-1,1)$均值接近 0这就解决了零中心化的问题。导数 $1 - f^2$ 的最大值也提升到了 1比 Sigmoid 好不少。但饱和的问题依然存在。输入 $z$ 超过 ±3 之后导数就掉到 0.01 以下了再深的网络照样会梯度消失。所以 Tanh 在隐藏层里的地位也逐渐被 ReLU 取代了。不过在 RNN 的隐藏状态更新、以及一些需要输出有正有负且范围受控的场景里Tanh 仍然是合适的选择。3.3 ReLU一个 $\max$ 解决了一大半问题ReLU 的定义简单到有点粗暴f(z) max(0, z)$z 0$ 时导数恒为 1梯度原封不动地传回去$z \le 0$ 时导数为 0输出也是 0。正是这个正半轴导数恒等的特性让深层网络的训练在 2012 年之后变得可行。它的优点很实在没有饱和区正半轴、计算极快、能产生稀疏激活大约一半神经元输出为 0稀疏性在某些任务上还带来了隐性的正则效果。但它也留下了三个新问题。第一是死神经元。如果一个神经元的预激活值长期为负导数恒为 0权重就永远得不到更新这个神经元等于死了再也不会复活。学习率设得太大时这种情况会成片出现。你可以在训练时统计每层激活值中为 0 的比例如果某些层长期高于 90%就得警惕了。第二是输出非零中心和 Sigmoid 一样的毛病只是程度上没那么严重。第三是无上界。输出可以无限制增大深层叠加时激活值可能爆炸式增长所以用 ReLU 的网络对初始化方式和学习率更敏感。这也是 He 初始化Kaiming 初始化被提出来专门配合 ReLU 的原因。3.4 Leaky ReLU、ELU、GELU、Swish 各自在补什么漏后面这一批激活函数基本都是在 ReLU 的基础上打补丁。Leaky ReLU给负半轴留了一个很小的斜率 $\alpha$通常取 0.01f(z) max(α·z, z)负半轴导数变成 $\alpha$ 而不是 0死神经元的问题大大缓解。缺点是 $\alpha$ 该设多少要试0.01 是经验值但并非对所有任务都最优。ELU把负半轴换成一条平滑的指数曲线 $\alpha(e^z - 1)$在 $z0$ 处导数连续输出均值更接近 0而且负半轴有下界 $-\alpha$抗噪声能力稍好。代价是要算指数慢一些。GELU的表达式是 $z \cdot \Phi(z)$其中 $\Phi$ 是标准正态分布的累积分布函数。它的直观含义是用输入自身的大小作为保留概率的门控。这个函数在 Transformer 系列模型里是默认配置BERT、GPT 系的隐藏层基本都是 GELU 或它的近似版本。它的曲线在小负值区域会先往下凹一点再回升属于非单调激活。Swish / SiLU是 $z \cdot \sigma(\beta z)$其中 $\beta$ 可调$\beta1$ 时就是 SiLU。它在深层网络上经常略优于 ReLU但计算量更高而且性能提升不是稳定复现的所以工业界用得不如 ReLU 和 GELU 普遍。选哪个我的实际经验是没有明确理由时隐藏层默认 ReLU做 NLP 或 Transformer 结构用 GELU发现死神经元比例异常高再考虑 Leaky ReLU 或 ELU。不要一上来就追求最新最花哨的那个先把基线跑通更重要。4. 写四十行代码把梯度消失看见概念讲再多不如亲手跑一遍。这一节我用 numpy 实现四个激活函数和它们的导数然后设计一个连乘实验让不同激活函数在深层网络里的梯度衰减差异变成具体的数字。4.1 numpy 实现激活函数与导数import numpy as np def sigmoid(z): return 1.0 / (1.0 np.exp(-z)) def sigmoid_grad(z): s sigmoid(z) return s * (1.0 - s) def tanh(z): return np.tanh(z) def tanh_grad(z): return 1.0 - np.tanh(z) ** 2 def relu(z): return np.maximum(0.0, z) def relu_grad(z): return (z 0).astype(z.dtype) def leaky_relu(z, alpha0.01): return np.where(z 0, z, alpha * z) def leaky_relu_grad(z, alpha0.01): return np.where(z 0, 1.0, alpha)有几个实现细节值得说。sigmoid在 $z$ 绝对值很大时会溢出警告稳妥的写法是按符号分段计算但对于本文的量级$|z|5$直接用就行。relu_grad用(z 0)而不是(z 0)这样 $z0$ 处的导数约定为 0和主流框架的默认行为一致。4.2 十层连乘不同激活函数差了几个数量级梯度消失的本质是导数连乘。我写了个小实验模拟十层网络每层抽一个预激活值样本把十层的导数相乘看最终结果的量级import numpy as np rng np.random.default_rng(42) def chain_decay(grad_fn, depth10, n200000, std1.5): # 每一行代表一条从输出层回到第 1 层的路径 z rng.normal(0.0, std, size(n, depth)) g np.abs(grad_fn(z)) return g.prod(axis1).mean() for name, fn in [(Sigmoid, sigmoid_grad), (Tanh, tanh_grad), (ReLU, relu_grad), (LeakyReLU, leaky_relu_grad)]: print(f{name:12s} 十层连乘后的平均梯度量级: {chain_decay(fn):.3e})我这边跑出来的结果大致是这样的量级Sigmoid 十层连乘后的平均梯度量级: 1.2e-08 Tanh 十层连乘后的平均梯度量级: 1.1e-02 ReLU 十层连乘后的平均梯度量级: 8.9e-04 LeakyReLU 十层连乘后的平均梯度量级: 1.3e-03Sigmoid 在这里被压到了 $10^{-8}$ 量级——这意味着第 1 层的参数几乎收不到任何有效的更新信号训练基本停在原地。Tanh 好一些但也掉了两个数量级。ReLU 系则维持在同一量级附近。当然这只是个简化模型真实网络里还有权重矩阵的乘法和归一化层的调节实际衰减曲线不会这么干净。但这个实验足以说明一件事梯度消失不是玄学它就是导数连乘的结果用几行代码就能复现。数字每次跑会有波动但不同激活函数之间的量级差距是稳定的。4.3 死神经元检测ReLU 的另一面ReLU 解决了梯度消失代价是可能产生死神经元。检测方法很简单在前向传播时统计每层激活值为 0 的比例def dead_ratio(activation): # activation: shape (batch, units) return float((activation 0).mean()) # 用法示意伪代码 # a relu(z) # print(f第 {layer_id} 层死神经元占比: {dead_ratio(a):.2%})我的经验判据是这样的正常情况下ReLU 层大约有 40% 到 60% 的神经元输出为 0这是稀疏性的正常表现。如果某一层长期超过 85%而且训练损失长时间不下降那基本可以判定出现了大面积死神经元。解决方法按优先级排降低学习率这是最直接有效的一招把 ReLU 换成 Leaky ReLU 或 ELU给负半轴留一条通路检查初始化方式改成 He 初始化标准差 $\sqrt{2/\text{fan_in}}$在激活函数前加 BatchNorm 或 LayerNorm把预激活值的分布拉回中心附近。注意死神经元的检测要放在训练过程中做而不是只看初始化后的第一次前向。初始化阶段权重随机激活分布还没稳定此时的统计没有参考价值。5. 真正动手建模时激活函数怎么选前面讲的是原理和实验这一节直接给可执行的选型规则。把这部分记下来能省掉你大量试错时间。5.1 隐藏层默认 ReLU 系三种情况要换隐藏层的默认答案就是 ReLU简单、快、表现稳定。但下面三种情况我建议换掉情况一出现大面积死神经元换成 Leaky ReLU$\alpha0.01$或 ELU。前者更快后者输出更平滑我一般先试 Leaky ReLU。情况二做 Transformer 或 Attention 结构直接用 GELU。这不是跟风而是这一类结构里 GELU 的平滑性对注意力权重的分布更友好实测收敛更稳。情况三网络极深超过 30 层且没有归一化层可以考虑 Swish/SiLU或者干脆补上残差连接和归一化比换激活函数更有效。有个反直觉的结论要提一下在网络已经很深、并且配了 BatchNorm 和残差连接的情况下激活函数的选择对最终精度的影响会明显变小。我做过几次对比ResNet 结构下把 ReLU 换成 GELU精度差异经常在 0.3 个百分点以内。所以别把调参精力全花在换激活函数上结构和数据质量的重要性远高于它。5.2 输出层按任务类型对号入座输出层是最不能乱来的地方因为它的输出要直接对应任务的语义。这张表我建议直接收藏任务类型输出层激活配套损失函数备注回归预测房价、温度无线性MSE / MAE千万别加 ReLU会把负值截断二分类Sigmoid单输出BCE推荐用BCEWithLogitsLoss合并多分类互斥Softmax交叉熵各类别概率和恒为 1多标签不互斥每个类别 SigmoidBCE每类独立判定概率和不必为 1这里有个坑必须点明多分类和多标签是两个不同的任务激活函数完全不同。一张图里既有猫又有狗这是多标签用 Sigmoid一张图里只能是猫或狗或鸟其中一个这是多分类用 Softmax。用错了不会报错但模型学不好而且你可能很久都发现不了。5.3 初始化必须和激活函数配套这一条是被最多人忽略的。激活函数换了初始化方式没跟着换训练照样出问题。推导逻辑是这样的假设某一层有 $n$ 个输入权重 $w_i$ 独立同分布、均值为 0、方差为 $\sigma_w^2$输入 $x_i$ 方差为 1那么预激活值 $z \sum w_i x_i$ 的方差就是 $n\sigma_w^2$。为了让 $z$ 的方差保持在 1 附近需要 $\sigma_w 1/\sqrt{n}$这就是 Xavier 初始化的思路。但 Xavier 推导时假设激活函数在原点附近近似线性、导数接近 1。ReLU 在正半轴导数是 1负半轴是 0平均下来相当于把方差砍了一半所以需要把权重方差放大一倍来补偿σ_w sqrt(2 / fan_in) # He 初始化配 ReLU σ_w sqrt(1 / fan_in) # Xavier 初始化配 Tanh / Sigmoidfan_in是这一层的输入维度。PyTorch 里的kaiming_normal_就是 He 初始化xavier_normal_是 Xavier。用 ReLU 却上了 Xavier前几层的激活值方差会逐层缩小网络越深越安静反过来用 Tanh 配 He激活值会逐层放大容易爆。5.4 学习率、归一化层和激活函数的三角关系这三者是联动的单看任何一个都容易判断失误。ReLU 因为没有上界激活值可能逐层放大所以对学习率更敏感学习率偏大时死神经元会成片出现。加入 BatchNorm 之后每层的预激活值被重新拉回均值 0、方差 1ReLU 的输入分布稳定了死神经元比例会明显下降同时学习率也可以适当调大。这就是为什么现代网络结构里BatchNorm 和 ReLU 几乎是绑定出现的。反过来如果你用的是 Tanh 或 Sigmoid 这种有界激活函数激活值本身不会爆对学习率的容忍度反而更高但梯度消失的问题会更突出这时候归一化层的作用就变成了缓解梯度衰减。实际操作顺序我一般是这样先定激活函数和初始化跑一次看损失曲线曲线不降就查梯度量级梯度没问题但收敛慢再加归一化层调学习率。一步一步来比一次性把所有变量都改了更容易定位问题。6. 我踩过的几个坑你可以直接绕开写了这么多原理和代码最后把几个真实踩过的坑摊开说说都是那种当时查了很久、后来发现原因特别简单的类型。第一个坑是在回归任务的输出层加了 ReLU。当时做的是一个预测温度变化的小项目模型结构照搬了一个图像分类的例子输出层保留了 ReLU。结果预测值全都大于等于 0负温度样本怎么都学不会误差大得离谱。回归任务的输出层必须是线性的什么都不加。这个错误的隐蔽之处在于训练损失会下降只是永远降不到该有的水平很容易误判成模型容量不够。第二个坑是把 Softmax 和交叉熵拆开写导致数值不稳定。我早期习惯先在模型里过一遍 Softmax再送到CrossEntropyLoss里结果遇到了nan。原因是 Softmax 里有指数运算logits 稍微大一点就溢出。正确做法是模型输出原始 logits把 Softmax 交给损失函数内部处理它会用 log-sum-exp 的技巧做数值稳定化。二分类同理用BCEWithLogitsLoss而不是Sigmoid BCELoss。第三个坑只换激活函数不动初始化。有一次想把隐藏层的 ReLU 换成 Tanh 试试效果改了一行代码就跑了结果训练直接崩了损失从一开始就是nan。查了半天才发现初始化还是按 ReLU 配的 He 初始化方差对 Tanh 来说太大第一层的激活值就爆了。换激活函数时初始化方式必须一起改。第四个坑忘了做梯度检查就上大模型。我在小网络上验证想法时经常跳过梯度检查因为跑得快、错了也能看出来。但有一次在大网络上调结构损失就是不降排查了一整天才发现是激活函数的导数实现写错了把1 - tanh²写成了1 - tanh。从那以后我改完任何涉及激活函数的代码都会先用两三个样本跑一次数值梯度对比几十行代码的事能省掉一整天的排查。第五个坑用测试集的表现反推激活函数的选择。这个属于方法论问题。有一段时间我在测试集上反复对比 ReLU 和 GELU挑了个测试集精度高的结果模型上线后表现很一般。原因是测试集被我反复使用实际上变成了验证集产生了信息泄漏。激活函数的对比实验应该在验证集上做测试集只在最后评估一次。第六个坑盲目相信最新最好。有一阵子我把所有模型的隐藏层都换成了 GELU觉得既然大模型都用它肯定没错。但在几个小规模的全连接网络上GELU 的表现和 ReLU 几乎没有差别训练时间却长了大约 20%。后来我的做法是先在 ReLU 上把基线跑通确认数据、结构、超参都没问题再考虑换激活函数而且换的时候只换这一个变量。顺带说一句关于人工智能学习路径的事。激活函数这个概念看起来只是神经网络里的一个小零件但它串起了线性代数矩阵连乘、微积分链式法则、概率论Softmax 和交叉熵和数值计算溢出与稳定性四块内容。我在学的时候是先背下了公式过了很久才真正理解导数连乘意味着什么。如果你正在这个阶段我建议别急着往前赶花两个小时把第 4 节那个连乘实验自己跑一遍把数字改一改、层数调一调看着梯度量级从 $10^{-2}$ 掉到 $10^{-8}$——那一下的直观感受比读十篇讲梯度消失的文章都管用。
返回列表