stMind

about Arsenal, Arsene Wenger, Tech, Computer vision and Machine learning

Deep Learning時代のPose Estimation研究

[1602.00134] Convolutional Pose Machines

[1611.08050] Realtime Multi-Person 2D Pose Estimation using Part Affinity Fields

少し前まではPose estimationは非常に難しい問題だったように思いますが、Convolutional Pose MachinesやRealtime Multi-Person 2D Pose Estimation using Part Affinity Fieldsといった論文において、Fully convolutionalなNNによって人体関節をconfidence map(もしくはbelief mapやheatmapとも呼ばれる)として推定する手法が提案、有効性が検証され、さらにそれを実装したソフトウェアが公開されて応用探索されていくのを見ていると、Pose Estimation自体は既に研究フェーズからは移行したように感じます。

最近、GoogleからWebブラウザで実行可能なPoseNetモデルがリリースされたというニュースがありましたが、OpenPoseのような関節間の接続を表現するPart Affinity Fieldではなく、各ピクセルの関節に対するオフセットを表すOffset Vectorを学習するモデルになっていて、これに限らず手法の工夫ポイントはまだあるようにも思いますが、進歩性としてはどこまで残っているんだろう?とは思います。

medium.com

Deep Learning時代は、研究から応用までのスピードが速いですね...

KerasでGAN

towardsdatascience.com

Mediumの記事を参考に、一番基本のGANについて試してみた。データセットはおなじみのfashion mnist。

GANのアーキテクチャ

ノイズ画像(100次元のランダムなベクトル)からfashion画像を生成するgeneratorは、3層の全結合層から成るネットワーク。各層の出力次元数は28, 29, 210としている。

def get_generator(optimizer, output_dim=784):
    generator = Sequential()
    generator.add(
        Dense(
            256,
            input_dim=random_dim,
            kernel_initializer=initializers.RandomNormal(stddev=0.02)))
    generator.add(LeakyReLU(0.2))

    generator.add(Dense(512))
    generator.add(LeakyReLU(0.2))

    generator.add(Dense(1024))
    generator.add(LeakyReLU(0.2))

    generator.add(Dense(output_dim, activation='tanh'))
    generator.compile(loss='binary_crossentropy', optimizer=optimizer)
    return generator

一方のdiscriminatorも3層の全結合層から成るネットワーク。出力ノードはrealかfakeかのバイナリ値。同じように、各層の出力次元数は210, 29, 28としている。

def get_discriminator(optimizer, input_dim=784):
    discriminator = Sequential()
    discriminator.add(
        Dense(
            1024,
            input_dim=input_dim,
            kernel_initializer=initializers.RandomNormal(stddev=0.02)))
    discriminator.add(LeakyReLU(0.2))
    discriminator.add(Dropout(0.3))

    discriminator.add(Dense(512))
    discriminator.add(LeakyReLU(0.2))
    discriminator.add(Dropout(0.3))

    discriminator.add(Dense(256))
    discriminator.add(LeakyReLU(0.2))
    discriminator.add(Dropout(0.3))

    discriminator.add(Dense(1, activation='sigmoid'))
    discriminator.compile(loss='binary_crossentropy', optimizer=optimizer)
    return discriminator

GANの学習

最初にdiscriminatorだけを学習してパラメータを更新した後、ノイズ画像(100次元ベクトル)からgeneratorとdiscriminatorを通してrealかfakeかを判定するend-to-endのネットワーク(get_gan_network)に対して、discriminatorは重みを固定してgeneratorだけを学習する、という二段階の処理です。

discriminatorの学習では、x_trainからランダムにピックアップしたreal画像とgeneratorで生成したfake画像をそれぞれbatch_size個用意して、それらを連結したデータ(Xとy_dis)を作成して与えています。また、ノイズ画像から生成した画像をrealと判定するようなgeneratorにするために、ラベルを1としてgeneratorを学習しています。

