データ活用基盤の今 〜DWH外観図〜

こんにちは、今年の1月に会員事業部から技術部データ基盤グループへ異動した佐藤です。先日、京まふ2019前夜祭イベントに参加するために人生で初めてピカピカ光る棒を買いました。

新卒で入社してから2年ほど分析作業をしていた身から、データ活用基盤を作る側へ立場を変えました。今回は新たに身を移したデータ活用基盤の外観を説明したいと思います。

2017年にも同内容の記事が投稿されていますので、当時との違いを中心に説明していきます。

外観図

以下が2019年10月現在におけるクックパッドのデータ活用基盤の全体像です。

クックパッドのDWH外観図
クックパッドのDWH外観図

masterデータのインポートがMySQL以外にも複数種対応し始めたことと、PrismとSpectrum(S3+Glue)周りと、Tableau Serverが大きな変更点となっています。2017年の図にDmemoはありませんでしたが、記事本文にある通り当時から活用していました。

図が煩雑にならないよう、レシピサービスを中心にまとめていますが、クックパッド社が運営する全サービスについて同様のワークロードでRedshiftにデータを集約させています。また、図では省略しましたが、Firebaseなどの外部サービスから得られるデータもRedshiftに集めています。

一つ一つの処理をみていきましょう。

入力: マスターデータの取り込み (master data flow)

クックパッド社内でサービスのマスターデータを管理するDBとして利用するDBMSは主にMySQL・PostrgreSQL・DynamoDBがあります。最も多いのがMySQLで、最近DynamoDBが増えつつあります。

MySQLについてはpipelined-migratorという独自開発の専用システムを利用しています。 こちらは管理コンソール用の専用Webサイトが社内ネットワーク上にあり、テーブル取り込みの様子を確認できます。また、ボタンひとつでRedshiftに取り込むテーブルやDBの追加・削除が手軽にできるようになっています。

pipelined-migratorのコンソール
pipelined-migratorのコンソール

PostgreSQLについてはAWS Database Migration Service(以下DMS)を利用しています。pipelined-migratorがまだ現段階ではMySQLにしか対応しておらず、かつ社内でPostgreSQLの利用が比較的少ないため、一時的にDMSを利用しています。PostgresSQL対応版も開発中であり、今後PostgreSQLからのテーブル取り込みもpipelined-migratorに一本化していく予定です。

DynamoDBについてはまだ実績が少ないのもあり、定型化しているものの手作業でインポートしています。 DynamoDB Streamでデータの更新を検知し、LambdaとKinesis Firehose経由で、S3に追加データを吐き出します。S3に配信後は後述するログデータと同様にRedshift Spectrumでクエリアクセスができるようになります。

DynamoDBデータの取り込みフロー
DynamoDBデータの取り込みフロー

pipelined-migratorはbricolageとmys3dumpを組み合わせて作られています。

入力: ログデータの取り込み (log data flow)

ログデータの取り込みには、2017年の記事に書かれたStreaming Loadシステムと昨年末に書かれた記事に登場したPrismの両方が使われています。 クックパッドのDWHではRedshift Spectrumを中心にして構築するようにデータ移行作業を実施中です。このため、Streaming Loadシステムからのログ取り込みはゆくゆくは退役する予定です。 現在は移行期であるため外観図にはStreaming LoadからRedshift内部へのロードとPrismからRedshift外部(Spectrumでアクセス可能なS3バケット)の2経路が同時に存在していますが、次にまたDWH外観図を書く頃にはPrismに一本化されていることでしょう。

サービス開発者側から「新規にログを取り始めたい」となった場合、以下の手順でロードを行います。

  1. *.strdefというYAML形式のファイルにログ定義を書く
  2. tech/dwhというDWHが管理するリポジトリへ上記ファイル追加PRを出してもらう
  3. ログ関係者&DWHチームメンバーがログ定義に関するレビューをする
  4. アプリケーションから送られてきてS3にログが到達したしたことを確認し、strdefファイルの適用を行う

サービス開発者に1~3までを行ってもらい、DWHでは3~4を担当します。 こうしてみるといちいちログをとるのに手順が多く、面倒に思われるかもしれません。このフローで運用している背景には、ログを取りたいと思った人にstrdef定義を通してきちんとログ設計をしてもらいたいという意図があります。これはDWHに限らない話ですが、ロギングが始まってしまえばログは修正できません。プログラムのリファクタ感覚でカラム名や型定義は変更できないのです。どうしてもやむを得ず発生することはありますが、問題を先送りにした場合はツケが回ってきます。ログ取り自体は気軽に行えるが、その設計にはきちんと考える時間を取りましょうという思いがあって、こういった手順となっています。

Streaming Load・Prismのどちらもコンソールが用意されており、ログが順次ロードされていく様子を確認できます。

