news 2026/9/20 19:40:42

MNIST机器学习工程实践:5种模型对比与参数化调优

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MNIST机器学习工程实践:5种模型对比与参数化调优

简介:本资源是合肥工业大学2023年《机器学习》课程大作业的完整实现方案,面向计算机、电子信息工程、数学等专业的本科生,聚焦MNIST手写数字识别这一经典任务,系统对比逻辑回归、SVM、FNN、CNN与RNN五类主流模型的建模思路与性能表现。压缩包共26个文件,含7个Jupyter Notebook(含各模型训练与可视化脚本)、7个Python源码(支持参数化调参与模块复用)、2份PDF文档(含实验报告与技术说明)、2个LaTeX源文件(便于学术排版)、1个Excel训练日志及原始MNIST数据集二进制文件等,整体12.56MB,结构清晰、即开即用。已有709人学习下载。读者可直接运行全部代码获取准确结果,所有脚本均经实测验证,注释详尽、逻辑分层明确,并附有预览图、模型对比分析与PyQt简易交互界面,兼顾教学理解与工程实践需求。

1. 这不是又一个 MNIST 教程:它是一份可直接进实验室复现的机器学习工程快照

你打开 Jupyter Notebook,mnist_fnn.ipynbmodel.fit()一跑,准确率 98.3% 跳出来——但你真正需要的,从来不是“能跑通”,而是“为什么这个 FNN 层结构比另一个少一层却更稳?”“SVM 的C=1.0gamma='scale'是怎么从网格搜索里筛出来的?”“CNN 中nn.Conv2d(1, 32, kernel_size=3)的 32 个通道数,是拍脑袋定的还是受内存/梯度流约束推导的?”这份合肥工业大学 2023 年《机器学习》大作业,本质是一套带工程上下文的模型对比实验包:5 种模型(Logistic Regression、SVM、FNN、CNN、RNN)全部基于原始 MNIST raw 数据构建,参数全部显式暴露、可调、有注释,每个.ipynb文件都包含数据加载→预处理→建模→训练→评估→可视化完整链路,且所有结果(含train_loss.xlsx中每 epoch 的 loss/acc 曲线)均已实测存档。它不教“什么是过拟合”,而是用svm.ipynbGridSearchCV的 36 种(C, gamma)组合告诉你,当C=10时验证集 acc 突然跌 1.2%,背后是支持向量数量激增导致的泛化边界收缩。适合正在赶课设 deadline 的本科生,也适合想快速验证某类模型在标准图像分类任务上 baseline 表现的算法工程师。

2. 从 raw 数据到可训练张量:MNIST 预处理链路与参数设计逻辑

2.1 原始 MNIST 数据结构解析与加载方式选择

项目中明确标注MNIST raw,意味着数据未经过torchvision.datasets.MNISTkeras.datasets.mnist的封装,而是直接使用官方提供的train-images-idx3-ubyte.gztrain-labels-idx1-ubyte.gz原始二进制文件。这种做法规避了torchvision在某些镜像源下因证书或路径问题导致的404错误(如热搜词中高频出现的 “torchvision下载mnist会404”),也强制开发者理解数据底层格式。mnist_preview.py是关键入口:它用struct.unpack_from解析 idx 文件头(magic number + image count + rows + cols),再将像素值从uint8归一化至[0,1]区间,并 reshape 为(28,28)单通道矩阵。注意,此处未做中心化(mean=0)或标准化(std=1),因为 Logistic Regression 和 SVM 对输入尺度敏感,而后续 CNN/FNN 模块内部已通过 BatchNorm 或权重初始化补偿——这是参数化编程的典型体现:预处理策略与模型类型强耦合。

提示:mnist_preview.pyshow_sample()函数调用matplotlib.pyplot.imshow(img, cmap='gray')时,必须确保imgnp.ndarray且 dtype 为float32,否则会报TypeError: Invalid shape (28, 28, 3) for image data。原始 raw 数据是单通道,强行转 RGB 会导致维度错乱。

