のんびりしているエンジニアの日記

ソフトウェアなどのエンジニア的な何かを書きます。

Kaggleの画像コンペで便利なライブラリを紹介します。

皆さんこんにちは。
お久しぶりです。本日はPyTorchを用いて、Kaggleをする際に便利なソフトウェアを紹介します。
この記事はPyTorch Advent Calendar 15日目です。

qiita.com

私自身、画像コンペィションでは、豊富な実装からPyTorchを利用するケースが多いです。
PyTorchで今回使っているライブラリを2点紹介したいと思います。

Pretrained PyTorch

Pretraining modelを用いてFine turningをすることで、
初期から学習したモデルよりも精度が向上します。

PyTorchには公式で配布している以上に、
github上に様々なPreTrainingモデルを公開されています。

github.com

学習済モデルの利用

from pretrainedmodels.models.resnext import resnext101_32x4d
model = resnext101_32x4d(1000, pretrained="imagenet")
model(torch.randn(1, 3, 224, 224))

FineTurning

上記のままだと出力が1000層にしかなりません。
そのため、10クラスのみ分類したい場合だとモデルの加工が必要になります。
最後の層のインスタンス変数を直接書き換えることで、カスタマイズされたモデルの作成ができます。

from pretrainedmodels.models.resnext import resnext101_32x4d
model = resnext101_32x4d(1000, pretrained="imagenet")
model(torch.randn(1, 3, 224, 224))

model.last_linear = nn.Sequential(
    nn.Linear(2048, 1000),
    nn.ReLU(inplace=True),
    nn.Linear(1000, 10)
)

Albumentation

AlbumentationはOpenCVをバックエンドで利用した効率の良いライブラリです。
PyTorchだと非常に簡単に使える口はありますが、他のライブラリにも簡単に組み込めます。
高速なAugmentationの実装は学習速度に大きく影響します。


単純なAlbumentationの実装

次に単純なAlbumentationの実装を記載します。
HorizonalFlipのみを行うコードです。

import cv2
from albumentations import (
    HorizontalFlip
) # (1)
from urllib.request import urlopen

def download_image(url):
    data = urlopen(url).read()
    data = np.frombuffer(data, np.uint8)
    image = cv2.imdecode(data, cv2.IMREAD_COLOR)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    return image
image = download_image('https://d177hi9zlsijyy.cloudfront.net/wp-content/uploads/sites/2/2018/05/11202041/180511105900-atlas-boston-dynamics-robot-running-super-tease.jpg') # (2)
auged_img= aug(image=image)["image"] # (3)

import文で、Horizonal Flipを読み込みます。
(2)のdownload_image関数で画像をWeb上から読み込みます。そして、最後の(3)を実行して、変換後の画像を獲得できます。

Compose

前章では、Augmentationは1種類でした。
ただし、Kaggleでは、複数のAugmentationを実行したいケースがあります。
例えば、水平反転->回転->ノイズ適用のような場合です。

Albumentationには、これを実現するComposeと呼ばれるクラスがあります。
Composeは、複数のAugmentationを順番に実行します。

aug = Compose([ # (1)
    HorizontalFlip(0.5), # (2)
    OneOf([
        MotionBlur(p=.2),
        MedianBlur(blur_limit=3, p=.1),
        Blur(blur_limit=3, p=.1),
    ], p=0.2) # (3),
    ShiftScaleRotate(shift_limit=0.0625, scale_limit=0.2, rotate_limit=45, p=.2),# (4)
    OneOf([
        RandomContrast(),
        RandomBrightness(),
    ], p=0.3) # (5)
], p=0.5) 

(1)でComposeを宣言しています。これで0.5の確率で実行されます。
Augmentationの内部をリストを定義しています。

まず、(2)で、水平反転を0.5の確率で実行します。続いて、(3)でブラーを適用します。
ただし、ここでOneOfと呼ばれるどれか一つが適用されるクラスです。

これにより、モーションブラー、メディアンブラー、ブラーのどれかが適用されます。
そして、(4)で、平行移動、スケール、回転を適用します。
最後に(5)でコントラスト変換、輝度変換のどちらかを0.3の確率で適用します。

日本語で書くと長文の説明になりますが、コードで綺麗に記載できます。
後はDataset Classを拡張すれば、実装が可能になります。

特別、ライブラリ依存の実装にはなっていません。
そのため、各DLフレームワークのAugmentationの組み込み方法がわかれば、導入できます。

最後に

PyTorch自体に豊富なライブラリがあって使いやすいと感じています。
非常にコンペティションでは特に使いやすいので興味ある人はぜひ使ってみてください!

