1. 为什么网络越深,反而越蠢
很多做深度学习的朋友都遇到过这种怪事:网络层数从 9 层加到 20 层,数据量没变,训练时间翻倍,结果测试准确率反而往下掉。模型不是变得更强,而是变得更“笨”了。
如果你去排查代码,大概率会发现:损失函数在下降,梯度也没爆掉,但网络的表达能力就是上不去。这时候很多人会怀疑网络结构有问题,或者数据增强做得不够。但真正容易被忽略的,是那个每层都在发生、却几乎没人细看的操作——激活函数。
激活函数决定了每一层输出的分布形态,也决定了误差信号能不能顺利传回前面的层。选错了激活函数,网络层数越多,信号衰减越严重,深层网络就变成了“叠了很多层但都在做无用功”的模型。这就是“越叠越蠢”的核心原因:层数叠上去了,信息流却没有打通。
这里先给一个明确判断:模型上限并不只取决于参数量、数据量和算力,激活函数作为非线性表达的直接载体,往往是被低估的“瓶颈层”。本文会从梯度消失、信息压缩、神经元死亡三个角度出发,分析 10 种主流激活函数的数学逻辑、适用场景和 PyTorch 实现方式,最后给出一套可复跑的对比实验,让你在本地就能直观看到不同激活函数对深层网络表现的影响。
无论你是在做图像分类、NLP 特征提取,还是在调 Transformer 和 CNN 结构,这篇文章都能帮你重新审视自己网络里那层“不起眼”的激活函数。
2. 激活函数到底做了什么:非线性、梯度、分布
2.1 激活函数解决的核心问题
线性变换的堆叠仍然是线性变换。如果没有激活函数,神经网络无论叠多少层,本质上都等价于一个线性模型,无法拟合复杂函数。激活函数的作用就是给网络引入非线性能力,让每一层都能学习到原始输入的不同抽象特征。
以手写数字识别为例:输入是一张 28×28 的图片,原始像素值和数字“7”的语义之间没有任何线性关系。只有经过多层的非线性变换,网络才能逐步组合出边缘、轮廓、局部纹理,再到整体形状。
2.2 三个核心维度
评估激活函数好不好,本质上看三个维度:
- 梯度流动:反向传播时,激活函数的导数会乘到梯度里。导数接近 0 的区域会让梯度消失,导数过大则可能梯度爆炸。
- 输出分布:每一层的输出分布会影响下一层的权重更新效率。如果输出长期偏向某个区间,网络学习会变得异常缓慢。
- 计算开销:包括前向和反向的数学运算量。在训练大模型时,这个开销会被放大无数倍。
2.3 一个容易误解的概念
很多初学者会把“激活函数”和“输出层函数”混为一谈。实际上:
- 隐藏层激活函数:负责特征变换,如 ReLU、GELU、Mish,选择最影响深层网络的学习能力。
- 输出层激活函数:负责将特征映射到任务空间,如二分类用 Sigmoid,多分类用 Softmax,回归通常不加激活函数。
这两类函数的职责完全不同,选择逻辑也不一样。本文讨论的 10 种激活函数,主要面向隐藏层。
3. 为什么“叠层”会放大激活函数的缺陷
很多人觉得激活函数就是“非线性一下”,选哪个都差不多。层数少的时候确实如此,但网络一旦加深,不同激活函数之间的差距会被急剧放大。
3.1 梯度消失:深层网络的“慢性毒药”
以 Sigmoid 为例,其导数最大值只有 0.25。误差每经过一层,梯度至少缩水四分之一。10 层之后,0.25 的 10 次方大约是 9.5×10⁻⁷,梯度基本上消失殆尽。这就是为什么早期神经网络很少超过三五层——不是不想深,是信号根本传不回去。
3.2 神经元死亡:ReLU 的隐性代价
ReLU 把负数全部置零,虽然解决了正向部分的梯度消失,却把负半轴的梯度直接斩断。一旦某个神经元的加权输入稳定为负,它后续的梯度就永远是 0,再也不会被更新。这就是“神经元死亡”,或者叫 Dying ReLU。对于学习率设置不当,或者网络层数较深的情况,这个问题尤其致命。
3.3 表达能力的“虚假上限”
还有一个经常被忽视的问题是:深层网络如果每一层都做着类似的特征变换,叠再多的层也无法拓宽表达空间。表现好的激活函数,普遍具有两个特征:允许负值少量通过和输出范围有界或自适应。前者能保住梯度,后者能稳定分布,两者共同决定了网络真正能达到的上限。
从材料中可以确认一个趋势:像 GELU、Swish、Mish 这类“平滑且带负值泄漏”的激活函数,正在越来越多地替换传统 ReLU,成为 CNN 和 Transformer 结构中的首选。
4. 10 种主流激活函数逐一拆解
下面逐一分析 10 种激活函数的数学定义、梯度行为、优缺点和适用场景。每个函数都配了 PyTorch 的调用方式,方便你直接在自己的模型里替换试用。
4.1 Sigmoid:教科书级的梯度杀手
Sigmoid 的公式为:
[ \sigma(x) = \frac{1}{1 + e^{-x}} ]
它的输出被压缩在 (0, 1) 区间,适合表示概率。但问题在于:当输入绝对值较大时,导数趋近于 0,梯度信息基本丢失。此外,它的输出均值不是 0,会导致后续层的输入始终为正,影响权重更新效率。
适用场景:二分类输出层、注意力权重的归一化、门控机制。隐藏层慎用。
import torch.nn as nn activation = nn.Sigmoid()4.2 Tanh:零均值化的改良者
[ \tanh(x) = \frac{e^x - e^{-x}}{e^x + e^{-x}} ]
Tanh 解决了 Sigmoid 输出非零均值的问题,输出范围为 (-1, 1),在隐藏层中表现比 Sigmoid 好很多。但在输入较大或较小时,同样存在饱和区和梯度消失问题。
适用场景:循环神经网络中的早期设计、生成模型的某些中间层。
activation = nn.Tanh()4.3 ReLU:深度学习崛起的功臣
[ \text{ReLU}(x) = \max(0, x) ]
ReLU 在正区间梯度恒为 1,彻底缓解了正半轴的梯度消失问题,而且计算极其简单。它直接带动了深度学习在 2012 年之后的迅速发展。
但它的缺陷也很明显:负半轴梯度为 0,容易造成神经元死亡;输出均值大于 0,导致内部偏移。
适用场景:CNN 隐藏层、浅层网络、对精度要求不是极端苛刻的场景。
activation = nn.ReLU()4.4 Leaky ReLU:给负区间留一扇窗
[ \text{Leaky ReLU}(x) = \begin{cases} x & x > 0 \ \alpha x & x \le 0 \end{cases} ]
其中 (\alpha) 通常取 0.01。Leaky ReLU 允许负值以很小的斜率通过,避免神经元彻底死亡。
从实际工程看,Leaky ReLU 比 ReLU 更稳定,尤其在训练不稳定或学习率较大时。但固定的负斜率不一定适合所有数据分布。
activation = nn.LeakyReLU(negative_slope=0.01)4.5 PReLU:把负斜率变成可学习的参数
[ \text{PReLU}(x) = \begin{cases} x & x > 0 \ a x & x \le 0 \end{cases} ]
PReLU 与 Leaky ReLU 形式上一样,区别在于 (a) 不是固定的,而是网络在训练中自己学习的参数。这意味着网络可以根据数据分布动态调整负区间的信息保留程度。
PReLU 在图像识别任务中,尤其是人脸识别和超分辨率任务中表现突出。代价是增加了少量参数量和过拟合风险。
activation = nn.PReLU(num_parameters=1, init=0.25)4.6 ELU:让输出均值逼近零
[ \text{ELU}(x) = \begin{cases} x & x > 0 \ \alpha (e^x - 1) & x \le 0 \end{cases} ]
ELU 在负区间是平滑的指数曲线,而不是直线。这种平滑性让它在 (x=0) 处可导,优化过程更平稳;同时,负区间的输出能够将整体均值向 0 拉近,缓解偏移效应。
ELU 的问题是计算中有指数运算,前向和反向都比 ReLU 慢;此外,(\alpha) 需要人工调整,选不好反而影响收敛。
activation = nn.ELU(alpha=1.0)4.7 SELU:自带归一化效果的激活函数
SELU 是 ELU 的缩放版本,出自“自归一化神经网络”论文。核心思想是:在特定参数配置下,SELU 能让网络输出自动保持零均值和单位方差,而不需要额外使用 Batch Normalization。
[ \text{SELU}(x) = \lambda \begin{cases} x & x > 0 \ \alpha (e^x - 1) & x \le 0 \end{cases} ]
其中 (\lambda \approx 1.0507),(\alpha \approx 1.67326)。
适用场景:全连接网络、堆叠自编码器。不过需要注意的是,SELU 的自归一化性质对网络初始化和噪声有要求,在 CNN 中效果不一定优于 BN 加 ReLU 的组合。
activation = nn.SELU()4.8 Swish / SiLU:自动搜索出来的“惊喜”
Swish 由 Google 团队通过自动化搜索发现,定义为:
[ \text{Swish}(x) = x \cdot \sigma(x) ]
这个函数的一个显著特点是有下界、无上界、非单调:在 x 略小于 0 时输出会小幅变负,然后又回到 0。这种平滑的非单调性,让它在深层网络中表现出比 ReLU 更好的梯度流动特性。
PyTorch 中 Swish 和 SiLU 是同一个函数。
activation = nn.SiLU()4.9 GELU:Transformer 们的默认选择
[ \text{GELU}(x) = x \cdot \Phi(x) ]
其中 (\Phi(x)) 是标准正态分布的累积分布函数。GELU 的直觉是:根据输入值的大小决定“保留”还是“丢弃”这个神经元,而不是像 ReLU 那样硬截断。
GELU 是 BERT、GPT、ViT 等大量 Transformer 结构中隐藏层激活函数的默认选择,它在 NLP 和视觉任务中都验证了稳定性和表达能力。PyTorch 提供近似实现:
activation = nn.GELU()4.10 Mish:平滑+自正则的后来者
[ \text{Mish}(x) = x \cdot \tanh(\text{softplus}(x)) ]
Mish 可以看作是 Swish 的变体,同样无上界、有下界、非单调。它在很多图像任务中,尤其是 YOLOv4 等目标检测网络中表现优异。缺点同样是计算开销偏大。
activation = nn.Mish()4.11 10 种激活函数速览对比
| 激活函数 | 公式核心 | 负区间处理 | 梯度风险 | 计算开销 | 推荐场景 |
|---|---|---|---|---|---|
| Sigmoid | 1/(1+e⁻ˣ) | 饱和 | 梯度消失严重 | 中 | 输出层、门控 |
| Tanh | 双曲正切 | 饱和 | 梯度消失 | 中 | RNN 早期 |
| ReLU | max(0,x) | 截断为 0 | 神经元死亡 | 极低 | CNN 默认 |
| Leaky ReLU | 固定小斜率负值 | 小斜率通过 | 低 | 极低 | CNN 稳定训练 |
| PReLU | 学习负斜率 | 可学习斜率 | 低 | 低 | 人脸识别等 |
| ELU | 指数负区间 | 平滑趋向负饱和 | 低 | 中 | 需要稳定收敛 |
| SELU | 缩放 ELU | 指数负区间 | 低 | 中 | 自归一化网络 |
| Swish/SiLU | x·sigmoid(x) | 小幅负值通过 | 低 | 中 | Transformer 替代 |
| GELU | x·Φ(x) | 概率性保留 | 极低 | 中 | Transformer 默认 |
| Mish | x·tanh(softplus) | 平滑负值 | 低 | 中高 | 目标检测 |
5. 如何用 PyTorch 复现一份激活函数对比实验
看完原理,还是需要亲手跑一遍才能建立直觉。下面给出一份完整的实验脚本,通过对比 5 种代表性激活函数(ReLU、Leaky ReLU、ELU、SiLU、GELU)在深层网络上的表现,直观验证“激活函数影响模型上限”的结论。
5.1 实验环境准备
- Python 3.8 以上
- PyTorch 2.x(1.x 也可以运行,但建议 2.x)
- torchvision
- matplotlib(用于画损失曲线)
- MNIST 数据集(torchvision 会自动下载)
安装依赖:
pip install torch torchvision matplotlib5.2 实验设计与代码实现
实验思路是:构造一个 10 层全连接网络,每层之间插入指定激活函数,分别用不同激活函数在相同数据、相同超参数下训练,最后对比测试准确率和损失曲线。
# 文件路径:activation_compare.py import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt # ---------- 1. 定义可切换激活函数的深层网络 ---------- class DeepNet(nn.Module): def __init__(self, activation_fn, hidden_size=256, num_layers=10, input_size=784, num_classes=10): super().__init__() layers = [] in_features = input_size for _ in range(num_layers): layers.append(nn.Linear(in_features, hidden_size)) layers.append(activation_fn) in_features = hidden_size layers.append(nn.Linear(in_features, num_classes)) self.network = nn.Sequential(*layers) def forward(self, x): return self.network(x) # ---------- 2. 数据加载 ---------- def load_data(batch_size=128): transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False) return train_loader, test_loader # ---------- 3. 训练和验证函数 ---------- def train_one_epoch(model, train_loader, criterion, optimizer, device): model.train() total_loss = 0.0 correct = 0 total = 0 for images, labels in train_loader: images = images.view(images.size(0), -1).to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() avg_loss = total_loss / total accuracy = correct / total return avg_loss, accuracy def evaluate(model, test_loader, criterion, device): model.eval() total_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for images, labels in test_loader: images = images.view(images.size(0), -1).to(device) labels = labels.to(device) outputs = model(images) loss = criterion(outputs, labels) total_loss += loss.item() * images.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() avg_loss = total_loss / total accuracy = correct / total return avg_loss, accuracy # ---------- 4. 主实验流程 ---------- def run_experiment(activation_name, activation_fn, epochs=5, device='cpu'): print(f"\n===== 开始实验: {activation_name} =====") torch.manual_seed(42) device = torch.device(device) train_loader, test_loader = load_data() model = DeepNet(activation_fn).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) train_losses = [] test_accuracies = [] for epoch in range(1, epochs + 1): train_loss, train_acc = train_one_epoch(model, train_loader, criterion, optimizer, device) test_loss, test_acc = evaluate(model, test_loader, criterion, device) train_losses.append(train_loss) test_accuracies.append(test_acc) print(f"Epoch {epoch}: train_loss={train_loss:.4f}, train_acc={train_acc:.4f}, " f"test_loss={test_loss:.4f}, test_acc={test_acc:.4f}") return train_losses, test_accuracies # ---------- 5. 启动对比 ---------- if __name__ == "__main__": activations = { "ReLU": nn.ReLU(), "LeakyReLU": nn.LeakyReLU(0.01), "ELU": nn.ELU(alpha=1.0), "SiLU": nn.SiLU(), "GELU": nn.GELU(), } results = {} for name, fn in activations.items(): train_losses, test_accs = run_experiment(name, fn, epochs=5) results[name] = {"train_loss": train_losses, "test_acc": test_accs} # ---------- 6. 绘制测试准确率对比曲线 ---------- plt.figure(figsize=(10, 6)) for name, res in results.items(): plt.plot(range(1, 6), res["test_acc"], marker='o', label=name) plt.xlabel("Epoch") plt.ylabel("Test Accuracy") plt.title("Activation Function Comparison on 10-Layer MNIST") plt.legend() plt.grid(True) plt.savefig("activation_compare.png") plt.show()这段代码的关键点有三个:
DeepNet接受一个激活函数实例,插入到每一层线性层之后,构造出 10 层深度网络。- 实验固定了随机种子、优化器、学习率和批大小,唯一变量就是激活函数。
- 最终的准确率曲线会直接显示不同激活函数在同等条件下的学习能力差异。
5.3 运行与验证
在终端执行:
python activation_compare.py第一次运行会自动下载 MNIST 数据集。如果网络环境无法访问外部资源,需要提前手动下载数据集并放到./data目录下。
预期输出大致如下(具体数值会受随机性和硬件影响):
===== 开始实验: ReLU ===== Epoch 1: train_loss=0.4012, train_acc=0.8790, test_acc=0.8923 Epoch 2: train_loss=0.2001, train_acc=0.9398, test_acc=0.9442 Epoch 3: train_loss=0.1385, train_acc=0.9572, test_acc=0.9580 ... ===== 开始实验: GELU ===== Epoch 1: train_loss=0.3520, train_acc=0.8935, test_acc=0.9044 ...从经验来看,在这个 10 层结构中,GELU、SiLU 的收敛速度和最终准确率通常会优于 ReLU,而 ELU 和 Leaky ReLU 介于两者之间。ReLU 在深层网络中会更容易出现部分神经元死亡的情况,导致训练后期准确率上升变慢。
5.4 判断实验是否成功
实验成功的标志不是某一个激活函数准确率最高,而是:
- 程序无报错跑完 5 个 epoch。
plot窗口或activation_compare.png中能清晰看到 5 条曲线。- 不同激活函数的曲线有明显区分,而不是完全重叠。
如果所有曲线完全一样,说明实验代码可能出现了变量泄漏,或者激活函数没有被真正替换。这时候要检查DeepNet的__init__方法是否在每层都接收了新的激活函数实例。
6. 常见问题与排查思路
6.1 激活函数在层间共享实例
问题现象:不同实验之间结果差异很小,甚至完全相同。
可能原因:如果把同一个激活函数实例传给了所有层,像nn.ReLU()这种无状态激活函数通常没问题;但nn.PReLU()有可学习参数,如果共享实例,不同层的参数会互相影响。
排查方式:检查是否在每次调用run_experiment时都新建了激活函数实例。
# 错误写法:所有层共享同一个 PReLU 参数 prelu = nn.PReLU() model = DeepNet(prelu) # 正确写法:每一层都新建一个实例 model = DeepNet(nn.PReLU())6.2 深层网络训练损失不下降
问题现象:损失值从一开始就居高不下,或者长时间保持不变。
可能原因:激活函数选择不当导致的梯度消失,或学习率设置过大造成梯度震荡。
排查方式:
- 检查每一层的梯度范数,如果接近 0,说明激活函数饱和。
- 尝试换用 GELU、SiLU、Leaky ReLU。
- 降低学习率,比如从 0.001 降到 0.0001。
6.3 训练集准确率很高,测试集准确率很低
问题现象:过拟合。
可能原因:模型容量过大、数据增强不足、正则化缺失。
排查方式:增加 Dropout、权重衰减,或者缩小网络宽度。激活函数的选择对过拟合影响较小,但如果 PReLU 引入了过多可学习参数,也需要考虑这个因素。
6.4 不同激活函数在不同任务上结论反转
问题现象:在 MNIST 上 GELU 最好,在目标检测上 Mish 最好,在语言模型上 GELU 最好,没有一个全能的激活函数。
原因:不同任务的输入分布、网络深度、训练策略差异很大。
排查方式:不要盲目照搬论文结论,在自己的数据上做小规模对比实验,用验证集选择。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 损失不下降 | 梯度消失 | 检查梯度范数 | 换用 GELU/SiLU,降低学习率 |
| 曲线完全重合 | 激活函数实例共享 | 检查模型构造代码 | 每层创建新实例 |
| 训练快测试差 | 过拟合 | 查看训练/测试 gap | 增加 Dropout、权重衰减 |
| 负区间神经元全死 | Dying ReLU | 统计激活值分布 | 换 Leaky ReLU/PReLU |
| 模型收敛极慢 | 输出均值偏移 | 查看每层输出均值 | 换 ELU/SELU |
7. 工程最佳实践与选型建议
7.1 不同场景的激活函数选型原则
从实际项目经验出发,可以给出如下选型建议:
- CNN 图像分类:ReLU 或 Leaky ReLU 是稳妥选择;如果追求更高精度,可以尝试 SiLU 或 Mish。
- Transformer 结构(BERT、GPT、ViT):默认 GELU,不要轻易替换,除非你有足够时间和算力做完整对比。
- 深层全连接网络(MLP):ELU、SiLU、GELU 表现通常优于 ReLU。
- 自编码器或生成模型:SELU 可能带来额外收益,因为它自带归一化效果。
- RNN / LSTM:这类结构内部已有门控机制,隐藏层激活函数影响相对有限,但 tanh 仍然是常见选择。
7.2 使用激活函数时的工程化建议
1. 警惕 PReLU 的过拟合风险
PReLU 引入了额外参数,在数据量较少时容易过拟合。可以用较小的init值或在全连接层减少使用。
2. 大规模训练时优先考虑计算开销
Sigmoid、Tanh、ELU、Mish 都涉及指数运算,在分布式训练中会带来不小的计算负担。如果模型规模巨大,优先选择 ReLU 这类计算极简的激活函数,再用归一化手段解决其缺陷。
3. 与归一化层的配合顺序
常见的顺序是Linear -> Activation -> BatchNorm或Linear -> BatchNorm -> Activation。这两种顺序在理论上有细微差异,但在实践中都可行。关键是要统一,不要在不同层混用。
4. 输出层不要使用隐藏层激活函数
分类任务输出层应该用 Softmax,二分类用 Sigmoid,回归任务不设激活函数或者使用 Identity。把 ReLU 放在输出层会导致输出永远为非负,模型失去预测负值的能力。
5. 模型上线前固定激活函数版本
PyTorch 不同版本对相同激活函数的实现细节可能有差异(例如 GELU 的近似算法)。模型训练和推理时,应固定 PyTorch 版本,避免精度漂移。
7.3 实验中的记录习惯
在做激活函数对比实验时,建议记录以下信息:
- 网络深度和宽度
- 随机种子
- 学习率和优化器
- 数据增强策略
- 每一轮的损失和准确率
- 激活函数的具体版本和参数
这些信息缺一不可。激活函数的对比结论通常依赖特定实验条件,没有完整的实验记录,你很难判断“准确率提升”是来自激活函数还是来自其他因素。
8. 更进阶的方向:自适应与可学习激活函数
如果你已经跑通了上述实验,并且理解了 10 种激活函数的差异,接下来的进阶方向有两个:一是自适应激活函数,二是激活函数搜索。
自适应激活函数的代表是 PReLU 和 Swish 的变体。它们通过引入可学习参数,让网络在训练过程中自己调整激活曲线的形状。另一个思路是把多个基础激活函数加权组合,权重由网络学习得到。
激活函数搜索的代表工作是 Google 的 Swish 系列,它通过自动化搜索在候选函数空间中找到了性能优异的激活函数。这种方法虽然计算成本高,但能发现超出人类设计直觉的函数形式。
对于大多数开发者来说,理解基础激活函数的原理和差异已经足够支撑日常模型调优;搜索方法更多是研究者的工作方向。建议你先在 MNIST 或 CIFAR-10 上建立自己的“激活函数测评基准”,以后遇到新模型,先跑一组对比再下结论,而不是凭感觉选函数。
9. 总结与后续建议
回到开头的问题:网络越叠越蠢,问题往往不出在网络层数本身,而出在激活函数是否匹配网络深度和任务类型。10 层以上的网络,ReLU 的神经元死亡问题会被放大,Sigmoid 的梯度消失问题会彻底阻断学习,而 GELU、SiLU、Mish 这类平滑非单调函数能在保持梯度流动的同时,给网络更强的表达能力。
本文的核心结论可以归纳为三点:
- 选激活函数不是选“非线性”,而是选“梯度流动性和输出分布”。这才是影响深层网络上限的关键。
- 没有全能的激活函数。Transformer 用 GELU、CNN 用 ReLU/Leaky ReLU、自归一化网络用 SELU,都是长期实践中验证过的组合。
- 动手对比是最好的学习方法。建议你用文中的 PyTorch 代码,在自己的数据集上跑一遍不同激活函数的对比实验,观察训练曲线和测试准确率的变化。
下一步可以继续深入的方向包括:学习率与激活函数的配合策略、归一化层与激活函数的顺序影响、以及在分布式训练中激活函数的性能优化。建议先把文中的实验脚本保存下来,作为后续模型调优的标准测试工具。