アイドル顔識別のLINE BOTを作りました

f:id:sugyan:20161128123639p:plain:w260 f:id:sugyan:20161128123706p:plain:w260

記事要約

機械学習によるアイドル顔識別のLINE BOTを作りました。アイドル分かる人は是非 友だち追加して アイドル画像を送りつけて遊んだりしてみてください。

むしろ識別対象のアイドルさん御本人にも使ってもらえたら最高

友だち追加

BOT概要

TensorFlowによるアイドル顔識別器の話 - 2016.9.28 TensorFlow勉強会 - Qiita

にまとめている、自作のアイドル顔識別器の 「入力した画像に対し、写っているのが『どのアイドル(人物)か』を機械学習により自動判定する」 という機能を実際に試すためのインタフェース。

トーク(1:1、Room, Group、いずれでも可)で画像を投稿されると、その画像に写っている人物の顔を識別してCarouselメッセージで結果を返す。

11月末現在の時点で、識別対象のアイドルは851人となっています。

顔識別API

元々 実際に学習結果を試すため画像を入力して結果を出力するインタフェースは TensorFlowによるディープラーニングで、アイドルの顔を識別する - すぎゃーんメモ あたりのときから作っていて、TensorFlowで学習したパラメータをtf.train.Saverでcheckpointファイルとして出力しておき、それを使って復元したモデルで入力画像に対する識別結果を返すだけのJSON APIFlaskで用意して使っている。

学習をするわけでなく結果を使って計算するだけならHerokuでもこれくらいは動かせる。

tf-face-recognizer/web.py at master · sugyan/tf-face-recognizer · GitHub

Twitter

なのでBOTを作る場合はそのAPIに繋ぎ込むだけでよくて、まずはMentionに反応するTwitter BOTで作ってちょいちょい動かしてみていたのだけど、

  • 画像が4つまでしか添付できない
  • 140文字までしかテキストを入力できない

の制限のため、1画像から5つ以上の顔が識別されると切り抜いても投稿できないし、そもそも識別結果の人物名・グループ名を入力すると3〜4人で140文字に達してしまうなどの問題があった。

あとどうしてもpublicなtimelineに流れることになってしまい(まぁDMに対応すれば良かったのかもしれないけど)、間違った結果などが投稿されて御本人のエゴサ結果を汚してしまう、などの問題もあり。

LINE版

LINE Messaging API を使った Template Message では、Carousel を使うことで1メッセージで5件まで結果を投稿できるし、1つずつ別個にテキスト情報を付与できるので表示がより分かりやすくなる。 ついでにその人物のプロフィールページ(今は取得元のTwitterプロフィールページにしてる)があれば URI action で開いたりもできるし。

実装

今回もGo SDKを使って作っている。動かす場所はどこでも良かったけど とりあえずHerokuで。

顔識別応答に必要な機能は

  • Image Message を受け取ったら、その画像データを取得し、
  • 自作の顔識別APIにその画像をそのまま投げ、
  • 結果のJSONをparseし、
  • それぞれの検出された顔・識別結果を Carousel Column にセットし、
  • 出来上がった Template Message を Messaging API に投げる

というだけではあるのだけど、Twitter BOTの場合とは画像の投稿まわりが異なるのでちょっと考えないといけない。

TwitterUploading Media API があって、そこにまずPOSTして そのmedia_idをリクエストに載せれば画像投稿が出来るのだけど、LINE Messaging API の場合は画像系の投稿は「画像URL」をリクエストに載せることになるので、その画像のホスティングは自分でどうにかしないといけない。

今回の顔識別BOTの場合は、検出された顔領域を切り抜いたものをthumbnailImageUrlとして Carousel Column に載せたいけど、これらを静的配信するだけのためにS3 bucketとか作って管理したくないなぁ、ということで 動的に生成しつつキャッシュでどうにかする戦略で作った。

検出された領域の抽出

顔識別APIでは、入力した画像に対し検出された顔の位置座標と傾き角度を返す(これは Cloud Vision API のレスポンスをそのまま使っている)ので、「どの部分に写っている顔がどう識別されたか」を示すために、検出された領域をそれぞれ切り抜きたい。せっかく傾きも判っているのならばそれも補正して真っ直ぐにした状態で。

以前の記事 で、 ImageMagick-distort SRTオプションでの変換を使った切り抜き方は把握していたけど、せっかくだから今回はPure Goだけでもやってみよう ということで 画像のScale, Rotate, Translateが可能なライブラリを探して、github.com/disintegration/giftが良さそうだったのでそれを使ってみた。(他にも良いのがあれば教えていただきたいところ…)

giftの画像変換はちょっとクセがあって、回転の中心を指定したり座標の移動などはできなくて しかも回転後の画像がすべて収まるよう画像サイズが再計算されるので、

  • まず傾き角度に合わせて画像全体をRotateし、
  • 元の画像で検出されていた顔領域座標を回転後の画像の座標に変換し、
  • その回転後の座標で領域部分をCropする

という感じに。回転行列とか使って座標をゴニョゴニョと変換する必要があった。

f:id:sugyan:20161126204211p:plain

抽出画像のキャッシュ

ともかくこうして抽出した顔画像を返すURLを生成してthumbnailImageUrlに渡すことでCarousel TemplateのMessageを送れるのだけど、このURLはMessageを受け取ったユーザがLINEで開くたびに取得しようとGETリクエストを送ってくることになる。

