03-Hugging Face 模型微调训练(基于 BERT 的中文评价情感分析)

1. 模型微调的基本概念与流程

微调是指在预训练模型的基础上,通过进一步的训练来适应特定的下游任务。BERT 模型通过预训练来学习语言的通用模式,然后通过微调来适应特定任务,如情感分析、命名实体识别等。微调过程中,通常冻结 BERT 的预训练层,只训练与下游任务相关的层。本课件将介绍如何使用 BERT 模型进行情感分析任务的微调训练。

1.1 ai开发的流程

其中,数据集是比较重要的,从用户获得原始数据,对数据的清洗,往往会影响模型的训练结果

1.2 训练过程 的三个状态

过拟合状态,会丧失模型的泛华能力

好的拟合:描述尾巴长长的,花色、大小、脚印的形状

过拟合:xxxx...会将绿色的毛毯也认为是猫的特征

怎么判断是否是一个好的拟合状态?

观察测试的loss的情况

2. 加载数据集

情感分析任务的数据通常包括文本及其对应的情感标签。使用 Hugging Face 的 datasets 库可以轻松地加载和处理数据集。

python 复制代码
from datasets import load_dataset,load_from_disk

#在线加载数据
# dataset = load_dataset(path="NousResearch/hermes-function-calling-v1",cache_dir="data/")
# print(dataset)
#转为csv格式
# dataset.to_csv(path_or_buf=r"D:\PycharmProjects\demo_02\data\ChnSentiCorp.csv")

# 加载缓存数据
data_path = r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\data\ChnSentiCorp"
datasets = load_from_disk(data_path)
print(datasets)

train_data = datasets["test"]
for data in train_data:
    print(data)

# 扩展:加载CSV格式数据
# dataset = load_dataset(path="csv",data_files=data_path)
# print(dataset)

魔搭社区下载+环境配置

采用魔搭社区下载数据集

python 复制代码
pip install oss2 addict

#升级
pip install --upgrade datasets
pip install --upgrade modelscope
python 复制代码
#数据集下载
from modelscope.msdatasets import MsDataset


#需要下载的绝对目录
cache_dir = r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\data"

ds =  MsDataset.load('AiNiklaus/ChnSentiCorp', subset_name='default', split='train', cache_dir=cache_dir)
#您可按需配置 subset_name、split,参照"快速使用"示例代码

2.1 数据集格式

Hugging Face 的 datasets 库支持多种数据集格式,如 CSV、JSON、TFRecord 等。在本案例中,使用CSV 格式,CSV 文件应包含两列:一列是文本数据,另一列是情感标签。

2.2 数据集信息

加载数据集后,可以查看数据集的基本信息,如数据集大小、字段名称等。这有助于我们了解数据的分布情况,并在后续步骤中进行适当的处理。

3. 制作 Dataset

加载数据集后,需要对其进行处理以适应模型的输入格式。这包括数据清洗、格式转换等操作。

python 复制代码
from datasets import Dataset  
# 制作 Dataset  
dataset = Dataset.from_dict({  
    'text': ['位置尚可,但距离海边的位置比预期的要差的多','5月8日付款成功,当当网显示5月10日发货,可是至今还没看到货物,也没收到任何通知,简不知怎么说好!!!','整体来说,本书还是不错的。至少在书中描述了许多现实中存在的司法系统方面的问题,这是值得每个法律工作者去思考的。尤其是让那些涉世不深的想加入到律师队伍中的年青人,看到了社会特别是中国司法界真实的一面。缺点是:书中引用了大量的法律条文和司法解释,对于已经是律师或有一定工作经验的法律工作者来说有点多余,而且所占的篇幅不少,有凑字数的嫌疑。整体来说还是不错的。不要对一本书提太高的要求。'],  
    'label': [0, 1, 1] # 0 表示负向评价,1 表示正向评价  
})  
# 查看数据集信息  
print(dataset)

介绍AI模型是如何处理字符数据的

图中对应着模型字典的顺序

python 复制代码
"""
本节小结核心介绍AI模型是如何处理字符数据的
"""
from transformers import BertTokenizer

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")
# print(token)


#准备要编码的文本数据
sents = ["白日依山尽,",
         "价格在这个地段属于适中, 附近有早餐店,小饭店, 比较方便,无早也无所"]

