定制化训练DeepSeek模型:LoAR、COT推理与SFT技术应用

DeepSeek-R1 模型微调系列

DeepSeek-R1 模型微调系列一. 前言介绍本文内容:1.1 项目背景1.2 LoRA和 QLoRA 简介1.3 LLaMA 架构和 Qwen 架构LLaMA 架构Qwen 架构二. 环境准备2.1 Unsloth 安装(显卡版本-暂时不用)2.2 创建Python项目2.3 python 依赖库2.2 LoRA peft 安装2.3 WandB 设置2.4 modelscope pull 模型2.5 测试模型使用三. 训练数据数据3.1 准备数据集3.2 数据清洗3.3 训练数据3.3 训练模型并保存3.4 合并模型文件3.4 评估和监控训练过程评估(eval/)相关信息:训练(train/)相关信息:

一. 前言介绍

本文内容:

  1. 模型加载与预处理 :详细讲解如何加载预训练模型、分词器,并处理输入数据集。

  2. LoRA配置 :介绍如何使用LoRA技术配置模型,并高效进行微调,节省计算资源。

  3. 训练过程 :展示了如何配置训练参数,使用SFTTrainer进行训练,并通过WandB记录训练日志。

  4. 模型保存与评估 :如何保存微调后的模型,以及如何通过合适的评估集对模型进行验证。

  5. 模型合并 :展示了如何通过加权平均的方式合并多个模型权重,得到一个更强大的模型。

1.1 项目背景

本文档描述了如何在MAC笔记本上对 DeepSeek-R1-Distill-Llama-1.5B Qwen架构 进行高效微调,使用** transformers 进行数据处理,并结合 LoRA 技术进行模型微调,使用 WandB 监控训练过程, ModelScope 下载模型。(训练数据量大约2w条左右)

  • 由于为MAC笔记本本地训练 无显卡支持 故而放弃(DeepSeek-R1-Distill-Qwen-7B Q wen)

下载的服务信息如下:

安装服务 版本名称 作用
Unsloth 用于数据处理和模型微调。
Transformers Hugging Face 提供的模型库,用于加载和微调 DeepSeek-R1。
WandB 用于训练过程的实时监控和可视化。
LoRA 用于微调的低秩适应技术。
ModelScope 用于下载 DeepSeek-R1-8b 模型。
python3.11 Python 3.11 用于执行 Python 脚本和训练任务。

1.2 LoRA和 QLoRA 简介

以下是 LoRA 和 QLoRA 的区别表格:

特性 LoRA (Low-Rank Adaptation) QLoRA (Quantized LoRA)
核心原理 通过低秩矩阵分解减少需要调整的参数量 在 LoRA 的基础上结合量化技术,进一步减少存储和计算需求
主要优点 降低训练时需要调整的参数数量,提高微调效率 除了低秩矩阵,还通过量化减少内存占用,适用于资源有限的环境
存储需求 较低,但不如 QLoRA 节省内存 显著减少内存使用,适合在内存受限的设备上使用
计算效率 提高训练效率,减少计算资源消耗 量化后的低精度计算进一步提高了计算效率,降低了开销
适用场景 计算资源有限但不需要极限压缩的场景 内存和计算资源极其有限的环境,特别是在边缘设备上使用
适用硬件 适用于大多数硬件设备,尤其是高性能计算环境 特别适合内存有限的硬件,如边缘设备、低内存服务器等

1.3 LLaMA 架构和 Qwen 架构

特性 LLaMA 架构 Qwen 架构
开发者 Meta(Facebook) 深度求索(DeepSeek)
设计目标 高效、轻量化 中文优化、多语言支持
参数量 7B、13B、33B、65B 等 7B、14B 等
开源情况 开源 部分开源或未完全公开
适用场景 资源有限的环境 中文任务、多语言任务

LLaMA 架构

  • 全称 :Large Language Model Meta AI(LLaMA)

  • 开发者 :由 Meta(原 Facebook)开发。

  • 特点

    • 高效性 :LLaMA 旨在以较少的参数量实现高性能,专注于优化计算效率。

    • 轻量化 :模型参数量相对较小(如 7B、13B、33B、65B),但通过高质量数据和训练方法,性能接近甚至超越更大的模型。

    • 开源 :Meta 发布了 LLaMA 的权重和代码,供研究社区使用。

  • 应用场景

    • 适合资源有限的环境,如本地部署或移动设备。

    • 适用于各种 NLP 任务,尤其是在生成、问答、文本分类等任务中,具有较好的性能和效率。


Qwen 架构

  • 开发者 :由中国的深度求索(DeepSeek)团队开发。

  • 特点

    • 定制化设计 :Qwen 可能是针对中文或特定任务优化的架构,具体细节未完全公开。

    • 多语言支持 :Qwen 系列模型通常对中文有较好的支持,同时在英文和多语言任务上也有不错的表现。

    • 参数量灵活 :Qwen 系列包括不同规模的模型(如 7B、14B 等),适合不同场景。

  • 应用场景

    • Qwen 适用于文本生成、自动化内容创作、对话系统、语音合成等任务。

