【NLP实践-Task10 BERT】Transformer & BERT

本文深入解析Transformer模型原理及其核心组件,如自我注意力机制、多头注意力等,并介绍BERT模型的创新之处,包括Masked LM和Next Sentence Prediction。此外,还提供了使用BERT进行文本分类和生成句向量的实战指南。
摘要由CSDN通过智能技术生成

目录

Transformer原理

Transformer总体结构

Self-Attention

Multi-Headed Attention

Positional Encoding

Layer normalization

Decoder层

Mask

Padding Mask

Sequence mask

输出层

BERT的原理

1. Masked LM (MLM)

2. Next Sentence Prediction (NSP)

如何使用 BERT?

利用预训练BERT文本分类实战(待复现)

将句子转换为句向量

传统的句向量

BERT句向量

文本分类

简介

前期准备

代码

优化

参考


Transformer原理

       论文地址:Attention Is All You Need:https://arxiv.org/abs/1706.03762

       Transformer是一种完全基于Attention机制来加速深度学习训练过程的算法模型。Transformer最大的优势在于其在并行化处理上做出的贡献。

       Transformer抛弃了以往深度学习任务里面使用到的 CNN 和 RNN ,目前大热的Bert就是基于Transformer构建的,这个模型广泛应用于NLP领域,例如机器翻译,问答系统,文本摘要和语音识别等等方向。

Transformer总体结构

       和Attention模型一样,Transformer模型中也采用了 encoer-decoder 架构。但其结构相比于Attention更加复杂,论文中encoder层由6个encoder堆叠在一起,decoder层也一样。

                

       每一个encoder和decoder的内部简版结构如下图:

                   

       对于encoder,包含两层,一个self-attention层和一个前馈神经网络,self-attention能帮助当前节点不仅仅只关注当前的词,从而能获取到上下文的语义。

       decoder也包含encoder提到的两层网络,但是在这两层中间还有一层attention层,帮助当前节点获取到当前需要关注的重点内容。

       现在我们知道了模型的主要组件,接下来我们看下模型的内部细节。首先,模型需要对输入的数据进行一个embedding操作,也可以理解为类似w2c的操作,embedding结束之后,输入到encoder层,self-attention处理完数据后把数据送给前馈神经网络,前馈神经网络的计算可以并行,得到的输出会输入到下一个encoder。

                     

Self-Attention

       接下来我们详细看一下self-attention,其思想和attention类似,但是self-attention是Transformer用来将其他相关单词的“理解”转换成我们正在处理的单词的一种思路,我们看个例子:
       The animal didn't cross the street because it was too tired
       这里的it到底代表的是animal还是street呢,对于我们来说能很简单的判断出来,但是对于机器来说,是很难判断的,self-attention就能够让机器把it和animal联系起来,接下来我们看下详细的处理过程。

       1、首先,self-attention会计算出三个新的向量,在论文中,向量的维度是512维,我们把这三个向量分别称为Query、Key、Value,这三个向量是用embedding向量与一个矩阵相乘得到的结果,这个矩阵是随机初始化的,维度为(64,512)注意第二个维度需要和embedding的维度一样,其值在BP的过程中会一直进行更新,得到的这三个向量的维度是64低于embedding维度的。
                  

       那么Query、Key、Value这三个向量又是什么呢?这三个向量对于attention来说很重要,当你理解了下文后,你将会明白这三个向量扮演者什么的角色。

       2、计算self-attention的分数值,该分数值决定了当我们在某个位置encode一个词时,对输入句子的其他部分的关注程度。这个分数值的计算方法是Query与Key做点成,以下图为例,首先我们需要针对Thinking这个词,计算出其他词对于该词的一个分数值,首先是针对于自己本身即q1·k1,然后是针对于第二个词即q1·k2

                       

       3、接下来,把点成的结果除以一个常数,这里我们除以8,这个值一般是采用上文提到的矩阵的第一个维度的开方即64的开方8,当然也可以选择其他的值,然后把得到的结果做一个softmax的计算。得到的结果即是每个词对于当前位置的词的相关性大小,当然,当前位置的词相关性肯定会会很大

                     

       4、下一步就是把Value和softmax得到的值进行相乘,并相加,得到的结果即是self-attetion在当前节点的值。

                         

       在实际的应用场景,为了提高计算速度,我们采用的是矩阵的方式,直接计算出Query, Key, Value的矩阵,然后把embedding的值与三个矩阵直接相乘,把得到的新矩阵Q与K相乘,乘以一个常数,做softmax操作,最后乘上V矩阵:

                                                       

                           

       这种通过 query 和 key 的相似性程度来确定 value 的权重分布的方法被称为scaled dot-product attention。

