からっぽのしょこ

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

3.3:学習データの準備【ゼロつく2のノート(実装)】

はじめに

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

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

 この記事は、3.3節「学習データの準備」の内容です。CBOWモデルの入力データの前処理を説明して、Pythonで実装します。

【前節の内容】

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

【他の節の内容】

www.anarchive-beta.com

【この節の内容】

3.3.1 コンテキストとターゲット

 テキストからコンテキストとターゲットを抽出する関数を実装します。2章で行った前処理に加えて、この節で行う処理までがCBOWモデルの前処理になります。

・処理の確認

 まずは2章で行った前処理をします。前処理関数preprocess()は、2.3.2項で実装しました。

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

# 前処理
corpus, word_to_id, id_to_word = preprocess(text)
print(word_to_id)
print(id_to_word)
print(corpus)
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
[0, 1, 2, 3, 4, 1, 5, 6]


 テキストの単語を単語IDに変換したcorpusからターゲットを抽出します。

 ターゲットはコンテキストの中央の単語なので、corpusの始めと終わりのウインドウサイズ分の単語は含めません。

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

# ターゲットを抽出
target = corpus[window_size:-window_size]
print(target)
[1, 2, 3, 4, 1, 5]

 ターゲット数は、コーパスの単語数からウインドウサイズの2倍の数を引いた数になります。

 ターゲットの抽出にはスライス機能を使っています。配列の一部を取り出すスライスに、負の値を指定すると後から数えた要素を返します。

# NumPy配列を作成
arr = np.arange(10)
print(arr)

# スライス
print(arr[-5]) # 後から5番目の要素
print(arr[:-5]) # 後から5番目未満の要素
[0 1 2 3 4 5 6 7 8 9]
5
[0 1 2 3 4]


 次にコンテキストを抽出します。

 ターゲットの単語に対して、for文で前後ウィンドウサイズの範囲の単語を順番に抽出しcsに格納します。つまりウィンドウサイズを1とすると、corpusにおけるターゲットのインデックスidxに対して、1つ前(idx - window_size)から1つ後(idx + window_size)までの範囲の単語を順番にcs格納します。ただしターゲット自体の単語はコンテキストに含めません。ただしrange()は第2引数未満の値を生成するので、+1した値を指定します。

 ターゲットのコンテキストのリストcsを、全てのコンテキストcontextsの行として格納します。

# コンテキストを初期化(受け皿を作成)
contexts = []

# 1つ目のターゲットのインデックス
idx = window_size

# 1つ目のターゲットのコンテキストを初期化(受け皿を作成)
cs = []

# 1つ目のターゲットのコンテキストを1単語ずつ格納
for t in range(-window_size, window_size + 1):
    
    # tがターゲットのインデックスのとき処理しない
    if t == 0:
        continue
    
    # コンテキストを格納
    cs.append(corpus[idx + t])
    print(cs)

# 1つ目のターゲットのコンテキストを格納
contexts.append(cs)
print(contexts)
[0]
[0, 2]
[[0, 2]]

 この処理を、1つ目のターゲットのインデックスの値window_sizeから、最後のターゲットのインデックスの値len(corpus) - window_sizeまでfor文で繰り返し行います。

・実装

 処理の確認ができたので、関数として実装します。

# コンテキストとターゲットの作成関数の実装
def create_contexts_target(corpus, window_size=1):
    
    # ターゲットを抽出
    target = corpus[window_size:-window_size]
    
    # コンテキストを初期化
    contexts = []
    
    # ターゲットごとにコンテキストを格納
    for idx in range(window_size, len(corpus) - window_size):
        
        # 現在のターゲットのコンテキストを初期化
        cs = []
        
        # 現在のターゲットのコンテキストを1単語ずつ格納
        for t in range(-window_size, window_size + 1):
            
            # 0番目の要素はターゲットそのものなので処理を省略
            if t == 0:
                continue
            
            # コンテキストを格納
            cs.append(corpus[idx + t])
            
        # 現在のターゲットのコンテキストのセットを格納
        contexts.append(cs)
    
    # NumPy配列に変換
    return np.array(contexts), np.array(target) ## (受け取り時に変数が2つ必要!)


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

