满树 Finetune:基于 Unsloth 的中文医疗问答大模型微调#

#LLM; #Finetune; #Unsloth; #LoRA; #Qwen

基于 Qwen2.5-7B-Instruct 与 Unsloth,使用 LoRA 在中文医疗问答数据集上微调,实现医疗领域问答能力增强。


项目仓库:(暂无,本地项目 manshu_finetune)


下文流程图概括了「数据准备 → 微调训练 → 推理验证」的主链路。

graph TD
    A[Chinese-medical-dialogue-data] --> B[数据清洗与格式转换]
    B --> C[Alpaca 格式 JSON]
    C --> D[Dataset.from_json]
    D --> E[Tokenize & Format]
    E --> F[Unsloth FastLanguageModel]
    F --> G[LoRA Adapter 注入]
    G --> H[SFTTrainer 微调]
    H --> I[LoRA Adapter 保存]
    I --> J[合并为完整模型]
    J --> K[推理验证]
    K --> L[GGUF 导出]

项目概述#

满树 Finetune(版本 v0.1.0)是个人代表项目之一,核心强调两点:基于 Unsloth 高效微调框架对 Qwen2.5-7B-Instruct 进行 LoRA 微调,以及使用真实医疗问答数据集(Chinese-medical-dialogue-data,约 79 万条问答对)实现医疗领域专业问答能力的增强。项目承担「数据清洗 → 格式转换 → 高效微调 → 模型合并 → 推理验证 → GGUF 导出」全链路,便于展示对 LLM 微调工程化的深度理解。

技术栈:Unsloth 作为高效微调框架(2-5 倍训练加速、50%-80% 显存节省);Qwen2.5-7B-Instruct 作为基座模型(阿里通义千问系列,支持中英文);LoRA/QLoRA 作为参数高效微调方法(仅训练约 0.06% 参数);Hugging Face Transformers + TRL 提供 SFTTrainer 与数据处理工具;bitsandbytes 实现 4-bit 量化加载;训练数据来自 Chinese-medical-dialogue-data(覆盖内科、外科、儿科、妇产科、男科、肿瘤科六大科室);支持导出 GGUF 格式用于 llama.cpp / Ollama 本地推理。

项目亮点#

  • 高效微调:基于 Unsloth 的 FastLanguageModel,相比原生 Transformers 训练速度提升 2-5 倍,显存占用降低 50%-80%,单张 RTX 4090(24GB)即可完成 7B 模型的 LoRA 微调。
  • 真实医疗数据:使用 Chinese-medical-dialogue-data 数据集,包含约 79 万条真实医疗问答对,覆盖内科(22 万)、外科(11.6 万)、儿科(10.2 万)、妇产科(18.4 万)、男科(9.5 万)、肿瘤科(7.6 万)六大科室。
  • 参数高效:LoRA(r=16, alpha=16)仅训练约 4200 万参数(占总参数 0.06%),在保持基座模型通用能力的同时注入医疗领域知识。
  • 全流程工程化:覆盖数据清洗、格式转换、分词、微调、模型合并、推理验证、GGUF 导出的完整链路,便于复现与迁移至其他领域。
  • 多格式导出:支持保存为 Hugging Face 格式(便于继续训练或部署)与 GGUF 格式(便于 llama.cpp / Ollama 本地推理)。

个人角色与产出#

在本项目中负责数据处理、微调训练与推理验证的全栈开发,重点强调:

  • 数据清洗与格式转换:将原始 CSV 数据(含 department、title、question、answer 四列)清洗并转换为 Alpaca 格式(instruction、input、output),处理空值、异常字符与过长文本。
  • 微调训练:基于 Unsloth FastLanguageModel 加载 4-bit 量化的 Qwen2.5-7B-Instruct,注入 LoRA Adapter(rank=16, alpha=16, target_modules 包含 q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj),使用 SFTTrainer 进行监督微调。
  • 推理与导出:实现推理脚本验证微调效果,支持合并 LoRA 为完整模型、导出 GGUF(q4_k_m / q8_0)用于本地部署。
