E検定で出てくるリカレントニューラルネットワークと強化学習について

# 初めに
本記事では深層学習分野で用いられる以下の事柄について説明します。

* リカレントニューラルネットワーク(以下、RNNと略)の概要と関連技術
* 強化学習の概要と関連技術

# RNN概要
RNNとは時系列のデータ(音声や株価の推移、等)に対応できるNNの一種となります。

NNで時系列を取り扱おうとした際、NNに以前入力された値は参照できないため、入力層に時系列データをすべて入力させることで実現することが考えられます。
しかし、その場合、入力層のデータ数が固定にする必要があるため、任意の長さの時系列データを扱うことが出来ません。

RNNではそういうった問題を解決するべく、基本的なコンセプトとして、前回入力した値を参照して予測を行うことをします。
具体的なイメージとして以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/d82ac2c5-7f84-f31a-bd6c-2f67918ab586.png)

図の通り、前回の層の出力を入力層の入力と共に中間層に渡すNNとなっております。

RNNの学習ですが、誤差逆伝搬法の一つであるBPTT(BackPropagation Through Time)と呼ばれる方法を使用します。
この方法は時系列データの最後のデータから順次誤差逆伝搬法を適用し、時系列データの最初のデータまで重み、バイアスの更新を行います。
注意点としては、この時系列データはすべてない(つまり、時系列データの一部だけを切り取ることは出来ない)と学習ができない点があげられます。

# RNNの実装例
RNNの実装例としてバイナリ加算器(2進数の足し算を行う計算機)を作成する例を示します。
具体的には2進数の数字$a$,$b$を入力としてその足し算の結果$a+b$を予測するタスクとなります。
やっていることの大枠はコードのコメントにも記載している通り以下の流れです。

① データを用意
② モデルのハイパーパラメータ設定
③ 学習開始(10000エポック実施)
③-1 RNNの2進数の入力値a,bとラベルdをランダムで生成
③-2 モデルが出力する結果とラベル値を求めて、誤差を計算する
③-3 RNNの重みを更新するために勾配計算
③-4 勾配適用
③-5 学習進捗をグラフ表示(100エポック経過ごとに)

●実装

