CyberAgentのアドテクインターンに行ってきた話をするよ

ぱじめまして。

先日、CyberAgentが主催するアドテクインターンに行って参りました。

学ぶことが本当に多いインターンで、自分の考えをまとめてそれに対して他の人からのフィードバックを貰いたいという思いが積もったために一筆したためました。
書き終えてみれば文字数も1万字を超えており、そこでふと振り返ってみる自分の頭に浮かび上がる1つのワードがありました。

今日はこれを是非みなさんに伝えたいと思います。

それでは聞いてください。


一万字越えててマジ万字

f:id:kitchen_py:20191013124301j:plain

アドテクインターンis何

アドテクっていうのは広告に関するテクノロジーのことです。

 広告分野は結構カオスなんですが、その仕組みの一つにメーカーさんとかの「広告出したい側」とブログ主なんかの「広告枠を持っている側(メディア側)」が、それぞれお互いの利益を最大化しようとする代理人をたてて広告枠をオークションする(RTB)というものがあります。

 今回のインターンではオークショニアの役割を果たすサーバが広告枠情報を送信し、それぞれの学生チームがその広告枠にいくらまで出すかを決めるサーバを実装します。

  • 期間
    3日
    開発は最終日のお昼までで、その後はシステムの運用フェーズ

  • チーム開発or個人開発
    チーム開発
    1チームはサーバサイド2〜3人+機械学習(以下ML)2人で構成される

  • 参加人数
    30-35人ぐらいだった気がする

  • 待遇・報酬
    遠方からの参加の場合東京までの交通費支給
    報酬は無いです

adtech.cyberagent.io

要件

 メンター側が運用しているメディアサイドのサーバ(Sell-Side-Platform)に対して、オークションを行う広告サイドのサーバ(Demand-Side-Platform)を実装&運営する。

  • 運営サーバからメディアの広告枠情報が各チームに毎秒2000リクエスト投げられる
  • 運営サーバから送られてくる広告枠情報を分析して、その枠にいくらまで出せるかを推定したレスポンスを返す
  • レスポンスは100[ms]以内 
  • レスポンスを受け取った運営サーバは、一番高く買ってくれるチームに対して落札完了の通知を行う
  • 各チームは予算を持っており、数時間にわたる運用フェーズ終了時には可能な限り使いきっている必要がある
  • 予算が尽きてオークションに参加しない時は204を返す等の細かいルール

 最終日はそれぞれのチームが作ったサーバの運用を行います。

インターン開催前夜

 渋谷駅に付いてるなんかすごく高いビルの高層階で顔合わせ会がありました。 ハッカソンタイプのインターンでは他のチームの人と関わる機会ってあんまりないと思うんですが、このインターンではチーム外の学生とも交流ができるようになってていました。ありがたい。

 チームメイトともこのタイミングで顔合わせできるようになっていて、お互いになにができる人だとかザックリどんな方針で推定しようかとかのお話ができました。インターン中だと時間制限があってゆっくり話せないし嬉しみがある。 あと、インターン前日の宿も手配してくれていて、これがなかったら近辺のカプセルホテルに泊まるとかになってたと思うので学生的には結構嬉しい要素でした。

f:id:kitchen_py:20190927211044j:plain
インターン前々日に泊まっていたネット繋がらなさそうなドミトリー(CAとは特に関係ない)

1日目

 午前中はアドテクの説明だけで終わりました。

 お昼ご飯を食べながら1時間ぐらいチームビルディングをして、午後から作業開始です。

チームビルディング

 チームビルディングではレクリエーション形式で自分が大事にしていることとか、逆に優先してないこととかをレクリエーションを通して共有しました。  この時点で積極的にリーダーシップを取ってまとめるタイプの人がいないことが分かったので、自分は普段人をまとめるタイプじゃないですが誰かがやらないとチームが回らなそうな予感がしたためにこのインターンではファシリテーターもどきの役割をすることにしました。あとこの時になんとなくMLとサーバサイドの間に立つコミュニケーションが必要なポジションは自分がやると良さそうかなーってざっくり考え始めてました。