# コンテキストとターゲットを作成
contexts, target = create_contexts_target(corpus, window_size=1)
print(contexts)
print(target)
[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
[1 2 3 4 1 5]


 以上で単語IDを格納したコンテキストとターゲットを作成できました。次項では、各単語IDをone-hot表現に変換します。

3.3.2 one-hot表現への変換

 単語IDを要素とするコンテキストとターゲットをone-hot表現のコンテキストとターゲットに変換する関数を実装します。

・処理の確認

 基本的な処理は、単語の種類数個の0を要素とするベクトルを作成し、単語ID番目の要素だけを1に置き換えます。このとき単語IDに対応する値である1つの要素をベクトルに変換するため、次元が1つ増えます。

 まずは、前項で作成した1次元配列のターゲットtargetを変換しましょう。
 targetは、要素数がターゲット数のベクトルです。変換後は、ターゲット数の行数、単語の種類数の列数の2次元配列になります。つまり、行が各ターゲットの単語、列が各単語IDに対応します。そして行ごとに1つだけ、値が1の要素を持ちます。

 np.zeros()で変換後の形状の2次元配列を作成し、for文で行ごとに単語ID番目の要素を1を代入します。

 enumerate()は、引数に渡したリストの要素とその要素のインデックスを出力します。

# ターゲットを確認
print(target)
print(target.shape)

# ターゲットの単語数を取得
N = target.shape[0]

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

# 全ての要素が0の変換後の形状の2次元配列を作成
one_hot = np.zeros((N, vocab_size), dtype=np.int32)
print(one_hot)

# 単語ID番目の要素を1に置換
for idx, word_id in enumerate(target):
    one_hot[idx, word_id] = 1
print(one_hot)
print(one_hot.shape)
[1 2 3 4 1 5]
(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 0 0 0 0 0 0]]
[[0 1 0 0 0 0 0]
 [0 0 1 0 0 0 0]
 [0 0 0 1 0 0 0]
 [0 0 0 0 1 0 0]
 [0 1 0 0 0 0 0]
 [0 0 0 0 0 1 0]]
(6, 7)

 行方向に単語の種類数分拡張されました。

 続いて、前項で作成した2次元配列のコンテキストcontextsを変換しましょう。
 contextsは、0次元目の要素数がターゲット数(N)、1次元目の要素数がウィンドウサイズの2倍(C)の2次元配列です。変換後は、3次元方向に単語の種類数vocab_size分拡張されます。

 np.zeros()の第1引数に(N, C, vocab_size)を指定して、全ての要素が0の変換後の形状の3次元配列を作成します。

# コンテキストを確認
print(contexts)
print(contexts.shape)

# ターゲットの単語数を取得
N = contexts.shape[0]

# コンテキストサイズを取得
C = contexts.shape[1]

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

# 全ての要素が0の変換後の形状の3次元配列を作成
one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
print(one_hot)

# 単語ID番目の要素を1に置換
for idx_0, word_ids in enumerate(contexts): # 0次元方向
    for idx_1, word_id in enumerate(word_ids): # 1次元方向
        one_hot[idx_0, idx_1, word_id] = 1
