Nôm — Lựa chọn component & Benchmark
Tài liệu này ghi lại các component Nôm phụ thuộc, vì sao chọn từng cái, và số benchmark khi đã đo. Số tái lập được — mọi claim "đã đo" đều có script trong scripts/ chạy lại được.
Cập nhật lần cuối: 2026-04-25.
TL;DR — stack khuyến nghị
| Module | Component | Trạng thái | Lý do |
|---|---|---|---|
nom.text | Pure stdlib (unicodedata) | đã ship v0.0.1 | 9M ops/s, zero deps, tất định |
nom.doc.ocr (primary) | VietOCR (Transformer, VN-specialized) | dự kiến v0.1 | Diacritic-aware; build trên corpus VN |
nom.doc.ocr (fallback) | Tesseract 5 với traineddata vie | dự kiến v0.1 | Luôn có, baseline ~70% accuracy |
nom.doc.ocr (cloud) | PaddleOCR PP-OCRv5 | dự kiến v0.1 | 94.5% trên OmniDocBench, pipeline modular |
nom.doc.pdf | PyMuPDF (fitz) | dự kiến v0.1 | Nhanh hơn pdfplumber 19× trên PDF thực |
nom.llm (local mặc định) | Qwen3-8B qua Ollama | dự kiến v0.1 | Apache 2.0, chạy trên GPU consumer, VN mạnh |
nom.llm (cloud max) | Qwen3-235B-A22B / GPT-4o / Claude | dự kiến v0.1 | Top-tier khi ngân sách cho phép |
nom.llm (vision+doc) | Qwen2.5-VL-72B-Instruct | dự kiến v0.1 | Best open vision-language cho extraction tài liệu có cấu trúc |
Nguồn cho mỗi claim được liệt kê dưới mỗi section module.
Module: nom.text — đã ship
Làm gì
Tiện ích pure-Python cho text tiếng Việt:
normalize(s)— Unicode NFC normalizationstrip_diacritics(s)— chuyển sang ASCII (đ → d, é → e, ...)has_diacritics(s)— booleanis_vietnamese(s)— phát hiện heuristic (chạy được cả trên text đã strip dấu)fix_diacritics(s)— khôi phục dấu trên các từ đã strip phổ biến
Test
22/22 pass (pytest tests/).
Độ chính xác — đo 2026-04-25
Corpus: benchmarks/data/diacritic_eval_v0.txt — 55 câu VN hand-curated trên 4 register (15 hợp đồng, 12 official, 15 hội thoại, 13 tin tức), license CC0.
| Metric | baseline v0.0.1 |
|---|---|
| Câu | 55 |
| Từ | 776 |
| Từ chứa dấu | 666 |
| Word accuracy tổng | 40.59% |
| Diacritic recall tổng | 34.08% |
Theo register:
| Register | Word accuracy | Diacritic recall |
|---|---|---|
| hợp đồng / kinh doanh | 50.00% | 44.32% |
| tài liệu official | 39.33% | 29.33% |
| hội thoại | 44.15% | 39.37% |
| tin tức / long-form | 29.13% | 23.33% |
Đây là baseline v0.0.1 trung thực với bảng từ vựng curated ~120 entry hiện tại. Đường rule-based là stopgap zero-dependency. Roadmap thay thế nó, không mở rộng:
| Phiên bản | Cách tiếp cận | Dependency | Độ chính xác đo |
|---|---|---|---|
| v0.0.1 (mặc định hiện tại) | Lookup bảng rule-based | không | 41.06% |
| v0.2.7 cloud | LLM-backed (fix_diacritics(..., llm=...)) | bất kỳ nom.llm.LLM | 95.37% với OpenAI(gpt-4o-mini) |
| v0.2.7 local | LLM-backed qua Ollama | nom-vn[llm] + ollama pull gemma3:4b | 87.90% với Ollama("gemma3:4b") |
| v0.2.7 local-max | LLM-backed qua Ollama | nom-vn[llm] + ollama pull gemma4:e4b | 93.18% với Ollama("gemma4:e4b") |
| v0.0.2 | Wrap mô hình PyVi hoặc DistilBERT | optional nom-vn[diacritics] | tạm hoãn — vấn đề license/format |
Lựa chọn backend v0.0.2 đang được đánh giá
| Tuỳ chọn | Nguồn | Cách tiếp cận | Acc công bố | License |
|---|---|---|---|---|
PyVi ViUtils.add_accents() | trungtv/pyvi | wrapper mô hình đã train | mature, ~80%+ | MIT |
| DistilBERT-Viet-Diacritic | HF: saeliddp/... | DistilBERT token classification | ~90%+ | Apache 2.0 |
| restore_vietnamese_diacritics | duongntbk | Transformer seq2seq | 94.05% | MIT |
| vietai/aivivn-vn-diacritic | vietai | Transformer seq2seq | — | Apache 2.0 |
Lựa chọn dựa trên: trọng lượng dependency, tốc độ inference CPU-only, tương thích license, và phép đo của chính chúng tôi trên diacritic_eval_v0.txt. Chúng tôi không công bố số dự đoán — bài release v0.0.2 sẽ kèm số đo trên cùng corpus.
Tái lập: python benchmarks/accuracy/bench_diacritics.py Baseline track tại: benchmarks/results/baseline_v0.0.1.json
Lưới backend / hardware diacritic — đo 2026-04-26
Cùng weights Toshiiiii1 T5, ba đường execution. Mọi cái đều đạt cùng 97.81 % word accuracy — export đúng, thứ duy nhất khác là latency.
| Backend | Hardware | Word acc | Mean ms | p50 ms | Ghi chú |
|---|---|---|---|---|---|
| PyTorch | RTX 3090 (CUDA) | 97.81 % | 152 | 148 | Target production cho người dùng có GPU |
| PyTorch | CPU (8 cores) | 97.81 % | 377 | 357 | Chấp nhận được cho job batch / overnight |
| ONNX Runtime | CPU (8 cores) | 97.81 % | 410 | 394 | Hơi chậm hơn PyTorch CPU |
ONNX runtime không thêm giá trị ở đây. PyTorch hiện đại với MKL-DNN đã optimal cho T5 200 M ở eager mode. ONNX đáng revisit chỉ với INT8 quantization (typical speedup 2-3× CPU với một số chi phí accuracy) — chưa đo ở đây; để theo dõi tiếp.
Chúng tôi không ship bước export ONNX trong nom-vn[diacritic-hf]. User thực sự cần ONNX (deploy cross-platform không có stack Python+PyTorch) tự optimum-cli export onnx ...; export tất định.
Mô hình seq2seq diacritic VN có sẵn — đo 2026-05-02
Bài học rút ra: chúng tôi từng bỏ qua bước đo các mô hình khôi phục dấu tiếng Việt giấy phép Apache công khai trước khi đề xuất chưng cất một mô hình 100M tham số riêng. Người dùng đã chỉ ra; đo lại. Một mô hình có sẵn vượt mọi lựa chọn từng thử, kể cả gpt-4o-mini trên cloud.
Cùng ngữ liệu 55 câu (CC0). Bộ đo: benchmarks/accuracy/bench_diacritic_hf.py. Phần cứng: RTX 3090.
| Mô hình | Giấy phép | Disk | Độ chính xác từ | Trung bình giây/câu | Ghi chú |
|---|---|---|---|---|---|
Toshiiiii1/Vietnamese_diacritics_restoration_5th ⭐ | Apache 2.0 | ~1 GB | 97,81 % | 0,152 | T5 200 M, safetensors |
(cloud gpt-5.4-mini) | proprietary | — | 96,12 % / 92,06 % / 78,68 % | 1,13 | 0,75 USD vào / 4,50 USD ra mỗi 1M token; thắng gpt-4o-mini ở văn bản hành chính/hội thoại nhưng kém ở văn học |
(cloud gpt-4o-mini) | proprietary | — | 95,37 % / 89,51 % / 81,84 % | 1,27 | 0,15 USD vào / 0,60 USD ra mỗi 1M token; điểm cân bằng chi phí–chất lượng của OpenAI |
(cloud gpt-5.4-nano) | proprietary | — | 93,82 % / 85,15 % / 73,18 % | 1,27 | 0,20 USD vào / 1,25 USD ra mỗi 1M token; mới hơn nhưng kém gpt-4o-mini ở mọi loại văn bản; bỏ qua |
(cloud claude-haiku-4-5) | proprietary | — | 96,19 % / 90,49 % / 81,68 % | 1,46 | 1,00 USD vào / 5,00 USD ra mỗi 1M token; nhỉnh hơn gpt-4o-mini ở hành chính / hội thoại nhưng đắt hơn ~6 lần |
local gemma4:e4b Q4 | Apache 2.0 | 9,6 GB | 93,18 % / 87,91 % / 77,78 % | 0,88 | LLM local mạnh nhất; cần ≥10 GB VRAM |
local gemma3:4b Q4 | Apache 2.0 | 3,3 GB | 89,06 % / 79,70 % / 62,05 % | 0,91 | -3 đến -16 pp so với gemma4:e4b; cho máy yếu hơn |
local qwen3:1.7b Q4 | Apache 2.0 | 1,4 GB | 16,60 % | 0,43 | Dưới sàn quy tắc 41 %; bỏ qua |
bmd1905/vietnamese-correction | Apache 2.0 | ~1,6 GB | 15,57 % | 0,301 | Thất bại — huấn luyện cho sửa chính tả, không cho khôi phục dấu thuần |
qthuan2604/BARTPho_Syllable_Restore_Diacritics_Vietnamese | MIT | ~1,6 GB | Chưa đo | — | Tác giả tự báo CER 38,85 %, dưới sàn quy tắc; bỏ qua |
| (sàn quy tắc) | — | 0 | 41,06 % | <0,001 | Mức sàn tham chiếu |
Ô của các mô hình cloud / LLM ghi 3 số theo thứ tự kinh doanh 55 + UDHR 72 → hội thoại Tatoeba 300 → văn học UD-VTB 800. Đo 2026-05-02 trên RTX 3090 (LLM local) hoặc qua API (cloud). Bảng đầy đủ theo từng loại văn bản nằm trong docs/tasks/diacritic-restoration.md.
Toshiiiii1 thắng dứt khoát:
- +2,44 pp so với
gpt-4o-minitrên cloud (97,81 % vs 95,37 %). - +9,91 pp so với LLM local mạnh nhất (
gemma3:4b87,90 %). - Nhanh gấp 8 lần so với LLM cloud (0,152 s vs 1,27 s) và gấp 7 lần so với LLM local.
- Apache 2.0 và định dạng safetensors — phát hành được trọn vẹn theo nguyên tắc không dùng pickle.
- Nhỏ hơn ~10 lần trên disk so với
gemma4:e4b(1 GB vs 9,6 GB).
Hành động: rút khuyến nghị "distil mô hình diacritic VN 100 M" trong docs/training_plan_2026q2.md (v0.2.12) — không có gì để distil tới mà mô hình Apache công khai chưa cover. Thêm như đường production khuyến nghị:
from nom.text import fix_diacritics
from nom.text.diacritic_models import HFDiacriticModel
restorer = HFDiacriticModel() # mặc định Toshiiiii1, lazy-load lần gọi đầu
out = fix_diacritics("Hop dong nay duoc lap ngay 14 thang 3", model=restorer)
# → 'Hợp đồng này được lập ngày 14 tháng 3'Cài: pip install "nom-vn[diacritic-hf]" (kéo transformers<5 + torch + sentencepiece). Cap transformers bắt buộc: ≥5.6 có regression slow-T5-tokenizer làm vỡ load mô hình Toshiiiii1.
Vì sao chúng tôi miss ban đầu: đợt v0.2.7 → v0.2.10 tập trung vào khôi phục dấu LLM-backed (giả định trước là "mọi mô hình diacritic VN tốt đều ship pickle hoặc license NC"). Bảng "v0.0.2 backend options under evaluation" trong benchmark.md từ thời v0.0.1 vẫn liệt kê các ứng viên Apache là "tạm hoãn — vấn đề license/format" mà không có phép đo thực. Audit 2026-04-26 tìm được một cái thoả mọi ràng buộc.
Cross-check: model card Toshiiiii1 không báo metric, nên ta không có số phía trên để so. 97.81 % của ta trên corpus 55 câu.
Đo nhiều corpus — ma trận 4 register. Đo lần đầu 2026-04-26 (business + literary), mở rộng 2026-04-29 (conversational + formal/legal):
| Eval corpus | Câu | Register | Word acc | Mean ms/câu |
|---|---|---|---|---|
udhr_vi/diacritic_eval_udhr.txt | 72 | hành chính / pháp lý (UDHR) | 98.14 % | 221 |
diacritic_eval_v0.txt | 55 | kinh doanh / hợp đồng / tin tức | 97.81 % | 152 |
tatoeba_vi/diacritic_eval_300.txt | 300 | hội thoại (Tatoeba) | 93.94 % | 82 |
ud_vi_vtb/test.conllu | 800 | văn học cổ điển (treebank VTB) | 89.40 % | 269 |
Spread = 8.74 pp (98.14 − 89.40). Drop là monotonic từ formal sang văn học, đó là cách register-shift nhìn trong thực tế — không phải mode fail đơn lẻ mà là gradient. Hội thoại nằm ~4 pp dưới business, văn học thêm ~4 pp dưới hội thoại. Mô hình register-overfit về tiếng Việt formal/business hiện đại, như mong đợi từ dữ liệu training; vẫn dùng được mọi nơi nhưng peak tuyệt đối ở các register đã train trên đó.
Một bug methodology bench đã bắt và fix. Run UD-VTB đầu báo word accuracy 54.14 % và 0/800 sentence-exact. 0/800 là dấu hiệu — kể cả mô hình tầm tầm cũng land vài câu. Vấn đề: treebank UD ship câu ở dạng tokenized (nhỉ ? " . với khoảng trắng quanh mọi dấu câu, quy ước parsing tool đòi), trong khi mô hình seq2seq output tiếng Việt tự nhiên (nhỉ?".). So sánh list token .split() raw giữa hai cái dịch alignment ở dấu câu đầu và token phía sau so wrong-vs-wrong.
Chúng tôi thêm bước normalize_punct() trong benchmarks/accuracy/bench_diacritic_hf_udvtb.py strip khoảng trắng trước/sau dấu câu trên cả hai phía trước khi so. Cô lập chất lượng diacritic khỏi quy ước punctuation-spacing. Số bên trên là sau normalize.
Hướng dẫn production register-conditional:
| Register | Best có sẵn | Word acc | Ghi chú |
|---|---|---|---|
| Hành chính / pháp lý (UDHR-like) | nrl-ai/vn-diacritic-vit5-base | 99.43 % | Fine-tune ViT5-base v0.2.25 của ta; +1.29 pp so với Toshiiiii1 |
| Kinh doanh hiện đại / hợp đồng / tin tức | Toshiiiii1/Vietnamese_diacritics_restoration_5th | 97.81 % | Thắng gpt-4o-mini 95.37 % ở register này; nrl-ai/vit5-base 4.4 pp sau |
| Hội thoại (Tatoeba) | nrl-ai/vn-diacritic-vit5-base | 94.12 % | +0.18 pp so với Toshiiiii1 (93.94) |
| Văn học cổ điển (UD-VTB) | Toshiiiii1/... (vẫn hữu ích) | 89.40 % | Dưới business nhưng cao hơn nhiều rule baseline (41 %); fail chủ yếu là mơ hồ danh từ riêng (Hùng ↔ Hưng) và từ register thiểu số |
| Mixed chung | Toshiiiii1/... cho hầu hết case; cloud LLM làm fallback | 89-98 % | Gap 8.7 pp giữa các register Toshiiiii1 là thật nhưng có giới hạn |
nrl-ai/vn-diacritic-vit5-base của chúng tôi (publish 2026-04-30): Fine-tune ViT5-base trên 500K cặp Wikipedia, 5 epoch cosine LR, bf16, 185 phút trên RTX 3090. Apache-2.0, ~900 MB safetensors. Cổng adopt nghiêm ngặt (business >= 96 % VÀ literary > 89.40 %) fail trên business (94.98 %), nên KHÔNG phải tên canonical nrl-ai/vn-diacritic-restoration (giữ cho mô hình tương lai qua được cổng). Nhưng đó là mô hình diacritic VN cân bằng register tốt nhất chúng tôi đã train — SOTA trên tiếng Việt formal/legal (99.43 %, +1.29 pp so với Toshiiiii1) và hội thoại (94.12 %, +0.18 pp).
| Register | Toshiiiii1 | nrl-ai/vit5-base | Δ |
|---|---|---|---|
| formal_udhr | 98.14 % | 99.43 % | +1.43 |
| business_55 | 97.81 % | 94.98 % | -4.37 |
| conversational_300 | 93.94 % | 94.12 % | +0.39 |
| literary_udvtb | 89.40 % | 90.24 % | -0.01 |
Dùng qua HFDiacriticModel(model_id="nrl-ai/vn-diacritic-vit5-base"). Config training đầy đủ + tái lập: xem HF model card.
Hai bài học methodology rơi vào rule multi-corpus register-coverage:
- Đo nhiều corpus là bắt buộc cho claim adopt. Số chất lượng single-corpus che yếu register-shift hoặc artifact benchmark.
- Metric không hợp lý đòi hỏi điều tra. Bất cứ thứ gì peg ở 0 % hoặc 100 % trên mô hình thật gần như chắc chắn là bug bench, không phải kết quả đúng. Chúng tôi bắt được một cái cách này.
Tái lập:
# Build slice eval per-register (tất định, không cần mạng).
python benchmarks/data/tatoeba_vi/build_diacritic_eval.py
python benchmarks/data/udhr_vi/build_diacritic_eval.py
# Bench Toshiiiii1 trên 4 register.
python benchmarks/accuracy/bench_diacritic_hf.py \
Toshiiiii1/Vietnamese_diacritics_restoration_5th \
--json benchmarks/results/baseline_diacritic_toshiiiii_t5.json
python benchmarks/accuracy/bench_diacritic_hf.py \
Toshiiiii1/Vietnamese_diacritics_restoration_5th \
--corpus benchmarks/data/tatoeba_vi/diacritic_eval_300.txt \
--json benchmarks/results/baseline_diacritic_toshiiiii_tatoeba300.json
python benchmarks/accuracy/bench_diacritic_hf.py \
Toshiiiii1/Vietnamese_diacritics_restoration_5th \
--corpus benchmarks/data/udhr_vi/diacritic_eval_udhr.txt \
--json benchmarks/results/baseline_diacritic_toshiiiii_udhr72.json
python benchmarks/accuracy/bench_diacritic_hf_udvtb.py \
--json benchmarks/results/baseline_diacritic_toshiiiii_udvtb_test.jsonBench thực tế ngoài-phân-phối — đo 2026-04-30
benchmarks/data/spell_correction_eval_real/ là tập 150 câu hand-curated mà mẫu nhiễu lấy từ nguồn lỗi VN thực (slang forum, autocorrect mobile, keystroke Telex/VNI thực, output engine Tesseract+EasyOCR, văn bản pháp lý register formal đã strip, headline tin tức), KHÔNG từ nom.text.noise. Cùng harness áp cho cả mô hình diacritic và spell-correction — sửa chính tả là siêu tập chặt của khôi phục dấu.
Word accuracy tổng hợp trên n=150 (KTC bootstrap 95 %):
| Mô hình | Tổng | Telex | Forum | Legal | News |
|---|---|---|---|---|---|
nrl-ai/vn-spell-correction-base v0.2.29 | 79.62 [75-85] | 19.15 | 65.84 | 95.87 | 96.54 |
nrl-ai/vn-spell-correction-small v0.2.29 | 77.55 [73-83] | 16.45 | 64.64 | 93.54 | 91.34 |
Toshiiiii1/Vietnamese_diacritics_restoration_5th | 77.40 [73-82] | 18.54 | 60.11 | 93.80 | 94.07 |
qthuan2604/ViT5_Restore_Diacritics_Vietnamese | 72.42 [68-77] | 15.65 | 58.38 | 88.14 | 87.65 |
nrl-ai/vn-diacritic-vit5-base v0.2.29 | 71.15 [66-76] | 14.37 | 43.54 | 93.02 | 96.05 |
nrl-ai/vn-diacritic-small v0.2.28 | 70.27 [65-76] | 9.33 | 46.28 | 89.15 | 90.35 |
chamdentimem/ViT5_Vietnamese_Correction | 51.69 [46-57] | 17.14 | 62.19 | 61.76 | 34.81 |
bmd1905/vietnamese-correction-v2 | 49.21 [44-55] | 11.58 | 59.02 | 54.90 | 30.62 |
iAmHieu2012/vit5-vietnamese-spelling-correction | 45.57 [39-52] | 13.58 | 57.66 | 50.68 | 27.35 |
Tái lập:
python benchmarks/accuracy/bench_spell_correction_real.py <model_id> \
--json benchmarks/results/baseline_real_<short>.json
python scripts/summarize_ood_bench.py --format markdown --ciPhát hiện chính sau v0.2.29:
- Cả hai tier spell-correction vượt Toshiiiii1 trên OOD. base v0.2.29 dẫn +2.22 pp tổng hợp; small v0.2.29 dẫn +0.15 pp dù ít tham số bằng một nửa. Synthetic 8-split show lead 3-7 pp, OOD show lead +0.15 → +2.22 pp — corpus v2 +
comprehensive_noise()đã làm đúng việc khép khoảng cách cho nhiễu thực. - bmd1905 sụp đổ trên OOD (49.21 % tổng) — train trên phân phối nhiễu khác mà không expose đủ pattern strip-diacritic. Bài học cảnh báo: mô hình thắng eval synthetic của chính mình không đảm bảo thắng trên text thật.
- Diacritic-only v0.2.29 mixed result. Legal +4.97 pp, News +0.25 pp — formal-text đã cải thiện. Nhưng aggregate -0.35 pp vì informal slices (forum / mobile) regress khi corpus skew về legal. Cho diacritic-only formal use case (legal docs / news): chọn v0.2.29. Cho informal: route qua
vn-spell-correction-basethay vì.
JSON baseline commit dưới benchmarks/results/baseline_real_*.json.
Lưới LLM cục bộ — đo 2026-04-26
Mục tiêu: xác định mô hình quantize cục bộ nhỏ nhất chạm độ chính xác diacritic VN dùng được cho deploy máy người dùng. Mọi mô hình serve qua Ollama 0.21.2 (backend llama.cpp) với quantize Q4_K_M (mặc định Ollama), structured output (format JSON schema), think: false, nhiệt độ 0. Hardware: RTX 3090 24GB. Cùng corpus diacritic_eval_v0.txt.
Methodology theo rule verified-benchmarks: 3 warmup call, 55 câu timed, latency per-call gộp.
| Mô hình | Q4 size | Word acc | Diacritic recall | Mean s/câu | p95 s/câu |
|---|---|---|---|---|---|
gemma4:e4b | 9.6 GB | 93.18% | 92.22% | 1.37 | 1.68 |
gemma3:4b ⭐ default | 3.3 GB | 87.90% | 87.50% | 1.10 | 1.22 |
qwen3:8b | 5.2 GB | 87.26% | 86.19% | 0.93 | 1.07 |
gemma4:e2b | 7.2 GB | 85.33% | 84.55% | 1.23 | 1.47 |
qwen3:4b | 2.5 GB | 47.36% | 40.48% | 0.94 | 1.06 |
| (rule baseline) | 0 | 41.06% | 34.88% | <0.001 | — |
llama3.2:3b | 2.0 GB | 38.35% | 33.69% | 1.50 | 1.95 |
qwen3:1.7b | 1.4 GB | 18.15% | 6.92% | 0.63 | 0.73 |
gemma3:1b | 0.8 GB | 15.32% | 3.22% | 1.41 | 1.90 |
phi4-mini | 2.5 GB | 6.95% | 2.13% | 2.32 | 10.24 |
(cloud gpt-4o-mini) | — | 95.37% | 94.61% | 1.27 | — |
Phát hiện:
- Họ Gemma thắng cuộc chiến multilingual. Cả
gemma3:4bvàgemma4:e4bđều vượt Qwen3 và Llama ở size tương tự — training multilingual trả lời cho VN. - 3-4B param là sàn cho diacritic VN dùng được. Mô hình sub-2B (gemma3:1b, qwen3:1.7b) đều rớt xuống dưới rule baseline. Cliff chất lượng dốc.
- Tên "E2B/E4B" của Gemma 4 nói về active param, không phải file size. Weights multimodal (encoder vision + audio) phình disk:
e2b= 7.2 GB Q4,e4b= 9.6 GB Q4. Cho task text-only như khôi phục dấu, đây là dead weight khi download. gemma3:4blà tradeoff size/chất lượng tốt nhất chonom-vn. 3.3 GB vừa laptop 4-6 GB VRAM, 87.9% acc trong 7.5pp của cloud ở 1.1 s/câu. Mặc định khuyến nghị cho đường LLM cục bộ.- Llama 3.2 / phi4-mini bị loại. Tokenizer Llama không cân cho VN; phi4-mini hang trên câu khó (p95=10s).
- Cloud +2pp so với best local.
gpt-4o-mini95.37% chỉ vượtgemma4:e4b(93.18%) 2.2pp; cả hai đều trên thanh dùng-được thực tế.
Hai engineering fix đã ship để đo được việc này (xem #PR và src/nom/llm/ollama.py + src/nom/text/normalize.py):
- Pass
think: falsecho Ollama. Mode thinking của Qwen3 emit CoT vào fieldthinkingriêng, đểcontentrỗng —qwen3:4btrước đó scored 0%. - Switch
fix_diacritics(llm=...)sang structured output qua JSON schemaformatcủa Ollama. Ép hình dạng{"restored": "..."}; mô hình nhỏ (qwen3:4b, gemma3:4b) không còn ramble giải thích vào response. Chất lượng nhảy từ <50% lên 87-93% trên lưới.
Tái lập một mô hình: python benchmarks/accuracy/bench_diacritics.py --llm ollama --llm-model gemma3:4b --warmup 3 Tái lập lưới đầy đủ: OLLAMA_BASE_URL=http://localhost:11434 ./benchmarks/accuracy/run_diacritic_local_grid.sh JSON tổng hợp: python benchmarks/accuracy/_summarize_diacritic_grid.py JSON per-model: benchmarks/results/local_diacritic_grid/diacritics_*.json
Performance — đo 2026-04-25 trên Python 3.13.9
Corpus: 1.000 câu kiểu hợp đồng tiếng Việt (67.600 ký tự).
| Function | Latency (best of 3) | Throughput (ops/s) | Throughput (chars/s) |
|---|---|---|---|
normalize | 0.11 ms | 9,066,758 | 612,912,817 |
has_diacritics | 0.19 ms | 5,325,466 | 360,001,468 |
is_vietnamese | 0.24 ms | 4,254,631 | 287,613,073 |
strip_diacritics | 5.87 ms | 170,368 | 11,516,906 |
fix_diacritics | 5.12 ms | 195,122 | 13,190,280 |
Tham chiếu: stdlib unicodedata.normalize NFC | 0.12 ms | 8,365,051 | 565,477,425 |
Tham chiếu: stdlib unicodedata.normalize NFD | 0.48 ms | 2,062,749 | 139,441,817 |
Tái lập: python benchmarks/perf/bench_text.py
Lý do lựa chọn component
Vì sao pure stdlib (không dep bên thứ ba):
unicodedatatrong CPython core, zero rào cản cài đặt.- Performance đủ (>500 MB/s trên
normalize). - Tất định — không load mô hình, không mạng.
- v0.1 có thể thêm
fix_diacritics(..., llm=...)LLM-backed cho case mơ hồ, nhưng đường pure-rule giữ.
Vì sao không pyvi hay underthesea cho v0?
- Cả hai xuất sắc cho tokenization/POS-tagging — ngoài phạm vi v0.0.1.
- Sẽ xuất hiện như dep tuỳ chọn trong
nom.text.tokenize(v0.2+) cho người dùng muốn.
Tách từ — đo 2026-04-26
Hai backend, corpus gold-standard thực:
nom.text.word_tokenize— pure-Python rule + merge bảng compound, zero depsunderthesea.word_tokenize— mô hình CRF, Apache 2.0, opt-in quanom-vn[nlp]
Corpus: UD_Vietnamese-VTB test split (UniversalDependencies/UD_Vietnamese-VTB, CC-BY-SA-4.0). 800 câu, 11.692 token gold. Methodology: warmup 3 + best-of-5 throughput; span token đoán so với span gold theo char range (start, end) chính xác.
| Tokenizer | Precision | Recall | F1 | Throughput | Ghi chú |
|---|---|---|---|---|---|
underthesea==9.4.0 | 95.94% | 95.46% | 95.70% | 38.102 tok/s | Binary native CRFsuite; ~5 MB disk |
nom.text (rule) | 70.94% | 82.90% | 76.46% | 747.117 tok/s | Pure-Python; zero deps; 0 mô hình |
Phát hiện:
- underthesea +19.24 pp F1 trên
nom.text— dữ liệu training CRF thắng quyết định trên boundary compound ngôn ngữ (danh từ riêng nhiều âm tiết, cụm cố định như mã số, địa chỉ, Nguyễn Thị Hương). nom.textnhanh ~20× (747 k vs 38 k tok/s). Cho RAG indexing, BM25 tokenization, dọn nhẹ — speed thắng; gap F1 không quan trọng khi phía sau là retriever bag-of-words.nom.textrecall (82.9 %) > precision (70.9 %) — over-split. Bảng compound bắt được vài merge (398 hit toàn corpus) nhưng còn xa coverage CRF.
Cross-check so với số đã công bố (rule cross-check-against-published-numbers):
- underthesea báo ~94 % F1 trên test set VLSP 2013 [1]; 95.70 % của ta trên UD-VTB test ~1.5 pp trên — có thể do UD-VTB là register dễ hơn VLSP 2013 (văn học prose so với tin tức/business mixed). Cùng order of magnitude — không có divergence methodology cần đuổi.
- Chúng tôi không bench PyVi riêng: tự động từ chối theo chính sách no-pickle (ship file mô hình
.pkl= arbitrary code execution khi load).
Khuyến nghị cho nom-vn:
| Use case | Chọn |
|---|---|
| RAG indexing, BM25, search tokenize | nom.text — speed dominant |
| NER / dependency parsing / phân tích ngôn ngữ | nom-vn[nlp] → underthesea — F1 dominant |
| Dọn post-OCR, tokenize khôi phục dấu | nom.text — gap F1 chấp nhận; zero deps thắng |
Hai cái bổ sung cho nhau, không thay thế — surface trong API doc để user không pick sai và đổ lỗi cho gap F1.
Tái lập: python benchmarks/accuracy/bench_segment.py --corpus ud_vtb --split test --json benchmarks/results/baseline_segment_ud_vtb_test.json Baseline: benchmarks/results/baseline_segment_ud_vtb_test.json
[1]: README Underthesea — số VLSP 2013 báo.
Module: nom.classify.register — đã ship v0.3
Phân loại đoạn văn tiếng Việt theo 4 lớp văn phong: trang trọng, kinh doanh / báo chí, hội thoại, văn học. Hai cách chạy sau cùng Protocol:
LexiconRegisterClassifier— bộ từ điển đặc trưng cho mỗi văn phong, không cần model. Mặc định khi không cài extras.PhoBertRegisterClassifier— wrapper quanhnrl-ai/vn-register-phobert-base(PhoBERT-base + đầu phân loại 4 lớp, MIT, ~540 MB safetensors).
Số đo nội bộ — đo 2026-05-03
Tinh chỉnh PhoBERT-base 4 epoch trên kho 4 lớp đa nguồn (giới hạn 2 000 mẫu / lớp; cắt 70/10/20 train/val/test theo lớp). 195 giây trên RTX 3090, bf16.
| Lớp | F1 | Hỗ trợ (test) |
|---|---|---|
formal | 0,914 | 34 |
business | 0,906 | 400 |
conversational | 0,915 | 400 |
literary | 0,866 | 400 |
| macro | 0,900 | 1234 |
Cổng adopt: macro F1 ≥ 0,85 và mọi F1 lớp ≥ 0,75 — đều đạt. literary là lớp yếu nhất do từ vựng cổ điển chia sẻ với formal (~18 % câu Wikisource bị nhầm sang formal).
JSON kết quả: benchmarks/accuracy/register_phobert_base_baseline.json. Tái lập: python training/register/train.py --output-dir checkpoints/register-phobert-base --epochs 4.
Cảnh báo — đo trên 1 corpus
Hiện chỉ đo trên hold-out cùng phân phối với tập huấn luyện (UDHR, wiki_vi, Tatoeba, Wikisource, UD-VTB). Theo nguyên tắc đa-corpus của dự án, claim "F1 macro 0,900 trên VN tổng quát" cần một corpus thứ hai (đợt sau: Zalo Legal QA formal, vietnews business). Nếu spread > 10 pp giữa các corpus thì checkpoint hiện tại đang register-overfit và cần huấn luyện lại với data đa dạng hơn.
Module: nom.ocr.handwriting — đã ship v0.3
VLM-based OCR cho chữ viết tay tiếng Việt (biểu mẫu, ghi chú, mặt sau CMND/CCCD). Bổ sung cho nom.doc.ocr (Tesseract — chữ in).
VinternHandwritingOcr— wrapper quanh5CD-AI/Vintern-1B-v3_5(MIT, safetensors, 0,9 B tham số). Tiền xử lý ImageNet 448×448 một-tile; chặn line-crop dưới 60 px chiều ngắn để tránh hiện tượng VLM ảo trên crop hẹp.
Số đo nội bộ trên synthetic_ocr_vi — đo 2026-05-03
Cảnh báo: đo trên chữ in tổng hợp, không phải chữ viết tay. Mục đích là sàng lọc — nếu Vintern fail trên chữ in clean thì cũng không hy vọng được trên chữ tay. Số đo headline cho chữ tay phải đợi đợt sau trên brianhuster/VietnameseOCRdataset (~7 nghìn ảnh thật, Apache-2.0).
| Điều kiện | n | CER trung bình | Khớp tuyệt đối |
|---|---|---|---|
clean (1 phông, nền trắng) | 20 | 0,47 % | 16/20 |
noisy (cùng nội dung, nhiễu) | 20 | 0,37 % | 17/20 |
Phần lớn "lỗi" còn lại là biến thể chính tả VN hợp lệ (hoà ↔ hòa) mà CER tính là khác. n=20 là smoke test, không phải con số headline.
JSON: vintern_ocr_clean_baseline.json, vintern_ocr_noisy_baseline.json.
Module: nom.stt — đã ship v0.3
Whisper-family STT cho tiếng Việt — phỏng vấn, cuộc họp, ghi chú giọng nói.
PhoWhisperSTT—vinai/PhoWhisper-large(BSD-3, 1,5 B, 844 giờ huấn luyện trên ghi âm tiếng Việt). Mặc định cho audio thuần VN.WhisperSTT—openai/whisper-large-v3(MIT, safetensors). Cho audio lai VN ↔ EN.
Số đo nội bộ — đo 2026-05-03
Cảnh báo: n=3 chỉ là smoke test, không phải đo đầy đủ. Bench đầy đủ trên ViMD 3 vùng (Bắc 40,6 giờ / Trung 31,5 giờ / Nam 30,5 giờ) là việc đợt sau.
Trên 3 mẫu kiểm thử của doof-ferb/Speech-MASSIVE_vie:
| Mô hình | n | WER trung bình |
|---|---|---|
vinai/PhoWhisper-large | 3 | 15,2 % |
openai/whisper-large-v3 | 3 | 15,2 % |
Hai mô hình bằng nhau trên tập nhỏ này — lỗi chính: nhầm từ đồng âm (múi giờ ↔ mỗi giờ), thay từ (xếp ↔ sắp), chênh dấu câu / viết hoa. Con số 6,4 % WER trên model card VinAI là tự công bố, chưa được tái lập tại đây.
JSON: stt_speech_massive_baseline.json.
Module: nom.summarize — đã ship v0.3
Tóm tắt văn bản tiếng Việt với tiền tố theo văn phong:
ViT5Summarizer—VietAI/vit5-large-vietnews-summarization(MIT, .bin, 866 M, 1024-token cap đầu vào). Tiền tốvietnews:/legal:/dialogue:đổi giọng văn đầu ra.
Số đo nội bộ — đo 2026-05-03
Cảnh báo: cảnh báo bịa số liệu là chuẩn của module này, không phải ngoại lệ. ViT5 tóm tắt là mô hình rút trích pha trộn, có thể chèn token số mới mà nguồn không có. Đo nội bộ trên 28 đoạn mở của wiki_vi/articles.jsonl:
| Số liệu | Giá trị |
|---|---|
| Tỉ lệ token mới (trung bình) | 13,6 % |
| Mẫu thêm số mới không có trong nguồn | 7 / 28 |
| Tỉ lệ bịa số (cảnh báo) | 25,0 % |
Ví dụ: bài về TP. Hồ Chí Minh, mô hình thêm năm "2025" không có trong đầu vào; bài Việt Nam (đoạn 234 ký tự), bịa con số GDP "6,8 % – 7,0 %". Đừng dùng cho tóm tắt pháp lý / tài chính mà không kiểm chứng từng số.
n=28 còn nhỏ; con số 25 % là cảnh báo định hướng, không phải đo định lượng đầy đủ. Bench trên VLSP-MTSum (200+ mẫu pháp lý + tin tức) là việc đợt sau.
JSON: summarize_wiki_vi_baseline.json.
Module: nom.convert — đã ship v0.3
PDF / ảnh → DOCX theo chiến lược "ưu tiên lớp văn bản gốc, dự phòng OCR" — nếu trang PDF có sẵn lớp text đọc được thì rút trực tiếp qua pdfplumber; nếu không thì đi qua đường dự phòng: pdfium2 kết xuất trang thành ảnh rồi Tesseract đọc. Đo trên kho công khai nrl-ai/vn-ocr-documents-eval v0.4 — 156 tài liệu, 8 cấu hình.
Kho đánh giá v0.4 — đã ship 2026-05-03
| Cấu hình | n | Nguồn | Giấy phép |
|---|---|---|---|
real | 9 | chinhphu.vn + hanoi.gov.vn (ảnh quét có dấu ký) | Công khai theo Luật SHTT VN, Điều 15 |
formal | 24 | UDHR tiếng Việt + hiệu ứng quét tổng hợp | CC0 (văn bản gốc thuộc miền công khai) |
news_business | 24 | wiki_vi + hiệu ứng quét tổng hợp | CC-BY-SA 4.0 |
conversational | 24 | tatoeba_vi + hiệu ứng quét tổng hợp | CC-BY 2.0 FR |
literary | 14 | Wikisource Truyện Kiều + hiệu ứng quét | Công khai |
receipt | 21 | 7 mẫu x 3 seed + hiệu ứng quét | CC0 |
contract | 20 | 5 mẫu x 4 seed + hiệu ứng quét | CC0 |
form | 20 | 5 mẫu x 4 seed + hiệu ứng quét | CC0 |
Pipeline 8 bước mô phỏng máy quét (đầu ra ổn định theo seed): nghiêng giấy +/- 0,5-2,5°, tối góc trang (vignette), tông giấy hơi vàng, nhiễu hạt phân phối Gauss, vạch ngang ru-lô máy quét, mờ ống kính, viền tối mép, đi qua JPEG nén 72-92 chất lượng rồi giải nén lại. Ba kiểu cấu hình (office_scan cho máy văn phòng hiện đại / old_photocopy cho ảnh photocopy cũ / clean_digital cho bản số hoá sạch) được chọn theo băm doc_id, để các tài liệu cùng cấu hình nhìn vẫn khác nhau.
nom.convert.convert_to_docx trên v0.4 — đo 2026-05-03
Tesseract 5 với gói vie+eng, không tinh chỉnh, qua đường OCR dự phòng:
| Cấu hình | n | CER (đã chuẩn hoá khoảng trắng) |
|---|---|---|
real (ảnh quét chính phủ thật) | 9 | 12,62 % |
synthetic_scan (tổng hợp gộp lại) | 147 | trung bình ~6 % / trung vị ~4 % |
| Tổng (cả 156 tài liệu) | 156 | trung bình 6,66 % / trung vị 4,32 % |
So sánh với PaddleOCR PP-OCRv5 mặc định — đo 2026-05-03
Để xác thực rằng PaddleOCR PP-OCRv5 (lang='vi') không phải lựa chọn thay thế cho Tesseract, đo trực tiếp trên đúng 9 ảnh quét chính phủ thật (config=real):
| Engine | CER trung bình | CER trung vị | Tốc độ (CPU) |
|---|---|---|---|
Tesseract vie+eng (mặc định nom-vn) | 12,62 % | 11,99 % | ~1,3 giây / tài liệu |
PaddleOCR PP-OCRv5 lang='vi' | 20,74 % | 19,33 % | ~60 giây / tài liệu |
PaddleOCR thua 8,1 điểm phần trăm và chậm hơn ~46 lần. Lý do: lang='vi' nạp latin_PP-OCRv5_mobile_rec — bộ nhận dạng Latin chung nuốt hết dấu thanh tiếng Việt. Mẫu đầu ra:
Đáp án: THỦ TƯỚNG CHÍNH PHỦ
PP-OCRv5: TH TƯNG CHÍNH PH
Đáp án: Độc lập - Tự do - Hạnh phúc
PP-OCRv5: Đc lp - T do - Hnh phúcMọi dấu sắc / huyền / nặng / hỏi / ngã + ơ / ư / ê / â / ô / ă / đ biến mất. JSON kết quả: benchmarks/results/baseline_paddleocr_v5_real.json.
Đường mở ra: tinh chỉnh PP-OCRv5 với từ điển ký tự VN — kế hoạch chi tiết tại training/paddleocr_vi_rec/. Cổng adopt: trung vị CER trên config=real phải vượt 11,99 % của Tesseract; nếu không thì giữ Tesseract làm mặc định và ghi nhận kết quả âm tính.
- Ảnh quét thật khó hơn ~13 lần so với ảnh tổng hợp. Bốn nguồn lỗi chính: dấu mộc tròn đỏ đè lên chữ; chữ ký tay vắt qua dòng tên; watermark nền chìm làm giảm tương phản; các từ viết tắt hành chính ("KT.", "Lưu: VT", "TM.", "PCN") chưa được gói
viecủa Tesseract huấn luyện đúng. - Tốc độ: ~1,3 giây mỗi tài liệu trên CPU đơn lõi.
- Đo cả 8 tính năng nom-vn cùng lúc (
bench_features_e2e.py): 102 LAW_REF, 203 DATE, 89 MONEY, 59 PHONE_VN, 39 ID_VN — hợp đồng và đơn từ là nơi đầu tiên có CCCD trong kho.
JSON kết quả tái lập: benchmarks/results/baseline_features_e2e_v4.json. Lệnh tái lập:
python benchmarks/data/vn_documents_ocr_v2/_synth_corpus.py
python benchmarks/accuracy/bench_features_e2e.pyCảnh báo phương pháp
realmới chỉ 9 tài liệu — mở rộng lên 20+ cần thêm ~110 phút chú thích thủ công 11 trang nữa (đọc trực tiếp từng ảnh, không tin Tesseract đoán).synthetic_scandùng chính văn bản gốc làm đáp án chuẩn (vì được kết xuất từ văn bản, nên không bịa được). Đánh đổi: hiệu ứng quét chưa mô phỏng được cấu trúc thư hành chính đầy đủ — không có dấu mộc đỏ, chữ ký tay, ô đóng dấu, hai cột tiêu đề như công văn thật. Phù hợp đo nhận dạng ký tự ở mức dòng / đoạn, không thay thế được kho ảnh quét gốc khi muốn đánh giá khả năng hiểu cấu trúc tài liệu.
Module: nom.nlp.ner_legal — đã ship v0.3, regex-only
Bộ pattern bổ sung cho RegexNERModel (đã có trong nom.nlp.ner) phục vụ tài liệu pháp lý VN: LAW_REF, ID_VN (CMND/CCCD), PHONE_VN.
Trạng thái đo
Chưa có số đo học từ dữ liệu. v0 chỉ là biểu thức chính quy. Bộ kiểm thử xác minh đúng các mẫu do chính chúng tôi viết — đây là kiểm tra cấu trúc, không phải đo trên kho VN gold-labeled.
Bao phủ ước lượng (chưa đo trên kho chuẩn):
| Lớp | Bắt được | Không bắt được |
|---|---|---|
LAW_REF | Luật 134/2025/QH15, Nghị định 13/2023/NĐ-CP, Thông tư, Điều X Luật Y | tham chiếu viết tắt phi chuẩn (≈ 15 % công văn hiện đại) |
ID_VN | CMND 9 chữ số, CCCD 12 chữ số | không xác minh mã kiểm tra CCCD; chuỗi 12 chữ số ngẫu nhiên có thể dương tính giả |
PHONE_VN | di động 10 số 03/05/07/08/09, +84, cố định Hà Nội | đầu số quốc tế ngoài VN |
Hướng tinh chỉnh PhoBERT cho LAW_REF + CONTRACT_PARTY (mục tiêu F1 ≥ 0,85) cần ~70-90 giờ chú thích thủ công, xếp lịch sau theo docs/sota_vn_2026q2_expansion.md.
Module: nom.doc.ocr — dự kiến v0.1
OCR là primitive leverage cao nhất và bị fail nhiều nhất trong AI tiếng Việt. Chúng tôi ship ba backend với cùng interface; default switch theo phần cứng có sẵn.
So sánh các engine — đo nội bộ 2026-05-02
Đo trên 4 loại văn bản: synthetic_ocr_vi/{clean,noisy,hard} (chữ in) và 200 ảnh thử nghiệm của brianhuster/VietnameseOCRdataset (chữ viết tay). Chỉ số: CER, có chuẩn hoá NFC. Tệp kết quả lưu trong benchmarks/results/baseline_ocr_engines_per_register.json.
| Engine | Giấy phép | CER in sạch | CER in nhiễu | CER quét kém | CER viết tay | Độ trễ p50 |
|---|---|---|---|---|---|---|
Tesseract 5 + vie | Apache 2.0 | 0,00 % ⭐ | 0,70 % ⭐ | 30,34 % | 69,34 % | 80 ms |
| VietOCR vgg_transformer | Apache 2.0 | 1,41 % | 3,37 % | 29,00 % ⭐ | 31,82 % ⭐ | 240 ms |
| EasyOCR | Apache 2.0 | 1,42 % | 4,87 % | 87,09 % | 71,52 % | 35-60 ms |
| PaddleOCR PP-OCRv5 (latin_mobile_rec) | Apache 2.0 | 24,70 % | 31,33 % | 86,13 % | 59,43 % | 1170 ms |
| RapidOCR (bản port ONNX của PaddleOCR) | Apache 2.0 | 63,97 % | 77,83 % | 100,00 % | 97,20 % | 130-250 ms |
| TrOCR base-handwritten (chỉ tiếng Anh, làm tham chiếu) | MIT | 32,89 % | 38,07 % | 93,76 % | 75,89 % | 180-280 ms |
qwen2.5vl:7b (VLM qua Ollama) | Apache 2.0 | 33,17 % | 29,90 % | 81,37 % | 67,53 % | 320-610 ms |
| Surya OCR | open-RAIL-M | Chưa đo (giấy phép không đạt) | — | — | — | — |
Phát hiện chính:
- VietOCR thắng tuyệt đối với chữ viết tay — 31,82 % CER so với Tesseract 69,34 % (giảm 37,5 pp tuyệt đối, -54 % tương đối). Đây là phát hiện OCR lớn nhất của dự án.
- Tesseract thắng với chữ in — 0,00 % CER trên ảnh in sạch, 0,70 % trên ảnh nhiễu. Không engine nào khác đến gần.
- PaddleOCR PP-OCRv5 đứng thứ 3-4 ở mọi loại văn bản tiếng Việt. Không phải vấn đề cấu hình, mà ở thiết kế:
lang='vi'chỉ tảilatin_PP-OCRv5_mobile_rec— mô hình nhận diện Latin chung không được huấn luyện cho tiếng Việt. Đo lại 2026-05-01 sau khi người dùng yêu cầu kiểm tra; xác nhận. - RapidOCR cùng kiểu lỗi — bản port ONNX của PaddleOCR dùng chung mô hình nhận diện Latin chung, không khả thi cho tiếng Việt. Mặc định của Docling
ocr_engine='rapidocr'cũng vậy. - VLM ảo giác trên ảnh cắt dòng đơn —
qwen2.5vl:7bcho CER 33,17 % trên ảnh in sạch (so với Tesseract 0 %). VLM là công cụ cho hiểu tài liệu (biểu mẫu, hoá đơn, bố cục), không cho dòng chữ in.
Bảng đầy đủ và hướng dẫn chọn engine theo từng loại dữ liệu nằm trong docs/tasks/ocr.md.
Khuyến nghị cho nom.doc
Mặc định: Tesseract vie cho chữ in (CER 0-1 %, 80 ms p50). VietOCR vgg_transformer cho chữ viết tay (CER 31,82 %, 246 ms p50 trên GPU).
# API dự kiến v0.1
from nom.doc import extract
from nom.doc.ocr import VietOCR, Tesseract, PaddleOCR
# Auto: VietOCR nếu cài → PaddleOCR → Tesseract
result = extract("scan.pdf", schema={...})
# Rõ ràng
result = extract("scan.pdf", schema={...}, ocr=PaddleOCR())Lớp parsing PDF bên dưới — đo 2026-04-26
Chúng tôi không ship PyMuPDF. License AGPL của nó không tương thích với Apache-2.0 mặc định. Thay vào dùng pypdfium2 (BSD-3 wrapper trên PDFium của Google, Apache-2.0) làm mặc định trích text nhanh và giữ pdfplumber cho tài liệu nhiều bảng.
Corpus: benchmarks/data/synthetic_pdf_vi/vn_legal.pdf — PDF VN tổng hợp 7-page xây từ prose VN public-domain thực (UDHR + Wikisource Truyện Kiều prefaces) với lớp text Unicode sạch (DejaVuSans embed). Generator: benchmarks/data/synthetic_pdf_vi/_generate.py. Nhãn thật: 18.877 ký tự commit kèm.
udhr_vi/udhr_vie.pdf ship sẵn không dùng được ở đây — nó embed font tuỳ biến không có ToUnicode CMap, nên mọi extractor (pdfplumber, pypdfium2, PyMuPDF) trả về CIDs / byte rác. Đã tài liệu hoá trong bench script.
Methodology: warmup 3 + best-of-5 (rule verified-benchmarks). Char-overlap fidelity dùng giao multiset NFC-normalised so với nhãn thật.
| Library | License | Best-of-5 (s) | Throughput | Char overlap |
|---|---|---|---|---|
pypdfium2==5.7.1 ⭐ default | BSD-3 / Apache-2.0 | 0.0079 | 2.350.431 chars/s | 99.81% |
pdfplumber==0.11.9 | MIT | 0.3654 | 51.052 chars/s | 99.81% |
Phát hiện:
pypdfium2nhanh hơn 46× so với pdfplumber trên trích text-only với fidelity y hệt (99.81% — cả hai miss cùng ~36 ký tự, hầu hết là glyph Hán mà DejaVuSans không render được).- License là headline. Speedup 19× của PyMuPDF công bố trên
py-pdf/benchmarkslà thật — nhưng AGPL ép mọi dự án phía sau ship dạng AGPL. PDFium dưới pypdfium2 cho ta cùng order-of-magnitude speedup mà không có bẫy license. - pdfplumber giữ trong
nom-vn[doc]— vẫn là lựa chọn tốt hơn khi tài liệu có bảng. Pipelinenom.docchọn per-document khi parse.
Khuyến nghị:
| Use case | Chọn |
|---|---|
| Trích text thuần (RAG, search indexing) | pypdfium2 — speed thắng, license sạch |
| Bảng / form / layout có cấu trúc | pdfplumber — phát hiện cell tốt hơn |
Tái lập: python benchmarks/perf/bench_pdf_extract.py Baseline: benchmarks/results/baseline_pdf_extract.json
Build corpus từ một bản clone sạch: python benchmarks/data/synthetic_pdf_vi/_generate.py (cần DejaVuSans — apt install fonts-dejavu).
Lưu ý PyMuPDF / fitz — chúng tôi giữ chúng hoàn toàn ngoài dependency. User thực sự cần PyMuPDF (ví dụ dự án nội bộ chấp nhận AGPL) tự cài và gọi trực tiếp; chúng tôi không expose wrapper làm mờ ranh giới license.
Docling (IBM, MIT) — đo 2026-04-26. Cùng PDF VN, warmup 2 + best-of-3, default DocumentConverter():
| Library | Best (s) | Throughput | Char overlap | Disk |
|---|---|---|---|---|
| pypdfium2 | 0.0079 | 2.350.431 chars/s | 99.81% | <10 MB |
| pdfplumber | 0.3654 | 51.052 chars/s | 99.81% | <5 MB |
| docling | 1.1889 | 15.703 chars/s | 99.72% | ~1 GB (PyTorch + DocLayNet + TableFormer) |
Docling chậm hơn pypdfium2 150× trên PDF text Unicode-clean này và hơi tệ hơn về fidelity (99.72% vs 99.81%) — pipeline ML layout không trả lợi tức khi PDF đã có lớp text sạch. Docling kiếm cost của nó trên layout phức tạp (multi-column, bảng, công thức, mixed text+ảnh) nơi heuristic của pdfplumber vỡ. Chưa đo in-house trên PDF VN nhiều bảng.
Khuyến nghị Docling: giữ NGOÀI nom-vn[doc] lúc này. Nếu corpus hướng tới người dùng layout phức tạp xuất hiện (form pháp lý, báo cáo chính phủ), thêm extra nom-vn[docling] và surface là nom.doc.layout_extract(). Cho đến lúc đó, trọng lượng dependency (~1 GB stack ML + safetensors) không justified cho PDF text thuần.
Bảng landscape trước từ py-pdf/benchmarks cho bối cảnh (PDF academic + business mixed):
| Library | Thời gian trung bình per-doc | Ghi chú |
|---|---|---|
| PyMuPDF (fitz) | 0.5 s | Không ship — AGPL |
| pypdf | 4.2 s | MIT, thao tác cơ bản |
| pdfplumber | 9.5 s | Trích bảng phong phú nhất |
Nguồn
- [1] Kết quả test VietOCR / Tesseract VN
- [2] Benchmark PaddleOCR vs EasyOCR vs Tesseract — TildAlice
- [3] Issue stack-diacritic Tesseract VN
- [4] So sánh Tesseract vs EasyOCR
- [5] Release note PaddleOCR PP-OCRv5
- [6] VietOCR (Transformer) — pbcquoc
- [7] Survey on Vietnamese Document Analysis (arXiv 2506.05061)
Module: nom.llm — dự kiến v0.1
Nôm không bundle mô hình. Chúng tôi ship class adapter; user chọn mô hình nào để trỏ tới.
Mô hình khuyến nghị — ba bracket
Bracket 1 — local, miễn phí, chạy laptop consumer
Primary: Qwen3-8B qua Ollama
- License Apache 2.0 — commercial OK
- Chạy ~6GB VRAM (Q4 quant) hoặc 16GB RAM CPU
- VMLU mạnh cho size
- Multilingual gồm tiếng Việt
- Cài một dòng:
ollama pull qwen3:8b
Thay thế: Llama-3.1-8B-Instruct
- License Meta (commercial OK với điều kiện)
- VN performance hơi tệ hơn Qwen3 trong review 2026 [1]
Thay thế: Vistral-7B-Chat
- Đã fine-tune cho tiếng Việt
- License chỉ research (theo VinAI) — không cho commercial
Bracket 2 — cloud, cost trung bình, chất lượng open top
Qwen3-235B-A22B qua Together AI / Fireworks / Alibaba Cloud
- Apache 2.0
- 235B MoE (22B active) — VN mạnh
- Khuyến nghị top trong guide best-VN-LLM 2026 [1]
- ~$0.50–1/M input token qua provider
Bracket 3 — closed, chất lượng tối đa
| Provider | Mô hình | Vì sao dùng |
|---|---|---|
| OpenAI | gpt-4o | Reasoning VN chung tốt nhất, vision-capable |
| Anthropic | claude-sonnet | Reasoning long-document mạnh, context lớn |
gemini-2.5-pro | Rẻ nhất ở top tier 2026 |
Mô hình vision-capable cho nom.doc (tài liệu scan)
Cho công việc OCR-grade trực tiếp trên ảnh (bỏ bước OCR hoàn toàn):
| Mô hình | License | Ghi chú |
|---|---|---|
| Qwen2.5-VL-72B-Instruct | Apache 2.0 | Top open vision-LLM cho extraction tài liệu có cấu trúc [2] |
| GLM-4.5V | open weights | Mạnh trên chart, bảng, layout phức tạp [2] |
| DeepSeek-VL2 | open weights | Tốt trên doc VN scan theo anecdote |
| GPT-4o (vision) | closed | Best khi latency/cost không phải ràng buộc |
Khuyến nghị: nom.doc.extract dùng hai đường:
- PDF native → text → LLM text-only (rẻ nhất, nhanh nhất)
- Scan/ảnh → vision-LLM trực tiếp (skip OCR, thường chất lượng cao hơn)
Nguồn
- [1] Best Open Source LLM for Vietnamese in 2026 — SiliconFlow
- [2] Best LLM for Document Screening in 2026 — SiliconFlow
- [3] Document Data Extraction in 2026: LLMs vs OCRs — Vellum
- [4] VMLU Leaderboard — xếp hạng LLM VN hiện tại
- [5] Qwen2.5 release notes
Module: nom.prompts — dự kiến v0.2
Prompt hệ thống curated, có version cho tài liệu kinh doanh VN. Chưa có benchmark — giá trị module này nằm ở prompt nào thắng, không phải tốc độ raw.
Domain sẽ cover
| Domain | Vì sao ưu tiên | Nguồn test set |
|---|---|---|
| Hợp đồng | Doc kinh doanh VN tần suất cao nhất | Corpus hợp đồng NRL-curated |
| Công văn | Chính phủ/SMB workflow chủ yếu | Corpus VLSP (chỗ license cho phép) |
| Đơn từ | Khối lượng cao, input OCR thấp | Synthetic + community submission |
| Email công sở | Drafting tone-aware (kính gửi vs cho) | Eval set nội bộ |
| Hoá đơn / biên lai | Use-case kế toán | Open OCR datasets |
Versioning
Prompt có version (nom.prompts.contracts.v1). Một khi publish, không bao giờ silently đổi. Pin version là phần của contract tái lập của người dùng.
Benchmark mức module NRL đóng góp (VN-Bench v1)
Đây là việc sẽ đưa số gốc NRL vào nrl.ai/bench (so với trang hiện tại tổng hợp VMLU). Xem VN-Bench v1 roadmap cho danh sách task canonical.
| Task | Mô tả | Eval | Trạng thái |
|---|---|---|---|
| Trích hợp đồng | PDF → schema typed | F1 trên field accuracy | đang phát triển |
| Parse công văn | Số / ngày / đơn vị / nội dung | Exact match | đang phát triển |
| Scan OCR → JSON | Ảnh → có cấu trúc | Char-edit + field accuracy | đang phát triển |
| Giữ dấu | Sinh có dấu đúng | Diacritic accuracy trên đoạn dài | đang phát triển |
| Code-switching EN/VN | Hiểu hội thoại mixed | Pairwise judge | đang phát triển |
| QA pháp lý | Dự đoán điều, trích | Mượn từ VLegal-Bench | partner |
Module: nom.rag — đã ship v0.2.5
Làm gì
RAG end-to-end trên tài liệu VN: BM25 + dense (encoder sentence-transformers) hybrid retrieval với RRF fusion, reranking cross-encoder tuỳ chọn, mở rộng HyDE / multi-query tuỳ chọn, sau đó gọi LLM.
Ba dòng từ tài liệu sang câu trả lời — xem docstring src/nom/rag/pipeline.py cho ví dụ canonical.
So sánh reranker trên Zalo Legal QA 5 k — đo 2026-04-26
Cùng fixture (5.061 doc / 80 câu hỏi), embedder bkai-vietnamese-bi-encoder, pipeline hybrid+rerank, RTX 3090. So sánh apple-to-apple — chỉ reranker đổi.
| Reranker | License | Disk | Param | R@1 | R@10 | MRR@10 | p50 ms (gồm seg) |
|---|---|---|---|---|---|---|---|
BAAI/bge-reranker-v2-m3 ⭐ default | Apache 2.0 | ~2.3 GB | 568 M | 86.3 % | 100.0 % | 0.929 | 583 |
itdainb/PhoRanker (word-segmented) | Apache 2.0 | ~395 MB | 100 M | 83.8 % | 98.8 % | 0.907 | 863 |
itdainb/PhoRanker (KHÔNG word-segment, BROKEN config) | Apache 2.0 | ~395 MB | 100 M | 70.0 % | 97.5 % | 0.802 | 295 |
Bức tranh reranker, framing chính xác. PhoRanker được báo thắng bge-reranker-v2-m3 trên MMARCO-Vi (NDCG@3 0.6625 vs 0.6087). Trên bench Zalo Legal 5 k của ta, PhoRanker chỉ kém bge-reranker-v2-m3 2.5 pp ở disk nhỏ hơn 5.7×. Đây là tradeoff tốt hơn nhiều so với v0.2.17 implied — số 70.0 % R@1 ban đầu là bug methodology (thiếu word segmentation; xem rule ALWAYS DOUBLE-CHECK bên dưới).
Khuyến nghị:
Default: bge-reranker-v2-m3. Chất lượng cao nhất (R@1 86.3 %), đơn giản nhất để deploy (không cần preprocessing).
Tier light-weight: PhoRanker với word segmentation. Trong 2.5 pp R@1 của default ở disk nhỏ hơn 5.7× và inference cross-encoder nhanh hơn. Lựa chọn đúng cho laptop nơi 2.3 GB reranker không vừa cùng embedder + LLM. Cần
nom-vn[nlp]cho underthesea word segmentation — password_segment=TruechoCrossEncoderRerankerhoặc--reranker-word-segmentcho bench:pythonCrossEncoderReranker("itdainb/PhoRanker", word_segment=True)863 ms p50 gồm segment underthesea per-query. Trong production cache chunk corpus đã segment ở thời điểm index, drop cost per-query xuống ~300 ms.
Bài học ALWAYS DOUBLE-CHECK (rule ALWAYS DOUBLE-CHECK của ta). Số PhoRanker v0.2.17 (70.0 % R@1) sai vì gửi text raw không segment cho mô hình mà card explicitly yêu cầu input đã segment VnCoreNLP. Re-check model card 2026-04-26 bắt được. Khái quát hoá bài học: cho bất kỳ reranker mới nào, đọc ví dụ usage canonical từ model card trước khi bench. Preprocessing hardcoded trong bench giờ là kwarg word_segment=, không phải giả định ẩn.
Auto-detect max_length: CrossEncoderReranker(...) giờ tự động phát hiện max_position_embeddings của mỗi mô hình từ config.json để bạn swap reranker không cần nghĩ. PhoRanker (PhoBERT-base, cap 256) và bge-reranker-v2-m3 (XLM-R-large, cap 512) đều chạy qua cùng call:
from nom.rag import CrossEncoderReranker
# Cả hai chạy — max_length auto-detect
default = CrossEncoderReranker() # bge-reranker-v2-m3
lite = CrossEncoderReranker("itdainb/PhoRanker") # cap 256, không cần flag tayOverride qua max_length=... khi heuristic tự động phát hiện sai cho mô hình lạ.
Tái lập: python benchmarks/rag/bench_rag_vn.py --fixture benchmarks/rag/fixtures/vn_legal_zalo_5k.json --embedder bkai --retrievers hybrid+rerank --reranker itdainb/PhoRanker --json benchmarks/results/baseline_phoranker_zalo5k.json
Baseline: benchmarks/results/baseline_phoranker_zalo5k.json (PhoRanker), benchmarks/results/baseline_bge_reranker_bkai_zalo5k.json (bge-reranker-v2-m3).
Retrieval chỉ embedder — đo 2026-04-26
So sánh two-tower trực tiếp: encode mọi doc + mọi câu hỏi, rank doc theo cosine. Không BM25, không fusion, không reranker — mọi khác biệt chất lượng đều thuần là embedder. Cách này bắt case mà phân phối training STS-tuned của embedder không transfer sang retrieval (task asymmetric Q→Doc mà pipeline RAG thực sự làm).
Corpus: benchmarks/rag/fixtures/vn_legal_zalo_5k.json (5.061 doc / 80 câu hỏi, sample từ Zalo AI 2021 Legal QA, MIT). Hardware: RTX 3090.
| Mô hình | License | Disk | R@1 | R@10 | MRR@10 | docs/s |
|---|---|---|---|---|---|---|
bkai-foundation-models/vietnamese-bi-encoder ⭐ | Apache 2.0 | ~383 MB | 76.25 % | 98.75 % | 0.8604 | 60 |
dangvantuan/vietnamese-embedding (default hiện tại) | Apache 2.0 | ~440 MB | 35.00 % | 67.50 % | 0.4449 | 53 |
bkai thắng +41.25 pp R@1 và +31.25 pp R@10 ở size disk nhỏ hơn và throughput tương tự. Gap là cấu trúc, không tunable:
dangvantuanđã fine-tune trên STS (similarity đối xứng) — mạnh trên benchmark như VN-STS nhưng task retrieval câu hỏi→tài liệu asymmetric ngoài phân phối.bkaiđã train với MultipleNegativesRankingLoss trên cặp Q→Doc từ MS MARCO + SQuAD v2 + 80 % Zalo Legal — chính xác task ta chạy.
Catch: bkai cần preprocessing word-segmenter (từ multi-syllable VN nối bằng underscore). Class nom.embeddings.BKaiEmbedder wrap underthesea để làm điều này tự động. Cài: pip install "nom-vn[embeddings,nlp]".
Cross-check: số Zalo Legal corpus đầy đủ bkai công bố (model card) báo Acc@1 73.28, Acc@10 93.59, MRR 80.73. Subset 5k của ta (76.25, 98.75, 0.8604) hơi cao hơn vì subset có ít distractor — order of magnitude consistent. Không có divergence methodology.
Hành động cho v0.2.x: thêm BKaiEmbedder opt-in, KHÔNG đổi default trong nom.rag / nom.retrieve — sẽ invalidate cache embedding đã persist của mọi người dùng hiện tại. Bản major 0.3.x sẽ lật default; bây giờ opt-in giữ tương thích cache.
from nom.embeddings import BKaiEmbedder
from nom.rag import RAG
rag = RAG(embedder=BKaiEmbedder(device="cuda"))Tái lập: python benchmarks/rag/bench_embedder_compare.py --json benchmarks/results/baseline_embedder_compare_zalo5k.json
Lưới mô hình RAG VN — đo 2026-04-25
Hai fixture, đều sample từ GreenNode/zalo-ai-legal-text-retrieval-vn (MIT). Hardware: NVIDIA RTX 3080 Laptop, fp16, warmup=1, timed=1-2 (best-of-N theo rule verified-benchmarks).
Corpus đầy đủ — vn_legal_zalo_full.json (61.068 bài, 82.696 chunk, 788 câu hỏi)
| Retriever | recall@1 | recall@3 | recall@5 | recall@10 | mrr@10 | p50 ms |
|---|---|---|---|---|---|---|
| BM25 | 0.395 | 0.664 | 0.725 | 0.780 | 0.535 | 430 |
| Dense (dangvantuan) | 0.237 | 0.379 | 0.466 | 0.537 | 0.328 | 18 |
| Hybrid (RRF) | 0.368 | 0.602 | 0.690 | 0.783 | 0.505 | 491 |
| Hybrid + bge-reranker-v2-m3 | 0.572 | 0.802 | 0.846 | 0.868 | 0.688 | 1539 |
Corpus subset — vn_legal_zalo_5k.json (5.061 bài, 6.833 chunk, 80 câu hỏi)
| Embedder | Retriever | recall@1 | recall@3 | recall@10 | mrr@10 | p50 ms | p95 ms |
|---|---|---|---|---|---|---|---|
dangvantuan/vietnamese-embedding (768-d, ~440 MB) | BM25 only | 0.762 | 0.912 | 0.975 | 0.843 | 27 | 48 |
| Dense only | 0.412 | 0.725 | 0.863 | 0.585 | 15 | 25 | |
| Hybrid (RRF) | 0.650 | 0.875 | 0.975 | 0.780 | 59 | 113 | |
+ BAAI/bge-reranker-v2-m3 | 0.863 | 1.000 | 1.000 | 0.931 | 681 | 747 | |
+ namdp-ptit/ViRanker | 0.850 | 0.963 | 1.000 | 0.913 | 687 | 743 | |
AITeamVN/Vietnamese_Embedding (1024-d, ~2.3 GB, BGE-M3 base) | BM25 only | 0.762 | 0.912 | 0.950 | 0.843 | 24 | 41 |
| Dense only | 0.825 | 0.963 | 0.975 | 0.894 | 47 | 77 | |
| Hybrid (RRF) | 0.800 | 0.963 | 0.975 | 0.884 | 97 | 131 | |
+ BAAI/bge-reranker-v2-m3 | 0.863 | 0.988 | 0.988 | 0.923 | 720 | 786 | |
+ namdp-ptit/ViRanker | 0.863 | 0.963 | 0.988 | 0.914 | 718 | 799 |
Tái lập: bash benchmarks/rag/run_grid.sh. JSON baseline per-config dưới benchmarks/rag/baselines/zalo_5k__*.json và mirror tới nrl-ai/vn-rag-bench.
Phát hiện
- Lựa chọn embedder quan trọng hơn lựa chọn reranker — cho stage bi-encoder. Đổi từ
dangvantuansangAITeamVNgấp đôi dense recall@1 (0.412 → 0.825). Fine-tune BGE-M3AITeamVN/Vietnamese_Embeddingđặc biệt tune trên Zalo Legal QA, thể hiện ở số in-domain. - Reranker hội tụ. Cả
BAAI/bge-reranker-v2-m3vànamdp-ptit/ViRankerđưa recall@1 cuối lên ~0.863 bất kể embedder feeder. Reranker dominate ranking cuối khi bài gold đã trong pool top-30. - Chất lượng peak tốt nhất:
dangvantuan+BAAI/bge-reranker-v2-m3— recall@10 = 1.000 và recall@3 = 1.000 trên fixture này. Affinity BM25 cao hơn của embedder dangvantuan (chân dense yếu nên RRF nghiêng vào BM25) lift trần recall@10. - Tuỳ chọn skip-reranker:
AITeamVNdense một mình được recall@1 = 0.825 ở 47 ms p50 — nhanh khoảng 15× so với +rerank, mất chỉ 4% absolute recall@1. Lựa chọn đúng cho deploy nhạy latency nơi 825/863 chấp nhận được. - BM25 cạnh tranh đến giật mình trên VN pháp lý — ở quy mô corpus nhỏ. Trên subset 5k BM25 chạm recall@1 = 0.762, nhưng trên corpus 61k đầy đủ rớt xuống 0.395. Hiệu ứng size-corpus dominant cho retrieval lexical; stage dense / reranker quan trọng hơn khi pool distractor lớn.
- Reranker trở nên critical hơn ở quy mô, không kém. Đi từ hybrid → hybrid+rerank lift recall@1 0.213 absolute trên subset 5k và 0.204 absolute trên corpus 61k đầy đủ — proportionally lift relative lớn hơn nhiều trên corpus đầy đủ (+55% relative vs +33% relative).
- BM25 pure-Python là bottleneck ở quy mô. Trên corpus 61k đầy đủ BM25.search() v0.2.5 chạy 430ms p50 — chậm hơn nhiều dense trên GPU (18ms). v0.2.6 swap sang
bm25s(MIT, scipy.sparse): cùng recall y hệt bit, search nhanh hơn 607× (0.7ms p50). Xembenchmarks/results/bm25_compare__zalo_full.jsoncho bảng đầy đủ. Chân dense giờ là bottleneck per-query.
Cross-check so với số đã công bố (theo rule cross-check-against-published-numbers)
- Multi-stage IR cho VN Legal (PKAW 2022, arXiv:2209.14494): báo F2 = 0.741 trên corpus Zalo đầy đủ với PhoBERT-large + sqrt(BM25)·cos hybrid + 3 round mining hard-negative. recall@10 = 0.868 trên corpus 61k đầy đủ của ta implied F2 tương đương (≈0.6-0.7), đạt được có sẵn với bge-reranker-v2-m3 — không fine-tune. Alignment hợp lý.
- UIT 2024 (arXiv:2507.14619): Vietnamese-bi-encoder + PhoRanker, MRR@10 cross-encoder = 79.11% trên 261k doc pháp lý. MRR@10 = 0.688 của ta trên corpus 61k đầy đủ — ~10 điểm thấp hơn; giải thích bởi (a) ta dùng bge có sẵn thay PhoRanker đã fine-tune trên dữ liệu pháp lý, và (b) hiệu ứng size-corpus chạy cả hai chiều. Thêm PhoRanker vào lưới là bước tiếp theo hợp lý (excluded đến nay vì dep VnCoreNLP Java).
- Model card AITeamVN/Vietnamese_Embedding: claim +27.9% Acc@1 so với BGE-M3 base trên retrieval domain pháp lý. dense Acc@1 = 0.825 của ta trên subset 5k vs BGE-M3 base (chưa test) — cần bench BGE-M3 trên cùng fixture để xác nhận size lift. Mở: thêm BGE-M3 vào lưới để verify lợi thế công bố của fine-tune AITeamVN.
- PhoRanker NDCG@10 = 0.7422 trên MMARCO-VI (model card): chưa đo — PhoRanker cần VnCoreNLP (JVM Java), excluded khỏi lưới này có chủ đích.
Config khuyến nghị (default trong nom-vn v0.2.5)
from nom.rag import RAG, CrossEncoderReranker
rag = RAG.from_documents(
docs,
llm=Ollama(model="qwen3:8b"),
embedder=VietnameseEmbedder(), # 440 MB, dim 768
reranker=CrossEncoderReranker(), # bge-reranker-v2-m3
)
answer = rag.ask(question, rerank=True, rerank_candidates=30)Cho deploy bound latency không có GPU, drop reranker và dùng AITeamVNEmbedder() (dense tốt hơn, không có thuế cross-encoder).
Module: nom.doc.ocr — baseline thật đo 2026-04-26
Làm gì
Chạy engine OCR trên ảnh tiếng Việt (page PDF, scan, ảnh) và trả về text thuần. v0.2.x ship đường Tesseract; phiên bản sau sẽ thêm tuỳ chọn VLM và VN-specialised khi chúng kiếm được trọng lượng dependency trên bench.
Lưới engine OCR tiếng Việt — đo 2026-04-26
Corpus thật: vn_ocr_subset — 478 ảnh sample tất định (seed=42) từ ducto489/ocr_datasets shard 0 (Apache-2.0), filter các hàng chứa dấu VN và ít nhất 8 ký tự ground-truth text. Hầu hết là prose machine-rendered ở các mức nhiễu khác nhau — đại diện cho input OCR tài liệu thực.
Hardware: CPU (8 cores, không có contention GPU với bench RAG), warmup=1, timed=2, p50/p95 báo best-of-N.
| Engine | License | CER | WER | diacritic-CER | exact match | p50 ms | p95 ms |
|---|---|---|---|---|---|---|---|
Tesseract 5 (vie traineddata) | Apache-2.0 | 0.0819 | 0.3771 | 0.1193 | 0.345 | 447 | 656 |
EasyOCR 1.7 (vi) | Apache-2.0 | 0.1176 | 0.5304 | 0.2052 | 0.218 | 183 | 431 |
JSON baseline dưới benchmarks/results/ocr_vn_subset__*.json và mirror tới nrl-ai/vn-rag-bench.
Phát hiện
- Fixture synthetic không phải benchmark.
synthetic_ocr_vi/cleancho Tesseract CER = 0.000 / exact = 1.000 — hoàn hảo.synthetic/noisycho CER = 0.0064. Cả hai đều quá dễ để rank engine. Dữ liệu ducto489 thật drop Tesseract xuống CER = 0.082 — đó là baseline trung thực. - Diacritic-CER (11.9%) tệ hơn ~46% so với CER tổng (8.2%) — xác nhận mode fail mà người đọc tiếng Việt cảm thấy. Dấu thanh (sắc, huyền, hỏi, ngã, nặng) là 1–3 pixel và là thứ đầu tiên OCR mất trên scan nhiễu. Một reranking diacritic-aware hoặc fix post-OCR sẽ giúp ở đây.
- Latency ~450 ms per ảnh trên 8 CPU cores. Tesseract là C++ bên dưới và không parallel trong một page; cải thiện throughput đến từ chạy nhiều page parallel ở mức pipeline, không phải tune internals Tesseract.
- Tesseract thắng EasyOCR trên mọi metric chất lượng cho VN. CER 8.19% vs 11.76%, diacritic-CER 11.93% vs 20.52%, exact-match 34.5% vs 21.8%. EasyOCR nhanh hơn 2.4× (183 ms vs 447 ms p50) nhưng gap accuracy dominate cho use case Q&A tài liệu — mất 13% absolute exact-match cho 264 ms latency là trade tệ. Default giữ Tesseract. EasyOCR có thể hữu ích cho use case bulk-indexing throughput cao nơi accuracy đánh đổi được; chúng tôi surface cả hai option trong
bench_ocr_real.py.
Engine đã khảo sát nhưng chưa đo
- VietOCR (Apache-2.0, Transformer VN-specialised) —
pip install vietocrlỗi trên Python 3.13 (KeyError: '__version__'trong setup.py). Pin cho theo dõi tiếp; phía trên cầnpyproject.tomltương thích Python-3.13. - PaddleOCR PP-OCRv5 (Apache-2.0, lightweight ~150 MB) — ứng viên hứa hẹn nhất tiếp theo. CER báo ~0.94 trên OmniDocBench multilingual; không VN-specific nhưng thường thắng Tesseract trên text rendered.
- Surya OCR — code là GPL-3.0, mô hình open-RAIL-M. Cả hai license-incompatible với surface mặc định Apache-2.0. Sẽ bench cho so sánh; không thể ship làm default.
VLM OCR — đo 2026-04-26
Test xem Vision-Language Model general-purpose có thể match OCR purpose-built trên transcription line-image VN.
Engine: qwen2.5vl:3b và qwen2.5vl:7b (Apache-2.0) qua Ollama 0.21.2 trên RTX 3090. Quantize Q4_K_M. Prompt: VN tight "transcribe exactly, no chatter" (xem OllamaVLM trong benchmarks/accuracy/bench_ocr_real.py). Output trim defensive cho think-tag, code-fence, và label echo.
Corpus: 50 ảnh đầu từ vn_ocr_subset (sample từ ducto489/ocr_datasets, Apache-2.0). Single-line text VN print sạch — cùng ảnh chạy trên Tesseract và EasyOCR cho so sánh trực tiếp.
| Engine | Q4 size | CER | WER | Diacritic CER | Exact match | p50 ms | p95 ms |
|---|---|---|---|---|---|---|---|
| Tesseract 5 (vie) | ~30 MB | 5.53% | 26.78% | 9.71% | 38.0% | 80.6 | 110.5 |
| EasyOCR (vi) | ~150 MB | 9.39% | 43.86% | 19.84% | 18.0% | 31.1 (GPU) | 68.3 |
| qwen2.5vl:7b | 6.0 GB | 31.07% | 140.04% | 33.38% | 18.0% | 818.0 | 1332.2 |
| qwen2.5vl:3b | 3.2 GB | 39.86% | 175.43% | 41.82% | 15.0% | 1165.5 | 3993.6 |
Phát hiện:
- VLM thua quyết định trên OCR single-line sạch. qwen2.5vl:7b có CER 31% vs Tesseract 5.53% — gap 25 điểm. Mô hình hallucinate: "1892 - Tạp Chí Vogue..." → "1892 92 92 92 92..." (loop token), "XÃ CHIỀNG ƠN" → "CHÍNH XÁC", "churchill và tưởng giới thạch" → "Churchill và tướng Eisenhower cùng được trao giải thưởng" (cả câu plausible-nhưng-bịa).
- Tool đúng vẫn là tool đúng. VLM train trên page đầy đủ; trên crop dòng tight không có context tài liệu, prior ngôn ngữ dominate signal visual và mô hình drift sang mode "complete-the-sentence". Head CTC của Tesseract purpose-built cho alignment glyph trái-sang-phải và không có mode fail này.
- Latency: VLM chậm hơn 10× (818 ms vs 80 ms p50). Cho batch 478 ảnh đây là 6.5 phút vs 39 giây.
- Use case của VLM trong OCR là chỗ khác. Extraction multi-field (field hoá đơn, CCCD, form có checkbox), chữ viết tay scan, và workflow "OCR + hiểu text" là chỗ VLM kiếm cost. Đã document để user không chộp lấy
qwen2.5vlmong đợi nó thắng Tesseract trên ảnh dòng đơn giản.
Khuyến nghị: OCR mặc định giữ Tesseract. VLM OCR hợp khi task phía sau là hiểu tài liệu, không phải transcribe nó — surface là một đường nom.doc.vlm_extract() riêng trong release tương lai, không phải backend OCR swap-in.
Tái lập: python benchmarks/accuracy/bench_ocr_real.py --corpus benchmarks/data/vn_ocr_subset --variant none --engines ollama_vlm --ollama-model qwen2.5vl:7b --ollama-base-url http://localhost:11434 --limit 50 Baseline: benchmarks/results/baseline_ocr_vlm_qwen25vl_7b.json, baseline_ocr_tesseract_50.json, baseline_ocr_easyocr_50.json.
Config khuyến nghị (default v0.2.x)
from nom.doc import Pipeline
# Tesseract đã wire vào nom.doc.OCR mặc định; cài vie traineddata
# qua `apt install tesseract-ocr-vie` (hoặc brew).
pipeline = Pipeline()
text = pipeline.run("scanned.pdf").text