mots quotidiens.
Daichi Mochihashi (持橋大地) daichi <at> ism.ac.jp by hns, version 2.10-pl1.

先月 2010年03月 来月
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 26 27
28 29 30 31

2010年03月24日(水) [n年日記]

#1 lwlm, The Latent Words Language Model. (2)

連休中に学習させておいた毎日新聞と New York Times のモデルを 置いておきました。
ただ, 教師なし学習に共通することですが, (識別学習のように「ルール」を使うわけ ではないので)適用したいドメインのテキストで直接学習した方が, 結果が良いよう です。データが少ないと, nグラムのスパースさに引きずられて, 意味的というより 文法的な同意語が出てしまうようになる模様。

さて, 潜在語言語モデルは, NLPの立場からは観測値のnグラムをそのまま使うのでは なく, 「真のnグラム」を推定して, そこから観測語が生まれたと考えるモデル ですが, この学習は言うほど簡単ではありません。
各単語について数万次元(=語彙次元)の隠れ変数が存在するため, EMアルゴリズムで Forward-Backwardを使おうとすると, 数億を超える組み合わせのテーブルを計算する ことになってしまうため不可能で, Gibbsで局所的に潜在語をサンプリングして学習 していくことになります。 *1

ただしこの時も, ナイーブにはコーパスの各語に対して数万個ある潜在語候補の確率 を求める必要があり, 語彙が多いときわめて遅くなります。
実際, 上のコードで learn.cpp や decode.cpp の中にある slice_gibbs_draw() を gibbs_draw() に変えると「サンプリングの耐えられない遅さ」 というどこかの小説のタイトルのような感じになりますので, 試してみると面白いかもしれません。

・ Slice sampling

この問題に対処するため, 上のコードでは, スライスサンプリングを使って確率的に 候補を減らしています。
スライスサンプリングは, 簡単に言うとNLPでよくある「閾値」を, それ自体確率的にサンプリングすることで, 効率的かつ正確な学習を可能に する方法です。 IBISの武井-牧野君の話ではBeam samplingとしてPCFGに適用していましたが, ここでは 動的計画法が実際上使えないので, 局所的なGibbsに使っています。

HPYLMの場合, nグラムが内部的には1グラム-2グラム-3グラムと樹構造になっています が, スライスがPY過程の「バックオフ係数」にあたる (θ+d*tu)/(θ+nu)より大きかった場合, 木を上にバックオフしてもスライスより確率の高い候補は 現れないため, 比較的効率的にサンプリングの候補を計算することができます。

..が, 実際やってみると, 特に名詞が来る位置で, 1グラムまでバックオフして スライスを超える候補を見つける必要があることが多いことがわかりました。
1グラム(木のルート)には全ての単語が存在しているため, スライス s を超える確率 を持った単語を探すためには, ここで数万個の候補をなめる必要が生じてしまいます。 ユニグラムが頻度順に並んでいたりすれば枝刈りは容易ですが, LWLMではnグラム自体 がサンプリングされた潜在語によって動的に書き換わるため, そうした静的な性質は 期待できません。

・ record

このために, record という特別なデータ構造を用意することに。 岡野原君に相談したところ, ベクトルで順番に持って, 頻度が変更された時に書き換え ればいいのではないかとのこと。(感謝。)
真面目に完全に頻度順にしようとすると, restaurant * が頻度順に 46 32 15 4 4 4 3 3 2 2 [2] 1 1 1 のように並んでいるベクトルで[2]->3になった時 に, 二分探索で最左の3を探し(数字を入れ換えるのではなくて, restaurant * を入れ換える), swap()を実行すればいいですが, 単語の頻度は学習時にもの凄い勢いで増減するので, 二分探索とはいえ, ここ自体が 計算のボトルネックになってしまう可能性があります。 *2

よく考えると, 完全に単語が頻度順に並んでいる必要はなく, 枝刈りができる程度に ソートされていればよいので, 少し考えて, 頻度が 2^n 以下になる境界だけを配列で コンパクトに持つことに。
頻度が1増減する毎に, それが2^nになっているかどうか調べ (これは二の補数表現を使うと, record.cpp の pow2() というトリッキーなコードで 高速に調べられます), 2^nになった/そこから減った場合は swap を行って境界の値を書き換えます。
さらにメタ関数 foreach を書いて, 2^n境界を使って頻度の高い順からアイテムに 関数ポインタで受け取った関数を実行する Lisp 的な関数を定義しました。
正直これを正しく, かつ美しく実装するのに3日かかりました。。(record.cpp)

効果は劇的で, ここまでやって初めて, 語彙の数にそれほど依存せずに高速な学習 ができるようになりました。長かった。。
最近, Phil Blunsomがslice samplingを使って Synchronous grammar の構文木を サンプリングするという話を出しているようですが(NAACL 2010), 莫大な候補から 効率的にサンプリングするという意味では, 似たところがあるかもしれません。

・ -

ちなみに, 実はこのコードの売りは, HPYLMの完全な実装が含まれていることかも しれません。 Songfang Huang のページでは, (恐らく彼がIBMに援助されているという事情もあって)ソースは公開 されていないので, これが多分, 現在唯一の公開されているベイズnグラム(HPYLM)の コードなのではないかと思います。
僕はNTTにいるので研究用のコードは公開できませんが, これは(実装に色々工夫が あるとはいえ)他の人の研究なので, 公開できるという感じです。


*1: 下で書くスライスサンプリングを使うと候補の数を減らすことができるため, 一見 Forward-backwardができるように見えますが, たとえsliceを使った場合でも, 特に 様々な名詞が来る位置などで確率的に万を超える候補が出る場合があり, それが隣合う (数百万語を超えるコーパスでは確率的には十分起こりうる)と, 同様に億や兆単位の 動的計画法を計算する必要が生じます。実際バイグラムまでは実装しましたが, トライグラムを書いている時点でこのことに気付き, 諦めました。
*2: 数字の境界を別に持っておけば大丈夫ですが, 数百万個のnグラムノードでそれを ナイーブに持つとメモリが破綻します。

1 days displayed.
タイトル一覧
カテゴリ分類
 なかのひと
Powered by hns-2.10-pl1, HyperNikkiSystem Project