Bootstrap

大模型微调---qwen实战

一、Qwen大模型的介绍

Qwen是阿里云开发的大语言模型,整个qwen系列的模型,由base模型、rm模型、chat模型、code模型、math模型等等。
[图片]

qwen采用chatml样式的格式来进行模型训练,chatml格式可以时模型有效区分各类信息,可以增强模型对复杂会话的处理分析能力。chatml的核心要素:

  • 消息(message):对话交互的基本单元,由角色和内容组成
  • 角色(Role):消息的发送方,可以是人(human)或者助手(assistant)
  • 内容(content):消息的具体内容,以markdown格式表示
  • 会话(conversation):由一系列消息构成的完整对话过程。
    在这里插入图片描述

大语言模型的训练过程包括:预训练阶段、监督训练阶段、RM奖励阶段、RL阶段。
预训练阶段
预训练数据集共3T,预训练权重数据主要涉及公共网络文档、百科全书、书籍、代码等,数据涉及多语言,以中文和英文为主。Qwen遵循自回归语言建模的标准方法(大模型的预训练阶段通常采用一种称为"自监督学习"的技术,例如掩码语言模型或者因果语言模型。掩码语言模型会将句子中某些词语遮挡住,然后让模型根据上下文预测这些被遮挡的词语;而因果语言模型则会让模型根据前面的词语预测下一个词语),通过前面Token的内容预测下一个Token。
预训练的目的是让模型学习通用的语言知识,使其成为一个优秀的语言编码器。然而,预训练模型通常缺乏针对特定任务或领域的知识。为了解决这个问题,我们需要进行下一步:微调。
SFT监督阶段
SFT是监督微调(Supervised Fine-Tun-ing)的缩写。相对于预训练的无监督微调,监督微调首先需要大量的标注数据用于目标任务的微调,如果标注数据不足,可能会导致微调后的模型表现不佳。其次,由于预训练模型的参数和结构对微调后的模型性能有很大的影响,因此,选择合适的预训练模型很重要。其中,微调方法中常见的是lora,本章也以lora微调方法作为实战演练。
RM奖励阶段
在大语言模型完成SFT监督微调后,下一阶段是构建一个奖励模型来对问答作出得分评价。奖励模型源于强化学习中的奖励函数,能对当前的状态刻画了一个分数,来说明这个状态产生的价值有多少。在大语言模型微调中的奖励模型是对输入的问题和答案计算出一个分数。输入的答案与问题匹配度越高,则奖励模型输出的分数也越高。
奖励模型(RM模型)讲SFT模型最后一层的softmax去掉,即最后一层不用softmax,改成一个线性层。RM模型的输入是问题和安安,输出是一个标量即分数。
奖励模型的训练数据是人工对问题的每个答案进行排名,如下图所示:
在这里插入图片描述

对于每个问题,给出若干答案,然后工人进行排序,而奖励模型就是利用排序的结果来进行反向传播。奖励模型的损失函数采用 Pairwise Ranking Loss(目标是使得排序高的答案yw对应的标量分数要高于排序低的答案y1对应的标量分数,且越高越好)
总之,奖励模型通过人类进行交互,获得对于生成响应质量的反馈信息,从而进一步提升大模型的生成能力和自然度。与监督模型不同的是,奖励模型通过打分的形式使得生成的文本更加自然逼真,让大模型的生成能力更进一步。
RL模型阶段
大语言模型完成奖励模型的训练后,下一个阶段是训练强化学习模型(RL模型),也是最后一个阶段。大语言模型微调中训练RL模型采用的优化算法是PPO(Proximal Policy Optimization,近端策略优化)算法,即对设定的目标函数通过随机梯度下降进行优化。近端策略优化是一种深度强化学习算法,用于训练智能体在复杂环境中学习和执行任务。通过智能体的训练,使得其在与环境的交互中能够最大化累积汇报,从而达成指定任务目标。这里的智能体在大模型中指的是就是RL模型
通过强化学习的训练方法,迭代式的更新奖励模型(RW 模型)以及策略模型(RL 模型),让奖励模型对模型输出质量的刻画愈加精确,策略模型的输出则愈能与初始模型拉开差距,使得输出文本变得越来越符合人的认知。这种训练方法也叫做 RLHF。

二、基于lora的qwen微调实战

1. 配置环境

本案例测试于modelscope1.14.0、transformers4.41.2、datasets2.18.0、peft0.11.1、accelerate0.30.1、swanlab0.3.11,配置环境的命令如下:

conda create --name Qwen2 python=3.8
conda activate Qwen2
pip install swanlab modelscope==1.14.0 transformers==4.41.2 datasets==2.18.0 peft==0.11.1 pandas accelerate==0.30.1
  1. 数据集下载
    本案例使用zh_cls_fudan-news数据集,该数据集主要被用于训练文本分类模型。
    zh_cls_fudan-news由几千条数据,每条数据包含text、category、output三列:
  • text 是训练语料,内容是书籍或新闻的文本内容
  • category 是text的多个备选类型组成的列表
  • output 则是text唯一真实的类型
    [图片]

