l0-python 0.4.2__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- l0/__init__.py +1 -1
- l0/calibration.py +50 -2
- {l0_python-0.4.2.dist-info → l0_python-0.5.0.dist-info}/METADATA +54 -101
- l0_python-0.5.0.dist-info/RECORD +12 -0
- {l0_python-0.4.2.dist-info → l0_python-0.5.0.dist-info}/WHEEL +1 -1
- l0_python-0.4.2.dist-info/RECORD +0 -12
- {l0_python-0.4.2.dist-info → l0_python-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {l0_python-0.4.2.dist-info → l0_python-0.5.0.dist-info}/top_level.txt +0 -0
l0/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ A PyTorch implementation of L0 regularization for neural network sparsification
|
|
|
5
5
|
and intelligent sampling, based on Louizos, Welling, & Kingma (2017).
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.5.0"
|
|
9
9
|
|
|
10
10
|
from .distributions import HardConcrete
|
|
11
11
|
from .gates import FeatureGate, HybridGate, L0Gate, SampleGate
|
l0/calibration.py
CHANGED
|
@@ -60,6 +60,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
60
60
|
log_weight_jitter_sd: float = 0.0,
|
|
61
61
|
log_alpha_jitter_sd: float = 0.01,
|
|
62
62
|
device: str | torch.device = "cpu",
|
|
63
|
+
use_gates: bool = True,
|
|
63
64
|
):
|
|
64
65
|
super().__init__()
|
|
65
66
|
self.n_features = n_features
|
|
@@ -69,6 +70,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
69
70
|
self.log_weight_jitter_sd = log_weight_jitter_sd
|
|
70
71
|
self.log_alpha_jitter_sd = log_alpha_jitter_sd
|
|
71
72
|
self.device = torch.device(device)
|
|
73
|
+
self.use_gates = use_gates
|
|
72
74
|
|
|
73
75
|
# Initialize weights (on original scale)
|
|
74
76
|
if init_weights is None:
|
|
@@ -193,6 +195,10 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
193
195
|
torch.Tensor
|
|
194
196
|
Positive calibration weights with L0 sparsity applied
|
|
195
197
|
"""
|
|
198
|
+
if not self.use_gates:
|
|
199
|
+
# No gates - just return positive weights
|
|
200
|
+
return torch.exp(self.log_weight)
|
|
201
|
+
|
|
196
202
|
# Sample or get deterministic gates
|
|
197
203
|
if deterministic:
|
|
198
204
|
gates = self.get_deterministic_gates()
|
|
@@ -453,6 +459,46 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
453
459
|
1 - active_info["count"] / self.n_features
|
|
454
460
|
)
|
|
455
461
|
|
|
462
|
+
# Weight distribution diagnostic
|
|
463
|
+
if active_weights.numel() > 0:
|
|
464
|
+
w_tiny = (active_weights < 0.01).sum().item()
|
|
465
|
+
w_small = (
|
|
466
|
+
((active_weights >= 0.01) & (active_weights < 0.1))
|
|
467
|
+
.sum()
|
|
468
|
+
.item()
|
|
469
|
+
)
|
|
470
|
+
w_med = (
|
|
471
|
+
((active_weights >= 0.1) & (active_weights < 1.0))
|
|
472
|
+
.sum()
|
|
473
|
+
.item()
|
|
474
|
+
)
|
|
475
|
+
w_normal = (
|
|
476
|
+
((active_weights >= 1.0) & (active_weights < 10.0))
|
|
477
|
+
.sum()
|
|
478
|
+
.item()
|
|
479
|
+
)
|
|
480
|
+
w_large = (
|
|
481
|
+
(
|
|
482
|
+
(active_weights >= 10.0)
|
|
483
|
+
& (active_weights < 1000.0)
|
|
484
|
+
)
|
|
485
|
+
.sum()
|
|
486
|
+
.item()
|
|
487
|
+
)
|
|
488
|
+
w_huge = (active_weights >= 1000.0).sum().item()
|
|
489
|
+
|
|
490
|
+
total_active = active_weights.numel()
|
|
491
|
+
weight_dist = (
|
|
492
|
+
f"[<0.01: {100*w_tiny/total_active:.1f}%, "
|
|
493
|
+
f"0.01-0.1: {100*w_small/total_active:.1f}%, "
|
|
494
|
+
f"0.1-1: {100*w_med/total_active:.1f}%, "
|
|
495
|
+
f"1-10: {100*w_normal/total_active:.1f}%, "
|
|
496
|
+
f"10-1000: {100*w_large/total_active:.1f}%, "
|
|
497
|
+
f">1000: {100*w_huge/total_active:.1f}%]"
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
weight_dist = "[no active weights]"
|
|
501
|
+
|
|
456
502
|
# Calculate components of the actual loss being minimized
|
|
457
503
|
actual_data_loss = data_loss.item()
|
|
458
504
|
actual_l0_loss = l0_loss.item()
|
|
@@ -464,7 +510,8 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
464
510
|
f"mean_group_mare={mean_group_mare:.4%}, "
|
|
465
511
|
f"max_error={max_rel_err:.1%}, "
|
|
466
512
|
f"total_loss={actual_total_loss:.3f}, "
|
|
467
|
-
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
|
|
513
|
+
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)\n"
|
|
514
|
+
f" Weight dist: {weight_dist}"
|
|
468
515
|
)
|
|
469
516
|
else:
|
|
470
517
|
print(
|
|
@@ -472,7 +519,8 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
472
519
|
f"mean_error={mean_rel_err:.4%}, "
|
|
473
520
|
f"max_error={max_rel_err:.1%}, "
|
|
474
521
|
f"total_loss={actual_total_loss:.3f}, "
|
|
475
|
-
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
|
|
522
|
+
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)\n"
|
|
523
|
+
f" Weight dist: {weight_dist}"
|
|
476
524
|
)
|
|
477
525
|
|
|
478
526
|
return self
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: l0-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
[](https://pypi.org/project/l0-python/)
|
|
69
69
|
[](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),
|
|
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
|
|
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,
|
|
121
|
-
init_weights=1.0,
|
|
121
|
+
init_keep_prob=0.5,
|
|
122
|
+
init_weights=1.0,
|
|
122
123
|
log_weight_jitter_sd=0.05,
|
|
123
|
-
device="cuda",
|
|
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,
|
|
131
|
-
lambda_l2=1e-8,
|
|
131
|
+
lambda_l0=1e-6,
|
|
132
|
+
lambda_l2=1e-8,
|
|
132
133
|
lr=0.15,
|
|
133
134
|
epochs=2000,
|
|
134
|
-
loss_type="relative",
|
|
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
|
|
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
|
|
145
|
+
### Key Features
|
|
149
146
|
|
|
150
|
-
- **Non-negative weights**:
|
|
151
|
-
- **
|
|
152
|
-
- **Relative loss**: Scale-invariant
|
|
153
|
-
- **Group-wise averaging**: Balance loss
|
|
154
|
-
- **GPU support**: CUDA acceleration for large
|
|
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
|
-
##
|
|
153
|
+
## Sparse Regression
|
|
157
154
|
|
|
158
|
-
|
|
155
|
+
For sparse linear regression with scipy sparse matrices:
|
|
159
156
|
|
|
160
157
|
```python
|
|
161
|
-
import
|
|
162
|
-
from l0 import
|
|
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
|
-
|
|
161
|
+
# Sparse design matrix
|
|
162
|
+
X = sp.random(1000, 500, density=0.1, format="csr")
|
|
163
|
+
y = np.random.randn(1000)
|
|
200
164
|
|
|
201
|
-
|
|
165
|
+
model = SparseL0Linear(n_features=500)
|
|
166
|
+
model.fit(X, y, lambda_l0=0.001, epochs=1000)
|
|
202
167
|
|
|
203
|
-
|
|
204
|
-
|
|
168
|
+
# Get sparse coefficients
|
|
169
|
+
coef = model.get_coefficients(threshold=0.01)
|
|
170
|
+
```
|
|
205
171
|
|
|
206
|
-
|
|
207
|
-
gate = SampleGate(n_samples=10000, target_samples=1000)
|
|
208
|
-
selected_data, indices = gate.select_samples(data)
|
|
172
|
+
## Example: Variable Selection
|
|
209
173
|
|
|
210
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
1.
|
|
228
|
-
|
|
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
|
|
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,12 @@
|
|
|
1
|
+
l0/__init__.py,sha256=ImRgZfVg-RJhLoqNaLc8ysjddZ7CYGHJPJbRUyfSoHY,1192
|
|
2
|
+
l0/calibration.py,sha256=1-C6B6O47l6qlEcQl3N8waAPAyj5PFlzBHl17KbXqGo,20018
|
|
3
|
+
l0/distributions.py,sha256=GvVoDasdxoDlPPdzt33lFzkuJgQsfkEn9e-abxru3nM,6325
|
|
4
|
+
l0/gates.py,sha256=54OORMg9qSsdbIyzYIL9t9CCHWsKbumSKl_d4-YFuNU,11443
|
|
5
|
+
l0/layers.py,sha256=j_DPEFdfUc37ojtoK2ufGzVYzZ_DwusqTcJXCVxZS68,13785
|
|
6
|
+
l0/penalties.py,sha256=okqXgX2eGUmg5prcDclicTQALPPPF9FA5eQP09V4GTE,9569
|
|
7
|
+
l0/sparse.py,sha256=UVpkrrJBnQ9iSNDoWK5F_sr43tQareWXzYHyMq_cKss,9726
|
|
8
|
+
l0_python-0.5.0.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
|
|
9
|
+
l0_python-0.5.0.dist-info/METADATA,sha256=zoUDdaSbSAWeWtWW-K_5Wgl6RbTqGHfGIyfYobvxEBQ,7095
|
|
10
|
+
l0_python-0.5.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
11
|
+
l0_python-0.5.0.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
|
|
12
|
+
l0_python-0.5.0.dist-info/RECORD,,
|
l0_python-0.4.2.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
l0/__init__.py,sha256=ezGVLGbdU_jAElAJSdLKjSWCnZRILAZi8qMBPjcHwQc,1192
|
|
2
|
-
l0/calibration.py,sha256=3VM-28M1yp-DM6AlHX7sqZCaE7OJevpY9Ey-syjSPik,17838
|
|
3
|
-
l0/distributions.py,sha256=GvVoDasdxoDlPPdzt33lFzkuJgQsfkEn9e-abxru3nM,6325
|
|
4
|
-
l0/gates.py,sha256=54OORMg9qSsdbIyzYIL9t9CCHWsKbumSKl_d4-YFuNU,11443
|
|
5
|
-
l0/layers.py,sha256=j_DPEFdfUc37ojtoK2ufGzVYzZ_DwusqTcJXCVxZS68,13785
|
|
6
|
-
l0/penalties.py,sha256=okqXgX2eGUmg5prcDclicTQALPPPF9FA5eQP09V4GTE,9569
|
|
7
|
-
l0/sparse.py,sha256=UVpkrrJBnQ9iSNDoWK5F_sr43tQareWXzYHyMq_cKss,9726
|
|
8
|
-
l0_python-0.4.2.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
|
|
9
|
-
l0_python-0.4.2.dist-info/METADATA,sha256=czuRwTuF5iOn6f-rsFSxk_gHXWuKXAaYAmDugkGw4rA,9074
|
|
10
|
-
l0_python-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
-
l0_python-0.4.2.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
|
|
12
|
-
l0_python-0.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|