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

Fine-tune toàn bộ trọng số để chuyển ticket thành fiche can thiệp

Một xưởng nhận yêu cầu tự do. Hướng dẫn này hiển thị đầu ra của cùng ticket với prompt đơn, sau SFT đầy đủ và sau adaptation giới hạn ở hai block cuối.

Parcours pédagogique : comprendre le cas, construire, mesurer puis vérifier.

Một xưởng nhận yêu cầu bằng văn bản tự do. Chúng tôi muốn fiche JSON sao chép thông tin có mặt, chọn hạng mục trong các hạng mục xác định và để null cho thông tin thiếu. Không có truy cập GED hay thực thi can thiệp.

Từ cùng ticket, hiển thị đầu ra mô hình chỉ prompt, sau SFT đầy đủ và sau adaptation chỉ hai block cuối. Parse JSON và kiểm tra trường không decoding ràng buộc để tách hiệu ứng học.

Tải dự án hoàn chỉnh, bản sửa đổi r002

Hành trình của hướng dẫn, từ trường hợp cụ thể đến bước kiểm chứng.

Đầu ra tốt được dữ liệu mô tả

SFT tối đa hóa likelihood của phản hồi mong đợi theo prompt. Đây không phải đọc lại ticket thô: các cặp ticket/fiche dạy một biến đổi chính xác. Base Qwen2.5-0.5B đã có trọng số tiền huấn luyện; bài này không tuyên bố học ngôn ngữ từ đầu.

InterventionPairs có 400 family gồm sáu cách diễn đạt, tức 2400 cặp. Family 0–279 cho 1680 train; 280–339 cho 360 validation; 340–399 cho 360 test. Cách diễn đạt gần của family ở cùng nhau. Các cặp mở/đóng và định danh đơn vị phân biệt family; từ vựng triệu chứng, vị trí và câu vẫn chung. Hash chuẩn hóa kiểm tra bản sao sau chuẩn hóa khoảng trắng và chữ hoa/thường, không chứng minh không chồng ngữ nghĩa.

Các trường chính xác là unit, symptom, location, scheduled_date. Giá trị là chuỗi hoặc null; symptom không null thuộc noise, leak, heat, vibration. Schema kiểm tra kiểu và khóa, không kiểm tra sự thật chuỗi hay lịch. Ngày hư cấu sai có thể qua schema nhưng lỗi đánh giá trường. Bình đẳng nghiệp vụ là giá trị chính xác, không là thứ tự khóa JSON.

Mục tiêu không lấy thông tin vắng trong ticket: mệnh đề vắng thay sự kiện liên quan. Ngày vắng trong hai trên sáu cách diễn đạt; ca khác bỏ vị trí, đơn vị hoặc triệu chứng. Tỷ lệ thực mỗi split được ghi và vượt một phần tư. Tám demonstration được chọn bằng ID train cố định trước kết quả test.

Ranh giới phản hồi và loss

Định dạng cục bộ là instruction, Ticket: ... rồi JSON:. Không template hội thoại ngầm. Tokenizer nhanh từ base cố định. Hợp đồng mã hóa riêng prompt và JSON, nối danh sách token rồi thêm EOS=151643. Nó không được xem như retokenize chuỗi nối: ranh giới rõ và kiểm tra bằng accent và JSON rỗng.

Label prompt và padding là −100. Mọi token phản hồi và EOS có mục tiêu. Để dự đoán token sau, logits[:, :-1] thẳng labels[:, 1:]. Token đầu không có tiền nhiệm và không bao giờ là mục tiêu, kể cả ablation giám sát prompt.

PAD và EOS cùng ID. Mask phụ thuộc vị trí thật, không bằng ID: bỏ mọi 151643 sẽ mất tín hiệu dừng. Không sequence nào được pack. Prompt SFT/zero-shot giới hạn 384 token; phản hồi 96 gồm EOS. Phản hồi rỗng hay đầu vào dài lỗi cùng ID ca; không cắt ngầm.

CE là tổng vị trí hoạt động chia đúng số lượng. Khi tích lũy tám ví dụ, mỗi microbatch dùng mẫu số tổng của tám ví dụ dù phản hồi dài khác nhau. Trung bình các trung bình đổi trọng số token. Decoder và chiếu vocabulary tính cả sequence, rồi mask chọn vị trí CE. Kiểm thử so logits và loss với Transformers trong dung sai FP32; prompt và padding không được giám sát.

Những tham số nào có thể đổi?

Base là Qwen/Qwen2.5-0.5B revision 060db6499f32faf8b98477b0a26969ef7d8b9987. Kiến trúc được kiểm tra: rộng 896, 24 block, 14 head query, hai K/V, MLP 4864, vocabulary 151936, context 32768. Embedding đầu vào và chiếu đầu ra chung Parameter; chương trình tính tham số duy nhất.

Full cho mọi tham số thay đổi. Partial chỉ cho block 22 và 23; embedding, đầu ra liên kết, normalization cuối và block khác đóng băng. Chọn lựa duyệt đối tượng kiến trúc thật và kiểm tra tên, định danh, không trùng optimizer. Hash trước/sau phải giữ tham số đóng băng, còn tham số cho phép phải thực sự đổi. Cho phép sửa không đảm bảo mọi tensor đổi số ở mỗi bước.

Hai nhánh bắt đầu cùng base FP32, attention eager và TF32 tắt. Chúng dùng cùng dữ liệu, thứ tự, hai epoch và token phản hồi giám sát. AdamW, weight_decay 0,01, microbatch 1, tích lũy tám, clipping 1, warm-up 42 rồi giảm tuyến tính tới 10 %. Full lr=0,00002, partial lr=0,00005: so sánh cũng đổi learning rate, không chỉ số tham số. Không rate hay checkpoint chọn theo test; checkpoint cuối cố định sau hai epoch.

Validation phản hồi được đo mỗi epoch không chọn checkpoint tốt nhất. Tiếp tục tại ranh giới epoch, sau epoch gián đoạn. Nó khôi phục trọng số, AdamW, scheduler, RNG Python/NumPy/PyTorch CPU/CUDA, sampler và lịch sử. Đường liên tục hai epoch và tiếp tục phải cho trạng thái giống hệt. Không hứa tiếp tục giữa microbatch hay ghi chưa xong.

Trạng thái .pt tạo cục bộ, ghi tệp tạm rồi đổi tên nguyên tử; không thay bằng tệp không tin cậy. Mô hình cuối cũng lưu Safetensors xử lý chia sẻ embedding/đầu ra, rồi tải lại vào instance riêng. Tệp và tham số kiểm tra SHA-256. Trọng số học không nằm trong ZIP công khai; người đọc tạo lại.

Baseline và sinh không ràng buộc

Base được đánh giá zero-shot và với tám demonstration cố định. Tám ví dụ dùng ngân sách context riêng 2048 token, không ép vào 384 token prompt khác. Chi phí thật được ghi. Sinh dự trữ 96 token mới trong context gốc, không cắt prompt.