数据集的下载网址:zh_cls_fudan-news,下载代码如下:

from modelscope import MsDataset
dataset = MsDataset.load('huangjintao/zh_cls_fudan-news', split='train')
test_dataset = MsDataset.load('huangjintao/zh_cls_fudan-news', subset_name='test', split='test')
print(dataset)
print(test_dataset)
  1. 完整的训练代码
import json
import pandas as pd
import torch
from datasets import Dataset
from modelscope import snapshot_download, AutoTokenizer
from swanlab.integration.huggingface import SwanLabCallback
from peft import LoraConfig, TaskType, get_peft_model
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer, DataCollatorForSeq2Seq
import os
import swanlab

def dataset_jsonl_transfer(origin_path, new_path):
    """
    将原始数据集转换为大模型微调所需数据格式的新数据集
    """
    messages = []
    # 读取旧的JSONL文件
    with open(origin_path, "r") as file:
        for line in file:
            # 解析每一行的json数据
            data = json.loads(line)
            context = data["text"]
            catagory = data["category"]
            label = data["output"]
            message = {
                # "instruction": 用于告诉模型它将扮演的角色
                # "input": 用户输入的文本
                # "output": 模型输出的文本标签
                "instruction": "你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项,请输出文本内容的正确类型",
                "input": f"文本:{context},类型选型:{catagory}",
                "output": label,
            }
            messages.append(message)

    # 保存重构后的JSONL文件
    with open(new_path, "w", encoding="utf-8") as file:
        for message in messages:
            file.write(json.dumps(message, ensure_ascii=False) + "\n")
            
            
def process_func(example):
    """
    将数据集进行预处理
    """
    # 设置最大序列长度:MAX_LENGTH = 384 限制了模型输入序列的最大长度
    MAX_LENGTH = 384 
    # 初始化ID列表:初始化三个列表input_ids, attention_mask, labels,分别用于存放模型所需的输入Token ID、注意力掩码以及标签(输出Token ID)。
    input_ids, attention_mask, labels = [], [], []
    instruction = tokenizer(
        f"<|im_start|>system\n你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项,请输出文本内容的正确类型<|im_end|>\n<|im_start|>user\n{example['input']}<|im_end|>\n<|im_start|>assistant\n",
        add_special_tokens=False,
    )
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    # input_ids: 将分词后的指令(instruction)和响应(response)的input_ids合并,并在末尾添加一个填充标记,以此构造完整的输入序列
    # attention_mask: 用来指示哪些tokens应当被模型关注(值为1的部分),哪些应当被忽略(值为0的部分)
    # 在自然语言处理领域,文本数据需要被转换成模型可以理解的数字形式---也就是数字序列。这些数字是词汇表中的索引。
    # 当一个文本处理器(如tokenizer)处理一段文本时,它会将文中的单词、字符或子词映射到一个唯一的数字ID,这个过程为"令牌化"。
    # 词汇表是模型训练之初,基于训练数据建立的,每个不同的词,标点符号、特殊字符等都有一个对应的ID。
    # input_ids数组或列表中的每一个数字代表原文本中的一个token(可能是词、子词或其他单位)在词汇表中的位置。这样做不仅使得文本可以被编码为神经网络可以运算的形式,还方便了模型理解和生成文本。

    # 至于attention_mask,它的作用在于帮助模型区分有效输入(即实际的文本内容)与填充内容(pad tokens)。模型在计算自注意力或交互时,会利用attention_mask来确定哪些部分需要关注(通常值为1),哪些部分(比如为了对齐长度而填充的tokens)应该被忽略(通常值为0)。
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    attention_mask = (
        instruction["attention_mask"] + response["attention_mask"] + [1]
    )
    # 对于labels,则用-100标记源序列的token(因为这些不是预测的目标,而是已知的输入),然后跟上响应序列的真实token ID,最后也是填充标记。
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]
    print(len(instruction["input_ids"]))
    # exit()
    #如果构建好的input_ids长度超过MAX_LENGTH,则会进行截断操作。
    if len(input_ids) > MAX_LENGTH:  # 做一个截断
        input_ids = input_ids[:MAX_LENGTH]
        attention_mask = attention_mask[:MAX_LENGTH]
        labels = labels[:MAX_LENGTH]
    return {"input_ids": input_ids, "attention_mask": attention_mask, "labels": labels}   

def predict(messages, model, tokenizer):
    device = "cuda"
    text = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True
    )
    model_inputs = tokenizer([text], return_tensors="pt").to(device)

    generated_ids = model.generate(
        model_inputs.input_ids,
        max_new_tokens=512
    )
    generated_ids = [
        output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
    ]
    
    response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
    
    print(response)
     
    return response
    

# 在modelscope上下载Qwen模型到本地目录下
model_dir = snapshot_download("qwen/Qwen2-1.5B-Instruct", cache_dir="./", revision="master")

