用 Unsloth 在 Colab 上微调 Qwen-3:一份能跑通的实战指南
为什么选 Qwen-3 来微调
DeepSeek 热度正高,但不少开发者已经悄悄转向 Qwen-3 做微调。原因很实在:Qwen-3 在代码、数学、通用能力等基准测试里分数漂亮,DeepSeek-R1、o1、o3-mini、Grok-3、Gemini-2.5-Pro 都被它压了一头。更关键的是,Qwen-3 的 MoE 架构让小模型也能打——Qwen3-30B-A3B 的激活参数量是 Qwen-32B 的 10 倍,而 Qwen3-4B 这种小不点居然能追上 Qwen2.5-72B-Instruct 的表现。
微调这事,说贵确实贵,但用对工具就能降下来。这篇指南全程在 Google Colab 免费 T4 GPU(15GB 显存)上跑通,目标是 Qwen-3-8B 的 4bit 量化版本。
准备工作:库、硬件和数据
要装的包
Unsloth 是核心,它能让 Llama、Mistral、Gemma、Qwen 这些模型的微调速度翻倍,内存省 70%,精度还不掉。配合 PyTorch、Transformers、TRL 就能开工。
!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本地有强卡的话直接 pip install unsloth 就行。
显存怎么算
Qwen-3-8B 的 4bit 量化版大概要吃 8-12GB 显存。Colab T4 的 15GB 刚好够,还能留点余量给训练时的激活值和优化器状态。
数据从哪来
微调需要任务相关的结构化数据。这里混了两个数据集:
unsloth/OpenMathReasoning-mini:增强推理和解题能力,只用它的 COT(思维链)部分mlabonne/FineTome-100k:提升通用对话能力
后面会按 7:3 的比例混合,让模型既能聊天又能算题。
加载模型和分词器
用 Unsloth 的 FastLanguageModel 加载预训练模型,比原生 Transformers 快一截:
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,
load_in_8bit = False,
full_finetuning = False,
)max_seq_length=2048 控制上下文长度,影响内存和精度。load_in_4bit=True 把模型压到 4bit,省显存。full_finetuning=False 表示不做全参数训练,后面用 LoRA 做参数高效微调。
加上 LoRA 适配器
LoRA 的核心思想很简单:冻结原模型,只在关键层旁边插两个小矩阵(低秩分解),训练时只更新这两个小矩阵。这样可训练参数从几十亿降到几百万,显存压力骤减。
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",
random_state = 3433,
)r=32 是 LoRA 的秩,控制矩阵大小。lora_alpha=64 是缩放因子,一般设成 r 的 1-2 倍。target_modules 列出了要加 LoRA 的层,Qwen-3 用的是标准 Transformer 结构,这些层覆盖了注意力机制和 FFN。
处理数据:从原始格式到对话结构
推理数据格式化
OpenMathReasoning-mini 里的数据是问题和解答分离的,需要转成对话格式:
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}
reasoning_conversations = tokenizer.apply_chat_template(
reasoning_dataset.map(generate_conversation, batched=True)["conversations"],
tokenize = False,
)apply_chat_template 把 Python 列表转成模型认识的字符串格式,带特殊 token 区分角色。
通用对话数据标准化
FineTome-100k 的格式不太统一,用 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,
)按 7:3 混合
聊天和推理的比例直接影响模型性格。这里想要一个偏聊天的模型,所以通用对话占 70%,数学题占 30%:
import pandas as pd
chat_percentage = 0.7
non_reasoning_subset = pd.Series(non_reasoning_conversations).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)训练配置和启动
用 TRL 库的 SFTTrainer 做监督微调,关键参数解释一下:
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,
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",
),
)
trainer_stats = trainer.train()per_device_train_batch_size=2:单步喂 2 条样本gradient_accumulation_steps=4:攒 4 步再反向传播,等效 batch size 是 8max_steps=30:演示用的,真训练要设num_train_epochs或更大的max_stepsoptim="adamw_8bit":8bit AdamW 优化器,省显存learning_rate=2e-4:LoRA 微调常用这个量级
跑起来后每步打印 loss,30 步在 T4 上大概几分钟。
推理测试:开不开思考模式
Qwen-3 有个特点:可以强制开启或关闭思考。测同一道题 (x+2)^2=0 看看区别。
关闭思考
messages = [{"role": "user", "content": "Solve (x + 2)^2 = 0."}]
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,
temperature = 0.7, top_p = 0.8, top_k = 20,
streamer = TextStreamer(tokenizer, skip_prompt = True),
)输出很直接:
To solve the equation (x + 2)² = 0, we can take the square root of both sides. This gives us x + 2 = 0. Then, we can subtract 2 from both sides to get x = -2.
没废话,一步出答案。
开启思考
把 enable_thinking=True,max_new_tokens 提到 1024:
text = tokenizer.apply_chat_template(
messages,
tokenize = False,
add_generation_prompt = True,
enable_thinking = True, # 打开
)
_ = model.generate(
**tokenizer(text, return_tensors = "pt").to("cuda"),
max_new_tokens = 1024,
temperature = 0.6, top_p = 0.95, top_k = 20,
streamer = TextStreamer(tokenizer, skip_prompt = True),
)输出里会夹带 <thinking>...</thinking> 标签,模型先把思考过程写一遍,再给答案。简单题看着啰嗦,复杂题这功能就值回票价了。
保存模型
训练完把 LoRA 权重和原模型合并,存成标准格式:
model.save_pretrained_merged(
"Qwen3-16bit-OpenMathReasoning-Finetuned-Merged",
tokenizer,
save_method = "merged_16bit"
)merged_16bit 表示合并后用 16bit 精度存,比 4bit 精度高,比 32bit 省空间。
几个实操要点
显存不够怎么办
- 把
max_seq_length降到 1024 或 512 r从 32 降到 16 或 8per_device_train_batch_size降到 1,同时提高gradient_accumulation_steps- 开
use_gradient_checkpointing="unsloth",用时间换空间
数据比例怎么调
- 要代码助手:多放代码数据集,少放闲聊
- 要数学能力:
OpenMathReasoning比例提到 50% 以上 - 要角色扮演:找对应风格的对话数据
学习率别乱动
LoRA 微调 2e-4 到 5e-4 是安全区。全参数微调才需要 1e-5 这种小学习率。
思考模式什么时候开
- 数学题、逻辑题、需要步骤解释的:开
- 闲聊、翻译、简单问答:关,省 token 也省时间
写在最后
这套流程走下来,用免费资源也能得到一个定制版的 Qwen-3。Unsloth 把显存优化和训练加速做了封装,LoRA 把可训练参数压到可接受范围,剩下的就是数据工程和调参的脏活累活。
微调不是炼丹,但确实需要迭代。先跑通小步数验证流程,再放大训练规模,比一上来就烧几个小时 GPU 更靠谱。