本地训练中医领域Qwen3模型:从数据清洗到Docker部署
时间:2026-08-21 | 作者:半糖攻略君 | 阅读:0从数据清洗到 Docker 部署:我在本地训练了一个中医领域 Qwen3 模型
最近我想完整学习一次大模型的训练和部署流程。我的目标不是调用一个在线 API,也不是简单下载模型运行,而是亲自走完下面这条链路:
公开数据 → 数据清洗 → LoRA 微调 → 独立评测→ 权重合并 → GGUF 转换 → Ollama → Docker → Open WebUI
走完整条流程后,这台 Windows 电脑上最终落地了一个名为 wo-x-tcm:pilot 的本地中医文献学习模型,同时也借助 Open WebUI 配好了可视化聊天界面。
这篇文章记录这次实验的真实代码思路、训练参数、部署方法和踩坑过程。它更像一个可以复现的学习项目,而不是一个已经能够用于临床的医疗模型。
一、我最终做出了什么
这次实验使用的底座模型是 Qwen/Qwen3-0.6B。我使用公开中医问答数据对它进行了 LoRA 指令微调,然后把 LoRA 与底座模型合并,转换为 GGUF,最后部署到 Docker Ollama 和 Open WebUI。
主要产物如下:
| 产物 | 大小 | 作用 |
|---|---|---|
| LoRA Adapter | 约 40.4 MB | 保存微调产生的参数差值 |
| 合并后的 Hugging Face 模型 | 约 1.192 GB | 完整的 Qwen3 权重与中医微调结果 |
| BF16 GGUF | 约 1.198 GB | 供 llama.cpp/Ollama 本地推理 |
| Ollama 模型 | wo-x-tcm:pilot | 供命令行、API 和 Open WebUI 调用 |
需要先说明:这不是从零训练的基础大模型。它更准确的名称是:
二、本地环境
本次实验使用的主要环境为:
- 操作系统:Windows,Docker Desktop 提供 Linux 容器环境
- GPU:NVIDIA GeForce RTX 5070
- Python 环境:
D:AIconda-envsllm - PyTorch:2.13.0 + CUDA 13.0
- Transformers:5.14.1
- PEFT:0.20.0
- TRL:1.9.2
- 底座:Qwen3-0.6B
所有大体积模型和数据主要放在 D 盘:
D:AIdatasets 原始数据D:AImodels Hugging Face、GGUF 和 Ollama 模型D:AIprojects 训练项目D:AIconda-envs Python 环境
三、先把大模型训练理解成一条流水线
项目中最重要的是四个 Python 文件:
prepare_data.py 整理教材train_lora.py 让模型学习教材evaluate.py 比较训练前后的回答merge_lora.py 把 LoRA 合并进底座模型
完整的数据流是:
15.7 万条原始记录↓清洗、过滤、去重↓train / validation / test↓Qwen3-0.6B + LoRA↓LoRA Adapter↓合并后的 Hugging Face 模型↓BF16 GGUF↓Ollama + Docker + Open WebUI
四、数据清洗:先决定模型应该学什么
原始数据来自 Baize TCM V3。数据卡声明的许可证为 Apache-2.0,但在公开或分发任何衍生模型之前,仍应再次核验底座模型、数据集和各个文件的许可范围。
原始记录一共有 157,438 条。我的脚本依次做了长度过滤、中医领域词过滤、行动性医疗问题过滤和问题去重。
实际统计为:
- 原始记录:157,438 条
- 长度不合格:15,809 条
- 明显非中医内容:72,554 条
- 带治疗、服用、处方、剂量等行动性问题:29,074 条
- 重复问题:893 条
- 清洗后候选:39,108 条
首轮实验只从候选数据中取了:
- 训练集:2,000 条
- 验证集:200 条
- 测试集:50 条
1. 为什么要过滤行动性医疗问题
我希望第一轮模型首先学习中医理论和文献表达,而不是学习直接给患者开药。因此代码使用正则表达式排除了包含“患者治疗、服用、用药、处方、剂量、首选方剂、选取xue位”等倾向的问题。
这并不能保证数据医学正确,但可以降低第一轮训练明显偏向直接医疗建议的风险。
2. 统一成聊天格式
通过清洗的每条数据最终被转换成下面的结构:
{"messages": [{"role": "system","content": "你是一个中医文献学习助手……"},{"role": "user","content": "为什么肺被称为娇脏和水之上源?"},{"role": "assistant","content": "肺被称为娇脏是因为……"}]}
模型训练时看到系统规则和用户问题,然后学习如何生成 assistant 对应的回答。
3. 固定随机种子
rng = random.Random(20260808)rng.shuffle(rows)
固定随机种子后,多次运行会得到相同的数据划分。这一点很重要,否则每次训练使用的验证集和测试集都不同,结果就很难比较。
五、LoRA:只训练少量附加参数
如果直接全量微调 Qwen3,需要为全部参数保存梯度和优化器状态。LoRA 的思路是基本保持原模型不变,只学习一组低秩参数。
可以把原始权重记为 W,LoRA 学习两个较小的矩阵 A 和 B:
新权重效果 = W + (alpha / r) × B × A
本项目使用:
peft_config = LoraConfig(task_type="CAUSAL_LM",r=16,lora_alpha=32,lora_dropout=0.05,bias="none",target_modules=["q_proj", "k_proj", "v_proj", "o_proj","gate_proj", "up_proj", "down_proj",],)
LoRA 被加在注意力层和前馈网络上。实际可训练参数为 10,092,544,只占总参数的约 1.665%。
这次使用的是普通 BF16 LoRA,不是 QLoRA。因为 0.6B 模型体积较小,在这张显卡上可以直接以 BF16 加载。
六、训练参数为什么这样设置
核心训练配置如下:
train_config = SFTConfig(max_steps=120,per_device_train_batch_size=1,gradient_accumulation_steps=8,learning_rate=1e-4,lr_scheduler_type="cosine",max_length=512,assistant_only_loss=True,bf16=True,gradient_checkpointing=True,eval_steps=40,sa ve_steps=40,)
有效批大小
显卡每次只处理一条样本,但连续累计 8 次梯度再更新参数:
有效批大小 = 1 × 8 = 8
这样可以降低单次显存压力。
只学习助手回答
assistant_only_loss=True 表示系统提示词和用户问题会作为上下文输入,但只对助手回答部分计算训练损失。模型学习的是“看到这个问题应该怎样回答”,而不是复述用户输入。
梯度检查点
gradient_checkpointing=True 会减少中间激活值的显存占用,代价是反向传播时进行一部分重复计算。
本次训练的峰值 CUDA 显存只有约 2.317 GiB。
为什么只训练 0.48 个 epoch
2,000 条训练数据、有效批大小为 8,完整学习一遍大约需要:
2000 ÷ 8 = 250 个优化步骤
本次只训练 120 步:
120 ÷ 250 = 0.48 epoch
这是一个用于验证完整工程流程的 pilot,而不是最终训练版本。
七、实际训练结果
训练耗时约 410.5 秒,也就是约 6 分 50 秒。
主要指标为:
- 初期训练损失约 2.383
- 第 120 步训练损失约 1.965
- 第 40 步验证损失:2.037
- 第 80 步验证损失:2.014
- 第 120 步验证损失:2.009
验证损失在缓慢下降,没有明显发散。不过下降幅度不大,这也说明少量数据和少量训练步骤带来的能力提升有限。
八、怎么比较微调前后的模型
评测脚本使用同一批问题,分别让原始 Qwen3 和挂载 LoRA 后的模型回答。
生成配置关闭了随机采样和思考模式:
output = model.generate(max_new_tokens=220,do_sample=False,repetition_penalty=1.05,)
这样可以减少随机性对前后比较的干扰。
首轮结果显示,微调后的模型更倾向使用中医领域表达,但冷门植物学名测试仍然出现了明显幻觉。这说明:
九、LoRA 为什么还要合并
训练得到的 adapter_model.safetensors 只有约 40 MB,它只是相对于 Qwen3 的参数差值,不能脱离底座模型独立运行。
合并代码的核心是:
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-0.6B",dtype=torch.bfloat16,)model = PeftModel.from_pretrained(base, adapter_path)model = model.merge_and_unload()model.sa ve_pretrained(output_path, safe_serialization=True)
merge_and_unload() 会把 LoRA 学到的变化加回原始权重,生成一个完整的 Hugging Face 模型。
十、从 Hugging Face 转换到 GGUF
合并后的模型仍然采用 Hugging Face 的目录结构,其中包含:
model.safetensorsconfig.jsontokenizer.jsontokenizer_config.jsonchat_template.jinja
为了让 Ollama 更方便地运行,我使用 llama.cpp 的转换脚本把它转换为 GGUF:
$py = 'D:AIconda-envsllmpython.exe'& $py `'D:AIpackagesllama.cppconvert_hf_to_gguf.py' `'D:AIprojectswo-x-tcm-qwenoutputswo-x-tcm-qwen-merged' `--outfile 'D:AImodelsggufwo-x-tcm-qwen-pilot-bf16.gguf' `--outtype bf16
GGUF 的优势很直接:它把模型张量、结构元数据、词表以及聊天模板都集中放进同一个文件里,管理起来更省事,也更贴合 llama.cpp、Ollama 这类本地推理工具的使用场景。
本次输出使用 BF16,没有进一步进行 Q4 量化。
十一、注册到 Ollama
Ollama 使用 Modelfile 描述模型文件、生成参数和系统提示词:
FROM D:/AI/models/gguf/wo-x-tcm-qwen-pilot-bf16.ggufPARAMETER temperature 0.3PARAMETER top_p 0.8PARAMETER repeat_penalty 1.05PARAMETER num_ctx 8192SYSTEM """你是 wo-x 中医文献学习模型,仅用于学习传统中医文献和大模型训练方法。回答时必须区分传统中医理论与现代医学证据……"""
然后注册模型:
$env:OLLAMA_MODELS = 'D:AImodelsollama'ollama create `'wo-x-tcm:pilot' `-f 'D:AIprojectswo-x-tcm-qwenModelfile'
ollama create 不会再次训练。它只是读取 GGUF,建立 Ollama 模型清单,并把名字 wo-x-tcm:pilot 指向对应权重和运行参数。
十二、Docker 是怎么使用 D 盘模型的
Docker Compose 中最重要的配置是:
services:ollama:image: ollama/ollama:latestgpus: allports:- "11435:11434"volumes:- D:/AI/models/ollama-docker:/root/.ollama- D:/AI/models/gguf:/models:ro
这里并不是把整个模型手动复制到一个 Linux 虚拟机里,而是把 Windows 的 D 盘目录映射为容器内目录:
Windows: D:AImodelsggufDocker:/models
因此 Docker 版 Modelfile 使用:
FROM /models/wo-x-tcm-qwen-pilot-bf16.gguf
gpus: all 允许 Ollama 容器使用 NVIDIA GPU。
端口映射:
Windows 原生 Ollama:127.0.0.1:11434Docker Ollama: 127.0.0.1:11435
十三、Open WebUI 怎么连接模型
Open WebUI 也运行在 Docker 中:
open-webui:ports:- "127.0.0.1:3000:8080"environment:OLLAMA_BASE_URL: http://ollama:11434
浏览器访问:
http://127.0.0.1:3000
请求链路为:
浏览器↓Open WebUI↓ Docker 内部网络http://ollama:11434↓Ollama↓RTX 5070 推理
Open WebUI 在容器内部通过服务名 ollama 找到 Ollama 容器,所以这里不能写成 127.0.0.1:11435。
十四、我遇到的几个坑
1. Docker 可以运行 Linux,但路径仍然不同
Windows 路径是:
D:AImodelsggufmodel.gguf
映射到容器后是:
/models/model.gguf
同一个文件,在宿主机和容器中的路径不同,因此需要 Windows 和 Docker 两份 Modelfile。
2. 一个很短的问题也可能消耗几千 Token
我曾经只输入“咽喉痛吃什么”,Open WebUI 却显示输入达到 5792 Token。
排查后发现,问题本身只有十个汉字,对话也没有历史或文件。真正占用上下文的是 Open WebUI 自动注入的知识库、聊天搜索、笔记、任务、日历等内置工具 JSON 定义。
解决办法是为普通问答模型建立一个 Open WebUI 模型预设,并关闭“内置工具”;或者在当前对话的高级参数中,把函数调用切换为“旧版”。
3. 上下文长度有多层含义
Qwen3-0.6B 的模型结构支持 40960 Token,但 Ollama 运行时原来只配置为 4096。后来我把 Modelfile 中的:
PARAMETER num_ctx 4096
改为:
PARAMETER num_ctx 8192
重新注册模型后,超过 4096 Token 的输入才能正常处理。
4. 损失下降不等于医学知识可靠
训练损失只能说明模型越来越会模仿训练答案,不能证明答案医学正确。数据本身如果存在错误,模型也会学习错误。
十五、这次实验的真正收获
这次实验最重要的结果不是得到一个可以直接用于医疗的模型,而是我真正跑通了领域大模型的完整工程链路:
- 知道如何审计和构造训练数据。
- 理解了 LoRA 只保存参数差值。
- 学会用验证集观察训练是否发散。
- 学会比较原始模型和微调模型。
- 理解了 Adapter、完整权重和 GGUF 的区别。
- 学会把 Hugging Face 模型迁移到 Ollama。
- 理解 Docker 卷映射、端口和内部服务网络。
- 学会通过日志排查上下文和隐藏提示词问题。
十六、下一步计划
当前版本只是教学性质的 pilot。下一轮我计划:
- 人工抽检并修订训练样本
- 增加急症、儿童、孕产妇和药物相互作用安全样本
- 建立完全隔离的专家测试集
- 加入来源可靠、许可明确的中医古籍 RAG 知识库
- 把 SFT 数据扩大到 1 万条以上
- 再研究中医原始文献的继续预训练
- 对比 LoRA、QLoRA 和不同量化格式的效果
如果从零开始学习本地大模型,我的建议是:先用一个 0.5B~1B 的小模型跑通完整流程,再扩大数据和模型规模。能跑通、能评测、能复现,比第一次就追求大参数更重要。
项目中的主要代码顺序是:
prepare_data.pytrain_lora.pyevaluate.pymerge_lora.pyModelfilecompose.yaml
按照这个顺序阅读,就能对应“数据、训练、评测、合并、部署、服务”的完整过程。
免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。
相关文章
更多-
- Qwen3.8-Max实战指南:2.4万亿参数到生产级智能体落地
- 时间:2026-08-21
-
- Qwen3.8-Max模型亮点解析与核心优势介绍
- 时间:2026-08-20
-
- 后摩智能漫界M50芯片适配阿里千问Qwen3.8-27B大模型完成验证
- 时间:2026-08-16
-
- 登临科技KS系列GPU首发适配阿里千问Qwen3.8-27B模型
- 时间:2026-08-16
-
- 阿里开放Qwen3.8-2.4T-A95B权重:原生256K上下文与95B激活模型
- 时间:2026-08-15
-
- 阿里云Token Plan升级:个人版上线团队版降价畅用Qwen3.8-Max
- 时间:2026-08-15
-
- Qwen 3.6 27B评测:本地模型性能接近GPT-5前沿水准
- 时间:2026-08-15
-
- Qwen 3.8 与 Qwen 3.7 Max 有哪些变化
- 时间:2026-08-13
精选合集
更多大家都在玩
大家都在看
更多-
- 糖尿病完全不能吃糖吗
- 时间:2026-09-15
-
- 蚂蚁庄园小课堂2026年9月16日最新题目答案
- 时间:2026-09-15
-
- 小鸡答题今天的答案是什么2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园每日答题答案2026年9月16日
- 时间:2026-09-15
-
- 以下哪种粮食是酿造绍兴黄酒的主要原料 蚂蚁庄园今日答案9月16日
- 时间:2026-09-15
-
- 劝学名句“及时当勉励,岁月不待人”出自哪位诗人 蚂蚁庄园今日答案9.16
- 时间:2026-09-15
-
- 蚂蚁庄园今天答题答案2026年9月16日
- 时间:2026-09-15
-
- 蚂蚁庄园答题今日答案2026年9月16日
- 时间:2026-09-15
