69自然語言處理預(yù)訓(xùn)練技術(shù)實踐--BERT 預(yù)訓(xùn)練模型及文本分類

BERT 預(yù)訓(xùn)練模型及文本分類

BERT 全稱為 Bidirectional Encoder Representations from Transformer,是谷歌在 2018 年 10 月發(fā)布的語言表示模型。BERT 通過維基百科和書籍語料組成的龐大語料進(jìn)行了預(yù)訓(xùn)練,使用時只要根據(jù)下游任務(wù)進(jìn)行輸出層的修改和模型微調(diào)訓(xùn)練,就可以得到很好的效果。BERT 發(fā)布之初,就在 GLUE、MultiNLI、SQuAD 等評價基準(zhǔn)和數(shù)據(jù)集上取得了超越當(dāng)時最好成績的結(jié)果。但在深入了解 BERT 結(jié)構(gòu)之前,先需要了解一下什么是語言模型,以及在 BERT 誕生之前人們是如何進(jìn)行文本向量化的。

語言模型和詞向量

語言模型 是用于計算文本序列概率的模型。在自然語言處理的發(fā)展中,應(yīng)用較為廣泛的語言模型有兩種:統(tǒng)計式語言模型和神經(jīng)網(wǎng)絡(luò)語言模型。接下來就將分別介紹一下它們。
統(tǒng)計式語言模型
統(tǒng)計式語言模型(Statistical Language Model)是根據(jù)概率分布,計算字詞所組成的字符串的幾率的模型,簡單來說,統(tǒng)計式語言模型就是計算一句話符不符合語言規(guī)律。比如,使用語言模型計算出「我今天吃了一個蘋果」的概率,一定比「蘋果個我今天吃了一」的概率大,所以前者比后者存在的可能性更大。
在具體構(gòu)建統(tǒng)計式語言模型時,了便于計算,轉(zhuǎn)化為公式 (2):

image.png

但是在實踐中,通常文本的長度較長,所以公式 (2) 的估算會非常困難,因此,研究者們根據(jù) 馬爾可夫鏈?zhǔn)椒▌t 提出了 N 元模型(N-Gram Model)。于是得到公式(3):
image.png

在 N 元模型中,一般采用字詞的出現(xiàn)頻率來估計 N 元條件概率??梢韵胂瘢?dāng) N 值非常大時,計算頻率時會存在數(shù)據(jù)稀疏問題,所以 N 的取值一般為 3 。統(tǒng)計式語言模型可以使用在許多自然語言處理方面的應(yīng)用上,如語音識別、機器翻譯、詞性標(biāo)注、句法分析和資訊檢索。不過統(tǒng)計式語言模型也有其局限性,IBM 曾進(jìn)行過一次信息檢索評測,發(fā)現(xiàn)二元語言模型需要數(shù)以億計的詞匯才能達(dá)到最優(yōu)表現(xiàn),而三元語言模型則需要數(shù)十億級別的詞匯。
神經(jīng)網(wǎng)絡(luò)語言模型
近年來隨著深度學(xué)習(xí)的發(fā)展,研究者們設(shè)計出了基于神經(jīng)網(wǎng)絡(luò)的語言模型,神經(jīng)網(wǎng)絡(luò)語言模型(Nerual Network Language Model)的結(jié)構(gòu)如下圖所示:
image.png