私がわかりにくいと思った「Faster RCNN」のポイントの解説

皆さんこんにちは
お元気ですか。ちゃっかりKaggleで物体検出のコンペもはじまりました。

Deep Learningは相変わらず日進月歩で凄まじい勢いで進化しています。
特に画像が顕著ですが、他でも色々と進歩が著しいです。

ところで色々感覚的にやりたいことが理解できるものがありますが、
あまり勉強していなかった分野として物体検出系のアルゴリズムがあります。
所謂、Faster RCNN, SSD, Yolo、最近、Mask R-CNNが該当します。

ただ、今回は、個人的に物体検出のアルゴリズム
なかなか調べても出てこない、あれここどうなってるんだっけ?と思った部分の解説をします。
そのため、案外変なところの解説かもしれません。
Faster RCNNの技術要素から説明しようかなとも思って作りました。

※数式、記号等の細かい説明は論文を参考にしてください。
3/7 15:15 Ancher→Anchorに修正しました

Faster RCNN

Faster RCNNは2015年に発表された論文です。
最近だと特許的な問題でも一時期盛り上がりを見せています。
Deep Learningを利用したEnd to Endで物体検出を
実施したニューラルネットワークです。

https://arxiv.org/pdf/1506.01497.pdf

※特許解説(日本語)
qiita.com

全体構成

Faster RCNNは特徴マップを抽出するConvolutional Layerと物体領域を抽出する
Region Proposal Networkに加え、分類、回帰の結果を出力するネットワークで構成されています。

f:id:tereka:20180303132456p:plain
※論文より引用

Conv Layerで得られた結果をRegion Proposal Networkを使い、領域を抽出し、分類しています。
特に重要なのはRegion Proposal Networkと記載されている部分で
ここが従来手法からのFaster RCNNの改善になる部分です。

Region Proposal Networkは次で説明します。

Region Proposal Network

概要・目的

Region Proposal Networkは画像内から物体領域の候補となる箇所を抽出します。
元々は「Fast RCNN」ではこの物体領域抽出にSelective Searchと呼ばれる手法を使っていたため、
物体候補領域の抽出に時間がかかっていました。
その箇所すらもEnd-to-Endで実施することで従来より高速・精度改善をしています。

全体的には次の処理があります。

  1. 物体の候補領域を見つけるAncherを作成する。
  2. (学習時)誤差を計算する。
  3. 候補の中からNMSなどを実施して、Region Proposal Networkでの物体の候補領域を抽出する。

その中で特に私が不明だった要素をいくつか紹介します。

Anchor

Anchorと呼ばれる領域を生成します。
一言で言えば、物体の領域を示している矩形です。
これらを画像内に一定間隔で生成します。これは後述のLoss Functionで利用されます。

f:id:tereka:20180303132658p:plain

Loss Function

RPN自身を更新するための誤差を計算し、
ニューラルネットワークの全体の誤差と併せて更新します。論文中の式は次の通り。

f:id:tereka:20180306220825p:plain:w300

RPNの誤差は基本的には2つの要素で成り立っています。
①その領域は物体か背景であるか、②どこに領域があるかの領域の位置(詳細)を計算します。
特に私が勉強するまでイメージを持てなかったのは「そもそも教師信号をどうやってこの式に当てはめるのか」です。そもそも与えてるのバウンディングボックスとクラスだけなのですが・・といった疑問です。

教師信号の決め方ですが、物体の有無はAncherと物体のIoU(Intersection over Union)が
0.7(論文中)以上の場合、物体が存在することを示します。(positive anchor)
また、IoUが0.3以下を物体が存在しないこと(negative anchor)を示します。

平たく言ってしまえば、ある程度重なっているAncherに物体が存在するラベルが振られ、
存在しないAncherに物体が存在しないラベルが振られます。
それ以外は学習に貢献しません。

Ancherは回帰を行います。
Ancherのデータの回帰ですが、次の通り計算を行って、スケールの調整を行っています。
(細かい記号は元論文を見てください)
この値からsmooth L1と呼ばれる誤差関数を用いて最適化します。

f:id:tereka:20180306220842p:plain:w300

Non-Maximum Suppression

後処理としてあるのがNon-Maximum Suppression(NMS)です。
これは同じ物体を示している矩形を抽出する手法です。
本手法を利用して、1つの矩形を残し、残りの出力を抑制します。

詳細は次のサイトが詳しいのでこちらに任せます。

meideru.com

これらによって得られた結果に対して、RoI Poolingを行い、分類、回帰を行うネットワークに接続します。