类别 技术选型 用途
基座模型 Qwen2.5-7B-Instruct 中英文指令微调基座
微调框架 Unsloth 高效微调(2-5x 加速、50%-80% 显存节省)
微调方法 LoRA / QLoRA 参数高效微调(仅训练 0.06% 参数)
量化 bitsandbytes 4-bit 显存优化,单卡 24GB 可训练 7B
训练工具 TRL SFTTrainer 监督微调训练器
数据集 Chinese-medical-dialogue-data 79 万条中文医疗问答
导出格式 GGUF llama.cpp / Ollama 本地推理

项目背景#

通用大模型在医疗领域的表现往往不够专业:对专业术语理解有限、回答可能不够准确或缺乏针对性。直接使用 ChatGPT 或通用 Qwen 回答医疗问题,可能给出过于笼统或不够专业的建议。本项目通过在大规模真实医疗问答数据上进行微调,使模型习得医疗领域的表达方式、专业术语与回答逻辑,从而在医疗咨询场景中提供更专业、更有针对性的回复。

为何选择 Qwen2.5-7B-Instruct? Qwen 系列在中文任务上表现优异,7B 规模在单卡 24GB 显存下可通过 4-bit 量化 + LoRA 完成微调,兼顾效果与资源成本;Instruct 版本已经过指令微调,具备良好的指令跟随能力,便于在医疗问答场景中进一步特化。

选择 Unsloth 是因为其对 Hugging Face 生态的无缝兼容与显著的效率提升:相同配置下训练速度提升 2-5 倍、显存占用降低 50%-80%,使得单卡即可完成原本需要多卡的训练任务,降低了实验与迭代的门槛。


系统架构与选型#

整体为单机训练架构,在单张 RTX 4090(24GB)上完成全部流程。

数据流:原始数据(Chinese-medical-dialogue-data 的 6 个科室 CSV 文件)经清洗脚本读取 → 提取 department、title、question、answer 四列 → 按 Alpaca 格式转换为 instruction(「现在你是一个{department}医生,请根据患者的问题给出建议:」)、input(title + question 拼接)、output(answer)→ 保存为 JSONL 用于训练。

训练流:Unsloth 的 FastLanguageModel.from_pretrained 加载 4-bit 量化的 Qwen2.5-7B-Instruct → 调用 get_peft_model 注入 LoRA Adapter(r=16, lora_alpha=16, target_modules 包含注意力与 FFN 的投影层)→ 使用 SFTTrainer(来自 TRL)配合 Alpaca prompt 模板进行监督微调(learning_rate=2e-4, batch_size=2, gradient_accumulation=4, max_seq_length=2048, num_train_epochs=1)→ 训练完成后保存 LoRA Adapter 与 tokenizer。

推理/导出流:加载基座模型与 LoRA Adapter → 调用 merge_and_unload 合并为完整模型(可选)→ 使用 Hugging Face generate 或 Unsloth 的 FastLanguageModel.for_inference 进行推理 → 可导出为 GGUF(q4_k_m / q8_0)用于 llama.cpp / Ollama。

选型考量:Unsloth 通过 Triton 内核优化与内存管理,在不改变模型结构的前提下大幅提升训练效率,且与 Hugging Face Transformers 完全兼容,便于后续部署与迁移。LoRA 仅训练低秩分解的增量矩阵,在保持基座模型能力的同时注入领域知识,且 Adapter 可独立保存与加载,便于多任务/多领域切换。4-bit 量化(NF4 + 双量化)将显存需求降至原生 float16 的约 1/4,使得 7B 模型在 24GB 显存下可训练。

与 Manshu LLM 的关系:Manshu LLM 侧重从零实现 Transformer 各模块以理解底层原理;满树 Finetune 则侧重利用现有预训练模型与高效微调工具,在真实数据上快速迭代出可用的领域模型。两者互为补充,分别覆盖「深度理解」与「工程落地」两个维度。

