知识图谱,在大语言模型出现之前就存在了,但是那个时候可能并不是很好构建,现在有了大语言模型,构建知识图谱的难度大大降低了。

知识图谱这个东西,可以拿来辅助大语言模型推理,可以展示给用户,显得系统很牛,为了降低以后的工作量,我要研究一个通用的知识图谱构建方法(老师的建议)

在此之前,我已经做过三个知识图谱了:

  • 电影信息的知识图谱,这是我由史以来做过最完整的知识图谱,因为电影信息爬过来就是结构化的,演员信息爬百度百科,也是结构化的。

  • 数据大屏项目里面的知识图谱,这个都不能叫知识图谱,感觉就像根据分类展开文章一样。

  • 瓷器信息的知识图谱,这个是爬的故宫博物院的非结构化数据,直接用大语言逐条生成实体和关系的Json,效果不怎么好,而且跑了1/10 Token就不够烧了(那个时候各大平台大语言模型还不免费)

这样一看,研究知识图谱的通用构建方法很重要。

梳理过程

任务是通过非结构化数据构建知识图谱,也就是我要从一些乱七八糟的文本中,找到里面的所有实体、这些实体相互之间的关系、实体各自的属性。

这个任务之前交给大语言模型就是简单粗暴,提示词+非结构化数据 -> 结果,但这样效果肯定不好。

于是,我们先拆分任务:

  1. 实体抽离。实体关系、实体属性先不管,就只抽离实体,这样这个子任务是很简单的。

  2. 实体分类。为了之后方便,只有实体的名称可能是不够的,最好是要知道实体的分类,像是 电视的类别是电器 ,这个任务似乎可以和上一步合在一起。

  3. 实体筛选。可能抽离出来不必要的实体或者同义实体,这些实体不应该作为实体,应该作为属性。

  4. 分类合并。上一步分类是人为规定的,可能就不需要这一步,如果上一步分类是自由发挥的,很可能要合并分类,比如可能出现 电器用电器

  5. 关系抽离。数据再次处理,找实体之间的关系。

  6. 属性抽离。数据再次处理,找实体的属性。

技术选型

我打算完全用大语言模型去解决这些问题。

现在用大语言模型解决问题不用考虑成本问题。

用大语言模型最好是能用点什么框架,虽说手搓工作量可能也不大,但何必呢。

大语言模型应用当然首选https://python.langchain.com/啦,但是这是Python的,我打算用SpringBoot来写,经过一番搜索,不想麻烦去调用Python,我发现了Langchain4j,于是就用它了。

https://docs.langchain4j.dev/

实体和分类抽离

2024年夏季奥林匹克运动会开幕式(法语:Cérémonie d'ouverture des Jeux olympiques d'été de 2024)于欧洲中部时间2024年7月26日晚上7点30分在法国首都巴黎举行,为奥运开幕礼第一次在户外举行。来自206个代表团的10500名运动员乘坐船只从巴黎植物园附近的奥斯特利茨桥,沿着塞纳河巡游,最终抵达埃菲尔铁塔对面的特罗卡德罗广场,主火炬塔则设在广场内。

最简单的任务,用框架三行两行就写完了,看看效果:

提示词:提示词:分析并抽离文本中出现的所有实体,例如人名、产品名、景点名、机构名、电影名等。只输出实体名,不输出实体类型,也不用输出括号里的解释说明。因为你是在向一个计算机程序输出,请严格按照格式输出,不要输出说明文本。

输出:[以下是文本中抽取出的实体名列表:, , 实体名:奥林匹克运动会开幕式, 实体名:巴黎, 实体名:欧洲中部时间, 实体名:奥斯特利茨桥, 实体名:塞纳河, 实体名:特罗卡德罗广场, 实体名:主火炬塔]

框架似乎带了一些提示词,用于格式化输出的,首先AI没有完全按照框架要求输出,输出了额外的东西,然后也不符合我的要求。

然后我调整提示词,这个大语言模型似乎不怎么聪明,输出总是要带着类型,本来把类型拆分到下一步是怕第一步输出不好,既然这样,那就顺水推舟(~ ̄▽ ̄)~

我把LLM的temperature和topP都调小,这种场景不需要他有多样性。

命名实体识别,需要实体名称和实体类别。在每行中输出如下格式:名称: XXX 类别: XXX

[名称: 夏季奥林匹克运动会开幕式 类别: 事件活动, 名称: 巴黎 类别: 地名(城市), 名称: 法国 类别: 国家, 名称: 奥斯特利茨桥 类别: 地名(桥梁), 名称: 塞纳河 类别: 地名(河流), 名称: 埃菲尔铁塔 类别: 地名(建筑), 名称: 特罗卡德罗广场 类别: 地名(广场)]

然后用程序处理一下,把名称和类别分开。

得到了这样的结果:[(夏季奥林匹克运动会开幕式, 事件活动), (巴黎, 地名(城市)), (法国, 国家), (奥斯特利茨桥, 地名(桥梁)), (塞纳河, 地名(河流)), (埃菲尔铁塔, 地名(建筑)), (特罗卡德罗广场, 地名(广场))]

