エンジニアの皆様こんにちは! Webサービスを開発しているとよく見る機能である、「関連する○○」や「他におすすめする○○」といったコンテンツのレコメンドについて弊社のケースをご紹介いたします。
日本語に直訳をしたら「お薦め」です。 Webサービスにおいてはユーザに対して「よりよい体験を提供したい」「新しい気付きを与えたい」といったときに提供される「お薦め」です。 サービスを利用しているユーザに対して、サービス提供者側から意図したモノをお薦めするタイプや、ユーザが持つ情報から興味が出ると想定されるモノをシステムがお薦めするタイプなど様々なパターンがあります。 ユーザが元々持っていた欲求に対するプラスアルファの価値を提供できたとき、レコメンドは成功したと考えています。
レコメンドには色々なタイプがあると挙げましたが、どんなものがあるのでしょうか。
専門的な説明になると分類は詳細になってしまうので、簡単に例を出してみます。
タイプ | 説明 |
---|---|
ルールベース | サービス提供者が意図的にルール化したモノをレコメンドする |
コンテンツ(アイテム)ベース | モノの持つ情報を数値化し、類似や関連を持つ似たモノをレコメンドする |
協調フィルタリング | ユーザの特徴、行動、嗜好などからパーソナライズし、似たユーザが好んだモノをレコメンドする |
CyberOwlでは「観たい」が見つかる作品検索アプリ「aukana」というサービスを提供しています。 aukanaは映画、ドラマ、アニメ、バラエティといったあらゆるジャンルを持つ映像作品を探すお手伝いをするスマートフォンアプリですが、その中で作品のレコメンドもしています。 このaukanaは、Elasticsearchを利用したコンテンツベースのレコメンドを採用しています。
Elasticsearch(エラスティックサーチ)とはElastic社が提供している全文検索エンジンです。ElasticsearchではRDBとは異なる用語が登場します。
以下のように置き換えて考えてください。
RDB | Elasticsearch |
---|---|
データベース | インデックス |
レコード | ドキュメント |
テーブル定義 | マッピング |
ドキュメントの登録、インデックスの作成、マッピングの定義、データの検索などは全てREST API形式のエンドポイントにjsonデータを渡すことで実行できます。
インデックスを作成する際、日付形式などデータから推測できるものは自動でマッピングを定義してくれます。
マッピングは自分で定義することもできます。
フィールドの型以外に、言語解析の方法などが定義できます。
レコメンドシステムを作る時、基本的に2つのアプローチが候補に上がります。コンテンツベースフィルタリング(内容ベースフィルタリング)と協調フィルタリングです。
特徴を持つ商品でA・B・Cがあり、ユーザがそれらに対し「好き/お気に入り」といった特徴が見られるとき、ユーザの好みや趣向の特徴と商品の特徴の類似度を元に、ユーザが好むであろう商品のリストを見つける事ができます。
ユーザAがとある商品群を好きだとします。また、ユーザBは別の商品群が好きだとします。ユーザAとBの好きな商品を比べて同じ商品がいくつかあるとした時、それに基づいて、ユーザBが好む可能性のある新たな商品のリストを見つける事ができます。 どちらのアプローチにも長所と短所があります。コンテンツベースフィルタリングは、比較的簡単なプログラミングで実装でき、新しく追加された製品をユーザーに紹介するのに適していますが、ユーザーが関心のある特徴のみサジェストされる事になり多様性が出ません。協調フィルタリングは、多様性の問題を克服できますが、コールドスタートの問題があります。 実際にはさまざまなアプローチを組み合わせて、レコメンダシステムを微調整し、さまざまな目的を達成します。多くの場合でコンテンツベースフィルタリングはサービスを開始し、コールドスタート問題を解決するために必要です。この記事では、協調フィルタリングは別の日のトピックであるため詳細には説明しません。
例えばaukanaでは、映画毎に事前定義、もしくはユーザ投稿から定義された特徴があるとします。たとえば、映画「Mission Impossible」は、スパイ、アクション、サスペンスなどのジャンル(特徴)で構成されており、対応するスコアが事前に定義されています。 また、映画には気分や感情などの追加情報を、ユーザが映画レビューを投稿するときに選択させ、それらの情報を含めることができます。 その結果「Mission Impossible」は、スパイ(0.9)、アクション(0.8)、サスペンス(0.3)、クール(0.85)、エキサイティング(0.68)などの情報を持った時、映画の特徴ベクトルを次のように定義できます。 映画の特徴Vm = [0.9, 0.8, 0.3, 0.85. 0.68] 同様に、ユーザーの特徴ベクトルVuは、集計関数(たとえば、ユーザーが過去30日間に気に入った商品の平均など)を使用して計算できます。 次に、Vm・Vuの内積を計算して、類似性スコアを取得できます。その後、類似性スコアで映画をオーダーして、ユーザーが興味を持っている可能性のある映画のリストを取得できます。
コンテンツベースのレコメンドの考え方はシンプルにコンテンツの属性毎にパラメータを用意し、検索対象の属性パラメータの近い順でソートすることに他ならない。これをRDBで実装しようとすると困ったことになります。 ストレートに考えれば、属性ごとにカラムを設けて数値化したパラメータを保持しソートします。この場合、結果のソートはどうしても属性ごとで優先順位が固定されてしまいます。そして、品質の良いレコメンド結果を得るためには大抵多くの属性を使うことになりますが、パフォーマンスの観点でも問題は多いでしょう。 これを解決するために、属性パラメータを一つのベクトルとしてまとめて、コサイン類似度で比較します。しかし、RDBでは2つのベクトルのコサイン類似度の計算はできても、ベクトルデータそのもののインデックスを作成し、高速に検索対象のベクトルとのコサイン類似度で全データをソートすることはできません。 この周りをいい具合に実装してくれたのがElasticsearchのRankFeature機能となります。
RankFeatureとは、ドキュメントをインデックスする際に、「特徴」として「カテゴリー」と「重み」を一緒に保存したり、それを利用して検索する機能の事です。 例えば「ドラえもん」をインデックスする時、 「アニメ=70点」「ほのぼの=30点」「子供向け=50点」「SF=20点」「恋愛=0点」 の様に、カテゴライズだけでなく重みもRankFeatureとして一緒に保存できます。 そうしておくことで、クエリーを飛ばす際に`rank_feature`フィールドを使用すれば、「アニメが大好き」「子供向けが好き」「SFがちょっと好き」といった特徴をもつユーザーに対して、狙ってドラえもんをレコメンドすることができます。
例 : 下記のクエリはElasticsearchにAukanaの作品がインデックスされ、そしてインデックスに作品の特徴を追加します。
PUT /product
{
"mappings": {
"properties": {
"categories": {
"type": "rank_features"
},
"net_rating_score": {
"type": "rank_feature"
},
"genres": {
"type": "rank_features"
}
}
}
}
rank_featureとrank_featuresのタイプが二つあるのでシングルデータもリストのデータもインデックスできます。
PUT /product/_doc/1?refresh
{
"title": "アベンジャーズ/インフィニティ・ウォー",
"net_rating_score": 40,
"categories": {
"洋画": 50,
},
"genres": {
"アクション": 50,
"SF": 45,
"ヒーロー": 50
}
}
PUT /product/_doc/2?refresh
{
"title": "君の名は",
"net_rating_score": 50,
"categories": {
"邦画": 50,
},
"genres": {
"アニメ": 50,
"ファンタジー": 40,
"ヒューマンドラマ": 50
}
}
下記のようなクエリで邦画のカテゴリ及びアニメジャンルに基づいて作品を検索することができる
GET /product/_search
{
"query": {
"bool": {
"should": [
{
"rank_feature": {
"field": "categories.邦画",
"boost": 1.5
}
},
{
"rank_feature": {
"field": "genres.アニメ",
"boost": 2
}
}
]
}
}
}
Elasticseachはデフォルトでscoreの高い順にsortされて返ってきます。boostは条件にマッチした場合にscoreを加算する機能でboost: 2の場合はscoreが2倍になります。
Elasticsearchは計算してレコメンドを表示する為、答えは常に同じになります。つまりユーザーは自分で好みを変えなければ、同じ商品や作品のオススメを永遠に受け続けることになるでしょう。これではせっかくのレコメンド機能も、ユーザー体験としてつまらないものになってしまいます。 それを解決するのに簡単でかつ効果的な方法は、「わざと」クエリーに対してプログラムで「重み」を追加したり、引いたりすることです。 例えば、aukanaでは月曜日はアクション映画を、火曜日は恋愛映画のレコメンドを表示しやすくなるようにユーザー変数から生成されるクエリーに対して、プログラムからRankFeatureの値を追加や削除を行い、同一コンテンツになるのを防ぎます。
コンテンツベースフィルタリングの主な欠点の1つは、アプリケーションが成熟していくと、ユーザーにとってプログラムがより完璧になり、驚きや意外性のあるレコメンドが何も表示されないことです。この問題を軽減するにはいくつかの方法があります。 それらの1つは、毎日または毎週選別された商品のリストを、固定位置で混在させることです。このアプローチにはある程度の手動操作が必要ですが、利点は選別された商品リストは非機械的で、人間に優しく、ほとんどのユーザーにとって感動体験となります。 もう1つの方法は、メインセクションに隣接する別のセクションに最新のアイテムでソートされた個別のリストを表示することです。これにより、システムに追加された最新のアイテムが流動的に導入され、最初に表示され評価される機会が与えられます。 最後に、機械学習を使用して協調フィルターを実装することで、対象となるユーザとは直接関連付けられていなくても、同様の関心を持つユーザのグループが好きなアイテムを提案する事ができます。
※2020年3月30日時点