MATHGRAM

主に数学とプログラミング、時々趣味について。

chainerでYahoo!の占有率推定を実装してみた

こんにちは. 今回は僕も参加したYahoo! JAPAN データ&サイエンス カンファレンス 2016で取り上げられていた占有率推定を実装してみました.

前書き

Yahoo! Japanは働き方改革のロールモデルとして, 頻繁にメディアに取り上げられていますね. 特に週休三日制度のインパクトは大きく, 各所で話題になっていることだろうと思います. 業務効率を落とさずにこの制度を現実なものとするためには単純作業をAIに行わせるということが不可欠となって来ます.

そんな中, つい先日こんな記事を見かけました. itpro.nikkeibp.co.jp

この技術は僕がカンファレンスに参加した時に一番興味を持った(画像系を主としている僕にはちゃんとわかるものがこれくらいしかなかった)ものでした. しかしその時はトリミングで使われるなどの説明は恐らくしていなかったように思えます. 僕の記憶が正しければ, 主に検索の最適化で使われると説明していました. 何はともわれ, この記事のおかげでカンファレンスのことやこの論文を実装しようとしていたことなどを思い出したので, 今回実装してみた次第です.

論文の概要

実装した論文はこちらにあります.

論 文 - Yahoo! JAPAN研究所 - ヤフー株式会社

モチベーション

  • 画像中に特定の物体が占める割合(占有率) を高速に求めたい.

既存手法との差異, 本手法の利点

  • 既存手法ではsegmentationを行なった後に占有率を算出していた.
  • 本手法ではsegmentationをせず, 占有率を直接算出できる.
  • 既存のImageNetの学習済みパラメータを利用できる.

アルゴリズム

  • 出力層をクラス数のunitからなる全結合にする.
  • 出力層の活性化関数をsoftmaxにし出力を [0, \,1]とする.
  • 占有率を以下のように定め教師データにすることで回帰問題に落とし込める.
 \displaystyle
t_n( \boldsymbol{x}) =  \frac{\sum_p \delta_{n, \,j_p}}{ \sum_i \sum_p \delta_{i, \,j_p}}

ここで,  n n番目の画像データ,  pピクセル,  iはクラス,  \deltaクロネッカーのデルタ,  j_pピクセル pのラベルである.

実験

環境

  • chainer 1.20.1
  • AWS: Bitfusion Boost Ubuntu 14 Torch 7

方法

  • 以前実装したFCNによるSegmentationと速度比較する. FCNについてはこちら.

www.mathgram.xyz

  • モデルはVGG16を使用する.
  • データセットPASCALVOC 2012のSegmentationClassを用いる.
  • 画像の大きさは224*224に固定. (全結合を用いるので固定せざるを得ない)
  • FCNは学習済みの重みを使用していないので今回も使っていない.
  • 論文にはLossに関する記述がないが回帰問題なのでMSEを用いた.
  • 最適化手法はAdamで学習率は 1.0*10^{-5}.

コードの説明

部分的にコードの説明をしておきます.

  • 占有率について
def calc_occupancy(x, n_class=21):
    v = xp.empty((n_class))
    h, w = x.shape

    for i in range(n_class):
        v[i] = np.sum(x == i)
    v /= h*w
    if v.sum != 1.0:
        diff = 1.0 - v.sum()
        v[0] += diff
    return v

教師データにはピクセルごとにクラスが記録されているので, それぞれのクラスごとに和をとり, 画像の大きさで割るだけです. また境界値は背景と同じクラス0でマスクしました.

しかし教師データによっては合計が1にならなかったりしたので, その差分は全て背景クラスに加算しています. 恐らく境界データの部分がうまくマスクできていないと思われます. 以下の部分がちょうどその演算にあたる部分です.

    if v.sum != 1.0:
        diff = 1.0 - v.sum()
        v[0] += diff
  • VGGモデル

PASCAL VOCのSegmentationClassは背景を含めて21のクラスで構成されていまので出力層のfull connectを21unitにしました.

