Help us understand the problem. What is going on with this article?

基礎的な文章のベクトル化を考える

More than 1 year has passed since last update.

はじめに

今回は機械学習の勉強として僕が読んでいる、Python機械学習プログラミング 達人データサイエンティストによる理論と実践 の226ページあたりに書いてある、BoWで文章のベクトル化とTF-IDFを用いて、各文章の重みをつけるというところを詳しく解説していきたいと思います。BoWとは何か、またTF-IDFとは具体的にどのような計算をしているかを書いていきます。

なぜベクトル化しなければいけないのか?

ベクトルの定義は以下です。
大きさ、向きを指定して表現できる量のことをベクトルといいます。

n個の実数x_i(i=1,2,\cdots,n)を縦に並べたもの\\
X=
\begin{pmatrix}
x_1\\
x_2\\
x_3\\
\vdots\\
x_n\\
\end{pmatrix}\\
を実数対R上のn次元ベクトル、あるいは単に実ベクトルといいます。

文章をベクトルにする理由は、ずばり計算する際に扱いしやすくなるためです。
計算する際の計算量をベクトルに変換した方が効率に計算できるからでしょう。

どのようにベクトルにするか

今回はBoW(Back of word)という手法を用いて特徴ベクトルにしていきたいと思います。
なんだか難しそうに感じるかもしれませんが、BoWの考え方は非常に明快で、対象の文章で単語が何回出現したかをカウントするだけです。

対象の文章は3つのドキュメントで構成されており、以下の通りです。

The sun is shining
The weather is sweet
The sun is shining, the weather is sweet, and one and is two

この三行の文章の全ての単語を洗い出してみることから始めてみます。
The, sun, is, shining,weather, sweet, ,the, and, one, two
の10個ですね。今回は大文字と小文字を区別しないのでTheとtheを同じとみなします。
したがって要素は9個になります。これを一意に対応づけたいと思います。

scikit-learnという機械学習のライブラリのCountVectorizerを使います。

CountVectorizer.py
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
count = CountVectorizer()
docs = np.array(['The sun is shining',
                 'The weather is shining',
                 'the sun is shining, the weather is sweet, and one and one is two'])
bag = count.fit_transform(docs)
print(count.vocabulary_)

出力結果
{'is': 1, 'one': 2, 'sweet': 5, 'two': 7, 'shining': 3, 'sun': 4, 'the': 6, 'weather': 8, 'and': 0}

数字の並びは単純にアルファベット順のようです。
次に対象となっている3つのドキュメントにこれらの単語が何回出ているかをカウントしてみます。
人間がやると数えて、上の出力結果の順に合わせて回数を書けばいいのですが、少し大変です。
そこで、

CountVectorizer.py
bag = count.fit_transform(docs)
print(bag.toarray())

出力結果

\begin{align}
[[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]]
\end{align}

1つ目のドキュメントの [0 1 0 1 1 0 1 0 0]はそれぞれ、先に述べた、単語に対応しています。
andが0回、isが1回、oneが0回、shiningが1回、sunが1回、sweetが0回、theが1回、twoが0回、weatherが0回という意味になっており、2つ目のドキュメントと3つ目のドキュメントも同様の意味になっております。

しかし、これだけではドキュメント全体に出てくる、isやtheという単語が意味のない単語が何回も出てきてし合うので、単語の軽さ、重さを決めたいです。このためにTF-IDF(Term Frequency Inverse Document Frequency)という手法を用いて、特徴ベクトルに重み付けをします。

TF-IDFを用いて重み付けを行う。

TF(単語の出現頻度)、IDF(逆文書頻度)の積として定義されています。

tf-idf(t,d)=tf(t,d)×idf(t,d)\\
(t=単語,d=ドキュメントの個数)

また逆文書頻度であるidfは以下のように定義されています。

idf(t,d)=log\frac{n_d}{1+df(t,d)}\\
(t=単語,d=ドキュメントの個数)

以上が通常のTF-IDFの定義ですが、scikit-learnのTF-IDFの定義は若干異なります。
以下、それぞれの定義になります。

