9.1 貝氏文字分類#
概念說明#
文字分類:給定一段文字,判斷它屬於哪個類別——垃圾郵件過濾、情感分析(正面/負面)、主題分類、語言辨識。單純貝氏分類器(Naive Bayes)是這類任務最古老的基準(垃圾郵件過濾自 1998 年起大量部署),至今仍是「先跑一版 baseline」的首選。
貝氏定理把問題翻轉:要算 P(類別|文字)(後驗,難直接估),改算 P(文字|類別)×P(類別)(似然×先驗,可從訓練資料統計):
P(c|d) ∝ P(c) · P(d|c)
「單純(naive)」指的是一個大膽假設:詞與詞在給定類別下條件獨立——P(d|c) = Π P(wᵢ|c)。語法上這顯然不成立(「不」與「好」明顯相依),但實務上分類只需要「誰的後驗較大」,不需要精確概率,所以假設雖錯、方向常對。這是「錯誤假設 + 正確決策」的經典範例。
多項式貝氏是文字任務的標準變體:把文件視為詞的袋(bag of words),P(w|c) = count(w, c)/Σcount(·, c)。兩個工程要點:
- 平滑(Laplace smoothing):測試文件出現訓練時某類別沒見過的詞,P(w|c)=0 會把整個乘積打成 0。加 α(常取 1):P(w|c) = (count+α)/(總數+α|V|)。
- 對數域計算:幾百個小於 1 的機率連乘會下溢(underflow)為 0;改連加 log,log P(c|d) = log P(c) + Σ log P(wᵢ|c),數值穩定又免乘法。
分詞(第7章)直接決定特徵品質;中文常用「字級 n-gram」當特徵——不依賴分詞器卻常出奇地好。模型可解釋性也是賣點:log P(w|spam) − log P(w|ham) 最大的詞就是「最有鑑別力的詞」,天然產出可讀的證據。
與其他方法的定位:單純貝氏是生成式(學每個類別如何產生文字);邏輯迴歸/SVM 是判別式(直接學類別邊界),效果通常更好但需要更多調校;預訓練語言模型(9.3 節)效果最好但最貴。快、省、可解釋,讓貝氏至今活躍於輕量場景。
演算法步驟#
- 訓練:對每個類別 c,統計先驗 P(c) = 該類文件數/總數;統計詞頻 count(w, c)。
- 平滑後估發射機率:P(w|c) = (count(w,c)+1)/(Σ_w' count(w',c)+|V|)。
- 預測:對新文件分詞,對每類計算 log P(c) + Σ log P(w|c)(未見詞用平滑機率)。
- 取分數最大的類別;可輸出各類後驗(softmax 歸一 log 分數)供信心評估。
Python 程式碼範例#
import math
from collections import Counter, defaultdict
train = [
("演算法 複雜度 資料結構 動態規劃", "技術"),
("演算法 與 資料結構 是 電腦 科學 核心", "技術"),
("深度學習 模型 訓練 需要 大量 資料", "技術"),
("今天 天氣 很好 適合 出遊 野餐", "生活"),
("週末 去 咖啡廳 看 書 很 放鬆", "生活"),
("旅行 計畫 風景 美食 心情 愉快", "生活"),
]
class NaiveBayes:
def __init__(self, alpha=1.0):
self.alpha = alpha
self.cls_count = Counter() # P(c)
self.word_count = defaultdict(Counter) # count(w, c)
self.vocab = set()
def fit(self, pairs):
for text, c in pairs:
self.cls_count[c] += 1
for w in text.split():
self.word_count[c][w] += 1
self.vocab.add(w)
def predict(self, text):
V = len(self.vocab)
N = sum(self.cls_count.values())
best, scores = None, {}
for c, nc in self.cls_count.items():
logp = math.log(nc / N) # 先驗
denom = sum(self.word_count[c].values()) + self.alpha * V
for w in text.split():
logp += math.log((self.word_count[c][w] + self.alpha) / denom)
scores[c] = logp
if best is None or logp > scores[best]:
best = c
# softmax 化的後驗(供信心參考)
mx = max(scores.values())
exp = {c: math.exp(s - mx) for c, s in scores.items()}
tot = sum(exp.values())
return best, {c: round(e / tot, 3) for c, e in exp.items()}
nb = NaiveBayes()
nb.fit(train)
for t in ["演算法 動態規劃 很 有趣", "週末 美食 之旅", "訓練 模型 需要 演算法"]:
label, conf = nb.predict(t)
print(f"{t} -> {label} {conf}")
# 鑑別力診斷:哪個詞最「偏向技術」?
c1, c2 = "技術", "生活"
V = len(nb.vocab)
def logodds(w):
def logp(c):
denom = sum(nb.word_count[c].values()) + V
return math.log((nb.word_count[c][w] + 1) / denom)
return logp(c1) - logp(c2)
print(sorted(set(w for _, c in train for w in _.split()),
key=logodds, reverse=True)[:5])
複雜度分析#
- 訓練:O(總詞數),一掃即成;空間 O(|V|×|C|)。
- 預測:O(|文件詞數|×|C|)——數百類也只毫秒級;適合即時過濾。
- 準確率定位:小型情感/主題語料上常達 80–90%;缺點:無法利用詞序(「不好」與「好」同袋)、對新詞與語義變化敏感(詞向量/語言模型可補)。
- 與其他方法量級比較:邏輯迴歸訓練 O(詞數×迭代);BERT 推論每文件毫秒~十毫秒級——貝氏快 1000 倍,這是工程取捨。
練習#
- 實作字級 bigram 特徵版:不靠分詞,把中文文字切成相鄰兩字為特徵重新訓練,與詞級版比較在「今天 天氣 很好」類短句上的表現。
- 把 α 從 0.01 掃到 10,觀察「訓練集準確率不變、測試集先升後降」的現象,解釋平滑為何本質上是正則化。
- 找一個真實情感資料集(如
nltk的 movie_reviews 或自行標註 50 筆),訓練貝氏分類器,輸出 log-odds 前 10 的正面/負面詞,檢查是否有「假訊號」(如資料集特殊的作者名)——這正是垃圾郵件過濾對抗 spammer 對抗的日常。