React Nativeで作った新アプリについて(5日間連載)

こんにちは投稿開発部の丸山@h13i32maruです。

今日から5日間、本ブログに投稿開発部メンバーで連載記事を書かせていただきます!

いきなり「投稿開発部で連載記事」と言われても何のことかわからないと思うので、まず投稿開発部について簡単に紹介させてもらいます。

投稿開発部は「クックパッドに投稿されるコンテンツ全般」について責任をもっている部署なのですが、中でもレシピ事業の根幹であるレシピ投稿者向けのサービス改善に力を入れています。レシピ投稿者向けのサービス改善は「どうすれば継続的に投稿したくなるのか?」「どうすれば投稿をはじめてみたくなるのか?」の2点に答えを出すことを目標に日々サービス開発に励んでいます。

そこで、本連載では投稿開発部が今年メインで取り組んでいる「クックパッド MYキッチン」という新しいアプリについて5人のメンバーで紹介させていただきます。

1日目(vol1)では「クックパッド MYキッチン」ができるまでの話をちょっとしたストーリー仕立ての文章で紹介させていただきます。普段の記事と比べると技術的なトピックは少なめなので、肩の力を抜いて気軽にお読みください。

そして、2日目以降は以下のような内容を予定しております。

「クックパッド MYキッチン」とは

今年、投稿開発部では「クックパッド MYキッチン」というアプリ(以下MYキッチンアプリ)の開発に注力しています。このアプリはレシピ投稿者が使いたいと思えるアプリを目指して、これまでのクックパッドアプリ上での体験をリデザインして作られているものです。

f:id:h13i32maru:20180413135704p:plain:w100 f:id:h13i32maru:20180413135755p:plain App Store / Google Play

ではなぜ既存のアプリ上でレシピ投稿者向けの体験をリデザインしなかったかというと、「開発・検証のスピードを上げるため」というのが大きな理由です。そのために「関わる人を少なくして、意思決定を速く」と「機能の制約を受け入れて、実装を速く」ということを行っています。

特に後者の「機能の制約を受け入れて、実装を速く」について、MYキッチンアプリではReact Nativeを採用してフルスクラッチで作られています。また、CodePushについても試し始めています。

ではここから、いかにしてMYキッチンアプリが出来上がっていったのかを紹介していきます。

Prototype Labs(2017年春)

時は遡り2017年春、当時同じチームだったiOSエンジニアがReact Nativeをアプリのプロトタイピングに使えないか調査していました。当時、彼が書いた社内ブログにはこのように書いてありました。

年末年始でReactNativeの調査をしていました。

目的はReactとcssの知見でネイティブのアプリが作れれば、アプリのプロトタイプできる人口を増やせるのでは?というところ。

xxx(とあるプロジェクト)の初期でいくつかの機能を試していたときに、「アプリに組み込んで手触りを試したい」という欲求があったものの ネイティブがかける人は少ないし、ネイティブでレイアウトを変えるトライアンドエラーはどうしてもコストが高いので 何か別の手段で試せた方が良いのでは・・?と考えたのがきっかけです。

この彼の取り組みを横目でみながら、「React Nativeというものがあって」「アプリのプロトタイピングに使えるかもしれない」という情報を得た僕は、自分でもちょっと試しに触ってみることにしました。

当時、どういうふうに試し始めたのかははっきりとは覚えていないのですが、「アイテムのリスト画面」と「アイテムの詳細画面」という基本の画面を作ったと思います。そして色々触ってみた結果「開発スピードをあげるために、完成度・機能・UI・パフォーマンスなどの制約を受け入れることができる」というものに向いていることがわかりました。そう、まさにプロトタイピングに向いていると思ったのです。*1

さらに、React Nativeを使ったプロトタイピングなら、これまで静的なプロトタイピング(ペーパーモック、InVisionなど)では諦めるしかなかった点もカバーできると思いました。

  • 日常生活で使うことができるプロトタイプ
  • 実際のデータを使ったプロトタイプ
  • データを書き込むことができるプロトタイプ

というわけで、僕の中の「React Nativeでプロトタイピング環境を作りたい」という欲求がむくむくと湧き上がっていきました。なので鉄は熱いうちに打ての精神で、React Nativeを使った社内用のプロトタイピング環境「Prototype Labs」を作りました*2

