优秀的编程知识分享平台

网站首页 > 技术文章 正文

高级RAG技术研究:增强全局理解

nanyue 2025-01-16 20:22:34 技术文章 3 ℃

许多重要的现实世界任务,包括科学文献综述、法律案例简报和医学诊断,都需要跨块或文档的知识理解。许多重要的现实世界任务,包括科学文献综述、法律案例简报和医学诊断,都需要跨块或文档的知识理解。

现有的 RAG 方法无法帮助 LLM 完成需要跨块边界理解信息的任务,因为每个块都是独立编码的。

本文将介绍四种创新方法,以增强全球对文档或语料库的理解,以及从中学到的见解和想法。

这四种方法如下:

  • RAPTOR:这是一个基于树的检索系统,它以递归方式嵌入、聚类和汇总文本块。
  • Graph RAG该方法结合了知识图谱生成、社区检测、RAG和查询集中摘要(QFS),便于全面理解整个文本语料库。
  • HippoRAG:这个检索框架从人类长期记忆的海马索引理论中汲取灵感。它与 LLM、知识图谱和个性化的 PageRank 算法协作。
  • spRAG:该方法通过两种关键技术(即 AutoContext 和相关段提取 (RSE))增强了标准 RAG 系统的性能。

RAPTOR:用于树组织检索的递归抽象处理

RAPTOR是一种新颖的基于树的检索系统,设计用于递归嵌入、聚类和汇总文本段。它自下而上地构建了一棵树,提供了不同级别的总结。

在推理过程中,RAPTOR 从此树中检索信息,在不同的抽象级别合并来自较长文档的数据。

关键理念

RAPTOR 采用递归方法,根据文本块的嵌入将文本块组织成集群。它为每个集群生成摘要,自下而上构建一棵树。此过程如图 1 所示。

Figure 1: Tree construction process of RAPTOR. Source: RAPTOR.

下面我们将深入探讨与图 1 相关的特定主题:

  • 构建 RAPTOR 树
  • 检索过程

构建RAPTOR树

文本分块

将检索语料库划分为连续的块,每个块有 100 个标记。如果块超过 100 个令牌,RAPTOR 将整个句子移动到下一个块,以保持上下文和语义的连贯性。

def split_text(
    text: str, tokenizer: tiktoken.get_encoding("cl100k_base"), max_tokens: int, overlap: int = 0
):
    """
    Splits the input text into smaller chunks based on the tokenizer and maximum allowed tokens.
    
    Args:
        text (str): The text to be split.
        tokenizer (CustomTokenizer): The tokenizer to be used for splitting the text.
        max_tokens (int): The maximum allowed tokens.
        overlap (int, optional): The number of overlapping tokens between chunks. Defaults to 0.
    
    Returns:
        List[str]: A list of text chunks.
    """
    ...
    ...        
        # If adding the sentence to the current chunk exceeds the max tokens, start a new chunk
        elif current_length + token_count > max_tokens:
            chunks.append(" ".join(current_chunk))
            current_chunk = current_chunk[-overlap:] if overlap > 0 else []
            current_length = sum(n_tokens[max(0, len(current_chunk) - overlap):len(current_chunk)])
            current_chunk.append(sentence)
            current_length += token_count
    ...
    ...

嵌入

用Sentence-BERT生成这些块的密集向量表示。

这些块,以及它们相应的嵌入,构成叶节点RAPTOR树结构。

class TreeBuilder:
    """
    The TreeBuilder class is responsible for building a hierarchical text abstraction
    structure, known as a "tree," using summarization models and
    embedding models.
    """
    ...
    ...
    def build_from_text(self, text: str, use_multithreading: bool = True) -> Tree:
        """Builds a golden tree from the input text, optionally using multithreading.

        Args:
            text (str): The input text.
            use_multithreading (bool, optional): Whether to use multithreading when creating leaf nodes.
                Default: True.

        Returns:
            Tree: The golden tree structure.
        """
        chunks = split_text(text, self.tokenizer, self.max_tokens)

        logging.info("Creating Leaf Nodes")

        if use_multithreading:
            leaf_nodes = self.multithreaded_create_leaf_nodes(chunks)
        else:
            leaf_nodes = {}
            for index, text in enumerate(chunks):
                __, node = self.create_node(index, text)
                leaf_nodes[index] = node

        layer_to_nodes = {0: list(leaf_nodes.values())}

        logging.info(f"Created {len(leaf_nodes)} Leaf Embeddings")
        ...
        ...

