NLP與深度學習(六)BERT模型的使用

ZacksTang發表於2021-10-09

1. 預訓練的BERT模型

從頭開始訓練一個BERT模型是一個成本非常高的工作,所以現在一般是直接去下載已經預訓練好的BERT模型。結合遷移學習,實現所要完成的NLP任務。谷歌在github上已經開放了預訓練好的不同大小的BERT模型,可以在谷歌官方的github repo中下載[1]

 

以下是官方提供的可下載版本:

 

其中L表示的是encoder的層數,H表示的是隱藏層的大小(也就是最後的前饋網路中的神經元個數,等同於特徵輸出維度)。

除此之外,谷歌還提供了BERT-uncased與BERT-cased格式,分別對應是否包含大小寫。一般來說,BERT-uncased(僅包含小寫)比較常用,因為大部分場景下,單詞是否大小寫對任務的影響並不大。但是在部分特定場景,例如命名體識別(NER),則BERT-cased是更合適的。

在應用BERT預訓練模型時,實際上就是遷移學習,所以用法就是2個:

  1. 特徵提取(feature extraction)
  2. 微調(fine-tune)

下面我們會分別介紹這2種方法的使用。

 

2. BERT特徵提取

特徵提取非常簡單,直接將單詞序列輸入到已經預訓練好的BERT中,得到的輸出即為單詞以及句子的特徵。

 

舉個例子,以情感分析任務為例,假設有條句子”I love Beijing” ,它的情感為正面情感。在對這個句子通過BERT做特徵提取時,首先使用WordPiece對它進行分詞,得到單詞列表:

Tokens = [ I, love, Beijing]

 

然後加上特殊token [CLS] 與 [SEP](它們的作用已在前面的章節進行過介紹,在此不再贅述):

Tokens = [ [CLS], I, love, Beijing, [SEP] ]

 

接下來,為了使得訓練集中所有句子的長度保持一致,我們會指定一個“最大長度” max_length。對於長度小於此max_length的句子,對它進行補全;而對於超過了此max_length的句子,對它進行裁剪。假設我們這裡指定了max_length=7,則對上面的句子進行補全時,使用特殊token [PAD],將句子長度補全到7。如:

Tokens = [ [CLS], I, love, Beijing, [SEP], [PAD], [PAD] ]

 

在使用了[PAD] 作為填充後,還需要一個指示標誌,用於表示 [PAD] 僅是用於填充,不代表任何意義。這裡用到了一個稱為attention mask的列表來表示,它的長度等同於max_length。對於每條句子,如果對應位置的單詞為[PAD],則attention mask此位置的元素取值為0,反之為1。例如,對於這個例子,attention mask的值為:

attention_mask = [ 1, 1, 1, 1, 1, 0, 0 ]

 

最後,由於模型無法直接識別單詞,僅能識別數字,所以還需要將單詞對映為數字。我們首先會對整個單詞庫做一個詞典,每個單詞都有對應的1個序號(此序號為不重複的數字)。在這個例子中,假設我們已經構建了一個字典,則對應的這些單詞的列表為:

token_ids = [101, 200, 303, 408, 102, 0, 0]

 

其中 101 即對應的是 [CLS] 的序號,200對應的即是單詞“I”的序號,依此類推。

 

在準備好以上資料後,即可將 token_ids 與 attention_mask 輸入到預訓練好的BERT模型中,便得到了每個單詞的embedding表示。如下圖所示:

在上圖中,為了表述方便,在輸入時還是使用的單詞,但是需要注意的是:實際的輸入是token_ids 與 attention_mask。在經過了BERT的處理後,即得到了每個單詞的嵌入表示(此嵌入表示包含了整句的上下文)。假設我們使用的是BERT-base模型,則每個詞嵌入的維度即為768。

在上一章介紹BERT訓練的時候我們提到過,可以使用E([CLS]) 來表示整個句子的資訊。並將它輸入到前饋網路與softmax中進行分類任務。但是僅使用 E([CLS]) 作為整個句子的表示資訊也並非總是最好的辦法。一種更高效的方法是給所有token的嵌入表示做平均或是池化(pooling),來代表整句的資訊。具體方法我們後續會做介紹。

