アプリとサービスのすすめ

アプリやIT系のサービスを中心に書いていきます。たまに副業やビジネス関係の情報なども気ままにつづります

VGGNetを参考にしたCNN(tensorflow)でマンション関連の画像を分類してみる

マンション関連の画像をCNNで分類する試みをしてみた。普通のcifar-10のモデルだと正解率が低かったが、VGGNet(Visual Geometry Group Networks)と呼ばれる高性能のCNNを実装しているサイト(すぎゃーんメモ)があったので、試しに実装してみたところ正解率87.8%を達成することができた。今回はそのログとして詳細を書いていこうと思う。


目次
1.画像の内訳
2.CNN
3.ラベル毎の正解率



1.画像の内訳

まず、訓練とテストに使った画像の枚数と、その内容の内訳は次の通りだ。


訓練画像→ 合計 25300枚
Label 0
玄関 50, 廊下 50, リビング 150, 寝室 100, クローゼット 100 (増幅して6300枚)

Label 1
キッチン 300 (増幅して6000枚)

Label 2
トイレ 400, 風呂 150, 洗面化粧室 150 (増幅して7000枚)

Label 3
眺望 300枚 (増幅して6000枚)



テスト画像→合計 540枚
Label 0
玄関 30, 廊下 30, リビング 30, 寝室 30, クローゼット 30 (150枚)

Label 1
キッチン 115(115枚)

Label 2
トイレ 80, 風呂 40, 洗面化粧室 40 (160枚)

Label 3
眺望 115 (115枚)

ラベル0〜3までの4種類で訓練画像は25300枚、テスト画像は540枚。
下にラベル毎の画像の一部を抜粋した。96pxで、左からラベル順になっている。

f:id:trafalbad:20170930083205p:plain

元画像は少ないものの、画像をうまく増幅させた(increacing_images.py)。画質に変化を加えるのは原則一回として、画質にあまり影響のない範囲で増幅させた。


元画像→ガンマ変換→コントラスト→.......→左右変換→角度変換

#左右変換
flip_img=[]
for i in seen5000:
    flip_img.append(cv2.flip(i, 1))

#角度変換
rad=np.pi/90 # circumference ratio
# distance to move to x-axis
move_x = 0
# distance to move to x-axis
move_y = 96 * -0.000000005
 
matrix = [[np.cos(rad),  -1 * np.sin(rad), move_x], [np.sin(rad),   np.cos(rad), move_y]]
 
affine_matrix3 = np.float32(matrix)
afn_90=[]
for i in gaikan:
    afn_90.append(cv2.warpAffine(i, affine_matrix3, size, flags=cv2.INTER_LINEAR))

”ガンマ変換”や”コントラスト”は元画像に一回しか適用できない。例えば”ガンマ変換”した画像にさらに”コントラスト”を適用すると画質がやばくなる。

しかし、”左右変換”と”角度変換”は”ガンマ変換やコントラスト”に適用しても画質は変換しないので、単純に全画像数を2倍にできる。最後にこの2つを持ってくることで効率よく画像を水増しできる。

”角度変換”は値を変えれば何回でも使えるので増幅にはおすすめだ。





2.CNN

元々はcifar-10のモデルで正解率を出したが、70%くらいだったので、もっといいのはないかと探していたところ、アイドル画像分類というサイト(すぎゃーんメモ)でVGGNetを参考にしたモデルが紹介されていた。かなりの精度だったので、今回はこれを適用してみた。


結果は87.8%というかなり精度の高い結果。cifar-10のシンプルなものよりも確実に上がっている。もっと層を厚くすれば正解率は上がりそうな気もするけど、精度としては十分なので、これを使った。


ただ唯一違うのはバッチノーマライゼーションを適用してる点だ。別に適用しなくても正解率はほとんど変わらないが、プーリング層だけのモデルと比較して2%ほど正解率が上昇した。

