l0-python 0.4.1__py3-none-any.whl → 0.4.2__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/calibration.py +4 -4
- {l0_python-0.4.1.dist-info → l0_python-0.4.2.dist-info}/METADATA +107 -68
- {l0_python-0.4.1.dist-info → l0_python-0.4.2.dist-info}/RECORD +6 -6
- {l0_python-0.4.1.dist-info → l0_python-0.4.2.dist-info}/WHEEL +0 -0
- {l0_python-0.4.1.dist-info → l0_python-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {l0_python-0.4.1.dist-info → l0_python-0.4.2.dist-info}/top_level.txt +0 -0
l0/calibration.py
CHANGED
|
@@ -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.2
|
|
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,29 @@ 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
|
+
A PyTorch implementation of L0 regularization based on [Louizos, Welling, & Kingma (2017)](https://arxiv.org/abs/1712.01312), with a **critical bug fix** to the original authors' implementation.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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.
|
|
78
86
|
|
|
79
87
|
## Installation
|
|
80
88
|
|
|
81
89
|
```bash
|
|
82
|
-
pip install l0
|
|
90
|
+
pip install l0-python
|
|
83
91
|
```
|
|
84
92
|
|
|
85
93
|
For development:
|
|
@@ -89,21 +97,76 @@ cd L0
|
|
|
89
97
|
pip install -e .[dev]
|
|
90
98
|
```
|
|
91
99
|
|
|
92
|
-
##
|
|
100
|
+
## Primary Use Case: Survey Calibration
|
|
101
|
+
|
|
102
|
+
This package was developed for PolicyEngine's survey calibration system, where we select a sparse subset of survey households while matching population targets.
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import numpy as np
|
|
106
|
+
from scipy import sparse as sp
|
|
107
|
+
from l0.calibration import SparseCalibrationWeights
|
|
108
|
+
|
|
109
|
+
# Setup: Q targets, N households
|
|
110
|
+
Q, N = 200, 10000
|
|
111
|
+
M = sp.random(Q, N, density=0.3, format="csr") # Household characteristics
|
|
112
|
+
y = np.random.uniform(1e6, 1e8, size=Q) # Population targets
|
|
113
|
+
|
|
114
|
+
# Initialize model
|
|
115
|
+
model = SparseCalibrationWeights(
|
|
116
|
+
n_features=N,
|
|
117
|
+
beta=0.35,
|
|
118
|
+
gamma=-0.1,
|
|
119
|
+
zeta=1.1,
|
|
120
|
+
init_keep_prob=0.5, # Start with 50% active probability
|
|
121
|
+
init_weights=1.0, # Or pass array of initial weights
|
|
122
|
+
log_weight_jitter_sd=0.05,
|
|
123
|
+
device="cuda", # GPU acceleration
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Train with L0+L2 regularization
|
|
127
|
+
model.fit(
|
|
128
|
+
M=M,
|
|
129
|
+
y=y,
|
|
130
|
+
lambda_l0=1e-6, # Controls sparsity level
|
|
131
|
+
lambda_l2=1e-8, # Prevents weight explosion
|
|
132
|
+
lr=0.15,
|
|
133
|
+
epochs=2000,
|
|
134
|
+
loss_type="relative", # Scale-invariant loss
|
|
135
|
+
verbose=True,
|
|
136
|
+
verbose_freq=100,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Get results
|
|
140
|
+
active = model.get_active_weights()
|
|
141
|
+
print(f"Selected {active['count']} of {N} households ({100*active['count']/N:.1f}%)")
|
|
142
|
+
print(f"Sparsity: {model.get_sparsity():.1%}")
|
|
143
|
+
|
|
144
|
+
# Predict calibrated totals
|
|
145
|
+
y_pred = model.predict(M)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Key Features for Calibration
|
|
149
|
+
|
|
150
|
+
- **Non-negative weights**: All weights constrained to be positive via log-space parameterization
|
|
151
|
+
- **Sparse solutions**: L0 penalty directly minimizes the count of active weights
|
|
152
|
+
- **Relative loss**: Scale-invariant loss for targets spanning orders of magnitude
|
|
153
|
+
- **Group-wise averaging**: Balance loss contributions across target groups with different cardinalities
|
|
154
|
+
- **GPU support**: CUDA acceleration for large-scale problems
|
|
155
|
+
|
|
156
|
+
## Neural Network Sparsification
|
|
93
157
|
|
|
94
|
-
|
|
158
|
+
The package also supports traditional neural network pruning:
|
|
95
159
|
|
|
96
160
|
```python
|
|
97
161
|
import torch
|
|
98
162
|
from l0 import L0Linear, compute_l0l2_penalty, TemperatureScheduler, update_temperatures
|
|
99
163
|
|
|
100
|
-
# Create a sparse model
|
|
101
164
|
class SparseModel(torch.nn.Module):
|
|
102
165
|
def __init__(self):
|
|
103
166
|
super().__init__()
|
|
104
167
|
self.fc1 = L0Linear(784, 256, init_sparsity=0.5)
|
|
105
168
|
self.fc2 = L0Linear(256, 10, init_sparsity=0.7)
|
|
106
|
-
|
|
169
|
+
|
|
107
170
|
def forward(self, x):
|
|
108
171
|
x = torch.relu(self.fc1(x))
|
|
109
172
|
return self.fc2(x)
|
|
@@ -112,95 +175,71 @@ model = SparseModel()
|
|
|
112
175
|
optimizer = torch.optim.Adam(model.parameters())
|
|
113
176
|
scheduler = TemperatureScheduler(initial_temp=1.0, final_temp=0.1)
|
|
114
177
|
|
|
115
|
-
# Training loop
|
|
116
178
|
for epoch in range(100):
|
|
117
|
-
# Update temperature
|
|
118
179
|
temp = scheduler.get_temperature(epoch)
|
|
119
180
|
update_temperatures(model, temp)
|
|
120
|
-
|
|
121
|
-
# Forward pass
|
|
181
|
+
|
|
122
182
|
output = model(input_data)
|
|
123
183
|
ce_loss = criterion(output, target)
|
|
124
|
-
|
|
125
|
-
# Add L0L2 penalty
|
|
126
184
|
penalty = compute_l0l2_penalty(model, l0_lambda=1e-3, l2_lambda=1e-4)
|
|
127
185
|
loss = ce_loss + penalty
|
|
128
|
-
|
|
129
|
-
# Backward pass
|
|
186
|
+
|
|
130
187
|
optimizer.zero_grad()
|
|
131
188
|
loss.backward()
|
|
132
189
|
optimizer.step()
|
|
133
190
|
```
|
|
134
191
|
|
|
135
|
-
###
|
|
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
|
|
198
|
+
|
|
199
|
+
## Intelligent Sampling Gates
|
|
200
|
+
|
|
201
|
+
Standalone gates for sample/feature selection:
|
|
136
202
|
|
|
137
203
|
```python
|
|
138
|
-
from l0 import SampleGate, HybridGate
|
|
204
|
+
from l0 import SampleGate, FeatureGate, HybridGate
|
|
139
205
|
|
|
140
|
-
#
|
|
206
|
+
# Select samples via learned gates
|
|
141
207
|
gate = SampleGate(n_samples=10000, target_samples=1000)
|
|
142
208
|
selected_data, indices = gate.select_samples(data)
|
|
143
209
|
|
|
144
|
-
#
|
|
210
|
+
# Select features
|
|
211
|
+
gate = FeatureGate(n_features=1000, max_features=50)
|
|
212
|
+
selected_data, indices = gate.select_features(data)
|
|
213
|
+
|
|
214
|
+
# Hybrid: combine L0 selection with random sampling
|
|
145
215
|
hybrid = HybridGate(
|
|
146
216
|
n_items=10000,
|
|
147
|
-
l0_fraction=0.25,
|
|
148
|
-
random_fraction=0.75
|
|
149
|
-
target_items=1000
|
|
217
|
+
l0_fraction=0.25, # 25% via learned L0 gates
|
|
218
|
+
random_fraction=0.75 # 75% random for coverage
|
|
150
219
|
)
|
|
151
220
|
selected, indices, types = hybrid.select(data)
|
|
152
221
|
```
|
|
153
222
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
```python
|
|
157
|
-
from l0 import FeatureGate
|
|
158
|
-
|
|
159
|
-
# Select top features
|
|
160
|
-
gate = FeatureGate(n_features=1000, max_features=50)
|
|
161
|
-
selected_data, feature_indices = gate.select_features(data)
|
|
162
|
-
|
|
163
|
-
# Get feature importance
|
|
164
|
-
importance = gate.get_feature_importance()
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## Integration with PolicyEngine
|
|
168
|
-
|
|
169
|
-
This package is designed to work with PolicyEngine's calibration system:
|
|
170
|
-
|
|
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
|
-
)
|
|
223
|
+
## How L0 Regularization Works
|
|
181
224
|
|
|
182
|
-
|
|
183
|
-
masked_weights = weights * gates()
|
|
184
|
-
```
|
|
225
|
+
Unlike post-hoc pruning (setting small weights to zero), L0 regularization integrates sparsity into the training objective:
|
|
185
226
|
|
|
186
|
-
|
|
227
|
+
1. **Stochastic gates**: Each weight has a learned gate parameter controlling activation probability
|
|
228
|
+
2. **Hard Concrete distribution**: Enables differentiable 0/1 gate values during training
|
|
229
|
+
3. **Expected L0 penalty**: Minimizes the expected number of active gates
|
|
230
|
+
4. **Temperature annealing**: Gradually sharpens gates from soft to hard decisions
|
|
187
231
|
|
|
188
|
-
|
|
232
|
+
The result: the network learns *which* weights should be zero as part of optimization, not as a post-processing step.
|
|
189
233
|
|
|
190
234
|
## Testing
|
|
191
235
|
|
|
192
|
-
Run tests with:
|
|
193
236
|
```bash
|
|
194
237
|
pytest tests/ -v --cov=l0
|
|
195
238
|
```
|
|
196
239
|
|
|
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
240
|
## Citation
|
|
202
241
|
|
|
203
|
-
If you use this package, please cite:
|
|
242
|
+
If you use this package, please cite the original paper:
|
|
204
243
|
|
|
205
244
|
```bibtex
|
|
206
245
|
@article{louizos2017learning,
|
|
@@ -213,4 +252,4 @@ If you use this package, please cite:
|
|
|
213
252
|
|
|
214
253
|
## License
|
|
215
254
|
|
|
216
|
-
MIT License - see [LICENSE](LICENSE)
|
|
255
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
l0/__init__.py,sha256=ezGVLGbdU_jAElAJSdLKjSWCnZRILAZi8qMBPjcHwQc,1192
|
|
2
|
-
l0/calibration.py,sha256=
|
|
2
|
+
l0/calibration.py,sha256=3VM-28M1yp-DM6AlHX7sqZCaE7OJevpY9Ey-syjSPik,17838
|
|
3
3
|
l0/distributions.py,sha256=GvVoDasdxoDlPPdzt33lFzkuJgQsfkEn9e-abxru3nM,6325
|
|
4
4
|
l0/gates.py,sha256=54OORMg9qSsdbIyzYIL9t9CCHWsKbumSKl_d4-YFuNU,11443
|
|
5
5
|
l0/layers.py,sha256=j_DPEFdfUc37ojtoK2ufGzVYzZ_DwusqTcJXCVxZS68,13785
|
|
6
6
|
l0/penalties.py,sha256=okqXgX2eGUmg5prcDclicTQALPPPF9FA5eQP09V4GTE,9569
|
|
7
7
|
l0/sparse.py,sha256=UVpkrrJBnQ9iSNDoWK5F_sr43tQareWXzYHyMq_cKss,9726
|
|
8
|
-
l0_python-0.4.
|
|
9
|
-
l0_python-0.4.
|
|
10
|
-
l0_python-0.4.
|
|
11
|
-
l0_python-0.4.
|
|
12
|
-
l0_python-0.4.
|
|
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
|
|
File without changes
|