ラビットチャレンジレポート 深層学習Day2 その3
Section4 畳み込みニューラルネットワークの概念
畳み込みニューラルネットワーク(CNN)は各次元で連続性のあるデータを扱うのに適したネットワークである。
CNNの構造の代表例は下記である。
入力層
↓
畳み込み層
↓
畳み込み層
↓
プーリング層
↓
畳み込み層
↓
畳み込み層
↓
プーリング層
↓
全結合層
↓
出力層
基本的な構造として有名なものは、LeNet(入力データ行列32×32)がある。
4-1畳み込み層
畳み込み層では、画像の場合、建て、横、チャンネルの3次元のデータをそのまま学習し、次に伝えることが出来る。
具体的には、入力画像Xに、フィルター行列Wを左端から順番にアダマール積をとって行列を出力する。パディング0、ストライド1の場合、入力データがm×m行列、フィルター行列が(m-t)×(m-t)ならば、出力行列は(t+1)×(t+1)行列になる。
4-1-1. 畳み込みの演算概念:バイアス
一般に畳み込み演算では、入力データとフィルタの演算後、バイアスを加える。バイアスの設定によって収束の速さが変化する。
4-1-1.畳み込みの演算概念:パディング
一般に畳み込み演算を行うと、入力データに対して、出力データは小さくなる。例えば、入力データが4×4行列、フィルタが3×3行列の場合、出力データは2×2行列になる。
このため、何度も畳み込みを繰り返すと出力データが小さくなりすぎたり、出力データが計算機処理には適さないサイズ(が適する)になる場合がある。
これを防ぐためにパディング処理がある。
パディングは、元の入力画像の周りにダミーにデータを配置する手法である。
元の入力データ
パディングデータ
ダミーデータは0のほか、隣接データを使用するなどの手法がある。
4-1-2.畳み込みの演算概念:ストライド
畳み込み時に入力データ上をフィルタが動くステップ間隔をストライドと呼ぶ。
ストライドは1のほか、2以上をとることもある。ストライドが大きいほど出力データは小さくなる。
4-1-3.畳み込みの演算概念:チャネル
入力データやフィルタの行列の枚数のことをチャネル言う。
全結合層で画像を学習した場合の欠点
画像の場合、縦、横、チャネルの各次元のデータの位置関係が重要だが、全結合層の場合、1次元のデータとして処理され、各次元間の関連性が反映されない。
そのため、画像を全結合層で学習するのは難しい。
4-2 プーリング層
プーリング層は、特徴マップを空間的な局所ごとに代表値にまとめて空間解像度を下げる(ダウンサンプリング)層である。これにより、入力データのフィルタ形状による位置ずれを吸収する役割がある。
プーリング層の具体的な操作は、畳み込みフィルタと同じく、ある大きさのウィンドウで左上からストライド分ずつ移動しながら代表値をとる。
代表値として、ウインドウ内の最大値をとる場合をMax Pooling、ウィンドウ内の平均値をとる場合をAverage Poolingとそれぞれ呼ぶ。
- 確認テスト
サイズ6×6の入力画像を、サイズ2×2のフィルタで畳み込んだ時の出力画像のサイズを答えよ。なおストライドとパディングは1とする。
[解答]
出力画像の高さOH、幅HWは下記の公式で導出できる
これらを用いると、出力画像のサイズは7×7である。
畳み込みの実装演習
画像データを2次元配列に変換する関数、畳み込み処理の関数、マックスプーリングの関数を定義し、CNNネットワークを実装する。
画像データを2次元配列に変換する(im2col)
import pickle import numpy as np from collections import OrderedDict from common import layers from common import optimizer from data.mnist import load_mnist import matplotlib.pyplot as plt # 画像データを2次元配列に変換 ''' input_data: 入力値 filter_h: フィルターの高さ filter_w: フィルターの横幅 stride: ストライド pad: パディング ''' def im2col(input_data, filter_h, filter_w, stride=1, pad=0): # N: number, C: channel, H: height, W: width N, C, H, W = input_data.shape # 切り捨て除算 out_h = (H + 2 * pad - filter_h)//stride + 1 out_w = (W + 2 * pad - filter_w)//stride + 1 img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant') col = np.zeros((N, C, filter_h, filter_w, out_h, out_w)) for y in range(filter_h): y_max = y + stride * out_h for x in range(filter_w): x_max = x + stride * out_w col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride] col = col.transpose(0, 4, 5, 1, 2, 3) # (N, C, filter_h, filter_w, out_h, out_w) -> (N, filter_w, out_h, out_w, C, filter_h) col = col.reshape(N * out_h * out_w, -1) return col
畳み込みのクラスを作成する
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 ||> プーリングのクラス >|python| 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
CNNのクラスを定義する
class SimpleConvNet: # conv - relu - pool - affine - relu - affine - softmax def __init__(self, input_dim=(1, 28, 28), conv_param={'filter_num':30, 'filter_size':5, 'pad':0, 'stride':1}, hidden_size=100, output_size=10, weight_init_std=0.01): filter_num = conv_param['filter_num'] filter_size = conv_param['filter_size'] filter_pad = conv_param['pad'] filter_stride = conv_param['stride'] input_size = input_dim[1] conv_output_size = (input_size - filter_size + 2 * filter_pad) / filter_stride + 1 pool_output_size = int(filter_num * (conv_output_size / 2) * (conv_output_size / 2)) # 重みの初期化 self.params = {} self.params['W1'] = weight_init_std * np.random.randn(filter_num, input_dim[0], filter_size, filter_size) self.params['b1'] = np.zeros(filter_num) self.params['W2'] = weight_init_std * np.random.randn(pool_output_size, hidden_size) self.params['b2'] = np.zeros(hidden_size) self.params['W3'] = weight_init_std * np.random.randn(hidden_size, output_size) self.params['b3'] = np.zeros(output_size) # レイヤの生成 self.layers = OrderedDict() self.layers['Conv1'] = layers.Convolution(self.params['W1'], self.params['b1'], conv_param['stride'], conv_param['pad']) self.layers['Relu1'] = layers.Relu() self.layers['Pool1'] = layers.Pooling(pool_h=2, pool_w=2, stride=2) self.layers['Affine1'] = layers.Affine(self.params['W2'], self.params['b2']) self.layers['Relu2'] = layers.Relu() self.layers['Affine2'] = layers.Affine(self.params['W3'], self.params['b3']) self.last_layer = layers.SoftmaxWithLoss() def predict(self, x): for key in self.layers.keys(): x = self.layers[key].forward(x) return x def loss(self, x, d): y = self.predict(x) return self.last_layer.forward(y, d) def accuracy(self, x, d, batch_size=100): if d.ndim != 1 : d = np.argmax(d, axis=1) acc = 0.0 for i in range(int(x.shape[0] / batch_size)): tx = x[i*batch_size:(i+1)*batch_size] td = d[i*batch_size:(i+1)*batch_size] y = self.predict(tx) y = np.argmax(y, axis=1) acc += np.sum(y == td) return acc / x.shape[0] def gradient(self, x, d): # forward self.loss(x, d) # backward dout = 1 dout = self.last_layer.backward(dout) layers = list(self.layers.values()) layers.reverse() for layer in layers: dout = layer.backward(dout) # 設定 grad = {} grad['W1'], grad['b1'] = self.layers['Conv1'].dW, self.layers['Conv1'].db grad['W2'], grad['b2'] = self.layers['Affine1'].dW, self.layers['Affine1'].db grad['W3'], grad['b3'] = self.layers['Affine2'].dW, self.layers['Affine2'].db return grad
mnistデータセットを用いて、CNNの学習と評価を行う。
from common import optimizer # データの読み込み (x_train, d_train), (x_test, d_test) = load_mnist(flatten=False) print("データ読み込み完了") # 処理に時間のかかる場合はデータを削減 x_train, d_train = x_train[:5000], d_train[:5000] x_test, d_test = x_test[:1000], d_test[:1000] network = SimpleConvNet(input_dim=(1,28,28), conv_param = {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1}, hidden_size=100, output_size=10, weight_init_std=0.01) optimizer = optimizer.Adam() iters_num = 1000 train_size = x_train.shape[0] batch_size = 100 train_loss_list = [] accuracies_train = [] accuracies_test = [] plot_interval=10 for i in range(iters_num): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] d_batch = d_train[batch_mask] grad = network.gradient(x_batch, d_batch) optimizer.update(network.params, grad) loss = network.loss(x_batch, d_batch) train_loss_list.append(loss) if (i+1) % plot_interval == 0: accr_train = network.accuracy(x_train, d_train) accr_test = network.accuracy(x_test, d_test) accuracies_train.append(accr_train) accuracies_test.append(accr_test) print('Generation: ' + str(i+1) + '. 正答率(トレーニング) = ' + str(accr_train)) print(' : ' + str(i+1) + '. 正答率(テスト) = ' + str(accr_test)) lists = range(0, iters_num, plot_interval) plt.plot(lists, accuracies_train, label="training set") plt.plot(lists, accuracies_test, label="test set") plt.legend(loc="lower right") plt.title("accuracy") plt.xlabel("count") plt.ylabel("accuracy") plt.ylim(0, 1.0) # グラフの表示 plt.show()
結果の図示
訓練制度、テスト精度とも1.0に近く、高い精度で学習と汎化が出来ていることが確認できる。
Section5 最新のCNN
ここでは、Alexnetを取り上げる。
Alexnetとは
Alex Krizhevskyが博士指導教官であるGeoffrey Hintonらとともに設計したネットワーク。
2012年の国際的な画像認識コンテストであるILSVRC2012に参加し、エラー率が次点より10.8%も低いという圧倒的なスコアで優勝した。
Alexの優勝によって、特徴量設計を人間が行うのではなく、たくさんのデータの中から機械が見つけ出す深層学習の特徴によって大きな優位性が実現できることが示され、深層学習ブームの端緒となった。
Alexnetの論文はコンピュータビジョンで発表された最も影響力のある論文と考えられている。
Alexnetの構造
8層の構造になっており、3つの畳み込み層、2つのプーリング層、及び3つの全結合層によって構成されている。活性化関数にはReLuを使用している。
全結合層の処理
- Flatten:前層のデータを、1次元に並べる処理。13×13×256行列なら、4096×1の1次元行列に変換する。
- Global max pooling: 1チャネルずつMax Poolingを行う処理。13×13×256行列なら、13×13行列にMax Poolingを適用し、256×1の1次元行列に変換する。
- Global average pooling: 1チャネルずつAverage Poolingを行う処理。13×13×256行列なら、13×13行列にAverage Poolingを適用し、256×1の1次元行列に変換する。
Flattenより、Global Max PoolingやGlobal Average Poolingのほうが特徴量をよくとらえ、より少ない要素数で、より高い性能を発揮する場合が多い。
実装演習
ここでは、畳み込み層6つ、プーリング層3つの構成で、出力の全結合層のドロップアウトを適用したDeepConvNetを作成し、学習評価する。
DeepConvNetの作成
import pickle import numpy as np from collections import OrderedDict from common import layers from data.mnist import load_mnist import matplotlib.pyplot as plt from common import optimizer 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): # 重みの初期化=========== # 各層のニューロンひとつあたりが、前層のニューロンといくつのつながりがあるか 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]) wight_init_scales = np.sqrt(2.0 / pre_node_nums) # Heの初期値 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)] = wight_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'] = wight_init_scales[6] * np.random.randn(pre_node_nums[6], hidden_size) print(self.params['W7'].shape) self.params['b7'] = np.zeros(hidden_size) self.params['W8'] = wight_init_scales[7] * np.random.randn(pre_node_nums[7], output_size) self.params['b8'] = np.zeros(output_size) # レイヤの生成=========== self.layers = [] self.layers.append(layers.Convolution(self.params['W1'], self.params['b1'], conv_param_1['stride'], conv_param_1['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Convolution(self.params['W2'], self.params['b2'], conv_param_2['stride'], conv_param_2['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Pooling(pool_h=2, pool_w=2, stride=2)) self.layers.append(layers.Convolution(self.params['W3'], self.params['b3'], conv_param_3['stride'], conv_param_3['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Convolution(self.params['W4'], self.params['b4'], conv_param_4['stride'], conv_param_4['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Pooling(pool_h=2, pool_w=2, stride=2)) self.layers.append(layers.Convolution(self.params['W5'], self.params['b5'], conv_param_5['stride'], conv_param_5['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Convolution(self.params['W6'], self.params['b6'], conv_param_6['stride'], conv_param_6['pad'])) self.layers.append(layers.Relu()) self.layers.append(layers.Pooling(pool_h=2, pool_w=2, stride=2)) self.layers.append(layers.Affine(self.params['W7'], self.params['b7'])) self.layers.append(layers.Relu()) self.layers.append(layers.Dropout(0.5)) self.layers.append(layers.Affine(self.params['W8'], self.params['b8'])) self.layers.append(layers.Dropout(0.5)) self.last_layer = layers.SoftmaxWithLoss() def predict(self, x, train_flg=False): for layer in self.layers: if isinstance(layer, layers.Dropout): x = layer.forward(x, train_flg) else: x = layer.forward(x) return x def loss(self, x, d): y = self.predict(x, train_flg=True) return self.last_layer.forward(y, d) def accuracy(self, x, d, batch_size=100): if d.ndim != 1 : d = np.argmax(d, axis=1) acc = 0.0 for i in range(int(x.shape[0] / batch_size)): tx = x[i*batch_size:(i+1)*batch_size] td = d[i*batch_size:(i+1)*batch_size] y = self.predict(tx, train_flg=False) y = np.argmax(y, axis=1) acc += np.sum(y == td) return acc / x.shape[0] def gradient(self, x, d): # forward self.loss(x, d) # 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
mnistデータによる学習と評価を行う。
(x_train, d_train), (x_test, d_test) = load_mnist(flatten=False) # 処理に時間のかかる場合はデータを削減 x_train, d_train = x_train[:5000], d_train[:5000] x_test, d_test = x_test[:1000], d_test[:1000] print("データ読み込み完了") network = DeepConvNet() optimizer = optimizer.Adam() iters_num = 1000 train_size = x_train.shape[0] batch_size = 100 train_loss_list = [] accuracies_train = [] accuracies_test = [] plot_interval=10 for i in range(iters_num): batch_mask = np.random.choice(train_size, batch_size) x_batch = x_train[batch_mask] d_batch = d_train[batch_mask] grad = network.gradient(x_batch, d_batch) optimizer.update(network.params, grad) loss = network.loss(x_batch, d_batch) train_loss_list.append(loss) if (i+1) % plot_interval == 0: accr_train = network.accuracy(x_train, d_train) accr_test = network.accuracy(x_test, d_test) accuracies_train.append(accr_train) accuracies_test.append(accr_test) print('Generation: ' + str(i+1) + '. 正答率(トレーニング) = ' + str(accr_train)) print(' : ' + str(i+1) + '. 正答率(テスト) = ' + str(accr_test)) lists = range(0, iters_num, plot_interval) plt.plot(lists, accuracies_train, label="training set") plt.plot(lists, accuracies_test, label="test set") plt.legend(loc="lower right") plt.title("accuracy") plt.xlabel("count") plt.ylabel("accuracy") plt.ylim(0, 1.0) # グラフの表示 plt.show()
結果の図示
訓練データ、テストデータ双方に98%以上の高い正解率を示した。