目录

记一次小模型微调/蒸馏学习(Qwen3-0.6B 从收货地址中提取结构化信息)

无意间,看见阿里云提供了针对 Qwen3-0.6B 的蒸馏教程,本文章本质上是跟着阿里云提供的教程学习微调过程,官方教程地址

借用下阿里云教程的介绍:

大参数模型效果好,但成本高、响应慢。为了在保障效果的同时提升推理速度、降低成本,可首先借助大参数模型完成目标任务的数据生成,并使用这些数据微调小参数模型,使其在特定任务中达到接近大参数模型的表现,这一过程也被称为模型蒸馏。

本方案将以从一句话中提取结构化信息(如收件人、地址、电话)为例,演示如何通过模型蒸馏,让 Qwen3-0.6B 模型在此任务上达到大参数模型的表现。

方案信息

教师模型: GLM4.7
样本信息: 由 GLM4.7 生成约1500+条虚拟数据
微调框架: ms-swift
模型推理: vllm

效果

https://s3.aixfan.com/img/screenshot/2026/03/02/1772387128_186b.png https://s3.aixfan.com/img/screenshot/2026/03/02/1772388039_187b.png

改动部分

  1. 新增了postal_coderemark字段,但postal_code在样本训练时没有特地添加,而是在 system 提示词内加的,所以不一定准确,最主要的还是“从地址推邮编”本质是 地理编码/检索问题,仅靠 0.6B 蒸馏感觉还不如由应用层处理。
  2. 我重新试用GLM4.7生成了一批训练样本,没有使用阿里云提供的,主要目的是想提取备注。

样本重新生成

  1. 包含功能区/开发区/高新区/未来科技城/管委会等
  2. 包含自治区/自治州/盟/旗等多级行政
  3. 包含英文/拼音混排行政区(extracted 中要映射为中文规范名称)
  4. 包含括号备注/路线提示/near地铁口/(原xxx)/收件要求/门禁暗号/时间要求
  5. 电话号码带空格、全角破折号、+86、分机/转接/ext(phone 规范化且保留转接信息)
  6. 只有城市,没有省份的地址
  7. 只有某个区,但没有城市、省份的地址

教程

注意:为了方便起见,建议直接使用阿里云教程内的使用环境(老鸟略过),可领取使用点后可以使用9个小时的环境,主要是已经安装好了 CUDA 等驱动,显卡与内存也给得挺大的。

下载训练数据

mkdir /root/qwen-0.6b
cd /root/qwen-0.6b && \
# 下载训练数据 train.jsonl
curl -f -o train.jsonl "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250610/azvmpb/train_with_system.jsonl"

安装依赖

  1. ms-swift 魔搭社区提供的训练框架,支持模型的下载、微调和权重合并,极大简化了微调流程。
  2. vllm 用于部署微调后的模型,支持高性能推理服务,不仅方便验证微调效果,还可用于生成 API,供业务方直接调用。
pip3 install vllm==0.9.0.1 ms-swift==3.5.0

ps: 建议分开安装,不然会卡很久。

pip3 install vllm==0.9.0.1
pip3 install ms-swift==3.5.0

模型微调

cd /root/qwen-0.6b && \
# 下载微调脚本 sft.sh
curl -f -o sft.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250623/cggwpz/sft.sh" && \
# 执行微调脚本
bash sft.sh

微调的核心代码为:

swift sft \
    --model Qwen/Qwen3-0.6B \
    --train_type lora \
    --dataset 'train.jsonl' \
    --torch_dtype bfloat16 \
    --num_train_epochs 10 \
    --per_device_train_batch_size 20 \
    --per_device_eval_batch_size 20 \
    --learning_rate 1e-4 \
    --lora_rank 8 \
    --lora_alpha 32 \
    --target_modules all-linear \
    --gradient_accumulation_steps 16 \
    --save_steps 1 \
    --save_total_limit 2 \
    --logging_steps 2 \
    --max_length 2048 \
    --output_dir output \
    --warmup_ratio 0.05 \
    --dataloader_num_workers 4

会自动下载 Qwen3-0.6B,我本地用的3080 12G跑的,大概在10分钟的样子。 https://s3.aixfan.com/img/screenshot/2026/03/02/1772380928_183b.png

https://s3.aixfan.com/img/screenshot/2026/03/02/1772391032_194b.png

根据脚本内容,训练完成后,会合并LoRA权重,生成合并后模型在output/v0-xxx-xxx/checkpoint-50-merged目录下。