聚类方法

聚类在构建 RAPTOR 树时至关重要,因为它将文本段落组织成连贯的组。通过将相关内容整合在一起,它增强了后续的检索过程。

RAPTOR 的聚类方法具有以下特点:

  1. 它使用高斯混合模型 (GMM) 和 UMAP 降维进行软聚类。
  2. 可以修改 UMAP 参数以识别全局和本地集群。
  3. 贝叶斯信息准则 (BIC) 用于模型选择,以确定最佳聚类数。

这种聚类方法的核心是一个节点可以属于多个聚类。这消除了对固定数量类别的需要,因为单个文本片段通常包含有关各种主题的信息,从而确保将其包含在多个摘要中。

使用 GMM 对节点进行聚类后,每个聚类中的节点将由 LLM 汇总。此过程将大块转换为所选节点的简洁、连贯的摘要。

在实现中,gpt-3.5 turbo 用于生成摘要。相应的提示如图 2 所示。

Figure 2: Prompt for Summarization. Source: RAPTOR.

构造算法

到目前为止,我们已经获得了整棵树的叶节点,并确定了聚类算法。

如图 1 中间所示,组合在一起的节点形成同级节点,而父节点包含该特定集群的摘要。生成的摘要包含树的非叶节点。

汇总的节点将重新嵌入,嵌入、聚类和汇总的过程将继续进行,直到进一步的聚类不再可行。这将生成原始文档的结构化、多层树表示形式。

对应代码如下所示。

class ClusterTreeConfig(TreeBuilderConfig):
    ...
    ...
    def construct_tree(
        self,
        current_level_nodes: Dict[int, Node],
        all_tree_nodes: Dict[int, Node],
        layer_to_nodes: Dict[int, List[Node]],
        use_multithreading: bool = False,
    ) -> Dict[int, Node]:
        ...
        ...

        for layer in range(self.num_layers):

            new_level_nodes = {}

            logging.info(f"Constructing Layer {layer}")

            node_list_current_layer = get_node_list(current_level_nodes)

            if len(node_list_current_layer) <= self.reduction_dimension + 1:
                self.num_layers = layer
                logging.info(
                    f"Stopping Layer construction: Cannot Create More Layers. Total Layers in tree: {layer}"
                )
                break

            clusters = self.clustering_algorithm.perform_clustering(
                node_list_current_layer,
                self.cluster_embedding_model,
                reduction_dimension=self.reduction_dimension,
                **self.clustering_params,
            )

            lock = Lock()

            summarization_length = self.summarization_length
            logging.info(f"Summarization Length: {summarization_length}")

            ...
            ...

检索过程

有了RAPTOR树之后,应该如何使用它进行查询?

有两种查询方式:基于树遍历和基于折叠树,如图 3 所示。

Figure 3: Illustration of the tree traversal and collapsed tree retrieval mechanisms. Nodes that were considered in the cosine similarity search are highlighted in both illustrations. Source: RAPTOR.

树遍历从树的根级别开始,并根据前 k 个节点(在本例中为 top-1)与查询向量的余弦相似度检索它们。在每个级别,它从上一层的 top-k 节点的子节点中检索 top-k 节点,对应的代码如下所示。

