跳至主要内容

微调 GPT-OSS

逐步指南:在医疗问答数据集上微调 OpenAI GPT-OSS 20B 模型,以同时提升准确性与风格一致性。
· 6分钟 阅读

使用 AI 探索

来自 OpenAI gpt-oss 系列的 GPT-OSS 20B 是一款开放权重的语言模型,专为低延迟与专业化场景而设计。它拥有 210 亿参数(其中 36 亿为激活参数),并通过MXFP4 量化优化,可在消费级硬件上高效运行,仅需约 16GB 内存。

尽管小于 GPT-OSS 120B,它仍支持诸如可配置推理级别(低、中、高)、用于调试与透明性的完整思维链访问,以及代理能力,包括函数调用、网页浏览与代码执行等高级特性。

在本指南中,我将带您完成设置与微调 GPT-OSS 20B 的完整流程,涵盖:

  1. 安装依赖并准备环境
  2. 加载模型与分词器
  3. 使用全新的 OpenAI Harmony Python 包创建提示风格
  4. 加载数据集并应用 Harmony 格式化
  5. 微调前运行一次简单推理
  6. 配置训练所需的模型设置
  7. 在医疗问答数据集上微调模型
  8. 微调后进行推理以评估改进

请注意,本教程仅用于研究与教育,不用于患者诊断或治疗。

图片由作者提供

1. 环境搭建

虽然 GPT-OSS 模型拥有 210 亿参数,但仍需要相当可观的 GPU 显存,至少 80GB VRAM 才能从容运行。一个快速入门方式是启动 RunPod H100 实例或类似的高显存 GPU 环境。

RunPod H100SXM machine

环境就绪后,安装用于数据处理、模型微调与实验追踪的所有必要 Python 包:

%%capture
%pip install -U accelerate 
%pip install -U peft 
%pip install -U trl 
%pip install -U bitsandbytes
%pip install -U transformers
%pip install -U trackio
%pip install -U openai-harmony
%pip install -U tiktoken
%pip install -U pyctcdecode

最后,使用您的 API 密钥登录 Hugging Face Hub。这样您就可以将微调后的模型和分词器直接保存并共享到您的 Hugging Face 账户:

from huggingface_hub import notebook_login

notebook_login()

2. 加载 GPT-OSS 模型与分词器

接下来,我们以 MXFP4 量化加载 GPT-OSS 20B,但在加载时立即将权重量化还原为 bfloat16,同时使用 eager 注意力,实现禁用缓存与自动设备分配。

import torch
from transformers import AutoModelForCausalLM, Mxfp4Config

quantization_config = Mxfp4Config(dequantize=True)
model_kwargs = dict(
    attn_implementation="eager",
    torch_dtype=torch.bfloat16,
    quantization_config=quantization_config,
    use_cache=False,
    device_map="auto",
)

model = AutoModelForCausalLM.from_pretrained("openai/gpt-oss-20b", **model_kwargs)

除模型外,我们还需要同一仓库的分词器。分词器负责将文本转换为模型可处理的 token(ID),并在推理后将输出解码回人类可读文本。

from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("openai/gpt-oss-20b")

3. 使用 OpenAI Harmony 创建提示风格

OpenAI Harmony 是 GPT-OSS 模型采用的专用响应格式,用于以一致的方式组织对话、推理输出与函数调用。

它定义了不同角色(system、developer、user、assistant)之间消息的组织方式,并使模型能够跨多通道输出——例如,将思维链推理、工具调用前言与最终面向用户的回复分离。

在本节中,我们将创建一个函数,接收问答对并将其格式化为 Harmony 风格的对话提示。它使用 openai_harmony 库以结构化方式对对话进行编码。

from openai_harmony import (
    load_harmony_encoding, HarmonyEncodingName,
    Conversation, Message, Role,
    SystemContent, DeveloperContent, ReasoningEffort
)

enc = load_harmony_encoding(HarmonyEncodingName.HARMONY_GPT_OSS)

def render_pair_harmony(question, answer):
    convo = Conversation.from_messages([
        Message.from_role_and_content(
            Role.DEVELOPER,
            DeveloperContent.new().with_instructions(
                "You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. "
                "Respond with ONLY the final diagnosis/cause in ≤5 words."
            )
        ),
        Message.from_role_and_content(Role.USER, question.strip()),
        Message.from_role_and_content(Role.ASSISTANT, answer.strip()),
    ])
    tokens = enc.render_conversation(convo)
    text = enc.decode(tokens)
    return text

4. 加载与格式化数据集

我们现在将加载医疗问答数据集(FreedomIntelligence/medical-o1-verifiable-problem),并对每个样本应用 prompt_style_harmony 函数。转换后的文本存放在名为 "text" 的新列中,形成与开发者提示风格一致的简洁、指令驱动格式。

from datasets import load_dataset

def prompt_style_harmony(examples):
    qs = examples["Open-ended Verifiable Question"]
    ans = examples["Ground-True Answer"]
    outputs = {"text": []}
    for q, a in zip(qs, ans):
        rendered = render_pair_harmony(q, a)
        outputs["text"].append(rendered)
    return outputs

