📚 课程目录

第3课:激活函数

📖 本课概述

激活函数赋予神经网络非线性能力。没有激活函数,再深的网络也只等价于一个线性变换。本课深入分析各种激活函数的数学性质、优缺点和适用场景。

⚡ 一、为什么需要激活函数

1.1 线性瓶颈

如果所有激活函数都是线性的(f(x)=x),则:

h₁ = W₁x + b₁

h₂ = W₂h₁ + b₂ = W₂(W₁x + b₁) + b₂ = (W₂W₁)x + (W₂b₁ + b₂)

→ 仍然是一个线性变换!深层网络失去意义。

激活函数引入非线性,使得神经网络可以逼近任意复杂的函数(万能逼近定理)。

1.2 万能逼近定理

具有非线性激活函数的单隐层前馈网络,只要隐层足够宽,可以逼近任意连续函数到任意精度。但实践中,深度比宽度更有效

📊 二、常见激活函数详解

2.1 Sigmoid

σ(x) = 1 / (1 + e⁻ˣ)

导数:σ'(x) = σ(x)(1 - σ(x))

输出范围:(0, 1)

优点:输出可解释为概率,适合二分类输出层

缺点:①梯度消失(两侧梯度趋0)②输出非零中心③指数运算慢

2.2 Tanh

tanh(x) = (eˣ - e⁻ˣ) / (eˣ + e⁻ˣ)

输出范围:(-1, 1)

优点:零中心化,收敛比Sigmoid快

缺点:仍有梯度消失问题

2.3 ReLU (Rectified Linear Unit)

ReLU(x) = max(0, x)

导数:ReLU'(x) = 1 if x>0, 0 if x≤0

优点:①计算极快②正区间梯度恒为1,缓解梯度消失③产生稀疏激活

缺点:①神经元死亡(Dead ReLU)②输出非零中心

2.4 Leaky ReLU / ELU / GELU

LeakyReLU(x) = x if x>0, αx if x≤0 (α通常=0.01)

ELU(x) = x if x>0, α(eˣ-1) if x≤0

GELU(x) ≈ x·Φ(x),Φ为标准正态CDF

2.5 函数值与导数对比


import torch
import torch.nn as nn
import math

# 常见激活函数对比
activations = {
    "Sigmoid": lambda x: 1/(1+math.exp(-x)),
    "Tanh": lambda x: math.tanh(x),
    "ReLU": lambda x: max(0, x),
    "LeakyReLU(0.01)": lambda x: x if x>0 else 0.01*x,
    "ELU(1.0)": lambda x: x if x>0 else math.exp(x)-1,
    "GELU": lambda x: 0.5*x*(1+math.tanh(math.sqrt(2/math.pi)*(x+0.044715*x**3))),
}

# 函数值
print("=== 激活函数值对比 ===")
print(f"{'x':>6} | " + " | ".join(f"{name:>14}" for name in activations))
print("-" * 100)
for x_val in [-3, -1, -0.5, 0, 0.5, 1, 3]:
    vals = [f"{fn(x_val):>14.6f}" for fn in activations.values()]
    print(f"{x_val:>6} | " + " | ".join(vals))

# 梯度值(导数)
print("\n=== 激活函数导数对比 ===")
print(f"{'x':>6} | " + " | ".join(f"{name:>14}" for name in activations))
print("-" * 100)
for x_val in [-3, -1, -0.5, 0, 0.5, 1, 3]:
    x_t = torch.tensor(float(x_val), requires_grad=True)
    vals = []
    for name in activations:
        if name == "Sigmoid":
            y = torch.sigmoid(x_t)
        elif name == "Tanh":
            y = torch.tanh(x_t)
        elif name == "ReLU":
            y = torch.relu(x_t)
        elif name.startswith("LeakyReLU"):
            y = torch.nn.functional.leaky_relu(x_t, 0.01)
        elif name.startswith("ELU"):
            y = torch.nn.functional.elu(x_t, 1.0)
        elif name == "GELU":
            y = torch.nn.functional.gelu(x_t)
        y.backward()
        vals.append(f"{x_t.grad.item():>14.6f}")
        x_t = torch.tensor(float(x_val), requires_grad=True)
    print(f"{x_val:>6} | " + " | ".join(vals))
🟢 运行结果 — 激活函数值与导数 ✅验证通过 === 激活函数值对比 === x | Sigmoid | Tanh | ReLU | LeakyReLU(0.01) | ELU(1.0) | GELU ---------------------------------------------------------------------------------------------------- -3 | 0.047426 | -0.995055 | 0.000000 | -0.030000 | -0.950213 | -0.003637 -1 | 0.268941 | -0.761594 | 0.000000 | -0.010000 | -0.632121 | -0.158808 -0.5 | 0.377541 | -0.462117 | 0.000000 | -0.005000 | -0.393469 | -0.154286 0 | 0.500000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 0.5 | 0.622459 | 0.462117 | 0.500000 | 0.500000 | 0.500000 | 0.345714 1 | 0.731059 | 0.761594 | 1.000000 | 1.000000 | 1.000000 | 0.841192 3 | 0.952574 | 0.995055 | 3.000000 | 3.000000 | 3.000000 | 2.996363 === 激活函数导数对比 === x | Sigmoid | Tanh | ReLU | LeakyReLU(0.01) | ELU(1.0) | GELU ---------------------------------------------------------------------------------------------------- -3 | 0.045177 | 0.009866 | 0.000000 | 0.010000 | 0.049787 | -0.011946 -1 | 0.196612 | 0.419974 | 0.000000 | 0.010000 | 0.367879 | -0.083315 -0.5 | 0.235004 | 0.786448 | 0.000000 | 0.010000 | 0.606531 | 0.132505 0 | 0.250000 | 1.000000 | 0.000000 | 0.010000 | 1.000000 | 0.500000 0.5 | 0.235004 | 0.786448 | 1.000000 | 1.000000 | 1.000000 | 0.867495 1 | 0.196612 | 0.419974 | 1.000000 | 1.000000 | 1.000000 | 1.083315 3 | 0.045177 | 0.009866 | 1.000000 | 1.000000 | 1.000000 | 1.011946