def train(epochs=1, batch_size=128):
    # Get the training and testing data
    (x_train, y_train, x_test, y_test), x_height_width = load_mnist_fashion()
    # Split the training data into batches of size 128
    batch_count = x_train.shape[0] // batch_size

    # Build our GAN netowrk
    adam = get_optimizer()
    generator = get_generator(adam, output_dim=x_train.shape[1])
    discriminator = get_discriminator(adam, x_train.shape[1])
    gan = get_gan_network(discriminator, random_dim, generator, adam)

    for e in range(1, epochs + 1):
        print('-' * 15, 'Epoch %d' % e, '-' * 15)
        for _ in tqdm(range(batch_count)):
            # Get a random set of input noise and images
            noise = np.random.normal(0, 1, size=[batch_size, random_dim])
            image_batch = x_train[np.random.randint(
                0, x_train.shape[0], size=batch_size)]

            # Generate fake images
            generated_images = generator.predict(noise)
            X = np.concatenate([image_batch, generated_images])

            # Labels for generated and real data
            y_dis = np.zeros(2 * batch_size)
            # One-sided label smoothing
            y_dis[:batch_size] = 1.0

            # Train discriminator
            discriminator.trainable = True
            discriminator.train_on_batch(X, y_dis)

            # Train generator
            noise = np.random.normal(0, 1, size=[batch_size, random_dim])
            y_gen = np.ones(batch_size)
            discriminator.trainable = False
            gan.train_on_batch(noise, y_gen)

        if e == 1 or e % 20 == 0:
            plot_generated_images(e, generator, train_shape=x_height_width)

生成された画像

batch_size=128で、300epoch学習を回した結果。

  • Epoch1では、まだ単なる矩形画像にしかなっていない

f:id:satojkovic:20180521235550p:plain

  • Epoch100では、各アイテムがかなり分かる程度になっているが、一部単なる矩形画像が残っている

f:id:satojkovic:20180521235705p:plain

  • Epoch300では、各アイテムがはっきりと分かる程度に生成された

f:id:satojkovic:20180521235922p:plain

レポジトリ

github.com

KerasでSemantic segmentation

画像ではなく、ピクセル単位でクラス分類するSegmentationのタスク。 fast.aiにあるtiramisuが実装もあって分かりやすいので試してみた。下記のコードスニペットは、fast.aiのオリジナル実装ではなく、keras2で書き直されたjupyter notebookのコードをベースに、自分で若干の手直しをしたものを使っている。

github.com

tiramisu

Down-sampling path

  • Down-sampling pathの構成と実装
    • DB(Dense Block)とTD(Transition Down)の繰り返しで特徴抽出
    • DBの最後の出力特徴マップをUp-sampling pathのためのskip connectionとして保存
def down_path(x, nb_layers, growth_rate, keep_prob, scale):
    skips = []
    for i, nb_layer in enumerate(nb_layers):
        x, added = dense_block(nb_layer, x, growth_rate, keep_prob, scale)
        skips.append(x)
        x = transition_down(x, keep_prob, scale)
    return skips, added
  • DBの構成と実装
    • BN(Batch Normalization)、ReLU、3x3 same convolution(とdropout)を組み合わせたLayerで特徴マップを生成
      • growth rateが特徴マップ数を表す
    • 特徴マップを入力に連結(concat)
    • 次のDBのために新しく生成した分の特徴マップを保存
def dense_block(nb_layers, x, growth_rate, keep_prob, scale):
    added = []
    for i in range(nb_layers):
        b = conv_relu_batch_norm(
            x, growth_rate, keep_prob=keep_prob, scale=scale)
        x = concat([x, b])
        added.append(b)
return x, added
  • TDは空間解像度の削減、要はpoolingと同等の処理
    • 論文では1x1 convolutionと2x2 poolingの組み合わせが提案されているが、fast.aiではstrideが2の1x1 convolutionを提案。
def transition_down(x, keep_prob, scale):
    return conv_relu_batch_norm(
        x,
        x.get_shape().as_list()[-1],
        ksize=1,
        scale=scale,
        keep_prob=keep_prob,
        stride=2)