Mọi nhánh dùng greedy, một beam và dừng EOS=151643, tối đa 96 token. Không schema bị ép khi decoding. ID vào/ra, văn bản thô, lý do dừng, token và thời lượng được giữ cho 360 ca validation và 360 test. Trọng số không điều chỉnh bởi bước này.

Parser phân biệt JSON parse được, schema hợp lệ rồi phản hồi hoàn chỉnh chấp nhận. Phản hồi chạm trần có JSON tốt vẫn bị từ chối. Không bỏ Markdown, thêm trường, sửa hay làm giàu. Khóa trùng và hằng không hữu hạn bị từ chối.

Exact match toàn cục và đúng từng trường dùng 360 ca test. Đầu ra không hợp lệ hoặc cắt ngắn lỗi mọi trường. Với thông tin vắng, báo cáo tách null đúng, giá trị không null bịa trong phản hồi hợp lệ, và đầu ra lỗi. Tỷ lệ bịa dùng mọi dịp trường thiếu; lỗi cấp null gồm đầu ra lỗi. Mô hình không JSON không thể tỏ ra tin cậy nhờ tỷ lệ bịa bằng không.

NLL phản hồi đơn trên test tính lại cùng prompt zero-shot cho base, full, partial và ablation, gồm EOS và cùng mục tiêu. Few-shot là so sánh riêng nhiều context hơn; chất lượng và token được báo, không nhầm chi phí đó với hiệu ứng học.

Bài tập: đưa prompt vào loss

Nhánh full thứ ba thực sự quay lại base hai epoch, cùng thứ tự tài liệu và full, nhưng giám sát cả token prompt. Vì thế nhiều token hoạt động hơn. Loss học giảm không thể so trực tiếp với trung bình phản hồi đơn.

NLL của nó được tính lại trên cùng phản hồi test và 720 lần sinh validation/test, rồi so exact match và trường vắng. Không áp đặt tăng/giảm. Mô hình và kết quả chính được SHA bảo vệ trước bài tập. Mask phản hồi là tham chiếu; ablation có tên riêng.

Giới hạn

Ticket tổng hợp và đều. Label phản ánh generator, không đa dạng yêu cầu thật. JSON hợp lệ có thể chứa thông tin sai. Một seed không ước lượng biến thiên huấn luyện. Quy tắc tạo mục tiêu là oracle dạy học, không khám phá mô hình. Không hồ sơ hay hành động can thiệp nào được tạo hoặc thực thi.

Xác định tác vụ, schema và family

Demonstration có giám sát

Lỗi thường gặp: Generator ghi ngày mục tiêu vắng trong ticket: kiểm tra từng trường với sự kiện đã render.

Bất biến: 1 680/360/360 cặp và tỷ lệ vắng được audit trước loại trừ.

Tệp src/sft_complet/data.py

import hashlib, json, re
from pathlib import Path
from .config import save_json, sha

FIELDS = ["unit", "symptom", "location", "scheduled_date"]
SYMPTOMS = ["noise", "leak", "heat", "vibration"]
LOCATIONS = [
    "inlet",
    "outlet",
    "motor bay",
    "upper housing",
    "control panel",
    "rear cover",
    "lower bracket",
    "supply line",
    "return line",
    "service hatch",
]
OPENINGS = [
    "A technician submitted this observation.",
    "The operator left a short report.",
    "The shift log contains this request.",
    "A maintenance note describes the following.",
    "The service team received this message.",
    "An internal ticket records these details.",
    "The workshop inbox received a report.",
    "A routine handover included this note.",
    "The operator asks for a review.",
    "The inspection desk received this request.",
    "The shift coordinator recorded a concern.",
    "A service request includes these facts.",
    "The team copied this observation into a ticket.",
    "The workshop clerk transcribed this report.",
    "A technician added the following account.",
    "The afternoon handover contains this message.",
    "The maintenance inbox lists this observation.",
    "The service desk forwarded this written request.",
    "An operator documented the following concern.",
    "A new workshop note contains these details.",
]
CLOSINGS = [
    "Please retain the original observations.",
    "The note contains no other confirmed details.",
    "This is a request for review only.",
    "No repair has been performed by this note.",
    "The text is an observation, not an instruction to run equipment.",
    "The report remains available for the next shift.",
    "Keep unreported details unspecified.",
    "The service desk can review the record.",
    "This message does not confirm a diagnosis.",
    "The observation is awaiting a technical review.",
    "Only explicitly reported facts should be copied.",
    "The ticket has not authorized any intervention.",
    "Retain uncertainty when details are missing.",
    "No additional findings are supplied here.",
    "The report is ready for manual review.",
    "This entry is a written observation only.",
    "The workshop has supplied no supplementary facts.",
    "The request can be filed for later inspection.",
    "The maintenance team should read the original note.",
    "The text does not provide a cause for the concern.",
]
ORDERS = [
    ["unit", "symptom", "location", "scheduled_date"],
    ["location", "unit", "scheduled_date", "symptom"],
    ["scheduled_date", "symptom", "unit", "location"],
    ["symptom", "location", "unit", "scheduled_date"],
    ["unit", "scheduled_date", "location", "symptom"],
    ["location", "symptom", "scheduled_date", "unit"],
]
MISSING = {
    "unit": "The unit identifier was not provided.",
    "symptom": "The symptom was not specified.",
    "location": "The location was not stated.",
    "scheduled_date": "No inspection date has been scheduled.",
}


def split_scenarios(family):
    return "train" if family < 280 else "validation" if family < 340 else "test"


def normalized(text):
    return " ".join(text.casefold().split())


def render_target(values):
    return json.dumps(
        {key: values[key] for key in FIELDS}, ensure_ascii=False, separators=(",", ":")
    )


def render_fact(key, value, j):
    if value is None:
        return MISSING[key]
    alternatives = {
        "unit": [
            f"The unit is {value}.",
            f"Unit {value} is named in the report.",
            f"The reported unit identifier is {value}.",
            f"This observation concerns unit {value}.",
            f"The unit was identified as {value}.",
            f"The record names unit {value}.",
        ],
        "symptom": [
            f"The reported symptom is {value}.",
            f"The operator reports {value}.",
            f"The symptom is described as {value}.",
            f"The note explicitly reports {value}.",
            f"The observed symptom is {value}.",
            f"The report describes {value}.",
        ],
        "location": [
            f"The location is {value}.",
            f"The issue was reported near the {value}.",
            f"The stated location is the {value}.",
            f"The observation was made at the {value}.",
            f"The report points to the {value}.",
            f"The location named is the {value}.",
        ],
        "scheduled_date": [
            f"The inspection is scheduled for {value}.",
            f"The scheduled inspection date is {value}.",
            f"An inspection has been scheduled on {value}.",
            f"The planned inspection date is {value}.",
            f"The inspection appointment is set for {value}.",
            f"The record gives {value} as the inspection date.",
        ],
    }
    return alternatives[key][j]


