关系抽取之Bert实现

前言

bert模型是谷歌2018年10月底公布的,反响巨大,效果不错,在各大比赛上面出类拔萃,它的提出主要是针对word2vec等模型的不足,在之前的预训练模型(包括word2vec,ELMo等)都会生成词向量,这种类别的预训练模型属于domain transfer。而近一两年提出的ULMFiT,GPT,BERT等都属于模型迁移,说白了BERT 模型是将预训练模型和下游任务模型结合在一起的,核心目的就是:是把下游具体NLP任务的活逐渐移到预训练产生词向量上。

本篇主要是讲实践方面(基于google公布的一个源代码):

https://github.com/google-research/bert

还有一个是将bert写成了service 的方式:https://github.com/hanxiao/bert-as-service

更多理论细节的话可以参考:

论文:https://arxiv.org/abs/1810.04805

一篇中文博客:https://www.cnblogs.com/rucwxb/p/10277217.html

这里简单概括一下bert的主要亮点:

一:双向Transformers

正如论文中所讲,目前的主要限制是当前模型不能同时考虑上下文,像上图的GPT只是一个从左到右,ELMo虽然有考虑从左到右和从右到左,但是是两个分开的网络,只有BERT是真真意义上的同时考虑了上下文

二:句子级别的应用

通过使用segment同时考虑了句子级别的预测,具体下面实践会看到其具体是怎么做的

三:能够解决的任务

google已经预预训练好了模型,我们要做的就是根据不同的任务,按照bert的输入要求(后面会看到)输入我们的数据,然后获取输出,在输出层加一层(通常情况下)全连接层就OK啦,整个训练过程就是基于预训练模型的微调,上述图片是其可以完成的几大类任务,ab都是sentence级别的(文本分类,关系抽取等等),cd是tokens级别的(如命名实体识别,知识问答)

下面首先解读一下怎么使用bert代码完成自己任务,一些代码的具体含义,最后具体看一个实践例子:实体关系关系抽取

https://github.com/yuanxiaosc/Entity-Relation-Extraction

黑色是一级标题,紫色是二级标题,绿色是三级标题,紫色是四级标题。

解读

在使用的时候,一般是需要下面三个脚本的,我们也不必修改,直接拿过来使用就ok

modeling.py

optimization.py

tokenization.py

其中tokenization是对原始句子内容的解析,分为BasicTokenizer和WordpieceTokenizer两个,一般来说BasicTokenizer

主要是进行unicode转换、标点符号分割、中文字符分割、去除重音符号等操作,最后返回的是关于词的数组(中文是字的数组),WordpieceTokenizer的目的是将合成词分解成类似词根一样的词片。例如将"unwanted"分解成["un", "##want", "##ed"]这么做的目的是防止因为词的过于生僻没有被收录进词典最后只能以[UNK]代替的局面,因为英语当中这样的合成词非常多,词典不可能全部收录。FullTokenizer的作用就很显而易见了,对一个文本段进行以上两种解析,最后返回词(字)的数组,同时还提供token到id的索引以及id到token的索引。这里的token可以理解为文本段处理过后的最小单元。上述来源https://www.jianshu.com/p/22e462f01d8c,更多该脚本的内容可以看该链接,下面主要用到FullTokenizer这个类

真正需要修改是:

run_classifier.py

run_squad.py

分别是解决分类,读理解任务,其实套路差不多,我们具体来看一下run_classifier.py

首先BERT主要分为两个部分。一个是训练语言模型(language model)的预训练(run_pretraining.py)部分。另一个是训练具体任务(task)的fine-tune部分,预训练部分巨大的运算资源,但是其已经公布了BERT的预训练模型

这里需要中文,直接下载就行,总得来说,我们要做的就是自己的数据集上进行fine-tune。

run_classifier.py中的类如下


InputExample类主要定义了一些数据预处理后要生成的字段名,如下:

guid就是一个id号,一般将数据处理成train、dev、test数据集,那么这里定义方式就可以是相应的数据集+行号(句子)

