Trao đổi về dự án
Trí Tuệ Nhân Tạo

Tái tạo các ô của bảng PDF với Table Transformer

Biến một vùng cắt PDF thành lưới, ô gộp và bản xuất có thể kiểm tra với TATR.

Définir le crop → Reconstruire la grille → Contrôler les cellules. Tableaux PDF

Bạn cần bổ sung chức năng nhập bảng PDF vào một ứng dụng web. Bạn bắt đầu bằng việc trích xuất văn bản: các từ và cả các con số đều có ở đó. Thế nhưng, khi dữ liệu được đưa vào bảng tính, một khoản tiền lại nằm sai cột, một tiêu đề xuất hiện hai lần và một ô trống biến mất. Việc trích xuất đã đọc được văn bản, nhưng chưa khôi phục được cách tổ chức của nó.

Đó là vấn đề chúng ta sẽ tìm hiểu qua báo cáo hư cấu Helix Quarterly Ledger. Bảng của báo cáo có tiêu đề “Quarter 1” gộp trên hai cột, các số thập phân và một ô cố ý để trống. Để tái tạo nó, chúng ta phải trả lời hai câu hỏi khác nhau: các ô nằm ở đâu, rồi từ nào thuộc về từng ô?

Chúng ta sẽ tinh chỉnh Table Transformer cho bài toán nhận diện cấu trúc, hay TATR, rồi dùng Python để tái tạo lưới và gắn các từ của PDF vào đó. Mô hình phát hiện các đối tượng trực quan; mã nguồn biến chúng thành một bảng có thể sử dụng, với các bản xuất HTML, CSV và JSON truy xuất nguồn gốc.

Tải xuống toàn bộ dự án hướng dẫn r003

Quy trình và các kết quả tham chiếu là của dự án r003. Các ví dụ số nhỏ được thêm vào để hiểu phép tính chỉ mang tính sư phạm: chúng không phải là các phép đo mới của mô hình. Tương tự, các hình minh họa cơ chế và kết quả đã quan sát, chứ không xác nhận kết quả trên tài liệu của riêng bạn.

Từ vùng cắt được cung cấp đến các hàng, cột và ô gộp của một bảng Helix.

Những gì chúng ta sẽ xây dựng

Một vùng bảng làm đầu vào, không phải một trang cần khám phá

Đầu vào là một PDF gốc, tức tài liệu có văn bản có thể trích xuất trực tiếp, đi kèm một hình chữ nhật được khai báo trong data/regions.json. Hình chữ nhật này chỉ ra vùng cần xử lý. Một ảnh của vùng đó, gọi là crop, được đưa cho mô hình.

Dự án không tự động tìm bảng trên cả trang. Bạn chỉ cho nó nơi cần quan sát, rồi đánh giá khả năng hiểu cấu trúc những gì nằm trong vùng đó. Tìm lại bảng trên trang sẽ là một bước bổ sung, với các sai số riêng cần đo lường.

Từ crop, TATR dự đoán sáu loại đối tượng: bảng, cột, hàng, tiêu đề cột, tiêu đề chiếu và ô gộp. Mỗi đối tượng đi kèm một hộp và một điểm số. Mã được trình bày bên dưới dùng hàng, cột và ô gộp để tạo lưới; vì thế không phải mọi hạng mục được dự đoán đều có cùng vai trò trong trích đoạn này.

Tên “Transformer” không có nghĩa là chúng ta sẽ yêu cầu một chatbot soạn bảng. Ở đây, đó là mô hình phát hiện trực quan. Tài liệu Table Transformer phân biệt rõ việc phát hiện bảng với việc nhận diện cấu trúc của bảng.

Hai luồng gặp nhau

Một bên, ảnh của vùng cho phép dự đoán các hàng, cột và phần gộp. Bên kia, văn bản gốc cung cấp các từ kèm tọa độ. Chương trình kết hợp hai thông tin này: tạo các ô, sau đó gán từng từ cho một ô hoặc báo rằng nó chưa được gán.

Nếu là lập trình viên web, hãy nghĩ đến một phép nối. Chúng ta có một tập hợp ô và một tập hợp từ. Nhưng các từ chưa có khóa ngoại cell_id: chúng ta phải xác định liên kết này từ hình học.

Kết quả giữ lại mối liên kết này. HTML có thể biểu diễn phần gộp bằng rowspan và colspan. Còn CSV căn các giá trị trong một lưới phẳng; nó không mô tả phần gộp như HTML. Vì vậy hãy lưu cả JSON truy xuất nguồn gốc: nó giữ cấu trúc và các tọa độ cần thiết để quay lại nguồn.

Kho dữ liệu gồm 480 PDF Helix hư cấu, từ 60 họ mẫu: 336 tài liệu huấn luyện, 72 tài liệu xác thực và 72 tài liệu kiểm thử. Các họ được tách riêng giữa những nhóm này. Văn bản bằng tiếng Anh, là văn bản gốc và được tổng hợp. Quy trình này không đánh giá OCR, bản quét hay PDF của khách hàng.

Những mốc cần biết trước khi xem mã

Các hộp trong cùng một hệ tọa độ

Một hộp, hay bounding box, ở đây là hình chữ nhật được mô tả bằng [trái, trên, phải, dưới]. Nó gần với điều bạn thao tác khi định vị một phần tử trong giao diện: chiều rộng, chiều cao và vị trí, được biểu diễn trong một hệ tọa độ nhất định.

Cái bẫy là trộn lẫn các hệ tọa độ. Các từ đến từ PDF; dự đoán đến từ ảnh crop. Cần tính đến gốc của vùng này, tỷ lệ kết xuất và padding, tức phần lề được thêm xung quanh ảnh.

