Una postazione di visione fittizia riceve silhouette di pezzi e deve predirne la famiglia geometrica e l'area visibile normalizzata. Vogliamo condividere un encoder, studiando esplicitamente l'effetto di due obiettivi e delle loro unità.
Per un'immagine, mostrate la classe e l'area predette, poi seguite su un piccolo esempio il prodotto scalare di due gradienti e la loro proiezione. Confrontate rete condivisa, reti separate e variante PCGrad.
Scaricare il progetto completo, revisione r001
Due target dalla stessa immagine
PiecesDeuxTaches-v1 contiene 300 famiglie di silhouette, 100 per classe rettangolo, ellisse e triangolo, con 20 viste per famiglia. Una partizione stratificata con seed 17 colloca 70/15/15 famiglie di ogni classe in train/validation/test: 4200/900/900 immagini. Le viste di una famiglia condividono una geometria vicina. Traslazione, angolo, scala, illuminazione e rumore cambiano; nessuna vista vicina attraversa una partizione.
Le immagini sono matrici uint8 di 64×64, divise per 255 per entrare nel CNN. I parametri del renderer, la classe e la maschera binaria restano label di audit e non sono mai feature. L'area è il numero esatto di pixel della maschera diviso per 4096. Non rappresenta mm². Il renderer rifiuta silhouette vuote o tagliate dal bordo. Gli intervalli di area delle tre classi si sovrappongono: la sola dimensione non deve definire la classe.
Media e deviazione standard della popolazione dell'area vengono calcolate sulle 4200 immagini train in FP64, salvate e poi congelate. Il target di regressione è (area−media_train)/deviazione_standard_train. La valutazione applica la trasformazione inversa. Calcolare queste statistiche con il test cambierebbe il protocollo.
Condividere parametri, mantenere due output
Il tronco originale possiede tre convoluzioni 3×3, stride 2 e padding 1, con 32/64/96 canali e ReLU. Le dimensioni spaziali passano da 64 a 32, 16 e poi 8. Un global average pooling produce 96 valori. La testa classe è Linear(96,3), la testa area Linear(96,1). Non vi sono né dropout, né batch normalization, né sigmoid sull'area.
Il tronco conta 74 208 parametri; le teste 291 e 97, cioè 74 596 parametri unici per la rete condivisa. Le due reti separate contano insieme 148 804 parametri: un tronco e una testa classe, poi un altro tronco e una testa area. Le copie hanno gli stessi valori iniziali del ramo condiviso, con storage distinti. I loro costi combinati vengono misurati. Lo stesso numero di batch non significa gli stessi FLOPs.
La loss di classe è l'entropia incrociata su tre logits. L'area standardizzata usa Huber delta=1: errore quadratico vicino a zero, crescita lineare oltre. Questi obiettivi hanno unità e curvature diverse. Il testimone condiviso ottimizza (L_classe+L_area)/2. I due modelli separati ricevono ciascuno la propria loss divisa per due, per mantenere la convenzione di scala.
Ciò che PCGrad proietta
Autograd calcola separatamente g_classe e g_area sulla lista ordinata dei parametri del tronco. Flatten concatena le loro coordinate secondo questo ordine; un gradiente mancante è un errore per un parametro dichiarato condiviso. L'opzione esplicita allow_unused consente zeri per altri usi, ma il tutorial non la attiva per mascherare una testa scollegata.
Il prodotto scalare negativo indica che le due direzioni locali si oppongono. Per ogni compito i, partiamo da una copia g_i_proj e visitiamo gli altri compiti j in un ordine casuale salvato. Se g_i_proj·g_j_original<0, sottraiamo [g_i_proj·g_j_original / ||g_j_original||²] g_j_original. I riferimenti restano i gradienti originali. Una norma esattamente nulla viene ignorata; non vengono introdotti né divisione per zero né epsilon nascosto.
Il gradiente assegnato al tronco è la media dei due vettori proiettati. Le teste ricevono i propri gradienti ordinari divisi per due. Non proiettiamo artificialmente una testa contro il gradiente inesistente dell'altro compito. Il testimone weighted calcola anch'esso i due gradienti per la diagnosi, poi la loro media; un test confronta questa operazione con il backward della media delle due loss.
L'esempio g1=(1,0), g2=(−1,1) dà g1_proj=(0,5;0,5), g2_proj=(0;1), media=(0,25;0,75). Questi vettori sono costruiti per spiegare il calcolo. Non provano che tutti i batch del CNN siano conflittuali. Il percorso ispeziona separatamente un vero batch iniziale e conserva prodotti/norme durante gli addestramenti.
Con due compiti, ogni compito incontra un solo altro riferimento: qui l'ordine non ha alcun effetto. Il codice accetta più compiti e un test con tre compiti verifica la ripresa dell'RNG e l'uso dei riferimenti originali. Proiezioni successive su più di due compiti non garantiscono la scomparsa di tutti i conflitti finali.
Questa variante è limitata al tronco condiviso e aggrega per media, allineata al testimone. Il repository TensorFlow storico aggrega per somma e la sua riga di mescolamento non usa il valore restituito; seguiamo l'ordine esplicito descritto dall'algoritmo dell'articolo. Anche il caso di norma nulla viene trattato esplicitamente qui. Non viene rivendicata alcuna equivalenza del trainer TensorFlow completo.
Addestrare, ispezionare e riprendere
I tre rami principali partono dallo stesso file iniziale. Ciascuno esegue 30 epoche, batch 64, ultimo batch di 40 immagini: 66 update per epoca e 1980 in totale. Le due reti separate eseguono ciascuna questi 1980 update. AdamW usa lr=0,001, betas=(0,9;0,999), eps=1e−8 e weight_decay=0,0001, senza scheduler né clipping dei gradienti. La validazione segue le loss, senza selezione del checkpoint migliore; lo stato finale è fissato in anticipo.
Il sampler CPU, seed 117, produce gli stessi ordini per tutti i rami. L'RNG di proiezione, seed 217, viene salvato anche se l'ordine a due compiti è banale. Ogni batch condiviso conserva prodotti scalari, norme originali/proiettate e ordine di proiezione. I primi batch delle epoche 1, 15 e 30 conservano i vettori completi, inclusi i gradienti delle teste. Questo sottoinsieme di sonde è fissato prima dell'addestramento; gli altri batch conservano gli scalari.
Ogni ramo confronta interruzione dopo 12 epoche (792 update), 30 epoche continue e ripresa delle ultime 18 (1188 nuovi update). Pesi, Adam, sampler, RNG di sistema, RNG di proiezione e storie numeriche devono essere esattamente uguali. Le durate restano fuori dal confronto. Gli stati locali gzip livello 1 vengono conservati con i loro hash; gli stati finali sono inoltre salvati in Safetensors e davvero ricaricati prima dell'inferenza. Il programma non carica pickle di terzi.
Valutare due unità
Ogni ramo predice le stesse 900 immagini di 45 famiglie riservate. Macro-F1 e matrice di confusione riguardano le tre classi. L'errore di area è espresso in punti percentuali: un errore di frazione 0,01 vale un punto. Pubblichiamo MAE e RMSE, oltre al numero di predizioni fuori da [0,1]. Non vengono sottoposte a clipping per migliorare la tabella.
Ogni famiglia comporta 20 immagini; riportiamo la sua accuracy di classe e la sua MAE di area. Un bootstrap appaiato estrae 45 famiglie con reinserimento, 1000 volte, seed 317. Gli intervalli riguardano le differenze di accuracy e di MAE, non una macro-F1 rinominata. I limiti sono i valori ordinati ai ranghi 25 e 975 su 1000. Questo descrive la variabilità tra famiglie di questo corpus, non tra seed di addestramento.
Non esiste un punteggio composito improvvisato tra F1 e area. Un guadagno su un compito può costare sull'altro. Tempi dei rami, validazione, inferenza, numero di parametri e memoria realmente osservata accompagnano la qualità. I costi di ripresa e dell'esercizio sono aggiuntivi. Il bootstrap non trasforma questo corpus fittizio in una valutazione industriale.
Esercizio: una loss cento volte più pesante
Due nuovi rami weighted-x100 e pcgrad-x100 ripartono realmente dall'inizializzazione, con gli stessi dati, normalizzazione, ordine e 1980 update. Solo la loss di area viene moltiplicata per 100, quindi anche il suo gradiente di testa. Il codice differenzia la Huber non ponderata, poi moltiplica esplicitamente i suoi gradienti per il coefficiente prima della proiezione e della media. Questa forma matematicamente equivalente fissa anche l'ordine numerico FP32 ed evita differenze di cancellazione dovute a una riscalatura prima della retropropagazione. Hanno stati di ripresa e pesi finali propri. I risultati principali sono protetti da impronte.
Confrontate le norme fin dal primo batch, le proiezioni e i due compiti test. PCGrad rimuove una componente opposta; non normalizza automaticamente l'influenza dei compiti. AdamW può a sua volta attenuare alcuni effetti di una riscalatura uniforme: i risultati dipendono anche da questo ottimizzatore. Non vengono imposti né un guadagno di PCGrad né un peggioramento della variante ×100.
Limiti
Le silhouette sintetiche sono regolari e i due compiti possono essere compatibili. Un prodotto scalare negativo è una diagnosi locale; non basta a predire il compromesso test. Un solo seed non misura la variabilità dell'apprendimento. Compiti realmente indipendenti o vincoli di distribuzione distinti possono giustificare due reti separate.
Preparare due target coerenti
Unità e partizioni per pezzo
Errore frequente: la normalizzazione include il test: ricalcolare sul train e congelarne le statistiche.
Invariante: area in [0,1], famiglie disgiunte e classi che coprono le stesse dimensioni.
File 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
Formare la baseline condivisa
Un tronco, due teste
Errore frequente: una testa è scollegata dal tronco: testare separatamente entrambi i gradienti.
Invariante: due output finiti e parametri condivisi identificabili.
File 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
)
Codificare PCGrad senza helper opaco
Proiezione sul sottospazio condiviso
Errore frequente: riutilizzare gradienti modificati come riferimenti: copiare gli originali.
Invariante: fixture vettoriale esatta e nessun NaN per gradiente nullo.
File 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,
}
Addestrare e riprendere i rami
Confronto a batch costanti
Errore frequente: l'accumulo di grafi trattiene l'intera epoca: liberare dopo ogni update.
Invariante: i parametri cambiano; la ripresa ripristina la permutazione delle proiezioni.
File 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
Interpretare i due compiti
Nessun punteggio composito improvvisato
Errore frequente: confrontare area standardizzata con pixel: applicare l'inverso della normalizzazione.
Invariante: tutti i risultati corrispondono agli stessi 900 casi previsti.
File 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()
}
Testare l'effetto delle unità
Proiezione rispetto alla ripesatura
Errore frequente: una diminuzione di un obiettivo nasconde l'altro: mostrare entrambe le metriche.
Invariante: il rapporto distingue direzione, norma e qualità a valle.
File 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"]
}
Fonti
Gradient Surgery for Multi-Task Learning, algoritmo 1, e il codice TensorFlow degli autori fissato sono stati riletti. L'implementazione PyTorch di questo tutorial è originale e le sue convenzioni esplicite differiscono dal trainer storico. Il repository degli autori è sotto MIT. Le regole di determinismo usate corrispondono al runtime PyTorch bloccato del progetto; non viene promesso alcun risultato identico su hardware diversi.
Osservazioni di sviluppo
Valori ricalcolati dalle uscite grezze di questo percorso. Non sono soglie di guadagno; il percorso dallo ZIP li ricalcola sulle stesse popolazioni.
{
"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."
}