コンピュータに単語の意味を理解させるためには。

  • シソーラスによる手法
  • カウントベースの手法
  • 推論ベースの手法 (word2vec) (これは次章)

2.2 シソーラス

シソーラス、同じ意味の単語が同じグループに分類されている辞書。 この手法の問題は、人手で辞書を作成しなければならないこと。

  • 時代の変化に対応するのが困難
  • 人な作業コストが高い
  • 単語の細かなニュアンスを表現できない

2.3 カウントベースの手法

  • コーパスとは?大量のテキストデータが、自然言語処理の研究やアプリケーションのために目的をもって収集されたテキストデータ。
    • Wikipedia, Google News, シェイクスピア, 夏目漱石

ここでは、"You say goodby and I say hello." という文章を使用する。

In [1]:
text = 'You say goodby and I say hello.'
text = text.lower()
text = text.replace('.', ' .')
text
Out[1]:
'you say goodby and i say hello .'
In [3]:
words = text.split()
words
Out[3]:
['you', 'say', 'goodby', 'and', 'i', 'say', 'hello', '.']

次に、Pythonのディクショナリを作成して、単語にIDを振ることにする。最後に、文章をIDリストに変換する。

In [10]:
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}
Out[10]:
array([0, 1, 2, 3, 4, 1, 5, 6])
In [13]:
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.3.2 単語の分散表現

単語を単語として表現するのではなく、よりコンパクトで理にかなったベクトルとして表現することを、「単語の分散表現」と呼ぶ。

2.3.3 分布仮説

「単語の意味は、周囲の単語によって形成される」という仮説を「分布仮説」と呼ぶ。

  • コンテキスト 注目する単語に対して、その周囲に存在する単語を「コンテキスト」と呼ぶ。
  • ウィンドウサイズ 注目する単語に対する、コンテキストのサイズ。左右の2つの単語までコンテキストに含むなら、ウィンドウサイズは2である。

2.3.4 共起行列

単語をベクトルで表す方法として素直な方法は、周囲の単語をカウントすること。 例えば上記の例であれば、7つの単語が登場しているので、行列として周囲の単語をカウントする。

In [14]:
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: '.'}
In [24]:
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))
Out[24]:
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)

2.3.5 ベクトル間の類似度

ベクトル間の類似度を計測する方法は様々あるが、ここでは、「コサイン類似度」を使用する。

下記のcos_similarityの実装において、epsを指定しているのはゼロ除算を避けるため。

In [26]:
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...となり、比較的高いことが分かる。

2.3.6 類似単語のランキング表示

In [27]:
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

上記の手法では、'goodbye' や 'hello' に類似度があるのは感覚とズレがあるため、これを改善する。

2.4 カウントベース手法の改善

2.4.1 相互情報量

相互情報量というのは、単語xと単語yの発生確率と、xyが同時に共起する確率から以下のように表現される。

[tex: PMI(x,y) = \log_2\dfrac{P(x,y)}{P(x)P(y)}]

これを使ってCorpusから相互情報量の行列を作成する。

In [28]:
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となる空間が多いので、次にベクトルの削減を行う。

2.4.2 次元削減

次元削減を行う手法の一つとして、特異値分解(Singlar Value Decomposition:SVD)を行う。

[tex: X = USV^{T}]

In [31]:
# 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'が近いので、ある程度直観に近い。

In [34]:
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)
Out[34]:
<matplotlib.collections.PathCollection at 0x7f0613bcfbe0>

2.4.4 PTBデータセット

PTBデータセットは、Penn Treebankとyばれるコーパス。本格的なコーパスである。

In [35]:
# 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

2.4.5 PTBデータセットでの評価

PTBデータセットを使ってカウントベースの手法を評価する。 SVDは自前のものを使ってもよいが、高速化するためにsklearnモジュールを使用する。

In [36]:
# 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を使用し、乱数を使うので実行結果は毎回異なる。