【出張開催レポ】Cookpad tech kitchen #11, #12 in 京都・福岡

こんにちは!人事部の冨永です。

2017/09/28~29の二日間に渡って、技術系イベント「Cookpad Tech Kitchen」を開催しました。クックパッドの技術的な知見を定期的にアウトプットすることを目的とする本イベント。#11, 12の今回は株式会社はてなさん(京都)とGMOペパボ株式会社さん(福岡)のお力をお借りして、京都・福岡での出張開催を実現しました!

テーマは一夜限りのPremium Talkと題して「各社開発の裏側」を発表。本イベントで初めて公開する情報や、ここだけでしか聞けない裏話など、興味深い内容が盛り沢山なイベントとなりました。

発表資料を交えてイベントのレポートをお届けします。

f:id:mamiracle:20171115180848j:plain

9月28日【京都開催 feat.はてな】Cookpad Tech Kitchen #11

f:id:mamiracle:20171115174411p:plain (Premium Talk in Kyoto)

京都のはてなオフィスにお邪魔しての開催。実は登壇者同士が学生時代のインターンシップ同期で良き仲間でありライバル(!)だったこともあり、和気あいあいとしながらも白熱した登壇となりました。

f:id:mamiracle:20171115174335j:plain (大盛り上がりだったQAディスカッションの様子)

それでは登壇内容をご紹介します。

「ScalaとPerlでMicroservices in production」中澤 亮太/株式会社はてな

1人目の登壇者である中澤 亮太さん(@aereal)は、アプリケーションエンジニアとしてご活躍されています。静的解析や静的型付けがお好きで、はてなブログチームのテックリードをお務めになられています。この日は、ScalaとPerlを使ったマイクロサービス化について登壇をしてくださいました。

speakerdeck.com

「イカリング2におけるシングルページアプリケーション」加藤 尋樹/株式会社はてな

2人目の登壇者である加藤 尋樹さん(@cockscomb)は、アプリケーションエンジニアとしてご活躍されています。モバイルアプリからWebサービスの開発まで積極的に取り組まれており、あの「イカリング2」の開発も担当されています。この日は、「イカリング2」の開発の裏側を教えて下さいました。

speakerdeck.com

f:id:mamiracle:20171115174355j:plain (美味しくておしゃれなケータリングに大喜び!)

はてなさんがいつもお世話になっているという京都のケータリングご飯を用意していただきました!発表を聞きながらでも楽しめるピンチョススタイルです。 イベントの後は、仕事終わりのはてなメンバーの方々と、行きつけだという居酒屋で打ち上げもしましたよ!はてなさん、ありがとうございました。

9月29日【福岡開催 feat.ペパボ】Cookpad Tech Kitchen #12

f:id:mamiracle:20171115174432p:plain (Premium Talk in Fukuoka)

翌日は、福岡のGMOペパボオフィスにお邪魔して開催!弊社社員は福岡初上陸のメンバーが多く、みんなのテンションが高かったことをよく覚えています。笑

f:id:mamiracle:20171115174808j:plain (こちらでもQAセッションが大盛り上がりでした)

それでは登壇内容をご紹介します。

「コンテナたちを計測すること - マネージドクラウドの今まさに開発中の裏側」近藤 うちお/GMOペパボ株式会社

3人目の登壇者である近藤 うちおさん(@udzura)は技術基盤チームに所属されています。mruby製のLinuxコンテナエンジン「Haconiwa」をリリースされたり、『パーフェクトRuby』『パーフェクトRuby on Rails』などを共著されたり広くご活躍されています。この日は、コンテナ計測について登壇してくださいました。

speakerdeck.com

「ムームードメイン ショッピングカート機能を支える技術」中村 光佑/GMOペパボ株式会社

4人目の登壇者である中村 光佑さん(@litencatt)ホスティング事業部ムームードメイングループに配属。Webサービス開発においてPHPやRuby、サービス知識など福岡支社の凄腕エンジニアたちに囲まれて日々多くのことを学ばれながら、毎日を全力で楽しんでいるそうです。この日は、ムームードメインに新しく追加されたショッピングカート機能の裏側についてお話してくださいました。

