
简介这份资源面向深度学习入门者、课程实践学生及毕业设计选题人群提供基于Pytorch的全连接神经网络垃圾邮件分类完整方案。项目使用MLP多层感知机模型与optimizer优化器完成有标签监督学习并借助PytorchViz库可视化网络结构通过Canvas库呈现损失函数值与识别精度随训练轮次的变化曲线帮助读者直观理解模型训练过程。压缩包共20个文件约7.18MB包含py主程序、csv与data数据文件、names与DOCUMENTATION说明、gv与png可视化图、docx报告、pdf作业要求及md说明等覆盖代码、数据、文档与图表多种类型。已有1016人学习下载适合希望快速跑通分类流程、参考网络可视化与训练曲线绘制、完成课程作业或毕业设计的读者直接使用。1. 垃圾邮件分类为什么全连接网络依然是性价比最高的起点邮箱里每天躺着几十封“恭喜您中奖”“发票代开”“低息贷款”手动删到手软。垃圾邮件分类这个任务说白了就是给一封邮件打上“垃圾”或“正常”的标签本质是一个二分类问题。很多同学一上来就想用 CNN、BERT觉得全连接网络太“浅”拿不出手。但我在实际项目里反复验证过在文本特征工程做到位的前提下一个三层全连接网络在垃圾邮件分类上的准确率能稳定跑到 97% 以上训练时间却只要几十秒。对于毕业设计来说这个方案既能体现深度学习完整流程又不会因为模型太复杂导致调参调到崩溃。这篇文章会从数据预处理、词袋特征、PyTorch 建模、训练调参到避坑排查把整条链路拆开讲清楚代码可以直接复制运行。适合正在做深度学习入门项目、需要一份能跑通的全连接网络实战案例的读者。2. 数据准备与文本向量化把邮件变成网络能吃的数字2.1 垃圾邮件数据集长什么样常见的公开垃圾邮件数据集一般是一个 CSV 或 TSV 文件两列一列是标签ham/spam 或 0/1一列是邮件原始文本。文本里包含邮件头、正文、HTML 标签、特殊符号长度从几十个字符到上万字符不等。我拿到的数据集通常有 5000 到 6000 条样本正负样本比例大概在 1:4 到 1:6 之间也就是说正常邮件远多于垃圾邮件。这个比例很重要后面讲评估指标时会用到。拿到数据后第一件事不是急着喂给模型而是做一遍探索性检查看有没有缺失值、标签分布是否极度不均衡、文本里有没有大量乱码。我一般会用 pandas 读进来打印前五行和标签计数确认数据格式没问题再往下走。import pandas as pd # 读取数据集假设是 tab 分隔列名为 label 和 text df pd.read_csv(spam.csv, sep\t, headerNone, names[label, text]) # 查看基本信息 print(df.head()) print(df[label].value_counts()) print(df.isnull().sum())这段代码做了三件事加载数据、看标签分布、检查缺失值。sep\t要根据实际文件调整有的数据集是逗号分隔。value_counts()的输出直接告诉你正负样本比例如果垃圾邮件只占 5% 以下后面训练时就要考虑加权损失函数。2.2 文本清洗的四个关键步骤原始邮件文本不能直接扔给模型里面全是噪声。我一般按四步走去 HTML 标签、转小写、去标点和数字、去停用词。去 HTML 用正则表达式[^]一把梭转小写是为了让 “Free” 和 “free” 被当成同一个词去标点用string.punctuation配合str.translate去停用词可以用 NLTK 的英文停用词表也可以自己维护一个精简列表。import re import string from nltk.corpus import stopwords stop_words set(stopwords.words(english)) def clean_text(text): # 去 HTML 标签 text re.sub(r[^], , text) # 转小写 text text.lower() # 去标点 text text.translate(str.maketrans(, , string.punctuation)) # 去数字 text re.sub(r\d, , text) # 分词并去停用词 words text.split() words [w for w in words if w not in stop_words and len(w) 2] return .join(words) df[clean_text] df[text].apply(clean_text)len(w) 2这个过滤条件是我踩坑后加的因为邮件里大量出现 “a”“an”“to” 这类短词去掉后特征维度能降不少。停用词表用 NLTK 的需要提前nltk.download(stopwords)如果环境不允许下载可以手写一个几十个词的列表替代。2.3 用词袋模型和 TF-IDF 把文本转成向量全连接网络的输入必须是固定长度的数值向量所以要把变长文本转成定长表示。最常用的两种方法是词袋模型Bag of Words和 TF-IDF。词袋模型只统计词频TF-IDF 则会给常见词降权、给稀有词升权。在垃圾邮件分类里TF-IDF 通常比纯词频效果好一到两个百分点因为像 “free”“win”“click” 这类词在垃圾邮件里高频出现但在正常邮件里也有TF-IDF 能更好地区分。from sklearn.feature_extraction.text import TfidfVectorizer vectorizer TfidfVectorizer(max_features5000, ngram_range(1, 2)) X vectorizer.fit_transform(df[clean_text]).toarray() y (df[label] spam).astype(int).valuesmax_features5000表示只保留词频最高的 5000 个词或词组这个值可以根据数据量调整数据大就设 10000数据小就设 3000。ngram_range(1, 2)表示同时考虑单个词和相邻两个词的组合比如 “click here” 作为一个特征这能捕捉到一些固定搭配。toarray()把稀疏矩阵转成稠密数组因为后面要转成 PyTorch 张量。注意如果max_features设得很大toarray()会消耗大量内存5000 维、6000 条样本大概是 240MB 左右一般机器扛得住。提示TF-IDF 的fit_transform只能用在训练集上验证集和测试集必须用同一个 vectorizer 的transform方法否则会造成数据泄露。3. PyTorch 全连接网络搭建三层结构就够了3.1 网络结构设计与维度推导输入维度是 5000TF-IDF 特征数输出维度是 1二分类用 Sigmoid 输出概率。中间隐藏层我一般用两层第一层 256 个神经元第二层 64 个神经元激活函数用 ReLU最后接一个 Sigmoid。为什么选这个结构因为垃圾邮件分类的特征空间虽然大但有效特征其实集中在几百个词上隐藏层太宽容易过拟合太窄又学不到足够模式。256-64 这个配置在我试过的五六个数据集上都表现稳定。import torch import torch.nn as nn class SpamClassifier(nn.Module): def __init__(self, input_dim): super(SpamClassifier, self).__init__() self.fc1 nn.Linear(input_dim, 256) self.fc2 nn.Linear(256, 64) self.fc3 nn.Linear(64, 1) self.relu nn.ReLU() self.sigmoid nn.Sigmoid() self.dropout nn.Dropout(0.3) def forward(self, x): x self.relu(self.fc1(x)) x self.dropout(x) x self.relu(self.fc2(x)) x self.dropout(x) x self.sigmoid(self.fc3(x)) return xDropout(0.3)是防止过拟合的关键训练时随机丢弃 30% 的神经元测试时自动关闭。fc3输出一个标量经过 Sigmoid 后变成 0 到 1 之间的概率值大于 0.5 判为垃圾邮件。输入维度input_dim要跟 TF-IDF 的max_features保持一致否则会报维度不匹配错误。3.2 数据加载与批训练配置PyTorch 用DataLoader做批训练需要先把 NumPy 数组转成 Tensor。我一般按 8:2 划分训练集和验证集用random_split或者手动切分。批大小设 64 或 128太小训练慢太大显存吃紧。优化器选 Adam学习率 0.001损失函数用BCELoss因为输出是二分类概率。from torch.utils.data import DataLoader, TensorDataset from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) train_dataset TensorDataset(torch.FloatTensor(X_train), torch.FloatTensor(y_train).unsqueeze(1)) val_dataset TensorDataset(torch.FloatTensor(X_val), torch.FloatTensor(y_val).unsqueeze(1)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) model SpamClassifier(input_dim5000) criterion nn.BCELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001)unsqueeze(1)把标签从形状(N,)变成(N, 1)跟模型输出维度对齐。shuffleTrue只在训练集上用验证集不需要打乱。random_state42保证每次划分结果一致方便复现。3.3 训练循环与验证指标训练循环里每跑完一个 epoch就在验证集上算一次准确率和损失。我一般跑 20 个 epoch如果验证损失连续 3 个 epoch 不下降就提前停。准确率在垃圾邮件分类里其实不够用因为正负样本不均衡模型全预测成正常邮件也能有 80% 准确率。所以我还会看精确率、召回率和 F1 分数。from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score for epoch in range(20): model.train() for batch_x, batch_y in train_loader: optimizer.zero_grad() output model(batch_x) loss criterion(output, batch_y) loss.backward() optimizer.step() model.eval() all_preds, all_labels [], [] with torch.no_grad(): for batch_x, batch_y in val_loader: output model(batch_x) preds (output 0.5).float() all_preds.extend(preds.numpy()) all_labels.extend(batch_y.numpy()) acc accuracy_score(all_labels, all_preds) precision precision_score(all_labels, all_preds) recall recall_score(all_labels, all_preds) f1 f1_score(all_labels, all_preds) print(fEpoch {epoch1}: Acc{acc:.4f}, Precision{precision:.4f}, Recall{recall:.4f}, F1{f1:.4f})model.eval()和torch.no_grad()是验证阶段必须加的前者关闭 Dropout后者节省显存。(output 0.5)把概率转成 0 或 1 的硬标签。如果召回率明显低于精确率说明模型漏掉了很多垃圾邮件可以调低分类阈值到 0.4 试试。4. 训练调参与效果验证让模型真正可用的几个操作4.1 学习率和批大小的组合实验学习率和批大小是影响训练效果最直接的两个超参数。我做过一组对比实验学习率 0.001 配批大小 64验证集 F1 在 0.96 左右学习率 0.01 配批大小 128F1 掉到 0.93而且训练损失震荡明显学习率 0.0001 配批大小 32F1 能到 0.97但训练时间翻倍。对于毕业设计来说0.001 配 64 是性价比最高的组合训练快且效果稳定。学习率批大小验证 F1训练时间20 epoch0.001640.96约 40 秒0.011280.93约 25 秒0.0001320.97约 90 秒如果训练损失一直不下降先检查学习率是不是太大如果训练损失下降但验证损失上升说明过拟合了加 Dropout 或者减小隐藏层维度。4.2 用混淆矩阵看模型到底错在哪准确率、F1 这些标量指标只能告诉你“好不好”不能告诉你“错在哪”。混淆矩阵能直观看到有多少垃圾邮件被误判成正常邮件有多少正常邮件被误判成垃圾邮件。后者更严重因为把正常邮件扔进垃圾箱可能导致用户漏看重要信息。from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues) plt.xlabel(Predicted) plt.ylabel(Actual) plt.show()如果混淆矩阵显示假阴性垃圾邮件判成正常很多可以调低阈值如果假阳性正常邮件判成垃圾很多可以调高阈值。我一般会把阈值从 0.5 调到 0.6牺牲一点召回率换取更高的精确率因为误杀正常邮件的代价更大。4.3 保存模型和推理单条新邮件训练完之后要把模型参数和 TF-IDF 向量器一起保存否则下次推理时没法复现同样的特征转换。PyTorch 用state_dict()保存权重用torch.save存成.pth文件。TF-IDF 向量器用 joblib 保存。import joblib # 保存模型权重 torch.save(model.state_dict(), spam_classifier.pth) # 保存 TF-IDF 向量器 joblib.dump(vectorizer, tfidf_vectorizer.pkl) # 推理新邮件 def predict_email(text, model, vectorizer): model.eval() cleaned clean_text(text) features vectorizer.transform([cleaned]).toarray() tensor torch.FloatTensor(features) with torch.no_grad(): prob model(tensor).item() return spam if prob 0.5 else ham, prob result, prob predict_email(Congratulations! You won a free iPhone!, model, vectorizer) print(fPrediction: {result}, Probability: {prob:.4f})推理时要保证clean_text函数和训练时完全一致否则特征分布对不上预测结果会莫名其妙地差。vectorizer.transform而不是fit_transform这个坑我见过太多人踩。5. 避坑与排查垃圾邮件分类项目里最容易翻车的五个地方5.1 数据泄露验证集准确率虚高到 99%现象训练时验证集准确率一路飙到 99%但拿新邮件测试时效果很差。原因在划分训练集和验证集之前就做了 TF-IDF 的fit_transform导致验证集的词汇信息泄露到了训练阶段。或者用fit_transform处理了全量数据再切分。解决先切分数据再在训练集上fit_transform验证集和测试集只用transform。这个顺序不能乱。5.2 维度不匹配RuntimeError: mat1 and mat2 shapes cannot be multiplied现象模型初始化时报维度错误提示输入特征数和全连接层权重形状对不上。原因TfidfVectorizer的max_features设了 5000但实际数据去重后只有 4800 个词toarray()出来的列数是 4800而nn.Linear(5000, 256)期望输入 5000 维。解决用vectorizer.get_feature_names_out()的长度作为input_dim或者直接X.shape[1]传给模型构造函数。不要硬编码 5000。5.3 过拟合训练集 F1 0.99验证集 F1 0.85现象训练集上表现完美验证集差一大截损失曲线一个降一个升。原因模型参数太多、训练数据太少、Dropout 没加或者太小。解决把隐藏层从 256-64 降到 128-32Dropout 从 0.3 提到 0.5或者加 L2 正则化在优化器里设weight_decay1e-4。数据量实在少的话可以做数据增强比如同义词替换。5.4 停用词表下载失败导致程序卡住现象运行nltk.download(stopwords)时网络超时程序一直卡在那里。原因NLTK 的数据服务器在境外网络不稳定时下载会失败。解决提前手动下载好停用词文件放到 NLTK 数据目录或者直接手写一个停用词列表替代。我一般手写 50 个最常见的英文停用词效果差别不大。5.5 阈值 0.5 不是万能精确率和召回率需要权衡现象模型输出的概率集中在 0.4 到 0.6 之间用 0.5 做阈值时精确率和召回率都不理想。原因Sigmoid 输出的概率分布受训练数据正负比例影响正样本少的时候概率会偏低。解决画一条精确率-召回率曲线找到 F1 最大的阈值点。或者直接用sklearn.metrics.precision_recall_curve自动选阈值。我一般会把阈值调到 0.6 左右优先保证正常邮件不被误杀。6. 从全连接网络再往前走一步特征工程和模型融合的实战技巧全连接网络跑通之后如果想进一步提升效果最有效的方向不是换更深的网络而是回头做特征工程。我在实际项目里试过几个技巧投入产出比很高。第一个是加字符级 TF-IDF把analyzerchar_wb和ngram_range(2, 4)跟词级特征拼在一起能捕捉到 “Fr33”“C1ick” 这类垃圾邮件常用的变形词。第二个是加邮件元特征比如大写字母比例、感叹号数量、URL 数量、邮件长度这些手工特征跟 TF-IDF 拼起来后F1 能再涨 1 到 2 个百分点。# 字符级 TF-IDF 与词级特征拼接 char_vectorizer TfidfVectorizer(analyzerchar_wb, ngram_range(2, 4), max_features2000) X_char char_vectorizer.fit_transform(df[clean_text]).toarray() X_combined np.hstack([X, X_char])np.hstack把两个特征矩阵按列拼接输入维度变成 7000。注意字符级特征也要在训练集上fit验证集用transform。这个操作会让训练时间增加 30% 左右但效果提升明显。另一个技巧是模型融合。训练三个不同随机种子的全连接网络推理时取平均概率。这个方法简单粗暴但能把 F1 再拉高 0.5 到 1 个百分点。我一般用三个种子42、123、2024分别训练后保存权重推理时加载三个模型取平均。def ensemble_predict(text, models, vectorizer): cleaned clean_text(text) features vectorizer.transform([cleaned]).toarray() tensor torch.FloatTensor(features) probs [] for model in models: model.eval() with torch.no_grad(): probs.append(model(tensor).item()) avg_prob sum(probs) / len(probs) return spam if avg_prob 0.5 else ham, avg_prob最后说一个我自己的习惯每次跑完实验不管效果好坏都把超参数、验证指标和混淆矩阵记到一个表格里。看起来麻烦但当你试到第十组参数时回头翻记录能省下大量重复实验的时间。垃圾邮件分类这个任务不难但要把每个环节做扎实从数据清洗到阈值选择每一步都有讲究。希望帮到你。本文还有配套的精品资源点击获取