位置:首页 > 进阶教程 > 本地训练中医领域Qwen3模型:从数据清洗到Docker部署

本地训练中医领域Qwen3模型:从数据清洗到Docker部署

时间:2026-08-21  |  作者:半糖攻略君  |  阅读:0

从数据清洗到 Docker 部署:我在本地训练了一个中医领域 Qwen3 模型

最近我想完整学习一次大模型的训练和部署流程。我的目标不是调用一个在线 API,也不是简单下载模型运行,而是亲自走完下面这条链路:

从数据清洗到 Docker 部署:我在本地训练了一个中医领域 Qwen3 模型

公开数据 → 数据清洗 → 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 学习两个较小的矩阵 AB

新权重效果 = 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. 损失下降不等于医学知识可靠

训练损失只能说明模型越来越会模仿训练答案,不能证明答案医学正确。数据本身如果存在错误,模型也会学习错误。

十五、这次实验的真正收获

这次实验最重要的结果不是得到一个可以直接用于医疗的模型,而是我真正跑通了领域大模型的完整工程链路:

  1. 知道如何审计和构造训练数据。
  2. 理解了 LoRA 只保存参数差值。
  3. 学会用验证集观察训练是否发散。
  4. 学会比较原始模型和微调模型。
  5. 理解了 Adapter、完整权重和 GGUF 的区别。
  6. 学会把 Hugging Face 模型迁移到 Ollama。
  7. 理解 Docker 卷映射、端口和内部服务网络。
  8. 学会通过日志排查上下文和隐藏提示词问题。

十六、下一步计划

当前版本只是教学性质的 pilot。下一轮我计划:

  • 人工抽检并修订训练样本
  • 增加急症、儿童、孕产妇和药物相互作用安全样本
  • 建立完全隔离的专家测试集
  • 加入来源可靠、许可明确的中医古籍 RAG 知识库
  • 把 SFT 数据扩大到 1 万条以上
  • 再研究中医原始文献的继续预训练
  • 对比 LoRA、QLoRA 和不同量化格式的效果

如果从零开始学习本地大模型,我的建议是:先用一个 0.5B~1B 的小模型跑通完整流程,再扩大数据和模型规模。能跑通、能评测、能复现,比第一次就追求大参数更重要。

项目中的主要代码顺序是:

prepare_data.pytrain_lora.pyevaluate.pymerge_lora.pyModelfilecompose.yaml

按照这个顺序阅读,就能对应“数据、训练、评测、合并、部署、服务”的完整过程。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多