2.2 四种预处理方案的适用场景与代码实现

项目未提供统一预处理函数,而是按模型需求分散实现,这恰恰反映真实工程逻辑。以下是各 notebook 中实际采用的方案及参数依据:

模型类型预处理操作关键参数设计理由
Logistic RegressionStandardScaler(fit on train, transform both)with_mean=True, with_std=TrueLR 对特征尺度极度敏感,StandardScaler保证权重更新方向稳定;with_mean=True消除偏置项干扰
SVMMinMaxScaler(feature_range=(0,1))feature_range=(0,1)SVM 的 RBF 核依赖样本间欧氏距离,归一至[0,1]比标准化更鲁棒,避免异常值拉伸距离尺度
FNN/CNNtorchvision.transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])(0.1307, 0.3081)MNIST 全局均值与标准差实测值,ToTensor()自动完成HWC→CHW转换和uint8→float32归一化,Normalize进一步中心化
RNNReshape(-1, 28)+torch.nn.utils.rnn.pad_sequencebatch_first=TrueRNN 输入需为(seq_len, batch, input_size),将 28×28 图像展平为 28 个长度为 28 的时间步,pad_sequence处理变长序列(虽 MNIST 固长,但保留接口)

对应代码片段(以logistic_regression.ipynb为例):

from sklearn.preprocessing import StandardScaler scaler = StandardScaler(with_mean=True, with_std=True) X_train_scaled = scaler.fit_transform(X_train) # 注意:仅对训练集 fit! X_test_scaled = scaler.transform(X_test) # 测试集仅 transform

fit_transform()transform()的分离是关键——若对测试集也调用fit_transform(),会导致数据泄露(data leakage),模型评估失真。StandardScalerwith_mean=True参数不可省略,否则 LR 的截距项(bias)会因特征均值非零而剧烈震荡。

2.3 数据划分与标签编码的隐含约束

所有模型均采用train_test_split(X, y, test_size=0.2, random_state=42, stratify=y),其中stratify=y保证测试集中 0~9 数字比例与训练集一致,避免类别不平衡影响评估。值得注意的是,y标签在 raw 加载后为np.uint8,但在传入sklearn模型前未做 one-hot 编码(因 LR/SVM 是原生多类分类器),而在 PyTorch 模型(FNN/CNN/RNN)中则通过torch.nn.CrossEntropyLoss自动处理整型标签(该 loss 内部执行 softmax+log+nll,无需手动 one-hot)。这种差异在mnist_fnn_m.ipynbDataset类中体现:

class MNISTDataset(Dataset): def __init__(self, images, labels): self.images = torch.FloatTensor(images).unsqueeze(1) # (N, 1, 28, 28) self.labels = torch.LongTensor(labels) # 注意:dtype=torch.long,非 float! def __getitem__(self, idx): return self.images[idx], self.labels[idx]

torch.LongTensor(labels)是硬性要求:CrossEntropyLoss的 target 必须是LongTensor,若误用FloatTensor会触发RuntimeError: expected scalar type Long but found Float。此细节在文档说明中未强调,但代码已强制校验。

3. 五种模型的实现差异与超参数调试证据链

3.1 Logistic Regression:正则化强度 C 的实证选择

logistic_regression.ipynb中,C参数(正则化强度的倒数)并非默认1.0,而是通过LogisticRegressionCV自动交叉验证确定:

from sklearn.linear_model import LogisticRegressionCV lr_cv = LogisticRegressionCV( Cs=[0.001, 0.01, 0.1, 1, 10, 100], cv=5, solver='lbfgs', max_iter=1000, n_jobs=-1 ) lr_cv.fit(X_train_scaled, y_train) print(f"Best C: {lr_cv.C_[0]:.3f}") # 输出:Best C: 1.000