現(xiàn)在來根據(jù)上圖分析一下模型的結(jié)構(gòu)。首先,最下層的是輸入模型的語句所對應(yīng)的在詞表中的編號,然后通過編號在查找表(Look-Up Table)中找到對應(yīng)的詞向量。接下來輸入全連接層,激活函數(shù)為 tanh,將這一層的輸出與原詞向量結(jié)合輸入到最后的全連接層中。最后使用 Softmax 計算在前文語境中下一個詞的預(yù)測結(jié)果。
可以看到,神經(jīng)網(wǎng)絡(luò)語言模型主要是使用了全連接層與激活函數(shù)代替了統(tǒng)計語言模型的概率計算。在神經(jīng)網(wǎng)絡(luò)中有一個副產(chǎn)品:第二個全連接層參數(shù),它就是我們接下來要講到的詞向量。
詞向量
受到神經(jīng)網(wǎng)絡(luò)語言模型的啟發(fā),研究者們發(fā)現(xiàn)了進(jìn)行分布式表示詞向量的方法,即通過一個詞的上下文語境來表示這個詞的含義。并且,對比生成基于稀疏表示(Sparse Representation)的詞向量的統(tǒng)計方法,例如詞袋模型,神經(jīng)網(wǎng)絡(luò)生成的分布式表示(Distributed Representation)詞向量獲得了更好的效果。
但這個方法也存在弊端,即無法表示多義詞,并且這個多義詞的含義會受到訓(xùn)練語料的影響。例如,「蘋果」這個詞,在「我愛吃蘋果」和「我喜歡蘋果公司」中表示的含義是不同的。但如果在訓(xùn)練語料中,大量的語料表示的是蘋果的水果的語義,那訓(xùn)練出的詞向量中的「蘋果」則會包含水果的語義,結(jié)果就會導(dǎo)致模型出現(xiàn)理解偏差。
為了解決這個問題,出現(xiàn)了基于上下文的表示(Contextualized Representation)生成詞向量的語言模型,這類模型在對句子進(jìn)行編碼時會結(jié)合每個詞所在語句的前后文語境,這種基于上下文的詞向量就成功解決了區(qū)分多義詞的問題。BERT 就是這樣一種生成基于上下文表示詞向量的語言模型,接下來了解一下 BERT 的具體結(jié)構(gòu)。

BERT 結(jié)構(gòu)詳解

BERT 的整體結(jié)構(gòu)如下圖所示,其是以 Transformer 為基礎(chǔ)構(gòu)建的,使用 WordPiece 的方法進(jìn)行數(shù)據(jù)預(yù)處理,最后通過 MLM 任務(wù)和下個句子預(yù)測任務(wù)進(jìn)行預(yù)訓(xùn)練的語言表示模型。下面我們從 BERT 的結(jié)構(gòu):Transformer 出發(fā),來一步步詳細(xì)解析一下 BERT。

image.png

Transformer
首先介紹 BERT 模型結(jié)構(gòu)的基礎(chǔ):Transformer。Transformer 是一個完全基于注意力機制(Attention mechanism)的模塊,對比 RNN(Recurrent Neural Network),當(dāng)輸入的句子是長句子時,RNN 可能會遺忘之前句子中出現(xiàn)的字詞,而 Transformer 的注意力機制使得句子中重要的字詞的權(quán)重增大,從而保證不會被遺忘。并且 Transformer 另一個巨大的優(yōu)勢在于,它可以使用并行的方法運行計算,從而加快了速度。Transformer 的具體結(jié)構(gòu)如下圖:
image.png

從上圖我們可以看到 Transformer 的內(nèi)部結(jié)構(gòu)為:輸入的 inputs 要經(jīng)過 Input Embedding 模塊進(jìn)行向量化,然后加上對其的 Positional Encoding,然后數(shù)據(jù)向上進(jìn)入由 Multi-Head Attention,Add & Norm,F(xiàn)eed Forward 以及又一個 Add & Norm 構(gòu)成的 N 個整體之中。
其中的 Multi-Head Attention 最為關(guān)鍵,在開始介紹 Transformer 的 Multi-Head Attention 機制之前,我們先簡單說一下 Positional Encoding。
Positional Encoding,從字面意思來講是位置編碼,就是用來表示輸入句子向量中每個字詞所對應(yīng)的位置。由于 Tranformer 的結(jié)構(gòu)不同,無法像 RNN 一樣獲取句子的時序信息,所以需要使用 Positional Encoding 表示字詞在句子中的先后順序。一種常見的計算方式是使用正弦函數(shù)和余弦函數(shù)來構(gòu)造每個位置的值,后來的研究發(fā)現(xiàn)通過可訓(xùn)練的參數(shù)來實現(xiàn)的也能夠達(dá)到同樣的效果,BERT 模型中就是通過可訓(xùn)練參數(shù)的方法來實現(xiàn)的。
現(xiàn)在來介紹 Transformer 結(jié)構(gòu)的重點:Multi-Head Attention。Multi-Head Attention 的組成因子是 Self-Attention,顧名思義,Self-Attention 就是自注意力,即語句對自身計算注意力權(quán)重。公式表示為:
image.png

