LIVESENSE Data Analytics Blog

リブセンスのデータ分析、機械学習、分析基盤に関する取り組みをご紹介するブログです。

LivesenseDataAnalyticsBlog

MENU

リブセンスのデータ専門組織のこれまでとこれから

はじめに

こんにちは。テクノロジカルマーケティング部の谷村です。

テクノロジカルマーケティング部(以下、テクマ部)は、 リブセンス内のデータ分析や機械学習、そのための基盤開発までデータまわりを手広くやっている部門です。

リブセンスはHR領域や不動産領域を中心として複数のメディアを運営しています。組織的にはメディア毎に事業部を編成する、いわゆる事業部制を採用しています。 メディア毎の意思決定スピードや戦略の柔軟性、等々が事業部制のメリットかと思いますが、テクマ部についてはこれら各事業を支援する形で横串の横断組織として編成されています。 横断の組織としているのは所属メンバーの専門性を高める目的や、全社状況にあわせたアサインメントを行う目的などがあります。 今回は、リブセンスがテクマ部を中心としてどのようにデータと向き合っているかをご紹介させていただきます。

リブセンスのデータ分析のこれまで

私がリブセンスに入社したのが2013年でしたが、リブセンスでは当時からデータドリブンな意思決定を大事にする文化がありました。 例えば、営業でもSQLを使うという文化、入社初日に目にして驚いたのを今も覚えています(営業さんまで、社員全員がSQLを使う 「越境型組織」 ができるまでの3+1のポイント)。

そんな中でここ数年のデータ関連のテクマ部での取り組みは、大きな流れとして下記のように進展してきました。

時期 概要 詳細・データ
~ 2014 アドホック分析中心 データは主に会員マスタや売上等のメディアのデータベースを中心に利用。各施策や広告効果などを多変量解析や時系列等で分析。
~ 2016 自社分析基盤の開発
機械学習のサービスへの適用
Web上での行動ログとメディアのデータベースを一元管理するようにDWHを整えたことで、全てのKPIを連続的に分析することが可能に。さらにユーザーに紐付いた行動ログが蓄積されたことで、レコメンドアルゴリズムや予測モデルのメディアへの組み込みが可能に。
2017 ~ 機械学習のインフラ整備
UX専門のグループ立ち上げ
データについてはアクセスログに限らず外部データを含めた取り込みを強化。各メディア毎にバラバラに開発してきた機械学習システムについて、基盤の統一を開始。

2015年には分析基盤となるDWHも整ったことで、個別の事業での機械学習の活用も進み、収益面でも大きな成果を残すことが出来ました。 そして昨年からは次のデータ活用フェーズを念頭に動きを開始しています。こちらの背景については後ほど説明させてください。

データに関わるメンバーの主な役割

弊社では各人を細かく職種で分けることはやっていませんが、大まかに意識している役割は下記のような形です。

役割 業務内容
データエンジニア 分析基盤開発・運用、データ収集、大量データの操作
機械学習基盤エンジニア 機械学習システム(インフラ・コード)の開発を担当
アルゴリズム開発者 アルゴリズムの実装、検証
データアナリスト データサイエンスを活用したプロダクト改善の企画立案
データベースマーケティングの推進
UXリサーチャー/UXデザイナー UXデザインの手法を駆使してサービスの課題・潜在ニーズの発見・解決方法の検討から実行を支援

正直なところ、呼称は定まっているわけではなく、役割も社内外の状況にあわせて年々変化しています。 例えば、機械学習基盤エンジニアとアルゴリズム開発者については以前は区別せずに「機械学習エンジニア」としていました。

各自のコアスキルの向上や、プロジェクトチームの編成を考える際にこれらの役割を意識していますが、現実には複数の役割を兼ねて動いているメンバーが多くなっています。

現在のテクマ部内のチーム構成

テクマ部では今年から3グループ体制で動いています。

グループ 所属メンバー ミッション
Data Marketing データアナリスト
UXリサーチャー/UXデザイナー
データからユーザー価値への転換の設計プロセスを担うミッション
Data Platform データエンジニア
機械学習基盤エンジニア
アルゴリズム開発者
データからユーザー価値への転換する仕組み(システム)を作る
Infra Structure インフラエンジニア 全サービスのインフラに責任を持つ

グループ化の背景については後述しますが、ここでテクマ部の中に社内の各サービスのインフラを担当するインフラエンジニアのグループを配置しているのが弊社の特徴です。 データに関わらずインフラ全般を担当しているため、データに関わる役割での紹介は省略しましたが、部内にインフラエンジニアがいることでシステム運用経験豊富なメンバーから助言、ノウハウ共有を行ってもらえると同時に、開発時のハブの役割も果たしてもらっています。

これからのこと

リブセンスでもここ数年で、既存の仕組みをより効率的にまわすことに対してデータ分析や機械学習が有効に機能するようになっています。 これからもそのようなアプローチは継続していきますが、一方でそのような効率化のアプローチは限界を抱えていると考えています。 特に大きいのが、データそのものに変化を生み出せないことです。 人工知能という言葉も流行し、各社とも取り組みを強化されていますが、大抵の場合、ユーザーへの恩恵が大きいのはアルゴリズムの洗練よりも、どのようなデータを収集するかということと、収集したデータからどのような価値への転換を行ってユーザーに提供するかということだと思います。 既存のデータを使って効率化のためにデータを使っているだけだと、この肝心のデータが成長しません。

そこで次のチャレンジとしては、データの使い方だけではなく、データそのものから考えていくフェーズに移りたいと考えています。 取得するデータの設計であったり、どうやったらそのデータを蓄積し続けることが出来るかというデータが集まる仕組みの設計、 そしてデータから価値への変換の設計、ひいてはサービス価値の定義、そしてそれを実行できるアルゴリズムの実装と、試行錯誤しながら取り組んでいきたいと考えています。下図でいうと、緑の吹き出し部分以外への注力をより高めるイメージです。 会社としても弊社代表の村上がリアルデータエンジニアリングという呼び声で推進している取り組みになりますが、事業部門と協力して進めていきます。

f:id:livesense-analytics:20180109095609p:plain

上記のような背景で、テクマ部でも「データから価値への変換の設計」を行っていくことを意識してUXDを専門に取り組むグループを昨年組織しました。 さらに今年はUXとアナリストを1つのグループに配置しData Marketingグループとしました。性格の異なる分野を1つにまとめた実験的な試みですが、事業部門と共同で新しいサービスの種を見つけていけることを期待しています。

また、コンセプトが決まればデータの性質や用途によって適切なアリゴリズムを開発する必要が出てきます。しかもなるべく高速に。 そこで、これまではアドホックに各事業への機械学習を導入してきましたが、 今後はアルゴリズムの開発・検証を高速でまわせるように分析基盤に加えて機械学習の基盤の開発にも力をいれるべく、Data Platformグループを編成しました。

