LIVESENSE Data Analytics Blog

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

LivesenseDataAnalyticsBlog

MENU

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

こんにちは、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から開発が必要でした。このような問題に対処すべく、機械学習を各事業で容易に導入できるようにする基盤を開発するプロジェクトを現在進めています。近いうちに紹介できると思うので乞うご期待。

LivesenseのData Analyticsブログはじめます

はじめまして。リブセンスの谷村です。

リブセンスではデータ分析、UXリサーチ、機械学習、分析基盤の開発等をテクノロジカルマーケティング部という事業横断の組織で行っています。
このブログでは、その日々の活動の中で獲得した情報を厳選してお届けしていきたいと思います。

リブセンスには数年前、LDM(Livesense Digital Marketing)というブログがありました。
リブセンスでのデータ活用の取り組みはその当時から脈々と続いています。
このブログではデジタルマーケティングからデータにテーマを広げて投稿を行っていきますが、ブログ開設の想いはLDMと同じです。
拙い知識ではありますが、私たちの学びが少しでもみなさまのお力となれれば幸いです。
それでは、どうぞよろしくお願いします。