☰
垃圾邮件分类实战:从高质量数据集构建到BERT微调
2026/10/20 2:56:12 网站建设 项目流程

做了几年文本类项目,收到最多的需求之一就是“帮我做个垃圾邮件过滤器”,但等真正拿到数据才开始头疼:公开的邮件数据集要么太旧,要么字段混乱,要么标签只有 spam/ham 却没有中英文混合样本,更别提可以直接用于深度学习模型微调的格式。后来我花了两周时间自己整理并优化了一套邮件分类数据集,也顺手把整个算法优化过程跑了一遍,从经典分类器一路试到 BERT 微调,把过程中踩到的坑和有效做法都沉淀了下来。这篇文章想把这些经验完整拆开讲清楚:数据到底怎么构建才算“高质量”、标签和字段如何设计才能直接用于训练、预处理环节哪些细节决定了模型上限,以及钓鱼邮件识别、多语言 NLP 场景下要特别注意什么。无论是想做毕业设计、公司内部的反垃圾系统,还是单纯对文本分类和模型微调感兴趣,按这条链路走都能少走不少弯路。

需要先说明一点,后面提到的所有处理和实验,我都以实际可复现的工程角度来写,会给出字段设计、清洗规则、脱敏方式、微调参数和踩坑记录。数据集的构建不是单纯“攒一批邮件”那么简单,有些背景知识不讲清楚,后面模型出了问题都很难定位。

1. 别急着上深度学习——先看清垃圾邮件数据集的“坑”

很多人拿到类似标题的数据集,第一反应就是直接丢给 BERT 微调。但我建议先把数据集本身摊开看一遍,因为垃圾邮件分类这个任务有个显著特点:它不像情感分类那样依赖长文本的全局语义,更多是短文本、关键词、URL、发件人特征和结构特征的组合判断。如果前期没把数据维度设计好,后面模型再强也救不回来。

1.1 公开数据集普遍存在的三个硬伤

先说我用过的几类公开邮件数据集,问题基本集中在三处。

第一是时效性差。垃圾邮件对抗性极强,三年前的特征到今天早就变形了。比如早期垃圾邮件喜欢堆“中奖”“免费”这类词,现在的钓鱼邮件开始伪装成官方通知,用“账号异常”“验证码过期”“紧急更新”等话术。拿旧数据训出来的模型,在真实环境误报率会高得离谱。

第二是标签不干净。很多公开集的标签来源是用户手动标记,本身就存在大量误标。更麻烦的是有些邮件属于“灰色地带”,比如正常的营销邮件、订阅通知,不同人判断不一致,模型训练时很容易学出错误的决策边界。

第三是字段过少。原始邮件数据集往往只保留正文和标签,丢掉了发件人域名、回复地址、邮件头信息、HTML 结构特征。这些结构特征恰恰在钓鱼邮件识别里非常关键。一套只含“文本+标签”的数据,等于砍掉了模型一半的线索。

1.2 什么才算“适合训练”的高质量邮件分类数据

我自己经过几轮返工后,总结出了一个可执行的质量标准,分享出来可以参考着自查。构建或筛选数据集时,我会按下面几项逐一核对:

  • 二元分类标签明确,且最好配套置信度或来源说明(如“规则标注”还是“用户反馈”)。
  • 正负样本比例可控,垃圾邮件和正常邮件的比例不要天然失衡到 1:20 以上。
  • 文本长度覆盖完整,既要有超短文本(一两句话的钓鱼邮件),也要有长文本(完整营销文),避免模型对长度产生隐性偏好。
  • 时间跨度广,至少包含跨月甚至跨年的样本,才能验证模型的时间鲁棒性。
  • 字段包含正文、发件人域名、主题行,最好还能有 HTML 标签剥离后的纯文本版本。

这不是吹毛求疵。我自己第一次拿一个结构简单的数据集做 BERT 微调时,F1 看似有 0.96,但换到真实收件箱一测,误判率惨不忍睹。后来逐条排查才发现,数据里 90% 的“垃圾邮件”长度都集中在 10-30 个 token,模型把“短文本”当成了垃圾邮件的先验特征,而不是真正学会识别恶意内容。这一点如果不亲自动手构建数据集,很难提前意识到。

2. 构建可复用的邮件分类 CSV:字段设计、清洗流程与合规脱敏细节

这部分我重点讲两个事:第一,一个能直接用于 BERT 微调和经典机器学习的 CSV 数据集应该长什么样;第二,从原始邮件到最终 CSV,清洗和脱敏环节必须过哪些关卡。别小看这一步,数据集质量好不好,基本都在这里决定。

2.1 字段设计:除了 text 和 label,你还缺什么

先给一套我最终采用的字段结构,这套结构在普通分类、钓鱼识别、多语言实验中都能直接落库:

字段名类型说明
idstring样本唯一编号,方便回溯
textstring清洗后邮件正文,中文/英文原样保留
subjectstring主题行,单独成列,便于做拼接特征
sender_domainstring发件人域名,小写归一化
body_lengthint字符长度,可作为辅助特征
labelint二分类标签:0 为正常邮件,1 为垃圾邮件
langstring语言标记,如 zh、en、zh-en,便于多语言筛选
sourcestring标注来源:rule、user_report、public_dataset 等

为什么要单独拆sender_domain?因为垃圾邮件和钓鱼邮件的判断不能只看正文,很多钓鱼邮件正文写得极其逼真,但发件域名是刚刚注册的随机字符串。把域名独立出来,既方便模型结合文本和域名特征做判断,也方便做规则层的黑白名单兜底。

subject单独成列同样重要。主题行和正文的语义有时是互补的,比如正文内容看起来很正规,但主题行用了“URGENT REPLY NEEDED”这类紧迫性词汇,大概率有问题。在训练时可以拼接subject + text,也可以分别编码后再融合,留出实验自由度。

2.2 清洗流程:HTML 剥离、编码修复、敏感信息识别

原始邮件数据通常是混合格式,有纯文本,有 HTML 邮件,还有带附件的 MIME 格式。不能直接拿来训练,至少要经过下面几步:

先用解析库提取正文。我实际用的是 email 标准库配合 BeautifulSoup 处理 HTML 部分,剥离 script、style 标签,只保留可见文本。如果原邮件是 Base64 编码的,先解码再转码,统一成 UTF-8。

然后是编码修复。中文邮件经常出现 GB2312、GBK、UTF-8 混用的情况,解析后会出现乱码。这里需要做一层编码检测和替换,统一转成 UTF-8 文本。我的做法是先尝试decode('utf-8'),失败就降级到 GBK 或 GB18030,再不行就丢弃该字段而不是保留乱码。

敏感信息识别和脱敏直接关系到 GDPR 合规。邮件里最常见的敏感数据包括个人邮箱地址、手机号码、姓名、地址、银行账号等。需要注意一个原则:模型不需要知道真实的联系方式,所以脱敏不能随机乱替换,而要保持格式一致性。比如把example@email.com替换为user001@domain.com,把手机号替换成13800000001这样的占位符。这样既保护隐私,又保留了“文本结构”供模型学习。

附件的处理我建议直接跳过正文部分。附件属于二进制内容,不在文本分类主线里。如果是包含附件的邮件,可以把“是否有附件”记录成一个布尔特征,而不把附件内容塞入训练集,避免引入大量噪声。

2.3 GDPR 合规处理在数据集构建中的实际落点

这部分如果没有实际处理过,很容易理解偏差。GDPR 要求可识别个人身份的信息在被处理时有合法依据,而训练集里的邮件正文天然包含大量个人信息。我的做法是分三层处理:

第一层是主动脱敏,在清洗阶段就把邮箱、电话、具体地址替换为伪数据,从源头降低敏感信息的存量。

第二层是保留期限与使用范围控制,训练用的副本不与原始邮件库长期共存,验证实验完成后,老副本直接删除,仅保留脱敏后的 CSV 版本。

第三层是标注环节的人为约束,人工标注人员只能看到脱敏后文本,不能访问原始邮件系统和附件。这个约束在实际操作中很容易忽视,一旦标注者能打开原始邮箱,脱敏就形同虚设。

我知道有些团队用纯规则做脱敏,比如正则匹配邮箱、手机号。正则的问题在于覆盖率有限。比较稳妥的做法是正则先粗筛,再用一个小的命名实体识别模型兜底,识别地址和人名。我在实验中的感受是,正则能覆盖六七成,加上实体识别模型能提到九成以上,剩下不到一成的边缘样本人工复核,也顺手清理了噪声标签。

3. BERT 微调做垃圾邮件分类:数据集设计如何决定模型上限

数据集梳理清楚之后,才轮到模型。为什么大家都往 BERT 上走?因为在短文本分类里,词序和上下文太重要了。像“您的中奖邮件”这个词组,词袋模型会当成垃圾特征,但实际放在正常营销语境里可能完全是另一回事。BERT 这类预训练语言模型能把它当成“交互上下文”去理解,所以效果普遍优于 TF-IDF 加逻辑回归。

但这里有个容易被忽略的点:BERT 是语言模型,不是规则引擎,它对数据格式和样本配比极其敏感。

3.1 训练集格式与 BERT 输入序列设计的匹配

