実況AI室 ベイズ推論を用いたリスク因子分析

実況AI室 ベイズ推論を用いたリスク因子分析

こんにちは!
ビズリーチのAI室でデータサイエンティストとして働いている dimensional_homegoer(異次元の帰宅者)です。この二つ名は大学院時代に隣のイケメンが付けてくれました。

みなさんはAI室というとどんな業務を想定されますか?今はディープラーニング全盛の時代ですし、ビズリーチは大量の自然言語情報を持っていることもあるので、ディープラーニングで自然言語処理とか如何にもやってそう、といったイメージを持たれているかもしれません(僕も入社前はそうでした)。 実際、過去記事で少し紹介された職務内容からの給与推定のように、教師データが揃っている比較的綺麗な予測タスクの際には、そういった王道的なアプローチもとります。

しかし、ビズリーチは大きくなったとは言っても、まだまだ様々な新規事業が立ち上がり続けている会社でもあります。そういった領域では、まだ活用できるデータが揃っていないことや、そもそも何をどう予測すればいいのか分からないことも多々あります。そういう状況は難しいのですが、今手持ちのもので何ができるかを自分で考えていく、という面白さもあります。 今回は、弊社のサービス「ビズリーチ・キャンパス」のサービス改善というミッションで、まさにそういった事例に出くわしたので、その話をさせて頂ければ、と思います。

背景・経緯

先日、弊社の製品ではないものの、あるOB/OG訪問アプリが悪用され、学生会員の方がOB会員から性的な被害にあわれるという大変痛ましい事件が明るみに出ました。
競合アプリであるビズリーチ・キャンパスを運営する当社としましても、今まで以上に厳格に学生会員のみなさまの安全を担保しなければならない、ということで、AI室でもメッセージの自動監視などでリスクの低減に貢献できないか、となりました。

しかし、現状では、メッセージの内容を適切/不適切に分類するような自然言語処理的なアプローチでは、

  • 継続的に分析を行うには匿名化によるプライバシーの担保が必要だが、それ自体コストがかかる。

という大きな課題がありました。

メタデータからのリスク推定

上記の事情から、自然言語処理方面の分析は一旦データを貯めてから再度行うこととし、当面は属性データからのリスク因子分析に舵を切りました。

ビズリーチキャンパスにおける通常のOB/OG訪問フローは、まず学生ユーザーからOB/OGユーザーへ訪問依頼メッセージが送付され、OB/OGユーザーは訪問を許可するならば返信する、というものです。
もしもOB/OGユーザーが出会い目的で当アプリを使用しているのであれば、リクエスト元の学生ユーザーが男性の場合と女性の場合で、顕著に返信率が異なっているはず、と仮定します。

ただ、単純に指標として返信率比を用いると、

  • メッセージの件数が少ない、新しい会員に対して 0 / 0が現れてしまいがちで、どうすればいいのか分からない
  • たまたま「男であろうが女であろうが是非応援したくなる異性の後輩」からメッセージが来てOKしたが、同性の後輩からのメッセージは来なかった場合に、「高リスク会員」と判定されてしまう

という問題があります。

モデル

そこで、以下のような確率モデルを立てることとしました。

  1. 各学生ユーザー \(i\) には「訪問の受け入れられ易さ」パラメータ \(s_i \sim \mathcal{N} (0, \sigma_s)\)が存在するとする
  2. OB/OGユーザー \(j\) 毎に「訪問に対する寛容さ」パラメータ \(o_j \sim \mathcal{N}(0, \sigma_o) \)と、「異性優先係数」\(g_j \sim \mathcal{N}(0, \sigma_g) \) が存在するとする。
  3. 学生ユーザー \( i \) のOB/OG訪問リクエストがOB/OGユーザー \( j \)にOKされるか否か (\( r_{i,j} \) と書く) は
    $$ r_{i,j } \sim \mathrm{Bernoulli}(\sigma( s_i + o_j + I(i,j) g_j + b)) $$ で定まるとする。ここで、\( \sigma \) はシグモイド関数、\( b \sim \mathcal{N}(0, \text{Some large number})\)はバイアス項、 \(I(i,j) \) は \( i \)と \( j \) が異性同士の組み合わせであれば1, そうでなければ0という値をとるとする。

