コンピュータに単語の意味を理解させるためには。
シソーラス、同じ意味の単語が同じグループに分類されている辞書。 この手法の問題は、人手で辞書を作成しなければならないこと。
ここでは、"You say goodby and I say hello." という文章を使用する。
text = 'You say goodby and I say hello.'
text = text.lower()
text = text.replace('.', ' .')
text
'you say goodby and i say hello .'
words = text.split()
words
['you', 'say', 'goodby', 'and', 'i', 'say', 'hello', '.']
次に、Pythonのディクショナリを作成して、単語にIDを振ることにする。最後に、文章をIDリストに変換する。
word_to_id = {}
id_to_word = {}
for word in words:
if word not in word_to_id:
new_id = len(word_to_id)
word_to_id[word] = new_id
id_to_word[new_id] = word
print(id_to_word)
print(word_to_id)
import numpy as np
corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
corpus
{0: 'you', 1: 'say', 2: 'goodby', 3: 'and', 4: 'i', 5: 'hello', 6: '.'} {'you': 0, 'say': 1, 'goodby': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
array([0, 1, 2, 3, 4, 1, 5, 6])
import numpy as np
def preprocess(text):
text = text.lower()
text = text.replace('.', ' .')
words = text.split()
word_to_id = {}
id_to_word = {}
for word in words:
if word not in word_to_id:
new_id = len(word_to_id)
word_to_id[word] = new_id
id_to_word[new_id] = word
corpus = np.array([word_to_id[w] for w in words])
return corpus, word_to_id, id_to_word
text = 'You say goodby and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
単語を単語として表現するのではなく、よりコンパクトで理にかなったベクトルとして表現することを、「単語の分散表現」と呼ぶ。
「単語の意味は、周囲の単語によって形成される」という仮説を「分布仮説」と呼ぶ。
注目する単語に対して、その周囲に存在する単語を「コンテキスト」と呼ぶ。
注目する単語に対する、コンテキストのサイズ。左右の2つの単語までコンテキストに含むなら、ウィンドウサイズは2である。
単語をベクトルで表す方法として素直な方法は、周囲の単語をカウントすること。 例えば上記の例であれば、7つの単語が登場しているので、行列として周囲の単語をカウントする。
text = 'You say goodbye and I say hello.'
corupus, word_to_id, id_to_word = preprocess(text)
print (corpus)
print (id_to_word)
[0 1 2 3 4 1 5 6] {0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
def create_co_matrix(corpus, vocab_size, window_size=1):
corpus_size = len(corpus)
co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)
for idx, word_id in enumerate(corpus):
for i in range(1, window_size+1):
left_idx = idx - i
right_idx = idx + i
if left_idx >= 0:
left_word_id = corpus[left_idx]
co_matrix[word_id, left_word_id] += 1
if right_idx < corpus_size:
right_word_id = corpus[right_idx]
co_matrix[word_id, right_word_id] += 1
return co_matrix
create_co_matrix(corpus, len(id_to_word))
array([[0, 1, 0, 0, 0, 0, 0], [1, 0, 1, 0, 1, 1, 0], [0, 1, 0, 1, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0], [0, 1, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0, 1], [0, 0, 0, 0, 0, 1, 0]], dtype=int32)
ベクトル間の類似度を計測する方法は様々あるが、ここでは、「コサイン類似度」を使用する。
下記のcos_similarity
の実装において、epsを指定しているのはゼロ除算を避けるため。
def cos_similarity(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)
vocab_size = len(word_to_id)
C = create_co_matrix(corpus, vocab_size)
c0 = C[word_to_id['you']]
c1 = C[word_to_id['i']]
print(cos_similarity(c0, c1))
0.7071067691154799
上記の結果から、'you'と'i'の類似度は0.70...となり、比較的高いことが分かる。
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
if query not in word_to_id:
print('%s is not found' % query)
return
print('\n[query] ' + query)
query_id = word_to_id[query]
query_vec = word_matrix[query_id]
vocab_size = len(id_to_word)
similarity = np.zeros(vocab_size)
for i in range(vocab_size):
similarity[i] = cos_similarity(word_matrix[i], query_vec)
count = 0
for i in (-1 * similarity).argsort():
if id_to_word[i] == query:
continue
print(' %s %s' % (id_to_word[i], similarity[i]))
count += 1
if count >= top:
return
most_similar('you', word_to_id, id_to_word, C, top=5)
[query] you goodbye 0.7071067691154799 i 0.7071067691154799 hello 0.7071067691154799 say 0.0 and 0.0
def ppmi(C, verbose=False, eps = 1e-8):
'''PPMI(正の相互情報量)の作成
:param C: 共起行列
:param verbose: 進行状況を出力するかどうか
:return:
'''
M = np.zeros_like(C, dtype=np.float32)
N = np.sum(C)
S = np.sum(C, axis=0)
total = C.shape[0] * C.shape[1]
cnt = 0
for i in range(C.shape[0]):
for j in range(C.shape[1]):
pmi = np.log2(C[i, j] * N / (S[j]*S[i]) + eps)
M[i, j] = max(0, pmi)
if verbose:
cnt += 1
if cnt % (total//100) == 0:
print('%.1f%% done' % (100*cnt/total))
return M
W = ppmi(C)
np.set_printoptions(precision=3)
print('covariance matrix')
print(C)
print('-'*50)
print('PPMI')
print(W)
covariance matrix [[0 1 0 0 0 0 0] [1 0 1 0 1 1 0] [0 1 0 1 0 0 0] [0 0 1 0 1 0 0] [0 1 0 1 0 0 0] [0 1 0 0 0 0 1] [0 0 0 0 0 1 0]] -------------------------------------------------- PPMI [[0. 1.807 0. 0. 0. 0. 0. ] [1.807 0. 0.807 0. 0.807 0.807 0. ] [0. 0.807 0. 1.807 0. 0. 0. ] [0. 0. 1.807 0. 1.807 0. 0. ] [0. 0.807 0. 1.807 0. 0. 0. ] [0. 0.807 0. 0. 0. 0. 2.807] [0. 0. 0. 0. 0. 2.807 0. ]]
PPMI行列を作成したが、この作成には時間がかかる。また、0となる空間が多いので、次にベクトルの削減を行う。
次元削減を行う手法の一つとして、特異値分解(Singlar Value Decomposition:SVD)を行う。
[tex: X = USV^{T}]
# SVDによる次元削減
U, S, V = np.linalg.svd(W)
print(C[0])
print(W[0])
print(U[0])
[0 1 0 0 0 0 0] [0. 1.807 0. 0. 0. 0. 0. ] [ 3.409e-01 0.000e+00 -1.205e-01 -3.886e-16 -9.323e-01 -1.110e-16 -2.426e-17]
各単語を2次元のベクトルで表し、それをグラフにプロットする。
'i' と 'you', 'goodbye' と 'hello'が近いので、ある程度直観に近い。
import matplotlib.pyplot as plt
for word, word_id in word_to_id.items():
plt.annotate(word, (U[word_id, 0], U[word_id, 1]))
plt.scatter(U[:,0], U[:,1], alpha=0.5)
<matplotlib.collections.PathCollection at 0x7f0613bcfbe0>
PTBデータセットは、Penn Treebankとyばれるコーパス。本格的なコーパスである。
# coding: utf-8
import sys
sys.path.append('..')
from dataset import ptb
corpus, word_to_id, id_to_word = ptb.load_data('train')
print('corpus size:', len(corpus))
print('corpus[:30]:', corpus[:30])
print()
print('id_to_word[0]:', id_to_word[0])
print('id_to_word[1]:', id_to_word[1])
print('id_to_word[2]:', id_to_word[2])
print()
print("word_to_id['car']:", word_to_id['car'])
print("word_to_id['happy']:", word_to_id['happy'])
print("word_to_id['lexus']:", word_to_id['lexus'])
Downloading ptb.train.txt ... Done corpus size: 929589 corpus[:30]: [ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29] id_to_word[0]: aer id_to_word[1]: banknote id_to_word[2]: berlitz word_to_id['car']: 3856 word_to_id['happy']: 4428 word_to_id['lexus']: 7426
PTBデータセットを使ってカウントベースの手法を評価する。 SVDは自前のものを使ってもよいが、高速化するためにsklearnモジュールを使用する。
# coding: utf-8
import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import ptb
window_size = 2
wordvec_size = 100
corpus, word_to_id, id_to_word = ptb.load_data('train')
vocab_size = len(word_to_id)
print('counting co-occurrence ...')
C = create_co_matrix(corpus, vocab_size, window_size)
print('calculating PPMI ...')
W = ppmi(C, verbose=True)
print('calculating SVD ...')
try:
# truncated SVD (fast!)
from sklearn.utils.extmath import randomized_svd
U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
random_state=None)
except ImportError:
# SVD (slow)
U, S, V = np.linalg.svd(W)
word_vecs = U[:, :wordvec_size]
querys = ['you', 'year', 'car', 'toyota']
for query in querys:
most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
counting co-occurrence ... calculating PPMI ... 1.0% done 2.0% done 3.0% done 4.0% done 5.0% done 6.0% done 7.0% done 8.0% done 9.0% done 10.0% done 11.0% done 12.0% done 13.0% done 14.0% done 15.0% done 16.0% done 17.0% done 18.0% done 19.0% done 20.0% done 21.0% done 22.0% done 23.0% done 24.0% done 25.0% done 26.0% done 27.0% done 28.0% done 29.0% done 30.0% done 31.0% done 32.0% done 33.0% done 34.0% done 35.0% done 36.0% done 37.0% done 38.0% done 39.0% done 40.0% done 41.0% done 42.0% done 43.0% done 44.0% done 45.0% done 46.0% done 47.0% done 48.0% done 49.0% done 50.0% done 51.0% done 52.0% done 53.0% done 54.0% done 55.0% done 56.0% done 57.0% done 58.0% done 59.0% done 60.0% done 61.0% done 62.0% done 63.0% done 64.0% done 65.0% done 66.0% done 67.0% done 68.0% done 69.0% done 70.0% done 71.0% done 72.0% done 73.0% done 74.0% done 75.0% done 76.0% done 77.0% done 78.0% done 79.0% done 80.0% done 81.0% done 82.0% done 83.0% done 84.0% done 85.0% done 86.0% done 87.0% done 88.0% done 89.0% done 90.0% done 91.0% done 92.0% done 93.0% done 94.0% done 95.0% done 96.0% done 97.0% done 98.0% done 99.0% done 100.0% done calculating SVD ... [query] you i: 0.700317919254303 we: 0.6367185115814209 anybody: 0.565764307975769 do: 0.563567042350769 'll: 0.5127798318862915 [query] year month: 0.6961644291877747 quarter: 0.6884941458702087 earlier: 0.6663320660591125 last: 0.6281364560127258 next: 0.6175755858421326 [query] car luxury: 0.6728832125663757 auto: 0.6452109813690186 vehicle: 0.6097723245620728 cars: 0.6032834053039551 corsica: 0.5698372721672058 [query] toyota motor: 0.7585658431053162 nissan: 0.7148030996322632 motors: 0.6926157474517822 lexus: 0.6583304405212402 honda: 0.6350275278091431
sklearnのrandomized_svd()
というメソッドを使用する。Truncated SVDを使用し、乱数を使うので実行結果は毎回異なる。