自然语言处理之BERT

1.简介

         嘘!BERT来了,就是那个同时刷新了11个NLP任务记录的模型。从本质上来bert属于一个预训练模型,模型可以理解上下文和单词之间的关系,也可以理解句子和句子之间的关系。针对不同的任务,可以利用BERT进行不同的迁移学习。模型结构上来讲,其相当于是对Transformer中Encoder层的堆积(Bidirectional Encoder Representations from Transformers)。就这么一个本质上朴实无华的模型,是如何一时激起千层浪,将平静的NLP世界搞的天翻地覆的。下面让我们一起来领略一下其迷人之处,如果还不了解Transfomer的同学强烈建议先去学习Transformer

2.BERT理论

      1.网络结构

              我们首先了解一下bert执行的整体流程,从图中看出预训练和微调时候使用的主体结构是不变的。首先每次的输入是两个句子组成的句子对,会随机的对一些词进行mask,使用CLS作为起始标志,SEP作为句子间分隔标志。然后这些词会经过图二中的嵌入层,有三层嵌入信息:词嵌入信息、句子嵌入信息、位置嵌入信息。之后接多个Transfomer中的encoder层。最后的输出去预测mask的单词,以及句子B是否为句子A的下一句子。

       

     

   2.多任务学习 

           从上面的介绍中我们知道,bert是一个多任务学习的模型。

              1.Masked Language Model:首先在每一个句子中会随机的mask掉一部分的单词,有点类似于Transfomer中的decoder部分的mask。在实际训练时,会随机的mask每个句子15%的词。80%的时间使用mask,10%的时间使用另外一个词代替mask,10%的时间使用全部句子信息。

             为什么要是使用mask,我们前面说过bert相当于是将transfomer中encoder进行堆叠。encoder中的第一层self-attention会进行单词之间两两计算,也就是每一个单词中都会包含所有单词的信息(这样相当于要预测的信息已经别暴露)。encoder的本质相当于是对单词进行编码的过程,输入和输出长度一样(不是为了预测任务而设计的)。所以为了使模型具有预测的功能,同时解决信息暴露的问题。需要对encoder进行改造,借鉴自然语言模型中使用周围的词来预测中间词的机制,同时借鉴decoder中的mask机制。我们在预测的时候随机mask掉一些词,并对这些词进行预测。
              2.Next Sentence Prediction:同时我们想让模型具备理解句子和句子之间关系的能力,比如给定两个句子,判断他们两之间是不是上下文的关系。对此我们设计了使用两个句子作为句子对进行输入。后面的embedding层会使每个单词携带各自所属的句子信息。

            这样,我们通过对输入和输出进行改造。使我们的预训练模型 具有了多重的功能,进行简单的微调就可以很好的完成多种工作,比如文本分类、序列标注(分词、实体识别、词性标注),判断句子关系(QA、自然语言推理)等

    3.Fine-tune

            BERT模型的初衷就是进行微调,以适应多种的下游任务。其设计也是非常方便进行微调。进行微调一般会有两种思路:

           1.将现有的模型作为一个特征提取器,去掉其输出层,后面接对应任务所需要的输出层。当然根据实际情况,并不一定只提去最后一层的特征输出,可以是自己需要的任何层。

           2.在现有模型的基础上进行训练,使新的模型更适合自己的任务。

           3.有时候根据实际的情况,我们并不需要对所有的层都进行fine-tune,这时候只对部分的层进行fine-tune,后面接自己的输出层。

   4.理论总结

             上面我们介绍了bert的理论部分,一种基于Transfomer的encoder层堆积模型。为了进行预测,并且防止信息泄露,采用了mask机制。为了提取更高级的语义信息,并且是模型具有判断句子关系的能力,使用了next-sentence机制。没有太多花哨的操作,说太多就啰嗦了。但 其效果确实刷新了人们的认知,这也许就是所谓的大道至简吧!致敬!

3.源码

         本文使用tensorflow版本BERT源进行学习,github原始链接戳此!BERT的运行对于硬件要求比较高,本人现在还是比较穷。所以我只是尝试着去使用BERT来Fine-Tuning自己的项目,后来很自然的失败了。当然为了对得起自己,我试过了各种办法,依然失败!此时我觉得这就是上帝的意思了,革命尚未成功、同志仍可放弃。再坚持也是浪费时间,所以我就果断的放弃。

         但是代码我还是有认认真真的看一遍的,总得尽人事嘛!我看代码分为两部分,一尝试着给源码写注释,二值得学习的部分自己敲一遍。最主要的是要心情轻松,带着脑子。下面,开启我们的源码之旅吧!

1.训练流程思考

         其实和tensorflow传统的训练流程是一样的,无非这么几步:数据预处理、构建模型、训练、验证。都是围绕着一个核心模型来的。我们把模型想象成一个婴儿,数据就是食物,构建模型就是他本身,训练就是他去学习成长,验证就是人生交出的一份份答卷。不断地迭代这个过程他就不断的长大,欠拟合就是孩子太自卑了,过拟合就是太自负。最终的目的就是长大成人,为社会做出自己的贡献,这意味着模型使用。就像人无完人一样,我们并不要求模型必须找到全局最优,这意味着一个原则——模型堪用就行。

        数据不一样,意味着有的孩子家庭富有,有人出身贫寒。数据决定了模型的上限,所以一个人出生的高度在一定程度上决定了人生高度的范围。当然学习的好可能会有更好的表现。不同的学习率,意味着孩子不一样的性格,有的积极却冲动,有的谨慎却胆小。

        当然,最终每个人的命运也是完全不一样的。伟大如BERT这样,那就万人追捧,时代的明星。像我4G的显存,抱着玩一玩心态训练出来的模型注定不得志。这就是我们的模型,这也是一部分的我们。

