starter_code.ipynb 32.6 KB

利用信息抽取技术搭建知识库

在这个notebook文件中,有些模板代码已经提供给你,但你还需要实现更多的功能来完成这个项目。除非有明确要求,你无须修改任何已给出的代码。以'【练习】'开始的标题表示接下来的代码部分中有你需要实现的功能。这些部分都配有详细的指导,需要实现的部分也会在注释中以'TODO'标出。请仔细阅读所有的提示。

提示:Code 和 Markdown 区域可通过 Shift + Enter 快捷键运行。此外,Markdown可以通过双击进入编辑模式。


让我们开始吧

本项目的目的是结合命名实体识别、依存语法分析、实体消歧、实体统一对网站开放语料抓取的数据建立小型知识图谱。

在现实世界中,你需要拼凑一系列的模型来完成不同的任务;举个例子,用来预测狗种类的算法会与预测人类的算法不同。在做项目的过程中,你可能会遇到不少失败的预测,因为并不存在完美的算法和模型。你最终提交的不完美的解决方案也一定会给你带来一个有趣的学习经验!


步骤 1:实体统一

实体统一做的是对同一实体具有多个名称的情况进行统一,将多种称谓统一到一个实体上,并体现在实体的属性中(可以给实体建立“别称”属性)

例如:对“河北银行股份有限公司”、“河北银行公司”和“河北银行”我们都可以认为是一个实体,我们就可以将通过提取前两个称谓的主要内容,得到“河北银行”这个实体关键信息。

公司名称有其特点,例如后缀可以省略、上市公司的地名可以省略等等。在data/dict目录中提供了几个词典,可供实体统一使用。

  • company_suffix.txt是公司的通用后缀词典
  • company_business_scope.txt是公司经营范围常用词典
  • co_Province_Dim.txt是省份词典
  • co_City_Dim.txt是城市词典
  • stopwords.txt是可供参考的停用词

练习1:

编写main_extract函数,实现对实体的名称提取“主体名称”的功能。

In [20]:
import jieba
import jieba.posseg as pseg
import re
import datetime


# 从输入的“公司名”中提取主体
def main_extract(input_str,stop_word,d_4_delete,d_city_province):
    # 开始分词并处理
    seg = pseg.cut(input_str)
    seg_lst = remove_word(seg,stop_word,d_4_delete)
    seg_lst = city_prov_ahead(seg,d_city_province)
    return seg_lst

    
#TODO:实现公司名称中地名提前
def city_prov_ahead(seg,d_city_province):
    city_prov_lst = []
    # TODO ...
    
    return city_prov_lst+seg_lst




#TODO:替换特殊符号
def remove_word(seg,stop_word,d_4_delete):
    # TODO ...
    
    return seg_lst


# 初始化,加载词典
def my_initial():
    fr1 = open(r"../data/dict/co_City_Dim.txt", encoding='utf-8')
    fr2 = open(r"../data/dict/co_Province_Dim.txt", encoding='utf-8')
    fr3 = open(r"../data/dict/company_business_scope.txt", encoding='utf-8')
    fr4 = open(r"../data/dict/company_suffix.txt", encoding='utf-8')
    #城市名
    lines1 = fr1.readlines()
    d_4_delete = []
    d_city_province = [re.sub(r'(\r|\n)*','',line) for line in lines1]
    #省份名
    lines2 = fr2.readlines()
    l2_tmp = [re.sub(r'(\r|\n)*','',line) for line in lines2]
    d_city_province.extend(l2_tmp)
    #公司后缀
    lines3 = fr3.readlines()
    l3_tmp = [re.sub(r'(\r|\n)*','',line) for line in lines3]
    lines4 = fr4.readlines()
    l4_tmp = [re.sub(r'(\r|\n)*','',line) for line in lines4]
    d_4_delete.extend(l4_tmp)
    #get stop_word
    fr = open(r'../data/dict/stopwords.txt', encoding='utf-8')   
    stop_word = fr.readlines()
    stop_word_after = [re.sub(r'(\r|\n)*','',stop_word[i]) for i in range(len(stop_word))]
    stop_word_after[-1] = stop_word[-1]
    stop_word = stop_word_after
    return d_4_delete,stop_word,d_city_province