言語選定

 まず、MLについてはデータ分析のためのライブラリが充実していることと習熟度からPythonを使うことは決定しました。これはそこまで議論は無かったと思います。

 次にサーバサイドメンバーが実装する部分についてですが、これについては結構迷いました。2つの選択肢が考えられていて、1つは毎秒2000リクエストをより確実に捌くためにGoかScalaを使う選択肢で、もうひとつがMLとの接続部分の実装を簡易にするためにPythonで実装する選択肢。

 GoかScalaを選ぶ場合はサーバの中で2つの言語の通信を行う必要があり工数が増えるというデメリットがある一方で、サーバサイドとMLでプログラムが完全に分かれるために責任範囲が明確になり、またコンフリクトしにくいというメリットがあります。

 Pythonを選ぶ場合はサーバサイドとMLサイドのプログラムを結合する部分が楽であるというメリットがありますが、実装が完了していざテストしてみると言語の速度の問題でリクエストを捌ききれなかった場合に絶望的な出戻りがあるというリスクがあります。

 結局、もしPythonを選んで言語の速度の問題でリクエストを捌ききれなかった時のリスクを回避するために比較的処理が早いGoを選ぶことにしました。

システムの設計

f:id:kitchen_py:20190916220431j:plain
実装したGoサーバとPythonサーバを通した処理の流れ
 フレームワークについてですが、Goは標準ライブラリが充実しているためにフレームワークを使わずに実装することにしました。 pythonサーバは確かflask使ってた気がします。正直あんまり覚えてないです。 2000QPSで送られてくるデータに対して高速に返答する必要があるため、DBにはKey-Valueでデータを取ってこれるRedisを採用しました。

 次にデプロイですが、タスク的にGCPでも足りましたがインフラをやってくれる人が新しいことに挑戦してみたいということでKubernetesを使うことにしました。

 全体的な処理の流れとしては、GoサーバはDBを参照して得たデータとリクエストごとの広告枠データをJSON形式のHTTPリクエストを使ってPythonサーバにPOSTします。そしてPythonサーバは前処理・推論の後にその広告枠の予測CTRを返します。次にGoサーバは得られたCTRに加えて、対象の広告主の予算をオーバーしていないか、制限時間が近いのに予算が余りすぎていないかなどから得られる係数を考慮して最終的な広告枠に出す価格をメディア側のサーバ(SSP)に返します。 最も高い価格を出していた場合は落札の通知がメディア側サーバから送られ、Goサーバは広告主の予算を更新します。

f:id:kitchen_py:20190927211049p:plain
オートスケーリングのイメージ

タスク割り振り

 さて、システムの設計は終わりました。次はタスクの割り振りです。

 サーバサイドですが、まずGCPのデプロイ経験があるサーバ担当の1人がインフラ担当、別のサーバ担当2人がGo言語が初めての人とGo言語慣れてる人のタッグでGoサーバを担当することになりました。 デプロイ経験がある人がいてくれるのはありがたい。

 MLは普通に二人でMLやるわけですが、2人とも共同で機械学習を行うのは初めてだったので、2人でやるいい感じの開発方法をいくつか考えてみたりしました。このフェーズが一番楽しかったかもしれない。

  • 弱肉強食作戦
     一番愚直な作戦で、それぞれに作った予測器を比較していい方を使おうというもの。

  • アンサンブル作戦
     2人が別々のCTR予測器を作ってアンサンブル学習する作戦です。  弱肉強食作戦と比較して一方のコードが全て消えるとかいう悲しいことは無いですが、 せっかく実効速度が早いモデルを作ってももうひとつのモデルがボトルネックになりそう。

  • 特長エンジニアリング+予測器作戦
     1人が効果的な特徴量を作り、もう一人が予測器の方に注力するという作戦。  個人芸のコラボレーションによって性能を上げる感じがとってもエモい。  予測器を作るだけだとその担当は余力が残りそうなので、追加でオンライン学習とか重みのオンラインストレージ保存とかやってもいいかもしれません。  個人的に推しな作戦です。

 まあそれはとりあえず前処理終わった時点で決めればいいかということで判断を保留して、最低限MLの間で何をどこに実装する必要があるのかを共有するためにペアプロ形式でファイル構成とかをやりました。

 やるべきことは決まった、さあ後はゴリゴリ実装するだけ。 だけどまぁデータの前処理せな話が始まらんよなぁということで、前処理担当とPythonサーバのモック担当に分かれて作業することにしました。 この時点で相方があんまり話すタイプでは無かったので、Goサーバ側と頻繁に打ち合わせをす必要があるPythonサーバのモック担当を自分がやって、相方にデータの前処理をお願いしました。 うん、バランス良さそうに見える。いい感じですね(フラグ)。


当時のぼくの心境「よしよし、いい感じに決まってきてる。gitあんまり使ったことない(?)人がいたり口数少ない人もいるけどみんなやること分かってるしなんとかなるやろ。」