class TreeRetriever(BaseRetriever):
    ...
    ...
    def retrieve_information(
        self, current_nodes: List[Node], query: str, num_layers: int
    ) -> str:
        """
        Retrieves the most relevant information from the tree based on the query.

        Args:
            current_nodes (List[Node]): A List of the current nodes.
            query (str): The query text.
            num_layers (int): The number of layers to traverse.

        Returns:
            str: The context created using the most relevant nodes.
        """

        query_embedding = self.create_embedding(query)

        selected_nodes = []

        node_list = current_nodes

        for layer in range(num_layers):

            embeddings = get_embeddings(node_list, self.context_embedding_model)

            distances = distances_from_embeddings(query_embedding, embeddings)

            indices = indices_of_nearest_neighbors_from_distances(distances)

            if self.selection_mode == "threshold":
                best_indices = [
                    index for index in indices if distances[index] > self.threshold
                ]

            elif self.selection_mode == "top_k":
                best_indices = indices[: self.top_k]

            nodes_to_add = [node_list[idx] for idx in best_indices]

            selected_nodes.extend(nodes_to_add)

            if layer != num_layers - 1:

                child_nodes = []

                for index in best_indices:
                    child_nodes.extend(node_list[index].children)

                # take the unique values
                child_nodes = list(dict.fromkeys(child_nodes))
                node_list = [self.tree.all_nodes[i] for i in child_nodes]

        context = get_text(selected_nodes)
        return selected_nodes, context

相反,折叠的树将树压缩为单个层并检索节点,直到达到令牌的阈值数,同样基于余弦相似度到查询向量,对应的代码如下所示。

class TreeRetriever(BaseRetriever):
    ...
    ...
    def retrieve_information_collapse_tree(self, query: str, top_k: int, max_tokens: int) -> str:
        """
        Retrieves the most relevant information from the tree based on the query.

        Args:
            query (str): The query text.
            max_tokens (int): The maximum number of tokens.

        Returns:
            str: The context created using the most relevant nodes.
        """

        query_embedding = self.create_embedding(query)

        selected_nodes = []

        node_list = get_node_list(self.tree.all_nodes)

        embeddings = get_embeddings(node_list, self.context_embedding_model)

        distances = distances_from_embeddings(query_embedding, embeddings)

        indices = indices_of_nearest_neighbors_from_distances(distances)

        total_tokens = 0
        for idx in indices[:top_k]:

            node = node_list[idx]
            node_tokens = len(self.tokenizer.encode(node.text))

            if total_tokens + node_tokens > max_tokens:
                break

            selected_nodes.append(node)
            total_tokens += node_tokens

        context = get_text(selected_nodes)
        return selected_nodes, context

那么哪种方法更好呢?

RAPTOR 进行了比较,如图 4 所示。

Figure 4: Comparison of querying methods. Source: RAPTOR.

如图 4 所示,具有 2000 个令牌的折叠树会产生最佳结果。这是因为它提供了比树遍历更大的灵活性。具体来说,通过同时搜索所有节点,它会在给定问题的适当粒度级别检索信息。

图 5 说明了 RAPTOR 如何检索与灰姑娘故事相关的两个查询的信息:“故事的中心主题是什么?”和“灰姑娘是如何找到幸福结局的?”。

Figure 5: Querying Process of RAPTOR. Source: RAPTOR.

突出显示的节点表示 RAPTOR 的选择,而箭头指向 DPR(密集通道检索)叶节点。重要的是,RAPTOR 提供的上下文通常包括 DPR 直接检索到的信息,或者在更高层次的摘要中检索到的信息。

Graph RAG

Graph RAG采用LLM分两个阶段构建基于图形的文本索引:

  • 最初,它从源文档派生知识图谱。
  • 随后,它为所有紧密关联的实体组生成社区摘要。

给定一个查询,每个社区摘要都有助于部分响应。然后将这些部分响应汇总以形成最终的全局答案。

概述

图 6 显示了 Graph RAG 的流水线。紫色框表示索引操作,绿色框表示查询操作。

Figure 6: The pipeline of Graph RAG. Source: Graph RAG.

Graph RAG 使用特定于数据集域的 LLM 提示来检测、提取和汇总节点(如实体)、边(如关系)和协变量(如声明)。

社区检测用于将图形划分为元素组(节点、边、协变量),LLM 可以在索引和查询时汇总这些元素。

通过对与该查询关联的所有社区摘要进行最后一轮以查询为中心的摘要,可以生成特定查询的全局答案。

下面将介绍图 6 中每个步骤的实现。值得注意的是,截至 2024 年 6 月 12 日,Graph RAG 目前不是开源的,所以不能与源代码一起讨论。

第 1 步:源文档→文本块

块大小的权衡是 RAG 长期存在的问题。