RoI Pooling

RoI Poolingは分類を行う層への入力を固定次元にする役割があります。
物体検出の領域は非常に可変長です。同じ画像から検出された椅子とりんごが同じサイズではないのと
同じようなものです。その得られた領域を次の分類のネットワークで処理するため、
固定次元に縮小する処理を行います。

処理の概要はこちらのサイトがわかりやすいです。

qiita.com

最後に

なんとなく自分が疑問に思った所を中心にまとめてみました。
ちょっとすっきりしました。間違っているところあれば、教えてください。

Jupyter Labのbetaが公開されました。操作性が向上していい感じ!

皆さんこんにちは。
お元気ですか。Advent Calendar執筆して以来、久々です。
今更ですが、あけましておめでとうございます。今年もよろしくお願いいたします。

いつものようにTwitterを見ていると
以前紹介したJupyter Labのbetaがリリースとのことで、次の記事が回ってきました。

blog.jupyter.org

せっかくなのでまた、お試しで使ってみました。

※Alpha版を試した記事はこちら
nonbiri-tereka.hatenablog.com

使ってみた

インストール

インストールは簡単にできます。
condaの場合

conda install -c conda-forge jupyterlab

pipの場合

pip install jupyterlab

起動

起動します。起動方法も簡単です。

jupyter lab

デフォルトの設定であれば、http://localhost:8888/labへアクセスします。
アクセスをすると若干のロードを待ちます。その後、次の画面になります。

f:id:tereka:20180220232302p:plain

過去のと比べてだいぶリッチに進化しており、非常に見やすい感じになっています!

少し触ってみた

早速触ってみました。次の画面は、csvの読み込みやターミナルを表示させた状態です。
alphaの頃はJupyter Notebookでできた操作ができなかったこともあり、正直不便だと思って
Jupyter Labを使っていませんでした。

しかし、操作性が向上し、よりユーザが使えるように進化したなぁといった印象です。
少し恒常的に利用してより便利な機能が使えないか探りたいと思います。

f:id:tereka:20180220233211p:plain

JSONも綺麗に表示ができます。実はVegaやVegaLiteなども対応しているようですが、利用したことはありません。

f:id:tereka:20180220233434p:plain

最後に

便利そうなので、より、使ってみます!

クリスマスにChainerを使って妄想力を可視化した。

皆さんこんにちは
お元気ですか。やっと書き切りました。

実は今年ももう少しです。
昨年度はカノジョを作成し、その前は友利奈緒を真面目に解析しています。

いやぁ時の流れは早いものです。
これを踏まえ、今年は何をしようか考えました。

Chainer Advent Calendar 25日目で、皆様の妄想力を具現化したいと思います。

qiita.com

妄想について

さて、考えてみて欲しい。妄想というものを
画像に不自然に暗い箇所、何か見にくいなぁと思う箇所。
ここはこんな感じになっていると推測しているのではないでしょうか。

そう、推測、いや、補完です。
さて、本題ですが、部分的にモザイクがかかった画像を見るとここにはこれがあると妄想するでしょう。
そんなわけで、本日、皆様の妄想を具現化する、つまり、モザイクを外すことを試みます。

モザイク処理外し

モザイク処理とは