該流程也可以參考下圖:
image.png

可以看出,BERT 的 Bidirection 特性就在 Self-Attention 機制中得到了體現(xiàn),即計算句子中的注意力對某個詞的分布時,既考慮了在該詞左側(cè)的詞,也考慮了在該詞右側(cè)的詞。
現(xiàn)在我們已經(jīng)了解了 Self-Attention,Multi-Heah Attention 實際上就是多個 Self-Attention 的堆疊。如下圖,多層疊加的 Self-Attention 組成了 Multi-Head Attention。不過因為多層的緣故,最后所有 Self-Attention 會生成多個大小相同的矩陣,處理方式是把這些矩陣拼接起來,然后通過乘上一個參數(shù)矩陣得到最后的計算結(jié)果。
image.png

Multi-Head Attention 通過多層的 Self-Attention 可以將輸入語句映射到不同的子空間中,于是能夠更好地理解到語句所包含的信息。
下面引入一段 BERT 模型對 Self-Attention 的實現(xiàn)代碼片段:

# 取自 hugging face 團隊實現(xiàn)的基于 pytorch 的 BERT 模型
class BERTSelfAttention(nn.Module):
    # BERT 的 Self-Attention 類
    def __init__(self, config):
        # 初始化函數(shù)
        super(BERTSelfAttention, self).__init__()
        if config.hidden_size % config.num_attention_heads != 0:
            raise ValueError(
                "The hidden size (%d) is not a multiple of the number of attention "
                "heads (%d)" % (config.hidden_size, config.num_attention_heads))
        self.num_attention_heads = config.num_attention_heads
        self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
        self.all_head_size = self.num_attention_heads * self.attention_head_size

        self.query = nn.Linear(config.hidden_size, self.all_head_size)
        self.key = nn.Linear(config.hidden_size, self.all_head_size)
        self.value = nn.Linear(config.hidden_size, self.all_head_size)

    def transpose_for_scores(self, x):
        # 調(diào)整維度,轉(zhuǎn)換為 (batch_size, num_attention_heads, hidden_size, attention_head_size)
        new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size)
        x = x.view(*new_x_shape)
        return x.permute(0, 2, 1, 3)

    def forward(self, hidden_states):
        # 前向傳播函數(shù)
        mixed_query_layer = self.query(hidden_states)
        mixed_key_layer = self.key(hidden_states)
        mixed_value_layer = self.value(hidden_states)

        query_layer = self.transpose_for_scores(mixed_query_layer) 
        key_layer = self.transpose_for_scores(mixed_key_layer)
        value_layer = self.transpose_for_scores(mixed_value_layer)

        # 將"query"和"key"點乘,得到未經(jīng)處理注意力值
        attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
        attention_scores = attention_scores / math.sqrt(self.attention_head_size)

        # 使用 softmax 函數(shù)將注意力值標(biāo)準(zhǔn)化成概率值
        attention_probs = nn.Softmax(dim=-1)(attention_scores)

        context_layer = torch.matmul(attention_probs, value_layer)
        context_layer = context_layer.permute(0, 2, 1, 3).contiguous()
        new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,)
        context_layer = context_layer.view(*new_context_layer_shape)
        return context_layer