Hãy lấy một ví dụ sư phạm, trong đó mọi tọa độ PDF đã được biểu diễn trong hệ có gốc ở góc trên bên trái. Vùng bắt đầu tại (100, 200). Một điểm của từ nằm tại (120, 230). Trong vùng, nó là (20, 30). Với tỷ lệ hai pixel trên một đơn vị và lề 10 pixel, nó thành (50, 70): ta trừ gốc, nhân hai, rồi cộng lề.

So sánh trực tiếp (120, 230) với một hộp biểu diễn bằng pixel của crop sẽ không nhất quán. Từ đó có thể bị coi là nằm ngoài một ô dù trực quan nó ở bên trong. Phép tính đơn giản này giải thích nhu cầu; để tái tạo chính xác các phép biến đổi, hãy giữ các phép biến đổi của dự án.

Có hàng và cột, nhưng chưa có ô

Một hàng xác định một dải ngang. Một cột xác định một dải dọc. Giao điểm của chúng tạo thành một ô ứng viên. Với ba hàng và hai cột, trước hết ta có sáu ô, ngay cả khi một số ô không chứa văn bản.

Việc gộp làm thay đổi tổ chức này. Tiêu đề “Quarter 1” phải chiếm một ô duy nhất phủ hai cột, chứ không phải hai ô lặp lại cùng văn bản. Chúng ta cũng gọi vùng này là một span: một ô trải trên nhiều vị trí của lưới.

Mã mô tả mỗi ô bằng bốn chỉ số: row_start, row_end, col_start, col_end. Điểm đầu được bao gồm và điểm cuối bị loại trừ, như trong lát cắt Python. Do đó, (0, 1, 0, 2) chỉ hàng có chỉ số 0 và các cột chỉ số 0 và 1. Từ đó suy ra rowspan = 1 - 0 = 1 và colspan = 2 - 0 = 2.

TATR học dự đoán điều gì

TATR dựa trên DETR, viết tắt của DEtection TRansformer. Mô hình biểu diễn ảnh bằng các đặc trưng số, rồi dùng các truy vấn đã học để tạo đề xuất đối tượng. Những truy vấn này là các vectơ được điều chỉnh trong quá trình học, không phải câu hỏi bằng tiếng Pháp hay truy vấn SQL. Các thành phần gọi là đầu dự đoán tạo ra, trong đó có, hạng mục và tọa độ của từng đề xuất. Kho DETR chính thức trình bày kiến trúc này.

Trong quá trình huấn luyện, chúng ta có chú thích: các đối tượng mong đợi, kèm hạng mục và hộp của chúng. Cần so sánh các đề xuất với những tham chiếu này. Nhưng đề xuất đầu tiên không nhất thiết là hàng đầu tiên của bảng; thứ tự đầu ra do đó không đủ để chọn các tương ứng.

Đó là lúc ghép cặp Hungary xuất hiện: tìm một liên kết giữa dự đoán và mục tiêu sao cho tổng chi phí là nhỏ nhất, mà không gán cùng một dự đoán cho hai mục tiêu.

Trong ví dụ được tạo ra này, chi phí càng nhỏ thì tương ứng càng tốt:

Đề xuất Hàng mong đợi A Hàng mong đợi B
P1 9 1
P2 2 8

Ghép các phần tử theo thứ tự xuất hiện sẽ có chi phí 9 + 8 = 17. Ghép P1 với B và P2 với A có chi phí 1 + 2 = 3. Chúng ta chọn tổ hợp thứ hai này. Nguyên tắc không phải là chọn riêng lẻ mục tiêu tốt nhất cho từng đề xuất: các lựa chọn phải tương thích với nhau.

Mã ghép cặp của DETR kết hợp chi phí hạng mục và hình học. Việc ghép với chú thích này phục vụ huấn luyện; suy luận không thể dùng nó để xem các đáp án mong đợi.

Một hàm mất mát để học, nhiều phép đo để hiểu

Một hàm mất mát gán một giá trị số cho lỗi của mô hình. Huấn luyện điều chỉnh các tham số để giảm giá trị này. Trong quy trình này, nó tập hợp phân loại, khoảng cách L1 trên tọa độ và GIoU.

Phân loại kiểm tra loại đối tượng: hàng, cột hay hạng mục khác. Khoảng cách L1 cộng các độ lệch tuyệt đối giữa những tọa độ tương ứng. Ví dụ, giữa các vectơ hư cấu (0,10 ; 0,20 ; 0,30 ; 0,40) và (0,12 ; 0,20 ; 0,28 ; 0,45), giá trị là 0,02 + 0 + 0,02 + 0,05 = 0,09. Ví dụ này giải thích thao tác, không phải các giá trị quan sát khi huấn luyện.

IoU, viết tắt của Intersection over Union, đo phần chồng lấp của hai hộp: diện tích chung chia cho diện tích hợp của chúng. Hai hộp giống hệt nhau có IoU bằng 1; hai hộp rời nhau có IoU bằng 0.

Nhưng một hộp nằm sát vị trí đúng và một hộp ở rất xa đều có thể có IoU bằng không. GIoU thêm thông tin liên quan đến vùng bao quanh cả hai hộp, để cung cấp tín hiệu ngay cả khi không chồng lấp. Phần giới thiệu GIoU của các tác giả cho phép bạn khám phá cơ chế này một cách trực quan.

Đừng nhầm các phép tính này với việc gán từ. Với một từ, chúng ta sẽ chia diện tích chung với một ô cho diện tích của từ, chứ không phải diện tích hợp của hai hình chữ nhật. Một hộp từ nhỏ có thể nằm hoàn toàn trong một ô lớn: khi đó độ phủ của nó bằng 1, dù IoU của chúng thấp.

Dữ liệu được kiểm soát, sự tách biệt thực sự

Bộ sinh làm thay đổi đường viền, lề trong, kích thước, số hàng và số cột. Nó thêm một hàng thân bảng trống và đôi khi một cột trống. Điều này buộc chương trình phân biệt một ô không có văn bản với một ô mà nó chưa tái tạo được.