```python
import numpy as np
from common import functions
import matplotlib.pyplot as plt

# ①データを用意
# 2進数の桁数
binary_dim = 8
# 最大値 + 1
largest_number = pow(2, binary_dim)
# largest_numberまで2進数を用意
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)

# ②モデルのハイパーパラメータ設定
input_layer_size = 2
hidden_layer_size = 16
output_layer_size = 1

weight_init_std = 1
learning_rate = 0.1

iters_num = 10000
plot_interval = 100

# ウェイト初期化 (バイアスは簡単のため省略)
W_in = weight_init_std * np.random.randn(input_layer_size, hidden_layer_size)
W_out = weight_init_std * np.random.randn(hidden_layer_size, output_layer_size)
W = weight_init_std * np.random.randn(hidden_layer_size, hidden_layer_size)

# 勾配
W_in_grad = np.zeros_like(W_in)
W_out_grad = np.zeros_like(W_out)
W_grad = np.zeros_like(W)

u = np.zeros((hidden_layer_size, binary_dim + 1))
z = np.zeros((hidden_layer_size, binary_dim + 1))
y = np.zeros((output_layer_size, binary_dim))

delta_out = np.zeros((output_layer_size, binary_dim))
delta = np.zeros((hidden_layer_size, binary_dim + 1))

all_losses = []

# 学習開始(10000エポック実施)
for i in range(iters_num):
    
    # ③-1RNNの2進数の入力値a,bとラベルdをランダムで生成
    # 入力値
    a_int = np.random.randint(largest_number/2)
    a_bin = binary[a_int] # binary encoding
    b_int = np.random.randint(largest_number/2)
    b_bin = binary[b_int] # binary encoding
    
    # 正解データ
    d_int = a_int + b_int
    d_bin = binary[d_int]
    
    # 出力バイナリ
    out_bin = np.zeros_like(d_bin)
    
    # 時系列全体の誤差
    all_loss = 0    
    
    # ③-2モデルが出力する結果とラベル値を求めて、誤差を計算する
    # 時系列ループ
    for t in range(binary_dim):
        # 入力値
        X = np.array([a_bin[ - t - 1], b_bin[ - t - 1]]).reshape(1, -1)
        # 時刻tにおける正解データ
        dd = np.array([d_bin[binary_dim - t - 1]])
        
        u[:,t+1] = np.dot(X, W_in) + np.dot(z[:,t].reshape(1, -1), W)
        z[:,t+1] = functions.sigmoid(u[:,t+1])
        y[:,t] = functions.sigmoid(np.dot(z[:,t+1].reshape(1, -1), W_out))


        #誤差
        loss = functions.mean_squared_error(dd, y[:,t])
        
        delta_out[:,t] = functions.d_mean_squared_error(dd, y[:,t]) * functions.d_sigmoid(y[:,t])        
        
        all_loss += loss

        out_bin[binary_dim - t - 1] = np.round(y[:,t])
    
    
    # ③-3RNNの重みを更新するために勾配計算
    for t in range(binary_dim)[::-1]:
        X = np.array([a_bin[-t-1],b_bin[-t-1]]).reshape(1, -1)        

        delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * functions.d_sigmoid(u[:,t+1])

        # 勾配更新
        W_out_grad += np.dot(z[:,t+1].reshape(-1,1), delta_out[:,t].reshape(-1,1))
        W_grad += np.dot(z[:,t].reshape(-1,1), delta[:,t].reshape(1,-1))
        W_in_grad += np.dot(X.T, delta[:,t].reshape(1,-1))
    
    # ③-4勾配適用
    W_in -= learning_rate * W_in_grad
    W_out -= learning_rate * W_out_grad
    W -= learning_rate * W_grad
    
    W_in_grad *= 0
    W_out_grad *= 0
    W_grad *= 0
    

    # ③-5 学習進捗をグラフ表示(100エポック経過ごとに)
    if(i % plot_interval == 0):
        all_losses.append(all_loss)        
        print("iters:" + str(i))
        print("Loss:" + str(all_loss))
        print("Pred:" + str(out_bin))
        print("True:" + str(d_bin))
        out_int = 0
        for index,x in enumerate(reversed(out_bin)):
            out_int += x * pow(2, index)
        print(str(a_int) + " + " + str(b_int) + " = " + str(out_int))
        print("------------")

lists = range(0, iters_num, plot_interval)
plt.plot(lists, all_losses, label="loss")
plt.show()
```

●実行例
4000エポック経過した後、急速に誤差がなくなり、予測精度が高いモデルが出来ました。

```
iters:0
Loss:1.6351348465021922
Pred:[1 1 1 1 1 1 1 1]
True:[1 0 0 1 0 0 1 1]
73 + 74 = 255
------------
iters:100
Loss:0.9955347216347874
Pred:[1 0 1 0 0 0 1 1]
True:[1 0 0 0 1 1 1 1]
90 + 53 = 163
------------
...
省略
...
------------
iters:9900
Loss:0.0008159301333967475
Pred:[1 1 0 0 0 0 1 1]
True:[1 1 0 0 0 0 1 1]
111 + 84 = 195
------------
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/c170caa2-ae34-6cd8-8ee5-c5ba8c3022ae.png)

## LSTM
RNNは時系列の長いデータを学習すると時系列の長さに比例して誤差の微分値が少なくなり、勾配消失問題が発生しがちになります。
その問題を解決するべく考案されたのがLSTMで、RNNの中間層を以下の3つのゲートと1つのゲートに置き換えたものとなります。

* CEC(Constant Error Carousel)・・・過去の入力値を記録するメモリの様な物で、これによって勾配消失問題を回避することが出来ます。
* 入力ゲート・・・現在の入力と一つ前の入力の値を制御するゲートです
* 出力ゲート・・・現在の出力と一つ前の出力の値を制御するゲートです
* 忘却ゲート・・・CECのメモリ内の入力値を削除するか/維持するかを判定するゲートです
* 覗き穴結合・・・入力・出力・忘却ゲートがそれぞれ説明した制御を行う際、CECの値を参照するために設ける入力です

### 実装例
今回もバイナリ加算器を使用した例でkerasで実装した例を示します。
kerasの場合、LSTMを手軽に扱えて、実行結果を見るとしっかり予測できていることがわかります。

●コード

```python
import numpy as np
from common import functions
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
from tensorflow.keras.layers import LSTM
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *

# データを用意
# 2進数の桁数
binary_dim = 8
# 最大値 + 1
largest_number = pow(2, binary_dim)
# バッチサイズ
batch_size = 1000
# エポック数
iters_num = 1000
# largest_numberまで2進数を用意
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)

input_layer_size = 8
hidden_layer_size = 16
output_layer_size = 1

# 訓練データの作成
def create_training_data():
  # 入力データ
  rand_select = randint(0, largest_number / 2,  (2, iters_num))
  a_bin = binary[rand_select[0]]
  b_bin = binary[rand_select[1]]
  X = np.array([np.concatenate([a_bin[i], b_bin[i]], 0) for i in range(iters_num)]).reshape(iters_num, 1, input_layer_size * 2)
  
  # 正解データ
  t = binary[rand_select[0] + rand_select[1]]
  
  return X, t

X, t = create_training_data()

t[100]

model = Sequential()
model.add(LSTM(hidden_layer_size,
               batch_input_shape=(None, binary_dim, input_layer_size * 2), 
               return_sequences=False))
model.add(Dense(input_layer_size))
model.add(Activation("linear"))
optimizer = Adam(lr=0.001)
model.compile(loss="mean_squared_error", optimizer=optimizer)

# 学習
model.fit(
    X, t,
    batch_size=batch_size,
    epochs=iters_num,
    validation_split=0.1
)

# 予測してみる
target_index = 0
t_val = t[target_index]
predicted =  np.array([  0 if x < 0.5 else 1 for x in model.predict(X[target_index].reshape(1,1,16))[0]])
print(f"入力値：{X[target_index]}")
print(f"期待結果：{t_val}")
print(f"予測結果：{predicted}")
```

●実行結果
実行結果の最後を見るとしっかり期待結果と予測結果と一致していることが確認できます。

```
Epoch 1/1000
WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='lstm_8_input'), name='lstm_8_input', description="created by layer 'lstm_8_input'"), but it was called on an input with incompatible shape (1000, 1, 16).
WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='lstm_8_input'), name='lstm_8_input', description="created by layer 'lstm_8_input'"), but it was called on an input with incompatible shape (1000, 1, 16).
1/9 [==>...........................] - ETA: 11s - loss: 0.4734WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='lstm_8_input'), name='lstm_8_input', description="created by layer 'lstm_8_input'"), but it was called on an input with incompatible shape (1000, 1, 16).
9/9 [==============================] - 2s 60ms/step - loss: 0.4613 - val_loss: 0.4433
...
省略
...
Epoch 1000/1000
9/9 [==============================] - 0s 7ms/step - loss: 0.0568 - val_loss: 0.0587
<tensorflow.python.keras.callbacks.History at 0x7fec0456b390>

入力値：[[0 0 0 0 0 0 0 1 0 0 1 0 0 1 0 1]]
期待結果：[0 0 1 0 0 1 1 0]
予測結果：[0 0 1 0 0 1 1 0]
```

## GRU
時系列の長いデータに対応できるLSTMですが、入出力ゲート、忘却ゲート、CECなど更新するパラメータが多く、学習に多くの時間がかかる問題がありました。
それを回避するべく、LSTMの入出力ゲート、忘却ゲート、CECの代わりにリセットゲート、更新ゲートの2つのみの構成で学習を行うため、学習時間がLSTMより大幅に改善されました。

### 実装例
LSTMと違いを比べるべく、今回もバイナリ加算器とkerasを使用して実装例を試します。
以下のコードはLSTMの層を定義するところをGRUの層に変更しております。

●コード

```python
import numpy as np
from common import functions
import matplotlib.pyplot as plt
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Activation
from tensorflow.keras.layers import GRU
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
import numpy as np
import matplotlib.pyplot as plt
from numpy.random import *

# データを用意
# 2進数の桁数
binary_dim = 8
# 最大値 + 1
largest_number = pow(2, binary_dim)
# バッチサイズ
batch_size = 1000
# エポック数
iters_num = 1000
# largest_numberまで2進数を用意
binary = np.unpackbits(np.array([range(largest_number)],dtype=np.uint8).T,axis=1)

input_layer_size = 8
hidden_layer_size = 16
output_layer_size = 1

# 訓練データの作成
def create_training_data():
  # 入力データ
  rand_select = randint(0, largest_number / 2,  (2, iters_num))
  a_bin = binary[rand_select[0]]
  b_bin = binary[rand_select[1]]
  X = np.array([np.concatenate([a_bin[i], b_bin[i]], 0) for i in range(iters_num)]).reshape(iters_num, 1, input_layer_size * 2)
  
  # 正解データ
  t = binary[rand_select[0] + rand_select[1]]
  
  return X, t

X, t = create_training_data()

t[100]

model = Sequential()
model.add(GRU(hidden_layer_size,
               batch_input_shape=(None, binary_dim, input_layer_size * 2), 
               return_sequences=False))
model.add(Dense(input_layer_size))
model.add(Activation("linear"))
optimizer = Adam(lr=0.001)
model.compile(loss="mean_squared_error", optimizer=optimizer)

# 学習
model.fit(
    X, t,
    batch_size=batch_size,
    epochs=iters_num,
    validation_split=0.1
)

# 予測してみる
target_index = 0
t_val = t[target_index]
predicted =  np.array([  0 if x < 0.5 else 1 for x in model.predict(X[target_index].reshape(1,1,16))[0]])
print(f"入力値：{X[target_index]}")
print(f"期待結果：{t_val}")
print(f"予測結果：{predicted}")
```

●実行結果
LSTMの場合、1000エポックで2分ほど時間がかかりましたが、GRUの場合、同条件で数十秒で学習が完了しました。
そのため、学習時間の大幅改善されていることを確認できましたが、LSTMの様にすべての履歴を覚えるCECがないため、以下の実行ログを見ると精度がLSTMより劣っていることがわかります。

```
Epoch 1/1000
WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='gru_input'), name='gru_input', description="created by layer 'gru_input'"), but it was called on an input with incompatible shape (None, 1, 16).
WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='gru_input'), name='gru_input', description="created by layer 'gru_input'"), but it was called on an input with incompatible shape (None, 1, 16).
1/1 [==============================] - ETA: 0s - loss: 0.5330WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='gru_input'), name='gru_input', description="created by layer 'gru_input'"), but it was called on an input with incompatible shape (None, 1, 16).
1/1 [==============================] - 3s 3s/step - loss: 0.5330 - val_loss: 0.5168
...
省略
...
Epoch 1000/1000
1/1 [==============================] - 0s 48ms/step - loss: 0.1985 - val_loss: 0.2118
<tensorflow.python.keras.callbacks.History at 0x7fec02743c50>

