投诉和夸奖用的词不一样,数一数就够做第一个分类器。朴素贝叶斯假设了一个并不存在的独立。它为什么还是有用?
朴素贝叶斯是一个由计数构成的快速文本分类基线。它假设在给定类别后各个词相互独立,这个假设通常不成立,但模型仍然有用。本课把计数、先验、平滑和对数计分全部写清楚。 你还会看到平滑为什么对未登录词重要,以及长文档为什么得到更负的分数。
按类别统计词元
训练从带标签的文档开始。对每个类别,统计每个词元出现了多少次。这些计数就是分类器使用的原始证据,标签本身不能作为特征混进词表。
docs = [
("good movie", "pos"),
("great film", "pos"),
("bad movie", "neg"),
("boring film", "neg"),
]
counts = {}
for text, label in docs:
counts.setdefault(label, {})
for word in text.split():
counts[label][word] = counts[label].get(word, 0) + 1
print(counts["pos"]["movie"])
print(counts["neg"]["bad"])每个类别各有一张计数表
先验与平滑似然
先验是某个类别在训练文档中的占比。某个词在该类别下的似然等于“该词计数加一”,除以“类别总词数加词表大小”。拉普拉斯平滑可以避免没见过的词把概率变成零。
docs = [
("good movie", "pos"),
("great film", "pos"),
("bad movie", "neg"),
("boring film", "neg"),
]
vocab = sorted({word for text, _ in docs for word in text.split()})
counts = {}
for text, label in docs:
counts.setdefault(label, {})
for word in text.split():
counts[label][word] = counts[label].get(word, 0) + 1
for label in sorted(counts):
total = sum(counts[label].values())
probability = (counts[label].get("great", 0) + 1) / (total + len(vocab))
print(label, round(probability, 3))先加一,再归一化
在对数空间计分
很多小概率连乘可能下溢成零。改为把对数相加,数值更安全,排序也不变。对数概率是负数,数值越大表示该类别越可能。
import math
print(round(math.log(0.5 * 0.2), 3))
print(round(math.log(0.5) + math.log(0.2), 3))排序相同,算术更安全
选出最可能的类别
对新文档,把对数先验与每个词的平滑对数似然相加,选择分数更大的类别。要规定平局规则,并且只在未参与训练的新文档上评估分类器。
import math
docs = [
("good movie", "pos"),
("great film", "pos"),
("bad movie", "neg"),
("boring film", "neg"),
]
vocab = sorted({word for text, _ in docs for word in text.split()})
counts = {}
class_docs = {}
for text, label in docs:
class_docs[label] = class_docs.get(label, 0) + 1
counts.setdefault(label, {})
for word in text.split():
counts[label][word] = counts[label].get(word, 0) + 1
def predict(text):
scores = {}
for label in counts:
total = sum(counts[label].values())
score = math.log(class_docs[label] / len(docs))
for word in text.split():
score += math.log((counts[label].get(word, 0) + 1) / (total + len(vocab)))
scores[label] = score
return max(scores, key=scores.get)
print(predict("great film"))对数分数更大的类别
平滑让未登录词的分数保持有限
不做平滑时,未登录词的似然为零,它的对数没有定义。加 alpha 平滑给词表中的每个词一个小的非零计数。alpha 是一种选择:太小仍然脆弱,太大则冲淡证据。
import math
count = 0
total = 2
vocab_size = 3
for alpha in [0, 1]:
probability = (count + alpha) / (total + alpha * vocab_size)
print(alpha, probability, round(math.log(probability), 3) if probability else "undefined")零计数需要一条策略
越长的文档,总分越负
朴素贝叶斯分数会为每个词元累加一个对数似然。因此较长文档即使平均证据相似,总分也往往更负。在把分数当成概率之前,应比较每词元平均对数似然,或在验证集上校准。
import math
log_prior = math.log(0.5)
log_word = math.log(0.8)
def score(tokens):
return log_prior + len(tokens) * log_word
for tokens in [["good"], ["good", "good"], ["good", "good", "good"]]:
print(len(tokens), round(score(tokens), 2), round((score(tokens) - log_prior) / len(tokens), 2))总分与每词元分数
本课要点
- 朴素贝叶斯按类别汇总词元计数来学习。
- 先验反映每个类别在训练数据中的常见程度。
- 拉普拉斯平滑防止没见过的词把分数归零。
- 概率连乘改为对数相加,可以避免下溢。
- 独立假设是简化选择,不是事实。
- 平滑处理未登录词;文档长度会改变对数分数的尺度。
常见错误
- 把标签本身当成词元特征。
- 只对当前文档出现的词做平滑。
- 在训练文档本身上报告准确率。
- 直接比较长度差异很大的文档的朴素贝叶斯原始分数。
使用四篇训练文档,计算在 neg 类别下出现 'bad' 的平滑概率,保留三位小数。
动手试试
改改变量、先预测输出,再运行代码。第一次点击“运行代码”时会下载 Python 运行时,可能需要几秒钟。
练习本课
12 道题,作答后立即看到解释。