NLP4J [006-034c] NLP4J で言語処理100本ノック #34 「AのB」をさらにスマートに解いてみる(完結編)


Indexに戻る

課題

NLP4J [006-034b] NLP4J で言語処理100本ノック #34 「AのB」の Annotator を作ってみる では以下のようなロジックを切り出して定義することでロジックの再利用を可能にしていました。

ところがこれでもまだ足りないのです。
キーワードの抽出ルールをロジックで書いているため、柔軟性が足りないのです。
「AのB」であればこのロジックで問題ありませんが、では「AがB」のようなものを抽出したいときはまた別のロジックを用意しなければなりません。

「AのB」を抽出するためだけにプログラム・ロジック・メソッドを作成するのは、勉強のためなら問題ありませんが、自然言語処理を業務で使うには効率がよくないのです。

/**
 * 「名詞の名詞」を「word_nn_no_nn」キーワードとして抽出します。
 * @author Hiroki Oya
 */
public class Nokku34Annotator extends AbstractDocumentAnnotator implements DocumentAnnotator {
    @Override
    public void annotate(Document doc) throws Exception {
        ArrayList<Keyword> newkwds = new ArrayList<>();
        Keyword meishi_a = null;
        Keyword no = null;
        for (Keyword kwd : doc.getKeywords()) {
            if (meishi_a == null && kwd.getFacet().equals("名詞")) {
                meishi_a = kwd;
            } //
            else if (meishi_a != null && no == null && kwd.getLex().equals("の")) {
                no = kwd;
            } //
            else if (meishi_a != null && no != null && kwd.getFacet().equals("名詞")) {
                Keyword kw = new DefaultKeyword();
                kwd.setLex(meishi_a.getLex() + no.getLex() + kwd.getLex());
                kwd.setFacet("word_nn_no_nn");
                kwd.setBegin(meishi_a.getBegin());
                kwd.setEnd(kwd.getEnd());
                kwd.setStr(meishi_a.getStr() + no.getStr() + kwd.getStr());
                kwd.setReading(meishi_a.getReading() + no.getReading() + kwd.getReading());
                newkwds.add(kw);
                meishi_a = null;
                no = null;
            } //
            else {
                meishi_a = null;
                no = null;
            }
        }
        doc.addKeywords(newkwds);
    }
}

どうやって解くか

言語処理100本ノック 2015 #34 を改めてみてみると、#34 は単に

「AのB」
2つの名詞が「の」で連結されている名詞句を抽出せよ

と書いてあるのみです。
この問題を解くためだけにわざわざロジックを作るということはAIっぽくありません。

そこで、独自のルール記述を開発することにします。

ルールの記述

2つの名詞が「の」で連結されている名詞句を抽出せよ

とのことなので、これを書くことができるようなルールを作ってみましょう。

みんな大好きなJSONで書いてみると、ルールとしては以下のような感じでしょうか。

[{'facet':'名詞'},{'lex':'の'},{'facet':'名詞'}]

JSON配列としてキーワードが並んでいて、「名詞,の,名詞」のようになっているのが抽出のルールとします。

抽出結果の記述

2つの名詞が「の」で連結されている名詞句を抽出せよ

とありますが、名詞句の何を抽出するのかは明記されていません。
人間的に考えてみると正規形(原形)だとしておきます。

抽出結果の記述ルールとしても文法を何にするかはいろいろ考えられますが、日本で最も大規模に使われているエンタープライズ向けテキストマイニングソフトウェアの IBM Watson Explorer と同じ形式にしておきたいと思います。(あえてJSONにはしない...)

IBM Watson Explorer のルールファイルについてのドキュメントは以下のものになります。
コンテンツ分析コレクションのカスタム・ルール・ファイル

難しいマニュアルですが、キーワード抽出部の書き方としては以下のようになります。

${0.lex}-${1.lex}-${2.lex}

数値は抽出されたキーワードのインデックス値です。
ピリオドの後に続く文字列(ここではlex)はキーワードの属性値です。lexは原形を意味します。
日本語に翻訳すると

${0番目Keywordの原形}-${1番目Keywordの原形}-${2番目Keywordの原形}

となり、抽出されたキーワードの原形をハイフンで連結するという記述になります。

${...} の部分以外は適当に記述できるので、単に連結して

${0.lex}${1.lex}${2.lex}

