CoolFace
Apppublic

Dreanhunter30/Light_Multimodal_Learning_The_SpectralMix_Head

sourceHugging Faceapache-2.0updated 1y agoView on Hugging Face
2likes
app.py472 linesDownload Raw Back to root
1# app.py — SpectralMix Super UI: One Head, Two Modalities + Simulator2# HF Spaces-friendly, CPU-ready (Gradio 4.x)3 4import io5import json6import platform7from pathlib import Path8 9import gradio as gr10import numpy as np11import torch12from PIL import Image13import matplotlib.pyplot as plt14 15# -------- local imports (должны быть в проекте) --------16# infer.py обязан экспонировать: load_classes, load_head, predict_image, routing_for_text17from infer import load_classes, load_head, predict_image, routing_for_text18 19# ---------------- Config ----------------20ROOT = Path(".")21CLASS_CANDIDATES = [ROOT / "classes.json", ROOT / "классы.json"]22WEIGHT_CANDIDATES = [ROOT / "head.pt", ROOT / "weights.pt", ROOT / "голова.pt", ROOT / "веса.pt"]23TAU_DEFAULT = 0.5  # стартовая температура (меняется слайдером)24 25# ---------------- Utilities ----------------26def pick_first_existing(paths):27    for p in paths:28        if p.exists():29            return p30    return None31 32def load_classes_fallback():33    p = pick_first_existing(CLASS_CANDIDATES)34    if p:35        try:36            return load_classes(str(p))37        except Exception:38            pass39    # fallback: try meta.json (root or 'artifacts'), else CIFAR-10 defaults40    for mp in [ROOT / "meta.json", ROOT / "artifacts" / "meta.json"]:41        if mp.exists():42            try:43                with open(mp, "r", encoding="utf-8") as f:44                    meta = json.load(f)45                    if isinstance(meta.get("classes"), list):46                        return meta["classes"]47            except Exception:48                pass49    return ["airplane","automobile","bird","cat","deer","dog","frog","horse","ship","truck"]50 51# Classes52CLASSES = load_classes_fallback()53 54# Weights (root only)55_weight_path = pick_first_existing(WEIGHT_CANDIDATES)56if _weight_path is None:57    print("[error] Model weights not found in repo root. Expected one of:",58          ", ".join([p.name for p in WEIGHT_CANDIDATES]))59 60# Загружаем голову (и переводим в eval)61HEAD = None62if _weight_path is not None:63    HEAD = load_head(str(_weight_path), num_classes=len(CLASSES))64    if hasattr(HEAD, "eval"):65        HEAD.eval()66    print(f"[info] Using local weights: {_weight_path}")67 68# ---------------- Plotting helpers (matplotlib; no custom colors) ----------------69def _fig_to_pil(fig):70    buf = io.BytesIO()71    fig.savefig(buf, format="png", bbox_inches="tight")72    plt.close(fig)73    buf.seek(0)74    return Image.open(buf)75 76def plot_topk(labels, probs, title="Top-5 classes"):77    fig = plt.figure()78    if labels and probs:79        x = list(range(len(labels)))80        plt.bar(x, probs)81        plt.xticks(x, labels, rotation=30, ha="right")82    plt.ylabel("Probability")83    plt.title(title)84    return _fig_to_pil(fig)85 86def plot_expert_bars(vec, title="Expert activations (predicted class)"):87    vec = np.asarray(vec, dtype=np.float64)88    fig = plt.figure()89    x = list(range(len(vec)))90    plt.bar(x, vec)91    plt.xticks(x, [f"E{i}" for i in x], rotation=0)92    plt.ylabel("Routing weight")93    plt.title(title)94    return _fig_to_pil(fig)95 96def plot_routing_heatmap(routing, class_labels, title="Routing heatmap (classes × experts)"):97    arr = np.array(routing, dtype=np.float32)98    fig = plt.figure()99    im = plt.imshow(arr, aspect="auto", vmin=0.0, vmax=1.0)100    plt.colorbar(im)101    plt.yticks(range(len(class_labels)), class_labels)102    plt.xlabel("Experts")103    plt.ylabel("Classes")104    plt.title(title)105    return _fig_to_pil(fig)106 107def _softmax_np(x, tau=1.0):108    x = np.asarray(x, dtype=np.float64) / max(tau, 1e-12)109    x = x - x.max()110    ex = np.exp(x)111    s = ex.sum()112    return (ex / (s if s > 0 else 1.0)).astype(np.float64)113 114def cosine_sim(a, b, eps=1e-12):115    a = np.asarray(a, dtype=np.float64)116    b = np.asarray(b, dtype=np.float64)117    na = np.linalg.norm(a)118    nb = np.linalg.norm(b)119    if na < eps or nb < eps:120        return 0.0121    return float(np.dot(a, b) / (na * nb))122 123# ---------------- Inference wrappers ----------------124def _ensure_ready():125    if HEAD is None:126        raise gr.Error(127            "Model weights not found in repo root. "128            "Place `head.pt` (или `weights.pt` / `голова.pt` / `веса.pt`) рядом с app.py."129        )130 131def _preprocess_image(img: Image.Image) -> Image.Image:132    if img is None:133        raise gr.Error("Upload an image.")134    img = img.convert("RGB")135    # лёгкий ресайз, чтобы не ронять CPU136    max_side = max(img.size)137    if max_side > 1024:138        scale = 1024.0 / max_side139        img = img.resize((int(img.width * scale), int(img.height * scale)))140    return img141 142@torch.no_grad()143def run_image(img: Image.Image, tau: float):144    _ensure_ready()145    img = _preprocess_image(img)146    # Предсказание (детерминированно, без Gumbel-шума — в infer.py выключен шум на eval)147    idxs, vals, routing = predict_image(HEAD, img, CLASSES, tau=float(tau))  # routing [K,R]148    if len(idxs) == 0:149        empty = {"class": [], "prob": []}150        blank = plot_topk([], [], title="Top-5 (image)")151        return empty, blank, blank, blank, "No prediction."152    labels = [CLASSES[i] for i in idxs]153    probs = [float(v) for v in vals]154    pred_idx = int(idxs[0])155    expert_vec = routing[pred_idx].tolist()156 157    top5_plot = plot_topk(labels, probs, title="Top-5 (image)")158    experts_plot = plot_expert_bars(expert_vec, title="Experts (image, predicted class)")159    heatmap_plot = plot_routing_heatmap(routing, CLASSES, title="Routing heatmap (image)")160 161    top_ex = torch.topk(torch.tensor(expert_vec), k=min(5, len(expert_vec)))162    details = (163        f"Predicted: {CLASSES[pred_idx]}  |  τ={tau:.3f}\n"164        f"Top experts (idx:weight): " +165        ", ".join([f"{int(i)}:{float(w):.3f}" for i, w in zip(top_ex.indices.tolist(), top_ex.values.tolist())]) +166        f"\nSum(weights)={float(np.sum(expert_vec)):.3f}"167    )168    return {"class": labels, "prob": probs}, top5_plot, experts_plot, heatmap_plot, details169 170@torch.no_grad()171def run_text(text: str, tau: float):172    _ensure_ready()173    if not text or not text.strip():174        empty = {"class": [], "prob": []}175        blank = plot_topk([], [], title="Top-5 (text)")176        return empty, blank, blank, blank, "Enter a non-empty text."177    probs, routing = routing_for_text(HEAD, text, tau=float(tau))  # probs [K], routing [K,R]178    if probs is None or routing is None:179        blank = plot_topk([], [], title="Top-5 (text)")180        return {"class": [], "prob": []}, blank, blank, blank, "No output from text pipeline."181    top = torch.topk(probs, k=min(5, len(CLASSES)))182    labels = [CLASSES[i] for i in top.indices.tolist()]183    pvals = [float(v) for v in top.values.tolist()]184    pred_idx = int(torch.argmax(probs).item())185    expert_vec = routing[pred_idx].tolist()186 187    top5_plot = plot_topk(labels, pvals, title="Top-5 (text)")188    experts_plot = plot_expert_bars(expert_vec, title="Experts (text, predicted class)")189    heatmap_plot = plot_routing_heatmap(routing, CLASSES, title="Routing heatmap (text)")190 191    top_ex = torch.topk(torch.tensor(expert_vec), k=min(5, len(expert_vec)))192    details = (193        f"Predicted: {CLASSES[pred_idx]}  |  τ={tau:.3f}\n"194        f"Top experts (idx:weight): " +195        ", ".join([f"{int(i)}:{float(w):.3f}" for i, w in zip(top_ex.indices.tolist(), top_ex.values.tolist())]) +196        f"\nSum(weights)={float(np.sum(expert_vec)):.3f}"197    )198    return {"class": labels, "prob": pvals}, top5_plot, experts_plot, heatmap_plot, details199 200@torch.no_grad()201def run_compare(img: Image.Image, text: str, tau: float):202    _ensure_ready()203    if img is None or not text or not text.strip():204        blank = plot_topk([], [], title="Top-5")205        return blank, blank, "Provide both image and text."206    img = _preprocess_image(img)207    # image208    idxs_i, _, routing_i = predict_image(HEAD, img, CLASSES, tau=float(tau))209    if len(idxs_i) == 0:210        blank = plot_topk([], [], title="Top-5")211        return blank, blank, "No image prediction."212    pred_i = int(idxs_i[0])213    vec_i = routing_i[pred_i].tolist()214    # text215    probs_t, routing_t = routing_for_text(HEAD, text, tau=float(tau))216    if probs_t is None:217        blank = plot_topk([], [], title="Top-5")218        return blank, blank, "No text prediction."219    pred_t = int(torch.argmax(probs_t).item())220    vec_t = routing_t[pred_t].tolist()221 222    sim = cosine_sim(vec_i, vec_t)223    bar_i = plot_expert_bars(vec_i, title=f"Experts (image → {CLASSES[pred_i]})")224    bar_t = plot_expert_bars(vec_t, title=f"Experts (text  → {CLASSES[pred_t]})")225    info = (226        f"Predicted (image): {CLASSES[pred_i]} | Predicted (text): {CLASSES[pred_t]}\n"227        f"Cosine similarity of expert vectors: {sim:.3f}  (1.0 = identical, 0 = orthogonal)  |  τ={tau:.3f}"228    )229    return bar_i, bar_t, info230 231# ---------------- Simulator (how the head works) ----------------232_rng_global = np.random.default_rng(12345)233 234def _gumbel_softmax_np(logits, tau=1.0, hard=False, rng=None):235    rng = rng or _rng_global236    U = rng.uniform(low=1e-8, high=1-1e-8, size=len(logits))237    g = -np.log(-np.log(U))238    y = _softmax_np(np.asarray(logits) + g, tau=tau)239    if hard:240        hard_vec = np.zeros_like(y)241        hard_vec[int(np.argmax(y))] = 1.0242        return hard_vec243    return y244 245def simulate_mix(R=4, C=10, tau=0.5, hard=False, couple=0.5, seed=42, use_gumbel=False):246    """247    Возвращает:248      W: [R, C] — компонентные логиты для классов249      alpha_img, alpha_txt: [R] — смеси экспертов250      z_img, z_txt: [C] — итоговые логиты по классам251    """252    rng = np.random.default_rng(int(seed))253    # компонентные "головы": W_r \in R^{C}254    W = rng.normal(loc=0.0, scale=1.0, size=(R, C)).astype(np.float64)255    # гейтовые логиты256    gate_img = rng.normal(size=R)257    noise = rng.normal(size=R)258    gate_txt = couple * gate_img + (1.0 - couple) * noise259 260    if use_gumbel:261        alpha_img = _gumbel_softmax_np(gate_img, tau=tau, hard=hard, rng=rng)262        alpha_txt = _gumbel_softmax_np(gate_txt, tau=tau, hard=hard, rng=rng)263    else:264        alpha_img = _softmax_np(gate_img, tau=tau)265        alpha_txt = _softmax_np(gate_txt, tau=tau)266        if hard:267            h = np.zeros_like(alpha_img); h[int(np.argmax(alpha_img))] = 1.0; alpha_img = h268            h = np.zeros_like(alpha_txt); h[int(np.argmax(alpha_txt))] = 1.0; alpha_txt = h269 270    z_img = alpha_img @ W271    z_txt = alpha_txt @ W272    return W, alpha_img, alpha_txt, z_img, z_txt273 274def plot_class_logits(logits, class_labels, title):275    probs = _softmax_np(np.asarray(logits), tau=1.0)276    fig = plt.figure()277    x = list(range(len(class_labels)))278    plt.bar(x, probs)279    plt.xticks(x, class_labels, rotation=30, ha="right")280    plt.ylabel("Probability")281    plt.title(title)282    return _fig_to_pil(fig)283 284def plot_W_heatmap(W, title="Component heads W (R × C)"):285    arr = np.asarray(W, dtype=np.float32)286    if arr.shape[1] > 0:287        col_std = arr.std(axis=0, keepdims=True) + 1e-9288        arr = (arr - arr.mean(axis=0, keepdims=True)) / col_std289    fig = plt.figure()290    im = plt.imshow(arr, aspect="auto")291    plt.colorbar(im)292    plt.xlabel("Classes")293    plt.ylabel("Experts")294    plt.title(title)295    return _fig_to_pil(fig)296 297def run_simulator(R, C, tau, couple, seed, hard, use_gumbel):298    # валидные диапазоны299    R = int(max(2, min(16, R)))300    C = int(max(2, min(20, C)))301    # классы для подписи302    if len(CLASSES) >= C:303        cls = CLASSES[:C]304    else:305        cls = (CLASSES + [f"class_{i}" for i in range(C - len(CLASSES))])[:C]306 307    W, a_img, a_txt, z_img, z_txt = simulate_mix(308        R=R, C=C, tau=float(tau), hard=bool(hard),309        couple=float(couple), seed=int(seed), use_gumbel=bool(use_gumbel)310    )311    bars_img = plot_expert_bars(a_img, title="α (image)")312    bars_txt = plot_expert_bars(a_txt, title="α (text)")313    heat_W  = plot_W_heatmap(W, title="Components W (experts × classes)")314    cls_img = plot_class_logits(z_img, cls, title="Mixed logits → probs (image)")315    cls_txt = plot_class_logits(z_txt, cls, title="Mixed logits → probs (text)")316 317    sim = cosine_sim(a_img, a_txt)318    info = (319        f"Cosine(α_image, α_text) = {sim:.3f}  |  τ={tau:.3f} "320        f"| {'Gumbel' if use_gumbel else 'Softmax'} | {'hard' if hard else 'soft'} | couple={couple:.2f}"321    )322    top_ex_img = ", ".join([f"E{i}:{w:.3f}" for i, w in enumerate(a_img)])323    top_ex_txt = ", ".join([f"E{i}:{w:.3f}" for i, w in enumerate(a_txt)])324    details = f"α_image: [{top_ex_img}]\nα_text : [{top_ex_txt}]"325    return bars_img, bars_txt, heat_W, cls_img, cls_txt, info, details326 327# ---------------- UI helpers ----------------328def _env_banner(weights_path: str, classes_len: int):329    import importlib330    pkgs = {}331    try:332        import torchvision as _tv333        pkgs["torchvision"] = _tv.__version__334    except Exception:335        pkgs["torchvision"] = "n/a"336    try:337        import transformers as _tf338        pkgs["transformers"] = _tf.__version__339    except Exception:340        pkgs["transformers"] = "n/a"341    try:342        import open_clip_torch as _oc343        pkgs["open_clip_torch"] = getattr(_oc, "__version__", "present")344    except Exception:345        pkgs["open_clip_torch"] = "n/a"346 347    rows = [348        f"- **torch**: {torch.__version__}",349        f"- **torchvision**: {pkgs['torchvision']}",350        f"- **gradio**: {gr.__version__}",351        f"- **numpy**: {np.__version__}",352        f"- **matplotlib**: {plt.matplotlib.__version__}",353        f"- **transformers**: {pkgs['transformers']}",354        f"- **open_clip_torch**: {pkgs['open_clip_torch']}",355    ]356    sysrow = f"- **Python**: {platform.python_version()}  |  **Device**: {'CUDA' if torch.cuda.is_available() else 'CPU'}"357    wrow = f"**Using weights**: `{weights_path}`  |  **#classes**: {classes_len}"358    return wrow + "<br>" + sysrow + "<br>" + "<br>".join(rows)359 360# ---------------- UI ----------------361with gr.Blocks(title="SpectralMix — One Head, Two Modalities (Image/Text)") as demo:362    gr.HTML("""363    <style>.notranslate { translate: no; }</style>364    <div class="notranslate" style="font-size:28px; font-weight:700;">🧠 SpectralMix — One Head, Two Modalities</div>365    <div>One classifier head for <i>image</i> and <i>text</i>. We mix a few <b>orthogonal experts</b> with (Gumbel-)Softmax.<br>366    Same class ⇒ same expert (similarity≈1). Different classes ⇒ different experts (≈0). This avoids catastrophic forgetting across stages.</div>367    """)368    gr.Markdown(_env_banner(str(_weight_path) if _weight_path else "N/A", len(CLASSES)))369 370    # Общая температура для инференса371    tau_slider = gr.Slider(0.1, 2.0, value=TAU_DEFAULT, step=0.05, label="Temperature τ (inference)")372 373    with gr.Tab("Image → classes & experts"):374        with gr.Row():375            img_in = gr.Image(type="pil", label="Image", height=260)376            with gr.Column():377                top5_out = gr.JSON(label="Top-5 classes")378                details = gr.Textbox(label="Details", lines=3)379        with gr.Row():380            probs_plot   = gr.Image(label="Top-5 probabilities", height=260)381            experts_plot = gr.Image(label="Expert activations (predicted class)", height=260)382            routing_plot = gr.Image(label="Routing heatmap (classes × experts)", height=260)383 384        gr.Button("Predict").click(385            fn=run_image,386            inputs=[img_in, tau_slider],387            outputs=[top5_out, probs_plot, experts_plot, routing_plot, details],388            concurrency_limit=2389        )390 391    with gr.Tab("Text → classes & experts"):392        with gr.Row():393            txt_in = gr.Textbox(label="Text prompt (e.g., 'a photo of a dog')", lines=2)394            with gr.Column():395                txt_top = gr.JSON(label="Top-5 classes")396                txt_details = gr.Textbox(label="Details", lines=3)397        with gr.Row():398            txt_probs_plot   = gr.Image(label="Top-5 probabilities", height=260)399            txt_experts_plot = gr.Image(label="Expert activations (predicted class)", height=260)400            txt_routing_plot = gr.Image(label="Routing heatmap (classes × experts)", height=260)401 402        gr.Button("Run").click(403            fn=run_text, inputs=[txt_in, tau_slider],404            outputs=[txt_top, txt_probs_plot, txt_experts_plot, txt_routing_plot, txt_details],405            concurrency_limit=2406        )407 408    with gr.Tab("Compare (Image vs Text)"):409        with gr.Row():410            c_img = gr.Image(type="pil", label="Image", height=240)411            c_txt = gr.Textbox(label="Text prompt", lines=2)412        with gr.Row():413            bar_i = gr.Image(label="Experts (image)", height=260)414            bar_t = gr.Image(label="Experts (text)", height=260)415        info = gr.Textbox(label="Similarity", lines=2)416        gr.Button("Compare").click(417            run_compare, inputs=[c_img, c_txt, tau_slider], outputs=[bar_i, bar_t, info],418            concurrency_limit=2419        )420        gr.Markdown(421            "Tip: same-class pairs should give similarity ≈ **1.0**, different-class pairs → ≈ **0.0**."422        )423 424    with gr.Tab("Simulator (how the head works)"):425        gr.Markdown("Interactive simulator of routing and mixing. "426                    "Adjust #experts, temperature, and modality coupling; see α-vectors and class probabilities.")427        with gr.Row():428            R_in   = gr.Slider(2, 16, value=4, step=1, label="#Experts (R)")429            C_in   = gr.Slider(2, 20, value=min(10, len(CLASSES)), step=1, label="#Classes (C)")430            tau_in = gr.Slider(0.1, 2.0, value=0.5, step=0.05, label="Temperature τ (sim)")431        with gr.Row():432            couple_in  = gr.Slider(0.0, 1.0, value=0.5, step=0.05, label="Coupling of modalities (0=different, 1=identical)")433            seed_in    = gr.Number(value=42, precision=0, label="Seed")434            hard_in    = gr.Checkbox(False, label="Hard routing (one-hot)")435            gumbel_in  = gr.Checkbox(False, label="Use Gumbel-Softmax (stochastic)")436 437        with gr.Row():438            sim_alpha_img = gr.Image(label="α (image)", height=220)439            sim_alpha_txt = gr.Image(label="α (text)",  height=220)440            sim_W_heat    = gr.Image(label="Components (W)", height=220)441        with gr.Row():442            sim_cls_img = gr.Image(label="Image → probs", height=240)443            sim_cls_txt = gr.Image(label="Text  → probs", height=240)444 445        sim_info    = gr.Textbox(label="Summary", lines=2)446        sim_details = gr.Textbox(label="Details", lines=3)447 448        gr.Button("Simulate").click(449            run_simulator,450            inputs=[R_in, C_in, tau_in, couple_in, seed_in, hard_in, gumbel_in],451            outputs=[sim_alpha_img, sim_alpha_txt, sim_W_heat, sim_cls_img, sim_cls_txt, sim_info, sim_details],452            concurrency_limit=2453        )454 455    # Healthcheck (скрытый блок, удобно при отладке)456    with gr.Row(visible=False):457        btn = gr.Button("Healthcheck")458        txt = gr.Textbox()459        def _healthcheck():460            try:461                _ensure_ready()462                return "ok"463            except Exception as e:464                return f"error: {e}"465        btn.click(lambda: _healthcheck(), outputs=txt, concurrency_limit=1)466 467# очередь и запуск (Gradio 4.x — без concurrency_count)468demo.queue()469 470if __name__ == "__main__":471    demo.launch(server_name="0.0.0.0", server_port=7860)472