87 lines
3.3 KiB
Python
87 lines
3.3 KiB
Python
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
|