BERT:代码解读、实体关系抽取实战

目录

前言    

一、BERT的主要亮点

1. 双向Transformers

2.句子级别的应用

3.能够解决的任务

二、BERT代码解读

1. 数据预处理

1.1 InputExample类

1.2 InputFeatures类

1.3 DataProcessor   重点

1.4 convert_single_example

1.5 file_based_convert_examples_to_features

1.6 file_based_input_fn_builder

1.7 _truncate_seq_pair

2. 模型部分

2.1 model_fn_builder

2.2 create_model   重点

3. main主函数

4. 总结

三、Entity-Relation-Extraction-master实战


BERT代码:参加另一篇文章《命名实体识别NER & 如何使用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的主要亮点

1. 双向Transformers

      BERT真真意义上同时考虑了上下文:

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

2.句子级别的应用

     通过使用segment同时考虑了句子级别的预测。

3.能够解决的任务

       google已经预预训练好了模型,我们要做的就是根据不同的任务,按照bert的输入要求(后面会看到)输入我们的数据,然后获取输出,在输出层加一层(通常情况下)全连接层就OK啦,整个训练过程就是基于预训练模型的微调,下述图片是其可以完成的几大类任务:

       a、b都是sentence级别的:文本分类,关系抽取等;

       c、d是tokens级别的:如命名实体识别,知识问答等。

二、BERT代码解读

BERT的代码主要分为两个部分:

1. 预训练部分:其入口是在run_pretraining.py。

2. Fine-tune部分:Fine-tune的入口针对不同的任务分别在run_classifier.py和run_squad.py。其中

  • run_classifier.py:适用的任务为分类任务,如CoLA、MRPC、MultiNLI等。而
  • run_squad.py:适用的是阅读理解任务,如squad2.0和squad1.1。

     在使用的时候,一般是需要下面三个脚本的,我们也不必修改,直接拿过来使用就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。

1. 数据预处理

    run_classifier.py中的类如下:             

                    

1.1 InputExample类

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

                                               

  • guid就是一个id号,一般将数据处理成train、dev、test数据集,那么这里定义方式就可以是相应的数据集+行号(句子)
  • text_a 就是当前的句子,text_b是另一个句子,因为有的任务需要两个两个句子,如果任务中没有的话,可以将text_b设为None
  • label就是标签

1.2 InputFeatures类

       主要是定义了bert的输入格式,形象化点就是特征,即上面的格式使我们需要将原始数据处理成的格式,但并不是bert使用的最终格式,且还会通过一些代码将InputExample转化为InputFeatures,这才是bert最终使用的数据格式,当然啦这里根据自己的需要还可以自定义一些字段作为中间辅助字段,但bert最基本的输入字段就需要input_ids,input_mask和segment_ids这三个字段,label_id是计算loss时候用到的:

  • input_ids,segment_ids:分别对应单词id和句子(上下句标示),input_ids、segment_ids分别代表token、segment。
  • Input_mask:记录的是填充信息,具体看下面

               

1.3 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:返回一个InputFeatures类
            file_based_convert_examples_to_features
            file_based_input_fn_builder
            truncate_seq_pair
只不过一般情况下我们不需要修改,它都是一个固定的流程。

1.4 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类:

                               

1.5 file_based_convert_examples_to_features

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

1.6 file_based_input_fn_builder

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

1.7 _truncate_seq_pair

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

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

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

2. 模型部分

  •  create_model
  •  model_fn_builder

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

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

2.1 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

2.2 create_model   重点

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

把这段代码贴出来吧

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 labels, num_labels, use_one_hot_embeddings):
  """Creates a classification model."""
  model = modeling.BertModel(
      config=bert_config,
      is_training=is_training,
      input_ids=input_ids,
      input_mask=input_mask,
      token_type_ids=segment_ids,
      use_one_hot_embeddings=use_one_hot_embeddings)
 
  # In the demo, we are doing a simple classification task on the entire
  # segment.
  #
  # If you want to use the token-level output, use model.get_sequence_output()
  # instead.
  output_layer = model.get_pooled_output()
 
  hidden_size = output_layer.shape[-1].value
 
  output_weights = tf.get_variable(
      "output_weights", [num_labels, hidden_size],
      initializer=tf.truncated_normal_initializer(stddev=0.02))
 
  output_bias = tf.get_variable(
      "output_bias", [num_labels], initializer=tf.zeros_initializer())
 
  with tf.variable_scope("loss"):
    if is_training:
      # I.e., 0.1 dropout
      output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)
 
    logits = tf.matmul(output_layer, output_weights, transpose_b=True)
    logits = tf.nn.bias_add(logits, output_bias)
    probabilities = tf.nn.softmax(logits, axis=-1)
    log_probs = tf.nn.log_softmax(logits, axis=-1)
 
    one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)
 
    per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
    loss = tf.reduce_mean(per_example_loss)
 
    return (loss, per_example_loss, logits, probabilities)

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

(1)bert模型的输入:input_ids,input_mask,segment_ids

model = modeling.BertModel(
      config=bert_config,
      is_training=is_training,
      input_ids=input_ids,
      input_mask=input_mask,
      token_type_ids=segment_ids,
      use_one_hot_embeddings=use_one_hot_embeddings
  • config:是bert的配置文件,在开头下载的中文模型中里面有,直接加载即可

                                               

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

(2)bert模型的输出:其有两种情况   

         model.get_sequence_output():第一种输出结果是[batch_size, seq_length, embedding_size]

         model.get_pooled_output():第二种输出结果是[batch_size, embedding_size]

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

(3)我们定义部分

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

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

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

3. main主函数

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

这里大体说一下流程:

processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
  }

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

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

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

4. 总结

(1)总体来说,在进行具体工作时,复制 BERT 的 run_classifier.py,修改核心内容作为自己的run函数,需要改的核心就是:

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

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

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

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

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

三、Entity-Relation-Extraction-master实战

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

基于 TensorFlow 的实体及关系抽取,2019语言与智能技术竞赛信息抽取(实体与关系抽取)任务解决方案。

实体关系抽取本模型过程:

该代码以管道式的方式处理实体及关系抽取任务,首先使用一个多标签分类模型判断句子的关系种类, 然后把句子和可能的关系种类输入序列标注模型中,序列标注模型标注出句子中的实体,最终结合预 测的关系和实体输出实体-关系列表:(实体1,关系,实体2)

  • 1. 先进行关系抽取:得到一句话中有几种关系
  • 2. 根据预测出来的关系类,如当前句子预测出3个关系,那么就重复该句话分成3个样本
  • 3. 再进行实体识别:根据序列标注找出实体
  • 4. 生成实体—关系三元组结果

模型过程总结:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章