さいごに

少しでもリブセンス(特にテクマ部)に興味を持っていただいた方向けに追加情報です。

仕事の進め方

役割によって異なりますが、データエンジニアのように運用すべきシステムを持っている場合はチームでタスクを分割して働いています。データアナリストやUXリサーチャーはプロジェクトチームを組んで動くことが多いです。

分析ツールや環境は、タスクの特性と各人の好みでわりと自由度高くやっています。Pythonを使うメンバーもいればRを使うメンバーもいます。最近ではJuliaも使い始めたりしています。 分析基盤のDWHはAWS Redshiftを中心に構成していますが、機械学習のインフラにはGCPを選んでいます。これらについては、今後のブログでも紹介していく予定です。

日常の学習

新しい技術もどんどん出てきている領域なので、業務を通じて学習しています。リブセンス自体が勉強会が盛んな会社なのですが、テクマ内でも毎週のように勉強会を行っています。 論文輪読会や、統計モデリングの勉強会等の分析・機械学習系の勉強をはじめ、Amazon Redshiftなどそれぞれの業務にあわせた勉強会を開催しています。隣に議論出来るメンバーがいるのは楽しいです。

募集について

リブセンスでは一緒に働いていただける仲間を募集しています。 変化の激しいデータ周辺の領域ですが、それにあわせてリブセンスでは組織もミッションも変化していますので、変化を楽しみたい方にはおすすめです。

少しでも興味を持っていただけた方は弊社採用サイトから、是非エントリーお願いします!

将棋盤を画像認識する

Analytics チームで転職会議のレコメンドを開発している @na_o_ys です。今回は業務のことは忘れて、趣味の将棋の話をしたいと思います。

この数年で将棋の学習環境はずいぶんリッチになりました。通勤電車では将棋アプリのネット対局をして、自宅ではオープンソースの強豪 AI を使って棋譜検討し、日々将棋を楽しんでいます。
一方で、顔を突き合わせて盤と駒を使って指す対局が一番楽しいのは変わりがありません。 リアルの対局を AI で検討するために、盤面を手軽にコンピュータに入力したい というのが今回のテーマの発端です。

TL;DR

f:id:na_o_s:20171220134558p:plain

盤上の駒を高い精度で推定することができました。

処理は大きく 2 つのステップからなります。

  1. 盤面の正規化
    • 盤面の四隅の座標を特定し、元画像から正規化画像への射影変換を得る
  2. マス目毎の内容を推定する
    • マス目毎に画像を切り出し、駒の有無・種類を推定する

ちなみに上記画像は私と同僚の将棋で、現局面は後手番です。後手玉は詰めろ飛車取りですが次の一手を考えてみてください。

ステップ 1. 盤面の正規化

正規化画像への射影変換を得るには、盤面の四隅の座標を特定できれば十分です。画像処理の要素技術としてエッヂ検出、輪郭検出、線分検出など多様な特徴抽出手法があります。今回は、 輪郭検出 + 線分検出 + 焼きなまし法 を組み合わせることで、高い精度で四隅の座標を特定することができました。

輪郭検出

将棋盤の大まかな位置を特定するために 輪郭検出 を利用します。

f:id:na_o_s:20171220182735p:plain

大まかな位置は特定できますが、マス目とぴったり一致しません。輪郭検出では罫線だけでなく将棋盤の端や机の角も検出してしまうためです。ここで特定した大まかな座標を出発点として、次に紹介する焼きなまし法でイテラティブに精度を高めていきます。

なお輪郭検出の結果は多角形ではなく曲線のため、直接四隅の座標は得られません。四角形の頂点座標を得るために以下の処理を行っています。

1. 輪郭検出
2. 凸包で近似
3. 凸包の頂点から四隅の座標候補 (最も正方形に近いもの) を選択

線分検出 + 焼きなまし法

焼きなまし法 はパラメータ (ここでは四隅の座標) を振動させながら、目的関数が最小となる値を探索する手法です。

以下の要件を満たす目的関数を設計する必要があります。

目的関数 f:
    f(四隅の座標候補) = 真の座標にどれだけ近いか

まず 線分検出 により将棋盤の罫線を大雑把に抽出します。これを 罫線画像 と呼ぶことにします。

f:id:na_o_s:20171220182805p:plain

目的関数の中で、与えられた四隅の座標をもとに マスク画像 を生成します。マスク画像は四隅の座標から仮定される罫線の位置に近ければ近いほど明るく、罫線から遠いと暗くなるようにします。

f:id:na_o_s:20171220182915p:plain

このマスク画像を罫線画像に重ねると、罫線の重なり具合が大きければ多くの罫線が透過されるのに対して、重なり具合が小さければ一部しか透過されないことが想像できます。 すなわち マスク画像と罫線画像の内積 が目的関数として利用できるということです。

この目的関数を利用して焼きなまし法を行うことで、輪郭検出結果からぴったりした座標が得られます。

f:id:na_o_s:20171220182927p:plain

四隅の座標が得られればマス目毎の画像を得るのは簡単です。

f:id:na_o_s:20171220183118p:plain

ステップ 2. マス目の内容推定

ディープラーニングします。

学習データ

f:id:na_o_s:20171220125829p:plain

学習データはインターネットで集めた約 400 枚の駒画像 (手作業でひと駒ずつトリミングしたのですが、この作業が一番大変でした) と、iPhone のカメラで撮影した盤面 10 枚です。盤面はテレビ画面やネット対局のキャプチャを含みます。

ステップ 1 で作成した盤面正規化プログラムを利用して事前にマス目毎の画像に分割し、手動で駒の有無・種類をラベリングしました。

ネットワーク

入力は単一のマスの画像で、画素数は 64 * 64 です。出力はマスの内容で、空 or 駒の種類 (先手後手それぞれ 15 種類) を判別する 31 のラベルです。ネットワークは畳み込み & プーリング層が 3 層と全結合層 3 層の構成です。

model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3), input_shape=input_shape))
model.add(BatchNormalization())
model.add(PReLU())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, kernel_size=(3, 3), input_shape=input_shape))
model.add(BatchNormalization())
model.add(PReLU())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(128, kernel_size=(3, 3), input_shape=input_shape))
model.add(BatchNormalization())
model.add(PReLU())
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(1024))
model.add(BatchNormalization())
model.add(PReLU())
model.add(Dropout(0.25))
model.add(Dense(1024))
model.add(BatchNormalization())
model.add(PReLU())
model.add(Dropout(0.5))
model.add(Dense(NUM_CLASSES, activation='softmax'))