參照之前的 Transformer 的結(jié)構(gòu),在 Multi-Head Attention 之后是 Add & Norm,將經(jīng)過注意力機制計算后的向量和原輸入相加并歸一化,進(jìn)入 Feed Forward Neural Network,然后再進(jìn)行一次和輸入的相加并完成歸一化。
分詞方法 WordPiece
前面介紹了 BERT 的具體結(jié)構(gòu),下面介紹 BERT 使用的數(shù)據(jù)預(yù)處理方法,數(shù)據(jù)預(yù)處理對于模型的訓(xùn)練十分重要,關(guān)系到模型的訓(xùn)練效率和準(zhǔn)確率的提升。BERT 在對數(shù)據(jù)預(yù)處理時,使用了 WordPiece 的方法,WordPiece 從字面意思理解就是把字詞拆成一片一片的。
舉個例子來講,如 look,looked,looking 這三個詞,它們其實有同樣的意思,但如果我們以詞作為單位,那它們就會被認(rèn)為成不同的詞。在英語中這樣的情況十分常見,所以為了解決這個問題,WordPiece 會把這三個詞拆分成 look,look 和 ##ed,look 和 ##ing,這個方法把詞本身和時態(tài)表示拆分開,不但能夠有效減少詞表的大小,提高效率,還能夠提升詞的區(qū)分度。
不過,這個方法對中文是無效的,因為在中文中每個字都是最小的單位,不像英文使用空格分詞,并且許多詞還能夠進(jìn)一步拆分,所以對中文使用 WordPiece 就相當(dāng)于按字分割,這也是 BERT 的中文預(yù)訓(xùn)練模型的一個局限。因此,盡管 BERT 中文預(yù)訓(xùn)練模型效果很好,但也還存在可以改進(jìn)的空間。有一些研究者就從這個角度出發(fā)對中文 BERT 進(jìn)行了改進(jìn),如這篇論文:中文全詞覆蓋 BERT ,研究者在預(yù)訓(xùn)練的數(shù)據(jù)處理過程中將原 BERT 中 WordPiece 的分詞方法換成了中文分詞的方法,然后對詞整體添加掩膜,最后進(jìn)行預(yù)訓(xùn)練。在中文數(shù)據(jù)集測試上,使用這個改進(jìn)后的預(yù)訓(xùn)練模型的測試結(jié)果優(yōu)于使用原版 BERT 的中文預(yù)訓(xùn)練模型的測試結(jié)果。

BERT 預(yù)訓(xùn)練模型

上面介紹了 BERT 的結(jié)構(gòu)和 BERT 進(jìn)行數(shù)據(jù)預(yù)處理使用的 WordPiece 方法,接下來,我們將要介紹 BERT 在預(yù)訓(xùn)練階段的兩個任務(wù):遮蔽語言模型和句子預(yù)測任務(wù)。也正是這兩個任務(wù),使得 BERT 學(xué)到了對自然語言的理解。
遮蔽語言模型
與常見的訓(xùn)練從左向右語言模型(Left-To-Right Language Model)的預(yù)訓(xùn)練任務(wù)不同,BERT 是以訓(xùn)練遮蔽語言模型(Masked Language Model)作為的預(yù)訓(xùn)練目標(biāo),具體來說就是把輸入的語句中的字詞隨機用 [Mask] 標(biāo)簽覆蓋,然后訓(xùn)練模型結(jié)合被覆蓋的詞的左側(cè)和右側(cè)上下文進(jìn)行預(yù)測。可以看出,BERT 的做法與從左向右語言模型只通過左側(cè)語句預(yù)測下一個詞的做法相比,遮蔽語言模型能夠生成同時融合了左、右上下文的語言表示。這種做法能夠使 BERT 學(xué)到字詞更完整的語義表示。

BERT 的論文中提到,增加掩膜的具體方式為:先對語句進(jìn)行 WordPiece 分割,分割后選擇句中 15% 的字符,例如選擇到了第i字符,接下來:
以80 的概率使用 [Mask] 替換。
以10 的概率使用一個隨機的字符替換。
以 10 的概率不進(jìn)行操作。

下面我們使用在 PyTorch-Transformers 模型庫中封裝好的 BERTForMaskedLM() 類來實際看一下 BERT 在預(yù)訓(xùn)練后對遮蔽字的預(yù)測效果。首先,需要安裝 PyTorch-Transformers。

!pip install pytorch-transformers==1.0  # 安裝 PyTorch-Transformers

PyTorch-Transformers 是一個以 PyTorch 深度學(xué)習(xí)框架為基礎(chǔ)構(gòu)建的自然語言處理預(yù)訓(xùn)練模型庫,早前稱之為 pytorch-pretrained-bert,如果已正式成為獨立項目。
使用 PyTorch-Transformers 模型庫,先設(shè)置好準(zhǔn)備輸入模型的例子,使用 BertTokenizer() 建立分詞器對象對原句進(jìn)行分詞,然后對照詞表將詞轉(zhuǎn)換成序號。

import torch
from pytorch_transformers import BertTokenizer