いざ開発

 Goサーバ側はGoに慣れている人が速攻でモックを作り上げてくれました。すごい。

 ML担当である自分の相方はデータの前処理が結構長引いていて自分がPythonサーバのモックを作った後にしばらく空き時間ができたので、Makefile書いたりデプロイのために使うライブラリのバージョンをリストアップしたり、MLなのに負荷テスト用のサーバをGoで書いたりと独立したタスクを刈り取ってました。  サーバと自分のタスクは順調に進んでいたんですが、デプロイとデータの前処理については手間取っているようでした。デプロイはその人にとって挑戦的なことをしているから時間かかるのは仕方ないけど、もっと人に聞きながら進めてもよかったねという反省を得て明日からはメンターさんに細かく聞きながら作業を進めるという方針になりました。  チームで決めていた一日目の目標は最小構成で統合して動作テストをすることでしたが、 結局ローカル環境でGoサーバとPythonサーバを導通させてテストすることも最小構成のデプロイもできてはいませんでした。


ぼくの心境「まぁちょっと進捗ヤバイけど、みんな今日の夜に作業してくれるみたいだしなんとかなるやろ」

f:id:kitchen_py:20190927211047j:plain
一日ごとにやったKPT

2日目前半

〜 午前の作業が終わり、2日目のお昼 〜
ML : 「データ前処理、、、終わってません」
デプロイ : 「最小構成のデプロイ、、、できてません」
f:id:kitchen_py:20190927200014p:plainf:id:kitchen_py:20190927200014p:plainf:id:kitchen_py:20190927200014p:plain


ぼくの心境「あかん、なんともならんかもしれん」

 結局データの前処理は大体2時間程度でおわるという共通認識でしたが2日目のお昼まで長引いてしまったため、この時点で自分の「 特長エンジニアリング+予測器作戦」とか「アンサンブル作戦」だとかモデルの妥当性を検証するだとかの計画は瓦解しました。

 これによって俺達コミュニケーション取れてなかっんじゃねって認識が受け入れてもらえて、その問題を仕組みで解決するべくメンターさんからのアドバイスでMLの間で「30分ごとの進捗報告」を取り入れてみることにしました。これは実際かなり上手く機能していて、これをきっかけにMLはチームとして機能し始めました。

2日目後半

 前処理が終わってからの自分はロジスティクス回帰の学習部分を書いていたんですが、何故か予測値はテストデータに対して0しか吐き出さないという状況に悩まされてました。
データの不均衡性が怪しかったのでclass_weightを考慮したりダウンサンプリングを使ったり、それでも良くならなくて教師データを見直したりで午後はこの問題に対応していました。

 データの不均衡周りを解決した辺りで施設が閉まる時間になり、MLは場所をスタバに移して深夜作業のための打ち合わせしたりしてました。直接会えない時間帯のコンフリクトや大きな出戻りが怖かったので。この時やったこととしては、共通して使う部分のコードの読み合わせと誰がどのファイルに何を実装するのかという確認です。そして深夜にやることとしては自分がストリームデータを受けて推定値を返すという最低限のPythonサーバの実装を担当して、相方がAUCなりの評価指標を使って推定器の評価をしてから寝ようねっていうことで解散しました。

f:id:kitchen_py:20190927211035j:plain
おしゃんな開発

2日目深夜

 2時ぐらいに寝る予定でしたが、途中から気分がノッてきて朝まで実装してました。

f:id:kitchen_py:20190927211056j:plain

 後はパラメータ調整するぐらいで、もうデプロイ班に引き渡してもいいぐらいまでには進捗が進みました。というか、空が徐々に明るくなってきているぐらいでちゃんと起きれる自信が無くなっていて、後は自分がいなくてもMLは大丈夫って言えるぐらいまで進ませなければならない必要性がでてきたのでやりましたって感じです。 やったことは以下です。

  • ストリームデータから推論する実装
  • ロジスティクス回帰の重みを外部保存、デプロイ時はロードのみを行う実装
  • 統合して動作確認
  • 負荷テスト

 CAに来る前の別のインターンではタスクに優先順位をつけて作業することができていないという反省があったので、ここではタスクに優先順位をつけて上から順に消化していくことに気をつけました。 評価がおざなりではありますが、MLサーバがやっておくべき最低限の機能と動作検証は大体出来たので個人的には満足な一夜でした。

3日目後半

 午前までは開発期間で、12時で一旦完全にコード編集を閉め切りになります。 午後はシステムの運営をしながら成果発表のためのパワポを作って発表します。

 サーバが謎の現象で落ちているチームもあったしローカルで動いていてもデプロイした後に上手く動かなくなったチームもあったりしてました。 自分たちのチームも例にもれず、デプロイしてみると処理速度が遅くてタイムアウトしていたりといったいうエラーに悩まされました。 Pythonサーバは高負荷時でも5[ms]程度だったので、おそらくGoサーバかどこかで動作が遅くなっていたということらしいです。

 この時自分は静かに発表用パワポを作っていることしかできず歯痒い思いをしてました。ちゃんとデプロイまわりも通して全体が分かるようになりたい...

