Neo4j GDSでのセマンティック検索を向上させるトピックの抽出技術
Neo4j GDSでのセマンティック検索を向上させるトピックの抽出技術
セマンティック検索は、ドキュメントに正確なキーワードが含まれていなくても、クエリの意味に一致するドキュメントを見つけるのに役立ちます。これは、特にRetrieval Augmented Generation (RAG)アプリケーションでの生成型AIに非常に有用です。
RAGアプリケーションは、セマンティック検索を使用して関連するドキュメントを見つけ、それらのドキュメントに基づいて大規模な言語モデルを使用して質問に答えます。
セマンティック検索は、テキストドキュメントの要約を数字、またはベクトルに変換して使用します。これらのベクトルは、Neo4jのようなベクトルストアに保存されます。ユーザーがクエリを作成すると、それはベクトルに変換されます。最も似ているベクトルを持つドキュメントが返されます。
長いドキュメントをベクトルの要約のためのより小さなチャンクに分解することは難しいです。チャンクが大きすぎると、一部の詳細が見逃されるかもしれません。チャンクが小さすぎると、重要な文脈が多くのチャンクに散らばる可能性があります。
この問題を解決する良い方法は、検索のためにドキュメントのトピックを使用することです。ユーザーの質問に一致するトピックを見つけ、それらのトピックを言及しているすべてのドキュメントを見つけます。
Neo4jはこれに強力なツールです。ドキュメントとそれらのトピックの知識グラフを作成できます。Neo4jはベクトル(数学的なオブジェクト)を使用してトピックとドキュメントを検索できます。Neo4j Graph Data Science (GDS)機能を使用すると、より良い検索結果のために重複するトピックを見つけて組み合わせることができます。
映画のプロットのデータベースでテストを行い、グラフベースのトピッククラスタを検索に使用すると、通常の検索よりも27%多くの関連結果を得られることがわかりました。
最近の映画のデータセット
テストでは、TMDB.orgから得た映画情報を使用し、Neo4j AuraDSグラフデータベースにロードしました。私は2023年9月1日以降に公開された映画のみを使用しました。これは、私が使用した大規模な言語モデルが映画について事前に何も知らないようにするためです。使用した映画は、大きなオスカー受賞作から学生の短編映画まで様々でした。
テストには16,156の映画ノードのデータセットを使用しました。各ノードにはタイトルとプロットの概要がありました。私は映画をDowload_TMDB_movies.ipynbノートブックのコードを使用してNeo4jにロードしました。これは私のプロジェクトリポジトリにあります。
Neo4j Bloomには、タイトルと概要のあるMovieノードが表示されます。
LLMで映画のテーマを抽出
私は映画のデータをNeo4jにロードし、LLMを使用して映画のタイトルと概要における主要なテーマを特定しました。これらは特定のオブジェクト、設定、またはアイデアに関連する可能性があります。これらは、人々が映画を検索するときに思い浮かぶ要素かもしれません。このタスクは、伝統的な名前エンティティ認識(ER)よりも少し簡単でした。なぜなら、私は見つかったテーマのタイプをカテゴライズしなかったからです。ERでは、アルゴリズムが日付、人、または組織などの特定のタイプのエンティティを見つけようとします。
こちらが私が使用した指示です:
You are a movie expert.
You are given the tile and overview of the plot of a movie.
Summarize the most memorable themes, settings, and public figures in the movie
into a list of up to eight one-to-two word phrases.
Only include the names of people if the person is a famous public figure.
Prioritize any phrases that appear in the movie's title.
You can provide fewer than eight phrases.
Return the phrases as a pipe separated list.
Return only the list without a heading.
このタスクのためにAnthropicのClaude 3 Sonnetモデルを選びました。理由は、彼らの新しいモデルをテストしたかったからです。他のLLMも良いかもしれません。
下記がインプットのサンプルです。
title: Maestro
overview: A towering and fearless love story chronicling the lifelong
relationship between Leonard Bernstein and Felicia Montealegre Cohn Bernstein.
A love letter to life and art, Maestro at its core is an emotionally epic
portrayal of family and love.
下記がLLMからの回答です。
meastro|family bonds|Emotional epic|Fearless passion|Lifelong relationship|
Towering love|Art devition
これらのLLMからの応答を、Neo4jのHAS_THEME
関係によってMovie
ノードに接続されたTheme
ノードに変換しました。
テーマ(青いノード)は、HAS_THEME関係で映画(黄色いノード)に接続します。
プロジェクトリポジトリ内のノートブックExtract themes.ipynbには、このプロセスのこのステップのコードが含まれています。
テーマの整理とテキスト埋め込みの生成
LLMはパイプで区切られたテーマのリストだけを完璧に返すわけではありませんでした。いくつかのケースでは、リストの前に「この映画の記憶に残るテーマ、設定、公的人物は:...」のような余分なテキストが付けられていました。場合によっては、LLMがテーマを見つけられなかったため、空のリストの代わりにそのことを伝える文を返しました。いくつかのケースでは、LLMは概要で説明された内容があまりにも過敏または露骨であると判断しました。これらの応答を整理するために使用したコードは、プロジェクトリポジトリのノートブックClean up themes and get embeddings.ipynbにあります。LLMの予測不能な性質のため、コードを実行する場合、テーマを整理するために少し異なる手順を取る必要があるかもしれません。
テーマを整理した後、私はOpenAIのtext-embedding-3-smallモデルを使用してテーマの埋め込みベクトルを生成しました。これらのベクトルはNeo4jの Theme
ノードのプロパティとして保存しました。また、映画のタイトルと映画の概要を連結した文字列の埋め込みも生成しました。これらの埋め込みはNeo4jの Movie
ノードのプロパティとして保存されました。
私は Theme
ベクトルと Movie
ベクトルのNeo4jベクトルインデックスを作成しました。これらのインデックスにより、私が提供するクエリベクトルに対するコサイン類似性に基づいて、最も類似した埋め込みベクトルを持つノードを効率的に見つけることができました。
Neo4jグラフデータサイエンスを使用したクラスタテーマ
LLMは言語を操作するのに驚くべき仕事をしますが、その出力を標準化するのは難しいです。私はLLMが近い同義語となるテーマをいくつか特定したことに気づきました。私はテーマのセマンティック検索をより効率的にするために、重複したテーマや非常に密接に関連したテーマを組み合わせることができることを望んでいました。テーマのクラスタリングと重複排除のすべてのコードは、プロジェクトリポジトリのノートブックCluster themes.ipynbに含まれています。
ステムを共有するテーマを見つけるための伝統的なNLPテクニックを使用する
私は伝統的な自然言語処理手法を使用して、同じ語幹に基づく単語を特定することから始めました。これは、語幹と呼ばれます。私はNLTK Python packageからのWordNet lemmatizerを使用して、共通の語幹を見つけるためにテーマから接頭辞と接尾辞を削除しました。私はグラフ内に Stem
ノードを作成し、それらを HAS_STEM
関係でテーマにリンクしました。私は最も多くの Movie
ノードに接続されている語幹グループ内の Theme
ノードからの埋め込みベクトルを Stem
ノードの埋め込みベクトルとして割り当てました。
例としてステム(緑色のノード)と関連するテーマ(青色のノード)があります。
ステムを共有しない同様のテーマを探索する
グラフの中には、ステムを共有していないにもかかわらず、非常に似た意味を持つ他のグループのテーマがありました。それらを特定し始めるために、グラフからいくつかのテーマ
ノードを選択し、その他のテーマ
ノードと最も類似したエンベッディングを見つけるためにベクトルインデックスに対してクエリを実行しました。私は、下記のコサイン類似性の妥当なカットオフを見つけようとしていましたが、これはおそらく二つのテーマの同義語ではありません。また、データセット内でテーマが持つ可能性のある近い同義語の数のカットオフを見つけようとしていました。
ここにテーマ「水中」の例があります:
「underwater」に最も類似したテーマを示す表。
私は「undersea」、「sub aquatic」、「underwater world」、「undersea world」はすべて基本的に「underwater」と同じものだと思いました。 「underwater music」テーマは、私が別のテーマとして保持したい新しいアイデアを導入し始めます。 0.880904以上の類似性のカットオフまたは4以下のトップkは、「underwater」と一緒にまとめるべきでないテーマを除外します。
ここに「fast food」のテーブルの最初の部分があります。
「fast food restaurant」と「Fast-food burger」の最初の2つのテーマは、1つの概念として十分に関連していると思いました。「street food」のテーマは、異なるグループにするに値するように思えます。0.804882以上の類似性カットオフまたは2以下のトップkを維持すれば、street foodとfast foodの違いを保つことができます。
「Africa」に最も似ているテーマは「Asia」でした。これはまったく別の大陸です。これら二つのテーマがクラスタに結合されることがないようにするには、0.829958以上の類似性カットオフが必要となります。
「Africa」に関する類似したテーマを示す表。
K Nearest Neighborsアルゴリズムを使用してIS_SIMILAR関係を作成する
いくつかの他のテーマの類似性を見てみた後、類似度のカットオフ値を0.83、トップkを2にすることにしました。Stem
ノードと関連性のないすべてのTheme
ノードを含むグラフの投影を作成しました。その後、選択した類似度のカットオフ値とトップkの閾値を超えるノード間にIS_SIMILAR
関係を追加するために、K Nearest Neighborsアルゴリズムを使用してグラフの投影を変更しました。
連結の弱いコンポーネントコミュニティをテストする
Weakly Connected Componentのコミュニティ検出アルゴリズムは、無向パスによって接続されているノードを同じコンポーネントに割り当てます。IS_SIMILAR
関係を作成したときに非常に厳格な基準を選んだ場合、WCCは類義語のクラスターを特定するための実行可能なアプローチであったかもしれません。AがBの類義語で、BがCの類義語であるという推移的な仮定を立てることができます。つまり、AはCの類義語です。
WCCを実行したところ、結果は理想的ではありませんでした。WCCの類似度の閾値を0.875に設定してみました。これは、私がKNN用に使用したカットオフ値よりも高い値です。非常に緊密に関連しているテーマだけがまとめられるようにしたかったのです。最大のWCCコミュニティには29のテーマが含まれていました。それらすべてにはChristmasに関する何かが含まれていましたが、「Christmas Terror」と「Christmas Magic」は、おそらく非常に異なる雰囲気の映画に関連するでしょう。
0.875のカットオフを持つ最大のWCCコミュニティのテーマ。
高い類似性のカットオフ、例えば0.875を設定すると、「乾燥した風景」や「荒廃した風景」のようなテーマが別々のコミュニティに分かれてしまうという問題がありました。これらは私が考えるに一緒にあるべきテーマです。そこで、WCCの代わりにLeidenコミュニティ検出アルゴリズムを試すことにしました。
Leidenコミュニティは大きなまたは小さなコミュニティに対して調整可能です
Leidenコミュニティ検出アルゴリズムは、コミュニティ内の関係がランダムに分布していると想定した場合よりも、コミュニティ内で始まりと終わりが高い割合を持つコミュニティを識別します。
ガンマというパラメータを調整することで、Leidenが生成するコミュニティの数を大きくしたり小さくしたりすることができます。私はガンマを、「コミュニティとしてラベル付けされたノード群が、関係がランダムに分布していると想定した場合よりもどれだけ相互接続されているか」を指定するものと考えています。ガンマが増加すると、クラスタ定義の基準がより厳格になります。高いガンマ値では、大規模で疎に接続されたコミュニティは基準をクリアできず、小規模で密に接続されたコミュニティが残ります。
Leidenを実行する前に、グラフプロジェクションを修正する必要がありました。Leidenは無向グラフで実行する必要がありますが、KNNは有向関係を生み出します。私はgds.graph.relationships.toUndirected()
手順を使用して、グラフプロジェクション中のIS_SIMILAR
関係をUNDIRECTED_SIMILAR
関係に変換しました。
Leidenは重み付きの関係で実行することができます。K最近傍用に選んだ類似性カットオフのため、UNDIRECTED_SIMILAR
関係上のすべての類似性スコアは0.83から1.0の間でした。私はmin-maxスケーリング式を使用して、これらの重みを0.0から1.0の範囲に変換し、近い接続を遠い接続よりも相対的に影響力があるようにしました。
私はアルファ値が1.0、2.0、4.0、8.0、16.0、32.0、64.0、128.0、256.0、512.0、および1024.0のLeidenを実行するテストを行いました。
ガンマの増加値におけるコミュニティサイズの表。
この表から、ガンマを増加させると最大のコミュニティのサイズが93ノードから9ノードへと減少することがわかります。私は、小さなコミュニティの方がテーマ間の微妙な違いを捉える可能性が高いと考えました。
私はいくつかのテーマを選び、ガンマの様々なレベルでそのコミュニティに含まれる他のテーマを見てみました。
ガンマ値が32.0の場合、Christmasに関連するすべてのテーマは何らかの形で休日と関連しています。しかし、「Christmas Terro」と「Christmas Miracle」は、私がWCCを試したときと同様にまだ一緒にいました。ガンマを128以上に上げると、他のテーマは消えましたが、「Xmas」および「Christmas」は一緒に残りました。
さまざまなガンマレベルでいくつかの異なるテーマを見ることに基づいて、私は同じライデンコミュニティ内のテーマをガンマレベル256.0で同じテーマグループに収集することを選びました。各コミュニティに対して、ThemeGroup
ノードを作成しました。コミュニティのTheme
ノードをThemeGroup
に関連付けるIN_GROUP
関係を作成しました。
失恋に関連するテーマ(青いノード)を持つ例のテーマグループ(ピンクのノード)。
テーマグループが整えられたことで、私は同じか類似のテーマを共有する映画をグラフから検索し始めることができました。以下の例では、2つの映画が「Opulent neighborhood」というテーマを共有しています。また、両方のテーマが同じテーマグループ内にあるため、「opulent home」というテーマを持つ第3の映画とも関連しています。
映画(黄色のノード)の視覚化とテーマ(青色のノード)がテーマグループ(ピンクのノード)内にあります。
Theme Groupのサマリー
一旦テーマグループが決まったら、私はOpenAIのChatGPT-3.5-turboにテーマグループの短い説明を書いてもらいました。このタスクのコードは、Summarize theme groups.ipynbノートブックにあります。私は「これらの映画はテーマ...を扱っています」というパターンに従って説明の最初の文を提供しました。スターターの文と一緒に、私はLLMにテーマを含む最大20のタイトルと概要のサンプルを提供しました。これが私が使用したプロンプトです。
You are a movie expert.
You will be given a list of information about movies and the first
sentence of a short paragraph that summarizes themes in the movies.
Write one or two additional sentences to complete the paragraph.
Do not repeat the first sentence in your answer.
Use the example movie information to guide your description of the
themes but do not include the titles of any movies in your sentences.
「locals vs visitors」、「guest」、「guests」、「visitor」、「visitors」というテーマグループを含むLLMは、次のような要約を作り出しました:
これらの映画は、「locals vs visitors」、「guest」、「guests」、「visitor」、「visitors」というテーマを扱っています。これらの映画は、地元の人々と訪問者との間のダイナミクスを探求し、異なる背景を持つ個々の人々が交流する際に生じる緊張と興奮をしばしば強調します。物語は、もてなし、関係性、そしてゲストとホスト、訪問者と地元の人々との間に形成される予想外のつながりの複雑さに深く踏み込みます。「locals vs visitors」や「guest」のテーマが物語の中に巧妙に織り込まれており、人間の経験と交流の豊かな織物を提供しています。
「scamming」、「Swindling」、「scam artist」、「swindle」というテーマグループは次のような要約を作り出しました:
これらの映画は、「Swindling」、「scam artist」、「scamming」、「swindle」というテーマを扱っています。これらの映画は、登場人物が自身の目標を達成するために詐欺や詐欺行為に訴える、欺瞞と操作の世界に深く踏み込みます。物語は、これらの行動の結果を探求し、人間の性質の複雑さと個人的な利得の追求における正と不正のぼやけた境界線を明らかにします。欺瞞を通じた自己発見の旅は、社会の暗い側面と個々が望みを叶えるためにどこまで行くかを明らかにします。
各要約は、スターターセンテンスに関連するすべてのテーマを含んでいたので、要約からキーワードが漏れることはありませんでした。LLMは、映画の例から引き出して1つまたは2つの単語のテーマを周りの文脈を提供するのに良い仕事をしたようです。
このLLMが生成したテーマグループの長い説明に加えて、私は「~についての映画」というパターンに従った短い説明を作成しました。上記のテーマグループの短い要約は「詐欺、詐欺師、詐欺行為、ねずみ講についての映画」でした。これにより、LLMによって提供される追加の文脈がより良い検索結果をもたらすかどうかをテストすることができました。
私は各テーマグループに関連した3つのベクトルプロパティを作成しました。OpenAIのtext-embedding-3-smallモデルを使用して、短いサマリーと長いサマリーの埋め込みを生成しました。3つ目の埋め込みは、テーマグループに関連するテーマキーワードの埋め込みの平均でした。
異なるベクトルインデックスに基づいてリトリーバーを比較する
この段階で、映画コンテンツに関連する5つのベクトルインデックスを作成しました:
-
タイトルと概要をカバーする
Movie
ノードのインデックス -
テーマワードをカバーする
Theme
ノードのインデックス -
テーマの長いLLM生成サマリーをカバーする
ThemeGroup
ノードのインデックス -
テーマの短いサマリ*”Movies about …”**をカバーする
ThemeGroup
ノードのインデックス -
ThemeGroup
に関連するTheme
ノードからのテーマベクトルの平均をカバーするThemeGroup
ノードのインデックス
すべてのインデックスを同じ一連の質問でテストし、どのインデックスが関連する映画を最もよく取り出すことができるかを見ました。各質問について、各インデックスが最大50本の映画を見つけることを許しました。私は、各インデックスの質問に一致する結果の映画の数を数えることによってリコールに焦点を当てました。私はその指標を選んだのは、取得の下流のプロセスが誤検出をフィルタリングし、ドキュメントをランク付けすることができるからです。これらの比較のためのコードは、Compare retrievers.ipynbノートブックにあります。
私がテストした質問は以下の通りです:
- 画家についてのドキュメンタリーは何がありますか?
- クラシック音楽についての映画は何がありますか?
- アイスホッケーについての映画は何がありますか?
- 野球についての映画は何がありますか?
- どの映画が犯人ですか?
- ダークコメディーは何がありますか?
- 1960年代のヨーロッパを舞台にしたものは何がありますか?
- 先コロンブス時代のアメリカについての映画は何がありますか?
- 鳥についての映画は何がありますか?
- 犬についての映画は何がありますか?
Neo4j知識グラフからのデータの取得は、私がクエリに似たベクトルをセマンティックに検索し、グラフの関係をたどって探していた映画を見つけるための柔軟性を与えてくれました。
映画のインデックスについては、クエリベクトルに最も近いベクトルを持つ50本の映画を取得しました。これは最も基本的な取得でした。以下は私が使用したCypherクエリです。
CALL db.index.vector.queryNodes("movie\_text\_vectors", 50, $query_vector)
YIELD node, score
RETURN $queryString AS query,
"movie" AS index,
score, node.tmdbId AS tmdbId, node.title AS title, node.overview AS overview,
node{question: $queryString, .title, .overview} AS map
ORDER BY score DESC
テーマインデックスのために、最も近いベクトルを持つ25のテーマノードを取得し、グラフ内のHAS_THEME
関連性を使用してそれらのテーマに関連するすべての映画を見つけました。クエリとのテーマの類似性によってソートされ、次にクエリとの映画の類似性によってソートされたテーマに関連するトップ50の映画を返しました。このCypherクエリを使用しました。
CALL db.index.vector.queryNodes("theme_vectors", 50, $query_vector)
YIELD node, score
MATCH (node)<-\[:HAS_THEME\]-(m)
RETURN $queryString AS query,
"theme" AS index,
collect(node.description) AS theme,
gds.similarity.cosine(m.embedding, $query_vector) AS score,
m.tmdbId AS tmdbId, m.title AS title, m.overview AS overview,
m{question: $queryString, .title, .overview} AS map
ORDER BY score DESC, gds.similarity.cosine(m.embedding, $query_vector) DESC
LIMIT 50
テーマグループに関連するインデックスについて、クエリベクトルに最も近い25のテーマグループを見つけました。IN GROUP
の関係を使用して、それらのテーマグループに関連するすべてのテーマを見つけました。次に、HAS_THEME
の関係を使用して、それらのテーマに関連するすべての映画を見つけました。クエリに対するテーマグループの類似性、そして映画の類似性によってソートされた上位50の映画を返しました。以下は、テーマグループの長いサマリーインデックスに基づいて映画を取得するために使用したCypherクエリです。短いサマリーと平均インデックスのクエリは、インデックス名を除いて同じでした。
CALL db.index.vector.queryNodes("theme\_group\_long\_summary\_vectors",
25, $query_vector) YIELD node, score
MATCH (node)<-\[:IN_GROUP\]-()<-\[:HAS_THEME\]-(m)
RETURN $queryString AS query,
"theme\_group\_long" AS index,
collect(node.descriptions) AS theme,
gds.similarity.cosine(m.embedding, $query_vector) AS score,
m.tmdbId AS tmdbId, m.title AS title, m.overview AS overview,
m{question: $queryString, .title, .overview} AS map
ORDER BY score DESC,
gds.similarity.cosine(m.embedding, $query_vector) DESC
LIMIT 50
私はChatGPT-3.5-turboに、取得した映画が質問にマッチするかどうかを判断してもらいました。また、私自身も映画を判断して、それが私の質問の意図に合っているかどうかを見ました。場合によっては、微妙な判断が求められました。野球の質問に対してソフトボールの映画はカウントすべきでしょうか?ホラー映画はどの程度ダークコメディとしてカウントされるべきでしょうか?私は一貫性を持って答えを出すように努め、映画の答えを評価した際にどのインデックスが映画を返したかを知らないようにしました。私が映画が質問にマッチするかどうかについてChatGPTと74%の時間で一致するという結果が出ました。全体として、私はChatGPTよりも取得した映画の多くを質問に関連があると分類しました。
すべての問いに関連すると私が判断した映画の総数を合計すると、テーマグループインデックスが他を明らかに上回り、映画インデックスよりも27%多くの関連映画を見つけ出しました。また、Graph Data Scienceを使用してThemeGroup
ノードを作成すると、LLMによって生成された生のTheme
ノードを使用するよりも良い結果を得られることがわかりました。
インデックスによる検索結果の全体結果。
以下のチャートは、クエリごとの結果の内訳を示しています。長いサマリーインデックスは常に良好なパフォーマンスを示し、常に基本的な映画インデックスを上回りましたが、すべての質問でトップのインデックスではありませんでした。
テーマインデックスはダークコメディを見つけるのに非常に優れていました。それは「ホラーコメディ」のテーマを独自に見つけ、ダークネスについてのテーマを優先する長いサマリーインデックスをパスしました。
短いサマリーインデックスは、クラシック音楽の質問で他のインデックスを上回りました。それは「作曲家」という言葉を含むテーマグループを他のインデックスが見逃したのを見つけました。
各インデックスに対する質問ごとの結果。
ChatGPT-3.5-turboが関連性があると考えた映画の総数を見ると、長い要約インデックスが他を上回っていましたが、映画インデックスを13%しか上回りませんでした。これは、LLMが取得したドキュメントを分析し、ユーザーに結果を返すときに何を言い換えるかを決定するRetrieval Augmented Generationアプリケーションで見ることができるより現実的な期待値かもしれません。
結論
私は、意味的検索のトピックモデリングにNeo4j Graph Data Scienceを使用する3つの主要な利点を見つけました。
- Neo4j Graph Data Scienceで作成された長いサマリーテーマグループインデックスは、他のインデックス戦略よりも多くの関連検索結果を提供しました。
- テーマグループを作成することで、非構造化テキストをデータの分析と視覚化に使用できる知識グラフに変えました。
- Neo4jでトピックモデリングを実行することは、トピックの抽出、語幹化、トピッククラスタリングの各層を通じて私の作業を整理し、表示するための素晴らしい方法を提供しました。データの取り込みとクリーニング、アルゴリズム分析、ベクトルベースの取得がすべて同じグラフプラットフォームで行われました。
Discussion