利用大型语言模型(LLMs)从零开始构建知识图谱

利用大型语言模型(LLMs)从零开始构建知识图谱

利用LLMs将你的Pandas数据框转换为知识图谱。从零开始构建你自己的LLM图谱构建器,通过LangChain实现LLMGraphTransformer,并对你的知识图谱(KG)进行问答。

维基百科中1000部电影的知识图谱——作者供图
在当今的人工智能世界中,知识图谱正变得愈发重要,因为它们为许多大型语言模型背后的知识检索系统提供了支持。许多公司的数据科学团队都在大力投资检索增强生成(RAG)技术,因为这是提高大型语言模型输出准确性并防止产生幻觉的有效方法。
但不仅如此,就个人而言,图谱 - RAG正在使人工智能领域更加民主化。这是因为以前如果我们想针对某个用例定制一个模型——无论是出于娱乐还是业务目的——我们有三个选择:预训练模型,使其对用例所属行业内的数据集有更多接触;在特定数据集上对模型进行微调;以及提供上下文提示。
至于预训练,这个选项成本极高且技术要求很高,对大多数开发者来说并不可行。
微调比预训练更容易,虽然微调的成本取决于模型和训练语料库,但总体来说是一个更经济实惠的选择。这曾是大多数人工智能开发者的首选策略。然而,每周都有新模型发布,你需要不断地对新模型进行微调。
第三个选项涉及直接在提示中提供知识。不过,只有当模型需要学习的知识量相对较小时,这种方法才有效。尽管模型的上下文在不断增大,但元素的召回准确率与所提供上下文的大小成反比。
这三个选项似乎都不太合适。那么,有没有其他办法能让模型学习到专门用于某个任务或主题所需的所有知识呢?答案是没有。
但是,模型不需要一次性学习所有知识,因为当我们向大型语言模型提问时,我们可能只是想获取一条或几条信息。这时图谱 - RAG就派上用场了,它提供了一种信息检索方式,能根据查询检索出所需信息,而无需进一步训练。
让我们来看一下“图谱 - RAG”是什么样的:

  • 图谱构建:在此阶段,我们从数据源创建节点(实体)和边(关系),并将它们加载到我们的知识图谱中。这通常是一个更手动的步骤,我们使用一种查询语言(通常是OpenCypher)来上传实体并通过边将它们相互连接起来。

  • 节点索引:这一步涉及创建一种数据结构,以便我们能够高效地检索数据。在知识图谱中,这通常需要创建一个向量搜索索引,其中每个索引都与一个向量嵌入相关联。

  • 图谱检索器:在这里,我们构建一个检索函数,用于计算相似度得分并检索出最相关的节点,这些节点将作为大型语言模型提供答案的上下文。在最简单的情况下,我们会计算查询(已转换为向量嵌入)与向量索引中所有向量嵌入之间的余弦相似度。

  • RAG评估:最后一步对于实际衡量大型语言模型的准确性和性能很有用。在实验过程中,它有助于了解不同的大型语言模型以及RAG框架在你的特定用例中的表现。
    既然我们已经对RAG流程的构成有了总体了解,我们可能会迫不及待地想要尝试复杂的数学函数来进行图谱检索,以确保尽可能达到最佳的信息检索准确性。但是……等等。我们还没有构建知识图谱呢。这一步可能看起来就像数据科学中经典的数据清理和预处理步骤(很枯燥……)。但如果我告诉你有更好的替代方法呢?一种更具科学性和自动化的选择。实际上,近期的研究正聚焦于如何自动化构建知识图谱,因为这一步对于良好的信息检索至关重要。试想一下,如果知识图谱中的数据质量不佳,那么你的图谱 - RAG就不可能有最先进的性能表现。
    在本文中,我们将深入探讨第一步:如何在不实际手动构建的情况下构建知识图谱。

    从CSV文件到知识图谱

    现在,让我们通过一个实际例子来让内容更具体些。让我们来解决一个最重要的现实问题:看什么电影?……有多少次你在工作后感到无聊和疲惫,唯一能做的事就是看电影?你开始在众多电影中浏览,结果不知不觉两个小时就过去了。
    为了解决这个问题,让我们使用维基百科电影数据集创建一个知识图谱,并与这个知识图谱进行交互。首先,我们使用LLMs实现一个“从零开始”的解决方案。然后,我们来看一下通过LangChain(截至2024年11月仍处于实验阶段)实现的最新方案之一,LangChain是目前可用的最流行、最强大的大型语言模型框架之一,还有LlamaIndex提供的另一种流行解决方案。
    让我们从Kaggle上下载这个公开数据集(许可协议:知识共享署名 - 相同方式共享4.0国际许可协议):
    [

    维基百科电影剧情

    约35,000部电影的剧情描述

    www.kaggle.com
    ](https://www.kaggle.com/datasets/jrobischon/wikipedia-movie-plots)
    或者,如果你嫌麻烦,直接去克隆我的GitHub仓库:
    [

    GitHub - cristianleoo/rag-knowledge-graph

    通过在GitHub上创建账号为cristianleoo/rag-knowledge-graph的开发做贡献。

    github.com
    ](https://github.com/cristianleoo/rag-knowledge-graph)
    “knowledge-builder”文件夹包含了本文将要涉及的Jupyter Notebook文件以及相关数据。

    先决条件

    在开始之前,我们需要能够访问Neo4j Desktop,并获取一个大型语言模型的API密钥或者有一个本地的大型语言模型。如果你已经具备这些条件,可以随意跳过这部分内容,直接进入操作环节。如果没有,那我们就来进行设置,别担心,这完全是免费的。
    使用Neo4j有多种方式,但为了简单起见,我们将使用Neo4j桌面版,因此我们会在本地托管数据库。不过这是一个小数据集,所以在你的笔记本电脑上运行这个应用程序不会有什么问题。
    要安装Neo4j,只需访问Neo4j桌面版下载页面并点击下载。安装完成后打开Neo4j Desktop。登录或创建一个Neo4j账户(激活软件需要此操作)。
    登录后,创建一个新项目

  • 点击左上角的**+**按钮。

  • 为项目命名(例如“维基电影知识图谱”)。
    在项目内部,点击**添加数据库**。选择本地数据库管理系统(Local DBMS)并点击创建本地图谱
    配置你的数据库:

  • 名称:输入一个名称(例如neo4j)。

  • 密码:设置一个密码(例如ilovemovies)。记住这个密码,稍后会用到。
    点击创建来初始化数据库。
    接下来,我们来看大型语言模型。运行这个Notebook的首选方式是使用Ollama。Ollama是一种本地托管的大型语言模型解决方案,它能让你非常轻松地在笔记本电脑上下载并设置大型语言模型。它支持许多开源的大型语言模型,包括Meta的Llama和谷歌的Gemma。我推荐这个步骤,因为运行本地的大型语言模型是免费的(不包括笔记本电脑的性能损耗/能耗成本),而且私密,感觉更有意思。
    要下载Ollama,请访问Ollama官方网站,并下载适用于你操作系统的安装程序。安装完成后打开Ollama应用程序。
    打开终端并使用以下命令列出可用模型:
    ollama list
    安装并运行一个模型。我们将使用qwen2.5-coder:latest,这是一个针对代码任务进行了微调的7B语言模型。
    ollama run qwen2.5-coder:latest
    验证安装情况:
    ollama list
    你现在应该能看到:
    qwen2.5-coder:latest
    另一个免费的替代方案是谷歌的Gemini,它允许我们每天进行1500次请求。这个解决方案实际上比前面那个效果更好,因为我们使用的是一个更大、更强大的模型。不过,根据你每天执行脚本的次数,可能会达到请求次数限制。
    要获取Gemini的免费API密钥,请访问网站并点击“获取API密钥”。然后按照说明操作,复制生成的API密钥。我们稍后会用到它。

    从零开始构建图谱

    首先,让我们导入项目所需的一些库:

    类型提示

    from typing import Any, Dict, List, Tuple

    标准库

    import ast
    import logging
    import re
    import warnings

    第三方包 - 数据操作

    import pandas as pd
    from tqdm import tqdm

    第三方包 - 环境与数据库

    from dotenv import load_dotenv
    from neo4j import GraphDatabase

    第三方包 - 错误处理与重试逻辑

    from tenacity import retry, stop_after_attempt, wait_exponential

    Langchain - 核心

    from langchain.chains import GraphCypherQAChain
    from langchain.prompts import PromptTemplate
    from langchain_core.documents import Document

    Langchain - 模型与连接器

    from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAI
    from langchain_ollama.llms import OllamaLLM

    Langchain - 图谱与实验性模块

    from langchain_community.graphs import Neo4jGraph
    from langchain_experimental.graph_transformers import LLMGraphTransformer

    抑制警告

    warnings.filterwarnings(‘ignore’)

    加载环境变量

    load_dotenv()
    如你所见,LangChain在代码组织方面做得不太好,导致导入的代码行数较多。让我们来分析一下我们正在导入的这些库:

  1. **os** dotenv:帮助我们管理环境变量(比如数据库凭证)。
  2. **pandas**:用于处理和操作电影数据集。
  3. **neo4j**:这个库用于将Python与Neo4j图数据库进行连接。
  4. **langchain**:提供了用于操作语言模型(LLMs)和图谱的工具。
  5. **tqdm**:为打印语句添加一个美观的用户界面。我们将使用它在循环中显示进度条,这样就能知道还有多少处理工作没完成。
  6. **warnings**:抑制不必要的警告,使输出更简洁。
    我们加载电影数据集,该数据集包含了来自世界各地的34,886部电影的相关信息。该数据集在Kaggle上公开可用(许可协议:知识共享署名 - 相同方式共享4.0国际许可协议)。不过,如果你克隆了我的GitHub仓库,数据集将已经存在于数据文件夹中:
    movies = pd.read_csv(‘data/wiki_movies.csv’) # 如果你手动下载了数据集,请调整路径
    movies.head()
    在这里,我们可以看到以下特征:
  • 发行年份:电影发行的年份。

  • 标题:电影的标题。

  • 产地/族裔:电影的来源地(例如美国、宝莱坞、泰米尔等)。

  • 导演:导演姓名。

  • 演员阵容:主要演员姓名。

  • 类型:电影类型。

  • 维基页面:从中提取剧情描述的维基百科页面的URL。

  • 剧情:电影剧情的详细描述。
    通过查看这些特征,我们可以很快想出一些希望在知识图谱中看到的标签和关系。由于这是一个电影数据集,“电影”就是其中一个节点标签。此外,我们可能会对查询特定演员和导演感兴趣。因此,我们最终为节点确定了三个标签:电影(Movie)、演员(Actor)和导演(Director)。当然,我们可以包含更多标签,但为了简单起见,我们先就到这里。
    为了简单起见,让我们对这个数据集进行一些清理,并仅提取前1000行数据:
    def clean_data(df: pd.DataFrame) -> pd.DataFrame:
    “”"清理并预处理数据框。

    Args:
    data: 输入数据框
    Returns:
    清理后的的数据框
    """
    df.drop(["维基页面"], axis=1, inplace=True)
    # 去除重复项(根据标题)
    df = df.drop_duplicates(subset='标题', keep='first')
    # 获取对象类型的列
    col_obj = df.select_dtypes(include=["对象"]).columns
    # 清理字符串列
    for col in col_obj:
    # 去除空白字符
    df[col] = df[col].str.strip()
    # 替换未知/空值
    df[col] = df[col].apply(
    lambda x: None if pd.isna(x) or x.lower() in ["", "未知"] 
    else x.capitalize()
    )
    # 删除包含任何空值的行
    df = df.dropna(how="any", axis=0)
    return df
    

    movies = clean_data(movies).head(1000)
    movies.head()
    在这里,我们删除了维基页面列,它包含了指向维基百科页面的链接。不过,你也可以选择保留它,因为它可以作为电影节点的一个属性。接下来,我们根据标题去除所有重复项,并清理所有的字符串对象类型)列。最后,我们只保留前1000部电影的数据。
    由于我们的知识图谱将托管在Neo4j上,让我们创建一个辅助类来建立连接并提供一些有用的方法:
    class Neo4jConnection:
    def init(self, uri, user, password):
    self.driver = GraphDatabase.driver(uri, auth=(user, password))
    def close(self):
    self.driver.close()
    print(“连接已关闭”)
    def reset_database(self):
    with self.driver.session() as session:
    session.run(“MATCH (n) DETACH DELETE n”)
    print(“数据库已重置成功!”)
    def execute_query(self, query, parameters=None):
    with self.driver.session() as session:
    result = session.run(query, parameters or {})
    return [record for record in result]
    在初始化(__init__方法)中,我们使用数据库的URL(uri)、用户名和密码建立与Neo4j数据库的连接。稍后在初始化这个类时,我们会传入这些变量。
    **close**方法用于终止与数据库的连接。
    **reset_database**方法使用Cypher命令MATCH (n) DETACH DELETE n删除数据库中的所有节点和关系。
    **execute_query**方法用于运行给定的查询(比如添加一部电影或获取关系的查询)并返回结果。
    接下来,让我们使用这个辅助类连接到数据库:
    uri = “bolt://localhost:7687”
    user = “neo4j”
    password = “你的密码”
    conn = Neo4jConnection(uri, user, password)
    conn.reset_database()
    默认情况下,uriuser将与上面提供的内容匹配。至于password,它将是你在创建数据库时定义的那个密码。此外,让我们通过**reset_database**方法来确保我们从一个干净的状态开始,即删除任何现有的数据。
    如果你遇到与数据库中未安装APOC相关的任何错误,请前往Neo4j -> 点击数据库 -> 插件 -> 安装APOC:
    Neo4j桌面版——作者供图
    我们现在需要从数据集中取出每一部电影,并将其转换为图谱中的一个节点。在本节中,我们将手动完成此操作,而在后续章节中,我们将借助大型语言模型来帮我们完成。

    def parse_number(value: Any, target_type: type) -> Optional[float]:
     """将字符串解析为数字,并进行适当的错误处理。"""
     if pd.isna(value):
     return None
     try:
     cleaned = str(value).strip().replace(',', '')
     return target_type(cleaned)
     except (ValueError, TypeError):
     return None
    def clean_text(text: str) -> str:
     """清理并规范化文本字段。"""
     if pd.isna(text):
     return ""
     return str(text).strip().title()
    

    让我们创建两个简短的函数——**parse_number** clean_text——用于将数据中的数值列转换为数字,并对文本列进行适当的格式化。如果转换失败(例如,值为空),对于数值列它们将返回None,对于对象类型列则返回空字符串。
    接下来,让我们创建一个函数来迭代地将数据加载到我们的知识图谱中:

    def load_movies_to_neo4j(movies_df: pd.DataFrame, connection: GraphDatabase) -> None:
     """将电影数据加载到Neo4j中,带有进度跟踪和错误处理功能。"""
     logger = logging.getLogger(__name__)
     logger.setLevel(logging.INFO)
     # 查询模板
     MOVIE_QUERY = """
     MERGE (movie:Movie {title: $title})
     SET movie.year = $year,
     movie.origin = $origin,
     movie.genre = $genre,
     movie.plot = $plot
     """
     DIRECTOR_QUERY = """
     MATCH (movie:Movie {title: $title})
     MERGE (director:Director {name: $name})
     MERGE (director)-[:DIRECTED]->(movie)
     """
     ACTOR_QUERY = """
     MATCH (movie:Movie {title: $title})
     MERGE (actor:Actor {name: $name})
     MERGE (actor)-[:ACTED_IN]->(movie)
     """
     # 处理每部电影
     for _, row in tqdm(movies_df.iterrows(), total=len(movies_df), desc="正在加载电影"):
     try:
     # 准备电影参数
     movie_params = {
         
         
     "title": clean_text(row["标题"]),
     "year": parse_number(row["发行年份"], int),
     "origin": clean_text(row["产地/族裔"]),
     "genre": clean_text(row["类型"]),
     "plot": str(row["剧情"]).strip()
     }
     # 创建电影节点
     connection.execute_query(MOVIE_QUERY, parameters=movie_params)
     # 处理导演信息
     for director in str(row["导演"]).split(" 和 "):
     director_params = {
         
         
     "name": clean_text(director),
     "title": movie_params["title"]
     }
     connection.execute_query(DIRECTOR_QUERY, parameters=director_params)
     # 处理演员阵容
     if pd.notna(row["演员阵容"]):
     for actor in row["演员阵容"].split(","):
     actor_params = {
         
         
     "name": clean_text(actor),
     "title": movie_params["title"]
     }
     connection.execute_query(ACTOR_QUERY, parameters=actor_params)
     except Exception as e:
     logger.error(f"加载 {
           
           row['标题']} 时出错: {
           
           str(e)}")
     continue
     logger.info("已完成将电影加载到Neo4j中")
    

    要理解上面的Cypher查询,有两个重要的关键字需要了解,即MERGESET
    **MERGE**确保节点或关系存在;如果不存在,则创建它。因此,它结合了MATCHCREATE子句,其中MATCH允许我们在图谱中搜索特定结构,而CREATE用于创建节点和关系。所以,MERGE首先会检查我们正在创建的节点/边是否已存在,如果不存在则创建它。
    在上述函数中,我们使用MERGE为每部电影、每位导演和每个演员创建节点。特别是,由于我们有演员相关的特征(主演

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值