l0-python 0.1.0__tar.gz → 0.3.0__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.1.0 → l0_python-0.3.0}/PKG-INFO +5 -4
- {l0_python-0.1.0 → l0_python-0.3.0}/l0/__init__.py +3 -0
- l0_python-0.3.0/l0/calibration.py +436 -0
- l0_python-0.3.0/l0/sparse.py +321 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0_python.egg-info/PKG-INFO +5 -4
- {l0_python-0.1.0 → l0_python-0.3.0}/l0_python.egg-info/SOURCES.txt +5 -1
- {l0_python-0.1.0 → l0_python-0.3.0}/l0_python.egg-info/requires.txt +4 -3
- {l0_python-0.1.0 → l0_python-0.3.0}/pyproject.toml +5 -20
- l0_python-0.3.0/tests/test_calibration.py +423 -0
- l0_python-0.3.0/tests/test_sparse.py +240 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/LICENSE +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/README.md +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0/distributions.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0/gates.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0/layers.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0/penalties.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0_python.egg-info/dependency_links.txt +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/l0_python.egg-info/top_level.txt +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/setup.cfg +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/tests/test_distributions.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/tests/test_gates.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/tests/test_layers.py +0 -0
- {l0_python-0.1.0 → l0_python-0.3.0}/tests/test_penalties.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: l0-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -41,14 +41,15 @@ Description-Content-Type: text/markdown
|
|
|
41
41
|
License-File: LICENSE
|
|
42
42
|
Requires-Dist: torch>=2.0.0
|
|
43
43
|
Requires-Dist: numpy>=1.20.0
|
|
44
|
+
Requires-Dist: scipy>=1.7.0
|
|
44
45
|
Provides-Extra: dev
|
|
45
46
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
46
47
|
Requires-Dist: pytest-cov>=3.0; extra == "dev"
|
|
47
48
|
Requires-Dist: black>=22.0; extra == "dev"
|
|
48
|
-
Requires-Dist: mypy>=0.900; extra == "dev"
|
|
49
|
-
Requires-Dist: ruff>=0.0.280; extra == "dev"
|
|
50
|
-
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
51
49
|
Requires-Dist: build>=1.3; extra == "dev"
|
|
50
|
+
Requires-Dist: scikit-learn>=1.0; extra == "dev"
|
|
51
|
+
Requires-Dist: yaml-changelog>=0.3.0; extra == "dev"
|
|
52
|
+
Requires-Dist: twine>=4.0.0; extra == "dev"
|
|
52
53
|
Provides-Extra: docs
|
|
53
54
|
Requires-Dist: mystmd>=1.3; extra == "docs"
|
|
54
55
|
Requires-Dist: jupyter>=1.0; extra == "docs"
|
|
@@ -26,6 +26,7 @@ from .penalties import (
|
|
|
26
26
|
get_sparsity_stats,
|
|
27
27
|
update_temperatures,
|
|
28
28
|
)
|
|
29
|
+
from .sparse import SparseL0Linear
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
31
32
|
# Distributions
|
|
@@ -36,6 +37,8 @@ __all__ = [
|
|
|
36
37
|
"L0DepthwiseConv2d",
|
|
37
38
|
"SparseMLP",
|
|
38
39
|
"prune_model",
|
|
40
|
+
# Sparse
|
|
41
|
+
"SparseL0Linear",
|
|
39
42
|
# Gates
|
|
40
43
|
"L0Gate",
|
|
41
44
|
"SampleGate",
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""
|
|
2
|
+
L0-regularized calibration weights for survey data.
|
|
3
|
+
|
|
4
|
+
Implements sparse calibration weights that are constrained to be non-negative,
|
|
5
|
+
suitable for survey weighting and importance sampling applications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# ruff: noqa: N803 N806
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy import sparse as sp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SparseCalibrationWeights(nn.Module):
|
|
17
|
+
"""
|
|
18
|
+
L0-regularized calibration weights with positivity constraint.
|
|
19
|
+
|
|
20
|
+
Designed for survey calibration where weights must be non-negative
|
|
21
|
+
and sparsity is desired to select a subset of households/units.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
n_features : int
|
|
26
|
+
Number of calibration weights (e.g., number of households)
|
|
27
|
+
beta : float
|
|
28
|
+
Temperature parameter for Hard Concrete distribution
|
|
29
|
+
gamma : float
|
|
30
|
+
Lower bound of stretched concrete distribution
|
|
31
|
+
zeta : float
|
|
32
|
+
Upper bound of stretched concrete distribution
|
|
33
|
+
init_keep_prob : float
|
|
34
|
+
Initial probability of keeping each weight active
|
|
35
|
+
init_weight_scale : float
|
|
36
|
+
Initial scale for log weights (controls initial weight magnitude)
|
|
37
|
+
device : str or torch.device
|
|
38
|
+
Device to run computations on ('cpu' or 'cuda')
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
n_features: int,
|
|
44
|
+
beta: float = 2 / 3,
|
|
45
|
+
gamma: float = -0.1,
|
|
46
|
+
zeta: float = 1.1,
|
|
47
|
+
init_keep_prob: float = 0.5,
|
|
48
|
+
init_weight_scale: float = 1.0,
|
|
49
|
+
device: str | torch.device = "cpu",
|
|
50
|
+
):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.n_features = n_features
|
|
53
|
+
self.beta = beta
|
|
54
|
+
self.gamma = gamma
|
|
55
|
+
self.zeta = zeta
|
|
56
|
+
self.device = torch.device(device)
|
|
57
|
+
|
|
58
|
+
# Log weights to ensure positivity via exp transformation
|
|
59
|
+
self.log_weight = nn.Parameter(
|
|
60
|
+
torch.normal(
|
|
61
|
+
mean=0.0,
|
|
62
|
+
std=init_weight_scale,
|
|
63
|
+
size=(n_features,),
|
|
64
|
+
device=self.device,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# L0 gate parameters
|
|
69
|
+
mu = torch.log(torch.tensor(init_keep_prob / (1 - init_keep_prob)))
|
|
70
|
+
self.log_alpha = nn.Parameter(
|
|
71
|
+
torch.normal(
|
|
72
|
+
mu.item(), 0.01, size=(n_features,), device=self.device
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Cache for sparse tensor conversion
|
|
77
|
+
self._cached_M_torch: torch.sparse.Tensor | None = None
|
|
78
|
+
self._cached_M_shape: tuple[int, int] | None = None
|
|
79
|
+
|
|
80
|
+
def _convert_sparse_to_torch(
|
|
81
|
+
self, M_sparse: sp.spmatrix
|
|
82
|
+
) -> torch.sparse.Tensor:
|
|
83
|
+
"""
|
|
84
|
+
Convert scipy sparse matrix to torch sparse tensor.
|
|
85
|
+
|
|
86
|
+
Caches the result if the shape matches to avoid redundant conversions.
|
|
87
|
+
"""
|
|
88
|
+
if (
|
|
89
|
+
self._cached_M_torch is not None
|
|
90
|
+
and self._cached_M_shape == M_sparse.shape
|
|
91
|
+
and M_sparse.nnz == self._cached_M_torch._nnz()
|
|
92
|
+
):
|
|
93
|
+
return self._cached_M_torch
|
|
94
|
+
|
|
95
|
+
M_coo = M_sparse.tocoo()
|
|
96
|
+
indices = torch.LongTensor(np.vstack([M_coo.row, M_coo.col])).to(
|
|
97
|
+
self.device
|
|
98
|
+
)
|
|
99
|
+
values = torch.FloatTensor(M_coo.data).to(self.device)
|
|
100
|
+
M_torch = torch.sparse_coo_tensor(
|
|
101
|
+
indices,
|
|
102
|
+
values,
|
|
103
|
+
M_sparse.shape,
|
|
104
|
+
dtype=torch.float32,
|
|
105
|
+
device=self.device,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Cache for future use
|
|
109
|
+
self._cached_M_torch = M_torch
|
|
110
|
+
self._cached_M_shape = M_sparse.shape
|
|
111
|
+
|
|
112
|
+
return M_torch
|
|
113
|
+
|
|
114
|
+
def _sample_gates(self) -> torch.Tensor:
|
|
115
|
+
"""Sample gates using Hard Concrete distribution."""
|
|
116
|
+
eps = 1e-6
|
|
117
|
+
u = torch.rand_like(self.log_alpha).clamp(eps, 1 - eps)
|
|
118
|
+
s = (torch.log(u) - torch.log(1 - u) + self.log_alpha) / self.beta
|
|
119
|
+
s = torch.sigmoid(s)
|
|
120
|
+
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
121
|
+
return s_bar.clamp(0, 1)
|
|
122
|
+
|
|
123
|
+
def get_deterministic_gates(self) -> torch.Tensor:
|
|
124
|
+
"""Get deterministic gate values (for inference)."""
|
|
125
|
+
s = torch.sigmoid(self.log_alpha / self.beta)
|
|
126
|
+
s_bar = s * (self.zeta - self.gamma) + self.gamma
|
|
127
|
+
return s_bar.clamp(0, 1)
|
|
128
|
+
|
|
129
|
+
def get_weights(self, deterministic: bool = False) -> torch.Tensor:
|
|
130
|
+
"""
|
|
131
|
+
Get effective calibration weights.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
deterministic : bool
|
|
136
|
+
Whether to use deterministic gates (for inference)
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
torch.Tensor
|
|
141
|
+
Positive calibration weights with L0 sparsity applied
|
|
142
|
+
"""
|
|
143
|
+
# Sample or get deterministic gates
|
|
144
|
+
if deterministic:
|
|
145
|
+
gates = self.get_deterministic_gates()
|
|
146
|
+
else:
|
|
147
|
+
gates = self._sample_gates()
|
|
148
|
+
|
|
149
|
+
# Apply exp transformation and gates together
|
|
150
|
+
# This ensures gradient flow matches successful implementations
|
|
151
|
+
return torch.exp(self.log_weight) * gates
|
|
152
|
+
|
|
153
|
+
def forward(
|
|
154
|
+
self,
|
|
155
|
+
M: sp.spmatrix,
|
|
156
|
+
deterministic: bool = False,
|
|
157
|
+
) -> torch.Tensor:
|
|
158
|
+
"""
|
|
159
|
+
Apply calibration weights to metric matrix.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
M : scipy.sparse matrix
|
|
164
|
+
Metric matrix of shape (n_targets, n_features)
|
|
165
|
+
deterministic : bool
|
|
166
|
+
Whether to use deterministic gates (for inference)
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
torch.Tensor
|
|
171
|
+
Weighted sums of shape (n_targets,)
|
|
172
|
+
"""
|
|
173
|
+
# Convert sparse matrix to torch
|
|
174
|
+
M_torch = self._convert_sparse_to_torch(M)
|
|
175
|
+
|
|
176
|
+
# Get effective weights
|
|
177
|
+
weights = self.get_weights(deterministic=deterministic)
|
|
178
|
+
|
|
179
|
+
# Sparse matrix multiplication
|
|
180
|
+
y = torch.sparse.mm(M_torch, weights.unsqueeze(1)).squeeze(1)
|
|
181
|
+
|
|
182
|
+
return y
|
|
183
|
+
|
|
184
|
+
def get_l0_penalty(self) -> torch.Tensor:
|
|
185
|
+
"""
|
|
186
|
+
Compute L0 complexity penalty.
|
|
187
|
+
|
|
188
|
+
Returns expected number of active weights.
|
|
189
|
+
"""
|
|
190
|
+
c = -self.beta * torch.log(
|
|
191
|
+
torch.tensor(-self.gamma / self.zeta, device=self.device)
|
|
192
|
+
)
|
|
193
|
+
pi = torch.sigmoid(self.log_alpha + c)
|
|
194
|
+
return pi.sum()
|
|
195
|
+
|
|
196
|
+
def get_l2_penalty(self) -> torch.Tensor:
|
|
197
|
+
"""
|
|
198
|
+
Compute L2 penalty on positive weights.
|
|
199
|
+
|
|
200
|
+
Helps prevent weight explosion when gates are partially open.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
torch.Tensor
|
|
205
|
+
L2 norm of positive weights
|
|
206
|
+
"""
|
|
207
|
+
positive_weights = torch.exp(self.log_weight)
|
|
208
|
+
return (positive_weights**2).sum()
|
|
209
|
+
|
|
210
|
+
def get_sparsity(self) -> float:
|
|
211
|
+
"""
|
|
212
|
+
Get current sparsity level.
|
|
213
|
+
|
|
214
|
+
Returns
|
|
215
|
+
-------
|
|
216
|
+
float
|
|
217
|
+
Fraction of weights that are effectively zero
|
|
218
|
+
"""
|
|
219
|
+
with torch.no_grad():
|
|
220
|
+
gates = self.get_deterministic_gates()
|
|
221
|
+
return (gates == 0).float().mean().item()
|
|
222
|
+
|
|
223
|
+
def get_active_weights(self) -> dict:
|
|
224
|
+
"""
|
|
225
|
+
Get indices and values of active (non-zero) weights.
|
|
226
|
+
|
|
227
|
+
Returns
|
|
228
|
+
-------
|
|
229
|
+
dict
|
|
230
|
+
Dictionary with 'indices' and 'values' of active weights
|
|
231
|
+
"""
|
|
232
|
+
with torch.no_grad():
|
|
233
|
+
weights = self.get_weights(deterministic=True)
|
|
234
|
+
active_mask = weights > 0
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"indices": torch.where(active_mask)[0],
|
|
238
|
+
"values": weights[active_mask],
|
|
239
|
+
"count": active_mask.sum().item(),
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
def fit(
|
|
243
|
+
self,
|
|
244
|
+
M: sp.spmatrix,
|
|
245
|
+
y: np.ndarray,
|
|
246
|
+
lambda_l0: float = 0.01,
|
|
247
|
+
lambda_l2: float = 0.0,
|
|
248
|
+
lr: float = 0.01,
|
|
249
|
+
epochs: int = 1000,
|
|
250
|
+
loss_type: str = "mse",
|
|
251
|
+
verbose: bool = False,
|
|
252
|
+
verbose_freq: int = 100,
|
|
253
|
+
target_groups: np.ndarray | None = None,
|
|
254
|
+
) -> "SparseCalibrationWeights":
|
|
255
|
+
"""
|
|
256
|
+
Fit calibration weights using gradient descent.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
M : scipy.sparse matrix
|
|
261
|
+
Metric matrix of shape (n_targets, n_features)
|
|
262
|
+
y : numpy.ndarray
|
|
263
|
+
Target values to match
|
|
264
|
+
lambda_l0 : float
|
|
265
|
+
L0 regularization strength
|
|
266
|
+
lambda_l2 : float
|
|
267
|
+
L2 regularization strength on positive weights
|
|
268
|
+
lr : float
|
|
269
|
+
Learning rate
|
|
270
|
+
epochs : int
|
|
271
|
+
Number of training epochs
|
|
272
|
+
loss_type : str
|
|
273
|
+
Type of loss function: 'mse' or 'relative'
|
|
274
|
+
verbose : bool
|
|
275
|
+
Whether to print progress
|
|
276
|
+
verbose_freq : int
|
|
277
|
+
How often to print progress
|
|
278
|
+
target_groups : numpy.ndarray, optional
|
|
279
|
+
Array of group IDs for each target. Targets in the same group
|
|
280
|
+
will be averaged together so each group contributes equally to loss.
|
|
281
|
+
If None, all targets are treated independently.
|
|
282
|
+
|
|
283
|
+
Returns
|
|
284
|
+
-------
|
|
285
|
+
self
|
|
286
|
+
Fitted model
|
|
287
|
+
"""
|
|
288
|
+
# Convert y to tensor
|
|
289
|
+
y = torch.tensor(y, dtype=torch.float32, device=self.device)
|
|
290
|
+
|
|
291
|
+
# Convert M to torch sparse (will be cached)
|
|
292
|
+
M_torch = self._convert_sparse_to_torch(M)
|
|
293
|
+
|
|
294
|
+
# Compute group weights for loss averaging
|
|
295
|
+
if target_groups is not None:
|
|
296
|
+
# Convert to tensor
|
|
297
|
+
target_groups = torch.tensor(
|
|
298
|
+
target_groups, dtype=torch.long, device=self.device
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Calculate group weights: 1 / group_size for each target
|
|
302
|
+
unique_groups = torch.unique(target_groups)
|
|
303
|
+
group_weights = torch.zeros_like(y)
|
|
304
|
+
|
|
305
|
+
for group_id in unique_groups:
|
|
306
|
+
group_mask = target_groups == group_id
|
|
307
|
+
group_size = group_mask.sum().item()
|
|
308
|
+
# Each target in the group gets weight 1/group_size
|
|
309
|
+
# so the group's total contribution is 1
|
|
310
|
+
group_weights[group_mask] = 1.0 / group_size
|
|
311
|
+
else:
|
|
312
|
+
# No grouping - all targets weighted equally
|
|
313
|
+
group_weights = torch.ones_like(y)
|
|
314
|
+
|
|
315
|
+
# Initialize weights
|
|
316
|
+
nn.init.normal_(self.log_weight, 0, 0.5)
|
|
317
|
+
|
|
318
|
+
# Setup optimizer
|
|
319
|
+
optimizer = torch.optim.Adam([self.log_weight, self.log_alpha], lr=lr)
|
|
320
|
+
|
|
321
|
+
# Training loop
|
|
322
|
+
for epoch in range(epochs):
|
|
323
|
+
# Forward pass
|
|
324
|
+
y_pred = self.forward(M, deterministic=False)
|
|
325
|
+
|
|
326
|
+
# Compute loss with group weighting
|
|
327
|
+
if loss_type == "relative":
|
|
328
|
+
# Relative error: (y - y_pred)^2 / (y + 1)^2
|
|
329
|
+
# Adding 1 to avoid division by zero
|
|
330
|
+
relative_errors = (y - y_pred) / (y + 1)
|
|
331
|
+
# Apply group weights and then average
|
|
332
|
+
weighted_squared_errors = (
|
|
333
|
+
relative_errors.pow(2) * group_weights
|
|
334
|
+
)
|
|
335
|
+
data_loss = (
|
|
336
|
+
weighted_squared_errors.sum()
|
|
337
|
+
) # Sum because weights already normalize
|
|
338
|
+
else:
|
|
339
|
+
# Standard MSE with group weighting
|
|
340
|
+
squared_errors = (y - y_pred).pow(2)
|
|
341
|
+
weighted_squared_errors = squared_errors * group_weights
|
|
342
|
+
data_loss = (
|
|
343
|
+
weighted_squared_errors.sum()
|
|
344
|
+
) # Sum because weights already normalize
|
|
345
|
+
|
|
346
|
+
l0_loss = self.get_l0_penalty()
|
|
347
|
+
loss = data_loss + lambda_l0 * l0_loss
|
|
348
|
+
|
|
349
|
+
# Add L2 penalty if specified
|
|
350
|
+
if lambda_l2 > 0:
|
|
351
|
+
l2_loss = self.get_l2_penalty()
|
|
352
|
+
loss = loss + lambda_l2 * l2_loss
|
|
353
|
+
|
|
354
|
+
# Backward pass
|
|
355
|
+
optimizer.zero_grad()
|
|
356
|
+
loss.backward()
|
|
357
|
+
optimizer.step()
|
|
358
|
+
|
|
359
|
+
# Print progress
|
|
360
|
+
if verbose and (epoch + 1) % verbose_freq == 0:
|
|
361
|
+
with torch.no_grad():
|
|
362
|
+
active_info = self.get_active_weights()
|
|
363
|
+
weights = self.get_weights(deterministic=True)
|
|
364
|
+
active_weights = weights[weights > 0]
|
|
365
|
+
|
|
366
|
+
# Compute relative errors for meaningful output
|
|
367
|
+
y_det = self.forward(M, deterministic=True)
|
|
368
|
+
if loss_type == "relative":
|
|
369
|
+
rel_errors = torch.abs((y - y_det) / (y + 1))
|
|
370
|
+
else:
|
|
371
|
+
# For MSE, show relative errors anyway for interpretability
|
|
372
|
+
rel_errors = torch.abs((y - y_det) / (y + 1))
|
|
373
|
+
|
|
374
|
+
# For reporting, we can show both overall and group-averaged errors
|
|
375
|
+
mean_rel_err = rel_errors.mean().item()
|
|
376
|
+
max_rel_err = rel_errors.max().item()
|
|
377
|
+
|
|
378
|
+
# Compute mean group loss if groups are used
|
|
379
|
+
if target_groups is not None:
|
|
380
|
+
# Calculate mean loss per group
|
|
381
|
+
group_losses = []
|
|
382
|
+
for group_id in torch.unique(target_groups):
|
|
383
|
+
group_mask = target_groups == group_id
|
|
384
|
+
group_mean_err = (
|
|
385
|
+
rel_errors[group_mask].mean().item()
|
|
386
|
+
)
|
|
387
|
+
group_losses.append(group_mean_err)
|
|
388
|
+
mean_group_loss = np.mean(group_losses)
|
|
389
|
+
else:
|
|
390
|
+
mean_group_loss = mean_rel_err
|
|
391
|
+
|
|
392
|
+
# Calculate sparsity percentage
|
|
393
|
+
sparsity_pct = 100 * (
|
|
394
|
+
1 - active_info["count"] / self.n_features
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Calculate components of the actual loss being minimized
|
|
398
|
+
actual_data_loss = data_loss.item()
|
|
399
|
+
actual_l0_loss = l0_loss.item()
|
|
400
|
+
actual_total_loss = loss.item()
|
|
401
|
+
|
|
402
|
+
if target_groups is not None:
|
|
403
|
+
print(
|
|
404
|
+
f"Epoch {epoch+1:4d}: "
|
|
405
|
+
f"mean_group_loss={mean_group_loss:.1%}, "
|
|
406
|
+
f"max_error={max_rel_err:.1%}, "
|
|
407
|
+
f"total_loss={actual_total_loss:.3f}, "
|
|
408
|
+
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
print(
|
|
412
|
+
f"Epoch {epoch+1:4d}: "
|
|
413
|
+
f"mean_error={mean_rel_err:.1%}, "
|
|
414
|
+
f"max_error={max_rel_err:.1%}, "
|
|
415
|
+
f"total_loss={actual_total_loss:.3f}, "
|
|
416
|
+
f"active={active_info['count']:4d}/{self.n_features} ({sparsity_pct:.1f}% sparse)"
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
return self
|
|
420
|
+
|
|
421
|
+
def predict(self, M: sp.spmatrix) -> torch.Tensor:
|
|
422
|
+
"""
|
|
423
|
+
Apply calibration weights to new data.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
M : scipy.sparse matrix
|
|
428
|
+
Metric matrix of shape (n_targets, n_features)
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
torch.Tensor
|
|
433
|
+
Calibrated predictions
|
|
434
|
+
"""
|
|
435
|
+
with torch.no_grad():
|
|
436
|
+
return self.forward(M, deterministic=True)
|