ネットワーク構成は こちら の漢字認識率 98.5 % のものを参考にさせて頂きました。

手書き漢字データによるファインチューニング

用意した学習データは駒の書体の種類が少ない (30種類程度) ため、局所特徴量の抽出を担う畳み込み層の学習に適しません。そこで、15 万文字の手書き漢字・ひらがなからなる 手書教育漢字データベース ETL8 を利用して、畳み込み層のパラメータを事前に学習しました。

学習・推定に使う将棋駒データはノイズが多い一方で、ETL8 は非常にノイズが少ないクリアな画像データです。そのため、そのまま学習してしまうと畳み込み層がノイズに対応できず将棋駒をうまく識別できませんでした。そこで、ETL8 に以下のノイズを乗せて学習を行いました。

  • ガウスノイズ (強)
  • ガウスノイズ (弱)
  • 白黒反転
  • 回転
  • 拡大縮小

その後畳み込み層を固定し、将棋駒データを使って全結合層のみを学習させます。これにより、高い精度の特徴抽出器 (畳み込み層) をそのまま将棋駒の分類に利用できます。

学習結果

Epoch 120/120
loss: 0.1981 - acc: 0.9379 - val_loss: 0.3054 - val_acc: 0.9245

92 % 程度の精度で駒の分類ができました。

今後の課題

いくつか大きな課題点を挙げます。一つは盤面検出の焼きなまし法に 10 秒程度の時間がかかることです。マスク画像を用いた目的関数は微分可能でないため確率的勾配降下法が利用できません。うまく微分を計算できる目的関数を設計すれば処理時間は大きく改善されるはずです。他にも、罫線の周期性を利用してスマートに盤面検出できないかなど考えましたが、具体的な手法は思いつきませんでした。

もう一つは、成銀・成桂・成香の分類問題です。将棋の駒には様々な書体がありますが、ものによっては人間でもそれらの識別が困難な場合があります。

f:id:na_o_s:20171220142832p:plain

特長が大きかったり一度学習した書体であれば間違えませんが、未知の書体について充分な精度を保証するのはまだ難しいようです。

最後に最も大きな課題は、持ち駒の検出ができないことです。画像によって持ち駒の位置や角度が異なるため、盤上の駒の推定とは違った要素技術が必要になります。

まとめ

将棋盤の画像から、盤面の状態を 92 % 程度の精度で得ることができました。盤面の正規化には輪郭検出・線分検出・焼きなまし法を使い、駒の推定にはディープラーニングを利用しました。普段業務で画像処理やディープラーニングを扱っていないため、今回の手法には冗長な手順や簡単な見落としもあるかと思います。お気づきの点やアイデアがあれば是非コメント頂ければ幸いです。

今回のプログラムは GitHub で参照頂けます。

github.com

おまけ

Mac で動く Electron 製の棋譜検討アプリ を絶賛開発中です。2018 年初頭には公開したいなあ。

Amazon Redshiftのデータ量監視とエンコードタイプ

データエンジニアリングチームのよしたけです。 弊社各サービスのデータ分析基盤であるLivesense Analyticsの開発、運用を行っています。

Livesense Analyticsのアーキテクチャ

Livesense AnalyticsはAWS上でシステムが構築されています。S3上にあるデータやtd-agent、Kinesis Firehoseなどを経由して集めたデータをAmazon Redshiftに格納し、データウェアハウスとして運用しています。詳細は、弊社大政がデータ分析基盤Night #1発表した内容をご参照ください。

https://speakerd.s3.amazonaws.com/presentations/c51a36e2acdc442ba4cf28d9df76b45b/slide_5.jpg

当時とは一部変更になっている部分もありますが、大枠は上記の図の構成になっています。

ディスク使用量

このLivesense Analyticsには、マッハバイト転職会議をはじめ、リブセンスで運用している多くのメディアの各種ログやデータが集められています。そのため、Redshift上にあるデータは日々増加の一途を辿っており、空き容量の確保や、ヤンチャなクエリがテンポラリテーブルでディスクを食いつぶすのを未然に止める、などの運用に日々追われています。

普段はRedshiftのコンソール画面からディスク空き容量などの監視を行っていますが、具体的にどのテーブルのサイズが増加しているのか、1ヶ月前と比べてどれくらい増加しているのか、といった分析がコンソール画面からは出来ません。

そこで、システムテーブルのSVV_TABLE_INFOのスナップショットを毎日取得するようにしています。このテーブルにはどのスキーマのどのテーブルがどれくらいの容量を使用しているのか、という情報が含まれているため、このデータを1日1回スナップショット格納用テーブルにSELECT INSERTすることでデータを貯めていきます。SVV_TABLE_INFOの持っているカラムの他に、スナップショットを取得した日時カラムも併せて保持しています。

このスナップショットを集めたテーブルを使い、re:dashでグラフ化して毎日のデータ増加量やスキーマごとのデータ量の割合、増加量を監視しています。弊社では、メディア、サービスごとにスキーマを分ける運用をしているため、どのメディアのデータが増加しているのかが分かるようになっています。

f:id:livesense-analytics:20171211153656p:plain f:id:livesense-analytics:20171211153709p:plain f:id:livesense-analytics:20171211153722p:plain

encode指定

では実際に使用量を削減していくにはどうしたらいいか、という話をしたいと思います。

Redshiftでテーブルを作成する際に、ENCODE指定を各カラムに行います。

ENCODEで指定できるエンコードタイプには以下の種類があります

VARCHAR SMALLINT INTEGER BIGINT DECIMAL REAL DOUBLE PRECISION DATE TIMESTAMP BOOLEAN
RAW
BYTEDICT ×
DELTA × × × ×
DELTA32K × × × × ×
LZO × × ×
MOSTLY8 × × × × × ×
MOSTLY16 × × × × × × ×
MOSTLY32 × × × × × × × ×
RUNLENGTH
TEXT255 × × × × × × × × ×
TEXT32K × × × × × × × × ×
ZSTD

各エンコードタイプの特徴はAWSのページを参照していただくとして、これらのうち、どのカラムにどのエンコードタイプを選択すればいいのでしょうか?

検証方法として、実際にそれぞれのエンコードタイプのカラムにデータを入れてみて、どれくらいのサイズになるのか確認してみるのがよさそうです。

以下のクエリはINTEGER型の値について、各エンコードタイプ指定がしてあるテーブルを作成し、対象カラムのデータをINSERTしてみる例です。

