Unsloth微调

微信公众号

数据处理的工程化时间

  • 数据清洗:去除噪声数据(比如乱码/重复文本),对不平衡数据进行重采样
  • 高效预处理:使用 HuggingFace Datasets 库实现流水线处理
  • 内存优化:对于超大规模数据集,我这里建议使用内存映射文件(MMAP)技术

LoRA各种策略

减少显存占用

bitsandbytes库简介

  • 通过将权重从 FP32 或 FP16 量化为更低比特的表示(如 4-bit、8-bit),大大减小模型的显存占用,从而让你能够在同样的硬件环境下加载更大的模型,或者用相同规模的模型获得更高批量大小(batch size)。
# 这里推荐使用 bitsandbytes 量化库降低显存占用
from transformers import BitsAndBytesConfig

quant_config = BitsAndBytesConfig(
    load_in_4bit=True, # 将模型权重量化为4-bit
    bnb_4bit_use_double_quant=True # 在量化时进行双重量化过程,可以进一步减少内存暂用、降低量化误差
)

model = AutoModel.from_pretrained("Llama-2-7b", quantization_config=quant_config)
  • BitsAndBytesConfig 是一个在 Hugging Face transformers 中用来配置 bitsandbytes 量化参数的类。你可以在创建模型时,将这个配置以 quantization_config 参数的形式传给 from_pretrained() 方法。
训练过程的精细化控制

学习率的三阶段策略

  • 预热阶段(前 10% steps):线性增长至 2e-5
    • 学习率从一个较低的初始值(通常可以是 0,也可以是非常小的值)线性上升到目标最大学习率(例:2e-5)。
    • 在训练的前期,让学习率从0(或一个小值)线性增长到设定的最大值(2e-5),可以避免训练初期学习率过大而导致梯度爆炸,同时也能让模型在一开始快速适应任务。
      • 常见做法:Warmup Steps 占总训练步数的 5% ~ 10%。
      • 参数:num_warmup_steps 表示预热步数。
  • 稳定阶段:余弦退火调节(中间 85% 的训练步数)
    • 学习率围绕最大值做余弦退火,从而逐渐衰减。
    • 在剩余的训练步数中,学习率会按照余弦曲线从最大学习率逐渐衰减到一个较低的值。余弦退火相较线性衰减,有时可以带来更平滑的效果,减轻在训练后期振荡过大的问题。
      • 对于 get_cosine_schedule_with_warmup,当超过 num_warmup_steps 后,学习率会根据余弦函数从当前最大值逐渐衰减至 0(或者一个 eta_min,依不同实现而定)。
  • 微调阶段(最后 5% steps):降至 1e-6
    • 在训练接近尾声时,将学习率下降到更小的值(例:1e-6),可以用线性或者继续让余弦退火到一个更低的值,帮助模型在后期收敛得更平滑或做小步调整。
optimizer = AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)
scheduler = get_cosine_schedule_with_warmup(
    optimizer, 
    num_warmup_steps=100, # 预热步数
    num_training_steps=1000 # 整个训练过程的步数
)
  • AdamW 是 Adam 优化器的一个变体,融合了 L2 正则/权重衰减(Weight Decay)在更新中的合理处理方式。
显存优化的三大技巧

下面我们就来详细探讨一下深度学习训练过程中常用的 显存优化 手段。这些方法在大模型微调(Fine-tuning)、分布式训练或硬件资源有限的情况下尤其常见。你提到的三大技巧包括:

1. 梯度累积(Gradient Accumulation)
1.1 原理

在默认的训练循环中,每处理一个批次(batch)后会立即计算梯度并进行一次参数更新。如果你的 GPU 显存有限,无法一次性放下较大的 batch,那么就可以通过“多次前向 + 反向”累积梯度,再进行一次参数更新,从而“模拟”较大批次。

简而言之:

  • 正常训练:1批次 → 1次反向传播 → 1次优化器更新
  • 梯度累积:N个批次 → 累计梯度N次 → 1次优化器更新
1.2 具体做法

使用 Hugging Face TrainingArguments 时,可以设置 gradient_accumulation_steps。比如:

training_args = TrainingArguments(
    ...,
    gradient_accumulation_steps=4
)

这表示在内部会将每 4 个 batch 的梯度相加后再执行一次 optimizer.step()。这样做的好处是:

  • 在同等显存下,相当于使用了“4 倍批次大小”的效果;
  • 对一些需要较大 batch size 才能收敛更好的任务(如语言模型预训练、特定 NLP 任务等),这是一个关键技巧。
1.3 注意事项
  1. 有效学习率:当你使用梯度累积时,相当于增大了批次大小,需要注意保持或调整学习率。例如,如果你原先 batch size=16,现在用 gradient_accumulation_steps=4 并把单次 batch size 改成4,总有效 batch size 就依然是16。这样你的学习率设置就不用大幅修改。
  2. 训练时间:梯度累积并不会减少计算量,对每个batch依然要做前向和反向,但它能帮助你在有限显存下进行较大的 batch 训练。
  3. 分布式训练配合:如果同时使用分布式训练(DDP),批次大小会再乘以 worker 数,需综合计算。

2. 混合精度训练(Mixed Precision)
2.1 原理

混合精度训练指的是使用更低的数值表示(如 FP16 / BF16)来存储和计算模型的部分权重、激活或梯度,从而减小显存占用并加速运算。

  • FP16 (float16):半精度浮点,可在大多数现代 GPU(如 V100、A100、T4 等)上运行。
  • BF16 (bfloat16):Google 提出的 16-bit 格式,数值范围比 FP16 更大,减少梯度溢出的风险,在 NVIDIA A100 等支持 bfloat16 的硬件上效果更佳。
2.2 在 Hugging Face 中的使用

TrainingArguments 中可以直接启用混合精度。

  • FP16

    training_args = TrainingArguments(
        ...,
        fp16=True
    )
    
  • BF16(如果硬件支持,如 A100):

    training_args = TrainingArguments(
        ...,
        bf16=True
    )
    

注意:同一时间你只能启用一种精度模式,比如只能启用 fp16=Truebf16=True 之一。

2.3 优势与注意点
  1. 显存占用降低:以 FP16 为例,理论上可以减少约一半的激活/梯度显存。
  2. 速度提升:现代 GPU 对半精度计算有 Tensor Cores 等硬件加速,推理和训练都能加速。
  3. 数值稳定性:
    • FP16 的动态范围较小,可能导致梯度爆炸/下溢等问题,通常需要配合 梯度缩放(Gradient Scaling) 来保持稳定。Hugging Face 的 Trainer 和 PyTorch AMP 会自动处理。
    • BF16 对数值范围更友好,一般更稳定一些。如果硬件支持,BF16 通常优于 FP16。

3. 激活检查点(Activation Checkpointing)
3.1 原理

在前向传播时,模型会生成中间激活值,这些激活在反向传播时需要用来计算梯度。激活检查点技术的核心思想是:在前向传播中,不保存所有中间激活(以节省显存),而是在反向传播时重新计算一部分激活(以增加一些计算量)。

  • 内存与计算间的权衡:牺牲额外的计算时间来换取显存的节省。
3.2 在 Hugging Face 中的使用

transformers 为例,如果模型支持激活检查点,一般可以用:

model.gradient_checkpointing_enable()

其中 model 通常是 AutoModel 或者类似基于 transformers 的自定义模型。在开启后,对支持该功能的某些层(如 GPT-2、BERT 等 Transformer 层),会把它们的前向计算拆分成多个段,并在反向时重新前向计算来获取激活。

3.3 注意事项
  1. 训练速度:激活检查点会增加额外的前向计算,训练速度会变慢,一般可达 20% ~ 30% 的速度损失。
  2. 层数越多,效果越明显:对于深层模型,激活数量大,通过 checkpointing 可以显著减少内存占用;浅层模型则收益有限。
  3. 兼容性:大多数 Transformer 模型默认都支持该特性,但需要确认你所使用的模型类型中是否实现了 gradient checkpointing。