Trong ứng dụng của bạn, một ô trống có thể mang ý nghĩa. Nó không tự động có nghĩa là số không và không được làm giá trị sau nó dịch sang trái. Vì vậy hướng dẫn giữ lại hình học trước khi đặt câu hỏi liệu có từ nào cần đưa vào đó hay không.

336 tài liệu huấn luyện dùng để điều chỉnh tham số. 72 tài liệu xác thực cho phép xem xét hành vi trong khi tinh chỉnh; 72 tài liệu kiểm thử phục vụ phép đo cuối cùng. Việc tách theo họ tránh để cùng một mẫu xuất hiện trong nhiều nhóm. Tuy nhiên điều đó không biến tài liệu tổng hợp thành mẫu đại diện cho mọi PDF.

Chú thích dùng để huấn luyện và đánh giá. Dịch vụ inference không thể mở các tệp của chúng. Còn hình chữ nhật vùng là đầu vào người dùng được cố định trước khi sinh và dự đoán: nó chỉ nơi cần quan sát mà không cung cấp lưới mong đợi.

Đây là khác biệt thiết yếu để đọc các điểm số. Chúng ta đo việc tái tạo khi biết rằng vùng đã được cung cấp. Việc sau này thêm bộ phát hiện bảng không cho phép giữ nguyên điểm số đó như thể việc định vị của nó luôn hoàn hảo. Cần đo bước mới này, rồi đo toàn bộ quy trình.

Ví dụ có thể tính: tiêu đề Helix

Hãy quay lại “Quarter 1” trong một phiên bản đơn giản hóa, với tọa độ được chọn để tính toán. Hàng chiếm khoảng dọc [20, 50]. Hai cột chiếm lần lượt [10, 110] và [110, 210] theo chiều ngang.

Trước khi gộp, ta có hai ô: [10, 20, 110, 50] và [110, 20, 210, 50]. Một span phủ hoàn toàn hai ô này cho phép thay chúng bằng một ô duy nhất: [10, 20, 210, 50], với các chỉ số (0, 1, 0, 2).

Bây giờ, chính ô duy nhất này nhận các từ “Quarter” và “1”. Đây là biểu diễn sư phạm của ô sau khi gán, với các định danh hư cấu:

{
  "row_start": 0,
  "row_end": 1,
  "col_start": 0,
  "col_end": 2,
  "box": [10, 20, 210, 50],
  "word_ids": [101, 102],
  "text": "Quarter 1"
}

Đây không phải bản xuất thật từ kho dữ liệu mà là ví dụ về cấu trúc được các hàm bên dưới thao tác. Điểm quan trọng là vẫn có một ô và hai từ, chứ không phải hai bản sao của tiêu đề.

Xử lý một từ nằm vắt qua ranh giới thế nào? Với ngưỡng phủ 0.5, một ô phải phủ ít nhất một nửa hộp của từ. Trong một ví dụ hư cấu khác, 60 pixel vuông chung trên một hộp từ 100 pixel vuông cho 0,6: ô này được chấp nhận. Nếu không ô nào đạt 0,5, từ vẫn ở trong unassigned_word_ids.

Các span cũng có thể xung đột. Mã xử lý trước những span tự tin hơn và ghi nhớ các vị trí đã gộp. Một xung đột chạm tới kiểm tra này làm tăng rejected_spans. Chúng ta sẽ đọc kỹ bộ đếm này, vì nó không đại diện cho mọi ứng viên bị loại.

Mã trọng tâm: tạo lưới rồi nối các từ

Tái tạo lưới từ các đối tượng dự đoán

Các hàm sau nhận những đối tượng đã được biểu diễn bằng label, score và box. label là hạng mục, score là điểm gắn với đề xuất, còn box là tọa độ của nó. Các hàm tiện ích được nhập tính diện tích, giao nhau và IoU.

Các định danh hữu ích ở đây là 1 cho cột, 2 cho hàng và 5 cho ô gộp. Các hạng mục 0, 3 và 4 lần lượt chỉ bảng và hai loại tiêu đề. Bạn có thể tìm quy ước này trong cấu hình mô hình TATR. Các lớp tiêu đề phân biệt vai trò trong bảng; chúng không được hàm tái tạo này dùng trực tiếp.

Trước hết hãy đọc hai hàm như một chuỗi: axis_intervals() tạo các dải có thứ tự; reconstruct() giao cắt các dải đó rồi áp dụng việc gộp.

from .boxes import area, intersection, iou


def axis_intervals(objects, label, axis, threshold):
    candidates = sorted(
        [o for o in objects if o["label"] == label], key=lambda o: -o["score"]
    )
    kept = []
    for item in candidates:
        if all(iou(item["box"], other["box"]) < threshold for other in kept):
            kept.append(item)
    kept.sort(key=lambda o: (o["box"][axis] + o["box"][axis + 2]) / 2)
    unique = []
    for item in kept:
        center = (item["box"][axis] + item["box"][axis + 2]) / 2
        if not unique or center - unique[-1][0] > 0.5:
            unique.append((center, item))
    if not unique:
        return []
    # Turn noisy overlapping intervals into a partition. This can be wrong geometrically; IoU and word scores expose it.
    bounds = (
        [min(o["box"][axis] for _, o in unique)]
        + [(a[0] + b[0]) / 2 for a, b in zip(unique, unique[1:])]
        + [max(o["box"][axis + 2] for _, o in unique)]
    )
    return [[a, b] for a, b in zip(bounds, bounds[1:]) if b > a]


