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/__init__.py +12 -0
- nnscope/adapters.py +233 -0
- nnscope/export.py +168 -0
- nnscope/insights.py +102 -0
- nnscope/py.typed +0 -0
- nnscope/scope.py +231 -0
- nnscope/server.py +94 -0
- nnscope/static/index.html +856 -0
- nnscope/stats.py +112 -0
- nnscope-0.1.0.dist-info/METADATA +164 -0
- nnscope-0.1.0.dist-info/RECORD +13 -0
- nnscope-0.1.0.dist-info/WHEEL +4 -0
- nnscope-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+

|
|
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
|
+

|
|
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,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.
|