(GAEで日本語の形態素解析を試してみる)第1回 lucene-gosenを使ってみる

GoogleAppEngineの勉強中です。
ニュースサイトが提供している、TwitterやRSSフィードの内容を定期的に取得して、そこに含まれる単語を集計してみたり、ってことをやってみたいと思った。とりあえず、IT系の情報を収集してみて、自分用の「今週のトレンド情報」を作ってみるとか。

で、そうすると必然的に、日本語の文章から単語を抽出する必要がある。
Pythonで使える「日本語の形態素解析ライブラリ」ないかなー、って探してみたけど、イマイチ。
というわけで、Javaでやることにした。

いくつか試してみたので、以下の3回に分けて書きます。
第1回 lucene-gosenを使ってみる
第2回 lucene-gosenのTokenFilterを試す
第3回 Kuromojiを使ってみる

今回は、第1回です。

使っている環境
Java JDK1.6.0_35(64bit版)
Google App Engine SDK for Java 1.7.1

Eclipse IDE for Java EE Developers_ Juno (4.2)64Bit版
The Google Plugin for Eclipse, for Eclipse 3.8/4.2 (Juno)

lucene-gosen-4.2.0-naist-chasen


(1)lucene-gosenダウンロード

こちらのサイトの、Downloadsから、
lucene-gosen – Japanese analysis for Apache Lucene/Solr 3.6 and 4.2 – Google Project Hosting

辞書を内包しているJarファイルをダウンロードします。
辞書の違い(naist-chasen、ipadic)があるみたいですが、どちらでもいいみたいです。
ただ、なんとなくnaist-chasenのほうがよさそうな気がしたので、僕はlucene-gosen-4.2.0-naist-chasen.jarにしました。

で、これをクラスパスに通しておけば、準備完了。


(2)コーディングの流れ

まずはじめに、注意点です。
僕はGoogleAppEngine上で動かしています。
で、今回テスト用に作成するものは、HTTPのGETリクエストに「q=解析したい文章」といったパラメータを付けることで、パラメータqに設定した文章を解析して、ブラウザに結果を返すように作っています。

そのため、以下のようにして、getリクエストのパラメータqを取得して、変数contentに格納するようにしています。

String content = req.getParameter("q");

また、結果をブラウザに返却するため、以下のメソッドで出力しています。

resp.getWriter().println("出力内容をここに書く");

このあたりはGAEならではの作法になります。

それを踏まえて、lucene-gosenの使い方をみていきます。
以下のように、taggerを用意してから、tagger.analyzeを実行すると、解析結果がlistの中に入ります。
(content には、解析対象の文章が入っています)

        StringTagger tagger = SenFactory.getStringTagger(null);
        List<Token> list = new ArrayList<Token>();
        list = tagger.analyze(content, list);

List<Token>の中身は、Tokenクラスのメソッドを使って取り出せます。たとえば、token.getSurface()と書くと、解析された単語が表示されます。
(参考)Token (lucene-gosen-2.0.2 API)

Tokenの中に、morpheme dataというものがあって、token.getMorpheme()を実行すると、Morphemeオブジェクトが返却されます。で、morpheme dataの内容は、Morphemeクラスのメソッドを使って取得することになります。

以下のような感じで書くことで、品詞(PartOfSpeech)を取得できます。
Morpheme morpheme = token.getMorpheme();
morpheme.getPartOfSpeech()
(参考)Morpheme (lucene-gosen-2.0.2 API)

まとめると、listの中身は、以下のような感じで書くと取り出せます。

            for (Token token : list) {

                resp.getWriter().println(
                        "======================================\n" +
                        "surface=" + token.getSurface() + "\t" +
                        "start=" + token.getStart() + "\t" +
                        "length=" + token.getLength() + "\t" +
                        "cost=" + token.getCost()
                        );

                Morpheme morpheme = token.getMorpheme();

                resp.getWriter().println(
                        "basicForm=" + morpheme.getBasicForm() + "\t" +
                        "cForm=" + morpheme.getConjugationalForm() + "\t" +
                        "cType=" + morpheme.getConjugationalType() + "\n" +
                        "partOfSpeech=" + morpheme.getPartOfSpeech() + "\t" +
                        "pron=" + morpheme.getPronunciations() + "\t" +
                        "read=" + morpheme.getReadings() + "\n" +
                        "additionalInfo=" + morpheme.getAdditionalInformation()
                    );
            }

ここまでのlucene-gosenの使い方は、こちらのサイトを参考にしています。
(参考)lucene-gosenで形態素解析 – Akira Koyasu’s WebLog

基本的な流れはこれでいいんですが、実際に動かしてみると、「辞書に載っていない英単語」や「数値」が、一文字ずつに分割されてしまいます。

たとえば以下の文章を形態素解析してみると、
「2013年のLucene/Solrは検索エンジンです。」
2013が、「2,0,1,3」の数字4つに分割され、Luceneは「L,u,c,e,n,e」というアルファベット6文字に分割されてしまいます。

これに対処するために、compositePOSという機能を使います。「トークン」を「合成」するための機能らしいです。

(参考)
compositePOSの利用例(naist-chasenでの英単語の出力方法例) | @johtani の日記
Solr/Tokenizer評価201105/JapaneseTokenizer – 春山征吾のWiki – livedoor Wiki(ウィキ)
Sprout #7 lucene-gosen | Apribase

トークンを合成する規則を設定することで、条件に一致したものをひとまとめにしてくれます。
書き方は「品詞名を半角スペース区切りで記述して、規則を設定する」とのことです。書き方はこうなっている。