def reconstruct(objects, config):
    objects = [
        o
        for o in objects
        if o["score"] >= config["score_threshold"] and area(o["box"]) > 0
    ]
    rows = axis_intervals(objects, 2, 1, config["nms_iou"])
    cols = axis_intervals(objects, 1, 0, config["nms_iou"])
    cells = []
    for r, (top, bottom) in enumerate(rows):
        for c, (left, right) in enumerate(cols):
            cells.append(
                {
                    "row_start": r,
                    "row_end": r + 1,
                    "col_start": c,
                    "col_end": c + 1,
                    "box": [left, top, right, bottom],
                }
            )
    rejected = 0
    occupied = set()
    for span in sorted(
        [o for o in objects if o["label"] == 5], key=lambda o: -o["score"]
    ):
        covered = [
            cell
            for cell in cells
            if area(intersection(cell["box"], span["box"])) / area(cell["box"])
            >= config["span_coverage"]
        ]
        if len(covered) < 2:
            continue
        r0 = min(c["row_start"] for c in covered)
        r1 = max(c["row_end"] for c in covered)
        c0 = min(c["col_start"] for c in covered)
        c1 = max(c["col_end"] for c in covered)
        positions = {(r, c) for r in range(r0, r1) for c in range(c0, c1)}
        if positions & occupied:
            rejected += 1
            continue
        occupied |= positions
        cells = [
            cell
            for cell in cells
            if not (r0 <= cell["row_start"] < r1 and c0 <= cell["col_start"] < c1)
        ]
        cells.append(
            {
                "row_start": r0,
                "row_end": r1,
                "col_start": c0,
                "col_end": c1,
                "box": [cols[c0][0], rows[r0][0], cols[c1 - 1][1], rows[r1 - 1][1]],
            }
        )
    cells.sort(key=lambda cell: (cell["row_start"], cell["col_start"]))
    return {
        "rows": len(rows),
        "columns": len(cols),
        "cells": cells,
        "rejected_spans": rejected,
    }

axis_intervals(): chuyển từ các hình chữ nhật nhiễu thành các dải

Trước hết, hàm chọn các đối tượng thuộc hạng mục được yêu cầu và sắp xếp chúng theo điểm giảm dần. Nó chỉ giữ một ứng viên nếu IoU của nó với từng đối tượng đã giữ vẫn dưới threshold. Đây là cách lọc phần chồng lấp, ở đây do nms_iou điều khiển: mục tiêu là tránh đếm nhiều đề xuất gần như giống hệt nhau thành nhiều hàng hoặc cột.

Sau đó, các đối tượng được giữ được sắp xếp theo tâm của chúng. Với axis=0, ta xét tọa độ ngang; với axis=1, tọa độ dọc. Danh sách unique cũng loại các tâm cách nhau không quá 0.5 đơn vị trong hệ tọa độ đang dùng. 0.5 này là khoảng cách, không phải ngưỡng tin cậy.

Cuối cùng, hàm biến các khoảng thành một phân hoạch: những dải liền kề gặp nhau ở một ranh giới chung. Các ranh giới bên trong được đặt ở điểm giữa tâm của các phần tử kề nhau.

Ví dụ, nếu hai khoảng hư cấu [0, 40] và [30, 70] còn lại sau các bộ lọc, tâm của chúng là 20 và 50. Ranh giới chung trở thành (20 + 50) / 2 = 35. Do đó các dải tái tạo là [0, 35] và [35, 70].

Chúng ta đã loại phần chồng lấp, nhưng chưa chứng minh 35 là ranh giới thật trong tài liệu. Đây là một quy tắc hậu xử lý, tức mã chạy sau các dự đoán của mô hình. Bình luận trong hàm nhắc điều đó: một lưới đều vẫn có thể sai về mặt hình học.

reconstruct(): tạo các giao điểm, rồi gộp

Bộ lọc đầu tiên loại các đối tượng có điểm không đủ hoặc hộp có diện tích bằng không. Các hàng và cột còn lại sau đó tạo mọi giao điểm nhờ hai vòng lặp for. Chính ở giai đoạn này một ô trống vẫn tồn tại: lưới được tạo mà không cần xem văn bản.

Tiếp theo là các span, được sắp từ điểm cao nhất đến thấp nhất. Với mỗi span, covered chứa các ô mà hộp của nó phủ đủ theo span_coverage. Mẫu số ở đây là diện tích của ô, không phải của từ hay của span.

Nếu ít hơn hai ô được phủ, ứng viên bị bỏ qua: không có nhiều ô để gộp. Nếu không, mã lấy các chỉ số biên của chúng và tạo positions, tập hợp các vị trí lưới nằm trong hình chữ nhật thu được.

occupied ghi nhớ các vị trí đã được những lần gộp chấp nhận chiếm. Biểu thức positions & occupied tìm giao giữa hai tập hợp này. Nếu giao không rỗng, phần gộp mới xung đột với phần trước: nó bị từ chối và rejected_spans tăng lên.

Nếu không, các ô cơ sở liên quan bị loại bỏ, rồi một ô gộp được thêm vào. Hộp của nó được tái tạo từ giới hạn hàng và cột; nó không mù quáng lấy lại tọa độ thô của span dự đoán. Lần sắp xếp cuối cùng xếp các ô theo hàng rồi theo cột.

Một lưu ý khi đọc mã: rejected_spans đếm các xung đột đi tới phép kiểm occupied, chứ không phải mọi span bị bỏ qua. Một ứng viên có thể bị loại ở ngưỡng điểm hoặc không còn phủ hai ô sau một lần gộp trước đó. Vì thế bộ đếm bằng không không có nghĩa là mọi đề xuất gộp đều được chấp nhận.

assign_words(): gán từng từ mà không nhân bản

Chúng ta đã có các ô; còn phải điền chúng. Hàm này không tự tạo ra văn bản: nó dùng các từ trích xuất từ PDF và các hộp của chúng.

from .boxes import area, intersection


