61 人が閲覧(直近 30 日) AI

n-gramモデルとは?バイグラムの確率計算・スムージングとPython実装を実行結果付きで解説

n-gramモデルとは?バイグラムの確率計算・スムージングとPython実装を実行結果付きで解説

n-gramモデルとは、文章を「連続するn個の単語(または文字)」の並びに区切り、直前のn-1個から次に来る語の確率を数え上げで求める言語モデルです。n=2ならバイグラム(2-gram)、n=3ならトライグラム(3-gram)と呼びます。「n-gram」という言葉自体は、この連続するn個の並びそのものを指し、言語モデルのほかに全文検索の索引や文書の類似度計算でも使われます。

この記事では、n-gramの意味と種類、バイグラムの確率を手で計算する方法、学習データに無い並びで確率が0になる問題とスムージング、パープレキシティによる評価を順に説明し、NLTK 3.10.3とscikit-learn 1.9.1で実際に動かしたコードと出力を載せます。後半では、検索エンジンやデータベースでのn-gram、LLMが主流になった今もn-gramが使われている場面、2026年に入ってLLMの内部へ組み込まれ始めた「n-gram埋め込み」を整理します。

まとめ:n-gramモデルの要点

  • n-gramは連続するn個の単位の並び。n=1がユニグラム、n=2がバイグラム、n=3がトライグラム。単語単位と文字単位があり、分かち書きの無い日本語では文字n-gramがよく使われる
  • n-gram言語モデルは「次の語は直前のn-1語だけで決まる」と仮定し、学習データでの出現回数の比で確率を求める。バイグラムならP(犬|は)=「は 犬」の回数÷「は」の回数
  • 学習データに一度も出ない並びは確率0になり、文全体の確率も0になる。これを避けるのがスムージングで、実用ではKneser-Neyが定番
  • モデルの良し悪しはパープレキシティ(小さいほど良い)で測る。Pythonではnltk.lmで学習と評価、scikit-learnのCountVectorizerで特徴量化ができる
  • 文章生成の主役はTransformerに移ったが、BLEUによる翻訳評価、学習データ汚染の検出、全文検索、推論高速化(n-gram投機的デコーディング)では今もn-gramが現役
  • 2026年にはDeepSeekのEngramのように、直前2〜3トークンのn-gramで巨大な埋め込み表を引く仕組みがLLMの内部に入った。確率を数えるn-gram言語モデルとは別物で、表を引くための鍵としてn-gramを使う

n-gramの意味:連続するn個の単語・文字の並び

n-gram(エヌグラム)の「n」は並びの長さ、「gram」は区切った単位を表します。「I love programming」を単語のバイグラムにすると「I love」「love programming」の2組、「自然言語処理」を文字のバイグラムにすると「自然」「然言」「言語」「語処」「処理」の5組です。1つずつずらしながら切り出すので、長さLの列からはL-n+1個のn-gramが取れます(Lがn以上の場合。nより短い列からは1つも取れません)。

ユニグラム・バイグラム・トライグラムの違い

nの値ごとに呼び名があり、2-gram・bigram・バイグラムはどれも同じものを指します。

n 呼び名 単語単位の例 文字単位の例
1 ユニグラム(unigram) I / love / programming 自 / 然 / 言
2 バイグラム(bigram) I love / love programming 自然 / 然言 / 言語
3 トライグラム(trigram) I love programming 自然言 / 然言語
4以上 4-gram、5-gram… 数字で呼ぶ 数字で呼ぶ

nを大きくすると並びが長くなり文脈を多く拾えますが、同じ並びが繰り返し現れる回数は急に減ります。語彙がV語なら、あり得るn-gramの種類はVのn乗です。語彙1万語のトライグラムなら1兆通りで、学習データのほとんどの組み合わせは一度も現れません。この「データの疎らさ」が、後で説明するスムージングが必要になる理由です。

単語n-gramと文字n-gram:日本語での使い分け