教程内容:

在output/v0-xxx-xxx路径下有 images 文件夹,打开 train_loss.png(反映训练集损失) 与 eval_loss.png(反映验证集损失),根据损失值的变化趋势初步判断当前模型的训练效果:

  • 在结束训练前 train_loss 与 eval_loss 仍有下降趋势(欠拟合) 可以增加 num_train_epochs(训练轮次,与训练深度正相关) 参数,或适当增大 lora_rank(低秩矩阵的秩,秩越大,模型能表达更复杂的任务,但更容易过度训练)的值后再进行训练,加大模型的对训练数据的拟合程度;

  • 在结束训练前 train_loss 持续下降,eval_loss 开始变大(过拟合) 可以减少 num_train_epochs 参数,或适当减小lora_rank的值后再进行训练,防止模型过度训练;

  • 在结束训练前 train_loss 与 eval_loss 均处于平稳状态(良好拟合) 模型处于该状态时,您可以进行后续步骤。本方案的 train_loss 与 eval_loss 变化如下表所示:

验证微调后模型效果(可选)

# 进入 /root 目录
cd /root/qwen-0.6b && \
# 下载并执行验证脚本
curl -o evaluate.py "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250612/bzszyc/evaluate.py" && \
curl -o evaluate.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250612/xtgxql/evaluate.sh" && \
bash evaluate.sh

部署为 API 服务

# 下载部署脚本 deploy.sh
curl -o deploy.sh "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250613/hbojjv/deploy.sh" && \
# 后台运行部署脚本
bash deploy.sh

https://s3.aixfan.com/img/screenshot/2026/03/02/1772380998_185b.png

调用方式

经过我测试,温度设置为 0 ,TOP P设置为 1 好一点。

1、curl

  • Authorization需要输入Token
curl --location 'http://localhost:8000/v1/chat/completions' \
--header 'Authorization: Bearer TOKEN' \
--header 'Content-Type: application/json' \
--data '{
    "model": "Qwen3-0.6B-SFT",
    "messages": [
      {
        "role": "system",
        "content": "你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的JSON信息,包含的Key有province(省份)、city(城市名称)、district(区县名称)、specific_location(街道、门牌号、小区、楼栋等详细信息)、name(收件人姓名)、phone(联系电话)、postal_code(邮编)、remark(备注)。必须遵守的规则:1、如果用户输入的文本没有明确 province/city/district/name,则对应字段输出空字符串,不准编造。2、remark不允许出现 province/city/district/name/specific_location 字段内的内容"
      },
      {
        "role": "user",
        "content": "西安,长安区,郭杜街道,樱花一路,茅坡新城,6号楼2单元 301室,联系人:周杰,电话:18666667777 (备用:13588889999)"
      }
    ],
    "chat_template_kwargs": {
      "enable_thinking": false
    },
    "guided_json": {
      "title": "Labels",
      "type": "object",
      "required": ["province", "city", "district", "specific_location", "name", "phone"],
      "properties": {
        "province": {
          "title": "Province",
          "type": "string"
        },
        "city": {
          "title": "City",
          "type": "string"
        },
        "district": {
          "title": "District",
          "type": "string"
        },
        "specific_location": {
          "title": "Specific Location",
          "type": "string"
        },
        "name": {
          "title": "Name",
          "type": "string"
        },
        "phone": {
          "title": "Phone",
          "type": "string"
        },
        "remark": {
          "title": "Remark",
          "type": "string"
        }
      }
    }
  }'

2、Chatbox客户端

添加模型提供方

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389387_188b.png

新会话

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389387_188b.png

https://s3.aixfan.com/img/screenshot/2026/03/02/1772389538_192b.png

总结

对于识别效果,如果总分给10分,我给8分,可能是0.6B参数量比较小,或者是训练样本的问题,对于 行政冲突、功能区映射与定位、复杂纠错与翻译,处理还是不行,而且还是建立在我重新使用新样本微调的情况下。
自带的样本微调出来只能给到6分左右。

测试地址(虚拟):

raw_text:
上海 浦东 张江高科(中区) 科苑路捌拾捌号 3#楼501 张三/前台收 13800138000(备注:别按门铃,电话可能打不通)

raw_text:
北京市海淀区 上地十街10号院(辉煌国际)2号楼-1单元 802,李四 186-0000-1234;备用:010-6299 7788(快递别放驿站)

