项目介绍
本项目使用hugging face上提供的Bert模型API,基于yelp数据集,在本地部署微调Bert模型,官方的文档链接为https://huggingface.co/docs/transformers/quicktour,但是在官方介绍中出现了太多的API调用接口,无法在真正意义上做到本地微调部署,本项目致力于只通过Bert模型的接口获得Bert模型,其他包括数据集预处理、损失函数定义、模型训练以后后续模型的调试部署都在本地进行,让微调的过程清晰化和透明化。
BERT模型
BERT(Bidirectional Encoder Representations from Transformers)是谷歌于2018年发布的自然语言处理模型。其核心创新在于双向上下文理解,允许模型同时考虑上下文中的前后词,从而提升对文本含义的理解。BERT的训练过程采用了无监督学习,使用大规模的文本数据进行预训练,然后通过微调适应具体任务,如问答或情感分析。这个模型的发布极大推动了NLP领域的发展,成为许多后续模型的基础。
BERT模型的预训练过程分为两个目标任务:
- 将训练数据集文本中的内容按照一定的比例挖空一些单词(或文字),BERT模型通过挖空单词的上下文本内容与语义复现出该位置上应该出现的单词;
- 将多个句子组合成一个句子组,让BERT模型判断句子之间是否存在上下语句的关系。
yelp数据集
Yelp文本分类数据集是一个用于自然语言处理(NLP)任务的公开数据集,主要用于训练和评估文本分类模型。该数据集包含来自Yelp网站的用户评论,通常包括以下几个关键特征:
-
评论文本:用户对商家的评论内容,通常包含对服务、食品质量、环境等方面的评价。
-
星级评分:用户根据他们的体验给出的评分,通常是1到5颗星。
-
商家信息:评论关联的商家信息,包括商家名称、类别和位置等。
代码实现
依赖环境
基于pytorch架构,显存最好大于或者等于4GB
python
import torch
import torch.nn as nn
from tqdm.auto import tqdm
from statistics import mean
from torch.optim import AdamW
import matplotlib.pyplot as plt
from transformers import get_scheduler
from torch.utils.data import DataLoader
from prepare_dataset_model import bert_dataset, model_bert,get_dataset_list
from transformers import AutoModel,AutoTokenizer,AutoModelForSequenceClassification
# from datasets import load_dataset
# 可以直接从huggingface上下载并预处理训练数据集
# 感兴趣的朋友可以自行查阅函数说明文档,本项目使用自定义的数据集预处理类与函数
prepare_dataset_model是定义在另一个脚本里的数据预处理函数,接下来先展开这一部分
数据集预处理
从官网上可以下载yelp对应的数据集,本项目选择的是csv格式的数据,使用pandas可以非常轻松的对csv格式的数据进行操作与处理
下面是放在prepare_dataset_model.py脚本中的代码
数据列表的获取
python
def get_dataset_list(data_path,simple_num, rate=0.8):
data = pd.read_csv(data_path)
data_list = []
print('loading dataset...')
show_bar = tqdm(range(simple_num))
for index,item in data.iterrows():
data_list.append({"text":item['text'], "label":item['label']})
show_bar.update(1)
if index==simple_num:
break
lenght = len(data_list)
train_data_list = data_list[:int(lenght*rate)]
test_data_list = data_list[int(lenght*rate):]
return train_data_list,test_data_list
ylep数据集有几万条数据,一次性全部读出来在大多数情况下显得不现实,于是定义simple_num 参数进行传入我们想要处理的数据数,rate是训练集与测试集的比例,最后返回按照比例的训练集列表和测试集列表
数据集类的定义
python
class bert_dataset(Dataset):
def __init__(self, data_list, tokenizer):
self.dataset = data_list
self.tokenizer = tokenizer
def __getitem__(self, idx):
item = self.dataset[idx]
text = item["text"]
label = item["label"]
inputs = self.tokenizer(text,padding="max_length",truncation=True,return_tensors='pt')
# inputs["label"] = label
return inputs,label
def __len__(self):
return len(self.dataset)
这一步是经典的自定义数据集操作,值得一提的是,假如直接把label 字段传入inputs 的字典,在微调的模型中同样也能够接受,并且在模型中可以直接返回按照交叉熵损失函数计算的loss值;也可以将label字段分开,另外定义损失函数进行计算,本项目选择的是第二种
完整代码
python
import torch.nn as nn
import pandas as pd
import torch.nn.functional as F
from tqdm.auto import tqdm
from torch.utils.data import Dataset
class bert_dataset(Dataset):
def __init__(self, data_list, tokenizer):
self.dataset = data_list
self.tokenizer = tokenizer
def __getitem__(self, idx):
item = self.dataset[idx]
text = item["text"]
label = item["label"]
inputs = self.tokenizer(text,padding="max_length",truncation=True,return_tensors='pt')
# inputs["label"] = label
return inputs,label
def __len__(self):
return len(self.dataset)
class model_bert(nn.Module):
def __init__(self, bert):
super(model_bert,self).__init__()
self.bert = bert
# self.out = nn.Linear(5,1)
def forward(self, input_ids=None, token_type_ids=None, attention_mask=None):
bert_out = self.bert(input_ids,token_type_ids,attention_mask)
out = F.softmax(bert_out.logits, dim=-1)
return out
def get_dataset_list(data_path,simple_num, rate=0.8):
data = pd.read_csv(data_path)
data_list = []
print('loading dataset...')
show_bar = tqdm(range(simple_num))
for index,item in data.iterrows():
data_list.append({"text":item['text'], "label":item['label']})
show_bar.update(1)
if index==simple_num:
break
lenght = len(data_list)
train_data_list = data_list[:int(lenght*rate)]
test_data_list = data_list[int(lenght*rate):]
return train_data_list,test_data_list
if __name__ == '__main__':
data_path = r'your data path'
train_list,test_list = get_dataset_list(data_path,5000)
print(f'len of trian:{len(train_list)}')
print(f'len of test:{len(test_list)}')
这里对模型也进行了调整,在输出的最后加一个softmax激活函数,最后输出分类的值,假如采用这种方式训练,损失函数的选择也应该进行对应的改变,在后面的代码中没有使用到这个模型,读者们可以自行对比一下使用原模型以及使用调整后的模型最后的训练效果
模型搭建
具体Bert模型的搭建可以使用huggingface提供的API接口
python
tokenizer = AutoTokenizer.from_pretrained('google-bert/bert-base-uncased')
model = AutoModelForSequenceClassification.from_pretrained('google-bert/bert-base-uncased')
具体的函数调用仍可以参考huggingface官方的说明文档,运行上面的语句后终端会显示一些下载的进度条,模型和token会被下载到C盘的.cache缓存文件夹中,以我的电脑为例,保存路径为
python
C:\Users\29278\.cache\huggingface
下载完成之后,以后的每一次调用都可以从这个路径上调用(除非在huggingface上的模型有更新),假如我们想直接调用本地的模型,可以通过save_pretrained 语句把模型保存到指定的路径中(详见官网介绍),再通过from_pretrained语句进行调用
如果想对Bert模型的结构进行进一步的调整,可以参考上一模块的model_bert类型的架构进行定义
python
model_use = model_bert(model)
损失函数,优化器和学习率优化
python
criterion = nn.CrossEntropyLoss()
optimizer = AdamW(model.parameters(),lr = 5e-5)
lr_scheduler = get_scheduler(name="linear",optimizer=optimizer,num_warmup_steps=0,num_training_steps=num_trainStep)
这里的学习率选择使用全连接的方式进行优化,num_trainStep定义为对每一个批次的训练之后进行学习率的调整
模型训练代码
python
import torch
import torch.nn as nn
from tqdm.auto import tqdm
from statistics import mean
from torch.optim import AdamW
import matplotlib.pyplot as plt
# from datasets import load_dataset
from transformers import get_scheduler
from torch.utils.data import DataLoader
from prepare_dataset_model import bert_dataset, model_bert,get_dataset_list
from transformers import AutoModel,AutoTokenizer,AutoModelForSequenceClassification
device = torch.device('cuda:0' if torch.cuda.is_available else 'cpu')
print(f'using {device}...')
epoch = 10
batch_size = 8
model_path = r"your model path".replace('\\', '/')
token_path = r"your token path".replace('\\','/')
data_path = r"your data path"
tokenizer = AutoTokenizer.from_pretrained(token_path)
model = AutoModelForSequenceClassification.from_pretrained(model_path,num_labels=5).to(device)
optimizer = AdamW(model.parameters(),lr = 5e-5)
criterion = nn.CrossEntropyLoss()
# dataset_raw = load_dataset("csv",data_files=r"your data path")
# dataset_list = dataset_raw['train']
# print(type(dataset_list))
dataset_list,dataset_list_test = get_dataset_list(data_path,10)
dataset_class = bert_dataset(dataset_list,tokenizer)
dataset_class_test = bert_dataset(dataset_list_test,tokenizer)
dataset_input = DataLoader(dataset_class,batch_size=batch_size)
dataset_input_test = DataLoader(dataset_class,batch_size=batch_size,shuffle=True)
num_trainStep = epoch*len(dataset_input)
show_num_step = epoch*(len(dataset_input)+len(dataset_input_test))
lr_scheduler = get_scheduler(name="linear",optimizer=optimizer,num_warmup_steps=0,num_training_steps=num_trainStep)
loss_list = []
correct_list = []
loss_list_test = []
correct_list_test = []
process_bar = tqdm(range(show_num_step))
for step in range(epoch):
loss_list_everyEpoch = []
correct_list_everyEpoch = []
model.train()
for token, label in dataset_input:
token = {k_in:v_in.squeeze(1).to(device) for k_in,v_in in token.items()}
label = label.to(device)
output = model(**token)
out = output.logits
# print(out)
# print("loss:",out.loss)
optimizer.zero_grad()
loss = criterion(out,label)
loss.backward()
optimizer.step()
lr_scheduler.step()
out_class = torch.argmax(out,dim=-1)
correct_num = (out_class==label).sum()
correct_rate = correct_num/label.shape[0]
loss_list_everyEpoch.append(loss.item())
correct_list_everyEpoch.append(correct_rate.item())
process_bar.update(1)
loss_list.append(mean(loss_list_everyEpoch))
correct_list.append(mean(correct_list_everyEpoch))
# print("train correct rate=",mean(correct_list_everyEpoch))
show_correct_rate = mean(correct_list_everyEpoch)
tqdm.write(f"train correct rate={show_correct_rate:2f}")
loss_list_everyEpoch_test = []
correct_list_everyEpoch_test = []
model.eval()
for token_test, label_test in dataset_input_test:
token_test = {k_in:v_in.squeeze(1).to(device) for k_in,v_in in token_test.items()}
label_test = label_test.to(device)
output_test = model(**token_test)
out_test = output_test.logits
out_class_test = torch.argmax(out_test, dim=-1)
# print(out_test)
loss_test = criterion(out_test,label_test)
loss_list_everyEpoch_test.append(loss_test.item())
correct_num_test = (out_class_test==label_test).sum()
correct_rate_test = correct_num_test/label_test.shape[0]
correct_list_everyEpoch_test.append(correct_rate_test.item())
process_bar.update(1)
loss_list_test.append(mean(loss_list_everyEpoch_test))
correct_list_test.append(mean(correct_list_everyEpoch_test))
# print(f"test correct={mean(correct_list_everyEpoch_test)}")
tqdm.write(f"test correct={mean(correct_list_everyEpoch_test):2f}")
print("train loss list:", loss_list)
print("test loss list:", loss_list_test)
print("train correct rate:", correct_list)
print("test correct rate:", correct_list_test)
结果
上述的代码仅提供一个demo,只去抽取了数据集中的10条数据进行训练,迭代10次,目的是让读者能够较快速地进行代码调试,再以此为基础对微调做更加针对性的操作
python
[06:15<00:00, 18.93s/it]train loss list: [1.7557673454284668, 1.3130085468292236, 1.1637543439865112, 1.1429853439331055, 1.0526454448699951, 1.0120376348495483, 0.9493937492370605, 0.8610967397689819, 0.8940214514732361, 0.8735412955284119]
test loss list: [1.3395963907241821, 1.1231462955474854, 1.0310029983520508, 0.9932389855384827, 0.9472938776016235, 0.9150838851928711, 0.8861185908317566, 0.8644040822982788, 0.845656156539917, 0.8328518271446228]
train correct rate: [0.25, 0.625, 0.75, 0.625, 0.75, 0.625, 1.0, 1.0, 0.75, 0.875]
test correct rate: [0.5, 0.625, 0.75, 0.875, 1.0, 1.0, 0.875, 0.875, 0.875, 0.875]
100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 20/20 [06:15<00:00, 18.77s/it]
可以看到在训练集和验证集上,可以明显体现出模型的微调训练效果,微调大模型的发挥空间有很多,欢迎大家一起讨论交流~