WARNING:tensorflow:Model was constructed with shape (None, 8, 16) for input KerasTensor(type_spec=TensorSpec(shape=(None, 8, 16), dtype=tf.float32, name='gru_input'), name='gru_input', description="created by layer 'gru_input'"), but it was called on an input with incompatible shape (None, 1, 16).
入力値：[[0 0 0 1 1 1 1 0 0 0 1 1 0 0 0 0]]
期待結果：[0 1 0 0 1 1 1 0]
予測結果：[0 0 0 1 0 0 0 0]
```

## 双方向RNN
これまで時系列的に現在と過去のデータから未来のデータを予測するRNNを見てきましたが、過去と未来のデータから現在のデータを予測するということもあります。
例えば、翻訳の際、翻訳結果を出力する際、これまでの時系列的に過去のデータだけでなく、未来のデータも参照することで精度が上がることが考えられます。
それを実現したRNNが双方向RNNと呼ばれるモデルで、出力する際、未来と過去の中間層を使用します。

### 例題で学ぶ双方RNN
例えば、以下のクイズを考えてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/dd251e92-5bcd-be9c-5e47-9a341a7c474f.png)

双方向RNNでは1つ前の中間層の重み、1つ後の中間層の重みのそれぞれを現在の中間層の重みに掛けて出力します。
そういった意味では(1)、(2)は前後の中間層の重みから新たな重みを作ることになってしまい、上記の想定された出力を得ることが出来ません。
そうなると(3)、(4)が解の候補となりますが、np.concatenateのaxisオプションの使用は以下の通りです。

●入力例
a = [1,2,3]
b = [4,5,6]

●axis=0の場合、
np.concatenate([a, b], axis=0) ⇒ [[1,2,3],[4,5,6]]

●axis=1の場合、
np.concatenate([a, b], axis=1) ⇒ [[1,4],[2,5],[3,6]]

今回、1つ前の中間層の重みと1つ後の中間層の重みにそれぞれ現在の中間層の重みを掛けて出力したいため、axis=1の場合が適切なため、答えは(4)となります。

## Seq2Seq
翻訳など入力となる時系列データから新たな時系列データを生成するということをやりたい場合、そのやり方の一つとして、入力となる時系列データをRNNを使用して特徴量を出力し、その特徴量を入力として新たな時系列データを生成するRNNを用意するということがあげられます。
これがSeq2Seqと呼ばれるモデルで、以下の構成となります。例えば、翻訳の場合、Encoderから日本語の文章を入力し、Decoderが英語の翻訳文章を出力します。
特徴しては、入力と出力の個数は必ずしも一致しなくても動くことがあげられます。(例えば、「これは　犬　です」の3入力から「this is a dog」という4出力も対応できる)

* Encoder・・・入力となる時系列データをRNNを使用して特徴量を出力するRNN
* 文脈コンテキスト・・・Encoderから出力された特徴量
* Decoder・・・文脈コンテキストを入力として新たな時系列データを生成するRNN

また、Seq2Seqは構造上、1問1答であり、話の文脈を考慮して回答を行うことはできません。
これを解決するべく、HREDと呼ばれる方式が考案されました。
HREDはContext RNNと呼ばれるEncoderでこれまで入力された文章を一つの特徴量にまとめてDecoderに渡すことで文脈に沿った適切な出力を行います。
しかし、HREDでは短い回答を行いがちで、意味のある出力が行われにくい課題等があります。

そこでHREDの改良案としてVHREDが考案されました。
VHREDはVAEと呼ばれる入力データから潜在変数をオートエンコーダを使って得るモデルをHREDに追加したものとなります。

### 例題で学ぶSeq2Seq
例えば、以下のクイズを考えてみます。
●No.1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/b57e63b6-9015-b59e-84b4-48ee49539268.png)

(1)は双方向RNNの説明のため、誤りです。
(2)Seq2Seqの説明のため、正解です。
(3)回帰型ニューラルネットワークの説明のため、誤りです。
(4)LSTMの説明のため、誤りです。

●No.2
これまでの話のまとめに近いクイズとなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/0ff1e97e-dac2-6cfb-e944-7a5fba0075db.png)

・seq2seq2とHREDの違い
seq2seqとHREDの共通点は文字列の入力から文字列を出力する点です。
しかし、seq2seqの場合、これまで話した文脈は考慮されず、今入力された文字列のみを見て文字列の出力を行う1問1答スタイルのモデルです。
しかし、HREDはこれまで入力された文章も考慮して文字列を出力するモデルとなります。

・HREDとVHREDの違い
HREDとVHREDの共通点は文字列の入力から同じ文脈に沿った文字列を出力する点です。
HREDの場合、文脈を考慮されますが、短い回答を行いがちで意味のある出力が行われにくい特徴があります。
VHREDの場合は、HREDにVAEと呼ばれる入力データから潜在変数をオートエンコーダを使って得るモデルを追加することで、HREDの上記課題を対応したものとなります。

## Word2Vec
翻訳などの単語をRNNに学習させる際、そのままRNNに入力できないため、数値化する必要があります。
その方法として、入力される単語ごとに対応したOne-Hotベクトルを変換して入力するというものがあります。
例えば、[dog,cat,tiger]の単語グループをOne-Hotベクトルに変換する場合、dogが入力されれば、$(1,0,0)$のベクトルにcatが入力されれば$(0,1,0)$のベクトルに変換するという感じです。
しかし、この場合、語彙が少ない場合は変換できますが、語彙が多い場合は巨大なベクトルに変換されメモリや計算量が大変になる問題があります。
そこで考案された物がWord2VecというNNで、単語から数百次元程度のベクトルに変換することが出来ます。

### Word2Vecの参考記事
昨今はWord2Vecの学習は非常に手軽に行えます。以下のURLではgensimというWord2Vecを使用した例ですが、以下の手順でWord2Vecの学習が行えます。

①学習データ用意(日本語等の文章を形態素解析した文字列の配列)
②gensimに学習データをインプットして学習を始める

例えば、以下のコードとなります。

```python
# モデル構築
model = word2vec.Word2Vec(word_list, size=100,min_count=5,window=5,iter=100)

