监督微调 (SFT)
SFT 将预训练模型变成能对话的助手,是 post-training 的第一步
在大模型体系中的位置
预训练 (Pre-training) → 学习语言知识和世界知识
↓
监督微调 (SFT) ← 你在这里 → 学习指令跟随能力
↓
偏好对齐 (RLHF/DPO/GRPO) → 学习人类偏好,安全有用预训练模型的目标是 next-token prediction,它学会了语言的统计规律,但不知道如何回答问题。SFT 的目标是将预训练模型从"续写文本"转变为"遵循指令"——通过在 (指令, 回答) 对上训练,模型学会了按照用户的意图生成有结构的回答,并在适当的时候停止输出。
什么是监督微调?
预训练模型的"复读机"问题
预训练模型直接用于对话时,往往会出现重复输出或不停止的情况:
# 预训练模型的典型输出(未经 SFT)
input_ids = tokenizer(['请解释什么是机器学习'], return_tensors='pt')['input_ids']
output_ids = model.generate(input_ids, max_new_tokens=128, do_sample=False)
result = tokenizer.decode(output_ids[0], skip_special_tokens=True)
# 输出: "请解释什么是机器学习 什么是深度学习 什么是强化学习 什么是神经网络
# 什么是卷积神经网络 什么是循环神经网络 什么是注意力机制 什么是 Transformer..."
# → 不断重复、无法停止,这就是"鹦鹉学舌"现象原因在于预训练数据不是每条都有 <EOS> 标记,模型没有学会在适当的时候停止生成(常被称为"复读机"现象)。
SFT 的核心思路
SFT 仍然使用 next-token prediction 的训练目标,但有两个关键差异:
- 训练数据是结构化的指令-回答对,而非原始文本
- Loss Masking:只对回答部分计算损失,不学习如何生成指令
经过 SFT 后,模型能够正确回答问题并预测 <EOS> 停止:
# SFT 后的模型输出
# 输出: "机器学习是人工智能的一个分支,它通过算法让计算机从数据中自动学习规律。
# 核心思想是:给模型大量样本数据,让它找到输入和输出之间的映射关系,
# 从而对新数据做出预测或决策。常见方法包括监督学习、无监督学习和强化学习。"
# → 有逻辑、有结构、能正常停止Full Fine-tuning
全参数微调(Full Fine-tuning)更新模型的所有参数,是最直接的微调方式。
from transformers import AutoTokenizer, AutoModelForCausalLM
# 加载预训练模型,所有参数均可训练
tokenizer = AutoTokenizer.from_pretrained('Qwen/Qwen3-0.6B')
model = AutoModelForCausalLM.from_pretrained('Qwen/Qwen3-0.6B')
# 全参微调:所有参数都参与梯度更新
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-5)| 优点 | 缺点 |
|---|---|
| 效果最好,模型充分适配下游任务 | 显存占用大:7B 模型全参微调需要 ~60GB 显存 |
| 实现简单,无需额外框架 | 每个任务需要存储完整模型副本 |
| 适合有充足算力的场景 | 容易过拟合,尤其在小数据集上 |
LoRA:低秩适配
核心原理
LoRA (Low-Rank Adaptation) 的核心假设是:微调过程中权重的更新矩阵是低秩的。
对于预训练权重
其中
数学直觉:一个
PyTorch 实现 LoRA
import torch
import torch.nn as nn
class LoRALinear(nn.Module):
"""LoRA 线性层:冻结原参数,只训练低秩旁路"""
def __init__(self, original_linear, rank=4, alpha=0.1):
super().__init__()
self.alpha = alpha # 缩放因子
self.dim_in = original_linear.in_features
self.dim_out = original_linear.out_features
self.r = rank # 低秩维度
# 冻结原始权重
self.weight = nn.Parameter(original_linear.weight.data)
self.weight.requires_grad = False
if original_linear.bias is not None:
self.bias = nn.Parameter(original_linear.bias.data)
self.bias.requires_grad = False
else:
self.register_parameter('bias', None)
# LoRA 旁路参数(可训练)
self.WA = nn.Linear(self.dim_in, self.r, bias=False) # 降维: d → r
self.WB = nn.Linear(self.r, self.dim_out, bias=False) # 升维: r → d
# 初始化:WA 置零保证初始时旁路输出为 0
nn.init.constant_(self.WA.weight.data, 0.0)
nn.init.normal_(self.WB.weight.data, mean=0.0, std=0.01)
def forward(self, X):
# 原始前向(无梯度)
bsz, seq_len, dim_in = X.shape
h = X.view(-1, dim_in) @ self.weight.T
h = h.view(bsz, seq_len, -1)
if self.bias is not None:
h += self.bias
# LoRA 旁路(有梯度): h_lora = alpha * X @ WA @ WB
h_lora = h + self.alpha * self.WB(self.WA(X))
return h_lora替换模型中的 Linear 层
def apply_lora_adapter(model, rank=4):
"""递归替换模型中所有 Linear 层为 LoRALinear"""
for name, module in model.named_children():
if isinstance(module, nn.Linear):
new_layer = LoRALinear(module, rank)
setattr(model, name, new_layer)
else:
apply_lora_adapter(module, rank) # 递归处理子模块
return model
# 替换后,只有 LoRA 参数有梯度
apply_lora_adapter(model, rank=4)
for name, param in model.named_parameters():
if param.requires_grad:
print(f"{name} {param.shape}") # 只有 WA, WB 的参数
else:
print(f"{name} no_grad") # 原始参数冻结关键参数详解
| 参数 | 含义 | 经验值 |
|---|---|---|
r (rank) | 低秩维度,控制旁路容量 | 8~64,推荐 16 |
alpha | 缩放因子,控制旁路影响强度 | 通常设为 r 或 2*r |
target_modules | 要加 LoRA 的层 | ["q_proj", "k_proj", "v_proj", "o_proj"] |
lora_dropout | LoRA 旁路的 Dropout | 0.05~0.1 |
使用 PEFT 库(推荐生产用法)
from peft import LoraConfig, get_peft_model
config = LoraConfig(
r=16,
lora_alpha=8,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, config)
model.print_trainable_parameters()
# 输出: trainable params: 1,310,720 || all params: 630,000,000 || trainable%: 0.21%QLoRA:量化 + LoRA
QLoRA 在 LoRA 基础上对原始参数进行量化,进一步降低显存占用。核心思想是:既然原始参数是冻结的,那就把它压缩存储,计算时再解压。
三大技术
- 4-bit NormalFloat (NF4) 量化:利用预训练权重近似正态分布的特点,用 4-bit 量化替代 FP16 存储
- 双重量化 (Double Quantization):对量化的缩放因子再做一次量化,进一步节省显存
- 分页优化器 (Paged Optimizers):利用 CPU 内存处理显存不足时的梯度状态
实现 QLoRA 核心:量化线性层
import bitsandbytes as bnb
class QuantizedLinear(nn.Module):
"""INT8 量化的线性层,前向时动态去量化"""
def __init__(self, module, dim_in, dim_out, block_size=64):
super().__init__()
self.in_features = dim_in
self.out_features = dim_out
self.block_size = block_size
# 量化原始权重
w = module.weight.data
quant_weight, quant_state = bnb.functional.quantize_blockwise(
w, blocksize=self.block_size
)
self.quant_weight = quant_weight # int8 类型,显存减半
self.weight_scale = quant_state # 量化缩放因子
self.weight = None # 删除原始参数节省显存
self.quantized = True
def forward(self, x):
# 计算时动态反量化回 fp16
dequant_weight = bnb.functional.dequantize_blockwise(
self.quant_weight, self.weight_scale, blocksize=self.block_size
)
return nn.functional.linear(x, dequant_weight, None)QLoRA = 量化 + LoRA
# 先对模型进行量化,再加上 LoRA 旁路
apply_quant(model, QuantizedLinear, dim=512, block_size=64)
apply_lora_adapter(model, LoRALinear, r=4)
# 前向时:量化权重无梯度,LoRA 参数有梯度
H = model(X)
loss.backward()
print(model.Wq.weight.quant_weight) # 无梯度,仍为 int8
print(model.Wq.WA.weight.grad) # 有梯度,正常更新Transformers 快捷调用
from transformers import BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 量化
bnb_4bit_quant_type="nf4", # NormalFloat4 量化类型
bnb_4bit_use_double_quant=True, # 双重量化
bnb_4bit_compute_dtype=torch.bfloat16 # 计算精度
)
model = AutoModelForCausalLM.from_pretrained(
'Qwen/Qwen3-0.6B', quantization_config=bnb_config
)
# 1B 模型 fp16 占 ~2GB 显存,4-bit 只占 ~0.5GBSFT 数据集准备
数据格式对比
SFT 数据通常有三种格式:
| 格式 | 结构 | 示例数据集 |
|---|---|---|
| Alpaca | instruction + input + output | tatsu-lab/alpaca |
| ShareGPT | 多轮 conversations 列表 | ShareGPT 系列 |
| OpenAI/ChatML | messages 列表 with role | OpenAI API 格式 |
Chat Template:对话模板
Chat Template 将对话结构化为模型能理解的格式。以 ChatML 格式为例:
DEFINED_SOS_TOKEN = '<|im_start|>'
DEFINED_EOS_TOKEN = '<|im_end|>'
DEFINIED_SYSTEM_PROMPT = '你是一个有帮助的AI助手,请准确详细地回答用户的问题'
# 使用 tokenizer 内置模板
messages = [
{'role': 'system', 'content': DEFINIED_SYSTEM_PROMPT},
{'role': 'user', 'content': '请解释什么是机器学习'},
{'role': 'assistant', 'content': '机器学习是人工智能的一个子领域,通过数据驱动的方式让模型自动学习规律...'},
]
prompt = tokenizer.apply_chat_template(messages, tokenize=False)
# 输出:
# <|im_start|>system
# 你是一个有帮助的AI助手,请准确详细地回答用户的问题<|im_end|>
# <|im_start|>user
# 请解释什么是机器学习<|im_end|>
# <|im_start|>assistant
# 机器学习是人工智能的一个子领域,通过数据驱动的方式让模型自动学习规律...<|im_end|>Token 级别的数据处理(含 Label Masking)
SFT 的关键在于只对 assistant 回答部分计算 loss。通过 is_label 标记区分:
def ChatTemplateToken(example, tokenizer):
"""将对话转为 token ids,并标记哪些 token 参与 loss 计算"""
sos_token_id = tokenizer(DEFINED_SOS_TOKEN).input_ids[0]
eos_token_id = tokenizer(DEFINED_EOS_TOKEN).input_ids[0]
input_ids = [sos_token_id]
is_labels = [0] # 0 表示不计算 loss,1 表示计算 loss
for item in example:
if item['role'] == 'ASSISTANT':
prompt = '\n#' + item['role'] + ':'
content = item['content']
prompt_ids = tokenizer(prompt).input_ids
content_ids = tokenizer(content).input_ids
# ASSISTANT 的角色标记不算 loss,内容和 EOS 算 loss
is_labels += [0]*len(prompt_ids) + [1]*len(content_ids) + [1]
input_ids += prompt_ids + content_ids + [eos_token_id]
else:
prompt = '\n#' + item['role'] + ':' + item['content']
prompt_ids = tokenizer(prompt).input_ids
input_ids += prompt_ids
is_labels += [0]*len(prompt_ids) # 系统/用户部分不算 loss
return input_ids, is_labelsPadding 与 Collate 函数
def paddding_collate_fn(batch_data, pad_token_id, ignore_index=-100):
"""将不等长的样本 padding 到同一长度,并构造 labels"""
bs = len(batch_data)
max_len = max(data['input_ids'].shape[0] for data in batch_data)
input_ids = torch.ones(bs, max_len, dtype=torch.long) * pad_token_id
attention_masks = torch.zeros(bs, max_len, dtype=torch.long)
labels = torch.ones(bs, max_len, dtype=torch.long) * ignore_index # -100
for i in range(bs):
seq_len = batch_data[i]['input_ids'].shape[0]
input_ids[i, :seq_len] = batch_data[i]['input_ids']
attention_masks[i, :seq_len] = 1
# 只有 is_label=1 的位置参与 loss 计算
idx = torch.where(batch_data[i]['is_label'] != 0)[0]
labels[i, idx - 1] = batch_data[i]['input_ids'][idx]
return {'input_ids': input_ids, 'attention_masks': attention_masks, 'labels': labels}注意:
labels中-100的位置会被CrossEntropyLoss(ignore_index=-100)自动忽略。
完整 SFT 训练流程
数据加载
from datasets import load_dataset
from torch.utils.data import DataLoader
# 加载 Alpaca 指令数据集
dataset = load_dataset('tatsu-lab/alpaca')
# DatasetDict({ train: Dataset({ features: ['instruction','input','output','text'], num_rows: 52002 }) })
# 转换为对话格式
def dataset_to_messageslist(dataset):
messages = []
for item in dataset['train']:
messages.append([
{'role': 'SYSTEM', 'content': DEFINIED_SYSTEM_PROMPT},
{'role': 'USER', 'content': item['instruction'] + item['input']},
{'role': 'ASSISTANT', 'content': item['output']},
])
return messages
# 构建 DataLoader
dataset_train = TokenSFTDataset(dataset_instruction, tokenizer)
collate_fn = PaddingCollateFunction(pad_token_id=pad_token_id, ignore_index=-100)
dataloader = DataLoader(dataset_train, batch_size=8, shuffle=True, collate_fn=collate_fn)训练循环
import torch.optim as optim
from tqdm import tqdm
learning_rate = 1e-5
epochs = 1
loss_fn = nn.CrossEntropyLoss(ignore_index=-100)
optimizer = optim.AdamW(model.parameters(), lr=learning_rate)
for epoch in range(epochs):
train_bar = tqdm(dataloader, desc=f'Epoch {epoch+1}/{epochs}')
for step, batch in enumerate(train_bar):
optimizer.zero_grad()
bsz, seq_len = batch['input_ids'].shape
# 前向传播
output = model(input_ids=batch['input_ids'],
attention_mask=batch['attention_masks'])
logits = output.logits
# 计算 loss(只在 label != -100 的位置)
loss = loss_fn(
logits.view(bsz * seq_len, -1), # (B*T, V)
batch['labels'].view(bsz * seq_len) # (B*T,)
)
# 反向传播 + 更新
loss.backward()
optimizer.step()
if step % 10 == 0:
train_bar.set_postfix(loss=f'{loss.item():.4f}')推理验证
def generate(model, tokenizer, prompt, max_new_tokens=128):
"""使用 SFT 模型生成回答"""
messages = [
{'role': 'SYSTEM', 'content': DEFINIED_SYSTEM_PROMPT},
{'role': 'USER', 'content': prompt},
{'role': 'ASSISTANT', 'content': ''},
]
input_ids, _ = ChatTemplateToken(messages, tokenizer)
input_ids = torch.tensor([input_ids[:-1]], dtype=torch.long) # 去掉末尾 EOS
output_ids = model.generate(input_ids, max_new_tokens=max_new_tokens, do_sample=False)
return tokenizer.decode(output_ids[0], skip_special_tokens=True)
result = generate(model, tokenizer, '请解释什么是机器学习')
# 输出: "机器学习是人工智能的一个分支,它通过算法让计算机从数据中自动学习规律。
# 核心思想是:给模型大量样本数据,让它找到输入和输出之间的映射关系,
# 从而对新数据做出预测或决策。常见方法包括监督学习、无监督学习和强化学习。"训练参数调优指南
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 学习率 | 1e-5 ~ 5e-5 | 比预训练低 1-2 个数量级 |
| Batch Size | 8~32 | 配合梯度累积使用 |
| Epochs | 1~3 | 过多会过拟合 |
| Warmup | 总步数的 3%~10% | 稳定训练初期 |
| 序列长度 | 512~2048 | 根据数据分布决定 |
| 梯度裁剪 | 1.0 | 防止梯度爆炸 |
数据质量 > 数据数量:LIMA 论文证明,1000 条精心标注的高质量数据优于 10 万条低质量数据。
TRL SFTTrainer 实战
TRL (Transformer Reinforcement Learning) 提供了开箱即用的 SFTTrainer,是目前最流行的 SFT 训练工具之一。它封装了 Hugging Face Trainer,并内置了 chat template 处理、packing、loss masking 等 SFT 专属功能。
基本用法
from trl import SFTConfig, SFTTrainer
training_args = SFTConfig(
output_dir="sft-model",
max_seq_length=2048,
packing=True, # 多条短样本打包为一条长序列
num_train_epochs=3,
)
trainer = SFTTrainer(
model="Qwen/Qwen2-0.5B",
args=training_args,
train_dataset=dataset,
)
trainer.train()为什么推荐 SFTTrainer?
相比手写训练循环,SFTTrainer 自动处理了 chat template 转换、loss masking、序列打包等繁琐细节,让你专注于数据质量和超参调优。
Chat Template 处理
TRL 自动调用模型 tokenizer 的 chat template 将对话格式转换为模型输入:
- 数据集格式:
{"messages": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]} - 自动添加 role tokens(如
<|im_start|>user) - 自动对 assistant 部分做 loss masking,user 和 system 部分不参与损失计算
数据格式详解
SFTTrainer 支持三种数据格式:
| 格式 | 数据列 | 适用场景 |
|---|---|---|
| Conversational | messages 列(标准 chat 格式) | 多轮对话、ChatBot |
| Instruction | prompt + completion 列 | 单轮指令跟随 |
| Language modeling | 纯 text 列 | 续写式微调 |
Conversational 格式示例
dataset = [
{"messages": [
{"role": "user", "content": "解释什么是梯度下降"},
{"role": "assistant", "content": "梯度下降是一种优化算法..."},
]}
]Packing(序列打包)
Packing 是提升 SFT 训练效率的关键技巧:
- 将多条短训练样本打包成一条长序列,提高 GPU 利用率
- 使用 attention mask 防止跨样本注意力泄露,保证训练正确性
- 设置
packing=True即可启用 - 特别适合训练数据长度差异大的场景
不使用 Packing:
[样本1--padding--padding] [样本2--------padding] [样本3-padding-padding]
↑ GPU 大量时间在处理 padding token
使用 Packing:
[样本1--样本2--------样本3] [样本4------样本5------]
↑ GPU 利用率大幅提升WARNING
Packing 会改变每个 batch 的有效样本数,可能需要相应调整学习率。
LoRA 集成
SFTTrainer 原生支持 PEFT,只需传入 peft_config 即可启用 LoRA 训练:
from peft import LoraConfig
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
)
trainer = SFTTrainer(
model="meta-llama/Llama-3-8B",
args=training_args,
train_dataset=dataset,
peft_config=peft_config, # 直接传入LoRA配置
)这种方式将 SFT 训练和 LoRA 微调无缝结合,无需手动修改模型结构。
多轮对话训练
实际场景中,用户与模型的交互往往是多轮对话。多轮对话训练需要特别注意 loss 计算和数据格式。
数据格式
# 多轮对话数据格式
dataset = [
{"messages": [
{"role": "system", "content": "你是一个有帮助的助手"},
{"role": "user", "content": "什么是 Transformer?"},
{"role": "assistant", "content": "Transformer 是一种基于自注意力的..."},
{"role": "user", "content": "它的核心创新是什么?"},
{"role": "assistant", "content": "核心创新是自注意力机制..."},
]}
]训练要点
- Loss Masking:每轮只对 assistant 回复计算 loss,system 和 user 部分被 mask 掉
- Chat Template:自动添加 role tokens,正确分隔每轮对话
- Truncation 策略:长对话可能超过
max_seq_length,需要合理截断
长对话截断建议
优先保留最后几轮对话(最相关的上下文),截断最早的轮次。也可以将长对话拆分为多条训练样本,每条保留完整的上下文窗口。
多轮对话 Loss Masking 示意:
[system] 你是一个助手 [user] 什么是 Transformer? [assistant] Transformer 是...
↑ 不计算 loss ↑ 不计算 loss ↑ 计算 loss ✓
[user] 核心创新? [assistant] 核心创新是自注意力...
↑ 不计算 loss ↑ 计算 loss ✓训练加速与显存优化
当模型规模增大或数据量增长时,训练效率和显存管理成为关键瓶颈。以下是常用的优化手段:
Gradient Checkpointing
以计算换显存——前向传播时不保存中间激活值,反向传播时重新计算:
training_args = SFTConfig(
gradient_checkpointing=True,
# ... 其他参数
)通常可节省 50%-70% 显存,代价是训练速度下降约 20%-30%。
Mixed Precision 混合精度训练
使用 bf16 或 fp16 替代 fp32,显存减半、计算加速:
training_args = SFTConfig(
bf16=True, # 推荐在 A100/H100 上使用 bf16
# fp16=True, # 旧款 GPU 可用 fp16
)bf16 vs fp16
bf16 的动态范围与 fp32 相同,不容易出现溢出,是目前主流选择。fp16 需要配合 loss scaling 使用。
DeepSpeed ZeRO
ZeRO 将优化器状态、梯度、模型参数分阶段切分到多张 GPU 上:
| 阶段 | 切分内容 | 显存节省 |
|---|---|---|
| ZeRO-1 | Optimizer States | ~4x |
| ZeRO-2 | + Gradients | ~8x |
| ZeRO-3 | + Parameters | ~Nx(N 为 GPU 数) |
# 使用 DeepSpeed ZeRO-2 启动训练
deepspeed --num_gpus=4 train.py --deepspeed ds_config_zero2.jsonGradient Accumulation
小批次累积模拟大 batch,在不增加显存的前提下提升有效 batch size:
training_args = SFTConfig(
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # 有效 batch size = 2 × 4 = 8
)Flash Attention
Flash Attention 通过 IO 感知的分块计算,将 attention 的显存复杂度从
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3-8B",
attn_implementation="flash_attention_2",
)vLLM 集成
TRL 支持使用 vLLM 做生成加速,主要用于需要在线生成的训练算法(如 GRPO、PPO)。vLLM 的 PagedAttention 和 continuous batching 可以大幅加速生成过程。
各优化技术对比总结
| 技术 | 节省显存 | 速度影响 | 适用场景 |
|---|---|---|---|
| Gradient Checkpointing | ✅ 大幅 | ⬇ 略慢 | 单卡大模型 |
| Mixed Precision (bf16) | ✅ 减半 | ⬆ 加速 | 所有场景 |
| DeepSpeed ZeRO | ✅ 线性扩展 | ➡ 通信开销 | 多卡训练 |
| Gradient Accumulation | ❌ 不变 | ➡ 不变 | 模拟大 batch |
| Flash Attention | ✅ 显著 | ⬆ 加速 | 长序列训练 |
| Packing | ❌ 不变 | ⬆ 加速 | 短样本数据集 |
苏格拉底时刻 💡
- LoRA 的低秩假设为什么成立? 微调只是让模型适配新任务,权重变化集中在少数方向上,不需要改变全部参数空间。
- SFT 为什么只需 1-3 epochs? 预训练模型已经学会了语言能力,SFT 只是"教它怎么用",少量训练即可。过多训练反而会导致过拟合——模型记住了训练数据的模式而非泛化能力。
- Loss Masking 为什么重要? 如果不做 masking,模型会学习生成用户提问的模式,导致输出中混入提问语气的内容。
- LoRA 的 rank 选多少? rank=8 适合简单任务(格式、风格),rank=64 适合复杂任务(新知识、新领域)。过高的 rank 接近全参微调,失去了效率优势。
常见问题 & 面试考点
- Q: LoRA 和 Full Fine-tuning 效果差多少? 在大多数任务上差距很小(<2%),但在需要大量新知识注入的场景下 Full FT 更优。
- Q: QLoRA 精度如何? 量化会引入精度损失,一般比 LoRA 略差。在显存受限时是性价比最高的选择。
- Q: SFT 数据中的 system prompt 有什么用? 定义模型的角色和行为边界,是控制模型行为的重要手段。
- Q: 为什么 SFT 后模型的通用能力可能下降? 这被称为"对齐税"(Alignment Tax),过度 SFT 会让模型过度拟合特定格式,损失预训练阶段学到的通用能力。
推荐资源
- LoRA: Low-Rank Adaptation of Large Language Models - LoRA 原始论文
- QLoRA: Efficient Finetuning of Quantized Language Models - QLoRA 论文
- LIMA: Less Is More for Alignment - 少量高质量数据的力量
- LLaMA-Factory - 一站式微调框架
- Hugging Face TRL - Transformer Reinforcement Learning
- Hugging Face PEFT - 参数高效微调库