In [21]:
# TODO:测试实体统一用例
d_4_delete,stop_word,d_city_province = my_initial()
company_name = "河北银行股份有限公司"
lst = main_extract(company_name,stop_word,d_4_delete,d_city_province)
company_name = ''.join(lst)  # 对公司名提取主体部分,将包含相同主体部分的公司统一为一个实体
print(company_name)
Out [21]:
Building prefix dict from the default dictionary ...
Loading model from cache C:\Users\ADMINI~1\AppData\Local\Temp\jieba.cache
Loading model cost 0.732 seconds.
Prefix dict has been built succesfully.

步骤 2:实体识别

有很多开源工具可以帮助我们对实体进行识别。常见的有LTP、StanfordNLP、FoolNLTK等等。

本次采用FoolNLTK实现实体识别,fool是一个基于bi-lstm+CRF算法开发的深度学习开源NLP工具,包括了分词、实体识别等功能,大家可以通过fool很好地体会深度学习在该任务上的优缺点。

在‘data/train_data.csv’和‘data/test_data.csv’中是从网络上爬虫得到的上市公司公告,数据样例如下:

In [4]:
train_data = pd.read_csv('../data/info_extract/train_data.csv', encoding = 'gb2312', header=0)
train_data.head()
id sentence tag member1 member2
0 6461 与本公司关系:受同一公司控制 2,杭州富生电器有限公司企业类型: 有限公司注册地址: 富阳市... 0 0 0
1 2111 三、关联交易标的基本情况 1、交易标的基本情况 公司名称:红豆集团财务有限公司 公司地址:无... 0 0 0
2 9603 2016年协鑫集成科技股份有限公司向瑞峰(张家港)光伏科技有限公司支付设备款人民币4,515... 1 协鑫集成科技股份有限公司 瑞峰(张家港)光伏科技有限公司
3 3456 证券代码:600777 证券简称:新潮实业 公告编号:2015-091 烟台新潮实业股份有限... 0 0 0
4 8844 本集团及广发证券股份有限公司持有辽宁成大股份有限公司股票的本期变动系买卖一揽子沪深300指数... 1 广发证券股份有限公司 辽宁成大股份有限公司
In [2]:
test_data = pd.read_csv('../data/info_extract/test_data.csv', encoding = 'gb2312', header=0)
test_data.head()
id sentence
0 9259 2015年1月26日,多氟多化工股份有限公司与李云峰先生签署了《附条件生效的股份认购合同》
1 9136 2、2016年2月5日,深圳市新纶科技股份有限公司与侯毅先
2 220 2015年10月26日,山东华鹏玻璃股份有限公司与张德华先生签署了附条件生效条件的《股份认购合同》
3 9041 2、2015年12月31日,印纪娱乐传媒股份有限公司与肖文革签订了《印纪娱乐传媒股份有限公司...
4 10041 一、金发科技拟与熊海涛女士签订《股份转让协议》,协议约定:以每股1.0509元的收购价格,收...

我们选取一部分样本进行标注,即train_data,该数据由5列组成。id列表示原始样本序号;sentence列为我们截取的一段关键信息;如果关键信息中存在两个实体之间有股权交易关系则tag列为1,否则为0;如果tag为1,则在member1和member2列会记录两个实体出现在sentence中的名称。

剩下的样本没有标注,即test_data,该数据只有id和sentence两列,希望你能训练模型对test_data中的实体进行识别,并判断实体对之间有没有股权交易关系。

练习2:

将每句句子中实体识别出,存入实体词典,并用特殊符号替换语句。

In [24]:
# 处理test数据,利用开源工具进行实体识别和并使用实体统一函数存储实体

import fool
import pandas as pd
from copy import copy


test_data = pd.read_csv('../data/info_extract/test_data.csv', encoding = 'gb2312', header=0)
test_data['ner'] = None
ner_id = 1001
ner_dict_new = {}  # 存储所有实体
ner_dict_reverse_new = {}  # 存储所有实体

