import os
os.environ["TF_ENABLE_ONEDNN_OPTS"]="0"
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from transformers import BertTokenizer,BertModel
from datasets import load_dataset # HuggingFace数据加载方法
from tqdm import tqdm
# 1- 定义变量
# 运行的设备
device = ("cuda" if torch.cuda.is_available() else "cpu")
# 预训练模型路径
model_path = r"D:\soft\PretrainedModel\bert-base-chinese"
# 创建词汇映射器:词汇映射器只是对句子进行分词,然后得到词索引,不涉及任何的需要学习的参数,因此可以不用放到GPU或CPU上
tokenizer = BertTokenizer.from_pretrained(model_path)
# 创建Bert预训练模型实例对象:需要学习相关的模型参数,因此必须放到GPU或CPU上
bert_model = BertModel.from_pretrained(model_path).to(device=device)
# 2- 数据探索【可选】
def load_data():
"""
参数解释:
path:既能够指定文件目录,也能够指定文件类型。推荐用来指定文件目录
data_files:可以传递文件路径,还可以传递数据类型和文件路径字典的形式。推荐直接传递文件路径
split:如果data_files是具体的文件路径,那么这里必须给train
"""
# 加载方式一:【掌握】如果data_files是具体的文件路径,那么这里必须给train
data = load_dataset(path="../data", data_files="train.csv", split="train")
# 加载方式二:如果data_files是具体的文件路径,那么这里必须给train
# data = load_dataset(path="data", data_files="test.csv", split="train")
# 加载方式三:path:既能够指定文件目录,也能够指定文件类型。推荐用来指定文件目录
# data = load_dataset(path="csv", data_files="data/test.csv", split="train")
# 加载方式四:数据类型和文件路径以字典的形式传递
# file_dict = {
# "train":"data/train.csv",
# "test":"data/test.csv",
# "aaaa":"data/test.csv"
# }
# data = load_dataset(path="csv", data_files=file_dict, split="train")
# data = load_dataset(path="csv", data_files=file_dict, split="test")
# data = load_dataset(path="csv", data_files=file_dict, split="aaaa")
print(type(data))
print(len(data))
print(data)
print(data[0])
# 3- 针对每个批次数据的具体处理函数
def collate_fn(batch_data):
# 1- 分别提取出特征和目标值
# 数据示例:[句子1, 句子2, ...]
sents = [line_dict.get("text") for line_dict in batch_data]
# 数据示例:[1, 0, 1, 1, 1, 0, 1, 0]
labels = [line_dict.get("label") for line_dict in batch_data]
# 2- 数据预处理
# 2.1- 特征进行分词,得到词索引组成的张量
# 因为DataLoader中batch_size>1,因此这里的padding必须为max_length,不能为True
sents_tensor = tokenizer.batch_encode_plus(
sents,
return_tensors="pt",
padding="max_length",
truncation=True,
max_length=300
)
# 将sents_tensor字典拆解出来
input_ids = sents_tensor.get("input_ids")
token_type_ids = sents_tensor.get("token_type_ids")
attention_mask = sents_tensor.get("attention_mask")
# 2.3- 目标值直接封装为张量
labels = torch.tensor(labels,dtype=torch.long)
return input_ids,token_type_ids,attention_mask,labels
# 4- 创建数据加载器
def get_dataloader(file_path):
# 1- 加载指定路径下的文件
dataset = load_dataset(path="../data", data_files=file_path, split="train")
# 2- 创建DataLoader实例对象
"""
参数解释:
dataset:数据集对象
batch_size:每个批次中样本的条数。如果样本长度不一致,batch_size只能设置为1
shuffle:是否对数据打散。让模型训练更加充分
drop_last:如果最后一个批次的样本条数不足batch_size大小,那么直接不要
collate_fn:针对每个批次数据的具体处理函数。注意:传递的是函数名称,不要带小括号
"""
dataloader = DataLoader(
dataset=dataset,
batch_size=8,
shuffle=True,
drop_last=True,
collate_fn=collate_fn
)
return dataloader
# 5- 搭建模型:Bert预训练模型+我们自定义的网络层
class AiModel(nn.Module):
def __init__(self):
# 1- 初始化父类
super().__init__()
# 2- 在Bert预训练模型的后面,增加自己的线性网络结构层
"""
这里的参数必须是768和2,为什么?
答:
768:Bert的词向量/隐藏状态向量维度是768维
2:目前案例是二分类场景
"""
self.linear = nn.Linear(in_features=768,out_features=2)
def forward(self,input_ids,token_type_ids,attention_mask):
# 1- 先调用Bert预训练模型
"""
注意如下两点:
1- 推荐使用torch.no_grad(),冻结Bert的参数训练。可以不加,那么回对Bert的110M个参数都会进行训练,比较耗时
2- bert_model()里面的参数要使用关键字传参
"""
with torch.no_grad():
bert_output = bert_model(input_ids=input_ids, token_type_ids=token_type_ids, attention_mask=attention_mask)
# 2- 再调用我们自定义的网络层
"""
为什么写成bert_output.last_hidden_state[:, 0]?
答:
1- last_hidden_state表示取的是Bert模型最后一层隐藏层中每个时间步的隐藏状态。等价于多层RNN中output
2- [:, 0]取到的是每条句子中第一个token(词),也就是[CLS]的隐藏状态
3- Bert预训练模型的设计思想:只有编码器端,编码器端注意力是自注意力,也就是每个词都能够获得其他词的隐藏状态信息
[CLS]是一个固定的标识,每个句子的开头都有
4- 结论:Bert做分类,固定写成bert_output.last_hidden_state[:, 0]
以上的解释,建议看论文:https://arxiv.org/pdf/1810.04805
对应内容:The final hidden state corresponding to this token is used as the aggregate sequence representation for classification tasks.
"""
return self.linear(bert_output.last_hidden_state[:, 0])
# 6- 模型训练
def train():
# 1- 加载数据
dataloader = get_dataloader("train.csv")
# 2- 禁用Bert预训练模型的梯度下降
for param in bert_model.parameters():
param.requires_grad_(False)
# 3- 创建对象
# 3.1- 创建模型对象
model = AiModel().to(device=device)
# 3.2- 优化器
optimizer = torch.optim.AdamW(params=model.parameters(),lr=1e-3)
# 3.3- 损失函数
loss = nn.CrossEntropyLoss()
# 4- 切换模式
model.train()
# 5- 训练
epochs = 1
for epoch in range(epochs):
for i,(input_ids,token_type_ids,attention_mask,labels) in enumerate(tqdm(dataloader),start=1):
# 5.1- 将数据全部发送到对应的设备
input_ids = input_ids.to(device)
token_type_ids = token_type_ids.to(device)
attention_mask = attention_mask.to(device)
labels = labels.to(device)
# 5.2- 前向传播
pred_result = model(input_ids,token_type_ids,attention_mask)
# 5.3- 计算损失值
loss_value = loss(pred_result,labels)
# 5.4- 固定代码
optimizer.zero_grad() # 梯度清零
loss_value.sum().backward() # 反向传播
optimizer.step() # 更新参数:不更新Bert的,只更新我们自己增加的线性层
# 6- 间隔输出相关指标
if i%20==0:
# 6.1- 获得预测概率最高的索引
pred_index = torch.argmax(pred_result,dim=-1)
# 6.2- 统计预测正确的样本条数
correct_count = (pred_index==labels).sum().item()
# 6.3- 计算准确率
acc = correct_count / len(labels)
print(f"第{epoch+1}轮次,已训练的样本批次{i},平均准确率{round(acc,4)}")
# 7- 保存训练好的模型
torch.save(model.state_dict(), "../model/bert.pkl")
# 7- 模型预测
def predict():
# 1- 加载数据
dataloader = get_dataloader("test.csv")
# 2- 加载训练好的模型
model = AiModel().to(device)
model.load_state_dict(torch.load("../model/bert.pkl"))
# 3- 定义准确率统计变量
correct_count = 0 # 预测正确的样本条数
total_sample_count = 0 # 已经预测的总样本条数
# 4- 预测
model.eval()
with torch.no_grad():
for i,(input_ids,token_type_ids,attention_mask,labels) in enumerate(tqdm(dataloader),start=1):
# 4.1- 将数据发送到对应设备
input_ids = input_ids.to(device)
token_type_ids = token_type_ids.to(device)
attention_mask = attention_mask.to(device)
labels = labels.to(device)
# 4.2- 预测得到结果
pred_result = model(input_ids,token_type_ids,attention_mask)
# 4.3- 计算准确率:算的累计平均准确率
pred_index = torch.argmax(pred_result,dim=-1)
correct_count += (pred_index==labels).sum().item()
total_sample_count += len(labels)
# print("labels-->",labels)
# print("pred_index-->",pred_index)
# print("input_ids-->",input_ids)
# 4.4- 间隔输出相关指标
if i % 20 == 0:
# 计算准确率
acc = correct_count / total_sample_count
print(f"已预测的样本批次{i},累计平均准确率{round(acc, 4)}")
# 恢复得到原始句子内容
text_content = "".join(tokenizer.convert_ids_to_tokens(input_ids[0], skip_special_tokens=True))
print(f"每个批次中第一条样本的原始评价内容:{text_content},预测的类别是:{pred_index[0]}")
if __name__ == '__main__':
# 测试数据探索
# load_data()
# 测试数据加载器
# dataloader = get_dataloader(file_path="train.csv")
# for input_ids,token_type_ids,attention_mask,labels in dataloader:
# print(token_type_ids)
# break
# train()
predict()