Cs列表覆盖 6 个数量级,cv=5表示 5 折交叉验证,solver='lbfgs'适配多类且支持 L2 正则。结果C=1.0被选中,说明在 MNIST 上,过强(C<1)或过弱(C>1)的正则化均降低泛化能力。max_iter=1000是必要设置——原始LogisticRegression默认max_iter=100,在高维特征(784 维)下极易不收敛,LogisticRegressionCV继承此参数,必须显式增大。

3.2 SVM:RBF 核的 gamma 与 C 的联合优化

svm.ipynb使用GridSearchCV(C, gamma)进行穷举搜索:

from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV param_grid = { 'C': [0.1, 1, 10, 100], 'gamma': ['scale', 'auto', 0.001, 0.01, 0.1, 1] } grid = GridSearchCV( SVC(kernel='rbf', random_state=42), param_grid, cv=3, scoring='accuracy', n_jobs=-1 ) grid.fit(X_train_scaled, y_train) print(f"Best params: {grid.best_params_}") # 输出:{'C': 10, 'gamma': 'scale'}

gamma='scale'表示gamma=1/(n_features * X.var()),自动适配数据方差;C=10比 LR 的C=1更大,说明 SVM 对误分类容忍度更低,需更强惩罚。cv=3而非5是权衡——SVM 训练耗时随样本量增长极快,X_train_scaled.shape[0]为 48000,5 折会显著拖慢调试周期。n_jobs=-1启用所有 CPU 核心,但需注意GridSearchCV的并行粒度是参数组合,而非单次训练。

3.3 FNN:全连接网络的宽度-深度平衡设计

mnist_fnn.ipynb的网络结构为784 → 128 → 64 → 10,激活函数为ReLU,输出层为LogSoftmax

import torch.nn as nn class SimpleFNN(nn.Module): def __init__(self, input_dim=784, hidden1=128, hidden2=64, num_classes=10): super().__init__() self.fc1 = nn.Linear(input_dim, hidden1) self.fc2 = nn.Linear(hidden1, hidden2) self.fc3 = nn.Linear(hidden2, num_classes) self.relu = nn.ReLU() self.log_softmax = nn.LogSoftmax(dim=1) def forward(self, x): x = x.view(x.size(0), -1) # flatten: (N, 1, 28, 28) → (N, 784) x = self.relu(self.fc1(x)) x = self.relu(self.fc2(x)) x = self.log_softmax(self.fc3(x)) return x

hidden1=128hidden2=64的递减设计,遵循“宽度随深度增加而收缩”的经验法则,防止参数爆炸。view(x.size(0), -1)是关键:PyTorch 的Linear层只接受 2D 输入,必须将(N,1,28,28)展平。若忘记此步,forward会报mat1 and mat2 shapes cannot be multipliedLogSoftmaxNLLLoss配对使用,比Softmax+CrossEntropyLoss数值更稳定。

3.4 CNN:卷积核尺寸与通道数的物理意义映射

mnist_cnn.ipynb的核心结构为:

self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1) # 输入1通道,输出32通道,3x3卷积 self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1) # 输入32通道,输出64通道 self.fc1 = nn.Linear(64 * 7 * 7, 128) # 经过2次2x2 MaxPool,28→14→7 self.fc2 = nn.Linear(128, 10)

kernel_size=3是 MNIST 的最优解:2x2 太小无法捕获笔画交点,5x5 在 28x28 图像上感受野过大易过拟合。padding=1保证卷积后尺寸不变(28→28),配合MaxPool2d(2)实现下采样。3264通道数非随意设定:32是 GPU 显存与特征表达力的平衡点(64*7*7=3136作为 FC 层输入,远小于784的原始维度,体现降维优势)。64*7*7的计算源于28→14→7的尺寸变化,若paddingstride改变,此数值必须重算,否则Linear层维度不匹配。

3.5 RNN:序列建模的图像行优先展开策略