raw_text:
广东省深圳市南山粤海街道 科技园(南区)科苑南路2666号 中国华润大厦T2 23F 2306室 收件:王五 13900001111 备注:到前台说“会议资料”

raw_text:
浙江杭州 余杭区(未来科技城)文一西路 969号 阿里巴巴西溪园区 3期 D区 5号楼 3-201 赵* 15100002222(门口保安不让进)

raw_text:
重庆 渝北 回兴(两路)金开大道 1881 号 融科金开中心 1栋 19-7 收:陈晨 133 3333 3333 备注:周末别送

raw_text:
江苏省南京市鼓楼区 中山北路 8O 号(注意是字母O不是零)德基广场二期 A座 1608,周先生 18000000000(发顺丰)

raw_text:
四川成都 高新区(其实是武侯?不确定)天府二街 138号 腾讯大厦 B座 12层 1203 张老师 177-7777-7777 备注:到了打座机 028-88886666

raw_text:
湖北武汉 江岸区 台北一路 9号 云林街坊? 4栋2单元502(原502现504) 收件人:刘女士 15200003333(别写“台北路”会丢件)

raw_text:
陕西西安 雁塔 曲江新区(功能区)芙蓉西路 99号 曲江大悦城 写字楼 1号楼 1402 杨洋 18700004444 备注:停车场入口旁

raw_text:
福建 厦门 思明区 软件园二期 观日路 18号 B10 5层(前台转) 收:林- 13600005555(上午送)

raw_text:
山东省青岛市 市北区(原四方)黑龙江南路 2号 万科中心 A座 1301,收件:孙悟空 17000006666(公司名别写,写了会被退)

raw_text:
河南郑州 郑东新区(功能区)商务外环路 5号 国际金融中心 IFC 2号楼 9-903 赵六 15900007777 备注:门牌有两个,以“2号楼”为准
样本 1:行政区划逻辑陷阱(跨省/市冲突)

   raw_text: "收件人:老王 电话:135****9988 地址:江苏省苏州市武侯区天府三街腾讯大厦A座18楼(如果有门禁请打手机,由于是旧地址,请务必送到现在的办公点)"

       测试点:武侯区实际属于四川成都市,而非江苏苏州市。看模型是盲目相信省市,还是能识别出区县冲突并修正/置空。

样本 2:功能区深度映射(无直接行政区名)

   raw_text: "西安高新区锦业路38号粤汉国际 B 座 2101 室,联系人:Jasmine,Tel:+86-029-88887777,备注:工作日派送"

       测试点:西安高新区锦业路段实际行政隶属于“雁塔区”。看模型能否通过知识库映射出“雁塔区”。

样本 3:极简混乱 + 拼音混排 + 楼层细节

   raw_text: "shanghai-pudong-beiboloo 518 hao, 3F room 302. 肖先生 13800001111 备注:别放菜鸟驿站!!"

       测试点:拼音地址识别、路名纠正(beiboloo -> 碧波路)、楼层信息精准提取。

样本 4:偏远地区 + 多重备注 + 引导词干扰

   raw_text: "收货信息如下:内蒙古 锡盟 东乌旗 乌里雅斯太镇 额尔敦路12号(原牧民服务中心旁) 乌兰其其格 139-4791-2233(备用号15047910000) 尽量下午送达,谢谢合作!"

       测试点:盟、旗、镇的多层级处理;备用号码与主号码的剥离;行政简称(锡盟 -> 锡林郭勒盟)的还原。

样本 5:英文格式 + 商业体名称 + 纠错

   raw_text: "Add: 5F, MixC Shopping Mall, No. 2888 Nanbin Road, Nanan District, CQ City. Rec: Mr. Chen, 18623456789."

       测试点:直辖市缩写(CQ -> 重庆市)、商业体(MixC -> 万象城)的识别与 specific_location 的规范化

本机微调过程(可忽略)

这里记录一下我自己使用本机训练的过程。

运行环境:Windows10(WSL2)
显卡: NVIDIA GeForce RTX 3080 12G
CUDA Version: 12.9
Driver Version: 576.88
NVIDIA-SMI 576.88 
python运行环境: uv

安装uv

curl -LsSf https://astral.sh/uv/install.sh | sh

安装完成后,可用以下命令验证安装

uv --version

创建虚拟环境

mkdir qwen-0.6b && cd qwen-0.6b
uv venv --python 3.12.7

默认会在当前目录创建 .venv 虚拟环境。

安装vllm

官方版本 根据自己的CUDA版本来。

