提到神经网络,就很难绕开激活函数。我最早接触深度学习时,干的第一件事就是把常见的几个激活函数的公式、导数、代码和图像全部过了一遍。今天这篇内容就是当时整理的强化版: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 在 x=0 这个点其实不可导,但工程实现上大家约定取 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 曲线,在 x=0 附近斜率最大,越往两边越平缓,最后趋近于 0 和 1。导数在 x=0 处取到最大值 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 输出非零中心的问题。梯度在 x=0 附近最大值是 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 或 1
ReLU 最强的地方有三个。正半轴导数恒为 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 <= 0
ELU 的好处是负数部分不是一条固定斜率的直线,而是逐渐饱和到 -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 在 x=0 处导数为 1,比 Sigmoid 在 x=0 处的 0.25 整整大了 4 倍,这就是为什么它和 Sigmoid 相比,在同等网络深度下梯度衰减慢得多。
3.3 ReLU、Leaky ReLU 与 ELU 的分段导数怎么确定
ReLU 在 x > 0 时导数为 1,x < 0 时导数为 0,x = 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 在后”,然后按 i=j 和 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): # 工程约定:x=0 处取 1 return np.where(x > 0, 1.0, 0.0) def leaky_relu(x, alpha=0.01): return np.where(x > 0, x, alpha * x) def leaky_relu_derivative(x, alpha=0.01): return np.where(x > 0, 1.0, alpha) def elu(x, alpha=1.0): return np.where(x > 0, x, alpha * (np.exp(x) - 1.0)) def elu_derivative(x, alpha=1.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, label="f(x)", linewidth=2) ax.plot(x, d, label="f'(x)", linewidth=2, linestyle="--") ax.axhline(0, color="gray", linewidth=0.8) ax.axvline(0, color="gray", linewidth=0.8) ax.set_title(name) ax.set_ylim(-1.5, 2.5) ax.grid(True, alpha=0.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 的函数曲线在 x=0 附近最陡,导数在 0.5 附近达到峰值 0.25,两端导数快速贴到 0,这就是梯度消失的图像证据。Tanh 曲线和导数曲线都在 x=0 附近表现更陡,导数峰值到 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 不下降或直接变成 NaN
Loss 一直不降,最常见的原因之一就是激活函数选错了位置。隐藏层全部用 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 输出出现 NaN
Softmax 的计算过程要先求 e^(z_i),如果 z_i 里有一个很大的值,比如 1000,e^1000 在浮点数中会直接溢出,结果变成 inf,负方向的 e^-1000 则变成 0,整条计算链都会污染成 NaN。
解决办法一句话:先减去最大值。因为 Softmax 对输入同时平移一个常数不改变结果:
e^(z_i - m) / sum_j e^(z_j - m),其中 m = max_j z_j
这个恒等式非常直观,平移之后所有指数幂里的底数最大就是 e^0=1,指数计算绝对不会溢出。所有主流深度学习框架的 Softmax 实现都做了这一步。如果你手写多分类训练代码,一定要记住这个技巧。我见过不少人在自研框架里因为没做减最大值处理,训练到中期 loss 直接变成 NaN,浪费了一整天排查时间。
5.4 导数实现容易出错的几个边界
导数实现出错的地方通常都很固定。第一,ReLU 在 x=0 处的导数取 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 曲线和验证集指标。这个操作能最直观地看出不同激活函数的差异,也方便自己形成经验。就我的经验来说,与其花大量时间调整复杂的网络结构,不如先检查组里有没有用错激活函数,很多时候性能瓶颈的根源就藏在这一行简单的公式里。