ARTICLE DETAIL

资讯详情

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

用PyTorch实现假钞识别:从MLP构建到模型训练全流程

用PyTorch实现假钞识别:从MLP构建到模型训练全流程 如果让我给刚接触 PyTorch 的朋友推荐一个“麻雀虽小五脏俱全”的练手项目假钞识别一定排在我心里的前三。原因很简单它足够小、足够完整、也足够有意思不是一个玩具级的“你好世界”而是真正带业务背景的机器学习任务。整个项目做下来你会对“数据到底怎么喂给模型”“训练一个网络到底要写哪些代码”“结果怎么看才靠谱”这些基本问题形成肌肉记忆再去看那些动辄几十万参数的模型心里也有底。这个项目的目标很直接拿钞票图像的一组统计特征训练一个多层感知机MLP模型把钞票分成“真”和“假”两类。数据集用的是公开的 Banknote Authentication 数据集一共 1372 条样本每条样本只有 4 个数值特征。换句话说不需要显卡不需要下载图片数据集你手头任意一台笔记本的 CPU 就能在几分钟内把从数据处理到模型评估的完整流程跑通。适合谁来学如果你有基本的 Python 语法基础想入门 PyTorch但还没搞明白“训练一个模型”这件事到底要走哪几步这个项目就是为你准备的。它把数据集处理、模型搭建、训练循环、结果评估这一整条链路完整走一遍学完之后你能很快迁移到其他表格类分类任务上。下面我把整个项目的构建过程拆开讲包括每一步为什么这么做踩过哪些坑希望能帮你少走一点弯路。1. 项目解读假钞识别到底在干什么1.1 这不是图像识别是特征分类先说一个容易搞混的点很多人听到“假钞识别”第一反应是“深度学习识别钞票图片”于是脑补出一套卷积神经网络CNN处理图像的流程。但这个项目完全不是这么回事。真实的假钞检测场景里银行或者验钞设备会通过紫外荧光、磁性油墨、水印、冠字号等多种手段综合判断。其中有一种常见做法是用工业相机拍下钞票图像然后对图像做小波变换从变换后的数据里提取统计量比如方差、偏度、峰度、熵这一类指标。最终拿到的不是一个像素矩阵而是几个数值。所以 Banknote Authentication 数据集里的每条样本就是“一张钞票图像的 4 个统计特征 一个真/假标签”。模型要学习的是这组特征和真伪之间的映射关系。这本质上是表格数据的二分类问题不是图像分类问题。这个认知很重要它决定了后面模型选型的思路你不需要卷积层不需要做图像增强直接上最简单的前馈网络就行。1.2 数据集长什么样数据集可以在 UCI Machine Learning Repository 找到文件名通常是data_banknote_authentication.txt。一共 1372 条样本每条 5 列前 4 列是特征最后一列是类别标签。列名含义说明variance小波变换图像的方差反映图像纹理的离散程度skewness小波变换图像的偏度反映分布对称性curtosis小波变换图像的峰度反映分布是陡峭还是平坦entropy图像熵反映图像信息量class标签0 代表真钞1 代表假钞类别分布大概是真钞 762 条假钞 610 条比例接近 6:4。这个均衡度对分类任务来说非常友好不用做太多类别不平衡处理。数据本身也没有缺失值省了做填充的功夫。对于习惯了“真实数据一堆脏东西”的从业者来说这个数据集干净得有点不真实但作为入门项目反而合适能把精力集中在模型流程本身。1.3 为什么选 MLP 而不是 CNN、逻辑回归或 XGBoost选什么模型取决于数据形态和任务规模。首先排除 CNN。CNN 擅长处理网格结构数据比如图像像素、语音频谱这里每条样本是 4 个独立数值没有空间结构硬塞给 CNN 反而会把简单问题复杂化。那为什么不直接上逻辑回归或者决策树呢我承认在这些小规模表格数据上传统机器学习模型往往表现非常好甚至可能比 MLP 更容易调参。但做这个项目的目标不只是“把准确率刷到多高”而是“用 PyTorch 完整跑通一个深度学习流程”。MLP 是最简单的前馈神经网络它比线性模型有更强的非线性表达能力又比 CNN、RNN 少了很多概念负担是练手的最佳抓手。另外我也建议跑通 MLP 之后可以拿逻辑回归、随机森林或者 XGBoost 来做个对比。你会发现不同模型的精度差异可能并不大但这个对比过程会让你体会到什么是“模型容量”“非线性拟合”这对后续学习更有价值。2. 环境准备与数据预处理2.1 环境配置这个项目对环境要求很低。我用的是 Python 3.9 PyTorch 1.13CPU 版本就够了安装命令很简单pip install torch --index-url https://download.pytorch.org/whl/cpu这里的核心依赖主要是这几样torch深度学习框架pandas读取和操作数据numpy数值计算scikit-learn数据标准化、划分数据集、评估指标matplotlib画训练曲线如果不想手动管理环境直接用 Anaconda 建一个独立环境也可以。我不建议在系统全局环境里直接装项目一多依赖容易打架这个坑踩过的都懂。安装好之后可以跑一下验证import torch print(torch.__version__) print(torch.cuda.is_available())输出 PyTorch 版本号是正常的cuda.is_available()返回 False 也没关系这个小项目 CPU 完全够用。2.2 加载数据与探索性分析先把数据读进来看一眼结构。因为原始文件没有表头需要手动指定列名import pandas as pd import numpy as np import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, confusion_matrix import matplotlib.pyplot as plt df pd.read_csv(data_banknote_authentication.txt, headerNone, names[variance, skewness, curtosis, entropy, class]) print(df.info()) print(---) print(df.describe().T) print(---) print(df[class].value_counts())这段代码做的事情很简单但每步都有它的价值df.info()用来确认有没有缺失值、每一列的数据类型是什么。df.describe().T输出每个特征的均值、标准差、最小最大值帮你对数据尺度有个直观认识。这一步我特别强调一下看一眼取值范围后面做标准化、判断训练曲线是否正常都有参考系。我当时跑describe()就发现特征之间尺度差异明显——有的特征取值范围在 -7 到 7 之间有的在 -8 左右到 8 左右方差差异也比较大。如果直接喂给模型梯度更新会被大尺度特征主导模型收敛会变得很慢。df[class].value_counts()检查类别分布这里确认 0 和 1 的数量没有严重失衡。2.3 标准化与数据划分数据探索完之后下一步是标准化这是整个预处理里最关键的一步。标准化的目的是把特征缩放到均值为 0、方差为 1 的范围。为什么一定要做拿神经网络训练来类比每个神经元做的事情是“输入做加权求和然后过激活函数”。如果某个特征的范围是 0 到 1000另一个特征的范围是 -1 到 1那么权重更新时大数值特征对应梯度会天然偏大梯度下降过程就容易震荡收敛速度奇慢。标准化之后所有特征被拉到同一尺度梯度更新更平稳训练效率会明显提升。实现标准化我习惯用 scikit-learn 的StandardScaler核心逻辑是先计算训练集的均值和标准差再用这个均值和标准差去变换训练集和测试集X df[[variance, skewness, curtosis, entropy]].values y df[class].values X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test)这里有一个新手特别容易犯的错先对整个数据集做标准化再划分训练集和测试集。这种做法会造成数据泄漏——测试集的信息提前混进了训练过程评估结果会虚高参考价值大打折扣。正确做法是先划分再在训练集上fit然后用训练集的统计量去transform测试集。stratifyy参数也很重要它保证划分后训练集和测试集里的真假比例和原始数据一致避免随机划分导致的分布偏移。random_state42则是固定随机过程让每次跑出来的划分结果一致方便复现和对比实验。3. 多层感知机模型设计与训练3.1 用大白话理解多层感知机在写代码之前先把多层感知机的原理捋一遍。MLP 的结构可以简单概括为三层输入层、隐藏层、输出层。输入层接收原始特征。这个项目里是 4 个特征所以输入层有 4 个神经元。隐藏层是模型内部的中间层负责对输入做非线性变换。输出层给出最终的预测结果这里因为要做真假二分类输出层设为 2 个神经元分别代表“真钞”和“假钞”的得分。每个神经元做的事情可以拆成两步先对输入做一个线性加权求和再加上一个偏置项然后把结果塞进一个激活函数。激活函数是 MLP 能处理非线性问题的关键如果没有它不管你堆多少层整个网络本质上还是一个线性模型表达能力非常有限。常用的激活函数是 ReLU公式是f(x) max(0, x)简洁高效计算快能缓解梯度消失问题。如果用一句话概括 MLP 在干什么它通过不断调整神经元之间的权重和偏置在输入特征空间里学出一个分界面把真钞和假钞两类样本尽量分开。这个过程就是训练。3.2 用 PyTorch 搭建 MLP 模型PyTorch 里定义模型的标准姿势是继承nn.Module在__init__里定义网络层在forward里定义前向传播逻辑。class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.net nn.Sequential( nn.Linear(4, 16), nn.ReLU(), nn.Linear(16, 16), nn.ReLU(), nn.Linear(16, 2) ) def forward(self, x): return self.net(x)这个网络结构是4 - 16 - 16 - 2一个输入层两个隐藏层一个输出层。隐藏层神经元数量选了 16这是个小数据集上比较常见的配置。为什么不选 64 或者 128因为样本量只有 1372 条特征维度才 4 个模型容量堆太大非常容易过拟合——训练集上 loss 降到很低测试集上表现却不尽如人意。386 个参数量的网络对这个任务来说已经有不弱的表达能力。为什么用两层隐藏层而不是一层或者三层这不是拍脑袋定的。一层隐藏层理论上能逼近任意连续函数但往往需要更多神经元两层隐藏层在同等效果下可以用更少的神经元训练也更容易稳定。三层以上对这个小任务来说就是浪费了参数一多调参负担和过拟合风险都会上来。3.3 损失函数与优化器的选择逻辑模型训练需要一个“指挥棒”告诉参数往哪个方向调这个指挥棒就是损失函数和优化器。二分类问题有两个常用组合输出 2 维得分搭配nn.CrossEntropyLoss()PyTorch 内部会自动做 Softmax输出 1 维得分搭配nn.BCEWithLogitsLoss()PyTorch 内部会自动做 Sigmoid。两种都能达到目的。我这里选了nn.CrossEntropyLoss()主要原因是代码里模型输出层是 2 维从逻辑上更贴近“两类得分”的直觉。优化器选的是 Adam初始学习率设为 0.001。Adam 是一种自适应学习率的优化算法它会根据每个参数的历史梯度动态调整学习率对新手上手非常友好。相比之下SGD 对学习率的设置很敏感调起来的成本高出不少。以我个人的经验在类似的小型表格分类任务上默认的 Adam lr1e-3就是一个相当稳的起点不需要做太多改动。3.4 完整训练循环代码训练循环是 PyTorch 项目里最核心的模板代码之后你写任何模型这个框架都是相通的。完整代码长这样def train_model(model, train_loader, test_loader, epochs100, lr1e-3): criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lrlr) train_losses, test_losses [], [] train_accs, test_accs [], [] for epoch in range(epochs): model.train() total_loss, correct, total 0.0, 0, 0 for xb, yb in train_loader: optimizer.zero_grad() out model(xb) loss criterion(out, yb) loss.backward() optimizer.step() total_loss loss.item() * xb.size(0) pred out.argmax(dim1) correct (pred yb).sum().item() total yb.size(0) train_loss total_loss / total train_acc correct / total model.eval() with torch.no_grad(): val_loss, val_correct, val_total 0.0, 0, 0 for xb, yb in test_loader: out model(xb) loss criterion(out, yb) val_loss loss.item() * xb.size(0) pred out.argmax(dim1) val_correct (pred yb).sum().item() val_total yb.size(0) test_loss val_loss / val_total test_acc val_correct / val_total train_losses.append(train_loss) test_losses.append(test_loss) train_accs.append(train_acc) test_accs.append(test_acc) if (epoch 1) % 10 0: print(fEpoch {epoch 1}: train_loss{train_loss:.4f}, ftest_loss{test_loss:.4f}, train_acc{train_acc:.4f}, ftest_acc{test_acc:.4f}) return train_losses, test_losses, train_accs, test_accs训练前要把数据封装成 PyTorch 的TensorDataset和DataLoaderX_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.long) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.long) train_dataset TensorDataset(X_train_t, y_train_t) test_dataset TensorDataset(X_test_t, y_test_t) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue) test_loader DataLoader(test_dataset, batch_size32, shuffleFalse)这里有几个细节值得展开说。model.train()和model.eval()的切换训练模式下模型启用 Dropout、BatchNorm 等训练专用行为评估模式下这些层会切换成推理行为。虽然当前这个简单 MLP 里没有这些层但养成习惯很重要以后模型一复杂就会用到。optimizer.zero_grad()必须在每次反向传播前调用否则梯度会累加起来相当于每个 batch 都在累计上一次的梯度更新方向就乱了。PyTorch 反传前不清梯度应该是新手最常见的 bug 之一。batch_size32是一个比较平衡的选择。batch 太小时梯度噪声大训练不稳定batch 太大时一个 epoch 内参数更新次数太少收敛变慢。32 在绝大多数表格分类任务上表现都挺稳不用过度纠结。训练轮数设 100 还是 200我的建议是先设 100打印 loss 和 acc 观察趋势。如果训练集准确率还在明显上升就增大 epoch如果已经到 99% 以上且测试集不再提升就可以停了。训练曲线上能直接看到这个信号。4. 模型评估与结果可视化4.1 要看的指标不是只有准确率很多人跑完模型只看一个准确率就得出结论。在小数据集上这样做问题不大放到真实场景里就危险了。假钞识别这个问题里两类错误的代价是完全不一样的真钞被误判为假钞假阳性客户体验受损银行向客户解释的成本高假钞被误判为真钞假阴性银行直接蒙受资金损失后果更严重。所以在评估时必须关注混淆矩阵以及从混淆矩阵里派生出的精确率、召回率、F1 分数。model.eval() with torch.no_grad(): y_pred [] for xb, _ in test_loader: out model(xb) pred out.argmax(dim1) y_pred.extend(pred.numpy().tolist()) print(classification_report(y_test, y_pred, digits4)) print(confusion_matrix(y_test, y_pred))classification_report会一次性输出每个类别的精确率、召回率、F1。我个人比较关注假钞这一类class 1的召回率它代表“假钞里有多少被正确找出来”。这个数字越高说明模型漏掉假钞的可能性越低。4.2 训练曲线怎么看训练曲线是最直观的“模型健康报告”。把训练过程里的 loss 和 acc 画出来plt.figure(figsize(12, 4)) plt.subplot(1, 2, 1) plt.plot(train_losses, labeltrain_loss) plt.plot(test_losses, labeltest_loss) plt.xlabel(epoch) plt.ylabel(loss) plt.legend() plt.subplot(1, 2, 2) plt.plot(train_accs, labeltrain_acc) plt.plot(test_accs, labeltest_acc) plt.xlabel(epoch) plt.ylabel(accuracy) plt.legend() plt.tight_layout() plt.show()怎么看这张图正常情况下 train loss 和 test loss 都在下降最终趋于平缓。如果 train loss 一直降但 test loss 先降后升这是过拟合的典型信号——模型开始死记训练数据失去了泛化能力。遇到这种情况优先考虑降低模型容量、增大 batch size或者加入 Dropout。反过来如果 train loss 和 test loss 都降不下去那多半是模型容量不够或者学习率不合适。这个项目我用上述配置跑完后测试集准确率在 98% 到 99% 之间对一个小型特征分类任务来说已经相当可以了。4.3 用测试集直接评估模型效果除了百分比指标我建议直接把预测结果和真实标签摆在一起看。打印前 20 条对比能帮你感知模型错在哪、对在哪。with torch.no_grad(): out model(X_test_t) y_pred_t out.argmax(dim1) comparison pd.DataFrame({ 真实标签: y_test, 预测标签: y_pred_t.numpy() }) print(comparison.head(20))这个方法看起来笨但对建立直观感受极其有效。你会看到多数样本都预测对了偶尔有几个错的——它们到底是临界样本还是离群点对比得多了你对模型的置信度会从“跑出来的数字很高”变成“我知道它擅长什么、不擅长什么”这是评估最有价值的部分。5. 实操中踩过的坑与排查技巧5.1 忘做标准化loss 卡在 0.69 上下不动我第一次带着原始特征直接喂给模型结果 loss 在前几十个 epoch 里几乎不动准确率也一直在 50% 左右徘徊整个模型看起来完全没在学习。查了半天发现就是没有做特征标准化。原因前面说过特征尺度差异大梯度更新被大数值特征主导小数值特征对应的权重几乎得不到有效更新。加上 Adam 的自适应机制在初始阶段会被异常梯度“带偏”表现看起来就是 loss 压不下去。解决方式很朴素先fit_transform训练集再transform测试集。如果你跑类似项目遇到 loss 不降第一步永远先检查数据预处理。5.2 学习率设得太大loss 直接变 NaN有次我图省事把学习率从 0.001 改成了 0.1想着“让模型学快点”结果跑了两三个 batch 后 loss 直接变成nan后续所有输出全是 nan。这是因为学习率过大会让参数更新步长超大权重值瞬间飞出合理范围梯度计算出现无穷值。学习率设置的思路应该是“小步快跑”。0.001 不行就试 0.0001、0.0003不要一上来就 0.1、1 这种量级。如果 loss 不降调整学习率之前先检查数据和代码逻辑别急着加大学习率。5.3 测试时忘了model.eval()结果忽高忽低如果你的模型里有 Dropout 或者 BatchNorm评估时忘了切换model.eval()预测结果每次跑都可能不一样。因为训练模式下 Dropout 会随机丢弃神经元BatchNorm 会使用当前 batch 的统计量这都属于“训练状态”的行为切换成评估状态才能用稳定的参数进行推理。当前这个 MLP 里没有这些层所以影响不大。但如果你在这个基础结构里加 Dropout 做实验就会立刻体会到这个坑。建议把model.eval()和torch.no_grad()当成评估代码里的固定搭档不管模型需不需要先写上再说。5.4 不固定随机种子实验结果飘忽不定深度学习里涉及很多随机过程包括参数初始化、数据 shuffle、dropout 等。如果不固定随机种子同样的代码跑两次结果可能差一两个百分点甚至更多。为了保证实验可以复现、调参时可以公平对比我习惯在训练前固定这几个种子import random random.seed(42) np.random.seed(42) torch.manual_seed(42)如果用了 GPU还要加一句torch.cuda.manual_seed_all(42)。固定完种子之后数据划分、模型初始化和训练顺序都确定了每次跑出来的结果才具有可比性。5.5 常见问题速查表我把实操里遇到频率最高的几个问题整理成了一张表方便你以后排查问题现象可能原因排查与解决办法loss 不降特征未标准化检查数据是否 fit_transform 后再喂模型loss 为 nan学习率过大降低学习率到 0.001 或更小测试集准确率远低于训练集模型过拟合降低模型容量、加 Dropout、增大 batch每次跑结果不一样随机种子未固定固定 random、numpy、torch 的种子预测结果波动很大评估时未切 eval 模式在评估时加 model.eval() 和 no_grad数据划分后分布偏移train_test_split 未分层增加 stratifyy 参数5.6 一个提升实战感的建议做一次交叉验证这个项目用单次 train/test split 已经够用但如果你想更严谨可以试试 K 折交叉验证。把数据分成 5 份轮流拿其中 1 份做测试其余 4 份做训练最后看 5 次结果的平均值和方差。这样能更全面地评估模型的泛化能力避免“正好这次划分结果特别好换一种划分就崩了”的错觉。交叉验证对这个小数据集来说非常友好总共才 1372 条样本跑 5 次也就多等十几秒。实现上可以直接用sklearn.model_selection.KFold配合已有的训练循环代码不需要改太多。另外你还可以把训练环节里的隐藏层神经元数量从 16 改成 8、32、64分别记录 train/test loss 曲线的变化直观感受“欠拟合——合适——过拟合”的过程。这种小实验的意义不在于调出最好成绩而是帮你建立对模型容量的直觉这是靠看文档很难获得的东西。我个人做完这个项目的最大体会是深度学习入门最关键的一步不是看懂某个模型的公式而是亲手把一个完整项目跑通每一步都知道自己在干嘛、为什么这么干。假钞识别这个任务足够简单、数据集足够干净、模型足够轻量适合反复折腾。把这篇文章里的代码敲一遍再改一改隐藏层、学习率、batch size你对 PyTorch 的训练流程会有完全不一样的理解。
返回列表