見出し画像

多様なコンテンツをとどける、レコメンドベースのnoteのホームタイムラインをつくる

本記事では、note社内において、レコメンドとパーソナライズをベースにした新しいホームタイムラインのMVP(Minimal Viable Productの意、開発コードネームはHorizon)を開発した経緯や思想とその推移を、エンジニアの観点から書いている。PdM的な観点から書いた以下の記事も参照いただけると幸いである。

想定する読者としては、以下のような読者を想定している。

  • 情報推薦や検索、データマイニング、機械学習の活用に興味があるエンジニア

  • ちょっと賢い機能をコアとしたプロダクトを開発したいと思っているプロダクト志向のエンジニア

  • パーソナライズや情報推薦をコアとしたプロダクトをマネジメントしている(しようとしている)プロダクトマネージャー

新しいホームタイムラインの実現のために、以下のような仕組みを実現した。

  • ユーザ閲覧履歴記事からのキーワード抽出

  • キーワード抽出 -> 記事推薦のアーキテクチャ

  • ホームタイムラインのセクションの動的な並び替え、セクションの並びをサーバサイドで決定する手法

  • 効果検証(ABテスト)やビッグバンリリースを防ぐcanaryリリースの手法

長くなるので本記事では「キーワード抽出」に関してだけ書く。もっと知りたければスキしていただけると幸いである。

noteの新しいホームタイムライン 

新しいホームタイムライン画面ファーストビュー

noteの新しいホームタイムラインはこんな感じである。noteの新しいホームタイムラインは15%程度のユーザにリリースされていて、順次開放の予定である(2022/8 現在)。

新しいホームタイムラインのファーストビューでは、ユーザがフォローしているクリエイターの記事と、今までのホームタイムラインでも表示されていた「あなたへのおすすめ」が、一つのセクションにまとまって横並びに表示されている。フォロー中の記事なのか、推薦記事なのかの識別性と視認性が改善した。

スクロールすると、ユーザの興味に基づいた推薦記事が、横並びのセクションで並ぶ。筆者の場合は、プロダクトマネジメント関連の記事をよく読んだり、note内でプログラミング言語LISPの記事をたまに検索で漁っていたりするので、推薦記事として表示されている。

ユーザーの興味に基づきパーソナライズされたホームの必要性

noteには、のべ2000万を超える多様な創作物が投稿されており、新しく2万を超える創作物が毎日投稿され続けている。noteという街のクリエイターは2022年5月に500万を超えた。noteは、クリエイターとその創作物、クリエイター同士の出会いの機会を生み出すことができ、創作をつづける中での「スキ」なものとの出会いをまだまだ創出できる可能性がある。

筆者のTwitterホームタイムライン。スターウォーズや自分の行動に基づいた推薦ツイートが並ぶ。
筆者のTwitterのホームタイムライン(ホーム表示)。スターウォーズや、行動に基づいた関連ツイートが並ぶ。

近年のコンテンツ系のプロダクトでは、ユーザーが新たなコンテンツと出会うことを促す仕組み(レコメンデーションやパーソナライズ)を搭載していることも多い。例えばTwitterでは、ユーザーの興味のありそうなトピックに基づくツイートがユーザーに表示される。

筆者のChromeホーム画面。Google Discoverの推薦記事がならぶ,
筆者のChromeのホーム画面。Google Discoverの推薦記事が並び、興味のないトピックは非表示にできる。

Google Chromeを例に挙げると、閲覧した記事のトピックに基づいた記事がホーム画面に推薦される機能がある。Spotifyでは、カテゴリ分けされた推薦楽曲や音声コンテンツが横並びのセクションで表示される。Instagramではハッシュタグをフォローすることで、好みのハッシュタグにひもづく新しい投稿を簡単にウォッチできる。

noteではのべ2000万を超える多様な創作コンテンツが投稿されているにも関わらず、「『よいコンテンツ』(よいコンテンツは人によって異なるというのも難しいポイント)が埋もれてしまっている」=「よい記事があるのに見られないことがある」という課題を抱えていた。とどくべき人に作品が届かないということは、クリエイターが長く創作をつづける上での妨げにもなってしまう。これを解決するために、上述のような「ユーザーが新たなコンテンツと簡単に出会う仕組み」を新たに検証することを考えることにした

