满树 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 |
清洗步骤:
- 读取各科室 CSV,统一编码为 UTF-8
- 过滤空值:剔除 question 或 answer 为空的行
- 文本清洗:去除首尾空白、替换异常字符(如乱码、特殊 Unicode)
- 长度过滤:剔除 question + answer 超过 1500 字符的样本(避免超长序列影响训练)
- 去重:按 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。
方案:
- 4-bit 量化加载:使用 bitsandbytes 的 NF4 量化,将模型权重从 float16(14GB)压缩至 4-bit(约 4GB)
- Unsloth 优化:Triton 内核融合减少中间激活显存,优化的梯度检查点进一步降低峰值显存
- 8-bit AdamW:优化器状态使用 8-bit 存储,相比 float32 节省 75% 显存
- 梯度累积:使用小 batch + 梯度累积,等效大 batch 的同时降低单步显存峰值
最终在 RTX 4090(24GB)上稳定训练,峰值显存约 18GB。
数据质量与格式#
难点:原始医疗问答数据存在噪声(空回答、重复问题、过长文本、编码问题),直接训练会影响模型质量。
方案:
- 多阶段清洗:空值过滤 → 编码修复 → 长度截断 → 去重
- 分科室统计:记录各科室清洗前后样本数,确保数据分布合理
- Alpaca 格式标准化:统一 prompt 模板,确保 instruction 明确指定医生角色与任务
清洗后保留约 75 万条高质量样本,覆盖六大科室。
训练稳定性#
难点:大规模数据 + 长序列训练容易出现梯度爆炸或损失不收敛。
方案:
- Warmup:前 100 步学习率从 0 线性增长至 2e-4,避免初期梯度不稳定
- 梯度裁剪:max_grad_norm=1.0,防止梯度爆炸
- bf16 混合精度:在支持的 GPU 上使用 bf16,数值范围更大、更稳定
- 定期 eval:每 500 步在验证集评估,监控是否过拟合
训练损失平稳下降,验证损失在 0.8-1.0 区间稳定。
总结#
作为个人代表项目,满树 Finetune 重点体现两点:一是基于 Unsloth + LoRA 的高效微调方案,在单卡 24GB 显存下完成 7B 模型的领域微调;二是使用真实医疗问答数据集(79 万条)实现医疗领域问答能力的增强。项目覆盖从数据清洗、格式转换、模型加载、LoRA 注入、训练、推理到 GGUF 导出的完整链路,具备可复现性与可迁移性。
后续可扩展方向:增加多轮对话数据的微调(当前为单轮问答);引入 DPO / RLHF 进一步对齐人类偏好;扩展至其他医疗数据源(如医学文献、病历摘要);部署为 API 服务或接入 RAG 系统实现知识增强型医疗问答。