如果块太长,则 LLM 调用的数量会减少。然而,由于上下文窗口的限制,完全理解和管理大量信息变得具有挑战性。这种情况可能导致召回率下降。

Figure 7: How the entity references detected in the HotPotQA dataset varies with chunk size and gleanings for the generic entity extraction prompt with gpt-4-turbo. Source: Graph RAG.

如图 7 所示,对于HotPotQA数据集中,与 2400 个令牌的块大小相比,600 个令牌的块大小提取的有效实体数量是其两倍。

步骤 2:文本块→元素实例(实体和关系)

该方法涉及通过从每个块中提取实体及其关系来构建知识图谱。这是通过LLM和提示工程的结合来实现的。

同时,Graph RAG 采用多阶段迭代过程。此过程要求 LLM 确定是否已提取所有实体,类似于二元分类问题。

第 3 步:元素实例→元素摘要 → Graph 社区→社区摘要

在上一步中,提取实体、关系和声明实际上是一种抽象汇总形式。

然而,Graph RAG 认为这还不够,需要使用 LLM 对这些“元素”进行进一步总结。

一个潜在的问题是,LLM 可能并不总是以相同的文本格式提取对同一实体的引用。这可能会导致重复的实体元素,从而在图形中生成重复的节点。

这种担忧将很快消散。

Figure 8: Graph communities were detected using Leiden algorithm on the MultiHop-RAG dataset. Circles in the graph are entity nodes, and their sizes are proportional to their degrees. The colors of the nodes represent different entity communities, shown at two levels of hierarchical clustering: (a) Level 0, which corresponds to the hierarchical partition with maximum modularity, and (b) Level 1, revealing the internal structure within these root-level communities. Source: Graph RAG.

Graph RAG 采用社区检测算法来识别图中的社区结构,将紧密链接的实体合并到同一个社区中。图 8 显示了在多跳-RAG数据集使用莱顿算法.

在这种情况下,即使 LLM 在提取过程中无法一致地识别实体的所有变体,社区检测也可以帮助建立这些变体之间的联系。一旦被分组到一个社区中,它意味着这些变体指的是相同的实体内涵,只是具有不同的表达方式或同义词。这类似于知识图谱领域的实体消歧。

Figure 9: The generation algorithm of community summaries. Source: Graph RAG.

确定社区后,我们可以为 Leiden 层次结构中的每个社区生成类似报告的摘要。这些摘要对于理解数据集的全局结构和语义具有独立的实用性。它们也可以用来毫无问题地理解语料库。

社区摘要的生成方法如图9所示。

第 4 步:社区摘要→社区答案→全局答案

我们现在已经到了最后一步:根据上一步的社区摘要生成最终答案。

由于社区结构的等级性质,不同层次的总结可以回答各种问题。

然而,这给我们带来了另一个问题:在多个级别的社区摘要可用的情况下,哪个级别可以在细节和覆盖范围之间取得平衡?

Graph RAG经过进一步的评估(Graph RAG论文中的第3节),选择最合适的抽象级别。

对于给定的社区级别,将生成任何用户查询的全局答案,如图 10 所示。

Figure 10: The process of generating the global answer for a given community level. Image by author.

HippoRAG

HippoRAG是一种新颖的检索框架,灵感来自人类长期记忆的海马索引理论。它与 LLM、知识图谱和个性化的 PageRank 算法协作。这种合作模仿了新皮层和海马体在人类记忆中的各种作用。

关键理念

图 11 说明了人脑如何相对容易地处理知识整合的困难任务。

海马记忆索引理论,一种著名的人类长期记忆理论,为这种非凡的能力提供了可能的解释。

具体来说,基于环境的、不断更新的记忆依赖于新皮层和C形海马体之间的相互作用。新皮层处理和存储实际的记忆表征,而海马体则维持海马体指数。该索引是一组相互关联的索引,指向新皮层中的记忆单元并存储它们的关联。

Figure 11: The performance of RAG, human memory, and HippoRAG on the knowledge integration task. Source: HippoRAG.

