l0-python 0.4.1__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.1/l0_python.egg-info → l0_python-0.4.3}/PKG-INFO +87 -95
  2. l0_python-0.4.3/README.md +143 -0
  3. {l0_python-0.4.1 → l0_python-0.4.3}/l0/calibration.py +4 -4
  4. {l0_python-0.4.1 → l0_python-0.4.3/l0_python.egg-info}/PKG-INFO +87 -95
  5. {l0_python-0.4.1 → l0_python-0.4.3}/pyproject.toml +1 -1
  6. {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_calibration.py +226 -0
  7. l0_python-0.4.1/README.md +0 -151
  8. {l0_python-0.4.1 → l0_python-0.4.3}/LICENSE +0 -0
  9. {l0_python-0.4.1 → l0_python-0.4.3}/l0/__init__.py +0 -0
  10. {l0_python-0.4.1 → l0_python-0.4.3}/l0/distributions.py +0 -0
  11. {l0_python-0.4.1 → l0_python-0.4.3}/l0/gates.py +0 -0
  12. {l0_python-0.4.1 → l0_python-0.4.3}/l0/layers.py +0 -0
  13. {l0_python-0.4.1 → l0_python-0.4.3}/l0/penalties.py +0 -0
  14. {l0_python-0.4.1 → l0_python-0.4.3}/l0/sparse.py +0 -0
  15. {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/SOURCES.txt +0 -0
  16. {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/dependency_links.txt +0 -0
  17. {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/requires.txt +0 -0
  18. {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/top_level.txt +0 -0
  19. {l0_python-0.4.1 → l0_python-0.4.3}/setup.cfg +0 -0
  20. {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_distributions.py +0 -0
  21. {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_gates.py +0 -0
  22. {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_layers.py +0 -0
  23. {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_penalties.py +0 -0
  24. {l0_python-0.4.1 → 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.1
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
@@ -65,21 +65,15 @@ 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
-
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
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.
78
72
 
79
73
  ## Installation
80
74
 
81
75
  ```bash
82
- pip install l0
76
+ pip install l0-python
83
77
  ```
84
78
 
85
79
  For development:
@@ -89,119 +83,117 @@ cd L0
89
83
  pip install -e .[dev]
90
84
  ```
91
85
 
92
- ## Quick Start
86
+ ## Our Approach to Test-Time Gates
93
87
 
94
- ### Neural Network Sparsification
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.
95
89
 
90
+ We include temperature at test time:
96
91
  ```python
97
- import torch
98
- from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
99
-
100
- # Create a sparse model
101
- class SparseModel(torch.nn.Module):
102
- def __init__(self):
103
- super().__init__()
104
- self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
105
- self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
106
-
107
- def forward(self, x):
108
- x = torch.relu(self.fc1(x))
109
- return self.fc2(x)
110
-
111
- model = SparseModel()
112
- optimizer = torch.optim.Adam(model.parameters())
113
- scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
114
-
115
- # Training loop
116
- for epoch in range(100):
117
- # Update temperature
118
- temp = scheduler.get_temperature(epoch)
119
- update_temperatures(model, temp)
120
-
121
- # Forward pass
122
- output = model(input_data)
123
- ce_loss = criterion(output, target)
124
-
125
- # Add L0L2 penalty
126
- penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
127
- loss = ce_loss + penalty
128
-
129
- # Backward pass
130
- optimizer.zero_grad()
131
- loss.backward()
132
- optimizer.step()
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
133
97
  ```
134
98
 
135
- ### Intelligent Sample Selection
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
+
101
+ ## Primary Use Case: Survey Calibration
102
+
103
+ This package was developed for PolicyEngine's survey calibration, where we select a sparse subset of survey households while matching population targets.
136
104
 
137
105
  ```python
138
- from l0 import SampleGate, HybridGate
139
-
140
- # Pure L0 selection
141
- gate = SampleGate(n_samples=10000, target_samples=1000)
142
- selected_data, indices = gate.select_samples(data)
143
-
144
- # Hybrid selection (25% L0, 75% random)
145
- hybrid = HybridGate(
146
- n_items=10000,
147
- l0_fraction=0.25,
148
- random_fraction=0.75,
149
- target_items=1000
106
+ import numpy as np
107
+ from scipy import sparse as sp
108
+ from l0.calibration import SparseCalibrationWeights
109
+
110
+ # Setup: Q targets, N households
111
+ Q, N = 200, 10000
112
+ M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
113
+ y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
114
+
115
+ # Initialize model
116
+ model = SparseCalibrationWeights(
117
+ n_features=N,
118
+ beta=0.35,
119
+ gamma=-0.1,
120
+ zeta=1.1,
121
+ init_keep_prob=0.5,
122
+ init_weights=1.0,
123
+ log_weight_jitter_sd=0.05,
124
+ device="cuda",
125
+ )
126
+
127
+ # Train with L0+L2 regularization
128
+ model.fit(
129
+ M=M,
130
+ y=y,
131
+ lambda_l0=1e-6,
132
+ lambda_l2=1e-8,
133
+ lr=0.15,
134
+ epochs=2000,
135
+ loss_type="relative",
136
+ verbose=True,
150
137
  )
151
- selected, indices, types = hybrid.select(data)
138
+
139
+ # Get results
140
+ active = model.get_active_weights()
141
+ print(f"Selected {active['count']} of {N} households")
142
+ print(f"Sparsity: {model.get_sparsity():.1%}")
152
143
  ```
153
144
 
154
- ### Feature Selection
145
+ ### Key Features
146
+
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
152
+
153
+ ## Sparse Regression
154
+
155
+ For sparse linear regression with scipy sparse matrices:
155
156
 
156
157
  ```python
157
- from l0 import FeatureGate
158
+ from scipy import sparse as sp
159
+ from l0.sparse import SparseL0Linear
158
160
 
159
- # Select top features
160
- gate = FeatureGate(n_features=1000, max_features=50)
161
- selected_data, feature_indices = gate.select_features(data)
161
+ # Sparse design matrix
162
+ X = sp.random(1000, 500, density=0.1, format="csr")
163
+ y = np.random.randn(1000)
162
164
 
163
- # Get feature importance
164
- importance = gate.get_feature_importance()
165
+ model = SparseL0Linear(n_features=500)
166
+ model.fit(X, y, lambda_l0=0.001, epochs=1000)
167
+
168
+ # Get sparse coefficients
169
+ coef = model.get_coefficients(threshold=0.01)
165
170
  ```
166
171
 
167
- ## Integration with PolicyEngine
172
+ ## Example: Variable Selection
168
173
 
169
- This package is designed to work with PolicyEngine's calibration system:
174
+ The `examples/sparse_regression_demo.py` script demonstrates L0 regularization on a simple problem where the true coefficients are `[1, 0, -2, 0]`:
170
175
 
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
- )
181
-
182
- # Apply gates during reweighting
183
- masked_weights = weights * gates()
176
+ ```bash
177
+ python examples/sparse_regression_demo.py
184
178
  ```
185
179
 
186
- ## Documentation
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
+ ```
187
186
 
188
- Full documentation available at: https://policyengine.github.io/L0/
187
+ The model correctly identifies that only variables 1 and 3 contribute to the outcome.
189
188
 
190
189
  ## Testing
191
190
 
192
- Run tests with:
193
191
  ```bash
194
192
  pytest tests/ -v --cov=l0
195
193
  ```
196
194
 
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
195
  ## Citation
202
196
 
203
- If you use this package, please cite:
204
-
205
197
  ```bibtex
206
198
  @article{louizos2017learning,
207
199
  title={Learning Sparse Neural Networks through L0 Regularization},
@@ -213,4 +205,4 @@ If you use this package, please cite:
213
205
 
214
206
  ## License
215
207
 
216
- MIT License - see [LICENSE](LICENSE) file for details.
208
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -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.
@@ -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.1
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
@@ -65,21 +65,15 @@ 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
-
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
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.
78
72
 
79
73
  ## Installation
80
74
 
81
75
  ```bash
82
- pip install l0
76
+ pip install l0-python
83
77
  ```
84
78
 
85
79
  For development:
@@ -89,119 +83,117 @@ cd L0
89
83
  pip install -e .[dev]
90
84
  ```
91
85
 
92
- ## Quick Start
86
+ ## Our Approach to Test-Time Gates
93
87
 
94
- ### Neural Network Sparsification
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.
95
89
 
90
+ We include temperature at test time:
96
91
  ```python
97
- import torch
98
- from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
99
-
100
- # Create a sparse model
101
- class SparseModel(torch.nn.Module):
102
- def __init__(self):
103
- super().__init__()
104
- self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
105
- self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
106
-
107
- def forward(self, x):
108
- x = torch.relu(self.fc1(x))
109
- return self.fc2(x)
110
-
111
- model = SparseModel()
112
- optimizer = torch.optim.Adam(model.parameters())
113
- scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
114
-
115
- # Training loop
116
- for epoch in range(100):
117
- # Update temperature
118
- temp = scheduler.get_temperature(epoch)
119
- update_temperatures(model, temp)
120
-
121
- # Forward pass
122
- output = model(input_data)
123
- ce_loss = criterion(output, target)
124
-
125
- # Add L0L2 penalty
126
- penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
127
- loss = ce_loss + penalty
128
-
129
- # Backward pass
130
- optimizer.zero_grad()
131
- loss.backward()
132
- optimizer.step()
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
133
97
  ```
134
98
 
135
- ### Intelligent Sample Selection
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
+
101
+ ## Primary Use Case: Survey Calibration
102
+
103
+ This package was developed for PolicyEngine's survey calibration, where we select a sparse subset of survey households while matching population targets.
136
104
 
137
105
  ```python
138
- from l0 import SampleGate, HybridGate
139
-
140
- # Pure L0 selection
141
- gate = SampleGate(n_samples=10000, target_samples=1000)
142
- selected_data, indices = gate.select_samples(data)
143
-
144
- # Hybrid selection (25% L0, 75% random)
145
- hybrid = HybridGate(
146
- n_items=10000,
147
- l0_fraction=0.25,
148
- random_fraction=0.75,
149
- target_items=1000
106
+ import numpy as np
107
+ from scipy import sparse as sp
108
+ from l0.calibration import SparseCalibrationWeights
109
+
110
+ # Setup: Q targets, N households
111
+ Q, N = 200, 10000
112
+ M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
113
+ y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
114
+
115
+ # Initialize model
116
+ model = SparseCalibrationWeights(
117
+ n_features=N,
118
+ beta=0.35,
119
+ gamma=-0.1,
120
+ zeta=1.1,
121
+ init_keep_prob=0.5,
122
+ init_weights=1.0,
123
+ log_weight_jitter_sd=0.05,
124
+ device="cuda",
125
+ )
126
+
127
+ # Train with L0+L2 regularization
128
+ model.fit(
129
+ M=M,
130
+ y=y,
131
+ lambda_l0=1e-6,
132
+ lambda_l2=1e-8,
133
+ lr=0.15,
134
+ epochs=2000,
135
+ loss_type="relative",
136
+ verbose=True,
150
137
  )
151
- selected, indices, types = hybrid.select(data)
138
+
139
+ # Get results
140
+ active = model.get_active_weights()
141
+ print(f"Selected {active['count']} of {N} households")
142
+ print(f"Sparsity: {model.get_sparsity():.1%}")
152
143
  ```
153
144
 
154
- ### Feature Selection
145
+ ### Key Features
146
+
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
152
+
153
+ ## Sparse Regression
154
+
155
+ For sparse linear regression with scipy sparse matrices:
155
156
 
156
157
  ```python
157
- from l0 import FeatureGate
158
+ from scipy import sparse as sp
159
+ from l0.sparse import SparseL0Linear
158
160
 
159
- # Select top features
160
- gate = FeatureGate(n_features=1000, max_features=50)
161
- selected_data, feature_indices = gate.select_features(data)
161
+ # Sparse design matrix
162
+ X = sp.random(1000, 500, density=0.1, format="csr")
163
+ y = np.random.randn(1000)
162
164
 
163
- # Get feature importance
164
- importance = gate.get_feature_importance()
165
+ model = SparseL0Linear(n_features=500)
166
+ model.fit(X, y, lambda_l0=0.001, epochs=1000)
167
+
168
+ # Get sparse coefficients
169
+ coef = model.get_coefficients(threshold=0.01)
165
170
  ```
166
171
 
167
- ## Integration with PolicyEngine
172
+ ## Example: Variable Selection
168
173
 
169
- This package is designed to work with PolicyEngine's calibration system:
174
+ The `examples/sparse_regression_demo.py` script demonstrates L0 regularization on a simple problem where the true coefficients are `[1, 0, -2, 0]`:
170
175
 
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
- )
181
-
182
- # Apply gates during reweighting
183
- masked_weights = weights * gates()
176
+ ```bash
177
+ python examples/sparse_regression_demo.py
184
178
  ```
185
179
 
186
- ## Documentation
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
+ ```
187
186
 
188
- Full documentation available at: https://policyengine.github.io/L0/
187
+ The model correctly identifies that only variables 1 and 3 contribute to the outcome.
189
188
 
190
189
  ## Testing
191
190
 
192
- Run tests with:
193
191
  ```bash
194
192
  pytest tests/ -v --cov=l0
195
193
  ```
196
194
 
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
195
  ## Citation
202
196
 
203
- If you use this package, please cite:
204
-
205
197
  ```bibtex
206
198
  @article{louizos2017learning,
207
199
  title={Learning Sparse Neural Networks through L0 Regularization},
@@ -213,4 +205,4 @@ If you use this package, please cite:
213
205
 
214
206
  ## License
215
207
 
216
- MIT License - see [LICENSE](LICENSE) file for details.
208
+ MIT License - see [LICENSE](LICENSE) for details.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "l0-python"
7
- version = "0.4.1"
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 = [
@@ -247,6 +247,232 @@ class TestSparseCalibrationWeights:
247
247
  weights_with_l2.max() <= weights_no_l2.max() * 2.0
248
248
  ), "L2 should prevent extreme weights"
249
249
 
250
+ def test_pure_l2_penalty(self):
251
+ """Test pure L2 regularization without L0."""
252
+ N = 50
253
+ Q = 30
254
+
255
+ np.random.seed(42)
256
+ torch.manual_seed(42)
257
+
258
+ # Create underdetermined problem where L2 helps regularize
259
+ M = sp.random(Q, N, density=0.5, format="csr")
260
+ M.data = np.abs(M.data) * 2 # Scale up to create larger weights
261
+ y = np.ones(Q) * 10 # Simple target
262
+
263
+ # Train without any regularization
264
+ model_no_reg = SparseCalibrationWeights(
265
+ n_features=N,
266
+ init_weights=2.0, # Start with larger weights
267
+ init_keep_prob=0.999, # Keep all weights active
268
+ )
269
+ model_no_reg.fit(
270
+ M,
271
+ y,
272
+ lambda_l0=0.0, # No L0
273
+ lambda_l2=0.0, # No L2
274
+ epochs=1000,
275
+ lr=0.02,
276
+ verbose=False,
277
+ )
278
+
279
+ # Train with strong L2 regularization
280
+ model_l2_only = SparseCalibrationWeights(
281
+ n_features=N,
282
+ init_weights=2.0, # Same starting point
283
+ init_keep_prob=0.999, # Keep all weights active
284
+ )
285
+ model_l2_only.fit(
286
+ M,
287
+ y,
288
+ lambda_l0=0.0, # No L0
289
+ lambda_l2=10.0, # Very strong L2 to see clear effect
290
+ epochs=1000,
291
+ lr=0.02,
292
+ verbose=False,
293
+ )
294
+
295
+ with torch.no_grad():
296
+ weights_no_reg = model_no_reg.get_weights(deterministic=True)
297
+ weights_l2 = model_l2_only.get_weights(deterministic=True)
298
+
299
+ # Get non-zero weights
300
+ active_no_reg = weights_no_reg[weights_no_reg > 1e-8]
301
+ active_l2 = weights_l2[weights_l2 > 1e-8]
302
+
303
+ # L2 should keep most weights active (no sparsity from L2)
304
+ assert (
305
+ len(active_l2) >= N * 0.9
306
+ ), f"Pure L2 should not induce sparsity, got {len(active_l2)}/{N}"
307
+
308
+ # L2 norm should be smaller with regularization
309
+ l2_norm_no_reg = (weights_no_reg**2).sum().sqrt()
310
+ l2_norm_with_l2 = (weights_l2**2).sum().sqrt()
311
+ assert (
312
+ l2_norm_with_l2 < l2_norm_no_reg
313
+ ), f"L2 regularization should reduce L2 norm: {l2_norm_with_l2:.2f} vs {l2_norm_no_reg:.2f}"
314
+
315
+ # Check coefficient of variation (CV = std/mean) is lower with L2
316
+ # This captures that L2 shrinks weights toward each other
317
+ if active_no_reg.mean() > 1e-6 and active_l2.mean() > 1e-6:
318
+ cv_no_reg = active_no_reg.std() / active_no_reg.mean()
319
+ cv_l2 = active_l2.std() / active_l2.mean()
320
+ assert (
321
+ cv_l2 < cv_no_reg * 1.2
322
+ ), f"L2 should reduce relative variation: CV {cv_l2:.2f} vs {cv_no_reg:.2f}"
323
+
324
+ # L2 should prevent extreme weights
325
+ assert (
326
+ weights_l2.max() < weights_no_reg.max() * 1.5
327
+ ), f"L2 should limit max weights: {weights_l2.max():.2f} vs {weights_no_reg.max():.2f}"
328
+
329
+ # Both should still fit reasonably well
330
+ y_pred_no_reg = model_no_reg.predict(M).cpu().numpy()
331
+ y_pred_l2 = model_l2_only.predict(M).cpu().numpy()
332
+
333
+ error_no_reg = np.abs((y - y_pred_no_reg) / y).mean()
334
+ error_l2 = np.abs((y - y_pred_l2) / y).mean()
335
+
336
+ # L2 model may have slightly worse fit due to regularization
337
+ assert (
338
+ error_l2 < 0.5
339
+ ), f"L2 model should still fit reasonably: {error_l2:.3f}"
340
+ # But the trade-off is worth it for regularization
341
+
342
+ def test_l0_l2_combination(self):
343
+ """Test that combining L0 and L2 gives both sparsity and regularization."""
344
+ N = 100 # features
345
+ Q = 50 # targets
346
+
347
+ np.random.seed(42)
348
+ torch.manual_seed(42)
349
+
350
+ # Create problem with potential for overfitting
351
+ M = sp.random(Q, N, density=0.4, format="csr")
352
+ M.data = np.abs(M.data) * 3
353
+ y = np.random.uniform(5, 20, size=Q)
354
+
355
+ # Model 1: Only L0 (sparsity without weight regularization)
356
+ model_l0_only = SparseCalibrationWeights(
357
+ n_features=N,
358
+ init_weights=2.0,
359
+ init_keep_prob=0.5, # Start with 50% probability
360
+ )
361
+ model_l0_only.fit(
362
+ M,
363
+ y,
364
+ lambda_l0=0.01, # Stronger L0 for sparsity
365
+ lambda_l2=0.0, # No L2
366
+ epochs=2000,
367
+ lr=0.02,
368
+ verbose=False,
369
+ )
370
+
371
+ # Model 2: Only L2 (weight regularization without sparsity)
372
+ model_l2_only = SparseCalibrationWeights(
373
+ n_features=N,
374
+ init_weights=2.0,
375
+ init_keep_prob=0.999, # Keep all weights active
376
+ )
377
+ model_l2_only.fit(
378
+ M,
379
+ y,
380
+ lambda_l0=0.0, # No L0
381
+ lambda_l2=0.1, # Moderate L2
382
+ epochs=1500,
383
+ lr=0.02,
384
+ verbose=False,
385
+ )
386
+
387
+ # Model 3: Combined L0+L2 (both sparsity and weight regularization)
388
+ model_l0_l2 = SparseCalibrationWeights(
389
+ n_features=N,
390
+ init_weights=2.0,
391
+ init_keep_prob=0.5, # Same starting point as L0-only
392
+ )
393
+ model_l0_l2.fit(
394
+ M,
395
+ y,
396
+ lambda_l0=0.01, # Same L0 as model 1
397
+ lambda_l2=0.1, # Add L2 regularization
398
+ epochs=2000,
399
+ lr=0.02,
400
+ verbose=False,
401
+ )
402
+
403
+ with torch.no_grad():
404
+ # Get weights and stats for all models
405
+ weights_l0_only = model_l0_only.get_weights(deterministic=True)
406
+ weights_l2_only = model_l2_only.get_weights(deterministic=True)
407
+ weights_l0_l2 = model_l0_l2.get_weights(deterministic=True)
408
+
409
+ # Count active weights
410
+ active_l0_only = (weights_l0_only > 1e-6).sum().item()
411
+ active_l2_only = (weights_l2_only > 1e-6).sum().item()
412
+ active_l0_l2 = (weights_l0_l2 > 1e-6).sum().item()
413
+
414
+ # L0-only should have sparsity
415
+ assert (
416
+ active_l0_only < N * 0.8
417
+ ), f"L0 should induce sparsity: {active_l0_only}/{N} active"
418
+
419
+ # L2-only should have no/little sparsity
420
+ assert (
421
+ active_l2_only > N * 0.9
422
+ ), f"L2 alone shouldn't induce sparsity: {active_l2_only}/{N} active"
423
+
424
+ # L0+L2 should have sparsity (from L0)
425
+ assert (
426
+ active_l0_l2 < N * 0.8
427
+ ), f"L0+L2 should have sparsity: {active_l0_l2}/{N} active"
428
+
429
+ # Among active weights, L0+L2 should have smaller magnitudes than L0-only (from L2)
430
+ active_mask_l0_only = weights_l0_only > 1e-6
431
+ active_mask_l0_l2 = weights_l0_l2 > 1e-6
432
+
433
+ if active_mask_l0_only.any() and active_mask_l0_l2.any():
434
+ # Compare L2 norms of active weights
435
+ l2_norm_l0_only = (
436
+ (weights_l0_only[active_mask_l0_only] ** 2).sum().sqrt()
437
+ )
438
+ l2_norm_l0_l2 = (
439
+ (weights_l0_l2[active_mask_l0_l2] ** 2).sum().sqrt()
440
+ )
441
+
442
+ # L0+L2 should have smaller weight norms than L0-only
443
+ # (L2 regularization effect on top of sparsity)
444
+ assert (
445
+ l2_norm_l0_l2 < l2_norm_l0_only * 1.2
446
+ ), f"L0+L2 should have controlled weights: {l2_norm_l0_l2:.2f} vs L0-only {l2_norm_l0_only:.2f}"
447
+
448
+ # Check prediction quality for all models
449
+ y_pred_l0_only = model_l0_only.predict(M).cpu().numpy()
450
+ y_pred_l2_only = model_l2_only.predict(M).cpu().numpy()
451
+ y_pred_l0_l2 = model_l0_l2.predict(M).cpu().numpy()
452
+
453
+ error_l0_only = np.abs((y - y_pred_l0_only) / (y + 1)).mean()
454
+ error_l2_only = np.abs((y - y_pred_l2_only) / (y + 1)).mean()
455
+ error_l0_l2 = np.abs((y - y_pred_l0_l2) / (y + 1)).mean()
456
+
457
+ # All should fit reasonably well (L2-only may have slightly worse fit due to regularization)
458
+ assert (
459
+ error_l0_only < 0.35
460
+ ), f"L0-only should fit well: {error_l0_only:.3f}"
461
+ assert (
462
+ error_l2_only < 0.35
463
+ ), f"L2-only should fit well: {error_l2_only:.3f}"
464
+ assert (
465
+ error_l0_l2 < 0.35
466
+ ), f"L0+L2 should fit well: {error_l0_l2:.3f}"
467
+
468
+ print(
469
+ f"\nL0-only: {active_l0_only}/{N} active, error={error_l0_only:.3f}"
470
+ )
471
+ print(
472
+ f"L2-only: {active_l2_only}/{N} active, error={error_l2_only:.3f}"
473
+ )
474
+ print(f"L0+L2: {active_l0_l2}/{N} active, error={error_l0_l2:.3f}")
475
+
250
476
  def test_group_wise_averaging(self):
251
477
  """Test that group-wise averaging balances loss contributions."""
252
478
  N = 100 # features (households)
l0_python-0.4.1/README.md DELETED
@@ -1,151 +0,0 @@
1
- # L0 Regularization
2
-
3
- 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).
4
-
5
- ## Features
6
-
7
- - **Hard Concrete Distribution**: Differentiable approximation of L0 norm
8
- - **Sparse Neural Network Layers**: L0Linear, L0Conv2d with automatic pruning
9
- - **Intelligent Sampling**: Sample/feature selection gates for calibration
10
- - **L0L2 Combined Penalty**: Recommended approach to prevent overfitting
11
- - **Temperature Scheduling**: Annealing for improved convergence
12
- - **TDD Development**: Comprehensive test coverage
13
-
14
- ## Installation
15
-
16
- ```bash
17
- pip install l0
18
- ```
19
-
20
- For development:
21
- ```bash
22
- git clone https://github.com/PolicyEngine/L0.git
23
- cd L0
24
- pip install -e .[dev]
25
- ```
26
-
27
- ## Quick Start
28
-
29
- ### Neural Network Sparsification
30
-
31
- ```python
32
- import torch
33
- from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
34
-
35
- # Create a sparse model
36
- class SparseModel(torch.nn.Module):
37
- def __init__(self):
38
- super().__init__()
39
- self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
40
- self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
41
-
42
- def forward(self, x):
43
- x = torch.relu(self.fc1(x))
44
- return self.fc2(x)
45
-
46
- model = SparseModel()
47
- optimizer = torch.optim.Adam(model.parameters())
48
- scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
49
-
50
- # Training loop
51
- for epoch in range(100):
52
- # Update temperature
53
- temp = scheduler.get_temperature(epoch)
54
- update_temperatures(model, temp)
55
-
56
- # Forward pass
57
- output = model(input_data)
58
- ce_loss = criterion(output, target)
59
-
60
- # Add L0L2 penalty
61
- penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
62
- loss = ce_loss + penalty
63
-
64
- # Backward pass
65
- optimizer.zero_grad()
66
- loss.backward()
67
- optimizer.step()
68
- ```
69
-
70
- ### Intelligent Sample Selection
71
-
72
- ```python
73
- from l0 import SampleGate, HybridGate
74
-
75
- # Pure L0 selection
76
- gate = SampleGate(n_samples=10000, target_samples=1000)
77
- selected_data, indices = gate.select_samples(data)
78
-
79
- # Hybrid selection (25% L0, 75% random)
80
- hybrid = HybridGate(
81
- n_items=10000,
82
- l0_fraction=0.25,
83
- random_fraction=0.75,
84
- target_items=1000
85
- )
86
- selected, indices, types = hybrid.select(data)
87
- ```
88
-
89
- ### Feature Selection
90
-
91
- ```python
92
- from l0 import FeatureGate
93
-
94
- # Select top features
95
- gate = FeatureGate(n_features=1000, max_features=50)
96
- selected_data, feature_indices = gate.select_features(data)
97
-
98
- # Get feature importance
99
- importance = gate.get_feature_importance()
100
- ```
101
-
102
- ## Integration with PolicyEngine
103
-
104
- This package is designed to work with PolicyEngine's calibration system:
105
-
106
- ```python
107
- # In policyengine-us-data or similar
108
- from l0 import HardConcrete
109
-
110
- # Use for household selection in CPS calibration
111
- gates = HardConcrete(
112
- len(household_weights),
113
- temperature=0.25,
114
- init_mean=0.999 # Start with most households
115
- )
116
-
117
- # Apply gates during reweighting
118
- masked_weights = weights * gates()
119
- ```
120
-
121
- ## Documentation
122
-
123
- Full documentation available at: https://policyengine.github.io/L0/
124
-
125
- ## Testing
126
-
127
- Run tests with:
128
- ```bash
129
- pytest tests/ -v --cov=l0
130
- ```
131
-
132
- ## Acknowledgments
133
-
134
- 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).
135
-
136
- ## Citation
137
-
138
- If you use this package, please cite:
139
-
140
- ```bibtex
141
- @article{louizos2017learning,
142
- title={Learning Sparse Neural Networks through L0 Regularization},
143
- author={Louizos, Christos and Welling, Max and Kingma, Diederik P},
144
- journal={arXiv preprint arXiv:1712.01312},
145
- year={2017}
146
- }
147
- ```
148
-
149
- ## License
150
-
151
- MIT License - see [LICENSE](LICENSE) file 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