前言

之前写三篇关于LoRA微调的一些基础知识,分别如下:
LoRA微调基础知识点
LoRA微调模型结构可训参数和配置详解
LoRA训练推理部署流程

本文主要是基于bert-chinese-base模型采用LoRA微调技术,进行10分类的一个微调过程。
由于LoRA是一种高效的预训练大模型微调技术,在模型选择方面BERT显然算不上什么大模型,但是可以学习LoRA微调的整个过程,BERT也可以进行全量的参数训练,便于比较微调和重训练的区别。

完整代码
如果对你有烦请帮我点个star,非常感谢~~

LoRA微调核心代码

LoRA微调的代码还是很简洁的,只需要在原始模型的某层(Linear层)增加额外的配置即可。

config = LoraConfig(
    task_type=TaskType.SEQ_CLS, 
    target_modules=["query", "key", "value"],
    inference_mode=False,
    r=8,
    lora_alpha=32,
    lora_dropout=0.1
)
model = get_peft_model(model, config)

核心超参数如下:
task_type:任务类型,例如序列分类(SEQ_CLS)。
r: LoRA模型中新增加的权重矩阵的秩。
lora_alpha:控制LoRA模块中缩放因子的大小,default=8 (peft==0.5.0)。
lora_dropout:LoRA模块中的dropout。
inference_mode:False为模型处于训练模式,LoRA 层会进行更新,反之LoRA层不更新(推理使用)。

完整训练代码如下:

import pandas as pd
from transformers import BertTokenizerFast, BertForSequenceClassification, Trainer, TrainingArguments
import torch
from torch.utils.data import Dataset
import model_config
from peft import get_peft_model, LoraConfig, TaskType
import time

train_data_path = model_config.train_data_path
dev_data_path = model_config.dev_data_path

train_data = pd.read_csv(train_data_path)
train_texts = train_data["0"].tolist()
train_labels = train_data["1"].tolist()

dev_data = pd.read_csv(dev_data_path)
eval_texts = dev_data["0"].tolist()
eval_labels = dev_data["1"].tolist()

# 分类标签数
num_labels = len(set(train_labels))

# 预训练模型,加载原始bert模型
model_name = model_config.model_name_tokenizer_path
tokenizer = BertTokenizerFast.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(model_name, num_labels=num_labels)

# 对文本进行编码
# 入参详解 请参考 https://blog.csdn.net/weixin_42924890/article/details/139269528
train_encodings = tokenizer(train_texts, truncation=True, padding=True, max_length=64)
eval_encodings = tokenizer(eval_texts, truncation=True, padding=True, max_length=64)


class TextDataset(Dataset):
    def __init__(self, encodings, labels):
        self.encodings = encodings
        self.labels = labels

    def __getitem__(self, idx):
        # print(idx)
        # for key, val in self.encodings.items():
        #     print(key)
        #     print(val)
        #     print(val[idx])

        item = {key: torch.tensor(val[idx]) for key, val in self.encodings.items()}
        item["labels"] = torch.tensor(self.labels[idx])
        return item

    def __len__(self):
        return len(self.labels)


train_dataset = TextDataset(train_encodings, train_labels)
eval_dataset = TextDataset(eval_encodings, eval_labels)

# 设置训练参数并创建Trainer
training_args = TrainingArguments(
    output_dir="./lora_results",          # 模型保存路径
    logging_dir="./lora_logs",            # 日志保存路径
    save_strategy="steps",           # 保存策略,按steps保存
    save_total_limit=1,              # 保存模型的最大数量
    evaluation_strategy="steps",     # 评估策略,按steps评估
    save_steps=250,                  # 每250个step保存一次
    eval_steps=125,                  # 每125个step评估一次
    load_best_model_at_end=True,     # 训练结束后加载在评估过程中表现最好的模型
    num_train_epochs=5,              # 训练轮数,epoch数
    per_device_train_batch_size=32,  # 训练时每个设备上的batch大小
    per_device_eval_batch_size=32,   # 评估时每个设备上的batch大小
    warmup_steps=1250,               # 预热步数,用于学习率warmup
    weight_decay=0.001,              # 权重衰减,防止过拟合
    dataloader_drop_last=True,       # 是否丢弃最后一个不完整的batch
)
config = LoraConfig(
    task_type=TaskType.SEQ_CLS, 
    target_modules=["query", "key", "value"],
    inference_mode=False,
    r=8,
    lora_alpha=32,
    lora_dropout=0.1
)
model = get_peft_model(model, config)
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
)

# 开始训练
t1 = time.time()
trainer.train()
t2 = time.time()
delta_time = t2 - t1
print(f"Train cost: {delta_time:.4f} s")

批量测试

LoRA训练完成,加载LoRA权重进行推理使用,推理的核心代码如下:

