l0-python 0.1.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 +53 -0
- l0/distributions.py +205 -0
- l0/gates.py +407 -0
- l0/layers.py +455 -0
- l0/penalties.py +381 -0
- l0_python-0.1.0.dist-info/METADATA +215 -0
- l0_python-0.1.0.dist-info/RECORD +10 -0
- l0_python-0.1.0.dist-info/WHEEL +5 -0
- l0_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- l0_python-0.1.0.dist-info/top_level.txt +1 -0
l0/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
L0 Regularization Package
|
|
3
|
+
|
|
4
|
+
A PyTorch implementation of L0 regularization for neural network sparsification
|
|
5
|
+
and intelligent sampling, based on Louizos, Welling, & Kingma (2017).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
from .distributions import HardConcrete
|
|
11
|
+
from .gates import FeatureGate, HybridGate, L0Gate, SampleGate
|
|
12
|
+
from .layers import (
|
|
13
|
+
L0Conv2d,
|
|
14
|
+
L0DepthwiseConv2d,
|
|
15
|
+
L0Linear,
|
|
16
|
+
SparseMLP,
|
|
17
|
+
prune_model,
|
|
18
|
+
)
|
|
19
|
+
from .penalties import (
|
|
20
|
+
PenaltyTracker,
|
|
21
|
+
TemperatureScheduler,
|
|
22
|
+
compute_l0_penalty,
|
|
23
|
+
compute_l0l2_penalty,
|
|
24
|
+
compute_l2_penalty,
|
|
25
|
+
get_active_parameter_count,
|
|
26
|
+
get_sparsity_stats,
|
|
27
|
+
update_temperatures,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# Distributions
|
|
32
|
+
"HardConcrete",
|
|
33
|
+
# Layers
|
|
34
|
+
"L0Linear",
|
|
35
|
+
"L0Conv2d",
|
|
36
|
+
"L0DepthwiseConv2d",
|
|
37
|
+
"SparseMLP",
|
|
38
|
+
"prune_model",
|
|
39
|
+
# Gates
|
|
40
|
+
"L0Gate",
|
|
41
|
+
"SampleGate",
|
|
42
|
+
"FeatureGate",
|
|
43
|
+
"HybridGate",
|
|
44
|
+
# Penalties
|
|
45
|
+
"compute_l0_penalty",
|
|
46
|
+
"compute_l2_penalty",
|
|
47
|
+
"compute_l0l2_penalty",
|
|
48
|
+
"get_sparsity_stats",
|
|
49
|
+
"get_active_parameter_count",
|
|
50
|
+
"TemperatureScheduler",
|
|
51
|
+
"update_temperatures",
|
|
52
|
+
"PenaltyTracker",
|
|
53
|
+
]
|
l0/distributions.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hard Concrete distribution for L0 regularization.
|
|
3
|
+
|
|
4
|
+
Based on Louizos, Welling, & Kingma (2017): "Learning Sparse Neural Networks
|
|
5
|
+
through L0 Regularization" (https://arxiv.org/abs/1712.01312)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HardConcrete(nn.Module):
|
|
15
|
+
"""
|
|
16
|
+
Hard Concrete distribution for L0 regularization.
|
|
17
|
+
|
|
18
|
+
This distribution enables differentiable approximation of the L0 norm
|
|
19
|
+
by using stochastic gates that can be optimized via gradient descent.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
input_dim : int
|
|
24
|
+
Size of the first dimension (e.g., number of samples or features)
|
|
25
|
+
output_dim : Optional[int]
|
|
26
|
+
Size of the second dimension for 2D gates (e.g., for weight matrices)
|
|
27
|
+
temperature : float
|
|
28
|
+
Temperature parameter controlling the hardness of the distribution.
|
|
29
|
+
Lower values make the distribution more discrete.
|
|
30
|
+
stretch : float
|
|
31
|
+
Stretch parameter for the hard concrete transformation
|
|
32
|
+
init_mean : float
|
|
33
|
+
Initial mean activation probability for the gates (0 to 1)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
input_dim: int,
|
|
39
|
+
output_dim: int | None = None,
|
|
40
|
+
temperature: float = 0.5,
|
|
41
|
+
stretch: float = 0.1,
|
|
42
|
+
init_mean: float = 0.5,
|
|
43
|
+
):
|
|
44
|
+
super().__init__()
|
|
45
|
+
|
|
46
|
+
# Set gate dimensions
|
|
47
|
+
if output_dim is None:
|
|
48
|
+
self.gate_size: tuple[int, ...] = (input_dim,)
|
|
49
|
+
else:
|
|
50
|
+
self.gate_size = (input_dim, output_dim)
|
|
51
|
+
|
|
52
|
+
# Learnable parameters - logits for each gate
|
|
53
|
+
self.qz_logits = nn.Parameter(torch.zeros(self.gate_size))
|
|
54
|
+
|
|
55
|
+
# Distribution parameters
|
|
56
|
+
self.temperature = temperature
|
|
57
|
+
self.stretch = stretch
|
|
58
|
+
self.gamma = -stretch # Lower bound after stretching
|
|
59
|
+
self.zeta = 1.0 + stretch # Upper bound after stretching
|
|
60
|
+
self.init_mean = init_mean
|
|
61
|
+
|
|
62
|
+
# Initialize parameters
|
|
63
|
+
self.reset_parameters()
|
|
64
|
+
|
|
65
|
+
def reset_parameters(self) -> None:
|
|
66
|
+
"""Initialize gate logits based on desired initial mean."""
|
|
67
|
+
if self.init_mean is not None:
|
|
68
|
+
# Convert mean probability to logit space
|
|
69
|
+
init_val = math.log(self.init_mean / (1 - self.init_mean))
|
|
70
|
+
self.qz_logits.data.fill_(init_val)
|
|
71
|
+
|
|
72
|
+
def forward(
|
|
73
|
+
self, input_shape: tuple[int, ...] | None = None
|
|
74
|
+
) -> torch.Tensor:
|
|
75
|
+
"""
|
|
76
|
+
Sample or compute gates.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
input_shape : Optional[Tuple[int, ...]]
|
|
81
|
+
Shape of the input tensor for broadcasting gates if needed
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
torch.Tensor
|
|
86
|
+
Gate values in [0, 1], either sampled (training) or deterministic (eval)
|
|
87
|
+
"""
|
|
88
|
+
if self.training:
|
|
89
|
+
gates = self._sample_gates()
|
|
90
|
+
else:
|
|
91
|
+
gates = self._deterministic_gates()
|
|
92
|
+
|
|
93
|
+
# Broadcast gates if needed for convolution layers
|
|
94
|
+
if input_shape is not None and len(input_shape) > len(gates.shape):
|
|
95
|
+
# Add dimensions for spatial dims (e.g., height, width in Conv2d)
|
|
96
|
+
for _ in range(len(input_shape) - len(gates.shape)):
|
|
97
|
+
gates = gates.unsqueeze(-1)
|
|
98
|
+
|
|
99
|
+
return gates
|
|
100
|
+
|
|
101
|
+
def _sample_gates(self) -> torch.Tensor:
|
|
102
|
+
"""
|
|
103
|
+
Sample gates using the reparameterization trick.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
torch.Tensor
|
|
108
|
+
Sampled gate values in [0, 1]
|
|
109
|
+
"""
|
|
110
|
+
# Sample uniform noise (avoiding exact 0 and 1 for numerical stability)
|
|
111
|
+
u = torch.zeros_like(self.qz_logits).uniform_(1e-8, 1.0 - 1e-8)
|
|
112
|
+
|
|
113
|
+
# Apply the concrete distribution transformation
|
|
114
|
+
# s = sigmoid((log(u) - log(1-u) + logits) / temperature)
|
|
115
|
+
s = torch.log(u) - torch.log(1 - u) + self.qz_logits
|
|
116
|
+
s = torch.sigmoid(s / self.temperature)
|
|
117
|
+
|
|
118
|
+
# Stretch and clamp to create hard concrete
|
|
119
|
+
s = s * (self.zeta - self.gamma) + self.gamma
|
|
120
|
+
gates = torch.clamp(s, 0, 1)
|
|
121
|
+
|
|
122
|
+
return gates
|
|
123
|
+
|
|
124
|
+
def _deterministic_gates(self) -> torch.Tensor:
|
|
125
|
+
"""
|
|
126
|
+
Compute deterministic gates for evaluation.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
torch.Tensor
|
|
131
|
+
Deterministic gate values in [0, 1]
|
|
132
|
+
"""
|
|
133
|
+
# Use mean of the distribution
|
|
134
|
+
probs = torch.sigmoid(self.qz_logits)
|
|
135
|
+
|
|
136
|
+
# Apply stretching transformation
|
|
137
|
+
gates = probs * (self.zeta - self.gamma) + self.gamma
|
|
138
|
+
|
|
139
|
+
return torch.clamp(gates, 0, 1)
|
|
140
|
+
|
|
141
|
+
def get_penalty(self) -> torch.Tensor:
|
|
142
|
+
"""
|
|
143
|
+
Compute the expected L0 norm (number of active gates).
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
torch.Tensor
|
|
148
|
+
Expected number of non-zero gates
|
|
149
|
+
"""
|
|
150
|
+
# Shift logits to account for hard concrete bounds
|
|
151
|
+
logits_shifted = self.qz_logits - self.temperature * math.log(
|
|
152
|
+
-self.gamma / self.zeta
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Probability that gate is active (non-zero)
|
|
156
|
+
prob_active = torch.sigmoid(logits_shifted)
|
|
157
|
+
|
|
158
|
+
return prob_active.sum()
|
|
159
|
+
|
|
160
|
+
def get_active_prob(self) -> torch.Tensor:
|
|
161
|
+
"""
|
|
162
|
+
Get the probability that each gate is active.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
torch.Tensor
|
|
167
|
+
Probability of each gate being non-zero
|
|
168
|
+
"""
|
|
169
|
+
logits_shifted = self.qz_logits - self.temperature * math.log(
|
|
170
|
+
-self.gamma / self.zeta
|
|
171
|
+
)
|
|
172
|
+
return torch.sigmoid(logits_shifted)
|
|
173
|
+
|
|
174
|
+
def get_sparsity(self) -> float:
|
|
175
|
+
"""
|
|
176
|
+
Get the overall sparsity level (fraction of inactive gates).
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
float
|
|
181
|
+
Sparsity level in [0, 1] where 1 means all gates are inactive
|
|
182
|
+
"""
|
|
183
|
+
with torch.no_grad():
|
|
184
|
+
prob_active = self.get_active_prob()
|
|
185
|
+
return 1.0 - prob_active.mean().item()
|
|
186
|
+
|
|
187
|
+
@torch.no_grad()
|
|
188
|
+
def get_num_active(self) -> int:
|
|
189
|
+
"""
|
|
190
|
+
Get the number of active gates (for monitoring).
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
int
|
|
195
|
+
Number of gates that are likely active
|
|
196
|
+
"""
|
|
197
|
+
prob_active = self.get_active_prob()
|
|
198
|
+
return int((prob_active > 0.5).sum().item())
|
|
199
|
+
|
|
200
|
+
def extra_repr(self) -> str:
|
|
201
|
+
"""String representation for printing."""
|
|
202
|
+
return (
|
|
203
|
+
f"gate_size={self.gate_size}, temperature={self.temperature}, "
|
|
204
|
+
f"stretch={self.stretch}, init_mean={self.init_mean}"
|
|
205
|
+
)
|
l0/gates.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone gate modules for sample and feature selection.
|
|
3
|
+
|
|
4
|
+
These gates can be used independently of neural network layers
|
|
5
|
+
for tasks like survey calibration and feature selection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import torch
|
|
10
|
+
import torch.nn as nn
|
|
11
|
+
|
|
12
|
+
from .distributions import HardConcrete
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class L0Gate(nn.Module):
|
|
16
|
+
"""
|
|
17
|
+
Generic L0 gate for selecting items (samples, features, etc.).
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
size : int
|
|
22
|
+
Number of items to gate
|
|
23
|
+
temperature : float
|
|
24
|
+
Temperature for HardConcrete distribution
|
|
25
|
+
init_mean : float
|
|
26
|
+
Initial mean activation probability
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
size: int,
|
|
32
|
+
temperature: float = 0.5,
|
|
33
|
+
init_mean: float = 0.5,
|
|
34
|
+
):
|
|
35
|
+
super().__init__()
|
|
36
|
+
self.size = size
|
|
37
|
+
self.temperature = temperature
|
|
38
|
+
self.init_mean = init_mean
|
|
39
|
+
|
|
40
|
+
# Create HardConcrete gates
|
|
41
|
+
self.gates = HardConcrete(
|
|
42
|
+
size,
|
|
43
|
+
temperature=temperature,
|
|
44
|
+
init_mean=init_mean,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_gates(self) -> torch.Tensor:
|
|
48
|
+
"""Get current gate values."""
|
|
49
|
+
gates_result: torch.Tensor = self.gates()
|
|
50
|
+
return gates_result
|
|
51
|
+
|
|
52
|
+
def apply_gates(self, x: torch.Tensor, dim: int = 0) -> torch.Tensor:
|
|
53
|
+
"""
|
|
54
|
+
Apply gates to a tensor along specified dimension.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
x : torch.Tensor
|
|
59
|
+
Input tensor to gate
|
|
60
|
+
dim : int
|
|
61
|
+
Dimension along which to apply gates
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
torch.Tensor
|
|
66
|
+
Gated tensor
|
|
67
|
+
"""
|
|
68
|
+
gates = self.get_gates()
|
|
69
|
+
|
|
70
|
+
# Reshape gates to match tensor dimension
|
|
71
|
+
shape = [1] * len(x.shape)
|
|
72
|
+
shape[dim] = -1
|
|
73
|
+
gates = gates.view(shape)
|
|
74
|
+
|
|
75
|
+
return x * gates
|
|
76
|
+
|
|
77
|
+
def get_penalty(self) -> torch.Tensor:
|
|
78
|
+
"""Get L0 penalty (expected number of active gates)."""
|
|
79
|
+
return self.gates.get_penalty()
|
|
80
|
+
|
|
81
|
+
def get_active_indices(self, threshold: float = 0.5) -> torch.Tensor:
|
|
82
|
+
"""
|
|
83
|
+
Get indices of active items.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
threshold : float
|
|
88
|
+
Threshold for considering a gate active
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
torch.Tensor
|
|
93
|
+
Indices of active items
|
|
94
|
+
"""
|
|
95
|
+
with torch.no_grad():
|
|
96
|
+
gates = self.get_gates()
|
|
97
|
+
active_mask = gates > threshold
|
|
98
|
+
return torch.where(active_mask)[0]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SampleGate(L0Gate):
|
|
102
|
+
"""
|
|
103
|
+
L0 gate specifically for sample/observation selection.
|
|
104
|
+
|
|
105
|
+
Useful for survey calibration and intelligent subsampling.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
n_samples : int
|
|
110
|
+
Total number of samples
|
|
111
|
+
target_samples : int
|
|
112
|
+
Target number of samples to select
|
|
113
|
+
temperature : float
|
|
114
|
+
Temperature for HardConcrete distribution
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
n_samples: int,
|
|
120
|
+
target_samples: int,
|
|
121
|
+
temperature: float = 0.25,
|
|
122
|
+
):
|
|
123
|
+
# Initialize with mean set to achieve target
|
|
124
|
+
init_mean = target_samples / n_samples
|
|
125
|
+
super().__init__(
|
|
126
|
+
size=n_samples,
|
|
127
|
+
temperature=temperature,
|
|
128
|
+
init_mean=init_mean,
|
|
129
|
+
)
|
|
130
|
+
self.n_samples = n_samples
|
|
131
|
+
self.target_samples = target_samples
|
|
132
|
+
|
|
133
|
+
def select_samples(
|
|
134
|
+
self, data: torch.Tensor
|
|
135
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
136
|
+
"""
|
|
137
|
+
Select samples from data.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
data : torch.Tensor
|
|
142
|
+
Data tensor of shape (n_samples, ...)
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
selected_data : torch.Tensor
|
|
147
|
+
Selected samples
|
|
148
|
+
indices : torch.Tensor
|
|
149
|
+
Indices of selected samples
|
|
150
|
+
"""
|
|
151
|
+
indices = self.get_active_indices()
|
|
152
|
+
selected_data = data[indices]
|
|
153
|
+
return selected_data, indices
|
|
154
|
+
|
|
155
|
+
def select_weighted_samples(
|
|
156
|
+
self, data: torch.Tensor, weights: torch.Tensor
|
|
157
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
158
|
+
"""
|
|
159
|
+
Select samples with associated weights.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
data : torch.Tensor
|
|
164
|
+
Data tensor of shape (n_samples, ...)
|
|
165
|
+
weights : torch.Tensor
|
|
166
|
+
Weight vector of shape (n_samples,)
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
selected_data : torch.Tensor
|
|
171
|
+
Selected samples
|
|
172
|
+
selected_weights : torch.Tensor
|
|
173
|
+
Weights of selected samples (scaled by gates)
|
|
174
|
+
indices : torch.Tensor
|
|
175
|
+
Indices of selected samples
|
|
176
|
+
"""
|
|
177
|
+
gates = self.get_gates()
|
|
178
|
+
|
|
179
|
+
# Apply gates to weights
|
|
180
|
+
gated_weights = weights * gates
|
|
181
|
+
|
|
182
|
+
# Select non-zero weights
|
|
183
|
+
active_mask = gated_weights > 1e-8
|
|
184
|
+
indices = torch.where(active_mask)[0]
|
|
185
|
+
|
|
186
|
+
selected_data = data[indices]
|
|
187
|
+
selected_weights = gated_weights[indices]
|
|
188
|
+
|
|
189
|
+
return selected_data, selected_weights, indices
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class FeatureGate(L0Gate):
|
|
193
|
+
"""
|
|
194
|
+
L0 gate for feature selection.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
n_features : int
|
|
199
|
+
Total number of features
|
|
200
|
+
max_features : int
|
|
201
|
+
Maximum number of features to select
|
|
202
|
+
temperature : float
|
|
203
|
+
Temperature for HardConcrete distribution
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(
|
|
207
|
+
self,
|
|
208
|
+
n_features: int,
|
|
209
|
+
max_features: int,
|
|
210
|
+
temperature: float = 0.1,
|
|
211
|
+
):
|
|
212
|
+
# Initialize to select approximately max_features
|
|
213
|
+
init_mean = min(max_features / n_features, 0.99)
|
|
214
|
+
super().__init__(
|
|
215
|
+
size=n_features,
|
|
216
|
+
temperature=temperature,
|
|
217
|
+
init_mean=init_mean,
|
|
218
|
+
)
|
|
219
|
+
self.n_features = n_features
|
|
220
|
+
self.max_features = max_features
|
|
221
|
+
|
|
222
|
+
def select_features(
|
|
223
|
+
self, data: torch.Tensor
|
|
224
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
225
|
+
"""
|
|
226
|
+
Select features from data.
|
|
227
|
+
|
|
228
|
+
Parameters
|
|
229
|
+
----------
|
|
230
|
+
data : torch.Tensor
|
|
231
|
+
Data tensor of shape (..., n_features)
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
selected_data : torch.Tensor
|
|
236
|
+
Data with selected features
|
|
237
|
+
feature_indices : torch.Tensor
|
|
238
|
+
Indices of selected features
|
|
239
|
+
"""
|
|
240
|
+
feature_indices = self.get_active_indices()
|
|
241
|
+
selected_data = data[..., feature_indices]
|
|
242
|
+
return selected_data, feature_indices
|
|
243
|
+
|
|
244
|
+
def select_features_with_names(
|
|
245
|
+
self, data: torch.Tensor, feature_names: list[str]
|
|
246
|
+
) -> tuple[torch.Tensor, list[str]]:
|
|
247
|
+
"""
|
|
248
|
+
Select features and return their names.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
data : torch.Tensor
|
|
253
|
+
Data tensor of shape (..., n_features)
|
|
254
|
+
feature_names : List[str]
|
|
255
|
+
Names of all features
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
selected_data : torch.Tensor
|
|
260
|
+
Data with selected features
|
|
261
|
+
selected_names : List[str]
|
|
262
|
+
Names of selected features
|
|
263
|
+
"""
|
|
264
|
+
selected_data, indices = self.select_features(data)
|
|
265
|
+
selected_names = [feature_names[i] for i in indices]
|
|
266
|
+
return selected_data, selected_names
|
|
267
|
+
|
|
268
|
+
def get_feature_importance(self) -> torch.Tensor:
|
|
269
|
+
"""
|
|
270
|
+
Get feature importance scores based on gate probabilities.
|
|
271
|
+
|
|
272
|
+
Returns
|
|
273
|
+
-------
|
|
274
|
+
torch.Tensor
|
|
275
|
+
Importance score for each feature
|
|
276
|
+
"""
|
|
277
|
+
with torch.no_grad():
|
|
278
|
+
return self.gates.get_active_prob()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class HybridGate(nn.Module):
|
|
282
|
+
"""
|
|
283
|
+
Hybrid selection combining L0 and random sampling.
|
|
284
|
+
|
|
285
|
+
Useful for combining intelligent selection with random sampling
|
|
286
|
+
for better coverage and reduced bias.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
n_items : int
|
|
291
|
+
Total number of items
|
|
292
|
+
l0_fraction : float
|
|
293
|
+
Fraction to select via L0 gates
|
|
294
|
+
random_fraction : float
|
|
295
|
+
Fraction to select randomly
|
|
296
|
+
target_items : int
|
|
297
|
+
Total target number of items
|
|
298
|
+
temperature : float
|
|
299
|
+
Temperature for L0 gates
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
n_items: int,
|
|
305
|
+
l0_fraction: float = 0.25,
|
|
306
|
+
random_fraction: float = 0.75,
|
|
307
|
+
target_items: int | None = None,
|
|
308
|
+
temperature: float = 0.25,
|
|
309
|
+
):
|
|
310
|
+
super().__init__()
|
|
311
|
+
|
|
312
|
+
assert l0_fraction + random_fraction <= 1.0
|
|
313
|
+
|
|
314
|
+
self.n_items = n_items
|
|
315
|
+
self.l0_fraction = l0_fraction
|
|
316
|
+
self.random_fraction = random_fraction
|
|
317
|
+
self.target_items = target_items or int(n_items * 0.2)
|
|
318
|
+
|
|
319
|
+
# Number of items for each selection method
|
|
320
|
+
self.n_l0 = int(self.target_items * l0_fraction)
|
|
321
|
+
self.n_random = int(self.target_items * random_fraction)
|
|
322
|
+
|
|
323
|
+
# L0 gates for intelligent selection
|
|
324
|
+
self.l0_gate = SampleGate(
|
|
325
|
+
n_samples=n_items,
|
|
326
|
+
target_samples=self.n_l0,
|
|
327
|
+
temperature=temperature,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def select(
|
|
331
|
+
self,
|
|
332
|
+
data: torch.Tensor,
|
|
333
|
+
random_seed: int | None = None,
|
|
334
|
+
) -> tuple[torch.Tensor, torch.Tensor, np.ndarray]:
|
|
335
|
+
"""
|
|
336
|
+
Select items using hybrid approach.
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
data : torch.Tensor
|
|
341
|
+
Data tensor of shape (n_items, ...)
|
|
342
|
+
random_seed : Optional[int]
|
|
343
|
+
Random seed for reproducibility
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
selected_data : torch.Tensor
|
|
348
|
+
Selected items
|
|
349
|
+
indices : torch.Tensor
|
|
350
|
+
Indices of selected items
|
|
351
|
+
selection_type : np.ndarray
|
|
352
|
+
Type of selection for each item ('l0' or 'random')
|
|
353
|
+
"""
|
|
354
|
+
if random_seed is not None:
|
|
355
|
+
torch.manual_seed(random_seed)
|
|
356
|
+
|
|
357
|
+
# L0 selection
|
|
358
|
+
l0_indices = self.l0_gate.get_active_indices()
|
|
359
|
+
|
|
360
|
+
# Random selection (excluding L0 selected indices)
|
|
361
|
+
all_indices = torch.arange(self.n_items)
|
|
362
|
+
mask = torch.ones(self.n_items, dtype=torch.bool)
|
|
363
|
+
mask[l0_indices] = False
|
|
364
|
+
available_indices = all_indices[mask]
|
|
365
|
+
|
|
366
|
+
# Randomly select from available indices
|
|
367
|
+
n_random_actual = min(self.n_random, len(available_indices))
|
|
368
|
+
if n_random_actual > 0:
|
|
369
|
+
perm = torch.randperm(len(available_indices))[:n_random_actual]
|
|
370
|
+
random_indices = available_indices[perm]
|
|
371
|
+
else:
|
|
372
|
+
random_indices = torch.tensor([], dtype=torch.long)
|
|
373
|
+
|
|
374
|
+
# Combine indices
|
|
375
|
+
all_selected = torch.cat([l0_indices, random_indices])
|
|
376
|
+
all_selected = torch.sort(all_selected)[0]
|
|
377
|
+
|
|
378
|
+
# Track selection type
|
|
379
|
+
# Create initial selection type array
|
|
380
|
+
_ = np.array(
|
|
381
|
+
["l0"] * len(l0_indices) + ["random"] * len(random_indices)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Reorder selection_type to match sorted indices
|
|
385
|
+
_ = torch.cat(
|
|
386
|
+
[
|
|
387
|
+
torch.arange(len(l0_indices)),
|
|
388
|
+
torch.arange(len(random_indices)) + len(l0_indices),
|
|
389
|
+
]
|
|
390
|
+
)
|
|
391
|
+
idx_map = {idx.item(): i for i, idx in enumerate(all_selected)}
|
|
392
|
+
l0_positions = [idx_map[idx.item()] for idx in l0_indices]
|
|
393
|
+
random_positions = [idx_map[idx.item()] for idx in random_indices]
|
|
394
|
+
|
|
395
|
+
selection_type_sorted = np.empty(len(all_selected), dtype="<U6")
|
|
396
|
+
for pos in l0_positions:
|
|
397
|
+
selection_type_sorted[pos] = "l0"
|
|
398
|
+
for pos in random_positions:
|
|
399
|
+
selection_type_sorted[pos] = "random"
|
|
400
|
+
|
|
401
|
+
selected_data = data[all_selected]
|
|
402
|
+
|
|
403
|
+
return selected_data, all_selected, selection_type_sorted
|
|
404
|
+
|
|
405
|
+
def get_penalty(self) -> torch.Tensor:
|
|
406
|
+
"""Get L0 penalty from the L0 component."""
|
|
407
|
+
return self.l0_gate.get_penalty()
|