https://daiwk.github.io/posts/nlp-bert.html
目錄
參考最強NLP預訓練模型!谷歌BERT橫掃11項NLP任務記錄
參考https://www.zhihu.com/question/298203515/answer/509703208
概述
本文介紹了一種新的語言表征模型BERT——來自Transformer的雙向編碼器表征。與最近的語言表征模型不同,BERT旨在基於所有層的左、右語境來預訓練深度雙向表征。BERT是首個在大批句子層面和token層面任務中取得當前最優性能的基於微調的表征模型,其性能超越許多使用任務特定架構的系統,刷新了11項NLP任務的當前最優性能記錄。
目前將預訓練語言表征應用於下游任務存在兩種策略:feature-based的策略和fine-tuning策略。
- feature-based策略(如 ELMo)使用將預訓練表征作為額外特征的任務專用架構。
- fine-tuning策略(如生成預訓練 Transformer (OpenAI GPT))引入了任務特定最小參數,通過簡單地微調預訓練參數在下游任務中進行訓練。
在之前的研究中,兩種策略在預訓練期間使用相同的目標函數,利用單向語言模型來學習通用語言表征。
作者認為現有的技術嚴重制約了預訓練表征的能力,微調策略尤其如此。其主要局限在於標准語言模型是單向的,這限制了可以在預訓練期間使用的架構類型。例如,OpenAI GPT使用的是從左到右的架構,其中每個token只能注意Transformer自注意力層中的先前token。這些局限對於句子層面的任務而言不是最佳選擇,對於token級任務(如 SQuAD 問答)則可能是毀滅性的,因為在這種任務中,結合兩個方向的語境至關重要。
BERT(Bidirectional Encoder Representations from Transformers)改進了基於微調的策略。
BERT提出一種新的預訓練目標——遮蔽語言模型(masked language model,MLM),來克服上文提到的單向局限。MLM 的靈感來自 Cloze 任務(Taylor, 1953)。MLM隨機遮蔽輸入中的一些token,目標在於僅基於遮蔽詞的語境來預測其原始詞匯id。與從左到右的語言模型預訓練不同,MLM目標允許表征融合左右兩側的語境,從而預訓練一個深度雙向Transformer。除了 MLM,我們還引入了一個“下一句預測”(next sentence prediction)任務,該任務聯合預訓練文本對表征。
貢獻:
- 展示了雙向預訓練語言表征的重要性。不同於 Radford 等人(2018)使用單向語言模型進行預訓練,BERT使用MLM預訓練深度雙向表征。本研究與 Peters 等人(2018)的研究也不同,后者使用的是獨立訓練的從左到右和從右到左LM的淺層級聯。
- 證明了預訓練表征可以消除對許多精心設計的任務特定架構的需求。BERT是首個在大批句子層面和token層面任務中取得當前最優性能的基於微調的表征模型,其性能超越許多使用任務特定架構的系統。
- BERT 刷新了11項NLP任務的當前最優性能記錄。本論文還報告了BERT的模型簡化測試(ablation study),證明該模型的雙向特性是最重要的一項新貢獻。代碼和預訓練模型將發布在goo.gl/language/bert。
BERT
模型架構
BERT 旨在基於所有層的左、右語境來預訓練深度雙向表征。因此,預訓練的 BERT 表征可以僅用一個額外的輸出層進行微調,進而為很多任務(如問答和語言推斷任務)創建當前最優模型,無需對任務特定架構做出大量修改。
BERT 的模型架構是一個多層雙向Transformer編碼器,基於Vaswani 等人 (2017)描述的原始實現,在tensor2tensor庫中發布(當然,可以抽空看看https://daiwk.github.io/posts/platform-tensor-to-tensor.html和https://daiwk.github.io/posts/platform-tensor-to-tensor-coding.html)。
本文中,我們將層數(即Transformer塊)表示為\(L\)
,將隱層的size表示為\(H\)
、自注意力頭數表示為\(A\)
。在所有實驗中,我們將feed-forward/filter的size設置為\(4H\)
,即H=768時為3072,H=1024時為4096。我們主要看下在兩種模型尺寸上的結果:
\(BERT_{BASE}\)
: L=12, H=768, A=12, Total Parameters=110M\(BERT_{LARGE}\)
: L=24, H=1024, A=16, Total Parameters=340M
其中,\(BERT_{BASE}\)
和OpenAI GPT的大小是一樣的。BERT Transformer使用雙向自注意力機制,而GPT Transformer使用受限的自注意力機制,導致每個token只能關注其左側的語境。雙向Transformer在文獻中通常稱為“Transformer 編碼器”,而只關注左側語境的版本則因能用於文本生成而被稱為“Transformer 解碼器”。
下圖顯示了BERT/GPT Transformer/ELMo的結構區別:

- BERT 使用雙向Transformer
- OpenAI GPT 使用從左到右的Transformer
- ELMo 使用獨立訓練的從左到右和從右到左LSTM的級聯來生成下游任務的特征。
三種模型中,只有BERT表征會基於所有層中的左右兩側語境。
Input Representation
論文的輸入表示(input representation)能夠在一個token序列中明確地表示單個文本句子或一對文本句子(例如, [Question, Answer])。對於給定token,其輸入表示通過對相應的token、segment和position embeddings進行求和來構造:

- 使用WordPiece嵌入【GNMT,Google’s neural machine translation system: Bridging the gap between human and machine translation】和30,000個token的詞匯表。用##表示分詞。
- 使用learned positional embeddings,支持的序列長度最多為512個token。
- 每個序列的第一個token始終是特殊分類嵌入([CLS])。對應於該token的最終隱藏狀態(即,Transformer的輸出)被用作分類任務的聚合序列表示。對於非分類任務,將忽略此向量。
- 句子對被打包成一個序列。以兩種方式區分句子。
- 首先,用特殊標記([SEP])將它們分開。
- 其次,添加一個learned sentence A嵌入到第一個句子的每個token中,一個sentence B嵌入到第二個句子的每個token中。
- 對於單個句子輸入,只使用 sentence A嵌入。
Pre-training Tasks
- 它在訓練雙向語言模型時以減小的概率把少量的詞替成了Mask或者另一個隨機的詞。感覺其目的在於使模型被迫增加對上下文的記憶。(知乎的回答)
- 增加了一個預測下一句的loss。
Task #1: Masked LM
標准條件語言模型只能從左到右或從右到左進行訓練,因為雙向條件作用將允許每個單詞在多層上下文中間接地“see itself”。
為了訓練一個深度雙向表示(deep bidirectional representation),研究團隊采用了一種簡單的方法,即隨機屏蔽(masking)部分輸入token,然后只預測那些被屏蔽的token。論文將這個過程稱為“masked LM”(MLM),盡管在文獻中它經常被稱為Cloze任務(Taylor, 1953)。
在這個例子中,與masked token對應的最終隱藏向量被輸入到詞匯表上的輸出softmax中,就像在標准LM中一樣。在團隊所有實驗中,隨機地屏蔽了每個序列中15%的WordPiece token。與去噪的自動編碼器(Vincent et al., 2008)相反,只預測masked words而不是重建整個輸入。
雖然這確實能讓團隊獲得雙向預訓練模型,但這種方法有兩個缺點。
- 缺點1:預訓練和finetuning之間不匹配,因為在finetuning期間從未看到
[MASK]
token。
為了解決這個問題,團隊並不總是用實際的[MASK]
token替換被“masked”的詞匯。相反,訓練數據生成器隨機選擇15%的token。
例如在這個句子“my dog is hairy”中,它選擇的token是“hairy”。然后,執行以下過程:
數據生成器將執行以下操作,而不是始終用[MASK]
替換所選單詞:
- 80%的時間:用
[MASK]
標記替換單詞,例如,my dog is hairy → my dog is [MASK]
- 10%的時間:用一個隨機的單詞替換該單詞,例如,
my dog is hairy → my dog is apple
- 10%的時間:保持單詞不變,例如,
my dog is hairy → my dog is hairy
. 這樣做的目的是將表示偏向於實際觀察到的單詞。
Transformer encoder不知道它將被要求預測哪些單詞或哪些單詞已被隨機單詞替換,因此它被迫保持每個輸入token的分布式上下文表示。此外,因為隨機替換只發生在所有token的1.5%(即15%的10%),這似乎不會損害模型的語言理解能力。
- 缺點2:每個batch只預測了15%的token,這表明模型可能需要更多的預訓練步驟才能收斂。
團隊證明MLM的收斂速度略慢於 left-to-right的模型(預測每個token),但MLM模型在實驗上獲得的提升遠遠超過增加的訓練成本。
Task #2: Next Sentence Prediction
在為了訓練一個理解句子的模型關系,預先訓練一個二分類的下一句測任務,這一任務可以從任何單語語料庫中生成。具體地說,當選擇句子A和B作為預訓練樣本時,B有50%的可能是A的下一個句子,也有50%的可能是來自語料庫的隨機句子。例如:
Input = [CLS] the man went to [MASK] store [SEP] he bought a gallon [MASK] milk [SEP] Label = IsNext Input = [CLS] the man [MASK] to the store [SEP] penguin [MASK] are flight ##less birds [SEP] Label = NotNext
完全隨機地選擇了NotNext語句,最終的預訓練模型在此任務上實現了97%-98%的准確率。
Pre-training Procedure
使用gelu激活函數(Bridging nonlinearities and stochastic regularizers with gaus- sian error linear units),在pytorch里實現如下:
class GELU(nn.Module): """ Paper Section 3.4, last paragraph notice that BERT used the GELU instead of RELU """ def forward(self, x): return 0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3))))
Fine-tuning Procedure
Comparison of BERT and OpenAI GPT
實驗
網絡結構如下:

GLUE Datasets
GLUE Results
SQuAD v1.1
Named Entity Recognition
SWAG
Ablation Studies
Effect of Pre-training Tasks
Effect of Model Size
Effect of Number of Training Steps
Feature-based Approach with BERT
代碼實現
pytorch版本
https://github.com/codertimo/BERT-pytorch
fork了一份:https://github.com/daiwk/BERT-pytorch
輸入data/corpus.small
:
Welcome to the \t the jungle \n I can stay \t here all night \n
可視化,需要:
brew install graphviz # mac pip3 install git+https://github.com/szagoruyko/pytorchviz
畫出bert的架構圖的方法(先生成vocab,如果機器的dot不支持pdf,只支持png/jpg等,需要在lib/python3.6/site-packages/torchviz/dot.py
中把dot = Digraph(node_attr=node_attr, graph_attr=dict(size="12,12"))
改成dot = Digraph(node_attr=node_attr, graph_attr=dict(size="12,12"), format="png")
):
import torch from torch import nn from torchviz import make_dot, make_dot_from_trace import sys sys.path.append("./bert_pytorch-0.0.1a4.src/") #from trainer import BERTTrainer from model import BERTLM, BERT from dataset import BERTDataset, WordVocab from torch.utils.data import DataLoader def demo(): lstm_cell = nn.LSTMCell(128, 128) x = torch.randn(1, 128) dot = make_dot(lstm_cell(x), params=dict(list(lstm_cell.named_parameters()))) file_out = "xx" dot.render(file_out) def bert_dot(): """ """ vocab_size = 128 train_dataset_path = "data/bert_train_data.xxx" vocab_path = "data/vocab.all.xxx" vocab = WordVocab.load_vocab(vocab_path) train_dataset = BERTDataset(train_dataset_path, vocab, seq_len=20, corpus_lines=2000, on_memory=True) train_data_loader = DataLoader(train_dataset, batch_size=8, num_workers=8) bert = BERT(len(vocab), hidden=256, n_layers=8, attn_heads=8) device = torch.device("cpu") mymodel = BERTLM(bert, vocab_size).to(device) data_iter = train_data_loader out_idx = 0 for data in data_iter: data = {key: value.to(device) for key, value in data.items()} if out_idx == 0: g = make_dot(mymodel(data["bert_input"], data["segment_label"]), params=dict(mymodel.named_parameters())) g