4. 综合对比与搭配使用
  1. 梯度累积
    • 主要目标:模拟更大 batch size,提升收敛表现或满足大 batch 需求;
    • 内存/显存节省:可以在一个较小的 batch 大小下实现等效大 batch 的效果,不会额外节省激活占用(因为每次 forward 依然用同样的计算图),但你无需在一次forward就放下一整个大batch;
    • 影响训练速度:没有直接加速或减速,只是分多次迭代同一个真实 batch,总体计算量不变;但确实提升了内存使用效率。
  2. 混合精度
    • 主要目标:减少模型参数、激活和梯度的存储占用,并利用硬件加速让训练更快
    • 常用:建议总是尝试使用混合精度(尤其是有 Tensor Core 的新 GPU),通常可以显著减小内存/显存,提升速度。
  3. 激活检查点
    • 主要目标:在前向时不保存全部激活,在反向时重算,显著减少激活占用;
    • 适用场景:当模型极深且显存不足,而训练又必须使用较大 batch size 或较长序列长度时;
    • 带来的折中:多次前向计算导致训练速度变慢
4.1 同时使用

这三种技术可以协同使用(也是大模型训练中常见的组合):

  • 梯度累积 + 混合精度:非常常见,对显存并不充足的环境很有效;
  • 混合精度 + 激活检查点:在超大模型微调时也很常见;
  • 梯度累积 + 激活检查点:在很极端的显存情况下,需要高效大 batch + 深层模型时必然要用到。
4.2 实践中的建议
  • 先试混合精度:省内存、提速度,几乎没有特别的坏处;
  • 再用梯度累积:如果还需要更大 batch size 或显存仍不足;
  • 最后考虑激活检查点:只在训练速度可以接受的情况下开启,以释放更多显存做更深/更大序列的训练。

数据格式

指令式微调(Instruction Tuning)

代码

数据格式化
def process_func(example):
    MAX_LENGTH = 384    # Llama分词器会将一个中文字切分为多个token,因此需要放开一些最大长度,保证数据的完整性
    # 输入id序列、注意力掩码和标签(训练时用来计算损失)
    input_ids, attention_mask, labels = [], [], []
    # 构造'指令'部分(prompt)的token编码
    instruction = tokenizer(f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n<|im_start|>assistant\n", add_special_tokens=False)  # add_special_tokens 不在开头加 special_tokens,因为这些特殊token已经在字符串中手动指定了
    # 对期望回答进行编码,同样不添加特殊token
    response = tokenizer(f"{example['output']}", add_special_tokens=False)
    # 将指令部分的token id与回答部分的token id与回答部分的token id连接起来,再在末尾添加一个pad token(用于标识结束),这样就构成了模型输入的完整的token序列
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    # 对指令和回答部分的attention mask进行拼接,末尾加上1(因为eos token也需要关注,即不屏蔽他)
    # attention mask指模型在自注意力机制中哪些token是有效的(1表示关注,0表示忽略)
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]  # 因为eos token咱们也是要关注的所以 补充为1
    # 当label为-100时,该token在计算损失时会被忽略,表示模型不需要再指令部分计算损失
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]  
    # 确保每个样本的长度不会超出模型的处理能力
    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
    }
训练流程概括

这段数据处理代码实际上是大模型微调过程中非常关键的一步,它将原始的文本数据转换为模型能直接处理的数值化表示。下面我结合整个微调流程,详细讲解一下训练过程以及代码中各个部分(尤其是 input_idsattention_masklabels)的作用。


