BERT论文总结
本次分享和總結自己所讀的論文 BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding,也就是大名鼎鼎的 BERT,網上對這篇論文的解讀非常多,在此就提下自己淺薄的見解。論文鏈接 BERT[1]
論文動機以及創新點
預訓練的語言模型已經在自然語言處理的各項任務上取得了有目共睹的效果。
目前有兩種途徑應用語言模型的預訓練 feature-base: 就是將預訓練后語言模型作為特征提取器,供下游模型使用。例如 ELMo fine-tune:將預訓練后的語言模型經過微調,使之適應下游任務,保留預訓練后的參數等,直接應用到下游任務中。
無論是 feature-base 還是 fine-tune,在預訓練階段都是采用單向的語言模型來學習語言表征。這種“單向”嚴重限制了模型的能力,特別是在 fine-tune 中。例如在 QA 任務中,我們需要結合上下兩個方向的文章內容來找答案。
為了解決單向的缺陷,論文中提出了兩種預訓練方法,分別是 “Masked Language model” 和 "next sentence prediction"
論文中同樣用到了 transformer,不過是雙向深層的 transformer encoder。
特別注意:這里面講的雙向并不是類似于 bi-lstm 上的時序上的雙向,而是指語義上的方向,代碼上是可以并行運行的。
BERT
輸入表征
在這里插入圖片描述Token Embeddings: 表示詞的 embedding
Segment Embedding:表示詞所在句子的 index Embedding
Position Embedding:表示詞所在位置的 index Embedding
這里無所謂輸入的一對句子和單句,如果一對句子,則用 sep 連接即可。
預訓練
Masked LM
直觀上,雙向網絡肯定比單向網絡更有威力。對于標準的語言模型,只能是從左向右,或從右向左的單向預測,為了能雙向的預訓練,論文中提出了類似于“完形填空”的方式來雙向的預訓練語言模型。簡單來說,就是把一段句子隨機的 15%詞扣除掉(用 mask token 替換),然后在網絡中被扣除對應位置出做 softmax 預測扣除出去的詞。是不是于 word2vec 的 BOW 訓練方式有異曲同工之妙。但是這樣做有兩點不妥:
pre_train 階段和 fine_tune 階段,存在一些 mismatch,在 fine_tune 時,是沒有 mask token 的,論文中是這樣解決的:在預訓練階段,對于隨機扣除的 15%的 token,采取不同的替換策略:1、80%概率用 mask token 替換 2、10%概率隨機用文章出現過的詞替換 3、10%概率不變,使用原來的詞
因為預測的只有 15%的詞,故相對標準的語言模型,收斂較慢。
注意:Masked LM 模型在被扣除詞的位置處的輸入是 mask token 或者被替換的詞,而不是原本的詞。
Next Sentence Prediction
對于一些任務,需要學習到句子間的關系,例如 QA、NLI。這種句子間關系,語言模型是不能很好的學習到的。因此論文中提出了 Next Sentence Prediction 任務。簡單來說,就是從單語料庫中隨機選擇兩條句子 A、B。如果句子 B 是句子 A 的下一句,則 label 為 IsNext,反正為 NotNext。訓練集中,有 50%的樣本是 IsNext,50%樣本是 NotNext,并且 NotNext 的樣本是隨機選取的。論文中說該分類模型達到 97%-98%的準確率。注意:
Masked LM 是捕捉詞之間的關系
Next Sentence Prediction 是捕捉句子之間的關系
Masked LM 和 Next Sentence Prediction 是放在一起訓練的
fine-tune
不同類型的任務需要對模型做不同的修改,但是修改都是非常簡單的,最多加一層神經網絡即可。如下圖所示
ELMo、openAI GPT、BERT 比較
在這里插入圖片描述ELMo 模型 是有兩個方向相反的 LSTM 網絡組合而成 單向的語言模型做預訓練。Feature-base 方式
OpenAI GPT 1、是 transform decoder,圖中每一層所有的 Trm 是一個 transformer 層,每層間做 self attention(masked,只和之前的做 self attention),然后將輸出結果喂給下一層,這就相當于下一層的一個 trm 會和上一層該位置之前的 trm 做了連接,后面的信息被屏蔽(代碼實現上用一個 mask 矩陣屏蔽)。2、單向的語言模型做預訓練 3、fine-tune 方式
BERT 1、同 OpenAI GPT,只不過變成雙向深層 transformer encoder,同樣每層之間做 self attention,然后將輸出結果喂給下一層,這就相當于與上一層前后兩個方向的 Trm 做了連接。2、Masked LM 和 Next Sentence Prediction 做預訓練 3、fine-tune 方式。
實驗部分
實驗部分是一片大好咯,各種牛逼。論文中也分析了不加 Masked LM 或不加 Next Sentence Prediction 預訓練對模型結果的影響如下
關鍵代碼分析
Masked token、Next Sentence
# MASK LM n_pred = min(max_pred, max(1, int(round(len(input_ids) * 0.15)))) # 15 % of tokens in one sentence cand_maked_pos = [i for i, token in enumerate(input_ids)if token != word_dict['[CLS]'] and token != word_dict['[SEP]']] shuffle(cand_maked_pos) masked_tokens, masked_pos = [], [] for pos in cand_maked_pos[:n_pred]:masked_pos.append(pos)masked_tokens.append(input_ids[pos])if random() < 0.8: # 80%input_ids[pos] = word_dict['[MASK]'] # make maskelif random() < 0.5: # 10%index = randint(0, vocab_size - 1) # random index in vocabularyinput_ids[pos] = word_dict[number_dict[index]] # replaceif tokens_a_index + 1 == tokens_b_index and positive < batch_size/2:batch.append([input_ids, segment_ids, masked_tokens, masked_pos, True]) # IsNextpositive += 1 elif tokens_a_index + 1 != tokens_b_index and negative < batch_size/2:batch.append([input_ids, segment_ids, masked_tokens, masked_pos, False]) # NotNextnegative += 1ScaledDotProductAttention
def forward(self, Q, K, V, attn_mask): ## self attention 操作scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k) # scores : [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]scores.masked_fill_(attn_mask, -1e9) #,注意把 padding部分的注意力去掉attn = nn.Softmax(dim=-1)(scores)context = torch.matmul(attn, V)return context, attnMultiHeadAttention
class MultiHeadAttention(nn.Module):def __init__(self):super(MultiHeadAttention, self).__init__()self.W_Q = nn.Linear(d_model, d_k * n_heads)self.W_K = nn.Linear(d_model, d_k * n_heads)self.W_V = nn.Linear(d_model, d_v * n_heads)def forward(self, Q, K, V, attn_mask):# q: [batch_size x len_q x d_model], k: [batch_size x len_k x d_model], v: [batch_size x len_k x d_model]residual, batch_size = Q, Q.size(0)# (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2) # q_s: [batch_size x n_heads x len_q x d_k]k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) # k_s: [batch_size x n_heads x len_k x d_k]v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) # v_s: [batch_size x n_heads x len_k x d_v]attn_mask = attn_mask.unsqueeze(1).repeat(1, n_heads, 1, 1) # attn_mask : [batch_size x n_heads x len_q x len_k]# context: [batch_size x n_heads x len_q x d_v], attn: [batch_size x n_heads x len_q(=len_k) x len_k(=len_q)]context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_mask)context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) # context: [batch_size x len_q x n_heads * d_v]output = nn.Linear(n_heads * d_v, d_model)(context)return nn.LayerNorm(d_model)(output + residual), attn # output: [batch_size x len_q x d_model]BERT
class PoswiseFeedForwardNet(nn.Module):def __init__(self):super(PoswiseFeedForwardNet, self).__init__()self.fc1 = nn.Linear(d_model, d_ff)self.fc2 = nn.Linear(d_ff, d_model)def forward(self, x):# (batch_size, len_seq, d_model) -> (batch_size, len_seq, d_ff) -> (batch_size, len_seq, d_model)return self.fc2(gelu(self.fc1(x)))class EncoderLayer(nn.Module):def __init__(self):super(EncoderLayer, self).__init__()self.enc_self_attn = MultiHeadAttention()self.pos_ffn = PoswiseFeedForwardNet()def forward(self, enc_inputs, enc_self_attn_mask):enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_mask) # enc_inputs to same Q,K,Venc_outputs = self.pos_ffn(enc_outputs) # enc_outputs: [batch_size x len_q x d_model]return enc_outputs, attnclass BERT(nn.Module):def __init__(self):super(BERT, self).__init__()self.embedding = Embedding()self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])self.fc = nn.Linear(d_model, d_model)self.activ1 = nn.Tanh()self.linear = nn.Linear(d_model, d_model)self.activ2 = geluself.norm = nn.LayerNorm(d_model)self.classifier = nn.Linear(d_model, 2)# decoder is shared with embedding layerembed_weight = self.embedding.tok_embed.weightn_vocab, n_dim = embed_weight.size()self.decoder = nn.Linear(n_dim, n_vocab, bias=False)self.decoder.weight = embed_weightself.decoder_bias = nn.Parameter(torch.zeros(n_vocab))def forward(self, input_ids, segment_ids, masked_pos):output = self.embedding(input_ids, segment_ids)enc_self_attn_mask = get_attn_pad_mask(input_ids, input_ids)for layer in self.layers:output, enc_self_attn = layer(output, enc_self_attn_mask)# output : [batch_size, len, d_model], attn : [batch_size, n_heads, d_mode, d_model]# it will be decided by first token(CLS)h_pooled = self.activ1(self.fc(output[:, 0])) # [batch_size, d_model]logits_clsf = self.classifier(h_pooled) # [batch_size, 2]masked_pos = masked_pos[:, :, None].expand(-1, -1, output.size(-1)) # [batch_size, maxlen, d_model]h_masked = torch.gather(output, 1, masked_pos) # masking position [batch_size, len, d_model]h_masked = self.norm(self.activ2(self.linear(h_masked)))logits_lm = self.decoder(h_masked) + self.decoder_bias # [batch_size, maxlen, n_vocab]return logits_lm, logits_clsf參考資料
[1]
BERT: https://arxiv.org/pdf/1810.04805.pdf
關于本站
“機器學習初學者”公眾號由是黃海廣博士創建,黃博個人知乎粉絲23000+,github排名全球前110名(32000+)。本公眾號致力于人工智能方向的科普性文章,為初學者提供學習路線和基礎資料。原創作品有:吳恩達機器學習個人筆記、吳恩達深度學習筆記等。
往期精彩回顧
那些年做的學術公益-你不是一個人在戰斗
適合初學者入門人工智能的路線及資料下載
吳恩達機器學習課程筆記及資源(github標星12000+,提供百度云鏡像)
吳恩達深度學習筆記及視頻等資源(github標星8500+,提供百度云鏡像)
《統計學習方法》的python代碼實現(github標星7200+)
精心整理和翻譯的機器學習的相關數學資料
首發:深度學習入門寶典-《python深度學習》原文代碼中文注釋版及電子書
備注:加入本站微信群或者qq群,請回復“加群”
加入知識星球(4300+用戶,ID:92416895),請回復“知識星球”
總結
- 上一篇: 首发:吴恩达的 CS229的数学基础(线
- 下一篇: 原创:机器学习代码练习(一、回归)