uv pip install vllm==0.15.1 --extra-index-url https://wheels.vllm.ai/0.15.1/cu129 --extra-index-url https://download.pytorch.org/whl/cu129 --index-strategy unsafe-best-match

更多安装教程,可自行搜索(VLLM 部署的一些细节,关于CUDA对应版本的问题)

安装ms-swift

uv pip install ms-swift==3.5.0

脚本改动

将sft.sh、deploy.sh 执行的python脚本改为 uv 环境运行。如: sft.sh

#!/bin/bash

# =============================================================================
# Qwen3 0.6B 模型LoRA微调脚本
# 
# 功能:
# 1. 自动下载Qwen3-0.6B模型
# 2. 使用物流填单数据集进行LoRA微调
# 3. 保存微调后的权重
# 4. 合并LoRA权重
# =============================================================================

if CUDA_VISIBLE_DEVICES=0 \
uv run swift sft \
    --model Qwen/Qwen3-0.6B \
    --train_type lora \
    --dataset 'train.jsonl' \
    --torch_dtype bfloat16 \
    --num_train_epochs 10 \
    --per_device_train_batch_size 20 \
    --per_device_eval_batch_size 20 \
    --learning_rate 1e-4 \
    --lora_rank 8 \
    --lora_alpha 32 \
    --target_modules all-linear \
    --gradient_accumulation_steps 16 \
    --save_steps 1 \
    --save_total_limit 2 \
    --logging_steps 2 \
    --max_length 2048 \
    --output_dir output \
    --warmup_ratio 0.05 \
    --dataloader_num_workers 4; then
    
    # 检查输出目录是否存在checkpoint
    echo "检查训练结果..."
    if [ -d "output" ] && [ "$(find output -name "checkpoint-*" -type d | wc -l)" -gt 0 ]; then
        echo "✓ 找到训练checkpoint文件"
        echo ""
        echo "✓ 模型微调完成!"
        echo "输出目录: output/"
    else
        echo "✗ 未找到训练checkpoint文件"
        echo "训练可能未成功完成"
        exit 1
    fi
else
    echo ""
    echo "✗ 模型微调失败!"
    echo "请检查错误信息"
    exit 1
fi

echo "检测最新的checkpoint路径..."
LATEST_CHECKPOINT=$(find output -name "checkpoint-*" -type d | sort -V | tail -1)

if [ -z "$LATEST_CHECKPOINT" ]; then
    echo "✗ 错误: 未找到checkpoint文件"
    echo "请确保已完成模型微调,并且output目录中存在checkpoint文件"
    exit 1
else
    echo "✓ 找到checkpoint: $LATEST_CHECKPOINT"
fi

echo "开始合并LoRA权重..."

# 执行权重合并
# --ckpt_dir: checkpoint目录路径(需要根据实际情况修改)
# --merge_lora: 启用LoRA权重合并
if uv run swift export \
--ckpt_dir "$LATEST_CHECKPOINT" \
--merge_lora true; then
    echo "✓ swift export 命令执行成功"
else
    echo "✗ swift export 命令执行失败"
    exit 1
fi

# 检查合并是否成功
echo "检查合并结果..."
MERGED_DIR="${LATEST_CHECKPOINT}-merged"
if [ -d "$MERGED_DIR" ]; then
    echo "✓ 合并目录创建成功: $MERGED_DIR"
    echo ""
    echo "✓ LoRA权重合并完成!"
    echo "合并后的模型路径: $MERGED_DIR"
else
    echo "✗ 权重合并失败,未找到合并目录: $MERGED_DIR"
    echo "请检查错误信息"
    exit 1
fi

deploy.sh

此处我固定了API KEY。

#!/bin/bash

# =============================================================================
# vLLM API服务部署脚本
# 
# 功能:
# 使用vLLM部署OpenAI兼容的API服务
# 
# 可自定义参数:
# - --model: 模型路径(需要根据merge.sh的输出修改)
# - --host: 服务监听地址(0.0.0.0表示所有网络接口)
# - --port: 服务端口(默认8000)
# - --api-key: API访问密钥(每次运行时自动生成)
# - --gpu-memory-utilization: GPU显存使用率(0.8表示80%)
# - --max-model-len: 最大模型长度
# 
# 安全提示:
# 1. API密钥每次启动时自动生成
# 2. 如果在生产环境使用,建议配置防火墙规则
# =============================================================================

# 生成随机API密钥
echo "生成API密钥..."
if API_KEY="sk-$(openssl rand -hex 24)"; then
    echo "✓ API密钥生成成功"
