fromcontextlibimportcontextmanagerfromfunctoolsimportpartialfromtypingimportTuple, Any, Callableimporttorchimporttorch.nn.functionalasFfrommatplotlibimportpyplotaspltfromtorchimportnn, TensorclassPartialConv2d(nn.Conv2d):
def__init__(self, *args, **kwargs):
# whether the mask is multi-channel or notif'multi_channel'inkwargs:
self.multi_channel=kwargs['multi_channel']
kwargs.pop('multi_channel')
else:
self.multi_channel=Falseif'return_mask'inkwargs:
self.return_mask=kwargs['return_mask']
kwargs.pop('return_mask')
else:
self.return_mask=Falsesuper(PartialConv2d, self).__init__(*args, **kwargs)
ifself.multi_channel:
self.register_buffer(name='weight_maskUpdater', persistent=False,
tensor=torch.ones(self.out_channels, self.in_channels,
self.kernel_size[0], self.kernel_size[1]))
else:
self.register_buffer(name='weight_maskUpdater', persistent=False,
tensor=torch.ones(1, 1, self.kernel_size[0], self.kernel_size[1]))
self.slide_winsize=self.weight_maskUpdater.shape[1] *self.weight_maskUpdater.shape[2] *self.weight_maskUpdater.shape[3]
self.last_size= (None, None, None, None)
self.update_mask=Noneself.mask_ratio=Nonedefforward(self, input, mask_in=None):
assertlen(input.shape) ==4ifmask_inisnotNoneorself.last_size!=tuple(input.shape):
self.last_size=tuple(input.shape)
withtorch.no_grad():
ifmask_inisNone:
# if mask is not provided, create a maskifself.multi_channel:
mask=torch.ones_like(input)
else:
mask=torch.ones(1, 1, input.data.shape[2], input.data.shape[3], device=input.device, dtype=input.dtype)
else:
mask=mask_inself.update_mask=F.conv2d(mask, self.weight_maskUpdater, bias=None, stride=self.stride, padding=self.padding, dilation=self.dilation, groups=1)
# for mixed precision training, change 1e-8 to 1e-6self.mask_ratio=self.slide_winsize/(self.update_mask+1e-8)
# self.mask_ratio = torch.max(self.update_mask)/(self.update_mask + 1e-8)self.update_mask=torch.clamp(self.update_mask, 0, 1)
self.mask_ratio=torch.mul(self.mask_ratio, self.update_mask)
raw_out=super(PartialConv2d, self).forward(torch.mul(input, mask) ifmask_inisnotNoneelseinput)
ifself.biasisnotNone:
bias_view=self.bias.view(1, self.out_channels, 1, 1)
output=torch.mul(raw_out-bias_view, self.mask_ratio) +bias_viewoutput=torch.mul(output, self.update_mask)
else:
output=torch.mul(raw_out, self.mask_ratio)
ifself.return_mask:
returnoutput, self.update_maskelse:
returnoutputclassMaskedConv2d(nn.Conv2d):
def__init__(
self,
in_channels: int,
out_channels: int,
kernel_size,
stride=1,
padding=0,
dilation=1,
groups: int=1,
bias: bool=True,
padding_mode: str='zeros',
eps=1e-8,
multichannel: bool=False,
partial_conv: bool=False,
device=None,
dtype=None
) ->None:
factory_kwargs= {'device': device, 'dtype': dtype}
super().__init__(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias, padding_mode, device, dtype)
ifmultichannel:
self.register_buffer('mask_weight', torch.ones(out_channels, self.in_channels//groups, *self.kernel_size, **factory_kwargs), persistent=False)
else:
self.register_buffer('mask_weight', torch.ones(1, 1, *self.kernel_size, **factory_kwargs), persistent=False)
self.eps=epsself.multichannel=multichannelself.partial_conv=partial_convdefget_mask(
self,
input: torch.Tensor,
mask: torch.Tensor|None
) -> (torch.Tensor, torch.Tensor):
ifmaskisNone:
ifself.multichannel:
mask=torch.ones_like(input)
else:
mask=torch.ones(1, 1, *input.shape[2:], device=input.device, dtype=input.dtype)
else:
ifself.multichannel:
mask=mask.expand_as(input)
else:
mask=mask.expand(1, 1, *input.shape[2:])
returnmaskdefforward(
self,
input: torch.Tensor,
mask: torch.Tensor|None=None
) -> (torch.Tensor, torch.Tensor|None):
ifmaskisnotNone:
input*=maskmask=self.get_mask(input, mask)
ifself.partial_conv:
output=F.conv2d(input, self.weight, None, self.stride, self.padding, self.dilation, self.groups)
mask=F.conv2d(mask, self.mask_weight, None, self.stride, self.padding, self.dilation, self.groupsifself.multichannelelse1)
mask_kernel_numel=self.mask_weight.data.shape[1:].numel()
mask_ratio=mask_kernel_numel/ (mask+self.eps)
mask.clamp_(0, 1)
# Apply re-weighting and biasoutput*=mask_ratioifself.biasisnotNone:
output+=self.bias.view(-1, 1, 1)
output*=maskelse:
output=F.conv2d(input, self.weight, self.bias, self.stride, self.padding, self.dilation, self.groups)
mask=F.conv2d(mask, self.mask_weight, None, self.stride, self.padding, self.dilation, self.groupsifself.multichannelelse1)
max_vals=mask.max(dim=2, keepdim=True)[0].max(dim=3, keepdim=True)[0]
mask=mask/max_valsreturnoutput, maskdefextra_repr(self):
returnf"{super().extra_repr()}, eps={self.eps}, multichannel={self.multichannel}, partial_conv={self.partial_conv}"classMaskedPixelUnshuffle(nn.PixelUnshuffle):
defforward(self, input: Tensor, mask: Tensor|None=None) -> (Tensor, Tensor|None):
returnsuper().forward(input), super().forward(mask) ifmaskisnotNoneelseNoneclassMaskedSequential(nn.Sequential):
defforward(self, input: Tensor, mask: Tensor|None=None) -> (Tensor, Tensor|None):
formoduleinself:
input, mask=module(input, mask)
returninput, mask@contextmanagerdefregister_hooks(
model: torch.nn.Module,
hook: Callable,
predicate: Callable[[str, torch.nn.Module], bool],
**hook_kwargs
):
handles= []
try:
forname, moduleinmodel.named_modules():
ifpredicate(name, module):
hook: Callable=partial(hook, name=name, **hook_kwargs)
handle=module.register_forward_hook(hook)
handles.append(handle)
yieldhandlesfinally:
forhandleinhandles:
handle.remove()
defactivations_recorder_hook(
module: torch.nn.Module,
input: torch.Tensor,
output: torch.Tensor,
name: str,
*,
storage: dict[str, Any]
):
ifnameinstorage:
ifisinstance(storage[name], list):
storage[name].append(output)
else:
storage[name] = [storage[name], output]
else:
storage[name] =outputdefforward_with_activations(
model: torch.nn.Module,
predicate: Callable[[str, torch.nn.Module], bool],
*model_args,
**model_kwargs,
) ->Tuple[torch.Tensor, dict[str, Any]]:
storage= {}
withregister_hooks(model, activations_recorder_hook, predicate, storage=storage):
output=model(*model_args, **model_kwargs)
returnoutput, storagedeftest_it():
torch.manual_seed(37)
in_channels=3downscale_factor=2scale=1base=2depth=8visualize_depth=4eps=1e-8pconv= []
foriinrange(depth):
pconv.append(MaskedPixelUnshuffle(downscale_factor))
pconv.append(PartialConv2d(
in_channels=scale*base** (i+1) *downscale_factor**2ifi>0elsein_channels*downscale_factor**2,
out_channels=scale*base**i*downscale_factor**2,
kernel_size=(3, 3), padding=1, bias=False, multi_channel=True, return_mask=True)
)
pconv=MaskedSequential(*pconv)
mpconv= []
foriinrange(depth):
mpconv.append(MaskedPixelUnshuffle(downscale_factor))
mpconv.append(MaskedConv2d(
in_channels=scale*base** (i+1) *downscale_factor**2ifi>0elsein_channels*downscale_factor**2,
out_channels=scale*base**i*downscale_factor**2,
kernel_size=(3, 3), padding=1, bias=False, multichannel=True, partial_conv=True)
)
mpconv=MaskedSequential(*mpconv)
mconv= []
foriinrange(depth):
mconv.append(MaskedPixelUnshuffle(downscale_factor))
mconv.append(MaskedConv2d(
in_channels=scale*base** (i+1) *downscale_factor**2ifi>0elsein_channels*downscale_factor**2,
out_channels=scale*base**i*downscale_factor**2,
kernel_size=(3, 3), padding=1, bias=False, multichannel=True, partial_conv=False)
)
mconv=MaskedSequential(*mconv)
withtorch.no_grad():
print(f"{pconv=}")
print(f"{mpconv=}")
print(f"{mconv=}")
print(f"{list(pconv.state_dict().keys())=}")
print(f"{list(mpconv.state_dict().keys())=}")
print(f"{list(mconv.state_dict().keys())=}")
mpconv.load_state_dict(pconv.state_dict())
mconv.load_state_dict(pconv.state_dict())
x=torch.randn(1, in_channels, downscale_factor**depth, downscale_factor**depth)
mask_pconv, mask_mpconv, mask_mconv=torch.ones_like(x), torch.ones_like(x), torch.ones_like(x)
defis_conv_predicate(name: str, module: torch.nn.Module):
returnisinstance(module, torch.nn.Conv2d)
(y_pconv, mask_pconv), activations_pconv=forward_with_activations(pconv, is_conv_predicate, x, mask_pconv)
(y_mpconv, mask_mpconv), activations_mpconv=forward_with_activations(mpconv, is_conv_predicate, x, mask_mpconv)
(y_mconv, mask_mconv), activations_mconv=forward_with_activations(mconv, is_conv_predicate, x, mask_mconv)
asserttorch.allclose(y_mpconv, y_pconv)
assertnottorch.allclose(y_mconv, y_mpconv)
print(f"{activations_pconv.keys()=}") # ['1', '3', '5', '7', '9', '11', '13', '15']# fig, axs = plt.subplots(nrows=visualize_depth, ncols=3, figsize=(12, 8), dpi=180)fig, axs=plt.subplots(nrows=3, ncols=visualize_depth, figsize=(12, 8), dpi=180)
axs=axs.flatten()
forimpl_i, (name, y, mask, activations) inenumerate([
("pconv", y_pconv, mask_pconv, activations_pconv),
("mpconv", y_mpconv, mask_mpconv, activations_mpconv),
("mconv", y_mconv, mask_mconv, activations_mconv)
]):
batch_i=0fordepth_iinrange(visualize_depth):
# ax = axs[depth_i * 3 + impl_i]ax=axs[impl_i*visualize_depth+depth_i]
output=activations[f"{depth_i*2+1}"][0][batch_i]
mask_output=activations[f"{depth_i*2+1}"][1][batch_i]
mean=output.mean()
std=output.std(unbiased=False)
skewness= ((output-mean) **3).mean() / (std**3+eps)
kurtosis= ((output-mean) **4).mean() / (std**4+eps)
print(f"{name=}, {depth_i=}, {mean=}, {std=}, {skewness=}, {kurtosis=}")
ax.imshow(output.mean(dim=0).numpy(), cmap='coolwarm', vmin=-std, vmax=std)
ax.set_title(f"{name}{depth_i=}")
ax.axis('off')
# plt.suptitle(f"Depth {depth_i}")plt.show()
if__name__=='__main__':
test_it()
I have implemented partialconv, and stumbled with the problem that layer activations are peaking at edges, though "Partial Convolution based Padding" paper at Figure 5 (paper) explicitly saying that "Red rectangles show the strong activation regions from VGG19 network with zero paddding":

I started to double check my implementation, and it turns out to be similar as this repo. After that I started to think about it, why this is happening. After trial and fail I came up with simple solution - just convolute mask on mask_weight, then normalize mask by dividing it with max value in the mask.
Here is code for your reference to double check your implementation, my implementation, and fix by yourself:
Code
Output:
pconvis an original implementation of partial conv (this repo)mpconvis my implementation of partial convmconvis my approach of masked convolutionHere is also activations on real images: