神经网络越叠越蠢,这个说法听起来有些夸张,但它描述的是一个真实存在的工程现象。模型层数从几层加深到几十层甚至上百层,参数量明显增加,训练误差却可能不降反升;或者在网络加深后,前面的层几乎学不到有效特征,后面层则在随机初始化的状态下输出无意义结果。堆叠层数必须同时解决两个问题:信息能否有效前向传播,梯度能否顺利反向回传。激活函数在这两条链路里都处于关键位置:它决定每一层非线性变换的方式,也决定反向传播时梯度的大小和走向。下面围绕 10 种常见激活函数,分析它们如何改善深层网络的梯度传递、非线性表达和训练稳定性,并给出可运行的 PyTorch 对比实验和排查路径。
1. 深层网络为什么越叠越蠢:退化、梯度消失与激活饱和
1.1 训练误差不降反升:优化困难,不等于过拟合
很多时候,网络加深后训练误差反而变高,第一反应容易误判为“过拟合”。但两者的外在表现完全不同。过拟合通常是训练集效果很好、验证集效果变差;而“越叠越蠢”的场景往往是训练集本身的误差就很高,模型根本没有把训练数据拟合到位。
这类现象更接近优化退化。模型容量虽然在增加,但优化器在参数空间中找不到一个足够好的解。一个典型的例子是:不加残差连接时,把一组全连接层从 10 层增加到 20 层甚至 50 层,训练损失可能先下降后反弹。如果每层权重初始化不合理,前向传播到深层时输出方差会不断放大,反向传播时梯度又难以回到浅层,网络参数几乎停在初始值附近。这时模型表达力并不弱,只是训练过程没有把表达能力发挥出来。
这个判断对排查问题很关键。如果训练集 loss 都降不下去,应该先怀疑“优化链路是否通”,也就是梯度传播是否正常、激活函数是否饱和、初始化是否匹配,而不是急着加正则化或者增大数据集。
1.2 反向传播链式法则下的梯度消失与梯度爆炸
反向传播的核心是链式法则。对于某个浅层参数,它的梯度等于后面每一层局部导数的连乘结果:
grad_prev = grad_next * w * activation_gradient如果每一层的梯度范数都小于 1,多层连乘后就会指数级趋近于 0,这叫梯度消失。如果每一层的梯度范数都大于 1,连乘后会指数级增大到天文数字,这叫梯度爆炸。
激活函数在其中是人为可控的一环。Sigmoid 的导数最大只有 0.25,在深层网络中连乘很容易让前面几层梯度接近 0。ReLU 在正半区导数恒为 1,至少不会主动缩小梯度,这就是深层网络通常默认使用 ReLU 的数学原因。
这里要区分梯度消失的两种触发方式:一种是激活函数饱和导致导数接近 0,另一种是权重初始化太小导致每层缩放因子都在缩小梯度。两者叠加时,网络会更快失去学习能力。
1.3 激活函数的饱和区会放大深层问题
“饱和”指的是激活函数输入落在某个范围后,输出变化变得非常缓慢,导数趋近于 0。Sigmoid 在输入很大或很小时输出接近 0 或 1,Tanh 在输入很大或很小时输出接近 -1 或 1。输入一旦进入饱和区,神经元输出对输入几乎不敏感,反向传播时这个神经元贡献的梯度也会归零。
多个饱和神经元串联起来,浅层参数很难获得有效更新。更麻烦的是,饱和不只会因为输入本身过大而触发。权重初始化太大、批归一化没有跟上、学习率过高,都可能让激活层的输入很快落入饱和区。
所以实际项目中,换激活函数通常会伴随初始化策略一起调整。Sigmoid/Tanh 这类中心对称激活更适合配合 Xavier 初始化,ReLU 系列更适合配合 Kaiming 初始化。初始化做得好,能让激活输入落在梯度较大的区间,这是后续一切训练稳定的基础。
2. 激活函数究竟改变了什么:从非线性到梯度通路
2.1 前向传播中的非线性:多个线性层等价于一个线性层
如果不加激活函数,一个多层网络的每一层只是线性变换:
h1 = W1 * x + b1 h2 = W2 * h1 + b2把两步合并,本质还是线性变换:
h2 = (W2 * W1) * x + (W2 * b1 + b2)无论网络叠多少层,表达能力都不会超过一个线性模型。这样的网络无法拟合异或这类非线性关系,更处理不了图像、语音和自然语言中的复杂模式。
激活函数的加入让每个线性变换之后多了一层非线性映射:
z = Wx + b a = activation(z)当激活函数是非线性函数时,多层网络才能逐步组合低级特征,形成更抽象的表示。这也是“深度”真正有意义的条件之一。
在这条链路里,bias 的作用也不可忽视。即使某一层的输入全为 0,bias 也能让加权和不为 0,从而避免激活函数输入长期停留在极端位置。实际网络初始化时,bias 通常初始化为 0 或很小的值,这个选择也需要和激活函数配合。
2.2 反向传播中的导数:激活函数决定梯度是放大还是缩小
反向传播只关心局部梯度。每一层的梯度要乘上当前层的权重,再乘上激活函数的导数:
delta_prev = delta_next * W^T * activation_gradientactivation_gradient是一个标量,如果它小于 1,经过多层连乘就会缩小梯度;如果它大于 1,就可能放大梯度。理想的激活函数应该在常见输入范围内保持非饱和、平滑,让梯度能稳定回传。
ReLU 正半区导数为 1,这是一个非常简洁的设计。它既不会放大梯度,也不会缩小梯度。但它在负半区导数为 0,一旦神经元进入负区间,这个神经元就不会再更新,也就是常说的 Dead ReLU。Leaky ReLU、PReLU、ELU、SELU 等改进版本,核心思路都是在负半区保留一点梯度通道,避免神经元完全死亡。
2.3 仅换激活函数还不够:初始化、归一化和残差需要一起设计
深层网络是否容易训练,是由一个组合机制决定的。激活函数是其中一个关键组件,但不是全部。比较常见的组合是:
- 权重初始化匹配激活函数,ReLU 系列用 Kaiming 初始化,Sigmoid/Tanh 系列用 Xavier 初始化;
- 在线性层之后加入批归一化或层归一化,让每层输入分布更稳定;
- 深度到达几十层以上时加入残差连接,让梯度可以跨层抄近路。
这里的关键是:激活函数解决“非线性表达”和“局部导数”问题,但不负责全局梯度路径。如果网络太深,还是需要残差这类结构来缩短梯度回传链条。理解这一点,就不会把激活函数当作解决所有深层训练问题的万能药。
3. 十种激活函数逐个拆解:公式、导数与适用场景
3.1 Sigmoid:适合理解历史,但不建议用于深层隐藏层
Sigmoid 的公式是:
sigmoid(x) = 1 / (1 + exp(-x))输出范围是 (0, 1),导数范围是 (0, 0.25),最大值为 0.25。
优点是输出可以解释为概率,特别适合二分类输出层。缺点是输出不是零中心化的,在深层网络中一方面饱和问题严重,另一方面导数最大值只有 0.25,多层连乘很容易造成梯度消失。
工程建议:隐藏层不要用 Sigmoid。二分类任务中,通常最后一层用 Sigmoid,或者更推荐在训练时使用带 logits 的BCEWithLogitsLoss,避免数值不稳定。
3.2 Tanh:零中心化改进版,但仍然会饱和
Tanh 的公式是:
tanh(x) = (exp(x) - exp(-x)) / (exp(x) + exp(-x))输出范围是 (-1, 1),导数最大为 1,但在输入绝对值较大时快速接近 0。
和 Sigmoid 相比,Tanh 输出是零中心化的,优化效果通常更好。但在层数较深时,饱和问题依然存在,输入过大或过小都会让梯度趋近 0。
适用场景包括 RNN 中的某些门控计算、浅层网络隐藏层。在深层全连接网络里,Tanh 不是首选。
3.3 ReLU:深层网络默认主力
ReLU 的公式是:
relu(x) = max(0, x)正半区导数为 1,负半区导数为 0。
优点非常明显:计算简单、正区间梯度恒定、输出有稀疏性。它不会主动缩小梯度,所以比 Sigmoid/Tanh 更适合深层网络。缺点是 Dead ReLU,神经元一旦激活输出为 0 且梯度为 0,就可能长期不更新。
工程建议:大多数 CNN 和 MLP 的隐藏层可以从 ReLU 开始。搭配 Kaiming 初始化和适量学习率,能避开大部分 Dead ReLU 问题。
3.4 Leaky ReLU:给负区间留一条小梯度通道
Leaky ReLU 的公式是:
leaky_relu(x) = x, x > 0 leaky_relu(x) = alpha * x, x <= 0alpha通常取 0.01。
它解决的是 ReLU 负区间完全死亡的缺陷。即使输入落在负区间,依然有一个很小的梯度可以让神经元有机会回到活跃状态。相比 ReLU,它在处理大量负特征时更稳定。
缺点是alpha是超参数,不同任务的最优值可能不同。不过大多数情况下,0.01 的默认值就能工作得不错。
3.5 PReLU:把泄漏系数变成可学习参数
PReLU 的公式和 Leaky ReLU 类似:
prelu(x) = x, x > 0 prelu(x) = alpha * x, x <= 0不同点在于alpha不是固定值,而是网络在训练过程中自己学习的参数。
优点是可以针对任务自适应负区间斜率,理论上更灵活。缺点是多了一部分可学习参数,增加了过拟合风险,也需要更细致的正则化和调参。
在大规模图像任务中,PReLU 有应用价值。但在小数据集上,Leaky ReLU 往往足够,不需要额外引入学习参数。
3.6 ELU:平滑负区间,输出均值更接近 0
ELU 的公式是:
elu(x) = x, x > 0 elu(x) = alpha * (exp(x) - 1), x <= 0alpha通常取 1.0。
和 ReLU 相比,ELU 在负区间是平滑曲线,而不是硬截断。它避免了 0 点处的突变,输出均值更接近 0,对下一层训练更友好。
缺点是负区间使用了指数运算,计算成本更高。另外,输入极度为负时,导数依然接近 0,只是不如 ReLU 那样完全截断。需要更高计算精度和更多算力的场景,要评估性能损耗。
3.7 SELU:让网络具备自我归一化能力
SELU 的公式是:
selu(x) = scale * x, x > 0 selu(x) = scale * alpha * (exp(x) - 1), x <= 0在原始论文的设计中,scale约为 1.0507,alpha约为 1.67326。
SELU 的特别之处在于,当网络结构满足特定条件时,输出能自动保持均值为 0、方差为 1 的分布,相当于内建了一部分归一化能力,可以替代某些场景下的批归一化。
但它的适用条件比较严格。原始设定下,网络需要以顺序结构堆叠,权重初始化也需要匹配,复杂结构中并不一定生效。生产环境使用前,一定要先在目标数据上做验证,不要因为“自归一化”这个特性就盲目替换原有激活。
3.8 GELU:Transformer 和预训练模型中的高频选择
GELU 的公式是:
gelu(x) = x * Phi(x)其中Phi(x)是标准正态分布的累积分布函数。工程实现常用近似公式:
gelu(x) ≈ 0.5 * x * (1 + tanh(sqrt(2 / pi) * (x + 0.044715 * x^3)))GELU 的设计思路是根据输入值大小施加不同权重:输入越大,保留程度越高;输入为负时,保留程度越低。它比 ReLU 更平滑,在 Transformer、BERT 等预训练模型中非常常见。
缺点是计算量比 ReLU 高。实际落地时,训练和推理必须使用相同的近似实现,否则模型输出会有细微偏差。
3.9 Swish / SiLU:无上界有下界,平滑非单调
Swish 的公式是:
swish(x) = x * sigmoid(x)它也叫 SiLU。输出无上界,有下界,在负区间呈现非单调特性。
Swish 的优点是平滑、非单调,在部分 CNN 和 Transformer 任务中表现优于 ReLU。MobileNetV3 等网络结构中就使用了 Swish 类似设计。
缺点是计算成本比 ReLU 稍高。很多推理框架对 ReLU 有专门优化,对 Swish 的支持取决于算子实现情况。
3.10 Mish:平滑性更强的探索型激活
Mish 的公式是:
mish(x) = x * tanh(softplus(x)) softplus(x) = ln(1 + exp(x))Mish 同样具有无上界、有下界、平滑、非单调的特点。原始论文中,在部分图像任务上报告了优于 ReLU/Swish 的结果。
但它的计算复杂度更高,涉及指数、对数、双曲正切多个算子。很多推理框架对 Mish 的支持不如 ReLU 成熟,部署阶段需要额外关注算子兼容性和性能。
下面是十种激活函数的速查表。
| 激活函数 | 表达式要点 | 导数特点 | 主要风险 | 典型场景 |
|---|---|---|---|---|
| Sigmoid | 1 / (1 + exp(-x)) | 最大 0.25,容易饱和 | 深层梯度消失 | 二分类输出层 |
| Tanh | (exp(x) - exp(-x)) / (exp(x) + exp(-x)) | 最大 1,仍会饱和 | 深层梯度消失 | 浅层网络、RNN 门控 |
| ReLU | max(0, x) | 正区间 1,负区间 0 | Dead ReLU | CNN、MLP 隐藏层默认 |
| Leaky ReLU | 负区间乘以 alpha | 负区间小梯度 | alpha 需要调整 | 出现 Dead ReLU 时 |
| PReLU | 负区间斜率可学习 | 自适应负梯度 | 参数变多,易过拟合 | 大数据量图像任务 |
| ELU | 负区间指数平滑 | 负区间平滑但不恒为 1 | 计算成本高 | 需要更稳定训练时 |
| SELU | 带 scale 的 ELU | 自归一化 | 条件严格 | 顺序全连接网络 |
| GELU | x * Phi(x) | 平滑非饱和 | 计算成本略高 | Transformer、BERT |
| Swish/SiLU | x * sigmoid(x) | 平滑非单调 | 算子支持待确认 | CNN、部分 Transformer |
| Mish | x * tanh(softplus(x)) | 平滑非单调 | 计算成本高 | 图像探索实验 |
4. 用 PyTorch 写一个深层网络实验,实测不同激活函数
4.1 准备环境与依赖
这个实验只需要 Python 和 PyTorch。建议 Python 3.9 以上,PyTorch 2.x 都可以。安装方式会根据操作系统和 CUDA 环境不同而不同,在只做 CPU 实验时,直接安装 CPU 版即可。
pip install torch如果是 GPU 环境,需要先到 PyTorch 官网确认与 CUDA 版本匹配的安装命令。实验本身不依赖 GPU,CPU 完全可以跑通。
4.2 构造等深度 MLP
核心思路是保持网络深度、宽度、优化器完全一致,只替换隐藏层激活函数。这样能比较公平地观察激活函数对训练过程的影响。
import torch import torch.nn as nn import torch.optim as optim class DeepMLP(nn.Module): def __init__( self, activation_factory, in_features=32, hidden_features=64, out_features=1, depth=12, use_bn=False, ): super().__init__() layers = [nn.Linear(in_features, hidden_features)] if use_bn: layers.append(nn.BatchNorm1d(hidden_features)) layers.append(activation_factory()) for _ in range(depth - 1): layers.append(nn.Linear(hidden_features, hidden_features)) if use_bn: layers.append(nn.BatchNorm1d(hidden_features)) layers.append(activation_factory()) layers.append(nn.Linear(hidden_features, out_features)) self.net = nn.Sequential(*layers) def forward(self, x): return self.net(x)这里使用activation_factory而不是直接传入一个激活函数实例,原因是要为每一层创建独立的激活模块。如果复用同一个 PReLU 实例,它的可学习参数会在多层之间共享,这通常不是实验期望的行为。
构造一个回归任务:输入是 32 维随机向量,输出是sin(x @ w),这样数据本身存在非线性关系,适合检验网络拟合能力。
torch.manual_seed(0) x = torch.randn(1024, 32) weight_true = torch.randn(32, 1) y = torch.sin(x @ weight_true).squeeze() def train_activation(activation_factory, depth=12, epochs=300): model = DeepMLP(activation_factory=activation_factory, depth=depth) optimizer = optim.Adam(model.parameters(), lr=1e-3) loss_fn = nn.MSELoss() model.train() for epoch in range(1, epochs + 1): optimizer.zero_grad() pred = model(x) loss = loss_fn(pred.squeeze(), y) loss.backward() total_grad_norm = 0.0 for param in model.parameters(): if param.grad is not None: total_grad_norm += param.grad.norm().item() ** 2 grad_norm = total_grad_norm ** 0.5 optimizer.step() if epoch % 50 == 0: print( f"epoch={epoch}, " f"loss={loss.item():.6f}, " f"grad_norm={grad_norm:.6f}" ) return loss.item()运行方式很简单:
python compare_activation.py在相同深度下,Sigmoid 很可能出现 loss 下降极慢、梯度范数极小的情况;ReLU、GELU、Swish 这类激活函数的收敛速度通常更快。但要注意,这个结果和随机种子、深度、学习率密切相关,不能直接当作普适结论。做对比实验时,应该固定所有其他变量,只改激活函数。
4.3 输出层激活函数必须与任务匹配
隐藏层激活函数和输出层激活函数是两套逻辑。隐藏层负责非线性表示,输出层负责把网络输出映射到任务目标空间:
- 回归任务:输出层通常不加激活函数,直接输出连续值;
- 二分类:输出层可用 Sigmoid,或更推荐直接输出 logits 配合
BCEWithLogitsLoss; - 多分类:输出层可用 Softmax,但训练时直接使用
CrossEntropyLoss时,最后一层保持原始 logits 即可,不需要手动加 Softmax。
不要在隐藏层使用 Softmax。Softmax 会把输出归一化成一组和为 1 的概率,这会让梯度在多层之间传递时出现明显抑制,训练稳定性反而更差。
5. 验证一套指标:怎么判断换激活函数确实有用
5.1 梯度范数检验
换完激活函数后,第一步要看梯度范数,而不是只看最终损失。梯度范数能直接反映反向传播链路是否通畅。
def print_layer_grad_norm(model): for name, param in model.named_parameters(): if param.grad is not None and "weight" in name: print(name, param.grad.norm().item())观察两个阶段:训练初期和训练中期。训练初期,前面层的梯度如果比后面层小几个数量级,说明梯度过早衰减。比如某层梯度是 1e-12 级别,那这层参数基本不会更新,网络再深也没有意义。
梯度范数也不宜过大。如果训练初期就出现 1e10 以上,说明存在梯度爆炸风险,需要先做梯度裁剪或降低学习率。
5.2 训练曲线和验证曲线
激活函数的效果不能只看训练集上的最终 loss,还要看训练曲线形态。同一个 epoch 数下,loss 下降更快,说明优化效率提升;loss 波动更小,说明训练更稳定。
验证集曲线用来判断泛化能力。如果训练 loss 很低但验证 loss 很高,问题是过拟合,和激活函数关系不大。如果训练 loss 本身不降,问题更可能是优化链路不通,这时候换激活函数才可能有效。
建议每轮训练都记录 train loss、val loss、grad_norm,三个指标一起观察。单独看其中一个,很难定位问题。
5.3 稳定性和可复现性清单
做激活函数 A/B 实验时,最容易出现的问题是结果不可复现。同一个实验跑两遍,损失曲线差异很大,结论自然不可靠。
可复现实验至少需要满足:
- 固定
torch.manual_seed; - 固定数据划分,训练集验证集不要每次随机生成;
- 固定优化器、学习率、batch size、深度和宽度;
- 同一组配置至少跑 3 个不同随机种子,记录均值或中位数;
- 不要把一次偶然的好结果当成确定结论。
这套清单在调试深层网络时非常实用。不管是换激活函数、换初始化还是换归一化层,都应该按这个标准执行。
6. 常见问题与排查路径:换完激活函数,网络还是不行怎么办
6.1 大量神经元输出恒为 0:Dead ReLU
现象是网络中间层输出大量 0,loss 长时间不下降。
常见原因是 ReLU 负区间导数始终为 0,学习率过大或权重初始化不合适,导致激活输入长期落在负区间。一旦神经元进入死亡状态,没有梯度能把它拉回来。
检查方式可以统计激活输出中 0 的比例:
def dead_ratio(module, x): with torch.no_grad(): out = module(x) return (out <= 0).float().mean().item()处理方式:换 Leaky ReLU、PReLU 或 ELU;降低学习率;改用 Kaiming 初始化;必要时增加批归一化。
6.2 深层网络输出全 0 或全部相同
现象是不同样本的输出非常接近,甚至完全相同。
常见原因是梯度消失导致前面层几乎没有更新;也可能是 batch size 过小导致批归一化统计不稳定;还可能最后一个线性层输出被某些极端值压到了几乎不变的位置。
检查方式是打印每层激活输出的均值和方差:
def print_activation_stats(model, x): with torch.no_grad(): h = x for i, layer in enumerate(model.net): h = layer(h) if hasattr(h, "mean") and hasattr(h, "std"): print(i, layer.__class__.__name__, h.mean().item(), h.std().item())处理方式:减少网络深度;加入残差连接;加入批归一化;换非饱和激活函数;检查输入数据是否需要标准化。
6.3 训练中途出现 NaN 或梯度爆炸
现象是 loss 在某一步突然变成nan,或者梯度范数在训练过程中不断暴涨。
常见原因是学习率过大、数据没有标准化、激活函数和权重初始化组合不当。
检查方式是在optimizer.step()之前打印梯度范数:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)如果梯度范数已经达到 1e8 以上,需要先处理梯度爆炸问题。推荐做法是配合梯度裁剪,同时降低学习率,并检查输入数据是否做了标准化。
6.4 训练不收敛但梯度正常
梯度范数正常不代表网络一定能收敛。可能原因还包括:
- 损失函数和输出层不匹配;
- 学习率过大导致 loss 震荡;
- 网络深度过大,信息在前向传播中已经失真;
- 数据本身没有归一化,特征尺度差异太大。
排查时可以按这个顺序走:
| 问题现象 | 检查顺序 | 常用处理 |
|---|---|---|
| loss 波动大 | 学习率是否过大 | 降低学习率或使用学习率调度 |
| loss 长期不降 | 输出层和损失函数是否匹配 | 二分类用 BCE,多分类用 CE,回归用 MSE |
| 加深后变差 | 梯度是否有效回传 | 加残差、BN,或更换激活函数 |
| 训练结果不稳定 | 随机种子和数据划分是否固定 | 固定 seed,多次实验取中位数 |
7. 实践选型清单:不同任务和环境下怎么选激活函数
7.1 快速选型表
大多数场景不需要追求复杂激活函数。下面这张表偏工程向,可以作为起点:
| 场景 | 推荐隐藏层激活 | 理由 | 注意点 |
|---|---|---|---|
| 通用 MLP/CNN 分类 | ReLU | 简单高效,默认可用 | 防止 Dead ReLU |
| 深层网络出现大量死亡神经元 | Leaky ReLU 或 ELU | 负区间保留梯度通道 | 参数需要验证 |
| Transformer / 预训练模型 | GELU 或 SwiGLU | 平滑性好,主流验证充分 | 训练推理实现要一致 |
| 回归任务 | ReLU 或 GELU | 中间层保持非线性 | 输出层不加激活 |
| 二分类输出层 | Sigmoid | 输出可解释为概率 | 训练时推荐 BCEWithLogitsLoss |
| 多分类输出层 | Softmax | 输出概率分布 | 训练时推荐 CrossEntropyLoss |
需要说明的是,从传统神经网络到 Transformer,激活函数从 ReLU 演进为 GELU、SwiGLU,本质仍然是解决深层训练中的梯度流动和表达能力问题,并没有脱离本文讨论的核心逻辑。
7.2 学习环境与生产环境的差异
学习环境里,激活函数可以随意更换,重点看训练曲线和梯度。生产环境则不同,激活函数会影响推理算子、量化、内存占用和延迟。
ReLU 在绝大多数推理框架中都有专门优化。GELU 在不同框架中可能存在近似实现差异,模型从 PyTorch 导出到 ONNX、TensorRT 时,要检查算子是否被正确映射。Mish 这类计算较复杂的激活函数,在部分推理设备上支持度有限,性能可能是瓶颈。
另一个容易忽略的点是训练和推理的一致性。训练时使用了某个自定义近似实现,导出模型时也必须保持一致,否则线上推理结果会和离线测试有偏差。
7.3 组合使用:激活函数无法单独解决所有深度问题
深层网络要达到稳定可训练的状态,通常需要把多个机制组合在一起:
- 残差连接,给梯度提供跨层通路;
- 批归一化或层归一化,稳定每层输入分布;
- 合理初始化,避免激活输入落在饱和区;
- 学习率调度和梯度裁剪,控制更新步长;
- 监控梯度范数和激活分布,把优化问题暴露在早期。
这几件事做得越稳,激活函数的效果越能体现出来。与其在多个激活函数之间频繁切换,不如固定一种主流激活函数,先把归一化、初始化和残差结构搭好,再针对具体瓶颈替换激活函数做实验。
8. 从激活函数开始,继续深入网络可训练性
8.1 下一步可以做什么实验
如果希望把激活函数的理解落到代码上,可以按下面的顺序实验:
- 手动实现 ReLU 和它的导数,理解 forward 和 backward 的关系;
- 用 10