今更だけどCNN動かしてみる

しばらくサボっていて忘れられそうなので、ブログタイトルも新たに久しぶりに書きました。

chainerでCNN動かしてみた

別件でDNNの中でもFNNとかRNNとかCNNをいろいろ試しみようという何かがありまして、、、
個人的に勉強がてら最早流行でもなくなったCNN(Convolutional Neural Network)をchainerで動かしてみました。

CNNの概要

CNNの全体像は、バリエーションは様々ですがシンプルなもので下のような感じになります。
f:id:kzzzz:20160404220122p:plain

入力画像の部分的な領域に対してフィルターを通すことで、部分的な特徴を抽出します(=特徴マップ)。異なるフィルターを使うことで様々な特徴を捉えることができ、フィルターの数だけ特徴マップができることになります。この処理が畳み込みと呼ばれています。

次に、出来上がった特徴マップに対して、似たような感じで局所領域だけが見えるウインドウを移動させながら、その中の特徴的な点のみを次の層へ伝播させます。この処理をプーリングと言い、代表的な方法ではウインドウ内の最も大きな値を伝播させたりします。このプーリング層を通すことで、対象画像の中の物体の位置のズレや変形を吸収できるので識別精度を上げられるそうです。

このような畳み込み層とプーリング層を繰り返した後で、通常のニューラルネットに入力して画像の判別を行うというのが典型的なCNNの構成になります。

画像判別

本来は、
f:id:kzzzz:20160404223714p:plain
↑こんな文字とか人の顔とかの画像を入れて、
f:id:kzzzz:20160404223719p:plain
↑この辺りの部分的な特徴から、この画像は数字の5だと判別するものですが、
手元に懐かしい写真があったので、いい感じで判別できないものかと入れてみました。

使ったのは次の3種類の画像で、3クラスの分類問題としました。

  f:id:kzzzz:20160404224246j:plain:w250 f:id:kzzzz:20160404224424j:plain:w250 f:id:kzzzz:20160404225006j:plain:w250

  • 街中の風景

 f:id:kzzzz:20160404224601j:plain:w250 f:id:kzzzz:20160404224615j:plain:w250 f:id:kzzzz:20160404225114j:plain:w250

  • F1サーキット

 f:id:kzzzz:20160404224759j:plain:w250 f:id:kzzzz:20160404224810j:plain:w250 f:id:kzzzz:20160404225139j:plain:w250

訓練データが92枚、テストデータが48枚という極小データセット( ;∀;)

画像読み込み

元画像がjpgのため、OpenCVを使って読み込みました。ここで気をつけなければいけないのが、OpenCVで読み込んだデータはchainerが受け付ける構造と微妙に違うところ。
OpenCVで読み込むと(height, width, channel(=rgb)) な構造になっているところを、(channel, height, width)に変換する必要があります。

img = cv2.imread("/data/img/train/0/001.jpg") # jpg読み込み
img = np.transpose(img, (2,0,1)) # データ項目入れ替え。チャンネルを一番前に。

訓練用データとテスト用データに分けて、あらかじめ3種類に分類してラベル付けしました。
汚いコードを抜粋すると以下の様な感じです。実験では、元画像を50×50ピクセルに縮小しました。3種類のラベルごとにフォルダを作って、フォルダ名でラベルを付与しています。

x_train = []
y_train = []
for i in range(0,3):
    files = os.listdir("/data/img/train/"+str(i))
    for img_index in range(len(files)):
        if files[img_index].find('.jpg') > 0:
            img = cv2.imread("/data/img/train/"+str(i)+"/"+files[img_index])
            resized_img = cv2.resize(img, (50, 50)) # 画像を縮小
            x_train.append(np.transpose(resized_img,(2,0,1))/255.0)
            y_train.append(i)
train_data = np.array(x_train).astype(np.float32).reshape(len(x_train), 3, 50, 50)/255 # 255で割って0から1の値になるように。
train_label = np.array(y_train).astype(np.int32)

テストデータもほとんど同様なので省略。

モデル

畳み込み層とプーリング層を2回ずつ通すモデルを作りました。
フィルターのサイズやウインドウのサイズは色々変えながら動かしました。ので、下のコード中の数字も適当に決めたうちの1つです。。。

model = chainer.FunctionSet(conv1 = F.Convolution2D(3, 20, 3, pad=1), # 入力が3チャンネル。出力が20チャンネル。フィルタサイズが3×3。paddingが1。
                            conv2 = F.Convolution2D(20, 50, 3, pad=1), # 出力が50チャンネル。
                           l1 = F.Linear(4050, 256), # <- 9*9*50  の特徴マップを256ユニットの隠れ層へ
                           l2 = F.Linear(256, 3)) # 256ユニットの隠れ層から出力層が3ユニット(最終的に分類したい種類)

def forward(x_data, y_data, train=True):
    x = chainer.Variable(np.array(x_data, dtype=np.float32), volatile=not train)
    t = chainer.Variable(np.array(y_data, dtype=np.int32), volatile=not train)
    h = F.max_pooling_2d(F.relu(model.conv1(x)), ksize=6,stride=3,pad=1) # <- ksize: 6*6のウインドウ。stride: 3ピクセルずつ移動。
    h = F.max_pooling_2d(F.relu(model.conv2(h)), ksize=4,stride=2,pad=1)
    h = F.dropout(F.relu(model.l1(h)), train=train)
    y = model.l2(h)
    if train:
        return F.softmax_cross_entropy(y, t)
    else:
        return F.accuracy(y, t)

学習

学習自体は至って普通の手順で回す感じです。抜粋は以下の通りです。データが少ないのでミニバッチで回す意味があるかどうかはアレですが、、

for epoch in six.moves.range(0, n_epoch):
    # 訓練
    for i in six.moves.range(0, N_train, batchsize_train):
        x_batch = train_data[perm[i:i + batchsize_train]]
        y_batch = train_label[perm[i:i + batchsize_train]]
        optimizer.zero_grads()
        loss = forward(x_batch, y_batch)
        loss.backward()
        optimizer.update()

    # テスト
    sum_acc = 0
    for i in six.moves.range(0, N_test, batchsize_test):
        x = test_data[perm_test[i:i + batchsize_test]]
        y = test_label[perm_test[i:i + batchsize_test]]
        acc = forward(x, y, train=False)
        sum_acc += float(acc.data) * len(y)
    print("test accuracy: {0}".format(sum_acc/N_test))

結果

上のコードでテスト結果のaccuracyは残念ながら0.5417でした。
フィルターサイズやユニット数を変えても精度に大きな変化はありませんでした。

まとめ

CNNのモデルを作って動かしてみるという目的は達成しました。
精度がよろしくなかったのは、データ量が少なすぎて学習できていないことがあると思います。が、明確な被写体がいない風景写真ばかりなので、分類しようがないという大きな問題も孕んでいそうです。そもそもCNN向きのタスクではないのかも、、、

データが少ないにもかかわらず、CPU1個では遅すぎたので、次はGPUで並列でまわしたいかなーっと思ってます。