S3とかにアップロードして配信していればあまり問題ないけど、今回それを使わずBOTのWebアプリケーションから配信するようにするために、JPEGバイナリをRedisでキャッシュすることにした。thumbnailの画像は無駄に大きい必要もないので302x200サイズに加工して(縦横1:1.51という比は固定のようで正方形で返すと上下が切り取られてしまうので左右を埋めて作った)、そうすると10KBにも満たない容量だしheroku-redishobby-devプラン(Memory 25MB)でも余裕で1000枚分くらいはキャッシュできるはず。

で、普通に考えると一番最初に Template Message をPOSTした瞬間が一番アクセスされることになる。特にRoomやGroupで複数人が居る場合は、それぞれのユーザがトーク画面を開いていると投稿した瞬間に同時に複数クライアントからGETリクエストが来るはず。なので、送信するthumbnailImageUrlの各画像JPEGバイナリは事前に生成してから投稿する。

そして、大抵のMessageは流れていくのでそれほど長期間保存しておく必要もないと思っていて、数時間〜数日保持してあれば問題なく。ただ遡ったときにまったく見えなくなっていると困るのでキャッシュが切れていたときは再び元画像を取得しなおして作れるように「元画像のmessage IDと、抽出すべき領域の情報」を復元できるURLにしておく。(任意のmessage IDを指定して画像取得されても困るので それは防ぐためにmessage IDは暗号化した文字列を使うようにしている)

Get content API での取得も、一定期間が経過すると削除されてしまって復元不可能になるけど、まぁそれくらい過去のものは諦めてください、ってことで。

これで、重い処理は極力避けつつも単一のアプリだけで画像の提供もできるBOTアプリを動かすことができた。

別機能 (推論フィードバック)

元々、自分で学習データを管理する用として Messaging APIを使ったLINE Botでアイドル顔画像管理 - すぎゃーんメモ という記事でも書いたとおり そういうBOTを作っていて、これはこれで独立した機能として上述の識別BOTに同居させられるな、と思い。

少し改良して

  • 1:1トークで 識別対象アイドルの名前(の一部でもOK)をテキスト入力すると、分類器によってその人物と推論された顔画像を返す
  • それらに対して「○」か「×」だけを選択できるようにして Postback 送信

というのを、自分だけでなく BOTを友だち追加した人が誰でもフィードバックを送ることができるようにした。これはRoomやGroupでは誰が送ったものか判別できないので1:1トークのみ対応の機能となる。

f:id:sugyan:20161127203707p:plain:w260

対象はあくまでもこちらの管理アプリが持っているデータに対してのみで、識別対象に入っていない限りは推論結果としても現れないのだけど、それでもフィードバックを送ってもらうことで学習データの増加と改善に役立つので、もし知っているアイドルが識別対象に入っているようであれば試していただいて、フィードバックを送っていただきたいです。

少しでも学習データの増加に繋がると嬉しい…。

Repository

余談

今回のBOTを作るにあたり、GoをずっとEmacsで書いていたのだけど、突如思い立って開発環境をAtomに乗り換えてみた。今この記事の下書きもAtomMarkdown Preview しながら書いている。 atomic-emacs の他、Goの開発用には go-plus を入れて、あとは適度に自分好みにkeymapを弄ったり。

ちょっとKarabinerにキーを奪われて上手く設定できなかったり Emacsと比較して dired-mode みたいなのが無くてファイル操作がしにくいな、とか 多少の不満はあるけれど、以外と馴染んで普通に使えるような気はしている。 他の言語(Java以外)での開発もこれでやっていくよう練習してみるつもり。

Messaging APIを使ったLINE Botでアイドル顔画像管理

先日の LINE DEVELOPER DAY 2016 で発表された Messaging API

LINE Developers - Messaging API - Overview

Template Messageという、Botインタラクティブなやりとりができるタイプのメッセージがあり、

https://devdocs.line.me/ja/#template-message

LINE 6.7.0以降の対応ということで iOS版はなかなか使えなかったけど、昨日ついにそれがリリースされ 自分のiPhoneでも使えるようになったので、それを使った自分用Botを作ってみた。

www.youtube.com

Template Message とは

ドキュメントに書いてある通りだけど、"Buttons", "Confirm", "Carousel"という3つのテンプレートが用意されていて、そこに画像URLやActionを指定することで 普通のテキストや画像のみのメッセージとは違う、ちょっとリッチなメッセージを送ることが出来る。

"Confirm"は確認のalertダイアログのようなもの、"Buttons"は画像とテキストを組み合わせたものを表示し、その下に複数のActionボタンを配置できる。"Carousel"はその"Buttons"と同等のものをさらに横に連ねてスクロールで操作できる。

ボタン押下の際のActionには"Postback", "Message", "URI"の3つがあり。"URI"は指定したリンクを開くもの、"Message"は指定されたテキストを発言するもの、そして"Postback"はBot用のPostback Eventを送信するだけ(同時にテキストを送信することもできるけど)、というもの。

入力インタフェースの変化

どういうことかと言うと、自分で何かメッセージを送らなくても その「ボタンを押す」という操作だけでBotに任意の情報を通知できる、ということ。

