2026年7月2日 周四晚上19:30,报名腾讯会议了解“如何构建自进化的动态知识库(Brain)”(限30人)
免费POC, 零成本试错
FDE知识库

FDE知识库

学习大模型的前沿技术与行业落地应用


收藏

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

发布日期:2025-03-01 06:36:34 浏览次数: 2553
作者:Datawhale

微信搜一搜,关注“Datawhale”

推荐语

零基础也能掌握的大模型微调技巧,让AI模型更懂你!

核心内容:
1. 大模型微调的直观感受与效果展示
2. 选择DeepSeek-R1-Distill-Qwen-7B模型进行微调
3. 微调教程复现与代码详解

杨芳贤
53AI创始人/腾讯云(TVP)最具价值专家


前言:大模型评测是一个系统工程,本文希望通过比较通俗的方式给大家直观感受大模型微调后的效果,相关是思路想法旨在起到抛砖引玉的效果,如果学习者对大模型评测有深厚的兴趣,可以从不同的角度进行学习。

三天前,看到了我们 Datawhale 公众号上发了文章《零基础入门:DeepSeek 微调教程来了!》反响很好,其中的内容写的非常接地气,适合学习者进行学习体验。

于是,我尝试在那篇文章的基础上进行了复现,并对内容进行了一些延伸,帮助读者更加直观的感受大模型微调对模型的调整。

为了方便学习与体验,本文中选择的模型是蒸馏后 DeepSeek-R1-Distill-Qwen-7B 模型,显卡选择是 RTX4090 24G。

Deepseek 模型以及数据集均来源于魔塔社区 medical-o1-reasoning-SFT。

1. 微调教程复现

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

os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 指定使用GPU 

# 配置路径(根据实际路径修改)
model_path = "xxxx"  # 模型路径
data_path = "xxxx"  # 数据集路径
output_path = "xxxx"  # 微调后模型保存路径


# 设置设备参数
DEVICE = "cuda"  # 使用CUDA
DEVICE_ID = "0"  # CUDA设备ID,如果未设置则为空
device = f"{DEVICE}:{DEVICE_ID}" if DEVICE_ID else 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,  # 防止删除未使用的列
    device="cuda:0" # 指定使用的GPU设备    
)

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
    )
    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=(106))
    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()
微调的相关讲解可以直接参考上一篇公众号的内容,我们看看 LOSS 曲线。

可以看到经过简单的微调,模型的 LOSS 值是有降低,说明 Deepseek 模型是对训练集的数据集有拟合的。

2.直观比较模型生成

模型微调完,生成的内容效果如何,怎么进行比较呢?

这个时候我们首先想到的是直接比较「微调模型」和「原始模型」对同一个问题生成的回答内容进行比较。

因此我们可以统一提示词,统一相关的问题,然后比较生成的答案。

具体代码如下:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import os
import json
from bert_score import score
from tqdm import tqdm
# 设置可见GPU设备(根据实际GPU情况调整)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 指定仅使用GPU 

# 路径配置 ------------------------------------------------------------------------
base_model_path = "xxxxx"  # 原始预训练模型路径
peft_model_path = "xxxxx"  # LoRA微调后保存的适配器路径

# 模型加载 ------------------------------------------------------------------------
# 初始化分词器(使用与训练时相同的tokenizer)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)

# 加载基础模型(半精度加载节省显存)
base_model = AutoModelForCausalLM.from_pretrained(
    base_model_path,
    torch_dtype=torch.float16,  # 使用float16精度
    device_map="auto"           # 自动分配设备(CPU/GPU)
)

# 加载LoRA适配器(在基础模型上加载微调参数)
lora_model = PeftModel.from_pretrained(
    base_model, 
    peft_model_path,
    torch_dtype=torch.float16,
    device_map="auto"
)
# 合并LoRA权重到基础模型(提升推理速度,但会失去再次训练的能力)
lora_model = lora_model.merge_and_unload()  
lora_model.eval()  # 设置为评估模式

