「俺はB級映画が見たいって言ったじゃないか!」
次に何を見たらよいのかわからず、Netflix を延々とスクロールし続けることにうんざりしていませんか? 次に観たい映画を正確に予測する独自のカスタム AI 駆動型推奨システムを構築できたらどうでしょうか?
このチュートリアルでは、ベクター データベース (VectorDB)を使用して映画推奨システムを作成するプロセスについて説明します。最新の AI 推奨エンジンの仕組みを学び、 Superlinkedを使用して独自のシステムを構築する実践的な経験を積むことができます。
(コードに直接ジャンプしたいですか?こちらのGitHub のリポジトリをご覧ください。独自のユースケースでレコメンデーション システムを試す準備はできましたか?こちらのデモを入手してください。)
この記事では、このノートブックに沿って進めていきます。また、 Colab を使用してブラウザから直接コードを実行することもできます。
Netflix の推奨アルゴリズムは、膨大な選択肢 (2023 年には約 16,000 本の映画と TV 番組) と、ユーザーに番組を提案しなければならないスピードを考えると、関連性の高いコンテンツを提案するのに非常に優れています。Netflix はどのようにしてそれを行っているのでしょうか。一言で言えば、セマンティック検索です。
セマンティック検索は、ユーザーのクエリや映画/テレビ番組の説明の背後にある意味とコンテキスト (属性と消費パターンの両方) を理解するため、従来のキーワードベースのアプローチよりもクエリと推奨事項のパーソナライズが向上します。
しかし、セマンティック検索にはいくつかの課題があります。その中でも最も重要なのは、1) 正確な検索結果の確保、2) 解釈可能性、3) スケーラビリティです。これらは、成功するコンテンツ推奨戦略が対処しなければならない課題です。Superlinked のライブラリを使用すると、これらの困難を克服できます。
この記事では、 Superlinked ライブラリを使用して独自のセマンティック検索を設定し、好みに基づいて関連する映画のリストを生成する方法を説明します。
セマンティック検索はベクトル検索において多くの価値をもたらしますが、開発者にとってベクトル埋め込みに関する 3 つの大きな課題をもたらします。
Superlinked ライブラリを使用すると、これらの課題に対処できます。以下では、特定の映画に関する情報から始めて、この情報をマルチモーダル ベクトルとして埋め込み、すべての映画の検索可能なベクトル インデックスを構築し、クエリの重みを使用して結果を微調整し、適切な映画の推奨に到達するコンテンツ レコメンダー (特に映画用) を構築します。それでは始めましょう。
以下では、Superlinked ライブラリの次の要素を使用して、Netflix 映画データセットでセマンティック検索を実行します。
映画をうまく推薦することが難しいのは、選択肢が非常に多い(2023 年には 9000 タイトル以上)ためであり、ユーザーはオンデマンドですぐに推薦を求めています。データ駆動型のアプローチで、見たいものを探してみましょう。映画のデータセットでは、次のことがわかっています。
これらの入力を埋め込み、埋め込みの上にベクトル インデックスをまとめることで、意味的に検索できる空間を作成できます。
インデックス付きベクトル空間を取得したら、次の操作を行います。
最初のステップは、ライブラリをインストールし、必要なクラスをインポートすることです。
(注: 以下、これをgoogle colabで実行している場合は、 alt.renderers.enable(“mimetype”)
をalt.renderers.enable('colab')
に変更します。 githubで実行している場合は、 “mimetype” をそのままにしておきます。)
%pip install superlinked==5.3.0 from datetime import timedelta, datetime import altair as alt import os import pandas as pd from superlinked.evaluation.charts.recency_plotter import RecencyPlotter from superlinked.framework.common.dag.context import CONTEXT_COMMON, CONTEXT_COMMON_NOW from superlinked.framework.common.dag.period_time import PeriodTime from superlinked.framework.common.schema.schema import schema from superlinked.framework.common.schema.schema_object import String, Timestamp from superlinked.framework.common.schema.id_schema_object import IdField from superlinked.framework.common.parser.dataframe_parser import DataFrameParser from superlinked.framework.dsl.executor.in_memory.in_memory_executor import ( InMemoryExecutor, InMemoryApp, ) from superlinked.framework.dsl.index.index import Index from superlinked.framework.dsl.query.param import Param from superlinked.framework.dsl.query.query import Query from superlinked.framework.dsl.query.result import Result from superlinked.framework.dsl.source.in_memory_source import InMemorySource from superlinked.framework.dsl.space.text_similarity_space import TextSimilaritySpace from superlinked.framework.dsl.space.recency_space import RecencySpace alt.renderers.enable("mimetype") # NOTE: to render altair plots in colab, change 'mimetype' to 'colab' alt.data_transformers.disable_max_rows() pd.set_option("display.max_colwidth", 190)
また、データセットを準備する必要があります。時間定数を定義し、データの URL の場所を設定し、データ ストア ディクショナリを作成し、CSV を pandas DataFrame に読み込み、データフレームとデータをクリーンアップして適切に検索できるようにし、簡単な検証と概要を実行します。(詳細については、 セル 3 と 4 を参照してください。)
データセットが準備されたので、Superlinked ライブラリを使用して取得を最適化できます。
Superlinked のライブラリには、インデックスの構築と検索の管理に使用するコア ビルディング ブロックのセットが含まれています。これらのビルディング ブロックの詳細については、 ここ をご覧ください。
まず、システムにデータを伝えるためにスキーマを定義する必要があります。
# accommodate our inputs in a typed schema @schema class MovieSchema: description: String title: String release_timestamp: Timestamp genres: String id: IdField movie = MovieSchema()
次に、スペースを使用して、埋め込み時にデータの各部分をどのように処理するかを指定します。どのスペースが使用されるかは、データ型によって異なります。各スペースは、可能な限り最高品質の検索結果を返すようにデータを埋め込むように最適化されます。
スペース定義では、データ内の意味関係を反映するために入力をどのように埋め込むべきかを説明します。
# textual fields are embedded using a sentence-transformers model description_space = TextSimilaritySpace( text=movie.description, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) title_space = TextSimilaritySpace( text=movie.title, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) genre_space = TextSimilaritySpace( text=movie.genres, model="sentence-transformers/paraphrase-MiniLM-L3-v2" ) # release date are encoded using our recency space # periodtimes aim to reflect notable breaks in our scores recency_space = RecencySpace( timestamp=movie.release_timestamp, period_time_list=[ PeriodTime(timedelta(days=4 * YEAR_IN_DAYS)), PeriodTime(timedelta(days=10 * YEAR_IN_DAYS)), PeriodTime(timedelta(days=40 * YEAR_IN_DAYS)), ], negative_filter=-0.25, ) movie_index = Index(spaces=[description_space, title_space, genre_space, recency_space])
スペースを設定してインデックスを作成したら、ライブラリのソース部分とエグゼキュータ部分を使用してクエリを設定します。 ノートブックのセル 10 ~ 13 を参照してください。
クエリの準備ができたので、クエリの実行と重みの調整による検索の最適化に進みましょう。
新しさのスペースを使用すると、データセットから古いリリースまたは新しいリリースを優先的に取得することで、クエリの結果を変更できます。期間として 4 年、10 年、40 年を使用して、タイトルが多い年に焦点を当てられるようにします ( セル 5を参照)。
4 年、10 年、40 年でスコアが途切れていることに注目してください。40 年以上前のタイトルにはnegative_filter
スコアが付けられます。
ノートブックに結果を表示するための簡単なユーティリティ関数を定義しましょう。
def present_result( result: Result, cols_to_keep: list[str] = ["description", "title", "genres", "release_year", "id"], ) -> pd.DataFrame: # parse result to dataframe df: pd.DataFrame = result.to_pandas() # transform timestamp back to release year df["release_year"] = [ datetime.fromtimestamp(timestamp).year for timestamp in df["release_timestamp"] ] return df[cols_to_keep]
Superlinked ライブラリを使用すると、さまざまな種類のクエリを実行できます。ここでは 2 つを定義します。両方のクエリ タイプ (シンプルとアドバンス) では、好みに応じて個々のスペース (説明、タイトル、ジャンル、もちろん最新) に重みを付けることができます。これらの違いは、シンプル クエリでは、1 つのクエリ テキストを設定すると、説明、タイトル、ジャンルのスペースで同様の結果が表示されることです。
高度なクエリを使用すると、よりきめ細かな制御が可能になります。必要に応じて、説明、タイトル、ジャンルの各スペースに異なるクエリ テキストを入力できます。クエリ コードは次のとおりです。
query_text_param = Param("query_text") simple_query = ( Query( movie_index, weights={ description_space: Param("description_weight"), title_space: Param("title_weight"), genre_space: Param("genre_weight"), recency_space: Param("recency_weight"), }, ) .find(movie) .similar(description_space.text, query_text_param) .similar(title_space.text, query_text_param) .similar(genre_space.text, query_text_param) .limit(Param("limit")) ) advanced_query = ( Query( movie_index, weights={ description_space: Param("description_weight"), title_space: Param("title_weight"), genre_space: Param("genre_weight"), recency_space: Param("recency_weight"), }, ) .find(movie) .similar(description_space.text, Param("description_query_text")) .similar(title_space.text, Param("title_query_text")) .similar(genre_space.text, Param("genre_query_text")) .limit(Param("limit")) )
単純なクエリでは、クエリ テキストを設定し、自分にとっての重要度に応じて異なる重みを適用します。
result: Result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=1, genre_weight=1, recency_weight=0, limit=TOP_N, ) present_result(result)
結果には、すでに見たことのあるタイトルがいくつか含まれています。この問題に対処するには、新しさに重み付けして、結果を最近のタイトルに偏らせます。重みは、合計が 1 になるように正規化されます (つまり、すべての重みは常に合計が 1 になるように調整されます)。そのため、重みの設定方法について心配する必要はありません。
result: Result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=1, genre_weight=1, recency_weight=3, limit=TOP_N, ) present_result(result)
私の結果(上記)はすべて 2021 年以降のものになります。
シンプルなクエリを使用すると、特定のスペース (説明、タイトル、ジャンル、または最新性) に重みを付けて、結果を返すときにそのスペースをより重視することができます。これを試してみましょう。以下では、ジャンルに重みを付け、タイトルの重みを下げます。クエリ テキストは基本的に、追加のコンテキストがあるジャンルだけです。最新性はそのままにしておきます。これは、結果が最近の映画に偏っているようにするためです。
result = app.query( simple_query, query_text="Heartfelt romantic comedy", description_weight=1, title_weight=0.1, genre_weight=2, recency_weight=1, limit=TOP_N, ) present_result(result)
このクエリは、リリース年を少し後ろにずらして、よりジャンルを重視した結果を表示します (以下)。
高度なクエリでは、さらにきめ細かな制御が可能です。新しさの制御はそのままに、説明、タイトル、ジャンルの検索テキストを指定し、それぞれに好みに応じて特定の重みを割り当てることもできます(以下およびセル 19 ~ 21 )。
result = app.query( advanced_query, description_query_text="Heartfelt lovely romantic comedy for a cold autumn evening.", title_query_text="love", genre_query_text="drama comedy romantic", description_weight=0.2, title_weight=3, genre_weight=1, recency_weight=5, limit=TOP_N, ) present_result(result)
たとえば、前回の映画検索結果で、すでに見た映画が見つかり、似たような映画を見たいと思ったとします。たとえば、1954 年のロマンティック コメディー「ホワイト クリスマス」(id = tm16479) が好きだとします。この映画は、歌手とダンサーが集まってステージ ショーを行い、バーモント州の経営難の宿に客を呼び込むという内容です。advanced_query に、追加のwith_vector
句 ( movie_id
パラメータ付き) を追加することで、with_movie_query ではこの映画 (または好きな映画) を使用して検索できるようになり、個別のサブ検索クエリ テキストと重み付けを細かく制御できるようになります。
まず、movie_id パラメータを追加します。
with_movie_query = advanced_query.with_vector(movie, Param("movie_id"))
そして、他のサブ検索クエリを空にするか、最も関連性の高いものに設定し、意味のある重み付けも設定できます。最初のクエリがホワイト クリスマスのステージ パフォーマンス/バンドの側面を反映した結果を返すとします ( セル 24 を参照)。ただし、私はもっと家族向けの映画を見たいと考えています。description_query_text を入力して、結果を希望の方向に偏らせることができます。
result = app.query( with_movie_query, description_query_text="family", title_query_text="", genre_query_text="", description_weight=1, title_weight=0, genre_weight=0, recency_weight=0, description_query_weight=1, movie_id="tm16479", limit=TOP_N, ) present_result(result)
しかし、結果を見ると、実はもっと気楽で面白いものを求めていることに気づきました。それに応じてクエリを調整してみましょう。
Result = app.query( with_movie_query, description_query_text="", title_query_text="", genre_query_text="comedy", description_weight=1, title_weight=0, genre_weight=2, recency_weight=0, description_query_weight=1, movie_id="tm16479", limit=TOP_N, ) present_result(result)
よし、結果はこちらの方が良い。このうちの 1 つを選ぶ。ポップコーンをつけて!
Superlinked を使用すると、検索品質のテスト、反復、改善が簡単になります。上記では、Superlinked ライブラリを使用して、Netflix のようにベクトル空間でセマンティック検索を実行し、正確で関連性の高い映画の結果を返す方法について説明しました。また、適切な結果が得られるまで重みと検索用語を微調整して、結果を微調整する方法についても説明しました。
さあ、 ノートブックを自分で試してみて、何ができるか見てみましょう。
推奨エンジンは、コンテンツを発見する方法を形作っています。映画、音楽、製品など、ベクター検索は未来です。そして今、独自のベクター検索を構築するためのツールが手に入りました。
著者:モル・カプロンツァイ