Có lẽ bạn đã từng sao chép văn bản từ PDF vào trình soạn thảo: mọi từ đều có mặt, nhưng các câu không còn nối tiếp nhau. Một dòng ở cột trái xen vào cột phải. Một khung thông báo nằm giữa đoạn văn. Chân trang xuất hiện trước khi văn bản kết thúc.
Vấn đề không nhất thiết nằm ở nhận dạng ký tự. Tìm lại các từ và tìm lại thứ tự đọc của chúng là hai nhiệm vụ khác nhau.
Trong hướng dẫn này, chúng ta sẽ huấn luyện một mạng nơ-ron nhỏ để trả lời câu hỏi rất cụ thể: « Trong hai dòng này, dòng nào nên được đọc trước? » Sau đó, chúng ta sẽ dùng các so sánh ấy để dựng lại thứ tự của cả trang.
Không có mô hình ngôn ngữ lớn để hỏi, cũng không có văn bản để nó tóm tắt. Mô hình chỉ nhận vị trí và cỡ chữ. Đây là dịp để hiểu, bằng Python và một tình huống trực quan, cách đi từ dữ liệu quan sát được đến quá trình học, rồi đến kết quả có thể kiểm tra.
Tải toàn bộ dự án của hướng dẫn
Các lệnh và phép đo được trình bày theo lộ trình tham chiếu r004 của dự án. Những ví dụ nhỏ bổ sung để giải thích phép tính chỉ là minh họa sư phạm, không phải kết quả bổ sung của lộ trình này.
Kết quả chúng ta hướng tới
Ví dụ xuyên suốt của chúng ta là một bản tin hư cấu, « Boreal Maintenance Bulletin ». Nó có một tiêu đề trải toàn chiều rộng, hai cột dòng « Check valve …: stable », một khung thông báo và hai dòng chân trang.
Bằng mắt, bạn nhận ra các khối. Nhưng một chương trình phải dựng lại lộ trình đọc từ những gì nó trích xuất. Nó bắt đầu bằng việc tìm các glyph, tức hình dạng của ký tự, rồi nhóm chúng thành dòng. Sau đó, nó gán thứ hạng cho từng dòng và tạo hai đầu ra bổ sung: Markdown để đọc văn bản theo thứ tự đề xuất, và lớp phủ SVG đánh số các dòng trên trang.
Vì vậy SVG giúp hiểu một quyết định, chứ không chỉ minh họa cho hướng dẫn. Nếu một thông báo được chèn quá sớm, bạn có thể tìm lại vị trí và số của nó trên trang.
Một chi tiết khiến thí nghiệm thú vị: bộ sinh cố ý xáo trộn thứ tự vẽ các đối tượng trong PDF. Chương trình không thể chỉ theo luồng đó để tìm đáp án. Với lập trình viên web, hãy hình dung các khối có thứ tự trực quan khác thứ tự trong DOM: duyệt phần tử không còn đủ để suy ra lộ trình mong muốn.
Mô hình không nhận văn bản, mã định danh dòng hay thứ tự tham chiếu. Văn bản vẫn có cho xuất dữ liệu nhưng không đi vào phép tính. Các chú thích, tức đáp án mong đợi, được dùng riêng cho huấn luyện và đánh giá. Dịch vụ dự đoán hoạt động mà không có chúng.
Phạm vi vẫn chính xác: PDF gốc, có thể trích xuất ký tự. Dự án không thực hiện OCR, tức nhận dạng văn bản trong ảnh, và không xác thực tài liệu nghiệp vụ thực tế nào.
Những mốc cần biết trước khi xem mã
Tìm lại các dòng trong cùng một hệ tọa độ
Hãy bắt đầu bằng một khái niệm quen thuộc trong phát triển giao diện: hộp bao quanh một phần tử. Ở đây, mỗi ký tự được trích xuất có một hộp, ký hiệu x0, y0, x1, y1. Trong hệ tọa độ cuối cùng mà dự án dùng, các giá trị này chỉ các cạnh trái, trên, phải và dưới.
Tuy vậy, cần chuẩn bị hệ tọa độ ấy. Trong hệ tọa độ PDF cổ điển, gốc ở góc dưới bên trái. Dự án áp dụng cắt CropBox và xoay, rồi chỉ chuyển đổi mỗi hộp một lần sang hệ chung có gốc ở góc trên bên trái. CropBox xác định vùng trang được giữ lại để hiển thị; xoay 90° và 270° hoán đổi chiều rộng và chiều cao.
Vì sao phải quan tâm trước mô hình? Bởi lỗi tọa độ có thể khiến cột phải trông như cột trái. Khi đó mạng sẽ học từ các vị trí không mô tả đúng điều bạn thấy.
Các ký tự gần nhau trên cùng một hàng tiếp đó được nhóm lại. Các glyph của « Check valve 0020: stable » trở thành một dòng với văn bản, hộp và cỡ chữ trung bình. Khoảng trống rất lớn giữa hai nhóm ký tự sẽ tách chúng ra: ta không muốn ghép hai cột thành một dòng.
Việc trích xuất này có thể đã sai. Việc xếp hạng sẽ không làm một dòng bị mất xuất hiện lại. Vì vậy chúng ta sẽ đo riêng sự hiện diện của dòng và chất lượng thứ tự.
Biến hai dòng thành 14 con số
Mạng không xử lý trực tiếp các hộp hay đoạn văn. Ta truyền cho nó một vector đặc trưng: danh sách số mô tả điều ta quan tâm.
Với mỗi dòng, ta lấy bốn tọa độ hộp và cỡ chữ. Tọa độ ngang được chia cho chiều rộng trang; tọa độ dọc và cỡ chữ được chia cho chiều cao. Đây là chuẩn hóa.
Một ví dụ nhỏ là đủ: vị trí ngang 60 trên trang rộng 600 thành 60 / 600 = 0,10. Vị trí 120 trên trang rộng 1 200 cũng cho 0,10. Trong cả hai trường hợp, dòng bắt đầu tại 10 % chiều rộng. Nhờ vậy ta so sánh vị trí tương đối thay vì khoảng cách thô.
Một cặp nhận năm giá trị của dòng đầu, rồi năm giá trị của dòng thứ hai. Ta thêm chênh lệch ngang, chênh lệch dọc và phần chồng lấp trên hai trục. Như vậy có 5 + 5 + 4 = 14 đặc trưng.
Chồng lấp ngang nghĩa là các hộp phần nào chiếm cùng vị trí từ trái sang phải; điều đó không có nghĩa các hình chữ nhật chạm nhau. Hai dòng xếp chồng trong một cột có thể chồng lấp ngang mà không chồng lấp dọc.
Học một ưu tiên, chưa phải xếp hạng
Với mỗi cặp, mô hình tạo một logit, điểm thô có thể âm hoặc dương. Hàm gọi là sigmoid biến nó thành giá trị từ 0 đến 1, ở đây được hiểu là xác suất dòng đầu đứng trước dòng thứ hai.
Với điểm được đổi thành 0,90, mô hình ưu tiên dòng đầu; với 0,10, nó ưu tiên dòng thứ hai. Ở 0,50, nó không ưu tiên dòng nào. Các giá trị này diễn đạt dự đoán, không phải sự chắc chắn về trang.
Trong huấn luyện, ta có đáp án đúng, gọi là mục tiêu hoặc label. Nó bằng 1 nếu dòng đầu phải đứng trước dòng thứ hai, nếu không là 0. Một hàm mất mát đo sai số: ở đây là BCE, hay entropy chéo nhị phân. Nếu mục tiêu bằng 1, dự đoán 0,01 bị phạt nhiều hơn 0,90.
Phép tính mất mát dùng trực tiếp các logit. Tài liệu hữu ích là BCEWithLogitsLoss của PyTorch: gộp sigmoid và BCE cho phép tính ổn định hơn áp dụng riêng. Vì vậy đừng nhầm xác suất dùng để giải thích kết quả với giá trị thô truyền vào hàm mất mát.
Từ ưu tiên đến trang dễ đọc
Giả sử mô hình ưu tiên A trước B, B trước C, nhưng C trước A. Không thể đồng thời tôn trọng cả ba quyết định: ta quay lại điểm xuất phát. Đó là một chu trình.
Bộ giải mã là phần mã biến các ưu tiên này thành danh sách. Bộ giải mã của dự án xem các quan hệ chắc chắn nhất trước và chỉ giữ một mũi tên nếu nó không tạo chu trình. Tập hợp nhận được là một DAG, đồ thị có hướng không chu trình. Mỗi dòng là một đỉnh; mỗi mũi tên nghĩa là « phải đứng trước ».
Một sắp xếp tô-pô sau đó có thể tạo thứ tự tôn trọng các mũi tên được giữ. Hãy nghĩ đến các tác vụ triển khai: một tác vụ khả dụng khi các điều kiện tiên quyết đã được xử lý. Tài liệu Python về đồ thị giải thích nguyên tắc này; hướng dẫn dùng bộ giải mã riêng, được trình bày bên dưới.
Hãy nhớ sự tách biệt: mạng ước lượng ưu tiên; bộ giải mã xây dựng thứ tự cuối. Loại chu trình khiến thứ tự nhất quán với các quan hệ được giữ, nhưng không đảm bảo thứ tự toàn cục tối ưu hay thứ tự đọc đúng.
Dữ liệu được kiểm soát để học cơ chế
Corpus là tổng hợp: chương trình tạo trang và biết thứ tự tham chiếu của chúng. Nó có 60 họ hình học, với 10 trang mỗi họ, tức 600 trang. Một họ gom các trang cùng hình học bố cục.
Việc tách theo họ diễn ra trước mọi huấn luyện. 42 họ, tức 420 trang, dùng để học; 9 họ, tức 90 trang, dùng để xác thực; 9 họ còn lại, tức 90 trang, dành cho kiểm thử.
Huấn luyện điều chỉnh tham số. Xác thực giúp theo dõi chất lượng mô hình mà không học trực tiếp từ các trang đó. Kiểm thử đo kết quả cuối. Chia theo họ thay vì trộn trang ngẫu nhiên tránh một họ xuất hiện đồng thời trong huấn luyện và kiểm thử. Tuy vậy điều này không biến corpus tổng hợp thành mẫu đại diện cho mọi PDF.
Quay lại Boreal. Trên trang đầu, « Check valve 0000: stable » ở cột trái, « Check valve 0020: stable » ở cột phải và « Notice 00-00-0 » trong khung. Bạn có thể đọc các văn bản này để theo ví dụ; mô hình phải quyết định đặt dòng ở đâu mà không đọc các chuỗi ấy.
Lộ trình quan sát được có 232 570 cặp dòng. Kiểm tra đầu vào xác nhận vector 14 chiều, không có văn bản hay mã định danh. Labels dùng để tạo mục tiêu huấn luyện và so sánh dự đoán với đáp án, nhưng không được thêm vào vector này.
Khi suy luận, tức dùng mô hình đã huấn luyện để dự đoán, thư mục labels không có trong container. Mã thất bại rõ ràng nếu nó được gắn vào. Đây là sự tách biệt cụ thể giữa « có đáp án để học » và « truy cập đáp án trong khi tuyên bố đang dự đoán ».
Mã trọng tâm: xây dựng đầu vào cho mô hình
Ta sẽ đọc ba phần: chuẩn bị cặp, mạng và bộ giải mã. Các trích đoạn là từ mô-đun trong ZIP; hãy đọc chúng trong môi trường dự án, không phải như một script độc lập để sao chép vào tệp trống.
pair_features() : chuẩn bị những gì mạng có thể thấy
Đây là các hàm trong src/pdforder/features.py. first và second mô tả hai dòng. Với mỗi cặp, ta chuẩn bị cả hai hướng: dòng đầu rồi dòng thứ hai, sau đó ngược lại.
def pair_features(first, second, width, height):
a = [
first["bbox"][0] / width,
first["bbox"][1] / height,
first["bbox"][2] / width,
first["bbox"][3] / height,
first["font_size"] / height,
]
b = [
second["bbox"][0] / width,
second["bbox"][1] / height,
second["bbox"][2] / width,
second["bbox"][3] / height,
second["font_size"] / height,
]
overlap_x = max(0, min(a[2], b[2]) - max(a[0], b[0]))
overlap_y = max(0, min(a[3], b[3]) - max(a[1], b[1]))
return a + b + [b[0] - a[0], b[1] - a[1], overlap_x, overlap_y]
def page_pairs(page, device="cpu"):
lines = page["lines"]
pairs = [(i, j) for i in range(len(lines)) for j in range(i + 1, len(lines))]
forward = torch.tensor(
[
pair_features(lines[i], lines[j], page["width"], page["height"])
for i, j in pairs
],
dtype=torch.float32,
device=device,
).reshape(-1, 14)
reverse = torch.tensor(
[
pair_features(lines[j], lines[i], page["width"], page["height"])
for i, j in pairs
],
dtype=torch.float32,
device=device,
).reshape(-1, 14)
return pairs, forward, reverse
Trong pair_features(), a và b là hai danh sách gồm năm số. Dấu + nối chúng lại: nó không cộng từng phần tử. Giá trị trả về đúng là 14 số, theo thứ tự cố định.
overlap_x và overlap_y đo độ dài chung của các khoảng trên từng trục, trong tọa độ chuẩn hóa. Nếu các khoảng không chồng lấp, max(0, ...) trả về không. Đây không phải tỷ lệ phần trăm diện tích chung.
Phần cuối thêm chênh lệch giữa các góc trên bên trái. Hai dòng cùng độ cao có chênh lệch dọc gần không; hai cột xa nhau tạo chênh lệch ngang lớn. Mạng có thể kết hợp các dấu hiệu này với kích thước hộp và cỡ chữ.
page_pairs() tiếp đó liệt kê các cặp chỉ số với i < j. Nó không ghép một dòng với chính nó và chỉ liệt kê mỗi cặp một lần, dù xây cả hai hướng. Với bốn dòng, ví dụ, ta có sáu cặp: (0, 1), (0, 2), (0, 3), (1, 2), (1, 3) và (2, 3).
forward và reverse trở thành các tensor, ở đây là mảng số hai chiều. Mỗi hàng mô tả một cặp với 14 cột. Trong reshape(-1, 14), -1 để PyTorch suy ra số hàng. float32 chỉ định định dạng số; device xác định nơi dữ liệu được đặt để tính. Giá trị mặc định "cpu" trong hàm này không phải xác thực toàn bộ lộ trình trên CPU.
Quan sát các đầu vào này, ta hiểu mô hình có thể học gì: quan hệ giữa chiều rộng, chiều cao, vị trí và phông chữ. Nó không thể dựa vào nghĩa của « Notice » hay số viết trong dòng vì chúng không được truyền vào.
PairOrderNet : so sánh hai hướng
Mô hình src/pdforder/model.py là một MLP, hay perceptron đa lớp. Đây là mạng nhỏ với các biến đổi số nối tiếp. Tham số của nó được khởi tạo cục bộ rồi điều chỉnh trong huấn luyện: ta không tải mô hình tiền huấn luyện nào cho nhiệm vụ này.
import torch
from torch import nn
class PairOrderNet(nn.Module):
def __init__(self, hidden=64):
super().__init__()
self.network = nn.Sequential(
nn.Linear(14, hidden),
nn.ReLU(),
nn.Linear(hidden, hidden),
nn.ReLU(),
nn.Linear(hidden, 1),
)
def forward(self, forward_features, reverse_features):
return (
self.network(forward_features) - self.network(reverse_features)
).squeeze(-1)
Với hidden=64, lộ trình là 14 giá trị → 64 → 64 → 1 điểm. Hai nhóm 64 giá trị là các lớp ẩn: các phép tính trung gian, không phải đầu vào hay đáp án cuối.
Một lớp nn.Linear kết hợp giá trị nhận được với trọng số và bias đã học. nn.ReLU giữ giá trị dương và thay giá trị âm bằng không. Các activation này cho phép chèn biến đổi phi tuyến giữa các lớp. nn.Sequential thực thi chúng theo thứ tự khai báo.
Phương thức forward() áp dụng cùng một mạng, với cùng tham số, cho hai hướng rồi trừ các điểm. Đây không phải hai mô hình được huấn luyện riêng.
Để thấy ích lợi, hãy hình dung điểm trực tiếp 2,4 và điểm ngược 0,9. Logit bằng 2,4 − 0,9 = 1,5. Đổi hai dòng: nó thành 0,9 − 2,4 = −1,5. Ưu tiên thực sự đổi chiều. .squeeze(-1) chỉ bỏ chiều cuối có kích thước 1 để giữ một điểm cho mỗi cặp.
Tính chất này chưa đủ để tránh chu trình giữa ba dòng. Nó đảm bảo nhất quán khi đảo cùng một cặp, không phải giữa mọi cặp của trang.
Nhưng việc học diễn ra ở đâu? Trong vòng lặp huấn luyện, không chỉ ở khai báo lớp. Mô hình tính điểm; mất mát so sánh chúng với mục tiêu; PyTorch tính cách tham số ảnh hưởng đến lỗi, rồi optimizer điều chỉnh chúng. Các chỉ dấu biến thiên này là gradient. Hướng dẫn chính thức về tối ưu hóa trình bày cơ chế chung này.
Trong dự án này, mất mát trung bình trước hết lấy trung bình các cặp của từng trang, rồi các trang. Vì vậy trang dày đặc không có trọng số lớn hơn chỉ vì nó có nhiều cặp hơn.
decode_dag() : ngăn mâu thuẫn chặn việc xếp hạng
Mạng hiện cung cấp các ưu tiên. Còn phải ghép chúng lại. Đây là bộ giải mã trong src/pdforder/decode.py :
def has_path(adjacency, start, target):
pending = [start]
visited = set()
while pending:
current = pending.pop()
if current == target:
return True
if current not in visited:
visited.add(current)
pending.extend(adjacency[current])
return False
def decode_dag(lines, pairs, probabilities):
adjacency = [set() for _ in lines]
candidates = []
for (first, second), probability in zip(pairs, probabilities):
source, target = (first, second) if probability >= 0.5 else (second, first)
candidates.append((abs(probability - 0.5), source, target))
candidates.sort(
key=lambda edge: (
-edge[0],
stable_key(lines[edge[1]]),
stable_key(lines[edge[2]]),
)
)
rejected = []
for confidence, source, target in candidates:
if has_path(adjacency, target, source):
rejected.append([source, target, confidence])
else:
adjacency[source].add(target)
degree = [0] * len(lines)
for targets in adjacency:
for target in targets:
degree[target] += 1
available = [i for i, d in enumerate(degree) if d == 0]
order = []
while available:
current = min(available, key=lambda i: stable_key(lines[i]))
available.remove(current)
order.append(current)
for target in adjacency[current]:
degree[target] -= 1
if degree[target] == 0:
available.append(target)
if len(order) != len(lines):
raise ValueError("Internal cycle escaped detection")
return order, rejected
Trước hết, định hướng các mũi tên. Nếu xác suất lớn hơn hoặc bằng 0.5, dòng đầu là nguồn và dòng thứ hai là đích. Nếu không, hướng đảo lại. adjacency lưu, với từng dòng, các dòng mà nó trỏ tới.
Tiếp theo, xử lý các ưu tiên chắc chắn nhất. abs(probability - 0.5) đo khoảng cách tới điểm lưỡng lự. Xác suất 0,95 cho 0,45; xác suất 0,10 cho 0,40, nhưng theo hướng khác. Khoảng cách càng lớn, quan hệ càng được xét sớm. Điều đó không bảo đảm quan hệ ấy đúng.
Trước mỗi lần thêm, kiểm tra khả năng quay lại. has_path(adjacency, target, source) tìm đường đi từ đích về nguồn. Nếu đã tồn tại, thêm mũi tên theo chiều kia sẽ khép một chu trình. Khi ấy nó được đưa vào rejected thay vì thêm vào đồ thị.
Cuối cùng, tạo danh sách. degree đếm các tiền nhiệm còn lại của mỗi dòng. available chứa các dòng không còn tiền nhiệm. Chương trình chọn một dòng, thêm nó vào order, rồi dần giải phóng các dòng kế tiếp. stable_key, hàm phụ trợ của dự án, phân xử các trường hợp bằng nhau theo cách có thể tái lập.
Kiểm tra cuối yêu cầu mọi dòng đều xuất hiện trong thứ tự nhận được. Nó báo lỗi nhất quán nội bộ nếu không phải vậy. Kết quả gồm danh sách đã sắp và các quan hệ bị từ chối: vì thế ta có thể xem xét các đánh đổi, thay vì chỉ nhận văn bản cuối.
Điều kiện và kiểm tra trước khi huấn luyện
Bạn cần đọc được hàm Python, thao tác danh sách và dùng terminal. Các khái niệm học máy cần thiết đã được giới thiệu ở trên; không cần kinh nghiệm trước về mô hình ngôn ngữ lớn.
Lộ trình đã xác thực dùng Linux, Docker Engine, Compose, driver NVIDIA và NVIDIA Container Toolkit. Cấu hình quan sát là RTX 3090 với PyTorch ở float32. Cấu hình CPU là NOT_TESTED: chúng tôi không trình bày nó như phương án thay thế đã được xác thực.
README dự kiến cần 16 Gio bộ nhớ khả dụng và khoảng 12 Gio đĩa cho image và dữ liệu. Đây là ước tính chuẩn bị, không bảo đảm mức tiêu thụ trên máy của bạn. Không cần mô hình tiền huấn luyện bên ngoài nào: MLP được khởi tạo cục bộ.
Giải nén ZIP và mở terminal tại thư mục gốc của dự án. Tất cả lệnh sau được chạy từ thư mục này, theo thứ tự.
Dịch vụ tutorial dùng để chuẩn bị, huấn luyện và đánh giá; inference dự đoán không dùng labels. Các phép tính của chúng chạy không cần mạng. Dịch vụ acquisition riêng biệt kiểm tra các tài nguyên bên ngoài cần cho dự án; ở đây nó không tải mô hình nào.
Bước 1 — chuẩn bị và kiểm tra môi trường
Preflight là kiểm tra trước: nó xác minh Docker và khả năng nhìn thấy GPU dưới Linux. Kiểm tra môi trường sau đó thực sự chạy phép tính CUDA trong Docker. Thấy card qua nvidia-smi trên máy chủ không chứng minh container dùng được nó.
Docker Compose cho phép cấu hình quyền truy cập GPU này cùng tích hợp do NVIDIA Container Toolkit cung cấp. Các lệnh dự án kiểm tra lộ trình ta cần.
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 --config configs/minimum.yaml
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, nghĩa là không báo lỗi. Bản tái lập tham chiếu đã xác thực 27 kiểm thử công khai, về tọa độ, nhóm ký tự, chu trình, đầu vào PDF bị từ chối và các biện pháp bảo vệ.
Các kiểm thử này kiểm soát hành vi của chương trình. Chúng không chứng minh mô hình sẽ đọc đúng tài liệu tương lai của bạn.
Bước 2 — tạo, kiểm tra và so sánh baseline
Trước khi huấn luyện bất cứ gì, ta chuẩn bị trang và xem một phương pháp đơn giản đã làm được gì. Đó là baseline, kết quả tham chiếu để so sánh việc học.
docker compose run --rm tutorial python -m pdforder.cli prepare --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdforder.cli inspect-pairs --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdforder.cli baseline --config configs/minimum.yaml
prepare phải tạo 600 trang, chia thành 420/90/90 cho huấn luyện, xác thực và kiểm thử.
inspect-pairs hiển thị số cặp, 14 đặc trưng và việc không có văn bản hay mã định danh trong đầu vào mạng. Đây là lúc kiểm tra mô hình chỉ nhận thông tin đã định.
baseline tính hai thứ tự không học. Sắp xếp dọc theo chiều cao của dòng: nó cố ý dễ sai khi nhiều cột chiếm cùng hàng. Phương pháp cột áp dụng quy tắc hình học rõ ràng.
Hãy giữ các đầu ra này. Câu hỏi không chỉ là « mô hình có hoạt động không? », mà còn là « nó có tốt hơn quy tắc đơn giản trên cùng dữ liệu không? ».
Bước 3 — huấn luyện và kiểm soát việc tiếp tục
Một epoch tương ứng một lượt đi qua dữ liệu huấn luyện. Lộ trình ở đây so sánh huấn luyện liên tục với huấn luyện bị ngắt rồi tiếp tục.
docker compose run --rm tutorial python -m pdforder.cli train --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdforder.cli train --config configs/minimum.yaml --mode continuous
docker compose run --rm tutorial python -m pdforder.cli train --config configs/minimum.yaml --mode resume
docker compose run --rm tutorial python -m pdforder.cli verify-resume --config configs/minimum.yaml
Lệnh đầu dừng sau epoch 6. Lệnh thứ hai chạy đối chứng liên tục: nó bắt đầu lại từ cùng trạng thái ban đầu và đạt 12 epoch. Lệnh thứ ba tiếp tục trạng thái bị ngắt và thực hiện sáu epoch còn lại. Lệnh cuối so sánh hai lộ trình.
Bản lưu dùng để tiếp tục gọi là checkpoint. Để tiếp tục huấn luyện trung thực, chỉ giữ trọng số mô hình là chưa đủ: cần khôi phục trạng thái chi phối các lần cập nhật tiếp theo. Tài liệu PyTorch về lưu huấn luyện giải thích rõ vai trò của trạng thái optimizer.
Trong dự án này, verify-resume so sánh tham số, optimizer, thứ tự lấy mẫu, bộ sinh ngẫu nhiên và checkpoint tốt nhất. Trong bản tái lập tham chiếu, độ lệch tối đa giữa lộ trình liên tục và tiếp tục là 0,0 sau 12 epoch.
Kiểm soát này trả lời câu hỏi về khả năng tái lập: việc tiếp tục có đi đúng cùng con đường trong điều kiện đã kiểm tra không? Nó chưa đo chất lượng thứ tự đọc.
Bước 4 — dự đoán và đo lường
Mô hình đã sẵn sàng. Bây giờ ta chuyển sang dự đoán, rồi so sánh nó với các tham chiếu.
docker compose run --rm inference python -m pdforder.predict --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdforder.cli evaluate --config configs/minimum.yaml
Dự đoán tạo bốn thứ tự trên cùng các dòng: dọc, cột, DAG và Borda. Borda là cách khác để kết hợp ưu tiên: nó cộng chúng để có điểm xếp hạng theo dòng, thay vì dựng đồ thị và từ chối chu trình.
Đánh giá chỉ nối với chú thích tham chiếu trên tập kiểm thử. Nó ghi ra các xuất ví dụ, metric của từng trang và metric gộp, tức các phép đo tóm tắt toàn bộ tập được đánh giá.
Mở Markdown và SVG đã tạo. Theo dõi tiêu đề, cột trái, cột phải, khung và chân trang. Mỗi phương pháp đi qua đâu? Trung bình giúp so sánh, nhưng trang có chú thích giúp hiểu điều gì thực sự đã được sắp lại.
Đọc các phép đo trước khi đọc số
Cái bẫy là đòi một « điểm đọc » duy nhất. Ta có nhiều giai đoạn, nên cần phân biệt nhiều loại lỗi.
Độ bao phủ: ta đã tìm được các dòng chưa?
Độ bao phủ đo phần các dòng tham chiếu được tìm lại khi trích xuất. Giá trị 1,0 nghĩa là tất cả đều có mặt. Chỉ riêng nó không nói gì về thứ tự.
Hãy hình dung năm dòng cần có A, B, C, D và E. Nếu trích xuất chỉ tìm A, B, D và E, độ bao phủ là bốn trên năm, tức 80 %. Ví dụ này minh họa phép đo; nó không phải trang bổ sung của corpus.
Đảo ngược có điều kiện: các dòng tìm được có đúng chiều không?
Tỷ lệ đảo ngược có điều kiện xét các cặp dòng đã trích xuất có thể ghép với dòng tham chiếu. Một cặp bị đảo nếu dự đoán đặt B trước A khi tham chiếu yêu cầu A trước B. Ở đây, giá trị càng thấp càng tốt.
Vì sao « có điều kiện »? Vì phép đo dựa trên dòng đã ghép. Trong ví dụ, A, B, D và E có thể được sắp hoàn hảo: tỷ lệ bằng không, dù C vẫn thiếu. Một giá trị tốt vì vậy chưa đủ chứng nhận trích xuất.
Thành công cặp đầu-cuối: các phần mất có còn nhìn thấy?
Thành công cặp đầu-cuối giữ các mất mát trích xuất trong mẫu số. Nói cách khác, nó không loại khỏi phép tính các cặp không thể dựng lại vì thiếu một dòng.
Nó bổ sung tỷ lệ có điều kiện. Một phương pháp không nên trông hoàn hảo chỉ vì nó sắp tốt tập con đã trích xuất được. Với phép đo này, giá trị gần 1 là mong muốn.
Adjacency F1: các hàng xóm đúng còn kề nhau không?
Adjacency F1 so sánh quan hệ giữa các dòng liền kề. Nếu tham chiếu là A, B, C, D, các láng giềng là A→B, B→C và C→D. Trong A, C, B, D, không quan hệ nào trong ba quan hệ ấy được giữ, dù tất cả dòng đều có mặt.
Điểm F1 kết hợp precision, phần láng giềng dự đoán đúng, và recall, phần láng giềng mong đợi được tìm lại. Tài liệu về điểm F1 nêu định nghĩa chung này; liên kết là tài liệu đọc thêm, không phải thêm dependency vào dự án.
Bây giờ ta có thể đọc kết quả mà không nhầm lẫn dòng hiện diện, cặp đúng hướng và láng giềng được giữ.
Kết quả của lộ trình tham chiếu
Bảng áp dụng cho 90 trang kiểm thử và làm tròn giá trị đến bốn chữ số thập phân. Giá trị tốt nhất là 0 cho đảo ngược, 1 cho ba phép đo khác.
Trên thiết bị di động, hãy cuộn bảng theo chiều ngang.
| Phương pháp | Độ bao phủ | Đảo ngược có điều kiện | Thành công đầu-cuối | Adjacency F1 |
|---|---|---|---|---|
| Sắp xếp dọc | 1,0000 | 0,2552 | 0,7448 | 0,1039 |
| Cột | 1,0000 | 0,0000 | 1,0000 | 1,0000 |
| DAG đã học | 1,0000 | 0,0000 | 1,0000 | 1,0000 |
| Borda | 1,0000 | 0,0000 | 1,0000 | 1,0000 |
Sắp xếp dọc giữ mọi dòng, nhưng tỷ lệ đảo ngược là 0,2552, tức khoảng 25,52 %. Adjacency F1 vẫn thấp, 0,1039. Đây đúng là vấn đề ban đầu: từ và dòng đều có mặt, nhưng lộ trình đọc bị xáo trộn.
Các phương pháp cột, DAG đã học và Borda đạt cùng kết quả hoàn hảo trong kiểm thử này. Vì vậy việc học không cho thấy lợi ích nào so với quy tắc hình học theo cột trong thí nghiệm này.
Đây không phải kết quả cần che giấu. Với phân phối này, quy tắc rõ ràng đã đủ. Mục tiêu hướng dẫn là hiểu và so sánh cơ chế, không phải gán lợi thế cho AI theo nguyên tắc.
Corpus vẫn tổng hợp, bằng tiếng Anh và được dựng bằng hình học đã biết. Các con số này không chứng minh MLP vượt trội nói chung trên hóa đơn, hợp đồng, bản quét, bảng hay PDF khách hàng. Chúng mô tả điều giao thức đã đo trên chính các trang kiểm thử.
Bài tập: dự đoán rồi quan sát sự hiệu chỉnh
Lấy một tờ giấy và vẽ ba dòng A, B và C. Thêm các ưu tiên A→B ở 0,95, C→A ở 0,90 và B→C ở 0,85.
Trước khi chạy lệnh, hãy tìm chu trình. DAG sẽ xem mũi tên nào trước? Nó phải từ chối mũi tên nào? Thứ tự nào còn khả dĩ? Với Borda, thử cộng xác suất có lợi cho từng dòng và nhận ra khả năng bằng nhau.
docker compose run --rm tutorial python -m pdforder.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
Bài tập không huấn luyện mô hình mới. Đầu ra là C, A, B cho DAG và A, C, B cho Borda. Hãy dành thời gian giải thích các kết quả này.
Với DAG, các khoảng cách tới 0,5 lần lượt là 0,45, 0,40 và 0,35. Bộ giải mã trước hết giữ A→B, rồi C→A. Do đó ta có đường C→A→B. Thêm B→C sẽ đưa ta về C: B→C bị từ chối, và thứ tự nhận được là C, A, B.
Borda tìm thỏa hiệp khác bằng cách cộng các ưu tiên. Với xác suất thập phân của đề bài, tính tay cho 0,95 + 0,10 = 1,05 với A, 0,05 + 0,85 = 0,90 với B và 0,90 + 0,15 = 1,05 với C. Các giá trị ngược đến từ 1 − p.
Một chi tiết đáng chú ý: A và C ở đây có cùng tổng lý thuyết. Chỉ các tổng này không thể phân định chúng. A, C, B là thứ tự được bài tập cung cấp báo cáo; không được suy ra A có tổng lớn hơn C. Hãy xem đầu ra để biết xếp hạng thực sự tạo ra.
Bài tập cũng ghi dấu vết, hay hash, của checkpoint dùng cho các so sánh thực. Các hình được tạo sau quan sát này. Hãy so sánh dự đoán của bạn với tệp đầu ra: điều quan trọng là theo dõi quyết định của bộ giải mã, không phải đoán thứ tự mà không xem phép tính.
Những điều chúng ta đã học
Thứ tự đọc đúng không nhất thiết là thứ tự các đối tượng được vẽ trong PDF. Ta đã tách việc lấy ký tự, nhóm chúng thành dòng, ước lượng ưu tiên giữa các cặp và xây dựng xếp hạng.
Mô hình học từ hình học mà không đọc văn bản. DAG biến ưu tiên thành quan hệ không chu trình; Borda cộng chúng và có thể cho thỏa hiệp khác. Vì vậy lựa chọn bộ giải mã là một phần của hệ thống, không kém chính mạng.
Cuối cùng, ta phân biệt dòng bị mất, cặp bị đảo và láng giềng được giữ. Kết quả không cho thấy lợi ích so với baseline hình học theo cột: chúng mang lại thí nghiệm tái lập để hiểu dữ liệu, đầu vào, mô hình, bộ giải mã và phép đo.
Kết quả bằng quy tắc đơn giản vẫn là thông tin hữu ích: nó chỉ ra điều mà việc học chưa chứng minh.
Điều chỉnh, khắc phục và dọn dẹp
Chuyển sang tài liệu khác mà không mất mốc
Để mở rộng dự án, hãy bắt đầu với PDF mà bạn được phép xử lý và chú thích phù hợp với khung, ghi chú, bảng, ảnh và trang hỗn hợp của chúng. Xác định thứ tự mong đợi là một phần công việc: không thể đánh giá đúng khung nếu chưa quyết định phần đọc của nó phải được chèn vào đâu.
Tách huấn luyện, xác thực và kiểm thử theo nguồn tài liệu. Thêm đánh giá của con người về khả năng đọc và kiểm tra lỗi theo họ. Kết quả trung bình thỏa đáng có thể che giấu một loại trang xử lý kém.
Các giới hạn của lộ trình vẫn còn: trích xuất có thể mất dòng; đồ thị có thể giữ một ưu tiên sai; kiểm thử xoay không chứng minh việc học đọc văn bản dọc.
Với dịch vụ sản xuất, cũng cần quyền truy cập, xác thực tệp, cô lập mạnh hơn cho parser PDF, giám sát, đo độ trễ và lỗi, kiểm thử tải, sao lưu và chính sách lưu giữ. Các chuỗi được trích xuất vẫn là dữ liệu: đừng bao giờ thực thi chúng như lệnh. Các kiểm soát sư phạm của dự án không phải biện pháp bảo vệ phổ quát trước lỗ hổng thư viện PDF.
Chẩn đoán bước mắc lỗi
CUDA thất bại? Hãy chạy lại kiểm tra môi trường trong Docker và xác minh tích hợp NVIDIA trước khi đụng đến mạng nơ-ron.
Các cột có vẻ bị đảo? Hãy kiểm tra CropBox, xoay và hệ tọa độ. Nếu tọa độ sai, thay đổi huấn luyện không sửa được nguyên nhân.
Các dòng đã lẫn lộn trước khi xếp hạng? Hãy kiểm tra việc nhóm theo hàng và tách các khoảng trống lớn. Đừng thay quan sát bằng labels để có đầu ra tốt hơn: bạn sẽ lách đúng vấn đề cần đo.
Giữ kết quả hữu ích rồi dọn dẹp
Sao chép các hình và metric bạn muốn giữ trước khi dọn dẹp. Script dưới đây chỉ nhắm outputs/; checkpoint vẫn ở assets/checkpoints/ và dữ liệu tạo ra ở data/generated/.
docker compose down
docker compose run --rm tutorial python scripts/clean.py --outputs-only
docker compose down
Lệnh đầu dừng môi trường Compose. Lệnh thứ hai chạy việc dọn dẹp do dự án quy định; lệnh cuối đóng lại môi trường. Nhờ vậy bạn giữ dữ liệu và bản lưu huấn luyện, không nhầm việc xóa chúng với chỉ xóa kết quả đã xuất.
Các nguồn chính thức đã tham khảo
Tài liệu tham chiếu của lộ trình
- Docker Compose: truy cập GPU: cấu hình truy cập GPU cho các dịch vụ.
- NVIDIA Container Toolkit: cài đặt và cấu hình Docker: tích hợp driver NVIDIA vào môi trường container.
- PyTorch: BCEWithLogitsLoss: tính ổn định mất mát nhị phân từ các logit.
Tài liệu đọc thêm
Các tham chiếu liên kết trong văn bản giải thích lớp Linear và ReLU, tối ưu hóa, checkpoint, sắp xếp tô-pô và điểm F1. Chúng làm sáng tỏ khái niệm; chúng không tạo thành xác thực mới cho dự án.
Để tái tạo các phép đo, hãy giữ tệp, dependency và cấu hình của ZIP r004. Các tài liệu hiện hành có thể thay đổi độc lập với phiên bản này.