class VGG(chainer.Chain):
    def __init__(self, n_class=21):
        super(VGG, self).__init__(
            conv1_1=L.Convolution2D(3, 64, 3, stride=1, pad=1),
            conv1_2=L.Convolution2D(64, 64, 3, stride=1, pad=1),

            conv2_1=L.Convolution2D(64, 128, 3, stride=1, pad=1),
            conv2_2=L.Convolution2D(128, 128, 3, stride=1, pad=1),

            conv3_1=L.Convolution2D(128, 256, 3, stride=1, pad=1),
            conv3_2=L.Convolution2D(256, 256, 3, stride=1, pad=1),
            conv3_3=L.Convolution2D(256, 256, 3, stride=1, pad=1),

            conv4_1=L.Convolution2D(256, 512, 3, stride=1, pad=1),
            conv4_2=L.Convolution2D(512, 512, 3, stride=1, pad=1),
            conv4_3=L.Convolution2D(512, 512, 3, stride=1, pad=1),

            conv5_1=L.Convolution2D(512, 512, 3, stride=1, pad=1),
            conv5_2=L.Convolution2D(512, 512, 3, stride=1, pad=1),
            conv5_3=L.Convolution2D(512, 512, 3, stride=1, pad=1),

            fc6=L.Linear(25088, 4096),
            fc7=L.Linear(4096, 4096),
            fc8=L.Linear(4096, n_class)
        )
        self.train = False

以上のようにほとんどVGGと変化はありません.

結果

申し訳ないのですが, サーバー側で色々実験をするのが面倒だった(画像を移動させる等)ので予測の部分はCPUの演算のみになっています. 速度比較という点では問題はないと思うので許してください.

環境は以下です.

また一番注意しなければならない点なのですが, FCNも含めて学習が不十分なため"精度“の点にはあまり注目しないで欲しいです. (論文では精度も本手法の方が良くなったという記述がある.) とにかく速度に注目して比較してみよう.

1枚目

まずはこの画像から.
占有率は以下.

label: occupancy
background: 38.18%
person: 61.82%

邪魔なのでほぼ0%のクラスは出力してません. 足しても100%にならないかもです.

FCNによる推定

label: occupancy
background: 40.68%
person: 59.22%

time: 1.88s

本手法による推定

background: 27.52%
cat: 1.85%
chair: 1.24%
person: 65.3%

time: 1.19s

2枚目

次はこの画像.
占有率は以下.

label: occupancy
background: 73.42%
bus: 26.58%

FCNによる推定

label: occupancy
background: 80.55%
bus: 19.45%

time: 1.81s

本手法による推定

label: occupancy
background: 37.96%
aeroplane: 4.65%
boat: 7.58%
bottle: 1.12%
bus: 24.83%
car: 6.87%
diningtable: 1.32%
motorbike: 4.79%
person: 1.23%
sofa: 2.14%
train: 1.72%
tv/monitor: 1.08%

time: 1.29s

ちょっとノイズが多いですね. でもバスはちゃんと24%です.

3枚目


占有率は以下.

label: occupancy
background: 87.16%
dog: 12.84%

FCNによる推定

label: occupancy
background: 90.6%
bird: 2.05%
dog: 5.58%
person: 1.32%

time: 1.89s

本手法による推定

background: 87.83%
aeroplane: 3.45%
dog: 8.41%

time: 1.30s

ラスト


占有率は以下.

label: occupancy
background: 20.0%
car: 80.0%

ぴったり?まじかw

FCNによる推定

label: occupancy
background: 25.92%
aeroplane: 2.89%
car: 70.61%

time: 1.99s

本手法による推定

label: occupancy
background: 61.32%
aeroplane: 3.54%
bird: 1.95%
boat: 12.49%
bus: 4.01%
car: 1.4%
motorbike: 2.11%
person: 4.12%
train: 2.68%

time: 1.26s

うーん, これは全然ダメだ.

考察

まず間違いなく学習が足りてませんね. もっと厳密に実験したいのですが, 今月のAWSの請求2万くらい行きそうなんでもう無理っすw

速度だけで見ると本手法の圧勝です. 精度に関しても学習が進めばしっかりと占有率を出してくれそうですね.

一番個人的に気になっているのが, トリミングの実用的にはどうしているのかって部分です. このモデルはFCNと違い, 全結合を用いているので任意の大きさに対応できません. つまり固定の大きさの画像しか占有率を計算できないということになります.

