上一篇文章中,我詳細講解了 BertModel
。
在今天這篇文章,我會使用 BertForSequenceClassification
,在自己的訓練集上訓練情感分類模型。
數據集來源於 https://github.com/bojone/bert4keras/tree/master/examples/datasets
是一個中文的情感二分類數據集。
而詞匯表 vocab.txt
來自於哈工大的中文預訓練語言模型 BERT-wwm, Chinese
。
地址:https://github.com/ymcui/Chinese-BERT-wwm#%E4%B8%AD%E6%96%87%E6%A8%A1%E5%9E%8B%E4%B8%8B%E8%BD%BD
以 PyTorch 版BERT-wwm, Chinese
為例,下載完畢后對zip文件進行解壓得到:
1 2 3 4
|
chinese-bert_chinese_wwm_pytorch.zip |- chinese_wwm_pytorch.bin # 模型權重 |- bert_config.json # 模型參數 |- vocab.txt # 詞表
|
我們前面提到,BertForSequenceClassification
是在 BertModel
的基礎上,添加了一個線性層 + 激活函數,用於分類。而 Huggingface 提供的預訓練模型 bert-base-uncased
只包含 BertModel
的權重,不包括線性層 + 激活函數的權重。在下面,我們會使用model = BertForSequenceClassification.from_pretrained("bert-base-uncased", config=config)
來加載模型,那么線性層 + 激活函數的權重就會隨機初始化。我們的目的,就是通過微調,學習到線性層 + 激活函數的權重。
我們這里預訓練模型使用 Huggingface 的 bert-base-uncased
,不使用哈工大模型的權重,因為我們是想要在 bert-base-uncased
的基礎上進行微調。因此只使用其中的 vocab.txt
。
我把數據、詞匯表(vocab.txt)以及代碼,放到了 github 上:https://github.com/zhangxiann/BertPractice。
下面開始講解代碼。
導入庫
1 2 3 4 5 6 7 8
|
import torch.nn as nn from transformers import AdamW from torch.utils.data import Dataset import pandas as pd import torch from transformers import BertConfig, BertForSequenceClassification from transformers import BertTokenizer from torch.utils.data import DataLoader
|
參數設置
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
# 超參數 hidden_dropout_prob = 0.3 num_labels = 2 learning_rate = 1e-5 weight_decay = 1e-2 epochs = 2 batch_size = 16 device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 文件路徑 data_path = ".\\sentiment\\" vocab_file = data_path+"vocab.txt" # 詞匯表 train_data = data_path + "sentiment.train.data" # 訓練數據集 valid_data = data_path + "sentiment.valid.data" # 驗證數據集
|
定義 Dataset,加載數據
在 Dataset
的 __getitem__()
函數里,根據 idx 分別找到 text 和 label,最后返回一個 dict。
DataLoader
的 batch_size
設置為 16。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
class SentimentDataset(Dataset): def __init__(self, path_to_file): self.dataset = pd.read_csv(path_to_file, sep="\t", names=["text", "label"]) def __len__(self): return len(self.dataset) def __getitem__(self, idx): # 根據 idx 分別找到 text 和 label text = self.dataset.loc[idx, "text"] label = self.dataset.loc[idx, "label"] sample = {"text": text, "label": label} # 返回一個 dict return sample # 加載訓練集 sentiment_train_set = SentimentDataset(data_path + "sentiment.train.data") sentiment_train_loader = DataLoader(sentiment_train_set, batch_size=batch_size, shuffle=True, num_workers=0) # 加載驗證集 sentiment_valid_set = SentimentDataset(data_path + "sentiment.valid.data") sentiment_valid_loader = DataLoader(sentiment_valid_set, batch_size=batch_size, shuffle=False, num_workers=0)
|
定義 Tokenizer 和 Model
這里定義了 BertConfig
,使用了上面定義的一些超參數:如類別數量,hidden_dropout_prob
等。
預訓練模型選擇 bert-base-uncased
。
1 2 3 4 5 6 7 8
|
# 定義 tokenizer,傳入詞匯表 tokenizer = BertTokenizer(data_path+vocab_file)
# 加載模型 config = BertConfig.from_pretrained("bert-base-uncased", num_labels=num_labels, hidden_dropout_prob=hidden_dropout_prob) model = BertForSequenceClassification.from_pretrained("bert-base-uncased", config=config) model.to(device)
|
定義損失函數和優化器
其中bias
和 LayerNorm
的權重不使用 weight_decay
。這是根據 https://huggingface.co/transformers/training.html 來設置的,暫未查到這么做的原因。如果你知道原因,歡迎留言告訴我。
1 2 3 4 5 6 7 8 9 10 11 12
|
# 定義優化器和損失函數 # Prepare optimizer and schedule (linear warmup and decay) # 設置 bias 和 LayerNorm.weight 不使用 weight_decay 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': weight_decay}, {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0} ]
#optimizer = AdamW(model.parameters(), lr=learning_rate) optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate) criterion = nn.CrossEntropyLoss()
|
定義訓練和驗證的函數
首先從 dataloader
獲取到 text 和 label。
然后通過
1
|
tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt")
|
獲得 tokenized_text
,包括 input_ids
, token_type_ids
, attention_mask
。
max_length=100
表示最大長度為 100,配合 truncation=True
,表示超過 100 則截斷。
padding=True
表示長度小於 100,則補全到 100。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
|
# 定義訓練的函數 def train(model, dataloader, optimizer, criterion, device): model.train() epoch_loss = 0 epoch_acc = 0 for i, batch in enumerate(dataloader): # 標簽形狀為 (batch_size, 1) label = batch["label"] text = batch["text"]
# tokenized_text 包括 input_ids, token_type_ids, attention_mask tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt") tokenized_text = tokenized_text.to(device) # 梯度清零 optimizer.zero_grad()
#output: (loss), logits, (hidden_states), (attentions) output = model(**tokenized_text, labels=label)
# y_pred_prob = logits : [batch_size, num_labels] y_pred_prob = output[1] y_pred_label = y_pred_prob.argmax(dim=1)
# 計算loss # 這個 loss 和 output[0] 是一樣的 loss = criterion(y_pred_prob.view(-1, 2), label.view(-1))
# 計算acc acc = ((y_pred_label == label.view(-1)).sum()).item()
# 反向傳播 loss.backward() optimizer.step()
# epoch 中的 loss 和 acc 累加 # loss 每次是一個 batch 的平均 loss epoch_loss += loss.item() # acc 是一個 batch 的 acc 總和 epoch_acc += acc if i % 200 == 0: print("current loss:", epoch_loss / (i+1), "\t", "current acc:", epoch_acc / ((i+1)*len(label)))
# len(dataloader) 表示有多少個 batch,len(dataloader.dataset.dataset) 表示樣本數量 return epoch_loss / len(dataloader), epoch_acc / len(dataloader.dataset.dataset)
def evaluate(model, iterator, device): model.eval() epoch_loss = 0 epoch_acc = 0 with torch.no_grad(): for _, batch in enumerate(iterator): label = batch["label"] text = batch["text"] tokenized_text = tokenizer(text, max_length=100, add_special_tokens=True, truncation=True, padding=True, return_tensors="pt") tokenized_text = tokenized_text.to(device)
output = model(**tokenized_text, labels=label) y_pred_label = output[1].argmax(dim=1) loss = output[0] acc = ((y_pred_label == label.view(-1)).sum()).item() # epoch 中的 loss 和 acc 累加 # loss 每次是一個 batch 的平均 loss epoch_loss += loss.item() # acc 是一個 batch 的 acc 總和 epoch_acc += acc
# len(dataloader) 表示有多少個 batch,len(dataloader.dataset.dataset) 表示樣本數量 return epoch_loss / len(iterator), epoch_acc / len(iterator.dataset.dataset)
|
開始訓練和驗證
1 2 3 4 5 6
|
# 開始訓練和驗證 for i in range(epochs): train_loss, train_acc = train(model, sentiment_train_loader, optimizer, criterion, device) print("train loss: ", train_loss, "\t", "train acc:", train_acc) valid_loss, valid_acc = evaluate(model, sentiment_valid_loader, criterion, device) print("valid loss: ", valid_loss, "\t", "valid acc:", valid_acc)
|
參考
https://www.cnblogs.com/dogecheng/p/11911909.html
轉自https://blog.zhangxiann.com/202008222159/