Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
cff-version: 1.2.0
message: "If you use this software, please cite it using the metadata below."
title: "perspic"
keywords:
- "training dynamics"
- "learning theory"
- "neural network analysis"
- "collective variables"
authors:
- given-names: Konstantin
family-names: Nikolaou
email: knikolaou@icp.uni-stuttgart.de
affiliation: University of Stuttgart
orcid: 'https://orcid.org/0009-0000-9926-680X'
- given-names: Jonas
family-names: Scheunemann
email: jscheunemann@icp.uni-stuttgart.de
affiliation: University of Stuttgart
orcid: 'https://orcid.org/0009-0000-6670-1596'
version: 0.0.1
date-released: "2026-05-05"
repository-code: "https://github.com/zincware/perspic"
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,48 @@
# Perspic
A tool to study neural network training dynamics.
A tool to study neural network training dynamics.

## What is Perspic?

Perspic provides efficient computation and logging of the loss-network-position (LNP) decomposition. Each of the components helps us to understand how neural networks learn. It integrates seamlessly with PyTorch Lightning by wrapping existing code, adding the analysis.

## Quick Start

```python
from perspic import analyzer
from pytorch_lightning import Trainer

# Wrap your Lightning module class
model = analyzer(
lightning_module=YourLightningModule, # class
model=your_model,
sample_wise_engine="opacus"
)

trainer = Trainer(...)
trainer.fit(model, train_dataloader, val_dataloader)
```

## Installation

**Note:** Perspic is not yet published on PyPI. Install it locally with dependencies:

```bash
git clone https://github.com/zincware/perspic.git
cd perspic
python -m pip install .
```

## Learning & Examples

Start with [examples/README.md](examples/README.md) for comprehensive tutorials on core workflows, advanced features, and expert topics using CIFAR-10 and Vision Transformers.

## Documentation

- [Examples](examples/) — Jupyter notebooks demonstrating key features
- [Tests](tests/) — Unit and integration tests


## License

See [LICENSE](LICENSE)

172 changes: 172 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Perspic Examples

Comprehensive examples demonstrating how to use perspic for analyzing neural network training dynamics.

---

## Quick Start

### Prerequisites

- PyTorch and PyTorch Lightning
- perspic (this package)
- CIFAR-10 dataset (auto-downloaded on first run)

### Running Your First Example

1. **Start with**: [cifar10.ipynb](cifar10.ipynb)
- Full training pipeline with perspic analyzer
- Shows how to wrap your model and collect metrics
- ~30 minutes to run (or longer on CPU)

---

## Learning Path

Follow this order for best understanding:

### Level 1: Core Workflows

#### 1. [cifar10.ipynb](cifar10.ipynb)
**Duration**: ~30 min | **GPU**: Recommended (works on CPU but slow)

**What you'll learn**:
- How to wrap your PyTorch Lightning module with `analyzer()`
- What metrics perspic automatically computes (chi_net, chi_loss, coupling coefficient)
- How to configure CIFAR-10 datasets and dataloaders
- How to visualize training dynamics using perspic metrics

**Key Insight**: The `analyzer` wrapper is a drop-in enhancement for your LightningModule that requires minimal code changes.

---

### Level 2: Advanced Features

#### 2. [logging_scheduler.ipynb](logging_scheduler.ipynb)
**Duration**: ~20 min | **GPU**: Recommended

**Focus**: Efficient logging with logarithmic scheduling

**What you'll learn**:
- How to use `logarithmic_windows()` for sparse metric collection
- Why log-spaced sampling is efficient for long training runs
- How to configure `analysis_schedule` in the analyzer
- Trade-offs between metric frequency and computational cost

**Key Insight**: You don't need to compute metrics every step. Log-spaced schedules capture dynamics while reducing overhead.

---

#### 3. [cifar10_cross_response.ipynb](cifar10_cross_response.ipynb)
**Duration**: ~25 min | **GPU**: Recommended

**Focus**: Analyzing model response on fixed data

**What you'll learn**:
- How to set up a measurement set (fixed batch of test samples)
- How to use cross-response analysis to study sensitivity to perturbations
- Relationship between cross-response metrics and network learning
- `MultiEpochsDataLoader` for repeated cycling through measurement data

**Key Insight**: Cross-response analysis reveals how predictions respond to small weight changes, complementing training-time gradient analysis.

#### 4. [mup_integration.ipynb](mup_integration.ipynb)
**Duration**: ~40 min | **GPU**: Strongly recommended

**Focus**: Integration with Maximal Update Parametrization (μP)

**What you'll learn**:
- How μP enables hyperparameter transfer across model widths
- Why `CachedMuReadout` is needed for functorch compatibility
- How to use μP optimizers (`MuAdam`, `MuSGD`) with perspic
- How to use `set_base_shapes()` to mark width dimensions
- Applying this to WideResNet, ViT, and Llama-style transformers

**Key Insight**: Combine μP with perspic to verify that hyperparameters transfer correctly across model widths.

