英文コーパスを対象とする検索や統計処理を行う際には、英単語の屈折を正規化したり展開したりしたくなるだろう。それを簡単に行うためのPythonライブラリを作ったという話。
「go」という単語の用例を英文コーパスから探したいとしよう。「go」をクエリとして検索すると、「I go to school.」はヒットする。しかし、「He goes to church.」や「You went to the hospital.」は、屈折形が使われているのでヒットしない。これを解決するには、二つの方法がある。インデックスを作る際に、「goes」「going」「went」「gone」を「go」に正規化しておくか、検索時にクエリ「go」を「go OR goes OR going OR went OR gone」に展開するかだ。
そうなると、屈折形の原形への正規化や原形からの屈折形への展開の仕事を引き受けるライブラリが欲しくなる。英単語の屈折は不規則変化や方言や曖昧性などの面倒事があるため、アルゴリズミックに扱うのは困難で、内部にデータベースを持ったライブラリを使わざるを得ない。PythonだとNLTK(Natural Language Toolkit)とかのライブラリがあるが、インストールが面倒くさいし、遅いし、精度もあんまり良くない。そこで、せっかくオープンな英和辞書を持っているのだから、そこから自前の屈折データベースを抽出して、ライブラリも書いみた。
まあそういうわけで、使ってみてほしい。Python3が動くマシンであれば、どこでも動くはずだ。このTSVファイルenglish_inflections.tsvとソースコードenglish_inflections.pyをダウンロードして適当な場所に置けばインストール完了だ。このライブラリはコマンドとしても動くので、まずはそれを使ってみよう。「go」の屈折形を知るには、以下のコマンドを実行する。
$ ./english_inflections.py go {'o': 'go', 'vs': ['goes'], 'vc': ['going'], 'vp': ['went', 'yode'], 'vx': ['gone', 'yode'], '_ph': ['vs', 'vc', 'vp', 'vx'], '_pi': 4.06}
このように、屈折形 "goes", "going", "went", "gone" を知ることができる。中英語(11世紀以前の英語)では過去形や過去分詞は"yode"だったらしいのでそれも出力されているが、二番目以降の候補を捨てるか使うかはユーザに委ねられる。他の語もやってみよう。今度は品詞を指定しないで全ての可能性を探る。
$ ./english_inflections.py good {'o': 'good', 'np': ['goods'], 'vs': ['goods'], 'vc': ['gooding'], 'vp': ['gooded'], 'vx': ['gooded'], 'ajc': ['better'], 'ajs': ['best'], 'avc': ['better'], 'avs': ['best'], '_ph': ['np', 'vs', 'vc', 'vp', 'vx', 'ajc', 'ajs', 'avc', 'avs'], '_pi': 4.352}
"o"は原形(Original)、"np"は名詞の複数形(Noun Plural)、"vs"は動詞の三人称単数現在形(Verb Singular)、"vc"は動詞の動名詞または現在分詞(Verb Continuous)、"vp"は動詞の過去形(Verb Past)、"vx" は動詞の過去分詞(Verb Past Particleだけど被るのでX)、"ajc" は形容詞の比較級(AdJective Comparative)、"ajs" は形容詞の最上級(AdJective Superative)、"avc" は副詞の比較級(AdVerb Comparative)、"ajs" は副詞の最上級(AdVerb Superative)である。
"_ph" や "_pi" はデバッグ用だ。結果のdictに"_ph"がある結果は、データベースに該当のレコードが含まれていたことを示す。その値に含まれる"vs"などは、そこからその要素のデータを採用したという意味だ。"_pi" はいわゆるIDF値だ。コーパス内の任意の文中にその語が出現する確率をpとすると、-log(p)がIDF値になる。この値が0に近いほど、よく出現する語だと判断できる。
他の語も試してみてほしい。規則変化だろうが不規則変化だろうが、大抵の語はデータベース内のデータだけで解決する。一方で、データベースに収録されていない語を入力してみると、屈折形の展開には失敗するし、メタデータの取得もできない。
$ ./english_inflections.py ganbarimanakan {'o': 'ganbarimanakan'}
データベースに収録されていない語は、標準英文法のルールに則って自動生成してもよさそうなものだ。データベース未収録語の場合にはそのフォールバックを発動させるオプションもついている。ちゃんと「ed」や「er」の前の「n」が2個になっているなど、中学で習うような、字面で自動的に判断できるルールは実装している。
$ ./english_inflections.py ganbarimanakan --fbgen {'o': 'ganbarimanakan', 'np': ['ganbarimanakans'], 'vs': ['ganbarimanakans'], 'vc': ['ganbarimanakanning'], 'vp': ['ganbarimanakanned'], 'vx': ['ganbarimanakanned'], 'ajc': ['ganbarimanakanner'], 'ajs': ['ganbarimanakannest'], 'avc': ['ganbarimanakanner'], 'avs': ['ganbarimanakannest']}
おまけで、複数語からなるフレーズにも対応している。その場合、動詞は最初の単語を優先して屈折させ、それ以外の品詞は最後の単語を優先して屈折させる。"_th"はトークン単位のデバッグ情報である。
$ ./english_inflections.py "power house" --fbgen {'o': 'power house', 'np': ['power houses'], 'vs': ['power houses'], 'vc': ['power housing'], 'vp': ['power housed'], 'vx': ['power housed'], 'ajc': ['power houser'], 'ajs': ['power housest'], 'avc': ['power houser'], 'avs': ['power housest'], '_th': ['np', 'vs', 'vc', 'vp', 'vx']}
次に、屈折形を与えて原形を検索する機能を使ってみよう。「went」の原形を知りたいならば、以下のコマンドを実行する。
$ ./english_inflections.py --search went go 5.06 vp gan 10.27 vp wend 17.17 vp,vx
原形の語とその出現コストとクエリが該当したラベルのリストが提示される。出現コストは、IDFなどから生成されているが、要はその値が小さいほどそれっぽいということだ。「gan」も「wend」も「go」とほぼ同じ意味の中英語らしいが、まあ無視して構わないだろう。以下のように曖昧な語の場合には、2番目以降の結果を使うべきかもしれない。
$ ./english_inflections.py --search saw see 4.99 vp saw 6.40 o $ ./english_inflections.py --search lay lay 6.94 o lie 7.07 vp
検索機能も複数語からなるフレーズに対応している。その場合、まずはそのフレーズ自身が原形かどうかをしらべ、次にフレーズ単位の屈折形の索引を調べる。それでもヒットしない場合、個々の単語を屈折させてみて、それで原形にヒットしたなら、それを提示する。
$ ./english_inflections.py --search "better children" better child 25.92 np well children 26.19 ajc,avc good children 26.35 ajc,avc bettor children 36.25 a
ライブラリとしての使用法も紹介する。データベースファイルを読み込ませたインスタンスを作り、それを使い回すことになる。インスタンス化には0.5秒とかかかるけども、一旦インスタンスを作ってしまえば、各種のメソッドは高速に動作する。
import english_inflections inflector = english_inflections.Inflector("english_inflections.tsv") inflector.InflectVerb("go") # -> {'o': 'go', 'vs': ['goes'], 'vc': ['going'], 'vp': ['went', 'yode'], # 'vx': ['gone', 'yode'], '_ph': ['vs', 'vc', 'vp', 'vx'], '_pi': 4.06} inflector.InflectVerb("ganbarimanakan") # -> {'o': 'ganbarimanakan'} inflector.InflectVerb("ganbarimanakan", True) # -> {'o': 'ganbarimanakan', 'vs': ['ganbarimanakans'], 'vc': ['ganbarimanakanning'], # 'vp': ['ganbarimanakanned'], 'vx': ['ganbarimanakanned']} inflector.Search("went") # -> [('go', 5.06, ['vp']), ('gan', 10.27, ['vp']), ('wend', 17.174, ['vp', 'vx'])] inflector.Search("saw") # -> [('see', 4.989, ['vp']), ('saw', 6.404, ['o'])] inflector.LookupPhraseInfo("good") # -> {'np': ['goods'], 'vs': ['goods'], 'vc': ['gooding'], 'vp': ['gooded'], # 'vx': ['gooded'], 'ajc': ['better'], 'ajs': ['best'], # 'avc': ['better'], 'avs': ['best'], 'i': 4.352}
屈折形の生成は品詞に着目して行うので、品詞はユーザが決めねばならない。品詞が何であれ良いという場合には、Inflectを呼んで全ての可能性を調べられる。第2パラメータで、フォールバックの自動生成を行うかどうかが決められる。なお、形容詞や副詞の場合、データベースにない語に対しては、無理やり屈折形を自動生成するのではなく、「more」や「most」を前置するのが自然だろう。
原形の検索は、Searchを呼んで行う。それっぽい順の結果のリストが返される。返された語の詳細が知りたければLookupPhraseInfoを呼ぶとよい。これは単にデータベースのエントリを調べて返す。もし該当がなければNoneを返す。
似たようなことができるライブラリは世の中に数多くあるが、このライブラリの特徴は、とにかくインストールが簡単で、しかもそこそこ高速に動作することだ。Wiktionaryのデータを使っているので、カバレッジもかなり高い。そして、データベースが単なるTSVファイルなので、お気軽にデータを改変したり追加したりできる。ソースを読んでいただければわかるが、TSVファイル読み込んで連想配列にデータを入れて検索するだけの簡単なお仕事だ。現状ではPython版しかないけども、他の言語への移植も容易なはずなので、暇な人はやってみていただきたい。
まとめ。英単語の屈折を扱うPythonライブラリを書いた。これがあれば、屈折形を原形に正規化するのも、原形を屈折形に展開するのも、楽ちんだ。ちょっとした検索システムや統計処理の実装のお供にどうぞ。