モザイク処理は次のように定義されているようです。(Wikipedia

モザイク処理(モザイクしょり)(英語: mosaic processing)・ピクセル化 (英語: pixelization)とは写真・画像・静止画・映像・動画において表示したくない部分をピクセル単位で見えにくくする映像処理。(by Wikipedia

要は見えにくい部分を加工する処理です。モザイク処理自体は後で詳細を述べます。

モザイク処理の種類

モザイク処理は大別して2種類の方式があります。
モザイクは、非可逆圧縮可逆圧縮の手法です。

非可逆圧縮は元に戻すことができない圧縮方式であり、モザイク画像から元画像を完全に復元することができません。
それに対し、可逆圧縮は元に戻すことが可能な方式であり、現在は非可逆圧縮が主流となっているそうです。
可逆圧縮の代表例としてFLMASKと呼ばれるツールが昔あったらしい、同じ会社の人に教えてもらった。)

現在は、非可逆圧縮の方式が主流のため、本実験でも同様の圧縮方式をベースに実現します。
本手法で選択したモザイクの手法は次のとおりです。

  1. 黒いモザイク・・・画面の一部が黒いモザイクとなる。
  2. 中央値を用いたモザイク・・・ぼかしたい箇所をmedian filterにする。
  3. Gaussian Filterを使ったモザイク・・・ぼかしたい箇所にGaussian Filterを使う。

今回は実用性を重視するため、部分画像への適用も試みます。
これは、全体を隠さなければならないケースは少なく
部分的に見てはいけないものとして隠していることもあるからです。

乱数による埋込も画像に対し、完成品があまりに不自然なので利用しません。

モザイク処理アルゴリズムの実装

本処理はPythonで実装します。

ランダムで64枚出力した画像は次の通りです。
f:id:tereka:20171108212215p:plain

データセットの収集

本画像処理で用いるデータセットを収集します。
今回はラブライブのデータを利用してモザイク除去の実験を行います。

Googliser

GoogliserはGoogle画像検索を用いて、画像を取得するソフトウェアです。
このソフトウェアを利用してラブライブのデータを集めます。

github.com

googliserはcloneして内部にあるgoogliser.shを動作させれば実行可能です。

./googliser.sh -p "矢澤にこ" --number 1000

モザイク処理外しのアルゴリズム

モザイク処理外し

はじめの方で一般的なモザイクが非可逆圧縮であると説明しました。
そのため、厳密にはモザイク処理を外すのではなく、モザイクがかかった箇所を補完する処理を行います。

この補完処理ですが、生成モデルで利用されるGANとSemantic Segmentationの技術を応用して実現します。
SIGGRAPH 2017で発表のあった「Globally and Locally Consistent Image Completion」を参考に実装します。

Globally and Locally Consistent Image Completion

Globally and Locally Consistent Image Completionは
Deep Learningを利用した画像補完アルゴリズムです。

f:id:tereka:20171216155459p:plain

最初にGeneratorを学習し、次にDiscriminatorを学習し、
最後にGANの方式でGenerator, Discriminatorの両方を学習し、
高速マーチング法と呼ばれるアルゴリズムで周囲を補完します。

内部でいくつか細かい手法はあるのですが、ネットワークの構造をそのまま利用し、
諸々の処理の実装をしていません。(高速マーチング法)

Generatorを学習する際には復元画像を生成し、Mask部の誤差により更新します。
解像度を落とさず、DilatedConvolutionで大域的な特徴を利用する箇所と後段のDiscriminatorでは、
全体の画像と部分画像(マスク)を入力とし、判定しています。

Chainerで構築したモザイク外しアルゴリズム

さて、作ってみました。結構長くなったので全体は後ほどgithubへ置いておきます。
モデルとアップロード部については次で記述します。
コードはあまりきっちり記載していないので、綺麗にするためにはもう少し改良が必要です。

画像は128x128で入力しています。(処理の都合)

Generator

まずは、Generatorのコードです。
ここで肝となるのは、Dilated Convolutionを使い、大域的な特徴を見ていること
そして、誤差計算部(__call__)の引数にmskを入れています。
マスクを共に入力とした場合にモザイク領域以外の誤差を0にしています。

class GLCICGenerator(chainer.Chain):
    def __init__(self):
        super(GLCICGenerator, self).__init__()
        with self.init_scope():
            self.conv0 = L.Convolution2D(4, 64, ksize=3, stride=1, pad=1)
            self.bn0 = L.BatchNormalization(64)

            self.conv1_1 = L.Convolution2D(64, 128, ksize=3, stride=2, pad=1)
            self.bn1_1 = L.BatchNormalization(128)
            self.conv1_2 = L.Convolution2D(128, 128, ksize=3, stride=1, pad=1)
            self.bn1_2 = L.BatchNormalization(128)

            self.conv2_1 = L.Convolution2D(128, 256, ksize=3, stride=2, pad=1)
            self.bn2_1 = L.BatchNormalization(256)
            self.conv2_2 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_2 = L.BatchNormalization(256)
            self.conv2_3 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_3 = L.BatchNormalization(256)
            self.conv2_4 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=2, dilate=2)
            self.bn2_4 = L.BatchNormalization(256)
            self.conv2_5 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=4, dilate=4)
            self.bn2_5 = L.BatchNormalization(256)
            self.conv2_6 = L.DilatedConvolution2D(256, 256, ksize=3, stride=1, pad=8, dilate=8)
            self.bn2_6 = L.BatchNormalization(256)
            self.conv2_7 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_7 = L.BatchNormalization(256)
            self.conv2_8 = L.Convolution2D(256, 256, ksize=3, stride=1, pad=1)
            self.bn2_8 = L.BatchNormalization(256)

            self.deconv2_1 = L.Deconvolution2D(256, 128, ksize=4, stride=2, pad=1)
            self.debn2_1 = L.BatchNormalization(128)
            self.deconv2_2 = L.Convolution2D(128, 128, ksize=3, stride=1, pad=1)
            self.debn2_2 = L.BatchNormalization(128)

            self.deconv1_1 = L.Deconvolution2D(128, 128, ksize=4, stride=2, pad=1)
            self.debn1_1 = L.BatchNormalization(128)
            self.deconv1_2 = L.Convolution2D(128, 64, ksize=3, stride=1, pad=1)
            self.debn1_2 = L.BatchNormalization(64)

            self.deconv0 = L.Convolution2D(64, 3, ksize=3, stride=1, pad=1)

    def predict(self, x):
        h = F.relu(self.bn0(self.conv0(x)))
        h = F.relu(self.bn1_1(self.conv1_1(h)))
        h = F.relu(self.bn1_2(self.conv1_2(h)))

        h = F.relu(self.bn2_1(self.conv2_1(h)))
        h = F.relu(self.bn2_2(self.conv2_2(h)))
        h = F.relu(self.bn2_3(self.conv2_3(h)))
        h = F.relu(self.bn2_4(self.conv2_4(h)))
        h = F.relu(self.bn2_5(self.conv2_5(h)))
        h = F.relu(self.bn2_6(self.conv2_6(h)))
        h = F.relu(self.bn2_7(self.conv2_7(h)))
        h = F.relu(self.bn2_8(self.conv2_8(h)))

        h = F.relu(self.debn2_1(self.deconv2_1(h)))
        h = F.relu(self.debn2_2(self.deconv2_2(h)))
        
        h = F.relu(self.debn1_1(self.deconv1_1(h)))
        h = F.relu(self.debn1_2(self.deconv1_2(h)))

        return F.sigmoid(self.deconv0(h))

    def __call__(self, x, msk=None, t=None):
        h = self.predict(x)
        if msk is not None:
            h = msk * h
            t = msk * t
            loss = F.mean_squared_error(h, t)
            chainer.report({'loss': loss}, self)
            return loss
        else:
            return h