英語は空白で単語が区切られているので、単語n-gramをそのまま作れます。日本語は空白が無いため、単語n-gramを作るにはまず形態素解析で単語に分ける必要があります。文字n-gramなら辞書が要らず、辞書に無い新語や固有名詞も同じ規則で分割できます。ただし索引の単位より短い検索語は一致しない製品があります(SQLiteのFTS5のtrigramトークナイザーは3文字未満の語に全文検索で一致しません)。また、「京都」を検索して「東京都」が当たるような、単語の境界をまたいだ一致が起きます。

実務では、言語モデルや特徴量で意味のまとまりを重視するならMeCabなどで分かち書きしてから単語n-gram、辞書の保守をしたくない検索や表記ゆれの吸収なら文字バイグラムかトライグラム、という分け方が基本です。LLMが使うBPEなどのサブワード分割は、この中間にあたる考え方です(詳しくはトークナイザーの解説)。

n-gram言語モデルの仕組み:直前のn-1語に基づく次の語の予測

言語モデルは、文の出やすさを確率で表すモデルです。文「私 は 犬 が 好き」の確率は、連鎖律で「最初の語の確率×1語目を見たときの2語目の確率×…」と分解できます。ただし文が長くなるほど、条件にする過去の語の並びは学習データにまず現れません。

マルコフ仮定:バイグラムは1次マルコフモデル

そこでn-gramモデルは「次の語は直前のn-1語だけで決まる」と割り切ります。これをマルコフ連鎖と同じ考え方でマルコフ仮定と呼びます。バイグラムなら条件は直前の1語だけで、「今の語」を状態、「次の語」への移りやすさを遷移確率とみなした1次マルコフモデルそのものです。トライグラムは、単語を状態とすれば直前2語に依存する2次マルコフモデルです。直前2語の組を1つの状態とすれば、1次マルコフモデルとして表せます。

P(私 は 犬 が 好き) ≒ P(私|<s>)×P(は|私)×P(犬|は)×P(が|犬)×P(好き|が)×P(</s>|好き)

<s>と</s>は文頭と文末を表す記号です。文頭記号を置くと「文の最初に来やすい語」を、文末記号を置くと「文が終わりやすい位置」を学習できます。

次の節で使う3文のコーパス(「私 は 犬 が 好き」「私 は 猫 が 好き」「彼 は 犬 が 嫌い」)からバイグラムを数えると、マルコフ連鎖の遷移表は次のようになります。各行の確率を足すと1になり、確率0の遷移(たとえば文頭から「猫」)は表に現れません。

今の状態(直前の語) 次の語:確率
<s>(文頭) 私:2/3 / 彼:1/3
私・彼 は:1
は 犬:2/3 / 猫:1/3
犬・猫 が:1
が 好き:2/3 / 嫌い:1/3
好き・嫌い </s>(文末):1

状態そのものが見えず、観測される単語から状態の並びを推定するのが隠れマルコフモデル(HMM)です。品詞タグ付けでは、品詞の並びのバイグラムを遷移確率に、品詞ごとの単語の出やすさを出力確率にして、文の品詞列を推定します。単語のバイグラムで文の確率を出すn-gram言語モデルとは、同じマルコフ仮定を何の並びに当てるかが違います。

この発想自体は古く、Claude Shannonは1948年の論文「A Mathematical Theory of Communication」で、直前の1文字に応じて次の文字を選ぶ「digram」、直前の2文字に応じる「trigram」の頻度で英語を近似し、単語でも遷移確率だけを正しくした「Second-order word approximation」の例文を載せています。今日のバイグラム・トライグラムと同じ考え方です。

最尤推定:出現回数の比による確率計算

各条件付き確率は、学習データでの出現回数から求めます(最尤推定)。バイグラムなら次の式です。

P(w | 直前の語) = 「直前の語 w」の出現回数 ÷ 「直前の語」の出現回数

3文だけの小さなコーパスで、標準ライブラリだけで計算してみます。

from collections import Counter
import math

