豪鬼メモ

一瞬千撃

日英対訳コーパスから自動的に英和辞書を作る

英和辞書を自動生成するにあたって、対訳コーパスを使って自動に対訳フレーズを抜き出す方法についてメモがてら説明する。


「Hello. Nice to meet you. I'm Tanaka.」といった英文の文章に対して「こんにちは。はじめまして。田中と言います。」といった和訳の文章を関連づけたものを、対訳コーパスと呼ぶ。実際には、日本語が原文で英語の方がその英訳だったりするかもしれないが、その方向はどっちでもいい。もちろん日英以外の対訳コーパスもある。有志が編纂した対訳コーパスがネット上にはいくつかあり、それらを使えば様々な研究ができる。Web等からデータを集めて自分で対訳コーパスを作ることもできる。良い時代だ。典型的な対訳コーパスにおいては、対訳と思われる2つの文書を関連づけた上で、さらにその中のそれぞれの文を関連づけるアラインメント処理が施されている。結果として、("Hello.", "こんにちは"), ("Nice to meet you.", "はじめまして"), ("I'm Tanaka.", "田中と言います") といった文の対(センテンスペア)のリストが収録される形になる。

統計的機械翻訳の基礎的な技術を応用して、英和辞書の訳語を自動生成する。ここでは、英文をソース側、和文をターゲット側と呼ぶ。基本的な考え方はこうだ。ソース側に "hello" が出現したとして、その際にターゲット側に "こんにちは" が出現する確率が高ければ、"こんにちは" が "hello" の訳語として相応しいと考えられる。この確率を P(T|S) と呼ぼう。同様に、ターゲット側に "こんにちは" が出現したとして、その際にソース側に "hello" が出現する確率が高ければ、"hello" が "こんにちは" の訳語として相応しいと考えら得れる。この確率を P(S|T) と呼ぼう。PTSを見るだけだと、ターゲット側の頻出語である "は", "が", "の", "に" といった単語まで訳語として検出されてしまうという問題がある。逆に、PSTを見るだけだと、ソース側の頻出語である "is", "do", "not", "of" といった語まで訳語として検出されてしまうという問題がある。よって、PTSとPSTの両方が高いもののみを訳語として選択するのだ。PTSとPSTの対数の各々に重みをつけて、その和が一定以上であるものを選択するのが一般的である。これはすなわち、PTSとPSTに対して重み付きの相乗平均を取っているのと同義だ。

PTSとPSTを算出するためには、ソース側とターゲット側の各単語とフレーズについて共出現の回数を数え上げたデータベースが必要となる。またまた、数え上げ問題に落とし込まれた。例えば「She lives in Tokyo.」という文がソース側に現れたら、"She", "lives", "in", "Tokyo"という4つの単語が検出され、さらに "She lives", "lives in", "in Tokyo"という2グラムのフレーズも検出でき、場合によっては3グラムや4グラムのフレーズも扱うことになる。今回は2グラムまで扱うことにしようか。そして、ターゲット側には「彼女は東京に住んでいる」という文が関連づけられているとしよう。そこでは "彼女", "は", "東京", "に", "住ん", "で", "いる' という7つの単語が検出される。"彼女 は", "は 東京" などの2グラムと "彼女 は 東京", "は 東京 に" などの3グラムのフレーズも扱うことにしようか。そして、ソース側の語とターゲット側の語の全ての組み合わせを数え上げる。ソース側で7語、ターゲット側で18語が検出されているので、7 * 18 = 136のフレーズペアの出現を数え上げることになる。また、後にPTSとPSTの分母として使うために、ソース側の語の出現頻度とターゲット側の語の出現頻度も数え上げるので、7 + 18 = 25が足されて、総計161レコードの数え上げということになる。文が長くなるほどにこの数は膨大になり、そしてこの処理を何千万文もの大規模なコーパスに対して行うのだ。単に文字列を数えているだけだが、なかなか大変な仕事とも言えよう。


数え上げの具体的な手順について述べる。今まで散々述べてきたように、スケーラブルな数え上げと言えばミニバッチ化と足切りとスキップリストである。入力データをシーケンシャルに読み込みつつ、途中経過が作業メモリに載る分量になったらその結果を一時ファイルに書き出す。省メモリの連想配列としてTkrzwのBabyDBMを用い、スキップリストのデータベースとしてこれまたTkrzwのSkipDBMを用いる。書き出す際には低頻度のレコードを足切りすることで、一時ファイルの容量が抑えられる。全ての入力データを処理し終えたら、全ての一時ファイルをマージする。スキップリストのデータベースがソート済みなのでマージ処理の空間効率と時間効率は理想的だ。

