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

Phân loại ticket khách hàng theo định dạng ứng dụng của bạn hiểu

Phân loại ticket theo danh mục, mức ưu tiên và sản phẩm mà không để mô hình tự tạo khóa. Bài hướng dẫn giới hạn từng bước câu trả lời trong 48 tổ hợp được phép, rồi kiểm tra riêng các trường có thực sự khớp ticket hay không.

Contrat fermé → Trie de tokens → Mesures. JSON contraint

Bạn đang phát triển một công cụ hỗ trợ khách hàng. Để sắp xếp các yêu cầu, ứng dụng của bạn cần một danh mục, một mức ưu tiên và một sản phẩm. Với biểu mẫu thông thường, bạn sẽ tạo ba danh sách thả xuống rồi kiểm tra giá trị ở phía máy chủ. Nhưng ở đây, người dùng viết một thông điệp tự do: mô hình phải tìm ra ba thông tin đó.

Liệu chỉ cần yêu cầu mô hình « trả lời bằng JSON » rồi chuyển câu trả lời của nó cho phần còn lại của ứng dụng? Chưa hẳn. Mô hình có thể thêm một câu, quên một khóa hoặc đề xuất giá trị mà chương trình của bạn không biết. Và ngay cả khi tuân thủ hoàn hảo định dạng, nó vẫn có thể chọn sai danh mục.

Vì vậy, chúng ta sẽ tách hai vấn đề: nhận được câu trả lời có dạng được phép, sau đó kiểm tra câu trả lời đó có khớp với ticket hay không. Với vấn đề thứ nhất, chúng ta can thiệp trong khi sinh, bằng cách giới hạn các token tiếp theo mà mô hình được chọn. Với vấn đề thứ hai, chúng ta giữ một bước kiểm tra độc lập.

Tải xuống toàn bộ dự án

Dự án chạy cục bộ với Python, Docker và GPU NVIDIA. Nó xây dựng một thí nghiệm phân loại, không phải giao diện web hay dịch vụ hỗ trợ sẵn sàng triển khai. Nó không đưa ra quyết định nghiệp vụ: phân loại ticket không cho phép hoàn tiền đơn hàng hoặc sửa đổi tài khoản.

Vấn đề và phần xây dựng

Ba trường, số lượng câu trả lời hữu hạn

Kho dữ liệu có 240 ticket giả bằng tiếng Anh: 48 ticket dùng để phát triển prompt và 192 ticket để kiểm thử. Prompt là toàn bộ chỉ dẫn và văn bản gửi cho mô hình. Ở đây, giai đoạn phát triển dùng để chốt các chỉ dẫn này; đó không phải là huấn luyện trọng số mô hình.

Mỗi câu trả lời phải kết hợp ba trường sau:

Trường Ý nghĩa Giá trị được phép
category Nhóm yêu cầu billing, delivery, technical, unknown
priority Mức độ ưu tiên low, normal, high
product Sản phẩm liên quan P01, P02, P03, unknown

Chúng ta giữ các tên và giá trị này bằng tiếng Anh: chúng thuộc về hợp đồng của chương trình. Chẳng hạn, technical và technique không phải là hai cách viết có thể hoán đổi cho trình xác thực.

Để theo dõi cơ chế, hãy dùng một ví dụ sư phạm được thêm vào phần giải thích, tách biệt với các ticket trong kho dữ liệu và các đầu ra được đo lường:

My P02 device will not start. This is urgent.

Với ví dụ này, chúng ta đặt câu trả lời tham chiếu là danh mục technical, mức ưu tiên high và sản phẩm P02:

{"category":"technical","priority":"high","product":"P02"}

Tham chiếu này là một nhãn chú thích: câu trả lời mong đợi được xác định độc lập với đầu ra mà mô hình sẽ tạo. Trong một ứng dụng thực tế, quyết định điều gì đáng mức ưu tiên high cần có quy tắc nghiệp vụ rõ ràng.

Có bốn lựa chọn danh mục, ba lựa chọn ưu tiên và bốn lựa chọn sản phẩm. Do đó chương trình có thể liệt kê 4 × 3 × 4 = 48 tổ hợp. Nó viết từng tổ hợp theo thứ tự ổn định, không có khoảng trắng thay đổi: ta có 48 chuỗi chuẩn, tức 48 cách biểu diễn văn bản được chọn làm tham chiếu cho quá trình sinh.

48 câu trả lời có thể có và 48 ticket phát triển là hai con số khác nhau về vai trò. Con số thứ nhất đến từ hợp đồng; con số thứ hai đến từ việc chia kho dữ liệu. Mỗi ticket có thể được xếp vào một trong 48 câu trả lời ấy.

Yêu cầu, ràng buộc, kiểm tra: ba thao tác khác nhau

Một ticket đi qua prompt, mô hình, trie các token được phép, rồi các bước kiểm tra JSON, lược đồ và giá trị.

Một chỉ dẫn yêu cầu mô hình tôn trọng định dạng. Một ràng buộc giải mã ngăn mô hình chọn token tiếp theo đi ra ngoài các đường dẫn được phép. Sau đó, một trình xác thực xem xét câu trả lời thu được.

Vì vậy, chúng ta không vá một JSON thiếu sau khi nó được tạo. Ở mỗi bước, chương trình chỉ giữ các token kéo dài một trong 48 chuỗi chuẩn. Tín hiệu kết thúc chỉ được phép khi một chuỗi đã hoàn chỉnh.