#批量编码句子
out = token.batch_encode_plus(
    batch_text_or_text_pairs=[sents[0],sents[1]],
    add_special_tokens=True,
    #当句子长度大于max_length(上限是model_max_length)时,截断
    truncation=True,
    max_length=15,
    #一律补0到max_length
    padding="max_length",
    #可取值为tf,pt,np,默认为list
    return_tensors=None,
    return_attention_mask=True,
    return_token_type_ids=True,
    return_special_tokens_mask=True,
    #返回序列长度
    return_length=True
)
#input_ids 就是编码后的词
#token_type_ids第一个句子和特殊符号的位置是0,第二个句子的位置1()只针对于上下文编码
#special_tokens_mask 特殊符号的位置是1,其他位置是0
#length 编码之后的序列长度
for k,v in out.items():
    print(k,":",v)

#解码文本数据
print(token.decode(out["input_ids"][0]),token.decode(out["input_ids"][1]))

3.1 数据集字段

在制作 Dataset 时,需定义数据集的字段。在本案例中,定义了两个字段:text(文本)和label (情感标签)。每个字段都需要与模型的输入和输出匹配。

3.2 数据集信息

制作 Dataset 后,可以通过 dataset.info 等方法查看其大小、字段名称等信息,以确保数据集的正确性和完整性。

4. vocab 字典操作

在微调 BERT 模型之前,需要将模型的词汇表(vocab)与数据集中的文本匹配。这一步骤确保输入的文本能够被正确转换为模型的输入格式。

python 复制代码
from transformers import BertTokenizer
# 加载 BERT 模型的 vocab 字典
tokenizer = BertTokenizer.from_pretrained('bert-base-chinese')
# 将数据集中的文本转换为 BERT 模型所需的输入格式
dataset = dataset.map(lambda x: tokenizer(x['text'], return_tensors="pt"),
batched=True)
# 查看数据集信息
print(dataset)

4.1 词汇表(vocab)

BERT 模型使用词汇表(vocab)将文本转换为模型可以理解的输入格式。词汇表包含所有模型已知的单词及其对应的索引。确保数据集中的所有文本都能找到对应的词汇索引是至关重要的。

4.2 文本转换

使用 tokenizer 将文本分割成词汇表中的单词,并转换为相应的索引。此步骤需要确保文本长度、特殊字符处理等都与 BERT 模型的预训练设置相一致。

5. 下游任务模型设计

在微调 BERT 模型之前,需要设计一个适应情感分析任务的下游模型结构。通常包括一个或多个全连接层,用于将 BERT 输出的特征向量转换为分类结果。

python 复制代码
from transformers import BertModel
import torch.nn as nn
class SentimentAnalysisModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.bert = BertModel.from_pretrained('bert-base-chinese')
        self.drop_out = nn.Dropout(0.3)
        self.linear = nn.Linear(768, 2)  # 假设情感分类为二分类
    def forward(self, input_ids, attention_mask):
        _, pooled_output = self.bert(
            input_ids=input_ids,
            attention_mask=attention_mask,
            return_dict=False
       )
        output = self.drop_out(pooled_output)
        return self.linear(output)

5.1 模型结构

下游任务模型通常包括以下几个部分:

BERT 模型:用于生成文本的上下文特征向量。

Dropout 层:用于防止过拟合,通过随机丢弃一部分神经元来提高模型的泛化能力。

全连接层:用于将 BERT 的输出特征向量映射到具体的分类任务上。

5.2 模型初始化

使用 BertModel.from_pretrained() 方法加载预训练的 BERT 模型,同时也可以初始化自定义的全连接层。初始化时,需要根据下游任务的需求,定义合适的输出维度。

6. 自定义模型训练

模型设计完成后,进入训练阶段。通过数据加载器(DataLoader)高效地批量处理数据,并使用优化器更新模型参数。

python 复制代码
from torch.utils.data import DataLoader  
from transformers import AdamW  
# 实例化 DataLoader
data_loader = DataLoader(dataset, batch_size=16, shuffle=True)
# 初始化模型和优化器
model = SentimentAnalysisModel()
optimizer = AdamW(model.parameters(), lr=5e-5)
# 训练循环
for epoch in range(3):  # 假设训练 3 个 epoch
    model.train()
    for batch in data_loader:
        optimizer.zero_grad()
        outputs = model(input_ids=batch['input_ids'], 
attention_mask=batch['attention_mask'])
        loss = nn.CrossEntropyLoss()(outputs, batch['labels'])
        loss.backward()
        optimizer.step()

