import torch import torch.nn as nn import torch.nn.functional as F class WF_Net(nn.Module): """ Implements Poppi et al.'s WF Model structure. Wraps a pre-trained ResNet-18 and dynamically applies weight-space gating matrix multiplication during the forward step. """ def __init__(self, original_model: nn.Module, num_classes: int): super().__init__() # Extract the sequence of blocks/layers L from the original model self.conv1 = original_model.conv1 self.bn1 = original_model.bn1 self.relu = original_model.relu self.maxpool = original_model.maxpool self.layer1 = original_model.layer1 self.layer2 = original_model.layer2 self.layer3 = original_model.layer3 self.layer4 = original_model.layer4 self.avgpool = original_model.avgpool self.fc = original_model.fc # Target layer for filtering: layer4 block 1 conv2 # We extract its static tensor data out of the autograd parameter pool self.target_conv = self.layer4[1].conv2 self.original_w = nn.Parameter(self.target_conv.weight.data.clone().detach(), requires_grad=False) # Require: Alpha gating matrix. Shape: (num_classes, out_channels) # Initialized to 1.5 as per Poppi et al.'s verbatim specification out_channels = self.original_w.shape[0] #self.alpha = nn.Parameter(torch.ones(num_classes, out_channels) * 1.5) self.alpha = nn.Parameter(torch.ones(num_classes, out_channels)) def forward(self, x: torch.Tensor, target_unlearn_class: int) -> torch.Tensor: """ Implements Algorithm 1: General forward step of a WF model Inputs: x: Input tensor (Xin) target_unlearn_class: The class index we are actively filtering out (Yunl) """ # 1. Run through early sequence of layers undisturbed x = self.maxpool(self.relu(self.bn1(self.conv1(x)))) x = self.layer1(x) x = self.layer2(x) x = self.layer3(x) # Run layer4 block 0 and block 1 conv1 normally x = self.layer4[0](x) identity = x x = self.layer4[1].conv1(x) x = self.layer4[1].bn1(x) x = self.layer4[1].relu(x) # 2. CORE WF-NET MATH: w_hat_l <- alpha_l[Yunl] ⊙ w_l # Extract 1D vector for target class and reshape to (out_channels, 1, 1, 1) for 4D convolution broadcasting mask = torch.sigmoid(self.alpha[target_unlearn_class]).view(-1, 1, 1, 1) w_hat = self.original_w * mask # 3. Pass gated weights straight to functional forward pass: l(Xi, w_hat_l) x = F.conv2d( x, weight=w_hat, bias=self.target_conv.bias, stride=self.target_conv.stride, padding=self.target_conv.padding ) x = self.layer4[1].bn2(x) # Handle residual shortcut skip connection manually since we opened up block 1 # In ResNet-18 layer4, block 1 has no downsample shortcut layer; it's a direct identity add x = self.layer4[1].relu(x + identity) # 4. Final Classification Head Sequence x = self.avgpool(x) x = torch.flatten(x, 1) y_out = self.fc(x) return y_out