def generate_tickets():
    pages = []
    gold = []
    templates = []
    for family in range(400):
        opening = OPENINGS[family // 20]
        closing = CLOSINGS[family % 20]
        templates.append(
            {
                "family": family,
                "split": split_scenarios(family),
                "opening": opening,
                "closing": closing,
            }
        )
        for j in range(6):
            values = {
                "unit": f"U{family+1000}",
                "symptom": SYMPTOMS[family % 4],
                "location": LOCATIONS[(family // 4) % 10],
                "scheduled_date": f"2026-{family%12+1:02d}-{family//12%27+1:02d}",
            }
            if j in [0, 3]:
                values["scheduled_date"] = None
            if j == 4 and family % 3 == 0:
                values["location"] = None
            if j == 5 and family % 4 == 0:
                values["unit"] = None
            if j == 2 and family % 7 == 0:
                values["symptom"] = None
            clauses = [render_fact(key, values[key], j) for key in ORDERS[j]]
            ticket = " ".join([opening, *clauses, closing])
            identifier = f"I{family:03d}-{j}"
            row = {
                "document_id": identifier,
                "family": family,
                "formulation": j,
                "split": split_scenarios(family),
                "ticket": ticket,
                "content_sha256": hashlib.sha256(ticket.encode()).hexdigest(),
                "normalized_sha256": hashlib.sha256(
                    normalized(ticket).encode()
                ).hexdigest(),
            }
            pages.append(row)
            gold.append(
                {
                    "document_id": identifier,
                    "target": values,
                    "target_json": render_target(values),
                    "absent_fields": [key for key in FIELDS if values[key] is None],
                }
            )
            # Every non-null label appears in a rendered clause; absent facts are
            # represented only by their missingness statement, never hidden metadata.
            for key in FIELDS:
                if values[key] is not None:
                    assert str(values[key]) in clauses[ORDERS[j].index(key)]
    assert len({r["normalized_sha256"] for r in pages}) == 2400
    return pages, gold, templates


def prepare(config):
    root = Path("data/generated")
    root.mkdir(parents=True, exist_ok=True)
    if (root / "manifest.json").exists():
        raise ValueError("Existing frozen corpus; extract into a new directory")
    pages, gold, templates = generate_tickets()
    byid = {r["document_id"]: r for r in gold}
    save_json(root / "input/pages.json", pages)
    save_json(root / "input/templates.json", templates)
    save_json(root / "labels/gold.json", gold)
    audit = {}
    for split in ["train", "validation", "test"]:
        selected = [r for r in pages if r["split"] == split]
        labels = [byid[r["document_id"]] for r in selected]
        save_json(root / f"{split}/pages.json", selected)
        if split != "test":
            save_json(root / f"labels/{split}.json", labels)
        absent = sum(bool(r["absent_fields"]) for r in labels)
        assert absent / len(labels) >= 0.25
        audit[split] = {
            "documents": len(selected),
            "families": len({r["family"] for r in selected}),
            "with_absent_field": absent,
            "with_absent_field_rate": absent / len(labels),
        }
    demo_ids = [
        f"I{family:03d}-{j}"
        for family, j in [
            (0, 0),
            (3, 1),
            (8, 2),
            (13, 3),
            (21, 4),
            (34, 5),
            (55, 0),
            (89, 1),
        ]
    ]
    lookup = {r["document_id"]: r for r in pages}
    demonstrations = [
        {
            "document_id": identifier,
            "ticket": lookup[identifier]["ticket"],
            "target_json": byid[identifier]["target_json"],
        }
        for identifier in demo_ids
    ]
    assert all(lookup[i]["split"] == "train" for i in demo_ids)
    save_json(root / "input/demonstrations.json", demonstrations)
    save_json(
        root / "manifest.json",
        {
            "dataset": "InterventionPairs-v1",
            "rights": "CC0-1.0",
            "seed": config["seed"],
            "split_audit": audit,
            "normalized_duplicates": 0,
            "demonstration_ids": demo_ids,
            "files": {str(f.relative_to(root)): sha(f) for f in root.rglob("*.json")},
        },
    )
    save_json(
        "outputs/data-audit.json",
        {"splits": audit, "normalized_duplicates": 0, "demonstration_ids": demo_ids},
    )
    return audit

Mã hóa collator phản hồi

Ranh giới token và EOS

Lỗi thường gặp: Cắt phải bỏ EOS và phản hồi: phát hiện trước học.

Bất biến: Mọi token phản hồi mong đợi có mục tiêu và prompt bị bỏ.

Tệp src/sft_complet/collator.py

import torch
import torch.nn.functional as F

INSTRUCTION = "Extract exactly unit, symptom, location, scheduled_date as a JSON object. Missing information must be null. A reported symptom is noise, leak, heat, or vibration. Return only the JSON object."


def prompt_text(ticket):
    return INSTRUCTION + "\nTicket: " + ticket + "\nJSON:"


def encode_prompt_response(
    tokenizer, ticket, response, config, identifier, supervise_prompt=False
):
    # Explicit concatenation of two separately encoded token streams. We do not
    # pretend it equals re-tokenizing the concatenated character string.
    prompt = tokenizer.encode(prompt_text(ticket), add_special_tokens=False)
    completion = tokenizer.encode(response, add_special_tokens=False) + [151643]
    if not prompt or len(prompt) > config["max_prompt_tokens"]:
        raise ValueError(
            identifier + ": prompt exceeds the declared token budget; no truncation"
        )
    if (
        len(completion) <= 1
        or len(completion) > config["max_response_tokens_including_eos"]
    ):
        raise ValueError(
            identifier + ": missing or overlength response including EOS; no truncation"
        )
    ids = prompt + completion
    labels = list(ids) if supervise_prompt else [-100] * len(prompt) + completion
    return {
        "document_id": identifier,
        "ids": ids,
        "labels": labels,
        "prompt_tokens": len(prompt),
        "response_tokens": len(completion),
        "supervise_prompt": supervise_prompt,
    }


def completion_labels(row):
    return row["labels"]


def collate(rows, device="cuda", pad_to=None):
    if not rows:
        raise ValueError("Empty batch")
    length = max(len(r["ids"]) for r in rows) if pad_to is None else pad_to
    if length < max(len(r["ids"]) for r in rows):
        raise ValueError("Silent truncation prohibited")
    ids = torch.full((len(rows), length), 151643, dtype=torch.long, device=device)
    labels = torch.full_like(ids, -100)
    mask = torch.zeros_like(ids, dtype=torch.bool)
    for i, row in enumerate(rows):
        n = len(row["ids"])
        ids[i, :n] = torch.tensor(row["ids"], device=device)
        labels[i, :n] = torch.tensor(row["labels"], device=device)
        mask[i, :n] = True
    return ids, mask, labels


def completion_nll(logits, labels):
    selected = labels[:, 1:] != -100
    if not selected.any():
        raise ValueError("No supervised next token")
    values = F.cross_entropy(
        logits[:, :-1][selected].float(), labels[:, 1:][selected], reduction="none"
    )
    return values.sum(), int(selected.sum()), values

Đo prompt đơn và chọn trọng số sửa được

Khởi tạo và không gian adaptation

Lỗi thường gặp: Embedding gắn lm_head vô tình mở băng: kiểm tra định danh storage và danh sách optimizer.

Bất biến: Hai block cuối được nhận diện và không tham số partial khác ở optimizer.

Tệp src/sft_complet/baseline.py

from .model import load_base, select_trainables, parameter_manifest, manifest_digest
from .config import save_json


def report_trainable_parameters():
    model = load_base()
    result = {}
    for branch in ["full", "partial"]:
        parameters = select_trainables(model, branch)
        rows = parameter_manifest(model)
        result[branch] = {
            "parameters": rows,
            "total_parameters": sum(r["numel"] for r in rows),
            "trainable_parameters": sum(r["numel"] for r in rows if r["requires_grad"]),
            "optimizer_parameter_objects": len(parameters),
            "unique_optimizer_parameter_objects": len({id(p) for p in parameters}),
            "model_digest": manifest_digest(rows),
            "tied_embedding_output": model.lm_head.weight
            is model.model.embed_tokens.weight,
        }
    if result["full"]["model_digest"] != result["partial"]["model_digest"]:
        raise ValueError("Selecting trainable parameters changed weights")
    save_json("outputs/parameter-audit.json", result)
    import shutil

    shutil.copyfile("assets/model/tokenizer.json", "outputs/tokenizer-reference.json")


def select_last_blocks(model):
    return select_trainables(model, "partial")


def evaluate_prompt_only(config, variant):
    from .evaluate import predict

    if variant not in ["zeroshot", "fewshot"]:
        raise ValueError("Expected frozen-base baseline")
    predict(config, variant)

Thực hiện SFT và tiếp tục

CE có điều kiện và trạng thái tối ưu

Lỗi thường gặp: Tích lũy trung bình phản hồi nhỏ cho gradient sai: cộng NLL và bộ đếm.

Bất biến: Trọng số full và partial dự kiến đổi; checkpoint tách, sampler và RNG khôi phục.

Tệp src/sft_complet/train.py

import copy, hashlib, math, os, time
from pathlib import Path
import numpy as np
import torch
from .config import read_json, save_json, sha, seed_all, rng_state, restore_rng
from .model import (
    load_base,
    load_tokenizer,
    select_trainables,
    parameter_manifest,
    manifest_digest,
    save_learned,
    active_response_logits,
)
from .collator import encode_prompt_response, collate


def equal(a, b):
    if isinstance(a, torch.Tensor):
        return isinstance(b, torch.Tensor) and torch.equal(a, b)
    if isinstance(a, np.ndarray):
        return isinstance(b, np.ndarray) and np.array_equal(a, b)
    if isinstance(a, dict):
        return (
            isinstance(b, dict)
            and a.keys() == b.keys()
            and all(equal(a[k], b[k]) for k in a)
        )
    if isinstance(a, (list, tuple)):
        return (
            type(a) == type(b)
            and len(a) == len(b)
            and all(equal(x, y) for x, y in zip(a, b))
        )
    return a == b


def save_resume_state(path, state):
    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.exists():
        raise ValueError("Existing training state is preserved")
    temporary = path.with_suffix(".pending")
    torch.save(state, temporary)
    os.replace(temporary, path)


def encode_split(config, tokenizer, split, supervise_prompt=False):
    pages = read_json(f"data/generated/{split}/pages.json")
    labels = {
        r["document_id"]: r for r in read_json(f"data/generated/labels/{split}.json")
    }
    if any(p["split"] != split for p in pages):
        raise ValueError("Wrong training/validation partition")
    return [
        encode_prompt_response(
            tokenizer,
            p["ticket"],
            labels[p["document_id"]]["target_json"],
            config,
            p["document_id"],
            supervise_prompt,
        )
        for p in pages
    ]


def learning_rate(config, branch, step, total):
    peak = (
        config["learning_rate_partial"]
        if branch == "partial"
        else config["learning_rate_full"]
    )
    warmup = config["warmup_updates"]
    if step < warmup:
        return peak * (step + 1) / warmup
    return peak * (
        1
        - min(1.0, (step - warmup) / max(1, total - warmup - 1))
        * (1 - config["minimum_lr_ratio"])
    )


@torch.no_grad()
def validate_response(model, rows):
    model.eval()
    total = 0.0
    count = 0
    for row in rows:
        ids, mask, labels = collate([row])
        _, n, values, _, _ = active_response_logits(model, ids, mask, labels)
        total += float(values.double().sum())
        count += n
    model.train()
    return {
        "response_nll_nats_per_token": total / count,
        "response_tokens": count,
        "documents": len(rows),
    }


def train_sft(config, branch="full", mode="continuous"):
    if (
        Path("data/generated/test").exists()
        or Path("data/generated/labels/gold.json").exists()
    ):
        raise ValueError("Learning service must not mount test inputs or gold")
    if mode not in ["interrupt", "continuous", "resume"]:
        raise ValueError("Unknown training mode")
    seed_all(config["seed"])
    tokenizer = load_tokenizer()
    rows = encode_split(config, tokenizer, "train", branch == "prompt_loss")
    validation = encode_split(config, tokenizer, "validation", False)
    if len(rows) != 1680 or len(validation) != 360:
        raise ValueError("Expected frozen1680/360 population")
    model = load_base()
    parameters = select_trainables(model, branch)
    initial = parameter_manifest(model)
    initial_digest = manifest_digest(initial)
    model = model.cuda()
    parameters = [p for p in model.parameters() if p.requires_grad]
    optimizer = torch.optim.AdamW(
        parameters, lr=config["learning_rate_full"], weight_decay=config["weight_decay"]
    )
    sampler = torch.Generator().manual_seed(config["seed"] + 100)
    updates_per_epoch = math.ceil(len(rows) / config["accumulation_examples"])
    total_updates = updates_per_epoch * config["epochs"]
    start = updates = 0
    history = []
    validation_history = []
    identity = {
        "branch": branch,
        "config": config,
        "base_sha256": sha("assets/model/model.safetensors"),
        "tokenizer_sha256": sha("assets/model/tokenizer.json"),
        "model_config_sha256": sha("assets/model/config.json"),
        "model_lock_sha256": sha("locks/models.lock.json"),
        "implementation_sha256": {
            name: sha("src/sft_complet/" + name) for name in ["model.py", "train.py"]
        },
        "train_input_sha256": sha("data/generated/train/pages.json"),
        "train_labels_sha256": sha("data/generated/labels/train.json"),
        "validation_input_sha256": sha("data/generated/validation/pages.json"),
        "validation_labels_sha256": sha("data/generated/labels/validation.json"),
        "collator_sha256": sha("src/sft_complet/collator.py"),
        "initial_model_digest": initial_digest,
        "trainable_names": [r["name"] for r in initial if r["requires_grad"]],
    }
    root = Path("assets/checkpoints")
    destination = root / f"{branch}-{mode}.pt"
    if destination.exists():
        raise ValueError("Preserve existing checkpoint")
    if mode == "resume":
        state = torch.load(
            root / f"{branch}-interrupt.pt", map_location="cpu", weights_only=False
        )
        if state["identity"] != identity:
            raise ValueError("Resume identity changed")
        model.load_state_dict(state["model"], strict=True)
        optimizer.load_state_dict(state["optimizer"])
        sampler.set_state(state["sampler"])
        restore_rng(state["rng"])
        start = state["epoch"]
        updates = state["updates"]
        history = state["history"]
        validation_history = state["validation_history"]
        if state["scheduler"] != {
            "next_update": updates,
            "next_lr": learning_rate(config, branch, updates, total_updates),
        }:
            raise ValueError("Resume scheduler changed")
        del state
    stop = config["interrupt_epoch"] if mode == "interrupt" else config["epochs"]
    begun = time.perf_counter()
    torch.cuda.reset_peak_memory_stats()
    model.train()
    for epoch in range(start, stop):
        permutation = torch.randperm(len(rows), generator=sampler).tolist()
        epoch_loss = 0.0
        epoch_tokens = 0
        for offset in range(0, len(rows), config["accumulation_examples"]):
            selected = permutation[offset : offset + config["accumulation_examples"]]
            denominator = sum(
                sum(v != -100 for v in rows[i]["labels"][1:]) for i in selected
            )
            lr = learning_rate(config, branch, updates, total_updates)
            for group in optimizer.param_groups:
                group["lr"] = lr
            optimizer.zero_grad(set_to_none=True)
            summed = 0.0
            for i in selected:
                ids, mask, labels = collate([rows[i]])
                loss_sum, n, values, _, _ = active_response_logits(
                    model, ids, mask, labels
                )
                if not torch.isfinite(loss_sum) or n != sum(
                    v != -100 for v in rows[i]["labels"][1:]
                ):
                    raise ValueError("Nonfinite or miscounted active-token loss")
                (loss_sum / denominator).backward()
                summed += float(values.detach().double().sum())
            norm = torch.nn.utils.clip_grad_norm_(
                parameters, config["gradient_clip"], error_if_nonfinite=True
            )
            optimizer.step()
            updates += 1
            epoch_loss += summed
            epoch_tokens += denominator
            history.append(
                {
                    "update": updates,
                    "epoch": epoch + 1,
                    "document_ids": [rows[i]["document_id"] for i in selected],
                    "active_tokens": denominator,
                    "nll_nats_per_active_token": summed / denominator,
                    "learning_rate": lr,
                    "gradient_norm_before_clip": float(norm),
                }
            )
            if updates % 42 == 0:
                print(history[-1], flush=True)
        observed = validate_response(model, validation)
        validation_history.append({"epoch": epoch + 1, **observed})
        print(
            {
                "epoch": epoch + 1,
                "train_nll_nats_per_active_token": epoch_loss / epoch_tokens,
                "train_active_tokens": epoch_tokens,
                "validation": observed,
            },
            flush=True,
        )
    torch.cuda.synchronize()
    final = parameter_manifest(model)
    before = {r["name"]: r for r in initial}
    changed = [r["name"] for r in final if r["sha256"] != before[r["name"]]["sha256"]]
    frozen_changed = [
        r["name"]
        for r in final
        if not before[r["name"]]["requires_grad"]
        and r["sha256"] != before[r["name"]]["sha256"]
    ]
    if not changed or frozen_changed:
        raise ValueError("No learning or frozen parameters changed")
    state = {
        "identity": identity,
        "epoch": stop,
        "updates": updates,
        "model": model.state_dict(),
        "optimizer": optimizer.state_dict(),
        "sampler": sampler.get_state(),
        "scheduler": {
            "next_update": updates,
            "next_lr": learning_rate(config, branch, updates, total_updates),
        },
        "rng": rng_state(),
        "history": history,
        "validation_history": validation_history,
    }
    save_resume_state(destination, state)
    if mode == "resume" or (branch == "prompt_loss" and mode == "continuous"):
        save_learned(model, root / f"{branch}-{mode}.safetensors", identity)
    save_json(
        f"outputs/train-{branch}-{mode}.json",
        {
            "branch": branch,
            "mode": mode,
            "identity": identity,
            "epochs": stop,
            "updates": updates,
            "history": history,
            "validation_history": validation_history,
            "tokenization": rows,
            "initial_parameters": initial,
            "final_parameters": final,
            "changed_parameters": changed,
            "frozen_changed": frozen_changed,
            "trainable_parameters": sum(
                r["numel"] for r in initial if r["requires_grad"]
            ),
            "total_parameters": sum(r["numel"] for r in initial),
            "initial_model_digest": initial_digest,
            "final_model_digest": manifest_digest(final),
            "checkpoint_sha256": sha(destination),
            "checkpoint_bytes": destination.stat().st_size,
            "max_torch_allocated_bytes": torch.cuda.max_memory_allocated(),
            "seconds": time.perf_counter() - begun,
            "test_visible": False,
        },
    )


def verify_resume(branch):
    root = Path("assets/checkpoints")
    a = torch.load(
        root / f"{branch}-continuous.pt", map_location="cpu", weights_only=False
    )
    b = torch.load(root / f"{branch}-resume.pt", map_location="cpu", weights_only=False)
    checks = {k: equal(a[k], b[k]) for k in a}
    if not all(checks.values()):
        raise ValueError("Full resume differs: " + str(checks))
    save_json(
        f"outputs/resume-{branch}.json",
        {
            "status": "PASS",
            "all_state_exact": True,
            "checks": checks,
            "boundary": "complete epoch; not a mid-microbatch recovery claim",
        },
    )

Đánh giá không ràng buộc decoding

Định dạng và ngữ nghĩa tách biệt

Lỗi thường gặp: Parser khoan dung điền trường thiếu: cấm làm giàu im lặng.

Bất biến: 360 ID mỗi nhánh, không bỏ JSON lỗi.

Tệp src/sft_complet/evaluate.py

import json, math, time
from pathlib import Path
import torch
from transformers import GenerationConfig
from .config import read_json, save_json, sha, seed_all
from .model import (
    load_base,
    load_tokenizer,
    reload_learned,
    active_response_logits,
    parameter_manifest,
    manifest_digest,
)
from .collator import prompt_text, encode_prompt_response, collate
from .data import FIELDS, SYMPTOMS


def parse_generated_record(text, finish_reason):
    def pairs(items):
        result = {}
        for key, value in items:
            if key in result:
                raise ValueError("Duplicate key")
            result[key] = value
        return result

    def constant(value):
        raise ValueError("Nonfinite JSON constant")

    try:
        record = json.loads(text, object_pairs_hook=pairs, parse_constant=constant)
    except (ValueError, TypeError):
        return {
            "json_parsable": False,
            "schema_valid": False,
            "completion_valid": False,
            "record": None,
            "reason": "TRUNCATED" if finish_reason != "eos" else "INVALID_JSON",
        }
    valid = (
        isinstance(record, dict)
        and set(record) == set(FIELDS)
        and all(v is None or type(v) == str for v in record.values())
    )
    if valid:
        valid = record["symptom"] is None or record["symptom"] in SYMPTOMS
    return {
        "json_parsable": True,
        "schema_valid": valid,
        "completion_valid": valid and finish_reason == "eos",
        "record": record if valid else None,
        "reason": (
            "TRUNCATED"
            if finish_reason != "eos"
            else "VALID_SCHEMA" if valid else "INVALID_SCHEMA"
        ),
    }


def generation_prompt(tokenizer, ticket, demonstrations, config):
    text = "\n\n".join(
        prompt_text(d["ticket"]) + d["target_json"] for d in demonstrations
    )
    if demonstrations:
        text += "\n\n"
    text += prompt_text(ticket)
    ids = tokenizer.encode(text, add_special_tokens=False)
    limit = (
        config["max_fewshot_prompt_tokens"]
        if demonstrations
        else config["max_prompt_tokens"]
    )
    if len(ids) > limit:
        raise ValueError("Generation prompt exceeds declared token cap; no truncation")
    return text, ids


@torch.no_grad()
def predict(config, variant):
    if Path("data/generated/labels").exists():
        raise ValueError("Free generation must not mount gold labels")
    seed_all(config["seed"])
    tokenizer = load_tokenizer()
    learned = variant in ["full", "partial", "prompt_loss"]
    path = Path("assets/checkpoints") / (
        variant
        + (
            "-continuous.safetensors"
            if variant == "prompt_loss"
            else "-resume.safetensors"
        )
    )
    if learned:
        model, metadata = reload_learned(path)
        identity = metadata["model_digest"]
    else:
        model = load_base()
        identity = manifest_digest(parameter_manifest(model))
    model = model.cuda().eval()
    pages = read_json("data/generated/input/pages.json")
    selected = [r for r in pages if r["split"] in ["validation", "test"]]
    demonstrations = (
        read_json("data/generated/input/demonstrations.json")
        if variant == "fewshot"
        else []
    )
    if len(selected) != 720 or (variant == "fewshot" and len(demonstrations) != 8):
        raise ValueError("Expected fixed720heldout cases and8train demos")
    generation = GenerationConfig(
        do_sample=False,
        num_beams=1,
        max_new_tokens=config["max_new_tokens"],
        use_cache=True,
        eos_token_id=151643,
        pad_token_id=151643,
        bos_token_id=151643,
    )
    rows = []
    begun = time.perf_counter()
    torch.cuda.reset_peak_memory_stats()
    for index, page in enumerate(selected):
        text, ids = generation_prompt(tokenizer, page["ticket"], demonstrations, config)
        input_ids = torch.tensor([ids], device="cuda")
        mask = torch.ones_like(input_ids)
        started = time.perf_counter()
        output = (
            model.generate(
                input_ids=input_ids, attention_mask=mask, generation_config=generation
            )[0, len(ids) :]
            .cpu()
            .tolist()
        )
        torch.cuda.synchronize()
        finish = "eos" if output and output[-1] == 151643 else "length"
        content = output[:-1] if finish == "eos" else output
        decoded = tokenizer.decode(
            content, skip_special_tokens=False, clean_up_tokenization_spaces=False
        )
        parsed = parse_generated_record(decoded, finish)
        rows.append(
            {
                "document_id": page["document_id"],
                "split": page["split"],
                "variant": variant,
                "input_ids": ids,
                "input_text": text,
                "output_ids": output,
                "text": decoded,
                "finish_reason": finish,
                "parsed": parsed,
                "input_tokens": len(ids),
                "output_tokens": len(output),
                "seconds": time.perf_counter() - started,
            }
        )
        if (index + 1) % 60 == 0:
            print(
                {"variant": variant, "generated": index + 1, "total": len(selected)},
                flush=True,
            )
    save_json(
        f"outputs/predictions-{variant}.json",
        {
            "variant": variant,
            "model_digest": identity,
            "model_file_sha256": (
                sha(path) if learned else sha("assets/model/model.safetensors")
            ),
            "tokenizer_sha256": sha("assets/model/tokenizer.json"),
            "input_sha256": sha("data/generated/input/pages.json"),
            "demonstration_ids": [d["document_id"] for d in demonstrations],
            "generation": generation.to_dict(),
            "rows": rows,
            "seconds": time.perf_counter() - begun,
            "max_torch_allocated_bytes": torch.cuda.max_memory_allocated(),
            "gold_visible": False,
        },
    )


def score_missing_fields(rows, gold):
    n = len(rows)
    correct = {k: 0 for k in FIELDS}
    absent = {k: 0 for k in FIELDS}
    invented = {k: 0 for k in FIELDS}
    null_correct = {k: 0 for k in FIELDS}
    invalid_absent = {k: 0 for k in FIELDS}
    exact = parsable = schema = truncated = 0
    details = []
    for row in rows:
        expected = gold[row["document_id"]]["target"]
        parsed = parse_generated_record(row["text"], row["finish_reason"])
        assert parsed == row["parsed"]
        valid = parsed["completion_valid"]
        record = parsed["record"]
        fields = {key: valid and record[key] == expected[key] for key in FIELDS}
        row_exact = all(fields.values())
        exact += row_exact
        parsable += parsed["json_parsable"]
        schema += parsed["schema_valid"]
        truncated += row["finish_reason"] == "length"
        for key in FIELDS:
            correct[key] += fields[key]
            if expected[key] is None:
                absent[key] += 1
                null_correct[key] += valid and record[key] is None
                invented[key] += valid and record[key] is not None
                invalid_absent[key] += not valid
        details.append(
            {
                "document_id": row["document_id"],
                "fields_correct": fields,
                "exact_match": row_exact,
                "absent_fields": [k for k in FIELDS if expected[k] is None],
            }
        )
    return {
        "documents": n,
        "json_parsable": parsable,
        "json_parsable_rate": parsable / n,
        "schema_valid": schema,
        "schema_valid_rate": schema / n,
        "exact_match": exact,
        "exact_match_rate": exact / n,
        "field_correct": correct,
        "field_accuracy": {k: v / n for k, v in correct.items()},
        "absent_opportunities": absent,
        "correct_null": null_correct,
        "invented_nonnull_in_valid_record": invented,
        "invalid_record_with_absent_field": invalid_absent,
        "invented_rate_over_all_absent_opportunities": {
            k: invented[k] / absent[k] if absent[k] else None for k in FIELDS
        },
        "null_failure_rate_over_all_absent_opportunities": {
            k: (absent[k] - null_correct[k]) / absent[k] if absent[k] else None
            for k in FIELDS
        },
        "truncated": truncated,
        "rows": details,
    }


def evaluate_branches(variants):
    gold = {r["document_id"]: r for r in read_json("data/generated/labels/gold.json")}
    result = {}
    for variant in variants:
        predictions = read_json(f"outputs/predictions-{variant}.json")
        result[variant] = {}
        for split in ["validation", "test"]:
            rows = [r for r in predictions["rows"] if r["split"] == split]
            if len(rows) != 360 or len({r["document_id"] for r in rows}) != 360:
                raise ValueError("Do not omit invalid heldout outputs")
            result[variant][split] = score_missing_fields(rows, gold)
    name = (
        "exercise-evaluation.json" if variants == ["prompt_loss"] else "evaluation.json"
    )
    save_json(
        "outputs/" + name,
        {
            "results": result,
            "quality_gain_required": False,
            "decoding_constraints": False,
            "invalid_outputs_discarded": False,
        },
    )
    print(
        {
            v: {k: value for k, value in splits["test"].items() if k != "rows"}
            for v, splits in result.items()
        },
        flush=True,
    )


@torch.no_grad()
def response_nll(config, variant):
    tokenizer = load_tokenizer()
    learned = variant in ["full", "partial", "prompt_loss"]
    path = Path("assets/checkpoints") / (
        variant
        + (
            "-continuous.safetensors"
            if variant == "prompt_loss"
            else "-resume.safetensors"
        )
    )
    model = reload_learned(path)[0] if learned else load_base()
    model = model.cuda().eval()
    pages = [
        r for r in read_json("data/generated/input/pages.json") if r["split"] == "test"
    ]
    gold = {r["document_id"]: r for r in read_json("data/generated/labels/gold.json")}
    records = []
    probes = []
    # Always the same zero-shot response-only conditioning contract, including
    # the prompt-loss ablation. Few-shot cost/quality is evaluated separately.
    for index, page in enumerate(pages):
        row = encode_prompt_response(
            tokenizer,
            page["ticket"],
            gold[page["document_id"]]["target_json"],
            config,
            page["document_id"],
            False,
        )
        ids, mask, labels = collate([row])
        _, n, losses, logits, targets = active_response_logits(model, ids, mask, labels)
        records.append(
            {
                "document_id": page["document_id"],
                "input_ids": row["ids"],
                "prompt_tokens": row["prompt_tokens"],
                "active_response_tokens": n,
                "response_targets": targets.cpu().tolist(),
                "response_nll": losses.cpu().tolist(),
            }
        )
        if index == 0:
            for pos in [0, n - 1]:
                probes.append(
                    {
                        "document_id": page["document_id"],
                        "response_position": pos,
                        "target": int(targets[pos]),
                        "logits": logits[pos].cpu().tolist(),
                        "nll": float(losses[pos]),
                    }
                )
    total = math.fsum(v for row in records for v in row["response_nll"])
    count = sum(r["active_response_tokens"] for r in records)
    save_json(
        f"outputs/response-nll-{variant}.json",
        {
            "variant": variant,
            "conditioning": "zero-shot completion-only for every compared weight state",
            "documents": len(records),
            "response_tokens": count,
            "response_nll_nats_per_token": total / count,
            "rows": records,
            "probes": probes,
        },
    )

Sửa mask để hiểu tín hiệu

Ablation giám sát

Lỗi thường gặp: Dùng lại kết quả full như ablation: huấn luyện nhánh mới và giữ định danh.

Bất biến: Bảng token hoạt động, chi phí và trường vắng cho mỗi mask.

Tệp src/sft_complet/exercise.py

from .config import read_json, save_json, sha


def preserve_main():
    paths = [
        "outputs/evaluation.json",
        "outputs/response-nll-full.json",
        "outputs/response-nll-partial.json",
        "outputs/resume-full.json",
        "outputs/resume-partial.json",
        "assets/checkpoints/full-resume.safetensors",
        "assets/checkpoints/partial-resume.safetensors",
    ]
    save_json("outputs/exercise-before.json", {name: sha(name) for name in paths})


def compare_response_losses():
    before = read_json("outputs/exercise-before.json")
    if any(sha(name) != digest for name, digest in before.items()):
        raise ValueError("Exercise changed main artifacts")
    main = read_json("outputs/evaluation.json")
    exercise = read_json("outputs/exercise-evaluation.json")
    rows = {}
    for branch in ["full", "partial", "prompt_loss"]:
        training = read_json(
            f"outputs/train-{branch}-"
            + ("continuous" if branch == "prompt_loss" else "resume")
            + ".json"
        )
        nll = read_json(f"outputs/response-nll-{branch}.json")
        metrics = (exercise if branch == "prompt_loss" else main)["results"][branch][
            "test"
        ]
        rows[branch] = {
            "training_active_tokens": sum(
                r["active_tokens"] for r in training["history"]
            ),
            "updates": training["updates"],
            "trainable_parameters": training["trainable_parameters"],
            "test_response_tokens": nll["response_tokens"],
            "test_response_nll_nats_per_token": nll["response_nll_nats_per_token"],
            "test_metrics": {k: v for k, v in metrics.items() if k != "rows"},
            "model_digest": training["final_model_digest"],
            "initial_model_digest": training["initial_model_digest"],
        }
    if (
        len({r["test_response_tokens"] for r in rows.values()}) != 1
        or len({r["initial_model_digest"] for r in rows.values()}) != 1
    ):
        raise ValueError("Ablation initial state or evaluation population changed")
    if (
        rows["prompt_loss"]["training_active_tokens"]
        <= rows["full"]["training_active_tokens"]
    ):
        raise ValueError("Prompt-loss branch must actually supervise additional tokens")
    save_json(
        "outputs/exercise/loss-masks.json",
        {
            "rows": rows,
            "protected_sha256": before,
            "main_artifacts_unchanged": True,
            "fresh_prompt_loss_training": True,
            "quality_gain_required": False,
            "training_denominators_differ": True,
        },
    )

Nguồn

Base Qwen2.5-0.5B, Qwen2 Transformers 4.56.2 và tensor chia sẻ Safetensors 0.6.2 được tham khảo để kiểm cơ chế. Mô hình là base tiền huấn luyện, không Instruct. Version chạy và hash có trong locks và requirements.lock.txt.

Diễn giải kết quả thực sự thu được

Khi phát triển, full và partial đều đạt 360 phản hồi đúng trên 360 ticket test. Base không ví dụ đạt 145; context tám demonstration không đạt phản hồi nào theo hợp đồng hoàn chỉnh. 360 đầu ra test đều chạm trần. Ví dụ thô bắt đầu JSON rồi tiếp tục instruction và ticket khác: base tiếp tục chuỗi demonstration. Chúng tôi không sửa bằng chỉ giữ object đầu. Kết luận này thuộc base và serialization này, không phải quy tắc chung chống few-shot.

Kho đều làm hai adaptation đủ. So sánh full/partial có hai learning rate khác, nên không cô lập chỉ số tham số. Thời gian học+validation liên tục là 372,91s full và 152,13s partial, đỉnh PyTorch 9 964 563 968 và 2 611 971 584 byte. Chúng phụ thuộc máy và tải; không gồm acquisition, generation, lặp tiếp tục, serialization cuối, cũng không là toàn VRAM Docker.

Bài tập giám sát prompt cũng đạt 360/360. Nó kích hoạt 430238 token so 105052 loss phản hồi đơn cho cùng 420 update. NLL phản hồi test là 1,06265e−5, so 6,89464e−7 full. Không có suy giảm đáp án cuối quan sát; điều đó không xóa khác biệt tín hiệu và mẫu số. Giá trị chi tiết và giới hạn được nêu dưới đây; luồng đọc tính lại toàn bộ.

Quan sát trong quá trình phát triển

Các giá trị được tính lại từ đầu ra thô của lượt chạy này. Chúng không phải ngưỡng cải thiện; lượt chạy từ ZIP sẽ tính lại chúng trên cùng các tập dữ liệu.

{
  "status": "PASS",
  "documents": 2400,
  "test_documents": 360,
  "parameters": 494032768,
  "partial_trainable_parameters": 29824768,
  "metrics": {
    "zeroshot": {
      "documents": 360,
      "json_parsable": 329,
      "json_parsable_rate": 0.9138888888888889,
      "schema_valid": 167,
      "schema_valid_rate": 0.4638888888888889,
      "exact_match": 145,
      "exact_match_rate": 0.4027777777777778,
      "field_correct": {
        "unit": 167,
        "symptom": 161,
        "location": 160,
        "scheduled_date": 158
      },
      "field_accuracy": {
        "unit": 0.4638888888888889,
        "symptom": 0.44722222222222224,
        "location": 0.4444444444444444,
        "scheduled_date": 0.4388888888888889
      },
      "absent_opportunities": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "correct_null": {
        "unit": 0,
        "symptom": 0,
        "location": 6,
        "scheduled_date": 71
      },
      "invented_nonnull_in_valid_record": {
        "unit": 0,
        "symptom": 6,
        "location": 7,
        "scheduled_date": 9
      },
      "invalid_record_with_absent_field": {
        "unit": 15,
        "symptom": 3,
        "location": 7,
        "scheduled_date": 40
      },
      "invented_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.6666666666666666,
        "location": 0.35,
        "scheduled_date": 0.075
      },
      "null_failure_rate_over_all_absent_opportunities": {
        "unit": 1.0,
        "symptom": 1.0,
        "location": 0.7,
        "scheduled_date": 0.4083333333333333
      },
      "truncated": 31
    },
    "fewshot": {
      "documents": 360,
      "json_parsable": 0,
      "json_parsable_rate": 0.0,
      "schema_valid": 0,
      "schema_valid_rate": 0.0,
      "exact_match": 0,
      "exact_match_rate": 0.0,
      "field_correct": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "field_accuracy": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "absent_opportunities": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "correct_null": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invented_nonnull_in_valid_record": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invalid_record_with_absent_field": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "invented_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "null_failure_rate_over_all_absent_opportunities": {
        "unit": 1.0,
        "symptom": 1.0,
        "location": 1.0,
        "scheduled_date": 1.0
      },
      "truncated": 360
    },
    "full": {
      "documents": 360,
      "json_parsable": 360,
      "json_parsable_rate": 1.0,
      "schema_valid": 360,
      "schema_valid_rate": 1.0,
      "exact_match": 360,
      "exact_match_rate": 1.0,
      "field_correct": {
        "unit": 360,
        "symptom": 360,
        "location": 360,
        "scheduled_date": 360
      },
      "field_accuracy": {
        "unit": 1.0,
        "symptom": 1.0,
        "location": 1.0,
        "scheduled_date": 1.0
      },
      "absent_opportunities": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "correct_null": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "invented_nonnull_in_valid_record": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invalid_record_with_absent_field": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invented_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "null_failure_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "truncated": 0
    },
    "partial": {
      "documents": 360,
      "json_parsable": 360,
      "json_parsable_rate": 1.0,
      "schema_valid": 360,
      "schema_valid_rate": 1.0,
      "exact_match": 360,
      "exact_match_rate": 1.0,
      "field_correct": {
        "unit": 360,
        "symptom": 360,
        "location": 360,
        "scheduled_date": 360
      },
      "field_accuracy": {
        "unit": 1.0,
        "symptom": 1.0,
        "location": 1.0,
        "scheduled_date": 1.0
      },
      "absent_opportunities": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "correct_null": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "invented_nonnull_in_valid_record": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invalid_record_with_absent_field": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invented_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "null_failure_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "truncated": 0
    },
    "prompt_loss": {
      "documents": 360,
      "json_parsable": 360,
      "json_parsable_rate": 1.0,
      "schema_valid": 360,
      "schema_valid_rate": 1.0,
      "exact_match": 360,
      "exact_match_rate": 1.0,
      "field_correct": {
        "unit": 360,
        "symptom": 360,
        "location": 360,
        "scheduled_date": 360
      },
      "field_accuracy": {
        "unit": 1.0,
        "symptom": 1.0,
        "location": 1.0,
        "scheduled_date": 1.0
      },
      "absent_opportunities": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "correct_null": {
        "unit": 15,
        "symptom": 9,
        "location": 20,
        "scheduled_date": 120
      },
      "invented_nonnull_in_valid_record": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invalid_record_with_absent_field": {
        "unit": 0,
        "symptom": 0,
        "location": 0,
        "scheduled_date": 0
      },
      "invented_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "null_failure_rate_over_all_absent_opportunities": {
        "unit": 0.0,
        "symptom": 0.0,
        "location": 0.0,
        "scheduled_date": 0.0
      },
      "truncated": 0
    }
  },
  "response_nll": {
    "zeroshot": 0.3579458403824175,
    "full": 6.894639002962341e-07,
    "partial": 1.5182218123592618e-05,
    "prompt_loss": 1.0626541892029729e-05
  },
  "quality_gain_required": false,
  "scope": "Data-only tokenizer decoding, label formulas, per-tensor hashes, exact active-token budgets, resume evidence, full-vocabulary CE probes and all-case metrics; not BPE re-encoding or scalar reimplementation of the 0.5B Transformer"
}

Các kết quả quan sát được, phạm vi của chúng và thí nghiệm được đề xuất.

Chia sẻ bài viết