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/layers.py
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
L0 regularized neural network layers.
|
|
3
|
+
|
|
4
|
+
Implements sparse layers using the Hard Concrete distribution
|
|
5
|
+
for various layer types (Linear, Conv2d, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
import torch.nn.functional as F
|
|
13
|
+
|
|
14
|
+
from .distributions import HardConcrete
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class L0Linear(nn.Module):
|
|
18
|
+
"""
|
|
19
|
+
Linear layer with L0 regularization using HardConcrete gates.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
in_features : int
|
|
24
|
+
Number of input features
|
|
25
|
+
out_features : int
|
|
26
|
+
Number of output features
|
|
27
|
+
bias : bool
|
|
28
|
+
Whether to include bias term
|
|
29
|
+
temperature : float
|
|
30
|
+
Temperature for HardConcrete distribution
|
|
31
|
+
init_sparsity : float
|
|
32
|
+
Initial sparsity level (1 - init_mean for gates)
|
|
33
|
+
use_l2 : bool
|
|
34
|
+
Whether to support L2 penalty computation (L0L2 regularization)
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
in_features: int,
|
|
40
|
+
out_features: int,
|
|
41
|
+
bias: bool = True,
|
|
42
|
+
temperature: float = 0.5,
|
|
43
|
+
init_sparsity: float = 0.5,
|
|
44
|
+
use_l2: bool = False,
|
|
45
|
+
):
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.in_features = in_features
|
|
48
|
+
self.out_features = out_features
|
|
49
|
+
self.use_l2 = use_l2
|
|
50
|
+
|
|
51
|
+
# Weight parameters
|
|
52
|
+
self.weight = nn.Parameter(torch.Tensor(out_features, in_features))
|
|
53
|
+
if bias:
|
|
54
|
+
self.bias = nn.Parameter(torch.Tensor(out_features))
|
|
55
|
+
else:
|
|
56
|
+
self.register_parameter("bias", None)
|
|
57
|
+
|
|
58
|
+
# L0 gates for weights
|
|
59
|
+
self.weight_gates = HardConcrete(
|
|
60
|
+
out_features,
|
|
61
|
+
in_features,
|
|
62
|
+
temperature=temperature,
|
|
63
|
+
init_mean=1.0 - init_sparsity, # Convert sparsity to activation
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
self.reset_parameters()
|
|
67
|
+
|
|
68
|
+
def reset_parameters(self) -> None:
|
|
69
|
+
"""Initialize weight and bias parameters."""
|
|
70
|
+
nn.init.kaiming_normal_(self.weight, mode="fan_out")
|
|
71
|
+
if self.bias is not None:
|
|
72
|
+
nn.init.zeros_(self.bias)
|
|
73
|
+
|
|
74
|
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
|
75
|
+
"""
|
|
76
|
+
Forward pass with gated weights.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
input : torch.Tensor
|
|
81
|
+
Input tensor of shape (batch_size, in_features)
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
torch.Tensor
|
|
86
|
+
Output tensor of shape (batch_size, out_features)
|
|
87
|
+
"""
|
|
88
|
+
gates = self.weight_gates()
|
|
89
|
+
masked_weight = self.weight * gates
|
|
90
|
+
return F.linear(input, masked_weight, self.bias)
|
|
91
|
+
|
|
92
|
+
def get_l0_penalty(self) -> torch.Tensor:
|
|
93
|
+
"""Get L0 penalty (expected number of active weights)."""
|
|
94
|
+
return self.weight_gates.get_penalty()
|
|
95
|
+
|
|
96
|
+
def get_l2_penalty(self) -> torch.Tensor:
|
|
97
|
+
"""Get L2 penalty (weight norm squared)."""
|
|
98
|
+
if not self.use_l2:
|
|
99
|
+
return torch.tensor(0.0, device=self.weight.device)
|
|
100
|
+
l2_penalty: torch.Tensor = (self.weight**2).sum()
|
|
101
|
+
return l2_penalty
|
|
102
|
+
|
|
103
|
+
def get_sparsity(self) -> float:
|
|
104
|
+
"""Get current sparsity level."""
|
|
105
|
+
return self.weight_gates.get_sparsity()
|
|
106
|
+
|
|
107
|
+
def extra_repr(self) -> str:
|
|
108
|
+
"""String representation."""
|
|
109
|
+
return f"in_features={self.in_features}, out_features={self.out_features}, bias={self.bias is not None}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class L0Conv2d(nn.Module):
|
|
113
|
+
"""
|
|
114
|
+
2D Convolutional layer with L0 regularization.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
in_channels : int
|
|
119
|
+
Number of input channels
|
|
120
|
+
out_channels : int
|
|
121
|
+
Number of output channels
|
|
122
|
+
kernel_size : int or tuple
|
|
123
|
+
Size of the convolutional kernel
|
|
124
|
+
stride : int or tuple
|
|
125
|
+
Stride of the convolution
|
|
126
|
+
padding : int or tuple
|
|
127
|
+
Padding added to input
|
|
128
|
+
temperature : float
|
|
129
|
+
Temperature for HardConcrete distribution
|
|
130
|
+
init_sparsity : float
|
|
131
|
+
Initial sparsity level
|
|
132
|
+
structured : bool
|
|
133
|
+
If True, use channel-wise structured sparsity
|
|
134
|
+
use_l2 : bool
|
|
135
|
+
Whether to support L2 penalty computation
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(
|
|
139
|
+
self,
|
|
140
|
+
in_channels: int,
|
|
141
|
+
out_channels: int,
|
|
142
|
+
kernel_size: int | tuple[int, int],
|
|
143
|
+
stride: int | tuple[int, int] = 1,
|
|
144
|
+
padding: int | tuple[int, int] = 0,
|
|
145
|
+
bias: bool = True,
|
|
146
|
+
temperature: float = 0.5,
|
|
147
|
+
init_sparsity: float = 0.5,
|
|
148
|
+
structured: bool = False,
|
|
149
|
+
use_l2: bool = False,
|
|
150
|
+
):
|
|
151
|
+
super().__init__()
|
|
152
|
+
self.in_channels = in_channels
|
|
153
|
+
self.out_channels = out_channels
|
|
154
|
+
|
|
155
|
+
# Handle kernel_size as int or tuple
|
|
156
|
+
if isinstance(kernel_size, int):
|
|
157
|
+
self.kernel_size = (kernel_size, kernel_size)
|
|
158
|
+
else:
|
|
159
|
+
self.kernel_size = kernel_size
|
|
160
|
+
|
|
161
|
+
self.stride = stride
|
|
162
|
+
self.padding = padding
|
|
163
|
+
self.structured = structured
|
|
164
|
+
self.use_l2 = use_l2
|
|
165
|
+
|
|
166
|
+
# Weight parameters
|
|
167
|
+
self.weight = nn.Parameter(
|
|
168
|
+
torch.Tensor(out_channels, in_channels, *self.kernel_size)
|
|
169
|
+
)
|
|
170
|
+
if bias:
|
|
171
|
+
self.bias = nn.Parameter(torch.Tensor(out_channels))
|
|
172
|
+
else:
|
|
173
|
+
self.register_parameter("bias", None)
|
|
174
|
+
|
|
175
|
+
# L0 gates
|
|
176
|
+
if structured:
|
|
177
|
+
# Channel-wise gates for structured sparsity
|
|
178
|
+
self.channel_gates = HardConcrete(
|
|
179
|
+
out_channels,
|
|
180
|
+
temperature=temperature,
|
|
181
|
+
init_mean=1.0 - init_sparsity,
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
# Per-weight gates
|
|
185
|
+
self.weight_gates = HardConcrete(
|
|
186
|
+
out_channels,
|
|
187
|
+
in_channels * self.kernel_size[0] * self.kernel_size[1],
|
|
188
|
+
temperature=temperature,
|
|
189
|
+
init_mean=1.0 - init_sparsity,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
self.reset_parameters()
|
|
193
|
+
|
|
194
|
+
def reset_parameters(self) -> None:
|
|
195
|
+
"""Initialize weight and bias parameters."""
|
|
196
|
+
nn.init.kaiming_normal_(self.weight, mode="fan_out")
|
|
197
|
+
if self.bias is not None:
|
|
198
|
+
nn.init.zeros_(self.bias)
|
|
199
|
+
|
|
200
|
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
|
201
|
+
"""
|
|
202
|
+
Forward pass with gated weights.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
input : torch.Tensor
|
|
207
|
+
Input tensor of shape (batch, in_channels, height, width)
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
torch.Tensor
|
|
212
|
+
Output tensor
|
|
213
|
+
"""
|
|
214
|
+
if self.structured:
|
|
215
|
+
# Apply channel-wise gates
|
|
216
|
+
gates = self.channel_gates()
|
|
217
|
+
gates = gates.view(-1, 1, 1, 1) # Reshape for broadcasting
|
|
218
|
+
masked_weight = self.weight * gates
|
|
219
|
+
else:
|
|
220
|
+
# Apply per-weight gates
|
|
221
|
+
gates = self.weight_gates()
|
|
222
|
+
gates = gates.view(self.weight.shape)
|
|
223
|
+
masked_weight = self.weight * gates
|
|
224
|
+
|
|
225
|
+
return F.conv2d(
|
|
226
|
+
input, masked_weight, self.bias, self.stride, self.padding
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def get_l0_penalty(self) -> torch.Tensor:
|
|
230
|
+
"""Get L0 penalty."""
|
|
231
|
+
if self.structured:
|
|
232
|
+
return self.channel_gates.get_penalty()
|
|
233
|
+
else:
|
|
234
|
+
return self.weight_gates.get_penalty()
|
|
235
|
+
|
|
236
|
+
def get_l2_penalty(self) -> torch.Tensor:
|
|
237
|
+
"""Get L2 penalty."""
|
|
238
|
+
if not self.use_l2:
|
|
239
|
+
return torch.tensor(0.0, device=self.weight.device)
|
|
240
|
+
l2_penalty: torch.Tensor = (self.weight**2).sum()
|
|
241
|
+
return l2_penalty
|
|
242
|
+
|
|
243
|
+
def get_sparsity(self) -> float:
|
|
244
|
+
"""Get current sparsity level."""
|
|
245
|
+
if self.structured:
|
|
246
|
+
return self.channel_gates.get_sparsity()
|
|
247
|
+
else:
|
|
248
|
+
return self.weight_gates.get_sparsity()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class L0DepthwiseConv2d(nn.Module):
|
|
252
|
+
"""
|
|
253
|
+
Depthwise 2D convolution with L0 regularization.
|
|
254
|
+
|
|
255
|
+
Each input channel is convolved with its own set of filters.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
def __init__(
|
|
259
|
+
self,
|
|
260
|
+
in_channels: int,
|
|
261
|
+
kernel_size: int | tuple[int, int],
|
|
262
|
+
stride: int | tuple[int, int] = 1,
|
|
263
|
+
padding: int | tuple[int, int] = 0,
|
|
264
|
+
bias: bool = True,
|
|
265
|
+
temperature: float = 0.5,
|
|
266
|
+
init_sparsity: float = 0.5,
|
|
267
|
+
):
|
|
268
|
+
super().__init__()
|
|
269
|
+
self.in_channels = in_channels
|
|
270
|
+
|
|
271
|
+
if isinstance(kernel_size, int):
|
|
272
|
+
self.kernel_size = (kernel_size, kernel_size)
|
|
273
|
+
else:
|
|
274
|
+
self.kernel_size = kernel_size
|
|
275
|
+
|
|
276
|
+
self.stride = stride
|
|
277
|
+
self.padding = padding
|
|
278
|
+
|
|
279
|
+
# Depthwise convolution: groups = in_channels
|
|
280
|
+
self.depthwise_conv = nn.Conv2d(
|
|
281
|
+
in_channels,
|
|
282
|
+
in_channels,
|
|
283
|
+
kernel_size=self.kernel_size,
|
|
284
|
+
stride=stride,
|
|
285
|
+
padding=padding,
|
|
286
|
+
groups=in_channels,
|
|
287
|
+
bias=bias,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Channel-wise gates for depthwise conv
|
|
291
|
+
self.channel_gates = HardConcrete(
|
|
292
|
+
in_channels,
|
|
293
|
+
temperature=temperature,
|
|
294
|
+
init_mean=1.0 - init_sparsity,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def forward(self, input: torch.Tensor) -> torch.Tensor:
|
|
298
|
+
"""Forward pass with gated channels."""
|
|
299
|
+
gates = self.channel_gates()
|
|
300
|
+
gates = gates.view(1, -1, 1, 1) # Reshape for broadcasting
|
|
301
|
+
|
|
302
|
+
# Apply gates to input channels
|
|
303
|
+
gated_input = input * gates
|
|
304
|
+
output: torch.Tensor = self.depthwise_conv(gated_input)
|
|
305
|
+
return output
|
|
306
|
+
|
|
307
|
+
def get_l0_penalty(self) -> torch.Tensor:
|
|
308
|
+
"""Get L0 penalty."""
|
|
309
|
+
return self.channel_gates.get_penalty()
|
|
310
|
+
|
|
311
|
+
def get_sparsity(self) -> float:
|
|
312
|
+
"""Get current sparsity level."""
|
|
313
|
+
return self.channel_gates.get_sparsity()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class SparseMLP(nn.Module):
|
|
317
|
+
"""
|
|
318
|
+
Example MLP with L0 regularization on all layers.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
input_dim : int
|
|
323
|
+
Input dimension (e.g., 784 for MNIST)
|
|
324
|
+
hidden_dim : int
|
|
325
|
+
Hidden layer dimension
|
|
326
|
+
output_dim : int
|
|
327
|
+
Output dimension (e.g., 10 for MNIST)
|
|
328
|
+
init_sparsity : float
|
|
329
|
+
Initial sparsity level for all layers
|
|
330
|
+
temperature : float
|
|
331
|
+
Temperature for HardConcrete distribution
|
|
332
|
+
use_l2 : bool
|
|
333
|
+
Whether to use L0L2 combined regularization
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
def __init__(
|
|
337
|
+
self,
|
|
338
|
+
input_dim: int = 784,
|
|
339
|
+
hidden_dim: int = 256,
|
|
340
|
+
output_dim: int = 10,
|
|
341
|
+
init_sparsity: float = 0.5,
|
|
342
|
+
temperature: float = 0.5,
|
|
343
|
+
use_l2: bool = False,
|
|
344
|
+
):
|
|
345
|
+
super().__init__()
|
|
346
|
+
|
|
347
|
+
self.fc1 = L0Linear(
|
|
348
|
+
input_dim,
|
|
349
|
+
hidden_dim,
|
|
350
|
+
init_sparsity=init_sparsity,
|
|
351
|
+
temperature=temperature,
|
|
352
|
+
use_l2=use_l2,
|
|
353
|
+
)
|
|
354
|
+
self.fc2 = L0Linear(
|
|
355
|
+
hidden_dim,
|
|
356
|
+
hidden_dim,
|
|
357
|
+
init_sparsity=init_sparsity,
|
|
358
|
+
temperature=temperature,
|
|
359
|
+
use_l2=use_l2,
|
|
360
|
+
)
|
|
361
|
+
self.fc3 = L0Linear(
|
|
362
|
+
hidden_dim,
|
|
363
|
+
output_dim,
|
|
364
|
+
init_sparsity=init_sparsity,
|
|
365
|
+
temperature=temperature,
|
|
366
|
+
use_l2=use_l2,
|
|
367
|
+
)
|
|
368
|
+
self.use_l2 = use_l2
|
|
369
|
+
|
|
370
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
371
|
+
"""Forward pass through the network."""
|
|
372
|
+
x = x.view(x.size(0), -1) # Flatten
|
|
373
|
+
x = F.relu(self.fc1(x))
|
|
374
|
+
x = F.relu(self.fc2(x))
|
|
375
|
+
x = self.fc3(x)
|
|
376
|
+
return x
|
|
377
|
+
|
|
378
|
+
def get_l0_loss(self) -> torch.Tensor:
|
|
379
|
+
"""Get total L0 penalty across all layers."""
|
|
380
|
+
l0_loss = torch.tensor(0.0)
|
|
381
|
+
for module in self.modules():
|
|
382
|
+
if isinstance(module, (L0Linear, L0Conv2d, L0DepthwiseConv2d)):
|
|
383
|
+
l0_loss = l0_loss + module.get_l0_penalty()
|
|
384
|
+
return l0_loss
|
|
385
|
+
|
|
386
|
+
def get_l2_loss(self) -> torch.Tensor:
|
|
387
|
+
"""Get total L2 penalty across all layers."""
|
|
388
|
+
if not self.use_l2:
|
|
389
|
+
return torch.tensor(0.0)
|
|
390
|
+
|
|
391
|
+
l2_loss = torch.tensor(0.0)
|
|
392
|
+
for module in self.modules():
|
|
393
|
+
if hasattr(module, "get_l2_penalty"):
|
|
394
|
+
l2_loss = l2_loss + module.get_l2_penalty() # type: ignore[operator]
|
|
395
|
+
return l2_loss
|
|
396
|
+
|
|
397
|
+
def get_sparsity_stats(self) -> dict[str, dict[str, Any]]:
|
|
398
|
+
"""Get sparsity statistics for all layers."""
|
|
399
|
+
stats = {}
|
|
400
|
+
for name, module in self.named_modules():
|
|
401
|
+
if hasattr(module, "get_sparsity") and hasattr(
|
|
402
|
+
module, "get_l0_penalty"
|
|
403
|
+
):
|
|
404
|
+
stats[name] = {
|
|
405
|
+
"sparsity": module.get_sparsity(),
|
|
406
|
+
"active_params": module.get_l0_penalty().item(),
|
|
407
|
+
}
|
|
408
|
+
return stats
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def prune_model(model: nn.Module, threshold: float = 0.05) -> nn.Module:
|
|
412
|
+
"""
|
|
413
|
+
Prune a model by zeroing weights with low activation probability.
|
|
414
|
+
|
|
415
|
+
Parameters
|
|
416
|
+
----------
|
|
417
|
+
model : nn.Module
|
|
418
|
+
Model to prune
|
|
419
|
+
threshold : float
|
|
420
|
+
Probability threshold below which to prune weights
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
nn.Module
|
|
425
|
+
Pruned model (modified in-place)
|
|
426
|
+
"""
|
|
427
|
+
model.eval() # Ensure deterministic gates
|
|
428
|
+
|
|
429
|
+
for module in model.modules():
|
|
430
|
+
if hasattr(module, "weight_gates"):
|
|
431
|
+
with torch.no_grad():
|
|
432
|
+
prob_active = module.weight_gates.get_active_prob() # type: ignore[union-attr,operator]
|
|
433
|
+
mask = (prob_active > threshold).float()
|
|
434
|
+
|
|
435
|
+
# Reshape mask to match weight dimensions
|
|
436
|
+
if len(module.weight.shape) > len(mask.shape): # type: ignore[arg-type]
|
|
437
|
+
mask = mask.view(module.weight.shape)
|
|
438
|
+
|
|
439
|
+
# Zero out pruned weights
|
|
440
|
+
module.weight.data *= mask
|
|
441
|
+
|
|
442
|
+
elif hasattr(module, "channel_gates"):
|
|
443
|
+
with torch.no_grad():
|
|
444
|
+
prob_active = module.channel_gates.get_active_prob() # type: ignore[union-attr,operator]
|
|
445
|
+
mask = (prob_active > threshold).float()
|
|
446
|
+
|
|
447
|
+
# Apply channel-wise masking
|
|
448
|
+
if isinstance(module, L0Conv2d):
|
|
449
|
+
mask = mask.view(-1, 1, 1, 1)
|
|
450
|
+
elif isinstance(module, L0DepthwiseConv2d):
|
|
451
|
+
mask = mask.view(1, -1, 1, 1)
|
|
452
|
+
|
|
453
|
+
module.weight.data *= mask
|
|
454
|
+
|
|
455
|
+
return model
|