查看原文
其他

【NLP实战系列】Tensorflow命名实体识别实战

小Dream哥 有三AI 2020-09-08

实战是学习一门技术最好的方式,也是深入了解一门技术唯一的方式。因此,NLP专栏计划推出一个实战专栏,让有兴趣的同学在看文章之余也可以自己动手试一试。


本篇介绍自然语言处理中一种非常重要的任务:命名实体识别。因为最常见的是Bilstm+CRF模型进行实体识别,本文介绍介绍另外一种有效的模型,Dilated-CNN+CRF模型,但是两种模型的代码都会给出。


作者&编辑 | 小Dream哥

1 命名实体识别任务介绍 

笔者在这篇文章中,曾经系统的介绍过命名实体识别任务的相关概念语料标注方式,不了解的同学可以先阅读这篇文章:

【NLP-NER】什么是命名实体识别?


关于Bilstm和Dilated-CNN两个模型理论方面的内容,笔者在这篇文章中做了详细的介绍,不了解的同学可以先阅读这篇文章:

【NLP-NER】命名实体识别中最常用的两种深度学习模型


话不多说,既然是实战篇,我们就赶紧开始吧。

2 数据预处理

1) 查看数据格式


先了解一下数据格式,方便后面进行处理。如下图所示,语料为标准的BIO标注方式,每个字和标记之间用空格隔开,语料之间用一个空行隔开

2)读取训练数据

def load_sentences(path, lower, zeros):
   """
   加载训练,测试,验证数据的函数
   """
   sentences = []
   sentence = []
   num = 0
   for line in codecs.open(path, 'r', 'utf8'):
       num+=1
       line = zero_digits(line.rstrip()) if zeros else line.rstrip()
       if not line:
           if len(sentence) > 0:
               if 'DOCSTART' not in sentence[0][0]:
                   sentences.append(sentence)
               sentence = []
       else:
           if line[0] == " ":
               line = "$" + line[1:]
               word = line.split()
           else:
               word= line.split( )
           assert len(word) == 2
           sentence.append(word)
   if len(sentence) > 0:
       if 'DOCSTART' not in sentence[0][0]:
           sentences.append(sentence)
   return sentences

上面这个函数是用来读取训练,测试,验证数据的,该函数返回一个列表,如下图所示:

3) 标记格式转化


因为原始语料用的是BIO标注方法,但是BIOES标注形式包含了实体内更多的位置和边界信息,最好将其转化为BIOES的形式,方法如下:

def update_tag_scheme(sentences, tag_scheme):
   """
将IOB格式转化为BIOES格式。
   """
   for i, s in enumerate(sentences):
       tags = [w[-1] for w in s]
       # 保证语料上BIO格式
       if not iob2(tags):
           s_str = '\n'.join(' '.join(w) for w in s)
##产生异常
       if tag_scheme == 'iob':
           # 转化为BIOES
           for word, new_tag in zip(s, tags):
               word[-1] = new_tag
       elif tag_scheme == 'iobes':
           new_tags = iob_iobes(tags)
           for word, new_tag in zip(s, new_tags):
               word[-1] = new_tag
       else:
           raise Exception('Unknown tagging scheme!')

看看效果如何:


4)构造字典

经过上述步骤,虽然我们将数据读到了一个列表里组织了起来,但是一个一个的汉字及其标签,后面的LSTM或者CNN网络是没有办法处理的,需要进行词嵌入。在此之前需要构造2个字典,汉字字典和标签字典,然后将上面的汉字和标签转化成字典里的序号。看看代码是怎么做的:

def char_mapping(sentences, lower):
      #生成字典和mapping
   chars = [[x[0].lower() if lower else x[0] for x in s] for s in sentences]
   dico = create_dico(chars)
   dico["<PAD>"] = 10000001
   dico['<UNK>'] = 10000000
   char_to_id, id_to_char = create_mapping(dico)
   #print("Found %i unique words (%i in total)" % (
   #    len(dico), sum(len(x) for x in chars)
   #))
   return dico, char_to_id, id_to_char


def create_dico(item_list):
  #根据传入的列表,生成一个字典
   assert type(item_list) is list
   dico = {}
   for items in item_list:
       for item in items:
           if item not in dico:
               dico[item] = 1
           else:
               dico[item] += 1
   return dico