Ngữ pháp nhỏ này cố ý hẹp hơn JSON. Nó không cho phép thêm văn bản tự do, khóa mới hay số tiền tùy ý. Cú pháp tổng quát được mô tả trong RFC 8259; mục tiêu của chúng ta là xây dựng một ngôn ngữ hữu hạn cho hợp đồng cụ thể này, không phải một trình phân tích mọi JSON.

Các khái niệm hữu ích trước khi chạy GPU

Mô hình đề xuất mảnh văn bản tiếp theo

Một mô hình ngôn ngữ tạo ra một chuỗi token, hay đơn vị mã hóa. Chúng không nhất thiết là ký tự hay từ hoàn chỉnh: một token có thể biểu diễn một phần từ hoặc các dấu. Tokenizer biến văn bản thành các định danh số và cho phép quay lại văn bản. Tài liệu Transformers về tokenization trình bày các cách phân tách này.

Tại một thời điểm, mô hình nhận văn bản đã có và tính một điểm cho từng token trong từ vựng. Điểm này gọi là logit. Trong bài này, chọn điểm lớn nhất tương đương chọn token tiếp theo; phép argmax trả về chỉ số của nó.

Logit không phải là phần trăm tin cậy về danh mục đúng của ticket. Nó tham gia vào việc chọn mảnh văn bản tiếp theo, không phải vào xác thực nghiệp vụ.

Trie mô tả các phần tiếp nối được phép

Một trie là cây tiền tố. Tiền tố đơn giản là phần đầu của một chuỗi. Nhiều câu trả lời bắt đầu giống nhau sẽ dùng chung đoạn đầu của đường đi, rồi tách ra tại nơi token của chúng khác nhau.

Hãy hình dung tính năng tự hoàn thành chỉ cho phép các phần tiếp nối của một danh mục đóng. Sau mỗi lựa chọn, ta tiến trong danh mục các đường đi còn lại. Trie không đọc ticket và không biết danh mục nào đúng: nó chỉ biết token nào vẫn có thể dẫn đến câu trả lời được phép.

Nếu A(s) chỉ các token được phép sau tiền tố s, chúng ta giữ các logit của chúng và thay tất cả token khác bằng âm vô cực, ký hiệu -inf. Nhờ vậy, argmax chọn trong các phần tiếp nối chấp nhận được, miễn là có ít nhất một phần có điểm hữu hạn. Ta sẽ theo dõi một ví dụ số trong mã.

Kết thúc quá trình sinh cũng là một quyết định

EOS, viết tắt của end of sequence, là token đặc biệt báo hiệu kết thúc. Nó không phải dấu ngoặc nhọn } của JSON. Chương trình phải phân biệt « đối tượng đã hoàn chỉnh » với « quá trình sinh đã kết thúc theo quy tắc định trước ».

Cho phép EOS ngay từ đầu sẽ để mô hình dừng mà không viết gì. Ở đây, EOS chỉ được đề xuất tại nút cuối của trie, tức sau một chuỗi hoàn chỉnh. Tuy nhiên, giới hạn độ dài có thể ngắt thực thi trước khi kết thúc này: chúng ta cũng sẽ đo trường hợp đó.

Ba câu hỏi cần đặt ra cho câu trả lời

Có đọc được không? Trình phân tích có thể chuyển văn bản thành một giá trị JSON không? Có phù hợp không? Có đủ ba khóa và các giá trị được phép không? Có chính xác không? Các giá trị có tương ứng với nhãn chú thích của ticket không?

Trong ví dụ của chúng ta, thay P02 bằng P03 không phá vỡ cú pháp hay hợp đồng: P03 là sản phẩm được phép. Tuy vậy câu trả lời trở thành sai. Chính khác biệt này thí nghiệm sẽ làm rõ.

Điều kiện tiên quyết về Docker và GPU

Bạn cần biết đọc hàm Python, danh sách và từ điển, sử dụng JSON, terminal và những kiến thức cơ bản về Docker. Các khái niệm giải mã được giải thích trong bài; không cần kinh nghiệm huấn luyện trước đó.

Môi trường đã xác thực dùng Linux x86_64, Docker với Compose, NVIDIA Container Toolkit và RTX 3090 24 GiB. Hãy dự trù 16 GiB RAM và 16 GiB dung lượng đĩa, không tính các lớp Docker dùng chung. Bộ nhớ video, hay VRAM, thuộc về card đồ họa; nó không thay thế RAM của máy. Không có lần chạy CPU nào được trình bày là tương đương với lộ trình GPU này.

Mô hình là Qwen2.5-0.5B-Instruct, tại bản sửa đổi công khai cố định 7ae557604adf67be50417f59c2c2f167def9a775. « Instruct » chỉ việc nó được điều chỉnh để làm theo chỉ dẫn; điều đó không đảm bảo nó làm theo không lỗi. Transformers tải nó cục bộ, không dùng mã từ xa. Trang mô hình ở bản sửa đổi này bổ sung mô tả.

Mô hình đã chứa trọng số, các tham số số có được trong quá trình học. Ở đây, chúng ta dùng chúng để tính đầu ra: đó là suy luận. Xây trie hoặc thay đổi các token được phép không huấn luyện các trọng số này.

Giải nén kho lưu trữ và mở terminal tại thư mục gốc của nó. Mọi lệnh trong bài đều xuất phát từ cùng thư mục này. setup.sh chuẩn bị thư mục và chỉ ghi UID/GID vào .env: các định danh này liên quan đến quyền của người dùng và nhóm đối với tệp.