def assign_words(grid, words, coverage=0.5):
    cells = [{**cell, "word_ids": [], "text": ""} for cell in grid["cells"]]
    unassigned = []
    for word in words:
        scores = [
            area(intersection(cell["box"], word["box"])) / area(word["box"])
            for cell in cells
        ]
        if scores and max(scores) >= coverage:
            cells[max(range(len(cells)), key=lambda i: scores[i])]["word_ids"].append(
                word["id"]
            )
        else:
            unassigned.append(word["id"])
    byid = {word["id"]: word for word in words}
    for cell in cells:
        cell["text"] = " ".join(byid[index]["text"] for index in cell["word_ids"])
    assigned = [index for cell in cells for index in cell["word_ids"]]
    assert len(set(assigned)) == len(assigned) and set(assigned + unassigned) == set(
        byid
    )
    return {**grid, "cells": cells, "unassigned_word_ids": unassigned}

Dòng đầu tiên tạo một bản sao các ô, với danh sách word_ids trống và văn bản trống. Với mỗi từ, scores chứa phần độ phủ của nó bởi từng ô.

Mã chọn ô có độ phủ tốt nhất, với điều kiện độ phủ đó đạt coverage. Nó không giữ mọi ô vượt ngưỡng: một từ chỉ được thêm vào một danh sách. Nếu hai ô có đúng cùng điểm tối đa, max() ở đây giữ ô gặp đầu tiên. Đây là cách phân xử xác định, không phải bằng chứng rằng việc gán là đúng.

Nếu không ô nào phù hợp, định danh được thêm vào unassigned. Vì thế từ không âm thầm biến mất khỏi việc hạch toán đầu vào.

Sau đó byid tạo một từ điển để nhanh chóng tìm lại một từ từ định danh của nó. Lệnh " ".join(...) ghép các văn bản gắn với một ô bằng dấu cách. Nó giữ thứ tự các từ đã được cung cấp: hàm này không tự tái tạo thứ tự đọc của chúng.

Cuối cùng, assertion kiểm tra hai tính chất: không định danh được gán nào lặp lại giữa các ô, và tập hợp định danh được gán hoặc chưa được gán khớp với các định danh đầu vào. Với các định danh đầu vào duy nhất, vòng lặp nhờ đó cho thấy số phận của từng từ.

Điều đó bảo vệ khỏi việc nhân bản âm thầm, nhưng không xác nhận đúng cột. Các từ đầu vào cũng phải có hộp diện tích dương, vì diện tích này dùng làm số chia; phần kiểm tra điều kiện đó không xuất hiện trong trích đoạn. Khi điều chỉnh quá trình nhập của bạn, hãy kiểm tra việc chuẩn bị đầu vào thay vì giả định hàm này xử lý mọi bất thường có thể.

Hãy ghi nhớ ba câu hỏi khác nhau: từ có được giữ lại không, có được gán đúng một lần không, và có được gán đúng chỗ không? Chỉ việc so sánh với tham chiếu mới trả lời được câu cuối.

Điều kiện tiên quyết, rồi đến các lệnh chính xác

Bạn cần biết đọc Python, dùng terminal và chạy Docker. Các khái niệm học máy được giới thiệu dọc theo quy trình; không cần từng huấn luyện một bộ phát hiện để theo dõi các phép tính lưới.

Quy trình tham chiếu dùng Linux, Docker Engine, Compose, một trình điều khiển NVIDIA và NVIDIA Container Toolkit. Nó đã được quan sát trên RTX 3090 24 Gio, ở float32, tức số thực dấu phẩy động 32 bit. Hãy dự kiến khoảng 24 Gio RAM và 18 Gio dung lượng đĩa: đây là ước lượng chuẩn bị, không phải mức tối thiểu được đo cho mọi máy. Cấu hình CPU là NOT_TESTED.

Trình điều khiển khiến GPU có thể sử dụng trong Linux; Toolkit cho phép container Docker truy cập GPU và không thay thế việc cài đặt trình điều khiển. nvidia-smi kiểm tra khả năng hiển thị ở phía máy chủ; sau đó check_environment.py kiểm tra một phép tính thực trong Docker.

Hãy giải nén ZIP vào một thư mục làm việc và mở terminal tại thư mục gốc của nó. Mọi lệnh bên dưới đều bắt đầu từ thư mục này. Hãy giữ các phụ thuộc và bản sửa mô hình đã khóa trong dự án, thay vì thay chúng bằng các phiên bản hiện hành trong tài liệu.

Bước 1 — chuẩn bị và kiểm tra

Chúng ta bắt đầu bằng cách kiểm soát môi trường, chuẩn bị các thư mục, xác thực Compose và xây dựng ba dịch vụ. acquisition tải các tệp mô hình; tutorial cho phép chuẩn bị, huấn luyện và đánh giá; inference tạo dự đoán mà không truy cập các chú thích.

sh scripts/preflight.sh
sh scripts/setup.sh
docker compose config --quiet
docker compose build acquisition tutorial inference
docker compose run --rm acquisition python scripts/acquire.py --config configs/minimum.yaml
docker compose run --rm tutorial python scripts/check_environment.py
docker compose run --rm tutorial python -m pytest -q

Mỗi lệnh phải kết thúc với mã trả về bằng không. Quá trình thu thập tải 115 Mo tệp TATR ở bản sửa đã khóa. Các kiểm thử đặc biệt kiểm tra tọa độ, ô gộp, việc dọn dẹp và thu thập.

Các kiểm thử này xác nhận chương trình hoạt động. Chúng không có nghĩa mô hình sẽ tái tạo đúng bất kỳ bảng nào. Hãy kiểm tra cả việc GPU thực sự khả dụng trong Docker trước khi chuyển sang huấn luyện.

Bước 2 — chuẩn bị crop và baseline

Một baseline là phương pháp tham chiếu mà chúng ta sẽ so sánh với mô hình đã tinh chỉnh. Nó tránh kết luận một giải pháp hữu ích chỉ vì nó dùng mạng nơ-ron.

docker compose run --rm tutorial python -m pdftables.cli prepare --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdftables.cli inspect-labels --config configs/minimum.yaml
docker compose run --rm inference python -m pdftables.cli baseline --config configs/minimum.yaml