在图 11 中,我们的目标是从可能描述数千名斯坦福教授和阿尔茨海默病研究人员的大量段落中确定一位参与阿尔茨海默病研究的斯坦福教授。

  • 传统的RAG独立编码段落,除非段落同时提到这两个特征,否则很难识别Thomas教授。
  • 相比之下,熟悉这位教授的人可以很快记住他,这要归功于我们大脑的联想记忆能力,据信这是由图11中蓝色所示的C形海马指数结构驱动的。
  • 受这种机制的启发,HippoRAG 使 LLM 能够构建和利用类似的关联图来管理知识集成任务。

概述

受图 11 的启发,HippoRAG 的每个组件对应于人类长期记忆的三个组件之一,如图 12 所示。

Figure 12: Detailed HippoRAG Methodology. Source: HippoRAG.

HippoRAG模拟人类长期记忆的三个组成部分,以模拟其模式分离和完成功能。

  • 对于离线索引,LLM 将段落处理为开放的 KG 三元组。然后将这些添加到人工海马指数中,而合成海马旁区域 (PHR) 检测同义词。在上面的例子中,HippoRAG提取了涉及Thomas教授的三元组,并将它们合并到KG中。
  • 对于在线检索,LLM neocortex 从查询中提取命名实体。然后,海马旁检索编码器将它们链接到海马索引。HippoRAG 利用个性化的 PageRank 算法进行基于上下文的检索,并提取与 Thomas 教授相关的信息。

整体工艺演示

下面是一个介绍HippoRAG流水线的实际示例。

图 13 显示了问题、答案以及支持和干扰段落。

Figure 13: HippoRAG Pipeline Example (Question and Annotations). (Top) An example question and its answer. (Middle & Bottom) The supporting and distractor passages for this question. Two supporting passages are needed to solve this question. The excerpts of the distractor passages are related to the “district” mentioned in the question. Source: HippoRAG.

图 14 描述了索引阶段,包括 OpenIE 过程和知识图谱的相关子图。

Figure 14: HippoRAG Pipeline Example (Indexing). NER and OpenIE are sequentially conducted on each passage of the corpus. Thus, an open knowledge graph is formed for the entire corpus. HippoRAG only shows the relevant subgraph from the KG. Source: HippoRAG.

最后,图 15 演示了检索阶段,展示了查询命名实体识别 (NER)、查询节点检索、个性化页面排名 (PPR) 算法对节点概率的影响以及排名靠前的检索结果的计算。

Figure 15: HippoRAG Pipeline Example (Retrieval). For retrieval, the named entities in the query are extracted from the question (Top), after which the query nodes are chosen using a retrieval encoder. In this case, the name of the query named entity, “Alhandra”, is equivalent to its KG node. (Middle) HippoRAG then sets the personalized probabilities for PPR based on the retrieved query nodes. After PPR, the query node probability is distributed according to the subgraph in Figure 14, leading to some probability mass on the node “Vila France de Xira”. (Bottom) These node probabilities are then summed over the passages they appear in to obtain the passage-level ranking. The top-ranked nodes after PPR are highlighted in the top-ranked passages. Source: HippoRAG.

下面,结合源码,我们从两个方面具体讨论 HippoRAG 如何构建长期记忆以及如何检索它。

如何建立长期记忆

建立长期记忆的过程主要包括以下三个步骤。

首先,使用LLM来提取一组命名实体从使用 OpenIE 检索语料库的每个段落中,如图 16 所示。

Figure 16: Prompt for passage NER during indexing. Source: HippoRAG.

接下来,将命名实体添加到 OpenIE 提示符中提取最后的三元组,如图 17 所示。

Figure 17: Prompt for OpenIE during indexing. Source: HippoRAG.

最后,利用微调的现成密集编码器来创建知识图谱,这也将用于检索。

如何检索

首先,使用LLM来提取一组命名实体从用户查询,如图 18 所示。

Figure 18: Prompt for query NER during retrieval. Source: HippoRAG.

然后,根据检索编码器确定的相似性将这些命名实体链接到知识图谱中的节点。我们将这些选定的节点称为查询节点。

在海马体中,海马指数元素之间的神经通路使相关邻域能够被激活并召回上游。