二. 环境准备

2.1 Unsloth 安装(显卡版本-暂时不用)

  • Unsloth 是一个用于数据处理和模型微调的工具。您可以通过以下命令安装:

  • MAC不试用,需要显卡

复制代码
​
##官网:https://github.com/unslothai/unsloth
​
#01 创建项目,并设置python虚拟环境,python3.11版本
​
#02 安装 unsloth(cpu版本)
brew install llvm(Homebrew clang version 19.1.7)
echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
​
pip install torch
pip install numpy
pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git"
​
​
​
#03 版本检查
python -c "import torch; print(torch.__version__)"
2.6.0
​
#04 引用
from unsloth import FastLanguageModel
​
​
​
​
​
​

安装完成后,您可以使用 Unsloth 进行数据的预处理、加载和微调模型。

  • 暂时不使用
复制代码
​
#01 linux 服务建议使用docker
​
​
#02 拉取镜像
docker pull modelscope-registry.us-west-1.cr.aliyuncs.com/modelscope-repo/modelscope:ubuntu22.04-py310-torch2.3.1-1.22.2
​
#03 启动
​
​

2.2 创建Python项目

复制代码
​
#01 环境是python3.11
​
#02 项目目录
Unsloth-DeepSeek-R1-8b/
├── data/                    # 存放训练数据、验证数据等
│   ├── raw/                 # 原始数据
│   └── processed/           # 预处理后的数据
│
├── models/                  # 存放模型文件
│   ├── checkpoints/         # 存储训练过程中的模型检查点
│   └── final_model/         # 存储最终微调后的模型
│
├── scripts/                 # 存放训练脚本、数据处理脚本等
│   ├── train.py             # 训练脚本
│   ├── data_preprocessing.py# 数据预处理脚本
│   └── evaluate.py          # 评估脚本
│
├── logs/                    # 存放训练日志文件
│   └── training_logs.txt    # 训练过程中的日志
│
├── wandb/                   # 存放 wandb 相关的配置和记录
│   └── wandb_config.py      # wandb 配置文件
│
├── environment/             # 环境配置文件
│   ├── requirements.txt     # 项目的 Python 依赖
│   └── environment.yml      # 如果使用 Conda,可以创建一个环境配置文件
│
├── main.py                  # 主运行文件,启动训练或其他任务
└── README.md                # 项目的描述文件,包含如何使用和运行的说明
​
​
#03 创建目录
# 创建子目录
mkdir -p data/raw
mkdir -p data/processed
mkdir -p models/checkpoints
mkdir -p models/final_model
mkdir -p scripts
mkdir -p logs
mkdir -p wandb
mkdir -p environment
​
# 创建文件
touch scripts/train.py
touch scripts/data_preprocessing.py
touch scripts/evaluate.py
touch logs/training_logs.txt
touch wandb/wandb_config.py
touch environment/requirements.txt
touch environment/environment.yml
touch main.py
touch README.md
​

2.3 python 依赖库

复制代码
​
#03 安装即可
pip install torch==2.6.0 transformers datasets
​
#03 更新证书(后续如果有pip网站使用https 会验证该证书)
/Applications/Python\ 3.11/Install\ Certificates.command
​
​

2.2 LoRA peft 安装

LoRA 和 PEFT 的安装

  • LoRAPEFT 是用于高效微调的技术。如果你想在 Mac 上使用这些技术来微调 DeepSeek 模型,你需要安装相关的依赖项。

  • PEFT 包含了 LoRA 的实现,并且它使得你能够通过修改模型的一部分参数来进行高效微调,从而不需要调整整个模型的权重。

复制代码
​
#01 安装 peft
pip install peft
​

2.3 WandB 设置

WandB 是一个用于训练过程实时监控和可视化的工具。您可以通过以下步骤设置 WandB

  1. 注册并登录 WandB官网

  2. 获取您的 API 密钥并配置环境变量:

复制代码
​
#01 aipkey (本人谷歌邮箱)

