nn.ReLU 详解:为什么神经网络离不开它

43 阅读1分钟

没有激活函数的神经网络,不管多深,都只是一条"直线"。

1. 一句话理解 ReLU

ReLU(Rectified Linear Unit,修正线性单元)的数学公式极其简单:

ReLU(x)=max(0,x)\text{ReLU}(x) = \max(0, x)

负数变 0,正数不变。 就这么简单。

输入 x输出 ReLU(x)
-3.00
-0.50
00
0.50.5
3.03.0

2. 为什么需要 ReLU?——引入非线性

这是 ReLU 存在的根本原因

2.1 线性层在做什么

nn.Linear 执行的是线性变换:y=Wx+by = Wx + b

线性变换有一个数学上的铁律:线性变换的组合,仍然是线性变换。

# 两层线性层
y = W₂(W₁x + b₁) + b₂
  = W₂W₁x + W₂b₁ + b₂
  = W'x + b'              # 数学上等价于一层!

不管你堆 2 层、10 层还是 100 层线性层,结果在数学上永远等价于一层。就像你连续旋转、缩放一张照片多次,最终效果总能用"一次旋转 + 一次缩放"替代。

2.2 一层能做什么?——只能画直线

一层线性层只能在空间中画一条直线(或高维超平面)来分类:

  能分开(线性可分)        分不开(线性不可分)

  ●●●●                     ●○●○
  ●●●●                     ○●○●
  ────── ← 一条直线         ●○●○  ← 没有一条直线能分开
  ○○○○                     ○●○●
  ○○○○

而现实中的任务——手写数字识别、图像分类、语音理解——决策边界几乎都是弯曲的。只靠直线,搞不定。

2.3 ReLU 引入非线性,让多层不再等于一层

在两个线性层之间插入 ReLU:

y = W₂ · ReLU(W₁x + b₁) + b₂

由于 ReLU 不是线性函数(它在 x=0x=0 处有一个"折点"),这个组合无法化简为一层。每多一层 Linear + ReLU,网络就多一次"折叠空间"的能力,最终可以拟合任意复杂的曲线。

# 没有 ReLU → 100 层 = 1 层(废了)
nn.Linear(784, 256)
nn.Linear(256, 10)

# 有 ReLU → 每层都有意义
nn.Linear(784, 256)
nn.ReLU()              # ← 非线性,让上下两层不能合并
nn.Linear(256, 10)

一句话:ReLU 的作用是引入非线性,让深度网络真正"深"起来。

3. PyTorch 中的用法

3.1 三种调用方式

import torch
import torch.nn as nn
import torch.nn.functional as F

x = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])

# 方式一:模块式(常用于 nn.Sequential)
relu = nn.ReLU()
print(relu(x))       # tensor([0., 0., 0., 1., 2.])

# 方式二:函数式
print(F.relu(x))     # tensor([0., 0., 0., 1., 2.])

# 方式三:inplace 模式(直接修改原 tensor,省内存)
relu_ip = nn.ReLU(inplace=True)
relu_ip(x)            # x 本身被修改

3.2 在网络中的典型位置

ReLU 放在线性层之后,通常最后一层不加 ReLU(输出层需要原始分数):

class Net(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 256)
        self.fc2 = nn.Linear(256, 128)
        self.fc3 = nn.Linear(128, 10)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.fc1(x))   # Linear → ReLU
        x = self.relu(self.fc2(x))   # Linear → ReLU
        x = self.fc3(x)              # 最后一层不加 ReLU
        return x

或用 nn.Sequential 简写:

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.ReLU(),
    nn.Linear(128, 10),    # 输出层,不加 ReLU
)

3.3 inplace 参数

nn.ReLU(inplace=False)  # 默认,返回新 tensor,原数据保留
nn.ReLU(inplace=True)   # 直接修改原 tensor,省内存
参数内存安全性建议
inplace=False多占一份安全训练阶段用这个
inplace=True省一份可能影响梯度计算推理阶段或确认安全时使用

4. ReLU 的优缺点

4.1 优点

优点说明
计算极快只做 max(0, x),没有指数运算,比 Sigmoid/Tanh 快得多
缓解梯度消失正数区间梯度恒为 1,不会像 Sigmoid 那样梯度越来越小
稀疏激活约 50% 神经元输出 0,产生稀疏表示,有类似正则化的效果

4.2 缺点

缺点说明
Dead ReLU如果某个神经元的输入始终为负,它的输出永远是 0,梯度也是 0,再也无法更新——这个神经元就"死了"
输出非零中心输出全 ≥ 0,可能导致下一层的梯度更新出现 zig-zag 现象

4.3 Dead ReLU 问题图解

正常神经元:                Dead 神经元:
输入有正有负                 输入始终为负

  ──╲  ╱──                  ────────── 0
     ╲╱                     
   有输出,能学习              永远输出 0,停止学习

常见原因:学习率设得太大,导致权重更新幅度过大,神经元被"推"到负区间后回不来。

