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

GoogleAppEngineの勉強中です。
3回に分けて、GAEで日本語の形態素解析を試してみたことを書いています。
第1回 lucene-gosenを使ってみる
第2回 lucene-gosenのTokenFilterを試す
第3回 Kuromojiを使ってみる

今回は、第2回です。

使っている環境
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
lucene-core-4.2.0


(1)はじめに
第1回で、lucene-gosenを使ってみました。
その後、lucene-gosenには、TokenFilter という機能があって、7種類のフィルタが使えるという情報を見つけました。
(参考) moco(beta)’s backup: lucene-gosen の Tokenizer, TokenFilter を動かしてみる

その中の、GosenWidthFilter というものが気になったので、今回、試してみました。
GosenWidthFilter というのは、

Folds fullwidth ASCII variants into the equivalent basic latin
 → 全角のASCII文字を半角にする
Folds halfwidth Katakana variants into the equivalent kana
 → 半角カタカナを全角にする

というものです。
(参考)GosenWidthFilter (lucene-gosen-2.0.2 API)

たとえば、「ニュースサイトが提供している、TwitterやRSSフィードの内容を定期的に取得して、そこに含まれる単語を集計してみたり」ってことをやるときに、同じ単語でも半角・全角がまざっているとうまく集計ができない。
自分でうまく変換すればいいのかもしれないけど、形態素解析ライブラリのほうで、うまいことやってくれるのなら助かります。

というわけで、準備に取り掛かります。


(2)lucene-gosenダウンロード
こちらについては、第1回めに記載してあるので、そちらを参照してください。


(3)lucene-coreダウンロード
GosenWidthFilter機能は、lucene-gosenの機能なので、lucene本体はいらないと思っていたんだけど、実際にGosenWidthFilterをつかおうとすると、lucene本体に含まれるクラスが必要でした。

こちらのサイトから、
Apache Lucene – Welcome to Apache Lucene

Apache LuceneのDOWNLOADをクリック。
ダウンロードしたlucene-4.2.0.tgz というファイルを展開すると、coreというフォルダの中に、lucene-core-4.2.0.jar というJarファイルがあります。

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


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

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

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

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

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

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

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

それを踏まえて、GosenWidthFilter機能の使い方をみていきます。

基本的には、先述したサイトに載っているコードを、ほぼそのまま使っています。
(参考) moco(beta)’s backup: lucene-gosen の Tokenizer, TokenFilter を動かしてみる

やり方は、解析したい文章をGosenTokenizerに掛けて、そのあとGosenWidthFilterでフィルタリング(?)するだけです。

        TokenStream stream = new GosenTokenizer(new StringReader(content));
        stream = new GosenWidthFilter(stream);

そうするとstreamの中に、形態素解析した結果が入ります。

解析結果は以下のような感じで、getAttributeメソッドでとりだせます。

while(stream.incrementToken()) {
    CharTermAttribute charAtt = stream.getAttribute(CharTermAttribute.class);
    BasicFormAttribute bfAtt = stream.getAttribute(BasicFormAttribute.class);
    ConjugationAttribute conjAtt = stream.getAttribute(ConjugationAttribute.class);
       :       :       :
        (中略)

    resp.getWriter().println(
        "======================================\n" +
        "CharTerm=" + charAtt + "\t" +  // トークン
        "BasicForm=" + bfAtt.getBasicForm() + "\t" +  // 基本形
        "ConjugationalForm=" + conjAtt.getConjugationalForm() + "\t" +  // 活用形
       :       :       :
        (中略)

        );
}

取得できる情報(Attribute)にどんなものがあるかについては、こちらのドキュメントが参考になります。
Attribute (Lucene 4.2.0 API)

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

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

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

今回もこの機能を使おうとしたんだけども、力及ばず、わからなかった。。。

第1回のときは、StringTaggerクラスを使って解析を行っていました。で、StringTaggerクラスのaddFilterメソッドで、CompositeTokenFilterを追加していました。
今回の場合、GosenTokenizerを使っているので、GosenTokenizerを動かす時にフィルタを適用すればいいと思って試してみたんだけど、ダメだった。

// String rule = "記号-アルファベット";
// CompositeTokenFilter ctFilter = new CompositeTokenFilter();
// ctFilter.readRules(new BufferedReader(new StringReader(rule)));
// TokenStream stream = new GosenTokenizer(new StringReader(content), ctFilter);

というわけで、若干、消化不良になってしまった。


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

まずは、ソースコードから。

package sample;

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

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

import org.apache.lucene.analysis.gosen.GosenWidthFilter;
import org.apache.lucene.analysis.gosen.GosenTokenizer;
import org.apache.lucene.analysis.gosen.tokenAttributes.*;

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.*;