model_name = 'bert-base-chinese'  # 指定實驗需下載的預(yù)訓(xùn)練模型參數(shù)

# BERT 在預(yù)訓(xùn)練中引入了 [CLS] 和 [SEP] 標(biāo)記句子的開頭和結(jié)尾
samples = ['[CLS] 中國的首都是哪里? [SEP] 北京是 [MASK] 國的首都。 [SEP]']  # 準(zhǔn)備輸入模型的語句

tokenizer = BertTokenizer.from_pretrained(model_name)
tokenized_text = [tokenizer.tokenize(i) for i in samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_ids = torch.LongTensor(input_ids)
input_ids

接下來使用 BertForMaskedLM() 建立模型,并將模型設(shè)置模型成驗證模式。由于 BERT 模型體積很大,且托管在外網(wǎng),所以本次實驗先從藍(lán)橋云課鏡像服務(wù)器下載預(yù)訓(xùn)練模型,本地實驗無需此步驟。

!wget -nc "https://labfile.oss.aliyuncs.com/courses/1372/bert-base-chinese-shiyanlou.zip"
!unzip -o "bert-base-chinese-shiyanlou.zip"
from pytorch_transformers import BertForMaskedLM

# 讀取預(yù)訓(xùn)練模型
model = BertForMaskedLM.from_pretrained(model_name, cache_dir="./")
model.eval()

此時,我們已經(jīng)準(zhǔn)備好了待輸入的語句和預(yù)訓(xùn)練模型,接下來需要做的就是讓模型去預(yù)測的覆蓋的詞的序號。

outputs = model(input_ids)
prediction_scores = outputs[0]
prediction_scores.shape

最后找到預(yù)測值中最大值對應(yīng)的序號,然后通過 tokenizer.convert_ids_to_tokens() 在詞表中查找,轉(zhuǎn)換成對應(yīng)的字。

import numpy as np

sample = prediction_scores[0].detach().numpy()
pred = np.argmax(sample, axis=1)

tokenizer.convert_ids_to_tokens(pred)[14]

輸出結(jié)果應(yīng)該是:中
可以看到,最后的預(yù)測結(jié)果是正確的的,說明 BERT 真的對語言有了理解。
句子預(yù)測任務(wù)
預(yù)訓(xùn)練 BERT 時除了 MLM 預(yù)訓(xùn)練策略,還要進(jìn)行預(yù)測下一個句子的任務(wù)。句子預(yù)測任務(wù)基于理解兩個句子間的關(guān)系,這種關(guān)系無法直接被 Masked Language Model 捕捉到。訓(xùn)練數(shù)據(jù)的構(gòu)成是由語料庫中的句子組成句子對,詳細(xì)地說,當(dāng)選擇兩個相鄰句子 A 和 B 組成預(yù)訓(xùn)練樣本時,有 50% 的幾率使句子 A 在句子 B 之前,50% 的幾率使句子 B 在句子 A 之前。盡管這個方法并不復(fù)雜,但是這個預(yù)訓(xùn)練對于問答任務(wù)和自然語言推理任務(wù)等下游任務(wù)有很好的幫助。
下面我們使用 PyTorch-Transformers 庫中的句子預(yù)測模型進(jìn)行實驗,觀察一下輸出結(jié)果。
首先構(gòu)造輸入樣本,然后進(jìn)行分詞和詞向序號的轉(zhuǎn)換。

samples = ["[CLS]今天天氣怎么樣?[SEP]今天天氣很好。[SEP]", "[CLS]小明今年幾歲了?[SEP]小明愛吃西瓜。[SEP]"]
tokenizer = BertTokenizer.from_pretrained(model_name)
tokenized_text = [tokenizer.tokenize(i) for i in samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_ids = torch.LongTensor(input_ids)
input_ids

構(gòu)造句子的分段 id,按照上下句分別標(biāo)為 0 和 1。

segments_ids = [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1],
                [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]]

segments_tensors = torch.tensor(segments_ids)
segments_tensors

接下來使用 BertForNextSentencePrediction() 初始化模型,再加載 BERT 的預(yù)訓(xùn)練參數(shù)。

from pytorch_transformers import BertForNextSentencePrediction

model = BertForNextSentencePrediction.from_pretrained(
    model_name, cache_dir="./")
model.eval()

最后將樣本輸入模型進(jìn)行預(yù)測,輸出模型的預(yù)測結(jié)果。

outputs = model(input_ids)
seq_relationship_scores = outputs[0]
seq_relationship_scores

sample = seq_relationship_scores.detach().numpy()
pred = np.argmax(sample, axis=1)
pred

最終的輸出結(jié)果應(yīng)該是:[0, 1]。
0 表示是上下句關(guān)系,1 表示不是上下句關(guān)系。因此從上面結(jié)果可以看到,模型預(yù)測第一個句子對是上下句關(guān)系,第二個句子對不是,對于這兩個樣本 BERT 的預(yù)測正確。

我們通過兩個例子來看 BERT 的效果,都是非常理想的。實際上,BERT 效果好的原因主要有兩點:
使用的雙向的 Transformer 結(jié)構(gòu)學(xué)習(xí)到左、右兩側(cè)上下文語境。
使用完整的文檔語料訓(xùn)練而不是打亂的句子,配合下個句子預(yù)測任務(wù),從而學(xué)習(xí)到了捕捉很長的連續(xù)語句中的信息的能力。

BERT 文本分類實踐

上面,我們使用 BERT 完成了兩個小例子。接下來,實驗嘗試?yán)?BERT 完成文本分類任務(wù)。實際上,當(dāng)使用 BERT 完成文本分類時,通常有 2 種方案:
從預(yù)訓(xùn)練好的 BERT 模型中提取特征向量,即 Feature Extraction 方法。
將下游任務(wù)模型添加到 BERT 模型之后,然后使用下游任務(wù)的訓(xùn)練集對進(jìn)行訓(xùn)練,即 Fine-Tuning 方法。

