语言模型的监督式微调

监督式微调(Supervised Fine‑Tuning, SFT)是一种把通用语言模型转换成任务型助手的方法。
通过训练提示与理想回应的成对数据,模型学会模仿示例中的回答,从而能够按照指令行事、展示期望的行为并正确调用工具。

SFT 的核心是让基础模型(只根据提示预测下一个 token)学会生成预期的回答,流程如下:

  • 基础模型:未经调整的 LLM 往往会给出泛泛或重复的回应,例如面对“你是谁?”这样的询问,它可能只是反问一句,而不是回答。
  • 带标签的数据集:收集并整理用户提示与理想助理回应的配对,例如“请告诉我你的身份——我是 Llama…”、“你最近怎么样?——我很好!”。
  • SFT 训练:对这些配对进行微调,通过最小化回应的交叉熵损失来训练模型:
    为什么通过交叉损失熵训练模型:

L S F T = − ∑ i = 1 N l o g ( p θ ( R e s p o n s e ( i ) ∣ P r o m p t ( i ) ) ) L_{SFT} = -\sum^{N}_{i=1} log(p_\theta(Response(i) | Prompt(i))) LSFT=i=1Nlog(pθ(Response(i)Prompt(i)))
这一损失鼓励模型最大化在每个提示条件下生成目标回应的概率。

  • 微调后的模型:完成训练后,模型可以针对新的查询给出合适的回复(例如向用户问好,而不是简单重复问题)
    在这里插入图片描述
    上面的公式也可以理解为最大化在提示条件下回应中所有 token 的联合概率。交叉熵损失会惩罚偏离标签回应的输出,因此 SFT 本质上是在教模型“模仿”。

SFT的最佳使用场景

  • 激发新的模型行为:

    • 将预训练模型转变为能遵循指令的助理
    • 让不具备推理能力的模型学会基本推理
    • 让模型在没有明确说明的情况下使用特定的工具
  • 提升模型能力

    • 利用强大的大模型生成高质量的合成数据,通过训练把这些能力蒸馏到小模型中。

    在这里插入图片描述
    这些例子显示了 SFT 作为预训练和更高级对齐方法之间的桥梁作用。当你需要模型快速适应新行为且有示例数据时,SFT 往往是正确的选择。

SFT数据策划原则

SFT 的效果高度依赖于数据质量。优质且多样的样本能让模型学到有用的行为;劣质样本则会让模型模仿不良习惯。常用的数据策划方法包括:

  • 蒸馏:用更强的指令模型生成回复,再训练小模型去模仿这些回复,把强模型的能力迁移到弱模型上。
  • Best‑of‑K / 拒绝采样:针对同一提示生成多个候选回复,再用奖励函数选出最好的作为训练数据。
  • 过滤:从大型 SFT 数据集中挑选出回应质量高且提示多样性好的样本,形成精简的高质量数据集。
    这里的核心是“质量比数量重要”。一千条精心挑选、题材丰富的样本往往比一百万条参差不齐的数据效果更好,因为 SFT 会迫使模型模仿它所见到的一切——包括糟糕的回答。

全参数微调 vs 参数高效微调
在执行 SFT(或其他对齐方法)时,需要决定如何更新模型权重:
在这里插入图片描述

  • 全参数微调:对每一层加入一个完整的权重更新矩阵ΔW,即修改所有参数。这可以显著提升性能,但需要大量存储和计算资源。
  • 参数高效微调:例如 LoRA(低秩适配)通过在每层引入小的低秩矩阵 A 和 B 来调整模型参数。这减少了可训练参数的数量,节省显存,缺点是学习和遗忘都更有限,因为更新的参数更少。
    这两种策略可以与任何训练方法结合。根据资源约束和性能要求,你可以选择全微调或参数高效微调。后者在硬件条件有限的情况下尤为受欢迎。

总结:
监督式微调是语言模型对齐的重要基础方法。
它通过最小化目标回复的负对数似然,使模型学会模仿期望的行为并在面对提示时做出合适回应。
SFT 特别适合用于启动新行为和从大模型向小模型“蒸馏”能力。然而,数据质量至关重要:蒸馏、拒绝采样和过滤等策略能显著胜过简单堆积大量普通数据。全参数与参数高效微调之间的选择则是性能和资源之间的权衡。

Code

import torch
import pandas as pd
from datasets import load_dataset, Dataset
from transformers import TrainingArguments, AutoTokenizer, AutoModelForCausalLM
from trl import SFTTrainer, DataCollatorForCompletionOnlyLM, SFTConfig

