自然言語処理の基礎である形態素解析からbowによるベクトル化、TF-IDFによる重み付けまで解説
Pythonをそれなりに書いており、専門的にやっているわけではありませんが、自分も業務などで機械学習を行った経験が少しあり、Pythonをやっていれば機械学習や自然言語処理などに触れる機会があります。。
今回は自然言語処理系の機械学習では、ほぼ必ず行う「形態素解析」から文字列の「ベクトル化」までの流れを初心者向けに解説します。
使用環境としてはPython3.5以上を想定しています。
自然言語処理の前処理の基本の流れ
機械学習でいう文字列の学習を行うには、前処理と呼ばれる工程で機械学習の形式に扱えるような文字列の処理をする必要があります。基本的な流れは以下のようになります。
- 1. 形態素解析(品詞の分解を行う)
- 2. Bag of words(bow)による文字のベクトル化
- 3. TF-IDFによる文字の重み付け
ベクトル化とは
機械学習を行うには、データーである文字列を機械学習で扱える形式である数値に変換しなければいけません。
具体的に言うならば以下のような二つの種類の数値です。整数だけで文字列の内容を表したものや、少数で文字列を表したものもあります。
[[0 1 0 1 1 0 1 0 0]
[0 1 0 1 0 0 1 0 1]
[2 3 2 1 1 1 2 1 1]]
[[0. 0.46 0. 0.46 0.6 0. 0.46 0. 0. ]
[0. 0.46 0. 0.46 0. 0. 0.46 0. 0.6 ]
[0.5 0.44 0.5 0.15 0.19 0.25 0.29 0.25 0.19]]
上記のような文字列を数値の形式に変換することをベクトル化といいます。
1. 形態素解析を行う
英語のような単語間に区切りがあれば、単語ごとを数値に変換することは可能です。しかし、日本語のような文字間が繋がっている文章であれば、単語ごとに数値ん変換することはできません。
まず最初に文字列を数値に変換(ベクトル化)するには、日本語のような文字の場合は、文字列を以下のように分解する必要があります。
私はラーメンがとても大好きです。
['私', 'は', 'ラーメン', 'が', 'とても', '大好き', 'です', '。']
今回は形態素解析では有名なMecabを使います。Mecab自体のインストール方法は各環境によって違うため、各自で調べてください。pythonでMecabを扱えるようにするには、以下のコマンドで入れてください。
$ pip install mecab-python3
形態素解析を行うコードは以下になります。今回は分解された品詞の中で、名詞のみを使います。
#!/usr/bin/env python
"""
Test of MeCab library
"""
import MeCab
# MeCab による単語への分割関数 (名詞のみ残す)
def split_text_only_noun(text):
tagger = MeCab.Tagger()
words = []
for c in tagger.parse(text).splitlines()[:-1]:
surface, feature = c.split('\t')
pos = feature.split(',')[0]
if pos == '名詞':
words.append(surface)
return ' '.join(words)
text = '私達はラーメンが大好きです。'
split_text = split_text_only_noun(text)
# 私達 ラーメン 大好き
上記の関数の引数に「私はラーメンがとても大好きです。」を渡して実行すると、「私達 ラーメン 大好き」と出力されるはずです。
コード内を見てみると、' '.join(words)とあるようにListを半角スペース事に一つの文字列に結合しています。
2. Bag of words(bow)による文字のベクトル化
それでは形態素解析で文字列が品詞ごとで区切られたので、これらをベクトル化していきます。それがbag of wordsという手法です。
bowは単語の順番を無視してはいるものの、登場した単語の数を文書内で数えたものです。例を言うならば以下のような感じです。以下の二つの文章をbowでベクトル化するとします。
bag of wordsは以下の説明がわかりやすいです。
bag-of-words
'私達はラーメンがとても大好きです。'
'私達は蕎麦がとても大好きです。'
上記二つの文章をbowによるベクトル化をすると以下のようになります。
[[1 1 1 0]
[0 1 1 1]]
# それぞれの順番は以下の通り
# ['ラーメン', '大好き', '私達', '蕎麦']
Pythonでのサンプルコードだと以下のようになります。今回はscikit-learnのCountVectorizerを使用します。
#!/usr/bin/env python
import numpy as np
import MeCab
from sklearn.feature_extraction.text import CountVectorizer
# MeCab による単語への分割関数 (名詞のみ残す)
def split_text_only_noun(text):
tagger = MeCab.Tagger()
words = []
for c in tagger.parse(text).splitlines()[:-1]:
surface, feature = c.split('\t')
pos = feature.split(',')[0]
if pos == '名詞':
words.append(surface)
return ' '.join(words)
messages_list = [
split_text_only_noun('私達はラーメンがとても大好きです。'),
split_text_only_noun('私達は蕎麦がとても大好きです。')
]
docs = np.array(messages_list)
# print(messages_list)
# ['私達 ラーメン 大好き', '私達 蕎麦 大好き']
# bow ( bag of words )
count = CountVectorizer()
bags = count.fit_transform(docs)
print(bags.toarray())
# [[1 1 1 0]
# [0 1 1 1]]
features = count.get_feature_names()
print(features)
# ['ラーメン', '大好き', '私達', '蕎麦']
3. TF-IDFによる文字の重み付け
Bag of Wordsとは、文書内の特徴語(名詞などの品詞類)を取り出し、それが文書内で単語が何個かを数えてるだけです。
そこで、ある文書内での出現頻度が頻繁に多い単語には大きな値を付けたり、いくつもの文書で横断的に使われている単語に関してはそんなに重要ではないと決めて、低い値を付けたりします。これはTF-IDFと呼ばれる手法です。
TF-IDFは、TF(Term Frequency)とIDF(Inverse Document Frequency)を組み合わせたものです。
TF(Term Frequency)
各文書においてその単語がどのくらい出現したのかを表します。その文書内でたくさん出ている単語ほど重要としています。
IDF(Inverse Document Frequency)
全文書内で単語の出現があまりない場合は高い値、様々な文書に出現する単語なら低い値とし、様々な文書で横断的に使われている単語は重要な単語ではないとします。
これらを掛け合わせたものをTF-IDFとします。TFやIDFの導き出す方法などの数式などは以下のリンクを参照してください。
TF-IDFで文書内の単語の重み付け
tf-idfについてざっくりまとめ_理論編
scikit-learnを使用して、bowでベクトル化した文書をTF-IDFの形式にします。以下がそのサンプルコードになります。
import numpy as np
from sklearn.feature_extraction.text import TfidfTransformer
# tf-idf
tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
np.set_printoptions(precision=2)
tf_idf = tfidf.fit_transform(bags)
print(tf_idf.toarray())
# [[0.7 0.5 0.5 0. ]
# [0. 0.5 0.5 0.7]]
以下のようにそれぞれの単語が数から少数の重み付けの値へと変化しました。
[[1 1 1 0]
[0 1 1 1]]
[[0.7 0.5 0.5 0. ]
[0. 0.5 0.5 0.7]]
['ラーメン', '大好き', '私達', '蕎麦']
最初の文字列の場合は、'大好き'と'私達'がどちらの文章にも現れているため、0.5と値は低めになっています。その代わり、'ラーメン'と'蕎麦'が片方しか出ていない単語のため、0.7と重み付けの値が少し高めになっています。
流れを一つソースに落とす
上記の1〜3までの流れを以下のように一つのソースコードに落とすと以下のようになります。
今回はpython3.6のドキュメントの一部の文章を使用しました。こちらを形態素解析して、bag of wordsでベクトル化し、それを再度tf-idfで重み付けをします。
#!/usr/bin/env python
import numpy as np
import MeCab
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
# MeCab による単語への分割関数 (名詞のみ残す)
def split_text_only_noun(text):
tagger = MeCab.Tagger()
words = []
for c in tagger.parse(text).splitlines()[:-1]:
surface, feature = c.split('\t')
pos = feature.split(',')[0]
if pos == '名詞':
words.append(surface)
return ' '.join(words)
python_1 = '''
Python は強力で、学びやすいプログラミング言語です。
効率的な高レベルデータ構造と、シンプルで効果的なオブジェクト指向プログラミング機構を備えています。
Python は、洗練された文法・動的なデータ型付け・インタープリタであることなどから、
スクリプティングや高速アプリケーション開発(Rapid Application Development: RAD)に理想的なプログラミング言語となっています。
Python Web サイト(https://www.python.org) は、 Python インタープリタと標準ライブラリのソースコードと、
主要プラットフォームごとにコンパイル済みのバイナリファイルを無料で配布しています。
また、Python Web サイトには、無料のサードパーティモジュールやプログラム、ツール、ドキュメントなども紹介しています。
'''
python_2 = '''
Python インタプリタは、簡単に C/C++ 言語などで実装された関数やデータ型を組み込み、拡張できます。
また、アプリケーションのカスタマイズを行う、拡張言語としても適しています。
このチュートリアルは、Python 言語の基本的な概念と機能を、形式ばらずに紹介します。
読むだけではなく、Pythonインタープリタで実際にサンプルを実行すると理解が深まりますが、
サンプルはそれぞれ独立していますので、ただ読むだけでも良いでしょう。
'''
python_3 = '''
標準オブジェクトやモジュールの詳細は、 Python 標準ライブラリを参照してください。
また、正式な言語定義は、Python 言語リファレンスにあります。
C 言語や C++ 言語で拡張モジュールを書くなら、
Python インタプリタの拡張と埋め込み や Python/C API リファレンスマニュアル を参照してください。
Python の解説書も販売されています。
'''
python_4 = '''
このチュートリアルは、Python全体を対象とした、包括的な解説書ではありません。
よく使われる機能に限っても、全ては紹介していません。その代わり、このチュートリアルでは、
Pythonのもっとも特徴的な機能を中心に紹介して、この言語の持ち味や、スタイルを感じられるようにしています。
このチュートリアルを読み終えると、Python のモジュールやプログラムを読み書きできるようになっているでしょう。
また、Python 標準ライブラリ のさまざまな Python ライブラリモジュールを、詳しく調べられるようになっているはずです。
'''
messages_list = [
split_text_only_noun(python_1),
split_text_only_noun(python_2),
split_text_only_noun(python_3),
split_text_only_noun(python_4),
]
docs = np.array(messages_list)
print(messages_list)
# bow ( bag of words )
count = CountVectorizer()
bags = count.fit_transform(docs)
features = count.get_feature_names()
print('bag : ', bags)
print(bags.toarray())
print("count.vocabulary_ : ", count.vocabulary_)
print('features:', features)
print(features)
print("---------------------------------------------------")
# tf-idf
tfidf = TfidfTransformer(use_idf=True, norm='l2', smooth_idf=True)
np.set_printoptions(precision=2)
tf_idf = tfidf.fit_transform(bags)
print(tf_idf)
print(tf_idf.toarray())
TF-IDFの配列の内容は、以下のようになりました。(上記のtf_idf.toarray()の標準出力結果)
[[0. 0.12 0.12 0.12 0.12 0.38 0.12 0.12 0.24 0.12 0.12 0.12 0. 0.
0. 0. 0.09 0. 0.19 0. 0.12 0. 0.12 0.24 0. 0.12 0.12 0.12
0. 0.12 0. 0.12 0.12 0. 0.12 0.12 0.12 0.12 0.12 0.12 0.24 0.09
0. 0.08 0.08 0. 0.12 0. 0.12 0. 0. 0. 0.12 0.12 0.12 0.
0. 0.12 0. 0. 0. 0. 0. 0.12 0. 0. 0. 0.12 0. 0.08
0.12 0. 0. 0.12 0.12 0.24 0. 0. 0.12 0. 0. 0.08 0. 0.
0. 0. 0. 0. 0.12 0.12 0. 0.12]
[0. 0. 0. 0. 0. 0.3 0. 0. 0. 0. 0. 0. 0. 0.19
0. 0. 0.15 0.15 0.15 0. 0. 0.19 0. 0. 0.38 0. 0. 0.
0. 0. 0.15 0. 0. 0.19 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0.19 0. 0.19 0.19 0. 0. 0.19 0.3 0. 0. 0.19 0.
0. 0.15 0. 0. 0. 0. 0. 0.19 0. 0.19 0.19 0.12 0. 0.
0.36 0. 0. 0. 0. 0. 0.19 0. ]
[0.17 0. 0. 0. 0. 0.43 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0.13 0. 0.17 0. 0. 0. 0. 0. 0. 0. 0.
0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0.17 0.21 0.11 0.33 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0.33 0. 0. 0.17 0. 0. 0. 0. 0. 0.26 0. 0. 0. 0.21
0. 0. 0.17 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.17
0.42 0.17 0. 0.17 0. 0. 0. 0. ]
[0. 0. 0. 0. 0. 0.4 0. 0. 0. 0. 0. 0. 0.15 0.
0.15 0.46 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
0.15 0. 0.36 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.12
0. 0.19 0.19 0. 0. 0.15 0. 0.15 0.15 0.15 0. 0. 0. 0.15
0. 0. 0. 0. 0. 0. 0.15 0. 0. 0. 0.15 0. 0. 0.1
0. 0.24 0. 0. 0. 0. 0.15 0. 0. 0. 0. 0.19 0.15 0.
0.1 0. 0.15 0. 0. 0. 0. 0. ]]