Multi-Headed Attention

       这篇论文更牛逼的地方是给self-attention加入了另外一个机制,被称为“multi-headed” attention,该机制理解起来很简单,就是说不仅仅只初始化一组Q、K、V的矩阵,而是初始化多组,tranformer是使用了8组,所以最后得到的结果是8个矩阵。

                         

                                   

       这给我们留下了一个小的挑战,前馈神经网络没法输入8个矩阵呀,这该怎么办呢?所以我们需要一种方式,把8个矩阵降为1个,首先,我们把8个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个矩阵和这个组合好的矩阵相乘,最后得到一个最终的矩阵。

                        

       这就是multi-headed attention的全部流程了,这里其实已经有很多矩阵了,我们把所有的矩阵放到一张图内看一下总体的流程。

        

Positional Encoding

       到目前为止,transformer模型中还缺少一种解释输入序列中单词顺序的方法。为了处理这个问题,transformer给encoder层和decoder层的输入添加了一个额外的向量Positional Encoding,维度和embedding的维度一样,这个向量采用了一种很独特的方法来让模型学习到这个值,这个向量能决定当前词的位置,或者说在一个句子中不同的词之间的距离。这个位置向量的具体计算方法有很多种,论文中的计算方法如下:

                                                        

       其中pos是指当前词在句子中的位置,i是指向量中每个值的index,可以看出,在偶数位置,使用正弦编码,在奇数位置,使用余弦编码,这里提供一下代码。
 

