ARTICLE DETAIL

资讯详情

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

基于CNN的垃圾邮件分类实战:从词袋到卷积核的完整指南

基于CNN的垃圾邮件分类实战:从词袋到卷积核的完整指南 简介这份毕业设计资源面向高校计算机相关专业学生与需要完成课程项目的开发者围绕基于Python卷积神经网络CNN的垃圾邮件分类系统展开帮助解决文本分类建模、模型训练与工程落地等实际问题。压缩包共14个文件约2.67MB包含4个py源码文件、5个pyc编译文件、2个pickle数据文件、1个pkl模型文件以及1份md说明和1份pdf报告覆盖数据读取、CNN模型定义、训练脚本与已训练模型等核心模块。资源中的源码均经过本地编译验证可运行评审分达到95分以上内容经助教老师审定难度适中。已有342人学习下载读者可据此快速理解邮件文本向量化、卷积特征提取与分类评估的完整流程并借助说明文档与报告梳理项目结构、复现实验步骤适合作为毕业设计参考或深度学习入门实践素材。1. 垃圾邮件分类为什么值得用 CNN 做一遍从词袋到卷积核的认知切换邮箱里那堆“发票代开”“低息贷款”不是随机骚扰它们有稳定的词序模式和局部搭配。传统做法是把邮件切成词袋丢给朴素贝叶斯或 SVM准确率能到 90% 上下但一遇到“免费 领取 优惠 券”这种拆开看都正常、连起来才可疑的短语词袋就抓瞎了。卷积神经网络 CNN 的价值恰好在这里它用滑动窗口在词向量序列上扫能捕捉“连续几个词一起出现”的局部特征再通过池化把最强信号留下来。这套毕业设计标题里包含源码、模型、说明文档和全部数据资料本质是让你用 Python 把“邮件文本 → 词向量矩阵 → 卷积提特征 → 分类”这条链路完整跑通。适合正在做 NLP 方向毕设、想找一个数据好找、模型不复杂、结果可解释的本科生也适合想补一次端到端深度学习落地经验的开发者。下面按“数据怎么进、模型怎么搭、参数怎么调、坑在哪”的顺序拆开讲。2. 数据准备与词向量化把一封邮件变成 CNN 能吃的矩阵2.1 垃圾邮件数据集长什么样标签怎么对齐常见做法是使用公开的 SMS Spam Collection 或 Enron-Spam 数据集前者约 5574 条、后者约 3 万条字段通常是label和text两列label 取值ham或spam。如果你拿到的压缩包里数据是 CSV 或 TSV先确认编码是 UTF-8否则中文邮件会乱码。标签对齐这一步不能省把ham映射为 0、spam映射为 1后面模型输出层用 sigmoid 做二分类损失函数用 binary crossentropy这是最省心的组合。import pandas as pd # 读取数据sep 按实际文件调整常见是逗号或制表符 df pd.read_csv(spam.csv, encodinglatin-1, usecols[0, 1]) df.columns [label, text] # 标签映射ham-0, spam-1 df[label] df[label].map({ham: 0, spam: 1}) # 去掉空文本和重复样本重复样本会让验证集虚高 df df.dropna(subset[text]).drop_duplicates(subset[text]) print(df[label].value_counts())逻辑说明usecols只取有用的两列避免多余列干扰drop_duplicates很关键垃圾邮件数据集里重复文本不少不去重会导致训练集和验证集泄漏准确率虚高到 99% 但实际泛化很差。参数上encoding要根据文件实际编码改英文数据集常用latin-1中文用utf-8。2.2 分词、去停用词与序列截断长度怎么定英文邮件按空格和标点切分即可中文邮件需要 jieba 之类的分词器。去停用词要谨慎free、win、cash这类词恰恰是垃圾邮件的强信号不能当停用词删掉。真正该删的是the、is、and这种高频无信息词。序列长度建议取所有邮件词数的 95 分位数常见在 50 到 100 之间。太短会截掉关键信息太长会引入大量 padding 拖慢训练。import numpy as np from tensorflow.keras.preprocessing.text import Tokenizer from tensorflow.keras.preprocessing.sequence import pad_sequences MAX_WORDS 10000 # 词表上限覆盖高频词即可 MAX_LEN 80 # 序列长度按数据 95 分位调整 tokenizer Tokenizer(num_wordsMAX_WORDS, oov_tokenOOV) tokenizer.fit_on_texts(df[text]) sequences tokenizer.texts_to_sequences(df[text]) # 统计长度分布确认 MAX_LEN 是否合理 lengths [len(s) for s in sequences] print(95分位长度:, np.percentile(lengths, 95)) X pad_sequences(sequences, maxlenMAX_LEN, paddingpost, truncatingpost) y df[label].values逻辑说明oov_token处理词表外词避免预测时遇到新词直接报错paddingpost在句尾补零配合后面的卷积层更自然。参数MAX_WORDS设 10000 是经验值词表再大低频词也学不到有效权重反而增加嵌入层参数量。2.3 嵌入层用预训练词向量还是从头训数据量小于 1 万条时从头训嵌入层容易过拟合建议加载 GloVe 或 word2vec 预训练向量把embedding_matrix传给Embedding层的weights参数并设trainableFalse先冻结。数据量超过 5 万条时可以从头训让模型自己学领域相关的词表示。这个选择直接决定模型收敛速度和最终 F1是选型阶段最该想清楚的一步。# 假设已加载 GloVe 词向量到 embeddings_index 字典 EMBED_DIM 100 embedding_matrix np.zeros((MAX_WORDS, EMBED_DIM)) for word, i in tokenizer.word_index.items(): if i MAX_WORDS: continue vec embeddings_index.get(word) if vec is not None: embedding_matrix[i] vec逻辑说明词表索引超过MAX_WORDS的直接跳过保证矩阵行数和嵌入层输出维度一致。没命中预训练向量的词保持全零训练中会慢慢学到值。这一步做完就可以进入模型搭建。3. 用 Keras 搭一个能跑通的 TextCNN结构、参数与训练循环3.1 TextCNN 的三层核心结构为什么这样设计TextCNN 的结构不复杂嵌入层输出(batch, seq_len, embed_dim)的矩阵然后并行走三个卷积核尺寸分别是 2、3、4每个尺寸 128 个滤波器。为什么用多个尺寸因为 2 元组捕捉“免费 领取”这种双词搭配3 元组捕捉“点击 链接 领取”4 元组捕捉更长的固定话术。每个卷积后面接全局最大池化把每个滤波器的最强响应取出来最后拼接成一个 384 维向量过 dropout 再进全连接层输出一个概率值。from tensorflow.keras import layers, models def build_textcnn(vocab_size, embed_dim, embed_matrix, max_len): inputs layers.Input(shape(max_len,)) x layers.Embedding(vocab_size, embed_dim, weights[embed_matrix], trainableFalse)(inputs) # 三种卷积核并行提取不同长度的 n-gram 特征 convs [] for kernel_size in [2, 3, 4]: c layers.Conv1D(128, kernel_size, activationrelu)(x) c layers.GlobalMaxPooling1D()(c) convs.append(c) x layers.Concatenate()(convs) x layers.Dropout(0.5)(x) x layers.Dense(64, activationrelu)(x) outputs layers.Dense(1, activationsigmoid)(x) model models.Model(inputs, outputs) return model model build_textcnn(MAX_WORDS, EMBED_DIM, embedding_matrix, MAX_LEN) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) model.summary()逻辑说明GlobalMaxPooling1D而不是普通池化是因为文本分类只关心“有没有出现强特征”不关心出现在哪个位置。Dropout(0.5)放在拼接后抑制全连接层过拟合。参数上滤波器数量 128 是常见起点数据量小可以降到 64数据量大可以升到 256。3.2 训练循环、早停与类别不平衡处理垃圾邮件数据通常 ham 多 spam 少比例可能 5:1 甚至 10:1。直接训练模型会偏向预测 ham召回率低。两种处理方式一是class_weight给 spam 更高权重二是过采样 spam 到和 ham 相当。推荐第一种改动小且不引入重复样本。from sklearn.utils import class_weight from tensorflow.keras.callbacks import EarlyStopping # 计算类别权重 weights class_weight.compute_class_weight( class_weightbalanced, classesnp.unique(y), yy ) class_weight_dict {0: weights[0], 1: weights[1]} early_stop EarlyStopping(monitorval_loss, patience3, restore_best_weightsTrue) history model.fit( X, y, validation_split0.2, epochs20, batch_size64, class_weightclass_weight_dict, callbacks[early_stop] )逻辑说明class_weightbalanced自动按类别频率反比给权重spam 少就权重大模型会更关注 spam 的错分。EarlyStopping的patience3表示验证损失连续 3 轮不降就停restore_best_weights回滚到最优轮次避免最后一轮过拟合。参数batch_size64在几千条数据上比较稳太大收敛慢太小梯度噪声大。3.3 评估指标不能只看准确率垃圾邮件分类里把正常邮件误判为垃圾假阳性比漏掉垃圾邮件假阴性更严重因为用户可能错过重要邮件。所以评估要看 precision、recall 和 F1尤其是 spam 类的 recall。混淆矩阵能直观看出两类错分情况。from sklearn.metrics import classification_report, confusion_matrix y_pred (model.predict(X) 0.5).astype(int) print(confusion_matrix(y, y_pred)) print(classification_report(y, y_pred, target_names[ham, spam]))逻辑说明阈值 0.5 是默认值如果更在意 spam 召回可以降到 0.3 到 0.4代价是假阳性上升。这个阈值要在验证集上调不能拍脑袋定。classification_report会给出每类的 precision、recall、F1重点看 spam 那一行。4. 调参与排错TextCNN 在垃圾邮件任务上的避坑清单4.1 避坑一验证集准确率 99% 但线上预测全错现象训练时验证准确率冲到 99%拿几条新邮件测试却几乎全判成 ham。原因数据去重没做训练集和验证集有重复样本模型只是记住了样本而不是学到模式。解决在划分训练验证集之前先drop_duplicates并且用train_test_split时设stratifyy保证两类比例一致。更严格的做法是按邮件来源或时间划分避免同源邮件同时出现在训练和验证集。4.2 避坑二嵌入层可训练导致过拟合现象训练损失持续下降验证损失从第 3 轮开始上升F1 卡在 0.85 上不去。原因数据量小的时候嵌入层参数量远大于样本数模型把词向量调得只适合训练集。解决把Embedding层的trainable设为False冻结预训练向量如果效果不够再解冻最后几层做微调。同时把 dropout 从 0.3 提到 0.5加 L2 正则。4.3 避坑三序列截断把关键信息切掉了现象模型对长邮件判断特别差短邮件正常。原因MAX_LEN设得太小比如设成 30而垃圾邮件往往话术很长关键短语在句尾被截掉。解决先统计序列长度分布取 95 分位作为MAX_LEN。如果长邮件确实重要可以取 99 分位但要注意显存和训练时间。另一个技巧是truncatingpre保留句尾因为很多垃圾邮件的行动号召在最后。4.4 避坑四中文邮件没做分词直接按字输入现象中文数据集上准确率只有 70% 左右明显低于英文。原因中文没有空格按字符切分虽然能跑但卷积核学不到词级别的搭配。解决用 jieba 分词后再进 Tokenizer或者直接用字符级加更大的卷积核。分词后记得把MAX_WORDS调大中文词表比英文大不少。4.5 避坑五预测时 tokenizer 和训练时不一致现象模型保存后重新加载预测结果和训练时对不上。原因只保存了模型权重没保存 tokenizer 的词表重新加载时 tokenizer 重新 fit 导致索引错位。解决用pickle把 tokenizer 一起存下来预测时加载同一个 tokenizer 做texts_to_sequences。或者用tf.keras.models.save_model保存完整模型但 tokenizer 仍需单独处理。import pickle # 保存 with open(tokenizer.pkl, wb) as f: pickle.dump(tokenizer, f) model.save(textcnn_spam.h5) # 加载预测 with open(tokenizer.pkl, rb) as f: tokenizer pickle.load(f) model tf.keras.models.load_model(textcnn_spam.h5) seq tokenizer.texts_to_sequences([free money click now]) pad pad_sequences(seq, maxlenMAX_LEN, paddingpost) print(model.predict(pad))逻辑说明tokenizer 保存的是词到索引的映射必须和模型权重配套使用。pad_sequences的参数要和训练时完全一致包括maxlen、padding、truncating任何一项不同都会导致输入分布偏移。5. 从能跑到好用模型固化、阈值调优与一个可复现的验证习惯模型训练完只是起点真正决定这套系统能不能用的是推理阶段的稳定性和阈值选择。我一般会把验证集上的预测概率画出来看 ham 和 spam 的概率分布有没有重叠区。如果重叠少0.5 阈值就够如果重叠多就要在 precision 和 recall 之间做取舍。具体做法是遍历 0.1 到 0.9 的阈值算每个阈值下的 F1取最高的那个。from sklearn.metrics import f1_score probs model.predict(X_val).ravel() best_thr, best_f1 0.5, 0 for thr in np.arange(0.1, 0.9, 0.05): preds (probs thr).astype(int) f1 f1_score(y_val, preds) if f1 best_f1: best_f1, best_thr f1, thr print(f最佳阈值 {best_thr:.2f}, F1 {best_f1:.4f})这段代码的逻辑很直白在验证集上找让 F1 最大的阈值而不是默认 0.5。参数np.arange(0.1, 0.9, 0.05)的步长 0.05 够用再细意义不大。注意这个阈值要在独立的验证集上调不能拿训练集或测试集调否则又是另一种泄漏。另一个容易被忽略的点是模型固化后的输入校验。线上来的邮件可能包含大量 HTML 标签、URL、特殊符号训练时如果没做清洗推理时就会遇到大量 OOV。我习惯在预处理里加一步正则清洗去掉 HTML 标签、把 URL 替换成统一占位符、把连续数字替换成NUM。这一步在训练和推理时必须用同一个函数不能训练时清、推理时不清。import re def clean_text(text): text re.sub(r[^], , text) # 去 HTML 标签 text re.sub(rhttp\S|www\.\S, URL , text) # URL 占位 text re.sub(r\d, NUM , text) # 数字占位 text re.sub(r\s, , text).strip() return text.lower()逻辑说明URL 和数字占位是为了减少词表膨胀同时保留“这里有链接”“这里有数字”的信号。lower()统一大小写避免Free和free被当成两个词。这个函数要同时用在训练数据预处理和推理输入上建议单独放一个模块里两边 import 同一个函数。最后说一个我踩过的坑有次把模型和 tokenizer 都保存了但忘了保存MAX_LEN和MAX_WORDS这两个常量换台机器推理时用了默认值结果输入序列长度对不上模型直接报维度错误。后来我养成的习惯是把所有预处理参数写进一个config.json和模型文件放一起加载时先读配置再建 tokenizer 和 padding。这个习惯看起来笨但省了无数次“明明训练时好好的”的排查时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表