Kneser-Ney Smoothing は高性能な言語モデルである。と、よく聞かされて知っているつもりだけど、まだ一度も試したことがなかったので、試してみた。
コードはここ。
実験用にべったり書いているのでコピペは多いし、速度やメモリの効率も悪いが、まあ気にしないで。
コーパスは最初手元の適当なニュースコーパスを使っていたんだけど、それだと再現検証できないので、nltk のコーパスを使うように変更した。
nltk.corpus の下にあるコーパスモジュール名を -c オプションで与えると、そのコーパスを使って additive smoothing と Kneser-Ney smoothing の perplexity を出してくれる。
デフォルトでは Brown コーパスを使うのだが、例えばロイターコーパスを使いたければこんな感じ。
$ ./knsmooth.py -c reuters
ちなみに、与えられた名前に対して対応するコーパスモジュールを動的に読み込むところはこんなコード。
m = __import__('nltk.corpus', globals(), locals(), [opt.corpus], -1) corpus = getattr(m, opt.corpus)
これで opt.corpus に "brown" が入っていれば corpus は nltk.corpus.brown になるし、"movie_reviews" なら nltk.corpus.movie_reviews になるって仕掛け。
出力はこんな感じ。brown コーパスの場合。
$ ./knsmooth.py -c brown --seed=0 found corpus : brown (D=500) # of terms=1045022, vocabulary size=47349 UNIGRAM: additive smoother: alpha1=1.0000, perplexity=1171.142 Kneser-Ney: heuristic D=0.604, perplexity=1171.068 Kneser-Ney: minimum D=1.000, perplexity=1152.640 modified Kneser-Ney: perplexity=1155.159 BIGRAM: additive smoother: alpha2=0.0031, perplexity=1335.287 Kneser-Ney: heuristic D=0.761, perplexity=562.420 Kneser-Ney: minimum D=0.872, perplexity=556.637 modified Kneser-Ney: perplexity=543.748 TRIGRAM: additive smoother: alpha3=0.0010, perplexity=9491.406 Kneser-Ney: heuristic D=0.881, perplexity=549.436 Kneser-Ney: minimum D=0.965, perplexity=542.305 modified Kneser-Ney: perplexity=520.433
コーパスからランダムにある割合でテストデータを選び、残りを訓練コーパスとしている。テストデータの割合は -r オプションで与えられる(デフォルト = 0.1)。テストデータを選ぶ乱数のシードは --seed で与える。
単語はコーパスモジュールの words() が返すものをすべて使っている(つまり記号類も有効)。正規化は小文字化のみしている。
結果は 1-gram から 3-gram まで、それぞれ以下の perplexity を求めている。
- 加算スムージング。パラメータは [0.0, 1.0] の範囲で perplexity が最小になる値を選ぶ
- Kneser-Ney スムージング。パラメータはヒューリスティックな値を選ぶ
- Kneser-Ney スムージング。パラメータは [0.0, 1.0] の範囲で perplexity が最小になる値を選ぶ
- modified Kneser-Ney スムージング。パラメータはヒューリスティックな値を選ぶ
結果をまとめると、こんな感じか。
- 1-gram では大差はない( interpolate ほぼしないから当然っちゃあ当然 )
- 2-gram 以上では Kneser-Ney は加算スムージングよりはるかに良い性能を叩き出す
- modified Kneser-Ney は オリジナルの Kneser-Ney より確かに性能が良いが、定性的に違いがわかるほどのレベルではないだろう。計算コストを考えるとオリジナルで十分に思える。
- 3-gram で加算スムージングが悪化しているのはコーパスが小さいせいか(それにしても悪すぎるんだが……)。Kneser-Ney はそんな状況でもちゃんと性能アップしている。
というわけで、確かに Kneser-Ney はなかなかいいね、という結果に。
KN で尤度を求めるには N1+ やらを数えて覚えておかないといけないので大変。modified KN だと数えておかないといけないものがさらに多くて、KN の2倍くらい大変。
でも尤度を求めるのではなく、「文生成などのために次の単語の分布を求める」という場合には N1 類を数えて保持しておく必要がない(語彙をぐるっと舐めている間に、どれだけ discount したかは当然わかる)ので、KN / modified KN ともに気楽に使える。
ロイターコーパスと movie_reviews コーパスの結果もおまけ。
$ ./knsmooth.py -c reuters --seed=0 found corpus : reuters (D=10788) # of terms=1549612, vocabulary size=29774 UNIGRAM: additive smoother: alpha1=1.0000, perplexity=854.850 Kneser-Ney: heuristic D=0.564, perplexity=855.384 Kneser-Ney: minimum D=1.000, perplexity=850.667 modified Kneser-Ney: perplexity=850.833 BIGRAM: additive smoother: alpha2=0.0018, perplexity=207.485 Kneser-Ney: heuristic D=0.670, perplexity=143.591 Kneser-Ney: minimum D=0.704, perplexity=143.527 modified Kneser-Ney: perplexity=142.154 TRIGRAM: additive smoother: alpha3=0.0004, perplexity=512.933 Kneser-Ney: heuristic D=0.762, perplexity=93.349 Kneser-Ney: minimum D=0.799, perplexity=93.246 modified Kneser-Ney: perplexity=89.477
$ ./knsmooth.py -c movie_reviews --seed=0 found corpus : movie_reviews (D=2000) # of terms=1422742, vocabulary size=38181 UNIGRAM: additive smoother: alpha1=1.0000, perplexity=908.244 Kneser-Ney: heuristic D=0.561, perplexity=908.691 Kneser-Ney: minimum D=1.000, perplexity=902.749 modified Kneser-Ney: perplexity=903.249 BIGRAM: additive smoother: alpha2=0.0023, perplexity=515.530 Kneser-Ney: heuristic D=0.739, perplexity=283.122 Kneser-Ney: minimum D=0.806, perplexity=282.376 modified Kneser-Ney: perplexity=278.357 TRIGRAM: additive smoother: alpha3=0.0007, perplexity=2735.080 Kneser-Ney: heuristic D=0.851, perplexity=239.729 Kneser-Ney: minimum D=0.918, perplexity=238.210 modified Kneser-Ney: perplexity=230.498