Uploading repository
Browse filesCopy from github repo
- L0_Regularizer.py +164 -0
- README.md +33 -0
L0_Regularizer.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch, math, copy
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class L0_Regularizer(torch.nn.Module):
|
| 5 |
+
|
| 6 |
+
def __init__(self, original_module: torch.nn.Module, lam: float, weight_decay: float = 0,
|
| 7 |
+
temperature: float = 2 / 3, droprate_init=0.2, limit_a=-.1, limit_b=1.1, epsilon=1e-6
|
| 8 |
+
):
|
| 9 |
+
super(L0_Regularizer, self).__init__()
|
| 10 |
+
self.module = copy.deepcopy(original_module)
|
| 11 |
+
|
| 12 |
+
self.pre_parameters = torch.nn.ParameterDict(
|
| 13 |
+
{name.replace(".", "#") + "_p": param for name, param in self.module.named_parameters()}
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
self.param_names = [name.replace(".", "#") for name, param in self.module.named_parameters()]
|
| 17 |
+
self.mask_parameters = torch.nn.ParameterDict()
|
| 18 |
+
self.lam = lam
|
| 19 |
+
self.weight_decay = weight_decay
|
| 20 |
+
self.temperature = temperature
|
| 21 |
+
self.droprate_init = droprate_init
|
| 22 |
+
self.limit_a = limit_a
|
| 23 |
+
self.limit_b = limit_b
|
| 24 |
+
self.epsilon = epsilon
|
| 25 |
+
|
| 26 |
+
for name, param in self.module.named_parameters():
|
| 27 |
+
mask = torch.nn.Parameter(torch.Tensor(param.size()))
|
| 28 |
+
self.mask_parameters.update({name.replace(".", "#") + "_m": mask})
|
| 29 |
+
|
| 30 |
+
# below code guts the module of its previous parameters,
|
| 31 |
+
# allowing them to be replaced by non-leaf tensors
|
| 32 |
+
|
| 33 |
+
self.reset_parameters()
|
| 34 |
+
self.to("cpu")
|
| 35 |
+
|
| 36 |
+
for name in self.param_names:
|
| 37 |
+
L0_Regularizer.recursive_del(self.module, name)
|
| 38 |
+
L0_Regularizer.recursive_set(self.module, name, self.sample_weights(name, 1))
|
| 39 |
+
|
| 40 |
+
'''
|
| 41 |
+
Below code direct copy with adaptations from codebase for:
|
| 42 |
+
|
| 43 |
+
Louizos, C., Welling, M., & Kingma, D. P. (2017).
|
| 44 |
+
Learning sparse neural networks through L_0 regularization.
|
| 45 |
+
arXiv preprint arXiv:1712.01312.
|
| 46 |
+
'''
|
| 47 |
+
|
| 48 |
+
def reset_parameters(self):
|
| 49 |
+
for name, weight in self.pre_parameters.items():
|
| 50 |
+
if "bias" in name:
|
| 51 |
+
torch.nn.init.constant_(weight, 0.0)
|
| 52 |
+
else:
|
| 53 |
+
torch.nn.init.xavier_uniform_(weight)
|
| 54 |
+
|
| 55 |
+
for name, weight in self.mask_parameters.items():
|
| 56 |
+
torch.nn.init.normal_(weight, math.log(1 - self.droprate_init) - math.log(self.droprate_init), 1e-2)
|
| 57 |
+
|
| 58 |
+
def constrain_parameters(self):
|
| 59 |
+
for name, weight in self.mask_parameters.items():
|
| 60 |
+
weight.data.clamp_(min=math.log(1e-2), max=math.log(1e2))
|
| 61 |
+
|
| 62 |
+
def cdf_qz(self, x, param):
|
| 63 |
+
"""Implements the CDF of the 'stretched' concrete distribution"""
|
| 64 |
+
# references parameters
|
| 65 |
+
xn = (x - self.limit_a) / (self.limit_b - self.limit_a)
|
| 66 |
+
logits = math.log(xn) - math.log(1 - xn)
|
| 67 |
+
return torch.sigmoid(
|
| 68 |
+
logits * self.temperature - self.mask_parameters[param + "_m"]).clamp(min=self.epsilon,
|
| 69 |
+
max=1 - self.epsilon)
|
| 70 |
+
|
| 71 |
+
def quantile_concrete(self, x, param):
|
| 72 |
+
"""Implements the quantile, aka inverse CDF, of the 'stretched' concrete distribution"""
|
| 73 |
+
# references parameters
|
| 74 |
+
y = torch.sigmoid(
|
| 75 |
+
(torch.log(x) - torch.log(1 - x) + self.mask_parameters[param + "_m"]) / self.temperature)
|
| 76 |
+
return y * (self.limit_b - self.limit_a) + self.limit_a
|
| 77 |
+
|
| 78 |
+
def _reg_w(self, param):
|
| 79 |
+
"""Expected L0 norm under the stochastic gates, takes into account and re-weights also a potential L2 penalty"""
|
| 80 |
+
"""is_neural is old method, calculates wrt columns first multiplied by expected values of gates
|
| 81 |
+
second method calculates wrt all parameters
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
# why is this negative? will investigate behavior at testing
|
| 85 |
+
# reversed negative value, value should increase with description length
|
| 86 |
+
logpw_l2 = - (.5 * self.weight_decay * self.pre_parameters[param + "_p"].pow(2)) - self.lam
|
| 87 |
+
logpw = torch.sum((1 - self.cdf_qz(0, param)) * logpw_l2)
|
| 88 |
+
|
| 89 |
+
return -logpw
|
| 90 |
+
|
| 91 |
+
def regularization(self):
|
| 92 |
+
r_total = torch.Tensor([])
|
| 93 |
+
for param in self.param_names:
|
| 94 |
+
device = self.mask_parameters[param + "_m"].device
|
| 95 |
+
r_total = torch.cat([r_total.to(device), self._reg_w(param).unsqueeze(dim=0)])
|
| 96 |
+
return r_total.sum()
|
| 97 |
+
|
| 98 |
+
def count_l0(self):
|
| 99 |
+
total = []
|
| 100 |
+
for param in self.param_names:
|
| 101 |
+
total.append(torch.sum(1 - self.cdf_qz(0, param)).unsqueeze(dim=0))
|
| 102 |
+
return torch.cat(total).sum()
|
| 103 |
+
|
| 104 |
+
def count_l2(self):
|
| 105 |
+
total = []
|
| 106 |
+
for param in self.param_names:
|
| 107 |
+
total.append(self._l2_helper(param).unsqueeze(dim=0))
|
| 108 |
+
return torch.cat(total).sum()
|
| 109 |
+
|
| 110 |
+
def _l2_helper(self, param):
|
| 111 |
+
return (self.sample_weights(param, False) ** 2).sum()
|
| 112 |
+
|
| 113 |
+
def get_eps(self, size):
|
| 114 |
+
"""Uniform random numbers for the concrete distribution"""
|
| 115 |
+
# Variable deprecated and removed
|
| 116 |
+
eps = torch.rand(size) * (1 - 2 * self.epsilon) + self.epsilon
|
| 117 |
+
return eps
|
| 118 |
+
|
| 119 |
+
def sample_z(self, param, sample=True):
|
| 120 |
+
"""Sample the hard-concrete gates for training and use a deterministic value for testing"""
|
| 121 |
+
size = self.mask_parameters[param + "_m"].size()
|
| 122 |
+
if sample:
|
| 123 |
+
device = self.mask_parameters[param + "_m"].device
|
| 124 |
+
eps = self.get_eps(size).to(device)
|
| 125 |
+
z = self.quantile_concrete(eps, param)
|
| 126 |
+
return torch.nn.functional.hardtanh(z, min_val=0, max_val=1)
|
| 127 |
+
else: # mode
|
| 128 |
+
pi = torch.sigmoid(self.mask_parameters[param + "_m"])
|
| 129 |
+
return torch.nn.functional.hardtanh(pi * (self.limit_b - self.limit_a) + self.limit_a, min_val=0, max_val=1)
|
| 130 |
+
|
| 131 |
+
def sample_weights(self, param, sample=True):
|
| 132 |
+
mask = self.sample_z(param, sample)
|
| 133 |
+
return mask * self.pre_parameters[param + "_p"]
|
| 134 |
+
|
| 135 |
+
def forward(self, x):
|
| 136 |
+
"""rewrite parameters (tensors) of core module and feedforward"""
|
| 137 |
+
for param in self.param_names:
|
| 138 |
+
L0_Regularizer.recursive_set(self.module, param, self.sample_weights(param, sample=self.training))
|
| 139 |
+
|
| 140 |
+
return self.module(x)
|
| 141 |
+
|
| 142 |
+
@staticmethod
|
| 143 |
+
def recursive_get(obj, att_name):
|
| 144 |
+
if "#" in att_name:
|
| 145 |
+
first, last = att_name.split("#", 1)
|
| 146 |
+
L0_Regularizer.recursive_get(getattr(obj, first), last)
|
| 147 |
+
else:
|
| 148 |
+
return getattr(obj, att_name)
|
| 149 |
+
|
| 150 |
+
@staticmethod
|
| 151 |
+
def recursive_set(obj, att_name, val):
|
| 152 |
+
if "#" in att_name:
|
| 153 |
+
first, last = att_name.split("#", 1)
|
| 154 |
+
L0_Regularizer.recursive_set(getattr(obj, first), last, val)
|
| 155 |
+
else:
|
| 156 |
+
setattr(obj, att_name, val)
|
| 157 |
+
|
| 158 |
+
@staticmethod
|
| 159 |
+
def recursive_del(obj, att_name):
|
| 160 |
+
if "#" in att_name:
|
| 161 |
+
first, last = att_name.split("#", 1)
|
| 162 |
+
L0_Regularizer.recursive_del(getattr(obj, first), last)
|
| 163 |
+
else:
|
| 164 |
+
delattr(obj, att_name)
|
README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# L0 Regularizer
|
| 2 |
+
PyTorch adaptation of Louizos (2017) to wrap PyTorch modules.
|
| 3 |
+
|
| 4 |
+
This code provides a method for wrapping an existing PyTorch module and regularizing it according to the L0 norm. It is an adaptation and extension of code from the repository [here](https://github.com/AMLab-Amsterdam/L0_regularization). The code works in the following manner:
|
| 5 |
+
|
| 6 |
+
1. The supplied module is copied.
|
| 7 |
+
2. The copied module has its existing parameters copied into a new parameter dictionary.
|
| 8 |
+
3. Parameters used for masking are generated for each copied parameter in the dictionary.
|
| 9 |
+
4. The parameters are then deleted from the copied module.
|
| 10 |
+
5. During a forward pass the parameters are masked and then plugged into the module as regular tensors.
|
| 11 |
+
|
| 12 |
+
This method allows for the generic adaptation of any PyTorch module, though it may brook further performance improvements.
|
| 13 |
+
|
| 14 |
+
An important divergence from Louizos that allows it to be generalized is that each parameter is optimized separately. In other words, it optimizes for connection, not neural, dropout. It also suffers from an inability to do large batches while also sampling each dropout independently. Nevertheless, the code has proven useful for analyzing the computational complexity of different data given an architecture.
|
| 15 |
+
|
| 16 |
+
I plan to revisit this code in the future and implement some or all of the following:
|
| 17 |
+
* A method to exclude parameters from regularization
|
| 18 |
+
* A method allowing batch processing with independent sampling
|
| 19 |
+
* Support for initializing parameters with different distributions
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# Using
|
| 23 |
+
|
| 24 |
+
Initializing requires a `module` of type `torch.nn.Module` and `lam` of type `float`, the module is used as explained above, and `lam` is the L0 regularization constant. Optional parameters include `weight_decay` (L2 regularization constant) and `droprate_init` (likelihood of masking at initialization). Other parameters are detailed in Louizos (2017) but have to do with the dropout sampling method and shouldn't need tweaking.
|
| 25 |
+
|
| 26 |
+
After initialization use your new module in the following way:
|
| 27 |
+
|
| 28 |
+
* `.forward()` should function as your module did initially
|
| 29 |
+
* `.regularization()` will return the combined L0 and L2 norm (avoid using L2 norm of existing PyTorch optimizers)
|
| 30 |
+
* `.count_l0` will return the expected value of the number of retained parameters during a forward pass
|
| 31 |
+
* `.count_l2` will return the expected cost of encoding the parameters (sum of squares of expected values AFTER masking)
|
| 32 |
+
|
| 33 |
+
The last two are useful for measurements, but `.regularization()` is the backprop supporting function.
|