使用合成推理数据集微调 Deepseek-R1

社区文章 发布于 2025 年 2 月 10 日

DeepSeek-R1 进入开源 AI 领域具有里程碑意义。其推理能力与 OpenAI-01 匹敌,吸引了研究人员和 AI 爱好者,登上全球头条新闻并颠覆了市场。作为一种推理模型,它模拟逐步的逻辑和演绎思维来解决复杂问题。这使得它非常适合数学、编码、逻辑、法律和医学任务,尽管其较高的推理要求使其在简单任务中效率较低。

这篇博客文章将引导您使用合成数据微调 DeepSeek。这种方法将使您能够为特定任务定制大型语言模型(LLM),即使在数据稀缺的领域也能提高准确性和可靠性。作为示例,我们将演示如何微调 Deepseek-R1 以使用 Python 解决问题。

我们将涵盖以下步骤:

让我们深入了解!

生成推理数据集

为了生成高质量的推理数据集,我们将使用 合成数据生成器,这是一个无需代码即可使用 LLM 创建自定义数据集的用户友好型应用程序。本指南中,我们将重点生成合成推理数据,以解决 Python 编码问题。然而,这种方法可以适用于广泛的领域。

有关合成数据生成器的详细信息和用法,请参阅 介绍合成数据生成器 - 使用自然语言构建数据集 和原始 GitHub 仓库

默认情况下,合成数据生成器通过无服务器推理 API 使用 meta-llama/Llama-3.1-8B-Instruct,该 API 免费但有速率限制。在最新版本中,您可以为补全配置不同的模型。由于我们需要推理数据集,我们将使用 deepseek-ai/DeepSeek-R1-Distill-Qwen-32B,它可以通过无服务器推理 API 访问。要添加此模型,您只需复制 Space 并设置 `MODEL_COMPLETION` 环境变量。

请记住,您还可以使用 Ollama 或 LlamaCpp 等其他提供商以及通过 推理提供商 本地 运行推理。

image/png

应用程序运行后,生成我们的合成数据集非常简单。它包括三个步骤:选择输入数据,配置生成器,然后生成完整数据集。

选择输入数据

由于我们正在为监督式微调(SFT)生成数据,我们将导航到“聊天数据”选项卡。在这里,有三个选项:从系统提示、现有数据集或您的文档生成。对于我们的用例,我们将重点关注第一个选项。如果您想了解其他选项如何工作,可以在此处查看。

我们需要描述数据集的主题和范围。例如,我们将其定义为:“一个解决 Python 编码问题的助手”。接下来,我们将点击“加载”,应用程序将开始生成改进的系统提示和一些示例。

配置生成器

如前一节所述,根据我们对数据集的描述,合成数据生成器将自动生成系统提示。此系统提示将作为模型生成与任务更好地对齐的数据的指南。因此,您可以进一步完善并迭代它以改进结果。我们将其定义为:

你是一位对 Python 拥有高级知识的专家程序员。你的任务是提供简洁易懂的解决方案。请回答以下 Python 问题。用户问题清晰简洁。

此外,还可以配置轮数。在单轮中,将生成两列:提示和补全。对于多轮对话,消息列将包含对话,包括系统、用户和助手角色。在我们的案例中,我们将轮数设置为 1,确保每个示例都包含一个输入编码问题(提示)和相应的推理 Python 解决方案(补全)。

生成数据集

完成上述步骤后,我们就可以生成完整的数据集了!生成的数据集将自动在 Hub 和 Argilla 中可用,随时可供审查和使用。

image/png

在我们的案例中,我们生成了 500 行数据,指令生成温度为 0.9,补全生成温度为 0.6,这与 DeepSeek 的建议一致。整个过程大约花费了 2 小时;然而,时间可能会因所使用的推理模型而异。在这种情况下,DeepSeek 需要更多的计算资源,因此处理时间更长。

太棒了!我们现在有了一个用于微调 Deepseek 的合成推理数据集!

以下部分提供了简化的代码片段,以便清晰易懂。如果您想探索完整的实现,可以访问完整笔记

微调 DeepSeek

为了微调我们的模型,我们将使用 Unsloth,一个优化微调的库。因此,即使在消费级硬件上,您也可以进行微调。

加载模型

第一步将是加载模型、分词器和适配器。具体来说,我们将微调 unsloth/DeepSeek-R1-Distill-Qwen-1.5B-unsloth-bnb-4bit,它是 deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B 的 4 位量化版本。这个模型比用于生成的模型小,因此我们预期输出质量会略低。然而,微调使我们能够使用更小的模型,同时保持准确性和可靠性。

# Load the 4bit pre quantized model of deepseek and the tokenizer
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = MODEL,
    max_seq_length = 2048,
    dtype = None,
    load_in_4bit = True,
)