text_a 就是当前的句子,text_b是另一个句子,因为有的任务需要两个两个句子,如果任务中没有的话,可以将text_b设为None

label就是标签

InputFeatures类主要是定义了bert的输入格式,形象化点就是特征,即上面的格式使我们需要将原始数据处理成的格式,但并不是bert使用的最终格式,且还会通过一些代码将InputExample转化为InputFeatures,这才是bert最终使用的数据格式,当然啦这里根据自己的需要还可以自定义一些字段作为中间辅助字段,但bert最基本的输入字段就需要input_ids,input_mask和segment_ids这三个字段,label_id是计算loss时候用到的,input_ids,segment_ids分别对应单词id和句子(上下句标示),Input_mask就是记录的是填充信息,具体看下面

 

DataProcessor,这是一个数据预处理的基类,里面定义了一些基本方法

XnliProcessor、MnliProcessor、MrpcProcessor、ColaProcessor四个类是对DataProcessor的具体实现,这里之所以列举了四个是尽可能多的给用户呈现出各种demo,具体到实际使用的时候我们只需要参考其写法,定义一个自己的数据预处理类即可,其中一般包括如下几个方法:get_train_examples,get_dev_examples,get_test_examples,get_labels,_create_examples

其中前三个都通过调用_create_examples返回一个InputExample类数据结构,get_labels就是返回类别,所以重点就是以下两个函数:也很简单

这里的tokenization的convert_to_unicode就是将文本转化为utf-8编码

上述就是数据预处理过程,也是需要我们自己根据自己的数据定义的,其实呢,这并不是Bert使用的最终样子,其还得经过一系列过程才能变成其能处理的数据格式,该过程是通过接下来的四个方法完成的:

convert_single_example

file_based_convert_examples_to_features

file_based_input_fn_builder

truncate_seq_pair

只不过一般情况下我们不需要修改,它都是一个固定的流程

convert_single_example:

bert的输入:

代码中的input_ids,segment_ids分别代表token,segment,同时其还在句子的开头结尾加上了[CLS]和SEP]标示

input_ids中就是记录的是使用FullTokenizer类convert_tokens_to_ids方法将tokens转化成单个字的id

segment_ids就是句子级别(上下句)的标签,大概形式:

# (a) For sequence pairs:
 #  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
 #  type_ids: 0     0  0    0    0     0       0 0     1  1  1  1   1 1
 # (b) For single sequences:
 #  tokens:   [CLS] the dog is hairy . [SEP]
 #  type_ids: 0     0   0   0  0     0 0

当没有text_b的时候,就都是0啦

还有一个input_mask,其就是和最大长度有关,假设我们定义句子的最大长度是120,当前句子长度是100,那么input_mask前100个元素都是1,其余20个就是0

最后返回的就是一个InputFeatures类

file_based_convert_examples_to_features

很简单啦,因为在训练的时候为了读写快速方便便将数据制作成TFrecords 数据格式,该函数主要就是将上述返回的InputFeatures类数据,保存成一个TFrecords数据格式,关于TFrecords数据格式的制作可以参考笔者另一篇https://blog.csdn.net/weixin_42001089/article/details/90236241

file_based_input_fn_builder

对应的就是从TFrecords 解析读取数据

truncate_seq_pair

就是来限制text_a和text_b总长度的,当超过的话,会轮番pop掉tokens

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

至此整个数据的预处理才算处理好,其实最后最关键的就是得到了那个TFrecords文件

下面看模型部分

create_model

model_fn_builder

整个模型过程采用了tf.contrib.tpu.TPUEstimator这一高级封装的API

model_fn_builder是壳,create_model是核心,其内部定义了loss,预测概率以及预测结果等等。

model_fn_builder

其首先调用create_model得到total_loss, per_example_loss, logits, probabilities等等,然后针对不同的状态返回不同的结果(output_spec),如果是train则返回loss,train_op等,如果是dev则返回一些评价指标如accuracy,如果是test则返回预测结果

所以我们如果想看一下别的指标什么的,可以在这里改 ,需要注意的是指标的定义这里因为使用了estimator API使得其必须返回一个operation,至于怎么定义f1什么的可以看下:

https://www.cnblogs.com/jiangxinyang/p/10341392.html

create_model

##############################################重中之重############################################

##############################################重中之重############################################

##############################################重中之重############################################

重要的事情说三遍!!!!!!!!!!!!!!!!!!!

这里可以说整个Bert使用的最关键的地方,我们使用Bert大多数情况无非进行在定义自己的下游工作进行fine-tune,就是在这里定义的

把这段代码贴出来吧

  1. def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
  2. labels, num_labels, use_one_hot_embeddings):
  3. """Creates a classification model."""
  4. model = modeling.BertModel(
  5. config=bert_config,
  6. is_training=is_training,
  7. input_ids=input_ids,
  8. input_mask=input_mask,
  9. token_type_ids=segment_ids,
  10. use_one_hot_embeddings=use_one_hot_embeddings)
  11. # In the demo, we are doing a simple classification task on the entire
  12. # segment.
  13. #
  14. # If you want to use the token-level output, use model.get_sequence_output()
  15. # instead.
  16. output_layer = model.get_pooled_output()
  17. hidden_size = output_layer.shape[-1].value
  18. output_weights = tf.get_variable(
  19. "output_weights", [num_labels, hidden_size],
  20. initializer=tf.truncated_normal_initializer(stddev=0.02))
  21. output_bias = tf.get_variable(
  22. "output_bias", [num_labels], initializer=tf.zeros_initializer())
  23. with tf.variable_scope("loss"):
  24. if is_training:
  25. # I.e., 0.1 dropout
  26. output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
  27. logits = tf.matmul(output_layer, output_weights, transpose_b=True)
  28. logits = tf.nn.bias_add(logits, output_bias)
  29. probabilities = tf.nn.softmax(logits, axis=-1)
  30. log_probs = tf.nn.log_softmax(logits, axis=-1)
  31. one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)
  32. per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
  33. loss = tf.reduce_mean(per_example_loss)
  34. return (loss, per_example_loss, logits, probabilities)

首先调用modeling.BertModel得到bert模型

bert模型的输入:input_ids,input_mask,segment_ids

  1. model = modeling.BertModel(
  2. config=bert_config,
  3. is_training=is_training,
  4. input_ids=input_ids,
  5. input_mask=input_mask,
  6. token_type_ids=segment_ids,
  7. use_one_hot_embeddings=use_one_hot_embeddings

config是bert的配置文件,在开头下载的中文模型中里面有,直接加载即可

use_one_hot_embeddings是根据是不是用GPU而定的,其他字段上述都说过啦

 

bert模型的输出:

其有两种情况

  1. model.get_sequence_output()
  2. model.get_pooled_output()

第一种输出结果是[batch_size, seq_length, embedding_size]

第二种输出结果是[batch_size, embedding_size]

第二种结果是第一种结果在第二个维度上面进行了池化,要是形象点比喻的话,第一种结果得到是tokens级别的结果,第二种是句子级别的,其实就是一个池化

我们定义部分

这部分就是需要我们根据自己的任务自己具体定义啦,假设是一个简单的分类,那么就是定义一个全连接层将其转化为[batch_size, num_classes]对吧

output_weights和output_bias就是对应全连接成的权值,后面就是loss,使用了tf.nn.log_softmax应该是一个多分类,多标签的话可以使用tf.nn.sigmoid,比较简单就不再说啦

总得来说,使用bert进行自己任务的时候,可以千变万化,变的就是这里这个下游

main

最后就是主函数,主要就是通过人为定义的一些配置值(FLAGS)将上面的流程整个组合起来

这里大体说一下流程:

  1. processors = {
  2. "cola": ColaProcessor,
  3. "mnli": MnliProcessor,
  4. "mrpc": MrpcProcessor,
  5. "xnli": XnliProcessor,
  6. }

这里就是定义数据预处理器的,记得把自己定义的预处理包含进来,名字嘛,随便起起啦,到时候通过外部参数字段task_name来指定用哪个(说白了就是处理哪个数据)

数据预处理完了,就使用tf.contrib.tpu.TPUEstimator定义模型

最后就是根据不同模式(train/dev/test,这也是运行时可以指定的)运行estimator.train,estimator.evaluate,estimator.predict

总结:

一:总体来说,在进行具体工作时,需要改的核心就是:

       1) 继承DataProcessor定义一个自己的数据预处理类

       2) 在create_model中定义自己的具体下游工作