ホームタイムラインを改修するモチベーション

届くべき人に適切に作品がとどかない状態、と言うと難しく聞こえるが、「ユーザとコンテンツが適切にマッチングする状態」と言い換えると少しやさしくなる。われわれは、現状の認識とwantな状態を定義するところ、PdM のIshizaka sanの助けを借りながら、理想像を具体化するところから初めた。

具体的には以下のような現状認識やto beを議論した。

  • note内にたくさんある作品の認知を広げたい: 単に記事をレコメンドするだけではなく「ユーザーの認知」を広げることができないだろうか。

    • これを実現するためには、こういうジャンルカテゴリがあるよというのをユーザに提示してあげる必要性がある。

  • 現状のホームタイムラインでは、好きなジャンルやカテゴリの記事があるから戻ってこようという認知が働きづらい

    • フォロー中のユーザーを見にくる場所として機能している
      セクションの並び替え

  • 「あなたへのおすすめ」は一定CTRはあるものの、既存ホームタイムラインのフォロー中の記事枠におもむろに差し込まれる、記事下に表示される仕様のため、一見見分けがつきづらいし、パーソナライズされている感じがない

    • たまにフォロー中に知らない記事差し込まれたけどなんやこれ的な声をいただく

どうするかの方針と基本的な考え方

パーソナライズやレコメンド自体は世の中(研究、実践含めて)に知見は山ほどあるし、そもそもnote内でも機械学習ベースのアルゴリズムで既に実現されていて価値を示していることは言うまでもない(本記事の読者の皆さまも「あなたへのおすすめ」で、すてきな作品やすてきなクリエイターと出会う体験をしたことがあるだろうと筆者は勝手に思っている)。

既存のホームタイムではフォロー中のユーザの記事の間に「あなたへのおすすめ」記事が差し込まれる

さらにto beに示したような「ラベルやカテゴリ付けされた記事群を推薦すること」のプロダクト的な価値自体は極めて未知数である。

そのため、検証ステップを経る必要があり、検証結果によっては作ったものを大胆に捨てる意思決定をする可能性がある 。それを考えるとローカロリーに推薦アルゴリズムの開発を進めるかつ必要とあらばモジュラーにエクステンド/パージできるようにする必要がある。ただしうまく行った場合には、モバイルなどに横展開できる必要がある。

検討した結果、以下のような示唆が得られた。

  • 「あなたへのおすすめ」のことを考えるに、ラベル(その記事がどんな意味を持つか)つきで記事を推薦することはユーザーの推薦記事への認知負荷を軽減する意味はありそうで、さらにnote内の記事への認知を広げることにつながりそうではないか(リテンションを促したり、PVを向上させるものであろう)

  • プロトタイプアルゴリズムとそのプロダクトを開発して、段階的に価値検証(ABテストなど)していくのはローカロリーでよさそう

  • いきなり機械学習系のアルゴリズムを投入するのは失敗した時のリスクが大きい、中身がブラックボックスなアルゴリズムも多いので、プロダクト開発が不測の事態に陥った時になぜそうなるのかの説明や対応するための改良プランを出すのが難しい、当然実計算時間もかかるアルゴリズムが多いので手数が減ってしまう

    • ダメだったら途中でストップ、改良すればよい

  • Keyphrase Extraction / Document Embeddingベースの比較的ハイカロリーな拡張を想定している

  • ユースケース、ホームタイムライン、セクションの並び替え

ユーザーの記事閲覧履歴に基づいた興味のありそうなキーワードの抽出と記事推薦

一般的に、documentから「そのdocumentがなにを意味するか」を抽出することは難しいが、先人達の研究や試みにより、トピックモデリングの分野として確立され実践されてきた。note MLチームでは、クリエイターが投稿した記事群から計算した文章埋め込み表現(document embedding)を独自にストアしており、将来的に、トピックモデリングに限らず、なにかしらの手法として活用する余地はありそうだということはわかっていた。

これを踏まえたうえで、MVPの実現のために、いくつか手早く実装できそうな手法と、プロダクト的にスケールしそうなアーキテクチャを検討する。

