摘要: 激活函数是深度神经网络中最核心的组件之一,它为网络引入非线性表达能力,使得堆叠多层神经元成为可能。本文系统梳理了深度学习中常用的激活函数,包括 Sigmoid、Tanh、ReLU、Leaky ReLU、ELU、SeLU、Swish、Mish 以及 Softmax 等,从数学公式、输出特性、梯度分析、优缺点对比四个维度展开深入剖析,并结合 NumPy 实现完整的代码示例与可视化对比,最后给出针对不同场景的激活函数选择指南,助力读者在实战中做出合理决策。
关键词: 激活函数、深度学习、ReLU、梯度消失、非线性
1. 激活函数的作用
1.1 为什么要非线性激活
神经网络之所以能够拟合任意复杂的非线性函数,核心原因在于激活函数的非线性。假设网络中有两层线性变换:
y = W2 · (W1 · x + b1) + b2 = (W2 · W1) · x + (W2 · b1 + b2)
无论我们堆叠多少层线性变换,它们的嵌套结果仍然只是一个线性变换------整个网络等价于一个单层线性模型,根本无法捕捉变量之间的非线性关系。激活函数的出现打破了这个困局:每层线性变换之后,激活函数以非线性方式对结果进行"扭曲",使得网络的表示能力呈指数级增长。Universal Approximation Theorem(通用逼近定理)告诉我们,只要网络中包含足够多的隐藏单元,单层前馈网络即可逼近任意连续函数;而多层的组合则进一步大幅降低了所需的参数量和计算复杂度。
1.2 线性激活的问题
如果执意使用线性激活函数(即令激活值直接等于输入),会产生以下问题:
-
表达能力退化为线性:无论网络多深,信息流始终是线性的组合,无法拟合阶跃、曲线、周期性等非线性模式。
-
梯度在链式求导中快速衰减(浅层网络)或爆炸(权重过大):线性函数的导数是常数,连乘后数值极不稳定。
-
无法构建"特征交叉":深度学习的强大之处在于隐式地自动进行特征交叉组合,而线性激活完全丧失了这一能力。
因此,在隐藏层中使用非线性激活函数是深度学习的基本共识。
2. Sigmoid 函数
2.1 数学公式与输出范围
Sigmoid(亦称 Logistic 函数)的数学表达式为:
\\sigma(x) = \\frac{1}{1 + e\^{-x}}
其输出值域为 (0, 1),曲线呈 S 形,关于点 (0, 0.5) 中心对称。
2.2 梯度特性
Sigmoid 的导数可以由自身表示,形式优雅:
\\sigma'(x) = \\sigma(x) \\cdot (1 - \\sigma(x))
这意味着在反向传播中,计算梯度只需用到当前层的激活值,无需额外存储。
2.3 核心优点
-
平滑可导:函数在整个实数域上无穷阶可导,适合梯度下降。
-
概率解释:输出天然位于 (0, 1),在二分类任务的输出层可以直接解释为"属于正类的概率"。
2.4 三大缺陷
| 缺陷 | 说明 |
|---|---|
| 梯度消失 | 当 $ |
| 计算开销大 | 涉及指数运算 e\^{-x},在硬件层面不如加减法和比较运算高效。 |
| 输出非零中心 | 输出始终为正数((0,1)),导致后一层的输入偏置同号。在梯度反向传播时,同号的梯度信号会让权重更新方向始终在第一、三象限摆动,减缓收敛速度。 |
2.5 代码实现
import numpy as np
def sigmoid(x: np.ndarray) -> np.ndarray:
"""
Sigmoid 激活函数
公式: σ(x) = 1 / (1 + exp(-x))
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的输出,值域 (0, 1)
"""
# 限制输入范围以避免 exp 溢出
x_clipped = np.clip(x, -500, 500)
return 1.0 / (1.0 + np.exp(-x_clipped))
def sigmoid_derivative(x: np.ndarray) -> np.ndarray:
"""
Sigmoid 函数的梯度
公式: σ'(x) = σ(x) * (1 - σ(x))
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的梯度数组
"""
s = sigmoid(x)
return s * (1.0 - s)
# ------------------- 示例 -------------------
if __name__ == "__main__":
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 400)
y = sigmoid(x)
dy = sigmoid_derivative(x)
plt.figure(figsize=(8, 4))
plt.plot(x, y, label="Sigmoid")
plt.plot(x, dy, label="导数", linestyle="--")
plt.axhline(0, color="gray", linewidth=0.5)
plt.axvline(0, color="gray", linewidth=0.5)
plt.title("Sigmoid 函数及其导数")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
3. Tanh 函数
3.1 数学公式与输出范围
Tanh(双曲正切)函数的表达式为:
\\tanh(x) = \\frac{e\^x - e\^{-x}}{e\^x + e\^{-x}}
其值域为 (-1, 1),零中心(zero-centered),关于原点对称。
数学关系 :
tanh(x) = 2σ(2x) - 1,即 Tanh 本质上是 Sigmoid 的一个线性变换版本。
3.2 零中心的优势
由于输出范围对称分布在负侧和正侧,梯度的正负号可以完整保留前一层的信号方向。这使得 Tanh 在实际训练中通常比 Sigmoid 收敛更快,是早期 RNN 和 NLP 任务中的常用选择。
3.3 仍存在的梯度消失
Tanh 的导数为:
\\tanh'(x) = 1 - \\tanh\^2(x)
当 \|x\| 较大时,\\tanh(x) \\to \\pm 1,导数趋于 0。深层网络中同样面临梯度消失问题,只是缓解程度优于 Sigmoid。
3.4 代码实现
import numpy as np
def tanh_func(x: np.ndarray) -> np.ndarray:
"""
Tanh(双曲正切)激活函数
公式: tanh(x) = (exp(x) - exp(-x)) / (exp(x) + exp(-x))
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的输出,值域 (-1, 1)
"""
# 稳定计算:避免大值时 exp 溢出
return np.tanh(x)
def tanh_derivative(x: np.ndarray) -> np.ndarray:
"""
Tanh 函数的梯度
公式: tanh'(x) = 1 - tanh²(x)
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的梯度数组
"""
return 1.0 - np.tanh(x) ** 2
# ------------------- 示例 -------------------
if __name__ == "__main__":
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 400)
y = tanh_func(x)
dy = tanh_derivative(x)
plt.figure(figsize=(8, 4))
plt.plot(x, y, label="Tanh")
plt.plot(x, dy, label="导数", linestyle="--")
plt.axhline(0, color="gray", linewidth=0.5)
plt.axvline(0, color="gray", linewidth=0.5)
plt.title("Tanh 函数及其导数")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
4. ReLU 函数
4.1 数学公式
ReLU(Rectified Linear Unit,线性整流单元)的表达式极为简洁:
f(x) = \\max(0, x) = \\begin{cases} 0 \& x \< 0 \\ x \& x \\ge 0 \\end{cases}
4.2 核心优势
| 优势 | 说明 |
|---|---|
| 计算极快 | 只需一次比较运算,无需指数操作,比 Sigmoid/Tanh 快数倍。 |
| 缓解梯度消失 | 正区间梯度恒为 1,反向传播时梯度稳定传递,深层网络也能正常训练。 |
| 稀疏激活性 | 负值输出为 0,使得部分神经元处于"关闭"状态,形成稀疏表示,具有一定正则化效果。 |
4.3 神经元死亡问题(Dying ReLU)
当大量输入持续为负时,对应的权重和偏置在反向传播中梯度始终为 0,参数永远无法更新,这些神经元永久"死亡"。在实际应用中,当学习率过大或权重初始化不当时,这一问题尤为突出。
4.4 代码实现
import numpy as np
def relu(x: np.ndarray) -> np.ndarray:
"""
ReLU(线性整流)激活函数
公式: f(x) = max(0, x)
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的输出,值域 [0, +∞)
"""
return np.maximum(0, x)
def relu_derivative(x: np.ndarray) -> np.ndarray:
"""
ReLU 函数的梯度
公式: f'(x) = 1 if x >= 0 else 0
参数:
x: 任意形状的输入数组
返回:
与输入形状相同的梯度数组,元素为 0 或 1
"""
return np.where(x >= 0, 1.0, 0.0)
# ------------------- 示例 -------------------
if __name__ == "__main__":
import matplotlib.pyplot as plt
x = np.linspace(-10, 10, 400)
y = relu(x)
dy = relu_derivative(x)
plt.figure(figsize=(8, 4))
plt.plot(x, y, label="ReLU")
plt.plot(x, dy, label="导数", linestyle="--")
plt.axhline(0, color="gray", linewidth=0.5)
plt.axvline(0, color="gray", linewidth=0.5)
plt.title("ReLU 函数及其导数")
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
5. Leaky ReLU
5.1 数学公式
Leaky ReLU 在 ReLU 的负区间引入一个极小的斜率 \\alpha(通常取 0.01),避免神经元完全死亡:
f(x) = \\max(\\alpha x, x) = \\begin{cases} \\alpha x \& x \< 0 \\ x \& x \\ge 0 \\end{cases}
5.2 解决神经元死亡
由于负区间有了一个很小的非零梯度,反向传播时即使输入长期为负,权重仍有微小的更新机会,从根本上规避了"Dying ReLU"问题。
5.3 代码实现
import numpy as np
def leaky_relu(x: np.ndarray, alpha: float = 0.01) -> np.ndarray:
"""
Leaky ReLU 激活函数
公式: f(x) = max(alpha * x, x),通常 alpha = 0.01
参数:
x: 任意形状的输入数组
alpha: 负区间的斜率,默认 0.01
返回:
与输入形状相同的输出
"""
return np.where(x >= 0, x, alpha * x)
def leaky_relu_derivative(x: np.ndarray, alpha: float = 0.01) -> np.ndarray:
"""
Leaky ReLU 的梯度
参数:
x: 任意形状的输入数组
alpha: 负区间的斜率,默认 0.01
返回:
与输入形状相同的梯度数组
"""
return np.where(x >= 0, 1.0, alpha)
6. 其他常用激活函数
6.1 ELU(Exponential Linear Unit)
ELU 在负区间使用指数函数,输出接近零均值,同时在负区间有一定的"软饱和"特性,有助于网络学习更鲁棒的表示:
f(x) = \\begin{cases} \\alpha (e\^x - 1) \& x \< 0 \\ x \& x \\ge 0 \\end{cases}
def elu(x: np.ndarray, alpha: float = 1.0) -> np.ndarray:
return np.where(x >= 0, x, alpha * (np.exp(np.clip(x, -500, 500)) - 1))
def elu_derivative(x: np.ndarray, alpha: float = 1.0) -> np.ndarray:
return np.where(x >= 0, 1.0, elu(x, alpha) + alpha)
6.2 SeLU(Scaled Exponential Linear Unit)
SeLU 由 Klambauer 等人在 2017 年提出,其自归一化特性使得网络在不使用 Batch Normalization 的情况下也能保持稳定的方差传播:
f(x) = \\lambda \\cdot \\text{ELU}(x), \\quad \\lambda \\approx 1.0507
def selu(x: np.ndarray) -> np.ndarray:
"""SeLU 激活函数,lambda ≈ 1.0507"""
ALPHA = 1.6732632423543772848170429916717
LAMBDA = 1.0507009873554804934193349852946
return LAMBDA * np.where(x >= 0, x, ALPHA * (np.exp(np.clip(x, -500, 500)) - 1))
6.3 Swish
Swish 由 Google Brain 在 2017 年提出,自门控机制(self-gated)使其具有平滑非单调特性,在许多任务上优于 ReLU:
f(x) = x \\cdot \\sigma(x) = \\frac{x}{1 + e\^{-x}}
def swish(x: np.ndarray) -> np.ndarray:
"""
Swish 激活函数
公式: f(x) = x * sigmoid(x)
"""
return x * sigmoid(x)
def swish_derivative(x: np.ndarray) -> np.ndarray:
"""
Swish 的梯度(使用链式法则推导)
f'(x) = sigmoid(x) + x * sigmoid(x) * (1 - sigmoid(x))
= sigmoid(x) + x * sigmoid(x) * (1 - sigmoid(x))
"""
s = sigmoid(x)
return s + x * s * (1 - s)
6.4 Mish
Mish 由 Diganta Misra 在 2019 年提出,同样采用自门控设计,在 ImageNet 分类等任务上取得了优于 ReLU 和 Swish 的效果:
f(x) = x \\cdot \\tanh(\\ln(1 + e\^x))
def mish(x: np.ndarray) -> np.ndarray:
"""
Mish 激活函数
公式: f(x) = x * tanh(softplus(x)),其中 softplus(x) = ln(1 + e^x)
"""
return x * np.tanh(np.log1p(np.exp(np.clip(x, -500, 500))))
6.5 Softmax(含温度参数)
Softmax 是多分类任务输出层的标准激活函数,将任意实数向量转换为概率分布:
\[\\text{Softmax}(x)\]*i = \\frac{e\^{x_i}}{\\sum*{j=1}\^{N} e\^{x_j}}
温度参数(Temperature)是 Softmax 的重要扩展。引入温度 T 后:
\[\\text{Softmax}*T(x)\]*i = \\frac{e\^{x_i / T}}{\\sum_{j=1}\^{N} e\^{x_j / T}}
-
T \\to 0:输出趋近于 one-hot(最max的项概率趋近于 1),即"硬"决策。
-
T = 1:标准 Softmax。
-
T \\to +\\infty:输出趋近于均匀分布(完全随机)。
在蒸馏学习(Knowledge Distillation)和强化学习策略网络中,温度参数是控制探索与利用平衡的关键超参数。
def softmax(x: np.ndarray, axis: int = -1, temperature: float = 1.0) -> np.ndarray:
"""
Softmax 激活函数(支持温度参数)
公式: softmax(x)_i = exp(x_i / T) / Σ_j exp(x_j / T)
参数:
x: 输入 logits,任意形状
axis: 计算 Softmax 的轴(默认为最后一维)
temperature: 温度参数 T,T 越大输出越平滑(趋近均匀)
返回:
与输入形状相同的概率分布,每行/列和为 1
"""
# 减去最大值以提升数值稳定性(不改变 Softmax 结果)
x_scaled = x / temperature
x_max = np.max(x_scaled, axis=axis, keepdims=True)
exp_x = np.exp(x_scaled - x_max) # 数值稳定的写法
return exp_x / np.sum(exp_x, axis=axis, keepdims=True)
def softmax_derivative(x: np.ndarray, temperature: float = 1.0) -> np.ndarray:
"""
Softmax 的雅可比矩阵(用于理解梯度流动)
对于第 i 个输出关于第 j 个输入:
d(softmax_i) / d(x_j) = softmax_i * (δ_ij - softmax_j)
注意:实际反向传播中通常直接使用 cross-entropy + softmax 的组合梯度,
该梯度简化为 (softmax(x) - y),无需显式计算雅可比矩阵。
"""
s = softmax(x, temperature=temperature)
# 构造雅可比矩阵(仅作演示,对大维度矩阵开销较大)
# jacobian[i, j] = s[i] * (δ[i,j] - s[j])
I = np.eye(x.shape[-1])
s_expanded = s[..., np.newaxis]
jacobian = s_expanded * (I - s_expanded.swapaxes(-1, -2))
return jacobian
7. 激活函数选择指南
7.1 按层类型选择
| 层级 | 推荐激活函数 | 原因 |
|---|---|---|
| 二分类输出层 | Sigmoid | 输出值域 (0,1),直接作为概率解释 |
| 多分类输出层 | Softmax | 输出归一化为概率分布,各类别互斥 |
| 回归输出层 | 恒等函数(无激活)/ ReLU(正值输出) | 目标值无界时直接线性输出 |
| 隐藏层(默认首选) | ReLU | 计算高效、梯度稳定,是工业界事实标准 |
| 隐藏层(需要更高精度) | Leaky ReLU / ELU | 避免神经元死亡,输出更接近零中心 |
| 隐藏层(追求SOTA性能) | Mish / Swish | 自门控机制带来更强的非线性表达能力 |
| 自归一化网络 | SeLU | 配合 AlphaDropout 可无需 BatchNorm |
7.2 按网络架构选择
-
MLP(全连接网络):隐藏层用 ReLU,输出层根据任务选择 Sigmoid/Softmax/恒等。
-
CNN(卷积网络):ReLU 系列为主(ReLU → Leaky ReLU → Mish 逐步升级)。
-
RNN/LSTM/GRU:Tanh 仍是主流(门控机制需要零中心输出),输出层可用 Sigmoid。
-
Transformer/Attention :GELU(近似实现为
0.5 * x * (1 + tanh(sqrt(2/pi) * (x + 0.044715*x^3))))已成为事实标准。
7.3 实战建议
-
从 ReLU 开始:大多数场景先用 ReLU 建立基线,简单高效。
-
警惕神经元死亡:训练 loss 不下降且梯度在浅层几乎为零时,换 Leaky ReLU 或 ELU。
-
学习率匹配 :ReLU 网络对学习率较敏感,建议配合 He 初始化(
fan_in方差)使用。 -
BatchNorm + 激活函数的顺序 :标准实践是
Linear → BatchNorm → Activation,注意卷积网络中的 Channel 维度归一化。 -
不要在输出层使用 ReLU/Sigmoid/Softmax 之外的函数------输出层通常需要特定的数值范围或概率语义。
8. 实战代码:各激活函数对比可视化
以下代码在一张图上同时展示所有主流激活函数的曲线及其梯度,方便直观对比:
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# 激活函数定义(汇总)
# ============================================================
def sigmoid(x):
x = np.clip(x, -500, 500)
return 1.0 / (1.0 + np.exp(-x))
def tanh_func(x):
return np.tanh(x)
def relu(x):
return np.maximum(0, x)
def leaky_relu(x, alpha=0.01):
return np.where(x >= 0, x, alpha * x)
def elu(x, alpha=1.0):
return np.where(x >= 0, x, alpha * (np.exp(np.clip(x, -500, 500)) - 1))
def selu(x):
ALPHA = 1.6732632423543772848170429916717
LAMBDA = 1.0507009873554804934193349852946
return LAMBDA * np.where(x >= 0, x, ALPHA * (np.exp(np.clip(x, -500, 500)) - 1))
def swish(x):
return x * sigmoid(x)
def mish(x):
return x * np.tanh(np.log1p(np.exp(np.clip(x, -500, 500)))))
def gelu(x):
"""GELU(近似公式),Transformer 架构常用"""
return 0.5 * x * (1 + np.tanh(np.sqrt(2 / np.pi) * (x + 0.044715 * x**3)))
# ============================================================
# 梯度定义
# ============================================================
def get_gradients(activations, x):
"""利用数值微分计算各激活函数的梯度(用于绘图)"""
eps = 1e-7
return {name: (激活(x + eps) - 激活(x - eps)) / (2 * eps)
for name, 激活 in activations.items()}
# ============================================================
# 绘图
# ============================================================
x = np.linspace(-4, 4, 500)
activations = {
"Sigmoid": sigmoid,
"Tanh": tanh_func,
"ReLU": relu,
"Leaky ReLU": leaky_relu,
"ELU": elu,
"SeLU": selu,
"Swish": swish,
"Mish": mish,
"GELU": gelu,
}
fig, axes = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
# 上图:激活函数曲线
ax1 = axes[0]
colors = plt.cm.tab10(np.linspace(0, 1, len(activations)))
for (name, func), color in zip(activations.items(), colors):
ax1.plot(x, func(x), label=name, color=color, linewidth=2)
ax1.axhline(0, color="black", linewidth=0.8, linestyle="-")
ax1.axvline(0, color="black", linewidth=0.8, linestyle="-")
ax1.set_title("常用激活函数对比", fontsize=14)
ax1.set_ylabel("Activation Output", fontsize=11)
ax1.legend(loc="lower right", ncol=3, fontsize=9)
ax1.grid(True, alpha=0.3)
ax1.set_ylim(-2.5, 3)
# 下图:梯度曲线
ax2 = axes[1]
grads = get_gradients(activations, x)
for (name, _), color in zip(activations.items(), colors):
ax2.plot(x, grads[name], label=name, color=color, linewidth=2)
ax2.axhline(0, color="black", linewidth=0.8, linestyle="-")
ax2.axvline(0, color="black", linewidth=0.8, linestyle="-")
ax2.set_title("激活函数梯度(导数)对比", fontsize=14)
ax2.set_xlabel("x", fontsize=11)
ax2.set_ylabel("Gradient (dy/dx)", fontsize=11)
ax2.legend(loc="upper right", ncol=3, fontsize=9)
ax2.grid(True, alpha=0.3)
ax2.set_ylim(-0.5, 1.5)
plt.tight_layout()
plt.savefig("activation_functions_comparison.png", dpi=150)
plt.show()
print("图表已保存至 activation_functions_comparison.png")
运行说明 :将上述代码保存为
.py文件(如plot_activations.py),确保已安装numpy和matplotlib(pip install numpy matplotlib),直接运行即可生成对比图。
9. 总结
激活函数虽小,却是深度学习网络的"灵魂"所在。本文系统梳理了从经典 Sigmoid/Tanh 到现代 ReLU 家族以及 Swish/Mish 等自适应激活函数的数学原理与实现细节,并给出了按场景分类的选择建议。核心要点回顾:
-
隐藏层优先选 ReLU,快速建立基线,再根据 Dying ReLU 问题考虑 Leaky ReLU/ELU。
-
输出层根据任务选择:二分类用 Sigmoid,多分类用 Softmax,回归用恒等函数。
-
前沿网络(Transformer/GNN)推荐 GELU,追求更高精度可尝试 Mish/Swish。
-
数值稳定性不可忽视:所有涉及指数运算的激活函数(Sigmoid、ELU、Swish、Mish)都应做好输入裁剪,避免溢出。
-
激活函数 + BatchNorm 的顺序 应严格遵循
Linear → BatchNorm → Activation,否则效果会大打折扣。
掌握激活函数的原理与选型逻辑,是构建高效深度学习模型的必经之路。希望本文能为你提供一份完整、实用的参考指南。