# Transformers加载模型权重
tokenizer = AutoTokenizer.from_pretrained("./qwen/Qwen2-1___5B-Instruct/", use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained("./qwen/Qwen2-1___5B-Instruct/", device_map="auto", torch_dtype=torch.bfloat16)
model.enable_input_require_grads()  # 开启梯度检查点时,要执行该方法

# 加载、处理数据集和测试集
train_dataset_path = "zh_cls_fudan-news/train.jsonl"
test_dataset_path = "zh_cls_fudan-news/test.jsonl"

train_jsonl_new_path = "new_train.jsonl"
test_jsonl_new_path = "new_test.jsonl"

if not os.path.exists(train_jsonl_new_path):
    dataset_jsonl_transfer(train_dataset_path, train_jsonl_new_path)
if not os.path.exists(test_jsonl_new_path):
    dataset_jsonl_transfer(test_dataset_path, test_jsonl_new_path)

# 得到训练集
train_df = pd.read_json(train_jsonl_new_path, lines=True)
train_ds = Dataset.from_pandas(train_df)
train_dataset = train_ds.map(process_func, remove_columns=train_ds.column_names)

config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    inference_mode=False,  # 训练模式
    r=8,  # Lora 秩
    lora_alpha=32,  # Lora alaph,具体作用参见 Lora 原理
    lora_dropout=0.1,  # Dropout 比例
)

model = get_peft_model(model, config)

args = TrainingArguments(
    output_dir="./output/Qwen2_0.5",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=20,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
    report_to="none",
)

swanlab_callback = SwanLabCallback(
    project="Qwen2-fintune",
    experiment_name="Qwen2-0.5B-Instruct",
    description="使用通义千问Qwen2-0.5B-Instruct模型在zh_cls_fudan-news数据集上微调。",
    config={
        "model": "qwen/Qwen2-0.5B-Instruct",
        "dataset": "huangjintao/zh_cls_fudan-news",
    }
)

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=train_dataset,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
    callbacks=[swanlab_callback],
)

trainer.train()

# 用测试集的前10条,测试模型
test_df = pd.read_json(test_jsonl_new_path, lines=True)[:10]

test_text_list = []
for index, row in test_df.iterrows():
    instruction = row['instruction']
    input_value = row['input']
    
    messages = [
        {"role": "system", "content": f"{instruction}"},
        {"role": "user", "content": f"{input_value}"}
    ]

    response = predict(messages, model, tokenizer)
    messages.append({"role": "assistant", "content": f"{response}"})
    result_text = f"{messages[0]}\n\n{messages[1]}\n\n{messages[2]}"
    test_text_list.append(swanlab.Text(result_text, caption=response))
    
swanlab.log({"Prediction": test_text_list})
swanlab.finish()

当运行上面代码出现“段错误”,重新运行即可。
如果你是第一次使用SwanLab,那么还需要去swanlab.cn上注册一个账号,在用户设置页面复制你的API Key,然后在训练开始时粘贴进去即可

4.推理代码

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

def predict(messages, model, tokenizer):
    device = "cuda"

    text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    model_inputs = tokenizer([text], return_tensors="pt").to(device)

    generated_ids = model.generate(model_inputs.input_ids, max_new_tokens=512)
    generated_ids = [output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)]
    response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]

    return response

# 加载原下载路径的tokenizer和model
tokenizer = AutoTokenizer.from_pretrained("./qwen/Qwen2-1___5B-Instruct/", use_fast=False, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained("./qwen/Qwen2-1___5B-Instruct/", device_map="auto", torch_dtype=torch.bfloat16)

# 加载训练好的Lora模型,将下面的checkpointXXX替换为实际的checkpoint文件名名称
model = PeftModel.from_pretrained(model, model_id="./output/Qwen2/checkpointXXX")

test_texts = {
    'instruction': "你是一个文本分类领域的专家,你会接收到一段文本和几个潜在的分类选项,请输出文本内容的正确类型",
    'input': "文本:航空动力学报JOURNAL OF AEROSPACE POWER1998年 第4期 No.4 1998科技期刊管路系统敷设的并行工程模型研究*陈志英* * 马 枚北京航空航天大学【摘要】 提出了一种应用于并行工程模型转换研究的标号法,该法是将现行串行设计过程(As-is)转换为并行设计过程(To-be)。本文应用该法将发动机外部管路系统敷设过程模型进行了串并行转换,应用并行工程过程重构的手段,得到了管路敷设并行过程模型。"
}

instruction = test_texts['instruction']
input_value = test_texts['input']

messages = [
    {"role": "system", "content": f"{instruction}"},
    {"role": "user", "content": f"{input_value}"}
]

response = predict(messages, model, tokenizer)
print(response)

参考链接:
https://zhuanlan.zhihu.com/p/644174810
https://zhuanlan.zhihu.com/p/642281542
https://blog.csdn.net/CSDN_224022/article/details/141193636

;