本文同時發布於我的個人網站,公式圖片顯示效果更好,歡迎訪問:https://lulaoshi.info/machine-learning/attention/bert
自BERT(Bidirectional Encoder Representations from Transformer)[1]出現後,NLP界開啟了一個全新的範式。本文主要介紹BERT的原理,以及如何使用HuggingFace提供的 transformers 庫完成基於BERT的微調任務。
預訓練BERT在一個較大的語料上進行預訓練(Pre-train)。預訓練主要是在數據和算力充足的條件下,訓練一個大模型,在其他任務上可以利用預訓練好的模型進行微調(Fine-tune)。
訓練目標BERT使用了維基百科等語料庫數據,共幾十GB,這是一個龐大的語料庫。對於一個GB級的語料庫,僱傭人力進行標註成本極高。BERT使用了兩個巧妙方法來無監督地訓練模型:Masked Language Modeling和Next Sentence Prediction。這兩個方法可以無需花費時間和人力標註數據,以較低成本無監督地得到訓練數據。圖1就是一個輸入輸出樣例。
對於Masked Language Modeling,給定一些輸入句子(圖1中最下面的輸入層),BERT將輸入句子中的一些單詞蓋住(圖1中Masked層),經過中間的詞向量和BERT層後,BERT的目標是讓模型能夠預測那些剛剛被蓋住的詞。還記得英語考試中,我們經常遇到「完形填空」題型嗎?能把完形填空做對,說明已經理解了文章背後的語言邏輯。BERT的Masked Language Modeling本質上就是在做「完形填空」:預訓練時,先將一部分詞隨機地蓋住,經過模型的擬合,如果能夠很好地預測那些蓋住的詞,模型就學到了文本的內在邏輯。
圖1 BERT預訓練的輸入和輸出除了「完形填空」,BERT還需要做Next Sentence Prediction任務:預測句子B是否為句子A的下一句。Next Sentence Prediction有點像英語考試中的「段落排序」題,只不過簡化到只考慮兩句話。如果模型無法正確地基於當前句子預測Next Sentence,而是生硬地把兩個不相關的句子拼到一起,兩個句子在語義上是毫不相關的,說明模型沒有讀懂文本背後的意思。
詞向量在基於深度學習的NLP方法中,文本中的詞通常都用一維向量來表示。某兩個詞向量的 Cosine 距離較小,說明兩個詞在語義上相似。
詞向量一般由Token轉換而成。英文中,一個句子中的詞由空格、句號等標點隔開,我們很容易從句子中獲得詞。英文的詞通常有前綴、後綴、詞根等,在獲得英文的詞後,還需要抽出詞根,比如圖1所展示的,將「playing」切分為「play」和「##ing」。如果不對英文詞進行類似詞根抽取,詞表過大,不容易擬合。對於英文,「play」和「##ing」分別對應兩個Token。
中文一般由多個字組成一個詞,傳統的中文文本任務通常使用一些分詞工具,得到嚴格意義上的詞。在原始的BERT中,對於中文,並沒有使用分詞工具,而是直接以字為粒度得到詞向量的。所以,原始的中文BERT(bert-base-chinese)輸入到BERT模型的是字向量,Token就是字。後續有專門的研究去探討,是否應該對中文進行必要的分詞,以詞的形式進行切分,得到向量放入BERT模型。
為了方面說明,本文不明確區分字向量還是詞向量,都統稱為詞向量。
我們首先需要將文本中每個Token都轉換成一維詞向量。假如詞向量的維度為hidden_size,句子的Token長度為seq_len,或者說句子共包含seq_len個Token,那麼上圖中,輸入就是seq_len * hidden_size。再加上batch_size,那麼輸入就是batch_size * seq_len * hidden_size。上圖只展示了一個樣本,未體現出batch_size,或者可以理解成batch_size = 1,即每次只處理一條文本。為便於理解,本文的圖解中不考慮batch_size這個維度,實際模型訓練時,batch_size通常大於1。
詞向量經過BERT模型一系列複雜的轉換後,模型最後仍然以詞向量的形式輸出,用以對文本進行語義表示。輸入的詞向量是seq_len * hidden_size,句子共seq_len個Token,將每個Token都轉換成詞向量,送入BERT模型。經過BERT模型後,得到的輸出仍然是seq_len * hidden_size維度。輸出仍然是seq_len的長度,其中輸出的i 個位置(0 < i < seq_len)的詞向量,表示經過了擬合後的第i個Token的語義表示。後續可以用輸出中每個位置的詞向量來進行一些其他任務,比如命名實體識別等。
除了使用Masked方法故意蓋住一些詞外,BERT還加了一些特殊的符號:[CLS]和[SEP]。[CLS]用在句首,是句子序列中i = 0位置的Token。BERT認為輸出序列的i = 0位置的Token對應的詞向量包含了整個句子的信息,可對整個句子進行分類。[SEP]用在分割前後兩個句子上。
微調經過預訓練後,得到的模型可以用來微調各類任務。
單文本分類任務。剛才提到,BERT模型在文本前插入一個[CLS]符號,並將該符號對應的輸出向量作為整篇文本的語義表示,用於文本分類,如圖2所示。對於[CLS]符號,可以理解為:與文本中已有的其它字/詞相比,這個無明顯語義信息的符號會更「公平」地融合文本中各個字/詞的語義信息。 圖2 單文本分類語句對分類任務。語句對分類任務的實際應用場景包括:問答(判斷一個問題與一個答案是否匹配)、語句匹配(兩句話是否表達同一個意思)等。對於該任務,BERT模型除了添加[CLS]符號並將對應的輸出作為文本的語義表示,輸入兩句話之間用[SEP]符號作分割。 圖3 語句對分類序列標註任務。序列標註任務的實際應用場景包括:命名實體識別、中文分詞、新詞發現(標註每個字是詞的首字、中間字或末字)、答案抽取(答案的起止位置)等。對於該任務,BERT模型利用文本中每個Token對應的輸出向量對該Token進行標註(分類),如下圖所示(B(Begin)、I(Inside)、E(End)分別表示一個詞的第一個字、中間字和最後一個字)。 圖4 序列標註模型結構Transformer是BERT的核心模塊,Attention注意力機制又是Transformer中最關鍵的部分。前一篇文章,我們介紹了Attention注意力機制和Transformer,這裡不再贅述。BERT用到的主要是Transformer的Encoder,沒有使用Transformer Decoder。
把多個Transformer Encoder組裝起來,就構成了BERT。在論文中,作者分別用12個和24個Transformer Encoder組裝了兩套BERT模型,兩套模型的參數總數分別為110M和340M。
圖5 BERT中的Transformer EncoderHuggingFace Transformers使用BERT和其他各類Transformer模型,繞不開HuggingFace(https://huggingface.co/)提供的Transformers生態。HuggingFace提供了各類BERT的API(transformers庫)、訓練好的模型(HuggingFace Hub)還有數據集(datasets)。最初,HuggingFace用PyTorch實現了BERT,並提供了預訓練的模型,後來。越來越多的人直接使用HuggingFace提供好的模型進行微調,將自己的模型共享到HuggingFace社區。HuggingFace的社區越來越龐大,不僅覆蓋了PyTorch版,還提供TensorFlow版,主流的預訓練模型都會提交到HuggingFace社區,供其他人使用。
使用transformers庫進行微調,主要包括:
Tokenizer:使用提供好的Tokenizer對原始文本處理,得到Token序列;構建模型:在提供好的模型結構上,增加下遊任務所需預測接口,構建所需模型;Tokenizer下面兩行代碼會創建 BertTokenizer,並將所需的詞表加載進來。首次使用這個模型時,transformers 會幫我們將模型從HuggingFace Hub下載到本地。
>>> from transformers import BertTokenizer
>>> tokenizer = BertTokenizer.from_pretrained('bert-base-cased')用得到的tokenizer進行分詞:
>>> encoded_input = tokenizer("我是一句話")
>>> print(encoded_input)
{'input_ids': [101, 2769, 3221, 671, 1368, 6413, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1, 1, 1]}得到的一個Python dict。其中,input_ids最容易理解,它表示的是句子中的每個Token在詞表中的索引數字。詞表(Vocabulary)是一個Token到索引數字的映射。可以使用decode()方法,將索引數字轉換為Token。
>>> tokenizer.decode(encoded_input["input_ids"])
'[CLS] 我 是 一 句 話 [SEP]'可以看到,BertTokenizer在給原始文本處理時,自動給文本加上了[CLS]和[SEP]這兩個符號,分別對應在詞表中的索引數字為101和102。decode()之後,也將這兩個符號反向解析出來了。
token_type_ids主要用於句子對,比如下面的例子,兩個句子通過[SEP]分割,0表示Token對應的input_ids屬於第一個句子,1表示Token對應的input_ids屬於第二個句子。不是所有的模型和場景都用得上token_type_ids。
>>> encoded_input = tokenizer("您貴姓?", "免貴姓李")
>>> print(encoded_input)
{'input_ids': [101, 2644, 6586, 1998, 136, 102, 1048, 6586, 1998, 3330, 102],
'token_type_ids': [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1],
'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}句子通常是變長的,多個句子組成一個Batch時,attention_mask就起了至關重要的作用。
>>> batch_sentences = ["我是一句話", "我是另一句話", "我是最後一句話"]
>>> batch = tokenizer(batch_sentences, padding=True, return_tensors="pt")
>>> print(batch)
{'input_ids':
tensor([[ 101, 2769, 3221, 671, 1368, 6413, 102, 0, 0],
[ 101, 2769, 3221, 1369, 671, 1368, 6413, 102, 0],
[ 101, 2769, 3221, 3297, 1400, 671, 1368, 6413, 102]]),
'token_type_ids':
tensor([[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]]),
'attention_mask':
tensor([[1, 1, 1, 1, 1, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 1, 1]])}對於這種batch_size = 3的場景,不同句子的長度是不同的,padding=True表示短句子的結尾會被填充[PAD]符號,return_tensors="pt"表示返回PyTorch格式的Tensor。attention_mask告訴模型,哪些Token需要被模型關注而加入到模型訓練中,哪些Token是被填充進去的無意義的符號,模型無需關注。
Model下面兩行代碼會創建BertModel,並將所需的模型參數加載進來。
>>> from transformers import BertModel
>>> model = BertModel.from_pretrained("bert-base-chinese")BertModel是一個PyTorch中用來包裹網絡結構的torch.nn.Module,BertModel裡有forward()方法,forward()方法中實現了將Token轉化為詞向量,再將詞向量進行多層的Transformer Encoder的複雜變換。
forward()方法的入參有input_ids、attention_mask、token_type_ids等等,這些參數基本上是剛才Tokenizer部分的輸出。
>>> bert_output = model(input_ids=batch['input_ids'])forward方法返回模型預測的結果,返回結果是一個tuple(torch.FloatTensor),即多個Tensor組成的tuple。tuple默認返回兩個重要的Tensor:
>>> len(bert_output)
2
last_hidden_state:輸出序列每個位置的語義向量,形狀為:(batch_size, sequence_length, hidden_size)。pooler_output:[CLS]符號對應的語義向量,經過了全連接層和tanh激活;該向量可用於下遊分類任務。下遊任務BERT可以進行很多下遊任務,transformers庫中實現了一些下遊任務,我們也可以參考transformers中的實現,來做自己想做的任務。比如單文本分類,transformers庫提供了BertForSequenceClassification類。
class BertForSequenceClassification(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels
self.config = config
self.bert = BertModel(config)
classifier_dropout = ...
self.dropout = nn.Dropout(classifier_dropout)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
...
def forward(
...
):
...
outputs = self.bert(...)
pooled_output = outputs[1]
pooled_output = self.dropout(pooled_output)
logits = self.classifier(pooled_output)
...在這段代碼中,BertForSequenceClassification在BertModel基礎上,增加了nn.Dropout和nn.Linear層,在預測時,將BertModel的輸出放入nn.Linear,完成一個分類任務。除了BertForSequenceClassification,還有BertForQuestionAnswering用於問答,BertForTokenClassification用於序列標註,比如命名實體識別。
transformers 中的各個API還有很多其他參數設置,比如得到每一層Transformer Encoder的輸出等等,可以訪問他們的文檔(https://huggingface.co/docs/transformers/)查看使用方法。
參考資料
Devlin J, Chang M-W, Lee K, Toutanova K. BERT: pre-training of deep bidirectional transformers for language understanding. Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers). Minneapolis, Minnesota: Association for Computational Linguistics, 2019: 4171–4186.徹底理解Google BERT(https://www.jianshu.com/p/46cb208d45c3)圖解BERT模型:從零開始構建BERT(https://cloud.tencent.com/developer/article/1389555)