position_encoding = np.array(
    [[pos / np.power(10000, 2.0 * (j // 2) / d_model) for j in range(d_model)] for pos in range(max_seq_len)])

position_encoding[:, 0::2] = np.sin(position_encoding[:, 0::2])
position_encoding[:, 1::2] = np.cos(position_encoding[:, 1::2])

       最后把这个Positional Encoding与embedding的值相加,作为输入送到下一层。

          

Layer normalization

       在transformer中,每一个子层(self-attetion,ffnn)之后都会接一个残缺模块,并且有一个Layer normalization

                       

       残缺模块相信大家都很清楚了,这里不再讲解,主要讲解下Layer normalization。Normalization有很多种,但是它们都有一个共同的目的,那就是把输入转化成均值为0方差为1的数据。我们在把数据送入激活函数之前进行normalization(归一化),因为我们不希望输入数据落在激活函数的饱和区。

       说到 normalization,那就肯定得提到 Batch Normalization。BN的主要思想就是:在每一层的每一批数据上进行归一化。我们可能会对输入数据进行归一化,但是经过该网络层的作用后,我们的数据已经不再是归一化的了。随着这种情况的发展,数据的偏差越来越大,我的反向传播需要考虑到这些大的偏差,这就迫使我们只能使用较小的学习率来防止梯度消失或者梯度爆炸。

       BN的具体做法就是对每一小批数据,在批这个方向上做归一化。如下图所示:   

                                                         
       可以看到,右半边求均值是沿着数据 batch_size的方向进行的,其计算公式如下:

                                                                 

       那么什么是 Layer normalization 呢?它也是归一化数据的一种方式,不过 LN 是在每一个样本上计算均值和方差,而不是BN那种在批方向计算均值和方差!

                                                            

 

       下面看一下 LN 的公式:

                                                   

       到这里为止就是全部encoders的内容了,如果把两个encoders叠加在一起就是这样的结构:

          

 

Decoder层

                           

 

       上图是transformer的一个详细结构,相比本文一开始结束的结构图会更详细些,接下来,我们会按照这个结构图讲解下decoder部分。

       可以看到decoder部分其实和encoder部分大同小异,不过在最下面额外多了一个masked mutil-head attetion,这里的mask也是transformer一个很关键的技术,我们一起来看一下。

Mask

       mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。

       其中,padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

Padding Mask

       什么是 padding mask 呢?因为每个批次输入序列长度是不一样的也就是说,我们要对输入序列进行对齐。具体来说,就是给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以我们需要进行一些处理。

       具体的做法是,把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!

       而我们的 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。

Sequence mask

       文章前面也提到,sequence mask 是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此我们需要想一个办法,把 t 之后的信息给隐藏起来。

       那么具体怎么做呢?也很简单:产生一个上三角矩阵,上三角的值全为0。把这个矩阵作用在每一个序列上,就可以达到我们的目的。

  • 对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。
  • 其他情况,attn_mask 一律等于 padding mask。

输出层

       当decoder层全部执行完毕后,怎么把得到的向量映射为我们需要的词呢,很简单,只需要在结尾再添加一个全连接层和softmax层,假如我们的词典是1w个词,那最终softmax会输入1w个词的概率,概率值最大的对应的词就是我们最终的结果。

           

 

BERT的原理

       BERT 的创新点在于它将双向 Transformer 用于语言模型,
       之前的模型是从左向右输入一个文本序列,或者将 left-to-right 和 right-to-left 的训练结合起来。
       实验的结果表明,双向训练的语言模型对语境的理解会比单向的语言模型更深刻,
       论文中介绍了一种新技术叫做 Masked LM(MLM),在这个技术出现之前是无法进行双向语言模型训练的。

       BERT 利用了 Transformer 的 encoder 部分。
       Transformer 是一种注意力机制,可以学习文本中单词之间的上下文关系的。
       Transformer 的原型包括两个独立的机制,一个 encoder 负责接收文本作为输入,一个 decoder 负责预测任务的结果。
       BERT 的目标是生成语言模型,所以只需要 encoder 机制。

       Transformer 的 encoder 是一次性读取整个文本序列,而不是从左到右或从右到左地按顺序读取,
       这个特征使得模型能够基于单词的两侧学习,相当于是一个双向的功能。

       下图是 Transformer 的 encoder 部分,输入是一个 token 序列,先对其进行 embedding 称为向量,然后输入给神经网络,输出是大小为 H 的向量序列,每个向量对应着具有相同索引的 token。

                     
       当我们在训练语言模型时,有一个挑战就是要定义一个预测目标,很多模型在一个序列中预测下一个单词,
       “The child came home from ___”
       双向的方法在这样的任务中是有限制的,为了克服这个问题,BERT 使用两个策略:

1. Masked LM (MLM)

       在将单词序列输入给 BERT 之前,每个序列中有 15% 的单词被 [MASK] token 替换。 然后模型尝试基于序列中其他未被 mask 的单词的上下文来预测被掩盖的原单词。

       这样就需要:

  1. 在 encoder 的输出上添加一个分类层
  2. 用嵌入矩阵乘以输出向量,将其转换为词汇的维度
  3. 用 softmax 计算词汇表中每个单词的概率

       BERT 的损失函数只考虑了 mask 的预测值,忽略了没有掩蔽的字的预测。这样的话,模型要比单向模型收敛得慢,不过结果的情境意识增加了。

                         

 

2. Next Sentence Prediction (NSP)

       在 BERT 的训练过程中,模型接收成对的句子作为输入,并且预测其中第二个句子是否在原始文档中也是后续句子。
在训练期间,50% 的输入对在原始文档中是前后关系,另外 50% 中是从语料库中随机组成的,并且是与第一句断开的。

       为了帮助模型区分开训练中的两个句子,输入在进入模型之前要按以下方式进行处理:

  1. 在第一个句子的开头插入 [CLS] 标记,在每个句子的末尾插入 [SEP] 标记。
  2. 将表示句子 A 或句子 B 的一个句子 embedding 添加到每个 token 上。
  3. 给每个 token 添加一个位置 embedding,来表示它在序列中的位置。

       为了预测第二个句子是否是第一个句子的后续句子,用下面几个步骤来预测:

  1. 整个输入序列输入给 Transformer 模型
  2. 用一个简单的分类层将 [CLS] 标记的输出变换为 2×1 形状的向量
  3. 用 softmax 计算 IsNextSequence 的概率

       在训练 BERT 模型时,Masked LM 和 Next Sentence Prediction 是一起训练的,目标就是要最小化两种策略的组合损失函数。

                    

如何使用 BERT?

       BERT 可以用于各种NLP任务,只需在核心模型中添加一个层,例如:

  1. 在分类任务中,例如情感分析等,只需要在 Transformer 的输出之上加一个分类层
  2. 在问答任务(例如SQUAD v1.1)中,问答系统需要接收有关文本序列的 question,并且需要在序列中标记 answer。 可以使用 BERT 学习两个标记 answer 开始和结尾的向量来训练Q&A模型。
  3. 在命名实体识别(NER)中,系统需要接收文本序列,标记文本中的各种类型的实体(人员,组织,日期等)。 可以用 BERT 将每个 token 的输出向量送到预测 NER 标签的分类层。

       在 fine-tuning 中,大多数超参数可以保持与 BERT 相同,在论文中还给出了需要调整的超参数的具体指导(第3.5节)。

                      

利用预训练BERT文本分类实战(待复现)

将句子转换为句向量

传统的句向量

       对于传统的句向量生成方式,更多的是采用word embedding的方式取加权平均,该方法有一个最大的弊端,那就是无法理解上下文的语义,同一个词在不同的语境意思可能不一样,但是却会被表示成同样的word embedding,BERT生成句向量的优点在于可理解句意,并且排除了词向量加权引起的误差。

BERT句向量

       BERT的包括两个版本,12层的transformer与24层的transformer,官方提供了12层的中文模型,下文也将会基于12层的模型来讲解。

       每一层transformer的输出值,理论上来说都可以作为句向量,但是到底应该取哪一层呢,根据hanxiao大神的实验数据,最佳结果是取倒数第二层,最后一层的值太接近于目标,前面几层的值可能语义还未充分的学习到。 
   

       接下来我们从代码的角度来进详细讲解。

       先看下args.py文件,该文件有几个句向量的重要参数,前几个都是路径,这里不再详细解释,这里主要说一下layer_indexes参数与max_seq_len参数,layer_indexes表示的是使用第几层的输出作为句向量,-2表示的是倒数第二层,max_seq_len表示的是序列的最大长度,因为输入的长度是不固定的,所以我们需要设置一个最大长度才能确保输出的维度是一样的,如果最大长度是20,当输入的序列长度小于20的时候,就会补0,如果大于20则会截取前面的部分 ,通常该值会取语料的长度的平均值+2,加2的原因是因为需要拼接两个占位符[CLS](表示序列的开始)与[SEP](表示序列的结束)

model_dir = os.path.join(file_path, 'chinese_L-12_H-768_A-12/')
config_name = os.path.join(model_dir, 'bert_config.json')
ckpt_name = os.path.join(model_dir, 'bert_model.ckpt')
output_dir = os.path.join(model_dir, '../tmp/result/')
vocab_file = os.path.join(model_dir, 'vocab.txt')
data_dir = os.path.join(model_dir, '../data/')

num_train_epochs = 10 
batch_size = 32 
learning_rate = 0.00005   
# gpu使用率 
gpu_memory_fraction = 0.8   
# 默认取倒数第二层的输出值作为句向量 
layer_indexes = [-2]

# 序列的最大程度,单文本建议把该值调小 
max_seq_len = 20

       再来看graph.py文件,该代码的主要目的是把预训练好的模型加载进来,并修改输出层,我们一步一步来看。

       首先创建一个目录,该目录用于存放待输出的文件,定义bert的配置信息路径,根据路径读取配置信息转化为bert_config对象。

tensorflow.python.tools.optimize_for_inference_lib import optimize_for_inference
tf.gfile.MakeDirs(args.output_dir)

config_fp = args.config_name
logger.info('model config: %s' % config_fp)

# 加载bert配置文件 with tf.gfile.GFile(config_fp, 'r') as f:
    bert_config = modeling.BertConfig.from_dict(json.load(f))

       定义三个占位符,分别表示的是对应文本的index,mask与type_index,其中index表示的是在词典中的index,mask表示的是该位置是否有内容,举个例子,例如序列的最大长度是20,有效的字符只有10个字,加上[CLS]与[SEP]两个占位符,那有8个字符是空的,该8个位置设置为0其他位置设置为1,type_index表示的是是否是第一个句子,是第一个句子则设置为1,因为该项目只有一个句子,所以均为1。

logger.info('build graph...')
input_ids = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_ids')
input_mask = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_mask')
input_type_ids = tf.placeholder(tf.int32, (None, args.max_seq_len), 'input_type_ids')

       根据上面定义的三个占位符,定义好输入的张量,实例化一个model对象,该对象就是预训练好的bert模型,然后从check_point文件中初始化权重:

input_tensors = [input_ids, input_mask, input_type_ids]

model = modeling.BertModel(
  config=bert_config,
  is_training=False,
  input_ids=input_ids,
  input_mask=input_mask,
  token_type_ids=input_type_ids,
  use_one_hot_embeddings=False)

tvars = tf.trainable_variables()

init_checkpoint = args.ckpt_name
(assignment_map, initialized_variable_names) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)

tf.train.init_from_checkpoint(init_checkpoint, assignment_map)

       接下来判断一下args.index_layeres参数的长度,如果长度为1,则只取改层的输出,否则遍历需要取的层,把所有层的weight取出来并拼接成一个768*层数的张量:

with tf.variable_scope("pooling"):
  if len(args.layer_indexes) == 1:
        encoder_layer = model.all_encoder_layers[args.layer_indexes[0]]
  else:
        all_layers = [model.all_encoder_layers[l] for l in args.layer_indexes]
        encoder_layer = tf.concat(all_layers, -1)

       接下来是句向量生成的核心代码,这里定义了两个方法,一个mul_mask 和一个masked_reduce_mean,我们先看masked_reduce_mean(encoder_layer, input_mask)这里调用方法时传入的是encoder_layer即输出值,与input_mask即是否有有效文本,masked_reduce_mean方法中又调用了mul_mask方法,即先把input_mask进行了一个维度扩展,然后与encoder_layer相乘,为什么要维度扩展呢,我们看下两个值的维度,我们还是假设序列的最大长度是20,那么encoder_layer的维度为[20,768],为了把无效的位置的内容置为0,input_mask的维度为[20],扩充之后变成了[20,1],两个值相乘,便把input_mask为0的位置的encoder_layer的值改为了0, 然后把相乘得到的值在axis=1的位置进行相加最后除以input_mask在axis=1的维度的和,然后把得到的结果添加一个别名final_encodes

mul_mask = lambda x, m: x * tf.expand_dims(m, axis=-1)
masked_reduce_mean = lambda x, m: tf.reduce_sum(mul_mask(x, m), axis=1) / (
        tf.reduce_sum(m, axis=1, keepdims=True) + 1e-10)

input_mask = tf.cast(input_mask, tf.float32)
pooled = masked_reduce_mean(encoder_layer, input_mask)
pooled = tf.identity(pooled, 'final_encodes')

output_tensors = [pooled]
tmp_g = tf.get_default_graph().as_graph_def()

       最后把得到的句向量重新添加进graph中,并返回graph的路径。

config = tf.ConfigProto(allow_soft_placement=True)
with tf.Session(config=config) as sess:
    logger.info('load parameters from checkpoint...')
    sess.run(tf.global_variables_initializer())
    logger.info('freeze...')
    tmp_g = tf.graph_util.convert_variables_to_constants(sess, tmp_g, [n.name[:-2] for n in output_tensors])
    dtypes = [n.dtype for n in input_tensors]
    logger.info('optimize...')
    tmp_g = optimize_for_inference(
        tmp_g,
  [n.name[:-2] for n in input_tensors],
  [n.name[:-2] for n in output_tensors],
  [dtype.as_datatype_enum for dtype in dtypes],
 False)
tmp_file = tempfile.NamedTemporaryFile('w', delete=False, dir=args.output_dir).name
logger.info('write graph to a tmp file: %s' % tmp_file)
with tf.gfile.GFile(tmp_file, 'wb') as f:
    f.write(tmp_g.SerializeToString())
return tmp_file

       实际的使用和BERT做文本分类的方法类似,只是在返回的EstimatorSpec不太一样,具体细节不在详解。

with tf.gfile.GFile(self.graph_path, 'rb') as f:
    graph_def = tf.GraphDef()
    graph_def.ParseFromString(f.read())

input_names = ['input_ids', 'input_mask', 'input_type_ids']

output = tf.import_graph_def(graph_def,
                             input_map={k + ':0': features[k] for k in input_names},
                             return_elements=['final_encodes:0'])

return EstimatorSpec(mode=mode, predictions={
    'encodes': output[0]
})

文本分类

简介

       BERT(Transformer双向编码器表示)在各种NLP任务中呈现最先进的结果,包括问答系统、自然语言推理等。BERT是一个已经事先采用大量数据进行过训练的模型,泛化能力极强,使用时只需要针对特定领域进行微调即可使用。对于NLP的正常流程来说,我们需要做一些预处理,例如分词、W2V等,BERT包含所有的预训练过程,只需要提供文本数据即可。

       数据集选取的是新浪新闻cnews,包括了[‘体育’, ‘财经’, ‘房产’, ‘家居’, ‘教育’, ‘科技’, ‘时尚’, ‘时政’, ‘游戏’, ‘娱乐’]总共十个主题的新闻数据。

前期准备

1.下载BERT

       我们要使用BERT模型的话,首先要去github上下载相关源码:

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

2.下载bert预训练模型

       Google提供了多种预训练好的bert模型,有针对不同语言的和不同模型大小的。Uncased参数指的是将数据全都转成小写的(大多数任务使用Uncased模型效果会比较好,当然对于一些大小写影响严重的任务比如NER等就可以选择Cased)

       对于中文模型,我们使用Bert-Base, Chinese。下载后的文件包括五个文件:

bert_model.ckpt:有三个,包含预训练的参数
vocab.txt:词表
bert_config.json:保存模型超参数的文件

3. 数据集准备

       数据使用的是新浪新闻分类数据集,每一行组成是 【标签+ TAB + 文本内容】

代码

       BERT非常友好的一点就是对于NLP任务,我们只需要对最后一层进行微调便可以用于我们的项目需求。我们只需要将我们的数据输入处理成标准的结构进行输入就可以了。

       DataProcessor基类

       首先在run_classifier.py文件中有一个基类DataProcessor类:

class DataProcessor(object):
  """Base class for data converters for sequence classification data sets."""

  def get_train_examples(self, data_dir):
    """Gets a collection of `InputExample`s for the train set."""
    raise NotImplementedError()

  def get_dev_examples(self, data_dir):
    """Gets a collection of `InputExample`s for the dev set."""
    raise NotImplementedError()

  def get_test_examples(self, data_dir):
    """Gets a collection of `InputExample`s for prediction."""
    raise NotImplementedError()

  def get_labels(self):
    """Gets the list of labels for this data set."""
    raise NotImplementedError()

  @classmethod
  def _read_tsv(cls, input_file, quotechar=None):
    """Reads a tab separated value file."""
    with tf.gfile.Open(input_file, "r") as f:
      reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
      lines = []
      for line in reader:
        lines.append(line)
      return lines

       在这个基类中定义了一个读取文件的静态方法_read_tsv,四个分别获取训练集,验证集,测试集和标签的方法。接下来我们要定义自己的数据处理的类,我们将我们的类命名为MyTaskProcessor

       编写MyTaskProcessor

       MyTaskProcessor继承DataProcessor,用于定义我们自己的任务

class MyTaskProcessor(DataProcessor):
  """Processor for my task-news classification """
  def __init__(self):
    self.labels = ['体育', '财经', '房产', '家居', '教育', '科技', '时尚', '时政', '游戏', '娱乐']

  def get_train_examples(self, data_dir):
    return self._create_examples(
      self._read_tsv(os.path.join(data_dir, 'cnews.train.txt')), 'train')

  def get_dev_examples(self, data_dir):
    return self._create_examples(
      self._read_tsv(os.path.join(data_dir, 'cnews.val.txt')), 'val')

  def get_test_examples(self, data_dir):
    return self._create_examples(
      self._read_tsv(os.path.join(data_dir, 'cnews.test.txt')), 'test')

  def get_labels(self):
    return self.labels

  def _create_examples(self, lines, set_type):
    """create examples for the training and val sets"""
    examples = []
    for (i, line) in enumerate(lines):
      guid = '%s-%s' %(set_type, i)
      text_a = tokenization.convert_to_unicode(line[1])
      label = tokenization.convert_to_unicode(line[0])
      examples.append(InputExample(guid=guid, text_a=text_a, label=label))
    return examples

       注意这里有一个self._read_tsv()方法,规定读取的数据是使用TAB分割的,如果你的数据集不是这种形式组织的,需要重写一个读取数据的方法,更改“_create_examples()”的实现。

       编写main以及训练

       至此我们就完成了对我们的数据加工成BERT所需要的格式,就可以进行模型训练了。

def main(_):
  tf.logging.set_verbosity(tf.logging.INFO)

  processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
      "mytask": MyTaskProcessor,
  }
python run_classifier.py \

 --task_name=mytask \

 --do_train=true \

 --do_eval=true \

 --data_dir=$DATA_DIR/ \

 --vocab_file=$BERT_BASE_DIR/vocab.txt \

 --bert_config_file=$BERT_BASE_DIR/bert_config.json \

 --init_checkpoint=$BERT_BASE_DIR/bert_model.ckpt \

 --max_seq_length=128 \

 --train_batch_size=32 \

 --learning_rate=2e-5 \

 --num_train_epochs=3.0 \

 --output_dir=/mytask_output

       其中DATA_DIR是你的要训练的文本的数据所在的文件夹,BERT_BASE_DIR是你的bert预训练模型存放的地址。task_name要求和你的DataProcessor类中的名称一致。下面的几个参数,do_train代表是否进行fine tune,do_eval代表是否进行evaluation,还有未出现的参数do_predict代表是否进行预测。如果不需要进行fine tune,或者显卡配置太低的话,可以将do_trian去掉。max_seq_length代表了句子的最长长度,当显存不足时,可以适当降低max_seq_length。 
   

优化

       指定训练时输出loss
       bert自带代码中是这样的,在run_classifier.py文件中,训练模型,验证模型都是用的tensorflow中的estimator接口,因此我们无法实现在训练迭代100步就用验证集验证一次,在run_classifier.py文件中提供的方法是先运行完所有的epochs之后,再加载模型进行验证。训练模型时的代码:

train_input_fn = file_based_input_fn_builder(
        input_file=train_file,
        seq_length=FLAGS.max_seq_length,
        is_training=True,
        drop_remainder=True)
    estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)

       想要实现在训练过程中输出loss日志,我们可以使用hooks参数:

train_input_fn = file_based_input_fn_builder(
        input_file=train_file,
        seq_length=FLAGS.max_seq_length,
        is_training=True,
        drop_remainder=True)
    tensors_to_log = {'train loss': 'loss/Mean:0'}
    logging_hook = tf.train.LoggingTensorHook(tensors=tensors_to_log, every_n_iter=100)
    estimator.train(input_fn=train_input_fn, hooks=[logging_hook], max_steps=num_train_steps)

       增加验证集输出的指标值

       原生BERT代码中验证集的输出指标值只有loss和accuracy,

def metric_fn(per_example_loss, label_ids, logits, is_real_example):
        predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
        accuracy = tf.metrics.accuracy(
            labels=label_ids, predictions=predictions, weights=is_real_example)
        loss = tf.metrics.mean(values=per_example_loss, weights=is_real_example)
        return {
            "eval_accuracy": accuracy,
            "eval_loss": loss,
        }

       但是在分类时,我们可能还需要分析auc,recall,precision等的值。

def metric_fn(per_example_loss, label_ids, logits, is_real_example):
        predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
        accuracy = tf.metrics.accuracy(
            labels=label_ids, predictions=predictions, weights=is_real_example)
        loss = tf.metrics.mean(values=per_example_loss, weights=is_real_example)
        auc = tf.metrics.auc(labels=label_ids, predictions=predictions, weights=is_real_example)
        precision = tf.metrics.precision(labels=label_ids, predictions=predictions, weights=is_real_example)
        recall = tf.metrics.recall(labels=label_ids, predictions=predictions, weights=is_real_example)

        return {
            "eval_accuracy": accuracy,
            "eval_loss": loss,
            'eval_auc': auc,
            'eval_precision': precision,
            'eval_recall': recall,
        }

参考

Transformer模型详解 https://blog.csdn.net/u012526436/article/details/86295971

https://terrifyzhao.github.io/2019/01/11/Transformer%E6%A8%A1%E5%9E%8B%E8%AF%A6%E8%A7%A3.html

搞懂Transformer结构,看这篇PyTorch实现就够了! https://www.tinymind.cn/articles/3834

Transformer注解及PyTorch实现 https://www.cnblogs.com/guoyaohua/p/transformer.html

深度学习:transformer模型 https://blog.csdn.net/pipisorry/article/details/84946653

5 分钟入门 Google 最强NLP模型:BERT https://www.jianshu.com/p/d110d0c13063

bert系列1 https://medium.com/dissecting-bert/dissecting-bert-part-1-d3c3d495cdb3

bert系列2 https://medium.com/dissecting-bert/dissecting-bert-part2-335ff2ed9c73

bert系列3 https://medium.com/dissecting-bert/dissecting-bert-appendix-the-decoder-3b86f66b0e5f

BERT – State of the Art Language Model for NLP https://www.lyrn.ai/2018/11/07/explained-bert-state-of-the-art-language-model-for-nlp/

google开源代码 https://github.com/google-research/bert

使用BERT生成句向量 https://blog.csdn.net/u012526436/article/details/87697242

Mapping a variable-length sentence to a fixed-length vector using BERT model https://github.com/hanxiao/bert-as-service

BERT生成句向量,BERT做文本分类、文本相似度计算 https://github.com/terrifyzhao/bert-utils

干货 | BERT fine-tune 终极实践教程 https://www.jianshu.com/p/aa2eff7ec5c1

小数据福音!BERT在极小数据下带来显著提升的开源实现 https://mp.weixin.qq.com/s?__biz=MzA3MzI4MjgzMw==&mid=2650752891&idx=5&sn=8a44293a57da96db51b9a13feb6223d7&chksm=871a8305b06d0a134e332a6831dbacc9ee79b28a79658c130fe6162f33211788cab18a55ec90&scene=21#wechat_redirect

BERT实战(源码分析+踩坑) https://zhuanlan.zhihu.com/p/58471554

BERT模型实战之多文本分类(附源码) https://blog.csdn.net/Kaiyuan_sjtu/article/details/88709580

使用BERT实现中文的文本分类(PyTorch) https://blog.csdn.net/Real_Brilliant/article/details/84880528

Bert预训练模型-中文文本分类 https://blog.csdn.net/ganxiwu9686/article/details/85061759

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值