Qwen-3微调全解:借助Python和Unsloth创建专属AI模型

2个月前发布 gsjqwyl
18 0 0

文章标题:

Qwen-3微调全攻略:运用Python与Unsloth打造专属AI模型

文章内容:

当下,众人皆聚焦于在DeepSeek上搭建应用,而聪慧的开发者们却已悄然洞察到Qwen-3的微调功能,这可谓是一处待挖掘的宝藏,能将通用型人工智能转变为契合你专属需求的数字专家。

通过本篇文章,你将掌握针对特定用途微调最新Qwen-3模型的方法。无论你是初涉AI领域的新手,还是经验丰富的AI工程师,都能从中获取适合自己的内容。

None
Qwen3迅速成为众多开发者的首选。其广受欢迎的缘由在于它在编码、数学、通用能力等竞争性评估中斩获的基准分数。

这些基准分数超越了主要的大语言模型,包括DeepSeek-R1、o1、o3-mini、Grok-3以及Gemini-2.5-Pro等模型。此外,小MoE模型Qwen3–30B-A3B在激活参数数量上是Qwen-32B的十倍之多,甚至像Qwen3–4B这样的小模型也能与Qwen2.5–72B-Instruct的性能相媲美。

None

Qwen-3模型基准

你可通过这里了解更多关于基准及其在特定任务中的表现。

在本篇文章中,你将深入探究如何运用Python和Unsloth对Qwen-3模型进行微调。

尽管微调是一项计算成本较高的任务,但本文借助Google的Colab Notebook,尽量使其易于上手。

条件与设置

首先来了解微调Qwen-3所需的条件。涵盖技术要求和设置要求的简要介绍。

Python库与框架

以下是微调Qwen-3模型所需的Python库和框架:

  • unsloth,该包能使Llama-3、Mistral、Gemma和Qwen等大语言模型的微调速度提升一倍,内存使用减少70%,且不降低准确性!你可在此处了解更多详情
  • torch,是使用PyTorch进行深度学习的基础包。它提供强大的张量库,类似NumPy,但具备GPU加速优势,这对处理大语言模型至关重要。
  • transformers是强大且流行的开源自然语言处理(NLP)库。它为各种先进的预训练模型提供易用接口。由于预训练模型是任何微调任务的基础,该包有助于便捷地访问训练好的模型。
  • trl包是专门用于强化学习(Reinforcement Learning, RL)与变换器模型的Python库。它基于Hugging Face的transformers库构建,利用其优势,使变换器的强化学习更易访问且高效。
计算需求

微调大语言模型(LLM)是一种技术,可在不进行完整(参数)训练的情况下,让模型的响应更具结构化且贴合领域。

然而,对于大多数普通计算机硬件而言,微调大语言模型仍不现实,因为所有可训练参数以及实际的大语言模型都存储在GPU的vRAM(虚拟RAM)中,而大语言模型的巨大规模是实现这一目标的主要阻碍。

因此,在本文中,我们将微调Qwen-3的量化版本,该版本有80亿参数。此大语言模型约需8 – 12 GB的vRAM,为使所有学习者都能参与,我们将使用Google Colab的免费T4 GPU,其有15 GB的vRAM。

数据准备策略

对于微调大语言模型,需要结构化且贴合任务的数据。有诸多数据准备策略,无论是从社交媒体平台、网站、书籍还是研究论文中抓取数据。

为微调我们的Qwen-3模型,将使用推理数据集和通用聊天交互数据集。如此,将为我们的大语言模型赋予增强的推理能力和改进的提示理解能力。

这两个数据集将从开源的Hugging Face Hub加载。我们将使用unsloth/OpenMathReasoning-minimlabonne/FineTome-100k数据集。

在此,unsloth/OpenMathReasoning-mini将增强我们模型的推理和解决问题能力,而mlabonne/FineTome-100k将提升通用对话能力。

Python实现

安装包

你需在内核中运行以下命令:

!pip install --no-deps bitsandbytes accelerate xformers==0.0.29.post3 peft trl==0.15.2 triton cut_cross_entropy unsloth_zoo
!pip install sentencepiece protobuf datasets huggingface_hub hf_transfer
!pip install --no-deps unsloth

若你有性能强劲的GPU且愿意在本地机器上进行微调任务,此过程相当简便。要安装该包,只需在终端运行以下命令:

!pip install unsloth
初始化LLM模型及其分词器

我们将使用unsloth包加载预训练模型。除了更快的下载速度外,它还提供有助于微调大语言模型的实用技术。

初始化模型和分词器的代码如下:

from unsloth import FastLanguageModel
import torch

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Qwen3-8B-unsloth-bnb-4bit",
    max_seq_length = 2048,   # 上下文长度
    load_in_4bit = True,     # 4bit使用更少内存
    load_in_8bit = False,    # 稍准确但内存使用翻倍
    full_finetuning = False, # 现在可进行全微调!
    # token = "<YOUR_HF_TOKEN>", # 若使用受限模型
)
  • 我们正通过Hugging Face Hub使用FastModel.from_pretrained()方法加载预训练的Qwen3–8B模型。
  • 第一个参数是model_name,即unsloth/Qwen3–8B-unsloth-bnb-4bit,这是Qwen-3模型的80亿参数版本,契合我们的需求。
  • 通过设置max_seq_length,我们能处理模型的序列长度,允许模型处理2048个标记的输入序列。它也会影响模型性能、内存使用和准确性。
  • load_in_4bit参数用于将模型量化为4位精度以减少内存使用量,若你的GPU支持,可将load_in_8bit设为True,因其会提高准确性但内存成本翻倍。
  • full_finetuning标志设为False,使我们能进行参数高效微调(PEFT)而非更新所有模型参数。
None

初始化Qwen-3模型和分词器

添加LoRA适配器

我们将为预训练的Qwen-3模型添加LoRA矩阵,这有助于微调模型的响应。使用unsloth,整个过程只需几行代码。

代码如下:

model = FastLanguageModel.get_peft_model(
    model,
    r = 32,
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 64,
    lora_dropout = 0,
    bias = "none",
    use_gradient_checkpointing = "unsloth", # True或"unsloth"用于非常长上下文
    random_state = 3433,
)

代码解释:

  • FastModel.get_peft_model()方法将参数高效微调(PEFT)应用于我们刚初始化的Qwen-3基础模型以实现高效适配。
  • r参数(秩)控制LoRA适配矩阵的大小,值越高可提高准确性但可能导致过拟合。lora_alpha作为缩放因子,通常设为r的相等或两倍。
  • 我们将lora_dropout设为0表示不应用dropout,bias="none"表示不微调偏置。
  • 设置random_state=3433如同给新模型留下指纹,确保微调过程中结果的一致性和可重复性。
数据准备

现在,我们已在预训练的Qwen-3模型上设置了LoRA适配器。接下来可开始准备用于训练模型的数据。

我们将使用unsloth/OpenMathReasoning-minimlabonne/FineTome-100k数据集微调模型,并从Hugging Face加载数据集。

加载数据集的代码如下:

from datasets import load_dataset
reasoning_dataset = load_dataset("unsloth/OpenMathReasoning-mini", split = "cot")
non_reasoning_dataset = load_dataset("mlabonne/FineTome-100k", split = "train")

在此,数据集unsloth/OpenMathReasoning-mini将用于增强大语言模型的推理能力,为此只需数据的COT(Chain-Of-Thought,思维链)部分。

现在需将数据结构标准化以匹配聊天式微调的预期格式,例如处理“用户”和“助手”角色。此步骤也可用于生成对话式输入以便将查询传递给数据集中的问题,从而对齐实际传递给LLM的查询方式。

标准化数据集的代码如下:

def generate_conversation(examples):
    problems  = examples["problem"]
    solutions = examples["generated_solution"]
    conversations = []
    for problem, solution in zip(problems, solutions):
        conversations.append([
            {"role" : "user",      "content" : problem},
            {"role" : "assistant", "content" : solution},
        ])
    return { "conversations": conversations, }

该函数generate_conversation将单独的查询及其各自的解决方案转换为包含用户和助手响应的对话。

现在需将此函数映射到实际数据集上,代码如下:

reasoning_conversations = tokenizer.apply_chat_template(
    reasoning_dataset.map(generate_conversation, batched = True)["conversations"],
    tokenize = False,
)

现在还需处理加载的非推理数据集。为此将使用UnSloth聊天模板库中的standardize_sharegpt函数修正数据集格式。

from unsloth.chat_templates import standardize_sharegpt
dataset = standardize_sharegpt(non_reasoning_dataset)

non_reasoning_conversations = tokenizer.apply_chat_template(
    dataset["conversations"],
    tokenize = False,
)
数据比例

现已准备好两个数据集,通常此时可开始训练模型,但还应考虑模型的聊天与推理比例

较高的聊天比例优先考虑对话流畅性和一般知识,较高的推理比例强调逻辑推理和解决问题能力。在两者间取得平衡对创建既能进行有趣对话又能解决复杂任务的多功能模型至关重要。

