Một trạm thị giác giả định nhận các bóng hình chi tiết và phải dự đoán họ hình học cùng diện tích hiển thị đã chuẩn hóa của chúng. Chúng ta muốn dùng chung một bộ mã hóa, đồng thời khảo sát rõ tác động của hai mục tiêu và đơn vị đo của chúng.
Với một ảnh, hãy hiển thị lớp và diện tích dự đoán, rồi theo dõi trên một ví dụ nhỏ tích vô hướng của hai gradient và phép chiếu của chúng. So sánh mạng dùng chung, các mạng riêng biệt và biến thể PCGrad.
Tải dự án hoàn chỉnh, bản sửa đổi r001
Hai mục tiêu từ cùng một ảnh
PiecesDeuxTaches-v1 có 300 họ bóng hình, 100 cho mỗi lớp hình chữ nhật, elip và tam giác, với 20 góc nhìn cho mỗi họ. Một phép chia phân tầng với hạt giống 17 đưa 70/15/15 họ của mỗi lớp vào train/validation/test: 4200/900/900 ảnh. Các góc nhìn của một họ có hình học gần nhau. Tịnh tiến, góc, tỉ lệ, ánh sáng và nhiễu thay đổi; không góc nhìn lân cận nào đi qua ranh giới phân vùng.
Ảnh là các ma trận uint8 64×64, được chia cho 255 trước khi vào CNN. Tham số renderer, lớp và mặt nạ nhị phân chỉ là nhãn kiểm toán, không bao giờ là feature. Diện tích là số pixel chính xác của mặt nạ chia cho 4096. Nó không biểu thị mm². Renderer từ chối bóng hình rỗng hoặc bị cắt ở mép. Dải diện tích của ba lớp chồng lấn nhau: chỉ kích thước không được dùng để xác định lớp.
Trung bình và độ lệch chuẩn tổng thể của diện tích được tính trên 4200 ảnh train ở FP64, lưu lại rồi cố định. Mục tiêu hồi quy là (diện tích−trung_bình_train)/độ_lệch_chuẩn_train. Đánh giá áp dụng phép biến đổi ngược. Tính các thống kê này với tập test sẽ thay đổi giao thức.
Chia sẻ tham số, giữ hai đầu ra
Thân mạng ban đầu có ba phép tích chập 3×3, stride 2 và padding 1, với 32/64/96 kênh và ReLU. Kích thước không gian đi từ 64 xuống 32, 16 rồi 8. Global average pooling cho 96 giá trị. Đầu lớp là Linear(96,3), đầu diện tích là Linear(96,1). Không có dropout, batch normalization hay sigmoid trên diện tích.
Thân mạng có 74 208 tham số; các đầu có 291 và 97, tức 74 596 tham số duy nhất cho mạng dùng chung. Hai mạng riêng có tổng 148 804 tham số: một thân và đầu lớp, rồi một thân khác cùng đầu diện tích. Các bản sao có cùng giá trị khởi tạo với nhánh dùng chung, nhưng vùng lưu trữ riêng. Chi phí kết hợp của chúng được đo. Cùng số batch không có nghĩa là cùng FLOPs.
Loss lớp là entropy chéo trên ba logits. Diện tích chuẩn hóa dùng Huber delta=1: sai số bình phương gần không, tăng tuyến tính khi xa hơn. Hai mục tiêu này có đơn vị và độ cong khác nhau. Đối chứng dùng chung tối ưu (L_lớp+L_diện_tích)/2. Hai mô hình riêng mỗi mô hình nhận loss riêng chia hai để giữ quy ước về thang đo.
PCGrad chiếu gì
Autograd tính riêng g_lớp và g_diện_tích trên danh sách có thứ tự các tham số thân mạng. Flatten nối các tọa độ theo thứ tự đó; gradient thiếu là lỗi đối với tham số được khai báo dùng chung. Tùy chọn rõ ràng allow_unused cho phép số không cho các mục đích khác, nhưng TP không bật nó để che một đầu bị ngắt kết nối.
Tích vô hướng âm nghĩa là hai hướng cục bộ đối lập. Với mỗi nhiệm vụ i, ta bắt đầu bằng bản sao g_i_proj rồi đi qua các nhiệm vụ j khác theo thứ tự ngẫu nhiên đã lưu. Nếu g_i_proj·g_j_original<0, ta trừ [g_i_proj·g_j_original / ||g_j_original||²] g_j_original. Tham chiếu vẫn là gradient gốc. Chuẩn đúng bằng không được bỏ qua; không đưa vào phép chia cho không hay epsilon ẩn.
Gradient gán cho thân mạng là trung bình của hai vector đã chiếu. Các đầu nhận gradient thông thường riêng của mình chia hai. Không chiếu giả tạo một đầu chống lại gradient không tồn tại của nhiệm vụ kia. Đối chứng weighted cũng tính hai gradient để chẩn đoán, rồi lấy trung bình; một kiểm thử so sánh thao tác này với backward của trung bình hai loss.
Ví dụ g1=(1,0), g2=(−1,1) cho g1_proj=(0,5;0,5), g2_proj=(0;1), trung bình=(0,25;0,75). Các vector này được dựng để giải thích phép tính. Chúng không chứng minh mọi batch của CNN đều xung đột. Hành trình kiểm tra riêng một batch thực ban đầu và lưu tích/chuẩn trong quá trình huấn luyện.
Với hai nhiệm vụ, mỗi nhiệm vụ chỉ gặp một tham chiếu khác: thứ tự không có tác động ở đây. Mã chấp nhận nhiều nhiệm vụ hơn và một kiểm thử ba nhiệm vụ kiểm tra việc tiếp tục RNG cùng việc dùng tham chiếu gốc. Các phép chiếu liên tiếp với hơn hai nhiệm vụ không bảo đảm mọi xung đột cuối cùng biến mất.
Biến thể này giới hạn ở thân dùng chung và gộp bằng trung bình, phù hợp với đối chứng. Kho TensorFlow lịch sử gộp bằng tổng và dòng trộn của nó không dùng giá trị trả về; chúng tôi theo thứ tự tường minh được thuật toán của bài báo mô tả. Trường hợp chuẩn bằng không cũng được xử lý tường minh ở đây. Không khẳng định tương đương với trainer TensorFlow đầy đủ.
Huấn luyện, kiểm tra và tiếp tục
Ba nhánh chính bắt đầu từ cùng một tệp khởi tạo. Mỗi nhánh chạy 30 epoch, batch 64, batch cuối 40 ảnh: 66 update mỗi epoch và tổng 1980. Hai mạng riêng mỗi mạng thực hiện 1980 update này. AdamW dùng lr=0,001, betas=(0,9;0,999), eps=1e−8 và weight_decay=0,0001, không scheduler hay gradient clipping. Validation theo dõi loss, không chọn checkpoint tốt nhất; trạng thái cuối được ấn định trước.
Sampler CPU, hạt giống 117, tạo cùng thứ tự cho mọi nhánh. RNG chiếu, hạt giống 217, được lưu dù thứ tự với hai nhiệm vụ là tầm thường. Mỗi batch dùng chung lưu tích vô hướng, chuẩn gốc/đã chiếu và thứ tự chiếu. Các batch đầu tiên của epoch 1, 15 và 30 lưu vector đầy đủ, gồm cả gradient của các đầu. Tập con probe này được cố định trước huấn luyện; các batch khác giữ các đại lượng vô hướng.
Mỗi nhánh so sánh gián đoạn sau 12 epoch (792 update), 30 epoch liên tục và tiếp tục 18 epoch còn lại (1188 update mới). Trọng số, Adam, sampler, RNG hệ thống, RNG chiếu và lịch sử số phải hoàn toàn bằng nhau. Thời lượng không thuộc phép so sánh. Các trạng thái cục bộ gzip mức 1 được giữ cùng hash; trạng thái cuối cũng lưu ở Safetensors và thực sự được tải lại trước suy luận. Chương trình không tải pickle bên thứ ba.
Đánh giá hai đơn vị
Mỗi nhánh dự đoán cùng 900 ảnh của 45 họ được giữ lại. Macro-F1 và ma trận nhầm lẫn áp dụng cho ba lớp. Sai số diện tích được biểu thị bằng điểm phần trăm: sai số phân số 0,01 bằng một điểm. Công bố MAE và RMSE, cùng số dự đoán ngoài [0,1]. Chúng không bị clip để làm đẹp bảng.
Mỗi họ có 20 ảnh; báo cáo accuracy lớp và MAE diện tích của họ đó. Bootstrap ghép cặp lấy 45 họ có hoàn lại, 1000 lần, hạt giống 317. Các khoảng áp dụng cho chênh lệch accuracy và MAE, không phải macro-F1 được đổi tên. Biên là các giá trị đã sắp xếp ở hạng 25 và 975 trên 1000. Điều này mô tả biến thiên giữa các họ của corpus này, không phải giữa các hạt giống huấn luyện.
Không có điểm tổng hợp tùy tiện giữa F1 và diện tích. Cải thiện một nhiệm vụ có thể phải trả giá ở nhiệm vụ kia. Thời gian nhánh, validation, inference, số tham số và bộ nhớ thực tế quan sát được đi kèm chất lượng. Chi phí tiếp tục và bài tập là bổ sung. Bootstrap không biến corpus giả định này thành đánh giá công nghiệp.
Bài tập: một loss nặng gấp một trăm lần
Hai nhánh mới weighted-x100 và pcgrad-x100 thực sự bắt đầu lại từ khởi tạo, với cùng dữ liệu, chuẩn hóa, thứ tự và 1980 update. Chỉ loss diện tích được nhân 100, nên cả gradient đầu của nó cũng vậy. Mã lấy đạo hàm Huber không trọng số, rồi nhân tường minh gradient của nó với hệ số trước phép chiếu và trung bình. Dạng tương đương về mặt toán học này cũng cố định thứ tự số FP32 và tránh khác biệt triệt tiêu do đổi tỉ lệ trước backpropagation. Chúng có trạng thái tiếp tục và trọng số cuối riêng. Kết quả chính được bảo vệ bằng hash.
So sánh các chuẩn từ batch đầu tiên, các phép chiếu và hai nhiệm vụ test. PCGrad loại bỏ một thành phần đối nghịch; nó không tự động chuẩn hóa ảnh hưởng của các nhiệm vụ. AdamW tự nó có thể làm giảm một số tác động của đổi tỉ lệ đồng nhất: kết quả cũng phụ thuộc optimizer này. Không áp đặt sự cải thiện nào của PCGrad hay suy giảm của biến thể ×100.
Giới hạn
Các bóng hình tổng hợp đều đặn và hai nhiệm vụ có thể tương thích. Tích vô hướng âm là một chẩn đoán cục bộ; nó không đủ để dự đoán sự đánh đổi trên test. Một hạt giống duy nhất không đo biến thiên học. Các nhiệm vụ thực sự độc lập hoặc ràng buộc triển khai khác nhau có thể biện minh cho hai mạng riêng.
Chuẩn bị hai mục tiêu nhất quán
Đơn vị và phân vùng theo chi tiết
Lỗi thường gặp: chuẩn hóa có cả test; tính lại trên train và cố định thống kê của nó.
Bất biến: diện tích trong [0,1], các họ rời nhau và các lớp bao phủ cùng kích thước.
Tệp src/pcgrad_multitache/data.py
"""Original raster silhouettes. Geometry is an audit label, never an input feature."""
import hashlib, json, math, random
from pathlib import Path
import numpy as np
CLASSES = ["rectangle", "ellipse", "triangle"]
def split_geometries():
rng = random.Random(17)
splits = {}
for class_id in range(3):
families = list(range(class_id, 300, 3))
rng.shuffle(families)
for i, family in enumerate(families):
splits[family] = "train" if i < 70 else "validation" if i < 85 else "test"
return splits
def geometry(family, view):
if (
type(family) is not int
or type(view) is not int
or not 0 <= family < 300
or not 0 <= view < 20
):
raise ValueError("Unknown family/view")
group = family // 3
kind = family % 3
rng = np.random.default_rng(17000 + family * 20 + view)
nominal = 0.06 + 0.10 * (group % 10) / 9
aspect = 0.8 + 0.45 * ((group // 10) % 10) / 9
scale = float(rng.uniform(0.90, 1.10))
area = nominal * 4096 * scale**2
factor = [4.0, math.pi, 2.0][kind]
a = math.sqrt(area * aspect / factor)
b = a / aspect
return {
"class_id": kind,
"class_name": CLASSES[kind],
"center_x": 31.5 + float(rng.uniform(-2, 2)),
"center_y": 31.5 + float(rng.uniform(-2, 2)),
"half_width": a,
"half_height": b,
"angle_radians": (group * 7 % 31) / 31 * 2 * math.pi
+ float(rng.uniform(-0.15, 0.15)),
"nominal_area_fraction": nominal,
"view_scale": scale,
"background": int(rng.integers(12, 50)),
"foreground": int(rng.integers(175, 241)),
"lighting_x": float(rng.uniform(-12, 12)),
"lighting_y": float(rng.uniform(-12, 12)),
"noise_seed": 27000 + family * 20 + view,
}
def render_part(parameters):
yy, xx = np.mgrid[0:64, 0:64]
dx = xx - parameters["center_x"]
dy = yy - parameters["center_y"]
cos = math.cos(parameters["angle_radians"])
sin = math.sin(parameters["angle_radians"])
x = cos * dx + sin * dy
y = -sin * dx + cos * dy
a = parameters["half_width"]
b = parameters["half_height"]
kind = parameters["class_id"]
if kind == 0:
mask = (np.abs(x) <= a) & (np.abs(y) <= b)
elif kind == 1:
mask = (x / a) ** 2 + (y / b) ** 2 <= 1
elif kind == 2:
mask = (y >= -b) & (y <= b) & (np.abs(x) <= a * (y + b) / (2 * b))
else:
raise ValueError("Unknown silhouette class")
if (
not mask.any()
or mask[0].any()
or mask[-1].any()
or mask[:, 0].any()
or mask[:, -1].any()
):
raise ValueError(
"Empty or border-clipped silhouette; do not silently change target"
)
noise = np.random.default_rng(parameters["noise_seed"]).integers(
-6, 7, size=(64, 64)
)
illumination = (
parameters["lighting_x"] * (xx - 31.5) / 32
+ parameters["lighting_y"] * (yy - 31.5) / 32
)
pixels = (
np.rint(
np.where(mask, parameters["foreground"], parameters["background"])
+ illumination
+ noise
)
.clip(0, 255)
.astype(np.uint8)
)
return pixels, mask
def compute_area(mask):
if (
not isinstance(mask, np.ndarray)
or mask.shape != (64, 64)
or mask.dtype != np.bool_
):
raise ValueError("Expected exact binary64x64mask")
return int(mask.sum()), int(mask.sum()) / 4096
def decode_pixels(page):
value = page["pixels_uint8_hex"]
if not isinstance(value, str) or len(value) != 8192:
raise ValueError("Expected4096uint8pixels")
try:
raw = bytes.fromhex(value)
except ValueError as exc:
raise ValueError("Invalid pixel encoding") from exc
if len(raw) != 4096 or hashlib.sha256(raw).hexdigest() != page["image_sha256"]:
raise ValueError("Image integrity mismatch")
return np.frombuffer(raw, dtype=np.uint8).reshape(64, 64).copy()
def generate_pieces():
splits = split_geometries()
pages = []
labels = []
for family in range(300):
for view in range(20):
identifier = f"PIECE{family:03d}-V{view:02d}"
parameters = geometry(family, view)
pixels, mask = render_part(parameters)
count, area = compute_area(mask)
raw = pixels.tobytes()
pages.append(
{
"document_id": identifier,
"family": f"PIECE{family:03d}",
"split": splits[family],
"view": view,
"width": 64,
"height": 64,
"pixels_uint8_hex": raw.hex(),
"image_sha256": hashlib.sha256(raw).hexdigest(),
}
)
labels.append(
{
"document_id": identifier,
"class_id": family % 3,
"class_name": CLASSES[family % 3],
"mask_pixels": count,
"total_pixels": 4096,
"area_fraction": area,
"mask_bits_hex": np.packbits(mask.reshape(-1), bitorder="big")
.tobytes()
.hex(),
"renderer_parameters": parameters,
}
)
return pages, labels
def audit_data(pages, labels):
if (
len(pages) != 6000
or len(labels) != 6000
or len({p["document_id"] for p in pages}) != 6000
or len({p["image_sha256"] for p in pages}) != 6000
):
raise ValueError("Expected6000uniqueimages")
gold = {g["document_id"]: g for g in labels}
families = {}
counts = {s: [0, 0, 0] for s in ["train", "validation", "test"]}
area_ranges = {k: [] for k in CLASSES}
train = []
for page in pages:
g = gold[page["document_id"]]
decode_pixels(page)
mask = (
np.unpackbits(
np.frombuffer(bytes.fromhex(g["mask_bits_hex"]), dtype=np.uint8),
bitorder="big",
)
.reshape(64, 64)
.astype(bool)
)
n, area = compute_area(mask)
if n != g["mask_pixels"] or area != g["area_fraction"] or not 0 < area < 1:
raise ValueError("Actual binarymaskarea differs")
if {
"class_id",
"area_fraction",
"renderer_parameters",
"mask_bits_hex",
} & page.keys():
raise ValueError("Targets must not be input features")
families.setdefault(page["family"], []).append(page)
counts[page["split"]][g["class_id"]] += 1
area_ranges[g["class_name"]].append(area)
if page["split"] == "train":
train.append(area)
if (
len(families) != 300
or any(
len(rows) != 20 or len({r["split"] for r in rows}) != 1
for rows in families.values()
)
or counts != {"train": [1400] * 3, "validation": [300] * 3, "test": [300] * 3}
):
raise ValueError("Whole-family balanced partitions differ")
ranges = {
k: {"minimum": min(values), "maximum": max(values)}
for k, values in area_ranges.items()
}
overlap = [
max(r["minimum"] for r in ranges.values()),
min(r["maximum"] for r in ranges.values()),
]
if overlap[0] >= overlap[1]:
raise ValueError("Area alone separates the classes: fix corpus before training")
values = np.array(train, dtype=np.float64)
normalization = {
"population": "train",
"examples": 4200,
"mean": float(values.mean()),
"std": float(values.std(ddof=0)),
"ddof": 0,
"unit": "fraction of4096binarypixels",
}
if normalization["std"] <= 0:
raise ValueError("Degenerate area normalization")
return {
"dataset": "PiecesDeuxTaches-v1",
"images": 6000,
"families": 300,
"class_counts_by_split": counts,
"normalization": normalization,
"area_ranges_by_class": ranges,
"common_area_interval": overlap,
"masks_are_supervision_not_model_inputs": True,
"real_physical_area_claim": False,
}
def prepare(config):
from .config import save_json, sha
root = Path("data/generated")
if config["seed"] != 17 or (root / "manifest.json").exists():
raise ValueError("Expectedseed17andfreshdataoutput")
pages, labels = generate_pieces()
audit = audit_data(pages, labels)
gold = {g["document_id"]: g for g in labels}
save_json(root / "input/pages.json", pages)
save_json(root / "labels/gold.json", labels)
for split in ["train", "validation"]:
selected = [p for p in pages if p["split"] == split]
save_json(root / f"{split}/pages.json", selected)
save_json(
root / f"labels/{split}.json", [gold[p["document_id"]] for p in selected]
)
save_json("outputs/data-audit.json", audit)
save_json("outputs/learning/normalization.json", audit["normalization"])
save_json(
root / "manifest.json",
{
"dataset": audit["dataset"],
"images": 6000,
"files": {
str(f.relative_to(root)): sha(f) for f in root.rglob("*") if f.is_file()
},
},
)
return audit
def load_learning(split):
from .config import read_json
import torch
if split not in ["train", "validation"]:
raise ValueError("Onlytrain/validationforlearning")
for forbidden in [
"data/generated/input",
"data/generated/labels/gold.json",
"outputs/inference",
]:
if Path(forbidden).exists():
raise ValueError("Learning scope exposesheldoutdata:" + forbidden)
pages = read_json(f"data/generated/{split}/pages.json")
gold = {
g["document_id"]: g for g in read_json(f"data/generated/labels/{split}.json")
}
norm = read_json("outputs/learning/normalization.json")
x = (
torch.from_numpy(np.stack([decode_pixels(p) for p in pages])[:, None]).float()
/ 255.0
)
classes = torch.tensor(
[gold[p["document_id"]]["class_id"] for p in pages], dtype=torch.long
)
area = torch.tensor(
[
(gold[p["document_id"]]["area_fraction"] - norm["mean"]) / norm["std"]
for p in pages
],
dtype=torch.float32,
)
return pages, x, classes, area, norm
Tạo baseline dùng chung
Một thân, hai đầu
Lỗi thường gặp: một đầu bị tách khỏi thân; kiểm thử riêng hai gradient.
Bất biến: hai đầu ra hữu hạn và các tham số dùng chung có thể nhận diện.
Tệp src/pcgrad_multitache/model.py
import copy, hashlib, json
import torch
from torch import nn
class SharedModel(nn.Module):
def __init__(self):
super().__init__()
self.trunk = nn.Sequential(
nn.Conv2d(1, 32, 3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(32, 64, 3, stride=2, padding=1),
nn.ReLU(),
nn.Conv2d(64, 96, 3, stride=2, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d(1),
nn.Flatten(),
)
self.class_head = nn.Linear(96, 3)
self.area_head = nn.Linear(96, 1)
def encode_part(self, x):
if x.ndim != 4 or x.shape[1:] != (1, 64, 64) or x.dtype != torch.float32:
raise ValueError("Only grayscale FP32 batch x1x64x64 inputs")
return self.trunk(x)
def forward(self, x):
encoded = self.encode_part(x)
return self.class_head(encoded), self.area_head(encoded).squeeze(-1)
class SeparateModels(nn.Module):
def __init__(self, initial):
super().__init__()
self.class_trunk = copy.deepcopy(initial.trunk)
self.area_trunk = copy.deepcopy(initial.trunk)
self.class_head = copy.deepcopy(initial.class_head)
self.area_head = copy.deepcopy(initial.area_head)
def forward(self, x):
return self.class_head(self.class_trunk(x)), self.area_head(
self.area_trunk(x)
).squeeze(-1)
def parameter_manifest(model):
return [
{
"name": n,
"shape": list(p.shape),
"numel": p.numel(),
"dtype": str(p.dtype),
"sha256": hashlib.sha256(
p.detach().cpu().contiguous().numpy().tobytes()
).hexdigest(),
}
for n, p in model.named_parameters()
]
def identity(model):
return hashlib.sha256(
json.dumps(
parameter_manifest(model), sort_keys=True, separators=(",", ":")
).encode()
).hexdigest()
def losses(logits, predicted_area, classes, standard_area, area_scale=1.0):
return torch.nn.functional.cross_entropy(
logits, classes
), area_scale * torch.nn.functional.huber_loss(
predicted_area, standard_area, delta=1.0
)
Mã hóa PCGrad không dùng helper mờ đục
Phép chiếu lên không gian con dùng chung
Lỗi thường gặp: tái dùng gradient đã sửa làm tham chiếu; sao chép các gradient gốc.
Bất biến: fixture vector chính xác và không có NaN với gradient bằng không.
Tệp src/pcgrad_multitache/pcgrad.py
"""Original gradient projection, restricted explicitly to shared parameters."""
import random
import torch
def flatten_gradients(gradients, parameters, allow_unused=False):
parameters = list(parameters)
gradients = list(gradients)
if (
not parameters
or len(parameters) != len(gradients)
or len({id(p) for p in parameters}) != len(parameters)
):
raise ValueError("Stable nonempty unique parameter list required")
flat = []
for gradient, parameter in zip(gradients, parameters):
if gradient is None:
if not allow_unused:
raise ValueError(
"A task is disconnected from a declared shared parameter"
)
gradient = torch.zeros_like(parameter)
if (
gradient.shape != parameter.shape
or gradient.dtype != parameter.dtype
or gradient.device != parameter.device
or not torch.isfinite(gradient).all()
):
raise ValueError("Gradient shape/dtype/device/finite contract differs")
flat.append(gradient.detach().reshape(-1))
return torch.cat(flat)
def project_conflicts(gradients, generator, orders=None):
if (
not isinstance(gradients, torch.Tensor)
or gradients.ndim != 2
or gradients.shape[0] < 2
or gradients.shape[1] < 1
or gradients.dtype not in [torch.float32, torch.float64]
or not torch.isfinite(gradients).all()
):
raise ValueError("Expected finite task-by-coordinate FP32/FP64 gradients")
if not isinstance(generator, random.Random):
raise ValueError("Use an explicit checkpointed projection RNG")
original = gradients.detach().clone()
projected = original.clone()
tasks = len(original)
if orders is None:
orders = []
for i in range(tasks):
others = [j for j in range(tasks) if j != i]
generator.shuffle(others)
orders.append(others)
if len(orders) != tasks or any(
sorted(order) != [j for j in range(tasks) if j != i]
for i, order in enumerate(orders)
):
raise ValueError("Each order must visit every other original task exactly once")
trace = []
for i, order in enumerate(orders):
for j in order:
reference = original[j]
norm2 = torch.dot(reference, reference)
dot = torch.dot(projected[i], reference)
if not torch.isfinite(norm2) or not torch.isfinite(dot):
raise ValueError("Gradient products overflow; no silent normalization")
changed = bool(norm2 > 0 and dot < 0)
coefficient = dot / norm2 if changed else torch.zeros_like(dot)
if changed:
projected[i] -= coefficient * reference
trace.append(
{
"task": i,
"reference": j,
"dot_before": float(dot),
"reference_norm_squared": float(norm2),
"coefficient": float(coefficient),
"projected": changed,
"zero_reference_skipped": bool(norm2 == 0),
}
)
if not torch.isfinite(projected).all():
raise ValueError("Nonfinite projection")
return (
projected,
projected.mean(dim=0),
{
"orders": orders,
"trace": trace,
"reference_gradients_unchanged": torch.equal(original, gradients.detach()),
"aggregation": "mean",
"zero_norm_rule": "skip-exact-zero",
},
)
def assign_gradients(parameters, flat):
parameters = list(parameters)
if (
not parameters
or len({id(p) for p in parameters}) != len(parameters)
or flat.ndim != 1
or sum(p.numel() for p in parameters) != flat.numel()
or not torch.isfinite(flat).all()
):
raise ValueError("Unflatten population/shape/finite mismatch")
offset = 0
for parameter in parameters:
if parameter.dtype != flat.dtype or parameter.device != flat.device:
raise ValueError("Assigned gradient dtype/device mismatch")
parameter.grad = (
flat[offset : offset + parameter.numel()]
.reshape_as(parameter)
.detach()
.clone()
)
offset += parameter.numel()
def task_gradients(model, class_loss, area_loss):
shared = list(model.trunk.parameters())
class_head = list(model.class_head.parameters())
area_head = list(model.area_head.parameters())
a = torch.autograd.grad(
class_loss, shared + class_head, retain_graph=True, allow_unused=False
)
b = torch.autograd.grad(
area_loss, shared + area_head, retain_graph=False, allow_unused=False
)
original = torch.stack(
[
flatten_gradients(a[: len(shared)], shared),
flatten_gradients(b[: len(shared)], shared),
]
)
return shared, class_head, area_head, original, a[len(shared) :], b[len(shared) :]
def set_multitask_gradients(
model, class_loss, area_loss, method, generator, area_scale=1.0
):
if method not in ["weighted", "pcgrad"]:
raise ValueError("Unknown shared update rule")
if not isinstance(area_scale, (int, float)) or not area_scale > 0:
raise ValueError("Use a finite positive area scale")
shared, class_head, area_head, original, head_class, head_area = task_gradients(
model, class_loss, area_loss
)
# Differentiate the unscaled Huber loss first, then apply the declared
# objective scale explicitly. This avoids scale-dependent floating-point
# rounding inside autograd and makes the x100 exercise auditable.
original[1].mul_(area_scale)
if method == "pcgrad":
projected, assigned, trace = project_conflicts(original, generator)
else:
projected = original.clone()
assigned = original.mean(0)
trace = {
"orders": [],
"trace": [],
"reference_gradients_unchanged": True,
"aggregation": "mean",
"zero_norm_rule": "not-applicable",
}
assign_gradients(shared, assigned)
# Disjoint heads receive ordinary own-task gradients scaled by the same 1/2.
a = flatten_gradients(head_class, class_head) * 0.5
b = flatten_gradients(head_area, area_head) * (0.5 * area_scale)
assign_gradients(class_head, a)
assign_gradients(area_head, b)
return {
"original": original,
"projected": projected,
"assigned": assigned,
"class_head_assigned": a,
"area_head_assigned": b,
"trace": trace,
}
Huấn luyện và tiếp tục các nhánh
So sánh với batch không đổi
Lỗi thường gặp: tích lũy graph giữ lại cả epoch; giải phóng sau mỗi update.
Bất biến: tham số thay đổi; việc tiếp tục khôi phục hoán vị các phép chiếu.
Tệp src/pcgrad_multitache/train.py
"""Explicit fixed-budget supervised updates and exact complete-state continuation."""
import copy, json, random, time
from pathlib import Path
import torch
from safetensors.torch import save_file, load_file
from .config import seed_all, save_json, read_json, sha, rng_state, restore_rng
from .model import SharedModel, SeparateModels, identity, parameter_manifest, losses
from .pcgrad import set_multitask_gradients
from .data import load_learning
from .state import save_resume_state, load_resume_state, equal
BRANCHES = {
"separate": ("separate", 1.0),
"weighted": ("weighted", 1.0),
"pcgrad": ("pcgrad", 1.0),
"weighted-x100": ("weighted", 100.0),
"pcgrad-x100": ("pcgrad", 100.0),
}
def create_initial():
seed_all(17)
model = SharedModel()
path = Path("assets/checkpoints/initial.safetensors")
if path.exists():
raise ValueError("Preserve existing initialization")
path.parent.mkdir(parents=True, exist_ok=True)
save_file(
{k: v.detach().cpu().contiguous() for k, v in model.state_dict().items()},
str(path),
)
report = {
"status": "CREATED_RANDOM_INITIALIZATION",
"seed": 17,
"shared_parameters": sum(p.numel() for p in model.parameters()),
"trunk_parameters": sum(p.numel() for p in model.trunk.parameters()),
"class_head_parameters": sum(p.numel() for p in model.class_head.parameters()),
"area_head_parameters": sum(p.numel() for p in model.area_head.parameters()),
"separate_combined_parameters": sum(
p.numel() for p in SeparateModels(model).parameters()
),
"initial_model_identity": identity(model),
"initial_file_sha256": sha(path),
"parameters": parameter_manifest(model),
"no_external_weights": True,
}
save_json("outputs/learning/initialization.json", report)
return {k: v for k, v in report.items() if k != "parameters"}
def initialize_branch(branch):
if branch not in BRANCHES:
raise ValueError("Unknown branch")
seed_all(17)
initial = SharedModel()
initial.load_state_dict(
load_file("assets/checkpoints/initial.safetensors"), strict=True
)
if (
identity(initial)
!= read_json("outputs/learning/initialization.json")["initial_model_identity"]
):
raise ValueError("Initial common state differs")
return SeparateModels(initial) if branch == "separate" else initial
@torch.no_grad()
def validation(model, x, classes, area):
sums = [0.0, 0.0]
correct = 0
rows = []
for start in range(0, len(x), 64):
logits, pred = model(x[start : start + 64].cuda())
c = classes[start : start + 64].cuda()
a = area[start : start + 64].cuda()
ce, hub = losses(logits, pred, c, a)
n = len(c)
sums[0] += float(ce) * n
sums[1] += float(hub) * n
correct += int((logits.argmax(-1) == c).sum())
rows.extend(
[
{"logits": z, "predicted_standard_area": v}
for z, v in zip(logits.cpu().tolist(), pred.cpu().tolist())
]
)
return {
"examples": len(x),
"ce_mean": sums[0] / len(x),
"unscaled_huber_mean": sums[1] / len(x),
"correct_class": correct,
"rows": rows,
"used_for_selection": False,
}
def scalar_diagnostics(trace):
original = trace["original"]
projected = trace["projected"]
a, b = original
norms = [float(torch.linalg.vector_norm(g)) for g in original]
dot = float(torch.dot(a, b))
den = norms[0] * norms[1]
return {
"original_dot": dot,
"original_norms": norms,
"original_cosine": dot / den if den else None,
"conflict": dot < 0,
"projected_norms": [float(torch.linalg.vector_norm(g)) for g in projected],
"assigned_norm": float(torch.linalg.vector_norm(trace["assigned"])),
"class_head_assigned_norm": float(
torch.linalg.vector_norm(trace["class_head_assigned"])
),
"area_head_assigned_norm": float(
torch.linalg.vector_norm(trace["area_head_assigned"])
),
"projection": trace["trace"],
}
def train(config, branch, mode):
if branch not in BRANCHES or mode not in ["interrupt", "continuous", "resume"]:
raise ValueError("Unknown branch/mode")
if (
config["seed"] != 17
or config["epochs"] != 30
or config["batch_size"] != 64
or config["diagnostic_epochs"] != [1, 15, 30]
):
raise ValueError("Frozen training budget changed")
pages, x, classes, area, norm = load_learning("train")
val_pages, vx, vc, va, vnorm = load_learning("validation")
if len(pages) != 4200 or len(val_pages) != 900 or norm != vnorm:
raise ValueError("Fixed partitions/normalization differ")
model = initialize_branch(branch).cuda()
model.eval()
method, area_scale = BRANCHES[branch]
initial = identity(model)
initial_parameters = parameter_manifest(model)
optimizer = torch.optim.AdamW(
model.parameters(), lr=0.001, weight_decay=0.0001, betas=(0.9, 0.999), eps=1e-8
)
sampler = torch.Generator().manual_seed(117)
projection_rng = random.Random(217)
identity_data = {
"config": config,
"branch": branch,
"method": method,
"area_scale": area_scale,
"initial_model_identity": initial,
"common_initial_sha256": sha("assets/checkpoints/initial.safetensors"),
"normalization_sha256": sha("outputs/learning/normalization.json"),
"data_sha256": {
name: sha("data/generated/" + name)
for name in [
"train/pages.json",
"validation/pages.json",
"labels/train.json",
"labels/validation.json",
]
},
"code_sha256": {
name: sha("src/pcgrad_multitache/" + name)
for name in ["data.py", "model.py", "pcgrad.py", "train.py", "state.py"]
},
}
root = Path("assets/checkpoints")
destination = root / f"{branch}-{mode}.pt.gz"
if destination.exists():
raise ValueError("Preserve existing checkpoint")
first_epoch = updates = 0
history = []
validations = []
if mode == "resume":
state = load_resume_state(root / f"{branch}-interrupt.pt.gz")
if state["identity"] != identity_data:
raise ValueError("Continuation identity changed")
model.load_state_dict(state["model"], strict=True)
optimizer.load_state_dict(state["optimizer"])
sampler.set_state(state["sampler"])
projection_rng.setstate(state["projection_rng"])
restore_rng(state["rng"])
first_epoch = state["epoch"]
updates = state["updates"]
history = state["history"]
validations = state["validation_history"]
if first_epoch != 12 or updates != 792 or state["scheduler"] is not None:
raise ValueError("Expected exact epoch12 boundary")
stop = 12 if mode == "interrupt" else 30
torch.cuda.reset_peak_memory_stats()
torch.cuda.synchronize()
started = time.perf_counter()
training_seconds = validation_seconds = 0.0
for epoch in range(first_epoch, stop):
order = torch.randperm(4200, generator=sampler).tolist()
torch.cuda.synchronize()
tick = time.perf_counter()
for offset in range(0, 4200, 64):
indices = order[offset : offset + 64]
batch = x[indices].cuda()
c = classes[indices].cuda()
a = area[indices].cuda()
optimizer.zero_grad(set_to_none=True)
logits, predicted = model(batch)
ce, huber = losses(logits, predicted, c, a, 1.0)
ce_value = float(ce.detach())
huber_value = float(huber.detach())
diagnostic = None
probe = None
if method == "separate":
((ce + area_scale * huber) / 2).backward()
else:
trace = set_multitask_gradients(
model, ce, huber, method, projection_rng, area_scale=area_scale
)
diagnostic = scalar_diagnostics(trace)
if epoch + 1 in [1, 15, 30] and offset == 0:
probe = {
k: trace[k].cpu().tolist()
for k in [
"original",
"projected",
"assigned",
"class_head_assigned",
"area_head_assigned",
]
}
if any(
p.grad is None or not torch.isfinite(p.grad).all()
for p in model.parameters()
):
raise ValueError("Missing or nonfinite actual gradient")
norm_grad = float(
torch.sqrt(sum(p.grad.square().sum() for p in model.parameters()))
)
if not torch.isfinite(torch.tensor(norm_grad)):
raise ValueError("Nonfinite update norm")
optimizer.step()
updates += 1
history.append(
{
"epoch": epoch + 1,
"update": updates,
"document_ids": [pages[i]["document_id"] for i in indices],
"examples": len(indices),
"class_ce": ce_value,
"unscaled_area_huber": huber_value,
"area_scale": area_scale,
"weighted_objective": (ce_value + area_scale * huber_value) / 2,
"parameter_gradient_norm": norm_grad,
"diagnostic": diagnostic,
"full_vector_probe": probe,
}
)
torch.cuda.synchronize()
training_seconds += time.perf_counter() - tick
tick = time.perf_counter()
validations.append({"epoch": epoch + 1, **validation(model, vx, vc, va)})
torch.cuda.synchronize()
validation_seconds += time.perf_counter() - tick
torch.cuda.synchronize()
seconds = time.perf_counter() - started
final = identity(model)
final_parameters = parameter_manifest(model)
if initial == final or not any(h["parameter_gradient_norm"] > 0 for h in history):
raise ValueError("No actual learning")
state = {
"identity": identity_data,
"model": {n: t.detach().cpu().clone() for n, t in model.state_dict().items()},
"optimizer": optimizer.state_dict(),
"sampler": sampler.get_state(),
"projection_rng": projection_rng.getstate(),
"rng": rng_state(),
"epoch": stop,
"updates": updates,
"history": history,
"validation_history": validations,
"scheduler": None,
}
storage = save_resume_state(destination, state)
report = {
"branch": branch,
"mode": mode,
"identity": identity_data,
"epochs": stop,
"updates": updates,
"initial_model_identity": initial,
"final_model_identity": final,
"initial_parameters": initial_parameters,
"final_parameters": final_parameters,
"history": history,
"validation_history": validations,
"actual_training_interval_epochs": stop - first_epoch,
"seconds": seconds,
"training_seconds": training_seconds,
"validation_seconds": validation_seconds,
"peak_allocated_bytes": torch.cuda.max_memory_allocated(),
"peak_reserved_bytes": torch.cuda.max_memory_reserved(),
"checkpoint_storage": storage,
"checkpoint_sha256": sha(destination),
"heldout_inputs_visible": False,
"separate_cost_includes_two_networks": method == "separate",
}
save_json(f"outputs/learning/{branch}/{mode}.json", report)
if mode == "resume":
safe = root / f"{branch}-resume.safetensors"
if safe.exists():
raise ValueError("Preserve existing learned model")
save_file(
{n: t.detach().cpu().contiguous() for n, t in model.state_dict().items()},
str(safe),
)
save_json(
safe.with_suffix(".json"),
{
"identity": identity_data,
"sha256": sha(safe),
"model_identity": final,
"parameters": final_parameters,
},
)
return {
"branch": branch,
"mode": mode,
"epochs": stop,
"updates": updates,
"seconds": seconds,
}
def verify_resume(branch):
if branch not in BRANCHES:
raise ValueError("Unknown branch")
a = Path("assets/checkpoints") / f"{branch}-continuous.pt.gz"
b = Path("assets/checkpoints") / f"{branch}-resume.pt.gz"
continuous = load_resume_state(a)
resumed = load_resume_state(b)
if not equal(continuous, resumed):
raise ValueError("Model/Adam/RNG/sampler/projection continuation differs")
report = {
"status": "PASS",
"branch": branch,
"all_state_exact": True,
"updates": resumed["updates"],
"continuous_sha256": sha(a),
"resume_sha256": sha(b),
"projection_rng_included": True,
}
save_json(f"outputs/learning/{branch}/resume-check.json", report)
return report
Diễn giải hai nhiệm vụ
Không có điểm tổng hợp tùy tiện
Lỗi thường gặp: so sánh diện tích chuẩn hóa với pixel; áp dụng phép nghịch đảo chuẩn hóa.
Bất biến: mọi kết quả tương ứng với cùng 900 trường hợp đã định.
Tệp src/pcgrad_multitache/evaluate.py
import json, math, random, time
from pathlib import Path
import numpy as np
import torch
from safetensors.torch import load_file
from .config import read_json, save_json, sha, seed_all
from .data import decode_pixels
from .model import SharedModel, SeparateModels, identity
def class_metrics(actual, predicted):
if (
not actual
or len(actual) != len(predicted)
or any(type(x) is not int or not 0 <= x < 3 for x in actual + predicted)
):
raise ValueError("Aligned nonempty threeclass predictions required")
matrix = [[0] * 3 for _ in range(3)]
for a, p in zip(actual, predicted):
matrix[a][p] += 1
f1 = []
for c in range(3):
tp = matrix[c][c]
fp = sum(matrix[r][c] for r in range(3) if r != c)
fn = sum(matrix[c][r] for r in range(3) if r != c)
f1.append(2 * tp / (2 * tp + fp + fn) if 2 * tp + fp + fn else 0.0)
return {
"cases": len(actual),
"confusion_matrix": matrix,
"class_support": [sum(row) for row in matrix],
"per_class_f1": f1,
"macro_f1": sum(f1) / 3,
"correct": sum(matrix[c][c] for c in range(3)),
"accuracy": sum(matrix[c][c] for c in range(3)) / len(actual),
}
def area_metrics(actual, predicted):
if (
not actual
or len(actual) != len(predicted)
or not all(math.isfinite(v) for v in actual + predicted)
or any(not 0 <= v <= 1 for v in actual)
):
raise ValueError("Finite aligned fractionarea targets required")
errors = [p - a for a, p in zip(actual, predicted)]
return {
"cases": len(actual),
"mae_percentage_points": 100 * sum(abs(e) for e in errors) / len(errors),
"rmse_percentage_points": 100
* math.sqrt(sum(e * e for e in errors) / len(errors)),
"prediction_outside_unit_interval": sum(p < 0 or p > 1 for p in predicted),
"prediction_clipping": False,
}
@torch.no_grad()
def predict(config, branch):
if branch not in ["separate", "weighted", "pcgrad", "weighted-x100", "pcgrad-x100"]:
raise ValueError("Unknown learned branch")
if Path("data/generated/labels").exists() or Path("outputs/learning").exists():
raise ValueError("Inference must not mount labels or learning outputs")
seed_all(17)
pages = read_json("data/generated/input/pages.json")
test = [p for p in pages if p["split"] == "test"]
warm = next(p for p in pages if p["split"] == "train")
if len(test) != 900:
raise ValueError("Exactly900testimages required")
path = Path("assets/checkpoints") / f"{branch}-resume.safetensors"
metadata = read_json(path.with_suffix(".json"))
if sha(path) != metadata["sha256"]:
raise ValueError("Learned state SHA mismatch")
started = time.perf_counter()
model = SharedModel()
model = SeparateModels(model) if branch == "separate" else model
model.load_state_dict(load_file(str(path)), strict=True)
if identity(model) != metadata["model_identity"]:
raise ValueError("Actual Safetensors reload differs")
model = model.cuda().eval().requires_grad_(False)
torch.cuda.synchronize()
load_seconds = time.perf_counter() - started
norm = read_json("assets/checkpoints/normalization.json")
warm_x = torch.from_numpy(decode_pixels(warm)[None, None]).float().cuda() / 255.0
torch.cuda.synchronize()
started = time.perf_counter()
warm_logits, warm_area = model(warm_x)
torch.cuda.synchronize()
warm_seconds = time.perf_counter() - started
rows = []
batch_times = []
torch.cuda.reset_peak_memory_stats()
started = time.perf_counter()
for start in range(0, 900, 64):
current = test[start : start + 64]
x = (
torch.from_numpy(np.stack([decode_pixels(p) for p in current])[:, None])
.float()
.cuda()
/ 255.0
)
torch.cuda.synchronize()
tick = time.perf_counter()
logits, area = model(x)
torch.cuda.synchronize()
batch_times.append(
{
"start": start,
"examples": len(current),
"forward_seconds": time.perf_counter() - tick,
}
)
raw_logits = logits.cpu().tolist()
raw_area = area.cpu().tolist()
for page, z, a in zip(current, raw_logits, raw_area):
predicted = int(max(range(3), key=z.__getitem__))
rows.append(
{
"document_id": page["document_id"],
"family": page["family"],
"image_sha256": page["image_sha256"],
"class_logits": z,
"predicted_class": predicted,
"predicted_standard_area": a,
"predicted_area_fraction": a * norm["std"] + norm["mean"],
}
)
torch.cuda.synchronize()
seconds = time.perf_counter() - started
if identity(model) != metadata["model_identity"] or not all(
math.isfinite(v)
for row in rows
for v in row["class_logits"] + [row["predicted_area_fraction"]]
):
raise ValueError("Inference changed parameters or returned nonfinite outputs")
report = {
"branch": branch,
"rows": rows,
"cases": 900,
"checkpoint_sha256": sha(path),
"model_identity": metadata["model_identity"],
"normalization_sha256": sha("assets/checkpoints/normalization.json"),
"normalization": norm,
"parameters": sum(p.numel() for p in model.parameters()),
"logical_parameter_bytes": sum(
p.numel() * p.element_size() for p in model.parameters()
),
"served_weight_file_bytes": path.stat().st_size,
"load_seconds": load_seconds,
"warmup": {
"document_id": warm["document_id"],
"seconds": warm_seconds,
"class_logits": warm_logits.cpu().tolist(),
"standard_area": warm_area.cpu().tolist(),
"outside_test": True,
},
"inference_seconds": seconds,
"forward_batches": batch_times,
"labels_visible": False,
"prediction_clipping": False,
"batch_size": 64,
"peak_allocated_bytes": torch.cuda.max_memory_allocated(),
"peak_reserved_bytes": torch.cuda.max_memory_reserved(),
"two_networks_combined": branch == "separate",
}
save_json(f"outputs/inference/{branch}.json", report)
return {
k: v
for k, v in report.items()
if k not in ["rows", "warmup", "forward_batches"]
}
def family_metrics(rows):
result = {}
for family in sorted({r["family"] for r in rows}):
chosen = [r for r in rows if r["family"] == family]
result[family] = {
"cases": len(chosen),
"class_accuracy": sum(
r["predicted_class"] == r["target_class"] for r in chosen
)
/ len(chosen),
"area_mae_percentage_points": 100
* sum(
abs(r["predicted_area_fraction"] - r["target_area_fraction"])
for r in chosen
)
/ len(chosen),
}
return result
def paired_family_bootstrap(a, b):
if (
set(a) != set(b)
or len(a) != 45
or any(a[k]["cases"] != 20 or b[k]["cases"] != 20 for k in a)
):
raise ValueError("Same45familiesof20images required")
families = sorted(a)
rng = random.Random(317)
draws = {k: [] for k in ["class_accuracy", "area_mae_percentage_points"]}
for _ in range(1000):
sample = [families[rng.randrange(45)] for _ in range(45)]
for k in draws:
draws[k].append(sum(b[f][k] - a[f][k] for f in sample) / 45)
return {
"draws": 1000,
"seed": 317,
"resampling_unit": "wholefamily",
"families": 45,
"difference": "second minus first",
"empirical_interval_order_statistics_1based": [25, 975],
"intervals": {
k: {
"observed": sum(b[f][k] - a[f][k] for f in families) / 45,
"lower": sorted(v)[24],
"upper": sorted(v)[974],
}
for k, v in draws.items()
},
"not_training_seed_variability": True,
"accuracy_interval_not_macro_f1_interval": True,
}
def evaluate(config, exercise=False):
gold = {r["document_id"]: r for r in read_json("data/generated/labels/gold.json")}
branches = (
["weighted-x100", "pcgrad-x100"]
if exercise
else ["separate", "weighted", "pcgrad"]
)
policies = {}
for branch in branches:
raw = read_json(f"outputs/inference/{branch}.json")
rows = [
{
**r,
"target_class": gold[r["document_id"]]["class_id"],
"target_area_fraction": gold[r["document_id"]]["area_fraction"],
}
for r in raw["rows"]
]
classes = class_metrics(
[r["target_class"] for r in rows], [r["predicted_class"] for r in rows]
)
areas = area_metrics(
[r["target_area_fraction"] for r in rows],
[r["predicted_area_fraction"] for r in rows],
)
policies[branch] = {
"rows": rows,
"class_metrics": classes,
"area_metrics": areas,
"families": family_metrics(rows),
"raw_sha256": sha(f"outputs/inference/{branch}.json"),
"costs": {
k: raw[k]
for k in [
"parameters",
"logical_parameter_bytes",
"served_weight_file_bytes",
"load_seconds",
"inference_seconds",
"forward_batches",
"peak_allocated_bytes",
"peak_reserved_bytes",
"two_networks_combined",
]
},
}
paired = (
{}
if exercise
else {
"weighted_to_pcgrad": paired_family_bootstrap(
policies["weighted"]["families"], policies["pcgrad"]["families"]
),
"separate_to_weighted": paired_family_bootstrap(
policies["separate"]["families"], policies["weighted"]["families"]
),
}
)
result = {
"policies": policies,
"paired_family_bootstrap": paired,
"scope": "Syntheticsilhouettes;3classmacroF1andareaMAEpercentagepoints keptseparate. Test900images45families; no clippingoutofrangearea andnoqualitygain imposed.",
}
save_json(
"outputs/exercise/results.json" if exercise else "outputs/results.json", result
)
return {
b: {"class_metrics": v["class_metrics"], "area_metrics": v["area_metrics"]}
for b, v in policies.items()
}
Kiểm tra tác động của đơn vị
Phép chiếu so với tái trọng số
Lỗi thường gặp: giảm một mục tiêu che khuất mục tiêu kia; hiển thị cả hai chỉ số.
Bất biến: báo cáo phân biệt hướng, chuẩn và chất lượng ở hạ nguồn.
Tệp src/pcgrad_multitache/exercise.py
from pathlib import Path
from .config import read_json, save_json, sha
from .evaluate import paired_family_bootstrap
def begin(config):
path = Path("outputs/exercise/protected-main.json")
if path.exists():
raise ValueError("Preserve existing exercise")
files = (
[
Path("configs/minimum.yaml"),
Path("outputs/results.json"),
Path("outputs/learning/normalization.json"),
]
+ list(Path("outputs/inference").glob("*.json"))
+ list(Path("assets/checkpoints").glob("*.safetensors"))
)
identities = {str(p): sha(p) for p in files}
save_json(path, identities)
return {
"protected_files": len(identities),
"exercise": "Scaleareaobjectiveby100; sameinitialization/data/optimizer/1980updates",
}
def finish(config):
protected = read_json("outputs/exercise/protected-main.json")
if any(sha(p) != h for p, h in protected.items()):
raise ValueError("Exercise changed main artifacts")
main = read_json("outputs/results.json")["policies"]
extra = read_json("outputs/exercise/results.json")["policies"]
summary = {}
paired = {}
for method in ["weighted", "pcgrad"]:
a = main[method]
b = extra[method + "-x100"]
paired[method] = paired_family_bootstrap(a["families"], b["families"])
for branch in [method, method + "-x100"]:
r = read_json(f"outputs/learning/{branch}/continuous.json")
first = r["history"][0]
if (
r["updates"] != 1980
or r["identity"]["common_initial_sha256"]
!= read_json("outputs/learning/weighted/continuous.json")["identity"][
"common_initial_sha256"
]
):
raise ValueError("Freshsameinitializationandbudgetrequired")
result = {**main, **extra}[branch]
summary[branch] = {
"class_metrics": result["class_metrics"],
"area_metrics": result["area_metrics"],
"updates": r["updates"],
"area_scale": r["identity"]["area_scale"],
"first_update_diagnostics": first["diagnostic"],
"final_model_identity": r["final_model_identity"],
"seconds": r["seconds"],
}
if len({r["final_model_identity"] for r in summary.values()}) != 4:
raise ValueError("Distinctactuallearnedbranchstatesrequired")
report = {
"status": "PASS",
"main_unchanged": True,
"protected_sha256": protected,
"branches": summary,
"paired_family_bootstrap": paired,
"data_or_normalization_changed": False,
"only_area_objective_rescaled": True,
"same_optimizer": True,
"quality_gain_required": False,
}
save_json("outputs/exercise/scale-losses.json", report)
return {
k: v for k, v in report.items() if k not in ["protected_sha256", "branches"]
}
Nguồn
Gradient Surgery for Multi-Task Learning, thuật toán 1, và mã TensorFlow đã cố định của tác giả đã được đọc lại. Triển khai PyTorch của TP này là nguyên gốc và các quy ước tường minh của nó khác trainer lịch sử. Kho của tác giả theo giấy phép MIT. Quy tắc xác định được dùng tương ứng runtime PyTorch đã khóa của dự án; không hứa hẹn kết quả giống hệt giữa các phần cứng.
Quan sát phát triển
Các giá trị được tính lại từ đầu ra thô của hành trình này. Chúng không phải ngưỡng tăng lợi ích; hành trình từ ZIP tính lại chúng trên cùng các quần thể.
{
"status": "PASS",
"topic": 54,
"images": 6000,
"test_images": 900,
"test_families": 45,
"learned_branches": 5,
"updates_per_branch": 1980,
"separate_network_updates_each": 1980,
"exact_resume_chains": 5,
"retained_resume_states": 15,
"full_vector_epochs": [
1,
15,
30
],
"metrics": {
"separate": {
"class": {
"cases": 900,
"confusion_matrix": [
[
298,
2,
0
],
[
0,
300,
0
],
[
6,
0,
294
]
],
"class_support": [
300,
300,
300
],
"per_class_f1": [
0.9867549668874173,
0.9966777408637874,
0.98989898989899
],
"macro_f1": 0.9911105658833982,
"correct": 892,
"accuracy": 0.9911111111111112
},
"area": {
"cases": 900,
"mae_percentage_points": 0.15215949880065946,
"rmse_percentage_points": 0.17987498195941423,
"prediction_outside_unit_interval": 0,
"prediction_clipping": false
}
},
"weighted": {
"class": {
"cases": 900,
"confusion_matrix": [
[
279,
14,
7
],
[
53,
247,
0
],
[
20,
1,
279
]
],
"class_support": [
300,
300,
300
],
"per_class_f1": [
0.8558282208588958,
0.8790035587188612,
0.9522184300341296
],
"macro_f1": 0.8956834032039621,
"correct": 805,
"accuracy": 0.8944444444444445
},
"area": {
"cases": 900,
"mae_percentage_points": 0.1864868760149981,
"rmse_percentage_points": 0.24498318404895442,
"prediction_outside_unit_interval": 0,
"prediction_clipping": false
}
},
"pcgrad": {
"class": {
"cases": 900,
"confusion_matrix": [
[
253,
43,
4
],
[
21,
279,
0
],
[
38,
2,
260
]
],
"class_support": [
300,
300,
300
],
"per_class_f1": [
0.826797385620915,
0.8942307692307693,
0.9219858156028369
],
"macro_f1": 0.8810046568181736,
"correct": 792,
"accuracy": 0.88
},
"area": {
"cases": 900,
"mae_percentage_points": 0.4766933405951065,
"rmse_percentage_points": 0.5675124362699432,
"prediction_outside_unit_interval": 0,
"prediction_clipping": false
}
},
"weighted-x100": {
"class": {
"cases": 900,
"confusion_matrix": [
[
0,
257,
43
],
[
0,
256,
44
],
[
0,
143,
157
]
],
"class_support": [
300,
300,
300
],
"per_class_f1": [
0.0,
0.5355648535564853,
0.5772058823529411
],
"macro_f1": 0.3709235786364755,
"correct": 413,
"accuracy": 0.4588888888888889
},
"area": {
"cases": 900,
"mae_percentage_points": 0.1418863554496759,
"rmse_percentage_points": 0.1719326515726746,
"prediction_outside_unit_interval": 0,
"prediction_clipping": false
}
},
"pcgrad-x100": {
"class": {
"cases": 900,
"confusion_matrix": [
[
150,
74,
76
],
[
54,
245,
1
],
[
14,
6,
280
]
],
"class_support": [
300,
300,
300
],
"per_class_f1": [
0.5791505791505791,
0.784,
0.852359208523592
],
"macro_f1": 0.7385032625580571,
"correct": 675,
"accuracy": 0.75
},
"area": {
"cases": 900,
"mae_percentage_points": 0.1476638544471312,
"rmse_percentage_points": 0.1908630628898403,
"prediction_outside_unit_interval": 0,
"prediction_clipping": false
}
}
},
"scope": "Independent scalar masks/area/normalization, complete recorded gradient projections at fixed probes, shapes/identities/budgets, predictions/metrics and family bootstrap. Full Adam/RNG equality is public comparator tied to retained state hashes; no private pickle load or independent CNN backpropagation."
}