データ基盤 データエンジニアリング

データ基盤チームでサイクルOJTを実施しました

データ基盤 データエンジニアリング

ペパボでは、「サイクルOJT」と呼ばれる独自の研修プログラムが実施されています。 このプログラムでは、新入社員が毎月異なる事業部に所属し、幅広いスキルと知識を獲得します。

8月は技術部内のデータ基盤チームで、2つのグループに分かれてOJTを行いました。 本記事では、新卒13期生がこのOJTで取り組んだ内容をご紹介します。

  1. データ異常検知
    1. 背景
    2. 動機・目的
    3. やったこと
    4. 感想
  2. bigfoot-banditリニューアル
    1. 背景
    2. やったこと
    3. 学んだこと
    4. 感想
  3. おわりに

データ異常検知

13期の@donokun@n01e0です。僕たちは今回のOJTで、データの異常値を検出する仕組みを作りました。

背景

ペパボではBigfootというデータ基盤を運用しており、ユーザーの行動ログの収集やその利活用を行なっています。詳しくは過去の記事をご覧ください。

Bigfootにはminneやsuzuriといったペパボの各サービスから、日夜大量のログが送信されてきます。 そしてこれらのログを元に機械学習を行いサービスの改善に役立てたり、サービスでユーザーに提供する統計情報を作成したりしています。

動機・目的

複数のサービスとつながり、機能を提供するBigfootではすでに監視が行われています。 例えばデータの流量を監視し、異常があれば通知するといったものです。 その他にも、Bigfootにデータを流入する手前で、suzuriやminneなどの各サービスによる監視も行われています。 そんな中で今回追加する監視ではデータの異常値を検出することを目的としました。

仮想的な例として、アプリケーションから送信したユーザーの行動ログを機械学習に活用している状況を考えます。 アプリケーションからBigfootに送信する行動ログの中には監視のためのダミーデータや無関係なデータが含まれるため、前処理で除外し、加工をした上で機械学習モデルに供給します。 フィルタイリングで除外対象としていた文字列が、仮にアプリケーションのライブラリのアップデートによって変化した場合、 除外すべきデータの影響を受けたデータが機械学習モデルに与えられることになり、期待する推論結果が得られないため困ります。 このような不具合は、ユーザーに影響があるにも関わらず、発生に気がつきにくいことが問題です。

このようにデータが異常値をとるケースは、既存のBigfootに実装されている異常検知の仕組みでは検出できませんでした。 アプリケーションからBigfootに送られる行動ログの量は変化しませんし、機械学習モデルに与えられるデータ型も変化しないからです。

今回の取り組みでは、このようなデータの異常値を検出することを目的としました。 また、単に異常を検知するだけでなく、検知の設定が楽であることも重要です。 例えばルールをそれぞれのデータに対して設定し、それに反するデータを検出するツールがありますが、 ルールを設定するためには、データの内容を理解する必要がある上に作業の量も膨大になります。 このような手間をかけない運用を実現することも、今回の目的の一つです。

データ異常検知チームのスライド

やったこと

TensorFlow Data Validation というOSSを参考に、今回の目的に必要な機能を実装しました。 TensorFlow Data Validation は、機械学習のワークフローの中でデータの異常値を検出するためのツールです。 このツールは、データに対してスキーマを定義し、そのスキーマに反するデータを検出します。 スキーマは、データの型や値の範囲、値の分布などを定義できます。

今回私たちが実装したのは、正常データからスキーマを自動生成し、そのスキーマに反するデータを検出する処理です。

異常検知ライブラリの実装

これらの処理をCLIとBigfootの双方で実行できるようにすることで、 検証処理のCLIでの試行錯誤と、Bigfootでの定期実行による自動的な検証を行えるようにしました。

このような仕組みを作った上で、実際にデータの異常値を検出するための処理を追加しました。 時間の問題で効果検証を十分に行えていませんが、ダミーデータの検出には成功しています。

感想

donokun

データの異常を検知する仕組みを考えるのは初めてで、何を持って正常・異常を切り分けるかの設計や、処理の実装を考えるのは面白かったです。 また、異常が検知されなかった場合、その仕組みは何を保証してくれるのかという観点を持つことができたのも良かったと思います。

1カ月で計画を立て、チームで実装を行うのは難しく、仕事の進め方の点で多く苦労しました。 次の仕事では仕事を着実に進めることを意識して取り組んでいきます。

n01e0

「異常検知」という点から、アンチウィルスに近い感覚を取り込めるのではないかという甘い見積もりをしていたが、実際はそこまで動けなかった。

1カ月という短い期間の上、さらに夏季休暇やその他のイベントも重なり、スケジュール調整が一番難しく、タスクを分解することの重要性がよくわかるOJTでした。

bigfoot-banditリニューアル

こちらの課題は@yumu@kromiiiで取り組みました。

背景

minneとSUZURIでは、多腕バンディットアルゴリズム(以下、bandit)を活用してアプリケーションの挙動を動的に変更しています。 多腕バンディットについて、詳しい内容はペパボ研究所のスライドをご覧ください。

