Bạn đang phát triển một màn hình quản lý cần trích xuất một khoản tiền từ PDF. Trợ lý trả lời 120.00. Rất tốt… nhưng con số này đến từ đâu? Từ chi phí ước tính, chi phí đã được phê duyệt hay một dòng khác trong tài liệu? Trước khi điền biểu mẫu, bạn muốn có thể nhấp vào câu trả lời và thấy vùng chứng minh cho nó.
Đó là điểm khởi đầu của hướng dẫn này. Chúng ta sẽ theo dõi biểu mẫu hư cấu Orion Service Record, gồm một ngày tháng, hai số sê-ri và một chi phí đã được phê duyệt. Chương trình của chúng ta phải trả lời một câu hỏi về trang, chỉ ra vị trí của giá trị và không trả lời khi trường được hỏi không tồn tại.
Chúng ta sẽ điều chỉnh một mô hình có thể xử lý hình ảnh và văn bản bằng LoRA, rồi kiểm soát đầu ra của nó bằng Python. Bạn không cần biết kỹ thuật này từ trước: chúng ta sẽ xem nó thay đổi gì, học như thế nào và quan trọng nhất là cách kiểm tra kết quả.
Lộ trình là hình ảnh trang → câu hỏi → mô hình đã điều chỉnh → câu trả lời và hình chữ nhật → xác thực → đánh giá. Một câu trả lời và một khung đẹp là chưa đủ: chúng ta sẽ đo riêng văn bản, vị trí và việc từ chối trả lời.
Vấn đề cụ thể
Đọc một trang và trả lời một câu hỏi là hai nhiệm vụ khác nhau. Một hệ thống có thể nhận ra nhiều khoản tiền mà không biết khoản nào tương ứng với « approved service cost ». Nó cũng có thể đưa ra đúng số tiền nhưng lại chỉ vào lần xuất hiện sai.
Ở đây, chúng ta làm VQA, viết tắt của Visual Question Answering: trả lời một câu hỏi từ một hình ảnh. PDF được kết xuất thành hình ảnh của trang, sau đó hình ảnh này đi kèm câu hỏi. Kết quả mong đợi không phải là bản tóm tắt tự do của tài liệu mà là dữ liệu có cấu trúc để chương trình có thể chấp nhận hoặc từ chối.
Với một lập trình viên web, phép so sánh rất đơn giản: như với API, chúng ta xác định một hợp đồng đầu ra. Nhưng JSON hợp lệ không bảo đảm dữ liệu là đúng. Chúng ta còn phải so sánh dự đoán với các chú thích tham chiếu, rồi xem vùng được chỉ ra trên trang.
Bộ dữ liệu là tổng hợp: các biểu mẫu được tạo ra cho một thí nghiệm có kiểm soát. Điều này cho phép biết các câu trả lời mong đợi và gây ra một số lỗi nhất định. Điều đó không tự động xác nhận việc xử lý PDF của khách hàng bạn.
Khái niệm trước khi viết mã
Một câu trả lời và một bằng chứng tạo thành một hợp đồng duy nhất
Câu hỏi xuyên suốt của chúng ta yêu cầu chi phí dịch vụ đã được phê duyệt. Mô hình không chỉ phải tạo ra 120.00, mà còn một hộp, tức hình chữ nhật của vùng chứa giá trị được yêu cầu.
Đây là minh họa về định dạng mong đợi. Tọa độ trong ví dụ này được chọn để giải thích; đây không phải dự đoán được đo trên bộ dữ liệu.
{
"answer": "120.00",
"box": [600, 700, 800, 750]
}
answer là một chuỗi ký tự, kể cả đối với khoản tiền. box gồm bốn tọa độ theo thứ tự trái, trên, phải, dưới. Chúng được biểu diễn trên thang từ 0 đến 1000, không trực tiếp bằng pixel.
Với một trường không có mặt, dạng thứ hai được phép là:
{
"answer": null,
"box": null
}
Đó là một sự từ chối trả lời: hệ thống không đề xuất giá trị hay vị trí nào. Việc từ chối phải đầy đủ. Giá trị không có hộp hoặc hộp không có giá trị đều bị từ chối. Tương tự, chuỗi "null" không phải giá trị JSON null: đó là văn bản, không phải sự vắng mặt.
Tại sao giữ hai thành phần này cùng nhau? Vì chúng ta muốn tránh hai lối tắt. Một số tiền chính xác gắn với hình chữ nhật sai không cho phép kiểm tra lần xuất hiện đúng. Ngược lại, một hình chữ nhật đặt đúng chỗ không khiến số tiền sai trở nên chấp nhận được.
Trong bài này, « bằng chứng được định vị » là vùng được đề xuất làm căn cứ. Cụm từ này không có nghĩa chương trình đã chứng minh sự thật của câu trả lời. Mô hình cũng dự đoán tọa độ: chúng có thể sai. Hợp đồng khiến đề xuất này có thể kiểm soát; việc đánh giá sẽ cho biết nó có đúng hay không.
LoRA thực sự điều chỉnh gì
Mô hình nền là Qwen2-VL-2B-Instruct. Đây là mô hình thị giác-ngôn ngữ, hay VLM: nó nhận thông tin hình ảnh và văn bản để tạo văn bản. Trang chính thức giới thiệu biến thể được dùng; chúng ta giữ biến thể của dự án, không thay bằng mô hình mới hơn.
Bộ mã hóa thị giác của nó biến hình ảnh thành các biểu diễn số. Bộ giải mã văn bản dùng các biểu diễn này và câu hỏi để lần lượt tạo các thành phần của câu trả lời. Một token là đơn vị văn bản mà mô hình xử lý: một từ, một phần của từ hay một ký hiệu có thể chiếm các token khác nhau.
Tokenizer chuyển văn bản thành các định danh số. Bộ xử lý cũng chuẩn bị hình ảnh cho mô hình. Tài liệu Qwen2-VL của Transformers minh họa bước chuẩn bị đa phương thức này. Bạn không phải viết bộ mã hóa thị giác trong hướng dẫn này: chúng ta dùng các khả năng của mô hình được huấn luyện trước.
Trọng số là các tham số số mà mạng đã học. Huấn luyện điều chỉnh chúng từ các ví dụ và câu trả lời mong đợi. Điều chỉnh một mô hình đã được huấn luyện cho một nhiệm vụ cụ thể được gọi là fine-tuning, hay tinh chỉnh chuyên biệt.
LoRA, viết tắt của Low-Rank Adaptation, là cách thực hiện sự điều chỉnh này mà không sửa mọi trọng số. Trong dự án của chúng ta, các trọng số nền vẫn đóng băng, nên không thể huấn luyện. Ta thêm các ma trận nhỏ học một hiệu chỉnh trong một số lớp. Tập tham số bổ sung này tạo thành adapter.
Hãy hình dung một thành phần sẵn có mà ta giữ nguyên xử lý chính rồi thêm một hiệu chỉnh chuyên biệt. Phép so sánh dừng ở đó: hiệu chỉnh này không phải danh sách điều kiện viết tay, mà là các con số được học. Hướng dẫn khái niệm LoRA của PEFT mô tả sự tách biệt giữa trọng số đóng băng và ma trận điều chỉnh.
Tại sao chọn cách này? Chúng ta đã có một mô hình thị giác-ngôn ngữ và muốn nghiên cứu sự điều chỉnh hướng tới hợp đồng Orion. LoRA giảm số tham số cần huấn luyện và cho phép nhận diện phần điều chỉnh đã lưu. Lộ trình so sánh mô hình đóng băng với phiên bản đã điều chỉnh; nó không so sánh LoRA với fine-tuning toàn bộ. Vì vậy, chúng ta sẽ không kết luận LoRA luôn là phương pháp tốt nhất.
Bộ dữ liệu Orion gồm 400 trang. Tập kiểm thử có 240 câu hỏi riêng biệt: 180 câu có thể trả lời và 60 câu về các trường không có mặt. Trang và câu hỏi không phải cùng một đơn vị: các con số này không mô tả 240 PDF bổ sung.
Bước chuẩn bị kết xuất các trang, xây dựng các cặp câu hỏi/câu trả lời và đặt các hình chữ nhật mong đợi. Các câu trả lời tham chiếu này, đôi khi gọi là gold, dùng để học và đánh giá. Suy luận, tức tạo dự đoán, không nạp các chú thích. Chương trình phải làm việc với đầu vào đã định, không được đọc lại lời giải.
Lệnh inspect-batch sẽ cho phép xem xét một lô: hình ảnh, câu hỏi, mục tiêu JSON và hộp. Đây là thời điểm thích hợp để hiểu các ví dụ mà chúng ta yêu cầu mô hình học, trước khi khởi chạy huấn luyện.
Ví dụ về bằng chứng được định vị
Quay lại hộp minh họa [600, 700, 800, 750]. Trên hình ảnh 1000 × 1000 pixel, nó tương ứng với hình chữ nhật từ 600 đến 800 pixel theo chiều ngang và từ 700 đến 750 pixel theo chiều dọc.
Trên cùng trang được kết xuất ở 2000 × 2000 pixel, hình chữ nhật này thành [1200, 1400, 1600, 1500] tính bằng pixel. Tuy nhiên, biểu diễn chuẩn hóa của nó không đổi. Để tìm hoành độ, ta tính x_pixels = x_normalisé × largeur / 1000; với tung độ, ta dùng chiều cao.
Thang này giống phần trăm, với 1000 là cận thay vì 100. Nó cho phép chỉ cùng một vùng tương đối ở các độ phân giải khác nhau, với điều kiện giữ cùng khung hình và hướng. Cắt xén làm thay đổi hệ quy chiếu; tăng độ phân giải cũng không tự động làm mô hình chính xác hơn.
Một trang có thể chứa « service cost » và « approved service cost ». Khung quanh trường đầu tiên không chứng minh câu trả lời về trường thứ hai, ngay cả khi các khoản tiền trùng nhau. Vì vậy cần đo độ chồng lấp với hộp mong đợi.
Phép đo được dùng là IoU, viết tắt của Intersection over Union: diện tích chung của hai hình chữ nhật chia cho tổng diện tích được một trong hai hình phủ. Giá trị 1 nghĩa là chúng trùng khớp; 0 nghĩa là không có diện tích chung. Chúng ta sẽ tự tính phép này sau mã xác thực.
Lập trình LoRA và chọn các token cần học
Hai ma trận nhỏ thay vì một hiệu chỉnh lớn tự do
Một lớp tuyến tính biến một danh sách số thành danh sách số khác. Các hệ số của nó được xếp trong ma trận W. Ma trận là bảng hai chiều; tensor là thuật ngữ tổng quát hơn mà PyTorch dùng cho các bảng số.
LoRA giữ W và thêm hiệu chỉnh dạng (alpha/r) × B × A. Ma trận A biến đầu vào sang một chiều trung gian nhỏ r; sau đó B biến nó sang chiều đầu ra. Hiệu chỉnh được gọi là hạng thấp: cách xây dựng của nó giới hạn số hướng độc lập mà nó có thể biểu diễn.
Trong dự án, r = 8 và alpha = 16, nên hệ số nhân là 16/8 = 2. Hệ số này điều chỉnh biên độ của hiệu chỉnh; nó không phải tốc độ học của optimizer.
Các lớp mục tiêu là q_proj, k_proj, v_proj và o_proj trong attention của bộ giải mã văn bản. Để định vị chúng mà không đi vào toàn bộ Transformer: phép chiếu Q và K tham gia tính mức gần nhau giữa các vị trí, V cung cấp thông tin để kết hợp và O biến đổi kết quả kết hợp đó. Phép tính attention được PyTorch ghi lại minh họa sự kết hợp Q, K và V. Những tên này chỉ các lớp của mạng, không phải bốn công cụ đọc PDF. Bộ mã hóa thị giác vẫn đóng băng.
Lấy một ví dụ số độc lập với kích thước thực của Qwen2-VL. Với một phép chiếu 1024 đầu vào sang 1024 đầu ra, W có 1024 × 1024 = 1 048 576 hệ số. Với hạng 8, A có 8 × 1024 = 8192 hệ số và B cũng có bấy nhiêu: 16 384 tham số điều chỉnh, ít hơn 64 lần so với hệ số của W, chưa tính một bias có thể có.
Phép tính này áp dụng cho một phép chiếu minh họa, không phải tổng số tham số của mô hình. Ma trận lớn vẫn hiện diện và được dùng. Vì vậy giảm các tham số có thể huấn luyện không có nghĩa chia toàn bộ bộ nhớ cho 64 hay loại bỏ chi phí của mô hình nền.
Đọc lớp LoRALinear
Đây là lớp được dùng trong src/pdfvqa/lora.py. Các import và mã chèn nó vào mô hình được cung cấp trong dự án hoàn chỉnh.
class LoRALinear(nn.Module):
def __init__(self, base, rank=8, alpha=16, dropout=0.05):
super().__init__()
if not isinstance(base, nn.Linear) or rank <= 0 or not 0 <= dropout < 1:
raise ValueError("Invalid LoRA layer")
self.base = base.requires_grad_(False)
self.A = nn.Parameter(
torch.empty(
rank, base.in_features, device=base.weight.device, dtype=torch.float32
)
)
self.B = nn.Parameter(
torch.zeros(
base.out_features, rank, device=base.weight.device, dtype=torch.float32
)
)
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
self.scale = alpha / rank
self.dropout = nn.Dropout(dropout)
def forward(self, x):
original = self.base(x)
update = torch.nn.functional.linear(
torch.nn.functional.linear(self.dropout(x.float()), self.A), self.B
)
return (original.float() + self.scale * update).to(original.dtype)
Phần đầu giữ lại lớp gốc. requires_grad_(False) cho PyTorch biết không tính gradient cho các tham số của nó. Gradient giúp xác định cách điều chỉnh một tham số để giảm lỗi học. Trọng số đóng băng vẫn tham gia phép tính; đơn giản là chúng không được cập nhật bởi sự điều chỉnh.
Phần thứ hai tạo các tham số cần học. nn.Parameter đăng ký A và B làm tham số của module. Kích thước của chúng khớp với lộ trình « chiều đầu vào → hạng → chiều đầu ra ». Chúng được đặt trên cùng thiết bị với lớp gốc.
A nhận khởi tạo ngẫu nhiên, trong khi B bắt đầu bằng không. Ban đầu, đi qua B vì thế cho hiệu chỉnh bằng không: adapter chưa sửa đầu ra của lớp. Sau đó, huấn luyện sẽ học hiệu chỉnh này thay vì bắt đầu bằng việc làm nhiễu mô hình một cách tùy ý.
Phương thức forward() tính hai nhánh. self.base(x) tạo đầu ra gốc. Hai lời gọi linear() lần lượt áp dụng A rồi B. Biểu thức PyTorch tuân theo quy ước mô tả bởi torch.nn.functional.linear, trong đó trọng số được lưu theo thứ tự đầu ra, đầu vào. Cuối cùng ta nhân hiệu chỉnh với self.scale, rồi cộng nó vào đầu ra gốc.
Dropout chỉ xuất hiện trong nhánh điều chỉnh. Trong lúc huấn luyện, giá trị 0.05 cho mỗi phần tử xác suất 5 % bị đưa về không; các phần tử được giữ sẽ được module đổi tỷ lệ. Ở chế độ đánh giá, nó không còn sửa đầu vào. Đây là hành vi được ghi trong nn.Dropout, không phải việc xóa vĩnh viễn tham số.
Cuối cùng, x.float(), các ma trận ở torch.float32 và original.float() làm rõ phép tính hiệu chỉnh và tổng trong FP32, một định dạng 32 bit. .to(original.dtype) đưa kết quả trở lại kiểu của đầu ra ban đầu. Đừng nhầm cách xử lý định dạng số này với việc đóng băng trọng số: một bên liên quan đến biểu diễn số, bên kia là các tham số ta huấn luyện.
Học câu trả lời, không sao chép toàn bộ ngữ cảnh
Chúng ta đã xác định điều gì có thể thay đổi trong mô hình. Bây giờ cần xác định những vị trí văn bản nào dùng để tính lỗi.
Trong quá trình huấn luyện, một chuỗi chứa ngữ cảnh — gồm câu hỏi và các phần gắn với hình ảnh — rồi đến câu trả lời mong đợi của trợ lý. Hàm mất mát, hay loss, đo chênh lệch giữa dự đoán và các token cần học. Chúng ta muốn huấn luyện câu trả lời JSON, không yêu cầu mô hình sao chép câu hỏi hay các vị trí đệm.
Padding, hay phần đệm, thêm các vị trí để sắp xếp những chuỗi có độ dài khác nhau trong các bảng. Các vị trí này không phải nội dung cần dự đoán. Vì vậy mã dùng giá trị đặc biệt -100 trong nhãn huấn luyện, labels, cho các vị trí bị hàm mất mát bỏ qua.
def assistant_labels(input_ids, attention_mask, prompt_length, forbidden_ids):
if (
input_ids.ndim != 1
or attention_mask.shape != input_ids.shape
or not 0 < prompt_length < len(input_ids)
):
raise ValueError("Invalid completion boundary")
labels = input_ids.clone()
labels[:prompt_length] = -100
labels[~attention_mask.bool()] = -100
for identifier in forbidden_ids:
labels[input_ids == identifier] = -100
if (labels != -100).sum() == 0:
raise ValueError("No assistant target token")
if (labels[:prompt_length] != -100).any():
raise ValueError("Prompt included in loss")
return labels
Kiểm tra đầu tiên yêu cầu một chuỗi một chiều, một attention_mask cùng hình dạng với input_ids và ranh giới prompt_length nằm sau đầu nhưng trước cuối. Ranh giới này tách ngữ cảnh khỏi câu trả lời.
clone() sao chép các định danh để xây dựng labels mà không sửa đầu vào. Ba thao tác tiếp theo đánh dấu -100 cho tiền tố prompt, các vị trí có attention mask bằng không và các định danh có trong forbidden_ids. Nếu không còn mục tiêu nào, hàm từ chối ví dụ thay vì khởi chạy huấn luyện không có câu trả lời để học. Kiểm tra cuối xác nhận prompt đã thực sự bị loại.
Lấy một ví dụ sư phạm về các định danh, không liên quan đến từ vựng thật của tokenizer. Đầu vào là [101, 501, 502, 601, 602, 603, 0], tiền tố chiếm ba vị trí đầu, vị trí cuối là padding và 603 thuộc các định danh bị cấm. Labels trở thành [-100, -100, -100, 601, 602, -100, -100].
Chỉ các mục tiêu 601 và 602 trực tiếp đóng góp vào mất mát. Tuy vậy ngữ cảnh không bị loại khỏi đầu vào. Mô hình vẫn có thể dùng nó để tạo câu trả lời. Che một mục tiêu huấn luyện không đồng nghĩa xóa câu hỏi hay hình ảnh.
Vì vậy giá trị -100 không phải token mà mô hình phải tạo, cũng không phải từ đồng nghĩa của JSON null. Nó chỉ có nghĩa « bỏ qua vị trí này khi tính mất mát », theo cơ chế ignore_index được PyTorch ghi lại.
Sau đó hàm mất mát dịch các mục tiêu: dự đoán tại một vị trí học token tiếp theo. Việc dịch này không được thực hiện trong assistant_labels(). Hàm được trình bày xây dựng mask; phép tính mất mát sử dụng nó sau đó. Trong mục tiêu JSON, các giá trị và tọa độ được học qua biểu diễn bằng token của chúng.
Cần nhớ: LoRA chọn những tham số thay đổi; mask chọn những mục tiêu đóng góp vào việc học; validator sẽ kiểm tra dạng của đầu ra được tạo. Ba cơ chế này có các vai trò khác nhau.
Xác thực câu trả lời được tạo
Sau suy luận, chúng ta nhận văn bản thô. Nó có thể không đầy đủ, sai định dạng hoặc chứa các tọa độ không thể có. Chúng ta kiểm tra nó trước khi coi đó là câu trả lời chương trình có thể dùng.
Đoạn sau đến từ src/pdfvqa/validate_answer.py. Nó không tham chiếu câu trả lời mong đợi để sửa dự đoán.
"""Validate raw model output, without repairing it from the expected answer."""
import json
def validate_answer(raw, terminated=True):
if not terminated:
return {"valid": False, "error": "length", "value": None}
try:
value = json.loads(raw)
except (ValueError, TypeError):
return {"valid": False, "error": "json", "value": None}
if not isinstance(value, dict) or set(value) != {"answer", "box"}:
return {"valid": False, "error": "schema", "value": None}
answer, box = value["answer"], value["box"]
if answer is None and box is None:
return {"valid": True, "error": None, "value": value}
if answer is None or box is None:
return {"valid": False, "error": "asymmetric_null", "value": None}
if not isinstance(answer, str) or not answer.strip():
return {"valid": False, "error": "answer", "value": None}
if (
not isinstance(box, list)
or len(box) != 4
or any(type(x) is not int for x in box)
):
return {"valid": False, "error": "box_type", "value": None}
if not 0 <= box[0] < box[2] <= 1000 or not 0 <= box[1] < box[3] <= 1000:
return {"valid": False, "error": "box_bounds", "value": None}
return {"valid": True, "error": None, "value": value}
def iou(a, b):
intersection = max(0, min(a[2], b[2]) - max(a[0], b[0])) * max(
0, min(a[3], b[3]) - max(a[1], b[1])
)
union = (a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - intersection
return intersection / union if union > 0 else 0.0
Từ văn bản thô đến đối tượng được chấp nhận hoặc từ chối
terminated cho biết việc tạo có kết thúc đúng hay không. Khi nó là False, hàm trả về lỗi length, ngay cả khi đầu ra ban đầu có vẻ dùng được. Bên gọi phải truyền trạng thái này: validator không thể đoán chỉ từ chuỗi rằng mô hình đã dừng thế nào.
json.loads() sau đó giải mã văn bản. Thất bại trả về json. Sau giải mã, chương trình yêu cầu một từ điển chứa đúng answer và box: thiếu hoặc thừa một khóa sẽ trả về schema.
Tiếp theo là kiểm tra cặp giá trị. Hai giá trị None, xuất phát từ hai null JSON, tạo thành một sự từ chối trả lời hợp lệ. Chỉ một giá trị null tạo ra asymmetric_null: việc từ chối chưa hoàn chỉnh. Nếu không, answer phải là chuỗi không rỗng sau khi kiểm tra khoảng trắng.
Hộp phải là danh sách bốn số nguyên, rồi tuân thủ 0 ≤ trái < phải ≤ 1000 và 0 ≤ trên < dưới ≤ 1000. Bất đẳng thức nghiêm ngặt cấm chiều rộng hoặc chiều cao bằng không. Kiểm tra type(x) is not int đặc biệt từ chối một boolean thay cho tọa độ. Các lỗi phân biệt kiểu sai (box_type) với hình học ngoài hợp đồng (box_bounds).
Hàm dùng answer.strip() để phát hiện chuỗi rỗng, nhưng trả về đối tượng gốc: nó không luôn làm sạch câu trả lời. Nó cũng không chuyển đổi khoản tiền, không kiểm tra tiền tệ và không thay tọa độ ngoài trang bằng giá trị giới hạn của chúng.
Vì thế valid=True có nghĩa « phù hợp với hợp đồng được kiểm tra ở đây », không phải « đúng trong tài liệu ». Một khoản tiền sai kèm hình chữ nhật đúng dạng có thể được thông qua. Tương tự, một sự từ chối có thể hợp lệ về cấu trúc trong khi trang chứa câu trả lời. Đánh giá độ đúng sẽ phân biệt các trường hợp này.
Tính IoU với một dịch chuyển nhỏ
Hàm iou() trước hết tính chiều rộng và chiều cao của phần giao. Các max(0, ...) ngăn tạo diện tích âm khi các hình chữ nhật không chạm nhau. Hợp là tổng hai diện tích trừ phần giao để không đếm hai lần vùng chung.
Quay lại một ví dụ số minh họa, không phải phép đo Orion. Hộp tham chiếu là [600, 700, 800, 750]. Dự đoán được dịch 100 đơn vị sang phải: [700, 700, 900, 750].
Mỗi hình chữ nhật có diện tích 200 × 50 = 10 000. Phần giao của chúng giờ chỉ còn 100 × 50 = 5000. Phần hợp là 10 000 + 10 000 − 5000 = 15 000. Vì thế IoU là 5000 / 15 000 = 1/3, tức xấp xỉ 0,3333.
Văn bản có thể vẫn chính xác là 120.00 trong cả hai câu trả lời. IoU giảm không đo thay đổi của văn bản: nó đo sự suy giảm của định vị. Bài tập cuối sẽ chủ ý tạo ra sự tách biệt này trên các đầu ra của dự án.
Hàm iou() giả định các hộp có thể dùng cho phép tính này; nó không thay thế validate_answer(). Còn các tọa độ dự đoán không bao giờ được sửa sau khi tạo bằng gold. Nếu không, chúng ta sẽ đo một câu trả lời được trợ giúp bởi lời giải, chứ không phải dự đoán của mô hình.
Điều kiện tiên quyết và lệnh
Bạn cần biết đọc Python, sử dụng terminal và chạy Docker. Các đoạn trước giải thích cơ chế; ZIP cung cấp dự án hoàn chỉnh cần thiết để thực thi chúng.
Cấu hình tham chiếu dùng Linux, Docker Engine, Compose, trình điều khiển NVIDIA và NVIDIA Container Toolkit, trên RTX 3090 24 Gio tương thích BF16. Lộ trình khuyến nghị 48 Gio RAM và 55 Gio dung lượng đĩa. Đây là các khuyến nghị chuẩn bị; cấu hình CPU chưa được xác nhận.
VRAM là bộ nhớ của card đồ họa, khác với RAM của máy. BF16 là định dạng số 16 bit, thuộc các kiểu số của PyTorch. Việc nó có mặt trong cấu hình không có nghĩa mọi phép tính dùng định dạng này: ta vừa thấy hiệu chỉnh LoRA được chuẩn bị rõ ràng ở FP32.
Trình điều khiển cho phép tính toán GPU trên máy chủ. NVIDIA Container Toolkit cung cấp tích hợp với Docker. Thấy card bằng nvidia-smi xác nhận nó hiển thị trên Linux, chưa xác nhận mô hình hoạt động trong container. check_environment.py thực hiện kiểm tra thứ hai này. Tài liệu chính thức của NVIDIA trình bày cài đặt phù hợp với hệ thống; các kiểm tra của dự án không âm thầm thay đổi hệ thống đó.
Giải nén ZIP và đặt terminal vào thư mục gốc của nó. Mọi lệnh dưới đây đều bắt đầu từ thư mục gốc này và chạy theo thứ tự. Dịch vụ acquisition lấy các tài sản của mô hình, tutorial chuẩn bị dữ liệu, huấn luyện và đánh giá, còn inference tạo dự đoán mà không xem labels.
Bước 1 — chuẩn bị
Chúng ta kiểm tra môi trường, chuẩn bị dự án, xây dựng dịch vụ và lấy các tệp mong đợi trước khi chạy kiểm thử.
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, tức không có lỗi được báo. Các kiểm tra này có mục tiêu khác nhau: Compose kiểm tra cấu hình, build xây dựng image, bước thu thập bị khóa lấy các tài sản đã định, rồi kiểm tra môi trường chạy mô hình trong Docker. Cuối cùng các kiểm thử xác minh các hành vi của chương trình.
Build thành công không thay thế kiểm tra GPU hay kiểm thử. Và kiểm thử thành công không có nghĩa mọi câu trả lời của mô hình sẽ đúng: phép đo này đến sau.
Bước 2 — xây dựng dự đoán
Hãy chuẩn bị các trang và câu hỏi, xem một lô, rồi tạo tham chiếu đầu tiên không có điều chỉnh.
docker compose run --rm tutorial python -m pdfvqa.cli prepare --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfvqa.cli inspect-batch --config configs/minimum.yaml
docker compose run --rm inference python -m pdfvqa.cli baseline --config configs/minimum.yaml
prepare xây dựng đầu vào và mục tiêu. inspect-batch cho phép kiểm tra những gì mô hình nhận được và những gì ta yêu cầu nó tạo. Với câu hỏi xuyên suốt, hãy nhận ra chi phí đã được phê duyệt, rồi kiểm tra mục tiêu thực sự chỉ trường này thay vì chi phí còn lại.
baseline nghĩa là tham chiếu để so sánh. Ở đây nó dùng mô hình đóng băng, không có điều chỉnh Orion, nhưng cùng hợp đồng đầu ra. Bước này trả lời một câu hỏi thiết yếu: mô hình đã làm được gì trước khi chúng ta huấn luyện?
Mở các đầu ra trung gian. Việc kiểm tra nhằm hiểu dữ liệu và lỗi, không phải viết lại câu trả lời tham chiếu sao cho khớp một dự đoán.
Bước 3 — đo lường và kiểm soát
Bây giờ chúng ta có thể huấn luyện adapter, kiểm tra việc tiếp tục sau gián đoạn, rồi tạo và đánh giá các câu trả lời đã điều chỉnh.
docker compose run --rm tutorial python -m pdfvqa.cli train --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfvqa.cli train-continuous --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfvqa.cli train-resume --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfvqa.cli verify-resume --config configs/minimum.yaml
docker compose run --rm inference python -m pdfvqa.cli predict --config configs/minimum.yaml
docker compose run --rm tutorial python -m pdfvqa.cli evaluate --config configs/minimum.yaml
train học các tham số LoRA và lưu một checkpoint, một trạng thái giúp giữ lại công việc đã thực hiện. Các lệnh train-continuous và train-resume so sánh một lần chạy liên tục với một lần chạy được tiếp tục. verify-resume kiểm tra việc gián đoạn không làm thay đổi thí nghiệm.
Tiếp tục đúng cách cần nhiều hơn việc tải lại vài ma trận. Optimizer, thành phần điều khiển cập nhật của chúng, cũng có trạng thái; các bộ sinh ngẫu nhiên phải được khôi phục. Kết quả tham chiếu r001 cho biết việc tiếp tục là EXACT. Điều đó mô tả kiểm tra của lộ trình này, không hứa hẹn đồng nhất số học trên mọi phần cứng hoặc sau khi thay đổi phụ thuộc.
predict sau đó chạy mô hình đã điều chỉnh trong dịch vụ suy luận. evaluate ghép dự đoán với chú thích để tính các phép đo. Vì thế labels dùng để chấm điểm sau dự đoán, không phải để giúp mô hình lúc trả lời.
Các phép đo trước các con số
Đọc các chỉ số theo đúng thứ tự
Hãy bắt đầu bằng tính hợp lệ của JSON và hợp đồng. Tên gọi có thể gợi đến việc chỉ giải mã JSON, nhưng kiểm tra trình bày ở đây đi xa hơn: khóa chính xác, kiểu, cặp câu trả lời/hộp và tọa độ. Một kết quả bị từ chối phải vẫn hiển thị; không được chỉ giữ những mảnh tạo cảm giác thành công.
Độ chính xác câu trả lời so sánh văn bản với tham chiếu sau quy tắc chuẩn hóa mà dự án quy định. Đó không phải đánh giá tự do về nghĩa. Đừng thay quy tắc này bằng chuyển đổi khoản tiền hay một cách diễn đạt lại được chọn sau khi thấy kết quả.
IoU đo độ chồng lấp của các hộp cho các câu hỏi có thể trả lời. Nó không áp dụng cho trường vắng mặt: không có hình chữ nhật mong đợi để so với một sự từ chối trả lời.
Độ đúng kết hợp yêu cầu kiểm tra câu trả lời, lược đồ và vị trí đều thỏa mãn đồng thời. Vì vậy một giá trị tốt không bù được hộp sai. Ngưỡng số để chấp nhận vị trí không được nêu chi tiết trong phần trình bày này: hãy giữ quy tắc của bộ đánh giá trong ZIP, không suy ra nó từ IoU trung bình hay thay bằng một ngưỡng tùy ý.
Cuối cùng, độ chính xác từ chối trả lời trả lời « trong các lần từ chối được đưa ra, bao nhiêu lần thực sự liên quan đến trường không có mặt? ». Độ bao phủ từ chối trả lời trả lời « trong các trường không có mặt, bao nhiêu trường thực sự dẫn đến một lần từ chối? ».
Ví dụ sư phạm: trên mười trường không có mặt, một hệ thống từ chối đúng tám lần, nhưng cũng từ chối một lần khi câu trả lời hiện diện. Độ bao phủ của nó là 8/10 = 0,8; độ chính xác là 8/9, tức khoảng 0,8889. Hai phép đo không kể cùng một lỗi. Ví dụ này không phải kết quả của mô hình Orion.
Một hệ thống luôn trả lời có thể quên các trường vắng mặt; một hệ thống luôn từ chối có thể bỏ lỡ mọi giá trị hiện diện. Đó là lý do chúng ta giữ cả hai nhóm và nhiều phép đo.
Diễn giải đầu ra từng bước
Sau predict, hãy tìm câu trả lời JSON thô, trạng thái xác thực, hộp và lớp phủ, còn gọi là overlay. Đó là hình chữ nhật vẽ trên ảnh để giúp bạn kiểm tra, không phải một xác thực bổ sung của mô hình.
Với đầu ra được chấp nhận, hãy so sánh văn bản, trường được yêu cầu và vùng được đóng khung. Với đầu ra bị từ chối, hãy giữ lỗi: json, schema, box_bounds hoặc nguyên nhân khác. Biến kết quả không hợp lệ thành câu nghe có vẻ hợp lý sẽ làm mất chẩn đoán.
Đây là các kết quả tham chiếu r001, trên 240 câu hỏi kiểm thử của bộ dữ liệu tổng hợp Orion:
Trên thiết bị di động, hãy cuộn bảng theo chiều ngang.
| Phép đo LoRA | Nhóm liên quan | Kết quả quan sát |
|---|---|---|
| Độ chính xác câu trả lời | 240 câu hỏi | 1,0000 |
| Tính hợp lệ JSON và hợp đồng | 240 câu hỏi | 1,0000 |
| Độ đúng kết hợp | 240 câu hỏi | 1,0000 |
| IoU trung bình | 180 câu hỏi có thể trả lời | 0,9857 |
| Độ chính xác từ chối trả lời | Các lần từ chối trong kiểm thử | 1,0000 |
| Độ bao phủ từ chối trả lời | 60 trường không có mặt | 1,0000 |
Mô hình đóng băng đạt 0,3000 về độ đúng kết hợp, so với 1,0000 sau điều chỉnh LoRA. Trên 240 câu hỏi, điều này tương ứng với 72 thành công kết hợp so với 240, theo tiêu chí của lộ trình này.
Đọc sự tương phản giữa độ đúng kết hợp hoàn hảo và IoU 0,9857 thế nào? Mọi câu trả lời đều qua tiêu chí vị trí của bộ đánh giá, nhưng không phải mọi hình chữ nhật đều trùng khớp chính xác với tham chiếu. Thành công theo tiêu chí không phải là tọa độ bằng nhau hoàn toàn.
Kết quả ở đây cho thấy điều chỉnh mang lại cải thiện so với mô hình đóng băng. Chúng không dự đoán hiệu năng trên chữ viết tay, bản quét kém chất lượng hay nhãn khách hàng khác. Chúng cũng không chứng minh ưu thế so với các phương pháp mà lộ trình này không so sánh.
Khắc phục sự cố và chuyển sang sản phẩm
Mô hình không dùng được trong Docker? Hãy quay lại kiểm tra môi trường, quyền truy cập GPU và tích hợp NVIDIA. Không thể chẩn đoán một vấn đề thực thi từ chỉ số của câu trả lời.
JSON bị từ chối? Hãy kiểm tra prompt, giới hạn sinh và phiên bản mô hình. Một câu trả lời dừng quá sớm có thể không đầy đủ. Hộp ngoài trang vẫn không hợp lệ: đừng cắt nó để qua kiểm tra và đừng lấy lại giá trị từ tham chiếu.
Văn bản chính xác nhưng hộp đặt sai? Hãy trình bày đầu ra này là chưa được chứng minh đầy đủ. Giao diện có thể giúp nhìn thấy lỗi; nó không được che giấu lỗi ấy dưới một điểm số chung.
Trong một sản phẩm, mỗi đề xuất nên luôn được liên kết với PDF bất biến, một trang, một vùng, một phiên bản mô hình và nhật ký quyết định. Đây là những thành phần cần dự tính xung quanh hướng dẫn, không phải các trường mới cần âm thầm thêm vào hợp đồng JSON đã kiểm thử.
Những điều chúng ta đã học
LoRA học một hiệu chỉnh hạng thấp trong các phép chiếu mục tiêu, không sửa các trọng số nền. Mask huấn luyện giữ ngữ cảnh trong đầu vào, nhưng giới hạn các mục tiêu mà mất mát được tính trên đó.
Khi câu trả lời đã được tạo, Python kiểm tra hợp đồng của nó. Sau đó đánh giá kiểm tra điều khác: đúng văn bản, đúng vùng và đúng quyết định trả lời hoặc từ chối. Không chiều nào trong số này thay thế được các chiều còn lại.
Trên Orion, sự điều chỉnh cải thiện rõ rệt độ đúng kết hợp so với mô hình đóng băng. Để hiểu vì sao một câu trả lời chính xác về văn bản vẫn chưa đủ, giờ chúng ta sẽ chỉ dịch chuyển phần chứng minh trực quan của nó.
Bài tập — di chuyển bằng chứng mà không đổi văn bản
Trước khi chạy bài tập, hãy viết giả thuyết của bạn. Độ chính xác văn bản, IoU và độ đúng kết hợp sẽ ra sao nếu mỗi hộp được dịch ngang 100 đơn vị chuẩn hóa mà không đổi câu trả lời? Điều gì sẽ xảy ra với các lần từ chối, vốn không có hộp?
Hãy nghĩ đến phép tính nhỏ ở trên: hai hình chữ nhật có thể vẫn nằm trong trang nhưng chồng lấp ít hơn rất nhiều. « Tọa độ được phép » và « định vị tốt » không phải là từ đồng nghĩa.
Sau đó chạy thí nghiệm được cung cấp mà không sửa mã đã xác nhận:
docker compose run --rm tutorial python -m pdfvqa.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 thực hiện bài tập. Lệnh tiếp theo tạo các hình, còn lệnh cuối kiểm tra các checkpoint mong đợi. Hãy so sánh đầu ra với dự đoán của bạn trước khi chỉ hài lòng với điểm cuối.
Kết quả quan sát: 240 văn bản vẫn giống hệt và độ chính xác văn bản bằng 1. IoU trung bình giảm xuống 0,1692 và độ đúng kết hợp còn 0,25. 60 lần từ chối vẫn đúng, và không hộp nào vượt giới hạn.
0,25 trở nên đặc biệt dễ hiểu: 60 lần từ chối chiếm 60/240 = 0,25 của kiểm thử. Chúng đủ để giải thích mọi thành công kết hợp còn lại. Văn bản không đổi; chính vị trí của các câu trả lời hiện diện không còn qua kiểm tra kết hợp.
Không có mâu thuẫn với validator. Một hình chữ nhật được dịch chuyển vẫn có thể giữ bốn số nguyên, chiều rộng dương và tọa độ giữa 0 và 1000. Nó vẫn đúng dạng trong khi chỉ vào vùng sai.
Sự xáo trộn đầu ra này khác với hai trang bổ sung của bài tập, trong đó một trường thực sự đã đổi vị trí. Trong một trường hợp, chúng ta di chuyển hộp sau dự đoán; trong trường hợp kia, chúng ta thay đổi tài liệu mà mô hình phải đọc. Đừng trình bày các con số của thí nghiệm đầu tiên như phép đo của thí nghiệm thứ hai.
Chọn hành động sau đánh giá
Một câu trả lời không được chứng minh đầy đủ phải được trình bày cho người vận hành cùng PDF và lớp phủ, không tự động gửi đến hệ thống kế toán. Một sự từ chối không được thay bằng giá trị gần đúng có vẻ gần nhất.
Tuy nhiên cần phân biệt một chỉ số đánh giá với kiểm tra sẵn có trong môi trường sản xuất. Độ đúng kết hợp và IoU với gold cần một chú thích đã biết. Trên PDF mới không có tham chiếu, chương trình không bỗng nhiên có các điểm này để tự quyết định. Vì vậy quy tắc từ chối, kiểm soát nghiệp vụ và rà soát con người phải được thiết kế và đánh giá riêng.
Tương tự, hợp đồng được kiểm thử biểu diễn việc từ chối bằng hai giá trị null; nó không có trường giải thích lý do. Giữ một lý do đã được xác minh trong nhật ký hoặc giao diện là công việc tích hợp bổ sung, không phải thông tin cần bịa ra từ null.
Để điều chỉnh thí nghiệm cho biểu mẫu của bạn, hãy tách dữ liệu theo họ và mẫu tài liệu. Giữ tập kiểm thử riêng trước các vòng lặp, rồi đo riêng câu trả lời, lược đồ, vị trí và việc từ chối. Mục tiêu không chỉ là đạt con số tốt hơn mà còn biết những lỗi nào nó có thể che giấu.
Đầu ra mong đợi và lỗi hữu ích
Sau prepare, hãy nhận ra các trang, câu hỏi và mục tiêu riêng biệt. Sau baseline, giữ các đầu ra của mô hình đóng băng để so sánh cùng các trường hợp. Sau predict, xem valid, error, rồi answer và box trong câu trả lời có cấu trúc khi chúng có mặt. Trong đoạn xác thực, câu trả lời này được đặt dưới value, có giá trị None khi bị từ chối.
evaluate nêu rõ các nhóm và phân biệt kiểm thử với bài tập. Sự phân biệt này tránh trộn các câu hỏi mới với sự xáo trộn nhân tạo của các dự đoán có sẵn.
Kết quả asymmetric_null báo hiệu một câu trả lời không có hộp hoặc một hộp không có câu trả lời. box_type báo vấn đề về biểu diễn; box_bounds báo vấn đề giới hạn hoặc thứ tự tọa độ. Những lỗi này là đầu ra hữu ích của chương trình. Làm chúng biến mất mà không hiểu nguyên nhân sẽ khiến hệ thống khó quan sát hơn chứ không chính xác hơn.
Vận hành, giới hạn và dọn dẹp
Hướng dẫn cung cấp một thí nghiệm trên các biểu mẫu hư cấu. Để chuyển sang tài liệu thật, hãy xây dựng các ví dụ được phép và có tính đại diện, với câu trả lời và vùng đã xác minh. Các biến thể về nhãn, chất lượng hình ảnh và bố cục phải được đánh giá thay vì được giả định là đã bao phủ.
Hãy giữ phiên bản mô hình, tài liệu, trang, vùng, đơn vị và quy tắc từ chối. Thêm quyền truy cập, giám sát và rà soát con người cho các chi phí hoặc quyết định hợp đồng. Sự vắng mặt, đơn vị mơ hồ hay hộp không hợp lệ không bao giờ được biến thành sự chắc chắn chỉ nhờ cách giao diện trình bày.
Trước khi dọn dẹp, hãy sao lưu các export hữu ích. Script dưới đây chỉ nhắm đến outputs/:
docker compose down
docker compose run --rm tutorial python scripts/clean.py --outputs-only
docker compose down
Lệnh docker compose down đầu tiên dừng các dịch vụ của dự án. Container được khởi chạy sau đó thực hiện dọn dẹp có mục tiêu, rồi lần dừng cuối kết thúc lộ trình. Các lệnh này không yêu cầu dọn dẹp Docker toàn cục.
Để tìm hiểu sâu hơn phương pháp, tài liệu chính thức về PEFT và LoRA trình bày các adapter trên mô hình huấn luyện trước. Bài báo nền tảng về LoRA mô tả ý tưởng hiệu chỉnh hạng thấp. Các tài liệu này bổ sung cho giải thích; triển khai trình bày ở đây vẫn là lớp LoRALinear trong ZIP, không phải viết lại bằng thư viện khác.
Các tài liệu được liên kết trong bài này dùng để hiểu cơ chế. Để tái tạo r001, hãy giữ các lệnh, cấu hình và phụ thuộc của dự án được cung cấp: một ví dụ trong tài liệu không phải lời mời thay đổi môi trường tham chiếu.
Những điều chúng ta đã học: một giá trị trở nên có thể kiểm soát khi nó vẫn liên kết với trang, vùng và các quy tắc xác thực rõ ràng. Bước cuối không phải tin vào khung hiển thị mà là kiểm tra điều nó cho thấy.