vlm-receipt-extractor:Qwen3-VL-8B QLoRA(收據影像 → 結構化 JSON)

Qwen3-VL-8B-Instruct 做 QLoRA 微調的 LoRA adapter,任務是把收據照片直接轉成固定 schema 的結構化 JSON(品項、單價、數量、小計、折扣、服務費、稅金、總額)。

任務說明

輸入一張收據照片,輸出:

{
  "items": [
    {"name": "Nasi Goreng", "count": 2, "unit_price": 25000, "price": 50000}
  ],
  "subtotal": 50000,
  "discount": null,
  "service": null,
  "tax": 5000,
  "total": 55000
}

Schema 完整對齊 CORD-v2gt_parse 標註(不含店名/日期,因為原始標註沒有這兩欄);收據上沒有印的欄位一律輸出 null,金額為純數值(已去除千分位符號)。

Foundation Model 與訓練設定

  • Foundation Model:unsloth/Qwen3-VL-8B-Instruct-unsloth-bnb-4bit(Unsloth 預量化的 4-bit 版本,vision tower 維持 bf16)
  • 方法:Unsloth QLoRA,finetune_vision_layers=Truefinetune_language_layers=Truefinetune_attention_modules=Truefinetune_mlp_modules=True
  • LoRA:r=16、alpha=16、dropout=0、bias=none
  • 訓練:2 epochs、batch size 2 × gradient accumulation 4(有效 batch size 8)、lr=2e-4(linear schedule)、seed=3407
  • 資料:CORD-v2 官方 train split(800 筆),影像最長邊縮至 1280px
  • 硬體:單張 RTX 4090(24GB),實際訓練耗時 19.2 分鐘、200 steps,峰值 VRAM 10.27GB
  • 驗證:epoch 1 eval_loss=0.0086 → epoch 2 eval_loss=0.0078(持續下降,無過擬合跡象)

微調前後對照(CORD-v2 test split,100 筆,greedy decoding、相同 prompt)

指標 微調前(zero-shot) 微調後
合法 JSON 率 100.0% 100.0%
total exact match 75.0% 90.0%
欄位級 micro F1 0.744 0.930

各欄位 F1 進步最多的:items.unit_price(0.32→0.91)、discount(0.27→0.86)、service(0.57→0.92)。

微調前的 zero-shot 模型有兩個系統性問題:(1) 把收據上印尼盾金額的千分位句點(如 25.000 = 兩萬五千)誤讀成小數點,輸出成 25.0;(2) 會自行「腦補」收據上沒印的 unit_price,或把品項名稱合併/截斷。微調後這兩類錯誤大幅減少。完整對照表與案例展示見 repo 內的 results/comparison.md

使用方式

import unsloth  # 必須放在最前面:修正 transformers/bitsandbytes 對 Qwen3-VL vision tower 的一個載入 bug,即使下面完全不用 FastVisionModel 也需要
from transformers import Qwen3VLForConditionalGeneration, AutoProcessor
from peft import PeftModel
from PIL import Image
import torch

BASE = "unsloth/Qwen3-VL-8B-Instruct-unsloth-bnb-4bit"
model = Qwen3VLForConditionalGeneration.from_pretrained(BASE, device_map="cuda")  # 不要額外傳 quantization_config,checkpoint 自帶
model = PeftModel.from_pretrained(model, "betty0/vlm-receipt-extractor")
processor = AutoProcessor.from_pretrained(BASE)

image = Image.open("receipt.jpg").convert("RGB")
prompt = (
    'Extract all information from this receipt image and return it as a JSON object with exactly this structure:\n'
    '{"items": [{"name": <string>, "count": <integer or null>, "unit_price": <number or null>, "price": <number or null>}], '
    '"subtotal": <number or null>, "discount": <number or null>, "service": <number or null>, "tax": <number or null>, "total": <number or null>}\n'
    'Rules: amounts must be plain numbers without currency symbols or thousands separators; use null for values not present on the receipt; '
    'output only the JSON object with no explanation and no markdown.'
)
messages = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": prompt}]}]
text = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=[text], images=[image], return_tensors="pt").to("cuda")
with torch.inference_mode():
    out = model.generate(**inputs, max_new_tokens=768, do_sample=False)
print(processor.batch_decode(out[:, inputs["input_ids"].shape[1]:], skip_special_tokens=True)[0])

資料集出處與授權

訓練與評估資料為 naver-clova-ix/cord-v2CORD: A Consolidated Receipt Dataset for Post-OCR Parsing,Park et al., NeurIPS 2019 Workshop),授權為 CC-BY-4.0。收據皆來自印尼零售/餐飲場景,語言以印尼文為主、金額為印尼盾。

已知限制

  • 訓練與評估資料集中在印尼零售/餐飲收據,換成其他國家/語言/版面的收據時準確率預期會下降。
  • Schema 不含店名與日期,因為 CORD-v2 的 gt_parse 標註本身沒有這兩欄。
  • 訓練集僅 800 筆、2 epochs,未涵蓋所有收據版面變化(例如手寫收據、極長品項清單)。
  • discountservice 在資料中出現率較低(訓練集分別 7.6%/12.2%),對應的 F1 雖有大幅提升但樣本數仍偏少,實際部署時建議針對目標場景另外驗證。

LoRA 設定細節

r=16lora_alpha=16lora_dropout=0bias=nonetarget_modules 是 Unsloth 產生的正規表示式,涵蓋 vision tower 與language model 兩側的 attention(q/k/v/o_proj)與 MLP(gate/up/down_proj、linear_fc1/fc2)投影層,完整內容見本 repo 的 adapter_config.json

Downloads last month
32
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for betty0/vlm-receipt-extractor

Dataset used to train betty0/vlm-receipt-extractor