nnscope 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nnscope/stats.py ADDED
@@ -0,0 +1,112 @@
1
+ """Numbers the dashboard shows: norms, update ratios, histograms, activation statistics and 2-D projections."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Dict, List, Optional
5
+
6
+ import numpy as np
7
+
8
+ SATURATING = {"tanh": (-0.97, 0.97), "sigmoid": (0.03, 0.97)} # outside this range the unit's gradient ≈ 0
9
+
10
+
11
+ def sig(x: np.ndarray, digits: int = 4) -> list:
12
+ """Round to `digits` significant figures and return a JSON-friendly nested list."""
13
+ a = np.asarray(x, dtype=np.float64)
14
+ with np.errstate(divide="ignore", invalid="ignore"):
15
+ mag = np.where(a == 0, 0, np.floor(np.log10(np.abs(a))))
16
+ scale = 10.0 ** (digits - 1 - mag)
17
+ r = np.round(a * scale) / scale
18
+ r[~np.isfinite(r)] = 0.0
19
+ return r.tolist()
20
+
21
+
22
+ def fnum(x: Optional[float], digits: int = 5) -> Optional[float]:
23
+ if x is None or not np.isfinite(x):
24
+ return None
25
+ return float(f"{float(x):.{digits}g}")
26
+
27
+
28
+ def select_neurons(n: int, max_n: int) -> List[int]:
29
+ """Indices of at most `max_n` neurons, evenly spread over `n` (all of them if n ≤ max_n)."""
30
+ if n <= max_n:
31
+ return list(range(n))
32
+ return sorted(set(np.round(np.linspace(0, n - 1, max_n)).astype(int).tolist()))
33
+
34
+
35
+ def histogram(x: Optional[np.ndarray], bins: int = 30) -> Optional[dict]:
36
+ if x is None:
37
+ return None
38
+ a = np.asarray(x, dtype=np.float64).ravel()
39
+ a = a[np.isfinite(a)]
40
+ if a.size == 0:
41
+ return None
42
+ lo, hi = float(a.min()), float(a.max())
43
+ if hi - lo < 1e-12:
44
+ lo, hi = lo - 0.5, hi + 0.5
45
+ counts, _ = np.histogram(a, bins=bins, range=(lo, hi))
46
+ return {"lo": fnum(lo), "hi": fnum(hi), "counts": counts.tolist()}
47
+
48
+
49
+ def layer_numbers(W: np.ndarray, dW: Optional[np.ndarray], prev_W: Optional[np.ndarray]) -> Dict[str, Optional[float]]:
50
+ """Per-layer scalars logged every step."""
51
+ w_norm = float(np.linalg.norm(W))
52
+ out = {
53
+ "w_norm": fnum(w_norm),
54
+ "w_mean_abs": fnum(np.mean(np.abs(W))),
55
+ "g_norm": fnum(np.linalg.norm(dW)) if dW is not None else None,
56
+ "g_mean_abs": fnum(np.mean(np.abs(dW))) if dW is not None else None,
57
+ "update_ratio": None,
58
+ }
59
+ if prev_W is not None and prev_W.shape == W.shape and w_norm > 0:
60
+ out["update_ratio"] = fnum(np.linalg.norm(W - prev_W) / max(np.linalg.norm(prev_W), 1e-12))
61
+ return out
62
+
63
+
64
+ def activation_numbers(A: Optional[np.ndarray], act: Optional[str]) -> Optional[dict]:
65
+ """Per-neuron statistics of a batch of activations A (batch, units).
66
+
67
+ * relu-like: a unit is *active* when its output > 0; a unit never active on the batch is *dead*.
68
+ * tanh / sigmoid: a unit is *active* when it is not saturated; one saturated on every sample is flagged *saturated*.
69
+ """
70
+ if A is None:
71
+ return None
72
+ A = np.asarray(A, dtype=np.float64)
73
+ if A.ndim == 1:
74
+ A = A[:, None]
75
+ A = A.reshape(A.shape[0], -1)
76
+ if act in SATURATING:
77
+ lo, hi = SATURATING[act]
78
+ active = (A > lo) & (A < hi)
79
+ kind = "saturated"
80
+ else:
81
+ active = A > 0 if act in (None, "relu", "leaky_relu", "elu", "gelu", "swish", "softplus") else np.abs(A) > 1e-8
82
+ kind = "dead"
83
+ frac = active.mean(axis=0)
84
+ dead = int(np.sum(frac == 0))
85
+ return {
86
+ "mean": A.mean(axis=0), "std": A.std(axis=0), "frac_active": frac,
87
+ "layer_mean": fnum(A.mean()), "layer_frac_active": fnum(frac.mean()), "dead": dead, "dead_kind": kind,
88
+ "batch": int(A.shape[0]),
89
+ }
90
+
91
+
92
+ def project_2d(Z: np.ndarray, max_points: int = 1000, seed: int = 0):
93
+ """2-D view of features Z (n, k): raw coordinates if k ≤ 2, else PCA via SVD.
94
+
95
+ Returns (coords (m, 2), explained_variance_ratio or None, indices of the m points used).
96
+ """
97
+ Z = np.asarray(Z, dtype=np.float64)
98
+ Z = Z.reshape(Z.shape[0], -1)
99
+ idx = np.arange(Z.shape[0])
100
+ if Z.shape[0] > max_points:
101
+ idx = np.sort(np.random.default_rng(seed).choice(Z.shape[0], max_points, replace=False))
102
+ Z = Z[idx]
103
+ if Z.shape[1] == 1:
104
+ return np.column_stack([Z[:, 0], np.zeros(len(Z))]), None, idx
105
+ if Z.shape[1] == 2:
106
+ return Z, None, idx
107
+ Zc = Z - Z.mean(axis=0)
108
+ _, s, Vt = np.linalg.svd(Zc, full_matrices=False)
109
+ Vt = Vt[:2] * np.sign(Vt[np.arange(2), np.argmax(np.abs(Vt[:2]), axis=1)])[:, None] # fixed sign → no flipping
110
+ var = s ** 2
111
+ explained = var[:2] / var.sum() if var.sum() > 0 else np.zeros(2)
112
+ return Zc @ Vt[:2].T, explained, idx
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: nnscope
3
+ Version: 0.1.0
4
+ Summary: Watch your NumPy neural network learn: a live, replayable dashboard for weights, gradients, activations and loss.
5
+ Keywords: neural-network,numpy,visualization,backpropagation,education,deep-learning,dashboard
6
+ Author: michaelks
7
+ Author-email: michaelks <michaelks540@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
+ Classifier: Topic :: Scientific/Engineering :: Visualization
17
+ Requires-Dist: numpy>=1.21
18
+ Requires-Dist: matplotlib>=3.5 ; extra == 'png'
19
+ Requires-Python: >=3.9
20
+ Project-URL: Homepage, https://github.com/Ashurali/nnscope
21
+ Project-URL: Repository, https://github.com/Ashurali/nnscope
22
+ Project-URL: Issues, https://github.com/Ashurali/nnscope/issues
23
+ Project-URL: Changelog, https://github.com/Ashurali/nnscope/blob/master/CHANGELOG.md
24
+ Provides-Extra: png
25
+ Description-Content-Type: text/markdown
26
+
27
+ # nnscope
28
+
29
+ **Watch your NumPy neural network learn.** Add one line to the training loop of a network you wrote yourself, and nnscope
30
+ shows it live in your browser: every weight, every gradient, every activation, the loss, and plain-language hints about
31
+ what is going on. Afterwards you get a standalone HTML replay you can scrub through, share, or export as images.
32
+
33
+ It was built for students implementing neural networks **from scratch** (forward pass, backprop and SGD in NumPy)
34
+ who want to *see* what their code does: gradients flowing backwards, ReLUs dying, sigmoids saturating, a hidden layer slowly
35
+ learning to separate the classes.
36
+
37
+ ![nnscope dashboard](https://raw.githubusercontent.com/Ashurali/nnscope/master/docs/dashboard.png)
38
+
39
+ - **Zero setup:** depends only on NumPy. The dashboard is one self-contained HTML file served by Python's standard library.
40
+ - **Works with your code:** auto-detects common NumPy model layouts, or pass plain arrays.
41
+ - **Live and replay:** a live view while training, then `save_html()` for an offline replay with a timeline slider.
42
+ - **Explains itself:** every panel has a (?) with the math behind it, and an insights feed diagnoses common problems.
43
+ - **Responsive:** works on a phone (the network turns vertical), light and dark themes.
44
+
45
+ ## Install
46
+
47
+ ```bash
48
+ pip install nnscope # NumPy is the only dependency
49
+ pip install "nnscope[png]" # optional: PNG snapshots from Python via matplotlib
50
+ ```
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ import nnscope
56
+
57
+ scope = nnscope.Scope("my-mlp") # opens http://127.0.0.1:8765 in your browser
58
+ for epoch in range(epochs):
59
+ ... # your own forward pass, backprop and SGD step
60
+ scope.log(epoch, model=model, loss=train_loss, metrics={"test_loss": test_loss})
61
+
62
+ scope.save_html("run.html") # standalone replay, no server needed
63
+ scope.snapshot("run.svg") # network structure + loss curve as an image
64
+ scope.wait() # keep the live page up until Ctrl+C
65
+ ```
66
+
67
+ In a Jupyter notebook, `scope.show()` puts the live dashboard inline. Use `nnscope.Scope(..., live=False)` to only record
68
+ (for `save_html` / `snapshot`).
69
+
70
+ ## Connecting your network
71
+
72
+ nnscope never runs your network; it only **reads arrays you already have**.
73
+
74
+ **1. A model object (auto-detected).** Any object with a `.layers` list whose dense layers expose weights and biases,
75
+ optionally with their gradients:
76
+
77
+ | what | attribute names tried |
78
+ |---|---|
79
+ | weights | `W`, `weight`, `weights`, `kernel` |
80
+ | biases | `b`, `bias` |
81
+ | gradients | `dW`, `grad_W`, `W_grad`, `dweight`, … and `db`, `grad_b`, … |
82
+ | cached input (→ activation stats of the previous layer) | `x`, `input`, `inputs`, `a_prev` |
83
+
84
+ Layers without weights are recognised as activations by class name (`ReLU`, `Tanh`, `Sigmoid`, `LeakyReLU`, …), so the graph
85
+ shows `relu`, `tanh`, … on each layer. CS231n-style parameter dicts (`{"W1": …, "b1": …}` plus a matching `grads` dict)
86
+ and lists of `(W, b)` pairs work too.
87
+
88
+ **2. Plain arrays.** No model object needed:
89
+
90
+ ```python
91
+ scope.log(step, weights=[W1, W2], biases=[b1, b2], grads=[dW1, dW2], loss=loss)
92
+ ```
93
+
94
+ Both `x @ W` (`W` is `(in, out)`) and `W @ x` (`W` is `(out, in)`) conventions are detected from how the shapes chain. Force
95
+ one with `Scope(layout="in_out")` or `layout="out_in"` if a network is ambiguous (e.g. every layer square).
96
+
97
+ **Optional extras for richer views:**
98
+
99
+ ```python
100
+ scope.log(step, model=model, loss=loss,
101
+ activations=[a1, a2], # per-layer outputs on a batch → activation statistics
102
+ deltas=[d1, d2, d3], # per-layer ∂L/∂z on a batch → shown in the gradient-flow panel
103
+ latent=z, labels=y) # features of any layer → latent-space scatter
104
+ ```
105
+
106
+ ## How to read the dashboard
107
+
108
+ | Panel | What it shows | What to look for |
109
+ |---|---|---|
110
+ | **Network** | Neurons (circles) and weights (lines): blue positive, orange negative, thickness ∝ \|w\|. Toggle the lines to **gradients** ∂L/∂w or **updates** Δw, and the node colour to mean activation or % active. | Click a neuron: the **inspector** shows z = Σ wᵢ aᵢ + b with the real numbers from your batch, its strongest inputs, their gradients and how they changed. Dead ReLUs are ringed in red. |
111
+ | **Loss & metrics** | Every scalar you log over time (log or linear scale). | Spikes: learning rate too high. Test loss rising while training loss falls: overfitting. |
112
+ | **Insights** | Plain-language diagnoses, each at the step where it appeared. Click to jump there. | Vanishing/exploding gradients, dead or saturated units, learning rate too high/low, overfitting. |
113
+ | **Gradient flow** | Mean \|∂L/∂W\| per layer, input → output, log scale. | Bars shrinking towards the input = vanishing gradients (e.g. deep sigmoid nets). |
114
+ | **Layer health** | ‖W‖, ‖∇W‖ and the update ratio ‖ΔW‖/‖W‖ per layer over time, plus W and ∇W histograms. | Update ratio ≈ 10⁻³ per SGD step is healthy; ≈ 10⁻¹ is too high. |
115
+ | **Activations** | Per neuron: mean activation and fraction of samples where it is active. | Neurons at 0 % active pass no gradient back and have stopped learning. |
116
+ | **Latent space** | Any layer's features (PCA if wider than 2), coloured by label, along the timeline. | Classes starting mixed and ending linearly separable: the network is building a representation. |
117
+
118
+ Use the **timeline** at the bottom (or ← → and space) to replay training. **Export** saves the network (SVG/PNG), the loss curve,
119
+ the whole dashboard (PNG) or the raw run data (JSON).
120
+
121
+ ![deep sigmoid network: vanishing gradients](https://raw.githubusercontent.com/Ashurali/nnscope/master/docs/vanishing.png)
122
+
123
+ ## Examples
124
+
125
+ The [`examples/`](https://github.com/Ashurali/nnscope/tree/master/examples) folder uses a tiny homework-style NumPy MLP ([`_mlp.py`](https://github.com/Ashurali/nnscope/blob/master/examples/_mlp.py)). Run them from that folder;
126
+ add `--no-open` to skip opening a browser tab, or `--headless` to only write `runs/*.html` and `runs/*.svg`.
127
+
128
+ | script | story |
129
+ |---|---|
130
+ | `01_quickstart_xor.py` | the basics: an MLP on XOR-like data, live |
131
+ | `02_plain_arrays.py` | no model object: raw `W1, b1, W2, b2`, hand-written backprop, `deltas=` |
132
+ | `03_latent_bottleneck.py --k 2` | watch a k-unit bottleneck layer separate two classes (try `--k 32`) |
133
+ | `04_vanishing_gradients.py --act sigmoid` | deep sigmoid net: gradients vanish, the insight explains why (compare `--act relu`) |
134
+ | `05_inr_spectral_bias.py [--fourier]` | fit an image f(x, y) → rgb: raw coordinates plateau, Fourier features keep improving |
135
+
136
+ ## API
137
+
138
+ ```python
139
+ nnscope.Scope(name="run", *, live=True, port=8765, host="127.0.0.1", open_browser=True, max_neurons=32,
140
+ snapshot_every=None, max_snapshots=300, max_records=5000, layout="auto", latent_points=1000, quiet=False)
141
+ ```
142
+
143
+ - `log(step=None, *, model=None, loss=None, metrics=None, weights=None, biases=None, grads=None, bias_grads=None, activations=None, deltas=None, latent=None, labels=None, snapshot=None)`
144
+ - `save_html(path)`: standalone replay. `snapshot(path, step=None)`: `.svg` (no dependencies) or `.png` (matplotlib).
145
+ - `show(height=820)`: Jupyter IFrame. `wait()`: serve until Ctrl+C. `close()`; also usable as a context manager.
146
+ - `data()`: everything recorded so far as a JSON-serialisable dict.
147
+
148
+ **Keeping it light.** Scalars are recorded at every `log`; full snapshots (edges, histograms, latent points) are thinned
149
+ adaptively so a run keeps at most `max_snapshots` of them. Layers wider than `max_neurons` draw an evenly spaced subset
150
+ (statistics always use all neurons). The live stream batches updates to about 10 per second.
151
+
152
+ **Limitations.** Dense (fully-connected) layers only for now; convolutional layers are not drawn. The server binds to
153
+ localhost by default.
154
+
155
+ ## Development
156
+
157
+ ```bash
158
+ uv sync --dev
159
+ uv run --dev pytest
160
+ ```
161
+
162
+ ## License
163
+
164
+ MIT
@@ -0,0 +1,13 @@
1
+ nnscope/__init__.py,sha256=uM7lw5W-qmVG8pMvyja1Igs5lBZFAmCnbbV5cclVjxY,346
2
+ nnscope/adapters.py,sha256=RBLjC0NqrcX0xyBbWRWzr4xUial3ZxbnnUNZuAl-u5Q,9991
3
+ nnscope/export.py,sha256=N7V0l_q0kmc424IEW8kHYrTS-zTKV7XNfosgt54LhDY,8393
4
+ nnscope/insights.py,sha256=VFQc00rILr9f02EdLQHYWxgh6YbZ9bQZJbkwFCEgxEw,6256
5
+ nnscope/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ nnscope/scope.py,sha256=fJGlKUJ7Pf5HTVgPrzgLHfT5Si5AQGI3_KKwaOhPytQ,11767
7
+ nnscope/server.py,sha256=pBYzHVp5SbyPvYmCdri9382iREeLIz1YiX00frutZRw,3732
8
+ nnscope/static/index.html,sha256=fL4B48bdcdPczvE77Zl1iDyM_v04QAwHBXaUiv31n4w,53296
9
+ nnscope/stats.py,sha256=pspFU_qLBFYHvpoDr5M1ERUgH30Nwfp3lG04tra2whk,4447
10
+ nnscope-0.1.0.dist-info/licenses/LICENSE,sha256=0uVeJfLU3mjdYwRrEGRh4X0RQC0fbqX3Hc8BjSaZSJg,1064
11
+ nnscope-0.1.0.dist-info/WHEEL,sha256=766qEDcnZQDL36MOdWBQ0gowd8OjUWAOwMNC7Vgvxts,81
12
+ nnscope-0.1.0.dist-info/METADATA,sha256=FoJ01Fm89CHJShSgnxgQE5p3nH64Hah5jHpCVGHSIUw,9155
13
+ nnscope-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.20
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael
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.