acquire tải các tệp công khai, xác minh chúng rồi chỉ đặt chúng dưới tên cuối cùng sau khi xác thực, bằng đổi tên nguyên tử. models/ giữ phiên bản cục bộ này, gọi là snapshot; outputs/ nhận số đo và artifacts/ nhận các checkpoint của dự án. Đừng nhầm những tệp kiểm soát này với một lần huấn luyện mới.

sh scripts/setup.sh
docker compose config --quiet
docker compose build acquire gpu
docker compose run --rm acquire python scripts/acquire.py --config configs/minimum.yaml
docker compose run --rm gpu python scripts/check_environment.py --config configs/minimum.yaml

Năm lệnh có trách nhiệm khác nhau. config --quiet kiểm tra cấu hình Compose. build xây dựng các image cần thiết. acquire.py chuẩn bị tệp mô hình. Cuối cùng, check_environment.py phải thành công trước khi suy luận.

CUDA ở đây cho phép chạy các phép tính trên GPU NVIDIA. Việc card hiển thị trong Linux chưa đủ chứng minh nó dùng được từ Docker. Nếu CUDA không khởi tạo trong container, hãy bắt đầu bằng tài liệu NVIDIA Container Toolkit, thay vì sửa bộ giải mã.

Các lớp Docker đã có sẵn có thể rút ngắn thời gian xây image. Thời gian chuẩn bị này không phải là phép đo giải mã của mô hình.

Kiểm tra chương trình trước khi kiểm tra động cơ

Trước khi yêu cầu mô hình phân loại ticket, hãy kiểm tra các quy tắc sẽ bao quanh nó. Bộ kiểm thử công khai bao phủ lược đồ, trie và các kịch bản thất bại. Chạy nó sau khi kiểm tra GPU:

docker compose run --rm gpu python -m pytest -q

Thành công có nghĩa là các hành vi được kiểm thử của chương trình khớp với điều mong đợi. Nó chưa có nghĩa mô hình sẽ hiểu các ticket. Đây cũng là khác biệt giữa kiểm tra trình xác thực của một biểu mẫu và xác minh tính đúng đắn của thông tin đã nhập.

Đừng bỏ qua bước này để có JSON đầu tiên nhanh hơn: một đầu ra có vẻ hợp lý sẽ không cho phép phát hiện nhánh cấm bị chấp nhận nhầm.

Dữ liệu và hợp đồng đầu ra

Xây dựng các câu trả lời có thể có mà không biết câu đúng

Để ràng buộc mô hình, chúng ta cần mọi câu trả lời được phép, không phải câu trả lời mong đợi cho một ticket nhất định. Vì thế hàm sau không nhận ticket hay nhãn chú thích nào:

def enumerate_canonical_outputs():
    return [
        json.dumps(dict(zip(SCHEMA, values)), separators=(",", ":"))
        for values in itertools.product(*SCHEMA.values())
    ]

Trong đoạn trích này, SCHEMA là ánh xạ giữa tên trường và danh sách giá trị của chúng. Tên gọi này không có nghĩa đây là tài liệu theo định dạng chuẩn JSON Schema: mã ở đây duyệt một hợp đồng được liệt kê.

SCHEMA.values() cung cấp các danh sách lựa chọn. Dấu sao trong itertools.product(*SCHEMA.values()) truyền chúng như các đối số riêng biệt. Tích Descartes tạo mọi tổ hợp, như ba vòng lặp lồng nhau: một danh mục, rồi một mức ưu tiên, rồi một sản phẩm. Tài liệu itertools.product mô tả chi tiết thao tác này.

Với tổ hợp như ("technical", "high", "P02"), zip(SCHEMA, values) gán mỗi tên cho giá trị của nó. dict(...) tạo đối tượng; json.dumps(...) chuyển nó thành văn bản. Cuối cùng, separators=(",", ":") buộc cách viết gọn, không có khoảng trắng giữa các phần tử.

Ta nhận được 48 chuỗi đã hứa. Chúng ta chưa chọn chuỗi phù hợp với ticket. Ngược lại, xây một trie chỉ chứa nhãn chú thích của nó sẽ là đưa đáp án cho bộ giải mã. Đó là rò rỉ mục tiêu: phép đo không còn đánh giá điều mô hình có thể tự tìm ra.

Vì sao phải cố định cách biểu diễn?

Hai cách viết sau biểu đạt cùng một đối tượng:

{"category":"technical","priority":"high","product":"P02"}
{
  "product": "P02",
  "priority": "high",
  "category": "technical"
}

Tuy vậy, chúng không phải cùng một chuỗi văn bản. Trie của chúng ta không cố cho phép mọi cách định dạng tương đương: nó giữ một cách viết chuẩn cho mỗi tổ hợp. Quyết định này đơn giản hóa các đường sinh. Nó không thay đổi thông tin mà đối tượng biểu đạt.

Kho dữ liệu cũng có các cách diễn đạt mơ hồ, dấu ngoặc nhọn nguyên văn và văn bản không đáng tin cố sửa đổi chỉ dẫn. Prompt yêu cầu coi ticket là dữ liệu, không bao giờ là lệnh. Chỉ dẫn này tự nó không phải là bảo vệ hoàn chỉnh: văn bản độc hại vẫn có thể cố hướng lựa chọn sang một giá trị sai nhưng được phép.