关键技术选型理由#

  • Unsloth

    基于 Triton 的高效训练框架,通过内核融合与内存优化实现 2-5 倍训练加速与 50%-80% 显存节省。与 Hugging Face Transformers 完全兼容,无需修改模型代码,只需替换加载方式即可获得效率提升。支持 Llama、Mistral、Qwen 等主流模型架构。

  • LoRA(Low-Rank Adaptation)

    在预训练权重旁注入低秩分解的增量矩阵(A @ B),仅训练 A、B 两个小矩阵(rank=16 时每层约几百 KB)。推理时可合并回原权重无额外延迟,或保持分离便于多任务切换。相比全量微调,训练参数减少约 1000 倍,显存与时间成本大幅降低。

  • Qwen2.5-7B-Instruct

    阿里通义千问系列的 7B 指令微调模型,在中文任务上表现优异。Instruct 版本已具备指令跟随与多轮对话能力,适合作为医疗问答的基座。7B 规模在 4-bit 量化下显存需求约 4-5GB,加上 LoRA 梯度与优化器状态,总显存占用约 15-18GB,单卡 24GB 可训练。

  • 4-bit 量化(NF4 + 双量化)

    bitsandbytes 的 NF4(Normal Float 4-bit)量化将权重压缩为 4-bit,配合双量化(quantize the quantization constants)进一步降低显存。量化仅影响权重存储,梯度与优化器状态仍为 float16/float32,对训练精度影响有限。

  • Alpaca 格式

    指令微调的标准格式,包含 instruction(指令)、input(输入/上下文)、output(期望输出)三个字段。通过 prompt 模板将三者拼接为完整的训练样本,模型学习根据 instruction + input 生成 output。格式简单、通用性强,便于适配不同数据源。


功能模块#

数据清洗与格式转换#

原始数据来自 Chinese-medical-dialogue-data,包含 6 个科室的 CSV 文件:

科室 文件 问答对数量
内科 IM_内科 220,606
外科 Surgical_外科 115,991
儿科 Pediatric_儿科 101,602
妇产科 OAGD_妇产科 183,751
男科 Andriatria_男科 94,596
肿瘤科 Oncology_肿瘤科 75,553
合计 792,099

清洗步骤

  1. 读取各科室 CSV,统一编码为 UTF-8
  2. 过滤空值:剔除 question 或 answer 为空的行
  3. 文本清洗:去除首尾空白、替换异常字符(如乱码、特殊 Unicode)
  4. 长度过滤:剔除 question + answer 超过 1500 字符的样本(避免超长序列影响训练)
  5. 去重:按 question 去重,保留首条(同一问题可能有多条相似回答)

格式转换

{
    "instruction": "现在你是一个内科医生,请根据患者的问题给出建议:",
    "input": "高血压患者能吃党参吗?我有高血压这两天女婿来的时候给我拿了些党参泡水喝,您好高血压可以吃党参吗?",
    "output": "高血压病人可以口服党参的。党参有降血脂,降血压的作用,可以彻底消除血液中的垃圾,从而对冠心病以及心血管疾病的患者都有一定的稳定预防工作作用,因此平时口服党参能远离三高的危害。另外党参除了益气养血,降低中枢神经作用,调整消化系统功能,健脾补肺的功能。感谢您的进行咨询,期望我的解释对你有所帮助。"
}

清洗后数据按 9:1 划分为训练集与验证集,保存为 JSONL 格式。

模型加载与 LoRA 注入#

使用 Unsloth 加载 4-bit 量化的 Qwen2.5-7B-Instruct:

from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen2.5-7B-Instruct-bnb-4bit",
    max_seq_length=2048,
    dtype=None,  # 自动检测
    load_in_4bit=True,
)

注入 LoRA Adapter:

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,
)

配置说明

  • r=16:LoRA 秩,控制增量矩阵的表达能力与参数量
  • lora_alpha=16:缩放因子,通常设为与 r 相同
  • target_modules:注入 LoRA 的模块,覆盖注意力(Q/K/V/O)与 FFN(gate/up/down)
  • use_gradient_checkpointing=“unsloth”:Unsloth 优化的梯度检查点,进一步节省显存

训练配置与执行#

使用 TRL 的 SFTTrainer 进行监督微调:

from trl import SFTTrainer
from transformers import TrainingArguments

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    dataset_text_field="text",
    max_seq_length=2048,
    args=TrainingArguments(
        per_device_train_batch_size=2,
        gradient_accumulation_steps=4,
        warmup_steps=100,
        num_train_epochs=1,
        learning_rate=2e-4,
        fp16=not torch.cuda.is_bf16_supported(),
        bf16=torch.cuda.is_bf16_supported(),
        logging_steps=10,
        eval_steps=500,
        save_steps=500,
        output_dir="outputs",
        optim="adamw_8bit",
    ),
)

trainer.train()

训练超参