corpus = [
    "私 は 犬 が 好き",
    "私 は 猫 が 好き",
    "彼 は 犬 が 嫌い",
]
sents = [["<s>"] + s.split() + ["</s>"] for s in corpus]
unigram = Counter(w for s in sents for w in s[:-1])      # 文脈側の出現回数
bigram = Counter((s[i], s[i + 1]) for s in sents for i in range(len(s) - 1))
V = len({w for s in sents for w in s}) - 1                # <s> を除いた語彙数

def p_mle(prev, w):
    return bigram[(prev, w)] / unigram[prev]

def p_laplace(prev, w):
    return (bigram[(prev, w)] + 1) / (unigram[prev] + V)

def score(sentence, p):
    toks = ["<s>"] + sentence.split() + ["</s>"]
    logp = 0.0
    for prev, w in zip(toks, toks[1:]):
        pr = p(prev, w)
        if pr == 0:
            return 0.0, float("inf")
        logp += math.log2(pr)
    n = len(toks) - 1
    return 2 ** logp, 2 ** (-logp / n)

print("P(犬|は) =", p_mle("は", "犬"))
print("P(が|犬) =", p_mle("犬", "が"))
for s in ["私 は 犬 が 好き", "猫 は 犬 が 好き"]:
    print(s, "MLE", score(s, p_mle), "Laplace", score(s, p_laplace))

実行結果(Python 3.13.14で確認)は次のとおりです。

P(犬|は) = 0.6666666666666666
P(が|犬) = 1.0
私 は 犬 が 好き MLE (0.2962962962962962, 1.2247448713915892) Laplace (0.0003169609316303532, 3.829708431025352)
猫 は 犬 が 好き MLE (0.0, inf) Laplace (3.873966942148761e-05, 5.436349233945088)

「は」は3回出現し、そのうち2回が「は 犬」なので、P(犬|は)=2÷3=0.667です。「私 は 犬 が 好き」は学習データそのものなので確率が0.296と高く出ます。一方「猫 は 犬 が 好き」は、どの語も学習データにあるのに確率が0になりました。「<s> 猫」(文が「猫」で始まる)というバイグラムが一度も出てこないためです。

ゼロ頻度問題とスムージング

最尤推定のままでは、学習データに無い並びを1つでも含む文は確率0になり、比較も評価もできなくなります。これをゼロ頻度問題と呼びます。対策は、観測済みの並びから確率を少し削り、未観測の並びに配るスムージングです。

ラプラス平滑化の計算と過剰な確率配分

最も単純なのは、すべてのバイグラムの回数に1を足す方法です。上のコードのp_laplaceが該当し、分母には語彙数Vを足します。「猫 は 犬 が 好き」の確率は0から約0.0000387になりました。

ただし、学習データにある文の確率も0.296から0.0003まで落ちています。語彙数万の実データでは、この削り方が大きすぎて観測済みの並びを過小評価します。仕組みを学ぶ教材には適していますが、大語彙の言語モデルでは、Kneser-Neyなどとテストデータ上の性能を比較して採否を決めます。

バックオフ・補間の違いとKneser-Neyの確率配分

実用で使われるのは、長いn-gramが見つからないときに短いn-gramの確率を使う方法です。

  • バックオフ:トライグラムが0回ならバイグラムへ、それも0回ならユニグラムへと順に下がる
  • 補間:トライグラム・バイグラム・ユニグラムの確率を重み付きで常に混ぜる
  • Kneser-Ney:短いn-gramに下がるとき、単純な出現回数ではなく「何種類の文脈の後ろに現れたか」で確率を決める。「Francisco」は回数は多くても「San」の後ろにしか現れないので、未知の文脈では低く見積もる

Kneser-Neyは1995年にReinhard KneserとHermann Neyが発表した手法です。Stanley ChenとJoshua Goodmanは1998年の技術報告(Harvard TR-10-98)で主要なスムージング手法を同じ条件で比較し、Kneser-Neyを改良した変種(modified Kneser-Ney)が評価したすべての手法を一貫して上回ったと報告しています。NLTKには補間型のKneserNeyInterpolatedが用意されています。

