optimised

This commit is contained in:
2026-07-01 21:05:01 +02:00
commit 434d3b8198
34 changed files with 3286 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import torch.nn as nn
from torchvision import models
from architectures.Model import Model
class DenseNet121(Model):
def get(self):
# load pretrained
m = models.densenet121(weights=models.DenseNet121_Weights.DEFAULT)
# will modify only the final layers
num_ftrs = m.classifier.in_features
m.classifier = nn.Linear(num_ftrs, self.size)
return m

View File

@@ -0,0 +1,19 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class EfficientNet(Model):
def get(self):
m = models.efficientnet_b1(weights=models.EfficientNet_B1_Weights.DEFAULT)
# Unfreeze the last block for a lighter touch
for param in m.features[-1].parameters(): param.requires_grad = True
# Standard classifier fix
m.classifier[1] = nn.Linear(m.classifier[1].in_features, self.size)
return m

View File

@@ -0,0 +1,31 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class GoogleNet(Model):
def get(self):
m = models.googlenet(weights=models.GoogLeNet_Weights.DEFAULT)
# 1. Handle the two Auxiliary Classifiers
# GoogLeNet has aux1 and aux2 to help training converge
#if m.aux_logits:
#m.aux1.fc = nn.Linear(m.aux1.fc.in_features, self.size)
#m.aux2.fc = nn.Linear(m.aux2.fc.in_features, self.size)
# 2. Handle the Main Classifier
m.fc = nn.Linear(m.fc.in_features, self.size)
#for param in m.parameters():
# param.requires_grad = False
# Unfreezing the final stages for identity recognition
#for name, param in m.named_parameters():
# if "inception5" in name or "fc" in name:
# param.requires_grad = True
return m

View File