BERT 的输入是 token 序列,最长通常 512。邮件正文一般不会超长,但要注意:直接把subject和body拼接时,要在中间加入分隔标记,比如最常见的[CLS] subject [SEP] body [SEP]。我一开始没有做显式分隔,就是把字符串硬拼起来,结果模型对拼接处的语义判断明显不稳定。

更好的做法是把发件人域名作为一个[MASK]前导片段拼进输入。比如最终序列设为:

[CLS] sender_domain:{domain} [SEP] subject:{subject} [SEP] body:{body} [SEP]

这个格式有两个好处:一是给模型明确的“领域切换”信号,让它知道哪个区域是域名,哪个区域是正文;二是方便后续做消融实验,比如分别测试“只看正文”和“正文加域名”的效果差异。

3.2 微调参数的选择与实验记录

我用的预训练模型是常见的中文 BERT 底座,如果你想做中英混合,也可以用多语言版本。核心参数我列在下面,这些数值是我跑了多组实验后相对稳定的配置:

参数推荐值说明
batch size16显存不够就 8,梯度累积两步
max length200覆盖绝大多数邮件,长文本截断
learning rate2e-5太大容易灾难性遗忘
epochs3-4小样本集尽量不超过 4
warmup steps500稳定训练初期
weight decay0.01缓解过拟合

实际训练时我有个习惯:把验证集固定住,每轮结束后记录精确率、召回率和 F1,而不是只看最终一轮。垃圾邮件分类里,正常邮件被误判为垃圾邮件的代价非常高,所以“精确率”和“召回率”必须分开看。我遇到过一个隐藏很深的坑:训练集里 95% 都是正常邮件,垃圾邮件只有 5%。模型学到最后,把绝大多数垃圾邮件都正确识别了,但正常邮件的被误判率也爬到 5%。看起来准确率 96%,实际上用户每收 20 封正常邮件就有 1 封被吞掉,这种模型根本不敢上线。

3.3 类别不平衡与负样本采样策略

针对上面的问题,我最后采用的是分层采样加类别权重结合的方式。具体做法:

先按 label 分层划分训练集/验证集,保证每一折里正负样本比例都和原始分布一致。然后在训练时给少数类样本提高损失权重,我用的是class_weight = {0: 1.0, 1: 2.5},这个数值可以根据验证集 F1 微调。同时,对多数类做下采样,让训练时正负比例大致在 1:3 到 1:4 之间,而不是天然的 1:20。这一套组合下来,在不牺牲正常邮件召回率的前提下,垃圾邮件的 F1 提升了约 7 个百分点。

负样本采样还有一个技巧:不要简单随机删除多数类,而是观察哪些正常邮件容易被模型误判,比如带“优惠”“点击链接”等词的高营销属性邮件,适当多保留一些。这样模型能更清晰地学习到“正常营销邮件”和“垃圾邮件”之间的模糊边界。

4. 钓鱼邮件识别与多语言场景:评估指标和数据增强是分水岭

把模型在通用垃圾邮件上跑出来并不算完成。真实的邮件环境里还有两类非常烦人的情况:钓鱼邮件和混合语言邮件。它们的表现形态和普通 spam 差异巨大,数据集的构建和模型调优也必须跟着变。

4.1 钓鱼邮件为什么不能只看正文语义

钓鱼邮件的最大特征是“伪装”。正文读起来完全像官方通知,甚至带着合法公司的签名和免责声明,但目的其实是想引导用户点击链接或输入账号密码。如果模型只依赖正文文本,这些邮件几乎拦不住。

我整理钓鱼邮件样本时,额外记录了三类特征:

  • 链接文本与目标地址的一致性:比如文字写的是security-check,实际链接指向一串乱码域名。
  • 发件人域名与声称机构域名是否匹配:比如声称来自某银行,但发件域是刚注册的免费域名。
  • 正文中是否存在紧急施压类话术:例如“24 小时内不处理将冻结账号”。

这三类特征可以作为规则层先做一道初筛,也可以作为结构化特征输入模型。我在数据集构建时,把这些信息折算成辅助列。这样 BERT 模型在微调时既能看到语义,又能看到结构化线索,对于高仿钓鱼邮件的识别率提升很明显。

4.2 多语言样本的组织与处理

邮件分类的多语言场景主要出现在中英文混合、以及少量亚洲语言混杂。处理多语言文本时,统一用多语言预训练模型是最稳妥的选择,因为它不需要单独训练多个单语模型。

但数据集格式要注意两点:语言标记列lang必须准确,不能只靠“文本里有没有中文字符”来判断,因为很多中文邮件实际包含大量英文品牌名、优惠码和链接。更合理的做法是检测正文主要语言占比,比如用字符分类器统计中文字符与拉丁字符的比例,超过阈值才标记为 zh。