Prototype Labsの中身はというと、React Nativeを社内のプロトタイピングに特化させるために、薄いラッパーと幾つかの便利機能を追加したものです。具体的には、ファイルの配置ルール、デプロイの仕組み、ドキュメントの構築、認証周りのデフォルト実装、サーバサイドのAPIを簡単に呼び出せる仕組み、よく使うカラー・レイアウトの提供、etcという感じです。

Prototype Labsとは APIリファレンス
f:id:h13i32maru:20180413135849p:plain f:id:h13i32maru:20180413135902p:plain

ドキュメントはESDocというJavaScript向けのドキュメンテーションツールで作りました

特にドキュメント周りは力を入れて整備しました。というのも、社内のデザイナー(HTML, CSS, JSに多少触れたことがある人)にも使ってもらえるようにというのを目標の1つにしていたからです。実際、デザイナーとペアプロ的にPrototype Labsを触ってもらい、簡単な画面を作ってもらったりもしました。

そして、Prototype Labsを使って「料理まとめ(自分が投稿したレシピを自由にまとめられるもの)」という機能のプロトタイピングをデザイナーと一緒に行いました。結果、実際に日常使いをしながら議論をすることができ、主要な要件を決めるのに非常に役立ちました。料理まとめはその後、iOS版のクックパッドアプリに実装され、現在、本番環境で元気に動いています。この時一緒にプロトタイピングをしたデザイナーが当時の様子を「React Nativeで作る 「触れるプロトタイプ」の活用」というタイトルで発表しているので興味がある方は見てみてください。

裏クックパッドアプリ(2017冬)

Prototype Labsを作った後、しばらく業務ではReact Nativeを触ることはありませんでした。

一方で、プライベートでは自分の料理レシピをクックパッドに投稿しはじめました。これまでもレシピはGoogle Docsやブログなどに書き散らかしていたのですが、それらをせっかくなのでクックパッドに集約しようと思い、どんどんレシピを投稿していきました。そうするとレシピ投稿者の視点でクックパッドアプリを見るようになり、「今までレシピ投稿者向けの開発はしたことがなかったけど、来年(2018年)はレシピ投稿者向けの開発をしたいな」と思うようになりました。

で、それなら「レシピ投稿者(自分)が使いたくなるクックパッドアプリ」を作ってしまえば良いんだと思い立ちました。またしても鉄は熱いうちに打ての精神で、React Nativeを使ってオリジナルのクックパッドアプリをまるっと作り変えてしまおうと開発にとりかかりました。これが後にMYキッチンアプリの土台となるもので、社内の一部からは「裏クックパッドアプリ」「RNクックパッドアプリ」などと呼ばれることになります。

上述したとおり、裏クックパッドアプリはレシピ投稿者が使いたくなるというのを目指していたので、コンセプトや体験はオリジナルのクックパッドアプリとは大きく異なります。具体的には・・・という話をしたいのですが、ここに書くのは長くなりそうなのと企業秘密というわけで詳細は伏せておきます。この話を聞いてみたい!という方がいらっしゃれば、TwitterのDMなどから是非とも僕までコンタクトしていただければと思います。

モード切替 キッチンモード さがすモード
f:id:h13i32maru:20180413140026j:plain f:id:h13i32maru:20180413140034j:plain f:id:h13i32maru:20180413140040j:plain

特徴は左下のクックパッドアイコン/ユーザアイコンからモードを切り替えるという点です

その他に気をつけたこととしては、オリジナルのクックパッドアプリにある機能はほぼ全て使えるようにするという点です。何故かと言うと、僕は普段使いのアプリをオリジナルから裏クックパッドアプリに完全に移行したいという考えがあったからです。そうしないと、結局オリジナルのアプリを使ってしまい、裏クックパッドアプリが中途半端なものになりうまく改善できなくなってしまうと危惧したからです。

