ゲームの夜です。友達はゲーム テーブルの周りに腰掛け、ダンジョンズ & ドラゴンズ (D&D) のどのキャラクターになり、どのクエストに乗り出すか待ち構えています。今夜、あなたはダンジョン マスター (ストーリーテラー兼ガイド) となり、プレイヤーに挑戦し魅了するスリリングな出会いを作り上げます。頼りになる D&D モンスター マニュアルには何千ものモンスターが収録されています。無数の選択肢の中から、それぞれの状況に最適なモンスターを見つけるのは大変なことです。理想的な敵は、その瞬間の設定、難易度、物語に合致している必要があります。
それぞれのシナリオに最も適したモンスターを瞬時に見つけるツールを作成できたらどうでしょうか?複数の要素を同時に考慮し、それぞれの遭遇が可能な限り没入感と興奮に満ちたものになるようにするツールでしょうか?
私たち自身の探求に乗り出しましょう: マルチ属性ベクトル検索の力を活用して、究極のモンスター検索システムを構築しましょう!
ベクトル検索は情報検索に革命をもたらします。コンテキストと意味論的意味を考慮するベクトル埋め込みにより、ベクトル検索はより関連性の高い正確な結果を返し、構造化データだけでなく非構造化データや複数の言語も処理し、拡張できるようになります。しかし、実際のアプリケーションで高品質の応答を生成するには、多くの場合、データ オブジェクトの特定の属性に異なる重みを割り当てる必要があります。
複数属性ベクトル検索には、2 つの一般的なアプローチがあります。どちらも、データ オブジェクトの各属性を個別に埋め込むことから始まります。これら 2 つのアプローチの主な違いは、埋め込みの保存方法と検索方法にあります。
spaces
使用すると、クエリ時に各属性に重みを付けて、後処理なしでより関連性の高い結果を表示できます。 以下では、これら 2 つのアプローチを使用して、複数属性ベクトル検索ツール (ダンジョンズ & ドラゴンズのモンスター ファインダー) を実装します。特に 2 番目のシンプルな実装では、ユース ケースに関係なく、複雑で多面的なクエリを簡単に処理できる、より強力で柔軟な検索システムを作成する方法を説明します。
ベクトル類似性検索を初めて使用する場合でも、心配しないでください。私たちがサポートします。ビルディング ブロックの記事をご覧ください。
よし、モンスター狩りに行こう!
まず、大規模言語モデル (LLM) を実行して、モンスターの小さな合成データセットを生成します。
Generate two JSON lists: 'monsters' and 'queries'. 1. 'monsters' list: Create 20 unique monsters with the following properties: - name: A distinctive name - look: Brief description of appearance (2-3 sentences) - habitat: Where the monster lives (2-3 sentences) - behavior: How the monster acts (2-3 sentences) Ensure some monsters share similar features while remaining distinct. 2. 'queries' list: Create 5 queries to search for monsters: - Each query should be in the format: {look: "...", habitat: "...", behavior: "..."} - Use simple, brief descriptions (1-3 words per field) - Make queries somewhat general to match multiple monsters Output format: { "monsters": [ {"name": "...", "look": "...", "habitat": "...", "behavior": "..."}, ... ], "queries": [ {"look": "...", "habitat": "...", "behavior": "..."}, ... ] }
LLM が生成したデータセットのサンプルを見てみましょう。注: LLM 生成は非決定論的であるため、結果が異なる場合があります。
最初の 5 体のモンスターは次のとおりです。
# | 名前 | 見て | 生息地 | 行動 |
---|---|---|---|---|
0 | ルミノス | 光る羽と触角を持つ蛾のような生き物 | 発光植物が生い茂る密林とジャングル | 心地よい光のパターンを発して、獲物とコミュニケーションをとり、引き寄せます。 |
1 | アクアレイス | 流れる水でできた半透明の人型像 | 河川、湖沼、沿岸地域 | 水域に溶け込むように形を変え、流れを制御する |
2 | ストーンハートゴーレム | 絡み合った岩石で構成された巨大な人型生物 | ロッキー山脈と古代遺跡 | 何世紀も冬眠し、縄張りを守るために目覚める |
3 | ウィスパリング・シェイド | 光る目を持つ影のような不定形の存在 | 暗い森と廃墟 | 恐怖を糧に、不安をかき立てる真実を囁く |
4 | ゼファーダンサー | 虹色の羽を持つ優雅な鳥類 | 高い山の頂上と風が吹き渡る平原 | 仲間を引き付ける魅惑的な空中ディスプレイを作成します |
...そして生成されたクエリ:
| 見て | 生息地 | 行動 |
---|---|---|---|
0 | 輝く | 暗い場所 | 光の操作 |
1 | エレメンタル | 極限環境 | 環境制御 |
2 | シェイプシフティング | 多様な風景 | 幻想の創造 |
3 | 結晶質 | 鉱物資源が豊富な地域 | エネルギー吸収 |
4 | エーテル | 雰囲気 | 心の影響 |
元のデータセットとクエリの例については、こちらをご覧ください。
以下では、ナイーブとスーパーリンクの両方のアプローチで使用するパラメータを設定しましょう。
ベクトル埋め込みは次のように生成します。
sentence-transformers/all-mpnet-base-v2.
簡単にするために、出力を上位 3 つの一致に制限します。(必要なインポートとヘルパー関数を含む完全なコードについては、ノートブックを参照してください。)
LIMIT = 3 MODEL_NAME = "sentence-transformers/all-mpnet-base-v2"
それでは、多属性モンスターの検索を始めましょう! まず、単純なアプローチを試してみます。
私たちの素朴なアプローチでは、属性を個別に埋め込み、異なるインデックスに保存します。クエリ時に、すべてのインデックスに対して複数の kNN 検索を実行し、すべての部分的な結果を 1 つに結合します。
まずクラスを定義する
NaiveRetriever
all-mpnet-base-v2
で生成された埋め込みを使用して、データセットに対して類似性ベースの検索を実行します。
class NaiveRetriever: def __init__(self, data: pd.DataFrame): self.model = SentenceTransformer(MODEL_NAME) self.data = data.copy() self.ids = self.data.index.to_list() self.knns = {} for key in self.data: embeddings = self.model.encode(self.data[key].values) knn = NearestNeighbors(metric="cosine").fit(embeddings) self.knns[key] = knn def search_key(self, key: str, value: str, limit: int = LIMIT) -> pd.DataFrame: embedding = self.model.encode(value) knn = self.knns[key] distances, indices = knn.kneighbors( [embedding], n_neighbors=limit, return_distance=True ) ids = [self.ids[i] for i in indices[0]] similarities = (1 - distances).flatten() # by definition: # cosine distance = 1 - cosine similarity result = pd.DataFrame( {"id": ids, f"score_{key}": similarities, key: self.data[key][ids]} ) result.set_index("id", inplace=True) return result def search(self, query: dict, limit: int = LIMIT) -> pd.DataFrame: results = [] for key, value in query.items(): if key not in self.knns: continue result_key = self.search_key(key, value, limit=limit) result_key.drop(columns=[key], inplace=True) results.append(result_key) merged_results = pd.concat(results, axis=1) merged_results["score"] = merged_results.mean(axis=1, skipna=False) merged_results.sort_values("score", ascending=False, inplace=True) return merged_results naive_retriever = NaiveRetriever(df.set_index("name"))
上記で生成したリストの最初のクエリを使用し、 naive_retriever
使用してモンスターを検索してみましょう。
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } naive_retriever.search(query)
私たちの
naive_retriever
各属性に対して次の検索結果を返します。
id | スコア_ルック | 見て |
---|---|---|
ウィスパリング・シェイド | 0.503578 | 光る目を持つ影のような不定形の存在 |
サンドストームジン | 0.407344 | 輝くシンボルが渦巻く砂の渦 |
ルミノス | 0.378619 | 光る羽と触角を持つ蛾のような生き物 |
素晴らしい! 返されたモンスターの結果は関連性があり、すべて何らかの「光る」特性を持っています。
他の 2 つの属性を検索したときに、単純なアプローチで何が返されるかを見てみましょう。
id | スコア_生息地 | 生息地 |
---|---|---|
ウィスパリング・シェイド | 0.609567 | 暗い森と廃墟 |
真菌ネットワーク | 0.438856 | 地下洞窟と湿った森 |
ソーンヴァインエレメンタル | 0.423421 | 草木が生い茂る遺跡と密林 |
id | スコア_行動 | 行動 |
---|---|---|
生きたグラフィティ | 0.385741 | 周囲に溶け込むように形を変え、色素を吸収する |
クリスタルウィング・ドレイク | 0.385211 | 貴重な宝石を蓄え、光を屈折させて強力な光線を作ることができる |
ルミノス | 0.345566 | 心地よい光のパターンを発して、獲物とコミュニケーションを取り、引き寄せます。 |
取得したモンスターはすべて、必要な属性を備えています。一見すると、単純な検索結果は有望に思えるかもしれません。しかし、 3 つの属性すべてを同時に備えたモンスターを見つける必要があります。結果をマージして、モンスターがこの目標をどの程度達成しているかを確認しましょう。
id | スコア_ルック | スコア_生息地 | スコア_行動 |
---|---|---|---|
ウィスパリング・シェイド | 0.503578 | 0.609567 | |
サンドストームジン | 0.407344 | | |
ルミノス | 0.378619 | | 0.345566 |
真菌ネットワーク | | 0.438856 | |
ソーンヴァインエレメンタル | | 0.423421 | |
生きたグラフィティ | | | 0.385741 |
クリスタルウィング・ドレイク | | | 0.385211 |
ここで、単純なアプローチの限界が明らかになります。評価してみましょう。
look
: 3 体のモンスターが回収されました (Whispering Shade、Sandstorm Djinn、Luminoth)。habitat
: look
結果から関連するモンスターは 1 体のみでした (Whispering Shade)。behavior
: look
結果から関連するモンスターは 1 体のみ (Luminoth) でしたが、これはhabitat
に関連するモンスターとは異なります。つまり、単純な検索アプローチでは、すべての条件を一度に満たすモンスターを見つけることができません。属性ごとに積極的にモンスターをさらに取得することで、この問題を解決できるかもしれません。属性ごとに 3 匹ではなく 6 匹のモンスターで試してみましょう。このアプローチで生成されるものを見てみましょう。
id | スコア_ルック | スコア_生息地 | スコア_行動 |
---|---|---|---|
ウィスパリング・シェイド | 0.503578 | 0.609567 | |
サンドストームジン | 0.407344 | 0.365061 | |
ルミノス | 0.378619 | | 0.345566 |
星雲クラゲ | 0.36627 | | 0.259969 |
ドリームウィーバータコ | 0.315679 | | |
量子ホタル | 0.288578 | | |
真菌ネットワーク | | 0.438856 | |
ソーンヴァインエレメンタル | | 0.423421 | |
ミストファントム | | 0.366816 | 0.236649 |
ストーンハートゴーレム | | 0.342287 | |
生きたグラフィティ | | | 0.385741 |
クリスタルウィング・ドレイク | | | 0.385211 |
アクアレイス | | | 0.283581 |
これまでに 13 体のモンスター (小さなデータセットの半分以上) を取得しましたが、依然として同じ問題が残っています。つまり、3 つの属性すべてについて、これらのモンスターの 1 体も取得されなかったのです。
回収するモンスターの数を増やすと(6 体以上)、問題は解決するかもしれませんが、追加の問題が発生します。
要するに、単純なアプローチは、特に本番環境で実行可能な複数属性検索には不確実性が高く非効率的です。
2 番目のアプローチを実装して、単純なアプローチよりも優れているかどうかを確認しましょう。
まず、スキーマ、スペース、インデックス、クエリを定義します。
@schema class Monster: id: IdField look: String habitat: String behavior: String monster = Monster() look_space = TextSimilaritySpace(text=monster.look, model=MODEL_NAME) habitat_space = TextSimilaritySpace(text=monster.habitat, model=MODEL_NAME) behavior_space = TextSimilaritySpace(text=monster.behavior, model=MODEL_NAME) monster_index = Index([look_space, habitat_space, behavior_space]) monster_query = ( Query( monster_index, weights={ look_space: Param("look_weight"), habitat_space: Param("habitat_weight"), behavior_space: Param("behavior_weight"), }, ) .find(monster) .similar(look_space.text, Param("look")) .similar(habitat_space.text, Param("habitat")) .similar(behavior_space.text, Param("behavior")) .limit(LIMIT) ) default_weights = { "look_weight": 1.0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
次に、エグゼキュータを起動してデータをアップロードします。
monster_parser = DataFrameParser(monster, mapping={monster.id: "name"}) source: InMemorySource = InMemorySource(monster, parser=monster_parser) executor = InMemoryExecutor(sources=[source], indices=[monster_index]) app = executor.run() source.put([df])
上記の単純なアプローチの実装で実行したのと同じクエリを実行してみましょう。
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } app.query( monster_query, limit=LIMIT, **query, **default_weights )
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ウィスパリング・シェイド | 0.376738 | 光る目を持つ影のような不定形の存在 | 暗い森と廃墟 | 恐怖を糧に、不安をかき立てる真実を囁く |
ルミノス | 0.340084 | 光る羽と触角を持つ蛾のような生き物 | 発光植物が生い茂る密林とジャングル | 心地よい光のパターンを発して、獲物とコミュニケーションをとり、引き寄せます。 |
生きたグラフィティ | 0.330587 | 平面に生息する二次元的でカラフルな生き物 | 都市部、特に壁や看板 | 周囲に溶け込むように形を変え、色素を吸収します |
さあ、出来上がりです! 今回は、戻ってきた上位のモンスターは、モンスターに持たせたい 3 つの特性の「平均」を表すスコアで高い順位にランクされています。各モンスターのスコアを詳しく見てみましょう。
id | 見て | 生息地 | 行動 | 合計 |
---|---|---|---|---|
ウィスパリング・シェイド | 0.167859 | 0.203189 | 0.005689 | 0.376738 |
ルミノス | 0.126206 | 0.098689 | 0.115189 | 0.340084 |
生きたグラフィティ | 0.091063 | 0.110944 | 0.12858 | 0.330587 |
2 番目と 3 番目の結果である Luminoth と Living Graffiti は、どちらも 3 つの望ましい特性をすべて備えています。トップの結果である Whispering Shade は、 behavior
スコア (0.006) に反映されているように、光の操作という点では関連性が低いものの、「光る」特徴と暗い環境により、 look
(0.168) とhabitat
(0.203) のスコアが非常に高く、合計スコアが最も高く (0.377)、全体的に最も関連性の高いモンスターとなっています。なんと素晴らしい進歩でしょう!
結果を再現できるでしょうか? 別のクエリを試して確認してみましょう。
query = { 'look': 'shapeshifting', 'habitat': 'varied landscapes', 'behavior': 'illusion creation' }
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ミストファントム | 0.489574 | 変化する特徴を持つ、幽玄で霧のようなヒューマノイド | 沼地、荒野、霧の海岸線 | 幻想とささやきで旅人を惑わす |
ゼファーダンサー | 0.342075 | 虹色の羽を持つ優雅な鳥類 | 高い山の頂上と風が吹き渡る平原 | 仲間を引き付ける魅惑的な空中ディスプレイを作成します |
ウィスパリング・シェイド | 0.337434 | 光る目を持つ影のような不定形の存在 | 暗い森と廃墟 | 恐怖を糧に、不安をかき立てる真実を囁く |
素晴らしい!私たちの成果は今回も素晴らしいです。
データセットから特定のモンスターに似たモンスターを見つけたい場合はどうすればよいでしょうか。まだ見たことのないモンスター、Harmonic Coral で試してみましょう。このモンスターの属性を抽出し、クエリ パラメータを手動で作成することもできます。ただし、Superlinked にはクエリ オブジェクトで使用できるwith_vector
メソッドがあります。各モンスターの ID は名前であるため、次のように簡単にリクエストを表現できます。
app.query( monster_query.with_vector(monster, "Harmonic Coral"), **default_weights, limit=LIMIT )
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ハーモニックコーラル | 1 | 振動する巻きひげを持つ、分岐した楽器のような構造 | 浅い海と潮だまり | 複雑なメロディーを創り、感情を伝え、影響を与える |
ドリームウィーバータコ | 0.402288 | オーロラのように光る触手を持つ頭足動物 | 深海の海溝と水中洞窟 | 近くの生き物の夢に影響を与える |
アクアレイス | 0.330869 | 流れる水でできた半透明の人型像 | 河川、湖沼、沿岸地域 | 水域に溶け込むように形を変え、流れを制御する |
一番上の結果は、予想通り、最も関連性の高いハーモニック コーラルそのものです。検索で取得される他の 2 つのモンスターは、ドリームウィーバー オクトパスとアクア レイスです。どちらも、ハーモニック コーラルと重要なテーマ (属性) 要素を共有しています。
habitat
)behavior
)look
)ここで、 look
属性にもっと重点を置きたいとします。スーパーリンク フレームワークを使用すると、クエリ時に重みを簡単に調整できます。簡単に比較できるように、 look
優先するように重みを調整して、Harmonic Coral に似たモンスターを検索します。
weights = { "look_weight": 1.0, "habitat_weight": 0, "behavior_weight": 0 } app.query( monster_query.with_vector(monster, "Harmonic Coral"), limit=LIMIT, **weights )
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ハーモニックコーラル | 0.57735 | 振動する巻きひげを持つ、枝分かれした楽器のような構造 | 浅い海と潮だまり | 複雑なメロディーを創り、感情を伝え、影響を与える |
ソーンヴァインエレメンタル | 0.252593 | ねじれた蔓と棘の体を持つ植物のような生き物 | 草木が生い茂る遺跡と密林 | 急速に成長し、周囲の植物を制御する |
プラズマサーペント | 0.243241 | パチパチと音を立てるエネルギーでできた蛇のような生き物 | 雷雨と発電所 | 電流を供給し、技術を短絡させる可能性がある |
私たちの結果はすべて(当然のことながら)似たような外観をしています - 「振動する巻きひげで枝分かれしている」、「ねじれた蔓とトゲでできた体を持つ植物のような生き物」、「ヘビのような」。
ここで、外見を無視して、 habitat
とbehavior
の点で同時に類似するモンスターを探す別の検索を実行してみましょう。
weights = { "look_weight": 0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ハーモニックコーラル | 0.816497 | 振動する巻きひげを持つ、分岐した楽器のような構造 | 浅い海と潮だまり | 複雑なメロディーを創り、感情を伝え、影響を与える |
ドリームウィーバータコ | 0.357656 | オーロラのように光る触手を持つ頭足動物 | 深海の海溝と水中洞窟 | 近くの生き物の夢に影響を与える |
ミストファントム | 0.288106 | 変化する特徴を持つ、幽玄で霧のようなヒューマノイド | 沼地、荒野、霧の海岸線 | 幻想とささやきで旅人を惑わす |
ここでも、スーパーリンク アプローチは素晴らしい結果を生み出します。3 つのモンスターはすべて水辺に生息し、マインド コントロール能力を備えています。
最後に、3 つの属性すべてに異なる重み付けをして、別の検索を試してみましょう。ハーモニック コーラルと比較して、見た目が多少似ていて、生息地が大きく異なり、行動も非常に似ているモンスターを見つけるためです。
weights = { "look_weight": 0.5, "habitat_weight": -1.0, "behavior_weight": 1.0 }
id | スコア | 見て | 生息地 | 行動 |
---|---|---|---|---|
ハーモニックコーラル | 0.19245 | 振動する巻きひげを持つ、枝分かれした楽器のような構造 | 浅い海と潮だまり | 複雑なメロディーを創り、感情を伝え、影響を与える |
ルミノス | 0.149196 | 光る羽と触角を持つ蛾のような生き物 | 発光植物が生い茂る密林とジャングル | 心地よい光のパターンを発して、獲物とコミュニケーションをとり、引き寄せます。 |
ゼファーダンサー | 0.136456 | 虹色の羽を持つ優雅な鳥類 | 高い山の頂上と風が吹き渡る平原 | 仲間を引き付ける魅惑的な空中ディスプレイを作成します |
また素晴らしい結果です! 回収した他の 2 体のモンスター、Luminoth と Zephyr Dancer は、Harmonic Coral と似た行動をしますが、Harmonic Coral とは異なる生息地に生息しています。また、Harmonic Coral とは見た目も大きく異なります。(Harmonic Coral の触手と Luminoth の触角は似た特徴ですが、 look_weight
を 0.5 だけ下げただけで、2 体のモンスターの類似点はそれだけです。)
これらのモンスターの総合スコアが個々の属性ごとにどのようになっているかを見てみましょう。
id | 見て | 生息地 | 行動 | 合計 |
---|---|---|---|---|
ハーモニックコーラル | 0.19245 | -0.3849 | 0.3849 | 0.19245 |
ルミノス | 0.052457 | -0.068144 | 0.164884 | 0.149196 |
ゼファーダンサー | 0.050741 | -0.079734 | 0.165449 | 0.136456 |
habitat_weight
に負の重み付け (-1.0) をすることで、同様の生息地を持つモンスターを意図的に「押しのけ」、代わりに Harmonic Coral とは異なる環境を持つモンスターを表面に出します。これは、Luminoth と Zephyr Dancer の負の生息habitat
スコアに見られるとおりです。Luminoth と Zephyr Dancer のbehavior
スコアは比較的高く、Harmonic Coral との行動の類似性を示しています。 look
スコアは正ですが、低く、Harmonic Coral との視覚的な類似性が多少あるが極端ではないことを反映しています。
つまり、 habitat_weight
を -1.0 に、 look_weight
を 0.5 に下げ、 behavior_weight
1.0 のままにする戦略は、Harmonic Coral と主要な行動特性を共有しながらも、環境が大きく異なり、少なくとも多少見た目が異なるモンスターを表面化させるのに効果的であることが証明されています。
マルチ属性ベクトル検索は情報検索の大きな進歩であり、基本的な意味的類似性検索よりも高い精度、コンテキスト理解、柔軟性を提供します。それでも、属性ベクトルを個別に保存して検索し、結果を結合する単純なアプローチ (上記) は、複数の同時属性を持つオブジェクトを取得する必要がある場合、機能、繊細さ、効率の点で制限があります。(さらに、複数の kNN 検索は、連結されたベクトルを使用した単一の検索よりも時間がかかります。)
このようなシナリオに対処するには、すべての属性ベクトルを同じベクトル ストアに保存し、単一の検索を実行して、クエリ時に属性に重み付けする方が適切です。スーパーリンク アプローチは、高速で信頼性が高く、微妙な違いのある複数の属性ベクトルの取得を必要とするあらゆるアプリケーションにおいて、単純なアプローチよりも正確で効率的、かつスケーラブルです。ユース ケースが、e コマースや推奨システムにおける現実世界のデータ課題への取り組みであっても、モンスターとの戦いなどまったく異なるものであっても同じです。
元々はここで公開されました。