一方で、データ量が多ければ単純な方法でも差が縮まります。GoogleのBrantsらは2007年の論文で、最大2兆トークンまでのデータで5-gramモデルを学習し(最大のモデルは約3,000億のn-gram)、正規化した確率を出さない簡易な方式「Stupid Backoff」を提案しました。学習が安く済み、データ量が増えるにつれてKneser-Neyの品質に近づくと報告しています。

パープレキシティによる言語モデルの評価

言語モデルの評価には、テスト文に対するパープレキシティ(perplexity)を使います。1語あたりの確率の逆数の幾何平均で、「次の語を平均して何択まで絞り込めているか」と読めます。値が小さいほど良いモデルです。上の実行結果では、学習データそのものの文が1.22、ラプラス平滑化後の未知の並びを含む文が5.44でした。最尤推定のままでは未知の並びを含む文のパープレキシティは無限大(inf)になり、比較できません。

パープレキシティを直接比べるときは、テストデータ・語彙・トークン分割・未知語や文末記号の扱いをそろえます。語彙や文末記号の扱いが違うと値が変わるためで、後述のNLTKの結果が手計算と一致しないのもこの理由です。

Pythonでのn-gram実装:標準ライブラリ・NLTK・scikit-learn

目的によって使うライブラリが変わります。n-gramを切り出すだけなら標準ライブラリ、言語モデルとして確率やパープレキシティを出すならNLTK、機械学習の特徴量にするならscikit-learnです。以下はいずれもPython 3.13.14、NLTK 3.10.3、scikit-learn 1.9.1で実行した結果です(検証日:2026年10月14日。NLTKとscikit-learnはこの日時点のPyPI最新版で、NLTK 3.10.3はPython 3.10以上、scikit-learn 1.9.1はPython 3.11以上が必要)。

Python標準機能によるn-gramの抽出

リストや文字列を1つずつずらしてスライスするだけで作れます。文字列を渡すと文字n-gramになります。

def ngrams(seq, n):
    return [tuple(seq[i:i + n]) for i in range(len(seq) - n + 1)]

words = "I love programming and I love learning".split()
print(ngrams(words, 2))
print(ngrams("自然言語処理", 2))
print(ngrams("自然言語処理", 3))
[('I', 'love'), ('love', 'programming'), ('programming', 'and'), ('and', 'I'), ('I', 'love'), ('love', 'learning')]
[('自', '然'), ('然', '言'), ('言', '語'), ('語', '処'), ('処', '理')]
[('自', '然', '言'), ('然', '言', '語'), ('言', '語', '処'), ('語', '処', '理')]

英文の例で「I love」が2回出ているように、n-gramは重複を含むリストとして返ります。頻度を数えるならcollections.Counterにそのまま渡します。

NLTKのnltk.lmによる言語モデルの学習

NLTKにはnltk.lmモジュールがあり、文頭・文末記号の付与から学習、確率とパープレキシティの計算までまとめて扱えます。先ほどと同じコーパスで、最尤推定(MLE)・ラプラス・Kneser-Ney補間を比べます。

from nltk.lm import MLE, Laplace, KneserNeyInterpolated
from nltk.lm.preprocessing import padded_everygram_pipeline, pad_both_ends
from nltk.util import bigrams

train = [s.split() for s in [
    "私 は 犬 が 好き",
    "私 は 猫 が 好き",
    "彼 は 犬 が 嫌い",
]]
test = "猫 は 犬 が 好き".split()
test_bigrams = list(bigrams(pad_both_ends(test, n=2)))

for cls in (MLE, Laplace, KneserNeyInterpolated):
    data, vocab = padded_everygram_pipeline(2, train)
    lm = cls(2)
    lm.fit(data, vocab)
    print(cls.__name__, round(lm.score("犬", ["は"]), 4), lm.perplexity(test_bigrams))

data, vocab = padded_everygram_pipeline(2, train)
lm = MLE(2); lm.fit(data, vocab)
print(lm.generate(5, text_seed=["<s>"], random_seed=3))
MLE 0.6667 inf
Laplace 0.2143 6.399722946466974
KneserNeyInterpolated 0.6389 5.536772120188106
['彼', 'は', '犬', 'が', '好き']

