l0-python 0.4.2__tar.gz → 0.4.3__tar.gz

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.
Files changed (24) hide show
  1. {l0_python-0.4.2/l0_python.egg-info → l0_python-0.4.3}/PKG-INFO +54 -101
  2. l0_python-0.4.3/README.md +143 -0
  3. {l0_python-0.4.2 → l0_python-0.4.3/l0_python.egg-info}/PKG-INFO +54 -101
  4. {l0_python-0.4.2 → l0_python-0.4.3}/pyproject.toml +1 -1
  5. l0_python-0.4.2/README.md +0 -190
  6. {l0_python-0.4.2 → l0_python-0.4.3}/LICENSE +0 -0
  7. {l0_python-0.4.2 → l0_python-0.4.3}/l0/__init__.py +0 -0
  8. {l0_python-0.4.2 → l0_python-0.4.3}/l0/calibration.py +0 -0
  9. {l0_python-0.4.2 → l0_python-0.4.3}/l0/distributions.py +0 -0
  10. {l0_python-0.4.2 → l0_python-0.4.3}/l0/gates.py +0 -0
  11. {l0_python-0.4.2 → l0_python-0.4.3}/l0/layers.py +0 -0
  12. {l0_python-0.4.2 → l0_python-0.4.3}/l0/penalties.py +0 -0
  13. {l0_python-0.4.2 → l0_python-0.4.3}/l0/sparse.py +0 -0
  14. {l0_python-0.4.2 → l0_python-0.4.3}/l0_python.egg-info/SOURCES.txt +0 -0
  15. {l0_python-0.4.2 → l0_python-0.4.3}/l0_python.egg-info/dependency_links.txt +0 -0
  16. {l0_python-0.4.2 → l0_python-0.4.3}/l0_python.egg-info/requires.txt +0 -0
  17. {l0_python-0.4.2 → l0_python-0.4.3}/l0_python.egg-info/top_level.txt +0 -0
  18. {l0_python-0.4.2 → l0_python-0.4.3}/setup.cfg +0 -0
  19. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_calibration.py +0 -0
  20. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_distributions.py +0 -0
  21. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_gates.py +0 -0
  22. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_layers.py +0 -0
  23. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_penalties.py +0 -0
  24. {l0_python-0.4.2 → l0_python-0.4.3}/tests/test_sparse.py +0 -0
