#はじめに
本記事は、日本ディープラーニング協会のDeep Learning資格試験(E資格)の受験に必要な、協会認定の講座プログラムのひとつであるラビットチャレンジのレポート記事(深層学習編(前半))となります。
#1.入力層〜中間層
1-1. ニューラルネットワークの全体像
-
深層学習でやろうとしていることは、明示的なプログラムの代わりに多数の中間層を持つニューラルネットワークを用いて、入力値から目的とする出力値に変換する数学モデルを構築することである
-
ニューラルネットワークは下図のように表せ、一番左の層を入力層、中間の層を中間層、一番右の層を出力層と呼ぶ
- 入力層→中間層、中間層→出力層への伝達時は、重み(W)とバイアス(b)がかかり、最終的には最適な重みとバイアスを発見することが学習の目的となる
1-2. 入力層〜中間層
- 入力層とは、ニューラルネットワークに何かしらの数値情報を入力するための層である
- 中間層とは、入力層が受け取った数値情報に重みとバイアスを加えた情報を受け取り、活性化関数を通して情報を出力層に伝達する層である
- 入力層で受け取った情報xは、重みWとバイアスbによって以下の様に編集される
W = \begin{bmatrix}w_1 \\ \vdots \\ w_l\end{bmatrix},
x = \begin{bmatrix}x_1 \\ \vdots \\ x_l\end{bmatrix} \\
\begin{align}
u &= w_1x_1 + w_2x_2 + \cdots + w_lx_l + b \\
&= \boldsymbol{W}\boldsymbol{x} + \boldsymbol{b}
\end{align}
1-3. 実装演習
# 順伝播(単層・複数ユニット)
# 重み
W = np.array([
[0.1, 0.2, 0.3],
[0.2, 0.3, 0.4],
[0.3, 0.4, 0.5],
[0.4, 0.5, 0.6]
])
print_vec("重み", W)
# バイアス
b = np.array([0.1, 0.2, 0.3])
print_vec("バイアス", b)
# 入力値
x = np.array([1.0, 5.0, 2.0, -1.0])
print_vec("入力", x)
# 総入力
u = np.dot(x, W) + b
print_vec("総入力", u)
# 中間層出力
z = functions.sigmoid(u)
print_vec("中間層出力", z)
# 実行結果
*** 重み ***
[[0.1 0.2 0.3]
[0.2 0.3 0.4]
[0.3 0.4 0.5]
[0.4 0.5 0.6]]
*** バイアス ***
[0.1 0.2 0.3]
*** 入力 ***
[ 1. 5. 2. -1.]
*** 総入力 ***
[1.4 2.2 3. ]
*** 中間層出力 ***
[0.80218389 0.90024951 0.95257413]
- 上記実装では、4つの入力xを4x3の重みWと3つのバイアスbを利用してミックスさせ、中間層に情報を渡している
ANS:u1 = np.dot(x, W1) + b1
#2.活性化関数
2-1. 活性化関数
- 活性化関数とは、ニューラルネットワークにおいて、次の層への出力の大きさを決める非線形の関数のことである
- 入力値の値によって、次の層への信号のON/OFFや強弱を定める働きをもつ
- 活性化関数は、非線形な関数であるが、線形な関数は加法性($f(x+y)=f(x)+f(y)$)と斉次性($f(kx)=kf(x)$)を満たすが、非線形な関数はこれらを満たさない
- 中間層用の活性化関数には、ReLU関数、シグモイド関数、ステップ関数がある
- 出力層用の活性化関数には、ソフトマックス関数、恒等写像、シグモイド関数がある
######ステップ関数
- 閾値を超えたら発火する関数であり、出力は常に1か0
- パーセプトロンで利用された関数
- 0-1間をうまく表現できず、線形分離可能なものしか学習ができないため、現在ではあまり使われない
f(x) = \left\{
\begin{array}{ll}
1 & (x \geqq 0) \\
0 & (x \lt 0)
\end{array}
\right.
#ステップ関数
def step_function(x):
if x > 0:
return 1
else:
return 0
######シグモイド関数
- 0〜1の間を緩やかに変化する関数で、ステップ関数ではON/OFFしかない状態に対し、信号の強弱を伝えられるようになり、予想ニューラルネットワーク普及のきっかけとなった
- 大きな値では出力の変化が微小なため、勾配消失問題を引き起こすことがある
f(u) = \frac{1}{1 + e^{-u}}
#シグモイド関数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
######ReLU関数
- 現在最もよく使われている活性化関数で、勾配消失問題の回避とスパース化に貢献することでよい成果をもたらしている
f(x) = \left\{
\begin{array}{ll}
x & (x > 0) \\
0 & (x \leqq 0)
\end{array}
\right.
#ReLU関数
def relu(x):
return np.maximum(0, x)
ANS:z1 = functions.sigmoid(u)
#3.出力層
3-1. 出力層
- 出力層の役割は、各クラスに分類される確率(私たちが欲しい情報)を出すことである
- 出力層と中間層の違いとして、中間層が閾値の前後で信号の強弱を調整する役割を担うのに対して、出力層では、信号の大きさ(比率)はそのままに変換を行う点が挙げられる
- 出力層で用いられる活性化関数には、恒等写像、シグモイド関数、ソフトマックス関数がある
######ソフトマックス関数
- 多値分類を行う際に利用される活性化関数で、出力を合計すると1になる特徴を持つ
y_k = \frac{exp(a_k)}{\sum_{i=1}^nexp(a_i)} \\
n:出力層の数, k:出力ラベル, a:入力信号
#ソフトマックス関数
def softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) #オーバーフロー対策
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
3-2. 誤差関数
- 出力層より得られた学習結果と訓練データより得られる正解を、誤差関数を使って検証することで、ニューラルネットワークによりどの程度よい結果が得られたかを判断することができる
- 誤差関数には、二乗和誤差と交差エントロピー誤差が使われる
######二乗和誤差
E = \frac{1}{2}\sum_k(y_k - t_k)^2 \\
y:モデルから得た出力値,t:対応する正解データ
- 二乗和誤差は上記式で表現できる
- 引き算を行うだけでは、各ラベルでの誤差で正負両方の値が発生し、全体の誤差を正しく表すのに都合が悪いため、2乗してそれぞれのラベルでの誤差が正の値となるようにしている
- 実際にネットワークを学習する際に行う、誤差逆伝播の計算において、微分した際の計算式を簡単にするために、1/2している
#二乗和誤差
def sum_squared_error(y, t):
return 0.5 * np.sum((y - t) **2)
######交差エントロピー誤差
- 交差エントロピー誤差は以下の式で表せる
E = -\sum_kt_klogy_k \\
y_k:ニューラルネットワークの出力, t_k:正解ラベル(t_kはone-hot表現)
#交差エントロピー誤差
def cross_entropy_error(y, t):
delta = 1e - 7 #np.log(0)の場合にマイナス無限大となることを防ぐ
return -np.sum(t * np.log(y + delta))
ANS:
二乗する理由:引き算を行うだけでは、各ラベルでの誤差で正負両方の値が発生し、全体の誤差を正しくあらわすのに都合が悪い。2乗してそれぞれのラベルでの誤差を正の値となるようにする
1/2する理由:実際にネットワークを学習する時に行う誤差逆伝播の計算で、誤差関数の微分を用いるが、その際の計算式を簡単にするため。
#4.勾配降下法
4-1. 勾配降下法
- 深層学習の目的は、学習を通して誤差を最小にするネットワークを作成すること、すなわち、誤差を最小化するようなパラメータを発見することである
- パラメータを最適化するための手法のひとつが勾配降下法である
- 勾配降下法は以下の式で表せる
w^{(t+1)} = w^{(t)} - ε\frac{∂E}{∂w} \\
ε:学習率
- 学習率が大きすぎると最小値にいつまでも辿りつかず発散してしまう
- 学習率が小さすぎると収束するまでに時間がかかってしまう
- 学習率の決定、収束性向上のためのアルゴリズムには、Momentum,AdaGrad,Adadelta,Adamなどがある
- 勾配降下法は以下のように実装できる
#勾配計算
def numerical_gradient(f, x):
h = 1e-4
grad = np.zeros_like(x)
for idx in range(x.size):
tmp_val = x[idx]
#f(x+h)の計算
x[idx] = tmp_val + h
fxh1 = f(x)
#f(x-h)の計算
x[idx] = tmp_val - h
fxh2 = f(x)
grad[idx] = (fxh1 - fxh2) / (2*h)
x[idx] = tmp_val #値を元に戻す
return grad
#勾配降下法
def gradient_descent(f, init_x, lr=0.01, step_num=100):
x = init_x
x_history = []
for i in range(step_num):
x_history.append( x.copy() )
grad = numerical_gradient(f, x)
x -= lr * grad
return x, np.array(x_history)
- 実装した関数を実行した結果は以下の様になり、勾配の更新を行うにつれて、最小値に近づいていることがわかる
#勾配降下法を使ってみる
def function_2(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)
#グラフ表示
plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')
plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()
4-2. 確率的勾配降下法
- ランダムに抽出したサンプルを用いて誤差を求める方法
- 勾配降下法と比較して、データが冗長な場合の計算コストの軽減、望まない局所極小解に収束するリスクの軽減、オンライン学習ができるなどのメリットがある
- オンライン学習とは、学習データが入ってくるたびに都度パラメータを更新し、学習を進めていく方法である
4-3. ミニバッチ勾配降下法
- オンライン学習の特徴をうまくバッチ学習で使えるようにした手法
- ランダムに分割したデータの集合(ミニバッチ)$D_t$に属するサンプルの平均誤差
w^{(t+1)} = w^{(t)} - ε∇E_t \\
E_t = \frac{1}{N_t}\sum_{n\in{D_t}}E_n \\
N_t = |D_t|
- 確率的勾配降下法のメリットを損なわず、計算機の計算資源を有効利用できる、すなわち、CPUを利用したスレッド並列化やGPUを利用したSIMD並列化を実現できるのがミニバッチ勾配降下法のメリットである
ANS:
network[key] -= learning_rate* grad[key]
grad = backward(x, d, z1, y)
ANS:
学習データが入ってくるたびに都度パラメータを更新し、学習を進めていく手法。
一方バッチ学習では、一度にすべての学習データを使ってパラメータ更新を行う。
#5.誤差逆伝播法
5-1. 誤差逆伝播法とは
- 誤差逆伝播法とは、算出された誤差を出力層側から順に微分し、前の層へと伝播させる手法
- 最小限の計算で各パラメータにおける微分値を解析的に計算することができる
5-2. 誤差逆伝播法の実装
- 誤差逆伝播法は以下のようにして実装できる
- なお、誤算関数には平均二乗誤差、活性化関数にはシグモイド関数を用いている
# dE/dy
delta2 = functions.d_mean_squared_error(d, y)
# W2の勾配
grad['W2'] = np.dot(z1.T, delta2)
# dE/dy * dy/du
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)
delta1 = delta1[np.newaxis, :]
x = x[np.newaxis, :]
# W1の勾配(dE/dy * dy/du * du/dw)
grad['W1'] = np.dot(x.T, delta1)
ANS:
# 出力層でのデルタ
delta2 = functions.d_mean_squared_error(d, y)
# b2の勾配
grad['b2'] = np.sum(delta2, axis=0)
# W2の勾配
grad['W2'] = np.dot(z1.T, delta2)
# 中間層でのデルタ
#delta1 = np.dot(delta2, W2.T) * functions.d_relu(z1)
## 試してみよう
delta1 = np.dot(delta2, W2.T) * functions.d_sigmoid(z1)
ANS:
delta2 = functions.d_mean_squared_error(d, y)
grad['W2'] = np.dot(z1.T, delta2)
#6.勾配消失問題
6-1. 勾配消失問題とは
- 勾配消失問題とは、誤差逆伝播が下位層に進んでいくにつれて、勾配がどんどん緩やかになっていき、下位層のパラメータがほとんど変わらず最適値に収束しなくなることを指す
- 誤差逆伝播の特徴として、下位層にいくにつれて、微分の回数が多くなる
- 微分値が1より小さい値をとる場合、計算結果はほとんど0に近い状態となり、勾配消失が起こる
- シグモイド関数の微分値は、最大0.25であるため、中間層が多くなるにつれて勾配消失が起こりやすくなる
6-2. 勾配消失の対策
6-2-1. 活性化関数の選択
- 微分値の大きい活性化関数を用いることで、勾配消失を防ぐことができる
- 例えば、ReLU関数は、微分値が0or1をとるため、勾配消失問題の回避とスパース化に貢献することで良い成果をもたらしている
6-2-2. 重みの初期値設定
- 重みの初期値を工夫することで、勾配消失を防ぐことができる
- 重みの初期値設定の手法のひとつにXavierがある
- Xavierは以下のようにして実装できる
network['W1'] = np.random.randn(input_layer_size, hidden_layer_size) / np.sqrt(input_layer_size)
network['W2'] = np.random.randn(hidden_layer_size, output_layer_size) / np.sqrt(hidden_layer_size)
- 上記のように、Xavierではノード数nに対して平均0、標準偏差$\frac{1}{\sqrt{n}}$である正規分布から重みを設定している
- Xavierは、活性化関数にシグモイド関数やtanh関数などのS字カーブ型の関数を使う際によく用いられる
- 活性化関数にReLU関数などのS字カーブ型ではない関数を使う際には、Heの初期値設定を利用する
- Heは以下のようにして実装できる
network['W1'] = np.random.randn(input_layer_size, hidden_layer_size) / np.sqrt(input_layer_size) * np.sqrt(2)
network['W2'] = np.random.randn(hidden_layer_size, output_layer_size) / np.sqrt(hidden_layer_size) * np.sqrt(2)
- 上記のように、Heではノード数nに対して、平均0、標準偏差$\sqrt{\frac{2}{n}}$である正規分布から重みを設定している
6-2-3. バッチ正規化
- バッチ正規化を用いることで、勾配消失を防ぐことができる
- バッチ正規化とは、ミニバッチ単位で入力値のデータの偏りを抑制する手法である
ANS:2(x+y)
ANS:0.25
ANS:
重みを0で設定すると正しい学習が行えなくなる
すべての重みの値が均一に更新されるため、多数の重みを持つ意味がなくなる
ANS:
より早く学習することができる点、パラメータの初期化が簡単な点、訓練データへの過剰適合を防ぐことができる点、などがある
#7.学習率最適化手法
7-1. 学習率と学習効率
- 学習率は、学習効率に大きく影響するパラメータである
- 学習率の値が大きい場合、最適値にいつまでも辿りつかず発散してしまう
- 学習率の値が小さい場合、発散することはないが、小さすぎると収束するまでに時間がかかってしまう。また、大域局所最適値に収束しづらくなる
7-2. 学習率の決め方
- 初期の学習率を大きく設定し、徐々に学習率を小さくしていく
- パラメータ毎に学習率を可変させる
- このような学習率最適化手法を利用して学習率を最適化する
7-3. 学習率最適化手法の具体例
7-3-1. モメンタム
- 誤差をパラメータで微分したものと学習率の積を減算した後、現在の重みに前回の重みを減算した値と慣性の積を加算する
- 局所的最適解にはならず、大域的最適解となる、谷間についてから最も低い位置にいくまでの時間が早いというのがモメンタムのメリット
self.v[key] = self.momentum * self.v[key] - self.learning_rate * grad[key]
params[keys] += self.v[key]
7-3-2. AdaGrad
- 誤差をパラメータで微分したものと再定義した学習率の積を減算する
- 勾配の緩やかな斜面に対して、最適値に近づくことができることがメリット
- 一方で、学習率が徐々に小さくなるため、鞍点問題を引き起こすこともある
self.h[key] = np.zeros_lile(val)
self.h[key] += grad[key] * grad[key]
params[key] -= self.learning_rate * grad[key]/(np.sqrt(self.h[key]) + 1e-7)
7-3-3. RMSProp
- 誤差をパラメータで微分したものと再定義した学習率の積を減算する
- 局所的最適解にはならず、大域的最適解となる
- ハイパーパラメータの調整が必要な場合が少ない
self.h[key] *= self.decay_rate
self.h[key] += (1-self.decay_rate) * grad[key] * grad[key]
params[key] -= self.learning_rate * grad[key]/(np.sqrt(self.h[key]) + 1e-7)
7-3-4. Adam
- モメンタムの過去の勾配の指数関数的減衰平均とRMSPropの過去の勾配の2乗の指数関数的減衰平均を取り入れた最適化アルゴリズム
ANS:
モメンタム:SGDに移動平均を適用する
AdaGrad:誤差をパラメータで微分したものと再定義した学習率の積を減算する
RMSProp:勾配の大きさに応じて学習率を調整する
#8.過学習
8-1. 過学習とは
- 過学習とは、学習が訓練データにフィットし過ぎてしまい、訓練誤差が減少しているのにも関わらず、テスト誤差が増加してしまう現象を指す
8-2. 過学習の対策手法
- 正則化手法(ネットワークの自由度を制約すること)を利用して過学習を抑制する
- L1正則化をラッソ回帰、L2正則化をリッジ回帰を呼ぶ
np.sum(np.abs(network.params['W' + str(ids)]))
weight_decay += weight_decay_lambda * np.sum(np.abs(network.params['W' + str(ids)]))
loss = network.loss(x_batch, d_batch) + weight_decay
- ドロップアウトという、ランダムにノードを削除して学習させることで過学習を抑止する
- データ量を変化させずに、異なるモデルを学習させていると解釈できる
ANS:(a)
ANS:Lasso推定量
#9.畳み込みニューラルネットワークの概念
9-1. CNNの概念
- CNNとは画像の識別や処理を行う際によく用いられるネットワークである
- CNNでは、音声などの1次元データ、カラー画像などの2次元データ、動画などの3次元データなど様々なデータを取り扱うことができる
- CNNは、入力層、畳み込み層、プーリング層、全結合層、出力層からなる
- 畳み込み層では、画像の場合、縦・横・チャンネルの3次元のデータをそのまま学習し、次に伝えることができる
- 畳み込み処理の実装は以下の通り
class Convolution:
# W: フィルター, b: バイアス
def __init__(self, W, b, stride=1, pad=0):
self.W = W
self.b = b
self.stride = stride
self.pad = pad
# 中間データ(backward時に使用)
self.x = None
self.col = None
self.col_W = None
# フィルター・バイアスパラメータの勾配
self.dW = None
self.db = None
def forward(self, x):
# FN: filter_number, C: channel, FH: filter_height, FW: filter_width
FN, C, FH, FW = self.W.shape
N, C, H, W = x.shape
# 出力値のheight, width
out_h = 1 + int((H + 2 * self.pad - FH) / self.stride)
out_w = 1 + int((W + 2 * self.pad - FW) / self.stride)
# xを行列に変換
col = im2col(x, FH, FW, self.stride, self.pad)
# フィルターをxに合わせた行列に変換
col_W = self.W.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b
# 計算のために変えた形式を戻す
out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)
self.x = x
self.col = col
self.col_W = col_W
return out
def backward(self, dout):
FN, C, FH, FW = self.W.shape
dout = dout.transpose(0, 2, 3, 1).reshape(-1, FN)
self.db = np.sum(dout, axis=0)
self.dW = np.dot(self.col.T, dout)
self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)
dcol = np.dot(dout, self.col_W.T)
# dcolを画像データに変換
dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)
return dx
- プーリング層では、対象領域のMax値または平均値を取得して、出力している
- プーリング処理の実装は以下の通り
class Pooling:
def __init__(self, pool_h, pool_w, stride=1, pad=0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad
self.x = None
self.arg_max = None
def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)
# xを行列に変換
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
# プーリングのサイズに合わせてリサイズ
col = col.reshape(-1, self.pool_h*self.pool_w)
#maxプーリング
arg_max = np.argmax(col, axis=1)
out = np.max(col, axis=1)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)
self.x = x
self.arg_max = arg_max
return out
def backward(self, dout):
dout = dout.transpose(0, 2, 3, 1)
pool_size = self.pool_h * self.pool_w
dmax = np.zeros((dout.size, pool_size))
dmax[np.arange(self.arg_max.size), self.arg_max.flatten()] = dout.flatten()
dmax = dmax.reshape(dout.shape + (pool_size,))
dcol = dmax.reshape(dmax.shape[0] * dmax.shape[1] * dmax.shape[2], -1)
dx = col2im(dcol, self.x.shape, self.pool_h, self.pool_w, self.stride, self.pad)
return dx
9-2. 確認テスト
ANS:
公式
$$
O_H = \frac{画像の高さ + 2 x パディング - フィルタ高さ}{ストライド} + 1 \
O_W = \frac{画像の幅 + 2 x パディング - フィルタ幅}{ストライド} + 1
$$
上記公式に当てはめると、出力画像サイズは、7x7となる
#10.最新のCNN
10-1. AlexNetのモデル説明
- AlexNetとは2012年に開かれた画像認識コンペティション2位に大差をつけて優勝したモデルである。
- AlexNetの登場で、ディープラーニングが大きく注目を集めた。
- LeNetが世に出てから20年近くが経過して、AlexNetが発表された
- AlexNetは、5層の畳み込み層およびプーリング層など、それに続く3層の全結合層から構成される
- 活性化関数にReLu関数を用いる点、LRNという局所的正規化を行う層を用いる点、ドロップアウトを使用する点がLeNetとの違いである
- AlexNetを発明したのは、当時トロント大学のアレックス・クリジェフスキー氏である。学生がこのような後世まで名を残す大発明をしたと知って、非常に驚いた
- また、AlexNetの実装については、下記記事が非常に参考になった
参考:AlexNet
- 詳細は、記事を読んでいただきたいが、学習の結果20Epochで80%の識別精度に到達しているそうだ
ANS:2x2の画像サイズとして出力される
#おわりに
本記事の作成にあたっては、オライリー社のゼロから作るDeep Learningを参考にさせていただきました。