CoolFace
Modelpublic

emilyxuan/accesscontrol

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
eval_embedding_sensitivity_l2.py1047 linesDownload Raw Back to eval
1import os2import sys3import csv4import shutil5from collections import OrderedDict6 7import numpy as np8import torch9import torch.nn.functional as F10from PIL import Image, ImageDraw, ImageFont11from torchvision import transforms12from torchvision.utils import save_image13 14SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))15PROJECT_DIR = os.path.dirname(SCRIPT_DIR)16sys.path.insert(0, PROJECT_DIR)17 18from model import UNet19from diffusion import GaussianDiffusionSampler20from trigger_encoder import TriggerEncoder21 22try:23    from torchmetrics.image.lpip import LearnedPerceptualImagePatchSimilarity24    from torchmetrics.image import PeakSignalNoiseRatio, StructuralSimilarityIndexMeasure25    TORCHMETRICS_OK = True26except Exception as e:27    TORCHMETRICS_OK = False28    TORCHMETRICS_IMPORT_ERROR = repr(e)29 30 31# ============================================================32# 0. 主要修改區33# ============================================================34 35DEVICE = "cuda:1" if torch.cuda.is_available() else "cpu"36 37SAVE_DIR = "./logs/cifar10_cond_acsctrl/cond128_neg0.25_0.25_seed42_Bsize128/cond128_neg0.5_seed42_20260511_000804/eval_/eval_embedding_sensitivity_l2_ckpt500"38 39BASELINE_CKPT_PATH = "./logs/cifar10_cond_acsctrl/cond128_baseline/cond128_neg0.2_seed42_20260506_221046/checkpoints/global_ckpt_round500.pt"40COND_CKPT_PATH = "./logs/cifar10_cond_acsctrl/cond128_neg0.25_0.25_seed42_Bsize128/cond128_neg0.5_seed42_20260511_000804/checkpoints/global_ckpt_round500.pt"41 42CKPT_NAME = "ckpt500"43 44CORRECT_CLIENT_IDS = [0, 1, 2, 3, 4]45 46FALLBACK_TRIGGER_DIR = "./logs/cifar10_cond_acsctrl/cond128_neg0.25_0.25_seed42_Bsize128/cond128_neg0.5_seed42_20260511_000804/client_triggers"47 48# 每個 synthetic embedding sample 幾張圖49NUM_SAMPLES_PER_EMB = 1050XT_SEED = 651 52# L2 sensitivity 設定53L2_VALUES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]54NUM_DIRECTIONS_PER_L2 = 555DIRECTION_SEED = 2026052156 57# 圖片輸出58SAVE_ROW_IMAGES = True59SAVE_FINAL_IMAGE = True60 61ROW_SCALE = 462FINAL_SCALE = 263LABEL_W = 24064COND_W = 0  # synthetic embedding 沒有 trigger 圖,所以不用留 trigger 欄65PAD = 466 67 68# ============================================================69# 1. default params70# ============================================================71 72DEFAULT_IMG_SIZE = 3273DEFAULT_COND_DIM = 12874DEFAULT_TRIGGER_BASE_CH = 3275 76DEFAULT_T = 100077DEFAULT_BETA_1 = 1e-478DEFAULT_BETA_T = 0.0279 80DEFAULT_CH = 12881DEFAULT_CH_MULT = [1, 2, 2, 2]82DEFAULT_ATTN = [1]83DEFAULT_NUM_RES_BLOCKS = 284DEFAULT_DROPOUT = 0.185 86MODEL_KEYS = [87    "global_ema_model",88    "ema_model",89    "global_model",90    "model",91    "net_model",92    "state_dict",93]94 95ENCODER_KEYS = [96    "trigger_encoder",97    "trigger_encoder_model",98    "global_trigger_encoder",99    "ema_trigger_encoder",100    "trigger_encoder_state_dict",101]102 103 104# ============================================================105# 2. basic utils106# ============================================================107 108def ensure_dir(path):109    os.makedirs(path, exist_ok=True)110 111 112def reset_dir(path):113    if os.path.exists(path):114        shutil.rmtree(path)115    os.makedirs(path, exist_ok=True)116 117 118def denorm_to_01(x):119    return ((x + 1.0) / 2.0).clamp(0, 1)120 121 122def tensor_to_pil_01(t, scale=1):123    t = t.detach().cpu().clamp(0, 1)124    arr = (t.permute(1, 2, 0).numpy() * 255).astype(np.uint8)125    img = Image.fromarray(arr)126    if scale != 1:127        img = img.resize((img.width * scale, img.height * scale), Image.NEAREST)128    return img129 130 131def save_tensor_image_01(img_01, path):132    save_image(img_01.detach().cpu().clamp(0, 1), path)133 134 135def read_image_as_tensor(img_path, img_size, device=None):136    tfm = transforms.Compose([137        transforms.Resize((img_size, img_size)),138        transforms.ToTensor(),139        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),140    ])141    img = Image.open(img_path).convert("RGB")142    x = tfm(img)143    if device is not None:144        x = x.to(device)145    return x146 147 148def get_config(ckpt):149    config = ckpt.get("config", {})150    if config is None:151        config = {}152    return config153 154 155def get_from_config(config, key, default):156    value = config.get(key, default)157    if value is None:158        return default159    return value160 161 162def append_csv_row(csv_path, row):163    file_exists = os.path.exists(csv_path)164 165    with open(csv_path, "a", newline="") as f:166        writer = csv.DictWriter(f, fieldnames=list(row.keys()))167 168        if not file_exists:169            writer.writeheader()170 171        writer.writerow(row)172 173 174# ============================================================175# 3. model / encoder / sampler176# ============================================================177 178def infer_model_params_from_ckpt(ckpt):179    config = get_config(ckpt)180 181    mode = get_from_config(config, "mode", "access_control")182 183    img_size = int(get_from_config(config, "img_size", DEFAULT_IMG_SIZE))184    cond_dim = get_from_config(config, "cond_dim", DEFAULT_COND_DIM)185 186    T = int(get_from_config(config, "T", DEFAULT_T))187    beta_1 = float(get_from_config(config, "beta_1", DEFAULT_BETA_1))188    beta_T = float(get_from_config(config, "beta_T", DEFAULT_BETA_T))189 190    ch = int(get_from_config(config, "ch", DEFAULT_CH))191    ch_mult = list(get_from_config(config, "ch_mult", DEFAULT_CH_MULT))192    attn = list(get_from_config(config, "attn", DEFAULT_ATTN))193    num_res_blocks = int(get_from_config(config, "num_res_blocks", DEFAULT_NUM_RES_BLOCKS))194    dropout = float(get_from_config(config, "dropout", DEFAULT_DROPOUT))195 196    if mode == "unconditional":197        model_cond_dim = None198    else:199        model_cond_dim = int(cond_dim)200 201    return {202        "mode": mode,203        "img_size": img_size,204        "cond_dim": None if model_cond_dim is None else int(model_cond_dim),205        "T": T,206        "beta_1": beta_1,207        "beta_T": beta_T,208        "ch": ch,209        "ch_mult": ch_mult,210        "attn": attn,211        "num_res_blocks": num_res_blocks,212        "dropout": dropout,213        "model_cond_dim": model_cond_dim,214        "trigger_base_ch": int(get_from_config(config, "trigger_base_ch", DEFAULT_TRIGGER_BASE_CH)),215        "trigger_grid_size": int(get_from_config(config, "trigger_grid_size", 8)),216        "trigger_hidden_dim": int(get_from_config(config, "trigger_hidden_dim", 256)),217        "trigger_use_coord_feat": bool(get_from_config(config, "trigger_use_coord_feat", True)),218    }219 220 221def load_state_by_keys(module, ckpt, keys, ckpt_path, what):222    for key in keys:223        if key in ckpt:224            module.load_state_dict(ckpt[key], strict=True)225            return key226 227    try:228        module.load_state_dict(ckpt, strict=True)229        return "<ckpt_is_state_dict>"230    except Exception:231        pass232 233    raise KeyError(f"{ckpt_path} 找不到 {what} 權重,已嘗試 keys={keys}")234 235 236def load_sampler_from_ckpt(ckpt_path, device):237    ckpt = torch.load(ckpt_path, map_location="cpu")238    params = infer_model_params_from_ckpt(ckpt)239 240    net = UNet(241        T=params["T"],242        ch=params["ch"],243        ch_mult=params["ch_mult"],244        attn=params["attn"],245        num_res_blocks=params["num_res_blocks"],246        dropout=params["dropout"],247        cond_dim=params["model_cond_dim"],248    ).to(device)249 250    model_key = load_state_by_keys(251        module=net,252        ckpt=ckpt,253        keys=MODEL_KEYS,254        ckpt_path=ckpt_path,255        what="UNet",256    )257 258    net.eval()259 260    sampler = GaussianDiffusionSampler(261        net,262        params["beta_1"],263        params["beta_T"],264        params["T"],265        params["img_size"],266        "epsilon",267        "fixedlarge",268    ).to(device)269 270    sampler.eval()271 272    return {273        "ckpt": ckpt,274        "params": params,275        "sampler": sampler,276        "model_key": model_key,277    }278 279 280def build_trigger_encoder_from_params(params, device):281    if params["cond_dim"] is None:282        raise ValueError("unconditional model has no trigger encoder")283 284    encoder = TriggerEncoder(285        in_ch=3,286        base_ch=params["trigger_base_ch"],287        cond_dim=params["cond_dim"],288        img_size=params["img_size"],289        grid_size=params["trigger_grid_size"],290        hidden_dim=params["trigger_hidden_dim"],291        use_coord_feat=params["trigger_use_coord_feat"],292    ).to(device)293 294    encoder.eval()295    return encoder296 297 298def load_cond_pack(ckpt_path, device):299    pack = load_sampler_from_ckpt(ckpt_path, device)300 301    if pack["params"]["mode"] == "unconditional":302        raise ValueError("COND_CKPT_PATH should not be unconditional.")303 304    encoder = build_trigger_encoder_from_params(pack["params"], device)305 306    encoder_key = load_state_by_keys(307        module=encoder,308        ckpt=pack["ckpt"],309        keys=ENCODER_KEYS,310        ckpt_path=ckpt_path,311        what="TriggerEncoder",312    )313 314    pack["trigger_encoder"] = encoder315    pack["encoder_key"] = encoder_key316 317    return pack318 319 320def encode_trigger(trigger_encoder, trigger_tensor, batch_size, device):321    x = trigger_tensor322 323    if x.dim() == 3:324        x = x.unsqueeze(0)325 326    if x.size(0) == 1:327        x = x.repeat(batch_size, 1, 1, 1)328 329    with torch.no_grad():330        return trigger_encoder(x.to(device))331 332 333def zero_cond(batch_size, cond_dim, device):334    return torch.zeros(batch_size, cond_dim, device=device)335 336 337def build_xT_batch(num_samples, img_size, device):338    xs = []339    seeds = []340 341    for i in range(num_samples):342        seed = XT_SEED + i343        gen = torch.Generator(device=device)344        gen.manual_seed(seed)345        x = torch.randn((1, 3, img_size, img_size), generator=gen, device=device)346        xs.append(x)347        seeds.append(seed)348 349    return torch.cat(xs, dim=0), seeds350 351 352def sample_batch(sampler, x_T, cond, params):353    with torch.no_grad():354        if params["mode"] == "unconditional":355            out = sampler(x_T, 0, params["T"])356        else:357            out = sampler(x_T, 0, params["T"], cond=cond)358 359    return denorm_to_01(out).detach().cpu()360 361 362def get_correct_triggers(cond_ckpt, img_size, device):363    triggers = []364    sources = []365 366    if "client_triggers" in cond_ckpt:367        client_triggers = cond_ckpt["client_triggers"]368 369        for cid in CORRECT_CLIENT_IDS:370            if cid >= len(client_triggers):371                raise IndexError(f"CLIENT_ID={cid}, but ckpt only has {len(client_triggers)} client_triggers")372 373            trig = client_triggers[cid].clone().float().to(device)374            triggers.append(trig)375            sources.append(f"ckpt.client_triggers[{cid}]")376 377        return triggers, sources378 379    for cid in CORRECT_CLIENT_IDS:380        path = os.path.join(FALLBACK_TRIGGER_DIR, f"client_{cid}.png")381        if not os.path.exists(path):382            raise FileNotFoundError(f"Cannot find fallback trigger: {path}")383 384        triggers.append(read_image_as_tensor(path, img_size, device=device))385        sources.append(path)386 387    return triggers, sources388 389 390# ============================================================391# 4. synthetic embedding generation392# ============================================================393 394def build_correct_embeddings(trigger_encoder, correct_triggers, device):395    correct_embs = OrderedDict()396 397    for cid, trig in zip(CORRECT_CLIENT_IDS, correct_triggers):398        emb = encode_trigger(trigger_encoder, trig, 1, device).detach()399        correct_embs[f"correct_client{cid}"] = emb400 401    return correct_embs402 403 404def normalize_direction(direction, eps=1e-12):405    norm = torch.norm(direction, dim=1, keepdim=True).clamp_min(eps)406    return direction / norm407 408 409def make_synthetic_embedding(center_emb, target_l2, seed, device):410    """411    center_emb: [1, cond_dim]412    target_l2: float413 414    回傳:415      synthetic_emb: [1, cond_dim]416      actual_l2: float417      direction_seed: int418    """419    if target_l2 == 0:420        return center_emb.detach().clone(), 0.0, seed421 422    gen = torch.Generator(device=device)423    gen.manual_seed(seed)424 425    direction = torch.randn(center_emb.shape, generator=gen, device=device)426    direction = normalize_direction(direction)427 428    synthetic_emb = center_emb + direction * float(target_l2)429    actual_l2 = torch.norm(synthetic_emb - center_emb).item()430 431    return synthetic_emb.detach(), float(actual_l2), seed432 433 434def repeat_cond(cond_1, batch_size):435    if cond_1.size(0) == batch_size:436        return cond_1437    return cond_1.repeat(batch_size, 1)438 439 440# ============================================================441# 5. metrics442# ============================================================443 444def build_metrics(device):445    metrics = {}446 447    if not TORCHMETRICS_OK:448        print("[WARN] torchmetrics import failed. SSIM / LPIPS will be nan.")449        print(TORCHMETRICS_IMPORT_ERROR)450        return metrics451 452    metrics["psnr"] = PeakSignalNoiseRatio(data_range=1.0).to(device).eval()453    metrics["ssim"] = StructuralSimilarityIndexMeasure(data_range=1.0).to(device).eval()454    metrics["lpips"] = LearnedPerceptualImagePatchSimilarity(net_type="alex").to(device).eval()455 456    return metrics457 458 459def psnr_from_mse(mse):460    if mse <= 1e-12:461        return float("inf")462    return float(10.0 * np.log10(1.0 / mse))463 464 465def compute_pair_metrics(pred_01, ref_01, metrics, device):466    pred = pred_01.unsqueeze(0).to(device).clamp(0, 1)467    ref = ref_01.unsqueeze(0).to(device).clamp(0, 1)468 469    mse = float(F.mse_loss(pred, ref).item())470 471    out = {472        "mse": mse,473        "psnr": psnr_from_mse(mse),474        "ssim": float("nan"),475        "lpips": float("nan"),476    }477 478    if "psnr" in metrics:479        out["psnr"] = float(metrics["psnr"](pred, ref).item())480 481    if "ssim" in metrics:482        out["ssim"] = float(metrics["ssim"](pred, ref).item())483 484    if "lpips" in metrics:485        pred_lp = (pred * 2.0 - 1.0).clamp(-1, 1)486        ref_lp = (ref * 2.0 - 1.0).clamp(-1, 1)487        out["lpips"] = float(metrics["lpips"](pred_lp, ref_lp).item())488 489    return out490 491 492def compute_list_metrics(pred_list, ref_list, metrics, device):493    assert len(pred_list) == len(ref_list)494 495    detail = []496 497    for i, (pred, ref) in enumerate(zip(pred_list, ref_list)):498        m = compute_pair_metrics(pred, ref, metrics, device)499        m["sample_idx"] = i500        detail.append(m)501 502    summary = {}503 504    for key in ["mse", "psnr", "ssim", "lpips"]:505        vals = np.array([x[key] for x in detail], dtype=np.float64)506        summary[f"mean_{key}"] = float(np.nanmean(vals))507        summary[f"std_{key}"] = float(np.nanstd(vals))508 509    return detail, summary510 511 512# ============================================================513# 6. image rows514# ============================================================515 516def make_labeled_row_image(label, imgs_01, scale=4, label_w=240):517    font = ImageFont.load_default()518 519    img_w = imgs_01[0].shape[-1] * scale520    img_h = imgs_01[0].shape[-2] * scale521    n = len(imgs_01)522 523    canvas_w = label_w + n * img_w + (n + 1) * PAD524    canvas_h = img_h + 2 * PAD525 526    canvas = Image.new("RGB", (canvas_w, canvas_h), "white")527    draw = ImageDraw.Draw(canvas)528 529    draw.text((8, canvas_h // 2 - 6), label, fill="black", font=font)530 531    x = label_w + PAD532    y = PAD533 534    for img_01 in imgs_01:535        pil = tensor_to_pil_01(img_01, scale=scale)536        canvas.paste(pil, (x, y))537        x += img_w + PAD538 539    return canvas540 541 542def save_row_image(label, imgs_01, path, scale=4):543    canvas = make_labeled_row_image(544        label=label,545        imgs_01=imgs_01,546        scale=scale,547        label_w=LABEL_W,548    )549    canvas.save(path)550 551 552def save_final_canvas(rows, path):553    row_images = []554 555    for label, imgs_01 in rows:556        row_img = make_labeled_row_image(557            label=label,558            imgs_01=imgs_01,559            scale=FINAL_SCALE,560            label_w=LABEL_W,561        )562        row_images.append(row_img)563 564    w = max(img.width for img in row_images)565    h = sum(img.height for img in row_images)566 567    canvas = Image.new("RGB", (w, h), "white")568 569    y = 0570    for img in row_images:571        canvas.paste(img, (0, y))572        y += img.height573 574    canvas.save(path)575 576 577# ============================================================578# 7. plots579# ============================================================580 581def save_line_plots(summary_csv_path, save_dir):582    try:583        import pandas as pd584        import matplotlib.pyplot as plt585    except Exception as e:586        print("[WARN] cannot plot because pandas/matplotlib import failed:", repr(e))587        return588 589    df = pd.read_csv(summary_csv_path)590 591    sub = df[592        (df["kind"] == "synthetic_embedding") &593        (df["comparison"] == "vs_own_correct")594    ].copy()595 596    if len(sub) == 0:597        return598 599    # plot per client average600    for metric in ["mean_mse", "mean_psnr", "mean_ssim", "mean_lpips"]:601        plt.figure(figsize=(8, 5))602 603        for center_name in sorted(sub["center_name"].unique()):604            cdf = sub[sub["center_name"] == center_name]605            grouped = cdf.groupby("target_l2")[metric].mean().reset_index()606            plt.plot(grouped["target_l2"], grouped[metric], marker="o", label=center_name)607 608        plt.xlabel("target_l2")609        plt.ylabel(metric)610        plt.title(f"L2 sensitivity | {metric} vs own correct")611        plt.grid(True)612        plt.legend()613 614        save_path = os.path.join(save_dir, f"l2_vs_{metric}_per_client.png")615        plt.savefig(save_path, dpi=200, bbox_inches="tight")616        plt.close()617        print("saved plot:", save_path)618 619    # all-client average620    for metric in ["mean_mse", "mean_psnr", "mean_ssim", "mean_lpips"]:621        grouped = sub.groupby("target_l2")[metric].agg(["mean", "std"]).reset_index()622 623        plt.figure(figsize=(8, 5))624        plt.errorbar(625            grouped["target_l2"],626            grouped["mean"],627            yerr=grouped["std"],628            marker="o",629            capsize=3,630        )631        plt.xlabel("target_l2")632        plt.ylabel(metric)633        plt.title(f"L2 sensitivity | all clients avg | {metric}")634        plt.grid(True)635 636        save_path = os.path.join(save_dir, f"l2_vs_{metric}_all_clients_avg.png")637        plt.savefig(save_path, dpi=200, bbox_inches="tight")638        plt.close()639        print("saved plot:", save_path)640 641 642# ============================================================643# 8. main644# ============================================================645 646def main():647    print("=== eval_embedding_sensitivity_l2.py ===")648    print("DEVICE =", DEVICE)649    print("SAVE_DIR =", SAVE_DIR)650    print("BASELINE_CKPT_PATH =", BASELINE_CKPT_PATH)651    print("COND_CKPT_PATH =", COND_CKPT_PATH)652    print("L2_VALUES =", L2_VALUES)653    print("NUM_DIRECTIONS_PER_L2 =", NUM_DIRECTIONS_PER_L2)654    print("NUM_SAMPLES_PER_EMB =", NUM_SAMPLES_PER_EMB)655 656    reset_dir(SAVE_DIR)657 658    rows_dir = os.path.join(SAVE_DIR, "rows")659    ensure_dir(rows_dir)660 661    plots_dir = os.path.join(SAVE_DIR, "plots")662    ensure_dir(plots_dir)663 664    summary_csv = os.path.join(SAVE_DIR, "metrics_summary.csv")665    detail_csv = os.path.join(SAVE_DIR, "metrics_detail.csv")666    report_path = os.path.join(SAVE_DIR, "report.txt")667 668    baseline_pack = load_sampler_from_ckpt(BASELINE_CKPT_PATH, DEVICE)669    cond_pack = load_cond_pack(COND_CKPT_PATH, DEVICE)670 671    baseline_params = baseline_pack["params"]672    cond_params = cond_pack["params"]673 674    img_size = cond_params["img_size"]675    cond_dim = cond_params["cond_dim"]676 677    if cond_dim is None:678        raise ValueError("conditional model cond_dim is None")679 680    if baseline_params["img_size"] != img_size:681        raise ValueError("baseline img_size != cond img_size")682 683    correct_triggers, correct_sources = get_correct_triggers(684        cond_pack["ckpt"],685        img_size,686        DEVICE,687    )688 689    correct_embs = build_correct_embeddings(690        cond_pack["trigger_encoder"],691        correct_triggers,692        DEVICE,693    )694 695    x_T, seed_list = build_xT_batch(NUM_SAMPLES_PER_EMB, img_size, DEVICE)696 697    if baseline_params["mode"] == "unconditional":698        baseline_cond = None699    else:700        baseline_cond = zero_cond(NUM_SAMPLES_PER_EMB, baseline_params["cond_dim"], DEVICE)701 702    cond_none = zero_cond(NUM_SAMPLES_PER_EMB, cond_dim, DEVICE)703 704    metrics = build_metrics(DEVICE)705 706    # --------------------------------------------------------707    # baseline708    # --------------------------------------------------------709    print("sampling baseline...")710    baseline_imgs = sample_batch(711        baseline_pack["sampler"],712        x_T,713        baseline_cond,714        baseline_params,715    )716    baseline_list = [baseline_imgs[i] for i in range(baseline_imgs.size(0))]717 718    # --------------------------------------------------------719    # correct outputs720    # --------------------------------------------------------721    correct_outputs = OrderedDict()722    final_rows = []723 724    final_rows.append(("baseline", baseline_list))725    if SAVE_ROW_IMAGES:726        save_row_image(727            "baseline",728            baseline_list,729            os.path.join(rows_dir, "000_baseline.png"),730            scale=ROW_SCALE,731        )732 733    row_counter = 1734 735    for center_name, center_emb in correct_embs.items():736        print(f"sampling {center_name}...")737 738        cond = repeat_cond(center_emb, NUM_SAMPLES_PER_EMB)739        imgs = sample_batch(740            cond_pack["sampler"],741            x_T,742            cond,743            cond_params,744        )745 746        img_list = [imgs[i] for i in range(imgs.size(0))]747        correct_outputs[center_name] = img_list748 749        final_rows.append((center_name, img_list))750 751        if SAVE_ROW_IMAGES:752            save_row_image(753                center_name,754                img_list,755                os.path.join(rows_dir, f"{row_counter:03d}_{center_name}.png"),756                scale=ROW_SCALE,757            )758 759        row_counter += 1760 761    # --------------------------------------------------------762    # none763    # --------------------------------------------------------764    print("sampling none...")765    none_imgs = sample_batch(766        cond_pack["sampler"],767        x_T,768        cond_none,769        cond_params,770    )771    none_list = [none_imgs[i] for i in range(none_imgs.size(0))]772 773    final_rows.append(("none", none_list))774 775    if SAVE_ROW_IMAGES:776        save_row_image(777            "none",778            none_list,779            os.path.join(rows_dir, f"{row_counter:03d}_none.png"),780            scale=ROW_SCALE,781        )782 783    row_counter += 1784 785    # --------------------------------------------------------786    # report header787    # --------------------------------------------------------788    with open(report_path, "w", encoding="utf-8") as f:789        f.write("embedding-space L2 sensitivity evaluation report\n")790        f.write(f"CKPT_NAME = {CKPT_NAME}\n")791        f.write(f"BASELINE_CKPT_PATH = {BASELINE_CKPT_PATH}\n")792        f.write(f"COND_CKPT_PATH = {COND_CKPT_PATH}\n")793        f.write(f"baseline_mode = {baseline_params['mode']}\n")794        f.write(f"cond_mode = {cond_params['mode']}\n")795        f.write(f"baseline_model_key = {baseline_pack['model_key']}\n")796        f.write(f"cond_model_key = {cond_pack['model_key']}\n")797        f.write(f"cond_encoder_key = {cond_pack['encoder_key']}\n")798        f.write(f"correct_sources = {correct_sources}\n")799        f.write(f"NUM_SAMPLES_PER_EMB = {NUM_SAMPLES_PER_EMB}\n")800        f.write(f"XT_SEED = {XT_SEED}\n")801        f.write(f"seed_list = {seed_list}\n")802        f.write(f"L2_VALUES = {L2_VALUES}\n")803        f.write(f"NUM_DIRECTIONS_PER_L2 = {NUM_DIRECTIONS_PER_L2}\n")804        f.write(f"DIRECTION_SEED = {DIRECTION_SEED}\n")805        f.write(f"SAVE_ROW_IMAGES = {SAVE_ROW_IMAGES}\n")806 807        if not TORCHMETRICS_OK:808            f.write(f"TORCHMETRICS_IMPORT_ERROR = {TORCHMETRICS_IMPORT_ERROR}\n")809 810        f.write("\n")811 812    # --------------------------------------------------------813    # synthetic embeddings814    # --------------------------------------------------------815    summaries_for_report = []816 817    for center_idx, (center_name, center_emb) in enumerate(correct_embs.items()):818        own_correct_list = correct_outputs[center_name]819 820        for target_l2 in L2_VALUES:821            if target_l2 == 0:822                direction_indices = [0]823            else:824                direction_indices = list(range(NUM_DIRECTIONS_PER_L2))825 826            for dir_idx in direction_indices:827                direction_seed = DIRECTION_SEED + center_idx * 100000 + int(target_l2 * 1000) + dir_idx828 829                synth_emb, actual_l2, used_seed = make_synthetic_embedding(830                    center_emb=center_emb,831                    target_l2=target_l2,832                    seed=direction_seed,833                    device=DEVICE,834                )835 836                cond = repeat_cond(synth_emb, NUM_SAMPLES_PER_EMB)837 838                label = f"{center_name}_L2{target_l2}_d{dir_idx}"839                print(f"sampling {label}: actual_l2={actual_l2:.6f}")840 841                imgs = sample_batch(842                    cond_pack["sampler"],843                    x_T,844                    cond,845                    cond_params,846                )847 848                img_list = [imgs[i] for i in range(imgs.size(0))]849                final_rows.append((label, img_list))850 851                if SAVE_ROW_IMAGES:852                    save_row_image(853                        label,854                        img_list,855                        os.path.join(rows_dir, f"{row_counter:03d}_{label}.png"),856                        scale=ROW_SCALE,857                    )858 859                row_counter += 1860 861                # -------------------------862                # metrics: vs own correct863                # -------------------------864                detail_rows, summary = compute_list_metrics(865                    img_list,866                    own_correct_list,867                    metrics,868                    DEVICE,869                )870 871                append_csv_row(summary_csv, {872                    "ckpt": CKPT_NAME,873                    "kind": "synthetic_embedding",874                    "center_name": center_name,875                    "target_l2": target_l2,876                    "direction_idx": dir_idx,877                    "direction_seed": used_seed,878                    "actual_l2": actual_l2,879                    "comparison": "vs_own_correct",880                    "mean_mse": summary["mean_mse"],881                    "std_mse": summary["std_mse"],882                    "mean_psnr": summary["mean_psnr"],883                    "std_psnr": summary["std_psnr"],884                    "mean_ssim": summary["mean_ssim"],885                    "std_ssim": summary["std_ssim"],886                    "mean_lpips": summary["mean_lpips"],887                    "std_lpips": summary["std_lpips"],888                })889 890                summaries_for_report.append({891                    "center_name": center_name,892                    "target_l2": target_l2,893                    "comparison": "vs_own_correct",894                    **summary,895                })896 897                for row in detail_rows:898                    append_csv_row(detail_csv, {899                        "ckpt": CKPT_NAME,900                        "kind": "synthetic_embedding",901                        "center_name": center_name,902                        "target_l2": target_l2,903                        "direction_idx": dir_idx,904                        "direction_seed": used_seed,905                        "actual_l2": actual_l2,906                        "comparison": "vs_own_correct",907                        "sample_idx": row["sample_idx"],908                        "mse": row["mse"],909                        "psnr": row["psnr"],910                        "ssim": row["ssim"],911                        "lpips": row["lpips"],912                    })913 914                # -------------------------915                # metrics: vs baseline916                # -------------------------917                detail_rows, summary_base = compute_list_metrics(918                    img_list,919                    baseline_list,920                    metrics,921                    DEVICE,922                )923 924                append_csv_row(summary_csv, {925                    "ckpt": CKPT_NAME,926                    "kind": "synthetic_embedding",927                    "center_name": center_name,928                    "target_l2": target_l2,929                    "direction_idx": dir_idx,930                    "direction_seed": used_seed,931                    "actual_l2": actual_l2,932                    "comparison": "vs_baseline",933                    "mean_mse": summary_base["mean_mse"],934                    "std_mse": summary_base["std_mse"],935                    "mean_psnr": summary_base["mean_psnr"],936                    "std_psnr": summary_base["std_psnr"],937                    "mean_ssim": summary_base["mean_ssim"],938                    "std_ssim": summary_base["std_ssim"],939                    "mean_lpips": summary_base["mean_lpips"],940                    "std_lpips": summary_base["std_lpips"],941                })942 943                for row in detail_rows:944                    append_csv_row(detail_csv, {945                        "ckpt": CKPT_NAME,946                        "kind": "synthetic_embedding",947                        "center_name": center_name,948                        "target_l2": target_l2,949                        "direction_idx": dir_idx,950                        "direction_seed": used_seed,951                        "actual_l2": actual_l2,952                        "comparison": "vs_baseline",953                        "sample_idx": row["sample_idx"],954                        "mse": row["mse"],955                        "psnr": row["psnr"],956                        "ssim": row["ssim"],957                        "lpips": row["lpips"],958                    })959 960                # -------------------------961                # metrics: vs none962                # -------------------------963                detail_rows, summary_none = compute_list_metrics(964                    img_list,965                    none_list,966                    metrics,967                    DEVICE,968                )969 970                append_csv_row(summary_csv, {971                    "ckpt": CKPT_NAME,972                    "kind": "synthetic_embedding",973                    "center_name": center_name,974                    "target_l2": target_l2,975                    "direction_idx": dir_idx,976                    "direction_seed": used_seed,977                    "actual_l2": actual_l2,978                    "comparison": "vs_none",979                    "mean_mse": summary_none["mean_mse"],980                    "std_mse": summary_none["std_mse"],981                    "mean_psnr": summary_none["mean_psnr"],982                    "std_psnr": summary_none["std_psnr"],983                    "mean_ssim": summary_none["mean_ssim"],984                    "std_ssim": summary_none["std_ssim"],985                    "mean_lpips": summary_none["mean_lpips"],986                    "std_lpips": summary_none["std_lpips"],987                })988 989                for row in detail_rows:990                    append_csv_row(detail_csv, {991                        "ckpt": CKPT_NAME,992                        "kind": "synthetic_embedding",993                        "center_name": center_name,994                        "target_l2": target_l2,995                        "direction_idx": dir_idx,996                        "direction_seed": used_seed,997                        "actual_l2": actual_l2,998                        "comparison": "vs_none",999                        "sample_idx": row["sample_idx"],1000                        "mse": row["mse"],1001                        "psnr": row["psnr"],1002                        "ssim": row["ssim"],1003                        "lpips": row["lpips"],1004                    })1005 1006    # --------------------------------------------------------1007    # final image1008    # --------------------------------------------------------1009    if SAVE_FINAL_IMAGE:1010        save_final_canvas(1011            final_rows,1012            os.path.join(SAVE_DIR, "final_embedding_sensitivity_l2.png"),1013        )1014 1015    # --------------------------------------------------------1016    # report summary1017    # --------------------------------------------------------1018    with open(report_path, "a", encoding="utf-8") as f:1019        f.write("\nSUMMARY, comparison = vs_own_correct\n")1020 1021        for target_l2 in L2_VALUES:1022            rows = [1023                s for s in summaries_for_report1024                if s["target_l2"] == target_l2 and s["comparison"] == "vs_own_correct"1025            ]1026 1027            if len(rows) == 0:1028                continue1029 1030            f.write(f"[L2={target_l2}]\n")1031            for key in ["mean_mse", "mean_psnr", "mean_ssim", "mean_lpips"]:1032                vals = np.array([r[key] for r in rows], dtype=np.float64)1033                f.write(f"  avg {key} = {np.nanmean(vals):.6f} ± {np.nanstd(vals):.6f}\n")1034 1035    save_line_plots(summary_csv, plots_dir)1036 1037    print("\ndone")1038    print("SAVE_DIR:", SAVE_DIR)1039    print("rows dir:", rows_dir)1040    print("summary csv:", summary_csv)1041    print("detail csv:", detail_csv)1042    print("report:", report_path)1043    print("final image:", os.path.join(SAVE_DIR, "final_embedding_sensitivity_l2.png"))1044 1045 1046if __name__ == "__main__":1047    main()