はじめに
『ゼロから作るDeep Learning 2――自然言語処理編』の初学者向け【実装】攻略ノートです。『ゼロつく2』学習の補助となるように適宜解説を加えています。本と一緒に読んでください。
本の内容を1つずつ確認しながらゆっくりと組んでいきます。
この記事は、3.3節「学習データの準備」の内容です。CBOWモデルの入力データの前処理を説明して、Pythonで実装します。
【前節の内容】
【他の節の内容】
【この節の内容】
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年.
おわりに
前処理は前処理で面白い。しかしこの節の処理を言葉で説明するの結構ムズかった。図解も好きなんだけど、イラレかぁイラレに投資する諸々の余裕がなぁ。。。
【次節の内容】