# 単語1に関連する順で単語を表示する
ret = model.wv.most_similar(positive=['単語1']) 
for item in ret:
    print(item[0], item[1])
```

15分でできる日本語Word2Vec(https://qiita.com/makaishi2/items/63b7986f6da93dc55edd)
【Python】Word2Vecの使い方(https://qiita.com/kenta1984/items/93b64768494f971edf86)

# Attention Mechanism
Seq2Seqは長い文章に弱い特徴があります。
その課題に対応したものがAttention Mechanismで入力単語と出力単語との関連性も学習することで精度の高い出力を行うことが出来ます。

## 例題で学ぶAttention Mechanism
まとめの趣旨が強いですが、以下の例題を考えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/618469/730fc84b-f8fa-0e0a-63d8-4d4d0a2b7d0d.png)

●RNNとWord2Vecの違い
RNN・・・時系列データからデータの特徴を取得するのに適したモデル
Word2Vec・・・単語から数百次元の特徴ベクトルに変換するモデル

●seq2seqとAttention Mechanismの違い
両方とも文字列を入力に文字列を出力するモデルですが、以下の点で異なります。
seq2seq・・・長い文字列の入力には適切な出力がされにくい
Attention Mechanism・・・長い文字列の入力にも適切な出力がされる

# 強化学習
## 概要
強化学習とは、教師あり・なし学習とは異なるAIの分野で以下の違いがあります。

* 教師あり・なし学習・・・入力データに対して適切な「出力値」を学習する
* 強化学習・・・環境において最も最適な「行動」を学習する

強化学習は以下の構成となっております。

* 環境・・・エージェントが行動することで、その行動の報酬をエージェントのフィードバックとして返す物で状態を持ちます。
* エージェント・・・環境に対して適切な行動を学習する主体で、エージェントが方策に基づいて行動を環境に対して起こし、その結果報酬を受け取り、次回の行動にフィードバックします(行動価値関数の更新)。

エージェント内には方策関数と価値関数の2つで構成されています。

* 価値関数・・・エージェントが行動すること得られる報酬を計算する関数です
* 方策関数・・・エージェントがどのような行動を起こすべきか確率を計算する関数です

学習の流れとしては、以下の通りです。

1. エージェントが方策関数に基づいて、最適と判断した行動を環境に行います
2. 環境はその行動を見て、報酬をエージェントに渡します
3. エージェントは報酬から行動価値関数の更新します


## AlphaGo
強化学習の応用例として、囲碁プログラムでプロの人間に勝利した初の囲碁プログラムとなります。
AlphaGoではPolicy NetとValue Netと呼ばれるCNNで行動と価値を決定し、RollOutPolicyと呼ばれる方策関数を使用します。
また、モンテカルロ木を使用して、何度もシミュレーションを行うことで、最善手の計算を行います。

## 軽量化・高速化技術
モデルを学習させるにあたって、モデルが複雑であればあるほど、多くの時間や計算リソースが必要となります。
そこで、考えられる対応策として以下が考えられますが、それぞれの対応方法についてみていきます。

* 計算リソースを分散処理することで並列に学習を行って処理時間を短縮⇒データ並列化、モデル並列化
* GPU(Graphic Processing Unit)を使用する
* 精度を維持しつつ、モデルのパラメータを削減することで学習の計算量を低減⇒量子化、蒸留、プルーニング

### データ・モデル並列化
モデルを複数の計算リソースを使用して、並列に学習を行う方式となります。
並列化の手法にも同期型と非同期型の2種類があり、それぞれの特徴は以下の通りで、非同期型が同期をとる時間がない分早いですが、学習が不安定になりやすい問題があります。

* 同期型・・・親モデルを複数の計算リソースに子モデルとしてコピーして、それぞれの計算リソースから算出された誤差の勾配をそれぞれの計算リソースと同期をとって親モデルに反映する方式
* 非同期型・・・親モデルを複数の計算リソースに子モデルとしてコピーして、それぞれの計算リソースから算出された誤差の勾配をそれぞれの計算リソースと同期をとらず親モデルに反映する方式

### GPUを使用する
GPUはCPUやGPGPU(Genral Purpose GPU)に比べ、安価で簡単な並列処理が得意な特徴があるため、簡単な行列演算がメインのNNでは最適な計算リソースとなります。
また、安価のため、手軽にGPUを増設することが出来、学習速度を簡単に上げることが出来ます。

### 量子化
量子化は重みやバイアスなどのパラメータの数値的な精度を落とすことでメモリと演算処理の削減を行うものです。
しかし、モデルの出力の精度が低下するデメリットがあります。

### 蒸留
親モデルからさらに軽量な子モデルを作成するための手法で、親モデルの入力・出力を子モデルが学習することで親モデルと同じ出力が出来る軽量な子モデルを作成します。

### プルーニング
NNが複雑になると重みやバイアス等のパラメータの中にモデルの出力にほとんど寄与していない物が出てくる可能性があります。
プルーニングは上記の寄与率が低い重み、バイアス等の削除を行いモデルの軽量化を行います。

## 応用モデル
### MobileNet
MobileNetはディープラーニングモデルを軽量化したCNNです。
精度が良いCNNは一般的にネットワークが複雑となり、計算量が多くなりますが、畳み込み層の計算をDepthwise ConvolutionとPointwise Convolutionを使用することで計算量の削減・軽量化を実現しています。

参考文献：
軽量モデルに新風を巻き起こした代表格！MobileNetV1 を詳細解説！(https://deepsquare.jp/2020/06/mobilenet-v1/)

### DenseNet
DenseNetは以下の構造をしたCNNの一種で、CNNの層の深さによる勾配消失問題を避けることが出来ます。

* 初期の畳み込み
* Denseブロック
* 変換レイヤー
* 判別レイヤー

代表的モデル「ResNet」、「DenseNet」を詳細解説！(https://deepsquare.jp/2020/04/resnet-densenet/#outline__4)

### Layer正規化/Instance正規化
Layer正規化はレイヤーノルムを使用し、畳み込み層の正規化を行うことで偏りの少ないデータに変換し、学習に悪影響を与えない工夫をした方法です。
Instance正規は、Batch Normalizationのバッチサイズが1の場合の正規化を行う方法です。

### Wavenet
WaveNetは名前の音声波形を生成するためのディープラーニングモデルで時系列データに対してDilated Convoluctionと呼ばれる畳み込みを適用することで実現しています。

## Transformer
TransformerとはHREDの様なEncode/Decodeモデルからさらに発展した物で、Encode/DecodeモデルにAttention Mechanismを組み合わせたモデルとなります。
なお、TransformerではRNNは使用せず、Attentionだけを使用しております。

## 物体検知・セグメンテーション
物体検知とは入力された画像に対して、特定の領域に物体を検出するタスクです。
具体的には以下のタスクがあり、下に行くほど、検知する対象の複雑さが増していきます。

* 分類・・・画像そのものに対して、この画像がなんであるかラベリングするタスク
* 物体検知・・・画像の中の物体を四角の領域(Bouding Box)として検出するタスク
* 意味領域分割・・・画像の中の物体をピクセル単位で検出するタスク
* 個体領域分割・・・画像の中の物体をピクセル単位でラベリング付きで検出するタスク

代表的な物体検知データセットとしてVOC12、ILSVRC17、MS COCO 18、OICOD18があり、VOC12、MS COCO 18、OICOD18は個体領域分割タスク用のデータセット、ILSVRC17は物体検知タスク用で使用されています。

物体検知における精度は通常のNNより複雑で以下の指標を組み合わせて評価を行います。

* モデルが予測した物体と正解のBound Boxの位置(IoUと呼ばれるBound Boxの重なり具合によって精度を評価)
* モデルが予測した物体と正解のラベル

物体検知モデルの応用として、SSD(Single Shot Multibox Detector)があります。
SSDは領域候補検出、クラス分類を1度に同時に行うことで物体を検知する速度を大幅に向上させました。

個体領域分割タスクにおいて、Semantic Segmentionと呼ばれるCNNの一種が使用されており、畳み込み＋プーリングで畳み込んだ後、全結合層にデータを渡し、全結合層に出力した値に基づいてアンプーリングを行い、物体の個体識別をピクセル単位で行います。