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.
- {l0_python-0.4.1/l0_python.egg-info → l0_python-0.4.3}/PKG-INFO +87 -95
- l0_python-0.4.3/README.md +143 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/calibration.py +4 -4
- {l0_python-0.4.1 → l0_python-0.4.3/l0_python.egg-info}/PKG-INFO +87 -95
- {l0_python-0.4.1 → l0_python-0.4.3}/pyproject.toml +1 -1
- {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_calibration.py +226 -0
- l0_python-0.4.1/README.md +0 -151
- {l0_python-0.4.1 → l0_python-0.4.3}/LICENSE +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/__init__.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/distributions.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/gates.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/layers.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/penalties.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0/sparse.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/SOURCES.txt +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/dependency_links.txt +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/requires.txt +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/l0_python.egg-info/top_level.txt +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/setup.cfg +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_distributions.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_gates.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_layers.py +0 -0
- {l0_python-0.4.1 → l0_python-0.4.3}/tests/test_penalties.py +0 -0
- {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.
|
|
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
|
-
|
|
68
|
+
[](https://pypi.org/project/l0-python/)
|
|
69
|
+
[](https://github.com/PolicyEngine/L0/actions)
|
|
69
70
|
|
|
70
|
-
|
|
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
|
-
##
|
|
86
|
+
## Our Approach to Test-Time Gates
|
|
93
87
|
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
|
158
|
+
from scipy import sparse as sp
|
|
159
|
+
from l0.sparse import SparseL0Linear
|
|
158
160
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
# Sparse design matrix
|
|
162
|
+
X = sp.random(1000, 500, density=0.1, format="csr")
|
|
163
|
+
y = np.random.randn(1000)
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
##
|
|
172
|
+
## Example: Variable Selection
|
|
168
173
|
|
|
169
|
-
|
|
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
|
-
```
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
208
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# L0 Regularization
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/l0-python/)
|
|
4
|
+
[](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
|
-
|
|
447
|
+
mean_group_mare = np.mean(group_losses)
|
|
448
448
|
else:
|
|
449
|
-
|
|
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"
|
|
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:.
|
|
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.
|
|
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
|
-
|
|
68
|
+
[](https://pypi.org/project/l0-python/)
|
|
69
|
+
[](https://github.com/PolicyEngine/L0/actions)
|
|
69
70
|
|
|
70
|
-
|
|
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
|
-
##
|
|
86
|
+
## Our Approach to Test-Time Gates
|
|
93
87
|
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
|
158
|
+
from scipy import sparse as sp
|
|
159
|
+
from l0.sparse import SparseL0Linear
|
|
158
160
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
# Sparse design matrix
|
|
162
|
+
X = sp.random(1000, 500, density=0.1, format="csr")
|
|
163
|
+
y = np.random.randn(1000)
|
|
162
164
|
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
##
|
|
172
|
+
## Example: Variable Selection
|
|
168
173
|
|
|
169
|
-
|
|
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
|
-
```
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
208
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|