nnscope 0.1.1__tar.gz → 0.1.2__tar.gz
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-0.1.1 → nnscope-0.1.2}/PKG-INFO +2 -2
- {nnscope-0.1.1 → nnscope-0.1.2}/README.md +1 -1
- {nnscope-0.1.1 → nnscope-0.1.2}/pyproject.toml +1 -1
- {nnscope-0.1.1 → nnscope-0.1.2}/pyproject.toml.orig +1 -1
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/__init__.py +1 -1
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/insights.py +13 -6
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/server.py +13 -2
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/static/index.html +6 -4
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/stats.py +3 -2
- {nnscope-0.1.1 → nnscope-0.1.2}/LICENSE +0 -0
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/adapters.py +0 -0
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/export.py +0 -0
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/py.typed +0 -0
- {nnscope-0.1.1 → nnscope-0.1.2}/src/nnscope/scope.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nnscope
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Watch your NumPy neural network learn: a live, replayable dashboard for weights, gradients, activations and loss.
|
|
5
5
|
Keywords: neural-network,numpy,visualization,backpropagation,education,deep-learning,dashboard
|
|
6
6
|
Author: michaelks
|
|
@@ -112,7 +112,7 @@ scope.log(step, model=model, loss=loss,
|
|
|
112
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
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
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. |
|
|
115
|
+
| **Activations** | Per neuron: mean activation and fraction of samples where it is active. | A ReLU at 0 % active is *dead*: no gradient flows through it, so its own weights stop learning (it can revive only if earlier layers change). A LeakyReLU at 0 % is just *inactive* and still learns slowly. |
|
|
116
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
117
|
|
|
118
118
|
Use the **timeline** at the bottom (or ← → and space) to replay training. **Export** saves the network (SVG/PNG), the loss curve,
|
|
@@ -86,7 +86,7 @@ scope.log(step, model=model, loss=loss,
|
|
|
86
86
|
| **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. |
|
|
87
87
|
| **Gradient flow** | Mean \|∂L/∂W\| per layer, input → output, log scale. | Bars shrinking towards the input = vanishing gradients (e.g. deep sigmoid nets). |
|
|
88
88
|
| **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. |
|
|
89
|
-
| **Activations** | Per neuron: mean activation and fraction of samples where it is active. |
|
|
89
|
+
| **Activations** | Per neuron: mean activation and fraction of samples where it is active. | A ReLU at 0 % active is *dead*: no gradient flows through it, so its own weights stop learning (it can revive only if earlier layers change). A LeakyReLU at 0 % is just *inactive* and still learns slowly. |
|
|
90
90
|
| **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. |
|
|
91
91
|
|
|
92
92
|
Use the **timeline** at the bottom (or ← → and space) to replay training. **Export** saves the network (SVG/PNG), the loss curve,
|
|
@@ -83,12 +83,19 @@ class InsightEngine:
|
|
|
83
83
|
kind = l.get("dead_kind") or "dead"
|
|
84
84
|
if dead is None or n <= 1:
|
|
85
85
|
continue
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
86
|
+
if kind == "saturated":
|
|
87
|
+
why = ("(stuck near ±1, so their gradient ≈ 0). Inputs to this layer are too large; normalise the inputs or use "
|
|
88
|
+
"smaller initial weights.")
|
|
89
|
+
elif kind == "inactive":
|
|
90
|
+
why = ("(never positive). Unlike ReLU they still pass a small gradient (α·δ), so they can recover, but slowly.")
|
|
91
|
+
else:
|
|
92
|
+
why = ("(never positive, so no gradient flows through them and their own weights stop learning). "
|
|
93
|
+
+ ("In the first layer the inputs never change, so they will not come back. " if k == 0 else
|
|
94
|
+
"They can only come back if the earlier layers' outputs change. ")
|
|
95
|
+
+ "Typical causes: too large first updates (lower the learning rate or warm it up) or poor init; "
|
|
96
|
+
"LeakyReLU keeps a small gradient flowing.")
|
|
97
|
+
self._check(f"{kind}:{k}", dead / n >= 0.5, step, "warn", "activations",
|
|
98
|
+
f"{dead} of {n} {act} units in {names[k]} are {kind} on the logged batch {why}", out)
|
|
92
99
|
|
|
93
100
|
test_key = next((k for k in scalars if ("test" in k or "val" in k) and "loss" in k), None)
|
|
94
101
|
if test_key and loss is not None and scalars[test_key] is not None and np.isfinite(scalars[test_key]):
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import queue
|
|
6
|
+
import sys
|
|
6
7
|
import threading
|
|
7
8
|
import time
|
|
8
9
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
@@ -13,6 +14,17 @@ PUSH_INTERVAL = 0.1 # seconds: batch messages → at most ~10 pushes per se
|
|
|
13
14
|
HEARTBEAT = 15.0
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class _QuietServer(ThreadingHTTPServer):
|
|
18
|
+
"""Don't print a traceback when a browser closes a tab or reloads mid-request: that is normal, not an error."""
|
|
19
|
+
|
|
20
|
+
daemon_threads = True
|
|
21
|
+
|
|
22
|
+
def handle_error(self, request, client_address):
|
|
23
|
+
if isinstance(sys.exc_info()[1], (ConnectionError, TimeoutError)):
|
|
24
|
+
return
|
|
25
|
+
super().handle_error(request, client_address)
|
|
26
|
+
|
|
27
|
+
|
|
16
28
|
def _handler_for(scope):
|
|
17
29
|
class Handler(BaseHTTPRequestHandler):
|
|
18
30
|
def log_message(self, *args): # keep the user's console clean
|
|
@@ -83,12 +95,11 @@ def start_server(scope, host: str = "127.0.0.1", port: int = 8765):
|
|
|
83
95
|
last_err = None
|
|
84
96
|
for p in ([0] if port == 0 else list(range(port, port + 20)) + [0]):
|
|
85
97
|
try:
|
|
86
|
-
server =
|
|
98
|
+
server = _QuietServer((host, p), handler)
|
|
87
99
|
break
|
|
88
100
|
except OSError as err:
|
|
89
101
|
last_err = err
|
|
90
102
|
else:
|
|
91
103
|
raise OSError(f"nnscope: no free port found ({last_err})")
|
|
92
|
-
server.daemon_threads = True
|
|
93
104
|
threading.Thread(target=server.serve_forever, daemon=True, name="nnscope-server").start()
|
|
94
105
|
return server, f"http://{host}:{server.server_address[1]}/"
|
|
@@ -212,9 +212,11 @@
|
|
|
212
212
|
<div class="help" id="help-acts">
|
|
213
213
|
<p>For each hidden layer, one cell per drawn neuron: the top strip is its <b>mean activation</b> on the logged batch, the bottom strip the
|
|
214
214
|
<b>fraction of samples where it is active</b> (ReLU: output > 0; tanh/sigmoid: not saturated).</p>
|
|
215
|
-
<p>A
|
|
216
|
-
<
|
|
217
|
-
|
|
215
|
+
<p>A ReLU at 0 % active is <b style="color:var(--bad)">dead</b>: no gradient flows through it, so its own weights stop learning.
|
|
216
|
+
It can only come back if its <i>inputs</i> change, which is impossible in the first layer, where the inputs are the data. A LeakyReLU
|
|
217
|
+
unit at 0 % is merely <b>inactive</b>: it still passes the small gradient α·δ and can recover. A tanh/sigmoid unit stuck near ±1 is
|
|
218
|
+
<b style="color:var(--bad)">saturated</b> (gradient ≈ 0). Activation stats need activations: they are read automatically from layers that
|
|
219
|
+
cache their input (e.g. <code>Linear.x</code>), or pass <code>activations=[a1, a2, …]</code>.</p></div>
|
|
218
220
|
<div id="acts"></div>
|
|
219
221
|
</section>
|
|
220
222
|
|
|
@@ -732,7 +734,7 @@ function drawActs() {
|
|
|
732
734
|
box.innerHTML = rows.map((r) => `<div class="act-row"><div class="nm" id="actnm${r.k}"></div><canvas data-a="${r.k}"></canvas></div>`).join("");
|
|
733
735
|
}
|
|
734
736
|
rows.forEach(({ l, k, nd, rec }) => {
|
|
735
|
-
const dead = rec && rec.dead ? rec.dead : 0, kind = rec && rec.dead_kind
|
|
737
|
+
const dead = rec && rec.dead ? rec.dead : 0, kind = (rec && rec.dead_kind) || "dead";
|
|
736
738
|
$("actnm" + k).innerHTML = `<b>layer ${k + 1}</b> · ${l.act || "linear"} · ${l.out} units` +
|
|
737
739
|
(rec && rec.frac_active !== undefined && rec.frac_active !== null ? ` · ${Math.round(rec.frac_active * 100)}% active on average` : "") +
|
|
738
740
|
(dead ? ` · <span class="dead">${dead} ${kind}</span>` : "");
|
|
@@ -64,7 +64,8 @@ def layer_numbers(W: np.ndarray, dW: Optional[np.ndarray], prev_W: Optional[np.n
|
|
|
64
64
|
def activation_numbers(A: Optional[np.ndarray], act: Optional[str]) -> Optional[dict]:
|
|
65
65
|
"""Per-neuron statistics of a batch of activations A (batch, units).
|
|
66
66
|
|
|
67
|
-
* relu-like: a unit is *active* when its output > 0; a unit never active on the batch is *dead
|
|
67
|
+
* relu-like: a unit is *active* when its output > 0; a unit never active on the batch is *dead* (no gradient flows
|
|
68
|
+
through it). For leaky_relu such a unit is only *inactive*: it still passes the small gradient α·δ.
|
|
68
69
|
* tanh / sigmoid: a unit is *active* when it is not saturated; one saturated on every sample is flagged *saturated*.
|
|
69
70
|
"""
|
|
70
71
|
if A is None:
|
|
@@ -79,7 +80,7 @@ def activation_numbers(A: Optional[np.ndarray], act: Optional[str]) -> Optional[
|
|
|
79
80
|
kind = "saturated"
|
|
80
81
|
else:
|
|
81
82
|
active = A > 0 if act in (None, "relu", "leaky_relu", "elu", "gelu", "swish", "softplus") else np.abs(A) > 1e-8
|
|
82
|
-
kind = "dead"
|
|
83
|
+
kind = "inactive" if act == "leaky_relu" else "dead"
|
|
83
84
|
frac = active.mean(axis=0)
|
|
84
85
|
dead = int(np.sum(frac == 0))
|
|
85
86
|
return {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|