すなわち、\( g_j \) はOB/OG \(j\) が(その真意はさておき)どの程度異性にゲタを履かせるか、というパラメータとなります。 \(\mathcal{N}(\mu, \sigma) \) はもちろん平均 \(\mu\), 標準偏差 \( \sigma \) の正規分布です。
これは非常に単純なモデルなんですが、その分解釈は明快かと思います。

この方法ですと、上で述べたような「だれが見ても是非応援したくなるような異性の後輩」からの訪問をOKするケースと 「普通の異性の後輩」の訪問をOKするケースで\(g_{j}\)の増えかたは異なってきます。

また、行動履歴がないユーザーに対しては「とりあえず事前分布を(そう仮定したんだし)信じる」という明快な指針があります。

あとは実際に観測されたデータから、 \(s_{i}, o_{j}, g_{j} \) の事後分布を推定します。

PyMC3 + 自動微分変分推論(ADVI)による事後分布の導出

事後分布の推定には(筆者が一番使い慣れているので)PyMC3を用いることにしました。
Theanoの開発が中止されて久しいので、早く何かに乗り換えたいとは思うのですが、なかなか他のはしっくり来ないんですよね。。PyMC4に期待したいところですが、僕はPyTorch派なのでPyroの今後の発展を望むべきかなのかなぁと思ったり。

推論アルゴリズムとしましては、さすがに結構規模が大きいため、MCMCは諦めて自動微分変分推論(元論文PyMC3版開発者の吉岡さんによる解説参照)で近似的な事後分布の導出を行います。有り難いことに超お手軽です。

以下はコード例になります。
pandasのデータフレームdfの各行はstudent_id, obg_id, student_gender, obg_gender, reply_flagとなっておりまして、学生ユーザーのid, OB/OGユーザーのid, 学生の性別, OB/OGの性別, 返信フラグを表します。

with pymc3.Model() as model:
    sigma_s = pymc3.HalfCauchy("sigma_s", 5)
    sigma_o = pymc3.HalfCauchy("sigma_o", 5)
    sigma_g = pymc3.HalfCauchy("sigma_g", 5)

    s = pymc3.Normal("student", mu=0, sd=sigma_s, shape=N_students)
    o = pymc3.Normal("ob_og", mu=0, sd=sigma_o, shape=N_obgs)
    g = pymc3.Normal("gender", mu=0, sd=sigma_g, shape=N_obgs)
    b = pymc3.Normal("bias", mu=0, sd=100)
    r = s[df.student_id.values] + o[df.obg_id.values] + (
        (~(df.student_gender == df.obg_gender)).values
    ) * g[df.obg_id.values] + b
    target = pymc3.Bernoulli("target", logit_p=r, observed=df.reply_flag.values)

    inference = pymc3.ADVI()
    approx = pymc3.fit(
        method=inference, n=20000,
        obj_optimizer=pymc3.adam()
    )

ここでは、考えるのも面倒なので\(\sigma_s\) などのパラメータも分散大きめの片側コーシー分布に従う確率変数だとしました。 こういった単純なモデルだと(ミニバッチ化しなくても)一瞬で計算してくれます!最高や!開発者に感謝。NUTSだととても時間がかかるので、とても有り難いですね。。

あとは、

samples = approx.sample(300)

で事後分布からのサンプルを得(今回は正規分布で近似を行うので本当は不要なんですが)、

gender_priority_mean = samples.get_values('gender').mean(axis=0)
gender_priority_std = samples.get_values('gender').std(axis=0)

で異性優先係数の平均と標準偏差を得ます。

ベイズ推論の利点はよく「オーバーフィットしないこと」と言われます。ここでもgender_priority_meanに加えてgender_priority_stdが計算できるので、gender_priority_meanが大きくなくても、gender_priority_stdが大きい、すなわち(行動履歴が少なくて)あまり推論に自信がないところがはっきりと分かります。

こうして、異性優先係数に基づいてメッセージの監視をする際の優先度がつけられるわけですが、ここでは分散値も考慮し、