@@ -0,0 +1,47 @@
import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import models
import time
# Base model
from architectures.Model import Model
class Inception(Model):
def get(self):
m = models.inception_v3(weights=models.Inception_V3_Weights.DEFAULT)
#for param in model.parameters():
# param.requires_grad = False
m.AuxLogits.fc = nn.Linear(m.AuxLogits.fc.in_features, self.size)
m.fc = nn.Linear(m.fc.in_features, self.size)
return m
def train(self, epochs, loader, rate):
# Override because Inception returns a tuple (main, aux)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(filter(lambda p: p.requires_grad, self.model.parameters()), lr=rate)
print(f"Starting training on {self.device}...")
start_time = time.time()
self.model.train()
for epoch in range(epochs):
total_loss = 0.0
for inputs, labels in loader:
inputs, labels = inputs.to(self.device), labels.to(self.device)
optimizer.zero_grad()
outputs, aux_outputs = self.model(inputs)
loss = criterion(outputs, labels) + 0.3 * criterion(aux_outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
print(f"Epoch {epoch+1}/{epochs} | Loss: {total_loss/len(loader):.4f}")
if self.device.type == 'cuda': torch.cuda.synchronize()
print(f"Training completed in: {time.time() - start_time:.2f}s")

225
architectures/Model.py Normal file
View File

@@ -0,0 +1,225 @@
from abc import ABC, abstractmethod
import torch
import torch.nn as nn
import torch.optim as optim
import time
import numpy as np
from sklearn.metrics import classification_report
from pathlib import Path
#from unlearning.Strategy import Strategy
import copy
from torch.optim.lr_scheduler import CosineAnnealingLR
import Util
class Model(ABC):
# need to add a weight decay here
def __init__(self, device, size):
self.device = device
self.size = size
self.model = self.get().to(self.device)
@abstractmethod
def get(self):
pass
'''
Have to have a new param here as mode, for example it would be base, or retrain
param mode = "base" or "retrain"
that way I can save time it takes to train and retrain.
file would be solved with Util functions
log_file = Path(f"reports/{mode}/{self.__class__.__name__}/time_metrics.txt")
Util._initialize_log_file(log_file):
strt = time.perf_counter()
end = time.perf_counter()
and then save logs
execution_time = end -strt
Util.log_metric(log_file, execution_time: float):
'''
def train(self, epochs, loader, rate, mode = "retrain"):
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(filter(lambda p: p.requires_grad, self.model.parameters()), lr=rate, weight_decay=0.1)
scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-6)
# to save reports
file_path = Path(f"{mode}/{self.__class__.__name__.lower()}/time_metrics.txt")
Util._initialize_log_file(file_path)
#save_dir.mkdir(parents=True, exist_ok=True)
print(f"Starting training on {self.device}...")
start_time = time.time()
self.model.train()
for epoch in range(epochs):
total_loss = 0.0
for inputs, labels in loader:
inputs, labels = inputs.to(self.device), labels.to(self.device)
optimizer.zero_grad()
outputs = self.model(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
scheduler.step()
print(f"Epoch {epoch+1}/{epochs} | Loss: {total_loss / len(loader):.4f}")
end_time = time.time()
Util.log_metric(log_file=file_path, execution_time=(end_time - start_time))
if self.device.type == 'cuda': torch.cuda.synchronize()
print(f"Training completed in: {time.time() - start_time:.2f}s")
def save(self, filename=None):
save_dir = Path("trained_models")
save_dir.mkdir(parents=True, exist_ok=True)
# Filename (Default to class name if not provided)
if filename is None:
filename = f"{self.__class__.__name__.lower()}.pth"
if not filename.endswith('.pth'):
filename += '.pth'
save_path = save_dir / filename
torch.save(self.model.state_dict(), save_path)
print(f'Model saved to {save_path}')
def load(self, arch):
file_path = Path("trained_models") / f'{arch.name.lower()}.pth'
# does file exist
if not file_path.exists():
raise FileNotFoundError(f'No checkpoint found at: {file_path}')
# Load the weights
state_dict = torch.load(file_path, map_location=self.device, weights_only=True)
self.model.load_state_dict(state_dict)
self.model.to(self.device)
print(f'Model loaded from {file_path}')
def unlearn(self, strategy: 'Strategy', forget_loader, retain_loader):
""" Executes a targeted unlearning strategy and profiles efficiency """
print(f"Executing: {strategy.__class__.__name__}...")
start_time = time.time()
# Delegate the actual algorithmic weight/logit manipulation to the strategy
strategy.apply(self.model, forget_loader, retain_loader)
elapsed_time = time.time() - start_time
print(f"{strategy.__class__.__name__} completed in {elapsed_time:.4f} seconds.")
return elapsed_time
def evaluate(self, loader, mode="eval"):
"""
Evaluates the model, prints terminal reports, and routes metrics to
a file logger based on the current context mode.
"""
self.model.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)
outputs = self.model(inputs)
_, predicted = torch.max(outputs, 1)
all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy())
# Extract only the active classes evaluated in this loader slice
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}%")
# 1. Print standard text report to terminal
print(classification_report(all_labels, all_preds, labels=classes, zero_division=0))
# 2. Extract structured dictionary metrics
report_dict = classification_report(
all_labels,
all_preds,
labels=classes,
output_dict=True,
zero_division=0
)
# 3. Delegate file tracking to isolated helper method
#self._log_to_csv(mode, accuracy,report_dict)
return accuracy, report_dict
# factory
@staticmethod
def create(arch, device, size):
print(f'>> MODEL ARCHITECTURE >> {arch.name}.')
match arch:
# ResNet18
case Architecture.RESNET18:
from architectures.ResNet18 import ResNet18
return ResNet18(device, size)
# ResNet34
case Architecture.RESNET34:
from architectures.ResNet34 import ResNet34
return ResNet34(device, size)
# ResNet50
case Architecture.RESNET50:
from architectures.ResNet50 import ResNet50
return ResNet50(device, size)
# INCEPTION
case Architecture.INCEPTION:
from architectures.Inception import Inception
return Inception(device, size)
# DENSENET121
case Architecture.DENSENET121:
from architectures.DenseNet121 import DenseNet121
return DenseNet121(device, size)
# googleNet
case Architecture.GOOGLENET:
from architectures.GoogleNet import GoogleNet
return GoogleNet(device, size)
# EfficientNet
case Architecture.EFFICIENTNET:
from architectures.EfficentNet import EfficientNet
return EfficientNet(device, size)
#ShuffleNet
case Architecture.SHUFFLENET:
from architectures.ShuffleNet import ShuffleNet
return ShuffleNet(device, size)
# wide ResNet
case Architecture.WIDE_RESNET:
from architectures.WideResNet import WideResNet
return WideResNet(device, size)
case _:
raise ValueError(f"Unknown model: {arch}")
# model architectures
from enum import Enum, auto
class Architecture(Enum):
RESNET18 = auto()
RESNET50 = auto()
RESNET34 = auto()
INCEPTION = auto()
DENSENET121 = auto()
GOOGLENET = auto()
EFFICIENTNET = auto()
SHUFFLENET = auto()
WIDE_RESNET = auto()

