開発したので紹介したい。勉強している人も参考になると思う。?KEAはニュージーランドのワイカト大学で開発されている、自然言語で書かれた文書からキーフレーズ(キーワード)を自動抽出するプログラムである。Algorithmの略であり、KEAプログラムを構成するアルゴリズムそのものを指す場合もある。文書に付加されたキーフレーズは、当該文書の意味的メタデータであり、文書の極端に短いサマリーとも言える。そのため、文書を読み込む時間がない場合にその文書のメタデータであるキーフレーズのリストをながめるだけでおおまかな内容をつかむことができる。限られた時間である調べ物をしていたとしよう。その調査に関係しそうな文書は目の前に山積みされているが、すべてに目を通している時間はない。そんなときは、文書に振られているキーフレーズをまずながめて、関係しそうな文書だけを選んで読み始めることができる。学術論文などではその論文の著者によってキーフレーズが付加されているものが多い。一般の文書や書籍はキーフレーズがつけられていないものがほとんどである。KEAはそのような文書から自動的にキーフレーズを抽出しようというプログラムである。KEAは著者によりキーフレーズが付加された文書を読み込んでその特徴を学習し、キーフレーズがつけられていない未知の文書からキーフレーズを自動抽出するという、教師あり機械学習プログラムである。KEAは言語(英語や日本語など)に関係なく動作可能なアルゴリズムとなっている。本稿はLuceneインデックスからキーフレーズを抽出しようという話なので、キーフレーズ抽出と情報検索の関係についても触れておこう。KEAの作者らはキーワードという言葉ではなく、キーフレーズという言葉を使っている点に注目したい。キーワードというと重要な(キー)1つの単語を連想するが、ワードではなくフレーズ(成句)という言葉を使うことで、キーとなる1つ以上の単語の連なりも文書から抽出できるということが強調されている。Luceneインデックスからキーフレーズが抽出できたとすると、情報検索にはどんないいことがあるだろうか。思い浮かぶのがクエリのサジェスチョン(またの名をオートコンプリート)である。Luceneインデックスはどうしても単語単位で文字列が管理されているので、サジェスチョンも単語単位となってしまう。キーとなるフレーズが自動抽出できれば連続した複数の単語が一度にサジェストでき、ぐっとありがたみが増す。して検索も同様である。検索結果一覧を表示するときに、ハイライト機能の代わりもしくはハイライト機能と一緒に当該文書のキーフレーズを表示することでユーザの文書選択の助けとすることも考えられる。ファセット(絞り込み検索)のキーとすることも考えられるので、キーフレーズが抽出できることのメリットは大きい。KEAの処理概要KEAの論文は格別難しいことが書いてあるわけではないので、時間のある方にはぜひ一読をお勧めする。本稿ではのちのちLuceneライブラリを使ったKEAの実装を解説するので、それが理解できるよう最低限の説明を行うことにしよう。KEAにおける処理は「学習過程」と「キーフレーズ抽出過程」に大別されるが、両者に共通な処理としてキーフレーズの候補をリストアップするプロセスが存在する。キーフレーズ候補は機械的にリストアップされる。そのようにリストアップされた多数のキーフレーズ候補から、学習時はキーフレーズになるなりやすさ(またはなりにくさ)を学習する。キーフレーズ抽出時は学習した確率モデルを参照して、多数のキーフレーズ候補をスコアづけしてスコアの大きいものから順にキーフレーズ候補を表示する。実際のキーフレーズ抽出時はそのように順位付けされたキーフレーズ候補のリストから適当なところで足切りを行う。本稿で解説するKEAの実装は、Luceneを大いに活用する。学習のプログラムでは、既知の(著者によりキーフレーズがつけられた)文書が入ったLuceneインデックスを作成した後そこからモデルファイルを作成する。未知の(キーフレーズがつけられていない)文書からキーフレーズを抽出するプログラムでは、その文書をLuceneインデックス登録してからキーフレーズを抽出する。キーフレーズ候補の列挙KEAではキーフレーズの候補として、1つから最大3つの連続する単語を列挙する。次のような文書があったとしよう。Yugawara.すると、KEAでは次のように10個の候補となるキーフレーズを列挙する。yugawaraキーフレーズ候補の列挙においては、ストップワードから始まるフレーズやストップワードで終わるフレーズは候補とならない。goなどは候補としてリストアップされない。情報検索やNLPでよく行われる文字や単語の正規化もこの段階で行われる。よって、TokyoやYugawaraはtokyoやyugawaraに、likesはステミングされてlikeになる。KEAにおけるモデルの学習KEAでは単純ベイズ分類器を用いて教師ありデータ(著者によりキーフレーズがつけられた文書データ)からキーフレーズ候補におけるキーフレーズになるなりやすさ(P[yes])となりにくさ(P[no])を学習する。具体的には次のような式を用いる。N)"の部分はキーフレーズ候補から求められる事前確率である。先の例文でアスタリスクがつけられたものがキーフレーズだとすると、つぎのようになる。10KEAでは文書D中のフレーズPを表現するのに2つの特徴量を用いている。1つめはTF*IDFであり、次のように計算される。N))freq(P,D)は文書D中にフレーズPが出現する回数、size(D)は文書の単語数、df(P)はフレーズPを含む文書数、Nは全文書数である。logの底は2とする。occurrence(別名、距離)と呼ばれるものである。これはフレーズPが文書Dに最初に登場する位置をsize(D)で割ったものである。先の例でtokyoは1/7、governorは2/7となる。2つの特徴量は正の実数となるが、KEAでは連続値を離散化した上で各離散値ごとの事後確率を計算する。前述の式のPt[t|yes](またはPt[t|no])はTF*IDFの事後確率、Pd[d|yes](またはPd[d|no])は距離の事後確率である。未知の文書Dからキーフレーズ抽出を行うには、Dから列挙したキーフレーズ候補について前述の2つの特徴量を計算してP[yes]とP[no]を求め、最終的に次の式を使ってスコアを算出して降順にソートし、適当なところで足切りする。P[no])なお、未知文書はモデルに含まれていないため、TF*IDFを計算する際は、df(P)とNは1を加算する。Luceneを使ったKEAプログラムでは前述のアルゴリズムを元に、Luceneライブラリをフル活用してKEAプログラム(KEA-luceneと呼ぶことにする)を自作してみよう。ここで紹介するプログラムはGithubに公開している。プログラムはわかりやすさを優先し、ディレクトリ名などが意図的にハードコーディングされている。Luceneを使うのか?ところでKEAを実装するのになぜLuceneを使うのだろうか。KEAに限らず自然言語処理のツールを実装する場合、単語の数を数えたり、ある単語を含む文書の数を数えたり、それ以前に文書を単語に区切ったりすることがよく行われる。Luceneはこういった処理を行うのによく整備されたAPIを備えている。Luceneの転置インデックス(以下単にインデックスと呼ぶ)は単語辞書としても優秀だ。APIを以下に紹介しよう。AnalyzerLuceneでは、文章を単語に区切るのにAnalyzerクラスを用いる。KEA-luceneでは、トークナイズのためにStandardTokenizerを、小文字への正規化のためにLowerCaseFilterを、そして単語N-gramをサポートするためにShingleFilterを使っている。残念ながらKEAのストップワードの考え方はLuceneのStopFilterでは実現できないので、KEAStopFilterという独自TokenFilterを実装している。最終的に次のようにKEAAnalyzerを組み上げた。各品詞からストップワードリストを構成している。これはLuceneがリストアップしているストップワードよりもはるかに多い。KEA-luceneではLuceneのストップワードに独自にストップワードを追加した。KEA-luceneでは単語N-gramのNを1から3に変えて個別のフィールドにインデックス登録するため、LuceneのPerFieldAnalyzerWrapperを次のように使用している。}単語辞書としてLuceneインデックスを活用するLuceneは単語をキーにした転置インデックスを作成する。これを単語辞書として使うのは実に自然であり理にかなっているといえるだろう。フィールドfieldの単語を最初から最後まで走査するには次のようにする。irはLuceneインデックスをオープンしているIndexReaderのオブジェクトである。:}Luceneのインデックスはただ単に単語がリストアップされているだけではない。前述の2つの特徴量を計算するために必要となる統計量も保存されている。freq(P,D)は文書D中にフレーズPが出現する回数であるが、これはMultiFieldsのgetTermDocsEnum()メソッドを使用して取得したPostingsEnumオブジェクトに対しnextDoc()で文書Dを特定した後、freq()を呼び出すことで取得できる。df(P)はフレーズPを含む文書数、Nは全文書数であるがこれは次のようにして取得できる。ir.numDocs();さらにはフレーズの距離を計算するためにポジション情報も必要になってくるが、これはMultiFieldsのgetTermPositionsEnum()メソッドを使ってPostingsEnumオブジェクトを取得後、advance()メソッドで文書を特定した後nextPosition()で最初のフレーズのポジションを求める。3-gramを独立したフィールドにしているので、nextPosition()で取得したポジションはそのまま距離計算に用いて問題ない。モデルの学習(モデルファイルの出力)KEA-luceneではKEAModelBuilderがモデルの学習を行っている。このプログラムはここからダウンロードできる教師データ(MAUI.tar.gzを展開しさらにその中にあるfao30.tar.gzを展開する)をdata/fao30/ディレクトリに配置されることを前提に書かれているので注意していただきたい。KEAではモデルの学習は単純ベイズを使っているので、学習には各種統計量をカウントした上で統計量の割り算を行う。割り算を計算しているのは後述のKeyphraseExtractor(2)プログラムであり、KEAModelBuilderプログラムでは統計量を求めているだけなので、正確にはモデルの学習というよりはモデルファイルを出力しているに過ぎない。KEAModelBuilderが出力するモデルファイルは次のようなスペース区切りのテキストファイルである。false1つのレコードは1つのキーフレーズ候補を表している。最初の数値はTF*IDFで、2番目の数値は距離を表している。3列目はキーフレーズ(true)か否か(false)を表現するクラスである。KEAModelBuilderプログラムは上記のようなモデルファイルを出力するために教師データからLuceneインデックスを作成し、Luceneインデックスを単語辞書とみなしてiterateしながら特徴量を計算しつつ、上記のモデルファイルを出力する。参考までに処理時間を示しておこう。i7)では5分程度かかった。Luceneインデックスを作成するのは一瞬だが、Luceneインデックスを走査しながらモデルファイルを出力するのに少々時間がかかっている。Rによる連続値の離散化KEAでは特徴量の実数値をそのまま用いるのではなく、MDLP(最小記述長原理)により求めた離散値にマッピングして用いる。ここではMDLPの計算のためにRを使って次のように求めた。0.0697899000ここで得られた結果は、次のプログラムKeyphraseExtractor(2)で使用するので、cutp-model.txtというファイル名で保存しておく。0.0697899000RによるMDLP計算は同じ環境で30分程度かかった。Luceneインデックスからのキーフレーズ抽出ではいよいよLuceneインデックスからキーフレーズを抽出してみよう。プログラムはKeyphraseExtractorとKeyphraseExtractor2である。前者はKEAModelBuilderで作成したインデックスのうち試しに既知のファイルa0011e00.txt(このファイル名もプログラム中にハードコーディングしてある)からキーフレーズを抽出するものである。KeyphraseExtractor2の方はfao780(前述のMAUI.tar.gzを展開した中にある)内の未知の文書t0073e.txtからキーフレーズを抽出するプログラムとなっている。KeyphraseExtractorの方は既存のLuceneインデックスから統計量を得ているのに対し、KeyphraseExtractor2の方は新規の文書から各種統計量を得るためにLuceneインデックスを作成するところから行っている。計算部分で分母、分子とも1が加算されているところも異なる。KeyphraseExtractorとKeyphraseExtractor2はともにLuceneインデックスに登録済みの文書を特定し、その文書におけるキーフレーズ候補をリストアップしながらスコアを計算する。したがって単語辞書をiterateするよりもLuceneのTermVectorを使いたい。そこでLuceneドキュメント登録は次のようにFieldTypeのsetStoreTermVectors(true)を呼んでいる。}キーフレーズ候補は前述のスコア計算式によりscore(P,D)が計算され降順にソートされる。KEA-luceneではそのためにLuceneのPriorityQueueを用いている。ところで、KEAでは特徴量として連続値ではなく離散値を使っているため、score(P,D)が多くのキーフレーズ候補間で同点になる事象が論文で指摘されている。その場合はTF*IDFを使って同点決勝を行うということなので、PriorityQueueの実装は次のようになっている。}またPriorityQueue中下位のキーフレーズ候補が、それより上位のキーフレーズ候補のサブフレーズであった場合ははじかれるように実装する必要もある。実行結果を見てみよう。KeyphraseExtractorを実行して抽出されたキーフレーズの上位20件は次のようである。括弧内の2つの数値は、スコアとTF*IDF特徴量を表している(スコアが同点の時はTFIDF値が高い方が上位にきているのがわかるだろう)。(0.076506,0.001615)これに対し人手でつけられたキーフレーズは次のようである(fao30データは複数の人間が思い思いにキーフレーズをつけているので、一回でもキーフレーズと認識されたフレーズをここではリストアップしている)。hygieneworld両者に共通して現れるキーフレーズは次のようになる。production決して正解率が高いとは言えないが、抽出されたキーフレーズはフレーズとして不自然なものが少ないように見える。自動抽出されたフレーズとしてはいいセン行っているのではないだろうか。KeyphraseExtractor2を実行して未知の文書からキーフレーズ抽出した結果は次のようである。上位20件を表示している。(0.029596,0.003984)一方で、人手でつけられたキーフレーズは以下である。cropsVegetablesこちらも結構妥当なフレーズが抽出されている。情報検索のキーワードサジェスチョンなどに用いるには十分な精度といってもいいだろう。まとめ本稿は教師あり機械学習でキーフレーズを抽出するKEAを、あえてLuceneライブラリを活用することで実装した。APIをどのように使えるのか、プログラムから読み取りやすいのではないかと考えている。読者が本稿を通じてLuceneライブラリへ興味を持ったり、ライブラリへの知識を少しでも広げていただけたなら幸いである。本稿を執筆した最初のプログラムのバージョンでは、以下の項目については時間の制約から実装していない。Stemmerなどを使えるはずである。記事にあるとおりRを使っている。RのMDLP実装はGPLなので、この部分はぜひ自作か非GPLライセンスの実装を使えるようにしたい。日本語データなど英語以外のテキストに対して試す。教師データが必要なのと、ストップワード周りのチューニングが必要になるだろう。最新のKEA実装は前述の2つの特徴量に追加して論文には触れられていない特徴量があるようだ。KEA-luceneはLucene学習者向けにあえてハードコーディングをしているが、汎用的なものを適宜NLP4Lを通じて提供する予定である。使える類義語辞書を自動生成する方法を紹介する。過去にはこちらで説明している方法で行ったりもしたが、今回はいわゆる外来語に限って言葉を収集するもので、はるかに精度がよい(測ってはいないが・・・)。通じて公開しているので、誰でも試せる。理屈がわかれば、日本語Wikipediaに限らず収集できる。日本語文書は次のようにカタカナ語とその語源の英語(等)のアルファベット文字列が近い距離に配置されて書かれることが頻繁にある。entertainment)とは、人々を楽しませる娯楽をいう。使えるテキストファイルに出力することを考える。近い場所に書かれているすべてのペアが必ずしも同じ意味を持つとは限らない。次のような文章では間違った結果を得てしまう。コンピューターはFORTRANのおかげでより身近になった。文章を相当大量に収集してカタカナ語とアルファベット文字列の共起を調べることでも可能かもしれないが、文章を大量に収集することは誰でもできることではない。プログラムと学習データがついている。NLP4LNLP4Lの日本語チュートリアルこのプログラムを使うとカタカナ語から英単語の綴りを推定できる。この推定値と文章から拾ったアルファベット文字列を比較し、文字列が似ていれば(適当に決めた編集距離以下なら)拾ったカタカナ語とアルファベット文字列は同じ意味を持つとして類義語辞書に出力する。インデックスに登録する。インデックスからカタカナ語とアルファベット文字列のペアを抽出する。以上の手順は日本語Wikipediaから抽出するやり方だが、社内文書や専門書などからも収集できる。先にあげたエンターテインメントやインタフェースのように、カタカナ語に表記揺れがある場合も次のように収集できるのも興味深い。entertainment,エンターテイメント,エンターテインメントinterface,インタフェース,インターフェースpennsylvania,ペンシルバニア,ペンシルベニアうまく使えば検索の再現率向上に大いに役立つだろう。FridaySolrを使って専門用語抽出Solrを使って簡単に専門用語抽出する方法を紹介しよう。専門用語とは?「専門用語」とは、Wikipediaによれば、「ある特定の職業に従事する者や、ある特定の学問の分野、業界等の間でのみ使用され、通用する言葉・用語群」である。そこで本記事では、ある特定の分野の文書が登録されたインデックスと、別の分野の文書が登録されたインデックスを比較し、互いに相手方のインデックスに含まれない単語集合を表示することで専門用語を抽出することにする。以降であれば動作する(はず)。Solrを持っていない人はダウンロードして用意しよう。下記のschema.xmlを指定してSolrを起動する。<uniqueKey>url</uniqueKey></schema>文書データはここではlivedoorニュースコーパスを用いる。以下のようにwgetでlivedoorニュースコーパスを入手して展開しておく。livedoor-news-data.tar.gzそしてSolrを起動して展開して得られたファイルのうちひとつ(例としてここでは「独女通信(dokujo-tsushin.xml)」をSolrに登録する。次に、SolrのTermsComponentを使ってインデックスから単語一覧を取得する。次のcurlコマンドを実行すると、bodyフィールドでの使用頻度の多い順に上位1万語の単語一覧が得られる。するたてでとにのはをもだいるないあることからなるれるかという人うやですますようんたい思うheadコマンドで上位30位の頻出単語を表示しているが、前掲のschema.xmlファイルを見ていただくとわかるとおり、ストップワードの処理を一切していないので、およそ検索には役に立たない(そして専門用語でもない)単語が並んでいるのがわかるだろう。次に異なる分野の文書を登録して同様に単語一覧を表示する。そのためにここでいったんSolrを停止してインデックスを削除し、再度起動して別の分野の文書(ここでは例として「スポーツウォッチ(sports-watch.xml)」)を登録する。同様にTermsComponentを使って単語一覧を得る。語る2つの単語集合を比較する最後に2つの単語集合を比較して差分を表示するプログラムを書く。ここでは以下のような簡単なScalaプログラムを作成した。}}実行すると次のように2行の出力が得られる。1行目は「独女通信」にのみ出現する単語であり、2行目は「スポーツウォッチ」にだけ出現する単語である。頻出していたストップワードっぽい単語はきれいに消えていることがわかる。以下省略)これをもってそれぞれの分野の専門用語であると言い切るには難があるが、どちらかが「独女通信」でどちらかが「スポーツウォッチ」だという2択の問題を出されたならば、ほとんどの人が正解するくらいには専門用語っぽい出力が得られているといえるのではないだろうか。開発したのでその使い方を紹介しよう。らによって提案/開発された単語をベクトルに変換するOSSツールである。自然言語で書かれたテキストファイルを読み込み、単語ベクトルを出力するようになっている。テキストファイルの代わりにLuceneインデックスを入力コーパスとして用いる。扱うテキストファイルは単語単位で分かち書きされている必要がある。この点は英語テキストなどでは問題ないが、単語で分かち書きされていない日本語などではあらかじめMeCabなどの形態素解析器や分かち書きツールを用いて単語で分かち書きされたテキストファイルを用意しておかなければならない。単語テキストの直後に続くピリオド/カンマの削除など、事前の正規化作業が必要になってくる。Luceneインデックスを入力コーパスとして用いるため、事前の正規化作業が不要である。デモを実行する手順を説明する。ダウンロード2.デモデータの準備3.Lucene/Solrインデックスの準備(デモデータの登録)4.単語ベクトルの作成5.単語ベクトルで遊ぶという感じである。Luceneインデックスを持っている方は手順2〜3の代わりに手持ちのLuceneインデックスを手順4で指定することにしてもよい。インデックスで動作するものと思われる。こと、Elasticsearch(バージョンはよく知らないが1.x系以降)で作成したインデックスも指定できる。手順5で楽しい結果を得るためにはそれなりに大きなコーパスが必要である。環境をインストールする想定で話を進める。text8.zip手順3.Lucene/Solrインデックスの準備(デモデータの登録)コンソールを2つ用意して最初のコンソールでSolrを起動する。start.jar2つめのコンソールではtext8コーパスをSolrに登録する。スクリプトにSolrコア名を指定してword2vecを実行する。実行結果はvectors.txtというファイルに出力される。実行するとvectors.txtファイルを読み込んで入力待ちになる。そこで単語を入力すると、その単語に最も近い(コサインを計算)ベクトルを持つ単語上位40個を表示する。実行するとvectors.txtファイルを読み込んで入力待ちになるので、3つの単語をスペース区切りで入力する。womanと入力しているが、「manに対するkingの関係を、womanに適用するとどうなるか」という意味である。するとqueenが期待値になるが、実行結果もそのようになる。いう英語の正規化済みのコーパスを使った手順であるが、以下では日本語の(正規化済みでない)コーパスを使った手順も紹介しよう。ニュースコーパスを用いる。ニュースコーパスをダウンロードする。livedoorニュースコーパスをSolrに登録し、word2vecを実行する。コア名にはldccを指定する。-aオプションでLuceneのAnalyzerクラスを指定する。以下では-fオプションで出力先ベクトルファイル名を指定している(指定しないとデフォルトのvectors.txtとなってしまい、先ほど作成した英語の単語ベクトルファイルを上書きしてしまう)。見てもらうとわかるように、いくつかのパラメータがハードコーディングで指定されている。以下はLuceneに関連するパラメータである。インデックスのデータでword2vecが動くはずである。Luceneインデックスのディレクトリを指定する。実行するLuceneインデックスのフィールド名を指定する。このフィールドはindexedでかつstoredでなければならない。-fieldオプションで指定されたフィールドのstoredデータをここで指定されたLuceneのAnalyzerクラスでanalyzeする。indexedでなければいけないかというと、(オリジナル実装でも)word2vecではコーパスを2度読んでいて、1回目の読み込みで単語表を作成しているが、indexedであればそこは一瞬で完了し、なんといってもLuceneっぽいからである。word2vecで解析したくなるようなLuceneフィールドはたいていindexedでかつstoredと考えられるので、多くのLuceneインデックスオーナーにはこの条件は受け入れられるはずである。Luceneを手持ちのLuceneインデックス上で試していただき、どんな結果になったか教えていただければありがたい。テキストアノテーションツールである。GUI(ビジュアルと操作性)とできあがりのアノテーション情報のわかりやすさ(シンプルなフォーマットが自作NLPプログラムから扱いやすい!)、導入の簡単さがすばらしいのでここで紹介しよう。サーバ上で動かして本格的に使う方法と、個人的にさくっと立ち上げて使う方法があり、ここでは後者を紹介する。適当なディレクトリにダウンロードして展開する。brat-v1.3_Crunchy_Frog.tar.gzそして、次のようにインストールのスクリプトを実行して質問に答えれば終わりである。ログインするユーザ名を新規設定する。メールアドレスを入れる。ディレクトリの下に用意する。livedoorニュースコーパスを使った例を示す。拡張子でなければならない。ファイルをあらかじめ用意しておく必要がある。係り受けのような構造的アノテーションもつけることができるが、本記事では固有表現タグをつける例を示す。デフォルトの状態でもいくつかの固有表現タグが設定されているが、ここでは独自タグを設定してみる。ディレクトリの直下に、次のように2つの設定ファイルを作成する。EOF最初の設定ファイルはタグの設定、2つめはタグの色の設定である。これらは必要最低限の設定である。ディレクトリ内に設定ファイルがあるので、そちらを参照していただきたい。スタンドアロンモードで起動できる。ブラウザからアクセスする。すると、次のような画面が表示される。上のブルーの部分にマウスポインタを持って行くと、右上にログインボタンが現れるのでクリックする。annotate)を使ってログインする。使ってテキストアノテーションログインしたらコーパスコレクションを選択する。すると、次のコーパスコレクション選択画面になる。ディレクトリの下から適当なファイルを選択する。タグをつけるためマウスドラッグする。すると、次のようにタグ一覧がポップアップ表示される。これを繰り返していくと、次のようにテキストがアノテーションされる。ファイルには次のようにアノテーション情報がシンプルなフォーマットで記録されている。949宮崎タグをつけた順番で記録されているので、プログラムで読み込むときは、3〜4列でソートする必要があるだろう。SaturdayCaboChaとKNBコーパスで固有表現抽出を行うさらにCaboChaの評価は続く。今回はKNBコーパスを使って固有表現抽出の交差検定(5分割)を行った。今回はprecisionやrecallをカウントする評価ツールも自作した。