従来のChat botって、大抵はテキストのメッセージを「コマンド」として利用して、例えばユーザからある文字列を入力された場合だけ(Bot側でそれを正規表現とか使ったif文やswitch文で判別して)指定された動作を行う、という感じだったと思うのだけど そういうのをPostbackを使うとイベント通知で置き換えることが出来るようになるわけで。

ユーザが「(テキストなどの)入力を行う」のではなく「ボタンを押す」という操作だけでBotに対して命令や情報を送ることが出来るようになる、というのがとても便利で面白い。Botとのやり取りをするインタフェースが大きく変わるぞ〜 と思った。

アンケートを取る、とか ユーザに何か選択させるようなものは格段にやりやすくなったんじゃないでしょうか(選択肢が多い場合は厳しいだろうけど)。

個人ツールへの応用

memo.sugyan.com

とか

qiita.com

とか でも書いているけど、いま個人の趣味でやっているアイドル顔画像のデータ収集では、手動でラベル付けするのがあまりにも大変なので、最近はラベル付け済みのデータを使って一通りモデルを学習させて そのモデルに未知の顔画像を推論してもらってそれを確認する、という作業を続けていて。

そのへんもずっと自作のWebアプリを使ってぽちぽちとやっていたのだけど、スマホからだと微妙に操作しづらく、もうちょっと見易く簡単に出来ないかなぁと思っていたところに ちょうど良くLINE Botのインタフェースが使えそうだったので、作ってみた次第。Web操作を代替するための、( 基本的に)自分だけがやり取りするBot

既存のWebアプリにJSON APIとtoken認証の仕組みを加えて、あとはBot用のWebアプリをherokuで動かして繋ぎ込むだけで出来た。

  • 推論結果を取得し、Carouselで同時に5つ表示
    • 情報源となっているURIを開くButton
    • 推論が合っている場合の承認Button
  • 承認ButtonからPostbackを受け取り更新処理
    • 更新結果をConfirmで通知
      • 更新の確認できるURIを開くButton
      • 間違っていた場合の取り消し処理(実装していないので何もしない)

という感じ。

冒頭のデモ動画の通り、起動のトリガーとして任意のテキストメッセージを受け取ったら作動、にしているけど(これは定時Pushとかにしても良いはず) それ以降は自分でメッセージを送信したりすることなく、ボタン操作のみで確認、更新などの作業ができている。 Chat logとして時系列で操作記録も残るし便利!

https://github.com/sugyan/face-manager-linebot

Bot SDK

公式からSDKも提供されているので簡単に作れました。

今回のは諸事情でGoで作ったけど、本日Python版もリリースされたので、例えばTensorFlowで機械学習させたものを動かしつつLINE Botに組み込んで インタラクティブに分類や生成を試したり、とかも出来そうで 夢が拡がりますね!

TensorFlowによるDCGANでアイドルの顔画像生成 その後の実験など

memo.sugyan.com

の続編。

あれから色々な変更しつつ実験してみたりしたのでその記録。 結論を先に書くと、これくらい改善した。

f:id:sugyan:20161012032542j:plain

DCGAN ざっくりおさらい

  • Generator: 乱数の入力から画像を生成する
  • Discriminator: 入力した画像がGeneratorが生成したものか学習データのものかを判別する

という2種類のネットワークを用意し、お互いを騙す・見破るように学習を行うことで Generatorが学習データそっくりの画像を生成できるようになる、というもの

学習用画像の増加

前回の記事では90人の顔画像データから生成していたけど、あれから収集を続けて もう少し多く集まったので、今回は260人から集めた顔画像100点ずつ、計26,000件を学習に使用した。

Feature matching

openai.com

の記事で紹介されている "Improved Techniques for Training GANs" という論文を読んで、使われたコードも読んでみまして、正直何やっているのか分からない部分が多く理解できていないことだらけなのだけど その中の "3.1 Feature matching" のところは分かりやすく効きそうだったので取り入れてみた。

原理としては、「Discriminatorの中間層出力には分類のための特徴(feature)が含まれるはずなので、それがGeneratorによるものと学習データ由来のものとで似たようなものになっていれば(学習データに近いものがGeneratorから生成されている、ということになるので)より良いはず」ということのようだ。

