AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


图谱为RAG提供了更安全的地面
发布日期:2024-04-19 19:13:54 浏览次数: 1808 来源:大数据技术体系


动机与背景

像往常一样,过程即是目的地。所以在进入代码部分之前,我先详细解释了我为什么要写这篇文章的WHY。如果您只对结果和代码感兴趣,可以直接跳转到“代码/执行细节”部分(以及GitHub上的笔记本[1])。但您将错过重要的背景信息....这为过渡到图谱主题提供了完美的契机:

为什么是图谱?

我之前在组织现实更灵活的可视化超越纯层级结构的背景下[2]使用过图谱。

尝试使用RAG(检索增强生成)[3]之后,我对图谱的兴趣被重新点燃。在纯粹统计词嵌入不够好并容易导致LLM产生幻觉的用例中,图谱作为RAG的基础变得有趣。

图谱为RAG提供了更安全的地面,因为它们封装了文本中包含的语义,并是促进理解的好方法,通过(a)潜在的语义关系可视化和(b)作为图嵌入的输入(与纯词嵌入相对)。

然而,完整的RAG是第二步。本文仅限于从非结构化文本创建图谱。嵌入图谱并将其用于语义驱动的RAG超出了本文范围,留待将来的文章。

为什么是本地LLM?

我从未在本地运行过LLM——但我清楚地看到了兴趣和需求:

  • • 隐私和保密性,例如在处理不能传输给不受控制的第三方公司信息时。本文中的示例不需要这种保密级别。但它作为一个概念验证,展示了一个解决方案,也适合从机密公司文件中提取知识,因为这些信息不会离开您的计算机。

  • • 使用和支持开源模型,以促进信息自主权,独立于“赢者通吃”的公司。如果您对为什么这很重要感兴趣,我推荐阅读Daniel Jeffries的这篇文章(并最终订阅他的Substack频道)[4]

  • • 硬件和软件的融合演进,使得在(相对)经济实惠的家庭解决方案上运行高质量的LLM成为可能。

具体选择是结合Ollama,在Macbook M1 Pro上运行Mixtral 8x7B专家模型混合。Ollama承诺以非常简单的方式使LLM在本地运行。我没有失望——它真的非常简单。

我听说Mixtral表现出非常出色的性能,与chatGPT 3.5相当。互联网上的计算和反馈表明,该模型能够在拥有64 GB RAM的Mac上运行。而且,随着M3处理器的推出,使用过的M1硬件价格的下降,使我能够以不到50%的价格获得这个选择——与新的M3基准相比,性能只牺牲了25% - 30%——而且我发现,即使对于任务非常计算密集的任务,性能仍然足够满足自己的用例。

顺便说一下,任务的计算强度也会将任何第三方API使用的成本推高到更高的水平——所以即使我还没有做过计算,我也认为在长期来看,投资自己的硬件也是成本效益的。当然,这是基于您确实长期使用它的假设。但图谱提取并不是唯一的用例。我还看到本地代理在支持日常任务上有广阔的领域。具体来说,这可以是什么样子,我已经尝试过“职业教练代理”[5]——只是当时的代码仍然依赖于OpenAI API。

下一步计划:

如上所述,知识提取和使用本地LLM都适合进行超出本文范围的更多实验(...但关于这些可能性的进一步文章可能会随之出现)。

  • • 对于提取的图的使用,这主要意味着将其作为基础,以改进基于语义的RAG应用增强知识图谱的RAG应用的准确性[6]

  • • 运行本地LLM的额外用途是(1)使用自己的数据对开源模型进行微调的可能性,帮助LLM提供更相关的答案,以更好地解决可用数据的特定用例,以及(2)上面提到的在本地运行代理框架的可能性。

用例细节:德国人历史的播客转录

作为第一个用例,我的目标是从我目前最喜欢的播客“德国人的历史”[7]的非结构化文本中提取一个知识图谱,这是由Dirk Hoffmann-Becking主讲的,我真诚而真挚地向任何历史爱好者推荐。

我已经从相关的网站上抓取了播客的转录文本,每个时期都有一个大型的文本语料库(例如“奥托人”、“萨利安人”、“霍亨斯陶芬人”等)[8]。然而,由于下面解释的原因,这个例子只适用于单个剧集的转录。

历史文本非常清楚地展示了“仅嵌入”RAG的不足,激发了对基于语义的知识图谱驱动的RAG查询文本的兴趣(见:“下一步计划”上文)。