rnn_minst.ipynb将图像视为 28 行、每行 28 像素的序列:

class MNISTRNN(nn.Module): def __init__(self, input_size=28, hidden_size=128, num_layers=2, num_classes=10): super().__init__() self.rnn = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 ) self.fc = nn.Linear(hidden_size, num_classes) def forward(self, x): # x shape: (N, 1, 28, 28) → (N, 28, 28) [N, seq_len, input_size] x = x.squeeze(1).permute(0, 2, 1) # squeeze channel, then permute to (N, 28, 28) _, (hn, _) = self.rnn(x) # hn: (num_layers, N, hidden_size) out = self.fc(hn[-1]) # 取最后一层隐状态 return out

squeeze(1)移除通道维,permute(0,2,1)(N,28,28)转为(N,28,28)—— 行优先(row-major)展开,使每行像素作为时间步输入。batch_first=True确保输入/输出张量为(N, seq_len, features),符合直觉。dropout=0.2施加于 LSTM 层间,缓解 RNN 的梯度消失,但不在输入层或输出层,这是 RNN 特有的正则化位置。

4. 模型评估与结果可视化:从 accuracy 到 loss 曲线的诊断价值

4.1 多维度评估指标的计算与解读

项目在README.md中声明“内含运行结果”,但实际评估逻辑分散在各 notebook 的末尾。以mnist_cnn.ipynb为例,除基础accuracy_score外,还计算混淆矩阵与分类报告:

from sklearn.metrics import classification_report, confusion_matrix y_pred = model.predict(X_test_tensor) # 假设 model 有 predict 方法 print(classification_report(y_test, y_pred)) cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(10,8)) sns.heatmap(cm, annot=True, fmt='d', cmap='Blues') plt.title('Confusion Matrix') plt.ylabel('True Label') plt.xlabel('Predicted Label')

classification_report输出 precision/recall/f1-score per class,揭示模型弱点:例如数字5的 recall 仅 0.92,说明模型漏检较多5;而precision为 0.95,说明预测为5的样本中 95% 确实是5。混淆矩阵热力图中,53列的高值表明5易被误判为3(笔画相似),这比单一 accuracy 更具指导意义。

4.2train_loss.xlsx的结构解析与训练过程诊断

train_loss.xlsx是项目独特资产,包含 5 个 sheet(对应 5 个模型),每 sheet 有epoch,train_loss,val_loss,train_acc,val_acc5 列。以 CNN sheet 为例,第 100 行显示val_loss=0.021,val_acc=0.983,但第 95 行val_loss=0.020更低——说明模型在 95 epoch 已达最佳,后续训练发生过拟合。这不是 bug,而是正常现象val_loss的波动由 mini-batch 随机性引起,需观察滑动平均趋势。若train_loss持续下降而val_loss持续上升,则确认过拟合;若两者同步上升,则是学习率过高或数据污染。

4.3 可视化技巧:用mnist_preview.py定位错误样本

mnist_preview.py不仅用于预览,更是调试利器。其plot_wrong_predictions(model, X_test, y_test, n_samples=10)函数可生成错误分类样本图:

