• SpringBoot 整合 Neo4j


    知识图谱技术

    三要素

    在知识图谱中,通过三元组 <实体 × 关系 × 属性> 集合的形式来描述事物之间的关系:

    • 实体:又叫作本体,指客观存在并可相互区别的事物,可以是具体的人、事、物,也可以是抽象的概念或联系,实体是知识图谱中最基本的元素

    • 关系:在知识图谱中,边表示知识图谱中的关系,用来表示不同实体间的某种联系

    • 属性:知识图谱中的实体和关系都可以有各自的属性

    这里所说的实体和普通意义上的实体略有不同,借用 NLP 中本体的概念来理解它会比较好:

    本体定义了组成主题领域的词汇表的基本术语及其关系,以及结合这些术语和关系来定义词汇表外延的规则

    例如我们要描述大学这一领域时,对它来说教工学生课程就是相对比较重要的概念,并且教工和学生之间也存在一定的关联关系,此外对象之间还存在一定的约束关系,例如一个系的教职员工数量不能少于 10 人。

    在了解了上面的三元组后,我们可以基于它构建下面这样的一个关系:

    可以看到,女王和王储通过母子关系关联在一起,并且每个人拥有自己的属性。

    当知识图谱中的节点逐渐增多后,它的表现形式就会类似于化学分子式的结构,一个知识图谱往往存在多种类型的实体与关系。

    知识图谱将非线性世界中的知识信息进行加工,做到这样的结构化、可视化,从而辅助人类进行推理、预判、归类。

    到这里,可以简单概括一下知识图谱的基本特征:

    • 知识结构网络化

    • 网络结构复杂

    • 网络由三元组构成

    • 数据主要由知识库承载

    场景

    搜索

    前面提到过,以前的搜索引擎是从海量的关键词中找出与查询匹配度最高的内容,按照查询结果把排序分值最高的一些结果返回给用户。在整个过程中,搜索引擎可能并不需要知道用户输入的是什么,因为系统不具备推理能力,在精准搜索方面也略显不足。而基于知识图谱的搜索引擎,除了能够直接回答用户的问题外,还具有一定的语义推理能力,大大提高了搜索的精确度。

    推荐

    在传统的推荐系统中,存在两个典型问题:

    • 数据稀疏问题:在实际应用场景中,用户和物品的交互信息往往是非常稀疏的,预测会产生过拟合风险

    • 冷启动问题:对于新加入的用户或者物品,由于系统没有其历史交互信息,因此无法进行准确地建模和推荐

    例如,在一个电影类网站中可能包含了上万部电影,然而一个用户打过分的电影可能平均只有几十部。使用如此少量的已观测数据来预测大量的未知信息,会极大地增加算法的过拟合风险。

    因此在推荐算法中会额外引入一些辅助信息作为输入,这些辅助信息可以丰富对用户和物品的描述,从而有效地弥补交互信息的稀疏或缺失。在各种辅助信息中,知识图谱作为一种新兴类型的辅助信息,这几年的相关研究比较多。

    下面就是一个基于知识图谱的推荐例子:

    在将知识图谱引入推荐系统后,具有以下优势:

    • 精确性:知识图谱为物品引入了更多的语义关系,可以深层次地发现用户兴趣

    • 多样性:知识图谱提供了实体之间不同的关系连接种类,有利于推荐结果的发散,避免推荐结果局限于单一类型

    • 可解释性:知识图谱可以连接用户的历史记录和推荐结果,从而提高用户对推荐结果的满意度和接受度,增强用户对推荐系统的信

    此外,知识图谱技术还在问答与对话系统、语言理解、决策分析等多个领域被广泛应用,它被挂载在这些系统之后,充当背景知识库的角色。总的来说,在这些场景下的应用,可以概括整个 AI 的发展趋势,就是从感知认知的一个过程。

    关系抽取

    回顾一下我们前面提到过的知识图谱三要素,分别是实体、关系和属性。关系抽取我们同样可以用一个三元组表示的RDF graph

    这样的一个(S,P,O)三元组,就可以将一份知识分解为主语、谓语、宾语。这样的 SPO 结构,在配合知识图谱进行存储时可以被用来当做存储单元。

    流程与算法

    在知识图谱的搭建过程中,仍然面临着各类算法难点,主要难点可归结为生产流程中的算法难点和算法性能上的难点。前者体现为知识获取受数据集限制、知识融合干扰因素较多、知识计算的数据集与算力不足等问题。

    而后者体现为算法泛化能力不足、鲁棒性不足、缺乏统一测评指标等问题。算法上的难点有赖于供需双方、学术界、政府持续攻坚,而非一方努力即可收获成功。

    Neo4j 实战

    我们上面介绍了关于知识图谱的一些基本理论知识,俗话说的好,光说不练假把式。

    下面将通过下面几个主要模块,构建自然界中实体间的联系,实现知识图谱描述:

    • 图数据库 Neo4j 安装

    • 简单 CQL 入门

    • Spring Boot 整合 Neo4j

    • 文本 SPO 抽取

    • 动态构建知识图谱

    Neo4j 安装

    知识图谱的底层依赖于关键的图数据库,在这里我们选择 Neo4j,它是一款高性能的 nosql 图形数据库,能够将结构化的数据存储在而不是中。

    首先进行安装,打开官网下载 Neo4j 的安装包,下载免费的 community 社区版就可以,地址放在下面:

    https://Neo4j.com/download/other-releases/

    需要注意的是,Neo4j 4.x 以上的版本都需要依赖 jdk11 环境,所以如果运行环境是 jdk8 的话,那么还是老老实实下载 3.x 版本就行,下载解压完成后,在bin目录下通过命令启动:

    Neo4console
    

    启动后在浏览器访问安装服务器的 7474 端口,就可以打开 Neo4j 的控制台页面:

    通过左侧的导航栏,我们依次可以查看存储的数据、一些基础查询的示例以及一些帮助说明。

    而顶部带有$符号的输入框,可以用来输入 Neo4j 特有的 CQL 查询语句并执行,具体的语法我们放在下面介绍。

    简单 CQL 入门

    就像我们平常使用关系型数据库中的 SQL 语句一样,Neo4j 中可以使用 Cypher 查询语言(CQL)进行图形数据库的查询,我们简单来看一下增删改查的用法。

    添加节点

    在 CQL 中,可以通过CREATE命令去创建一个节点,创建不含有属性节点的语法如下:

    CREATE (:)
    

    CREATE语句中,包含两个基础元素,节点名称node-name和标签名称lable-name。标签名称相当于关系型数据库中的表名,而节点名称则代指这一条数据。

    以下面的CREATE语句为例,就相当于在Person这张表中创建一条没有属性的空数据。

    CREATE (索尔:Person)
    

    而创建包含属性的节点时,可以在标签名称后面追加一个描绘属性的json字符串:

    1. CREATE (
    2. <node-name>:<label-name>
    3. {
    4. <key1>:<value1>,
    5. <keyN>:<valueN>
    6. }
    7. )

    用下面的语句创建一个包含属性的节点:

    CREATE (洛基:Person {name:"洛基",title:"诡计之神"})
    

    查询节点

    在创建完节点后,我们就可以使用MATCH匹配命令查询已存在的节点及属性的数据,命令的格式如下:

    MATCH (:)
    

    通常,MATCH命令在后面配合RETURNDELETE等命令使用,执行具体的返回或删除等操作。

    执行下面的命令:

    MATCH (p:Person) RETURN p
    

    查看可视化的显示结果:

    可以看到上面添加的两个节点,分别是不包含属性的空节点和包含属性的节点,并且所有节点会有一个默认生成的id作为唯一标识。

    删除节点

    接下来,我们删除之前创建的不包含属性的无用节点,上面提到过,需要使用MATCH配合DELETE进行删除。

    1. MATCH (p:Person) WHERE id(p)=100
    2. DELETE p

    在这条删除语句中,额外使用了WHERE过滤条件,它与 SQL 中的WHERE非常相似,命令中通过节点的id进行了过滤。

    删除完成后,再次执行查询操作,可以看到只保留了洛基这一个节点:

    添加关联

    在 Neo4j 图数据库中,遵循属性图模型来存储和管理数据,也就是说我们可以维护节点之间的关系。

    在上面,我们创建过一个节点,所以还需要再创建一个节点作为关系的两端:

    CREATE (p:Person {name:"索尔",title:"雷神"})
    

    创建关系的基本语法如下:

    1. CREATE (:)
    2. - [:]
    3. -> (:)

    当然,也可以利用已经存在的节点创建关系,下面我们借助MATCH先进行查询,再将结果进行关联,创建两个节点之间的关联关系:

    1. MATCH (m:Person),(n:Person)
    2. WHERE m.name='索尔' and n.name='洛基'
    3. CREATE (m)-[r:BROTHER {relation:"无血缘兄弟"}]->(n)
    4. RETURN r

    添加完成后,可以通过关系查询符合条件的节点及关系:

    1. MATCH (m:Person)-[re:BROTHER]->(n:Person)
    2. RETURN m,re,n

    可以看到两者之间已经添加了关联:

    需要注意的是,如果节点被添加了关联关系后,单纯删除节点的话会报错,:

    1. Neo.ClientError.Schema.ConstraintValidationFailed
    2. Cannot delete node<85>, because it still has relationships. To delete this node, you must first delete its relationships.

    这时,需要在删除节点时同时删除关联关系:

    1. MATCH (m:Person)-[r:BROTHER]->(n:Person)
    2. DELETE m,r

    执行上面的语句,就会在删除节点的同时,删除它所包含的关联关系了。

    那么,简单的 cql 语句入门到此为止,它已经基本能够满足我们的简单业务场景了,下面我们开始在 springboot 中整合 Neo4j。

    SpringBoot 整合 Neo4j

    创建一个 springboot 项目,这里使用的是2.3.4版本,引入 Neo4j 的依赖坐标:

    1. <dependency>
    2.     <groupId>org.springframework.boot</groupId>
    3.     <artifactId>spring-boot-starter-data-Neo4j</artifactId>
    4. </dependency>

    application.yml中配置 Neo4j 连接信息:

    1. spring:
    2.   data:
    3.     Neo4j:
    4.       uri: bolt://127.0.0.1:7687
    5.       username: Neo4j
    6.       password: 123456

    大家如果对jpa的应用非常熟练的话,那么接下来的过程可以说是轻车熟路,因为它们基本上是一个模式,同样是构建 model 层、repository 层,然后在此基础上操作自定义或模板方法就可以了。

    节点实体

    我们可以使用基于注解的实体映射来描述图中的节点,通过在实体类上添加@NodeEntity表明它是图中的一个节点实体,在属性上添加@Property代表它是节点中的具体属性。

    1. @Data
    2. @NodeEntity(label = "Person")
    3. public class Node {
    4.     @Id
    5.     @GeneratedValue
    6.     private Long id;
    7.     @Property(name = "name")
    8.     private String name;
    9.     @Property(name = "title")
    10.     private String title;
    11. }

    这样一个实体类,就代表它创建的实例节点的Person,并且每个节点拥有nametitle两个属性。

    Repository 持久层

    对上面的实体构建持久层接口,继承Neo4jRepository接口,并在接口上添加@Repository注解即可。

    1. @Repository
    2. public interface NodeRepository extends Neo4jRepository {
    3.     @Query("MATCH p=(n:Person) RETURN p")
    4.     List selectAll();
    5.     @Query("MATCH(p:Person{name:{name}}) return p")
    6.     Node findByName(String name);
    7. }

    在接口中添加了个两个方法,供后面测试使用,selectAll()用于返回全部数据,findByName()用于根据name查询特定的节点。

    接下来,在 service 层中调用 repository 层的模板方法:

    1. @Service
    2. @AllArgsConstructor
    3. public class NodeServiceImpl implements NodeService {
    4.     private final NodeRepository nodeRepository;
    5.     @Override
    6.     public Node save(Node node) {
    7.         Node save = nodeRepository.save(node);
    8.         return save;
    9.     }
    10. }

    前端调用save()接口,添加一个节点后,再到控制台用查询语句进行查询,可以看到新的节点已经通过接口方式被添加到了图中:

    在 service 中再添加一个方法,用于查询全部节点,直接调用我们在NodeRepository中定义的selectAll()方法:

    1. @Override
    2. public List<Node> getAll() {
    3.     List<Node> nodes = nodeRepository.selectAll();
    4.     nodes.forEach(System.out::println);
    5.     return nodes;
    6. }

    在控制台打印了查询结果:

    图片

    对节点的操作我们就介绍到这里,接下来开始构建节点间的关联关系。

    关联关系

    在 Neo4j 中,关联关系其实也可以看做一种特殊的实体,所以可以用实体类来对其进行描述。与节点不同,需要在类上添加@RelationshipEntity注解,并通过@StartNode@EndNode指定关联关系的开始和结束节点。

    1. @Data
    2. @RelationshipEntity(type = "Relation")
    3. public class Relation {
    4.     @Id
    5.     @GeneratedValue
    6.     private Long id;
    7.     @StartNode
    8.     private Node startNode;
    9.     @EndNode
    10.     private Node endNode;
    11.     @Property
    12.     private String relation;
    13. }

    同样,接下来也为它创建一个持久层的接口:

    1. @Repository
    2. public interface RelationRepository extends Neo4jRepository {
    3.     @Query("MATCH p=(n:Person)-[r:Relation]->(m:Person) " +
    4.             "WHERE id(n)={startNode} and id(m)={endNode} and r.relation={relation}" +
    5.             "RETURN p")
    6.     List findRelation(@Param("startNode") Node startNode,
    7.                                 @Param("endNode") Node endNode,
    8.                                 @Param("relation") String relation);
    9. }

    在接口中自定义了一个根据起始节点、结束节点以及关联内容查询关联关系的方法,我们会在后面用到。

    创建关联

    在 service 层中,创建提供一个根据节点名称构建关联关系的方法:

    1. @Override
    2. public void bind(String name1String name2String relationName) {
    3.     Node start = nodeRepository.findByName(name1);
    4.     Node end = nodeRepository.findByName(name2);
    5.     Relation relation =new Relation();
    6.     relation.setStartNode(start);
    7.     relation.setEndNode(end);
    8.     relation.setRelation(relationName);
    9.     relationRepository.save(relation);
    10. }

    通过接口调用这个方法,绑定海拉索尔之间的关系后,查询结果:

    文本 SPO 抽取

    在项目中构建知识图谱时,很大一部分场景是基于非结构化的数据,而不是由我们手动输入确定图谱中的节点或关系。因此,我们需要基于文本进行知识抽取的能力,简单来说就是要在一段文本中抽取出 SPO 主谓宾三元组,来构成图谱中的点和边。

    这里我们借助 Git 上一个现成的工具类,来进行文本的语义分析和 SPO 三元组的抽取工作,项目地址:

    https://download.csdn.net/download/m0_37935175/86578896

    这个项目虽然比较简单一共就两个类两个资源文件,但其中的工具类却能够有效帮助我们完成句子中的主谓宾的提取,使用它前需要先引入依赖的坐标:

    1. <dependency>
    2.     <groupId>com.hankcs</groupId>
    3.     <artifactId>hanlp</artifactId>
    4.     <version>portable-1.2.4</version>
    5. </dependency>
    6. <dependency>
    7.     <groupId>edu.stanford.nlp</groupId>
    8.     <artifactId>stanford-parser</artifactId>
    9.     <version>3.3.1</version>
    10. </dependency>

    然后把这个项目中com.hankcs.nlp.lex包下的两个类拷到我们的项目中,把resources下的models目录拷贝到我们的resources下。

    完成上面的步骤后,调用MainPartExtractor工具类中的方法,进行一下简单的文本 SPO 抽取测试:

    1. public void mpTest(){
    2.     String[] testCaseArray = {
    3.             "我一直很喜欢你",
    4.             "你被我喜欢",
    5.             "美丽又善良的你被卑微的我深深的喜欢着……",
    6.             "小米公司主要生产智能手机",
    7.             "他送给了我一份礼物",
    8.             "这类算法在有限的一段时间内终止",
    9.             "如果大海能够带走我的哀愁",
    10.             "天青色等烟雨,而我在等你",
    11.             "我昨天看见了一个非常可爱的小孩"
    12.     };
    13.     for (String testCase : testCaseArray) {
    14.         MainPart mp = MainPartExtractor.getMainPart(testCase);
    15.         System.out.printf("%s   %s   %s \n",
    16.                 GraphUtil.getNodeValue(mp.getSubject()),
    17.                 GraphUtil.getNodeValue(mp.getPredicate()),
    18.                 GraphUtil.getNodeValue(mp.getObject()));
    19.     }
    20. }

    在处理结果MainPart中,比较重要的是其中的subjectpredicateobject三个属性,它们的类型是TreeGraphNode,封装了句子的主谓宾语成分。下面我们看一下测试结果:

    可以看到,如果句子中有明确的主谓宾语,那么则会进行抽取。如果某一项为空,则该项为null,其余句子结构也能够正常抽取。

    动态构建知识图谱

    在上面的基础上,我们就可以在项目中动态构建知识图谱了,新建一个TextAnalysisServiceImpl,其中实现两个关键方法。

    首先是根据句子中抽取的主语或宾语在 Neo4j 中创建节点的方法,这里根据节点的name判断是否为已存在的节点,如果存在则直接返回,不存在则添加:

    1. private Node addNode(TreeGraphNode treeGraphNode){
    2.     String nodeName = GraphUtil.getNodeValue(treeGraphNode);
    3.     Node existNode = nodeRepository.findByName(nodeName);
    4.     if (Objects.nonNull(existNode))
    5.         return existNode;
    6.     Node node =new Node();
    7.     node.setName(nodeName);
    8.     return nodeRepository.save(node);
    9. }

    然后是核心方法,说白了也很简单,参数传进来一个句子作为文本先进行 spo 的抽取,对实体进行Node的保存,再查看是否已经存在同名的关系,如果不存在则创建关联关系,存在的话则不重复创建。下面是关键代码:

    1. @Override
    2. public List<Relation> parseAndBind(String sentence) {
    3.     MainPart mp = MainPartExtractor.getMainPart(sentence);
    4.     TreeGraphNode subject = mp.getSubject();    //主语
    5.     TreeGraphNode predicate = mp.getPredicate();//谓语
    6.     TreeGraphNode object = mp.getObject();      //宾语
    7.     if (Objects.isNull(subject) || Objects.isNull(object))
    8.         return null;
    9.     Node startNode = addNode(subject);
    10.     Node endNode = addNode(object);
    11.     String relationName = GraphUtil.getNodeValue(predicate);//关系词
    12.     List<Relation> oldRelation = relationRepository
    13.             .findRelation(startNode, endNode,relationName);
    14.     if (!oldRelation.isEmpty())
    15.         return oldRelation;
    16.     Relation botRelation=new Relation();
    17.     botRelation.setStartNode(startNode);
    18.     botRelation.setEndNode(endNode);
    19.     botRelation.setRelation(relationName);
    20.     Relation relation = relationRepository.save(botRelation);
    21.     return Arrays.asList(relation);
    22. }

    创建一个简单的 controller 接口,用于接收文本:

    1. @GetMapping("parse")
    2. public List<Relationparse(String sentence) {
    3.     return textAnalysisService.parseAndBind(sentence);
    4. }

    接下来,我们从前端传入下面几个句子文本进行测试:

    1. 海拉又被称为死亡女神
    2. 死亡女神捏碎了雷神之锤
    3. 雷神之锤属于索尔

    调用完成后,我们再来看看 Neo4j 中的图形关系,可以看到海拉死亡女神索尔这些实体被关联在了一起:

    到这里,一个简单的文本处理和图谱创建的流程就被完整的串了起来,但是这个流程还是比较粗糙

  • 相关阅读:
    esbuild中文文档-路径解析配置项(Path resolution - External、Main fields)
    NMS(Python实现)
    Python A 组 G 题,全排列的价值 (AC)
    java计算机毕业设计中国民航酒店分销系统源码+系统+lw+数据库+调试运行
    python实验10_文件与异常Ⅱ
    Win11打不开exe应用程序怎么办?Win11无法打开exe程序解决方法
    使用canvas做了一个最简单的网页版画板,5分钟学会
    【C语言百炼成神】功法四 · 数组
    【C++】string类超详细解析
    交换机端口镜像详解
  • 原文地址:https://blog.csdn.net/m0_37935175/article/details/126957210