PaperMate / docs /parser /ocr-evaluation.md
ntphuc149's picture
refactor: reorganize docs/ into database/ and parser/ subdirectories
7fe0356
|
Raw
History Blame
21.4 kB

Đánh giá chất lượng OCR — PeerRead ACL 2017

Tài liệu này mô tả toàn bộ cơ chế: OCR paper PDF bằng Docling → chuẩn hóa output về đúng định dạng bộ test → đo chất lượng bằng bộ metric theo từng loại dữ liệu.

Ground truth là PeerRead ACL-2017 test set (test/parsed_pdfs/, 7 paper) — JSON do GROBID/CRF sinh, ở dạng plain-text.


1. Mục tiêu

Cho một paper PDF, sinh ra JSON đúng schema PeerRead (giống hệt các file trong test/parsed_pdfs/), rồi so sánh với ground truth để biết OCR tốt tới đâu ở từng phần: text, bảng, công thức, thứ tự đọc, references, citations.


2. Tổng quan pipeline

PDF
 │  (1) OCR: Docling standard pipeline + formula enrichment + TableFormer
 ▼
DoclingDocument  (text, bảng→cấu trúc, công thức→LaTeX)
 │  (2) Linearize: _docling_doc_to_text  → plain-text giống template GT
 ▼
Markdown phẳng  (bảng phẳng, công thức unicode, heading #/##)
 │  (3) Parse: md_to_peerread  → JSON PeerRead
 ▼
{id}.pdf.json   (title, sections, references, referenceMentions, ...)
 │  (4) Compare: run_compare  → metric theo từng loại dữ liệu
 ▼
Bảng điểm

Toàn bộ logic ở scripts/test_docling_ocr.py. Hai notebook Kaggle (kaggle_docling_batch_ocr.ipynb, kaggle_docling_server.ipynb) dùng chính xác cùng các hàm này.


3. OCR — Docling standard pipeline + formula enrichment

Dùng standard PDF pipeline của Docling (KHÔNG dùng VLM granite, vì granite không decode được công thức — trả <!-- formula-not-decoded -->):

po = PdfPipelineOptions()
po.do_ocr = False                                              # PDF digital: dùng text layer có sẵn
po.do_table_structure = True
po.table_structure_options = TableStructureOptions(mode=TableFormerMode.ACCURATE)
po.do_formula_enrichment = True                               # decode công thức → LaTeX (model CodeFormula)
po.accelerator_options = AcceleratorOptions(device=CUDA)
  • Text: lấy từ text-layer của PDF + model layout (đúng reading order).
  • Bảng: model TableFormer (ACCURATE) → cấu trúc ô.
  • Công thức: model CodeFormula (bật bằng do_formula_enrichment) → LaTeX. Đây là điểm mấu chốt để công thức không còn là placeholder.

4. Chuẩn hóa output về template ground-truth (_docling_doc_to_text)

GT là plain-text, nên KHÔNG dùng export_to_markdown() (nó sinh pipe-table, $$LaTeX$$, <!-- image -->, <!-- formula-not-decoded -->). Thay vào đó linearizer tự duyệt doc.iterate_items() và xuất plain-text giống GT:

Phần tử Xử lý Kết quả (giống GT)
Công thức _latex_to_plain: LaTeX→unicode (greek, ∑∏∫, \fraca/b, bỏ _/^ giữ nội dung) ri = σ(Wr xi + br) thay vì $$...$$
Bảng _table_to_plain: làm phẳng cả bảng về 1 dòng, cell cách space Corpus # words # chunks ... Test 54,088 - 1,812
Caption giữ lại Table 1: ..., Figure 2: ... inline trong text
Heading giữ # /## để bước parse tách section
Ảnh / page header / footer bỏ (GT cũng không có)
HTML comment xóa sạch (<!--...-->) không bao giờ lọt placeholder

_latex_to_plain không phải renderer LaTeX đầy đủ — chỉ đủ để khớp văn bản plain của GT.


5. Trích xuất metadata (md_to_peerread)

Markdown phẳng → JSON PeerRead, theo đúng quy ước của GT:

  • name = "<id>.pdf".
  • Thứ tự key: source, title, authors, emails, sections, references, referenceMentions, year, abstractText, creator.
  • sections: loại bỏ block tiêu đề, Abstract (chỉ nằm ở abstractText), References (chỉ nằm ở references[]); giữ phần Appendix; giữ cả heading có body rỗng (giống GT).
  • references (author-year ACL Authors. Year. Title. Venue.): parse ra title/author/venue/year + citeRegEx/shortCiteRegEx đúng kiểu GROBID (Apidianaki.,? 2016, Baroni and Lenci.,? 2011, Arora et al\.,? 2016).
  • referenceMentions: match citeRegEx trong body → mention theo thứ tự xuất hiện, context là trọn câu (sentence-bounded) + offset, map về referenceID.

6. Cơ chế đo (metrics)

6.1. Nguyên tắc chung: chuẩn hóa CẢ HAI bên, chấm lỏng về khoảng cách

GT (GROBID) mang nhiễu mà output sạch không có: số dòng lề, số trang, mojibake (σ→Ï, −→â1), và OCR hay chèn space giữa ký tự (h i -1, s ( c ) k). Nếu so thô, output sạch bị phạt oan. Nên trước khi đo, cả GT lẫn output đều được đưa về dạng chuẩn (_loose):

  1. Bỏ dòng chỉ chứa số (số dòng lề / số trang).
  2. Fold non-ASCII (mojibake và ký hiệu unicode đều quy về ASCII) → công bằng.
  3. Lowercase.
  4. Xóa toàn bộ khoảng trắng → chấm lỏng về spacing (đo nội dung ký tự, không đo layout).

6.2. Bộ metric — mỗi loại dữ liệu một cách đo

Tất cả 0–1, càng cao càng tốt, đo trên text đã chuẩn hóa.

Loại dữ liệu Metric Cách đo
Text token recall ↑ Tỉ lệ token văn xuôi GT được tìm thấy trong output sau chuẩn hóa lỏng; không phạt output trích thêm caption/figure/boilerplate hữu ích
Table data-value coverage ↑ Tỉ lệ giá trị dữ liệu (số thập phân / % / số ≥2 chữ số hoặc có dấu phẩy) GROBID đặt trong dòng-bảng (đã lọc dòng-rác) xuất hiện ở bất kỳ đâu trong output. Bỏ số 1 chữ số (GROBID đọc dấu ✓/† thành 7). Robust với khác biệt flatten/spacing và dòng GT bị nhân bản
Formula recall ↑ min(#'=' pred, #'=' GT) / #'=' GT, chỉ đếm trên dòng không-rác (một = mojibake trong figure đọc sai sẽ không thổi phồng mẫu số GT)
Order LCS recall ↑ Độ dài chuỗi con chung dài nhất của dãy heading / số heading GT — kiểm tra heading GT có xuất hiện đúng thứ tự
References recall ↑ Recall trên tập khóa (last-name, year); precision/F1 xem trong detail
Mentions recall ↑ recall mention sau khi map referenceID(last-name, year)

Bảng chi tiết kèm theo:

  • TblP/TblR/TblF ↑ : precision/recall/F1 trên multiset số trong bảng.
  • TblTxt ↑ : char-similarity trên bảng đã làm phẳng, chỉ để diagnostic vì dễ phạt oan khi pred dài/sạch hơn GT.
  • TxtP/TxtF/TxtChr ↑ : precision/F1 token văn xuôi và char-similarity cũ, chỉ để diagnostic.
  • Ref P/R, Mention P/R.

Phân biệt loại metric: recall/coverage (Text/Table/Formula/Refs/Mentions) lo độ bao phủ; LCS recall (Order) lo thứ tự; char-similarity (TxtChr/TblTxt) chỉ là diagnostic.

6.3. Vì sao chọn các metric này

  • Text → token recall thay cho CER/char-similarity: CER và char-sim phạt mạnh khi output sạch hơn hoặc trích thêm caption/figure/boilerplate mà GT không có. Token recall hỏi đúng câu hỏi OCR chính: “bao nhiêu nội dung văn xuôi GT đã được bắt lại?”. Precision/F1/TxtChr vẫn ở detail để phát hiện dư/rác.
  • Table → data-value coverage thay cho char-similarity/cell-F1 và thay cho max(P,R) trên bucket bảng phía pred: GROBID và Docling làm phẳng bảng rất khác nhau (space-join vs dot-join), nên phân loại dòng-bảng độc lập ở phía pred là không đáng tin — cách cũ từng chấm một paper 0.000 dù output bắt đủ 100% số (paper 768). Cách mới chỉ lấy giá trị dữ liệu GROBID đặt trong bảng (đã lọc dòng mojibake) rồi đo bao nhiêu giá trị đó xuất hiện ở bất kỳ đâu trong output → recall trên tập giá trị, không phụ thuộc cách flatten. Giữ TblP/TblR/TblF/TblTxt để diagnostic.
  • Formula → recall: công thức không thể khớp ký tự 100% với GT (GT là text-layer: K∏ xuống dòng, số (15), mojibake) nên đo "bắt đủ công thức không" qua số dấu =, chỉ đếm trên dòng không-rác để một = mojibake trong figure đọc sai không thổi phồng mẫu số GT.
  • Order → LCS recall, Refs → recall, Mentions → recall: chấm theo độ bao phủ GT. Extra heading/ref/citation được đưa xuống detail precision, không kéo headline xuống. Khi --compare, mention của cả GT lẫn output được rebuild từ abstractText + sections + references bằng regex hiện tại để các fix parser (ví dụ Author (Year), 2016a/2016b) có hiệu lực mà không cần OCR lại PDF và không phạt pred theo quirks của mention extractor cũ trong GT.

6.4. Kiểm chứng công cụ chấm

So GT-với-GT cho điểm tuyệt đối (Text/Table/Formula/Order = 1.0, mọi F1/recall = 1.0) → công cụ chấm không sai. Đã test thêm với input cố tình lỗi → metric phản hồi đúng.


7. Cách chạy

7.1. Batch OCR trên Kaggle (khuyến nghị) — kaggle_docling_batch_ocr.ipynb

  1. Tạo Dataset Kaggle chứa các PDF (vd test/pdfs/), add vào notebook.
  2. Settings → Accelerator = GPU, Internet = On. Run All (lần đầu tải model ~vài phút).
  3. Tải docling_output.zip ở tab Output.
  4. Giải nén các *.pdf.json vào test/docling_output/, rồi chạy bước so sánh (7.3).

7.2. Server trên Kaggle — kaggle_docling_server.ipynb

  1. Run All (GPU + Internet). Cell cuối expose URL qua Cloudflare Tunnel + publish lên ntfy.sh (topic khớp NTFY_TOPIC trong .env). Giữ notebook mở.
  2. Trên máy (PowerShell):
    cd C:\Users\DELL\Desktop\paper-review\docs
    $PY = "C:\Users\DELL\AppData\Local\Programs\Python\Python312\python.exe"
    $env:PYTHONIOENCODING = "utf-8"
    & $PY scripts\test_docling_ocr.py --provider docling-granite      # gọi server -> test/docling_output/
    

7.3. So sánh với bộ test

cd C:\Users\DELL\Desktop\paper-review\docs
$PY = "C:\Users\DELL\AppData\Local\Programs\Python\Python312\python.exe"
$env:PYTHONIOENCODING = "utf-8"
& $PY scripts\test_docling_ocr.py --compare

7.4. Provider khi chạy local

Provider Mô tả
docling-standard-local standard pipeline + formula enrichment (decode công thức) — cần GPU
docling-granite gọi server Kaggle (server hiện chạy standard+formula)
docling-granite-local Granite VLM trên máy (KHÔNG decode công thức)
local PyMuPDF baseline
& $PY scripts\test_docling_ocr.py --provider docling-standard-local

Flags: --pdf-dir (mặc định test/pdfs), --out-dir (test/docling_output), --gt-dir (test/parsed_pdfs), --overwrite, --compare.


8. Kết quả hiện tại (5 paper)

Metric áp dụng cho từng cột:

Cột Metric áp dụng Ý nghĩa ngắn
Text text_r — token recall trên prose Tỉ lệ token văn xuôi GT được tìm thấy trong output
Table tbl_num_cov — data-value coverage Tỉ lệ giá trị dữ liệu bảng trong GT xuất hiện ở output, sau khi lọc dòng-rác
Formula formula — equation recall bằng dấu = Tỉ lệ phương trình GT được bắt lại, chỉ đếm trên dòng không-rác
Order order — heading LCS recall Tỉ lệ heading GT xuất hiện đúng thứ tự trong output
Refs ref_r — reference recall Tỉ lệ reference GT khớp theo khóa (first-author last-name, year)
Mentions men_r — citation mention recall Tỉ lệ citation mention GT được map đúng về reference key
ID       Text  Table  Formula  Order   Refs  Mentions
323     0.978  1.000    1.000  1.000  1.000     1.000
355     0.974  1.000    1.000  1.000  1.000     1.000
435     0.949  1.000    1.000  1.000  0.960     1.000
49      0.966  0.947    1.000  1.000  1.000     1.000
768     0.932  0.981    1.000  1.000  1.000     1.000
AVG     0.960  0.986    1.000  1.000  0.992     1.000
  • Formula 1.000: output decode đủ mọi phương trình (paper 49 decode trọn (1)–(22), còn chính xác hơn GT). Điểm 0.968 cũ là do GT có 1 dấu = mojibake trong figure đọc sai — đã loại bằng bộ lọc dòng-rác.

  • Table 0.986 theo coverage giá trị dữ liệu. Cải thiện lớn so với 0.772 cũ:

    • Paper 768 từ 0.000 → 0.981: lỗi cũ là artifact của metric (Docling nối cell bằng . nên heuristic phân loại nhầm thành prose), không phải OCR sai — output bắt đủ 211/211 số.
    • Paper 435 từ 0.955 → 1.000: số GT cũ (7,929/15,534) thực ra là GROBID đọc SAI; bảng thật là 7,606 / 79,290 / 86,896Docling đọc đúng.
    • Paper 49 0.947: còn vài token số rác CJK trong figure GROBID gộp nhầm vào bảng; bảng thật đã match đủ.

    Truy vết theo metric hiện tại (tbl_num_cov = distinct GT table data-values found anywhere in pred):

    ID Table Hit / GT values Missing values Lý do
    323 1.000 29 / 29 Bắt đủ mọi data-value GT sau lọc dòng-rác
    355 1.000 57 / 57 Bắt đủ mọi data-value GT sau lọc dòng-rác
    435 1.000 79 / 79 Metric mới không phạt các số GT cũ bị GROBID làm méo; các giá trị GT-clean đều xuất hiện trong output
    49 0.947 18 / 19 34 34 đến từ rác figure/CJK trong GT (9:34;+/ ..., %34$ ...), không phải số bảng thật bị mất
    768 0.981 102 / 104 0.5, 0.25 Hai số này nằm ở dòng baseline Random 0.5 0.5 0.5 0.5 0.25; phần data-value score chính gần như đã bắt đủ
  • Text 0.960, Refs 0.992 (chỉ lệch ở Pylkkänen 2008 do PDF tách dấu ¨), Order 1.000.

  • Mentions 1.000 recall sau khi rebuild đối xứng cho cả GT/pred từ abstractText + sections. Detail MenP vẫn cho biết pred có bắt thêm citation ngoài tập GT-normalized hay không.

Lưu ý diễn giải: headline là độ bao phủ (recall/coverage), không phải "độ chính xác OCR". Các điểm < 1.000 còn lại hầu hết do GROBID ground truth bị lỗi/nhiễu (đọc sai số, mojibake, gộp figure/CJK vào bảng) chứ không phải Docling sai — ở nhiều chỗ Docling còn đúng hơn GT.

Câu báo cáo khuyến nghị

Text      0.960
Table     0.986
Formula   1.000
Order     1.000
Refs      0.992
Mentions  1.000

Trên 5 paper hiện có, OCR/parser đạt recall/coverage cao trên mọi kênh: text, table, formula, heading order, references và citation mentions. Headline đo độ bao phủ ground truth, không phải "độ chính xác OCR" tuyệt đối. Các điểm < 1.000 còn lại hầu hết do GROBID ground truth bị lỗi/nhiễu (đọc sai số ở 435, mojibake/figure ở 49) — nhiều chỗ Docling còn chính xác hơn GT — và một lỗi parse tên có dấu thật sự ở reference Pylkkänen 2008. Bản đo trước (Table 0.772, Formula 0.994) thấp giả tạo do hai lỗi metric đã được sửa (xem mục 9).


9. Tại sao KHÔNG đạt tuyệt đối 1.000? (nguyên nhân theo từng điểm)

Điểm số đo mức khớp với ground-truth GROBID — mà bản thân GROBID không hoàn hảo. Vì vậy < 1.000 không đồng nghĩa OCR sai. Phân loại nguyên nhân thành 3 nhóm:

  • 🟥 GT lỗi/nhiễu — ground truth GROBID tự nó sai hoặc rác (KHÔNG phải lỗi pred).
  • 🟦 Tokenization/normalization — nội dung tương đương nhưng tách token khác (KHÔNG mất nội dung).
  • 🟧 Pred thật sự lệch — output Docling thật sự thiếu/sai (lỗi cần sửa).
Metric Điểm Nguyên nhân gốc (đã truy về dữ liệu thật) Nhóm
Formula 1.000 Decode đủ mọi phương trình; không còn lỗi
Order 1.000 Mọi heading GT xuất hiện đúng thứ tự
Mentions 1.000 Recall citation→reference đầy đủ
Text 0.960 (1) Biến công thức có subscript tokenize khác: GT gộp w_l→wl, h_i→hi, s_j→sj, GRU, x_i; output giữ spacing khác → cùng nội dung, khác token (chiếm phần lớn token "thiếu" ở 49/768/323). (2) GROBID cắt vụn chữ: paper 435 GT có mảnh so, ca, th, fo, re, e1, e2 do text-layer vỡ chữ — output sạch không tái tạo. (3) Vài số/ký hiệu inline. → Không phải mất văn xuôi 🟦 phần lớn + 🟥 (435)
Table 0.986 Paper 49 (0.947): còn vài token số rác CJK figure GROBID gộp vào bảng (34 từ 9:34;+/). Paper 768 (0.981): thiếu vài hằng số baseline lặp (0.5/0.25 của dòng "Random") — số dữ liệu thật đã bắt đủ 211/211 🟥
Refs 0.992 Chỉ paper 435: reference Liina Pylkk¨ anen. → parse thành anenPDF text-layer tách dấu ¨ rời ra + space, _last_name lấy token cuối. 1/25 ref 🟧 (lỗi pred thật duy nhất)

Tóm lại — vì sao không full 1.0:

  1. Text 0.960: không phải OCR bỏ chữ, mà do cách tokenize biến công thứcGROBID cắt vụn chữ ở GT. Đây là sàn nhiễu cố hữu khi so token với GROBID, không kéo lên 1.0 được trừ khi nới lỏng tokenization (sẽ làm metric kém nhạy).
  2. Table 0.986 / 49 còn 0.947: GT trộn rác figure CJK vào bảng; bộ lọc dòng-rác đã bỏ phần lớn nhưng vài token tọa độ figure (9:34) vẫn lọt vì chỉ gồm ký tự "sạch".
  3. Refs 0.992: lỗi pred thật duy nhất — parser chưa gộp dấu phụ (diacritic) bị PDF tách rời. Sửa được nếu cần (xem mục 9-fix bên dưới).

Kết luận: trong 3 điểm chưa tuyệt đối, 2 (Text, Table) là do GT nhiễu + cách đo, chỉ 1 (Refs, 1 ref) là lỗi pred thật. Không có điểm nào phản ánh "OCR đọc thiếu nội dung".

9-fix. Nếu muốn đẩy Refs lên 1.000

Sửa _parse_reference/_last_name: trước khi tách tên, gộp dấu phụ bị tách (Pylkk¨ anenPylkkänen) bằng cách bỏ khoảng trắng quanh modifier diacritic (¨ ´ ˜ ^) rồi unicodedata.normalize("NFC")`. Đây là thay đổi ở parser, không phải metric.


10. Giới hạn đã biết (do GT / model, không phải lỗi format)

Vấn đề Mô tả
sections[0] heading=null GT luôn có 1 block rác số dòng lề; output sạch không tái tạo → căn eval theo heading, không theo index
authors/emails GT luôn []; parser của ta có trích → khác nhau (không nằm trong metric chính)
Công thức char-exact Không thể khớp 100% ký tự (GT là text-layer lỗi) → đo bằng recall
GT đọc sai số (vd 435) GROBID có thể đọc sai số trong bảng (79,2907,929); Docling đúng nhưng nếu so cứng sẽ bị phạt → dùng coverage tập giá trị, lọc dòng-rác
GT gộp figure/CJK vào bảng (vd 49) GROBID flatten figure/caption CJK thành dòng "bảng" mojibake → lọc bằng _is_junk_line trước khi chấm
Mention precision chưa tuyệt đối Pred có thể bắt thêm citation từ references mà GT bỏ sót; headline dùng recall nên không phạt phần dư này

11. Cấu trúc thư mục

paper-review/
├── .env                                   # NTFY_TOPIC (cho server mode)
└── docs/
    ├── ocr-evaluation.md                  # tài liệu này
    ├── kaggle_docling_batch_ocr.ipynb     # Kaggle: batch OCR → JSON (khuyến nghị)
    ├── kaggle_docling_server.ipynb        # Kaggle: server FastAPI + Cloudflare Tunnel
    ├── scripts/
    │   └── test_docling_ocr.py            # OCR + linearize + parse + metric
    └── test/
        ├── pdfs/                          # PDF đầu vào (7 paper)
        ├── parsed_pdfs/                   # ground truth (GROBID/CRF)
        └── docling_output/                # output JSON để so sánh