def cnn(x):
    BATCH_SIZE = 128
    def _variable_with_weight_decay(name, shape, stddev, wd):
        var = tf.get_variable(name, shape=shape, initializer=tf.truncated_normal_initializer(stddev=stddev))
        if wd is not None:
            weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
            tf.add_to_collection('losses', weight_decay)
        return var

    def _activation_summary(x):
        tensor_name = x.op.name
        tf.summary.histogram(tensor_name+'/activations', x)
        tf.summary.scalar(tensor_name + '/sparsity', tf.nn.zero_fraction(x))

    with tf.variable_scope('conv1') as scope:
        kernel = _variable_with_weight_decay('weights', shape=[3, 3, 3, 32], stddev=0.1, wd=0.0)
        conv = tf.nn.conv2d(x, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[32], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv1 = tf.nn.relu(bias, name='conv1')
        _activation_summary(conv1)
    pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool1')
    norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
    
    with tf.variable_scope('conv2') as scope:
        kernel = _variable_with_weight_decay('weights',shape=[3, 3, 32, 64],stddev=0.1,wd=0.0)
        conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[64], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv2 = tf.nn.relu(bias, name='conv2')
        _activation_summary(conv2)
    pool2 = tf.nn.max_pool(conv2, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool2')
    norm2 = tf.nn.lrn(pool2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
    
    with tf.variable_scope('conv3') as scope:
        kernel = _variable_with_weight_decay('weights',shape=[3, 3, 64, 128],stddev=0.1,wd=0.0)
        conv = tf.nn.conv2d(norm2, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[128], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv3 = tf.nn.relu(bias, name='conv3')
        _activation_summary(conv3)
    pool3 = tf.nn.max_pool(conv3, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool3')
    norm3 = tf.nn.lrn(pool3, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
    
    with tf.variable_scope('conv4') as scope:
        kernel = _variable_with_weight_decay('weights',shape=[3, 3, 128, 256],stddev=5e-2,wd=0.0)
        conv = tf.nn.conv2d(norm3, kernel, [1, 1, 1, 1], padding='SAME')
        biases = tf.get_variable('biases', shape=[256], initializer=tf.constant_initializer(0.0))
        bias = tf.nn.bias_add(conv, biases)
        conv4 = tf.nn.relu(bias, name='conv4')
        _activation_summary(conv4)
    pool4 = tf.nn.max_pool(conv4, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1], padding='SAME', name='pool4')
    norm4 = tf.nn.lrn(pool4, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='norm1')
    
    with tf.variable_scope('fc5') as scope:
        dim = 1
        for d in pool4.get_shape()[1:].as_list():
            dim *= d
        reshape = tf.reshape(pool4, [BATCH_SIZE, dim])
        weights = _variable_with_weight_decay('weights', shape=[dim, 1024],stddev=0.02, wd=0.005)
        biases = tf.get_variable('biases', shape=[1024], initializer=tf.constant_initializer(0.0))
        fc5 = tf.nn.relu(tf.nn.bias_add(tf.matmul(reshape, weights), biases), name='fc5')
        _activation_summary(fc5)

    with tf.variable_scope('fc6') as scope:
        weights = _variable_with_weight_decay('weights', shape=[1024, 256],stddev=0.02, wd=0.005)
        biases = tf.get_variable('biases', shape=[256], initializer=tf.constant_initializer(0.0))
        fc6 = tf.nn.relu(tf.nn.bias_add(tf.matmul(fc5, weights), biases), name='fc6')
        _activation_summary(fc6)

    with tf.variable_scope('fc7') as scope:
        weights = _variable_with_weight_decay('weights', [256, NUMCLASS], stddev=0.02, wd=0.0)
        biases = tf.get_variable('biases', shape=[NUMCLASS], initializer=tf.constant_initializer(0.0))
        fc7 = tf.nn.bias_add(tf.matmul(fc6, weights), biases, name='fc7')
        _activation_summary(fc7)

    return fc7   # shape=(BATCH_SIZE, NUMCLASS)

すぎゃーんメモではアイドル画像という単体の物体に適用していたが、今回はマンション関連の画像を対象にしたところがキー。どこが違うのかというと、マンション関連の画像は結構いろいろな関係ない物体が写っているので、

トイレ=トイレ
キッチン=キッチン

とキレイに分類できないところで、CNNも上手く分類できないのではないか心配だった。

しかし、ラベル設定を適切にして、ラベルを貼る人間がこれは「キッチンだな」とか「室内やな」みたいに確信を持って言えるほど混乱しない画像なら、CNNでも高い正解率は叩き出せるっぽい。

各ラベル毎の正解率は次にまとめた。





3.ラベル毎の正解率

各ラベル毎の正解率は以下の通り


Label 0 96.1%

Label 1 77.3%

Label 2 92.5%

Label 3 93.9%

トータルの正解率は87.8%だけど、ラベル毎の正解率を見てみるとやっぱり、キッチン画像がかなり正解率が低い。





→理由を考えてみる


室内 (Label 0)
→とりあえずベットとか天井とかあるので、室内とはわかる

キッチン (Label 1)
→基準が「コンロと水面台が写ってること」なので、それ以外にもいろいろ写ってるとわかりにくい

トイレ (Label 2)
→便器があればとりあえずわかる

眺望 (Label 3)
→景色の一部が画像の大半を占めているので、多分一番わかりやすい



キッチンは「コンロと水面台が写っている」を基準にした。しかし、他に別なものが写ってる確率が高いキッチンは、ラベル0の”室内”にカウントされてしまうっぽい。

下の画像はキッチンの画像だけど、人間でも「室内」といわれれば、そう見えなくもない感じなので、機械にも分類しにくいんだなと考えられる。


f:id:trafalbad:20170930141735p:plain



これが多分、正解率が下がった原因。ここは明確にキッチンとわかる画像を選べばいいんだけど、汎用性が高くないと意味がない。結局、慈悲は無粋だなというわけで、ちょっとわかりにくいのを配置してみたら結果的に正解率が微妙になった。


今回はマンション関連の画像を分類してみた。なんかいろいろとCNNの分類例はある。

けど、動物や顔だったりの単体の物体ばっかりで、鍋料理みたいにいろいろ入ってるマンション画像を分類する例はあまりないのではと思った。けどまあ、適切にラベル設定をして、ラベル付する人間でも迷わない範囲の画像ならCNNでも高い精度は出た。




*one-hot表現について
参考にしたすぎゃーんメモでは損失関数のところでone-hotを適用してたけど、cifar-10形式では特に必要ないらしい。ただしラベルは0から始めないといけないらしく、出力層(NUMCLASS)が4でもラベルは0〜3にしないといけない。これで一回ハマったので、注意。



本当はDCGANでデータセットを作ってから分類させようとしたんだけど、諸々の事情でCNNを先に試しました。DCGANは上手くいったらまた別記事で書く予定。