至此,我們已經介紹了使用預訓練BERT做特徵提取的過程,下面介紹如何使用python lib庫來實現此過程。

 

3. Hugging Face transformers

Hugging Face是一個專注於NLP技術的公司,提供了很多預訓練的模型以及資料集供直接使用,包括很多大家可能已經瞭解過的模型,例如bert-base、roberta、gpt2等等。其官網地址為: https://huggingface.co/

除了提供預訓練的模型外,Hugging Face提供的transformers庫也是在NLP社群非常熱門的庫。並且transformers的庫同時支援pytorch與tensorflow。

安裝transformers 庫非常簡單:

!pip install transformers

import transformers
transformers.__version__
'4.11.3'

 

4. 生成BERT Embedding

前面我們介紹了BERT特徵提取,下面通過程式碼實現此功能。

 

首先引入包並下載所需模型:

from transformers import TFBertModel, BertTokenizer
import tensorflow as tf

# download bert-base-uncased model
model = TFBertModel.from_pretrained('bert-base-uncased')
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

 

我們使用的是tensorflow,所以引入的是TFBertModel。如果有使用pytorch的讀者,可以直接引入BertModel。

通過 from_pretrained() 方法可以下載指定的預訓練好的模型以及分詞器,這裡我們使用的是bert-base-uncased。前面對bert-based 有過介紹,它包含12個堆疊的encoder,輸出的embedding維度為768。

對於所有可用預訓練模型,可以通過Hugging face 官網查詢[2]

 

引入模型後,下面從一個例子來看輸入資料的處理。

 

先對句子做分詞,然後加上特殊token [CLS] 與 [SEP]。假設我們指定的max_length 為7,則繼續補上 [PAD]:

sentence = 'I love Beijing'

tokens = tokenizer.tokenize(sentence)
print(tokens)

tokens = ['[CLS]'] + tokens + ['[SEP]']
print(tokens)

tokens = tokens + ['[PAD]'] * 2
print(tokens)

['i', 'love', 'beijing']
['[CLS]', 'i', 'love', 'beijing', '[SEP]']
['[CLS]', 'i', 'love', 'beijing', '[SEP]', '[PAD]', '[PAD]']

 

然後根據tokens構造attention_mask:

attention_mask = [ 1 if t != '[PAD]' else 0 for t in tokens]
print(attention_mask)

[1, 1, 1, 1, 1, 0, 0]

 

將所有tokens 轉為 token id:

token_ids = tokenizer.convert_tokens_to_ids(tokens)
print(token_ids)

[101, 1045, 2293, 7211, 102, 0, 0]

 

將token_ids 與 attention_mask 轉為tensor:

token_ids = tf.convert_to_tensor(token_ids)
token_ids = tf.reshape(token_ids, [1, -1])

attention_mask = tf.convert_to_tensor(attention_mask)
attention_mask = tf.reshape(attention_mask, [1, -1])

 

在這些步驟後,我們下一步即可將它們輸入到預訓練的模型中,得到embedding:

output = model(token_ids, attention_mask = attention_mask)
print(output[0].shape, output[1].shape)

(1, 7, 768) (1, 768)

 

根據TFModel的API說明[3],這2個返回分別為:

  1. BERT模型最後一層的輸出。由於輸入有7個tokens,所以對應有7個token的Embedding。其對應的維度為(batch_size, sequence_length, hidden_size)
  2. 輸出層中第1個token(這裡也就是對應 的[CLS])的Embedding,並且已被一個線性層 + Tanh啟用層處理。線性層的權重由NSP作業預訓練中得到。其對應的維度為(batch_size, hidden_size)

 

4.1. 是否需要其他隱藏層輸出

上面介紹瞭如何獲取BERT最後一層的輸出表示,要獲取每層的表示也非常簡單,僅需要新增引數 output_hidden_states=True 即可,例如:

output = model(token_ids, attention_mask = attention_mask, output_hidden_states=True)

不過這裡有一點需要討論的是:是否需要中間隱藏層的輸出?還是僅使用最後一層的輸出就足夠了?

 

