
小白也能读懂的GraphRAG知识图谱全流程解析,多图预警! 原创
今天,我要和你分享的是如何用GraphRAG从一个普通的txt文件中创建知识图谱,准备好了吗?那就让我们开始吧!
GraphRAG解决了什么问题
当你问:“这个数据集的主题是什么?”这类高级别、概括性的问题时,传统的RAG可能就会束手无策。为什么呢?那是因为这本质上是一个聚焦于查询的总结性任务(Query-Focused Summarization,QFS),而不是一个明确的检索任务。
我知道你现在可能在想,“那我们该如何解决这个问题呢?”好消息是,有人已经找到了解决方案,而且还被详细地描述在论文中:
简单来说,就是利用社区检测算法(如Leiden算法)将整个知识图谱划分模块化的社区(包含相关性较高的节点),然后大模型自下而上对社区进行摘要,最终再采取map-reduce方式实现QFS: 每个社区先并行执行Query,最终汇总成全局性的完整答案.
与其他RAG系统类似,GraphRAG整个Pipeline也可划分为索引(Indexing)与查询(Query)两个阶段。索引过程利用LLM提取出节点(如实体)、边(如关系)和协变量(如 claim),然后利用社区检测技术对整个知识图谱进行划分,再利用LLM进一步总结。
鉴于篇幅原因,今天的这篇文章主要聚焦于indexing, 下一篇文章会介绍Query的工作原理,敬请期待!
pipeline
当你运行 "poetry run poe index" 命令时,它会执行 graphrag.index.cli 目录下的 index_cli 入口函数。在 GraphRAG 中,构建知识图谱被视为一个流水线(pipeline)过程,这个流水线包含多个工作流(workflow),例如文本分块、使用LLM来识别实体等。pipeline涵盖的workflow是通过 settings.yml 配置文件进行指定的。index_cli 的主要任务是创建 pipeline_config 对象,并利用 run_pipeline_with_config 函数来运行流水线。所以,我们可以将整个流程概括如下:
整个过程体现了自上而下的编程思想——每个结果依赖于更底层函数的执行,从顶部开始调用,然后逐步深入到底层函数。这样的结构使得整体流程清晰明了,这也是我们平时在项目开发中的编程思路。
workflow
讨论workflow之前,先简单了解下项目使用的另一个框架: DataShaper 是微软开源的一款用于执行工作流处理的库,内置了很多组件(专业名词叫做Verb). 通过定义一个数据处理的工作流,你可以对输入的数据(比如Pandas的DataFrame)定义一系列数据操作的动作(DataShaper中称作Verb)、参数与步骤,执行这个工作流即可完成数据处理过程。在DataShaper中提供了很多开箱即用的Verb,你也可以自定义Verb。多个子工作流也可以组合定义成一个更大的工作流。
当你通过命令行执行完indexing之后,你会看到如下的输出内容:
从这个可以看出GraphRAG的indexing共经历了14个workflow:
- create_base_documents
- create_final_documents
- create_base_text_units
- join_text_units_to_entity_ids
- join_text_units_to_relationship_ids
- create_final_text_units
- create_base_extracted_entities
- create_summarized_entities
- create_base_entity_graph
- create_final_entities
- create_final_relationships
- create_final_nodes
- create_final_communities
- create_final_community_reports
基本的处理过程如下:首先,它会将输入文本进行拆分,然后提取实体与关系,生成摘要信息,并根据这些信息构建内存中的图(Graph)结构。接下来,它会从这个图中识别出各个社区,为每个社区创建报告,并在图中创建文本块节点和文档节点。
当然,以上只是些核心步骤,但在实际的处理过程中还涉及到许多细节处理,比如生成嵌入(embedding),持久化到存储,以及应用不同的算法策略等等。当然这些并不是这篇文章的重点。如果感兴趣,评论区留言我会单独开别的文章来讲解。
这里可以多说一点,这14个workflow其实又可以进一步细分为四大类:
1.关于文档的document_workflows
- create_base_document
- craete_final_documents
2.关于文档单元的text_unit_workflows
- create_base_text_units
- join_text_units_to_entity_ids
- join_text_units_to_relationship_ids
- create_final_text_units
3.构建图谱的graph_workflows
- create_base_extracted_entities
- create_summarized_entities
- create_base_entity_graph
- create_final_entities
- create_final_relationships
- create_final_nodes
4.社区聚类的community_workflows
- create_final_communities
- create_final_community_reports
此外,各个工作流之间存在一定的依赖关系,形成了一个工作流流程图。输入数据为存放在 input 目录下的 txt 或 csv 文件(目前只支持这两种,后面我会自己支持更多格式),经过这些工作流组成的流程图处理后,输出的结果就是最终构建的知识图谱。
接下来,我将以一个包含 "海贼王" 的 txt 文件为例(摘自百度百科),逐步解析它经历的各个工作流,以及每个工作流的输入和输出是什么.
1. create_base_text_units
整个pipeline的入口输入在源码中是个叫dataset的变量,其存储的值Pandas DataFrame,Pandas DataFrame可以简单看做是一张table, 这个table的每一行代表一个txt文件,text列是txt文件的内容:
create_base_text_units 是整个pipeline的第一个workflow, 它的作用是对txt的文件内容按照特定的策略进行切分(chunking)操作,目前只支持两种策略: 按照token和按照sentence,默认是按照token, chunk操作的输入是text:
对于一个text 经过chunking操作后会得到多个chunks:
Microsoft GraphRAG在索引构建的过程中其中间数据主要使用Pandas DataFrame这种结构化类型进行交换, 可以简单理解为Mysql中的table,对DataFrame的一些操作比如select、join等可以类比mysql的select, join等sql语句来理解。
2. create_base_extracted_entities
一旦我们得到了相应的chunk,GraphRAG就会采用特定的策略从每个chunk来提取需要的实体entity。
目前,GraphRAG支持两种抽取策略:
- graph_intelligence:这是默认的策略。
- nltk:另一种可选策略。
在源码中的ExtractEntityStrategyType里,尽管定义了一个名为“graph_intelligence_json”的枚举值,但是目前还未对它进行支持。
当处理多个数据块时,GraphRAG会并行调用LLM来抽取实体,而且默认情况下,它会选择使用多线程。不过,如果你想的话,也可以通过配置修改成asyncio模式。
在此流程中,GraphRAG会调用run_extract_entities进行实体抽取,该函数会利用目录下的entity_extraction.txt中的prompt来调用LLM完成实体提取。默认的 entity_extraction prompt 抽取的实体类型是 ['organization', 'person', 'geo', 'event'],你可以根据你的文件内容来修改settings.yml中entity_extraction,后面我会介绍如何通过prompt tuning来自动适配prompt.
我截取了其中一个chunk得到的LLM调用结果的部分内容:
GraphRAG会对LLM的输出结果进行后处理post_processing,最终形成Graph对象的。我们先看一下实体(entities),每一个实体都有四个主要的属性:name、description、source_id 和 type。
- Name:这是实体的名称。
- Description:对实体的描述。
- Source_id:在此情况下,source_id是指那些生成这个特定实体的数据块(chunk)的识别号。
- Type:实体的类型。
每个chunk都生成对应的实体后,会把这些实体添加到一个列表entities中,并把每段chunk对应的表达图形结构的Graphml也放到一个列表entity_graph中:
Microsoft GraphRAG在索引构建的过程中对于Graph数据的交换使用Graphml(一种xml表示的graph)
这里有个情况需要考虑,不同的数据块(chunks)可能会抽取出相同的实体。比如说,第一个和第二个数据块都可能包含"草帽路飞"这个实体。这时候,GraphRAG会采用一种名为merge_graphs的操作,把多个子图合并成一个新的大图。如果遇到相同的节点,那么GraphRAG就会执行concat操作,也就是将对应的属性和关系进行合并。
比如对于一个实体:'哥尔·D·罗杰', 经过merge之后会包含多个description的列表: ['哥尔·D·罗杰是罗杰海贼团的船长', '哥尔·D·罗杰是被称为“海贼王”的男人,他在被行刑受死之前说了一句话,开启了“大海贼时代”']
通过merge_graphs操作,GraphRAG能够有效地处理重复的实体,并把多个chunk对应的Graph整合成一个新的Graph,形成一个更加完善和详细的数据图:
3. create_summarized_entities
通过merge_graphs的操作,将多个子图合并到一个全新的大图之后,GraphRAG会进一步这个大图的节点(node)和关系(relationship)的描述(descriptions)进行总结。
这样做的目的是为了方便查询,因为查询时需要根据问题匹配知识库中的实体信息和关系信息时,只需要根据总结后的实体描述和关系描述就可以进行匹配了. 不然得遍历description list进行匹配。
GraphRAG目前支持的summarize的策略只有一种:graph_intelligence。
summarize使用的prompt中文翻译如下:
执行summarize_descriptions操作后,原来图形中的多个description就被整合为了一个全新的、详尽的描述。可以说,summarize_descriptions是把前一步得到的Graph进行整理的过程,使得Graph更加清晰、准确。
经过summarize之后,上一个workflow create_base_extracted_entities 得到的Graph被更进一步完善了:
生成这种摘要的好处是:可以借助嵌入embedding向量更有效与准确的对这些实体与关系进行检索。
4. create_base_entity_graph
这一步是做社群检查的: 将实体进行分类,拿三国举例,比如周瑜和孙策属于吴国,曹操和司马懿属于魏国,刘备和关羽属于蜀国,而吴国、魏国、蜀国都属于东汉,其中东汉是一个大社群,魏蜀吴是三个小社群,当执行查询时,可以指定社区的级别,如果指定的是低级别社群,那么查找的结果就比较微观,比如问三国时期有哪些著名人物,如果指定的社群为吴国,那么匹配的就只有周瑜和孙策,如果指定的社群为东汉,那么就能找到更多的著名人物。
create_base_entity_graph这个workflow会对Graph应用应用层次聚类算法(对应源码中的cluster_graph方法), 在Graph中识别出层次结构和社区结构:一个Level对应多个community。
GraphRAG在源码中借助了Graspologic库实现的Leiden算法: Leiden算法通常比许多其他的社区检测算法更稳定,能更可靠地复现结果, 但是Leiden算法在某些情况下可能会比其他方法慢一些。
在这个workflow中先会进行run_layout 布局分析,应用Leiden算法对nodes分社区,完成这些步骤后,每一个社区的节点都会被赋予以下属性:
- Level:表示节点所在的层次。
- Cluster:表示节点所在社区的编号。
- Human_readable_id:这可以被看做是实体(entity)在同一个社区内的编码,从0开始
经过create_base_entity_graph之后,Graph按照层级被划分出多个子图,每个子图对应一个level:
5. create_final_entities
create_final_entities这个workflow的功能是对节点做embedding,方便进行之后的query。
在做embedding之前,为了更好地表示每个节点,我们将节点的'name'和'description'字段拼接起来,形成一个新的'name_description'字段。这样,每个节点都将有一个通俗易懂,并且信息丰富的标签。
然后,我们把这个新生成的'name_description'字段通过嵌入过程转换成一个向量表示。这种方法能够捕获和表示文本数据的复杂模式,也使得我们可以针对这些节点进行高效的计算和分析。
经过上面的一些图的修整之后,我们还需要对entity做进一步的embedding操作。在这之前,会经过embedding操作,embedding会对node的 name和description 拼接的 name: description 组成name_description字段,对这个字段做embedding操作。
经过embedding之后,新增了一列description_embedding字段:
image-20240809153319632
6. create_final_nodes
Network Visualization 阶段,由于生成的图谱一般不是一个平面图(可以通过在平面上绘制其顶点和边而不出现边的交叉),通过使用降维技术操作将非平面图映射到平面上,可以更直观地观察和理解数据的结构和模式。
在图论和网络分析中,图的布局算法(layout algorithm)用于将图中的节点和边在二维或三维空间中进行合理的排列和可视化。其主要目标是使图的结构和关系尽可能清晰地展示出来,以便于人类理解和分析。create_final_nodes会对Graph应用layout算法,GraphRAG目前支持两种算法:
- umap:默认值
- zero
workflow的输入是create_base_entity_graph的输出:
每个entity的所有属性现在长这样:
7. create_final_communities
这个workflow用于创建community table, 步骤如下:
我们从第4步生成的create_base_entity_graph中抽取节点数据,形成一个名为graph_nodes的表:
然后,我们同样从create_base_entity_graph中提取边信息,生成另一个名为graph_edges的表:
然后,我们将graph_nodes和graph_edges进行left_join操作,这个新生成的表命名为combined_clusters:
紧接着,我们对combined_clusters进行进一步的聚合操作,同样是按照cluster和level进行分组。在这个过程中,我们会把edge的id_2去重后组合成一个数组,命名为relationship_ids;同时,也会把node的source_id_1去重后组合成另一个数组,命名为text_unit_ids:
以cluster = 1为例,通过text_unit_ids,我们能够知道这个社区来源于哪些chunk;通过relationship_ids,我们则可以确定这个社区包含了哪些边。
最后,我们还会对上述数据进行一次处理,主要是生成每个社区的名称:
image-20240810214443777
8. join_text_units_to_entity_ids
join_text_units_to_entity_ids的作用是建立text_unit到entity的映射关系。
首先,GraphRAG会提取出每个实体的"id",以及表示实体来源的字段"text_unit_ids"。接着,我们对"text_unit_ids"进行“打平”操作,也就是将嵌套的数据结构转化为一维的形式。
然后,我们进行聚合操作。具体来说,我们会按照"text_unit_id"对数据进行分类,并把相同类别的实体id聚合成一个数组,命名为"entity_ids"。
这样,在最终的结果中,每一条记录都会包含一个"text_unit_id",以及一个与之关联的"entity_ids"数组:
image-20240810091048672
9. create_final_relationships
create_final_relationships用于创建relationship table, 步骤如下:
首先,我们从create_base_entity_graph中得到的Graph提取出所有的边关系:
接下来,我们会对edge和nodes使用left_join操作,在这个步骤中,我们将新增两列:source_degree和target_degree。这两列分别表示源实体和目标实体的度数,也就是每个实体连接的边的数量。
最后,我们会创建一个新的列"rank"。这个列的值是通过将source_degree和target_degree相加得到的。这样,我们就可以根据rank的值,了解每条边连接的两个实体的总度数:
source | target | weight | description | text_unit_ids | id | human_readable_id | source_degree | target_degree | rank |
蒙奇·D·路飞 | 草帽一伙 | 1.0 | 蒙奇·D·路飞是草帽一伙的船长和创立者 | ['2808e991f29115cba505836944beb514'] | 392be891f8b649fabdc20e7bf549f669 | 0 | 11 | 19 | 30 |
蒙奇·D·路飞 | 香克斯 | 1.0 | 蒙奇·D·路飞为了实现与香克斯的约定而出海 | ['2808e991f29115cba505836944beb514'] | 0111777c4e9e4260ab2e5ddea7cbcf58 | 1 | 11 | 2 | 13 |
蒙奇·D·路飞 | ONE PIECE | 1.0 | 蒙奇·D·路飞为了寻找传说中的大秘宝ONE PIECE而扬帆起航 | ['2808e991f29115cba505836944beb514'] | 785f7f32471c439e89601ab81c828d1d | 2 | 11 | 1 | 12 |
10. join_text_units_to_relationship_ids
这个workflow和我们之前讨论过的join_text_units_to_entity_ids非常相似,主要区别在于,现在我们是将text_unit_id映射到它所包含的relationship_id,而不再是entity_ids。
简单来说,我们的目标是理解每个text_unit_id(对应"chunk")都包含了哪些关系(relationship)。为了实现这个目标,我们会创建一种映射关系,把每个text_unit_id连接到它所涉及的所有relationship_id。结果将以类似于字典的形式呈现,其中键是text_unit_id,值是一个列表,包含了所有相关的relationship_id:
image-20240810094149840
11. create_final_community_reports
这个workflow用于生成社区摘要:借助LLM生成每个社区的摘要信息,用来了解数据集的全局主题结构和语义。这也是Microsoft GraphRAG的核心价值所在,也是回答QFS问题的关键。具体步骤如下:
首先借助create_final_nodes
的输出,并添加了一个node_details
列以存储更多关于节点的信息:
然后对这些nodes使用community_hierarchy
来构建社区的层次结构,通过对(community, level) 的分组,将同一组内的节点title聚合成数组:
从上图我们可以看到每个community包含了哪些entity。
接下来,GraphRAG开始分析父子社区的构造情况,如果上一级的社区包含了全部下一级社区的成员,那么它们之间就构成了父子社区的关系,我们发现社区1是个大社区,包含了12、13、14三个子社区,但是它们都属于同一个level:
紧接着GraphRAG会基于三个table: node_df、edge_df、claim_df 做聚合操作,生成每个社区的context_string: 包含社区的所有节点和relationships信息:
image-20240810122232624
为了方便你看到context_string的内容,我摘取了某个社区的context_string内容:
接着LLM会使用community_report.txt中的prompt并把context_string作为输入,对社区按照level进行自下而上的总结,使用的默认prompt中文翻译如下:
我们看下某个社区的报告内容:
这份报告包含了社区的总体title、summary和发现等等,这个过程也是最耗费token的。
12. create_final_text_units
这个workflow很简单,就是把对应的chunk和这个chunk有的document_ids, entity_ids, relationship_ids 做关联,成一张表
- id: 表示每条记录的唯一标识符。
- text: 包含文本内容的列。
- n_tokens: 表示文本内容中包含的标记(token)的数量。
- document_ids: 包含一个或多个文档标识符的列,表示该记录与哪些文档相关联。
- entity_ids: 包含一个或多个实体标识符的列,表示该记录中提到的实体。
- relationship_ids: 包含一个或多个关系标识符的列,表示该记录中涉及到的关系。
image-20240810150028338
13. create_base_documents
这个流程也很简单,主要是建立document和text_unit的对应关系表
image-20240810150221714
14. create_final_documents
这个流程完成的工作基本和create_base_documents一致,只是把text_units列名换成了text_unit_ids而已
image-20240810223341019
总结
当GraphRAG完成索引过程后,它默认会将构建知识图谱所需的所有数据持久化。这些数据被存储在输出目录中,并采用Parquet文件格式。Parquet是一种列式压缩存储格式,专为高效的数据存储和分析而设计。你可以将其视为DataFrame的一种持久化方式。
在查询阶段,这些Parquet文件会被加载到内存和向量数据库中。这样做的好处在于,我们可以直接从内存和数据库中检索信息,而无需再次从原始数据源抽取和处理数据。这大大提高了查询的效率和速度。
由于parquet是一种底层文件格式,我们无法用来直观的了解与观察上面构建的知识图谱索引的细节,有什么办法可以做更直观的可视化、分析与检索呢?
由于parquet文件可以很简单的通过pandas库读取成DataFrame表,所以在了解其结构后,就可以通过Cypher语句导入成Neo4j图数据库中的节点与关系。在Github上已经有人完成这样的工作:https://github.com/tomasonjo/blogs/blob/master/msft_graphrag/ms_graphrag_import.ipynb。你如果嫌麻烦,也可以把parquet转成csv格式进行查看,代码也非常简单,不到20行左右,感兴趣的可以评论区留言。下图是抽取的Entity的Neo4j展示:
image-20240811200959883
基于GraphRAG生成的数据导入到Neo4j之后,我们完全可以不再依赖于GraphRAG项目自带的Query功能,可以结合自己的项目需求在自己的Neo4j图数据库上定义自己的RAG应用检索与生成器,从而带来极大的灵活性。
本文转载自公众号AI 博物院 作者:longyunfeigu
