Leeps commited on
Commit
37d933c
·
1 Parent(s): 60b47d1

Use fixed notebook activation layers

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. app.py +53 -122
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 how the neural net's layer activations change as the pixels move through the model.
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
- DEFAULT_LAYER = "14: InvertedResidual"
33
- DEFAULT_CHANNELS = 8
 
34
  PREVIEW_SIZE = 360
35
- TILE_SIZE = 240
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 layer_overview_gallery(activations):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  choices = layer_choices()
298
  outputs = []
299
 
300
- for index, activation in enumerate(activations):
 
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 {index}\n"
306
- f"{choices[index].split(': ', 1)[1]} | {channels} maps | {height}x{width}"
 
307
  )
308
- outputs.append((activation_map, caption))
309
 
310
  return outputs
311
 
312
 
313
- def feature_map_gallery(activation, count):
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
- f"### Layer {layer_index}: {stage_name(layer_index)}\n"
328
- f"{stage_explanation(layer_index)}\n\n"
329
- f"This layer outputs **{channels} activation maps** on a **{height}x{width}** grid. "
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, layer_label=DEFAULT_LAYER, channels=DEFAULT_CHANNELS):
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
- layer_index = max(0, min(layer_index, len(activations) - 1))
344
- selected = activations[layer_index]
345
 
346
  return (
347
  image,
348
- layer_overview_gallery(activations),
349
- feature_map_gallery(selected, channels),
350
- overlay_heatmap(fit_image(image), activation_energy(selected), alpha=0.48, cmap_name="magma"),
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, channels_gallery, overlay, summary = analyze_image(
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(layer_label, channels, evt: gr.SelectData):
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, channels_gallery, overlay, summary = analyze_image(
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, channels_gallery, overlay, summary, umap_plot, relation, closest_gallery, farthest_gallery, index
413
 
414
 
415
- def analyze_upload(image, layer_label, channels):
416
- selected_image, layer_gallery, channels_gallery, overlay, summary = analyze_image(image, layer_label, channels)
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, channels_gallery, overlay, summary, umap_plot, relation, closest_gallery, farthest_gallery, -1
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 the activation maps "
455
- "the neural network produces at each layer.\n\n"
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="Single selected-layer overlay", type="pil", interactive=False)
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="Activation maps as the image moves through the model",
492
- columns=3,
493
- height=760,
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=[layer, channels],
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, layer, channels],
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