投訴和誇獎用的詞不一樣,數一數就夠做第一個分類器。樸素貝葉斯假設了一個並不存在的獨立。它為什麼還是有用?
樸素貝葉斯是一個由計數構成的快速文字分類基線。它假設在給定類別後各個詞相互獨立,這個假設通常不成立,但模型仍然有用。本課把計數、先驗、平滑和對數計分全部寫清楚。 你還會看到平滑為什麼對未知詞重要,以及長文件為什麼得到更負的分數。
按類別統計詞元
訓練從帶標籤的文件開始。對每個類別,統計每個詞元出現了多少次。這些計數就是分類器使用的原始證據,標籤本身不能作為特徵混進詞表。
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 道題,作答後立即看到解釋。