Setting up helper functions


#生成模型对用户输入信息的响应,通过tokenizer的chat template 格式化输入,调用模型生成回复
def generate_responses(model, tokenizer, user_message, system_message=None, 
                       max_new_tokens=100):
   #model :预训练的语言模型
   #tokenizer:与模型配套的分词器,用于处理输入和输出文本
   #user_message:用户输入的消息(字符串)
   #system_message:可选的系统消息,用于设置模型的上下文或角色(默认None)
   #max_new_tokens:生成的最大新token数量(默认100)

# 返回 模型生成的相应(字符串)——

    # Format chat using tokenizer's chat template
    messages = []
    if system_message:
        messages.append({"role": "system", "content": system_message})
    
    # We assume the data are all single-turn conversation
    messages.append({"role": "user", "content": user_message})
        
    prompt = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=False,
    )
    #格式化输入,支持系统消息和用户消息

    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
#编码成pytorch张量,并且移动到模型所在的设备
#使用推荐的生成方式(vllm,sglang或TensorRT)这里使用标准PyTorch

    # Recommended to use vllm, sglang or TensorRT
    #使用推荐生成方式vllm,sglang,TensorRT,这里使用标准PyTorch
    with torch.no_grad():
    #生成响应,设置最大新token数量,禁用采样、指定填充和结束token
        outputs = model.generate(
            **inputs,
            max_new_tokens=max_new_tokens,
            do_sample=False,
            pad_token_id=tokenizer.eos_token_id,
            eos_token_id=tokenizer.eos_token_id,
        )
        #使用model.generate生成响应,设置了do_sample=False禁用采样,使用确定性生成以及填充和结束token
        
      #获取输入token的长度,仅解码生成的部分。  
    input_len = inputs["input_ids"].shape[1]
    generated_ids = outputs[0][input_len:]
    #解码生成的token,跳过特殊token并去除多余空格
    response = tokenizer.decode(generated_ids, skip_special_tokens=True).strip()

    return response

def test_model_with_questions(model, tokenizer, questions, 
                              system_message=None, title="Model Output"):
#功能:测试模型在给定问题列表上的表现,逐一生成并打印每个问题的响应

#输入
#model:预训练或微调后的语言模型
#tokenizer:与模型配套的分词器
#questions:包含测试问题(字符串)的列表
#system_message:可选的系统消息
#title 测试输出的标题


#输出
#无返回值,直接打印每个问题的输入和模型生成的响应。

#关键点
#遍历问题列表,调用generate_response函数生成每个问题的响应
#格式化输出,清晰展示每个问题的输入输出。

    print(f"\n=== {title} ===")
    for i, question in enumerate(questions, 1):
        response = generate_responses(model, tokenizer, question, 
                                      system_message)
        print(f"\nModel Input {i}:\n{question}\nModel Output {i}:\n{response}\n")
def load_model_and_tokenizer(model_name, use_gpu = False):
    #加载预训练模型和分词器,并进行必要的配置
    		#model_name:模型的路径或huggingface模型名称
    		#use_gpu:是否将模型加载到GPU(默认False,使用CPU)
    # Load base model and tokenizer
    #关键点:
    		#使用HuggingFace的AutoTokenizer和AutoModelForCausalLM加载模型和分词器
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForCausalLM.from_pretrained(model_name)
    
    if use_gpu:
        model.to("cuda")
    #如果分词器没有chat_template,设置默认模板
    if not tokenizer.chat_template:
        tokenizer.chat_template = """{% for message in messages %}
                {% if message['role'] == 'system' %}System: {{ message['content'] }}\n
                {% elif message['role'] == 'user' %}User: {{ message['content'] }}\n
                {% elif message['role'] == 'assistant' %}Assistant: {{ message['content'] }} <|endoftext|>
                {% endif %}
                {% endfor %}"""
    
    # Tokenizer config
    #分词器配置:如果没有pad token,设置为eos token
    if not tokenizer.pad_token:
        tokenizer.pad_token = tokenizer.eos_token
        
    return model, tokenizer

