ユニグラム言語モデル
最も単純な言語モデル
自然言語処理では、解析対象の文書を何らかの形で確率モデル上の現象として扱う。 そのようなモデルのうち、文書に対してそれが生成される確率を与えるモデルを 言語モデル と呼び、他の様々なモデルの基礎となっている。
ユニグラム (unigram) 言語モデル は言語モデルの一つであり、 全ての単語が独立に出現するという強い仮定の下で導出される、最も単純なモデルである。
今、 個の単語の列 が得られているとしよう。 これは 1 文でもよいし、1 文書でもよいし、クローリングで得られた全ての文書を結合したものと考えてもよい。 番目の単語 は 語彙 (vocabulary) に含まれる記号である。 例えば英語であれば、 などと表すことができる。
ここで、 から 個の単語を復元抽出した結果 が得られたと考えよう。 このとき、 の各単語はどのような確率で出現すると言えるだろうか。
は求めたい確率分布に関して我々が持っている唯一の証拠なので、 あらゆる確率分布の中から に対する値が最大になるものを選ぶことにしよう。 は復元抽出で得られたので、単語 の確率を と書くことにすると、
と書けることが分かる。 ここで は全ての の集合であり、確率分布の挙動に影響を与える パラメータ (parameter) である。 は同じ単語を複数回含む可能性があるので、 が に出現する回数を と書くことにすると、これは更に
と纏めることができる。 この が最大になる を求めればよいことが分かる。 このような方針のことを一般に 最尤推定 (maximum likelihood estimation) と呼び、 その対象となる確率分布関数(ここでは )を 尤度関数 (likelihood function) と呼ぶ。
上記の確率そのままでは累乗が厄介なので、対数を取って式を簡単な形に変形してしまおう。
対数は狭義単調増加関数なので、対数を取る前後で式の値が最大になる は変化しない。 このため、式 の最大化と式 の最大化は同じ問題を解いていることになる。 ところで は確率なので、その総和が 1 になるという確率の公理を満たさなければならない。 結局、解くべき問題は次の形となる。
これは未定乗数法で解くことができ、次の解が得られる。
つまり、 中の の出現頻度そのものが確率の推定値となる。 最初に復元抽出を仮定したので、各単語の確率は周辺の他の単語に依存しない形で定式化された。 このようなモデルをユニグラム言語モデル、または 1 グラム (1-gram) 言語モデル と呼ぶ。 一般に周辺 単語に依存する言語モデルを n グラム (n-gram) 言語モデル と呼ぶが、 ユニグラム言語モデルはその特殊形の一つである。
ユニグラム言語モデルは非常に強い独立性の仮定を置いているため、 言語モデルとして利用しようとすると性能面で問題が多い。 一方で統計モデルとしての解析は非常に容易であり、様々な派生モデルの基礎として利用されている。 あるモデルが単語の頻度を利用しているなら、 何らかの形でユニグラムモデルの特徴を借用していると考えてもよいだろう。
ユニグラム言語モデルの学習
実際にユニグラムモデルを学習データから獲得する場合、 そのものから毎回確率を求めるのではなく、 単語ごとの確率 もしくは単語の出現回数 だけを記録しておけば十分であることが分かる。 このような、元のデータに代わって確率分布を十分に説明する統計量のことを 十分統計量 (sufficient statistics) と呼ぶ。
与えられた単語のリストからユニグラムモデルを学習するコードを次に示す。
このコードではモデル本体を str
から float
への辞書で表現しており、
辞書のキーに 、値に を保存している。
統計モデルの計算では確率の乗算が頻出するが、
元の確率値や統計量のまま扱うよりも、これらの対数値を保存しておく方がしばしば便利である。
from collections import Counter
from dataclasses import dataclass
import math
@dataclass(frozen=True)
class UnigramModel:
# Unigram language model.
total: float # log(Total number of words)
unigram: dict[str, float] # log(Unigram occurrence)
@staticmethod
def train(corpus: list[str]) -> "UnigramModel":
# Trains a unigram language model.
total = math.log(len(corpus))
unigram = {k: math.log(v) for k, v in Counter(corpus).items()}
return UnigramModel(total=total, unigram=unigram)
def logprob(self, word: str) -> float:
# Calculates log(P(w)).
return self.unigram.get(word, float("-inf")) - self.total