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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nnscope
3
- Version: 0.1.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. | Neurons at 0 % active pass no gradient back and have stopped learning. |
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. | Neurons at 0 % active pass no gradient back and have stopped learning. |
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,
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nnscope"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Watch your NumPy neural network learn: a live, replayable dashboard for weights, gradients, activations and loss."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nnscope"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Watch your NumPy neural network learn: a live, replayable dashboard for weights, gradients, activations and loss."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,7 +4,7 @@
4
4
  scope = nnscope.Scope("my-mlp")
5
5
  scope.log(step, model=model, loss=loss)
6
6
  """
7
- __version__ = "0.1.1"
7
+ __version__ = "0.1.2"
8
8
 
9
9
  from .adapters import LayerView, extract_layers # noqa: E402
10
10
  from .scope import Scope # noqa: E402
@@ -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
- word = "dead" if kind == "dead" else "saturated"
87
- self._check(f"{word}:{k}", dead / n >= 0.5, step, "warn", "activations",
88
- f"{dead} of {n} {act} units in {names[k]} are {word} on the logged batch "
89
- + ("(never positive → zero gradient forever). Lower the learning rate, use He init or LeakyReLU."
90
- if word == "dead" else "(stuck near ±1 → gradient ≈ 0). Inputs to this layer are too large; "
91
- "normalise inputs or use smaller initial weights."), out)
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 = ThreadingHTTPServer((host, p), handler)
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 &gt; 0; tanh/sigmoid: not saturated).</p>
215
- <p>A neuron with 0 % active passes no gradient back, so it has stopped learning (<b style="color:var(--bad)">dead ReLU</b> or
216
- <b style="color:var(--bad)">saturated</b> unit). Activation stats need activations: they are read automatically from layers that cache
217
- their input (e.g. <code>Linear.x</code>), or pass <code>activations=[a1, a2, …]</code>.</p></div>
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 === "saturated" ? "saturated" : "dead";
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