StatsFragments

Python, R, Rust, 統計, 機械学習とか

Chainer で Deep Learning: model zoo で R-CNN やりたい

ニューラルネットワークを使ったオブジェクト検出の手法に R-CNN (Regions with CNN) というものがある。簡単にいうと、R-CNN は以下のような処理を行う。

  1. 入力画像中からオブジェクトらしい領域を検出し切り出す。
  2. 各領域を CNN (畳み込みニューラルネットワーク) にかける。
  3. 2での特徴量を用いて オブジェクトかどうかをSVMで判別する。

R-CNN については 論文著者の方が Caffe (Matlab) での実装 (やその改良版) を公開している。

github.com

が、自分は Matlab のライセンスを持っていないので Python でやりたい。Python でやるなら 今 流行りの Chainer を使ってみたい。その試行の記録。

準備

とりあえず論文の再現は目指さず、R-CNN っぽい処理 (オブジェクト検出 -> CNN) を Python から回せるようにしたい。

まずは オブジェクト検出について調べてみると、上述の論文 & 実装で利用されている Selective Search という手法は ( Python の wrapper はあるが) Matlab なしでは利用できないようだ。Python から使える代替手法として GOP (Geodesic Object Proposals) という方法を見つけた。著者がパッケージも作っているのだが、どうもオブジェクト検出の方法がよくわからない、、、が矩形を切り取ることはできそうだ。

ということで今回は以下の処理が流せるようにしたい。モデルの修正やファインチューニングはせず、 caffenet をそのまま使う(済まぬ、、)。環境は EC2 の GPU インスタンス上に作成する。

  1. オブジェクトっぽい矩形をクロップ (Geodesic K-means)
  2. クロップした領域を CNN で判別 ( Chainer model zoo で caffenet を利用)
  3. 判別した結果を描画

都合上、 2 -> 1 -> 3 の順で記載する。

Chainer model zoo の利用

本日時点で PyPI にリリースされている v1.0.1 標準にはない機能のため、GitHub からダウンロードする。自分が利用したのは 本日時点 5222fe572b のリビジョン。

model zoo のダウンロード

chainer/examples/modelzoo から以下を実行してモデル と mean ファイルをダウンロードする。

$ python download_model.py caffenet
$ python download_mean_file.py

また、データセットの ID / ラベルを含む synset_words.txt を別途ダウンロードしておく。これは以下のファイルに含まれている。

$ wget http://dl.caffe.berkeleyvision.org/caffe_ilsvrc12.tar.gz
$ mkdir ilsvrc
$ tar zxvf caffe_ilsvrc12.tar.gz -C ilsvrc

model zoo の読み込み

以降は IPython Notebook で。最初に必要なパッケージをロードする。

%matplotlib inline
import os
import sys

import numpy as np

import chainer
from chainer import cuda
import chainer.functions as F
from chainer.functions import caffe

import matplotlib.pyplot as plt

次に、先ほどダウンロードした caffenet のパスを指定し、Chainer のモデルとして読み込む。 また、synset_words.txtnp.array として読み込む。

base = os.path.join('chainer', 'examples', 'modelzoo')
model = os.path.join(base, 'bvlc_reference_caffenet.caffemodel')
func = caffe.CaffeFunction(model)
cuda.init(0)
func.to_gpu()

synset_words = np.array([line[:-1] for line in open(os.path.join(base, 'ilsvrc', 'synset_words.txt'), 'r')])
synset_words[:10]
# array(['n01440764 tench, Tinca tinca',
#        'n01443537 goldfish, Carassius auratus',
#        'n01484850 great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias',
#        'n01491361 tiger shark, Galeocerdo cuvieri',
#        'n01494475 hammerhead, hammerhead shark',
#        'n01496331 electric ray, crampfish, numbfish, torpedo',
#        'n01498041 stingray', 'n01514668 cock', 'n01514859 hen',
#        'n01518878 ostrich, Struthio camelus'], 
#       dtype='|S131')

続けて、画像に対する caffenet の判別結果を返す関数を準備する。画像のリストを入力とし、一度に入力された画像群に対してはバッチで処理するようにした。

in_size = 224
cropwidth = 256 - in_size
start = cropwidth // 2
stop = start + in_size

# caffenet
meannpy = os.path.join(base, 'ilsvrc_2012_mean.npy')
mean_image = np.load(meannpy)
mean_image = mean_image[:, start:stop, start:stop].copy()

