目次
- ベクトル化ってどういうこと?
- 単語をベクトル化する方法
- 生のカウントのベクトルを相互情報量を使って精度UP
- 無駄を取り除いて、ノイズに強いベクトルにする
- 単語ベクトルを使って「類似の単語」を判断する
- ベクトルの求め方には「推論ベース」もあります
ベクトル化ってどういうこと?
ベクトルは向きと大きさを持つ量です。
大きさだけを持つ量がスカラーです。
そのように学校では習いました。
でも、その定義を厳密に考え出すと迷うので、今回の「単語のベクトル化」で何をするかといえば、ある単語(例えば「私」みたいな)を[0,1,0,0,1,1,0,0]みたいな数値のリストで表現するようなことを意味すると、ざっくり理解することにします。
このようなにする理由は、単語を機械学習他のコンピュータ処理ための前処理です。
単語をベクトル化する方法
ベクトル「{0,1,0,0,1,1,0,0}」に変換する方法は色々と研究されています。
今回は、一番多く採用されていて、かつ、シンプルな「単語の意味は周囲の単語によって形成される」というルールを使う方法にします。
このルールを、例文「僕は君の笑顔が好き。君は僕の何が好き?」の「笑顔」を例にとって説明すると、この「笑顔」の意味は、両隣にある「君」と「好き」という言葉で説明されていると考えるということです。
実際には「笑顔」の両隣は「の」と「が」なんですが、これでは意味をなさないので、「の」「が」のように不適切なものは無視して考えることになってます。
それを踏まえて、以下のサンプルでは「名詞・動詞」だけ抜き出すようにしてます。
# -*- coding: utf-8 -*- from janome.tokenizer import Tokenizer import re pat = r'名詞|動詞' regex = re.compile(pat) t = Tokenizer() malist = t.tokenize("僕は君の笑顔が好き。君は僕の何が好き?") for n in malist: if(regex.match(n.part_of_speech)): print(n.surface,end=' ')
この処理の結果はこうです。
僕 君 笑顔 好き 君 僕 何 好き
単語を登場順にならべると、「僕、君、笑顔、好き、何」の5つです。
この5単語にインデックスをつけます。
ざっくり「0:僕、1:君、2:笑顔、3:好き、4:何」にしました。
そして、「基準となる単語の前後にある単語をカウントする」というルールで表をつくるとこうなります
・ | 0:僕 | 1:君 | 2:笑顔 | 3:好き | 4:何 |
0:僕 | 0 | 2 | 0 | 0 | 1 |
1:君 | 2 | 0 | 1 | 1 | 0 |
2:笑顔 | 0 | 1 | 0 | 1 | 0 |
3:好き | 0 | 1 | 1 | 0 | 1 |
4:何 | 1 | 0 | 0 | 1 | 0 |
「僕 君 笑顔 好き 君 僕 何 好き」で、「僕」の両隣は「僕 君 笑顔 好き 君 僕 何 好き」なので「君が2、何が1」となります。
次に、「君」に着目して同じようにすると、今度は「僕 君 笑顔 好き 君 僕 何 好き」なので、「僕2、笑顔1、好き1」となります。
上記表はそれにそって数字をうめてるわけです。
これで、「カウントベースの手法で単語をベクトル化した」ことになります。
僕={0,2,0,0,1}
君={2,0,1,1,0}
笑顔={0,1,0,1,0}
好き={0,1,1,0,1}
何={1,0,0,1,0}
生のカウントのベクトルを相互情報量を使って精度UP
上記のように生のカウントした数をそのまま使うのは、わかりやすいですが、ちょっと精度的に問題があり、普通は、確率的に処理して、もう少し上等のベクトルにします。
その方法を「相互情報量(PMI)」といいます。
例えば、上記の例で「僕」と「君」という2つの単語に着目すると。
- 僕が登場する確率→P(僕)
- 君が登場する確率→P(君)
- 僕と君が同じ文章内に同時に登場(共起するといいます)する確率→P(僕、君)
としたら、「P(僕、君)をP(僕)×P(君)で割り、その結果の2の対数をとる」という方法で計算することで、データとしての精度をあげるわけです。
実際にpythonで計算してみます。
インプットは上記で作成したベクトルを使ってます。
僕={0,2,0,0,1}
君={2,0,1,1,0}
笑顔={0,1,0,1,0}
好き={0,1,1,0,1}
何={1,0,0,1,0}
import numpy as np ar_1 = [[0,2,0,0,1],[2,0,1,1,0],[0,1,0,1,0],[0,1,1,0,1],[1,0,0,1,0]] eps = 1e-8 cm = np.array(ar_1) pmi = np.zeros_like(cm,dtype=np.float32) nsum = np.sum(cm) ssum = np.sum(cm,axis=0) for i in range(cm.shape[0]): for j in range(cm.shape[1]): pw = np.log2(cm[i,j] * nsum / (ssum[j] * ssum[i]) + eps) pmi[i,j] = max(0,pw) print(pmi)
出力結果です。
[[0. 1.2223924 0. 0. 1.2223924 ]
[1.2223924 0. 0.8073549 0.22239244 0. ]
[0. 0.8073549 0. 1.2223924 0. ]
[0. 0.22239244 1.2223924 0. 1.2223924 ]
[1.2223924 0. 0. 1.2223924 0. ]]
うん・・数字だけ見ても。ピンときません。
だた、数学的には「よりよい単語ベクトル」になったと考えて次にすすみます。
無駄を取り除いて、ノイズに強いベクトルにする
上記のより良い「単語ベクトル」ですが、改善の余地があります。
それは「0」の場所が多いことです。
データ処理で虫される「0」はようするに「無駄」です。
こういう(0が多い)データはノイズに弱いといわれます。
それを改善するために「次元圧縮」をします。
次元圧縮の手順は、ざっくり、以下のとおりです。
- 特異値分析(SVD)して、特異値の大きい順(ざっくり言えば、重要な要素順)に並べなおす。
- 特異値の大きいものから任意の数のみ抽出する
簡単にイメージをかくと。
僕=[0 , 1.2223924, 0 , 0 ,1.2223924 ]
この、5列(5次元)の単語ベクトルに、特異値分析(SVD)という計算をしかけると。
僕=[-0.45890802 0.5310011 -0.3688437 0.609104 -0.01969519]
という5次元のベクトルに変換されます。
この結果は前の方が「重要な要素」になっています。
なので、ノイズに強くするため、前の2列分だけ取り出し「2次元に次元圧縮」します。
[-0.45890802 0.5310011 ]
それを、pythonでやってみます。
import numpy as np ar_1 = [[0,2,0,0,1],[2,0,1,1,0],[0,1,0,1,0],[0,1,1,0,1],[1,0,0,1,0]] eps = 1e-8 cm = np.array(ar_1) pmi = np.zeros_like(cm,dtype=np.float32) nsum = np.sum(cm) ssum = np.sum(cm,axis=0) for i in range(cm.shape[0]): for j in range(cm.shape[1]): pw = np.log2(cm[i,j] * nsum / (ssum[j] * ssum[i]) + eps) pmi[i,j] = max(0,pw) print("PMI処理後") print(pmi) U,S,V = np.linalg.svd(pmi) print("SVD処理後") print(U) goal = U[:,:2] print("次元圧縮後") print(goal)
実行した結果です。
PMI処理後
[[0. 1.2223924 0. 0. 1.2223924 ]
[1.2223924 0. 0.8073549 0.22239244 0. ]
[0. 0.8073549 0. 1.2223924 0. ]
[0. 0.22239244 1.2223924 0. 1.2223924 ]
[1.2223924 0. 0. 1.2223924 0. ]]
SVD処理後
[[-0.45890802 0.5310011 -0.3688437 0.609104 -0.01969519]
[-0.41171417 -0.29887974 0.53542835 0.25440666 -0.6243045 ]
[-0.38795787 -0.1713638 -0.61455464 -0.5281229 -0.40439087]
[-0.48511937 0.4878484 0.4430743 -0.5141807 0.2568395 ]
[-0.48377967 -0.60112154 -0.05725896 0.14482397 0.61673135]]
次元圧縮後
[[-0.45890802 0.5310011 ]
[-0.41171417 -0.29887974]
[-0.38795787 -0.1713638 ]
[-0.48511937 0.4878484 ]
[-0.48377967 -0.60112154]]
この「次元圧縮後」の単語ベクトルが、最終的に使うものです。
単語と関連づけると。
僕 :[-0.45890802 0.5310011 ]
君 : [-0.41171417 -0.29887974]
笑顔 :[-0.38795787 -0.1713638 ]
好き : [-0.48511937 0.4878484 ]
何 : [-0.48377967 -0.60112154]
です。
単語ベクトルを使って「類似の単語」を判断する
ここまでできたら、とりあえず計算で「類似の単語」を判断できるようになります。
ベクトルの類似を計算する最も有名な方法は「コサイン類似度」です。
計算式とかはこちらをどうぞ。
2つのベクトルがどのくらい同じ方向を向いているか?で比較するものです。
計算式はややこしいですが、pythonで書くと簡単です。
単語ベクトル「x」と「y」を比較する式はこんな感じ。
eps = 1e-8 nx = x / (np.sqrt(np.sum(x ** 2)) + eps) ny = y / (np.sqrt(np.sum(y ** 2)) + eps) return np.dot(nx, ny)
実際に、上記の次元圧縮後のベクトルの、僕と君で計算してみます。
goal[0] が「僕」で[-0.45890802 0.5310011 ]という単語ベクトル。
goal[1]が「君」で[-0.41171417 -0.29887974]という単語ベクトルです。
eps = 1e-8 nx = goal[0] / (np.sqrt(np.sum(goal[0] ** 2)) + eps) ny = goal[1] / (np.sqrt(np.sum(goal[1] ** 2)) + eps) print(np.dot(nx, ny))
結果として求められる値は以下です。
0.08467308
ようするに、こんな感じである単語のベクトルと、他の単語のベクトルを計算して、結果の大きい順にならべれば「類似度の高い順」になるわけです。
自然言語処理のデモなんかで、キーワードの単語を入力して、類似する単語をリストするみたいなものがありますが、それは、こういうことをやっているわけです。
ベクトルの求め方には「推論ベース」もあります
今回のやり方はベクトル化では、一番わかりやすい方法なのですが、実践的な面では難があります。
このやり方は「全部をまとめて処理しないといけない」ので、文章データが大きくなって、対象の単語が増えるとエライことになることです。
単語が10万語でてきたら、10万×10万の行列を処理しないといけないとか・・。
なので、その制約を回避するWord2Vecのような「推論ベース」のベクトル化が実践的には使われることになるわけです。
ということで。
今回はこんなところで。
ではでは。