论文:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
作者:Jacob Devlin, Ming-Wei Chang, Kenton Lee, Kristina Toutanova
时间:2018
地址:google-research/bert: TensorFlow code and pre-trained models for BERT (github.com)
一、完整代码
完整代码如下:
# 完整代码在这里 import tensorflow as tf import keras_nlp dataset = tf.data.TextLineDataset( ['./data/CoLA_train.tsv'] ) def process_data(x): x = tf.strings.split(x, '\t') return x[3], x[1] dataset = dataset.map(process_data).batch(64, drop_remainder=True) vocabulary = keras_nlp.tokenizers.compute_word_piece_vocabulary( dataset.map(lambda x, y: x), vocabulary_size=4000, lowercase=True, strip_accents=True, ) tokenizer = keras_nlp.tokenizers.WordPieceTokenizer( vocabulary=vocabulary, sequence_length=None, lowercase=True, strip_accents=True, ) cls = tokenizer.vocabulary.index('[CLS]') sep = tokenizer.vocabulary.index('[SEP]') pad = tokenizer.vocabulary.index('[PAD]') segment_packer = keras_nlp.layers.MultiSegmentPacker( sequence_length=128, start_value=cls, end_value=sep, sep_value=sep, pad_value=pad, ) def process_data_(x, y): x = tokenizer(x) input_token, segment_token = segment_packer(x) padding_token = tf.cast(input_token != 0, dtype=tf.int32) x = { 'token_ids': input_token, 'segment_ids': segment_token, 'padding_mask': padding_token, } y = tf.strings.to_number(y, tf.int32) return x, y dataset = dataset.map(process_data_) import numpy as np def positional_encoding(length, depth): depth = depth/2 positions = np.arange(length)[:, np.newaxis] # (seq, 1) depths = np.arange(depth)[np.newaxis, :]/depth # (1, depth) angle_rates = 1 / (10000**depths) # (1, depth) angle_rads = positions * angle_rates # (pos, depth) pos_encoding = np.concatenate([np.sin(angle_rads), np.cos(angle_rads)],axis=-1) return tf.cast(pos_encoding, dtype=tf.float32) class Bert(tf.keras.Model): def __init__(self, vocabulary_size, d_model, encoder_num, intermediate_dim, num_heads, dropout=0.1): super().__init__() self.token_embedding = tf.keras.layers.Embedding(vocabulary_size, d_model) self.segment_embedding = tf.keras.layers.Embedding(2, d_model) self.position_embedding = positional_encoding(128, d_model) self.encoder = [keras_nlp.layers.TransformerEncoder( intermediate_dim=intermediate_dim, num_heads=num_heads, dropout=dropout, ) for _ in range(encoder_num)] self.add = tf.keras.layers.Add() # 分类任务 self.dense_1 = tf.keras.layers.Dense(128, activation='relu') self.dense_2 = tf.keras.layers.Dense(1, activation='sigmoid') def call(self, x): token_embedding = self.token_embedding(x['token_ids']) segment_embedding = self.segment_embedding(x['segment_ids']) position_embedding = tf.concat([positional_encoding(128, 512)[tf.newaxis]]*64, axis=0) output = self.add([token_embedding, segment_embedding, position_embedding]) for item in self.encoder: output = item(output) # 预测序列 # output = self.dense(output) # 分类任务 output = self.dense_1(output[:,0,:]) output = self.dense_2(output) return output bert = Bert(tokenizer.vocabulary_size(), 512, 8, 1024, 8) bert(s[0]) bert.summary() bert.compile( loss=tf.keras.losses.BinaryCrossentropy(), optimizer='adam', metrics=[tf.keras.metrics.BinaryAccuracy()] ) bert.fit(dataset, epochs=10) # 不知道出什么问题,accuracy卡住不动了
二、论文解读
从论文题目BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding中就可以看出来,这是一个语言理解的预训练的双向的Transformers
模型;
BERT其全称为Bidirectional Encoder Representations from Transformers;BERT的设计目的是通过在所有层中联合调节左右上下文,从未标记的文本中预训练深度双向表示;
预先训练过的BERT模型可以只需通过一个额外的输出层来进行微调,从而为广泛的任务创建最先进的模型,如问题回答和语言推理,而不需要进行实质性的任务特定的体系结构修改。类似于ELMo
;
一般来说利用预训练语言模型取完成下游任务有两种方式:feature-based
and fine-tuning
;
feature-based
:类似于ELMo
,把BERT
当作词嵌入模型,再加上特定的结构,对所有预先训练好的参数不在进行训练;fine-tuning
:类似于GPT
,引入最小的任务特定参数,并通过简单地微调所有预先训练好的参数来对下游任务进行训练;
Bert
的结构优势如下所示:
- 使用双向语言模型并结合MLM(掩蔽语言模型)来实现预训练的深度;
- 预先训练的表示减少了对许多精心设计的任务特定架构的需求;
- 使用该预训练提高了11个NLP任务的最新水平;
2.1 模型架构
Bert的主要框架是由Transformer
中的解码块组成的,下图是Transformer
架构:
BERT
有base模型和large模型;
- base:L=12, H=768, A=12, Total Parameters=110M
- large:L=24, H=1024, A=16, Total Parameters=340M
其中L
表示Encoder Block块数,H
表示隐藏层维度,A表示多头注意力的头数
在这里是不是很奇怪,Bert
不是Bidirectional Encoder Representations from Transformers吗?双向在哪里?这里要解释的是,Transformer
的encoder
模块其本质是多连接的,这就导致其可以与前面的元素交互,也可以对后面的元素交互,所以叫做Bidirectional Encoder
被BERT
使用,而Transformer
的decoder
需要context
做key
和value
,同时有mask
机制,掩盖了后面的信息,让文本只注意到前面的信息,是一个left-to-right
的架构做的文本生成;
模型架构如图:
有没有感觉非常简单,其中我感觉只需要注意的是输入层的和输出层;其中有两个关键的模型Masked LM
和Next Sentence Prediction
;
2.2 输入层
首先语言模型的输入基本都是一串字符串,第一步要进行分词处理,论文中采取的办法是WordPiece
,其模型实现很简单,我之后会在另一篇文章NLP: tokenizer-CSDN博客中对所有的NLP tokenizer
进行分析,这里不再阐述;
分词时需要注意的是,sentences
被打包成一个单一的sequence
,每个sequence
的第一个标记总是一个特殊的分类标记[CLS]
。与该标记对应的最终隐藏状态被用作分类任务的聚合序列表示。我们用两种方式来区分这些句子。首先,我们用一个特殊的标记[SEP]
将它们分开。
分词后,我们把my dog is cute, he likes playing
转化为了['<cls>', 'my', 'dog', 'is', 'cute', '<sep>', 'he', 'likes', 'play', '##ing', '<sep>']
接下来就是关键模型Masked LM
;
Masked LM
从直觉来说,深度的双向语言模型要比浅层的单向模型更强大,因为深度模型可以学到的东西更丰富;但是标准语言模型的训练模式一般是left-to-right
或者right-to-left
,而bert
是一个双向的语言模型,在训练过程中可以清楚的看到上下文信息,所以简单的提高模型的深度并不能够让机器更好的深入理解文本信息;为了合理的提高模型的深度,让机器深入理解文本信息,这里论文中使用了一个很巧妙的方法,一般来说,语言序列中缺少某几个字基本不会对我们理解的大意产生影响,例如英语的完形填空;为了让模型可以像我们一样深入的理解语言序列,我们可以随机的对一些词语进行mask
,让语言模型在训练的时候做预测任务进而加强对语言的理解;
MLM是指在训练的时候随即从输入语料上mask掉一些单词,然后通过的上下文预测该单词,该任务非常像我们在中学时期经常做的完形填空。正如传统的语言模型算法和RNN匹配那样,MLM的这个性质和Transformer的结构是非常匹配的。在BERT的实验中,15%的WordPiece Token会被随机Mask掉。在训练模型时,一个句子会被多次喂到模型中用于参数学习,但是Google并没有在每次都mask掉这些单词,而是在确定要Mask掉的单词之后,做以下处理。
- 80%的时候会直接替换为[Mask],将句子 “my dog is cute” 转换为句子 “my dog is [Mask]”。
- 10%的时候将其替换为其它任意单词,将单词 “cute” 替换成另一个随机词,例如 “apple”。将句子 “my dog is cute” 转换为句子 “my dog is apple”。
- 10%的时候会保留原始Token,例如保持句子为 “my dog is cute” 不变。
这么做的原因是如果句子中的某个Token 100%都会被mask掉,那么在fine-tuning的时候模型就会有一些没有见过的单词。加入随机Token的原因是因为Transformer要保持对每个输入token的分布式表征,否则模型就会记住这个[mask]是token ’cute‘。至于单词带来的负面影响,因为一个单词被随机替换掉的概率只有15%*10% =1.5%,这个负面影响其实是可以忽略不计的。 另外文章指出每次只预测15%的单词,因此模型收敛的比较慢。
优点:
- 被随机选择15%的词当中以10%的概率用任意词替换去预测正确的词,相当于文本纠错任务,为BERT模型赋予了一定的文本纠错能力;
- 被随机选择15%的词当中以10%的概率保持不变,缓解了finetune时候与预训练时候输入不匹配的问题(预训练时候输入句子当中有mask,而finetune时候输入是完整无缺的句子,即为输入不匹配问题)。
缺点: - 针对有两个及两个以上连续字组成的词,随机mask字割裂了连续字之间的相关性,使模型不太容易学习到词的语义信息。主要针对这一短板,因此google此后发表了BERT-WWM,国内的哈工大联合讯飞发表了中文版的BERT-WWM。
Embedding
BERT 模型将 Token Embeddings
,Segment Embeddings
,Position Embeddings
利用求和的方式得到一个 Embedding 作为模型的输入,如图所示:
这里和Transformer
不同的是,这里还多了一个Segment Embedding
;Token Embedding
和Segment Embedding
的编码方式如图所示:
而对于Position Embedding
,采用的是直接从[0,1,2,...,sequence_length]
然后再来一层embedding层的方式,效果和三角函数差不多;大家可以进行实现测试,试一试到底哪个好;
Segment Embeddings
:padding部分用第一部分的补齐;即0000001111000000
2.3 BERT架构层
这一层的讲解可以看一下下面这篇文章,介绍的很详细;
[transformer]论文实现:Attention Is All You Need-CSDN博客
注意力机制是怎么工作的:
图中有四个东西,Query
,Key
,Value
,Attention
,图中把Key
和Value
放在了一起是因为其产生的output
是与Key
,Value
的维度是无关的,只与Query
的维度有关;文中把Key
,Value
称作为Context sequence
,Query
还是称作为Query sequence
;为什么要这么做?可以看模型中右上方的Multi-Head Attention
和左下角的Multi-Head Attention
的区别进行分析;
Query
,Key
,Value
这三个东西可以用python
中的字典来解释,Key
,Value
表示字典中的键值对,而Query
表示我们需要查询的键,Query
与Key
,Value
匹配其得到的结果就是我们需要的信息;但是在这里并不要求Query
与Key
严格匹配,只需要模糊匹配就可以;Query
对每一个Key
进行一次模糊的匹配,并给出匹配程度,越适配权重就越大,然后根据权重再与每一个Value
进行组合,得到最后的结果;其匹配程度的权重就代表了注意力机制的权重;
多头注意力机制就是把Query
,Key
,Value
多个维度的向量分为几个少数维度的向量组合,再在Query_i
,Key_i
,Value_i
上进行操作,最后把结果合并;
而Transformer
的encoder
中只使用了the global self attention layer
和前馈神经网络
;这里对其做一个简单的介绍;
the global self attention layer
:模型左下角(编码器)的Multi-Head Attention
,这一层负责处理上下文序列,并沿着他的长度去传播信息即Query
与Query
之间的信息;
Query与Query之间的信息传播有很多种方式,例如在Transformer没出来之间我们普遍采用Bidirectional RNNs
和 CNNs
的方式来处理;
但是为什么这里不使用RNN
和CNN
的方法呢?
RNN
和CNN
的限制:
- RNN 允许信息沿着序列一路流动,但是它要经过许多处理步骤才能到达那里(限制梯度流动)。这些 RNN 步骤必须按顺序运行,因此 RNN 不太能够利用现代并行设备的优势。
- 在 CNN 中,每个位置都可以并行处理,但它只提供有限的接收场。接收场只随着 CNN 层数的增加而线性增长,需要叠加许多卷积层来跨序列传输信息(小波网通过使用扩张卷积来减少这个问题)。
the global self attention layer
允许每个序列元素直接访问每个其他序列元素,只需少量操作,并且所有输出都可以并行计算。 就像下图这样:
虽然图像类似于线性层,其本质好像也是线性层,但是其信息传播能力要比普通的线性层要强;
前馈神经网络:
该网络由两个线性层组成。中间有一个relu
激活函数,还有一个dropout
层;这里面维度变化是把d_model
维先提升到dff
维度,然后把dff
维度降低到d_model
维度;
2.4 输出层
许多重要的下游任务,如问答(QA)和自然语言推理(NLI),都是基于对两个句子之间的关系的理解,而这并不是由语言建模直接捕获的。为了训练一个能理解句子关系的模型,使用一个binarized next sentence prediction
(NSP)任务对BERT
进行预训练;
NSP训练方式:binarized
,即两个标记,isnext
和notnext
;isnext
指的是input
的下一句sentence
;notnext
指的是不是input
的下一句;通过输出50%
的可能是isnext
,有50%
的可能是notnext
来对模型进行训练,让模型理解句子关系;
[CLS]的作用
BERT在第一句前会加一个
[CLS]
标志,最后一层该位对应向量可以作为整句话的语义表示,从而用于下游的分类任务等。因为与文本中已有的其它词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个词的语义信息,从而更好的表示整句话的语义。 具体来说,self-attention是用文本中的其它词来增强目标词的语义表示,但是目标词本身的语义还是会占主要部分的,因此,经过BERT的12层(BERT-base为例),每次词的embedding融合了所有词的信息,可以去更好的表示自己的语义。而[CLS]
位本身没有语义,经过12层,句子级别的向量,相比其他正常词,可以更好的表征句子语义,这样我们就可以用其输出来判断两个句子之间的关系并做文本分类任务;
2.5 BERT微调
BERT
微调是利用训练好的BERT
去适配下游任务,一般来说,我们只需要调整端到端的参数,再把特定任务的输入和输出放入模型中进行训练就可以,我们并不需要训练已经训练好的BERT
,只需要训练调整的参数即可;
三、过程实现
接下来我们使用Tensorflow
构建一个BERT
模型;
3.1 导包
这里使用tensorflow
和keras_nlp
两个包
import tensorflow as tf import keras_nlp
3.2 数据准备
这里的数据来自GLUE Benchmark中的The Corpus of Linguistic Acceptability
dataset = tf.data.TextLineDataset( ['./data/CoLA_train.tsv'] ) def process_data(x): x = tf.strings.split(x, '\t') return x[3], x[1] dataset = dataset.map(process_data).batch(64, drop_remainder=True) vocabulary = keras_nlp.tokenizers.compute_word_piece_vocabulary( dataset.map(lambda x, y: x), vocabulary_size=4000, lowercase=True, strip_accents=True, ) tokenizer = keras_nlp.tokenizers.WordPieceTokenizer( vocabulary=vocabulary, sequence_length=None, lowercase=True, strip_accents=True, ) cls = tokenizer.vocabulary.index('[CLS]') sep = tokenizer.vocabulary.index('[SEP]') pad = tokenizer.vocabulary.index('[PAD]') segment_packer = keras_nlp.layers.MultiSegmentPacker( sequence_length=128, start_value=cls, end_value=sep, sep_value=sep, pad_value=pad, ) def process_data_(x, y): x = tokenizer(x) input_token, segment_token = segment_packer(x) padding_token = tf.cast(input_token != 0, dtype=tf.int32) x = { 'token_ids': input_token, 'segment_ids': segment_token, 'padding_mask': padding_token, } y = tf.strings.to_number(y, tf.int32) return x, y dataset = dataset.map(process_data_) s = dataset.take(1).get_single_element() # ({'token_ids': <tf.Tensor: shape=(64, 128), dtype=int32, numpy= # array([[ 1, 324, 388, ..., 0, 0, 0], # [ 1, 144, 82, ..., 0, 0, 0], # [ 1, 144, 82, ..., 0, 0, 0], # ..., # [ 1, 87, 503, ..., 0, 0, 0], # [ 1, 87, 503, ..., 0, 0, 0], # [ 1, 87, 503, ..., 0, 0, 0]])>, # 'segment_ids': <tf.Tensor: shape=(64, 128), dtype=int32, numpy= # array([[0, 0, 0, ..., 0, 0, 0], # [0, 0, 0, ..., 0, 0, 0], # [0, 0, 0, ..., 0, 0, 0], # ..., # [0, 0, 0, ..., 0, 0, 0], # [0, 0, 0, ..., 0, 0, 0], # [0, 0, 0, ..., 0, 0, 0]])>, # 'padding_mask': <tf.Tensor: shape=(64, 128), dtype=int32, numpy= # array([[1, 1, 1, ..., 0, 0, 0], # [1, 1, 1, ..., 0, 0, 0], # [1, 1, 1, ..., 0, 0, 0], # ..., # [1, 1, 1, ..., 0, 0, 0], # [1, 1, 1, ..., 0, 0, 0], # [1, 1, 1, ..., 0, 0, 0]])>}, # <tf.Tensor: shape=(64,), dtype=int32, numpy= # array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, # 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, # 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0])>)
3.3 模型建立
用下面代码来建立模型:
import numpy as np def positional_encoding(length, depth): depth = depth/2 positions = np.arange(length)[:, np.newaxis] # (seq, 1) depths = np.arange(depth)[np.newaxis, :]/depth # (1, depth) angle_rates = 1 / (10000**depths) # (1, depth) angle_rads = positions * angle_rates # (pos, depth) pos_encoding = np.concatenate([np.sin(angle_rads), np.cos(angle_rads)],axis=-1) return tf.cast(pos_encoding, dtype=tf.float32) class Bert(tf.keras.Model): def __init__(self, vocabulary_size, d_model, encoder_num, intermediate_dim, num_heads, dropout=0.1): super().__init__() self.token_embedding = tf.keras.layers.Embedding(vocabulary_size, d_model) self.segment_embedding = tf.keras.layers.Embedding(2, d_model) self.position_embedding = positional_encoding(128, d_model) self.encoder = [keras_nlp.layers.TransformerEncoder( intermediate_dim=intermediate_dim, num_heads=num_heads, dropout=dropout, ) for _ in range(encoder_num)] self.add = tf.keras.layers.Add() # 分类任务 self.dense_1 = tf.keras.layers.Dense(128, activation='relu') self.dense_2 = tf.keras.layers.Dense(1, activation='sigmoid') def call(self, x): token_embedding = self.token_embedding(x['token_ids']) segment_embedding = self.segment_embedding(x['segment_ids']) position_embedding = tf.concat([positional_encoding(128, 512)[tf.newaxis]]*64, axis=0) output = self.add([token_embedding, segment_embedding, position_embedding]) for item in self.encoder: output = item(output) # 预测序列 # output = self.dense(output) # 分类任务 output = self.dense_1(output[:,0,:]) output = self.dense_2(output) return output
3.4 模型训练
模型配置和训练代码如下:
bert = Bert(tokenizer.vocabulary_size(), 512, 8, 1024, 8) bert(s[0]) bert.summary() bert.compile( loss=tf.keras.losses.BinaryCrossentropy(), optimizer='adam', metrics=[tf.keras.metrics.BinaryAccuracy()] ) bert.fit(dataset, epochs=10) # 不知道出什么问题,accuracy卡住不动了
四、整体总结
基于Transformer模型的BERT,建立起来很简单;这里代码似乎有问题,需要解答;