對於此問題,BERT的研究人員做了進一步研究。在命名體識別任務中,研究人員除了使用BERT最後一層的輸出作為提取的特徵外,還嘗試使用了其他層的輸出(例如通過拼接的方式進行組合),並得到以下F1分數:

 

 

Fig.1 Sudharsan Ravichandiran. Getting Started with Google BERT[4]

從這個結果可以看到,在使用最後4層(h9到h12層的拼接)的輸出時,能得到比僅使用h12的輸出更高的F1分數96.1。所以僅使用BERT最後一層的輸出並非在所有場景下都是最好的選擇,有時候也需要嘗試使用其他層的輸出,觀察是否能得到更好的效果。

 

5. Fine-Tune BERT

上面介紹了BERT在遷移學習中的一種用法——特徵提取(feature extraction)。除此之外,還有另一種用法,稱為微調(Fine-tune)。兩者主要的區別在於:特徵提取直接獲取預訓練的BERT模型的輸出作為特徵,對預訓練的BERT的模型引數不會有任何改動。而微調是將預訓練的BERT與下游任務結合使用,在訓練過程中預訓練BERT模型的引數會被更新。

下面我們會介紹如何將預訓練的BERT與下游任務結合起來,對預訓練的BERT進行Fine-Tune。一般下游任務包括:文字分類、自然語言推理(Natural language inference)、命名體識別(NER)、問答系統(QA)等。這裡我們主要介紹一種文字分類任務:情感分析。

 

5.1. 文字分類

以情感分析為例,我們的資料集是一條條文字,每條文字對應一個label。Label可以是1或0,分別代表“正面情感”和“負面情感“。情感分析的任務是:輸入一條文字,判斷這條文字的label是0還是1。也就是說,這是一個二分類任務。當然,這只是一個最簡單的情感分析任務。稍微複雜點的例如:label有多個分類,分別代表“高興”、“悲傷”、“憤怒”等等,是一個多分類任務。再複雜一點的例如:除了判斷這個文字的情感外,還要判斷這個情感的強度,例如,label為“高興”、程度為3。在這裡我們僅介紹最簡單的情感分類,label只有0和1。

 

還是以之前的句子“I love Beijing”為例,我們對句子做分詞、加上特定tokens、補全到max_length、生成token_id、attention_mask,並送入到BERT。得到最後一層輸出的 [CLS]

的Embedding表示。此時E([CLS]) 包含了整個句子的表示,所以可以將此表示輸入到前饋網路與softmax中,輸出類別概率,用於判斷這個句子屬於哪個類別。此時:

  1. 若是使用的Fine-Tune,則在訓練過程中,預訓練的BERT模型的引數與前饋網路的引數都會得到更新;
  2. 若是使用的Feature-Extraction,則在訓練過程中,僅有前饋網路的引數會得到更新,預訓練的BERT模型的引數不會更新

下面以IMDB資料集為例,介紹BERT fine-tune的方法。

 

首先引入依賴包、載入資料集、載入預訓練的模型:

import tensorflow as tf 
from tensorflow import keras
import tensorflow_datasets as tfds
import numpy as np
from transformers import TFBertForSequenceClassification, BertTokenizerFast

# load dataset
imdb_train, df_info = tfds.load(name='imdb_reviews', split='train', with_info=True, as_supervised=True)
imdb_test = tfds.load(name='imdb_reviews', split='test', as_supervised=True)

# load pretrained bert model
model = TFBertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")

這裡需要注意的是,我們使用的是TFBertForSequenceClassification和BertTokenizerFast。TFBertForSequenceClassification是包裝好的類,專門用於做分類,由1層bert、1層Dropout、1層前饋網路組成,其定義可以參考官網[5]。BertTokenizerFast 也是一個方便的tokenizer類,會比BertTokenizer更快一些。

 

對輸入資料做分詞:

# tokenize every sequence
def bert_encoder(review):
    encoded = tokenizer(review.numpy().decode('utf-8'), truncation=True, max_length=150, pad_to_max_length=True)
    return encoded['input_ids'], encoded['token_type_ids'], encoded['attention_mask']

bert_train = [bert_encoder(r) for r, l in imdb_train]
bert_label = [l for r, l in imdb_train]