22
architectures/ResNet18.py Normal file
View File

@@ -0,0 +1,22 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class ResNet18(Model):
def get(self):
m = models.resnet18(weights=models.ResNet18_Weights.DEFAULT)
# freez all layers
#for param in m.parameters():
# param.requires_grad = False
# unfreez the last two
#for param in m.layer3.parameters(): param.requires_grad = True
#for param in m.layer4.parameters(): param.requires_grad = True
m.fc = nn.Linear(m.fc.in_features, self.size)
return m

22
architectures/ResNet34.py Normal file
View File

@@ -0,0 +1,22 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class ResNet34(Model):
def get(self):
m = models.resnet34(weights=models.ResNet34_Weights.DEFAULT)
# freez all layers
#for param in m.parameters():
# param.requires_grad = False
# unfreez the last two
#for param in m.layer3.parameters(): param.requires_grad = True
#for param in m.layer4.parameters(): param.requires_grad = True
m.fc = nn.Linear(m.fc.in_features, self.size)
return m

36
architectures/ResNet50.py Normal file
View File

@@ -0,0 +1,36 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class ResNet50(Model):
# NOTE:
# This model had it's best performance with the following configs
# numbre of classes
# CLASS_SIZE = 20
# BATCH_SIZE = 16
# SAMPLE_SIZE = 30
# TRAINING_SMPLE = 28
# LR_RATE = 0.0001
# EPOCHS = 15
# RESOLUTION = 224
# NOTE: But it may be a one time thing.
# because testing again didn't repeat
def get(self):
m = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
# freez all layers
#for param in m.parameters():
#param.requires_grad = False
# unfreez the last two
# NOTE: Freezing everything and unfrizing the last 3 yeilded the best performance
#for param in m.layer2.parameters(): param.requires_grad = True
#for param in m.layer3.parameters(): param.requires_grad = True
#for param in m.layer4.parameters(): param.requires_grad = True
m.fc = nn.Linear(m.fc.in_features, self.size)
return m

View File

@@ -0,0 +1,17 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class ShuffleNet(Model):
def get(self):
m = models.shufflenet_v2_x1_0(weights=models.ShuffleNet_V2_X1_0_Weights.DEFAULT)
num_ftrs = m.fc.in_features
m.fc = nn.Linear(num_ftrs, self.size)
return m

230
architectures/WFNet.py Normal file
View File