剩下的就是一些零零碎碎的小地方啦,也很简单

二 :关于bert上游的具体模型定义这里没有,实在感兴趣可以看modeling.py脚本,优化器部分是optimization.py

三 :这里没有从头训练bert模型,因为耗时耗力,没有资源一般来说很难,关于预训练的部分是run_pretraining.py

实践

前言

说了这么多,可能还是有点不明白,下面结合一个例子就会更清楚一点

https://github.com/yuanxiaosc/Entity-Relation-Extraction

这个例子是一个比赛,其目的是从给定的一个句子中抽取所有三元组(关于三元组抽取,这里有另外一篇https://kexue.fm/archives/6736#%E5%85%B3%E7%B3%BB%E6%8A%BD%E5%8F%96),涉及到的关系有

例子 输入句子: "text": "《古世》是连载于云中书城的网络小说,作者是未弱"

输出三元组: "spo_list": [{"predicate": "作者", "object_type": "人物", "subject_type": "图书作品", "object": "未弱", "subject": "古世"}, {"predicate": "连载网站", "object_type": "网站", "subject_type": "网络小说", "object": "云中书城", "subject": "古世"}]}

解决思路如下:

先使用bert搭建关系的分类模型,简单来看就是一个多标签分类任务,类别就是上述的那几种关系

接着用预测出来的关系和文本,使用bert搭建一个实体抽取的模型,其简单来看也是一个分类模型,类别是

SUB对应的就是subject,B-SUB就是第一个实体开始的位置,后续的是I-SUB

OBJ就是第二个实体

所以第二个模型就是预测每一个tokens的标示,最后根据标示可提取出实体对

第二个模型是一个多分类的单标签任务,我们知道一句话中有可能有多个三元组,为此在进行第二个模型的时候,是先依据第一个模型预测出来的关系类如当前句子预测出3个关系,那么就重复该句话分成3个样本,那么3个样本就对应的是3个多分类单标签任务,为了使实体对和关系对应,所以第二个模型在计算loss的时候是综合考虑了关系和tokens标示的预测的。

过程中所有结果都会生成保存在out文件夹下

总体简介

run_predicate_classification.py, run_sequnce_labeling.py

分别对应的就是第一,二个模型,其写法套路参考就是上述介绍的run_classifier.py函数

produce_submit_json_file.py 就是将结果转化成比赛需要提交的数据格式,注意到最后的格式还需要实体对的类型,我们通过上面得到了三元组,但是还缺实体的类型,代码中是使用了字典,依据关系确定对应实体类型

类如当前是父亲关系的话,这一对实体应该说的是两个人

pretrained_model

是一个文件夹,将预训练好的中文模型解压文件夹放到该文件夹下

bert

也是一个文件夹,主要放一些run_predicate_classification.py, run_sequnce_labeling.py需要用到的脚本,其实主要的就是上述介绍的那三个基本的脚本modeling.pyoptimization.pytokenization.py

bin

该文件夹下主要就是一些数据预处理的文件

evaluation文件夹是评价函数

predicate_classifiction和subject_object_labeling是对应的第一个模型和第二个模型进行前需要的数据预处理脚本

raw_data

该文件夹下主要就是放的原始数据

下面部分Github已经给出了具体步骤,这里主要结合步骤强调一些细节部分

预测阶段

 

准备关系分类数据

python bin/predicate_classifiction/predicate_data_manager.py

原始数据

train和dev两个json文件大体样式是:

test.json: 没有spo_list,待预测

