基于pytorch本地部署微调bert模型(yelp文本分类数据集)

项目介绍

本项目使用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. 评论文本:用户对商家的评论内容,通常包含对服务、食品质量、环境等方面的评价。

  2. 星级评分:用户根据他们的体验给出的评分,通常是1到5颗星。

  3. 商家信息:评论关联的商家信息,包括商家名称、类别和位置等。


代码实现

依赖环境

基于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]

可以看到在训练集和验证集上,可以明显体现出模型的微调训练效果,微调大模型的发挥空间有很多,欢迎大家一起讨论交流~


相关推荐
桥田智能几秒前
工博会动态 | 来8.1馆 看桥田如何玩转全场
人工智能·机器人·自动化
深度学习的奋斗者几秒前
YOLOv8+注意力机制+PyQt5玉米病害检测系统完整资源集合
python·深度学习·yolo
—你的鼬先生2 分钟前
从零开始使用树莓派debian系统使用opencv4.10.0进行人脸识别(保姆级教程)
python·opencv·debian·人脸识别·二维码识别·opencv安装
Eric.Lee202117 分钟前
数据集-目标检测系列-口罩检测数据集 mask>> DataBall
人工智能·目标检测·计算机视觉·数据集·口罩检测
界面开发小八哥25 分钟前
如何用LightningChart Python实现地震强度数据可视化应用程序?
开发语言·python·信息可视化
ChinaZ.AI28 分钟前
ComfyUI 速度更快,显存占用更低的图像反推模型Florence2PromptGen,效果媲美JoyCaption,还支持Flux训练打标
人工智能·stable diffusion·aigc·flux·comfyui·florence
小强在此1 小时前
机器学习【教育领域及其平台搭建】
人工智能·学习·机器学习·团队开发·教育领域·机器
憨憨憨憨憨到不行的程序员1 小时前
【OpenCV】场景中人的识别与前端计数
人工智能·opencv·计算机视觉
剑指~巅峰1 小时前
亲身体验Llama 3.1:开源模型的部署与应用之旅
人工智能·深度学习·opencv·机器学习·计算机视觉·数据挖掘·llama
DisonTangor1 小时前
Llama 3.2:利用开放、可定制的模型实现边缘人工智能和视觉革命
人工智能·llama