學(xué)習(xí)2

# 查看當(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

image

上圖是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)的輸出担钮。

image

在使用代碼上就要考慮到底是使用第一種輸出橱赠,還是第二種了。大部分情況是是會(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

image

我們?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的含義了吧诚些。

image

在實(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ù)擒权。

image

那隨機(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ù)樣本。

image

從上圖可以看出耻涛,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)
image

4.2 文本分類任務(wù)

我們先看看文本分類任務(wù)的基本微調(diào)操作。如下圖所示吱殉,最基本的做法就是將預(yù)訓(xùn)練的BERT讀取進(jìn)來(lái)掸冤,同時(shí)在[CLS]的輸出基礎(chǔ)上加上一個(gè)全連接層,全連接層的輸出維度就是分類的類別數(shù)友雳。

image

從代碼實(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)如下圖所示萎坷。

image

雖然文本匹配問(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)圖兄一。

image

理解序列標(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ì)的,如下圖所示挤聘。

image

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é)合。

image

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ì)好一些淹接。

image

不過(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ì)有兩種選擇:

  1. 把BERT當(dāng)做特征提取器或者句向量霹抛,不在下游任務(wù)中微調(diào)搓逾。
  2. 把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è)提交要求:

  1. 修改baseline筛婉,利用前面課程中提出的任何一種方法(用cnn等改造微調(diào)模型簇爆、調(diào)參、改變遷移策略等等)爽撒,并跑至少4個(gè)epoch入蛆。同時(shí)將print的結(jié)果圖片發(fā)到這里(本文最后我留一行讓大家加圖片)。
  2. 將你設(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)粱栖,可以自行修改代碼话浇。

image

2.訓(xùn)練過(guò)程中需要查看GPU使用情況,可以如下圖所示打開(kāi)一個(gè)新的終端闹究,并在終端中執(zhí)行下列代碼幔崖。

In [ ]

watch -n 0.1 -d nvidia-smi
image

3.下圖就是大家需要提交自己訓(xùn)練結(jié)果的截圖實(shí)例。

image
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末渣淤,一起剝皮案震驚了整個(gè)濱河市赏寇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌价认,老刑警劉巖嗅定,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異用踩,居然都是意外死亡渠退,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)捶箱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)智什,“玉大人,你說(shuō)我怎么就攤上這事丁屎≤В” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵晨川,是天一觀的道長(zhǎng)证九。 經(jīng)常有香客問(wèn)我删豺,道長(zhǎng),這世上最難降的妖魔是什么愧怜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任呀页,我火速辦了婚禮,結(jié)果婚禮上拥坛,老公的妹妹穿的比我還像新娘蓬蝶。我一直安慰自己,他們只是感情好猜惋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布丸氛。 她就那樣靜靜地躺著,像睡著了一般著摔。 火紅的嫁衣襯著肌膚如雪缓窜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天谍咆,我揣著相機(jī)與錄音禾锤,去河邊找鬼。 笑死摹察,一個(gè)胖子當(dāng)著我的面吹牛恩掷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播港粱,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼螃成,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼旦签!你這毒婦竟也來(lái)了查坪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宁炫,失蹤者是張志新(化名)和其女友劉穎偿曙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體羔巢,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡望忆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了竿秆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片启摄。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖幽钢,靈堂內(nèi)的尸體忽然破棺而出歉备,到底是詐尸還是另有隱情,我是刑警寧澤匪燕,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布蕾羊,位于F島的核電站喧笔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏龟再。R本人自食惡果不足惜书闸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望利凑。 院中可真熱鬧浆劲,春花似錦、人聲如沸哀澈。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)日丹。三九已至走哺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間哲虾,已是汗流浹背丙躏。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留束凑,地道東北人晒旅。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像汪诉,于是被迫代替她去往敵國(guó)和親废恋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容