l0-python 0.4.2__py3-none-any.whl → 0.4.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: l0-python
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
@@ -68,21 +68,7 @@ Dynamic: license-file
68
68
  [![PyPI version](https://badge.fury.io/py/l0-python.svg)](https://pypi.org/project/l0-python/)
69
69
  [![CI](https://github.com/PolicyEngine/L0/actions/workflows/push.yml/badge.svg)](https://github.com/PolicyEngine/L0/actions)
70
70
 
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.
72
-
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.
71
+ A PyTorch implementation of L0 regularization based on [Louizos, Welling, & Kingma (2017)](https://arxiv.org/abs/1712.01312), designed for survey calibration and sparse regression.
86
72
 
87
73
  ## Installation
88
74
 
@@ -97,9 +83,24 @@ cd L0
97
83
  pip install -e .[dev]
98
84
  ```
99
85
 
86
+ ## Our Approach to Test-Time Gates
87
+
88
+ The original Hard Concrete formulation uses temperature (β) during training to control the sharpness of stochastic gates. At test time, there's a design choice: whether to include temperature in the deterministic gate computation.
89
+
90
+ We include temperature at test time:
91
+ ```python
92
+ # Our approach: include temperature
93
+ z = sigmoid(log_alpha / beta) * (zeta - gamma) + gamma
94
+
95
+ # Alternative: omit temperature
96
+ z = sigmoid(log_alpha) * (zeta - gamma) + gamma
97
+ ```
98
+
99
+ Including temperature produces sharper 0/1 decisions, which we find beneficial for achieving clean sparsity in our applications. See `examples/sparse_regression_demo.py` for a demonstration on a 4-variable regression problem.
100
+
100
101
  ## Primary Use Case: Survey Calibration
101
102
 
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
+ This package was developed for PolicyEngine's survey calibration, where we select a sparse subset of survey households while matching population targets.
103
104
 
104
105
  ```python
105
106
  import numpy as np
@@ -117,119 +118,73 @@ model = SparseCalibrationWeights(
117
118
  beta=0.35,
118
119
  gamma=-0.1,
119
120
  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
121
+ init_keep_prob=0.5,
122
+ init_weights=1.0,
122
123
  log_weight_jitter_sd=0.05,
123
- device="cuda", # GPU acceleration
124
+ device="cuda",
124
125
  )
125
126
 
126
127
  # Train with L0+L2 regularization
127
128
  model.fit(
128
129
  M=M,
129
130
  y=y,
130
- lambda_l0=1e-6, # Controls sparsity level
131
- lambda_l2=1e-8, # Prevents weight explosion
131
+ lambda_l0=1e-6,
132
+ lambda_l2=1e-8,
132
133
  lr=0.15,
133
134
  epochs=2000,
134
- loss_type="relative", # Scale-invariant loss
135
+ loss_type="relative",
135
136
  verbose=True,
136
- verbose_freq=100,
137
137
  )
138
138
 
139
139
  # Get results
140
140
  active = model.get_active_weights()
141
- print(f"Selected {active['count']} of {N} households ({100*active['count']/N:.1f}%)")
141
+ print(f"Selected {active['count']} of {N} households")
142
142
  print(f"Sparsity: {model.get_sparsity():.1%}")
143
-
144
- # Predict calibrated totals
145
- y_pred = model.predict(M)
146
143
  ```
147
144
 
148
- ### Key Features for Calibration
145
+ ### Key Features
149
146
 
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
147
+ - **Non-negative weights**: Constrained via log-space parameterization
148
+ - **L0 sparsity**: Directly minimizes the count of active weights
149
+ - **Relative loss**: Scale-invariant for targets spanning orders of magnitude
150
+ - **Group-wise averaging**: Balance loss across target groups with different sizes
151
+ - **GPU support**: CUDA acceleration for large problems
155
152
 
156
- ## Neural Network Sparsification
153
+ ## Sparse Regression
157
154
 
158
- The package also supports traditional neural network pruning:
155
+ For sparse linear regression with scipy sparse matrices:
159
156
 
160
157
  ```python
161
- import torch
162
- from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
163
-
164
- class SparseModel(torch.nn.Module):
165
- def __init__(self):
166
- super().__init__()
167
- self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
168
- self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
169
-
170
- def forward(self, x):
171
- x = torch.relu(self.fc1(x))
172
- return self.fc2(x)
173
-
174
- model = SparseModel()
175
- optimizer = torch.optim.Adam(model.parameters())
176
- scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
177
-
178
- for epoch in range(100):
179
- temp = scheduler.get_temperature(epoch)
180
- update_temperatures(model, temp)
181
-
182
- output = model(input_data)
183
- ce_loss = criterion(output, target)
184
- penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
185
- loss = ce_loss + penalty
186
-
187
- optimizer.zero_grad()
188
- loss.backward()
189
- optimizer.step()
190
- ```
191
-
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
158
+ from scipy import sparse as sp
159
+ from l0.sparse import SparseL0Linear
198
160
 
199
- ## Intelligent Sampling Gates
161
+ # Sparse design matrix
162
+ X = sp.random(1000, 500, density=0.1, format="csr")
163
+ y = np.random.randn(1000)
200
164
 
201
- Standalone gates for sample/feature selection:
165
+ model = SparseL0Linear(n_features=500)
166
+ model.fit(X, y, lambda_l0=0.001, epochs=1000)
202
167
 
203
- ```python
204
- from l0 import SampleGate, FeatureGate, HybridGate
168
+ # Get sparse coefficients
169
+ coef = model.get_coefficients(threshold=0.01)
170
+ ```
205
171
 
206
- # Select samples via learned gates
207
- gate = SampleGate(n_samples=10000, target_samples=1000)
208
- selected_data, indices = gate.select_samples(data)
172
+ ## Example: Variable Selection
209
173
 
210
- # Select features
211
- gate = FeatureGate(n_features=1000, max_features=50)
212
- selected_data, indices = gate.select_features(data)
174
+ The `examples/sparse_regression_demo.py` script demonstrates L0 regularization on a simple problem where the true coefficients are `[1, 0, -2, 0]`:
213
175
 
214
- # Hybrid: combine L0 selection with random sampling
215
- hybrid = HybridGate(
216
- n_items=10000,
217
- l0_fraction=0.25, # 25% via learned L0 gates
218
- random_fraction=0.75 # 75% random for coverage
219
- )
220
- selected, indices, types = hybrid.select(data)
176
+ ```bash
177
+ python examples/sparse_regression_demo.py
221
178
  ```
222
179
 
223
- ## How L0 Regularization Works
224
-
225
- Unlike post-hoc pruning (setting small weights to zero), L0 regularization integrates sparsity into the training objective:
226
-
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
180
+ Output:
181
+ ```
182
+ True coefficients: [ 1. 0. -2. 0.]
183
+ Recovered coefficients: [ 1.039 0. -2.069 -0. ]
184
+ Gates: [1. 0. 1. 0.]
185
+ ```
231
186
 
232
- The result: the network learns *which* weights should be zero as part of optimization, not as a post-processing step.
187
+ The model correctly identifies that only variables 1 and 3 contribute to the outcome.
233
188
 
234
189
  ## Testing
235
190
 
@@ -239,8 +194,6 @@ pytest tests/ -v --cov=l0
239
194
 
240
195
  ## Citation
241
196
 
242
- If you use this package, please cite the original paper:
243
-
244
197
  ```bibtex
245
198
  @article{louizos2017learning,
246
199
  title={Learning Sparse Neural Networks through L0 Regularization},
@@ -5,8 +5,8 @@ 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.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,,
8
+ l0_python-0.4.3.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
9
+ l0_python-0.4.3.dist-info/METADATA,sha256=gN2ufd7zkVDielkTS2iGbUeKkf9bIMTiGZtvCkpc960,7095
10
+ l0_python-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ l0_python-0.4.3.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
12
+ l0_python-0.4.3.dist-info/RECORD,,