1. 大模型微调整体流程

  1. 数据准备

    • 收集数据

      :通常会收集指令数据或对话数据,例如用户提问和预期的回答。数据格式一般为:

      {
        "instruction": "你是谁?",
        "input": "",
        "output": "家父是大理寺少卿甄远道。"
      }
      
    • 数据预处理:将文本数据转换为模型可以理解的数字序列,这里就需要用到 tokenizer 进行编码,同时构造输入、输出及对应的注意力掩码。

  2. 数据处理(Process Function)

    • 构造 Prompt:代码中通过拼接特殊标记(如 <|im_start|>system<|im_start|>user<|im_start|>assistant 等),构造了一个完整的对话上下文。这样做的目的是让模型知道当前的角色和上下文,例如先设定系统角色,再加入用户问题,然后留出位置让模型生成答案。

    • Tokenization

      :使用 tokenizer 将拼接好的文本转换为 token id 序列,得到两个部分:

      • 指令部分(包括系统和用户内容)
      • 回答部分(期望模型输出的文本)
    • 拼接和截断:将这两个部分拼接成一个长序列,并在末尾加上 pad token。若超过最大长度,则进行截断。

  3. 构造训练样本

    • 预处理后的数据包含三个核心字段:
      • input_ids:模型的输入序列,即拼接后的所有 token id。
      • attention_mask:指示模型哪些位置是有效 token(1 表示有效,0 表示填充部分),帮助模型在注意力计算中忽略填充位。
      • labels:监督信号,即模型应当预测的目标 token。这里对前面的指令部分,我们不需要计算损失,所以使用了 -100(在 Hugging Face 中,标签为 -100 的 token 会被忽略,不计算梯度),而只在回答部分保留实际的 token id 作为训练目标。
  4. 模型训练(微调)

    • 加载数据:预处理后的数据会通过 DataLoader 或 Trainer 被加载到模型中。
    • 前向传播:输入 input_idsattention_mask 到模型中,模型根据这些 token 预测下一个 token 的概率分布。
    • 损失计算:模型输出与 labels 进行比较,计算交叉熵损失。由于 labels 中指令部分的 token 被设置为 -100,所以仅对回答部分计算损失,这样模型只学习生成正确的回答。
    • 反向传播和参数更新:计算得到的损失会被反向传播,从而更新模型参数。很多时候,微调时可能只更新部分参数(例如 LoRA 方法只更新一些附加参数),以达到高效微调的目的。
  5. 验证与推理

    • 微调完成后,可以将训练好的模型加载起来进行推理,使用类似的 tokenizer 构造 prompt,再让模型生成回答,并对生成结果进行解码输出。

2. 重点解析:input_ids、attention_mask 和 labels

input_ids

  • 作用
    • input_ids 是经过 tokenizer 编码后得到的数字序列,代表了整个输入文本(指令部分 + 回答部分)的 token id。
    • 模型的输入就是这些数字,模型通过嵌入层将它们映射到向量空间,然后进行后续的 Transformer 计算。
  • 在训练中的作用
    • 作为模型的输入,决定了模型的上下文和生成条件。

attention_mask

  • 作用
    • attention_mask 是一个与 input_ids 长度相同的二值数组,用来告诉模型哪些位置是真实的 token(1)哪些位置是填充(0)。
    • 在自注意力机制中,模型会根据这个 mask 忽略掉填充部分,避免无意义的计算干扰。
  • 在训练中的作用
    • 帮助模型集中注意力在真实的文本部分,提高训练和推理效率。

labels

  • 作用
    • labels 是用于计算损失的目标 token 序列。
    • 在这段代码中,为了让模型只在生成回答时计算损失,对指令部分用 -100 占位,这样在计算交叉熵损失时,这些位置会被忽略(Hugging Face 的默认设置)。
  • 在训练中的作用
    • 指定模型在每个位置应该输出什么 token,从而计算预测误差。
    • 通过对回答部分的监督,模型学习如何生成符合预期的回复。

3. 总结

整个微调过程可以归纳为以下几个步骤:

  1. 构造输入:将系统提示、用户指令和期望回答拼接成一个完整文本,并进行 tokenization,得到 input_idsattention_mask
  2. 构造目标:设置 labels,确保模型只在回答部分计算损失(对前面指令部分用 -100 忽略)。
  3. 数据送入模型训练:通过前向传播、损失计算和反向传播,微调模型参数,使模型能够根据给定的指令生成正确的回答。

在这个过程中,input_ids 提供了输入的数值表示,attention_mask 告诉模型哪些 token 需要关注,而 labels 则为训练提供了目标监督信号。通过这种精心设计的数据处理方式,可以有效地对大模型进行指令微调,从而让模型更好地理解和生成符合任务要求的内容。

汇总

