Chainerのimagenetサンプルで学習データの拡張/whitening

# Chainer imagenet example

機械学習で多クラス画像分類を行いたいと思った時に、Chainerのサンプルコード https://github.com/pfnet/chainer/tree/master/examples/imagenet は非常によくできていて、ほぼ手をつけずに応用が効いて便利です。
こちらの投稿「[PFN発のディープラーニングフレームワークchainerで画像分類をするよ(chainerでニューラルネット1) - 人工言語処理入門](http://hi-king.hatenablog.com/entry/2015/06/11/021144)」に従ってデータを用意・処理すれば、大体思うように動いてくれます(データセットが適切である必要はもちろんありますが)。

このサンプルでは、下処理として学習画像の平均値を用いたデータの正規化を行っています。また、データ拡張として、ランダムな領域のクリッピングと左右反転を行っています。

## TensowFlowのCIFAR-10サンプルコード

一方、TensowFlowのCIFAR-10サンプルコード　https://github.com/tensorflow/tensorflow/tree/master/tensorflow/models/image/cifar10 は、入力データがCIFAR-10にほぼに特化しているので、直接応用させるのは難しいところがあります。
とはいえ、構造はシンプルでチュートリアルとしては非常によくできていると思います。コードを読み進めていくと、Chainerのサンプルよりもより幅広いデータ拡張をしてることがわかります。平均値を用いた正規化は行わず、ランダムクリッピングと反転に加え、以下の処理を行っています。

* 輝度(brightness)の変更
* コントラストの変更
* whitening(白色化)

これらをChainerのサンプルに適用させることを考えてみました。

# データ拡張の実装

実装はnumpyだけでやってみます。

## 輝度

輝度の変更は非常に単純です。ある範囲の乱数を各画素に単純に加算するだけです。

```py
def random_brightness(image, max_delta=63, seed=None):
    delta = np.random.uniform(-max_delta, max_delta)
    newimg = image + delta
    return newimg
```

## コントラスト

コントラストの変更は、各ピクセルのR面G面B面平均値mを求めて、各面の値をそれぞれ(x - m) * factor + mとします。

```py
def random_contrast(image, lower, upper, seed=None):
    f = np.random.uniform(lower, upper)
    mean = (image[0] + image[1] + image[2]).astype(np.float32) / 3
    ximg = xp.zeros(image.shape, xp.float32)
    for i in range(0, 3):
        ximg[i] = (image[i] - mean) * f + mean
    return ximg
```

Chainerでは画像をrgb * width * heightという形式で扱うので、画素ごとのRGB平均値を求めるのは簡単です。
TensorFlowの場合、ここの処理はC++で記述されています。画像をwidth * height * rgbの形式で扱うからではないかと予想しています。その分、直接pillowやOpenCVなどで読み込んだ結果を扱えるという利点があるのですけど。

## whitening

whiteningはTensorFlowの[per_image_whiteningの解説](https://www.tensorflow.org/versions/r0.7/api_docs/python/image.html#per_image_whitening)どおりに実装すればよいです。

```py
def image_whitening(img):
    img = img.astype(np.float32)
    d, w, h = img.shape
    num_pixels = d * w * h
    mean = img.mean()
    variance = np.mean(np.square(img)) - np.square(mean)
    stddev = np.sqrt(variance)
    min_stddev = 1.0 / np.sqrt(num_pixels)
    scale = stddev if stddev > min_stddev else min_stddev
    img -= mean
    img /= scale
    return img
```

whiteningに関しては、書籍「深層学習 (機械学習プロフェッショナルシリーズ)」(ISBN-13: 978-4061529021)の5-5に詳しい解説があります。

# 実コード

```py:distortion.py
#
import numpy as np
import random

def random_brightness(image, max_delta=63, seed=None):
    delta = np.random.uniform(-max_delta, max_delta)
    newimg = image + delta
    return newimg

def random_contrast(image, lower, upper, seed=None):
    f = np.random.uniform(-lower, upper)
    mean = (image[0] + image[1] + image[2]).astype(np.float32) / 3
    ximg = xp.zeros(image.shape, xp.float32)
    for i in range(0, 3):
        ximg[i] = (image[i] - mean) * f + mean
    return ximg

def image_whitening(img):
    img = img.astype(np.float32)
    d, w, h = img.shape
    num_pixels = d * w * h
    mean = img.mean()
    variance = np.mean(np.square(img)) - np.square(mean)
    stddev = np.sqrt(variance)
    min_stddev = 1.0 / np.sqrt(num_pixels)
    scale = stddev if stddev > min_stddev else min_stddev
    img -= mean
    img /= scale
    return img

insize = 227
cropwidth = 256 - insize

def read_dist_image(path, center=False, flip=False):
    image = np.asarray(Image.open(path)).transpose(2, 0, 1)
    if center:
        top = left = cropwidth / 2
    else:
        top = random.randint(0, cropwidth - 1)
        left = random.randint(0, cropwidth - 1)
    bottom = insize+top
    right = insize+left

    # clipping
    image = image[:, top:bottom, left:right].astype(np.float32)
    # left-right flipping
    if flip and random.randint(0, 1) == 0:
        image =  image[:, :, ::-1]
    # random brightness
    if random.randint(0, 1) == 0:
        image = random_brightness(image)
    # random contrast
    if random.randint(0, 1) == 0:
        image = random_contrast(image, lower=0.2, upper=1.8)
    # whitening
    image = image_whitening(image)
    image.flags.writeable = True
    return image

```

オリジナルのtrain_imagenet.pyを修正して、read_image内部で新たに作成したコードを呼びだすよう修正すれば完成です。

```diff
--- train_imagenet.py   2016-02-18 14:51:48.595572992 +0900
+++ train_imagenet.py.new       2016-02-18 14:53:09.827572982 +0900
@@ -30,6 +30,7 @@
 from chainer import optimizers
 from chainer import serializers

+import distorion

 parser = argparse.ArgumentParser(
     description='Learning convnet from ILSVRC2012 dataset')
@@ -126,6 +127,7 @@


 def read_image(path, center=False, flip=False):
+    return distorion.read_dist_image(path)
     # Data loading routine
     image = np.asarray(Image.open(path)).transpose(2, 0, 1)
     if center:
```

## 注意
numpyだけで実装したので、結構メモリを食います。メモリが少ない環境では、trian_imagenet.pyの引数に-j 5などを指定して、読み込み処理をするスレッド数を少なくしてみてください。

# 認識

自分は、Network-In-Network構造を使って処理をさせてみました。サンプルには認識用のメソッドが定義されていないので、以下のように追加しています。

```diff
--- nin.py     2016-02-18 15:01:43.211572921 +0900
+++ nin.py.new      2016-02-16 13:35:06.035594161 +0900
@@ -35,3 +35,12 @@
         self.loss = F.softmax_cross_entropy(h, t)
         self.accuracy = F.accuracy(h, t)
         return self.loss
+    def evaluate(self, x):
+        h = F.max_pooling_2d(F.relu(self.mlpconv1(x)), 3, stride=2)
+        h = F.max_pooling_2d(F.relu(self.mlpconv2(h)), 3, stride=2)
+        h = F.max_pooling_2d(F.relu(self.mlpconv3(h)), 3, stride=2)
+        h = self.mlpconv4(F.dropout(h, train=self.train))
+        h = F.reshape(F.average_pooling_2d(h, 6), (x.data.shape[0], 1000))
+
+        return F.softmax(h)
```

train_imagenet.pyをベースに認識用のコマンドを作成します。

```py:identify.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import argparse
import os
import random
import sys
import threading
import time

from PIL import Image
import six
import six.moves.cPickle as pickle

import chainer
from chainer import cuda
from chainer import optimizers
from chainer import serializers

parser = argparse.ArgumentParser(
    description='Chainter NIN model identifyer')
parser.add_argument('imgfile', help='Path to identify image-file')
parser.add_argument('--gpu', '-g', default=0, type=int,
                    help='GPU ID (negative value indicates CPU)')
parser.add_argument('--model', '-m', default='model',
                    help='Path to load serialized model')

args = parser.parse_args()
if args.gpu >= 0:
    cuda.check_cuda_available()
import numpy as np
xp = cuda.cupy if args.gpu >= 0 else np

size = 256

def load_single_image(file):
    image = Image.open(file)
    image = image.resize((size,size))
    image = image.convert("RGB")
    image = np.asarray(image).transpose(2, 0, 1)
    img = image.astype(np.float32)
    d, w, h = img.shape
    num_pixels = d * w * h
    mean = img.mean()
    variance = np.mean(np.square(img)) - np.square(mean)
    stddev = np.sqrt(variance)
    min_stddev = 1.0 / np.sqrt(num_pixels)
    scale = stddev if stddev > min_stddev else min_stddev
    img -= mean
    img /= scale
    return img

import nin
model = nin.NIN()

if args.gpu >= 0:
    cuda.get_device(args.gpu).use()
    model.to_gpu()
#
serializers.load_hdf5(args.model, model)
model.train = False

# image handling
img = load_single_image(args.imgfile)

x = chainer.Variable(xp.asarray([img]),volatile='on')
y = model.evaluate(x).data

print(y[0])
```

出力は1000クラスそれぞれの確率になります。適時加工して使ってください。

# おまけ: TensorFlowのCIFAR-10サンプルで任意の画像ファイルを読み込ませるには

TensorFlowのCIFAR-10サンプルも、FixedLengthRecordReaderを使う代わりにTextLineReader, decode_csv, read_file, decode_jpegを組み合わせれば任意のjpeg画像データを使って学習させることができます。
そのための記事「[TensorFlowのReaderクラスを使ってみる](http://qiita.com/knok/items/2dd15189cbca5f9890c5)」を以前書いたので参考にしてください。

## TensorFlow 0.7.0
この記事を書いていて気づいたのですが、昨日(2016/2/17)の段階でTensorFlow 0.7.0が出ていたようです。