使用Neo4j GDS进行主题分析:提升语义搜索效果

2024年05月15日 由 alex 发表 36 0

语义搜索允许搜索系统检索与查询含义相匹配的文档,即使查询中的确切关键词并不存在于文档中。这种灵活的检索能力是生成式人工智能中许多检索增强生成(RAG)应用的关键部分。


RAG 应用程序使用语义搜索查找相关文档,然后要求大型语言模型根据检索到的文档回答问题。


语义搜索依赖于数字向量形式的文本文档摘要。这些向量存储在像 Neo4j 这样的向量存储库中。在查询时,用户的查询会被转换成向量,然后向量存储中具有最相似向量表示的文档会被返回。


将较长的文档分解成较小的块以便进行向量摘要有一定的技巧。如果分块过大,矢量摘要可能无法突出文档中可能与用户问题相关的次要主题。如果分块太小,对回答用户问题很重要的上下文可能会被分散到多个文本分块中。


解决这一问题的方法之一是从文档中提取主题,并将其作为语义搜索的基础。一旦在向量存储中发现了与用户问题最相似的主题,就可以检索引用这些主题的所有文档。


Neo4j 为基于主题的语义搜索提供了丰富的工具集。通过 Neo4j 图数据库平台,我们可以将文档和相关主题表示为知识图谱。Neo4j 的矢量搜索功能可对主题和文档的矢量表示进行语义搜索。利用 Neo4j 图形数据科学(GDS),我们可以使用图形算法来识别和合并重复的主题,从而提高搜索效率。


在一个包含电影情节的数据库中,我发现使用基于图的主题簇进行语义搜索比单独对基础文档进行语义搜索多出 27% 的相关文档。


最新电影数据集

我选择在从 TMDB.org 下载并加载到 Neo4j AuraDS 图数据库的电影信息上测试用于语义搜索的主题建模。我将数据集限制在 2023 年 9 月 1 日之后上映的影片。该发布日期晚于我在项目中使用的大型语言模型的训练截止日期。这样,我就能确保大型语言模型使用的是从 Neo4j 获取的数据来描述电影,而不是大型语言模型从训练数据中了解到的电影信息。数据集中的电影包括从奥斯卡获奖影片到学生制作的短片等所有影片。


结果数据集有 16,156 个电影节点。每个节点都有一个标题属性和一个包含电影情节摘要的概述属性。我用于将电影加载到 Neo4j 的代码位于项目库中的 Dowload_TMDB_movies.ipynb 笔记本中。


1


使用 LLM 提取主题