今のところ, 僕が運用として思いつくのは, 様々な比に対応したモデルを複数用意するくらいです. ここはYahoo!ニュースなどがどのようなルールでトリミングを行なっているかによりますが, トリミングの比がある程度定まっているのであれば, そこまで大変なことではないと思います. AWSを借りてるような僕にはできませんがw

まぁとりあえず僕的には満足の行く実験&結果でした.

既存モデルのたった1層に工夫をするだけで, 会社に大きな貢献をすることができる.

とっても夢のある話じゃないでしょうか.

まとめ

  • CNNによる占有率推定を実装しました
  • 速度向上は測れたが画像によっては精度が悪い
  • 学習が不十分なのがとても心残り

一応レポジトリあげときますが, 出力の部分とか手元でざっとスクリプト書いたのでここに含まれてないです. まぁほんの参考程度に.

GitHub - k3nt0w/occupancy_estimate

以上です.

chainerに復帰したくてFCN実装した

もともと, chainerユーザーだった僕ですが, 5月くらいにKerasを使い出してからchainerにはあまり触れてきませんでした. しかし先日の分散処理の発表なども含め, バージョンアップが早く, 常に速度の向上を図っているchainerにはもう1度触れておきたいと思い, 今回ハンドリングとしてFCNを実装した次第です. (ちょうどFCNを使ってやりたいこともあったしね.)

レポジトリはここに置いときます.

GitHub - k3nt0w/chainer_fcn: Implementation FCN via chainer

あと学習済みパラメータもここに置いときます. chainerってついてる方です. お間違いのないように.

ぱらめーたー - Google ドライブ

FCNは以前Kerasでも実装していて, FCNそのものの説明もこちらに詳しくしているので, もし"FCNってなんじゃい“って方がおりましたら, こちらを参照してください.

www.mathgram.xyz

いやーしかし・・・, chainer使いやすすぎだろ!! 誰だよ,
あれ...?chainerよりkerasの方が書きやすくね? - MATHGRAM
なんて言ったの!死んで詫びろ!
昔使っていた貯金はありますが, 今回の実装には3時間もかかってないと思います. しかもほとんどは画像の出力方法の部分. ただ僕が使っていた時には無かったtrainerの部分は全く触れずに実装しちゃいました. 必要性を感じたら勉強しよう.

keras2も気になりますが, chainerもいっぱい使っていこうと思います.

目次

環境

実験方法

実験にはPASCAL VOC2012のSegmentationClassを使いました. クラス数は背景を含めて21. trainデータは, VOCのtrain&valを全部使っちゃってて約2500枚, epochは100, batchsizeは5です.

境界線には-1を当てて計算から除外しました. 任意のサイズの画像をinputできますが, 速度向上のため256に統一しています. 予測は任意のサイズいけます.

また今回もこの論文からそのまま実装しました. 厳密にはFCN-8sの実装となっています.

実験結果

lossはまだまだ下がりきってないけど, AWSの請求が半端なくなりそうなので, とりあえずここで区切る.

あとtrainデータにのみ予測を行なっています. お叱りを受けそうですが, 今回の目的はFCNの精度を上げることではないので許してください. むしろいいGPUマシン持っている方, fine-tuningしてくれたらくそ喜びます.

人間

f:id:ket-30:20170218120353p:plain

f:id:ket-30:20170218115535p:plain

バス

f:id:ket-30:20170218115050p:plain

クルマ

f:id:ket-30:20170218120620p:plain

Kerasの時もそうだったけど, 犬って難しいのかな? f:id:ket-30:20170218120815p:plain

おまけ

VGG16のpretrain modelを使う

今回はなぜか1から学習させてしまいましたが, FCN−8sはほとんどVGGと同じ構成なので学習済みのパラメータを使うことができます. モデルの定義をVGGとその出力を受け取る部分に分ければいいだけですね.

インデックスカラーについて

前回の実装では, PASCALVOCの画像をRGBに変換し, 色ごとに自分でクラスを分けるというクッソだるいことしていましたが, なんとインデックスカラーなるものがこの世には存在するようです.

インデックスカラー - Wikipedia

VOCのSegmentationClassに存在する画像はこのインデックスカラーによって着色されたもので, pngで画像を読み込めばそのままクラスを表すarrayになってくれるんです.

画像で説明するとこんな感じ. f:id:ket-30:20170218124136p:plain