dataset = load_dataset(
    "FreedomIntelligence/medical-o1-verifiable-problem",
    split="train[0:1000]"
)
dataset = dataset.map(prompt_style_harmony, batched=True)
print(dataset.column_names)

print(dataset[0]["text"])

数据集现已包含新列 "text",其中保存了 Harmony 格式的对话,包含:

  • 开发者指令(例如,要求简洁给出诊断)
  • 用户消息(医疗问题)
  • 助手消息(标准答案)
['Open-ended Verifiable Question', 'Ground-True Answer', 'text']
<|start|>developer<|message|># Instructions

You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. Respond with ONLY the final diagnosis/cause in ≤5 words.<|end|><|start|>user<|message|>An 88-year-old woman with osteoarthritis is experiencing mild epigastric discomfort and has vomited material resembling coffee grounds multiple times. Considering her use of naproxen, what is the most likely cause of her gastrointestinal blood loss?<|end|><|start|>assistant<|message|>Gastric ulcer<|end|>

5. 微调前的模型推理

在训练模型之前,评估其基线表现对于后续对比非常重要。为此,我们定义辅助函数 render_inference_harmony,构造包含开发者严格指令与用户问题的 Harmony 风格提示,但留空助手回复以供模型补全。

def render_inference_harmony(question):
    convo = Conversation.from_messages([
        Message.from_role_and_content(
            Role.DEVELOPER,
            DeveloperContent.new().with_instructions(
                "You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. "
                "Respond with ONLY the final diagnosis/cause in ≤5 words."
            )
        ),
        Message.from_role_and_content(Role.USER, question.strip()),
    ])
    tokens = enc.render_conversation_for_completion(convo, Role.ASSISTANT)
    text = enc.decode(tokens)
    return text

随后,我们取数据集的首个样本,使用该函数进行格式化,分词后送入模型生成回复:

question = dataset[0]["Open-ended Verifiable Question"]

text = render_inference_harmony(question)

inputs = tokenizer(
    [text + tokenizer.eos_token], return_tensors="pt"
).to("cuda")
outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=20,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs)
print(response[0])

在这次测试中,即使开发者指令明确要求简洁给出诊断(≤5 个词),模型输出仍明显更长、更具描述性。

<|start|>developer<|message|># Instructions

You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. Respond with ONLY the final diagnosis/cause in ≤5 words.<|end|><|start|>user<|message|>An 88-year-old woman with osteoarthritis is experiencing mild epigastric discomfort and has vomited material resembling coffee grounds multiple times. Considering her use of naproxen, what is the most likely cause of her gastrointestinal blood loss?<|end|><|start|>assistant<|return|>We have an old woman with NSAID use causing ulcer bleed. The question: "An 88

模型并未直接回答“Gastric ulcer”(标准标签),而是给出了关于 NSAID 使用与溃疡出血的冗长解释。

dataset[0]["Ground-True Answer"]
'Gastric ulcer'

6. 训练前的模型设置

为高效微调模型,我们使用LoRA(低秩适配),仅训练一小部分参数并冻结基座模型,从而大幅降低显存占用与训练成本。

我们首先定义一个 LoraConfig,指定秩、缩放系数以及插入 LoRA 适配器的目标层。

from peft import LoraConfig, get_peft_model

peft_config = LoraConfig(
    r=8,
    lora_alpha=16,
    target_modules="all-linear",
    target_parameters=[
        "7.mlp.experts.gate_up_proj",
        "7.mlp.experts.down_proj",
        "15.mlp.experts.gate_up_proj",
        "15.mlp.experts.down_proj",
        "23.mlp.experts.gate_up_proj",
        "23.mlp.experts.down_proj",
    ],
)
peft_model = get_peft_model(model, peft_config)
peft_model.print_trainable_parameters()

可训练参数仅约 1500 万(约占全模型的 0.07%),适配器大小约 16 MB,与全量微调相比效率提升巨大。

trainable params: 15,040,512 || all params: 20,929,797,696 || trainable%: 0.0719

接着,我们使用 trl 库的 SFTConfig 配置训练,包括学习率、批大小、梯度累积、预热比例与调度器类型等超参数:

from trl import SFTConfig

training_args = SFTConfig(
    learning_rate=2e-4,
    gradient_checkpointing=True,
    num_train_epochs=1,
    logging_steps=10,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    max_length=2048,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine_with_min_lr",
    lr_scheduler_kwargs={"min_lr_rate": 0.1},
    output_dir="gpt-oss-20b-medical-qa",
    report_to="trackio",
    push_to_hub=True,
)

7. 训练模型

数据集已准备、LoRA 适配器挂载完毕、训练参数配置完成后,我们即可启动微调流程。使用 trl 库的 SFTTrainer,可在同一处处理模型、分词器、数据集与训练循环:

from trl import SFTTrainer