将电影数据加载到 Neo4j 后,我要求 LLM 从电影标题和概述中找出关键主题。这些主题可能是指特定的对象、场景或想法。我在寻找人们在搜索移动时可能会想到的钩子。由于我没有试图对发现的主题类型进行分类,因此这项任务比传统的命名实体识别(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 模型进行提取,因为我一直在寻找机会尝试他们最近发布的模型。我认为其他大型语言模型也能很好地完成这项任务。


下面是我发送给模型的输入示例:


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


我在 Neo4j 中通过 HAS_THEME 关系将这些来自 LLM 的回复转化为与电影节点相连的主题节点。


2


清理主题并生成文本嵌入

LLM 在只返回以管道分隔的主题列表方面做得并不完美。在某些情况下,列表前会加上一些额外的文字,如 "这部电影中令人难忘的主题、场景和公众人物是:......"。在某些情况下,LLM 找不到任何主题,它返回的是一句话,而不是一个空列表。在少数情况下,LLM 认为概述中描述的内容过于敏感或明确,因此无法进行总结。由于 LLM 的不可预测性,如果你运行该代码,可能需要采取略有不同的步骤来清理主题。


清理主题后,我使用 OpenAI 的 text-embedding-3-small 模型为主题生成嵌入向量。我将这些向量存储为 Neo4j 中主题节点的属性。我还为包含电影标题和电影概述的字符串生成了嵌入向量。这些嵌入作为属性存储在 Neo4j 的 "电影 "节点上。


我为主题向量和电影向量创建了 Neo4j 向量索引。通过这些索引,我可以根据与我提供的查询向量的余弦相似度,高效地找到具有最相似嵌入向量的节点。


使用 Neo4j 图形数据科学进行主题

LLM 在处理语言方面做得非常出色,但要让它们的输出标准化却很难。我发现 LLM 识别出的一些主题是近义词。我希望可以通过合并重复的或关系非常密切的主题来提高主题语义搜索的效率。


使用传统 NLP 技术查找共享词干的主题

我首先使用传统的自然语言处理技术来识别基于相同词根(称为词干)的单词。我使用 NLTK Python 软件包中的 WordNet 词法生成器,从主题中去除前缀和后缀,以找到共同的词根。我在图中创建了词干节点,并通过 HAS_STEM 关系将其链接到主题。我将 "电影 "节点最多的干群中主题节点的嵌入向量指定为干群节点的嵌入向量。


3


探索不共用题干的相似主题

图中还有其他组的主题,虽然没有共用词干,但含义却非常相似。为了开始识别它们,我从图中选取了一些主题节点,并对向量索引进行查询,以找到具有最相似嵌入的其他主题节点。我试图为下面的余弦相似度找到一个合理的分界线,这可能不是两个主题的同义词。我还试图为数据集中一个主题可能拥有的近义词数量找到一个临界值。


下面是主题 "水下 "的例子:


4


我认为“undersea”、“subaquatic”、“underwater world”和“undersea world”本质上是一回事。水下音乐 "主题开始引入一个新的概念,我希望将其作为一个单独的主题保留下来。如果相似度高于 0.880904 或最高 k 小于或等于 4,就可以排除不应与 "水下 "混为一谈的主题。


下面是 "underwater "的第一部分表格。


5


我认为前两个主题 fast food restaurant”, “Fast-food burger”关系密切,可以作为一个概念。而 "street food"主题似乎应该归入另一组。如果相似度的分界线高于 0.804882,或者最高 K 值小于或等于 2,那么街头小吃和快餐之间的区别就会被保留下来。


与 "非洲 "最相似的主题是 "亚洲"--一个完全不同的大陆。我需要一个高于 0.829958 的相似度截止值,以确保这两个主题不会合并在一个聚类中。


6


使用 K 最近邻算法创建 IS_SIMILAR 关系

在研究了其他几个主题的相似性后,我决定使用 0.83 的相似性截止值和 2 的最高 K 值。我创建了一个图投影,其中包括茎节点以及所有与茎无关的主题节点。然后,我运行 K Nearest Neighbors 算法,在余弦相似度高于我选择的相似度截止值和前 k 个阈值的节点之间添加 IS_SIMILAR 关系,从而改变图投影。


测试弱连接组件群落

弱连接组件群落检测算法会将通过无向路径连接的任何节点分配给同一个组件。如果我在创建 IS_SIMILAR 关系时选择了非常严格的标准,那么 WCC 可能是识别同义词聚类的可行方法。我们可以做这样一个反式假设:如果 A 是 B 的同义词,而 B 是 C 的同义词,那么 A 就是 C 的同义词。


我运行了 WCC,发现结果并不理想。我尝试将 WCC 相似性阈值设为 0.875,这比我用于 KNN 的截止值要高。我想确保只有关系非常密切的主题才会被放在一起。最大的 WCC 社区包含 29 个主题。所有这些主题都与圣诞节有关,但“Christmas terror” and Christmas magic” 可能会与氛围截然不同的电影有关。


7


设置 0.875 这样一个高相似度截断值的另一个问题是,像 "干旱景观 "和 "荒凉景观 "这样的主题--我认为它们属于同一个主题--最终被归入了不同的社区。我决定尝试Leiden 社区检测算法,而不是 WCC。


Leiden 社区可根据社区大小进行调整

Leiden 社群检测算法可以识别图中的社群,在这些社群中,开始和结束于该社群的关系的比例要高于我们对随机分布关系的预期。


我们可以调整一个名为gamma的参数,使Leiden算法产生更多或更少的社群。我认为伽马参数指定的是,如果关系是随机分布的,那么被标记为社群的节点组的相互连接程度必须高于我们的预期。随着gamma值的增加,聚类的定义标准也变得更加严格。当伽玛值较高时,连接松散的大型群落将无法通过标准,而连接密集的小型群落则会保留下来。


在运行Leiden之前,我需要修改图形投影。莱顿必须在无向图上运行,但 KNN 会产生有向关系。我使用 gds.graph.relationships.toUndirected() 过程将图投影中的 IS_SIMILAR 关系转换为 UNDIRECTED_SIMILAR 关系。


Leiden可以使用加权关系运行。由于我为 K 最近邻域选择的相似度截断点,所有 UNDIRECTED_SIMILAR 关系的相似度得分都在 0.83 和 1.0 之间。我使用最小-最大缩放公式将这些权重转换到 0.0 和 1.0 之间的范围,使较近的连接比较远的连接更有影响力。


我测试了在运行 Leiden 时,α 值为 1.0、2.0、4.0、8.0、16.0、32.0、64.0、128.0、256.0、512.0 和 1024.0。


8


从表中可以看出,增加gamma值可以将最大社区的规模从 93 个节点减少到 9 个节点。我认为较小的社区更有可能捕捉到不同主题之间的细微差别。


我选择了几个主题,并查看了在不同的伽马值水平下它们的社区中包含的其他主题。


9


在gamma值为 32.0 时,所有与圣诞节有关的主题都与节日有关。但是,"圣诞恐怖 "和 "圣诞奇迹 "仍然在一起,就像我尝试 WCC 时一样。当我把伽马值提高到 128 或更高时,其他主题都消失了,但 "Xmas "和 "christmas "仍然在一起。


在观察了不同gamma水平下的几个不同主题后,我选择将同一莱顿社区中伽玛水平为 256.0 的主题收集到同一个主题组中。对于每个社区,我创建了一个ThemeGroup节点。我创建了将社区中的节点IN_GROUP与.ThemeThemeGroup


10


有了主题组之后,我就可以开始查询图表,寻找主题相同或相似的电影。在下面的例子中,有两部电影的共同主题是 "富丽堂皇的街区"。这两部电影还与第三部主题为 "富丽堂皇的家 "的电影有关,因为这两个主题都在同一个主题组中。


11


总结主题组

确定主题组后,我要求 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.


鉴于主题组包括 "当地人与游客"、"客人"、"客人"、"游客 "和 "游客",法律硕士总结出了以下内容:


这些电影涉及 "当地人与游客"、"客人"、"客人"、"游客 "和 "游客 "等主题。这些电影探讨了当地人和游客之间的动态关系,往往突出了来自不同背景的个人在互动时产生的紧张关系和阴谋。这些叙事深入探讨了待客之道、人际关系的复杂性,以及客人与主人、游客与当地人之间可能形成的意想不到的联系。当地人与游客 "和 "客人 "的主题错综复杂地交织在故事中,展现了人类经历和互动的丰富多彩。


由 "诈骗"、"骗子"、"骗局"、"诈骗 "等主题组成的主题小组制作了本摘要:


这些电影的主题是 "诈骗"、"诈骗艺术家"、"诈骗 "和 "诈骗"。这些电影深入探讨了欺骗和操纵的世界,其中的人物通过诈骗和骗局来达到自己的目的。这些叙事探讨了这些行为的后果,揭示了人性的复杂性以及在追求个人利益过程中模糊的是非界限。人物通过欺骗进行自我发现的历程揭示了社会的阴暗面,以及个人为满足自己的欲望而不惜一切代价。


每份摘要都在起始句中包含了所有相关主题,因此摘要中没有遗漏任何关键词。LLM 似乎很好地利用了电影实例来提供单词或双词主题的上下文。


除了这个由 LLM 生成的主题组的长描述外,我还创建了一个简短的描述,其模式为 "关于<主题列表>的电影"。上述主题组的简短摘要为 "关于诈骗、诈骗艺术家、诈骗和诈骗的电影"。这样,我就可以测试 LLM 提供的额外上下文是否会带来更好的检索结果。


我创建了与每个主题组相关的三个向量属性。我使用 OpenAI 的文本嵌入-3-small 模型生成了短摘要和长摘要的嵌入。第三个嵌入是与主题组相关的主题关键词嵌入的平均值。


比较基于不同向量索引的检索器

此时,我已经创建了五个与电影内容相关的向量索引:


  • 包含标题和概述的电影节点索引
  • 涵盖主题词的主题节点索引
  • 涵盖 LLM 生成的主题长摘要的主题组节点索引
  • 涵盖主题 "关于......的电影 "简短摘要的主题组节点索引
  • 主题组节点索引,涵盖与主题组相关的主题节点的主题向量的平均值


我用同一组问题对所有五个索引进行了测试,看看哪个索引在检索相关电影方面做得最好。我允许每个索引为每个问题找到最多 50 部电影。我通过计算每个索引检索到的与问题相符的电影数量来重点考察检索率。我之所以选择这个指标,是因为检索的下游流程可以处理过滤误报和文档排序的问题。


这些是我测试过的问题:


  • 有哪些关于画家的纪录片?
  • 有哪些关于古典音乐的电影?
  • 关于冰球的电影有哪些?
  • 有哪些关于棒球的电影?
  • 有哪些 "谁干的 "电影?
  • 有哪些黑色喜剧片?
  • 有哪些以 20 世纪 60 年代的欧洲为背景的电影?
  • 有哪些关于前哥伦布时期美洲的电影?
  • 有哪些关于鸟类的电影?
  • 有哪些关于狗的电影?


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% 的相关电影。我还发现,与使用 LLM 生成的原始主题节点相比,使用 Graph Data Science 创建主题组节点能得到更好的结果。


12


下图显示了查询结果的细分。虽然长摘要索引的表现一直很好,而且总是优于基本电影索引,但它并不是每个问题的首选索引。


主题索引在查找黑色喜剧方面表现出色。它锁定了 "恐怖喜剧 "这一主题,而长摘要指数则忽略了黑暗主题。


在古典音乐问题上,短摘要指数的表现优于其他指数。它找到了主题组,包括其他索引忽略的 "作曲家 "一词。


13


如果我们看一下 ChatGPT-3.5-turbo 认为相关的电影总数,长摘要索引的表现仍然优于其他索引,但它只比电影索引高出 13%。这可能是我们在 "检索增强生成"(Retrieval Augmented Generation)应用中看到的更现实的预期,在该应用中,LLM 将分析检索到的文档,并决定在将结果传回用户时应重新措辞哪些内容。


14


结论

我看到了在语义搜索中使用 Neo4j Graph Data Science 进行主题建模的三大好处。


  1. 与其他索引策略相比,使用 Neo4j Graph Data Science 创建的长摘要主题组索引提供了更多的相关搜索结果。
  2. 创建主题组将非结构化文本转化为知识图谱,可用于数据分析和可视化。
  3. 在 Neo4j 中执行主题建模为我提供了一个很好的方法,通过主题提取、词干和主题聚类等层级来保持条理并展示我的工作。数据摄取和清理、算法分析和基于向量的检索都在同一个图形平台上进行。




文章来源:https://medium.com/neo4j/topic-extraction-with-neo4j-graph-data-science-for-better-semantic-search-c5b7f56c7715
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消