gender_priority_mean + a * gender_priority_std

をリスクの目安値とすることで、適切な対応を検討することができます。aはなるべく大きく取っておいた方が安全なんですが、大きくすると今度は上位に並ぶのが分散が大きいものだらけになってしまって、折角の推定が意味をなさなくなってしまいます。というわけで、当たり前ですが異性優先係数の分散は小さく抑えたいところです。

モデルの改良

上記の事情があるので、ここでは他のメタ情報を使って(特に行動履歴が少ないユーザーに対して)分散を低減することを考えたいと思います。
ここでは異性優先係数が $$ g^{(\text{all})} = g^{(\text{company})} _{k} + g_{j} $$ という形に分解されていると仮定します。\(k\) はOB/OGユーザー\(j\)の所属企業を表すid, \(g^{(\text{company})}_k \) は「企業毎の異性優先係数」という意味になります。簡潔に言うと、企業イメージをモデル化したものが \( g^{(\text{company} )}\) ということになります。 \( g_j\) は相変わらずOB/OGユーザー個人に対して割り振られており、ここでは会社名からの期待のズレ、といった位置付けとも思えます。

ついでに、会社毎に「多忙なのでOB/OG訪問はNGしがち」という場合もあると思うので、上での \(o_{j}\) も同様に分解します。 修正版のモデルと学習は、こんな感じになります。

with pymc3.Model() as model_with_company:
    sigma_s = pymc3.HalfCauchy("sigma_s", 5)
    sigma_o = pymc3.HalfCauchy("sigma_o", 5)
    sigma_g = pymc3.HalfCauchy("sigma_g", 5)
    sigma_o_company = pymc3.HalfCauchy("sigma_o_company", 5)
    sigma_g_company = pymc3.HalfCauchy("sigma_g_company", 5)

    s = pymc3.Normal("student", mu=0, sd=sigma_s, shape=N_students)
    o = pymc3.Normal("ob_og", mu=0, sd=sigma_o, shape=N_obgs)
    g = pymc3.Normal("gender", mu=0, sd=sigma_g, shape=N_obgs)
    o_compamy = pymc3.Normal("bias_company", mu=0, sd=sigma_o_company, shape=N_company)
    g_company = pymc3.Normal("gender_company", mu=0, sd=sigma_g_company, shape=N_company)

    ob_bias = o[df.obg_id.values] + o_compamy[df.company_id.values]
    g_all = pymc3.Deterministic(
        'gender_coeff_all',
        g[df.obg_id.values] + g_company[df.company_id.values]
    )

    b = pymc3.Normal("bias", mu=0, sd=100)
    r = s[df.student_id.values] + ob_bias + (
        (~(df.student_gender == df.obg_gender)).values
    ) * g_all + b
    target = pymc3.Bernoulli("target", logit_p=r, observed=df.reply_flag.values)

    inference = pymc3.ADVI()
    approx = pymc3.fit(
        method=inference, n=30000,
        obj_optimizer=pymc3.adam()
    )

これで実際に分散の値は小さくなりました!

尚、得られたg_companyと企業名を照合すると、それなりに企業イメージは正しいのかもしれないと感じました。

まとめ & 今後の展望

以上、AI室の取り組みのほんの一端について紹介しました。
最後の企業毎の異性優先係数の結果はなかなか唸らせるものだったのですが、極めて秘匿性が高い情報でもあるので公開できないことが残念です。

今回は非常に簡単なモデルを作るに留めたのですが、さらに精度をあげるには、行列分解のように個人同士の相性をベクトル因子の内積で表す、などが考えられます。そうすると、リスク因子の推論だけではなく、訪問を受け入れてくれやすいユーザーを学生ユーザーに推薦することもできるようになります(現状でもできるのですが、精度向上や個別化、という意味で)。

また、今回は自然言語処理からのアプローチが異常系の少なさ(というか皆無?)からできませんでしたが、 異性優先係数によるメッセージ監視の運用で効率的に問題のありそうなメッセージデータが貯まれば、 そちらと併せて使っていくことも考えて行きたいと思います。