N-gram 語言模型實作:從機率原理到 Python Trigram 範例
N-gram 是學習自然語言處理時很適合入門的語言模型:規則清楚、需要的資源不多,也能直接把「前面幾個詞出現後,下一個詞有多大機率」算出來。本文保留原文的實作方向,重新整理成一個可以照著理解與執行的 Python trigram 範例,並說明分詞、平滑、評估與模型限制。
N-gram 語言模型是什麼?
N-gram 指的是連續的 N 個 token。token 可以是詞、字元或經過分詞工具切出的中文字詞;模型會從語料統計這些片段的出現次數,估計下一個 token 的機率。
| 模型 | 觀察的片段 | 簡單例子 |
|---|---|---|
| Unigram | 1 個 token | 「語言」 |
| Bigram | 連續 2 個 token | 「語言 模型」 |
| Trigram | 連續 3 個 token | 「語言 模型 可以」 |
真正的句子機率可以依照鏈式法則拆開,但完整歷史會讓估計變得困難。N-gram 以有限馬可夫假設近似它:預測下一個 token 時,只看前面固定數量的 token。Trigram 因此只使用前兩個 token 作為條件。
從句子機率到 trigram 機率
若句子由 w₁, w₂, …, wₜ 組成,語言模型要估計的是整句出現的機率。Trigram 的近似可以寫成:
P(w₁…wₜ) ≈ ∏ᵢ P(wᵢ | wᵢ₋₂, wᵢ₋₁)
最直接的最大概似估計(MLE)是用計數相除:
P(wᵢ | u, v) = count(u, v, wᵢ) / count(u, v)
這個公式很容易理解,但只要測試資料出現訓練資料沒有見過的片段,分子就會是 0,整句機率也會被乘成 0。因此實作時必須處理稀疏與未知詞問題。
為什麼要加入開始、結束與未知詞標記?
訓練前通常會在句首加入 <s>,句尾加入 </s>。對 trigram 而言,句首需要兩個開始標記,模型才能學到「一個句子通常如何開始」。把訓練語料中太少見的詞先映射成 <UNK>,也能讓測試階段遇到新詞時仍有機率可算。
中文還有一個額外步驟:先決定 token 是單字還是詞。下面的範例直接使用已分詞的中文字詞,避免把整句中文誤當成一個 token;實際專案可在這一層接上適合語料的分詞工具。
用 Python 建立最小 trigram 模型
先準備幾句已分詞的訓練資料。程式只依賴 Python 標準函式庫,方便先觀察計數與機率,不把套件封裝細節藏起來。
from collections import Counter
def pad(sentence):
"""在句首、句尾加入邊界標記。"""
return ["<s>", "<s>", *sentence, "</s>"]
def train_trigram(sentences):
trigram_counts = Counter()
context_counts = Counter()
vocabulary = set()
for sentence in sentences:
tokens = pad(sentence)
vocabulary.update(tokens)
for i in range(2, len(tokens)):
context = (tokens[i - 2], tokens[i - 1])
word = tokens[i]
trigram_counts[(*context, word)] += 1
context_counts[context] += 1
return trigram_counts, context_counts, vocabulary
接著用加一平滑(Laplace smoothing)計算條件機率。它不是所有任務的最佳選擇,但很適合用來示範「沒有看過的 trigram 也保留一點機率」這個概念。
def probability(context, word, trigram_counts, context_counts,
vocabulary, alpha=1.0):
context = tuple(context)
numerator = trigram_counts[(*context, word)] + alpha
denominator = context_counts[context] + alpha * len(vocabulary)
return numerator / denominator
def top_next(context, trigram_counts, context_counts, vocabulary,
top_k=5, alpha=1.0):
candidates = [
(word, probability(context, word, trigram_counts,
context_counts, vocabulary, alpha))
for word in vocabulary
if word not in {"<s>"}
]
return sorted(candidates, key=lambda item: item[1], reverse=True)[:top_k]
用幾句小語料測試:
corpus = [
["花博", "即將", "在", "台中", "舉行"],
["台中", "花博", "吸引", "許多", "遊客"],
["遊客", "可以", "搭乘", "大眾運輸"],
]
trigrams, contexts, vocabulary = train_trigram(corpus)
print(top_next(("花博", "即將"), trigrams, contexts, vocabulary))
輸出會是一組依機率排序的候選 token。語料非常小時,候選結果只代表示範計算流程,不能當成具備泛化能力的正式模型。
平滑、回退與資料稀疏
語料越大,可能出現的 n-gram 組合也越多,未觀察到的組合幾乎不可避免。常見處理方式如下:
| 方法 | 概念 | 適合用來理解什麼 |
|---|---|---|
| MLE | 直接用計數相除 | 最基本的機率估計,但遇到零計數會失效 |
| Laplace/add-one | 每個計數先加一 | 容易實作的入門平滑方法 |
| 插值或 backoff | 高階 n-gram 不足時退回低階模型 | 在資料稀疏時保留較穩定的估計 |
| Kneser–Ney 等方法 | 重新分配不同 n-gram 的機率質量 | 進一步改善語言模型的稀疏問題 |
若要使用成熟實作,NLTK 的 nltk.lm 提供 MLE、Laplace、Absolute Discounting 與 Kneser–Ney 等模型;先理解上面的計數版本,再換成套件,通常比較容易找出資料或參數問題。
如何評估 N-gram 模型?
不要只看模型生成的一句話。應將資料拆成訓練集與測試集,並在測試集上計算對數機率或困惑度(perplexity)。困惑度可粗略理解為模型對下一個 token 的不確定程度;在相同資料切分、tokenizer 與評估設定下,數值較低通常代表模型對測試語料的預測較集中。
PP(W) = P(w₁…wₜ)−1/t
評估時要固定資料切分,避免把測試句子或其 n-gram 提前放進訓練資料;否則看似漂亮的結果可能只是資料洩漏。
N-gram 的優點與限制
- 優點:計算與解釋簡單、訓練速度快、可在資源有限的環境中作為基準模型。
- 限制:只能看固定長度的上下文,對未見過的組合敏感,也不會真正理解語意或世界知識。
- 適用場合:教學、搜尋/輸入法的基礎候選、拼寫或語料統計基線,以及需要可解釋性的快速實驗。
Transformer 等神經語言模型能處理更長的上下文,但不代表 N-gram 沒有價值。用一個透明的 N-gram baseline 先確認資料、分詞與評估流程,往往能讓後續模型比較更可靠。
實作時最常見的錯誤
- 把整句中文當成一個 token:模型只會記住句子,無法學到詞與詞之間的關係。
- 沒有邊界標記:模型無法區分句首與句尾,生成時也容易出現不合理的開頭。
- 完全不處理未知詞:測試資料一出現新詞,機率就會變成零。
- 先看測試資料再調參:會造成資料洩漏,評估結果不能代表泛化能力。
- 只追求更大的 N:N 越大不一定越好,稀疏、記憶體與計算成本可能快速增加。
常見問題 FAQ
N 越大,語言模型一定越準嗎?
不一定。較大的 N 能看到更多上下文,但也需要更多資料才能觀察到足夠的組合;應以固定測試集比較,不能只憑 N 的大小判斷。
N-gram 和大型語言模型是同一種東西嗎?
兩者都能估計下一個 token,但方法不同。N-gram 主要依賴有限上下文的計數;大型語言模型則以神經網路學習參數化表示與更長的上下文。
中文應該用字元 N-gram 還是詞 N-gram?
取決於任務與資料。字元 N-gram 不需要分詞、對新詞較有韌性;詞 N-gram 可保留詞義單位,但依賴分詞品質與詞彙覆蓋率。可以在同一份驗證資料上比較兩者。
還值得學 N-gram 嗎?
值得。它是理解語言模型、平滑、資料稀疏與 perplexity 的低門檻入口,也能作為更複雜模型的可解釋基準。
延伸閱讀與實作方向
想把範例延伸到真實資料,可以先建立固定的 tokenizer 與資料切分,再比較不同 N、平滑方法與未知詞策略。接著再閱讀 NLTK 語言模型模組文件,以及 Stanford CS224N 的 N-gram 語言模型講義。站內也可以延伸閱讀 語言模型推論與加速相關文章,或瀏覽 AI 與 LLM 分類。
總結
N-gram 的核心就是用有限上下文的計數來估計下一個 token。從正確分詞、加入邊界標記,到處理未知詞、平滑與測試集評估,每一步都會影響結果。先用小而透明的 trigram 實作驗證流程,再逐步增加資料與模型複雜度,會比直接套用大型模型更容易理解問題出在哪裡。