そんなこんなで、コンセプトや体験の見直しをして、それを実現させる機能を実装し、さらに既存の主要な機能の実装を完了させ、僕は裏クックパッドアプリに完全に移行することができました。開発に取り組み始めてから2ヶ月ほどかかりましたが、実際に使った時間は10日間ほどでした。しかもこの期間でAndroidとiOSの両方を作ることができたのもReact Nativeの強みだと思います。裏クックパッドアプリを社内にリリースした時のブログに同僚が以下のようにコメントしてくれました。

10日間でここまで作り上げられるのはプロトタイピングにすごいインパクトがあることだと思いました。

この速さなら自分が鍵だと思ってるコンセプトを形にして提案することで、細かい調整(人的リソース、仕様共に)に時間を取られずに本質的な議論を始めやすくなるように思います。

プロダクト開発って結構一部を変えようと思っても全体を整えていかないといけない(けど時間がないからスコープを絞って細部の変更に留まってしまう)ということがありがちだと思うのでアプリ全体を素早く作り変えて試せるのは大きな価値だと思います。

クックパッド MYキッチン(2018年春)

そして、自分で毎日裏クックパッドアプリを使ってみて、この新しいアプリに未来を感じました。なので、2018年は裏クックパッドアプリを使ってレシピ投稿者向けの改善に取り組んでいくことを決めました。

そこから裏クックパッドを「クックパッド MYキッチン」に改名し、デプロイの仕組みやコードの整理、足りていない機能の追加やデザインの修正、アイコンの作成などを経て、2018年3月にAndroid/iOSともにプロダクションにリリースすることができました。

というわけで、続く3日間ではプロダクションリリースするために取り組んだ技術的な話、アプリアイコンの話、そして最終日はユーザの課題と解決策をどのように探っているかの話を各メンバーが紹介してくれます。お楽しみに!(ちなみに明日からの記事は今回のようなストーリー仕立てではなく、いつもの雰囲気に戻ると思うのでご安心ください)

自己紹介

最後になりましたが、簡単に自己紹介をさせていただきます。

僕は2014年にクックパッドに入社しWebやAndroid周りの機能実装を担当していました。その後に幾つかの機能のPMを担当して、今年から投稿開発部のマネージャー(部長)という役割を担っています。

プライベートではESDoc(JavaScriptのドキュメンテーションジェネレーター)やJasper(GitHub向けのIssueリーダー)というソフトウェアを開発しています。あと、CodeLunch.fmというポッドキャストをやっていたりもします。

僕個人に関してもっと詳しい話はForkwell Pressのインタビュー記事でお話させてもらっているので、興味のある方はご覧ください。


この連載を通して「仮説を素早くプロトタイプにしていく開発」や「React Nativeを使った開発」などに興味を持たれた方がいらっしゃれば、丸山(TwitterのDM)までお気軽にご連絡ください!もちろん、採用ページから応募していただくのも大歓迎です😊

最後に、この記事を読んだ印象を簡単なアンケートでご回答いただけるとうれしいです!

アンケートリンク

*1:ちなみに、プロダクションに使えるかどうかはどちらかわからないというのが当時に意見でした

*2:社内のエンジニア数名にも手伝ってもらいながら

クッキングLIVEアプリcookpadTVのコメント配信技術

こんにちは。メディアプロダクト開発部の長田です。

この記事では、クッキングLIVEアプリcookpadTVのLIVE中のコメント配信について工夫したことを紹介したいと思います。

2018/3/28 (水)に開催されたCookpad Tech Kitchen #15資料も合わせてご覧いただけると、分かりやすい部分もあるかと思います。

cookpadTV

cookpadTVでは、料理家や料理上手な有名人による料理のLIVE配信を視聴することができます。iOS/Androidのアプリがリリースされおり、LIVE配信を通して、分かりづらい工程や代替の材料の質問などをコメント機能を使って質問することができます。また、他のLIVE配信アプリのようにハートを送ることでLIVEを盛り上げることができます。

以下では、LIVE中のコメント配信を実装するにあたって私が課題だと感じたものと、それらをどう解決したのかを紹介します。

コメント配信の課題

コメント配信には次のような課題があると感じています。

1つ目は、パフォーマンスの問題です。LIVEの日時に合わせてユーザーが同時に集まるので、人気な配信ほど多くのユーザーがサーバーにリクエストしてきます。また、コメントだけではなくハートを送信する機能を設けており、これは気軽に連打できるようにしてあるので、リクエスト数も多くなることが予想されました。

