知识图谱实验一

[TOC]

实验一 知识图谱初步认识:

实验目的

  • 掌握图数据库运行环境的安装和配置
  • 图中节点和链接的创建
  • 知识图谱的创建流程以及可视化展示

实验内容

  1. 安装 jdk;
  2. 安装 Neo4j;
  3. 安装 Python 和 Pycharm 编辑器;
  4. 下载同学爱好数据;
  5. 构建图谱关系;
  6. 图谱三元组导入。

实验原理

Neo4j

介绍

Neo4j是一个高性能的NOSQL图形数据库,它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事务特性的Java持久化引擎,但是它将结构化数据存储在网络(从数学角度叫做图)上而不是表中。Neo4j也可以被看作是一个高性能的图引擎,该引擎具有成熟数据库的所有特性。程序员工作在一个面向对象的、灵活的网络结构下,而不是严格、静态的表中。但是他们可以享受到具备完全的事务特性、企业级的数据库的所有好处。 Neo4j因其嵌入式、高性能、轻量级等优势,越来越受到关注。

图形数据结构

在一个图中包含两种基本的数据类型:Nodes(节点) 和 Relationships(关系)。Nodes 和 Relationships 包含key/value形式的属性。Nodes通过Relationships所定义的关系相连起来,形成关系型网络结构。

网络结构图

查询语言

neo4j采用自己设计的查询语言cypher,其特点和sql有很多相似的地方。match、where、return是最常用到的关键词:

  • match: 相当于 sql中的select,用来说明查询匹配的数据模式(或者说图模式 );
  • where: 用来限制node或者关系中部分属性的属性值,从而返回我们想要的数据;
  • return: 返回节点或者关系。

数据导入

  1. 把文件放到neo4j根目录下的import文件夹内,使用LOAD......AS ROW语句读取。
  2. 通过 Python 中的 py2neo 进行导入。

小组分工

单人完成,无具体分工。

实验流程及结果分析

相关软件的安装及配置

  • jdk 安装完成。

image-20220517140840628

  • Neo4j 安装完成。

image-20220517140854071

  • Pycharm 安装及相关配置完成。

image-20220517141311987

图数据库的构建与运用

原数据的清洗与分析

数据清洗,即是重新检查和验证数据的过程,目的在于删除重复信息,纠正现有错误并且提供数据一致性。

与普通的数据不同,我们并不需要对这个数据进行过多的清洗分析(如清除过大/过小值,查看均值等),但是我们通过原数据进行查看可以得知,这个数据的 hobbies项存在多个数值,如下:

姓名 学号 兴趣爱好
小李 007 高雅艺术,欧洲文学,人生哲学
小红 008 美食,手工,看书,听音乐

此种格式的数据会对我们后续的处理过程造成麻烦。

同时,由于统计的同学数目较多且并没有统一规范输入的格式,因此此不同的同学 “兴趣爱好” 的差异较大。

处理过程如下:

首先通过 python 中的 pandas 包将 hobbeis.csv文件导入,

import pandas
datas = pandas.read_csv(".\hobbies.csv", encoding="gbk")

此处注意文件导入时的编码格式应采用”gbk”;

print(datas)

输出的数据如下:

 name        id            hobbies
0   南佳霖  19011402     高雅艺术,欧洲文学,人生哲学
1   毛泓涛  19011443     程序设计,美食品鉴,影视欣赏
2   赵松青  19011418     时事政治,佳肴美酒,美与生活
3   程相龙  19011401  听音乐,看电影,打篮球,rushB
4   赵琰晴  19011385          看电影,阅读,听歌
..  ...       ...                ...
71   黄坚  19011438           运动,音乐,追剧
72  杨雨欣  19011388           音乐、篮球、阅读
73  李婷谊  19011442          音乐,阅读,美食,
74   莫婼  19011455              画画,游戏
75   苏玥  19011383           看书,音乐,电影

对数据进行处理,清除其中全为 NAN 的行和全为 NAN 的列;

#去掉全为 NAN 的行
datas = datas.dropna(axis=0, how = "all")
#去掉全为 NAN 的列
datas = datas.dropna(axis = 1, how = "all")