f:id:kitchen_py:20190930205904j:plain
プレゼント待ってます



振り返りパート

 ここまでがアドテクコンペのインターンの流れを追った実体験という話で、ここから先は振り返りとか懺悔とかやります。

1日目: チームビルディング・コミュニケーションの振り返り

 チームメイト同士で何がどの程度できるかの技術レベルを把握しきれてなかったのはミスでした。
割り振っていたタスクがなかなか終わらず、丸一日自分の作業が滞る事態が発生していました。

 自分が知らない分野の技術レベルを測るのってやっぱ難しいです。でも例えばデプロイをやったことがないがデプロイをやりたといった場合、その人が出す目安の「〇時間かかる」の信用度はその人がソフトウェア全般に対してどの程度の経験があるかで大まかに推測できたと思います。

 問題を回避するためのよりいい方法としては、今までに作った作品の見せ合いをするとかが考えられると思います。
副産的に「方向性は自分とは違うけど、君結構おもろいことやっとるやんけ!」ってな具合に互いを認め合う機会にもなったんじゃないでしょうか。

1日目: ミーティングの振り返り

ぼく 「あの~、チーム内でタスクの重さとかの共通認識持ってたいんだけど、いい?」
みんな「(カタカタ...)」

ぼく

f:id:kitchen_py:20190916194416p:plain
からすさん絵

当時の自分としては「ミーティング中は話を聞くのが普通では?」って考えでしたが、時間をおいて考えてみると自分の話し方にもいくつか改善できる点が見つかりました。

  • ミーティングの必要性が説明されていない
  • その話にどれぐらいの時間がかかるのかが明確ではない

なので二日目からは「認識の違いから来る出戻りを少なくしたいから、これについて目安5分で話そう」っみたいに時間と必要性を明確にするようにしました。 そこに気をつけるようにしてからはみんな話を聞いてくれるようになったのでいい改善だったんじゃないかと思います。

1日目: 弱みをさらけ出せてなかった

やっぱり初手で弱みをさらけ出すのは超重要だと思います。再確認しました。 例えば「集中力切れると不機嫌になる」とか「コミュニケーションが苦手」とか言いにくいことは初手で消化しときましょ。これ大事。

1日目: 言語選定の振り返り

 結果的にではありますが、言語選定でPythonとGoを分けたのはかなりいい選択だったと思っています。というのは、与えられたタスクにおいてはモックを一度作ればサーバサイドとMLサイドが歩調を合わせて実装するべきところが少なく、それぞれのプログラムが完全に分離されていることによってMLだけでテストしたりするのが非常に楽だったためです。ROSがなぜあの形になっているのかがよく分かる体験でした。

 あと、後から分かったことなんですがチームメンバでgitに慣れていない人が多かったためにプログラムのロールバックが起こったりしていたらしいんですよね。これがもし全てPythonでやっていたのなら必然的に複数人が同じファイルを触るケースは多くなるわけで、きっとコンフリクトは勃発するわロールバック祭りだわで大変なことになってたと思います。

f:id:kitchen_py:20190930205801j:plain
唐突に現れる渋谷周辺のラーメン春日亭、これがまたおいしい

2日目:データの前処理の長引き

これは非常に考えさせられるテーマでした。

ハッカソン中にこの問題を振り返った時の思考プロセスはこんな感じでした。

  1. データの前処理は必須のタスクで絶対にやらなくてはいけない
  2. もし自分が前処理をしていたら早く終わっていただろう
  3. でもその場合は相方はサーバ経験とGoの経験を持っていないために数時間手が空いてしまっただろう。それに、Goサーバでも遅れがあったから自分が手伝いに行く必要性もあった。 ↓
  4. もしハッカソン開始時点で前処理をやってもらうと時間がかかることが分かっていたとしても今の状況を選んでいたんじゃないか。

これに対する懇親会やカンファレンスなどでいただいた意見を列挙しておきます。

  • アジャイル的な考えでいくならば、タスクを可能な限り細分化する * 一般的には有効な方法だと思います。 ただ、今回はタスクは要素を分解して二人で並列してやっていくことはかなり非効率的なものだったために分けることはできなかったと思っています。

  • 進捗が「少し不安かな?」って思ったタイミングでペアプロに切り替える
    これは目からウロコでした。 タイミングによっては言い出しづらいことですが、当時の自分はムードメーカーとしての立ち回りをしていた(つもりな)のでペアプロする?みたいにフラットにフォローしに行けたはず。