既に述べたように、数え上げる主な対象はソース側の語とターゲット側の語のペアである。ソース側の語とターゲット側の語をタブで連結した文字列をキーとした連想配列を管理し、その値として出現頻度を記録し、加算していく。ソース側の語そのものの出現頻度は、ソース側の語にタブを後置したキーのレコードで管理する。ターゲット側の語そのものの出現頻度は、ターゲット側の語にタブを前置したキーのレコードで管理する。よって、こんなような連想配列をメモリ上に持ち、それを一定の周期でファイルに書き出すことになる。さらに、読み込んだ文章の数は空文字列のキーのレコードで管理する。

{
  "She\t彼女": 9342,  # フレーズペア
  "She\tは": 8533,
  "She\tの": 10639,
  ...
  "\tShe": 14352,     # ソース側
  "\tlives": 7210,
  "\tShe lives": 32,
  ...
  "彼女\t": 8987,     # ターゲット側
  "は\t": 39892,
  "の\t": 42117,
  ...
  "": 432734,         # 全文章数
}

入力データについて考えよう。ここでは、ちょっとした工夫が求められる。ナイーブな数え上げに基づくアルゴリズムの弱点の一つとして、入力データの偏りをそのまま反映してしまうことが挙げられる。多くの場合、入力データには強い偏りがある。クローリングされやすいサイトや文書、アラインメントが取りやすいサイトや文書があるのだ。特に、Webページを対象にデータを収集した場合、同じサイトやドメインから似たような文書を大量に集めてしまう傾向がある。例えば野球のサイトからデータを集め過ぎたとしよう。そうすると、"run" と "盗塁" の共起が数多く記録され、"walk" と "フォアボール" の共起が数多く記録され、一般的な認識とはかなりずれた辞書が出来上がってしまう。したがって、入力データのバイアスを取り除く「パランス」処理が非常に重要になってくる。Webで言うなら、同一のドメインから取得する文書数を制限したり、同一の文書から抽出するセンテンスペアの数を制限したりといった工夫だ。

文書単位でのバランス処理は対訳コーパスの作成者が既に行ってくれていると仮定しよう。それでも、文書の長さはまちまちであり、長い文書から似たようなセンテンスペアを大量に抽出してしまうのは問題だ。そこで、数え上げプログラムの入力として、はソース側の文とターゲット側の文に加えて、そのセンテンスペアが所属する文書のIDと、そのセンテンスペアのアラインメントのスコアを入力してもらう。文書ID、スコア、ソース文、ターゲット文の順にタブで区切ったレコードを行で区切って並べたTSVファイルだ。例えばこんな感じ。

dbmx.net/index.html    0.895    How to use DBM.        DBMの使い方
dbmx.net/index.html    0.732    DBM is a simple DB.    DBMはシンプルなDBだ。
example.com/hello      0.981    Hello World            こんにちは世界

シーケンシャル処理の都合上、入力データの行は文書IDでソートされているものとする。より正確に言えば、同一文書IDのレコードが連続して現れさえすれば、順番はどうでもよい。文書IDとしてはURLでもUUIDでも何でも良いが、ユニークな文字列を用いる。同一文書から最大100センテンスペアを読み込むとして、100を超えていたらスコアが高い順に読むことになる。スコアの概念がない入力データの場合、全部1とかの同じ値にした上で、同一文書内でランダムシャッフルしたレコードを入力すればよい。

そして、さらなるロバストネスへの工夫として、同一文書内での数え上げに上限値を持たせたり対数で調整するということが考えられる。今回は上限1にした。つまり、("hello", "こんにちは") というフレーズペアがその文書内に何度現れようとも、1を計上するのだ。これによって、長すぎる文書からの悪影響を抑えられる。同一ドメインに大量の類似文書があることが疑われるような場合、文書IDをドメイン名にして、つまりそのドメインの全ての文書を単一の文書であるとみなして扱うことも考えられる。このあたりの匙加減を入力データの作り方で調整できるようにしておくことが重要だ。

