l0-python 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.
l0/penalties.py ADDED
@@ -0,0 +1,381 @@
1
+ """
2
+ Penalty computation and management utilities for L0 regularization.
3
+
4
+ Provides functions for computing penalties, tracking sparsity,
5
+ and managing temperature schedules.
6
+ """
7
+
8
+ from collections import defaultdict
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn as nn
14
+
15
+
16
+ def compute_l0_penalty(model: nn.Module) -> torch.Tensor:
17
+ """
18
+ Compute total L0 penalty across all L0 layers in a model.
19
+
20
+ Parameters
21
+ ----------
22
+ model : nn.Module
23
+ Model containing L0 layers
24
+
25
+ Returns
26
+ -------
27
+ torch.Tensor
28
+ Total L0 penalty (expected number of active parameters)
29
+ """
30
+ penalty = torch.tensor(0.0)
31
+
32
+ for module in model.modules():
33
+ if hasattr(module, "get_l0_penalty"):
34
+ penalty = penalty + module.get_l0_penalty() # type: ignore[operator]
35
+
36
+ return penalty
37
+
38
+
39
+ def compute_l2_penalty(model: nn.Module) -> torch.Tensor:
40
+ """
41
+ Compute L2 penalty (weight decay) across all layers.
42
+
43
+ Parameters
44
+ ----------
45
+ model : nn.Module
46
+ Model to compute L2 penalty for
47
+
48
+ Returns
49
+ -------
50
+ torch.Tensor
51
+ Total L2 penalty (sum of squared weights)
52
+ """
53
+ penalty = torch.tensor(0.0)
54
+
55
+ for name, param in model.named_parameters():
56
+ if "weight" in name and param.requires_grad:
57
+ penalty = penalty + (param**2).sum()
58
+
59
+ return penalty
60
+
61
+
62
+ def compute_l0l2_penalty(
63
+ model: nn.Module,
64
+ l0_lambda: float = 1e-3,
65
+ l2_lambda: float = 1e-4,
66
+ ) -> torch.Tensor:
67
+ """
68
+ Compute combined L0L2 penalty.
69
+
70
+ This combination is recommended in practice to prevent overfitting
71
+ while maintaining sparsity.
72
+
73
+ Parameters
74
+ ----------
75
+ model : nn.Module
76
+ Model to compute penalties for
77
+ l0_lambda : float
78
+ Weight for L0 penalty
79
+ l2_lambda : float
80
+ Weight for L2 penalty
81
+
82
+ Returns
83
+ -------
84
+ torch.Tensor
85
+ Combined penalty value
86
+ """
87
+ l0_penalty = compute_l0_penalty(model)
88
+ l2_penalty = compute_l2_penalty(model)
89
+
90
+ return l0_lambda * l0_penalty + l2_lambda * l2_penalty
91
+
92
+
93
+ def get_sparsity_stats(model: nn.Module) -> dict[str, dict[str, Any]]:
94
+ """
95
+ Get sparsity statistics for all L0 layers in a model.
96
+
97
+ Parameters
98
+ ----------
99
+ model : nn.Module
100
+ Model containing L0 layers
101
+
102
+ Returns
103
+ -------
104
+ Dict[str, Dict[str, Any]]
105
+ Dictionary mapping layer names to their statistics
106
+ """
107
+ stats = {}
108
+
109
+ for name, module in model.named_modules():
110
+ if hasattr(module, "get_sparsity") and hasattr(
111
+ module, "get_l0_penalty"
112
+ ):
113
+ active_params = module.get_l0_penalty().item()
114
+
115
+ # Calculate total parameters
116
+ if hasattr(module, "weight"):
117
+ total_params = module.weight.numel()
118
+ elif hasattr(module, "gates"):
119
+ total_params = module.gates.gate_size
120
+ if isinstance(total_params, tuple):
121
+ total_params = np.prod(total_params)
122
+ else:
123
+ total_params = active_params / (1 - module.get_sparsity())
124
+
125
+ stats[name] = {
126
+ "sparsity": module.get_sparsity(),
127
+ "active_params": active_params,
128
+ "total_params": total_params,
129
+ }
130
+
131
+ return stats
132
+
133
+
134
+ def get_active_parameter_count(model: nn.Module) -> int:
135
+ """
136
+ Get the total number of active parameters in the model.
137
+
138
+ Parameters
139
+ ----------
140
+ model : nn.Module
141
+ Model containing L0 layers
142
+
143
+ Returns
144
+ -------
145
+ int
146
+ Total number of active parameters
147
+ """
148
+ total = 0
149
+
150
+ for module in model.modules():
151
+ if hasattr(module, "get_l0_penalty"):
152
+ total += int(module.get_l0_penalty().item()) # type: ignore[operator]
153
+
154
+ return total
155
+
156
+
157
+ class TemperatureScheduler:
158
+ """
159
+ Temperature scheduler for annealing during training.
160
+
161
+ Gradually decreases temperature to make gates more discrete.
162
+
163
+ Parameters
164
+ ----------
165
+ initial_temp : float
166
+ Starting temperature
167
+ final_temp : float
168
+ Final temperature
169
+ anneal_epochs : int
170
+ Number of epochs over which to anneal
171
+ schedule : str
172
+ Type of schedule ('linear', 'exponential', 'cosine')
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ initial_temp: float = 2.0,
178
+ final_temp: float = 0.1,
179
+ anneal_epochs: int = 100,
180
+ schedule: str = "exponential",
181
+ ):
182
+ self.initial_temp = initial_temp
183
+ self.final_temp = final_temp
184
+ self.anneal_epochs = anneal_epochs
185
+ self.schedule = schedule
186
+
187
+ def get_temperature(self, epoch: int) -> float:
188
+ """
189
+ Get temperature for a given epoch.
190
+
191
+ Parameters
192
+ ----------
193
+ epoch : int
194
+ Current epoch number
195
+
196
+ Returns
197
+ -------
198
+ float
199
+ Temperature value
200
+ """
201
+ if epoch >= self.anneal_epochs:
202
+ return self.final_temp
203
+
204
+ progress = epoch / self.anneal_epochs
205
+
206
+ if self.schedule == "linear":
207
+ temp = (
208
+ self.initial_temp
209
+ - (self.initial_temp - self.final_temp) * progress
210
+ )
211
+
212
+ elif self.schedule == "exponential":
213
+ log_temp = (
214
+ np.log(self.initial_temp)
215
+ - (np.log(self.initial_temp) - np.log(self.final_temp))
216
+ * progress
217
+ )
218
+ temp = np.exp(log_temp)
219
+
220
+ elif self.schedule == "cosine":
221
+ temp = (
222
+ self.final_temp
223
+ + (self.initial_temp - self.final_temp)
224
+ * (1 + np.cos(np.pi * progress))
225
+ / 2
226
+ )
227
+
228
+ else:
229
+ raise ValueError(f"Unknown schedule type: {self.schedule}")
230
+
231
+ return float(temp)
232
+
233
+
234
+ def update_temperatures(model: nn.Module, temperature: float) -> None:
235
+ """
236
+ Update temperature for all HardConcrete distributions in model.
237
+
238
+ Parameters
239
+ ----------
240
+ model : nn.Module
241
+ Model containing HardConcrete gates
242
+ temperature : float
243
+ New temperature value
244
+ """
245
+ for module in model.modules():
246
+ # Check for HardConcrete gates in layers
247
+ if hasattr(module, "weight_gates"):
248
+ module.weight_gates.temperature = temperature # type: ignore[union-attr]
249
+ if hasattr(module, "channel_gates"):
250
+ module.channel_gates.temperature = temperature # type: ignore[union-attr]
251
+ if hasattr(module, "gates"):
252
+ if hasattr(module.gates, "temperature"):
253
+ module.gates.temperature = temperature
254
+
255
+ # Direct HardConcrete modules
256
+ from .distributions import HardConcrete
257
+
258
+ if isinstance(module, HardConcrete):
259
+ module.temperature = temperature
260
+
261
+
262
+ class PenaltyTracker:
263
+ """
264
+ Track penalties and sparsity metrics during training.
265
+
266
+ Useful for monitoring and visualization.
267
+ """
268
+
269
+ def __init__(self) -> None:
270
+ self.history: dict[str, list[float]] = defaultdict(list)
271
+
272
+ def log(self, name: str, value: float) -> None:
273
+ """
274
+ Log a metric value.
275
+
276
+ Parameters
277
+ ----------
278
+ name : str
279
+ Metric name
280
+ value : float
281
+ Metric value
282
+ """
283
+ self.history[name].append(value)
284
+
285
+ def get_history(self, name: str) -> list[float]:
286
+ """
287
+ Get history for a metric.
288
+
289
+ Parameters
290
+ ----------
291
+ name : str
292
+ Metric name
293
+
294
+ Returns
295
+ -------
296
+ List[float]
297
+ History of values
298
+ """
299
+ return self.history[name]
300
+
301
+ def get_stats(self, name: str) -> dict[str, float]:
302
+ """
303
+ Get statistics for a metric.
304
+
305
+ Parameters
306
+ ----------
307
+ name : str
308
+ Metric name
309
+
310
+ Returns
311
+ -------
312
+ Dict[str, float]
313
+ Statistics (mean, min, max, last)
314
+ """
315
+ values = self.history[name]
316
+ if not values:
317
+ return {}
318
+
319
+ return {
320
+ "mean": float(np.mean(values)),
321
+ "std": float(np.std(values)),
322
+ "min": float(np.min(values)),
323
+ "max": float(np.max(values)),
324
+ "last": values[-1],
325
+ }
326
+
327
+ def clear(self) -> None:
328
+ """Clear all history."""
329
+ self.history.clear()
330
+
331
+ def save_to_csv(self, filepath: str) -> None:
332
+ """
333
+ Save history to CSV file.
334
+
335
+ Parameters
336
+ ----------
337
+ filepath : str
338
+ Path to save CSV
339
+ """
340
+ import pandas as pd
341
+
342
+ df = pd.DataFrame(self.history)
343
+ df.to_csv(filepath, index=False)
344
+
345
+ def plot_history(
346
+ self,
347
+ metrics: list[str] | None = None,
348
+ save_path: str | None = None,
349
+ ) -> None:
350
+ """
351
+ Plot metric history.
352
+
353
+ Parameters
354
+ ----------
355
+ metrics : Optional[List[str]]
356
+ Metrics to plot (all if None)
357
+ save_path : Optional[str]
358
+ Path to save figure
359
+ """
360
+ import matplotlib.pyplot as plt
361
+
362
+ metrics = metrics or list(self.history.keys())
363
+
364
+ fig, axes = plt.subplots(
365
+ len(metrics), 1, figsize=(10, 4 * len(metrics)), squeeze=False
366
+ )
367
+
368
+ for i, metric in enumerate(metrics):
369
+ values = self.history[metric]
370
+ axes[i, 0].plot(values)
371
+ axes[i, 0].set_title(metric)
372
+ axes[i, 0].set_xlabel("Step")
373
+ axes[i, 0].set_ylabel("Value")
374
+ axes[i, 0].grid(True)
375
+
376
+ plt.tight_layout()
377
+
378
+ if save_path:
379
+ plt.savefig(save_path)
380
+ else:
381
+ plt.show()
@@ -0,0 +1,215 @@
1
+ Metadata-Version: 2.4
2
+ Name: l0-python
3
+ Version: 0.1.0
4
+ Summary: L0 regularization for sparse neural networks and intelligent sampling
5
+ Author-email: PolicyEngine <hello@policyengine.org>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 PolicyEngine
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/PolicyEngine/L0
29
+ Project-URL: Repository, https://github.com/PolicyEngine/L0
30
+ Project-URL: Documentation, https://github.com/PolicyEngine/L0#readme
31
+ Project-URL: Issues, https://github.com/PolicyEngine/L0/issues
32
+ Keywords: l0,regularization,sparsity,neural networks,calibration
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Intended Audience :: Science/Research
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.13
38
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
39
+ Requires-Python: >=3.13
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: torch>=2.0.0
43
+ Requires-Dist: numpy>=1.20.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest>=7.0; extra == "dev"
46
+ Requires-Dist: pytest-cov>=3.0; extra == "dev"
47
+ Requires-Dist: black>=22.0; extra == "dev"
48
+ Requires-Dist: mypy>=0.900; extra == "dev"
49
+ Requires-Dist: ruff>=0.0.280; extra == "dev"
50
+ Requires-Dist: pre-commit>=3.0; extra == "dev"
51
+ Requires-Dist: build>=1.3; extra == "dev"
52
+ Provides-Extra: docs
53
+ Requires-Dist: mystmd>=1.3; extra == "docs"
54
+ Requires-Dist: jupyter>=1.0; extra == "docs"
55
+ Requires-Dist: notebook>=7.0; extra == "docs"
56
+ Requires-Dist: ipykernel>=6.0; extra == "docs"
57
+ Provides-Extra: examples
58
+ Requires-Dist: matplotlib>=3.3; extra == "examples"
59
+ Requires-Dist: tqdm>=4.60; extra == "examples"
60
+ Requires-Dist: pandas>=1.3; extra == "examples"
61
+ Requires-Dist: scikit-learn>=1.0; extra == "examples"
62
+ Requires-Dist: torchvision>=0.15; extra == "examples"
63
+ Dynamic: license-file
64
+
65
+ # L0 Regularization
66
+
67
+ A PyTorch implementation of L0 regularization for neural network sparsification and intelligent sampling, based on [Louizos, Welling, & Kingma (2017)](https://arxiv.org/abs/1712.01312).
68
+
69
+ ## Features
70
+
71
+ - **Hard Concrete Distribution**: Differentiable approximation of L0 norm
72
+ - **Sparse Neural Network Layers**: L0Linear, L0Conv2d with automatic pruning
73
+ - **Intelligent Sampling**: Sample/feature selection gates for calibration
74
+ - **L0L2 Combined Penalty**: Recommended approach to prevent overfitting
75
+ - **Temperature Scheduling**: Annealing for improved convergence
76
+ - **TDD Development**: Comprehensive test coverage
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install l0
82
+ ```
83
+
84
+ For development:
85
+ ```bash
86
+ git clone https://github.com/PolicyEngine/L0.git
87
+ cd L0
88
+ pip install -e .[dev]
89
+ ```
90
+
91
+ ## Quick Start
92
+
93
+ ### Neural Network Sparsification
94
+
95
+ ```python
96
+ import torch
97
+ from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
98
+
99
+ # Create a sparse model
100
+ class SparseModel(torch.nn.Module):
101
+ def __init__(self):
102
+ super().__init__()
103
+ self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
104
+ self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
105
+
106
+ def forward(self, x):
107
+ x = torch.relu(self.fc1(x))
108
+ return self.fc2(x)
109
+
110
+ model = SparseModel()
111
+ optimizer = torch.optim.Adam(model.parameters())
112
+ scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
113
+
114
+ # Training loop
115
+ for epoch in range(100):
116
+ # Update temperature
117
+ temp = scheduler.get_temperature(epoch)
118
+ update_temperatures(model, temp)
119
+
120
+ # Forward pass
121
+ output = model(input_data)
122
+ ce_loss = criterion(output, target)
123
+
124
+ # Add L0L2 penalty
125
+ penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
126
+ loss = ce_loss + penalty
127
+
128
+ # Backward pass
129
+ optimizer.zero_grad()
130
+ loss.backward()
131
+ optimizer.step()
132
+ ```
133
+
134
+ ### Intelligent Sample Selection
135
+
136
+ ```python
137
+ from l0 import SampleGate, HybridGate
138
+
139
+ # Pure L0 selection
140
+ gate = SampleGate(n_samples=10000, target_samples=1000)
141
+ selected_data, indices = gate.select_samples(data)
142
+
143
+ # Hybrid selection (25% L0, 75% random)
144
+ hybrid = HybridGate(
145
+ n_items=10000,
146
+ l0_fraction=0.25,
147
+ random_fraction=0.75,
148
+ target_items=1000
149
+ )
150
+ selected, indices, types = hybrid.select(data)
151
+ ```
152
+
153
+ ### Feature Selection
154
+
155
+ ```python
156
+ from l0 import FeatureGate
157
+
158
+ # Select top features
159
+ gate = FeatureGate(n_features=1000, max_features=50)
160
+ selected_data, feature_indices = gate.select_features(data)
161
+
162
+ # Get feature importance
163
+ importance = gate.get_feature_importance()
164
+ ```
165
+
166
+ ## Integration with PolicyEngine
167
+
168
+ This package is designed to work with PolicyEngine's calibration system:
169
+
170
+ ```python
171
+ # In policyengine-us-data or similar
172
+ from l0 import HardConcrete
173
+
174
+ # Use for household selection in CPS calibration
175
+ gates = HardConcrete(
176
+ len(household_weights),
177
+ temperature=0.25,
178
+ init_mean=0.999 # Start with most households
179
+ )
180
+
181
+ # Apply gates during reweighting
182
+ masked_weights = weights * gates()
183
+ ```
184
+
185
+ ## Documentation
186
+
187
+ Full documentation available at: https://policyengine.github.io/L0/
188
+
189
+ ## Testing
190
+
191
+ Run tests with:
192
+ ```bash
193
+ pytest tests/ -v --cov=l0
194
+ ```
195
+
196
+ ## Acknowledgments
197
+
198
+ This implementation is inspired by and builds upon the [original L0 regularization code](https://github.com/AMLab-Amsterdam/L0_regularization) by AMLab Amsterdam, which accompanied the paper by Louizos et al. (2018).
199
+
200
+ ## Citation
201
+
202
+ If you use this package, please cite:
203
+
204
+ ```bibtex
205
+ @article{louizos2017learning,
206
+ title={Learning Sparse Neural Networks through L0 Regularization},
207
+ author={Louizos, Christos and Welling, Max and Kingma, Diederik P},
208
+ journal={arXiv preprint arXiv:1712.01312},
209
+ year={2017}
210
+ }
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -0,0 +1,10 @@
1
+ l0/__init__.py,sha256=meL6jJ1CxO6pO373nY-nwGXnpzWZGC1Ts6q2TLyAeLM,1122
2
+ l0/distributions.py,sha256=GvVoDasdxoDlPPdzt33lFzkuJgQsfkEn9e-abxru3nM,6325
3
+ l0/gates.py,sha256=54OORMg9qSsdbIyzYIL9t9CCHWsKbumSKl_d4-YFuNU,11443
4
+ l0/layers.py,sha256=j_DPEFdfUc37ojtoK2ufGzVYzZ_DwusqTcJXCVxZS68,13785
5
+ l0/penalties.py,sha256=okqXgX2eGUmg5prcDclicTQALPPPF9FA5eQP09V4GTE,9569
6
+ l0_python-0.1.0.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
7
+ l0_python-0.1.0.dist-info/METADATA,sha256=hrAugZ-F7oKmleyJ8WjVTykRixBmuTiQ2M4oae1Ceu4,6794
8
+ l0_python-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ l0_python-0.1.0.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
10
+ l0_python-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PolicyEngine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ l0