当たり前な人にとっては当たり前なのかもしれませんが, 多分1人で勉強してるとなかなか分からなかったりすると思います. この情報だけで実装にかかる時間は何倍も短縮できますからね. この情報が誰かの役に立つことを望みます.

アルファブレンドについて

今回の出力は元画像にsegmentationした画像を重ねているのですが, こういうのをアルファブレンドって言ったりするそうです.

アルファブレンド - Wikipedia

こういう画像そのものの学習を自分は怠っていると感じました, pngとかjpgとかちゃんとわかってないしなぁ. もっと基本的なところから勉強して, いずれ記事にまとめたいところ.

PILでアルファブレンドする方法がわからなかったので, そこだけopencv使っています.

o = cv2.imread("out/original.jpg", 1)
p = cv2.imread("out/pred.png", 1)

# 0.6, 0.4 は比です.
pred = cv2.addWeighted(o, 0.6, p, 0.4, 0.0)

参考

今回もたくさんのブログ, レポジトリを参考にさせていただきました.
ありがとうございました.

chainerそのものとFCNの参考
https://github.com/yusuketomoto/chainer-fast-neuralstyle https://github.com/pfnet/PaintsChainer
memo: Fully Convolutional Networks 〜 Chainerによる実装 〜

PILでは透化の合成ができなかったけど, このへん試しました.
Python + Pillow(PIL)で、透過したpng画像を作成する - Symfoware 画像処理についてあれこれ: Python Imaging Libraryを使用して画像を重ね合わせる

結果的にはこちらの方法です. ありがとうございました.
Python3 OpenCV3で平均値画像(Alpha Blending)を作成 | from umentu import stupid

[Keras] backend functionを使いこなそう!

みなさん, keraってますか.

今回はloss関数やlayerの実装に欠かせない, backend functionをまとめていきます.

ただし自分が主に使ってる関数のみ紹介するので, 絶対Document読む方がいいですよ. これ以外にも色々ありますからね.

こいつを使いこなして, どんどんオリジナルのlayerなどを実装していきましょう!

インポート

まずバックエンド関数をインポートしましょう.

from keras import backend as K

# arrayを使うので, numpyもimportします.
import numpy as np

今回はこれだけしか使いません.
Kを使って自由にテンソルを扱っていきましょう!

numpy.arrayからテンソルを作る: K.variable

そもそもテンソルを用意しないと演算もくそもありません.

A = np.asarray([
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ])

B = np.asarray([
        [9, 8, 7],
        [6, 5, 4],
        [3, 2, 1]
    ])

こいつらをバックエンドで扱えるテンソルに変えるにはK.variableを使います.

A = K.variable(A)
B = K.variable(B)

これで準備オッケー. 今回Aくん, Bくんにはかなり働いてもらうよ.

テンソルの中身を見る: K.get_value, K.eval

テンソルの中身を確認したい. そんな時はK.get_value.

K.get_value(A)

出力

array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.],
       [ 7.,  8.,  9.]], dtype=float32)

K.evalも同じです.

和: +

要素ごとの足し算. 要素の合計ではないです. 気をつけて.
これは + 演算子オーバーロードされています.

K.get_value(A + B)

出力

array([[ 10.,  10.,  10.],
       [ 10.,  10.,  10.],
       [ 10.,  10.,  10.]], dtype=float32)

intやfloatも足せます.

K.get_value(A + 1)

出力

array([[  2.,   3.,   4.],
       [  5.,   6.,   7.],
       [  8.,   9.,  10.]], dtype=float32)

差: -

引き算も同じ, もちろんint, floatいけます.

K.get_value(A - B)

出力

array([[-8., -6., -4.],
       [-2.,  0.,  2.],
       [ 4.,  6.,  8.]], dtype=float32)

積: *

要素ごとに掛け算, 行列の積ではないので注意. int, floatもいけますよ.

K.get_value(A * B)

出力

array([[  9.,  16.,  21.],
       [ 24.,  25.,  24.],
       [ 21.,  16.,   9.]], dtype=float32)

商: /

要素ごとに割り算. int, floatもいけます.

K.get_value(A / B)

出力

array([[ 0.11111111,  0.25      ,  0.42857143],
       [ 0.66666669,  1.        ,  1.5       ],
       [ 2.33333325,  4.        ,  9.        ]], dtype=float32)

