数据处理的工程化时间
- 数据清洗:去除噪声数据(比如乱码/重复文本),对不平衡数据进行重采样
- 高效预处理:使用 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 Facetransformers
中用来配置 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 注意事项
- 有效学习率:当你使用梯度累积时,相当于增大了批次大小,需要注意保持或调整学习率。例如,如果你原先 batch size=16,现在用
gradient_accumulation_steps=4
并把单次 batch size 改成4,总有效 batch size 就依然是16。这样你的学习率设置就不用大幅修改。 - 训练时间:梯度累积并不会减少计算量,对每个batch依然要做前向和反向,但它能帮助你在有限显存下进行较大的 batch 训练。
- 分布式训练配合:如果同时使用分布式训练(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=True
或bf16=True
之一。
2.3 优势与注意点
- 显存占用降低:以 FP16 为例,理论上可以减少约一半的激活/梯度显存。
- 速度提升:现代 GPU 对半精度计算有 Tensor Cores 等硬件加速,推理和训练都能加速。
- 数值稳定性:
- 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 注意事项
- 训练速度:激活检查点会增加额外的前向计算,训练速度会变慢,一般可达 20% ~ 30% 的速度损失。
- 层数越多,效果越明显:对于深层模型,激活数量大,通过 checkpointing 可以显著减少内存占用;浅层模型则收益有限。
- 兼容性:大多数 Transformer 模型默认都支持该特性,但需要确认你所使用的模型类型中是否实现了 gradient checkpointing。
4. 综合对比与搭配使用
- 梯度累积
- 主要目标:模拟更大 batch size,提升收敛表现或满足大 batch 需求;
- 内存/显存节省:可以在一个较小的 batch 大小下实现等效大 batch 的效果,不会额外节省激活占用(因为每次 forward 依然用同样的计算图),但你无需在一次forward就放下一整个大batch;
- 影响训练速度:没有直接加速或减速,只是分多次迭代同一个真实 batch,总体计算量不变;但确实提升了内存使用效率。
- 混合精度
- 主要目标:减少模型参数、激活和梯度的存储占用,并利用硬件加速让训练更快;
- 常用:建议总是尝试使用混合精度(尤其是有 Tensor Core 的新 GPU),通常可以显著减小内存/显存,提升速度。
- 激活检查点
- 主要目标:在前向时不保存全部激活,在反向时重算,显著减少激活占用;
- 适用场景:当模型极深且显存不足,而训练又必须使用较大 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_ids
、attention_mask
和 labels
)的作用。
1. 大模型微调整体流程
-
数据准备
-
收集数据
:通常会收集指令数据或对话数据,例如用户提问和预期的回答。数据格式一般为:
{ "instruction": "你是谁?", "input": "", "output": "家父是大理寺少卿甄远道。" }
-
数据预处理:将文本数据转换为模型可以理解的数字序列,这里就需要用到 tokenizer 进行编码,同时构造输入、输出及对应的注意力掩码。
-
-
数据处理(Process Function)
-
构造 Prompt:代码中通过拼接特殊标记(如
<|im_start|>system
、<|im_start|>user
、<|im_start|>assistant
等),构造了一个完整的对话上下文。这样做的目的是让模型知道当前的角色和上下文,例如先设定系统角色,再加入用户问题,然后留出位置让模型生成答案。 -
Tokenization
:使用 tokenizer 将拼接好的文本转换为 token id 序列,得到两个部分:
- 指令部分(包括系统和用户内容)
- 回答部分(期望模型输出的文本)
-
拼接和截断:将这两个部分拼接成一个长序列,并在末尾加上 pad token。若超过最大长度,则进行截断。
-
-
构造训练样本
- 预处理后的数据包含三个核心字段:
input_ids
:模型的输入序列,即拼接后的所有 token id。attention_mask
:指示模型哪些位置是有效 token(1 表示有效,0 表示填充部分),帮助模型在注意力计算中忽略填充位。labels
:监督信号,即模型应当预测的目标 token。这里对前面的指令部分,我们不需要计算损失,所以使用了-100
(在 Hugging Face 中,标签为 -100 的 token 会被忽略,不计算梯度),而只在回答部分保留实际的 token id 作为训练目标。
- 预处理后的数据包含三个核心字段:
-
模型训练(微调)
- 加载数据:预处理后的数据会通过 DataLoader 或 Trainer 被加载到模型中。
- 前向传播:输入
input_ids
和attention_mask
到模型中,模型根据这些 token 预测下一个 token 的概率分布。 - 损失计算:模型输出与
labels
进行比较,计算交叉熵损失。由于labels
中指令部分的 token 被设置为-100
,所以仅对回答部分计算损失,这样模型只学习生成正确的回答。 - 反向传播和参数更新:计算得到的损失会被反向传播,从而更新模型参数。很多时候,微调时可能只更新部分参数(例如 LoRA 方法只更新一些附加参数),以达到高效微调的目的。
-
验证与推理
- 微调完成后,可以将训练好的模型加载起来进行推理,使用类似的 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. 总结
整个微调过程可以归纳为以下几个步骤:
- 构造输入:将系统提示、用户指令和期望回答拼接成一个完整文本,并进行 tokenization,得到
input_ids
和attention_mask
。 - 构造目标:设置
labels
,确保模型只在回答部分计算损失(对前面指令部分用 -100 忽略)。 - 数据送入模型训练:通过前向传播、损失计算和反向传播,微调模型参数,使模型能够根据给定的指令生成正确的回答。
在这个过程中,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)
详细讲解
- 配置路径与 LoRA 参数
- 我们首先指定了基础模型(
model_path
)、数据集文件(dataset_path
)以及输出目录(output_dir
)。 - 接着定义了 LoRA 的配置参数:
target_modules
指明哪些层会被注入 LoRA 模块。r
表示低秩矩阵分解中的秩;lora_alpha
是缩放因子(实际缩放为 lora_alpha / r);lora_dropout
设置在 LoRA 部分应用的 dropout 比例。
- 我们首先指定了基础模型(
- 加载 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()
进入训练模式。
- 使用
- 数据预处理
process_func
函数用于将每个样本转换为模型训练所需格式。- 该函数构造了一个完整的上下文,包含了系统提示、用户输入以及助手回答的开始标记。
- 使用 tokenizer 分别编码指令部分和回答部分,然后将它们拼接在一起。
- 对于标签(
labels
),我们将指令部分设置为 -100,确保在计算损失时忽略这些 token;只对回答部分计算损失。 - 最后对超过最大长度的序列进行截断,防止超长输入。
- 配置训练参数和创建 Trainer
- 利用
TrainingArguments
设置训练过程中的各种超参数,如每个设备的 batch size、梯度累积步数、学习率、保存模型的步数等。 - 使用
DataCollatorForSeq2Seq
自动为 batch 内样本进行 padding。 - 创建
Trainer
对象,它负责管理训练过程,包括前向传播、反向传播、梯度更新和模型保存。
- 利用
- 训练和保存模型
- 在主函数中调用
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)
说明
- 模型加载部分
使用FastLanguageModel.from_pretrained
代替了原来的AutoModelForCausalLM.from_pretrained
,并设置了max_seq_length
、torch_dtype
与load_in_4bit
参数。 - LoRA 整合
使用FastLanguageModel.get_peft_model
传入相同的 LoRA 参数,实现 LoRA 适配器的加载。 - 数据预处理和训练流程
数据预处理、TrainingArguments、DataCollator 和 Trainer 的创建与原来类似,可直接沿用。
这样,你就可以使用 Unsloth 进行大模型微调,同时享受其在加速和内存优化上的优势。