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

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

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

text 复制代码
公开数据 → 数据清洗 → 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 调用

需要先说明:这不是从零训练的基础大模型。它更准确的名称是:

基于 Qwen3-0.6B 的中医领域 LoRA 指令微调模型。

二、本地环境

本次实验使用的主要环境为:

  • 操作系统:Windows,Docker Desktop 提供 Linux 容器环境
  • GPU:NVIDIA GeForce RTX 5070
  • Python 环境:D:\AI\conda-envs\llm
  • PyTorch:2.13.0 + CUDA 13.0
  • Transformers:5.14.1
  • PEFT:0.20.0
  • TRL:1.9.2
  • 底座:Qwen3-0.6B

所有大体积模型和数据主要放在 D 盘:

text 复制代码
D:\AI\datasets             原始数据
D:\AI\models               Hugging Face、GGUF 和 Ollama 模型
D:\AI\projects             训练项目
D:\AI\conda-envs           Python 环境

三、先把大模型训练理解成一条流水线

项目中最重要的是四个 Python 文件:

text 复制代码
prepare_data.py   整理教材
train_lora.py     让模型学习教材
evaluate.py       比较训练前后的回答
merge_lora.py     把 LoRA 合并进底座模型

完整的数据流是:

text 复制代码
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. 为什么要过滤行动性医疗问题

我希望第一轮模型首先学习中医理论和文献表达,而不是学习直接给患者开药。因此代码使用正则表达式排除了包含"患者治疗、服用、用药、处方、剂量、首选方剂、选取穴位"等倾向的问题。

这并不能保证数据医学正确,但可以降低第一轮训练明显偏向直接医疗建议的风险。

2. 统一成聊天格式

通过清洗的每条数据最终被转换成下面的结构:

json 复制代码
{
  "messages": [
    {
      "role": "system",
      "content": "你是一个中医文献学习助手......"
    },
    {
      "role": "user",
      "content": "为什么肺被称为娇脏和水之上源?"
    },
    {
      "role": "assistant",
      "content": "肺被称为娇脏是因为......"
    }
  ]
}

模型训练时看到系统规则和用户问题,然后学习如何生成 assistant 对应的回答。

3. 固定随机种子

python 复制代码
rng = random.Random(20260808)
rng.shuffle(rows)

固定随机种子后,多次运行会得到相同的数据划分。这一点很重要,否则每次训练使用的验证集和测试集都不同,结果就很难比较。

五、LoRA:只训练少量附加参数

如果直接全量微调 Qwen3,需要为全部参数保存梯度和优化器状态。LoRA 的思路是基本保持原模型不变,只学习一组低秩参数。

可以把原始权重记为 W,LoRA 学习两个较小的矩阵 AB

text 复制代码
新权重效果 = W + (alpha / r) × B × A

本项目使用:

python 复制代码
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 加载。

六、训练参数为什么这样设置

核心训练配置如下:

python 复制代码
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,
    save_steps=40,
)

有效批大小

显卡每次只处理一条样本,但连续累计 8 次梯度再更新参数:

text 复制代码
有效批大小 = 1 × 8 = 8

这样可以降低单次显存压力。

只学习助手回答

assistant_only_loss=True 表示系统提示词和用户问题会作为上下文输入,但只对助手回答部分计算训练损失。模型学习的是"看到这个问题应该怎样回答",而不是复述用户输入。

梯度检查点

gradient_checkpointing=True 会减少中间激活值的显存占用,代价是反向传播时进行一部分重复计算。

本次训练的峰值 CUDA 显存只有约 2.317 GiB。

为什么只训练 0.48 个 epoch

2,000 条训练数据、有效批大小为 8,完整学习一遍大约需要:

text 复制代码
2000 ÷ 8 = 250 个优化步骤

本次只训练 120 步:

text 复制代码
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 后的模型回答。

生成配置关闭了随机采样和思考模式:

python 复制代码
output = model.generate(
    max_new_tokens=220,
    do_sample=False,
    repetition_penalty=1.05,
)

这样可以减少随机性对前后比较的干扰。

首轮结果显示,微调后的模型更倾向使用中医领域表达,但冷门植物学名测试仍然出现了明显幻觉。这说明:

少量 SFT 可以改变表达方式和回答倾向,但不能替代可靠知识库、人工校勘和专家评测。

九、LoRA 为什么还要合并

训练得到的 adapter_model.safetensors 只有约 40 MB,它只是相对于 Qwen3 的参数差值,不能脱离底座模型独立运行。

合并代码的核心是:

python 复制代码
base = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen3-0.6B",
    dtype=torch.bfloat16,
)

model = PeftModel.from_pretrained(base, adapter_path)
model = model.merge_and_unload()
model.save_pretrained(output_path, safe_serialization=True)

merge_and_unload() 会把 LoRA 学到的变化加回原始权重,生成一个完整的 Hugging Face 模型。

十、从 Hugging Face 转换到 GGUF

合并后的模型仍然采用 Hugging Face 的目录结构,其中包含:

text 复制代码
model.safetensors
config.json
tokenizer.json
tokenizer_config.json
chat_template.jinja

为了让 Ollama 更方便地运行,我使用 llama.cpp 的转换脚本把它转换为 GGUF:

powershell 复制代码
$py = 'D:\AI\conda-envs\llm\python.exe'

& $py `
  'D:\AI\packages\llama.cpp\convert_hf_to_gguf.py' `
  'D:\AI\projects\wo-x-tcm-qwen\outputs\wo-x-tcm-qwen-merged' `
  --outfile 'D:\AI\models\gguf\wo-x-tcm-qwen-pilot-bf16.gguf' `
  --outtype bf16

GGUF 可以在一个文件中保存模型张量、结构元数据、词表和聊天模板,更适合 llama.cpp/Ollama 这一类本地推理工具。

本次输出使用 BF16,没有进一步进行 Q4 量化。

十一、注册到 Ollama

Ollama 使用 Modelfile 描述模型文件、生成参数和系统提示词:

dockerfile 复制代码
FROM D:/AI/models/gguf/wo-x-tcm-qwen-pilot-bf16.gguf

PARAMETER temperature 0.3
PARAMETER top_p 0.8
PARAMETER repeat_penalty 1.05
PARAMETER num_ctx 8192

SYSTEM """
你是 wo-x 中医文献学习模型,仅用于学习传统中医文献和大模型训练方法。
回答时必须区分传统中医理论与现代医学证据......
"""

然后注册模型:

powershell 复制代码
$env:OLLAMA_MODELS = 'D:\AI\models\ollama'

ollama create `
  'wo-x-tcm:pilot' `
  -f 'D:\AI\projects\wo-x-tcm-qwen\Modelfile'

ollama create 不会再次训练。它只是读取 GGUF,建立 Ollama 模型清单,并把名字 wo-x-tcm:pilot 指向对应权重和运行参数。

十二、Docker 是怎么使用 D 盘模型的

Docker Compose 中最重要的配置是:

yaml 复制代码
services:
  ollama:
    image: ollama/ollama:latest
    gpus: all
    ports:
      - "11435:11434"
    volumes:
      - D:/AI/models/ollama-docker:/root/.ollama
      - D:/AI/models/gguf:/models:ro

这里并不是把整个模型手动复制到一个 Linux 虚拟机里,而是把 Windows 的 D 盘目录映射为容器内目录:

text 复制代码
Windows: D:\AI\models\gguf
Docker:  /models

因此 Docker 版 Modelfile 使用:

dockerfile 复制代码
FROM /models/wo-x-tcm-qwen-pilot-bf16.gguf

gpus: all 允许 Ollama 容器使用 NVIDIA GPU。

端口映射:

text 复制代码
Windows 原生 Ollama:127.0.0.1:11434
Docker Ollama:       127.0.0.1:11435

十三、Open WebUI 怎么连接模型

Open WebUI 也运行在 Docker 中:

yaml 复制代码
open-webui:
  ports:
    - "127.0.0.1:3000:8080"
  environment:
    OLLAMA_BASE_URL: http://ollama:11434

浏览器访问:

text 复制代码
http://127.0.0.1:3000

请求链路为:

text 复制代码
浏览器
  ↓
Open WebUI
  ↓ Docker 内部网络
http://ollama:11434
  ↓
Ollama
  ↓
RTX 5070 推理

Open WebUI 在容器内部通过服务名 ollama 找到 Ollama 容器,所以这里不能写成 127.0.0.1:11435

十四、我遇到的几个坑

1. Docker 可以运行 Linux,但路径仍然不同

Windows 路径是:

text 复制代码
D:\AI\models\gguf\model.gguf

映射到容器后是:

text 复制代码
/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 中的:

dockerfile 复制代码
PARAMETER num_ctx 4096

改为:

dockerfile 复制代码
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 的小模型跑通完整流程,再扩大数据和模型规模。能跑通、能评测、能复现,比第一次就追求大参数更重要。


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

text 复制代码
prepare_data.py
train_lora.py
evaluate.py
merge_lora.py
Modelfile
compose.yaml

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

相关推荐
opensnn1 小时前
当闭源AI遇上开源反击:未来竞争的核心不是模型,而是生态
人工智能·开源
一碗白开水一1 小时前
入门实践工程九:基于 BERT 的中文情感分类微调~附:安装依赖库及工程源码
人工智能·深度学习·机器学习·自然语言处理·分类·bert
空堂与归1 小时前
大模型幻觉:一篇文章搞懂成因、分类与四种解决方案
人工智能
CoordClaw1 小时前
主流多智能体架构为什么大多失败——它们输在结构,不在模型
人工智能·架构
一只游鱼1 小时前
PianoAgent:开源 AI 钢琴作曲 Agent,用自然语言谱写钢琴曲
人工智能
weixin_446260851 小时前
资源授权:面向部署式AI智能体的参与式治理机制设计模型
人工智能
康谋自动驾驶1 小时前
高保真+强可控:自动驾驶仿真的混合渲染方案
人工智能·机器学习·自动驾驶
JJJennie7772 小时前
ChatGPT 更新 GPT-5.6 Sol,免费用户将可无限文本聊天
人工智能·gpt·chatgpt
OceanBase数据库官方博客2 小时前
让 DRP全域数据智能流转OceanBase AI 数据库支撑央国企落地穿透式监
数据库·人工智能·oceanbase