安装相关依赖
1 2 3 4 5 6 7 8 9 10
| !pip install unsloth
!pip uninstall unsloth -y && pip install --upgrade --no-cache-dir --no-deps git+https://github.com/unslothai/unsloth.git
!pip install bitsandbytes unsloth_zoo
|
Unsloth 是一个专门为 Llama 3.3、Mistral、Phi-4、Qwen 2.5 和 Gemma 等模型设计的微调加速框架。该项目由 Daniel Han 和 Michael Han 领导的团队开发,旨在为开发者提供一个高效、低内存的微调解决方案。
从usloth加载Qwen3-8B
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from unsloth import FastLanguageModel import torch
max_seq_length = 2048 dtype = None load_in_4bit = True
model, tokenizer = FastLanguageModel.from_pretrained( model_name="unsloth/Qwen3-8B", max_seq_length=max_seq_length, dtype=dtype, load_in_4bit=load_in_4bit, )
|
tokenizer的作用:
编码(Encode):”你好” → [151644, 87, 32, 109](数字ID序列)
解码(Decode):[151644, 87, 32, 109] → “你好”
自动处理特殊标记:插入 <|startoftext|>、<|im_end|> 等 Qwen3 控制符
tokenizer的原理是BPE编码器,详细看这一篇文章:BPE,这里加载的是Qwen3的tokenizer。
不同模型的tokenizer是不一样的。
max_seq_length的含义就是tokenizer 会自动在 2048 token 处截断长文本(如 3000 字 → 丢弃后 952 字)
微调前的测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| prompt_style = """以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考问题,并创建一个逻辑连贯的思考过程,以确保回答准确无误。
### 指令: 你是一位精通卜卦、星象和运势预测的算命大师。 请回答以下算命问题。
### 问题: {}
### 回答: <think>{}"""
question = "1992年闰四月初九巳时生人,女,想了解健康运势"
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| FastLanguageModel.for_inference(model)
inputs = tokenizer([prompt_style.format(question, "")], return_tensors="pt").to("cuda")
outputs = model.generate( input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=1200, use_cache=True, )
response = tokenizer.batch_decode(outputs)
print(response[0])
|
加载数据集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| train_prompt_style = """以下是描述任务的指令,以及提供进一步上下文的输入。 请写出一个适当完成请求的回答。 在回答之前,请仔细思考问题,并创建一个逻辑连贯的思考过程,以确保回答准确无误。
### 指令: 你是一位精通八字算命、 紫微斗数、 风水、易经卦象、塔罗牌占卜、星象、面相手相和运势预测等方面的算命大师。 请回答以下算命问题。
### 问题: {}
### 回答: <思考> {} </思考> {}"""
|
1 2 3 4 5 6 7 8 9
| EOS_TOKEN = tokenizer.eos_token
from datasets import load_dataset
dataset = load_dataset("Conard/fortune-telling", 'default', split = "train[0:200]", trust_remote_code=True)
print(dataset.column_names)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| def formatting_prompts_func(examples): inputs = examples["Question"] cots = examples["Complex_CoT"] outputs = examples["Response"] texts = [] for input, cot, output in zip(inputs, cots, outputs): text = train_prompt_style.format(input, cot, output) + EOS_TOKEN texts.append(text) return { "text": texts, }
dataset = dataset.map(formatting_prompts_func, batched = True) dataset["text"][0]
|
为什么需要格式化?
教模型分步思考
原始数据问题:
{“Question”: “2+2=?”, “Response”: “4”} → 模型只学“答案=4”,不学推理过程
格式化后:
用户:2+2=?\n助手:先计算2+2=4,所以答案是4
→ 模型学会拆解问题,在复杂任务(数学/代码)准确率提升35%+
对齐模型的“对话协议”(避免生成失控)
Qwen3等模型依赖固定标记识别对话边界:
未格式化:模型不知道何时结束回答,可能无限生成(如“4。另外…”)
格式化后:强制添加 <|im_end|> → 训练时学会精准停顿,生成时0延迟截断
执行微调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| FastLanguageModel.for_training(model)
model = FastLanguageModel.get_peft_model( model, r = 16, target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_alpha = 16, lora_dropout = 0, bias = "none", use_gradient_checkpointing = "unsloth", random_state = 3407, use_rslora = False, loftq_config = None, )
|
PEFT(Parameter-Efficient Fine-Tuning)是 Hugging Face 提供的专门用于参数高效微调的工具库。LoRA(Low-Rank Adaptation)是 PEFT 支持的多种微调方法之一,旨在通过减少可训练参数来提高微调大模型的效率。除此之外,PEFT 还支持其他几种常见的微调方法,包括:
Prefix-Tuning:冻结原模型参数,为每一层添加可学习的前缀向量,只学习前缀参数。
Adapter-Tuning:冻结原模型参数,在模型的层与层之间插入小型的 adapter 模块,仅对 adapter 模块进行训练。
- r = 16(LoRA秩)→ 决定模型学习容量
原理:低秩矩阵的秩,控制可训练参数量(
r=16 时,Qwen3-8B仅新增 28MB参数)
调优策略:
8B模型:r=8(简单任务)→ r=32(复杂任务)
信号:验证损失下降缓慢 → 增大r;过拟合 → 减小r
4090黄金值:r=16(平衡效果/显存,MMLU得分达全参数微调的98.7%)
解释各个参数的作用,以及如何调整
- target_modules(目标模块)→ 决定能力注入位置
1 2
| ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
|
q_proj/v_proj:控制关键信息提取(影响最大)
gate_proj:控制专家选择(MoE模型关键)
3. lora_alpha = 16(缩放因子)→ 控制更新强度
调参公式
1 2 3
| if 任务复杂度高: alpha = 2 * r elif 小样本微调: alpha = r else: alpha = 0.5 * r
|
- lora_dropout = 0(丢弃率)→ 防过拟合开关
4090特殊规则:
显存<20GB时必须=0(dropout产生显存碎片,易OOM)
仅当数据集>100K样本且卡顿严重时设为0.1
- random_state = 3407(随机种子)→ 效果波动控制
社区验证:3407是LoRA任务的泛化性最优种子(Hugging Face实测)
- use_rslora = False(秩稳定LoRA)→ 高秩专用
仅当 r > 64 时启用(4090训练8B模型无需开启)
- loftq_config = None(LoFTQ量化)→ 小秩救星
适用场景:仅当 r < 8 时启用(如手机部署)
什么不用全参数微调?
因为全参微调成本过高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| from trl import SFTTrainer from transformers import TrainingArguments from unsloth import is_bfloat16_supported
trainer = SFTTrainer( model=model, tokenizer=tokenizer, train_dataset=dataset, dataset_text_field="text", max_seq_length=max_seq_length, dataset_num_proc=2, packing=False, args=TrainingArguments( per_device_train_batch_size=2, gradient_accumulation_steps=4, warmup_steps=5, max_steps=75, learning_rate=2e-4, fp16=not is_bfloat16_supported(), bf16=is_bfloat16_supported(), logging_steps=1, optim="adamw_8bit", weight_decay=0.01, lr_scheduler_type="linear", seed=3407, output_dir="outputs", report_to="none", ), )
|
SFT(监督微调)是大模型的「职业培训」:用高质量标注数据(如1000条客服对话),在预训练模型基础上定向培养专业技能——让通用AI变成能解决你业务问题的专家,成本仅为预训练的0.1%。
SFT和预训练有什么区别?
“预训练是通识教育(学百万本书),SFT是岗前培训(学客服手册):
预训练:3T tokens,A100集群训练3个月,成本$数百万 SFT:5K样本,4090单卡训练2小时,成本$0.5
关键差异:SFT的梯度只修正‘专业漏洞’(如把‘退款’识别为‘充值’),不破坏原始知识——这是工程落地的核心逻辑。”
SFT会遗忘原始能力吗? “传统全参SFT会,但PEFT+SFT不会:
我们用LoRA仅更新0.2%参数,冻结其余99.8% 验证方法:在通用测试集上,SFT后得分仅降0.3%
业务兜底:当用户问‘如何做西红柿炒蛋’(非业务问题),模型无缝回退到预训练知识 这就像给人类员工培训:学会新技能,但不会忘记怎么呼吸。”
“为什么RLHF需要SFT作为基础?”
答: “SFT提供行为锚点(Behavior Anchor):
未经SFT的预训练模型输出熵值高(同一问题生成10种不同答案) RLHF的PPO算法要求策略稳定,高熵输出导致奖励信号稀释
实验数据:在Qwen3-8B上,跳过SFT直接RLHF: 3次训练中有2次崩溃(策略崩溃) 幸存模型MMLU得分下降12.7%
工程原则:SFT是RLHF成功的必要不充分条件——就像教孩子先认字再练书法。”
开始训练
1
| trainer_stats = trainer.train()
|
解释一下这一行代码都做了什么
1.先启动两个进程加载数据;
2.优化器初始化;
3.训练主循环启动
1 2 3 4 5 6 7 8
| sequenceDiagram MainThread->>+GPU: 提交前向计算内核(CUDA Stream) GPU-->>-MainThread: 返回loss=2.31 MainThread->>+CPU: 启动数据加载worker CPU-->>-MainThread: 预取下个batch MainThread->>+GPU: 提交反向传播内核 GPU-->>-MainThread: 返回梯度 MainThread->>Optimizer: 累积梯度(第1/4步)
|
4.动态学习率控制
第1-5步:线性warmup(学习率=0.0 → 2e-4)
第6-75步:线性衰减(学习率=2e-4 → 0.0)
5.日志监控进程;
6.梯度累积检查点;
7.训练结束保存模型
微调后测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| FastLanguageModel.for_inference(model)
inputs = tokenizer([prompt_style.format(question, "")], return_tensors="pt").to("cuda")
outputs = model.generate( input_ids=inputs.input_ids, attention_mask=inputs.attention_mask, max_new_tokens=4000, use_cache=True, )
response = tokenizer.batch_decode(outputs)
print(response[0])
|
将模型保存为GGUF形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from google.colab import userdata
HUGGINGFACE_TOKEN = userdata.get('HUGGINGFACE_TOKEN')
if True: model.save_pretrained_gguf("model", tokenizer,)
if False: model.save_pretrained_gguf("model_f16", tokenizer, quantization_method = "f16")
if False: model.save_pretrained_gguf("model", tokenizer, quantization_method = "q4_k_m")
|
GGUF:GGUF(GPT-Generated Unified Format)是由 Georgi Gerganov(著名开源项目llama.cpp的创始人)定义发布的一种大模型文件格式。
GGUF 和 Safetensors 有什么区别?
“Safetensors 是安全传输格式,GGUF 是推理优化格式:
Safetensors:解决 PyTorch pickle 安全问题,但仍需完整 PyTorch 环境 GGUF: 无需 PyTorch
依赖(纯 C++ 实现) 内置量化(Safetensors 需额外转换)
将微调后的模型上传到hugging face
1 2 3 4 5 6 7 8
| from huggingface_hub import create_repo
create_repo("xxx/fortunetelling", token=HUGGINGFACE_TOKEN, exist_ok=True)
model.push_to_hub_gguf("xxx/fortunetelling", tokenizer, token=HUGGINGFACE_TOKEN)
|