print(one_hot)
print(one_hot.shape)
[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
(6, 2)
[[[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]
  [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 1 0 0 0 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 1 0 0 0]]

 [[0 0 1 0 0 0 0]
  [0 0 0 0 1 0 0]]

 [[0 0 0 1 0 0 0]
  [0 1 0 0 0 0 0]]

 [[0 0 0 0 1 0 0]
  [0 0 0 0 0 1 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 0 0 0 1]]]
(6, 2, 7)


 このように、入力データが2次元配列なのか3次元配列なのかによって処理が異なるので、if文で処理を切り替える必要があります。

・実装

 処理の確認ができたので、関数として実装します。

# one-hot表現への変換関数の実装
def convert_one_hot(corpus, vocab_size):
    
    # ターゲットの単語数を取得
    N = corpus.shape[0]
    
    # one-hot表現に変換
    if corpus.ndim == 1: # 1次元配列のとき
        
        # 変換後の形状の2次元配列を作成
        one_hot = np.zeros((N, vocab_size), dtype=np.int32)
        
        # 単語ID番目の要素を1に置換
        for idx, word_id in enumerate(corpus):
            one_hot[idx, word_id] = 1
    
    elif corpus.ndim == 2: # 2次元配列のとき
        
        # コンテキストサイズを取得
        C = corpus.shape[1]
        
        # 変換後の形状の3次元配列を作成
        one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
        
        # 単語ID番目の要素を1に置換
        for idx_0, word_ids in enumerate(corpus): # 0次元方向
            for idx_1, word_id in enumerate(word_ids): # 1次元方向
                one_hot[idx_0, idx_1, word_id] = 1
    
    return one_hot


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

 まずはターゲットをone-hot表現に変換します。

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

# one-hot表現に変換
target_one_hot = convert_one_hot(target, vocab_size)
print(target)
print(target.shape)
print(target_one_hot)
print(target_one_hot.shape)
[1 2 3 4 1 5]
(6,)
[[0 1 0 0 0 0 0]
 [0 0 1 0 0 0 0]
 [0 0 0 1 0 0 0]
 [0 0 0 0 1 0 0]
 [0 1 0 0 0 0 0]
 [0 0 0 0 0 1 0]]
(6, 7)

 targetの各要素とtarget_one_hotの各行が対応します。またtargetの各要素の値とtarget_one_hotの列番号が対応します。

 続いてコンテキストにも行います。

# one-hot表現に変換
contexts_one_hot = convert_one_hot(contexts, vocab_size)
print(contexts)
print(contexts.shape)
print(contexts_one_hot)
print(contexts_one_hot.shape)
[[0 2]
 [1 3]
 [2 4]
 [3 1]
 [4 5]
 [1 6]]
(6, 2)
[[[1 0 0 0 0 0 0]
  [0 0 1 0 0 0 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 1 0 0 0]]

 [[0 0 1 0 0 0 0]
  [0 0 0 0 1 0 0]]

 [[0 0 0 1 0 0 0]
  [0 1 0 0 0 0 0]]

 [[0 0 0 0 1 0 0]
  [0 0 0 0 0 1 0]]

 [[0 1 0 0 0 0 0]
  [0 0 0 0 0 0 1]]]
(6, 2, 7)

 contextsの行(0次元)とcontexts_one_hotの0次元、列(1次元)と1次元が対応します。またcontextsの各要素の値とcontexts_one_hotの2次元方向の要素番号が対応します。

 0番目のターゲット「say」に関するデータを取り出して確認してみます。コンテキストは「you(単語ID:0)」と「goodbye(単語ID:2)」です。

print(contexts[0])
print(contexts[0].shape)
print(contexts_one_hot[0])
print(contexts_one_hot[0].shape)
[0 2]
(2,)
[[1 0 0 0 0 0 0]
 [0 0 1 0 0 0 0]]
(2, 7)

 0番目のターゲットのコンテキストcontexts[0]の0番目の要素の値が0(youの単語ID)なので、contexts_one_hot[0]の0行目の0番目の要素が1となります。同様に、contexts[0]の1番目の要素の値が2(goodbyeの単語ID)なので、contexts_one_hot[0]の1行目の2番目の要素が1となります。

 この節では、テキストからone-hot表現のコンテキストとターゲットを作成しました。これはCBOWモデルの入力データになります。次節では、CBOWモデルを実装し、学習を行います。

参考文献

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

おわりに

 前処理は前処理で面白い。しかしこの節の処理を言葉で説明するの結構ムズかった。図解も好きなんだけど、イラレかぁイラレに投資する諸々の余裕がなぁ。。。

【次節の内容】

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