Up-sampling path

  • Up-sampling pathの構成と実装
    • TU(Transition Up)、skip connectionとの連結(concat)、DBの繰り返しで入力画像サイズに復元
def up_path(added, skips, nb_layers, growth_rate, keep_prob, scale):
    for i, nb_layer in enumerate(nb_layers):
        x = transition_up(added, scale)
        x = concat([x, skips[i]])
        x, added = dense_block(nb_layer, x, growth_rate, keep_prob, scale)
    return x
  • TUは特徴マップをupsamplingするtransposed convolutionで構成
    • 入力と同じch数で、3x3 kernel size、strides 2で2倍の特徴マップを生成
def transition_up(added, scale):
    x = concat(added)
    _, row, col, ch = x.get_shape().as_list()
    return Conv2DTranspose(
        ch,
        3,
        kernel_initializer='he_uniform',
        padding='same',
        strides=(2, 2),
        kernel_regularizer=l2(scale))(x)

学習

  • 56層バージョン。FC-DenseNet56
  • OptimizerはRMSProp、学習率の初期値は1e-3、学習率減衰は0.00005
  • L2正則化の係数1e-4、Dropout rateは0.2、growth rateは12
  • データセットはcamvid、360x480から224x224をクロップして入力画像とする
  • batchsizeは10、epochsは30で実行

結果

  • 24epochでvalidation accuracyが85%
  • 結果画像を見ると、道路と白線は分類出来ているようだが、車と背景との分離はもう一歩。Ground-Truthにはない画像右上の柱が推定出来ている。
  • パラメータ数が少ないモデルという特徴があり、実際にGPUの無いMacでも学習を実行出来るのは良い点。

f:id:satojkovic:20180501234738p:plain

WIndowsでTensorflow C++ APIを使う準備

TL;DR

WindowsでTensorflowのC++ APIを使わなければいけないケースというのはそれほど多くないと思うし、どうしてもという理由がなければやらない方が良いと思う!

ステップ

基本は、下記のブログを参照すれば出来る。

tadaoyamaoka.hatenablog.com

  1. cmakeを使って、VS2015のソリューションを生成
    • Tensorflowのソースディレクトリはtensorflow/tensorflow/contrib/cmake
    • ソリューションファイルの生成先は、例えばtensorflo/cmake_buildを作って指定する
    • CUDAは9.0で、CuDNNは7.0
  2. configureして、swigが見つからないというエラーが出たらswig.exeのパスを指定
    • swigwin-3.0.12をダウンロード
    • swigwin-3.0.12/swig.exeを指定するだけ
  3. 再度configure、configure doneになったらgenerate
  4. generate doneになったらOpen projectでソリューションファイルを開く
  5. x64でReleaseビルド

注意点

  • ビルドが終了するまで数時間はかかる
  • メモリ16GBのマシンでビルドしたら、いくつかのプロジェクトでリソース不足的なエラーでビルド出来てなかった。結構なハイスペックが要求されそう。

あなたは世界を変えない

m.signalvnoise.com

Ruby on Railsの作者DHHさんのmediumエントリー。最近読んだ中でも一番良いなと思った。

TwitterFacebookも世界を変えよう❗という明確なミッションを持って始まったわけではないし、そういうミッションを持つことは大変な重荷。壮大な幻想(ミッション)を持つことで、毎日毎日会議を繰り返す日々に無理やり意味をもたせようとしているのでは?みたいな指摘も。

Set out to do good work. Set out to be fair in your dealings with customers, employees, and reality. Leave a lasting impressions on the people you touch, and worry less (or not at all!) about changing the world. Chances are you won’t, and if you do, it’s not going to be because you said you would.

良い仕事をしよう、顧客や従業員、現実に対してフェアでいよう、関わる人に対して良い印象を残そう。そういうことに集中することの方がより大事だよというアドバイスでした。

I have no illusions that my time in this world is going to be remembered through the ages. When it’s over, I’ll be so fortunate as to have left an impression on my friends, my family, and a few colleagues in the industry. And those impressions will fade quickly, so they aren’t even worth elevating much.

