からっぽのしょこ

読んだら書く!書いたら読む!読書読読書読書♪同じ事は二度調べ(たく)ない

2.3.1-4:共起行列【ゼロつく2のノート(実装)】

はじめに

 『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。

 本の内容を1つずつ確認しながらゆっくりと組んでいきます。

 この記事は、2.3.1項「Pythonによるコーパスの下準備」と2.3.4項「共起行列」の内容です。テキスト中のある単語の前後の単語の出現回数から単語ベクトルを作成します。また全ての単語のベクトルをまとめた共起行列の作成関数をPythonで実装します。

【前節の内容】

www.anarchive-beta.com

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

2.3.1 Pythonによるコーパスの下準備

 この項では、テキスト(文章)の前処理関数を実装します。

# NumPyをインポート
import numpy as np


・処理の確認

 例として、簡単なテキストを用意します。

# テキストを設定
text = 'You say goodbye and I say hello.'

 この節では、このテキストを用いて必要な処理を確認していきます。

 テキスト自体を調整します。

# 小文字に変換
text = text.lower()
print(text)

# ピリオドの前にスペースを挿入
text = text.replace('.', ' .')
print(text)

# 単語ごとに分割
words = text.split(' ')
print(words)
you say goodbye and i say hello.
you say goodbye and i say hello .
['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello', '.']


 分割した単語と、単語ごとに通し番号を割り振ったIDを2つのディクショナリに格納します。ただし単語が重複しないようにします。

# ディクショナリを初期化
word_to_id = {}
id_to_word = {}

# 未収録の単語をディクショナリに格納
for word in words:
    if word not in word_to_id: # 未収録の単語のとき
        # 次の単語のidを取得
        new_id = len(word_to_id)
        
        # 単語IDを格納
        word_to_id[word] = new_id
        
        # 単語を格納
        id_to_word[new_id] = word

 textに含まれる単語のリストwordsから1語ずつ取り出してディクショナリに格納していきます。ただし既にword_to_idに格納されている単語であれば、not in(含まれていない)がFalseとなるので、if文の処理を行わず次の単語に移ります。これで単語が重複するのを防ぎます。

 2つのディクショナリの要素を確認しましょう。

# 単語IDを指定すると単語を返す
print(id_to_word)
print(id_to_word[5])

# 単語を指定すると単語IDを返す
print(word_to_id)
print(word_to_id['hello'])
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
hello
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
5


 textに含まれる単語を出現順に格納したリストをwordsとしました。それを単語IDのリストに変換します。

# リストを初期化
corpus = []

# 単語ごとにIDをリストに格納
for word in words:
    # リストに追加
    corpus.append(word_to_id[word])

# NumPy配列に変換
corpus = np.array(corpus)
print(corpus)
[0 1 2 3 4 1 5 6]

 これは、元のテキストを構成する単語を単語IDで表現したものです。

 このfor文の処理は、リスト内包表記という機能を使うことで一行で行えます。

# リストに変換
corpus = [word_to_id[word] for word in words]

# NumPy配列に変換
corpus = np.array(corpus)
print(corpus)
[0 1 2 3 4 1 5 6]

 word_to_idとは、単語IDを重複して格納している点が異なります。word_to_idid_to_wordからは、元のテキストを再現することはできません。

・実装

 ではここまでの処理を関数として実装します。

# 前処理関数の実装
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: # 未収録の単語のとき
            # 次の単語のidを取得
            new_id = len(word_to_id)
            
            # 単語をキーとして単語IDを格納
            word_to_id[word] = new_id
            
            # 単語IDをキーとして単語を格納
            id_to_word[new_id] = word
    
    # 単語IDリストを作成
    corpus = [word_to_id[w] for w in words]
    
    return corpus, word_to_id, id_to_word # (受け取るのに3つの変数が必要!)


 実装した関数を試してみましょう。

# テキストを設定
text = 'You say goodbye and I say hello.'

# 単語と単語IDに関する変数を取得
corpus, word_to_id, id_to_word = preprocess(text)
print(id_to_word)
print(word_to_id)
print(corpus)
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
[0, 1, 2, 3, 4, 1, 5, 6]

 同じものを作成できました。

 ここまでの処理が前処理になります。次はここで用意した各種変数を用いて、単語ベクトルを作成します。

2.3.4 共起行列

 各単語ベクトルを行方向に並べた行列を共起行列と呼びます。この項では、共起行列を作成するための関数を実装します。

・処理の確認

 まずは共起行列の作成に必要な値を設定します。注目する単語の前後何語までを範囲とするかの値をウインドウサイズと呼ぶことにします。

# ウィンドウサイズを指定
wndow_size = 1

# 単語の種類数を取得
vocab_size = len(word_to_id)
print(vocab_size)

