seedl 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.
- seedl/__init__.py +47 -0
- seedl/_activations.py +33 -0
- seedl/_compare.py +127 -0
- seedl/_datasets.py +191 -0
- seedl/_educational.py +331 -0
- seedl/_embeddings.py +134 -0
- seedl/_explain.py +321 -0
- seedl/_export.py +143 -0
- seedl/_gradients.py +24 -0
- seedl/_inspect.py +1357 -0
- seedl/_models.py +272 -0
- seedl/_network_renderers.py +1301 -0
- seedl/_neurons.py +450 -0
- seedl/_predictions.py +301 -0
- seedl/_spatial.py +269 -0
- seedl/_tensors.py +123 -0
- seedl/_training.py +348 -0
- seedl/_transformers.py +114 -0
- seedl/_vision.py +123 -0
- seedl/api.py +620 -0
- seedl/exceptions.py +13 -0
- seedl/py.typed +0 -0
- seedl/renderers.py +1471 -0
- seedl/view.py +134 -0
- seedl-0.1.0.dist-info/METADATA +469 -0
- seedl-0.1.0.dist-info/RECORD +28 -0
- seedl-0.1.0.dist-info/WHEEL +4 -0
- seedl-0.1.0.dist-info/licenses/LICENSE +21 -0
seedl/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""SeeDL: simple, deep-learning-aware visualizations."""
|
|
2
|
+
|
|
3
|
+
from ._training import Tracker
|
|
4
|
+
from .api import (
|
|
5
|
+
activations,
|
|
6
|
+
attention,
|
|
7
|
+
compare,
|
|
8
|
+
compare_predictions,
|
|
9
|
+
convolution,
|
|
10
|
+
dataset,
|
|
11
|
+
embeddings,
|
|
12
|
+
explain,
|
|
13
|
+
gradients,
|
|
14
|
+
learn,
|
|
15
|
+
loss_landscape,
|
|
16
|
+
predictions,
|
|
17
|
+
see,
|
|
18
|
+
track,
|
|
19
|
+
watch,
|
|
20
|
+
)
|
|
21
|
+
from .exceptions import MissingDependencyError, SeeDLError, UnsupportedObjectError
|
|
22
|
+
from .view import Visualization
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"MissingDependencyError",
|
|
26
|
+
"SeeDLError",
|
|
27
|
+
"Tracker",
|
|
28
|
+
"UnsupportedObjectError",
|
|
29
|
+
"Visualization",
|
|
30
|
+
"activations",
|
|
31
|
+
"attention",
|
|
32
|
+
"compare",
|
|
33
|
+
"compare_predictions",
|
|
34
|
+
"convolution",
|
|
35
|
+
"dataset",
|
|
36
|
+
"embeddings",
|
|
37
|
+
"explain",
|
|
38
|
+
"gradients",
|
|
39
|
+
"learn",
|
|
40
|
+
"loss_landscape",
|
|
41
|
+
"predictions",
|
|
42
|
+
"see",
|
|
43
|
+
"track",
|
|
44
|
+
"watch",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
seedl/_activations.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Intermediate activation and feature map extraction with diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .exceptions import MissingDependencyError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def capture_activations(
|
|
11
|
+
model: Any,
|
|
12
|
+
example_input: Any,
|
|
13
|
+
*,
|
|
14
|
+
layers: str | list[str] = "all",
|
|
15
|
+
max_points: int = 2_048,
|
|
16
|
+
) -> tuple[dict[str, Any], str, list[str]]:
|
|
17
|
+
"""Capture intermediate layer activations, feature maps, and saturation diagnostics."""
|
|
18
|
+
from ._inspect import _activation_data, _torch_module
|
|
19
|
+
|
|
20
|
+
torch = _torch_module()
|
|
21
|
+
if torch is None or not isinstance(model, torch.nn.Module):
|
|
22
|
+
raise MissingDependencyError(
|
|
23
|
+
"Activation inspection requires PyTorch. Install it with `pip install 'seedl[torch]'`."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
data, summary, notes = _activation_data(model, example_input, max_points)
|
|
27
|
+
|
|
28
|
+
if layers != "all" and isinstance(layers, (list, tuple, set)):
|
|
29
|
+
data["layers"] = [
|
|
30
|
+
layer for layer in data["layers"] if layer["name"] in layers or layer["type"] in layers
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
return data, summary, notes
|
seedl/_compare.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Model, training history, and prediction comparison utilities."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._models import inspect_model_architecture
|
|
10
|
+
from ._predictions import compare_predictions
|
|
11
|
+
from ._training import compare_histories
|
|
12
|
+
from .exceptions import SeeDLError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _torch():
|
|
16
|
+
try:
|
|
17
|
+
return importlib.import_module("torch")
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compare_objects(
|
|
23
|
+
obj_a: Any,
|
|
24
|
+
obj_b: Any,
|
|
25
|
+
*,
|
|
26
|
+
names: tuple[str, str] = ("Model A", "Model B"),
|
|
27
|
+
max_points: int = 2_048,
|
|
28
|
+
) -> tuple[str, dict[str, Any], str, list[str]]:
|
|
29
|
+
"""Compare two compatible deep learning objects: models, training histories, or predictions."""
|
|
30
|
+
torch = _torch()
|
|
31
|
+
|
|
32
|
+
# Model comparison
|
|
33
|
+
is_mod_a = torch is not None and isinstance(obj_a, torch.nn.Module)
|
|
34
|
+
is_mod_b = torch is not None and isinstance(obj_b, torch.nn.Module)
|
|
35
|
+
|
|
36
|
+
if is_mod_a and is_mod_b:
|
|
37
|
+
return _compare_models(obj_a, obj_b, names)
|
|
38
|
+
|
|
39
|
+
# History comparison
|
|
40
|
+
if isinstance(obj_a, Mapping) and isinstance(obj_b, Mapping):
|
|
41
|
+
hist_data, sum_str, notes = compare_histories(obj_a, obj_b, names, max_points)
|
|
42
|
+
return "history_comparison", hist_data, sum_str, notes
|
|
43
|
+
|
|
44
|
+
# Prediction comparison
|
|
45
|
+
is_pred_a = hasattr(obj_a, "tolist") or isinstance(obj_a, (list, tuple))
|
|
46
|
+
is_pred_b = hasattr(obj_b, "tolist") or isinstance(obj_b, (list, tuple))
|
|
47
|
+
|
|
48
|
+
if is_pred_a and is_pred_b:
|
|
49
|
+
kind, pred_data, sum_str, notes = compare_predictions(obj_a, obj_b, names=names)
|
|
50
|
+
return kind, pred_data, sum_str, notes
|
|
51
|
+
|
|
52
|
+
raise SeeDLError(
|
|
53
|
+
f"Cannot compare objects of type {type(obj_a).__name__} and {type(obj_b).__name__}. "
|
|
54
|
+
"sd.compare() supports pairs of PyTorch nn.Modules, training history dicts, or prediction tensors."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _compare_models(
|
|
59
|
+
model_a: Any,
|
|
60
|
+
model_b: Any,
|
|
61
|
+
names: tuple[str, str],
|
|
62
|
+
) -> tuple[str, dict[str, Any], str, list[str]]:
|
|
63
|
+
name_a, name_b = names
|
|
64
|
+
data_a, _, _ = inspect_model_architecture(model_a)
|
|
65
|
+
data_b, _, _ = inspect_model_architecture(model_b)
|
|
66
|
+
|
|
67
|
+
param_a = data_a["parameters"]
|
|
68
|
+
param_b = data_b["parameters"]
|
|
69
|
+
diff_params = param_b - param_a
|
|
70
|
+
diff_sign = "+" if diff_params > 0 else ""
|
|
71
|
+
|
|
72
|
+
train_a = data_a["trainable"]
|
|
73
|
+
train_b = data_b["trainable"]
|
|
74
|
+
|
|
75
|
+
layers_a = data_a["layers"]
|
|
76
|
+
layers_b = data_b["layers"]
|
|
77
|
+
|
|
78
|
+
summary = (
|
|
79
|
+
f"{name_a} ({param_a:,} params) vs {name_b} ({param_b:,} params) · "
|
|
80
|
+
f"Diff: {diff_sign}{diff_params:,} ({diff_params / max(param_a, 1) * 100:+.1f}%)"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
notes = [
|
|
84
|
+
f"Architecture comparison between {name_a} and {name_b}.",
|
|
85
|
+
f"Total parameter difference: {diff_sign}{diff_params:,} parameters.",
|
|
86
|
+
f"Trainable parameters: {train_a:,} vs {train_b:,}.",
|
|
87
|
+
f"Module count: {len(layers_a)} vs {len(layers_b)}.",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Align layers by name
|
|
91
|
+
names_a = {layer["name"]: layer for layer in layers_a}
|
|
92
|
+
names_b = {layer["name"]: layer for layer in layers_b}
|
|
93
|
+
all_names = list(dict.fromkeys(list(names_a) + list(names_b)))
|
|
94
|
+
|
|
95
|
+
comparison_rows = []
|
|
96
|
+
for l_name in all_names[:50]: # Up to 50 rows
|
|
97
|
+
la = names_a.get(l_name)
|
|
98
|
+
lb = names_b.get(l_name)
|
|
99
|
+
p_la = la["parameters"] if la else "—"
|
|
100
|
+
p_lb = lb["parameters"] if lb else "—"
|
|
101
|
+
type_a = la["type"] if la else "—"
|
|
102
|
+
type_b = lb["type"] if lb else "—"
|
|
103
|
+
match = la is not None and lb is not None and la["type"] == lb["type"]
|
|
104
|
+
|
|
105
|
+
comparison_rows.append(
|
|
106
|
+
{
|
|
107
|
+
"layer": l_name or "(root)",
|
|
108
|
+
"type_a": type_a,
|
|
109
|
+
"type_b": type_b,
|
|
110
|
+
"params_a": p_la,
|
|
111
|
+
"params_b": p_lb,
|
|
112
|
+
"matched": match,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
data = {
|
|
117
|
+
"name_a": name_a,
|
|
118
|
+
"name_b": name_b,
|
|
119
|
+
"params_a": param_a,
|
|
120
|
+
"params_b": param_b,
|
|
121
|
+
"trainable_a": train_a,
|
|
122
|
+
"trainable_b": train_b,
|
|
123
|
+
"diff_params": diff_params,
|
|
124
|
+
"layer_rows": comparison_rows,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return "model_comparison", data, summary, notes
|
seedl/_datasets.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Dataset and DataLoader batch inspection with bounded sampling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ._tensors import make_image_grid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _torch():
|
|
13
|
+
try:
|
|
14
|
+
return importlib.import_module("torch")
|
|
15
|
+
except ModuleNotFoundError:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def inspect_dataset(
|
|
20
|
+
data: Any,
|
|
21
|
+
*,
|
|
22
|
+
labels: Any | None = None,
|
|
23
|
+
max_samples: int = 64,
|
|
24
|
+
) -> tuple[str, dict[str, Any], str, list[str]]:
|
|
25
|
+
"""Inspect a dataset, DataLoader batch, or sample collection safely."""
|
|
26
|
+
torch = _torch()
|
|
27
|
+
|
|
28
|
+
# If it's a PyTorch DataLoader, safely extract first batch without consuming whole iterator
|
|
29
|
+
samples = None
|
|
30
|
+
extracted_labels = labels
|
|
31
|
+
|
|
32
|
+
if hasattr(data, "__iter__") and not isinstance(data, (list, tuple, dict)):
|
|
33
|
+
try:
|
|
34
|
+
it = iter(data)
|
|
35
|
+
first_batch = next(it)
|
|
36
|
+
if isinstance(first_batch, (tuple, list)) and len(first_batch) >= 2:
|
|
37
|
+
samples = first_batch[0]
|
|
38
|
+
extracted_labels = first_batch[1]
|
|
39
|
+
else:
|
|
40
|
+
samples = first_batch
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
if samples is None:
|
|
45
|
+
samples = data
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
hasattr(samples, "__len__")
|
|
49
|
+
and hasattr(samples, "__getitem__")
|
|
50
|
+
and not hasattr(samples, "shape")
|
|
51
|
+
):
|
|
52
|
+
try:
|
|
53
|
+
count = len(samples)
|
|
54
|
+
sample_items = [samples[i] for i in range(min(count, max_samples))]
|
|
55
|
+
if sample_items and isinstance(sample_items[0], (tuple, list)):
|
|
56
|
+
if len(sample_items[0]) >= 2:
|
|
57
|
+
raw_x = [item[0] for item in sample_items]
|
|
58
|
+
extracted_labels = [item[1] for item in sample_items]
|
|
59
|
+
else:
|
|
60
|
+
raw_x = [item[0] for item in sample_items]
|
|
61
|
+
else:
|
|
62
|
+
raw_x = sample_items
|
|
63
|
+
|
|
64
|
+
if torch is not None and raw_x and isinstance(raw_x[0], torch.Tensor):
|
|
65
|
+
samples = torch.stack(raw_x)
|
|
66
|
+
else:
|
|
67
|
+
samples = raw_x
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
# Extract shape if tensor/numpy
|
|
72
|
+
shape = getattr(samples, "shape", None)
|
|
73
|
+
if shape is not None:
|
|
74
|
+
shape = tuple(int(s) for s in shape)
|
|
75
|
+
|
|
76
|
+
# Convert labels to list of strings/ints if present
|
|
77
|
+
label_list = None
|
|
78
|
+
if extracted_labels is not None:
|
|
79
|
+
if hasattr(extracted_labels, "tolist"):
|
|
80
|
+
label_list = extracted_labels.tolist()
|
|
81
|
+
elif isinstance(extracted_labels, (list, tuple)):
|
|
82
|
+
label_list = list(extracted_labels)
|
|
83
|
+
|
|
84
|
+
class_counts: dict[str, int] = {}
|
|
85
|
+
if label_list:
|
|
86
|
+
counts = Counter(str(lbl) for lbl in label_list)
|
|
87
|
+
class_counts = dict(counts.most_common(20))
|
|
88
|
+
|
|
89
|
+
notes = [
|
|
90
|
+
"Dataset inspection is bounded; no iterators were exhausted and no data was downloaded."
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# Check for image batch: 4D tensor (N, C, H, W) or (N, H, W, C)
|
|
94
|
+
if shape is not None and len(shape) == 4 and shape[0] >= 1:
|
|
95
|
+
n, c_or_h = shape[0], shape[1]
|
|
96
|
+
is_chw = c_or_h in (1, 3, 4) and min(shape[2:]) >= 4
|
|
97
|
+
is_hwc = shape[-1] in (1, 3, 4) and min(shape[1:3]) >= 4
|
|
98
|
+
|
|
99
|
+
if is_chw or is_hwc:
|
|
100
|
+
channels = c_or_h if is_chw else shape[-1]
|
|
101
|
+
num_display = min(n, 16)
|
|
102
|
+
image_tiles = []
|
|
103
|
+
for i in range(num_display):
|
|
104
|
+
img = samples[i]
|
|
105
|
+
if is_chw and torch is not None and isinstance(img, torch.Tensor):
|
|
106
|
+
img = img.permute(1, 2, 0)
|
|
107
|
+
if hasattr(img, "detach") and hasattr(img, "cpu"):
|
|
108
|
+
arr = img.detach().cpu().to(dtype=torch.float32)
|
|
109
|
+
# normalize
|
|
110
|
+
low, high = float(arr.min().item()), float(arr.max().item())
|
|
111
|
+
if high > low:
|
|
112
|
+
norm = ((arr - low) / (high - low)).tolist()
|
|
113
|
+
else:
|
|
114
|
+
norm = torch.full_like(arr, 0.5).tolist()
|
|
115
|
+
image_tiles.append(norm if channels > 1 else [[p[0]] for p in norm])
|
|
116
|
+
elif hasattr(img, "tolist"):
|
|
117
|
+
image_tiles.append(img.tolist())
|
|
118
|
+
|
|
119
|
+
grid, _, rows, cols = make_image_grid(image_tiles, channels, max_images=16)
|
|
120
|
+
summary = f"Image dataset preview: {n} sample(s) with shape {shape[1:]}"
|
|
121
|
+
notes.append(f"Showing {num_display} sample images arranged in a {rows}×{cols} grid.")
|
|
122
|
+
if class_counts:
|
|
123
|
+
notes.append(
|
|
124
|
+
f"Observed classes in sample: {', '.join(f'{k} ({v})' for k, v in list(class_counts.items())[:5])}."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
data_payload = {
|
|
128
|
+
"kind": "image_dataset",
|
|
129
|
+
"pixels": grid,
|
|
130
|
+
"grid": grid,
|
|
131
|
+
"channels": channels,
|
|
132
|
+
"shape": shape,
|
|
133
|
+
"sample_count": n,
|
|
134
|
+
"num_samples": n,
|
|
135
|
+
"class_counts": class_counts,
|
|
136
|
+
"labels": label_list[:num_display] if label_list else None,
|
|
137
|
+
}
|
|
138
|
+
return "dataset_images", data_payload, summary, notes
|
|
139
|
+
|
|
140
|
+
# Tabular / 2D numerical inspection
|
|
141
|
+
if shape is not None and len(shape) == 2:
|
|
142
|
+
num_samples, num_features = shape
|
|
143
|
+
summary = f"Tabular dataset: {num_samples} sample(s) × {num_features} feature(s)"
|
|
144
|
+
notes.append(f"Shape: {num_samples} rows × {num_features} columns.")
|
|
145
|
+
|
|
146
|
+
# Compute summary stats across columns (up to 20 columns)
|
|
147
|
+
feature_stats = []
|
|
148
|
+
for col_idx in range(min(num_features, 20)):
|
|
149
|
+
if hasattr(samples, "shape"):
|
|
150
|
+
col = samples[:, col_idx] # type: ignore[call-overload]
|
|
151
|
+
else:
|
|
152
|
+
col = [row[col_idx] for row in samples if len(row) > col_idx]
|
|
153
|
+
if hasattr(col, "detach") and hasattr(col, "cpu"):
|
|
154
|
+
vals = col.detach().cpu().to(dtype=torch.float32)
|
|
155
|
+
mean_v = float(vals.mean().item())
|
|
156
|
+
min_v = float(vals.min().item())
|
|
157
|
+
max_v = float(vals.max().item())
|
|
158
|
+
elif hasattr(col, "mean"):
|
|
159
|
+
mean_v = float(col.mean())
|
|
160
|
+
min_v = float(col.min())
|
|
161
|
+
max_v = float(col.max())
|
|
162
|
+
else:
|
|
163
|
+
mean_v, min_v, max_v = 0.0, 0.0, 0.0
|
|
164
|
+
feature_stats.append(
|
|
165
|
+
{
|
|
166
|
+
"feature": f"Feature {col_idx}",
|
|
167
|
+
"mean": mean_v,
|
|
168
|
+
"min": min_v,
|
|
169
|
+
"max": max_v,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
data_payload = {
|
|
174
|
+
"kind": "tabular_dataset",
|
|
175
|
+
"samples": num_samples,
|
|
176
|
+
"num_samples": num_samples,
|
|
177
|
+
"features": num_features,
|
|
178
|
+
"feature_stats": feature_stats,
|
|
179
|
+
"class_counts": class_counts,
|
|
180
|
+
}
|
|
181
|
+
return "dataset_tabular", data_payload, summary, notes
|
|
182
|
+
|
|
183
|
+
# General sequence or item collection
|
|
184
|
+
count = len(samples) if hasattr(samples, "__len__") else max_samples
|
|
185
|
+
summary = f"Dataset collection: {count} sample(s)"
|
|
186
|
+
data_payload = {
|
|
187
|
+
"kind": "general_dataset",
|
|
188
|
+
"count": count,
|
|
189
|
+
"class_counts": class_counts,
|
|
190
|
+
}
|
|
191
|
+
return "dataset_general", data_payload, summary, notes
|