ソース側から語のリストを切り出す際には、英語であれば、単に空白で区切って、ラテン文字の連続を単語とみなせば良い。既に述べたように、今回は単語の連続を2グラムまで扱う。また、オプションとして、予め定めたフレーズリストに含まれる語彙のみに絞る機能もつける。今回はWordNetの見出し語に対訳をつけるのが目的なので、それに含まれないパターンに対して数え上げを行うのは完全に無駄だ。この絞り込みを行うとソース側のデータ量が激減する。機械翻訳のモデルを作る際には必要な語彙を予め絞るのは難しいかもしれないが、辞書を作るには予め見出し語の候補を絞ることができるので、その知識を使わない手はない。ソース側のステミングは行わない。「dry」と「dried」は含意は違うだろうから、数え上げの時点で同一視するのは危険だ。屈折形の多くは見出語になっていないので単に無視されてしまうのだが、今回はそれで構わない。

ターゲット側から語のリストを切り出す際には、日本語であれば、MeCabなどの解析器を使って分かち書きを行う。今回は単語の連続を3グラムまで扱う。なぜ日本語側の範囲を広くしたかというと、形態素で考えると英語の単語より細かくなることが多いからだ。「崩壊 し た」とかの短い語でも形態素で数えると3グラムもある。欲を言えば5グラムくらいは扱いたいのだが、今回は1台で処理する前提なので割愛した。さらに、ターゲット側ではステミングも行う。見出語は基本形であるべきだからだ。語が複数の形態素からなる場合、最後の形態素のみを基本形になおす。したがって、「崩壊した」からは "崩壊", "崩壊 する", "崩壊 し た" が抽出される。日本語側の活用は英語側の屈折と必ずしも対応しないので、日本語側の数え上げはできるだけノーマライズをしていかないとPTSが低くなり過ぎてしまうのだ。


数え上げが終わったら、仕上げを行う。個々のセンテンスペアについてPSTとPTSを計算して、その合成値が一定を上回ったら、対訳の辞書として書き出すのだ。そのアルゴリズムについて考えてみよう。構造的には、データベースをストリーミングとして処理することで、一定の作業メモリで処理が行えるように配慮している。

おさらいすると、PSTは P(S|T) であり、ターゲットが出現している状況においてソースが出現する確率だ。つまり、分母はターゲットの出現数で、分子はセンテンスペアの出現数だ。ターゲットの出現数のキーはタブを前置してあるので、辞書順に並んだスキップリストデータベースにおいては、最初に現れる。このターゲットの出現数だけはメモリに載せてランダムアクセスできるようにしておかねばならない。さて、PTSは P(T|S) であり、ソースが出現している状況においてターゲットが出現する確率だ。つまり、分母はソースの出現数で、分詞はセンテンスペアの出現数だ。ソースの出現数のキーはソースの文字列にタブを後置してあるので、つまり、そのソースが関わるセンテンスペアのレコード群が現れる直前にソース自体の出現率が得られる。ということで、まずはデータベースの冒頭にあるターゲットの出現数をメモリ上に読み込む。その後、残りのレコードを読み込む。キーがタブで始まっている場合、それはソースの出現数のレコードなので、それを記憶する。そうでない場合、ソースとターゲットのセンテンスペアのデータをひとつずつ読み込み、判定を行う。PSTもPTSも手持ちのデータで計算できる。

個々のフレーズペアに対してPSTとPTSを計算したら、最低限の足切りをした上で、結果をTSVファイルに書き出す。このプロセスはそこそこ時間がかかるので、この時点で細かいチューニングは行わない。よって、後でチューニングできるように、必要なデータは全て出力しておく。ソース語、ソースの出現数、ターゲット語とPTSとPSTを "|" で区切った文字列、をタブで区切ってTSVファイルとして吐き出す。以下のような感じだ。

walk  1234   歩く|0.432|0.832  歩行|0.211|0.732  ウォーク|0.088|0.958
run   8922   走る|0.252|0.782  走行|0.323|0.549  ラン|0.292|0.891

