前置知识: 机器学习

朴素贝叶斯

4 minBeginner

朴素贝叶斯用条件概率和"朴素"独立性假设实现快速分类

朴素贝叶斯

朴素贝叶斯用条件概率和”朴素”独立性假设实现快速分

类型: 构建 语言: Python 前置条件: Phase 2 第1课,Phase 1 (概率) 时间: ~60 分钟

学习目标

  • 从贝叶斯定理推导朴素贝叶斯分
  • 从零实现高斯朴素贝叶斯和多项式朴素贝叶斯
  • 解释”朴素”独立性假设为何在实践中有效
  • 在文本分任务上应用朴素贝叶斯

问题

你想分邮件为垃圾邮件或非垃圾邮件。你观察到垃圾邮件中”免费”出现频率很高。贝叶斯定理让你从特征概率推断别概率。朴素贝叶斯假设所有特征条件独立——这几乎从不成立——但出奇地有效。

概念

贝叶斯定理

P(类别|特征) = P(特征|类别) * P(类别) / P(特征)
  • P(别|特征):后验概率——看到特征后别是多少
  • P(特征|别):似然——该别下特征出现的概率
  • P(别):先验概率——别的基础概率
  • P(特征):证据——归一化常数

朴素独立性假设

对于多个特征,计算P(x1, x2, …, xn | 别)需要知道特征的联合分布,这需要大量数据。

朴素假设:给定别,特征之间相互独立:

P(x1, x2, ..., xn | 类别) = P(x1|类别) * P(x2|类别) * ... * P(xn|类别)

这几乎从不成立(词”免费”和”中奖”显然相关)。但分只需要正确排序后验概率,不需要精确值。所以即使概率估计偏差很大,分结果仍然正确。

三种朴素贝叶斯

高斯朴素贝叶斯:假设连续特征服从高斯分布。P(xi|别)用均值和方差计算。

多项式朴素贝叶斯:假设特征是计数(词频)。P(xi|别)用多项式分布。适合文本分

伯努利朴素贝叶斯:假设特征是二值的(词是否出现)。适合短文本。

拉普拉斯平滑

如果测试时遇到训练时没见过的特征值,P(xi|别)=0,整个乘积变为0。拉普拉斯平滑解决:

P(xi|类别) = (count(xi, 类别) + alpha) / (count(类别) + alpha * n_features)

alpha=1是最常用的拉普拉斯平滑。

动手构建

import random
import math

class GaussianNaiveBayes:
    def __init__(self):
        self.classes = None
        self.means = {}
        self.vars = {}
        self.priors = {}

    def fit(self, X, y):
        from collections import Counter
        self.classes = list(set(y))
        counts = Counter(y)
        n = len(y)
        for c in self.classes:
            self.priors[c] = counts[c] / n
            X_c = [X[i] for i in range(len(y)) if y[i] == c]
            n_features = len(X[0])
            self.means[c] = [sum(X_c[i][j] for i in range(len(X_c))) / len(X_c) for j in range(n_features)]
            self.vars[c] = [sum((X_c[i][j] - self.means[c][j]) ** 2 for i in range(len(X_c))) / len(X_c)
                           for j in range(n_features)]
        return self

    def _gaussian_prob(self, x, mean, var):
        if var < 1e-10:
            return 1.0 if abs(x - mean) < 1e-10 else 1e-10
        return (1.0 / math.sqrt(2 * math.pi * var)) * math.exp(-(x - mean) ** 2 / (2 * var))

    def predict_proba(self, X):
        results = []
        for x in X:
            posteriors = {}
            for c in self.classes:
                log_prob = math.log(self.priors[c])
                for j in range(len(x)):
                    prob = self._gaussian_prob(x[j], self.means[c][j], self.vars[c][j])
                    log_prob += math.log(max(prob, 1e-300))
                posteriors[c] = log_prob
            total = sum(math.exp(p - max(posteriors.values())) for p in posteriors.values())
            probs = {c: math.exp(posteriors[c] - max(posteriors.values())) / total for c in self.classes}
            results.append(probs)
        return results

    def predict(self, X):
        probas = self.predict_proba(X)
        return [max(p, key=p.get) for p in probas]

    def accuracy(self, X, y):
        preds = self.predict(X)
        return sum(p == t for p, t in zip(preds, y)) / len(y)


