Bag of wordsでのテキストマイニング最速精度向上方法

※サンプル・コード掲載

あらすじ

空前のAI(人工知能)ブームで、NLP(自然言語処理)に興味を持ち、MeCabやKuromoji等の形態素解析器を試した方は多いと思います。

ただし、いまいち形態素解析器が何に活かせるのか把握していない人は多く、その出力をどう料理すればいいのかわからない人が多いのも事実です。

そこで、本記事は形態素解析のアウトプットを利用し、最速でBag of wordsベースのテキストマイニングをする方法を解説します。

1.使用した環境

  • Windows or Mac
  • Java1.8x(最新のバージョン)
  • EclipseをIDEとして使用し、Mavenプロジェクトを作成
  • ライブラリ等の依存関係はMavenのpom.xmlファイルによって解決

2.Kuromojiのセットアップ

以下参照
Kuromoji(形態素解析)を2分で使えるようにする方法(Java)

3.シンプルなBag of wordsの実装例

以下、非常にシンプルにBag of words分析をJavaで実装する例を示します。形態素解析器はKuromojiを使用します。

なお、解析対象のテキストは、以下のコードの、strに格納されている、日本語の文章とします。

public static void main(String[] args) {
	String str = "自然言語処理(しぜんげんごしょり、英語: natural language processing、略称:NLP)は、人間が日常的に使っている自然言語をコンピュータに処理させる一連の技術であり、人工知能と言語学の一分野である。「計算言語学」(computational linguistics)との類似もあるが、自然言語処理は工学的な視点からの言語処理をさすのに対して、計算言語学は言語学的視点を重視する手法をさす事が多い[1]。データベース内の情報を自然言語に変換したり、自然言語の文章をより形式的な(コンピュータが理解しやすい)表現に変換するといった処理が含まれる。応用例としては予測変換、IMEなどの文字変換が挙げられる。"; // 形態素解析対象文字列

	Tokenizer tokenizer = Tokenizer.builder().build(); // Kuromojiオブジェクト作成

	List tokens = tokenizer.tokenize(str); // Tokenize

	Map<String, Integer> bowMap = new HashMap<>(); // Bag of words分析用のマップ

	for (Token token : tokens) {
		String surface = token.getSurfaceForm(); // Tokenの表層情報の取得

		int count = 1;
		if (bowMap.containsKey(surface)) {
			count += bowMap.get(surface); // キーが存在する場合カウントアップ
		}

		bowMap.put(surface, count);
	}

	bowMap.entrySet().forEach(System.out::println);
}

基本的にこれだけです。ただし、このまま出力を確認すると、以下のようになっています。

一=1
情報=1
、=8
分野=1
。=3
も=1
的=4
より=1
といった=1
内=1
表現=1
げん=1
(=3
)=3
り=1
language=1
事=1
例=1
「=1
類似=1
」=1
たり=1
あり=1
言語=10
ある=2
を=6
人間=1
データベース=1
理解=1
せる=1
から=1
:=1
自然=5
計算=2
computational=1
 =4
IME=1
さす=2
natural=1
略称=1
一連=1
工学=1
予測=1
ぜん=1
1=1
やすい=1
文章=1
知能=1
processing=1
含ま=1
など=1
:=1
文字=1
応用=1
いる=1
linguistics=1
に対して=1
]。=1
が=6
使っ=1
NLP=1
する=2
さ=1
挙げ=1
し=3
[=1
英語=1
ごしょ=1
技術=1
多い=1
人工=1
て=1
学=4
手法=1
形式=1
で=2
と=2
処理=5
な=2
に=4
として=1
の=8
は=4
視点=2
られる=1
変換=4
れる=1
重視=1
コンピュータ=2
日常=1

これでは見にくいので、徐々に結果を加工していきましょう。

4.Frequency(頻度)順に降順にソート

テキストマイニングを行う際の考え方として、重要語は何度も繰り返される。つまり、頻度の高い単語が重要語だと仮定できます。

ですので、一旦、上記の結果を頻度の高い順にソートしてみます。

bowMap.entrySet().stream().sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())).forEach(System.out::println);
言語=10
、=8 ★
の=8 ★
を=6 ★
が=6 ★
自然=5
処理=5
的=4
 =4 ★
