search
LoginSignup
7

More than 1 year has passed since last update.

posted at

updated at

Organization

ゼロから作るDeep Learning❷で素人がつまずいたことメモ:4章

はじめに

ふと思い立って勉強を始めた「ゼロから作るDeep Learning❷ーー自然言語処理編」の4章で私がつまずいたことのメモです。

実行環境はmacOS Catalina + Anaconda 2019.10、Pythonのバージョンは3.7.4です。詳細はこのメモの1章をご参照ください。

(このメモの他の章へ:1章 / 2章 / 3章 / 4章 / 5章 / 6章 / 7章 / 8章 / まとめ

4章 word2vecの高速化

この章は、3章で作ったword2vecのCBOWモデルの高速化です。

4.1 word2vecの改良①

まず入力層から中間層までの高速化です。この部分は単語を分散表現に変換する埋め込みの役割を担いますが、MatMulレイヤーだと無駄が多いのでEmbeddingレイヤーへ置き換えます。

Embeddingレイヤーはシンプルなのですが、逆伝播の実装でidxが重複している場合に $ dW $ を加算する部分が少し分かりにくいかも知れません。本では図4-5で取り上げられていて、「なぜ加算を行うかは、各自で考えてみましょう」と解説が省略されています。

そこで、MatMulレイヤーの時の逆伝播の計算と比較することで考えてみました。EmbeddingレイヤーはMatMulレイヤーと同じ結果にならないといけないためです。

まず、図4-5における $ idx $ をMatMulレイヤーにおける $ x $ に戻します。

\begin{align}
idx &= 
\begin{pmatrix}
0\\
2\\
0\\
4\\
\end{pmatrix}\\
\\
x &=
\begin{pmatrix}
1 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 1 & 0 & 0 & 0 & 0\\
1 & 0 & 0 & 0 & 0 & 0 & 0\\
0 & 0 & 0 & 0 & 1 & 0 & 0\\
\end{pmatrix}
\end{align}

MatMulレイヤーの逆伝播の式は $ \frac{\partial L}{\partial W} = x^T\frac{\partial L}{\partial y} $ (P.33参照)なので、図4-5の表記に置き換えると $ dw = x^Tdh $ になります。ここに $ x $ と図4-5の $ dh $ を当てはめて$ dW $を計算すると次のようになります。なお、本当は $dh$ を図4-5の通りにしたかったのですが本のように●の濃淡を表現できないので、ここでは $ ●、◆、a、b $ で表現しています。

\begin{align}
dW &= x^Tdh\\
\\
\begin{pmatrix}
? & ? & ? \\
○ & ○ & ○ \\
●_1 & ●_2 & ●_3 \\
○ & ○ & ○ \\
◆_1 & ◆_2 & ◆_3 \\
○ & ○ & ○ \\
○ & ○ & ○ \\
\end{pmatrix}
&=
\begin{pmatrix}
1 & 0 & 1 & 0\\
0 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 0 & 0\\
0 & 0 & 0 & 0\\
\end{pmatrix}
\begin{pmatrix}
a_1 & a_2 & a_3 \\
●_1 & ●_2 & ●_3 \\
b_1 & b_2 & b_3 \\
◆_1 & ◆_2 & ◆_3 \\
\end{pmatrix}\\
\end{align}

計算すると分かりますが、$ dh $ の2行目( $ ●_1 ●_2 ●_3 $)と4行目( $ ◆_1 ◆_2 ◆_3 $)は、図4-5のようにそのまま $ dW $ の3行目と5行目になります。そして、問題になっている $ dW $ の1行目の $ ? $ は、次のようになります。

\begin{align}
\begin{pmatrix}
a_1 + b_1 & a_2 + b_2 & a_3 + b_3 \\
○ & ○ & ○ \\
●_1 & ●_2 & ●_3 \\
○ & ○ & ○ \\
◆_1 & ◆_2 & ◆_3 \\
○ & ○ & ○ \\
○ & ○ & ○ \\
\end{pmatrix}
&=
\begin{pmatrix}
1 & 0 & 1 & 0\\
0 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 0 & 0\\
0 & 0 & 0 & 1\\
0 & 0 & 0 & 0\\
0 & 0 & 0 & 0\\
\end{pmatrix}
\begin{pmatrix}
a_1 & a_2 & a_3 \\
●_1 & ●_2 & ●_3 \\
b_1 & b_2 & b_3 \\
◆_1 & ◆_2 & ◆_3 \\
\end{pmatrix}
\end{align}

つまり、$ dh $ の1行目と3行目を加算していることが分かります。このMatMulレイヤーの計算と同じことをEmbeddingレイヤーでも実装しないといけないので、加算する必要があるという訳です。

4.2 word2vecの改良②

続いて中間層から出力層への改良です。負例を使った学習を大胆に削減してしまおうというNegative Samplingのアイデアは面白いですね。

大きくつまずく点はなかったのですが、本ではEmbedding Dotレイヤーの逆伝播の解説が「難しい問題ではないので、各自で考えてみましょう」ということで省略されているので、ここを少しまとめてみます。

図4-12の中で、Embedding Dotレイヤーの部分だけ切り出すと次のようになります。

図1.png

dotノードでやっていることは各要素ごとの乗算と、それらの結果の加算です。そのため、乗算ノード(1章の「1.3.4.1 乗算ノード」参照)とSumノード(1章の「1.3.4.4 Sumノード」参照)に分解して逆伝播を考えます。そうすると次の形になります。青字が逆伝播です。

図2.png

これを1つ前のDotノードの図に戻すと、次の形になります。

図3.png

この図の通りに実装すればOKですが、そのままでは dout の形状が htarget_W と合わないので、NumPyの * で要素ごとの積が求められません。そのため最初に dout.reshape(dout.shape[0], 1) で形状を合わせてから積を求めます。この流れで実装すると、本の EmbeddingDot.backwad() のコードになることがわかるかと思います。

4.3 改良版word2vecの学習

学習の実装は特につまずくところはありません。本ではPTBコーパスを使っていますが、やっぱり日本語が好きなので、2章同様に青空文庫の分かち書き済みテキストで学習してみました。

コーパスの取得はdataset/ptb.pyの代わりに改造版のdataset/aozorabunko.pyを使います。このソースや仕組みなどについては、2章のメモの「カウントベースの手法の改善」のところに書いていますので、そちらをご参照ください。

ch04/train.pyも、以下のように青空文庫のコーパスを使うように変更しています。コメントにのあるのが変更箇所です。

ch04/train.py
# coding: utf-8
import sys
sys.path.append('..')
from common import config
# GPUで実行する場合は、下記のコメントアウトを消去(要cupy)
# ===============================================
# config.GPU = True
# ===============================================
from common.np import *
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
from cbow import CBOW
from skip_gram import SkipGram
from common.util import create_contexts_target, to_cpu, to_gpu
from dataset import aozorabunko  # ★青空文庫のコーパスを利用するように変更

# ハイパーパラメータの設定
window_size = 5
hidden_size = 100
batch_size = 100
max_epoch = 10

# データの読み込み
corpus, word_to_id, id_to_word = aozorabunko.load_data('train')  # ★コーパス変更
vocab_size = len(word_to_id)

contexts, target = create_contexts_target(corpus, window_size)
if config.GPU:
    contexts, target = to_gpu(contexts), to_gpu(target)

# モデルなどの生成
model = CBOW(vocab_size, hidden_size, window_size, corpus)
# model = SkipGram(vocab_size, hidden_size, window_size, corpus)
optimizer = Adam()
trainer = Trainer(model, optimizer)

# 学習開始
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 後ほど利用できるように、必要なデータを保存
word_vecs = model.word_vecs
if config.GPU:
    word_vecs = to_cpu(word_vecs)
params = {}
params['word_vecs'] = word_vecs.astype(np.float16)
params['word_to_id'] = word_to_id
params['id_to_word'] = id_to_word
pkl_file = 'cbow_params.pkl'  # or 'skipgram_params.pkl'
with open(pkl_file, 'wb') as f:
    pickle.dump(params, f, -1)

なお、手元の環境では学習に8時間くらいかかりました。
result.png
続いて結果の確認です。標準入力からいろいろな言葉を試せるように、ch04/eval.pyを少し変えました。が変更箇所です。

ch04/eval.py
# coding: utf-8
import sys
sys.path.append('..')
from common.util import most_similar, analogy
import pickle


pkl_file = 'cbow_params.pkl'
# pkl_file = 'skipgram_params.pkl'

with open(pkl_file, 'rb') as f:
    params = pickle.load(f)
    word_vecs = params['word_vecs']
    word_to_id = params['word_to_id']
    id_to_word = params['id_to_word']

# most similar task ★クエリを標準入力する形に変更
while True:
    query = input('\n[similar] query? ')
    if not query:
        break
    most_similar(query, word_to_id, id_to_word, word_vecs, top=5)


# analogy task ★クエリを標準入力する形に変更
print('-'*50)
while True:
    query = input('\n[analogy] query? (3 words) ')
    if not query:
        break
    a, b, c = query.split()
    analogy(a, b, c,  word_to_id, id_to_word, word_vecs)

以下、いろいろ試してみた結果です。

まず類似単語の確認です。比較のために2章で試したカウントベースのものも記載ました。また、CBOWのウィンドウサイズが本のコードでは5でしたが、カウントベースの時と同じ2も試してみました。

類似単語 2章のカウントベース
(ウィンドウサイズ:2)
CBOW
(ウィンドウサイズ:5)
CBOW
(ウィンドウサイズ:2)
あなた 奥さん: 0.6728986501693726
妻: 0.6299399137496948
K: 0.6205178499221802
父: 0.5986840128898621
私: 0.5941839814186096
お前: 0.7080078125
奥さん: 0.6748046875
妻: 0.64990234375
お嬢さん: 0.63330078125
あたし: 0.62646484375
奥さん: 0.7373046875
お前: 0.7236328125
妻: 0.68505859375
本人: 0.677734375
先生: 0.666015625
反: 0.8162745237350464
百: 0.8051895499229431
分: 0.7906433939933777
八: 0.7857747077941895
円: 0.7682645320892334
円: 0.78515625
分: 0.7744140625
年間: 0.720703125
世紀: 0.70751953125
時半: 0.70361328125
坪: 0.71923828125
メートル: 0.70947265625
分: 0.7080078125
分の: 0.7060546875
秒: 0.69091796875
ドア: 0.6294019222259521
ドアー: 0.6016885638237
自動車: 0.5859153270721436
門: 0.5726617574691772
カーテン: 0.5608214139938354
上半身: 0.74658203125
蔵: 0.744140625
洋館: 0.7353515625
階段: 0.7216796875
ドア: 0.71484375
階段: 0.72216796875
自動車: 0.7216796875
洞窟: 0.716796875
地下: 0.7138671875
扉: 0.71142578125
トヨタ トヨタ is not found トヨタ is not found トヨタ is not found
晩: 0.7267987132072449
ごろ: 0.660172164440155
昼: 0.6085118055343628
夕方: 0.6021789908409119
翌: 0.6002975106239319
夕方: 0.65576171875
国元: 0.65576171875
最初: 0.65087890625
天長節: 0.6494140625
次: 0.64501953125
夕方: 0.68115234375
昼: 0.66796875
ゆうべ: 0.6640625
晩: 0.64453125
門内: 0.61376953125
学校 東京: 0.6504884958267212
高等: 0.6290650367736816
中学校: 0.5801640748977661
大学: 0.5742003917694092
下宿: 0.5358142852783203
大学: 0.81201171875
下宿: 0.732421875
住田: 0.7275390625
生徒: 0.68212890625
中学校: 0.6767578125
中学: 0.69677734375
大学: 0.68701171875
近頃: 0.6611328125
東京: 0.65869140625
ここ: 0.65771484375
座敷 書斎: 0.6603355407714844
椽側: 0.6362787485122681
室: 0.6142982244491577
部屋: 0.6024710536003113
台所: 0.6014574766159058
床: 0.77685546875
机: 0.76513671875
敷居: 0.76513671875
本堂: 0.744140625
玄関: 0.73681640625
机: 0.69970703125
床: 0.68603515625
椽: 0.6796875
書斎: 0.6748046875
雑司ヶ谷: 0.6708984375
着物 髯: 0.5216895937919617
黒: 0.5200990438461304
服: 0.5096032619476318
洋服: 0.48781922459602356
帽子: 0.4869200587272644
避け: 0.68896484375
冷汗: 0.6875
醒す: 0.67138671875
襯衣: 0.6708984375
とどのつまり: 0.662109375
装束: 0.68359375
見世: 0.68212890625
綿: 0.6787109375
奏する: 0.66259765625
硯: 0.65966796875
吾輩 主人: 0.6372452974319458
余: 0.5826579332351685
金田: 0.4684762954711914
彼等: 0.4676626920700073
迷亭: 0.4615904688835144
主人: 0.7861328125
彼等: 0.7490234375
余: 0.71923828125
猫: 0.71728515625
やむを得ない: 0.69287109375
主人: 0.80517578125
彼等: 0.6982421875
猫: 0.6962890625
細君: 0.6923828125
レッシング: 0.6611328125
犯人 怪人: 0.6609077453613281
賊: 0.6374931931495667
団員: 0.6308270692825317
あいつ: 0.6046633720397949
潜航: 0.5931873917579651
こんど: 0.7841796875
首領: 0.75439453125
あいつ: 0.74462890625
宝石: 0.74169921875
わし: 0.73779296875
魚つり: 0.77392578125
あいつ: 0.74072265625
近日: 0.7392578125
軽気球: 0.7021484375
難症: 0.70166015625
注文 話: 0.6200630068778992
相談: 0.5290789604187012
多忙: 0.5178924202919006
親切: 0.5033778548240662
講釈: 0.4894390106201172
催促: 0.6279296875
鑑定: 0.61279296875
卒業: 0.611328125
総会: 0.6103515625
贅沢: 0.607421875
相談: 0.65087890625
忠告: 0.63330078125
鑑定: 0.62451171875
辞儀: 0.61474609375
発議: 0.61474609375
無鉄砲 陳腐: 0.7266454696655273
古風: 0.6771457195281982
鋸: 0.6735808849334717
鼻息: 0.6516652703285217
無知: 0.650424063205719
信条: 0.7353515625
上分別: 0.7294921875
主役: 0.693359375
産まれ: 0.68603515625
受売: 0.68603515625
立場: 0.724609375
手近: 0.71630859375
路次: 0.71142578125
貌: 0.70458984375
演題: 0.69921875
南無: 0.6659030318260193
信女: 0.5759447813034058
墨: 0.5374482870101929
身分: 0.5352671146392822
普通: 0.5205280780792236
智識: 0.728515625
吾輩: 0.71728515625
画: 0.70751953125
胃弱: 0.67431640625
食意地: 0.66796875
吾輩: 0.6962890625
中学: 0.6513671875
恋: 0.64306640625
彼等: 0.63818359375
豚: 0.6357421875
書物: 0.5834404230117798
茶: 0.469807893037796
休ん: 0.4605821967124939
食う: 0.44864168763160706
棒: 0.4349029064178467
飲ん: 0.6728515625
喧嘩: 0.6689453125
飯: 0.66259765625
山越: 0.646484375
蕎麦: 0.64599609375
ヴァイオリン: 0.63232421875
月給: 0.630859375
薬: 0.59521484375
手榴弾: 0.59521484375
綺羅: 0.5947265625
料理 かせぎ: 0.5380040407180786
落款: 0.5214874744415283
原: 0.5175281763076782
法: 0.5082278847694397
屋: 0.5001937747001648
館: 0.68896484375
史: 0.615234375
小説: 0.59912109375
文芸: 0.5947265625
採る: 0.59033203125
雑誌: 0.666015625
小間物: 0.65625
鍛冶: 0.61376953125
音楽: 0.6123046875
呉服: 0.6083984375

2章の時と変わらず、なかなかの混乱ぶりです。とても優劣は付けられません。「我輩」で「猫」とか出てきてしまう辺りがコーパスの偏りを示していますね。夏目漱石、宮沢賢治、江戸川乱歩の3名の作品しか使っていないのと、コーパスのサイズが小さすぎることが原因だと思われます。

続いて類推問題です。

類推問題 CBOW(ウィンドウサイズ:5) CBOW(ウィンドウサイズ:2)
男:王 = 女:? ぬ: 5.25390625
ない: 4.2890625
ず: 4.21875
るる: 3.98828125
糞: 3.845703125
大鳥: 3.4375
一刻: 3.052734375
裏門: 2.9140625
かげ: 2.912109375
床柱: 2.873046875
体:顔 = 自動車:? 警官: 6.5
ドア: 5.83984375
ふたり: 5.5625
警部: 5.53515625
係長: 5.4765625
ドア: 3.85546875
穴: 3.646484375
電灯: 3.640625
警部: 3.638671875
肩: 3.6328125
行く:来る = 話す:? 言う: 4.6640625
十一: 4.546875
十三: 4.51171875
聞く: 4.25
尋ねる: 4.16796875
聞く: 4.3359375
惜しい: 4.14453125
宮: 4.11328125
いう: 3.671875
十一: 3.55078125
飯:食う = 書物:? 有し: 4.3671875
求める: 4.19140625
人望: 4.1328125
山路: 4.06640625
受ける: 3.857421875
促: 3.51171875
碌: 3.357421875
いう: 3.2265625
聞く: 3.2265625
ぬすみだす: 3.17578125
夏:暑い = 冬:? たまる: 5.23828125
てる: 4.171875
くる: 4.10546875
いたる: 4.05859375
いく: 3.978515625
十一: 4.29296875
済ん: 3.853515625
十三: 3.771484375
なる: 3.66015625
わるい: 3.66015625

いきなり最初の問題で、低いスコアながらも困った結果が混ざっています。学習データが不十分だと怖いことが起きますね。昨今の「説明可能なAI」が求められる背景を垣間見た感じがします。

他の結果もボロボロですが、かろうじて「体:顔 = 自動車:?」や「行く:来る = 話す:?」には正解も混ざっていました。「自動車」で「警官」や「警部」が出てくるのは江戸川乱歩の影響でしょう。

やっぱり素直にWikipediaの日本語版を使うべきだったかも知れませんが、天邪鬼なもので:sweat: なお、Wikipediaを試している方はたくさんいるので、興味のある方は「wikipedia 日本語 コーパス」などでググってみてください。

4.4 word2vecに関する残りのテーマ

転移学習の例としてメールのネガポジ判定が解説されていますが、この章までの知識では単語を固定長のベクトルに変換できても、メールのような文章を固定長のベクトルに変換することはできません。そのため、まだこのようなタスクには挑戦できません。

あと、分散表現の質に関して、日本語の場合は事前の分かち書きの質も大きく影響しそうです。日本語の分散表現モデルがいくつか公開されていますが、それらを転移学習することを考えた場合、同じ分かち書きの仕組み(ロジックや辞書の内容、パラメーターなど)を使うことが前提になるかと思います。そうなると、例えば業界や個社特有の専門用語などを扱うタスクでは、簡単には転移学習できないということになるのでしょうか。日本語はいろいろ大変な感じです。

4.5 まとめ

やっとこの本の前半が終わりましたが、1章は前巻のおさらいだったことを考えると、まだまだ1/3くらいかも知れません。先は長そうです……

この章は以上です。誤りなどありましたら、ご指摘いただけますとうれしいです。

(このメモの他の章へ:1章 / 2章 / 3章 / 4章 / 5章 / 6章 / 7章 / 8章 / まとめ

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7