Skip to content

零基础入门:DeepSeek微调教程来了!

· 32 min

开始之前,记得免费领取火山引擎 DeekSeek R1 满血版,速度极快。 点此立即领取

开门见山,直接给大家展示微调前后的效果。

微调前:

img.png

微调后:

img_1.png

在此处可以看到很明显大模型进行微调后口吻已经发生了更改。据笔者使用下来的记录表示,微调后的大模型思考时间更加短暂。

接下来,让我们一起逐步完成微调实践,共同优化模型性能!

一、什么是大模型微调?#

微调就像给一个“学霸”补课,让它从“通才”变成某个领域的“专家”。

此处以本文进行微调的医学数据进行举例: 假设你有一个很聪明的朋友,他读过全世界的书(相当于大模型的预训练阶段),能和你聊历史、科学、文学等各种话题。 但如果你需要他帮你看医学报告段),能和你聊历史、科学、文学等各种话题。 但如果你需要他帮你看医学报告,虽然他懂一些基础知识,但可能不够专业。这时候,你给他一堆医学书籍和病例,让他专门学习这方面的知识(这就是微调),他就会变得更擅长医疗领域的问题。

📖 故事解释:#

想象你有一个会画小猫的机器人🤖(这就是预训练模型)。现在你想让它学会画戴帽子的小猫🎩🐱。不需要从头教它画画,只需要给它看很多”戴帽子小猫”的图片,然后说:“保持原来的画画能力,但要学会加帽子哦!” 这就是微调!

📖 生活案例解释:#

案例1:智能音箱调方言#

案例2:相机滤镜原理#

加强版解释:乐高城堡改造成儿童医院#

第一步:原有结构 —— 通用乐高城堡#

[通用城堡]

▸ 比喻:就像网购的”标准款城堡积木套装”,有城墙、塔楼、尖顶,能当普通房子用。

▸ 对应技术:预训练模型(比如 ChatGPT),已经学会通用语言能力,但不够专业。

第二步:局部改造 —— 低成本改装#

① 拆尖顶 → 改圆顶

[尖顶改圆顶]

▸ 操作:把塔顶的尖积木换成圆积木,更温和可爱。

▸ 技术含义:微调模型顶层参数(比如修改分类头),让输出风格更适合儿童对话。

② 加装旋转门[旋转门]

▸ 操作:在门口插入一个可旋转的积木模块,不破坏原有门结构。

▸ 技术含义:插入适配器模块(Adapter),让模型新增儿科医学术语理解能力,且不干扰原有知识。

③ 涂装医院标志

[医院标志]

▸ 操作:在城堡外墙贴上”十字符号”和卡通动物贴纸。

▸ 技术含义:特征空间偏移(Feature Shift),调整模型内部表示,让它更关注医疗相关词汇和童趣表达。

第三步:新功能 —— 变身儿童医院#

[儿童医院]

▸ 成果:改装后的城堡能接待小患者,有玩具区、温和的医生(圆顶),还有专用医疗设备(旋转门)。

▸ 技术含义:通过轻量改造,通用模型变成”儿科医疗问答机器人”,专精儿童健康咨询。

二、当前尝试过的硬件配置#

显卡:NVIDIA GeForce RTX 4060

CPU:Intel Core i7-13700H

内存:16 G(因为家庭电脑所以日常状态是 8.8/15.7 GB)

三、微调工作#

(1) 数据集准备#

本文数据集来源,魔搭社区的 medical-o1-reasoning-SFT。

本文主要说明,数据集格式是: img_2.png

在 DeepSeek 的蒸馏模型微调过程中,数据集中引入 Complex_CoT(复杂思维链)是关键设计差异。若仅使用基础问答对进行训练,模型将难以充分习得深度推理能力,导致最终性能显著低于预期水平。这一特性与常规大模型微调的数据要求存在本质区别。

img_3.png

(2) 模型微调代码(此处是无框架纯手搓)——直接上了,后面会有细节讲解#

