Datawhale干货
作者:吴锦凤,Datawhale优秀学习者
开门见山,直接给大家展示微调前后的效果。
微调前:
微调后:
在此处可以看到很明显大模型进行微调后口吻已经发生了更改。据笔者使用下来的记录表示,微调后的大模型思考时间更加短暂。
接下来,让我们一起逐步完成微调实践,共同优化模型性能!
微调就像给一个“学霸”补课,让它从“通才”变成某个领域的“专家”。
此处以本文进行微调的医学数据进行举例: 假设你有一个很聪明的朋友,他读过全世界的书(相当于大模型的预训练阶段),能和你聊历史、科学、文学等各种话题。 但如果你需要他帮你看医学报告段),能和你聊历史、科学、文学等各种话题。 但如果你需要他帮你看医学报告,虽然他懂一些基础知识,但可能不够专业。这时候,你给他一堆医学书籍和病例,让他专门学习这方面的知识(这就是微调),他就会变得更擅长医疗领域的问题。
想象你有一个会画小猫的机器人🤖(这就是预训练模型)。现在你想让它学会画戴帽子的小猫🎩🐱。不需要从头教它画画,只需要给它看很多"戴帽子小猫"的图片,然后说:"保持原来的画画能力,但要学会加帽子哦!" 这就是微调!
案例1:智能音箱调方言
案例2:相机滤镜原理
[通用城堡]
▸ 比喻:就像网购的"标准款城堡积木套装",有城墙、塔楼、尖顶,能当普通房子用。
▸ 对应技术:预训练模型(比如 ChatGPT),已经学会通用语言能力,但不够专业。
① 拆尖顶 → 改圆顶
[尖顶改圆顶]
▸ 操作:把塔顶的尖积木换成圆积木,更温和可爱。
▸ 技术含义:微调模型顶层参数(比如修改分类头),让输出风格更适合儿童对话。
② 加装旋转门[旋转门]
▸ 操作:在门口插入一个可旋转的积木模块,不破坏原有门结构。
▸ 技术含义:插入适配器模块(Adapter),让模型新增儿科医学术语理解能力,且不干扰原有知识。
③ 涂装医院标志
[医院标志]
▸ 操作:在城堡外墙贴上"十字符号"和卡通动物贴纸。
▸ 技术含义:特征空间偏移(Feature Shift),调整模型内部表示,让它更关注医疗相关词汇和童趣表达。
[儿童医院]
▸ 成果:改装后的城堡能接待小患者,有玩具区、温和的医生(圆顶),还有专用医疗设备(旋转门)。
▸ 技术含义:通过轻量改造,通用模型变成"儿科医疗问答机器人",专精儿童健康咨询。
显卡:NVIDIA GeForce RTX 4060
CPU:Intel Core i7-13700H
内存:16 G(因为家庭电脑所以日常状态是 8.8/15.7 GB)
本文数据集来源,魔搭社区的 medical-o1-reasoning-SFT。
本文主要说明,数据集格式是:
在 DeepSeek 的蒸馏模型微调过程中,数据集中引入 Complex_CoT(复杂思维链)是关键设计差异。若仅使用基础问答对进行训练,模型将难以充分习得深度推理能力,导致最终性能显著低于预期水平。这一特性与常规大模型微调的数据要求存在本质区别。
需要引入的库:
pip install torch transformers peft datasets matplotlib accelerate safetensors
import torch
import matplotlib.pyplot as plt
from transformers import (
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
TrainerCallback
)
from peft import LoraConfig, get_peft_model
from datasets import load_dataset
import os
# 配置路径(根据实际路径修改)
model_path = r"你的模型路径" # 模型路径
data_path = r"你的数据集路径" # 数据集路径
output_path = r"你的保存微调后的模型路径" # 微调后模型保存路径
# 强制使用GPU
assert torch.cuda.is_available(), //"必须使用GPU进行训练!"
device = torch.device("cuda")
# 自定义回调记录Loss
class LossCallback(TrainerCallback):
def __init__(self):
self.losses = []
def on_log(self, args, state, control, logs=None, **kwargs):
if "loss" in logs:
self.losses.append(logs["loss"])
# 数据预处理函数
def process_data(tokenizer):
dataset = load_dataset("json", data_files=data_path, split="train[:1500]")
def format_example(example):
instruction = f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"
inputs = tokenizer(
f"{instruction}\n### 答案:\n{example['Response']}<|endoftext|>",
padding="max_length",
truncation=True,
max_length=512,
return_tensors="pt"
)
return {"input_ids": inputs["input_ids"].squeeze(0), "attention_mask": inputs["attention_mask"].squeeze(0)}
return dataset.map(format_example, remove_columns=dataset.column_names)
# LoRA配置
peft_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 训练参数配置
training_args = TrainingArguments(
output_dir=output_path,
per_device_train_batch_size=2, # 显存优化设置
gradient_accumulation_steps=4, # 累计梯度相当于batch_size=8
num_train_epochs=3,
learning_rate=3e-4,
fp16=True, # 开启混合精度
logging_steps=20,
save_strategy="no",
report_to="none",
optim="adamw_torch",
no_cuda=False, # 强制使用CUDA
dataloader_pin_memory=False, # 加速数据加载
remove_unused_columns=False # 防止删除未使用的列
)
def main():
# 创建输出目录
os.makedirs(output_path, exist_ok=True)
# 加载tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.pad_token = tokenizer.eos_token
# 加载模型到GPU
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16,
device_map={"": device} # 强制使用指定GPU
)
model = get_peft_model(model, peft_config)
model.print_trainable_parameters()
# 准备数据
dataset = process_data(tokenizer)
# 训练回调
loss_callback = LossCallback()
# 数据加载器
def data_collator(data):
batch = {
"input_ids": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device),
"attention_mask": torch.stack([torch.tensor(d["attention_mask"]) for d in data]).to(device),
"labels": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device) # 使用input_ids作为labels
}
return batch
# 创建Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=data_collator,
callbacks=[loss_callback]
)
# 开始训练
print("开始训练...")
trainer.train()
# 保存最终模型
trainer.model.save_pretrained(output_path)
print(f"模型已保存至:{output_path}")
# 绘制训练集损失Loss曲线
plt.figure(figsize=(10, 6))
plt.plot(loss_callback.losses)
plt.title("Training Loss Curve")
plt.xlabel("Steps")
plt.ylabel("Loss")
plt.savefig(os.path.join(output_path, "loss_curve.png"))
print("Loss曲线已保存")
if __name__ == "__main__":
main()
功能总结:导入项目依赖的第三方库,包括 PyTorch 基础库、HuggingFace 工具库、可视化库等。
import torch
import matplotlib.pyplot as plt
from transformers import ( # HuggingFace Transformer模型工具
AutoTokenizer,
AutoModelForCausalLM,
TrainingArguments,
Trainer,
TrainerCallback
)
from peft import LoraConfig, get_peft_model # 参数高效微调库
from datasets import load_dataset # 数据集加载工具
import os # 系统路径操作
功能总结:配置模型/数据路径,强制检查GPU可用性
# 配置路径(根据实际路径修改)
model_path = r"你的模型路径" # 预训练模型存放路径
data_path = r"你的数据集路径" # 训练数据路径(JSON格式)
output_path = r"你的保存微调后的模型路径" # 微调后模型保存位置
# 强制使用GPU(确保CUDA可用)
assert torch.cuda.is_available(), "必须使用GPU进行训练!"
device = torch.device("cuda") # 指定使用CUDA设备
3.
自定义训练回调类功能总结:实现自定义回调,在模型训练过程中,实时记录损失值(Loss)的变化。损失值是用来衡量模型预测结果与真实结果之间的差距的,损失值越小,说明模型的表现越好。
class LossCallback(TrainerCallback):
def __init__(self):
self.losses = [] # 存储损失值的列表
# 当训练过程中有日志输出时触发
def on_log(self, args, state, control, logs=None, **kwargs):
if "loss" in logs: # 过滤并记录损失值
self.losses.append(logs["loss"])
功能总结:加载并格式化训练数据,将原始数据集转换为模型可以理解的格式。
def process_data(tokenizer):
# 从JSON文件加载数据集(仅取前1500条)
dataset = load_dataset("json", data_files=data_path, split="train[:1500]")
# 单条数据格式化函数
def format_example(example):
# 拼接指令和答案(固定模板)
instruction = f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"
inputs = tokenizer(
f"{instruction}\n### 答案:\n{example['Response']}<|endoftext|>", # 添加结束符
padding="max_length", # 填充至最大长度
truncation=True, # 超长截断
max_length=512, # 最大序列长度
return_tensors="pt" # 返回PyTorch张量
)
# 返回处理后的输入(移除batch维度)
return {"input_ids": inputs["input_ids"].squeeze(0),
"attention_mask": inputs["attention_mask"].squeeze(0)}
# 应用格式化函数并移除原始列
return dataset.map(format_example, remove_columns=dataset.column_names)
instruction = f"诊断问题:{example['Question']}\n详细分析:{example['Complex_CoT']}"
inputs = tokenizer(
f"{instruction}\n### 答案:\n{example['Response']}<|endoftext|>", # 添加结束符
padding="max_length", # 填充至最大长度
truncation=True, # 超长截断
max_length=512, # 最大序列长度
return_tensors="pt" # 返回PyTorch张量
)
return {"input_ids": inputs["input_ids"].squeeze(0),
"attention_mask": inputs["attention_mask"].squeeze(0)}
return dataset.map(format_example, remove_columns=dataset.column_names)
功能总结:配置LoRA参数,指定要适配的模型模块。
peft_config = LoraConfig(
r=16, # LoRA秩(矩阵分解维度)
lora_alpha=32, # 缩放系数(控制适配器影响强度)
target_modules=["q_proj", "v_proj"], # 要适配的注意力模块(查询/值投影)
lora_dropout=0.05, # 防止过拟合的Dropout率
bias="none", # 不训练偏置参数
task_type="CAUSAL_LM" # 任务类型(因果语言模型)
)
"相当于给AI的‘学习笔记’设置 16 页的篇幅限制"
→ 页数少(r小):学得快但可能漏细节
→ 页数多(r大):学得细但速度慢
就像是,音量旋钮的大小决定了声音的响亮程度。如果旋钮转得太大,声音可能会震耳欲聋,甚至让人难以忍受;如果旋钮转得太小,声音又可能太小,听不清楚。
过大的 lora_alpha 可能会导致模型的训练变得不稳定,就像声音太大可能会让人感到不适一样。可能会导致过拟合,因为模型对训练数据的细节调整过于敏感。
较小的 lora_alpha 会导致模型在训练过程中会更保守地调整权重,训练过程更稳定,但适应新任务的速度可能会较慢。
功能总结:设置训练超参数和硬件相关选项。
training_args = TrainingArguments(
output_dir=output_path, # 输出目录(模型/日志)
per_device_train_batch_size=2, # 单GPU批次大小(显存优化)
gradient_accumulation_steps=4, # 梯度累积步数(等效batch_size=8)
num_train_epochs=3, # 训练轮次
learning_rate=3e-4, # 初始学习率
fp16=True, # 启用混合精度训练(节省显存)
logging_steps=20, # 每隔20步记录日志
save_strategy="no", # 不保存中间检查点
report_to="none", # 禁用第三方报告(如W&B)
optim="adamw_torch", # 优化器类型
no_cuda=False, # 强制使用CUDA
dataloader_pin_memory=False, # 禁用锁页内存(加速数据加载)
remove_unused_columns=False # 保留未使用的列(避免数据错误)
)
功能总结:整合所有组件,执行完整训练流程。
def main():
# 创建输出目录(如果不存在)
os.makedirs(output_path, exist_ok=True)
# 加载Tokenizer并设置填充符
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.pad_token = tokenizer.eos_token # 使用EOS作为填充符
# 加载预训练模型(半精度+指定GPU)
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16, # 半精度加载(节省显存)
device_map={"": device} # 指定使用的GPU设备
)
# 应用LoRA适配器
model = get_peft_model(model, peft_config)
model.print_trainable_parameters() # 打印可训练参数量
# 准备训练数据集
dataset = process_data(tokenizer)
# 初始化损失记录回调
loss_callback = LossCallback()
# 数据整理函数(构造批次)
def data_collator(data):
batch = {
"input_ids": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device),
"attention_mask": torch.stack([torch.tensor(d["attention_mask"]) for d in data]).to(device),
"labels": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device) # 标签=输入(因果LM任务)
}
return batch
# 初始化Trainer
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=data_collator, # 自定义数据整理
callbacks=[loss_callback] # 添加回调
)
# 执行训练
print("开始训练...")
trainer.train()
# 保存微调后的模型
trainer.model.save_pretrained(output_path)
print(f"模型已保存至:{output_path}")
# 绘制损失曲线
plt.figure(figsize=(10, 6))
plt.plot(loss_callback.losses)
plt.title("Training Loss Curve")
plt.xlabel("Steps")
plt.ylabel("Loss")
plt.savefig(os.path.join(output_path, "loss_curve.png")) # 保存为PNG
print("Loss曲线已保存")
if __name__ == "__main__":
main()
tokenizer = AutoTokenizer.from_pretrained(model_path)
tokenizer.pad_token = tokenizer.eos_token # 使用EOS作为填充符
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.float16, # 半精度加载(节省显存)
device_map={"": device} # 指定使用的GPU设备
)
def data_collator(data):
batch = {
"input_ids": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device),
"attention_mask": torch.stack([torch.tensor(d["attention_mask"]) for d in data]).to(device),
"labels": torch.stack([torch.tensor(d["input_ids"]) for d in data]).to(device) # 标签=输入(因果LM任务)
}
return batch
trainer = Trainer(
model=model,
args=training_args,
train_dataset=dataset,
data_collator=data_collator, # 自定义数据整理
callbacks=[loss_callback] # 添加回调
)
非常感谢 Deepseek 官网满血版在本章的代码修改、资料收集以及文章润色方面提供的宝贵帮助!
本章的微调部分目前还较为基础,导致损失函数的收敛效果不够理想,仍有较大的优化空间。例如,数据集构建可以更加精细化,代码结构也有待进一步优化和调整。我们非常期待各位小伙伴的宝贵建议和指正,让我们共同进步,一起在 AI 学习的道路上探索更多乐趣!