根据对原数据的观察和统计,发现同学使用 (中文逗号)对兴趣爱好进行划分最多,因此先将其余的划分方式进行统一;

    for index,row in datas.iterrows():
        #print(row["hobbies"])
        row.iloc[2] = row["hobbies"].replace(",", ",")
        row.iloc[2] = row["hobbies"].replace(" ", ",")
        row.iloc[2] = row["hobbies"].replace("、", ",")

然后再将兴趣爱好进行划分;

row.iloc[2] = row["hobbies"].split(",")

部分输出结果如下:

['程序设计', '美食品鉴', '影视欣赏']
['时事政治', '佳肴美酒', '美与生活']
['听音乐', '看电影', '打篮球', 'rushB']
['看电影', '阅读', '听歌']
['唱歌', '篮球', '阅读', '演讲']
['美食', '电影', '乒乓球']

当我们认为已经要结束的时候,其实还远远没有呢(

我们可以发现,在兴趣爱好中会出现“看电影” 和 “欣赏电影” 或是“电影”等字眼,对于我们人来说,很容易就可以将这些数据默认为同一个兴趣爱好,即“看电影”,而对于机器而言则很难去做到。因此我们还需要做的是对实体进行统一(Entity Resolution)。

实体统一过程

由于这是一个小实验且给的时间有限,因此并没有考虑过要通过机器学习/模型训练的方法来进行实体的统一。当然,直接去网上抄袭别人的代码也是可耻的。最后决定采用 Dedupe 库 来对实体进行统一。具体的操作过程如下:

首先进行包的安装

需要安装的包共有3个,分别为:future, dedupe, unidecode;

简单的通过 pip install 安装即可,若出现网络问题可走镜像。

image-20220518205702452

原数据导出

将原有的兴趣爱好汇聚成一个数组,并最终转化为 DataFrame类型,写入 .csv 文件。

arr = []
for index,row in datas.iterrows():
	row.iloc[2] = row["hobbies"].replace(",", ",")
    row.iloc[2] = row["hobbies"].replace(" ", ",")
    row.iloc[2] = row["hobbies"].replace("、", ",")

    row.iloc[2] = row["hobbies"].split(",")
   	arr += [x for x in row.iloc[2] if x != ""]
    #if 判断条件用于删除空白值


其中,arr 的部分输出如下:

['高雅艺术', '欧洲文学', '人生哲学', '程序设计', '美食品鉴', '影视欣赏', '时事政治', '佳肴美酒', '美与生活', '听音乐'......]

导出 csv 文件:

my_dict = {"Name": arr}
df = pandas.DataFrame(my_dict)
df.to_csv("test.csv",encoding='utf_8_sig')

此处注意导出文件的编码格式,否则会导致乱码;

结果如下:

image-20220518212429042

训练过程:

参照 dedupe 源文件中的 example可知,训练过程大致如下:

定义模型需要注意的字段:

fields = [
            {'field': '\ufeffId', 'type':"Text"},
            {'field': "Name", 'type': 'String'}
            #{'field': 'Site name', 'type': 'String'},
            #{'field': 'Address', 'type': 'String'},
            #{'field': 'Zip', 'type': 'Exact', 'has missing': True},
            #{'field': 'Phone', 'type': 'String', 'has missing': True},
            ]

注意,这里的type 只能从下述中取值:·Categorical, Exists, ShortString, String, Text, DateTime, Exact, LatLong, Price, Set

导入数据,进行抽样;

deduper.prepare_training(data_d)

进行 Active learning;此步骤即是确保后续我们与terminal 之间的交互,让模型抛出其不确定的样本对并且让我们进行判断。

进行训练

deduper.train()

完整代码如下:

#!/usr/bin/python
# -*- coding: utf-8 -*-


import os
import csv
import re
import logging
import optparse

import dedupe
from unidecode import unidecode


def preProcess(column):
    """
    Do a little bit of data cleaning with the help of Unidecode and Regex.
    Things like casing, extra spaces, quotes and new lines can be ignored.
    """
    column = unidecode(column)
    column = re.sub('  +', ' ', column)
    column = re.sub('\n', ' ', column)
    column = column.strip().strip('"').strip("'").lower().strip()
    # If data is missing, indicate that by setting the value to `None`
    if not column:
        column = None
    return column


def readData(filename):
    """
    Read in our data from a CSV file and create a dictionary of records,
    where the key is a unique record ID and each value is dict
    """

    data_d = {}
    with open(filename, encoding="utf-8") as f:
        reader = csv.DictReader(f)
        for row in reader:
            clean_row = [(k, preProcess(v)) for (k, v) in row.items()]
            print(clean_row)
            print("row:", row)
            row_id = int(row['\ufeffId'])
            data_d[row_id] = dict(clean_row)

    return data_d


if __name__ == '__main__':

    # ## Logging

    # Dedupe uses Python logging to show or suppress verbose output. This
    # code block lets you change the level of loggin on the command
    # line. You don't need it if you don't want that. To enable verbose
    # logging, run `python examples/csv_example/csv_example.py -v`
    optp = optparse.OptionParser()
    optp.add_option('-v', '--verbose', dest='verbose', action='count',
                    help='Increase verbosity (specify multiple times for more)'
                    )
    (opts, args) = optp.parse_args()
    log_level = logging.WARNING
    if opts.verbose:
        if opts.verbose == 1:
            log_level = logging.INFO
        elif opts.verbose >= 2:
            log_level = logging.DEBUG
    logging.getLogger().setLevel(log_level)

    # ## Setup

    #input_file = 'csv_example_messy_input.csv'
    #output_file = 'csv_example_output.csv'
    input_file = "test.csv"
    output_file = "testout.csv"
    settings_file = 'csv_example_learned_settings'
    training_file = 'csv_example_training.json'

    print('importing data ...')
    data_d = readData(input_file)

    # If a settings file already exists, we'll just load that and skip training
    if os.path.exists(settings_file):
        print('reading from', settings_file)
        with open(settings_file, 'rb') as f:
            deduper = dedupe.StaticDedupe(f)
    else:
        # ## Training

        # Define the fields dedupe will pay attention to
        fields = [
            {'field': '\ufeffId', 'type':"Text"},
            {'field': "Name", 'type': 'String'}
            #{'field': 'Site name', 'type': 'String'},
            #{'field': 'Address', 'type': 'String'},
            #{'field': 'Zip', 'type': 'Exact', 'has missing': True},
            #{'field': 'Phone', 'type': 'String', 'has missing': True},
            ]

        # Create a new deduper object and pass our data model to it.
        deduper = dedupe.Dedupe(fields)

        # If we have training data saved from a previous run of dedupe,
        # look for it and load it in.
        # __Note:__ if you want to train from scratch, delete the training_file
        if os.path.exists(training_file):
            print('reading labeled examples from ', training_file)
            with open(training_file, 'rb') as f:
                deduper.prepare_training(data_d, f)
        else:
            deduper.prepare_training(data_d)

        # ## Active learning
        # Dedupe will find the next pair of records
        # it is least certain about and ask you to label them as duplicates
        # or not.
        # use 'y', 'n' and 'u' keys to flag duplicates
        # press 'f' when you are finished
        print('starting active labeling...')

        dedupe.console_label(deduper)

        # Using the examples we just labeled, train the deduper and learn
        # blocking predicates
        deduper.train()

        # When finished, save our training to disk
        with open(training_file, 'w') as tf:
            deduper.write_training(tf)

        # Save our weights and predicates to disk.  If the settings file
        # exists, we will skip all the training and learning next time we run
        # this file.
        with open(settings_file, 'wb') as sf:
            deduper.write_settings(sf)

    # ## Clustering

    # `partition` will return sets of records that dedupe
    # believes are all referring to the same entity.

    print('clustering...')
    clustered_dupes = deduper.partition(data_d, 0.5)

    print('# duplicate sets', len(clustered_dupes))
    print(clustered_dupes)
    # ## Writing Results

    # Write our original data back out to a CSV with a new column called
    # 'Cluster ID' which indicates which records refer to each other.

    cluster_membership = {}
    for cluster_id, (records, scores) in enumerate(clustered_dupes):
        for record_id, score in zip(records, scores):
            cluster_membership[record_id] = {
                "Cluster ID": cluster_id,
                "confidence_score": score
            }
    print()
    print(cluster_membership)

    with open(output_file,  "w",encoding="utf_8_sig") as f_output, open(input_file, encoding="utf-8") as f_input:

        reader = csv.DictReader(f_input)
        fieldnames = ['Cluster ID', 'confidence_score'] + reader.fieldnames

        writer = csv.DictWriter(f_output, fieldnames=fieldnames)
        writer.writeheader()

        for row in reader:
            row_id = int(row['\ufeffId'])
            row.update(cluster_membership[row_id])
            writer.writerow(row)

交互过程如下:

image-20220518221310143

image-20220518221327943

image-20220518222507868

训练完成后,最终输出文件如下:

image-20220518223255873

其中, Cluster ID 相同的 爱好会被认为是同一个爱好, confidence_score 则是他们被认为相同的置信度,从部分数据我们可以看出,虽然模型较小且训练时间较少,该数据仍然具有可信度。

image-20220518223428010

image-20220518223444844

实体统一过后,将相关的数据存储在字典中,数据格式为:{cluster_id: “hobbies”},方便接下来数据的运用和图数据库的创建;

result_df = pandas.read_csv("testout.csv", encoding="utf-8")
    cluster_id_dict = {}
    #创建字典,方便查找
    for index, row in result_df.iterrows():
        key = row["Cluster ID"]
        value = row["Name"]

        if key in cluster_id_dict.keys():
            cluster_id_dict[key].add(value)
        else:
            cluster_id_dict[key] = set()
            cluster_id_dict[key].add(value)

value 值使用的数据结构为集合,以排除可能会多次重复的兴趣值,以减少数据存储的占用。

部分输出如下:

{33: {'高雅艺术'}, 34: {'欧洲文学'}, 35: {'人生哲学'}, 36: {'程序设计'}, 0: {'美食品鉴', '美食', '美妆', '美与生活'}, 1: {'影视', '影视鉴赏',......}

此说明,美食品鉴、美食、美妆、美与生活为同一个实体,影视、影视鉴赏为同一个实体,等…

接下来进行图数据库的创建;

图数据库创建

若通过 python 创建图数据库,主要使用的包是:py2neo

首先使用 py2neo 中的 Graph 创建与 Neo4j 的连接,其中第一项参数为 HTTP port,后面两项依次是你的用户名和连接的库的密码;(由于隐私问题,将下述代码中的密码用 * 代替)。

g = Graph('http://localhost:7474', user='neo4j', password='*********')

使用 Node 创建节点,其中两个参数分别对应 Neo4j 中的两个属性,即 labelname,根据需要创建即可;

在此处创建一个used_id 数组来存储使用过的 cluster_id,在创建 Hobby 节点时,在使用上一个步骤中所创建的字典,即可达到实体统一的目的。

使用 Relationship 创建节点之间的关系,参数为(Node, re, Node);

完整代码如下:

g = Graph('http://localhost:7474', user='neo4j', password='*********')
used_id = []
    for index, row in datas.iterrows():
        name = row["name"]
        id = row["id"]
        name_node = Node("Person", name=name)
        g.create(name_node)
        #暂时不存储 id_node(学号)
        # id_node = Node("A", name=id)
        # g.create(id_node)
        # re = Relationship(name_node, "学号", id_node)
        # g.create(re)
        for hobby in row["hobbies"]:
            temp_cluster_id = -1
            for key, value in cluster_id_dict.items():
                if hobby in value:
                    temp_cluster_id = key
                    if temp_cluster_id in used_id:    #之前已经用过
                        hobby = list(value)[0]                                        #统一使用第一个元素
                    else:                                                       #没用过
                       used_id.append(key)
                    break                             #找到即只有一个,跳出

            hobby_node = Node("Hobby", name=hobby)
            g.create(hobby_node)
            re = Relationship(name_node, "LIKES", hobby_node)
            g.create(re)

创建的图数据库如下:

image-20220518233923692

image-20220518234610771

遇到的问题与解决方法

在整个的实验过程中,遇到不少问题;其中部分通过查询互联网解答,部分通过咨询毛同学而得到答案。对于整个实验流程比较满意,但是做完之后看下来,也有需要改进和提高的地方。

实验总结

通过这次实验,我学会了 Neo4j 图数据库的创建和数据的导入,同时学会了使用 dedupe 来达到统一实体的目的;更重要的是在这个过程中,提高了自己的动手能力和实践能力,使我收益颇丰。