def create_mapping(dico):
   #生成连个字典,id->word word->id
   sorted_items = sorted(dico.items(), key=lambda x: (-x[1], x[0]))
   id_to_item = {i: v[0] for i, v in enumerate(sorted_items)}
   item_to_id = {v: k for k, v in id_to_item.items()}
   return item_to_id, id_to_item

上述过程并不难理解,大家好好看看,我们看看生成的字典:

5) 根据字典组织好训练数据


训练时,应该是输入汉字序列,然后预测出一个BIO序列,所以我们的输入数据还需要整理,将汉字和标记分开,得到汉字序列及其标记序列,并且要将汉字及其标记转换成他们在字典里的序号,转化结果如下:

至此,我们将训练数据结构化的组织了起来,放在一个大列表里,并且列表里的数据通过生成的字典转化成了id。

3 模型实现

1)word embedding

我们将语料中的中文换成了他们在字典里的ID,了解词向量的同学都知道我们还需要对词进行词嵌入,后面的深度学习模型才能进行处理。不了解的同学可以找词向量的文章看一下。


下面看看如何实现:

def embedding_layer(self, char_inputs, seg_inputs, config, name=None):
   """
:param char_inputs: one-hot encoding of sentence
   :param seg_inputs: segmentation feature
   :param config: wither use segmentation feature
   :return: [1, num_steps, embedding size],

   """

   embedding = []
   with tf.variable_scope("char_embedding" if not name else name), tf.device('/cpu:0'):

# 生成一个表,用于后面查表
       self.char_lookup = tf.get_variable(
               name="char_embedding",
               shape=[self.num_chars, self.char_dim],
               initializer=self.initializer)

# 查表,将词向量化 

        embedding.append(tf.nn.embedding_lookup(self.char_lookup, char_inputs))

# 分词的维度,可先不关注
       if config["seg_dim"]:
           with tf.variable_scope("seg_embedding"), tf.device('/cpu:0'):
               self.seg_lookup = tf.get_variable(
                   name="seg_embedding",
                   shape=[self.num_segs, self.seg_dim],
                   initializer=self.initializer)

        embedding.append(tf.nn.embedding_lookup(self.seg_lookup, seg_inputs))
       embed = tf.concat(embedding, axis=-1)
   return embed

也许有同学会问,这里的词向量表是随机初始化的。如果我之前训练好了一份词向量,该如何使用呢?可以在定义好模型之后,char_lookup.assign函数为该词向量表赋值,参考如下:

通过embedding_layer,就将形状为[bacth_size, seq_length]的输入数据转化为形状为[bacth_size, seq_length,embeding_dim]的矩阵了。词向量化几乎是所有自然语言处理任务所必须要经过的步骤,读者务必弄明白。


2)构建dilated CNN模型

首先,做一些数据的维度变化,并做一次CNN特征提取

def IDCNN_layer(self, model_inputs,
               name=None):
    #param idcnn_inputs: [batch_size, num_steps, emb_size]
    #return: [batch_size, num_steps, cnn_output_width]


   model_inputs = tf.expand_dims(model_inputs, 1)
   reuse = False
   if self.dropout == 1.0:
       reuse = True
   with tf.variable_scope("idcnn" if not name else name):
       shape=[1, self.filter_width, self.embedding_dim,
                  self.num_filter]
       filter_weights = tf.get_variable(
           "idcnn_filter",
           shape=[1, self.filter_width, self.embedding_dim,
                  self.num_filter],
           initializer=self.initializer)

        #输入的尺寸 = [batch, in_height, in_width, in_channels]
        #卷积核尺寸 [filter_height, filter_width, in_channels, out_channels]


       layerInput = tf.nn.conv2d(model_inputs,
                                 filter_weights,
                                 strides=[1, 1, 1, 1],
                                 padding="SAME",                                                         name="init_layer",

                                 use_cudnn_on_gpu=True)
       finalOutFromLayers = []
       totalWidthForLastDim = 0

#重复4次增强特征提取能力
       for j in range(self.repeat_times):

#每次有3次卷积操作,前两次卷积膨胀系数为

              1,后一次膨胀系数为2
           for i in range(len(self.layers)):
               dilation = self.layers[i]['dilation']
               isLast = True if i == (len(self.layers) - 1) else False
               with tf.variable_scope("atrous-conv-layer-

                                      %d" % i,
                                      reuse=True
                                      if (reuse or j > 0) else False):