Bây giờ hãy chuẩn bị ticket, đối tượng chuẩn và khóa của prompt:

docker compose run --rm gpu python -m json_decode.cli prepare --config configs/minimum.yaml

Ở giai đoạn này, chúng ta cố định các yếu tố của thí nghiệm. Chúng ta không sửa một dự đoán và cũng không dạy mô hình các nhãn chú thích.

Cơ chế trung tâm: tham chiếu, trie và quá trình sinh

So sánh hai cách trên cùng các ticket

Chúng ta bắt đầu bằng tham chiếu tự do, hay baseline. Đây là hành vi để so sánh: cùng mô hình nhận cùng prompt nhưng không bị trie hạn chế từ vựng.

docker compose run --rm gpu python -m json_decode.cli baseline --config configs/minimum.yaml
docker compose run --rm gpu python -m json_decode.cli inspect-trie --config configs/minimum.yaml
docker compose run --rm gpu python -m json_decode.cli generate --config configs/minimum.yaml

baseline trước hết chạy 48 ticket phát triển và cố định prompt trước khi kiểm thử. Lỗi vẫn được lưu lại: chúng ta không chỉnh sửa chỉ dẫn sau khi đọc kết quả kiểm thử để làm đẹp phép so sánh.

inspect-trie làm rõ việc kiểm soát tokenization. generate chạy cả hai chế độ và lưu các câu trả lời thô cũng như lý do dừng. 192 ticket kiểm thử giống nhau ở cả hai cách.

Đi từ chuỗi sang cây token

Trước khi sinh, dự án kiểm tra lượt đi-về giữa văn bản và token để giữ các chuỗi chuẩn mục tiêu. Kiểm tra này thuộc khâu chuẩn bị và kiểm tra: lớp bên dưới không chứa tokenizer. Nó nhận các chuỗi định danh đã được xây sẵn.

class TokenTrie:
    def __init__(self, sequences):
        self.children = [{}]
        self.terminal = set()
        for sequence in sequences:
            node = 0
            for token in sequence:
                if token not in self.children[node]:
                    self.children[node][token] = len(self.children)
                    self.children.append({})
                node = self.children[node][token]
            self.terminal.add(node)

    def allowed(self, node, eos_ids):
        return sorted(
            set(self.children[node])
            | (set(eos_ids) if node in self.terminal else set())
        )

    def advance(self, node, token):
        if token not in self.children[node]:
            raise ValueError("Token forbidden by trie")
        return self.children[node][token]

self.children là danh sách các từ điển. Mỗi vị trí biểu diễn một nút của cây. Từ điển đầu tiên, ở chỉ số 0, là gốc. Trong một từ điển, khóa là định danh token và giá trị là chỉ số nút kế tiếp.

Với mỗi câu trả lời, vòng lặp bắt đầu lại từ gốc. Nếu nhánh đã tồn tại, nó đi theo; nếu không, nó tạo một nút. Vì vậy phần đầu chung không bị nhân bản. Cuối mỗi chuỗi, chỉ số hiện tại được thêm vào self.terminal.

Phương thức allowed() trả về các token có thể dùng từ một nút. Nó chỉ thêm định danh EOS nếu nút này là nút cuối. Sắp xếp theo số làm ổn định danh sách trả về: nó không xếp token theo sở thích của mô hình.

Cuối cùng, advance() đi theo một token thông thường trong cây và từ chối nhánh không tồn tại. EOS cần được xử lý riêng trong vòng sinh: đó là tín hiệu dừng, không tự động là một nhánh để đi qua bằng advance().

Theo dõi một cây nhỏ bằng tay

Hãy lấy hai chuỗi hư cấu, [10, 20, 30] và [10, 40, 30], với 99 là định danh EOS. Các số này chỉ giúp đọc cơ chế: chúng không phải định danh Qwen được ghi nhận, cũng không phải tokenization thật của JSON chúng ta.

Tiền tố đã chọn Token tiếp theo được phép Vì sao?
Không có 10 Cả hai chuỗi đều bắt đầu như vậy.
10 20 hoặc 40 Hai đường đi tách ra ở đây.
10, 20 30 Chỉ còn một đường đi tương thích.
10, 20, 30 99, tức EOS Chuỗi đã hoàn chỉnh.

Sau 10, 20, chọn 40 sẽ không thể hoàn thành một trong hai chuỗi đã lưu: chương trình từ chối nó. Ngược lại, cho phép 99 tại đây sẽ cho phép dừng giữa đường.

Trie thật áp dụng đúng nguyên tắc này cho các chuỗi dài hơn từ 48 JSON. Nó không suy ra cấu trúc của chúng từng ký tự từ minh họa này; nó dùng tokenization đã được xác minh của mô hình.

Giữ các điểm hữu ích, chặn các điểm khác

Trie biết các token chấp nhận được. Còn mô hình cung cấp các logit. Hàm sau ghép hai yếu tố lại:

def select_token(logits, allowed):
    if not allowed:
        raise ValueError("Impossible grammar: no allowed token")
    index = torch.tensor(sorted(allowed), device=logits.device, dtype=torch.long)
    scores = logits[index]
    if (
        torch.isnan(scores).any()
        or torch.isposinf(scores).any()
        or not torch.isfinite(scores).any()
    ):
        raise ValueError("No usable finite allowed logits")
    masked = torch.full_like(logits, -torch.inf)
    masked[index] = scores
    return int(masked.argmax()), masked