参数 说明
per_device_train_batch_size 2 单卡 batch size
gradient_accumulation_steps 4 梯度累积,等效 batch size = 8
learning_rate 2e-4 学习率
warmup_steps 100 预热步数
num_train_epochs 1 训练轮数(79 万样本,1 轮约 10 万步)
max_seq_length 2048 最大序列长度
optim adamw_8bit 8-bit AdamW,节省优化器状态显存

推理验证#

训练完成后加载 LoRA Adapter 进行推理:

FastLanguageModel.for_inference(model)

inputs = tokenizer(
    [alpaca_prompt.format(
        "现在你是一个内科医生,请根据患者的问题给出建议:",
        "我最近经常头晕,血压也偏高,请问需要注意什么?",
        ""
    )],
    return_tensors="pt"
).to("cuda")

outputs = model.generate(**inputs, max_new_tokens=256, temperature=0.7)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

验证指标

  • 人工评估:抽取 100 条验证集样本,对比微调前后回答的专业性与相关性
  • BLEU-4 / ROUGE-L:与参考答案的文本相似度(仅供参考,医疗问答非唯一标准答案)

模型导出#

保存 LoRA Adapter

model.save_pretrained("lora_model")
tokenizer.save_pretrained("lora_model")

合并为完整模型

model.save_pretrained_merged("merged_model", tokenizer, save_method="merged_16bit")

导出 GGUF(用于 llama.cpp / Ollama):

model.save_pretrained_gguf("gguf_model", tokenizer, quantization_method="q4_k_m")

支持的量化方法:q4_k_m(4-bit,推荐)、q8_0(8-bit,精度更高)、f16(半精度,无损)。


技术难点与实现#

显存优化#

难点:7B 参数模型的全量微调需要 60-80GB 显存,单卡 24GB 无法完成;即使使用 LoRA,原生 Transformers 加载 + 训练仍需约 30GB。

方案

  1. 4-bit 量化加载:使用 bitsandbytes 的 NF4 量化,将模型权重从 float16(14GB)压缩至 4-bit(约 4GB)
  2. Unsloth 优化:Triton 内核融合减少中间激活显存,优化的梯度检查点进一步降低峰值显存
  3. 8-bit AdamW:优化器状态使用 8-bit 存储,相比 float32 节省 75% 显存
  4. 梯度累积:使用小 batch + 梯度累积,等效大 batch 的同时降低单步显存峰值

最终在 RTX 4090(24GB)上稳定训练,峰值显存约 18GB。

数据质量与格式#

难点:原始医疗问答数据存在噪声(空回答、重复问题、过长文本、编码问题),直接训练会影响模型质量。

方案

  1. 多阶段清洗:空值过滤 → 编码修复 → 长度截断 → 去重
  2. 分科室统计:记录各科室清洗前后样本数,确保数据分布合理
  3. Alpaca 格式标准化:统一 prompt 模板,确保 instruction 明确指定医生角色与任务

清洗后保留约 75 万条高质量样本,覆盖六大科室。

训练稳定性#

难点:大规模数据 + 长序列训练容易出现梯度爆炸或损失不收敛。

方案

  1. Warmup:前 100 步学习率从 0 线性增长至 2e-4,避免初期梯度不稳定
  2. 梯度裁剪:max_grad_norm=1.0,防止梯度爆炸
  3. bf16 混合精度:在支持的 GPU 上使用 bf16,数值范围更大、更稳定
  4. 定期 eval:每 500 步在验证集评估,监控是否过拟合

训练损失平稳下降,验证损失在 0.8-1.0 区间稳定。


总结#

作为个人代表项目,满树 Finetune 重点体现两点:一是基于 Unsloth + LoRA 的高效微调方案,在单卡 24GB 显存下完成 7B 模型的领域微调;二是使用真实医疗问答数据集(79 万条)实现医疗领域问答能力的增强。项目覆盖从数据清洗、格式转换、模型加载、LoRA 注入、训练、推理到 GGUF 导出的完整链路,具备可复现性与可迁移性。

后续可扩展方向:增加多轮对话数据的微调(当前为单轮问答);引入 DPO / RLHF 进一步对齐人类偏好;扩展至其他医疗数据源(如医学文献、病历摘要);部署为 API 服务或接入 RAG 系统实现知识增强型医疗问答。