6.1 数据加载

使用 DataLoader 实现批量数据加载。DataLoader 自动处理数据的批处理和随机打乱,确保训练的高效性和数据的多样性。

6.2 AdamW 优化器

AdamW 是一种适用于 BERT 模型的优化器,结合了 Adam 和权重衰减的特点,能够有效地防止过拟合。

6.3 训练循环

训练循环包含前向传播(forward pass)、损失计算(loss calculation)、 反向传播(backwardpass)、参数更新(parameter update)等步骤。每个 epoch 都会对整个数据集进行一次遍历,更新模型参数。通常训练过程中会跟踪损失值的变化,以判断模型的收敛情况。

6.4 保存参数

在保存参数的方法中,要确保目录存在,pytorch保存不会自己创建目录

python 复制代码
        # #每训练完一轮,保存一次参数
        # torch.save(model.state_dict(),f"params/{epoch}_bert.pth")
        # print(epoch,"参数保存成功!")
            #根据验证准确率保存最优参数
            if val_acc > best_val_acc:
                best_val_acc = val_acc
                torch.save(model.state_dict(),"params/best_bert.pth")
                print(f"EPOCH:{epoch}:保存最优参数:acc{best_val_acc}")
        #保存最后一轮参数
        torch.save(model.state_dict(), "params/last_bert.pth")
        print(f"EPOCH:{epoch}:最后一轮参数保存成功!")

6.5 训练批次设置

根据gpu显存占用,选择合适的大小批次

6.6 训练效果判断

acc在0.91左右就好,因为模型比较小

正在训练

7. 最终效果评估与测试

在模型训练完成后,需要评估其在测试集上的性能。通常使用准确率、精确率、召回率和 F1 分数等指标来衡量模型的效果。

7.1 准确率(Accuracy)

准确率是衡量分类模型整体性能的基本指标,计算公式为正确分类的样本数量除以总样本数量。

python 复制代码
from sklearn.metrics import accuracy_score
# 假设有一个测试集 `test_dataset`,并且已经经过与训练集相同的预处理
test_loader = DataLoader(test_dataset, batch_size=16, shuffle=False)
# 评估模型在测试集上的性能
model.eval()
predictions, true_labels = [], []
with torch.no_grad():
    for batch in test_loader:
        outputs = model(input_ids=batch['input_ids'], 
attention_mask=batch['attention_mask'])
        _, preds = torch.max(outputs, dim=1)
        predictions.extend(preds.cpu().numpy())
        true_labels.extend(batch['labels'].cpu().numpy())

accuracy = accuracy_score(true_labels, predictions)  
print(f"Accuracy: {accuracy:.4f}") 

7.2 精确率、召回率和 F1 分数

精确率(Precision)和召回率(Recall)是分类模型的另两个重要指标,分别反映模型在正例预测上的精确性和召回能力。F1 分数是精确率和召回率的调和平均数,通常用于不均衡数据集的评估。

python 复制代码
from sklearn.metrics import precision_score, recall_score, f1_score  
precision = precision_score(true_labels, predictions, average='weighted')  
recall = recall_score(true_labels, predictions, average='weighted')  
f1 = f1_score(true_labels, predictions, average='weighted')  
print(f"Precision: {precision:.4f}")  
print(f"Recall: {recall:.4f}")  
print(f"F1 Score: {f1:.4f}") 

7.3 结果分析与模型优化

通过分析测试集上的结果,可以发现模型的强项和弱项。例如,如果 F1 分数较低,可能是由于数据集不平衡,导致模型在某些类别上表现不佳。通过调整超参数、改进数据预处理步骤,或使用更复杂的模型结构,可以进一步提高模型性能。

7.4 保存与加载模型

为了在未来使用训练好的模型,可以将其保存为文件,之后再加载进行推理或进一步的微调。

