上一篇文章中,我详细讲解了 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/