通常 Fine-Tuning 方法更常被人們使用,因為通過結(jié)合下游任務(wù)的數(shù)據(jù)集進(jìn)行微調(diào)從而調(diào)整預(yù)訓(xùn)練模型參數(shù),使模型能夠更好捕捉到下游任務(wù)的數(shù)據(jù)特征。下面使用 Fine-Tuning 方法應(yīng)用 BERT 預(yù)訓(xùn)練模型進(jìn)行情感分類任務(wù)。
首先,下載一個 情感分類數(shù)據(jù)集,我們已經(jīng)提前下載好并放在藍(lán)橋云課服務(wù)器中,可通過以下命令讀取。

!wget -nc "https://labfile.oss.aliyuncs.com/courses/1372/clothing_comment.zip"
!unzip -o "clothing_comment.zip"

下載好數(shù)據(jù)集后,讀取數(shù)據(jù)文件。

with open('./negdata.txt', 'r', encoding='utf-8') as f:
    neg_data = f.read()
with open('./posdata.txt', 'r', encoding='utf-8') as f:
    pos_data = f.read()

neg_datalist = neg_data.split('\n')
pos_datalist = pos_data.split('\n')
len(neg_datalist), len(pos_datalist)

在讀取到數(shù)據(jù)后,我們將將數(shù)據(jù)存到一個列表中,并構(gòu)建標(biāo)簽列表,用 1 表示正面的評論,用 0 表示負(fù)面的評論。

import numpy as np

dataset = np.array(pos_datalist + neg_datalist)
labels = np.array([1] * len(pos_datalist) + [0] * len(neg_datalist))
len(dataset)  # 共 3000 條數(shù)據(jù)

利用 NumPy 庫使樣本數(shù)據(jù)隨機排列。

np.random.seed(10)
mix_index = np.random.choice(3000, 3000)
dataset = dataset[mix_index]
labels = labels[mix_index]
len(dataset), len(labels)

然后以取 2500 條數(shù)據(jù)作為訓(xùn)練集,取 500 條數(shù)據(jù)作為驗證集。

TRAINSET_SIZE = 2500
EVALSET_SIZE = 500

train_samples = dataset[:TRAINSET_SIZE]  # 2500 條數(shù)據(jù)
train_labels = labels[:TRAINSET_SIZE]
eval_samples = dataset[TRAINSET_SIZE:TRAINSET_SIZE+EVALSET_SIZE]  # 500 條數(shù)據(jù)
eval_labels = labels[TRAINSET_SIZE:TRAINSET_SIZE+EVALSET_SIZE]

