在這篇文章中,我們將使用來自 Hugging Face 的預(yù)訓(xùn)練 BERT 模型進(jìn)行文本分類任務(wù)。一般而言,文本分類任務(wù)中模型的主要目標(biāo)是將文本分類為預(yù)定義的標(biāo)簽或標(biāo)簽之一。 本文中,我們使用 BBC 新聞分類數(shù)據(jù)集,使用預(yù)訓(xùn)練的 BERT 模型來分類新聞文章的文本是否可以分類為體育、政治、商業(yè)、娛樂或科技類別。 BERT 是 Bidirectional Encoder Representations from Transformers 的首字母縮寫詞。 BERT 架構(gòu)由多個(gè)堆疊在一起的 Transformer 編碼器組成。每個(gè) Transformer 編碼器都封裝了兩個(gè)子層:一個(gè)自注意力層和一個(gè)前饋層。 有兩種不同的 BERT 模型: BERT 是一個(gè)強(qiáng)大的語言模型至少有兩個(gè)原因: BERT 模型需要一系列 tokens (words) 作為輸入。在每個(gè)token序列中,BERT 期望輸入有兩個(gè)特殊標(biāo)記: 為什么選它們( 具體來說,self-attention 是用文本中的其它詞來增強(qiáng)目標(biāo)詞的語義表示,但是目標(biāo)詞本身的語義還是會(huì)占主要部分的,因此,經(jīng)過BERT的12層,每次詞的 Embedding 融合了所有詞的信息,可以去更好的表示自己的語義。 而 就像Transformer的普通編碼器一樣,BERT 將一系列單詞作為輸入,這些單詞不斷向上流動(dòng)。每一層都應(yīng)用自我注意,并將其結(jié)果通過前饋網(wǎng)絡(luò)傳遞,然后將其傳遞給下一個(gè)編碼器。 舉個(gè)簡單的例子以更清楚說明,假設(shè)我們有一個(gè)包含以下短句的文本: 第一步,需要將這個(gè)句子轉(zhuǎn)換為一系列tokens (words) ,這個(gè)過程稱為 雖然已經(jīng)對輸入句子進(jìn)行了標(biāo)記,但還需要再做一步。在將其用作 BERT 模型的輸入之前,我們需要通過添加 [CLS] 和 [SEP] 標(biāo)記來對 tokens 的 sequence 重新編碼。 其實(shí)我們只需要一行代碼(即使用 還需要注意的是,可以輸入 BERT 模型的最大tokens大小為 512。如果sequence中的tokens小于 512,我們可以使用填充來用 [PAD] 填充未使用的tokens。如果sequence中的tokens長于 512,那么需要進(jìn)行截?cái)唷?/p> 每個(gè)位置輸出一個(gè)大小為 該向量現(xiàn)在可以用作我們選擇的分類器的輸入。該論文僅使用單層神經(jīng)網(wǎng)絡(luò)作為分類器就取得了很好的效果。 如果有更多標(biāo)簽,只需調(diào)整分類器網(wǎng)絡(luò)以獲得更多輸出神經(jīng)元然后通過softmax輸出多標(biāo)簽分類。 本文的主題是用 BERT 對文本進(jìn)行分類。在這篇文章中,我們將使用kaggle上的 數(shù)據(jù)集已經(jīng)是 CSV 格式,它有 2126 個(gè)不同的文本,每個(gè)文本都標(biāo)記在 5 個(gè)類別中的一個(gè)下: 看一下數(shù)據(jù)集的樣子: 如上表所示,數(shù)據(jù)框只有兩列, BERT 預(yù)訓(xùn)練模型的下載有許多方式,比如從github官網(wǎng)上下載(官網(wǎng)下載的是tensorflow版本的),還可以從源碼中找到下載鏈接,然后手動(dòng)下載,最后還可以從huggingface中下載。 在搜索框搜索到你需要的模型。 來到下載頁面: 注意,這里常用的幾個(gè)預(yù)訓(xùn)練模型,bert-base-cased、bert-base-uncased及中文bert-base-chinese。其中前兩個(gè)容易混淆。bert-base-cased是區(qū)分大小寫,不需要事先lower-case;而bert-base-uncased不能區(qū)分大小寫,因?yàn)樵~表只有小寫,需要事先lower-case。 基本使用示例: 現(xiàn)在我們基本熟悉了 BERT 的基本使用,接下來為其準(zhǔn)備輸入數(shù)據(jù)。一般情況下,在訓(xùn)練模型前,都需要對手上的數(shù)據(jù)進(jìn)行預(yù)處理,以滿足模型需要。 前面已經(jīng)介紹過了,模型輸入數(shù)據(jù)中,需要通過添加 [CLS] 和 [SEP] 這兩個(gè)特殊的token,將文本轉(zhuǎn)換為 BERT 所期望的格式。 首先,需要通過 pip 安裝 Transformers 庫: 為了更容易理解得到的輸出 下面是對上面 從上面的變量中看到的輸出 1. 第一行是 由上述結(jié)果所示, 2. 第二行是 3. 第三行是 注意到,我們使用了一個(gè)預(yù)訓(xùn)練 如果有來自不同語言的數(shù)據(jù)集,可能需要使用 現(xiàn)在我們知道從 在上面實(shí)現(xiàn)的代碼中,我們定義了一個(gè)名為 定義Dataset類后,將數(shù)據(jù)框拆分為訓(xùn)練集、驗(yàn)證集和測試集,比例為 至此,我們已經(jīng)成功構(gòu)建了一個(gè) Dataset 類來生成模型輸入數(shù)據(jù)?,F(xiàn)在使用具有 12 層 Transformer 編碼器的預(yù)訓(xùn)練 BERT 基礎(chǔ)模型構(gòu)建實(shí)際模型。 如果數(shù)據(jù)集中的文本是中文的,需要使用 從上面的代碼可以看出,BERT 模型輸出了兩個(gè)變量: 然后將 接下來是訓(xùn)練模型。使用標(biāo)準(zhǔn)的 PyTorch 訓(xùn)練循環(huán)來訓(xùn)練模型。 上下滑動(dòng)查看更多源碼 我們對模型進(jìn)行了 5 個(gè) epoch 的訓(xùn)練,我們使用 Adam 作為優(yōu)化器,而學(xué)習(xí)率設(shè)置為1e-6。因?yàn)楸景咐惺翘幚矶囝惙诸悊栴},則使用分類交叉熵作為我們的損失函數(shù)。 建議使用 GPU 來訓(xùn)練模型,因?yàn)?BERT 基礎(chǔ)模型包含 1.1 億個(gè)參數(shù)。 顯然,由于訓(xùn)練過程的隨機(jī)性,每次可能不會(huì)得到與上面截圖類似的損失和準(zhǔn)確率值。如果在 5 個(gè) epoch 之后沒有得到好的結(jié)果,可以嘗試將 epoch 增加到 10 個(gè),或者調(diào)整學(xué)習(xí)率。 現(xiàn)在我們已經(jīng)訓(xùn)練了模型,我們可以使用測試數(shù)據(jù)來評估模型在未見數(shù)據(jù)上的性能。下面是評估模型在測試集上的性能的函數(shù)。 運(yùn)行上面的代碼后,我從測試數(shù)據(jù)中得到了 0.994 的準(zhǔn)確率。由于訓(xùn)練過程中的隨機(jī)性,將獲得的準(zhǔn)確度可能會(huì)與我的結(jié)果略有不同。 這里有個(gè)問題:使用BERT預(yù)訓(xùn)練模型為什么最多只能輸入512個(gè)詞,最多只能兩個(gè)句子合成一句? 這是Google BERT預(yù)訓(xùn)練模型初始設(shè)置的原因,前者對應(yīng)Position Embeddings,后者對應(yīng)Segment Embeddings 在BERT中,Token,Position,Segment Embeddings 都是通過學(xué)習(xí)來得到的,pytorch代碼中它們是這樣的 而在BERT config中 因此,在直接使用 Google 的 BERT 預(yù)訓(xùn)練模型時(shí),輸入最多512個(gè)詞(還要除掉[CLS]和[SEP]),最多兩個(gè)句子合成一句。這之外的詞和句子會(huì)沒有對應(yīng)的 Embedding 。 當(dāng)然,如果有足夠的硬件資源自己重新訓(xùn)練 BERT,可以更改 此外還有人問 BERT的三個(gè)Embedding直接相加會(huì)對語義有影響嗎? 這是一個(gè)非常有意思的問題,蘇劍林老師也給出了回答,真的很妙?。?/p> Embedding 的數(shù)學(xué)本質(zhì),就是以 one hot 為輸入的單層全連接。也就是說,世界上本沒什么 Embedding,有的只是one hot。 在這里想用一個(gè)例子再嘗試解釋一下: 假設(shè) token Embedding 矩陣維度是 [4,768];position Embedding 矩陣維度是 [3,768];segment Embedding 矩陣維度是 [2,768]。 對于一個(gè)字,假設(shè)它的 token one-hot 是[1,0,0,0];它的 position one-hot 是[1,0,0];它的 segment one-hot 是[1,0]。 那這個(gè)字最后的 word Embedding,就是上面三種 Embedding 的加和。 如此得到的 word Embedding,和concat后的特征:[1,0,0,0,1,0,0,1,0],再過維度為 [4+3+2,768] = [9, 768] 的全連接層,得到的向量其實(shí)就是一樣的。 再換一個(gè)角度理解: 直接將三個(gè)one-hot 特征 concat 起來得到的 [1,0,0,0,1,0,0,1,0] 不再是one-hot了,但可以把它映射到三個(gè)one-hot 組成的特征空間,空間維度是 432=24 ,那在新的特征空間,這個(gè)字的one-hot就是[1,0,0,0,0...] (23個(gè)0)。 此時(shí),Embedding 矩陣維度就是 [24,768],最后得到的 word Embedding 依然是和上面的等效,但是三個(gè)小 Embedding 矩陣的大小會(huì)遠(yuǎn)小于新特征空間對應(yīng)的 Embedding 矩陣大小。 當(dāng)然,在相同初始化方法前提下,兩種方式得到的 word Embedding 可能方差會(huì)有差別,但是,BERT還有Layer Norm,會(huì)把 Embedding 結(jié)果統(tǒng)一到相同的分布。 BERT的三個(gè)Embedding相加,本質(zhì)可以看作一個(gè)特征的融合,強(qiáng)大如 BERT 應(yīng)該可以學(xué)到融合后特征的語義信息的 這就是 BERT 期望的所有輸入。 然后,BERT 模型將在每個(gè)token中輸出一個(gè)大小為 768 的 Embedding 向量。我們可以將這些向量用作不同類型 NLP 任務(wù)的輸入,無論是文本分類、本文生成、命名實(shí)體識(shí)別 (NER) 還是問答。 對于文本分類任務(wù),我們將注意力集中在特殊 [CLS] token 的 embedding 向量輸出上。這意味著我們將使用具有 [CLS] token 的大小為 768 的 embedding 向量作為分類器的輸入,然后它將輸出一個(gè)大小為分類任務(wù)中類別個(gè)數(shù)的向量。 現(xiàn)在我們?nèi)鐚W(xué)會(huì)了何利用 Hugging Face 的預(yù)訓(xùn)練 BERT 模型進(jìn)行文本分類任務(wù)的步驟。我希望在你開始使用 BERT是,這篇文章能幫到你。我們不僅可以使用來自 BERT 的embedding向量來執(zhí)行句子或文本分類任務(wù),還可以執(zhí)行更高級的 NLP 應(yīng)用,例如問答、文本生成或命名實(shí)體識(shí)別 (NER)任務(wù)。 預(yù)訓(xùn)練標(biāo)記器的名稱: https://huggingface.co/transformers/pretrained_models.html文本中,小猴子和大家一起學(xué)習(xí),如何利用 Hugging Face 的預(yù)訓(xùn)練 BERT 模型對新聞文章的文本(BBC 新聞分類數(shù)據(jù)集)進(jìn)行分類。
什么是 BERT
論文
源碼
BERT 輸入
[CLS]
:這是每個(gè)sequence的第一個(gè)token,代表分類token。[SEP]
:這是讓BERT知道哪個(gè)token屬于哪個(gè)序列的token。這一特殊表征法主要用于下一個(gè)句子預(yù)測任務(wù)或問答任務(wù)。如果我們只有一個(gè)sequence,那么這個(gè)token將被附加到序列的末尾。[CLS]/[SEP]
)呢,因?yàn)榕c文本中已有的其它詞相比,這個(gè)無明顯語義信息的符號(hào)會(huì)更“公平”地融合文本中各個(gè)詞的語義信息,從而更好的表示整句話的語義。[CLS]
位本身沒有語義,經(jīng)過12層,得到的是attention后所有詞的加權(quán)平均,相比其他正常詞,可以更好的表征句子語義。tokenization
。BertTokenizer
)就可以將輸入句子轉(zhuǎn)換為 BERT 所期望的tokens 序列。BERT 輸出
hidden_ size
的向量(BERT Base 中為 768)。對于我們在上面看到的句子分類示例,我們只關(guān)注第一個(gè)位置的輸出(將特殊的 [CLS] token 傳遞到該位置)。使用 BERT 進(jìn)行文本分類
BBC 新聞分類數(shù)據(jù)集
。sport(體育),business(商業(yè)),politics(政治),tech(科技),entertainment(娛樂)
。category
將作為標(biāo)簽,text
將作為 BERT 的輸入數(shù)據(jù)。預(yù)模型下載和使用
從huggingface
下載預(yù)訓(xùn)練模型的地址:https://huggingface.co/modelsfrom transformers import BertModel,BertTokenizer
BERT_PATH = './bert-base-cased'
tokenizer = BertTokenizer.from_pretrained(BERT_PATH)
print(tokenizer.tokenize('I have a good time, thank you.'))
bert = BertModel.from_pretrained(BERT_PATH)
print('load bert model over')['I', 'have', 'a', 'good', 'time',
',', 'thank', 'you', '.']
load bert model over預(yù)處理數(shù)據(jù)
%%capture
!pip install transformerstokenization
,我們以一個(gè)簡短的文本為例。from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
example_text = 'I will watch Memento tonight'
bert_input = tokenizer(example_text,padding='max_length',
max_length = 10,
truncation=True,
return_tensors='pt')
# ------- bert_input ------
print(bert_input['input_ids'])
print(bert_input['token_type_ids'])
print(bert_input['attention_mask'])tensor([[ 101, 146, 1209, 2824, 2508,
26173, 3568, 102, 0, 0]])
tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 0, 0]])BertTokenizer
參數(shù)的解釋:padding
:將每個(gè)sequence填充到指定的最大長度。max_length
: 每個(gè)sequence的最大長度。本示例中我們使用 10,但對于本文實(shí)際數(shù)據(jù)集,我們將使用 512,這是 BERT 允許的sequence 的最大長度。truncation
:如果為True,則每個(gè)序列中超過最大長度的標(biāo)記將被截?cái)唷?/section>return_tensors
:將返回的張量類型。由于我們使用的是 Pytorch,所以我們使用pt
;如果你使用 Tensorflow,那么你需要使用tf
。bert_input
,是用于稍后的 BERT 模型。但是這些輸出是什么意思?input_ids
,它是每個(gè) token 的 id 表示。實(shí)際上可以將這些輸入 id 解碼為實(shí)際的 token,如下所示:example_text = tokenizer.decode(bert_input.input_ids[0])
print(example_text)'[CLS] I will watch Memento tonight
[SEP] [PAD] [PAD]'BertTokenizer
負(fù)責(zé)輸入文本的所有必要轉(zhuǎn)換,為 BERT 模型的輸入做好準(zhǔn)備。它會(huì)自動(dòng)添加 [CLS]、[SEP] 和 [PAD] token。由于我們指定最大長度為 10,所以最后只有兩個(gè) [PAD] token。token_type_ids
,它是一個(gè) binary mask,用于標(biāo)識(shí) token 屬于哪個(gè) sequence。如果我們只有一個(gè) sequence,那么所有的 token 類型 id 都將為 0。對于文本分類任務(wù),token_type_ids
是 BERT 模型的可選輸入?yún)?shù)。attention_mask
,它是一個(gè) binary mask,用于標(biāo)識(shí) token 是真實(shí) word 還是只是由填充得到。如果 token 包含 [CLS]、[SEP] 或任何真實(shí)單詞,則 mask 將為 1。如果 token 只是 [PAD] 填充,則 mask 將為 0。BertTokenizer
的bert-base-cased
模型。如果數(shù)據(jù)集中的文本是英文的,這個(gè)預(yù)訓(xùn)練的分詞器就可以很好地工作。bert-base-multilingual-cased
。具體來說,如果你的數(shù)據(jù)集是德語、荷蘭語、中文、日語或芬蘭語,則可能需要使用專門針對這些語言進(jìn)行預(yù)訓(xùn)練的分詞器??梢栽诖颂幉榭聪鄳?yīng)的預(yù)訓(xùn)練標(biāo)記器的名稱[1]。特別地,如果數(shù)據(jù)集中的文本是中文的,需要使用bert-base-chinese
模型,以及其相應(yīng)的BertTokenizer
等。數(shù)據(jù)集類
BertTokenizer
中獲得什么樣的輸出,接下來為新聞數(shù)據(jù)集構(gòu)建一個(gè)Dataset
類,該類將作為一個(gè)類來將新聞數(shù)據(jù)轉(zhuǎn)換成模型需要的數(shù)據(jù)格式。上下滑動(dòng)查看更多源碼
import torch
import numpy as np
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-cased')
labels = {'business':0,
'entertainment':1,
'sport':2,
'tech':3,
'politics':4
}
class Dataset(torch.utils.data.Dataset):
def __init__(self, df):
self.labels = [labels[label] for label in df['category']]
self.texts = [tokenizer(text,
padding='max_length',
max_length = 512,
truncation=True,
return_tensors='pt')
for text in df['text']]
def classes(self):
return self.labels
def __len__(self):
return len(self.labels)
def get_batch_labels(self, idx):
# Fetch a batch of labels
return np.array(self.labels[idx])
def get_batch_texts(self, idx):
# Fetch a batch of inputs
return self.texts[idx]
def __getitem__(self, idx):
batch_texts = self.get_batch_texts(idx)
batch_y = self.get_batch_labels(idx)
return batch_texts, batch_ylabels
的變量,它是一個(gè)字典,將DataFrame中的 category
映射到 labels
的 id 表示。注意,上面的__init__
函數(shù)中,還調(diào)用了BertTokenizer
將輸入文本轉(zhuǎn)換為 BERT 期望的向量格式。80:10:10
。np.random.seed(112)
df_train, df_val, df_test = np.split(df.sample(frac=1, random_state=42),
[int(.8*len(df)), int(.9*len(df))])
print(len(df_train),len(df_val), len(df_test))1780 222 223
構(gòu)建模型
bert-base-chinese
模型。from torch import nn
from transformers import BertModel
class BertClassifier(nn.Module):
def __init__(self, dropout=0.5):
super(BertClassifier, self).__init__()
self.bert = BertModel.from_pretrained('bert-base-cased')
self.dropout = nn.Dropout(dropout)
self.linear = nn.Linear(768, 5)
self.relu = nn.ReLU()
def forward(self, input_id, mask):
_, pooled_output = self.bert(input_ids= input_id, attention_mask=mask,return_dict=False)
dropout_output = self.dropout(pooled_output)
linear_output = self.linear(dropout_output)
final_layer = self.relu(linear_output)
return final_layer_
包含sequence中所有 token 的 Embedding 向量層。pooled_output
包含 [CLS] token 的 Embedding 向量。對于文本分類任務(wù),使用這個(gè) Embedding 作為分類器的輸入就足夠了。pooled_output
變量傳遞到具有 ReLU
激活函數(shù)的線性層。在線性層中輸出一個(gè)維度大小為 5 的向量,每個(gè)向量對應(yīng)于標(biāo)簽類別(運(yùn)動(dòng)、商業(yè)、政治、 娛樂和科技
)。訓(xùn)練模型
from torch.optim import Adam
from tqdm import tqdm
def train(model, train_data, val_data, learning_rate, epochs):
# 通過Dataset類獲取訓(xùn)練和驗(yàn)證集
train, val = Dataset(train_data), Dataset(val_data)
# DataLoader根據(jù)batch_size獲取數(shù)據(jù),訓(xùn)練時(shí)選擇打亂樣本
train_dataloader = torch.utils.data.DataLoader(train, batch_size=2, shuffle=True)
val_dataloader = torch.utils.data.DataLoader(val, batch_size=2)
# 判斷是否使用GPU
use_cuda = torch.cuda.is_available()
device = torch.device('cuda' if use_cuda else 'cpu')
# 定義損失函數(shù)和優(yōu)化器
criterion = nn.CrossEntropyLoss()
optimizer = Adam(model.parameters(), lr=learning_rate)
if use_cuda:
model = model.cuda()
criterion = criterion.cuda()
# 開始進(jìn)入訓(xùn)練循環(huán)
for epoch_num in range(epochs):
# 定義兩個(gè)變量,用于存儲(chǔ)訓(xùn)練集的準(zhǔn)確率和損失
total_acc_train = 0
total_loss_train = 0
# 進(jìn)度條函數(shù)tqdm
for train_input, train_label in tqdm(train_dataloader):
train_label = train_label.to(device)
mask = train_input['attention_mask'].to(device)
input_id = train_input['input_ids'].squeeze(1).to(device)
# 通過模型得到輸出
output = model(input_id, mask)
# 計(jì)算損失
batch_loss = criterion(output, train_label)
total_loss_train += batch_loss.item()
# 計(jì)算精度
acc = (output.argmax(dim=1) == train_label).sum().item()
total_acc_train += acc
# 模型更新
model.zero_grad()
batch_loss.backward()
optimizer.step()
# ------ 驗(yàn)證模型 -----------
# 定義兩個(gè)變量,用于存儲(chǔ)驗(yàn)證集的準(zhǔn)確率和損失
total_acc_val = 0
total_loss_val = 0
# 不需要計(jì)算梯度
with torch.no_grad():
# 循環(huán)獲取數(shù)據(jù)集,并用訓(xùn)練好的模型進(jìn)行驗(yàn)證
for val_input, val_label in val_dataloader:
# 如果有GPU,則使用GPU,接下來的操作同訓(xùn)練
val_label = val_label.to(device)
mask = val_input['attention_mask'].to(device)
input_id = val_input['input_ids'].squeeze(1).to(device)
output = model(input_id, mask)
batch_loss = criterion(output, val_label)
total_loss_val += batch_loss.item()
acc = (output.argmax(dim=1) == val_label).sum().item()
total_acc_val += acc
print(
f'''Epochs: {epoch_num + 1}
| Train Loss: {total_loss_train / len(train_data): .3f}
| Train Accuracy: {total_acc_train / len(train_data): .3f}
| Val Loss: {total_loss_val / len(val_data): .3f}
| Val Accuracy: {total_acc_val / len(val_data): .3f}''') EPOCHS = 5
model = BertClassifier()
LR = 1e-6
train(model, df_train, df_val, LR, EPOCHS)在測試數(shù)據(jù)上評估模型
def evaluate(model, test_data):
test = Dataset(test_data)
test_dataloader = torch.utils.data.DataLoader(test, batch_size=2)
use_cuda = torch.cuda.is_available()
device = torch.device('cuda' if use_cuda else 'cpu')
if use_cuda:
model = model.cuda()
total_acc_test = 0
with torch.no_grad():
for test_input, test_label in test_dataloader:
test_label = test_label.to(device)
mask = test_input['attention_mask'].to(device)
input_id = test_input['input_ids'].squeeze(1).to(device)
output = model(input_id, mask)
acc = (output.argmax(dim=1) == test_label).sum().item()
total_acc_test += acc
print(f'Test Accuracy: {total_acc_test / len(test_data): .3f}')
evaluate(model, df_test)討論兩個(gè)問題
self.word_embeddings = Embedding(config.vocab_size, config.hidden_size)
self.position_embeddings = Embedding(config.max_position_embeddings, config.hidden_size)
self.token_type_embeddings = Embedding(config.type_vocab_size, config.hidden_size)'max_position_embeddings': 512
'type_vocab_size': 2BERT config
,設(shè)置更大 max_position_embeddings
和 type_vocab_size
值去滿足自己的需求。寫在最后
參考資料
聯(lián)系客服