はじめに
今回の記事は、前回に引き続き、私が受講しているE資格 JDLA認定プログラムの「ラビットチャレンジ」のレポート記事です。
今回のテーマは深層学習です。
講座ではDay1〜Day4まで分かれており、今回はDay3(前半)に取り組みます。
Day3は主に、再帰型ニューラルネットワーク(RNN)について扱います。
確認テスト、実装演習も併せて載せていきます。
再帰型ニューラルネットワーク(RNN)
再帰型ニューラルネットワーク(RNN)とは、時系列データを扱うことができるニューラルネットワークです。
時系列データとは、時間的順序を追って一定間隔ごとに観察され、しかも相互に統計的依存関係が認められるようなデータの系列のことです。音声データやテキストデータなどが挙げられます。
時間的つながりをうまく勉強させるため、中間層に当たるところにループをおいて、入力層からの情報を中間層で処理し、出力したものを再度入力して、前の時間の中間層からの出力を用います。
中間層はその1つ前の時間での中間層の出力を用いますが、実際には\(t-1\)の中間層が\(t-2\)の情報も得ています。
前\(t−1\)の隠れ層の値が次\(t\)の隠れ層の計算へと使われていきます。
前の中間層の出力をどれくらい使うのかという重みが出現します。
基本の考え方はこれまでと変わりませんが、前の層からの\(Wz^{t-1}\)が増えていることが下記の式からわかります。
$$
u^{t}=W_{(in)}x^{t}+Wz^{t-1}+b
$$
$$
z^{t}=f(W_{(in)}x^{t}+Wz^{t-1}+b)
$$
$$
v^{t}=W_{(out)}z^{t}+c
$$
$$
v^{t}=g(W_{(out)}z^{t}+c)
$$
Backpropagation Through Time(BPTT)
Backpropagation Through Time(BPTT)とは、RNNにおける誤差逆伝播法の1つで、時系列の考え方を取り入れたものです。
重み
$$
\frac{\partial E}{\partial W_{(in)}}=\frac{\partial E}{\partial u^{t}}
\left[
\frac{\partial u^{t}}{\partial W_{(in)}}
\right]^{T}=
\delta^{t}[x^{t}]^{T}
$$
$$
\frac{\partial E}{\partial W_{(out)}}=\frac{\partial E}{\partial v^{t}}
\left[
\frac{\partial v^{t}}{\partial W_{(out)}}
\right]^{T}=
\delta^{out, t}[z^{t}]^{T}
$$
$$
\frac{\partial E}{\partial W}=\frac{\partial E}{\partial u^{t}}
\left[
\frac{\partial u^{t}}{\partial W}
\right]^{T}=
\delta^{t}[z^{t-1}]^{T}
$$
バイアス
$$
\frac{\partial E}{\partial b}=\frac{\partial E}{\partial u^{t}}
\frac{\partial u^{t}}{\partial b}=\delta^{t}
$$
$$
\frac{\partial E}{\partial c}=\frac{\partial E}{\partial v^{t}}
\frac{\partial v^{t}}{\partial c}=\delta^{out, t}
$$
パラメータの更新
重み
$$
W_{(in)}^{t+1}=W_{(in)}^{t}-\epsilon\frac{\partial E}{\partial W_{(in)}}=W_{(in)}^{t}-\epsilon\sum_{z=0}^{T_{t}}{\delta^{t-z}[x^{t-z}]^{T}}
$$
$$
W_{(out)}^{t+1}=W_{(out)}^{t}-\epsilon\frac{\partial E}{\partial W_{(out)}}=W_{(out)}^{t}-\epsilon \delta^{out,t}[x^{t}]^{T}
$$
$$
W^{t+1}=W^{t}-\epsilon\frac{\partial E}{\partial W}=W^{t}-\epsilon\sum_{z=0}^{T_{t}}{\delta^{t-z}[x^{t-z-1}]^{T}}
$$
バイアス
$$
b^{t+1}=b^{t}-\epsilon\frac{\partial E}{\partial b}=b^{t}-\epsilon\sum_{z=0}^{T_{t}}{\delta^{t-z}}
$$
$$
c^{t+1}=c^{t}-\epsilon\frac{\partial E}{\partial c}=c^{t}-\epsilon \delta^{out,t}
$$
確認テスト
- サイズ5×5の入力画像をサイズ3×3のフィルタで畳み込んだ時の出力画像のサイズを答えよ。なお、ストライドは2、パディングは1とする。
⇨(画像の高さor幅+2*パディング-フィルタの高さor幅)/ストライド+1
\((5+2*1-3)/2+1=3\)
高さ、幅ともに同じため3×3となります。
- RNNのネットワークには大きく分けて3つの重みがある。
1つは入力から現在の中間層を定義する際に掛けられる重み、もう1つは中間層から出力を定義する際に掛けられる重みである。
残り1つの重みについて説明せよ。
⇨前の中間層から現在の中間層(または現在の中間層から次の中間層)を定義する際に掛けられる重みです。
- 連鎖律の原理を使い、dz/dxを求めよ。
$$
z=t^2
$$
$$
t=x+y
$$
⇨連鎖律によって以下の式になります。
$$
\frac{dz}{dx}=\frac{dz}{dt}\frac{dt}{dx}
$$
それぞれ解くと、
$$
\frac{dz}{dt}=2t
$$
$$
\frac{dt}{dx}=1
$$
となり、連鎖律で積を取ればいいので、
$$
2t・1=2(x+y)
$$
- 図の\(y_1\)を\(x_1,s_0,s_1,w_{in},w,w_{out}\)を用いて数式で表わせ。また、中間層の出力にシグモイド関数を作用させよ。
$$
z_1=sigmoid \left( s_0 W+x_1W_{in}+b \right)
$$
$$
y_1=sigmoid \left( z_1 W_{out}+c \right)
$$
RNN 実装演習
Xavier、Heの場合や、パラメータの更新の部分など変更する部分は # でコメントアウトしています。
import sys
sys.path.append('./DNN_code_colab_lesson_3_4')
import numpy as np
from common import functions
import matplotlib.pyplot as plt
def d_tanh(x):
return 1/(np.cosh(x) ** 2)
# データを用意
# 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)
# Xavier
# W_in = np.random.randn(input_layer_size, hidden_layer_size) / (np.sqrt(input_layer_size))
# W_out = np.random.randn(hidden_layer_size, output_layer_size) / (np.sqrt(hidden_layer_size))
# W = np.random.randn(hidden_layer_size, hidden_layer_size) / (np.sqrt(hidden_layer_size))
# He
# W_in = np.random.randn(input_layer_size, hidden_layer_size) / (np.sqrt(input_layer_size)) * np.sqrt(2)
# W_out = np.random.randn(hidden_layer_size, output_layer_size) / (np.sqrt(hidden_layer_size)) * np.sqrt(2)
# W = np.random.randn(hidden_layer_size, hidden_layer_size) / (np.sqrt(hidden_layer_size)) * np.sqrt(2)
# 勾配
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 = []
for i in range(iters_num):
# A, B初期化 (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
# 時系列ループ
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])
# z[:,t+1] = functions.relu(u[:,t+1])
# z[:,t+1] = np.tanh(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])
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])
# delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * functions.d_relu(u[:,t+1])
# delta[:,t] = (np.dot(delta[:,t+1].T, W.T) + np.dot(delta_out[:,t].T, W_out.T)) * d_tanh(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))
# 勾配適用
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
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()
出力は途中を省略しています。
iters:9900
Loss:0.007797138978910862
Pred:[0 1 1 1 1 0 1 1]
True:[0 1 1 1 1 0 1 1]
61 + 62 = 123
6000を超えたあたりから落ち着きを見せています。
LSTM(Long Short Term Memory)
RNNでは時系列を過去に遡るほど勾配が消失します。
活性化関数や重み等の初期化の手法を変えることも抑制する方法の1つですが、ニューラルネットワークの構造を変えて解決したものがLSTMです。
CEC
過去のデータを保存するための部分です。
CEC単独では以下のような問題があるため、ゲートや覗き穴結合で解決する。
- 時間依存度に関係なく重みが一律(重み衝突)になってしまう
- 過去の情報をすべて持っているため、その情報が必要なくなった後も影響を及ぼしてしまう
- CEC自身の値はゲートの制御に影響を与えないため、CECの過去の情報を任意のタイミングで伝搬・忘却が出来ない
入力ゲート
1つ前のユニットの入力をどの程度受け取るか(一律ではなく可変の重み)を調整します。
$$
i=\sigma(W_{i}h_{t-1}+U_{i}x_{t})
$$
出力ゲート
1つ前のユニットの入出力をどの程度受け取るか(一律ではなく可変の重み)を調整します。
$$
o=\sigma(W_{o}h_{t-1}+U_{o}x_{t})
$$
忘却ゲート
CECの中身をどの程度残すか(不必要な情報を忘却する)を調整します。
$$
f=\sigma(W_{f}h_{t-1}+U_{f}x_{t})
$$
メモリセル
$$
g=tanh(W_{g}h_{t-1}+U_{g}x_{t})
$$
$$
c_{t}=(c_{t-1}\otimes f)\oplus(g\otimes i)
$$
出力
$$
h_{t}=tanh(c_{t})\otimes o
$$
確認テスト
- シグモイド関数を微分した時、入力値が0の時に最大の値を取る。その値はいくつか。
⇨シグモイド関数の微分
$$
f'(x)=(1-f(x))・f(x)
$$
シグモイド関数の入力値\(x\)に0を代入すると
$$
sigmoid(x)=\frac{1}{1+e^{-x}}=\frac{1}{2}=0.5
$$
微分の式に当てはめると
$$
f'(x)=(1-0.5)・0.5=0.25
$$
- 以下の文章をLSTMに入力し、空欄に当てはまる単語を予測したいとする。
「映画おもしろかったね。ところで、とてもお腹が空いたから何か____。」
文中の「とても」という言葉は空欄の予測においてなくなっても影響を及ぼさないと考えられる。
このような場合、どのゲートが作用すると考えられるか。
⇨忘却ゲート
LSTM 実装演習
クリッピング
勾配のノルムが閾値を超えた場合、勾配のノルムを正規化することで勾配爆発を防ぐ手法です。
def gradient_clipping(grad, threshould):
norm = np.linalg.norm(grad)
rate = threshould / norm
if rate < 1:
return grad * rate
return grad
LSTM
def lstm(x, prev_h, prev_c, W, U, b):
# セルへの入力やゲートをまとめて計算し、分割
lstm_in = _activation(x.dot(W.T)) + prev_h.dot(U.T) + b)
a, i, f, o = np.hsplit(lstm_in, 4)
# 値を変換、セルへの入力:(-1, 1)、ゲート:(0, 1)
a = np.tanh(a)
input_gate = _sigmoid(i)
forget_gate = _sigmoid(f)
output_gate = _sigmoid(o)
# セルの状態を更新し、中間層の出力を計算
c = input_gate * a + forget_gate * c
h = output_gate * np.tanh(c)
return c, h
GRU(Gated Recurrent Unit)
LSTMはパラメータが多く、計算負荷が高くなる問題がありました。
GRUでは、構造を変えることでパラメータを削減し、タスクによっては精度も同様またはそれ以上を望めるようになりました。
いわばLSTMを簡易化したものということです。
リセットゲート
過去の情報を保持させるか忘却させるかを決定します。
LSTMで言うところの忘却ゲートに該当します。
$$
r=\sigma(W_{r}h_{t-1}+U_{r}x_{t})
$$
更新ゲート
過去の情報をどれだけ取り込むかを決定します。
$$
z=\sigma(W_{z}h_{t-1}+U_{z}x_{t})
$$
出力
$$
c=tanh(W_{c}(h_{t-1}\otimes r)+U_{c}x_{t})
$$
$$
h_{t}=((1-z)\otimes h_{t-1})\oplus(z\otimes c)
$$
確認テスト
- LSTMとGRUの違いを簡潔に述べよ。
⇨LSTMは、入力ゲート、出力ゲート、忘却ゲートの3つのゲートがあり、ゲートが多いためその分パラメータ数も多い。
GRUは、リセットゲート、更新ゲートの2つで、簡略化されている分、パラメータの数が少ない。
GRU 実装演習
def gru(x, h, W_r, U_r, W_z, U_z, W, U):
# リセットゲート
r = _sigmoid(x.dot(W_r.T) + h.dot(U_r.T))
# 更新ゲート
z = _sigmoid(x.dot(W_z.T) + h.dot(U_z.T))
# 仮h
h_bar = np.tanh(x.dot(W.T) + (r * h).dot(U.T))
# h_{t}
h_new = (1 - z) * h + z * h_bar
return h_new
LSTMと同様に順伝播の個々の計算はシンプルです。
双方向RNN(Bindirectional RNN)
通常のRNNは過去の情報を保持することで時系列データの学習を実現していました。
双方向RNNの場合は、過去の情報に加えて未来の情報も加味させます。つまり、中間層の出力を、未来への順伝播と過去への逆伝播の両方向に伝播します。
また、学習時に過去と未来の情報の入力を必要とすることから、運用時も過去から未来までのすべての情報を入力してはじめて予測できるようになります。
双方向RNN 実装演習
def bindirectional_rnn_net(xs, W_f, U_f, W_b, U_b, V):
# W_f, U_f:入力から中間層、前の中間層から今の中間層の順方向の重み
# W_b, U_b:W_f, U_fの逆方向
# V:順方向、逆方向の中間層から出力層の重み
xs_f = np.zeros_like(xs)
xs_b = np.zeros_like(xs)
for i, x in enumerate(xs):
xs_f[i] = x
xs_b[i] = x[::-1]
hs_f = _rnn(xs_f, W_f, U_f)
hs_b = _rnn(xs_b, W_b, U_b)
hs = [np.concatenate([h_f, h_b[::-1]], axis=0)] for h_f, h_b in zip(hs_f, hs_b)
ys = hs.dot(V.T)
return ys