なので、Discriminatorの最終出力(入力画像が学習データのものか否かを判定するもの)の1つ前の、4回の畳み込みを行った段階での出力をそれぞれ(Generator由来の画像を入力した場合/学習データの画像を入力したとき)で取得し、各mini batchごとの平均値の差分が少なくなるよう 適当な倍率を掛けてGeneratorのloss値として加えた。

    def build(self, input_images,
              learning_rate=0.0002, beta1=0.5, feature_matching=0.0):
        """build model, generate losses, train op"""
        generated_images = self.g(self.z)[-1]
        outputs_from_g = self.d(generated_images)
        outputs_from_i = self.d(input_images)
        logits_from_g = outputs_from_g[-1]
        logits_from_i = outputs_from_i[-1]
        if feature_matching > 0.0:
), feature_matching))
            features_from_g = tf.reduce_mean(outputs_from_g[-2], reduction_indices=(0))
            features_from_i = tf.reduce_mean(outputs_from_i[-2], reduction_indices=(0))
            tf.add_to_collection('g_losses', tf.mul(tf.nn.l2_loss(features_from_g - features_from_i), feature_matching))
        tf.add_to_collection('g_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_g, tf.ones([self.batch_size], dtype=tf.int64))))
        tf.add_to_collection('d_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_i, tf.ones([self.batch_size], dtype=tf.int64))))
        tf.add_to_collection('d_losses', tf.reduce_mean(tf.nn.sparse_softmax_cross_entropy_with_logits(logits_from_g, tf.zeros([self.batch_size], dtype=tf.int64))))

後述するけれど、従来の方法だと 学習を続けていくと生成画像が全体的に白っぽく薄くなる、という現象があって、おそらくこれはDiscriminatorが画像を判別する際に全体の色合いなどは注視しないからなのではないかと思われ(以前の記事で実験しているように、人間の感覚とは全然違う特徴抽出しているようだ)、 それを防ぐためにも このfeature matchingと同様のものを最終出力の画像にも適用してみた。

         logits_from_g = outputs_from_g[-1]
         logits_from_i = outputs_from_i[-1]
         if feature_matching > 0.0:
+            mean_image_from_g = tf.reduce_mean(generated_images, reduction_indices=(0))
+            mean_image_from_i = tf.reduce_mean(input_images, reduction_indices=(0))
+            tf.add_to_collection('g_losses', tf.mul(tf.nn.l2_loss(mean_image_from_g - mean_image_from_i), feature_matching))
             features_from_g = tf.reduce_mean(outputs_from_g[-2], reduction_indices=(0))
             features_from_i = tf.reduce_mean(outputs_from_i[-2], reduction_indices=(0))
             tf.add_to_collection('g_losses', tf.mul(tf.nn.l2_loss(features_from_g - features_from_i), feature_matching))

比較結果が以下の動画。左側が従来の普通のDCGAN、右側がfeature matchingを加えたもの。


DCGAN with feature matching

左側は全体的にずっとガチャガチャと目まぐるしく変化していて落ち着かない感じなのが、右側は比較的早い段階から顔っぽいものが出来てゆるやかに安定していくように変化している様子が伺える。また左は14,000stepくらいから全体的に白っぽく薄くなっていっているのが 右側では起こらなくなっているのも確認できる。

Learning rate, Batch size

しかし上記の方法でもどうにも限界があるようで ある程度まではキレイに顔っぽいものが生成するようになっても、まだまだ崩れたものになってしまう場合も多い。 変化を観察していると10,000stepくらいでそれなりのクオリティになって、そこからは30,000stepくらいまで続けてもあまり変化が見られない、という感じだった。

どうにかもっと良い画像が生成されるように改善されないか、とlearning_rateをデフォルトより小さめにしてみたり、batch_size128よりもっと大きくしてみたりもしたけど、結局どれもそれほど効果は無さそうだった。

Discriminatorの出力を見る

とはいえ Generatorは無限の乱数入力から無限のパターンを生成するわけで、すべてがキレイな顔画像になるわけがない というのは当たり前といえば当たり前。ならば複数生成されるものから上手くいったものだけ自動で抽出できれば良いのでは?

ということで学習済みのGeneratorとDiscriminatorを使って、mini batchで生成される複数の画像をDiscriminatorに通した結果のsoftmax値の高い順に表示してみた。

    # 乱数mini batchから画像を生成する
    images = sess.run(dcgan.g(dcgan.z)[-1])
    # discriminatorの出力にsoftmaxかけたものを反転してtop_kを抽出
    # `0`を高く出力したもの `1`を高く出力したもの 上位10件ずつの値とindexが取れる
    values, indices = tf.nn.top_k(tf.transpose(tf.nn.softmax(dcgan.d(images)[-1])), 10)
    for x in sess.run([values, indices]):
        print(x.tolist())
    # top_kで得たindicesを使って生成画像から抽出し、縦横に連結
    rows = []
    for cols in tf.split(0, 2, tf.gather(images, indices)):
        rows.append(tf.concat(3, tf.split(1, 10, cols)))
    result = tf.squeeze(tf.concat(2, rows), [0, 1])
    # 余計な次元を削減してjpeg画像に変換して出力
    img = tf.image.encode_jpeg(tf.image.convert_image_dtype((result + 1.0) / 2.0, tf.uint8))

    filename = os.path.join(FLAGS.images_dir, 'out.jpg')
    with open(filename, 'wb') as f:
        print('write to %s' % filename)
        f.write(sess.run(img))

f:id:sugyan:20161012011133j:plain

上段が、Discriminatorのsoftmax出力が0で高かったもの上位。自分のDCGAN実装ではこれはDiscriminatorがGeneratorによる画像だと判定したもの。下段が、softmax出力が1で高かったもの上位 すなわち学習データと判定されたもの(うまく騙せたもの)、となる。

うーん、確かに下段のものの方がキレイに出来ているものが多いような気もするけど、別に全部が良いわけでもないし 上段にもそれなりのものが出てきてたりするし… これも以前の記事で確認した通り、モデルが判別する特徴は人間の感覚と全然ちがうからあまり当てにはならない、のかも知れない…。

Web UIで入出力を調べる

ならば入力の値を弄ってどうにかすることはできないか、と思ったのだけど Generatorはブラックボックスすぎて「どんな値を入力すると どんな画像が生成されるか」が直感的にはまったく分からない。

ので、入力値を色々変えて実験できるよう こんなWeb UIを作ってみた。

f:id:sugyan:20161012012411p:plain

入力乱数を16次元の数値として、それらは実際には小数値なのだけど分かりやすいように0-255の整数値に置き換えてスライダーなどで操作できるように。それらの値に応じてAPI経由でその入力値からGeneratorによる顔画像生成を行い結果を描画。そのときの入力値を32文字のhex stringで表現して再現に使えるようにする。

というもの。Reactとか勉強しながらMaterial-UIで作ってみた。

入力値を操作する

このUIで色々とランダムな入力で試してみると、例えばすごく崩れる顔がどんな入力から生まれるのかが把握できる。

82763953b2740fef4d321dde7af002f7 f:id:sugyan:20161012014750p:plain

75cd0382329c4a341e296530c615b674 f:id:sugyan:20161012014758p:plain

54795b1ef616f55d2cd32f288a3f41f2 f:id:sugyan:20161012014800p:plain

という感じに。 これらを幾つか抽出して雑に平均を取ってみると

samples = %w(
  82763953b2740fef4d321dde7af002f7
  75cd0382329c4a341e296530c615b674
  54795b1ef616f55d2cd32f288a3f41f2
  b21ce031415c8abb73d8200bc115476c
  b0064a3e5ec757ae09898814edd94264
  2e790129bd66adfc8796201ff947259a
  097c5a73700498603e43ab439a854a83
  2a57676c479b4953d1694c45074229a2
  584bcb88c0609c61161ef62cab740b98
  725f3fe61612a4becf920302e936b022
  618b23b2189ea810998968b7dc60e4b5
  0f254708a300a458f504d97fcba07442
)

lists = samples.map do |hex|
  hex.scan(/.{2}/).map(&:hex)
end
avg = lists.transpose.map do |a|
  a.inject(&:+).to_f / a.size
end
puts avg.map { |e| format('%02x', e) }.join
$ ruby average.rb
5b605461755d86826d6b6348b168598d

というのが得られ、これを入力として使ってみると…

5b605461755d86826d6b6348b168598d f:id:sugyan:20161012015218p:plain

と、およそ顔とも分からないようなすごいのが出力されることが分かる。

逆に、そこそこキレイに上手く生成されたものを集めて平均を取ってみると

samples = %w(
  e7a8fea0affc366aa0fc77911c54201a
  c5c3ede294e988a1e8ebb7941def3297
  a1b3f8d8be647c6775cd94e184bb4f08
  75dcabe39c9f8b7e908ecd88546e2c9d
  a582debdcf74d579b990ce7123a48675
  ede6a4fc6cdab7828677e7dd6a998880
  d6e1a99db03f44a29fc49c9427c70569
  fcd35bd348836cc7a18d92d29367196c
  e789ec78b2cf2ba5e5bd9f87723e913f
  f788ded2733f4eb7e7fa9acc2a4aae26
  b2c5b8d3a6b54bd5e7e1cc90838774cf
  dcd9b5bb87a86ec8c3dbace763a65d5c
  c8f8a9e199e244e0f59ab4db62466783
)

lists = samples.map do |hex|
  hex.scan(/.{2}/).map(&:hex)
end
avg = lists.transpose.map do |a|
  a.inject(&:+).to_f / a.size
end
puts avg.map { |e| format('%02x', e) }.join

cbbfc4c798a26ba1babeaeaf53865766 f:id:sugyan:20161012015859p:plain

と、とても自然なイイカンジの画像が生成されることが確認できる。

この「良い例」と「悪い例」の差分を取って、「悪い入力」から「良い入力」へ向かうベクトルを乱数入力値にオフセットとして加えてやれば、より良い結果が生まれやすいのでは!? ということでやってみた結果がこれ。

f:id:sugyan:20161012030450j:plain

入力値の範囲が狭められたことで ちょっと似たり寄ったりなものが多いような気もしないでもないけど、より高確度で比較的安定した顔画像が得られるようになった。 一応ちゃんとそれぞれ髪型や顔の角度・表情は違っているし、悪くないと思う。

この「入力値にオフセットを加える」手法で、例えば「左向きの顔が出力される入力」「右向きの顔が出力される入力」を調べて差分ベクトルを入力値に加えることで右向きばかりのものが生成されるようになったり、表情や髪・肌の色とか 色んな要素を調節しつつ生成できるようになることが期待できる。(まだそこまでは出来ていない。そういった特徴を抽出するのもなかなか面倒…。)

今後の展望

もうちょっと、入力値の良い取り方などは研究してみたいところ。金髪とかショートとか離れ目とか笑顔とか、様々な成分を指定して自由に自分好みの顔を生成できるようになる、のが目標かな。

あとは複数の顔をモーフィングで遷移するアニメーションとか作ると面白そうだと思っているので、そういうのも生成するUIを作ってみたいと思っている。

そもそもDCGANでの生成がこれが限界なのかどうか。さらに改良する方法や、またDCGANではない生成方法も調べて試してみたいところ。

Repository

"Write Code Every Day" 1年

元記事
John Resig - Write Code Every Day
日本語訳
毎日コードを書くこと - snowlongの日記

この記事を読んだときは「へー」くらいにしか感じていなかったのだけど、

1年前の10月5日のjava-ja.OSSでのid:t-wadaさんの発表を聴いて、実際に身近な知っている人たちが実践しているのを知って、「よし自分もやってみよう」と始めたのがきっかけ。

www.slideshare.net

元記事で

  • ブログ記事を書いたりすることは、コードを書いた上でのプラスアルファでなければならない(まずコードを書き、余裕があればそれに加えて他のことをしてもよい)
  • インデントの修正やコードの見た目の調整は(書いたコードに)含めない。可能であればリファクタリングも含めない(これらの全ては、日中の仕事でなければ許可されることだ)。
  • 必ずコードは真夜中より早い時間に書かれること。
  • コードはオープンソースとしてGithubにアップすること。

というルールを定めていたのは出来るだけ従うようにしたけど 実際ぜんぜん出来なくて、package.jsonGemfile.lockの更新だけになってしまう日もあったり 日付を跨いでのcommitで稼いでしまっている日もあったけど、「とりあえず1日も欠かさずに1年は続ける」というのだけ決めていたのは守った。

「草を生やす」は、それだけを目的にしてしまうのはアレだし賛否あるでしょうけども 分かりやすくモチベーションになるし、ってことで何となく始めるときに決めた。 それを継続するために、アイドルの遠征いくときもMBP担いでいって出発前の空港や遠征先のカフェでコード書いたり、女の子と飲みに行ったときも酔っ払いながらスマホからコード編集してpushしたり、それなりに泥臭く頑張って続けてみた。

終盤(9月末)は会社から出すオーエスエスにコントリビュートする仕事?をさせていただいたおかげでラクになりました

なんだかんだで緑色で埋まっているとやっぱり嬉しい気持ちにはなる。

成果

…は正直ほとんど無くて、

とかそれくらい。「OSS!」って感じのものはなく趣味のアイドル顔識別関連が主で、学習パラメータの数字を変更しただけ、の日なんかもあってショボい感じはある…

でも普段仕事では使わないRuby, Python, JavaScriptとかをそれなりに好き勝手に書くことができて楽しかった。

変化

  • コードのことを考える時間は圧倒的に増えた

これを始めるまでは四六時中 推しのアイドルちゃんのことくらいしか考えてなかったと思うけど、「毎日何かを書く」って意識していると 通勤中とか昼食で丸亀製麺まで歩く間のちょっとした時間とかも「今日は何をやればノルマ達成できるかな…」とか考えるようになって 短時間で(その日のうちに)pushできる形にするにはどうしたら…とか 実際に作業している時間以上に影響は大きかったと思う。

  • 「毎日続ける」ことに慣れる

アイドル顔画像識別の学習データ集めとか、実際ちょっとした思い付きでは簡単には出来なかったと思うのだけど、これを続けている中で「毎日少しずつ方法を改善しながらコツコツやっていけばそれなりの量が貯まるだろう」っていう予測が出来たし続けていける自信もあったし こういう少し大きめのプロジェクト(?)も毎日続けて積み重ねることである程度のものが達成できる、という感覚が掴めた、と思う。

  • 自信…?

当然、始める前は「こんなの自分に出来るかな…?」とか思ってたけど、意外とまぁなんとかなって 話を聞いて思い立った翌日からすぐに始めてちゃんと1年間は続けることができたので 自分の行動力と継続力を多少は信頼できるようになったかな、と。

今後

とはいえやっぱり「毎日欠かさず」ってずっと続けるのは疲れるので、これからは 今までの感覚は維持しつつも「毎日」にはそれほどこだわらずにやっていこうかな〜と。

でもまぁなかなかエキサイティングな1年を過ごすことが出来たと思うので、やったことない人は挑戦してみるのをオススメします。

ISUCON6 予選敗退で終わってしまった

ISUCON6id:uzulla さんと id:moznion さんとチーム「[=======> ] 80%」で出場して、予選通過できず敗退しました。

残念…。

前日までの準備

かくかくしかじかでこの3人で出場することには決まったものの、とにかく2人とは一緒に仕事もしたことないし 顔合わせして練習が必要ですね、ということで8月末と9月前半、2回集まって過去問を使って練習会をした。

まず使用言語。2人はPHP強いけど僕は1行も書いたことないし、Goは3人ともある程度は出来そうだけどまぁやっぱりPerlが(最近は全然さわってないにしても)業務でも使って慣れているし安心かな、ということでPerlで。

役割分担としては、

という感じで。

インフラ・ミドルウェア周り完全にuzullaさんにお任せすることになってしまい、負担が大きくなかなかアプリのコードまで見てもらう余裕を作れず申し訳なかったです。ベンチマークまわしてログを解析してボトルネックを見つけ出す、とかもほぼ任せっきりでやってもらってしまいましたが とにかくそのへんのオペレーションやチューニングは完全に信頼できてとても良い仕事をしていただいて、本当に感謝しております。

アプリのコードはmoznionさんと練習もして息を合わせて作業することができて良かったです。お互いローカル環境でも動作確認程度には動かせるようにしつつコードを読んで動作・仕様を把握し。ログからボトルネックを確認できたらまずそこを解消するためにどう変更するか方針をちゃんと相談し、分担できるところは分担して作業。出来るだけちゃんとプルリクを作ってレビューした上でmerge。お互い無駄なミスや手戻りすることなくスムーズに出来たんじゃないかと思っています。

場当たり的に重そうなところを変更していくのではなく、しっかりログを解析してボトルネックを把握して潰し効果を確認し、また次のボトルネックが分かったらそこを潰す、という流れを繰り返す。2回の練習でその感覚はかなりついて手応えはあった(し、実際当日も効果あった)ので 練習会は本当にやっておいて良かったと思います。

当日・前半

気兼ねなく声出して議論しながら作業できる会議室を確保できたのでそこに集合、準備万端で競技開始。

uzullaさんがAzureについてはかなり使い込んで詳しくなってくれていたおかげで最初のサーバ立ち上げや初期セッティングは練習通りでスムーズに。

こちら側も練習通りにコードをGitHub repositoryに入れてローカルで環境作って…(Perl 24.0なんて入ってなかった!!1 plenv installからだ!!!)

アプリ・DBが2個に分かれてるのか…面倒だな、はやめに統一できるならしてしまいたい、ということでボトルネック計測と並行でその作業から。 isutarの方はごっそり移行できそうだったので慎重に変更しつつisudaに統一して2アプリ間での無駄なやりとりを削減。 (11:30頃)

MySQLクエリ解析の結果keywordを長さ順に全件取得してるのが重い、ということが分かったので、これはとりあえず変更されるものではないしlengthカラムを別に保持してそれで引くようにしよう、と。 (11:45頃)

これだけでそれなりの効果が出て、18,515点で一気に断トツ首位へ。このへんまでの流れ練習通りでとてもスムーズに出来て良かった。 f:id:sugyan:20160918225859p:plain

そもそもこのkeywordの長さ順クエリってhtmlify時に置換すべきキーワードを探すための正規表現を作るだけのためのものだし、毎回引く必要はなくて正規表現はアプリで保持しておいてentryが追加・削除されたときだけ(実際にはベンチマークで削除操作は無かったっぽい?)作り直せばいいじゃないか、ということでPOST時に作ってRedisに正規表現文字列を持たせるよう変更。encode_utf8とかdecode_utf8を挟まないとRedisに入らない、とかでハマってPerlムズい、ってなったりしたけど、そんなに無駄にハマり続けたりすることもなく完了 (12:40頃)

これもしっかり効果が出て、追いついてきていた2位チームを一気に引き離して 40,321点に。 f:id:sugyan:20160918230952p:plain

ここまでは良かった…。

当日・後半

ここからtotal_entriesをRedisに載せたり starをRedisに載せる闇改修をmoznionさんが行ったりしている間に、14時過ぎに10万点超えのチームに一気に抜かされ。

やはりhtmlifyの結果そのものをキャッシュしていかないとダメだ、という話になったが、entryが追加されるごとに正規表現も変更されhtmlifyの結果も変更されないといけないはずだから、単純にはキャッシュできないよな…と悩む(ここでもっと更新されるべき内容を精査するとか考えを巡らせるべきだった)

とにかくアクセスの多いのは/だから、ここで表示される10件だけなら毎回POSTされるたびに作っても1秒以内のレスポンスは出来るし効果があるでしょう、ということで新規entryが投稿されるたびに/で表示される10件だけhtmlifyの結果をRedisに載せて使うように。 (15:10頃)

これで64,434点くらいまでは伸びたが、10万点にはとても届きそうにない…

htmlifyの結果を全部をキャッシュに載せようとするとどうしてもPOST時に関連entryの結果を更新できそうにない、別プロセスでどうにか出来ないか…?と取り組み始めて、アプリ内でforkしてみたりRedisをJobQueue代わりに使って裏のワーカープロセスでhtmlify結果を作ってキャッシュに載せる、とかを試みたが、どれも上手くいかずスコアは伸びず… 結局この悪戦苦闘が17時過ぎまでいっても大きな効果を出せず、すべて戻すことに…

その間にサーバ側で様々なチューニングを行ったuzullaさんの変更がプラスされて、再起動テストなどしていたところ 72,018点という結果が出たのでそれを最終スコアとして時間切れで競技終了。

最終的なコードなどはこちら : https://github.com/uzulla/isucon6-q

反省

予選通過ラインは9〜10万点ということで、どうにもそのラインまで届かせることはできなかった。 序盤で確実にスコアを伸ばしていったのは良かったが、そこから先の大幅な点数アップに辿り着けなかったのは力不足だった。

ある程度のリンク作成ミスがあってもスコア計算としてはとにかく成功GETレスポンスが多ければそのミスを上回る効果があった、という話を後で聞いて、もっとミスを恐れずに貪欲にキャッシュしていくべきだったか、と思ったり。そのへんは試算が足りていなかったし、moznionさんも提案して実装しようとしてくれたところに「元の実装と挙動が変わることになるのは納得いかない」と変なこだわりを持って撥ね除けてしまったのが良くなかった。 そもそもstarのキャッシュとかで結構元の実装と挙動違う実装にしていてベンチマークを通していたのだからそんなこだわりを捨ててとにかくベンチマークが通って高い得点を出すことができるのなら色々試してみても良かった。

というかキャッシュに関しても「POST時に全部作ってGET時は読むだけ」みたいな形でやり始めて思考停止してしまっていたので良くなかった。「POST時には関連するものだけ破棄して、GET時に作ってキャッシュする」という形をちゃんと想定していたら正しい実装でももっとスコアは伸ばせていたのかもしれない。どういうわけか当日はこういう考えが思い浮かんでいなかった。

感想

取り組み甲斐のある良い問題で、楽しく挑戦することができました。 他の出場者の方々もレベル高く、やはり簡単には勝てないのだな、と痛感いたしました。様々な参加エントリを読んで復習していきたいと思います。

出題・運営の皆様ありがとうございました!

一緒にチームを組んで戦ってくださった id:uzulla さん、 id:moznion さん、ありがとうございました!!

Links

TOKYO IDOL FESTIVAL 2016 のタイムテーブル画像化ツールを作った

日本最大規模のアイドルの祭典・"TOKYO IDOL FESTIVAL"(通称TIF)。

今年は明日からの8/5〜8/7の3日間の日程で、始まります。

TOKYO IDOL FESTIVAL 2016

で、こういったフェスってステージが複数あって観たいステージが幾つもあるなか、どの時間にどれを見るか とか決めるのが大変で、そういうのを解決するためのツールみたいなのを多くの人たちが作っていたりしますね。 今回のTIF 2016でも幾つかそういうのが有志によって公開されています。

今年はメインのタイテ情報がJSONで取得できるようになっていたのでこういったツールも作りやすい環境になっていたかと思います。

で、自分も何か作ろうかな…と思って

自分の場合、タイテ情報ってけっこう画像をそのままダウンロードしたりWebで載ってるものをスクリーンショットで保存して使うことが多くて。ネットワークの繋がらないところでもすぐに確認できるのが便利なので。

なので自分で組んだ自分だけのタイテを一枚絵の画像でダウンロードできるようなのがあればいいな、と思ったのでそういうのを作ってみた。

条件を指定して絞り込んで表示した一覧から自分が行きたいものだけを選択すると、それだけを抽出してこういった画像を生成する、というだけのもの。

↓例 f:id:sugyan:20160804230856p:plain

技術的にも難しいことは特にしてなくて

  • サーバサイド: RailsJSON APIを用意
    • 公式タイムテーブル情報は定期タスクでfetch & parseして整形しキャッシュに突っ込んでおいてそれを読むだけ
    • 選択したものを元にRMagickでテキスト描画しつつ画像を生成
  • クライアントサイド: ReactでUI生成
    • データは全件取得した上で絞り込みによる表示変更
    • 生成結果の画像を表示切り替えで雑にSPAっぽく

という感じ。

Repository : https://github.com/sugyan/tif2016-mytt

JSのビルドには最近webpackを使うようにしていて、開発時にはwebpack-dev-serverを使った。

などを参考にして、webpack-dev-server --inlineなどでソース変更時に再ビルドとリロードが自動で走るようにしつつ配信しておき、Rails側では

module ApplicationHelper
  def javascript_include_tag(*sources)
    if Rails.env.development?
      opts = {
        src: '//localhost:8080/javascripts/main.js'
      }
      return content_tag(:script, '', opts)
    end
    super(*sources)
  end
end

みたいな感じで開発時のみJSがそっちを向くように設定したら捗った。

Asset Piplineを使わずに/public以下に直接成果物を配置すればいいかな、と思ったけど本番の更新時にキャッシュとか制御できるか不安だったので結局一度/assets以下に吐いてAsset Piplineでfingerprint付きのpathで配信するようにした…。


と、こんな感じで作ったアプリをHerokuにdeployしてちょっとお知らせしてみたところ 色んな界隈のヲタクの方々から400RTくらい拡散していただいて。どれくらいPVあったかは分からないけど 使ってくれたヒトが作った画像を載せてくれて「それどうやって作ったの?」「ここから!」みたいにTwitter上でクチコミで広まったりして、思っていた以上に使ってもらえて、作った甲斐あったわー という感じでとても嬉しい。

「TensorFlowはじめました」を読んだ

著者の有山さんとは、TensorFlowでの独自の画像データセットの分類に取り組む同士(?)として勉強会などでお話する機会があり、そんな縁もありまして有り難いことに献本ということで読ませていただくことができました。

第1章の「TensorFlowの基礎」では最初にまずデータフローグラフの「構築」と「実行」で分かれているという概念について、丁寧に説明されていてとても良かったです。いきなり何も知らずに公式Tutorialだけ始めていた自分は、こういう概念について理解するのが遅かった…。

第2章ではCIFAR-10の学習モデルと評価。公式Tutorialの英語を問題なく読めて ある程度のCNNの知識があれば困らないかもしれないけど こうやって日本語で解説されているものがあるというのは(数ヶ月前の自分のように)新しく始める人にとってはとても有り難いだろうなと思います。

第3章ではもうちょっと踏み込んで、TensorFlowでのデータ保存・読込や可視化について。

第4章「CIFAR-10奮闘記」がとても面白かった。 モデル構成や学習データを変えつつ評価結果を確認し、と試行錯誤を繰り返しながら少しずつ正答率を上げる方法を模索していく。自分もアイドル顔識別でそういった試行錯誤を続けていたので とても共感できる内容でした。何をどうすれば正答率が上がる、ってなかなか感覚的な部分もあったりして 最良の道はそう簡単には見つけだせないんですよね…。


というわけで新たにTensorFlowを使って機械学習・画像分類とかをやってみたい、というヒトにはとてもオススメの1冊でした。本当にあと数ヶ月はやくこれが出ていれば僕も最初の躓きが少なかっただろうに…!

巻末の参考文献に私の「すぎゃーんメモ」も載せていただいていて、非常に光栄であります。 この先も続く挑戦の道のり、楽しみにしています!ありがとうございました!!