2つ目は、双方向通信です。cookpadTVでは「料理家や有名人にその場で質問できる」のを価値にしていて、ユーザーのコメントは演者が読み上げて回答してくれたりします。 演者とユーザーのコミュニケーションと、それを見ている他のユーザーの体験を損なわないようにするために、サーバーとアプリの情報をある程度同期させておく必要がありました。

パフォーマンスを出すために

コメントを受けるAPIは別アプリケーションとして構築しました。コメントを受けるAPIはその他のAPIとは特性が違うので、コメントを受けるAPIだけをチューニングしやすくなるからです。 以下では、このコメントを受けるAPIサーバーのことを メッセージサーバー と呼び、その他のAPIサーバーを 通常のAPIサーバー と呼ぶことにします。*1

f:id:osadake212:20180412145148p:plain

まず、実装言語はgolangを採用しました。採用理由は以下が挙げられます。

  • 並列処理が得意な言語なので、同時接続を受け付けやすい
  • 後述のFirebaseを使うためのAdmin SDKが提供されていた
  • golang書きたかった

クックパッドはRubyの会社というイメージがあると思いますが、特性に応じてRuby以外の言語を選択できるよう、hakoを使ったDockerコンテナのデプロイ環境が全社的に整備されており、他のサービスでもRuby以外の言語で実装されているものがあります。*2 *3

また、hakoによってDockerコンテナがECSにデプロイされるようになっており、必要に応じてECSのAuto Scalingの設定ができるので、このメッセージサーバーも設定しています。これにより、アクセスが増えてきてサーバーリソースが消費され始めたらスケールアウトして、アクセスが減ってサーバーリソースに余裕がでてきたらスケールインするようになります。 また、Auto Scalingが間に合わないことが予想される場合は、予めコンテナ数を増やしておくようにしています。*4

さらに、WebアプリケーションはDBアクセスがボトルネックになりがちだと思うのですが、メッセージサーバーではDBにアクセスをしない、という選択をしました。一方で、DBにアクセスしないので認証と永続化について工夫する必要がありました。

認証については、メッセージサーバー用の寿命の短い認証情報(トークン)を通常のAPIサーバーで発行しておき、それをキャッシュに乗せておきます。各アプリはそのトークンを乗せてリクエストするので、メッセージサーバーはキャッシュを見に行くことで認証を実現しています。

また、永続化については非同期で行うようにしました。 コメント/ハートは後述のFirebase Realtime Databaseを使って各アプリに配信されており、LIVE配信中に永続化できなくてもよかったので非同期で行う選択をしました。

永続化の流れは、fluentdを使ってコメント/ハートのデータをS3に送ったあと、弊社のデータ基盤を使うことで、Redshiftに継続的に取り込まれるようになっています。*5 さらに、Redshiftに入ったデータは、Kuroko2を使ったバッチ処理によりMySQLに取り込む流れになります。*6

f:id:osadake212:20180412145141p:plain

これらの工夫をして、直近の配信ではピーク時 5,100rpm のメッセージを無事捌くことができました。

双方向通信

コメントやハートのやり取りで使用する、iOS/Androidアプリとの双方向通信を行うためにいくつかの手段を検討しました。

などを検討した結果、最終的にFirebase Realtime Databaseを使うことにしました。選択した理由としては、

  • iOS/AndroidのSDKが提供されており、アプリの実装工数が減らせる
  • 社内の他プロジェクトで導入されており、知見があった

というのが挙げられます。

また、Firebase Realtime Databaseに直接アプリが書き込むのではなく、以下の図のように、一度メッセージサーバーがリクエストを受け付けて、その内容をFirebase Realtime Databaseに書き込むようにしました。こうすることで、認証と永続化を実現しています。 つまり、Firebase Realtime Databaseをストレージとしてではなく、イベント通知をするために利用しています。これに関しては、この後のデータ構造の工夫と合わせて詳しく説明します。

f:id:osadake212:20180412145041p:plain

Firebase Realtime Databaseを使うことにしたので、データ構造を工夫する必要がありました。

