在本篇文章中,我们将深入探讨 PyTorch 中的激活函数。无论你是刚刚踏入深度学习大门的新人,还是希望夯实基础的开发者,理解激活函数的工作原理对于构建高效、准确的神经网络至关重要。我们将不仅学习“是什么”,更会深入理解“为什么”以及“怎么做”,通过实际的代码示例,带你领略这些函数背后的数学直觉与工程智慧。
目录
什么是激活函数,为什么要使用它们?
在开始编写代码之前,让我们先退后一步,思考一下人工神经网络的本质。你可以把神经网络想象成是一个巨大的、复杂的数学函数,试图将输入(比如一张图片的像素)映射到输出(比如“这是猫”的概率)。
如果我们在网络中只使用线性变换(即矩阵乘法和加法),那么无论网络有多少层,它最终本质上还是一个线性模型。这意味着它无法解决像异或(XOR)这样的简单非线性问题,更不用说识别图像中的复杂特征了。
这就是激活函数发挥作用的地方。
为什么我们需要它们?
- 引入非线性:激活函数是神经网络能够学习复杂模式和复杂决策边界的关键。它们就像是神经元的“开关”,决定是否应该将信号传递给下一层。没有它们,深度学习网络就只是一个简单的回归模型。
- 模拟生物神经元:虽然人工神经网络是对人脑的简化模拟,但激活函数模仿了生物神经元只有当电位超过阈值时才会放电(激活)的特性。
- 归一化输出:某些激活函数(如 Sigmoid 或 Softmax)可以将输出限制在特定范围内(如 0 到 1),这对于表示概率非常有用。
所有的激活函数都包含在 torch.nn 模块中,我们可以方便地调用它们。现在,让我们通过代码来深入了解 PyTorch 中最常用的几种激活函数。
1. ReLU 激活函数
ReLU 代表修正线性单元。它是目前深度学习中最流行、最常用的激活函数。它的数学定义非常简单:
$$f(x) = \max(0, x)$$
也就是说,如果输入 $x$ 大于 0,输出就是 $x$;如果 $x$ 小于等于 0,输出就是 0。
为什么选择 ReLU?
相比于传统的 Sigmoid 或 Tanh 函数,ReLU 有几个显著的优势:
- 计算效率高:只需要判断是否大于 0,没有复杂的指数运算。
- 缓解梯度消失问题:在正区间内,导数恒为 1,这使得梯度在反向传播时能够顺畅地流过深层网络,不会像 Sigmoid 那样迅速衰减。
- 稀疏性:由于负数输出被置为 0,这意味着在同一时间,只有一部分神经元被激活,这种稀疏性使得模型更具鲁棒性。
潜在问题:神经元“死亡”
ReLU 并不是完美的。如果输入总是负数,梯度将变为 0,权重将不再更新。这种现象被称为“Dead ReLU”或神经元死亡。一旦神经元死亡,它就可能永久无法恢复对数据的响应。
实战演示
让我们在 PyTorch 中实现 ReLU,并观察它如何处理包含负值的张量。
import torch
import torch.nn as nn
# 定义 ReLU 激活函数实例
# 这里的 inplace=False 表示不修改输入张量,而是返回一个新的张量
relu = nn.ReLU(inplace=False)
# 创建一个包含正数和负数的输入张量
input_tensor = torch.tensor([1.0, -2.0, 3.0, -5.0, 0.0])
print(f"输入张量: {input_tensor}")
# 将张量传递给 ReLU 函数
output_tensor = relu(input_tensor)
# 打印输出结果
print(f"ReLU 输出: {output_tensor}")
# 你可以尝试检查梯度(在训练模式下)
# 如果 x 0,导数为 1
输出:
输入张量: tensor([ 1., -2., 3., -5., 0.])
ReLU 输出: tensor([1., 0., 3., 0., 0.])
2. Leaky ReLU 激活函数
为了解决标准 ReLU 中神经元“死亡”的问题,Leaky ReLU 被提出。
它是如何工作的?
Leaky ReLU 允许在输入为负时有一个很小的非零梯度。通常我们把这个小的斜率称为 $\alpha$(例如 0.01 或 0.2)。
$$f(x) = \max(\alpha x, x)$$
这意味着,当 $x < 0$ 时,函数不再是硬 0,而是一个微小的线性值。这确保了即使在负区域,神经元也能继续学习,不会完全“死掉”。
实战演示
让我们看看如何设置负斜率,并观察其对输出的影响。
import torch
import torch.nn as nn
# 定义 Leaky ReLU
# 这里的 0.2 就是负半轴的斜率
# 我们可以称之为 alpha,它控制负值信息的保留程度
leaky_relu = nn.LeakyReLU(negative_slope=0.2)
input_tensor = torch.tensor([1.0, -2.0, 3.0, -5.0])
print(f"输入: {input_tensor}")
# 应用 Leaky ReLU
output_tensor = leaky_relu(input_tensor)
print(f"Leaky ReLU 输出: {output_tensor}")
# 计算说明:
# -2.0 * 0.2 = -0.4
# -5.0 * 0.2 = -1.0
输出:
输入: tensor([ 1., -2., 3., -5.])
Leaky ReLU 输出: tensor([ 1.0000, -0.4000, 3.0000, -1.0000])
最佳实践建议:如果你发现网络中有很多神经元不再更新(这在学习率很高时很常见),尝试将 ReLU 替换为 Leaky ReLU 往往能带来性能提升。
3. Sigmoid 激活函数
Sigmoid 是深度学习早期的“元老”级激活函数。它将任何实数输入映射到 $(0, 1)$ 的区间内。
$$f(x) = \frac{1}{1 + e^{-x}}$$
特点与应用
- 概率解释:由于输出范围在 0 到 1 之间,它非常适合用来表示概率。例如,在二分类问题中,输出层通常使用 Sigmoid,输出值直接代表“正类”的概率。
- 平滑性:它是一个可微的平滑函数,这在数学优化上很优雅。
那个著名的“坑”:梯度消失
虽然 Sigmoid 看起来很完美,但在深层网络中它有一个致命弱点。当输入非常大或非常小时,Sigmoid 曲线变得非常平缓,导数趋近于 0。
在反向传播时,根据链式法则,如果这一层的导数接近 0,那么传到前面层的梯度就会变得极小,导致浅层网络的权重几乎无法更新。这就是梯度消失问题。如果你使用的是非常深的网络,通常要避免在隐藏层使用 Sigmoid。
实战演示
import torch
import torch.nn as nn
# 定义 Sigmoid 函数
sigmoid = nn.Sigmoid()
input_tensor = torch.tensor([1.0, -2.0, 3.0, -5.0])
# 应用 Sigmoid
output_tensor = sigmoid(input_tensor)
print("Sigmoid 输出:")
print(output_tensor)
# 注意观察:
# -5 是一个很大的负数,Sigmoid 把它压缩到了接近 0 的位置
# 3 是一个正数,Sigmoid 把它压缩到了接近 1 的位置
# 这种“挤压”效应在深层网络叠加时会导致信息丢失
输出:
Sigmoid 输出:
tensor([0.7311, 0.1192, 0.9526, 0.0067])
4. Tanh 激活函数
Tanh(双曲正切)函数与 Sigmoid 非常相似,也是 S 形曲线,但它的输出范围是 $(-1, 1)$。
$$f(x) = \frac{e^x – e^{-x}}{e^x + e^{-x}}$$
为什么要在意 Tanh?
Tanh 的输出是以 0 为中心的。相比之下,Sigmoid 的输出总是正的(0 到 1)。
这一点很重要,因为如果下一层的输入总是正的,那么在权重更新时,梯度要么全正,要么全负,这会导致梯度下降的路径呈现“锯齿状”,收敛速度变慢。Tanh 的零中心特性使得这种“之”字形摆动大大减少,因此在很多循环神经网络(RNN)中,Tanh 往往比 Sigmoid 表现更好。
当然,它依然受困于梯度消失的问题。
实战演示
import torch
import torch.nn as nn
# 定义 Tanh 函数
tanh = nn.Tanh()
input_tensor = torch.tensor([1.0, -2.0, 3.0, -5.0])
# 应用 Tanh
output_tensor = tanh(input_tensor)
print("Tanh 输出:")
print(output_tensor)
# 观察:
# 负数变成了负值,正数变成了正值,区间在 (-1, 1) 之间
# 这种“有正有负”的输出有利于后续层的权重学习
输出:
Tanh 输出:
tensor([ 0.7616, -0.9640, 0.9951, -0.9999])
5. Softmax 激活函数
Softmax 是多分类问题中的“王者”。虽然原文没有详细展开,但我认为你有必要了解它,因为它与 Sigmoid 关系密切,但用途不同。
Sigmoid 用于二分类(一个是,或不是),而 Softmax 用于多分类(是猫、是狗还是鸟)。
核心特性:它不仅对每个输出进行“Sigmoid 式”的压缩,更重要的是,它确保所有输出的概率之和等于 1。
实战演示:多分类场景
假设我们有一个模型,输出了三个类别的原始分数(Logits)。
import torch
import torch.nn as nn
# 定义 Softmax
# dim=1 表示我们希望在每一行内部进行概率归一化
softmax = nn.Softmax(dim=1)
# 模拟一个 batch 的数据,2 个样本,3 个类别
logits = torch.tensor([
[2.0, 1.0, 0.1], # 样本1:倾向于第1类
[0.5, 2.5, 0.3] # 样本2:倾向于第2类
])
probs = softmax(logits)
print("Softmax 概率:")
print(probs)
# 验证每行的和是否为 1
print("
每行概率之和:")
print(probs.sum(dim=1))
输出:
Softmax 概率:
tensor([[0.6590, 0.2424, 0.0986],
[0.1745, 0.7054, 0.1201]])
每行概率之和:
tensor([1.0000, 1.0000])
总结与最佳实践
通过这篇文章,我们深入探讨了 PyTorch 中最常用的几种激活函数。让我们总结一下你在构建网络时的决策指南:
- 首选 ReLU:对于大多数深度神经网络的隐藏层,
nn.ReLU是你的默认选择。它计算快,且能缓解梯度消失。
- 尝试 Leaky ReLU:如果你发现模型训练停滞不前,或者网络很深,
nn.LeakyReLU通常能带来惊喜。
- 输出层的决定:
* 二分类:输出层使用 nn.Sigmoid()。
* 多分类:输出层使用 INLINECODE6694ef3c(或者在 PyTorch 中常用 INLINECODE23ff86e0 直接接受原始 logits,不需要手动加 Softmax)。
* 回归问题(输出数值):通常输出层不需要激活函数,或者使用 Tanh(如果你知道目标范围在 -1 到 1 之间)。
- 关于 Sigmoid 和 Tanh:在现代架构中,它们在隐藏层中使用得越来越少了,主要归因于梯度计算缓慢和梯度消失问题。但在特定的架构(如 LSTM、GRU)内部,Tanh 依然占有一席之地。
性能优化小贴士
- INLINECODE858458b8:在 PyTorch 中定义激活函数时,你可以看到 INLINECODE1548d07c 参数。如果设置为
True,函数会直接修改输入张量的内存,而不是创建一个新的张量并占用显存。这在内存受限时非常有用,但可能会在调试时引起麻烦,因为你会丢失原始输入值。
# 节省显存的写法
relu = nn.ReLU(inplace=True)
希望这篇文章能帮助你更好地理解 PyTorch 中的激活函数。现在,打开你的 Python 编辑器,尝试修改上面的代码,看看这些函数是如何处理不同数据的吧!