def predict(images):
    """画像のリストに対する判別を行う"""
    global mean_image, in_size, cropwidth, start, stop
    
    def swap(x):
        x = np.array(x)[:, :, ::-1]
        x = np.swapaxes(x, 0, 2)
        x = np.swapaxes(x, 1, 2)
        return x
    
    if not isinstance(images, list):
        images = [images]
    batch_size = len(images)
    x_data = np.ndarray((batch_size, 3, in_size, in_size), dtype=np.float32)
    
    for i, image in enumerate(images):
        image = swap(image)
        image = image[:, start:stop, start:stop].copy().astype(np.float32)
        image -= mean_image

        x_data[i] = image
        
    x_data = cuda.to_gpu(x_data)
    x = chainer.Variable(x_data, volatile=True)
    
    y, = func(inputs={'data': x}, outputs=['fc8'], train=False)
    y.data = cuda.to_cpu(y.data)
    indexer = y.data.argmax(axis=1)
    return synset_words[indexer], y.data.max(axis=1)

動作確認

自分は ImageNet へのアクセス権を持っていないため、wikimedia で適当な画像をみつくろって試す。そのため、URL を指定して画像をダウンロードする関数、画像を caffenet へ入力するためにリサイズする関数を作成する。

def get_image(url):
    """URLから画像をダウンロード"""
    import urllib
    import StringIO
    import PIL.Image as Image
    return Image.open(StringIO.StringIO(urllib.urlopen(url).read())).convert("RGB")

def resize(image):
    """画像をリサイズ"""
    import PIL.Image as Image
    return image.resize((256, 256), Image.ANTIALIAS)

いくつか結果を添付する。

url1 = "https://upload.wikimedia.org/wikipedia/commons/0/0e/BIRD_PARK_8_0189.jpg"
img = get_image(url1)
plt.imshow(img)

f:id:sinhrks:20150705190953p:plain

img = resize(img)
plt.imshow(img)

f:id:sinhrks:20150705191003p:plain

predict(img)
# (array(['n01806143 peacock'], 
#        dtype='|S131'), array([ 26.1235218], dtype=float32))

別の画像。

url2 = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f4/Ostrich_Ngorongoro_05.jpg/640px-Ostrich_Ngorongoro_05.jpg"
img = get_image(url2)
plt.imshow(img)

f:id:sinhrks:20150705191014p:plain

img = resize(img)
plt.imshow(img)

f:id:sinhrks:20150705191022p:plain

predict(img)
# (array(['n01518878 ostrich, Struthio camelus'], 
#        dtype='|S131'), array([ 22.44941521], dtype=float32))

定量的に測った訳ではないが、魚類は正解しにくい気がする。

url3 = "https://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Peixe010eue.jpg/640px-Peixe010eue.jpg"
img = get_image(url3)
plt.imshow(img)

f:id:sinhrks:20150705195422p:plain

img = resize(img)
plt.imshow(img)

f:id:sinhrks:20150705195440p:plain

predict(img)
# (array(['n01443537 goldfish, Carassius auratus'], 
#        dtype='|S131'), array([ 24.29219818], dtype=float32))

複数画像を処理する場合は、画像のリストを渡す。

images = [resize(get_image(url1)), resize(get_image(url2)), resize(get_image(url3))]
labels, scores = predict(images)
labels
# array(['n01806143 peacock', 'n01518878 ostrich, Struthio camelus',
#        'n01443537 goldfish, Carassius auratus'], 
#       dtype='|S131')
scores
# array([ 26.1235218 ,  22.44941521,  24.29219818], dtype=float32)

矩形のクロップ (Geodesic K-means)

Geodesic Object Proposals 著者のサイトから Code, Data をダウンロードする。README が少しわかりにくいが、以下の方法でインストールできた。

# eigen のダウンロード
$ wget http://bitbucket.org/eigen/eigen/get/3.2.5.zip
$ unzip 3.2.5.zip 

# GOP のダウンロード
$ wget http://googledrive.com/host/0B6qziMs8hVGieFg0UzE0WmZaOW8/code/gop_1.3.zip
$ unzip gop_1.3.zip 

# GOP の解凍ディレクトリ/external/eigen に Eigen ディレクトリをコピー
$ mkdir gop_1.3/external/eigen
$ cp  -r eigen-eigen-bdd17ee3b1b3/Eigen/ gop_1.3/external/eigen/
$ ls gop_1.3/external/eigen
# Eigen

$ cd gop_1.3/build
# -DUSE_PYTHON で Python 2.x とのリンクを指定
$ cmake .. -DCMAKE_BUILD_TYPE=Release -DUSE_PYTHON=2
$ sudo make install

また、ダウンロードした Data は gop_1.3/data ディレクトリの中に入れておく。

$ unzip gop_data.zip