bert_train = np.array(bert_train)
bert_label = tf.keras.utils.to_categorical(bert_label, num_classes=2)

print(bert_train.shape, bert_label.shape) 
(25000, 3, 150) (25000, 2)

訓練資料的格式是(sentence, label),對每個sentence通過BertTokenizerFast做tokenize後,會直接得到input_ids, token_type_ids 以及attention_mask,它們便是需要輸入到BERT的格式。最後將它們轉為numpy 陣列。

bert_train的維度為(25000, 3, 150),即分別對應了:

  1. 資料總條數;
  2. 每條資料對應的input_ids、token_type_ids、attention_mask
  3. Max_length(指定的長度為150)

 

將訓練集繼續分割為訓練集與驗證集:

# create training and validation splits
from sklearn.model_selection import train_test_split

x_train, x_val, y_train, y_val = train_test_split(bert_train, 
                                         bert_label,
                                         test_size=0.2, 
                                         random_state=42)
print(x_train.shape, y_train.shape)
(20000, 3, 150) (20000, 2)

 

進一步對輸入資料進行處理:

def example_to_features(input_ids,attention_masks,token_type_ids,y):
    return {"input_ids": input_ids,
          "attention_mask": attention_masks,
          "token_type_ids": token_type_ids},y

train_ds = tf.data.Dataset.from_tensor_slices((tr_reviews, tr_masks, tr_segments, y_train)).map(example_to_features).shuffle(100).batch(16)
valid_ds = tf.data.Dataset.from_tensor_slices((val_reviews, val_masks, val_segments, y_val)).map(example_to_features).shuffle(100).batch(16)

TFBertForSequenceClassification的輸入格式(除label外的輸入)可以以有多種形式提供,這裡使用的是字典的形式。具體格式說明可以參考官方文件的說明[5]

 

指定訓練引數並進行訓練:

optimizer = tf.keras.optimizers.Adam(learning_rate=2e-5)
loss = tf.keras.losses.BinaryCrossentropy(from_logits=True)
model.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])

bert_history = model.fit(train_ds, epochs=4, validation_data=valid_ds)

Epoch 1/4
1250/1250 [==============================] - 701s 551ms/step - loss: 0.4085 - accuracy: 0.8131 - val_loss: 0.3011 - val_accuracy: 0.8718
Epoch 2/4
1250/1250 [==============================] - 689s 551ms/step - loss: 0.2104 - accuracy: 0.9186 - val_loss: 0.3252 - val_accuracy: 0.8858
Epoch 3/4
1250/1250 [==============================] - 689s 551ms/step - loss: 0.1105 - accuracy: 0.9622 - val_loss: 0.4201 - val_accuracy: 0.8816
Epoch 4/4
1250/1250 [==============================] - 689s 551ms/step - loss: 0.0696 - accuracy: 0.9774 - val_loss: 0.4153 - val_accuracy: 0.8876

這裡作為演示,僅訓練了4輪。從驗證集的準確率來看,還有上升的趨勢,所以理論上還可以增加epoch輪數。

 

6. 總結

BERT預訓練模型與遷移學習的結合使用,當前仍是NLP各類應用以及比賽的主流。不過,當前我們僅介紹了BERT預訓練模型中最基本的一種:bert-base。除此之外,BERT還有很多的變種,例如allbert、roberta、electra、spanbert等等,分別用於不同的場景。下一章我們繼續介紹BERT的這些變種。

 

 

References

[1] https://github.com/google-research/bert

[2] Pretrained models — transformers 4.11.2 documentation (huggingface.co)

[3] BERT — transformers 4.12.0.dev0 documentation (huggingface.co)

[4] Getting Hands-On with BERT | Getting Started with Google BERT (oreilly.com)

[5]https://huggingface.co/transformers/master/_modules/transformers/models/bert/modeling_tf_bert.html#TFBertForSequenceClassification

[6] https://learning.oreilly.com/library/view/advanced-natural-language/9781800200937/Chapter_4.xhtml

[7] https://huggingface.co/transformers/master/_modules/transformers/models/bert/tokenization_bert_fast.html#BertTokenizerFast

 

相關文章