私の人生は世代を通じて思い出されるという幻想は持ってない。終わったときに、友達や家族、何人かの同僚に思い出してもらえれば相当幸せだろう。それらの印象もいずれは薄れていくし、もてはやすようなものではない。

相当なオッサンになってきたので、こういう考えもそうだよなぁと思うようになった。

P.S. いくつかmediumでエントリーを見たけど、DHHさんの英語って難しくないですか❓独特の言い方というか、自分が知らないだけなのか…

CourseraのDeep Learning Specializationコースのまとめノートを見つけた

先日全てのレクチャーをやり終えたCourseraのDeep Learning Specializationコース。そのまとめノートをSlideShareで公開している人がいた❗

図解が上手い❗今まで見てきたDeep Learningの解説資料の中でも、分かりやすいランキングのかなり上の方に位置すると個人的に思う。

PASCAL-Part Datasetのmatファイルフォーマットがようやくわかった

f:id:satojkovic:20180228120016p:plain

FCNでSemantic segmentationをやってみようと思いたち、物体のパーツ単位で正解マスクが提供されているPASCAL-Part Datasetを使うことにした。のだが、正解マスクが含まれているmatファイルのフォーマットがよくわからない!データセットにはオリジナル画像 やマスク画像をタイル表示するデモ用のmatlabスクリプトがあるけれど、matlabはあまり慣れてない... こういうのは慣れてるものを使えばいいやということで、ipythonでインタラクティブに解析することにしたよ。

ルート > オブジェクト > パーツ

結局、データのルート > オブジェクト > オブジェクト毎パーツというような階層になってて、書いてみれば直感的にわかるけど、多量にネストされていて理解するまでに時間がかかった...

例えば、下の画像であれば

f:id:satojkovic:20180228141358j:plain

In [1]: import scipy.io as sio

In [2]: data = sio.loadmat('Annotations_Part/2008_003228.mat')

In [3]: data.keys()
Out[3]: dict_keys(['__header__', '__version__', '__globals__', 'anno'])

In [4]: data['anno'][0][0][0]
Out[4]: array(['2008_003228'], dtype='<U11')

# ここがルートで、この画像に含まれるオブジェクト数は4
In [5]: data['anno'][0][0][1].shape
Out[5]: (1, 4)

# オブジェクトの情報
# オブジェクトはpersonが3個、catが1個(画像左の黒い領域)
In [6]: data['anno'][0][0][1][:, 0][0][0]
Out[6]: array(['person'], dtype='<U6')

In [7]: data['anno'][0][0][1][:, 1][0][0]
Out[7]: array(['person'], dtype='<U6')

In [8]: data['anno'][0][0][1][:, 2][0][0]
Out[8]: array(['person'], dtype='<U6')

In [9]: data['anno'][0][0][1][:, 3][0][0]
Out[9]: array(['cat'], dtype='<U3')

# パーツの情報
# 1番目のオブジェクトのパーツ数, personはmaxで24パーツまで
In [11]: data['anno'][0][0][1][:, 1][0][3].shape
Out[11]: (1, 16)

# 0番目のパーツの名前 => head
In [16]: data['anno'][0][0][1][:, 1][0][3][:, 0][0][0]
Out[16]: array(['head'], dtype='<U4')

# 1番目のパーツの名前 => left ear
In [21]: data['anno'][0][0][1][:, 1][0][3][:, 1][0][0]
Out[21]: array(['lear'], dtype='<U4')

# 正解マスク
In [22]: data['anno'][0][0][1][:, 1][0][3][:, 1][0][1]
Out[22]: 
array([[0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]], dtype=uint8)

# 正解マスクのサイズ = 画像サイズ
In [23]: data['anno'][0][0][1][:, 1][0][3][:, 1][0][1].shape
Out[23]: (375, 500)

後は、これを取り出して画像とペアにすれば、FCN用のPart datasetが準備出来ることになる!(ほんとはまだ)

おまけで、matlabのデモコードを実行したときのマスク画像(右下)はこんなの。

f:id:satojkovic:20180228143346p:plain