import streamlit as st import torch import torch.nn.functional as F from torchvision import transforms from PIL import Image import numpy as np from io import BytesIO import requests from adavit_model import AdaViTDynamic # ───────────────────────────────────────────────────────────────────────────── # PAGE CONFIG # ───────────────────────────────────────────────────────────────────────────── st.set_page_config( page_title="PatchWise — Adaptive Sparse ViT", layout="wide", initial_sidebar_state="collapsed", ) # ───────────────────────────────────────────────────────────────────────────── # GLOBAL CSS # ───────────────────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # DEVICE + MODEL # ───────────────────────────────────────────────────────────────────────────── device = torch.device("cpu") @st.cache_resource def load_model(): model = AdaViTDynamic( image_size=32, patch_size=4, num_classes=10, dim=256, depth=6, heads=8, mlp_dim=512, ) checkpoint = torch.load("best_model.pth", map_location=device) model.load_state_dict(checkpoint, strict=False) model.eval() return model model = load_model() CLASSES = [ "airplane", "automobile", "bird", "cat", "deer", "dog", "frog", "horse", "ship", "truck" ] transform = transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), ]) # ───────────────────────────────────────────────────────────────────────────── # INFERENCE HELPER # ───────────────────────────────────────────────────────────────────────────── def run_inference(image: Image.Image): """Returns (predicted_class, confidence, keep_rate, flops_saved, patch_mask_8x8).""" tensor = transform(image).unsqueeze(0) with torch.no_grad(): outputs = model(tensor) # Unpack — model may return (logits, extras) tuple or a dict if isinstance(outputs, tuple): raw, extras = outputs[0], outputs[1] if len(outputs) > 1 else {} else: raw = outputs if isinstance(raw, dict): logits = raw["logits"] mask_flat = raw.get("mask", None) else: logits = raw mask_flat = extras.get("mask", None) if isinstance(extras, dict) else None probs = F.softmax(logits, dim=1) pred_idx = torch.argmax(probs, dim=1).item() confidence = probs[0][pred_idx].item() if mask_flat is not None: mask = mask_flat[0].cpu().numpy() keep_rate = float(mask.mean()) * 100 patch_grid = mask.reshape(8, 8) else: keep_rate = 100.0 patch_grid = np.ones((8, 8)) flops_saved = 100.0 - keep_rate return CLASSES[pred_idx], confidence * 100, keep_rate, flops_saved, patch_grid # ───────────────────────────────────────────────────────────────────────────── # SAMPLE IMAGES # ───────────────────────────────────────────────────────────────────────────── # Handpicked samples — clear, colorful, subject-centered images. # Using Wikimedia Commons with proper headers — reliably public domain. # Four demo images — multiple URL fallbacks per class so if one CDN # blocks, the next is tried. All are subject-centered, colorful images # that produce interesting patch routing maps. DEMO_SAMPLES = [ ("Frog", [ "https://images.pexels.com/photos/2062316/pexels-photo-2062316.jpeg", "https://images.pexels.com/photos/70083/frog-macro-amphibian-green-70083.jpeg", "https://images.pexels.com/photos/145683/pexels-photo-145683.jpeg", "https://images.pexels.com/photos/1490908/pexels-photo-1490908.jpeg", ]), ("Automobile", [ "https://images.pexels.com/photos/170811/pexels-photo-170811.jpeg", "https://images.pexels.com/photos/1149831/pexels-photo-1149831.jpeg", "https://images.pexels.com/photos/707046/pexels-photo-707046.jpeg", "https://images.pexels.com/photos/3802510/pexels-photo-3802510.jpeg", ]), ] @st.cache_resource(show_spinner=False) def load_demo_samples(): """For each demo class, try each fallback URL until one succeeds. Downscales to 32x32 for inference, upscales to 256x256 for display.""" import requests as _req headers = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.pexels.com/", "Accept": "image/webp,image/apng,image/*,*/*;q=0.8", } samples = [] for label, urls in DEMO_SAMPLES: img_32 = None for url in urls: try: resp = _req.get(url, headers=headers, timeout=10) if resp.status_code == 200: img = Image.open(BytesIO(resp.content)).convert("RGB") img_32 = img.resize((32, 32), Image.LANCZOS) break except Exception: continue if img_32 is None: # All URLs failed — make a synthetic fallback that's still recognisable img_32 = _make_synthetic(label) img_256 = img_32.resize((256, 256), Image.NEAREST) samples.append((label, img_32, img_256)) return samples # [(label, img_32, img_256), ...] def _make_synthetic(label: str) -> Image.Image: """Generates a simple recognisable 32x32 stand-in if all URLs fail.""" from PIL import ImageDraw, ImageFilter img = Image.new("RGB", (32, 32), (20, 20, 28)) draw = ImageDraw.Draw(img) if label == "Dog": draw.rectangle([0, 0, 32, 32], fill=(200, 170, 120)) draw.ellipse([8, 8, 24, 24], fill=(160, 120, 70)) draw.ellipse([10, 5, 22, 15], fill=(170, 130, 80)) draw.ellipse([7, 4, 13, 12], fill=(130, 90, 50)) draw.ellipse([19, 4, 25, 12], fill=(130, 90, 50)) elif label == "Frog": draw.rectangle([0, 0, 32, 32], fill=(60, 120, 50)) draw.ellipse([6, 10, 26, 28], fill=(50, 160, 45)) draw.ellipse([8, 4, 24, 18], fill=(60, 175, 55)) draw.ellipse([7, 3, 14, 11], fill=(220, 220, 60)) draw.ellipse([18, 3, 25, 11], fill=(220, 220, 60)) draw.ellipse([8, 4, 13, 10], fill=(20, 70, 20)) draw.ellipse([19, 4, 24, 10], fill=(20, 70, 20)) elif label == "Ship": draw.rectangle([0, 0, 32, 18], fill=(100, 150, 210)) draw.rectangle([0, 18, 32, 32], fill=(40, 90, 160)) draw.polygon([(3, 18), (29, 18), (27, 25), (5, 25)], fill=(230, 230, 230)) draw.rectangle([8, 11, 24, 18], fill=(200, 200, 200)) draw.rectangle([12, 6, 20, 11], fill=(210, 210, 210)) draw.rectangle([15, 2, 17, 6], fill=(180, 50, 50)) elif label == "Automobile": draw.rectangle([0, 0, 32, 32], fill=(160, 185, 160)) draw.rectangle([0, 24, 32, 32], fill=(70, 70, 70)) draw.rectangle([3, 15, 29, 24], fill=(200, 40, 40)) draw.polygon([(8, 9), (24, 9), (28, 15), (4, 15)], fill=(175, 30, 30)) draw.rectangle([9, 10, 15, 14], fill=(140, 200, 230)) draw.rectangle([17, 10, 23, 14], fill=(140, 200, 230)) draw.ellipse([4, 20, 12, 28], fill=(25, 25, 25)) draw.ellipse([20, 20, 28, 28], fill=(25, 25, 25)) return img.filter(ImageFilter.GaussianBlur(0.3)) # selected_sample_idx: int index into demo_samples list, or None if "selected_sample_idx" not in st.session_state: st.session_state.selected_sample_idx = None # ───────────────────────────────────────────────────────────────────────────── # NAV # ───────────────────────────────────────────────────────────────────────────── st.markdown("""
""", unsafe_allow_html=True) # ───────────────────────────────────────────────────────────────────────────── # HERO # ───────────────────────────────────────────────────────────────────────────── st.markdown("""PatchWise uses a reinforcement-learned policy to decide, per image, which patches are worth computing — and physically drops the rest. Same accuracy. Half the FLOPs.
Or try a sample — processed at CIFAR-10 resolution (32×32)
""", unsafe_allow_html=True) s_cols = st.columns(len(demo_samples), gap="medium") for i, (label, img_32, img_256) in enumerate(demo_samples): with s_cols[i]: st.image(img_256, use_container_width=True) if st.button(label, key=f"sample_{i}"): st.session_state.selected_sample_idx = i uploaded = None # ── Resolve active image ────────────────────────────────────────────────────── active_image = None active_label = None if uploaded is not None: active_image = Image.open(uploaded).convert("RGB") active_label = "uploaded" st.session_state.selected_sample_idx = None elif st.session_state.selected_sample_idx is not None: idx = st.session_state.selected_sample_idx active_label = demo_samples[idx][0] active_image = demo_samples[idx][1] # use the 32×32 version for inference # ── Inference output ────────────────────────────────────────────────────────── st.markdown("", unsafe_allow_html=True) if active_image is not None: pred_class, confidence, keep_rate, flops_saved, patch_grid = run_inference(active_image) out_img_col, out_patch_col, out_result_col = st.columns([1, 1, 1.4], gap="large") with out_img_col: st.markdown('Input image
', unsafe_allow_html=True) st.image(active_image, use_container_width=True) with out_patch_col: st.markdown('Active patch map
', unsafe_allow_html=True) # Render patch grid as a colour-coded image patch_vis = np.zeros((patch_grid.shape[0], patch_grid.shape[1], 3), dtype=np.uint8) patch_vis[patch_grid == 1] = [56, 189, 248] # cyan-ish = kept patch_vis[patch_grid == 0] = [18, 18, 22] # near-black = pruned patch_img = Image.fromarray(patch_vis).resize((128, 128), Image.NEAREST) st.image(patch_img, use_container_width=True) with out_result_col: st.markdown(f"""Upload an image or select a sample above
to observe adaptive patch routing live.
x.detach()
gating. Critic reduces variance via advantage estimation.
| Backbone | Custom Vision Transformer (DeiT-inspired) |
| RL Controller | Asynchronous A3C · 4 workers |
| Policy distribution | Categorical (Softmax, 2 logits / patch) |
| Dataset | CIFAR-10 · CIFAR-100 stress test |
| Input resolution | 32 × 32 px |
| Patch size | 4 × 4 → 64 tokens total |
| Embedding dim | 256 |
| Depth | 6 transformer layers |
| Attention heads | 8 |
| Target keep rate | 50% (EMA-enforced) |
| Edge hardware | NVIDIA Jetson AGX Orin |
| CIFAR-100 stability | ~57% accuracy @ 300 epochs |