f:id:kitchen_py:20190930210332j:plain
堅い話をしてしまったので場を和ませに来てくれた渋谷のラーメン風ジャンクフード、これがまたおいしい

まずは最低限の実装をしよう

相手が求めている最低限の実装を先にCommitする習慣を付ける必要性を感じました。 自分はこだわるところにはとことんこだわるタチなので、先にセーフティネットを用意してからこだわるようにしていきたいと思います。

分からないことを分からないって言うこと、無知をさらけ出すこと

ハッカソンはいろんな分野の人が集まるので必然的に自分の知らない技術とかライブラリも出てくるわけで、 そこでプライドを捨てれなくて「あ〜それね、知っているが??」みたいになるとせっかくの知るチャンスを失うことになるので勿体無いです。

その点で言うと自分は馬鹿みたいに「Redisってなに?Kubernetesってなに?」って聞きまくることができていて、無知をさらけ出すことについてはガチプロだったと思います

f:id:kitchen_py:20190930210622j:plain
もはや渋谷にすら関係がない。次郎系ラーメン店の総本山

時間の見積もりの甘さ

「その作業どれぐらいで終わる?」って聞かれた時、自分はこの時間までに終わりたいっていう目標の時間を伝えていました。 それで目標の時間はあくまでも目標なので大体オーバーするんですよね。 1時間で終わるって言ってたものが3時間とかかかってたら当然相手は不安になるわけで、これは良くない癖だと認識できたので改善策を立てていきたいと思います。

改善策

  • バッファをとった時間を伝える
    目標の時間を持つこと自体は悪くないと思うので、「目標としては1時間で終わらせたいけど、2時間ぐらいは見ておいてほしい」っていうふうに正直に伝えようと思います。
  • 遅れそうになった時に伝えること
    遅れることには理由があるはずで、その理由とあとどれぐらいかかりそうかを伝えられればチームメイトも安心。

運用フェーズに入ってからの問題発生はどうやったら避けれたか

私達のチームでは、運用フェーズに入ってからローカル上では見えなかった問題が発生していました。 これの根本的な原因としては自分を含むML班やGoサーバ班がGKEによるデプロイのタスクの重さを見誤り大きく遅れてしまったためにGoサーバが細かくデプロイして検証していくことができず、結果として運用でエラーがでたと解釈できます。 もし自分達がGKEがリスキーだと認識していたならば、まずはGCEでデプロイできるようにセーフティネットを張って、その後に挑戦的なことをやろうと提案できていたんじゃないでしょうか。

これを経験して流石にデプロイを知らなさすぎるということで、具体的な改善方法としてDocker本を買ったのでこれを読んだうえで自作サービスをデプロイすることで理解を深めていこうと思います。 f:id:kitchen_py:20190927211041j:plain

ペアプロの際にvimで感動を与えることができた

うろ覚えですが「vimって使い手によってここまで変わるのか...」って言葉を引き出せました。 自分の力はエディタ界隈においてまだ小学生レベルですが、それでも誰かに認められるのって嬉しいですね。

まとめ

 手が空いた時は片っ端から浮いてるタスクを刈り取っていけたり、ML担当になのにGo書いてたり、一夜でロジスティクス回帰の学習部分実装と外部への重み保存とストリームデータから推論する実装と統合テストと負荷テストを終わらせたり、個人としてはこれ以上無いほどにベストを尽くしました。 他にもサーバ間の通信部分などコミュニケーションが必要となる部分を率先して担当し、密に確認をとりながら進めることで大きな出戻りも無く終われたことは目立たないながらもファインプレーだったと思います。

 ただ個人としてベストを尽くした一方でチームがよりうまく回るためになにかできたことがあったんじゃないかという思いもあり、実際にやってるときはベストのように思っていても時間をおいてみるとよりベターな方法があったんじゃないかと考えさせられることが多いインターンでした。

 今までは自分は一人のエンジニアとしてチームの足りてない部分を埋める役割かゴリゴリコードを書く役割が多かったのですが、今後はエンジニアという枠にとらわれずに必要があればファシリテーターのような役割を補う選択肢も考えていこうと思います。

最後に

 一緒に実装したチームのみんな、メンターの方々、エモいが口癖になってたメンターの方々、そしてCAを紹介していただいたyodaさん、本当にありがとうございました!

あと最後に1つ、これから誰かと一緒にコードを書くみんなに伝えたい。
人に渡すコードには分かりやすい変数名を使ってくれよな! f:id:kitchen_py:20190927211045j:plain