1. 为什么需要激活函数?

在讲解 ReLU 之前,我们首先要明白为什么神经网络需要激活函数。

一个典型的神经网络层通常执行一个线性操作,例如 output = weight * input + bias。如果我们堆叠多个这样的线性层,而不使用任何激活函数,会发生什么?

假设我们有两层网络:

  • h = W1 * x + b1
  • y = W2 * h + b2

将 h 代入第二个方程:
y = W2 * (W1 * x + b1) + b2 = (W2 * W1) * x + (W2 * b1 + b2)

令 W_new = W2 * W1 和 b_new = W2 * b1 + b2,那么整个两层网络就等价于一个新的单层网络:y = W_new * x + b_new

结论:如果只使用线性层,无论堆叠多少层,整个网络本质上仍然是一个线性模型。这样的模型无法学习和拟合复杂的非线性关系,其表达能力非常有限。

激活函数的作用:在网络中引入非线性(Non-linearity)。它被应用在线性层的输出上,使得神经网络能够学习和逼近几乎任何复杂的函数,从而极大地增强了模型的表达能力。

2. 什么是 ReLU?

ReLU 的全称是 Rectified Linear Unit(修正线性单元)。它是目前深度学习领域中最常用、最流行的激活函数之一。

数学定义
ReLU 函数的定义非常简单:
f(x) = max(0, x)

这意味着:

  • 如果输入 x 大于 0,则输出就是 x 本身。
  • 如果输入 x 小于或等于 0,则输出为 0。

图形表示
ReLU 的函数图像非常直观:

  • 在 x<0 的部分,它是一条在 y=0 上的水平线。
  • 在 x>0 的部分,它是一条 45 度的斜线(y=x)。
      ^ y
      |     /
      |    /
      |   /
      |  /
------|----------------> x
      |
3. ReLU 的优点:为什么它如此受欢迎?

相比于传统的激活函数,如 Sigmoid 和 Tanh,ReLU 有几个显著的优势:

  1. 解决了梯度消失问题(Vanishing Gradient Problem)

    • Sigmoid/Tanh 的问题:这两个函数的导数(梯度)在输入值的绝对值很大时会趋近于 0。在深度网络中,反向传播时梯度需要连乘,多个接近 0 的数相乘会导致最终的梯度变得极其微小,使得网络深层的参数几乎无法更新。这就是梯度消失。
    • ReLU 的优势:对于所有正数输入,ReLU 的导数恒为 1。这意味着在反向传播过程中,只要神经元的输入为正,梯度就可以无衰减地传递下去,极大地缓解了梯度消失问题,使得训练更深的网络成为可能。
  2. 计算效率高

    • ReLU 的计算非常简单,只需要一个 max(0, x) 的判断,不涉及复杂的指数运算(如 Sigmoid 的 e^-x)。这使得无论是在前向传播还是反向传播中,它的计算速度都非常快。
  3. 促进稀疏性(Sparsity)

    • ReLU 会将所有负数输入置为 0。这意味着在任何时刻,网络中只有一部分神经元是“激活”的(输出非零值)。这种稀疏性使得网络的计算和表示在某些方面更有效率。
4. 如何在 PyTorch 中使用 ReLU?

在 PyTorch 中,使用 ReLU 有两种主要方式:模块化接口(torch.nn.ReLU)和函数式接口(torch.nn.functional.relu)。

方法一:使用 torch.nn.ReLU(推荐用于定义模型结构)

这是一种面向对象的方式,将 ReLU 作为一个网络层来使用。它非常适合在 nn.Sequential 或自定义的 nn.Module 中定义网络结构。

import torch
import torch.nn as nn
# 示例 1: 在 nn.Sequential 中使用
model = nn.Sequential(
    nn.Linear(10, 20),   # 输入维度10,输出维度20
    nn.ReLU(),           # 应用 ReLU 激活函数
    nn.Linear(20, 5),    # 输入维度20,输出维度5
    nn.ReLU()            # 再次应用 ReLU
)
# 示例 2: 在自定义 Module 中使用
class MyModel(nn.Module):
    def __init__(self):
        super(MyModel, self).__init__()
        self.fc1 = nn.Linear(10, 20)
        self.relu1 = nn.ReLU() # 将 ReLU 定义为一个层
        self.fc2 = nn.Linear(20, 5)
    def forward(self, x):
        x = self.fc1(x)
        x = self.relu1(x) # 调用定义好的层
        x = self.fc2(x)
        # 注意:最后一层通常不加 ReLU,除非有特殊需求
        return x
# 查看模型结构
my_model = MyModel()
print(my_model)

输出:

MyModel(
  (fc1): Linear(in_features=10, out_features=20, bias=True)
  (relu1): ReLU()
  (fc2): Linear(in_features=20, out_features=5, bias=True)
)