Discriminator

次にDiscriminatorです。特徴は入力が全体の画像とモザイク画像を中心とする
64x64の画像を入力としています。最後に結合し、出力を算出しています。

class GLCICDiscriminator(chainer.Chain):
    def __init__(self):
        super(GLCICDiscriminator, self).__init__()
        with self.init_scope():
            self.c0_l = L.Convolution2D(3, 32, 3, 2, 1)
            self.bn0_l = L.BatchNormalization(32)
            self.c1_l = L.Convolution2D(32, 64, 3, 2, 1)
            self.bn1_l = L.BatchNormalization(64)
            self.c2_l = L.Convolution2D(64, 128, 3, 2, 1)
            self.bn2_l = L.BatchNormalization(128)
            self.c3_l = L.Convolution2D(128, 256, 3, 2, 1)
            self.bn3_l = L.BatchNormalization(256)
            self.c4_l = L.Convolution2D(256, 512, 3, 2, 1)
            self.bn4_l = L.BatchNormalization(512)

            self.c0_g = L.Convolution2D(3, 16, 3, 2, 1)
            self.bn0_g = L.BatchNormalization(16)
            self.c1_g = L.Convolution2D(16, 32, 3, 2, 1)
            self.bn1_g = L.BatchNormalization(32)
            self.c2_g = L.Convolution2D(32, 64, 3, 2, 1)
            self.bn2_g = L.BatchNormalization(64)
            self.c3_g = L.Convolution2D(64, 128, 3, 2, 1)
            self.bn3_g = L.BatchNormalization(128)
            self.c4_g = L.Convolution2D(128, 256, 3, 2, 1)
            self.bn4_g = L.BatchNormalization(256)
            self.c5_g = L.Convolution2D(256, 512, 3, 2, 1)
            self.bn5_g = L.BatchNormalization(512)

            self.fc = L.Linear(None, 1)

    def __call__(self, x1, x2):
        h1 = F.leaky_relu(self.bn0_l(self.c0_l(x1)))
        h1 = F.leaky_relu(self.bn1_l(self.c1_l(h1)))
        h1 = F.leaky_relu(self.bn2_l(self.c2_l(h1)))
        h1 = F.leaky_relu(self.bn3_l(self.c3_l(h1)))
        h1 = F.leaky_relu(self.bn4_l(self.c4_l(h1)))

        h2 = F.leaky_relu(self.bn0_g(self.c0_g(x2)))
        h2 = F.leaky_relu(self.bn1_g(self.c1_g(h2)))
        h2 = F.leaky_relu(self.bn2_g(self.c2_g(h2)))
        h2 = F.leaky_relu(self.bn3_g(self.c3_g(h2)))
        h2 = F.leaky_relu(self.bn4_g(self.c4_g(h2)))
        h2 = F.leaky_relu(self.bn5_g(self.c5_g(h2)))

        concat_h = F.concat([h1, h2])
        return self.fc(concat_h)