CREATE TABLE encode_test (
  value_raw INTEGER ENCODE RAW,
  value_bytedict INTEGER ENCODE BYTEDICT,
  value_delta INTEGER ENCODE DELTA,
  value_delta32k INTEGER ENCODE DELTA32K,
  value_lzo INTEGER ENCODE LZO,
  value_mostly8 INTEGER ENCODE MOSTLY8,
  value_mostly16 INTEGER ENCODE MOSTLY16,
  value_runlength INTEGER ENCODE RUNLENGTH,
  value_zstd INTEGER ENCODE ZSTD
);
INSERT INTO encode_test (
  value_raw,
  value_bytedict,
  value_delta,
  value_delta32k,
  value_lzo,
  value_mostly8,
  value_mostly16,
  value_runlength,
  value_zstd
)
SELECT
  target_value,
  target_value,
  target_value,
  target_value,
  target_value,
  target_value,
  target_value,
  target_value,
  target_value
FROM
  src_table;

次にこのテーブルの各カラムがどれくらいディスクを使用しているかを確認します。STV_BLOCKLISTというシステムテーブルから、列ごとにどれくらいのブロックが使用されているかMB単位で取得することが出来ます。

SELECT
  col,  -- 0オリジンで、encode_testテーブルに指定したカラムに対応
  MAX(blocknum)
FROM
  stv_blocklist AS b,
  stv_tbl_perm AS p
WHERE
  b.tbl = p.id
  AND p.id = (
    SELECT
      table_id
    FROM
      svv_table_info
    WHERE
      "table" = 'encode_test'
  )
  AND col < 9  -- encode_testテーブルで作成したカラムの数を指定
GROUP BY
  name,
  col
ORDER BY
  col;

実際のデータを用いて得られた結果をお見せします。弊社のあるサービスのアクセスログが格納されているテーブルについて、最適なエンコードタイプがどれになるか見てみようと思います。

各カラムについてデータの特性がイメージしてもらえるよう、以下の値を併せて出しています。

  • 平均文字列長(varchar型のみ)
  • ユニークデータ数
  • 値ごとの平均レコード数
  • NULL値列
  • 値ごとのレコード数の標準偏差

また、各エンコードタイプの圧縮率をまとめています。圧縮率はエンコードタイプrawのディスクサイズとの割合から算出しています。

検証を行った環境は、ノードタイプdc2.large、ノード数18台のRedshiftクラスタを使用しています。対象となるアクセスログテーブルは約3億件のレコードが格納されています。

varchar型カラム

カラム 平均文字列長 ユニークデータ数 値ごとの平均レコード数 NULL値率 raw lzo zstd bytedict runlength text255 text32k
access_id 36.00 305260271 1.00 0.00% 100.00% 77.95% 45.02% 95.47% 100.30% 120.54% 179.76%
session_id 36.00 94677977 3.22 0.00% 100.00% 85.50% 48.94% 95.17% 100.00% 122.36% 183.99%
user_id 31.99 35762700 8.54 0.01% 100.00% 56.19% 32.44% 93.98% 100.00% 117.06% 172.91%
ip 13.99 16101001 18.96 0.00% 100.00% 51.35% 33.11% 124.32% 100.68% 89.86% 95.27%
url 87.34 51500368 5.93 0.00% 100.00% 33.64% 22.82% 4091.82% 100.26% N/A 122.30%
url_host 8.04 3 101,768,565.67 0.00% 100.00% 0.95% 0.95% 8.57% 0.95% 77.14% 139.05%
url_path 14.22 5292074 57.69 0.00% 100.00% 41.77% 29.75% 1786.71% 97.47% N/A 100.00%
user_agent 130.20 471837 647.06 0.00% 100.00% 14.91% 10.16% 270.08% 96.07% N/A 93.69%
device 8.41 7 43,615,099.57 0.00% 100.00% 8.33% 3.70% 8.33% 25.93% 15.74% 30.56%
os 6.98 43 7,100,132.49 0.00% 100.00% 17.71% 9.38% 9.38% 68.75% 20.83% 39.58%
browser 7.10 29 10,527,782.66 0.00% 100.00% 16.33% 8.16% 9.18% 62.24% 17.35% 34.69%
page_type 7.57 50 6,106,113.94 0.00% 100.00% 25.74% 13.86% 8.91% 88.12% 28.71% 54.46%

上記の結果から、圧倒的にZstandardの圧縮率が高いことがわかります。今年の1月に新しいエンコードタイプとして追加され、それ以前は迷ったときのlzo頼みだったのですが、今やそのポジションは同じような特性を持ちかつ圧縮率のよいZstandardに置き換わったといえます。

一方、osやpage_type(URLによって付けられるページ種類のタグのようなもの)カラムのように、値の種類が限定されるカラムについてはbytedictが有効です。公式ページによると

このエンコードは、列に含まれる一意の値の数が制限されている場合に非常に効果的です。このエンコードは、列のデータドメインが一意の値 256 個未満である場合に最適です。

とのことですが、種類が少ないとあまり効果は出ないようです。50個程度であれば効果が期待できます。また、長い文字列など値のサイズが大きい場合、bytedictは極端に性能が悪化するので注意が必要です。

また特定の値(NULLとか)が殆どの場合は、runlengthが効果を発揮します。公式ページによると

たとえば、大きなディメンションテーブルの列に、予測どおりに小さなドメイン (10 個未満の可能値を持つ COLOR 列など) がある場合、データがソートされなくても、これらの値はテーブル全体にわたって長いシーケンスになる可能性が高くなります。

とありますが、こちらの傾向としては特定の値が99%以上を占めるようなデータの場合に効果が出るようで、deviceのように値の種類が10個未満でも効果が出ない場合があったり、逆に種類が多くても特定の値が99%を占めるようなデータであればrunlengthが効果的な場合もあります。

integer型カラム

カラム ユニークデータ数 値ごとの平均レコード数 NULL値率 raw lzo zstd bytedict delta delta32k mostly8 mostly16 runlength
access_count 24422 12,501.26 0.00% 100.00% 45.45% 24.24% 27.27% 27.27% 51.52% 30.30% 54.55% 109.09%
session_count 6088 50,148.77 0.00% 100.00% 45.45% 24.24% 27.27% 27.27% 51.52% 30.30% 54.55% 106.06%
elapsed_sec 42663 7,156.22 0.00% 100.00% 57.58% 39.39% 66.67% 81.82% 51.52% 69.70% 54.55% 112.12%
entry_id 1198370 254.77 99.33% 100.00% 100.00% 100.00% 100.00% 100.00% 100.00% 200.00% 200.00% 100.00%
item_id 1874261 162.89 62.79% 100.00% 100.00% 69.23% 115.38% 123.08% 123.08% 115.38% 115.38% 123.08%

integerについても圧倒的にZstandardの圧縮率が高いことがわかります。