for i in range(len(test_data)):
    sentence = copy(test_data.iloc[i, 1])
    # TODO:调用fool进行实体识别,得到words和ners结果
    # TODO ...
    
    
    ners[0].sort(key=lambda x:x[0], reverse=True)
    for start, end, ner_type, ner_name in ners[0]:
        if ner_type=='company' or ner_type=='person':
            # TODO:调用实体统一函数,存储统一后的实体
            # 并自增ner_id
            # TODO ...
            
            
            # 在句子中用编号替换实体名
            sentence = sentence[:start] + ' ner_' + str(ner_dict_new[company_main_name]) + '_ ' + sentence[end-1:]
    test_data.iloc[i, -1] = sentence

X_test = test_data[['ner']]
In [26]:
# 处理train数据,利用开源工具进行实体识别和并使用实体统一函数存储实体
train_data = pd.read_csv('../data/info_extract/train_data.csv', encoding = 'gb2312', header=0)
train_data['ner'] = None

for i in range(len(train_data)):
    # 判断正负样本
    if train_data.iloc[i,:]['member1']=='0' and train_data.iloc[i,:]['member2']=='0':
        sentence = copy(train_data.iloc[i, 1])
        # TODO:调用fool进行实体识别,得到words和ners结果
        # TODO ...
    
    
        ners[0].sort(key=lambda x:x[0], reverse=True)
        for start, end, ner_type, ner_name in ners[0]:
            if ner_type=='company' or ner_type=='person':
                # TODO:调用实体统一函数,存储统一后的实体
                # 并自增ner_id
                # TODO ...


                # 在句子中用编号替换实体名
                sentence = sentence[:start] + ' ner_' + str(ner_dict_new[company_main_name]) + '_ ' + sentence[end-1:]
        train_data.iloc[i, -1] = sentence
    else:
        # 将训练集中正样本已经标注的实体也使用编码替换
        sentence = copy(train_data.iloc[i,:]['sentence'])
        for company_main_name in [train_data.iloc[i,:]['member1'],train_data.iloc[i,:]['member2']]:
            # TODO:调用实体统一函数,存储统一后的实体
            # 并自增ner_id
            # TODO ...


            # 在句子中用编号替换实体名
            sentence = re.sub(company_main_name, ' ner_%s_ '%(str(ner_dict_new[company_main_name])), sentence)
        train_data.iloc[i, -1] = sentence
        
y = train_data.loc[:,['tag']]
train_num = len(train_data)
X_train = train_data[['ner']]

# 将train和test放在一起提取特征
X = pd.concat([X_train, X_test])

步骤 3:关系抽取

目标:借助句法分析工具,和实体识别的结果,以及文本特征,基于训练数据抽取关系,并存储进图数据库。

本次要求抽取股权交易关系,关系为无向边,不要求判断投资方和被投资方,只要求得到双方是否存在交易关系。

模板建立可以使用“正则表达式”、“实体间距离”、“实体上下文”、“依存句法”等。

答案提交在submit目录中,命名为info_extract_submit.csv和info_extract_entity.csv。

  • info_extract_entity.csv格式为:第一列是实体编号,第二列是实体名(实体统一的多个实体名用“|”分隔)
  • info_extract_submit.csv格式为:第一列是关系中实体1的编号,第二列为关系中实体2的编号。

示例:

  • info_extract_entity.csv
实体编号 实体名
1001 小王
1002 A化工厂
  • info_extract_submit.csv
实体1 实体2
1001 1003
1002 1001

练习3:提取文本tf-idf特征

去除停用词,并转换成tfidf向量。

In [28]:
# code
from sklearn.feature_extraction.text import TfidfTransformer  
from sklearn.feature_extraction.text import CountVectorizer  
from pyltp import Segmentor


# 实体符号加入分词词典
with open('../data/user_dict.txt', 'w') as fw:
    for v in ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']:
        fw.write( v + '号企业 ni\n')

# 初始化实例
segmentor = Segmentor()  
# 加载模型,加载自定义词典
segmentor.load_with_lexicon('/Users/Badrain/Downloads/ltp_data_v3.4.0/cws.model', '../data/user_dict.txt')  