Updater

最後に更新用のUpdaterです。
このUpdaterはDiscriminator更新とGAN更新用です。
本体はupdate_core関数になります。update_core部では次のことをしています。

  1. Discriminatorの誤差に必要な情報を計算(オリジナル画像)
  2. Generatorで画像を生成
  3. Discriminatorの誤差に必要な情報を計算(生成画像)
  4. updateで更新

メソッド名とその説明を次に掲載します。

メソッド名 説明
loss_dis 識別器(Discriminator)を更新
loss_gen 生成器(Generator)を更新
extract_img モザイク部近辺の画像を切り取るメソッド
extract_mosaic_area モザイク部近辺の画像を切り取るメソッド(バッチ的に処理する部分)
update_core 更新用の関数を呼び出すコア部分
class GLCICUpdater(chainer.training.StandardUpdater):
    def __init__(self, is_gen_training=True, alpha=4e-4, *args, **kwargs):
        self.gen, self.dis = kwargs.pop('models')
        self.is_gen_training = is_gen_training
        self.alpha = alpha
        super(GLCCICUpdater, self).__init__(*args, **kwargs)

    def loss_dis(self, dis, y_fake, y_real):
        batchsize = len(y_fake)
        L1 = F.sum(F.softplus(-y_real)) / batchsize
        L2 = F.sum(F.softplus(y_fake)) / batchsize

        loss = (L1 + L2) * self.alpha
        chainer.report({'loss': loss}, dis)
        return loss

    def loss_gen(self, gen, y_fake, x_fake, img_batch_variable, masks):
        batchsize = len(y_fake)
        h = masks * x_fake
        t = masks * img_batch_variable
        abs_pixel_loss = F.mean_squared_error(h, t)
        loss = (F.sum(F.softplus(-y_fake)) * self.alpha) / batchsize + abs_pixel_loss
        chainer.report({'loss': loss, 'pixel_loss': abs_pixel_loss}, gen)
        return loss

    def extract_img(self, img, bbox):
        while True:
            min_h = max(min(bbox[3], 127) - 64, 0)
            max_h = min(bbox[2], 63)

            min_w = max(min(bbox[1], 127) - 64, 0)
            max_w = min(bbox[0], 63)

            start_h = random.randint(min_h, max_h)
            end_h = start_h + 64
            start_w = random.randint(min_w, max_w)
            end_w = start_w + 64

            if start_h >= 0 and start_w >= 0 and end_w < img.shape[1] and end_h < img.shape[2]:
                return img[:, start_h: end_h, start_w: end_w]

    def extract_mosaic_area(self, images, bboxs):
        mosaic_region_imgs = []
        for fake_variable, bbox_variable in zip(images.data, bboxs):
            fake = chainer.cuda.to_cpu(fake_variable)
            bbox = chainer.cuda.to_cpu(bbox_variable)
            mosaic_region_img = self.extract_img(fake, bbox).transpose((1, 2, 0))
            mosaic_region_imgs.append(cv2.resize(mosaic_region_img, (64, 64)).transpose((2, 0, 1)))
        return mosaic_region_imgs

    def update_core(self):
        if self.is_gen_training:
            gen_optimizer = self.get_optimizer('gen')
        dis_optimizer = self.get_optimizer('dis')

        batch = self.get_iterator('main').next()
        img_batch, mosaic_batch_imgs, img_with_mask_batch, bbox_batch, masks = self.converter(batch, self.device)
        img_batch_variable = Variable(img_batch)
        x_real = Variable(img_with_mask_batch)
        xp = chainer.cuda.get_array_module(x_real.data)

        gen, dis = self.gen, self.dis

        region_real_images = self.extract_mosaic_area(img_batch_variable, bbox_batch)
        region_real_images_variable = Variable(xp.asarray(region_real_images))
        y_real = dis(region_real_images_variable, img_batch_variable)  # cut off image
        x_fake = gen(x_real)

        region_fake_images = self.extract_mosaic_area(x_fake, bbox_batch)
        region_fake_images_variable = Variable(xp.asarray(region_fake_images))
        y_fake = dis(region_fake_images_variable, x_fake)

        dis_optimizer.update(self.loss_dis, dis, y_fake, y_real)
        if self.is_gen_training is True:
            gen_optimizer.update(self.loss_gen, gen, y_fake, x_fake, img_batch_variable, masks)