解决方案:使用 LeakyReLU 等变体(见第 6 节)。

5. ReLU vs Sigmoid vs Tanh

在 ReLU 出现之前,Sigmoid 和 Tanh 是主流的激活函数。ReLU 之所以取代它们,主要因为梯度消失问题

5.1 三者对比

属性ReLUSigmoidTanh
公式max(0,x)\max(0,x)11+ex\frac{1}{1+e^{-x}}exexex+ex\frac{e^x-e^{-x}}{e^x+e^{-x}}
输出范围[0,+)[0, +\infty)(0,1)(0, 1)(1,1)(-1, 1)
梯度范围0 或 1(0,0.25](0, 0.25](0,1](0, 1]
计算成本极低高(指数运算)高(指数运算)
梯度消失正区间不会严重较严重
零中心

5.2 为什么 Sigmoid 会梯度消失

Sigmoid 的梯度最大只有 0.25。在反向传播中,梯度要逐层相乘

10 层网络的梯度:0.25¹⁰ = 0.00000095

梯度几乎为零,前面的层学不动了。

ReLU 的正区间梯度恒为 1,不管多少层,梯度都不会被压缩:

10 层网络的梯度:1¹⁰ = 1

梯度完整传播,每层都能有效学习。

5.3 各自的适用场景

激活函数适用场景
ReLU隐藏层的默认选择
Sigmoid二分类输出层(输出概率)
Tanh需要零中心输出的场景(如 RNN)

6. ReLU 家族:常用变体

当原版 ReLU 遇到 Dead ReLU 等问题时,可以换用以下变体:

6.1 LeakyReLU

给负数区间一个小斜率(默认 0.01),防止神经元"死掉":

LeakyReLU(x)={xx>00.01xx0\text{LeakyReLU}(x) = \begin{cases} x & x > 0 \\ 0.01x & x \leq 0 \end{cases}

nn.LeakyReLU(negative_slope=0.01)

6.2 PReLU

和 LeakyReLU 类似,但斜率 α\alpha可学习的参数

PReLU(x)={xx>0αxx0\text{PReLU}(x) = \begin{cases} x & x > 0 \\ \alpha x & x \leq 0 \end{cases}

nn.PReLU()  # α 初始值 0.25,通过训练自动调整

6.3 ELU

负数区间用指数曲线,输出均值更接近 0:

ELU(x)={xx>0α(ex1)x0\text{ELU}(x) = \begin{cases} x & x > 0 \\ \alpha(e^x - 1) & x \leq 0 \end{cases}

nn.ELU(alpha=1.0)

6.4 GELU

Transformer 时代的主流激活函数(GPT、BERT 都在用):

GELU(x)=xΦ(x)\text{GELU}(x) = x \cdot \Phi(x)

其中 Φ(x)\Phi(x) 是标准正态分布的累积分布函数。直觉上可以理解为:以概率的方式决定是否保留输入。

nn.GELU()

6.5 选择指南

场景推荐
CNN 隐藏层(默认)nn.ReLU()
遇到 Dead ReLU 问题nn.LeakyReLU()nn.PReLU()
Transformer / NLPnn.GELU()
需要零中心输出nn.ELU()
不确定选哪个先用 nn.ReLU(),效果不好再换

7. 常见问题

Q1:最后一层要加 ReLU 吗?

不加。 最后一层的输出是原始分数(logits),需要保留正负信息:

  • 分类任务:logits 交给 CrossEntropyLoss(内部会做 Softmax)
  • 回归任务:直接输出预测值
model = nn.Sequential(
    nn.Linear(784, 256),
    nn.ReLU(),              # 隐藏层加 ReLU
    nn.Linear(256, 10),     # 输出层不加 ReLU
)

Q2:ReLU 放在 BatchNorm 前面还是后面?

两种都有人用,但先 BN 后 ReLU 是更常见的做法:

nn.Linear(784, 256),
nn.BatchNorm1d(256),   # 先归一化
nn.ReLU(),             # 再激活

理由:BN 把数据分布拉回均值 0,让更多值落在 ReLU 的正区间,减少 Dead ReLU。

Q3:ReLU 和 Dropout 的顺序?

先 ReLU 后 Dropout

nn.Linear(784, 256),
nn.ReLU(),             # 先激活
nn.Dropout(0.5),       # 再随机丢弃
nn.Linear(256, 10),

Q4:为什么 ReLU 这么简单却这么有效?

  1. 计算快 —— GPU 上 max 操作极其高效
  2. 梯度好 —— 正区间梯度恒为 1,不消失不爆炸
  3. 稀疏性 —— 天然产生稀疏激活,类似大脑中只有少部分神经元同时激活

8. 一句话总结

ReLU 的本质作用是引入非线性,让多层网络不退化为一层。 它用最简单的方式——max(0,x)\max(0, x)——赋予了神经网络拟合任意复杂函数的能力,是现代深度学习的基石。