引言
本文是【理論篇】是時候徹底弄懂BERT模型了的姊妹篇,在本文中,我們通過🤗的transformers庫來實戰使用預訓練的BERT模型,
我們主要會實戰文本分類中的情緒識別任務和自然語言推理中的問答任務,
注意,文中提到的
嵌入表示、嵌入、嵌入向量、向量表示、表示說的都是同一個東西,
探索預訓練的BERT模型
在理論篇中,我們知道了如何使用屏蔽語言建模和下一句預測任務來預訓練BERT模型,
但是從開開始預訓練BERT模型是很耗資源的,所以我們可以下載預訓練好的BERT模型,Google開源了預訓練的BERT模型,我們可以從https://github.com/google-research/bert中下載,他們開源了多種配置,如下圖所示, L L L代表編碼器層數, H H H代表隱藏單元大小:

預訓練模型也可用于 BERT-uncased 和 BERT-cased 格式,在BERT-uncased中,所有的標記都是小寫的,但是在BERT-cased中的標記沒有轉換為小寫,而是直接用來訓練,
其中BERT-uncased模型是最常用的,但是如果我們在像命名物體識別任務時,此時我們應該保留大小寫,所以我們應該使用BERT-cased模型,除此之外,Google還釋放了使用整詞屏蔽(whole word masking,WWM)方法預訓練的模型,
我們可以以下面兩個方式使用預訓練模型:
- 作為抽取嵌入表示的特征抽取器
- 通過在下游任務像文本分類、問答等任務的微調預訓練的BERT模型
在后文中我們會學習這兩種方式是如何使用的,
從預訓練的BERT中抽取嵌入表示
我們通過一個實體來理解,考慮一個句子I love Paris,假設我們要抽取該句子中的每個標記的背景關系嵌入,首先,我們對句子分詞然后喂給預訓練的BERT模型,它會回傳每個標記的嵌入表示,除了可以獲取標記級(單詞級)的嵌入表示,我們還可以獲取句子級的表示,
在本節中,我們來學習具體如何從預訓練的BERT模型中抽取單詞級和句子級的嵌入表示,
假設我們想要完成一個情感分析任務,然后我們有下面的資料集:

我們有句子以及對應的標簽,其中 1 1 1代表正向情感而 0 0 0代表負向情感,我們可以使用給定的資料集訓練一個分類器來分類句子的情感,
首先,我們需要對資料集中的文本進行向量化,我們可以使用諸如TF-IDF、word2vec等方法進行向量化,但既然我們已經知道BERT可以學到單詞的背景關系嵌入表示,我們何不直接使用預訓練的BERT模型去向量化資料集中的句子呢?
考慮我們資料集中第一個句子:I love Paris,首先,我們通過WordPiece分詞器進行分詞并得到分詞后的單詞(標記):
tokens = [I, love, Paris]
然后增加[CLS]和SEP標記:
tokens = [ [CLS], I, love, Paris, [SEP] ]
類似的,我們就可以對訓練集中的所有句子進行同樣的操作,但是每個句子的長度是不固定的,所以我們需要保證所有的單詞串列長度是一致的,假設我們保持資料集中的句子長度為
7
7
7,如果我們查看上面的單詞串列,它的長度為
5
5
5,為了滿足長度為
7
7
7的要求,我們需要增加一個新的填充標記叫[PAD],這樣我們得到的單詞串列如下:
tokens = [ [CLS], I, love, Paris, [SEP], [PAD], [PAD] ]
這樣我們的單詞串列長度就變成
7
7
7了,下一步就是讓我們的模型理解[PAD]標記是用于填充而不是實際的標記,因此我們引入一個注意力屏蔽(mask),我們將注意力mask中所有實際標記位置都設為
1
1
1,而[PAD]標記位置都設為
0
0
0,我們就有了下面的注意力mask:
attention_mask = [ 1,1,1,1,1,0,0]
接下來我們映射所有標記到一個獨立的標記ID,假設用映射后的標記ID如下:
token_ids = [101, 1045, 2293, 3000, 102, 0, 0]
它的意思是標記[CLS]的ID是
101
101
101;標記I的DI是
1045
1045
1045等等,
現在,我們將token_ids以及attention_mask作為BERT模型的輸入,然后獲取每個標記的相應嵌入表示,
下圖顯示我們如何使用預訓練的BERT模型來獲得嵌入表示的,為了清晰起見,我們畫出了標記本身而不是它的ID,一旦我們將標記作為輸入喂給BERT,編碼器1(Encoder 1)計算所有標記的嵌入表示然后傳給下一個編碼器——編碼器2,編碼器2將編碼器1計算的嵌入表示作為輸入,然后輸出一個新的嵌入表示,接著繼續傳遞給下一個編碼器,這樣每個編碼器都把自己輸出的嵌入表示傳遞到它上面的下一個編碼器,而最后的編碼器,回傳的是我們句子中所有標記的最終嵌入表示,