len(train_samples), len(eval_samples)

構(gòu)建函數(shù) get_dummies ,作用是把標(biāo)簽轉(zhuǎn)換成 one-hot 的表示形式,例如將 1 表示成 [0, 1],0 表示成 [1, 0] 的形式。

def get_dummies(l, size=2):
    res = list()
    for i in l:
        tmp = [0] * size
        tmp[i] = 1
        res.append(tmp)
    return res

這里使用 PyTorch 提供的 DataLoader() 構(gòu)建訓(xùn)練集數(shù)據(jù)集表示,使用 TensorDataset() 構(gòu)建訓(xùn)練集數(shù)據(jù)迭代器。

from torch.utils.data import DataLoader, TensorDataset

tokenized_text = [tokenizer.tokenize(i) for i in train_samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_labels = get_dummies(train_labels)  # 使用 get_dummies 函數(shù)轉(zhuǎn)換標(biāo)簽

for j in range(len(input_ids)):
    # 將樣本數(shù)據(jù)填充至長度為 512
    i = input_ids[j]
    if len(i) != 512:
        input_ids[j].extend([0]*(512 - len(i)))

# 構(gòu)建數(shù)據(jù)集和數(shù)據(jù)迭代器,設(shè)定 batch_size 大小為 4
train_set = TensorDataset(torch.LongTensor(input_ids),
                          torch.FloatTensor(input_labels))
train_loader = DataLoader(dataset=train_set,
                          batch_size=4,
                          shuffle=True)
train_loader

與構(gòu)建訓(xùn)練集數(shù)據(jù)迭代器類似,構(gòu)建驗證集的數(shù)據(jù)迭代器。

tokenized_text = [tokenizer.tokenize(i) for i in eval_samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_labels = eval_labels

for j in range(len(input_ids)):
    i = input_ids[j]
    if len(i) != 512:
        input_ids[j].extend([0]*(512 - len(i)))

eval_set = TensorDataset(torch.LongTensor(input_ids),
                         torch.FloatTensor(input_labels))
eval_loader = DataLoader(dataset=eval_set,
                         batch_size=1,
                         shuffle=True)
eval_loader

檢查是否機器有 GPU,如果有就在 GPU 運行,否則就在 CPU 運行。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

構(gòu)建一個用于分類的類,加入 BERT 模型,在 BERT 模型下加入一個 Dropout 層用于防止過擬合,和一個 Linear 全連接層。

import torch.nn as nn
import torch.nn.functional as F
from pytorch_transformers import BertModel


class fn_cls(nn.Module):
    def __init__(self):
        super(fn_cls, self).__init__()
        self.model = BertModel.from_pretrained(model_name, cache_dir="./")
        self.model.to(device)
        self.dropout = nn.Dropout(0.1)
        self.l1 = nn.Linear(768, 2)

    def forward(self, x, attention_mask=None):
        outputs = self.model(x, attention_mask=attention_mask)
        x = outputs[1]  # 取池化后的結(jié)果 batch * 768
        x = x.view(-1, 768)
        x = self.dropout(x)
        x = self.l1(x)
        return x

定義損失函數(shù),建立優(yōu)化器。

from torch import optim

cls = fn_cls()
cls.to(device)
cls.train()

criterion = nn.BCELoss()
sigmoid = nn.Sigmoid()
optimizer = optim.Adam(cls.parameters(), lr=1e-5)

構(gòu)建預(yù)測函數(shù),用于計算預(yù)測結(jié)果。

def predict(logits):
    res = torch.argmax(logits, 1)
    return res

構(gòu)建訓(xùn)練函數(shù)并開始訓(xùn)練。這里需要說一下,因為 GPU 內(nèi)存的限制,訓(xùn)練集的 batch_size 設(shè)為了 4,這樣的 batch_size 過小,使得梯度下降方向不準(zhǔn),引起震蕩,難以收斂。所以,在訓(xùn)練時使用了梯度積累的方法,即計算 8 個小批次的梯度的平均值來更新模型,從而達(dá)到了 32 個小批次的效果。

from torch.autograd import Variable
import time

pre = time.time()

accumulation_steps = 8
epoch = 3

for i in range(epoch):
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = Variable(data).to(device), Variable(
            target.view(-1, 2)).to(device)

        mask = []
        for sample in data:
            mask.append([1 if i != 0 else 0 for i in sample])
        mask = torch.Tensor(mask).to(device)
        
        output = cls(data, attention_mask=mask)
        pred = predict(output)

        loss = criterion(sigmoid(output).view(-1, 2), target)

        # 梯度積累
        loss = loss/accumulation_steps
        loss.backward()

        if((batch_idx+1) % accumulation_steps) == 0:
            # 每 8 次更新一下網(wǎng)絡(luò)中的參數(shù)
            optimizer.step()
            optimizer.zero_grad()

        if ((batch_idx+1) % accumulation_steps) == 1:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss:{:.6f}'.format(
                i+1, batch_idx, len(train_loader), 100. *
                batch_idx/len(train_loader), loss.item()
            ))
        if batch_idx == len(train_loader)-1:
            # 在每個 Epoch 的最后輸出一下結(jié)果
            print('labels:', target)
            print('pred:', pred)