运行完上述程序后会在bin/predicate_classifiction/文件下生成一个classification_data文件夹里面对应的train,test,valid文件夹,文件夹下生成基本都是

predicate_out.txt  :提取出每一个句子的关系集合

text.txt  :每一个句子的文本

token_in_not_UNK.txt :分字

token_in.txt

后面两个主要还是使用了tokenization脚本中的FullTokenizer一些类方法

注意test是没有标签的,因为要看线下效果,即可以将dev数据集当成test数据集处理放在test目录下,后面评价的话就可以啦,

所以如果是比赛的话直接按上面运行,非比赛的的可以:

predicate_data_manager.py set: Competition_Mode = False

其实区别就是test文件夹到底是真的test还是dev

关系分类模型训练

  1. python run_predicate_classification.py \
  2. --task_name=SKE_2019 \
  3. --do_train=true \
  4. --do_eval=false \
  5. --data_dir=bin/predicate_classifiction/classification_data \
  6. --vocab_file=pretrained_model/chinese_L-12_H-768_A-12/vocab.txt \
  7. --bert_config_file=pretrained_model/chinese_L-12_H-768_A-12/bert_config.json \
  8. --init_checkpoint=pretrained_model/chinese_L-12_H-768_A-12/bert_model.ckpt \
  9. --max_seq_length=128 \
  10. --train_batch_size=32 \
  11. --learning_rate=2e-5 \
  12. --num_train_epochs=6.0 \
  13. --output_dir=./output/predicate_classification_model/epochs6/

经过我们上面分析,我们知道其是通过改造run_classifier.py得到的,那么主要变得地方就是定义了自己的数据预处理器和模型下游,具体看一下

自定义的数据预处理器是

SKE_2019_Multi_Label_Classification_Processor

套路一样,这里主要内部主要多了一个get_examples方法,因为当前的数据label和文本放在两个不同的文件,这个方法就是同时加载两个文件对应组合

再看一下create_model,最关键的就是:

  1. with tf.variable_scope("loss"):
  2. if is_training:
  3. # I.e., 0.1 dropout
  4. output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
  5. logits_wx = tf.matmul(output_layer, output_weights, transpose_b=True)
  6. logits = tf.nn.bias_add(logits_wx, output_bias)
  7. probabilities = tf.sigmoid(logits)
  8. label_ids = tf.cast(labels, tf.float32)
  9. per_example_loss = tf.reduce_sum(
  10. tf.nn.sigmoid_cross_entropy_with_logits(logits=logits, labels=label_ids), axis=-1)
  11. loss = tf.reduce_mean(per_example_loss)
  12. return loss, per_example_loss, logits, probabilities

就是加一了一层全连接,因为是多标签嘛,所以用的是sigmoid

其他的话就是在评价的时候即model_fn_builder中,概率阈值使用的是0.5

在预测的时候,采用了同样的阈值(0.5),该部分在main中最后

因为要将结果保存起来,所以在main中定义了一下写入操作都也很简单。

运行的最后结果就是会在out文件夹下生成一个 predicate_classification_model文件夹保存该模型训练好的模型参数

准备序列标注数据

python bin/subject_object_labeling/sequence_labeling_data_manager.py

运行完上述程序后会在bin/subject_object_labeling/文件下生成一个sequence_labeling_data 文件夹里面对应的train,test,valid文件夹,文件夹下生成基本都是

bert_tokener_error_log.txt

text.txt:可以看到如果句子有多个三元组是分成多个样本的

token_in_not_UNK.txt  token_in.txt和前面类似

token_label_and_one_prdicate_out.txt:可以看到SUB和OBJ就是对应实体对的标示,后面是关系

