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

# はじめに
ふと思い立って勉強を始めた[「ゼロから作るDeep Learning❷ーー自然言語処理編」](https://www.oreilly.co.jp/books/9784873118369/)の4章で私がつまずいたことのメモです。

実行環境はmacOS Catalina + Anaconda 2019.10、Pythonのバージョンは3.7.4です。詳細は[このメモの1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25)をご参照ください。

（このメモの他の章へ：[1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25) / [2章](https://qiita.com/segavvy/items/52feabbf7867020e117d) / [3章](https://qiita.com/segavvy/items/1510d8c264edd291e0ef) / 4章 / [5章](https://qiita.com/segavvy/items/3e6a9f6bff103824598f) / [6章](https://qiita.com/segavvy/items/fba1595cf64a2462b415) / [7章](https://qiita.com/segavvy/items/883671d07cd7ab26fbe0) / [8章](https://qiita.com/segavvy/items/69ca97ff2d4e8a7144fd) / [まとめ](https://qiita.com/segavvy/items/0f2980ad746d797dd8c1)）

:::note info
この記事は個人で作成したものであり、内容や意見は所属企業・部門見解を代表するものではありません。
:::

# 4章 word2vecの高速化

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

## 4.1 word2vecの改良①

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

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

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

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

```math
\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 $ で表現しています。

```math
\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行目の $ ? $ は、次のようになります。

```math
\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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139624/32ab63c2-9fde-16fc-6515-658c28895590.png)

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

![図2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139624/d46e1fee-5aac-c0e3-7c67-d2095398f0f7.png)

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

![図3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139624/69564296-89e5-3700-ae26-4bfad5a62733.png)

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

## 4.3 改良版word2vecの学習

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

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

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

```python: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139624/7caad06f-2fe5-07bc-d81d-66b62bba4bb2.png)
続いて結果の確認です。標準入力からいろいろな言葉を試せるように、`ch04/eval.py`を少し変えました。`★`が変更箇所です。

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

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

続いて類推問題です。

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

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

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

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


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

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

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

## 4.5 まとめ

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

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

（このメモの他の章へ：[1章](https://qiita.com/segavvy/items/91be1d4fc66f7e322f25) / [2章](https://qiita.com/segavvy/items/52feabbf7867020e117d) / [3章](https://qiita.com/segavvy/items/1510d8c264edd291e0ef) / 4章 / [5章](https://qiita.com/segavvy/items/3e6a9f6bff103824598f) / [6章](https://qiita.com/segavvy/items/fba1595cf64a2462b415) / [7章](https://qiita.com/segavvy/items/883671d07cd7ab26fbe0) / [8章](https://qiita.com/segavvy/items/69ca97ff2d4e8a7144fd) / [まとめ](https://qiita.com/segavvy/items/0f2980ad746d797dd8c1)）