各行は「モデル名、P(犬|は)、テスト文のパープレキシティ」です。MLEは手計算と同じ0.6667で、未知の「<s> 猫」を含むテスト文はinfになります。ラプラスは確率を大きく削るのでP(犬|は)が0.2143まで下がり、Kneser-Neyは0.6389と観測値に近い値を保ったまま、テスト文のパープレキシティを有限の値にしています。

手計算のラプラス(5.44)とNLTKのラプラス(6.40)がずれるのは、NLTKの語彙に<s>・</s>・<UNK>(未知語)が含まれ、語彙数が11になるためです。generateは学習した確率に従って語をサンプリングし、学習データに無い「彼 は 犬 が 好き」を作っています。数語の並びは自然でも、長い文になると前後がつながらないのがn-gramの限界です。

NLTKのnltk.lmは学習用・検証用の実装で、数億語規模のコーパスには向きません。大規模データでn-gram言語モデルを作るなら、KenLMのような専用ツールを使います。

scikit-learnによる文字n-gram特徴量と類似度の計算

文書分類や類似文書検索では、n-gramの出現回数をベクトルにして使います。CountVectorizerのanalyzer="char"とngram_rangeで、日本語でも形態素解析なしに文字バイグラムのベクトルが作れます。

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity

docs = ["機械学習の入門書", "機械学習入門", "深層学習の入門書"]
vec = CountVectorizer(analyzer="char", ngram_range=(2, 2))
X = vec.fit_transform(docs)
print(vec.get_feature_names_out())
print(cosine_similarity(X).round(3))

def jaccard(a, b, n=2):
    A = {a[i:i + n] for i in range(len(a) - n + 1)}
    B = {b[i:i + n] for i in range(len(b) - n + 1)}
    if not (A | B):
        raise ValueError(f"どちらの文字列も{n}文字未満です")
    return len(A & B) / len(A | B)
print(round(jaccard(docs[0], docs[1]), 3), round(jaccard(docs[0], docs[2]), 3))

en = CountVectorizer(ngram_range=(1, 2))
en.fit(["new york is big", "york is old"])
print(en.get_feature_names_out())
['の入' '入門' '学習' '層学' '械学' '機械' '深層' '習の' '習入' '門書']
[[1.    0.676 0.714]
 [0.676 1.    0.338]
 [0.714 0.338 1.   ]]
0.5 0.556
['big' 'is' 'is big' 'is old' 'new' 'new york' 'old' 'york' 'york is']

「機械学習の入門書」との類似度は、コサイン類似度では「深層学習の入門書」(0.714)が「機械学習入門」(0.676)より高く、ジャカード係数でも同じ順(0.556と0.5)になりました。文字n-gramは「どの文字の並びを共有しているか」しか見ないので、「機械学習」と「深層学習」の意味の違いは反映されません。意味の近さを測りたいなら、TF-IDFの重み付けだけでは足りず、埋め込みベクトルに切り替えます。TF-IDFは語の出現頻度に重みを付ける処理で、意味を学習するものではありません。計算式の詳細はコサイン類似度の解説にまとめています。

英語で単語の組み合わせを拾いたいときはngram_range=(1, 2)とし、「new york」のような2語の表現を1つの特徴として扱います。範囲を広げるほど特徴量の次元が増えるので、min_dfで、出現する文書数(または文書の割合)が少ないn-gramを落とすのが定石です。

検索エンジン・データベースでのn-gram

全文検索では、文書を文字n-gramに分けて索引を作ると、辞書が無くても部分一致の検索ができます。主要なソフトウェアは次のようにn-gramの仕組みを持っています。

ソフトウェア 仕組み 既定のn
Elasticsearch ngram tokenizer min_gram 1・max_gram 2
MySQL ngram全文パーサー 2(ngram_token_size・1〜10)
PostgreSQL pg_trgm拡張 3(トライグラム固定)
SQLite FTS5のtrigramトークナイザー 3(トライグラム固定)