def plot_wrong_predictions(model, X_test, y_test, n_samples=10): preds = model.predict(X_test) wrong_idx = np.where(preds != y_test)[0][:n_samples] fig, axes = plt.subplots(2, 5, figsize=(12,6)) for i, idx in enumerate(wrong_idx): ax = axes[i//5, i%5] ax.imshow(X_test[idx].reshape(28,28), cmap='gray') ax.set_title(f'True:{y_test[idx]}, Pred:{preds[idx]}') ax.axis('off') plt.tight_layout() plt.show()

运行此函数,可直观发现:SVM 将模糊的7误判为1(因7缺少横杠,形似1),而 CNN 将带噪点的4误判为9(因 CNN 对局部纹理更敏感)。这种定位能力远超accuracy数值,直接指向数据增强方向(如对7添加横杠扰动,对4添加闭合环扰动)。

5. 参数化编程的落地实践:如何安全地修改模型与超参数

5.1 修改 CNN 通道数与学习率的安全边界

mnist_cnn_m.ipynb是参数化版本,所有可调参数集中于顶部:

# 可配置参数区 BATCH_SIZE = 64 LEARNING_RATE = 0.001 NUM_EPOCHS = 20 CONV1_OUT_CHANNELS = 32 # ← 修改此处 CONV2_OUT_CHANNELS = 64 # ← 修改此处 FC_HIDDEN_SIZE = 128 # ← 修改此处

修改CONV1_OUT_CHANNELS时,必须同步调整fc1的输入维度:若设为16,则fc1应改为nn.Linear(16 * 7 * 7, FC_HIDDEN_SIZE)LEARNING_RATE=0.001是 Adam 优化器的常用起点,若改为0.01train_loss会在前 5 epoch 剧烈震荡,需配合torch.optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)逐步衰减。NUM_EPOCHS=20是平衡效果与耗时的选择——实测epoch=15val_acc=0.982epoch=25val_acc=0.983仅提升 0.001,但训练时间增加 33%。

5.2 替换优化器与损失函数的兼容性检查

若将mnist_fnn.ipynb中的optim.Adam替换为optim.SGD,必须添加momentum=0.9并增大learning_rate0.01

# 原始 Adam optimizer = optim.Adam(model.parameters(), lr=0.001) # 替换为 SGD(需调整) optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9) scheduler = optim.lr_scheduler.ReduceLROnPlateau( optimizer, mode='min', factor=0.5, patience=3 ) # 当 val_loss 3 epoch 不降,lr 减半

SGD收敛更慢但泛化稍好,momentum=0.9缓解震荡。ReduceLROnPlateau是必需配套——SGD对学习率更敏感,固定lr易陷入局部最优。mode='min'对应val_loss,若监控val_acc则需mode='max'

5.3 快速验证新模型结构的三步法

当想尝试 ResNet 结构时,不必重写全部代码,利用现有框架快速验证:

  1. 继承SimpleFNN,在__init__中添加残差块:
    class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1) self.bn2 = nn.BatchNorm2d(out_channels) self.shortcut = nn.Sequential() if in_channels != out_channels: self.shortcut = nn.Conv2d(in_channels, out_channels, 1) def forward(self, x): out = F.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) return F.relu(out)
  2. 替换mnist_cnn.ipynbforward函数,插入ResidualBlock(32,32)
  3. 复用原有train_loop,仅修改model = ResNetModel(),运行train_loss.xlsx自动生成新曲线。
    此方法将验证周期压缩至 1 小时内,避免从零搭建环境的风险。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 19:40:22

大文件传输提速:局域网共享与网线直连实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 19:39:36

DNV船舶入级规则RU-SHIP 2023-07:版本解读与设计送审实战指南

简介&#xff1a;面向船舶设计、审图与入级检验人员&#xff0c;DNV船级社2023年7月发布的《船舶分类规则》RU-SHIP PDF提供了系统化的船舶入级技术依据。该版规则在2022版框架基础上&#xff0c;重点更新法定认证证书签发、船旗国授权条件处理等要求&#xff0c;并涵盖总则、材…

作者头像 李华
网站建设 2026/9/20 19:38:22

VMware Workstation虚拟机创建超详细指南(17.6.4版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 19:37:56

acme.sh + 阿里云DNS API:SSL证书自动续期完全指南

你还在每 90 天手动续一次 SSL 证书吗&#xff1f;如果是&#xff0c;我猜你已经设了好几个“证书还有 XX 天过期”的闹钟&#xff0c;甚至可能哪天手一抖忘了&#xff0c;第二天就迎来浏览器那个刺眼的红色警告页面。我自己手上十几个域名跑着 HTTPS 服务&#xff0c;以前每逢…

作者头像 李华