学=4
に=4 ★
は=4 ★
変換=4
。=3 ★
(=3 ★
)=3 ★
し=3 ★
ある=2
計算=2
さす=2 ★
する=2 ★
で=2 ★
と=2 ★
な=2 ★
視点=2
コンピュータ=2
一=1
情報=1
分野=1
も=1
より=1
といった=1
内=1
表現=1
げん=1
り=1
language=1
事=1
例=1
「=1
類似=1
」=1
たり=1
あり=1
人間=1
データベース=1
理解=1
せる=1
から=1
:=1
computational=1
IME=1
natural=1
略称=1
一連=1
工学=1
予測=1
ぜん=1
1=1
やすい=1
文章=1
知能=1
processing=1
含ま=1
など=1
:=1
文字=1
応用=1
いる=1
linguistics=1
に対して=1
]。=1
使っ=1
NLP=1
さ=1
挙げ=1
[=1
英語=1
ごしょ=1
技術=1
多い=1
人工=1
て=1
手法=1
形式=1
として=1
られる=1
れる=1
重視=1
日常=

「言語」「自然」「処理」など、関連のありそうな語が、上位に表示されるようになりました。

ただ、★印を書いた「、」「の」「を」「が」等、関係の無さそうな言葉も上位に表示されてしまい、これでは、まだテキストマイニングとしては弱そうです。

これらを取り除く事ができれば、結果はよくなりそうですね。

5.精度向上テクニック1: 品詞による絞り込み

前述の「、」「の」「を」「が」等、「助詞」「助動詞」「記号」等の品詞は、重要語として扱う必要がなさそうに思えます。

参考までに、ipadic(Kuromojiが参照している辞書)のPOS(品詞情報)一覧は、以下に定義されています。
https://ja.osdn.net/projects/ipadic/docs/postag.txt/ja/1/postag.txt.txt

これを眺めて見ると、重要語として効いてきそうなのは、「名詞」「動詞」「形容詞」「副詞」位でしょうか?

単語の品詞をこれらに絞り込む事ができると、結果も良くなりそうです。

では、実際に、品詞による絞り込みの機能を実装してみましょう。こちらはKuromojiなら非常に簡単にできます(Bag of words分析コードの一部に加筆します)。

for (Token token : tokens) {
	String pos = token.getAllFeaturesArray()[0]; // POSの第一分類(名詞、動詞、形容詞)等を取得
	if (pos.equals("名詞") || pos.equals("動詞") || pos.equals("形容詞") || pos.equals("副詞")) { // 品詞の絞り込み
		String surface = token.getSurfaceForm(); // Tokenの表層情報の取得
		int count = 1;
		if (bowMap.containsKey(surface)) {
			count += bowMap.get(surface); // キーが存在する場合カウントアップ
		}
		bowMap.put(surface, count);
	}
}

さて、この品詞による絞り込みを実装して、結果がどう変わったかを見てみます。

言語=10
自然=5
処理=5
的=4
学=4
変換=4
し=3 ★
計算=2
さす=2 ★
する=2 ★
視点=2
コンピュータ=2
一=1
情報=1
分野=1
より=1 ★
内=1
表現=1
げん=1
language=1
事=1
例=1
類似=1
ある=1 ★
人間=1
データベース=1
理解=1
せる=1 ★
computational=1
IME=1
natural=1
略称=1
一連=1
工学=1
予測=1
ぜん=1 ★
1=1
やすい=1 ★
文章=1
知能=1
processing=1
含ま=1 ★
:=1 ★
文字=1
応用=1
いる=1 ★
linguistics=1
]。=1
使っ=1
NLP=1
さ=1 ★
挙げ=1
[=1
英語=1
ごしょ=1
技術=1
多い=1
人工=1
手法=1
形式=1
の=1 ★
られる=1 ★
れる=1 ★
重視=1
日常=1

だいぶ結果が見やすくなった事を体感できたかと思います。

ただ、それでも、まだまだ★印部分等、要らない情報は散見していますね。

これらにフォーカスを当ててみると「し」「さ」「含ま」等、「動詞」等の活用形がそのまま結果に表示されてしまっていて、まだ見にくいですね。

活用形で無く、語幹に表記を揃えられると、もっと見やすくなりそうです。

6.精度向上テクニック2: Lemmatize

「動詞」等、活用形がある単語について、活用形で無く、その語幹を特定する処理を、自然言語処理では「Lemmatize」と呼んでいます。

たまに、Reductive stemとも呼ばれるのですが、日本語で説明するならば、「語幹処理」という事になるでしょうか?

この語幹処理はKuromojiでも簡単に使用する事ができます。

今までは、単語の表層情報を取得していましたが、以下のようにして、単語の語幹情報を取得できます。

for (Token token : tokens) {
	String pos = token.getAllFeaturesArray()[0]; // POSの第一分類(名詞、動詞、形容詞)等を取得
	if (pos.equals("名詞") || pos.equals("動詞") || pos.equals("形容詞") || pos.equals("副詞")) { // 品詞の絞り込み
		String baseform = token.getBaseForm(); // Lemmatize:
		// Tokenの語幹情報の取得
		if (baseform == null || baseform.trim().length() == 0) {
			baseform = token.getSurfaceForm(); // もし語幹情報が取得できない場合は、表層情報を代わりにセット
		}

		int count = 1;
		if (bowMap.containsKey(baseform)) {
			count += bowMap.get(baseform); // キーが存在する場合カウントアップ
		}
		bowMap.put(baseform, count);
	}
}

それでは、これで実行結果がどう変わったか見てみましょう。

言語=10
する=6 ◆
自然=5
処理=5
的=4 ★
学=4 ★
変換=4
計算=2
さす=2 ★
視点=2
コンピュータ=2
一=1
情報=1
分野=1
より=1
内=1
表現=1
げん=1
language=1
事=1
例=1
類似=1
ある=1
人間=1
データベース=1
理解=1
せる=1
ごする=1
computational=1
IME=1
natural=1
略称=1
挙げる=1
一連=1
工学=1
予測=1
使う=1
ぜん=1
1=1
やすい=1
文章=1
知能=1
processing=1
:=1
文字=1
含む=1 ◆
応用=1
いる=1
linguistics=1
]。=1
NLP=1
[=1
英語=1
技術=1
多い=1
人工=1
手法=1
形式=1
の=1
られる=1
れる=1
重視=1
日常=1

◆部分:「し」「さ」→「する」、「含ま」→「含む」等、単語が語幹になっていて、見やすくなっていますね。

ただ、それでもまだ、★印の部分等、要らない情報もありそうです。

これらを無くすにはどうすればいいでしょうか?

7.精度向上テクニック3: stop words

「品詞での絞り込み」「Lemmatize」を行った後でも、まだまだ要らない情報は散見しています。

これらを全て対応しようとすると非常に大変なのですが、「する」「さす」のように頻度が高くかつ、重要でない単語をスキップする事は重要になりそうです。

その場合に活躍するのが、「stop words」と呼ばれる方法で、上記の様な単語を、stop listに登録し、リストにあるものは分析結果に表示させないような方法です。

勿論、Lemmatizeを行った上で、「する」「さす」「的」「学」等の単語の表層を、stop listに追加するだけでも、十分効果を見込めます。

しかし、形態素解析の特徴から、単語の表層が同じでも、異なった品詞と判定される事もあるため、stop listは、単語の表層と、第一品詞まで考慮したものにしておいた方がベターです。

ですので、stop listを、単語の表層をキーで、品詞を値として持つ、Mapとして以下のように構成します。

Map<String, String> stopList = new HashMap<String, String>() { // 第一品詞までチェックするstop
	// list
	{
		put("する", "動詞");
	}
	{
		put("さす", "動詞");
	}
	{
		put("的", "名詞");
	}
	{
		put("学", "名詞");
	}
};

また、stop listに対象の単語があった場合に、それを分析対象から外すロジックを、以下のように実装します。

for (Token token : tokens) {
	String pos = token.getAllFeaturesArray()[0]; // POSの第一分類(名詞、動詞、形容詞)等を取得
	if (pos.equals("名詞") || pos.equals("動詞") || pos.equals("形容詞") || pos.equals("副詞")) { // 品詞の絞り込み
		String baseform = token.getBaseForm(); // Lemmatize:
		// Tokenの語幹情報の取得
		if (baseform == null || baseform.trim().length() == 0) {
			baseform = token.getSurfaceForm(); // もし語幹情報が取得できない場合は、表層情報を代わりにセット
		}

		if (stopList.containsKey(baseform) && pos.equals(stopList.get(baseform))) {
			continue; // stop list に対象がある場合、スキップ
		}
		int count = 1;
		if (bowMap.containsKey(baseform)) {
			count += bowMap.get(baseform); // キーが存在する場合カウントアップ
		}
		bowMap.put(baseform, count);
	}
}

結果を見てみましょう

言語=10
自然=5
処理=5
変換=4
計算=2
視点=2
コンピュータ=2
一=1
情報=1
分野=1
より=1
内=1
表現=1
げん=1
language=1
事=1
例=1
類似=1
ある=1
人間=1
データベース=1
理解=1
せる=1
ごする=1
computational=1
IME=1
natural=1
略称=1
挙げる=1
一連=1
工学=1
予測=1
使う=1
ぜん=1
1=1
やすい=1
文章=1
知能=1
processing=1
:=1
文字=1
含む=1
応用=1
いる=1
linguistics=1
]。=1
NLP=1
[=1
英語=1
技術=1
多い=1
人工=1
手法=1
形式=1
の=1
られる=1
れる=1
重視=1
日常=1

見事に、「する」「さす」「的」「学」が現れなくなりましたね!だいぶよくなりましたが、まだまだゴミ情報は散見しています。

ここで、Bag of words分析の原点に立ち返ると、単語の出現頻度が高いもの程、重要な単語だろうという仮定がありましたね。

8.精度向上テクニック4: 頻度フィルター

Bag of words分析で、単語の出現頻度の閾値によって、分析結果に含めるかどうか、という単純なロジックを実装すればいいだけの話なので、実装方法については、ここでは割愛させて頂きます。

上記の結果で、頻度が2以上の単語の結果のみ表示すると下記のようになります。

言語=10
自然=5
処理=5
変換=4
計算=2
視点=2
コンピュータ=2

結果を見て頂くと、まさに、「自然言語処理」というテーマに関連の強い単語のみ表示されるようになった印象がありませんか?

ただし、この方法だと「情報」「予測」等、頻度としては(今回の文章中には)一回しか出現しませんが、重要な単語は、フィルターアウトされてしまいました。

次回は、こういった単語を上位に出現させるような、更なる高度な自然言語処理のテクニックを紹介させて頂きます。

ご期待下さい!