print('訓(xùn)練時間:', time.time()-pre)

訓(xùn)練結(jié)束后,可以使用驗證集觀察模型的訓(xùn)練效果。

from tqdm.notebook import tqdm

cls.eval()

correct = 0
total = 0

for batch_idx, (data, target) in enumerate(tqdm(eval_loader)):
    data = data.to(device)
    target = target.long().to(device)

    mask = []
    for sample in data:
        mask.append([1 if i != 0 else 0 for i in sample])
    mask = torch.Tensor(mask).to(device)

    output = cls(data, attention_mask=mask)
    pred = predict(output)

    correct += (pred == target).sum().item()
    total += len(data)

# 準(zhǔn)確率應(yīng)該達(dá)到百分之 90 以上
print('正確分類的樣本數(shù):{},樣本總數(shù):{},準(zhǔn)確率:{:.2f}%'.format(
    correct, total, 100.*correct/total))

訓(xùn)練結(jié)束后,還可以隨意輸入一些數(shù)據(jù),直接觀察模型的預(yù)測結(jié)果。

test_samples = ['東西很好,好評!', '東西不好,差評!']

cls.eval()
tokenized_text = [tokenizer.tokenize(i) for i in test_samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_ids = torch.LongTensor(input_ids).cuda()

mask = torch.ones_like(input_ids).to(device)

output = cls(input_ids, attention_mask=mask)
pred = predict(output)
pred

Kaggle 電影評論情感分析

挑戰(zhàn)說明

本次挑戰(zhàn)使用 Kaggle 平臺上的 電影評論情感分析 比賽作為數(shù)據(jù)集,數(shù)據(jù)集中有五種情感標(biāo)簽:0 - negative, 1 - somewhat negative, 2 - neutral, 3 - somewhat positive, 4 - positive.
題目:請閱讀電影評論情感分析比賽說明,并使用 PyTorch-Transformers 模型庫中封裝好的 BERT 預(yù)訓(xùn)練模型完成該比賽,最后嘗試通過 Kaggle 提交獲取排名成績。
你可以下載數(shù)據(jù)集在本地完成,也可以使用 Kaggle 提供的 Kaggle Notebook 環(huán)境在線完成。最后,通過比賽頁面的右上角的 Submission Predictions 提交挑戰(zhàn)結(jié)果后即可看到排名信息。
本地練習(xí)時,數(shù)據(jù)集下載地址:

# 數(shù)據(jù)版權(quán)歸 Kaggle 及原作者所有,復(fù)制鏈接粘貼到瀏覽器下載
https://labfile.oss.aliyuncs.com/courses/1372/sentiment-analysis-on-movie-reviews.zip  

數(shù)據(jù)集包含 3 個文件,釋義如下:

├── sample_submission.csv  # 預(yù)測數(shù)據(jù)提交示例格式
├── test.tsv  # 比賽需預(yù)測數(shù)據(jù)集
└── train.tsv  # 比賽用訓(xùn)練數(shù)據(jù)集
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容