合計: K.sum

こっちが要素の合計.

K.get_value(K.sum(A))

出力

45.0

K.sumはテンソルを返すので, 合計をlossに足したい!なんて時はlossもテンソルとして扱うためK.sumだけで問題ないです.

※ intを足しても問題ないんですけどね

結構大事なのが, 軸を指定できる点.

画像を扱っていると, RGBのチャネルごとに何らかの操作をしたいときがありますしね.

行ごとの和

K.get_value(K.sum(A, axis=0))
array([ 12.,  15.,  18.], dtype=float32)

列ごとの和

K.get_value(K.sum(A, axis=1))
array([  6.,  15.,  24.], dtype=float32)

平均: K.mean

要素の平均. lossの実装でめっちゃ使う. まじでめっちゃ使う.

K.get_value(K.mean(A))

出力

5.0

こっちもテンソルを返す. またmeanもsum同様にaxisを設定できます.

行列積: K.dot

こっちが行列積

K.get_value(K.dot(A, B))

出力

array([[  30.,   24.,   18.],
       [  84.,   69.,   54.],
       [ 138.,  114.,   90.]], dtype=float32)

型の取得: K.int_shape

これは画像のshapeとって割る時とかに結構使う. ちなみにtensorflowでのみ動作します.
(つか悪いことは言わないからバックエンドはtensorfowを使え)

K.int_shape(A)

出力

(3, 3)

tuppleで返ってくるので注意.

要素の数を数える: K.count_params

明示的にint_shapeで受け取らなくても, 単に要素数を取ってくることもできる.

K.count_params(A)

出力

9

次元数の取得: K.ndim

次元の間違いがないよう, assertでエラー吐かせるために使ったりする.

K.ndim(A)

出力

2

np.zeros的なやつ: K.zeros

正直使ったことはないw 同様にK.onesK.eye(単位行列)もあるよ.

K.zeros((3,3))

出力

array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)

転置行列: K.transpose

K.transpose(A)

出力

array([[ 1.,  4.,  7.],
       [ 2.,  5.,  8.],
       [ 3.,  6.,  9.]], dtype=float32)

テンソルの欲しいところだけ集める: K.gataher

ここだけ欲しいって時に使う. 列指定はできないんじゃないかな. ちょっと確かなことわかってません.

K.get_value(K.gather(A, [0,2]))

出力

array([[ 1.,  2.,  3.],
       [ 7.,  8.,  9.]], dtype=float32)

reshape: K.reshape

np.reshapeはほんとよく使いますよね. Kでも一緒です.

K.get_value(K.reshape(A, (1,9)))

出力

array([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.]], dtype=float32)

要素のクリップ: K.clip

画像を扱う時はクリップも結構使うんじゃないでしょうか.
全ての値を設定した最小値と最大値以内に書き換えます.

K.get_value(K.clip(A, 2, 5))

出力

array([[ 2.,  2.,  3.],
       [ 4.,  5.,  5.],
       [ 5.,  5.,  5.]], dtype=float32)

要素を逆順に並べる: K.reverse

(追記: 2017/2/14)

K.reverse(x, axes=-1)

(batch, h, w, ch)のテンソルのRGBをBGRに変える時に使いました.

テンソルのslice

バックエンドとは関係ないですが, sliceは重要なので紹介しときます.

2行目のみ抽出する.

K.get_value(A[:,1])

出力

array([ 2.,  5.,  8.], dtype=float32)

(1,1)を指定する.

K.get_value(A[1,1])

出力

5.0

このように, 特に難しい操作は入らずにテンソルのsliceは可能です.

ほかの基本演算たち

sin, cos, exp, abs, logとか, まぁ有名な演算はもちろん全部あります. めんどいんで, 書きませんがこいつらは全部要素ごとです.

まとめ

今回はバックエンドの演算を中身を確認しながらまとめてみました.

lossの実装で必要なのは, 多分sumとmeanが主に使う関数じゃないでしょうか. あとはint_shapeとか結構使うかな.

これさえ扱えれば, Kerasの実装の幅は跳ね上がるので, Document見ながらbackendの使い方に慣れていきましょう. 僕も勉強中です!

以上です.