optimised
This commit is contained in:
15
architectures/DenseNet121.py
Normal file
15
architectures/DenseNet121.py
Normal 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
|
||||
19
architectures/EfficentNet.py
Normal file
19
architectures/EfficentNet.py
Normal 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
|
||||
31
architectures/GoogleNet.py
Normal file
31
architectures/GoogleNet.py
Normal 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
|
||||
47
architectures/Inception.py
Normal file
47
architectures/Inception.py
Normal 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
225
architectures/Model.py
Normal 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
22
architectures/ResNet18.py
Normal 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
22
architectures/ResNet34.py
Normal 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
36
architectures/ResNet50.py
Normal 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
|
||||
17
architectures/ShuffleNet.py
Normal file
17
architectures/ShuffleNet.py
Normal 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
230
architectures/WFNet.py
Normal 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()
|
||||
15
architectures/WideResNet.py
Normal file
15
architectures/WideResNet.py
Normal 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
|
||||
Reference in New Issue
Block a user