# We add the LORA adapters to the model
model = FastLanguageModel.get_peft_model(
    model,
    r=16,
    target_modules=[
        "q_proj",
        "k_proj",
        "v_proj",
        "o_proj",
        "gate_proj",
        "up_proj",
        "down_proj",
    ],
    lora_alpha=16,
    lora_dropout=0,
    bias="none",
    use_gradient_checkpointing="unsloth",
    random_state=3407,
    use_rslora=False,
    loftq_config=None,
)

准备训练数据集

接下来,我们将通过定义一个提示模板并包含我们生成的数据集中的提示和补全来准备训练数据集。

# Prepare the dataset

prompt_style = """Below is an instruction that describes a task, paired with a question that provides further context.
Write a response that appropriately answers the question.
Before answering, think carefully but concisely about the question and create a step-by-step chain of thoughts to ensure a logical and accurate response.

### Instruction:
You are an expert programmer with advanced knowledge of Python. Your task is to provide concise and easy-to-understand solutions. Please answer the following python question.

### Question:
{}

### Response:
<think>
{}
"""

EOS_TOKEN = tokenizer.eos_token
def formatting_prompts_func(examples):
    prompts = examples["prompt"]
    completions = examples["completion"]
    texts = []
    for prompt,completion in zip(prompts, completions):
        text = prompt_style.format(prompt, completion) + EOS_TOKEN
        texts.append(text)
    return {
        "text": texts,
    }

dataset = load_dataset("sdiazlor/python-reasoning-dataset", split="train")
dataset = dataset.map(formatting_prompts_func, batched = True,)
dataset["text"][0]

训练模型

让我们开始训练模型!Unsloth 完全兼容 `trl` 库。因此,我们可以定义 SFTTrainer 并开始微调。请记住根据您的资源需求配置训练参数,以提高性能和准确性。您可以将微调后的模型本地保存或以 `float16` (sdiazlor/deepseek-r1-distill-qwen-1.5-unsloth-sft-python) 或 GGUF (sdiazlor/deepseek-r1-distill-qwen-1.5-unsloth-sft-python_q4_k_m) 格式推送到 Hub。

# Configure the trainer
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    dataset_num_proc=2,
    packing=False,
    args=training_arguments,
)

# Train the model
trainer_stats = trainer.train()

# Save the fine-tuned model
model.save_pretrained_merged(MODEL_NAME, tokenizer, save_method="merged_16bit")
model.push_to_hub_merged(fine_tuned_model, tokenizer, save_method="merged_16bit")
model.push_to_hub_gguf(
    f"{fine_tuned_model}_q4_k_m", tokenizer, quantization_method="q4_k_m"
)

运行推理并评估模型

是时候评估我们的微调模型是否有所改进了!为此,我们将运行推理并比较模型在微调前后的结果。

# Run inference
question = "How can I get the prime numbers from 0 to 125?"

FastLanguageModel.for_inference(model)
inputs = tokenizer([prompt_style.format(question, "")], return_tensors="pt").to("cuda")

outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=2048,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs)
response

下面的比较表明,经过微调后,模型会生成更全面的响应,包括代码示例。相比之下,预训练模型只提供高级概述,没有额外细节。

# Response before fine-tuning
<think>
To find all prime numbers between 0 and 125, I can follow these steps:

1. **Define the range**: Identify the start and end of the range, which in this case are 0 and 125.

2. **Create a boolean array**: Initialize an array of booleans with the same length as the range. Each index in this array will represent whether the corresponding number is prime (True) or not (False).

3. **Mark non-prime numbers**: Starting from the first prime number (2), iterate through each number. For each prime, mark all its multiples as non-prime in the array.

4. **Identify primes**: After marking the non-primes, the remaining True values in the array correspond to prime numbers within the specified range.

5. **Output the results**: Extract the indices from the array where the value is True and list them to obtain all prime numbers from 0 to 125.

This method ensures that we efficiently identify primes using the Sieve of Eratosthenes algorithm.
</think>

To find all prime numbers between 0 and 125, follow these steps:

[...]

5. **Output the Results**: Extract these indices and list them to obtain all prime numbers from 0 to 125.

This method efficiently finds primes using the Sieve of Eratosthenes algorithm.<|end▁of▁sentence|>

# Response after fine-tuning
Okay, so I need to find all the prime numbers between 0 and 125. Hmm, primes are numbers greater than 1 that have no divisors other than 1 and themselves. So, first, I should probably start by listing all numbers from 0 to 125 and then eliminate the non-primes.

Wait, but 0 and 1 aren't primes. So I can ignore them. The smallest prime is 2. So maybe I should start checking from 2 onwards.

I remember that one method to check for primes is the Sieve of Eratosthenes.

[...]

So, the steps in code:

1. Define a function get_primes(n) that returns primes up to n.
2. Inside the function, create a list of booleans from 0 to n, initializing to True, then setting 0 and 1 to False.
3. For each i from 2 to sqrt(n) + 1, check if it's still True. If so, mark all multiples of i as False.
4. Collect all indices that are still True and return them as a list.

