# 查看當(dāng)前掛載的數(shù)據(jù)集目錄, 該目錄下的變更重啟環(huán)境后會(huì)自動(dòng)還原
# View dataset directory.
# This directory will be recovered automatically after resetting environment.
!ls /home/aistudio/data
In [ ]
# 查看工作區(qū)文件, 該目錄下的變更將會(huì)持久保存. 請(qǐng)及時(shí)清理不必要的文件, 避免加載過(guò)慢.
# View personal work directory.
# All changes under this directory will be kept even after reset.
# Please clean unnecessary files in time to speed up environment loading.
!ls /home/aistudio/work
In [1]
# 如果需要進(jìn)行持久化安裝, 需要使用持久化路徑, 如下方代碼示例:
# If a persistence installation is required,
# you need to use the persistence path as the following:
!mkdir /home/aistudio/external-libraries
!pip install transformers==3.4.0 # 直接執(zhí)行此步安轉(zhuǎn)
# !pip install beautifulsoup4 -t /home/aistudio/external-libraries
In [ ]
# 同時(shí)添加如下代碼, 這樣每次環(huán)境(kernel)啟動(dòng)的時(shí)候只要運(yùn)行下方代碼即可:
# Also add the following code,
# so that every time the environment (kernel) starts,
# just run the following code:
import sys
sys.path.append('/home/aistudio/external-libraries')
請(qǐng)點(diǎn)擊此處查看本環(huán)境基本用法.
Please click here for more detailed instructions.
1 BERT的token細(xì)節(jié)
1.1 CLS與SEP
上圖是BERT模型輸入Embedding的過(guò)程,注意到兩個(gè)特殊符號(hào)检痰,一個(gè)是[CLS]崭捍,一個(gè)是[SEP]墓毒。在序列的開(kāi)頭添加的[CLS]主要是用來(lái)學(xué)習(xí)整個(gè)句子或句子對(duì)之間的語(yǔ)義表示识窿。[SEP]主要是用來(lái)分割不同句子换团。
之所以會(huì)選擇[CLS]把还,因?yàn)榕c文本中已有的其他詞相比协屡,這個(gè)無(wú)明顯語(yǔ)義信息的符號(hào)會(huì)更公平地融合文本中各個(gè)詞的語(yǔ)義信息俏脊,從而更好的表示整句話的語(yǔ)義。
1.2 對(duì)應(yīng)token位置的輸出
有了各種各樣的token輸入之后肤晓,BERT模型的輸出是什么呢爷贫。通過(guò)下圖能夠看出會(huì)有兩種輸出认然,一個(gè)對(duì)應(yīng)的是紅色框,也就是對(duì)應(yīng)的[CLS]的輸出漫萄,輸出的shape是[batch size卷员,hidden size];另外一個(gè)對(duì)應(yīng)的是藍(lán)色框腾务,是所有輸入的token對(duì)應(yīng)的輸出毕骡,它的shape是[batch size,seq length窑睁,hidden size]挺峡,這其中不僅僅有[CLS]對(duì)于的輸出,還有其他所有token對(duì)應(yīng)的輸出担钮。
在使用代碼上就要考慮到底是使用第一種輸出橱赠,還是第二種了。大部分情況是是會(huì)選擇[CLS]的輸出箫津,再進(jìn)行微調(diào)的操作狭姨。不過(guò)有的時(shí)候使用所有token的輸出也會(huì)有一些意想不到的效果。
BertPooler就是代表的就是[CLS]的輸出苏遥,可以直接調(diào)用饼拍。大家可以修改下代碼,使其跑通看看田炭。
In [2]
import torch
from torch import nn
In [3]
class BertPooler(nn.Module):
def __init__(self, config):
super().__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.
# hidden_states.shape 為[batch_size, seq_len, hidden_dim]
# assert hidden_states.shape == torch.Size([8, 768])
first_token_tensor = hidden_states[:, 0]
pooled_output = self.dense(first_token_tensor)
pooled_output = self.activation(pooled_output)
return pooled_output
In [4]
class Config:
def __init__(self):
self.hidden_size = 768
self.num_attention_heads = 12
self.attention_probs_dropout_prob = 0.1
config = Config()
bertPooler = BertPooler(config)
input_tensor = torch.ones([8, 50, 768])
output_tensor = bertPooler(input_tensor)
assert output_tensor.shape == torch.Size([8, 50, 768])
上面的代碼會(huì)報(bào)錯(cuò)吧师抄,看看錯(cuò)在哪里,有助于大家理解輸出層的維度教硫。
1.3 BERT的Tokenizer
我們?cè)倏纯瓷厦孢@張關(guān)于BERT模型的輸入的圖叨吮,我們會(huì)發(fā)現(xiàn),在input這行瞬矩,對(duì)于英文的輸入是會(huì)以一種subword的形式進(jìn)行的茶鉴,比如playing這個(gè)詞,是分成play和##ing兩個(gè)subword景用。那對(duì)于中文來(lái)說(shuō)涵叮,是會(huì)分成一個(gè)字一個(gè)字的形式。這么分subword的好處是減小了字典vocab的大小伞插,同時(shí)會(huì)減少OOV的出現(xiàn)割粮。那像playing那樣的分詞方式是怎么做到呢,subword的方式非常多媚污,BERT采用的是wordpiece的方法穆刻,具體知識(shí)可以閱讀補(bǔ)充資料《深入理解NLP Subword算法:BPE、WordPiece杠步、ULM》氢伟。
BERT模型預(yù)訓(xùn)練階段的vocab榜轿,可以點(diǎn)擊data/data56340/vocab.txt查看。
下圖截了一部分朵锣,其中[unused]是可以自己添加token的預(yù)留位置谬盐,101-104會(huì)放一些特殊的符號(hào),這樣大家就明白第一節(jié)最后代碼里添加102的含義了吧诚些。
在實(shí)際代碼過(guò)程中飞傀,有關(guān)tokenizer的操作可以見(jiàn)Transformers庫(kù)中tokenization_bert.py。
里面有很多的可以操作的接口诬烹,大家可以自行嘗試砸烦,下面列了其中一個(gè)。
In [5]
from typing import List, Optional, Tuple
def build_inputs_with_special_tokens(self, token_ids_0: List[int], token_ids_1: Optional[List[int]] = None) -> List[int]:
"""
Build model inputs from a sequence or a pair of sequence for sequence classification tasks
by concatenating and adding special tokens.
A BERT sequence has the following format:
- single sequence: ``[CLS] X [SEP]``
- pair of sequences: ``[CLS] A [SEP] B [SEP]``
Args:
token_ids_0 (:obj:`List[int]`):
List of IDs to which the special tokens will be added.
token_ids_1 (:obj:`List[int]`, `optional`):
Optional second list of IDs for sequence pairs.
Returns:
:obj:`List[int]`: List of `input IDs <../glossary.html#input-ids>`__ with the appropriate special tokens.
"""
if token_ids_1 is None:
return [self.cls_token_id] + token_ids_0 + [self.sep_token_id]
cls = [self.cls_token_id]
sep = [self.sep_token_id]
return cls + token_ids_0 + sep + token_ids_1 + sep
大家改改下面的code試一試绞吁。
In [6]
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('/home/aistudio/data/data56340')
inputs_1 = tokenizer("歡迎大家來(lái)到后廠理工學(xué)院學(xué)習(xí)幢痘。")
print(inputs_1)
inputs_2 = tokenizer("歡迎大家來(lái)到后廠理工學(xué)院學(xué)習(xí)。", "hello")
print(inputs_2)
inputs_3 = tokenizer.encode("歡迎大家來(lái)到后廠理工學(xué)院學(xué)習(xí)家破。", "hello")
print(inputs_3)
inputs_4 = tokenizer.build_inputs_with_special_tokens(inputs_3)
print(inputs_4)
2 MLM和NSP預(yù)訓(xùn)練任務(wù)
此階段我們開(kāi)始對(duì)兩個(gè)BERT的預(yù)訓(xùn)練任務(wù)展開(kāi)學(xué)習(xí)颜说,Let`s go!
2.1 MLM
如何理解MLM汰聋,可以先從LM(language model门粪,語(yǔ)言模型)入手,LM的目地是基于上文的內(nèi)容來(lái)預(yù)測(cè)下文的可能出現(xiàn)的詞烹困,由于LM是單向的玄妈,要不從左到右要不從右到左,很難做到結(jié)合上下文語(yǔ)義髓梅。為了改進(jìn)LM拟蜻,實(shí)現(xiàn)雙向的學(xué)習(xí),MLM就是一種女淑,通過(guò)對(duì)輸入文本序列隨機(jī)的mask,然后通過(guò)上下文來(lái)預(yù)測(cè)這個(gè)mask應(yīng)該是什么詞辜御,至此解決了雙向的問(wèn)題鸭你。這個(gè)任務(wù)的表現(xiàn)形式更像是完形填空,通過(guò)此方向使得BERT完成自監(jiān)督的學(xué)習(xí)任務(wù)擒权。
那隨機(jī)的mask是怎么做的呢袱巨?具體的做法是,將每個(gè)輸入的數(shù)據(jù)句子中15%的概率隨機(jī)抽取token碳抄,在這15%中的80%概論將token替換成[MASK]愉老,如上圖所示,15%中的另外10%替換成其他token剖效,比如把‘理’換成‘后’嫉入,15%中的最后10%保持不變焰盗,就是還是‘理’這個(gè)token。
之所以采用三種不同的方式做mask咒林,是因?yàn)楹竺娴膄ine-tuning階段并不會(huì)做mask的操作熬拒,為了減少pre-training和fine-tuning階段輸入分布不一致的問(wèn)題,所以采用了這種策略垫竞。
如果使用MLM澎粟,它的輸出層可以參照下面代碼,取自Transformers庫(kù)中modeling_bert.py欢瞪。
In [7]
class BertLMPredictionHead(nn.Module):
def __init__(self, config):
super().__init__()
# 這部操作加了一些全連接層和layer歸一化
self.transform = BertPredictionHeadTransform(config)
# The output weights are the same as the input embeddings, but there is an output-only bias for each token.
# 在nn.Linear操作過(guò)程中的權(quán)重和bert輸入的embedding權(quán)重共享活烙,思考下為什么需要共享?原因見(jiàn)下面描述遣鼓。
# self.decoder在預(yù)測(cè)生成token的概論
self.decoder = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.bias = nn.Parameter(torch.zeros(config.vocab_size))
# decoder層雖然權(quán)重是共享的啸盏,但是會(huì)多一個(gè)bias偏置項(xiàng),在此設(shè)置
# Need a link between the two variables so that the bias is correctly resized with `resize_token_embeddings`
self.decoder.bias = self.bias
def forward(self, hidden_states):
hidden_states = self.transform(hidden_states)
hidden_states = self.decoder(hidden_states)
return hidden_states
Embedding層和FC層(上面代碼nn.Linear層)權(quán)重共享譬正。
Embedding層可以說(shuō)是通過(guò)onehot去取到對(duì)應(yīng)的embedding向量宫补,F(xiàn)C層可以說(shuō)是相反的,通過(guò)向量(定義為 v)去得到它可能是某個(gè)詞的softmax概率曾我,取概率最大(貪婪情況下)的作為預(yù)測(cè)值粉怕。那哪一個(gè)會(huì)是概率最大的呢?Embedding層和FC層權(quán)重共享抒巢,Embedding層中和向量 v 最接近的那一行對(duì)應(yīng)的詞贫贝,會(huì)獲得更大的預(yù)測(cè)概率。實(shí)際上蛉谜,Embedding層和FC層有點(diǎn)像互為逆過(guò)程稚晚。
通過(guò)這樣的權(quán)重共享可以減少參數(shù)的數(shù)量,加快收斂型诚。
我們有了BertLMPredictionHead后客燕,就可以完成MLM的預(yù)訓(xùn)練任務(wù)了。有兩種選擇狰贯,第一個(gè)是BertOnlyMLMHead也搓,它是只考慮單獨(dú)MLM任務(wù)的,通過(guò)BertForMaskedLM完成最終的預(yù)訓(xùn)練涵紊,Loss是CrossEntropyLoss傍妒;第二個(gè)是BertPreTrainingHeads,它是同時(shí)考慮MLM和NSP任務(wù)的摸柄,通過(guò)BertForPreTraining完成颤练,Loss是CrossEntropyLoss。原本論文肯定是第二種MLM和NSP一塊訓(xùn)練的驱负,但如果有單獨(dú)訓(xùn)練任務(wù)需求是使用者可自行選擇嗦玖。
以上提到的如BertOnlyMLMHead類患雇,可以查閱Transformers庫(kù)modeling_bert.py。
2.2 NSP
BERT的作者在設(shè)計(jì)任務(wù)時(shí)踏揣,還考慮了兩個(gè)句子之間的關(guān)系庆亡,來(lái)補(bǔ)充MLM任務(wù)能力,設(shè)計(jì)了Next Sentence Prediction(NSP)任務(wù)捞稿,這個(gè)任務(wù)比較簡(jiǎn)單又谋,NSP取[CLS]的最終輸出進(jìn)行二分類,來(lái)判斷輸入的兩個(gè)句子是不是前后相連的關(guān)系娱局。
構(gòu)建數(shù)據(jù)的方法是彰亥,對(duì)于句子1,句子2以50%的概率為句子1相連的下一句衰齐,以50%的概率在語(yǔ)料庫(kù)里隨機(jī)抽取一句任斋。以此構(gòu)建了一半正樣本一半負(fù)樣本。
從上圖可以看出耻涛,NSP任務(wù)實(shí)現(xiàn)比較簡(jiǎn)單废酷,直接拿[CLS]的輸出加上一個(gè)全連接層實(shí)現(xiàn)二分類就可以了。
self.seq_relationship = nn.Linear(config.hidden_size, 2)
最后采用CrossEntropyLoss計(jì)算損失抹缕。
3 代碼實(shí)操預(yù)訓(xùn)練
BERT預(yù)訓(xùn)任務(wù)分為MLM和NSP澈蟆,后續(xù)一些預(yù)訓(xùn)練模型的嘗試發(fā)現(xiàn),NSP任務(wù)其實(shí)應(yīng)該比較小卓研,所以如果大家在預(yù)訓(xùn)練模型的基礎(chǔ)上繼續(xù)訓(xùn)練趴俘,可以直接跑MLM任務(wù)。
3.1 mask token 處理
在進(jìn)行BERT的預(yù)訓(xùn)練時(shí)奏赘,模型送進(jìn)模型的之前需要對(duì)數(shù)據(jù)進(jìn)行mask操作寥闪,處理代碼如下:
In [8]
def mask_tokens(inputs: torch.Tensor, tokenizer: PreTrainedTokenizer, args) -> Tuple[torch.Tensor, torch.Tensor]:
""" Prepare masked tokens inputs/labels for masked language modeling: 80% MASK, 10% random, 10% original. """
if tokenizer.mask_token is None:
raise ValueError(
"This tokenizer does not have a mask token which is necessary for masked language modeling. Remove the --mlm flag if you want to use this tokenizer."
)
labels = inputs.clone()
# We sample a few tokens in each sequence for masked-LM training (with probability args.mlm_probability defaults to 0.15 in Bert/RoBERTa)
probability_matrix = torch.full(labels.shape, args.mlm_probability)
# 調(diào)出[MASK]
special_tokens_mask = [
tokenizer.get_special_tokens_mask(val, already_has_special_tokens=True) for val in labels.tolist()
]
probability_matrix.masked_fill_(torch.tensor(special_tokens_mask, dtype=torch.bool), value=0.0)
if tokenizer._pad_token is not None:
padding_mask = labels.eq(tokenizer.pad_token_id)
probability_matrix.masked_fill_(padding_mask, value=0.0)
masked_indices = torch.bernoulli(probability_matrix).bool()
labels[~masked_indices] = -100 # We only compute loss on masked tokens
# 80% of the time, we replace masked input tokens with tokenizer.mask_token ([MASK])
indices_replaced = torch.bernoulli(torch.full(labels.shape, 0.8)).bool() & masked_indices
inputs[indices_replaced] = tokenizer.convert_tokens_to_ids(tokenizer.mask_token)
# 10% of the time, we replace masked input tokens with random word
indices_random = torch.bernoulli(torch.full(labels.shape, 0.5)).bool() & masked_indices & ~indices_replaced
random_words = torch.randint(len(tokenizer), labels.shape, dtype=torch.long)
inputs[indices_random] = random_words[indices_random]
# The rest of the time (10% of the time) we keep the masked input tokens unchanged
return inputs, labels
3.2 大型模型訓(xùn)練策略
對(duì)于BERT的預(yù)訓(xùn)練操作,會(huì)涉及很多訓(xùn)練策略磨淌,目地都是解決如何在大規(guī)模訓(xùn)練時(shí)減少訓(xùn)練時(shí)間疲憋,充分利用算力資源。以下代碼實(shí)例梁只。
In [9]
# gradient_accumulation梯度累加
# 一般在單卡GPU訓(xùn)練時(shí)常用策略缚柳,以防止顯存溢出
if args.max_steps > 0:
t_total = args.max_steps
args.num_train_epochs = args.max_steps // (len(train_dataloader) // args.gradient_accumulation_steps) + 1
else:
t_total = len(train_dataloader) // args.gradient_accumulation_steps * args.num_train_epochs
In [ ]
# Nvidia提供了一個(gè)混合精度工具apex
# 實(shí)現(xiàn)混合精度訓(xùn)練加速
if args.fp16:
try:
from apex import amp
except ImportError:
raise ImportError("Please install apex from https://www.github.com/nvidia/apex to use fp16 training.")
model, optimizer = amp.initialize(model, optimizer, opt_level=args.fp16_opt_level)
In [ ]
# multi-gpu training (should be after apex fp16 initialization)
# 一機(jī)多卡
if args.n_gpu > 1:
model = torch.nn.DataParallel(model)
In [ ]
# Distributed training (should be after apex fp16 initialization)
# 多機(jī)多卡分布式訓(xùn)練
if args.local_rank != -1:
model = torch.nn.parallel.DistributedDataParallel(
model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=True
)
以上代碼都是常添加在BERT訓(xùn)練代碼中的策略方法,這里提供一個(gè)補(bǔ)充資料《神經(jīng)網(wǎng)絡(luò)分布式訓(xùn)練敛纲、混合精度訓(xùn)練喂击、梯度累加...一文帶你優(yōu)雅地訓(xùn)練大型模型》剂癌。
在訓(xùn)練策略上淤翔,基于Transformer結(jié)構(gòu)的大規(guī)模預(yù)訓(xùn)練模型預(yù)訓(xùn)練和微調(diào)都會(huì)采用wramup的方式。
scheduler = get_linear_schedule_with_warmup(
optimizer, num_warmup_steps=args.warmup_steps, num_training_steps=t_total
)
那BERT中的warmup有什么作用呢佩谷?
在預(yù)訓(xùn)練模型訓(xùn)練的開(kāi)始階段旁壮,BERT模型對(duì)數(shù)據(jù)的初始分布理解很少监嗜,在第一輪訓(xùn)練的時(shí)候,模型的權(quán)重會(huì)迅速改變抡谐。如果一開(kāi)始學(xué)習(xí)率很大裁奇,非常有可能對(duì)數(shù)據(jù)產(chǎn)生過(guò)擬合的學(xué)習(xí),后面需要很多輪的訓(xùn)練才能彌補(bǔ)麦撵,會(huì)花費(fèi)更多的訓(xùn)練時(shí)間刽肠。但模型訓(xùn)練一段時(shí)間后,模型對(duì)數(shù)據(jù)分布已經(jīng)有了一定的學(xué)習(xí)免胃,這時(shí)就可以提升學(xué)習(xí)率音五,能夠使得模型更快的收斂,訓(xùn)練也更加穩(wěn)定羔沙,這個(gè)過(guò)程就是warmup躺涝,學(xué)習(xí)率是從低逐漸增高的過(guò)程。
那為什么warmup之后會(huì)有decay的操作扼雏?
當(dāng)BERT模型訓(xùn)練一定時(shí)間后坚嗜,尤其是后續(xù)快要收斂的時(shí)候,如果還是比較大的學(xué)習(xí)率诗充,比較難以收斂苍蔬,調(diào)低學(xué)習(xí)率能夠更好的微調(diào)。
更多的思考可以閱讀《神經(jīng)網(wǎng)絡(luò)中 warmup 策略為什么有效其障;有什么理論解釋么银室?》。
好了励翼,預(yù)訓(xùn)練的知識(shí)基本就這些了蜈敢,挖的比較深。
如果你想自己來(lái)一些預(yù)訓(xùn)練的嘗試汽抚,可以github上找一份源碼抓狭,再去找一個(gè)中文數(shù)據(jù)集試一試。
如果只是想用一用BERT造烁,那就可以繼續(xù)下一節(jié)課微調(diào)模型的學(xué)習(xí)否过,以后的工作中大部分時(shí)間會(huì)花在處理微調(diào)模型的過(guò)程中。
同學(xué)們加油惭蟋!
4 BERT微調(diào)細(xì)節(jié)詳解
上面我們已經(jīng)對(duì)BERT的預(yù)訓(xùn)練任務(wù)有了深刻的理解苗桂,本環(huán)節(jié)將對(duì)BERT的Fine-tuning微調(diào)展開(kāi)探討。
預(yù)訓(xùn)練+微調(diào)技術(shù)掌握熟練后告组,就可以在自己的業(yè)務(wù)上大展身教了煤伟,可以做一些大膽的嘗試。
4.1 BERT微調(diào)任務(wù)介紹
微調(diào)(Fine-tuning)是在BERT強(qiáng)大的預(yù)訓(xùn)練后完成NLP下游任務(wù)的步驟,這也是所謂的遷移策略便锨,充分應(yīng)用大規(guī)模的預(yù)訓(xùn)練模型的優(yōu)勢(shì)围辙,只在下游任務(wù)上再進(jìn)行一些微調(diào)訓(xùn)練,就可以達(dá)到非常不錯(cuò)的效果放案。
下圖是BERT原文中微調(diào)階段4各種類型的下游任務(wù)姚建。其中包括:
- 句子對(duì)匹配(sentence pair classification)
- 文本分類(single sentence classification)
- 抽取式問(wèn)答(question answering)
- 序列標(biāo)注(single sentence tagging)
4.2 文本分類任務(wù)
我們先看看文本分類任務(wù)的基本微調(diào)操作。如下圖所示吱殉,最基本的做法就是將預(yù)訓(xùn)練的BERT讀取進(jìn)來(lái)掸冤,同時(shí)在[CLS]的輸出基礎(chǔ)上加上一個(gè)全連接層,全連接層的輸出維度就是分類的類別數(shù)友雳。
從代碼實(shí)現(xiàn)上看可以從兩個(gè)角度出發(fā):
1.直接調(diào)用Transformers庫(kù)中BertForSequenceClassification類實(shí)現(xiàn)贩虾,代碼如下:
In [10]
import torch
import torch.nn as nn
class BertForSequenceClassification(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
# 考慮多分類的問(wèn)題
self.num_labels = config.num_labels
# 調(diào)用bert預(yù)訓(xùn)練模型
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 在預(yù)訓(xùn)練的BERT上加上一個(gè)全連接層,用于微調(diào)分類模型
# config.num_labels是分類數(shù)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
2.如果想做一些更復(fù)雜的微調(diào)模型沥阱,可以參照上述封裝好的類缎罢,寫(xiě)一個(gè)自己需要的微調(diào)層滿足分類的需求,代碼如下:
In [11]
class NewModel(nn.Module):
def __init__(self):
super(NewModel, self).__init__()
# 調(diào)用bert預(yù)訓(xùn)練模型
self.model = BertModel.from_pretrained(modelPath)
# 可以自定義一些其他網(wǎng)絡(luò)做為微調(diào)層的結(jié)構(gòu)
self.cnn = nn.Conv2d()
self.rnn = nn.GRU()
self.dropout = nn.Dropout(0.1)
# 最后的全連接層考杉,用于分類
self.l1 = nn.Linear(768, 2)
對(duì)比一下上述兩個(gè)類策精,你會(huì)發(fā)現(xiàn)如果是調(diào)用Transformers中的BertForSequenceClassification,加載bert預(yù)訓(xùn)練模型僅傳了一個(gè)config崇棠,而自己創(chuàng)建類咽袜,要傳整個(gè)預(yù)訓(xùn)練模型的路徑(其中包括config和model文件)。大家思考下枕稀,看看源碼尋找答案询刹?
4.3 文本匹配任務(wù)
接著我們看下匹配問(wèn)題是如何搭建的,網(wǎng)絡(luò)結(jié)構(gòu)如下圖所示萎坷。
雖然文本匹配問(wèn)題的微調(diào)結(jié)構(gòu)和分類問(wèn)題有一定的區(qū)別凹联,它的輸入是兩個(gè)句子,但是它最終的輸出依然是要做一個(gè)二分類的問(wèn)題哆档,所以如果你想用BERT微調(diào)一個(gè)文本匹配模型蔽挠,可以和分類問(wèn)題用的代碼是一樣的,依然可以采用Transformers庫(kù)中BertForSequenceClassification類實(shí)現(xiàn)瓜浸,只不過(guò)最終全連接層輸出的維度為2澳淑。
tips:實(shí)際在工程中,經(jīng)過(guò)大量的驗(yàn)證插佛,如果直接采用上述的BERT模型微調(diào)文本匹配問(wèn)題杠巡,效果不一定很好。一般解決文本匹配問(wèn)題會(huì)采用一些類似孿生網(wǎng)絡(luò)的結(jié)構(gòu)去解決雇寇,該課就不過(guò)多介紹了氢拥。
4.4 序列標(biāo)注任務(wù)
下面我們看一下序列標(biāo)注問(wèn)題绑改,BERT模型是如何進(jìn)行微調(diào)的。下圖是原論文中給出的微調(diào)結(jié)構(gòu)圖兄一。
理解序列標(biāo)注問(wèn)題,要搞清楚它主要是在做什么事情识腿。一般的分詞任務(wù)出革、詞性標(biāo)注和命名體識(shí)別任務(wù)都屬于序列標(biāo)注問(wèn)題。這類問(wèn)題因?yàn)檩斎刖渥拥拿恳粋€(gè)token都需要預(yù)測(cè)它們的標(biāo)簽渡讼,所以序列標(biāo)注是一個(gè)單句多l(xiāng)abel分類任務(wù)骂束,BERT模型的所有輸出(除去特殊符號(hào))都要給出一個(gè)預(yù)測(cè)結(jié)果。
同時(shí)成箫,我們要保證BERT的微調(diào)層的輸出是[batch_size, seq_len, num_labels]展箱。
如果繼續(xù)使用Transformers庫(kù),可以直接調(diào)用BertForTokenClassification類蹬昌。部分代碼如下:
In [ ]
class BertForTokenClassification(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
# 序列標(biāo)注的類別數(shù)
self.num_labels = config.num_labels
# 調(diào)用BERT預(yù)訓(xùn)練模型混驰,同時(shí)關(guān)掉pooling_layer的輸出,原因在上段有解釋皂贩。
self.bert = BertModel(config, add_pooling_layer=False)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 增加一個(gè)微調(diào)階段的分類器栖榨,對(duì)每一個(gè)token都進(jìn)行分類
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
同理,如果想進(jìn)一步提升序列標(biāo)注的性能明刷,也是要自己增加一些層婴栽,感興趣的可以自己試試啊。
4.5 問(wèn)答任務(wù)
論文里還有最后一種微調(diào)結(jié)構(gòu)辈末,就是抽取式的QA微調(diào)模型愚争,該問(wèn)題是在SQuAD1.1設(shè)計(jì)的,如下圖所示挤聘。
QA問(wèn)題的微調(diào)模型搭建也不難轰枝,一些初始化的操作見(jiàn)下面代碼(源自Transformers庫(kù)):
In [ ]
class BertForQuestionAnswering(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
# 判斷token是答案的起點(diǎn)和終點(diǎn)的類別,也就是一個(gè)二分類的問(wèn)題组去,此處應(yīng)該等于2
self.num_labels = config.num_labels
# 導(dǎo)入BERT的預(yù)訓(xùn)練模型狸膏,同時(shí)不輸出pooling層,那就是把所有token對(duì)應(yīng)的輸出都保留
# 輸出維度是[batch_size, seq_len, embedding_dim]
self.bert = BertModel(config, add_pooling_layer=False)
# 通過(guò)一個(gè)全連接層實(shí)現(xiàn)抽取分類任務(wù)
self.qa_outputs = nn.Linear(config.hidden_size, config.num_labels)
說(shuō)到這里添怔,大家可能還是不太好理解QA問(wèn)題的微調(diào)過(guò)程湾戳,我們?cè)诳聪孪鄬?duì)應(yīng)的forward代碼。
In [ ]
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,
output_attentions=None,
output_hidden_states=None,
return_dict=None,
):
return_dict = return_dict if return_dict is not None else self.config.use_return_dict
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,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
# 拿到所有token的輸出
sequence_output = outputs[0]
# 得到每個(gè)token對(duì)應(yīng)的分類結(jié)果广料,就是分為start位置和end位置的概論
logits = self.qa_outputs(sequence_output)
start_logits, end_logits = logits.split(1, dim=-1)
start_logits = start_logits.squeeze(-1)
end_logits = end_logits.squeeze(-1)
total_loss = None
if start_positions is not None and end_positions is not None:
# If we are on multi-GPU, split add a dimension
if len(start_positions.size()) > 1:
start_positions = start_positions.squeeze(-1)
if len(end_positions.size()) > 1:
end_positions = end_positions.squeeze(-1)
# sometimes the start/end positions are outside our model inputs, we ignore these terms
ignored_index = start_logits.size(1)
start_positions.clamp_(0, ignored_index)
end_positions.clamp_(0, ignored_index)
# 通過(guò)交叉熵來(lái)計(jì)算loss
loss_fct = CrossEntropyLoss(ignore_index=ignored_index)
start_loss = loss_fct(start_logits, start_positions)
end_loss = loss_fct(end_logits, end_positions)
total_loss = (start_loss + end_loss) / 2
if not return_dict:
output = (start_logits, end_logits) + outputs[2:]
return ((total_loss,) + output) if total_loss is not None else output
# 結(jié)果是要返回start和end的結(jié)果
return QuestionAnsweringModelOutput(
loss=total_loss,
start_logits=start_logits,
end_logits=end_logits,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
以上四個(gè)任務(wù)就是BERT原論文中提到的微調(diào)任務(wù)砾脑,實(shí)現(xiàn)方式大體都比較相像,在實(shí)際的使用過(guò)程中可以借鑒艾杏。
5 微調(diào)模型的設(shè)計(jì)問(wèn)題
5.1 預(yù)訓(xùn)練模型輸入長(zhǎng)度的限制
我們通過(guò)對(duì)BERT預(yù)訓(xùn)練模型的了解韧衣,可以知道,BERT預(yù)設(shè)的最大文本長(zhǎng)度為512。
# Transformers源碼configuration_bert.py中的定義
def __init__(
self,
vocab_size=30522,
hidden_size=768,
num_hidden_layers=12,
num_attention_heads=12,
intermediate_size=3072,
hidden_act="gelu",
hidden_dropout_prob=0.1,
attention_probs_dropout_prob=0.1,
max_position_embeddings=512, # 通過(guò)這個(gè)參數(shù)可以得知預(yù)訓(xùn)練bert的長(zhǎng)度
type_vocab_size=2,
initializer_range=0.02,
layer_norm_eps=1e-12,
pad_token_id=0,
gradient_checkpointing=False,
**kwargs
):
也就是說(shuō)畅铭,BERT模型要求輸入句子的長(zhǎng)度不能超過(guò)512氏淑,同時(shí)還要考慮[CLS]這些特殊符號(hào)的存在,實(shí)際文本的長(zhǎng)度會(huì)更短硕噩。
究其原因假残,隨著文本長(zhǎng)度的不斷增加,計(jì)算所需要的顯存也會(huì)成線性增加炉擅,運(yùn)行時(shí)間也會(huì)隨著增長(zhǎng)辉懒。所以輸入文本的長(zhǎng)度是需要加以控制的。
在實(shí)際的任務(wù)中我們的輸入文本一般會(huì)有兩個(gè)方面谍失,要不就是特別長(zhǎng)眶俩,比如文本摘要、閱讀理解任務(wù)快鱼,它們的輸入文本是有可能超過(guò)512颠印;另外一種就是一些短文本任務(wù),如短文本分類任務(wù)抹竹。
下面我們會(huì)給出一些方法嗽仪。
5.2 長(zhǎng)文本問(wèn)題
說(shuō)到長(zhǎng)文本處理,最直接的方法就是截?cái)唷?/p>
由于 Bert 支持最大長(zhǎng)度為 512 個(gè)token,那么如何截取文本也成為一個(gè)很關(guān)鍵的問(wèn)題。
《How to Fine-Tune BERT for Text Classification?》給出了幾種解決方法:
- head-only: 保存前 510 個(gè) token (留兩個(gè)位置給 [CLS] 和 [SEP] )
- tail-only: 保存最后 510 個(gè)token
- head + tail : 選擇前128個(gè) token 和最后382個(gè) token
作者是在IMDB和Sogou News數(shù)據(jù)集上做的試驗(yàn)棚辽,發(fā)現(xiàn)head+tail效果會(huì)更好一些。但是在實(shí)際的問(wèn)題中窿凤,大家還是要人工的篩選一些數(shù)據(jù)觀察數(shù)據(jù)的分布情況,視情況選擇哪種截?cái)嗟姆椒ā?/p>
除了上述截?cái)嗟姆椒ㄖ饪缧罚€可以采用sliding window的方式做雳殊。
用劃窗的方式對(duì)長(zhǎng)文本切片,分別放到BERT里窗轩,得到相對(duì)應(yīng)的CLS夯秃,然后對(duì)CLS進(jìn)行融合,融合的方式也比較多痢艺,可以參考以下方式:
- max pooling最大池化
- avg pooling平均池化
- attention注意力融合
- transformer等
相關(guān)思考可以參考:《Multi-passage BERT: A Globally Normalized BERT Model for Open-domain Question Answering》和《PARADE: Passage Representation Aggregation for Document Reranking》
5.3 短文本問(wèn)題
在遇到一些短文本的NLP任務(wù)時(shí)仓洼,我們可以對(duì)輸入文本進(jìn)行一定的截?cái)啵驗(yàn)檫^(guò)長(zhǎng)的文本會(huì)增加相應(yīng)的計(jì)算量堤舒。
那如何選取短文本的輸入長(zhǎng)度呢色建?需要大家對(duì)數(shù)據(jù)進(jìn)行簡(jiǎn)單的分析。雖然簡(jiǎn)單舌缤,但這往往是工作中必須要注意的細(xì)節(jié)箕戳。
5.4 微調(diào)層的設(shè)計(jì)
針對(duì)不同的任務(wù)大家可以繼續(xù)在bert的預(yù)訓(xùn)練模型基礎(chǔ)上加一些網(wǎng)絡(luò)的設(shè)計(jì)某残,比如文本分類上加一些cnn;比如在序列標(biāo)注上加一些crf等等陵吸。
往往可以根據(jù)經(jīng)驗(yàn)進(jìn)行嘗試玻墅。
5.4.1 Bert+CNN
CNN結(jié)構(gòu)在學(xué)習(xí)一些短距離文本特征上有一定的優(yōu)勢(shì),可以和Bert進(jìn)行結(jié)合壮虫,會(huì)有不錯(cuò)的效果澳厢。
下圖是TextCNN算法的結(jié)構(gòu)示意圖,同學(xué)們可以嘗試補(bǔ)全下面代碼旨指,完成Bert和TextCNN的結(jié)合。
In [ ]
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import BertPreTrainedModel, BertModel
class Conv1d(nn.Module):
def __init__(self, in_channels, out_channels, filter_sizes):
super(Conv1d, self).__init__()
self.convs = nn.ModuleList([
nn.Conv1d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=fs)
for fs in filter_sizes
])
self.init_params()
def init_params(self):
for m in self.convs:
nn.init.xavier_uniform_(m.weight.data)
nn.init.constant_(m.bias.data, 0.1)
def forward(self, x):
return [F.relu(conv(x)) for conv in self.convs]
In [ ]
class BertCNN(BertPreTrainedModel):
def __init__(self, config, num_labels, n_filters, filter_sizes):
# total_filter_sizes = "2 2 3 3 4 4"
# filter_sizes = [int(val) for val in total_filter_sizes.split()]
# n_filters = 6
super(BertCNN, self).__init__(config)
self.num_labels = num_labels
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.convs = Conv1d(config.hidden_size, n_filters, filter_sizes)
self.classifier = nn.Linear(len(filter_sizes) * n_filters, num_labels)
self.apply(self.init_bert_weights)
def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None):
"""
Args:
input_ids: 詞對(duì)應(yīng)的 id
token_type_ids: 區(qū)分句子喳整,0 為第一句谆构,1表示第二句
attention_mask: 區(qū)分 padding 與 token, 1表示是token框都,0 為padding
"""
encoded_layers, _ = self.bert(
input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False)
# encoded_layers: [batch_size, seq_len, bert_dim=768]
encoded_layers = self.dropout(encoded_layers)
"""
one code # 對(duì)encoded_layers做維度調(diào)整
one code # 調(diào)用conv層
one code # 圖中所示采用最大池化融合
"""
cat = self.dropout(torch.cat(pooled, dim=1))
# cat: [batch_size, filter_num * len(filter_sizes)]
logits = self.classifier(cat)
# logits: [batch_size, output_dim]
if labels is not None:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
return loss
else:
return logits
上面代碼共有三行需要填寫(xiě)搬素,主要是TextCNN結(jié)構(gòu)的邏輯,大家要多加思考魏保。
填完后熬尺,可以參照下面代碼答案。
class BertCNN(nn.Module):
def __init__(self, config, num_labels, n_filters, filter_sizes):
super(BertCNN, self).__init__(config)
self.num_labels = num_labels
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.convs = Conv1d(config.hidden_size, n_filters, filter_sizes)
self.classifier = nn.Linear(len(filter_sizes) * n_filters, num_labels)
self.apply(self.init_bert_weights)
def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None):
"""
Args:
input_ids: 詞對(duì)應(yīng)的 id
token_type_ids: 區(qū)分句子谓罗,0 為第一句粱哼,1表示第二句
attention_mask: 區(qū)分 padding 與 token, 1表示是token檩咱,0 為padding
"""
encoded_layers, _ = self.bert(
input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False)
# encoded_layers: [batch_size, seq_len, bert_dim=768]
encoded_layers = self.dropout(encoded_layers)
"""
one code # 對(duì)encoded_layers做維度調(diào)整
one code # 調(diào)用conv層
one code # 圖中所示采用最大池化融合
"""
encoded_layers = encoded_layers.permute(0, 2, 1)
# encoded_layers: [batch_size, bert_dim=768, seq_len]
conved = self.convs(encoded_layers)
# conved 是一個(gè)列表揭措, conved[0]: [batch_size, filter_num, *]
pooled = [F.max_pool1d(conv, conv.shape[2]).squeeze(2)
for conv in conved]
# pooled 是一個(gè)列表, pooled[0]: [batch_size, filter_num]
cat = self.dropout(torch.cat(pooled, dim=1))
# cat: [batch_size, filter_num * len(filter_sizes)]
logits = self.classifier(cat)
# logits: [batch_size, output_dim]
if labels is not None:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
return loss
else:
return logits
5.4.2 Bert+LSTM
那要是想加上一個(gè)lstm呢刻蚯?參照下面代碼绊含。
In [ ]
class BertLSTM(BertPreTrainedModel):
def __init__(self, config, num_labels, rnn_hidden_size, num_layers, bidirectional, dropout):
super(BertLSTM, self).__init__(config)
self.num_labels = num_labels
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.rnn = nn.LSTM(config.hidden_size, rnn_hidden_size, num_layers,bidirectional=bidirectional, batch_first=True, dropout=dropout)
self.classifier = nn.Linear(rnn_hidden_size * 2, num_labels)
self.apply(self.init_bert_weights)
def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None):
encoded_layers, _ = self.bert(
input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False)
encoded_layers = self.dropout(encoded_layers)
# encoded_layers: [batch_size, seq_len, bert_dim]
_, (hidden, cell) = self.rnn(encoded_layers)
# outputs: [batch_size, seq_len, rnn_hidden_size * 2]
hidden = self.dropout(
torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)) # 連接最后一層的雙向輸出
logits = self.classifier(hidden)
if labels is not None:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
return loss
else:
return logits
5.4.3 Bert+attention
當(dāng)然,你也可以加一個(gè)attention炊汹。
In [ ]
class BertATT(BertPreTrainedModel):
"""BERT model for classification.
This module is composed of the BERT model with a linear layer on top of
the pooled output.
Params:
`config`: a BertConfig class instance with the configuration to build a new model.
`num_labels`: the number of classes for the classifier. Default = 2.
"""
def __init__(self, config, num_labels):
super(BertATT, self).__init__(config)
self.num_labels = num_labels
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, num_labels)
self.W_w = nn.Parameter(torch.Tensor(config.hidden_size, config.hidden_size))
self.u_w = nn.Parameter(torch.Tensor(config.hidden_size, 1))
nn.init.uniform_(self.W_w, -0.1, 0.1)
nn.init.uniform_(self.u_w, -0.1, 0.1)
self.apply(self.init_bert_weights)
def forward(self, input_ids, token_type_ids=None, attention_mask=None, labels=None):
"""
Args:
input_ids: 詞對(duì)應(yīng)的 id
token_type_ids: 區(qū)分句子躬充,0 為第一句,1表示第二句
attention_mask: 區(qū)分 padding 與 token讨便, 1表示是token充甚,0 為padding
"""
encoded_layers, _ = self.bert(
input_ids, token_type_ids, attention_mask, output_all_encoded_layers=False)
encoded_layers = self.dropout(encoded_layers)
# encoded_layers: [batch_size, seq_len, bert_dim=768]
score = torch.tanh(torch.matmul(encoded_layers, self.W_w))
# score: [batch_size, seq_len, bert_dim]
attention_weights = F.softmax(torch.matmul(score, self.u_w), dim=1)
# attention_weights: [batch_size, seq_len, 1]
scored_x = encoded_layers * attention_weights
# scored_x : [batch_size, seq_len, bert_dim]
feat = torch.sum(scored_x, dim=1)
# feat: [batch_size, bert_dim=768]
logits = self.classifier(feat)
# logits: [batch_size, output_dim]
if labels is not None:
loss_fct = CrossEntropyLoss()
loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
return loss
else:
return logits
6 微調(diào)階段的調(diào)整策略
6.1不同學(xué)習(xí)率的設(shè)置
在《How to Fine-Tune BERT for Text Classification?》一文中作者提到了一個(gè)策略。
這個(gè)策略叫作 slanted triangular(繼承自 ULM-Fit)霸褒。它和 BERT 的原版方案類似津坑,都是帶 warmup 的先增后減。通常來(lái)說(shuō)傲霸,這類方案對(duì)初始學(xué)習(xí)率的設(shè)置并不敏感疆瑰。但是眉反,在 fine-tune階段使用過(guò)大的學(xué)習(xí)率,會(huì)打亂 pretrain 階段學(xué)習(xí)到的句子信息穆役,造成“災(zāi)難性遺忘”寸五。
比如下方的圖(源于論文),最右邊學(xué)習(xí)率=4e-4的loss已經(jīng)完全無(wú)法收斂了耿币,而學(xué)習(xí)率=1e-4的loss曲線明顯不如學(xué)習(xí)率=2e-5和學(xué)習(xí)率=5e-5的低梳杏。
綜上所述,對(duì)于BERT模型的訓(xùn)練和微調(diào)學(xué)習(xí)率取2e-5和5e-5效果會(huì)好一些淹接。
不過(guò)對(duì)于上述的學(xué)習(xí)率針對(duì)的是BERT沒(méi)有下游微調(diào)結(jié)構(gòu)的十性,是直接用BERT去fine-tune。
那如果微調(diào)的時(shí)候接了更多的結(jié)構(gòu)塑悼,是不是需要再考慮下學(xué)習(xí)率的問(wèn)題呢劲适?大家思考一下?
答案是肯定的厢蒜,我們需要考慮不同的學(xué)習(xí)率來(lái)解決不同結(jié)構(gòu)的問(wèn)題霞势。比如BERT+TextCNN,BERT+BiLSTM+CRF斑鸦,在這種情況下愕贡。
BERT的fine-tune學(xué)習(xí)率可以設(shè)置為5e-5, 3e-5, 2e-5。
而下游任務(wù)結(jié)構(gòu)的學(xué)習(xí)率可以設(shè)置為1e-4巷屿,讓其比bert的學(xué)習(xí)更快一些固以。
至于這么做的原因也很簡(jiǎn)單:BERT本體是已經(jīng)預(yù)訓(xùn)練過(guò)的,即本身就帶有權(quán)重嘱巾,所以用小的學(xué)習(xí)率很容易fine-tune到最優(yōu)點(diǎn)嘴纺,而下接結(jié)構(gòu)是從零開(kāi)始訓(xùn)練,用小的學(xué)習(xí)率訓(xùn)練不僅學(xué)習(xí)慢浓冒,而且也很難與BERT本體訓(xùn)練同步栽渴。
為此,我們將下游任務(wù)網(wǎng)絡(luò)結(jié)構(gòu)的學(xué)習(xí)率調(diào)大稳懒,爭(zhēng)取使兩者在訓(xùn)練結(jié)束的時(shí)候同步:當(dāng)BERT訓(xùn)練充分時(shí)闲擦,下游任務(wù)結(jié)構(gòu)也能夠訓(xùn)練充分。
6.2 weight decay權(quán)重衰減
權(quán)重衰減等價(jià)于L2范數(shù)正則化场梆。正則化通過(guò)為模型損失函數(shù)添加懲罰項(xiàng)使得學(xué)習(xí)的模型參數(shù)值較小墅冷,是常用的過(guò)擬合的常用手段。
權(quán)重衰減并不是所有的權(quán)重參數(shù)都需要衰減或油,比如bias寞忿,和LayerNorm.weight就不需要衰減。
具體實(shí)現(xiàn)可以參照下面部分代碼顶岸。
In [ ]
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
{'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': 1e-2},
{'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
# 對(duì)應(yīng)optimizer_grouped_parameters中的第一個(gè)dict腔彰,這里面的參數(shù)需要權(quán)重衰減
need_decay = []
for n, p in model.named_parameters():
if not any(nd in n for nd in no_decay):
need_decay.append(p)
# 對(duì)應(yīng)optimizer_grouped_parameters中的第二個(gè)dict叫编,這里面的參數(shù)不需要權(quán)重衰減
not_decay = []
for n, p in model.named_parameters():
if any(nd in n for nd in no_decay):
not_decay.append(p)
# AdamW是實(shí)現(xiàn)了權(quán)重衰減的優(yōu)化器
optimizer = AdamW(optimizer_grouped_parameters, lr=1e-5)
criterion = nn.CrossEntropyLoss()
6.3 實(shí)戰(zhàn)中的遷移策略
那拿到一個(gè)BERT預(yù)訓(xùn)練模型后,我們會(huì)有兩種選擇:
- 把BERT當(dāng)做特征提取器或者句向量霹抛,不在下游任務(wù)中微調(diào)搓逾。
- 把BERT做為下游業(yè)務(wù)的主要模型,在下游任務(wù)中微調(diào)杯拐。
具體的使用策略要多加嘗試霞篡,沒(méi)有絕對(duì)的正確。
那如何在代碼中控制BERT是否參與微調(diào)呢端逼?代碼如下:
In [ ]
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
init_checkpoint = config['init_checkpoint']
freeze_bert = config['freeze_bert']
dropout = config['dropout']
self.use_bigru = config['use_bigru']
self.output_hidden_states = config['output_hidden_states']
self.concat_output = config['concat_output']
self.config = config
bert_config = BertConfig.from_pretrained(os.path.join(init_checkpoint, 'bert_config.json'),
output_hidden_states=self.output_hidden_states)
self.model = BertModel.from_pretrained(os.path.join(init_checkpoint, 'pytorch_model.bin'),
config=bert_config)
self.dropout = nn.Dropout(dropout)
# bert是否參與微調(diào)朗兵,可以通過(guò)一下代碼實(shí)現(xiàn)
if freeze_bert:
for p in self.model.parameters():
p.requires_grad = False # 亦可以針對(duì)性的微調(diào)或者凍結(jié)某層參數(shù)
if self.use_bigru:
self.biGRU = torch.nn.GRU(768, 768, num_layers=1, batch_first=True, bidirectional=True)
self.dense = nn.Linear(bert_config.hidden_size * 2, 3) # 連接bigru的輸出層
elif self.concat_output:
self.dense = nn.Linear(bert_config.hidden_size * 3, 3) # 連接concat后的三個(gè)向量
else:
self.dense = nn.Linear(bert_config.hidden_size, 3) # 輸出3維(3分類)
那如果有選擇的進(jìn)行bert某些層的凍結(jié)可以參照以下代碼。
In [ ]
# Freeze parts of pretrained model
# config['freeze'] can be "all" to freeze all layers,
# or any number of prefixes, e.g. ['embeddings', 'encoder']
if 'freeze' in config and config['freeze']:
for name, param in self.base_model.named_parameters():
if config['freeze'] == 'all' or 'all' in config['freeze'] or name.startswith(tuple(config['freeze'])):
param.requires_grad = False
logging.info(f"Froze layer {name}...")
In [ ]
if freeze_embeddings:
for param in list(model.bert.embeddings.parameters()):
param.requires_grad = False
print ("Froze Embedding Layer")
# freeze_layers is a string "1,2,3" representing layer number
if freeze_layers is not "":
layer_indexes = [int(x) for x in freeze_layers.split(",")]
for layer_idx in layer_indexes:
for param in list(model.bert.encoder.layer[layer_idx].parameters()):
param.requires_grad = False
print ("Froze Layer: ", layer_idx)
7 完成你的BERT任務(wù)(作業(yè)在其中)
在做項(xiàng)目前可以執(zhí)行下列語(yǔ)句安裝所需的庫(kù)顶滩。
In [1]
# 也可以在終端里安裝余掖,注意下版本
!pip install transformers==3.4.0
該部分項(xiàng)目采用數(shù)據(jù)集為中文文本分類數(shù)據(jù)集THUCNews。
THUCNews是根據(jù)新浪新聞RSS訂閱頻道2005~2011年間的歷史數(shù)據(jù)篩選過(guò)濾生成诲祸,包含74萬(wàn)篇新聞文檔(2.19 GB)浊吏,均為UTF-8純文本格式而昨。我們?cè)谠夹吕诵侣劮诸愺w系的基礎(chǔ)上救氯,重新整合劃分出14個(gè)候選分類類別:財(cái)經(jīng)、彩票歌憨、房產(chǎn)着憨、股票、家居务嫡、教育甲抖、科技、社會(huì)心铃、時(shí)尚准谚、時(shí)政、體育去扣、星座柱衔、游戲、娛樂(lè)愉棱。
該部分?jǐn)?shù)據(jù)已經(jīng)經(jīng)過(guò)處理唆铐,放在了data/data59734下。如果有想了解原始數(shù)據(jù)的同學(xué)奔滑,可以去官網(wǎng)查詢艾岂。
訓(xùn)練過(guò)程中所需要的預(yù)訓(xùn)練模型在data/data56340下。
ok朋其,到這里我們有關(guān)BERT的課程就基本結(jié)束了王浴,最后留給大家一個(gè)代碼作業(yè)脆炎。
到這里,大家可以啟動(dòng)GPU環(huán)境來(lái)完成作業(yè)了叼耙。
在work/TextClassifier-main中提供了一個(gè)基于bert的baseline腕窥,大家針對(duì)下面要求完成作業(yè)就好。
作業(yè)提交要求:
- 修改baseline筛婉,利用前面課程中提出的任何一種方法(用cnn等改造微調(diào)模型簇爆、調(diào)參、改變遷移策略等等)爽撒,并跑至少4個(gè)epoch入蛆。同時(shí)將print的結(jié)果圖片發(fā)到這里(本文最后我留一行讓大家加圖片)。
- 將你設(shè)計(jì)的方法相關(guān)代碼(或文字說(shuō)明)復(fù)制到我預(yù)留的位置硕勿,方便老師查閱哨毁。
7.1 訓(xùn)練過(guò)程中的注意事項(xiàng)
1.原始數(shù)據(jù)大概要35w條,為了縮短計(jì)算時(shí)間源武,如下如所示扼褪,我將數(shù)據(jù)做了5w條的采樣。大家如果想用全量數(shù)據(jù)試驗(yàn)粱栖,可以自行修改代碼话浇。
2.訓(xùn)練過(guò)程中需要查看GPU使用情況,可以如下圖所示打開(kāi)一個(gè)新的終端闹究,并在終端中執(zhí)行下列代碼幔崖。
In [ ]
watch -n 0.1 -d nvidia-smi
3.下圖就是大家需要提交自己訓(xùn)練結(jié)果的截圖實(shí)例。