python 复制代码
# 保存模型  
torch.save(model.state_dict(), 'sentiment_analysis_model.pth')  
# 加载模型  
model = SentimentAnalysisModel()  
model.load_state_dict(torch.load('sentiment_analysis_model.pth', model.eval())

8. 完整代码

整体流程

第一步:准备数据。加载文本数据,用BERT分词器编码成模型可处理的数字格式(input_ids, attention_mask等),并打包成批次。

第二步:定义模型。加载预训练BERT模型,冻结其参数,然后添加一个全连接层(768维输入,2维输出)作为分类头。

第三步:配置训练环境。设置计算设备(GPU/CPU)、优化器(AdamW)、损失函数(交叉熵损失),并初始化跟踪最佳验证准确率的变量。

第四步:执行训练循环

  • 每个训练批次中:将数据输入模型,计算预测结果和损失,执行反向传播更新分类头参数。

  • 每隔几个批次打印训练损失和准确率。

  • 每个训练轮次(epoch)结束后,切换到评估模式,在整个验证集上计算损失和准确率。

第五步:保存模型 。根据验证集准确率保存性能最佳的模型参数(best_bert.pth),并在每轮结束后也保存最新参数(last_bert.pth)。

train_val.py

python 复制代码
#模型训练
import torch
from torch.optim import AdamW

from MyData import MyDataset
from torch.utils.data import DataLoader
from net import Model
from transformers import BertTokenizer

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#定义训练的轮次(将整个数据集训练完一次为一轮)
EPOCH = 30000

#加载字典和分词器
token = BertTokenizer.from_pretrained(r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f")

#将传入的字符串进行编码
def collate_fn(data):
    sents = [i[0]for i in data]
    label = [i[1] for i in data]
    #编码
    data = token.batch_encode_plus(
        batch_text_or_text_pairs=sents,
        # 当句子长度大于max_length(上限是model_max_length)时,截断
        truncation=True,
        max_length=512,
        # 一律补0到max_length
        padding="max_length",
        # 可取值为tf,pt,np,默认为list
        return_tensors="pt",
        # 返回序列长度
        return_length=True
    )
    input_ids = data["input_ids"]
    attention_mask = data["attention_mask"]
    token_type_ids = data["token_type_ids"]
    label = torch.LongTensor(label)
    return input_ids,attention_mask,token_type_ids,label



#创建数据集
train_dataset = MyDataset("train")
train_loader = DataLoader(
    dataset=train_dataset,
    #训练批次,根据gpu的内存抉择我的是4G
    batch_size=50,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
#创建验证数据集
val_dataset = MyDataset("validation")
val_loader = DataLoader(
    dataset=val_dataset,
    #训练批次
    batch_size=40,
    #打乱数据集
    shuffle=True,
    #舍弃最后一个批次的数据,防止形状出错
    drop_last=True,
    #对加载的数据进行编码
    collate_fn=collate_fn
)
if __name__ == '__main__':
    #开始训练
    print(DEVICE)
    model = Model().to(DEVICE)
    #定义优化器
    optimizer = AdamW(model.parameters())
    #定义损失函数
    loss_func = torch.nn.CrossEntropyLoss()

    #初始化验证最佳准确率
    best_val_acc = 0.0

    for epoch in range(EPOCH):
        for i,(input_ids,attention_mask,token_type_ids,label) in enumerate(train_loader):
            #将数据放到DVEVICE上面
            input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE),attention_mask.to(DEVICE),token_type_ids.to(DEVICE),label.to(DEVICE)
            #前向计算(将数据输入模型得到输出)
            out = model(input_ids,attention_mask,token_type_ids)
            #根据输出计算损失
            loss = loss_func(out,label)
            #根据误差优化参数
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            #每隔5个批次输出训练信息
            if i%5 ==0:
                out = out.argmax(dim=1)
                #计算训练精度
                acc = (out==label).sum().item()/len(label)
                print(f"epoch:{epoch},i:{i},loss:{loss.item()},acc:{acc}")
        #验证模型(判断模型是否过拟合)
        #设置为评估模型
        model.eval()
        #不需要模型参与训练
        with torch.no_grad():
            val_acc = 0.0
            val_loss = 0.0
            for i, (input_ids, attention_mask, token_type_ids, label) in enumerate(val_loader):
                # 将数据放到DVEVICE上面
                input_ids, attention_mask, token_type_ids, label = input_ids.to(DEVICE), attention_mask.to(
                    DEVICE), token_type_ids.to(DEVICE), label.to(DEVICE)
                # 前向计算(将数据输入模型得到输出)
                out = model(input_ids, attention_mask, token_type_ids)
                # 根据输出计算损失
                val_loss += loss_func(out, label)
                #根据数据,计算验证精度
                out = out.argmax(dim=1)
                val_acc+=(out==label).sum().item()
            val_loss/=len(val_loader)
            val_acc/=len(val_loader)
            print(f"验证集:loss:{val_loss},acc:{val_acc}")
        # #每训练完一轮,保存一次参数
        # torch.save(model.state_dict(),f"params/{epoch}_bert.pth")
        # print(epoch,"参数保存成功!")
            #根据验证准确率保存最优参数
            if val_acc > best_val_acc:
                best_val_acc = val_acc
                torch.save(model.state_dict(),"params/best_bert.pth")
                print(f"EPOCH:{epoch}:保存最优参数:acc{best_val_acc}")
        #保存最后一轮参数
        torch.save(model.state_dict(), "params/last_bert.pth")
        print(f"EPOCH:{epoch}:最后一轮参数保存成功!")

MyData

python 复制代码
from torch.utils.data import Dataset
from datasets import load_from_disk

class MyDataset(Dataset):
    #初始化数据集
    def __init__(self,split):
        #从磁盘加载数据
        # self.dataset = load_from_disk( r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\model\bert-base-chinese\models--bert-base-chinese\snapshots")
        # 修改 MyData.py 第 8 行
        self.dataset = load_from_disk(
            r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\data\ChnSentiCorp")

        if split == "train":
            self.dataset = self.dataset["train"]
        elif split == "test":
            self.dataset = self.dataset["test"]
        elif split == "validation":
            self.dataset = self.dataset["validation"]
        else:
            print("数据名错误!")

    #返回数据集长度
    def __len__(self):
        return len(self.dataset)

    #对每条数据单独做处理
    def __getitem__(self, item):
        text = self.dataset[item]["text"]
        label = self.dataset[item]["label"]

        return text,label

if __name__ == '__main__':
    dataset = MyDataset("train")
    for data in dataset:
        print(data)

net

python 复制代码
import torch
from transformers import BertModel

#定义设备信息
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(DEVICE)

#加载预训练模型
pretrained = BertModel.from_pretrained(r"F:\26_01\第六期\python\L2_study\L2\day04-基于BERT模型的自定义微调训练\demo_04\model\bert-base-chinese\models--bert-base-chinese\snapshots\c30a6ed22ab4564dc1e3b2ecbf6e766b0611a33f").to(DEVICE)
print(pretrained)

#定义下游任务(增量模型)
class Model(torch.nn.Module):
    def __init__(self):
        super().__init__()
        #设计全连接网络,实现二分类任务
        self.fc = torch.nn.Linear(768,2)
    #使用模型处理数据(执行前向计算)
    def forward(self,input_ids,attention_mask,token_type_ids):
        #冻结Bert模型的参数,让其不参与训练
        with torch.no_grad():
            out = pretrained(input_ids=input_ids,attention_mask=attention_mask,token_type_ids=token_type_ids)
        #增量模型参与训练
        out = self.fc(out.last_hidden_state[:,0])
        return out
相关推荐
ZPC82102 小时前
PPO 在ROS2 中训练与推理
人工智能·算法·机器人
薛定猫AI2 小时前
【技术干货】从 Alpha Evolve 到 Open Viking:前沿 AI 技术在开发实战中的落地路径
人工智能
智能工业品检测-奇妙智能2 小时前
金属矿山安全智能AI视觉识别
服务器·人工智能·安全·openclaw·奇妙智能
我认不到你2 小时前
使用Ollama运行本地模型,模型参数选择(保姆级图文讲解)
人工智能·深度学习·ai·语言模型
冴羽2 小时前
OpenClaw 接入飞书配置指南
人工智能·aigc
向上的车轮2 小时前
SAP AI系统融合:嵌入式智能、Joule助手与BTP扩展的实践路径
人工智能
Fleshy数模2 小时前
OpenCV图像处理实战:旋转、多模板匹配与金字塔
图像处理·人工智能·opencv
九硕智慧建筑一体化厂家2 小时前
楼宇的能耗与碳排放问题,如何达到降本和减碳?
人工智能
焦耳热科技前沿2 小时前
厦门大学Nat. Commun.:闪蒸焦耳热诱导富镍层状氧化物正极表面尖晶石化以稳定晶格氧
人工智能·科技·自动化·能源·材料工程