Streaming Loadのコンソール
Streaming Loadのコンソール
Prismのコンソール
Prismのコンソール

Streaming Loadの実装はbricolage-streaming-preprocssorとbricolage-streaming-loaderとして公開されています。

Redshift内部での加工処理

Prism登場によりRedshift Spectrum活用が進んできましたが、Redshift内部の処理に関しては既にだいぶ完成していたため新たに手を加える必要はありませんでした。 2017年当時と同様、bricolageを用いて書かれたSQLバッチをKuroko2というジョブ管理システムで定期実行しています。Redshift内部のデータアーキテクチャについても従来通りの入力層・論理DWH層・論理データマート層の3層区切りとなっています。ただ、2年間運用してきたことでこれら各層に格納されているデータは充実してきており、2年前と比べて3層ともに成長しています。

Redshift内部の加工処理はbricolageとKuroko2を組み合わせて作られています。

出力: アドホックな分析

社内からのアドホックな分析に用いられるツールに関しては特に変化がなく、Bdash・Postico・Jupyter がそのほとんどです。あるいはTableau Desktopでアドホックな分析作業を行っているかもしれません。これら各ツールは全て内部テーブルと同じようにSpectrumテーブルへアクセスできるため、各自分析者が自分の使いたいツールを自由に選べる状態になっています。

分析者は後述するDmemoやSlackにおけるデータ分析お悩み相談チャンネル、#data-analysisを駆使して社内のデータを分析しています。日々、#data-analysisやissue上で分析用SQLのレビューが行われています。

BdashはこちらのGithubリポジトリで公開されています

出力: ダッシュボード用BIツール

from redash to tableau

2017年の記事においてRedashからTableauへの移行を検討中と書かれていましたが、現在では完全に移行体制が整い社内のほとんどのダッシュボードはTableau Serverに移行されました。移行に至った理由は過去の記事にも書かれた通り、Redashのキューまわりの実装に難があったためです。

Tableau Serverに移行した今では各部署で活用されており、エンジニア以外にも職種を問わず広く利用されています。

ただし、Tableau ServerとTableau Desktopではライセンスが別契約となるため、実際にダッシュボードを作りたい人はライセンス申請が必要となります。この申請フローは定型化されており、ヘルプデスクへ申請を出すだけでライセンスキーが割り当てられるようになっています。

Redashは諸事情がありまだ完全退役とはなっていませんが、アクセス&更新がなされているダッシュボードはほぼありません。

出力: DWH内のデータベースドキュメント

データベースドキュメント管理システム dmemo のご案内 にて登場したDmemoも現役で活躍中です。 Dmemoは毎晩Redshiftにアクセスし、DWH内部にある最新の全テーブル情報を取り込みます。取り込んだDB・スキーマ・テーブル・カラムの各階層ごとに説明を書くことができ、その履歴も残せます。Dmemoに十分情報が蓄積されていれば、PR上でのやり取りやデータ分析についてSlack上で聞かれたときなどにDmemoのURLを貼って一言二言伝えるだけでスムーズにデータに関する情報伝達が行えるようになります。

Dmemo操作例
Dmemo操作例(Techlife用にローカル開発環境上でダミーデータを用いて撮影したものです)

新しく入社した社員やインターン生にデータに触れてもらうときに一旦DmemoのURLを共有しておくとその後の話が円滑に進むようになります。また、ログに関する何かしらのインシデントが発生した際には「v1.0.0のアプリケーションではIDがズレている」等々のメモを書いておくことで、後でログデータを集計して奇妙な結果が出たときに即座に気づくことができるようになります。

Redshift Spectrum移行作業に合わせて、外部テーブルやRedshift特有の機能であるlate-binding viewにも対応しました。

DmemoはこちらのGithubリポジトリで公開されています。

出力: バッチ処理用バルクエクスポート

こちらも2017年の記事と変わらず、Queueryとredshift_connectorが使われています。他のサービスからも利用できるようなDWHを構築しておくことで、データ活用基盤が分析のみならず様々なサービスやプロダクトにまで活用されるようになります。

過去にTechlifeでご紹介したデータ活用基盤を利用したシステム運用の記事を下記に載せておきます。

Redshiftから外部システムへのバルクエクスポートはQueueryとredshift_connectorが使われています。

DWHの課題

上記がデータ活用基盤の外観図となります。しかし、まだこれで完成ではなくこれからも開発を続けていく必要があります。最後に、DWHに残っている主な課題について述べたいと思います。

Redshift Spectrum移行

