NLP/Bert 文本分类实战

NLP/Bert 文本分类实战

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

DataLoaderbatch_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)

定义损失函数和优化器

其中biasLayerNorm 的权重不使用 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_idstoken_type_idsattention_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


如果你觉得这篇文章对你有帮助,不妨点个赞,让我有更多动力写出好文章。

我的文章会首发在公众号上,欢迎扫码关注我的公众号张贤同学


评论