prepare phải thông báo 480 trang, phân thành 336/72/72. Hãy kiểm tra các crop và chú thích học trước khi huấn luyện: các hàng trống có hiện diện không? Tiêu đề có hiển thị đầy đủ không? Các cột của nó có được biểu diễn đúng không? Hãy dành riêng các tham chiếu kiểm thử cho đánh giá, không dùng chúng để chỉnh khung hình hoặc thiết lập.

So sánh cuối cùng sẽ xét pdfplumber, TATR tiền huấn luyện và TATR đã tinh chỉnh, tất cả đều áp dụng cho cùng những vùng đã biết. Tài liệu pdfplumber mô tả việc trích xuất dựa vào đường kẻ đồ họa hoặc sự căn chỉnh từ, các giao điểm của chúng và các ô thu được. Đây là một điểm so sánh cụ thể, không phải đối thủ cần cố tình làm yếu đi.

Bước 3 — tinh chỉnh và kiểm tra việc tiếp tục

Chúng ta bắt đầu từ mô hình đã huấn luyện Table Transformer structure recognition. Tinh chỉnh, hay fine-tuning, là tiếp tục học trên kho dữ liệu của chúng ta thay vì khởi tạo ngẫu nhiên toàn bộ mô hình.

Trong dự án này, chỉ bộ giải mã, các truy vấn đã học và các đầu được tinh chỉnh. Bộ giải mã biến các truy vấn và thông tin trực quan thành biểu diễn phục vụ dự đoán; các đầu suy ra từ đó hạng mục và hộp. Các trọng số khác bị đóng băng: chúng vẫn được dùng trong tính toán nhưng không bị thay đổi khi huấn luyện. Dự án lưu một bảng kê các trọng số này.

Vì vậy đây là tinh chỉnh một phần, không phải huấn luyện lại hoàn toàn. Quy trình không so sánh mọi chiến lược đóng băng có thể có; đừng gán cho nó một kết quả mà nó không đo.

docker compose run --rm tutorial python -m pdftables.cli train --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdftables.cli train-continuous --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdftables.cli train-resume --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdftables.cli verify-resume --config configs/minimum.yaml

Một epoch là một lượt đi qua các ví dụ huấn luyện. Lần chạy đầu dừng sau bốn epoch. Đối chứng liên tục khởi đầu từ cùng mô hình tiền huấn luyện và thực hiện tám epoch. Việc tiếp tục tải lại lần chạy bị gián đoạn và thực hiện bốn epoch còn lại.

Vì sao lặp lại? Để kiểm tra gián đoạn không làm thay đổi thí nghiệm. Một checkpoint là bản lưu trạng thái. Chỉ tải lại trọng số không đủ để tiếp tục đúng cùng phép tính: còn phải khôi phục optimizer, thành phần điều khiển việc cập nhật chúng, và trạng thái của các bộ sinh giả ngẫu nhiên, được gọi là RNG. Tài liệu PyTorch về checkpoint giải thích khác biệt giữa lưu để dự đoán và lưu để tiếp tục huấn luyện.

Trong quy trình đã xác thực, verify-resume kết luận EXACT. Kết quả này áp dụng cho các kiểm tra và môi trường của lần tái tạo đó; không nên mặc định nó vẫn đúng sau khi thay đổi phần cứng hoặc phụ thuộc.

Bước 4 — dự đoán và đo lường

Suy luận sử dụng các trọng số đã lưu để tạo đối tượng, tái tạo lưới và gán từ. Sau đó, đánh giá diễn ra riêng biệt với các chú thích tham chiếu.

docker compose run --rm inference python -m pdftables.cli predict --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdftables.cli evaluate --config configs/minimum.yaml

Liên hệ từng lỗi với một quyết định

Sau predict, hãy mở regions-and-grid.png, các hộp dự đoán và export/table.json. Bắt đầu với một bảng, không phải với trung bình tổng thể. Theo dõi một giá trị từ PDF đến định danh từ, ô của nó và văn bản đã xuất.

Một hàng thiếu làm giảm số giao điểm khả dụng. Một cột phân ranh sai có thể đưa một từ vào nhầm ô. Một lần gộp sai có thể hợp nhất hai ô vốn tách biệt. Một từ chưa được gán có thể cho thấy vấn đề về độ phủ hoặc hệ tọa độ. Không phải mọi lỗi này đều được sửa bằng cách huấn luyện mạng nhiều hơn.

HTML giúp bạn kiểm tra phần gộp; CSV giúp đọc dạng bảng; JSON giữ tọa độ và các định danh cần cho chẩn đoán. Nếu hiển thị có vẻ đúng, vẫn hãy kiểm tra các khoản tiền và các liên kết.

Bộ đánh giá xem xét ba lớp: vị trí ô, các cặp từ-ô và văn bản đã chuẩn hóa. Nếu cấu trúc xấu đi, hãy bắt đầu với các dải và span. Nếu cấu trúc đúng nhưng liên kết giảm, hãy kiểm tra các từ gần ranh giới và ngưỡng phủ. Nếu chỉ văn bản khác, hãy kiểm tra việc trích xuất ký tự và chuẩn hóa chúng, tức các biến đổi áp dụng trước khi so sánh.

Một ô trống cũng đáng được chú ý như vậy: đừng đơn giản xóa nó để bản xuất gọn hơn. Sự vắng mặt của nó có thể che giấu lỗi cấu trúc.

Đọc chỉ số trước khi đọc con số

Precision, recall và F1: ba câu hỏi đơn giản

Precision hỏi: trong các phần tử được dự đoán, có bao nhiêu phần tử đúng? Recall hỏi: trong các phần tử mong đợi, chúng ta tìm lại được bao nhiêu? F1 kết hợp hai giá trị bằng công thức 2 × precision × recall / (precision + recall) khi mẫu số khác không.

Giả sử cần có bốn ô và chúng ta dự đoán ba ô, tất cả đều đúng. Precision là 3/3 = 1, recall là 3/4 = 0,75 và F1 xấp xỉ 0,857. Chúng ta không bịa ra gì, nhưng vẫn thiếu một ô: chỉ precision sẽ che giấu điều đó.

Cấu trúc và liên kết: hai cấp độ khác nhau

Cell F1 so sánh vị trí ô, được mô tả bằng (bắt đầu hàng, kết thúc hàng, bắt đầu cột, kết thúc cột). Một ô phủ hai cột không tương đương hai ô tách biệt. Đây là cấu trúc lưới, không chỉ là độ giống trực quan giữa hai hình chữ nhật.

Association F1 so sánh các cặp gồm một từ và ô của nó. Dù mọi từ đều hiện diện, đưa chúng vào nhầm cột vẫn tạo ra liên kết sai. Và hai khoản tiền giống nhau vẫn là hai từ riêng biệt nhờ các định danh của chúng.

Sự khác biệt này giải thích vì sao không có trùng lặp trong assign_words() là chưa đủ: một phép gán duy nhất vẫn có thể là gán sai.

Vì sao “nội dung hoàn hảo” có thể gây hiểu lầm

Độ chính xác nội dung có điều kiện chỉ xem các ô chung giữa dự đoán và tham chiếu. Nó trả lời câu hỏi: “trên những ô có vị trí đã được tìm lại, văn bản có đúng không?”

Độ chính xác nội dung đầu-cuối dùng mọi ô tham chiếu trong mẫu số. Vì vậy nó cũng phạt các ô bị thiếu. Ở đây, “đầu-cuối” vẫn chỉ tương đối với quy trình hướng dẫn, có vùng được cung cấp và văn bản gốc: nó không bao gồm phát hiện toàn trang hay OCR.

Hãy lấy ví dụ sư phạm với mười ô mong đợi. Chương trình chỉ tìm lại hai ô nhưng văn bản của chúng chính xác. Nội dung có điều kiện là 2/2 = 1. Nội dung đầu-cuối là 2/10 = 0,2. Cả hai phép đo đều đúng; đơn giản là chúng không trả lời cùng một câu hỏi.

Trong kết quả thật của baseline pdfplumber, 371 ô chung chứa đúng các từ: vì vậy nội dung có điều kiện bằng 1. Tuy nhiên chỉ 16 bảng trên 72 hoàn toàn chính xác và nội dung đầu-cuối bằng 0,1593. Đó chính là cái bẫy cần học cách nhận ra.

Một bảng hoàn toàn chính xác là tiêu chí khắt khe hơn

table_accuracy đòi hỏi cả cấu trúc lẫn văn bản chính xác cho một tài liệu. Một bảng chứa nhiều ô đúng và chỉ một lỗi không được tính là bảng chính xác.

Các chỉ số này là của dự án: chúng không phải chỉ số GriTS. Kho Table Transformer chính thức mô tả GriTS, một giao thức khác để so sánh lưới. Tên gọi và định nghĩa không thể hoán đổi; không thể đặt cạnh nhau những số tính theo cách khác nhau như thể chúng đo cùng một thứ.

Kết quả của quy trình r003

Trên 72 tài liệu kiểm thử, các giá trị được làm tròn đến bốn chữ số thập phân:

Trên thiết bị di động, hãy cuộn ngang bảng.

Biến thể Cell F1 Association F1 Nội dung có điều kiện Nội dung đầu-cuối Bảng chính xác
pdfplumber 0,2748 0,3140 1,0000 0,1593 16/72
TATR tiền huấn luyện 0,8897 0,6532 0,7473 0,6909 0/72
TATR đã tinh chỉnh 0,9974 0,9858 0,9871 0,9820 67/72

Ở đây, việc tinh chỉnh cải thiện các phép đo cấu trúc, liên kết và nội dung so với mô hình tiền huấn luyện. Nó đạt 67/72 bảng chính xác, so với 0/72 cho TATR tiền huấn luyện và 16/72 cho pdfplumber.

Số không của mô hình tiền huấn luyện không có nghĩa là nó không tìm thấy gì: cell F1 của nó là 0,8897. Nó có nghĩa là không bảng nào trong 72 bảng đồng thời thỏa mọi tiêu chí chính xác. Lỗi có thể ít trong mỗi tài liệu nhưng vẫn đủ khiến tiêu chí nghiêm ngặt này thất bại.

Kết quả đã tinh chỉnh giữ 80/80 ô gộp, nhưng năm bảng không chính xác. Vì vậy tìm lại tốt phần gộp không bảo đảm toàn bộ cấu trúc và nội dung đều đúng. Với một khoản tiền, một lỗi cục bộ quan trọng hơn ấn tượng chung về một bảng được vẽ đẹp.

Các kết quả này áp dụng cho các crop Helix tổng hợp của quy trình. Chúng không chứng minh chất lượng tương tự trên bản quét, bảng thật, hóa đơn hoặc việc phát hiện toàn trang.

So sánh các đầu ra cấu trúc và văn bản trên kho kiểm thử Helix.

Những gì chúng ta đã học

Trích xuất một bảng đòi hỏi tái tạo tổ chức của nó, rồi tìm lại liên kết giữa ô và từ. TATR đề xuất các đối tượng trực quan; hậu xử lý biến các đề xuất này thành dải, giao điểm và phần gộp; việc gán hình học đặt văn bản gốc vào đó.

Thông tin truy xuất nguồn gốc giúp các quyết định này có thể kiểm tra. Nó cho phép quay từ một giá trị đã xuất về các từ và tọa độ của nó, nhưng bản thân nó không bảo đảm quyết định là đúng.

Cuối cùng, hình thức và nội dung được kiểm tra riêng biệt. Một ô trống phải vẫn hiển thị, một phần gộp không được nhân bản văn bản của nó, và một điểm có điều kiện tốt không được che giấu các ô bị thiếu. Trên kho tham chiếu, việc tinh chỉnh đem lại mức tăng đã đo, mà không làm cả 72 bảng đều chính xác.