下面给出一个将用于微调的 Notebook 脚本合并成单个 Python 脚本的示例,同时在代码中添加详细注释,讲解每个部分的作用。假设你训练的是 Qwen2.5-0.5B-Instruct 模型,并使用 LoRA 微调,下面的脚本将展示完整流程,从加载模型、数据预处理到启动训练:


#!/usr/bin/env python
# coding=utf-8
"""
本脚本展示如何对 Qwen2.5-0.5B-Instruct 模型使用 LoRA 进行微调,
将 Notebook 中的所有步骤合并为一个单独的 Python 脚本。
"""

import os
import torch
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq,
)
from peft import LoraConfig, TaskType, get_peft_model
from datasets import load_dataset

# =============================================================================
# 1. 配置路径与 LoRA 参数
# =============================================================================

# 基础模型的存储路径(请确保该路径下包含完整的模型文件)
model_path = '/root/autodl-tmp/qwen/Qwen2.5-0.5B-Instruct'
# 数据集路径,这里假设数据为 JSON 格式,包含 instruction、input 和 output 字段
dataset_path = './dataset/huanhuan.json'
# 微调后模型及检查点保存的目录
output_dir = './output/Qwen2.5_instruct_lora'

# 定义 LoRA 配置参数
# 注意:target_modules 列表指定了哪些模块将添加 LoRA 适配器,
# r 表示低秩分解中的秩,lora_alpha 是缩放因子(通常 lora_alpha / r 为实际缩放比例),lora_dropout 为 dropout 概率。
lora_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_alpha=32,
    lora_dropout=0.1,
)

# =============================================================================
# 2. 加载 Tokenizer 和基础模型,并整合 LoRA 模块
# =============================================================================

# 加载分词器(注意使用 trust_remote_code=True 以支持自定义模型代码)
tokenizer = AutoTokenizer.from_pretrained(model_path, use_fast=False, trust_remote_code=True)

# 加载因果语言模型(Causal LM),使用半精度(bfloat16)加载,并自动分配设备
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    device_map="auto",
    torch_dtype=torch.bfloat16,
    trust_remote_code=True
)

# 将 LoRA 适配器整合到模型中
model = get_peft_model(model, lora_config)
model.train()  # 设置模型为训练模式

# =============================================================================
# 3. 数据预处理:构造输入、注意力掩码和标签
# =============================================================================

def process_func(example):
    """
    对每个数据样本进行预处理:
      - 拼接系统、用户和助手部分,形成完整的上下文。
      - 使用 tokenizer 将文本转换为 token id 序列。
      - 构造 attention_mask,指示哪些 token 是有效的。
      - 对于计算损失,只对助手部分(output)生效,其他部分设置为 -100 以忽略损失计算。
    """
    MAX_LENGTH = 384  # 设置最大 token 数量,避免过长序列导致截断

    # 构造指令部分
    # 这里我们拼接了一个系统提示(让模型扮演甄嬛),用户输入(instruction 和 input)以及助手部分的起始标记。
    instruction = tokenizer(
        f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n"
        f"<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n"
        f"<|im_start|>assistant\n",
        add_special_tokens=False
    )
    # 对输出部分(期望回答)进行编码
    response = tokenizer(f"{example['output']}", add_special_tokens=False)

    # 拼接整个 token 序列:指令部分 + 回答部分 + 最后的 pad token
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    # attention_mask 同理,指令部分和回答部分对应 mask,pad 部分设为 1(这里也可设置为1确保结束符被关注)
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
    # 构造 labels:对指令部分使用 -100 忽略损失计算,仅对回答部分计算损失
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]

    # 如果总长度超过最大值,则进行截断
    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
    }

# 使用 Hugging Face 的 datasets 库加载 JSON 数据集
raw_dataset = load_dataset("json", data_files={"train": dataset_path})
# 对训练集的每个样本调用 process_func 进行预处理,移除原始字段
tokenized_dataset = raw_dataset["train"].map(process_func, remove_columns=raw_dataset["train"].column_names)

# =============================================================================
# 4. 配置训练参数并创建 Trainer
# =============================================================================

# 配置 TrainingArguments,设定 batch_size、梯度累积、学习率、保存步数等参数
training_args = TrainingArguments(
    output_dir=output_dir,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
)