else
    echo "✗ API密钥生成失败"
    exit 1
fi

API_KEY="sk-23faf2fd47f91300d7a6c6656128e7b53f036408195acc77"

# 自动检测最新的合并模型路径
echo "检测合并模型路径..."
MERGED_MODEL=$(find output -name "*-merged" -type d | sort -V | tail -1)

if [ -z "$MERGED_MODEL" ]; then
    echo "✗ 错误: 未找到合并后的模型"
    echo "请确保已运行merge.sh并成功合并LoRA权重"
    echo "或者手动指定模型路径"
    exit 1
else
    echo "✓ 找到合并模型: $MERGED_MODEL"
fi

echo ""
echo "✓ 所有检查通过,启动vLLM API服务..."
echo ""
echo "服务配置:"
echo "- 模型路径: $MERGED_MODEL"
echo "- 监听地址: 0.0.0.0:8000"
echo "- API密钥: $API_KEY"
echo "- 模型名称: Qwen3-0.6B-SFT"
echo ""
echo "请保存上面的API密钥,用于客户端连接!"
echo ""

# 启动vLLM API服务器
echo "正在后台启动vLLM API服务器..."

# 启动服务器并获取进程ID
uv run python -m vllm.entrypoints.openai.api_server \
  --model "$MERGED_MODEL" \
  --host 0.0.0.0 \
  --port 8000 \
  --served-model-name Qwen3-0.6B-SFT \
  --trust-remote-code \
  --gpu-memory-utilization 0.8 \
  --max-model-len 2048 \
  --api-key "$API_KEY" > vllm.log 2>&1 &

VLLM_PID=$!

# 等待服务启动
echo "等待服务启动中..."

# 循环检查服务是否启动成功
MAX_WAIT=120  # 最大等待2分钟
WAIT_TIME=0
SERVICE_READY=false

while [ $WAIT_TIME -lt $MAX_WAIT ]; do
    if kill -0 $VLLM_PID 2>/dev/null; then
        # 进程存在,检查API是否可用
        if curl -s http://localhost:8000/health > /dev/null 2>&1; then
            SERVICE_READY=true
            break
        fi
    else
        echo "✗ vLLM进程意外停止"
        break
    fi
    
    echo "等待中... ($WAIT_TIME/$MAX_WAIT 秒)"
    sleep 5
    WAIT_TIME=$((WAIT_TIME + 5))
done

# 检查服务是否正常运行
if [ "$SERVICE_READY" = true ]; then
    echo ""
    echo "✓ API服务启动成功!"
    echo "✓ 服务进程ID: $VLLM_PID"
    echo "✓ 日志文件: vllm.log"
    
    # 注意事项说明
    echo ""
    echo "重要提示:"
    echo "1. API密钥: $API_KEY"
    echo "2. 服务地址: http://0.0.0.0:8000"
    echo "3. 日志查看: tail -f vllm.log"
    echo "4. 停止服务: kill $VLLM_PID"
    echo "5. 如需外网访问,请配置防火墙规则"
    
    echo ""
    echo "服务部署完成!按 Ctrl+C 停止服务。"
    
    # 示例:保存API密钥到文件
    echo "$API_KEY" > api_key.txt
    echo "✓ API密钥已保存到 api_key.txt"
    
    # 示例:检查服务状态
    echo ""
    echo "服务状态检查:"
    if curl -s http://localhost:8000/health > /dev/null 2>&1; then
        echo "✓ API服务健康检查通过"
    else
        echo "⚠ API服务可能还在启动中,请稍后检查"
    fi
    
    # 信号处理函数
    cleanup() {
        echo ""
        echo "正在停止vLLM服务..."
        kill $VLLM_PID 2>/dev/null
        wait $VLLM_PID 2>/dev/null
        echo "✓ 服务已停止"
        exit 0
    }
    
    # 注册信号处理
    trap cleanup SIGINT SIGTERM
    
    echo ""
    echo "服务正在运行中... (使用 Ctrl+C 停止)"
    echo "============================================"
    
    # 保持脚本运行,等待用户中断
    while true; do
        if ! kill -0 $VLLM_PID 2>/dev/null; then
            echo "✗ vLLM服务意外停止,退出..."
            exit 1
        fi
        sleep 10
    done
    
else
    echo ""
    echo "✗ API服务启动失败"
    echo "请检查日志文件: vllm.log"
    exit 1
fi