這樣
R
[CLS]
R_{\text{[CLS]}}
R[CLS]?就是標記[CLS]的嵌入,
R
I
R_I
RI?是標記I的嵌入,等等,
在本例中,每個標記的嵌入表示大小為768,
我們現在得到了句子中每個單詞的嵌入表示,那我們如何得到整個句子的嵌入表示呢?
我們在句首位置有[CLS]標記,該標記的輸出會保存了整個完整句子的聚合資訊,所以,我們可以忽略所有其他標記的嵌入,僅用[CLS]標記的輸出嵌入來作為我們句子的表示,
使用同樣的方式,我們可以計算訓練集中所有句子的向量表示,一旦我們有了這些句子表示,我們就可以把它們作為輸入去訓練一個分類器完成情緒識別任務,
請注意使用[CLS]標記的輸出向量作為句子表示并不總是一個好主意,獲得句子表示的有效方法是對所有標記進行平均或池化(pooling),
在下節中,我們會學習如何使用Hugging Face🤗提供的transformers庫來實作我們上面說的這些事情,
Hugging Face transformers
Hugging Face🤗總部位于紐約,是一家專注于自然語言處理、人工智能和分布式系統的創業公司,他們所提供的聊天機器人技術一直頗受歡迎,但更出名的是他們在NLP開源社區上的貢獻,Hugging Face一直致力于自然語言處理NLP技術的平民化(democratize),希望每個人都能用上最先進(SOTA, state-of-the-art)的NLP技術,而非困窘于訓練資源的匱乏,他們的transformers庫同時提供了TesnforFlow和PyTorch版,
我們可以使用pip安裝transformers:
pip install transformers==4.10.0
生成BERT嵌入
本節中,我們會學習如何從預訓練的BERT模型中抽取嵌入表示,考慮句子:I love Paris,我們來看看如何獲得句子中所有單詞的背景關系單詞嵌入,
首先我們引入需要的包:
from transformers import BertModel, BertTokenizer
import torch
下面我們下載預訓練好的BERT模型,我們可以從https://huggingface.co/models頁面查看所有可用的預訓練模型,我們使用的是bert-base-uncased模型,它基于12個編碼器層、并且在小寫的標記中訓練,表示向量的大小為768,
下載并加載預訓練的bert-base-uncased模型:
model = BertModel.from_pretrained('bert-base-uncased')
下面我們下載并加載用來預訓練bert-base-uncased模型的分詞器:
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
現在,我們看看如何處理輸入,
預處理輸入
定義句子:
sentence = 'I love Paris'
分詞并獲得分詞后的標記:
tokens = tokenizer.tokenize(sentence)
print(tokens)
輸出:
['i', 'love', 'paris']
接著,我們增加[CLS]標記到標記串列頭部;增加[SEP]到標記串列尾部:
tokens = ['[CLS]'] + tokens + ['[SEP]']
print(tokens)
輸出:
['[CLS]', 'i', 'love', 'paris', '[SEP]']
現在我們的標記串列tokens大小為
5
5
5,假設我們想保持tokens長度為
7
7
7;此時,我們增加兩個填充標記[PAD]到tokens末尾:
tokens = tokens + ['[PAD]'] + ['[PAD]']
tokens
輸出:
['[CLS]', 'i', 'love', 'paris', '[SEP]', '[PAD]', '[PAD]']
下面,我們創建一個注意力屏蔽attention_mask:
attention_mask = [1 if i!= '[PAD]' else 0 for i in tokens]
attention_mask
輸出:
[1, 1, 1, 1, 1, 0, 0]
接著,我們將所有的標記轉換為對應的ID:
token_ids = tokenizer.convert_tokens_to_ids(tokens)token_ids
輸出:
[101, 1045, 2293, 3000, 102, 0, 0]
現在,我們通過將token_ids和attention_mask轉換為Tensor:
token_ids = torch.tensor(token_ids).unsqueeze(0)attention_mask = torch.tensor(attention_mask).unsqueeze(0)
接下來,我們把這兩個變數喂給預訓練的BERT模型得到嵌入表示,
獲得嵌入表示
如下面的代碼所示,我們將token_ids和attention_mask傳入模型得到嵌入表示,注意模型回傳的輸出是有兩個值的元組,第一個值代表隱藏狀態表示hidden_rep,它包含從最終編碼器(encoder 12)獲取的所有標記的嵌入表示;第二個值cls_head包含[CLS]標記的嵌入表示:
hidden_rep, cls_head = model(token_ids, attention_mask=attention_mask,return_dict=False)
hidden_rep包含輸入中所有單詞的嵌入表示,我們列印一下它的形狀:
print(hidden_rep.shape)
輸出:
torch.Size([1, 7, 768]) # [batch_size, sequence_length, hidden_size]
我們的批大小為 1 1 1;序列長度就是標記串列長度,為 7 7 7;隱藏大小是嵌入表示大小,為 768 768 768,
我們可以像下面的方式獲取每個表示的嵌入表示:
hidden_rep[0][0]回傳第一個標記[CLS]的嵌入表示hidden_rep[0][1]回傳第二個標記I的嵌入表示hidden_rep[0][2]回傳第三個標記love的嵌入表示
這樣,我們就得到了所有標記的背景關系表示,這基本上是給定句子中所有單詞的背景關系詞嵌入表示,
現在,我們來看下cls_head,它包含[CLS]標記的嵌入表示,我們列印一下它的形狀:
print(cls_head.shape)
輸出:
torch.Size([1, 768]) # [batch_size, hidden_size]
我們知道cls_head包含整個句子的聚合表示,所以我們可以使用該物件作為句子I love Paris的嵌入表示,
我們學習了如何從預訓練的 BERT 模型中提取嵌入, 但這些是僅從 BERT 的最頂層編碼器層(即encoder 12)獲得的嵌入,我們是否也可以從 BERT 的所有編碼器層中提取嵌入?
我們將在下一節中了解如何做到這一點,
從BERT所有編碼器層中抽取嵌入
我們已經知道如何從預訓練的BERT中抽取嵌入,那如何從BERT的所有編碼器層中抽取嵌入呢?
假設輸入層為 h 0 h_0 h0?,第一個編碼器層為 h 1 h_1 h1?,第二個編碼器層為 h 2 h_2 h2?,以此類推,最后一個編碼器層為 h 12 h_{12} h12?:

BERT的研究者已經實驗過從不同的編碼器層中提取嵌入,
比如,以NER任務來說,研究者已經使用與訓練的BERT模型來抽取特征,他們實驗了不同編碼器層的特征,然后得到了如下的F1值:

如上表所示,拼接最后4個編碼器層的表示(conncat last four hidden)可以得到最好的F1值——96.1%,這說明了,不僅僅是抽取最后一個編碼器層的輸出,我們也可以嘗試使用其他編碼器層,
現在, 我們接下來會學習如何使用transformers從所有的編碼器層中抽取嵌入表示,
抽取嵌入表示
首先,我們匯入必須的包:
from transformers import BertModel, BertTokenizer
import torch
接著,下載預訓練的BERT模型和分詞器,正如我們看到的,在下載預訓練的BERT模型時,我們需要設定output_hidden_states=True,這樣我們就能獲得所有編碼器層的嵌入:
model = BertModel.from_pretrained('bert-base-uncased', output_hidden_states = True)
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
下面,我們對輸入進行一些預處理,
預處理輸入
假設我們還是考慮上小節看到的例子,首先,我們對句子進行分詞并增加[CLS]和[SEP]標記:
sentence = 'I love Paris'
tokens = tokenizer.tokenize(sentence)
tokens = ['[CLS]'] + tokens + ['[SEP]']
假設我們需要保證標記串列的長度為
7
7
7,所以,我們增加填充標記[PAD]同時定義注意力mask:
tokens = tokens + ['[PAD]'] + ['[PAD]']attention_mask = [1 if i!= '[PAD]' else 0 for i in tokens]
下面我們將tokens轉換為對應的ID:
token_ids = tokenizer.convert_tokens_to_ids(tokens)
現在, 我們轉換token_ids和attention_mask到Tensor:
token_ids = torch.tensor(token_ids).unsqueeze(0)
attention_mask = torch.tensor(attention_mask).unsqueeze(0)
這樣我們就處理完畢了,下面可以獲取嵌入了,
得到嵌入表示
由于我們在定義模型時設定了 output_hidden_states = True 以從所有編碼器層獲取嵌入,現在模型回傳一個三個元素的元組:
last_hidden_state, pooler_output, hidden_states = model(token_ids, attention_mask = attention_mask, return_dict = False)
在上面的代碼中,分析如下:
- 第一個值,
last_hidden_state包含所有標記的嵌入表示,但是僅來自最后一個編碼器層(encoder 12) pooler_output代表從最后的編碼器層得到的[CLS]標記對應的嵌入表示,它可以進一步地通過一個線性和tanh激活函式處理hidden_states包含從所有編碼器層得到的所有標記的嵌入表示
現在,我們逐個了解下每個值,
首先,我們看一下last_hidden_state,我們輸出它的形狀:
last_hidden_state.shape.shape
輸出:
torch.Size([1, 7, 768]) # [batch_size, sequence_length, hidden_size]
我們的批大小為 1 1 1,序列長度就是標記串列長度,為 7 7 7,隱藏大小是嵌入表示的大小,這里是 768 768 768,
我們可以通過類似下面的方法獲取每個標記的嵌入表示:
-
last_hidden_state[0][0]輸出第一個標記[CLS]的嵌入表示 -
last_hidden_state[0][1]輸出第二個標記I的嵌入表示 -
last_hidden_state[0][2]輸出第三個標記love的嵌入表示
類似地,我們就可以從最后一個編碼器層得到所有標記的嵌入表示,
下面,我們來看pooler_output,它包含最后的編碼器層得到的[CLS]標記對應的嵌入表示,我們列印它的形狀:
pooler_output.shape
輸出:
torch.Size([1, 768]) # [batch_size, hidden_size]
我們知道[CLS]標記保存的是整個句子的聚合表示,我們就可以使用pooler_output來作為句子I love Paris的句子表示,
最后,我們來看一下hidden_states,它是一個包含13個值的元組,保存了從輸入層
h
0
h_0
h0?到最后一個編碼器層
h
12
h_{12}
h12?的所有嵌入表示:
len(hidden_states)
輸出:
13
我們可以看到,這13個值包含了所有層的表示:
-
hidden_states[0]包含輸入嵌入層 h 0 h_0 h0?的所有標記的嵌入表示 -
hidden_states[1]包含第一個編碼器嵌入層 h 1 h_1 h1?的所有標記的嵌入表示 -
hidden_states[2]包含第二個編碼器嵌入層 h 2 h_2 h2?的所有標記的嵌入表示 -
hidden_states[12]包含最后一個編碼器嵌入層 h 1 2 h_12 h1?2的所有標記的嵌入表示
我們接著列印hidden_states[0]的形狀:
hidden_states[0].shape
輸出:
torch.Size([1, 7, 768]) # [batch_size, sequence_length, hidden_size]
然后,列印hidden_states[1]的形狀:
hidden_states[1].shape
輸出:
torch.Size([1, 7, 768])
這樣我們就可以得到所有編碼器層的標記對應的嵌入表示,下面我們來學習下如何把預訓練的BERT模型
應用到下游任務,比如情緒分析,
為下游任務微調BERT
注意微調意味著我們不會從頭開始訓練BERT,而是使用預訓練的BERT,然后根據任務來更新它的權重引數,
在本節中,我們會學習到如何為以下的下游任務微調BERT模型:
-
文本分類
-
自然語言推理
-
命名物體識別
-
問答任務
文本分類
我們先來學習如何為一個文本分類任務微調BERT模型,假設我們具體要做的是情緒識別,在情緒識別任務中,我們的目標是判斷一個句子到底是正向的還是負向的,假設我們有一個包含句子和對應標簽的資料集,
考慮句子:I love Paris,首先,當然還是分詞,然后增加[CLS]和[SEP]標記,
然后我們將這些標記輸入到BERT模型,得到每個標記的嵌入表示,
下面我們只拿[CLS]標記對應的嵌入表示
R
[CLS]
R_{\text{[CLS]}}
R[CLS]?,而忽略所有其他的標記,因為我們知道[CLS]的嵌入包含了整個句子的聚合表示,我們將
R
[CLS]
R_{\text{[CLS]}}
R[CLS]?喂給一個分類器(帶有softmax函式的前饋神經網路),然后訓練該分類器去做情緒識別任務,
等等!這和我們在本節開頭看到的有什么不同?微調預訓練的 BERT 模型與使用預先訓練的 BERT 模型作為特征提取器有何不同?
在從預訓練的BERT中抽取嵌入表示小節中,我們知道在提取句子的嵌入后,我們將 R [CLS] R_{\text{[CLS]}} R[CLS]?提供給分類器并訓練分類器執行分類,類似地,在微調期間,我們也將 R [CLS] R_{\text{[CLS]}} R[CLS]?喂給一個分類并訓練分類器執行分類,
不同點在于當我們微調預訓練的BERT模型時,我們在更新分類器的引數時也會更新BERT的引數,但當我們使用預訓練的BERT模型作為特征抽取器時,我們只更新分類器的引數,而不更新預訓練的BERT模型,
在微調期間,我們可以通過以下兩個方式調整BERT模型的引數:
- 隨著分類器一起更新預訓練的BERT模型的引數
- 僅更新分類器的引數而不更新預訓練BERT模型的,此時,這種方式類似將預訓練的BERT模型作為特征抽取器的情況,
下圖闡述了我們如何為一個情緒分類任務微調預訓練的BERT模型:

正如我們看到的,我們將標記串列喂給BERT模型并得到這些標記的嵌入,然后我們拿[CLS]標記的嵌入作為前饋神經網路的輸入,并進行分類任務,
在下一小節中,讓我們通過代碼對預訓練的 BERT 模型進行微調來更好地了解微調的作業原理,
為情緒識別微調BERT
讓我們探索如何使用 IMDB 資料集為情感分析任務微調預訓練的 BERT 模型, IMDB 資料集由電影評論以及評論對應情緒組成,
引入依賴
首先我們安裝必要的依賴:
!pip install nlp==0.4.0
!pip install transformers==4.10.0
引入必要的包:
from transformers import BertForSequenceClassification, BertTokenizerFast, Trainer, TrainingArguments
from nlp import load_dataset
import torch
import numpy as np
加載模型和資料集
首先,我們使用nlp包下載并加載資料集:
!gdown https://drive.google.com/uc?id=11_M4ootuT7I1G0RlihcC0cA3Elqotlc-
dataset = load_dataset('csv', data_files='./imdbs.csv', split='train')
我們查看資料型別:
type(dataset)
輸出:
nlp.arrow_dataset.Dataset
下面,將資料集拆分為訓練和測驗集:
dataset = dataset.train_test_split(test_size=0.3)
我們輸出資料集dataset:
{'test': Dataset(features: {'text': Value(dtype='string', id=None), 'label': Value(dtype='int64', id=None)}, num_rows: 30),
'train': Dataset(features: {'text': Value(dtype='string', id=None), 'label': Value(dtype='int64', id=None)}, num_rows: 70)}
現在我們創建訓練和測驗集:
train_set = dataset['train']
test_set = dataset['test']
接下來,我們下載并加載預訓練的BERT模型,在本例中,我們使用預訓練的bert-base-uncased模型,因為我們在做序列分類任務,我們就可以使用BertForSequenceClassification類:
model = BertForSequenceClassification.from_pretrained('bert-base-uncased')
下面,我們下載并加載用于預訓練bert-base-uncased模型的分詞器,
這里我們使用BertTokenizerFast類創建分詞器而不是BertTokenizer,因為BertTokenizerFast相比BertTokenizer具有很多優勢,我們下節會探討這一點,
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')
現在我們已經加載好了資料集赫爾模型,讓我們開始預處理資料集吧,
預處理資料集
我們可以使用我們的分詞器快速地預處理資料集,比如,考慮句子:I love Paris,
首先,我們分詞并增加[CLS]和[SEP]標記:
tokens = [ '[CLS]', 'I', 'love', 'Paris', '[SEP]' ]
下面,我們映射標記到獨立的輸入ID,假設我們得到下面的ID:
input_ids = [101, 1045, 2293, 3000, 102]
然后,我們需要增加片段ID(segment ID,標記型別ID),??什么片段ID?假設我們的輸入有兩個句子,此時,片段ID用于區分這兩個句子,所有來自第一個句子的標記會映射到0;所有來自第二個句子的標記會映射到1,因為我們這里只有一個句子,所以所有的標記都會映射到0:
token_type_ids = [0, 0, 0, 0, 0]
現在我們需要創建注意力mask,我們知道注意力mask用于區分真實標記和填充標記[PAD],假設我們的標記串列長度應該為
5
5
5,我們這里的標記串列的長度應是
5
5
5了,所以不需要增加[PAD]標記,那么我們的注意力mask就會如下:
attention_mask = [1, 1, 1, 1, 1]
我們可以使用分詞器來為我們做上面這些手動的步驟,只需要將句子傳遞給分詞器即可:
tokenizer('I love Paris')
輸出如下,我們可以看到,輸入句子被分詞了并且映射到input_ids、token_type_ids,同時還有attention_mask:
{
'input_ids': [101, 1045, 2293, 3000, 102],
'token_type_ids': [0, 0, 0, 0, 0],
'attention_mask': [1, 1, 1, 1, 1]
}
通過分詞器,我們也可以傳入任意數量的句子并動態地執行填充,我們執行需要設定padding為True和最大的句子長度max_length,比如,下面的代碼中,我們傳入3個句子,并將max_length設為5:
tokenizer(['I love Paris', 'birds fly','snow fall'], padding = True, max_length=5)
前面的代碼將回傳以下內容, 如我們所見,所有句子都映射到 input_ids、token_type_ids和 attention_mask, 第二句和第三句只有兩個標記,加上[CLS]和[SEP]后,就會有4個標記, 由于我們將 padding 設定為 True 并將 max_length 設定為 5,因此第二個和第三個句子中會添加一個額外的[PAD]標記,這就是我們在第二個和第三個句子的注意力mask中值為 0 的原因:
{
'input_ids': [[101, 1045, 2293, 3000, 102], [101, 5055, 4875, 102, 0], [101, 4586, 2991, 102, 0]],
'token_type_ids': [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1], [1, 1, 1, 1, 0], [1, 1, 1, 1, 0]]
}
看,通過分詞器我們就可以很容易的預處理資料集,所以我們可以定義如下函式來預處理資料集:
def preprocess(data):
return tokenizer(data['text'], padding=True, truncation=True)
現在,我們使用preprocess函式預處理訓練和測驗資料集:
train_set = train_set.map(preprocess, batched=True,
batch_size=len(train_set))
test_set = test_set.map(preprocess, batched=True, batch_size=len(test_set))
下面,我們使用set_format函式來選擇我們在資料集中需要的列:
train_set.set_format('torch',
columns=['input_ids', 'attention_mask', 'label'])
test_set.set_format('torch',
columns=['input_ids', 'attention_mask', 'label'])
這樣,我們的資料集準備好了,可以開始訓練模型了,
訓練模型
我們先定義批大小和epoch大小:
batch_size = 8
epochs = 2
定義熱身步數和權重衰減:
warmup_steps = 500
weight_decay = 0.01
定義訓練引數:
training_args = TrainingArguments(
output_dir='./results',
num_train_epochs=epochs,
per_device_train_batch_size=batch_size,
per_device_eval_batch_size=batch_size,
warmup_steps=warmup_steps,
weight_decay=weight_decay,
logging_dir='./logs'
)
然后定義訓練器:
trainer = Trainer(
model=model,
args=training_args,
train_dataset=train_set,
eval_dataset=test_set
)
開始訓練:
trainer.train()
在訓練完成之后,我們可以通過evaluate函式評估模型:
trainer.evaluate()
輸出為:
{'epoch': 2.0,
'eval_loss': 0.6827354431152344,
'eval_runtime': 2.131,
'eval_samples_per_second': 14.078,
'eval_steps_per_second': 1.877}
這樣我們就能微調預訓練的BERT模型,既然我們已經學會了如何為一個文本分類任務微調BERT模型,在下一小節中,我們看看如何為自然語言推理任務微調BERT模型,
自然語言推理
在自然語言推理中,我們模型的目標是在給定前提(premise)的情況下,確定假設(hypothesis)是蘊涵(true)、矛盾(false)還是不確定(neutral), 讓我們通過微調 BERT 來學習如何解決自然語言推理任務,
考慮下面的資料集,
我們有一個前提和一個帶有標簽的假設,表明它們是蘊涵、矛盾還是不確定:

現在,我們模型的目標是確定句子對(前提-假設對)是蘊涵、矛盾還是不確定, 讓我們通過一個例子來了解如何做到這一點, 考慮以下前提-假設對:
Premise: He is playing
Hypothesis: He is sleeping
首先我們對句子分詞,然后增加[CLS]和[SEP]標記:
tokens = [ [CLS], He, is, playing, [SEP], He, is, sleeping [SEP]]
現在, 我們將這些標記喂給預訓練的BERT模型并得到每個單詞的嵌入,
我們拿出[CLS]標記的嵌入表示
R
[CLS]
R_{\text{[CLS]}}
R[CLS]?,并喂給一個分類器(前饋網路+softmax),該分類器會回傳句子是蘊含、矛盾還是不確定的概率:

為問答任務微調BERT
在本節中,我們學習如何用一個預訓練的問答BERT模型來做問答任務,首先,我們匯入必要的包:
from transformers import BertForQuestionAnswering, BertTokenizer
import torch
現在,我們下載并加載模型,我們使用bert-large-uncased-whole-word-masking-finetuned-squad模型,它是在SQUAD資料集上微調過的:
model = BertForQuestionAnswering.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')
然后,我們下載并加載分詞器:
tokenizer = BertTokenizer.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')
預處理輸入
首先,我們定義BERT的輸入,為問題和文本段落:
question = "What is the immune system?"
paragraph = "The immune system is a system of many biological structures and processes within an organism that protects against disease. To function properly, an immune system must detect a wide variety of agents, known as pathogens, from viruses to parasitic worms, and distinguish them from the organism's own healthy tissue."
增加[CLS]和[SEP]標記到問題和段落中:
question = '[CLS] ' + question + '[SEP]'
paragraph = paragraph + '[SEP]'
然后,對問題和段落進行分詞:
question_tokens = tokenizer.tokenize(question)
paragraph_tokens = tokenizer.tokenize(paragraph)
組合問題和段落標記,并將它們轉換為input_ids:
tokens = question_tokens + paragraph_tokens
input_ids = tokenizer.convert_tokens_to_ids(tokens)
接下來,定義segment_ids,這里,segment_ids會把所有來自問題的標記映射為0;把所有來自段落的標記映射為1:
segment_ids = [0] * len(question_tokens)
segment_ids += [1] * len(paragraph_tokens)
然后,把input_ids和segment_ids轉換為Tensor:
input_ids = torch.tensor([input_ids])
segment_ids = torch.tensor([segment_ids])
獲取答案
我們將input_ids和segment_ids輸入到模型中,它會回傳所有標記作為答案開始位置和結束位置的得分:
start_scores, end_scores = model(input_ids, token_type_ids = segment_ids, return_dict = False)
接著,我們找到start_index,就是最高答案開始位置得分對應的標記索引,和end_index,為最高答案結束位置得分對應的標記索引:
start_index = torch.argmax(start_scores)
end_index = torch.argmax(end_scores)
好了,我們就可以輸出開始索引和結束索引之間的問題片段了:
print(' '.join(tokens[start_index:end_index+1]))
輸出:
a system of many biological structures and processes within an organism that protects against disease
命名物體識別
在命名物體識別(NER)中,我們的目標是將命名物體分類到預設好的類別中,比如,考慮句子:Jeremy lives in Paris,其中的Jeremy應該被分類為人名,Paris應該分類為地點,
現在,我們學習如何微調預訓練的BERT模型去做NER任務,首先,我們對句子分詞,然后增加[CLS]和[SEP]標記,接著,我們將這些標記喂給預訓練的BERT模型,得到每個標記的嵌入表示,然后,將這些嵌入表示都喂給一個分類器(前饋網路+softmax函式),最后,該分類器回傳每個命名物體對應的類別,
如下圖所示:

參考
Getting Started with Google BERT
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/299460.html
標籤:AI
上一篇:如何閱讀NLP論文
下一篇:《深度學習100例》資料和代碼