# 生成函数 ------------------------------------------------------------------------
def generate_response(model, prompt):
    """统一的生成函数
    参数:
        model : 要使用的模型实例
        prompt : 符合格式要求的输入文本
    返回:
        清洗后的回答文本
    """

    # 输入编码(保持与训练时相同的处理方式)
    inputs = tokenizer(
        prompt,
        return_tensors="pt",          # 返回PyTorch张量
        max_length=1024,               # 最大输入长度(与训练时一致)
        truncation=True,              # 启用截断
        padding="max_length"          # 填充到最大长度(保证batch一致性)
    ).to(model.device)               # 确保输入与模型在同一设备

    # 文本生成(关闭梯度计算以节省内存)
    with torch.no_grad():
        outputs = model.generate(
            input_ids=inputs.input_ids,
            attention_mask=inputs.attention_mask,
            max_new_tokens=1024,       # 生成内容的最大token数(控制回答长度)
            temperature=0.7,         # 温度参数(0.0-1.0,值越大随机性越强)
            top_p=0.9,               # 核采样参数(保留累积概率前90%的token)
            repetition_penalty=1.1,  # 重复惩罚系数(>1.0时抑制重复内容)
            eos_token_id=tokenizer.eos_token_id,  # 结束符ID
            pad_token_id=tokenizer.pad_token_id,  # 填充符ID 
        )
    
    # 解码与清洗输出
    full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)  # 跳过特殊token
    answer = full_text.split("### 答案:\n")[-1].strip()  # 提取答案部分
    return answer

# 对比测试函数 --------------------------------------------------------------------
def compare_models(question):
    """模型对比函数
    参数:
        question : 自然语言形式的医疗问题
    """

    # 构建符合训练格式的prompt(注意与训练时格式完全一致)
    prompt = f"诊断问题:{question}\n详细分析:\n### 答案:\n"
    
    # 双模型生成
    base_answer = generate_response(base_model, prompt)  # 原始模型
    lora_answer = generate_response(lora_model, prompt)  # 微调模型
    
    # 终端彩色打印对比结果
    print("\n" + "="*50)  # 分隔线
    print(f"问题:{question}")
    print("-"*50)
    print(f"\033[1;34m[原始模型]\033[0m\n{base_answer}")  # 蓝色显示原始模型结果
    print("-"*50)
    print(f"\033[1;32m[LoRA模型]\033[0m\n{lora_answer}")  # 绿色显示微调模型结果
    print("="*50 + "\n")

# 主程序 ------------------------------------------------------------------------
if __name__ == "__main__":
        # 测试问题集(可自由扩展)
    test_questions = [
        "根据描述,一个1岁的孩子在夏季头皮出现多处小结节,长期不愈合,且现在疮大如梅,溃破流脓,口不收敛,头皮下有空洞,患处皮肤增厚。这种病症在中医中诊断为什么病?"
    ]
    
    # 遍历测试问题
    for q in test_questions:
        compare_models(q)

来看看模型对同一个问题输出结果的差异,这里为了凸显图像微调后与原始模型的差异,选择了训练集中的一条数据进行测试,读者可以根据自己的情况随机测试。
我们来看看生成的内容。

根据生成的内容,看起来 LoRA 微调后的模型好像还是和原始模型有些不同的,但是这个回答要比较的话就很抽象,毕竟作为学习者我们对医疗领域的问题可能了解的也不太多,能否通过一些比较直观的方法来体现微调后模型与原始模型的差异呢?

这个时候我们想到了能否通过文本的相似性来评估,可以使用 bertscore 对模型进行比较,那 bertscore 是什么呢?我们来看看 Deepseek 满血版给我的答复,输出的内容太多了,这里就不全部粘贴过来,主体来说就是衡量语意的相似性,那我们似乎可以通过 berscore 来比较训练集的答案和模型生成的答案,来比较直观的看看微调后的模型与原始模型的差异。