序列标注模型训练

  1. python run_sequnce_labeling.py \
  2. --task_name=SKE_2019 \
  3. --do_train=true \
  4. --do_eval=false \
  5. --data_dir=bin/subject_object_labeling/sequence_labeling_data \
  6. --vocab_file=pretrained_model/chinese_L-12_H-768_A-12/vocab.txt \
  7. --bert_config_file=pretrained_model/chinese_L-12_H-768_A-12/bert_config.json \
  8. --init_checkpoint=pretrained_model/chinese_L-12_H-768_A-12/bert_model.ckpt \
  9. --max_seq_length=128 \
  10. --train_batch_size=32 \
  11. --learning_rate=2e-5 \
  12. --num_train_epochs=9.0 \
  13. --output_dir=./output/sequnce_labeling_model/epochs9/

同理其还是对run_classifier.py进行了改造,先看数据预处理函数器:

其里面最大不同就是定义了两个label函数

一个是实体标示label,一个是关系label

接着看一下create_model,其分别两大部分:predicate_loss和token_label_loss即关系预测和实体标示预测

先看关系部分predicate_loss

  1. predicate_output_layer = model.get_pooled_output()
  2. intent_hidden_size = predicate_output_layer.shape[-1].value
  3. predicate_output_weights = tf.get_variable(
  4. "predicate_output_weights", [num_predicate_labels, intent_hidden_size],
  5. initializer=tf.truncated_normal_initializer(stddev=0.02))
  6. predicate_output_bias = tf.get_variable(
  7. "predicate_output_bias", [num_predicate_labels], initializer=tf.zeros_initializer())
  8. with tf.variable_scope("predicate_loss"):
  9. if is_training:
  10. # I.e., 0.1 dropout
  11. predicate_output_layer = tf.nn.dropout(predicate_output_layer, keep_prob=0.9)
  12. predicate_logits = tf.matmul(predicate_output_layer, predicate_output_weights, transpose_b=True)
  13. predicate_logits = tf.nn.bias_add(predicate_logits, predicate_output_bias)
  14. predicate_probabilities = tf.nn.softmax(predicate_logits, axis=-1)
  15. predicate_prediction = tf.argmax(predicate_probabilities, axis=-1, output_type=tf.int32)
  16. predicate_labels = tf.one_hot(predicate_label_id, depth=num_predicate_labels, dtype=tf.float32)
  17. predicate_per_example_loss = tf.reduce_sum(tf.nn.sigmoid_cross_entropy_with_logits(logits=predicate_logits, labels=predicate_labels), -1)
  18. predicate_loss = tf.reduce_mean(predicate_per_example_loss)

因为该部分使用的数据是将句子已经分成单标签了,所以使用的是sigmoid,其实基本和关系分类模型训练中的差不多,只不过一个是多标签一个是单标签

再看token_label_loss部分

最需要注意的就是上面红框的部分,其不再用bert的model.get_pooled_output()输出模式,而是使model.get_sequence_output(),因为结果需要预测每个tokens的标示,每个tokens应该只有一个标示,也是一个多分类任务

最后的loss是综合两部分

loss = 0.5 * predicate_loss + token_label_loss

这里的predicate_probabilities就是预测关系的概率值,predicate_prediction是概率最大的那个关系,token_label_predictions就是预测的实体标示(当然啦,也可以返回具体每一个tokens关于每一个标示的概率)

运行的最后结果就是会在out文件夹下生成一个 sequnce_labeling_model文件夹保存该模型训练好的模型参数

##########################################训练过程就结束啦##########################################

说到这里可能会有这样的疑惑?两个模型(所谓的管道),似乎没有交集,都是先处理准备好各自数据,然后各自训练各自的,而且第二个模型同时进行了关系和实体标示预测,那么第一个模型只预测了关系,那么第一个模型存在的意义是什么?直接用第二模型不就可以啦?

逻辑是这样的:

训练确实没有交集,各自训练各自的,因为训练样本都是精确的,无可厚非,但是在预测的时候我们只有一句话,要预测出这句话中的所有三元组,如果只采用第二个模型的话,它一句话根据当前的关系只能预测出一种三元组,所以需要第一个模型打前阵,先将句子中有多少种关系预测出来,然后再将句子依照关系分成一句话一个三元组,训练的时候我们是知道每一个句子有多少种关系,但是预测的时候我们并不知道,这就是第一个模型存在的意义,那么我就要用第二个模型同时解决一句话预测所有三元组呢?其实很难,因为首先tokens标记就是问题,怎么将标示和关系对应呢?是吧,所以这里所谓管道,其实是在预测过程体现的,下面就可以看到