足切りがしてあることでこのTSVは十分に小さくなるので、最後の仕上げの処理はデータ全体をメモリに読み込んでから処理できる。ところで、ソース側である英語の単語が屈折することで、PTSは理想的な値より低くなる傾向がある。例えば、ターゲット語 "歩く" に対応するソース語として "walk" が挙げられる。日本語の活用によってターゲットが "歩く" と "歩いた" 等に分散される事態はステミングによって抑止されている。一方で、"walked" に関連する可能性が高い "歩いた" が "歩く" に正規化されることにより、ターゲット後 "歩く" のソースとの対応づけが "walk" と "walked" 等に分割されるので、結果としてPSTが下がる傾向にある。この事態を救済するには、("walk", "歩く") のPSTと ("walked", "歩く") のPSTを合算してやることが望ましい。そうするとランダムアクセスが発生するので、それを面倒がって全データをメモリに展開している。同じ理屈で、キャピタライズの対処も行う。英語の語が文頭にきた場合に先頭文字が大文字になることで、"walk" に流れるべき票が "Walk" に流れてPSTが下がる可能性があり、それを救済することが望ましい。実際には、"walk" と "walked" はかなり近くにあることが多いし、"Walk" 等の大文字語を扱う処理も逐次的なので、データベースのランダムアクセスをしたとしても局所正は高いので、性能の悪化は許容範囲だろう。

最終的に、ソースの出現数が100を超えて、かつPTSとPSTの相乗平均が0.3を超えたくらいのレコードのみを出力すれば、そこそこの対訳辞書が出力できる。より自然な訳語を選ぶには、日本語の大規模なモノリンガルコーパスから作った言語モデルで確率を計算して、それが低いものを捨てたりペナルティをかけたりするのと良いだろう。今回はWikipedia日本語版から作った3-grammモデル使ったフィルタを実装している。とはいえ、そういった努力をしてもなお、精度は完璧には遠い。結果の例を挙げるとこんな感じだ。

abalone         アワビ  アワビの
abdomen         腹部
aberration      収差    色収差
ability         能力
abiotic         非生物的
ablation        アブレーション
able            を
abnormal        異常    異常だ
abortion        中絶
about           について
above           上記の  上記
abrasion        摩耗
abrasive        研磨
abscess         膿瘍
absentee        不在者投票      不在者
absolute        絶対    絶対的

自動的に作っている割には、まともな結果だと思う。でも「able」の訳語が「を」なのはさすがに変だ。また、「aberration」の訳語に「色収差」を含んでしまうのは、コーパスの中で「chromatic abberation」として偏って使われているからだろう。同様に、「absentee」の訳語が「不在者投票」になるのは、コーパスの中で「absentee voting」として偏って出ているからだろう。「chromatic abberation」の訳語としても「色収差」が取れているので、「chromatic」が「色」で「abberation」は「収差」であろうという推定をして切り分けることができなくはないが、一筋縄ではいかない気もする。あの手この手のヒューリスティクスを適用すればもう少しマシにはなると思うが、それよりは、コーパスの規模やバランスを改善することに努めた方が良さそうだ。

「Japanese」の扱いにも困る。「Japanese」の訳語としては「日本の」「日本語」「日本人」のいずれもが典型的なので、票が割れてPTSが下がってしまう。さらに、どれも「日本」がついているので、PTSが最高になるのは「日本」である。「日本」に着目するとPSTが最高なのは「Japan」だが、「Japanese」も負けないくらい出てくる。数え上げのデータだけでこれをうまく切り分けることは難しいだろう。

検出した訳語がどのくらい妥当かという精度の問題に関しては、それぞれの語を確かめれば求められる。一方で、理想的には検出すべき訳語がきちんと取れているかという再現率の問題に関しては、理想の訳語辞書がない以上、簡単には調べられない。ただ、主観的な評価を敢えて言えば、この手法は再現率もよくない。上記の例の「able」とかもそうだが、多義語や訳語が定まらないような抽象語の訳語はPTSもPSTも低くなるので、よっぽど閾値を下げても拾えない。「have」「take」「put」「get」「leave」とかの基本動詞も全くダメだ。これは構造的な問題なので、この手法に基づいている限りは解決できないだろう。とはいえ、そういった基本語の種類は限られているので、それらに機械的に訳語を割り当てる必要はあまりない。この手法はロングテールな語を扱うためのものと割り切って考えるべきだ。


まとめ。日英の対訳コーパスから英和辞書の訳語を自動生成する方法について検討した。具体的な実装はcount_para_domain_phrases.pyextract_para_domain_trans.pyorganize_para_domain_trans.pyとして公開しておく。こんな単純なアルゴリズムでも、対訳コーパスの質がまともなら、それなりの精度の結果が得られる。とはいえ、その結果をそのまま採用して対訳辞書として日常使いするほどの精度や再現率には至らない。対症療法的な改善策はいくつか考えられるが、抜本的な改善はなさそうだ。統計的手法の限界なのか。いや、私がまともな手法を知らないだけなのかもしれないけども。