这里为了方便学习者进行学习,以下代码中选择的 bert 模型是最基础的 bert-base-chinese 模型,同样可以在魔塔社区进行下载。

需要说明的是,考虑到部分学习者可能无法访问 hugging face 的官网,这里的 bert-base-chinese 模型采用离线的模型进行加载。

温馨提示,模型的评估非常消耗资源,这里建议学习者只调用 10 条数据集即可。

ok,我们来看看代码:

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
import os
import json
from bert_score import score
from tqdm import tqdm
# 设置可见GPU设备(根据实际GPU情况调整)
os.environ["CUDA_VISIBLE_DEVICES"] = "0"  # 指定仅使用GPU 

# 路径配置 ------------------------------------------------------------------------
base_model_path = "xxxxxx/DeepSeek-R1-Distill-Qwen-7B"  # 原始预训练模型路径
peft_model_path = "xxxxxx/output"  # LoRA微调后保存的适配器路径

# 模型加载 ------------------------------------------------------------------------
# 初始化分词器(使用与训练时相同的tokenizer)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)

# 加载基础模型(半精度加载节省显存)
base_model = AutoModelForCausalLM.from_pretrained(
    base_model_path,
    torch_dtype=torch.float16,  # 使用float16精度
    device_map="auto"           # 自动分配设备(CPU/GPU)
)

# 加载LoRA适配器(在基础模型上加载微调参数)
lora_model = PeftModel.from_pretrained(
    base_model, 
    peft_model_path,
    torch_dtype=torch.float16,
    device_map="auto"
)
# 合并LoRA权重到基础模型(提升推理速度,但会失去再次训练的能力)
lora_model = lora_model.merge_and_unload()  
lora_model.eval()  # 设置为评估模式

# 生成函数 ------------------------------------------------------------------------
def generate_response(model, prompt):
    """统一的生成函数
    参数:
        model : 要使用的模型实例
        prompt : 符合格式要求的输入文本
    返回:
        清洗后的回答文本
    """

    # 输入编码(保持与训练时相同的处理方式)
    inputs = tokenizer(
        prompt,
        return_tensors="pt",          # 返回PyTorch张量
        max_length=1024,               # 最大输入长度(与训练时一致)
        truncation=True,              # 启用截断
        padding="max_length"          # 填充到最大长度(保证batch一致性)
    ).to(model.device)               # 确保输入与模型在同一设备

    # 文本生成(关闭梯度计算以节省内存)
    with torch.no_grad():
        outputs = model.generate(
            input_ids=inputs.input_ids,
            attention_mask=inputs.attention_mask,
            max_new_tokens=1024,       # 生成内容的最大token数(控制回答长度)
            temperature=0.7,         # 温度参数(0.0-1.0,值越大随机性越强)
            top_p=0.9,               # 核采样参数(保留累积概率前90%的token)
            repetition_penalty=1.1,  # 重复惩罚系数(>1.0时抑制重复内容)
            eos_token_id=tokenizer.eos_token_id,  # 结束符ID
            pad_token_id=tokenizer.pad_token_id,  # 填充符ID 
        )
    
    # 解码与清洗输出
    full_text = tokenizer.decode(outputs[0], skip_special_tokens=True)  # 跳过特殊token
    answer = full_text.split("### 答案:\n")[-1].strip()  # 提取答案部分
    return answer

# 对比测试函数 --------------------------------------------------------------------
def compare_models(question):
    """模型对比函数
    参数:
        question : 自然语言形式的医疗问题(如"小孩感冒怎么办?")
    """

    # 构建符合训练格式的prompt(注意与训练时格式完全一致)
    prompt = f"诊断问题:{question}\n详细分析:\n### 答案:\n"
    
    # 双模型生成
    base_answer = generate_response(base_model, prompt)  # 原始模型
    lora_answer = generate_response(lora_model, prompt)  # 微调模型
    
    # 终端彩色打印对比结果
    print("\n" + "="*50)  # 分隔线
    print(f"问题:{question}")
    print("-"*50)
    print(f"\033[1;34m[原始模型]\033[0m\n{base_answer}")  # 蓝色显示原始模型结果
    print("-"*50)
    print(f"\033[1;32m[LoRA模型]\033[0m\n{lora_answer}")  # 绿色显示微调模型结果
    print("="*50 + "\n")

