pyhaya’s diary

機械学習系の記事をメインで書きます

Tensorflowで犬猫画像分類する

最近Tensorflowを勉強していて、試しに定番の(?)犬猫の画像分類をしてみました。僕がやったことをまとめると

  • CNN
  • tf.kerasは使わない
  • TFRecordにデータを保存してそこからデータを引っ張り出してくる
  • もちろんBatch

こんな感じのことを書きます。なのでこの記事の位置づけは、画像解析手法を書くというよりかはTensorflowの使い方みたいな感じです。

環境

使ったデータ

データの出処

今回使ったデータはKaggleからダウンロードしました。
www.kaggle.com

データのディレクトリ関係

今回の分析でのディレクトリ構造は下のようになっています

.
├── log
└── training_set
|    ├── cats
|    └── dogs
└── tf_cnn.ipynb

catsディレクトリとdogsディレクトリにはそれぞれ約4000枚のJPEG画像が保存されています。中身を見てみると、必ずしも犬・猫のみが写ったものだけではなく、人も一緒に写っていたり、イラストだったりします。

TFRecordにデータを保存

今回はデータを一度TFRecordにバイナリ形式で保存します。まずは各画像ファイルへのパスとラベルをリストの中に収納します。ラベルは猫が1、犬が0となっています。

import numpy as np
import tensorflow as tf


cat_dir = './training_set/cats/'
dog_dir = './training_set/dogs/'

image_paths = []
labels = []

for fname in os.listdir(cat_dir):
    if '.jpg' in fname:
        image_paths.append(cat_dir + fname)
        labels.append(1)
        
for fname in os.listdir(dog_dir):
    if '.jpg' in fname:
        image_paths.append(dog_dir + fname)
        labels.append(0)

# シャッフルする
shuffle_ind = np.random.permutation(len(labels))
image_paths = np.array(image_paths)[shuffle_ind]
labels = np.array(labels)[shuffle_ind]

リストの後ろから1000個のファイルをテストデータとして切り分けてそれぞれ別ファイルに保存します。

def _bytes_feature(value):
    return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))

def _float_feature(value):
    return tf.train.Feature(float_list=tf.train.FloatList(value=[value]))

def _int64_feature(value):
    return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))


from PIL import Image

# トレーニングデータの保存
with tf.python_io.TFRecordWriter('training_data.tfrecords') as writer:
    for fname, label in zip(image_paths[:-1000], labels[:-1000]):
        image = Image.open(fname)
        image_np = np.array(image)
        image_shape = image_np.shape
        image = open(fname, 'rb').read()

        feature = {
            'height' : _int64_feature(image_shape[0]),
            'width' : _int64_feature(image_shape[1]),
            'channel' : _int64_feature(image_shape[2]),
            'image_raw' : _bytes_feature(image),    # 画像はバイトとして保存する
            'label' : _int64_feature(label)
        }
        tf_example = tf.train.Example(features=tf.train.Features(feature=feature))
        writer.write(tf_example.SerializeToString())

# テストデータの保存
with tf.python_io.TFRecordWriter('test_data.tfrecords') as writer:
    for fname, label in zip(image_paths[-1000:], labels[-1000:]):
        image = Image.open(fname)
        image_np = np.array(image)
        image_shape = image_np.shape
        image = open(fname, 'rb').read()

        feature = {
            'height' : _int64_feature(image_shape[0]),
            'width' : _int64_feature(image_shape[1]),
            'channel' : _int64_feature(image_shape[2]),
            'image_raw' : _bytes_feature(image),
            'label' : _int64_feature(label)
        }
        tf_example = tf.train.Example(features=tf.train.Features(feature=feature))
        writer.write(tf_example.SerializeToString())

これでデータの保存はできました。

CNNによるモデルの構築

次に、画像の分類に用いるモデルをCNNで構築します。

tf.reset_default_graph()

X = tf.placeholder(tf.float32, shape=[None, 150, 150, 3])
y = tf.placeholder(tf.int32, shape=[None])