speakerdeck.com

f:id:mamiracle:20171115174515j:plain (ペパボさんのケータリングご飯もボリュームたっぷりで大満足!)

この日のご飯には、「minne」に出品されていたクラフトチーズも登場。お洒落で美味しくて感動の一言でした…。ペパボさん、ありがとうございました!

また今回、クックパッドからは2名が登壇しました。

「機械学習でサービスの常識を破壊する」杉本 風斗/クックパッド株式会社

サービス開発部エンジニアの杉本 風斗(@uiureo)。機械学習を使ったプロダクト開発に携わっています。入社当初はAndroid開発を担当するはずが、気づいたらPythonやRedshiftを使ったバックエンド開発が中心になっていたそうです。この日は機械学習を活用したクックパッドの新機能「料理きろく」について登壇しました。

speakerdeck.com

「巨大アプリにおける新規開発とチームビルディング」勝間 亮/クックパッド株式会社

現在サービス開発部長を務める勝間 亮(@ryo_katsuma)は、これまで新規事業、検索、投稿、会員事業などの部門での開発およびエンジニアリーダーを担当してきました。著書に「Webサービス開発徹底攻略」「すべての人に知っておいてほしいJavaScriptの基本原則」などがあり、この日は大規模なチーム開発におけるチームビルディングの方法について登壇をしました。

speakerdeck.com

f:id:mamiracle:20171115174454j:plain (楽しかった2日間の様子です♪)

まとめ

いかがでしたか?とても実りが大きい出張イベントだったため、また開催したいと思っています。次はどこの会社にお邪魔させていただこうかと考え中です…!

クックパッドでは毎月テーマを変えて技術イベントを開催しておりますので、ご興味のある方はイベント Connpassページをご覧くださいね。

▼イベントページ

cookpad.connpass.com

新しい仲間を募集中

クックパッドではレシピサービスの更なるパワーアップと、新規事業の開発に注力をしています!少しでも興味のある方は、まずお気軽にご連絡をお待ちしております!

■ Android アプリエンジニアの募集はこちら
https://cookpad.wd3.myworkdayjobs.com/ja-JP/jobs/job/-/Android---_R-000145

■ iOS アプリエンジニアの募集はこちら
https://cookpad.wd3.myworkdayjobs.com/ja-JP/jobs/job/-/iOS---_R-000162

■ Web アプリエンジニアの募集はこちら
https://cookpad.wd3.myworkdayjobs.com/ja-JP/jobs/job/-/Web--_R-000095

料理画像判定のためのバックエンドアーキテクチャ

サービス開発部の外村 (@hokaccha)です。

クックパッドのアプリには「料理きろく」という機能があります。 モバイル端末から料理画像のみを抽出して記録することで食べたものが自動的に記録されていくという機能です。

f:id:hokaccha:20171108125806p:plain

今回はこの料理きろくで画像判定をおこなっているバックエンドのアーキテクチャについて紹介します。なお、実際に判定をおこなう機械学習のモデルのはなしは以下の記事に書かれているのでそちらを参照してください。

料理きろくにおける料理/非料理判別モデルの詳細 - クックパッド開発者ブログ

また、以下のスライドでも料理きろくのバックエンドについて紹介されているのでこちらも参照してみてください。

処理の概要

ざっくりとした画像判定のフローとしては、次のようになります。

  1. クライアントアプリは端末内の画像を判定用に縮小してサーバーにアップロードする
  2. サーバーはアップロードされた画像を機械学習を用いて料理画像かどうかを判定する
  3. クライアントアプリは判定結果を受け取る

クライアントアプリが結果を受け取った後にオリジナルの画像をアップロードしたり、RDBMSにデータを保存するといった処理もありますが、今回は判定処理をクライアントアプリが受け取るまでの流れに絞って解説します。

初期のアーキテクチャ

プロトタイプの段階ではこの処理を単純に料理画像判定用のHTTPのAPIエンドポイントを用意し、そこに画像をアップロードして判定処理を行い、結果をレスポンスで返すという構成で実現していました。

f:id:hokaccha:20171108125340p:plain

