1. 项目概述
最近在探索如何利用LlamaFactory工具对多模态大模型进行微调,目标是构建一个能够精准识别发票信息的轻量化模型。这个项目源于实际业务需求——我们需要在边缘设备上部署一个高效的发票识别系统,但直接使用Qwen2.5-VL-72B这样的大模型既不现实也不经济。
经过多次尝试,我总结出了一套完整的解决方案:先用大模型生成高质量训练数据,再用这些数据微调小模型。这种方法不仅显著降低了硬件需求,还保持了相当不错的识别准确率。下面我将详细记录整个流程,包括数据集制作、模型微调、测试验证等关键环节。
2. 环境准备与工具安装
2.1 数据集制作工具部署
数据集制作工具分为前端和后端两个部分,需要分别部署:
bash复制# 后端环境准备
cd data_create/backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
bash start.sh
前端部分需要Node.js环境:
bash复制# 安装Node.js 18 LTS
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -
apt-get install -y nodejs
# 启动前端
cd data_create
npm install
npm run dev
注意:如果遇到vite配置问题,需要修改vite.config.ts文件,添加必要的alias配置。我在实际操作中发现,直接使用项目提供的默认配置可能会导致某些依赖解析失败。
2.2 多模态大模型部署
数据集制作过程中需要调用大模型进行OCR识别,我使用的是vLLM部署的Qwen2-5-VL-72B-Instruct-quantized-w8a8模型:
bash复制CUDA_VISIBLE_DEVICES=0-7 python -m vllm.entrypoints.openai.api_server \
--served-model-name Qwen2-5-VL-72B-Instruct-quantized-w8a8 \
--model /path/to/model \
--host 0.0.0.0 \
--port 9024 \
--tensor-parallel-size 8 \
--gpu-memory-utilization=0.85 \
--trust-remote-code
部署完成后,可以通过简单的curl命令测试模型是否正常工作:
bash复制curl -X POST http://192.168.8.221:9024/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen2-5-VL-72B-Instruct-quantized-w8a8",
"messages": [
{"role": "system", "content": "你是一个AI智能助手"},
{"role": "user", "content": "请用中文介绍西湖醋鱼的做法"}
],
"stream": false
}'
3. 数据集制作流程
3.1 数据集制作工具配置
为了让数据集制作工具能够正确调用我们部署的大模型,需要修改后端代码中的模型配置:
python复制# 修改data_create/backend/app.py
MODEL_ENDPOINT = "http://192.168.8.221:9024/v1/chat/completions"
MODEL_NAME = "Qwen2-5-VL-72B-Instruct-quantized-w8a8"
同时,我们需要设计合适的提示词(prompt)来确保模型输出格式统一:
text复制你是一个文本分析助手。请从图像中提取发票信息,并以JSON对象形式返回,只包含以下键值对字段:
{
"发票名称": "",
"购买方": {
"名称": "",
"纳税人识别号": "",
"地址、电话": "",
"开户行及账号": ""
},
"销售方": {
"名称": "",
"纳税人识别号": "",
"地址、电话": "",
"开户行及账号": ""
},
"发票代码": "",
"发票号码": "",
"开票日期": "",
"校验码": "",
"金额": "",
"税额": "",
"价税合计(大写)": ""
"价税合计(小写)": ""
}
3.2 批量处理脚本开发
为了提高效率,我开发了一个Python脚本来自动化处理大量图片:
python复制import os
import json
import base64
import httpx
from pathlib import Path
from datetime import datetime
async def process_image(image_path: Path):
img_b64 = image_to_base64(image_path)
response = await call_vl_api(img_b64)
training_data = {
"messages": [
{"role": "user", "content": "从下图发票图像中抽取键值对并按JSON格式返回..."},
{"role": "assistant", "content": response}
],
"images": [str(image_path)] # 使用绝对路径
}
return training_data
async def main():
image_dir = Path("/path/to/images")
images = list(image_dir.glob("*.jpg")) + list(image_dir.glob("*.png"))
results = []
for img_path in images:
try:
data = await process_image(img_path)
results.append(data)
except Exception as e:
print(f"处理失败: {img_path.name}, 错误: {e}")
# 保存为JSONL格式
with open("invoice_train.jsonl", "w") as f:
for item in results:
f.write(json.dumps(item, ensure_ascii=False) + "\n")
关键点:确保images字段使用绝对路径,否则后续训练时可能找不到图片文件。
4. 模型微调实战
4.1 LlamaFactory环境准备
首先下载并安装LlamaFactory:
bash复制git clone https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
pip install -r requirements.txt
然后下载要微调的模型(以Qwen2.5-VL-7B为例):
bash复制modelscope download --model Qwen/Qwen2.5-VL-7B-Instruct --local_dir ./
4.2 数据集准备
将生成的jsonl文件放在data目录下,并在dataset_info.json中注册:
json复制"gqr_invoice_train_elec": {
"file_name": "gqr_invoice_train_elec.jsonl",
"formatting": "sharegpt",
"columns": {
"messages": "messages",
"images": "images"
}
}
4.3 启动WebUI进行微调
bash复制llamafactory-cli webui
在Web界面中需要进行以下关键配置:
- 选择"qwen2_vl"作为对话模板
- 设置学习率为2e-5
- 使用LoRA进行高效微调
- 启用gradient checkpointing以节省显存
4.4 微调参数建议
基于我的实践经验,推荐以下参数组合:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 学习率 | 2e-5 | 太大容易震荡,太小收敛慢 |
| Batch Size | 8 | 根据显存调整 |
| Epochs | 3 | 通常2-3个epoch足够 |
| LoRA Rank | 64 | 平衡效果和效率 |
| Max Length | 4096 | 根据输入长度调整 |
5. 模型测试与部署
5.1 微调前后效果对比
测试同一张发票图片,观察模型输出的差异:
微调前输出:
json复制{
"发票名称": "增值税专用发票",
"购买方": {
"名称": "某某科技有限公司",
"纳税人识别号": "91310101MA1FPX1234"
}
// 其他字段缺失或不全
}
微调后输出:
json复制{
"发票名称": "增值税专用发票",
"购买方": {
"名称": "某某科技有限公司",
"纳税人识别号": "91310101MA1FPX1234",
"地址、电话": "上海市浦东新区张江路123号 021-12345678",
"开户行及账号": "中国银行上海分行 1234567890123456"
},
"销售方": {
"名称": "某某商贸有限公司",
"纳税人识别号": "91310101MA1FPY5678",
"地址、电话": "北京市海淀区中关村大街1号 010-87654321",
"开户行及账号": "工商银行北京分行 9876543210987654"
},
"发票代码": "123456789012",
"发票号码": "12345678",
"开票日期": "2024年3月15日",
"校验码": "1234 5678 9012",
"价税合计(大写)": "壹万贰仟叁佰肆拾伍元整",
"价税合计(小写)": "¥12,345.00"
}
5.2 模型导出与部署
训练完成后,可以通过LlamaFactory导出完整模型:
bash复制python src/export_model.py \
--model_name_or_path path/to/pretrained \
--adapter_name_or_path path/to/lora \
--output_dir path/to/merged_model
导出的模型可以直接用于推理:
python复制from transformers import AutoModelForCausalLM, AutoTokenizer
model = AutoModelForCausalLM.from_pretrained("path/to/merged_model")
tokenizer = AutoTokenizer.from_pretrained("path/to/merged_model")
# 准备输入
inputs = tokenizer("从下图发票中提取信息...", return_tensors="pt")
image = Image.open("invoice.jpg")
# 进行推理
outputs = model.generate(**inputs, images=[image])
print(tokenizer.decode(outputs[0]))
6. 经验总结与避坑指南
6.1 关键经验
-
数据质量决定上限:大模型生成的数据并非完美,需要人工抽查验证。我发现约5%的样本需要手动修正。
-
路径问题:确保jsonl文件中的图片路径是绝对路径,否则训练时会报错找不到文件。
-
显存优化:对于7B模型,使用gradient checkpointing和LoRA可以将显存需求从24GB降到12GB左右。
-
提示词设计:明确的输出格式要求能显著提高数据一致性。我迭代了7版提示词才达到理想效果。
6.2 常见问题排查
问题1:训练时出现CUDA out of memory错误
- 解决方案:减小batch size,启用gradient checkpointing,使用LoRA等参数高效方法
问题2:模型输出格式不符合预期
- 解决方案:检查提示词是否足够明确,增加格式要求的权重
问题3:前端工具无法启动
- 解决方案:确保Node.js版本≥18,检查vite配置中的alias设置
问题4:微调后模型效果不升反降
- 解决方案:检查数据质量,适当降低学习率,增加warmup步骤
7. 性能优化建议
-
量化部署:使用AWQ或GPTQ对模型进行4bit量化,可将7B模型的显存需求降到6GB左右。
-
TensorRT加速:转换模型为TensorRT引擎,在NVIDIA显卡上可获得2-3倍的推理速度提升。
-
批处理优化:对于大量发票处理场景,合理设置batch size可以显著提高吞吐量。
-
缓存机制:对相同版式的发票,可以缓存部分中间结果以减少重复计算。
这个项目从开始到最终部署用了约两周时间,最大的收获是验证了"大模型生成数据+小模型微调"这一技术路线的可行性。相比直接使用大模型,我们的方案将推理速度提高了5倍,硬件成本降低了80%,而准确率只下降了不到3%,完全达到了业务需求。