2.参数定义 

        BERT运行时需要指定一些参数,包括data_dir(数据)、bert_config_file(配置文件)、task_name(任务名)、vocab_file(词表)、init_checkpoint(预训练模型)等必传项和一些非必传项。使用tf.flags类来实现获取命令行参数功能,其中tf.app.run()方法会找定义的 main方法,并且校验参数必传项!   

#使用tf.flags来定义命令行参数,然后使用FLAGS来获取对应的参数
def main(_):
    print('she is a very beautilful girl,she name is',FLAGS.name)
flags = tf.flags
FLAGS = flags.FLAGS
flags.DEFINE_string('name', None, 'who is the one you love')
flags.mark_flag_as_required('name')
tf.app.run()

3.训练步数疑惑

  我一直对epoch和step的区别搞的不是很清楚。接着这个机会解释一下。

     batch_size:每个批次的样本数量

     steps:训练的步数,每更新一次参数对应的是一步(相当于梯度下降了一步)

     epochs:表示全部的数据要被过几遍。

    当然,一般还会接一个warmup(预热的步数),一般使用一个较小的学习率训练一定的轮次,目的是使得模型能够稳定下来。可以在一定程度上防止过拟合。

  if FLAGS.do_train:
    #获取训练的数据
    train_examples = processor.get_train_examples(FLAGS.data_dir)
    #训练步数 = 总样本数*轮次/批次大小
    num_train_steps = int(
        len(train_examples) / FLAGS.train_batch_size * FLAGS.num_train_epochs)
    #根据warmup比例计算warmup的步数
    num_warmup_steps = int(num_train_steps * FLAGS.warmup_proportion)

4.embedding层

      首先会得到输入的每个词对应的embedding,然后初始化位置信息embedding层,并且根据单词所属的句子计算Token_Type的embedding。将三者加和得到最终的嵌入层,对应论文中的三层嵌入。只不过位置embedding并非使用余弦正弦函数,而是通过网络的学习得到。

5.mask的实现

      attention_mask中,正常的词是1,被mask的词是0。最终计算后得到的adder,正常的词为0,mask的词为-10000.再将该分值加到原来的注意力score上,被mask的单词就是一个很小的值。以这样的方式来进行mask。

  if attention_mask is not None:
    # `attention_mask` = [B, 1, F, T]
    attention_mask = tf.expand_dims(attention_mask, axis=[1])

    # Since attention_mask is 1.0 for positions we want to attend and 0.0 for
    # masked positions, this operation will create a tensor which is 0.0 for
    # positions we want to attend and -10000.0 for masked positions.
    adder = (1.0 - tf.cast(attention_mask, tf.float32)) * -10000.0

    # Since we are adding it to the raw scores before the softmax, this is
    # effectively the same as removing these entirely.
    attention_scores += adder

6.变量上下文管理

        tf.variable_scope可以作为一个变量上下文管理器,使用tf.get_variable()定义变量。即该变量是属于该上下文的,当变量比较多,变量名重复的时候。使用这种方法可以很好的将不同上下文下的变量区分开来,变量名相同,但是在不同的上下文,即属于不同的变量。

        同时可以使用reuse,重复使用一个上下文。

with tf.variable_scope("lover"):
    v=tf.get_variable("V",[1])
with tf.variable_scope("lover",reuse=True):
    v1 = tf.get_variable("V", [1])
if v == v1:
    print('两者是同一个变量')
else:
    print('两者不是同一个变量')

4.废话

             爱是一切力量的本源,一直以来我以为灵魂是被肉体所囚禁的,它原本是那么潇洒、自由。直到有一天我忽然明白,心灵的世界再怎么自由,也只是孤零零的一个。它享受着时间、空间。可是它却没有办法去和别的世界交流,融合。只有通过这幅躯壳,这个世界里的爱才可以流出去,别的世界里的爱才可以流进来。我们的灵魂是像水一样的,它需要有温度,而爱就是灵魂的温度。没有了温度的水,就不再流动,也没有快乐。当然,它死不了,只会暂时的冻结。如果有一天温暖的阳光重新照射进来,它还是可以被融化的。爱的能力也是有大有小,形式多种多样。伟大的土地有它的爱,天空白云有它们的爱,日月星辰都有自己那源源不断的爱。我不知道它们的眼睛和心灵在哪里,毕竟我只是生活在其表面的一个渺小的人而已。但我相信,它们生活的世界一定会有更丰富的色彩,更美妙的音乐,那个大世界一定更热闹。也许当我这短暂的一生结束后,也可以参与到它们之中。所以我并不害怕此生结束后,面对的是一片黑暗。人们可以看到的光是很少的一部分,想象有一天我可以看到光全部的样子。当然,到时候肯定会有不一样的眼睛。至此,我热爱此生,任凭时光流逝,一无所惧!

【Trap音乐 X黑客帝国】

猜你喜欢

转载自blog.csdn.net/gaobing1993/article/details/108711664