l0-python 0.4.0__py3-none-any.whl → 0.4.2__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/calibration.py CHANGED
@@ -444,9 +444,9 @@ class SparseCalibrationWeights(nn.Module):
444
444
  rel_errors[group_mask].mean().item()
445
445
  )
446
446
  group_losses.append(group_mean_err)
447
- mean_group_loss = np.mean(group_losses)
447
+ mean_group_mare = np.mean(group_losses)
448
448
  else:
449
- mean_group_loss = mean_rel_err
449
+ mean_group_mare = mean_rel_err
450
450
 
451
451
  # Calculate sparsity percentage
452
452
  sparsity_pct = 100 * (
@@ -461,7 +461,7 @@ class SparseCalibrationWeights(nn.Module):
461
461
  if target_groups is not None:
462
462
  print(
463
463
  f"Epoch {epoch+1:4d}: "
464
- f"mean_group_loss={mean_group_loss:.1%}, "
464
+ f"mean_group_mare={mean_group_mare:.4%}, "
465
465
  f"max_error={max_rel_err:.1%}, "
466
466
  f"total_loss={actual_total_loss:.3f}, "
467
467
  f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
@@ -469,7 +469,7 @@ class SparseCalibrationWeights(nn.Module):
469
469
  else:
470
470
  print(
471
471
  f"Epoch {epoch+1:4d}: "
472
- f"mean_error={mean_rel_err:.1%}, "
472
+ f"mean_error={mean_rel_err:.4%}, "
473
473
  f"max_error={max_rel_err:.1%}, "
474
474
  f"total_loss={actual_total_loss:.3f}, "
475
475
  f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: l0-python
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: L0 regularization for sparse neural networks and intelligent sampling
5
5
  Author-email: PolicyEngine <hello@policyengine.org>
6
6
  License: MIT License
@@ -51,7 +51,7 @@ Requires-Dist: scikit-learn>=1.0; extra == "dev"
51
51
  Requires-Dist: yaml-changelog>=0.3.0; extra == "dev"
52
52
  Requires-Dist: twine>=4.0.0; extra == "dev"
53
53
  Provides-Extra: docs
54
- Requires-Dist: mystmd>=1.3; extra == "docs"
54
+ Requires-Dist: jupyter-book>=1.0; extra == "docs"
55
55
  Requires-Dist: jupyter>=1.0; extra == "docs"
56
56
  Requires-Dist: notebook>=7.0; extra == "docs"
57
57
  Requires-Dist: ipykernel>=6.0; extra == "docs"
@@ -65,21 +65,29 @@ Dynamic: license-file
65
65
 
66
66
  # L0 Regularization
67
67
 
68
- 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
+ [![PyPI version](https://badge.fury.io/py/l0-python.svg)](https://pypi.org/project/l0-python/)
69
+ [![CI](https://github.com/PolicyEngine/L0/actions/workflows/push.yml/badge.svg)](https://github.com/PolicyEngine/L0/actions)
69
70
 
70
- ## Features
71
+ A PyTorch implementation of L0 regularization based on [Louizos, Welling, & Kingma (2017)](https://arxiv.org/abs/1712.01312), with a **critical bug fix** to the original authors' implementation.
71
72
 
72
- - **Hard Concrete Distribution**: Differentiable approximation of L0 norm
73
- - **Sparse Neural Network Layers**: L0Linear, L0Conv2d with automatic pruning
74
- - **Intelligent Sampling**: Sample/feature selection gates for calibration
75
- - **L0L2 Combined Penalty**: Recommended approach to prevent overfitting
76
- - **Temperature Scheduling**: Annealing for improved convergence
77
- - **TDD Development**: Comprehensive test coverage
73
+ ## Why This Package?
74
+
75
+ The original L0 implementation from AMLab-Amsterdam contains a bug in test-time gate computation where the temperature parameter is incorrectly omitted. This prevents proper sparsity and degrades performance. Our implementation corrects this error:
76
+
77
+ ```python
78
+ # Original (buggy): gates never fully close
79
+ pi = sigmoid(log_alpha)
80
+
81
+ # Corrected: temperature division required for proper sparsity
82
+ pi = sigmoid(log_alpha / temperature)
83
+ ```
84
+
85
+ This fix enables gates to achieve true 0/1 values, producing exact sparsity as intended by the L0 formulation.
78
86
 
79
87
  ## Installation
80
88
 
81
89
  ```bash
82
- pip install l0
90
+ pip install l0-python
83
91
  ```
84
92
 
85
93
  For development:
@@ -89,21 +97,76 @@ cd L0
89
97
  pip install -e .[dev]
90
98
  ```
91
99
 
92
- ## Quick Start
100
+ ## Primary Use Case: Survey Calibration
101
+
102
+ This package was developed for PolicyEngine's survey calibration system, where we select a sparse subset of survey households while matching population targets.
103
+
104
+ ```python
105
+ import numpy as np
106
+ from scipy import sparse as sp
107
+ from l0.calibration import SparseCalibrationWeights
108
+
109
+ # Setup: Q targets, N households
110
+ Q, N = 200, 10000
111
+ M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
112
+ y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
113
+
114
+ # Initialize model
115
+ model = SparseCalibrationWeights(
116
+ n_features=N,
117
+ beta=0.35,
118
+ gamma=-0.1,
119
+ zeta=1.1,
120
+ init_keep_prob=0.5, # Start with 50% active probability
121
+ init_weights=1.0, # Or pass array of initial weights
122
+ log_weight_jitter_sd=0.05,
123
+ device="cuda", # GPU acceleration
124
+ )
125
+
126
+ # Train with L0+L2 regularization
127
+ model.fit(
128
+ M=M,
129
+ y=y,
130
+ lambda_l0=1e-6, # Controls sparsity level
131
+ lambda_l2=1e-8, # Prevents weight explosion
132
+ lr=0.15,
133
+ epochs=2000,
134
+ loss_type="relative", # Scale-invariant loss
135
+ verbose=True,
136
+ verbose_freq=100,
137
+ )
138
+
139
+ # Get results
140
+ active = model.get_active_weights()
141
+ print(f"Selected {active['count']} of {N} households ({100*active['count']/N:.1f}%)")
142
+ print(f"Sparsity: {model.get_sparsity():.1%}")
143
+
144
+ # Predict calibrated totals
145
+ y_pred = model.predict(M)
146
+ ```
147
+
148
+ ### Key Features for Calibration
149
+
150
+ - **Non-negative weights**: All weights constrained to be positive via log-space parameterization
151
+ - **Sparse solutions**: L0 penalty directly minimizes the count of active weights
152
+ - **Relative loss**: Scale-invariant loss for targets spanning orders of magnitude
153
+ - **Group-wise averaging**: Balance loss contributions across target groups with different cardinalities
154
+ - **GPU support**: CUDA acceleration for large-scale problems
155
+
156
+ ## Neural Network Sparsification
93
157
 
94
- ### Neural Network Sparsification
158
+ The package also supports traditional neural network pruning:
95
159
 
96
160
  ```python
97
161
  import torch
98
162
  from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
99
163
 
100
- # Create a sparse model
101
164
  class SparseModel(torch.nn.Module):
102
165
  def __init__(self):
103
166
  super().__init__()
104
167
  self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
105
168
  self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
106
-
169
+
107
170
  def forward(self, x):
108
171
  x = torch.relu(self.fc1(x))
109
172
  return self.fc2(x)
@@ -112,95 +175,71 @@ model = SparseModel()
112
175
  optimizer = torch.optim.Adam(model.parameters())
113
176
  scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
114
177
 
115
- # Training loop
116
178
  for epoch in range(100):
117
- # Update temperature
118
179
  temp = scheduler.get_temperature(epoch)
119
180
  update_temperatures(model, temp)
120
-
121
- # Forward pass
181
+
122
182
  output = model(input_data)
123
183
  ce_loss = criterion(output, target)
124
-
125
- # Add L0L2 penalty
126
184
  penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
127
185
  loss = ce_loss + penalty
128
-
129
- # Backward pass
186
+
130
187
  optimizer.zero_grad()
131
188
  loss.backward()
132
189
  optimizer.step()
133
190
  ```
134
191
 
135
- ### Intelligent Sample Selection
192
+ ### Available Layers
193
+
194
+ - `L0Linear`: Fully connected layer with L0 gates on weights
195
+ - `L0Conv2d`: 2D convolution with channel-wise L0 gates
196
+ - `L0DepthwiseConv2d`: Depthwise convolution with L0 gates
197
+ - `SparseMLP`: Multi-layer perceptron with built-in L0 regularization
198
+
199
+ ## Intelligent Sampling Gates
200
+
201
+ Standalone gates for sample/feature selection:
136
202
 
137
203
  ```python
138
- from l0 import SampleGate, HybridGate
204
+ from l0 import SampleGate, FeatureGate, HybridGate
139
205
 
140
- # Pure L0 selection
206
+ # Select samples via learned gates
141
207
  gate = SampleGate(n_samples=10000, target_samples=1000)
142
208
  selected_data, indices = gate.select_samples(data)
143
209
 
144
- # Hybrid selection (25% L0, 75% random)
210
+ # Select features
211
+ gate = FeatureGate(n_features=1000, max_features=50)
212
+ selected_data, indices = gate.select_features(data)
213
+
214
+ # Hybrid: combine L0 selection with random sampling
145
215
  hybrid = HybridGate(
146
216
  n_items=10000,
147
- l0_fraction=0.25,
148
- random_fraction=0.75,
149
- target_items=1000
217
+ l0_fraction=0.25, # 25% via learned L0 gates
218
+ random_fraction=0.75 # 75% random for coverage
150
219
  )
151
220
  selected, indices, types = hybrid.select(data)
152
221
  ```
153
222
 
154
- ### Feature Selection
155
-
156
- ```python
157
- from l0 import FeatureGate
158
-
159
- # Select top features
160
- gate = FeatureGate(n_features=1000, max_features=50)
161
- selected_data, feature_indices = gate.select_features(data)
162
-
163
- # Get feature importance
164
- importance = gate.get_feature_importance()
165
- ```
166
-
167
- ## Integration with PolicyEngine
168
-
169
- This package is designed to work with PolicyEngine's calibration system:
170
-
171
- ```python
172
- # In policyengine-us-data or similar
173
- from l0 import HardConcrete
174
-
175
- # Use for household selection in CPS calibration
176
- gates = HardConcrete(
177
- len(household_weights),
178
- temperature=0.25,
179
- init_mean=0.999 # Start with most households
180
- )
223
+ ## How L0 Regularization Works
181
224
 
182
- # Apply gates during reweighting
183
- masked_weights = weights * gates()
184
- ```
225
+ Unlike post-hoc pruning (setting small weights to zero), L0 regularization integrates sparsity into the training objective:
185
226
 
186
- ## Documentation
227
+ 1. **Stochastic gates**: Each weight has a learned gate parameter controlling activation probability
228
+ 2. **Hard Concrete distribution**: Enables differentiable 0/1 gate values during training
229
+ 3. **Expected L0 penalty**: Minimizes the expected number of active gates
230
+ 4. **Temperature annealing**: Gradually sharpens gates from soft to hard decisions
187
231
 
188
- Full documentation available at: https://policyengine.github.io/L0/
232
+ The result: the network learns *which* weights should be zero as part of optimization, not as a post-processing step.
189
233
 
190
234
  ## Testing
191
235
 
192
- Run tests with:
193
236
  ```bash
194
237
  pytest tests/ -v --cov=l0
195
238
  ```
196
239
 
197
- ## Acknowledgments
198
-
199
- 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).
200
-
201
240
  ## Citation
202
241
 
203
- If you use this package, please cite:
242
+ If you use this package, please cite the original paper:
204
243
 
205
244
  ```bibtex
206
245
  @article{louizos2017learning,
@@ -213,4 +252,4 @@ If you use this package, please cite:
213
252
 
214
253
  ## License
215
254
 
216
- MIT License - see [LICENSE](LICENSE) file for details.
255
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -1,12 +1,12 @@
1
1
  l0/__init__.py,sha256=ezGVLGbdU_jAElAJSdLKjSWCnZRILAZi8qMBPjcHwQc,1192
2
- l0/calibration.py,sha256=EnIhQPkC6yvRp0-o1qG3SCVUp6pIqwkqkx3DX3uWdG0,17838
2
+ l0/calibration.py,sha256=3VM-28M1yp-DM6AlHX7sqZCaE7OJevpY9Ey-syjSPik,17838
3
3
  l0/distributions.py,sha256=GvVoDasdxoDlPPdzt33lFzkuJgQsfkEn9e-abxru3nM,6325
4
4
  l0/gates.py,sha256=54OORMg9qSsdbIyzYIL9t9CCHWsKbumSKl_d4-YFuNU,11443
5
5
  l0/layers.py,sha256=j_DPEFdfUc37ojtoK2ufGzVYzZ_DwusqTcJXCVxZS68,13785
6
6
  l0/penalties.py,sha256=okqXgX2eGUmg5prcDclicTQALPPPF9FA5eQP09V4GTE,9569
7
7
  l0/sparse.py,sha256=UVpkrrJBnQ9iSNDoWK5F_sr43tQareWXzYHyMq_cKss,9726
8
- l0_python-0.4.0.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
9
- l0_python-0.4.0.dist-info/METADATA,sha256=tVMaa0UIafPSIv9V9WWoxga7x2xtIVMhQ_egiX3C3y4,6833
10
- l0_python-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
- l0_python-0.4.0.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
12
- l0_python-0.4.0.dist-info/RECORD,,
8
+ l0_python-0.4.2.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
9
+ l0_python-0.4.2.dist-info/METADATA,sha256=czuRwTuF5iOn6f-rsFSxk_gHXWuKXAaYAmDugkGw4rA,9074
10
+ l0_python-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ l0_python-0.4.2.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
12
+ l0_python-0.4.2.dist-info/RECORD,,