varcharではNULLデータが多いカラムだとrunlengthが効果的でしたが、integerについては1データのサイズがvarchatと比べて小さいためか、entry_idのようにあまり差が出ませんでした。

date型カラム

カラム ユニークデータ数 値ごとの平均レコード数 NULL値率 raw lzo zstd bytedict delta delta32k runlength
access_day 1165 262,064.98 0.00% 100.00% 3.03% 3.03% 27.27% 27.27% 51.52% 3.03%
session_start_day 1165 262,064.98 0.00% 100.00% 3.03% 3.03% 27.27% 27.27% 51.52% 3.03%

dateカラムについてはZstandardだけでなく、LZO、runlengthも効果的なことがわかります。値によってデータ数に差があまりなく、同じ値が連続しているため、runlengthでも効果が大きかったのではないかと推測します。

timestamp型カラム

カラム ユニークデータ数 値ごとの平均レコード数 NULL値率 raw lzo zstd bytedict delta delta32k runlength
hit_time 85443858 3.57 0.00% 100.00% 81.54% 52.31% 112.31% 112.31% 124.62% 112.31%
visit_time 55451781 5.51 0.00% 100.00% 80.00% 53.85% 112.31% 112.31% 124.62% 112.31%

一方timestampカラムについてはZstandard一択と言えそうです。dateカラムと違い、値がミリ秒単位であるため、runlengthやdeltaの効果が出にくいのかもしれません。

結果

今回の結果を受け、このアクセスログテーブルのエンコードタイプ指定を見直してみました。RedshiftにZstandardが対応される前に作られたテーブルだったため、主にlzoを使っており、まだZstandard導入をしていませんでした。

エンコード指定を見直した結果、テーブルのディスク使用量が76,883MBから50,849MBと約2/3に削減されました。同じ手順で他のテーブルにもエンコードタイプ見直しを進めていくことで、効果的なサイズ削減が実現できそうです。

最後に

Amazon Redshiftの増加していくデータに対してどう向き合っていくかという話をさせていただきました。re:dashを使ってデータ増加を監視しつつ、エンコードタイプを見直してデータ量削減を効果的に行うことが出来ました。特にZstandardの導入が効果的であることが分かりました。

なお今回はデータ量についてフォーカスさせていただきましたので、エンコードタイプによる実行速度の比較は行いませんでした。こちらは別の機会にまとめてみたいと思います。

エンコードタイプ見直しの他にもDISTKEYの見直しやRedshift Spectrumの導入、データ移行など、色々な施策に取り組んでおります。こちらもまた別の機会にご紹介したいと思います。

マルチコンテナ構成による機械学習アルゴリズムとアプリケーションの疎結合化

こんにちは、Analyticsグループの田中です。 以前は主にデータ分析基盤を開発・運用していましたが、現在は機械学習基盤の開発に携わっています。

今回はレコメンドエンジンを題材に、コンテナ技術を活用した機械学習システムのアーキテクチャをご紹介します。

リブセンスでのレコメンドエンジン開発

リブセンスでは以前から機械学習を利用したレコメンドエンジン開発に力を入れています。 このブログの初回の記事でも社内で利用しているBPMFアルゴリズムについて解説しています。

この記事にもあるのですが、社内のレコメンドをとりまく状況は次のような点が特徴です。

  • 複数のサービスでそれぞれのニーズに合わせたレコメンドエンジンを開発・運用している
    • マッハバイト・転職ナビ・転職会議などの各メディアでそれぞれ独自のレコメンドエンジンが稼働している
  • 主な事業領域である求人・不動産サービスでは、アイテムの件数は多くないがアイテムあたりのCV単価が高い傾向がある
    • アイテムが求人や物件であるため、(Web広告等に比べれば)比較的扱いやすい件数である

データの性質からアルゴリズムにはスケーラビリティよりも精度が求められることが多く、最近では論文などからフルスクラッチでアルゴリズムを実装することも増えています。

アルゴリズムの実装は機械学習エンジニアが担当しますが、それを各サービスのアプリケーションで利用する際にはアプリケーションエンジニアの手が必要となるため、開発体制は概ね次のようになります。

  • Analyticsグループの機械学習エンジニア
    • データ分析とそれに基づくアルゴリズムの選定
    • アルゴリズムの実装や実環境でのテスト結果に基づくチューニング
  • 各サービスのアプリケーションエンジニア
    • 機械学習ライブラリなどを含む実行環境の整備
    • アルゴリズム実装のアプリケーションへの組み込み

以前のシステム構成の課題

このような体制のもとで、以前は各サービスのインフラ環境やアプリケーション設計に合わせてレコメンドエンジンを開発してきました。 この結果、アプリケーションのコードベースがアルゴリズムを内包するようなシステム構成となっていました。

f:id:yubessy:20171206171226p:plain

この構成にはサービスのニーズに合わせて設計やアルゴリズムのチューニングを柔軟に変えられるメリットがあります。 とはいえ密結合した構成の常として、システムに手を加えれば加えるほど複雑化によりメンテナビリティは下がってしまいます。 実際に社内でも次のような課題が顕在化していました。

  • アルゴリズム実装の再利用性・可搬性が低い
    • あるサービスで利用しているアルゴリズムをそのまま別のサービスに移植しにくい
    • 結果的に似たようなアルゴリズムの実装が複数のアプリケーションのコードベースに散在する
  • アルゴリズム実装に利用できる技術がアプリケーションの実行環境に制約される
    • 新しい技術を導入する際はアプリケーションの動作に影響がないかを慎重に検証する必要がある
    • 運用負担を考えると、例えば実験的にJuliaを使いたいといった要望に簡単には応えにくい

アルゴリズムとアプリケーションの疎結合化

今回のテーマは、これらの課題を解決するために密結合しているアプリケーションとアルゴリズムを分離し、それぞれを独立して開発できるようにすることです。

f:id:yubessy:20171206171246p:plain

実現したいのは次のようなことです。

  • ひとつのアルゴリズムを実装すれば複数のサービスで利用できる
  • アルゴリズムとアプリケーションの実行環境を独立化し、それぞれで利用する技術を自由に選択できる
  • 機械学習エンジニアとアプリケーションエンジニアが担う運用上の役割を分担し、それぞれの得意な領域に集中できる

マルチコンテナ化による実行環境の分離

単純にアルゴリズムを共通化したければライブラリ化すればよいのですが、それだけでは実行環境の独立化までは達成できません。 例えばアプリケーションをRubyやPHP・アルゴリズムをPythonやJuliaで実装したいときに、それらがひとつの環境に同居すると依存関係などの問題が発生しやすくなります。

また機械学習ではしばしば複雑なビルド工程を要する外部ライブラリを利用しますが、これによりプロビジョニングステップが肥大化し運用負担が増大することも懸念されます。

