第4章:朴素贝叶斯(Naive Bayes)
一、导读
1.1 本章学习目标
朴素贝叶斯是 NLP 中最简单也最经典的分类算法之一。尽管它的"朴素"假设——特征之间条件独立——在现实中几乎从不成立,但朴素贝叶斯在文本分类任务中表现出色,是理解概率分类方法的重要起点。
本章的核心学习目标包括:
- 理解贝叶斯定理:先验概率、似然、后验概率的含义和关系
- 掌握朴素贝叶斯分类器:如何从贝叶斯定理推导出分类决策规则
- 学会训练朴素贝叶斯模型:如何从训练数据中估计参数
- 理解"朴素"假设的影响:条件独立假设为什么在实践中有效?
- 掌握平滑技术:如何处理训练集中未出现的特征
- 了解朴素贝叶斯在 NLP 中的应用:情感分析、垃圾邮件检测、文档分类
1.2 为什么本章如此重要
朴素贝叶斯是概率分类方法的入门模型。它虽然简单,但蕴含了机器学习的核心思想:用概率模型建模数据,用贝叶斯定理进行推理。理解朴素贝叶斯,是学习更复杂的概率模型(如隐马尔可夫模型、条件随机场、主题模型)的基础。
在深度学习时代之前,朴素贝叶斯是文本分类的首选方法之一。即使在今天,它在以下场景中仍然有重要价值:
- 基线模型:作为分类任务的简单基线,帮助评估更复杂模型的提升
- 低资源场景:训练数据有限时,朴素贝叶斯的简单性反而成为优势
- 实时分类:推理速度极快,适合对延迟要求高的场景
- 可解释性:模型参数有明确的概率含义,易于理解和调试
1.3 核心问题引导
贝叶斯定理如何将先验知识更新为后验知识?
朴素贝叶斯的"朴素"假设是什么?为什么这个假设在实践中不成立但模型仍然有效?
如何处理零概率问题(训练集中未出现的词)?
多项式朴素贝叶斯和伯努利朴素贝叶斯有什么区别?分别适用于什么场景?
朴素贝叶斯与逻辑回归有什么关系和区别?
二、核心概念详解
4.1 贝叶斯定理
贝叶斯定理是概率论的核心定理之一,描述了如何在观察到新证据后更新信念:
$$P(H|D) = \frac{P(D|H) \cdot P(H)}{P(D)}$$
其中:
- $P(H)$ 是先验概率:在观察数据之前对假设的信念
- $P(D|H)$ 是似然:在假设成立的条件下观察到数据的概率
- $P(H|D)$ 是后验概率:观察到数据之后对假设的信念
- $P(D)$ 是证据(边际似然):数据的总概率
在分类任务中:
- $H$ 是类别(如"正面"或"负面")
- $D$ 是观察到的文档(词的序列)
$$P(c|d) = \frac{P(d|c) \cdot P(c)}{P(d)}$$
由于 $P(d)$ 对所有类别相同,分类时只需比较分子:
$$\hat{c} = \arg\max_c P(d|c) \cdot P(c)$$
4.2 朴素贝叶斯分类器
4.2.1 条件独立假设
直接计算 $P(d|c)$ 需要估计指数级数量的参数。朴素贝叶斯引入条件独立假设:给定类别,文档中的每个词独立于其他词。
$$P(d|c) = P(w_1, w_2, \ldots, w_n | c) \approx \prod_{i=1}^{n} P(w_i | c)$$
完整的分类公式:
$$\hat{c} = \arg\max_c P(c) \prod_{i=1}^{n} P(w_i | c)$$
取对数避免下溢:
$$\hat{c} = \arg\max_c \log P(c) + \sum_{i=1}^{n} \log P(w_i | c)$$
4.2.2 参数估计
使用最大似然估计:
先验概率:
$$P(c) = \frac{N_c}{N}$$
其中 $N_c$ 是类别 c 的文档数,$N$ 是总文档数。
条件概率(多项式模型):
$$P(w_i|c) = \frac{\text{count}(w_i, c)}{\sum_{w} \text{count}(w, c)}$$
其中 $\text{count}(w_i, c)$ 是词 $w_i$ 在类别 c 的所有文档中出现的总次数。
import math
from collections import defaultdict, Counter
class NaiveBayesClassifier:
def __init__(self, alpha=1.0):
self.alpha = alpha # 拉普拉斯平滑参数
self.class_priors = {}
self.conditional_probs = {}
self.vocab = set()
self.classes = set()
def train(self, documents, labels):
# 统计类别分布
class_counts = Counter(labels)
total = len(labels)
self.classes = set(labels)
# 计算先验概率
for c in self.classes:
self.class_priors[c] = class_counts[c] / total
# 统计每个类别中每个词的出现次数
class_word_counts = defaultdict(Counter)
class_total_words = Counter()
for doc, label in zip(documents, labels):
for word in doc:
class_word_counts[label][word] += 1
class_total_words[label] += 1
self.vocab.add(word)
# 计算条件概率(带平滑)
for c in self.classes:
self.conditional_probs[c] = {}
total_in_class = class_total_words[c]
vocab_size = len(self.vocab)
for word in self.vocab:
count = class_word_counts[c][word]
self.conditional_probs[c][word] = (
(count + self.alpha) / (total_in_class + self.alpha * vocab_size)
)
def predict(self, document):
scores = {}
for c in self.classes:
score = math.log(self.class_priors[c])
for word in document:
if word in self.conditional_probs[c]:
score += math.log(self.conditional_probs[c][word])
else:
score += math.log(self.alpha / (sum(1 for _ in self.vocab) + self.alpha * len(self.vocab)))
scores[c] = score
return max(scores, key=scores.get)
def predict_proba(self, document):
log_scores = {}
for c in self.classes:
score = math.log(self.class_priors[c])
for word in document:
if word in self.conditional_probs[c]:
score += math.log(self.conditional_probs[c][word])
log_scores[c] = score
# 转换为概率(log-sum-exp 技巧)
max_score = max(log_scores.values())
exp_scores = {c: math.exp(s - max_score) for c, s in log_scores.items()}
total = sum(exp_scores.values())
return {c: s / total for c, s in exp_scores.items()}
# 训练示例
documents = [
["好", "电影", "精彩", "推荐"],
["非常", "好看", "剧情", "好"],
["垃圾", "烂片", "浪费", "时间"],
["难看", "失望", "不好", "差"],
["精彩", "好看", "推荐", "好"],
["烂", "浪费", "时间", "难看"],
]
labels = ["正面", "正面", "负面", "负面", "正面", "负面"]
classifier = NaiveBayesClassifier(alpha=1.0)
classifier.train(documents, labels)
# 预测
test_doc = ["这个", "电影", "很", "精彩"]
print(f"预测类别: {classifier.predict(test_doc)}")
print(f"类别概率: {classifier.predict_proba(test_doc)}")4.3 朴素贝叶斯的变体
4.3.1 多项式朴素贝叶斯(Multinomial NB)
考虑词频(词在文档中出现的次数),适合文本分类。
4.3.2 伯努利朴素贝叶斯(Bernoulli NB)
只考虑词是否出现(二值特征),不考虑词频。适合短文本分类。
class BernoulliNB:
def __init__(self, alpha=1.0):
self.alpha = alpha
self.class_priors = {}
self.word_probs = {} # P(w=1|c)
self.vocab = set()
def train(self, documents, labels):
class_counts = Counter(labels)
total = len(labels)
classes = set(labels)
for c in classes:
self.class_priors[c] = class_counts[c] / total
# 收集词汇表
for doc in documents:
self.vocab.update(doc)
# 计算 P(w=1|c)
for c in classes:
self.word_probs[c] = {}
doc_count = class_counts[c]
for word in self.vocab:
count = sum(1 for doc, label in zip(documents, labels)
if label == c and word in set(doc))
self.word_probs[c][word] = (count + self.alpha) / (doc_count + 2 * self.alpha)
def predict(self, document):
doc_set = set(document)
scores = {}
for c in self.class_priors:
score = math.log(self.class_priors[c])
for word in self.vocab:
if word in doc_set:
score += math.log(self.word_probs[c][word])
else:
score += math.log(1 - self.word_probs[c][word])
scores[c] = score
return max(scores, key=scores.get)4.3.3 高斯朴素贝叶斯(Gaussian NB)
假设特征服从高斯分布,适合连续特征。
4.4 常见误区与注意事项
4.4.1 条件独立假设的影响
"朴素"假设在实践中几乎从不成立。例如,"纽约"和"时代广场"在文本中高度相关。然而,朴素贝叶斯仍然有效,原因是:
- 分类只需要正确的类别排序,不需要精确的概率估计
- 相关特征的"投票"效果会叠加,反而可能增强分类能力
- 在特征空间的高维中,独立假设的偏差影响较小
4.4.2 零概率问题
如果某个词在训练集的某个类别中没有出现过,其条件概率为 0,导致整个乘积为 0。解决方案是平滑,最常用的是拉普拉斯平滑(加一平滑)。
4.4.3 数值下溢
多个小概率相乘会导致浮点数下溢。解决方案是在对数空间计算:
# 不好的做法
prob = prior * p1 * p2 * p3 * ... # 可能下溢
# 好的做法
log_prob = log(prior) + log(p1) + log(p2) + log(p3) + ...三、实战案例
3.1 垃圾邮件检测
from sklearn.naive_bayes import MultinomialNB, BernoulliNB
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
# 模拟数据
emails = [
"免费赢取百万大奖点击链接立即参与",
"会议通知明天下午三点开部门例会",
"恭喜您中奖了请汇款手续费领取奖金",
"项目报告请在本周五之前提交",
"限时优惠打折促销买一送一",
"请查收附件中的合同文件",
"独家秘方包治百病无效退款",
"周末一起吃饭怎么样",
]
labels = ["spam", "ham", "spam", "ham", "spam", "ham", "spam", "ham"]
# 特征提取
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(emails)
y = labels
# 训练分类器
clf = MultinomialNB(alpha=1.0)
clf.fit(X, y)
# 测试
test_emails = [
"免费赠送礼品点击领取",
"明天的会议改到下午四点",
]
X_test = vectorizer.transform(test_emails)
predictions = clf.predict(X_test)
for email, pred in zip(test_emails, predictions):
print(f"'{email}' → {pred}")3.2 情感分析
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score
# 使用 20 Newsgroups 数据集
categories = ["rec.sport.baseball", "sci.space", "comp.graphics"]
train_data = fetch_20newsgroups(subset="train", categories=categories, random_state=42)
test_data = fetch_20newsgroups(subset="test", categories=categories, random_state=42)
# TF-IDF 特征
vectorizer = TfidfVectorizer(max_features=5000, stop_words="english")
X_train = vectorizer.fit_transform(train_data.data)
X_test = vectorizer.transform(test_data.data)
# 训练和评估
clf = MultinomialNB()
clf.fit(X_train, train_data.target)
y_pred = clf.predict(X_test)
accuracy = accuracy_score(test_data.target, y_pred)
print(f"Accuracy: {accuracy:.4f}")
# 分析最重要的特征
feature_names = vectorizer.get_feature_names_out()
for i, category in enumerate(train_data.target_names):
top_indices = clf.feature_log_prob_[i].argsort()[-10:][::-1]
top_features = [feature_names[idx] for idx in top_indices]
print(f"\n{category} 最重要的特征:")
print(", ".join(top_features))3.3 语言识别
class LanguageDetector:
def __init__(self, n=3):
self.n = n
self.clf = MultinomialNB()
self.vectorizer = None
def _extract_char_ngrams(self, text):
text = text.lower()
return " ".join([text[i:i+self.n] for i in range(len(text) - self.n + 1)])
def train(self, texts, languages):
ngram_texts = [self._extract_char_ngrams(t) for t in texts]
self.vectorizer = CountVectorizer(analyzer="word")
X = self.vectorizer.fit_transform(ngram_texts)
self.clf.fit(X, languages)
def predict(self, text):
ngram_text = self._extract_char_ngrams(text)
X = self.vectorizer.transform([ngram_text])
return self.clf.predict(X)[0]
# 示例
texts = [
"Hello how are you doing today",
"The weather is nice and sunny",
"Bonjour comment allez-vous aujourd'hui",
"Le temps est beau et ensoleillé",
"Hallo wie geht es Ihnen heute",
"Das Wetter ist schön und sonnig",
]
languages = ["en", "en", "fr", "fr", "de", "de"]
detector = LanguageDetector(n=3)
detector.train(texts, languages)
test_texts = ["Good morning everyone", "Je suis très content", "Ich liebe Programmierung"]
for text in test_texts:
print(f"'{text}' → {detector.predict(text)}")四、本章小结
本章系统讲解了朴素贝叶斯分类器:
贝叶斯定理:将先验概率通过似然更新为后验概率,是概率推理的基础。
朴素贝叶斯模型:通过条件独立假设简化参数估计,使模型在高维空间中仍然可用。
参数估计:使用最大似然估计计算先验概率和条件概率,配合拉普拉斯平滑处理零概率问题。
模型变体:多项式 NB 考虑词频,伯努利 NB 只考虑词是否出现,高斯 NB 处理连续特征。
应用场景:垃圾邮件检测、情感分析、文档分类、语言识别等。
优缺点:训练快、推理快、可解释性强;但条件独立假设限制了表达能力。
朴素贝叶斯是理解概率分类方法的起点。从下一章开始,我们将学习更强大的分类方法——逻辑回归,它放弃了条件独立假设,通过特征权重直接建模决策边界。
五、思考与练习
思考题
在什么情况下朴素贝叶斯的条件独立假设近似成立?在什么情况下完全不成立?
比较朴素贝叶斯和逻辑回归的优缺点。在什么场景下你会选择朴素贝叶斯?
为什么在文本分类中,多项式朴素贝叶斯通常优于伯努利朴素贝叶斯?
拉普拉斯平滑中的参数 α 如何影响模型?α 太大或太小分别有什么问题?
编程练习
在 20 Newsgroups 数据集上实现朴素贝叶斯文本分类器,比较不同平滑参数对性能的影响。
实现一个带有特征选择的朴素贝叶斯分类器:使用互信息(Mutual Information)选择最有信息量的 K 个特征,分析 K 对分类性能的影响。
比较朴素贝叶斯和逻辑回归在情感分析任务上的性能差异。分析在训练数据量不同时,两者的表现如何变化。