Add a tested PyTorch shape lesson
#1
by nityanandmathur - opened
- .gitattributes +1 -0
- DESIGN.md +39 -0
- LICENSE +21 -0
- README.md +85 -6
- VALIDATION.md +68 -0
- app.mjs +82 -0
- index.html +102 -17
- logo.png +3 -0
- shape-math.mjs +43 -0
- shape-math.test.mjs +45 -0
- styles.css +108 -0
- verify_pytorch.py +90 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
logo.png filter=lfs diff=lfs merge=lfs -text
|
DESIGN.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Design and acceptance notes
|
| 2 |
+
|
| 3 |
+
This is an interactive educational page, not an installation landing page.
|
| 4 |
+
The design uses the existing tensorViz white background, dark green mark and
|
| 5 |
+
system fonts. The canonical supplied logo is copied unchanged and never redrawn.
|
| 6 |
+
The [Hugging Face configuration reference](https://huggingface.co/docs/hub/spaces-config-reference)
|
| 7 |
+
supports a `thumbnail` URL for social sharing. The README points that field at
|
| 8 |
+
the immutable public copy of the supplied logo; the lesson loads its local copy.
|
| 9 |
+
|
| 10 |
+
## Surface and controls
|
| 11 |
+
|
| 12 |
+
- Header: supplied logo, `tensorViz / shape lab`, runnable-example link.
|
| 13 |
+
- Introduction: one problem statement and an explicit arithmetic-only boundary.
|
| 14 |
+
- Main lesson: batch/height/width inputs, three spatial presets and three
|
| 15 |
+
classifier choices alongside an ordered shape trace. Mismatch/success uses
|
| 16 |
+
both text and restrained color. Linear weight/transposed dimensions are explicit.
|
| 17 |
+
- Reshape counterexample: compare original feature-map rows with the inferred
|
| 18 |
+
first dimension. Distinguish changed batch, impossible reshape and redundant
|
| 19 |
+
reshape when the width already matches.
|
| 20 |
+
- Conclusion: preserve batch, declare the intended input contract, and separate
|
| 21 |
+
shape compatibility from numerical correctness. Optional public source and
|
| 22 |
+
recorded-model links are available without an embedded remote page.
|
| 23 |
+
|
| 24 |
+
Main content is at most 1,040 pixels wide inside a 1,136-pixel outer container.
|
| 25 |
+
Below 760 pixels, controls precede results. Below 390 pixels, shape labels and
|
| 26 |
+
values stack. Interactive targets have native labels and keyboard focus styles;
|
| 27 |
+
validation/results use status announcements. No animation or external fonts.
|
| 28 |
+
|
| 29 |
+
## Rendered acceptance
|
| 30 |
+
|
| 31 |
+
The design follows the existing brand directly; no generated concept is used.
|
| 32 |
+
Automated arithmetic checks do not establish rendered acceptance.
|
| 33 |
+
|
| 34 |
+
Check the actual page on desktop and at 320-pixel width before publication:
|
| 35 |
+
logo proportions, legible controls/shapes, no horizontal
|
| 36 |
+
overflow, all presets/modes/reset, empty and invalid fields, keyboard focus,
|
| 37 |
+
long error wrapping and the optional links. The exact example must begin with
|
| 38 |
+
`(2x512 and 128x5)`, and the unsafe reshape must show two images becoming eight
|
| 39 |
+
rows. Record observed results separately from arithmetic tests.
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 tensorViz
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,91 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description: Follow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: PyTorch Shape Lab
|
| 3 |
+
emoji: 🧮
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: gray
|
| 6 |
sdk: static
|
| 7 |
+
app_file: index.html
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
short_description: Follow a CNN's shapes without losing the batch dimension.
|
| 11 |
+
thumbnail: https://raw.githubusercontent.com/tensorviz-org/tensorviz-examples/6e5cdcfc8ec3ab39c0bb1264bf534210ae5c1981/assets/tensorviz-logo-white.png
|
| 12 |
+
tags:
|
| 13 |
+
- pytorch
|
| 14 |
+
- education
|
| 15 |
+
- tensor-shapes
|
| 16 |
---
|
| 17 |
|
| 18 |
+
# PyTorch shape lab
|
| 19 |
+
|
| 20 |
+
A small interactive lesson by **tensorViz**. Change the batch and image size,
|
| 21 |
+
follow a fixed convolution/pooling stack, and find a mismatch at `Linear`.
|
| 22 |
+
Compare a correctly sized fixed head with an explicit architecture change using
|
| 23 |
+
adaptive average pooling. See how `reshape(-1, 128)` can silently change the
|
| 24 |
+
number of samples.
|
| 25 |
+
|
| 26 |
+
**This is shape arithmetic, not model execution.** The browser runs no Python,
|
| 27 |
+
inference, training or tensorViz backend. Compatible shapes do not establish
|
| 28 |
+
prediction quality or numerical equivalence. The lesson code has no tracking,
|
| 29 |
+
third-party script, iframe, runtime fetch or account requirement. External
|
| 30 |
+
resources load only when the reader follows a link. This describes the lesson
|
| 31 |
+
code, not the hosting platform's ordinary request handling or platform analytics.
|
| 32 |
+
|
| 33 |
+
The original executable example is
|
| 34 |
+
[available with its checks and explanation](https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md).
|
| 35 |
+
The static arithmetic was independently checked against actual CPU PyTorch
|
| 36 |
+
operations; see [validation](VALIDATION.md).
|
| 37 |
+
|
| 38 |
+
## Run locally
|
| 39 |
+
|
| 40 |
+
No package installation or build is needed. Serve this folder using an existing
|
| 41 |
+
Python installation:
|
| 42 |
+
|
| 43 |
+
```sh
|
| 44 |
+
python3 -m http.server 4177 --bind 127.0.0.1
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Open `http://127.0.0.1:4177`. ES modules need an HTTP server; opening `index.html`
|
| 48 |
+
as a local `file:` URL is not the supported preview path.
|
| 49 |
+
|
| 50 |
+
## Lesson boundaries
|
| 51 |
+
|
| 52 |
+
- The fixed feature stack is `Conv2d(3, 8, kernel_size=3, padding=1)`, `ReLU`,
|
| 53 |
+
`MaxPool2d(2)`, then `Flatten(start_dim=1)`.
|
| 54 |
+
- Inputs are NCHW with three channels. This teaching UI accepts batch 1–64 and
|
| 55 |
+
integer height/width 2–256. These UI bounds are not a general PyTorch limit.
|
| 56 |
+
- Pooling uses the default stride of two, padding zero and `ceil_mode=False`.
|
| 57 |
+
Odd spatial dimensions round down.
|
| 58 |
+
- The original head expects 128 features. A 16 × 16 image yields 512. A fixed
|
| 59 |
+
512-feature head fits that example, but not every other input size.
|
| 60 |
+
- The optional adaptive pool reduces each of the eight spatial channels to one
|
| 61 |
+
value and uses an eight-feature head. That changes computation and parameters;
|
| 62 |
+
it is not an interchangeable pretrained checkpoint repair.
|
| 63 |
+
- The unsafe reshape demonstration uses the original feature map before any
|
| 64 |
+
adaptive pool and the original 128-feature head.
|
| 65 |
+
|
| 66 |
+
For real model exploration, [tensorViz's recorded browser examples](https://tensorviz.dev/explore/)
|
| 67 |
+
are an optional next step. Capturing your own PyTorch model requires the separate
|
| 68 |
+
tensorViz extension and your model environment. This repository contains only
|
| 69 |
+
the lesson, not extension implementation.
|
| 70 |
+
|
| 71 |
+
## Validate changes
|
| 72 |
+
|
| 73 |
+
Use existing Node.js and a Python environment with PyTorch installed. Nothing in
|
| 74 |
+
the checks downloads dependencies or enables telemetry.
|
| 75 |
+
|
| 76 |
+
```sh
|
| 77 |
+
node --test shape-math.test.mjs
|
| 78 |
+
python verify_pytorch.py
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
`verify_pytorch.py` also invokes Node.js to compare the JavaScript results with
|
| 82 |
+
PyTorch. Visual and accessibility acceptance remain separate from these checks.
|
| 83 |
+
The intended layout and review cases are in [design notes](DESIGN.md).
|
| 84 |
+
|
| 85 |
+
## License and attribution
|
| 86 |
+
|
| 87 |
+
Original lesson code and documentation are [MIT licensed](LICENSE), including
|
| 88 |
+
the small test model derived from the public tensorViz example linked above.
|
| 89 |
+
PyTorch is a separate dependency under its own license. The supplied tensorViz
|
| 90 |
+
logo identifies the project; this license does not grant trademark rights.
|
| 91 |
+
The extension and its dependencies retain their own licenses.
|
VALIDATION.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Validation
|
| 2 |
+
|
| 3 |
+
This page models a fixed CPU-compatible PyTorch example. It does not execute
|
| 4 |
+
Python in the browser or provide a general-purpose shape inference engine.
|
| 5 |
+
|
| 6 |
+
The JavaScript tests cover the canonical mismatch, batch-preserving fixed head,
|
| 7 |
+
non-square inputs, floor pooling for odd dimensions, adaptive-pooling dimensions,
|
| 8 |
+
unsafe reshape, and invalid input rejection. The separate Python check invokes
|
| 9 |
+
actual PyTorch Conv2d, ReLU, MaxPool2d, Flatten, Linear and AdaptiveAvgPool2d,
|
| 10 |
+
then compares their shapes and errors with the JavaScript calculator for 21 cases.
|
| 11 |
+
For successful paths it also compares full-batch and per-image outputs.
|
| 12 |
+
|
| 13 |
+
## Executed checks — October 11, 2026
|
| 14 |
+
|
| 15 |
+
Environment: Node.js **26.5.0**, Python **3.12.14**, PyTorch **2.7.1**, CPU.
|
| 16 |
+
No dependencies were downloaded or installed for this check.
|
| 17 |
+
|
| 18 |
+
- **5 JavaScript tests passed**, zero failures and zero skips.
|
| 19 |
+
- **21 actual PyTorch comparisons passed**: all three heads for batch/image
|
| 20 |
+
combinations `(1,16,16)`, `(2,16,16)`, `(5,16,16)`, `(2,16,20)`,
|
| 21 |
+
`(3,17,19)`, `(2,8,8)` and `(1,3,3)`.
|
| 22 |
+
- Successful paths preserved the number of samples, and full-batch outputs
|
| 23 |
+
matched concatenated per-image outputs (`atol=1e-6`, `rtol=1e-5`).
|
| 24 |
+
- JavaScript syntax checks passed for both application modules.
|
| 25 |
+
|
| 26 |
+
Representative observations:
|
| 27 |
+
|
| 28 |
+
| Input NCHW | Flatten before optional adaptive pool | Original 128 head | Fixed 512 head | Adaptive pool + 8 head |
|
| 29 |
+
| --- | --- | --- | --- | --- |
|
| 30 |
+
| `[2,3,16,16]` | `[2,512]` | Exact error `(2x512 and 128x5)` | `[2,5]` | `[2,5]` |
|
| 31 |
+
| `[2,3,16,20]` | `[2,640]` | Mismatch | Exact error `(2x640 and 512x5)` | `[2,5]` |
|
| 32 |
+
| `[3,3,17,19]` | `[3,576]` | Mismatch | Mismatch | `[3,5]` |
|
| 33 |
+
| `[2,3,8,8]` | `[2,128]` | `[2,5]` | Mismatch | `[2,5]` |
|
| 34 |
+
| `[1,3,3,3]` | `[1,8]` | Mismatch | Mismatch | `[1,5]` |
|
| 35 |
+
|
| 36 |
+
For the default input, `reshape(-1,128)` changes `[2,512]` to `[8,128]` and
|
| 37 |
+
produces `[8,5]` through the original head. The operation runs but does not
|
| 38 |
+
preserve one output row per image. Odd-sized pooling and impossible/redundant
|
| 39 |
+
reshape cases were also checked against PyTorch.
|
| 40 |
+
|
| 41 |
+
Browser rendering and interaction acceptance are not established by these tests.
|
| 42 |
+
No model accuracy, checkpoint compatibility or extension capture result is
|
| 43 |
+
claimed by this standalone lesson.
|
| 44 |
+
|
| 45 |
+
## Browser QA — October 11, 2026
|
| 46 |
+
|
| 47 |
+
A separate in-app browser pass checked the locally served lesson at **1280 × 720**
|
| 48 |
+
and **390 × 844**. It verified these interactions:
|
| 49 |
+
|
| 50 |
+
- The default input displays `512 ≠ 128` at the original classifier.
|
| 51 |
+
- Choosing the 512-feature head produces the calculated output `[2,5]`.
|
| 52 |
+
- Changing the image to 16 × 20 produces `640 ≠ 512` with that fixed head.
|
| 53 |
+
- Adding adaptive pooling produces `[2,5]` and shows the explicit warning that
|
| 54 |
+
the architecture and computation change.
|
| 55 |
+
- A 17 × 19 image pools to spatial size 8 × 9.
|
| 56 |
+
- Batch zero shows the validation message; Reset restores the starting lesson.
|
| 57 |
+
|
| 58 |
+
The 390-pixel-wide page had readable text and no horizontal overflow. A full-page
|
| 59 |
+
mobile screenshot was saved as `tensorviz-shape-lab-mobile-2026-10-11.png`.
|
| 60 |
+
No lesson-origin console errors were observed. These results cover the two
|
| 61 |
+
listed viewports; 320-pixel rendering and hosted Hugging Face behavior were not
|
| 62 |
+
established by this local pass.
|
| 63 |
+
|
| 64 |
+
A source review also confirmed the visible arithmetic-only notice, batch-preserving
|
| 65 |
+
flatten, explicit adaptive-pooling semantics, unsafe-reshape sample-count warning,
|
| 66 |
+
and distinction between this lesson and actual tensorViz model capture. The code
|
| 67 |
+
adds no runtime fetch or tracking, and the footer limits its claim to calculations
|
| 68 |
+
staying in the reader's browser.
|
app.mjs
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { inspectShapes } from './shape-math.mjs';
|
| 2 |
+
|
| 3 |
+
const byId = (id) => document.getElementById(id);
|
| 4 |
+
const shape = (dimensions) => `[${dimensions.join(', ')}]`;
|
| 5 |
+
const text = (id, value) => { byId(id).textContent = value; };
|
| 6 |
+
const form = byId('shape-form');
|
| 7 |
+
|
| 8 |
+
function render() {
|
| 9 |
+
const result = inspectShapes({
|
| 10 |
+
batch: byId('batch').valueAsNumber,
|
| 11 |
+
height: byId('height').valueAsNumber,
|
| 12 |
+
width: byId('width').valueAsNumber,
|
| 13 |
+
mode: form.elements.mode.value,
|
| 14 |
+
});
|
| 15 |
+
byId('invalid').hidden = result.valid;
|
| 16 |
+
byId('calculation').hidden = !result.valid;
|
| 17 |
+
byId('reshape-result').hidden = !result.valid;
|
| 18 |
+
if (!result.valid) { text('invalid', result.message); return; }
|
| 19 |
+
|
| 20 |
+
const r = result;
|
| 21 |
+
text('shape-input', shape(r.input));
|
| 22 |
+
text('shape-conv', shape(r.conv));
|
| 23 |
+
text('shape-pool', shape(r.pool));
|
| 24 |
+
text('shape-flat', shape(r.flattened));
|
| 25 |
+
byId('adaptive-row').hidden = !r.adaptive;
|
| 26 |
+
if (r.adaptive) text('shape-adaptive', shape(r.adaptive));
|
| 27 |
+
const linearName = byId('linear-name');
|
| 28 |
+
linearName.replaceChildren(document.createTextNode(`Linear(${r.inFeatures}, 5)`));
|
| 29 |
+
const small = document.createElement('small');
|
| 30 |
+
small.textContent = `Expects ${r.inFeatures} features per image`;
|
| 31 |
+
linearName.append(small);
|
| 32 |
+
text('shape-output', r.output ? shape(r.output) : `${r.features} ≠ ${r.inFeatures}`);
|
| 33 |
+
byId('linear-row').className = r.matches ? 'match' : 'mismatch';
|
| 34 |
+
byId('verdict').className = `verdict ${r.matches ? 'match' : 'mismatch'}`;
|
| 35 |
+
text('verdict-title', r.matches ? `The widths match. Batch stays ${r.batch}.` : `${r.features} features arrived. The head expects ${r.inFeatures}.`);
|
| 36 |
+
text('feature-math', r.adaptive
|
| 37 |
+
? '8 channels × 1 height × 1 width = 8 features per image after adaptive pooling.'
|
| 38 |
+
: `8 channels × ${r.pool[2]} height × ${r.pool[3]} width = ${r.features} features per image.`);
|
| 39 |
+
text('matrix-left', shape(r.flattened));
|
| 40 |
+
text('matrix-right', shape([r.inFeatures, 5]));
|
| 41 |
+
text('matrix-answer', r.output ? `= ${shape(r.output)}` : 'incompatible');
|
| 42 |
+
text('weight-shape', shape(r.weight));
|
| 43 |
+
byId('error-message').hidden = r.matches;
|
| 44 |
+
text('error-message', r.error ?? '');
|
| 45 |
+
const notes = {
|
| 46 |
+
broken: r.matches
|
| 47 |
+
? 'This input produces exactly 128 features per image. Try 16 × 16: the same fixed head will no longer fit.'
|
| 48 |
+
: 'The fixed head expects 128 features per image. Match the head to the intended input contract; changing the batch does not change this width.',
|
| 49 |
+
fixed: r.matches
|
| 50 |
+
? '512 fits the 16 × 16 example. Try a wider image: changing the input can break a fixed-width head again.'
|
| 51 |
+
: '512 is fixed for the original 16 × 16 example. This input has a different feature width. A successful earlier capture does not cover every input.',
|
| 52 |
+
pooled: 'Adaptive pooling averages each spatial channel to one value, so this head receives 8 features. This changes the architecture and the classifier weights; it is not the same computation as the fixed-width model.',
|
| 53 |
+
};
|
| 54 |
+
text('mode-note', notes[r.mode]);
|
| 55 |
+
text('reshape-before', shape([r.batch, r.originalFeatures]));
|
| 56 |
+
const unsafe = r.unsafeReshape;
|
| 57 |
+
text('reshape-after', unsafe.possible ? shape(unsafe.shape) : 'cannot reshape');
|
| 58 |
+
const warning = byId('reshape-warning');
|
| 59 |
+
warning.replaceChildren();
|
| 60 |
+
const strong = document.createElement('strong');
|
| 61 |
+
strong.textContent = !unsafe.possible
|
| 62 |
+
? `${unsafe.elements} elements cannot fill rows of 128.`
|
| 63 |
+
: unsafe.preservesBatch
|
| 64 |
+
? `The feature width was already 128.`
|
| 65 |
+
: `${r.batch} ${r.batch === 1 ? 'image became' : 'images became'} ${unsafe.shape[0]} ${unsafe.shape[0] === 1 ? 'row' : 'rows'}.`;
|
| 66 |
+
warning.append(strong, document.createTextNode(!unsafe.possible
|
| 67 |
+
? ' This reshape would fail before Linear. Use a batch-preserving flatten and inspect the feature width.'
|
| 68 |
+
: unsafe.preservesBatch
|
| 69 |
+
? ' This reshape is redundant here. It does not repair a mismatch or make other image sizes safe.'
|
| 70 |
+
: ` Linear can now return ${shape(unsafe.output)}, but that is not one prediction per image.`));
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
form.addEventListener('input', render);
|
| 74 |
+
form.addEventListener('change', render);
|
| 75 |
+
form.addEventListener('submit', (event) => event.preventDefault());
|
| 76 |
+
document.querySelectorAll('[data-height]').forEach((button) => button.addEventListener('click', () => {
|
| 77 |
+
byId('height').value = button.dataset.height;
|
| 78 |
+
byId('width').value = button.dataset.width;
|
| 79 |
+
render();
|
| 80 |
+
}));
|
| 81 |
+
byId('reset').addEventListener('click', () => { form.reset(); render(); });
|
| 82 |
+
render();
|
index.html
CHANGED
|
@@ -1,19 +1,104 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<meta name="description" content="An interactive PyTorch shape-arithmetic lesson: follow a small CNN, find a Linear mismatch, and see why a reshape can change your batch.">
|
| 7 |
+
<meta name="theme-color" content="#ffffff">
|
| 8 |
+
<title>PyTorch shape lab · tensorViz</title>
|
| 9 |
+
<link rel="icon" href="logo.png">
|
| 10 |
+
<link rel="stylesheet" href="styles.css">
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<a class="skip-link" href="#lab">Skip to the lesson</a>
|
| 14 |
+
<main>
|
| 15 |
+
<header class="brand">
|
| 16 |
+
<img src="logo.png" width="34" height="34" alt="tensorViz logo">
|
| 17 |
+
<span>tensorViz <span class="brand-separator">/</span> <span class="brand-label">shape lab</span></span>
|
| 18 |
+
<a href="https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md">Runnable example</a>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<section class="intro" aria-labelledby="page-title">
|
| 22 |
+
<h1 id="page-title">Your batch is fine.<br>Check the features.</h1>
|
| 23 |
+
<p>A small image model reaches <code>Linear</code> with the wrong width.
|
| 24 |
+
Change the input, follow the shapes, and find the contract that broke.</p>
|
| 25 |
+
<p class="boundary">Shape arithmetic only. This page does not run Python, a model or inference.</p>
|
| 26 |
+
</section>
|
| 27 |
+
|
| 28 |
+
<section id="lab" class="lab" aria-labelledby="lab-title">
|
| 29 |
+
<div class="controls">
|
| 30 |
+
<h2 id="lab-title">Try an input</h2>
|
| 31 |
+
<p class="section-note">NCHW · 3 input channels</p>
|
| 32 |
+
<form id="shape-form" novalidate>
|
| 33 |
+
<div class="dimensions">
|
| 34 |
+
<label for="batch">Batch <input id="batch" name="batch" type="number" min="1" max="64" step="1" value="2" inputmode="numeric" aria-describedby="input-bounds"></label>
|
| 35 |
+
<label for="height">Height <input id="height" name="height" type="number" min="2" max="256" step="1" value="16" inputmode="numeric" aria-describedby="input-bounds"></label>
|
| 36 |
+
<label for="width">Width <input id="width" name="width" type="number" min="2" max="256" step="1" value="16" inputmode="numeric" aria-describedby="input-bounds"></label>
|
| 37 |
+
</div>
|
| 38 |
+
<p id="input-bounds" class="field-help">Batch 1–64. Height and width 2–256.</p>
|
| 39 |
+
<div class="presets" aria-label="Example image sizes">
|
| 40 |
+
<button type="button" data-height="16" data-width="16">16 × 16</button>
|
| 41 |
+
<button type="button" data-height="16" data-width="20">16 × 20</button>
|
| 42 |
+
<button type="button" data-height="17" data-width="19">17 × 19</button>
|
| 43 |
+
</div>
|
| 44 |
+
<fieldset>
|
| 45 |
+
<legend>Choose the classifier</legend>
|
| 46 |
+
<label class="mode"><input type="radio" name="mode" value="broken" checked><span>Original head<strong>Linear(128, 5)</strong></span></label>
|
| 47 |
+
<label class="mode"><input type="radio" name="mode" value="fixed"><span>Match the 16 × 16 input<strong>Linear(512, 5)</strong></span></label>
|
| 48 |
+
<label class="mode"><input type="radio" name="mode" value="pooled"><span>Add adaptive average pooling<strong>Pool to 1 × 1 → Linear(8, 5)</strong></span></label>
|
| 49 |
+
</fieldset>
|
| 50 |
+
</form>
|
| 51 |
+
<button id="reset" type="button" class="reset">Reset lesson</button>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="results" aria-label="Calculated tensor shapes">
|
| 55 |
+
<div id="invalid" class="invalid" role="status" hidden></div>
|
| 56 |
+
<div id="calculation">
|
| 57 |
+
<ol class="flow" aria-label="Shape at each layer">
|
| 58 |
+
<li><div><span class="step">01</span><span>Input</span></div><output id="shape-input">[2, 3, 16, 16]</output></li>
|
| 59 |
+
<li><div><span class="step">02</span><span>Conv2d + ReLU<small>3 → 8 channels · kernel 3 · padding 1</small></span></div><output id="shape-conv">[2, 8, 16, 16]</output></li>
|
| 60 |
+
<li><div><span class="step">03</span><span>MaxPool2d(2)<small>Spatial sizes round down</small></span></div><output id="shape-pool">[2, 8, 8, 8]</output></li>
|
| 61 |
+
<li id="adaptive-row" hidden><div><span class="step">+</span><span>AdaptiveAvgPool2d((1, 1))<small>New architecture: average each channel</small></span></div><output id="shape-adaptive"></output></li>
|
| 62 |
+
<li><div><span class="step">04</span><span>Flatten(start_dim=1)<small>Keep the first dimension: batch</small></span></div><output id="shape-flat">[2, 512]</output></li>
|
| 63 |
+
<li id="linear-row" class="mismatch"><div><span class="step">05</span><span id="linear-name">Linear(128, 5)<small>Expects 128 features per image</small></span></div><output id="shape-output">512 ≠ 128</output></li>
|
| 64 |
+
</ol>
|
| 65 |
+
|
| 66 |
+
<section id="verdict" class="verdict mismatch" aria-live="polite" aria-atomic="true">
|
| 67 |
+
<h3 id="verdict-title">512 features arrived. The head expects 128.</h3>
|
| 68 |
+
<p id="feature-math">8 channels × 8 height × 8 width = 512 features per image.</p>
|
| 69 |
+
<p class="matrix"><span id="matrix-left">[2, 512]</span><span aria-label="multiplied by">×</span><span id="matrix-right">[128, 5]</span><span id="matrix-answer">incompatible</span></p>
|
| 70 |
+
<p class="matrix-note">Linear stores its weight as <code id="weight-shape">[5, 128]</code> and multiplies by its transpose.</p>
|
| 71 |
+
<code id="error-message" class="error">mat1 and mat2 shapes cannot be multiplied (2x512 and 128x5)</code>
|
| 72 |
+
</section>
|
| 73 |
+
<p id="mode-note" class="mode-note">Change the head to 512 to match this 16 × 16 example. Then try a wider image: the fixed width is a contract, not a universal fix.</p>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</section>
|
| 77 |
+
|
| 78 |
+
<section class="reshape-section" aria-labelledby="reshape-title">
|
| 79 |
+
<div>
|
| 80 |
+
<h2 id="reshape-title">A reshape can hide the error.</h2>
|
| 81 |
+
<p>On the original feature map, <code>reshape(-1, 128)</code> asks PyTorch to infer a new first dimension. That dimension may no longer count your images.</p>
|
| 82 |
+
<p class="field-help">This comparison uses the feature map before any optional adaptive pooling.</p>
|
| 83 |
+
</div>
|
| 84 |
+
<div id="reshape-result" class="reshape-result" aria-live="polite">
|
| 85 |
+
<p class="reshape-flow"><code id="reshape-before">[2, 512]</code><span>→ reshape →</span><code id="reshape-after">[8, 128]</code></p>
|
| 86 |
+
<p id="reshape-warning"><strong>2 images became 8 rows.</strong> Linear can now return [8, 5], but that is not one prediction per image.</p>
|
| 87 |
+
</div>
|
| 88 |
+
</section>
|
| 89 |
+
|
| 90 |
+
<section class="takeaway" aria-labelledby="takeaway-title">
|
| 91 |
+
<h2 id="takeaway-title">Make the contract explicit.</h2>
|
| 92 |
+
<p>Use <code>Flatten(start_dim=1)</code> to preserve the batch. Match the classifier width to the feature extractor and intended input size. Adaptive pooling is another design choice: it changes the computation and needs a different classifier.</p>
|
| 93 |
+
<p>Matching shapes only makes these operations compatible. It does not establish prediction quality or numerical equivalence.</p>
|
| 94 |
+
<div class="resource-links">
|
| 95 |
+
<a href="https://github.com/tensorviz-org/tensorviz-examples/blob/1baecebf25ff1067a9112bde382c0a398fb99747/examples/shape-mismatch/README.md">Read and run the PyTorch example</a>
|
| 96 |
+
<a href="https://tensorviz.dev/explore/">Explore recorded models in tensorViz</a>
|
| 97 |
+
</div>
|
| 98 |
+
</section>
|
| 99 |
+
<footer>Made by tensorViz with AI assistance. Original lesson code is MIT licensed. Calculations stay in your browser.</footer>
|
| 100 |
+
<noscript><p class="invalid">Enable JavaScript to change the inputs. The displayed default example shows a batch of two 16 × 16 images failing at Linear(128, 5).</p></noscript>
|
| 101 |
+
</main>
|
| 102 |
+
<script type="module" src="app.mjs"></script>
|
| 103 |
+
</body>
|
| 104 |
</html>
|
logo.png
ADDED
|
Git LFS Details
|
shape-math.mjs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const MODES = Object.freeze({ broken: 128, fixed: 512, pooled: 8 });
|
| 2 |
+
|
| 3 |
+
// This lesson has a fixed Conv2d/ReLU/MaxPool2d stack, not a general shape engine.
|
| 4 |
+
export function inspectShapes({ batch, height, width, mode = 'broken' }) {
|
| 5 |
+
const values = { batch, height, width };
|
| 6 |
+
for (const [name, value] of Object.entries(values)) {
|
| 7 |
+
const min = name === 'batch' ? 1 : 2;
|
| 8 |
+
const max = name === 'batch' ? 64 : 256;
|
| 9 |
+
if (!Number.isInteger(value) || value < min || value > max) {
|
| 10 |
+
return { valid: false, message: `${name === 'batch' ? 'Batch size' : name === 'height' ? 'Height' : 'Width'} must be a whole number from ${min} to ${max}.` };
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
if (!Object.hasOwn(MODES, mode)) return { valid: false, message: 'Choose one of the three classifier heads.' };
|
| 14 |
+
|
| 15 |
+
const pooledHeight = Math.floor(height / 2);
|
| 16 |
+
const pooledWidth = Math.floor(width / 2);
|
| 17 |
+
const originalFeatures = 8 * pooledHeight * pooledWidth;
|
| 18 |
+
const features = mode === 'pooled' ? 8 : originalFeatures;
|
| 19 |
+
const inFeatures = MODES[mode];
|
| 20 |
+
const matches = features === inFeatures;
|
| 21 |
+
const reshapedBatch = batch * originalFeatures / 128;
|
| 22 |
+
const canReshape = Number.isInteger(reshapedBatch);
|
| 23 |
+
return {
|
| 24 |
+
valid: true,
|
| 25 |
+
batch, height, width, mode,
|
| 26 |
+
input: [batch, 3, height, width],
|
| 27 |
+
conv: [batch, 8, height, width],
|
| 28 |
+
pool: [batch, 8, pooledHeight, pooledWidth],
|
| 29 |
+
adaptive: mode === 'pooled' ? [batch, 8, 1, 1] : null,
|
| 30 |
+
flattened: [batch, features],
|
| 31 |
+
weight: [5, inFeatures],
|
| 32 |
+
output: matches ? [batch, 5] : null,
|
| 33 |
+
features, originalFeatures, inFeatures, matches,
|
| 34 |
+
error: matches ? null : `mat1 and mat2 shapes cannot be multiplied (${batch}x${features} and ${inFeatures}x5)`,
|
| 35 |
+
unsafeReshape: {
|
| 36 |
+
possible: canReshape,
|
| 37 |
+
shape: canReshape ? [reshapedBatch, 128] : null,
|
| 38 |
+
output: canReshape ? [reshapedBatch, 5] : null,
|
| 39 |
+
preservesBatch: canReshape && reshapedBatch === batch,
|
| 40 |
+
elements: batch * originalFeatures,
|
| 41 |
+
},
|
| 42 |
+
};
|
| 43 |
+
}
|
shape-math.test.mjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import test from 'node:test';
|
| 2 |
+
import assert from 'node:assert/strict';
|
| 3 |
+
import { inspectShapes } from './shape-math.mjs';
|
| 4 |
+
|
| 5 |
+
test('the canonical mismatch keeps two images and locates the feature error', () => {
|
| 6 |
+
const r = inspectShapes({ batch: 2, height: 16, width: 16 });
|
| 7 |
+
assert.deepEqual(r.flattened, [2, 512]);
|
| 8 |
+
assert.deepEqual(r.weight, [5, 128]);
|
| 9 |
+
assert.equal(r.error, 'mat1 and mat2 shapes cannot be multiplied (2x512 and 128x5)');
|
| 10 |
+
assert.equal(r.output, null);
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
test('a fixed-width repair preserves batch but does not promise arbitrary inputs', () => {
|
| 14 |
+
for (const batch of [1, 2, 5]) {
|
| 15 |
+
assert.deepEqual(inspectShapes({ batch, height: 16, width: 16, mode: 'fixed' }).output, [batch, 5]);
|
| 16 |
+
}
|
| 17 |
+
assert.equal(inspectShapes({ batch: 2, height: 16, width: 20, mode: 'fixed' }).matches, false);
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
test('odd dimensions use floor pooling and the explicit adaptive stage changes the head', () => {
|
| 21 |
+
const r = inspectShapes({ batch: 3, height: 17, width: 19, mode: 'pooled' });
|
| 22 |
+
assert.deepEqual(r.pool, [3, 8, 8, 9]);
|
| 23 |
+
assert.deepEqual(r.adaptive, [3, 8, 1, 1]);
|
| 24 |
+
assert.deepEqual(r.flattened, [3, 8]);
|
| 25 |
+
assert.deepEqual(r.output, [3, 5]);
|
| 26 |
+
});
|
| 27 |
+
|
| 28 |
+
test('making Linear run by reshape must not be mistaken for preserving samples', () => {
|
| 29 |
+
const r = inspectShapes({ batch: 2, height: 16, width: 16 });
|
| 30 |
+
assert.deepEqual(r.unsafeReshape.shape, [8, 128]);
|
| 31 |
+
assert.deepEqual(r.unsafeReshape.output, [8, 5]);
|
| 32 |
+
assert.equal(r.unsafeReshape.preservesBatch, false);
|
| 33 |
+
assert.equal(inspectShapes({ batch: 1, height: 3, width: 3 }).unsafeReshape.possible, false);
|
| 34 |
+
assert.equal(inspectShapes({ batch: 2, height: 8, width: 8 }).unsafeReshape.preservesBatch, true);
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
test('invalid inputs are rejected instead of showing stale or impossible shapes', () => {
|
| 38 |
+
const base = { batch: 2, height: 16, width: 16 };
|
| 39 |
+
for (const [name, value] of [['batch', 0], ['batch', 65], ['height', 1], ['width', 257], ['width', 2.5], ['height', NaN], ['batch', Infinity], ['mode', 'other']]) {
|
| 40 |
+
const r = inspectShapes({ ...base, [name]: value });
|
| 41 |
+
assert.equal(r.valid, false, `${name}=${value}`);
|
| 42 |
+
assert.ok(r.message);
|
| 43 |
+
assert.equal(r.flattened, undefined);
|
| 44 |
+
}
|
| 45 |
+
});
|
styles.css
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: light;
|
| 3 |
+
--ink: #1b2520;
|
| 4 |
+
--muted: #5f6e65;
|
| 5 |
+
--line: #dce4de;
|
| 6 |
+
--green: #176047;
|
| 7 |
+
--green-light: #edf6f0;
|
| 8 |
+
--red: #a1392a;
|
| 9 |
+
--red-light: #fff3ee;
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 11 |
+
color: var(--ink);
|
| 12 |
+
background: #ffffff;
|
| 13 |
+
}
|
| 14 |
+
* { box-sizing: border-box; }
|
| 15 |
+
body { margin: 0; }
|
| 16 |
+
main { max-width: 1136px; padding: 28px 48px 24px; margin: auto; }
|
| 17 |
+
a { color: var(--green); text-underline-offset: 4px; text-decoration-thickness: 1px; }
|
| 18 |
+
a:hover { text-decoration-thickness: 2px; }
|
| 19 |
+
button, input { font: inherit; }
|
| 20 |
+
button { cursor: pointer; }
|
| 21 |
+
button:focus-visible, input:focus-visible, a:focus-visible { outline: 3px solid #76aa90; outline-offset: 4px; }
|
| 22 |
+
[hidden] { display: none !important; }
|
| 23 |
+
code, output { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
| 24 |
+
code { font-size: .9em; }
|
| 25 |
+
.brand { display: flex; gap: 12px; align-items: center; font-size: 18px; font-weight: 650; }
|
| 26 |
+
.brand img { object-fit: contain; flex: 0 0 auto; }
|
| 27 |
+
.brand-separator { color: #a4b0a8; margin: 0 8px; font-weight: 400; }
|
| 28 |
+
.brand-label { font-weight: 450; }
|
| 29 |
+
.brand a { margin-left: auto; font-size: 13px; font-weight: 500; }
|
| 30 |
+
.intro { padding: 58px 0 32px; max-width: 730px; }
|
| 31 |
+
h1 { font-size: clamp(32px, 4.5vw, 50px); line-height: 1.06; letter-spacing: -.045em; margin: 0 0 20px; font-weight: 650; }
|
| 32 |
+
p { font-size: 16px; line-height: 1.6; margin: 0 0 12px; }
|
| 33 |
+
.intro > p { max-width: 650px; color: var(--muted); }
|
| 34 |
+
.intro .boundary { font-size: 12px; color: var(--muted); margin-top: 18px; }
|
| 35 |
+
.lab { display: grid; grid-template-columns: 285px minmax(0, 1fr); border-block: 1px solid var(--line); }
|
| 36 |
+
.controls { padding: 28px 26px 28px 0; border-right: 1px solid var(--line); }
|
| 37 |
+
h2 { font-size: 21px; letter-spacing: -.025em; line-height: 1.25; margin: 0 0 9px; font-weight: 630; }
|
| 38 |
+
.section-note, .field-help { font-size: 12px; color: var(--muted); }
|
| 39 |
+
.section-note { margin-bottom: 21px; }
|
| 40 |
+
.dimensions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
| 41 |
+
.dimensions label { font-size: 12px; font-weight: 600; }
|
| 42 |
+
.dimensions input { display: block; margin-top: 8px; width: 100%; border: 1px solid #b8c7bd; border-radius: 7px; background: #fff; padding: 10px 8px; font-size: 19px; color: var(--ink); }
|
| 43 |
+
.field-help { margin-top: 8px; line-height: 1.5; }
|
| 44 |
+
.presets { display: flex; gap: 6px; margin: 16px 0 29px; flex-wrap: wrap; }
|
| 45 |
+
.presets button { font-size: 11px; padding: 7px 9px; background: white; border: 1px solid var(--line); border-radius: 5px; color: var(--muted); }
|
| 46 |
+
.presets button:hover { border-color: var(--green); color: var(--green); }
|
| 47 |
+
fieldset { border: 0; padding: 0; margin: 0; }
|
| 48 |
+
legend { font-size: 12px; font-weight: 650; margin-bottom: 10px; }
|
| 49 |
+
.mode { display: flex; align-items: flex-start; gap: 9px; padding: 12px 7px; border-bottom: 1px solid var(--line); font-size: 12px; line-height: 1.4; cursor: pointer; }
|
| 50 |
+
.mode:has(input:checked) { background: var(--green-light); border-color: transparent; border-radius: 6px; }
|
| 51 |
+
.mode input { accent-color: var(--green); margin: 2px 0 0; flex: 0 0 auto; }
|
| 52 |
+
.mode strong { display: block; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 11px; font-weight: 500; margin-top: 5px; color: var(--muted); }
|
| 53 |
+
.reset { border: 0; background: none; padding: 0; color: var(--muted); text-decoration: underline; text-underline-offset: 4px; margin-top: 22px; font-size: 12px; }
|
| 54 |
+
.results { padding: 19px 0 26px 30px; min-width: 0; }
|
| 55 |
+
.flow { list-style: none; padding: 0; margin: 0; }
|
| 56 |
+
.flow li { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
|
| 57 |
+
.flow li > div { display: flex; gap: 11px; align-items: baseline; }
|
| 58 |
+
.step { color: #89988e; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
| 59 |
+
.flow small { display: block; color: var(--muted); font-size: 11px; line-height: 1.5; margin-top: 3px; }
|
| 60 |
+
.flow output { font-size: 12px; white-space: nowrap; }
|
| 61 |
+
.flow .mismatch output { color: var(--red); font-weight: 600; }
|
| 62 |
+
.flow .match output { color: var(--green); font-weight: 600; }
|
| 63 |
+
.verdict { padding: 18px 20px; margin-top: 20px; border-radius: 8px; }
|
| 64 |
+
.verdict.mismatch { background: var(--red-light); }
|
| 65 |
+
.verdict.match { background: var(--green-light); }
|
| 66 |
+
h3 { margin: 0 0 8px; font-size: 16px; line-height: 1.4; font-weight: 650; }
|
| 67 |
+
.verdict p { font-size: 12px; line-height: 1.55; }
|
| 68 |
+
.matrix { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; padding: 11px 0 2px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
|
| 69 |
+
.matrix span:nth-child(1), .matrix span:nth-child(3) { font-size: 20px; letter-spacing: -.04em; }
|
| 70 |
+
.matrix span:last-child { font-size: 11px; color: var(--muted); }
|
| 71 |
+
.matrix-note { color: var(--muted); margin-top: 5px; }
|
| 72 |
+
.error { display: block; font-size: 11px; line-height: 1.6; color: var(--red); overflow-wrap: anywhere; margin-top: 10px; }
|
| 73 |
+
.mode-note { font-size: 12px; color: var(--muted); margin: 14px 0 0; }
|
| 74 |
+
.invalid { padding: 18px; border-left: 3px solid var(--red); background: var(--red-light); color: var(--red); font-size: 14px; }
|
| 75 |
+
.reshape-section { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; padding: 38px 0; border-bottom: 1px solid var(--line); }
|
| 76 |
+
.reshape-section p, .takeaway p { font-size: 14px; color: var(--muted); }
|
| 77 |
+
.reshape-section .field-help { font-size: 11px; }
|
| 78 |
+
.reshape-result { align-self: center; background: #f5f7f5; padding: 22px; border-radius: 8px; }
|
| 79 |
+
.reshape-result p { font-size: 13px; }
|
| 80 |
+
.reshape-result strong { color: var(--ink); }
|
| 81 |
+
.reshape-flow { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
|
| 82 |
+
.reshape-flow code { font-size: 20px; color: var(--ink); }
|
| 83 |
+
.reshape-flow span { font-size: 11px; }
|
| 84 |
+
.takeaway { max-width: 800px; padding: 32px 0 28px; }
|
| 85 |
+
.resource-links { display: flex; flex-wrap: wrap; gap: 12px 28px; margin-top: 22px; font-size: 13px; }
|
| 86 |
+
footer { border-top: 1px solid var(--line); padding-top: 17px; color: var(--muted); font-size: 11px; line-height: 1.6; }
|
| 87 |
+
.skip-link { position: absolute; left: 12px; top: -100px; background: #fff; padding: 10px; z-index: 1; }
|
| 88 |
+
.skip-link:focus { top: 10px; }
|
| 89 |
+
@media (max-width: 760px) {
|
| 90 |
+
main { padding: 22px 24px; }
|
| 91 |
+
.brand a { display: none; }
|
| 92 |
+
.intro { padding-top: 40px; }
|
| 93 |
+
.lab { grid-template-columns: 1fr; }
|
| 94 |
+
.controls { border-right: 0; border-bottom: 1px solid var(--line); padding-right: 0; }
|
| 95 |
+
.dimensions { max-width: 380px; }
|
| 96 |
+
.presets { margin-bottom: 24px; }
|
| 97 |
+
.results { padding-left: 0; padding-top: 12px; }
|
| 98 |
+
.reshape-section { grid-template-columns: 1fr; gap: 10px; padding: 30px 0; }
|
| 99 |
+
}
|
| 100 |
+
@media (max-width: 390px) {
|
| 101 |
+
main { padding: 18px 18px; }
|
| 102 |
+
.brand { font-size: 16px; }
|
| 103 |
+
.brand-separator { margin: 0 4px; }
|
| 104 |
+
h1 { font-size: 32px; }
|
| 105 |
+
.flow li { align-items: flex-start; flex-direction: column; gap: 7px; }
|
| 106 |
+
.flow output { padding-left: 25px; }
|
| 107 |
+
.verdict { padding: 16px; }
|
| 108 |
+
}
|
verify_pytorch.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare the teaching arithmetic with real CPU operations; no downloads or UI.
|
| 2 |
+
|
| 3 |
+
Model derived from the MIT tensorViz shape-mismatch example, commit
|
| 4 |
+
1baecebf25ff1067a9112bde382c0a398fb99747. See README for the public source.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import platform
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from torch import nn
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parent
|
| 17 |
+
CASES = [
|
| 18 |
+
{"batch": batch, "height": height, "width": width, "mode": mode}
|
| 19 |
+
for batch, height, width in [
|
| 20 |
+
(1, 16, 16), (2, 16, 16), (5, 16, 16),
|
| 21 |
+
(2, 16, 20), (3, 17, 19), (2, 8, 8), (1, 3, 3),
|
| 22 |
+
]
|
| 23 |
+
for mode in ["broken", "fixed", "pooled"]
|
| 24 |
+
]
|
| 25 |
+
NODE_SCRIPT = """
|
| 26 |
+
import { inspectShapes } from './shape-math.mjs';
|
| 27 |
+
let text = '';
|
| 28 |
+
for await (const chunk of process.stdin) text += chunk;
|
| 29 |
+
console.log(JSON.stringify(JSON.parse(text).map(inspectShapes)));
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
actual_js = json.loads(subprocess.check_output(
|
| 35 |
+
["node", "--input-type=module", "-e", NODE_SCRIPT],
|
| 36 |
+
input=json.dumps(CASES), text=True, cwd=ROOT,
|
| 37 |
+
))
|
| 38 |
+
torch.manual_seed(0)
|
| 39 |
+
checked = []
|
| 40 |
+
with torch.inference_mode():
|
| 41 |
+
for case, result in zip(CASES, actual_js, strict=True):
|
| 42 |
+
b, h, w = case["batch"], case["height"], case["width"]
|
| 43 |
+
x = torch.randn(b, 3, h, w, device="cpu")
|
| 44 |
+
features = nn.Sequential(nn.Conv2d(3, 8, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2)).eval()
|
| 45 |
+
conv = features[:2](x)
|
| 46 |
+
pooled = features[2](conv)
|
| 47 |
+
original_flat = nn.Flatten(start_dim=1)(pooled)
|
| 48 |
+
assert list(x.shape) == result["input"]
|
| 49 |
+
assert list(conv.shape) == result["conv"]
|
| 50 |
+
assert list(pooled.shape) == result["pool"]
|
| 51 |
+
assert original_flat.shape[0] == b, "Flatten must keep the sample count"
|
| 52 |
+
stage = nn.AdaptiveAvgPool2d((1, 1))(pooled) if case["mode"] == "pooled" else pooled
|
| 53 |
+
if case["mode"] == "pooled":
|
| 54 |
+
assert list(stage.shape) == result["adaptive"]
|
| 55 |
+
flat = nn.Flatten(start_dim=1)(stage)
|
| 56 |
+
assert list(flat.shape) == result["flattened"]
|
| 57 |
+
head = nn.Linear({"broken": 128, "fixed": 512, "pooled": 8}[case["mode"]], 5).eval()
|
| 58 |
+
assert list(head.weight.shape) == result["weight"]
|
| 59 |
+
try:
|
| 60 |
+
out = head(flat)
|
| 61 |
+
except RuntimeError as exc:
|
| 62 |
+
assert not result["matches"]
|
| 63 |
+
assert str(exc) == result["error"]
|
| 64 |
+
else:
|
| 65 |
+
assert result["matches"]
|
| 66 |
+
assert list(out.shape) == result["output"] == [b, 5]
|
| 67 |
+
# A batch-preserving path must give one independently computed row per image.
|
| 68 |
+
per_image = []
|
| 69 |
+
for image in x.split(1):
|
| 70 |
+
value = features(image)
|
| 71 |
+
if case["mode"] == "pooled":
|
| 72 |
+
value = nn.AdaptiveAvgPool2d((1, 1))(value)
|
| 73 |
+
per_image.append(head(value.flatten(1)))
|
| 74 |
+
torch.testing.assert_close(out, torch.cat(per_image), atol=1e-6, rtol=1e-5)
|
| 75 |
+
try:
|
| 76 |
+
unsafe = original_flat.reshape(-1, 128)
|
| 77 |
+
except RuntimeError:
|
| 78 |
+
assert not result["unsafeReshape"]["possible"]
|
| 79 |
+
else:
|
| 80 |
+
expected = result["unsafeReshape"]
|
| 81 |
+
assert list(unsafe.shape) == expected["shape"]
|
| 82 |
+
unsafe_out = nn.Linear(128, 5)(unsafe)
|
| 83 |
+
assert list(unsafe_out.shape) == expected["output"]
|
| 84 |
+
assert (unsafe.shape[0] == b) == expected["preservesBatch"]
|
| 85 |
+
checked.append({**case, "flattened": result["flattened"], "output": result["output"], "error": result["error"]})
|
| 86 |
+
print(json.dumps({"python": platform.python_version(), "torch": torch.__version__, "device": "cpu", "cases_passed": len(checked), "cases": checked}, indent=2))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
main()
|