結果

Generator

まずは、Generator部を利用しました。
Googliserで画像は収集し、その中から一部をテスト用としたので、検索が被っていれば重複の可能性があります。ご了承ください。

1epochの画像 何も表現できていません。
f:id:tereka:20171225212515j:plain:w640,h640

150epoch 少しずつ表現ができてきた?
f:id:tereka:20171225212923j:plain:w640,h640

400epoch あんまり変わらない。
f:id:tereka:20171225212953j:plain:w640,h640

GAN

そして、GANを利用した生成処理を実施します。
1 epoch 表現が残念なところからスタート
f:id:tereka:20171225213334j:plain:w640,h640

150epoch
f:id:tereka:20171225213434j:plain:w640,h640

おまけ

少しおもしろいのがデバッグ用画像です。
このデバッグ用画像はGeneratorの出力をそのまま表示している画像で、
元画像での上書きも何もしていません。

Generatorのみ学習した場合
f:id:tereka:20171225213716j:plain:w640,h640

GANの場合
f:id:tereka:20171225220845j:plain:w640,h640

入力されていないと思われるラブライブ画像(サンシャインから取ってきた)

元画像1
f:id:tereka:20171225233213j:plain:w256,h256
大量に生成したモザイク+復元画像1
f:id:tereka:20171225233204j:plain:w640,h640

元画像2
f:id:tereka:20171225233532j:plain:w256,h256

大量に生成したモザイク+復元画像2
f:id:tereka:20171225233521j:plain:w640,h640

思ったよりも綺麗に生成されていますね。

最後に

妄想力を完全に可視化するにはまだまだかかるようです。
結構ネタとして面白かったのでちょっと画像大きくしてやってみるのも面白いかなぁと思いました。
※何かバグや違い、こうすれば良い等あれば教えてください。

画像のDataAugmentationで最速のライブラリを探してみた

皆さんこんにちは
お元気ですか。年末に向けて燃え尽きようとしています。

OpenCVアドベントカレンダー第22日です。

qiita.com

今回、PythonのDataAugmentationで最速の方法を探してみます。
画像処理のDeepLearningでは、頻繁にDataAugmentationが使われています。

このData Augmentationは画像に結果がない程度に変換を加え、
ニューラルネットワークに入力する手法です。

以前、こんな資料を作りました。

www.slideshare.net

DataAugmentationにも様々な手法がありますが、次の処理する場合が非常に多いです。

  1. 画像をファイルから取得する。
  2. リサイズ
  3. アフィン変換
  4. 左右反転

この処理ですが、入力時に一定の確率で実行されます。
そのため、この処理が遅い場合には性能のボトルネックになります。
そのため、実装を高速化することは学習時間の短縮に非常に有用となります。

GPUが関わる処理で学習を高速化したい場合はCPU部も高速化することが必要でしょう。

速度を計測するにあたり、使用したライブラリをご紹介

OpenCV

当記事のアドベントカレンダーの掲載先となるフレームワーク
かつ、最も画像処理で有名なライブラリである言わずもがなOpenCVです。
Wikipediaの概要では、次のように記載されています。

画像処理・画像解析および機械学習等の機能を持つC/C++JavaPythonMATLAB用ライブラリ

opencv.org

Scikit-image

私個人はこれを推しています。
Pythonで完結するため、非常にインストールが楽です。
こちらも相当の人が使っており、サンプルも非常に多いです。

scikit-image: Image processing in Python — scikit-image

Pillow

このライブラリのサンプルを見たことある人はかなりいるのではないでしょうか?
Pythonの画像処理ライブラリで、かつて存在したPython Imaging Library (PIL)の fork プロジェクトです。

普段、Scikit-imageを利用している私は殆ど使ったことがありませんが、今回試しに使ってみました。

python-pillow.org

ぶっちゃけ全部画像処理ライブラリとしかいっていない

計測

では、早速計測を行います。
本手法における計測はtimeitを利用して実施します。

対象となる画像は皆さん大好きLennaさん(高225x幅225)です。
f:id:tereka:20171004005907j:plain

※ライブラリによって実装方法が異なるので、結果が多少ことなることがあるかもしれません。
 その点は確認して、利用してください。

画像読み込み

画像読み込みとすると非常に曖昧のため、今回は次の定義とします。

  1. 画像をファイルから読み込む
  2. 0-1へ画像を正規化
  3. float32に変換する
OpenCV