index đặt danh sách định danh được phép trên cùng thiết bị với các logit. scores = logits[index] lấy điểm của chúng. torch.full_like tạo một mảng cùng hình dạng chứa -inf, rồi masked[index] = scores chỉ khôi phục các giá trị chấp nhận được.

Đây là một ví dụ số được dựng lên, trên từ vựng chỉ có bốn token:

Định danh token Logit đề xuất Được phép? Logit sau khi che
0 9 Không -inf
1 2 Có 2
2 5 Không -inf
3 -1 Có -1

Không có ràng buộc, mô hình sẽ chọn token 0. Với ràng buộc, argmax trả về 1, định danh của token tốt nhất vẫn được phép. Nó không trả về điểm 2 của token đó. Tài liệu PyTorch về argmax mô tả hành vi này.

Vì sao không thay các điểm cấm bằng không? Vì điểm được phép có thể âm. Khi đó một số không bị cấm có thể thắng. -inf dùng để loại trừ một khả năng, không phải gán cho nó một điểm chỉ là thấp.

Các kiểm tra số bảo vệ điều gì?

Danh sách allowed rỗng cho thấy một ngữ pháp không thể tiếp tục. Hàm dừng rõ ràng thay vì chọn một token tùy ý.

Sau đó nó kiểm tra các điểm được phép. NaN là giá trị số không xác định; +inf là vô cực dương. Sự hiện diện của chúng gây lỗi. Không có bất kỳ điểm hữu hạn nào cũng gây lỗi.

Điểm tinh tế này hữu ích: một điểm được phép bằng -inf vẫn được chấp nhận nếu có ít nhất một điểm được phép khác là hữu hạn. Vì vậy đoạn trích không từ chối chung mọi giá trị không hữu hạn. Mục tiêu là giữ ít nhất một lựa chọn sử dụng được mà không để bất thường số trở thành một quyết định.

Cả hai chế độ đều dùng argmax. Chế độ tự do dùng nó trên toàn bộ từ vựng; chế độ ràng buộc dùng sau khi che. Đó là lựa chọn cục bộ, token sau token, không phải so sánh toàn cục 48 câu trả lời hoàn chỉnh. Ràng buộc đóng các đường đi; nó không chỉ ra đường nào mô tả ticket đúng.

Đo lường mà không nhầm lẫn giữa hình thức và nội dung

API sinh của Transformers cung cấp các tham số và cơ chế giải mã. Tài liệu chính thức đặt công việc này vào bối cảnh rộng hơn. Tuy nhiên, với thí nghiệm này, hãy giữ các lệnh và mã trong kho lưu trữ: phần giải thích không đòi hỏi thay thế động cơ sinh.

Đọc hàm gán điểm

row chứa đặc biệt văn bản đã sinh và lý do dừng. label chứa đối tượng mong đợi cho ticket này. Chúng ta chỉ đối chiếu câu trả lời với tham chiếu này ở thời điểm đánh giá:

def parse_result(row, label):
    try:
        value = json.loads(row["text"])
        valid_json = True
    except (ValueError, TypeError):
        value = None
        valid_json = False
    valid_schema = validate_schema(value)
    complete = row["finish_reason"] == "eos"
    return {
        "json_valid": valid_json,
        "schema_valid": valid_schema,
        "completed": complete,
        "length": row["finish_reason"] == "length",
        "exact_tuple": complete and valid_schema and value == label,
        "fields": {
            k: bool(complete and valid_schema and value[k] == label[k]) for k in SCHEMA
        },
    }

Khối đầu tiên cố chuyển row["text"] bằng json.loads. Nếu thất bại, json_valid là False. Chúng ta không bỏ một câu dẫn, không xóa khối Markdown và không bổ sung dấu ngoặc nhọn để cứu kết quả.

Kiểm tra validate_schema(value) đặt câu hỏi thứ hai: đối tượng có tôn trọng hợp đồng không? Phần triển khai nằm trong dự án; đoạn trích này cho thấy cách dùng, không phải tất cả kiểm thử nội bộ.

Biến complete chỉ phụ thuộc vào finish_reason == "eos". Lý do length nghĩa là đã chạm giới hạn sinh. Nó ngăn thành công hoàn toàn, ngay cả khi văn bản đã có thể giải mã.

Cuối cùng, exact_tuple đòi hỏi ba điều kiện: kết thúc EOS, đối tượng phù hợp và bằng với nhãn chú thích. « Bộ ba » chỉ ba giá trị nghiệp vụ; câu trả lời được thao tác ở đây vẫn là một từ điển Python.

So sánh value == label áp dụng cho các đối tượng đã giải mã, không phải tính đồng nhất từng byte của văn bản thô. Vì thế hai JSON trình bày ở trên, khác thứ tự khóa và khoảng trắng, cho cùng một đối tượng trong so sánh này. Quá trình sinh chuẩn hẹp hơn tiêu chí bằng nhau đó.

Chi tiết làm thay đổi cách đọc điểm theo trường

Hãy nhìn đầu biểu thức trong fields: complete and valid_schema. Một trường chỉ nhận điểm nếu toàn bộ câu trả lời phù hợp và kết thúc bằng EOS.

Nếu câu trả lời có hai giá trị đúng và một sản phẩm được phép nhưng sai, hai trường đó có thể được ghi điểm. Trái lại, nếu sản phẩm là P99, nằm ngoài hợp đồng, cả ba trường đều bằng không: trình xác thực từ chối toàn bộ đối tượng.

