Cahiner 1.6.1 を使って Convolution Neural Netowork を作ってみたので、メモしておく。
チュートリアルなどではなかなか分かり難かったのだが、
http://jprogramer.com/ai/3808 「Chainerを使って畳み込みを実装する」
を参考にさせて頂いて作成することができた。
Convolution 畳み込みの投射を形成するには、L.Convolution2D() を使う。
ネットワークを作るには以下の様なコードを用意した。
class MyNN(Chain):
def __init__(self, in_size_x, in_size_y, middle_size, out_size, filtersize = (3,3)):
convOutPtn = 1
afterconv_size = convOutPtn * (in_size_x - filtersize[0] + 1)*(in_size_y - filtersize[1] + 1)
super(MyNN,self).__init__(
Conv = L.Convolution2D(1, convOutPtn, filtersize),
MtoM = L.Linear(afterconv_size, middle_size),
MtoO = L.Linear(middle_size, out_size),
)
self.train = True
self.dr = 0.4
def __call__(self, x):
h = F.relu(self.Conv(x))
h = F.dropout(F.relu(self.MtoM(h)), train=self.train, ratio=self.dr)
h = F.relu(self.MtoO(h))
return h
L.Convolution2D の引数の意味と入力データのarray形状の準備がポイント。
まず、L.Convolution2D の引数について。
L.Convolution2D(inputPtn, convOutPtn, filtersize) で、
まず第1引数は Convolution をアプライする入力の層の厚みの数。
32x32 のような例えば画像の x, y のサイズや総ピクセル数ではない。なので、例えば、画像データなどでRGB別々にデータを用意しているのであれば 3、ということになる。白黒だけということなら1ということで良い。
1枚の画像の x, y のサイズが 32x32 なのか、64x64 なのか、あるいは 3x80 なのか、といったdimension の数字は引数には渡さない。自動的に判別される。
第2引数は適用するフィルターの種類の数。この数が、Convolution 層の出力の厚みになる。
第3引数はフィルターの dimension。3とすれば 3x3 のことになるが、3x4 等としたい場合は、タプルで (3, 4) と渡す事ができる。
入力データは、array の shape を [厚み, x-size, y-size] に整形する必要がある。
例えば、一枚の白黒画像であれば、[1, 64, 64] などとなる(64x64 は例えば画像の x, y サイズ)。
学習用に 100 枚の画像があれば、入力データの array は [100, 1, 64, 64] という shape になる。
64x64 の画像に 4x4 のフィルターを畳み込んだとすると、
出力は 61x61 のサイズになる。フィルターと元のデータ完全に重なったところから畳み込みの計算が始まるので、その端の分のサイズが縮小される。
縦横それぞれ別々に縮小することになるので、例えば、32x64 の入力に 3x4 のフィルターを畳み込んだとすると、30x61 が出力される。
(1x)64x64 の画像に 4x4 のフィルターを5種類畳み込めば、5x61x61の出力になる。
したがって、Convolution 層の後に中間層としてパーセプトロンを続けるとすれば、その層のセル数は 5x61x61 = 18605 となる。
上の MyNN では入力層に対して畳み込みを計算して次の層への入力としているので、
ネットワークを作る引数として、入力データの x, y サイズと、フィルターサイズを与えることで、次の中間層のセル数を
afterconv_size = convOutPtn * (in_size_x - filtersize[0] + 1)*(in_size_y - filtersize[1] + 1)
と計算している。
上の例では、この中間層に dropout を適用している。
局所的特徴を抽出した後、位置のずれにロバストにしたければ、Convolution のあと max pooling をかける必要がある。
実施例は以下のような感じ。
いつもの import 準備をし、
import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
import matplotlib.pyplot as plt
この後に上の MyNN クラスを定義する。
そして、テスト入力データの生成。
>>> xin = np.array(np.random.rand(5,1,10,10), dtype=np.float32)
Array の shape は以下のようになる。
>>> xin.shape
(5, 1, 10, 10)
ニューラルネットワークを作る。
>>> mnn = MyNN(10, 10, 20, 3, (3,3))
>>> mnn.Conv.W.data.shape
(1, 1, 3, 3)
>>> mnn.MtoM.W.data.shape
(20, 64) # <--- 1x8x8 のサイズになっている
>>> mnn.MtoO.W.data.shape
(3, 20)
入力してみる。
>>> mnn.train = False
>>> mnn(Variable(xin)).data
array([[ 0.08023222, 0. , 0.02786355],
[ 0. , 0. , 0.01061315],
[ 0. , 0. , 0.02424588],
[ 0. , 0. , 0.11227448],
[ 0.04323203, 0. , 0. ]], dtype=float32)
入力5種類に対して、出力が5種類出てきている。
あとは、
model = L.Classifier(mnn, lossfun=F.mean_squared_error)
model.compute_accuracy = False
mopt = optimizers.SGD(0.003)
mopt.setup(model)
として、教師データを準備して、
mnn.train = True
mopt.update(model, data, result)
とすれば、学習を進めることができる
ここで、data, result は Variable のオブジェクト。
MyNN の convOutPtn を 6 にすると
>>> mnn.Conv.W.data.shape
(6, 1, 3, 3)
>>> mnn.MtoM.W.data.shape
(20, 384) # <--- 6x8x8 のサイズになっている
出力サイズはもちろん変わらず 5x3 のまま。
>>> mnn(Variable(xin)).data
array([[ 0.20676044, 0.18182571, 0. ],
[ 0. , 0.145252 , 0.47918049],
[ 0. , 0.4610014 , 0.00183053],
[ 0.17483145, 0.25387597, 0.29274395],
[ 0. , 0.25358814, 0. ]], dtype=float32)