---

## Additional Examples

### [batch_size_scaling_analysis.ipynb](batch_size_scaling_analysis.ipynb)
**Duration**: ~5 min | **GPU**: Not required

**What it demonstrates**:
- How batch size affects gradient metrics
- Why repeated samples are useful for controlled experiments
- Expected scaling behavior: network gradients (linear) vs loss gradients (inverse)
- How perspic's normalization ensures batch-size invariant metrics

**Key Insight**: Using `reduction="mean"` in your loss combined with perspic's normalization produces batch-size invariant metrics.

This example is self-contained and doesn't require external data. Use it to understand batch-size scaling theory before moving to full training examples.

---

### [core/hutchinson_convergence.py](core/hutchinson_convergence.py)
**Duration**: ~2 min | **GPU**: Not required

**What it demonstrates**:
- Convergence of Hutchinson's trace estimator for per-sample gradient norms
- How random Rademacher projections approximate exact gradient norm computation
- Trade-off between estimation accuracy and computational cost
- Visualization of convergence rates and relative error

**Key Insight**: Hutchinson's estimator efficiently approximates squared gradient norms with just a handful of random projections, enabling fast per-sample gradient computations in perspic without looping over all output dimensions.

Run this script standalone to verify estimator accuracy: `python core/hutchinson_convergence.py`


---

## Tips & Troubleshooting

### "Why aren't my metrics being computed?"
- Check that `disable_analyzer=False` in the analyzer setup
- Verify that `analysis_schedule` includes the training steps you're running

### "The mup_integration notebook has a KeyboardInterrupt error"
- This is often a kernel timeout during torch import. Restart the kernel and re-run.

### "Can I modify the model architecture?"
- Yes! All CIFAR-10 notebooks focus on perspic mechanics, not specific architectures
- Swap models easily: try `WideResNet(28, 160)` or `BatchNormMLP(...)` instead of ViT

---

## Next Steps

- **Explore the source**: Look at `perspic/analyzer.py` to understand how metrics are computed
- **Run your own**: Use these notebooks as templates for your own models and datasets
- **Ask questions**: Open an issue if you have questions or suggestions

---

## File Structure

```
examples/
├── README.md # This file
├── __init__.py
├── models/ # Example model definitions
│ ├── __init__.py
│ ├── cnns.py
│ ├── lightning_modules.py
│ ├── mlps.py
│ └── utils.py
├── batch_size_scaling_analysis.ipynb # Theory + implementation
├── cifar10.ipynb # Core workflow
├── logging_scheduler.ipynb # Efficient logging
├── cross10_cross_response.ipynb # Response analysis
├── mup_integration.ipynb # Advanced: μP + scaling laws
└── core/
└── hutchinson_convergence.py # Support utilities
```

---

## See Also