🔬 三、激活函数对训练的影响


import torch
import torch.nn as nn

# 激活函数对训练的影响实验
torch.manual_seed(42)
X = torch.randn(500, 20)
y = (X[:, 0] + X[:, 1] > 0).float().unsqueeze(1)

results = {}
for act_name, act_fn in [
    ("Sigmoid", nn.Sigmoid()),
    ("Tanh", nn.Tanh()),
    ("ReLU", nn.ReLU()),
    ("LeakyReLU", nn.LeakyReLU(0.01)),
    ("GELU", nn.GELU()),
]:
    model = nn.Sequential(
        nn.Linear(20, 64), act_fn,
        nn.Linear(64, 32), act_fn if act_name != "GELU" else nn.GELU(),
        nn.Linear(32, 1), nn.Sigmoid()
    )
    optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
    loss_fn = nn.BCELoss()
    
    losses = []
    for epoch in range(200):
        optimizer.zero_grad()
        output = model(X)
        loss = loss_fn(output, y)
        loss.backward()
        optimizer.step()
        losses.append(loss.item())
    
    results[act_name] = losses
    print(f"{act_name:>12}: 初始loss={losses[0]:.4f}, 最终loss={losses[-1]:.4f}, 改善={losses[0]-losses[-1]:.4f}")

# 梯度流分析
print("\n=== 梯度流分析(10层网络)===")
for act_name, act_cls in [
    ("Sigmoid", nn.Sigmoid),
    ("ReLU", nn.ReLU),
    ("Tanh", nn.Tanh),
]:
    layers = [nn.Linear(10, 10)]
    for _ in range(10):
        layers.extend([act_cls(), nn.Linear(10, 10)])
    model = nn.Sequential(*layers)
    
    x = torch.randn(2, 10)
    y = model(x).sum()
    y.backward()
    
    grad_norms = []
    for i, layer in enumerate(model):
        if isinstance(layer, nn.Linear) and layer.weight.grad is not None:
            grad_norms.append(layer.weight.grad.norm().item())
    
    print(f"{act_name:>8}: 梯度范数变化 = ", end="")
    for g in grad_norms[:5]:
        print(f"{g:.6f} → ", end="")
    print(f"... → {grad_norms[-1]:.6f}")
🟢 运行结果 — 训练对比与梯度流 ✅验证通过 Sigmoid: 初始loss=0.6920, 最终loss=0.1540, 改善=0.5381 Tanh: 初始loss=0.7034, 最终loss=0.0164, 改善=0.6871 ReLU: 初始loss=0.6948, 最终loss=0.0111, 改善=0.6838 LeakyReLU: 初始loss=0.7003, 最终loss=0.0099, 改善=0.6905 GELU: 初始loss=0.6837, 最终loss=0.0092, 改善=0.6745 === 梯度流分析(10层网络)=== Sigmoid: 梯度范数变化 = 0.000000 → 0.000000 → 0.000001 → 0.000007 → 0.000058 → ... → 9.453672 ReLU: 梯度范数变化 = 0.000366 → 0.000538 → 0.000601 → 0.001607 → 0.002313 → ... → 2.223222 Tanh: 梯度范数变化 = 0.007456 → 0.017573 → 0.017997 → 0.032720 → 0.059523 → ... → 5.289428

📋 四、选择指南

场景推荐激活函数原因
隐藏层(通用)ReLU / GELU效果好,计算快
TransformerGELUBERT/GPT标配
二分类输出Sigmoid输出(0,1)可解释为概率
多分类输出Softmax输出概率分布
回归输出恒等函数输出无约束
Dead ReLU问题LeakyReLU/ELU负区间有梯度

🔬 五、高级激活函数

5.1 Swish / SiLU

Swish(x) = x · σ(βx)

当β=1时,即SiLU(Sigmoid Linear Unit)

特点:非单调性,在负区间有小幅非零输出

5.2 Mish

Mish(x) = x · tanh(softplus(x)) = x · tanh(ln(1+eˣ))

平滑、非单调、自正则化

在YOLOv4等模型中被验证有效

5.3 激活函数的选择策略

💡 实践建议:

📊 六、激活函数发展时间线

年份激活函数提出者关键创新
1944Sigmoid-最早使用
1998TanhLeCun等零中心化
2010ReLUNair & Hinton简单高效
2013LeakyReLUMaas等解决Dead ReLU
2015ELUClevert等负值平滑
2016SeLUKlambauer等自归一化
2017SwishGoogle Brain非单调
2018GELUHendrycks & Gimpel概率性门控
2019MishMisra平滑+非单调

📖 七、延伸阅读

💡 推荐资源:

📝 练习

练习1:实现Swish激活函数

Swish(x) = x · σ(βx),实现并对比ReLU。观察β参数的影响。

练习2:Dead ReLU实验

用大学习率训练ReLU网络,观察有多少神经元"死亡"(输出恒为0)。

练习3:Mish激活函数

实现Mish(x) = x·tanh(softplus(x)),在CIFAR10上对比ReLU和Mish的效果。

🏆

成就解锁:非线性觉醒

你已经理解了激活函数如何赋予神经网络非线性能力。
没有激活函数,深度学习就只是线性代数!