首页 / 资讯中心 / 文章详情

PyTorch MNIST实战:透彻理解深度学习数据流与模型训练

PyTorch MNIST实战:透彻理解深度学习数据流与模型训练 ★ FEATURED ARTICLE
做深度学习图像分类入门MNIST 是一个绕不开的经典任务。28x28 的灰度手写数字10 个类别6 万张训练图1 万张测试图数据规模小、任务标准明确、不需要大规模算力几乎是被无数人验证过无数遍的“标准跑道”。我第一次完整跑通模型训练、看到准确率一点点爬上去用的也是这个任务。对于想彻底搞清楚“数据流是怎么流动的、模型训练又是怎么闭环的”的人来说MNIST 算得上成本最低的完整样例。这篇文章会把从原始数据到模型评估的整条链路拆开讲附带可以直接运行的完整代码以及一张数据流转过程的文字流程图。适合刚接触深度学习、想动手实操 PyTorch 的初学者也适合那些模型能跑通但总觉得“中间过程很黑盒”的读者。很多朋友问我说“MNIST 代码我看得懂但换个数据集就不会改”我想说问题多半出在你没有真正理解数据流。1. 为什么是 MNIST一场恰到好处的入门实战先别急着写代码。MNIST 这个任务最值得研究的不是它 99% 以上的准确率而是它把深度学习里最重要的一条链路完整地串起来了原始图像数据从磁盘加载经过张量化和归一化被分批送入网络网络前向传播得到预测结果损失函数计算误差反向传播求出梯度优化器更新权重最后在测试集上评估效果。这条链路就是所有视觉模型、乃至大部分深度学习模型的骨架。1.1 数据流才是真正的核心很多人学深度学习第一个动作就是上网找模型结构图把卷积、池化、全连接抄下来然后跑通就觉得自己会了。可真到换数据集、改任务的时候往往一头雾水输入尺寸改了哪里要跟着改为什么明明换了图片却报错为什么训练集 loss 很低测试集却很惨这些问题的答案几乎都在“数据流”里。数据流可以理解成一条流水线。原始图片是原材料每一步处理是一个工位。ToTensor 是贴标签Normalize 是校准规格DataLoader 是打包运输模型是加工机床损失函数是质检员反向传播是修正工艺参数。任何一个工位出了问题产线都会出乱子。所以这篇实战我不会一上来堆一个高大上的模型而是带着你看清楚每个环节里 tensor 的形状、范围、语义让你从“代码能跑”变成“知道它为什么能跑”。MNIST 数据流的基本形态是这样的原始图像 (60000, 1, 28, 28) uint8 ↓ ToTensor 像素值转浮点并缩放到 0~1 ↓ Normalize 标准化接近标准正态分布 ↓ DataLoader 按 batch_size64 打包并随机打乱 ↓ 模型前向 卷积/池化/全连接 → logits (64, 10) ↓ CrossEntropyLoss loss 标量 ↓ 反向传播 计算每个参数的梯度 ↓ 优化器 更新权重1.2 这整条链路里到底发生了什么把上面的流程再翻译成训练循环其实就是五件事取一批数据、前向算损失、反向算梯度、更新参数、定期评估。整个过程重复几百次之后模型里的权重会被慢慢调整到能让损失变小的方向最后在测试集上表现出很高的识别正确率。MNIST 之所以适合入门是因为它的每个环节都非常“标准”。图片通道数只有 1 个尺寸统一是 28x28类别数是 10数据量不大单卡 CPU 都能在几分钟内完成训练。遇到问题也容易被隔离如果准确率上不去要么数据预处理有问题要么模型结构有问题要么训练策略有问题排查起来边界清晰。等你把这条链路彻底吃透再去看 CIFAR-10、ImageNet 子集、或者自己业务里的图片数据会发现只是数据格式和网络深度变了核心流程没有本质变化。2. 环境准备与数据加载把 28x28 的图像变成张量训练环境我推荐直接用 PyTorch因为它的张量体系和自动求导机制非常适合理解数据流。CPU 版本完全够跑 MNIST没有 GPU 也能在几十秒到几分钟内完成一个 epoch。2.1 安装与引入创建虚拟环境后安装核心依赖pip install torch torchvisiontorchvision 会提供 MNIST 数据集的下载接口和常用的图像变换工具。脚本开头通常这样写import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torch.utils.data import DataLoader这里注意torchvision.transforms 里的 transform 并不是模型的一部分它是“数据预处理流水线”作用在图片进入模型之前。2.2 Dataset 与 Transform 的配合加载 MNIST 的标准写法如下transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set torchvision.datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_set torchvision.datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform )这里有两个关键点需要解释清楚。第一ToTensor() 做了什么。原始 MNIST 图片读进来是一个 PIL Image像素值范围是 0 到 255通道顺序是 HWC高度、宽度、通道数据类型是 uint8。ToTensor() 会把它转成 float32 的张量把像素值除以 255 缩放到 0 到 1 之间并且把维度顺序调整为 CHW也就是 (通道, 高度, 宽度)。MNIST 是灰度图只有 1 个通道所以每个样本的张量形状是 (1, 28, 28)。这一步如果省掉模型接收到的可能就是形状完全不对的 PIL 对象后面所有维度计算都会崩。第二Normalize 为什么用这两个数字。0.1307 和 0.3081 是 MNIST 训练集所有像素的统计均值和标准差。Normalize 做的事情是 (x - mean) / std经过这一步后数据分布大致落在标准正态分布附近。这样做的意义是让模型各个层的输入分布相对稳定梯度更新时不容易出现一个方向被某个大数值特征主导的情况。实际训练中不做归一化也能收敛但收敛速度和稳定性通常明显不如归一化后。这两个数不是随便拍脑袋定的而是从数据集统计出来的经验值拿到其他数据集上不能直接套用。另外downloadTrue 会在首次运行时把数据集下载到 root 目录。如果下载失败常见原因是网络访问不到目标地址解决办法是手动下载数据文件放到 ./data/MNIST/Processed 目录下或者换一个能正常访问网络的环境来获取。重复运行不会重复下载因为 PyTorch 会检查文件是否已经存在。2.3 DataLoader 的批量与打乱逻辑模型训练不是一张图一张图地看而是按批处理。DataLoader 就是把 Dataset 按指定方式打包的调度器。train_loader DataLoader( datasettrain_set, batch_size64, shuffleTrue, num_workers2, drop_lastFalse ) test_loader DataLoader( datasettest_set, batch_size256, shuffleFalse, num_workers2 )batch_size64 是一个很常见的起步值。它意味着每个批次包含 64 张图前向传播时输入张量形状是 (64, 1, 28, 28)。batch 太小梯度更新频繁但噪声大batch 太大每个 epoch 内更新次数变少而且显存占用高。MNIST 单张图片极小64 或 128 都是合理区间。shuffleTrue 只应该用于训练集。它的作用是让每个 epoch 中样本的顺序不同避免模型学习到数据顺序里的虚假规律。测试集不需要打乱因为评估时我们希望结果稳定可复现样本顺序不会影响准确率但打乱会让调试时难以排查具体样本。num_workers 是读取数据时启用的子进程数量。在 Windows 上如果设成 4 或更高有时会出现程序卡住不动的情况这是多进程数据加载和主程序交互的兼容性问题。稳妥的做法是设成 0 或 2。0 表示使用主进程读取速度稍慢但不容易出问题在 Linux 上可以适当调大。3. 模型构建从全连接到卷积的核心细节MNIST 最简单的模型是全连接网络把 28x28 的像素直接拉平成 784 维向量然后接几个线性层。这种模型能轻松达到 97% 左右的准确率但很难突破到 99% 以上。卷积模型通过局部感受野和参数共享能更有效地提取空间特征所以入门实战我推荐直接写一个轻量 CNN。3.1 输入张量的维度推导模型设计最怕的就是维度算错。只要理解了每一层输入输出形状的变化任何模型结构都难不倒你。以 batch_size64 为例输入形状从 (64, 1, 28, 28) 开始网络层输入形状输出形状Conv2d(1, 32, 3, padding1)(64, 1, 28, 28)(64, 32, 28, 28)ReLU(64, 32, 28, 28)(64, 32, 28, 28)MaxPool2d(2)(64, 32, 28, 28)(64, 32, 14, 14)Conv2d(32, 64, 3, padding1)(64, 32, 14, 14)(64, 64, 14, 14)ReLU(64, 64, 14, 14)(64, 64, 14, 14)MaxPool2d(2)(64, 64, 14, 14)(64, 64, 7, 7)Flatten(64, 64, 7, 7)(64, 64773136)Linear(3136, 10)(64, 3136)(64, 10)卷积层尺寸变化公式是 out (in 2p - k) / s 1。上面用的卷积核 k3、padding1、stride1所以输出尺寸和输入尺寸一致。池化层核大小为 2、步长为 2输出尺寸正好减半。全连接层的输入维度就是上一层所有元素个数也就是 64 个通道乘以 7 再乘以 7等于 3136。这种从输入到输出逐步推导形状的习惯是调试深度学习模型最重要的基本功。很多维度报错本质上就是某个中间层输出和你下一层期望的输入对不上。3.2 一个可复用的简单 CNN 实现我常用的一个轻量结构如下class CNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Dropout(0.25), nn.Linear(64 * 7 * 7, 10), ) def forward(self, x): x self.features(x) x x.flatten(1) x self.classifier(x) return x这个模型参数量非常小。第一个卷积层参数是 1x32x3x3 32 320第二个卷积层是 32x64x3x3 64 18496全连接层是 3136x10 10 31370加起来五万左右比很多大模型小了若干个数量级。但它在 MNIST 上跑 5 个 epoch 就能到 99% 附近充分说明对于简单任务模型复杂度并不需要很高。这里有两个细节需要注意。第一Dropout 放在了全连接层之前它的作用是在训练时随机把一部分神经元输出置为零强迫网络不依赖某几个节点从而缓解过拟合。注意 Dropout 只在训练阶段生效模型切换为 eval 模式后它会自动变成恒等映射。第二最后一层 Linear 输出的是 10 个类别的 logit也就是未经过 softmax 的原始分数。这是因为后面搭配的交叉熵损失函数内部已经包含了 log_softmax 的计算不需要你手动再加一层 softmax。3.3 损失函数与优化器选择的逻辑分类任务最常用的损失函数是 CrossEntropyLoss。它的数学形式是交叉熵直接衡量预测概率分布和真实标签分布的差异。PyTorch 的 CrossEntropyLoss 比较特殊输入是原始 logits 和整数标签它内部会先算 softmax再算负对数似然。为什么不用 MSE 做分类MSE 适合回归任务它假设误差服从高斯分布而且平方项会让错误很大的样本产生超大梯度。分类任务的输出是类别概率分布用交叉熵在数学上更自然梯度也更平稳。如果非要用 MSE你得先把标签转成 one-hot 向量最后一层加 softmax训练速度和稳定性通常都不如交叉熵。优化器我推荐先选 Adam学习率用 0.001。Adam 对学习率的敏感度低自带自适应调节非常适合入门阶段快速看到结果。等你想冲更高准确率可以换成带动量的 SGD配合学习率衰减策略往往能再涨零点几个百分点。MNIST 上这个差距不明显但迁移到更复杂任务时会发现调优空间更大。4. 完整训练流程把数据流闭环跑通这一节给出一套可以直接运行的完整代码。它包含数据加载、模型定义、训练循环和评估函数。脚本设计上刻意做了模块化方便你以后改成自己的数据集。4.1 可直接运行的完整代码import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torch.utils.data import DataLoader class CNN(nn.Module): def __init__(self): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Dropout(0.25), nn.Linear(64 * 7 * 7, 10), ) def forward(self, x): x self.features(x) x x.flatten(1) x self.classifier(x) return x def train_one_epoch(model, loader, criterion, optimizer, device): model.train() total_loss 0 correct 0 total 0 for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * images.size(0) pred outputs.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy def evaluate(model, loader, criterion, device): model.eval() total_loss 0 correct 0 total 0 with torch.no_grad(): for images, labels in loader: images images.to(device) labels labels.to(device) outputs model(images) loss criterion(outputs, labels) total_loss loss.item() * images.size(0) pred outputs.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) avg_loss total_loss / total accuracy correct / total return avg_loss, accuracy def main(): device torch.device(cuda if torch.cuda.is_available() else cpu) transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,)) ]) train_set torchvision.datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) test_set torchvision.datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2) test_loader DataLoader(test_set, batch_size256, shuffleFalse, num_workers2) model CNN().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) epochs 5 for epoch in range(epochs): train_loss, train_acc train_one_epoch( model, train_loader, criterion, optimizer, device ) test_loss, test_acc evaluate(model, test_loader, criterion, device) print(fEpoch {epoch 1}/{epochs} | fTrain Loss {train_loss:.4f} | Train Acc {train_acc:.4f} | fTest Loss {test_loss:.4f} | Test Acc {test_acc:.4f}) if __name__ __main__: main()在 CPU 上这段代码每个 epoch 大约需要几十秒整体跑完大概几分钟。输出会像这样Epoch 1/5 | Train Loss 0.1731 | Train Acc 0.9471 | Test Loss 0.0641 | Test Acc 0.9804 Epoch 2/5 | Train Loss 0.0598 | Train Acc 0.9816 | Test Loss 0.0431 | Test Acc 0.9867 Epoch 3/5 | Train Loss 0.0424 | Train Acc 0.9869 | Test Loss 0.0341 | Test Acc 0.9894 Epoch 4/5 | Train Loss 0.0316 | Train Acc 0.9904 | Test Loss 0.0285 | Test Acc 0.9906 Epoch 5/5 | Train Loss 0.0246 | Train Acc 0.9924 | Test Loss 0.0263 | Test Acc 0.99124.2 训练循环里每个步骤在做什么train_one_epoch 里面有几个容易出错的地方值得单独说。model.train() 和 model.eval() 必须配对使用。它们切换的是模型里的 BatchNorm、Dropout 这类在训练和推理时行为不同的层。很多人把模型定义完后直接丢进循环训练没问题一到评估就发现结果忽高忽低多半就是忘了调用 model.eval()。这个函数并不会关闭梯度计算它只是改变模块的运行模式。optimizer.zero_grad() 必须在 backward 之前调用。PyTorch 的梯度是累加的如果不清零下一个 batch 的梯度会叠加上当前 batch 的梯度更新出来的参数方向就会错。我见过不少新手第一次写训练循环时漏掉这行代码结果 loss 曲线一路乱跳就是因为梯度一直在累加。loss.backward() 是自动求导的核心。它会从 loss 出发沿着计算图反向传播给每个需要梯度的参数填上 grad 属性。注意这一步只是“计算梯度”还没修改任何权重。真正更新权重的是 optimizer.step()。理解这个顺序再看“为什么每次迭代要清空梯度”就一目了然。torch.no_grad() 用在评估阶段目的是告诉 PyTorch 不要构建计算图。评估时我们不需要反向传播也不需要保存中间变量关闭梯度跟踪可以大大减少内存占用并且避免误操作把评估数据也计入梯度。inside这个上下文里所有 tensor 操作都不会记录梯度模型输出也不会生成计算图。epoch 的计算方式也值得一说。训练集有 60000 张图batch_size64所以每个 epoch 有 60000/64 937.5也就是 938 个 batch。五轮下来模型一共做了大约 4690 次梯度更新。这个数字说明 MNIST 任务要学到好效果需要的更新次数其实非常少。4.3 超参数与训练节奏的经验值MNIST 的网络小数据干净超参数容错率很高。但如果你想让训练过程更稳有几个经验值可以记住。学习率 0.001 搭配 Adam基本不会翻车。如果看到 loss 在第一个 epoch 徘徊不降先别急着改网络考虑把学习率降到 0.0003或者检查数据归一化是不是没做好。学习率 0.01 对 MNIST 的小网络也不是不能跑但刚开始学习率过大容易造成 loss 震荡。等模型训练到后期想让准确率继续往上顶可以每 3 个 epoch 把学习率乘以 0.1效果比一直用固定学习率稍好。Dropout 的 0.25 不是拍脑袋。它既能让网络具备一定的抗过拟合能力又不至于因为随机失活太多而欠拟合。MNIST 本身数据量大且分布均匀过拟合问题不像小数据集那么严重所以这个值足够用了。如果你后面切换到自己的小数据集可以把 Dropout 调到 0.5 再对比效果。5. 模型评估从准确率到混淆矩阵的完整视角训练完模型后的评估环节往往是初学者最容易忽略的部分。看到测试准确率 99% 就高高兴兴收工但其实还可以从更多角度理解模型到底学到了什么。5.1 准确率是怎么算出来的准确率代码核心逻辑很简单correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images images.to(device) labels labels.to(device) outputs model(images) pred outputs.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) accuracy correct / totaloutputs.argmax(dim1) 会沿着类别维度找出每个样本得分最高的那个类别编号。pred 的形状和 labels 一样都是 (batch_size,) 的一维整数张量。比较后得到布尔张量sum() 统计出正确个数再用 .item() 把张量值取成普通 Python 数字避免叠加时生成计算图。这个评估逻辑很简单但它揭示了一个关键点测试集在训练过程中绝对不能参与权重更新。如果让模型见过测试数据评估结果就失去了意义。MNIST 官方划分已经帮你分好训练集和测试集但真实项目里很多人自己划分数据时不小心把重复样本同时放进训练集和测试集导致准确率虚高。5.2 混淆矩阵看模型到底错在哪准确率只是一个总分它掩盖了很多细节。混淆矩阵是逐类别统计真实标签和预测标签的对应关系能直接看出模型把哪些类别混在一起。用 sklearn 很简单from sklearn.metrics import confusion_matrix all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: images images.to(device) outputs model(images) preds outputs.argmax(dim1).cpu() all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) print(cm)MNIST 上常见的混淆集中在 4 和 9、3 和 8、7 和 1 这几对数字之间。原因很直观这些数字的局部笔画差异小光照和书写风格不同时人眼都经常看错。如果你发现某一类的召回率明显偏低可以从训练集中把这类样本和目标类别的样本抽样出来对比看看。有时候模型并没有“错”而是某些训练样本本身的标注就模糊不清。把错误可视化以后你会发现很多 9 写得确实像 4。这种分析在真实项目里特别重要因为它能帮你判断是模型能力不足还是数据质量本身就存在上限。5.3 带数据形状的完整流程图这里把整个数据流画得更细每个环节都标出了数据形状。放在你调试代码时对照着看能省大量时间。训练阶段 原始图片 (60000, 1, 28, 28, uint8) → ToTensor()像素值转 float32缩放到 0~1 → Normalize()减去 0.1307 再除以 0.3081 → DataLoader(batch_size64, shuffleTrue, num_workers2) → 单批数据 (64, 1, 28, 28) → Conv2d(1→32) ReLU (64, 32, 28, 28) → MaxPool2d(2) (64, 32, 14, 14) → Conv2d(32→64) ReLU (64, 64, 14, 14) → MaxPool2d(2) (64, 64, 7, 7) → flatten (64, 3136) → Linear(3136, 10) (64, 10) logits → CrossEntropyLoss loss 标量 → loss.backward() 计算每个参数的梯度 → optimizer.step() 更新权重 评估阶段 测试图片 (10000, 1, 28, 28) → 同样的 transform → DataLoader(batch_size256, shuffleFalse) → model.eval() torch.no_grad() → logits (256, 10)最后一个 batch 可能不足 256 → argmax(dim1) 得到预测类别 → 和真实标签比较 → 准确率 / 混淆矩阵这张图里每一行都对应代码中的一个环节。遇到报错时先定位是在哪一行再看该环节输入输出形状是否匹配问题通常就能快速锁定。6. 常见问题与排查技巧实录新手避坑必看训练 MNIST 的过程中我把新手最容易踩的坑整理成一张速查表每个问题后面附上我的排查思路。症状常见原因处理方式loss 不下降学习率过大或数据未归一化学习率降到 1e-3检查输入范围loss 变成 NaN学习率过大 / 梯度爆炸学习率降到 1e-4加梯度裁剪训练准确率高但测试准确率低过拟合增大 Dropout、减少 epoch、加数据增强维度报错全连接输入尺寸算错用上文的形状表逐层推导Windows 下程序卡住num_workers 过大设为 0 或 2报错 device mismatch模型和数据不在同一设备对数据和模型统一调用 .to(device)准确率总是 10% 附近标签错位或 shuffle 异常抽样打印图片和标签检查对齐6.1 loss 不下降或变成 NaN这个问题的出现频率非常高。先确认一件事输入数据的范围对不对。ToTensor 之后应该在 0 到 1 之间Normalize 之后应该大致落在 -1 到 1 附近甚至有少量超过 2 的值也算正常。如果某次误操作把输入变成了 0 到 255 的 float 直接喂给模型那么 loss 大概率会爆炸。学习率过大是另一个主要原因。Adam 默认 0.001 在 MNIST 上很稳但如果你把学习率调到 0.1很容易出现训练初期 loss 直接变成 NaN。遇到这种情况第一动作不是改网络而是把学习率降到 0.0001 重新跑。如果问题依旧就要检查数据里是否存在异常值。6.2 训练集准确率高但测试集低一截MNIST 上因为数据量够大过拟合通常不严重但如果你把模型在训练集上跑了几十个 epoch又没加任何正则化测试准确率反而可能回退。我最开始跑 MNIST 时试过 20 个 epoch 不调参结果测试准确率只有 98.7%比 5 个 epoch 的 99.1% 还低这就是典型的过拟合。解决办法优先级很清晰减少 epoch 数或者在模型里加 Dropout再或者对训练图片做一点轻微的随机变换。MNIST 因为图片本身已经居中和标准化做复杂数据增强反而可能破坏数字结构简单的随机平移和轻微旋转就足够。6.3 维度报错全连接层输入尺寸算错新手最常遇到的报错是“mat1 and mat2 shapes cannot be multiplied”。这通常出现在 Linear 层的输入维度和模型期望维度不一致时。我的经验是不要靠猜直接在 forward 里临时打印形状def forward(self, x): x self.features(x) print(x.shape) # 调试用训完删掉 x x.flatten(1) print(x.shape) x self.classifier(x) return x把每个中间层形状打印一遍对照上文的维度表马上就能知道哪里不对。等你熟练了甚至可以心算出整个网络每个阶段的 shape这是深度学习调试的基本功。6.4 DataLoader 在 Windows 上卡住不动这个坑很典型。代码在 Linux 上跑得好好的拿到 Windows 上运行程序启动后卡在第一个 epoch 加载数据不动。八成是 num_workers 设置的问题。Windows 的多进程数据加载和 Linux 行为不太一样num_workers 设过大时某些 Python 环境下子进程会反复启动失败或主进程阻塞。最省事的解决方式是把 num_workers 设为 0用主进程读取数据。MNIST 数据很小速度损失完全可接受。另外如果你在完整脚本中使用了多进程数据加载一定要把执行入口放在 ifname main: 里否则 Windows 上会出现无限递归式地启动子进程。6.5 评估阶段忘记切模式导致结果不对我见过一个很奇怪的现象训练过程一切正常但测试集准确率打印出来忽高忽低有时候还比训练集高很多。排查后发现是评估时没有调用 model.eval()模型的 Dropout 还在随机失活导致输出不稳定。这个问题对于带 Dropout 的网络尤其明显因为推理时 Dropout 本来应该关闭。正确的做法是评估函数里确保先 model.eval()再进入 torch.no_grad() 循环。训练函数里再切换回 model.train()。这套切换非常机械但每次都要做养成肌肉记忆就好了。6.6 数据与模型设备不一致用 GPU 训练时如果模型通过 model.to(device) 放到了 GPU但数据还是 CPU tensor那么前向传播会直接报错。常见写法是每个 batch 都要执行 images images.to(device) 和 labels labels.to(device)。也有人习惯在 DataLoader 里加一个自定义 collate 函数自动把数据搬到设备但新手阶段老老实实写在循环里最不容易出错。我在实际使用中还有一个习惯把所有需要设备的地方集中到 main 函数开头用 device 变量统一管理。这样之后想切换 CPU/GPU只需要改这一处。最后再分享一个我操作里很受益的小习惯。写 MNIST 这类入门脚本我不会一上来就追求高准确率而是先打印每一条数据流里的 tensor shape。把每层输出形状打印一遍比任何教程都管用。我当年就是靠这句“shape debug”消除了对深度学习模型的恐惧。等你能闭着眼睛说出一个 batch 的数据经过每一步时的形状再去看真实项目里的数据集你会发现自己已经不会只盯着网络结构打转了而是会自然地去想数据有没有对齐预处理有没有做对评估时是不是只看了 top-1 准确率。这种感觉就是入门到进阶的分水岭。
阅读完成 · 觉得有帮助?
咨询建站