ログデータ取り込みの項目でも書きましたが、現在は内部テーブルへロードする旧方式のStreaming Loadと外部テーブルへロード(S3へのParquet変換)する新方式のPrismの両システムが並列で稼働しています。 Streaming Loadでのロードを廃止するためには内部テーブルに依存している全ジョブの停止・外部テーブルへの移行が必要となります。 186のジョブと284テーブルを一つ一つ検証した上で移行と削除を行っていく作業は自動化ができない、泥臭い手作業となります。その中で歴史的経緯に基づく仕様を発見したり、現状のジョブが間違っていたり等が発見されていきます。 この移行作業についてはDWH総出で丁寧に移していく他無く、地道にやっていくことになります。

Tableau Server運用

Tableau Serverの運用も依然として作業コストがかかっており、なんとかすべき課題です。 まず、Tableau ServerはTableau社側から新しいバージョンが提供されるたびにアップグレードしていく必要がありますが、ダッシュボードのポータルサイトという重要なサービスであるためこの作業は慎重に行う必要があります。アップグレード作業の際には事前に社内アナウンスを出し、メンテナンス時間を確保して行っています。 過去にアップグレード作業に失敗し、Tableauサポートと連絡をとりつつアップグレードをするということもありました。このアップグレード作業をより低コストに抑えることができないか、というのがTableau Server運用における1つ目の課題です。

また、TableauはSlackとの連携が弱いという弱点があります。一応、メール通知とZapierという外部のオートメーション化サービスを組み合わせることで自動通知を実現できますが、こちらにも実は問題があります。現在クックパッドでは専用botでの通知運用をしつつ、Tableau公式によるSlack連携機能がくる日を待ちわびています。

さらにライセンス管理についても課題があります。先にはダッシュボード用BIツールの項目で「ヘルプデスクへ申請を出すだけでライセンスキーが割り当てられる」と書きましたが、定型化したとはいえここは裏側で手作業が発生しています。根本的な負荷削減にはなっていないため、できる限りライセンス付与の作業負荷がなくなるよう自動化をしたいところです。

データ活用の推進

データ活用基盤が整いつつある今、基盤業務のその先のデータ分析の啓蒙活動に比重を置くフェイズに突入しつつあります。 今までも社内でSQL勉強会が開かれたり等、各所でデータ分析の民主化は進んできていました。ですが、草の根運動に頼るだけでなく、DWHが組織として継続的にデータ活用を推進していく必要があります。 こちらに関しては具体的な方策やマイルストーンもありませんが、来期からの一番の課題となることでしょう。

まとめ

今回は以前お伝えしてから2年経ったデータ活用基盤の全体像をお話しました。一度に書ききるには多すぎるため、前回との差分を中心に書きましたのでぜひ2017年に公開した記事と比較してみてください。

私達、技術部データ基盤グループは「クックパッドの全社員がデータに基づいた意思決定を行えるようにする」を目標に日々の業務に取り組んでいます。 分析者の望む最強のデータ分析環境を提供したい方、余計な手間をを要せず分析作業にのみ集中できる最高のデータ分析環境に浸ってみたい方、ぜひ私達と一緒にデータを駆使してより良いサービス作りをしてみませんか。

クックパッド株式会社 | クックパッド株式会社 採用サイト

クックパッド社内に工房(Fab)を作ってプロトタイプ開発をした話

今年1月に研究開発部から分離して発足しましたスマートキッチン事業部の山本です。
スマートキッチン事業部では、クックパッドが提供するレシピ情報を様々な家電機器と連携させて、料理体験をより楽しく快適にする、スマートキッチンサービス OiCy の開発をすすめています。

クックパッド社内に工房(Fab)を作りました

スマートキッチンサービスOiCyは、レシピ情報と家電機器の連携で生み出されるサービスで、サービス開発に加えてサービスと連携する家電機器が必要になります。そのため、家電メーカーとの連携をすすめていますが、同時に自前での家電機器開発も行っています。そして、自前の家電開発を効率的に行なえるようにする目的で、社内で加工製作ができる工房(Fab) を、恵比寿のクックパッドオフィス内に立ち上げました。工房には、3Dプリンタやレーザーカッターなどの加工設備が設置されており、業務内外を問わず社員の利用が可能になっています。(要安全講習)

f:id:ymmttks:20191016150422j:plain:w420
工房の様子

工房生まれの自前(改造)家電たちの紹介

この工房で生まれた、クックパッド自前(改造)家電の一部が、先日開催されたスマートキッチンサミットジャパン2019(SKSJ2019)で公開されました。

OiCyService動画

SKSJ2019クックパッドデモの動画
↓SKSJ2019の関連記事はこちらを参照
・PC Watch ロボット化する家電から寿司シンギュラリティまで、人を食でエンパワーする「スマートキッチンサミット2019」 森山 和道
・CNET JAPAN 未来の台所を創造する「SKSJ 2019」から見えてくるもの 近藤克己

OiCy Water