trainer = SFTTrainer(
    model=peft_model,
    args=training_args,
    train_dataset=dataset,
    processing_class=tokenizer,
)
trainer.train()

训练过程中,loss 逐步下降,表明模型正在学习与 Harmony 格式医疗问答数据的对齐。

Model training loss

训练完成后,我们将微调模型保存至本地,并推送到Hugging Face Hub。需注意,上传的仅为 LoRA 适配器权重(非完整 20B 模型),使得仓库更轻量、便于共享:

trainer.save_model(training_args.output_dir)
trainer.push_to_hub(dataset_name="kingabzpro/gpt-oss-20b-medical-qa")

这将在 Hugging Face 上创建一个新的模型仓库:kingabzpro/gpt-oss-20b-medical-qa。

来源:kingabzpro/gpt-oss-20b-medical-qa

8. 微调后的模型推理

微调完成后,我们可以测试模型是否更严格地遵循开发者指令。为此,先加载基座 GPT-OSS 20B 模型,然后挂载微调好的 LoRA 适配器。

from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel

# Load the tokenizer
tokenizer = AutoTokenizer.from_pretrained("openai/gpt-oss-20b")

# Load the original model first
model_kwargs = dict(attn_implementation="eager", torch_dtype="auto", use_cache=True, device_map="auto")
base_model = AutoModelForCausalLM.from_pretrained("openai/gpt-oss-20b", **model_kwargs).cuda()

# Merge fine-tuned weights with the base model
peft_model_id = "gpt-oss-20b-medical-qa"
model = PeftModel.from_pretrained(base_model, peft_model_id)
model = model.merge_and_unload()

接着,我们对数据集中的一个样本进行推理。将问题格式化为 Harmony 风格、分词、送入模型并解码为文本:

question = dataset[0]["Open-ended Verifiable Question"]

text = render_inference_harmony(question)

inputs = tokenizer(
    [text + tokenizer.eos_token], return_tensors="pt"
).to("cuda")
outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=20,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs)
print(response[0])

微调后的模型现在给出了简短且准确的回答,更接近标准标签(“Gastric ulcer”),并遵循了 ≤5 个词的约束。

<|start|>developer<|message|># Instructions

You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. Respond with ONLY the final diagnosis/cause in ≤5 words.<|end|><|start|>user<|message|>An 88-year-old woman with osteoarthritis is experiencing mild epigastric discomfort and has vomited material resembling coffee grounds multiple times. Considering her use of naproxen, what is the most likely cause of her gastrointestinal blood loss?<|end|><|start|>assistant<|return|><|message|>Stomach ulcer<|end|><|return|>

我们也可以测试另一个样本以验证一致性:

question = dataset[10]["Open-ended Verifiable Question"]

text = render_inference_harmony(question)

inputs = tokenizer(
    [text + tokenizer.eos_token], return_tensors="pt"
).to("cuda")
outputs = model.generate(
    input_ids=inputs.input_ids,
    attention_mask=inputs.attention_mask,
    max_new_tokens=20,
    eos_token_id=tokenizer.eos_token_id,
    use_cache=True,
)
response = tokenizer.batch_decode(outputs)
print(response[0])

很好。微调后的模型能够生成简洁、准确且遵循指令的回复。

<|start|>developer<|message|># Instructions

You are a medical expert with advanced knowledge in clinical reasoning and diagnostics. Respond with ONLY the final diagnosis/cause in ≤5 words.<|end|><|start|>user<|message|>What substance is primarily filtered in the renal tubules with minimal secretion or re-absorption?<|end|><|start|>assistant<|return|><|message|>creatinine<|end|><|return|>

结语

对开源 AI 社区而言,这是令人振奋的时期。尽管新的 GPT-OSS 模型尚未在各项基准上名列前茅,但它们展现出强劲潜力,尤其在各自的参数规模段内,小模型同样能够带来亮眼表现。

在本教程中,我们使用 LoRA 适配器与 Harmony 提示风格,对 GPT-OSS 20B 进行了医疗问答方向的微调。结果令人鼓舞:微调模型能够输出与临床推理任务一致的简洁、准确答案。通过增加训练轮次、扩充数据集与优化系统提示,性能还有进一步提升空间。

如果您想通过实战课程熟悉微调流程,不妨查看Fine-Tuning With Llama 3。

Abid Ali Awan

作者

Abid Ali Awan

作为一名持证的数据科学家,我热衷于利用前沿技术打造创新的机器学习应用。凭借在语音识别、数据分析与报告、MLOps、对话式人工智能以及自然语言处理方面的扎实背景,我不断打磨构建智能系统的能力,力求带来切实影响。除技术专长外,我也擅长沟通,能够将复杂概念提炼为清晰、简明的表述。因此,我成为数据科学领域备受关注的博主,与不断壮大的数据专业人士社区分享洞见与实践经验。目前,我专注于内容创作与编辑,借助大语言模型打造有力且吸引人的内容,帮助企业与个人更好地发挥数据价值。

主题