概要

MahoutのJavaDocを見てたら、org.apache.mahout.common.nlp.NGramsというクラスを見かけた。

試しに使ってみたのだけど、当然ながらこの子は日本語向けではなかった。

日本語で2~3gramを切り出したいのだけど、さて、何を使おうか。

@CretedDate 2012/08/05
@Env Java7, Mahout0.7

NGramsクラスについて

org.apache.mahout.common.nlp.NGramsは、スペースタブでsplitしている。

下記のように「i am a pen.」というカッコいい英語を分割する場合は、こんなコードになる。

NGrams ngrams = new NGrams("i am a pen.", 2);

List<String> list = ngrams.generateNGramsWithoutLabel();
for( String gram : list )
    System.out.println(gram);

結果は下記。

i
i
i am
am
am a
a
a pen.

ちょっとガッカリな結果。

これに日本語を渡した場合はスペースもタブも含まれないことが多いので、ほとんど分割されずに終わる。

Luceneに頼ってみる

MahoutのdependencyにはLuceneも入ってる。この子を使えば記号とかを飛ばした日本語のNGramも簡単に作ってくれそう。

NGramTokenizerを使えば、日本語のNGramの結果が得られる。

StringReader reader = new StringReader("私はペンです。");

// 2gramと3gramを出力する
TokenStream stream = new NGramTokenizer(reader, 2, 3);

while (stream.incrementToken()) {
    CharTermAttribute term = stream.getAttribute(CharTermAttribute.class);
    System.out.println(term.toString());
}

結果はこんな感じ。

私は
はペ
ペン
ンで
です
す。
私はペ
はペン
ペンで
ンです
です。

良い感じ。

bigramで、記号は含まず、英語はbigramにしなくて良い場合は、StandardAnalyzer + CJKAnalyzerが良いやもしれない。

StringReader reader = new StringReader("私はペンです。");

TokenStream stream = new StandardTokenizer(Version.LUCENE_36, reader);
stream = new CJKBigramFilter(stream);

while (stream.incrementToken()) {
    CharTermAttribute term = stream.getAttribute(CharTermAttribute.class);
    System.out.println(term.toString());
}
私は
はペ
ペン
ンで
です

記号抜きで2〜3gramを取りたい場合はどうしよう

できれば記号にはご退場頂きたい。

自前で記号を判定して抜いても良いのだけど、なんとなく自前でTokenizer書いてみる。

とりあえずCharTokenizer使って、文字か数字以外は全部splitしてしまう的なTokenizerを書いて。

public class SymbolTokenizer extends CharTokenizer {

	public SymbolTokenizer(Version version, Reader input) {
		super(version, input);
	}

	protected boolean isTokenChar(int c) {
		return Character.isLetter(c) || Character.isDigit(c);
	}
}

これとNGramTokenFilterを組み合わせれば。

StringReader reader = new StringReader("私はペンです。");

TokenStream stream = new SymbolTokenizer(Version.LUCENE_36, reader);

stream = new NGramTokenFilter(stream, 2, 3);

while (stream.incrementToken()) {
    CharTermAttribute term = stream.getAttribute(CharTermAttribute.class);
    System.out.println(term.toString());
}

文字と数値だけを使ったNGramが好きなサイズで作れそう。

StringReader reader = new StringReader("私はペンです・・・よね?");

TokenStream stream = new SymbolTokenizer(Version.LUCENE_36, reader);

stream = new NGramTokenFilter(stream, 2, 3);

while (stream.incrementToken()) {
    CharTermAttribute term = stream.getAttribute(CharTermAttribute.class);
    System.out.println(term.toString());
}

結果はこんな感じ。

私は
はペ
ペン
ンで
です
私はペ
はペン
ペンで
ンです
よね