@@ -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},
@@ -0,0 +1,143 @@
1
+ # L0 Regularization
2
+
3
+ [![PyPI version](https://badge.fury.io/py/l0-python.svg)](https://pypi.org/project/l0-python/)
4
+ [![CI](https://github.com/PolicyEngine/L0/actions/workflows/push.yml/badge.svg)](https://github.com/PolicyEngine/L0/actions)
5
+
6
+ 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.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install l0-python
12
+ ```
13
+
14
+ For development:
15
+ ```bash
16
+ git clone https://github.com/PolicyEngine/L0.git
17
+ cd L0
18
+ pip install -e .[dev]
19
+ ```
20
+
21
+ ## Our Approach to Test-Time Gates
22
+
23
+ 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.
24
+
25
+ We include temperature at test time:
26
+ ```python
27
+ # Our approach: include temperature
28
+ z = sigmoid(log_alpha / beta) * (zeta - gamma) + gamma
29
+
30
+ # Alternative: omit temperature
31
+ z = sigmoid(log_alpha) * (zeta - gamma) + gamma
32
+ ```
33
+
34
+ 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.
35
+
36
+ ## Primary Use Case: Survey Calibration
37
+
38
+ This package was developed for PolicyEngine's survey calibration, where we select a sparse subset of survey households while matching population targets.
39
+
40
+ ```python
41
+ import numpy as np
42
+ from scipy import sparse as sp
43
+ from l0.calibration import SparseCalibrationWeights
44
+
45
+ # Setup: Q targets, N households
46
+ Q, N = 200, 10000
47
+ M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
48
+ y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
49
+
50
+ # Initialize model
51
+ model = SparseCalibrationWeights(
52
+ n_features=N,
53
+ beta=0.35,
54
+ gamma=-0.1,
55
+ zeta=1.1,
56
+ init_keep_prob=0.5,
57
+ init_weights=1.0,
58
+ log_weight_jitter_sd=0.05,
59
+ device="cuda",
60
+ )
61
+
62
+ # Train with L0+L2 regularization
63
+ model.fit(
64
+ M=M,
65
+ y=y,
66
+ lambda_l0=1e-6,
67
+ lambda_l2=1e-8,
68
+ lr=0.15,
69
+ epochs=2000,
70
+ loss_type="relative",
71
+ verbose=True,
72
+ )
73
+
74
+ # Get results
75
+ active = model.get_active_weights()
76
+ print(f"Selected {active['count']} of {N} households")
77
+ print(f"Sparsity: {model.get_sparsity():.1%}")
78
+ ```
79
+
80
+ ### Key Features
81
+
82
+ - **Non-negative weights**: Constrained via log-space parameterization
83
+ - **L0 sparsity**: Directly minimizes the count of active weights
84
+ - **Relative loss**: Scale-invariant for targets spanning orders of magnitude
85
+ - **Group-wise averaging**: Balance loss across target groups with different sizes
86
+ - **GPU support**: CUDA acceleration for large problems
87
+
88
+ ## Sparse Regression
89
+
90
+ For sparse linear regression with scipy sparse matrices:
91
+
92
+ ```python
93
+ from scipy import sparse as sp
94
+ from l0.sparse import SparseL0Linear
95
+
96
+ # Sparse design matrix
97
+ X = sp.random(1000, 500, density=0.1, format="csr")
98
+ y = np.random.randn(1000)
99
+
100
+ model = SparseL0Linear(n_features=500)
101
+ model.fit(X, y, lambda_l0=0.001, epochs=1000)
102
+
103
+ # Get sparse coefficients
104
+ coef = model.get_coefficients(threshold=0.01)
105
+ ```
106
+
107
+ ## Example: Variable Selection
108
+
109
+ The `examples/sparse_regression_demo.py` script demonstrates L0 regularization on a simple problem where the true coefficients are `[1, 0, -2, 0]`:
110
+
111
+ ```bash
112
+ python examples/sparse_regression_demo.py
113
+ ```
114
+
115
+ Output:
116
+ ```
117
+ True coefficients: [ 1. 0. -2. 0.]
118
+ Recovered coefficients: [ 1.039 0. -2.069 -0. ]
119
+ Gates: [1. 0. 1. 0.]
120
+ ```
121
+
122
+ The model correctly identifies that only variables 1 and 3 contribute to the outcome.
123
+
124
+ ## Testing
125
+
126
+ ```bash
127
+ pytest tests/ -v --cov=l0
128
+ ```
129
+
130
+ ## Citation
131
+
132
+ ```bibtex
133
+ @article{louizos2017learning,
134
+ title={Learning Sparse Neural Networks through L0 Regularization},
135
+ author={Louizos, Christos and Welling, Max and Kingma, Diederik P},
136
+ journal={arXiv preprint arXiv:1712.01312},
137
+ year={2017}
138
+ }
139
+ ```
140
+
141
+ ## License
142
+
143
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -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},
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "l0-python"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "L0 regularization for sparse neural networks and intelligent sampling"
9
9
  readme = "README.md"
10
10
  authors = [
l0_python-0.4.2/README.md DELETED
@@ -1,190 +0,0 @@
1
- # L0 Regularization
2
-
3
- [![PyPI version](https://badge.fury.io/py/l0-python.svg)](https://pypi.org/project/l0-python/)
4
- [![CI](https://github.com/PolicyEngine/L0/actions/workflows/push.yml/badge.svg)](https://github.com/PolicyEngine/L0/actions)
5
-
6
- 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.
7
-
8
- ## Why This Package?
9
-
10
- 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:
11
-
12
- ```python
13
- # Original (buggy): gates never fully close
14
- pi = sigmoid(log_alpha)
15
-
16
- # Corrected: temperature division required for proper sparsity
17
- pi = sigmoid(log_alpha / temperature)
18
- ```
19
-
20
- This fix enables gates to achieve true 0/1 values, producing exact sparsity as intended by the L0 formulation.
21
-
22
- ## Installation
23
-
24
- ```bash
25
- pip install l0-python
26
- ```
27
-
28
- For development:
29
- ```bash
30
- git clone https://github.com/PolicyEngine/L0.git
31
- cd L0
32
- pip install -e .[dev]
33
- ```
34
-
35
- ## Primary Use Case: Survey Calibration
36
-
37
- This package was developed for PolicyEngine's survey calibration system, where we select a sparse subset of survey households while matching population targets.
38
-
39
- ```python
40
- import numpy as np
41
- from scipy import sparse as sp
42
- from l0.calibration import SparseCalibrationWeights
43
-
44
- # Setup: Q targets, N households
45
- Q, N = 200, 10000
46
- M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
47
- y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
48
-
49
- # Initialize model
50
- model = SparseCalibrationWeights(
51
- n_features=N,
52
- beta=0.35,
53
- gamma=-0.1,
54
- zeta=1.1,
55
- init_keep_prob=0.5, # Start with 50% active probability
56
- init_weights=1.0, # Or pass array of initial weights
57
- log_weight_jitter_sd=0.05,
58
- device="cuda", # GPU acceleration
59
- )
60
-
61
- # Train with L0+L2 regularization
62
- model.fit(
63
- M=M,
64
- y=y,
65
- lambda_l0=1e-6, # Controls sparsity level
66
- lambda_l2=1e-8, # Prevents weight explosion
67
- lr=0.15,
68
- epochs=2000,
69
- loss_type="relative", # Scale-invariant loss
70
- verbose=True,
71
- verbose_freq=100,
72
- )
73
-
74
- # Get results
75
- active = model.get_active_weights()
76
- print(f"Selected {active['count']} of {N} households ({100*active['count']/N:.1f}%)")
77
- print(f"Sparsity: {model.get_sparsity():.1%}")
78
-
79
- # Predict calibrated totals
80
- y_pred = model.predict(M)
81
- ```
82
-
83
- ### Key Features for Calibration
84
-
85
- - **Non-negative weights**: All weights constrained to be positive via log-space parameterization
86
- - **Sparse solutions**: L0 penalty directly minimizes the count of active weights
87
- - **Relative loss**: Scale-invariant loss for targets spanning orders of magnitude
88
- - **Group-wise averaging**: Balance loss contributions across target groups with different cardinalities
89
- - **GPU support**: CUDA acceleration for large-scale problems
90
-
91
- ## Neural Network Sparsification
92
-
93
- The package also supports traditional neural network pruning:
94
-
95
- ```python
96
- import torch
97
- from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
98
-
99
- class SparseModel(torch.nn.Module):
100
- def __init__(self):
101
- super().__init__()
102
- self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
103
- self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
104
-
105
- def forward(self, x):
106
- x = torch.relu(self.fc1(x))
107
- return self.fc2(x)
108
-
109
- model = SparseModel()
110
- optimizer = torch.optim.Adam(model.parameters())
111
- scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
112
-
113
- for epoch in range(100):
114
- temp = scheduler.get_temperature(epoch)
115
- update_temperatures(model, temp)
116
-
117
- output = model(input_data)
118
- ce_loss = criterion(output, target)
119
- penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
120
- loss = ce_loss + penalty
121
-
122
- optimizer.zero_grad()
123
- loss.backward()
124
- optimizer.step()
125
- ```
126
-
127
- ### Available Layers
128
-
129
- - `L0Linear`: Fully connected layer with L0 gates on weights
130
- - `L0Conv2d`: 2D convolution with channel-wise L0 gates
131
- - `L0DepthwiseConv2d`: Depthwise convolution with L0 gates
132
- - `SparseMLP`: Multi-layer perceptron with built-in L0 regularization
133
-
134
- ## Intelligent Sampling Gates
135
-
136
- Standalone gates for sample/feature selection:
137
-
138
- ```python
139
- from l0 import SampleGate, FeatureGate, HybridGate
140
-
141
- # Select samples via learned gates
142
- gate = SampleGate(n_samples=10000, target_samples=1000)
143
- selected_data, indices = gate.select_samples(data)
144
-
145
- # Select features
146
- gate = FeatureGate(n_features=1000, max_features=50)
147
- selected_data, indices = gate.select_features(data)
148
-
149
- # Hybrid: combine L0 selection with random sampling
150
- hybrid = HybridGate(
151
- n_items=10000,
152
- l0_fraction=0.25, # 25% via learned L0 gates
153
- random_fraction=0.75 # 75% random for coverage
154
- )
155
- selected, indices, types = hybrid.select(data)
156
- ```
157
-
158
- ## How L0 Regularization Works
159
-
160
- Unlike post-hoc pruning (setting small weights to zero), L0 regularization integrates sparsity into the training objective:
161
-
162
- 1. **Stochastic gates**: Each weight has a learned gate parameter controlling activation probability
163
- 2. **Hard Concrete distribution**: Enables differentiable 0/1 gate values during training
164
- 3. **Expected L0 penalty**: Minimizes the expected number of active gates
165
- 4. **Temperature annealing**: Gradually sharpens gates from soft to hard decisions
166
-
167
- The result: the network learns *which* weights should be zero as part of optimization, not as a post-processing step.
168
-
169
- ## Testing
170
-
171
- ```bash
172
- pytest tests/ -v --cov=l0
173
- ```
174
-
175
- ## Citation
176
-
177
- If you use this package, please cite the original paper:
178
-
179
- ```bibtex
180
- @article{louizos2017learning,
181
- title={Learning Sparse Neural Networks through L0 Regularization},
182
- author={Louizos, Christos and Welling, Max and Kingma, Diederik P},
183
- journal={arXiv preprint arXiv:1712.01312},
184
- year={2017}
185
- }
186
- ```
187
-
188
- ## License
189
-
190
- MIT License - see [LICENSE](LICENSE) for details.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes