我对AI的初始印象就是在大学课程里学的贝叶斯分类器,它解决的问题是:
给定观测到的特征 \(x\),类别 \(y\) 的概率是多少?
现代 LLM 回答的问题在形式上非常相似:
给定前文 \(x_{< t}\),下一个 token \(x_t\) 的概率是多少?
含义:
例子:
\[P(\text{spam}|\text{包含“免费”})\]表示:已知一封邮件包含“免费”,它是垃圾邮件的概率。
含义:
关系:
\[P(A,B)=P(A|B)P(B)\]也等于:
\[P(A,B)=P(B|A)P(A)\]这两个式子只是先后顺序不同,描述的是同一件事实。
因为: \(P(x,y) = P(x|y)P(y) = P(y|x)P(x)\)
所以有: \(P(y|x)=\frac{P(x|y)P(y)}{P(x)}\)
各项含义:
| 符号 | 名称 | 含义 |
|---|---|---|
| \(P(y)\) | 先验概率 | 在看到 \(x\) 之前,对类别 \(y\) 的信念 |
| \(P(x \mid y)\) | 似然 | 假设类别是 \(y\) 时,看到特征 \(x\) 的可能性 |
| \(P(x)\) | 证据 | \(x\) 本身出现的总概率,起到归一化作用 |
| \(P(y \mid x)\) | 后验概率 | 看到 \(x\) 之后,更新得到的类别概率 |
贝叶斯定理做的事情是:
这里的信念,可以简单理解为历史频率,贝叶斯定律简单理解就是:根据历史上\(y\)发生时,\(x\)出现的频率,来计算当前\(x\)发生时,\(y\)发生的概率。
举例说明:
假设你在判断一封邮件是不是垃圾邮件。
在还没看内容之前,你知道:
那么你的“先验信念”就是:
P(spam) = 0.3
意思是:
在没有任何额外信息时,你认为这封邮件是垃圾邮件的置信度是 30%。
现在你打开邮件,看到:
Free money click here
这些词在垃圾邮件里很常见,在正常邮件里很少见。
于是你会想:
“看到这些词的情况下,这封邮件是垃圾邮件的可能性好像变大了。”
这个“变大”不是拍脑袋,而是由似然来量化的:
P(这些词|spam)
和:
P(这些词|ham)
前者远大于后者,即在垃圾邮件中出现这些词的概率远大于正常邮件中出现这些词的概率。
贝叶斯定理做的事情是:
先验信念 × 证据强度 → 后验信念
比如更新后变成:
P(spam|这些词) = 0.92
这就表示:
看到邮件内容之后,你现在有 92% 的把握认为它是垃圾邮件。
注意:
分类器通常选择后验概率最大的类别:
\[\hat{y}=\arg\max_y P(y|x)\]含义:
因为 \(P(x)\) 对所有类别相同,所以可以简化为:
\[\hat{y}=\arg\max_y P(x|y)P(y)\]这叫 MAP,最大后验概率决策。
如果 \(x\) 是很多特征的集合:
\[x=(x_1,x_2,\dots,x_n)\]直接建模 \(P(x \mid y)\) 很难。朴素贝叶斯假设:
\[P(x_1,x_2,\dots,x_n|y)=\prod_{i=1}^n P(x_i|y)\]也就是说:
在给定类别 \(y\) 的条件下,各特征相互独立。
这个假设很强,现实中往往不成立,但它让计算变得非常简单。
假设有两个类别:
spam(垃圾邮件)ham(正常邮件)邮件内容:
Free money click here
朴素贝叶斯会计算:
\[P(\text{spam}|\text{Free},\text{money},\text{click},\text{here}) \propto P(\text{spam})\prod_i P(w_i|\text{spam})\]以及:
\[P(\text{ham}|\text{Free},\text{money},\text{click},\text{here}) \propto P(\text{ham})\prod_i P(w_i|\text{ham})\]然后比较两边大小。
很多小概率相乘会导致数值下溢:
\[0.001^{100}=1e-300\]因此实际实现通常取对数:
\[\log P(y)+\sum_i \log P(x_i|y)\]好处:
如果某个词在训练集中从未出现过,则:
\[P(w|y)=0\]整个乘积会直接变成 0。Laplace 平滑的做法是:
\(P(w|y)=\frac{\text{count}(w,y)+1}{\text{count}(y)+|V|}\) 其中:
直观上就是“给每个词至少留一点概率”。
朴素贝叶斯是生成式模型:
逻辑回归和神经网络是判别式模型:
现代 LLM 更接近判别式条件模型:
\[P(x_t|x_{<t})\]但它通常由一个生成式训练目标学得。
以下代码实现了一个玩具版朴素贝叶斯垃圾邮件分类器,包括:
#!/usr/bin/env python
from __future__ import annotations
import argparse
import math
import re
from collections import Counter
from dataclasses import dataclass
TOKEN_PATTERN = re.compile(r"\w+")
@dataclass(frozen=True)
class Sample:
text: str
label: str
class NaiveBayesClassifier:
def __init__(self, smoothing: float = 1.0) -> None:
self.smoothing = smoothing
self.labels: list[str] = []
self.label_counts: Counter[str] = Counter()
self.word_counts: dict[str, Counter[str]] = {}
self.vocabulary: set[str] = set()
self.total_words_per_label: dict[str, int] = {}
@staticmethod
def tokenize(text: str) -> list[str]:
return TOKEN_PATTERN.findall(text.lower())
def fit(self, samples: list[Sample]) -> None:
for sample in samples:
self.label_counts[sample.label] += 1
self.word_counts.setdefault(sample.label, Counter())
tokens = self.tokenize(sample.text)
self.word_counts[sample.label].update(tokens)
self.vocabulary.update(tokens)
self.labels = sorted(self.label_counts)
self.total_words_per_label = {
label: sum(self.word_counts[label].values()) for label in self.labels
}
def log_prior(self, label: str) -> float:
total = sum(self.label_counts.values())
return math.log(self.label_counts[label] / total)
def log_likelihood(self, token: str, label: str) -> float:
count = self.word_counts[label][token]
total = self.total_words_per_label[label]
smoothed = (count + self.smoothing) / (total + self.smoothing * len(self.vocabulary))
return math.log(smoothed)
def predict_log_scores(self, text: str) -> dict[str, float]:
tokens = self.tokenize(text)
scores: dict[str, float] = {}
for label in self.labels:
score = self.log_prior(label)
for token in tokens:
score += self.log_likelihood(token, label)
scores[label] = score
return scores
def predict(self, text: str) -> str:
scores = self.predict_log_scores(text)
return max(scores.items(), key=lambda item: item[1])[0]
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--smoothing", type=float, default=1.0)
args = parser.parse_args()
training_data = [
Sample("Free money click here", "spam"),
Sample("Win a free prize now", "spam"),
Sample("Cheap pills online", "spam"),
Sample("Meeting tomorrow at ten", "ham"),
Sample("Project review is attached", "ham"),
Sample("Lunch after the standup", "ham"),
]
test_data = [
"Free prize click now",
"Review the project tomorrow",
"Cheap online offer",
"Lunch meeting after standup",
"totally unseen words",
]
classifier = NaiveBayesClassifier(smoothing=args.smoothing)
classifier.fit(training_data)
print(f"labels: {classifier.labels}")
print(f"vocabulary size: {len(classifier.vocabulary)}")
print(f"log priors: { {label: classifier.log_prior(label) for label in classifier.labels} }")
print("\npredictions:")
for text in test_data:
scores = classifier.predict_log_scores(text)
prediction = classifier.predict(text)
formatted_scores = ", ".join(f"{label}={score:.3f}" for label, score in scores.items())
print(f" {text!r} -> {prediction} ({formatted_scores})")
if __name__ == "__main__":
main()
| 符号 / 名词 | 含义 |
|---|---|
| \(P(A)\) | 事件 \(A\) 发生的概率,取值范围 \([0,1]\) |
| \(P(A \mid B)\) | 条件概率,已知 \(B\) 发生时 \(A\) 发生的概率 |
| \(P(A,B)\) | 联合概率,\(A\) 和 \(B\) 同时发生的概率 |
| \(P(x \mid y)\) | 似然,类别为 \(y\) 时观察到 \(x\) 的概率 |
| \(P(y \mid x)\) | 后验概率,观察到 \(x\) 后类别为 \(y\) 的概率 |
| \(P(y)\) | 先验概率,未观察 \(x\) 前对 \(y\) 的信念 |
| \(P(x)\) | 证据,\(x\) 出现的总概率,用于归一化 |
| \(\arg\max\) | 使函数取最大值的输入 |
| \(\prod\) | 连乘符号 |
| \(\sum\) | 求和符号 |
| \(\log\) | 对数函数,把乘法变成加法 |
| MAP | Maximum A Posteriori,最大后验概率决策 |
| 朴素假设 | 给定类别时特征条件独立 |
| Laplace 平滑 | 给未出现事件分配小概率,避免零概率 |
| 生成式模型 | 显式建模 \(P(x,y)\) 或 \(P(x \mid y)\) |
| 判别式模型 | 直接建模 \(P(y \mid x)\) |