一定期間更新がないため広告を表示しています
関口宏司のLuceneブログOSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
2016.07.09 Saturday
キーフレーズ抽出ツール KEA を Lucene 上で実装する
キーフレーズ抽出ツール KEA を Lucene ライブラリを使って実装した KEA-lucene を開発したので紹介したい。Solr や Elasticsearch 利用が講じて Lucene API を勉強している人も参考になると思う。 KEA とは?KEAはニュージーランドのワイカト大学で開発されている、自然言語で書かれた文書からキーフレーズ(キーワード)を自動抽出するプログラムである。KEAはKeyphrase Extraction Algorithmの略であり、KEAプログラムを構成するアルゴリズムそのものを指す場合もある。 文書に付加されたキーフレーズは、当該文書の意味的メタデータであり、文書の極端に短いサマリーとも言える。そのため、文書を読み込む時間がない場合にその文書のメタデータであるキーフレーズのリストをながめるだけでおおまかな内容をつかむことができる。たとえば限られた時間である調べ物をしていたとしよう。その調査に関係しそうな文書は目の前に山積みされているが、すべてに目を通している時間はない。そんなときは、文書に振られているキーフレーズをまずながめて、関係しそうな文書だけを選んで読み始めることができる。 学術論文などではその論文の著者によってキーフレーズが付加されているものが多い。しかしながら一般の文書や書籍はキーフレーズがつけられていないものがほとんどである。KEAはそのような文書から自動的にキーフレーズを抽出しようというプログラムである。KEAは著者によりキーフレーズが付加された文書を読み込んでその特徴を学習し、キーフレーズがつけられていない未知の文書からキーフレーズを自動抽出するという、教師あり機械学習プログラムである。KEAは言語(英語や日本語など)に関係なく動作可能なアルゴリズムとなっている。 キーフレーズ抽出と情報検索の関係本稿はLuceneインデックスからキーフレーズを抽出しようという話なので、キーフレーズ抽出と情報検索の関係についても触れておこう。 まずKEAの作者らはキーワードという言葉ではなく、キーフレーズという言葉を使っている点に注目したい。キーワードというと重要な(キー)1つの単語を連想するが、ワードではなくフレーズ(成句)という言葉を使うことで、キーとなる1つ以上の単語の連なりも文書から抽出できるということが強調されている。 Luceneインデックスからキーフレーズが抽出できたとすると、情報検索にはどんないいことがあるだろうか。まず思い浮かぶのがクエリのサジェスチョン(またの名をオートコンプリート)である。Luceneインデックスはどうしても単語単位で文字列が管理されているので、サジェスチョンも単語単位となってしまう。しかしキーとなるフレーズが自動抽出できれば連続した複数の単語が一度にサジェストでき、ぐっとありがたみが増す。もしかして検索も同様である。 また検索結果一覧を表示するときに、ハイライト機能の代わりもしくはハイライト機能と一緒に当該文書のキーフレーズを表示することでユーザの文書選択の助けとすることも考えられる。さらにはファセット(絞り込み検索)のキーとすることも考えられるので、キーフレーズが抽出できることのメリットは大きい。 KEAの処理概要KEAの論文は格別難しいことが書いてあるわけではないので、時間のある方にはぜひ一読をお勧めする。本稿ではのちのちLuceneライブラリを使ったKEAの実装を解説するので、それが理解できるよう最低限の説明を行うことにしよう。 KEAにおける処理は「学習過程」と「キーフレーズ抽出過程」に大別されるが、両者に共通な処理としてキーフレーズの候補をリストアップするプロセスが存在する。キーフレーズ候補は機械的にリストアップされる。そのようにリストアップされた多数のキーフレーズ候補から、学習時はキーフレーズになるなりやすさ(またはなりにくさ)を学習する。そしてキーフレーズ抽出時は学習した確率モデルを参照して、多数のキーフレーズ候補をスコアづけしてスコアの大きいものから順にキーフレーズ候補を表示する。実際のキーフレーズ抽出時はそのように順位付けされたキーフレーズ候補のリストから適当なところで足切りを行う。 本稿で解説するKEAの実装は、Luceneを大いに活用する。学習のプログラムでは、既知の(著者によりキーフレーズがつけられた)文書が入ったLuceneインデックスを作成した後そこからモデルファイルを作成する。未知の(キーフレーズがつけられていない)文書からキーフレーズを抽出するプログラムでは、その文書をLuceneインデックス登録してからキーフレーズを抽出する。 キーフレーズ候補の列挙KEAではキーフレーズの候補として、1つから最大3つの連続する単語を列挙する。たとえば、次のような文書があったとしよう。
すると、KEAでは次のように10個の候補となるキーフレーズを列挙する。
キーフレーズ候補の列挙においては、ストップワードから始まるフレーズやストップワードで終わるフレーズは候補とならない。よって、単独のtoのみならず、like(s) toやto goなどは候補としてリストアップされない。また情報検索やNLPでよく行われる文字や単語の正規化もこの段階で行われる。よって、TokyoやYugawaraはtokyoやyugawaraに、likesはステミングされてlikeになる。 KEAにおけるモデルの学習KEAでは単純ベイズ分類器を用いて教師ありデータ(著者によりキーフレーズがつけられた文書データ)からキーフレーズ候補におけるキーフレーズになるなりやすさ(P[yes])となりにくさ(P[no])を学習する。具体的には次のような式を用いる。
ここで"Y / (Y + N)"や"N / (Y + N)"の部分はキーフレーズ候補から求められる事前確率である。先の例文でアスタリスクがつけられたものがキーフレーズだとすると、つぎのようになる。
KEAでは文書D中のフレーズPを表現するのに2つの特徴量を用いている。1つめはTF*IDFであり、次のように計算される。
freq(P,D)は文書D中にフレーズPが出現する回数、size(D)は文書の単語数、df(P)はフレーズPを含む文書数、Nは全文書数である。なおlogの底は2とする。 2つめの特徴量はfirst 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])は距離の事後確率である。 KEAにおけるキーフレーズ抽出未知の文書Dからキーフレーズ抽出を行うには、Dから列挙したキーフレーズ候補について前述の2つの特徴量を計算してP[yes]とP[no]を求め、最終的に次の式を使ってスコアを算出して降順にソートし、適当なところで足切りする。
なお、未知文書はモデルに含まれていないため、TF*IDFを計算する際は、df(P)とNは1を加算する。 Apache Luceneを使ったKEAプログラムでは前述のアルゴリズムを元に、Luceneライブラリをフル活用してKEAプログラム(KEA-luceneと呼ぶことにする)を自作してみよう。ここで紹介するプログラムはGithubに公開している。なお、プログラムはわかりやすさを優先し、ディレクトリ名などが意図的にハードコーディングされている。 なぜLuceneを使うのか?ところでKEAを実装するのになぜLuceneを使うのだろうか。KEAに限らず自然言語処理のツールを実装する場合、単語の数を数えたり、ある単語を含む文書の数を数えたり、それ以前に文書を単語に区切ったりすることがよく行われる。Luceneはこういった処理を行うのによく整備されたAPIを備えている。さらにはLuceneの転置インデックス(以下単にインデックスと呼ぶ)は単語辞書としても優秀だ。特にKEA-luceneのために使用したLucene APIを以下に紹介しよう。 AnalyzerLuceneでは、文章を単語に区切るのにAnalyzerクラスを用いる。KEA-luceneでは、トークナイズのためにStandardTokenizerを、小文字への正規化のためにLowerCaseFilterを、そして単語N-gramをサポートするためにShingleFilterを使っている。残念ながらKEAのストップワードの考え方はLuceneのStopFilterでは実現できないので、KEAStopFilterという独自TokenFilterを実装している。そして最終的に次のようにKEAAnalyzerを組み上げた。 public class KEAAnalyzer extends Analyzer { private final int n; public KEAAnalyzer(int n){ this.n = n; } @Override protected TokenStreamComponents createComponents(String fieldName) { Tokenizer source = new StandardTokenizer(); TokenStream lcf = new LowerCaseFilter(source); if(n == 1){ TokenStream stf = new KEAStopFilter(lcf, n, Commons.stopWords, Commons.beginStopWords, Commons.endStopWords); return new TokenStreamComponents(source, stf); } else{ assert n >= 2; ShingleFilter shf = new ShingleFilter(lcf, n, n); shf.setOutputUnigrams(false); KEAStopFilter keasf = new KEAStopFilter(shf, n, Commons.stopWords, Commons.beginStopWords, Commons.endStopWords); return new TokenStreamComponents(source, keasf); } } } KEAの論文によると、conjunctions, articles, particles, prepositions, pronouns, anomalous verbs, adjectives および adverbs の各品詞からストップワードリストを構成している。これはLuceneがリストアップしているストップワードよりもはるかに多い。KEA-luceneではLuceneのストップワードに独自にストップワードを追加した。 さらにKEA-luceneでは単語N-gramのNを1から3に変えて個別のフィールドにインデックス登録するため、LuceneのPerFieldAnalyzerWrapperを次のように使用している。 public static Analyzer getKEAAnalyzer(String fieldName){ Map 単語辞書としてLuceneインデックスを活用するLuceneは単語をキーにした転置インデックスを作成する。これを単語辞書として使うのは実に自然であり理にかなっているといえるだろう。フィールドfieldの単語を最初から最後まで走査するには次のようにする。ただしirはLuceneインデックスをオープンしているIndexReaderのオブジェクトである。 Terms terms = MultiFields.getTerms(ir, field); TermsEnum te = terms.iterator(); for(BytesRef rawPhrase = te.next(); rawPhrase != null; rawPhrase = te.next()){ String phrase = rawPhrase.utf8ToString(); : } Luceneのインデックスはただ単に単語がリストアップされているだけではない。前述の2つの特徴量を計算するために必要となる統計量も保存されている。たとえばfreq(P,D)は文書D中にフレーズPが出現する回数であるが、これはMultiFieldsのgetTermDocsEnum()メソッドを使用して取得したPostingsEnumオブジェクトに対しnextDoc()で文書Dを特定した後、freq()を呼び出すことで取得できる。 またdf(P)はフレーズPを含む文書数、Nは全文書数であるがこれは次のようにして取得できる。 int dfreq = ir.docFreq(new Term(field, phrase)); : int n = ir.numDocs(); さらにはフレーズの距離を計算するためにポジション情報も必要になってくるが、これはMultiFieldsのgetTermPositionsEnum()メソッドを使ってPostingsEnumオブジェクトを取得後、advance()メソッドで文書を特定した後nextPosition()で最初のフレーズのポジションを求める。KEA-luceneでは1-gram, 2-gram, 3-gramを独立したフィールドにしているので、nextPosition()で取得したポジションはそのまま距離計算に用いて問題ない。 モデルの学習(モデルファイルの出力)KEA-luceneではKEAModelBuilderがモデルの学習を行っている。このプログラムはここからダウンロードできる教師データ(MAUI.tar.gzを展開しさらにその中にあるfao30.tar.gzを展開する)をdata/fao30/ディレクトリに配置されることを前提に書かれているので注意していただきたい。 KEAではモデルの学習は単純ベイズを使っているので、学習には各種統計量をカウントした上で統計量の割り算を行う。割り算を計算しているのは後述のKeyphraseExtractor(2)プログラムであり、KEAModelBuilderプログラムでは統計量を求めているだけなので、正確にはモデルの学習というよりはモデルファイルを出力しているに過ぎない。 KEAModelBuilderが出力するモデルファイルは次のようなスペース区切りのテキストファイルである。 $ head features-model.txt 0.000852285 0.231328 false 0.000284095 0.980489 false 0.000426143 0.0124768 false 0.000426143 0.429134 false 2.01699e-05 0.0479968 false 0.000284095 0.0160665 false 0.000426143 0.726610 false 0.000136409 0.000752663 false 0.000226198 0.379661 false 0.000284095 0.478057 false 1つのレコードは1つのキーフレーズ候補を表している。最初の数値はTF*IDFで、2番目の数値は距離を表している。3列目はキーフレーズ(true)か否か(false)を表現するクラスである。 KEAModelBuilderプログラムは上記のようなモデルファイルを出力するために教師データからLuceneインデックスを作成し、Luceneインデックスを単語辞書とみなしてiterateしながら特徴量を計算しつつ、上記のモデルファイルを出力する。 参考までに処理時間を示しておこう。私のMacBook Pro(Processor 2.3 GHz Intel Core i7)では5分程度かかった。Luceneインデックスを作成するのは一瞬だが、Luceneインデックスを走査しながらモデルファイルを出力するのに少々時間がかかっている。 Rによる連続値の離散化KEAでは特徴量の実数値をそのまま用いるのではなく、MDLP(最小記述長原理)により求めた離散値にマッピングして用いる。ここではMDLPの計算のためにRを使って次のように求めた。 data <- read.table('features-model.txt') mdlp(data)$cutp [[1]] [1] 0.0003538965 0.0013242950 0.0041024750 [[2]] [1] 0.0003553105 0.0144056500 0.0697899000 ここで得られた結果は、次のプログラムKeyphraseExtractor(2)で使用するので、cutp-model.txtというファイル名で保存しておく。 $ cat cutp-model.txt 0.0003538965 0.0013242950 0.0041024750 0.0003553105 0.0144056500 0.0697899000 RによるMDLP計算は同じ環境で30分程度かかった。 Luceneインデックスからのキーフレーズ抽出ではいよいよLuceneインデックスからキーフレーズを抽出してみよう。プログラムはKeyphraseExtractorとKeyphraseExtractor2である。前者はKEAModelBuilderで作成したインデックスのうち試しに既知のファイルa0011e00.txt(このファイル名もプログラム中にハードコーディングしてある)からキーフレーズを抽出するものである。KeyphraseExtractor2の方はfao780(前述のMAUI.tar.gzを展開した中にある)内の未知の文書t0073e.txtからキーフレーズを抽出するプログラムとなっている。 KeyphraseExtractorの方は既存のLuceneインデックスから統計量を得ているのに対し、KeyphraseExtractor2の方は新規の文書から各種統計量を得るためにLuceneインデックスを作成するところから行っている。また、KeyphraseExtractor2の方はモデルに含まれない新規文書であるために、df(P) / N の計算部分で分母、分子とも1が加算されているところも異なる。 KeyphraseExtractorとKeyphraseExtractor2はともにLuceneインデックスに登録済みの文書を特定し、その文書におけるキーフレーズ候補をリストアップしながらスコアを計算する。したがって単語辞書をiterateするよりもLuceneのTermVectorを使いたい。そこでLuceneドキュメント登録は次のようにFieldTypeのsetStoreTermVectors(true)を呼んでいる。 static Document getDocumentWithTermVectors(String fn, String content) throws IOException { Document doc = new Document(); doc.add(new StringField(FILE_NAME, fn, Field.Store.YES)); doc.add(new StoredField(DOC_SIZE_FIELD_NAME, Commons.getDocumentSize(content))); FieldType ft = new FieldType(); ft.setStored(true); ft.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS); ft.setStoreTermVectors(true); doc.add(new Field(Commons.getFieldName(FIELD_NAME, 1), content, ft)); doc.add(new Field(Commons.getFieldName(FIELD_NAME, 2), content, ft)); doc.add(new Field(Commons.getFieldName(FIELD_NAME, 3), content, ft)); return doc; } キーフレーズ候補は前述のスコア計算式によりscore(P,D)が計算され降順にソートされる。KEA-luceneではそのためにLuceneのPriorityQueueを用いている。ところで、KEAでは特徴量として連続値ではなく離散値を使っているため、score(P,D)が多くのキーフレーズ候補間で同点になる事象が論文で指摘されている。その場合はTF*IDFを使って同点決勝を行うということなので、PriorityQueueの実装は次のようになっている。 static class KeyphraseScorePQ extends PriorityQueue またPriorityQueue中下位のキーフレーズ候補が、それより上位のキーフレーズ候補のサブフレーズであった場合ははじかれるように実装する必要もある。 実行結果を見てみよう。KeyphraseExtractorを実行して抽出されたキーフレーズの上位20件は次のようである。なお、括弧内の2つの数値は、スコアとTF*IDF特徴量を表している(スコアが同点の時はTFIDF値が高い方が上位にきているのがわかるだろう)。 animal (0.311211,0.001698) standards setting (0.102368,0.014469) sps (0.102368,0.009091) chains (0.102368,0.008121) food chains (0.102368,0.008038) food safety (0.102368,0.008004) sps standards (0.102368,0.006865) value chain (0.102368,0.005468) setting process (0.102368,0.005250) standards setting process (0.102368,0.004846) livestock food (0.102368,0.004823) oie (0.102368,0.004501) poor to cope (0.102368,0.004442) animal health (0.102368,0.004255) animal production (0.089053,0.000425) consultation (0.076506,0.004025) assisting the poor (0.076506,0.002827) sanitary and technical (0.076506,0.001615) requirements assisting (0.076506,0.001615) dynamics of sanitary (0.076506,0.001615) これに対し人手でつけられたキーフレーズは次のようである(fao30データは複数の人間が思い思いにキーフレーズをつけているので、一回でもキーフレーズと認識されたフレーズをここではリストアップしている)。 $ cat $(find fao30 -name a0011e00.key)|sort|uniq animal health animal production animal products capacity building consumers developing countries development policies disease surveillance domestic markets empowerment fao food chains food safety food security hygiene information dissemination livestock markets meat hygiene mechanics phytosanitary measures poverty public health regulations risk analysis risk management rural population standards technical aid technology trade veterinary hygiene world 両者に共通して現れるキーフレーズは次のようになる。
決して正解率が高いとは言えないが、抽出されたキーフレーズはフレーズとして不自然なものが少ないように見える。自動抽出されたフレーズとしてはいいセン行っているのではないだろうか。 またKeyphraseExtractor2を実行して未知の文書からキーフレーズ抽出した結果は次のようである。同じく上位20件を表示している。 post harvest (0.628181,0.004842) vegetables (0.311211,0.002956) root crops (0.311211,0.002523) fruits (0.311211,0.002316) vegetables and root (0.311211,0.002161) food losses (0.311211,0.001900) harvest food losses (0.311211,0.001582) post harvest food (0.311211,0.001582) fresh produce (0.102368,0.011087) packing (0.102368,0.007199) decay (0.102368,0.005210) containers (0.102368,0.004370) prevention of post (0.089053,0.001231) fruits vegetables (0.089053,0.000472) handling (0.076506,0.002345) storage (0.076506,0.002177) tubers (0.076506,0.001889) marketing (0.076506,0.001474) roots (0.076506,0.001389) potatoes (0.029596,0.003984) 一方で、人手でつけられたキーフレーズは以下である。 $ cat t0073e.key Fruits Marketing Packaging Postharvest losses Postharvest technology Root crops Vegetables こちらも結構妥当なフレーズが抽出されている。情報検索のキーワードサジェスチョンなどに用いるには十分な精度といってもいいだろう。 まとめ本稿は教師あり機械学習でキーフレーズを抽出するKEAを、あえてLuceneライブラリを活用することで実装した。KEAプログラムはアルゴリズムが簡単明瞭なので、キーフレーズ候補の各種統計量を得るためにLucene APIをどのように使えるのか、プログラムから読み取りやすいのではないかと考えている。 読者が本稿を通じてLuceneライブラリへ興味を持ったり、ライブラリへの知識を少しでも広げていただけたなら幸いである。 課題本稿を執筆した最初のプログラムのバージョンでは、以下の項目については時間の制約から実装していない。
KEA-luceneはLucene学習者向けにあえてハードコーディングをしているが、汎用的なものを適宜NLP4Lを通じて提供する予定である。 2015.06.24 Wednesday
日本語Wikipediaからの類義語辞書の自動生成
日本語Wikipediaから Lucene / Solr / Elasticsearch で使える類義語辞書を自動生成する方法を紹介する。 過去にはこちらで説明している方法で行ったりもしたが、今回はいわゆる外来語に限って言葉を収集するもので、はるかに精度がよい(測ってはいないが・・・)。また実装は NLP4L を通じて公開しているので、誰でも試せる。また理屈がわかれば、日本語Wikipediaに限らず収集できる。 日本語文書は次のようにカタカナ語とその語源の英語(等)のアルファベット文字列が近い距離に配置されて書かれることが頻繁にある。 エンターテインメント(英: entertainment)とは、人々を楽しませる娯楽をいう。(Wikipedia「エンターテインメント」より抜粋) そこで文書を大量に収集して互いに近い距離にあるカタカナ語とアルファベット文字列を類義語のペアとして Lucene/Solr に使えるテキストファイルに出力することを考える。しかし、近い場所に書かれているすべてのペアが必ずしも同じ意味を持つとは限らない。たとえば、次のような文章では間違った結果を得てしまう。 コンピューターはFORTRANのおかげでより身近になった。 文章を相当大量に収集してカタカナ語とアルファベット文字列の共起を調べることでも可能かもしれないが、文章を大量に収集することは誰でもできることではない。 NLP4L には英単語とカタカナ語の Transliteration のプログラムと学習データがついている。 このプログラムを使うとカタカナ語から英単語の綴りを推定できる。この推定値と文章から拾ったアルファベット文字列を比較し、文字列が似ていれば(適当に決めた編集距離以下なら)拾ったカタカナ語とアルファベット文字列は同じ意味を持つとして類義語辞書に出力する。 手順は次の通り。
以上の手順は日本語Wikipediaから抽出するやり方だが、社内文書や専門書などからも収集できる。 先にあげたエンターテインメントやインタフェースのように、カタカナ語に表記揺れがある場合も次のように収集できるのも興味深い。 entertainment,エンターテイメント,エンターテインメント interface,インタフェース,インターフェース pennsylvania,ペンシルバニア,ペンシルベニア うまく使えば検索の再現率向上に大いに役立つだろう。 2014.12.19 Friday
Solrを使って専門用語抽出
Solrを使って簡単に専門用語抽出する方法を紹介しよう。 専門用語とは?「専門用語」とは、Wikipediaによれば、「ある特定の職業に従事する者や、ある特定の学問の分野、業界等の間でのみ使用され、通用する言葉・用語群」である。そこで本記事では、ある特定の分野の文書が登録されたインデックスと、別の分野の文書が登録されたインデックスを比較し、互いに相手方のインデックスに含まれない単語集合を表示することで専門用語を抽出することにする。 Solrとインデックスの準備ここで紹介する方法はSolr 1.4 以降であれば動作する(はず)。Solrを持っていない人はダウンロードして用意しよう。そして下記のschema.xmlを指定してSolrを起動する。 <?xml version="1.0" encoding="UTF-8" ?> <schema name="example" version="1.5"> <types> <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/> <fieldType name="int" class="solr.TrieIntField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/> <fieldType name="long" class="solr.TrieLongField" precisionStep="0" positionIncrementGap="0"/> <fieldType name="date" class="solr.TrieDateField" omitNorms="true" precisionStep="0" positionIncrementGap="0"/> <fieldType name="text_ja" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="false"> <analyzer> <tokenizer class="solr.JapaneseTokenizerFactory" mode="search"/> <filter class="solr.JapaneseBaseFormFilterFactory"/> <filter class="solr.CJKWidthFilterFactory"/> <filter class="solr.JapaneseKatakanaStemFilterFactory" minimumLength="4"/> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> </fieldType> </types> <fields> <field name="url" type="string" indexed="true" stored="true" required="true" /> <field name="cat" type="string" indexed="true" stored="true"/> <field name="date" type="date" indexed="true" stored="true"/> <field name="title" type="text_ja" indexed="true" stored="true"/> <field name="body" type="text_ja" indexed="true" stored="false" multiValued="true"/> </fields> <uniqueKey>url</uniqueKey> </schema> 文書データはここではlivedoorニュースコーパスを用いる。以下のようにwgetでlivedoorニュースコーパスを入手して展開しておく。 $ wget http://www.rondhuit.com/download/livedoor-news-data.tar.gz $ tar xvzf livedoor-news-data.tar.gz そしてSolrを起動して展開して得られたファイルのうちひとつ(例としてここでは「独女通信(dokujo-tsushin.xml)」をSolrに登録する。 $ ./post.sh dokujo-tsushin.xmlTermsComponentで単語リストを取得 次に、SolrのTermsComponentを使ってインデックスから単語一覧を取得する。次のcurlコマンドを実行すると、bodyフィールドでの使用頻度の多い順に上位1万語の単語一覧が得られる。 $ curl "http://localhost:8983/solr/collection1/terms?terms.fl=body&terms.limit=10001&omitHeader=true&wt=json&indent=true" | tail -n 10001|head -n 10000|cut -d , -f1|cut -d ¥" -f2 > dokujo-tsushin.txt $ head -n 30 dokujo-tsushin.txt が する た て で と に の は を も だ いる ない ある こと から なる れる か という 人 う や です ます よう ん たい 思う headコマンドで上位30位の頻出単語を表示しているが、前掲のschema.xmlファイルを見ていただくとわかるとおり、ストップワードの処理を一切していないので、およそ検索には役に立たない(そして専門用語でもない)単語が並んでいるのがわかるだろう。 次に異なる分野の文書を登録して同様に単語一覧を表示する。そのためにここでいったんSolrを停止してインデックスを削除し、再度起動して別の分野の文書(ここでは例として「スポーツウォッチ(sports-watch.xml)」)を登録する。そして同様にTermsComponentを使って単語一覧を得る。 $ ./post.sh sports-watch.xml $ curl "http://localhost:8983/solr/collection1/terms?terms.fl=body&terms.limit=10001&omitHeader=true&wt=json&indent=true" | tail -n 10001|head -n 10000|cut -d , -f1|cut -d ¥" -f2 > sports-watch.txt $ head -n 30 sports-watch.txt の は する た に が で と を て だ 日 も いる ない れる なる から ある こと か という です ます ん その 日本 1 選手 語る2つの単語集合を比較する 最後に2つの単語集合を比較して差分を表示するプログラムを書く。ここでは以下のような簡単なScalaプログラムを作成した。 package terms import scala.io.Source object TermsExtractor { def main(args: Array[String]): Unit = { val set1 = termsSet("/Users/koji/work/ldcc/dokujo-tsushin.txt") val set2 = termsSet("/Users/koji/work/ldcc/sports-watch.txt") println(set1 &~ set2) println(set2 &~ set1) } def termsSet(file: String): Set[String] = { val src = Source.fromFile(file, "UTF-8") var result : Set[String] = Set() try{ src.getLines.foreach{ line: String => result = result + line } result } finally { src.close } } } 実行すると次のように2行の出力が得られる。1行目は「独女通信」にのみ出現する単語であり、2行目は「スポーツウォッチ」にだけ出現する単語である。頻出していたストップワードっぽい単語はきれいに消えていることがわかる。 Set(今風, 味の素, 呼び名, 愛車, ヨネスケ, 週末, 空き, 華子, 姓名, 時折, 越える, サン, 思い当たる, はしご, ロードショ, 新鮮味, 調べる, 札, 執筆, 麻美, 養子, 駅, ミッション, フミ, 初恋, 鉄則, 落とし穴, さゆり, メモリ, 飲み食い, 和子, 避妊, 苦痛, アカシックレコードリーディング, ラッキ, 本体, 真美子, よそ見, マザー, 尚更, だれ, 盛, sweet, 余震, 心得, アドレス, 器用貧乏, 渋谷, 次男, 法的, 知人, 集まり, ミニサイズ, 皆無, へえ, 眺める, 洋子, 膨大, あっぱれ, 数少ない, ハッピ, 手数料, ローラ, 安価, 波風, 甲斐, 統計, 労働省, 主演, ntt, 以下省略) Set(緊迫, かしぐ, ほとぼり, 在米, フジ, 見れる, フェンシング, 連夜, 被, 205, 兼備, 2014, 藤田, 長島, 曙, 颯爽, プロレスラ, セーフコ, 説く, はしる, 知良, アスリートファン, 鳥肌, 主審, 沸く, 周知, ちょう, 円熟, オールナイト, 卓, あしらい, 不用意, 栗山, 火蓋, やらす, 弘和, 修一, 一角, 平泳ぎ, 98, ゴールデンゴールズ, 演説, 敗, 今井, アリエン, スプリングス, 戦後, beautiful, しょうもない, 113, 敗者, 野, 仰天, 素性, 聖, sunday, 井端, イーグルススカウト, 批評, スルーパス, 前線, 弁解, ユース, クロー, 人情, 用水, やぎ, cb, クーラ, 興行, 選考, mmaplanet, きまる, 哲也, 織る, ラピュタ, グルノーブル, ロマン, クロアチア, brad, 山里, wbc, 権限, pm, ぶっつける, 抜擢, いづみ, アフリカ, 以下省略) これをもってそれぞれの分野の専門用語であると言い切るには難があるが、どちらかが「独女通信」でどちらかが「スポーツウォッチ」だという2択の問題を出されたならば、ほとんどの人が正解するくらいには専門用語っぽい出力が得られているといえるのではないだろうか。 2014.12.04 Thursday
word2vec for Lucene ご紹介
Luceneインデックスをコーパスとみなして動作する word2vec for Lucene を開発したのでその使い方を紹介しよう。word2vec は Tomas Mikolov らによって提案/開発された単語をベクトルに変換するOSSツールである。オリジナルの word2vec は自然言語で書かれたテキストファイルを読み込み、単語ベクトルを出力するようになっている。 word2vec for Lucene はテキストファイルの代わりにLuceneインデックスを入力コーパスとして用いる。オリジナルの word2vec が扱うテキストファイルは単語単位で分かち書きされている必要がある。この点は英語テキストなどでは問題ないが、単語で分かち書きされていない日本語などではあらかじめMeCabなどの形態素解析器や分かち書きツールを用いて単語で分かち書きされたテキストファイルを用意しておかなければならない。また英語であっても大文字→小文字変換や単語テキストの直後に続くピリオド/カンマの削除など、事前の正規化作業が必要になってくる。word2vec for Lucene では Luceneインデックスを入力コーパスとして用いるため、事前の正規化作業が不要である。 以下では簡単に試していただけるよう、word2vec for Lucene のデモを実行する手順を説明する。大まかな手順は:
1.word2vec for Lucene のダウンロード という感じである。すでにLuceneインデックスを持っている方は手順2〜3の代わりに手持ちのLuceneインデックスを手順4で指定することにしてもよい。word2vec for LuceneはLucene 4.10.2ベースで作成したが、(試していないが)たいていのLucene 4.x インデックスで動作するものと思われる。したがって、Solr 4.x はもちろんのこと、Elasticsearch(バージョンはよく知らないが1.x系以降)で作成したインデックスも指定できる。 手順5で楽しい結果を得るためにはそれなりに大きなコーパスが必要である。数百万文書など大きなLuceneインデックスを持っている方はぜひ word2vec for Lucene を試していただきたい。 以下手順を示すが、簡単のために /Users/koji/work ディレクトリ以下に Solr 環境と word2vec for Lucene 環境をインストールする想定で話を進める。 手順1.word2vec for Lucene のダウンロード$ pwd /Users/koji/work $ git clone https://github.com/kojisekig/word2vec-lucene.git手順2.デモデータの準備 $ pwd /Users/koji/work $ cd word2vec-lucene # オリジナルの word2vec でも使用されている英語コーパス text8 をダウンロードして Solr 形式に変換 $ ant t8-solr $ ls -l text8* -rw-r--r-- 1 koji staff 100000000 6 9 2006 text8 -rw-r--r-- 1 koji staff 100017005 12 3 13:44 text8.txt -rw-r--r-- 1 koji staff 100017078 12 3 13:44 text8.xml -rw-r--r-- 1 koji staff 31344016 12 3 13:40 text8.zip手順3.Lucene/Solrインデックスの準備(デモデータの登録) コンソールを2つ用意して最初のコンソールでSolrを起動する。 $ pwd /Users/koji/work # Solrのダウンロード $ wget http://ftp.meisei-u.ac.jp/mirror/apache/dist/lucene/solr/4.10.2/solr-4.10.2.tgz $ tar xvzf solr-4.10.2.tgz # Solrの起動 $ cd solr-4.10.2/example $ java -Dsolr.solr.home=/Users/koji/work/word2vec-lucene/solrhome -Dsolr.dir=/Users/koji/word/solr-4.10.2 -jar start.jar 2つめのコンソールではtext8コーパスをSolrに登録する。 $ pwd /Users/koji/work $ cd word2vec-lucene # Solr形式に変換した英語コーパスをSolrに登録 $ ./post.sh collection1 text8.xml手順4.単語ベクトルの作成 demo-word2vec.sh スクリプトにSolrコア名を指定してword2vecを実行する。実行結果はvectors.txtというファイルに出力される。 $ pwd /Users/koji/work $ cd word2vec-lucene $ ./demo-word2vec.sh collection1 $ ls -l vectors.txt -rw-r--r-- 1 koji staff 136053041 12 3 15:31 vectors.txt手順5.単語ベクトルで遊ぶ demo-distance.sh を実行するとvectors.txtファイルを読み込んで入力待ちになる。そこで単語を入力すると、その単語に最も近い(コサインを計算)ベクトルを持つ単語上位40個を表示する。 $ ./demo-distance.sh cat Word: cat Position in vocabulary: 2601 Word Cosine distance ------------------------------------------------------------------------ cats 0.511078 dog 0.471308 dogs 0.469539 sighthound 0.452233 bobtail 0.436424 tapir 0.424105 demo-analogy.sh を実行するとvectors.txtファイルを読み込んで入力待ちになるので、3つの単語をスペース区切りで入力する。以下ではman king womanと入力しているが、「manに対するkingの関係を、womanに適用するとどうなるか」という意味である。するとqueenが期待値になるが、実行結果もそのようになる。 $ ./demo-analogy.sh man king woman日本語で試す場合 以上の手順はオリジナルの word2vec でも使用している text8 という英語の正規化済みのコーパスを使った手順であるが、以下では日本語の(正規化済みでない)コーパスを使った手順も紹介しよう。デモデータは livedoor ニュースコーパスを用いる。ロンウイットのサイトから Solr ネイティブ形式に加工した livedoor ニュースコーパスをダウンロードする。 $ pwd /Users/koji/work $ cd word2vec-lucene $ mkdir work $ cd work $ pwd /Users/koji/work/word2vec-lucene/work $ wget http://www.rondhuit.com/download/livedoor-news-data.tar.gz $ tar xvzf livedoor-news-data.tar.gz $ cd .. ダウンロードした livedoorニュースコーパスをSolrに登録し、word2vecを実行する。コア名にはldccを指定する。また、-aオプションでLuceneのAnalyzerクラスを指定する。また以下では-fオプションで出力先ベクトルファイル名を指定している(指定しないとデフォルトのvectors.txtとなってしまい、先ほど作成した英語の単語ベクトルファイルを上書きしてしまう)。 $ ./post.sh ldcc work/*.xml $ ./demo-word2vec.sh ldcc -a org.apache.lucene.analysis.ja.JapaneseAnalyzer -f vectors-ldcc.txt $ ./demo-distance.sh -f vectors-ldcc.txt 結婚 野球デモデータ以外のLuceneインデックスで試す場合の注意点 word2vec for Lucene で配布しているデモ実行スクリプト demo-word2vec.sh を見てもらうとわかるように、いくつかのパラメータがハードコーディングで指定されている。以下はLuceneに関連するパラメータである。これを手持ちのLuceneインデックスに合うように適宜指定しなおせばLucene 4.x インデックスのデータでword2vecが動くはずである。
-analyzer オプションについては、storedデータではなくTermVectorでもよかったのだが、TermVectorを保存しているLuceneインデックスは少なく、処理速度的にも疑問だったため、storedデータをここで指定するLuceneのAnalyzerクラスでanalyze する方針をとった。ではなぜindexedでなければいけないかというと、(オリジナル実装でも)word2vecではコーパスを2度読んでいて、1回目の読み込みで単語表を作成しているが、indexedであればそこは一瞬で完了し、なんといってもLuceneっぽいからである。また、word2vecで解析したくなるようなLuceneフィールドはたいていindexedでかつstoredと考えられるので、多くのLuceneインデックスオーナーにはこの条件は受け入れられるはずである。 相当数の文書が入っているLuceneインデックスを持っている方は、デモデータで動作を確認したあと、ぜひword2vec for Luceneを手持ちのLuceneインデックス上で試していただき、どんな結果になったか教えていただければありがたい。 2014.01.30 Thursday
テキストアノテーションツール brat がすごい件
brat は OSS のテキストアノテーションツールである。GUI(ビジュアルと操作性)とできあがりのアノテーション情報のわかりやすさ(シンプルなフォーマットが自作NLPプログラムから扱いやすい!)、導入の簡単さがすばらしいのでここで紹介しよう。 brat のインストールApache などの Web サーバ上で動かして本格的に使う方法と、個人的にさくっと立ち上げて使う方法があり、ここでは後者を紹介する。この場合インストールは簡単で、brat を適当なディレクトリにダウンロードして展開する。 $ mkdir work $ cd work $ wget http://weaver.nlplab.org/~brat/releases/brat-v1.3_Crunchy_Frog.tar.gz $ tar xvzf brat-v1.3_Crunchy_Frog.tar.gzそして、次のようにインストールのスクリプトを実行して質問に答えれば終わりである。 $ cd brat-v1.3_Crunchy_Frog $ ./install.sh # brat にログインするユーザ名を新規設定する。 Please the user name that you want to use when logging into brat editor # パスワードを入れる。 Please enter a brat password (this shows on screen) annotate # メールアドレスを入れる。 Please enter the administrator contact email example@brat.orgコーパスの準備 次にアノテーションを入れる対象のコーパスを data ディレクトリの下に用意する。ここでは livedoorニュースコーパスを使った例を示す。 $ wget http://www.rondhuit.com/download/ldcc-20120915.tar.gz $ tar xvzf ldcc-20120915.tar.gzコーパスファイルは UTF-8 で保存され、.txt の拡張子でなければならない。さらに、次のようにアノテーションを実行する .txt ファイルひとつひとつに対応する空の .ann ファイルをあらかじめ用意しておく必要がある。 $ find text -name '*.txt' | sed -e 's|¥.txt|.ann|g' | xargs touch固有表現タグ設定と brat の起動 brat は係り受けのような構造的アノテーションもつけることができるが、本記事では固有表現タグをつける例を示す。デフォルトの状態でもいくつかの固有表現タグが設定されているが、ここでは独自タグを設定してみる。livedoorニュースコーパスを tar で展開したときにできた text ディレクトリの直下に、次のように2つの設定ファイルを作成する。 $ cd text $ cat > annotation.conf <<EOF > [entities] > ORGANIZATION > FACILITY > PERSON > TITLE > LOCATION > > [relations] > [events] > [attributes] > EOF $ $ cat > visual.conf <<EOF > [labels] > > [drawing] > ORGANIZATION bgColor:#8fb2ff > FACILITY bgColor:#aaaaee > PERSON bgColor:#ffccaa > TITLE bgColor:#7fe2ff > LOCATION bgColor:#6fffdf > EOF最初の設定ファイルはタグの設定、2つめはタグの色の設定である。なおこれらは必要最低限の設定である。設定ファイルを完璧に設定したい方は、brat ディレクトリ内に設定ファイルがあるので、そちらを参照していただきたい。 以上の設定が終われば、brat をスタンドアロンモードで起動できる。なお、brat は Python で書かれているので、Python の環境が必要である。 $ cd ../.. $ python standalone.pybrat へのログイン
上の起動時に表示された URL http://127.0.0.1:8001 にブラウザからアクセスする。すると、次のような画面が表示される。
ログインしたらコーパスコレクションを選択する。それにはタブキーを押下するか、上のメニューから [Collection] を選んでクリックする。すると、次のコーパスコレクション選択画面になる。 このときすでに、サーバサイドの対応する .ann ファイルには次のようにアノテーション情報がシンプルなフォーマットで記録されている。 $ cat movie-enter-5840081.ann T1 PERSON 81 86 宮崎あおい T2 PERSON 87 90 堺雅人 T3 TITLE 1046 1048 さん T4 PERSON 447 450 堺雅人 T5 PERSON 975 977 宮崎 T6 TITLE 977 979 さん T7 PERSON 980 981 堺 T8 TITLE 981 983 さん T9 PERSON 1021 1023 宮崎 T10 PERSON 1196 1197 堺 T11 PERSON 1245 1247 宮崎 T12 PERSON 1254 1255 堺 T13 PERSON 947 949 宮崎タグをつけた順番で記録されているので、プログラムで読み込むときは、3〜4列でソートする必要があるだろう。
SolrとMahoutのトレーニングコース、ただいま2月の受講者を募集中です! 2012.08.11 Saturday
CaboChaとKNBコーパスで固有表現抽出を行う
さらにCaboChaの評価は続く。今回はKNBコーパスを使って固有表現抽出の交差検定(5分割)を行った。
今回はprecisionやrecallをカウントする評価ツールも自作した。実際にどのようにカウントするかをフィクションのデータを例にした下表を使って説明する:
以上から、このフィクションのデータの例では: precision = tp / (tp + fp) = 2 / (2 + 2) = 0.5 recall = tp / (tp + fn) = 2 / (2 + 2) = 0.5 F = 2PR / (P + R) = 2 * 0.5 * 0.5 / (0.5 + 0.5) = 0.5 (ここで、P:precision, R:recall) 結果 結果は予想していた通りF値で40から60ポイント程度と低い値となった。原因としては、KNBコーパスはデータ量が多くないがその中でも固有表現タグはさらに数が限定されるためと考えられる。また、モデルファイルのとりかたによって固有表現タグの出現に濃淡があり、そのため正解率にばらつきがある。固有表現タグの種類(ORGANIZATIONやLOCATIONなど)にも数の多い少ないがあり(詳細はKNBコーパスの論文を参照)、これらも影響している可能性があるだろう。さらに、学習データを作成する際に、複数のNEタグ候補があるOPTIONALタグをそのままOPTIONALタグとして出力した。複数候補をCaboChaに学習させる方法がわからなかったためだが、これも学習データの量を減少させ、正解率を落とした原因といえる(このため、評価ツールでは正解データがOPTIONALの場合、fnをカウントしないこととした)。いずれにしてもデータ量が少ないのが残念だ。
今回作成したプログラム KNBコーパスをNEタグ付きのCaboCha形式に変更するプログラム knbc2cabocha_ne.rb と 評価プログラム eval_ne.rb を作成したので掲載する: knbc2cabocha_ne.rb このプログラムでは、OPTIONALタグはそのままOPTIONALタグとして出力した。しかし、(おそらくは)CaboChaではOPTIONALタグはスキップしており(?)、OPTIONALの実際の候補であるLOCATIONやORGANIZATIONなどを出力すべきであると考えられる。そのため、次の評価プログラムでは正解データがOPTIONALの場合は、fnはカウントしないようにした。 KNBC_DIR = ARGV[0] CATEGORIES = ["Keitai", "Kyoto", "Gourmet", "Sports"] def ne(terms) ne_m = / eval_ne.rb def next_term_ne(file) while line = file.gets next if /¥A¥*/ =~ line return "EOS", nil if /¥AEOS/ =~ line terms = line.split return terms[0], terms[-1] end return nil, nil end def no_ne(ne_a) return true if ne_a.length == 1 && ne_a[0] == nil ne_a.each{ |ne| return false unless ne == "O" } true end def print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a) if ans_str_a != out_str_a || ans_ne_a != out_ne_a puts "#{ans_str_a.join("/")}(#{ans_ne_a.join(",")}) #{out_str_a.join("/")}(#{out_ne_a.join(",")})" end end def num_ne(ne_a) return 0 if ne_a.length == 1 && ne_a[0] == nil num = 0 ne_a.each{ |ne| num += 1 unless ne == "O" } num end def check(ans_str_a, ans_ne_a, out_str_a, out_ne_a, tp, fp, fn) if !no_ne(ans_ne_a) print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a) if !no_ne(out_ne_a) i = 0 while true ans_ne_c = i < ans_ne_a.length ? ans_ne_a[i] : "" out_ne_c = i < out_ne_a.length ? out_ne_a[i] : "" break if ans_ne_c == "" && out_ne_c == "" if ans_ne_c != "O" && out_ne_c != "O" if ans_ne_c == out_ne_c tp += 1 else fn += 1 if ans_ne_c != "O" && ans_ne_c != "" && ans_ne_c.index("OPTIONAL") != nil fp += 1 if out_ne_c != "O" && out_ne_c != "" end end i += 1 end else fn += num_ne(ans_ne_a) end elsif !no_ne(out_ne_a) fp += num_ne(out_ne_a) print_terms(ans_str_a, ans_ne_a, out_str_a, out_ne_a) end return tp, fp, fn end ans_file = File.open(ARGV[0]) out_file = File.open(ARGV[1]) tp = fp = fn = 0 while true ans_str_a = [] ans_ne_a = [] out_str_a = [] out_ne_a = [] ans_term, ans_ne = next_term_ne(ans_file) ans_str_a << ans_term ans_ne_a << ans_ne out_term, out_ne = next_term_ne(out_file) out_str_a << out_term out_ne_a << out_ne break if ans_term == nil || out_term == nil while out_str_a.join("").length != ans_str_a.join("").length if out_str_a.join("").length > ans_str_a.join("").length ans_term, ans_ne = next_term_ne(ans_file) ans_str_a << ans_term ans_ne_a << ans_ne elsif out_str_a.join("").length < ans_str_a.join("").length out_term, out_ne = next_term_ne(out_file) out_str_a << out_term out_ne_a << out_ne end end tp, fp, fn = check(ans_str_a, ans_ne_a, out_str_a, out_ne_a, tp, fp, fn) end ans_file.close out_file.close printf("tp = %d, fp = %d, fn = %d¥n", tp, fp, fn) precision = tp.to_f / (tp.to_f + fp.to_f) recall = tp.to_f / (tp.to_f + fn.to_f) f = 2.0 * precision * recall / (precision + recall) printf("precision = %f, recall = %f, F = %f¥n", precision, recall, f) オペレーションメモ # KNBコーパスをNEタグ付きCaboCha形式に変更。 $ ruby knbc2cabocha_ne.rb ../KNBC_v1.0_090925/corpus1 # NEタグ付きCaboCha形式のデータをマージしながら同程度の5つに分割。 $ ruby rearrange.rb 4 837 Keitai.dat Kyoto.dat Gourmet.dat Sports.dat # モデルのソースを生成。 $ cat G0402.dat G0403.dat G0404.dat G0405.dat > model.source.ne.1 $ cat G0401.dat G0403.dat G0404.dat G0405.dat > model.source.ne.2 $ cat G0401.dat G0402.dat G0404.dat G0405.dat > model.source.ne.3 $ cat G0401.dat G0402.dat G0403.dat G0405.dat > model.source.ne.4 $ cat G0401.dat G0402.dat G0403.dat G0404.dat > model.source.ne.5 # NEを学習。 $ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.1 ne.juman.1 $ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.2 ne.juman.2 $ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.3 ne.juman.3 $ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.4 ne.juman.4 $ /usr/local/libexec/cabocha/cabocha-learn -e ne -P JUMAN model.source.ne.5 ne.juman.5 # CaboCha形式のデータから原文テキストを逆再生。 $ ruby make_source.rb 0401 $ ruby make_source.rb 0402 $ ruby make_source.rb 0403 $ ruby make_source.rb 0404 $ ruby make_source.rb 0405 # cabocha -e ne で固有表現抽出。 $ cat S0401-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 > O0401.txt $ cat S0401-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 >> O0401.txt $ cat S0401-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.1 -O1 -f1 -n1 >> O0401.txt $ cat S0402-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 > O0402.txt $ cat S0402-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 >> O0402.txt $ cat S0402-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.2 -O1 -f1 -n1 >> O0402.txt $ cat S0403-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 > O0403.txt $ cat S0403-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 >> O0403.txt $ cat S0403-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.3 -O1 -f1 -n1 >> O0403.txt $ cat S0404-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 > O0404.txt $ cat S0404-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 >> O0404.txt $ cat S0404-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.4 -O1 -f1 -n1 >> O0404.txt $ cat S0405-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 > O0405.txt $ cat S0405-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 >> O0405.txt $ cat S0405-03.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./ne.juman.5 -O1 -f1 -n1 >> O0405.txt # 正解データと比較。 $ ruby eval_ne.rb G0401.dat O0401.txt $ ruby eval_ne.rb G0402.dat O0402.txt $ ruby eval_ne.rb G0403.dat O0403.txt $ ruby eval_ne.rb G0404.dat O0404.txt $ ruby eval_ne.rb G0405.dat O0405.txt 2012.08.10 Friday
CaboChaでKNBコーパスを使う
またまたCaboChaの続きで、今回は京都コーパスではなくKNBコーパス(KNBC)を使う。KNBコーパスは京都コーパスと異なり、タグ情報だけでなく本文情報も含まれているので、この記事の通りのオペレーションを行えば、(途中までは)毎日新聞データを持たない読者も実際にCaboChaの学習や評価を試すことができる。
KNBコーパスにはさまざまな特徴があるが、ここで注目するのは京都コーパスと比較しての下記の点である:
KNBコーパスは4つのカテゴリ「携帯電話」「京都観光」「グルメ」「スポーツ」のどれかについて書かれたブログ記事に文境界、形態素、係り受け、格・省略・照応、固有表現、評価表現の各種アノテーションを付加した解析済みコーパスである。毎日新聞の記事データにアノテーションを付加した京都コーパスに比べデータ量が約一桁少ない(京都コーパス38400文に対し、KNBコーパスは4184文)。また、ブログ記事ということで新聞記事と比べて「文境界があいまい」「構文構造の解析を困難にする括弧表現」「誤字、方言、顔文字などの多様な形態素」というCGM的な特徴を有する。 このような特徴を踏まえ、今回は次のような評価を行った:
1.はKNBコーパス内部の学習・評価を行うものである。ここでは簡単に522文からなるスポーツカテゴリを評価用に残し、それ以外のカテゴリである携帯電話、京都観光、グルメの全合計3662文を学習することにした。量的には前の記事で作成した chunk.juman.10.1 モデルファイルでのテスト(3200文;エラー率0.2116)に近い量である。 2.と3.はブログ記事と新聞記事のクロス的・相互的な学習・評価のテストであり、ブログ記事を学習してモデルファイルを作成し新聞記事を解析した場合やその逆を行ってみて、解析性能がどの程度になるか見るものである。 結果 前の記事と比較のため、また、自作したツール類が再利用できることなどから、同様にCaboChaのchunkを対象に評価を行った。結果は下表の通り:
KNBC-KNBCテストでは、ブログ記事の3662文を学習したモデルが、量的にはそれよりも少ない京都コーパス3200文を学習したモデルよりもエラー率が高くなった。 KNBC-KCテストは全ブログ記事のコーパスを学習したモデルで新聞記事を解析するテストだが、やはりエラー率はさらに上がった。 KC-KNBCテストはKNBC-KCテストとは逆に、新聞記事である京都コーパスを学習したモデルでブログ記事を解析するテストであるが、同様にエラー率は高かった。 今回作成したプログラム KNBコーパスのcorpus1ディレクトリを第一引数に指定するとコーパスデータをCaboCha形式のKeitai.dat, Kyoto.dat, Gourmet.dat, Sports.datのデータに出力する knbc2cabocha_chunk.rb というプログラムを作成した: KNBC_DIR = ARGV[0] CATEGORIES = ["Keitai", "Kyoto", "Gourmet", "Sports"] CATEGORIES.each{|category| File.open("#{category}.dat", "w"){|ofile| Dir.glob(KNBC_DIR+"/KN???_#{category}_*").sort!.each{|article_dir_path| File.open("#{article_dir_path}/fileinfos"){|info_file| while info_line = info_file.gets File.open("#{article_dir_path}/#{info_line.split[1].split(/:/)[1]}"){|ifile| chunk_num = 0 while line = ifile.gets next if /¥A# S-ID:/ =~ line # skip the comment line terms = line.split if terms[0] == "*" # ofile.puts "* #{chunk_num} #{terms[1]}" ofile.puts "* #{chunk_num} -1D" chunk_num += 1 elsif terms[0] == "+" next elsif terms[0] == "EOS" ofile.puts "EOS" else ofile.puts "#{terms[0]}¥t#{terms[3]},#{terms[5]},*,*,#{terms[2]},#{terms[1]},*" end end } end } } } } オペレーションメモ # KNBコーパスをCaboCha形式に変形しつつ、4つのカテゴリごとのファイルに分ける。 $ ruby knbc2cabocha_chunk.rb ../KNBC_v1.0_090925/corpus1 # 前の記事の習慣に合わせリネーム。 $ mv Keitai.dat G41.dat $ mv Kyoto.dat G42.dat $ mv Gourmet.dat G43.dat $ mv Sports.dat G44.dat # [KNBC-KNBC] モデルのソースを作成。 $ cat G4[1-3].dat > model.source.knbc-knbc # [KNBC-KNBC] 学習。 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.knbc-knbc chunk.juman.knbc-knbc # [KNBC-KNBC] CaboCha形式の学習データG44.datから原文テキストファイルS44-x.txtを逆再生 $ ruby make_source.rb 44 # [KNBC-KNBC] cabochaの実行。 $ cat S44-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.knbc-knbc -O2 -f1 -n0 > O44.txt $ cat S44-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.knbc-knbc -O2 -f1 -n0 >> O44.txt # [KNBC-KNBC] 正解データとcabochaの出力を比較用に正規化。 $ ruby norm_chunk.rb G44.dat > G44-norm.dat $ ruby norm_chunk.rb O44.txt > O44-norm.txt # [KNBC-KNBC] エラー率の表示。 $ ruby diff_chunk.rb G44-norm.dat O44-norm.txt error = 0.2989 (156/522) # ----------- # [KNBC-KC] モデルのソースを作成。 $ cat G4[1-4].dat > model.source.knbc-kc # [KNBC-KC] 学習。 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.knbc-kc chunk.juman.knbc-kc # [KNBC-KC] cabochaの実行。 $ cat S1011-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.knbc-kc -O2 -f1 -n0 > O1011.txt $ cat S1011-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.knbc-kc -O2 -f1 -n0 >> O1011.txt : $ cat S1011-16.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.knbc-kc -O2 -f1 -n0 >> O1011.txt # [KNBC-KC] cabochaの出力を比較用に正規化。 $ ruby norm_chunk.rb O1011.txt > O1011-norm.txt # [KNBC-KC] エラー率の表示。 $ ruby diff_chunk.rb G1011-norm.dat O1011-norm.txt error = 0.3214 (2057/6400) # ----------- # [KC-KNBC] cabochaの実行。 $ cat S44-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.10 -O2 -f1 -n0 > O1044.txt $ cat S44-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.10 -O2 -f1 -n0 >> O1044.txt # [KC-KNBC] cabochaの出力を比較用に正規化。 $ ruby norm_chunk.rb O1044.txt > O1044-norm.txt # [KC-KNBC] エラー率の表示。 $ ruby diff_chunk.rb G44-norm.dat O1044-norm.txt error = 0.2969 (155/522) 2012.08.08 Wednesday
モデルとなる京都コーパスの量を変えたときのCaboChaの性能を見る
CaboCha+京都コーパスの続きである。
前の記事ではdat/syn/ ディレクトリにある拡張子が.KNPのファイル28個を上から6個、6個、6個、5個、5個ずつまとめて交差検定を行った。すると、学習データの量に偏りが出たせいか、エラー率は12%から18%とばらつきがあった。 そこで今度は学習データのファイルを文の個数で正確に分割し、学習データの量とchunkの正解率の変化を見てみることにした。具体的には、データが38400文あったのでこれを12分割(すると38400/12=3200となる)し、最後の2つ分、6400文を正解データと固定して、残りの10個分をモデルファイルのソースとし、これを1つ分、2つ分、... と増やしていったときのエラー率に注目する。 結果は期待通り、データ量が増えるにしたがいエラー率が下がった:
ここで、chunk.juman.10.1は3200文、chunk.juman.10.2は6400文と学習データ量が増えていき、chunk.juman.10.10は32000文である。グラフ化すると次の通り: 今回作成したプログラム G5[1-5].datという5つのファイルから同じ数の文に再整理して出力する rearrange.rb というプログラムを作成した。 rearrange.rb TARGET_NUM = ARGV[0].to_i SENTENCE_NUM = ARGV[1].to_i def target_file_name(num) sprintf("G%02d%02d.dat", TARGET_NUM, num) end $ai = 1 $ifile = nil $end_of_sources = false def next_source $ifile.close if $ifile $ai = $ai + 1 return nil if $ai >= ARGV.length puts "reading #{ARGV[$ai]} ..." $ifile = File.open(ARGV[$ai]) end def read_sentence sentence = Array.new while true do while line = $ifile.gets do sentence << line return sentence if /¥AEOS/ =~ line end unless next_source $end_of_sources = true return [] end end end return nil if $ai >= ARGV.length puts "reading #{ARGV[$ai]} ..." $ifile = File.open(ARGV[$ai]) end def read_sentence sentence = Array.new while true do while line = $ifile.gets do sentence << line return sentence if /¥AEOS/ =~ line end unless next_source $end_of_sources = true return [] end end end # create target files - for model files next_source TARGET_NUM.times {|tnum| snum = 0 File.open(target_file_name(tnum+1), "w"){|ofile| while snum < SENTENCE_NUM read_sentence.each{|line| ofile.puts line } snum = snum + 1 end } } # also create a target file - for evaluation File.open(target_file_name(TARGET_NUM+1), "w"){|ofile| until $end_of_sources do read_sentence.each{|line| ofile.puts line } end } オペレーションメモ # 学習データと評価用データの再作成 # 第1引数: ターゲット学習データの個数 # 第2引数: 1個あたりのターゲット学習データ内の文の個数 # 第3引数以降: CaboCha形式ソースデータファイル $ ruby rearrange.rb 10 3200 G5?.dat # モデルのソースを生成 $ cat G100[1].dat > model.source.10.1 $ cat G100[1-2].dat > model.source.10.2 $ cat G100[1-3].dat > model.source.10.3 $ cat G100[1-4].dat > model.source.10.4 $ cat G100[1-5].dat > model.source.10.5 $ cat G100[1-6].dat > model.source.10.6 $ cat G100[1-7].dat > model.source.10.7 $ cat G100[1-8].dat > model.source.10.8 $ cat G100[1-9].dat > model.source.10.9 $ cat G100[1-9].dat G1010.dat > model.source.10.10 # 学習。規模に応じてだんだん時間がかかる(20秒〜150秒程度) $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.1 chunk.juman.10.1 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.2 chunk.juman.10.2 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.3 chunk.juman.10.3 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.4 chunk.juman.10.4 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.5 chunk.juman.10.5 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.6 chunk.juman.10.6 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.7 chunk.juman.10.7 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.8 chunk.juman.10.8 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.9 chunk.juman.10.9 $ /usr/local/libexec/cabocha/cabocha-learn -e chunk -P JUMAN model.source.10.10 chunk.juman.10.10 # CaboCha形式の学習データG1011.datから原文テキストファイルS1011-x.txtを逆再生 $ ruby make_source.rb 1011 # 各モデルファイルを使い原文テキストS1011-x.datに対してcabochaを実行 $ cat S1011-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.1 -O2 -f1 -n0 > O10A01.txt $ cat S1011-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.1 -O2 -f1 -n0 >> O10A01.txt : $ cat S1011-01.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.2 -O2 -f1 -n0 > O10A02.txt $ cat S1011-02.txt | cabocha -d /usr/local/lib/mecab/dic/jumandic -M ./chunk.juman.10.2 -O2 -f1 -n0 >> O10A02.txt : # 正解データの正規化 $ ruby norm_chunk.rb G1011.dat > G1011-norm.dat # cabochaの出力を正規化 $ ruby norm_chunk.rb O10A01.txt > O10A01-norm.txt $ ruby norm_chunk.rb O10A02.txt > O10A02-norm.txt : $ ruby norm_chunk.rb O10A10.txt > O10A10-norm.txt # モデルファイル量ごとの評価(エラー率の表示) $ ruby diff_chunk.rb G1011-norm.dat O10A01-norm.txt error = 0.2116 (1354/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A02-norm.txt error = 0.1861 (1191/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A03-norm.txt error = 0.1708 (1093/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A04-norm.txt error = 0.1598 (1023/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A05-norm.txt error = 0.1509 (966/6400) $ ruby .diff_chunk.rb G1011-norm.dat O10A06-norm.txt error = 0.1448 (927/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A07-norm.txt error = 0.1383 (885/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A08-norm.txt error = 0.1341 (858/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A09-norm.txt error = 0.1272 (814/6400) $ ruby diff_chunk.rb G1011-norm.dat O10A10-norm.txt error = 0.1259 (806/6400) 2012.07.28 Saturday
CaboChaと京都コーパスで交差検定
ロンドンオリンピック真っ最中で仕事がはかどらない中、日本語係り受け解析器CaboChaと京都コーパスで交差検定を行ったのでメモを残す。
この作業の目的は、本件交差検定というよりはどちらかというとこの作業を通じてCaboChaの学習や評価のツールを正しく使えるように私自身が練習し、作業記録を残すことである。また、必要に応じてテキスト処理のためのツールを整備することも行う。 以下の作業では京都コーパスの方は毎日新聞のCD-ROMを用意し、付属のauto_convを実行した後のデータがあることを前提とする。なおこの方法で作成したモデルファイルは(そしてCaboChaが現在デフォルトで内包しているモデルファイルも)学術研究用に利用が限定されることをここで念のため書き添えておく。 CaboChaのツールで共通して使えるmode(-eオプション)はchunk,dep,neのうち、chunkを用いることにした。depは学習(cabocha-learnの実行)に4時間かけても終わらなかったため、残念ながら時間的な制約から今回は作業対象からはずした。また、入手できたデータにneタグがついていなかったため、neも同様に対象外とした。 文字コードはコーパスに合わせてEUCを選択。京都コーパスのdat/syn/ ディレクトリにある拡張子が.KNPの全ファイルをファイル単位で5分割して交差検定を行う。ファイル数は28個なのでこれを上から6個、6個、6個、5個、5個ずつまとめて各グループとし(下表)、5グループのCaboCha形式の学習データを得る。なお、5分割の5の根拠は特にない。
またG5N.datのCaboCha形式の学習データから、1カラム目の単語情報を抜き出して原文テキストを逆再生し、それをS5N-x.txtというテキストファイルにする。ここで"-x"の部分は"-01"から"-20"程度の値をとるように分割する(G5N.datを対応するひとつの原文テキストファイルにするとcabochaコマンドで解析するには大きすぎるため、複数個のS5N-x.txtファイルに分割している)。評価時のことも考慮すると、G5N.datはS5N-x.txtと対応する正解データのファイルG5N-eval-x.datに分けておく(ただし、評価は後述するようにCaboCha付属の評価ツールが使えなかったので、結果としてG5N-eval-x.datは不要になった)。これらのファイルを出力するプログラムmake_source.rbは別途自作した。 上記のCaboCha形式の学習データを1つ残して4つずつまとめて学習させ、下表の5通りのモデルファイルと正解データのペアを得る。
また、Juman辞書のバージョン(?)の違いのせいか、はたまた形態素解析器の違いのせいかどうかはわからないが、京都コーパスの形態素の単位と、今回cabochaで原文テキストを解析したときの形態素の単位が異なってしまい、結果としてCaboChaの評価ツールcabocha-system-evalが動かなかった(tree size is differentやToken size is differentというエラーになる)。形態素の単位が異なる部分としては、具体的には次のような部分である: $ diff c.txt d.txt 89,90c89 < 至ら 動詞,*,子音動詞ラ行,未然形,至る,いたら,* < ない 接尾辞,形容詞性述語接尾辞,イ形容詞アウオ段,基本形,ない,ない,* --- > 至らない 形容詞,*,イ形容詞アウオ段,基本形,至らない,いたらない,代表表記:至らない この問題に対応するため、形態素を文節ごとにまとめて文節番号とともに出力し、1文で1行に出力するプログラムnorm_chunk.rbを自作した。このプログラムを使うと、CaboChaの出力を次のように正規化できる: $ ruby norm_chunk.rb G51-eval-1.dat (0)村山富市首相は(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)示した。 : そして評価は、CaboChaの出力と正解データの出力をともにnorm_chunk.rbにかけ、それらをdiffで比較することで異なる文の数(異なる行数)をカウントすることで評価することにした。異なる部分の例として、diff出力の一部を以下に記す: $ diff a-diff-source.txt G51-diff-source.txt 46c46 < (0)世界が(1)アッと(2)驚く(3)若い(4)首相が(5)誕生し、(6)がんじがらめの(7)規制が(8)たった(9)一本の(10)法律で(11)撤廃された(12)――新春の(13)初夢であり、(14)期待です。 --- > (0)世界が(1)アッと(2)驚く(3)若い(4)首相が(5)誕生し、(6)がんじがらめの(7)規制が(8)たった(9)一本の(10)法律で(11)撤廃された――(12)新春の(13)初夢であり、(14)期待です。 : ただ、正解データとcabochaの出力の異なる文を表示するにはdiffコマンドは便利だが、両方のテキストファイルを比べて正解率(文節がすべて一致する文の割合)を見るには不便なので、別途この目的だけに使える簡易diffプログラムdiff_chunk.rbを作成した。 今回作成したプログラム 前述の通り、今回の作業のため以下のmake_source.rb, norm_chunk.rbおよびdiff_chunk.rbの3つのプログラムを自作したのでそれを記す: make_source.rb SPS = 400 # number of Sentences Per Source ifname = "G#{ARGV[0]}.dat" $osfprefix = "S#{ARGV[0]}" $oefprefix = "G#{ARGV[0]}-eval" $onum = 0 def next_out_files $onum = $onum + 1 ofname1 = sprintf("%s-%02d.txt", $osfprefix, $onum) ofname2 = sprintf("%s-%02d.dat", $oefprefix, $onum) return File.open(ofname1,"w"), File.open(ofname2,"w") end # osfile : output source file # oefile : output eval file osfile, oefile = next_out_files count = 0 # EOS(sentence) counter File.open(ifname){ |ifile| while line = ifile.gets oefile.puts line next if /¥A¥*/ =~ line if /¥AEOS/ =~ line osfile.print "¥n" # print CRLF for EOS count = count + 1 if count > SPS osfile.close oefile.close osfile, oefile = next_out_files count = 0 end next end osfile.print line.split(nil)[0] end } osfile.close oefile.close norm_chunk.rb ifname = ARGV[0] chunk = "" chunk_num = 0 File.open(ifname){ |ifile| while line = ifile.gets if /¥A¥*/ =~ line print "(#{chunk_num})#{chunk}" unless chunk.empty? chunk = "" chunk_num = line.split(nil)[1] elsif /¥AEOS/ =~ line print "(#{chunk_num})#{chunk}¥n" unless chunk.empty? chunk = "" else chunk = chunk + line.split(nil)[0] end end } diff_chunk.rb ifname1 = ARGV[0] ifname2 = ARGV[1] out_diff = ARGV[2] ? true : false sentences = 0 diffs = 0 File.open(ifname1){ |ifile1| File.open(ifname2){ |ifile2| while line1 = ifile1.gets line2 = ifile2.gets sentences = sentences + 1 if line1 != line2 if out_diff puts line1 puts line2 puts "---" end diffs = diffs + 1 end end } } error_rate = diffs.to_f / sentences.to_f printf("error = %1.4f (%d/%d)¥n", error_rate, diffs, sentences) 交差検定の結果 前述のdiff_chunk.rbで正解データとcabochaの出力を各文ごとに比較し、すべての文節が一致するものを正解、それ以外を不正解(エラー)とし、エラーの文の数が全体の文の数に占める割合をエラー率とすると以下のようになった:
エラー率は12%から18%となった。28個のKNPファイルから5グループのモデルファイルを作成したわけだが、学習データを「多く」使えたchunk.juman.4とchunk.juman.5(6x3+5=23個のKNPファイル)はエラー率が12%程度と低く、学習データが「少ない」それ以外のモデル(6x2+5x2=22個のKNPファイル)ではエラー率が15%から18%程度となった(ただし、学習データ量の「多い」「少ない」は、KNPファイルの個数なので正確ではない)。 オペレーションメモ 以下にまっさらなUbuntu Server 10.04.3にCaboChaのインストールから本件交差検定を行ったときのオペレーションを示す。 # 必要ツール類のインストール $ sudo apt-get openssh-server install build-essential nkf emacs23 unzip ruby # CRF++ のインストール $ wget http://crfpp.googlecode.com/files/CRF%2B%2B-0.57.tar.gz $ tar xvzf CRF++-0.57.tar.gz $ cd CRF++-0.57 $ ./configure $ make $ sudo make install # MeCab のインストール $ wget http://mecab.googlecode.com/files/mecab-0.994.tar.gz $ tar xvzf mecab-0.994.tar.gz $ cd mecab-0.994 $ ./configure --with-charset=euc $ make $ make check $ sudo make install $ sudo ldconfig # MeCab Juman辞書のインストール $ wget http://mecab.googlecode.com/files/mecab-jumandic-5.1-20070304.tar.gz $ tar xvzf mecab-jumandic-5.1-20070304.tar.gz $ cd mecab-jumandic-5.1-20070304 $ ./configure --with-charset=euc-jp $ make $ sudo make install # MeCab Perlバインディングのインストール $ wget http://mecab.googlecode.com/files/mecab-perl-0.994.tar.gz $ tar xvzf mecab-perl-0.994.tar.gz $ cd mecab-perl-0.994 $ perl Makefile.PL $ make $ sudo make install # MeCab+Juman 辞書の実行(ここまでの動作確認) # 「すもももももももものうち」を解析するも、Jumanだと「すもも」が「す股」と解釈される(笑) $ mecab -d /usr/local/lib/mecab/dic/jumandic すもももももももものうち す 接頭辞,名詞接頭辞,*,*,す,す,* もも 名詞,普通名詞,*,*,もも,もも,代表表記:股 も 助詞,副助詞,*,*,も,も,* もも 名詞,普通名詞,*,*,もも,もも,代表表記:股 も 助詞,副助詞,*,*,も,も,* もも 名詞,普通名詞,*,*,もも,もも,代表表記:股 の 助詞,接続助詞,*,*,の,の,* うち 名詞,副詞的名詞,*,*,うち,うち,* EOS ^D # CaboChaのインストール(Juman品詞セット) $ wget http://cabocha.googlecode.com/files/cabocha-0.64.tar.gz $ tar xvzf cabocha-0.64.tar.gz $ cd cabocha-0.64 $ ./configure --with-charset=EUC-JP --with-posset=JUMAN # make時のエラーを避けるため、Makefileを編集して # CABOCHA_MODEL_LIST および CABOCHA_TXTMODEL_LISTから*.ipa.*のメンバーを除く $ emacs Makefile model/Makefile $ sudo ldconfig $ make $ make check $ sudo make install $ sudo ldconfig # CaboChaの動作確認 $ cabocha -d /usr/local/lib/mecab/dic/jumandic オリンピックがいよいよロンドンで開幕した。 オリンピックが-----D いよいよ---D 2012.06.16 Saturday
Lucene/Solr インデックスの中を覗いてジップの法則を確認する
単語の出現頻度の順位nと出現確率P(n)の間には、ジップの法則(Zipf's Law)と呼ばれる次の経験則が成り立つ:
P(n) = c / n ジップは英語の場合、c=0.1であると推定している。 単語の出現頻度などはLucene/Solrのインデックスを調べれば簡単にわかるので、早速手元のインデックス(歴代の内閣総理大臣の所信表明演説が登録されている)でやってみたところ、下図のように見事に直線上に並んだ。ただし、c=0.162とした: プログラムは以下の通り。試すときは、INDEXとFのString定数に、それぞれインデックスの絶対パスと、調べる対象のフィールド名を指定する。 public final class TestZipfsLaw { final static String INDEX = "/path/to/index"; final static String F = "fieldNameToBeChecked"; public static void main(String[] args) throws Exception { Directory dir = getDir(); List 実行すると、標準出力に次のように2列の数値が表示されるので、これをExcelに貼り付けグラフを表示すると、上手のようになる(はず)。2列の数値の1列目は単語の出現頻度の順位で、2列目は出現回数である。 1, 4483 2, 2415 3, 1239 4, 1228 5, 1091 : : 元ヤフー社員も大満足のロンウイットのSolrトレーニング・・・受講者インタビュー記事 Solr 3.6 7月 トレーニング受講者募集中 |
+ Solrによるブログ内検索
+ PROFILE
+ LINKS
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
+ RECOMMEND
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
+ SPONSORED LINKS
|
(C) 2024 ブログ JUGEM Some Rights Reserved.
|
PAGE TOP |