“一人ひとりにパーソナライズ”を実現する、リアルタイム推薦システムの仕組み

渡邉直樹 氏(以下、渡邉):みなさんこんにちは。「The Art of Smart Channel」と題して、渡邉直樹が発表させていただきます。このセッションでは「Smart Channel」というサービスのご紹介と、このサービスの中でされているさまざまな工夫についてお話をさせていただきます。

これがSmart ChannelのUIです。

Smart Channelとは、LINEアプリのトークタブの上部のこの部分に表示された小さなバナー領域を指しています。まずみなさまに質問をさせていただきたいんですが、トークタブのこの場所に何か表示されているのを見たことがある方、挙手していただいてもよろしいでしょうか。

(会場挙手)

ありがとうございます。ほとんどの方が何かしら見たことがあると思いますが、実はここに表示されているコンテンツはみなさん一人ひとりにパーソナライズされており、どんなものが表示されるかは人によって違っています。

表示されるコンテンツもさまざまなものがあります。天気や占いのほか、LINEスタンプやLINEマンガの新作情報、ニュースなど。ここに挙げているのはあくまでも一例で、他にもたくさんあります。そしてコンテンツの種類は、日々増えていっています。

日本だけではなく、台湾とタイでもコンテンツを配信しています。日本とまったく同じコンテンツが表示されているというわけではなくて、それぞれの国に合ったコンテンツを、それぞれの国の担当者が、その国のユーザが便利と思ってもらえるように運用しています。

毎日1億人のユーザにパーソナライズしたコンテンツを届ける

Smart Channelのコンセプトを紹介させてください。LINEにはチャットアプリ以外にもさまざまなサービスが存在します。Smart Channelは、こういったLINEに存在するあらゆるサービスから情報を集めてきて、ユーザ一人ひとりに最も適したコンテンツを最も適したタイミングでリアルタイムに表示するというのがコンセプトです。ここでいうコンテンツはどんなものでも良いわけではなくて、あくまでもユーザが実際に使ったことがあるサービスを指しています。

今までLINEでは各サービスごとにパーソナライズをしたレコメンデーションを作成したことはたくさんあるんですが、複数のサービスを横断してコンテンツを集めてきてリアルタイムに最適化をするというのは初めての試みでした。これをどのように実現しているかは、後ほど詳しくお話します。

コンテンツを表示するだけではなくユーザの体験にもこだわっています。これは私が実際に体験した「LINEマンガ」の例です。

『キングダム』という漫画の新刊が出たときに、その前の巻をLINEマンガで買っていた私に表示されたバナーです。このバナーをタップするとそのままLINEマンガアプリが立ち上がり、その場で購入することができます。発売日にわずか数クリックで最新刊を読み始めることができて、自分にとってはとても良い体験でした。

もちろん、アプリからの通知やLINEマンガのLINE公式アカウントからの通知もありますが、うっかり見落とすことが多いので、僕はいつもSNS上で誰かがコメントしているのを見て「あ、発売されたんだなぁ」と気付いて買いに行くんですが、こうやってお知らせをしてくれるのは私にとってはとても便利な経験でした。

この新刊漫画のNotificationは、私のまわりでもとくに評判の良いコンテンツの1つです。こういった、ユーザにとって便利であることを、このサービスでは大事にしています。

サービスの紹介の最後として、規模を知ってもらうために数値を3つ持って参りました。

1つ目は5億。これは1日のインプレッションの数です。非常にたくさんのコンテンツが毎日表示されています。そして、グローバルでは毎日1億人以上のユーザに対して、パーソナライズをしたコンテンツを届けています。瞬間的なトラフィックは25万リクエスト/秒。これはなかなか痺れる数値なんですが、このトラフィックが日々出ているわけではなくて、このトラフィックはニューイヤーのピークを基にしています。

非常に規模の大きいサービスであることが、おわかりいただけると思います。

Smart Channelを形作る5つのコンポーネントとフロー

さて、そんな規模のシステムを我々はどのように構築したかについてお話をしていきたいと思います。最初に全体的なフローをご紹介します。

Smart Channelでは大きく分けて5つのコンポーネントが連携して動作しています。まず各サービスからコンテンツを集めてくるImporter。これは非同期に動作しています。

CRS Engineは、LINEアプリからリクエストを受けてリアルタイムにパーソナライズされたコンテンツを選択して返却します。LINEアプリでコンテンツの表示や、クリックをされたときのイベントを受け取るのはEvent Trackerの役割です。このログをJoinerというコンポーネントでマシンラーニングで使いやすいかたちに変換して、最後にLearning workerで学習を行います。