Elasticsearchのドキュメントは、min_gramとmax_gramを同じ値にするのが通常は妥当で、トライグラムから始めるのがよいとしています。短いほど多くの文書が当たり、一致の質は下がるためです。min_gramとmax_gramの差は索引設定index.max_ngram_diffで制限され、既定値は1です。min_gram 2・max_gram 4のように幅を持たせるなら、この設定を先に広げます。日本語で形態素解析(kuromoji)とn-gramをどう併用するかはElasticsearchの日本語全文検索の解説で扱っています。

PostgreSQLのpg_trgmは、文字列を単語ごとに区切り、各単語の前に空白2つ・後ろに空白1つを補ってからトライグラムに分けます。PostgreSQL 18.6でshow_trgm('word')を実行すると「” w”」「” wo”」「ord」「”rd “」「wor」の5つが返り、similarity('programming', 'programing')は0.7692でした。%演算子で「似ている」と判定する閾値pg_trgm.similarity_thresholdの既定値は0.3です。LIKE検索を速くするには、pg_trgmの演算子クラスでGINインデックスを張ります(GINインデックスの解説)。SQLiteでの日本語検索はFTS5の解説を参照してください。

文字n-gramの索引は、形態素解析の索引より大きくなり、先ほどの「京都」と「東京都」のような境界をまたいだ誤一致も出ます。検索結果の並び順まで気にするなら、形態素解析の索引と併用して、n-gram側は取りこぼし防止に使う構成が一般的です。

Ngram Viewer:語の使用頻度の推移を調べるツール

「ngram」で検索すると、言語モデルではなく、書籍の中で語がどれだけ使われてきたかをグラフにするツールが上位に出ます。仕組みは同じで、大量の書籍をn-gramに分けて年ごとの出現頻度を集計しています。

  • Google Books Ngram Viewer:Googleがスキャンした書籍を対象に、英語・中国語(簡体字)・フランス語・ドイツ語・ヘブライ語・イタリア語・ロシア語・スペイン語の語句の出現頻度の推移を表示します。日本語のコーパスはありません。2024年7月に新しいデータセットが追加され、検索語は最大7語(それまでは5語)まで入れられるようになりました。
  • NDL Ngram Viewer:国立国会図書館が2022年5月31日に公開したサービスで、国立国会図書館デジタルコレクションのOCRテキストから、出版年代ごとの語句の出現頻度を表示します。2023年1月時点の対象は図書約97万点・雑誌約132万点から集計した約17億種類のキーワードです。日本語の語の推移を調べるならこちらを使います。集計データはGitHub(ndl-lab/ndlngramdata)で公開されています。

どちらも言語モデルを作るツールではなく、語の流行や表記の移り変わりを見るための集計ツールです。書籍のOCR誤りや収録冊数の年ごとの偏りがそのまま出るので、ある年の急な増減は元データの偏りを疑ってから解釈します。

LLM時代のn-gramの使いどころと選ばない場面

文章を生成する言語モデルとしては、n-gramはTransformerベースのLLMに置き換わりました。直前のn-1語しか見られないn-gramは、段落をまたいだ話の流れを扱えないためです。それでも、次の用途では今もn-gramが使われています。

評価指標と学習データ汚染の検出

機械翻訳の自動評価指標BLEU(Papineniら、2002年)は、1-gramから4-gramまでの修正n-gram適合率(参照訳に出る回数を上限に数えた一致率)の幾何平均に、訳文が短すぎる場合の短文ペナルティを掛けたスコアです(論文のベースラインはN=4・均等な重み)。LLM評価で参照テキストとの一致度を測るときも、BLEUやROUGEのようなn-gram一致の指標がまず使われます。ただし言い換えを正しく評価できないので、要約や対話ではLLMに採点させる方法と組み合わせます。