完成!

实体筛选和分类合并

看上面的输出,AI抽离的实体还是比较全的,这其中可能有很多是并不必要的,分类可能分的过于详细了。

但是要筛选掉哪些实体和要合并哪些分类并不应该由AI决定,应该用户根据实际情况决定。

所以这个地方就交给前端。

关系抽离

实体有了,接下来就是实体关系啦

此处我有两个想法:

  • 把原来抽离实体那一段的文本再给AI,同时给他实体列表,让他找这些实体的关系。

  • 把原来抽离实体那一段的文本再给AI,逐个给它实体,让他针对于一个实体找关系。

第二种应该效果是更好的。

但是既然是要做到通用,那要考虑,与某一个实体有关系的实体是不是只有抽离实体的那一段文本才有呢?

思考:RAG解决问题

为了效果,用一下RAG(检索增强生成)技术,说人话,就是知识库,向量数据库,只把和实体相关的数据喂给大语言模型。

给这波操作起个名字——知识库辅助的知识图谱实体关系抽离。

但是如果要用RAG就免不了要Embedding和向量数据库,而这些东西用Java不好弄,`(*>﹏<*)′

而且用这个还要考虑文本怎么切分的问题。

所以,我想要一个更轻量的解决方案,轻量到在我2G内存的小服务器能跑得动。

思考:不解决问题

啊——我想不到轻量且通用的解决方法,解决不了问题那就解决提出问题的人,我只要保证前面那个条件,与某一个实体有关系的实体是只有抽离实体的那一段文本才有呢?

先暂时不解决这个问题,等最后完全弄完看看召回率再决定要不要弄。


传输控制协议(英语:Transmission Control Protocol,缩写:TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中,它完成第四层传输层所指定的功能。用户数据报协议(UDP)是同一层内另一个重要的传输协议。在因特网协议族(Internet protocol suite)中,TCP层是位于IP层之上,应用层之下的中间层。不同主机的应用层之间经常需要可靠的、像管道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

提示词:

System:命名实体关联关系识别。在每行中输出如下格式:名称: XXX 关系: XXX

User:从输入信息中识别与 ”{{target}}“ 有关联关系的其他命名实体,并输出其他实体名称和其他实体与当前实体的关联关系,关联关系用一个词语表示:{{input}}

[以下是基于给定信息提取的TCP协议相关的命名实体关联关系:, , 名称: IETF(Internet Engineering Task Force)关系: 定义TCP协议, 名称: RFC(Request for Comments)关系: TCP协议由RFC 793定义, 名称: OSI模型关系: TCP完成OSI模型中第四层传输层所指定的功能, 名称: 用户数据报协议(UDP)关系: TCP和UDP都是同一层(传输层)的重要传输协议, 名称: 因特网协议族(Internet protocol suite)关系: TCP位于因特网协议族的IP层之上,应用层之下, 名称: 流机制(Stream mechanism)关系: IP层不提供可靠的流机制,而TCP提供像管道一样的连接, 名称: 包交换(Packet switching)关系: TCP和IP层一起工作以实现可靠的包交换通信协议]

调整提示词···

提示词:

System:NLP任务,命名实体关联关系识别。在每行中输出如下格式:名称: XXX 关系: XXX

User:{{input}}\n与{{target}}相关的其他实体:

[名称: Transmission Control Protocol(传输控制协议) 关系: 面向连接、可靠、基于字节流的传输层通信协议, , 名称: RFC 793 关系: 定义TCP协议的文档, , 名称: IETF 关系: TCP协议的制定和维护组织, , 名称: 用户数据报协议(UDP) 关系: 与TCP协议同为OSI模型中传输层的另一个重要传输协议,与TCP协议相互独立, , 名称: 因特网协议族(Internet protocol suite) 关系: TCP协议是其中的一部分,位于IP层之上,应用层之下, , 名称: 流机制 关系: TCP层提供可靠的、像管道一样的连接,而IP层不提供这样的流机制, , 请注意,以上信息是基于您提供的文本进行实体关系识别的结果,实际中TCP协议还有许多其他相关实体和细节。]

但是无论我怎么调提示词它似乎都不怎么聪明···

退而求其次,让他逐个实体识别吧···

提示词:

System:根据信息分析实体A和实体B的关系。关系使用一个词语总结概括。如果没关系请输出”无“。在一行内输出一个词语使用引号”“包裹。

User:{{input}}\n{{entityA}}和{{entityB}}的关系是什么?

TCP协议和IP层的关系是:“基于”。

先用一个O(n2)的方法解决这个问题。

最终得到了这样的结果:

提示词:

System:根据信息分析实体A和实体B的关系。关系使用一个词语总结概括。

User:{{input}}\n{{entityA}}和{{entityB}}的关系是什么?不要解释,将关系概况成一个词,使用引号包裹。无直接关系就输出“无关系”。

[((传输控制协议, RFC 793), 定义和实现关系), ((传输控制协议, 用户数据报协议), 并列关系), ((传输控制协议, OSI模型), 传输控制协议与OSI模型第四层传输层存在关联), ((传输控制协议, 应用层), TCP层位于IP层之上,应用层之下,因此传输控制协议与应用层有密切关系。), ((传输控制协议, IP层), 基于IP层之上), ((RFC 793, 用户数据报协议), 同一层内的传输协议), ((RFC 793, IP层), 位于IP层之上), ((用户数据报协议, IP层), 基于IP层进行数据传输), ((应用层, IP层), 间接关系)]

这里面有一些关系是要删掉的,而且缺乏概括,后边可能还要再处理。

属性抽离

自由女神像(英语:Statue of Liberty)又名自由照耀世界(英语:Liberty Enlightening the World,法语:La Liberté éclairant le monde),是位于美国纽约港自由岛的巨型古典主义塑像,由弗雷德里克·奥古斯特·巴托尔迪设计,古斯塔夫·埃菲尔建造,1886年10月28日落成,是法国人民送给美国人民的礼物。塑像人物是身穿长袍的女子,代表罗马神话中的自主神,她右手高举火炬,左手的册子上用罗马数字写有美国独立宣言签署日期:“JULY IV MDCCLXXVI”(1776年7月4日),脚下还有断裂的锁链。这座塑像是自由和美国的象征,也是对外来移民的欢迎信号。

这里和关系抽离差不多···

但是这里大语言模型没让人血压那么高

提示词:

System:请根据信息提取与实体相关的属性,同一个属性的属性名和值在同一行输出。每行输出格式为:属性名: XXX 值: XXX

User:{{input}}\n{{entity}}的属性有哪些?

[(名称, 自由女神像和法语名称La Liberté éclairant le monde。), (位置, 美国纽约港自由岛。), (设计者, 弗雷德里克·奥古斯特·巴托尔迪。), (建造者, 古斯塔夫·埃菲尔。), (完成日期, 1886年10月28日。), (塑像人物, 身穿长袍的女子,代表罗马神话中的自主神。), (象征意义, 自由和美国的象征,对外来移民的欢迎信号。), (主要特征, 右手高举火炬,左手的册子上写有美国独立宣言签署日期:“JULY IV MDCCLXXVI”,脚下有断裂的锁链。)]

这个似乎没有什么问题。

测试和总结

跑通单元测试之后,当然还要用户界面,于是我用React+Antd写了个知识图谱构建的操作界面,不包括人工调整部分(这又是个大工程)

经过测试,这套方法构建出来的知识图谱比较一般,需要人为做很多调整。

测试1

我拿洛谷的新用户必读的一部分,这种数据拿来构建知识图谱肯定是不合格的,但还是试了试···

出现了好多因为AI不听话,输出了程序看不懂的东西导致的警告:

因为还没写可视化,就看数据库凑合一下吧( ̄▽ ̄)"

实体表:

关系表:

属性表:

其中,最一言难尽的表是关系表,其中大部分关系要删除,可能方法不是很合适。

测试2

想拿之前做过的青瓷的数据跑一下试试,很快找到了之前爬的数据但看到这个我挺无奈的:

这玩意儿我懒得打开了,我打不开,它旁边有个1.1M的Json,忽然想起,当时爬的时候,这个大的Json是爬的原始数据放进去了(网页直接丢进去),小的那个是提取了有用的信息。信噪比0.23%。

这里面一共868条数据,跑怎么多不知道跑到什么时候,而且跑一半崩了也不是不可能,先拿20条跑···

和上次一样,出现了大量的这个警告:

AI输出的时候把名称和类别之间加了换行,属性抽离单元测试时候老是出这个问题,我是考虑了的,但是实体抽离就没写

最后我得到了一个错误:

额···他说插入实体的时候ID冲突了···

但是ID不是数据库说了算嘛?

突然发现ID都很奇怪,为什么又乱又长?因为数据对象偷懒主键没写注解

因为@TableId(type = IdType.AUTO) 注解没写。

这玩意儿本来我是写的,后来看文档说如果主键名叫id就不用写(#°Д°)

我这运气也是,插百来个数据,居然随机ID会冲突。

话说我为什么要先构建完再存数据库呢?不能边构建边存吗?下一步怎么改,有方向了。

重新开始,但这次我只放三条···

实体表:

关系表:

图片-mqfg.png

属性表:

属性抽取效果很不错,关系抽取嘛···

总结

还有很大的改进空间。

在程序逻辑上:

  • 增加更多判断条件去读取AI的回复

  • 边构建边写数据库

在图谱构建上:

  • 实体抽取要约束,不然会抽取出来不需要的和错误的实体,并且会拖慢后续步骤

  • 实体关系抽取方法要改进,现在即慢又烧钱效果还不好

我能想到的,最大的成功就是无愧于自己的心。