Bản quét không phải là văn bản
Bạn đang phát triển chức năng nhập tài liệu. Một người dùng tải lên phiếu giao hàng PDF « Cedar Parts »: trên màn hình, bạn dễ dàng đọc được BOLT AA11 12.50 và SEAL B00 7.20. Bạn chạy trích xuất văn bản… nhưng không nhận được gì.
Vì sao? Vì PDF này chứa ảnh của trang, chứ không phải các ký tự bạn nhìn thấy. Nó giống như nhận ảnh chụp màn hình của một biểu mẫu thay vì các trường của nó: thông tin vẫn hiện ra, nhưng không còn tồn tại dưới dạng chuỗi ký tự.
Đó là điểm khởi đầu của OCR, viết tắt của Optical Character Recognition hay nhận dạng ký tự quang học. Ta phải chuyển từ pixel thành văn bản. Và chúng ta sẽ tự xây dựng bước chuyển này, thay vì chỉ gọi một dịch vụ che giấu cách hoạt động.
Phạm vi thực hành sẽ cố ý giới hạn: các dòng in từ một tập dữ liệu hư cấu, với bảng chữ cái thu hẹp. Chúng ta sẽ không học cách đọc mọi hóa đơn trên thế giới, chữ viết tay hay các trang bố cục phức tạp. Bù lại, chúng ta sẽ hiểu cách một mạng học phiên âm một dòng mà không được chỉ vị trí của từng ký tự.
Kết quả cũng là một phần của việc học: mô hình nhỏ này tiến bộ, nhưng chất lượng cuối cùng vẫn chưa đủ để tự động hóa việc trích xuất nghiệp vụ. Chúng ta sẽ xem chính xác điều gì hoạt động và điều gì không.
Tải xuống dự án hoàn chỉnh của hướng dẫn
Những gì bạn sẽ xây dựng và học
Chuỗi xử lý đi theo tài liệu: PDF quét → ảnh → dòng được phát hiện → mạng CNN-GRU → điểm ký tự → giải mã CTC → văn bản.
Bạn chưa cần ghi nhớ mọi chữ viết tắt. Trước hết, hãy hình dung ba trách nhiệm.
Tìm các dòng. Chúng ta xác định các vùng trang có văn bản và cắt một ảnh nhỏ cho mỗi dòng. Bước này được gọi là phân đoạn.
Phân tích từng ảnh dòng. CNN biến pixel thành các mẫu thị giác. GRU liên kết thông tin này khi duyệt dòng theo hai chiều. Sự kết hợp của chúng tạo ra một dãy điểm cho các ký tự có thể có.
Học và giải mã mà không cắt từng chữ cái. CTC cho phép huấn luyện mạng với toàn bộ bản phiên âm của dòng, rồi diễn giải một chuỗi có thể chứa lặp lại và các ký hiệu kỹ thuật.
Chúng ta sẽ huấn luyện mạng này từ trạng thái khởi tạo, kiểm tra nó có thể tiếp tục sau khi bị ngắt hay không, rồi đo riêng việc cắt và nhận dạng. Cuối cùng là một thí nghiệm: làm bản quét mờ hơn mà không huấn luyện lại mô hình, dự đoán điều sẽ xảy ra rồi so sánh trực giác với kết quả.
Sợi chỉ xuyên suốt sẽ là AA11. Mã nhỏ này đặc biệt thú vị: làm sao phân biệt hai chữ A thật với cùng một A mà mạng đã đề xuất nhiều lần? Đây chính là một trong các vấn đề CTC xử lý.
Chuẩn bị dự án và kiểm tra GPU
Bạn cần biết lập trình Python, dùng terminal và chạy Docker. Bạn không cần từng huấn luyện mô hình. Chúng tôi sẽ giải thích các khái niệm khi dùng đến.
Tuy nhiên, một từ cần được định nghĩa trước: tensor ở đây là một mảng số có một hay nhiều chiều. Ảnh, một lô ảnh hoặc điểm ký tự đều được biểu diễn bằng các mảng như vậy. Mạng cũng chứa các số, tức tham số của nó, mà quá trình huấn luyện điều chỉnh để giảm lỗi trên những bản phiên âm đã biết.
Dùng lại môi trường của lộ trình đã xác thực
Lộ trình đã chạy với RTX 3090, Python 3.11.16, PyTorch 2.8.0+cu126 và CUDA 12.6. PyTorch là thư viện dùng để xây dựng và huấn luyện mạng; CUDA cho phép thực hiện các phép tính dự kiến trên GPU NVIDIA.
Các phiên bản này mô tả môi trường đã được xác thực. Dockerfile trong ZIP cố định các phụ thuộc: đừng thay chúng bằng bản mới nhất khi tái tạo lần đầu. Xem README để biết yêu cầu kích thước, và dành đủ chỗ cho ảnh Docker, bản quét được tạo và checkpoint, tức các bản lưu huấn luyện.
Giải nén ZIP và đặt terminal ở thư mục gốc của nó, nơi có README.md. Mọi lệnh sau đều chạy từ thư mục này, theo đúng thứ tự. Các đoạn mã giải thích ở dưới thuộc dự án hoàn chỉnh; chúng không thay thế các tệp khác.
Hãy bắt đầu bằng kiểm tra máy chủ, gọi là preflight:
sh scripts/preflight.sh
GPU hiển thị được dưới Linux không bảo đảm Docker dùng được nó. NVIDIA Container Toolkit cung cấp tích hợp giữa Docker và trình điều khiển máy chủ; tài liệu chính thức trình bày cách cài đặt theo hệ thống. Chúng ta sẽ kiểm tra quyền truy cập CUDA của container sau khi xây dựng.
Xây dựng các dịch vụ và hiểu vai trò của chúng
Hãy chuẩn bị thư mục, kiểm tra cấu hình Compose rồi xây dựng các dịch vụ:
sh scripts/setup.sh
docker compose config --quiet
docker compose build acquisition tutorial inference
acquisition kiểm tra tài nguyên của dự án. tutorial chuẩn bị dữ liệu và huấn luyện mạng. inference phiên âm các bản quét bằng trọng số đã lưu. Tệp được phân bổ trong data/generated/ cho bản quét, assets/checkpoints/ cho trạng thái mô hình và outputs/ cho kết quả.
Không có trọng số OCR tiền huấn luyện nào cần tải xuống. Ở đây mạng học từ lúc khởi tạo; chúng ta không chuyên biệt hóa một bộ máy đã được huấn luyện. Bước thu nhận vẫn được giữ trong lộ trình và vì vậy báo cáo không byte trọng số bên ngoài nào. Điều đó không có nghĩa là việc xây dựng ảnh Docker không cần tải gì.
docker compose run --rm acquisition python scripts/acquire.py --config configs/minimum.yaml
Bây giờ hãy kiểm tra CUDA từ container, sau đó là các kiểm thử của dự án:
docker compose run --rm tutorial python scripts/check_environment.py
docker compose run --rm tutorial python -m pytest -q
Kiểm tra môi trường phải xác nhận truy cập GPU. 26 kiểm thử thành công trong lộ trình tham chiếu bao phủ, nhất là, phân đoạn, độ dài và giải mã.
Hãy nghĩ đến kiểm thử ứng dụng của bạn: xác minh một hàm tuân thủ hợp đồng không giống với xác minh tính phù hợp của toàn bộ dữ liệu nó sẽ tạo ra. Ở đây cũng vậy, kiểm thử thành công không có nghĩa mọi bản phiên âm đều đúng.
Sau đó, các dịch vụ huấn luyện và suy luận không có mạng. Tệp Compose đầy đủ mô tả các volume chỉ đọc, người dùng không đặc quyền, các capability bị loại bỏ và giới hạn tài nguyên. Các hạn chế này giảm quyền truy cập của chương trình; chúng không phải bảo vệ hoàn chỉnh trước mọi lỗ hổng của thư viện native.
Phiếu giao hàng thực sự được raster hóa
Tạo bản quét mà ta biết đáp án
Để huấn luyện OCR, ta cần ảnh và bản phiên âm mong đợi. Dự án tự tạo các ví dụ này: nhờ vậy ta biết đáp án mà không cần nhờ ai chép lại từng dòng.
Bộ sinh prepare, trong src/pdfctc/corpus.py, tạo 600 trang gồm mười dòng. Nó render chúng thành ảnh, chèn lại ảnh này vào PDF, rồi lại biến PDF thành ảnh. Sự biến đổi sang pixel này gọi là raster hóa.
Vì sao cần các bước này? Để tài liệu gửi vào chuỗi của chúng ta thực sự là PDF quét, không phải PDF mà chương trình có thể chỉ đọc lại các ký tự ẩn phía sau hiển thị. Bộ sinh kiểm tra văn bản gốc rỗng.
Mỗi trang có ba dạng: sạch, suy giảm hoặc rất mờ. Đây là các phiên bản khác nhau của cùng một trang, không phải ba họ tài liệu độc lập. Corpus là hư cấu, được in bằng ký tự ASCII và phông PDF chuẩn. Nó không đại diện cho sự đa dạng hóa đơn, ngôn ngữ hay chữ viết tay của một doanh nghiệp.
Tách tài liệu trước khi học
60 họ tài liệu được chia thành 42 cho huấn luyện, 9 cho xác thực và 9 cho kiểm thử. Tương ứng là 4 200, 900 và 900 dòng.
Huấn luyện dùng để điều chỉnh tham số. Xác thực dùng để quan sát hành vi trong lúc tinh chỉnh, nhưng không phải tập dữ liệu dùng để học các tham số đó. Kiểm thử dùng để đo kết quả trên các ví dụ dành riêng cho đánh giá cuối.
Việc chia tách thực hiện theo họ bố cục, không chỉ phân phối ngẫu nhiên toàn bộ dòng. Vì thế cùng một họ không nằm ở cả hai bên ranh giới giữa học và đánh giá. Điều này hạn chế so sánh quá dễ giữa các tài liệu liên quan, dù vẫn không bao phủ mọi đa dạng tài liệu thực.
Ở đây, một annotation chứa bản phiên âm mong đợi và khung bao quanh dòng. Vì vậy ta biết văn bản của dòng và vị trí tổng thể của nó, nhưng không cung cấp khung cho từng chữ cái. CTC giúp tránh annotation theo từng ký tự; nó không xóa nhu cầu có bản phiên âm để huấn luyện mô hình.
Tìm dòng trước khi nhận dạng
Hàm line_boxes, trong src/pdfctc/segment.py, tìm các hàng pixel tối, gom các hàng lân cận rồi tạo một khung cho mỗi dòng. Để hình dung, hãy tưởng tượng bạn quét trang từ trên xuống dưới và đo nơi có mực.
Phương pháp này tường minh, không phải một mạng thứ hai cần huấn luyện. Nó định vị các dòng; nó chưa biết chúng chứa gì. Phần ảnh lấy từ một khung gọi là crop, hay ảnh cắt.
Hãy tạo các bản quét rồi xem những ảnh cắt này:
docker compose run --rm tutorial python -m pdfctc.cli prepare --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfctc.cli inspect-lines --config configs/minimum.yaml
Bạn phải thấy 600 trang và ba dạng của chúng. Trong tệp kiểm tra, hãy xem các khung quanh dòng và chiều rộng sau khi đổi kích thước. PDF được tạo phải vẫn không có lớp văn bản gốc: nếu không, ta không còn kiểm thử nhận dạng từ pixel.
Hãy chọn một dòng và tự hỏi hai câu khác nhau: « tôi đã lấy được cả dòng chưa? » và « mạng có biết đọc nó không? ». Câu đầu về phân đoạn, câu sau về nhận dạng.
Đưa ảnh về một định dạng chung
normalize, cũng trong src/pdfctc/segment.py, đưa mỗi crop về cao 32 pixel, đồng thời giữ tỷ lệ. Chiều rộng lớn hơn 512 pixel gây lỗi: không bao giờ âm thầm cắt phần cuối văn bản để ép ảnh vừa.
Đây là hàm của dự án:
def normalize(image, height=32, max_width=512):
width = max(4, round(image.width * height / image.height))
if width > max_width:
raise ValueError(
f"Line width {width} exceeds {max_width}; no text truncation allowed"
)
array = (
1.0
- np.asarray(
image.convert("L").resize((width, height), Image.Resampling.BILINEAR),
dtype=np.float32,
)
/ 255.0
)
return array
Lệnh đầu tiên tính chiều rộng mới từ tỷ lệ các chiều cao, với tối thiểu 4 pixel. Là ví dụ tính toán, ảnh rộng 400 pixel và cao 64 pixel thành ảnh 200 × 32 pixel. Các kích thước này minh họa mã; chúng không mô tả một phép đo bổ sung của corpus.
image.convert("L") biến ảnh thành thang xám. resize đổi kích thước bằng phép nội suy song tuyến tính chỉ ra trong mã. Các thao tác này được ghi trong module Image của Pillow.
np.asarray(..., dtype=np.float32) sau đó biến ảnh thành mảng số. Chia cho 255 đưa cường độ về từ 0 đến 1; 1.0 - ... đảo thang đó. Pixel đen ban đầu là 0 trở thành 1. Pixel trắng ban đầu là 255 trở thành 0. Vì vậy mực tối tương ứng với giá trị cao.
Giờ ta có ảnh số với chiều cao đã biết mà không ép mọi dòng phải chứa cùng số ký tự.
CNN rồi GRU: xây dựng chuỗi
CNN: học mẫu, không phải một quy tắc cho từng chữ
CNN, viết tắt của Convolutional Neural Network, là mạng tích chập. Nó áp dụng các bộ lọc lên những vùng khác nhau của ảnh. Giá trị các bộ lọc này được học khi huấn luyện: ta không viết chuỗi điều kiện kiểu « nếu hình này giống chữ A… ».
Các bộ lọc này có thể biểu diễn cạnh, mảnh chữ cái hoặc khoảng trắng. Đây là cách hiểu vai trò của chúng, không phải quan sát các bộ lọc thực sự đã học trong thí nghiệm.
Trong mạng của chúng ta, ba lớp tích chập xây dựng các biểu diễn thị giác này. Hai thao tác max-pooling giảm độ phân giải bằng cách giữ giá trị lớn nhất trong vùng nhỏ. Mã dùng vùng 2 × 2, nên dần giảm chiều cao và chiều rộng. Hợp đồng của các thao tác này có trong tài liệu Conv2d và MaxPool2d.
GRU: đặt mỗi vị trí vào ngữ cảnh lân cận
GRU, viết tắt của Gated Recurrent Unit, là mạng hồi quy: nó giữ một trạng thái khi duyệt một chuỗi. Ở đây chuỗi này tương ứng với chiều rộng của dòng sau khi đi qua CNN.
Phiên bản hai chiều duyệt biểu diễn này từ trái sang phải và từ phải sang trái. Nhờ đó, một vị trí có thể được diễn giải với ngữ cảnh hai bên. Ngữ cảnh này có thể giúp phân biệt 0 với O, nhưng không bảo đảm phân biệt đúng. Tài liệu GRU của PyTorch mô tả sự kết hợp hai hướng này.
Sự kết hợp mạng tích chập và mạng hồi quy ở đây gọi là CRNN. Một phép chiếu cuối tạo, cho mỗi vị trí, một điểm cho từng ký tự của bảng chữ cái và một điểm cho ký hiệu kỹ thuật CTC blank.
Theo dõi đường đi của các con số trong mã
Đây là lớp hoàn chỉnh trong src/pdfctc/model.py. Hãy quan sát ba phần: cnn, gru và projection.
class CRNN(nn.Module):
def __init__(self, classes):
super().__init__()
self.cnn = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
)
self.gru = nn.GRU(128, 128, num_layers=2, bidirectional=True, batch_first=True)
self.projection = nn.Linear(256, classes)
def forward(self, images, lengths):
features = self.cnn(images).mean(dim=2).transpose(1, 2)
# The backward GRU must not read the right-hand padding as real context.
packed = pack_padded_sequence(
features, lengths.cpu(), batch_first=True, enforce_sorted=False
)
encoded, _ = self.gru(packed)
encoded, _ = pad_packed_sequence(
encoded, batch_first=True, total_length=features.shape[1]
)
return self.projection(encoded).transpose(0, 1)
__init__ định nghĩa các lớp và kích thước của chúng. forward mô tả đường đi của dữ liệu qua các lớp: chính phương thức này biến một lô ảnh thành các điểm.
Các lớp tích chập đi từ một kênh đầu vào, tức thang xám, lên 32, rồi 64, rồi 128 kênh đặc trưng. ReLU đưa các biến đổi phi tuyến vào giữa các lớp tích chập. Khi đã trích xuất các mẫu này, mean(dim=2) lấy trung bình chiều cao còn lại; transpose(1, 2) đặt các vị trí ngang theo thứ tự GRU mong đợi.
Để làm các phép biến đổi này cụ thể, hãy theo dõi một ảnh chuẩn hóa 200 × 32 pixel, chỉ như ví dụ kích thước suy ra từ mã. Đây không phải kết quả thực thi được công bố.
| Bước | Biểu diễn của dòng này |
|---|---|
| Đầu vào | 1 kênh, cao 32, rộng 200. |
| Sau ba lớp tích chập và hai max-pooling | 128 kênh, cao 8, rộng 50. |
| Sau trung bình theo chiều cao và chuyển vị | 50 vị trí liên tiếp, mỗi vị trí được mô tả bởi 128 giá trị. |
| Sau GRU hai chiều | 50 vị trí, mỗi vị trí được mô tả bởi 256 giá trị: 128 cho mỗi hướng. |
| Sau phép chiếu | 50 vị trí, với một điểm cho mỗi ký hiệu có thể. |
Các vị trí này gọi là bước thời gian. Thuật ngữ có thể gây ngạc nhiên: ở đây ta không xử lý video hay tín hiệu âm thanh. « Thời gian » chỉ đơn giản là tiến trình trên chuỗi ngang.
50 bước không có nghĩa là 50 ký tự. Nhiều bước có thể góp phần cho cùng một ký tự, và một số có thể tạo blank. Vì vậy cần một cơ chế căn chỉnh và giải mã.
Phép chuyển vị cuối trả các điểm theo thứ tự kích thước CTC mong đợi: bước thời gian, phần tử lô, lớp ký hiệu. Một điểm thô gọi là logit. Nó chưa phải xác suất cũng chưa là chữ cái được nhận dạng dứt khoát.
Không nhầm phần đệm với nội dung
Để xử lý cùng lúc nhiều dòng có độ rộng khác nhau, ta đặt chúng trong một lô, hay batch, có độ rộng chung. Vì thế dòng ngắn hơn có phần biên nhân tạo ở bên phải: padding, hay phần đệm.
lengths lưu độ dài thật sau khi CNN giảm kích thước. pack_padded_sequence cho phép GRU làm việc trên các độ dài đó thay vì toàn bộ chiều rộng đã đệm. Điều này đặc biệt quan trọng theo chiều phải-trái: nếu không, nó sẽ bắt đầu đọc biên nhân tạo như thể là một phần của dòng.
pad_packed_sequence sau đó đưa kết quả trở lại mảng có kích thước chung. Bạn sẽ tìm thấy hợp đồng của cách đóng gói này trong tài liệu pack_padded_sequence.
CTC: blank, lặp lại và độ dài
Vấn đề: có bản phiên âm nhưng không có vị trí từng chữ
Ta biết dòng của mình phải chứa BOLT AA11 12.50. Nhưng ta không chỉ cho mạng biết bước nào tương ứng với chữ A đầu, A thứ hai, rồi hai số 1.
CTC, viết tắt của Connectionist Temporal Classification, xử lý việc thiếu căn chỉnh chính xác này. Thay vì áp đặt một ký tự mong đợi tại mỗi bước, nó cộng xác suất của các đường đi tương thích với toàn bộ bản phiên âm. Một đường đi là dãy ký hiệu, một ký hiệu mỗi bước, trước khi được rút gọn thành văn bản.
Nguyên lý này được trình bày trong bài báo gốc của Graves, Fernández, Gomez và Schmidhuber về CTC (2006). Nó cho phép học từ chuỗi mong đợi mà không đòi hỏi phân đoạn từng ký tự. Tuy nhiên trong dự án này, định vị dòng vẫn là một bước riêng.
Blank: dấu tách kỹ thuật, không phải khoảng trắng
Hãy lấy đường đi của ví dụ có sẵn: A, A, blank, A, 1, 1, blank, 1.
Để biến nó thành văn bản, phải gộp các lần lặp liên tiếp rồi loại blank, theo đúng thứ tự này:
Chemin initial : A, A, blank, A, 1, 1, blank, 1
Après fusion : A, blank, A, 1, blank, 1
Après retrait : A, A, 1, 1
Texte obtenu : AA11
Hai chữ A đầu đứng cạnh nhau nên thành một A. Chữ tiếp theo được ngăn bởi blank nên vẫn là ký tự khác. Cơ chế tương tự áp dụng cho hai số 1 mà ta muốn giữ.
Bây giờ hãy thử thứ tự ngược, cố ý sai:
Chemin initial : A, A, blank, A, 1, 1, blank, 1
Retrait prématuré : A, A, A, 1, 1, 1
Fusion ensuite : A, 1
Texte obtenu : A1
Ta đã mất các lần lặp là một phần của mã định danh. Đây không phải chi tiết thẩm mỹ: AA11 và A1 không cùng một mã.
Blank không phải ký tự khoảng trắng. Trong BOLT AA11 12.50, các khoảng trắng nhìn thấy thuộc bảng chữ cái và phải được nhận dạng. Blank phục vụ cơ chế căn chỉnh; nó không được xuất hiện trong bản phiên âm cuối.
Blank giữ sự tách biệt giữa hai ký tự giống nhau cho đến hết quá trình giải mã.
Bộ giải mã ngắn, nhưng thứ tự thao tác then chốt
Bộ giải mã xử lý mã số của các ký hiệu. Trong src/pdfctc/decode.py, collapse thực hiện rút gọn đúng, còn wrong_collapse giúp kiểm thử lỗi vừa nêu.
Đây là hàm trung tâm. Mã của blank là 0:
def collapse(path, blank=0):
"""Merge runs first; a blank separates identical visible symbols."""
result = []
previous = None
for symbol in path:
symbol = int(symbol)
if symbol != previous and symbol != blank:
result.append(symbol)
previous = symbol
return result
previous ghi nhớ ký hiệu trước đó. Ta chỉ thêm ký hiệu hiện tại nếu nó khác ký hiệu trước và không phải blank.
Điểm quyết định nằm ở cuối vòng lặp: previous được cập nhật cả khi gặp blank. Vì vậy một A sau blank được so với blank, không phải A nhìn thấy cuối cùng. Nó có thể được giữ lại.
Như vậy mã thực hiện trong một lượt hai thao tác mà ta đã tách ra để giải thích. Cũng đừng nhầm mã kỹ thuật 0 với chữ số in 0: lớp blank và lớp ký tự nhìn thấy có vai trò khác nhau.
Hãy kiểm tra cơ chế này bằng lệnh của dự án:
docker compose run --rm tutorial python -m pdfctc.cli explain-ctc --text AA11
Hãy quan sát các đường đi và văn bản nhận được. Các ví dụ giải mã trên giải thích cơ chế này; chúng không phải dự đoán từ các bản quét.
Vì sao bốn ký tự có thể cần sáu bước
Để tạo AA11, cần ít nhất một bước cho mỗi ký tự, cộng một bước tách giữa hai A và giữa hai 1. Vì thế một đường đi tối thiểu là A, blank, A, 1, blank, 1: sáu bước cho bốn ký tự.
Tổng quát hơn, mức tối thiểu là số ký tự cộng số cặp ký tự giống nhau liền kề. Đây là loại ràng buộc minimum_steps kiểm tra trước khi tính loss. Nếu chuỗi thị giác quá ngắn, bài toán không thể thực hiện trong khuôn khổ này, ngay cả với tham số tốt.
Hãy nhớ hai độ dài: độ dài đầu ra theo thời gian của mạng và độ dài bản phiên âm mong đợi. Chúng không có cùng giá trị cũng không có cùng chức năng.
Loss CTC: tín hiệu giúp học
Một hàm mất mát, hay loss, đo chênh lệch giữa thứ mô hình tạo ra và thứ ta mong đợi. Nó cung cấp tín hiệu dùng để điều chỉnh tham số. Ở đây CTC làm việc trên xác suất các đường đi tương thích, chứ không phải một khung mong đợi cho từng chữ.
Lời gọi tuân theo hợp đồng của torch.nn.CTCLoss: log-xác suất, mục tiêu, độ dài đầu vào và độ dài mục tiêu. Tài liệu tương ứng PyTorch 2.8 cho phép xem hợp đồng này cho phiên bản của lộ trình.
Hàm dự án trước hết kiểm tra dữ liệu có cho phép phép tính này hay không:
def ctc_loss(logits, lengths, targets, target_lengths):
offset = 0
for time, size in zip(lengths.tolist(), target_lengths.tolist()):
target = targets[offset : offset + size].tolist()
offset += size
if not target or 0 in target or time < minimum_steps(target):
raise ValueError(
f"CTC impossible: T={time}, L={size}, minimum={minimum_steps(target)}"
)
if offset != len(targets):
raise ValueError("Target lengths do not sum correctly")
# Transfer is differentiable: all network parameters and their gradients remain on CUDA.
log_probs = logits.log_softmax(-1).cpu()
loss = torch.nn.functional.ctc_loss(
log_probs,
targets.cpu(),
lengths.cpu(),
target_lengths.cpu(),
blank=0,
reduction="mean",
zero_infinity=False,
)
if not torch.isfinite(loss):
raise ValueError("Non-finite CTC loss")
return loss
targets chứa các bản phiên âm đã mã hóa thành mã số và nối lại. target_lengths cho biết có bao nhiêu mã thuộc mỗi bản phiên âm. Vì thế offset di chuyển trong mảng lớn này để tìm mục tiêu của từng dòng.
Kiểm tra đầu từ chối mục tiêu rỗng, blank trong mục tiêu hoặc chuỗi quá ngắn. Kiểm tra thứ hai xác minh các độ dài khai báo thực sự khớp tổng số mã. Các từ chối này là hàng rào của dự án: chúng làm lỗi hiện rõ trước khi học.
log_softmax(-1) biến logit thành log-xác suất, trên chiều ký hiệu. Đây là biểu diễn CTC cần; tài liệu log_softmax giải thích phép tính kết hợp và tính ổn định số.
reduction="mean" tạo loss tổng hợp: PyTorch chia loss của các ví dụ cho độ dài mục tiêu, rồi lấy trung bình trên lô. zero_infinity=False tránh tự động thay loss vô hạn bằng không, và torch.isfinite(loss) từ chối loss vô hạn hoặc không phải số. Che giấu điều không thể không giải quyết được nó.
Điểm đặc biệt cuối: loss CTC được tính trên CPU trong dự án này, trong khi tham số mạng và gradient của chúng vẫn ở CUDA. Việc chuyển vẫn khả vi: nó không cắt theo dõi thao tác giúp tín hiệu học lan ngược. Một gradient chỉ cách một biến thiên nhỏ của tham số ảnh hưởng loss; tài liệu Autograd trình bày theo dõi tự động này.
Đây là lựa chọn của mã được minh họa, không phải khuyến nghị hiệu năng chung. Loss CTC dùng để huấn luyện mạng; tỷ lệ lỗi ký tự sẽ dùng sau đó để đánh giá văn bản thực sự tạo ra. Đừng nhầm hai thước đo này.
Huấn luyện và kiểm tra việc tiếp tục
So sánh hai cách học
Một epoch là một lượt đi qua các ví dụ huấn luyện. Qua các lượt này, mạng tạo điểm, loss CTC được tính, rồi optimizer điều chỉnh tham số dựa trên gradient.
Dự án so sánh một mô hình được huấn luyện có suy giảm với mô hình chỉ huấn luyện trên ảnh sạch, cùng ngân sách và cùng khởi tạo. Việc thêm suy giảm là một dạng tăng cường dữ liệu: ta cho mạng thấy các biến thiên thị giác để cố làm nó vững hơn.
Ý tưởng nghe hợp lý, nhưng không bảo đảm. Cần kiểm tra liệu mô hình tăng cường thực sự tốt hơn trên chính các ảnh kiểm thử đó không.
Ngắt, tiếp tục rồi so sánh
Ta cũng sẽ kiểm tra việc tiếp tục huấn luyện. Hãy chạy huấn luyện bị ngắt, biến thể sạch, tham chiếu liên tục rồi tiếp tục:
docker compose run --rm tutorial python -m pdfctc.cli train --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfctc.cli train-clean --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfctc.cli train-continuous --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfctc.cli train-resume --config configs/minimum.yaml
train dừng sau 7 epoch. Tham chiếu train-continuous chạy 15 epoch không bị ngắt. train-resume nạp lại trạng thái bị ngắt rồi thực hiện 8 epoch còn lại. Lệnh train-clean cung cấp mô hình so sánh được huấn luyện trên ảnh sạch.
Vì vậy các lệnh này không phải đều kéo dài cùng một mô hình trong một chuỗi dài: chúng tạo các biến thể cần thiết cho so sánh.
Một checkpoint không chỉ là trọng số mạng. Để tiếp tục cùng thí nghiệm, cần lấy lại cả trạng thái optimizer và các trạng thái khác hữu ích cho việc tiếp tục. Optimizer là cơ chế cập nhật tham số; bộ nhớ của nó có thể ảnh hưởng các bước kế tiếp.
Bây giờ hãy kiểm tra việc ngắt không làm thay đổi kết quả:
docker compose run --rm tutorial python -m pdfctc.cli verify-resume --config configs/minimum.yaml
Lộ trình đã xác thực trả về một lần tiếp tục chính xác. So sánh bao gồm tham số, optimizer, trạng thái ngẫu nhiên, lịch sử và dữ liệu. Vì thế ta không chỉ thấy hai đường cong có vẻ giống nhau.
Tính chính xác này áp dụng cho môi trường đã xác thực. Đổi phụ thuộc hoặc phần cứng cần kiểm tra lại hành vi; kết quả không hứa hẹn giống hệt tự động trên mọi máy.
Phiên âm mà không đọc đáp án mong đợi
Suy luận là lúc ta dùng các tham số đã học để tạo bản phiên âm. Không còn điều chỉnh mạng dựa vào đáp án đã biết.
Dịch vụ suy luận gắn pixel và checkpoint ở chế độ chỉ đọc. Annotation không có trong dịch vụ này: nó phải tìm dòng, tính điểm thời gian và giải mã ký tự từ ảnh.
docker compose run --rm inference python -m pdfctc.cli predict --config configs/minimum.yaml
Các dự đoán được ghi trong outputs/. Hãy xem vài dòng trước khi nhìn các trung bình toàn cục. Một chuỗi có thể trông hợp lý nhưng vẫn sai mã định danh hay số tiền.
Đánh giá sau đó lấy lại annotation để so sánh văn bản. Nó ghép dự đoán với tham chiếu từ các khung, chứ không chọn sau đó văn bản cho điểm tốt nhất. Chương trình phải so sánh đúng dòng với đúng tham chiếu, ngay cả khi nhận dạng sai.
Đọc chỉ số trước khi đọc giá trị
Đếm lỗi ký tự
CER, viết tắt của Character Error Rate, là tỷ lệ lỗi ký tự. Nó bằng (S + I + D) / N.
S đếm thay thế, khi một ký tự thay ký tự khác. I đếm chèn, khi một ký tự xuất hiện thừa. D đếm xóa, khi thiếu một ký tự mong đợi. N là số ký tự trong tham chiếu.
Ví dụ, một thay thế và một xóa trên mười ký tự mong đợi cho (1 + 0 + 1) / 10 = 0,2. Đây là ví dụ tính toán, không phải phép đo bổ sung của mô hình.
CER 0,34 không có nghĩa 34 % số dòng sai. Nó báo cáo lỗi ký tự so với số ký tự mong đợi. Nó thậm chí có thể vượt 1 nếu lỗi, nhất là chèn, đủ nhiều. Vì vậy đây không phải tỷ lệ đơn giản của dòng thành công hay thất bại.
Cũng kiểm tra thứ ứng dụng thực sự cần dùng
Độ chính xác dòng yêu cầu cả dòng đúng. Chỉ một chữ sai cũng đủ làm kiểm tra thất bại, dù phần còn lại đọc được.
Độ chính xác mã định danh kiểm tra riêng trường thứ hai. Trong ví dụ BOLT AA11 12.50, trường này là AA11. Đây là thước đo hữu ích để không quên điều quan trọng khi nhập: phần lớn văn bản đúng không bù được mã sản phẩm sai.
Cuối cùng, độ bao phủ phân đoạn là số dòng được ghép theo hình học chia cho số dòng tham chiếu. Độ bao phủ 1 nghĩa là mọi dòng đã được ghép. Nó không nói văn bản đúng, cũng không nói mỗi ảnh cắt giống hệt annotation.
Phân biệt chuỗi hoàn chỉnh với chẩn đoán « gold crop »
Các phép đo thực hiện trên 900 dòng và 19 358 ký tự kiểm thử.
Phép đo đầu-cuối dùng toàn bộ chuỗi, gồm cả phân đoạn theo phép chiếu tìm dòng từ hàng pixel tối. Đây là cách gần nhất với đường đi thực của tài liệu trong dự án.
Chẩn đoán gold crop đưa trực tiếp ảnh cắt tham chiếu cho bộ nhận dạng. Nhờ đó ta loại lựa chọn khung do phân đoạn thực hiện để xem việc đọc trên vùng đã chú thích.
Từ gold chỉ tham chiếu, không phải lời hứa điểm tốt hơn. Chẩn đoán này không thay thế phép đo chuỗi hoàn chỉnh và không đại diện cho dự đoán tự chủ từ cả trang.
Hãy tính các phép đo trên dự đoán:
docker compose run --rm tutorial python -m pdfctc.cli evaluate --config configs/minimum.yaml
Những gì kết quả của lộ trình r001 cho thấy
Mô hình ban đầu, trước huấn luyện, đạt CER 0,9950 trên cả dạng sạch lẫn suy giảm. Các kết quả sau cho phép so sánh ảnh hưởng của huấn luyện và ảnh dùng đánh giá. CER thấp hơn là tốt hơn.
| Mô hình | Dạng được đánh giá | CER |
|---|---|---|
| Ban đầu, chưa huấn luyện | Sạch và suy giảm | 0,9950 |
| Huấn luyện trên ảnh sạch, chuỗi hoàn chỉnh | Sạch | 0,3387 |
| Huấn luyện trên ảnh sạch, chuỗi hoàn chỉnh | Suy giảm | 0,3694 |
| Huấn luyện có suy giảm, chuỗi hoàn chỉnh | Sạch | 0,4103 |
| Huấn luyện có suy giảm, chuỗi hoàn chỉnh | Suy giảm | 0,4131 |
Kết quả tốt nhất vì vậy là mô hình huấn luyện trên bản quét sạch, được đánh giá đầu-cuối trên dạng sạch: CER 0,3387, với 900 dòng dự đoán cho 900 tham chiếu và độ bao phủ 1,00.
Nhưng nó cũng đạt 0 dòng chính xác và 0 mã định danh chính xác trên 900. Chuỗi tìm được các dòng, và mô hình đã tiến bộ so với khởi tạo, nhưng kết quả chưa là bộ trích xuất dữ liệu nghiệp vụ dùng được.
Trên dạng suy giảm, chính mô hình này đạt 0,3694 trong chuỗi hoàn chỉnh và 0,3803 với gold crop. Ở đây chẩn đoán trên ảnh cắt tham chiếu không cho CER thấp hơn. Chỉ hai trung bình này chưa đủ để xác định nguyên nhân chênh lệch: cần xem ví dụ trước khi đưa ra giải thích.
Cuối cùng, mô hình huấn luyện có suy giảm vẫn ở 0,4103 trên dạng sạch, rồi 0,4131 đầu-cuối trên dạng suy giảm. Tăng cường được chọn không thắng huấn luyện sạch trên corpus này với ngân sách này. Đây là kết quả âm cần giữ lại, không phải bất thường để xóa khỏi câu chuyện.
Điều này không chứng minh mọi tăng cường dữ liệu đều vô ích. Nó cho thấy tăng cường này trong thí nghiệm này không đem lại mức tăng kỳ vọng. Đổi phép biến đổi hoặc ngân sách sẽ cần phép đo mới.
Những điều chúng ta đã học
OCR tập hợp nhiều vấn đề: định vị văn bản, biểu diễn pixel, đặt chuỗi vào ngữ cảnh rồi giải mã. Câu trả lời tốt cho một vấn đề không tự động giải quyết các vấn đề sau.
CNN học các mẫu cục bộ; GRU liên kết vị trí ngang; CTC cho phép học từ bản phiên âm mà không áp đặt khung cho từng ký tự. Blank của nó quản lý căn chỉnh và lặp lại: nó không biểu diễn khoảng trắng.
Các phép đo cho ta hai bài học cụ thể. Tìm được mọi dòng không có nghĩa biết đọc chúng. Và thêm suy giảm không bảo đảm mô hình vững hơn. Câu hỏi hữu ích vẫn là: lỗi nào còn lại, trên tài liệu nào và hậu quả ra sao?
Bài tập: dự đoán trước khi đo
Viết giả thuyết trước khi chạy lệnh
Ta sẽ thay đổi một biến duy nhất: cường độ làm mờ, không huấn luyện lại mạng.
Trước khi chạy thí nghiệm, hãy ghi điều bạn mong đợi. CER sẽ tăng không? Phân đoạn có nhất thiết mất dòng không? Mô hình huấn luyện có suy giảm có nhất thiết chịu tốt hơn mô hình ảnh sạch không? Hãy phân biệt giả thuyết hợp lý với kết quả đã biết.
Để kiểm tra hiểu biết của bạn về blank, cũng hãy tự tính giải mã đường đi A, blank, A, A, 1, blank, 1, 1. Hãy thử đúng thứ tự thao tác rồi thứ tự đảo. Viết hai văn bản nhận được trước khi đọc lời giải.
Chạy biến thể được cung cấp
configs/exercise.yaml đặt độ mờ 1,4 pixel và giữ các checkpoint. Vì vậy ta dùng cùng mô hình để quan sát ảnh khó hơn, không dạy chúng thích nghi.
docker compose run --rm tutorial python -m pdfctc.cli exercise --config configs/exercise.yaml
Mở kết quả bài tập và so sánh với dự đoán. Với đường đi đề xuất, giải mã đúng cho AA11; loại blank trước dẫn đến A1. Các tệp tạo ra và lesson/SOLUTIONS.md cho phép học lời giải của lộ trình.
Biến thể rất mờ, không huấn luyện lại, xác nhận chênh lệch. Mô hình huấn luyện trên ảnh sạch đạt CER 0,5479 đầu-cuối và 0,5660 với gold crop. Mô hình tăng cường lần lượt đạt 0,5617 và 0,5918.
Độ bao phủ vẫn là 1,00, trong khi độ chính xác dòng và mã định danh vẫn bằng không. Chương trình tiếp tục tìm được dòng, nhưng nội dung của chúng khó phiên âm hơn. Vì vậy phân đoạn và nhận dạng không nhất thiết suy giảm theo cùng cách.
Mở rộng thí nghiệm mà không đổi mọi thứ cùng lúc
Sau đó, bạn có thể chỉ thay đổi cường độ một suy giảm của bộ sinh. Giữ mô hình, trang và annotation, rồi dự đoán và đo lại.
Sửa đổi cá nhân này khác với biến thể có sẵn: kết quả của nó chưa biết trước. Đừng đồng thời thay dữ liệu và huấn luyện, nếu không bạn sẽ không biết quy chênh lệch cho điều gì.
Tiến triển không nhất thiết đơn điệu với từng trang: tăng suy giảm không bảo đảm mỗi bản phiên âm riêng lẻ sẽ xấu đi ở mọi lần thử. Cũng hãy xem các ví dụ trái trực giác và giải thích chúng thay vì gạt bỏ.
Tạo hình và kiểm tra tệp
Tạo các hình từ kết quả của bạn, rồi kiểm tra checkpoint:
docker compose run --rm tutorial python lesson/figures/plot.py
docker compose run --rm tutorial python scripts/check_checkpoints.py
Sáu checkpoint của lộ trình tham chiếu đều qua kiểm tra cấu trúc và danh tính. Kiểm tra này áp dụng cho các tệp mong đợi; nó không biến mô hình kém chính xác thành đáng tin cậy.
Sau đó hãy xem đường cong và ví dụ dòng trong outputs/. CER tổng hợp cho biết độ lớn toàn cục của lỗi, nhưng ví dụ cho phép thấy chuyện gì xảy ra: ký tự bị nhầm, thêm hay mất? Hãy so bản phiên âm với ảnh thay vì chỉ tin vẻ hợp lý của nó.
Kiểm thử lỗi và khắc phục sự cố
Một trường hợp âm kiểm tra chương trình từ chối tình huống không nhất quán. Vì vậy qua kiểm thử này nghĩa là nhận được từ chối mong đợi, chứ không phải vẫn cố tạo đầu ra.
Kiểm thử bao gồm mục tiêu CTC rỗng, blank bị cấm trong bản phiên âm mục tiêu, không đủ bước thời gian và dòng quá rộng. Đừng né các lỗi này bằng cách âm thầm cắt văn bản hay che giấu một loss không thể có: bạn sẽ thay đổi bài toán thay vì giải nó.
Trên di động, hãy cuộn bảng theo chiều ngang.
| Triệu chứng | Kiểm tra và khắc phục |
|---|---|
| GPU xuất hiện trên máy chủ nhưng CUDA lỗi trong Docker | So sánh preflight và check_environment.py, rồi sửa tích hợp NVIDIA trước khi huấn luyện. |
| Một dòng vượt chiều rộng dự kiến | Kiểm tra khung và việc đổi kích thước. Corpus cá nhân có thể cần phân đoạn khác, phải đánh giá như một sửa đổi dự án. |
| CTC từ chối mục tiêu | Kiểm tra độ dài, lần lặp và không có blank trong ký tự mong đợi. |
| Thiếu checkpoint | Tìm bước huấn luyện tương ứng và lỗi của nó; tệp vắng mặt không được thay bằng trọng số tùy tiện. |
| Đĩa đầy | Kiểm tra chỗ cho bản quét, checkpoint và kết quả. Giữ các tệp hữu ích trước khi dọn dẹp có mục tiêu bên dưới. |
Dừng và dọn dẹp
Trước tiên, sao chép các kết quả và hình bạn muốn giữ. Lệnh đầu tiên dừng dự án. Script hiển thị rồi chỉ xóa outputs/. Lệnh dừng cuối kết thúc lộ trình sau khi chạy dọn dẹp.
Nội dung của outputs/ sẽ bị xóa. Các bản quét vẫn ở data/generated/ và checkpoint ở assets/checkpoints/.
docker compose down
docker compose run --rm tutorial python scripts/clean.py --outputs-only
docker compose down
Không có volume Docker toàn cục nào bị xóa. Để chạy lại lộ trình, hãy dùng cùng hướng dẫn và các phụ thuộc được khóa trong ZIP. Dọn dẹp có mục tiêu giúp biết rõ thứ bị xóa mà không chạm vào dự án khác.
Từ hướng dẫn đến ứng dụng thực
Công việc đầu tiên không phải thêm một route HTTP, mà là có tài liệu đại diện và chất lượng được đo trên các tài liệu đó.
Hãy thay corpus hư cấu bằng tài liệu được phép, kèm annotation. Chia dữ liệu để tránh rò rỉ giữa huấn luyện và kiểm thử theo nhà cung cấp, mẫu biểu hoặc lô. Đo CER và các tiêu chí thực sự hữu ích cho ứng dụng của bạn trên các nguồn riêng biệt. Ngôn ngữ, xoay, bảng, trang phức tạp và chữ viết tay cần dữ liệu và đánh giá bổ sung.
Tích hợp web đi kèm các trách nhiệm khác: xác thực, quyền tài liệu, giới hạn tệp, xử lý bất đồng bộ, giám sát và kiểm thử tải, tùy ngữ cảnh. Ví dụ API bất đồng bộ có thể tách việc nộp tài liệu khỏi lấy kết quả; đó không phải tính năng hướng dẫn này cung cấp.
Hãy bảo vệ dữ liệu, quản lý phiên bản mô hình và bố trí kiểm soát con người cho mã định danh và số tiền quan trọng. Cô lập parser PDF, tức thư viện mở và diễn giải tệp, cũng như bảo trì thư viện native đều là một phần vận hành.
Mạng trong hướng dẫn này không có chất lượng đủ để tự động hóa trích xuất nghiệp vụ nếu không kiểm soát. Giá trị của nó là làm cơ chế và giới hạn trở nên quan sát được, để biết cần cải thiện và kiểm tra gì tiếp theo.
Đi xa hơn
Hãy bắt đầu bằng xây dựng corpus có annotation đại diện cho các bản quét của riêng bạn. Nghiên cứu riêng lỗi phân đoạn và lỗi nhận dạng: ảnh dòng cắt sai và chữ nhận dạng sai không cần cùng một cách điều tra.
Sau đó bạn có thể so sánh ngân sách huấn luyện khác trong khi giữ tập kiểm thử cố định, hoặc thử một kiểu giải mã khám phá nhiều đường đi. Trong cả hai trường hợp, hãy đo đóng góp thật thay vì suy ra chỉ từ lựa chọn kỹ thuật. Đây là các hướng mở rộng cần thử, không phải cải tiến đã được xác thực trong lộ trình r001.
Các liên kết tài liệu trong bài đào sâu cơ chế: thao tác ảnh trong Pillow, các lớp và độ dài chuỗi trong PyTorch, rồi nguyên lý CTC trong bài báo gốc. Các tham chiếu PyTorch 2.8 khớp phiên bản dùng ở đây; tài liệu khác có thể thay đổi. Để tái tạo thí nghiệm, cấu hình và phụ thuộc khóa trong ZIP vẫn là điểm xuất phát của bạn.