GPT-3の論文(Brownら、2020年)は、ベンチマークと学習データの重なりを13-gramの一致で探し、一致した13-gramと周囲200文字の範囲を学習データから取り除こうとしました。ただしフィルタの不具合で除去は一部にとどまり、再学習もできなかったと論文自身が書いています。そのため事後の分析では、データセットごとに語数Nを変えたn-gram(非合成タスクでは最小8、全タスクで最大13)で重なりを判定し、重なりのある問題を除いた「クリーン版」でも評価しています。ベンチマークの問題文が学習データに含まれていないかを確かめる作業は、今もn-gramの一致で行うのが基本です。

推論の高速化と巨大コーパスの検索

LLMの推論を速くする投機的デコーディングにも、n-gram方式があります。推論エンジンvLLMのドキュメントは、n-gram方式とsuffix decodingを「ピーク時の負荷を増やさずに控えめな高速化をもたらす単純な方式」と位置づけ、最も大きな遅延削減はEAGLEやMTPなどモデルを使う方式が担うとしています。n-gram方式はプロンプト中に既に出てきた並びを探し、続きの候補をそのまま提案する仕組みで、コード修正や文書の要約のように入力の一部を出力に写す作業で効きます。

n-gram言語モデルそのものを大規模化する研究も続いています。infini-gram(Liuら、arXiv:2401.17377、2024年1月初版)は、ニューラルLLMと同じ5兆トークンで作られた、論文によれば過去最大のn-gram言語モデルです。出現回数の表を事前に作る代わりにサフィックス配列で都度数えることで、nに上限を設けない「∞-gram」の確率をミリ秒単位で計算します。論文では、次のトークンの予測精度が47%に達し、ニューラルLLMと組み合わせるとパープレキシティを大きく下げられたと報告しています。

LLM内部のn-gram埋め込み:Engramと採用モデル

「n-gram embedding」は、n-gramにベクトル表現を対応付けるものです。ここでは、LLMの層内でトークンのn-gramを鍵に埋め込み表を引く方式を扱います。DeepSeek-AIと北京大学が2026年1月に公開した論文「Conditional Memory via Scalable Lookup」(実装はGitHubのdeepseek-ai/Engram)は、これをEngramと名付け、「古典的なN-gram埋め込みをO(1)で引けるよう現代化したモジュール」と説明しています。仕組みは次の2段階です。

  • 検索:各位置で直前のトークン列の末尾(2-gramと3-gram)をハッシュに通し、巨大な静的埋め込み表から行を取り出す。衝突を減らすため、nごとに複数のハッシュ関数(ハッシュヘッド)を使う。表記ゆれをまとめる前処理で、128kトークナイザーの実質的な語彙を23%減らしている
  • 融合:取り出した行を、アテンションで文脈を集めた隠れ状態と照らし合わせ、合わない行はゲートで抑えてから足し込む

確率を数える従来のn-gram言語モデルとの違いは、n-gramを「次の語の予測」ではなく「知識を引く住所」に使う点です。表の位置はトークン列だけで決まるので、計算の前に引く行が分かり、表をGPUではなくCPU側のメモリに置けます。論文は、1,000億パラメータの表をすべてホストメモリに置いた実験(NVIDIA H800・512系列・系列長100〜1,024)で、推論スループットの低下が最大2.8%(8Bモデル)だったと報告しています。また、Mixture-of-Experts(MoE)に割り当てる疎なパラメータの約20〜25%をEngramに回すと検証損失が最も下がり、同じパラメータ数・計算量のMoEと比べたEngram-27BはMMLUで+3.4、BBHで+5.0、HumanEvalで+3.0の改善だったとしています。4-gramまで広げても、表の容量が一定なら2-gram・3-gramに回したほうがわずかに良かったという結果も載っています。