Web APIはRailsとUnicornで動いているサーバーで、その後ろに実際に判定をおこなうPythonのサーバーがたっていてRailsのアプリケーションはそこと通信しています。

この処理は画像アップロードに加えて、料理画像判定の処理にディープラーニングを用いているのでそれなりに重たい処理になります。APIサーバーで利用しているUnicornは仕組み上プロセスがリクエストを処理している間他のリクエストをブロックするため、大量のアクセスがきた場合にI/O待ちによりworkerを使い切ってしまいます*1

機能の性質上、端末にある画像をすべてアップロードする*2ので利用者が増えると判定の処理が大量にくる可能性は高かったため、アーキテクチャを変更する必要がありました。

現状のアーキテクチャ

そこでシステムの安定性を高めるため、以下のような方法を取ることにしました。

  • 画像はAPIサーバーにアップロードせず、S3に直接アップロードする
  • サーバー側は料理画像の判定処理をバックグラウンドワーカーで非同期に行う

APIサーバーで重い処理を受けないようにすることでアクセスが集中してもAPIサーバーが詰まらないようにし、バックグラウンドワーカーをスケールアウトさせればサービスがスケール可能になるアーキテクチャを目指しました。

他にもI/O多重化ができるサーバーを導入するであったり、コストを覚悟でUnicornのサーバーをスケールアウトするという方法も考えられますが、今回は社内のノウハウなども加味して一番安定して稼働できそうという判断のもと、このアーキテクチャを採用しました。

最終的な構成としては次のようになります。

f:id:hokaccha:20171108125343p:plain

順に説明していきます。

S3に直接画像をアップロードする

まず、画像アップロードの負荷を軽減するため、S3に直接アップロードすることにしました。そのためにS3のPre-Signed URLを利用しています。

Uploading Objects Using Pre-Signed URLs - Amazon Simple Storage Service

クライアントがアップロード処理を開始するときにAPIサーバーにリクエストし(1)、アップロード可能なS3のURLを取得します。このときAPIサーバーはRDBMSにレコードを作成し、アップロードの状態管理を開始します。クライアントアプリは返ってきたURLに対して画像をアップロードすることでS3に画像がアップロードされます(2)

S3のイベント通知でSQSにエンキューする

S3にはEvent Notificationsという機能があります。

Configuring Amazon S3 Event Notifications - Amazon Simple Storage Service

この機能を使うと、S3にファイルアップロードされたり、変更されるといったイベントが発生されときに、特定のサービスに通知を送ることができます。これを利用し、S3にファイルがアップロードされたときにSQSにエンキューが行われるようにします(3)

バックグラウンドワーカーの処理

バックグラウンドワーカーはSQSのキューをポーリングしていて、エンキューされたメッセージはバックグラウンドワーカーがデキューすることで処理を行います(4)

メッセージのペイロードにはS3にアップロードされたオブジェクトのキーなどの情報が入っているので、その情報を元にバックグラウンドワーカーはS3からアップロードされた画像を取得し(5)、機械学習による判定を行い(6)、結果をAPIサーバーにHTTPで送信し(7)、メッセージの処理を完了させます。

このワーカーはPythonで書かれていて、hakoを利用しECS上で動いており、オートスケールの設定もhakoでおこなっています。

結果の取得

最後にクライアントアプリは結果をAPIサーバーから取得(8)してフローは終了です。

ただ、処理を非同期にした場合に難しいのがこの結果の取得です。同期的に判定ができるのであれば、クライアントアプリは判定結果が返ってくるのを待ってから次の処理に進めばいいのですが、非同期の場合はいつ処理が終わるかクライアントアプリにはわかりません。

今回はクライアントアプリが定期的に結果取得のAPIエンドポイントにリクエストすることで結果を取得するようにしています。料理きろくという機能はユーザーが写真を撮って、後で見た時に自動で写真が記録されている、という機能なのでそのケースではリアルタイム性が求められないので数十分に一回程度のリクエストで後から結果が取得できれば問題ありません*3

ただ、クライアントアプリでそういった処理を定期実行するのは簡単ではなく、様々な苦労がありました。詳しくは料理きろくのAndroidの実装を行った吉田の以下の資料にまとまっています。

