この記事は、合併前の旧ブログに掲載していた記事(初出:2023年2月27日)を現在のブログへ移管したもので、2022年11月開催の「Tech-Verse 2022」で発表したセッションを要約した内容です。内容は初出時点のものです。
Yahoo! JAPANアプリのトップページの上部には、編集者によってピックアップされた「トピックス」と呼ばれるトップニュースが6本並んでいます。編集者が選定した質の高い記事を提供していますが、必ずしも各ユーザーの興味に適した記事が表示されているとは限りません。そのため、スクロールすると、記事推薦システムによって各ユーザーの好みを考慮した記事が自動で表示される仕組みになっています。
ニュース記事の推薦で特に重要なのは「即時性」です。ニュース記事では、情報が更新されると古い記事は役に立ちません。そのため、入稿された記事がいち早く推薦対象になることが重要になります。
たとえば、事前にユーザーごとの推薦記事一覧(レコメンドリスト)を作成しておくという方法は適していません。レコメンドリストを生成してからユーザーがサービスに訪れるまでの間に入稿されたニュース記事が、候補から抜け落ちてしまうためです。 また、協調フィルタリングのように、記事のクリック実績に依存してしまう手法も望ましくありません。こちらの理由は、記事が入稿された直後は実績がなく、すぐに推薦対象にならないからです。
そこで私たちは、ユーザーが訪れた瞬間に、最新の記事からレコメンドリストを作りつつ、計算量を減らしてレイテンシを抑える工夫を行っています。
下図が記事推薦システムの全体構成です。ユーザーが訪れる前に事前に計算しておく部分と、ユーザーが訪れた瞬間にその場で計算を行う部分の2つに大別されます。複雑な機械学習モデルが関係するのは事前に計算しておく部分で、1つの「PV Prediction Model」と2つの「Vectorize Model」の部分に配置されています。

事前に計算しておく部分
「UserLogs」から「User Vector KVS」の処理(図の左下)
各ユーザーの行動ログからユーザーの特徴量を抽出してベクトル化し、ユーザーIDをキーとしたKVSに格納します。この処理は、ユーザーが起こした行動が、ユーザーが次回サービスを利用するときまでに反映されていれば良いため、リアルタイムに処理する必要はなく、1日に数度のバッチ処理を分散環境で実施しています。
記事入稿から「Vector search Engine」の処理(図の左上)
入稿された記事を解析し、特徴量ベクトルに変換してベクトル検索エンジンにインデックスしています。記事入稿があるたびに実行されますが、ユーザーのアプリ利用とは独立して、事前に計算します。加えて、記事の入稿直後の閲覧数を推定し、特徴量の1つとして検索エンジンに入れておきます。
ユーザーが訪れた瞬間にその場で計算を行う部分
ユーザーがサービスに訪問してきたら、ユーザーIDをキーにして、KVSからユーザーの特徴量ベクトルを取得します。それをクエリとして、ベクトル検索エンジンにリクエストし、ユーザーベ クトルに近い記事リストを取得します。これに重複排除処理を加えてユーザーへのレコメンドリストが完成します。ユーザーからのリクエストを受けてから行う処理は、ほぼ類似ベクトルの検索処理だけのため、高速で結果を返せます。
ユーザーの行動は、ログとして後にフィードバックされ、次回ユーザーがサービスを利用するときまでにユーザーの履歴に反映されて、特徴量が更新されます。
入稿直後は推定だった記事の閲覧実績は、ユーザーからのログがたまれば、実績値に置き換えます。数日たったらその間にたまったデータを使い、各モデルの再学習を定期的に実施してモデルを差し替えます。

