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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroplot
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A lightweight, modular real-time visualization and diagnostics library for PyTorch.
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: torch
@@ -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
- self.history = {plot_type: [] for plot_type in plots if plot_type not in ["boundary", "gradients", "latent"]}
32
- self.smoothed_history = {plot_type: [] for plot_type in plots if plot_type not in ["boundary", "gradients", "latent"]}
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. Install via 'pip install imageio'.")
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
- for key, value in metrics.items():
111
- if key in self.history:
112
- self.history[key].append(value)
113
- prev_smooth = self.smoothed_history[key][-1] if self.smoothed_history[key] else value
114
- smoothed_val = self.smooth_alpha * value + (1 - self.smooth_alpha) * prev_smooth
115
- self.smoothed_history[key].append(smoothed_val)
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 key == "loss" and value < self.best_loss:
118
- self.best_loss = value
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 plot_type == 'loss' else 'green', lw=2, label='EMA Trend')
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
- ax.set_xlabel("Hidden Dim 1")
178
- ax.set_ylabel("Hidden Dim 2")
179
- ax.grid(True, linestyle='--', alpha=0.6)
180
-
181
- # 4. Gradient Flow Plot
182
- elif plot_type == "gradients" and self.model is not None:
183
- ave_grads = []
184
- layers = []
185
- for n, p in self.model.named_parameters():
186
- if p.requires_grad and ("bias" not in n) and (p.grad is not None):
187
- layers.append(n.split(".")[0])
188
- ave_grads.append(p.grad.abs().mean().item())
189
-
190
- if ave_grads:
191
- ax.plot(ave_grads, alpha=0.7, color="blue", lw=2)
192
- ax.bar(range(len(ave_grads)), ave_grads, alpha=0.3, color="blue")
193
- ax.set_xticks(range(len(layers)))
194
- ax.set_xticklabels(layers, rotation=30, ha='right', fontsize=8)
195
- ax.set_title("Gradient Flow (Layer-wise)")
196
- ax.set_xlabel("Layers")
197
- ax.set_ylabel("Avg Gradient Magnitude")
198
- ax.set_yscale("log")
199
- ax.grid(True, linestyle='--', alpha=0.6)
200
-
201
- # 5. Custom Callbacks
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
- plt.draw()
207
- self.fig.canvas.flush_events()
208
- plt.pause(0.001)
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
- abs_ckpt = os.path.abspath(self.checkpoint_path)
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
- abs_path = os.path.abspath(self.gif_name)
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
- print(f"GIF successfully compiled and saved at:\n---> {abs_path}")
230
-
231
- plt.show()
254
+ if matplotlib.get_backend() != 'Agg':
255
+ plt.show()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: neuroplot
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A lightweight, modular real-time visualization and diagnostics library for PyTorch.
5
5
  Description-Content-Type: text/markdown
6
6
  Requires-Dist: torch
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "neuroplot"
7
- version = "0.1.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
@@ -1,9 +0,0 @@
1
- # PyTorch Foundations
2
-
3
- Daily practice for mechanistic interpretability thesis.
4
-
5
- Rules:
6
- - One concept per day
7
- - One script per concept
8
- - Type every line, no copy-paste
9
- - Every day ends with a commit
File without changes
File without changes