まとめ

料理きろくの料理画像判定部分のアーキテクチャについて解説しました。現時点で一日あたり約50万枚以上の画像を判定していますが、大きな問題はなく安定して稼働しています。基本的にはよくあるジョブキューを利用した非同期処理のアーキテクチャですが、S3やSQSはこういったシステムを組むのにとても便利です。

また、最近ではモバイル端末でも機械学習のモデルが動くようになってきています。まだ実用段階まではいっていませんが、それが実現すればこのようなシステムは一切いらなくなり、クライアントの実装も非常にシンプルになるので、できるだけ早く実現してこのシステムがいらなくなる日がくればよいと思っています。

*1:Railsを噛ませずにPythonのサーバーで直接受けてもHTTPサーバーの仕組みがUnicornと同じであれば大きな違いはありません

*2:料理画像以外も判定のために一旦サーバーに保存しているため、ユーザーのプライバシーについては十分に配慮し、判定後には画像は完全に削除し、開発者であってもどのような画像が判定に使われたかを見ることがはできないようになっています

*3:ユーザーが最初に料理きろくを利用する場合に数十枚の画像の判定を進捗を見せながら表示するという要件もあり、この場合は間隔の短いポーリングを行っています

Encoder-Decoder でレシピの材料名を正規化する

研究開発部の原島です。部のマネージメントのかたわら、自然言語処理関連の開発に従事しています。本エントリでは、最近社内で開発した自然言語処理システムを紹介します。

■ 「しょうゆ」のバリエーションは 100 種類以上

クックパッドで以前から解決したかった課題の一つに材料の名前(以下、材料名)の正規化があります。

f:id:jharashima:20171030074621j:plain

クックパッドのレシピは複数の材料から構成され、各材料は名前と分量から構成されています。例えば、上のレシピの一つ目の材料は「豚薄切り肉」が名前で、「200g」が分量です。

さて、この材料名はこのレシピでは「豚薄切り肉」という表現でした。しかし、他のレシピでは「豚うす切り肉」という表現かもしれません。「豚うすぎり肉」や「ぶた薄切り肉」、「豚薄ぎり肉」等の表現もありえますね。

これは異表記同義(いわゆる表記揺れ)の問題ですが、同様の問題は他にも沢山あります。例えば、以下のようなものです。

  • 同義表現系の問題
    • 異表記同義(上述)
    • 異形同義(e.g.「じゃがいも」と「馬鈴薯」)
    • 略記(e.g.「ホットケーキミックス」と「HM」)
    • 綴り間違い(e.g.「アボカド」と「アボガド」)
  • 付属表現系の問題
    • 記号(e.g.「★味噌」)
    • 括弧表現(e.g.「油(炒め用)」)
    • 接頭辞(e.g.「お砂糖」)
    • その他(e.g.「お好みでローズマリー」)

面倒なことに、これらの問題は複合的に発生することも珍しくありません。その結果、例えば、クックパッドにおける「しょうゆ」には、分かっているだけで、100 種類以上のバリエーションが存在しています。

時に、これがサービス開発において問題となります。

例えば、あるレシピのカロリーを計算するとしましょう。そのためには、そのレシピ中の全ての材料名のカロリーが必要です。つまり、材料名ごとにカロリーを登録したデータベースが必要です。

しかし、クックパッドには数百万種類の材料名が存在します。これらの全てに人手でカロリーを登録するのは途方もない作業です。「しょうゆ」は大さじ 1 杯で約 13 kcal ですが、あらゆる「しょうゆ」のバリエーションにこの情報を登録するのは大変です。

こういった時に、「材料名を正規化したい」という考えに至ります。あらゆる「しょうゆ」のバリエーションを全て「しょうゆ」に正規化できれば、「しょうゆ」にだけカロリーを登録すれば良いわけです。大分コストを削減できそうですね。

このように、材料名の正規化は、以前から解決したいと思っていた課題の一つでした。

■ 正規化しよう!とは言え...

どうすれば材料名を正規化できるのでしょうか。

最初に検討すべきは正規表現です。例えば、材料名の冒頭の記号と末尾の括弧表現を除去するというのはどうでしょう。しかし、この方法だと、同義表現系の問題は全く解決できません。