需要引入的库:
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['train'].map(format_example, remove_columns=dataset['train'].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()

(3) 代码详细讲解#

功能总结:导入项目依赖的第三方库,包括 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 # 系统路径操作

有关类库介绍:#

  1. torch (PyTorch 库的核心模块)
  1. matplotlib.pyplot (Matplotlib 绘图库)
  1. transformers (HuggingFace Transformers 库)
  1. peft (Parameter-Efficient Fine-Tuning)
  1. datasets (HuggingFace Datasets 库)
  1. os (操作系统接口)

2. 配置路径和硬件检查#

# 配置路径(根据实际路径修改)
model_path = r"你的模型路径" # 预训练模型存放路径
data_path = r"你的数据集路径" # 训练数据路径(JSON格式)
output_path = r"你的保存微调后的模型路径" # 微调后模型保存位置
# 强制使用GPU(确保CUDA可用)
assert torch.cuda.is_available(), "必须使用GPU进行训练!"
device = torch.device("cuda") # 指定使用CUDA设备
  1. 自定义训练回调类
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"])
  1. 数据预处理函数
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['train'].map(format_example, remove_columns=dataset['train'].column_names)

关键代码#

1.拼接指令和答案

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张量
)
  1. 返回处理后的输入
return {"input_ids": inputs["input_ids"].squeeze(0),
"attention_mask": inputs["attention_mask"].squeeze(0)}
  1. 应用格式化函数
return dataset.map(format_example, remove_columns=dataset.column_names)
  1. LoRA微调配置

    功能总结:配置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" # 任务类型(因果语言模型)
)
  1. r=16:LoRA 的秩

→ 页数少(r小):学得快但可能漏细节

→ 页数多(r大):学得细但速度慢

  1. lora_alpha=32:缩放系数
  1. target_modules=[“q_proj”, “v_proj”]:目标模块

    • 作用:指定需要插入低秩矩阵的模型模块。
    • 解释:
      • q_proj 和 v_proj 是 Transformer 模型中的注意力机制模块:
      • q_proj:查询(Query)投影矩阵。
      • v_proj:值(Value)投影矩阵。
      • LoRA 会在这两个模块中插入低秩矩阵。
    • 影响:
      • 选择不同的模块会影响微调的效果。
      • 通常选择 q_proj 和 v_proj 是因为它们对模型的表现影响较大
  2. lora_dropout=0.05:Dropout 率

    • 作用:防止过拟合。
    • 解释:
      • Dropout 是一种正则化技术,随机丢弃一部分神经元,防止模型过度依赖某些特征。
      • lora_dropout=0.05 表示在训练过程中,有 5% 的低秩矩阵参数会被随机丢弃。
    • 影响:
      • 较大的 Dropout 率会增加模型的鲁棒性,但可能会降低训练效率。
      • 较小的 Dropout 率会减少正则化效果,但可能会提高训练速度。
  3. bias=“none”:偏置参数

  1. task_type=“CAUSAL_LM”:任务类型

训练参数配置#

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 # 保留未使用的列(避免数据错误)
)
  1. output_dir=output_path:输出目录
    • 作用:指定训练过程中模型和日志的保存路径。此处的 output_path 之前已经写在了最前面的变量之中。
    • 解释:
      • 训练过程中生成的模型检查点、日志文件等都会保存到这个目录。
    • 示例:
      • 如果 output_path = ”./output”,所有文件都会保存到 ./output 目录下。

训练参数配置#

  1. per_device_train_batch_size=2:单 GPU 批次大小
  1. gradient_accumulation_steps=4:梯度累积步数
  1. num_train_epochs=3:训练轮次
  1. learning_rate=3e-4:初始学习率
  1. fp16=True:混合精度训练
  1. logging_steps=20:日志记录频率
  1. save_strategy=“no”:保存策略
  1. report_to=“none”:禁用第三方报告
  1. optim=“adamw_torch”:优化器类型
  1. no_cuda=False:强制使用 CUDA
  1. dataloader_pin_memory=False:禁用锁页内存
  1. 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()

关键代码:#

  1. 加载 Tokenizer 并设置填充符
  1. 加载预训练模型
  1. 数据整理函数
  1. 初始化 Trainer

四、完结感言#

非常感谢 Deepseek 官网满血版在本章的代码修改、资料收集以及文章润色方面提供的宝贵帮助!

本章的微调部分目前还较为基础,导致损失函数的收敛效果不够理想,仍有较大的优化空间。例如,数据集构建可以更加精细化,代码结构也有待进一步优化和调整。我们非常期待各位小伙伴的宝贵建议和指正,让我们共同进步,一起在 AI 学习的道路上探索更多乐趣!

文章来源:原创 吴锦凤 Datawhale https://mp.weixin.qq.com/s/hOgeu6EPbuaQgVHyjij-kg