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/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