Tương tự, ba giá trị chính xác trong một lần sinh bị dừng vì length không mang lại điểm nào theo trường trong giao thức này. Đây là điểm có điều kiện về sự phù hợp và kết thúc sinh, không phải thống kê mọi mảnh đúng thấy được trong văn bản.

So sánh sáu câu trả lời cho cùng một ví dụ

Hãy trở lại nhãn chú thích sư phạm technical / high / P02. Bảng sau mô tả các trường hợp được dựng để hiểu hàm, không phải đầu ra ghi lại từ mô hình.

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

Trường hợp Văn bản hoặc thay đổi Kết thúc JSON đọc được Lược đồ hợp lệ Trường được tính Bộ ba chính xác
A Đối tượng mong đợi eos Có Có 3 trên 3 Có
B Cùng đối tượng, nhưng product là P03 eos Có Có 2 trên 3 Không
C Cùng đối tượng, nhưng product là P99 eos Có Không 0 trên 3 Không
D Đối tượng mong đợi, không thay đổi gì khác length Có Có 0 trên 3 Không
E Văn bản Voici le résultat : theo sau bởi đối tượng eos Không Không 0 trên 3 Không
F Đối tượng mong đợi, với khóa đổi thứ tự và thụt lề eos Có Có 3 trên 3 Có

Các trường hợp A và D cho thấy vì sao chúng ta giữ lý do dừng. B và C cho thấy khác biệt giữa giá trị được phép nhưng sai và giá trị bị cấm. F nhắc rằng một đối tượng chính xác không nhất thiết có cách viết chuẩn trong chế độ tự do.

Một lưu ý về trình phân tích được dùng

Trong đoạn trích này, json_valid nghĩa là « được json.loads chấp nhận với thiết lập mặc định ». Python chấp nhận một số hằng không hữu hạn và giữ giá trị cuối của một khóa lặp, chẳng hạn. Tài liệu mô-đun json trình bày chi tiết các hành vi đó. Vì vậy riêng lời gọi này không phải kiểm tra nghiêm ngặt mọi đặc điểm của RFC.

48 chuỗi chuẩn không đưa vào các hằng đó hay khóa lặp. Với đầu vào tổng quát hơn, cần định nghĩa và kiểm thử một chính sách rõ ràng. Đó sẽ là một bước phát triển của dự án, không phải lý do để âm thầm sửa các câu trả lời hoặc điểm tham chiếu.

Các phép đo trước kết quả

Giữ mọi ticket trong phép tính

Ở mỗi chế độ, evaluate dùng 192 ticket kiểm thử làm mẫu số. Nó tính tỷ lệ JSON hợp lệ, tỷ lệ lược đồ hợp lệ, tỷ lệ kết thúc EOS, độ chính xác bộ ba và độ chính xác từng trường. Không ticket thất bại nào bị loại bỏ.

Ví dụ, tỷ lệ « Sản phẩm đúng » là số ticket mà fields["product"] bằng True, chia cho 192. Đó không phải trung bình chỉ trên các câu trả lời phù hợp, cũng không phải phần trăm có được bằng cách yêu cầu mô hình tự chấm điểm.

Để kiểm tra quy tắc rõ ràng, hãy lấy một lô sư phạm nhỏ gồm bốn câu trả lời: A, B, D và E của bảng trước. Ba văn bản có thể giải mã và phù hợp là A, B và D. Ba lần kết thúc là EOS: A, B và E. Tuy vậy chỉ A có bộ ba chính xác.

Do đó ta có 3/4 = 75 % JSON có thể giải mã, 75 % phù hợp, 75 % kết thúc EOS, nhưng 1/4 = 25 % bộ ba chính xác. Danh mục và mức ưu tiên được tính cho A và B, tức mỗi trường 2/4 = 50 %; sản phẩm chỉ cho A, tức 25 %.

Lô nhỏ này không phải là phép đo Qwen. Nó cho thấy vì sao từng chỉ số trả lời một câu hỏi khác nhau. Loại E, vốn không giải mã được, sẽ đổi mẫu số và làm đẹp giả tạo nhiều tỷ lệ.

Chạy đánh giá và đọc các phép đo quan sát được

docker compose run --rm gpu python -m json_decode.cli evaluate --config configs/minimum.yaml

Các giá trị sau là phép đo cục bộ được quan sát với bản sửa đổi, mô hình và kho dữ liệu tổng hợp này. Chúng không dự báo tỷ lệ thành công trên ticket của doanh nghiệp bạn.

Giải mã ràng buộc cố định cú pháp và lược đồ, trong khi lỗi nội dung vẫn hiện rõ theo từng trường.

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

Chế độ, 192 ticket kiểm thử JSON hợp lệ Lược đồ hợp lệ Bộ ba chính xác Danh mục đúng Ưu tiên đúng Sản phẩm đúng
Tự do 27,60 % 2,08 % 0 % 0 % 2,08 % 2,08 %
Ràng buộc 100 % 100 % 25 % 47,40 % 64,06 % 75 %

Điều kết quả xác lập — và điều nó không xác lập

Quan sát đầu tiên rất rõ: trong thí nghiệm này, cách ràng buộc tạo ra 100 % JSON hợp lệ và 100 % đối tượng phù hợp. Mọi lần sinh đều kết thúc bằng EOS; không lần nào chạm giới hạn độ dài tối đa.