class MultinomialNaiveBayes:
    def __init__(self, alpha=1.0):
        self.alpha = alpha
        self.classes = None
        self.feature_counts = {}
        self.class_counts = {}
        self.priors = {}

    def fit(self, X, y):
        from collections import Counter
        self.classes = list(set(y))
        counts = Counter(y)
        n = len(y)
        n_features = len(X[0])

        for c in self.classes:
            self.priors[c] = counts[c] / n
            self.class_counts[c] = counts[c]
            self.feature_counts[c] = [0] * n_features
            for i in range(len(y)):
                if y[i] == c:
                    for j in range(n_features):
                        self.feature_counts[c][j] += X[i][j]
        return self

    def predict(self, X):
        results = []
        for x in X:
            best_class = None
            best_log_prob = float('-inf')
            for c in self.classes:
                log_prob = math.log(self.priors[c])
                total = sum(self.feature_counts[c]) + self.alpha * len(x)
                for j in range(len(x)):
                    prob = (self.feature_counts[c][j] + self.alpha) / total
                    log_prob += x[j] * math.log(max(prob, 1e-300))
                if log_prob > best_log_prob:
                    best_log_prob = log_prob
                    best_class = c
            results.append(best_class)
        return results

    def accuracy(self, X, y):
        preds = self.predict(X)
        return sum(p == t for p, t in zip(preds, y)) / len(y)


random.seed(42)
N = 300
X = []
y = []
for _ in range(N // 3):
    X.append([random.gauss(0, 1), random.gauss(0, 1)])
    y.append(0)
for _ in range(N // 3):
    X.append([random.gauss(3, 1), random.gauss(0, 1)])
    y.append(1)
for _ in range(N // 3):
    X.append([random.gauss(1.5, 1), random.gauss(3, 1)])
    y.append(2)

split = int(0.8 * N)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]

print("=== Gaussian Naive Bayes ===")
gnb = GaussianNaiveBayes()
gnb.fit(X_train, y_train)
print(f"Train accuracy: {gnb.accuracy(X_train, y_train):.4f}")
print(f"Test accuracy:  {gnb.accuracy(X_test, y_test):.4f}")

print("\n=== Multinomial Naive Bayes (Text Classification) ===")
docs = [
    ([3, 0, 1, 0, 2], 1), ([2, 1, 0, 0, 1], 1), ([0, 0, 0, 3, 2], 0),
    ([1, 0, 2, 1, 0], 1), ([0, 2, 0, 4, 1], 0), ([0, 1, 0, 2, 3], 0),
    ([4, 0, 1, 0, 0], 1), ([0, 3, 0, 2, 1], 0), ([2, 0, 3, 0, 1], 1),
    ([0, 2, 0, 3, 2], 0),
]
X_text = [d[0] for d in docs]
y_text = [d[1] for d in docs]

mnb = MultinomialNaiveBayes(alpha=1.0)
mnb.fit(X_text, y_text)
print(f"Training accuracy: {mnb.accuracy(X_text, y_text):.4f}")
test_doc = [[1, 0, 2, 0, 0]]
print(f"Test doc {test_doc[0]} -> predicted: {mnb.predict(test_doc)[0]} (likely class 1=spam)")

实际使用

from sklearn.naive_bayes import GaussianNB, MultinomialNB
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

iris = load_iris()
X_tr, X_te, y_tr, y_te = train_test_split(iris.data, iris.target, test_size=0.3, random_state=42)

gnb = GaussianNB()
gnb.fit(X_tr, y_tr)
print(f"Gaussian NB accuracy: {accuracy_score(y_te, gnb.predict(X_te)):.4f}")

练习

  1. 在垃圾邮件分数据集上比较多项式朴素贝叶斯和伯努利朴素贝叶斯。哪种更适合?
  2. 实现伯努利朴素贝叶斯(特征为二值)。在二值化后的数据集上测试。
  3. 朴素贝叶斯的独立性假设何时会严重失败?构造一个特征高度相关的数据集,展示朴素贝叶斯vs逻辑回归的差异。

关键术语

| 术语 | 人们怎么说 | 实际含义 | | ------------ | -------------- | ------------------------------------ | --------------- | ----- | | 贝叶斯定理 | “反转概率” | 从P(特征 | 别)计算P(别 | 特征) | | 朴素假设 | “假装特征独立” | 给定别时假设特征相互独立,简化计算 | | 拉普拉斯平滑 | “加一平滑” | 给所有计数加alpha避免零概率 | | 先验概率 | “基础概率” | 不看特征时别的概率 | | 后验概率 | “更新后概率” | 看到特征后别的概率 |