深層学習／ゼロから作るDeep Learning２　第３章メモ

#1.はじめに
　名著、**「ゼロから作るDeep Learning２」**を読んでいます。今回は３章のメモ。
　コードの実行はGithubからコード全体をダウンロードし、ch03の中で jupyter notebook にて行っています。

#2.CBOWモデル
　シンプルな word2vec の CBOWモデル を動かしてみます。 ch03/train.py を実行します。

```python
import sys
sys.path.append('..')  # 親ディレクトリのファイルをインポートするための設定
from common.trainer import Trainer
from common.optimizer import Adam
from simple_cbow import SimpleCBOW
from common.util import preprocess, create_contexts_target, convert_one_hot

window_size = 1
hidden_size = 5
batch_size = 3
max_epoch = 1000

# コーパス、辞書の取得
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)

# コンテキスト、ターゲットの取得
contexts, target = create_contexts_target(corpus, window_size)

# ワンホット表現化
vocab_size = len(word_to_id)
contexts = convert_one_hot(contexts, vocab_size)
target = convert_one_hot(target, vocab_size)

# ネットワーク構築
model = SimpleCBOW(vocab_size, hidden_size)

# 学習とロス推移表示
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()

# 単語のベクトル表示
word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
    print(word, word_vecs[word_id])
```
![スクリーンショット 2020-05-11 18.33.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/c47a55e1-bb4e-7475-ccde-b504f5d5cfd9.png)

　たった7語のword2vecですが、ロスが順調に下がって各単語の5次元ベクトルが得られると嬉しいですね。それでは、コードを順番に見て行きます。

```python
# コーパス、辞書の取得
text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocess(text)
```
 `preprocess()`は、common/util.py にあるので、そこを参照します。

```python
# -------------- from common/util.py --------------- 
def preprocess(text):
    text = text.lower()  # 大文字を小文字に
    text = text.replace('.', ' .')  # ピリオドの前に空白を
    words = text.split(' ')  # 空白で分離して単語をリスト化

    word_to_id = {}
    id_to_word = {}

    for word in words:  # リストから1つづつ単語を word へ
        if word not in word_to_id:  # 単語が word_to_id になかったら
            new_id = len(word_to_id)  # word_to_id の登録数を id に設定
            word_to_id[word] = new_id  # word_to_id の登録  
            id_to_word[new_id] = word  # id_to_word の登録
        
    corpus = np.array([word_to_id[w] for w in words])  # corpus を id に変換
    return corpus, word_to_id, id_to_word
```
　text を単語に分解して corpus を得ます。辞書（単語→数字, 数字→単語)を作成し、その辞書を使って corpus を id に置き換えます。
　　　corpus = [0 1 2 3 4 1 5 6]
　　　word_to_id = {'you': 0, 'say': 1, 'goodbye': 2, 'and':3 , 'i': 4, 'hello': 5, '.': 6}
　　　id_to_word = {0 :'you', 1 :'say', 2 :'goodbye', 3 :'and', 4 :'i', 5 :'hello', 6 : '.'}

```python
# コンテキストとターゲットの取得
contexts, target = create_contexts_target(corpus, window_size)
```
　`create_contexts_target()`は、common/util.py にあるので、そこを参照します。

```python
# -------------- from common/util.py ---------------
def create_contexts_target(corpus, window_size=1):

    # target は corpus の前後にwindow_sizeを引いたもの
    target = corpus[window_size:-window_size]
    contexts = []

    # target の前後t分を contexts とする       
    for idx in range(window_size, len(corpus)-window_size):  # idx = 1 〜 6
        cs = []
        for t in range(-window_size, window_size + 1):  # t = -1, 0, 1
           if t == 0:
                continue  # t = 0 のときは何もしない            
           cs.append(corpus[idx + t])  # cs = courpus[idx-1, idx+1]
        contexts.append(cs)
    return np.array(contexts), np.array(target)
```
　target は corpus の前後から window_size を引いたものです。そして、idx に target が corpus のどの位置かを入れ、t でその前後を指定することで、contexts を得ています。
　　　contexts = [[[0 2][1 3][2 4][3 1][4 5][1 6]]] 
　　　target =　[1 2 3 4 1 5]

```python
# ワンホット表現化
vocab_size = len(word_to_id)
contexts = convert_one_hot(contexts, vocab_size)
target = convert_one_hot(target, vocab_size)
```
　`convert_one_hot()`は、common/util.py にあるので、そこを参照します。