# 数据整理器,负责在训练时自动填充 batch 中的样本
data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)

# 创建 Trainer 对象,负责管理训练循环、验证和保存模型等任务
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    data_collator=data_collator,
)

# =============================================================================
# 5. 训练并保存模型
# =============================================================================

if __name__ == '__main__':
    trainer.train()
    # 训练结束后保存模型到 output_dir
    trainer.save_model(output_dir)

详细讲解

  1. 配置路径与 LoRA 参数
    • 我们首先指定了基础模型(model_path)、数据集文件(dataset_path)以及输出目录(output_dir)。
    • 接着定义了 LoRA 的配置参数:
      • target_modules 指明哪些层会被注入 LoRA 模块。
      • r 表示低秩矩阵分解中的秩;
      • lora_alpha 是缩放因子(实际缩放为 lora_alpha / r);
      • lora_dropout 设置在 LoRA 部分应用的 dropout 比例。
  2. 加载 Tokenizer 和基础模型
    • 使用 AutoTokenizer.from_pretrained 加载分词器,参数 trust_remote_code=True 允许加载自定义代码。
    • 使用 AutoModelForCausalLM.from_pretrained 加载因果语言模型,利用 device_map="auto" 自动分配 GPU/CPU,并设置 torch_dtype=torch.bfloat16 加载半精度权重。
    • 通过 get_peft_model 将 LoRA 模块整合到基础模型中,从而实现微调时只更新 LoRA 参数而保持大部分参数冻结。
    • 最后调用 model.train() 进入训练模式。
  3. 数据预处理
    • process_func 函数用于将每个样本转换为模型训练所需格式。
    • 该函数构造了一个完整的上下文,包含了系统提示、用户输入以及助手回答的开始标记。
    • 使用 tokenizer 分别编码指令部分和回答部分,然后将它们拼接在一起。
    • 对于标签(labels),我们将指令部分设置为 -100,确保在计算损失时忽略这些 token;只对回答部分计算损失。
    • 最后对超过最大长度的序列进行截断,防止超长输入。
  4. 配置训练参数和创建 Trainer
    • 利用 TrainingArguments 设置训练过程中的各种超参数,如每个设备的 batch size、梯度累积步数、学习率、保存模型的步数等。
    • 使用 DataCollatorForSeq2Seq 自动为 batch 内样本进行 padding。
    • 创建 Trainer 对象,它负责管理训练过程,包括前向传播、反向传播、梯度更新和模型保存。
  5. 训练和保存模型
    • 在主函数中调用 trainer.train() 启动训练过程。
    • 训练完成后调用 trainer.save_model(output_dir) 将微调后的模型保存到指定的输出目录中。

下面给出使用 Unsloth 框架进行 LoRA 微调的示例代码,与原始代码结构基本类似,主要改动在模型加载和 LoRA 模块的整合部分,改为调用 Unsloth 的 API:

#!/usr/bin/env python
# coding=utf-8
"""
本脚本展示如何对 Qwen2.5-0.5B-Instruct 模型使用 Unsloth 结合 LoRA 进行微调,
将 Notebook 中的所有步骤合并为一个单独的 Python 脚本。
"""

import os
import torch
from transformers import (
    TrainingArguments,
    Trainer,
    DataCollatorForSeq2Seq,
)
from datasets import load_dataset
# 使用 Unsloth 框架加载模型及整合 LoRA 适配器
from unsloth import FastLanguageModel

# =============================================================================
# 1. 配置路径与 LoRA 参数
# =============================================================================

# 基础模型的存储路径(请确保该路径下包含完整的模型文件)
model_path = '/root/autodl-tmp/qwen/Qwen2.5-0.5B-Instruct'
# 数据集路径,这里假设数据为 JSON 格式,包含 instruction、input 和 output 字段
dataset_path = './dataset/huanhuan.json'
# 微调后模型及检查点保存的目录
output_dir = './output/Qwen2.5_instruct_unsloth'

# 定义 LoRA 配置参数(与之前保持一致)
lora_config = {
    "r": 8,
    "target_modules": ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
    "lora_alpha": 32,
    "lora_dropout": 0.1,
    "inference_mode": False,  # 训练模式
}

