前言 在完成微调前备知识的学习后,正式开始使用unsloth对Qwen3-8B-unsloth-bnb-4bit模型的lora微调实战
模型加载 1 2 3 4 5 6 7 8 9 10 11 12 13 from unsloth import FastLanguageModel import torch max_seq_length = 8192 dtype = None load_in_4bit = True model, tokenizer = FastLanguageModel.from_pretrained( model_name = "/workspace/qwen3-8b", max_seq_length = max_seq_length, dtype = dtype, load_in_4bit = load_in_4bit, )
FastLanguageModel 是 Unsloth 框架的核心入口类 ,即“把 Hugging Face 的 transformers 模型‘加速’成支持 QLoRA 微调、显存占用减半、速度提升 2-5 倍的封装器。”
max_seq_length = 8192作用 :告诉框架 “后续所有输入序列的最大长度” 。内部一次性为位置编码、注意力掩码、KV-Cache 等开辟的张量尺寸 ,因此显存随它 平方级增长 。
dtype = None作用 :让 Unsloth 自动选择最合适的浮点精度 。
load_in_4bit = True作用 :把模型权重量化成 4-bit ,显存降到 1/4,QLoRA 微调必备。
查看模型与分词器信息 模型信息 运行
通过阅读模型信息我们可以了解到:
1 (embed_tokens): Embedding(151936, 4096, padding_idx=151654)
模型有 15 万个 token 的字典,每个字/词被翻译成 4096 维向量,第 151 654 号 token 被官方指定为填充符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 (layers): ModuleList( (0-2): 3 x Qwen3DecoderLayer( (self_attn): Qwen3Attention( (q_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) (k_proj): Linear4bit(in_features=4096, out_features=1024, bias=False) (v_proj): Linear4bit(in_features=4096, out_features=1024, bias=False) (o_proj): Linear4bit(in_features=4096, out_features=4096, bias=False) (q_norm): Qwen3RMSNorm((128,), eps=1e-06) (k_norm): Qwen3RMSNorm((128,), eps=1e-06) (rotary_emb): LlamaRotaryEmbedding() ) (mlp): Qwen3MLP( (gate_proj): Linear(in_features=4096, out_features=12288, bias=False) (up_proj): Linear(in_features=4096, out_features=12288, bias=False) (down_proj): Linear(in_features=12288, out_features=4096, bias=False) (act_fn): SiLU() ) (input_layernorm): Qwen3RMSNorm((4096,), eps=1e-06) (post_attention_layernorm): Qwen3RMSNorm((4096,), eps=1e-06) )
共有36层Qwen3DecoderLayer ,每层包含Qwen3Attention ,Qwen3MLP (一个 SwiGLU 前馈网络 ),Qwen3RMSNorm (两个归一化层 ,对 4096 维的隐藏向量做“均方根归一化”,防止梯度爆炸、稳定训练。)
大模型-qwen3 模型结构解读-66 - jack-chen666 - 博客园
LoRA可以插到哪里呢?
凡是打印里每层 Decoder 中出现的 Linear4bit(q/k/v/o + gate/up/down)就是 LoRA 可插、且默认会被插入的位置。
分词器信息 运行
查看tokenizer信息
1 2 3 4 5 6 7 8 Qwen2TokenizerFast(name_or_path='/workspace/qwen3-8b', vocab_size=151643, model_max_length=40960, is_fast=True, padding_side='left', truncation_side='right', special_tokens={'eos_token': '<|im_end|>', 'pad_token': '<|vision_pad|>', 'additional_special_tokens': ['<|im_start|>', '<|im_end|>', '<|object_ref_start|>', '<|object_ref_end|>', '<|box_start|>', '<|box_end|>', '<|quad_start|>', '<|quad_end|>', '<|vision_start|>', '<|vision_end|>', '<|vision_pad|>', '<|image_pad|>', '<|video_pad|>']}, clean_up_tokenization_spaces=False, added_tokens_decoder={ 151643: AddedToken("<|endoftext|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 151644: AddedToken("<|im_start|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 151645: AddedToken("<|im_end|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 151646: AddedToken("<|object_ref_start|>", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 截取部分 } )
vocab_size=151643:模型真正能理解和生成的子词/符号有这 151643 种,其余位置是预留空白。
model_max_length=40960:理论最大输入长度 40k token (实际受显存限制)
is_fast=True:表示 tokenizer 使用的是 Hugging Face 的「Rust 高速实现」 (即 tokenizers 库)
special_tokens:打印的 special_tokens 字典 & added_tokens_decoder 已经把 151643-151668 全部列出,共 26 个 。
模拟一次模型处理流程 将对话内容通过tokenizer进行处理
1 2 3 4 5 6 7 8 9 10 messages = [ {"role" : "user", "content" : "你好,好久不见!"} ] text = tokenizer.apply_chat_template( messages, tokenize = False, add_generation_prompt = True, enable_thinking = False, # 设置不思考 )
apply_chat_template 是把「人类对话格式的 Python 列表」一键翻译成 模型能直接理解的带特殊标记的文本字符串(或 token id 序列) 的“官方模板引擎”。
转化后的格式为:
1 '<|im_start|>user\n你好,好久不见!<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n'
然后将转化后的字符串转成 GPU 上的 PyTorch token 张量,准备直接送进模型推理或训练。
1 inputs = tokenizer(text, return_tensors="pt").to("cuda")
以上代码共做了三步:
tokenizer(text) 把前面 apply_chat_template 得到的字符串按词表切成 token id 列表 。
return_tensors=”pt” 把列表包成 PyTorch 张量 (shape = [1, seq_len])。
.to(“cuda”) 把张量搬到 GPU 显存 。
输出如下
1 2 3 {'input_ids': tensor([[151644, 872, 198, 108386, 3837, 111920, 101571, 6313, 151645, 198, 151644, 77091, 198, 151667, 271, 151668, 271]], device='cuda:0'), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]], device='cuda:0')}
键
形状
每个数字的含义
input_ids
[1, 17]
17 个 token 的 ID 列表,已放到 GPU
attention_mask
[1, 17]
17 个 1 ,表示“这些位置都是有效 token,无填充”
1 2 3 4 5 6 outputs = model.generate( input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=max_seq_length, use_cache=True,#启用 KV-Cache,避免重复计算,显存换时间 )
让模型在 GPU 上 根据已有 token 继续生成文本 ,直到达到 max_new_tokens 或遇到终止符。
outputs格式和inputs类似,使用nput_ids表示后续字符
1 response = tokenizer.batch_decode(outputs)
把模型输出的 token id 序列 (outputs)一次性还原成 人类可读的字符串 。
输出如下
1 '<|im_start|>user\n你好,好久不见!<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n你好!好久不见!最近过得怎么样?有什么新鲜事想和我分享吗?😊<|im_end|>'
这里展示的是没有思考过程的,最简单对话流程,若设置思考模式,完整代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 text = tokenizer.apply_chat_template( messages, tools = tools,#同样,可以设置function calling tokenize = False, add_generation_prompt = True, enable_thinking = True, # 设置思考 ) inputs = tokenizer(text, return_tensors="pt").to("cuda") outputs = model.generate( input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=max_seq_length, use_cache=True, ) response = tokenizer.batch_decode(outputs)
当然,除了使用上述底层API进行对话外,Unsloth还提供了更加便捷的流式输出模型对话信息的函数,基本对话效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 messages = [ {"role" : "user", "content" : "你好,好久不见!"} ] text = tokenizer.apply_chat_template( messages, tokenize = False, add_generation_prompt = True, enable_thinking = False, ) _ = model.generate( **tokenizer(text, return_tensors = "pt").to("cuda"), max_new_tokens = 256, # Increase for longer outputs! temperature = 0.7, top_p = 0.8, top_k = 20, # For non thinking streamer = TextStreamer(tokenizer, skip_prompt = True),#实时流式输出:每解码一个 token 就立刻打印到终端 )
准备数据集 下载数据集 选取的两个数据集
我们使用 Open Math Reasoning 数据集,该数据集曾被用于赢得 AIMO(AI 数学奥林匹克 - 第二届进步奖)挑战!我们从中抽取了 10% 可验证的推理轨迹,这些轨迹是基于 DeepSeek R1 模型生成的,并且准确率超过 95%。数据集地址:https://huggingface.co/datasets/unsloth/OpenMathReasoning-mini
我们还利用了 Maxime Labonne 的 FineTome-100k 数据集,该数据集风格类似 ShareGPT。但我们需要将其转换为 HuggingFace 通用的多轮对话格式。数据集地址:https://huggingface.co/datasets/mlabonne/FineTome-100k
在实际微调过程中,大多都会使用huggingface的datasets库进行数据集下载和管理,实际下载流程如下:
1 !pip install --upgrade datasets huggingface_hub
datasets 是 Hugging Face 提供的一个高效数据处理库,专为机器学习和大语言模型(LLM)训练而设计。它支持加载、处理、转换和保存各种格式的数据(如 JSON、CSV、Parquet 等),并能与 transformers 模型无缝集成。通过 datasets,开发者可以快速完成数据清洗、切分、tokenization 等常见任务,大大提升训练效率,特别适合用于指令微调、对话生成、Function Calling 等任务的数据预处理。
然后分别下载并导入这两个库:
1 reasoning_dataset = load_dataset("unsloth/OpenMathReasoning-mini", split = "cot")
cot全称为Chain-of-Thought,思维链 ,是「一步一步把思考过程写出来 」的解题方式,而不是直接给出最终答案。
只下 cot 是因为任务只需要“带推理过程”的那部分数据,其他子集对当前微调目标无用,避免冗余下载。
1 non_reasoning_dataset = load_dataset("mlabonne/FineTome-100k", split = "train")
查看数据集 然后输入数据集名称,即可查看数据集基本信息:
1 2 3 4 Dataset({ features: ['expected_answer', 'problem_type', 'problem_source', 'generation_model', 'pass_rate_72b_tir', 'problem', 'generated_solution', 'inference_mode'], num_rows: 19252 })
一共 19 252 条 CoT(思维链)数学题 ,每条包含 8 个字段,可直接用来训练/评估模型的逐步推理能力。
generated_solution:模型自己写的 逐步推理 + 最终答案(就是你想要的 CoT)
expected_answer:标准答案(通常是一个简洁数字或表达式)
generation_model:生成这条 CoT 的“教师模型”名字,比如 qwen2-72b
加上索引则可以直接查看对应数据集信息:
1 2 3 4 5 6 7 8 {'expected_answer': '14', 'problem_type': 'has_answer_extracted', 'problem_source': 'aops_c4_high_school_math', 'generation_model': 'DeepSeek-R1', 'pass_rate_72b_tir': '0.96875', 'problem': 'Given $\\sqrt{x^2+165}-\\sqrt{x^2-52}=7$ and $x$ is positive, find all possible values of $x$.', 'generated_solution': "<think>\nOkay, let's see. I need to solve the equation √(x² + 165) - √(x² - 52) = 7, a截取部分", 'inference_mode': 'cot'}
能够看出这是一个基于DeepSeek R1回答的数学数据集,其中problem是问题,generated_solution是数学推导过程(即思考过程),而expected_answer则是最终的答案。该数据集总共接近2万条数据
而对话数据集如下:
1 2 3 4 Dataset({ features: ['conversations', 'source', 'score'], num_rows: 100000 })
1 non_reasoning_dataset[0]
1 2 3 4 5 6 {'conversations': [{'from': 'human', 'value': 'Explain what boolean operators are, what they do, and provide examples of how they can be used in programming. Additionally, describe the concept of operator precedence and prov截取'}, {'from': 'gpt', 'value': 'Boolean operators are logical operators used in programming to manipulate boolean values. The截取'}], 'source': 'infini-instruct-top-500k', 'score': 5.212620735168457}
其中每一条数据都是一个对话,包含一组或者多组ChatGPT的聊天信息,其中from代表是用户消息还是大模型回复消息,而value则是对应的文本。该对话数据集总共包含10万条数据
能够看出dataset是一种类似json的数据格式,每条数据都以字段格式进行存储,在实际微调过程中,我们需要先将数据集的目标字段进行提取和拼接,然后加载到Qwen3模型的提示词模板中,并最终带入Unsloth进行微调。
数据集清洗 对话数据集的清洗 接下来尝试对上述两个格式各异的数据集进行数据清洗,主要是围绕数据集进行数据格式 的调整,便于后续带入Qwen3提示词模板 。对于dataset格式的数据对象来说,可以先创建满足格式调整的函数,然后使用map方法对数据集格式进行调整。
1 2 3 4 5 6 7 8 9 10 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_dataset中的每一条数据进行格式调整,即通过新创建一个新的特征conversations,来以对话形式保存历史问答数据:
1 2 3 4 reasoning_data = reasoning_dataset.map( generate_conversation, # 处理函数 batched=True # 批量处理,加快速度 )
map:对数据集中的每一批样本调用 generate_conversation
batched=True:一次传入一批(几百到几千条)样本,避免逐行慢速 Python 循环
接下来将其带入Qwen3的提示词模板中进行转化:
1 2 3 4 reasoning_conversations = tokenizer.apply_chat_template( reasoning_data["conversations"], tokenize = False, )
之后即可带入这些数据进行微调。能看出每条数据的格式都和Unsloth底层对话API创建的数据格式类似,之后我们或许可以借助Unsloth底层对话API来创建微调数据集。
推理数据集的推理 然后继续处理non_reasoning_conversations数据集,由于该数据集采用了sharegpt对话格式,因此可以直接借助Unsloth的standardize_sharegpt库进行数据集的格式转化,转化效果如下所示:
1 from unsloth.chat_templates import standardize_sharegpt
standardize_sharegpt的作用
把“ShareGPT 格式”的对话数据一键转成 Unsloth / Hugging Face 通用的 role/content 列表,后续就能直接用 apply_chat_template 生成训练文本。
1️⃣ ShareGPT 原始长什么样?
1 2 { "from" : "human" , "value" : "1+1=?" } { "from" : "gpt" , "value" : "2" }
2️⃣ 转换后长什么样?
1 2 { "role" : "user" , "content" : "1+1=?" } { "role" : "assistant" , "content" : "2" }
1 dataset = standardize_sharegpt(non_reasoning_dataset)
接下来即可直接带入Qwen3对话模板中进行格式调整:
1 2 3 4 non_reasoning_conversations = tokenizer.apply_chat_template( dataset["conversations"], tokenize = False, )
数据集采样 自此即完成了每个数据集的格式调整工作,不过这两个数据集并不均衡,能看得出非推理类数据集的长度更长。我们假设希望模型保留一定的推理能力,但又特别希望它作为一个聊天模型来使用。
因此,我们需要定义一个 仅聊天数据的比例 。目标是从两个数据集中构建一个混合训练集 。这里我们可以设定一个 25% 推理数据、75% 聊天数据的比例:也就是说,从推理数据集中抽取 25%(或者说,抽取占比为 100% - 聊天数据占比 的部分),最后将这两个数据集合并起来即可。
1 2 3 4 5 6 7 8 9 10 chat_percentage = 0.75 import pandas as pd #先把非推理对话列表转成 Pandas Series,方便后续抽样 non_reasoning_subset = pd.Series(non_reasoning_conversations) non_reasoning_subset = non_reasoning_subset.sample(#sample(...)为无放回随机抽样 int(len(reasoning_conversations) * (1.0 - chat_percentage)),#计算 需要抽多少条非推理样本 random_state = 2407, )
这里我们需要先将上述list格式的数据转化为pd.Series数据,然后进行采样,并最终将其转化为dataset类型对象。(此外也可以先转化为dataset对象类型,然后再进行采样)
1 2 3 4 5 6 7 8 9 10 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)#用固定种子随机打乱顺序
pd.concat([…]):纵向拼接 → 一条长 Series,顺序:先推理,后非推理
Dataset.from_pandas(…):把 Pandas Series 转成 Hugging Face Dataset
把“推理对话”和“抽样后的非推理对话”合并成一个 随机打乱 的 Dataset 对象,后面可直接拿去训练。
查看数据集
1 2 {'text': "<|im_start|>user\nCalculate the pH during a titration when 9.54 mL of a 0.15 M HCl solution has reacted with 22.88 mL of a 0.14 M NaOH solution?<|im_end|>\n<|im_st截取", '__index_level_0__': 49038}
其中text字段就是后续带入微调的字段。
数据集保存 1 combined_dataset.save_to_disk("/workspace/cleaned_qwen3_dataset")
后续使用时即可使用如下代码进行读取:
1 2 from datasets import load_from_disk combined_dataset = load_from_disk("cleaned_qwen3_dataset")
Qwen3推理能力高效微调流程 准备完数据之后,即可开始进行微调。这里我们先进行少量数据微调测试,程序能够基本跑通后,我们再进行大规模数据集微调。
进行LoRA参数注入 1 2 3 4 5 6 7 8 9 10 11 12 13 model = FastLanguageModel.get_peft_model( model, r = 32, # 秩(LoRA 低秩矩阵的列数)。越大可学习参数越多,显存也越高。常用 8/16/32/64/128 target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # 在哪些线性层插入 LoRA 适配器(Attention + MLP) lora_alpha = 32, # 缩放因子。经验值 = rank 或 2×rank,控制更新强度 lora_dropout = 0, # LoRA 本身的 dropout 比例;0 省显存且速度最快 bias = "none", # 是否训练原 Linear 的偏置。设为 "none" 不训练,进一步节省显存 use_gradient_checkpointing = "unsloth", # 梯度检查点:True 省显存,"unsloth" 再省 30 %,超长上下文必开 random_state = 3407, # 随机种子,保证 LoRA 初始化可复现 use_rslora = False, # 默认 False,True 则启用 Rank-Stabilized LoRA(训练更稳,但显存稍高) loftq_config = None, # LoftQ 量化初始化,None 表示不用;若配置可进一步压缩初始权重 )
这一步“LoRA 参数注入” 就是:在不改动原模型权重的前提下,给指定层插入少量 可训练低秩矩阵 (LoRA 适配器),从而只更新 < 1 % 的参数,完成高效微调。
不是“在原有层之外再增加一层”,而是把 LoRA 的“小矩阵”插到 原有线性层内部 :
原层结构(冻结):x → Linear4bit(W) → y
注入后结构(冻结 + 可训练):x → [Linear4bit(W) + LoRA(A·B)] → y
A 和 B 两个低秩矩阵被 注册为同一层的新参数 ,不新建网络层 ,参数在 前向时相加 ,反向只更新 A 和 B 。
设置微调参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from trl import SFTTrainer, SFTConfig trainer = SFTTrainer( model=model, # 已插入 LoRA 的 4-bit 模型 tokenizer=tokenizer, # 对应 tokenizer(含 chat 模板) train_dataset=combined_dataset, # 训练集:聊天+推理对话 eval_dataset=None, # 如需验证,把验证集放进来即可 args=SFTConfig( dataset_text_field="text", # 训练集中每条样本的字段名(对话列表) per_device_train_batch_size=2, # 每张显卡上的 batch_size(显存决定) gradient_accumulation_steps=4, # 4 次累积 → 全局有效 batch = 2×4 = 8 warmup_steps=5, # 前 5 步线性预热学习率 max_steps=30, # 训练 30 步(调试阶段);正式可用 num_train_epochs learning_rate=2e-4, # LoRA 常用 2e-4;长训降到 2e-5 logging_steps=1, # 每 1 步打印一次日志 optim="adamw_8bit", # 8-bit AdamW,省显存 weight_decay=0.01, # L2 正则 lr_scheduler_type="linear", # 线性衰减到 0 seed=3407, # 固定随机种子 report_to="swanlab", # 把指标推送到 swanlab ), )
TRL (Transformers Reinforcement Learning,用强化学习训练Transformers模型) 是一个领先的Python库,旨在通过监督微调(SFT)、近端策略优化(PPO)和直接偏好优化(DPO)等先进技术,对基础模型进行训练后优化。TRL 建立在 🤗 Transformers 生态系统之上,支持多种模型架构和模态,并且能够在各种硬件配置上进行扩展。
其中SFTTrainer:一个专门为指令微调设计的训练器,封装了 Hugging Face 的 Trainer,而SFTConfig:配置训练参数的专用类,功能类似 TrainingArguments。而SFTConfig核心参数解释如下:
参数名
含义
dataset_text_field="text"
数据集中用于训练的字段名称,如 text 或 prompt
per_device_train_batch_size=2
每张 GPU 上的 batch size 是 2
gradient_accumulation_steps=4
梯度累计 4 次后才进行一次反向传播(等效于总 batch size = 2 × 4 = 8)
warmup_steps=5
前 5 步进行 warmup(缓慢提升学习率)
max_steps=30
最多训练 30 步(适合调试或快速实验)
learning_rate=2e-4
初始学习率(短训练可用较高值)
logging_steps=1
每训练 1 步就打印一次日志
optim="adamw_8bit"
使用 8-bit AdamW 优化器(节省内存,Unsloth 支持)
weight_decay=0.01
权重衰减,用于防止过拟合
lr_scheduler_type="linear"
线性学习率调度器(从高到低线性下降)
seed=3407
固定随机种子,确保结果可复现
report_to="none"
不使用 WandB 或 TensorBoard 等日志平台(可改为 "wandb")
per_device_train_batch_size=2 每次前向只用了 2 条样本 → 显存占用小,单卡就能跑。
batch_size 决定「每一步真正喂给模型的样本数量」,越大训练越稳,但对显存要求越高。
gradient_accumulation_steps=4 把这 2 条样本算出的梯度先攒起来,攒够 4 次再一次性做反向传播 → 等效于一次性看了 2 × 4 = 8 条样本 ,但显存仍按 2 条算。
此时基本训练过程为:
从 combined_dataset 中取出一批样本(2 条)
重复上面过程 4 次(gradient_accumulation_steps=4)
将累计的梯度用于更新模型一次参数(等效于一次大 batch 更新)
重复上述过程,直到 max_steps=30 停止
设置训练可视化swanlab 🤗HuggingFace Trl | SwanLab官方文档
只需要在你的训练代码中,找到HF的Config部分(比如SFTConfig、GRPOConfig等),添加report_to="swanlab"参数,即可完成集成。
1 2 3 4 5 6 7 8 from trl import SFTConfig, SFTTrainer args = SFTConfig( ..., report_to="swanlab" ) trainer = Trainer(..., args=args)
默认下,项目名会使用你运行代码的目录名。
如果你想自定义项目名,可以设置SWANLAB_PROJECT环境变量:
1 2 import os os.environ["SWANLAB_PROJECT"]="qwen2-sft"
微调执行流程 一切准备就绪后,接下来即可开始进行微调。由于本次微调总共只运行30个step,整个过程并不会很长,实际执行过程如下:
1 trainer_stats = trainer.train()
保存模型 1. 保存 LoRA Adapter
1 2 3 4 # 保存 LoRA adapter(仅几十 MB) save_path = "./lora-adapter" model.save_pretrained(save_path) # LoRA 权重 tokenizer.save_pretrained(save_path) # 词表
以后加载:
1 2 3 4 5 6 7 8 from unsloth import FastLanguageModelmodel, tokenizer = FastLanguageModel.from_pretrained( model_name = "base-model-name-or-path" , max_seq_length = 2048 , load_in_4bit = True , ) model = FastLanguageModel.get_peft_model(model, ...) model.load_adapter(save_path)
2.合并 LoRA → 完整模型
如果你想把 LoRA 权重合并到基座 得到一个独立的大模型(方便推理、上传 Hub):
1 2 3 4 merged_model = model.merge_and_unload() merged_model.save_pretrained("./merged-model" ) tokenizer.save_pretrained("./merged-model" )
合并后就是完整的大模型(GB 级),可直接用 AutoModelForCausalLM.from_pretrained("./merged-model") 加载,不依赖 Unsloth。
微调结果 可视化结果
图表 | Fine-tune-Qwen-8B/rat-2
指标名称
含义
单位/范围提示
常见关注点
train/loss
训练损失(Training Loss)
标量,越小越好
是否持续下降、是否震荡、是否过拟合
train/grad_norm
梯度范数(Gradient Norm)
标量,通常 0.01–1.0 为合理区间
是否爆炸(>10)或消失(<1e-4)
train/learning_rate
学习率(Learning Rate)
标量,如 1e-4、5e-4 等
是否过大导致震荡、过小导致收敛慢
train/epoch
已训练的轮次(Epoch)
标量,1.0 表示完整遍历一次训练集
当前已训练多少轮、是否还需继续训练
train/global_step
全局步数(Global Step)
整数,每个 batch +1
与 epoch 对应,计算已见样本量
对话测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 messages = [ {"role" : "user", "content" : "解决(x + 2)^2 = 0."} ] text = tokenizer.apply_chat_template( messages, tokenize = False, add_generation_prompt = True, # Must add for generation enable_thinking = True, # Disable thinking ) from transformers import TextStreamer _ = model.generate( **tokenizer(text, return_tensors = "pt").to("cuda"), max_new_tokens = 20488, # Increase for longer outputs! temperature = 0.6, top_p = 0.95, top_k = 20, # For thinking streamer = TextStreamer(tokenizer, skip_prompt = True), )