Learning workerで学習された内容は、次回の配信で活用されることになります。ちなみに真ん中のCRS EngineのCRSというのはSmart Channelのコアシステムの名前で、Contents Recommender Serviceの略です。このCRS Engineからグルっと回ってLearning workerで学習されて次の配信で活かされます。この学習のサイクルをずっと繰り返していて、システムがどんどん賢くなっていってます。

パーソナライズを実現する2つのレイヤー

ここからは、具体的にどのようにコンテンツをパーソナライズしているかをご説明いたします。パーソナライズのために、我々は2つのレイヤーを用意しています。

1層目は各サービスごとにレコメンドのコンテンツを生成しています。これらのサービスでは既にレコメンドの生成を行っているものも多く、その機能を最大限に再利用しています。また、1つのサービスで複数のロジック、つまり条件でコンテンツを生成するケースもあります。

例えば天気であれば、今日の天気と明日の天気。先ほど例に挙げた漫画では、過去に購入した漫画の新刊が出たときにお知らせをするNotificationと、今購入して読んでいる漫画と類似している漫画をおすすめするレコメンデーションのロジックがあります。これらの複数のサービスからレコメンドされたコンテンツを集め、まったく違うジャンルのコンテンツの中からどれか1つだけを選ぶという全体最適化の部分が2層目になります。

これをレイヤーごとにもう少し深く見ていきましょう。まず、この1層目からお話をします。1層目を連携するサービスと協力して担当するのがImporterです。

各サービスには2つの機能を提供してもらいます。Mapping ProviderとData Providerです。

まずMapping Providerから、どのユーザに対してどのコンテンツを表示するかというマッピングをファイル形式でHDFSに置いてもらいます。

マッピングファイルを置いたらサービス側からImporterのAPIにNotificationを送ります。ImporterはこのNotificationを受け取ると、HDFSからマッピングファイルを受け取り処理を開始します。このマッピングだけだとどのユーザに何を出すか、ニュースAとかニュースBを表示するということしかわからないので、実際にそのコンテンツがどんなタイトルやどんな画像を持っているのかを知るためにサービス側のData Providerにマスタデータを問い合わせます。

そして、このマッピングとマスタデータをセットにしてRedisクラスタに一時的に保存しておきます。なぜ、このようにMapping ProviderとData Providerを分けているかと言うと、理由が2つあります。1つ目は、マッピングは前日までのログをベースに作られるため、基本的には1日1回生成することが多いんですが、マスタデータは常に変化しています。

例えば天気予報が時間で変わったり、ニュースの内容が更新されたりします。そのため、常に情報の鮮度を保つためマスタデータは任意のタイミングでリフレッシュできるようにしています。そのため、この2つの処理を分離しておくほうが、オペレーション上都合が良いという理由からです。

もう1つの理由はシンプルで、このMapping Providerはマシンラーニングのエンジニアが作ることが多く、Data Providerは各サービスのサーバサイドのエンジニアが作ることが多いです。これは実装する人が違うので分けたほうが作業がしやすいという効率性によるものです。

ベストなコンテンツを“1つだけ”選ぶには?

さて、先ほどの図に戻って参りました。次は、緑の丸で囲んでいる2層目の部分です。1層目が個々のサービスでパーソナライズをバッチ的に処理をしている層であったのに対し、ここは全体最適化を行う層で、リアルタイムに処理がされています。アーキテクチャ図でいうと、ここのCRS Engineが担当しています。

さて、この部分はけっこう難しくて、私たちが最も悩んだところの1つです。1層目で各サービスからコンテンツを集めてきたのは良いんですが、この中からこの瞬間、この人にベストなコンテンツを1つだけ選ばないといけません。この「1つだけ」というのが肝です。しかも、このコンテンツは刻々と入れ替わっていきます。これらのコンテンツを実際に表示する前に、ユーザにマッチしているかどうかを予測する必要があります。

しかし、事前にわかっている情報は意外と少なくて、コンテンツのカテゴリなどのスタティックな情報か、ユーザの性別と年代をLINEの行動ログの一部から推定したものだけです。

この問題は非常に難しい問題なんですが、我々はこの問題を解くのにバンディットアルゴリズムを採用しました。

バンディットアルゴリズムとは何か?

バンディットアルゴリズムは写真にあるようなスロットマシンでの利益を最大化する問題の解法として考案されました。スロットマシンの横にはレバーが付いているんですが、これがスロットマシンの起動スイッチになっています。このレバーのことをArmと呼んでいるんですが、スロットマシンがたくさんあるとArmもたくさんあるので、Multiarmed Bandit Problemと言ったりします。