混合语言样本非常考验 BERT 的分词能力。中文用字粒度,英文用词粒度,中英混排时,文本会被切得很碎。不要人为添加“中文和英文之间加空格”这种后处理,反而破坏了原始语言的形态。我是直接保留原文本,让模型自己学习对齐。

4.3 数据增强策略:别做无意义的同义词替换

很多教程喜欢用同义词替换做数据增强,但我建议在垃圾邮件分类里慎用。垃圾邮件的文本特征经常是高度反常规的拼写、数字、缩写,比如“f-r-e-e”、乱序大写、谐音词。同义词替换会把这些对抗特征“纠正”掉,等于抹掉了模型需要学习的关键痕迹。

真正有效的数据增强手段是:对已有垃圾邮件样本做小幅度扰动,比如随机丢弃部分 HTML 标签文本、保留多个版本的纯文本提取结果、对换主题行和正文的顺序,或者合成少量“伪装式”钓鱼邮件(正常邮件 + 替换链接域名)。这些小扰动不改变核心语义,但能提高模型对不同排版和文本形态的鲁棒性。

我自己还试过资金预算充足的方式,用生成模型构造假的正规通知邮件,配合真实垃圾邮件做半监督训练,效果确实能提升 2 到 3 个点的 F1,但需要额外标注成本,适合数据量已经较大时再做。

5. 实测踩坑记录:从标签翻转、数据泄漏到调参方向

这部分写一些亲测过的翻车经历,真心希望有人看了能绕开。很多实验看着跑得很顺,实际上模型成果没法落地,写完才发现是某些低级细节搞砸了。

5.1 数据泄漏翻车实录

第一次做邮件分类实验时,我犯了一个特别隐蔽的错误。我在清洗阶段把包含“unsubscribe”的退订链接文本全部删掉了,本意是减少噪声。结果验证集里几乎所有的正常营销邮件都因为包含退订链接特征被模型识别为正常,而垃圾邮件没有退订链接。模型过于依赖“有无 unsubscribe 字样”这条特征,测试集上 F1 极高,一等换了真实数据立刻崩盘。

教训有两层:第一,不能在构建训练集时做面向标签差异的特征删减,因为它会引入隐性泄漏;第二,验证集和测试集必须保持原始邮件的结构完整性,不能和训练集共用同一套清洗管道中“基于语义朝向标签”的规则。数据预处理规则要和训练流程解耦,避免让模型钻规则的空子。

5.2 标签噪声与置信度修正

垃圾邮件标注里,很大一部分标注误差来自“灰色邮件”。比如带促销性质的金融邮件、展会邀请、校友捐赠邮件,它们确实不该进收件箱最明显的位置,但也不至于标注成垃圾邮件。早期我没有处理这种灰色地带,导致模型在这部分样本上反复横跳。

后来我改成了三级标注:0 正常、1 明确垃圾、2 灰色(丢弃或单独存留)。训练时只用 0 和 1,并且给 1 类样本增加一个置信度列,来源为“规则 + 用户反馈”双确认的样本,置信度设为 1.0,只有单一来源的设为 0.8。处理完这部分,模型决策边界反而更清晰,F1 也稳定了不少。

建议各位如果是从公开数据集中二次清洗,一定要抽样人工复核标签一致性。我用了一个简单脚本:随机抽 500 条样本,让三个人独立打标,统计一致率。低于 95% 的标签,基本说明原数据不干净,不能直接用于微调。

5.3 调参方向:从“提高准确率”转向“控制误报成本”

最后聊一下调参心态。垃圾邮件分类和很多 NLP 任务不同,它不是把准确率拉高就完事。在实际应用里,最大的成本是“正常邮件被误判为垃圾邮件”,这不仅影响用户体验,还可能造成业务事故。所以在调参时,我会盯住一个综合指标:正常邮件召回率保持在 98% 以上的前提下,垃圾邮件的 F1 尽可能高。

想要达到这个目标,不能只调学习率和 batch size。我更常用的做法是调整“判定阈值”:BERT 输出的概率默认以 0.5 为界,但可以根据业务风险偏好,把阈值调高到 0.7 或 0.8。这样模型只在确信度高时才判为垃圾,误杀明显减少,代价是漏过少量低确信度的垃圾邮件,可以用规则层策略补上。这个思路不算新颖,但职场上真的有很多人只盯 F1,忽略了业务侧的接受度。

如果你正在用相似的数据集做自己的项目,我的建议是:尽量把时间花在数据清洗和字段设计上,而不是反复换模型结构。垃圾邮件分类的难点不在模型容量,而在数据噪声和特征表达。整理好一份干净、字段完整、标签清晰的邮件分类数据,你的模型就算用最简单的结构也能跑出不错的效果;反之,数据一塌糊涂,再强的模型都是空中楼阁。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询