# 加载停用词
fr = open(r'../data/dict/stopwords.txt', encoding='utf-8')   
stop_word = fr.readlines()
stop_word = [re.sub(r'(\r|\n)*','',stop_word[i]) for i in range(len(stop_word))]

# 分词
f = lambda x: ' '.join([for word in segmentor.segment(x) if word not in stop_word and not re.findall(r'ner\_\d\d\d\d\_', word)])
corpus=X['ner'].map(f).tolist()


from sklearn.feature_extraction.text import TfidfVectorizer
# TODO:提取tfidf特征
# TODO ...


练习4:提取句法特征

除了词语层面的句向量特征,我们还可以从句法入手,提取一些句法分析的特征。

参考特征:

1、企业实体间距离

2、企业实体间句法距离

3、企业实体分别和关键触发词的距离

4、实体的依存关系类别

In [29]:
# -*- coding: utf-8 -*-
from pyltp import Parser
from pyltp import Segmentor
from pyltp import Postagger
import networkx as nx
import pylab
import re

postagger = Postagger() # 初始化实例
postagger.load_with_lexicon('/Users/Badrain/Downloads/ltp_data_v3.4.0/pos.model', '../data/user_dict.txt')  # 加载模型
segmentor = Segmentor()  # 初始化实例
segmentor.load_with_lexicon('/Users/Badrain/Downloads/ltp_data_v3.4.0/cws.model', '../data/user_dict.txt')  # 加载模型



def parse(s):
    """
    对语句进行句法分析,并返回句法结果
    """
    tmp_ner_dict = {}
    num_lst = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']

    # 将公司代码替换为特殊称谓,保证分词词性正确
    for i, ner in enumerate(list(set(re.findall(r'(ner\_\d\d\d\d\_)', s)))):
        try:
            tmp_ner_dict[num_lst[i]+'号企业'] = ner
        except IndexError:
            # TODO:定义错误情况的输出
            # TODO ...
            
            
        s = s.replace(ner, num_lst[i]+'号企业')
    words = segmentor.segment(s)
    tags = postagger.postag(words)
    parser = Parser() # 初始化实例
    parser.load('/Users/Badrain/Downloads/ltp_data_v3.4.0/parser.model')  # 加载模型
    arcs = parser.parse(words, tags)  # 句法分析
    arcs_lst = list(map(list, zip(*[[arc.head, arc.relation] for arc in arcs])))
    
    # 句法分析结果输出
    parse_result = pd.DataFrame([[a,b,c,d] for a,b,c,d in zip(list(words),list(tags), arcs_lst[0], arcs_lst[1])], index = range(1,len(words)+1))
    parser.release()  # 释放模型
    # TODO:提取企业实体依存句法类型
    # TODO ...
    
    

    # 投资关系关键词
    key_words = ["收购","竞拍","转让","扩张","并购","注资","整合","并入","竞购","竞买","支付","收购价","收购价格","承购","购得","购进",
             "购入","买进","买入","赎买","购销","议购","函购","函售","抛售","售卖","销售","转售"]
    # TODO:*根据关键词和对应句法关系提取特征(如没有思路可以不完成)
    # TODO ...
    
    
    parser.release()  # 释放模型
    return your_result


def shortest_path(arcs_ret, source, target):
    """
    求出两个词最短依存句法路径,不存在路径返回-1
    arcs_ret:句法分析结果
    source:实体1
    target:实体2
    """
    G=nx.DiGraph()
    # 为这个网络添加节点...
    for i in list(arcs_ret.index):
        G.add_node(i)
    # TODO:在网络中添加带权中的边...(注意,我们需要的是无向边)
    # TODO ...
    

    try:
        # TODO:利用nx包中shortest_path_length方法实现最短距离提取
        # TODO ...
        
        
        return distance
    except:
        return -1


def get_feature(s):
    """
    汇总上述函数汇总句法分析特征与TFIDF特征
    """
    # TODO:汇总上述函数汇总句法分析特征与TFIDF特征
    # TODO ...
    
    
    return features

练习5:建立分类器

利用已经提取好的tfidf特征以及parse特征,建立分类器进行分类任务。

In [51]:
# 建立分类器进行分类
from sklearn.ensemble import RandomForestClassifier
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV

# TODO:定义需要遍历的参数


# TODO:选择模型


# TODO:利用GridSearchCV搜索最佳参数


# TODO:对Test_data进行分类



# TODO:保存Test_data分类结果
# 答案提交在submit目录中,命名为info_extract_submit.csv和info_extract_entity.csv。
# info_extract_entity.csv格式为:第一列是实体编号,第二列是实体名(实体统一的多个实体名用“|”分隔)
# info_extract_submit.csv格式为:第一列是关系中实体1的编号,第二列为关系中实体2的编号。

 

练习6:操作图数据库

对关系最好的描述就是用图,那这里就需要使用图数据库,目前最常用的图数据库是noe4j,通过cypher语句就可以操作图数据库的增删改查。可以参考“https://cuiqingcai.com/4778.html”。

本次作业我们使用neo4j作为图数据库,neo4j需要java环境,请先配置好环境。

将我们提出的实体关系插入图数据库,并查询某节点的3层投资关系,即三个节点组成的路径(如果有的话)。如果无法找到3层投资关系,请查询出任意指定节点的投资路径。

from py2neo import Node, Relationship, Graph

graph = Graph(
    "http://localhost:7474", 
    username="neo4j", 
    password="person"
)

for v in relation_list:
    a = Node('Company', name=v[0])
    b = Node('Company', name=v[1])
    
    # 本次不区分投资方和被投资方,无向图
    r = Relationship(a, 'INVEST', b)
    s = a | b | r
    graph.create(s)
    r = Relationship(b, 'INVEST', a)
    s = a | b | r
    graph.create(s)
# TODO:查询某节点的3层投资关系

步骤4:实体消歧

解决了实体识别和关系的提取,我们已经完成了一大截,但是我们提取的实体究竟对应知识库中哪个实体呢?下图中,光是“苹果”就对应了13个同名实体。 <img src="../image/baike2.png", width=340, heigth=480>

在这个问题上,实体消歧旨在解决文本中广泛存在的名称歧义问题,将句中识别的实体与知识库中实体进行匹配,解决实体歧义问题。

练习7:

匹配test_data.csv中前25条样本中的人物实体对应的百度百科URL(此部分样本中所有人名均可在百度百科中链接到)。

利用scrapy、beautifulsoup、request等python包对百度百科进行爬虫,判断是否具有一词多义的情况,如果有的话,选择最佳实体进行匹配。

使用URL为‘https://baike.baidu.com/item/’+人名 可以访问百度百科该人名的词条,此处需要根据爬取到的网页识别该词条是否对应多个实体,如下图: <img src="../image/baike1.png", width=440, heigth=480> 如果该词条有对应多个实体,请返回正确匹配的实体URL,例如该示例网页中的‘https://baike.baidu.com/item/陆永/20793929’。

  • 提交文件:entity_disambiguation_submit.csv
  • 提交格式:第一列为实体id(与info_extract_submit.csv中id保持一致),第二列为对应URL。
  • 示例:
实体编号 URL
1001 https://baike.baidu.com/item/陆永/20793929
1002 https://baike.baidu.com/item/王芳/567232
import jieba
import pandas as pd

# 找出test_data.csv中前25条样本所有的人物名称,以及人物所在文档的上下文内容
test_data = pd.read_csv('../data/info_extract/test_data.csv', encoding = 'gb2312', header=0)

# 存储人物以及上下文信息(key为人物ID,value为人物名称、人物上下文内容)
person_name = {}

# 观察上下文的窗口大小
window = 10  

# 遍历前25条样本
for i in range(25):
    sentence = copy(test_data.iloc[i, 1])
    words, ners = fool.analysis(sentence)
    ners[0].sort(key=lambda x:x[0], reverse=True)
    for start, end, ner_type, ner_name in ners[0]:
        if ner_type=='person':
            # TODO:提取实体的上下文
            



# 利用爬虫得到每个人物名称对应的URL
# TODO:找到每个人物实体的词条内容。

# TODO:将样本中人物上下文与爬取词条结果进行对比,选择最接近的词条。



# 输出结果
pd.DataFrame(result_data).to_csv('../submit/entity_disambiguation_submit.csv', index=False)