Spaces:
Sleeping
Sleeping
Use fixed notebook activation layers
Browse files
README.md
CHANGED
|
@@ -13,7 +13,7 @@ license: mit
|
|
| 13 |
|
| 14 |
# Model Layers Viewer
|
| 15 |
|
| 16 |
-
A small CPU-friendly Gradio app for workshop participants to inspect MobileNetV2 activation maps on MoMA artwork images. Pick a MoMA image or upload your own image, then watch
|
| 17 |
|
| 18 |
The UMAP tab shows how the MoMA images relate after MobileNetV2 processes them, including the closest and farthest images in feature space.
|
| 19 |
|
|
|
|
| 13 |
|
| 14 |
# Model Layers Viewer
|
| 15 |
|
| 16 |
+
A small CPU-friendly Gradio app for workshop participants to inspect MobileNetV2 activation maps on MoMA artwork images. Pick a MoMA image or upload your own image, then watch pixel activations at the same intermediate layers used in the workshop notebook: 0, 2, 4, 7, 14, and 18.
|
| 17 |
|
| 18 |
The UMAP tab shows how the MoMA images relate after MobileNetV2 processes them, including the closest and farthest images in feature space.
|
| 19 |
|
app.py
CHANGED
|
@@ -29,10 +29,11 @@ HEADERS = {
|
|
| 29 |
}
|
| 30 |
MOMA_SAMPLE_SIZE = 36
|
| 31 |
MOMA_CANDIDATES = 140
|
| 32 |
-
|
| 33 |
-
|
|
|
|
| 34 |
PREVIEW_SIZE = 360
|
| 35 |
-
TILE_SIZE =
|
| 36 |
|
| 37 |
|
| 38 |
def stage_name(layer_index):
|
|
@@ -109,13 +110,6 @@ def layer_choices():
|
|
| 109 |
return [f"{index}: {layer.__class__.__name__}" for index, layer in enumerate(model.features)]
|
| 110 |
|
| 111 |
|
| 112 |
-
def parse_layer_index(layer_label):
|
| 113 |
-
try:
|
| 114 |
-
return int(str(layer_label).split(":", 1)[0])
|
| 115 |
-
except Exception:
|
| 116 |
-
return 14
|
| 117 |
-
|
| 118 |
-
|
| 119 |
def rgb_image(image):
|
| 120 |
if image is None:
|
| 121 |
return None
|
|
@@ -293,62 +287,63 @@ def strongest_channels(activation, count):
|
|
| 293 |
return [(channel, fmap[channel].numpy()) for channel in channel_ids]
|
| 294 |
|
| 295 |
|
| 296 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
choices = layer_choices()
|
| 298 |
outputs = []
|
| 299 |
|
| 300 |
-
for
|
|
|
|
| 301 |
_, channels, height, width = activation.shape
|
| 302 |
-
energy = activation_energy(activation)
|
| 303 |
-
activation_map = colorize(energy, "magma").resize((TILE_SIZE, TILE_SIZE), Image.Resampling.BILINEAR)
|
| 304 |
caption = (
|
| 305 |
-
f"Layer {
|
| 306 |
-
f"
|
|
|
|
| 307 |
)
|
| 308 |
-
outputs.append((
|
| 309 |
|
| 310 |
return outputs
|
| 311 |
|
| 312 |
|
| 313 |
-
def
|
| 314 |
-
cards = []
|
| 315 |
-
|
| 316 |
-
for channel, values in strongest_channels(activation, count):
|
| 317 |
-
heat = colorize(values, "viridis").resize((TILE_SIZE, TILE_SIZE), Image.Resampling.BILINEAR)
|
| 318 |
-
cards.append((heat, f"channel {channel}"))
|
| 319 |
-
|
| 320 |
-
return cards
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
def layer_summary(layer_index, activation, count):
|
| 324 |
-
_, channels, height, width = activation.shape
|
| 325 |
-
selected_count = min(int(count), channels)
|
| 326 |
return (
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
f"The gallery below shows **{selected_count} channels** from this layer. "
|
| 331 |
-
"Bright areas are where that channel is responding most strongly."
|
| 332 |
)
|
| 333 |
|
| 334 |
|
| 335 |
-
def analyze_image(image
|
| 336 |
image = rgb_image(image)
|
| 337 |
if image is None:
|
| 338 |
items, _ = load_moma_items()
|
| 339 |
image = items[0]["img"]
|
| 340 |
|
| 341 |
-
layer_index = parse_layer_index(layer_label)
|
| 342 |
activations = collect_activations(image)
|
| 343 |
-
|
| 344 |
-
selected = activations[layer_index]
|
| 345 |
|
| 346 |
return (
|
| 347 |
image,
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
layer_summary(layer_index, selected, channels),
|
| 352 |
)
|
| 353 |
|
| 354 |
|
|
@@ -378,17 +373,12 @@ def make_umap_plot(selected_index=0):
|
|
| 378 |
def initialize():
|
| 379 |
items, _ = load_moma_items()
|
| 380 |
selected_index = 0
|
| 381 |
-
selected_image, layer_gallery,
|
| 382 |
-
items[selected_index]["img"],
|
| 383 |
-
DEFAULT_LAYER,
|
| 384 |
-
DEFAULT_CHANNELS,
|
| 385 |
-
)
|
| 386 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(selected_index)
|
| 387 |
return (
|
| 388 |
gallery_items(),
|
| 389 |
selected_image,
|
| 390 |
layer_gallery,
|
| 391 |
-
channels_gallery,
|
| 392 |
overlay,
|
| 393 |
summary,
|
| 394 |
umap_plot,
|
|
@@ -399,40 +389,27 @@ def initialize():
|
|
| 399 |
)
|
| 400 |
|
| 401 |
|
| 402 |
-
def select_moma(
|
| 403 |
index = evt.index if isinstance(evt.index, int) else 0
|
| 404 |
items, _ = load_moma_items()
|
| 405 |
index = max(0, min(index, len(items) - 1))
|
| 406 |
-
selected_image, layer_gallery,
|
| 407 |
-
items[index]["img"],
|
| 408 |
-
layer_label,
|
| 409 |
-
channels,
|
| 410 |
-
)
|
| 411 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(index)
|
| 412 |
-
return selected_image, layer_gallery,
|
| 413 |
|
| 414 |
|
| 415 |
-
def analyze_upload(image
|
| 416 |
-
selected_image, layer_gallery,
|
| 417 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(0)
|
| 418 |
relation = (
|
| 419 |
"### Uploaded image\n"
|
| 420 |
"The UMAP tab is built from the MoMA image set. Uploaded images are shown in the layer viewer, "
|
| 421 |
"but are not inserted into the precomputed MoMA map."
|
| 422 |
)
|
| 423 |
-
return selected_image, layer_gallery,
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def update_layer_view(image, selected_index, layer_label, channels):
|
| 427 |
-
selected_image, layer_gallery, channels_gallery, overlay, summary = analyze_image(image, layer_label, channels)
|
| 428 |
-
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(selected_index if selected_index is not None and selected_index >= 0 else 0)
|
| 429 |
-
return selected_image, layer_gallery, channels_gallery, overlay, summary, umap_plot, relation, closest_gallery, farthest_gallery
|
| 430 |
|
| 431 |
|
| 432 |
def build_app():
|
| 433 |
-
choices = layer_choices()
|
| 434 |
-
selected_layer = DEFAULT_LAYER if DEFAULT_LAYER in choices else choices[-1]
|
| 435 |
-
|
| 436 |
theme = gr.themes.Soft(
|
| 437 |
primary_hue="blue",
|
| 438 |
secondary_hue="pink",
|
|
@@ -451,8 +428,8 @@ def build_app():
|
|
| 451 |
|
| 452 |
gr.Markdown(
|
| 453 |
"# Model Layers Viewer\n"
|
| 454 |
-
"Choose a MoMA image or upload your own. The app runs MobileNetV2 on CPU and shows
|
| 455 |
-
"the
|
| 456 |
"Inspired by [Small ML: The Art of Data Discovery]"
|
| 457 |
"(https://colab.research.google.com/drive/1_8GasNHKJpO8x-AAt93D3LfrvCLGzsbj?usp=sharing), "
|
| 458 |
"a workshop by Sam Keene at [ITP Camp 2026](https://itp.nyu.edu/camp/2026/session/205)."
|
|
@@ -469,28 +446,18 @@ def build_app():
|
|
| 469 |
elem_classes=["moma-gallery"],
|
| 470 |
)
|
| 471 |
upload = gr.Image(label="Upload your own image", type="pil", sources=["upload", "clipboard"])
|
| 472 |
-
with gr.Row():
|
| 473 |
-
layer = gr.Dropdown(choices=choices, value=selected_layer, label="Layer to inspect")
|
| 474 |
-
channels = gr.Slider(4, 16, value=DEFAULT_CHANNELS, step=4, label="Feature maps to show")
|
| 475 |
|
| 476 |
with gr.Column(scale=2, min_width=520):
|
| 477 |
with gr.Tabs():
|
| 478 |
with gr.Tab("Layer activations"):
|
| 479 |
with gr.Row(equal_height=False):
|
| 480 |
selected_image = gr.Image(label="Selected image", type="pil", interactive=False)
|
| 481 |
-
selected_overlay = gr.Image(label="
|
| 482 |
layer_summary_md = gr.Markdown()
|
| 483 |
-
channel_explanations = gr.Gallery(
|
| 484 |
-
label="Selected layer feature maps",
|
| 485 |
-
columns=2,
|
| 486 |
-
height=520,
|
| 487 |
-
object_fit="contain",
|
| 488 |
-
elem_classes=["activation-gallery"],
|
| 489 |
-
)
|
| 490 |
layer_progression = gr.Gallery(
|
| 491 |
-
label="
|
| 492 |
-
columns=
|
| 493 |
-
height=
|
| 494 |
object_fit="contain",
|
| 495 |
elem_classes=["activation-gallery"],
|
| 496 |
)
|
|
@@ -523,7 +490,6 @@ def build_app():
|
|
| 523 |
moma_gallery,
|
| 524 |
selected_image,
|
| 525 |
layer_progression,
|
| 526 |
-
channel_explanations,
|
| 527 |
selected_overlay,
|
| 528 |
layer_summary_md,
|
| 529 |
umap_plot,
|
|
@@ -536,11 +502,10 @@ def build_app():
|
|
| 536 |
)
|
| 537 |
moma_gallery.select(
|
| 538 |
select_moma,
|
| 539 |
-
inputs=
|
| 540 |
outputs=[
|
| 541 |
selected_image,
|
| 542 |
layer_progression,
|
| 543 |
-
channel_explanations,
|
| 544 |
selected_overlay,
|
| 545 |
layer_summary_md,
|
| 546 |
umap_plot,
|
|
@@ -553,11 +518,10 @@ def build_app():
|
|
| 553 |
)
|
| 554 |
upload.change(
|
| 555 |
analyze_upload,
|
| 556 |
-
inputs=[upload
|
| 557 |
outputs=[
|
| 558 |
selected_image,
|
| 559 |
layer_progression,
|
| 560 |
-
channel_explanations,
|
| 561 |
selected_overlay,
|
| 562 |
layer_summary_md,
|
| 563 |
umap_plot,
|
|
@@ -568,39 +532,6 @@ def build_app():
|
|
| 568 |
],
|
| 569 |
show_progress="minimal",
|
| 570 |
)
|
| 571 |
-
layer.change(
|
| 572 |
-
update_layer_view,
|
| 573 |
-
inputs=[selected_image, selected_index, layer, channels],
|
| 574 |
-
outputs=[
|
| 575 |
-
selected_image,
|
| 576 |
-
layer_progression,
|
| 577 |
-
channel_explanations,
|
| 578 |
-
selected_overlay,
|
| 579 |
-
layer_summary_md,
|
| 580 |
-
umap_plot,
|
| 581 |
-
relation_md,
|
| 582 |
-
closest_gallery,
|
| 583 |
-
farthest_gallery,
|
| 584 |
-
],
|
| 585 |
-
show_progress="minimal",
|
| 586 |
-
)
|
| 587 |
-
channels.change(
|
| 588 |
-
update_layer_view,
|
| 589 |
-
inputs=[selected_image, selected_index, layer, channels],
|
| 590 |
-
outputs=[
|
| 591 |
-
selected_image,
|
| 592 |
-
layer_progression,
|
| 593 |
-
channel_explanations,
|
| 594 |
-
selected_overlay,
|
| 595 |
-
layer_summary_md,
|
| 596 |
-
umap_plot,
|
| 597 |
-
relation_md,
|
| 598 |
-
closest_gallery,
|
| 599 |
-
farthest_gallery,
|
| 600 |
-
],
|
| 601 |
-
show_progress="minimal",
|
| 602 |
-
)
|
| 603 |
-
|
| 604 |
return demo
|
| 605 |
|
| 606 |
|
|
|
|
| 29 |
}
|
| 30 |
MOMA_SAMPLE_SIZE = 36
|
| 31 |
MOMA_CANDIDATES = 140
|
| 32 |
+
NOTEBOOK_LAYERS = [0, 2, 4, 7, 14, 18]
|
| 33 |
+
CHANNELS_PER_LAYER = 8
|
| 34 |
+
OVERLAY_LAYER = 14
|
| 35 |
PREVIEW_SIZE = 360
|
| 36 |
+
TILE_SIZE = 120
|
| 37 |
|
| 38 |
|
| 39 |
def stage_name(layer_index):
|
|
|
|
| 110 |
return [f"{index}: {layer.__class__.__name__}" for index, layer in enumerate(model.features)]
|
| 111 |
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
def rgb_image(image):
|
| 114 |
if image is None:
|
| 115 |
return None
|
|
|
|
| 287 |
return [(channel, fmap[channel].numpy()) for channel in channel_ids]
|
| 288 |
|
| 289 |
|
| 290 |
+
def channel_grid_image(activation, count=CHANNELS_PER_LAYER):
|
| 291 |
+
selected = strongest_channels(activation, count)
|
| 292 |
+
cols = 4
|
| 293 |
+
rows = math.ceil(len(selected) / cols)
|
| 294 |
+
label_h = 20
|
| 295 |
+
grid = Image.new("RGB", (cols * TILE_SIZE, rows * (TILE_SIZE + label_h)), (248, 248, 246))
|
| 296 |
+
draw = ImageDraw.Draw(grid)
|
| 297 |
+
|
| 298 |
+
for position, (channel, values) in enumerate(selected):
|
| 299 |
+
x = (position % cols) * TILE_SIZE
|
| 300 |
+
y = (position // cols) * (TILE_SIZE + label_h)
|
| 301 |
+
tile = colorize(values, "viridis").resize((TILE_SIZE, TILE_SIZE), Image.Resampling.BILINEAR)
|
| 302 |
+
grid.paste(tile, (x, y))
|
| 303 |
+
draw.text((x + 6, y + TILE_SIZE + 3), f"channel {channel}", fill=(35, 39, 47))
|
| 304 |
+
|
| 305 |
+
return grid
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
def fixed_layer_gallery(activations):
|
| 309 |
choices = layer_choices()
|
| 310 |
outputs = []
|
| 311 |
|
| 312 |
+
for layer_index in NOTEBOOK_LAYERS:
|
| 313 |
+
activation = activations[layer_index]
|
| 314 |
_, channels, height, width = activation.shape
|
|
|
|
|
|
|
| 315 |
caption = (
|
| 316 |
+
f"Layer {layer_index}: {stage_name(layer_index)}\n"
|
| 317 |
+
f"torch.Size([1, {channels}, {height}, {width}])\n"
|
| 318 |
+
f"{choices[layer_index].split(': ', 1)[1]} | {CHANNELS_PER_LAYER} channel maps"
|
| 319 |
)
|
| 320 |
+
outputs.append((channel_grid_image(activation), caption))
|
| 321 |
|
| 322 |
return outputs
|
| 323 |
|
| 324 |
|
| 325 |
+
def layer_summary():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
return (
|
| 327 |
+
"### Pixel activations through the model\n"
|
| 328 |
+
"These are the same intermediate layers checked in the notebook: 0, 2, 4, 7, 14, and 18. Each panel is one layer. "
|
| 329 |
+
"Inside each panel are several channel maps from that layer: bright pixels are places where that map activated strongly."
|
|
|
|
|
|
|
| 330 |
)
|
| 331 |
|
| 332 |
|
| 333 |
+
def analyze_image(image):
|
| 334 |
image = rgb_image(image)
|
| 335 |
if image is None:
|
| 336 |
items, _ = load_moma_items()
|
| 337 |
image = items[0]["img"]
|
| 338 |
|
|
|
|
| 339 |
activations = collect_activations(image)
|
| 340 |
+
overlay_activation = activations[OVERLAY_LAYER]
|
|
|
|
| 341 |
|
| 342 |
return (
|
| 343 |
image,
|
| 344 |
+
fixed_layer_gallery(activations),
|
| 345 |
+
overlay_heatmap(fit_image(image), activation_energy(overlay_activation), alpha=0.48, cmap_name="magma"),
|
| 346 |
+
layer_summary(),
|
|
|
|
| 347 |
)
|
| 348 |
|
| 349 |
|
|
|
|
| 373 |
def initialize():
|
| 374 |
items, _ = load_moma_items()
|
| 375 |
selected_index = 0
|
| 376 |
+
selected_image, layer_gallery, overlay, summary = analyze_image(items[selected_index]["img"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(selected_index)
|
| 378 |
return (
|
| 379 |
gallery_items(),
|
| 380 |
selected_image,
|
| 381 |
layer_gallery,
|
|
|
|
| 382 |
overlay,
|
| 383 |
summary,
|
| 384 |
umap_plot,
|
|
|
|
| 389 |
)
|
| 390 |
|
| 391 |
|
| 392 |
+
def select_moma(evt: gr.SelectData):
|
| 393 |
index = evt.index if isinstance(evt.index, int) else 0
|
| 394 |
items, _ = load_moma_items()
|
| 395 |
index = max(0, min(index, len(items) - 1))
|
| 396 |
+
selected_image, layer_gallery, overlay, summary = analyze_image(items[index]["img"])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(index)
|
| 398 |
+
return selected_image, layer_gallery, overlay, summary, umap_plot, relation, closest_gallery, farthest_gallery, index
|
| 399 |
|
| 400 |
|
| 401 |
+
def analyze_upload(image):
|
| 402 |
+
selected_image, layer_gallery, overlay, summary = analyze_image(image)
|
| 403 |
umap_plot, relation, closest_gallery, farthest_gallery = make_umap_plot(0)
|
| 404 |
relation = (
|
| 405 |
"### Uploaded image\n"
|
| 406 |
"The UMAP tab is built from the MoMA image set. Uploaded images are shown in the layer viewer, "
|
| 407 |
"but are not inserted into the precomputed MoMA map."
|
| 408 |
)
|
| 409 |
+
return selected_image, layer_gallery, overlay, summary, umap_plot, relation, closest_gallery, farthest_gallery, -1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
|
| 412 |
def build_app():
|
|
|
|
|
|
|
|
|
|
| 413 |
theme = gr.themes.Soft(
|
| 414 |
primary_hue="blue",
|
| 415 |
secondary_hue="pink",
|
|
|
|
| 428 |
|
| 429 |
gr.Markdown(
|
| 430 |
"# Model Layers Viewer\n"
|
| 431 |
+
"Choose a MoMA image or upload your own. The app runs MobileNetV2 on CPU and shows pixel activation maps "
|
| 432 |
+
"at the same intermediate layers used in the workshop notebook.\n\n"
|
| 433 |
"Inspired by [Small ML: The Art of Data Discovery]"
|
| 434 |
"(https://colab.research.google.com/drive/1_8GasNHKJpO8x-AAt93D3LfrvCLGzsbj?usp=sharing), "
|
| 435 |
"a workshop by Sam Keene at [ITP Camp 2026](https://itp.nyu.edu/camp/2026/session/205)."
|
|
|
|
| 446 |
elem_classes=["moma-gallery"],
|
| 447 |
)
|
| 448 |
upload = gr.Image(label="Upload your own image", type="pil", sources=["upload", "clipboard"])
|
|
|
|
|
|
|
|
|
|
| 449 |
|
| 450 |
with gr.Column(scale=2, min_width=520):
|
| 451 |
with gr.Tabs():
|
| 452 |
with gr.Tab("Layer activations"):
|
| 453 |
with gr.Row(equal_height=False):
|
| 454 |
selected_image = gr.Image(label="Selected image", type="pil", interactive=False)
|
| 455 |
+
selected_overlay = gr.Image(label="One overlay: layer 14 activation over image", type="pil", interactive=False)
|
| 456 |
layer_summary_md = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
layer_progression = gr.Gallery(
|
| 458 |
+
label="Notebook layers: activation maps across channels",
|
| 459 |
+
columns=2,
|
| 460 |
+
height=860,
|
| 461 |
object_fit="contain",
|
| 462 |
elem_classes=["activation-gallery"],
|
| 463 |
)
|
|
|
|
| 490 |
moma_gallery,
|
| 491 |
selected_image,
|
| 492 |
layer_progression,
|
|
|
|
| 493 |
selected_overlay,
|
| 494 |
layer_summary_md,
|
| 495 |
umap_plot,
|
|
|
|
| 502 |
)
|
| 503 |
moma_gallery.select(
|
| 504 |
select_moma,
|
| 505 |
+
inputs=None,
|
| 506 |
outputs=[
|
| 507 |
selected_image,
|
| 508 |
layer_progression,
|
|
|
|
| 509 |
selected_overlay,
|
| 510 |
layer_summary_md,
|
| 511 |
umap_plot,
|
|
|
|
| 518 |
)
|
| 519 |
upload.change(
|
| 520 |
analyze_upload,
|
| 521 |
+
inputs=[upload],
|
| 522 |
outputs=[
|
| 523 |
selected_image,
|
| 524 |
layer_progression,
|
|
|
|
| 525 |
selected_overlay,
|
| 526 |
layer_summary_md,
|
| 527 |
umap_plot,
|
|
|
|
| 532 |
],
|
| 533 |
show_progress="minimal",
|
| 534 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
return demo
|
| 536 |
|
| 537 |
|