ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

全连接神经网络实战MNIST:从训练到部署的完整指南

全连接神经网络实战MNIST:从训练到部署的完整指南 简介这份资源面向想从零理解神经网络底层原理的Python学习者与入门开发者围绕MNIST手写数字识别任务提供一套可直接运行的全连接神经网络DNN实践材料帮助读者摆脱只调库、不懂内部计算的困境。压缩包共6个文件约29.81MB以4个csv数据文件、1个py程序脚本和1个txt模型文件为主csv承载6万条原始手写数字样本及不同规模的训练子集py文件为基于numpy手写实现的全连接网络代码txt则是已训练完成、准确率达93.21%的模型参数。目前已有2944人学习下载。借助这套材料读者可对照理论推导复现前向传播、反向传播与梯度更新全过程用现成模型直接验证识别效果也可自行调整网络结构、学习率与迭代次数做对比实验在真实数据上体会调参与排错思路适合作为深度学习入门阶段的理论与代码对照练习素材。1. 全连接神经网络跑 MNIST为什么它至今仍是入门首选很多人第一次接触深度学习绕不开的就是 MNIST 数据集和全连接神经网络。你拿到一份「完整程序代码 已经训练好的模型」最想知道的其实是三件事这玩意儿能不能直接跑起来、跑出来准不准、我改哪里能让它更好。MNIST 是 6 万张 28×28 的手写数字灰度图配上标签 0 到 9全连接网络就是最朴素的「输入层 → 隐藏层 → 输出层」结构。它不涉及卷积、不涉及注意力所有像素拉平成一个 784 维向量直接喂进去。正因为简单它成了验证环境、验证代码、验证训练流程是否跑通的最佳试验田。这篇文章就围绕一份可复现的全连接方案把数据加载、模型定义、训练循环、模型保存与加载、以及已经训练好的模型怎么用一步步拆开讲清楚。适合刚配好环境想跑通第一个模型的人也适合想拿它当基线做对比的熟手。2. 全连接网络在 MNIST 上到底学了什么从 784 维输入到 10 类输出2.1 为什么 MNIST 上的全连接网络仍然值得认真对待MNIST 常被调侃为「深度学习界的 Hello World」但调侃归调侃全连接网络在这个数据集上能稳定做到 97% 到 98% 的测试准确率这个数字背后有实实在在的信息量。28×28 的图片拉平后是 784 个像素值每个像素取值 0 到 255归一化到 0 到 1 之间。全连接层做的事情本质是对这 784 个输入做加权求和再经过非线性激活。隐藏层的作用是把原始像素空间映射到一个更容易线性分割的空间输出层再把这个空间里的特征映射到 10 个类别上。这里有个容易被忽略的点全连接网络对像素位置是「一视同仁」的。它不知道相邻像素在空间上有关联也不知道把图片平移几个像素后还是同一个数字。这意味着它学到的更多是「哪些像素位置亮起来大概是什么数字」这种统计规律而不是真正的形状特征。所以它在 MNIST 上表现不错但换到稍微复杂一点的数据集比如 Fashion-MNIST 或者带旋转、缩放的手写数字准确率就会明显往下掉。理解这个边界比单纯跑出一个高准确率更重要。我一般会建议把全连接网络在 MNIST 上的表现当作一个「基线」来看待。后面你上卷积网络、上数据增强、上更复杂的结构都可以拿这个基线做对比。如果连全连接网络都跑不到 97% 以上那大概率是数据加载、归一化或者训练循环哪里出了问题而不是模型本身不行。2.2 网络结构怎么定层数、激活函数、Dropout 的取舍一个典型的全连接网络结构是这样的输入 784 维接一个 256 或 512 维的隐藏层用 ReLU 激活再接一个 10 维的输出层用 Softmax 转成概率。隐藏层可以堆两层但 MNIST 这个任务上一层隐藏层已经能到 97% 以上两层能到 98% 左右再深收益很小反而容易过拟合。激活函数选 ReLU 基本是默认选项它计算快、梯度不容易消失。输出层用 Softmax 是因为要做 10 分类需要输出每个类别的概率。损失函数配交叉熵这是分类任务的标准组合。Dropout 要不要加我的经验是如果只训练几十个 epoch不加 Dropout 也能到 98%但如果训练轮数多或者隐藏层维度给得很大比如 1024加一个 0.2 到 0.5 的 Dropout 能明显缓解过拟合。Dropout 的位置一般放在隐藏层激活之后、下一层之前。权重初始化也有讲究。全连接层默认用 PyTorch 的 Kaiming 初始化或者 Xavier 初始化就行不要手动全零初始化否则所有神经元学到的梯度一样网络退化成线性模型。偏置项初始化为 0 没问题。优化器选 Adam 还是 SGDAdam 收敛快学习率设 1e-3 通常就能跑得不错SGD 需要调学习率和动量但最终泛化可能略好。入门阶段我建议先用 Adam把流程跑通再换 SGD 对比。2.3 用 PyTorch 搭一个最小可跑的全连接网络下面这段代码定义了一个两层全连接网络输入 784隐藏层 512输出 10。代码里加了 Dropout 和 ReLU结构清晰可以直接抄。import torch import torch.nn as nn import torch.nn.functional as F class FCNet(nn.Module): def __init__(self, hidden_dim512, dropout0.3): super(FCNet, self).__init__() # 第一层全连接784 - hidden_dim self.fc1 nn.Linear(28 * 28, hidden_dim) # Dropout 层训练时随机置零防止过拟合 self.dropout nn.Dropout(dropout) # 第二层全连接hidden_dim - 10 类 self.fc2 nn.Linear(hidden_dim, 10) def forward(self, x): # 把 28x28 的图片拉平成 784 维向量 x x.view(-1, 28 * 28) # 第一层线性变换 ReLU 激活 x F.relu(self.fc1(x)) # Dropout 只在训练时生效推理时自动关闭 x self.dropout(x) # 输出层不做 Softmax因为 CrossEntropyLoss 内部会做 x self.fc2(x) return x这段代码的关键点有三个。第一x.view(-1, 28 * 28)把 batch 里的每张图拉平-1 表示自动推断 batch size。第二输出层没有加 Softmax因为 PyTorch 的CrossEntropyLoss内部已经包含了 LogSoftmax再加就重复了。第三Dropout 在model.eval()时会自动关闭所以推理时不需要手动改。参数方面hidden_dim设 256 到 1024 都合理512 是个稳妥的中间值。dropout设 0.2 到 0.5太小没效果太大欠拟合。如果发现训练准确率远高于测试准确率就把 dropout 调大或者减少隐藏层维度。3. 数据加载与训练循环把 MNIST 喂进网络的完整流程3.1 MNIST 数据集下载与预处理归一化到底怎么算MNIST 数据集下载在 PyTorch 里是一行代码的事但预处理有几个细节容易翻车。torchvision.datasets.MNIST会自动下载数据到指定目录返回的是 PIL 图片和标签。你需要用transforms.ToTensor()把图片转成张量同时把像素值从 0 到 255 缩放到 0 到 1。归一化要不要做常见做法是用 MNIST 的全局均值和标准差再做一次标准化。MNIST 训练集的像素均值大约是 0.1307标准差大约是 0.3081。用这两个数做标准化能让输入分布更接近标准正态训练更稳定。from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理转张量 标准化 transform transforms.Compose([ transforms.ToTensor(), # 像素值从 0-255 缩放到 0-1 transforms.Normalize((0.1307,), (0.3081,)) # MNIST 全局均值和标准差 ]) # 下载并加载训练集 train_dataset datasets.MNIST( root./data, trainTrue, downloadTrue, transformtransform ) # 下载并加载测试集 test_dataset datasets.MNIST( root./data, trainFalse, downloadTrue, transformtransform ) # 训练集打乱顺序测试集不需要打乱 train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size1000, shuffleFalse)batch_size设 64 或 128 都常见太小训练慢太大显存吃紧且可能影响泛化。测试集的 batch_size 可以设大一点比如 1000因为推理不需要梯度速度快。shuffleTrue只对训练集开测试集打乱没有意义。注意如果你已经有一份「已经训练好的模型」那数据预处理必须和训练时完全一致。均值和标准差对不上推理结果会直接崩掉。这是最常见的翻车点之一。3.2 训练循环epoch、学习率、损失曲线怎么看训练循环的骨架是前向传播算输出算损失反向传播算梯度优化器更新参数。下面是一个完整的训练函数包含训练和测试两个阶段。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model FCNet(hidden_dim512, dropout0.3).to(device) optimizer optim.Adam(model.parameters(), lr1e-3) criterion nn.CrossEntropyLoss() def train(model, device, train_loader, optimizer, epoch): model.train() # 切换到训练模式Dropout 生效 for batch_idx, (data, target) in enumerate(train_loader): data, target data.to(device), target.to(device) optimizer.zero_grad() # 清空上一轮梯度 output model(data) # 前向传播 loss criterion(output, target) # 算交叉熵损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 if batch_idx % 100 0: print(fEpoch {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}] Loss: {loss.item():.4f}) def test(model, device, test_loader): model.eval() # 切换到推理模式Dropout 关闭 test_loss 0 correct 0 with torch.no_grad(): # 推理不需要算梯度省显存 for data, target in test_loader: data, target data.to(device), target.to(device) output model(data) test_loss criterion(output, target).item() pred output.argmax(dim1) # 取概率最大的类别 correct pred.eq(target).sum().item() test_loss / len(test_loader) acc 100. * correct / len(test_loader.dataset) print(fTest Loss: {test_loss:.4f}, Accuracy: {acc:.2f}%) return acc学习率设 1e-3 是 Adam 的常用起点。如果损失曲线震荡厉害降到 1e-4如果下降太慢升到 3e-3 试试。epoch 数一般 10 到 20 就能到 98% 左右再多收益递减。看损失曲线有个经验训练损失持续下降但测试损失开始上升就是过拟合的信号该加 Dropout 或者早停。训练损失和测试损失都居高不下那是欠拟合该加层或者加宽。3.3 模型保存与加载已经训练好的模型怎么用训练完之后保存模型有两种方式保存整个模型结构加参数或者只保存参数字典。推荐后者因为加载时更灵活也不依赖原始类定义的位置。# 保存参数字典 torch.save(model.state_dict(), fcnet_mnist.pth) # 加载时先实例化同样的结构再加载参数 model_loaded FCNet(hidden_dim512, dropout0.3).to(device) model_loaded.load_state_dict(torch.load(fcnet_mnist.pth, map_locationdevice)) model_loaded.eval() # 推理前必须切 eval 模式 # 用加载好的模型做单张图片推理 with torch.no_grad(): sample test_dataset[0][0].unsqueeze(0).to(device) # 加 batch 维度 output model_loaded(sample) pred output.argmax(dim1).item() print(f预测数字: {pred})map_locationdevice是为了在 CPU 上加载 GPU 训练的模型时不报错。unsqueeze(0)是给单张图片加一个 batch 维度因为模型 forward 期望输入是[batch, 784]。推理前必须调model.eval()否则 Dropout 还在随机置零结果每次都不一样。如果你拿到的是一份「已经训练好的模型」文件先确认它的结构定义和保存方式。如果是state_dict你需要知道隐藏层维度和 Dropout 配置才能正确加载。对不上就会报size mismatch错误。4. 避坑与排查全连接网络跑 MNIST 最常见的 5 个翻车现场4.1 现象测试准确率卡在 10% 左右损失不下降原因通常是标签和输出对不上或者数据没有归一化。MNIST 标签是 0 到 9 的整数CrossEntropyLoss期望的 target 就是类别索引不需要 one-hot。如果你手动做了 one-hot 又传给CrossEntropyLoss就会出错。另一个可能是学习率太大梯度爆炸损失直接变成 NaN。解决检查 target 的形状是不是[batch]而不是[batch, 10]。学习率从 1e-3 开始试不要一上来就 0.1。归一化确认均值和标准差用的是 MNIST 的全局值。4.2 现象训练准确率 99%测试准确率只有 95%这是典型的过拟合。全连接网络参数多MNIST 训练集只有 6 万张很容易记住训练样本。测试集是另外 1 万张模型没见过表现就掉下来。解决加 Dropout把隐藏层维度从 1024 降到 256 或 512或者加 L2 正则化。早停也有效看测试损失开始上升就停。4.3 现象加载已经训练好的模型时报 size mismatch原因是你实例化的网络结构和保存参数时的结构不一致。比如保存时隐藏层是 512加载时写成了 256fc1.weight的形状对不上。解决确认保存时的hidden_dim和dropout配置。如果只有模型文件没有配置说明可以打印state_dict的键和形状来反推。state torch.load(fcnet_mnist.pth, map_locationcpu) for k, v in state.items(): print(k, v.shape)看到fc1.weight是[512, 784]就知道隐藏层是 512。4.4 现象推理结果每次都不一样原因是没有调model.eval()。训练模式下 Dropout 会随机置零神经元导致每次前向传播结果都不同。推理时必须切到 eval 模式Dropout 和 BatchNorm 都会切换到推理行为。解决推理前加model.eval()并用with torch.no_grad()包住前向传播。4.5 现象GPU 显存不够报 CUDA out of memory原因通常是 batch_size 设太大或者没有用torch.no_grad()做推理。全连接网络本身不大但如果你在测试时还保留计算图显存会持续累积。解决把 batch_size 从 256 降到 64 或 32。测试阶段一定用with torch.no_grad()。如果还是不够把模型放到 CPU 上跑MNIST 的全连接网络在 CPU 上推理也很快。5. 把准确率从 98% 推到 99%几个我反复用过的调参习惯全连接网络在 MNIST 上到 98% 不难但想再往上走一个点就需要一些细调。下面这几个习惯是我反复验证过的按优先级排。第一学习率调度。Adam 配 1e-3 跑 10 个 epoch 后把学习率降到 1e-4 再跑 5 个 epoch通常能涨 0.3 到 0.5 个百分点。用torch.optim.lr_scheduler.StepLR就能做step_size5, gamma0.5是个稳妥配置。第二集成。训练 3 到 5 个结构相同但初始化不同的模型推理时把它们的 Softmax 输出平均取 argmax。这个方法在 MNIST 上能把准确率推到 99% 以上代价是推理时间翻几倍。如果只是学习目的跑两个模型集成一下就能看到效果。第三数据增强。MNIST 本身是居中的手写数字随机平移几个像素、随机旋转小角度能增加样本多样性。用transforms.RandomAffine(degrees10, translate(0.1, 0.1))加在 ToTensor 之前。注意增强只加在训练集测试集保持原样。第四权重衰减。Adam 的weight_decay设 1e-4 到 1e-5能轻微抑制过拟合。不要设太大否则欠拟合。第五验证集划分。从训练集里切 5000 张出来做验证用验证集选超参数而不是用测试集。测试集只在最后评估一次这样得到的准确率才是可信的。# 学习率调度示例 scheduler optim.lr_scheduler.StepLR(optimizer, step_size5, gamma0.5) for epoch in range(1, 16): train(model, device, train_loader, optimizer, epoch) test_acc test(model, device, test_loader) scheduler.step() # 每个 epoch 后更新学习率最后说一个我自己的习惯每次跑实验把配置、随机种子、最终准确率记在一个文本文件里。MNIST 实验跑得快很容易一天跑十几个配置不记下来回头就忘了哪个配置对应哪个结果。随机种子固定住torch.manual_seed(42)这样结果可复现。踩过几次坑之后我现在连数据加载的 shuffle 种子都会固定。希望帮到你。本文还有配套的精品资源点击获取
返回列表