Usage¶
One-shot analysis¶
import parx
import numpy as np
import torch.nn as nn
model = nn.Sequential(
nn.Linear(2, 8), nn.ReLU(),
nn.Linear(8, 8), nn.ReLU(),
nn.Linear(8, 1),
)
# Warm up Julia JIT (optional but recommended)
parx.precompile()
# Compute the partition
X = np.random.uniform(-1, 1, (1000, 2))
partition = parx.compute_partition(model, X, method="sparse_julia")
# Analyze
print(parx.complexity_profile(partition))
print("Dead neurons:", parx.dead_neurons(partition))
print("Size summary:", parx.region_size_summary(partition))
# Visualize
from parx.viz import plot_partition_2d
plot_partition_2d(partition).show()
# Save (no Julia needed to reload)
parx.save_partition(partition, "partition.npz")
Training-time tracking¶
The primary research use case: compute the partition at multiple checkpoints and track how it evolves. The two-moons notebook walks through this end-to-end, comparing sparse and exact partitions before/during/after training.
partitions = []
labels = []
for epoch, state_dict in parx.iter_state_dicts("checkpoints.h5"):
p = parx.compute_partition(state_dict, X, method="sparse_julia")
partitions.append(p)
labels.append(epoch)
parx.save_partition(p, f"partitions/epoch_{epoch}.npz")
# Track complexity over training
stats = parx.analysis.complexity_over_epochs(partitions, labels=labels)
# stats["n_regions"], stats["dead_neuron_count"], stats["mean_chebyshev_radius"]
# Animated visualization
from parx.viz import animate_epochs, animate_epochs_video
animate_epochs(partitions, epoch_labels=labels).show()
animate_epochs_video(partitions, "evolution.gif", epoch_labels=labels)
Geometric analysis with exact partitions¶
# Exact enumeration: complete partition from a single starting point
partition = parx.compute_partition(model, np.zeros((1, 2)), method="exact_julia")
# Every region has geometry
for region in partition.regions:
D, g = partition.halfspaces(region) # polytope constraints
A, b = partition.local_affine(region) # local linear map
x0 = region.centroid # interior point
print(f" bounded={region.bounded}, n_constraints={D.shape[0]}")
# Verify geometric correctness
from parx.verify import check_no_overlaps, check_covers_space
X_test = np.random.uniform(-1, 1, (5000, 2))
assert check_no_overlaps(partition, X_test)[0]
assert check_covers_space(partition, X_test)[0]
# Volume estimates (slow)
from parx.analysis import partition_volume_estimates
volumes = partition_volume_estimates(partition, n_samples=5000)
Visualization¶
Every plotting function accepts a keyword-only backend: Literal["plotly", "matplotlib"] = "plotly" argument. "plotly" (default) returns an interactive plotly.graph_objects.Figure; "matplotlib" returns a static matplotlib.figure.Figure (or matplotlib.animation.FuncAnimation for animate_epochs) and requires pip install "parx[animate]". Matplotlib output has no hover tooltips, and animate_epochs(backend="matplotlib") has no play/pause/slider controls — use .to_jshtml() in a notebook or .save(...) to export it.
from parx.viz import (
plot_partition_2d, # 2D input space; color by metric
plot_partition_slice, # slice higher-dim partition to 2D
plot_partition_projection, # project halfspace normals to 2D
plot_partition_pca, # PCA projection (requires parx[analysis])
plot_region_counts, # bar chart of regions per layer
plot_halfspaces, # halfspace boundary overlay
)
fig = plot_partition_2d(partition, backend="matplotlib")
fig.savefig("partition.png")
Color-by callables: affine_frobenius, affine_spectral, affine_det, active_neuron_count (all in parx.viz). plot_partition_2d(..., layer=l) collapses regions to their first l layers, showing how depth builds the partition — see the notebooks for a worked example.
Utilities¶
parx.precompile() # warm up Julia JIT (call once at startup)
parx.list_methods() # → list of registered method names
from parx.diagnostics import thread_info, benchmark_method
thread_info() # Julia thread count and environment info
benchmark_method(model, X, method="sparse_julia") # timing comparison
Limitations¶
These are current constraints, not design goals. See the project's TODO.md for planned extensions.
Architecture support: Only sequential Linear → ReLU stacks. ResNets, transformers, CNNs, networks with batch normalization or dropout, and any model with branching structure are not supported.
Exact enumeration scaling: The DFS is exponential in the worst case. In practice input_dim=2 with up to ~20 neurons per layer is reliable; input_dim=3 is feasible for small networks; beyond that exact enumeration is often infeasible.
Sparse enumeration completeness: Sparse mode can only find regions that your sample points land in. With input_dim > 5 and complex networks, many regions may never appear in any finite sample.
Julia startup: The first call to any Julia-backed method in a process incurs 10–30 seconds of JIT compilation. Call parx.precompile() once at startup to amortize this. Subsequent calls within the same process are fast.
Visualization: plot_partition_2d requires input_dim == 2. For higher-dimensional networks use plot_partition_slice (fix all but two dimensions) or plot_partition_projection (project onto a 2D subspace) — both are approximations.