#卷积核

                    w = tf.get_variable(
                       "filterW",
                       shape=[1, self.filter_width, 

                                    self.num_filter,
                                    self.num_filter],

                       initializer=tf.contrib.layers.

                                         xavier_initializer())
                   b = tf.get_variable("filterB", shape=[self.num_filter])
                   conv = tf.nn.atrous_conv2d(layerInput,w,
                                              rate=dilation,
                                              padding="SAME")
                   conv = tf.nn.bias_add(conv, b)
                   conv = tf.nn.relu(conv)
                   if isLast:
                       finalOutFromLayers.append(conv)
                       totalWidthForLastDim += self.num_filter
                   layerInput = conv
       finalOut = tf.concat(axis=3, values=finalOutFromLayers)
       keepProb = 1.0 if reuse else 0.5
       finalOut = tf.nn.dropout(finalOut, keepProb)

       finalOut = tf.squeeze(finalOut, [1])
       finalOut = tf.reshape(finalOut, [-1, totalWidthForLastDim])
       self.cnn_output_width = totalWidthForLastDim
       return finalOut

重复做卷积操作进行特征提取,膨胀卷积可以扩大卷积核的感受野,提高卷积操作对长序列特征的提取能力。同时加深深度,也可以进一步提高特征提取能力。如下,repeat times为4,dilation数组为[1,1,2]。


3)project层

用一个中间层,将CNN的输出转化为shape为[batch_size, num_steps, num_tags]的输出,后面可以接softmax作为输出,也可以接crf计算loss


4)CRF层计算loss

采用CRF计算损失几乎是序列标注问题的标准做法,关于CRF的相关问题,笔者在机器学习模型介绍条件随机场CRF一文中做了详细介绍,不了解的读者可以出门看看。

4 开始训练

在main文件开头的参数配置中,选择train为True:

flags.DEFINE_boolean("train",       True,      "Whether train the model")

执行python main.py即可以开始进行训练,学习率,batch_size,dropout等超参数,读者可以自行调整。

训练开始后,会有如下的打印信息,读者可以观察loss下降的趋势。

训练结束后,会在ckpt文件夹生成模型文件。

5 模型测试

在main文件开头的参数配置中,选择train为False:

flags.DEFINE_boolean("train",       False,      "Whether train the model")

我们看看结果是怎么样的:

输入“明天我想去深圳市”模型将“明天”作为TIME提取出来了,将“深圳市”作为LOC提取出来了。


至此,介绍了如何利用构建DI-CNN模型进行命名实体识别,代码在我们有三AI的github可以下载:https://github.com/longpeng2008/yousan.ai/tree/master/natural_language_processing

找到ner文件夹,执行python3 main.py就可以训练或者测试了。

总结


NER是一个非常基础,但是非常重要的任务,在具体的操作中,相信大家能够更为细致的体会NER任务的真正作用和意涵。


我们也会在知识星球还介绍了基于BERT来做NER的方法,感兴趣扫描下面的二维码了解。


读者们可以留言,或者加入我们的NLP群进行讨论。感兴趣的同学可以微信搜索jen104,备注"加入有三AI NLP群"


下期预告:命名实体识别实践

知识星球推荐

扫描上面的二维码,就可以加入我们的星球,助你成长为一名合格的自然语言处理算法工程师。


知识星球主要有以下内容:


(1) 聊天机器人。考虑到聊天机器人是一个非常复杂的NLP应用场景,几乎涵盖了所有的NLP任务及应用。所以小Dream哥计划以聊天机器人作为切入点,通过介绍聊天机器人的原理和实践,逐步系统的更新到大部分NLP的知识,会包括语义匹配,文本分类,意图识别,语义匹配命名实体识别、对话管理以及分词等。


(2) 知识图谱。知识图谱对于NLP各项任务效果好坏的重要性,就好比基础知识对于一个学生成绩好坏的重要性。他是NLP最重要的基础设施,目前各大公司都在着力打造知识图谱,作为一个NLP工程师,必须要熟悉和了解他。


(3) NLP预训练模型。基于海量数据,进行超大规模网络的无监督预训练。具体的任务再通过少量的样本进行Fine-Tune。这样模式是目前NLP领域最火热的模式,很有可能引领NLP进入一个全新发展高度。你怎么不深入的了解?


转载文章请后台联系

侵权必究

往期精选


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存