另外按照上面的思路,该模型bert的输入端一个样本的特征应该是当前句子和关系,而不仅仅是一个句子,不然的话将一句话拆分成多个样本还有什么意义?大家不就都一样啦,正是由于还有关系,才能实现同一句子抽取出不同的实体对(对应当前关系),那么输入端是怎么将将关系整合进去的呢?

解决办法是这样的:

将关系作为当前句子的下句,还记得text_b吗?第一个模型设为了None,这里将类别平铺成和text_a一样长度,当成一句话作为text_a(当前句子)的下文,该部分具体看convert_single_example:

假设当前句子长度为5,关系是作者,那么tokens_b(text_b) = ['作者','作者','作者','作者','作者'],具体代码都很简单,这里就举一个例子,一目了然

假设原始tokens : 胡歌是仙剑的主演,关系是:主演

那么经过convert_single_example函数处理后大体相当于:

tokens     :           CLS   胡   歌   是   仙   剑   的   主   演   SEP   主演   主演   主演   主演   主演   主演   主演   主演  SEP

input_ids :           101   200 201 202 203 204 205 206 207 102   2  2   2  2  2   2  2   2   102  0 0 0

segment_ids :       0       0     0     0    0     0    0     0     0     0        1       1        1         1         1         1      1         1       1 0 0 0

input_mask :     1   1   1   1   1    1    1     1     1     1    1       1        1         1         1         1      1         1       1  0 0 0

token_label   :        [CLS]    B-SUB I-SUB o B-OBJ I-OBJ o o o [SEP] [category] [category] [category] [category] [category]

                             [category] [category] [category]  [SEP] [Padding] [Padding] [Padding]

predicate_id : 1

几点说明:

一  bert输入需要字段input_ids ,segment_ids,input_mask,计算loss需要token_label predicate_id

二 红色的部分是填充到最大长度,填充部分都是0

三 关于词的id 原始文本就使用tokenizer.convert_tokens_to_ids就可以得到(上面的101,200等等),类别("主演")的id呢?

     这里是将所有类别制作成一个字典,类如主演的value就是1,上述之所以是2,是因为代码中同一加了一个偏置

    那么这里类别的id和经过tokenizer.convert_tokens_to_ids转化后的字id有没有可能是冲突的呢?没有!,因为

    tokenizer.convert_tokens_to_ids没有用1-100之内的id,当前类别又没有超过100,所以不会冲突

四  还记得论文中这幅图吗?

对应到这里:

第一个模型应该是对应图b

第二个模型应该包括两部分一部分是关系预测一部分是实体预测,关系预测相当于图a(sentence 2 = text_b),实体预测部分相当于c ,注意图d举的例子本来可以看做是一个实体识别的例子,但我们这里输入毕竟使用了 text_b,如果强行对应的话,只能看做是和c(知识问答)最对应了吧

##################################################################################################

预测阶段

关系分类模型预测

运行

  1. python run_predicate_classification.py \
  2. --task_name=SKE_2019 \
  3. --do_predict=true \
  4. --data_dir=bin/predicate_classifiction/classification_data \
  5. --vocab_file=pretrained_model/chinese_L-12_H-768_A-12/vocab.txt \
  6. --bert_config_file=pretrained_model/chinese_L-12_H-768_A-12/bert_config.json \
  7. --init_checkpoint=output/predicate_classification_model/epochs6/model.ckpt-478 \
  8. --max_seq_length=128 \
  9. --output_dir=./output/predicate_infer_out/epochs6/ckpt478

这里就是加载上述已经训练好的模型进行关系预测,这里要加载哪个模型需要手动设置一下即

init_checkpoint

运行完后,会在out文件夹下生成一个predicate_infer_out文件夹,里面主要有:

predicate_predict.txt类别预测结果

