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