import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import io
from sklearn.model_selection import train_test_split
from tensorflow.keras.preprocessing.sequence import pad_sequences
import torch
from torch.utils.data import DataLoader, TensorDataset, RandomSampler, SequentialSampler
from torch.optim.lr_scheduler import ExponentialLR
from transformers import BertTokenizer, BertConfig, BertModel
from transformers import AdamW, BertForSequenceClassification, get_linear_schedule_with_warmup
from tqdm import tqdm, trange
import warnings
warnings.filterwarnings("ignore")
使用 GPU。
# 检查本机GPU是否可用
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# GPU型号
torch.cuda.get_device_name()
我电脑真跑不了,果断用了
Colab
,真的好用,感谢GPU
吧🐶,推荐大家使用,省时又省心。
加载数据集。
# 读取数据
df = pd.read_csv("./train.tsv", delimiter='\t', header=None, names=['label', 'sentence'])
df.head()
构建 BERT 的输入语料格式。
# 语料
sentences = df.sentence.values
# BERT的输入语料格式: [CLS] + sentence + [SEP]
sentences = ["[CLS] " + sen + " [SEP]" for sen in sentences]
sentences[:5]
# 标签
labels = df.label.values
获取 BERT 的分词器 tokenizer 进行分词和转换。
# 1 从 hugging face 获取预训练分词模型
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased", do_lower_case=True) # 全部转换为小写
# 2 对语料进行分词
sentence_tokens = [tokenizer.tokenize(sen) for sen in sentences]
# 3 将token转换为id,并固定每个语句的长度
max_len = 128
sentence_ids = [tokenizer.convert_tokens_to_ids(sen) for sen in sentence_tokens]
# 4 将所有语句的长度固定到 max_len
sentence_ids = pad_sequences(sentence_ids, maxlen=max_len, dtype='long', truncating='post', padding='post')
print(sentence_ids[0])
# 5 根据 sentence_ids 创建 attention mask
attention_mask = [[1 if id > 0 else 0 for id in sen] for sen in sentence_ids]
print(attention_mask[0])
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0]
数据集进行分离:train 和 eval。
X_train, X_eval, y_train, y_eval = train_test_split(sentence_ids, labels, test_size=0.2, random_state=666)
train_masks, eval_masks, _, _ = train_test_split(attention_mask, sentence_ids, test_size=0.2, random_state=666)
第二段代码,
train_test_split
返回了四个变量,分别是train_masks
、eval_masks
、_
和_
。其中train_masks
和eval_masks
是训练集和验证集的attention_mask
,用于指示输入序列的哪些部分是真正的输入。_
和_
是被忽略的变量,因为在这里我们不需要使用它们。
数据类型转换,全部转换为 tensor。
X_train = torch.tensor(X_train)
X_eval = torch.tensor(X_eval)
y_train = torch.tensor(y_train)
y_eval = torch.tensor(y_eval)
train_masks = torch.tensor(train_masks)
eval_masks = torch.tensor(eval_masks)
数据打包,便于进行批量训练(PyTorch的基本操作)。
batch_size = 32
# 1 训练集
# 数据打包
train_dataset = TensorDataset(X_train, train_masks, y_train)
# 一批一批地读取
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
#2 验证集
# 数据打包
eval_dataset = TensorDataset(X_eval, eval_masks, y_eval)
# 一批一批地读取
eval_dataloader = DataLoader(eval_dataset, batch_size=batch_size, shuffle=True)
模型训练。
model = BertForSequenceClassification.from_pretrained("bert-base-uncased", num_labels=2)
model.cuda()
model.cuda()
是将 PyTorch 模型移动到 GPU 设备上进行加速计算的方法之一。在 PyTorch 中,所有的计算都是在 Tensor 上进行的。通过将 Tensor 移动到 GPU 上,可以利用 GPU 并行计算的能力,从而大幅提高计算速度。在使用 PyTorch 模型进行训练和推理时,通常需要将输入数据和模型参数移动到 GPU 上,并使用 model.cuda() 将模型移动到 GPU 上进行计算。如果想将模型从 GPU 移回 CPU,可以使用model.cpu()
。
# 训练轮次
EPOCHS = 10
# 优化器
optimizer = AdamW(model.parameters(), lr=2e-5, eps=1e-8)
# 学习率
scheduler = get_linear_schedule_with_warmup(optimizer, num_warmup_steps=0, num_training_steps=len(train_dataloader)*EPOCHS)
使用
get_linear_schedule_with_warmup()
函数为神经网络优化器设置学习率,用于创建一个学习率调度器。optimizer
参数是要调整学习率的优化器对象。
num_warmup_steps
参数确定在训练开始时要使用多少个步骤进行热身。在热身期间,学习率会逐渐从0增加到优化器设置的初始学习率。在这段代码中,num_warmup_steps的值为0,这意味着没有热身期。
num_training_steps
参数指定模型的总训练步骤数,这是通过将一个epoch中的批次数(len(train_dataloader))乘以epoch数(EPOCHS)来计算的。在热身期间,学习率会线性增加,然后在训练结束时线性减少到0。
总体而言,使用学习率调度器的目的是在训练过程中调整模型的学习率,以提高其收敛和泛化性能。get_linear_schedule_with_warmup()函数是在自然语言处理(NLP)任务中常用的学习率调度方法之一。
定义 accuracy。
# 计算 真值labels 与 预测值preds 的 accuracy
def accuracy(labels, preds):
preds = np.argmax(preds, axis=1).flatten() # shape = (1, :)
labels = labels.flatten()
acc = np.sum(preds == labels) / len(preds) # 准确率
return acc
# 模型训练
train_loss = []
for i in tqdm(range(EPOCHS), desc='Epoch'):
model.train() # 定义训练模式
tr_loss = 0
tr_examples = 0 # 样本量
tr_steps = 0
for i, batch_data in enumerate(train_dataloader):
# 一批一批的读取数据
batch_data = tuple(data.to(device) for data in batch_data) # 部署到gpu
# 解析,元组分包,一一顺序对应
inputs_ids, inputs_masks, inputs_labels = batch_data
# 梯度置零
optimizer.zero_grad()
# 前向传播,模型的输出有两个结果
outputs = model(inputs_ids, token_type_ids=None, attention_mask=inputs_masks, labels=inputs_labels)
#print("keys : ", outputs.keys()) # ['loss', 'logits']
# 获取loss
loss = outputs['loss']
# 保存loss,记录loss的变化过程
train_loss.append(loss.item())
# 累计loss,方便计算平均 loss
tr_loss += loss.item()
# 累计样本量
tr_examples += inputs_ids.size(0)
# 多少批次batch
tr_steps += 1
# 反向传播
loss.backward()
# 更新优化器参数
optimizer.step()
# 更新学习率
scheduler.step()
print("Training loss : {}".format(tr_loss / tr_steps))
# 计算平均损失
# 定义模型验证模式
model.eval()
eval_acc = 0.0
eval_steps, eval_examples = 0.0, 0.0 # 轮数和样本数
for batch in eval_dataloader:
# 部署到GPU
batch = tuple(data.to(device) for data in batch_data)
# 解析
inputs_ids, inputs_masks, inputs_labels = batch
# 验证阶段不需要计算梯度
with torch.no_grad():
preds = model(inputs_ids, token_type_ids=None, attention_mask=inputs_masks)
# 将 preds 和 labels 部署到cpu计算
preds = preds['logits'].detach().to('cpu').numpy() # 将preds从计算图剥离,不计算梯度
labels = inputs_labels.to('cpu').numpy()
# 计算 accuracy,累加,计算平均准确率
eval_acc += accuracy(labels, preds)
eval_steps += 1
print("Eval Accuracy : {}".format(eval_acc / eval_steps))
print("\n\n")
训练了10轮,train 了24分钟,验证集准确率100%。
# 可视化 train loss
plt.figure(figsize=(12,10))
plt.title("Training Loss")
plt.xlabel("Batch")
plt.ylabel("Loss")
plt.plot(train_loss)
plt.show()
模型测试(首先是和训练数据集相同的数据预处理)。
# 加载测试集
df = pd.read_csv("./test.tsv", delimiter='\t', header=None, names=['label', 'sentence'])
sentences = df.sentence.values
labels = df.label.values
# 构造输入格式
sentences = ["[CLS] " + sen + " [SEP]" for sen in sentences]
# 分词
sentences_tokens = [tokenizer.tokenize(sen) for sen in sentences]
# token --> id
sentence_ids = [tokenizer.convert_tokens_to_ids(sen) for sen in sentences_tokens]
# 将所有语句的长度固定到 max_len
sentence_ids = pad_sequences(sentence_ids, maxlen=max_len, dtype='long', truncating='post', padding='post')
# 根据 sentence_ids 创建 attention mask
attention_mask = [[1 if id > 0 else 0 for id in sen] for sen in sentence_ids]
# 数据类型转换,全部转换为 tensor
sentence_ids = torch.tensor(sentence_ids)
attention_mask = torch.tensor(attention_mask)
labels = torch.tensor(labels)
# 数据打包
test_dataset = TensorDataset(sentence_ids, attention_mask, labels)
# 一批一批地读取数据
test_dataloader = DataLoader(test_dataset, batch_size, shuffle=True)
定义模型测试模式。
# 定义模型测试模式
model.eval()
test_loss, test_acc = 0.0, 0.0
steps = 0
num = 0
for batch in test_dataloader:
# 部署到 GPU
batch = tuple(data.to(device) for data in batch)
# 数据解包
inputs_ids, inputs_masks, inputs_labels = batch
# 验证阶段不需要计算梯度
with torch.no_grad():
preds = model(inputs_ids, token_type_ids=None, attention_mask=inputs_masks) # 模型预测
# 将preds从计算图剥离,不计算梯度
preds = preds['logits'].detach().to('cpu').numpy()
inputs_labels = inputs_labels.to('cpu').numpy()
# 计算 acc
acc = accuracy(inputs_labels, preds)
# 累计 acc
test_acc += acc
# 累计轮次
steps += 1
# 累计 test 样本数量
num += len(inputs_ids)
print("steps = ", steps)
print("test number = ", num)
print("test acc : {}".format(test_acc / steps))
效果一般,可能是数据比较小,发挥不了 BERT 的真实实力。
种一棵树最好的时间是十年前,其次是现在。