# 総単語数を取得
corpus_size = len(corpus)
print(corpus_size)
7
8

 「say」が重複しているため、単語の種類数vocab_sizeよりも総単語数corpus_sizeの方が大きくなります。

 共起行列の受け皿を作成します。行・列共に単語の種類数になります。

# 共起行列を初期化
co_matrix = np.zeros((vocab_size, vocab_size), dtype=np.int32)
print(co_matrix)
print(co_matrix.shape)
[[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 0 0 0]
 [0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0]]
(7, 7)

 ここにカウントしていきます。

 コーパス(テキスト)の7語目の単語「hello」に注目してみます。ただしPythonのインデックスは0から数えるので6を指定します。

# 単語インデックスを指定
idx = 6

# 指定した単語のIDを取得
word_id = corpus[idx]
print(word_id)
print(id_to_word[word_id])
5
hello

 idxはテキストのインデックス(何語目か)を表す値で、word_idは単語IDを表す値です。

 対象となる単語の左右のインデックスを計算します。

# 左隣のインデックス
left_idx = idx - 1
print(left_idx)

# 右隣のインデックス
right_idx = idx + 1
print(right_idx)
5
7

 ただし対象となる単語が、最初の要素のときは左隣に単語は存在せず、このときleft_idxが負の値になります。また最後の単語のときは右隣に単語は存在せず、このときright_idxcorpus_sizeを上回ります。そのため条件分岐によって処理を制御する必要があります。
 またウィンドウサイズを1より大きい値にする場合は、1つ隣だけでなく更にその隣にも同じ処理を行う必要があります。これはfor文でループ処理します。

 対象となる単語より左側にある単語のカウントします。

# 左隣の単語IDを取得
left_word_id = corpus[left_idx]
print(left_word_id)
print(id_to_word[left_word_id])

# 共起行列に記録(加算)
co_matrix[word_id, left_word_id] += 1
print(co_matrix)
1
say
[[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 0 0 0]
 [0 0 0 0 0 0 0]
 [0 1 0 0 0 0 0]
 [0 0 0 0 0 0 0]]

 行が対象となる単語で、列が範囲内の単語です。

 続いて右側にも同様に行います。

# 右隣の単語IDを取得
right_word_id = corpus[right_idx]
print(right_word_id)
print(id_to_word[right_word_id])

# 共起行列に記録(加算)
co_matrix[word_id, right_word_id] += 1
print(co_matrix)

# 対象の単語ベクトル
print(co_matrix[word_id])
6
.
[[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 0 0 0]
 [0 0 0 0 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 0 0]]
[0 1 0 0 0 0 1]

 ここまでの処理を全ての単語で繰り返し行い、共起行列を作成します。

 for文の際にenumerate()を使うと、要素のインデックスと要素を同時に受け取ることができます。

# インデックスと要素を取得
for idx, element in enumerate(['rio', 'homa', 'mei']):
    # インデックスを表示
    print(idx)
    
    # 要素を表示
    print(element)
0
rio
1
homa
2
mei


・実装

 では処理の確認ができたので、共起行列を作成する関数を実装します。

# 共起行列作成関数の実装
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)
    
    # 1語ずつ処理
    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: # 対象の単語が最初の単語でないとき
                # 単語IDを取得
                left_word_id = corpus[left_idx]
                
                # 共起行列にカウント
                co_matrix[word_id, left_word_id] += 1
            
            # 右側の単語の処理
            if right_idx < corpus_size: # 対象の単語が最後の単語でないとき
                # 単語IDを取得
                right_word_id = corpus[right_idx]
                
                # 共起行列にカウント
                co_matrix[word_id, right_word_id] += 1
    
    return co_matrix


 実装した関数を試してみましょう。

# テキストを設定
text = 'You say goodbye and I say hello.'

# 単語と単語IDに関する変数を取得
corpus, word_to_id, id_to_word = preprocess(text)

# 単語の種類数を取得
vocab_size = len(word_to_id)

# 共起行列を作成
co_matrix = create_co_matrix(corpus, vocab_size, window_size=1)
print(co_matrix)
print(co_matrix.shape)
[[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]]
(7, 7)

 図2-7の共起行列を作成できました。

 次項では、共起行列の各単語ベクトル(各行)を用いて単語間の類似度を求めます。

参考文献

  • 斎藤康毅『ゼロから作るDeep Learning 2――自然言語処理編』オライリー・ジャパン,2018年.

おわりに

 2章1つ目の記事です。最近は他の本と並行して進めているので、1巻のときほどポンポンと更新できませんが順調に進んでます。

 ところでバック・トゥ・ザ・フューチャーは私が2番目に好きな洋画シリーズです。

 2章の挿入歌です♪


Let's say "Hello!"

【次節の内容】

https://www.anarchive-beta.com/entry/2020/09/07/190000www.anarchive-beta.com