このArmという用語は、このあと非常によく出てくるので覚えておいてください。ポイントとしては2つです。時間の流れをいくつかのラウンドに分割するんですが、1つのラウンドで同時に引けるArmは1個だけです。そして、どの台がどれだけ勝率が良いかは事前にはわからないという条件があります。これは我々が解きたい問題と非常によく似ています。

この条件下でリワード、つまり儲けを最大化することを目指します。Smart Channelにおいては、報酬は儲けではなくてユーザが喜んでくれることです。

バンディットアルゴリズムはExploration(探索)と、Exploitation(活用)を組み合わせて、この問題を解決しようとします。このExploration(探索)とExploitation(活用)について挙動を概念化した図です。

始めは情報が少ないので、まずはデータを集める意味でいろんなコンテンツを出してみて探索を行います。ある程度情報が集まってくると、学習された内容を使って効果が良さそうなコンテンツを選んで返します。これが活用です。実際はこんなに綺麗にパッと切り替わるわけではなくて、ある程度学習が進んだ段階で適度に探索を進めながら活用の割合をだんだんと増やしていってリワードを確保します。

図で言うと、この青いContentAがリワードの大きいコンテンツ、つまりユーザにマッチするコンテンツということになります。時間が経過すれば経過するほど学習が進み、確率は収束していきます。この探索と活用をどういったルールで行うかはバンディットアルゴリズムの実装に依存しています。

実際にSmart Channelでは4つのアルゴリズムをサポートしています。

はじめにLinUCBというアルゴリズムを実装しました。しばらくして性能改善のためにLiner Regression、Factorization Machinesが追加で実装されました。現在では性能比較のABテストをして、一番数値の良かったFactorization Machinesを全体的に採用しています。最後のNeural Factorization Machinesはちょうど開発が終わったところでして、今後ABテストをして性能が良ければ全体的に採用していこうと考えています。

Smart Channelに対するユーザーの3つのリアクション

ところで、学習のためにはバンディットのリワードとして、ユーザが喜んでくれたかどうかをシステム的に検知する必要があります。これはこれで別の難しさがあります。今度はコンテンツの評価についてお話をしていきましょう。

Smart Channelのバナーが表示されたときに、ユーザが取り得る行動は大きく3つあります。

1つ目はバナーをクリックしたとき。これはコンテンツに対するポジティブな反応であると考えられます。2つ目は何もしなかったとき。これはややネガティブな反応なんですが、例えば天気のように見るだけで満足するというようなコンテンツもあるので、このシステムではフラットな反応という扱いにしています。

最後は、ここにバツボタンがあるんですが、これをクリックしたときです。このバツボタンをクリックするとバナーが一時的に消えます。我々はこれをミュートと呼んでいて、強いネガティブな反応であると学習しています。

そういったイベントはEvent Trackerで受け取り、Joinerで加工されてLearning workerまで渡っていくことになるんですが、ここでもちょっとした工夫をしています。

ユーザがコンテンツに対してリアクションをするときのタイムラインを示してみました。

このタイムライン上で表示されたコンテンツに対して、クリックされたかミュートされたかを判定することになります。ここまでは良いんですが、問題は何もしなかったケースです。何もしないということに対するイベントというのは発生しないので、それをシステム的にどうやって検知するかといった問題があります。

答えは非常にシンプルなんですが、一定時間……このシステムでは10分間待ってから判断します。10分経った時点でミュートやクリックもなければ何もしなかったと判断することができます。もちろん、それまでにクリックとかミュートがあれば、それをそのまま採用します。この処理はJoinerと呼ばれる部分で実装しているのですが、ステップで説明します。

コンテンツが表示されると、まずはEvent Trackerにインプレッションのイベントが届きます。impのイベントはKafkaにProduceされてConsumerで処理されるんですが、通常のimpの処理に加えて、ここで10分間のタイマーを付けてタスク管理用の別のKafkaにプロデュースしておきます。一方で、遅れてきたクリックやミュートはシンプルにKafkaを通じて、そのままRedisに格納しておきます。

そして、このタイマーが切れる10分後、impがJoinerで処理されるときにこのRedisを参照して、もしクリックやミュートがあれば、そのままJoinしてKafkaに入れます。もし何もなければ何も反応しなかったということで、そのままKafkaに入れます。これがマシンラーニングのインプットになってきます。