Quan sát thứ hai cũng quan trọng không kém: chỉ 48 trong 192 bộ ba là chính xác, tức 25 %. 144 câu trả lời ràng buộc còn lại tôn trọng định dạng nhưng sai ít nhất một giá trị. Ràng buộc đã giải quyết vấn đề hình thức ở lô này, không phải toàn bộ vấn đề phân loại.

Sản phẩm đúng trong 75 % trường hợp, mức ưu tiên trong 64,06 % và danh mục trong 47,40 %. Các tỷ lệ này không cộng lại thành tỷ lệ ticket thành công: các trường phải đúng cùng lúc, trên cùng ticket. Điểm bộ ba đo chính xác yêu cầu này.

Các số không ở chế độ tự do cần được đọc thận trọng hơn. Điểm theo trường đã yêu cầu một đối tượng phù hợp và kết thúc EOS. Vì thế số không ở danh mục không có nghĩa từ mong đợi không bao giờ xuất hiện trong các câu trả lời thô. Nó nghĩa là không câu trả lời nào thỏa mọi điều kiện để ghi điểm cho trường đó. Vẫn cần đọc đầu ra thô để phân biệt lỗi trình bày với lỗi nội dung.

Vì sao trie ở đây có thể đảm bảo hình thức mà không đảm bảo câu trả lời đúng? Bởi các đường của nó chỉ được xây từ 48 chuỗi được phép. Tính chất này giả định quá trình sinh đi theo trie đến một kết thúc được phép. Một lần ngắt hoặc lỗi số vẫn phải là thất bại, không phải câu trả lời thành công theo mặc định.

Cuối cùng, bảng không cho phép quy lỗi cho một nguyên nhân duy nhất, cũng không cho phép kết luận về tốc độ của một chế độ so với chế độ kia. Nó báo cáo các quan sát của giao thức này, không cô lập tác động của kích thước mô hình, prompt hay từng nhóm ticket.

Kiểm tra các nhánh bị từ chối

Một minh họa mà mọi thứ dường như đều ổn là chưa đủ. Cũng cần kiểm tra các thao tác phải thất bại: đó là vai trò của các trường hợp âm.

Dự án kiểm tra một ngôn ngữ không thể tiếp tục, các tiền tố bị cấm và quy tắc cho phép EOS. Chạy các kiểm tra này sau khi đánh giá:

docker compose run --rm gpu python -m json_decode.cli negative-cases --config configs/minimum.yaml

Với nhà phát triển web, điều này tương đương một kiểm thử cố ý gửi giá trị bị cấm vào biểu mẫu. Kiểm thử thành công khi sự từ chối mong đợi thực sự xảy ra, không phải khi đầu vào sai được chấp nhận.

Nếu một nhánh không tồn tại trở nên đi qua được hoặc EOS có thể xuất hiện quá sớm, cơ chế đã hỏng, dù một vài ticket vẫn tạo JSON đẹp. Một lỗi mong đợi không còn được phát hiện là một hồi quy.

Điều chúng ta đã học

Chúng ta đã biến hợp đồng ba trường thành tập hợp câu trả lời, rồi thành các đường token. Mô hình đề xuất điểm; trie chỉ ra các lựa chọn được phép; bộ chọn chọn trong số đó. Không bước nào trong các bước này tham khảo nhãn chú thích của ticket để đưa ra đáp án đúng.

Ở đầu ra, các trách nhiệm vẫn tách biệt. Trình phân tích kiểm tra việc đọc văn bản. Lược đồ kiểm tra khóa và giá trị được chấp nhận. Nhãn chú thích cho phép đo độ chính xác. Lý do dừng cho biết quá trình sinh có kết thúc như dự kiến không.

Kết quả 100 % phù hợp và 25 % bộ ba chính xác tóm tắt khác biệt này: đóng các khả năng trả lời không bổ sung cho mô hình kiến thức về ticket. Chúng ta có một cơ chế kiểm soát hình thức có thể kiểm chứng, không phải bằng chứng về tính đúng đắn nghiệp vụ.

Bài tập: đưa ra giả thuyết trước lệnh

Bây giờ hãy tưởng tượng một thay đổi: EOS sẽ được phép ở mọi nút của trie, kể cả ở gốc.

Trước khi chạy lệnh, hãy viết dự đoán của bạn. Nếu các logit ưu tiên EOS, mô hình có thể dừng ngay không? Nó có thể kết thúc sau một tiền tố JSON chưa hoàn chỉnh không? Văn bản trống có được trình phân tích chấp nhận không?

Mô-đun src/json_decode/exercise.py đã so sánh hai quy tắc với các logit được kiểm soát ưu tiên EOS. Bạn không cần sửa bộ giải mã tham chiếu. Chúng ta chủ ý kiểm tra một quy tắc dừng, không chạy chiến dịch mới nhằm cải thiện điểm.

docker compose run --rm gpu python -m json_decode.cli exercise --config configs/exercise.yaml

Hãy so sánh unsafe_immediate_eos, safe_first_token và empty_json_valid với dự đoán của bạn. Giá trị thứ nhất cho phép quan sát kết thúc tức thì với quy tắc nguy hiểm; giá trị thứ hai cho thấy lựa chọn đầu tiên với quy tắc được bảo vệ; giá trị thứ ba kiểm tra việc đọc một văn bản trống.

Định danh safe_first_token thuộc tokenization của dự án: đừng thay nó bằng 10 của cây hư cấu. Điều quan trọng là nó tạo thành một phần đầu được phép, thay vì dừng sớm.

