neuroplot 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.
- {neuroplot-0.1.1 → neuroplot-0.1.2}/PKG-INFO +1 -1
- neuroplot-0.1.2/neuroplot/README.md +68 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot/visualizer.py +100 -76
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot.egg-info/PKG-INFO +1 -1
- {neuroplot-0.1.1 → neuroplot-0.1.2}/pyproject.toml +1 -1
- neuroplot-0.1.1/README.md +0 -9
- {neuroplot-0.1.1/neuroplot → neuroplot-0.1.2}/README.md +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot/__init__.py +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot.egg-info/SOURCES.txt +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot.egg-info/dependency_links.txt +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot.egg-info/requires.txt +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/neuroplot.egg-info/top_level.txt +0 -0
- {neuroplot-0.1.1 → neuroplot-0.1.2}/setup.cfg +0 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# NeuroPlot 🧠📊
|
|
2
|
+
A lightweight, modular real-time training telemetry and visualization library for PyTorch.
|
|
3
|
+
|
|
4
|
+
NeuroPlot provides live, multi-panel diagnostic monitoring during your training loops—allowing you to track decision boundaries, latent space representations, loss curves, and accuracy metrics seamlessly.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
- Live Multi-Panel Dashboard: Watch decision boundaries and latent spaces evolve in real-time alongside loss and accuracy curves.
|
|
8
|
+
- Automatic GIF Compilation: Automatically capture training progress and compile it into a clean GIF.
|
|
9
|
+
- Best-Model Checkpointing: Automatically track and save the best-performing model weights.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
pip install neuroplot
|
|
13
|
+
|
|
14
|
+
## Quick Start Usage Guide
|
|
15
|
+
import torch
|
|
16
|
+
import torch.nn as nn
|
|
17
|
+
import torch.optim as optim
|
|
18
|
+
import sklearn.datasets as datasets
|
|
19
|
+
from neuroplot import LiveVisualizer
|
|
20
|
+
|
|
21
|
+
# 1. Prepare data and model
|
|
22
|
+
X, y = datasets.make_moons(n_samples=100, noise=0.1)
|
|
23
|
+
x = torch.tensor(X, dtype=torch.float32)
|
|
24
|
+
y = torch.tensor(y, dtype=torch.float32).view(-1, 1)
|
|
25
|
+
|
|
26
|
+
class Moon_MLP(nn.Module):
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.fc1 = nn.Linear(2, 8)
|
|
30
|
+
self.fc2 = nn.Linear(8, 8)
|
|
31
|
+
self.fc3 = nn.Linear(8, 1)
|
|
32
|
+
|
|
33
|
+
def forward(self, x):
|
|
34
|
+
x = torch.relu(self.fc1(x))
|
|
35
|
+
x = torch.relu(self.fc2(x))
|
|
36
|
+
x = torch.sigmoid(self.fc3(x))
|
|
37
|
+
return x
|
|
38
|
+
|
|
39
|
+
model = Moon_MLP()
|
|
40
|
+
optimizer = optim.Adam(model.parameters(), lr=0.01)
|
|
41
|
+
|
|
42
|
+
# 2. Initialize NeuroPlot
|
|
43
|
+
viz = LiveVisualizer(
|
|
44
|
+
plots=["boundary", "latent", "loss", "accuracy"],
|
|
45
|
+
model=model,
|
|
46
|
+
data=(x, y),
|
|
47
|
+
update_every=50,
|
|
48
|
+
save_gif=True,
|
|
49
|
+
gif_name="neuroplot_demo.gif",
|
|
50
|
+
save_best_model=True,
|
|
51
|
+
checkpoint_path="best_model.pth"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# 3. Training Loop
|
|
55
|
+
for epoch in range(1000):
|
|
56
|
+
y_pred = model(x)
|
|
57
|
+
loss = nn.BCELoss()(y_pred, y)
|
|
58
|
+
|
|
59
|
+
preds = (y_pred >= 0.5).float()
|
|
60
|
+
acc = (preds == y).float().mean().item()
|
|
61
|
+
|
|
62
|
+
optimizer.zero_grad()
|
|
63
|
+
loss.backward()
|
|
64
|
+
optimizer.step()
|
|
65
|
+
|
|
66
|
+
viz.step(epoch, loss=loss.item(), accuracy=acc)
|
|
67
|
+
|
|
68
|
+
viz.close()
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
# import necessary libraries
|
|
2
2
|
import torch
|
|
3
3
|
import torch.nn as nn
|
|
4
|
+
import matplotlib
|
|
4
5
|
import matplotlib.pyplot as plt
|
|
5
6
|
import numpy as np
|
|
6
7
|
import os
|
|
7
8
|
import tempfile
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# --- FEATURE: Headless Fallback ---
|
|
12
|
+
if not os.environ.get('DISPLAY') and sys.platform != 'darwin':
|
|
13
|
+
matplotlib.use('Agg')
|
|
14
|
+
else:
|
|
15
|
+
try:
|
|
16
|
+
plt.ion()
|
|
17
|
+
except Exception:
|
|
18
|
+
matplotlib.use('Agg')
|
|
8
19
|
|
|
9
20
|
try:
|
|
10
21
|
import imageio
|
|
@@ -16,7 +27,7 @@ class LiveVisualizer:
|
|
|
16
27
|
def __init__(self, plots, model=None, data=None, update_every=50,
|
|
17
28
|
save_gif=False, gif_name="training_progress.gif",
|
|
18
29
|
save_best_model=False, checkpoint_path="best_model.pth",
|
|
19
|
-
smooth_alpha=0.1):
|
|
30
|
+
smooth_alpha=0.1, custom_plot_fn=None):
|
|
20
31
|
self.plots = plots
|
|
21
32
|
self.model = model
|
|
22
33
|
self.data = data
|
|
@@ -26,10 +37,12 @@ class LiveVisualizer:
|
|
|
26
37
|
self.save_best_model = save_best_model
|
|
27
38
|
self.checkpoint_path = checkpoint_path
|
|
28
39
|
self.smooth_alpha = smooth_alpha
|
|
40
|
+
self.custom_plot_fn = custom_plot_fn
|
|
29
41
|
|
|
30
42
|
# Dynamic history dictionary
|
|
31
|
-
|
|
32
|
-
self.
|
|
43
|
+
ignored_plots = ["boundary", "gradients", "latent", "grad_norm", "regression_fit"]
|
|
44
|
+
self.history = {plot_type: [] for plot_type in plots if plot_type not in ignored_plots}
|
|
45
|
+
self.smoothed_history = {plot_type: [] for plot_type in plots if plot_type not in ignored_plots}
|
|
33
46
|
self.custom_plots = {}
|
|
34
47
|
|
|
35
48
|
# Best metric trackers
|
|
@@ -39,7 +52,7 @@ class LiveVisualizer:
|
|
|
39
52
|
# GIF frame storage
|
|
40
53
|
self.frames = []
|
|
41
54
|
if self.save_gif and not IMAGEIO_AVAILABLE:
|
|
42
|
-
print("Warning: 'imageio' package not found. GIF saving will be skipped.
|
|
55
|
+
print("Warning: 'imageio' package not found. GIF saving will be skipped.")
|
|
43
56
|
self.save_gif = False
|
|
44
57
|
|
|
45
58
|
if self.save_gif:
|
|
@@ -56,7 +69,6 @@ class LiveVisualizer:
|
|
|
56
69
|
cols = min(num_plots, 3)
|
|
57
70
|
rows = (num_plots + cols - 1) // cols
|
|
58
71
|
|
|
59
|
-
plt.ion()
|
|
60
72
|
self.fig, self.axes = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows))
|
|
61
73
|
|
|
62
74
|
if num_plots == 1:
|
|
@@ -79,17 +91,14 @@ class LiveVisualizer:
|
|
|
79
91
|
self.custom_plots[name] = func
|
|
80
92
|
|
|
81
93
|
def _extract_hidden_activations(self, x):
|
|
82
|
-
"""Helper to extract hidden layer representations for mechanistic tasks."""
|
|
83
94
|
if self.model is None:
|
|
84
95
|
return None
|
|
85
|
-
|
|
86
96
|
activation = {}
|
|
87
97
|
def get_activation(name):
|
|
88
98
|
def hook(model, input, output):
|
|
89
99
|
activation[name] = output.detach()
|
|
90
100
|
return hook
|
|
91
101
|
|
|
92
|
-
# Automatically find the second-to-last linear layer or hook a fallback
|
|
93
102
|
target_layer = None
|
|
94
103
|
for name, module in self.model.named_modules():
|
|
95
104
|
if isinstance(module, nn.Linear):
|
|
@@ -103,52 +112,72 @@ class LiveVisualizer:
|
|
|
103
112
|
return activation.get('hidden', None)
|
|
104
113
|
return None
|
|
105
114
|
|
|
106
|
-
def step(self, epoch, **metrics):
|
|
115
|
+
def step(self, epoch, loss=None, accuracy=None, **metrics):
|
|
107
116
|
if epoch % self.update_every != 0:
|
|
108
117
|
return
|
|
109
118
|
|
|
110
|
-
|
|
111
|
-
if
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
if loss is not None:
|
|
120
|
+
if isinstance(loss, dict):
|
|
121
|
+
for k, v in loss.items():
|
|
122
|
+
key_name = f"{k}_loss" if k != "loss" else "loss"
|
|
123
|
+
if key_name not in self.history:
|
|
124
|
+
self.history[key_name] = []
|
|
125
|
+
self.smoothed_history[key_name] = []
|
|
126
|
+
self.history[key_name].append(v)
|
|
127
|
+
prev_smooth = self.smoothed_history[key_name][-1] if self.smoothed_history[key_name] else v
|
|
128
|
+
self.smoothed_history[key_name].append(self.smooth_alpha * v + (1 - self.smooth_alpha) * prev_smooth)
|
|
129
|
+
|
|
130
|
+
if (k == "loss" or key_name == "loss") and v < self.best_loss:
|
|
131
|
+
self.best_loss = v
|
|
132
|
+
if self.save_best_model and self.model is not None:
|
|
133
|
+
torch.save(self.model.state_dict(), self.checkpoint_path)
|
|
134
|
+
else:
|
|
135
|
+
if "loss" not in self.history:
|
|
136
|
+
self.history["loss"] = []
|
|
137
|
+
self.smoothed_history["loss"] = []
|
|
138
|
+
self.history["loss"].append(loss)
|
|
139
|
+
prev_smooth = self.smoothed_history["loss"][-1] if self.smoothed_history["loss"] else loss
|
|
140
|
+
self.smoothed_history["loss"].append(self.smooth_alpha * loss + (1 - self.smooth_alpha) * prev_smooth)
|
|
116
141
|
|
|
117
|
-
if
|
|
118
|
-
self.best_loss =
|
|
119
|
-
if self.save_best_model and self.model is not None:
|
|
120
|
-
torch.save(self.model.state_dict(), self.checkpoint_path)
|
|
121
|
-
elif key == "accuracy" and value > self.best_acc:
|
|
122
|
-
self.best_acc = value
|
|
142
|
+
if loss < self.best_loss:
|
|
143
|
+
self.best_loss = loss
|
|
123
144
|
if self.save_best_model and self.model is not None:
|
|
124
145
|
torch.save(self.model.state_dict(), self.checkpoint_path)
|
|
146
|
+
|
|
147
|
+
if accuracy is not None:
|
|
148
|
+
if "accuracy" not in self.history:
|
|
149
|
+
self.history["accuracy"] = []
|
|
150
|
+
self.smoothed_history["accuracy"] = []
|
|
151
|
+
self.history["accuracy"].append(accuracy)
|
|
152
|
+
prev_smooth = self.smoothed_history["accuracy"][-1] if self.smoothed_history["accuracy"] else accuracy
|
|
153
|
+
self.smoothed_history["accuracy"].append(self.smooth_alpha * accuracy + (1 - self.smooth_alpha) * prev_smooth)
|
|
154
|
+
if accuracy > self.best_acc:
|
|
155
|
+
self.best_acc = accuracy
|
|
156
|
+
if self.save_best_model and self.model is not None:
|
|
157
|
+
torch.save(self.model.state_dict(), self.checkpoint_path)
|
|
158
|
+
|
|
159
|
+
for key, value in metrics.items():
|
|
160
|
+
if key not in self.history:
|
|
161
|
+
self.history[key] = []
|
|
162
|
+
self.smoothed_history[key] = []
|
|
163
|
+
self.history[key].append(value)
|
|
164
|
+
prev_smooth = self.smoothed_history[key][-1] if self.smoothed_history[key] else value
|
|
165
|
+
self.smoothed_history[key].append(self.smooth_alpha * value + (1 - self.smooth_alpha) * prev_smooth)
|
|
125
166
|
|
|
126
167
|
for i, plot_type in enumerate(self.plots):
|
|
127
168
|
ax = self.axes[i]
|
|
128
169
|
ax.clear()
|
|
129
170
|
|
|
130
|
-
# 1. Metric Curves with EMA Trendlines
|
|
131
171
|
if plot_type in self.history:
|
|
132
172
|
raw_values = self.history[plot_type]
|
|
133
173
|
smooth_values = self.smoothed_history[plot_type]
|
|
134
|
-
|
|
135
174
|
ax.plot(raw_values, color='gray', alpha=0.3, lw=1, label='Raw')
|
|
136
|
-
ax.plot(smooth_values, color='red' if
|
|
175
|
+
ax.plot(smooth_values, color='red' if 'loss' in plot_type else 'green', lw=2, label='EMA Trend')
|
|
137
176
|
ax.set_title(f"{plot_type.capitalize()} Curve")
|
|
138
177
|
ax.set_xlabel("Update Step")
|
|
139
178
|
ax.set_ylabel(plot_type.capitalize())
|
|
140
179
|
ax.grid(True, linestyle='--', alpha=0.6)
|
|
141
|
-
|
|
142
|
-
if plot_type == "loss" and raw_values:
|
|
143
|
-
best_idx = np.argmin(raw_values)
|
|
144
|
-
ax.scatter(best_idx, raw_values[best_idx], color='gold', s=100, zorder=5, label=f'Best: {raw_values[best_idx]:.4f}')
|
|
145
|
-
ax.legend(loc='upper right', fontsize=7)
|
|
146
|
-
elif plot_type == "accuracy" and raw_values:
|
|
147
|
-
best_idx = np.argmax(raw_values)
|
|
148
|
-
ax.scatter(best_idx, raw_values[best_idx], color='gold', s=100, zorder=5, label=f'Best: {raw_values[best_idx]:.4f}')
|
|
149
|
-
ax.legend(loc='lower right', fontsize=7)
|
|
150
|
-
|
|
151
|
-
# 2. Decision Boundary Plot
|
|
180
|
+
|
|
152
181
|
elif plot_type == "boundary" and self.model is not None and self.data is not None:
|
|
153
182
|
X, y = self.data
|
|
154
183
|
if self.grid_tensor is not None:
|
|
@@ -158,54 +187,54 @@ class LiveVisualizer:
|
|
|
158
187
|
self.model.train()
|
|
159
188
|
Z = Z.reshape(self.xx.shape)
|
|
160
189
|
ax.contourf(self.xx, self.yy, Z, levels=50, cmap=plt.cm.Spectral, alpha=0.7)
|
|
161
|
-
|
|
162
190
|
ax.scatter(X[:, 0], X[:, 1], c=y.squeeze(), cmap=plt.cm.Spectral, edgecolors='k')
|
|
163
191
|
ax.set_title(f"Decision Boundary (Epoch {epoch})")
|
|
164
192
|
|
|
165
|
-
# 3. Mechanistic Latent Space / Internal Representation Warp Plot
|
|
166
193
|
elif plot_type == "latent" and self.model is not None and self.data is not None:
|
|
167
194
|
X, y = self.data
|
|
168
195
|
hidden_acts = self._extract_hidden_activations(X)
|
|
169
196
|
if hidden_acts is not None:
|
|
170
197
|
h = hidden_acts.cpu().numpy()
|
|
171
|
-
# If hidden dimension > 2, take the first 2 principal components or dimensions for visualization
|
|
172
198
|
h_x = h[:, 0]
|
|
173
199
|
h_y = h[:, 1] if h.shape[1] > 1 else np.zeros_like(h_x)
|
|
174
|
-
|
|
175
200
|
ax.scatter(h_x, h_y, c=y.squeeze(), cmap=plt.cm.Spectral, edgecolors='k', s=40)
|
|
176
201
|
ax.set_title(f"Internal Latent Space (Epoch {epoch})")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
+
|
|
203
|
+
elif plot_type == "grad_norm" and self.model is not None:
|
|
204
|
+
total_norm = sum(p.grad.norm().item() ** 2 for p in self.model.parameters() if p.grad is not None) ** 0.5
|
|
205
|
+
if not hasattr(self, 'grad_norms'):
|
|
206
|
+
self.grad_norms = []
|
|
207
|
+
self.grad_norms.append(total_norm)
|
|
208
|
+
ax.plot(self.grad_norms, color="purple", lw=2)
|
|
209
|
+
ax.set_title("Total Gradient Norm")
|
|
210
|
+
ax.set_xlabel("Update Step")
|
|
211
|
+
ax.grid(True, linestyle='--', alpha=0.6)
|
|
212
|
+
|
|
213
|
+
elif plot_type == "regression_fit" and self.model is not None and self.data is not None:
|
|
214
|
+
X, y = self.data
|
|
215
|
+
self.model.eval()
|
|
216
|
+
with torch.no_grad():
|
|
217
|
+
preds = self.model(X).detach().cpu().numpy()
|
|
218
|
+
self.model.train()
|
|
219
|
+
y_np = y.cpu().numpy().squeeze()
|
|
220
|
+
preds = preds.squeeze()
|
|
221
|
+
ax.scatter(y_np, preds, color="teal", alpha=0.7, edgecolors='k')
|
|
222
|
+
min_v, max_v = min(y_np.min(), preds.min()), max(y_np.max(), preds.max())
|
|
223
|
+
ax.plot([min_v, max_v], [min_v, max_v], 'r--', label="Ideal (y=x)")
|
|
224
|
+
ax.set_title("Regression: Actual vs Predicted")
|
|
225
|
+
ax.legend(fontsize=7)
|
|
226
|
+
ax.grid(True, linestyle='--', alpha=0.6)
|
|
227
|
+
|
|
202
228
|
elif plot_type in self.custom_plots:
|
|
203
229
|
self.custom_plots[plot_type](ax, self.model, self.data)
|
|
230
|
+
elif self.custom_plot_fn is not None and plot_type == "custom":
|
|
231
|
+
self.custom_plot_fn(ax, self.model, self.data)
|
|
204
232
|
|
|
205
233
|
plt.tight_layout()
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
234
|
+
if matplotlib.get_backend() != 'Agg':
|
|
235
|
+
plt.draw()
|
|
236
|
+
self.fig.canvas.flush_events()
|
|
237
|
+
plt.pause(0.001)
|
|
209
238
|
|
|
210
239
|
if self.save_gif and IMAGEIO_AVAILABLE:
|
|
211
240
|
frame_path = os.path.join(self.temp_dir.name, f"frame_{self.frame_count:04d}.png")
|
|
@@ -215,17 +244,12 @@ class LiveVisualizer:
|
|
|
215
244
|
|
|
216
245
|
def close(self):
|
|
217
246
|
plt.ioff()
|
|
218
|
-
|
|
219
247
|
if self.save_best_model:
|
|
220
|
-
|
|
221
|
-
print(f"Best model automatically saved to:\n---> {abs_ckpt}")
|
|
222
|
-
|
|
248
|
+
print(f"Best model saved to: {os.path.abspath(self.checkpoint_path)}")
|
|
223
249
|
if self.save_gif and IMAGEIO_AVAILABLE and self.frames:
|
|
224
|
-
|
|
225
|
-
print(f"Compiling training progress into GIF...")
|
|
250
|
+
print("Compiling training progress into GIF...")
|
|
226
251
|
images = [imageio.v3.imread(f) for f in self.frames]
|
|
227
252
|
imageio.mimsave(self.gif_name, images, duration=200, loop=0)
|
|
228
253
|
self.temp_dir.cleanup()
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
plt.show()
|
|
254
|
+
if matplotlib.get_backend() != 'Agg':
|
|
255
|
+
plt.show()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "neuroplot"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "A lightweight, modular real-time visualization and diagnostics library for PyTorch."
|
|
9
9
|
readme = "neuroplot/README.md"
|
|
10
10
|
dependencies = [
|
neuroplot-0.1.1/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|