f:id:ymmttks:20191016150433j:plain:w320
OiCyWater外観
OiCy Waterは、水の『硬度』と『分量』をレシピに合わせてボタン一つで出してくれる、電動ウォーターサーバーです。スマートフォンのアプリ上でレシピを閲覧すると、そのレシピに書かれている水の『硬度』と『分量』(※)が自動的に装置に転送されます。ユーザーは装置上のボタンを押すだけで、閲覧しているレシピで使うための適切な水を得ることができます。『硬度』と『分量』は、ジョグダイヤルを回して手動で調整することもできます。

※現状ではクックパッドのレシピ全てに硬度に関する記述があるわけではありません。

↓水の硬度が料理に与える影響についてはこちらを参照
SKSJ2019 ⾃分の「おいしい」を⾃分でつくれる感動を クックパッド 金子晃久

OiCyWaterの構成

f:id:ymmttks:20191016150453p:plain:w420
OiCyWaterの構成
本機は、2つのチューブポンプを用いて、『硬水』と『軟水』2つのボトルから水を排出する装置です。使用したチューブポンプは3.5ml刻みで排出量を制御、最速で1分間に1.4Lの水を排出することができます。
制御用のマイコンシステムにはM5Stackを使いました。理由は、技適が取れていてかつとても安価、ネット上に参考にできる情報が豊富にあって、ライブラリも充実しているからです。周辺デバイスへの信号は、モーターの速度制御をするPWM信号のみM5StackのGPIOから直接出していますが、それ以外はI2C接続したGPIOエクスパンダを経由してやり取りをしています。GPIOエクスパンダ側で、信号の変化を割り込み制御をする予定でしたが、WiFiのライブラリと同時使用するとファームにリセットがかかるという現象があり、イベント直前で時間がなかったためこの問題解析は保留して、割り込みなしのポーリングで、ジョグやモーターの回転を拾う処理になっています。
モーターの回転は、チューブポンプの回転部分とポンプ外装の間に隙間があるので、100円ショップのネオジウム磁石をそこに接着、ポンプ外側に設置した磁気センサから非接触で回転検出をしています。この方法では回転方向は分からないのですが、チューブポンプは負荷が非常に大きくギア比の大きなモーターが付いており、外部から強制的に回すことはほぼ不可能です。そのためポンプ回転部分はモータードライバに入れている信号の向きにしか回らないため、特に回転方向を検出する必要はありません。磁石の貼り付け位置を、チューブポンプのローラー部分にすることで、水の排出綺麗に途切れるところで正確にモーターを止められる”位置制御にも利用しています。
f:id:ymmttks:20191016150442p:plain:w320
OiCyWaterのメカ構造
チューブポンプはモーター部分が長く突き出した構造をしていて筐体へのおさまりが悪いため、ベルトとプーリーを用いてモーター部分をポンプ本体とタンデム構造にし、2つのチューブポンプを向かい合わせに対向させるメカ構成にしました。2つのチューブポンプ、2つのモーターは2mm厚のステンレス製の背骨に固定されて、チューブポンプを回す強力なモータートルクに負けない強靭な剛性を持たせました。工房のレーザーカッターでは、金属の切り出しはできないため、この背骨部分の制作のみ外注先に頼んで特急で作っていただきました。ペットボトルを下向きに指すジョイント部分については、弁のついたペットボトルキャップと交換する部分はペット用の給水器の部品を流用、刺さる側の部品は3DプリンタをつかってABS樹脂で成形しました。2Lの水の水圧がかかっても水漏れをしない構造を作るのには試作検証改良を繰り返す必要がありましたが、3Dプリンタが手元にあることは短期間での開発にとても役立ちました。

OiCyサービス対応電子レンジ

f:id:ymmttks:20191016150446j:plain:w420
OiCyサービス対応電子レンジ SIGMA
メーカー様からお叱りを受けるかもしれないのであまり大きな声では言えないのですが、市販の電子レンジを改造して、スマートフォンのアプリ上で閲覧しているレシピに書かれている 『加熱ワット数』、『加熱時間』をWiFiを通じて自動的に装置に転送されるようにしたものが、OiCyサービス対応WiFi電子レンジ(開発名:SIGMA)です。

SIGMAの構成

