动机和背景
和往常一样,路途就是目的地。所以在进入代码部分之前,我先详细介绍了我写这篇文章的原因。如果你只对结果和代码感兴趣,请随意跳到“代码/执行细节”(以及 GitHub 上的笔记本)。但你会错过重要的背景信息……这完美地过渡到图表主题:
为何使用图表? 我之前已经使用过图表,目的是更灵活地可视化纯粹的层次结构之外的组织现实。
在玩了 RAG(检索增强生成)之后,这种兴趣又重新燃起。在纯统计词嵌入不够好且有利于让 LLM 产生幻觉的用例中,以图表为基础的 RAG 变得很有趣。
图表通过提供更好的上下文为 RAG 提供了更安全的基础,因为它们封装了文本中包含的语义,并且是通过 (a) 语义关系的潜在可视化和 (b) 重新用作图表嵌入的输入(与纯词嵌入相比)来促进理解的好方法。
但是,完整的 RAG 是第二步。本文仅限于从非结构化文本创建图。嵌入图并将其用于语义驱动的 RAG 超出了范围,留待以后的文章讨论。
为什么要获得本地LLM? 我从未在本地开启LLM,但我清楚地看到了人们的兴趣和需求:
隐私和保密性,例如,在处理不能传输给不受控制的第三方的公司信息时。此代码中的示例不需要这种级别的保密性。但它可以作为概念证明,展示一种解决方案,该解决方案也适用于从机密的公司文件中提取知识,因为这些信息不会离开您的计算机。
使用和支持开源模型来促进信息专制,并摆脱“赢家通吃”公司的桎梏。如果你想知道为什么这很重要,我建议你阅读 Daniel Jeffries 的这篇文章(并最终订阅他的 substack 频道)。
硬件和软件的融合发展,使得在(相对)实惠的家庭解决方案上运行高质量的 LLM 成为可能。
具体选择是将 Ollama、Mixtral 8x7B Mixture-of-Expert 模型与 Macbook M1 Pro 结合使用。Ollama 承诺提供一种简单的方法,让 LLM 以非常简单的方式在本地运行。我并没有失望——它真的非常简单。
我听说 Mixtral 的性能非常好,与 chatGPT 3.5 相当。计算和来自互联网的反馈表明该模型必须能够在具有 64 GB RAM 的 Mac 上运行。M3 处理器推出后,二手 M1 硬件的价格下降,让我可以做出这个选择,只需“牺牲” 25% — — 与新的 M3 基准相比,性能降低了 30%,价格不到 50% — — 并且性能(我后来发现)仍然足以满足我自己的使用情况,即使这些任务的计算非常密集。
顺便说一句,任务的计算强度也会使任何第三方 API 的使用成本大幅上升——所以即使我还没有算过账,我也认为对自己硬件的投资最终也是划算的。当然,这是基于你长期实际使用它的假设。但图形提取并不是唯一的用例。我还看到了本地代理为日常任务提供支持的广阔空间。具体来说,我已经尝试过“职业教练代理”——只是当时的代码仍然依赖于 OpenAI API。
后续步骤: 如上所述,知识提取和本地 LLM 的使用都适合进行超出本文范围的更多实验(......但最终可能会有关于这些可能性的更多文章)。
用例详情:德国播客记录的历史 作为第一个用例,我的目标是从我目前最喜欢的播客“德国人的历史”中的非结构化文本中提取知识图谱,该播客由 Dirk Hoffmann-Becking 主持:对于任何历史爱好者来说,这都是一个真诚而真实的推荐。
我已经从优秀的关联网站上抓取了播客记录,每个时期都有一个大型文本语料库(例如“奥托人”、“萨利安人”、“霍恩斯陶芬”等)。但是,出于下面解释的原因,此示例仅适用于单个剧集的记录。
历史文本很好地展示了“仅嵌入” RAG 的缺点,激发了人们对更具语义驱动的基于知识图谱的 RAG 来查询文本的兴趣(参见:上面的“下一步”)。
证明这一点的方法是:我已经基于转录语料库创建了一个 GPT。但之后查询文本时,结果却参差不齐。
年表和关系显然是历史文本中非常重要的概念——但词嵌入不能很好地捕捉这些概念。
一个很好的例子:虽然现在听起来很奇怪,但在本书所涵盖的时期,通过教皇对皇帝进行逐出教会是一种强大的政治工具,经常被使用(……任何有自尊心的皇帝都不会容忍至少一次没有被逐出教会……)。但当然,教皇 P1 是否逐出皇帝 E1 或皇帝 E2 很重要。特别是如果皇帝 E2 恰好是皇帝 E1 的曾孙,而教皇 P1 在皇帝 E2 开始统治的几十年前就已经步入花甲之年。
词向量确实能很好地捕捉“教皇逐出皇帝”的关系……但它们很快就开始产生相应的名称(例如:如果教皇 P1 逐出皇帝 E1,为什么他不应该逐出皇帝 E2?)。正是因为词向量无法明确捕捉它们嵌入的单词之间的时间顺序或关系方面。
建立这种联系的字面意思是建立一张图。在知识图谱表示中,只有从教皇 P1 到皇帝 E1 的“边”,而没有到 E2 的“边”(因为他们相隔一生,从而防止任何共现)。
这就是我想要测试基于知识图谱的 RAG 的原因
作为第一步,这意味着能够建立这个图表(并将其可视化,因为可视化是一种非常好的理解方式)
在 GitHub 上的具体示例中,代码使用了第 96 集“萨克森和向东扩张:遇见邻居”的文字记录。
顺便说一句,您的“无用知识百科全书”中还添加了一个很棒的琐事:在这一集中,撒克逊人遇到了蓝牙国王哈拉尔 (Harald Bluetooth),他是当今蓝牙技术的名字来源(如果我没记错的话,之所以选择这个名字,是因为它是诺基亚和爱立信的联合创新……而蓝牙国王哈拉尔恰好是第一个成功统一瑞典人和挪威人(或者至少是他们的前辈)的人:-)
黑客马拉松 这就是我的初衷……缺少的是机会。它以杜塞尔多夫 Python 用户组 PyDDF 的 Python Hackathon 的形式出现,由 Marc-André Lemburg 和 Charly Clark(以及 openpyxl 包的维护者)组织、主持和推动。如果您对该组的更多信息感兴趣,请查阅他们的网页或YouTube频道。
在 Hackathon 周末之前,我做了一些研究,偶然发现了 Medium 上的一篇精彩文章,它有可能帮助我实现 80%-90% 的目标。
因此,黑客马拉松的任务是理解和修改本文中的代码,以便能够从“德国人的历史”播客记录的部分内容中提取包含语义信息的知识图谱,作为未来基于图的 RAG 聊天的输入。
激发一切的文章——以及对它的修改 正如所说,我发现的这篇鼓舞人心的文章提供了一个很好的基础,展示了如何准确地实现最初的意图:从非结构化文本中提取和可视化知识图谱:
本示例对代码所做的更改和修改主要包括:
最后一点让我学到了很多关于提示的知识:SYS_PROMPT 是真正的提示,而 USER_PROMPT 实际上不是一个提示,而是(与 RAG 相当)SYS_PROMPT 执行任务的上下文信息。
根据改变后的用例,需要仔细修改此 SYS_PROMPT:这篇鼓舞人心的文章重点关注印度的医疗系统。这是一个与中世纪德国历史截然不同的领域。第一次运行的结果令人失望……直到我检查了 SYS_PROMPT 中包含的说明中的每一行:例如,将人识别为实体被明确排除在概念提取提示之外。这对涵盖历史的文本产生了相当大的限制。将 SYS_PROMPT 调整为涵盖的历史领域后,结果得到了很大改善,特别关注作为代理或实体的人。
SYS_PROMPT 也是一个很好的切入点,有助于我们了解基于 LLM 的处理与“传统”编程之间的区别:尽管所使用的 SYS_PROMPT 的指令很清晰,但它们并非每次都能产生正确的 JSON 输出格式。需要手动检查输出的质量(即尝试将来自 LLM-prompt-call 的 JSON 字符串加载到结果列表时产生错误的块数)。偶尔跳过一个块应该不会有太大问题,但如果从文本块到 JSON 格式的成功转换与失败转换的比例太低,则可能应该处理文本输入或开始修改和改进 SYS_PROMPT。
改变 LLM 可能会显得有点过头。如果一个更小、更专注的模型不能显示出更好的效率,那就需要进行测试。但应用“为什么鸡要过马路”的逻辑(答案:因为它们可以!),我选择了在上述硬件上运行的性能最高的模型。而这个模型恰好就是 Mixtral。
代码/执行细节 设置和导入
导入常见的软件包,也就是完成工作的软件包。用于建立和可视化图表的软件包稍后再导入。
UUID 包用于为每个块提供唯一的 ID——这对于以后的自连接(在同一个块中同时出现的概念之间创建边)很重要。
Ollama 被设置为客户端。Ollama 调用的具体模型(本例中为 Mixtral)稍后定义。
# ## 设置 import pandas as pd import numpy as np import os from langchain_community.document_loaders import TextLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from pathlib import Path import random # 辅助函数使用的包 import uuid # 用于提示定义的包 import sys sys.path.append( ".." ) import json # 将 Ollama 设置为 LLM from langchain_community.llms import Ollama import ollama from ollama import Client client = Client(host= 'http://localhost:11434' ) “graphPrompt” 函数
此代码中最重要的函数。它用于从提供给函数的文本块中提取所谓的三元组(节点 - 边 - 节点)。这些三元组以 JSON 文件的格式返回,表示文本块中包含的语义信息。在此示例的历史背景下(正如您从结果文件中看到的那样),这主要归结为“演员 A 对演员 B 做了这件事和那件事”的描述。其中演员 A 是三元组中的节点 1,演员 B 是三元组中的节点 2,“这和那”描述了两者之间的关系,从而使其成为三元组的边。
在此函数中,Mixtral 被定义为 Ollama 要使用的模型。当然:必须先下载该模型才能供 Ollama 使用。有关如何执行此操作的详细说明,请参见此处:https://medium.com/llamaindex-blog/running-mixtral-8x7-locally-with-llamaindex-e6cebeabe0ab
正如前面提到的:这里定义的SYS_PROMPT对于实现主要目标非常重要:它强制 Mixtral 提取文本块中的语义关系并以精确定义的 JSON 格式返回它们(……如上所述,这并不总是有效)。我感到非常幸运,这篇励志文章的作者 Rahul Nayak 知道他在做什么。我不认为我能想出这个。但是,如上所述,仍然需要调整提示:与历史播客的记录相比,有关印度卫生系统的文章(励志文章中讨论的背景)中的其他内容更相关。
顺便说一句,USER_PROMPT 只是要处理的文本块。
从代码中可以看出,我在处理实际文本时发现了两个问题:块 ID 并不总是正确地包含在输出 JSON 中。因此,我恢复了一种相当不寻常的“如果它很愚蠢但它有效,那么它就不是愚蠢的”解决方案。
我从测试运行中发现,Mixtral 不知何故倾向于在开始包含 JSON 反馈的列表之前插入不同长度的转义序列以及一些附加信息。在返回结果列表之前,这两个问题都得到了解决。
最后:我还打印出 JSON 字符串(无论正确与否)以便能够监视处理状态。
################################## # 所用 LLM 的定义 ################################## ###################################################################### def graphPrompt ( input : str , metadata={}, model= "mixtral:latest" ): if model == None : model = "mixtral:latest" chunk_id = metadata.get( 'chunk_id' , None ) # model_info = client.show(model_name=model) # print(chalk.blue(model_info)) SYS_PROMPT = ( "您是网络图制作者,可以从给定上下文中提取术语及其关系。" "您将获得一个上下文块(以 ``` 分隔),您的任务是提取 给定上下文中提到的术语的本体。这些术语应根据上下文代表关键概念。\n" "思考 1:遍历每个句子时,思考关键术语其中提到。\n" "\t术语可能包括人(代理人)、地点、组织、日期、持续时间、\n" "\t条件、概念、对象、实体等。\n" "\t术语应尽可能原子化\n\n" "思考 2:思考这些术语如何与其他术语建立一对一的关系。\n" "\t在同一句子或同一段落中提到的术语通常彼此相关。\n" "\t术语可以与许多其他术语相关\n\n" "思考 3:找出每对相关术语之间的关系。\n\n" "将输出格式化为 json 列表。列表的每个元素都包含一对术语" "以及它们之间的关系,如下所示。切勿更改此提示中定义的 chunk_ID 的值:\n" "[\n" " {\n" ' "chunk_id": "CHUNK_ID_GOES_HERE",\n' ' "node_1": "从提取的本体中的概念",\n' ' "node_2": "从提取的本体中相关的概念",\n' ' "edge": "一两句话中两个概念 node_1 和 node_2 之间的关系"\n' " }, {...}\n" "]" ) SYS_PROMPT = SYS_PROMPT.replace( 'CHUNK_ID_GOES_HERE' , chunk_id) USER_PROMPT = f"context: ``` { input } ``` \n\n output: " response = client.生成(model = “mixtral:latest”,system = SYS_PROMPT,prompt = USER_PROMPT) aux1 = response [ 'response' ] # 查找第一个左括号'['的索引 start_index = aux1.find( '[' ) # 从 start_index 中切分字符串以提取 JSON 部分并修复插入转义符的意外问题(为什么?) json_string = aux1[start_index:] json_string = json_string.replace( '\\\\\_' , '_' ) json_string = json_string.replace( '\\\\_' , '_' ) json_string = json_string.replace( '\\\_' , '_' ) json_string = json_string.replace( '\\_' , '_' ) json_string = json_string.replace( '\_' , '_' ) json_string.lstrip() # 消除最终的前导空格 ####################################################### print ( "json-string:\n" + json_string) ######################################################## 尝试: result = json.loads(json_string) result = [ dict (item) for item in result] 除外: print ( "\n\nERROR ### 这是有缺陷的响应:" , response, "\n\n" ) result = None print ( "§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§" ) 返回结果 其他辅助函数
documents2Dataframe:根据输入文本文档的块创建一个数据框;在这里,添加了 UUID(唯一标识符)以明确区分从原始文本中“撕掉”的每个块——这对于进一步处理很重要。顺便说一句,我认为使用数据框的索引就足够了……但他们说什么:“永远不要碰正在运行的系统!”
df2Graph:一种更重要的 graphPrompt() 函数的包装函数;此函数将 graphPrompt() 函数应用于使用前一个 documents2Dataframe() 函数创建的数据框的每一行(以及文本块)
graph2DF:“反向函数”。此函数获取包含 Mistral 从文本块中提取的语义信息的 JSON 三元组列表,并将它们转换为数据框。
contextual_proximity:上面链接的鼓舞人心的文章很好地解释了这个函数的作用。它本质上是一个自连接,用于识别给定块中概念的共现次数。假设共现次数越多,两个链接概念之间的关系(以及语义含义)就越相关。
# ## 函数 def documents2Dataframe ( documents ) -> pd.DataFrame: rows = [] for chunk in documents: row = { "text" : chunk.page_content, **chunk.metadata, "chunk_id" : uuid.uuid4(). hex , } rows = rows + [row] df = pd.DataFrame(rows) return df def df2Graph ( dataframe: pd.DataFrame, model= None ) -> list : # dataframe.reset_index(inplace=True) results = dataframe.apply( lambda row: graphPrompt(row.text, { "chunk_id" : row.chunk_id}, model), axis= 1 ) # 无效的 json 导致 NaN results = results.dropna() results = results.reset_index(drop= True ) ## 将列表列表展平为一个实体列表。 concept_list = np.concatenate(results).ravel().tolist() return concept_list def graph2Df ( nodes_list ) -> pd.DataFrame: ## 删除所有 NaN 实体 graph_dataframe = pd.DataFrame(nodes_list).replace( " " , np.nan) graph_dataframe = graph_dataframe.dropna(subset=[ "node_1" , "node_2" ]) graph_dataframe[ "node_1" ] = graph_dataframe[ "node_1" ].apply( lambda x: x.lower()) graph_dataframe[ "node_2" ] = graph_dataframe[ "node_2" ].apply( lambda x: x.lower()) return graph_dataframe def contextual_proximity ( df: pd.DataFrame ) -> pd.DataFrame: ## 将数据框融入节点列表 dfg_long = pd.melt( df, id_vars=[ "chunk_id" ], value_vars=[ "node_1" , "node_2" ], value_name= "node" ) dfg_long.drop(columns=[ "variable" ], inplace= True ) # 以块 id 为键的自连接将在同一文本块中出现的术语之间创建链接。 dfg_wide = pd.merge(dfg_long, dfg_long, on= "chunk_id" ,suffixes=( "_1" , "_2" )) # 删除自循环 self_loops_drop = dfg_wide[dfg_wide[ "node_1" ] == dfg_wide[ "node_2" ]].index dfg2 = dfg_wide.drop(index=self_loops_drop).reset_index(drop= True ) ## 分组并计数边。 dfg2 = ( dfg2.groupby([ "node_1" , "node_2" ]) .agg({ "chunk_id" : [ "," .join, "count" ]}) .reset_index() ) dfg2.columns = [ "node_1" , "node_2" , "chunk_id" , "count" ] dfg2.replace("" , np.nan, inplace= True) dfg2.dropna(subset=[ "node_1" , "node_2" ], inplace= True) # 删除计数为 1 的边 dfg2 = dfg2[dfg2[ "count" ] != 1 ] dfg2[ "edge" ] = "contextual vicinity" return dfg2 输入和输出变量
此部分定义了输入所来自的子目录和精确的文件名以及写入结果的文件名。单独的“仅可视化”笔记本使用相同的约定,以便它可以直接读取此代码生成的结果。
显然,如果应用于其他用例,这是需要根据实际数据源进行修改的部分。
# ## 变量 ## 输入数据目录 ############################################################# input_file_name = "Saxony_Eastern_Expansion_EP_96.txt" ########################################################## data_dir = "HotG_Data/" +input_file_name inputdirectory = Path( f"./ {data_dir} " ) ## 这是输出 csv 文件的写入位置 outputdirectory = Path( f"./data_output" ) output_graph_file_name = f"graph_ {input_file_name[:- 4 ]} .csv" output_graph_file_with_path = outputdirectory/output_graph_file_name output_chunks_file_name = f"chunks_ {input_file_name[:- 4 ]} .csv" output_chunks_file_with_path =输出目录/输出块文件名 output_context_prox_file_name = f“graph_contex_prox_ {输入文件名[:-4 ] } .csv” output_context_prox_file_with_path = 输出目录/输出上下文_prox_file_name 其余代码与励志文章中的非常相似:
加载并分块源文档
# ## 加载文档 #loader = TextLoader("./HotG_Data/Hanse.txt") loader = TextLoader(inputdirectory) Document = loader.load() # 清除不必要的换行符 Document[ 0 ].page_content = Document[ 0 ].page_content.replace( "\n" , " " ) splitter = RecursiveCharacterTextSplitter( chunk_size= 1000 , chunk_overlap= 100 , length_function= len , is_separator_regex= False , ) pages = splitter.split_documents(Document) print ( "块数 = " , len (pages)) print (pages[ 5 ].page_content) 根据数据块创建数据框
# ## 创建所有块的数据框 df = documents2Dataframe(pages) print (df.shape) df.head() 提取概念:核心代码片段!
df2Graph 在包含文本块的整个数据框上调用;如上所述,df2Graph 将 graphPrompt() 函数应用于数据框中的每个块。并且此 graphPrompt() 函数根据 SYS_PROMPT 中给出的指令从文本块中执行实际的“知识提取”。
包含分块文本的数据框以及包含检索到的三元组的图形数据框都保存到之前定义的输出目录中,以避免必须重新创建简单可视化的信息。
# ## 提取概念 ## 要使用 LLM 重新生成图形,请将其设置为 True ################## regenerate = False # 如果需要耗时(重新)生成知识提取,则切换为 True ################## if regenerate: ####################################################### ideas_list = df2Graph(df, model= 'mixtral:latest' ) ############################################################ dfg1 = graph2Df(concepts_list) if not os.path.exists(outputdirectory): os.makedirs(outputdirectory) dfg1.to_csv(output_graph_file_with_path, sep= ";" , index= False ) df.to_csv(output_chunks_file_with_path, sep= ";" , index= False ) else : dfg1 = pd.read_csv(output_graph_file_with_path, sep= ";" ) dfg1.replace( "" , np.nan, inplace= True ) dfg1.dropna(subset=[ "node_1" , "node_2" , 'edge' ], inplace= True ) dfg1[ 'count' ] = 4 ## 将关系权重增加到 4。 ## 当稍后计算上下文接近度时,我们将分配 1 的权重。print ( dfg1.shape) dfg1.head() 计算上下文接近度
如上所述:这部分代码识别给定块中概念的共现次数。假设共现次数越多,两个链接概念之间的关系(以及语义含义)就越相关。
请注意,上下文邻近数据框也以 CSV 格式保存到定义的输出目录中。
# ## 计算上下文接近度 dfg2 = contextual_proximity(dfg1) dfg2.to_csv(output_context_prox_file_with_path, sep= ";" , index= False ) dfg2.tail() # # ### 合并两个数据框 dfg = pd.concat([dfg1, dfg2], axis= 0 ) dfg = ( dfg.groupby([ "node_1" , "node_2" ]) .agg({ "chunk_id" : "," .join, "edge" : ',' .join, 'count' : 'sum' }) .reset_index() ) 图形可视化部分 与鼓舞人心的文章相比,这部分代码保持不变。再次强调:我最高兴的是 Rahul Nayak 显然知道自己在做什么!
此处的更改可以添加有趣的新方面。例如,我假设还有其他社区生成算法(代码使用 Girvan-Newman),最终可以更好地适应用例。因此,这里再次是一个充足的实验领域。
实例化 NetworkX 图对象
# ## 计算 NetworkX 图 nodes = pd.concat([dfg[ 'node_1' ], dfg[ 'node_2' ]], axis= 0 ).unique() nodes.shape import networkx as nx G = nx.Graph() ## 将节点添加到图 for node in nodes: G.add_node( str (node) ) ## 将边添加到图 for index, row in dfg.iterrows(): G.add_edge( str (row[ "node_1" ]), str (row[ "node_2" ]), title=row[ "edge" ], weight=row[ 'count' ]/ 4 ) 计算社区
# ### 计算用于为节点着色的社区 community_generator = nx.community.girvan_newman(G) top_level_communities = next (communities_generator) next_level_communities = next (communities_generator) communitys = sorted ( map ( sorted , next_level_communities)) print ( "社区数量 = " , len (communities)) print (communities) 准备数据以使用颜色代码信息增强图形,并向图形中添加颜色信息
根据之前计算的每个节点的社区成员身份,对图形节点应用颜色代码。
# ### 为社区颜色创建数据框 import seaborn as sns palette = "hls" ## 现在将这些颜色添加到社区并创建另一个数据框 def colors2Community ( community ) -> pd.DataFrame: ## 定义调色板 p = sns.color_palette(palette, len (communities)).as_hex() random.shuffle(p) rows = [] group = 0 for community in community: color = p.pop() group += 1 for node in community: rows += [{ "node" : node, "color" : color, "group" : group}] df_colors = pd.DataFrame(rows) return df_colors colors = colors2Community(communities) colors # ### 向图表中添加颜色 for index, row in colors.iterrows(): G.nodes[row[ 'node' ]][ 'group' ] = row[ 'group' ] G.nodes[row[ 'node' ]][ '颜色' ] = row[ '颜色' ] G.节点[row[ '节点' ]][ '大小' ] = G.度[row[ '节点' ]] 实例化 pyviz 网络对象并显示结果图
从pyvis.network导入网络 net = 网络( notebook= True, #bgcolor="#1a1a1a", cdn_resources= "remote", height= "800px", width= "100%", select_menu= True, #font_color="#cccccc", filter_menu= False, ) net.from_nx(G) net.force_atlas_2based(central_gravity= 0.015,gravity= -31) net.show_buttons(filter_=[ 'physics' ]) net.show(“knowledge_graph.html”) 最后,你应该得到如下结果:
该图是交互式的:您可以放大和缩小,将节点拖到不同位置等。这是预期目的的完美切入点:直观的知识发现和探索!是否存在我以前从未见过的关系?是否存在“意外联系”?是否存在我以前认为很重要的东西?等等。
但是在您对图表本身着迷之前,请还请注意上面代码块中的一段代码:
net.show_buttons(filter_=['physics']) 我花了一段时间才注意到:但这会在图表下方添加一个交互式控制字段,允许您调整图表可视化的物理行为。人们“仅”需要知道并向下滚动即可看到它。这为探索可能性增加了另一个维度。为了确保您知道要寻找什么:
到目前为止的代码:如果您有兴趣,这里再次是相应 GitHub 存储库的链接:
GitHub - syrom/LocalKnowledgeGraphExtraction:从正常数据中提取知识图谱的代码…… 从正常的非结构化文本中提取知识图谱并可视化生成的图谱的代码…… github.com
结语: Ollama 漏洞 整个领域仍然很新 — — 新软件通常仍处于实验阶段。这也适用于 Ollama。我最初尝试(一夜之间)在涵盖整个历史时期(即萨利安王朝、霍亨斯陶芬王朝等)的记录上运行上述代码 — — 从而一次最多运行 40 集。但这行不通,因为 Ollama 会在某个时候停止生成对代码对 Mixtral 的调用的响应。
这个错误似乎与某些内存溢出或泄漏有关,因为它发生在相当恒定的生成次数之后(获取文本块并从中生成 JSON 格式)
该错误已在 GitHub 上发现并标记.....截至今天的 Ollama 更新(2024-03-29),已得到部分修复:
此次更新之后,首次可以使用下面的代码来处理大文本,在给定的情况下,包含 100 个块,每个块大小为 1,000 个字符(其中 100 个字符重叠)。
不幸的是,当块大小 > 120 时,我仍然不可避免地遇到了 LLM 调用的停滞:即使内核仍处于活动状态,代码执行也会停止并且不再返回任何结果。不过,这仍然足够好,可以一次处理大约 3 个播客剧集的记录(但如上所述,GitHub 示例仅使用单个剧集的文本来确保它确实有效)。
这个问题肯定是由于使用的所有工具都很新颖造成的——并且可能会也可能不会随着进一步的更新而完全消失。
表现 如果您认为本地发电轻而易举,那么请再想想!
在本地机器(MacBook M1 Pro)上,知识提取过程的性能很慢。这显示了引擎盖下有多少事情在发生。我计算出每个块的处理时间为 30 秒到大约不到一分钟,以生成平均约 40 秒的 JSON 字符串。因此,大约 100 个块的文本(即基于 1000 个块大小的 100,000 个字符长度)需要超过一小时的处理时间来提取知识图谱。另外:你最好不要拔掉电源线。一旦脚本开始运行,原本非常省电的 MacBook 就会开始耗电。
因此,代码还将结果以多种形式保存为 CSV 文件。提取完成后,只需加载包含提取过程结果的文件,即可更快地重现知识图谱。或者,输出可在第二步中用作 RAG 输入。
如前所述:有一个专门的笔记本,用于从 GitHub 上保存的文件中重现知识图谱,从而跳过耗费时间和精力的提取部分。