- [PyTorch Lightning Documentation](https://pytorch-lightning.readthedocs.io)
- [Hutchinson Trace Estimation Paper](https://arxiv.org/abs/2312.14499)
- [Maximal Update Parametrization (μP) Paper](https://arxiv.org/abs/2203.03466)
41 changes: 23 additions & 18 deletions examples/batch_size_scaling_analysis.ipynb

Large diffs are not rendered by default.

131 changes: 94 additions & 37 deletions examples/cifar10.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -64,41 +64,106 @@
},
{
"cell_type": "code",
"execution_count": 2,
"id": "44f888e9",
"execution_count": null,
"id": "26a039f8",
"metadata": {},
"outputs": [],
"source": [
"# CIFAR10 mean and std\n",
"stats = ((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))\n",
"train_transform = torchvision.transforms.Compose([\n",
" torchvision.transforms.RandomCrop(32, padding=4),\n",
" torchvision.transforms.RandomHorizontalFlip(),\n",
" torchvision.transforms.ToTensor(),\n",
" torchvision.transforms.Normalize(*stats),\n",
"])\n",
"test_transform = torchvision.transforms.Compose([\n",
" torchvision.transforms.ToTensor(),\n",
" torchvision.transforms.Normalize(*stats),\n",
"])\n",
"# ============================================================================\n",
"# CIFAR-10 Utilities (Shared Across Examples)\n",
"# ============================================================================\n",
"# These utilities are defined here and can be reused by other notebooks via copy/paste\n",
"\n",
"# Load datasets\n",
"# We instantiate the dataset twice to apply different transforms to train and val splits\n",
"train_dataset_full = CIFAR10(PATH_DATASETS, train=True, download=True, transform=train_transform)\n",
"val_dataset_full = CIFAR10(PATH_DATASETS, train=True, download=True, transform=test_transform)\n",
"test_set = CIFAR10(PATH_DATASETS, train=False, download=True, transform=test_transform)\n",
"CIFAR10_STATS = ((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))\n",
"\n",
"def create_cifar10_transforms():\n",
" \"\"\"\n",
" Create standardized CIFAR-10 train and test transforms.\n",
" \n",
" Returns:\n",
Comment thread
jonscheunemann marked this conversation as resolved.
" tuple: (train_transform, test_transform)\n",
" \"\"\"\n",
" train_transform = torchvision.transforms.Compose([\n",
" torchvision.transforms.RandomCrop(32, padding=4),\n",
" torchvision.transforms.RandomHorizontalFlip(),\n",
" torchvision.transforms.ToTensor(),\n",
" torchvision.transforms.Normalize(*CIFAR10_STATS),\n",
" ])\n",
" test_transform = torchvision.transforms.Compose([\n",
" torchvision.transforms.ToTensor(),\n",
" torchvision.transforms.Normalize(*CIFAR10_STATS),\n",
" ])\n",
" return train_transform, test_transform\n",
"\n",
"# Split train/val (45k/5k)\n",
"# We use the same seed to ensure the split indices are identical for both dataset instances\n",
"generator = torch.Generator().manual_seed(42)\n",
"train_set, _ = random_split(train_dataset_full, [45000, 5000], generator=generator)\n",
"_, val_set = random_split(val_dataset_full, [45000, 5000], generator=generator)\n",
"\n",
"# DataLoaders\n",
"# We set drop_last=True to avoid spikes in gradient norms caused by smaller last batches\n",
"train_dataloader = DataLoader(train_set, batch_size=BATCH_SIZE, shuffle=True, num_workers=NUM_WORKERS, drop_last=True)\n",
"val_dataloader = DataLoader(val_set, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, drop_last=True)\n",
"test_dataloader = DataLoader(test_set, batch_size=BATCH_SIZE, shuffle=False, num_workers=NUM_WORKERS, drop_last=True)"
"def setup_cifar10_dataloaders(batch_size=None, num_workers=None, path_datasets=\".\"):\n",
" \"\"\"\n",
" Setup CIFAR-10 dataloaders with train/val/test splits.\n",
" \n",
" Args:\n",
" batch_size: Batch size (default: 256 if GPU available, else 64)\n",
" num_workers: Number of workers (default: cpu_count/2)\n",
" path_datasets: Path to store datasets\n",
" \n",
" Returns:\n",
" dict: {'train': train_dataloader, 'val': val_dataloader, 'test': test_dataloader}\n",
" \"\"\"\n",
" if batch_size is None:\n",
" batch_size = 256 if torch.cuda.is_available() else 64\n",
" if num_workers is None:\n",
" cpu_count = os.cpu_count()\n",
" num_workers = max(0, (cpu_count or 0) // 2)\n",
" \n",
" train_transform, test_transform = create_cifar10_transforms()\n",
" \n",
" # Load datasets (instantiate twice for different transforms on train/val)\n",
" train_dataset_full = CIFAR10(path_datasets, train=True, download=True, transform=train_transform)\n",
" val_dataset_full = CIFAR10(path_datasets, train=True, download=True, transform=test_transform)\n",
" test_set = CIFAR10(path_datasets, train=False, download=True, transform=test_transform)\n",
" \n",
" # Split train/val (45k/5k)\n",
" generator = torch.Generator().manual_seed(42)\n",
" train_set, _ = random_split(train_dataset_full, [45000, 5000], generator=generator)\n",
" _, val_set = random_split(val_dataset_full, [45000, 5000], generator=generator)\n",
" \n",
" # Create dataloaders (drop_last=True avoids spikes in gradient norms from smaller last batch)\n",
" train_dataloader = DataLoader(\n",
" train_set, batch_size=batch_size, shuffle=True, \n",
" num_workers=num_workers, drop_last=True\n",
" )\n",
" val_dataloader = DataLoader(\n",
" val_set, batch_size=batch_size, shuffle=False, \n",
" num_workers=num_workers, drop_last=True\n",
" )\n",
" test_dataloader = DataLoader(\n",
" test_set, batch_size=batch_size, shuffle=False, \n",
" num_workers=num_workers, drop_last=True\n",
" )\n",
" \n",
" return {\n",
" \"train\": train_dataloader,\n",
" \"val\": val_dataloader,\n",
" \"test\": test_dataloader,\n",
" }\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "44f888e9",
"metadata": {},
"outputs": [],
"source": [
"# Setup CIFAR-10 dataloaders\n",
"dataloaders = setup_cifar10_dataloaders(\n",
" batch_size=BATCH_SIZE,\n",
" num_workers=NUM_WORKERS,\n",
" path_datasets=PATH_DATASETS\n",
")\n",
"\n",
"train_dataloader = dataloaders[\"train\"]\n",
"val_dataloader = dataloaders[\"val\"]\n",
"test_dataloader = dataloaders[\"test\"]\n"
]
},
{
Expand Down Expand Up @@ -1203,14 +1268,6 @@
"# Example usage with current trainer\n",
"plot_training_metrics(f\"{trainer.logger.log_dir}/metrics.csv\", title=\"ViT Small Training\", figsize=(12, 12))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "db8531c5",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
Expand Down
Loading
Loading