资讯动态

手写两层神经网络:PyTorch反向传播与训练调试实战

发布时间:2026/10/14 15:25:20 来源:尧图企业网站定制
1. 为什么我建议先手写一个“两层网络”而不是直接去调现成模型如果你已经在 PyTorch 里跑过几个别人写好的模型突然要你自己从零实现一个最简单的两层神经网络你会发现真正困难的地方不是矩阵乘法而是那些文档里不会写清楚的细节参数怎么初始化、梯度怎么往回传、损失曲线炸了到底是哪里出了问题。我自己带新人的时候几乎都会让他们先写一个两层 MLP原因很简单这个网络足够小所有中间结果都可以打印出来检查又足够完整前向传播、反向传播、优化器、数据迭代器一个都跑不掉。把这两层的每一行代码都吃透后面再看卷积、Transformer 那些复杂结构至少不会觉得底层机制是黑盒。这个项目看起来是一个“玩具”实际用途却一点都不玩具。我在实际工作中经常用它做三件事第一验证某个新写的自定义层是否正确比如自己实现了一个奇怪的激活函数直接塞进两层网络里对比数值梯度第二作为新数据集上的快速基线模型先跑一个两层 MLP看损失能不能降、准确率大概在什么水平再去上复杂模型第三排查环境问题CUDA 装没装好、数据加载有没有 bug用两层网络几分钟就能验证。这篇文章不会只贴一段能跑的代码就结束我会把整个实现过程拆开从数学记号到反向传播的手动推导从 nn.Module 封装到训练循环的细节再到我踩过的坑完整过一遍。你可以把它当成一个“面向调试的两层网络实现指南”也可以当成 PyTorch 内部机制的入门解剖。无论哪种用途看完之后你都应该能不看任何参考代码独立写出一个可以训练、可以验证、可以扩展的两层网络。2. 动手之前先把两层网络的结构和数学记号定死经常有人一上来就写torch.nn.Linear(784, 100)然后接ReLU代码能跑但问他一轮迭代里每个张量的 shape 是怎么变的、梯度更新的时候每一层分别更新了什么就说不清了。所以我建议先在一张纸上把这个结构画出来再动手写代码。2.1 我们说的“两层网络”到底指什么这里要澄清一个容易混淆的点。很多教材里说“两层神经网络”指的是一个隐藏层加一个输出层参数只有两组从输入到隐藏层的权重矩阵 W1以及从隐藏层到输出层的权重矩阵 W2。也有人把输入层也算进去叫“三层网络”这两种叫法在讨论的时候很容易打架。我后面统一按“两层 一个隐藏层 一个输出层”来写代码里变量名也用W1、b1、W2、b2免得产生歧义。网络的前向过程可以写成这样输入x维度是[batch_size, input_dim]隐藏层线性变换z1 x W1 b1其中W1维度是[input_dim, hidden_dim]b1维度是[hidden_dim]隐藏层激活a1 relu(z1)输出层线性变换z2 a1 W2 b2其中W2维度是[hidden_dim, output_dim]b2维度是[output_dim]如果是分类任务后面还可以接softmax或者直接算交叉熵损失。我选择隐藏层大小hidden_dim 64输入维度看数据集输出维度也看任务。做 MNIST 手写数字分类就是input_dim 784、output_dim 10做回归任务就是output_dim 1。这个结构的核心思想是先用一个线性变换把原始特征映射到隐空间再用非线性激活函数引入非线性能力最后再用一个线性变换映射到输出空间。2.2 为什么隐藏层要用非线性激活函数如果去掉激活函数两层线性变换叠在一起(x W1 b1) W2 b2最终就等于一个线性变换因为x (W1 W2) ...可以合并成一个权重矩阵。这样以来无论堆多少层模型表达能力都和单层线性模型一样根本没法处理非线性问题。ReLU 这个函数实现简单max(0, x)梯度在正区间是 1负区间是 0计算开销极小收敛速度在大多数场景下比 Sigmoid 快所以隐藏层我默认用它。这里有一个新手经常忽略的点ReLU 在负区间梯度是 0意味着如果某次更新后某个神经元对所有输入都输出负数那它的梯度永远是 0这个神经元就“死”了后续再也不会恢复。这就是所谓的“神经元死亡”问题。解决思路包括用 Leaky ReLU、控制学习率、做合理的初始化。在两层网络里只要梯度下降的步长不是特别离谱死亡现象不会太严重但你要知道它的存在后面排查损失曲线不下降时这是一条重要的线索。2.3 损失函数和评估指标先分开想清楚分类任务我一般用交叉熵损失。PyTorch 里torch.nn.CrossEntropyLoss()已经帮我们把 softmax 和交叉熵合并在一起做了所以模型的最后一层不要手动加 softmax直接输出原始 logits 就好。很多人第一次写的时候会在最后一层加softmax然后发现损失一直不对劲原因是CrossEntropyLoss内部已经做了 log_softmax外面再套一层 softmax 并不会出错但会在数值上损失一点精度更重要的是容易让代码语义混乱。回归任务则直接用MSELoss也就是均方误差。评估指标是另一回事分类看准确率回归看 R2 或平均绝对误差。损失函数用于梯度下降评估指标用于人类判断模型好坏两者可以不同不要混在一起。3. 第一版实现不用 nn.Module手动写前向和反向传播很多人习惯一上来就用nn.Linear和loss.backward()这些自动微分机制当然好用但如果从来没用“手动算梯度”的方法训练过一次网络你对反向传播的理解就会始终隔着一层。所以第一版我故意不用nn.Module只用基础张量操作和autograd手动写梯度代码会啰嗦一点但你能看到每一层的梯度是怎么算出来的。3.1 准备数据用 MNIST 的一个子集就够了先准备数据。完整 MNIST 有 6 万张训练图片两层网络在 CPU 上跑也很快但为了方便调试我习惯只取一部分数据来验证代码正确性。下面的代码用torchvision下载 MNIST然后把图像展平成向量再做标准化。import torch import torchvision import torchvision.transforms as transforms transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_dataset torchvision.datasets.MNIST( root./data, trainTrue, transformtransform, downloadTrue ) test_dataset torchvision.datasets.MNIST( root./data, trainFalse, transformtransform, downloadTrue ) # 这里截取前 10000 条训练数据加快调试速度 train_dataset torch.utils.data.Subset(train_dataset, range(10000)) test_dataset torch.utils.data.Subset(test_dataset, range(2000)) train_loader torch.utils.data.DataLoader( train_dataset, batch_size128, shuffleTrue ) test_loader torch.utils.data.DataLoader( test_dataset, batch_size256, shuffleFalse )这里有两个细节值得说明。一是Normalize((0.1307,), (0.3081,))这两个数分别是 MNIST 数据集的均值和标准差提前算好了直接填进去效果比只做 [0,1] 归一化更好因为标准化后每个像素的数值分布在 0 附近有利于梯度下降收敛。二是Subset截断数据只是为了让你在 CPU 上也能快速迭代代码。等到代码验证没问题再把完整数据集放进去。3.2 初始化参数不能全零也不能太大按照前面的记号参数有W1, b1, W2, b2。权重初始化的原则是“打破对称性”如果所有权重初始化为 0那么同一层的所有神经元会完全一样反向传播时它们的梯度也完全相同网络就退化成了每层只有一个有效神经元的线性模型。所以权重要用随机数初始化且数值不能太大否则输入进激活函数后容易落在饱和区梯度很小。这里我用 PyTorch 自带的均匀初始化。input_dim 28 * 28 # 784 hidden_dim 64 output_dim 10 # 使用 Kaiming 均匀初始化适合配合 ReLU 使用 W1 torch.randn(input_dim, hidden_dim) * torch.sqrt(2.0 / input_dim) b1 torch.zeros(hidden_dim) W2 torch.randn(hidden_dim, output_dim) * torch.sqrt(2.0 / hidden_dim) b2 torch.zeros(output_dim) # 开启梯度追踪 W1.requires_grad_(True) b1.requires_grad_(True) W2.requires_grad_(True) b2.requires_grad_(True)这里我用了 He 初始化也就是标准差为sqrt(2 / fan_in)的正态分布随机数。为什么是 2 而不是 1因为 ReLU 在负区间会把一半的激活值置零输出信号的方差会减小把初始权重方差稍微放大一点可以保持信号在前向传播中不萎缩。这个细节在两层网络里影响可能没那么明显但如果你想自己实现更深的网络初始化策略对收敛速度的影响会非常大。3.3 前向传播写成普通函数不用nn.Linear的情况下前向传播就是矩阵乘法和激活函数的堆叠。注意 PyTorch 中二维矩阵乘法用对应的是torch.matmul。b1和b2在广播机制下会自动扩展到整个 batch所以不需要手动expand但你要理解广播的规则[batch, hidden] [hidden, output]得到[batch, output]偏置向量[output]会按行广播到每个样本上。def forward(x): z1 x W1 b1 a1 torch.relu(z1) z2 a1 W2 b2 return z2 def predict(x): logits forward(x) return logits.argmax(dim1)这里不直接返回预测值而是先返回 logits因为后面计算损失时需要用到。如果你只是想快速看准确率predict函数里用argmax取出概率最大的类别即可。对于 MNIST 这种 10 分类任务logits 的维度是[batch_size, 10]argmax(dim1)得到每个样本预测的数字。3.4 手动计算反向传播从损失到每一层参数的梯度这是整个实验最核心的部分。虽然在 PyTorch 里你永远不需要手动写反向传播但推导一遍之后你会明白框架到底帮你做了什么。这里我推导的链条是loss - z2 - a1 - z1 - W1/b1。先说输出层。用CrossEntropyLoss时损失对 logitsz2的梯度有一个非常简洁的形式softmax(z2) - y_onehot其中y_onehot是样本真实标签的 one-hot 编码。这个结论很多文章直接给出但推导过程值得理解交叉熵损失是-log(softmax(z2)[y])对它求导后除了真实类别对应的项之外其他项都抵消了最后正好剩下softmax(z2) - y_onehot。拿到dL/dz2之后往前面传就是链式法则。z2 a1 W2 b2所以dL/dW2 a1^T (dL/dz2)dL/db2 dL/dz2在 batch 维度上求和dL/da1 (dL/dz2) W2^T。为什么要转置因为前向传播是a1 W2你希望反向传播时梯度形状能和参数一致。简单记法如果前向是Y X W那么dW X^T dYdX dY W^T。这个规则适用于任何线性层非常实用。接下来是隐藏层。a1 relu(z1)ReLU 的导数是分段函数输入大于 0 时导数为 1小于等于 0 时导数为 0。所以dL/dz1 dL/da1 * (z1 0)这里的(z1 0)是布尔张量转成浮点张量。再往下dL/dW1 x^T (dL/dz1)dL/db1 dL/dz1在 batch 维度求和输入 x 不需要梯度因为我们只是训练参数。用代码实现整套更新逻辑可以写成下面这样我故意选用了标准的梯度下降更新方式而不是调用torch.optim目的是让你看清楚一个更新步骤到底在做什么criterion torch.nn.CrossEntropyLoss() def train_step_sgd(x, y, lr0.1): # 前向 logits forward(x) loss criterion(logits, y) # 反向先手动计算 dloss/dz2 softmax_z2 torch.softmax(logits, dim1) y_onehot torch.eye(output_dim)[y] dloss_dz2 softmax_z2 - y_onehot # 输出层梯度 dloss_dW2 a1_cache.T dloss_dz2 dloss_db2 dloss_dz2.sum(dim0) dloss_da1 dloss_dz2 W2.T # 隐藏层梯度 dloss_dz1 dloss_da1 * (z1_cache 0).float() dloss_dW1 x.T dloss_dz1 dloss_db1 dloss_dz1.sum(dim0) # 更新参数标准梯度下降 with torch.no_grad(): W1 - lr * dloss_dW1 b1 - lr * dloss_db1 W2 - lr * dloss_dW2 b2 - lr * dloss_db2有一个实现上的小问题上面的代码里z1_cache和a1_cache是哪里来的如果直接复制这段代码你会发现在forward函数里没有保存中间结果。所以要么你改造forward让它把z1和a1存下来要么干脆用一个类来管理缓存。个人建议如果只是做一遍手动梯度推导直接把它封装成一个类把forward里算出来的z1, a1作为成员变量保存然后再写backward。这其实就是后来所有深度学习框架中“计算图 自动微分”的雏形。3.5 用数值梯度验证你写的手动反向传播是否正确手写反向传播最容易出错而且错得悄无声息。如果梯度算错了有时不会立刻让损失爆炸而是会让模型收敛得非常慢。所以我强烈建议在正式训练之前先用数值梯度验证一下每个参数的梯度是否正确。原理很简单对某个参数 θ给一个小扰动 ε用中心差分近似导数def numerical_gradient(param, x, y, eps1e-5): grad torch.zeros_like(param) flat_param param.view(-1) flat_grad grad.view(-1) for i in range(flat_param.numel()): old flat_param[i].item() flat_param[i] old eps loss1 criterion(forward(x), y) flat_param[i] old - eps loss2 criterion(forward(x), y) flat_param[i] old flat_grad[i] (loss1 - loss2) / (2 * eps) return grad在两层网络里参数量是784*64 64*10差不多 5 万多个逐参数数值梯度在 CPU 上要跑几分钟不算太久完全可以等。将数值梯度和你手写反传的梯度对比误差在1e-5量级就说明梯度是对的。这一步做完你对“梯度到底对不对”就有底了后面调模型时才不会总是怀疑自己的反向传播写错了。4. 改用 nn.Module 重写更简洁但需要注意参数管理方式手动版本验证完计算逻辑之后就可以切换到更工程化的实现方式了。用nn.Module的最大价值是帮你管理参数、缓冲区和子模块比如model.parameters()可以直接拿给优化器优化model.train()/model.eval()会切换 Dropout 和 BatchNorm 的状态。两层网络用不用nn.Module影响不大但一旦模型深了手写参数管理就会变成灾难。4.1 封装一个 TwoLayerMLP 类按 PyTorch 的习惯我把模型定义为nn.Module的子类。构造函数传入input_dim, hidden_dim, output_dim在init里创建两个nn.Linear和一个激活函数。前向传播只需要把层串起来代码可读性好也方便以后添加 Dropout 或 BatchNorm。import torch.nn as nn import torch.nn.functional as F class TwoLayerMLP(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super().__init__() self.fc1 nn.Linear(input_dim, hidden_dim) self.relu nn.ReLU() self.fc2 nn.Linear(hidden_dim, output_dim) def forward(self, x): x self.fc1(x) x self.relu(x) x self.fc2(x) return x这里有个容易被忽略的点nn.Linear默认会对权重做 Kaiming 均匀初始化偏置则初始化为 0所以你在init里不需要再手动设置参数。但如果你想用预训练权重或者某种特殊初始化有两种方式一是直接修改model.fc1.weight和model.fc1.bias比如nn.init.xavier_uniform_(model.fc1.weight)二是通过apply递归地初始化所有子模块。我个人建议新手先用默认初始化跑通了再尝试控制初始化方式否则你会分不清模型收敛到底是因为初始化好还是因为网络结构好。4.2 标准训练循环DataLoader、optimizer、loss 三件套用nn.Module重写后训练循环就变得非常统一了几乎任何 PyTorch 项目都是这个骨架构造 DataLoader、构造优化器、遍历每个 epoch、遍历每个 mini-batch、前向、算损失、optimizer.zero_grad()、loss.backward()、optimizer.step()。def train_model(model, train_loader, epochs5, lr0.1): criterion nn.CrossEntropyLoss() optimizer torch.optim.SGD(model.parameters(), lrlr) for epoch in range(epochs): total_loss 0.0 correct 0 total 0 model.train() for images, labels in train_loader: images images.view(images.size(0), -1) # [batch, 784] optimizer.zero_grad() logits model(images) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() * images.size(0) correct (logits.argmax(dim1) labels).sum().item() total labels.size(0) accuracy correct / total avg_loss total_loss / total print(fepoch {epoch 1}, loss: {avg_loss:.4f}, acc: {accuracy:.4f})推进度条不是我关注的重点我只想让你看这几件事。第一optimizer.zero_grad()必须在loss.backward()之前调用否则梯度会累加起来。PyTorch 的反向传播是“累加梯度”到.grad属性上的如果你在一个 batch 迭代中忘了清零相当于用多个 batch 的梯度求和去做一次更新损失曲线就会乱跳。第二images.view(images.size(0), -1)把[batch, 1, 28, 28]的图片张量展平成[batch, 784]视图操作不会复制数据只改变张量的 strides效率很高。第三model.train()放在训练循环开头是一个好习惯现在网络里没有 Dropout 和 BatchNorm影响不大但写成习惯可以避免以后在复杂模型上忘记切换模式。4.3 为什么这里选 SGD 而不是 Adam很多人默认用 Adam因为它在很多任务上表现得更好且不需要精细调学习率。但两层网络作为教学 demo我建议先用带动量的 SGD。原因有两个第一SGD 的更新行为更容易理解梯度下降的几何意义直观第二SGD 加上合适的动量在 MNIST 这种小任务上收敛效果也很好而且更容易观察学习率的影响。如果你用 Adam它的自适应学习率会让很多初始化错误“自动修复”反而掩盖了你对梯度流的理解。如果确实想用 Adam代码只需要改一行optimizer torch.optim.Adam(model.parameters(), lr0.001)。但注意 Adam 的默认学习率是1e-3而 SGD 常常要用0.1这类较大的值。不同的优化器对超参数的敏感度完全不同不能盲目套用。5. 训练中必看的三个关键点维度、学习率与损失曲线解释我自己在跑两层网络时遇到过很多次“代码明明没报错但损失不下降”的情况。这种问题最考验排查能力下面这三个方面是我每次都会先检查的。5.1 张量维度不匹配通过 shape 推导快速定位最常见的维度错误有两类一类是在view时把 batch 维搞丢另一类是矩阵乘法维度对不上。前者通常发生在你从 DataLoader 取数据后忘记展平图像后者的报错信息会长这样RuntimeError: mat1 and mat2 shapes cannot be multiplied (128x64 and 10x10)看到这种报错不要慌直接把每个张量的shape打印出来从后往前推x是什么形状fc1的输出是什么形状fc2的输入应该是什么形状。通常来说你在forward里写清晰的注释和 shape 检查能够在 30 秒内定位问题。我自己的习惯是在模型forward里加一段临时的assert比如assert x.dim() 2, fexpected 2D input, got {x.shape}训练跑通后可以去掉。不要怕 assert 影响性能两层网络这点开销可以忽略不计它帮你提前拦截了大量潜在问题。5.2 学习率过大或过小的表现形态学习率是两层网络里最敏感的超参数。学习率过大损失曲线会在下降过程中突然变成 nan或者在一个很大的值附近反复震荡学习率过小损失会缓慢下降甚至在你设定的 epoch 数内几乎没有变化。我平时调参的顺序是先用一个相对大的学习率试跑 10 个 epoch看损失是否下降如果炸了就按 10 倍递减如果下降太慢就按 3 倍递增。这里给出一个常见学习率范围供参考优化器推荐初始学习率表现特征SGD0.1 ~ 0.010.1 在 MNIST 上通常下降明显0.01 稍慢但稳定SGD Momentum0.01 ~ 0.001动量会加速收敛学习率可以适当调小Adam0.001 ~ 0.00010.001 是默认值大多数情况能 work但上限不高这个表格是我从大量小规模实验里总结出的经验值不是理论推导但作为起始点足够用了。5.3 损失曲线是“先平后降”还是“一直不降”损失曲线有很多形态常见的有三种。第一种是快速下降后趋缓这是正常的训练过程说明网络在逐步拟合数据。第二种是前期持平很久然后突然下降这通常是因为初始化后某些神经元处于 ReLU 死亡区域随着训练推进才慢慢被“激活”平缓期可能持续几十个 iteration。第三种是一直不降这种情况十有八九是梯度计算错误、数据没标准化或者学习率太小。辅助判断梯度是否正确的方法除了我前面说的数值梯度还有一个更轻量的方法检查第一轮迭代中参数的梯度是否与理论符号一致。比如对于 W1梯度应该能反映哪些输入特征对正确分类贡献更大如果你把所有梯度打印出来发现全部为 0那就要检查是不是 ReLU 把所有激活都置零了或者requires_grad设置有问题。6. 踩坑实录隐藏层神经元死亡、准确率卡在 80% 附近的排查过程这一节专门说说我在实现过程中真正遇到过的两个问题以及完整的排查链路。这不是虚构的案例而是我在调试类似项目时反复走过的路径。6.1 隐藏层神经元死亡损失一直不降的元凶某一次我故意用W1 torch.randn(input_dim, hidden_dim) * 0.01这种比较小的初始化和一个很大的学习率 0.5 训练。结果发现损失在前几次迭代中下降到某个值后就不再动了准确率卡在 85% 左右。我一开始怀疑是数据没加载对打印了几张图片发现没问题又怀疑是损失函数写错了损失值本身也不是 nan。后来我检查了训练过程中隐藏层激活值的分布发现一个严重的问题a1中有大量神经元的值始终为 0。原因很简单学习率太大权重在一次更新中发生了大幅变化许多隐藏单元的线性输出z1变成负数ReLU 将这些神经元永久置零。一旦神经元死了它的梯度就是 0之后永远不会被更新网络实际容量下降准确率自然上不去。解决办法是重新初始化模型然后用更合理的步长重新训练。我把学习率从 0.5 降到 0.1并把初始化换成 He 初始化神经元死亡的情况就明显减少了。这个案例提醒我遇到不收敛先检查激活值分布再检查梯度分布而不是凭感觉乱改网络结构。6.2 梯度没有归零导致的“奇怪周期性振荡”另一个印象深刻的问题是损失曲线出现周期性振荡每 100 次迭代左右损失突然飙高一次。第一次遇到的时候我觉得非常困惑因为模型结构没问题、学习率也不算大。后来打印了每个 batch 的梯度范数发现在某些 batch 上梯度特别大。仔细一查原因是optimizer.zero_grad()被放到了loss.backward()之后。在那个位置调用zero_grad等于把刚算好的梯度清空然后用上一步积累的梯度更新参数逻辑完全反了。修复之后周期性振荡立刻消失。这种错误在代码写得很短的时候很容易忽略所以我把“梯度清零必须放在 forward 之前”写进了团队的新手 checkList。6.3 准确率卡住时先检查数据预处理而不是网络结构还有一个常见情况模型训练得很顺利损失一直在降但验证集准确率迟迟不涨。这个问题的根源通常不在网络结构而在数据预处理和标签处理。比如ToTensor()之前忘了Normalize导致输入像素值范围差异很大模型需要更长的时间才能学到有效特征再比如标签没有转成torch.longCrossEntropyLoss内部需要索引操作类型不对时会报错但有时在代码中会被隐式转换掩盖掉。所以我养成了一个习惯从一开始就在脚本里写一个“数据验证”小节专门打印每一批数据的shape、dtype、像素范围和标签分布。不要嫌麻烦这一步省下来的调试时间比想象中多得多。7. 两层网络在真实项目中的延伸从验证工具到基线模型两层网络本身能力有限但在真实工作流里它有不可替代的位置。这里分享几个我常用的延伸方式帮助你把这个小项目从“练习”变成“效率工具”。7.1 用两层网络验证自定义层和自定义激活函数的正确性如果你自己实现了一个新的激活函数、损失函数或初始化方法直接套在一个大模型上很难定位错误。我的做法是写一个极简实验把自定义的计算单元插入到两层网络里和 PyTorch 官方实现或数值梯度做对比误差在可接受范围内认为实现正确。这个方法快速、可靠而且不用依赖复杂环境。举个例子如果你想验证自己写的swish激活函数是否正确可以写一个非常小的循环每次激活函数前向一次、反向一次同时和F.silu的梯度对比。两层网络在这里只是一个承载测试的骨架。7.2 新数据集上的快速基线与数据质量探针接到一个新的二分类或多分类数据集时我通常不会马上设计复杂模型而是先用两层 MLP 跑一版 baseline。如果两层网络都无法让训练损失明显下降那很可能不是模型复杂度不够而是数据质量有问题标签错乱、特征尺度差异太大、或者数据划分有泄漏。在这种情况下复杂模型只会放大问题不会解决根本矛盾。反过来如果两层网络在 5 个 epoch 内就能达到 90% 以上的准确率说明任务本身比较线性可分或者数据比较干净后续设计更复杂模型时应重点关注特征抽象能力而不是把时间耗在费力的数据清洗上。7.3 扩展到更深的网络时保留哪些习惯从两层扩展到三层、四层 MLP再到卷积网络很多经验是相通的同一份代码里尽量让网络模块化每一层都考虑初始化方法训练循环统一封装成函数检查梯度范数是否异常损失曲线一定要可视化。这些习惯一旦在两层网络上养成了后面的模型再复杂也不会手足无措。如果你有兴趣可以在两层网络基础上加一个Dropout层观察过拟合现象也可以改成nn.Sequential来写一个更灵活的前馈网络还可以把CrossEntropyLoss换成F.nll_loss配合log_softmax从数学上体会不同实现方式对数值稳定性的影响。每一层修改都当作一次独立实验保存日志和曲线这样你积累的直觉会越来越准确。最后再分享一个小技巧把完整训练脚本的随机种子固定下来在文件开头加上torch.manual_seed(42)。这样每次运行的结果可复现你在调试时能确认改变是代码引起的还是随机波动引起的。别小看这件事它是我在无数次对比实验里最省心的习惯之一。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