クラウドソーシングはどうでしょう。人手で正規化すれば、全ての問題を解決できそうです。しかし、1 個の材料名を 10 円で正規化できたとしても、数千万円が必要です。仕様変更等でやり直しが発生すれば、さらに必要ですね。

機械学習の出番かもしれません。例えば、何らかの語彙を定義できれば、SVM や Random Forest 等で各材料名を語彙のいずれかに分類できそうです。しかし、そもそも語彙を定義するのが困難です。未知語とかどうしましょう。

材料名の各文字について CRF で「必要」もしくは「不要」というラベルを付与して、必要な文字だけを抽出するというのはどうでしょう。しかし、この方法でも、同義表現系の問題は解決できません。

いずれの手法にも何かしらの問題がありそうです。

■ Encoder-Decoder で正規化しよう!

そこで、クックパッドでは Encoder-Decoder を利用することにしました。

Encoder-Decoder は、おおまかには、「入力の情報を何らかのベクトルに集約し、そのベクトルから出力を生成するモデル」です。前半を処理する部分が Encoder で、後半を処理する部分が Decoder です。

このモデルの利用例としては機械翻訳が代表的です。日英翻訳であれば、日本語文が入力で、その英訳が出力です。入力と出力は一般的には単語列です。正解データ(この場合は日英対訳)さえあれば、翻訳モデルが構築できます。

材料名を正規化する場合、材料名が入力で、その正規化後の表現が出力です。イメージは下図の通りです。上述の通り、Encoder-Decoder の一般的な設定では入力も出力も単語列ですが、我々の設定では文字列になります。図の EOW は End-of-Word の略です。

f:id:jharashima:20171030074722p:plain

Encoder-Decoder は入力から出力を抽出するわけではなく、生成します。そのため、付属表現系の問題だけではなく、同義表現系の問題も解決できます。また、正規化のパターンが学習できれば、未知語にもある程度は対応できます。

なお、このように Encoder-Decoder を正規化に利用するのは我々が最初ではありません(業務に導入したのは我々が最初かもしれません)。例えば、以下の 2 本の論文で同様のアイデアが提案されています。

  • Japanese Text Normalization with Encoder-Decoder Model(Ikeda et al. 2016)
  • 疑似データの事前学習に基づくEncoder-decoder型日本語崩れ表記正規化(斉藤ら 2017)

■ 全体像はどうなってる?

正規化システムの全体像は大きく三つに分割できます。正解データ収集とモデル開発、バッチ実行です。以下でそれぞれについて概説します。

f:id:jharashima:20171030074818p:plain

正解データ収集

Encoder-Decoder を学習(と検証、テスト)するには正解データが必要です。そこで、スタッフが毎日利用する社内ツール(クックパッドと同様、Rails 製)に、正解データを収集するための仕組みを追加しました。普段の業務をこなすと、正解データが自然と MySQL に蓄積されるようにしました。

本エントリの執筆時点で、約 12,000 個の正解データが収集されていました。

モデル開発

開発環境には p2.8xlarge を、フレームワークには Chainer を利用しました。やや詳細な話になりますが、Encoder と Decoder には Stacked Uni-directional LSTM(3 層)を使用しました。なお、我々のタスクでは Bi-directional LSTM や Attention はあまり効果がありませんでした。

PR がマージされると Jenkins が起動して、プログラムの実行環境をまとめた Docker イメージが社内の Docker レポジトリに登録されます。このイメージは後述するバッチで利用されます。

バッチ実行

クックパッドは主に Rails で構築されているため、そのデータの多くは MySQL に蓄積されています。さらに、これらはログデータ等とともに Redshift に集約されています。正規化バッチはこれらのデータをもとに動作しています。

まず、正規化の対象とする材料名を Redshift から取得して、S3 に保存します。次に、Docker イメージをもとに Encoder-Decoder の実行環境をインスタンスに準備した後、材料名を正規化します。最後に、S3 を経由して、結果を MySQL に保存します。

■ 正答率はどれくらい?

最後に、簡単な実験結果を紹介します。実験では正解データの 10% をテストデータに、別の 10% を検証データに、残りの 80% を学習データに使用しました。