これまでbanditを使用するには、複数の社内向けgem(bigfoot-bandit、bigfoot-client、rack-bigfoot)のインストールや設定、さらには多数のコード変更が必要であり、これがbanditの利用ハードルを高くしていました。

そこで今回のOJTでは、この導入プロセスの簡略化を目指し、bigfoot-banditを再設計しました。 将来的には、banditによる比較検討を当たり前にしたいと考えています。

やったこと

今回は、大きく分けて2点の改修を行いました。

1点目は、バンディットの腕を取得する部分です。 以前の仕組みでは、多腕バンディットで使用する腕を特定するためのcurrent_groupメソッドの導入に多くの設定や手続きが要求されました。例えば、initializerにはbanditの使用シーンごとの設定や評価アルゴリズムの定義、そしてcontrollerには複数のbefore_actionの設定が必要でした。

banditリニューアルチームのスライド

before_action -> { set_testable_scene(:products_layout) }
before_action :set_testable_group_by_key

def index
  @shop = SHOPS.sample
  @products = (0...PRODUCTS.size).to_a.shuffle[0,10].map{|i| [i, PRODUCTS[i]]}
  template = case current_group
             when 'pop'
               :index_pop
             else
               :index_cool
             end

  render template
end

def set_testable_group_by_key
  testable_key = uid
  set_testable_group
end

そこで今回、新しくget_armメソッドを定義し、シーンごとのさまざまな設定を引数として受け取ることができるように変更しました。これにより、新しい使用シーンの追加時にも、controllerの一部分のみを変更すれば済むようになりました。

def index
  @shop = SHOPS.sample
  @products = (0...PRODUCTS.size).to_a.shuffle[0,10].map{|i| [i, PRODUCTS[i]]}
  arm = get_arm(
    scene: :products_layout,
    groups: %w[pop cool],
    selection_identifier: uid,
  )

  template = case arm
             when 'pop'
               :index_pop
             else
               :index_cool
             end

  render template
end

2点目はバンディットの報酬を取得する部分です。 多腕バンディットは機械学習のカテゴリで言うところの強化学習に分類されます。強化学習は、報酬を最大化するように学習するため、報酬を取得する部分は非常に重要です。

これまでのバンディットでは使いたい場面ごとに、報酬を取得するためのメソッドを定義する必要がありました。例えば、商品一覧ページでのバンディットの報酬を取得するためには、以下のようなコードが必要でした。

case scene # banditを使用するシーン毎に処理を分岐
when :pop
  Bigfoot::Bandit::EpsilonGreedy.import_rewards(scene)
when :cool
  Bigfoot::Bandit::LinearThompsonSampling.import_rewards(scene, span: 1.day)
end

しかしリニュアルしたバンディットでは以下のようなコードで一括で全ての場面に対する報酬を取得できます。

Bigfoot::Bandit::Job.import_rewards_all

場面ごとの処理が不要になることによって、バンディットの使用シーンの追加が容易になりました。

学んだこと

kromiii

Ruby の gem を開発するのは初めてだったので、gem の作り方やテストの書き方などを学ぶことができました。gem は他のアプリで使われることを前提としているので、他の人が使いやすいように設計することが大切だと感じました。また、開発を進める中で既存の OSS への修正を提案したり、バグを報告したりすることもありました。これまでお世話になっているばかりだった OSS に対して貢献する方法を学べたのは良い経験になりました。

yumu

今回の目標は、banditをサービスに容易に導入できるようにすることでした。 ユーザーの体験を最優先に考え、使いやすさやREADMEのわかりやすさに注力することは、これまであまり経験したことがなかったので有意義な学びとなりました。 さらに、エラーが発生した際にユニットテストを活用することで、問題を迅速に特定し解決する方法も学びました。

感想

kromiii

研修が終わって初めてのOJTということもあり、これまでと違う ユーザー目線 での開発を意識して取り組みました。開発を始めた当初は「できることから始めよう」と手をつけやすいところから改修するプランを立てていたのですが、メンターの @pyama86 さんから「ユーザーの目線に立って、ユーザーが使いやすいと思えるにはどうしたらいいか?」という質問を受け、「できそうかどうか」ではなく「どうしたら使ってもらえるか」から考えることが大切だと気づかされました。来月からは別の部署でのOJTになりますが、今回の経験を活かして、ユーザー目線での開発を意識して取り組んでいきたいと思います。

yumu

予定よりは時間がかかりましたが、OJT期間中に設定された課題を無事に完了し、大いに達成感を感じています。 gemの開発や、複数の事業部にまたがるプロジェクトは初めての経験であり、非常に多くのことを学ぶことができました。 データ基盤チームのエンジニアの方々は非常にレベルが高く、質の高いレビューを受けることができて楽しかったです。 今後は他の事業部でのOJTが控えているので、今回の学びを活かして仕事の量と質をアップデートしていきます。

おわりに

この記事ではデータ基盤チームでのサイクルOJTの様子をご紹介しました。

9月からはまた別の部署で、今回の学びを活かしつつ新たな課題に取り組んでいます。

データ基盤チームの皆様、ここまで読んでくれた皆様、ありがとうございました!!