inplace 参数
nn.ReLU(inplace=True) 是一个常见的优化。

  • inplace=False(默认):y = relu(x) 会创建一个新的张量 y 来存储结果,x 保持不变。
  • inplace=True:直接在输入张量 x 的内存上进行计算和覆盖,不创建新的张量。
  • 优点:节省内存,对于处理大尺寸输入(如图像)时很有用。
  • 缺点:会覆盖原始输入,如果你在计算图的别处还需要原始的、未经过 ReLU 的输出,使用 inplace=True 会导致错误。对于初学者,建议保持默认的 False

方法二:使用 torch.nn.functional.relu(推荐用于 forward 方法中)

这是一种函数式的方式,更灵活。它不需要先实例化一个层。通常在自定义 Module 的 forward 方法中直接调用。

import torch.nn.functional as F
class MyFunctionalModel(nn.Module):
    def __init__(self):
        super(MyFunctionalModel, self).__init__()
        self.fc1 = nn.Linear(10, 20)
        self.fc2 = nn.Linear(20, 5)
    def forward(self, x):
        # 直接调用 F.relu 函数,代码更简洁
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x
my_func_model = MyFunctionalModel()
print(my_func_model) # 注意,打印结构时看不到 ReLU,因为它只是一个函数调用

nn.ReLU vs. F.relu

  • nn.ReLU 是一个有状态的 nn.Module。虽然 ReLU 本身没有可学习的参数,但它作为一个模块可以被 PyTorch 的工具(如 nn.Sequential)识别和管理。
  • F.relu 是一个无状态的函数。它更直接,代码写起来可能更简洁。
  • 如何选择? 两种方式在功能上没有区别。通常的惯例是:在 __init__ 中定义那些有可学习参数的层(如 LinearConv2d),在 forward 中可以使用函数式接口(如 F.reluF.dropout)来串联它们。如果你想用 nn.Sequential 快速搭建,那么 nn.ReLU 是必须的。
5. ReLU 的缺点和变体

尽管 ReLU 非常强大,但它也有一个著名的问题:

Dying ReLU Problem(死亡 ReLU 问题)
如果在训练过程中,一个神经元的输入持续为负数,那么它的输出将永远是 0。更重要的是,它的梯度也将永远是 0。这意味着这个神经元的权重将永远不会得到更新。这个神经元就“死亡”了,对整个网络不再有任何贡献。这个问题通常在学习率设置过大时更容易出现。

为了解决这个问题,研究者们提出了一系列 ReLU 的变体。

1. Leaky ReLU

  • 定义f(x) = max(αx, x),其中 α 是一个很小的正常数(如 0.01)。
  • 思想:当输入为负数时,不再输出 0,而是输出一个非常小的负值(αx)。这保证了即使在输入为负时,梯度也不会为 0,从而避免了神经元死亡。
  • PyTorch 实现nn.LeakyReLU(negative_slope=0.01) 或 F.leaky_relu(x, negative_slope=0.01)

2. Parametric ReLU (PReLU)

  • 定义:与 Leaky ReLU 相同,但 α 不再是固定的超参数,而是一个可学习的参数
  • 思想:让网络自己学习在负数区间的最佳斜率 α
  • PyTorch 实现nn.PReLU()。注意,PReLU 会为每个输入通道学习一个 α,因此它会给模型增加少量参数。

3. Exponential Linear Unit (ELU)

  • 定义
    • f(x) = x 如果 x > 0
    • f(x) = α(exp(x) - 1) 如果 x <= 0
  • 思想:融合了 Sigmoid 和 ReLU 的优点。负数区间的输出是负值,可以使神经元的平均激活值更接近 0,这被认为有助于加速学习。同时,它也解决了 Dying ReLU 问题。缺点是计算量比 ReLU 大。
  • PyTorch 实现nn.ELU(alpha=1.0) 或 F.elu(x, alpha=1.0)
总结和选择建议
激活函数定义优点缺点/注意事项
ReLUmax(0, x)计算快,缓解梯度消失可能导致神经元死亡(Dying ReLU)
Leaky ReLUmax(αx, x)解决了 Dying ReLU,计算仍然很快α 需要手动设置,效果不一定总比 ReLU 好
PReLULeaky ReLU 且 α 可学习理论上比 Leaky ReLU 更优,自适应斜率增加模型参数,有轻微过拟合风险
ELUx if x>0α(exp(x)-1) if x<=0Leaky ReLU 的所有优点,且输出均值接近0计算密集

实践建议

  1. 首选 ReLU:它简单、快速,并且在绝大多数情况下表现优异。作为你的默认选择。
  2. 观察 “Dying ReLU”:如果在训练中发现损失不再下降,或者网络性能不佳,可以考虑换用 Leaky ReLU 或 ELU。
  3. 尝试 Leaky ReLU:这是一个安全且有效的替代方案,通常能带来轻微的性能提升或解决神经元死亡问题。
  4. 使用 ELU/PReLU:如果计算资源充足,并且你正在追求极致的性能,可以尝试 ELU 或 PReLU。PReLU 在图像任务中表现尤为出色。
---

相关课程

学习不止于此,推荐继续深入:

  1. 机器学习40讲系统学习机器学习核心算法

☁️ 云服务推荐