证明这一点的是:我已经根据转录语料库创建了一个基于GPT的模型。但随后对文本的查询显示出非常混合的结果。

时间顺序和关系显然是历史文本中非常重要的概念——但词嵌入并没有很好地捕捉到这些概念。

一个很好的例子:尽管如今听起来可能很奇怪,但在语料库涵盖的时期,教皇对皇帝的绝罚是一种强大的政治工具,经常被定期使用(...而且任何自尊的皇帝都不会忍受至少不被绝罚一次...)。当然,教皇P1绝罚皇帝E1还是皇帝E2是有区别的。特别是如果皇帝E2恰好是皇帝E1的曾孙,而教皇P1在皇帝E2统治开始前几十年就已经去世了。

词嵌入很好地捕捉到了“教皇绝罚皇帝”的关系....但他们开始太快地幻想相应的名字(例如:如果教皇P1绝罚了皇帝E1,为什么他没有绝罚皇帝E2?)。正是因为嵌入不能明确捕捉它们嵌入的单词之间的时间顺序或关系方面。

建立这种联系实际上意味着建立一个图谱。在知识图谱表示中,只会有从教皇P1到皇帝E1的“边”,而不是到E2(因为他们被一生的时间隔开,阻止了任何共现)。

这就是我想要测试基于知识图谱的RAG的例子

作为第一步,这意味着能够建立这个图谱(并可视化它,因为可视化是理解的一个很好的方式)

GitHub上的具体示例中,代码使用了第96集“萨克森和东扩:遇见邻居”的转录[9]

顺便说一下,给你的“无用知识百科全书”增加了一个有趣的小知识:在这集中,撒克逊人遇到了包括今天的蓝牙技术名字来源的Harald Bluetooth在内的其他人(如果我没记错的话,这个名字被选中是因为它是诺基亚和爱立信的联合创新....而且国王Harald Bluetooth碰巧是第一个成功统一瑞典人和挪威人的困难任务的人[10](或者至少是他们的前身):-))

黑客马拉松

所以我有了我的意图....缺少的是机会。这以杜塞尔多夫Python用户组PyDDF的Python黑客马拉松的形式出现,由Marc-André Lemburg和Charly Clark(openpyxl包的维护者等)组织、主持和推动。如果您对了解更多关于这个组织的信息感兴趣,请查看他们的网页[11]他们的YouTube频道[12]

在黑客马拉松周末之前,我做了一些研究,并偶然发现了一篇优秀的Medium文章,有潜力帮我完成80%-90%的目标。

所以黑客马拉松的任务是理解和修改这篇文章的代码,以便能够从“德国人的历史”播客转录的部分中提取一个包含语义信息的知识图谱,作为未来基于图谱的RAG聊天的输入,与这些内容相关。

激发一切的文章——以及对它的改变

正如所说,我找到的启发性文章提供了一个很好的基础,展示了如何实现最初的目标:从非结构化文本中提取并可视化知识图谱:

  • • "如何将任何文本转换为概念图谱"[13] 作者Rahul Nayak

对这篇文章的代码所做的更改和修改主要是:

  • • 转换为单一的全能笔记本

  • • 使用不同的LLM(现在使用更强大的Mixtral模型)

  • • 从代码库中消除一些看似未使用的函数定义

  • • 根据历史用例对SYS_PROMPT进行相关调整

最后一点让我了解到了很多关于提示的信息:SYS_PROMPT是真正的提示,而USER_PROMPT实际上是较少的提示,而是(与RAG相当)上下文信息,SYS_PROMPT执行任务。

而且这个SYS_PROMPT需要根据改变的用例进行仔细的修订:启发性文章关注的是印度卫生系统的文章。这是一个与中世纪德国历史非常不同的领域。第一次运行产生了令人失望的结果...直到我检查了SYS_PROMPT中包含的每条指令:例如,将人识别为实体明确排除在概念提取提示之外。这对涵盖历史的文本产生了很大的限制。在将SYS_PROMPT调整到涵盖历史的领域后,结果有了很大的改善,特别关注作为代理或实体的人。

SYS_PROMPT也是了解LLM基于处理与“传统”编程有多大不同的一个很好的切入点:即使SYS_PROMPT使用的指令是清晰的,它们并不总是每次都产生正确的JSON输出格式。需要手动检查输出的质量(也就是尝试从LLM-提示调用到结果列表加载JSON字符串时产生错误的块的数量)。偶尔跳过一个块不应该太成问题,但如果从文本块到JSON格式的成功转换与不成功转换的比率变得太低,人们应该考虑要么改进文本输入,要么开始修改和改进SYS_PROMPT。