# =============================================================================
# 2. 使用 Unsloth 加载模型和分词器,并整合 LoRA 模块
# =============================================================================

# 使用 Unsloth 加载模型和分词器
# 注意:这里设置 max_seq_length 与后续数据预处理中的 MAX_LENGTH 保持一致(例如 384)
model, tokenizer = FastLanguageModel.from_pretrained(
    model_path,
    max_seq_length=384,
    torch_dtype=torch.bfloat16,
    load_in_4bit=True,
    trust_remote_code=True  # 如需要支持自定义代码,请设置为 True
)

# 将 LoRA 适配器整合到模型中,使用 Unsloth 提供的 get_peft_model
model = FastLanguageModel.get_peft_model(
    model=model,
    r=lora_config["r"],
    target_modules=lora_config["target_modules"],
    lora_alpha=lora_config["lora_alpha"],
    lora_dropout=lora_config["lora_dropout"],
)
model.train()  # 设置模型为训练模式

# =============================================================================
# 3. 数据预处理:构造输入、注意力掩码和标签
# =============================================================================

def process_func(example):
    """
    对每个数据样本进行预处理:
      - 拼接系统提示、用户输入和助手回答,形成完整的上下文。
      - 使用 tokenizer 将文本转换为 token id 序列。
      - 构造 attention_mask。
      - 对于计算损失,仅对助手部分(output)计算,其他部分设为 -100。
    """
    MAX_LENGTH = 384  # 最大 token 数量

    # 拼接系统提示、用户输入和助手起始标记
    instruction = tokenizer(
        f"<|im_start|>system\n现在你要扮演皇帝身边的女人--甄嬛<|im_end|>\n"
        f"<|im_start|>user\n{example['instruction'] + example['input']}<|im_end|>\n"
        f"<|im_start|>assistant\n",
        add_special_tokens=False
    )
    # 对期望输出(回答)进行编码
    response = tokenizer(f"{example['output']}", add_special_tokens=False)

    # 拼接整个 token 序列:指令部分 + 回答部分 + 最后一个 pad token
    input_ids = instruction["input_ids"] + response["input_ids"] + [tokenizer.pad_token_id]
    attention_mask = instruction["attention_mask"] + response["attention_mask"] + [1]
    # 构造 labels:对指令部分忽略损失(-100),仅对回答部分计算损失
    labels = [-100] * len(instruction["input_ids"]) + response["input_ids"] + [tokenizer.pad_token_id]

    # 截断到 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
    }

# 使用 Hugging Face 的 datasets 库加载 JSON 数据集
raw_dataset = load_dataset("json", data_files={"train": dataset_path})
# 对训练集的每个样本调用 process_func 进行预处理
tokenized_dataset = raw_dataset["train"].map(process_func, remove_columns=raw_dataset["train"].column_names)

# =============================================================================
# 4. 配置训练参数并创建 Trainer
# =============================================================================

training_args = TrainingArguments(
    output_dir=output_dir,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    logging_steps=10,
    num_train_epochs=3,
    save_steps=100,
    learning_rate=1e-4,
    save_on_each_node=True,
    gradient_checkpointing=True,
)

data_collator = DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=tokenized_dataset,
    data_collator=data_collator,
)

# =============================================================================
# 5. 训练并保存模型
# =============================================================================

if __name__ == '__main__':
    trainer.train()
    trainer.save_model(output_dir)

说明

  1. 模型加载部分
    使用 FastLanguageModel.from_pretrained 代替了原来的 AutoModelForCausalLM.from_pretrained,并设置了 max_seq_lengthtorch_dtypeload_in_4bit 参数。
  2. LoRA 整合
    使用 FastLanguageModel.get_peft_model 传入相同的 LoRA 参数,实现 LoRA 适配器的加载。
  3. 数据预处理和训练流程
    数据预处理、TrainingArguments、DataCollator 和 Trainer 的创建与原来类似,可直接沿用。

这样,你就可以使用 Unsloth 进行大模型微调,同时享受其在加速和内存优化上的优势。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值