@SuppressWarnings("serial")
public class TestGosenWidthFilter_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("GosenWidthFilter テスト");

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

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

        // String rule = "記号-アルファベット";
        // CompositeTokenFilter ctFilter = new CompositeTokenFilter();
        // ctFilter.readRules(new BufferedReader(new StringReader(rule)));
        // TokenStream stream = new GosenTokenizer(new StringReader(content), ctFilter);
        TokenStream stream = new GosenTokenizer(new StringReader(content));

        stream = new GosenWidthFilter(stream);

        try {

            while(stream.incrementToken()) {
                CharTermAttribute charAtt = stream.getAttribute(CharTermAttribute.class);
                BasicFormAttribute bfAtt = stream.getAttribute(BasicFormAttribute.class);
                ConjugationAttribute conjAtt = stream.getAttribute(ConjugationAttribute.class);
                PartOfSpeechAttribute posAtt = stream.getAttribute(PartOfSpeechAttribute.class);
                PronunciationsAttribute pronAtt = stream.getAttribute(PronunciationsAttribute.class);
                List pronunciations = pronAtt.getPronunciations();
                ReadingsAttribute readAtt = stream.getAttribute(ReadingsAttribute.class);
                List readings = readAtt.getReadings();

                SentenceStartAttribute senAtt = stream.getAttribute(SentenceStartAttribute.class);
                CostAttribute costAtt = stream.getAttribute(CostAttribute.class);
                OffsetAttribute offsetAtt = stream.getAttribute(OffsetAttribute.class);
                TermToBytesRefAttribute term_bytesAtt = stream.getAttribute(TermToBytesRefAttribute.class) ;

                resp.getWriter().println(
                    "======================================\n" +
                    "CharTerm=" + charAtt + "\t" +  // トークン
                    "OffsetAttribute=" + offsetAtt.startOffset() + "\t" +
                    "CharTerm.length=" + charAtt.length() + "\t" +  // トークン
                    "CostAttribute=" + costAtt.getCost() + "\n" +

                    "BasicForm=" + bfAtt.getBasicForm() + "\t" +  // 基本形
                    "ConjugationalForm=" + conjAtt.getConjugationalForm() + "\t" +  // 活用形
                    "ConjugationalType=" + conjAtt.getConjugationalType() + "\n" +  // 活用形

                    "PartOfSpeechAttribute=" + posAtt.getPartOfSpeech() + "\t" + // 品詞
                    "PronunciationsAttribute=" + (pronunciations.size() > 0 ? pronAtt.getPronunciations().get(0) : "") + "\t" +  // 発音
                    "ReadingsAttribute=" + (readings.size() > 0 ? readAtt.getReadings().get(0) : "") + "\n" +  // 読み

                    "term_bytesAtt=" + term_bytesAtt+ "\n"

                    );
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

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

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

量が多いので、途中を省略してあります。
全角アルファベット「Solr」が半角に変換され、間隔カタカナ「エンジン」が全角「エンジン」に変換されました!

GosenWidthFilter テスト
content= 2013年のLucene/Solrは検索エンジンです。
======================================
CharTerm=2	OffsetAttribute=0	CharTerm.length=1	CostAttribute=4688
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-数	PronunciationsAttribute=ニ	ReadingsAttribute=ニ
term_bytesAtt=2

======================================
CharTerm=0	OffsetAttribute=1	CharTerm.length=1	CostAttribute=3759
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-数	PronunciationsAttribute=ゼロ	ReadingsAttribute=ゼロ
term_bytesAtt=0

     :     :
     (中略)

======================================
CharTerm=年	OffsetAttribute=4	CharTerm.length=1	CostAttribute=1053
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-接尾-助数詞	PronunciationsAttribute=ネン	ReadingsAttribute=ネン
term_bytesAtt=年

======================================
CharTerm=の	OffsetAttribute=5	CharTerm.length=1	CostAttribute=721
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=助詞-連体化	PronunciationsAttribute=ノ	ReadingsAttribute=ノ
term_bytesAtt=の

======================================
CharTerm=L	OffsetAttribute=6	CharTerm.length=1	CostAttribute=4543
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=エル	ReadingsAttribute=エル
term_bytesAtt=L

======================================
CharTerm=u	OffsetAttribute=7	CharTerm.length=1	CostAttribute=3248
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=ユー	ReadingsAttribute=ユー
term_bytesAtt=u

     :     :
     (中略)

======================================
CharTerm=/	OffsetAttribute=12	CharTerm.length=1	CostAttribute=3896
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-一般	PronunciationsAttribute=/	ReadingsAttribute=/
term_bytesAtt=/

======================================
CharTerm=S	OffsetAttribute=13	CharTerm.length=1	CostAttribute=2414
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=エス	ReadingsAttribute=エス
term_bytesAtt=S

======================================
CharTerm=o	OffsetAttribute=14	CharTerm.length=1	CostAttribute=3248
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-アルファベット	PronunciationsAttribute=オー	ReadingsAttribute=オー
term_bytesAtt=o

     :     :
     (中略)

======================================
CharTerm=検索	OffsetAttribute=18	CharTerm.length=2	CostAttribute=3599
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=名詞-サ変接続	PronunciationsAttribute=ケンサク	ReadingsAttribute=ケンサク
term_bytesAtt=検索

======================================
CharTerm=エンジン	OffsetAttribute=20	CharTerm.length=4	CostAttribute=30883
BasicForm=*	ConjugationalForm=null	ConjugationalType=null
PartOfSpeechAttribute=未知語	PronunciationsAttribute=	ReadingsAttribute=
term_bytesAtt=エンジン

======================================
CharTerm=です	OffsetAttribute=25	CharTerm.length=2	CostAttribute=2328
BasicForm=*	ConjugationalForm=基本形	ConjugationalType=特殊・デス
PartOfSpeechAttribute=助動詞	PronunciationsAttribute=デス	ReadingsAttribute=デス
term_bytesAtt=です

======================================
CharTerm=。	OffsetAttribute=27	CharTerm.length=1	CostAttribute=361
BasicForm=*	ConjugationalForm=*	ConjugationalType=*
PartOfSpeechAttribute=記号-句点	PronunciationsAttribute=。	ReadingsAttribute=。
term_bytesAtt=。
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