ソースコード

%timeit cv2.imread("./lenna.jpeg").astype(np.float32) / 255.0
2.06 ms ± 39.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Scikit-Image

ソースコード

import skimage.io
%timeit skimage.io.imread("./lenna.jpeg")
2.1 ms ± 40.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

from PIL import Image
def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

%timeit load_img("./lenna.jpeg")
2.02 ms ± 61.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

画像のリサイズ

続いて画像のリサイズです。
ニューラルネットワークの入力は固定長になる場合が多いので、基本的にはどこかでリサイズ処理が必要になり、
ImageNetを利用する場合は必須に近い技術となるでしょう。
そのため、この部分の速度を改善できるとDataAugmentationに非常に有用な手法となります。

f:id:tereka:20171221233331j:plain

OpenCV

ソースコード

%timeit cv2.resize(img,(128,128))
271 µs ± 4.26 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-image

ソースコード

%timeit skimage.transform.resize(img, (128,128))
4.32 ms ± 142 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL

ソースコード

%timeit p_img.resize((128,128))
23.2 µs ± 850 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

アフィン変換を行う

アフィン変換を実施します。
回転、平行移動、スケール変化、引き伸ばしをこのアフィン変換で実現できます。
DataAugmentationにおいても非常にメジャーな技術と言えるでしょう。
時間の都合もあり、条件が若干ばらばらですが、アフィン変換の計測をしました。

全て微妙に違う画像ですが、こんな感じで変換を行っています。

f:id:tereka:20171222005403j:plain

OpenCV
import cv2
def transform_opencv(img):
    size = int(img.shape[0] / 2)
    affine = cv2.getRotationMatrix2D((size, size), 45,  0.8)
    affine[0][2] += 10
    affine[1][2] += 15
    img_afn = cv2.warpAffine(img, affine, img.shape[0:2],flags=cv2.INTER_LINEAR)

%timeit transform_opencv(img)
771 µs ± 139 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Scikit-Image
def tranform_skimage(img):
    rotation = 45
    rotation = np.deg2rad(rotation)
    tform = AffineTransform(scale=(1.3, 1.3), rotation=rotation,
                            shear=1.0,
                            translation=(3, 3))
    warped_img = warp(img, tform)

%timeit tranform_skimage(img)
4.69 ms ± 72.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
PIL
from PIL import Image

def load_img(path):
    with open(path, 'rb') as f:
        with Image.open(f) as img:
            return img.convert('RGB')

def ScaleRotateTranslate(image, angle, center = None, new_center = None, scale = None,expand=False):
    if center is None:
        return image.rotate(angle)
    angle = -angle/180.0*math.pi
    nx,ny = x,y = center
    sx=sy=1.0
    if new_center:
        (nx,ny) = new_center
    if scale:
        (sx,sy) = scale
    cosine = math.cos(angle)
    sine = math.sin(angle)
    a = cosine/sx
    b = sine/sx
    c = x-nx*a-ny*b
    d = -sine/sy
    e = cosine/sy
    f = y-nx*d-ny*e
    return image.transform(image.size, Image.AFFINE, (a,b,c,d,e,f), resample=Image.BICUBIC)

def scale():
    image =load_img("./lenna.jpeg")
    transformed = ScaleRotateTranslate(image, 15, None, (0, 0), scale=0.9)

%timeit scale()
3.03 ms ± 83 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

左右反転

左右反転を行います。処理後の画像は次の通り

f:id:tereka:20171221232633j:plain

OpenCV
%timeit cv2.flip(img,0)
13 µs ± 162 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Numpyの方式
%timeit img[:, ::-1]
404 ns ± 12.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
PIL
%timeit img.transpose(p_img.FLIP_LEFT_RIGHT)
56.5 µs ± 4.97 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

まとめ

OpenCV強いと書こうと思ったらPILの方が速い場面があった。
OpenCVの方が使いやすいのですが・・・

ライブラリ 画像の読み込み リサイズ アフィン変換 フリップ
OpenCV 2.06ms 271 µs 771 µs 13 µs
Scikit-image 2.1 ms 4.32 ms 4.69 ms なし(numpyで実施)
PIL 2.02ms 23.2 µs 3.03 ms 56.5 µs
Numpy(一部) なし なし なし 404 ns

最後に

OpenCVが想像以上にScikit-imageより速いがPILの方が速い。そのため、もしこの処理で困っているならば
OpenCV、もしくは、PILを利用すると高速化できそうです。

もちろん、状況次第によって変化するかもしれませんが、
この結果を参考に良い実装をしてみてください。