使用OpenAI函数从文本构建知识图

2023年10月24日 由 alex 发表 577 0

Neo4j环境设置


你需要设置一个Neo4j环境,以便能够按照本文中的示例进行操作。最简单的方法是在Neo4j Aura上启动一个免费实例,它提供了Neo4j数据库的云实例。或者,你也可以通过下载Neo4j Desktop应用程序并创建一个本地数据库实例来设置一个本地的Neo4j数据库。


以下代码将实例化一个LangChain包装器来连接Neo4j数据库。


from langchain.graphs import Neo4jGraph
url = "neo4j+s://databases.neo4j.io"
username ="neo4j"
password = ""
graph = Neo4jGraph(
    url=url,
    username=username,
    password=password
)


信息提取管道


典型的信息提取流程包含以下步骤。


5


在第一步中,我们将输入文本通过一个指代消解模型。指代消解是找到所有指代特定实体的表达式的任务。简单来说,它将所有代词与所指实体进行链接。在流程的命名实体识别部分,我们尝试提取所有提及的实体。上述示例包含三个实体:Tomaz、Blog和Diagram。下一步是实体消岐步骤,这是信息抽取流程中一个重要但经常被忽视的部分。实体消岐是准确识别和区分具有相似名称或引用的实体的过程,以确保在给定上下文中识别出正确的实体。在最后一步中,模型试图识别各种实体之间的关系。例如,它可以找到Tomaz和Blog实体之间的LIKES关系。


使用OpenAI函数提取结构化信息


OpenAI函数非常适合从自然语言中提取结构化信息。OpenAI函数的想法是让一个LLM输出一个预定义的带有填充值的JSON对象。预定义的JSON对象可以作为RAG应用程序中的其他函数的输入,或者可以用于从文本中提取预定义的结构化信息。


在LangChain中,你可以传递一个Pydantic类作为OpenAI函数特性的所需JSON对象的描述。因此,我们将首先定义我们要从文本中提取的信息的所需结构。LangChain已经有了可以重用的节点和关系的Pydantic类的定义。


class Node(Serializable):
    """Represents a node in a graph with associated properties.
    Attributes:
        id (Union[str, int]): A unique identifier for the node.
        type (str): The type or label of the node, default is "Node".
        properties (dict): Additional properties and metadata associated with the node.
    """
    id: Union[str, int]
    type: str = "Node"
    properties: dict = Field(default_factory=dict)

class Relationship(Serializable):
    """Represents a directed relationship between two nodes in a graph.
    Attributes:
        source (Node): The source node of the relationship.
        target (Node): The target node of the relationship.
        type (str): The type of the relationship.
        properties (dict): Additional properties associated with the relationship.
    """
    source: Node
    target: Node
    type: str
    properties: dict = Field(default_factory=dict)


很不幸,目前OpenAI的功能不支持将字典对象作为一个值。因此,我们需要重写属性定义以遵守函数端点的限制。


from langchain.graphs.graph_document import (
    Node as BaseNode,
    Relationship as BaseRelationship
)
from typing import List, Dict, Any, Optional
from langchain.pydantic_v1 import Field, BaseModel
class Property(BaseModel):
  """A single property consisting of key and value"""
  key: str = Field(..., description="key")
  value: str = Field(..., description="value")
class Node(BaseNode):
    properties: Optional[List[Property]] = Field(
        None, description="List of node properties")
class Relationship(BaseRelationship):
    properties: Optional[List[Property]] = Field(
        None, description="List of relationship properties"
    )


在这里,我们已经将属性值重写为Property类的列表,而不是字典,以克服API的限制。因为你只能向API传递单个对象,所以我们可以将节点和关系组合到一个称为KnowledgeGraph的单个类中。


class KnowledgeGraph(BaseModel):
    """Generate a knowledge graph with entities and relationships."""
    nodes: List[Node] = Field(
        ..., description="List of nodes in the knowledge graph")
    rels: List[Relationship] = Field(
        ..., description="List of relationships in the knowledge graph"
    )


现在唯一剩下的事情就是做一些快速工程,然后我们就可以开始了。我通常进行快速工程的方法如下:


1. 遍历提示并使用自然语言改进结果
2. 如果某些内容不能按预期运行,就请ChatGPT让它更清楚,以便LLM可以理解任务
3. 最后,当提示中包含所有所需的指示时,请ChatGPT用markdown格式总结指示,以节省令牌并可能获得更清晰的指示。


