原來你是這樣的BERT,i了i了! —— 超詳細BERT介紹(三)BERT下游任務
BERT(Bidirectional Encoder Representations from Transformers)是谷歌在2018年10月推出的深度語言表示模型。
一經推出便席卷整個NLP領域,帶來了革命性的進步。
從此,無數英雄好漢競相投身於這場追劇(芝麻街)運動。
只聽得這邊G家110億,那邊M家又1750億,真是好不熱鬧!
然而大家真的了解BERT的具體構造,以及使用細節嗎?
本文就帶大家來細品一下。
前言
本系列文章分成三篇介紹BERT,上兩篇分別介紹了BERT主模型的結構及其組件相關和BERT預訓練相關,這一篇是最終話,介紹如何將BERT應用到不同的下游任務。
文章中的一些縮寫:NLP(natural language processing)自然語言處理;CV(computer vision)計算機視覺;DL(deep learning)深度學習;NLP&DL 自然語言處理和深度學習的交叉領域;CV&DL 計算機視覺和深度學習的交叉領域。
文章公式中的向量均為行向量,矩陣或張量的形狀均按照PyTorch的方式描述。
向量、矩陣或張量后的括號表示其形狀。
本系列文章的代碼均是基於transformers庫(v2.11.0)的代碼(基於Python語言、PyTorch框架)。
為便於理解,簡化了原代碼中不必要的部分,並保持主要功能等價。
閱讀本系列文章需要一些背景知識,包括Word2Vec、LSTM、Transformer-Base、ELMo、GPT等,由於本文不想過於冗長(其實是懶),以及相信來看本文的讀者們也都是沖着BERT來的,所以這部分內容還請讀者們自行學習。
本文假設讀者們均已有相關背景知識。
目錄
3、序列分類
序列分類任務就是輸入一個序列,輸出整個序列的標簽。
輸入的序列可以是單句也可以是雙句。
單句序列分類任務就是文本分類(text classification)任務,包括主題(topic)、情感(sentiment)、垃圾郵件(spam)等的分類任務;雙句序列分類任務包括相似度(similarity)、釋義(paraphrase)、蘊含(entailment)等的分類任務。
根據標簽數量分,可以分成單標簽和多標簽(multi-label)的分類任務。
根據標簽的類別數量分,可以分成二分類或三分類、五分類等多分類任務。
BERT中的序列分類任務包括單句和雙句的單標簽回歸或分類任務,涉及到語言可接受性(linguistic acceptability)、情感、相似度、釋義、蘊含等特征的分類,即GLUE(General Language Understanding Evaluation)中的任務。
如下為一個相似度回歸任務的例子(來自transformers庫的示例):
5.000 A plane is taking off. ||| An air plane is taking off.
3.800 A man is playing a large flute. ||| A man is playing a flute.
3.800 A man is spreading shreded cheese on a pizza. ||| A man is spreading shredded cheese on an uncooked pizza.
其中,最左邊的是標簽,表示兩句話的相似度分數,分數越高,相似度越高,分數的取值范圍是\([0, 5]\)。
再如下為一個雙句釋義二分類任務的例子(來自transformers庫的示例):
1 He said the foodservice pie business ... ||| The foodservice pie business ...
0 Magnarelli said Racicot hated ... ||| His wife said he was ...
0 The dollar was at 116.92 yen against the yen ... ||| The dollar was at 116.78 yen JPY ...
其中,最左邊的是標簽,如果后句是前句的釋義,即解釋說明,那么標簽為1,否則為0。
序列分類代碼如下:
代碼
# BERT之序列分類
class BertForSeqCls(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.config = config
# 標簽的類別數量
self.num_labels = config.num_labels
# 主模型
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 線性回歸或分類器
self.cls = nn.Linear(config.hidden_size, config.num_labels)
# 回歸或分類損失函數
self.loss_fct = LossRgrsCls(config.num_labels)
self.init_weights()
def forward(self,
tok_ids, # 標記編碼(batch_size * seq_length)
pos_ids=None, # 位置編碼(batch_size * seq_length)
sent_pos_ids=None, # 句子位置編碼(batch_size * seq_length)
att_masks=None, # 注意力掩碼(batch_size * seq_length)
labels=None, # 標簽(batch_size)
):
_, pooled_outputs = self.bert(
tok_ids,
pos_ids=pos_ids,
sent_pos_ids=sent_pos_ids,
att_masks=att_masks,
)
pooled_outputs = self.dropout(pooled_outputs)
logits = self.cls(pooled_outputs)
if labels is None:
return logits # 對數幾率(batch_size * num_labels)
loss = self.loss_fct(logits, labels)
return loss
其中,
num_labels
是標簽的類別數量(注意:並不是標簽數量,BERT的序列分類任務均為單標簽分類任務),=1時為回歸任務。
4、標記分類
標記分類任務就是輸入一個序列,輸出序列中每個標記的標簽。
輸入的序列一般是單句。
標記分類任務就是序列標注(sequence tagging)任務,包括中文分詞(Chinese word segmentation)、詞性標注(Part-of-Speech tagging,POS tagging)、命名實體識別(named entity recognition,NER)等。
序列標注任務常規的做法是BIO標注,B表示需要標注的片段的開頭標記,I表示非開頭標記,O表示不需要標注的標記。
如下為一個NER任務的例子(來自transformers庫的示例):
例子
Schartau B-PER
sagte O
dem O
" O
Tagesspiegel B-ORG
" O
vom O
Freitag O
, O
Fischer B-PER
sei O
" O
in O
einer O
Weise O
aufgetreten O
, O
die O
alles O
andere O
als O
überzeugend O
war O
" O
. O
Firmengründer O
Wolf B-PER
Peter I-PER
Bree I-PER
arbeitete O
Anfang O
der O
siebziger O
Jahre O
als O
Möbelvertreter O
, O
als O
er O
einen O
fliegenden O
Händler O
aus O
dem O
Libanon B-LOC
traf O
. O
Ob O
sie O
dabei O
nach O
dem O
Runden O
Tisch O
am O
23. O
April O
in O
Berlin B-LOC
durch O
ein O
pädagogisches O
Konzept O
unterstützt O
wird O
, O
ist O
allerdings O
zu O
bezweifeln O
. O
其中,每一行為一個標記和其標簽,空行分隔不同的句子;PER
是人名、ORG
是組織名、LOC
是地名。
標記分類代碼如下:
代碼
# BERT之標記分類
class BertForTokCls(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.config = config
# 標簽的類別數量
self.num_labels = config.num_labels
# 主模型
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 線性分類器
self.cls = nn.Linear(config.hidden_size, config.num_labels)
# 分類損失函數
self.loss_fct = LossCls(config.num_labels)
self.init_weights()
def forward(self,
tok_ids, # 標記編碼(batch_size * seq_length)
pos_ids=None, # 位置編碼(batch_size * seq_length)
sent_pos_ids=None, # 句子位置編碼(batch_size * seq_length)
att_masks=None, # 注意力掩碼(batch_size * seq_length)
labels=None, # 標簽(batch_size * seq_length)
):
outputs, _ = self.bert(
tok_ids,
pos_ids=pos_ids,
sent_pos_ids=sent_pos_ids,
att_masks=att_masks,
)
outputs = self.dropout(outputs)
logits = self.cls(outputs)
if labels is None:
return logits # 對數幾率(batch_size * seq_length * num_labels)
# 只計算非填充標記的損失
if att_masks is not None:
active = att_masks.view(-1)>0
logits = logits.view(-1, self.num_labels)[active]
labels = labels.view(-1)[active]
loss = self.loss_fct(logits, labels)
return loss
5、選擇題
BERT中的選擇題是給出前句以及num_choices
個后句,選擇最優的后句。
如下(來自SWAG數據集):
2
Students lower their eyes nervously. She
pats her shoulder, then saunters toward someone.
turns with two students.
walks slowly towards someone.
wheels around as her dog thunders out.
其中,第一行是標簽,第二行是前句,第三行到最后是四個后句;標簽數字從0開始計數,即標簽為2表示第三個(walks slowly towards someone.
)為正確選項。
BERT將每個樣本轉換成num_choices
個雙句:
Students lower their eyes nervously. ||| She pats her shoulder, then saunters toward someone.
Students lower their eyes nervously. ||| She turns with two students.
Students lower their eyes nervously. ||| She walks slowly towards someone.
Students lower their eyes nervously. ||| She wheels around as her dog thunders out.
然后每個雙句的序列表示產生一個對數幾率,num_choices
個雙句就得到一個長度為num_choices
的對數幾率向量,最后將這個向量作為這個樣本的輸出,計算損失即可。
選擇題代碼如下:
代碼
# BERT之選擇題
class BertForMultiChoice(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.config = config
# 選項個數
self.num_choices = config.num_choices
# 主模型
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 線性分類器
self.cls = nn.Linear(config.hidden_size, 1)
# 分類損失函數
self.loss_fct = LossCls(1)
self.init_weights()
def forward(self,
tok_ids, # 標記編碼(batch_size * num_choices * seq_length)
pos_ids=None, # 位置編碼(batch_size * num_choices * seq_length)
sent_pos_ids=None, # 句子位置編碼(batch_size * num_choices * seq_length)
att_masks=None, # 注意力掩碼(batch_size * num_choices * seq_length)
labels=None, # 標簽(batch_size)
):
seq_length = tok_ids.shape[-1]
# 調整形狀,每個前句-后句選項對看作一個雙句輸入
tok_ids = tok_ids.view(-1, seq_length)
if pos_ids is not None: pos_ids = pos_ids.view(-1, seq_length)
if sent_pos_ids is not None: sent_pos_ids = sent_pos_ids.view(-1, seq_length)
if att_masks is not None: att_masks = att_masks.view(-1, seq_length)
_, pooled_outputs = self.bert(
tok_ids,
pos_ids=pos_ids,
sent_pos_ids=sent_pos_ids,
att_masks=att_masks,
)
pooled_outputs = self.dropout(pooled_outputs)
logits = self.cls(pooled_outputs)
# 調整形狀,每num_choices個對數幾率看作一個樣本的輸出
logits = logits.view(-1, self.num_choices)
if labels is None:
return logits # 對數幾率(batch_size * num_choices)
loss = self.loss_fct(logits, labels)
return loss
其中,
num_choices
是選項個數。
6、問答
BERT中的問答任務其實是抽取式的機器閱讀理解(machine reading comprehension)任務,即給定一段話,給定一個問題,問題的答案來自這段話的某個連續的片段。
如下(來自transformers庫的示例):
0 Computational complexity theory
What branch of theoretical computer science deals with broadly classifying computational problems by difficulty and class of relationship?
Computational complexity theory is a branch of the theory of computation in theoretical computer science that focuses on classifying computational problems according to their inherent difficulty ...
其中,第一行是答案,答案左邊的數字表示這個答案在給定的這段話的起始位置(從0開始計數),第二行是問題,第三行是給定的一段話。
BERT將這個抽取式任務轉化為一個預測答案起始和結束位置的分類任務,標簽的類別數量是seq_length
,起始位置和結束位置分別預測,即相當於兩個標簽。
注意:這個起始和結束位置是標記化等預處理后答案在輸入的編碼向量里的位置。
BERT將所有的標記表示轉化成兩個對數幾率,然后橫向切片,得到兩個長度為seq_length
的對數幾率向量,分別作為起始和結束位置的預測,最后計算損失即可。
問答代碼如下:
代碼
# BERT之問答
class BertForQustAns(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.config = config
# 主模型
self.bert = BertModel(config)
# 線性分類器
self.cls = nn.Linear(config.hidden_size, 2)
self.init_weights()
def forward(self,
tok_ids, # 標記編碼(batch_size * seq_length)
pos_ids=None, # 位置編碼(batch_size * seq_length)
sent_pos_ids=None, # 句子位置編碼(batch_size * seq_length)
att_masks=None, # 注意力掩碼(batch_size * seq_length)
start_pos=None, # 起始位置標簽(batch_size)
end_pos=None, # 結束位置標簽(batch_size)
):
seq_length = tok_ids.shape[-1]
outputs, _ = self.bert(
tok_ids,
pos_ids=pos_ids,
sent_pos_ids=sent_pos_ids,
att_masks=att_masks,
)
logits = self.cls(outputs)
# 拆分起始和結束位置對數幾率
start_logits, end_logits = logits.split(1, dim=-1)
start_logits = start_logits.view(-1, seq_length)
end_logits = end_logits.view(-1, seq_length)
if start_pos is None or end_pos is None:
return (
start_logits, # 起始位置對數幾率(batch_size * seq_length)
end_logits, # 結束位置對數幾率(batch_size * seq_length)
)
# 標簽值裁剪,使值 (- [0, seq_length],
# 其中合法值 (- [0, seq_length-1],非法值 = seq_length
start_pos = start_pos.clamp(0, seq_length)
end_pos = end_pos.clamp(0, seq_length)
# ignore_index=seq_length:忽略標簽值 = seq_length對應的損失
loss_fct = LossCls(seq_length, ignore_index=seq_length)
start_loss = loss_fct(start_logits, start_pos)
end_loss = loss_fct(end_logits, end_pos)
loss = (start_loss + end_loss) / 2
return loss
后記
本文作為系列的最后一篇文章,詳細地介紹了BERT下游任務,BERT的通用性就體現在只需要添加少量模塊就能應用到各種不同的下游任務。
BERT充分地利用了主模型輸出的標記表示和序列表示,並對其進行一定地修改,從而可以應用到各種不同的下游任務中。
其中應用到選擇題和問答任務的方式特別巧妙,分別活用了序列和標記表示。
然而,如同預訓練,標記分類任務每個標記的標簽是獨立產生的,以及問答任務的起始和結束位置也是獨立產生的,這其實不是非常合理。