この仕組みは研究段階を出ています。Qwenが2026年8月に公開したQwen3.8-Flash-Nextのモデルカードは、パラメータ構成を「125B(活性6B)に加えて51BのN-gram埋め込みと4BのMTP」とし、第2層でバイグラムとトライグラムを引く2,000万行の表を持つと書いています。推論エンジンのvLLMのドキュメントは、DeepSeek-V4.1-Flash(Engram層2つ、各約3億8,400万行で表だけで約200GB)とQwen系のこの方式に対応し、表は既定でCPU側のピン留めメモリに置くと説明しています。表の大きさやnはチェックポイントで決まっていて、利用側が変えられるのは置き場所と分割の仕方だけです。また、ここで参照したvLLMのEngram実装は、CUDA互換プラットフォームを対象としています。こうしたモデルを自前で動かすときは、GPUメモリだけでなくホスト側のメモリ容量を先に見積もる必要があります。

n-gramを選ばない場面

判断の基準は「意味を理解する必要があるかどうか」です。次の場面ではn-gramを主役にしません。

  • 意味が同じで表現が違う文を同じと判定したい:「解約したい」と「退会の手続き」は共有する文字n-gramが少なく、類似度が低く出る。埋め込みベクトルで比べる。問い合わせ対応のように言い換えを吸収したい用途は、埋め込み検索とLLMを組み合わせるAIチャットボット開発の領域になる
  • 長い文脈に依存する予測:チャットの応答生成や文書要約。n-gramは直前数語しか見ない
  • 学習データが少ない分類:nを上げると特徴量が疎になり、過学習しやすい。低次のn-gram(ユニグラム・バイグラム)を基準線にして、事前学習済みモデルと同じ評価データで比べてから選ぶ

逆に、完全一致・部分一致を保証したい検索、再現性が要る評価、GPUを使えない環境での軽量な判定では、n-gramを選ぶ理由がはっきりあります。

n-gramモデルに関するよくある質問

n-gramの読み方と「n」の意味は?

エヌグラムと読みます。「n」は並びに含める単語や文字の個数です。n=2をバイグラム(2-gram)、n=3をトライグラム(3-gram)と呼びます。

n-gramと形態素解析の違いは?

形態素解析は辞書と文法の情報を使って文を意味のある単語に分ける処理で、n-gramは辞書を使わず機械的にn文字(またはn語)ずつ切り出す処理です。形態素解析の結果(単語列)からさらに単語n-gramを作ることもでき、両者は対立するものではありません。

2-gramとbigramに違いはある?

違いはありません。2-gram、bigram、バイグラム、bi-gramはいずれも連続する2つの単位の並びを指します。

nはいくつにすればよい?

言語モデルではトライグラム(n=3)から5-gramまでがよく使われます。学習データが少ないのにnを大きくすると、ほとんどの並びが0回になりスムージング頼みになります。全文検索の文字n-gramでは、MySQLのngramパーサーの既定値が2、Elasticsearchのドキュメントはトライグラムから始めることを勧めています。SQLiteのFTS5の標準trigramトークナイザーはnを変えられず、3文字未満の語は全文検索で一致しません。2文字の語で検索されることが多いなら、MySQLのngramパーサーの既定(2)やElasticsearchのngram tokenizer(min_gram・max_gramを2に設定)のように、バイグラムを選べる製品を使います。

n-gram言語モデルとLLMの違いは?

どちらも「次の語の確率」を出す言語モデルです。n-gram言語モデルは、直前のn-1語に次の語を加えた並びの出現回数を、その文脈の出現回数で割って確率を推定し、必要に応じてスムージングをかけます。LLMはニューラルネットワークで文脈全体から確率を計算します。n-gramは学習が速く結果の理由を説明しやすい一方、長い文脈や言い換えを扱えません。

関連記事

お気に入りに入れた記事の一覧

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.23 テックブログ Renta!不正アクセスでサービス停止:再開未定の現状と運営側の停止判断・証拠保全・段階的再開
  2. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  3. 2026.10.24 テックブログ カプコン不正アクセス:CAPCOM ID 1,177万件漏えいと196件の乗っ取りへの対処
  4. 2026.10.24 テックブログ ブラストメール不正アクセス:配信先288件の漏えいと利用企業の報告・通知の手順
  5. 2026.10.14 テックブログ ローソンID不正アクセス:215万件漏えいの対象確認と本物メールの見分け方

RELATED POSTS 関連記事

目次