跳轉到

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)。兩個工程要點:

  1. 平滑(Laplace smoothing):測試文件出現訓練時某類別沒見過的詞,P(w|c)=0 會把整個乘積打成 0。加 α(常取 1):P(w|c) = (count+α)/(總數+α|V|)。
  2. 對數域計算:幾百個小於 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 節)效果最好但最貴。快、省、可解釋,讓貝氏至今活躍於輕量場景。

演算法步驟#

  1. 訓練:對每個類別 c,統計先驗 P(c) = 該類文件數/總數;統計詞頻 count(w, c)。
  2. 平滑後估發射機率:P(w|c) = (count(w,c)+1)/(Σ_w' count(w',c)+|V|)。
  3. 預測:對新文件分詞,對每類計算 log P(c) + Σ log P(w|c)(未見詞用平滑機率)。
  4. 取分數最大的類別;可輸出各類後驗(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 倍,這是工程取捨。

練習#

  1. 實作字級 bigram 特徵版:不靠分詞,把中文文字切成相鄰兩字為特徵重新訓練,與詞級版比較在「今天 天氣 很好」類短句上的表現。
  2. 把 α 從 0.01 掃到 10,觀察「訓練集準確率不變、測試集先升後降」的現象,解釋平滑為何本質上是正則化。
  3. 找一個真實情感資料集(如 nltk 的 movie_reviews 或自行標註 50 筆),訓練貝氏分類器,輸出 log-odds 前 10 的正面/負面詞,檢查是否有「假訊號」(如資料集特殊的作者名)——這正是垃圾郵件過濾對抗 spammer 對抗的日常。