idf(t,d)=log\frac{1+n_d}{1+df(t,d)}\\
(t=単語,d=ドキュメントの個数)
tf-idf=tf(t,d)×(idf(t,d)+1)\\
(t=単語,d=ドキュメントの個数)

まずは手計算でtf-idfを計算してみましょう。
参考書ではisを計算いるので、まずはandを計算してみます。

まず、idfを計算しています。参考書でも3つ目のドキュメントを主に扱っているので3つ目のドキュメントを扱います。
'the sun is shining, the weather is sweet, and one and one is two'

idf("and",d_3)=log\frac{1+3}{1+2}\\
分子は1+ドキュメントの総数になるので毎回4になります。\\
分母は1+3つ目のドキュメントに存在するandの数です。andは二つあるので2になります。\\
log\frac{4}{2}=0.693\cdots\\
tf-idf("and",d_3)=2×(1+idf("and",a_3))=2×(1+0.693)=3.386\cdots≒3.39\\
tfとは全てのドキュメント(3つのドキュメント)のうちandが存在するドキュメントの個数です。\\
一つのドキュメントに2回andが存在していてもドキュメントとしては1つなので1になります。\\
(d_3は3つ目のドキュメントを表す。)

あともう一つくらい計算してみましょう。次はweatherのTF-IDFを計算してます。
分子は1+3(ドキュメント総数)と決まっているので、初めから4にしときます。

idf("weather",d_3)=log\frac{4}{1+2}=log\frac{4}{3}=0.287\cdots\\
tf-idf("weather",d_3)=1×(1+idf("weather",d_3))=1×1.287=1.287≒1.29

このようにして全ての単語のTF-IDFを計算していくと
[3.39,3.0,3.39,1.29,1.29,1.29,2.0,1.69,1.29] と求まります。

しかし、これで終わりではなく、最後にL2正規化を行う必要があります。
数式は以下の通りです。分母の部分がL2正規化です。

v_{norm}=\frac{v}{||v||_2}=\frac{v}{\sqrt{v_1^2+v_2^2+v_3^2+\cdots+v_n^2}}=\frac{v}{(\sum_{i=1}^nv_i^2)^{1/2}}

vをL2正規化で割ることによって1の長さのベクトルが返されます。

手計算では、流石にきついので、pythonで計算しましょう。
例えば最初の3.39の場合は

l2.py
import math
print(3.39/math.sqrt(3.39**2+3.0**2+3.39**2+1.29**2+1.29**2+1.29**2+2.0**2+1.69**2+1.29**2))

これを動かすと0.5025852626461178が返されます。小数点第2位で四捨五入すると0.50なので参考書通りです。
これをすべての単語で計算すれば、以下のようになります。
[0.5, 0.45, 0.5, 0.19, 0.19, 0.19, 0.3, 0.25, 0.19]

これにより、0~1の間で値をとるようになったのでより扱いやすくなりました。

pythonではこの作業を以下の4行で済みます。

tfidf.py
from sklearn.feature_extraction.text import TfidTransformer
tfidf = TfidTransformer(use_idf=True, norm='l2', smooth_idf=True)
np.set_printoptions(precsion=2)
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())

終わりに

もちろんコードで書けばこれまでなのですが、僕は正直最初にこれを書いても一体このコードは何を意味しているのかと分かりませんでした。ベクトル化できたとわかっても何だかモヤモヤして研究は進みませんでした(今も進んでいないかw)。だけど手計算で数式の意味を見ていくうちにコードが行っている意味がわかってきたので意味があったのかなと多います。

データマイニングの講義を行っている先生の言葉を借りると「体系的に学ぶ」ということが大切なんだと思います。当然時間はかかりますが、将来的には時間をかけて意味を理解した方が絶対いいでしょう。

以上が基本的なベクトル化になります。

この後にサポートベクトルマシンなどにかけるとより精度の高い分類が行うことができると思います。

あとがき

Bowは現在でも利用されていますが,今後使われるようになるであろう最近のモデルとしてgoogleが発表したword2vecというものがあります.参考URLを載せておきます.
https://code.google.com/archive/p/word2vec/

参考資料:
Python機械学習プログラミング 達人データサイエンティストによる理論と実践

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした