前回までは基本的なニューラルネットワークを勉強していました。
急に難しいことはできないので、次はCNNをやってみたいと思います。 今回も参考にしたのはこちらです。
ゼロから作るDeep Learning ―Pythonで学ぶディープラーニングの理論と実装
- 作者: 斎藤康毅
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/09/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (18件) を見る
CNN (Convolutional Neural Network)
ざっくりとした歴史背景
はじめにちょっとだけ歴史的な背景を書いておくと、最近やたらAIだのディープラーニングだのが流行っていますが、その火付け役がCNN (Convolutional Neural Network)です。
もともとニューラルネットワークの基礎理論自体は大昔(1950年代)からありました。 1980年代後半になって程度ディープラーニングの理論は確立してきましたが、当時のマシンパワーでは全く歯が立ちませんでした。 そんな中理論が先行して、1990年台にはCNNの原型が論文発表されました。 しかし、それでもマシンパワーが足りず、目立った結果を出せませんでした。
そこに、GPUコンピューティングが爆発的に研究されるようになり、計算性能が飛躍的に向上しました。 そして、2012年にGPUとCNNを組み合わせたAlexNetが発表され、機械学習がうまくいくことが実証されました。 こうして現在のアホみたいな機械学習の盛り上がりに繋がったというわけです。
ネットワークの構成
ニューラルネットワークで画像認識なんかをすると、一旦二次元の画像を一次元に並べ直すため、全ての画素が平等に取り扱われてしまいます。 CNNの最大の利点としては、画像の形状を二次元のまま取り扱うことができるため、二次元の情報を考慮した学習ができることです。
基本的なニューラルネットワークは全てのノードがすべて結合しています。
これがCNNになるとこんな感じになります。
これでどんな学習がされるかを表したわかりやすい図がこちらです。
Machine Learning, Neural Networks and Algorithms – Chatbots Magazine
なんとなく、物体の輪郭を学習している層、車のパーツを学習している層、全体像を学習していることがわかります。 ふわっと伝わってれば問題ないです。
さて、ざっくりとCNNの特徴を抑えたところで実装について考えます。 CNNでは、前回使っていたAffine層の代わりにConvolution層とPooling層を使用します。
前回作っていた普通のNNのイメージとしてはこんな感じ。
そんでもって、今回作ってみるCNNはこんな感じ。
活性化関数がSigmoid関数からReLU関数に変更されています。 先人の知恵で、こっちの方がうまくいくみたいです。
こんな感じに前使ってた層をガチャンと外して、新しくConvolution層とPooling層をガッチャンコすればCNNになります。
Convolution層
まずはConvolution層について見ていきます。
forward
Convolution層では積和演算というのをやっていて、イメージで書くとこんな感じの演算です。
試しに出力を計算すると、
入力された信号とフィルターの対応する位置の要素同士をかけ合わせて最後にバイアスを含めて足し合わせます。 99%写経した中身を見てみるとこんな感じです。
def forward(self, x): # フィルターと出力の形状のセットアップ FN, C, FH, FW = self.W.shape N, C, H, W = x.shape out_h = 1 + int((H + 2*self.pad - FH) / self.stride) out_w = 1 + int((W + 2*self.pad - FW) / self.stride) # フィルターのサイズに分割、1次元のベクトルに整形 col = im2col(x, FH, FW, self.stride, self.pad) 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
backward
逆伝播に関しては、計算された重みに対して誤差の分をかけ合わせてネットワークの重みを修正します。
def backward(self, dout): FN, C, FH, FW = self.W.shape dout = dout.transpose(0,2,3,1).reshape(-1, FN) # affine層と同様の逆伝播 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) dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad) return dx
Pooling層
次にPooling層について見てみます。
pooling層では対応する領域内から最大値を取得していきます。 イメージとしてはこんな感じです。
forward
写経した中身を見てみると、行列の次元の変更等はありますがその他は最大値を抽出しているだけです。
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) # 2D => 1D col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad) col = col.reshape(-1, self.pool_h*self.pool_w) # max pooling 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
backward
Pooling層は重みを持たないので、活性化関数なんかと同じで逆伝播の際には次の層の入力を求めるだけです。 最大値が出力された要素にロスの分を格納して逆伝播させます。
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ってどんくらいの精度がでんねん」ってとこだと思うので、実行してみます。
前回の普通のNNが16epochで精度が97%越えるくらい、今回は99% 超えとかなので若干いいくらいでしょうか。 MNISTなのであんまり参考にはなりませんが、まぁこんなもんでしょう。 もうちょい複雑なことやらないと良さは実感できないってことですね。
参考
今回もちゃんとやった証拠にgithubにあげました。証拠を残す意味なので意味なので特に意味はありません。