f:id:ymmttks:20191016150457p:plain:w420
SIGMAの構成
電子レンジは、強電系に非常に高圧な回路と大容量コンデンサを搭載しており、改造には危険を伴います。専門的な知識のない場合には絶対に真似しないようにお願いします。今回の改造は、できる限り装置の深い制御部分に手を入れず、UI部分を乗っ取る形でHackしました。こうすることで、強電系の回路に一切触れずに欲しい機能を実現することができました。
電気量販店で、改造しやすそうな電子レンジを探すところから、開発は始まります。改造用の電子レンジを選択する上でのポイントは、液晶表示やタッチパネルなどを用いてるものは、現在の状態を正確に把握するための難易度が高いため避けます。LEDのみ、物理スイッチのみでUIが構成されていて、かつスイッチに複数の機能が割り当てられていないものが好適です。
OiCy Waterと同様に、制御用のマイコンシステムにはM5Stackを使いました。この電子レンジは、UIが、LEDと2つのジョグスイッチだけで構成されていたので、これらの入出力と扉の開閉センサをロジック回路処理(時分割表示のLED信号の復調回路)を噛ませてGPIOエクスパンダに接続し、M5Stackからは全体が一つのI2Cデバイスとして見えるようにしました。 外装は、操作パネル部分のジョグやLEDをすべて外し、レーザーカッターで加工した3mm厚の乳白色のパネルで覆い、表面上からはM5Stackの表示パネルとボタンのみしか見えない構造になっています。スマホからのレシピ情報転送以外に、M5Stackのボタン操作によるレンジ設定変更が可能です。加熱のスタートやキャンセルはドアの開閉で行い、ボタン操作は不要です。

OiCyサービス対応IHプレート

f:id:ymmttks:20191016150426j:plain:w420
OiCyサービス対応IHプレート OMEGA
こちらもメーカー様からお叱りを受けるかもしれないのであまり大きな声では言えないのですが、市販のIHプレートを改造して、スマホ端末で閲覧しているレシピに書かれている『火力』、『加熱時間』をWiFiを通じて自動的に装置に転送されるようにしたものが、OiCyサービス対応WiFi IHプレート(開発名:OMEGA)です。

OMEGAの構成

f:id:ymmttks:20191016150502p:plain:w420
OMEGAの構成

改造用のIHプレートを選ぶポイントも電子レンジとほぼ同じです。リバースエンジニアリングの結果、この機器は本体部分とUI部分をクロック同期式の変則的な双方向シリアル通信で構成されていることが分かったので、UI基板を取り外し、代わりにロジック回路処理(入出力信号の分離)を噛ませてGPIOエクスパンダに接続して、M5Stackからは全体が一つのI2Cデバイスとして見えるようにしました。
外装は、UIパネル幅に対してM5Stackの方が奥行方向に長いため、張り出した顎の部分を3Dプリンタで成形して、表面処理をした上で塗装したものを取り付けてあります。パネル部分は、電子レンジと同様にレーザーカッターで加工した3mm厚の乳白色のパネルで覆って統一感を持たせてあります。

社内に工房がある強み

 製品やサービス、機能など、我々がこれから作ろうとしているものが、本当に顧客にとって価値があるのか?モノができてから実際にユーザーに提供してみたら、期待した価値がなく失敗・・・のでは時間と資本を大きくロスしてしまいます。そこで、我々は、Googleのスプリントに則って、3〜5日で課題から仮説を導きだし、ソリューションを立て、プロトタイプを作り、ターゲットユーザに当てて仮説検証を行う、という方法をよく用いています。
 ↓高速コンセプト開発メソッドについてはこちらを参照
 ・SKSJ2019 一週間で回す高速コンセプト開発メソッド教えます クックパッド 佐藤彩香
 
 スマートキッチン事業部でも、短いものでは3日程度で、ハードウェアのプロトタイプを作って、実際にユーザーに使ってもらって、顧客価値を検証しています。今回紹介した3つの家電デバイスは、各2週間程度の開発期間がかかっていますが、顧客価値検証用に現場で実際に使用し、継続的に改良が行われています。こういった機器の開発改良は、外部のリソースに頼っていてると短時間での開発は難しく、価値検証に時間がかかってしまいます。使いたいときにいつでも加工・製作に使える『場』と『機材』が社内にあることは、超高速ハードウェアプロトタイピングでは極めて重要です。そして、この『場』と『機材』を活かしきれる、メカ設計〜加工〜回路設計〜回路製作〜ファームウェア設計実装〜実機デバッグといった一連のプロトタイプ開発を一人で一貫してできる、フルスタックエンジニア人材を絶賛募集中です。

まとめ

『その程度のプロト、俺がやれば1週間でできる!』『3日でできる!』というプロトタイプエンジニアスキルをお持ちの猛者のかたは、是非我々のプロジェクトにJoinしてください。
クックパッド キャリア採用 職種:プロトタイプエンジニア(スマートキッチン)

同時に、こんなイカれたデバイスを操作するiOSアプリを書きたいという、キワモノ好きのiOSエンジニアとデバイスとアプリをユーザーとつなぐ素敵なUI/UXデザインを担当するデザイナーも絶賛募集中です。
クックパッド キャリア採用 職種:iOS エンジニア
クックパッド キャリア採用 職種:UI/UXデザイナー(スマートキッチン)

データ分析プロジェクトの品質をキープしつつ効率的な検証をサポートする一時ファイル群の管理

研究開発部の takahi_i です。本稿はデータ分析、 機械学習関係のプロジェクトで数多く生成される一時オブジェクトおよびそれらのオブジェクトを保持するファイル(一時ファイル)を管理する取り組みについて解説します。