例えば、以下のようにA,B,Cのような3つのアーキテクチャを考案した。

  • A:「あなたへのおすすめ」からトピック(キーワード)を抽出してグルーピングする

    • MLパイプラインを含めて既存構造の拡張が必要そうだ

  • B: ユーザの記事閲覧履歴からトピック(キーワード)を抽出して、関連する記事を(可能なら意味的内容も含めて)全文検索する

    • 記事推薦を、1) キーワード抽出、2) クエリにマッチするdocumentをrankingする、という問題に変換する

    • キーワード抽出と記事推薦を別の問題として分解できるので細かいチューニングなどはしやすそう

    • 全文検索エンジンで完結でき、アーキテクチャ的にもスケールしやすそう

      • 推薦のインターフェイスとして全文検索エンジンを使うと良さそう

      • 今後document embeddingをインデックスして検索することもあり得る 

  • C: 既存の「カテゴリ」を応用する

    • note では大ざっぱなカテゴリのデータをストアしていてこれを活用できないか

本件ではプロダクト水準がMVP〜であることも考慮して、MLパイプラインには手を入れない、Bの手法を採用することにした。

ユーザの興味のありそうなキーワードを抽出する(Elasticsearch編)

Analyzerなどが適切に設定された全文検索エンジン(Elasticsearch)に、記事のデータがインデックスされてさえいれば、独自のスコアを定義して記事をrankingすることで、リアルタイムな記事推薦を実現できる。加えて、Elasticsearch独自の機能を利用することで、記事の集合からその記事集合内の重要語句の抽出を実現できる。Significant Terms Aggregation (以後、STAと略す)と呼ばれている機能だ。STAでは、JLH Score (単語部分集合ごとの出現割合)に基づいて、重要語句を抽出する。

本件では、まずユーザの閲覧履歴の記事に基づいてSTAで重要語句を抽出し、さらに当該キーワードに関連するキーワードをそのキーワードを含むドキュメントを対象にSTAで抽出するという二段構えの手法を試した。

擬似コードを以下に示す。

def aggr(name):
    df_ids = pd.read_csv(name)
    note_ids = df_ids["note_id"].tolist()
    query = {"query":{
                "bool":{
                    "filter": {
                        "terms": {
                        "id": note_ids
                    }
                } 
             }
         },
         "aggregations": {
             "significantAggTypes": {
                 "significant_terms": {
                     "field""free_body",
                     "size"20
                 }
             }
         }
        }
    return client.search(index=index_name, body=query)

def related_words(word):
    query = {"query":{
                "match":{"free_body": word}
             },
             "aggregations": {
                "significantAggTypes": {
                "significant_terms": {
                    "field""free_body",
                    "size"50
                }
             }
            }
          }
    result = client.search(index=index_name, body=query)
    return [[res["key"], res["score"] ] for res in result["aggregations"]["significantAggTypes"]["buckets"]]
