4语音与语言处理(第3版)

朴素贝叶斯

文本分类

朴素贝叶斯情感分析文本分类

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 的所有文档中出现的总次数。

python
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)

只考虑词是否出现(二值特征),不考虑词频。适合短文本分类。

python
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 数值下溢

多个小概率相乘会导致浮点数下溢。解决方案是在对数空间计算:

python
# 不好的做法
prob = prior * p1 * p2 * p3 * ...  # 可能下溢

# 好的做法
log_prob = log(prior) + log(p1) + log(p2) + log(p3) + ...

三、实战案例

3.1 垃圾邮件检测

python
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 情感分析

python
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 语言识别

python
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 对分类性能的影响。

比较朴素贝叶斯和逻辑回归在情感分析任务上的性能差异。分析在训练数据量不同时,两者的表现如何变化。


4/12朴素贝叶
0%