本稿の前半はデータを分析するプロジェクトの一般的なフローと起こりがちな問題(コードの品質管理)について解説します。後半はプログラム上で生成されるオブジェクト群をファイルに自動でキャッシュを管理するツール(Hideout)を使って、コードを整理整頓しやすくする施策について紹介します。

データを分析するプロジェクトの一般的なフロー

まずデータを処理するプロジェクトや機械学習プロジェクトの典型的なフローについて考えてみます。まずは単純に機械学習器を取得した入力に対して適用するプロジェクト、次にもう少し複雑な事例、アプリケーションで利用するデータを生成するプロジェクトのフローについて見てゆきます。

単一の機械学習器を適用するプロジェクト

データ分析するプロジェクトの典型例として機械学習器を入力データに対して適用するプロジェクトを考えます。

学習用のデータのように静的に変化しないデータはレポジトリに同梱されることもありますが、Webサービスのデータが対象の場合、推論用のデータの多くは日々変化するため、データベース(DWH)から動的に取得します。

機械学習を適用するプロジェクトは取得したデータを多段に変換しながら出力となる判別結果を生成してゆきます。

以下の図は一般的な機械学習を利用したプロジェクト検証スクリプトのフローです。

f:id:takahi-i:20191015114010p:plain
単一の機械学習器を適用するプロジェクト

上記のプロジェクトでは必要なデータを取得、学習、推論(テスト)と連続して処理がおこなれています。各処理ごとにデータは変換され、生成されます(例えば学習後にはモデルオブジェクトが生成されます)。

たとえば、レシピに含まれるステップを本来のステップとそれ以外(コメントなど)に分類する バッチ処理(こちらを参照)はちょうどこのような内容になっています。このバッチでは一部のデータを切り出して処理しているのですが、それでも取得、学習、推論(テスト)ステージでかなりの時間がかかっています。

アプリケーション用データを生成するプロジェクト

さらに自然言語のような非定形データを扱う中でも複雑なタスク(対話や質問応答)ではDWHから取得した データを多くのオブジェクトを生成しつつ多段で加工してゆくことも一般的です(End-to-Endの学習で一度に処理してしまう場合もありますが)。

f:id:takahi-i:20191015114231p:plain
多段に処理をするプロジェクト

クックパッドの研究開発部においてこのような多段の処理を適用するプロジェクトに レシピのMRRへの変換や、 クックパッドのAlexaスキルが提供する調理補助用の質問応答 で利用されているKnowledge Baseの生成プロジェクトがあります。

これまで紹介した2つのタイプのプロジェクト(単一の機械学習器を適用するプロジェクト、アプリケーション用データを生成するプロジェクト)のどちらのプロジェクトタイプでも、まず一つ以上の入力データを抽出します。データは入力データ、必要なリソース(辞書)、機械学習器が出力したアノテーション結果などがあります。そしてその後のデータ変換、集約処理が多段に続きます。各処理では一時オブジェクトを入力として別のオブジェクトを生成します。

そしてこのオブジェクトの集約、加工、生成処理の実行はそれぞれ時間がかかります。このことがデータ分析プロジェクトを中長期メインテナンスする場合に品質上の問題を引き起こします。

データ処理スクリプトに対する修正要求とコード品質問題

データを分析、加工するプログラムでも普通のプログラムと同じように機能追加の要求にさらされ修正され続けます。 多くの場合、修正するべき箇所はプログラムの中の一部に過ぎませんが、修正が問題をはらんでいないかをチェックする にはE2Eテスト(機械学習の学習、Inferenceなど)を走らせます。また、自然言語を入力とするタスクの場合にはテスト しきれないことがどうしても発生するため、一部のデータを使ってプログラムを実行して変更が問題を発生していないかも確認したくなります。

このような検証目的の実行時に扱うデータの規模は大規模ではなくても、各ステージごとの処理に十秒から数分かかってしまいます。

結果、微細な修正をした後にコードに問題がないかを確認するだけでも結構な時間を消費してしまいます。 実行に時間がかかりコードを修正をするコストが大きくなるため、コードを修正するサイクルが大きく(試行できる回数が少なく)なり、コードを整理するハードルが高くなってしまいます。このような状況ではプログラムの検証実行や、テストに時間がかかりすぎるためコードの品質をキープしづらくなります。

不十分な解決方法

検証時の実行に時間がかかってしまう問題に対するする解決策として、以下のような対処方法が考えられます。

一時ファイルの手動追加

データの取得、生成時間を省略するために、データベースから切り出した入力データや機械学習器が出力したモデルファイルのような一時データを保持するファイル(一時ファイル)をレポジトリや、ローカルディレクトリに同梱しているプロジェクトを見かけます。データ分析プロジェクトの一時ファイルは、モデル、データベースから抽出した辞書リソース、前処理済みの入力データなどがあります。