こういった問題を回避すべく、中核となるアルゴリズムと、アルゴリズム・アプリケーション間の仲立ちをする部分とを実行環境ごと独立させたいと考えました。 具体的には、新しい構成はアルゴリズムコンテナとオーガナイザコンテナと呼ぶ複数のコンテナからなるシステムとして開発しています。

f:id:yubessy:20171207180324j:plain

各コンテナでは次のような処理を行います。

  • オーガナイザコンテナ
    • アプリケーションのDBや分析基盤からデータを取得する
    • 取得したデータをアルゴリズムコンテナで処理するフォーマットに合わせて整形する
    • 必要に応じて生成されたレコメンド結果をフィルタリングする
    • アプリケーションで利用可能な形式でデータをエクスポートする
  • アルゴリズムコンテナ
    • オーガナイザコンテナから渡されたデータと設定をもとにレコメンド結果を生成する
    • 必要に応じてオフライン評価による精度検証なども同時に行う

コンテナ間でのデータの受け渡しはマウントされた共有ボリュームを介して行います。 データのフォーマットを決めておくことで、アルゴリズムコンテナのイメージの可搬性・再利用性を高めることができます。

オーガナイザコンテナはアプリケーション毎に実装する必要がありますが、アルゴリズムコンテナは複数のアプリケーション間で共通化できます。 このような構成にすれば、次のように目標としていた要件は実現できそうです。

  • アルゴリズムの実行環境がポータブルになり、複数のサービスでの利用が簡単になる
  • アルゴリズム実装のための技術を機械学習エンジニアが自由に選択できる
  • 機械学習エンジニアとアプリケーションエンジニアがそれぞれの役割に集中できる

加えてコンテナ技術を採用することで次のようなメリットも生まれます。

  • 機械学習系のライブラリのビルドを含むプロビジョニング・デプロイフローが単純化できる
  • コンテナイメージがバージョン管理でき、サービスごとに別のバージョンのアルゴリズムを利用できる

実現可能性と今後の展望

マルチコンテナ構成のシステムを実行するには、Docker ComposeやKubernetesのようなコンテナ管理・オーケストレーションツールが必要です。 これに関して社内の新しい機械学習基盤はGCP上で開発・運用しており、バッチの実行基盤にはKubernetes/GKEを利用しているため、こういった構成を実現する上でのハードルは低くなっています。

実は今回紹介した取り組みは、この新しい機械学習基盤の開発プロジェクトの一環として行っているものです。 この中ではレコメンドエンジンの他に、Webテスト・最適化システムなどの開発も行っており、一部ですでに実証実験を始めています。 またこの記事の内容は主にバッチジョブを対象としたものでしたが、チーム内ではオンラインでの学習・予測を想定したシステム構成についての調査・検証も進めています。

プロジェクト自体はまだまだ始まったばかりですが、機械学習エンジニアにとっての自由度向上のメリットは徐々に現れてきています。 一例として、従来はアルゴリズムの実装言語をPythonに限定していましたが、最近では要件に合わせてJuliaを試験的に導入するといった試みも行っています。 そういった話も今後このブログでご紹介できればと思います。

BPMF(Bayesian Probabilistic Matrix Factorization)によるレコメンド

こんにちは、リブセンスで機械学習関係の仕事をしている北原です。
弊社の転職ナビアプリには求人をレコメンドする機能が実装されていて、求人の好みを回答すると各ユーザーに合った求人がレコメンドされるようになっています。このサービスではいくつかのレコメンドアルゴリズムが使われているのですが、その中にBPMF(Bayesian Probabilistic Matrix Factorization)というアルゴリズムがあります。基本的な問題をフルベイズで扱っている典型的なベイズ手法なのですが、使いどころが難しいのか、使われているのをあまり見たことがありません。そこで、今回はこのBPMFを紹介しようと思います。


アプリの求人レコメンド

レコメンドに限らず機械学習では、やりたいことや使えるデータの種類、特徴に応じて適切なアルゴリズムを使うことが大事です。BPMFを使った背景として、まず簡単に求人レコメンドの目的とアプリの特徴について説明します。

転職ナビは成功報酬型の転職サイトであるため、掲載求人数が多いという特徴があります。働きたい仕事や会社の雰囲気などは人によってさまざまなので、多くの求人があるほうが自分に合った仕事探しがしやすいです。一方で、スマホでは画面が小さいため検索で多くの求人をチェックするのは大変です。そこで、求人の好みを回答するだけで自分に合った求人がレコメンドされると便利です。

レコメンドの精度はユーザーの回答が多いほど向上するので、使い続けるほど適切な求人がレコメンドされやすくなります。そのため、長く使い続けてもらいたいのですが、一般にアプリユーザーは利用回数が少ない段階で離脱することも多く、弊社アプリも例外ではありません。使い始めやたまにしか使わないユーザーにも高精度なレコメンドを行えるようにする必要があります。

データの特徴として、アクティブユーザー数は比較的少なく、求人数もECサイトの商品数などと比べれば少ないと言えるでしょう。そうなると、使うレコメンドアルゴリズムも大規模データを扱う場合とは異なるものが適している場合があります。今回は計算量が多少多くても構わないので、データが少なくても精度が高くなるものが必要です。それに、できれば調整パラメータが少なくシステムへの導入がしやすいと楽です。

BPMFは今回の目的に合っていて、計算量は多いものの評価数が少ないユーザーの精度が高いという特徴があります。正則化項の係数を調整する必要もありませんし、ユーザーの求人評価データだけを使うので既存のレコメンドアルゴリズムをそのまま置き換えやすく、システムへの導入もしやすいという事情がありました。また、階層ベイズが実システムでどれくらい有効なのかについての技術的興味もありました。そんなわけで、BPMFを使ってみることにしました。

PMF

BPMFはベースとなったPMF(Probabilistic Matrix Factorization)というモデルと比較するとわかりやすいので、まずPMFについて説明しようと思います。PMFはMF(Matrix Factorization)を確率モデルで表現したもので、MFと同じく協調フィルタリングアルゴリズムの一つです。協調フィルタリングやMFについては、検索すると多くの記事が見つかるのでここでは説明を省きます。

