簡介
Transformers是一個用於自然語言處理(NLP)的Python第三方庫,實現Bert、GPT-2和XLNET等比較新的模型,支持TensorFlow和PyTorch。本文介對這個庫進行部分代碼解讀,目前文章只針對Bert,其他模型看心情。
手把手教你用PyTorch-Transformers是我記錄和分享自己使用 Transformers 的經驗和想法,因為個人時間原因不能面面俱到,有時間再填
本文是《手把手教你用Pytorch-Transformers》的第一篇,主要對一些源碼進行講解
目前只對 Bert 相關的代碼和原理進行說明,GPT2 和 XLNET 應該是沒空寫了
實戰篇手把手教你用Pytorch-Transformers——實戰(二)已經完成一部分
Model相關
BertConfig
BertConfig 是一個配置類,存放了 BertModel 的配置。比如:
- vocab_size_or_config_json_file:字典大小,默認30522
- hidden_size:Encoder 和 Pooler 層的大小,默認768
- num_hidden_layers:Encoder 的隱藏層數,默認12
- num_attention_heads:每個 Encoder 中 attention 層的 head 數,默認12
完整內容可以參考:https://huggingface.co/transformers/v2.1.1/model_doc/bert.html#bertconfig
BertModel
實現了基本的Bert模型,從構造函數可以看到用到了embeddings,encoder和pooler。
下面是允許輸入到模型中的參數,模型至少需要有1個輸入: input_ids 或 input_embeds。
- input_ids 就是一連串 token 在字典中的對應id。形狀為 (batch_size, sequence_length)。
- token_type_ids 可選。就是 token 對應的句子id,值為0或1(0表示對應的token屬於第一句,1表示屬於第二句)。形狀為(batch_size, sequence_length)。
Bert 的輸入需要用 [CLS] 和 [SEP] 進行標記,開頭用 [CLS],句子結尾用 [SEP]
兩個句子:
tokens:[CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
token_type_ids:0 0 0 0 0 0 0 0 1 1 1 1 1 1
一個句子:
tokens:[CLS] the dog is hairy . [SEP]
token_type_ids:0 0 0 0 0 0 0
- attention_mask 可選。各元素的值為 0 或 1 ,避免在 padding 的 token 上計算 attention(1不進行masked,0則masked)。形狀為(batch_size, sequence_length)。
- position_ids 可選。表示 token 在句子中的位置id。形狀為(batch_size, sequence_length)。形狀為(batch_size, sequence_length)。
- head_mask 可選。各元素的值為 0 或 1 ,1 表示 head 有效,0無效。形狀為(num_heads,)或(num_layers, num_heads)。
- input_embeds 可選。替代 input_ids,我們可以直接輸入 Embedding 后的 Tensor。形狀為(batch_size, sequence_length, embedding_dim)。
- encoder_hidden_states 可選。encoder 最后一層輸出的隱藏狀態序列,模型配置為 decoder 時使用。形狀為(batch_size, sequence_length, hidden_size)。
- encoder_attention_mask 可選。避免在 padding 的 token 上計算 attention,模型配置為 decoder 時使用。形狀為(batch_size, sequence_length)。
encoder_hidden_states 和 encoder_attention_mask 可以結合論文中的Figure 1理解,左邊為 encoder,右邊為 decoder。
論文《Attention Is All You Need》:https://arxiv.org/pdf/1706.03762.pdf
如果要作為 decoder ,模型需要通過 BertConfig 設置 is_decoder 為 True
def __init__(self, config): super(BertModel, self).__init__(config) self.config = config self.embeddings = BertEmbeddings(config) self.encoder = BertEncoder(config) self.pooler = BertPooler(config) self.init_weights() def forward(self, input_ids=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, encoder_hidden_states=None, encoder_attention_mask=None): ...
BertPooler
在Bert中,pool的作用是,輸出的時候,用一個全連接層將整個句子的信息用第一個token來表示,源碼如下
每個 token 上的輸出大小都是 hidden_size (在BERT Base中是768)
class BertPooler(nn.Module): def __init__(self, config): super(BertPooler, self).__init__() self.dense = nn.Linear(config.hidden_size, config.hidden_size) self.activation = nn.Tanh() def forward(self, hidden_states): # We "pool" the model by simply taking the hidden state corresponding # to the first token. first_token_tensor = hidden_states[:, 0] pooled_output = self.dense(first_token_tensor) pooled_output = self.activation(pooled_output) return pooled_output
所以在分類任務中,Bert只取出第一個token的輸出再經過一個網絡進行分類就可以了,就像之前的文章中談到的垃圾郵件識別
BertForSequenceClassification
BertForSequenceClassification 是一個已經實現好的用來進行文本分類的類,一般用來進行文本分類任務。構造函數如下
def __init__(self, config): super(BertForSequenceClassification, self).__init__(config) self.num_labels = config.num_labels self.bert = BertModel(config) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, self.config.num_labels) self.init_weights()
我們可以通過 num_labels 傳遞分類的類別數,從構造函數可以看出這個類大致由3部分組成,1個是Bert,1個是Dropout,1個是用於分類的線性分類器Linear。
Bert用於提取文本特征進行Embedding,Dropout防止過擬合,Linear是一個弱分類器,進行分類,如果需要用更復雜的網絡結構進行分類可以參考它進行改寫。
他的 forward() 函數里面已經定義了損失函數,訓練時可以不用自己額外實現,返回值包括4個內容
def forward(...): ... if labels is not None: if self.num_labels == 1: # We are doing regression loss_fct = MSELoss() loss = loss_fct(logits.view(-1), labels.view(-1)) else: loss_fct = CrossEntropyLoss() loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) outputs = (loss,) + outputs return outputs # (loss), logits, (hidden_states), (attentions)
其中 hidden-states 和 attentions 不一定存在
BertForTokenClassification
BertForSequenceClassification 是一個已經實現好的在 token 級別上進行文本分類的類,一般用來進行序列標注任務。構造函數如下。
代碼基本和 BertForSequenceClassification 是一樣的
def __init__(self, config): super(BertForTokenClassification, self).__init__(config) self.num_labels = config.num_labels self.bert = BertModel(config) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, config.num_labels) self.init_weights()
不同點在於 BertForSequenceClassification 我們只用到了第一個 token 的輸出(經過 pooler 包含了整個句子的信息)
下面是 BertForSequenceClassification 的中 forward() 函數的部分代碼
outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds) pooled_output = outputs[1]
bert 是一個 BertModel 的實例,它的輸出有4個部分,如下所示
def forward(...): ... return outputs # sequence_output, pooled_output, (hidden_states), (attentions)
從上面可以看到 BertForSequenceClassification 用到的是 pooled_output,即用1個位置上的輸出表示整個句子的含義
下面是 BertForTokenClassification 的中 forward() 函數的部分代碼,它用到的是全部 token 上的輸出。
outputs = self.bert(input_ids, attention_mask=attention_mask, token_type_ids=token_type_ids, position_ids=position_ids, head_mask=head_mask, inputs_embeds=inputs_embeds) sequence_output = outputs[0]
BertForQuestionAnswering
實現好的用來做QA(span extraction,片段提取)任務的類。
有多種方法可以根據文本回答問題,一個簡單的情況就是將任務簡化為片段提取
這種任務,輸入以 上下文+問題 的形式出現。輸出是一對整數,表示答案在文本中的開頭和結束位置
圖片來自:https://blog.csdn.net/weixin_37923278/article/details/103006269
參考文章:https://blog.scaleway.com/2019/understanding-text-with-bert/
example:
文本:
Architecturally, the school has a Catholic character. Atop the Main Building's gold dome is a golden statue of the Virgin Mary. Immediately in front of the Main Building and facing it, is a copper statue of Christ with arms upraised with the legend "Venite Ad Me Omnes". Next to the Main Building is the Basilica of the Sacred Heart. Immediately behind the basilica is the Grotto, a Marian place of prayer and reflection. It is a replica of the grotto at Lourdes, France where the Virgin Mary reputedly appeared to Saint Bernadette Soubirous in 1858. At the end of the main drive (and in a direct line that connects through 3 statues and the Gold Dome), is a simple, modern stone statue of Mary.
問題:
The Basilica of the Sacred heart at Notre Dame is beside to which structure?
答案:
start_position: 49,end_position: 51(按單詞計算的)49-51 是 the Main Building 這3個單詞在句中的索引
下面是它的構造函數,和 Classification 相比,這里沒有 Dropout 層
def __init__(self, config): super(BertForQuestionAnswering, self).__init__(config) self.num_labels = config.num_labels self.bert = BertModel(config) self.qa_outputs = nn.Linear(config.hidden_size, config.num_labels) self.init_weights()
模型的輸入多了兩個,start_positions 和 end_positions ,它們的形狀都是 (batch_size,)
start_positions 標記 span 的開始位置(索引),end_positions 標記 span 的結束位置(索引),被標記的 token 用於計算損失
即答案在文本中開始的位置和結束的位置,如果答案不在文本中,應設為0
除了 start 和 end 標記的那段序列外,其他位置上的 token 不會被用來計算損失。
def forward(self, input_ids=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None,
inputs_embeds=None, start_positions=None, end_positions=None): ...
可以參考下圖幫助理解
圖片來自Bert原論文:https://arxiv.org/pdf/1810.04805.pdf
從圖中可以看到,QA 任務的輸入是兩個句子,用 [SEP] 分隔,第一個句子是問題(Question),第二個句子是含有答案的上下文(Paragraph)
輸出是作為答案開始和結束的可能性(Start/End Span)
BertForMultipleChoice
實現好的用來做多選任務的,比如SWAG和MRPC等,用來句子對判斷語義、情感等是否相同
下面是它的構造函數,可以到看到只有1個輸出,用來輸出情感、語義相同的概率
def __init__(self, config): super(BertForMultipleChoice, self).__init__(config) self.bert = BertModel(config) self.dropout = nn.Dropout(config.hidden_dropout_prob) self.classifier = nn.Linear(config.hidden_size, 1) self.init_weights()
一個簡單的例子
example:
tokenizer = BertTokenizer("vocab.txt")
model = BertForMultipleChoice.from_pretrained("bert-base-uncased")
choices = ["Hello, my dog is cute", "Hello, my cat is pretty"]
input_ids = torch.tensor([tokenizer.encode(s) for s in choices]).unsqueeze(0) # 形狀為[1, 2, 7]
labels = torch.tensor(1).unsqueeze(0)
outputs = model(input_ids, labels=labels)
BertForMultipleChoice 也是用到經過 Pooled 的 Bert 輸出,forward() 函數同樣返回 4 個內容
def forward(self, input_ids=None, attention_mask=None, token_type_ids=None, position_ids=None, head_mask=None, inputs_embeds=None, labels=None): ... pooled_output = outputs[1] ... return outputs # (loss), reshaped_logits, (hidden_states), (attentions)
tokenization相關
對於文本,常見的操作是分詞然后將 詞-id 用字典保存,再將分詞后的詞用 id 表示,然后經過 Embedding 輸入到模型中。
Bert 也不例外,但是 Bert 能以 字級別 作為輸入,在處理中文文本時我們可以不用先分詞,直接用 Bert 將文本轉換為 token,然后用相應的 id 表示。
tokenization 庫就是用來將文本切割成為 字或詞 的,下面對其進行簡單的介紹
BasicTokenizer
基本的 tokenization 類,構造函數可以接收以下3個參數
- do_lower_case:是否將輸入轉換為小寫,默認True
- never_split:可選。輸入一個列表,列表內容為不進行 tokenization 的單詞
- tokenize_chinese_chars:可選。是否對中文進行 tokenization,默認True
tokenize() 函數是用來 tokenization 的,這里的 tokenize 僅僅使用空格作為分隔符,輸入的文本會先進行一些數據處理,處理掉無效字符並將空白符(“\t”,“\n”等)統一替換為空格。如果 tokenize_chinese_chars 為 True,則會在每個中文“字”的前后增加空格,然后用 whitespace_tokenize() 進行 tokenization,因為增加了空格,空白符又都統一換成了空格,實際上 whitespace_tokenize() 就是用了 Python 自帶的 split() 函數,處理前用先 strip() 去除了文本前后的空白符。whitespace_tokenize() 的函數內容如下:
def whitespace_tokenize(text): """Runs basic whitespace cleaning and splitting on a piece of text.""" text = text.strip() if not text: return [] tokens = text.split() return tokens
用 split() 進行拆分后,還會將 標點符號 從文本中拆分出來(不是去除)
example:① → ② → ③
① "Hello, Marry!"
② ["Hello,", "Marry!"]
③ ["Hello", ",", "Marry", "!"]
WordpieceTokenizer
WordpieceTokenizer 對文本進行 wordpiece tokenization,接收的文本最好先經過 BasicTokenizer 處理
wordpiece簡介:https://www.jianshu.com/p/60fc9253a0bf
簡單說就是把 單詞 變成一片一片的,在BERT實戰——基於Keras一文中,2.1節我們使用的 tokenizer 就是這樣的
它將 “unaffable” 分割成了 “un”, “##aff” 和 “##able”
他的構造函數可以接收下面的 3 個參數
- vocab:給定一個字典用來 wordpiece tokenization
- unk_token:碰到字典中沒有的字,用來表示未知字符,比如用 "[UNK]" 表示未知字符
- max_input_chars_per_word:每個單詞最大的字符數,如果超過這個長度用 unk_token 對應的 字符 表示,默認100
tokenize()函數
這個類的 tokenize() 函數使用 貪婪最長匹配優先算法(greedy longest-match-first algorithm) 將一段文本進行 tokenization ,變成相應的 wordpiece,一般針對英文
example :
input = "unaffable" → output = ["un", "##aff", "##able"]
BertTokenizer
一個專為 Bert 使用的 tokenization 類,使用 Bert 的時候一般情況下用這個就可以了,構造函數可以傳入以下參數
- vocab_file:一個字典文件,每一行對應一個 wordpiece
- do_lower_case:是否將輸入統一用小寫表示,默認True
- do_basic_tokenize:在使用 WordPiece 之前是否先用 BasicTokenize
- max_len:序列的最大長度
- never_split:一個列表,傳入不進行 tokenization 的單詞,只有在 do_wordpiece_only 為 False 時有效
我們可以使用 tokenize() 函數對文本進行 tokenization,也可以通過 encode() 函數對 文本 進行 tokenization 並將 token 用相應的 id 表示,然后輸入到 Bert 模型中
BertTokenizer 的 tokenize() 函數會用到 WordpieceTokenizer 和 BasicTokenizer 進行 tokenization(實際上由 _tokenize() 函數調用)
_tokenize() 函數的代碼如下,其中basic_tokenizer 和 wordpiece_tokenizer 分別是 BasicTokenizer 和 WordpieceTokenizer 的實例。
def _tokenize(self, text): split_tokens = [] if self.do_basic_tokenize: for token in self.basic_tokenizer.tokenize(text, never_split=self.all_special_tokens): for sub_token in self.wordpiece_tokenizer.tokenize(token): split_tokens.append(sub_token) else: split_tokens = self.wordpiece_tokenizer.tokenize(text) return split_tokens
使用 encode() 函數將 tokenization 后的內容用相應的 id 表示,主要由以下參數:
- text:要編碼的一個文本(第一句話)
- text_pair:可選。要編碼的另一個文本(第二句話)
- add_special_tokens:編碼后,序列前后是否添上特殊符號的id,比如前面添加[CLS],結尾添加[SEP]
- max_length:可選。序列的最大長度
- truncation_strategy:與 max_length 結合使用的,采取的截斷策略。
- 'longest_first':迭代減少序列長度,直到小於 max_length,適用於輸入兩個文本的情況,處理后兩個文本的序列長度之和是 max_length
- 'only_first':僅截斷第一個文本
- 'only_second':僅截斷第二個文本
- 'do_not_truncate':不截斷,如果輸入序列大於 max_length 則會報錯
- return_tensors:可選。返回 TensorFlow (傳入'tf')還是 PyTorch(傳入'pt') 中的 Tensor,而不是返回 Python 列表,前提是已經裝好 TensorFlow 或 PyTorch
注意 encode 只會返回 token id,Bert 我們還需要輸入句子 id,這時候我們可以使用 encode_plus(),它返回 token id 和 句子 id
encode() 實際上就是用了 encode_plus,但是只選擇返回 token_id,代碼如下
...
encoded_inputs = self.encode_plus(text, text_pair=text_pair, max_length=max_length, add_special_tokens=add_special_tokens, stride=stride, truncation_strategy=truncation_strategy, return_tensors=return_tensors, **kwargs) return encoded_inputs["input_ids"]
encode_plus() 的參數與 encode 是一樣的,可以根據實際需求來選擇需要使用的函數