​
#02 命令
pip install wandb
wandb login
​
#02 运行文件
import wandb  # 导入 wandb 库,用于跟踪和可视化实验
import random  # 导入 random 库,用于生成随机数
​
# 开始一个新的 wandb 运行来跟踪当前脚本
wandb.init(
    # 设置 wandb 项目,所有与该运行相关的数据将被记录到这个项目中
    project="my-awesome-project",  # 项目名称,你可以在 wandb 仪表盘中看到这个项目
    
    # 追踪超参数和运行的元数据
    config={
        "learning_rate": 0.02,  # 设置学习率
        "architecture": "CNN",  # 模型架构(这里是卷积神经网络)
        "dataset": "CIFAR-100",  # 使用的数据集(这里是 CIFAR-100 数据集)
        "epochs": 10,  # 训练的轮数
   }
)
​
# 模拟训练过程
epochs = 10  # 总训练轮数
offset = random.random() / 5  # 生成一个小的随机偏移量,用于模拟训练过程中一些不确定性
​
# 开始训练循环,模拟 2 到 10 轮的训练过程
for epoch in range(2, epochs):  # 从第二轮开始,到第 10 轮结束
    # 模拟准确率的变化,随着 epoch 的增加,准确率逐渐提升
    acc = 1 - 2 ** -epoch - random.random() / epoch - offset
    
    # 模拟损失的变化,随着 epoch 的增加,损失逐渐减少
    loss = 2 ** -epoch + random.random() / epoch + offset
​
    # 使用 wandb 记录每一轮的准确率(acc)和损失值(loss)
    wandb.log({"acc": acc, "loss": loss})
​
# [可选] 结束 wandb 运行,确保数据被正确上传并完成记录
wandb.finish()
​
​
​
​

2.4 modelscope pull 模型

复制代码
​
#01 安装modelscope 
pip install modelscope
​
#02 下载模型文件
mkdir -p ./models/DeepSeek-R1-Distill-Llama-8B
mkdir -p ./models/DeepSeek-R1-Distill-Qwen-1.5B
mkdir -p ./models/DeepSeek-R1-Distill-Qwen-7B
​
modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Llama-8B --local_dir ./models/DeepSeek-R1-Distill-Llama-8B
​
modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B --local_dir ./models/DeepSeek-R1-Distill-Qwen-1.5B
​
modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Qwen-7B --local_dir ./models/DeepSeek-R1-Distill-Qwen-7B
​
​
​
modelscope download --model deepseek-ai/DeepSeek-R1-Distill-Llama-8B --local_dir ./DeepSeek-R1-Distill-Llama-8B

2.5 测试模型使用

复制代码
​
"""
​
​
训练前询问问题:
 皮质醇增多症患者在血浆ACTH明显升高且大剂量地塞米松抑制试验阳性的情况下,应考虑哪种疾病?
 
训练后再次询问:
​
​
scripts/test_inference.py
​
"""
​
​
import os
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
​
# 获取当前脚本的路径
current_dir = os.path.dirname(__file__)
​
# 拼接模型和分词器路径
model_dir = os.path.join(current_dir, '..', 'models', 'DeepSeek-R1-Distill-Qwen-1.5B')
​
# 打印路径确认
print(f"Model path: {model_dir}")
​
# 确保模型和分词器的路径存在
if not os.path.exists(model_dir):
    raise ValueError(f"Model directory does not exist at {model_dir}")
else:
    print("Model directory exists, proceeding with loading.")
​
# 加载模型和分词器
print("Loading model and tokenizer...")
model = AutoModelForCausalLM.from_pretrained(model_dir)
tokenizer = AutoTokenizer.from_pretrained(model_dir)
​
# 打印模型和分词器的配置信息
print(f"Model config: {model.config}")
print(f"Tokenizer config: {tokenizer}")
​
# 输入中文文本
input_text = "皮质醇增多症患者在血浆ACTH明显升高且大剂量地塞米松抑制试验阳性的情况下,应考虑哪种疾病?"
print(f"User input: {input_text}")
​
# 结构化的 prompt
prompt_style_chat = f"""请写出一个恰当的回答来完成当前对话任务。
​
### Instruction:
你是一名助人为乐的助手。
​
### Question:
{input_text}
​
### Response:
<think>"""
​
# 使用分词器处理输入文本
inputs = tokenizer(prompt_style_chat, return_tensors="pt", padding=True, truncation=True, max_length=512)
​
# 打印 tokenized 输入
print(f"Tokenized input: {inputs}")
​
# 打印输入形状
print(f"Input shape: {inputs['input_ids'].shape}")
​
# 打印模型的最大长度
print(f"Model max length: {model.config.max_position_embeddings}")
​
# 将模型移至正确的设备(使用 GPU 如果可用)
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
​
# 打印设备信息
print(f"Using device: {device}")
​
# 打印分词器的 pad_token_id
pad_token_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else model.config.pad_token_id
print(f"Using pad_token_id: {pad_token_id}")
​
# 生成模型输出
print("Generating response...")
# 使用 max_new_tokens 来控制生成长度
with torch.no_grad():  # 禁用梯度计算,节省内存
    try:
        print("Calling model.generate()...")
        outputs = model.generate(
            inputs['input_ids'].to(device),
            attention_mask=inputs['attention_mask'].to(device),
            max_new_tokens=1200,  # 设置最大生成的 token 数量
            temperature=1.0,
            top_p=0.9,
            pad_token_id=pad_token_id
       )