PMFでは、ユーザー数を\(N\)、アイテム数を\(M\)、因子数を\(D\)とし、評価行列を\(R \in \mathbb{R}^{N \times M}\)、分解後のユーザー因子行列を\(U \in \mathbb{R}^{D \times N}\)、アイテム因子行列を\(V \in \mathbb{R}^{D \times M}\)としたとき
\begin{eqnarray}
p(R|U, V, \alpha) &=&
\prod_{i=1}^{N} \prod_{j=1}^{M} \bigg[\mathcal{N}(R_{ij} | U_i^T V_j, \alpha^{-1}) \bigg]^{I_{ij}}  
\label{eq:p_R} \\
p(U|\alpha_U) &=& \prod_{i=1}^{N} \mathcal{N}(U_i | 0, \alpha_U^{-1} I) \\
p(V|\alpha_V) &=& \prod_{j=M}^{N} \mathcal{N}(V_j | 0, \alpha_V^{-1} I)
\end{eqnarray}
というモデルを考えます。\(I_{ij}\)は\(i\)番目のユーザーが\(j\)番目のアイテムを評価している時のみ1、それ以外は0となる変数で、評価済みのデータのみを扱うようにするために導入されています。\(\mathcal{N}(x|\mu, \alpha^{-1})\)は、平均を\(\mu\)、分散を\(\alpha^{-1}\)とする正規分布です。\(R\)の各成分は\(U\)と\(V\)の積を平均とした正規分布で表した素直なモデルになっています。

これをMAP推定すると、正則化項のついた通常のMFと同じ更新式が得られます。MFの確率モデル解釈が得られるので、MFの正則化項の係数がPMFの分散に対応することなどがわかって理解は進みますが、基本的にはMFと同じです。

MFを使うときには因子数や正則化係数を適切な値にしないと精度が上がりません。パラメータ調整は面倒なので、これらのパラメータも推定できるとうれしいです。

また、\(U\)や\(V\)の分散に注目すると、PMFでは各因子の分散は全て同一で因子間は無相関であることを仮定していることがわかります。因子の分散が全て同じというのは仮定として強すぎる気がします。例えば、因子が年収と福利厚生に関するものだった場合、年収を気にする人のばらつきと福利厚生を気にする人のばらつきは違いそうです。因子同士の関係も、理想的には無相関であってほしいところですが、因子数の選び方によっては相関があってもおかしくありません。

BPMFのモデル

BPMFは、PMFにおける\(U\)と\(V\)のパラメータにも事前分布を設定したモデルです。分布の分散を一般的な分散共分散行列\(\Lambda\)とし、事前分布に共役事前分布を使うことでギブスサンプリングを使えるようにしたところが特徴です。

\(p(R|U, V, \alpha)\)はPMFと同じで、\(U\)、\(V\)についてはより一般的な表現
\begin{eqnarray}
p(U| \mu_U, \Lambda_U) &=& \prod_{i=1}^{N} \mathcal{N}(U_i | \mu_U, \Lambda_U^{-1}) \label{eq:bpmf_p_U} \\
p(V| \mu_V, \Lambda_V) &=& \prod_{j=M}^{N} \mathcal{N}(V_j | \mu_V, \Lambda_V^{-1}) \label{eq:bpmf_p_V}
\end{eqnarray}
に拡張されています。\(\Lambda\)がポイントです。これをみるとBPMFはPMFの自然な一般化であることがわかると思います。

\(\mu_U\), \(\Lambda_U\), \(\mu_V\), \(\Lambda_V\)の事前分布は、自由度\(\nu_0\)、\(D \times D\)のスケール行列\(W_0\)のWishart分布\(\mathcal{W}(\Lambda | W_0, \nu_0)\)を使って
\begin{eqnarray}
p(\mu_U, \Lambda_U| \mu_0, \nu_0, W_0) &=&
\mathcal{N}(\mu_U | \mu_0, (\beta_0 \Lambda_U)^{-1}) \mathcal{W}(\Lambda_U | W_0, \nu_0)  \label{eq:bpmf_p_muU_lamU} \\
p(\mu_V, \Lambda_V| \mu_0, \nu_0, W_0) &=&
\mathcal{N}(\mu_V | \mu_0, (\beta_0 \Lambda_V)^{-1}) \mathcal{W}(\Lambda_V | W_0, \nu_0) \label{eq:bpmf_p_muV_lamV}
\end{eqnarray}
と設定されます。これはNormal-Wishart分布で、多次元正規分布の共役事前分布であることが知られています。このように設定することで、事後分布\(p(\mu_U, \Lambda_U | U)\)、\(p(\mu_V, \Lambda_V | V)\)が容易に計算できるようになっています。ベイズに馴染みのない方には複雑に見えるかもしれませんが、事前分布に共役事前分布を使って計算を容易にさせるのはよく用いられるテクニックです。

BPMFの利点としては

  • 分散\(\Lambda\)も推定されるので正則化の調整が不要であること
  • 評価数の少ないユーザーの推定精度が高いこと

が挙げられます。一方で、欠点としては、計算時間が比較的長いことが挙げられます。これらの特徴は階層ベイズを使った機械学習でよく見られるものと同じです。

MFを一般化したFM(Factorization Machine)でもMCMCを使う方法がありますが、こちらでは分散行列を対角化し共役事前分布にNormal-Gamma分布を使っています。分散行列の逆行列演算はBPMFの計算量が多い一因となっているので、因子間の相関が小さい場合はFMと同じやり方でもよいかもしれません。

BPMFの実装

BPMFはギブスサンプリングを使って実装できます。条件付き分布\(p(U | R, V, \mu_U, \Lambda_U)\)、\(p(V | R, U, \mu_V, \Lambda_V)\)、\(p(\mu_U, \Lambda_U | U)\)、\(p(\mu_V, \Lambda_V | V)\)が計算できればギブスサンプリングを使うことができます。ここで共役事前分布がフル活用されます。

それでは条件付き分布を導出してみましょう。ユーザー同士、アイテム同士は条件付き独立と考えると
\begin{eqnarray}
p(U| R, V, \mu_U, \Lambda_U) &=& \prod_{i=1}^N p(U_i | R, V, \mu_U, \Lambda_U) \\
p(V| R, U, \mu_V, \Lambda_V) &=& \prod_{j=1}^M p(V_j | R, U, \mu_V, \Lambda_V)
\end{eqnarray}
と分解できるので、\(U_i\)について考えてみます。ベイズの定理と式\ref{eq:p_R}、\ref{eq:bpmf_p_U}より
\begin{eqnarray}
p(U_i| R, V, \mu_U, \Lambda_U, \alpha)
&\sim& p(R | U_i, V, \alpha) p(U_i | \mu_U, \Lambda_U) \\
&=& \prod_{j=1}^M \bigg[\mathcal{N}(R_{ij} | U_i^T V_j, \alpha^{-1} )\bigg]^{I_{ij}} \mathcal{N}(U_i | \mu_U, \Lambda_U^{-1})
\end{eqnarray}
となります。平均が未知、分散が既知で共役事前分布に正規分布を使っているので、事後分布も正規分布になります。指数部分に着目して整理すると事後分布は
\begin{eqnarray}
p(U_i| R, V, \mu_U, \Lambda_U, \alpha) &=& \mathcal{N}(U_i | \mu_i^{\ast}, {\Lambda_i^{\ast}}^{-1}) \\
\Lambda_i^{\ast} &=& \Lambda_U + \alpha \sum_{j=1}^M [V_j V_j^T]^{I_{ij}} \\
\mu_i^{\ast} &=& {\Lambda_i^{\ast}}^{-1} \bigg( \alpha \sum_{j=1}^M [V_j R_{ij}]^{I_{ij}} + \Lambda_U \mu_U \bigg)
\end{eqnarray}
となることがわかります。\(V_j\)についても同様なので、他のパラメータを固定すれば、\(U\)、\(V\)は正規分布を使ってサンプリングできます。