GOP を IPython Notebook から使ってみる。GOP は site-packages にインストールされないため、ロードするには sys に対して path を追加してやる必要がある。矩形を選択する方法のうち、今のところ動かせたのは Geodesic K-means のみなのでそれを使う。GOP では Geodesic K-means の後にオブジェクト検出を行うようなのだが、その結果を利用して矩形選択する方法はまだわかってない (そのため、オブジェクト検出はまだできていないという理解だが Proposal の処理を追っていないので自信はない)。

sys.path.append('./gop_1.3/src/')

from gop import *
from util import *

prop = proposals.Proposal(setupLearned(200, 4, 0.8))

detector = contour.MultiScaleStructuredForest()
detector.load("./gop_1.3/data/sf.dat")

def get_boundaries(image):
    # 画像データを直接入力とする方法が不明のため、一時ファイルに保存
    img.save('tmp.png')
    s = segmentation.geodesicKMeans(imgproc.imread('tmp.png'), detector, 100)
    b = prop.propose(s)
    boxes = s.maskToBox(b)
    return boxes

以下の画像を使って確認してみる。

url = "https://upload.wikimedia.org/wikipedia/commons/8/8f/Dogs_playing_on_the_beach_in_the_sand.jpg"
img = get_image(url)
plt.imshow(img)

f:id:sinhrks:20150705221048p:plain

fig, ax = plt.subplots(1, 1)
ax.imshow(img)
boxes = get_boundaries(img)

def plot_boxes(ax, boxes, labels=None):
    """ax への矩形とラベルの追加"""
    if labels is None:
        labels = [None] * len(boxes)
        
    history = []
    from matplotlib.patches import FancyBboxPatch    
    for box, label in zip(boxes, labels):
        coords = (box[0], box[1])
        b = FancyBboxPatch(coords, box[2]-box[0], box[3]-box[1],
                            boxstyle="square,pad=0.", ec="b", fc="none", lw=0.5)             
        mindist = 100000
        if len(history) > 0:
            mindist = min([sum((box - h) ** 2) for h in history])
        # ほぼ重なる矩形は描画しない
        if mindist > 30000:
            if label is not None:
                ax.text(coords[0], coords[1], label, color='b')
            ax.add_patch(b)
            history.append(box)

plot_boxes(ax, boxes)

f:id:sinhrks:20150705221100p:plain

クロップされた画像はこんな感じ。

cropped = img.crop(boxes[13])
plt.imshow(cropped)

f:id:sinhrks:20150705221115p:plain

リサイズして caffenet にかける。犬という意味では近いが、品種は当たってない。

predict(resize(cropped))
# (array(['n02105412 kelpie'], 
#        dtype='|S131'), array([ 10.38668251], dtype=float32))

各領域への CNN の適用

Geodesic K-means で抽出された各領域をクロップした画像のリストを作り、caffenet に渡す。

images = [resize(img.crop(box)) for box in boxes]

labels = []
scores = []
nunit = len(images) / 5
unit = len(images) / nunit
for i in range(nunit+1):
    l, s = predict(images[i*unit:min(i*unit+unit, len(images))])
    labels.extend(l.tolist())
    scores.extend(s.tolist())

import pandas as pd
df = pd.DataFrame({'Labels': labels, 'Scores': scores})
df = df.sort('Scores', ascending=False)
df.head()

f:id:sinhrks:20150705221028p:plain

判別結果のうちスコアがよい部分をラベル付きで描画する。やはりうまくオブジェクトが切り出せていない感じがする。

fig, ax = plt.subplots(1, 1)
ax.imshow(img)

heads = df.head(n=50)
labels = [l.split(' ', 1)[1] for l in heads['Labels'].tolist()]
plot_boxes(ax, boxes[heads.index], labels=labels)

f:id:sinhrks:20150705221127p:plain

まとめ

今回、Python から以下の処理をまわすことはできた。

  1. オブジェクトっぽい矩形をクロップ
  2. クロップした領域を CNN で判別
  3. 判別した結果を描画

が、判別結果自体は微妙な感じなので、以下2つについてはもう少し調べたい。

  • Geodesic Object Proposals でのオブジェクト検出と矩形の選択。
  • PASCAL VOC データ + SVM を使ってのファインチューニング。

同日追記

パラメータ調整したら矩形抽出は多少ましになったような、、(画像末端まで選択されてしまうことが減っている)。元論文では候補を 2000 個抽出しているため、同程度以上にしておけばよさそう。

f:id:sinhrks:20150705235744p:plain

深層学習 (機械学習プロフェッショナルシリーズ)

深層学習 (機械学習プロフェッショナルシリーズ)