# 主程序 ------------------------------------------------------------------------
if __name__ == "__main__":
    # 测试问题集(可自由扩展)
    # test_questions = [
    #     "根据描述,一个1岁的孩子在夏季头皮出现多处小结节,长期不愈合,且现在疮大如梅,溃破流脓,口不收敛,头皮下有空洞,患处皮肤增厚。这种病症在中医中诊断为什么病?"
    # ]
    
    # # 遍历测试问题
    # for q in test_questions:
    #     compare_models(q)
    # 加载测试数据
    ####-----------批量测试---------------#
    with open("xxxxxx/data/medical_o1_sft_Chinese.json"as f:
        test_data = json.load(f) 

    # 数据量比较大,我们只选择10条数据进行测试
    test_data=test_data[:10]
    # 批量生成回答
    def batch_generate(model, questions):
        answers = []
        for q in tqdm(questions):
            prompt = f"诊断问题:{q}\n详细分析:\n### 答案:\n"
            ans = generate_response(model, prompt)
            answers.append(ans)
        return answers

    # 生成结果
    base_answers = batch_generate(base_model, [d["Question"for d in test_data])
    lora_answers = batch_generate(lora_model, [d["Question"for d in test_data])
    ref_answers = [d["Response"for d in test_data]

    bert_model_path="xxxxx/model/bert-base-chinese"
    # 计算BERTScore
    _, _, base_bert = score(base_answers, ref_answers, lang="zh",model_type=bert_model_path,num_layers=12,device="cuda")
    _, _, lora_bert = score(lora_answers, ref_answers, lang="zh",model_type=bert_model_path,num_layers=12,device="cuda")
    print(f"BERTScore | 原始模型: {base_bert.mean().item():.3f} | LoRA模型: {lora_bert.mean().item():.3f}")

我们来看看结果:

结果

可以看到利用 bertscore 比较数据集的参考答案与模型生成答案的相似性来看,LoRA微调后的结果和原始模型相比还是有细微的差异,随着 LoRA 微调的训练轮次加深,甚至我们故意让大模型产生“过拟合”后,比较这个相似性,这个结果的差异应该会进一步加大,可以从一个相对定性的角度给学习者提供一个新的视角。

3. 后记

大模型的评测是一个相对来说比较复杂且体系的内容,特别是金融与医疗领域涉及到比较强专业性,实际的企业部署过程中会有更加多样化的方法来评估模型生成的好坏。

本文尽可能的从初学者的角度去切入,让学习者能比较简单且直接的了解模型微调后与原始模型的差异。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询

扫码登录
登录即表示您同意《53AI网站服务协议》
服务协议

欢迎您使用【53AI 官方网站】(以下简称“本网站”或“我们”)。本《会员服务协议》(以下简称“本协议”)是您(以下简称“会员”或“用户”)与【深圳市博思协创网络科技有限公司】之间关于注册、登录及使用本网站会员服务所订立的法律协议。

在您注册或登录前,请务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的条款、知识产权条款、争议解决条款等。此类条款将以加粗形式提示您注意。 当您通过微信公众号授权、手机验证码验证或其他方式成功登录本网站时,即视为您已完全理解并同意接受本协议的全部内容。

一、 定义

本网站:指由【深圳市博思协创网络科技有限公司】运营的,域名为【53ai.com】的网站及相关移动端页面。

会员服务:指本网站向注册会员提供的知识库文章查阅、内容检索及其他相关增值服务。

知识库内容:指本网站发布的包括但不限于文字、图表、数据、研究报告、行业分析等数字化内容资源。

二、 账号注册与登录

登录方式:本网站支持以下登录方式,您可根据实际情况选择:

微信公众号授权登录:您同意将您的微信OpenID信息授权给本网站,用于创建或关联会员账号。

手机验证码登录:您需提供真实有效的手机号码,并通过短信验证码完成身份验证与登录/注册。

账号安全:您的账号仅限您本人使用,禁止赠与、借用、租用、转让或售卖。因您保管不善导致的账号被盗、密码泄露等损失,由您自行承担。

实名认证:根据相关法律法规要求,我们可能要求您在特定功能下完成实名认证。如您拒绝提供,可能无法使用部分或全部服务。

未成年人保护:若您未满18周岁,请在法定监护人的陪同下阅读本协议,并在征得监护人同意后使用本服务。

三、 服务内容与规范

知识库查阅权限:会员登录后,有权按照其会员等级对应的权限范围,在线浏览、检索本网站知识库中的相关文章及内容。

服务变更:我们有权根据业务发展需要,调整、变更或终止部分服务内容,并将以网站公告、公众号消息等方式提前通知。

禁止行为:您在使用服务时不得实施以下行为:

利用技术手段批量爬取、下载、转存知识库内容;

将知识库内容用于商业目的或未经授权地向第三方传播;

干扰本网站正常运行或侵犯其他用户合法权益;

发布违法违规信息或从事违反公序良俗的活动。

四、 知识产权声明

权利归属:本网站知识库中的排版设计、软件代码等内容的知识产权均归【公司全称】或原权利人所有,受《中华人民共和国著作权法》等法律保护。

有限许可:本网站授予会员一项非独占、不可转让、不可转授权的普通许可,仅限于个人学习、研究之目的在线查阅知识库内容。

侵权追责:未经书面许可,任何单位或个人不得以任何形式复制、转载、摘编、镜像、汇编或以其他方式使用上述内容。一经发现,我们保留追究其法律责任的权利。

五、 个人信息保护

我们重视对您个人信息的保护。关于我们如何收集、使用、存储和保护您的个人信息,请单独阅读 《隐私政策》。

您通过微信公众号授权或手机号验证所提供的信息,我们将严格按照《个人信息保护法》的规定处理,仅用于身份识别、服务提供及安全验证等必要用途。

您可以随时通过网站设置或联系客服行使查阅、更正、删除个人信息及撤回授权同意的权利。

六、 免责声明

内容准确性:知识库内容仅供参考,不构成专业建议。我们不对其完整性、准确性、时效性作任何明示或暗示的保证,您应自行判断并承担使用风险。

不可抗力:因自然灾害、政策法规变化、网络故障、第三方平台接口异常(如微信接口维护、运营商短信通道故障)等不可抗力导致的服务中断或延迟,我们不承担违约责任。

第三方链接:本网站可能包含指向第三方网站的链接,该等网站的内容和服务不受我们控制,请您自行甄别风险。

七、 违约责任

如您违反本协议约定,我们有权视情节采取警告、限制功能、暂停服务、注销账号等措施,并保留要求赔偿损失的权利。

如因您的违约行为导致我们遭受行政处罚、第三方索赔或商誉损失,您应承担全部赔偿责任(包括但不限于罚款、赔偿金、律师费、公证费等)。

八、 法律适用与争议解决

本协议的订立、执行和解释均适用中华人民共和国大陆地区法律。

因本协议产生的或与本协议有关的任何争议,双方应友好协商解决;协商不成的,任何一方均可向【公司所在地】有管辖权的人民法院提起诉讼。

九、 其他

本协议构成双方就本服务达成的完整协议,取代此前任何口头或书面约定。

本协议任一条款被认定为无效或不可执行的,不影响其他条款的效力。

我们对本协议享有最终解释权,并在法律允许的范围内保留随时修改的权利。修改后的协议一经公布即生效,继续使用服务即视为同意修订内容。


已查阅