def display_dataset(dataset):
    # Visualize the dataset 
    #可视化数据集的前三条记录,展示用户提示和助手响应的表格
    #dataset: Hugging Face 的 Dataset 对象,包含 messages 字段(每条记录包含系统、用户和助手消息)。
    #输出
    #无返回值,使用pandas显示一个表格,包含用户提示和助手响应

		#关键点
		#从数据集中提取前三条记录的用户消息和助手消息。	
		#使用 pandas 创建表格并设置显示选项以避免截断长字符串。
		#通过 display 函数展示表格(适用于 Jupyter 环境)。
    rows = []
    for i in range(3):
        example = dataset[i]
        user_msg = next(m['content'] for m in example['messages']
                        if m['role'] == 'user')
        assistant_msg = next(m['content'] for m in example['messages']
                             if m['role'] == 'assistant')
        rows.append({
            'User Prompt': user_msg,
            'Assistant Response': assistant_msg
        })
    
    # Display as table
    df = pd.DataFrame(rows)
    pd.set_option('display.max_colwidth', None)  # Avoid truncating long strings
    display(df)

Load base model & test on simple questions

USE_GPU = False
#设置 USE_GPU=False,默认使用 CPU。
#定义三个测试问题,覆盖不同任务(介绍、计算、概念解释)。
#使用 load_model_and_tokenizer 加载模型和分词器。
#调用 test_model_with_questions 测试并打印结果。
#测试完成后删除模型和分词器以释放内存。

# 定义测试问题列表
questions = [
    "Give me an 1-sentence introduction of LLM.",
    "Calculate 1+1-1",
    "What's the difference between thread and process?"
]
# 加载 Qwen3-0.6B 基础模型和分词器
model, tokenizer = load_model_and_tokenizer("./models/Qwen/Qwen3-0.6B-Base", USE_GPU)
# 测试基础模型在问题列表上的表现
test_model_with_questions(model, tokenizer, questions, 
                          title="Base Model (Before SFT) Output")
# 删除模型和分词器以释放内存
del model, tokenizer

SFT results on Qwen3-0.6B model

在小模型上进行SFT

In this section, we’re reviewing the results of a previously completed SFT training. Due to limited resources, we won’t be running the full training on a relatively large model like Qwen3-0.6B. However, in the next section of this notebook, you’ll walk through the full training process

model, tokenizer = load_model_and_tokenizer("./models/banghua/Qwen3-0.6B-SFT", USE_GPU)

test_model_with_questions(model, tokenizer, questions, 
                          title="Base Model (After SFT) Output")

del model, tokenizer

Doing SFT on a small model

using a smaller model and a lightweight dataset.

  • 加载小型模型和分词器。
  • 加载 banghua/DL-SFT-Dataset 数据集,若不使用 GPU,仅使用前 100 条数据以减少计算需求。
  • 调用 display_dataset 可视化数据集前三条记录。
  • 配置 SFTConfig 参数,包括学习率、训练轮数、批大小等。
  • 使用 SFTTrainer 执行微调训练。
# 加载小型模型 SmolLM2-135M
model_name = "./models/HuggingFaceTB/SmolLM2-135M"
model, tokenizer = load_model_and_tokenizer(model_name, USE_GPU)
# 加载 SFT 训练数据集
train_dataset = load_dataset("banghua/DL-SFT-Dataset")["train"]
# 如果不使用 GPU,仅使用前 100 条数据
if not USE_GPU:
    train_dataset=train_dataset.select(range(100))
# 可视化数据集前三条记录
display_dataset(train_dataset)
# SFTTrainer config 
# 配置 SFTTrainer 参数
sft_config = SFTConfig(
    learning_rate=8e-5,  # 训练学习率
    num_train_epochs=1,  # 训练轮数
    per_device_train_batch_size=1,  # 每设备批大小
    gradient_accumulation_steps=8,# 梯度累积步数
    gradient_checkpointing=False, # 是否启用梯度检查点(节省内存)
    logging_steps=2,  # Frequency of logging training progress (log every 2 steps). # 日志记录频率(每 2 步记录一次)
)

)


# 初始化 SFTTrainer 并进行训练
sft_trainer = SFTTrainer(
    model=model,
    args=sft_config,
    train_dataset=train_dataset, 
    processing_class=tokenizer,
)
sft_trainer.train()

Testing training results on small model and small dataset

# 如果不使用 GPU,将模型移动到 CPU
if not USE_GPU: # move model to CPU when GPU isn’t requested
    sft_trainer.model.to("cpu")
test_model_with_questions(sft_trainer.model, tokenizer, questions, 
                          title="Base Model (After SFT) Output")
# 测试微调后小型模型在问题列表上的表现
Logo

更多推荐