ゼロから作るDeepLearning 8章メモ(その1)
ソースコード:deep-learning-from-scratch/ch08/deep_convnet.py
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 親ディレクトリのファイルをインポートするための設定
import pickle
import numpy as np
from collections import OrderedDict
from common.layers import *
class DeepConvNet:
"""認識率99%以上の高精度なConvNet
ネットワーク構成は下記の通り
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
affine - relu - dropout - affine - dropout - softmax
"""
def __init__(self, input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10):
# 重みの初期化===========
# 各層のニューロンひとつあたりが、前層のニューロンといくつのつながりがあるか(TODO:自動で計算する)
pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
weight_init_scales = np.sqrt(2.0 / pre_node_nums) # ReLUを使う場合に推奨される初期値
self.params = {}
pre_channel_num = input_dim[0]
for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
self.params['W' + str(idx+1)] = weight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
pre_channel_num = conv_param['filter_num']
self.params['W7'] = weight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
self.params['b7'] = np.zeros(hidden_size)
self.params['W8'] = weight_init_scales[7] * np.random.randn(hidden_size, output_size)
self.params['b8'] = np.zeros(output_size)
# レイヤの生成===========
self.layers = []
self.layers.append(Convolution(self.params['W1'], self.params['b1'],
conv_param_1['stride'], conv_param_1['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W2'], self.params['b2'],
conv_param_2['stride'], conv_param_2['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W3'], self.params['b3'],
conv_param_3['stride'], conv_param_3['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W4'], self.params['b4'],
conv_param_4['stride'], conv_param_4['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Convolution(self.params['W5'], self.params['b5'],
conv_param_5['stride'], conv_param_5['pad']))
self.layers.append(Relu())
self.layers.append(Convolution(self.params['W6'], self.params['b6'],
conv_param_6['stride'], conv_param_6['pad']))
self.layers.append(Relu())
self.layers.append(Pooling(pool_h=2, pool_w=2, stride=2))
self.layers.append(Affine(self.params['W7'], self.params['b7']))
self.layers.append(Relu())
self.layers.append(Dropout(0.5))
self.layers.append(Affine(self.params['W8'], self.params['b8']))
self.layers.append(Dropout(0.5))
self.last_layer = SoftmaxWithLoss()
def predict(self, x, train_flg=False):
for layer in self.layers:
if isinstance(layer, Dropout):
x = layer.forward(x, train_flg)
else:
x = layer.forward(x)
return x
def loss(self, x, t):
y = self.predict(x, train_flg=True)
return self.last_layer.forward(y, t)
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1 : t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx, train_flg=False)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
tmp_layers = self.layers.copy()
tmp_layers.reverse()
for layer in tmp_layers:
dout = layer.backward(dout)
# 設定
grads = {}
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
grads['W' + str(i+1)] = self.layers[layer_idx].dW
grads['b' + str(i+1)] = self.layers[layer_idx].db
return grads
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
self.layers[layer_idx].W = self.params['W' + str(i+1)]
self.layers[layer_idx].b = self.params['b' + str(i+1)]
解説
【1】初期化
def __init__(self, input_dim=(1, 28, 28),
conv_param_1 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_2 = {'filter_num':16, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_3 = {'filter_num':32, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_4 = {'filter_num':32, 'filter_size':3, 'pad':2, 'stride':1},
conv_param_5 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
conv_param_6 = {'filter_num':64, 'filter_size':3, 'pad':1, 'stride':1},
hidden_size=50, output_size=10):
- 入力データ:チャネル数1, サイズ28×28
- 畳込み層①:フィルター数16, サイズ3×3, パディング1, スライド1
- 畳込み層②:フィルター数16, サイズ3×3, パディング1, スライド1
- 畳込み層③:フィルター数32, サイズ3×3, パディング1, スライド1
- 畳込み層④:フィルター数32, サイズ3×3, パディング1, スライド1
- 畳込み層⑤:フィルター数64, サイズ3×3, パディング1, スライド1
- 畳込み層⑥:フィルター数64, サイズ3×3, パディング1, スライド1
- 全結合層:ノード数50
- 出力層:ノード数10
pre_node_nums = np.array([1*3*3, 16*3*3, 16*3*3, 32*3*3, 32*3*3, 64*3*3, 64*4*4, hidden_size])
各層における前層ノード数(チャネル数×フィルターサイズ縦×フィルターサイズ横)を計算し、リストに格納する。
ここでいう前層ノード数とは、フィルターを通される側のデータをブロックで考えた際の1回分の適応領域を指す。
例えば、畳込み層①であれば、
フィルター適応領域:チャネル数1×フィルターサイズ縦3×フィルターサイズ3
畳込み層②であれば、
フィルター適応領域:チャネル数16×フィルターサイズ縦3×フィルターサイズ3
<畳込み層②のフィルター適応領域算出>
畳込み層①の、
input:(1, 28, 28)
フィルター:(16, 1, 3, 3)
OH = (28 + -3) / 1 + 1 = 26
OW = (28 + -3) / 1 + 1 = 26
であるため、
inputの行列サイズ | im2col後の行列のサイズ |
---|---|
(1, 28, 28) | (26×26, 9) |
im2col後の行列 × フィルターを縦方向に1列に展開して並べた行列
(26×26, 9) × (9, 16)
= (26×26, 16)
reshape((26×26, 16)) = (16, 26, 26)
畳込み層①の出力データは、(16, 26, 26)の行列となる。
よって、畳込み層②のフィルター適応領域は
チャネル数16×フィルターサイズ縦3×フィルターサイズ3 となる。
weight_init_scales = np.sqrt(2.0 / pre_node_nums) # ReLUを使う場合に推奨される初期値
ReLuに特化した初期値「Heの初期値」の計算に必要な標準偏差の値を求める。
「Heの初期値」は前層ノードをn個としたとき、√(2/n)を標準偏差とするガウス分布。
for idx, conv_param in enumerate([conv_param_1, conv_param_2, conv_param_3, conv_param_4, conv_param_5, conv_param_6]):
self.params['W' + str(idx+1)] = weight_init_scales[idx] * np.random.randn(conv_param['filter_num'], pre_channel_num, conv_param['filter_size'], conv_param['filter_size'])
self.params['b' + str(idx+1)] = np.zeros(conv_param['filter_num'])
pre_channel_num = conv_param['filter_num']
以下を、畳込み層の数だけループ処理する。
-
フィルターの重み
self.params['W' + str(idx+1)]
の算出し、辞書params
に格納する。
np.random.randn()
はガウス分布からランダムに値を出力する関数。
よって、self.params['W' + str(idx+1)]
=「Heの初期値」となる。 -
バイアス
self.params['b' + str(idx+1)]
の算出し、辞書params
に格納する。
np.zeros(conv_param['filter_num'])
はフィルター数の大きさでゼロベクトルを作成している。 -
前層ノードのチャネル数を格納している変数
pre_channel_num
の値を、フィルター数で上書きする。
self.params['W7'] = weight_init_scales[6] * np.random.randn(64*4*4, hidden_size)
self.params['b7'] = np.zeros(hidden_size)
self.params['W8'] = weight_init_scales[7] * np.random.randn(hidden_size, output_size)
self.params['b8'] = np.zeros(output_size)
畳込み層以外の重みとバイアスを算出し、辞書params
に格納する。
【2】レイヤの作成
ネットワーク構成は下記の通り
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
conv - relu - conv- relu - pool -
affine - relu - dropout - affine - dropout - softmax
説明省略
【3】推論
def predict(self, x, train_flg=False):
for layer in self.layers:
if isinstance(layer, Dropout):
x = layer.forward(x, train_flg)
else:
x = layer.forward(x)
return x
layer.forward
で前層の出力データを入力データとし、次の層に渡す出力データを作成している。
Dropoutレイヤのときは、引数として、train_flg
を指定する。
※train_flg=False
の時は、Dropoutしない
【4】損失関数を求める
def loss(self, x, t):
y = self.predict(x, train_flg=True)
return self.last_layer.forward(y, t)
predict(self, x, train_flg=False)
で導き出した値y
と、正解ラベルt
を引数として、
self.last_layer.forward(y, t)
によって損失関数を導き出す。
ここで、last_layer
は引数y
をソフトマックス関数に通し、出力された値と正解ラベルt
とで交差エントロピー誤差を計算するSoftmaxWithLoss()
であることに注意する。
【5】正解率を求める
def accuracy(self, x, t, batch_size=100):
if t.ndim != 1 : t = np.argmax(t, axis=1)
acc = 0.0
for i in range(int(x.shape[0] / batch_size)):
tx = x[i*batch_size:(i+1)*batch_size]
tt = t[i*batch_size:(i+1)*batch_size]
y = self.predict(tx, train_flg=False)
y = np.argmax(y, axis=1)
acc += np.sum(y == tt)
return acc / x.shape[0]
-
if t.ndim != 1 : t = np.argmax(t, axis=1)
で正解ラベルが1次元でなければ、軸に対して最も大きな値を取得し、1次元に成形する。 -
バッチサイズごとに正解数を計算し、合算する。
int(x.shape[0] / batch_size))
:入力データ数 / バッチサイズ
x[i*batch_size:(i+1)*batch_size]
:入力データのリストの「i×バッチサイズ~(i+1)×バッチサイズ」番目
t[i*batch_size:(i+1)*batch_size]
:正解ラベルのリストの「i×バッチサイズ~(i+1)×バッチサイズ」番目
acc
:正解数 -
正解数を返す
acc / x.shape[0]
:正解率
【6】勾配計算
def gradient(self, x, t):
# forward
self.loss(x, t)
# backward
dout = 1
dout = self.last_layer.backward(dout)
tmp_layers = self.layers.copy()
tmp_layers.reverse()
for layer in tmp_layers:
dout = layer.backward(dout)
# 設定
grads = {}
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
grads['W' + str(i+1)] = self.layers[layer_idx].dW
grads['b' + str(i+1)] = self.layers[layer_idx].db
return grads
-
順伝播し、損失関数を求める
-
逆伝播し、各パラメータで損失関数を偏微分した値を求める
dout = layer.backward(dout)
で局所的な微分の値が計算される。 -
辞書
grads
勾配を格納する
enumerate((0, 2, 5, 7, 10, 12, 15, 18))
は、layers
リストの中で、パラメータが存在する層のインデックスを指定している。
【7】ハイパーパラメータの保存と取り出し
def save_params(self, file_name="params.pkl"):
params = {}
for key, val in self.params.items():
params[key] = val
with open(file_name, 'wb') as f:
pickle.dump(params, f)
def load_params(self, file_name="params.pkl"):
with open(file_name, 'rb') as f:
params = pickle.load(f)
for key, val in params.items():
self.params[key] = val
for i, layer_idx in enumerate((0, 2, 5, 7, 10, 12, 15, 18)):
self.layers[layer_idx].W = self.params['W' + str(i+1)]
self.layers[layer_idx].b = self.params['b' + str(i+1)]
save_params(self, file_name="params.pkl")
で学習後のハイパーパラメータをファイルに保存する。
load_params(self, file_name="params.pkl")
でファイルに保存されているハイパーパラメータを取り出し初期値とする。
(学習後のパラメータをそのまま使って推論がすぐにできる)