朴素贝叶斯垃圾邮件分类器
用最基础的条件概率公式实现一个简单的中文垃圾邮件分类器,不调 sklearn。
学完本案例你将理解:贝叶斯定理 + 「各词独立」假设 = 一个能用的文本分类器。
生活引入
邮箱的垃圾邮件过滤
你的邮箱每天收到 100 封邮件。怎么让程序自动判断一封新邮件是不是垃圾邮件?程序的做法:统计历史邮件中,垃圾邮件里出现「免费」「中奖」这些词的概率,以及正常邮件里出现「会议」「项目」的概率。
新邮件来了,算一下:在垃圾邮件的假设下这些词同时出现的概率 vs 在正常邮件的假设下。谁大就判给谁。
直观理解
朴素贝叶斯的关键简化——「朴素假设」:假设各个词出现与否是相互独立的。虽然这个假设在现实中不成立(「免费」和「奖品」经常一起出现),但实用效果却出奇地好。
| 技巧 | 做法 | 解决的问题 |
|---|---|---|
| 对数概率 | 用 log 将连乘转为连加 | 很多小数连乘会变成 0(数值下溢) |
| 拉普拉斯平滑 | 每个词计数 +1 而不是 +0 | 训练集里没见过的词导致概率为 0 |
Python 动手实践
实例
from collections import defaultdict
import math
# 训练数据(8 条标注好的中文短信)
train_data = [
("赢取 免费 奖品 立即 点击", "spam"),
("恭喜 中奖 领取 现金 优惠", "spam"),
("限时 优惠 点击 链接 免费", "spam"),
("贷款 低息 立即 申请 现金", "spam"),
("会议 安排 明天 上午 讨论", "ham"),
("项目 进度 汇报 会议 记录", "ham"),
("周末 聚餐 安排 大家 参加", "ham"),
("报告 数据 分析 项目 完成", "ham"),
]
# 统计每个类别的词频
class_word_counts = defaultdict(lambda: defaultdict(int))
class_doc_counts = defaultdict(int)
vocab = set()
for text, label in train_data:
class_doc_counts[label] += 1
for word in text.split():
class_word_counts[label][word] += 1
vocab.add(word)
vocab_size = len(vocab)
total_docs = len(train_data)
# 朴素贝叶斯分类(对数概率 + 拉普拉斯平滑)
def predict(text, alpha=1.0):
words = text.split()
scores = {}
for label in class_doc_counts:
log_prob = math.log(class_doc_counts[label] / total_docs)
total_words = sum(class_word_counts[label].values())
for word in words:
word_count = class_word_counts[label][word]
word_prob = (word_count + alpha) / (total_words + alpha * vocab_size)
log_prob += math.log(word_prob)
scores[label] = log_prob
return max(scores, key=scores.get), scores
# 测试
test_emails = [
"免费 领取 现金 奖品",
"明天 会议 项目 汇报",
"限时 点击 优惠 链接",
"周末 大家 一起 讨论 报告",
]
print("RUNOOB 朴素贝叶斯分类结果:\n")
for email in test_emails:
label, scores = predict(email)
print(f" {email!r:28s} -> 【{label}】")
import math
# 训练数据(8 条标注好的中文短信)
train_data = [
("赢取 免费 奖品 立即 点击", "spam"),
("恭喜 中奖 领取 现金 优惠", "spam"),
("限时 优惠 点击 链接 免费", "spam"),
("贷款 低息 立即 申请 现金", "spam"),
("会议 安排 明天 上午 讨论", "ham"),
("项目 进度 汇报 会议 记录", "ham"),
("周末 聚餐 安排 大家 参加", "ham"),
("报告 数据 分析 项目 完成", "ham"),
]
# 统计每个类别的词频
class_word_counts = defaultdict(lambda: defaultdict(int))
class_doc_counts = defaultdict(int)
vocab = set()
for text, label in train_data:
class_doc_counts[label] += 1
for word in text.split():
class_word_counts[label][word] += 1
vocab.add(word)
vocab_size = len(vocab)
total_docs = len(train_data)
# 朴素贝叶斯分类(对数概率 + 拉普拉斯平滑)
def predict(text, alpha=1.0):
words = text.split()
scores = {}
for label in class_doc_counts:
log_prob = math.log(class_doc_counts[label] / total_docs)
total_words = sum(class_word_counts[label].values())
for word in words:
word_count = class_word_counts[label][word]
word_prob = (word_count + alpha) / (total_words + alpha * vocab_size)
log_prob += math.log(word_prob)
scores[label] = log_prob
return max(scores, key=scores.get), scores
# 测试
test_emails = [
"免费 领取 现金 奖品",
"明天 会议 项目 汇报",
"限时 点击 优惠 链接",
"周末 大家 一起 讨论 报告",
]
print("RUNOOB 朴素贝叶斯分类结果:\n")
for email in test_emails:
label, scores = predict(email)
print(f" {email!r:28s} -> 【{label}】")
RUNOOB 朴素贝叶斯分类结果: '免费 领取 现金 奖品' -> 【spam】 '明天 会议 项目 汇报' -> 【ham】 '限时 点击 优惠 链接' -> 【spam】 '周末 大家 一起 讨论 报告' -> 【ham】
AI 中的应用场景
| 场景 | 说明 |
|---|---|
| 文本分类 | 垃圾邮件过滤、新闻分类、情感分析——朴素贝叶斯是经典 baseline |
| 文档主题建模 | LDA(Latent Dirichlet Allocation)的数学基础 |
| 异常检测 | 计算新样本属于「正常分布」的概率,太低则判为异常 |
