v1 re-scored heuristic + fused usage; v2 col
Browse files
README.md
CHANGED
|
@@ -34,42 +34,58 @@ two self-contained **ONNX** graphs — an image tower and a text tower — that
|
|
| 34 |
|
| 35 |
| Benchmark | **v1 (open)** | v2 (commercial) | best cloud VLM |
|
| 36 |
| --- | ---: | ---: | ---: |
|
| 37 |
-
| DocLayNet |
|
| 38 |
-
| Forms | 0.80 | 1.00 | 1.00 |
|
| 39 |
-
| Tobacco | 0.
|
| 40 |
-
| OOD (unseen types) | 0.
|
| 41 |
-
| OOV (synonym wording) | 0.
|
| 42 |
|
| 43 |
Every entry is scored by the same open scorer — full ranking, plus a **generalist zero-shot baseline** and
|
| 44 |
each cloud model, on the
|
| 45 |
-
[leaderboard](https://huggingface.co/spaces/nutrientdocs/document-classification-leaderboard). v1
|
| 46 |
-
|
| 47 |
-
|
|
|
|
| 48 |
|
| 49 |
## Usage (ONNX)
|
| 50 |
|
| 51 |
```python
|
| 52 |
import numpy as np, onnxruntime as ort, json
|
| 53 |
-
from transformers import
|
| 54 |
from huggingface_hub import hf_hub_download
|
|
|
|
| 55 |
|
| 56 |
R = "nutrientdocs/document-classification-v1"
|
| 57 |
-
img_sess = ort.InferenceSession(hf_hub_download(R, "modules/omni-image/image_model.onnx"))
|
| 58 |
-
txt_sess = ort.InferenceSession(hf_hub_download(R, "modules/omni-image/text_model.onnx"))
|
| 59 |
cal = json.load(open(hf_hub_download(R, "modules/omni-image/config.json")))["calibration"]
|
| 60 |
-
proc =
|
| 61 |
-
tok = AutoTokenizer.from_pretrained(R, subfolder="modules/omni-image")
|
| 62 |
|
| 63 |
-
from PIL import Image
|
| 64 |
labels = ["invoice", "letter", "memo", "form", "scientific article", "resume"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
pix = proc(images=[Image.open("doc.png").convert("RGB")], return_tensors="np")["pixel_values"].astype(np.float16)
|
| 66 |
-
ie
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
```
|
| 74 |
|
| 75 |
## What's in this repo
|
|
|
|
| 34 |
|
| 35 |
| Benchmark | **v1 (open)** | v2 (commercial) | best cloud VLM |
|
| 36 |
| --- | ---: | ---: | ---: |
|
| 37 |
+
| DocLayNet | 0.75 | **0.97** | 0.83 |
|
| 38 |
+
| Forms | 0.80 | **1.00** | 1.00 |
|
| 39 |
+
| Tobacco | 0.61 | 0.74 | **0.85** |
|
| 40 |
+
| OOD (unseen types) | 0.86 | **0.95** | — |
|
| 41 |
+
| OOV (synonym wording) | 0.73 | **0.83** | — |
|
| 42 |
|
| 43 |
Every entry is scored by the same open scorer — full ranking, plus a **generalist zero-shot baseline** and
|
| 44 |
each cloud model, on the
|
| 45 |
+
[leaderboard](https://huggingface.co/spaces/nutrientdocs/document-classification-leaderboard). v1 is the
|
| 46 |
+
free, open-weight sibling: it trails the commercial [`v2`](https://huggingface.co/nutrientdocs/document-classification-v2)
|
| 47 |
+
and the large cloud VLMs on accuracy, but it's Apache-2.0 and downloadable. Like all embedding models it
|
| 48 |
+
trails VLMs most on Tobacco (a read-the-header task). ~**5.7 pages/s on an A40** (fused image+text).
|
| 49 |
|
| 50 |
## Usage (ONNX)
|
| 51 |
|
| 52 |
```python
|
| 53 |
import numpy as np, onnxruntime as ort, json
|
| 54 |
+
from transformers import AutoImageProcessor, AutoTokenizer
|
| 55 |
from huggingface_hub import hf_hub_download
|
| 56 |
+
from PIL import Image
|
| 57 |
|
| 58 |
R = "nutrientdocs/document-classification-v1"
|
| 59 |
+
img_sess = ort.InferenceSession(hf_hub_download(R, "modules/omni-image/image_model.onnx")) # SigLIP image tower
|
| 60 |
+
txt_sess = ort.InferenceSession(hf_hub_download(R, "modules/omni-image/text_model.onnx")) # Qwen text tower
|
| 61 |
cal = json.load(open(hf_hub_download(R, "modules/omni-image/config.json")))["calibration"]
|
| 62 |
+
proc = AutoImageProcessor.from_pretrained(R, subfolder="modules/omni-image") # SigLIP image processor
|
| 63 |
+
tok = AutoTokenizer.from_pretrained(R, subfolder="modules/omni-image") # Qwen tokenizer
|
| 64 |
|
|
|
|
| 65 |
labels = ["invoice", "letter", "memo", "form", "scientific article", "resume"]
|
| 66 |
+
calib = lambda cos: 1 / (1 + np.exp(-(cal["scale"] * cos + cal["bias"])))
|
| 67 |
+
|
| 68 |
+
def embed_text(texts, maxlen):
|
| 69 |
+
e = tok(texts, padding=True, truncation=True, max_length=maxlen, return_tensors="np")
|
| 70 |
+
return txt_sess.run(["text_emb"], {"input_ids": e["input_ids"].astype(np.int64),
|
| 71 |
+
"attention_mask": e["attention_mask"].astype(np.int64)})[0] # [.,1024] L2
|
| 72 |
+
|
| 73 |
+
lab = embed_text(labels, 64) # label embeds, once
|
| 74 |
+
|
| 75 |
+
# --- image branch: page image vs labels (image ONNX has batch=1; loop+pool for multi-page) ---
|
| 76 |
pix = proc(images=[Image.open("doc.png").convert("RGB")], return_tensors="np")["pixel_values"].astype(np.float16)
|
| 77 |
+
ie = img_sess.run(["image_emb"], {"pixel_values": pix})[0] # [1,1024] L2
|
| 78 |
+
image_probs = calib((ie @ lab.T)[0]) # [N]
|
| 79 |
+
|
| 80 |
+
# --- text branch: the page's OCR text vs labels (up to ~2048 tokens) ---
|
| 81 |
+
doc_text = open("doc.txt").read()
|
| 82 |
+
text_probs = calib((embed_text([doc_text], 2048) @ lab.T)[0]) # [N]
|
| 83 |
+
|
| 84 |
+
# --- reliability fusion: weight each branch by how DECISIVE it is (top1-top2 margin) ---
|
| 85 |
+
margin = lambda p: float(np.partition(p, -2)[-1] - np.partition(p, -2)[-2])
|
| 86 |
+
wi, wt = margin(image_probs), margin(text_probs); s = wi + wt + 1e-9
|
| 87 |
+
fused = (wi / s) * image_probs + (wt / s) * text_probs
|
| 88 |
+
print(dict(zip(labels, fused.round(3).tolist())))
|
| 89 |
```
|
| 90 |
|
| 91 |
## What's in this repo
|