連結品詞名 構成品詞名1 構成品詞名2 ... 構成品詞名n
 ・連結品詞名:合成したあとのトークンの品詞として出力する品詞名
 ・構成品詞名:合成したい品詞名(スペース区切りで複数指定可能)

今回は、「名詞-数」と、「記号-アルファベット」の規則を設定することにしました。

コーディングのときは、以下のような書き方になる。

        CompositeTokenFilter ctFilter = new CompositeTokenFilter();

        ctFilter.readRules(new BufferedReader(new StringReader("名詞-数")));
        tagger.addFilter(ctFilter);

        ctFilter.readRules(new BufferedReader(new StringReader("記号-アルファベット")));
        tagger.addFilter(ctFilter);

このcompositePOS設定を追加した後に、改めて以下の文章を形態素解析すると、
「2013年のLucene/Solrは検索エンジンです。」

こんな感じに分割されます。
「2013,年,の,Lucene,/,Solr,は,検索,エンジン,です,。」


(3)ソースコード

実際に作成したサンプルアプリのソースコードと、動かした出力結果を書いておきます。
まずは、ソースコードから。

package sample;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.java.sen.SenFactory;
import net.java.sen.StringTagger;
import net.java.sen.dictionary.Morpheme;
import net.java.sen.dictionary.Token;
import net.java.sen.filter.stream.CompositeTokenFilter;

@SuppressWarnings("serial")
public class TestGosen_Servlet extends HttpServlet {
    public void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {

        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("text/plain; charset=UTF-8");
        resp.getWriter().println("Gosen テスト");

        // パラメータ取得
        String content = req.getParameter("q");
        if (content == null || content.length() == 0){
            content = "";
        }

        resp.getWriter().printf("content= %s%n", content);

        // Gosen
        StringTagger tagger = SenFactory.getStringTagger(null);

        // compositePOSを設定
        CompositeTokenFilter ctFilter = new CompositeTokenFilter();

        ctFilter.readRules(new BufferedReader(new StringReader("名詞-数")));
        tagger.addFilter(ctFilter);

        ctFilter.readRules(new BufferedReader(new StringReader("記号-アルファベット")));
        tagger.addFilter(ctFilter);

        // 解析実行
        try {
            List<Token> list = new ArrayList<Token>();
            list = tagger.analyze(content, list);

            for (Token token : list) {

                resp.getWriter().println(
                        "======================================\n" +
                        "surface=" + token.getSurface() + "\t" +
                        "start=" + token.getStart() + "\t" +
                        "length=" + token.getLength() + "\t" +
                        "cost=" + token.getCost()
                        );

                Morpheme morpheme = token.getMorpheme();

                resp.getWriter().println(
                        "basicForm=" + morpheme.getBasicForm() + "\t" +
                        "cForm=" + morpheme.getConjugationalForm() + "\t" +
                        "cType=" + morpheme.getConjugationalType() + "\n" +
                        "partOfSpeech=" + morpheme.getPartOfSpeech() + "\t" +
                        "pron=" + morpheme.getPronunciations() + "\t" +
                        "read=" + morpheme.getReadings() + "\n" +
                        "additionalInfo=" + morpheme.getAdditionalInformation()
                    );
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

次に、動かしてみた結果を載せておきます。
「2013年のLucene/Solrは検索エンジンです。」という文章を、形態素解析させてみました。以下のような感じでgetリクエストを送った後に帰ってきた結果になります。

http://URL/?q=2013年のLucene/Solrは検索エンジンです。

Gosen テスト
content= 2013年のLucene/Solrは検索エンジンです。
======================================
surface=2013	start=0	length=4	cost=41969
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-数	pron=[ニゼロイチサン]	read=[ニゼロイチサン]
additionalInfo=null
======================================
surface=start=4	length=1	cost=17460
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-接尾-助数詞	pron=[ネン]	read=[ネン]
additionalInfo=null
======================================
surface=start=5	length=1	cost=18181
basicForm=*	cForm=*	cType=*
partOfSpeech=助詞-連体化	pron=[ノ]	read=[ノ]
additionalInfo=null
======================================
surface=Lucene	start=6	length=6	cost=185064
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-アルファベット	pron=[エルユーシーイーエヌイー]	read=[エルユーシーイーエヌイー]
additionalInfo=null
======================================
surface=/	start=12	length=1	cost=42860
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-一般	pron=[/]	read=[/]
additionalInfo=null
======================================
surface=Solr	start=13	length=4	cost=200584
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-アルファベット	pron=[エスオーエルアール]	read=[エスオーエルアール]
additionalInfo=null
======================================
surface=	start=17	length=1	cost=56498
basicForm=*	cForm=*	cType=*
partOfSpeech=助詞-係助詞	pron=[ワ]	read=[ハ]
additionalInfo=null
======================================
surface=検索	start=18	length=2	cost=60097
basicForm=*	cForm=*	cType=*
partOfSpeech=名詞-サ変接続	pron=[ケンサク]	read=[ケンサク]
additionalInfo=null
======================================
surface=エンジン	start=20	length=5	cost=90980
basicForm=*	cForm=null	cType=null
partOfSpeech=未知語	pron=[]	read=[]
additionalInfo=null
======================================
surface=です	start=25	length=2	cost=93308
basicForm=*	cForm=基本形	cType=特殊・デス
partOfSpeech=助動詞	pron=[デス]	read=[デス]
additionalInfo=null
======================================
surface=	start=27	length=1	cost=93669
basicForm=*	cForm=*	cType=*
partOfSpeech=記号-句点	pron=[。]	read=[。]
additionalInfo=null
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s