1.bert簡單介紹
BERT(Bidirectional Encoder Representations from Transformers)是谷歌在2018年10月份的論文《Pre-training of Deep Bidirectional Transformers for Language Understanding》中提出的一個預(yù)訓(xùn)練模型框架,發(fā)布后對NLP領(lǐng)域產(chǎn)生了深遠(yuǎn)影響豹芯,各種基于bert的模型如雨后春筍般涌出亿卤。
在此對bert模型做一個簡單的記錄用于后期學(xué)習(xí)參考,文中會標(biāo)注相關(guān)出處热凹,如遇未注明或出現(xiàn)錯誤泵喘,請告知。如遇侵權(quán)般妙,請告知刪除~
bert模型分為pre-training
和fine-tuning
兩個階段纪铺。
1.1. pre-training階段
在預(yù)訓(xùn)練階段,bert構(gòu)造了兩個訓(xùn)練任務(wù)Mask LM
和Next Sentence Prediction
碟渺。
Mask LM
Mask LM
是谷歌提出的一種學(xué)習(xí)句子內(nèi)部關(guān)系的一種trick鲜锚,該trick的靈感來源于完形填空(跟word2vec
的CBOW
模型類似),基本思想是隨機(jī)遮掩(mask)句子中一些詞苫拍,并利用上下文信息對其進(jìn)行預(yù)測芜繁。
mask的具體做法是隨機(jī)對每個句子中15%的詞語按如下規(guī)則進(jìn)行
# 80%的時間采用[mask]
my dog is hairy ===> my dog is [MASK]
# 10%的時間隨機(jī)取一個詞來代替mask的詞
my dog is hairy ===> my dog is apple
# 10%的時間保持不變
my dog is hairy ===> my dog is hairy
使用該trick后可以使得模型對上下文信息具有完全意義上的雙向表征。
Q:模型在進(jìn)行[mask]
的時候為什么要以一定的概率保持不變呢绒极?
A:如果100%的概率都用[MASK]來取代被選中的詞骏令,那么在fine tuning的時候模型可能會有一些沒見過的詞。
Q:那么為什么要以一定的概率使用隨機(jī)詞呢垄提?
A:這是因為Transformer要保持對每個輸入token分布式的表征榔袋,否則Transformer很可能會記住這個[MASK]
就是"hairy"。至于使用隨機(jī)詞帶來的負(fù)面影響铡俐,文章中說了凰兑,所有其他的token(即非"hairy"的token)共享15%*10% = 1.5%的概率,其影響是可以忽略不計的审丘。
然而聪黎,該trick具有以下兩個缺點:
1)pre-training和fine-tuning階段不一致,該trick在fine-tuning階段是不可見的备恤,只有在pre-training階段才是用該trick稿饰。
2) 模型收斂速度慢。在每個batch中只有15%的token被預(yù)測露泊,因此需要在pre-training階段花費更多的訓(xùn)練次數(shù)喉镰。
Next Sentence Prediction
在自動問答(QA)、自然語言理解(NLI)等任務(wù)中惭笑,需要弄清上下兩句話之間的關(guān)系侣姆,為了模型理解這種關(guān)系生真,需要訓(xùn)練Next Sentence Prediction。
構(gòu)造訓(xùn)練語料方式:50%時間下一句是真正的下一句捺宗,50%時間下一句是語料中隨機(jī)的一句話柱蟀。
在pre-training階段,模型的損失函數(shù)是Mask LM和Next Sentence Prediction最大似然概率均值之和蚜厉。
1.2. fine-tuning階段
完成預(yù)訓(xùn)練之后的bert模型就具備了fine-tuning的能力长已,論文中將bert應(yīng)用于11項NLP任務(wù)中,在當(dāng)時均取得了STOA的效果昼牛。
- MNLI(Multi-Genre Natural Language Inference):輸入句子對术瓮,預(yù)測第二個句子相對于第一個句子是entailment, contradiction, or neutral三種類型中的哪一種。
- QQP(Quora Question Pairs):輸入句子對贰健,二分類任務(wù)胞四,判斷兩個問句在語義上是否等價。
- QNLI(Question Natural Language Inference):輸入句子對伶椿,二分類任務(wù)辜伟,正樣本包含答案,負(fù)樣本不包含脊另。
- STS-B(Semantic Textual Similarity Benchmark):輸入句子對导狡,計算兩個句子之間的語義相似度得分(1-5分)。
- MRPC(Microsoft Research Paraphrase Corpus):輸入句子對尝蠕,判斷兩個句子在語義上是否等價。
- RTE(Recognizing Textual Entailment):輸入句子對载庭,是否是蘊(yùn)含關(guān)系看彼,與MNLI類似,只是樣本數(shù)量小得多囚聚。
- SWAG(Situations With Adversarial Generations):給定一個句子靖榕,判斷候選的四個句子那個是輸入句子的續(xù)寫。
- SST-2(Stanford Sentiment Treebank):電影評論的情感分析顽铸。
- CoLA(Corpus of Linguistic Acceptability):判斷一個句子是否在語義上是可接受的茁计。
- SQuAD(Standford Question Answering Dataset):知識問答,給定一個問題和一段包含答案的文本谓松,找出該文本中答案的所在的范圍星压。
- CoNLL 2003 Named Entity Recognition:命名實體識別。
在fine-tuning階段鬼譬,根據(jù)下游任務(wù)的性質(zhì)娜膘,可選擇不同的bert輸出特征作為下游任務(wù)的輸入。bert模型的輸出主要有model.get_pooling_out()
和model.get_sequence_out()
model.get_pooling_out()
輸出的是每個句子開頭位置[CLS]的向量表示优质,也可以簡單理解為該句子所屬類別的向量表示竣贪,其shape=[batch size, hidden size]
model.get_sequence_out()
輸出的是整個句子每個token的向量表示军洼,需要注意的是該向量表示也包括了[CLS],其shape=[batch_size, seq_length, hidden_size]
2.模型結(jié)構(gòu)
2.1. bert輸入
bert的輸入由以下三部分組成
單詞embedding(token embeddings)演怎,這個就是我們之前一直提到的單詞embedding匕争,表示當(dāng)前詞的embedding唬涧。
句子embedding(segmentation embeddings )脊奋,表示當(dāng)前詞所在句子的index embedding丁溅,因為bert的輸入是由兩個句子構(gòu)成的尝偎,那么每個句子有個整體的embedding項對應(yīng)給每個單詞晚树。
位置信息embedding(position embeddings)镜沽,表示當(dāng)前詞所在位置的index embedding迈窟,這是因為NLP中單詞順序是很重要的特征睁枕,需要在這里對位置信息進(jìn)行編碼盗胀。
把單詞對應(yīng)的三個embedding疊加艘蹋,就形成了Bert的輸入。bert輸入的三個embedding都是通過學(xué)習(xí)得到的票灰。
2.2.bert結(jié)構(gòu)
對比OpenAI GPT(Generative pre-trained transformer)女阀,BERT是雙向的Transformer block連接;就像單向RNN和雙向RNN的區(qū)別屑迂,直覺上來講效果會好一些浸策。
對比ELMo,雖然都是“雙向”惹盼,但目標(biāo)函數(shù)其實是不同的庸汗。ELMo是分別以和作為目標(biāo)函數(shù),獨立訓(xùn)練處兩個representation然后拼接手报,而BERT則是以作為目標(biāo)函數(shù)訓(xùn)練LM蚯舱。
原文請參考:https://zhuanlan.zhihu.com/p/46652512
通過閱讀bert源碼,可以很清晰地得知其模型結(jié)構(gòu)掩蛤,bert模型實現(xiàn)部分主要在modeling.py文件中枉昏。
embedding_lookup
函數(shù)實現(xiàn)的是token embedding
embedding_postprocessor
函數(shù)實現(xiàn)的是segment embedding
和position embedding
將token embedding
、segment embedding
和position embedding
相加就可以得到bert模型的輸入(也即源碼中的self.embedding_output
)揍鸟,然后將self.embedding_output
輸入到12層transformer(只有encoder)中兄裂,即可得到bert的model.get_sequence_out()
輸出。
而model.get_pooling_out()
則是在model.get_sequence_out()
中取出第一個token
(也即[CLS])對應(yīng)的向量表示阳藻。
然為了在下游的分類任務(wù)中能夠得到相同維度的representation晰奖,因此會經(jīng)過一個dense層將[CLS]的representation轉(zhuǎn)換為固定維度(hidden_size),所以model.get_pooling_out()
的維度是[batch size, hidden size]
如需要詳細(xì)的bert源碼解讀腥泥,可參考https://zhuanlan.zhihu.com/p/69106080 (PART I)畅涂,也可關(guān)注筆者公眾號【NLPer筆記簿】,后臺回復(fù)bert
即可獲取bert源碼解讀完整版道川。
with tf.variable_scope(scope, default_name="bert", reuse=tf.AUTO_REUSE):
with tf.variable_scope("embeddings"):
# Perform embedding lookup on the word ids.
(self.embedding_output, self.embedding_table) = embedding_lookup(
input_ids=input_ids,
vocab_size=config.vocab_size,
embedding_size=config.hidden_size,
initializer_range=config.initializer_range,
word_embedding_name="word_embeddings",
use_one_hot_embeddings=use_one_hot_embeddings)
# Add positional embeddings and token type embeddings, then layer
# normalize and perform dropout.
self.embedding_output = embedding_postprocessor(
input_tensor=self.embedding_output,
use_token_type=True,
token_type_ids=token_type_ids,
token_type_vocab_size=config.type_vocab_size,
token_type_embedding_name="token_type_embeddings",
use_position_embeddings=True,
position_embedding_name="position_embeddings",
initializer_range=config.initializer_range,
max_position_embeddings=config.max_position_embeddings,
dropout_prob=config.hidden_dropout_prob)
with tf.variable_scope("encoder"):
# This converts a 2D mask of shape [batch_size, seq_length] to a 3D
# mask of shape [batch_size, seq_length, seq_length] which is used
# for the attention scores.
attention_mask = create_attention_mask_from_input_mask(
input_ids, input_mask)
# Run the stacked transformer.
# `sequence_output` shape = [batch_size, seq_length, hidden_size].
self.all_encoder_layers = transformer_model(
input_tensor=self.embedding_output,
attention_mask=attention_mask,
hidden_size=config.hidden_size,
num_hidden_layers=config.num_hidden_layers,
num_attention_heads=config.num_attention_heads,
intermediate_size=config.intermediate_size,
intermediate_act_fn=get_activation(config.hidden_act),
hidden_dropout_prob=config.hidden_dropout_prob,
attention_probs_dropout_prob=config.attention_probs_dropout_prob,
initializer_range=config.initializer_range,
do_return_all_layers=True)
self.sequence_output = self.all_encoder_layers[-1]
# The "pooler" converts the encoded sequence tensor of shape
# [batch_size, seq_length, hidden_size] to a tensor of shape
# [batch_size, hidden_size]. This is necessary for segment-level
# (or segment-pair-level) classification tasks where we need a fixed
# dimensional representation of the segment.
with tf.variable_scope("pooler"):
# We "pool" the model by simply taking the hidden state corresponding
# to the first token. We assume that this has been pre-trained
first_token_tensor = tf.squeeze(self.sequence_output[:, 0:1, :], axis=1)
self.pooled_output = tf.layers.dense(
first_token_tensor,
config.hidden_size,
activation=tf.tanh,
kernel_initializer=create_initializer(config.initializer_range))
2.3.bert輸出
其實bert輸出已經(jīng)在模型結(jié)構(gòu)章節(jié)介紹過了午衰,bert輸出主要有model.get_sequence_out()
和model.get_pooling_out()
兩種輸出立宜,其shape分別為[batch_size, seq_length, hidden_size]和[batch_size, hidden_size]。
model.get_sequence_out()
輸出主要用于特征提取再處理的序列任務(wù)臊岸,而model.get_pooling_out()
輸出可直接接softmax進(jìn)行分類(當(dāng)然需要外加一層dense層將hidden_size轉(zhuǎn)換為num_tag)橙数。