在本文中假设想要一个聊天模型,因此将聊天部分设为70%,推理部分设为30%。实现方法如下:

import pandas as pd

# 定义聊天部分比例
chat_percentage = 0.7

# 按比例对非推理数据集采样
non_reasoning_subset = pd.Series(non_reasoning_conversations)
non_reasoning_subset = non_reasoning_subset.sample(
    int(len(reasoning_conversations) * (1.0 - chat_percentage)),
    random_state = 2407,
)

现在数据准备过程的最后一步是合并两个数据集,代码如下:

data = pd.concat([
    pd.Series(reasoning_conversations),
    pd.Series(non_reasoning_subset)
])
data.name = "text"

from datasets import Dataset
combined_dataset = Dataset.from_pandas(pd.DataFrame(data))
combined_dataset = combined_dataset.shuffle(seed = 3407)

训练

现已准备好结构化数据和带有LoRA适配器或矩阵的模型,可开始训练模型。

为训练模型需初始化一些超参数,这些参数将影响训练过程及模型准确性。

将使用SFTTrainer初始化一个trainer并设置超参数。

from trl import SFTTrainer, SFTConfig
trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = combined_dataset, # 结构化数据集
    eval_dataset = None,
    args = SFTConfig(
        dataset_text_field = "text", # 数据集中用于训练的结构化字段
        per_device_train_batch_size = 2, # 每个设备每个批次处理的样本数量
        gradient_accumulation_steps = 4, # 执行反向传播前累积梯度的步数
        warmup_steps = 5, # 训练开始时逐步增加学习率的步数
        # num_train_epochs = 1, # 设置为1进行完整训练运行
        max_steps = 30, # 要执行的总训练步数
        learning_rate = 2e-4, # 训练期间更新权重的学习率
        logging_steps = 1, # 训练指标记录频率(以步数为单位)
        optim = "adamw_8bit", # 优化器
        weight_decay = 0.01, # 防止过拟合的正则化
        lr_scheduler_type = "linear", # 控制学习率衰减
        seed = 3407,
        report_to = "none", # 记录指标的平台,也可为'wandb'
    ),
)

代码解释:

  • SFTTrainertrl库中用于在自定义数据集上微调大语言模型的工具。它提供如梯度累积、混合精度优化等技术,非常适合指令微调、对话生成及特定领域大语言模型适配等任务。
  • SFTTrainer中传入模型、分词器和刚准备好的训练数据集,将评估数据集设为None以满足用例。

现已完成所有设置,模型准备好开始训练。开始训练的代码如下:

trainer_stats = trainer.train()

这将在内核中每训练一步打印训练损失,如下所示:

None

训练带有LoRA适配器的Qwen3模型

推理

现已完成模型训练,接下来对微调后的模型进行推理以评估其响应。

有两种推理方法,一种是启用思考,另一种是禁用思考。

不思考模式

禁用思考时对模型进行推理的代码如下:

messages = [
    {"role" : "user", "content" : "Solve (x + 2)^2 = 0."}
]
text = tokenizer.apply_chat_template(
    messages,
    tokenize = False,
    add_generation_prompt = True, # 必须添加以进行生成
    enable_thinking = False, # 禁用思考
)

from transformers import TextStreamer
_ = model.generate(
    **tokenizer(text, return_tensors = "pt").to("cuda"),
    max_new_tokens = 256, # 增加此值获更长输出
    temperature = 0.7, top_p = 0.8, top_k = 20,
    streamer = TextStreamer(tokenizer, skip_prompt = True),
)
  • 在此为对模型推理,初始化分词器以处理Qwen3聊天格式/模板中的对话。
  • chat_template应用于消息(输入)时明确指定模型响应时不进行思考。
  • max_new_token设为256,可根据需要调整以获更长输出。

此推理输出如下:

None

禁用思考时微调后的Qwen-3模型的推理输出。

若图片不清晰,以下是文字版输出内容:

<|im_start|> To solve the equation (x + 2)² =0, we can take the square
root of the both sides. This gives us x+2 = 0. Then, we can subtract 2 from
both sides to get x = -2. <|im_end|>

思考模式

刚看到模型不思考时直接给出答案,这是期望的!

禁用思考有优缺点,虽减少计算需求并加快输出生成速度,但解决复杂问题时不可靠——此时需模型思考。

要启用思考以进行响应,需按以下方式操作:

“`python
messages = [
{“role” : “user”, “content” : “Solve (x + 2)^

© 版权声明

相关文章

没有相关内容!

暂无评论

none
暂无评论...