cookpadTVでは、データ転送量を抑えるために最新のコメントだけを保存するようにしました。 具体的には以下のようなJSON構造にしています。(これはイメージなので実際のものとは異なります。)

{
  "latest_comment": {
    "user_id" : 1,
    "text": "こんにちはー"
  }
}

このような構造にしておいて、 latest_comment を上書き更新することで、各アプリに配布するデータは最新のコメント分だけになるので、転送量を抑えることができます。過去のコメントはアプリ側で保持しておいて、LIVE中に受け取ったデータは遡れるようになっています。

ただしこのデータ構造には、途中からLIVE配信を見始めたユーザーは過去のコメントを見ることが出来ないという課題が残っています。 この課題に関しては、直近のコメントはいくつか保持しておく、というものと、非同期での永続化のラグを短くした上でAPIでコメントを返せるようにする、という2つのアプローチのあわせ技で解決したいと思っています。

まとめ

この記事では、cookpadTVのLIVE中のコメント配信について工夫したことを紹介しました。 最後になりましたが、この記事がLIVE配信サービスの開発について、少しでもお役に立てれば幸いです。

*1:コメントだけではなく、ハート等、他のメッセージも受けるのでメッセージサーバーと呼んでいます。

*2:hakoの近況は本ブログでも紹介されています。http://techlife.cookpad.com/entry/2018/04/02/140846

*3:2018/02/10に開催されたCookpad TechConf 2018では、「Rubyの会社でRustを書くということ」というタイトルで弊社のkobaによる発表が行われました。 https://techconf.cookpad.com/2018/hidekazu_kobayashi.html

*4:LIVEコンテンツの集客予想に応じて、自動でコンテナ数を増やす仕組みを実装しています。

*5:本ブログの過去のエントリで、クックパッドのデータ基盤について紹介しているものがあるので、詳細はこちらを御覧ください。 http://techlife.cookpad.com/entry/2017/10/06/135527

*6:弊社のオープンソースで、WebUIが用意されているジョブスケジューラーです。

ディープラーニングによるホットドッグ検出器のレシピ

研究開発部の画像解析担当のレシェックです。techlife を書くのは初めてです。よろしくお願いいたします。

最先端の機械学習を使うためには、常に自分のスキルアップが必要です。そのために、毎日論文を読んだり、新しいオープンソースのコードを試してみたり、クックパッドのデータで実験しています。これはちょっと料理の練習と似ています。新しいモデルを学習させるのは料理をオーブンに入れるのと同じ気持ちです。オーブンの温度は学習率と同じで、低すぎだとよく焼けず、高すぎだと焦げてしまいます。しかし、ちゃんと他のリサーチャーの論文やブログの中のレシピを見ながら自分のデータでモデルを学習させると、失敗せずに済むかもしれません。

このエントリでは、そういった機械学習のレシピの一例を紹介します。

f:id:lunardog:20180405185342j:plain

このブログで使っているテスト画像はPixabayから取得した、Creative Commonsのライセンスの写真です。

概要

クックパッドは料理/非料理のモデルを開発しています。ここでは、このモデルのミニチュア版のレシピを紹介します。カテゴリは「料理」と「非料理」の代わりに、「ホットドッグ」と「非ホットドッグ」にします。そして、パッチ化した画像に対する認識モデルを使って、画像の中でホットドッグがどこにあるかを検出します。

調理器具

  • python
  • Keras
  • numpy
  • pillow (PIL)
  • jupyter notebook(お好みでお使い下さい。)

KerasはTensorflow、CNTKやTheano上で動く高水準のライブラリーです。Keras は特に画像データに対して、単なる学習以外にも前処理などでも様々な機能があります。

材料

KaggleからHot Dog - Not Hot Dogのデーターセットをダウンロードしてください。なお、ダウンロードするには Kaggle の登録が必要です。

ダウンロードした後、seefood.zipunzipしてください。

アーカイブの中に、2つのディレクトリtraintestがあります。

seefood/train/not_hot_dog
seefood/train/hot_dog
seefood/test/not_hot_dog
seefood/test/hot_dog

hot_dogディレクトリの中にホットドッグの画像が入っており、not_hot_dogの中にそれ以外の画像が入っています。新しい機械学習のレシピを開発する時はテストデータを分けるべきです。しかし、今回は画像が少ないので、テストデータも学習に使いましょう。