config = LoraConfig(
    task_type=TaskType.SEQ_CLS, 
    target_modules=["query", "key", "value"],
    inference_mode=True,
    r=8,
    lora_alpha=32,
    lora_dropout=0.1
)
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=model_path_loar, config=config)

构建配置参数,使用PeftModel加载LoRA权重。
注意:model_path_loar是LoRA微调的checkpoint保存路径。

完整代码如下:

import pandas as pd
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix
from transformers import BertTokenizerFast, BertForSequenceClassification
import torch
import seaborn as sns
from torch.utils.data import DataLoader
from datasets import Dataset
import matplotlib.pyplot as plt
import model_config
from peft import get_peft_model, PeftModel, LoraConfig, TaskType

# 加载测试集
test_data_path = model_config.test_data_path
test_data = pd.read_csv(test_data_path) 
# 简单测试请指定测试集数量
# test_data = pd.read_csv(test_data_path, nrows=32)
texts = test_data["0"].tolist()
labels = test_data["1"].tolist()
# 原始模型
model_path = model_config.model_name_tokenizer_path
model_path_loar = model_config.model_path_lora

tokenizer = BertTokenizerFast.from_pretrained(model_path)
model = BertForSequenceClassification.from_pretrained(model_path, num_labels=10)

config = LoraConfig(
    task_type=TaskType.SEQ_CLS, 
    target_modules=["query", "key", "value"],
    inference_mode=True,
    r=8,
    lora_alpha=32,
    lora_dropout=0.1
)
# 加载lora权重
model = PeftModel.from_pretrained(model, model_id=model_path_loar, config=config)
# print(model)

# 定义数据具体处理逻辑
def collate_fn(batch):
    texts = [item["text"] for item in batch]
    labels = [item["label"] for item in batch]
    encoding = tokenizer(texts, padding=True, truncation=True, max_length=64, return_tensors="pt")
    encoding["labels"] = torch.tensor(labels)
    return encoding


batch_size = model_config.test_batch_size
# 创建Dataset对象
dataset = Dataset.from_dict({"text": texts, "label": labels})
data_loader = DataLoader(dataset, batch_size=batch_size, collate_fn=collate_fn)

predictions = []
for batch in data_loader:
    inputs = {k: v for k, v in batch.items() if k != "labels"}
    with torch.no_grad():
        outputs = model(**inputs)

    logits = outputs.logits
    if model.config.num_labels > 2:
        # 多分类任务,取概率最高的类别
        batch_predictions = torch.argmax(logits, dim=1).tolist()
    else:
        # 二分类任务,取大于0.5的概率作为正类
        batch_predictions = (logits > 0.5).squeeze().tolist()

    predictions.extend(batch_predictions)

# 计算准确度、精确度和召回率
accuracy = accuracy_score(labels, predictions)
precision = precision_score(labels, predictions, average="weighted", zero_division=0)
recall = recall_score(labels, predictions, average="weighted", zero_division=0)

# 输出结果
print(f"Accuracy: {accuracy:.4f}")
print(f"Precision: {precision:.4f}")
print(f"Recall: {recall:.4f}")

# 绘制混淆矩阵
cm = confusion_matrix(labels, predictions)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", cbar=False)
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.title("Confusion Matrix")
plt.savefig("img/confusion_matrix_lora.png")
plt.show()

训练可视化

模型重训练在完整的代码仓中,这里仅将结果展示出来,训练参数都是一致的。
重训练的评估过程如下:
在这里插入图片描述训练损失如下:
在这里插入图片描述训练时长:1746.2688 s
训练的损失一直在降低,而评估数据集出现类似抛物线的走势,可以知道此时已经发生过拟合了。

LoRA微调训练的评估过程如下:
在这里插入图片描述训练损失如下:

在这里插入图片描述
训练时长:3140.1472s
训练的损失一直在降低,而评估数据集也是趋于一个收敛的状态,并没有发生过拟合的现象。

评估结果可视化

通过比较LoRA微调和重训练的一个混淆矩阵可以看出,LoRA的评估指标还是会稍逊全量微调的
全量微调的混淆矩阵:
在这里插入图片描述
评估指标 含义参考 分类模型常用的评估指标
Accuracy: 0.9370
Precision: 0.9369
Recall: 0.9370

LoRA微调的混淆矩阵:
在这里插入图片描述
Accuracy: 0.9325
Precision: 0.9327
Recall: 0.9325
在模型表现上LoRA微调会比全量训练是会有一定的差距,但是并不明显。如果是大型预训练模型那么进行重训练是比较困难的,通过这种技术可以使得微调大模型成为一种可能,并且在只关注模型在特定任务上的表现还是可以接收的。

Logo

更多推荐