提到神经网络就很难绕开激活函数。我最早接触深度学习时干的第一件事就是把常见的几个激活函数的公式、导数、代码和图像全部过了一遍。今天这篇内容就是当时整理的强化版Sigmoid、Tanh、ReLU、Leaky ReLU、ELU、Softmax每个都会有数学公式、导数推导过程、可以直接跑的Python实现以及画出来的可视化图像。同时我会把训练中常见的梯度消失、神经元死亡、Softmax数值溢出这些坑一并聊清楚。如果你正在入门深度学习或者写反向传播时被导数折腾得头疼这篇内容基本就是一份可以照着用的速查手册。1. 激活函数为什么神经网络离不开这道“开关”1.1 没有激活函数堆再深也只是线性变换很多初学者会问神经网络隐藏层里为什么不能直接做线性变换一定要在中间插入一个非线性函数先算一笔账。假设我不用激活函数只做线性变换两层网络的计算就是 y W2 (W1 x b1) b2把括号展开之后得到 y (W2 W1) x (W2 b1 b2)。这个形式和单层线性层 y W x b 完全一样。换句话说不管堆多少层整个网络本质上等价于一层线性变换。而线性变换能拟合的空间非常有限面对图像分类、语音识别这类复杂问题根本不够用。激活函数做的事情就是给每一层输出加入非线性。比如 ReLU 的 max(0, x)一剪刀下去负数直接清零正数保留网络开始有能力去逼近任意复杂的函数。这也是“万能逼近定理”能成立的基础只要有足够的神经元、合适的权重并且每个神经元后面接一个非线性激活函数整个网络就可以逼近绝大多数连续函数。1.2 一个理想激活函数应该同时具备哪些特质我在实际项目里评估一个激活函数好不好用一般会看四个维度。公式要可导。反向传播要求每一层的梯度可以向前传递而梯度依赖导数的乘法链。如果函数处处不可导或者基本不可导梯度更新就走不下去。ReLU 在 x0 这个点其实不可导但工程实现上大家约定取 0 或 1这种“点上的例外”对训练影响极小。梯度要处处“健康”。这个背后就是梯度消失和梯度爆炸的问题。如果一个函数在某段区间导数接近 0深层的梯度传到浅层时会被反复乘小最后变成接近 0 的数导致浅层权重完全不动。反过来说如果导数值太大乘多了就溢出成 NaN。激活函数的导数最好保持在比较稳的范围内比如 0 到 1 之间或者 1 左右。输出最好接近零中心化。输出均值不是 0 会带来一个很隐蔽的问题下一层神经元拿到的输入全为正或全为负更新权重时梯度方向会朝同一侧偏优化起来不够灵活。这类问题在 Sigmoid 上特别明显而 Tanh 就是专门为解决这个缺陷出现的。最后是计算成本。训练一个模型要算几百万次激活和导数如果函数相对 exp 这类指数计算开销会明显高于 max、乘加这类简单运算。ReLU 之所以成为默认选择除了效果好也有一部分原因是它便宜到几乎可以忽略不计。2. 常见激活函数的公式与核心性质2.1 Sigmoid经典 S 曲线只推荐用在二分类输出层Sigmoid 的公式是sigma(x) 1 / (1 e^(-x))对应的导数是sigma(x) sigma(x) * (1 - sigma(x))它把任意实数压缩到 (0, 1) 区间天然适合表示概率、置信度一类的东西。图像是一条平滑的 S 曲线在 x0 附近斜率最大越往两边越平缓最后趋近于 0 和 1。导数在 x0 处取到最大值 0.25然后向两侧指数级衰减。这个“指数级衰减”就是它在深层网络里不受欢迎的原因。你设想一下神经网络 10 层每层导数即便都取最大值 0.25反向传播梯度相乘就是 0.25^10约等于 0.00000095传到前两层几乎变成零。这就叫梯度消失。真实情况下大部分神经元根本达不到最大值所以 Sigmoid 只适合放在最后一层做二分类输出中间隐藏层用它的效果往往很差。另一个明显的缺陷是输出非零中心。所有输出都在 (0, 1)意味着后一层输入永远是正数权重更新时要么全部正向要么全部负向收敛速度会被拖慢。此外公式里有 e^(-x)在 x 非常负时 np.exp(-x) 可能溢出后面代码部分我会给一个更稳的写法。2.2 Tanh把输出中心化到零的进化版Tanh 的公式是tanh(x) (e^x - e^(-x)) / (e^x e^(-x))它和 Sigmoid 之间有一个简单关系tanh(x) 2 * sigma(2x) - 1导数为tanh(x) 1 - tanh^2(x)Tanh 把输出范围压缩到 (-1, 1)并且以 0 为中心这就解决了 Sigmoid 输出非零中心的问题。梯度在 x0 附近最大值是 1比 Sigmoid 的 0.25 高不少梯度衰减速度显著降低。正因如此在早期神经网络和很多循环神经网络的结构里Tanh 比 Sigmoid 更受青睐。比如一些 LSTM 变体设计候选记忆状态时还是会使用 Tanh 来把新鲜信息控制在 -1 到 1 之间。它的缺点在两端依然存在x 很大或很小时导数趋近于 0照样梯度饱和。因此它适合做短路径的中间变换不适合在超深网络中从头用到尾。2.3 ReLU隐藏层的默认选择ReLU 的全称是 Rectified Linear Unit公式极其简单f(x) max(0, x)分段写作f(x) 0, x 0 f(x) x, x 0导数为f(x) 0, x 0 f(x) 1, x 0 x 0 处不可导工程实现上一般取 0 或 1ReLU 最强的地方有三个。正半轴导数恒为 1梯度在正区间可以无损通过这就有效缓解了梯度消失。计算只有一次比较大小比指数运算便宜得多训练速度快。负数直接置零带来稀疏激活模拟了生物神经元“跨过阈值才响应”的特性。但它也有一个著名的问题叫神经元死亡。如果某个神经元在训练中被更新成对所有输入都输出负值那它的输出恒为 0导数在反向传播时恒为 0梯度无法继续更新这个神经元它就彻底死掉再也不会被激活。后面我专门有一节聊这个问题。2.4 Leaky ReLU 与 ELU处理神经元死亡的两个方案Leaky ReLU 的公式是f(x) x, x 0 f(x) alpha * x, x 0其中 alpha 一般取 0.01。导数f(x) 1, x 0 f(x) alpha, x 0它的思路很简单ReLU 死掉是因为负数部分梯度恒为 0那我就给负数部分一个非常小的非零梯度比如 0.01让它在负数区域也能缓慢更新有机会“复活”。我在实际训练中观察过使用 Leaky ReLU 后网络里的死神经元数量通常比 ReLU 少很多尤其是使用较小学习率时这种优势更稳定。alpha 如果变成可学习的参数就是 PReLU如果训练时随机采样、测试时用均值就是 RReLU。ELU 在负数部分变得更平滑公式是f(x) x, x 0 f(x) alpha * (e^x - 1), x 0通常 alpha 1导数f(x) 1, x 0 f(x) alpha * e^x, x 0ELU 的好处是负数部分不是一条固定斜率的直线而是逐渐饱和到 -alpha 的平滑曲线对输入的扰动容忍度更好输出均值也更接近 0。代价是引入了指数运算计算量比 Leaky ReLU 大很多训练速度会慢一些。我的建议是追求极致的训练吞吐优先选 Leaky ReLU追求更平滑的梯度行为和稍好的精度可以试 ELU。2.5 Softmax从得分到概率分布Softmax 跟前几种激活函数最大的区别是输入不是单个数值而是一个向量 z输出也不是单个数值而是一个向量 y。定义如下y_i e^(z_i) / sum_j e^(z_j)对所有 i 求和得到 1所有 y_i 都大于 0于是输出向量可以被解释为一个概率分布。多分类神经网络的最后一层通常就是 Softmax。Softmax 的导数不能只写一个标量表达式。因为输出向量的每个分量 y_i 都受到输入向量所有分量 z_j 的影响所以导数是雅可比矩阵矩阵元素为dy_i / dz_j y_i * (delta_ij - y_j)其中 delta_ij 是克罗内克符号。当 i j 时导数等于 y_i * (1 - y_i)当 i ! j 时导数等于 -y_i * y_j。这个导数形式虽然不算复杂但手工推导时容易搞混行列顺序。更实用的是记住当 Softmax 和交叉熵损失联合使用做多分类时反向传播梯度会非常简洁地变成 y_hat - y其中 y_hat 是预测概率y 是 one-hot 标签。这个我在推导部分会展开讲。3. 导数推导梯度公式究竟怎么来的3.1 Sigmoid 导数推导的完整过程Sigmoid 的导数值得亲手推一遍因为 Tanh 的推导也会用到同样思路。由 sigma(x) 1 / (1 e^(-x))对分式求导d sigma / dx d( (1 e^(-x))^(-1) ) / dx -(1 e^(-x))^(-2) * d(1 e^(-x)) / dx -(1 e^(-x))^(-2) * (-e^(-x)) e^(-x) / (1 e^(-x))^2这个时候推到 e^(-x) / (1 e^(-x))^2 已经算完成但为了在代码里方便实现通常会再改写一步。分子分母同时除以 (1 e^(-x))^2 的等价变形sigma(x) [1 / (1 e^(-x))] * [e^(-x) / (1 e^(-x))] sigma(x) * [1 - 1 / (1 e^(-x))] sigma(x) * (1 - sigma(x))所以最终得到 sigma(x) sigma(x) * (1 - sigma(x))。这个形式最大的好处是前向传播如果能顺手保存下当时的 sigma(x) 值反向传播直接做一次乘法和一次减法就拿到导数不需要重新计算 exp。3.2 Tanh 导数推导的两种思路第一种思路直接对定义求导。tanh(x) (e^x - e^(-x)) / (e^x e^(-x))用商法则展开后经过化简可以得到 tanh(x) 1 - tanh^2(x)。这个过程符号比较多容易出错我习惯用第二种思路。第二种思路利用 tanh(x) 2 * sigma(2x) - 1 的关系。两边同时求导tanh(x) 2 * sigma(2x) * 2 4 * sigma(2x) * (1 - sigma(2x))再利用 tanh(x) 2 * sigma(2x) - 1 反解出 sigma(2x) (tanh(x) 1) / 2代进去tanh(x) 4 * [(tanh(x) 1) / 2] * [1 - (tanh(x) 1) / 2] 4 * [(tanh(x) 1) / 2] * [(1 - tanh(x)) / 2] (tanh(x) 1) * (1 - tanh(x)) 1 - tanh^2(x)下面是完整过程。这个方法逻辑清晰而且能看出 Tanh 和 Sigmoid 之间的亲缘关系。Tanh 在 x0 处导数为 1比 Sigmoid 在 x0 处的 0.25 整整大了 4 倍这就是为什么它和 Sigmoid 相比在同等网络深度下梯度衰减慢得多。3.3 ReLU、Leaky ReLU 与 ELU 的分段导数怎么确定ReLU 在 x 0 时导数为 1x 0 时导数为 0x 0 处严格来说不可导。数学上会写出一个分段表达式加一个“x 0 处不可导”的说明工程代码里则用 np.where(x 0, 1.0, 0.0) 或者 np.where(x 0, 1.0, 0.0) 来近似处理。两种取法对训练结果的影响微乎其微因为输入刚好精确等于 0 的概率极低即使出现无论梯度取 0 还是 1影响也只是一个点的参数更新量。Leaky ReLU 的分段导数只需要把负数部分从 0 换成 alpha。代码里特别注意一点np.where 的三个分支类型最好统一返回浮点数。我见过有人写成 np.where(x 0, 1, 0)在后续乘浮点梯度时类型自动转换通常没问题但风格上建议直接写 1.0 和 0.0。ELU 在 x 0 时导数为 1在 x 0 时对 alpha * (e^x - 1) 求导得到 alpha * e^x。这里有个很容易被忽略的等价关系alpha * e^x 恰好等于 f(x) alpha所以代码里也可以写成 np.where(x 0, 1.0, f(x) alpha)前提是 f(x) 已经算出来并且 x 恰好小于等于 0 时这个数值才有效。3.4 Softmax 的雅可比矩阵推导与实用化简化对 Softmax 求导时要小心因为每个输出 y_i 的分母里都包含所有 e^(z_j)所以 y_i 对任意一个 z_j 都有导数。先写出y_i e^(z_i) / S其中 S sum_k e^(z_k)对 j 求导。当 j i 时分子和分母都依赖 z_j要同时作用d y_i / d z_i (e^(z_i) * S - e^(z_i) * e^(z_i)) / S^2 (e^(z_i) / S) - (e^(z_i) / S)^2 y_i - y_i^2 y_i * (1 - y_i)当 j ! i 时分子不依赖 z_j只有分母依赖d y_i / d z_j (-e^(z_i) * e^(z_j)) / S^2 -y_i * y_j到此为止雅可比矩阵的每个元素都有了。写成矩阵形式就是J diag(y) - y * y^T这个式子看起来很规整实际手动推导的时候最常遇到的问题是下标搞反到底是 dy_i/dz_j 还是 dy_j/dz_i。记法上明确“输出下标 i 在前、输入下标 j 在后”然后按 ij 和 i≠j 分情况讨论就不会乱。但在实际训练里很少真的有代码会手写这个雅可比矩阵因为 Softmax 几乎必然和交叉熵损失成对出现。交叉熵 L -sum t_i * log(y_i) 对 z_j 求导经过化简后得到d L / d z_j y_j - t_j其中 t 是 one-hot 标签向量。这个梯度就是“预测减真实”。这也是为什么绝大多数框架的交叉熵和 Softmax 会合并成一个算子实现不是因为不能分开而是合起来算更快、数值也更稳。4. 代码实现与可视化公式落地的完整流程4.1 编程环境与绘图约定我推荐直接使用标准的数据科学三件套NumPy 做数值计算Matplotlib 做绘图科学计算或交互式环境中可以直接跑。本文的所有东西不需要安装深度学习框架因为激活函数和它们的导数本质上是纯数学函数NumPy 的向量化运算就能完美表达。绘图方面我习惯把函数值画成实线导数画成虚线用同一张图来对比。x 轴范围设为 -5 到 5这个区间足以展示各种激活函数的典型行为。如果是在无中文环境或者遇到字体乱码可以把图例改成英文或者用 Matplotlib 的中文字体设置解决。为了保证代码在任何机器上都能直接复制运行示例代码里的图例我会用英文标签避免本地没有中文字体时出现乱码方框。4.2 激活函数与导数函数定义下面是完整代码。我把每个函数单独定义同时保证数值稳定性而不是简简单单地照抄公式。import numpy as np import matplotlib.pyplot as plt def sigmoid(x): # 分段实现防止 exp 溢出 return np.where( x 0, 1.0 / (1.0 np.exp(-x)), np.exp(x) / (1.0 np.exp(x)) ) def sigmoid_derivative(x): s sigmoid(x) return s * (1.0 - s) def tanh(x): return np.tanh(x) def tanh_derivative(x): return 1.0 - np.tanh(x) ** 2 def relu(x): return np.maximum(0.0, x) def relu_derivative(x): # 工程约定x0 处取 1 return np.where(x 0, 1.0, 0.0) def leaky_relu(x, alpha0.01): return np.where(x 0, x, alpha * x) def leaky_relu_derivative(x, alpha0.01): return np.where(x 0, 1.0, alpha) def elu(x, alpha1.0): return np.where(x 0, x, alpha * (np.exp(x) - 1.0)) def elu_derivative(x, alpha1.0): result np.where(x 0, 1.0, alpha * np.exp(x)) return result这里实现 Sigmoid 时我做了数值稳定处理。如果直接用 1 / (1 np.exp(-x))当 x 很小比如 -800 时np.exp(800) 会触发溢出警告结果变成 0当 x 很大时虽然不会溢出但会损失精度。分成 x 0 和 x 0 两段分别用 np.exp(-x) 和 np.exp(x)就能安全处理极端输入。4.3 可视化脚本与图像解读激活函数和对应的导数放在同一张子图里看看它们的相对关系。x np.linspace(-5.0, 5.0, 1000) activations [ (Sigmoid, sigmoid, sigmoid_derivative), (Tanh, tanh, tanh_derivative), (ReLU, relu, relu_derivative), (Leaky ReLU, leaky_relu, leaky_relu_derivative), (ELU, elu, elu_derivative), ] fig, axes plt.subplots(2, 3, figsize(14, 8)) axes axes.flatten() for ax, (name, func, dfunc) in zip(axes, activations): y func(x) d dfunc(x) ax.plot(x, y, labelf(x), linewidth2) ax.plot(x, d, labelf(x), linewidth2, linestyle--) ax.axhline(0, colorgray, linewidth0.8) ax.axvline(0, colorgray, linewidth0.8) ax.set_title(name) ax.set_ylim(-1.5, 2.5) ax.grid(True, alpha0.3) ax.legend() # 最后一个子图展示 Softmax 对输入向量的响应 z np.array([2.0, 1.0, 0.1]) softmax_out np.exp(z - z.max()) / np.sum(np.exp(z - z.max())) ax axes[-1] ax.bar([0, 1, 2], softmax_out) ax.set_title(Softmax Output) ax.set_xticks([0, 1, 2]) ax.set_ylim(0, 1) fig.tight_layout() plt.show()跑完这张图你能读出很多信息。Sigmoid 的函数曲线在 x0 附近最陡导数在 0.5 附近达到峰值 0.25两端导数快速贴到 0这就是梯度消失的图像证据。Tanh 曲线和导数曲线都在 x0 附近表现更陡导数峰值到 1且函数以 0 为中心所以比 Sigmoid 更适合作隐藏层激活。ReLU 的导数在正半轴是一条水平直线这是它能保持梯度流动的原因也让它的图像特征看起来最“凌厉”。Leaky ReLU 和 ELU 在负数部分都留下了修正可以看到导数在负半轴不再恒为 0区别在于 Leaky ReLU 是水平线ELU 是慢慢爬升的曲线。4.4 代码层面的几个细节对于 Sigmoid 和 ELU代码里用了 np.where这是向量化写法可以得到和 for 循环一样的结果但速度快很多。有个细节是 np.where(x 0, 1.0, 0.0) 中三个参数最好都是同一类型避免后续计算时类型被隐式转换。如果写成 np.where(x 0, 1, 0) 也通常没问题但返回的是 int 数组和浮点数组相乘时结果依然是浮点只是风格上不太一致。绘图时的 y 轴范围我固定到 -1.5 到 2.5不然 Tanh 的曲线会把整个图撑得很高ReLU 在 x 轴正半轴的值又会反向拉长坐标轴导致小尺度细节看不清。实际项目中如果你画出来发现某条曲线在边界处截断那就是数据范围输出了预设区间证明当前输入超过了可视化窗口。最后要验证自己写的导数实现对不对最简单的方式是准备一组随机的 x然后和数值近似对比对每个 x 用 (f(x h) - f(x - h)) / (2h) 算一次中心差分h 取 1e-6看两边差多少。误差应该在 1e-6 量级如果差异明显说明公式或者下标写错了。这个方法在我的调试中救过很多次。5. 训练中的诡异问题排查与实战避坑5.1 Loss 不下降或直接变成 NaNLoss 一直不降最常见的原因之一就是激活函数选错了位置。隐藏层全部用 Sigmoid网络稍微深一点梯度传到中间层就衰减到几乎为零前面几层权重根本更新不动整个模型相当于只有最后两层在训练。另一个极端是梯度爆炸损失直接变成 NaN。这种情况常见于网络层数较深、权重初始化过大、学习率过高的时候。激活函数如果还有很大的导数比如 ReLU 正区间导数为 1多层相乘后数值也可能指数放大。排查思路从日志开始每个 epoch 记录 loss如果第一轮就出 NaN优先怀疑输入数据里有没有无穷大或 NaN其次看学习率是不是太大。如果训练中途才出现 NaN再配合梯度裁剪或者降低学习率。激活函数这层检查主要看有没有在隐藏层用了 Sigmoid以及有没有忘记对输出层做合理选择。5.2 大量神经元输出恒为 0网络“死亡”ReLU 把一个输入为负的神经元输出置零这本身是特点但如果大量神经元在训练后对所有输入都输出 0就成了灾难。这种“神经元死亡”会让网络容量实际变小模型可能训练不动或者收敛到很差的结果。我经常用一个小脚本检查这个问题拿一批验证数据跑前向传播统计每个 ReLU 层的输出中 0 的占比。如果某层有超过 40% 甚至更多的神经元长期输出 0就要警惕了。应对手段无非三条降低学习率换 Leaky ReLU 或 ELU检查权重初始化是否合理。初始化太激进比如标准差设得过大很多神经元的输入一开始就落在负区间神经元可能在第一轮就被“杀死”后面再也缓不过来。5.3 Softmax 输出出现 NaNSoftmax 的计算过程要先求 e^(z_i)如果 z_i 里有一个很大的值比如 1000e^1000 在浮点数中会直接溢出结果变成 inf负方向的 e^-1000 则变成 0整条计算链都会污染成 NaN。解决办法一句话先减去最大值。因为 Softmax 对输入同时平移一个常数不改变结果e^(z_i - m) / sum_j e^(z_j - m)其中 m max_j z_j这个恒等式非常直观平移之后所有指数幂里的底数最大就是 e^01指数计算绝对不会溢出。所有主流深度学习框架的 Softmax 实现都做了这一步。如果你手写多分类训练代码一定要记住这个技巧。我见过不少人在自研框架里因为没做减最大值处理训练到中期 loss 直接变成 NaN浪费了一整天排查时间。5.4 导数实现容易出错的几个边界导数实现出错的地方通常都很固定。第一ReLU 在 x0 处的导数取 0 还是取 1虽然理论上无伤大雅但如果你边写测试边和框架对比两边取法不一样会造成小的数值差异。第二Leaky ReLU 的 alpha 很容易被写成 0.1 或 0.2然后代码里忘记同步到导数函数中。我更建议把 alpha 定义成模块级常量或者用带默认参数的函数让函数和导数的默认值永远保持一致。第三ELU 的导数 x 0 部分是 alpha * e^x如果你用 np.where(x 0, 1.0, elu(x) alpha)必须确保传入 np.where 的 elu(x) 先被赋值否则表达式会因为在分支里递归调用自身而报错。5.5 激活函数选择速查表每个人的项目场景不同但选择逻辑是可以模板化的。我经常用的一个速查表是这样的应用位置推荐选择理由前馈网络隐藏层默认ReLU简单、快、能缓解梯度消失隐藏层频繁出现死神经元Leaky ReLU / PReLU让负数部分保留梯度隐藏层希望输出平滑ELU / GELU负区间平滑接近零中心二分类输出层Sigmoid输出可以解释为概率多分类输出层Softmax输出总和为 1 的概率分布回归输出层无激活函数线性输出直接预测连续值循环神经网络内部Tanh 或相关变体梯度幅度更大适合序列建模Transformer / 大规模模型中间层GELU / Swish平滑更利于深度网络训练收敛这张表不是死规则。比如卷积网络里用 ReLU 通常很好但轻量级网络为了压缩模型尺寸可能也会考虑更平滑的激活函数来提升精度。最终还是要用你自己的验证集数据说话。我的习惯是默认先跑一版 ReLU如果训练曲线“诡异”或性能不理想再快速替换成 Leaky ReLU 对比几轮。激活函数替换成本极低但选型对训练收敛速度的影响可能非常明显。最后分享一个我做对比实验时惯用的办法在同一个模型上只改激活函数这一处其他超参数全部固定然后分别记录训练 20 轮的 loss 曲线和验证集指标。这个操作能最直观地看出不同激活函数的差异也方便自己形成经验。就我的经验来说与其花大量时间调整复杂的网络结构不如先检查组里有没有用错激活函数很多时候性能瓶颈的根源就藏在这一行简单的公式里。