with tf.name_scope('layer1'):
    conv1 = tf.layers.conv2d(X, filters=32, kernel_size=4, strides=1, activation=tf.nn.relu, name='conv1')
    pool1 = tf.nn.max_pool(conv1, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool1')
    
with tf.name_scope('layer2'):
    conv2 = tf.layers.conv2d(pool1, filters=64, kernel_size=3, strides=1, activation=tf.nn.relu, name='conv2')
    pool2 = tf.nn.max_pool(conv2, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool2')
    
with tf.name_scope('layer3'):
    conv3 = tf.layers.conv2d(pool2, filters=128, kernel_size=3, strides=1, activation=tf.nn.relu, name='conv3')
    pool3 = tf.nn.max_pool(conv3, ksize=[1,2,2,1], strides=[1,2,2,1], padding='VALID', name='pool3')
    
with tf.name_scope('dense'):
    flatten = tf.reshape(pool3, shape=[-1, 32768], name='flatten')
    dense1 = tf.layers.dense(flatten, 512, activation=tf.nn.relu, name='dense1')
    dense2 = tf.layers.dense(dense1, 2, activation=None, name='dense2')
    output = tf.nn.softmax(dense2, name='output')
    
with tf.name_scope('train'):
    xentropy = tf.losses.sparse_softmax_cross_entropy(logits=dense2, labels=y)
    loss = tf.reduce_mean(xentropy)
    optimizer = tf.train.AdamOptimizer()
    training_op = optimizer.minimize(loss)

with tf.name_scope('eval'):
    correct = tf.nn.in_top_k(dense2, y, 1)
    acc = tf.reduce_mean(tf.cast(correct, tf.float32))
    
with tf.name_scope('save'):
    train_acc = tf.summary.scalar('train_acc', acc)
    valid_acc = tf.summary.scalar('valid_acc', acc)
    file_writer = tf.summary.FileWriter('./log/190401/', tf.get_default_graph())
    saver = tf.train.Saver()

入力画像の形状は(150, 150, 3)で、カラー画像です。3つの畳み込み層と3つのプーリング層を重ね、最後に全結合層で長さ2のベクトルを出力しています。出力ベクトルのそれぞれの要素は、画像が犬である確率と猫である確率をそれぞれ表しています。

訓練はAdamを用いて行います。そして訓練の途中結果とパラメータを保存するためにFileWriterとSaverを用意します。

訓練する

では実際に訓練をします。まず、TFRecordからデータを取り出すための準備をしておきます。

image_feature_description = {
    'height' : tf.FixedLenFeature([], tf.int64),
    'width' : tf.FixedLenFeature([], tf.int64),
    'channel' : tf.FixedLenFeature([], tf.int64),
    'image_raw' : tf.FixedLenFeature([], tf.string),
    'label' : tf.FixedLenFeature([], tf.int64),
}

def _parse_fun(example_proto):
    feature = tf.parse_single_example(example_proto, image_feature_description)
    feature['image_raw'] = tf.image.decode_jpeg(feature['image_raw'])
    feature['image_raw'] = tf.cast(feature['image_raw'], tf.float32) / 255.0    #floatにキャストしてから255で割って正規化
    feature['image_raw'] = tf.image.resize_images(feature['image_raw'], (150, 150))    #150x150にリサイズ
    
    feature['label'] = tf.cast(feature['label'], tf.int32)
    
    return feature

では実際に訓練します。

epochs = 31
batch_size = 500

with tf.Session() as sess:
    raw_image_dataset = tf.data.TFRecordDataset('training_data.tfrecords')
    test_dataset = tf.data.TFRecordDataset('test_data.tfrecords')
    
    parsed_image_dataset = raw_image_dataset.map(_parse_fun)
    test_dataset = test_dataset.map(_parse_fun).batch(100)
    batched_dataset = parsed_image_dataset.batch(batch_size)
    
    init = tf.global_variables_initializer()
    init.run()

    for epoch in range(epochs):
        iterator = batched_dataset.make_one_shot_iterator()
        test_iter = test_dataset.make_one_shot_iterator()
        while True:
            try:
                batched = iterator.get_next()
                batched_eval = sess.run(batched)
                X_batch = batched_eval['image_raw']
                y_batch = batched_eval['label']
                sess.run(training_op, feed_dict={X: X_batch, y: y_batch})
            except tf.errors.OutOfRangeError:
                break
        
        if epoch % 5 == 0:
            print(f"finished epoch #{epoch}")
            test_data = test_iter.get_next()
            test_data_eval = sess.run(test_data)
            X_test = test_data_eval['image_raw']
            y_test = test_data_eval['label']
            train_acc_str = train_acc.eval(feed_dict={X: X_batch, y: y_batch})
            valid_acc_str = valid_acc.eval(feed_dict={X: X_test, y: y_test})
            file_writer.add_summary(train_acc_str, epoch)
            file_writer.add_summary(valid_acc_str, epoch)
            save_path = saver.save(sess, './log/190401/model_ckpt_{}.ckpt'.format(epoch))

file_writer.close()

訓練ではバッチサイズを500として30エポック訓練します。訓練では、イテレータを作っておいてバッチサイズずつデータを取り出して訓練します。イテレータで次のデータを取り出せなくなったらtf.error.OutOfRangeErrorを送出するので、それを受け取ってwhileループを抜けます。エポック数が5の倍数のときに訓練データとテストデータでの精度とモデルの重みパラメータを保存します。

訓練結果をTensorboardで見てみる

訓練したら、その結果をTensorboardで見てみます。シェルから

tensorboard --logdir=./log/190401/

と入力すると、アドレスが出てくるので、そこにアクセスすると、下のような画面が出てきます。

f:id:pyhaya:20190410212610p:plain

左側が訓練データの精度で右側がテストデータの精度です。これを見ると訓練データでは精度がほとんど1になっているのに対してテストデータでは精度が0.68にしかなっておらず、過学習していることがわかります。これを解消するには、モデルのパラメータを減らす方法やドロップアウトなどの正則化をかける方法、そして画像の水増しなどの方法があります。これらの方法についてはまた別の場所で書きます。

scikit-learnとTensorFlowによる実践機械学習

scikit-learnとTensorFlowによる実践機械学習