mkdir seefood/all
cp -r seefood/test/* seefood/train/* seefood/all

以降では、seefood/allのディレクトリを使います。

データ拡張

Keras のモバイルネットは(224px・224px)のフィックスサイズの画像しか認識できないので、これから学習や認識用にサイズを変換します。

IMG_SIZE=[224, 224]

テストデータを学習に使っても、このデータセットはまだ小さいので、データ拡張を使いましょう。

KerasのImageDataGeneratorは学習時に画像を一つずつ変換します。

import keras.preprocessing.image

image_generator = keras.preprocessing.image.ImageDataGenerator(
        rescale=1./255,
        shear_range=0.0,
        width_shift_range=0.1,
        height_shift_range=0.1,
        rotation_range=10,
        fill_mode="wrap",
        vertical_flip=True,
        horizontal_flip=True
)

上のimage_generator"seefood/all"のディレクトリで動かします。

train_generator = image_generator.flow_from_directory(
    "seefood/all",
    target_size=IMG_SIZE,
    batch_size=32,
    class_mode="categorical",
    classes=["not_hot_dog", "hot_dog"]
)

モデルの作り方

以下のレシピでは、3 個のモデルを 3 層のスポンジケーキのように積み重ねています。

  1. base_modelMobileNetです。転移学習のために使います。
  2. その上のpatch_modelは画像のパッチごとに分類できます。
  3. さらにその上のclassifierは「ホットドッグ」と「非ホットドッグ」の二値分類器です。

まずkerasimportします:

import keras

ベースとして、Googleで開発されたMobileNetというモデルを使います。

weights="imagenet"は、ILSVRCのコンペティションのデータセットで学習されたパラメタを使って、転移学習することを意味しています。

base_model = keras.applications.mobilenet.MobileNet(
    input_shape=IMG_SIZE + [3], 
    weights="imagenet",
    include_top=False
)

ベースモデルの一番上のフィーチャサイズは1024です。パッチレイヤが学習できるようにちょっと下げましょう。

drop1 = keras.layers.SpatialDropout2D(0.3)(base_model.output)
conv_filter = keras.layers.convolutional.Conv2D(
    4, (1,1),
    activation="relu",
    use_bias=True,
    kernel_regularizer=keras.regularizers.l2(0.001)
)(drop1)

パッチレイヤもConv2Dのタイプのレイヤです。この場合、softmaxを使えば、パッチごとに分類できるようになります。

drop2 = keras.layers.SpatialDropout2D(0.3)(conv_filter)
patch = keras.layers.convolutional.Conv2D(
    2, (3, 3),
    name="patch",
    activation="softmax",
    use_bias=True,
    padding="same",
    kernel_regularizer=keras.regularizers.l2(0.001)
)(drop2)

これでパッチモデルができました。

patch_model = keras.models.Model(
    inputs=base_model.input, 
    outputs=patch
)

パッチモデルをベースにして、最後の出力レイヤを追加して分類モデルを作ります。

pool = keras.layers.GlobalAveragePooling2D()(patch)
logits = keras.layers.Activation("softmax")(pool)


classifier = keras.models.Model(
    inputs=base_model.input, 
    outputs=logits
)

学習

ベースモデルは学習させません。

for layer in base_model.layers:
    layer.trainable = False

そして全体のモデルをcompileします。

classifier.compile(optimizer="rmsprop", loss="categorical_crossentropy", metrics=["accuracy"])

では、学習を始めましょう!

いくつか実験をした結果、以下のようにnot_hot_dogのクラスのclass_weightを高くするほうが良いことが分かりました。

%%time
classifier.fit_generator(
    train_generator, 
    class_weight={0: .75, 1: .25}, 
    epochs=10
)
Epoch 1/10
32/32 [==============================] - 148s 5s/step - loss: 0.3157 - acc: 0.5051
Epoch 2/10
32/32 [==============================] - 121s 4s/step - loss: 0.3017 - acc: 0.5051
Epoch 3/10
32/32 [==============================] - 122s 4s/step - loss: 0.2961 - acc: 0.5010
Epoch 4/10
32/32 [==============================] - 121s 4s/step - loss: 0.2791 - acc: 0.5862
Epoch 5/10
32/32 [==============================] - 122s 4s/step - loss: 0.2681 - acc: 0.6380
Epoch 6/10
32/32 [==============================] - 123s 4s/step - loss: 0.2615 - acc: 0.6876
Epoch 7/10
32/32 [==============================] - 121s 4s/step - loss: 0.2547 - acc: 0.6790
Epoch 8/10
32/32 [==============================] - 122s 4s/step - loss: 0.2522 - acc: 0.7052
Epoch 9/10
32/32 [==============================] - 123s 4s/step - loss: 0.2522 - acc: 0.7045
Epoch 10/10
32/32 [==============================] - 145s 5s/step - loss: 0.2486 - acc: 0.7164
CPU times: user 1h 4min 20s, sys: 2min 35s, total: 1h 6min 56s
Wall time: 21min 8s

このデータセットの場合、10エポックぐらいが良さそうです。パッチベースを使っているので、精度は100%にならないほうがいいです。70%ぐらいがちょうどいいです。

私の MacBook Pro では10エポックで20分ぐらいかかりました。

確認作業

画像とデータの変換のために、PILnumpyを使います。

import numpy as np
from PIL import Image

画像をインファレンスする前に、numpyのデータに変換します。

def patch_infer(img):
    data = np.array(img.resize(IMG_SIZE))/255.0
    patches = patch_model.predict(data[np.newaxis])
    return patches

そして、元の画像とインファレンス結果をビジュアライズします。

def overlay(img, patches, threshold=0.99):
    # transposeはパッチをクラスごとに分けます。
    patches = patches[0].transpose(2, 0, 1)
    # hot_dogパッチ - not_hot_dogパッチ
    patches = patches[1] - patches[0]
    # 微妙なパッチをなくして
    patches = np.clip(patches, threshold, 1.0)
    patches = 255.0 * (patches - threshold) / (1.0 - threshold)
    # 数字を画像にして
    patches = Image.fromarray(patches.astype(np.uint8)).resize(img.size, Image.BICUBIC)
    # もとの画像を白黒に
    grayscale = img.convert("L").convert("RGB").point(lambda p: p * 0.5)
    # パッチをマスクに使って、元の画像と白黒の画像をあわせて
    composite = Image.composite(img, grayscale, patches)
    return composite

まとめて、インファレンスとビジュアライズを一つのファンクションにすると、

def process_image(path, border=8):
    img = Image.open(path)
    patches = patch_infer(img)
    result = overlay(img, patches)
    # 元の画像と変換された画像をカンバスに並べます
    canvas = Image.new(
        mode="RGB", 
        size=(img.width * 2 + border, img.height), 
        color="white")
    canvas.paste(img, (0,0))
    canvas.paste(result, (img.width + border, 0))
    return canvas

では、結果を見てみましょう!

f:id:lunardog:20180405185418j:plain きれいですね!

f:id:lunardog:20180405185437j:plain ホットドッグの色はちょっと隣のコーヒーに移りましたが、ほとんど大丈夫です。

f:id:lunardog:20180405185457j:plain フォーカスが足りないところは認識にならなかったみたいです。なぜでしょう?学習データにフォーカスが当たらないホットドッグがなかったからです。

f:id:lunardog:20180405185342j:plain こちらも、左側のホットドッグはフォーカスが当たっておらず、モデルはホットドッグを認識できませんでした。

ホットドッグではない画像は? f:id:lunardog:20180405185526j:plain

f:id:lunardog:20180405185541j:plain

f:id:lunardog:20180405185558j:plain

f:id:lunardog:20180405185609j:plain

ホットドッグではない画像には、パッチはゼロやゼロに近い値になります。

まとめ

転移学習を使えば、データが少なくても、それなりの識別器が作れますね!

パッチごとの分類を使えば、画像の中の認識したいフィーチャーを可視化できます。

モバイルネット(MobileNet)のおかげで、CPU でもモデルを学習できます。

いかがでしたでしょうか。 クックパッドでは、機械学習を用いて新たなサービスを創り出していける方を募集しています。 興味のある方はぜひ話を聞きに遊びに来て下さい。