# Someone's read log is given
result = aggr("./kihaya_read_note_ids.csv")
base_prefs_words = [res["key"for res in result["aggregations"]["significantAggTypes"]["buckets"]]

res = []
# Extract related words from base words
for word in base_prefs_words:
    res = res + related_words(word)

sorted(res, reverse=True, key=lambda x: x[1])

筆者が読んだnote記事のデータに対して上記のコードを実行すると、次のようなスコア付きの出力が得られる。`powershell` や `parallels` といった固有名詞もうまく抽出できていることがわかる。最初Kuromojiで頑張っていたが、Neologdに辞書を変えたら少しだけ抽出語句に改善が見られた。

['linux'173.34883720930233],
 ['gb'103.12500000000001],
 ['macbook'78.75531914893617],
 ['os'53.32608695652174],
 ['windows'52.550000000000004],
 ['ipad'48.00000000000001],
 ['mac'41.11797752808989],
 ['インストール'40.65],
 ['iphone'34.032710280373834],
 ['pro'28.285156250000004],
 ['apple'23.82450331125828],
 ['ubuntu'23.404493829588475],
 ['ssd'20.381944444444446],
 ['pc'18.272493573264782],
 ['メモリ'16.775193798449614],
 ['apt'16.125473228772307],
 ['powershell'16.125473228772307],
 ['usr'16.125473228772307],
 ['ram'16.113078703703703],
 ['os'14.123624563646198],
 ['インチ'13.826456541421459],
 ['インチ'13.467239583333335],
 ['512'13.303240740740742],
 ['cpu'13.155048076923077],
 ['ストレージ'13.137500000000001],
 ['unix'12.881773931855058],
 ['parallels'12.881773931855058],
 ['macos'12.554421308815577],
 ['スペック'12.219862459546928],
 ['sudo'12.094104921579229],
 ['freebsd'12.094104921579229],

この手法はうまくいっているように見えるが、課題がある。今回の例では、比較的内容がまとまった記事をよく読んでいるユーザを例にあげた。しかし、小説やエッセイといった内容が多岐にわたる記事を多く読んでいるユーザの興味のあるキーワードは抽出しにくいことがわかった。 具体的には、「写真」や「日記」といった、ごく汎用的なキーワードが抽出されてしまい、意味をなす結果が得られなかった。「インストール」などの名詞としては意味があるが、ラベルとして利用できるかどうかはわからないキーワードの対応など、抽出キーワードのコントロールが難しいというのも課題である。

ユーザの興味のありそうなキーワードを抽出する(ヒューリスティック編)

結果的にMVP開発のために採用したのは、noteにあるハッシュタグを活用したヒューリスティックな手法だ。この手法にはSTAを利用した自動的なキーワード抽出手法と比較していくつかメリットがある。

  • 抽出キーワードのコントロールが容易

    • 抽出されるキーワードのホワイトリスト・ブラックリストをつくることができる

  • 計算が超シンプル = SQLだけで完結する

    • note内のドキュメント数、ハッシュタグ数、記事閲覧履歴をもとにしたTF-IDF like なクエリを実装

    • Redashからでも抽出処理が実行できる

  • クリエイターがannotateしたデータを活用できる

    • クリエイターがこの記事はこのトピックでというのをannotateしてくれている

    • わざわざキーワード抽出する必要がない

クエリは社内的な事情もあるのでここではお見せできないが、先述の課題を解決するような仕組みを構築できた。あとは、キーワードを使って、記事を全文検索エンジンでrankingするだけだ。

MVPを評価する

MVPといってもアルゴリズム(キーワード抽出・記事推薦)的な側面と、UI/UXや実際のユーザの反応など含めたプロダクト的な側面があるが、本件では、その両面から評価を行ったのでいくつか紹介する。

アルゴリズム的な側面

  • 定性評価(オフライン評価):社内ユーザ何人か分のキーワード抽出・記事推薦の結果をスプレッドシートに吐き出し、目視で確認してもらう。何日か分の記事推薦をシミュレートして、推薦結果の更新性を確認する。

  • 定量評価:既存のホームタイムライン中の差し込み枠を活用したABテストを本番環境にリリースする

プロダクト的な側面

  • UI/UXのプロトタイピング・社内ユーザインタビュー:先述のスプレッドシートに吐き出した推薦記事を、Figma上に作成したUIプロトタイプに流し込んで、ユーザインタビューを実施

  • さくっとアーキテクチャを設計し、さくっとMVPを開発する

    • リアルタイムに記事推薦を行うので、レスポンス性能・負荷などを検証

  • 旧ホームタイムラインとのABテスト:旧ホームタイムラインから移行するために、PVなどを指標としたABテストを実施

まとめと課題

本記事では、noteの新しいホームタイムラインをつくる試みと、その手法について、部分的に紹介した。今後も多くのクリエイターの作品がより多くの読者にとどくための改善をつづけていく。

準備していた旧ホームタイムラインとのABテストでも、ポジティブな結果が得られた。しかし、ユーザからポジティブ・ネガティブ含めて、さまざまな意見を頂いている。いくつか紹介させてもらうとともに、実装するかしないかはおいておいて、今後の展望なども述べておく。

  • フォロー中のユーザの記事が追いづらくなった:セクションごとに記事が並ぶので、一画面で視認できるフォロー中ユーザの記事が少なくなってしまった

    • 本当に追いたいユーザを追いかけられるようにしたらどうだろうか?

最後に


noteのレコメンデーション・機械学習・プロダクト開発領域では以下のポジションで仲間を募集している、ご応募を心よりお待ちしています。


この記事が気に入ったらサポートをしてみませんか?