たとえば以下のプロジェクトでは入力ファイルに前処理を適用したファイル(preprocessed_input1.txt)と、機械学習器が生成したモデルファイル(validation_model1.dat)をレポジトリに同梱しています。

.
├── Makefile
├── README.md
├── config
│   ├── __init__.py
│   └── env.py
├── data
│   ├── dictionary.dic
│   ├── preprocessed
│   │   ├── preprocessed_input1.txt
│   │   └── preprocessed_input2.txt
│   ├── models
│   │   ├── validation_model1.dat
│   │   └── validation_model2.dat
...

もちろんこれらの一時ファイル群は本来はレポジトリに含まれるべきではありません。それでも、こういった一時ファイルを利用することでプログラムの動作検証の速度を向上できます。

しかし、このように安易に加工済み入力ファイルやモデルファイルをレポジトリに同梱してしまうと問題が発生します。

問題の一つは加工済み入力データの生成方法がコードから分離してしまい、プロジェクトが進むにつれデータの加工方法と乖離してしまう点です。 入力データも含め、プログラムで扱うデータやオブジェクトはコード修正とともに変化してゆきます。 たとえばモデルファイルのような生成されたデータを一時ファイルから読み出して検証的に実行している場合、コードの修正によってデータがファイルから読み出しているものから変化し本来は実行時に問題が発生していることがあります。残念ながら一時ファイルを利用して検証実行している場合、このような問題に気がつくのは難しいです。というのも修正した部分(たとえば検証用に切り出した小規模データでの学習処理)はキャッシュファイルを使うと実行されず、テストやローカル環境での実行は中途半端にうまく動作してしまうのです。

さらにテストがレポジトリに添付されたモデルファイルを利用してしまっている場合には、CI環境でもコードの修正にともなうバグを検知できません。 このような状況で問題が発覚したときにはコミットすでにがかなり積まれてしまい、問題箇所を同定するのが難しくなっていることがあります。

もう一つの問題は、レポジトリを中長期メインテナンスすると発生します。本来は一時的な目的でVCSレポジトリに追加されたはずの中間オブジェクトを保持するファイル群は、役割を全うした後も消されることなく(消し忘れ)レポジトリにとどまり続けることがあります。このような消し忘れファイル群はプロジェクトの開発時には問題にならないのですが、時間経過(半年、一年)を経るとエンジニアは生成方法を記憶していないため特に引き継ぎ時に大きな問題になります。

キャッシュ処理の追加

必要な一時ファイルを活用するためのもう少しましな解決方法に、関数へのキャッシュ処理の追加があります。 たとえばMRRの生成プロジェクトでは、入力、中間データ(それぞれが数十〜数百MBのデータ)をファイルにキャッシュをすることで、 検証時の実行速度を向上して開発速度を高めました。以下はMRRの生成プロジェクトで利用されているメソッドの一部です。

def get_ingredient_id_map(cache_file_path):
    if os.path.exists(cache_file_path) and not self.force:
        with open(cache_file_path, mode='rb') as f:
            return pickle.load(f)

    ingredient_id_map = _get_ingredient_map_impl()

    if not os.path.exists(cache_file_path) or not self.force:
        with open(cache_file_path, mode='wb') as f:
            pickle.dump(ingredient_id_map, f)
    return ingredient_id_map

この関数は、キャッシュファイルがあればロードしたものを返し、なければ生成したうえで、(キャッシュ)ファイルに保存します。 この関数を使うことでローカル環境で(2回目の実行以降)オブジェクトを生成するコストは低減できます。

大きめのオブジェクトを生成する関数にキャッシュファイルを生成する処理を付与することでテストや検証目的に実行していたプログラムの実行時間が、数分から10秒程度に減少できました。これによりコードを積極的に整理できるようになりました。 またCIで簡単な学習➡推論をすることで、学習プロセスに問題ないかを常時クリーン環境でテストし続けられるという 利点があります。

しかしこのやり方にも問題があります。数多く存在する中間オブジェクトごとに上記のようなキャッシュする処理をつけるのは面倒ですし 、処理内容とは関係のない内容で関数を埋めてしまうのにも抵抗があります。また、多くの機能追加の要請で必要な修正は コードの一部のコンポーネントに限られます。そのため一部のキャッシュだけは効かせたくない場合がありますが、 各変換処理にキャッシング処理をベタ書きした状態では対応が難しいです。

こういった問題を解決するため、最近はオブジェクトのファイルへのキャッシュ処理を自動化する Hideout という簡素なツールを作って利用しています。

Hideout: データ分析プロジェクト用、ファイルキャッシュ