手法 正答率
Baseline A(変更なし) 20.8%
Baseline B(正規表現) 28.6%
Encoder-Decoder 71.2%

まず、ベースラインの結果を確認しましょう。Baseline A は、入力をそのまま出力した場合の結果です。正答率(正解との完全一致率)は 20.8% でした。これは、テストデータの約 80% に何らかの正規化が必要ということを意味しています。

Baseline B は正規表現です。冒頭の記号と末尾の括弧表現を入力から除去した場合の結果です。正答率は 28.6% でした。上でも議論したように、正規表現だけで正規化するのはやはり困難でした。

最後に、Encoder-Decoder の正答率は 71.2% でした。同義表現系の問題(e.g. 桜エビ → サクラエビ)や付属表現系の問題(e.g. ※牛乳 → 牛乳)、それらの複合的な問題(e.g. ☆ローリエ → 月桂樹)に幅広く対応できたので、ベースラインより圧倒的に高い正答率となりました。

補足として、この数字はあくまでもテストデータでの値です。我々のテストデータ(と言うより、正解データ)は、テストのため、低頻度の材料名を少なからず含んでいます。現実のデータにおける数字はもう少し高いです。

また、実際に正規化結果を利用する際はスタッフがざっと確認して、明らかな間違いは修正しています。なお、修正されたデータは新たな正解データとして利用できます。

■ Appendix

以下は少し発展的な話題です。Encoder-Decoder でも失敗するのはどういうケースでしょうか。そして、それらのケースはどうすれば改善できるのでしょうか。

探索範囲が狭い

Decoder が N 文字目を出力する際は、その時点での生成確率が一番高い文字を選択します。しかし、確率が二番目に高い文字は無視して良いのでしょうか。N+1 文字目以降を出力する際に後悔しないでしょうか。

そこで、ビームサーチです。N 文字目を出力する際、確率が高い文字を M 個キープしておきます。そして、最終的に生成確率が一番高い文字列を出力としましょう。追加実験では、M = 10 の時、正答率が 72.3% になりました(統計的に優位でした。以降の数字も同様)。

存在しえない文字列を出力

例えば、「こめ油」を「米油」と正規化すべきところ、「コー油」と正規化してしまうケースです。「コー油」という文字列は少なくともクックパッドには存在しません。機械翻訳でも、Encoder-Decoder がおかしな文(単語列)を出力してしまうのはよくある話です。

そこで、このような文字列を出力しないように、Decoder を制御してみました。具体的には、ビームサーチの途中でクックパッドに存在しない文字列が生成された場合、その文字列を正規化候補から除外してみました。トライ木を利用すれば簡単に実装できます。正答率は 72.8% になりました。

入力と無関係の文字列を出力

例えば、「トーモロコシ」を「とうもろこし」と正規化すべきところ、「とろろこんぶ」と正規化してしまうケースです。「とろろこんぶ」は世の中に存在する文字列ですが、もちろんこの正規化は間違いです。

そこで、ビームサーチで取得した正規化候補を入力との類似度(と Encoder-Decoder のスコア)でリランキングしてみました。類似度の計算には word2vec を、その学習データにはクックパッドの全レシピを利用しました。正答率は 73.3% になりました。

その他

さて、まだまだ改善の余地があります。しかし、調査してみたところ、これらの多くは学習データの不足に起因するものでした。ここから先はモデルを改善するよりも、学習データを追加する方が楽かもしれませんね。

■ おわりに

本エントリでは、Encoder-Decoder でレシピの材料名を正規化するシステムについて紹介しました。

このモデルには、レシピの材料名だけでなく、EC サイトの商品名や不動産サイトの物件名等も正規化できる可能性があります。本エントリが、正規化に頭を抱える誰かのお役に立てば幸いです。

最後に、クックパッドの研究開発部では自然言語処理や画像認識の専門家を募集しています。あなたの知識・スキルで世界中の毎日の料理を楽しみにしませんか。ご興味がある方は採用ページを是非ご覧ください。ご連絡をお待ちしております。

クックパッド株式会社 研究開発部 採用情報