Bài tập — phân biệt hình thức và nội dung

Trước khi chạy kịch bản, hãy dành chút thời gian dự đoán kết quả. Trước hết, hãy tưởng tượng hai cột được gộp mà không đổi các từ. Giữ lại toàn bộ văn bản có đủ để duy trì chỉ số cấu trúc và liên kết tốt không?

Bây giờ hãy tưởng tượng thay một chữ số nhưng các ô và định danh từ vẫn ở nguyên vị trí. Chỉ số nào có thể vẫn tốt? Bảng có còn được coi là chính xác không? Hãy ghi lại lập luận trước khi chạy lệnh.

docker compose run --rm tutorial python -m pdftables.cli exercise --config configs/exercise.yaml
docker compose run --rm tutorial python lesson/figures/plot.py
docker compose run --rm tutorial python scripts/check_checkpoints.py

Lệnh đầu tiên chạy bài tập được cung cấp. Lệnh kế tiếp tạo hình từ đầu ra, sau đó lệnh cuối kiểm tra checkpoint, các tệp mô hình đã lưu.

Trong kịch bản đầu tiên, các span thay đổi và từ không còn được gán vào cùng các ô: cấu trúc và liên kết xấu đi, ngay cả khi không mất từ.

Trong kịch bản thứ hai, lưới có thể vẫn đúng trong khi văn bản trở nên sai. Nếu các định danh và cách gán của chúng được giữ, các chỉ số liên kết tự chúng không phát hiện việc thay chữ số; cần xem nội dung. Do đó, một bảng có thể có cấu trúc hoàn hảo mà vẫn chứa giá trị sai.

Hãy so sánh các bảng và chỉ số của chúng với các giả thuyết đã ghi. Đừng sửa từ hay chú thích để được điểm tốt hơn: bài tập chính là để làm rõ sự tách biệt này.

Bạn cũng có thể tự tính lại độ phủ của từ rộng 100 pixel vuông. Với 60 pixel vuông nằm trong một ô, từ được chấp nhận ở ngưỡng 0.5. Với chỉ 40 pixel vuông và không ô nào khác phủ đủ, nó không được gán. Phép tính sư phạm này không cần huấn luyện mới.

Tinh chỉnh, khắc phục sự cố và dọn dẹp

Chẩn đoán trước khi thay đổi mô hình

GPU hiển thị trong Linux nhưng CUDA thất bại trong Docker? Trước tiên hãy kiểm tra tích hợp với tài liệu NVIDIA Container Toolkit. CUDA là môi trường dùng cho tính toán trên GPU; một card hiện diện ở máy chủ không chứng minh chương trình trong container có thể sử dụng nó.

Tiêu đề bị cắt? Hãy xem vùng được cung cấp và lề crop. Một mô hình không thể tái tạo trung thực những gì đã bị loại khỏi đầu vào. Với tài liệu riêng của bạn, hãy sửa khung trước dự đoán mới. Để đo giao thức kiểm thử, đừng điều chỉnh lại khung này dựa trên đáp án mong đợi.

Một từ dường như biến mất? Hãy xem unassigned_word_ids, rồi kiểm tra hộp từ, crop và ngưỡng phủ. Đừng tùy tiện thêm nó vào một ô để cải thiện hiển thị. Cũng hãy kiểm tra thứ tự từ trước khi ghép: assign_words() giữ thứ tự nhận được.

HTML có vẻ đúng nhưng các con số sai? Hãy quay lại JSON truy xuất nguồn gốc và so sánh các từ nguồn. Thay đổi cột hoặc ký tự không đòi hỏi cùng chẩn đoán với một hàng bị thiếu.

Chuyển sang tài liệu của bạn: một phần mở rộng cần đánh giá

Trong một ứng dụng thực tế, hãy tách việc phát hiện bảng, OCR nếu tài liệu cần, tái tạo cấu trúc và kiểm tra nội dung. Các bước bổ sung này không được các điểm số của hướng dẫn xác thực.

Hãy thêm các kiểm tra phù hợp với dữ liệu: kiểu mong đợi, đơn vị, định dạng số và xác thực của con người cho các trường hợp mơ hồ. Một chuỗi như 12.50 trước hết phải được trích xuất và định vị đúng; diễn giải nó là khoản tiền thuộc về một quyết định khác của ứng dụng.

Hãy ghi nhật ký vùng, tỷ lệ, điểm số, các từ chưa được gán và phiên bản mô hình. Giữ các chú thích đáng tin cậy đại diện cho tài liệu của bạn để đo lỗi. Chẳng hạn, một giao diện duyệt có thể cho phép nhấp vào ô đã xuất để tìm lại các từ của nó trên trang: đó là một hướng tích hợp, không phải tính năng được công bố là có trong ZIP.

Các tài liệu chính thức được liên kết trong bài viết giúp tìm hiểu sâu hơn về cơ chế: Table Transformer và DETR cho dự đoán, GIoU cho hình học, pdfplumber cho trích xuất, PyTorch cho lưu trạng thái và NVIDIA cho truy cập GPU. Chúng không thay thế các tệp và lệnh của quy trình r003.

Giữ các bản xuất, rồi dọn dẹp

Hãy sao chép các kết quả và hình bạn muốn giữ trước khi chạy các lệnh sau. Việc dọn dẹp chỉ xóa outputs/; nó giữ data/generated/ và assets/checkpoints/.

docker compose down
docker compose run --rm tutorial python scripts/clean.py --outputs-only
docker compose down

Lệnh đầu tiên dừng dự án. Sau đó dịch vụ tutorial thực hiện dọn dẹp có mục tiêu, rồi lần dừng cuối cùng kết thúc quy trình. Không cần xóa toàn bộ dữ liệu Docker để học hướng dẫn này.

Chia sẻ bài viết