```python
# -------------- from common/util.py ---------------
def convert_one_hot(corpus, vocab_size):

    N = corpus.shape[0]

    if corpus.ndim == 1:  # 1次元の場合 (target の場合)
        one_hot = np.zeros((N, vocab_size), dtype=np.int32)  # ゼロ行列作成
        for idx, word_id in enumerate(corpus):  # targetからword_idへ順次代入
            one_hot[idx, word_id] = 1

    elif corpus.ndim == 2:  # 2次元の場合 (contexts の場合)
        C = corpus.shape[1]
        one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)  # ゼロ行列作成
        for idx_0, word_ids in enumerate(corpus):  # contextsからword_idsへ順次代入
            for idx_1, word_id in enumerate(word_ids):  # word_idsからword_idへ順次代入
                one_hot[idx_0, idx_1, word_id] = 1

    return one_hot
```
![スクリーンショット 2020-05-12 14.51.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/b254ddb6-56b2-2c4a-70f6-7c50d7b132a9.png)
　**target** の場合は、(N, vocab_size)でゼロ行列を作り、one_hot[idx, word_id]で指定箇所を1にしています。

![スクリーンショット 2020-05-12 15.05.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/d66a8971-ac19-12ab-9564-35275472a11d.png)

　**contexts**の場合は、2次元なので、(N, C, vocab_size)でゼロ行列を作り、one_hot[idx_0, idx_1, word_id]で指定箇所を1にしています。

```python
# ネットワーク構築
model = SimpleCBOW(vocab_size, hidden_size)
```
　ネットワーク構築の部分です。クラス `SimpleCBOW()` がある、simple_cbow.py を順次見て行きます。

```python
# -------------- from simple_cbow.py ---------------
class SimpleCBOW:
    def __init__(self, vocab_size, hidden_size):
        V, H = vocab_size, hidden_size

        # 重みの初期化
        W_in = 0.01 * np.random.randn(V, H).astype('f')
        W_out = 0.01 * np.random.randn(H, V).astype('f')

        # レイヤの生成
        self.in_layer0 = MatMul(W_in)
        self.in_layer1 = MatMul(W_in)
        self.out_layer = MatMul(W_out)
        self.loss_layer = SoftmaxWithLoss()

        # すべての重みと勾配をリストにまとめる
        layers = [self.in_layer0, self.in_layer1, self.out_layer]
        self.params, self.grads = [], []
        for layer in layers:
            self.params += layer.params
            self.grads += layer.grads

        # メンバ変数に単語の分散表現を設定
        self.word_vecs = W_in
```
![スクリーンショット 2020-05-12 16.06.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/b85ac73b-e736-605b-b8c6-7e630d2fef7a.png)
　window_size = 1なので、入力は２箇所。入力は語彙数と同じ7個のワンホットベクトル、隠れ層は5個、出力は語彙数と同じ7個。

　**分布仮説**「単語の意味は、周囲の単語によって形成される」を基に、2つの単語に挟まれた単語は何かという穴埋め問題を解けるように学習すると、$W_{in}$が単語の分散表現になっているわけです。

　最後に、word_vecs に　重みW_inを代入しています。これは学習後に、単語のベクトル表示用として使います。


```python
# -------------- from simple_cbow.py ---------------
    def forward(self, contexts, target):
        h0 = self.in_layer0.forward(contexts[:, 0])
        h1 = self.in_layer1.forward(contexts[:, 1])
        h = (h0 + h1) * 0.5
        score = self.out_layer.forward(h)
        loss = self.loss_layer.forward(score, target)
        return loss
```
![スクリーンショット 2020-05-12 17.04.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/488affb1-c353-9959-ee25-9c7c3e22749e.png)
　layer0とlayer1の重み$W_{in}$は共用です。layer0とlayer1の信号を加算した後2で割っています。

```python
# -------------- from simple_cbow.py ---------------
    def backward(self, dout=1):
        ds = self.loss_layer.backward(dout)
        da = self.out_layer.backward(ds)
        da *= 0.5
        self.in_layer1.backward(da)
        self.in_layer0.backward(da)
        return None
```
![スクリーンショット 2020-05-12 17.04.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/209705/7267ef45-fc0a-4c80-e503-8672fe812c46.png)
　誤差逆伝播です。これは、特に問題ないでしょう。


```python
# 学習とロス推移グラフ表示
optimizer = Adam()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()
```
　common/trainer.py の `class Trainer()` を、先程ネットワーク構築したモデル、オプチマイザーはAdamで、インスタンス化します。後は、fitで学習、plotでロス推移グラフ表示します。

```python
# 単語のベクトル表示
word_vecs = model.word_vecs  # 重みW_in(単語ベクトル)の取得
for word_id, word in id_to_word.items():  # id_to_word からインデックスと単語を取得
    print(word, word_vecs[word_id])  # 単語とベクトルを表示
```
　最後に、学習した単語ベクトル`model.word_vecs`を呼び出し、単語とベクトルを表示します。