機械学習モデルの学習方法
ここでは、特徴量ベクトルを抽出する「記事をベクトル化するモデル」と「ユーザーをベクトル化するモデル」の2つに絞って説明します。これらは、最終的にユーザーベクトルをクエリとして記事ベクトルを検索できるようにするため、同時に学習しています。
モデルの学習は、記事データだけを用いた「事前学習」と、記事データとユーザーの行動履歴データを突き合わせて行う「本学習」の2段階のフェーズに分かれています。
事前学習フェーズ
大量の記事データを使用して自然文で書かれた記事を解釈するBERTモデルを学習します。
BERTモデルは、汎用的なコーパスで事前学習された日本語のものも配布されていますが、私たちのチーム では過去にヤフーに入稿されたニュース記事をたくさん持っているため、それを用いて学習したものを使っています。ニュースの見出しと本文を入力し、MLM(Masked Language Model)とNSP(Next Sentence Prediction)を行っています。
本学習フェーズ
事前学習モデルで得られた記事特徴量をユーザーの行動履歴に基づいて集約し、実際のクリック情報と突き合わせることで、記事ベクトルモデルのファインチューニングとユーザーベクトルモデルの学習を行います。
ここではユーザーの過去の閲覧記事の履歴を、BERTを用いてベクトル列に変換します。それを、リカレントニューラルネットワーク(RNN)を通して一つのベクトルに集約します。こうしてできたものをユーザーベクトルと呼んでいます。
このユーザーベクトルと、後にユーザーに推薦してクリックされた記事の記事ベクトル、推薦したがユーザーがクリックしなかった記事ベクトルの3つを突き合わせます。そして、クリックされた記事ベクトルにユーザーがより近くなり、クリックされなかった記事ベクトルから遠くなる「メトリックラーニング」と呼ばれる学習を行い、このRNNとBERTの一部をチューニングします。
ユーザーの履歴を集約する部分では、単なる履歴の記事ベクトルの平均ではなく、閲覧順を加味できるRNNを用いることで、クリック数を10%程度改善できました。
次にレコメンドシステムに関連した、最近注力している課題と対応策を3つ紹介します。
1. 推薦コンテンツの多様性
1つ目は、推薦コンテンツの多様性についての課題です。多様性を考慮せず に、推薦システムをそのまま適用してユーザーの関心度が高い順に推薦した場合、同じ内容の記事が並んだ推薦リストになることがあります。
その理由の1つは、ヤフーが複数の媒体から入稿された記事を扱っているためです。世間の関心が高い出来事が起こると、複数の媒体がその出来事を扱ったニュースをそれぞれ入稿してきます。そのため記事在庫には、媒体が違うだけのほぼ同じ内容の記事がたくさん入ってきます。
もう1つの理由は、類似ベクトル検索を用いたレコメンドを適用すると、その記事のスコアが、記事内容とユーザーの興味を反映したユーザーベクトルとの距離で決まるからです。内容が近い記事はベクトルも近くなり、必然的に近いスコアとなります。似たような記事がたくさん入稿されれば、近いスコアのそれらの記事が連続して並びます。そのため、類似ベクトル検索を行った後に、結果から重複を省く処理が必要です。
この問題について2つのアプローチを試しました。
1つ目は、記事ベクトル間の類似度を基に重複を判定する方法です。類似したコンテンツでは、内容を表す記事のベクトルは非常に近くなります。例えば、コサイン類似度を測って「0.98」という値が出たとしたら、それが一定のしきい値を上回っていた場合に、両方の記事を掲載するのではなく、片方をスキップして非表示にするというアプローチです。
2つ目は、クラスタリングを用いる方法です。事前に記事ベクトルをいくつかのクラスタに分割するモデルを学習しておき、その上で表示されたコンテンツが特定のクラスタの記事に偏った場合、それ以上は同一クラスタの記事が表示され ないようにスキップするアプローチです。この手法は、1つ目の方法と比較して、やや粗い粒度で同一のジャンルで出面が埋まることを防ぐ効果があります。
2つの方法を組み合わせて使うことで、重複した同一記事の掲載は抑制され、全体のクリック数も増加しました。 一方で、これらの手法では限界があることも感じています。
世間の関心度が高いニュースの場合、数百件単位で類似コンテンツが入稿される場合があります。すると、例えばしきい値を0.8に設定するなどしてスキップしようとしても、数百件のうち2~3件はしきい値をわずかに下回り、抜けてしまうことがあります。たとえ一定のスキップが機能していたとしても、結局似た記事が並んでいるように見えてしまいます。
対応策
そこで、次の打ち手としてディストリビューション・アウェアなリランキングを考えています。 これまでの方法では、例えば、記事x のスコアは、その記事よりも上部にどんな記事が掲載されているかは加味されず、その記事の内容とユーザーの興味の近さによってのみ決まっていました。
Score(x | selected) = Relevance(x)
これに、トピック分布によるペナルティを付け加えることを検討しています。ユーザーごとに理想と思われる話題の分布「p」をあらかじめ設定しておきます。第2項は、既にそのユーザーに掲載したコンテンツに新しく1件付け加えたときに、表示されているコンテンツの分布が理想の分布pからどの程度離れてしまうかが表現されており、それによってペナルティがかかるようになっています。
Score(x | selected) = Relevance(x) ‐ λ KL(p,selected + x)

既に上部にたくさん掲載されているトピックの記事の場合は、第2項のペナルティが大きくかかるため、相対的に順位が下げられます(下図内の赤い矢印)。一方で、第1項の興味度が中程度だったために、上部のコンテンツに押し出された別のトピックコンテンツは、ペナルティが相対的に小さくなり、上部に引き上げられてコンテンツの途中に差し込まれます(下図内の青い矢印)。こうして、スコアが同程度の重複コンテンツが大量に固まっている場合でも、下から上に差し込まれるコンテンツによって、多様性が確保できます。

しかし、この手法を実際に使う場合、計算量の障壁があります。スキップベースの手法の場合は、記事のスコアは一度だけ計算すれば、あとはスコアが高い方からスキップするか否かの判定だけで済みます。一方で、多様性を考慮したリランキングを用いようとすると、掲載記事が1つ決まるごとにペナルティの項が変化するため、スコアを再計算して並び替える処理が1本選ぶごとに必要になってしまいます。現在は、この計算量を加味して効果を見ながら、より良いバランスを探している状況です。
2. ユーザーからネガティブなフィードバック(dislikeシグナル)を受けった際の挙動
推薦された記事が自分の好みと合わなかった場合に、類似の記事を減らすフィードバックを記事推薦システムに送る機能があります。(この機能は、Yahoo! JAPANアプリではまだ未実装で、Yahoo!ニュースアプリのみに実装されています)
この機能を実装するにあたって、推薦モデルに要求されていることは大きく2つあります。1つ目は、ユーザーからの減らしてほしいというフィードバック通りに、次回以降のセッションでそのユーザーに類似記事を推薦する割合を減らすこと。2つ目は、その記事を減らすだけではなく、他にどんなコンテンツを推薦するかまでを含めて、推薦全体の質を向上させることです。
この課題についても2つの方法を試しました。
1つ目は、ユーザーベクトルの学習時にユーザーの行動履歴を入力したのと同様に、dislikeシグナルを送った履歴も特徴量の1つして利用して、ユーザーベクトルを学習する方法です。もし、dislikeシグナルを送られた記事はユーザーにクリックされないのだとしたら、このレコメンドモデルは、dislikeされた記事のスコアを下げた方が良いことを学習すると考えました。
2つ目は、入力としてdislikeシグナルを利用するのに加えて、メトリックラーニングでdislikeした記事を負例として使い、dislikeした記事とユーザーベクトルの距離を離す学習を直接行う方法です。明示的に距離を離すように学習するので、必然的にdislikeされた記事はスコアが下がります。
下図は2つの手法を試した結果です。
