Files
Finetuning/architectures/WFNet.py
2026-06-28 01:48:49 +02:00

127 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import DataLoader
import numpy as np
from sklearn.metrics import classification_report
from architectures.Model import Model
class WF_Module(nn.Module):
"""
Pure PyTorch Neural Network module graph.
Keeps parameter registration and autograd tracking separate from
the framework's high-level Model abstractions to prevent recursion collisions.
"""
def __init__(self, original_model: nn.Module, num_classes: int):
super().__init__()
self.original_model = original_model
# Target layer for weight filtering (layer4 block 1 conv2 or conv3 depending on arch)
last_layer = original_model.layer4[1]
# Some versions are limited to 2 convolutional layers
if hasattr(last_layer, "conv3"):
self.target_conv = last_layer.conv3
else:
self.target_conv = last_layer.conv2
# Completely freeze the original ResNet parameters
for param in self.parameters():
param.requires_grad = False
# Initialize the alpha parameter matrix (Rows = Classes, Cols = Channels)
out_channels = self.target_conv.weight.shape[0]
self.alpha = nn.Parameter(torch.full((num_classes, out_channels), 3.0))
'''
Poppi et_al's Single-shot multiclass unlearning.
This calculation happens only once to generate the mask. once the mask is generated,
Unlearning and remembering becomes a matter of switching gates on and off.
'''
def forward(self, x: torch.Tensor, target_class_indices: torch.Tensor) -> torch.Tensor:
# we linearly loop through layers 1 to 4[block 1] (for ResNet)
# for i in M_{|L|} do l <- l[i]
x = self.original_model.maxpool(self.original_model.relu(self.original_model.bn1(self.original_model.conv1(x))))
x = self.original_model.layer1(x)
x = self.original_model.layer2(x)
x = self.original_model.layer3(x)
x = self.original_model.layer4[0](x)
# The second block execute its internal transformations natively
# This handles conv1->conv2 (ResNet18) or conv1->conv2->conv3 (ResNet50) automatically!
# Xi+1 <- l(Xi, ˆwl)
x = self.original_model.layer4[1](x)
# Apply mask dynamically to the completed block feature map
# wl <- αl[Yunl] ⊙ ˆwl
batch_alpha = self.alpha[target_class_indices]
mask = torch.sigmoid(batch_alpha).view(x.size(0), -1, 1, 1)
x = x * mask
# Remaining standard head steps
x = self.original_model.avgpool(x)
x = torch.flatten(x, 1)
# so here we are returning the output logits
# the result of classification is then
# argmax(x)
return self.original_model.fc(x)
class WF_Net_Model(Model):
def __init__(self, device, size, original_model: nn.Module, target_class_index: int):
self.device = device
self.size = size
self.wf_module = WF_Module(
original_model = original_model,
num_classes = size
).to(self.device)
# this index indicates which row of the mask should be active (gate closed).
self.target_class_index = target_class_index
self.model = self.wf_module
def get(self):
return self.wf_module
'''
We override the evaluate method from the base class,
because how we evaluate is different here from that of a normal torch nn.Module object
'''
def evaluate(self, loader, mode="eval"):
self.wf_module.eval()
all_preds, all_labels = [], []
print(f"\nEvaluating Domain: [{mode}]...")
with torch.no_grad():
for inputs, labels in loader:
inputs, labels = inputs.to(self.device), labels.to(self.device)
# we apply the filter
gate_signals = torch.full((inputs.size(0),), self.target_class_index, dtype=torch.long, device=self.device)
# pass prediction through the filter
outputs = self.wf_module(inputs, target_class_indices=gate_signals)
# return argmax(x)
_, predicted = torch.max(outputs, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
classes = sorted(list(set(all_labels)))
accuracy = 100 * (np.array(all_preds) == np.array(all_labels)).sum() / len(all_labels)
print(f"Test Accuracy: {accuracy:.2f}%")
print(classification_report(all_labels, all_preds, labels=classes, zero_division=0))
report = classification_report(all_labels, all_preds, labels=classes, output_dict=True, zero_division=0)
return accuracy, report
def eval(self):
"""Safely intercept any fallback base class calls targeting .eval()"""
self.wf_module.eval()