[Bert]论文实现:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

简介: [Bert]论文实现:BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding

论文: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的结构优势如下所示:

  1. 使用双向语言模型并结合MLM(掩蔽语言模型)来实现预训练的深度;
  2. 预先训练的表示减少了对许多精心设计的任务特定架构的需求;
  3. 使用该预训练提高了11个NLP任务的最新水平;

2.1 模型架构

      Bert的主要框架是由Transformer中的解码块组成的,下图是Transformer架构:

BERTbase模型和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吗?双向在哪里?这里要解释的是,Transformerencoder模块其本质是多连接的,这就导致其可以与前面的元素交互,也可以对后面的元素交互,所以叫做Bidirectional EncoderBERT使用,而Transformerdecoder需要contextkeyvalue,同时有mask机制,掩盖了后面的信息,让文本只注意到前面的信息,是一个left-to-right的架构做的文本生成;

模型架构如图:

有没有感觉非常简单,其中我感觉只需要注意的是输入层的和输出层;其中有两个关键的模型Masked LMNext 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 EmbeddingsSegment EmbeddingsPosition Embeddings 利用求和的方式得到一个 Embedding 作为模型的输入,如图所示:

     这里和Transformer不同的是,这里还多了一个Segment EmbeddingToken EmbeddingSegment Embedding的编码方式如图所示:

而对于Position Embedding,采用的是直接从[0,1,2,...,sequence_length]然后再来一层embedding层的方式,效果和三角函数差不多;大家可以进行实现测试,试一试到底哪个好;

Segment Embeddings:padding部分用第一部分的补齐;即0000001111000000

2.3 BERT架构层

这一层的讲解可以看一下下面这篇文章,介绍的很详细;

[transformer]论文实现:Attention Is All You Need-CSDN博客

注意力机制是怎么工作的

图中有四个东西,QueryKeyValueAttention,图中把KeyValue放在了一起是因为其产生的output是与KeyValue的维度是无关的,只与Query的维度有关;文中把KeyValue称作为Context sequenceQuery还是称作为Query sequence;为什么要这么做?可以看模型中右上方的Multi-Head Attention和左下角的Multi-Head Attention的区别进行分析;

QueryKeyValue这三个东西可以用python中的字典来解释,KeyValue表示字典中的键值对,而Query表示我们需要查询的键,QueryKeyValue匹配其得到的结果就是我们需要的信息;但是在这里并不要求QueryKey严格匹配,只需要模糊匹配就可以;Query对每一个Key进行一次模糊的匹配,并给出匹配程度,越适配权重就越大,然后根据权重再与每一个Value进行组合,得到最后的结果;其匹配程度的权重就代表了注意力机制的权重;

多头注意力机制就是把QueryKeyValue多个维度的向量分为几个少数维度的向量组合,再在Query_iKey_iValue_i上进行操作,最后把结果合并;

Transformerencoder中只使用了the global self attention layer前馈神经网络;这里对其做一个简单的介绍;

the global self attention layer:模型左下角(编码器)的Multi-Head Attention,这一层负责处理上下文序列,并沿着他的长度去传播信息即QueryQuery之间的信息;

Query与Query之间的信息传播有很多种方式,例如在Transformer没出来之间我们普遍采用Bidirectional RNNsCNNs的方式来处理;

但是为什么这里不使用RNNCNN的方法呢?

RNNCNN的限制

  • 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,即两个标记,isnextnotnextisnext指的是input的下一句sentencenotnext指的是不是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 导包

这里使用tensorflowkeras_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,建立起来很简单;这里代码似乎有问题,需要解答;


目录
相关文章
|
5天前
|
机器学习/深度学习 自然语言处理 API
[DistilBERT]论文实现:DistilBERT:a distilled version of BERT: smaller, faster, cheaper and lighter
[DistilBERT]论文实现:DistilBERT:a distilled version of BERT: smaller, faster, cheaper and lighter
14 0
|
9月前
|
机器学习/深度学习 编解码 自然语言处理
BEIT: BERT Pre-Training of Image Transformers论文解读
本文介绍了一种自监督视觉表示模型BEIT,即图像transformer的双向编码器表示。继自然语言处理领域开发的BERT之后
388 0
|
机器学习/深度学习 自然语言处理 算法
【论文解读】文本分类上分利器:Bert微调trick大全
【论文解读】文本分类上分利器:Bert微调trick大全
765 0
【论文解读】文本分类上分利器:Bert微调trick大全
|
自然语言处理 PyTorch TensorFlow
【BERT-多标签文本分类实战】之五——BERT模型库的挑选与Transformers
【BERT-多标签文本分类实战】之五——BERT模型库的挑选与Transformers
746 0
【BERT-多标签文本分类实战】之五——BERT模型库的挑选与Transformers
|
机器学习/深度学习 自然语言处理 大数据
【文本分类】BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
【文本分类】BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
112 0
【文本分类】BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding
|
机器学习/深度学习 自然语言处理 算法
用于自然语言处理的BERT-双向Transformers的直观解释
用于自然语言处理的BERT-双向Transformers的直观解释
174 0
用于自然语言处理的BERT-双向Transformers的直观解释
|
机器学习/深度学习 自然语言处理 算法
【论文笔记】融合标签向量到BERT:对文本分类进行改进
【论文笔记】融合标签向量到BERT:对文本分类进行改进
518 0
【论文笔记】融合标签向量到BERT:对文本分类进行改进
|
数据采集 机器学习/深度学习 自然语言处理
【论文笔记】当Bert炼丹不是玄学而是哲学:Mengzi模型
【论文笔记】当Bert炼丹不是玄学而是哲学:Mengzi模型
292 0
【论文笔记】当Bert炼丹不是玄学而是哲学:Mengzi模型
|
机器学习/深度学习 自然语言处理 测试技术
给Bert加速吧!NLP中的知识蒸馏论文 Distilled BiLSTM解读
给Bert加速吧!NLP中的知识蒸馏论文 Distilled BiLSTM解读
373 0
|
自然语言处理 知识图谱
ACL 2019 | 基于知识增强的语言表示模型,多项NLP任务表现超越BERT(附论文解读)
本文提出了一种新方法,将知识图谱的信息加入到模型的训练中。
1141 0

热门文章

最新文章

http://www.vxiaotou.com