ともできますし、

${0.lex} ... ${1.lex} ... ${2.lex}

のようにも記述できます。

CODE

コードとしては

String rule = "[{facet:'名詞'},{lex:'の'},{facet:'名詞'}]";
String facet = "word_nn_no_nn";
String value = "${0.lex}-${1.lex}-${2.lex}";

という設定だけを用意して、これで抽出できればスマートということになります。
ちょっとAIっぽくなりましたね。(ルールを自然言語で書ければさらにスマートですが...)

指定されたルールでキーワードを抽出するAnnotatorとして
nlp4j.annotator.KeywordSequencePatternAnnotator
を用意しました。コードが長くなったのでここでの掲載は省略します。

String rule = "[{facet:'名詞'},{lex:'の'},{facet:'名詞'}]"; // #34 これだけ
String facet = "word_nn_no_nn"; // #34 これだけ
String value = "${0.lex}-${1.lex}-${2.lex}"; // #34 これだけ

// NLP4Jが提供するテキストファイルのクローラーを利用する
Crawler crawler = new TextFileLineSeparatedCrawler();
crawler.setProperty("file", "src/test/resources/nlp4j.crawler/neko_short_utf8.txt");
crawler.setProperty("encoding", "UTF-8");
crawler.setProperty("target", "text");

// ドキュメントのクロール
List<Document> docs = crawler.crawlDocuments();

// NLPパイプライン(複数の処理をパイプラインとして連結することで処理する)の定義
DocumentAnnotatorPipeline pipeline = new DefaultDocumentAnnotatorPipeline();
{
    // Yahoo! Japan の形態素解析APIを利用するアノテーター
    DocumentAnnotator annotator = new YJpMaAnnotator();
    pipeline.add(annotator);
}
{
    KeywordSequencePatternAnnotator annotator = new KeywordSequencePatternAnnotator();
    annotator.setProperty("rule[0]", rule);
    annotator.setProperty("facet[0]", facet);
    annotator.setProperty("value[0]", value);
    pipeline.add(annotator);
}
// アノテーション処理の実行
pipeline.annotate(docs);

System.err.println("<抽出されたキーワード>");
for (Document doc : docs) {
    for (Keyword kwd : doc.getKeywords(facet)) {
        System.err.println(kwd);
    }
}
System.err.println("</抽出されたキーワード>");

結果

以下のようになりました。
ルールを指定するだけでキーワードを抽出できましたね!

<抽出されたキーワード>
彼-の-掌 [sequence=-1, facet=word_nn_no_nn, lex=彼-の-掌, str=彼-の-掌, reading=null, count=-1, begin=2, end=5, correlation=0.0]
掌-の-上 [sequence=-1, facet=word_nn_no_nn, lex=掌-の-上, str=掌-の-上, reading=null, count=-1, begin=0, end=3, correlation=0.0]
書生-の-顔 [sequence=-1, facet=word_nn_no_nn, lex=書生-の-顔, str=書生-の-顔, reading=null, count=-1, begin=11, end=15, correlation=0.0]
はず-の-顔 [sequence=-1, facet=word_nn_no_nn, lex=はず-の-顔, str=はず-の-顔, reading=null, count=-1, begin=13, end=17, correlation=0.0]
顔-の-真中 [sequence=-1, facet=word_nn_no_nn, lex=顔-の-真中, str=顔-の-真中, reading=null, count=-1, begin=5, end=9, correlation=0.0]
穴-の-中 [sequence=-1, facet=word_nn_no_nn, lex=穴-の-中, str=穴-の-中, reading=null, count=-1, begin=6, end=9, correlation=0.0]
</抽出されたキーワード>

Maven

以上のコードは nlp4j-core 1.2.0.0 以上で動作します。

<dependency>
  <groupId>org.nlp4j</groupId>
  <artifactId>nlp4j-core</artifactId>
  <version>1.2.0.0</version>
</dependency>

1.2.0.0 はビルド+アップロードからMavenでデプロイされるまで、12時間以上かかったようです。
サーバーが混雑している?ようです。

所感

ルールを記述して解くというのは他の言語処理100本ノック#34と比較してもスマートだと思います。

まとめ

NLP4J を使うと、Javaで簡単に自然言語処理ができますね!

プロジェクトURL

https://www.nlp4j.org/


Indexに戻る