predicate_score_value.txt:类别概率,以0.5为阈值确定最终结果

把关系分类模型预测结果转换成序列标注模型的预测输入

python bin/predicate_classifiction/prepare_data_for_labeling_infer.py

其主要的作用就是像上面所说的依照关系将其分成单句对应一个关系

会在bin//subject_object_labeling/sequence_labeling_data/test 文件夹下生成

text_and_one_predicate.txt:单句文本

token_in_and_one_predicate.txt:单句文本以及对应的关系

token_in_not_UNK_and_one_predicate.txt

序列标注模型预测

  1. python run_sequnce_labeling.py \
  2. --task_name=SKE_2019 \
  3. --do_predict=true \
  4. --data_dir=bin/subject_object_labeling/sequence_labeling_data \
  5. --vocab_file=pretrained_model/chinese_L-12_H-768_A-12/vocab.txt \
  6. --bert_config_file=pretrained_model/chinese_L-12_H-768_A-12/bert_config.json \
  7. --init_checkpoint=output/sequnce_labeling_model/epochs9/model.ckpt-1237 \
  8. --max_seq_length=128 \
  9. --output_dir=./output/sequnce_infer_out/epochs9/ckpt1237

手动设置一下要加载的模型init_checkpoint

运行完后会在out文件夹下生成 sequnce_infer_out文件夹,里面主要是:

predicate_predict.txt:

predicate_probabilities.txt:关系预测概率,单标签,选取最大的概率作为最终结果

token_label_predictions.txt:实体表示预测结果(其实对应每一个token也是单标签多分类)

生成实体-关系结果

python produce_submit_json_file.py

主要就是按句子分组整合,生成最终预测数据

运行完后会在out文件夹下生成final_text_spo_list_result文件夹,里面:

keep_empty_spo_list_subject_predicate_object_predict_output.json:

因为这里采用的是非比赛模型,所以test中的数据其实是dev数据

评价

关系抽取模型性能

运行

python bin/evaluation/evaluate_classifiction.py

结果

过程很简单,就是看每一条样本的具有的真实关系集合和预测关系集合(如下代码中的golden_data_list, predict_data_list),然后看两者是否相同,不相同的话再看看谁是谁的子集,最后分别统计结果

从上面可以看出,一共有1000条样本,完全预测正确的是551,除此之外,只预测出真实关系集合中的一个子集数是83个,另外还有125个是预测的关系集合中完全包含了真实关系集合,当然啦剩下的1000-551-83-125=241个样本的预测结果都是交叉的。

实体抽取模型性能

运行

python bin/evaluation/evaluate_labeling.py 

结果

其过程就是将看预测的三元组对的个数,correct spo num是预测对的三元组个数,submitted spo num是一共预测出的三元组个数,golden set spo num是真实的三元组个数,当然得在text分组下对比是否相等才有意义,最后就是根据这三个数计算准确率,回归率以及F1

总结

一:把握住  bert的输入:input_ids,input_mask,segment_ids (主要在convert_single_example生成)

                    bert的输出:model.get_sequence_output() model.get_pooled_output()

二:具体做的时候主要修改:继承DataProcessor类定义自己的数据预处理器在create_model定义自己具体下游的任务

三 : 本篇的实践是先进行关系抽取,然后进行实体抽取,以前各种模型(pcnn,birnn这里笔者也

         写过一篇博客https://blog.csdn.net/weixin_42001089/article/details/95493249)都是先进行实体抽取,后进行关系抽取,

         如果想要既使用后面的顺序(因为有的场景是给定句子和一个实体对来抽取判断关系)又想利用bert进行相关工作,那么

         借鉴这里的思路,也是两个模型,第一个模型没问题就是使用实体标示的样本去训练,就是一个单标签多分类,那么第二

         在进行第二个模型的时候可能要想一下,即输入怎么将当前实体对这一信息输入进去,有想法的可以试一下

 

 

 

 

发布了0 篇原创文章 · 获赞 0 · 访问量 18

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 深蓝海洋 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览