# expense-extractor **Repository Path**: yaohx/expense-extractor ## Basic Information - **Project Name**: expense-extractor - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-18 - **Last Updated**: 2026-07-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 订单消费识别模型与服务(expense-extractor) 把**订单 / 消费截图 OCR 文本**自动抽取为**结构化 JSON**(`date / time / amount / merchant / category / payment / note`)的一整套方案: > 截图 OCR → 训练数据 → LoRA 微调(Qwen2-0.5B)→ 合并模型 → GGUF → llama.cpp 推理 → FastAPI 服务 → Docker 镜像(CPU / GPU 双版本) --- ## 一、整体流程 ``` 订单截图 │ ├─[data/ocr.py]──────────────────► OCR 文本(rapidocr-onnxruntime,本地离线) │ ├─[data/build_training_data.py]──► training_data.xlsx(双 sheet:训练数据 / 字段解析) │ ├─[data/prepare_dataset.py]──────► train_data.jsonl(清洗 + 统一字段 + 去掉 commodity) │ │ │ └─(首次仅 43 条真实样本,需扩写) │ ├─[data/augment_dataset.py]──────► 扩写到 2643 条(字段级增强,保留 43 条真实 + 2600 条合成) │ ▼ [train/train.py]───────────────────► LoRA 微调(r=16, bf16, 4 epoch)→ 合并模型 qwen2-0.5b-expense/ [train/export_onnx.py]────────────► ONNX(端侧 / 鸿蒙备选路线) │ ├─[convert_hf_to_gguf.py]────────► GGUF:f16 / q8_0 / q4_k_m ▼ [service/app.py]──────────────────► FastAPI 网关,转发到 llama-server(:8080) │ ├─[Dockerfile + compose]────────► CPU 版镜像(端口 8000) └─[Dockerfile.gpu + compose.gpu]► GPU 版镜像(端口 8001,CUDA 12.8 / sm_120) ``` --- ## 二、目录结构 ``` expense-extractor/ ├── data/ # 准备训练集 │ ├── ocr.py # 批量离线 OCR(rapidocr) │ ├── build_training_data.py# OCR 文本 → training_data.xlsx(字段解析) │ ├── prepare_dataset.py # xlsx → 清洗后 train_data.jsonl │ ├── augment_dataset.py # 字段级增强,扩写到 2643 条 │ └── train_data.jsonl # 最终训练集(已入库,2643 条) ├── train/ # 训练与导出 │ ├── train.py # LoRA 微调 + 合并模型 │ └── export_onnx.py # 导出 ONNX(鸿蒙备选) ├── model/ # 模型产物说明(权重不入库) │ └── harmonyos_deploy.md # 鸿蒙端侧部署说明 ├── service/ # 推理服务 + Docker │ ├── app.py # FastAPI 网关 │ ├── start.sh / start.gpu.sh │ ├── Dockerfile / Dockerfile.gpu │ ├── docker-compose.yml / docker-compose.gpu.yml │ ├── requirements.txt / .dockerignore │ ├── API.md # API 调用说明 │ ├── README.md # 服务说明 │ ├── monitor_gpu_build.sh # GPU 镜像构建监控(失败/成功推 MeoW) │ ├── notify_meow.py # MeoW 推送工具 │ └── req_sample.json / req_batch.json ├── examples/ │ └── training_data.clean.xlsx # 清洗后样例数据 └── README.md # 本文件 ``` > ⚠️ **路径说明**:脚本中的 `BASE / DATA / OUT_*` 等路径当前硬编码为作者本机 > `D:\yaoho\DeskTop\112233\...`。克隆到别处后请改成你自己的绝对路径或改为命令行参数。 --- ## 三、环境准备 - Python 3.11+(本项目在 3.13 venv 验证) - 关键依赖:`rapidocr-onnxruntime`、`openpyxl`、`pandas`、`transformers`、`peft`、`torch`、`datasets`、`optimum` - 基座模型:`Qwen2-0.5B-Instruct`(从 ModelScope 下载到 `model/base/`) - 推理引擎:`llama.cpp`(llama-server) --- ## 四、阶段一:准备训练集 | 步骤 | 脚本 | 作用 | 输出 | |---|---|---|---| | 1 | `data/ocr.py` | 批量 OCR 识别截图 | `ocr_raw.json` | | 2 | `data/build_training_data.py` | 解析为「训练数据 / 字段解析」双 sheet | `training_data.xlsx` | | 3 | `data/prepare_dataset.py` | 清洗 JSON、统一字段、剔除 commodity | `train_data.jsonl` + `training_data.clean.xlsx` | | 4 | `data/augment_dataset.py` | 字段级增强扩写 | 2643 条 xlsx + jsonl | **输出 JSON 字段**(顺序固定): ```json {"date":"2026-07-06","time":"11:47","amount":168.0,"merchant":"海底捞火锅","category":"餐饮","payment":"招商银行信用卡","note":"聚餐"} ``` `amount` 为数字(无法识别则为 `""`);`build_training_data.py` 内置 6 类单据解析器: 微信支付账单 / 支付宝账单 / 美团订单 / 美团团购券 / 联通交费 / 微信支付成功页。 **扩写策略**(`augment_dataset.py`):保留 43 条真实样本 + 基于真实 OCR 排版做字段替换 (商家名按分类、支付方式、收单机构、备注),分布:餐饮 800 / 交通 400 / 购物 400 / 文娱 350 / 居住 350 / 通讯 300 = 2600 条合成,合计 **2643 条**。 --- ## 五、阶段二:训练 `train/train.py` 关键超参: | 参数 | 值 | |---|---| | 基座 | Qwen2-0.5B-Instruct | | 方法 | LoRA(CAUSAL_LM) | | `r` / `lora_alpha` | 16 / 32 | | `target_modules` | q,k,v,o_proj + gate,up,down_proj | | `lora_dropout` | 0.05 | | `per_device_train_batch_size` | 8 | | `gradient_accumulation_steps` | 2 | | `learning_rate` | 1e-4 | | `num_train_epochs` | 4 | | 精度 | bf16(RTX 5060 Ti) | | `lr_scheduler` | cosine | | `max_length` | 1024 | 训练后产出:LoRA adapter(`qwen2-0.5b-expense-lora/`)+ 合并模型(`qwen2-0.5b-expense/`)。 导出 ONNX(鸿蒙备选):`train/export_onnx.py`(`optimum.onnxruntime`,含 past_key_values)。 --- ## 六、阶段三:GGUF 转换 用 `llama.cpp` 的 `convert_hf_to_gguf.py` 把合并模型转 GGUF: | 文件 | 大小 | 用途 | |---|---|---| | `qwen2-0.5b-expense-f16.gguf` | 988 MB | 全精度源 | | `qwen2-0.5b-expense-q8_0.gguf` | 525 MB | 高精度(GPU 版默认) | | `qwen2-0.5b-expense-q4_k_m.gguf` | 397 MB | **推荐端侧版** | > GGUF 权重体积大,**不入库**。自行转换即可,或参见 `model/harmonyos_deploy.md`。 --- ## 七、阶段四:构建服务 `service/app.py` 是 FastAPI 网关:接收 `{ocr_text}`,拼 Qwen2 ChatML prompt,转发给 同容器内的 `llama-server`(:8080),返回结构化 JSON。 接口(详细见 `service/API.md`): | 方法 | 路径 | 说明 | |---|---|---| | GET | `/health` | 健康检查 | | POST | `/extract` | 单条抽取 | | POST | `/extract/batch` | 批量抽取 | --- ## 八、阶段五:打包 Docker 镜像 | 版本 | 文件 | 端口 | 说明 | |---|---|---|---| | CPU | `Dockerfile` + `docker-compose.yml` | 8000 | `python:3.11-slim` + 预编译 CPU 版 llama-server | | GPU | `Dockerfile.gpu` + `docker-compose.gpu.yml` | 8001 | 多阶段:CUDA 12.8 源码编译 llama.cpp(sm_120 / Blackwell),`-ngl 99` 全量 GPU 卸载 | **CPU 版运行:** ```bash cd service docker compose up -d # 镜像 expense-extractor:latest curl http://127.0.0.1:8000/health ``` **GPU 版运行:** ```bash cd service # 把 q8_0.gguf 放到 service/model/ 后 docker compose -f docker-compose.gpu.yml up -d # 镜像 expense-extractor-gpu:latest curl http://127.0.0.1:8001/health ``` GPU 版构建采用**官方 `GGML_CUDA_NO_VMM=ON`** 规避 CUDA driver 链接问题(详见 `service/README.md` 排错章节)。 --- ## 九、文档 - `service/API.md` —— REST API 调用说明(字段表、curl/Python 示例、错误码) - `service/README.md` —— 服务与镜像说明、环境变量、排错 - `model/harmonyos_deploy.md` —— 鸿蒙端侧部署(GGUF+llama.cpp / ONNX+.ms 两条路线) --- ## 十、关键踩坑记录(可复用) 1. **accelerate Windows 32767 字符 bug**:环境变量单值过长导致崩溃 → 在 `train.py` / `export_onnx.py` 开头截断超长环境变量。 2. **llama.cpp CUDA 链接失败**(`undefined reference to cuMem*`):CUDA devel 镜像里 `libcuda` 只有 stubs,且 Ubuntu 22.04 链接器默认 `--as-needed` 会丢依赖 → 用官方 `GGML_CUDA_NO_VMM=ON` 彻底规避(禁用 VMM 后不再引用 `cu*` 符号,对 0.5B 小模型无影响)。 3. **Docker 容器启动崩溃**(`cannot open libllama-server-impl.so`):瘦二进制运行时要 动态加载同目录 `.so` → `COPY --from=builder /build/build/bin/ ./llama_bin/` 整目录拷贝。 4. **Docker 构建期直连 GitHub 中断**:改用本机经 GHProxy 镜像下载源码后 `COPY` 进上下文。 5. **MeoW 推送中文 500**:服务端 JSON 解析不支持直发 UTF-8 中文 → 发送时转 `\u` 转义 (`notify_meow.py` 已处理);emoji 也会触发 500 → 过滤。 --- ## 十一、已知限制 - 2643 条中 2600 条为合成增强(基于 43 条真实样本),**真实世界新排版单据泛化会打折**。 上线前建议补几十~上百条真实新单据再训一轮。 - 0.5B 模型仅适合「固定字段抽取」窄任务,不做复杂推理。 --- ## 十二、Git 仓库 - 地址:`https://git.yaohx.cn:8001/yaohx/expense-extractor` - 分支:`main` - 注:权重二进制(GGUF / HF / ONNX)与 `llama.cpp-src`、`llama_bin` 等不入库(见 `.gitignore`)。