Hideoutはオブジェクトを生成するタイミングでキャッシュファイルもあわせて生成するツールです。 実行時に環境変数で指定するキャッシュ設定がオンになっていてかつ生成されたキャッシュファイルが存在すれば、 キャッシュファイルをロードしてオブジェクトを返し、なければ指定された生成用関数を呼び出します。

基本的な使い方

たとえば以下の generate_large_object 関数はオブジェクトを生成するのに時間がかかります(人工的なサンプルですが)。

def generate_large_object(times):
    sleep(1000)
    return map(lambda x: x*2, range(times))

この関数から生成されるオブジェクトを Hideout でキャッシュするには以下のように記述します。

large_object = hideout.resume_or_generate(
    label="large_object",
    func=generate_large_object,
    func_args={"times": 10}
)

funcにはオブジェクトを生成する関数、func_argにはfuncを実行するのに必要な引数を辞書として渡します。

HideoutはデフォルトではキャッシュがOffになっています。そのため、 デフォルトではキャッシュはされず単にfuncに指定された関数を実行してオブジェクトを生成します。

キャシュをOnにしてオブジェクトを使いまわしたい場合には環境変数、HIDEOUT_ENABLE_CACHETrueを設定します。 ローカルで検証しているときにはコマンドを実行するターミナルで環境変数を指定します。

使用例

クックパッドのAlexaスキルで使用している質問応答用のKnowledge Baseを生成するプロジェクトではHideoutを利用して 生成される中間オブジェクトの一部をキャッシュしています。

該当レポジトリは Cookiecutter Docker Science テンプレートで生成されているので、 Makefileをワークフローの管理に使用しています。MakefileにはKnowledge Base生成用のターゲットを登録してあります。 ローカルにおける検証では以下のようにキャッシュをOnにして実行しています。

$ make generate BATCH_SIZE=500 HIDEOUT_ENABLE_CACHE=True 

テストではモデルファイルを使ったE2Eのケースも含まれていますが、同じように make コマンドに HIDEOUT_ENABLE_CACHE=True を指定した上で実行すると数秒で終わります。

$ make test HIDEOUT_ENABLE_CACHE=True

Hideoutにおいてキャッシュ設定はデフォルトではOffになっているので、CIやプロダクション環境で誤ってキャッシュファイルが生成されることはありません。 そのため修正時にはPull Requestをこまめに作り細かくコミットをプッシュすると、キャッシュが効いていない環境でテストが走るため、思わぬ不具合に気づけて便利です。

ステージごとにキャッシュOffを指定

本稿の前半で紹介したようにデータ処理をするプロジェクトには複数のデータソースを複数のステージで多段に加工するものがあります。

f:id:takahi-i:20191015114414p:plain
複数のステージから成るプロジェクト

上記の図ではデータを抽出した後「Preliminary1」や「Preliminary2」、「Transform」というステージがあります。 このような少々の複雑さをもつプロジェクトであっても機能拡張依頼が来たとき、多くの場合には修正する箇所は ソースコードの一部でしかありません。

このようなとき修正が必要な一部のステージだけキャッシュファイルをロードする処理をオフにしたいことがあります。この目的のために HideoutはHIDEOUT_SKIP_STAGESという環境変数を提供しています。たとえばキャッシュした ファイルを利用して実行したいが、Preliminary2Transform ステージだけはキャッシュを Offにしたい場合が考えられます。 このような場合、make build HIDEOUT_ENABLE_CACHE=True HIDEOUT_SKIP_STAGES=Preliminary1,Transform と キャッシュをしないステージを指示します。

Hideoutにおいて指定するステージ名はhideout.resume_or_generatelabelオプションで付与します。

large_object = hideout.resume_or_generate(
    label="Preliminary1",
    func=generate_preliminary_object,
    func_args={"times": 10}
)

今後

Hideoutを利用するユーザはプログラム中の関数ではなく、キャッシュファイルを生成している部分に適用する部分に処理を追加します。これはキャッシュする部分を追いやすくするためで、インターフェース名もHideoutが利用される箇所がわかりやすくなるように長く(resume_or_generate)なっています。 ただ最近、同僚から「デコレータでやったほうがシンプルなんでは」というコメントを頂きました。そこで今後デコレータのインターフェースも提供してみたいと考えています。

まとめ

本稿はデータ分析をするプロジェクトにおける一時オブジェクトを保存したファイル(一時ファイル)の扱いについて解説しました。一時ファイルはデータ分析の結果を高速に検証するのに便利ですが、安易に レポジトリに追加するとプロジェクトの保守性が下がります。

また、必要な入力データや中間オブジェクトをファイルにキャッシュする処理を追加するのも コストがかかります。そこで本稿の後半では、このようなプロジェクトを保守するのに役立つキャッシュツール、Hideoutについて紹介し、一時ファイルを利用しつつもレポジトリをクリーンに保つ方法について解説しました。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/