​
        print("Model.generate() completed.")
    except Exception as e:
        print(f"Error generating response: {e}")
        raise
​
# 打印生成的输出 ID 和它们的形状
print(f"Generated output IDs: {outputs}")
print(f"Shape of generated output: {outputs.shape}")
​
# 解码生成的输出文本
try:
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(f"Generated response: {response}")
except Exception as e:
    print(f"Error decoding output: {e}")
​
​
​
​
  • 问题回答
复制代码
User input: 皮质醇增多症患者在血浆ACTH明显升高且大剂量地塞米松抑制试验阳性的情况下,应考虑哪种疾病?
Tokenized input: {'input_ids': tensor([[151646,  14880, 112672,  46944, 112449, 111423,  36407,  60548,  67949,
         105051,  88802,   3407,  14374,  29051,    510,  56568, 110124,  99262,
         103247,  99350,   9370, 110498,   3407,  14374,  15846,    510,  99888,
          99178, 103032, 107284,  99769, 101924,  18493,  99389, 101498,   6823,
             39, 100687, 109061, 100136,  26288, 114786,  29490, 101202,  72261,
         100180, 106555, 102360, 112758, 104248,   3837,  50511, 101118, 113195,
         101160,  26850,  14374,   5949,    510, 151648]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
Input shape: torch.Size([1, 60])
Model max length: 131072
Using device: cpu
Using pad_token_id: 151643
Generating response...
Calling model.generate()...
Model.generate() completed.
​
Generated response: 请写出一个恰当的回答来完成当前对话任务。
​
### Instruction:
你是一名助人为乐的助手。
​
### Question:
皮质醇增多症患者在血浆ACTH明显升高且大剂量地塞米松抑制试验阳性的情况下,应考虑哪种疾病?
​
### Response:
<think>
好的,我现在需要仔细分析这个问题并给出一个合适的回答。首先,问题描述的是皮质醇增多症(PHT)患者在血浆ACTH明显升高且大剂量地塞米松抑制试验(SSDS)显示阳性的情况下,应考虑哪种疾病。
​
首先,我记得皮质醇增多症是由于皮质醇分泌异常导致,通常由代谢紊乱或神经退行性疾病引起,比如皮质醇过激释放症、皮质醇过激释放性代谢综合征等。通常,患者可能表现出皮质醇水平升高,血浆ACTH显著升高,这符合题意的第一个条件。
​
接下来,第二个条件是SSDS试验阳性。SSDS试验主要用于检测皮质醇释放的细胞因子,比如PD-L1,这些因子在疾病早期有显著的表观变化。皮质醇增多症患者的皮质醇释放确实受阻,导致细胞因子释放减少,这在SSDS中会被检测出来,所以这种情况属于皮质醇增多症。
​
综合这两个条件,患者的血浆ACTH升高和SSDS阳性,符合皮质醇增多症的特征。因此,这种情况下应考虑的是皮质醇增多症。
​
我需要确保我没有遗漏其他可能导致SSDS试验阳性的情况。比如,是否有一些其他类型的疾病,比如胰岛素素合成障碍或胰岛素缺乏,也会影响皮质醇释放?不过,这些更可能是胰岛素素合成障碍,而不是直接由皮质醇释放受阻引起的。皮质醇增多症通常是由于皮质醇释放异常,因此SSDS阳性更直接与皮质醇释放受阻相关。
​
此外,ACTH升高可能与皮质醇增多症不同,而更可能是由于激素分泌过量或其他激素调节问题。因此,ACTH升高的信号应该更多指向皮质醇增多症。
​
综上所述,这种情况下应该考虑的疾病是皮质醇增多症。
</think>
​
应考虑皮质醇增多症(Pantoprazolidone Phenomenon)。
​
因为:
​
1. 血浆ACTH显著升高,符合皮质醇增多症的特征。
2. SSDS试验阳性,表明皮质醇释放受阻,属于皮质醇增多症的表现。
​
​
​

三. 训练数据数据

3.1 准备数据集

复制代码
​
#01 我们使用COT格式 医学领域 medical-o1-reasoning-SFT 数据集
https://huggingface.co/datasets/FreedomIntelligence/medical-o1-reasoning-SFT
​
#02 b本地导入方式()
from datasets import load_dataset
ds = load_dataset("FreedomIntelligence/medical-o1-reasoning-SFT", "zh")
​
​
  • Hugging face 数据集

  • modelscope

复制代码
​
#01 使用modelscope 数据集 官网地址
https://www.modelscope.cn/datasets/YIRONGCHEN/PsyDTCorpus/files
​
#02 下载完整数据集repo
modelscope download --dataset YIRONGCHEN/PsyDTCorpus --local_dir ./dir
​
​
#03 下载单个文件到指定本地文件夹(以下载README.md到当前路径下"dir"目录为例)
modelscope download --dataset YIRONGCHEN/PsyDTCorpus README.md --local_dir ./dir
​
​
  • 下载图示

  • 官网

3.2 数据清洗

复制代码
​
#01 用于对medical-o1-reasoning-SFT数据集进行修改,Complex_CoT列和Response列进行拼接,并加上文本结束标记:
def formatting_prompts_func(examples, EOS_TOKEN):
    """
   格式化数据集中的每个示例,使其符合训练的要求。
​
   Args:
       examples (dict): 数据集中的输入示例
       EOS_TOKEN (str): 结束符
​
   Returns:
       dict: 格式化后的文本数据
   """
    train_prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. 
   Write a response that appropriately completes the request. 
   Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.
​
   ### Instruction:
   You are a medical expert with advanced knowledge in clinical reasoning, diagnostics, and treatment planning. 
   Please answer the following medical question. 
​
   ### Question:
   {}
​
   ### Response:
   <think>
   {}
   </think>
   {}"""
​
    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}
​
​
  
"""
​
问题({}) 被嵌套到 ### Question: 下面,替换掉 {}。
推理过程({}) 被嵌套到 <think></think> 标签内,替换掉第二个 {}。
答案({}) 被嵌套到模板的最后,替换掉第三个 {}。
具体替换流程:
{} 第一个位置将会被每个样本中的问题(examples["Question"])替换。
{} 第二个位置将会被每个样本中的推理过程(examples["Complex_CoT"])替换。
{} 第三个位置将会被每个样本中的答案(examples["Response"])替换。
例如,如果输入数据如下:
​
问题(Question): "What is the cause of fever?"
推理过程(Complex_CoT): "Fever is usually caused by an infection or inflammation. We need to identify the source."
答案(Response): "The most common causes of fever are bacterial or viral infections."
​
"""  
  
  
  • 原数据格式
复制代码
{
    "Question": [
        "What is the cause of headache?",
        "How do you treat a cold?"
   ],
    "Complex_CoT": [
        "The causes of headaches are numerous, including tension, dehydration, or sinus issues.",
        "Treating a cold typically involves rest, fluids, and over-the-counter medications for symptoms."
   ],
    "Response": [
        "A headache can be caused by stress, lack of sleep, or a sinus infection.",
        "For a cold, hydration and rest are key. Medications like ibuprofen can help with symptoms."
   ]
}
​
  • 格式化后数据
复制代码
{
    "text": [
        """Below is an instruction that describes a task, paired with an input that provides further context. 
       Write a response that appropriately completes the request. 
       Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.
​
       ### Instruction:
       You are a medical expert with advanced knowledge in clinical reasoning, diagnostics, and treatment planning. 
       Please answer the following medical question. 
​
       ### Question:
       What is the cause of headache?
​
       ### Response:
       <think>
       The causes of headaches are numerous, including tension, dehydration, or sinus issues.
       </think>
       A headache can be caused by stress, lack of sleep, or a sinus infection. <|endoftext|>
       """,
        """Below is an instruction that describes a task, paired with an input that provides further context. 
       Write a response that appropriately completes the request. 
       Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.
​
       ### Instruction:
       You are a medical expert with advanced knowledge in clinical reasoning, diagnostics, and treatment planning. 
       Please answer the following medical question. 
​
       ### Question:
       How do you treat a cold?
​
       ### Response:
       <think>
       Treating a cold typically involves rest, fluids, and over-the-counter medications for symptoms.
       </think>
       For a cold, hydration and rest are key. Medications like ibuprofen can help with symptoms. <|endoftext|>
       """
   ]
}
​

3.3 训练数据

  1. setup_wandb : 配置并登录到 wandb 进行实验跟踪和日志记录。

  2. set_paths : 设置根目录、模型路径、数据集路径和保存微调模型的路径。

  3. load_model_and_tokenizer : 加载预训练模型和分词器,获取结束符。

  4. formatting_prompts_func : 格式化数据集中的问题和回答,以便训练。

  5. setup_lora : 配置并应用LoRA(低秩适配器)到模型。

  6. load_dataset_func : 加载数据集并进行切分,返回训练集和评估集。

  7. setup_training_args : 设置训练参数,包括学习率、批处理大小、训练周期等。

  8. train_model : 使用 SFTTrainer 进行模型训练。

  9. save_model : 保存训练好的模型到指定路径。

复制代码
import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from datasets import load_dataset
from peft import get_peft_model, LoraConfig
from trl import SFTTrainer  # 使用 SFTTrainer
import wandb
from config import setting
​
# 设置环境变量,禁用tokenizer的并行化
os.environ["TOKENIZERS_PARALLELISM"] = "false"
​
​
# 登录wandb
def setup_wandb():
    """
   登录到wandb以便记录训练过程中的日志和指标。
   """
    wandb.login()
​
​
# 设置路径
def set_paths():
    """
   设置项目根目录、模型路径、数据集路径和最终模型保存路径。
​
   Returns:
       model_dir (str): 模型文件路径
       dataset_path (str): 数据集路径
       final_model_dir (str): 微调后模型的保存路径
   """
    root_dir = setting.root_dir  # 项目根路径
    model_dir = os.path.join(root_dir, 'models', 'DeepSeek-R1-Distill-Qwen-1.5B')  # 模型文件路径
    dataset_path = os.path.join(root_dir, 'data', 'medical-o1-reasoning-SFT')  # 数据集路径
    final_model_dir = os.path.join(root_dir, 'models', 'final_model')  # 高效微调后模型保存路径
    print(f'设置模型路径:{model_dir} | 数据集位置:{dataset_path}')
    return model_dir, dataset_path, final_model_dir
​
​
# 加载模型和分词器
def load_model_and_tokenizer(model_dir):
    """
   加载预训练模型和对应的分词器,并获取结束符(EOS_TOKEN)。
​
   Args:
       model_dir (str): 模型的文件路径
​
   Returns:
       model (AutoModelForCausalLM): 加载的模型
       tokenizer (AutoTokenizer): 加载的分词器
       EOS_TOKEN (str): 模型的结束符(如果没有,使用默认值)
   """
    print("加载分词器:Loading model and tokenizer...")
    model = AutoModelForCausalLM.from_pretrained(model_dir)
    tokenizer = AutoTokenizer.from_pretrained(model_dir)
​
    EOS_TOKEN = tokenizer.eos_token
    if EOS_TOKEN is None:
        EOS_TOKEN = "<|endoftext|>"
​
    print(f'结束符:{EOS_TOKEN}')
    return model, tokenizer, EOS_TOKEN
​
​
# 格式化训练数据
def formatting_prompts_func(examples, EOS_TOKEN):
    """
   格式化数据集中的每个示例,使其符合训练的要求。
​
   Args:
       examples (dict): 数据集中的输入示例
       EOS_TOKEN (str): 结束符
​
   Returns:
       dict: 格式化后的文本数据
   """
    train_prompt_style = """Below is an instruction that describes a task, paired with an input that provides further context. 
   Write a response that appropriately completes the request. 
   Before answering, think carefully about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.
​
   ### Instruction:
   You are a medical expert with advanced knowledge in clinical reasoning, diagnostics, and treatment planning. 
   Please answer the following medical question. 
​
   ### Question:
   {}
​
   ### Response:
   <think>
   {}
   </think>
   {}"""
​
    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}
​
​
# 设置LoRA配置
def setup_lora(model):
    """
   设置LoRA(低秩适配器)配置,并将其应用到模型。
​
   Args:
       model (AutoModelForCausalLM): 加载的模型
​
   Returns:
       model (AutoModelForCausalLM): 应用LoRA后的模型
   """
    print("设置LoRA: Setting up LoRA configuration...")
    lora_config = LoraConfig(
        r=8,  # adapter的秩
        lora_alpha=32,  # 缩放因子
        lora_dropout=0.1,  # LoRA层的dropout
        bias="none",  # LoRA的偏置项
   )
    return get_peft_model(model, lora_config)
​
​
# 加载数据集
def load_dataset_func(dataset_path, train_size=100):
    """
   从指定路径加载数据集,训练集大小为 train_size,评估集为训练集的10%,但至少为1。
   """
    print(f"从 {dataset_path} 加载数据集...")
    # 加载数据集
    dataset = load_dataset(dataset_path, "en", split="train", trust_remote_code=True)
​
    # 计算评估集大小
    eval_size = max(1, int(train_size * 0.1))  # 评估集大小是训练集的10%,但至少为1
​
    # 切分数据集
    train_dataset = dataset.select(range(train_size))  # 使用前 train_size 条作为训练集
    eval_dataset = dataset.select(range(train_size, train_size + eval_size))  # 剩余部分作为评估集
​
    print(f"训练集大小: {len(train_dataset)}, 评估集大小: {len(eval_dataset)}")
    return train_dataset, eval_dataset
​
​
# 配置训练参数
def setup_training_args(final_model_dir, enable_evaluation=True):
    """
   设置训练参数,包括输出目录、学习率、批处理大小等,并根据参数控制是否启用评估。
​
   Args:
       final_model_dir (str): 微调后模型保存的路径
       enable_evaluation (bool): 是否启用评估。默认为True,启用评估;为False时禁用评估。
​
   Returns:
       training_args (TrainingArguments): 训练参数
   """
    # 根据是否启用评估设置 evaluation_strategy
    evaluation_strategy = "epoch" if enable_evaluation else "no"
​
    training_args = TrainingArguments(
        output_dir=final_model_dir,
        evaluation_strategy=evaluation_strategy,  # 控制评估策略
        learning_rate=5e-5,
        per_device_train_batch_size=2,  # 适当减少批处理大小(根据M3 Pro的内存限制)
        gradient_accumulation_steps=4,  # 使用梯度累积,模拟更大的批量
        num_train_epochs=3,  # 训练3个周期
        report_to="wandb",  # 使用wandb进行训练日志记录
        weight_decay=0.01,
        logging_dir=os.path.join(setting.root_dir, 'logs'),
        logging_steps=50,  # 减少日志记录频率
        save_steps=500,  # 增加模型保存的步数频率,减少频繁保存
        save_total_limit=2,  # 保存最多2个模型
        dataloader_num_workers=4,  # 设置数据加载器的并行数(根据需要调整)
   )
    return training_args
​
​
​
# 训练模型
def train_model(model, training_args, dataset, eval_dataset, tokenizer, enable_evaluation=True):
    """
   使用SFTTrainer进行模型训练。
​
   Args:
       model (AutoModelForCausalLM): 需要训练的模型
       training_args (TrainingArguments): 训练参数
       dataset (Dataset): 用于训练的数据集
       eval_dataset (Dataset): 用于评估的数据集
       tokenizer (AutoTokenizer): 分词器
       enable_evaluation (bool): 是否进行评估
​
   Returns:
       trainer (SFTTrainer): 训练器实例
   """
    # 如果启用了评估,传递评估集
    trainer = SFTTrainer(
        model=model,
        args=training_args,
        train_dataset=dataset,
        eval_dataset=eval_dataset if enable_evaluation else None,  # 根据参数决定是否传递评估集
        tokenizer=tokenizer,
        data_collator=None,  # 可以选择合适的data collator
   )
    trainer.train()
    return trainer
​
​
# 保存模型
def save_model(trainer, final_model_dir):
    """
   保存训练后的模型到指定目录。
​
   Args:
       trainer (SFTTrainer): 训练器实例
       final_model_dir (str): 模型保存路径
   """
    print("Saving model...")
    trainer.save_model(final_model_dir)
​
​
​
def merge_models(models, weights, device="cpu"):
    """
   合并多个模型的权重(加权平均)。
​
   Args:
       models (list): 模型列表
       weights (list): 权重列表,权重数量与模型数量一致
       device (str): 设备,可以是 "cuda" 或 "cpu"
​
   Returns:
       merged_model (nn.Module): 合并后的模型
   """
    # 确保模型数量与权重数量一致
    assert len(models) == len(weights), "模型数量与权重数量不一致"
​
    # 将所有模型加载到相同的设备
    for i in range(len(models)):
        models[i] = models[i].to(device)
​
    # 获取第一个模型的状态字典
    merged_state_dict = models[0].state_dict()
​
    # 对每一层的权重进行加权平均
    for key in merged_state_dict.keys():
        merged_state_dict[key] = torch.zeros_like(merged_state_dict[key])
        for model, weight in zip(models, weights):
            merged_state_dict[key] += model.state_dict()[key] * weight
​
    # 创建一个新的模型并加载合并后的权重
    merged_model = models[0].__class__.from_pretrained(models[0].config)
    merged_model.load_state_dict(merged_state_dict)
    return merged_model
​
​
# 主函数
def main():
    """
   主函数,执行整个训练流程:设置路径、加载模型、训练并保存模型。
​
   参数设置:
           enable_evaluation = False # 设置为False以禁用评估 如果性能慢可以设置 False
​
   加载数据集:
       train_size=10 设置数据集大小,评估集是数据集百分之10(如果小于1 则等于1 )
       train_dataset, eval_dataset = load_dataset_func(dataset_path, train_size=10)
​
​
   """
    setup_wandb()  # 登录wandb
    model_dir, dataset_path, final_model_dir = set_paths()  # 设置路径
​
    model, tokenizer, EOS_TOKEN = load_model_and_tokenizer(model_dir)  # 加载模型和分词器
​
    train_dataset, eval_dataset = load_dataset_func(dataset_path, train_size=5)  # 加载数据集
    train_dataset = train_dataset.map(lambda examples: formatting_prompts_func(examples, EOS_TOKEN), batched=True)  # 格式化数据集
    eval_dataset = eval_dataset.map(lambda examples: formatting_prompts_func(examples, EOS_TOKEN), batched=True)  # 格式化评估集
    print(train_dataset["text"][0])  # 打印格式化后的数据
​
    model = setup_lora(model)  # 配置LoRA
    # 设置是否开启评估
    enable_evaluation = True  # 设置为False以禁用评估
    training_args = setup_training_args(final_model_dir,enable_evaluation)  # 配置训练参数
    trainer = train_model(model, training_args, train_dataset, eval_dataset, tokenizer, enable_evaluation)  # 开始训练
​
    save_model(trainer, final_model_dir)  # 保存模型
    wandb.finish()  # 完成wandb记录
​
​
​
​
# 执行主函数
if __name__ == "__main__":
    main()
​
  • 训练

  • 训练过程

3.3 训练模型并保存

复制代码
​
"""
保存在本地 models/final_model 路径下
​
"""
​
def save_model(trainer, final_model_dir):
    """
   保存训练后的模型到指定目录。
​
   Args:
       trainer (SFTTrainer): 训练器实例
       final_model_dir (str): 模型保存路径
   """
    print("Saving model...")
    trainer.save_model(final_model_dir)
​
    
​
​

3.4 合并模型文件

复制代码
​
#01 执行即可
new_model_local = "DeepSeek-R1-Medical-COT-Tiny"
model.save_pretrained(new_model_local) 
tokenizer.save_pretrained(new_model_local)
model.save_pretrained_merged(new_model_local, tokenizer, save_method = "merged_16bit",)
​

3.4 评估和监控训练过程

评估(eval/)相关信息:

  • eval/runtime 18.3908 : 评估过程总共耗时18.39秒。

  • eval/samples_per_second 0.054 : 每秒处理的样本数为0.054,表示评估的速度较慢。

  • eval/steps_per_second 0.054 : 每秒进行评估步数为0.054,说明每个评估步骤的时间消耗较大。

训练(train/)相关信息:

  • train/epoch 0 : 当前训练轮次是第0轮。

  • train/global_step 0 : 当前全局步骤为0,表示尚未进行任何训练步骤。

  • train_loss 14435.36663 : 当前训练的损失为14435.37,表明模型的表现尚不理想,通常需要更多的训练来降低损失。

  • train/runtime 251.7582 : 训练总时间为251.76秒。

  • train/samples_per_second 0.06 : 每秒处理的训练样本数为0.06,训练的速度较慢。

  • train/steps_per_second 0.012 : 每秒进行的训练步数为0.012,表示每个训练步骤消耗的时间较长。

复制代码
​
​
#02 详细日志
wandb: ⭐️ View project at https://wandb.ai/z15119911990-beijing/huggingface
wandb: 🚀 View run at https://wandb.ai/z15119911990-beijing/huggingface/runs/mgrko2jv
  0%|          | 0/3 [00:00<?, ?it/s]
{'eval_runtime': 14.8693, 'eval_samples_per_second': 0.067, 'eval_steps_per_second': 0.067, 'epoch': 0}
                                     
  0%|          | 0/3 [00:30<?, ?it/s]
100%|██████████| 1/1 [00:00<00:00, 1461.94it/s]
                                               
                                     
{'eval_runtime': 21.2073, 'eval_samples_per_second': 0.047, 'eval_steps_per_second': 0.047, 'epoch': 0}
  0%|          | 0/3 [02:11<?, ?it/s]
100%|██████████| 1/1 [00:00<00:00, 33.69it/s]
                                             
                                     
  0%|          | 0/3 [04:02<?, ?it/s]
100%|██████████| 1/1 [00:00<00:00, 334.66it/s]
                                             {'eval_runtime': 18.3908, 'eval_samples_per_second': 0.054, 'eval_steps_per_second': 0.054, 'epoch': 0}
{'train_runtime': 251.7582, 'train_samples_per_second': 0.06, 'train_steps_per_second': 0.012, 'train_loss': 14435.3666305542, 'epoch': 0}
  0%|          | 0/3 [04:10<?, ?it/s]
wandb:                                                                                
wandb: 
wandb: Run history:
wandb:            eval/runtime ▁█▅
wandb: eval/samples_per_second █▁▃
wandb:   eval/steps_per_second █▁▃
wandb:             train/epoch ▁▁▁▁
wandb:       train/global_step ▁▁▁▁
wandb: 
wandb: Run summary:
wandb:             eval/runtime 18.3908
wandb:  eval/samples_per_second 0.054
wandb:    eval/steps_per_second 0.054
wandb:               total_flos 43804457687040.0
wandb:              train/epoch 0
wandb:        train/global_step 0
wandb:               train_loss 14435.36663
wandb:            train_runtime 251.7582
wandb: train_samples_per_second 0.06
wandb:   train_steps_per_second 0.012
wandb: 
wandb: 🚀 View run /Users/ningcaichen/Documents/02-python相关文档/01-AI系列/LoRA-DeepSeek-R1/models/final_model at: https://wandb.ai/z15119911990-beijing/huggingface/runs/mgrko2jv
wandb: ⭐️ View project at: https://wandb.ai/z15119911990-beijing/huggingface
wandb: Synced 5 W&B file(s), 0 media file(s), 0 artifact file(s) and 0 other file(s)
wandb: Find logs at: ./wandb/run-20250212_133457-mgrko2jv/logs
​
​
​
  • 图示