@@ -0,0 +1,230 @@
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_Module(nn.Module):
def __init__(self, original_model: nn.Module, num_classes: int, arch_enum):
super().__init__()
# If your model classes contain the raw inner torch model under an attribute,
# extract it. Otherwise, use it directly.
self.original_model = getattr(original_model, "model", original_model)
# Freeze the original model parameters completely
for param in self.original_model.parameters():
param.requires_grad = False
# Target layer discovery using your clean Enum contract
self.target_layer = self._deduce_target_layer(self.original_model, arch_enum)
# Derive channel dimensions dynamically from the deduced layer
out_channels = self._extract_channels(self.target_layer, self.original_model)
# Initialize alpha parameter matrix (Rows = Classes, Cols = Channels)
self.alpha = nn.Parameter(torch.full((num_classes, out_channels), 3.0))
self._current_target_indices = None
def _deduce_target_layer(self, model: nn.Module, arch_enum) -> nn.Module:
"""
Scans the architecture topology to target the final deep feature extraction block
right before global pooling/classification using strict Enum configurations.
"""
match arch_enum:
# --- RESNET FAMILY ---
case arch_enum.RESNET18 | arch_enum.RESNET34 | arch_enum.RESNET50 | arch_enum.WIDE_RESNET:
return model.layer4[-1]
# --- GOOGLENET ---
case arch_enum.GOOGLENET:
return model.inception5b
# --- INCEPTION V3 ---
case arch_enum.INCEPTION:
return model.Mixed_7c
# --- DENSENET 121 ---
case arch_enum.DENSENET121:
return model.features.norm5
# --- EFFICIENTNET ---
case arch_enum.EFFICIENTNET:
return model.features[-1]
# --- SHUFFLENET ---
case arch_enum.SHUFFLENET:
return model.conv5
case _:
# Robust Fallback Strategy
target = None
for module in model.modules():
if isinstance(module, nn.Conv2d):
target = module
if target is not None:
return target
raise RuntimeError(f"Could not locate filtration anchor for Enum target: {arch_enum}")
def _extract_channels(self, target_layer: nn.Module, model: nn.Module) -> int:
"""Helper to determine channel depth across varied layers types."""
if hasattr(target_layer, "out_channels"):
return target_layer.out_channels
if hasattr(target_layer, "num_features"):
return target_layer.num_features
if hasattr(target_layer, "weight"):
return target_layer.weight.shape[0]
# Classifier fallback mapping
if hasattr(model, "fc"):
return model.fc.in_features
if hasattr(model, "classifier"):
if isinstance(model.classifier, nn.Linear):
return model.classifier.in_features
if isinstance(model.classifier, nn.Sequential):
return model.classifier[0].in_features
return 512
def _filtration_hook(self, module: nn.Module, hook_input: tuple, hook_output: torch.Tensor) -> torch.Tensor:
if self._current_target_indices is None:
return hook_output
batch_alpha = self.alpha[self._current_target_indices]
if len(hook_output.shape) == 4:
mask = torch.sigmoid(batch_alpha).view(hook_output.size(0), -1, 1, 1)
else:
mask = torch.sigmoid(batch_alpha).view(hook_output.size(0), -1)
return hook_output * mask
def forward(self, x: torch.Tensor, target_class_indices: torch.Tensor) -> torch.Tensor:
self._current_target_indices = target_class_indices
hook_handle = self.target_layer.register_forward_hook(self._filtration_hook)
try:
logits = self.original_model(x)
finally:
hook_handle.remove()
self._current_target_indices = None
return logits
class WF_Net_Model(Model):
def __init__(self, device, size, original_model: nn.Module, target_class_index: int, arch):
self.device = device
self.size = size
self.wf_module = WF_Module(
arch_enum=arch,
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()

View File

@@ -0,0 +1,15 @@
import torch.nn as nn
from torchvision import models
# Base model
from architectures.Model import Model
class WideResNet(Model):
def get(self):
# wide_resnet50_2 is a common high-performance choice
m = models.wide_resnet50_2(weights=models.Wide_ResNet50_2_Weights.DEFAULT)
m.fc = nn.Linear(m.fc.in_features, self.size)
return m