Để giải thích kết quả, hãy phân biệt cho phép một phần tiếp nối với chấp nhận một đối tượng đã hoàn tất. Văn bản trống không phải tài liệu JSON. Văn bản "", gồm hai dấu ngoặc kép, biểu diễn một chuỗi JSON trống, nhưng vẫn không phải đối tượng ba trường được yêu cầu ở đây. Cú pháp và hợp đồng là hai bước kiểm tra khác nhau.

Điều chỉnh cơ chế và chuẩn bị cho môi trường sản xuất

Khi trie hữu hạn này vẫn phù hợp

Cơ chế này dễ kiểm tra vì hợp đồng nhỏ và đóng. Để sửa nó, hãy cùng lúc phát triển danh sách giá trị, cách tuần tự hóa, các kiểm thử và trình xác thực độc lập. Hãy duy trì việc tách phát triển/kiểm thử trước khi sửa prompt.

Tuy nhiên, số tổ hợp có thể tăng nhanh. Chỉ để minh họa, bốn danh mục, ba mức ưu tiên và 10 000 giá trị sản phẩm có thể có sẽ cho 4 × 3 × 10 000 = 120 000 câu trả lời cần liệt kê. Các tiền tố chung có thể được chia sẻ, nhưng mỗi tổ hợp vẫn phải được tính đến trong bước xây dựng này.

Trường nhận xét tự do không còn được mô tả bằng 48 đầu ra của bài hướng dẫn. Khi đó cần một ngữ pháp phù hợp, giới hạn độ phức tạp và độ dài, cùng kiểm thử tokenization. Phần mở rộng này thay đổi bài toán; nó không phải là tháo một hàng rào cho đến khi chương trình chấp nhận trường mới.

Giá trị được phép không phải là quyền được hành động

unknown được dự kiến cho danh mục và sản phẩm, không cho mức ưu tiên. Sự có mặt của nó đưa ra một giá trị có thể; nó không đảm bảo mô hình sẽ chọn khi lưỡng lự. Nó cũng không phải là phép đo đã hiệu chỉnh về sự không chắc chắn của mô hình.

Với ticket thực, cần có dữ liệu được phép và quản lý phiên bản, các nhãn chú thích được rà soát và quy tắc rõ ràng để xử lý mơ hồ. Chính sách từ chối và, khi hành động nhạy cảm, sự giám sát hoặc phê duyệt của con người vẫn cần thiết.

Ứng dụng cũng phải xử lý xác thực, kiểm soát quyền, khả năng quan sát — nhật ký, lỗi và phép đo hữu ích —, thử tải, sao lưu và kiểm toán phụ thuộc. Không tính chất nào trong số này bắt nguồn từ bảng điểm. Đầu ra có cấu trúc vẫn là đầu vào không đáng tin cho đến khi nghiệp vụ xác thực nó.

Khắc phục sự cố và dọn dẹp

Kiểm tra môi trường thất bại. Hãy dừng ở check_environment.py. Kiểm tra driver, NVIDIA Container Toolkit và card được Docker hiển thị. Vấn đề truy cập GPU không được sửa bằng cách thay đổi các lớp được phép.

Kiểm tra trie thất bại. Đừng chuyển sang generate. Trước hết hãy kiểm tra tuần tự hóa chuẩn và lượt đi-về tokenization/giải mã. Cây phải mô tả các chuỗi dự kiến, không phải một bản gần giống.

Các trường hợp âm không còn bắt được sự từ chối. Hãy xem các tiền tố bị cấm và quy tắc EOS trước khi đọc lại điểm mô hình. Một bản minh họa đơn lẻ tạo JSON không sửa được hồi quy này.

Để kết thúc lộ trình, hãy tạo các hình, kiểm tra checkpoint, xem trước việc dọn dẹp và dừng các dịch vụ. Chạy bốn lệnh này theo thứ tự, luôn từ gốc dự án:

docker compose run --rm gpu python lesson/figures/plot.py
docker compose run --rm gpu python scripts/check_checkpoints.py
docker compose run --rm gpu python scripts/clean.py --scope generated --dry-run
docker compose down --remove-orphans

Tùy chọn --dry-run yêu cầu xem trước: nó dùng để xem xét việc dọn dẹp dự kiến, không để thực hiện xóa. Đừng bỏ nó khi chưa kiểm tra những gì sẽ bị xóa. Lệnh cuối dừng tài nguyên Compose của dự án; không cần dọn sạch toàn cục các dự án Docker khác.

Nguồn chính

Định dạng và mã Python. RFC 8259 — định dạng JSON mô tả ngôn ngữ tổng quát. Tài liệu Python cho mô-đun json nêu rõ việc tuần tự hóa, giải mã và các đặc điểm của chúng; tài liệu itertools.product giải thích việc liệt kê các tổ hợp.

Mô hình và giải mã. Trang Qwen2.5-0.5B-Instruct tại bản sửa đổi được dùng, giới thiệu tokenization trong Transformers, API sinh của Transformers và hàm torch.argmax đào sâu các thành phần đã học.

Môi trường. Hướng dẫn cài đặt NVIDIA Container Toolkit liên quan đến truy cập GPU từ container.

Các tham chiếu này bổ sung cho phần giải thích. Để tái tạo các phép đo, hãy giữ nguyên mã, bản sửa đổi mô hình và các tham số đã cố định trong kho r1: các trang tài liệu hiện hành không tạo thành một cấu hình mới đã được xác thực cho bài hướng dẫn.

Chia sẻ bài viết