为了模仿这种高效的图形搜索过程,HippoRAG 利用了个性化 PageRank (PPR) 算法,PageRank 的一个版本,它仅通过一组用户定义的源节点在图形中分布概率。对应代码如下所示。

    def rank_docs(self, query: str, top_k=10):
        """
        Rank documents based on the query
        @param query: the input phrase
        @param top_k: the number of documents to return
        @return: the ranked document ids and their scores
        """
        ...
        ...
        # Run Personalized PageRank (PPR) or other Graph Alg Doc Scores
        if len(query_ner_list) > 0:
            combined_vector = np.max([top_phrase_vectors], axis=0)

            if self.graph_alg == 'ppr':
                ppr_phrase_probs = self.run_pagerank_igraph_chunk([top_phrase_vectors])[0]
            elif self.graph_alg == 'none':
                ppr_phrase_probs = combined_vector
            elif self.graph_alg == 'neighbor_2':
                ppr_phrase_probs = self.get_neighbors(combined_vector, 2)
            elif self.graph_alg == 'neighbor_3':
                ppr_phrase_probs = self.get_neighbors(combined_vector, 3)
            elif self.graph_alg == 'paths':
                ppr_phrase_probs = self.get_neighbors(combined_vector, 3)
            else:
                assert False, f'Graph Algorithm {self.graph_alg} Not Implemented'

            fact_prob = self.facts_to_phrases_mat.dot(ppr_phrase_probs)
            ppr_doc_prob = self.docs_to_facts_mat.dot(fact_prob)
            ppr_doc_prob = min_max_normalize(ppr_doc_prob)
        else:
            ppr_doc_prob = np.ones(len(self.extracted_triples)) / len(self.extracted_triples)
        ...
        ...

最后,就像在上游发送海马信号时所做的那样,HippoRAG聚合输出 PPR 节点概率在先前索引的段落上,并使用它来对它们进行排名以供检索。

spRAG

spRAG 是一种用于管理复杂查询的方法。它通过两个关键技术增强了标准RAG的性能:

  1. 自动上下文
  2. 相关区段提取 (RSE)

我们重点介绍 spRAG 如何处理跨块的复杂查询。值得注意的是,目前还没有关于spRAG的论文,只有分析与代码相结合。

AutoContext:自动注入文档级上下文

在传统的 RAG 中,文档通常被划分为固定长度的块进行嵌入。这种简单的方法通常会忽略文档级上下文信息,从而导致上下文嵌入的准确性和完整性降低。

为了解决这个问题,开发了 AutoContext。它的关键思想是在嵌入之前自动将文档级上下文信息合并到每个块中。

具体说来它创建文档摘要1-2 个句子,并将其与文件名一起添加到每个块的开头。因此,每个块不再是孤立的,而是携带整个文档的上下文信息。获取文档摘要的代码如下所示。

def get_document_context(auto_context_model: LLM, text: str, document_title: str, auto_context_guidance: str = ""):
    # truncate the content if it's too long
    max_content_tokens = 6000 # if this number changes, also update the truncation message above
    text, num_tokens = truncate_content(text, max_content_tokens)
    if num_tokens < max_content_tokens:
        truncation_message = ""
    else:
        truncation_message = TRUNCATION_MESSAGE
    
    # get document context
    prompt = PROMPT.format(auto_context_guidance=auto_context_guidance, document=text, document_title=document_title, truncation_message=truncation_message)
    chat_messages = [{"role": "user", "content": prompt}]
    document_context = auto_context_model.make_llm_call(chat_messages)
    return document_context

相关区段提取:相关文本块的智能组合

RSE 是一个后处理步骤。其目标是智能地识别和组合可以提供最相关信息的块,从而形成更长的段。

具体而言,RSE 首先对检索到的内容相似或语义相关的块进行分组。然后,根据查询要求,它会智能地选择并组合这些块以形成最佳区段。对应代码如下所示。

