ARTICLE DETAIL

资讯详情

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

PyTorch全连接神经网络实现垃圾邮件分类实战与避坑指南

PyTorch全连接神经网络实现垃圾邮件分类实战与避坑指南 简介这是一份基于Pytorch的垃圾邮件分类完整实现面向深度学习初学者以及需要完成课程设计、毕业设计的计算机专业学生。项目采用MLP全连接神经网络与优化器进行有监督分类完整覆盖spambase数据读取、特征处理、模型搭建、迭代训练与效果评估流程并使用PytorchViz将网络结构可视化用Canvas绘制损失函数与识别精度的动态变化曲线便于直观理解训练过程。压缩包共20个文件包含可直接运行的Python源码、csv数据文件、网络与曲线可视化图png/gv格式、PDF/Word/文本文档等整体仅7.18MB目录清晰、依赖简单可快速迁移到类似文本分类任务。目前已有1016人学习下载实用性已得到验证。资源内的报告文档和可视化图表能够帮助使用者梳理MLP分类原理并为毕业设计说明书或课程报告提供现成材料。1. 拿 PyTorch 全连接神经网络做垃圾邮件分类这份代码为什么能直接跑如果你在找毕业设计的落脚点大概率见过两类资源一类把原理写到天花乱坠代码却缺胳膊少腿另一类代码齐全但 README 写得像天书跑起来全是坑。「深度学习 PyTorch 全连接神经网络 垃圾邮件的分类」这套资源难得的是把完整代码和可直接用的数据集一起给你模型能训练、能评估、能对单条短信做预测属于打开即用的完整闭环。它主线是经典的全连接网络MLP做文本二分类预处理用 TF-IDF 向量化框架是 PyTorch。尤其适合基础一般、没多少时间折腾环境、只求把流程跑通再谈改进的学生。我拆完这套资源的整体思路和落坑记录都在下文照着做能省下你至少两周的弯路。2. 数据处理和 TF-IDF 向量化模型效果七成由这一步决定2.1 数据长什么样确认标签、样本量和编码这套资源默认用的是公开的 SMS Spam Collection 类数据集特点非常典型英文短信为主字段只有两列一列是 labelspam / ham一列是短信原文。这类数据集的干净程度比中文评论数据好很多因为没有分词负担但仍有格式问题需要处理。拿到代码后第一步先读数据并做基础统计我一般在项目里会先加一段最基础的数据探查代码import pandas as pd from sklearn.model_selection import train_test_split df pd.read_csv(./data/spam.csv, encodinglatin-1) # 原数据集常有多余的无名列先清掉 df df[[v1, v2]].copy() df.columns [label, text] print(df[label].value_counts()) print(空值数量, df.isnull().sum().sum()) print(重复样本数, df.duplicated().sum())这里有几个参数值得注意。encodinglatin-1是这个数据集的老坑很多渠道下载的版本带特殊字符用utf-8读会直接报错。v1、v2是原始列名v1是标签、v2是短信内容不同版本列名可能略有差异建议先打印df.columns确认一次。数据集里 ham 远多于 spam这种类别不平衡会直接影响后面训练需要记一下比例。重复样本删除与否取决于你是否担心模型把重复文本的标签背下来我一般会保留因为真实场景里重复内容出现很正常。2.2 把文本变成张量输入TF-IDF 的参数取值逻辑全连接神经网络不能直接吃字符串需要把文本转成数值向量。这套资源选用 TF-IDF 而不是 Word2Vec 或 BERT核心原因是数据量小TF-IDF 足够表达词的重要性且训练快、内存占用低。我拆过的类似项目里TF-IDF 加全连接在垃圾邮件这个任务上效果不比词向量差太多但调试成本低一个量级。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer( lowercaseTrue, analyzerword, stop_wordsenglish, max_features5000, ngram_range(1, 2), sublinear_tfTrue ) X vectorizer.fit_transform(df[text]) y (df[label] spam).astype(int).values # 全连接网络输入是稠密向量转成数组 X X.toarray() print(向量维度, X.shape) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy )六个参数里max_features5000是影响最大的一个。它限制词汇表大小防止稀疏矩阵维度爆炸同时也算一种轻量特征筛选。ngram_range(1, 2)同时保留单词和相邻双词组合对垃圾邮件里 “free prize” 这类复合表达很有用。sublinear_tfTrue用 1 log(tf) 替代原始词频削弱高频词的主导地位。stop_wordsenglish直接过滤掉 the、and 这类无意义词。stratifyy保证切分后训练集和测试集里正负样本比例一致这一步对类别不平衡数据非常重要忘了会导致测试集里 spam 占比失真。toarray()把稀疏矩阵转成普通二维数组因为后面要喂给全连接层。提示如果你的数据是中文短信analyzerword就不适用了需要先用 jieba 分词再传入stop_words也要换成中文停用词表。这套资源是英文数据中文场景可以直接沿用 TF-IDF 模块只换预处理。2.3 DataLoader 封装与批次设计PyTorch 训练需要数据迭代器这里把已经向量化好的 NumPy 数组封装成 Dataset 与 DataLoader。import torch from torch.utils.data import TensorDataset, DataLoader from sklearn.preprocessing import StandardScaler # 对输入向量做标准化加速全连接网络收敛不是必须但建议加 scaler StandardScaler(with_meanFalse) X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) X_train_t torch.tensor(X_train, dtypetorch.float32) y_train_t torch.tensor(y_train, dtypetorch.float32).view(-1, 1) X_test_t torch.tensor(X_test, dtypetorch.float32) y_test_t torch.tensor(y_test, dtypetorch.float32).view(-1, 1) train_dataset TensorDataset(X_train_t, y_train_t) test_dataset TensorDataset(X_test_t, y_test_t) batch_size 64 train_loader DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue) test_loader DataLoader(test_dataset, batch_sizebatch_size, shuffleFalse) print(训练批次数量, len(train_loader))StandardScaler在这里用的with_meanFalse是因为 TF-IDF 矩阵是稀疏的按列做减去均值的操作会让矩阵变稠密直接爆内存。如果不做标准化全连接网络也能收敛但收敛速度明显慢。view(-1, 1)把标签形状从[N]变成[N, 1]这样和模型输出的形状能直接做 BCELoss 计算。shuffleTrue只用于训练集每次迭代打乱样本顺序避免模型学到批次内的固定排列。3. 搭建全连接网络与训练管线PyTorch 的核心代码拆解3.1 网络结构设计隐藏层维度怎么定垃圾邮件分类是二分类问题输出层一个节点即可。这套资源的网络结构是典型的全连接堆叠输入层维度等于 TF-IDF 特征数接着若干隐藏层每层后面接 ReLU 激活和 Dropout最后接 Sigmoid。import torch.nn as nn class SpamClassifier(nn.Module): def __init__(self, input_dim, hidden_dims[256, 128], dropout_prob0.5): super().__init__() layers [] prev_dim input_dim for hidden_dim in hidden_dims: layers.append(nn.Linear(prev_dim, hidden_dim)) layers.append(nn.ReLU()) layers.append(nn.Dropout(dropout_prob)) prev_dim hidden_dim layers.append(nn.Linear(prev_dim, 1)) self.net nn.Sequential(*layers) def forward(self, x): return self.net(x) model SpamClassifier(input_dimX_train.shape[1], hidden_dims[256, 128]) print(model)hidden_dims[256, 128]是这份资源在测试集上表现比较稳定的组合。输入 5000 维压到 256再到 128最后输出 1逐层降维。隐藏层不是越宽越好5000 维直接映射到 1 维是线性模型的做法非线性拟合能力不够直接上 512、512 这种大结构在小数据集上几乎必然过拟合。dropout_prob0.5每次训练随机丢弃一半的神经元连接相当于隐性地做模型集成对文本高维稀疏输入特别有效。ReLU 不用多解释线性层之后没有非线性激活多层就等价于一层这是必须的。3.2 损失函数、优化器和训练循环为什么用 BCELoss 而不是 CrossEntropyLoss输出层是 1 个节点过 Sigmoid 得到概率值对应二分类正反两类互斥所以用BCELoss。如果你把输出层改成 2 个节点再过 Softmax那就该用CrossEntropyLoss。两者数学上等价但后者还包含 LogSoftmax数值稳定性更好。这里的代码选择前一种代码更少。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.BCELoss() optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-5) def train_epoch(model, loader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for X_batch, y_batch in loader: X_batch, y_batch X_batch.to(device), y_batch.to(device) optimizer.zero_grad() outputs torch.sigmoid(model(X_batch)) loss criterion(outputs, y_batch) loss.backward() optimizer.step() preds (outputs 0.5).float() total_loss loss.item() * X_batch.size(0) correct (preds y_batch).sum().item() total y_batch.size(0) return total_loss / total, correct / total train_loss, train_acc train_epoch(model, train_loader, criterion, optimizer, device) print(f训练集 loss: {train_loss:.4f}, acc: {train_acc:.4f})weight_decay1e-5是 L2 正则化的 PyTorch 实现和 Dropout 双管齐下压制过拟合。lr0.001是 Adam 的默认学习率对这个任务通常够用。model.train()打开 Dropout 和 BatchNorm 的训练模式这个细节一旦漏了Dropout 失效训练和预测时模型行为不一致。optimizer.zero_grad()必须在每个 batch 之前清空梯度否则梯度会在 batch 之间累积。输出层前面手动加了torch.sigmoid而nn.BCELoss要求输入已经是概率值记住这个对应关系排错时会省很多事。3.3 完整训练流程多少个 epoch 合适epochs 20 best_acc 0 for epoch in range(epochs): train_loss, train_acc train_epoch( model, train_loader, criterion, optimizer, device ) print(fEpoch {epoch1:02d} | fTrain Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f})20 个 epoch 不是拍脑袋来的。这个数据量级别的垃圾邮件分类任务用 TF-IDF 加全连接网络通常在 10 到 25 轮之间收敛超过 30 轮后训练 loss 降得很慢测试指标反而会逐步下滑。best_acc变量用来记录当前最优模型后面讲模型保存时会用它做条件判断。训练过程中可以顺手打印每个 epoch 的训练 loss观察 loss 是否平稳下降如果出现震荡或直接原地不动先去查学习率而不是加 epoch 数量。4. 评估与模型保存别只盯着准确率召回率才是垃圾邮件的关键4.1 混淆矩阵、精确率、召回率和 F1垃圾邮件分类和一般图像分类不一样图像分类看准确率基本够用垃圾邮件不行。试想30 条重要邮件里有 1 条被误判成垃圾邮件这个模型体验是灾难性的。实际操作中宁可让 5 条垃圾邮件漏网也别误杀一条正常邮件。所以评估阶段必须看混淆矩阵和召回率。from sklearn.metrics import ( confusion_matrix, classification_report, precision_recall_fscore_support ) model.eval() all_preds, all_labels [], [] with torch.no_grad(): for X_batch, y_batch in test_loader: X_batch X_batch.to(device) outputs torch.sigmoid(model(X_batch)) preds (outputs 0.5).float().cpu() all_preds.extend(preds.numpy().flatten()) all_labels.extend(y_batch.numpy().flatten()) print(confusion_matrix(all_labels, all_preds)) print(classification_report(all_labels, all_preds, target_names[ham, spam]))model.eval()关闭 Dropout这一点和前面的model.train()对应。如果没有这行代码同样的输入在两次预测中结果可能不同因为 Dropout 在推理时还在随机丢神经元。torch.no_grad()告诉 PyTorch 不需要计算梯度推理时显存占用更低。target_names的顺序spam 是正类1ham 是负类0如果查classification_report出现对应错位先查标签映射关系。提示阈值 0.5 不是不可变的。如果你的应用更在意「不能漏掉垃圾邮件」可以把阈值降到 0.3更在意「不能误杀正常邮件」就调到 0.7。这个操作不需要重新训练只调整判定边界即可。4.2 保存和加载模型要存 state_dict 而不是整个模型模型训练耗时不算长但保存仍然是必须的因为你不可能每次预测都重新训练一遍。PyTorch 官方推荐的方式是只保存state_dict不保存整个模型对象这样和 PyTorch 版本兼容性更好。# 保存模型参数 torch.save(model.state_dict(), ./checkpoints/spam_model.pt) # 预测阶段加载模型 model SpamClassifier(input_dimX_train.shape[1], hidden_dims[256, 128]) model.load_state_dict(torch.load(./checkpoints/spam_model.pt, map_locationcpu)) model.eval()map_locationcpu解决 GPU 训练的模型在纯 CPU 机器上加载的问题。如果训练时用了devicecuda加载时不加这个参数在无 GPU 环境会直接报错。加载后必须再执行一次model.eval()因为 load 之后模型默认还是在train()模式。顺带说一句整套资源里最好把词表vectorizer也保存下来没有它等于只拿到了枪没拿到子弹——预测新文本时需要走同一个 TF-IDF 变换流程维度才能对齐。5. 避坑与常见问题这份资源最容易翻车的五个地方5.1 报错RuntimeError: size mismatch现象训练第一个 epoch 时报size mismatch for net.0.weight: copying a param with shape torch.Size([256, 5000]) ...。原因加载预训练模型时input_dim和当前数据特征数不一致通常是你换了自己的数据集词表大小变了但还想着用原模型的权重。解决模型第一个 Linear 层的输入维度必须严格等于 TF-IDF 特征数。换数据就重新训练或者修改max_features保持维度一致后重新训练不要硬加载旧权重。我一般会写一个断言启动时先打印model里的第一层维度再和X_train.shape[1]比对。5.2 现象测试准确率 99%但实测新短信几乎全判成 spam原因这是信息泄漏的经典案例。如果在fitTF-IDF 之前先做了train_test_split然后在全量数据上拟合 vectorizer测试集的信息就已经参与了特征构建。更常见的变体是在向量化之后做标准化但scaler.fit用的是全量 X。解决正确顺序是——先切分数据再在训练集上调fit_transform用同一个已拟合的 vectorizer 对测试集只做transform。资源里有时为了简洁没强调这个顺序如果你拿到的代码是「先 fit 再 split」请务必改过来。这条不修模型指标好看但完全没实用价值。5.3 现象模型预测结果全是 0全是 ham原因类别不平衡加上随机初始化模型陷入局部最优把所有样本都预测为多数类。BCELoss 在这种场景下很容易出现「学不到少数类」的问题。解决先确认y的标签映射是对的。如果正负比超过 1:5考虑在损失函数中加权重torch.nn.BCELoss(weighttorch.tensor([5.0]))其中 5 是负样本数除以正样本数的比值。或者用WeightedRandomSampler做有放回采样。这套资源的数据里 spam 占比约 13%不算极端但如果你把数据换成自己收集的比例可能完全不同。5.4 现象Anaconda 里 PyTorch 装完能 import但torch.cuda.is_available()永远返回 False原因安装的是 CPU 版 PyTorch。PyTorch 的安装包区分 CUDA 版和 CPU 版在 PyPI 官方源里默认拉到的就是 CPU 版pip install torch不会自动给你带 CUDA 支持的版本。解决去 PyTorch 官网选你的 CUDA 版本对应的安装命令比如pip install torch --index-url https://download.pytorch.org/whl/cu118CUDA 11.8。装完验证两件事torch.__version__里是否带cu后缀以及torch.cuda.is_available()是否返回 True。整套资源不依赖 GPU纯 CPU 也能跑完 20 个 epoch只是每个 epoch 会慢 3 到 5 倍。5.5 现象训练 loss 在 0.69 附近震荡怎么调参都不降原因这是模型没有学到任何有效信息的典型信号。0.69 约等于-ln(0.5)说明模型输出概率接近 0.5等价于随机猜测。解决按顺序排查——输入特征是否全零TF-IDF 参数设太大所有值被截断标签是否已经映射正确全是 0 或全是 1学习率是否过大导致发散改成 0.0001 试一下。三个都查完最后看X_train[0]的前 20 个值是否非零。最常见的原因是max_features设置过小整个向量化后每个样本的非零特征极少模型没有有效输入。6. 把模型用起来从 Jupyter 到命令行预测脚本资源里的代码大概率以 Jupyter Notebook 组织但毕业设计答辩时只拿出一个带输出的 notebook 是不够分量的。把训练好的模型封装成命令行工具会是一个亮点。我用argparse写了一个可接收单条文本、输出预测结果的脚本这里分享核心段落。import argparse import torch from sklearn.feature_extraction.text import TfidfVectorizer import joblib def parse_args(): parser argparse.ArgumentParser(description垃圾邮件分类预测) parser.add_argument(--text, typestr, requiredTrue, help输入短信文本) parser.add_argument(--model_path, typestr, default./checkpoints/spam_model.pt) parser.add_argument(--vectorizer_path, typestr, default./checkpoints/vectorizer.pkl) return parser.parse_args() args parse_args() vectorizer joblib.load(args.vectorizer_path) model SpamClassifier(input_dimlen(vectorizer.vocabulary_), hidden_dims[256, 128]) model.load_state_dict(torch.load(args.model_path, map_locationcpu)) model.eval() X vectorizer.transform([args.text]).toarray() X scaler.transform(X) # 别忘了保存并加载训练时的 scaler with torch.no_grad(): prob torch.sigmoid(model(torch.tensor(X, dtypetorch.float32))).item() print(fSpam 概率: {prob:.4f}, 判定: {spam if prob 0.5 else ham})注意这里input_dim用的是len(vectorizer.vocabulary_)对应训练时的词表大小。第 5 章提到的 scaler 在这里也要保存和加载没有它的标准化步骤模型输入分布和训练时不一致预测概率会偏移得离谱。保存方式用joblib.dump(scaler, ./checkpoints/scaler.pkl)加载时保持同名。如果你想把项目再往上提一个档次可以加一个简单的混淆矩阵可视化图以及用matplotlib画训练和验证的 loss 曲线图。这些图放到毕业论文里比大段文字说明直观得多。我还习惯在训练脚本里加一个--eval_only模式跳过训练直接跑评估省去重新训练的时间消耗。从那次拆包之后我每拿到一份开源资源都强制先跑通再改结构不跳过预处理直接开训练。这个小习惯让我避开了至少三次「数据泄漏导致的虚假高准确率」。希望这套拆解帮你在毕业设计里少走几步弯路把时间花在真正有增量的事情上。本文还有配套的精品资源点击获取
返回列表