l0-python 0.6.1__py3-none-any.whl → 0.6.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/__init__.py +3 -0
- l0/calibration.py +60 -44
- l0/distributions.py +35 -14
- l0/gates.py +3 -9
- l0/layers.py +2 -6
- l0/penalties.py +3 -9
- l0/sparse.py +36 -21
- {l0_python-0.6.1.dist-info → l0_python-0.6.2.dist-info}/METADATA +2 -2
- l0_python-0.6.2.dist-info/RECORD +12 -0
- {l0_python-0.6.1.dist-info → l0_python-0.6.2.dist-info}/WHEEL +1 -1
- l0_python-0.6.1.dist-info/RECORD +0 -12
- {l0_python-0.6.1.dist-info → l0_python-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {l0_python-0.6.1.dist-info → l0_python-0.6.2.dist-info}/top_level.txt +0 -0
l0/__init__.py
CHANGED
|
@@ -7,6 +7,7 @@ and intelligent sampling, based on Louizos, Welling, & Kingma (2017).
|
|
|
7
7
|
|
|
8
8
|
__version__ = "0.5.0"
|
|
9
9
|
|
|
10
|
+
from .calibration import SparseCalibrationWeights
|
|
10
11
|
from .distributions import HardConcrete
|
|
11
12
|
from .gates import FeatureGate, HybridGate, L0Gate, SampleGate
|
|
12
13
|
from .layers import (
|
|
@@ -39,6 +40,8 @@ __all__ = [
|
|
|
39
40
|
"prune_model",
|
|
40
41
|
# Sparse
|
|
41
42
|
"SparseL0Linear",
|
|
43
|
+
# Calibration
|
|
44
|
+
"SparseCalibrationWeights",
|
|
42
45
|
# Gates
|
|
43
46
|
"L0Gate",
|
|
44
47
|
"SampleGate",
|
l0/calibration.py
CHANGED
|
@@ -47,6 +47,12 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
47
47
|
Set to 0 to disable jitter. Default is 0.01 (following Louizos et al.).
|
|
48
48
|
device : str or torch.device
|
|
49
49
|
Device to run computations on ('cpu' or 'cuda')
|
|
50
|
+
seed : int, optional
|
|
51
|
+
Seed for the RNG used by the `log_alpha` init jitter and (inside
|
|
52
|
+
``fit``) the `log_weight` jitter. When set, two models with the same
|
|
53
|
+
inputs produce byte-identical initial `log_alpha` and jitter without
|
|
54
|
+
the caller having to manage PyTorch's global RNG. ``None`` (default)
|
|
55
|
+
preserves legacy behaviour of using the global RNG.
|
|
50
56
|
"""
|
|
51
57
|
|
|
52
58
|
def __init__(
|
|
@@ -61,6 +67,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
61
67
|
log_alpha_jitter_sd: float = 0.01,
|
|
62
68
|
device: str | torch.device = "cpu",
|
|
63
69
|
use_gates: bool = True,
|
|
70
|
+
seed: int | None = None,
|
|
64
71
|
):
|
|
65
72
|
super().__init__()
|
|
66
73
|
self.n_features = n_features
|
|
@@ -71,6 +78,17 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
71
78
|
self.log_alpha_jitter_sd = log_alpha_jitter_sd
|
|
72
79
|
self.device = torch.device(device)
|
|
73
80
|
self.use_gates = use_gates
|
|
81
|
+
self.seed = seed
|
|
82
|
+
|
|
83
|
+
# Local RNG (only used when `seed` is provided). Kept on the module so
|
|
84
|
+
# `fit`'s `log_weight` jitter uses the same deterministic stream.
|
|
85
|
+
if seed is not None:
|
|
86
|
+
self._generator: torch.Generator | None = torch.Generator(
|
|
87
|
+
device=self.device
|
|
88
|
+
)
|
|
89
|
+
self._generator.manual_seed(int(seed))
|
|
90
|
+
else:
|
|
91
|
+
self._generator = None
|
|
74
92
|
|
|
75
93
|
# Initialize weights (on original scale)
|
|
76
94
|
if init_weights is None:
|
|
@@ -121,7 +139,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
121
139
|
# Add jitter to break symmetry (if specified)
|
|
122
140
|
if self.log_alpha_jitter_sd > 0:
|
|
123
141
|
jitter = (
|
|
124
|
-
torch.randn(n_features, device=self.device)
|
|
142
|
+
torch.randn(n_features, generator=self._generator, device=self.device)
|
|
125
143
|
* self.log_alpha_jitter_sd
|
|
126
144
|
)
|
|
127
145
|
self.log_alpha = nn.Parameter(mu + jitter)
|
|
@@ -132,9 +150,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
132
150
|
self._cached_M_torch: torch.sparse.Tensor | None = None
|
|
133
151
|
self._cached_M_shape: tuple[int, int] | None = None
|
|
134
152
|
|
|
135
|
-
def _convert_sparse_to_torch(
|
|
136
|
-
self, M_sparse: sp.spmatrix
|
|
137
|
-
) -> torch.sparse.Tensor:
|
|
153
|
+
def _convert_sparse_to_torch(self, M_sparse: sp.spmatrix) -> torch.sparse.Tensor:
|
|
138
154
|
"""
|
|
139
155
|
Convert scipy sparse matrix to torch sparse tensor.
|
|
140
156
|
|
|
@@ -148,9 +164,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
148
164
|
return self._cached_M_torch
|
|
149
165
|
|
|
150
166
|
M_coo = M_sparse.tocoo()
|
|
151
|
-
indices = torch.LongTensor(np.vstack([M_coo.row, M_coo.col])).to(
|
|
152
|
-
self.device
|
|
153
|
-
)
|
|
167
|
+
indices = torch.LongTensor(np.vstack([M_coo.row, M_coo.col])).to(self.device)
|
|
154
168
|
values = torch.FloatTensor(M_coo.data).to(self.device)
|
|
155
169
|
M_torch = torch.sparse_coo_tensor(
|
|
156
170
|
indices,
|
|
@@ -166,18 +180,26 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
166
180
|
|
|
167
181
|
return M_torch
|
|
168
182
|
|
|
183
|
+
# Bounds for `log_alpha` before scaling by `beta`. Values outside
|
|
184
|
+
# ``[-LOG_ALPHA_BOUND, LOG_ALPHA_BOUND]`` saturate the sigmoid and
|
|
185
|
+
# vanish gradients; clamping keeps sampling and deterministic gates
|
|
186
|
+
# well-defined under fp16/bf16.
|
|
187
|
+
LOG_ALPHA_BOUND: float = 20.0
|
|
188
|
+
|
|
169
189
|
def _sample_gates(self) -> torch.Tensor:
|
|
170
190
|
"""Sample gates using Hard Concrete distribution."""
|
|
171
191
|
eps = 1e-6
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
193
|
+
u = torch.rand_like(log_alpha).clamp(eps, 1 - eps)
|
|
194
|
+
s = (torch.log(u) - torch.log(1 - u) + log_alpha) / self.beta
|
|
174
195
|
s = torch.sigmoid(s)
|
|
175
196
|
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
176
197
|
return s_bar.clamp(0, 1)
|
|
177
198
|
|
|
178
199
|
def get_deterministic_gates(self) -> torch.Tensor:
|
|
179
200
|
"""Get deterministic gate values (for inference)."""
|
|
180
|
-
|
|
201
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
202
|
+
s = torch.sigmoid(log_alpha / self.beta)
|
|
181
203
|
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
182
204
|
return s_bar.clamp(0, 1)
|
|
183
205
|
|
|
@@ -249,7 +271,8 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
249
271
|
c = -self.beta * torch.log(
|
|
250
272
|
torch.tensor(-self.gamma / self.zeta, device=self.device)
|
|
251
273
|
)
|
|
252
|
-
|
|
274
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
275
|
+
pi = torch.sigmoid(log_alpha + c)
|
|
253
276
|
return pi.sum()
|
|
254
277
|
|
|
255
278
|
def get_l2_penalty(self) -> torch.Tensor:
|
|
@@ -360,9 +383,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
360
383
|
|
|
361
384
|
# Validate group_multipliers
|
|
362
385
|
if group_multipliers is not None and target_groups is None:
|
|
363
|
-
raise ValueError(
|
|
364
|
-
"group_multipliers requires target_groups to be set"
|
|
365
|
-
)
|
|
386
|
+
raise ValueError("group_multipliers requires target_groups to be set")
|
|
366
387
|
|
|
367
388
|
# Compute group weights for loss averaging
|
|
368
389
|
if target_groups is not None:
|
|
@@ -385,8 +406,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
385
406
|
group_mask = target_groups == gid
|
|
386
407
|
if not group_mask.any():
|
|
387
408
|
raise ValueError(
|
|
388
|
-
f"group_multipliers key {gid} not found "
|
|
389
|
-
f"in target_groups"
|
|
409
|
+
f"group_multipliers key {gid} not found in target_groups"
|
|
390
410
|
)
|
|
391
411
|
group_weights[group_mask] *= mult
|
|
392
412
|
else:
|
|
@@ -396,9 +416,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
396
416
|
if target_groups is not None:
|
|
397
417
|
n_groups = len(torch.unique(target_groups))
|
|
398
418
|
parts = [f"{n_groups} groups"]
|
|
399
|
-
parts.append(
|
|
400
|
-
f"normalize={'on' if normalize_groups else 'off'}"
|
|
401
|
-
)
|
|
419
|
+
parts.append(f"normalize={'on' if normalize_groups else 'off'}")
|
|
402
420
|
if group_multipliers is not None:
|
|
403
421
|
parts.append(f"multipliers={group_multipliers}")
|
|
404
422
|
print(f"Groups: {', '.join(parts)}")
|
|
@@ -408,10 +426,17 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
408
426
|
# Add jitter to weights to break symmetry (if jitter_sd > 0)
|
|
409
427
|
if self.log_weight_jitter_sd > 0:
|
|
410
428
|
with torch.no_grad():
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
429
|
+
# `torch.randn_like` can't take a generator kwarg, so draw
|
|
430
|
+
# explicitly via `torch.randn` when a local RNG is set.
|
|
431
|
+
if self._generator is not None:
|
|
432
|
+
noise = torch.randn(
|
|
433
|
+
self.log_weight.shape,
|
|
434
|
+
generator=self._generator,
|
|
435
|
+
device=self.device,
|
|
436
|
+
)
|
|
437
|
+
else:
|
|
438
|
+
noise = torch.randn_like(self.log_weight)
|
|
439
|
+
jitter = noise * self.log_weight_jitter_sd
|
|
415
440
|
self.log_weight.data += jitter
|
|
416
441
|
|
|
417
442
|
# Setup optimizer
|
|
@@ -428,9 +453,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
428
453
|
# Adding 1 to avoid division by zero
|
|
429
454
|
relative_errors = (y - y_pred) / (y + 1)
|
|
430
455
|
# Apply group weights and then average
|
|
431
|
-
weighted_squared_errors = (
|
|
432
|
-
relative_errors.pow(2) * group_weights
|
|
433
|
-
)
|
|
456
|
+
weighted_squared_errors = relative_errors.pow(2) * group_weights
|
|
434
457
|
data_loss = (
|
|
435
458
|
weighted_squared_errors.sum()
|
|
436
459
|
) # Sum because weights already normalize
|
|
@@ -480,18 +503,14 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
480
503
|
group_losses = []
|
|
481
504
|
for group_id in torch.unique(target_groups):
|
|
482
505
|
group_mask = target_groups == group_id
|
|
483
|
-
group_mean_err = (
|
|
484
|
-
rel_errors[group_mask].mean().item()
|
|
485
|
-
)
|
|
506
|
+
group_mean_err = rel_errors[group_mask].mean().item()
|
|
486
507
|
group_losses.append(group_mean_err)
|
|
487
508
|
mean_group_mare = np.mean(group_losses)
|
|
488
509
|
else:
|
|
489
510
|
mean_group_mare = mean_rel_err
|
|
490
511
|
|
|
491
512
|
# Calculate sparsity percentage
|
|
492
|
-
sparsity_pct = 100 * (
|
|
493
|
-
1 - active_info["count"] / self.n_features
|
|
494
|
-
)
|
|
513
|
+
sparsity_pct = 100 * (1 - active_info["count"] / self.n_features)
|
|
495
514
|
|
|
496
515
|
# Weight distribution diagnostic
|
|
497
516
|
if active_weights.numel() > 0:
|
|
@@ -512,10 +531,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
512
531
|
.item()
|
|
513
532
|
)
|
|
514
533
|
w_large = (
|
|
515
|
-
(
|
|
516
|
-
(active_weights >= 10.0)
|
|
517
|
-
& (active_weights < 1000.0)
|
|
518
|
-
)
|
|
534
|
+
((active_weights >= 10.0) & (active_weights < 1000.0))
|
|
519
535
|
.sum()
|
|
520
536
|
.item()
|
|
521
537
|
)
|
|
@@ -523,12 +539,12 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
523
539
|
|
|
524
540
|
total_active = active_weights.numel()
|
|
525
541
|
weight_dist = (
|
|
526
|
-
f"[<0.01: {100*w_tiny/total_active:.1f}%, "
|
|
527
|
-
f"0.01-0.1: {100*w_small/total_active:.1f}%, "
|
|
528
|
-
f"0.1-1: {100*w_med/total_active:.1f}%, "
|
|
529
|
-
f"1-10: {100*w_normal/total_active:.1f}%, "
|
|
530
|
-
f"10-1000: {100*w_large/total_active:.1f}%, "
|
|
531
|
-
f">1000: {100*w_huge/total_active:.1f}%]"
|
|
542
|
+
f"[<0.01: {100 * w_tiny / total_active:.1f}%, "
|
|
543
|
+
f"0.01-0.1: {100 * w_small / total_active:.1f}%, "
|
|
544
|
+
f"0.1-1: {100 * w_med / total_active:.1f}%, "
|
|
545
|
+
f"1-10: {100 * w_normal / total_active:.1f}%, "
|
|
546
|
+
f"10-1000: {100 * w_large / total_active:.1f}%, "
|
|
547
|
+
f">1000: {100 * w_huge / total_active:.1f}%]"
|
|
532
548
|
)
|
|
533
549
|
else:
|
|
534
550
|
weight_dist = "[no active weights]"
|
|
@@ -540,7 +556,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
540
556
|
|
|
541
557
|
if target_groups is not None:
|
|
542
558
|
print(
|
|
543
|
-
f"Epoch {epoch+1:4d}: "
|
|
559
|
+
f"Epoch {epoch + 1:4d}: "
|
|
544
560
|
f"mean_group_mare={mean_group_mare:.4%}, "
|
|
545
561
|
f"max_error={max_rel_err:.1%}, "
|
|
546
562
|
f"total_loss={actual_total_loss:.3f}, "
|
|
@@ -549,7 +565,7 @@ class SparseCalibrationWeights(nn.Module):
|
|
|
549
565
|
)
|
|
550
566
|
else:
|
|
551
567
|
print(
|
|
552
|
-
f"Epoch {epoch+1:4d}: "
|
|
568
|
+
f"Epoch {epoch + 1:4d}: "
|
|
553
569
|
f"mean_error={mean_rel_err:.4%}, "
|
|
554
570
|
f"max_error={max_rel_err:.1%}, "
|
|
555
571
|
f"total_loss={actual_total_loss:.3f}, "
|
l0/distributions.py
CHANGED
|
@@ -69,9 +69,7 @@ class HardConcrete(nn.Module):
|
|
|
69
69
|
init_val = math.log(self.init_mean / (1 - self.init_mean))
|
|
70
70
|
self.qz_logits.data.fill_(init_val)
|
|
71
71
|
|
|
72
|
-
def forward(
|
|
73
|
-
self, input_shape: tuple[int, ...] | None = None
|
|
74
|
-
) -> torch.Tensor:
|
|
72
|
+
def forward(self, input_shape: tuple[int, ...] | None = None) -> torch.Tensor:
|
|
75
73
|
"""
|
|
76
74
|
Sample or compute gates.
|
|
77
75
|
|
|
@@ -98,6 +96,16 @@ class HardConcrete(nn.Module):
|
|
|
98
96
|
|
|
99
97
|
return gates
|
|
100
98
|
|
|
99
|
+
# Bounds for `qz_logits` before sigmoid/temperature scaling. Values
|
|
100
|
+
# outside ``[-LOG_ALPHA_BOUND, LOG_ALPHA_BOUND]`` saturate the sigmoid
|
|
101
|
+
# and vanish gradients; clamping keeps sampling and deterministic gates
|
|
102
|
+
# well-defined in fp16/bf16 as well. See Louizos et al. 2017.
|
|
103
|
+
LOG_ALPHA_BOUND: float = 20.0
|
|
104
|
+
|
|
105
|
+
# Epsilon for uniform sampling; fp16-safe (``1e-8`` underflows to zero
|
|
106
|
+
# at fp16 and ``log(0) = -inf``).
|
|
107
|
+
_UNIFORM_EPS: float = 1e-6
|
|
108
|
+
|
|
101
109
|
def _sample_gates(self) -> torch.Tensor:
|
|
102
110
|
"""
|
|
103
111
|
Sample gates using the reparameterization trick.
|
|
@@ -107,12 +115,19 @@ class HardConcrete(nn.Module):
|
|
|
107
115
|
torch.Tensor
|
|
108
116
|
Sampled gate values in [0, 1]
|
|
109
117
|
"""
|
|
110
|
-
#
|
|
111
|
-
|
|
118
|
+
# Clamp logits to a safe range so log-odds stay finite under fp16/bf16
|
|
119
|
+
# and gradients don't vanish at saturation.
|
|
120
|
+
logits = self.qz_logits.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
121
|
+
|
|
122
|
+
# Sample uniform noise (avoiding exact 0 and 1 for numerical stability).
|
|
123
|
+
# ``1e-6`` matches calibration.py / sparse.py and is fp16-safe.
|
|
124
|
+
u = torch.zeros_like(logits).uniform_(
|
|
125
|
+
self._UNIFORM_EPS, 1.0 - self._UNIFORM_EPS
|
|
126
|
+
)
|
|
112
127
|
|
|
113
128
|
# Apply the concrete distribution transformation
|
|
114
129
|
# s = sigmoid((log(u) - log(1-u) + logits) / temperature)
|
|
115
|
-
s = torch.log(u) - torch.log(1 - u) +
|
|
130
|
+
s = torch.log(u) - torch.log(1 - u) + logits
|
|
116
131
|
s = torch.sigmoid(s / self.temperature)
|
|
117
132
|
|
|
118
133
|
# Stretch and clamp to create hard concrete
|
|
@@ -125,13 +140,20 @@ class HardConcrete(nn.Module):
|
|
|
125
140
|
"""
|
|
126
141
|
Compute deterministic gates for evaluation.
|
|
127
142
|
|
|
143
|
+
Uses the mean of the Hard Concrete distribution, which applies the
|
|
144
|
+
temperature scaling to ``qz_logits`` so that ``eval()`` output is
|
|
145
|
+
consistent with ``_sample_gates`` and ``get_penalty`` /
|
|
146
|
+
``get_active_prob`` (see Louizos et al. 2017, Eq. 11).
|
|
147
|
+
|
|
128
148
|
Returns
|
|
129
149
|
-------
|
|
130
150
|
torch.Tensor
|
|
131
151
|
Deterministic gate values in [0, 1]
|
|
132
152
|
"""
|
|
133
|
-
#
|
|
134
|
-
|
|
153
|
+
# Clamp logits so eval output stays well-defined in fp16/bf16.
|
|
154
|
+
logits = self.qz_logits.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
155
|
+
# Mean of the binary concrete before stretch: sigmoid(logits / beta).
|
|
156
|
+
probs = torch.sigmoid(logits / self.temperature)
|
|
135
157
|
|
|
136
158
|
# Apply stretching transformation
|
|
137
159
|
gates = probs * (self.zeta - self.gamma) + self.gamma
|
|
@@ -147,10 +169,10 @@ class HardConcrete(nn.Module):
|
|
|
147
169
|
torch.Tensor
|
|
148
170
|
Expected number of non-zero gates
|
|
149
171
|
"""
|
|
172
|
+
# Clamp logits so sigmoid doesn't saturate and kill gradients.
|
|
173
|
+
logits = self.qz_logits.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
150
174
|
# Shift logits to account for hard concrete bounds
|
|
151
|
-
logits_shifted =
|
|
152
|
-
-self.gamma / self.zeta
|
|
153
|
-
)
|
|
175
|
+
logits_shifted = logits - self.temperature * math.log(-self.gamma / self.zeta)
|
|
154
176
|
|
|
155
177
|
# Probability that gate is active (non-zero)
|
|
156
178
|
prob_active = torch.sigmoid(logits_shifted)
|
|
@@ -166,9 +188,8 @@ class HardConcrete(nn.Module):
|
|
|
166
188
|
torch.Tensor
|
|
167
189
|
Probability of each gate being non-zero
|
|
168
190
|
"""
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
191
|
+
logits = self.qz_logits.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
192
|
+
logits_shifted = logits - self.temperature * math.log(-self.gamma / self.zeta)
|
|
172
193
|
return torch.sigmoid(logits_shifted)
|
|
173
194
|
|
|
174
195
|
def get_sparsity(self) -> float:
|
l0/gates.py
CHANGED
|
@@ -130,9 +130,7 @@ class SampleGate(L0Gate):
|
|
|
130
130
|
self.n_samples = n_samples
|
|
131
131
|
self.target_samples = target_samples
|
|
132
132
|
|
|
133
|
-
def select_samples(
|
|
134
|
-
self, data: torch.Tensor
|
|
135
|
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
133
|
+
def select_samples(self, data: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
136
134
|
"""
|
|
137
135
|
Select samples from data.
|
|
138
136
|
|
|
@@ -219,9 +217,7 @@ class FeatureGate(L0Gate):
|
|
|
219
217
|
self.n_features = n_features
|
|
220
218
|
self.max_features = max_features
|
|
221
219
|
|
|
222
|
-
def select_features(
|
|
223
|
-
self, data: torch.Tensor
|
|
224
|
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
220
|
+
def select_features(self, data: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
|
225
221
|
"""
|
|
226
222
|
Select features from data.
|
|
227
223
|
|
|
@@ -377,9 +373,7 @@ class HybridGate(nn.Module):
|
|
|
377
373
|
|
|
378
374
|
# Track selection type
|
|
379
375
|
# Create initial selection type array
|
|
380
|
-
_ = np.array(
|
|
381
|
-
["l0"] * len(l0_indices) + ["random"] * len(random_indices)
|
|
382
|
-
)
|
|
376
|
+
_ = np.array(["l0"] * len(l0_indices) + ["random"] * len(random_indices))
|
|
383
377
|
|
|
384
378
|
# Reorder selection_type to match sorted indices
|
|
385
379
|
_ = torch.cat(
|
l0/layers.py
CHANGED
|
@@ -222,9 +222,7 @@ class L0Conv2d(nn.Module):
|
|
|
222
222
|
gates = gates.view(self.weight.shape)
|
|
223
223
|
masked_weight = self.weight * gates
|
|
224
224
|
|
|
225
|
-
return F.conv2d(
|
|
226
|
-
input, masked_weight, self.bias, self.stride, self.padding
|
|
227
|
-
)
|
|
225
|
+
return F.conv2d(input, masked_weight, self.bias, self.stride, self.padding)
|
|
228
226
|
|
|
229
227
|
def get_l0_penalty(self) -> torch.Tensor:
|
|
230
228
|
"""Get L0 penalty."""
|
|
@@ -398,9 +396,7 @@ class SparseMLP(nn.Module):
|
|
|
398
396
|
"""Get sparsity statistics for all layers."""
|
|
399
397
|
stats = {}
|
|
400
398
|
for name, module in self.named_modules():
|
|
401
|
-
if hasattr(module, "get_sparsity") and hasattr(
|
|
402
|
-
module, "get_l0_penalty"
|
|
403
|
-
):
|
|
399
|
+
if hasattr(module, "get_sparsity") and hasattr(module, "get_l0_penalty"):
|
|
404
400
|
stats[name] = {
|
|
405
401
|
"sparsity": module.get_sparsity(),
|
|
406
402
|
"active_params": module.get_l0_penalty().item(),
|
l0/penalties.py
CHANGED
|
@@ -107,9 +107,7 @@ def get_sparsity_stats(model: nn.Module) -> dict[str, dict[str, Any]]:
|
|
|
107
107
|
stats = {}
|
|
108
108
|
|
|
109
109
|
for name, module in model.named_modules():
|
|
110
|
-
if hasattr(module, "get_sparsity") and hasattr(
|
|
111
|
-
module, "get_l0_penalty"
|
|
112
|
-
):
|
|
110
|
+
if hasattr(module, "get_sparsity") and hasattr(module, "get_l0_penalty"):
|
|
113
111
|
active_params = module.get_l0_penalty().item()
|
|
114
112
|
|
|
115
113
|
# Calculate total parameters
|
|
@@ -204,16 +202,12 @@ class TemperatureScheduler:
|
|
|
204
202
|
progress = epoch / self.anneal_epochs
|
|
205
203
|
|
|
206
204
|
if self.schedule == "linear":
|
|
207
|
-
temp = (
|
|
208
|
-
self.initial_temp
|
|
209
|
-
- (self.initial_temp - self.final_temp) * progress
|
|
210
|
-
)
|
|
205
|
+
temp = self.initial_temp - (self.initial_temp - self.final_temp) * progress
|
|
211
206
|
|
|
212
207
|
elif self.schedule == "exponential":
|
|
213
208
|
log_temp = (
|
|
214
209
|
np.log(self.initial_temp)
|
|
215
|
-
- (np.log(self.initial_temp) - np.log(self.final_temp))
|
|
216
|
-
* progress
|
|
210
|
+
- (np.log(self.initial_temp) - np.log(self.final_temp)) * progress
|
|
217
211
|
)
|
|
218
212
|
temp = np.exp(log_temp)
|
|
219
213
|
|
l0/sparse.py
CHANGED
|
@@ -7,7 +7,6 @@ sparse input matrices without converting them to dense format.
|
|
|
7
7
|
|
|
8
8
|
# ruff: noqa: N803 N806
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
import numpy as np
|
|
12
11
|
import torch
|
|
13
12
|
import torch.nn as nn
|
|
@@ -37,6 +36,12 @@ class SparseL0Linear(nn.Module):
|
|
|
37
36
|
Initial probability of keeping each feature
|
|
38
37
|
device : str or torch.device
|
|
39
38
|
Device to run computations on ('cpu' or 'cuda')
|
|
39
|
+
seed : int, optional
|
|
40
|
+
Seed for the RNG used by the ``log_alpha`` init. When set, two
|
|
41
|
+
models with the same inputs produce byte-identical initial
|
|
42
|
+
``log_alpha`` without the caller having to manage PyTorch's
|
|
43
|
+
global RNG. ``None`` (default) preserves legacy behaviour of
|
|
44
|
+
using the global RNG.
|
|
40
45
|
"""
|
|
41
46
|
|
|
42
47
|
def __init__(
|
|
@@ -48,6 +53,7 @@ class SparseL0Linear(nn.Module):
|
|
|
48
53
|
zeta: float = 1.1,
|
|
49
54
|
init_keep_prob: float = 0.5,
|
|
50
55
|
device: str | torch.device = "cpu",
|
|
56
|
+
seed: int | None = None,
|
|
51
57
|
):
|
|
52
58
|
super().__init__()
|
|
53
59
|
self.n_features = n_features
|
|
@@ -56,6 +62,15 @@ class SparseL0Linear(nn.Module):
|
|
|
56
62
|
self.gamma = gamma
|
|
57
63
|
self.zeta = zeta
|
|
58
64
|
self.device = torch.device(device)
|
|
65
|
+
self.seed = seed
|
|
66
|
+
|
|
67
|
+
if seed is not None:
|
|
68
|
+
self._generator: torch.Generator | None = torch.Generator(
|
|
69
|
+
device=self.device
|
|
70
|
+
)
|
|
71
|
+
self._generator.manual_seed(int(seed))
|
|
72
|
+
else:
|
|
73
|
+
self._generator = None
|
|
59
74
|
|
|
60
75
|
# Model parameters
|
|
61
76
|
self.weight = nn.Parameter(torch.zeros(n_features, device=self.device))
|
|
@@ -64,21 +79,16 @@ class SparseL0Linear(nn.Module):
|
|
|
64
79
|
else:
|
|
65
80
|
self.register_parameter("bias", None)
|
|
66
81
|
|
|
67
|
-
# L0 gate parameters
|
|
82
|
+
# L0 gate parameters: mu + N(0, 0.01) jitter, optionally seeded.
|
|
68
83
|
mu = torch.log(torch.tensor(init_keep_prob / (1 - init_keep_prob)))
|
|
69
|
-
self.
|
|
70
|
-
|
|
71
|
-
mu.item(), 0.01, size=(n_features,), device=self.device
|
|
72
|
-
)
|
|
73
|
-
)
|
|
84
|
+
noise = torch.randn(n_features, generator=self._generator, device=self.device)
|
|
85
|
+
self.log_alpha = nn.Parameter(mu.item() + 0.01 * noise)
|
|
74
86
|
|
|
75
87
|
# Cache for sparse tensor conversion
|
|
76
88
|
self._cached_X_torch: torch.sparse.Tensor | None = None
|
|
77
89
|
self._cached_X_shape: tuple[int, int] | None = None
|
|
78
90
|
|
|
79
|
-
def _convert_sparse_to_torch(
|
|
80
|
-
self, X_sparse: sp.spmatrix
|
|
81
|
-
) -> torch.sparse.Tensor:
|
|
91
|
+
def _convert_sparse_to_torch(self, X_sparse: sp.spmatrix) -> torch.sparse.Tensor:
|
|
82
92
|
"""
|
|
83
93
|
Convert scipy sparse matrix to torch sparse tensor.
|
|
84
94
|
|
|
@@ -92,9 +102,7 @@ class SparseL0Linear(nn.Module):
|
|
|
92
102
|
return self._cached_X_torch
|
|
93
103
|
|
|
94
104
|
X_coo = X_sparse.tocoo()
|
|
95
|
-
indices = torch.LongTensor(np.vstack([X_coo.row, X_coo.col])).to(
|
|
96
|
-
self.device
|
|
97
|
-
)
|
|
105
|
+
indices = torch.LongTensor(np.vstack([X_coo.row, X_coo.col])).to(self.device)
|
|
98
106
|
values = torch.FloatTensor(X_coo.data).to(self.device)
|
|
99
107
|
X_torch = torch.sparse_coo_tensor(
|
|
100
108
|
indices,
|
|
@@ -110,18 +118,26 @@ class SparseL0Linear(nn.Module):
|
|
|
110
118
|
|
|
111
119
|
return X_torch
|
|
112
120
|
|
|
121
|
+
# Bounds for `log_alpha` before scaling by `beta`. Values outside
|
|
122
|
+
# ``[-LOG_ALPHA_BOUND, LOG_ALPHA_BOUND]`` saturate the sigmoid and
|
|
123
|
+
# vanish gradients; clamping keeps sampling and deterministic gates
|
|
124
|
+
# well-defined under fp16/bf16.
|
|
125
|
+
LOG_ALPHA_BOUND: float = 20.0
|
|
126
|
+
|
|
113
127
|
def _sample_gates(self) -> torch.Tensor:
|
|
114
128
|
"""Sample gates using Hard Concrete distribution."""
|
|
115
129
|
eps = 1e-6
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
131
|
+
u = torch.rand_like(log_alpha).clamp(eps, 1 - eps)
|
|
132
|
+
X = (torch.log(u) - torch.log(1 - u) + log_alpha) / self.beta
|
|
118
133
|
s = torch.sigmoid(X)
|
|
119
134
|
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
120
135
|
return s_bar.clamp(0, 1)
|
|
121
136
|
|
|
122
137
|
def get_deterministic_gates(self) -> torch.Tensor:
|
|
123
138
|
"""Get deterministic gate values (for inference)."""
|
|
124
|
-
|
|
139
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
140
|
+
X = log_alpha / self.beta
|
|
125
141
|
s = torch.sigmoid(X)
|
|
126
142
|
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
127
143
|
return s_bar.clamp(0, 1)
|
|
@@ -178,7 +194,8 @@ class SparseL0Linear(nn.Module):
|
|
|
178
194
|
c = -self.beta * torch.log(
|
|
179
195
|
torch.tensor(-self.gamma / self.zeta, device=self.device)
|
|
180
196
|
)
|
|
181
|
-
|
|
197
|
+
log_alpha = self.log_alpha.clamp(-self.LOG_ALPHA_BOUND, self.LOG_ALPHA_BOUND)
|
|
198
|
+
pi = torch.sigmoid(log_alpha + c)
|
|
182
199
|
return pi.sum()
|
|
183
200
|
|
|
184
201
|
def get_sparsity(self) -> float:
|
|
@@ -291,7 +308,7 @@ class SparseL0Linear(nn.Module):
|
|
|
291
308
|
with torch.no_grad():
|
|
292
309
|
n_active = (self.get_deterministic_gates() > 0.01).sum()
|
|
293
310
|
print(
|
|
294
|
-
f"Epoch {epoch+1:4d}: "
|
|
311
|
+
f"Epoch {epoch + 1:4d}: "
|
|
295
312
|
f"loss={loss.item():.4f}, "
|
|
296
313
|
f"mse={mse_loss.item():.4f}, "
|
|
297
314
|
f"l0={l0_loss.item():.2f}, "
|
|
@@ -300,9 +317,7 @@ class SparseL0Linear(nn.Module):
|
|
|
300
317
|
|
|
301
318
|
return self
|
|
302
319
|
|
|
303
|
-
def predict(
|
|
304
|
-
self, X_sparse: sp.spmatrix | torch.sparse.Tensor
|
|
305
|
-
) -> torch.Tensor:
|
|
320
|
+
def predict(self, X_sparse: sp.spmatrix | torch.sparse.Tensor) -> torch.Tensor:
|
|
306
321
|
"""
|
|
307
322
|
Make predictions on new data.
|
|
308
323
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: l0-python
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -45,7 +45,7 @@ Requires-Dist: scipy>=1.7.0
|
|
|
45
45
|
Provides-Extra: dev
|
|
46
46
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
47
47
|
Requires-Dist: pytest-cov>=3.0; extra == "dev"
|
|
48
|
-
Requires-Dist:
|
|
48
|
+
Requires-Dist: ruff>=0.9.0; extra == "dev"
|
|
49
49
|
Requires-Dist: build>=1.3; extra == "dev"
|
|
50
50
|
Requires-Dist: scikit-learn>=1.0; extra == "dev"
|
|
51
51
|
Requires-Dist: yaml-changelog>=0.3.0; extra == "dev"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
l0/__init__.py,sha256=ByyYJoiHgx0c32P0VxSaV8gMWnEaWsNlr69Nu9ydgi0,1292
|
|
2
|
+
l0/calibration.py,sha256=U_w_oteOKMEavagMy2TiR8binHZbRhgbOvt01r0LJNA,22957
|
|
3
|
+
l0/distributions.py,sha256=My-d-IXwj0i-H2LZme1EUVqzV54VC7rtQLwQ5gFNx7I,7758
|
|
4
|
+
l0/gates.py,sha256=D5x6M4-mVykKnaLYZSf4mG3cWCGdXacN4ry8KIepC-A,11393
|
|
5
|
+
l0/layers.py,sha256=wBYcykrzVD2ZrkOpvDwlTwpglLKJwONc5M8JAC7uiNs,13733
|
|
6
|
+
l0/penalties.py,sha256=h1cKZtY4-h_IpFnPzbDI-3nhwrlqmJArTGGbqEAc6is,9483
|
|
7
|
+
l0/sparse.py,sha256=zKNB_2q9dyN9z7RkLTmZ29xNNyBPXMkCR1fGHKCp58E,10871
|
|
8
|
+
l0_python-0.6.2.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
|
|
9
|
+
l0_python-0.6.2.dist-info/METADATA,sha256=JXbdK2cxPFow8ybTrgJ3UYvhY0SpRYFqRErQgfz1BbU,7144
|
|
10
|
+
l0_python-0.6.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
11
|
+
l0_python-0.6.2.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
|
|
12
|
+
l0_python-0.6.2.dist-info/RECORD,,
|
l0_python-0.6.1.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
l0/__init__.py,sha256=ImRgZfVg-RJhLoqNaLc8ysjddZ7CYGHJPJbRUyfSoHY,1192
|
|
2
|
-
l0/calibration.py,sha256=xOSkuqU72rfFV8OJi-OntYGcPwqabDGrQ12VHBZqNK0,21514
|
|
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.6.1.dist-info/licenses/LICENSE,sha256=LoyFJ8qmpwWPp5RmxFGHhdBxsemvp_BqGyiCgNAZHlk,1069
|
|
9
|
-
l0_python-0.6.1.dist-info/METADATA,sha256=kf7gdRFBO8E9hIbx3w80qDG8xBP8fkOipr8wJ_Nn310,7144
|
|
10
|
-
l0_python-0.6.1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
11
|
-
l0_python-0.6.1.dist-info/top_level.txt,sha256=brP2SoElgmGhNJk-qRFEQZ5UE-d1e87Tr60hSSh8n34,3
|
|
12
|
-
l0_python-0.6.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|