我特意选择了markdown格式,因为我在某个地方看到OpenAI模型对提示中的markdown语法有更好的响应,根据我的经验,这至少是有道理的。


在进行快速工程的迭代过程中,我为信息提取流水线设计了以下系统提示。


llm = ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0)
def get_extraction_chain(
    allowed_nodes: Optional[List[str]] = None,
    allowed_rels: Optional[List[str]] = None
    ):
    prompt = ChatPromptTemplate.from_messages(
    [(
      "system",
      f"""# Knowledge Graph Instructions for GPT-4
## 1. Overview
You are a top-tier algorithm designed for extracting information in structured formats to build a knowledge graph.
- **Nodes** represent entities and concepts. They're akin to Wikipedia nodes.
- The aim is to achieve simplicity and clarity in the knowledge graph, making it accessible for a vast audience.
## 2. Labeling Nodes
- **Consistency**: Ensure you use basic or elementary types for node labels.
  - For example, when you identify an entity representing a person, always label it as **"person"**. Avoid using more specific terms like "mathematician" or "scientist".
- **Node IDs**: Never utilize integers as node IDs. Node IDs should be names or human-readable identifiers found in the text.
{'- **Allowed Node Labels:**' + ", ".join(allowed_nodes) if allowed_nodes else ""}
{'- **Allowed Relationship Types**:' + ", ".join(allowed_rels) if allowed_rels else ""}
## 3. Handling Numerical Data and Dates
- Numerical data, like age or other related information, should be incorporated as attributes or properties of the respective nodes.
- **No Separate Nodes for Dates/Numbers**: Do not create separate nodes for dates or numerical values. Always attach them as attributes or properties of nodes.
- **Property Format**: Properties must be in a key-value format.
- **Quotation Marks**: Never use escaped single or double quotes within property values.
- **Naming Convention**: Use camelCase for property keys, e.g., `birthDate`.
## 4. Coreference Resolution
- **Maintain Entity Consistency**: When extracting entities, it's vital to ensure consistency.
If an entity, such as "John Doe", is mentioned multiple times in the text but is referred to by different names or pronouns (e.g., "Joe", "he"), 
always use the most complete identifier for that entity throughout the knowledge graph. In this example, use "John Doe" as the entity ID.  
Remember, the knowledge graph should be coherent and easily understandable, so maintaining consistency in entity references is crucial. 
## 5. Strict Compliance
Adhere to the rules strictly. Non-compliance will result in termination."""),
        ("human", "Use the given format to extract information from the following input: {input}"),
        ("human", "Tip: Make sure to answer in the correct format"),
    ])
    return create_structured_output_chain(KnowledgeGraph, llm, prompt, verbose=False)


你可以看到我们正在使用GPT-3.5模型的16k版本。主要原因是OpenAI函数的输出是一个结构化的JSON对象,而结构化的JSON语法会增加很多标记开销到结果中。


除了一般的指令之外,我还添加了限制从文本中提取哪些节点或关系类型的选项。通过示例,你将看到这可能会有用。


我们已经准备好Neo4j连接和LLM提示,这意味着我们可以将信息提取流程定义为一个单独的函数。


def extract_and_store_graph(
    document: Document,
    nodes:Optional[List[str]] = None,
    rels:Optional[List[str]]=None) -> None:
    # Extract graph data using OpenAI functions
    extract_chain = get_extraction_chain(nodes, rels)
    data = extract_chain.run(document.page_content)
    # Construct a graph document
    graph_document = GraphDocument(
      nodes = [map_to_base_node(node) for node in data.nodes],
      relationships = [map_to_base_relationship(rel) for rel in data.rels],
      source = document
    )
    # Store information into a graph
    graph.add_graph_documents([graph_document])


该函数接受一个LangChain文档,以及可选的节点和关系参数,用于限制我们希望LLM识别和提取的对象类型。


评估


我们将从华特·迪士尼的维基百科页面提取信息并构建一个知识图谱来测试流水线。在这里,我们将利用LangChain提供的维基百科加载器和文本块模块。


from langchain.document_loaders import WikipediaLoader
from langchain.text_splitter import TokenTextSplitter
# Read the wikipedia article
raw_documents = WikipediaLoader(query="Walt Disney").load()
# Define chunking strategy
text_splitter = TokenTextSplitter(chunk_size=2048, chunk_overlap=24)
# Only take the first the raw_documents
documents = text_splitter.split_documents(raw_documents[:3])


你可能注意到我们使用了一个相对较大的chunk_size值。原因是我们希望在核心指向消解部分尽可能提供尽可能多的上下文,以便使其发挥最佳作用。请记住,只有当实体及其引用出现在同一个块中时,核心指向步骤才能正常工作;否则,LLM没有足够的信息来将两者联系起来。


现在,我们可以继续通过信息提取流程运行文档。


from tqdm import tqdm
for i, d in tqdm(enumerate(documents), total=len(documents)):
    extract_and_store_graph(d)


整个过程大约需要5分钟,相对来说比较慢。因此,在生产环境中,你可能希望使用并行的API调用来解决这个问题并实现一定的可伸缩性。


让我们首先看一下LLM识别出的节点和关系类型。


6


由于没有提供图形模式,LLM会根据需要决定使用什么类型的节点标签和关系类型。例如,我们可以观察到有CompanyOrganization的节点标签。这两个事物可能在语义上非常相似或相同,所以我们希望只有一个节点标签代表这两者。这个问题在关系类型上更加明显。例如,我们有CO-FOUNDERCOFOUNDEROF关系以及DEVELOPERDEVELOPEDBY关系。


对于任何更严肃的项目,你应该定义LLM应该提取的节点标签和关系类型。幸运的是,我们已经通过传递额外的参数来限制提示中的类型的选项。


# Specify which node labels should be extracted by the LLM
allowed_nodes = ["Person", "Company", "Location", "Event", "Movie", "Service", "Award"]
for i, d in tqdm(enumerate(documents), total=len(documents)):
    extract_and_store_graph(d, allowed_nodes)


在这个示例中,我只限制了节点标签,但是你可以通过向extract_and_store_graph函数传递另一个参数来轻松限制关系类型。


提取的子图的可视化具有以下结构。


7


这个图形比预期的好。我无法在可视化中完整地捕捉到整个图形,但你可以在Neo4j浏览器或其他工具中自己探索。


实体消岐


需要提及的一件事是,我们部分跳过了实体消岐部分。我们使用了较大的块大小,并在系统提示中添加了特定的指令以进行共指消解和实体消岐。然而,由于每个块都是分开处理的,无法保证不同文本块之间的实体一致性。例如,你可能会得到代表同一个人的两个节点。


8


在这个例子中,Walt Disney和Walter Elias Disney指的是同一个现实世界的人。实体消岐问题并不新鲜,已经有多种解决方案提出来解决这个问题:


1. 使用实体链接或实体消岐的自然语言处理模型
2. 通过LLM进行第二次遍历,并要求其执行实体消岐
3. 基于图的方法


你应该根据你的领域和用例来选择使用哪种解决方案。然而,要记住实体消岐步骤不容忽视,因为它对于RAG应用的准确性和效果会产生重大影响。


RAG应用


最后我们将向你展示如何通过构建Cypher语句在知识图谱中浏览信息。Cypher是一种结构化查询语言,用于处理图数据库,类似于SQL用于关系数据库。LangChain有一个GraphCypherQAChain,它读取图的模式,并根据用户输入构建适当的Cypher语句。


# Query the knowledge graph in a RAG application
from langchain.chains import GraphCypherQAChain
graph.refresh_schema()
cypher_chain = GraphCypherQAChain.from_llm(
    graph=graph,
    cypher_llm=ChatOpenAI(temperature=0, model="gpt-4"),
    qa_llm=ChatOpenAI(temperature=0, model="gpt-3.5-turbo"),
    validate_cypher=True, # Validate relationship directions
    verbose=True
)
cypher_chain.run("When was Walter Elias Disney born?")


结果如下:


9


结论


知识图谱非常适合在你的RAG应用程序中组合结构化和非结构化数据。在本文中,你已经学习到如何使用OpenAI函数在任意文本上构建Neo4j中的知识图谱。OpenAI函数提供了整齐结构化的输出,使其非常适合提取结构化信息。在使用LLMs构建图谱时,为了获得最佳体验,请确保尽可能详细地定义图谱架构,并在提取后添加实体消歧步骤。


文章来源:https://medium.com/@bratanic-tomaz/constructing-knowledge-graphs-from-text-using-openai-functions-096a6d010c17
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消