def get_best_segments(all_relevance_values: list[list], document_splits: list[int], max_length: int, overall_max_length: int, minimum_value: float) -> list[tuple]:
    """
    This function takes the chunk relevance values and then runs an optimization algorithm to find the best segments.

    - all_relevance_values: a list of lists of relevance values for each chunk of a meta-document, with each outer list representing a query
    - document_splits: a list of indices that represent the start of each document - best segments will not overlap with these

    Returns
    - best_segments: a list of tuples (start, end) that represent the indices of the best segments (the end index is non-inclusive) in the meta-document
    """
    best_segments = []
    total_length = 0
    rv_index = 0
    bad_rv_indices = []
    while total_length < overall_max_length:
        # cycle through the queries
        if rv_index >= len(all_relevance_values):
            rv_index = 0
        # if none of the queries have any more valid segments, we're done
        if len(bad_rv_indices) >= len(all_relevance_values):
            break        
        # check if we've already determined that there are no more valid segments for this query - if so, skip it
        if rv_index in bad_rv_indices:
            rv_index += 1
            continue
        
        # find the best remaining segment for this query
        relevance_values = all_relevance_values[rv_index] # get the relevance values for this query
        best_segment = None
        best_value = -1000
        for start in range(len(relevance_values)):
            # skip over negative value starting points
            if relevance_values[start] < 0:
                continue
            for end in range(start+1, min(start+max_length+1, len(relevance_values)+1)):
                # skip over negative value ending points
                if relevance_values[end-1] < 0:
                    continue
                # check if this segment overlaps with any of the best segments
                if any(start < seg_end and end > seg_start for seg_start, seg_end in best_segments):
                    continue
                # check if this segment overlaps with any of the document splits
                if any(start < split and end > split for split in document_splits):
                    continue
                # check if this segment would push us over the overall max length
                if total_length + end - start > overall_max_length:
                    continue
                segment_value = sum(relevance_values[start:end]) # define segment value as the sum of the relevance values of its chunks
                if segment_value > best_value:
                    best_value = segment_value
                    best_segment = (start, end)
        
        # if we didn't find a valid segment, mark this query as done
        if best_segment is None or best_value < minimum_value:
            bad_rv_indices.append(rv_index)
            rv_index += 1
            continue

        # otherwise, add the segment to the list of best segments
        best_segments.append(best_segment)
        total_length += best_segment[1] - best_segment[0]
        rv_index += 1
    
    return best_segments

见解与思考

算法和数据结构的比较

RAPTOR通过聚类构建树状数据结构,并基于该结构进行检索。

尽管 Graph RAG 和 HippoRAG 都使用知识图谱,但它们有一些区别:

  • 在数据结构方面,Graph RAG通过总结知识元素来整合信息。因此,每当添加新数据时,都需要重复汇总过程。RAPTOR 也是如此。相反,HippoRAG 只需在其知识图谱中添加边缘即可无缝集成新知识。
  • 在检索算法的上下文中,Graph RAG 依赖于社区检测,而 HippoRAG 使用个性化 PageRank (PPR) 算法。

与其他 spRAG 不同,spRAG 不使用高级数据结构。它只是将文档摘要和文件名添加到每个块,然后根据相关性值执行检索。这也表明 spRAG 的索引和查询速度应该是最快的。

关于性能

HippoRAG 以 RAPTOR 为基线进行了实验,展示了一些超过 RAPTOR 的结果,如图 19 所示。

Figure 19: Single-step retrieval performance. HippoRAG outperforms all baselines on MuSiQue and 2WikiMultiHopQA and achieves comparable performance on the less challenging HotpotQA dataset. Source: HippoRAG.

在Graph RAG 论文,不包括性能比较实验。

此外,目前没有关于spRAG的论文。

关于增强型产品系列

这四种方法——RAPTOR、Graph RAG、HippoRAG 和 spRAG——旨在增强对整个语料库的理解。

他们各自基于整个语料库构建数据结构。

关于可定制性

在这种情况下,HippoRAG 的表现更胜一筹,因为它的所有组件都是现成的,无需额外的训练,如图 20 所示。

Figure 20: Dissecting HippoRAG. To understand what makes it work well, substitute the OpenIE module and PPR with plausible alternatives and ablate node specificity and synonymy-based edges. Source: HippoRAG.

因此,通过微调特定组件,有很大的改进潜力。

结论

本文介绍了四种新方法,以增强对文档或语料库的传统 RAG 的全局理解,并辅以代码说明。它还包括我的见解和想法。

参考:
https://ai.gopubby.com/advanced-rag-12-enhancing-global-understanding-b13dc9a8db39

最近发表
标签列表