
简介本资源是合肥工业大学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 Notebookmnist_fnn.ipynb里model.fit()一跑准确率 98.3% 跳出来——但你真正需要的从来不是“能跑通”而是“为什么这个 FNN 层结构比另一个少一层却更稳”“SVM 的C1.0和gammascale是怎么从网格搜索里筛出来的”“CNN 中nn.Conv2d(1, 32, kernel_size3)的 32 个通道数是拍脑袋定的还是受内存/梯度流约束推导的”这份合肥工业大学 2023 年《机器学习》大作业本质是一套带工程上下文的模型对比实验包5 种模型Logistic Regression、SVM、FNN、CNN、RNN全部基于原始 MNIST raw 数据构建参数全部显式暴露、可调、有注释每个.ipynb文件都包含数据加载→预处理→建模→训练→评估→可视化完整链路且所有结果含train_loss.xlsx中每 epoch 的 loss/acc 曲线均已实测存档。它不教“什么是过拟合”而是用svm.ipynb里GridSearchCV的 36 种(C, gamma)组合告诉你当C10时验证集 acc 突然跌 1.2%背后是支持向量数量激增导致的泛化边界收缩。适合正在赶课设 deadline 的本科生也适合想快速验证某类模型在标准图像分类任务上 baseline 表现的算法工程师。2. 从 raw 数据到可训练张量MNIST 预处理链路与参数设计逻辑2.1 原始 MNIST 数据结构解析与加载方式选择项目中明确标注MNIST raw意味着数据未经过torchvision.datasets.MNIST或keras.datasets.mnist的封装而是直接使用官方提供的train-images-idx3-ubyte.gz和train-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)单通道矩阵。注意此处未做中心化mean0或标准化std1因为 Logistic Regression 和 SVM 对输入尺度敏感而后续 CNN/FNN 模块内部已通过 BatchNorm 或权重初始化补偿——这是参数化编程的典型体现预处理策略与模型类型强耦合。提示mnist_preview.py中show_sample()函数调用matplotlib.pyplot.imshow(img, cmapgray)时必须确保img是np.ndarray且 dtype 为float32否则会报TypeError: Invalid shape (28, 28, 3) for image data。原始 raw 数据是单通道强行转 RGB 会导致维度错乱。2.2 四种预处理方案的适用场景与代码实现项目未提供统一预处理函数而是按模型需求分散实现这恰恰反映真实工程逻辑。以下是各 notebook 中实际采用的方案及参数依据模型类型预处理操作关键参数设计理由Logistic RegressionStandardScalerfit on train, transform bothwith_meanTrue, with_stdTrueLR 对特征尺度极度敏感StandardScaler保证权重更新方向稳定with_meanTrue消除偏置项干扰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_firstTrueRNN 输入需为(seq_len, batch, input_size)将 28×28 图像展平为 28 个长度为 28 的时间步pad_sequence处理变长序列虽 MNIST 固长但保留接口对应代码片段以logistic_regression.ipynb为例from sklearn.preprocessing import StandardScaler scaler StandardScaler(with_meanTrue, with_stdTrue) X_train_scaled scaler.fit_transform(X_train) # 注意仅对训练集 fit X_test_scaled scaler.transform(X_test) # 测试集仅 transformfit_transform()与transform()的分离是关键——若对测试集也调用fit_transform()会导致数据泄露data leakage模型评估失真。StandardScaler的with_meanTrue参数不可省略否则 LR 的截距项bias会因特征均值非零而剧烈震荡。2.3 数据划分与标签编码的隐含约束所有模型均采用train_test_split(X, y, test_size0.2, random_state42, stratifyy)其中stratifyy保证测试集中 0~9 数字比例与训练集一致避免类别不平衡影响评估。值得注意的是y标签在 raw 加载后为np.uint8但在传入sklearn模型前未做 one-hot 编码因 LR/SVM 是原生多类分类器而在 PyTorch 模型FNN/CNN/RNN中则通过torch.nn.CrossEntropyLoss自动处理整型标签该 loss 内部执行 softmaxlognll无需手动 one-hot。这种差异在mnist_fnn_m.ipynb的Dataset类中体现class MNISTDataset(Dataset): def __init__(self, images, labels): self.images torch.FloatTensor(images).unsqueeze(1) # (N, 1, 28, 28) self.labels torch.LongTensor(labels) # 注意dtypetorch.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], cv5, solverlbfgs, max_iter1000, n_jobs-1 ) lr_cv.fit(X_train_scaled, y_train) print(fBest C: {lr_cv.C_[0]:.3f}) # 输出Best C: 1.000Cs列表覆盖 6 个数量级cv5表示 5 折交叉验证solverlbfgs适配多类且支持 L2 正则。结果C1.0被选中说明在 MNIST 上过强C1或过弱C1的正则化均降低泛化能力。max_iter1000是必要设置——原始LogisticRegression默认max_iter100在高维特征784 维下极易不收敛LogisticRegressionCV继承此参数必须显式增大。3.2 SVMRBF 核的 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(kernelrbf, random_state42), param_grid, cv3, scoringaccuracy, n_jobs-1 ) grid.fit(X_train_scaled, y_train) print(fBest params: {grid.best_params_}) # 输出{C: 10, gamma: scale}gammascale表示gamma1/(n_features * X.var())自动适配数据方差C10比 LR 的C1更大说明 SVM 对误分类容忍度更低需更强惩罚。cv3而非5是权衡——SVM 训练耗时随样本量增长极快X_train_scaled.shape[0]为 480005 折会显著拖慢调试周期。n_jobs-1启用所有 CPU 核心但需注意GridSearchCV的并行粒度是参数组合而非单次训练。3.3 FNN全连接网络的宽度-深度平衡设计mnist_fnn.ipynb的网络结构为784 → 128 → 64 → 10激活函数为ReLU输出层为LogSoftmaximport torch.nn as nn class SimpleFNN(nn.Module): def __init__(self, input_dim784, hidden1128, hidden264, num_classes10): 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(dim1) 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 xhidden1128与hidden264的递减设计遵循“宽度随深度增加而收缩”的经验法则防止参数爆炸。view(x.size(0), -1)是关键PyTorch 的Linear层只接受 2D 输入必须将(N,1,28,28)展平。若忘记此步forward会报mat1 and mat2 shapes cannot be multiplied。LogSoftmax与NLLLoss配对使用比SoftmaxCrossEntropyLoss数值更稳定。3.4 CNN卷积核尺寸与通道数的物理意义映射mnist_cnn.ipynb的核心结构为self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) # 输入1通道输出32通道3x3卷积 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) # 输入32通道输出64通道 self.fc1 nn.Linear(64 * 7 * 7, 128) # 经过2次2x2 MaxPool28→14→7 self.fc2 nn.Linear(128, 10)kernel_size3是 MNIST 的最优解2x2 太小无法捕获笔画交点5x5 在 28x28 图像上感受野过大易过拟合。padding1保证卷积后尺寸不变28→28配合MaxPool2d(2)实现下采样。32和64通道数非随意设定32是 GPU 显存与特征表达力的平衡点64*7*73136作为 FC 层输入远小于784的原始维度体现降维优势。64*7*7的计算源于28→14→7的尺寸变化若padding或stride改变此数值必须重算否则Linear层维度不匹配。3.5 RNN序列建模的图像行优先展开策略rnn_minst.ipynb将图像视为 28 行、每行 28 像素的序列class MNISTRNN(nn.Module): def __init__(self, input_size28, hidden_size128, num_layers2, num_classes10): super().__init__() self.rnn nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.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 outsqueeze(1)移除通道维permute(0,2,1)将(N,28,28)转为(N,28,28)—— 行优先row-major展开使每行像素作为时间步输入。batch_firstTrue确保输入/输出张量为(N, seq_len, features)符合直觉。dropout0.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, annotTrue, fmtd, cmapBlues) 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。混淆矩阵热力图中5行3列的高值表明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_loss0.021,val_acc0.983但第 95 行val_loss0.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_samples10)函数可生成错误分类样本图def plot_wrong_predictions(model, X_test, y_test, n_samples10): 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), cmapgray) ax.set_title(fTrue:{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_RATE0.001是 Adam 优化器的常用起点若改为0.01train_loss会在前 5 epoch 剧烈震荡需配合torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.1)逐步衰减。NUM_EPOCHS20是平衡效果与耗时的选择——实测epoch15时val_acc0.982epoch25时val_acc0.983仅提升 0.001但训练时间增加 33%。5.2 替换优化器与损失函数的兼容性检查若将mnist_fnn.ipynb中的optim.Adam替换为optim.SGD必须添加momentum0.9并增大learning_rate至0.01# 原始 Adam optimizer optim.Adam(model.parameters(), lr0.001) # 替换为 SGD需调整 optimizer optim.SGD(model.parameters(), lr0.01, momentum0.9) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) # 当 val_loss 3 epoch 不降lr 减半SGD收敛更慢但泛化稍好momentum0.9缓解震荡。ReduceLROnPlateau是必需配套——SGD对学习率更敏感固定lr易陷入局部最优。modemin对应val_loss若监控val_acc则需modemax。5.3 快速验证新模型结构的三步法当想尝试 ResNet 结构时不必重写全部代码利用现有框架快速验证继承SimpleFNN类在__init__中添加残差块class ResidualBlock(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.conv1 nn.Conv2d(in_channels, out_channels, 3, padding1) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, 3, padding1) 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)替换mnist_cnn.ipynb的forward函数插入ResidualBlock(32,32)复用原有train_loop仅修改model ResNetModel()运行train_loss.xlsx自动生成新曲线。此方法将验证周期压缩至 1 小时内避免从零搭建环境的风险。本文还有配套的精品资源点击获取