本ページは n-gram(N-gram)を多角的に解説します。 上のチップは、 検索・関連語の手がかりです。
「n gram」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「n gram」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。
これらのキーワードは「n gram の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。
🍰 まずはやさしく
文字をいくつかの組に分ける方法です。
文章のルールを簡単に表すために使います。
スマホの予測変換などで役立っています。
この手法の結論を短くまとめます。
🍰 まずはやさしく
言葉を扱う技術の基礎となる考え方です。
検索や翻訳などの仕組みを作るために使います。
ネットでの調べものなど身近な場所で動いています。
どのような場面で使われるかを見ていきましょう。
n-gram は自然言語処理(NLP)の最も基本的なテキスト表現の 1 つです。 1980〜2010 年代の言語モデルの主役で、 Google の N-gram コーパスは検索品質に大きく貢献しました。 現在は Transformer が主流ですが、 軽量・高速・解釈容易な性質から、 検索エンジン・スペル訂正・自動補完で 今も現役。 BLEU や ROUGE の評価指標の核でもあります。
🍰 まずはやさしく
文章を小さな窓で切り取るイメージです。
言葉のつながりを直感的に捉えるために使います。
部活の報告書などを短く切る様子を想像してください。
具体的にどう切り分けるかを解説します。
n-gram のイメージを掴むには、 文章を n 単語の窓でスライドさせて切り取る作業を思い浮かべましょう。
例:「今日 は 良い 天気 です」(5 単語)
| n | 名前 | 抽出される n-gram | 個数 |
|---|---|---|---|
| 1 | ユニグラム | 今日/は/良い/天気/です | 5 |
| 2 | バイグラム | 今日_は/は_良い/良い_天気/天気_です | 4 |
| 3 | トライグラム | 今日_は_良い/は_良い_天気/良い_天気_です | 3 |
n が大きいほど 長い文脈を保持できますが、 同時に データのスパース性が爆発的に増えます(コーパスに現れない組み合わせばかりになる)。 実務では n=2〜5 が標準。
N-gramを 30 秒で言えば「テキストを連続する N 個のトークン(文字 / 単語)の組として表現する手法。 N=2 ならバイグラム。」ですが、 実務で迷わないためにはもう一段深い理解が必要です。 ここでは「何が分かれば自信を持って使えるか」を、 3 つの観点で整理します。
| 観点 | 問い | 答え方の指針 |
|---|---|---|
| 定義の根拠 | なぜこの式・この定義になったのか? | 「何を最小化/最大化したいか」から逆算する |
| 境界条件 | いつ使える/使えないのか? | 「データの形」「分布の前提」を確認する |
| 他との関係 | 隣接概念とは何が違うのか? | 「共通点」と「分かれ目」を 1 つずつ挙げる |
💡 暗黙の前提:N-gram が「うまく機能する」には、 データに対する暗黙の仮定(独立同分布、 適切な前処理、 十分なサンプル数)があります。 これを言語化できるかどうかで、 失敗時のデバッグ力が大きく変わります。
N グラム(n-gram)とは、 テキスト中で連続する N 個のトークン(単語・文字・サブワード)の組のことを指す。 例えば「広島県の県庁所在地は広島市」という文を文字 N グラムにすると、 N=2 のとき「広島/島県/県の/の県/県庁/庁所/所在/在地/地は/は広/広島/島市」となる。 N=1 を unigram、 N=2 を bigram、 N=3 を trigram と呼ぶ。 これだけの単純な構造でありながら、 機械翻訳の BLEU 評価、 文書分類の特徴量、 統計的言語モデル、 入力補完、 スペル訂正、 そして近年では subword tokenizer(BPE, SentencePiece)の前段にまで広く使われている。
N グラムの威力は「N を増やすと文脈が伸び、 N を減らすと一般化が効く」という単純なトレードオフにある。 N=1 では「広島」が単独でどれだけ出るかしか分からないが、 N=3 にすると「広島県の」が「広島市の」より頻出するかなどが見える。 ただし N=5 にすると組合せ数が爆発して、 ほとんどの 5-gram が訓練データに 0 回しか出ないという data sparsity 問題が一気に深刻化する。 だから現実の N グラム言語モデルでは Kneser-Ney smoothing などの巧みな平滑化技法が必須となる。
SSDSE-B-2026 の 47 県名(北海道、 青森県、 岩手県、 …、 沖縄県)を結合して「北海道青森県岩手県…沖縄県」という長い文字列にしてみる。 これに対する文字 2-gram を抽出すると、 47 県のうち実に 43 県が「県」で終わるため「○県」 の 2-gram が支配的(例:「森県」「手県」「城県」…)。 さらに 3-gram にすると「青森県」「岩手県」「宮城県」のような 各県固有の 3-gram が現れる。 「県」で終わらない北海道・東京都・大阪府・京都府のうち、 例えば「北海道」は「北海」「海道」のような独自パターンを示す。 ここから「N が大きいほど固有名詞識別性は上がるが、 共有パターンは少なくなる」 という N グラムの核心が体感できる。
🍰 まずはやさしく
文字の並び方を数式で表したものです。
次の言葉が来る確率を計算するために使います。
買い物リストの単語の並び方を数えるようなものです。
計算の方法を詳しく説明します。
n-gram 言語モデルの基本:
数式 $P(w_n | w_1, \dots, w_{n-1}) \approx P(w_n | w_{n-N+1}, \dots, w_{n-1})$ を「ぼんやり眺める」から「自分の言葉で説明できる」レベルに引き上げます。
左辺は何か(スカラー?関数?)、 右辺は和・積・最大化のどれが主役か。 ここで「式の文型」が見えます。
記号それぞれに「データ/パラメータ/確率/集合」のラベルを貼り、 「これは固定」「これは動かす」を区別します。
サンプルが 1 個、 すべて同じ値、 完全にランダム、 などの極端なケースで式がどう振る舞うか確認すると、 数式が「ただの記号」から「動く道具」になります。
N グラム言語モデルは、 自己回帰確率分解 $P(\mathbf{w}) = \prod_t P(w_t \mid w_{ $$P(w_t \mid w_1, \dots, w_{t-1}) \approx P(w_t \mid w_{t-N+1}, \dots, w_{t-1})$$ この確率は最尤推定(MLE)で次のように計算する: $$P_{\mathrm{MLE}}(w_t \mid w_{t-N+1}, \dots, w_{t-1}) = \frac{C(w_{t-N+1}, \dots, w_t)}{C(w_{t-N+1}, \dots, w_{t-1})}$$ ここで $C(\cdot)$ はその N グラムの訓練データでの出現回数。 ただし MLE は未観測 N グラムに対し確率 0 を与えるという致命的欠陥がある(zero probability 問題)。 これを救うのが平滑化。 最も単純な add-k smoothing(Lidstone): $$P_{\mathrm{add\text{-}k}}(w_t \mid w_{ $k$ は典型的に $0.01\sim 1$、 $|V|$ は語彙サイズ。 さらに良いのが Kneser-Ney smoothing で、 これは「ある単語が いくつ違う文脈の続きとして 現れるか」を考慮する。 評価には perplexity を使う: $$\mathrm{PPL}(\mathbf{w}) = \exp\!\left(-\frac{1}{T}\sum_{t=1}^{T} \log P(w_t \mid w_{t-N+1}, \dots, w_{t-1})\right)$$ $P(w_t \mid w_{ $P_{\mathrm{MLE}} = C(w_{1:N}) / C(w_{1:N-1})$ は「N グラムの回数 ÷ 直前 (N-1) グラムの回数」。 数式を言葉で読み解くと、 これは「広島県の」 が 100 回出てそのうち 80 回が「県庁所在地」 だったなら、 P(県庁所在地 | 広島県の) = 0.80」という条件付き頻度。 自然で直感的だが、 訓練データに 1 度も出ない N グラムは即 0 確率になり、 言語モデルとしての PPL を発散させる。 だから「見えなかった事象にも小さい確率を割り振る」平滑化が必須。 $P_{\mathrm{add\text{-}k}} = (C + k) / (C_{\mathrm{prev}} + k|V|)$ は「分子に k を足し、 分母に k×語彙サイズを足す」。 数式を言葉で読み解くと、 これは「未観測の語彙全てに『仮想 k 回観測された』 と見なす」操作。 k=1 (Laplace) だと過剰に滑らかになり既知 N グラムの確率まで下がるので、 k=0.01 など小さい値が好まれる。 Kneser-Ney はこの「未観測語の確率を割り振る」設計がさらに洗練されている。 文書類似度の計算では、 「単位を文字にするか、 単語にするか」 で精度が変わる。 SSDSE 県紹介短文で実測する。 このコードでやること:47 県名を文字 trigram と単語 bigram で表現し、 cosine 類似度を比較。 📤 実行結果: 💬 文字 trigram の方が「テンプレ文の共通部分(は人口、 万人、 高齢者)」を拾える分、 単語 bigram より類似度が高くなる(0.062 > 0.048)。 単語 bigram は固有名(東京、 大阪)や数値の違いを強調する。 なお絶対値が小さいのは、 数値トークンが県ごとに異なり共有 N グラムが限られるため。 現代の LLM(GPT、 Llama、 Claude)はトークン単位として BPE / SentencePiece / WordPiece を使う。 これは「文字 N-gram と単語 N-gram の中間」に相当する位置づけ。 → 結果として「頻出単語は単 1 トークン、 珍しい固有名詞は文字単位」というハイブリッド表現になる。 💬 BPE は「東京」のような頻出ペアを 1 トークン化し、 「那覇」のような珍しい組合せは文字単位にバラす。 これにより未知語・誤字に強く、 語彙サイズも抑えられる。 SSDSE-B-2026 から作った県紹介短文 47 件をコーパスとした場合の perplexity の目安。 ※ 本表の数値は挙動の傾向を示す例示値(コーパス構成・実装で変動する。 実測値ではない)。 💬 N=2/3 + KN が n-gram 系の上限。 同じコーパスで neural LM は半分以下、 GPT-2 は 1/3 以下に PPL を落とす → n-gram は教育・軽量推論向け、 本番 NLP は neural に移行済み。 N-gram は「N 個の連続トークンの共起頻度」が本質である。 ここでは SSDSE-B-2026 を元に作成した県紹介短文 47 本 の bigram 統計から、 (1) bigram 頻度のヒストグラム、 (2) 主要 bigram 散布図(unigram 頻度 vs bigram 頻度の連動)、 (3) スムージング前後の確率分布の箱ひげの 3 枚を示す。 数式の意味を視覚化することで「なぜスムージングが必要か」「なぜ N を上げるとデータ不足になるか」が直感で理解できる。 ※ 本節で挙げる比率・相関などの数値(78%、 r ≈ 0.93 等)は説明用の例示値であり、 コーパスの作り方(テンプレ文の文言)によって変動する。 読み取り: bigram の 78% は頻度 1(出現回数 1 回)、 14% が頻度 2、 5% が頻度 3、 残り 3% が頻度 4 以上である。 横軸は出現回数、 縦軸は該当する bigram 種別数。 典型的な Zipf 則の形をしており、 ごく少数の高頻度 bigram(「都道府県 が」「人口 は」など)が確率質量の大半を占める一方、 ロングテール側に大量の「1 回しか見たことがない bigram」がぶら下がっている。 これが 未知 bigram に確率 0 が割り当てられる構造的理由である。 もし新しい文に頻度 1 の bigram がひとつでも含まれていれば、 MLE 推定では P=0 となり全体の確率も 0 になる(perplexity が ∞ に発散)。 ヒストグラムの右側裾の薄さは、 高い N(trigram、 4-gram)に増やすほど指数関数的に深刻化する。 💬 視覚的教訓: bigram で既にロングテール。 trigram にすると「頻度 1 が 95% 以上」になり、 統計的に不安定。 Add-1、 Kneser-Ney、 Witten-Bell などのスムージングは「ロングテールに微小確率を与え、 ゼロを回避する」操作であることが図から読み取れる。 読み取り: 横軸は単語 w の unigram 頻度、 縦軸は w を含む bigram の延べ頻度。 強い正の相関(r ≈ 0.93)が読み取れ、 高頻度語ほど多様な bigram に参加することがわかる。 ただし「は」「が」「の」のような機能語は同じ unigram 頻度でも特に多くの bigram に参加する一方、 「合計特殊出生率」のような内容語は unigram 頻度が高くても少数の決まった bigram にしか現れない。 図の右上に飛び抜けて位置するのは「都道府県」「人口」「面積」など SSDSE-B-2026 が頻出させる名詞群、 左下のクラスタは数値や固有名詞である。 この図は Kneser-Ney スムージングが「unigram 頻度ではなく『何種類の異なる文脈と共起したか』を重視する」発想の根拠を視覚化している。 機能語は頻度は高いが「サンキスコープ(後続文脈の多様性)」が中程度、 一方「鳥取」「島根」のような県名は頻度こそ低いが「県」「は」「の」など決まった後続と高頻度で共起する。 Kneser-Ney はこの「文脈多様性」を直接モデル化する。 💬 視覚的教訓: 「頻度」と「文脈多様性」は別物。 これが「Kneser-Ney は Add-1 より圧倒的に PPL が低い」理由を示す。 散布図上で「同じ unigram 頻度でも縦軸が大きく異なる単語ペア」が存在することが視覚化の鍵である。 読み取り: 3 つの推定方式(MLE、 Add-1、 Kneser-Ney)について、 全 bigram の確率を箱ひげで比較した。 MLE は中央値が比較的高く、 ばらつきも大きいが、 ヒゲの下端は 0 に張り付く(未知 bigram で確率 0)。 Add-1 は全体的に確率が押し下げられ、 高頻度 bigram の確率も大きく目減りする(語彙数 V が大きいほどこの劣化が深刻になる)。 Kneser-Ney は高頻度 bigram の確率を可能な限り保持しつつ、 未観測 bigram にも文脈多様性に応じた確率を配分しているため、 箱の高さ(IQR)が穏やかで、 ヒゲの下端も 0 から離れている。 これが PPL 28.7 → 22.1 という劇的な改善の正体である。 視覚的には「Add-1 は全体的に平らで情報量が失われ、 Kneser-Ney は富む者と貧する者の間を程よく均す」と読める。 💬 視覚的教訓: スムージングは「ゼロ回避」だけでなく「確率分布全体の形を整える」操作。 Add-1 のように雑にやると高頻度 bigram の確率が大きく削られ、 言語の自然さが失われる。 Kneser-Ney のように「観測された分布の良いところは残し、 未観測部分のみ補正する」方式が望ましい。 図だけでは「なんとなく」で終わってしまうため、 実際の数値で「bigram 頻度・条件付き確率・perplexity」をミニ計算する。 SSDSE-B-2026 から構成した短文「人口 は 多い」「人口 は 少ない」「面積 は 広い」「面積 は 狭い」を仮想コーパスとする。 全 token 数は 12、 全 bigram 数は 8(各文 2 個 × 4 文)。 計算の読み取り: MLE では「は 高い」のような未観測 bigram に確率 0 が割り当てられるため、 これを含む新しい文の確率はすべて 0 になり、 perplexity は ∞ に発散する。 Add-1 スムージング(V=7 の語彙)では分子に +1、 分母に +V=+7 を足すことで P=(0+1)/(4+7)=0.091 という小さいが非ゼロの確率を割り当てる。 一方、 高頻度 bigram「人口 は」は MLE で 1.000 だったものが Add-1 で 0.333 まで削られており、 「ロングテール救済の代償として高頻度バンドが大きく目減りする」現象を実感できる。 Kneser-Ney ならこの代償をはるかに小さく抑えられる(高頻度 bigram の P を 0.85 以上保ったまま、 未観測 bigram に 0.05 程度を配分)。 テスト文「人口 は 多い」(3 トークン、 2 bigram)について、 各方式の perplexity を計算する。 定義は PPL = exp(-1/N × Σ log P)。 MLE:P(人口|BOS)=2/4=0.5、 P(は|人口)=1.0、 P(多い|は)=0.25。 平均対数確率 = (log 0.5 + log 1.0 + log 0.25)/3 = -0.693。 PPL = exp(0.693) ≈ 2.00。 Add-1(V=7):P(人口|BOS)=(2+1)/(4+7)≈0.273、 P(は|人口)=0.333、 P(多い|は)=0.182。 平均対数確率 = (log 0.273 + log 0.333 + log 0.182)/3 = -1.367。 PPL ≈ exp(1.367) ≈ 3.93。 Kneser-Ney(推定値):それぞれ 0.22、 0.85、 0.24。 平均対数確率 ≈ -1.03。 PPL ≈ 2.82。 同じテスト文でも MLE は 2.00、 Add-1 は 3.93、 Kneser-Ney は 2.82 と推定方式で大きく異なる。 MLE が最も低い PPL に見えるが、 これはテスト文に未観測 bigram が含まれない「ラッキー」なケースであり、 ひとつでも未観測 bigram があれば PPL は ∞ となる脆さを抱えている。 平均的な perplexity(多数のテスト文での幾何平均)では Kneser-Ney が最良になる。 N-gram の根幹は (N-1) 次 Markov 仮定 P(wi|w1, ..., wi-1) ≈ P(wi|wi-N+1, ..., wi-1) である。 図 2 の散布図はこの仮定が「単語の使われ方を {直前 N-1 語} だけで表現する」操作になっていることを物語る。 文の遠く離れた位置にある主語と動詞の整合性(例:「鳥取県の人口は…多い」の主語と述語)はモデルに見えない。 これが N-gram が長距離依存性を捉えられない構造的限界であり、 後述の RNN / LSTM / Transformer の動機となる。 図 1 のロングテールは「N を上げるほどデータ不足になる」呪い(curse of dimensionality)の視覚化で、 図 3 の箱ひげは「スムージングがその呪いを部分的に救う」操作の視覚化である。 3 枚の図を組み合わせると、 N-gram の長所(高速、 解釈可能)と短所(長距離依存性無視、 ゼロ確率脆弱性、 N の上限)の両方が一望できる。 SSDSE-B-2026 の県紹介テンプレート 47 文を bigram でモデル化した実例を見る。 「鳥取県の人口は約 55 万人で全国 47 位である」のような定型句が多いため、 高頻度 bigram の上位 20 件で全 bigram 出現の 38% を占める一方、 47 都道府県の県名・数値で構成される名前 bigram は単発出現が大半である。 そのため bigram で県名を予測しようとすると、 既出県は予測できるが新規県名は確率 0 になる。 ここで Add-1 を入れると新規県名にも非ゼロ確率を割り当てられるが、 「広い県」「狭い県」のような形容詞 + 県カテゴリの正確な共起頻度は失われる。 Kneser-Ney なら「県」という後続が多数の形容詞・動詞と共起してきた事実をモデル化できるため、 高頻度共起を保ったまま新規形容詞にも合理的な確率を割り当てられる。 図 1-3 の説明はそのまま実コーパスでの挙動と一致する。 演習 1(理解問題): 図 1 のロングテール構造から、 「trigram に拡張するとなぜデータ不足が深刻化するか」を 100-150 字で説明せよ。 また、 同じ SSDSE-B-2026 コーパスで trigram の頻度 1 の割合が bigram より大きくなる理由を、 組合せ論的観点から述べよ。 解答例: bigram の頻度 1 比率は 78% だが、 trigram は「3 トークンの組合せ」になるためコーパスサイズが同じでも観測されうる組合せ数が爆発し、 既観測組合せの大半が頻度 1 になる。 SSDSE-B-2026 短文 47 文 (約 600 token) で語彙 V=200 なら、 理論上の bigram 数は V²=40,000、 trigram 数は V³=8,000,000 で、 観測比率はそれぞれ ~1.5%、 ~0.0075% と桁違いに薄くなり、 トレーニング時の頻度 1 比率は trigram で 95% を超える。 演習 2(計算問題): 上掲のミニコーパス(人口 は 多い/少ない、 面積 は 広い/狭い)に対し、 新しいテスト文「面積 は 高い」(「高い」は未観測形容詞)の bigram 確率と perplexity を、 (a) MLE、 (b) Add-1 (V=7) で計算せよ。 MLE では P(面積|BOS) = 2/4 = 1/2 とする。 解答例: (a) MLE:P(面積|BOS)=0.5、 P(は|面積)=1.0、 P(高い|は)=0/4=0 → 文全体の確率 0、 PPL=∞。 (b) Add-1:P(面積|BOS)=(2+1)/(4+7)≈0.273、 P(は|面積)=(2+1)/(2+7)=0.333、 P(高い|は)=(0+1)/(4+7)=0.091。 文全体の確率 = 0.273 × 0.333 × 0.091 ≈ 0.00827。 平均対数尤度 = log(0.00827)/3 ≈ -1.60。 PPL = exp(1.60) ≈ 4.95。 MLE は破綻するが Add-1 なら有限値で評価できる。 演習 3(応用問題): 図 3 の Kneser-Ney がなぜ「ゼロ確率を回避しつつ高頻度バンドの確率も保つ」のかを、 unigram の continuation probability の考え方を用いて 150-200 字で説明せよ。 Add-1 が苦手とする「高頻度バンドが圧縮される」問題を、 Kneser-Ney がどう回避しているかを含めること。 解答例: Kneser-Ney は単語 w の確率を「w が何種類の異なる文脈と共起したか」(continuation count)で評価する。 例:「は」は機能語で多くの文脈と共起するため continuation 確率が高く、 「合計特殊出生率」は内容語で少数の文脈にしか現れないため低くなる。 Add-1 は全 bigram に一律 +1 する「一律補正」のため、 高頻度バンドからも +V 分の分母増加で確率を奪うが、 Kneser-Ney は「観測 bigram には絶対割引 d を引き、 引いた分を未観測 bigram に continuation 確率に比例して再配分」する。 これにより高頻度バンドの確率は (count - d)/count で大半が保たれ、 未観測には continuation 確率に応じた合理的な値が配分される。 結果として PPL は Add-1 比で半分以下まで改善する。 視覚で N-gram の挙動を理解したら、 次は「自分の手で 50 行程度の bigram 言語モデルを書く」段階に進む。 SSDSE-B-2026 から構成した県紹介短文 47 本をコーパスに、 collections.Counter で unigram・bigram の頻度を数え、 MLE と Add-1 と Kneser-Ney の 3 方式で同じテスト文を評価する流れを示す。 ここでは詳細コードは別セクションに譲り、 視覚的観点で「各方式が確率分布の形をどう変えるか」を解説する。 MLE では辞書 {(wi-1, wi): count / unigram[wi-1]} がそのまま確率テーブルとなり、 未観測キーへのアクセスは KeyError か defaultdict で 0 を返す。 Add-1 では辞書をループせず、 関数 P(wi | wi-1) = (count.get((wi-1, wi), 0) + 1) / (unigram.get(wi-1, 0) + V) として動的に計算する。 Kneser-Ney は前計算が複雑で、 (a) 各 wi-1 に対する絶対割引 d × 観測種別数 / 観測合計、 (b) wi の continuation 確率 = w が「何種類の異なる文脈の後に現れたか」 / 全 bigram 種別数、 の二段構えになる。 この実装段階で初めて「Kneser-Ney は単なる式変形ではなく、 別のデータ構造が必要」だと痛感する。 視覚的にはこの実装差が、 図 3 の箱ひげの形状差として現れている。 講義では 50 行のコードを LiveShare で書きながら、 図 3 をリアルタイムに更新する演習を推奨する。 N-gram は古典手法だが、 現代 Transformer 系モデルの内部にも「短距離 N-gram 的挙動」が残存する。 attention の注意重みを heatmap で可視化すると、 大半のヘッドが直前 2-5 token に強い重みを置いており、 N=2-5 の bigram/5-gram に近い挙動を学習している。 これは「Transformer が自動で N を可変に伸縮する N-gram になっている」という解釈を可能にする。 実際、 distillation 研究では Transformer の attention を bigram カウントテーブルで近似してもタスク性能の 70-80% を保てる場合がある。 図 2 の散布図が示す「頻度と文脈多様性の二軸」は、 Transformer の subword embedding 空間でも保存されており、 高頻度 subword は embedding ノルムが小さく、 低頻度 subword は大きい(=確率質量を保持するために大きなベクトルが必要)という現象として現れる。 図 3 の箱ひげが示す「確率分布の形」も、 softmax 温度パラメータ T の調整に対応する。 T=1.0 は MLE、 T→∞ は Add-1 に似た一様化、 T<1.0 は Kneser-Ney に似た「高頻度バンドを保ちつつ未観測も拾う」分布。 この三層構造を視覚的に把握すると、 古典 N-gram の知識が現代 Transformer の理解に直結する。 N の値(1=unigram、 2=bigram、 3=trigram、 4 以上=高次 n-gram)は、 タスクとコーパスサイズで決まる。 視覚的決定基準は次の通りである。 (1) コーパスサイズ < 100 万 token:N=2 が安全。 ヒストグラム(図 1 のような分布)の頻度 1 比率が 90% を超えたら、 N を 1 下げる。 (2) 100 万 ≤ コーパスサイズ < 1 億 token:N=3 が標準。 ただし perplexity 比較で trigram が bigram より 10% 以上改善しなければ、 計算コスト的に N=2 で十分。 (3) コーパスサイズ ≥ 1 億 token:N=4-5 まで上げられるが、 メモリと推論速度の制約で N=3 + interpolation を選ぶ場合が多い。 (4) 形態素か文字単位か:日本語の文字 N-gram は N=4-6 程度まで安定するが、 形態素 N-gram は N=3 で十分。 (5) ドメイン適応:医療・法律など定型表現の多いドメインでは N を高めに、 SNS・口語など多様性の高いドメインでは N を低めに。 これらの基準は、 図 1 のヒストグラム形状(左寄り or 右寄り)、 図 2 の散布図のクラスタ数、 図 3 の箱ひげの IQR で視覚的に判断できる。 N-gram を学んだ後の次のステップを、 視覚的アンカー付きで提案する。 (1) HMM・CRF:図 2 の「文脈多様性」概念を時系列構造に拡張。 N-gram が「直前 N-1 語」しか見ないのに対し、 HMM は「隠れ状態系列全体」を考慮する。 (2) word2vec・GloVe:図 2 の散布図を 100-300 次元の意味空間に拡張。 「は・が・の」のような機能語と「鳥取・島根」のような内容語が、 多次元空間で異なるクラスタを形成することを学ぶ。 (3) RNN・LSTM 言語モデル:図 1 のロングテール問題を「連続表現」で回避。 未観測 bigram でも近い意味の bigram から確率を補間できる。 (4) Transformer・BERT・GPT:図 3 の箱ひげの「適応的形状調整」を attention で実現。 softmax 温度・top-k・top-p sampling などのテクニックは、 N-gram の Kneser-Ney に相当する確率分布調整手法である。 各ステップで「N-gram 時代に見た 3 枚の図」を思い出すと、 新しい手法も視覚的にアンカーされて記憶に定着する。 図 1(bigram 頻度ヒストグラム)は matplotlib.pyplot.hist で 1 行。 collections.Counter で bigram 頻度を数え、 そのカウント値([1, 1, 2, 1, 3, ...])をヒストグラム化する。 ビン数は np.logspace で対数スケールにすると右側裾も見えやすい。 図 2(unigram-bigram 散布図)は plt.scatter で、 横軸 = unigram[w]、 縦軸 = sum(bigram[(w, *)] for any *)。 高頻度語の名前は plt.annotate で図上に表示すると視覚的に内容語と機能語の分離が見える。 図 3(箱ひげ)は plt.boxplot で、 3 つの確率リスト(MLE 推定値・Add-1 推定値・Kneser-Ney 推定値)を渡すだけ。 中央値・IQR・ヒゲ・外れ値が一目で比較できる。 これらはすべて pandas + matplotlib の標準操作で書けるため、 講義や勉強会で生徒に書かせる課題に最適である。 SSDSE-B-2026 のデータパスは N-gram の挙動は、 人間の言語処理にも一部対応する。 認知心理学の研究では、 人間の言語予測は「直前 2-3 単語」の文脈に強く依存しており、 これは bigram/trigram のモデル化単位と一致する。 図 2 の散布図で「は・が・の」が高頻度語クラスタに位置し、 内容語が低頻度クラスタに位置することは、 人間の脳内で機能語が「自動処理」され、 内容語が「意識的処理」されるという二段構えに対応する。 図 1 のロングテールは、 人間の語彙獲得が「ジップの法則」に従うことと同じ起源を持つ。 つまり、 N-gram は「人間の短期記憶ベースの言語処理」を統計的にモデル化したもの、 と認知科学的に位置付けられる。 これは Transformer の attention が「人間の選択的注意」をモデル化していることと並列で、 古典 N-gram も決して「時代遅れの統計モデル」ではなく、 認知科学的に妥当な近似であることを示している。 教育現場では、 この認知科学的観点を踏まえて N-gram を導入すると、 学生が「なぜ N=2-3 が普遍的に使われるか」を直感的に理解できる。 N-gram の概念は 1948 年 Claude Shannon の「Mathematical Theory of Communication」で導入された。 Shannon は英語テキストを文字 N-gram でモデル化し、 N を増やすほど生成テキストが英語らしくなることを示した。 これは情報理論の発展史で重要な実験であり、 図 1 のロングテール問題も Shannon の時代から認識されていた。 1980 年代に音声認識・機械翻訳の実用化に伴い N-gram の重要性が再認識され、 IBM の Brown らが大規模コーパスで bigram/trigram 言語モデルを構築した。 1990 年代に Kneser・Ney がスムージング理論を完成させ、 図 3 で示した「形状保持型スムージング」を理論化した。 2000 年代には Goodman の「A Bit of Progress in Language Modeling」が古典 N-gram の到達点をまとめ、 perplexity 比較の標準ベンチマーク(Penn Treebank、 Wikipedia)を確立した。 2013 年の word2vec、 2014 年の seq2seq、 2017 年の Transformer により N-gram は学術的には「歴史的手法」として扱われるようになったが、 実装の単純さ・解釈可能性・低リソース対応の点で現在も実用的に使われ続けている。 図 1-3 の知見はこの 80 年の累積であり、 視覚化することで一気に理解できる。 N-gram は現代でも下記のシーンで実務的に使われる。 (1) 音声認識のデコーダ後処理:音響モデルが出力する候補列を N-gram で再ランキングすると認識精度が 5-10% 向上する。 軽量で実装が単純なため、 デバイス上でも実行可能。 (2) スペル訂正:図 2 で示した「文脈多様性」を使い、 「typo の修正候補のうち、 文脈に最も自然な候補」を選ぶ。 Google・Apple の予測変換にも組み込まれている。 (3) 盗作・剽窃検出:論文・レポート中の N-gram シーケンスをコーパスと照合し、 重複度を測定。 Turnitin など商用ツールは内部で N-gram マッチングを使用。 (4) 機械翻訳の BLEU スコア:翻訳品質評価の de facto standard。 1-gram から 4-gram までの精度の幾何平均で算出。 (5) 言語識別:文字 N-gram の分布特性で文の言語を判別。 日本語と中国語の判別は文字 trigram で 99% 以上の精度。 (6) 軽量チャットボット:FAQ ボットなど決まった応答パターンのみ必要なシステムでは、 N-gram 検索で十分。 GPU 不要・レイテンシ < 1ms。 これらの実務シーンを念頭に置くと、 図 1-3 の視覚的理解が実装段階で即座に活きる。 N-gram モデルの評価には複数の指標があり、 タスクに応じた選択が必要である。 perplexity (PPL) は最も標準的で、 PPL = exp(-平均対数尤度)。 値が低いほどモデルが「テストデータを予測しやすい」ことを示す。 図 3 の箱ひげで Kneser-Ney が PPL 22.1 と最良だった理由は、 ロングテールを救済しつつ高頻度バンドを保持できたため。 cross-entropy は PPL の対数版で、 単位は bit。 PPL=2 = entropy 1 bit。 情報理論的解釈に便利。 BLEU score は機械翻訳で標準。 1-gram から 4-gram までの precision の幾何平均に brevity penalty を掛ける。 図 2 で示した「N=4 まで一致する区間が長いほど高 BLEU」という直感がそのまま指標化されている。 ROUGE は要約タスクで標準、 N-gram recall ベース。 METEOR は同義語・語幹も考慮する高度な指標。 distinct-n は生成多様性の評価で、 ユニークな N-gram の比率。 図 1 のロングテールが「広く分布している」モデルほど distinct-n が高い。 self-BLEU は生成文同士の BLEU で、 値が低いほど多様性が高い(reverse 指標)。 これらの指標を使い分けることで、 N-gram モデルの強み・弱みを多角的に評価できる。 学生が実装で躓くポイントを 10 個列挙する。 (1) BOS/EOS の扱い:文頭・文末を Transformer が支配的になった現在でも、 N-gram には固有の強みがある。 まず 解釈可能性:図 2 のような散布図で「どの bigram が高頻度か」を直接見られる。 Transformer の attention は heatmap で可視化できるが、 数百のヘッドと数千の次元を解釈するのは困難。 次に 推論速度:N-gram は辞書ルックアップで O(1)、 Transformer は O(N²) の自己注意で 100-1000 倍遅い。 IoT デバイスや低遅延チャットボットで N-gram が現役の理由。 さらに 少データ対応:図 1 のロングテール救済(スムージング)が確立しており、 1 万 token のミニコーパスでも実用的なモデルが組める。 Transformer は最低 10 万 token、 LLM は数億 token が必要。 また 更新の柔軟性:新しいドメインデータが来たら Counter を増分更新するだけ。 Transformer は再学習や fine-tuning が必要。 最後に ハイブリッド戦略:N-gram と Transformer を組み合わせ、 高頻度パターンを N-gram で処理、 長距離依存を Transformer で処理する研究が活発。 KNN-LM、 RETRO、 SPALM などが代表例で、 視覚的には「Transformer の上に N-gram レイヤーを乗せる」設計と言える。 これらの強みは、 図 1-3 で可視化したロングテール救済・頻度多様性分離・確率分布形状調整の三本柱に支えられている。 大学・大学院の NLP 講義で N-gram は依然として最初の 1-2 回で扱われる。 理由は「最小コードで言語モデルの本質に触れられる」「確率論・統計学・情報理論の応用例として完璧」「現代モデルへの橋渡し」の三点。 図 1-3 の視覚化教材があれば、 学生は 60 分の講義で「何が起きているか」を完全理解できる。 高校・高専でも、 文字 N-gram でしりとりゲームを作る・SNS テキストの簡易自動補完を作るなど、 親しみやすい題材で導入可能。 SSDSE-B-2026 のような公的データを使えば、 統計・データ解析コンペの参加者にとっても「自分のデータで N-gram を回す」体験が容易になる。 この教材で重要なのは「3 枚の図を自分で描く演習」を含めること。 描画コードは 30 分で書け、 描いた瞬間に「ロングテールってこういうことか」「Kneser-Ney がなぜ強いか」が腑に落ちる。 視覚化を伴わない数式だけの導入は、 古典的だが学習効率が悪い。 必ず図を伴って導入してほしい。 これは N-gram に限らず、 全ての確率モデル(HMM、 トピックモデル、 VAE、 GAN)の教育に通じる原則である。 N-gram の中核式 P(wi|wi-N+1, ..., wi-1) = c(wi-N+1, ..., wi) / c(wi-N+1, ..., wi-1) を、 図 1-3 の視覚と対応付けて再解説する。 図 1 のヒストグラムは「分子の頻度分布」を示し、 ロングテールが「分子 = 1」の事例の多さを表す。 図 2 の散布図は「分母(unigram 頻度)と分子の総和(bigram 頻度)の関係」を示し、 高頻度語ほど多様な bigram の分子に寄与することを表す。 図 3 の箱ひげは「分子 / 分母」で得られる確率値の分布を示し、 スムージング方式による形状差を表す。 Add-1 スムージングは P(wi|wi-1) = (c(wi-1, wi) + 1) / (c(wi-1) + V) と書け、 図 3 で「全体的に低くなる」現象は分母 +V の効果。 Kneser-Ney は P(wi|wi-1) = max(c(wi-1, wi) - d, 0) / c(wi-1) + λ(wi-1) × P_cont(wi)、 ここで P_cont(wi) は wi の文脈多様性確率。 図 2 の縦軸が直接 P_cont の元になる。 この対応関係を理解すると、 数式を見たときに即座に図が浮かび、 図を見たときに即座に式が浮かぶ「双方向理解」が確立される。 これが視覚化教材の最大の効能である。 本セクションを 60 分授業の教材として使う場合の進行例を示す。 (0-10 分) 導入:日常での自動補完体験を 2-3 例挙げ、 「これが N-gram でどう実現されるか」と問いかける。 (10-25 分) 図 1 のロングテール解説:実コーパスで Counter してヒストグラムを描く演習。 「頻度 1 の bigram が 78%」を体感させる。 (25-40 分) 図 2 の散布図解説:頻度と文脈多様性が別物であることを散布図で示す。 「は・が・の」と「鳥取・島根」の対比が効果的。 (40-50 分) 図 3 のスムージング解説:MLE・Add-1・Kneser-Ney を箱ひげで比較し、 PPL の劇的改善を実感させる。 (50-60 分) 演習・質疑:3 問の記述問題から 1 問を選び、 グループディスカッション。 講師は学生の解答例を発表させ、 図 1-3 を参照しながらフィードバック。 この進行で、 学生は「N-gram が何か」「なぜスムージングが必要か」「現代モデルとの繋がり」を一気通貫で理解できる。 復習教材として、 本ページの 3 図再現コードと演習解答例をリンク配布すれば、 自主学習にも繋がる。 N-gram モデルが持つ数学的特性を 8 項目で整理する。 (1) Markov 性:将来の予測は直前 N-1 状態のみに依存。 N=1 は無記憶、 N→∞ は完全文脈。 (2) 最尤推定の一貫性:コーパスサイズ → ∞ で MLE は真の分布に収束(ただしロングテール部分は遅い)。 (3) perplexity の幾何平均性:PPL = (Π P)^(-1/N) は対数和の指数で、 外れ値(低 P トークン)に過敏。 (4) スムージングと bayesian 事前分布の関係:Add-1 は Dirichlet(1, ..., 1) の MAP 推定、 Add-k は Dirichlet(k, ..., k)、 Kneser-Ney は Pitman-Yor 過程に対応。 (5) back-off と interpolation:N→N-1 への退化。 図 1 のロングテールを部分的に救う代替手法。 (6) perplexity の bound:下限は entropy H(L)、 上限は V(語彙サイズ)。 ベンチマークコーパスで PPL=50-100 が古典 N-gram の典型値。 (7) 計算量:トレーニング O(NT)、 推論 O(1) または O(N) (back-off)。 (8) 記憶容量:N-gram 辞書サイズ O(V^N) の上限。 実コーパスでは O(T) 程度に収まる(観測 N-gram のみ保持)。 これらの数学的特性が、 図 1-3 で可視化した挙動の理論的裏付けである。 本セクションで習得した「3 枚の図で N-gram を読む」スキルは、 そのまま他の確率モデル理解に転用できる。 HMM では「観測 N-gram の代わりに隠れ状態の N-gram」、 topic model では「文書 N-gram の代わりにトピック分布」、 VAE では「N-gram の離散確率の代わりに潜在変数の連続分布」を考えればよい。 図 1 のロングテールはどのモデルでも生じ、 図 2 の頻度多様性分離もユニバーサルで、 図 3 のスムージング相当(正則化・温度・ドロップアウト)もどのモデルにもある。 N-gram を視覚的にマスターした学生は、 数か月後に Transformer・LLM を学ぶときも「あの 3 枚の図はここで使われている」と即座に接続できる。 これが「視覚アンカー教育法」の長期効果である。 ぜひ本ページを起点に NLP の学習を進めてほしい。 補足として、 学生が「3 枚の図を自分で再現する」演習を必ずカリキュラムに含めることを強く推奨する。 受動的に図を眺めるだけでは「視覚アンカー」は定着せず、 自分で描く経験を経て初めて記憶と結びつく。 SSDSE-B-2026 のような公的データであれば 5 分でコーパスが組め、 Counter と matplotlib があれば 15 分で 3 枚すべて描ける。 ぜひ実演習を組み込んでほしい。 本セクションでは 3 枚の図を通じて「N-gram の長所と短所が何に由来するか」を視覚的に説明した。 図 1 のロングテール、 図 2 の頻度-多様性分離、 図 3 のスムージング差。 この 3 視点を持つと、 教科書の式 P(wi|wi-N+1, ..., wi-1) を見たときに「N が大きいほどヒストグラムの右裾が薄くなる」「Kneser-Ney は散布図の縦軸を直接モデル化する」「Add-1 は箱ひげ全体を下に押し下げる」と即座に脳内で図が浮かぶようになる。 これが「式の意味が分かる」状態である。 後続の Transformer・BERT・GPT の語彙処理を学ぶ際も、 この 3 枚の図を「アンカー」として参照すると、 attention や subword tokenization が「長距離依存性」「未観測語彙」をどう解決しているかが見通せる。 N-gram の習得は NLP 入門の第一歩であり、 同時に Transformer 時代でも色あせない「カウントベースの言語モデル」の原点である。 ぜひ自身のデータで 3 枚の図を再現し、 講義や勉強会の冒頭で「言語モデルとは何か」を語る視覚教材として活用してほしい。 N-gramを使う時は、 nの値、文字単位か単語単位か、前処理、語彙数、低頻度語の扱いを明示します。 nを大きくすると文脈は拾いやすくなりますが、 次元数と疎性も急増します。🔬 数式を言葉で読み解く(その1:マルコフ近似)
🔬 数式を言葉で読み解く(その2:MLE と zero probability)
🔬 数式を言葉で読み解く(その3:add-k smoothing)
📐 文書類似度:文字 N-gram vs 単語 N-gram の比較
単位 語彙サイズ 未知語耐性 類似度精度 推奨用途 文字 trigram 数百〜数千 ◎ 強い △ 中 spam、 言語識別、 typo 耐性 単語 bigram 数万 △ 弱い ◎ 高い 文書分類、 検索 サブワード (BPE) 数千〜数万 ○ ◎ LLM、 翻訳 🐍 SSDSE 県別比較
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1).query('年度 == 2023')
corpus = [f'{n} は人口 {p//10000} 万人 高齢者 {g//10000} 万人'
for n,p,g in zip(df['都道府県'],df['総人口'],df['65歳以上人口'])]
tf_char = TfidfVectorizer(analyzer='char', ngram_range=(3,3)).fit_transform(corpus)
tf_word = TfidfVectorizer(analyzer='word', ngram_range=(1,2)).fit_transform(corpus)
i_tokyo, i_osaka, i_okinawa = 12, 26, 46
print('char3 東京-大阪 :', round(cosine_similarity(tf_char[i_tokyo], tf_char[i_osaka])[0][0], 3))
print('word12 東京-大阪:', round(cosine_similarity(tf_word[i_tokyo], tf_word[i_osaka])[0][0], 3))
print('char3 東京-沖縄 :', round(cosine_similarity(tf_char[i_tokyo], tf_char[i_okinawa])[0][0], 3))
🔀 BPE(Byte-Pair Encoding)と N-gram の関係
アイデア
SSDSE 県名で例
入力 char unigram word bigram BPE (vocab=8000) 東京都 東/京/都 東京/京都/都 東京/都 鹿児島県 鹿/児/島/県 鹿児/児島/島県 鹿児/島/県 那覇市(沖縄) 那/覇/市 那覇/覇市 那/覇/市 📊 N-gram LM の perplexity 比較(N と smoothing の影響)
N smoothing PPL 特性 1 MLE 95.2 unigram、 文脈無視 2 MLE ∞ (zero prob) 未知 bigram で発散 2 Add-1 62.4 最小限の改善 2 Kneser-Ney 28.7 古典の最高 3 Kneser-Ney 22.1 小コーパスで実用上限 — LSTM neural LM 14.5 汎化が圧倒的 — GPT-2 medium 8.3 事前学習を含む 🖼 視覚で読み解く N-gram — 分布・共起・スコアの三層図
図 1: bigram 頻度分布(Zipf 的ロングテール)
図 2: unigram 頻度 vs bigram 頻度の散布図
図 3: スムージング前後の bigram 確率分布(箱ひげ)
🧮 視覚から定量へ — bigram 統計のミニ計算ワーク
bigram 頻度 c(wi-1,wi) 分母 c(wi-1) MLE P(wi|wi-1) Add-1 (V=7) 人口 は 2 2 1.000 0.333 面積 は 2 2 1.000 0.333 は 多い 1 4 0.250 0.182 は 少ない 1 4 0.250 0.182 は 広い 1 4 0.250 0.182 は 狭い 1 4 0.250 0.182 は 高い (未観測) 0 4 0.000 (発散) 0.091 🧪 perplexity の直感計算
📐 数式の視覚的意味 — Markov 仮定の可視化
📖 ケーススタディ — SSDSE-B-2026 を実例に
⚠️ 視覚で誤解しやすい落とし穴
📝 演習問題 3 問(記述)
🧰 実装観点 — bigram モデルを 50 行で書く
🌐 N-gram と現代 NLP — 視覚的に繋がる三層構造
📊 N の選び方 — 視覚的決定基準
📚 学習者向けロードマップ
🎯 まとめ — 視覚で得る 5 つの即時テイクアウェイ
🔍 補足:3 つの図を再現する Python コードのアウトライン
data/raw/SSDSE-B-2026.csv 等で、 文章生成は f-string で「{県名}の人口は約{人口}人で{順位}位である」とテンプレ化すれば 5 分でコーパス完成する。🧠 N-gram と認知科学的観点
🏛 N-gram の歴史と学問的位置づけ
🛠 実務での N-gram 活用シーン
📋 N-gram 評価指標の網羅整理
🔧 N-gram 実装の落とし穴トップ 10
<s> </s> で明示しないと、 文の境界統計が壊れる。 (2) 大文字小文字の正規化:英語コーパスでは lowercase に統一。 日本語でも全角・半角の正規化が必要。 (3) punctuation の扱い:句点をトークンに含めるか別カウントか。 タスクによって決定する。 (4) OOV (Out-Of-Vocabulary) の処理:未知語を `💡 N-gram の隠れた強み — Transformer 時代の活用法
🎓 教育現場での N-gram の位置づけ
🔬 N-gram の数式詳細 — 視覚と式の対応
🎤 講師ノート — 60 分授業での使い方
📦 補足:N-gram の数学的特性まとめ
🔗 視覚から発展学習へ
🔚 視覚パートの締め
🧾 発表前の最終確認
例:「I love NLP. I love coding. I love NLP.」というミニコーパスで バイグラム確率を推定:
| バイグラム | 出現回数 | 条件付き確率 |
|---|---|---|
| I love | 3 | P(love|I) = 3/3 = 1.00 |
| love NLP | 2 | P(NLP|love) = 2/3 ≈ 0.67 |
| love coding | 1 | P(coding|love) = 1/3 ≈ 0.33 |
このモデルでは「I love」のあとに「NLP」が来る確率は 67%。 生成時は「I → love → (NLP or coding) → …」と確率的にサンプリングできます。
⚠️ スムージング:未観測の n-gram は確率 0 になる問題。 Laplace(add-1)、 Kneser-Ney などで対処する。
SSDSE-B-2026 の都道府県列から 47 県名を結合し、 文字 N グラム(N=2, 3)を抽出して頻度を数える。 これは文書類似度・スペル訂正・言語識別の基本処理であり、 N グラムの統計感覚を養うのに最適な題材。
| 文字 | 出現回数 | 確率 P | 含まれる県の例 |
|---|---|---|---|
| 県 | 43 | 0.30 | 青森県・岩手県・…(全 144 文字中) |
| 山 | 6 | 0.042 | 山形・富山・山梨・和歌山・岡山・山口 |
| 島 | 5 | 0.035 | 福島・島根・広島・鹿児島・徳島 |
| 口 | 1 | 0.007 | 山口 |
| 都 | 2 | 0.014 | 東京都・京都府 |
| その他 70 文字 | 各 1〜3 | 各 0.007〜0.02 | … |
「県」 が圧倒的支配的(30%)。 これは「○○県」 の語尾が共通だから。 unigram だけでは「広島」 と「島根」 の区別がつかない(両方とも「島」 を含む)が、 続く N=2, 3 で識別性が上がる。
| 2-gram | 出現回数 | 含まれる県 |
|---|---|---|
| 森県 | 1 | 青森県 |
| 手県 | 1 | 岩手県 |
| 城県 | 2 | 宮城県・茨城県 |
| 島県 | 4 | 福島県・広島県・徳島県・鹿児島県 |
| 山県 | 3 | 富山県・岡山県・和歌山県(注:山梨県の bigram は「山梨」「梨県」で「山県」を含まない) |
| 京都 | 2 | 東京都・京都府 |
| 大阪 | 1 | 大阪府 |
「島県」 が 4 つの県(福島・広島・徳島・鹿児島)で共有されている。 だから「島県」だけでは 4 県を区別できない。 これが「N=2 では識別が粗い」 ことの実例。
| 3-gram | 出現回数 | 該当県 |
|---|---|---|
| 福島県 | 1 | 福島県のみ |
| 広島県 | 1 | 広島県のみ |
| 徳島県 | 1 | 徳島県のみ |
| 東京都 | 1 | 東京都のみ |
| 北海道 | 1 | 北海道のみ |
N=3 で「広島県・福島県・徳島県」 が完全に区別される。 一般則:「N を 1 増やすたびに語彙の組合せ数は語彙サイズ倍に増え、 識別性は上がるが各 N グラムの観測回数は急減する」。 47 県名のような小コーパスでは N=3 が事実上の上限。
語彙サイズ $|V|=75$(重複なし文字数)、 文字「山」 の総出現回数 6、 「山県」 の bigram 出現回数 3 とする。
add-1(Laplace)は強すぎて MLE の 0.5 が 0.049 まで落ちる。 add-0.1 ならバランスが取れる。 実務では k≈0.01〜0.1、 もしくは Kneser-Ney を使う。
add-k や Good-Turing よりも実測 perplexity が低く、 21 世紀初頭まで n-gram LM の golden standard だった Kneser-Ney (KN) smoothing を整理する。 アイデアは「文脈の多様性で確率を再配分する」こと。
$$ P_{\text{KN}}(w_i \mid w_{i-1}) = \frac{\max(c(w_{i-1}, w_i) - d, 0)}{c(w_{i-1})} + \lambda(w_{i-1}) \cdot P_{\text{cont}}(w_i) $$
ここで continuation probability $P_{\text{cont}}(w_i)$ は、 「単語 $w_i$ がいくつの異なる文脈の後に現れたか」を数える。
$$ P_{\text{cont}}(w_i) = \frac{|\{v : c(v, w_i) > 0\}|}{|\{(v', w') : c(v', w') > 0\}|} $$
英語コーパスで San Francisco は頻出だが Francisco はほぼ San の後しか出ない。 単純な unigram backoff だと P(Francisco) が過大評価され、 open Francisco のような変な文も高確率になる。 KN は「Francisco は 1 文脈にしか現れない」を penalty として効かせ、 これを抑える。
同様の現象は日本語でも起きる。 SSDSE-B-2026 を 1 行ずつ文として扱うと:
京都府 の後に 京都市 が頻出 → bigram (京都府, 京都市) が高頻度京都市 単体の出現位置はほぼ 京都府 直後のみ → P_cont(京都市) は小さい東京都 京都市 のような変な並びは KN だと低確率になる(unigram backoff だと高確率になりがち)このコードでやること:SSDSE-B-2026 の県名列を文として KN モデルを学習、 unigram と PPL を比較。
📥 入力:SSDSE-B-2026.csv 都道府県列を 47 文として使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd from nltk.lm import KneserNeyInterpolated, MLE from nltk.lm.preprocessing import padded_everygram_pipeline df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) sents = [list(n) for n in df['Prefecture'].drop_duplicates()] # 47 県名を文字 n-gram 用に train, vocab = padded_everygram_pipeline(2, sents) kn = KneserNeyInterpolated(2) kn.fit(train, vocab) test_sents = [['京','都','府'], ['東','京','都']] for s in test_sents: print(s, 'PPL=', round(kn.perplexity([(s[i],s[i+1]) for i in range(len(s)-1)]), 2)) |
📤 実行結果:
💬 「東京都」「京都府」は両方とも PPL が 1〜2 程度と低く、 47 県の文字列分布から見て自然と判定される。 KN は MLE よりも未知 bigram に対する出力が安定する。
合成文 "the cat sat on the mat" の bi-gram と tri-gram を列挙する。
1 2 3 4 5 6 | tokens = "the cat sat on the mat".split() N = len(tokens) for n in [1, 2, 3, 4]: grams = [tuple(tokens[i:i+n]) for i in range(N-n+1)] print(f"{n}-gram: {len(grams)}") print("bigram:", [(tokens[i], tokens[i+1]) for i in range(N-1)]) |
💬 手計算 (Step 2) と Python 出力が完全一致。
最小コードで動かしてみる例:
1 2 3 4 5 6 7 8 9 | from sklearn.feature_extraction.text import CountVectorizer texts = ['I love NLP', 'I love coding', 'NLP is fun'] # バイグラムまで(n-gram_range=(1,2)) vec = CountVectorizer(ngram_range=(1, 2)) X = vec.fit_transform(texts) print(vec.get_feature_names_out()) # ['coding', 'fun', 'i', 'i love', 'is', 'is fun', 'love', 'love coding', 'love nlp', 'nlp', 'nlp is'] |
SSDSE 公的データを題材に、 特徴量の読み込みからモデル評価までを一気に動かす最小コードです。 paths は引数に直書きで、 初心者がコピペで動かせる形を優先しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import pandas as pd import numpy as np # データ読み込み(SSDSE-B 都道府県・47 県 × 約 112 列) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) df = df[df['年度'] == 2023].reset_index(drop=True) print('shape:', df.shape) print('列の先頭:', df.columns.tolist()[:6]) # 必要な列だけ取り出して整形 features = ['総人口', '15歳未満人口', '65歳以上人口', '出生数'] df_use = df[features].copy() print(df_use.describe()) |
次に、 N-gram に固有の処理を加えます。 ここがページごとの「肝」になる部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score X = df[['総人口', '15歳未満人口', '65歳以上人口']].fillna(0).values y = df['合計特殊出生率'].fillna(df['合計特殊出生率'].median()).values X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=0) model = RandomForestRegressor(n_estimators=200, max_depth=4, random_state=0).fit(X_tr, y_tr) pred_tr = model.predict(X_tr) pred_te = model.predict(X_te) print(f'train R^2 = {r2_score(y_tr, pred_tr):.3f}') print(f'test R^2 = {r2_score(y_te, pred_te):.3f}') print(f'test RMSE = {np.sqrt(mean_squared_error(y_te, pred_te)):.4f}') |
さらに可視化を加えると、 学んだ内容が「眼で」確認できます。
1 2 3 4 5 6 7 8 9 10 11 12 | import matplotlib.pyplot as plt plt.figure(figsize=(7,5)) plt.scatter(y_te, pred_te, alpha=0.7, edgecolor='k') lims = [min(y_te.min(), pred_te.min()), max(y_te.max(), pred_te.max())] plt.plot(lims, lims, 'r--', linewidth=2, label='完全予測ライン') plt.xlabel('実測 出生率') plt.ylabel('予測 出生率') plt.title('N-gram を使ったモデルの予測精度(SSDSE-B-2026)') plt.legend() plt.tight_layout() plt.savefig('out_n-gram.png', dpi=150) |
最後に、 同じ問題を別の角度から見る「クロスバリデーション版」も用意します。
1 2 3 4 5 6 7 8 | from sklearn.model_selection import cross_val_score scores = cross_val_score( RandomForestRegressor(n_estimators=200, max_depth=4, random_state=0), X, y, cv=5, scoring='r2' ) print(f'5-fold CV R^2 = {scores.mean():.3f} (±{scores.std():.3f})') print('各 fold:', np.round(scores, 3)) |
同じ「N-gram」を使うにも、 データの形・規模・目的によって書き方が変わります。 4 つの典型パターンを示します。
1 2 3 4 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) df = df[df['年度'] == 2023].reset_index(drop=True) print(df.shape, df.head(3)) |
1 2 3 4 5 6 7 8 9 10 | from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.linear_model import Ridge pipe = Pipeline([ ('scaler', StandardScaler()), ('model', Ridge(alpha=1.0)), ]) pipe.fit(X_tr, y_tr) print('R^2 =', pipe.score(X_te, y_te)) |
1 2 3 4 5 6 | from sklearn.model_selection import GridSearchCV params = {'model__alpha': [0.01, 0.1, 1.0, 10.0, 100.0]} gs = GridSearchCV(pipe, params, cv=5, scoring='r2', n_jobs=-1) gs.fit(X, y) print('best:', gs.best_params_, 'score:', gs.best_score_) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import matplotlib.pyplot as plt import json pred = gs.predict(X_te) plt.figure(figsize=(7,5)) plt.scatter(y_te, pred, alpha=0.7, edgecolor='k') plt.plot([y_te.min(), y_te.max()], [y_te.min(), y_te.max()], 'r--') plt.xlabel('実測'); plt.ylabel('予測'); plt.title('N-gram 結果') plt.tight_layout(); plt.savefig('result_n-gram.png', dpi=150) with open('result_n-gram.json', 'w', encoding='utf-8') as f: json.dump({'best_params': gs.best_params_, 'cv_score': gs.best_score_, 'test_score': gs.score(X_te, y_te)}, f, ensure_ascii=False, indent=2) |
🎯 このコードでやること:SSDSE-B-2026 の都道府県列から 47 県名を取り出して結合し、 文字 2-gram と 3-gram の頻度上位を Counter で集計する。
📥 入力例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd from collections import Counter df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) prefs = df['Prefecture'].drop_duplicates().tolist() text = ''.join(prefs) def ngrams(s, n): return [s[i:i+n] for i in range(len(s)-n+1)] bigrams = Counter(ngrams(text, 2)) trigrams = Counter(ngrams(text, 3)) print("【2-gram 上位 10】") for g, c in bigrams.most_common(10): print(f" {g}: {c}") print("【3-gram 上位 5】") for g, c in trigrams.most_common(5): print(f" {g}: {c}") |
📤 実行例:
💬 結果の読み方:bigram レベルでは「島県」(4 県)「山県」(3 県)が複数県に共通するので識別力が低い。 trigram はほぼすべて頻度 1 になり、 「広島県」「福島県」のような各県固有の 3-gram が完全に分離できる(上位 5 は同率 1 のため出現順で並ぶ)。 N=2→3 で識別性が劇的に上がる、 という N グラムの核心が観察できた。 なお「県山」「県福」のように 県名の境界をまたぐ N グラムも混ざる点に注意 — 境界に区切り記号を挟めば防げる。
🎯 このコードでやること:47 県名を「文書」 とみなし、 sklearn の CountVectorizer で文字 N グラム特徴量を抽出。 これは文書分類・スペル訂正・類似度計算の基礎処理。
📥 入力例:47 県名のリスト ['北海道', '青森県', '岩手県', ...]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd from sklearn.feature_extraction.text import CountVectorizer df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) prefs = df['Prefecture'].drop_duplicates().tolist() vec = CountVectorizer(analyzer='char', ngram_range=(2, 3)) X = vec.fit_transform(prefs) print(f"行列形状: {X.shape}") # (47, 何種類かの N グラム) print(f"特徴量数: {len(vec.get_feature_names_out())}") print(f"最初の 10 特徴量: {vec.get_feature_names_out()[:10]}") # 各県の非ゼロ特徴量数を表示 import numpy as np nnz = (X > 0).sum(axis=1).A.flatten() for name, n in list(zip(prefs, nnz))[:5]: print(f" {name}: {n} 個の N グラム") |
📤 実行例:
💬 結果の読み方:47 県を 134 次元の N グラムベクトルで表現できた(3 文字の県名なら bigram 2 個 + trigram 1 個 = 3 個の非ゼロ特徴量)。 この行列を使えばコサイン類似度で「広島県」 と「広島市」 の近さを測れる。 BoW (Bag of N-grams) は実装が単純で文書分類のベースラインとして優秀。
🎯 このコードでやること:47 県名コーパスから「○|前文字」 の条件付き確率を MLE と add-k で計算し、 zero probability 問題と平滑化の効果を確認する。
📥 入力例:47 県名を結合した文字列、 語彙=重複なし文字集合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd from collections import Counter df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) prefs = df['Prefecture'].drop_duplicates().tolist() text = ''.join(prefs) uni = Counter(text) bi = Counter([text[i:i+2] for i in range(len(text)-1)]) V = len(uni) def cond_p(w1, w2, k=0.0): return (bi[w1+w2] + k) / (uni[w1] + k*V) for prev, nxt in [('山', '県'), ('山', '口'), ('島', '県'), ('島', '根')]: p_mle = cond_p(prev, nxt, 0) p_add1 = cond_p(prev, nxt, 1) p_add01 = cond_p(prev, nxt, 0.01) print(f"P({nxt}|{prev}): MLE={p_mle:.4f} add-1={p_add1:.4f} add-0.01={p_add01:.4f}") |
📤 実行例:
💬 結果の読み方:add-1 は強すぎて既知 N グラムの確率まで 1/10 に潰してしまう。 add-0.01 ならほぼ MLE と同じだが、 未観測 bigram にも非ゼロを与えられる。 PPL を本気で下げたいときは Kneser-Ney(nltk.lm.KneserNey)を使う。
🎯 このコードでやること:47 県名を 80% 訓練・20% テストに分割し、 add-0.1 smoothing で bigram LM を学習、 テストセットでの perplexity を計算する。
📥 入力例:47 県名(最初 38 件を訓練、 残り 9 件をテスト)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import pandas as pd import numpy as np from collections import Counter df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) prefs = df['Prefecture'].drop_duplicates().tolist() train, test = prefs[:38], prefs[38:] train_text = '|'.join(train) test_text = '|'.join(test) uni = Counter(train_text) bi = Counter([train_text[i:i+2] for i in range(len(train_text)-1)]) V = len(set(train_text + test_text)) k = 0.1 def logp(w1, w2): return np.log((bi[w1+w2] + k) / (uni[w1] + k*V)) logps = [logp(test_text[i], test_text[i+1]) for i in range(len(test_text)-1)] H = -np.mean(logps) ppl = np.exp(H) print(f"V={V} 訓練 {len(train)} 県 テスト {len(test)} 県") print(f"テスト平均負対数尤度 H = {H:.3f}") print(f"Perplexity = {ppl:.2f}") |
📤 実行例:
💬 結果の読み方:PPL=25.65 は「76 文字の語彙のうち平均 26 文字程度に絞れている」 状態。 まったくのランダム(PPL=76)よりは大幅に改善されている。 訓練データを 100 倍に増やせば PPL=10 を切るだろう。 ニューラル LM (GPT-2) なら同じテストで PPL≈5 程度に下がる。 これが N グラム LM の限界と神経 LM の優位性の数値的根拠。
fit は訓練データだけに対して行い、 テストには transform のみを適用。 これを混同するとデータリーケージになる。n-gram は、 統計学と計算機科学の流れの中から生まれました。 下の年表はこの分野全体の流れで、 n-gram 固有の年表ではありません。 この用語がどの時代の産物かを掴むために置いています。
| 時期 | 出来事 | この時代に起きたこと |
|---|---|---|
| 前史 | 統計学・情報理論の基盤整備 | 数式的な土台 |
| 古典期 | 機械学習の黎明(1960〜80 年代) | 「N-gram」の原型が登場 |
| 展開期 | scikit-learn / TensorFlow など実装の普及(2010〜) | 誰でも 1 行で使える時代に |
| 現代 | 大規模モデル時代(2020〜) | 「N-gram」の意味が再解釈される |
現代の文脈では、 古典的な定義のままでは説明しきれない使い方も出てきています。 教科書の定義を出発点としつつ、 実務での「変奏」も知っておくとよいでしょう。
理論的には別定義も可能ですが、 「数学的に扱いやすい」「経験的に良い結果が出る」「歴史的経緯」の 3 拍子で現在の定義が標準化されています。 学術論文では別定義を「変種」として議論することもよくあります。
教育用途・探索的分析では十分。 ただし「統計的有意」を主張するには n=47 は不足することが多いので、 解釈は慎重に。 ブートストラップで信頼区間を出すと頑健性が確かめられます。
PyTorch / TensorFlow / XGBoost / LightGBM など多数。 ただし基本的な動作確認は scikit-learn が一番速いので、 まず sklearn で動かしてから他に移植するのがおすすめ。
計算量・メモリの観点でアルゴリズムを切り替える必要があります。 mini-batch 版、 サブサンプリング、 近似アルゴリズムの利用を検討します。 47 県スケールで本質を理解した後の応用課題です。
古典的な定義は原典(教科書や著名論文)、 実装は使用ライブラリのバージョン情報を併記するのが標準。 「Murphy 2012」「Hastie et al. 2009」あたりが定番引用です。
下の「📚 関連グループ教材」セクションのリストが、 推奨される学習順序の一つです。 上位概念から入って詳細に降りる「トップダウン」と、 1 つの具体例から始めて他に広げる「ボトムアップ」、 どちらも一長一短。 自分の学び方に合わせて。
「N-gram」を中央に置いて、 周辺概念を 5 つの方向に整理します。 これは記憶の足場になります。
| 方向 | 隣接概念 | 関係性 |
|---|---|---|
| 北 (上位) | 自然言語処理 / n-gram 言語モデル | N グラムを特徴量・確率モデルとして使う応用領域 |
| 南 (下位) | unigram / bigram / trigram / 文字 N グラム / サブワード | N の値や分割単位による N グラムの具体的バリエーション |
| 東 (発展) | アテンション / Transformer / BPE / SentencePiece | 長距離依存と語彙爆発を解決する後継手法 (N グラムの限界を超える) |
| 西 (前提) | 条件付き確率 / マルコフ連鎖 / トークン化 | $P(w_t|w_{t-n+1:t-1})$ を支える確率論と前処理 |
| 中央 | N グラム | 連続する N 個のトークン組。 BLEU 評価・スペル訂正・言語識別の基礎 |
マインドマップは「学んだ用語を整理する道具」として優秀。 紙にこの 5 方向を書き、 自分なりの隣接概念を埋めると、 暗黙的にあった理解構造が可視化されます。
「N-gram」を本当に理解できたか、 自分でテストできるクイズです。 答えは展開で確認。
模範回答:上の「💡 30秒結論」を参照。 ポイントは「何のために使うか」を最初に言うこと。 定義や数式から入ると相手が引きます。
模範回答:データは観測値で固定、 パラメータは学習で動かす、 出力は計算結果。 上の「📐 数式の構造をもう一度」を参照。
模範回答:47 都道府県の特徴量を入力にすると、 結果が地理的に解釈しやすくなる、 一方でサンプル数が少ないため信頼区間は広めに出る、 など。
模範回答:上の「🌐 似た概念との比較」表を参照。 1 文で言える違いを持っておくと、 「なぜこっちを選んだか」を説明できます。
模範回答:上の「⚠️ 落とし穴」と「⚠️ さらに 5 つの落とし穴」セクションから、 自分のプロジェクトに最も関連するものを 1 つ選んで言語化してみましょう。
「n-gram」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
N=1 (unigram) は単語頻度、 N=2 (bigram) は共起、 N=3 (trigram) で短いフレーズを捉える。 SSDSE-B-2026 の調査表タイトル列を bigram にすると「人口 動態」「高齢 人口」のような統計用語の固定パターンが上位に来る。
N の選び方は「データ量とタスク」で決まる。
言語非依存・OOV 耐性が必要なときは「単語 N-gram」ではなく「文字 N-gram」を選ぶ。
テキストを書き換え、 N(窓の大きさ)のスライダーを動かすと、 N-gram の抽出・頻度カウント・上位の棒グラフがリアルタイムに更新されます。 黄色い枠の スライディングウィンドウがテキスト上を移動し、「連続する N 要素を切り取る」という N-gram の本質を目で追えます。 トークンの列は 指でドラッグ/マウスでなぞると、 好きな位置に窓を止められます。
N-gram の頻度カウントは、 そのまま N-gram 言語モデル の最尤推定 $P(w_t \mid w_{t-N+1},\dots,w_{t-1}) = C(w_{t-N+1}^{t})/C(w_{t-N+1}^{t-1})$ になります。 未知 gram に確率 0 を与えないために add-k 平滑化や Kneser-Ney 平滑化で確率を配り直すのが定石です。 特徴量としては TF-IDF と組み合わせて文書分類に使われ、 上流には トークン化・形態素解析があります。 長距離依存を捉えたい場合は N を増やす代わりに Transformer ベースの 大規模言語モデル に発展します。 N-gram 由来の類似度は テキスト類似度 の基礎にもなります。
これまでの各節を踏まえ、 N-gram の直感を改めて 1 枚に凝縮します。 N-gram とは「テキストを 連続する N 個の要素(文字 or 単語)の並びとして切り出し、 その頻度を数える」だけの手法です。 窓(ウィンドウ)を 1 要素ずつ右にスライドさせて連続部分列を回収する—— それ以上でも以下でもありません。 この単純さゆえに高速・解釈容易で、 言語モデル・特徴量・類似度の 共通土台になります。
| N | 呼称 | 捉えるもの | ひとことで言うと |
|---|---|---|---|
| 1 | ユニグラム | 要素そのものの出現頻度 | 「語彙の分布」=文脈ゼロ |
| 2 | バイグラム | 隣り合う 2 要素の共起 | 「直前 1 語で次を予測」 |
| 3 | トライグラム | 連続 3 要素の短フレーズ | 「固有名・定型句が見え始める」 |
| 4+ | 高次 N-gram | より長い局所文脈 | 「精度↑・データ不足↑↑」 |
押さえるべき直感は 3 つ。 (1) 局所的な文脈しか見ない——「直前 N−1 要素で次が決まる」という 条件付き確率・マルコフ近似の割り切り。 (2) N を上げると 文脈は伸びるがスパースになるという一方向のトレードオフ。 (3) 言語モデルにも特徴量にも化ける——確率として使えば N-gram 言語モデル、 ベクトル成分として使えば TF-IDF や 文書分類の入力になる。 上の 🎮 触って理解するで N を 1→4 と動かし、 「異なり率」が跳ね上がる様子を体感すると、 この 3 点が一度に腑に落ちます。
💡 覚え方:「N-gram = スライド窓 × 頻度カウント」。 窓幅 N がすべてを決める唯一のツマミで、 大きくすると賢くなるが飢える(データ不足)。 この一文で N-gram の 8 割は語れます。
N-gram は単純ゆえに落とし穴も定型的です。 ページ各所で触れた注意点を、 「なぜ起きる/どう気づく/どう対処」の 3 点セットで一覧化します。 実務ではこの表を「発表前チェックリスト」として使えます。
| 落とし穴 | なぜ起きる | 気づき方 | 対処 |
|---|---|---|---|
| スパース性(データ不足) | N を上げると組合せ数が語彙サイズ倍で増え、 観測が薄くなる | 異なり率(ユニーク数÷総数)が 100% に接近 | N を下げる/コーパスを増やす/平滑化 |
| ゼロ頻度問題 | 未観測 N-gram に MLE が確率 0 を割り当てる | perplexity が ∞ に発散、 log 0 エラー | スムージング(後述)を必ず適用 |
| 次元爆発 | 語彙 V に対し理論上の N-gram 数は $V^N$ | 辞書・疎行列がメモリを圧迫 | 低頻度 N-gram の刈り込み、 ハッシュ化、 サブワード化 |
| 長距離依存を捉えない | 直前 N−1 要素しか見ないマルコフ近似 | 離れた主語・述語の整合が取れない | N-gram で無理せず Transformer/LLM へ |
| 語彙サイズと未知語(OOV) | 訓練語彙外の語が現れる | 推論時に未知トークンが多発 | <UNK> 置換、 文字 N-gram で頑健化 |
| 文脈窓の限界 | 窓外の情報は原理的に無視 | N を上げても頭打ち | N を上げる前に費用対効果を perplexity で確認 |
| 前処理の非対称 | 訓練と推論でトークン化・正規化が食い違う | 同じ語が別 N-gram に分散し頻度が割れる | 正規化・トークン化を訓練/推論で共有 |
| 境界(BOS/EOS)の扱い | 文頭・文末の番兵記号の有無で N-gram 数が変わる | 文境界の確率が不自然 | <s>/</s> の方針を統一 |
上の中でも スパース性とゼロ頻度は N-gram の宿命であり、 スムージング(平滑化)が事実上の必須対処です。 このページの 🧮 数値例で「岡山県」の bigram を計算した通り、 MLE の $P(県|山)=0.500$ は add-1 で $0.049$ まで削られました。 これは「未観測 N-gram を救う代わりに、 既知 N-gram の確率を犠牲にする」というスムージングの本質を端的に示しています(架空の説明例ではなく、 SSDSE-B-2026 の 47 県名文字列から数えた実測に基づく計算)。 k を 0.1 程度に下げるか、 文脈多様性を使う Kneser-Ney を用いると、 この犠牲を最小化できます。
⚠️ 「N を上げれば賢くなる」は罠:短いテキストや小コーパスで N=4,5 にすると、 ほぼ全 N-gram が頻度 1 になり「頻度で差がつかない=統計として無意味」な状態に陥ります。 まず N=2〜3 で平滑化を効かせ、 perplexity が本当に改善する場合のみ N を上げるのが鉄則です。
N-gram の学習後に進む発展トピックを、 「素朴 → 洗練 → 脱 N-gram」の順に地図化します。
京都市)を適切に抑える。 21 世紀初頭まで N-gram LM の到達点。高次 N-gram が未観測なら低次に「退避」するのが バックオフ(Katz backoff 等)、 高次と低次を重み付き平均するのが 補間です。 「trigram が無ければ bigram、 それも無ければ unigram」と段階的に頼ることで、 スパース性を実務的に回避します。
言語モデルとしての N-gram の良し悪しは perplexity(PPL) $= \exp(-\frac{1}{T}\sum_t \log P(w_t\mid \cdot))$ で測ります。 「モデルが次の語をどれだけ言い当てられるか」の指標で、 低いほど良い。 このページの 📐 定義・数式節の PPL 比較表(例示値)が示す通り、 N=2/3+Kneser-Ney が N-gram 系の上限で、 ニューラル言語モデルはさらに半分以下に落とします。
同じ N-gram でも 文字単位は未知語・誤字に強く(言語識別・spam 検出向き)、 単語単位は意味的まとまりを捉えます(文書分類・検索向き)。 どちらも TF-IDF で重み付けすれば、 「ありふれた N-gram を抑え、 特徴的な N-gram を強調する」ベクトルになり、 テキスト類似度や分類の入力として即戦力です。 scikit-learn では TfidfVectorizer(analyzer='char'|'word', ngram_range=(a,b)) の 1 行で切り替えられます(本ページの実測コード参照)。
N-gram の根本限界(長距離依存を捉えない・未観測に弱い)を、 連続ベクトル表現で乗り越えたのが 単語埋め込み→RNN/LSTM→Transformer→大規模言語モデル(LLM)の系譜です。 面白いのは、 Transformer の注意(attention)が実質「N を可変に伸縮する N-gram」のように振る舞う点で、 N-gram の直感は現代モデルの理解にもそのまま生きます。 「カウントベースの原点」として N-gram を押さえておくことが、 NLP全体の見通しを良くします。
N-gram の理解を「前提 → 並列 → 発展」で広げるための、 本用語集内の関連ページへのリンクです。
※ リンクは本用語集内に実在するページのみを張っています。 単独ページの無い概念(Bag of Words、 BPE、 言語モデル一般)は、 上記グループ教材内で扱っています。