[...]

</think>

To find all prime numbers from 0 to 125, we can use the Sieve of Eratosthenes algorithm, which efficiently identifies primes by marking non-prime numbers. Here's a concise Python solution:

```python
import math

def get_primes_up_to_n(n):
    if n < 2:
        return []
    sieve = [True] * (n + 1)
    sieve[0] = sieve[1] = False
    for i in range(2, int(math.sqrt(n)) + 1):
        if sieve[i]:
            for j in range(i * i, n + 1, i):
                sieve[j] = False
    primes = [i for i, is_prime in enumerate(sieve) if is_prime]
    return primes

primes = get_primes_up_to_n(125)
print(primes)
```

This code initializes a boolean list for numbers up to 125, marks non-primes, and returns the primes. The result is a list of primes from 2 to 125.
<|end▁of▁sentence|>

结论

在这篇博客文章中,我们使用合成数据生成器创建了一个定制的高质量合成推理数据集,用于使用 DeepSeek-R1-Distill-Qwen-32B 解决 Python 编码问题。然后,我们使用这个数据集微调了一个较小的模型 DeepSeek-R1-Distill-Qwen-1.5B。微调后,我们观察到模型产生了更详细和解释性的响应。因此,这个工作流程可以作为一个基础示例,用于实际应用中进行扩展。

下一步

还在等什么?立即开始合成

社区

很好的教程,谢谢。
除了成本和时间之外,有没有什么原因不使用最大的 R1 模型来生成数据集呢?

·
文章作者

@shymkovic !本教程旨在突出过程,以便您可以根据自己的配置或所需的 LLM 进行复现。使用蒸馏版本并没有特殊原因,只是因为它可以通过无服务器推理 API 获得,因此每个人都可以测试它。

嘿!感谢这篇精彩的文章。

我在下面的步骤中迷失了方向:
“在最新版本中,您可以为补全配置不同的模型。由于我们需要推理数据集,我们将使用 deepseek-ai/DeepSeek-R1-Distill-Qwen-32B,它可以通过无服务器推理 API 访问。要添加此模型,您只需复制 Space 并设置 MODEL_COMPLETION 环境变量。”

您能否更详细地解释如何继续操作?我现在已经复制了 Space,但我不明白您所说的模型补全环境变量是什么意思(请看附件)

Screenshot 2025-02-12 at 17.30.25.png

·
文章作者

@Socialmediaprophet !如图所示,您只需点击“新变量”按钮即可向 Space 添加环境变量。然后,您可以将名称设置为“MODEL_COMPLETION”,值为 `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`。这样,Llama 模型将生成提示,而 DeepSeek 模型将生成推理完成。这是文档:https://huggingface.co/docs/hub/en/spaces-overview#managing-secrets

通常,我们通过环境变量来配置应用程序。如果您对每个环境变量的含义有疑问,可以在此处查看:https://github.com/argilla-io/synthetic-data-generator/blob/main/README.md#environment-variables

嗨,感谢您的工作
当我配置 Deepseek-R1 时,我需要配置自己的 API 吗?

此外,当我复制您的空间时,我无法访问 Llama-3.1-8B-Instruct

image.png

·
文章作者

嗨! @Aristo2333 !关于 DeepSeek,我们使用的是蒸馏版本,它可以通过无服务器推理 API (https://huggingface.co/docs/api-inference/index) 访问,这意味着不需要进一步的配置。Provider 下方会显示它是否可用。

Screenshot 2025-02-19 at 14.28.14.png

Llama 的问题是您需要去原始仓库请求访问权限。在获得批准(通常很快)后,您也可以通过无服务器推理 API 使用该模型。

嗨,感谢您的教程!我有一个问题:您为什么使用自定义提示模板而不是 R1 原生提示模板,其中包含 ... 标签?

·

其中有思考标签 <>

您好,非常感谢您提供了出色的教程和解决方案!如果可能的话,您能否制作一个关于本地运行合成数据生成器的教程?正如其他人提到的,Llama 可能不太稳定,我想尝试通过 Ollama 在本地运行它。然而,我自己操作时遇到了一些麻烦。即使只是一些关于配置设置的提示也会有很大帮助!

·
文章作者

@3rica !您有没有查看示例文件夹,例如这个:https://github.com/argilla-io/synthetic-data-generator/blob/main/examples/ollama-different-model-for-completion.py。如果您仍然遇到问题并认为更详细的文档会有意义,请告诉我。

嘿,我这边有个问题,我现在计算资源比较少,但我可以拿到 4 块 A100 80GB 的显卡,每块都有 32 个 CPU……

我想微调 DeepSeek 671B,但由于计算资源限制,目前无法进行。所以我想在 DeepSeek 16B 上测试我的整个训练流程。那么这两种情况下的训练流程应该几乎相同,对吗?

注册登录 发表评论