垃圾邮件过滤成功,朴素贝叶斯算法科普,奥秘揭晓?
摘要:引子 你的邮箱是否经常遭遇一堆邮件的狂轰滥炸?看到未读邮件数量突破四位数大关是否有种无力感?要命的是其中很多都是垃圾邮件,毫无价值,可你又不得不一个个地点开确认,以免错过重要邮件。 那么你很幸运,今天我们将揭秘垃圾邮件过滤系统背后的数学原理
引子
你的邮箱是否经常遭遇一堆邮件的狂轰滥炸?看到未读邮件数量突破四位数大关是否有种无力感?要命的是其中很多都是垃圾邮件,毫无价值,可你又不得不一个个地点开确认,以免错过重要邮件。
那么你很幸运,今天我们将揭秘垃圾邮件过滤系统背后的数学原理,高中生就能学会,超简单的哦!(๑•̀ㅂ•́)و✧
良心知识点回顾
我们首先抬起右手与胸齐平(双手也不介意),摸摸对自己高中数学老师的良心 (如果有的话 ,问问自己还记不记得条件概率这玩意(贝叶斯就算了,难度过高 ´﹏`)
好吧这都无所谓,我们有良心的知识点回顾供我们良心地回顾知识点(
所谓条件概率,符号 \(P(B|A)\),表示在 \(A\) 发生的前提下 \(B\) 也发生的概率
想想我们直觉很容易接受的古典概型,\(N\) 表示所有事情的数量,\(n(A)\) 表示 \(A\) 事件的数量,那么 \(P(A)= \frac{n(A)}{N},P(A|B)= \frac{n(A \cap B)}{n(B)} = \frac{P(A \cap B)}{P(B)}\)
同样地我们看看 \(P(B|A)= \frac{n(A \cap B)}{n(A)} = \frac{P(A \cap B)}{P(A)}\)
注意到 \(P(A|B)\) 与 \(P(B|A)\) 的表达式都有一个共同的 \(P(A \cap B)\) !
那就连起来看
\[P(A\cap B) = P(A|B) \cdot P(B) = P(B|A) \cdot P(A)
\]
选定 \(P(A|B)\) 为我们研究的主体对象,\(P(B|A)\) 为得到的关键信息,写成
\[P(A|B) = \frac{P(B|A) \cdot P(A)}{P(B)}
\]
这就是伟大的贝叶斯公式了!
๑•́ ₃ •̀๑
(通关 大捷 bushi
好吧也差不多了
想想这个公式要表达什么
举个栗子:天气预报告诉你今天有 \(50\%\) 的概率(对应 \(P(A)\))会下雨,然后你惊恐地发现地面是湿的 (对应 \(B\) 事件),而你又知道下雨的话地面多半是湿的(对应 \(P(B|A)\),上面这些都在右边式子),你观察到的这个新的情况 (事件 \(B\)) 强化了今天下雨的可能性,也就是说,在地面变湿的新情况下,今天会下雨的可能性变成了 \(P(A|B)\)。
一言以蔽之,新的信息可以更新旧的判断,让判断更接近真相
在这里说明一下严谨的概念定义,当然并不重要(科普嘛)
\(P(A)\) 叫做先验概率,就是没有新信息之前我们的判断
\(P(B)\) 是证据,观察到的新信息
\(P(B|A)\) 称为似然度,现实中的由因到果,即已经发生了A(下雨啦),然后 \(B\)(地面湿了)发生的概率
\(P(A|B)\) 就是后验概率,即我们要更新的判断
诶?这过程好像有点像我判断垃圾邮件:一开始不确定,然后看到 “免费” 二字,再看到 “中奖” ,甚至又看到 “点击链接” ,绷不住了再见了……
经验告诉我们,垃圾邮件中这些词极有可能会出现,出现这些词的极有可能是垃圾邮件
就这样我们看到的新信息逐步强化了我们对垃圾邮件判断的准确性
朴素贝叶斯算法
完整地梳理一遍
\[P(\text{垃圾|词})=\frac{P(\text{词|垃圾})\cdot P(\text{垃圾})}{P(\text{词})}
\]
首先我们得有一些经验,从以往的一堆邮件中得知 \(P(\text{垃圾 or 正常}) = \frac{n(\text{垃圾 or 正常})}{n(\text{总邮件数})}\),即先验概率
然后从当前邮件中扒拉出一堆词 \(w_1,...,w_n\),
算这两个玩意 \(P(\text{垃圾}|w_1,...,w_n),P(\text{正常}|w_1,...,w_n)\)
比比谁大作判断
当然 \(P(\text{词})\) 都一样,于是只要算 \(P(w_i|c)\),即如果邮件是 \(c\) 这种类型(正常或垃圾),那么出现 \(w_i\) 这个词的概率,这个怎么算我们稍后再说
朴素贝叶斯中,我们认为
\[P(c|w_1,...,w_n)=\frac{P(c)P(w_1,...,w_n|c)}{P(w)}=\frac{P(c)\prod_{i=1}^n P(w_i|c)}{P(w)}
\]
也就是说假设 \(w_i\) 相互独立。虽然这样的假设往往是错的,但最终判断的结果却很好。因为我们只要比比概率相对大小根本不关心概率究竟是多少,只要定性做出分类判断而已。而且这样做也使得数学方面异常简单,计算起来很方便,所以朴素也是一种力量哦
说回这个 \(P(w_i|c)\) 怎么算,想象中应该是 \(\frac{n(\text{在该类别中该词出现数})}{n(\text{该类别中词出现总数})}\)
但会有问题,因为我们上面用了连乘算结果,如果这个词没有在该类别出现,那么 \(P(w_i|c)=0\) ,结果也会变成 \(0\),但我们不能因此判断这个邮件就不是该类别
所以我们假设所有不同的词都已经出现过一次
那么 \(P(w_i|c)=\frac{n(\text{在该类别中该词出现数})+1}{n(\text{该类别中词出现总数})+n(\text{不同词总数})}\)
这个操作被称为拉普拉斯平滑
当然概率都是小于 \(1\) 的数,这么多乘起来很小,计算机极有可能认为是 \(0\),称为下溢
为了防止数值下溢,我们取对数
最终表示
\[F(c)=\log P(c)+\sum_{w_i\in\text{该邮件}}\log P(w_i|c)
\]
算法流程
先有一堆邮件集合和类别(垃圾或正常),分词,得到所有不同的单词
接着按上文计算 \(P(c),P(w_i|c)\)
然后得到一份待判断类别的邮件,分词,计算 \(F(c)\),判断是垃圾还是正常邮件
下面用北太天元实现了垃圾邮件分类的简化版演示代码 naive_Bayes.m
输出结果到 result.txt 中
$\text{naive_Bayes.m}$
% 朴素贝叶斯垃圾邮件分类
% 北太天元
clear all; close all; clc;
%% 打开输出文件
fid = fopen('result.txt', 'w');
fprintf(fid, '朴素贝叶斯垃圾邮件分类\n');
fprintf(fid, '======================\n\n');
%% 1. 关键词
keywords = {'中奖','免费','优惠','红包','现金','点击','链接','注册','验证','账户',...
'恭喜','幸运','获奖','领取','名额','会议','报告','项目','学习','请假'};
n_features = length(keywords);
fprintf(fid, '关键词数量: %d\n', n_features);
%% 2. 训练数据(50封邮件:25垃圾,25正常)
p_spam_garbage = 0.7; % 垃圾邮件中出现垃圾词的概率
p_spam_normal = 0.1; % 垃圾邮件中出现正常词的概率
p_normal_garbage = 0.1; % 正常邮件中出现垃圾词的概率
p_normal_normal = 0.7; % 正常邮件中出现正常词的概率
X_train = zeros(50, n_features);
y_train = zeros(50, 1);
% 生成25封垃圾邮件
for i = 1:25
for j = 1:15
if rand() < p_spam_garbage
X_train(i, j) = 1;
end
end
for j = 16:20
if rand() < p_spam_normal
X_train(i, j) = 1;
end
end
y_train(i) = 1;
end
% 生成25封正常邮件
for i = 26:50
for j = 1:15
if rand() < p_normal_garbage
X_train(i, j) = 1;
end
end
for j = 16:20
if rand() < p_normal_normal
X_train(i, j) = 1;
end
end
y_train(i) = 0;
end
n_samples = 50;
n_spam = 25;
n_normal = 25;
fprintf(fid, '训练数据: %d封 (垃圾:%d, 正常:%d)\n', n_samples, n_spam, n_normal);
%% 3. 计算先验概率
P_spam = n_spam / n_samples;
P_normal = n_normal / n_samples;
fprintf(fid, '先验概率: P(垃圾)=%.3f, P(正常)=%.3f\n', P_spam, P_normal);
%% 4. 计算条件概率(拉普拉斯平滑)
alpha = 1;
P_given_spam = zeros(1, n_features);
P_given_normal = zeros(1, n_features);
for i = 1:n_features
count_spam = 0;
count_normal = 0;
for j = 1:n_samples
if y_train(j) == 1 && X_train(j, i) == 1
count_spam = count_spam + 1;
end
if y_train(j) == 0 && X_train(j, i) == 1
count_normal = count_normal + 1;
end
end
P_given_spam(i) = (count_spam + alpha) / (n_spam + 2 * alpha);
P_given_normal(i) = (count_normal + alpha) / (n_normal + 2 * alpha);
end
%% 5. 输出条件概率
fprintf(fid, '\n条件概率(拉普拉斯平滑alpha=1):\n');
fprintf(fid, '%-10s %-20s %-20s\n', '关键词', 'P(特征|垃圾)', 'P(特征|正常)');
fprintf(fid, '%s\n', repmat('-', 1, 55));
for i = 1:n_features
fprintf(fid, '%-10s %-20.3f %-20.3f\n', keywords{i}, P_given_spam(i), P_given_normal(i));
end
%% 6. 测试数据(20封新邮件)
X_test = zeros(20, n_features);
test_true = zeros(20, 1);
% 生成10封垃圾测试邮件
for i = 1:10
for j = 1:15
if rand() < p_spam_garbage
X_test(i, j) = 1;
end
end
for j = 16:20
if rand() < p_spam_normal
X_test(i, j) = 1;
end
end
test_true(i) = 1;
end
% 生成10封正常测试邮件
for i = 11:20
for j = 1:15
if rand() < p_normal_garbage
X_test(i, j) = 1;
end
end
for j = 16:20
if rand() < p_normal_normal
X_test(i, j) = 1;
end
end
test_true(i) = 0;
end
%% 7. 预测并输出结果
fprintf(fid, '\n测试结果:\n');
fprintf(fid, '%s\n', repmat('-', 1, 80));
correct = 0;
for i = 1:20
log_spam = log(P_spam);
log_normal = log(P_normal);
for j = 1:n_features
if X_test(i, j) == 1
log_spam = log_spam + log(P_given_spam(j));
log_normal = log_normal + log(P_given_normal(j));
else
log_spam = log_spam + log(1 - P_given_spam(j));
log_normal = log_normal + log(1 - P_given_normal(j));
end
end
if log_spam > log_normal
pred = 1;
pred_label = '垃圾';
else
pred = 0;
pred_label = '正常';
end
if test_true(i) == 1
true_label = '垃圾';
else
true_label = '正常';
end
if pred == test_true(i)
correct = correct + 1;
result = '正确';
else
result = '错误';
end
n_garbage = sum(X_test(i, 1:15));
n_normal_words = sum(X_test(i, 16:20));
fprintf(fid, '测试%d: 垃圾词=%d, 正常词=%d -> 预测:%s 真实:%s %s\n', ...
i, n_garbage, n_normal_words, pred_label, true_label, result);
end
fprintf(fid, '%s\n', repmat('-', 1, 80));
acc = correct / 20 * 100;
fprintf(fid, '准确率: %.1f%% (%d/20)\n', acc, correct);
%% 8. 计算混淆矩阵
% 重新计算一遍预测结果用于混淆矩阵
predictions = zeros(20, 1);
for i = 1:20
log_spam = log(P_spam);
log_normal = log(P_normal);
for j = 1:n_features
if X_test(i, j) == 1
log_spam = log_spam + log(P_given_spam(j));
log_normal = log_normal + log(P_given_normal(j));
else
log_spam = log_spam + log(1 - P_given_spam(j));
log_normal = log_normal + log(1 - P_given_normal(j));
end
end
if log_spam > log_normal
predictions(i) = 1;
else
predictions(i) = 0;
end
end
TP = 0; FP = 0; TN = 0; FN = 0;
for i = 1:20
if test_true(i) == 1 && predictions(i) == 1
TP = TP + 1;
elseif test_true(i) == 0 && predictions(i) == 1
FP = FP + 1;
elseif test_true(i) == 0 && predictions(i) == 0
TN = TN + 1;
elseif test_true(i) == 1 && predictions(i) == 0
FN = FN + 1;
end
end
fprintf(fid, '\n混淆矩阵:\n');
fprintf(fid, ' 预测垃圾 预测正常\n');
fprintf(fid, '实际垃圾 %2d %2d\n', TP, FN);
fprintf(fid, '实际正常 %2d %2d\n', FP, TN);
if (TP + FP) > 0
precision = TP / (TP + FP) * 100;
fprintf(fid, '\n精确率(垃圾): %.1f%% (%d/%d)\n', precision, TP, TP+FP);
end
if (TP + FN) > 0
recall = TP / (TP + FN) * 100;
fprintf(fid, '召回率(垃圾): %.1f%% (%d/%d)\n', recall, TP, TP+FN);
end
%% 9. 输出模型总结
fprintf(fid, '\n模型总结:\n');
fprintf(fid, '========================================\n');
fprintf(fid, '训练集大小: 50封邮件\n');
fprintf(fid, '特征数量: %d个关键词\n', n_features);
fprintf(fid, '平滑参数alpha: %d\n', alpha);
fprintf(fid, '测试集大小: 20封邮件\n');
fprintf(fid, '测试准确率: %.1f%%\n', acc);
fprintf(fid, '\n最具区分度的垃圾词:\n');
for i = 1:5
ratio = P_given_spam(i) / (P_given_normal(i) + 0.001);
fprintf(fid, ' %s (比值=%.2f)\n', keywords{i}, ratio);
end
fprintf(fid, '\n最具区分度的正常词:\n');
for i = 16:20
ratio = P_given_normal(i) / (P_given_spam(i) + 0.001);
fprintf(fid, ' %s (比值=%.2f)\n', keywords{i}, ratio);
end
%% 关闭文件
fclose(fid);
fprintf('结果已保存到 result.txt\n');
朴素贝叶斯垃圾邮件分类
======================
关键词数量: 20
训练数据: 50封 (垃圾:25, 正常:25)
先验概率: P(垃圾)=0.500, P(正常)=0.500
条件概率(拉普拉斯平滑alpha=1):
关键词 P(特征|垃圾) P(特征|正常)
-------------------------------------------------------
中奖 0.815 0.074
免费 0.741 0.074
优惠 0.704 0.111
红包 0.667 0.148
现金 0.704 0.148
点击 0.667 0.037
链接 0.741 0.111
注册 0.556 0.074
验证 0.704 0.074
账户 0.667 0.074
恭喜 0.704 0.074
幸运 0.778 0.074
获奖 0.593 0.111
领取 0.704 0.148
名额 0.778 0.037
会议 0.111 0.667
报告 0.148 0.593
项目 0.185 0.815
学习 0.111 0.593
请假 0.074 0.556
测试结果:
--------------------------------------------------------------------------------
测试1: 垃圾词=12, 正常词=2 -> 预测:垃圾 真实:垃圾 正确
测试2: 垃圾词=9, 正常词=0 -> 预测:垃圾 真实:垃圾 正确
测试3: 垃圾词=8, 正常词=1 -> 预测:垃圾 真实:垃圾 正确
测试4: 垃圾词=11, 正常词=3 -> 预测:垃圾 真实:垃圾 正确
测试5: 垃圾词=9, 正常词=1 -> 预测:垃圾 真实:垃圾 正确
测试6: 垃圾词=11, 正常词=1 -> 预测:垃圾 真实:垃圾 正确
测试7: 垃圾词=11, 正常词=0 -> 预测:垃圾 真实:垃圾 正确
测试8: 垃圾词=14, 正常词=0 -> 预测:垃圾 真实:垃圾 正确
测试9: 垃圾词=12, 正常词=0 -> 预测:垃圾 真实:垃圾 正确
测试10: 垃圾词=12, 正常词=0 -> 预测:垃圾 真实:垃圾 正确
测试11: 垃圾词=0, 正常词=3 -> 预测:正常 真实:正常 正确
测试12: 垃圾词=3, 正常词=4 -> 预测:正常 真实:正常 正确
测试13: 垃圾词=4, 正常词=3 -> 预测:正常 真实:正常 正确
测试14: 垃圾词=1, 正常词=3 -> 预测:正常 真实:正常 正确
测试15: 垃圾词=3, 正常词=4 -> 预测:正常 真实:正常 正确
测试16: 垃圾词=0, 正常词=3 -> 预测:正常 真实:正常 正确
测试17: 垃圾词=0, 正常词=3 -> 预测:正常 真实:正常 正确
测试18: 垃圾词=0, 正常词=4 -> 预测:正常 真实:正常 正确
测试19: 垃圾词=2, 正常词=3 -> 预测:正常 真实:正常 正确
测试20: 垃圾词=1, 正常词=4 -> 预测:正常 真实:正常 正确
--------------------------------------------------------------------------------
准确率: 100.0% (20/20)
混淆矩阵:
预测垃圾 预测正常
实际垃圾 10 0
实际正常 0 10
精确率(垃圾): 100.0% (10/10)
召回率(垃圾): 100.0% (10/10)
模型总结:
========================================
训练集大小: 50封邮件
特征数量: 20个关键词
平滑参数alpha: 1
测试集大小: 20封邮件
测试准确率: 100.0%
最具区分度的垃圾词:
中奖 (比值=10.85)
免费 (比值=9.87)
优惠 (比值=6.28)
红包 (比值=4.47)
现金 (比值=4.72)
最具区分度的正常词:
会议 (比值=5.95)
报告 (比值=3.97)
项目 (比值=4.38)
学习 (比值=5.29)
请假 (比值=7.40)
在这个简单的测试中该分类器准确率做到了 \(100\%\)
总结
作为机器学习十大算法的朴素贝叶斯算法,它用一条极其“天真”的假设,将复杂的概率计算变得极其简单可行,并且在很多场景下效果出奇地好,因此成为文本分类等领域的经典基准算法。
它并没有被诸如 MLP 这样可作强大分类器的人工智能网络彻底取代,DeepSeek 老师给出的对比表格颇有意思,附上
维度
MLP / Transformer
朴素贝叶斯
建模能力
神级
草履虫级
数据需求
万级以上
10 条就能跑
硬件需求
GPU / NPU
51 单片机
可解释性
黑盒
白盒 (公式写脸上)
适用领域
图像、语义理解、大模型
Baseline、实时风控、极小样本文本分类
看完这张表你就明白:
朴素贝叶斯不是不行,是“聪明得刚刚好” 😄
完结撒花花花花花花花花花花 ✿✿ヽ(°▽°)ノ✿✿