ハイパーパラメータについては共役事前分布が設定されているので、Normal-Wishart分布を共役事前分布としたときの事後分布の公式が使えて
\begin{eqnarray}
p(\mu_U, \Lambda_U | U, \mu_0, \nu_0, W_0) &=&
\mathcal{N}(\mu_U | \mu_0^{\ast}, {\beta_0^{\ast} \Lambda_U}^{-1})
\mathcal{W}(\Lambda_U | W_0^{\ast}, \nu_0^{\ast}) \\
\mu_0^{\ast} &=& \frac{\beta_0 \mu_0 + N \overline{U}}{\beta_0 + N} \\
\beta_0^{\ast} &=& \beta_0 + N \\
{[W_0^{\ast}]}^{-1} &=& W_0^{-1} + N \overline{S} + \frac{\beta_0 N}{\beta_0 + N} (\mu_0 - \overline{U})(\mu_0 - \overline{U})^T\\
\overline{U} &=& \frac{1}{N} \sum_{i=1}{N} U_i\\
\overline{S} &=& \frac{1}{N} \sum_{i=1}^N U_I U_i^T
\end{eqnarray}
となります。\(\mu_V\)、\(\Lambda_V\)も同様です。これで\(\mu_U\)、\(\Lambda_U\)、\(\mu_V\)、\(\Lambda_V\)も他のパラメータを固定すればサンプリングできることがわかりました。

必要な条件付き分布がわかったので、これらを使ってギブスサンプリングを実装します。実装は以下のようになります。

def bpmf_gibbs_sampling(R, U, V, N, M, D, n_sample):
    # 初期値(BPMFの論文と同じ)
    beta0 = 2
    mu0 = 0
    nu0 = D
    W0 = np.identity(D)
    alpha = 2

    for t_ in range(n_sample - 1):
        # sample lam_u
        S_bar = np.sum([np.outer(U[t_, i, :], U[t_, i, :].T) for i in range(N)], axis=0) / N
        U_bar = np.sum(U[t_], axis=0) / N
        W0_ast = inv(inv(W0) + N * S_bar +
                     (beta0 * N / (beta0 + N)) * np.outer(mu0 - U_bar, (mu0 - U_bar).T))
        lam_u = wishart.rvs(df=nu0 + N, scale=W0_ast)

        # sample mu_u
        mu0_ast =(beta0 * mu0 + N * U_bar) / (beta0 + N)
        mu_u = multivariate_normal(mu0_ast, inv((beta0 + N) * lam_u))

        # sample lam_v
        S_bar = np.sum([np.outer(V[t_, :, j], V[t_, :, j].T) for j in range(M)], axis=0) / M
        V_bar = np.sum(V[t_], axis=1) / M
        W0_ast = inv(inv(W0) + M * S_bar +
                     (beta0 * M / (beta0 + M)) * np.outer(mu0 - V_bar, (mu0 - V_bar).T))
        lam_v = wishart.rvs(df=nu0 + M, scale=W0_ast)

        # sample mu_v
        mu0_ast = (beta0 * mu0 + M * V_bar) / (beta0 + M)
        mu_v = multivariate_normal(mu0_ast, inv((beta0 + M) * lam_v))

        # sample U
        for i in range(N):
            V_VT_I = np.sum([np.outer(V[t_, :, j], V[t_, :, j].T)
                             for j in R.getrow(i).nonzero()[1]], axis=0)
            lam_ast_inv = inv(lam_u + alpha * V_VT_I)

            V_R_I = np.sum([V[t_, :, j] * r for j, r in zip(R.getrow(i).nonzero()[1],
                                                            R.getrow(i).data[0])], axis=0)
            mu_ast = lam_ast_inv.dot((alpha * V_R_I + lam_u.dot(mu_u.T)).T)

            U[t_ + 1, i, :] = multivariate_normal(mu_ast, lam_ast_inv)

        # sample V
        for j in range(M):
            U_UT_I = np.sum([np.outer(U[t_ + 1, i, :], U[t_ + 1, i, :].T)
                             for i in R.getcol(j).nonzero()[0]], axis=0)
            lam_ast_inv = inv(lam_v + alpha * U_UT_I)

            U_R_I = np.sum([U[t_ + 1, i, :] * r for i, r in zip(R.getcol(j).nonzero()[0],
                                                                R.getcol(j).data)], axis=0)
            mu_ast = lam_ast_inv.dot((alpha * U_R_I + lam_v.dot(mu_v.T)).T)

            V[t_ + 1, :, j] = multivariate_normal(mu_ast, lam_ast_inv)

    return U, V

同じようなコードをUとVについて書いているので長く感じるかもしれませんが、条件付き分布をそのままコードに落としていることがわかると思います。実際に運用されているものは高速化や安定化のため、もう少し複雑になっていますが基本的には同じです。実際に動作させる関数呼び出しは以下。

A/Bテストのようなしっかりした比較は行えていないものの、BPMFの前に使っていたMFと比較すると、ポジティブ評価の割合は4割程度向上していました。思ったより効果があったようです。

最後に

今回はBPMFというレコメンドアルゴリズムの紹介をしました。最近ではStanやEdwardなどベイズ手法を簡単に使える環境が整ってきたので、実システムでベイズを使う場面も増えるのではないでしょうか。今後はStanなどを使った事例や他のアルゴリズムの紹介もしていこうと思います。

今回はアルゴリズムの紹介をしましたが、実際の業務ではアルゴリズム以外の部分も開発する必要があるので、そちらの開発に工数を取られて、すぐに導入できる簡単なアルゴリズムしか使えないということも多いのではないでしょうか。弊社も似たような案件を個別に開発を行っていたので、ある事業でうまくいった方法を別事業で使おうとすると1から開発が必要でした。このような問題に対処すべく、機械学習を各事業で容易に導入できるようにする基盤を開発するプロジェクトを現在進めています。近いうちに紹介できると思うので乞うご期待。