l0-python 0.3.0__tar.gz → 0.3.1__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.3.0 → l0_python-0.3.1}/PKG-INFO +2 -2
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/calibration.py +80 -21
- {l0_python-0.3.0 → l0_python-0.3.1}/l0_python.egg-info/PKG-INFO +2 -2
- {l0_python-0.3.0 → l0_python-0.3.1}/pyproject.toml +2 -2
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_calibration.py +127 -5
- {l0_python-0.3.0 → l0_python-0.3.1}/LICENSE +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/README.md +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/__init__.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/distributions.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/gates.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/layers.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/penalties.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0/sparse.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0_python.egg-info/SOURCES.txt +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0_python.egg-info/dependency_links.txt +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0_python.egg-info/requires.txt +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/l0_python.egg-info/top_level.txt +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/setup.cfg +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_distributions.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_gates.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_layers.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_penalties.py +0 -0
- {l0_python-0.3.0 → l0_python-0.3.1}/tests/test_sparse.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: l0-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -36,7 +36,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
39
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
42
|
Requires-Dist: torch>=2.0.0
|
|
@@ -30,10 +30,21 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
30
30
|
Lower bound of stretched concrete distribution
|
|
31
31
|
zeta : float
|
|
32
32
|
Upper bound of stretched concrete distribution
|
|
33
|
-
init_keep_prob : float
|
|
34
|
-
Initial probability of keeping each weight active
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
init_keep_prob : float or array-like
|
|
34
|
+
Initial probability of keeping each weight active.
|
|
35
|
+
If float, all weights use the same probability.
|
|
36
|
+
If array, must have shape (n_features,)
|
|
37
|
+
init_weights : float, array-like, or None
|
|
38
|
+
Initial weight values (on original scale, not log).
|
|
39
|
+
If float, all weights initialized to this value.
|
|
40
|
+
If array, must have shape (n_features,).
|
|
41
|
+
If None, defaults to 1.0 for all weights.
|
|
42
|
+
log_weight_jitter_sd : float
|
|
43
|
+
Standard deviation of noise added to log weights at start of fit() to break symmetry.
|
|
44
|
+
Set to 0 to disable jitter. Default is 0.0 (no jitter).
|
|
45
|
+
log_alpha_jitter_sd : float
|
|
46
|
+
Standard deviation of noise added to log_alpha at initialization to break symmetry.
|
|
47
|
+
Set to 0 to disable jitter. Default is 0.01 (following Louizos et al.).
|
|
37
48
|
device : str or torch.device
|
|
38
49
|
Device to run computations on ('cpu' or 'cuda')
|
|
39
50
|
"""
|
|
@@ -44,8 +55,10 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
44
55
|
beta: float = 2 / 3,
|
|
45
56
|
gamma: float = -0.1,
|
|
46
57
|
zeta: float = 1.1,
|
|
47
|
-
init_keep_prob: float = 0.5,
|
|
48
|
-
|
|
58
|
+
init_keep_prob: float | np.ndarray = 0.5,
|
|
59
|
+
init_weights: float | np.ndarray | None = None,
|
|
60
|
+
log_weight_jitter_sd: float = 0.0,
|
|
61
|
+
log_alpha_jitter_sd: float = 0.01,
|
|
49
62
|
device: str | torch.device = "cpu",
|
|
50
63
|
):
|
|
51
64
|
super().__init__()
|
|
@@ -53,25 +66,65 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
53
66
|
self.beta = beta
|
|
54
67
|
self.gamma = gamma
|
|
55
68
|
self.zeta = zeta
|
|
69
|
+
self.log_weight_jitter_sd = log_weight_jitter_sd
|
|
70
|
+
self.log_alpha_jitter_sd = log_alpha_jitter_sd
|
|
56
71
|
self.device = torch.device(device)
|
|
57
72
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
# Initialize weights (on original scale)
|
|
74
|
+
if init_weights is None:
|
|
75
|
+
# Default: all weights start at 1.0
|
|
76
|
+
weight_values = torch.ones(n_features, device=self.device)
|
|
77
|
+
elif isinstance(init_weights, (int, float)):
|
|
78
|
+
# Scalar: all weights start at this value
|
|
79
|
+
weight_values = torch.full(
|
|
80
|
+
(n_features,), float(init_weights), device=self.device
|
|
65
81
|
)
|
|
66
|
-
|
|
82
|
+
else:
|
|
83
|
+
# Array: use provided values
|
|
84
|
+
weight_values = torch.tensor(
|
|
85
|
+
init_weights, dtype=torch.float32, device=self.device
|
|
86
|
+
)
|
|
87
|
+
if weight_values.shape != (n_features,):
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"init_weights array must have shape ({n_features},), "
|
|
90
|
+
f"got {weight_values.shape}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Convert to log space to ensure positivity via exp transformation
|
|
94
|
+
# Add small epsilon to avoid log(0)
|
|
95
|
+
self.log_weight = nn.Parameter(torch.log(weight_values + 1e-8))
|
|
67
96
|
|
|
68
97
|
# L0 gate parameters
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
# Handle init_keep_prob as scalar or array
|
|
99
|
+
if isinstance(init_keep_prob, (int, float)):
|
|
100
|
+
# Scalar: broadcast to all features
|
|
101
|
+
keep_prob_values = torch.full(
|
|
102
|
+
(n_features,), float(init_keep_prob), device=self.device
|
|
73
103
|
)
|
|
74
|
-
|
|
104
|
+
else:
|
|
105
|
+
# Array: use provided values
|
|
106
|
+
keep_prob_values = torch.tensor(
|
|
107
|
+
init_keep_prob, dtype=torch.float32, device=self.device
|
|
108
|
+
)
|
|
109
|
+
if keep_prob_values.shape != (n_features,):
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"init_keep_prob array must have shape ({n_features},), "
|
|
112
|
+
f"got {keep_prob_values.shape}"
|
|
113
|
+
)
|
|
114
|
+
# Clip to valid probability range to avoid log(0) or log(inf)
|
|
115
|
+
keep_prob_values = keep_prob_values.clamp(1e-6, 1 - 1e-6)
|
|
116
|
+
|
|
117
|
+
# Convert probabilities to log_alpha
|
|
118
|
+
mu = torch.log(keep_prob_values / (1 - keep_prob_values))
|
|
119
|
+
# Add jitter to break symmetry (if specified)
|
|
120
|
+
if self.log_alpha_jitter_sd > 0:
|
|
121
|
+
jitter = (
|
|
122
|
+
torch.randn(n_features, device=self.device)
|
|
123
|
+
* self.log_alpha_jitter_sd
|
|
124
|
+
)
|
|
125
|
+
self.log_alpha = nn.Parameter(mu + jitter)
|
|
126
|
+
else:
|
|
127
|
+
self.log_alpha = nn.Parameter(mu)
|
|
75
128
|
|
|
76
129
|
# Cache for sparse tensor conversion
|
|
77
130
|
self._cached_M_torch: torch.sparse.Tensor | None = None
|
|
@@ -312,8 +365,14 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
312
365
|
# No grouping - all targets weighted equally
|
|
313
366
|
group_weights = torch.ones_like(y)
|
|
314
367
|
|
|
315
|
-
#
|
|
316
|
-
|
|
368
|
+
# Add jitter to weights to break symmetry (if jitter_sd > 0)
|
|
369
|
+
if self.log_weight_jitter_sd > 0:
|
|
370
|
+
with torch.no_grad():
|
|
371
|
+
jitter = (
|
|
372
|
+
torch.randn_like(self.log_weight)
|
|
373
|
+
* self.log_weight_jitter_sd
|
|
374
|
+
)
|
|
375
|
+
self.log_weight.data += jitter
|
|
317
376
|
|
|
318
377
|
# Setup optimizer
|
|
319
378
|
optimizer = torch.optim.Adam([self.log_weight, self.log_alpha], lr=lr)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: l0-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
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
|
|
@@ -36,7 +36,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
37
|
Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
39
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
42
|
Requires-Dist: torch>=2.0.0
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "l0-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "L0 regularization for sparse neural networks and intelligent sampling"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
11
11
|
{name = "PolicyEngine", email = "hello@policyengine.org"},
|
|
12
12
|
]
|
|
13
13
|
license = {file = "LICENSE"}
|
|
14
|
-
requires-python = ">=3.
|
|
14
|
+
requires-python = ">=3.11"
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 4 - Beta",
|
|
17
17
|
"Intended Audience :: Science/Research",
|
|
@@ -21,7 +21,7 @@ class TestSparseCalibrationWeights:
|
|
|
21
21
|
M = sp.random(Q, N, density=0.3, format="csr")
|
|
22
22
|
y = np.random.randn(Q) + 10
|
|
23
23
|
|
|
24
|
-
model = SparseCalibrationWeights(n_features=N)
|
|
24
|
+
model = SparseCalibrationWeights(n_features=N, init_weights=1.0)
|
|
25
25
|
model.fit(M, y, epochs=100, verbose=False)
|
|
26
26
|
|
|
27
27
|
# Check positivity
|
|
@@ -57,7 +57,8 @@ class TestSparseCalibrationWeights:
|
|
|
57
57
|
gamma=-0.1,
|
|
58
58
|
zeta=1.1,
|
|
59
59
|
init_keep_prob=0.3,
|
|
60
|
-
|
|
60
|
+
init_weights=1.0, # Start all weights at 1.0
|
|
61
|
+
log_weight_jitter_sd=0.5, # Add jitter for symmetry breaking
|
|
61
62
|
)
|
|
62
63
|
|
|
63
64
|
model.fit(
|
|
@@ -171,7 +172,7 @@ class TestSparseCalibrationWeights:
|
|
|
171
172
|
def test_get_active_weights(self):
|
|
172
173
|
"""Test active weight extraction."""
|
|
173
174
|
N = 100
|
|
174
|
-
model = SparseCalibrationWeights(n_features=N)
|
|
175
|
+
model = SparseCalibrationWeights(n_features=N, init_weights=1.0)
|
|
175
176
|
|
|
176
177
|
# Simple test data
|
|
177
178
|
M = sp.eye(N, format="csr")
|
|
@@ -202,7 +203,7 @@ class TestSparseCalibrationWeights:
|
|
|
202
203
|
M = sp.random(Q, N, density=0.5, format="csr")
|
|
203
204
|
y = np.random.randn(Q)
|
|
204
205
|
|
|
205
|
-
model = SparseCalibrationWeights(n_features=N)
|
|
206
|
+
model = SparseCalibrationWeights(n_features=N, init_weights=1.0)
|
|
206
207
|
model.fit(M, y, epochs=100, verbose=False)
|
|
207
208
|
|
|
208
209
|
# Multiple predictions should be identical
|
|
@@ -357,7 +358,7 @@ class TestSparseCalibrationWeights:
|
|
|
357
358
|
M = sp.random(Q, N, density=0.3, format="csr")
|
|
358
359
|
y = np.random.uniform(100, 1000, size=Q)
|
|
359
360
|
|
|
360
|
-
model = SparseCalibrationWeights(n_features=N)
|
|
361
|
+
model = SparseCalibrationWeights(n_features=N, init_weights=1.0)
|
|
361
362
|
|
|
362
363
|
# Test 1: All targets in one group (should behave like no grouping)
|
|
363
364
|
target_groups_single = np.zeros(Q, dtype=int)
|
|
@@ -421,3 +422,124 @@ class TestSparseCalibrationWeights:
|
|
|
421
422
|
assert (
|
|
422
423
|
np.mean(small_group_errors) < 0.5
|
|
423
424
|
), "Small groups should not be ignored"
|
|
425
|
+
|
|
426
|
+
def test_init_weights_options(self):
|
|
427
|
+
"""Test different weight initialization options."""
|
|
428
|
+
N = 50
|
|
429
|
+
|
|
430
|
+
# Test 1: Default (None) should give all weights = 1.0
|
|
431
|
+
model_default = SparseCalibrationWeights(n_features=N)
|
|
432
|
+
with torch.no_grad():
|
|
433
|
+
weights = torch.exp(model_default.log_weight)
|
|
434
|
+
assert torch.allclose(weights, torch.ones(N), atol=1e-6)
|
|
435
|
+
|
|
436
|
+
# Test 2: Scalar initialization
|
|
437
|
+
model_scalar = SparseCalibrationWeights(n_features=N, init_weights=2.5)
|
|
438
|
+
with torch.no_grad():
|
|
439
|
+
weights = torch.exp(model_scalar.log_weight)
|
|
440
|
+
assert torch.allclose(weights, torch.full((N,), 2.5), atol=1e-6)
|
|
441
|
+
|
|
442
|
+
# Test 3: Array initialization
|
|
443
|
+
init_array = np.random.uniform(0.5, 2.0, size=N)
|
|
444
|
+
model_array = SparseCalibrationWeights(
|
|
445
|
+
n_features=N, init_weights=init_array
|
|
446
|
+
)
|
|
447
|
+
with torch.no_grad():
|
|
448
|
+
weights = torch.exp(model_array.log_weight).cpu().numpy()
|
|
449
|
+
np.testing.assert_allclose(weights, init_array, rtol=1e-5)
|
|
450
|
+
|
|
451
|
+
# Test 4: Wrong shape should raise error
|
|
452
|
+
with pytest.raises(ValueError, match="must have shape"):
|
|
453
|
+
SparseCalibrationWeights(n_features=N, init_weights=np.ones(N + 1))
|
|
454
|
+
|
|
455
|
+
def test_weight_jitter(self):
|
|
456
|
+
"""Test that weight jitter works correctly."""
|
|
457
|
+
N = 100
|
|
458
|
+
Q = 20
|
|
459
|
+
|
|
460
|
+
np.random.seed(42)
|
|
461
|
+
torch.manual_seed(42)
|
|
462
|
+
|
|
463
|
+
M = sp.random(Q, N, density=0.3, format="csr")
|
|
464
|
+
y = np.random.randn(Q) + 10
|
|
465
|
+
|
|
466
|
+
# Model with jitter
|
|
467
|
+
model_with_jitter = SparseCalibrationWeights(
|
|
468
|
+
n_features=N, init_weights=1.0, log_weight_jitter_sd=0.5
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Store initial weights
|
|
472
|
+
initial_weights = model_with_jitter.log_weight.data.clone()
|
|
473
|
+
|
|
474
|
+
# Fit should add jitter
|
|
475
|
+
model_with_jitter.fit(M, y, epochs=10, verbose=False)
|
|
476
|
+
|
|
477
|
+
# Weights should have changed due to jitter (and training)
|
|
478
|
+
final_weights = model_with_jitter.log_weight.data
|
|
479
|
+
assert not torch.allclose(initial_weights, final_weights)
|
|
480
|
+
|
|
481
|
+
# Model without jitter
|
|
482
|
+
torch.manual_seed(42) # Reset seed
|
|
483
|
+
model_no_jitter = SparseCalibrationWeights(
|
|
484
|
+
n_features=N,
|
|
485
|
+
init_weights=1.0,
|
|
486
|
+
log_weight_jitter_sd=0.0, # No jitter
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
initial_weights_no_jitter = model_no_jitter.log_weight.data.clone()
|
|
490
|
+
model_no_jitter.fit(M, y, epochs=1, verbose=False) # Just 1 epoch
|
|
491
|
+
|
|
492
|
+
# After 1 epoch, change should be small without jitter
|
|
493
|
+
weights_after_1_epoch = model_no_jitter.log_weight.data
|
|
494
|
+
# The change is due to gradient updates only
|
|
495
|
+
change = (
|
|
496
|
+
(weights_after_1_epoch - initial_weights_no_jitter).abs().max()
|
|
497
|
+
)
|
|
498
|
+
assert change < 1.0, "Without jitter, initial change should be small"
|
|
499
|
+
|
|
500
|
+
def test_init_keep_prob_options(self):
|
|
501
|
+
"""Test init_keep_prob as scalar and array."""
|
|
502
|
+
n_features = 20
|
|
503
|
+
|
|
504
|
+
# Test 1: Scalar init_keep_prob (existing behavior)
|
|
505
|
+
model_scalar = SparseCalibrationWeights(
|
|
506
|
+
n_features=n_features,
|
|
507
|
+
init_keep_prob=0.7,
|
|
508
|
+
)
|
|
509
|
+
# All log_alpha values should be similar (around log(0.7/0.3) plus small jitter)
|
|
510
|
+
expected_mu = np.log(0.7 / 0.3)
|
|
511
|
+
with torch.no_grad():
|
|
512
|
+
log_alphas = model_scalar.log_alpha.numpy()
|
|
513
|
+
# Check they're all close to expected value (within jitter range)
|
|
514
|
+
assert np.all(np.abs(log_alphas - expected_mu) < 0.1)
|
|
515
|
+
|
|
516
|
+
# Test 2: Array init_keep_prob
|
|
517
|
+
keep_probs = np.linspace(0.1, 0.9, n_features)
|
|
518
|
+
model_array = SparseCalibrationWeights(
|
|
519
|
+
n_features=n_features,
|
|
520
|
+
init_keep_prob=keep_probs,
|
|
521
|
+
)
|
|
522
|
+
# Each log_alpha should correspond to its keep_prob
|
|
523
|
+
with torch.no_grad():
|
|
524
|
+
log_alphas = model_array.log_alpha.numpy()
|
|
525
|
+
expected_mus = np.log(keep_probs / (1 - keep_probs))
|
|
526
|
+
# Check each is close to its expected value
|
|
527
|
+
assert np.all(np.abs(log_alphas - expected_mus) < 0.1)
|
|
528
|
+
|
|
529
|
+
# Test 3: Wrong shape should raise error
|
|
530
|
+
with pytest.raises(ValueError, match="must have shape"):
|
|
531
|
+
SparseCalibrationWeights(
|
|
532
|
+
n_features=10,
|
|
533
|
+
init_keep_prob=np.ones(5), # Wrong size
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Test 4: Edge case probabilities get clamped
|
|
537
|
+
extreme_probs = np.array([0.0, 0.5, 1.0])
|
|
538
|
+
model_extreme = SparseCalibrationWeights(
|
|
539
|
+
n_features=3,
|
|
540
|
+
init_keep_prob=extreme_probs,
|
|
541
|
+
)
|
|
542
|
+
with torch.no_grad():
|
|
543
|
+
log_alphas = model_extreme.log_alpha.numpy()
|
|
544
|
+
# Should not have inf or -inf values
|
|
545
|
+
assert np.all(np.isfinite(log_alphas))
|
|
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
|
|
File without changes
|