更改LLM可能看起来像是小题大做。需要测试的是,一个更小、更专注的模型是否会显示出更好的效率。但是应用“为什么鸡要过马路”的逻辑(答案:因为他们可以!),我选择了在上述硬件上运行的最高性能模型。而那就是Mixtral。

代码/执行细节

设置和导入

导入通常的嫌疑人,也就是完成工作的包。建立和可视化图谱的包将在之后导入。

UUID包用于为每个块赋予唯一ID - 这对于后续的自连接(在同一个块中共同出现的concept之间创建边)非常重要。

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是三元组的第一个节点,行动者B是第二个节点,而“这个和那个”描述了两者之间的关系,从而使其成为三元组的边。

在这个函数中,Mixtral被定义为Ollama使用的模型。当然:首先必须下载模型才能供Ollama使用。有关如何做到这一点的良好描述可以在这里找到:在本地使用LlamaIndex运行Mixtral 8x7[14]

正如已经提到的:这里定义的SYS_PROMPT非常重要,以达到主要目标:它强制Mixtral提取文本块中的语义关系并以精确定义的JSON格式返回(...并不总是有效,如上所述)。我感到很幸运,启发性文章的作者Rahul Nayak知道他在做什么。我不认为我能想出这个。但是,正如上面解释的,仍然需要调整提示:在印度卫生系统的文章(启发性文章处理的背景)中,与历史播客的转录中相比,其他事情是相关的。

顺便说一下,USER_PROMPT就是待处理的文本块。

从代码中可以看到,我在处理真实文本时遇到了两个问题:块ID并不总是正确地包含在输出的JSON中。因此,我回到了一个相当非传统的“如果它愚蠢但有效,那它就不愚蠢”的解决方案。

而且我从测试运行中看到,Mixtral不知何故倾向于在开始包含JSON反馈的列表之前插入不同长度的转义序列以及一些额外信息。这两个问题在返回结果列表之前都已解决。

最后:我还打印出json字符串(无论正确与否),以便能够监控处理状态。

#################################
# Definition of used 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 = ("You are a network graph maker who extracts terms and their relations from a given context. "
        "You are provided with a context chunk (delimited by ```) Your task is to extract the ontology "
        "of terms mentioned in the given context. These terms should represent the key concepts as per the context. \n"
        "Thought 1: While traversing through each sentence, Think about the key terms mentioned in it.\n"
            "\tTerms may include person (agent), location, organization, date, duration, \n"
            "\tcondition, concept, object, entity  etc.\n"
            "\tTerms should be as atomistic as possible\n\n"
        "Thought 2: Think about how these terms can have one on one relation with other terms.\n"
            "\tTerms that are mentioned in the same sentence or the same paragraph are typically related to each other.\n"
            "\tTerms can be related to many other terms\n\n"
        "Thought 3: Find out the relation between each such related pair of terms. \n\n"
        "Format your output as a list of json. Each element of the list contains a pair of terms"
        "and the relation between them like the follwing. NEVER change the value of the chunk_ID as defined in this prompt: \n"
        "[\n"
        "   {\n"
        '       "chunk_id": "CHUNK_ID_GOES_HERE",\n'
        '       "node_1": "A concept from extracted ontology",\n'
        '       "node_2": "A related concept from extracted ontology",\n'
        '       "edge": "relationship between the two concepts, node_1 and node_2 in one or two sentences"\n' 
        "   }, {...}\n"
        "]"
    )
    SYS_PROMPT = SYS_PROMPT.replace('CHUNK_ID_GOES_HERE', chunk_id)

    USER_PROMPT = f"context: ```{input}``` \n\n output: "

    response = client.generate(model="mixtral:latest", system=SYS_PROMPT, prompt=USER_PROMPT)

    aux1 = response['response']
    # Find the index of the first open bracket '['
    start_index = aux1.find('[')
    # Slice the string from start_index to extract the JSON part and fix an unexpected problem with insertes escapes (WHY ?)
    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() # eliminate eventual leading blank spaces
#####################################################
    print("json-string:\n" + json_string)
#####################################################         
    try:
        result = json.loads(json_string)
        result = [dict(item) for item in result]
    except:
        print("\n\nERROR ### Here is the buggy response: ", response, "\n\n")
        result = None
    print("§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§")

    return result

其他辅助函数

  • • 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"] = "上下文接近度"
    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 = outputdirectory/output_chunks_file_name

output_context_prox_file_name = f"graph_contex_prox_{input_file_name[:-4]}.csv"
output_context_prox_file_with_path = outputdirectory/output_context_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;如上所述,df2GraphgraphPrompt()函数应用于数据框中的每个块。而这个graphPrompt()函数根据SYS_PROMPT中给出的指令,实际从文本块中“提取知识”。

块文本的数据框以及特别是包含检索到的三元组的图数据框都被保存到之前定义的输出目录中,以避免为了简单的可视化而不得不重新创建信息。

# ## 提取概念
## 重新生成图,将LLM设置为True
##################
regenerate = False  # 如果需要重新生成耗时的知识提取(重新生成),将其切换为True
##################
if regenerate:
#########################################################    
    concepts_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
= 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
    )

计算社区

# ### 计算社区以便对节点进行着色
communities_generator = nx.community.girvan_newman(G)
top_level_communities = next(communities_generator)
next_level_communities = next(communities_generator)
communities = sorted(map(sorted, next_level_communities))
print("社区数量 = ", len(communities))
print(communities)

准备数据以增强图的颜色编码信息,并将颜色信息添加到图中

根据每个节点之前计算的社区成员身份,对图节点应用颜色编码。

# ### 为社区颜色创建数据框
import seaborn as sns
palette = "hls"
## 现在将这些颜色添加到社区,并创建另一个数据框
def colors2Community(communities) -> pd.DataFrame:
    ## 定义颜色调色板
    p = sns.color_palette(palette, len(communities)).as_hex()
    random.shuffle(p)
    rows = []
    group = 0
    for community in communities:
        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']]['color'] = row['color']
    G.nodes[row['node']]['size'] = G.degree[row['node']]

实例化pyviz网络对象并显示结果图

from pyvis.network import Network
net = Network(
    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: 从普通文本中提取知识图谱的代码...[15]

后记:

Ollama的bug

整个领域仍然非常新——新软件通常仍处于试验阶段。这也适用于例如Ollama。我最初尝试在一夜之间在本地运行上述代码(覆盖整个历史时期,即像萨利安、霍亨斯陶芬等王朝)——因此一次处理多达40集的转录。但这行不通,因为Ollama在某个时候会停止对代码对Mixtral的调用生成响应。

这个错误似乎与某些内存溢出或泄漏有关,因为它在生成一定数量的响应后发生(获取文本块并生成JSON格式)

这个错误在GitHub上被识别并标记.....并且截至今天的Ollama更新(2024–03–29)部分修复:

  • • https://github.com/ollama/ollama/issues/1863#issuecomment-2010185380

在这个更新之后,代码首次能够处理大量文本,给定情况下有> 100个块,块大小为1,000个字符(重叠100个字符)。

不幸的是,对于块大小> 120,我仍然不可避免地遇到了LLM调用的停滞:代码执行会简单地停止并不再返回任何结果,尽管内核仍然活跃。不过,这已经足够好了,可以处理大约3个播客剧集的转录一次一批(但是,如上所述,GitHub示例只使用单个剧集的文本,以确保它真正有效)。

这个问题肯定是由于使用的所有工具的新颖性——并且可能会也可能不会随着进一步的更新而完全消失。

性能

如果你认为本地生成是在轻而易举地完成的:再想一想!

知识提取过程在本地机器(MacBook M1 Pro)上的性能很慢。这表明底层正在进行大量工作。我计算的处理时间是每块30秒到不到一分钟,平均约为40秒,以生成包含JSON字符串的文本块。因此,大约100个块的文本(基于1000的块大小的100,000个字符长度)需要超过一个小时的处理时间来提取知识图谱。另外:你最好不要拔掉电源线。一旦脚本开始运行,否则极其节俭的MacBook会开始疯狂地消耗电力。

因此,代码还将结果以几种形式保存为CSV文件。因此,一旦提取过程完成,知识图谱可以更快地重现,只需加载包含提取过程结果的文件即可。或者输出可以用作第二步的RAG输入。

正如前面所说:有一个专门的笔记本,仅用于从保存的文件中重现知识图谱在GitHub上,跳过耗时和能源密集的提取部分。



53AI,企业落地应用大模型首选服务商

产品:大模型应用平台+智能体定制开发+落地咨询服务

承诺:先做场景POC验证,看到效果再签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

与创始人交个朋友

回到顶部

 
扫码咨询