unlearning LF

This commit is contained in:
2026-06-07 10:36:03 +02:00
parent e90480adbe
commit 61c3447150
10 changed files with 395 additions and 73 deletions

38
Data.py
View File

@@ -1,8 +1,10 @@
from torchvision import datasets, transforms, models from torchvision import datasets, transforms, models
from torch.utils.data import Dataset, DataLoader, Subset
import torch import torch
import numpy as np import numpy as np
# train set transform # train set transform
def train_transform(res): def train_transform(res):
return transforms.Compose([ return transforms.Compose([
@@ -101,3 +103,39 @@ def get_indices(dataset, identities, split_at, size = 30):
test_indices.extend(indices[split_at:size]) test_indices.extend(indices[split_at:size])
return train_indices, test_indices return train_indices, test_indices
def get_forget_retain_loaders(dataset: Dataset, forget_class_idx: int, batch_size: int = 32) -> tuple[DataLoader, DataLoader]:
"""
Splits an IdentitySubset or standard Dataset into forget and retain sets
based on a remapped target class index.
"""
# 1. Safely extract targets whether it's a standard dataset or a Subset wrapper
if hasattr(dataset, 'targets'):
targets = dataset.targets
elif hasattr(dataset, 'identity'): # Raw CelebA support
targets = dataset.identity
else:
# If it's an IdentitySubset or standard Subset, extract mapped targets sequentially
# This guarantees we get the 0 -> (n-1) remapped labels
targets = [dataset[i][1] for i in range(len(dataset))]
if not isinstance(targets, torch.Tensor):
targets = torch.tensor(targets)
# 2. Generate mask indices local to this subset
forget_indices = torch.where(targets == forget_class_idx)[0].tolist()
retain_indices = torch.where(targets != forget_class_idx)[0].tolist()
# 3. Create PyTorch Subsets
forget_subset = Subset(dataset, forget_indices)
retain_subset = Subset(dataset, retain_indices)
# 4. Wrap into clean DataLoaders
forget_loader = DataLoader(forget_subset, batch_size=batch_size, shuffle=False)
retain_loader = DataLoader(retain_subset, batch_size=batch_size, shuffle=True)
print(f"[Data Split] Local Class {forget_class_idx}: {len(forget_subset)} samples | Remaining Classes: {len(retain_subset)} samples.")
return forget_loader, retain_loader

View File

@@ -0,0 +1,7 @@
execution_time_sec
0.000996
0.030071
0.001182
0.001176
0.001229
0.001257

120
Tune.py
View File

@@ -7,12 +7,13 @@ from sklearn.metrics import classification_report
import SetUp import SetUp
from Data import * from Data import *
#from datasets.Casia import * #from datasets.Casia import *
#from IdentitySubset import IdentitySubset from IdentitySubset import IdentitySubset
from datasets.UniversalIdentitySubset import UniversalIdentitySubset as IdentitySubset #from datasets.UniversalIdentitySubset import UniversalIdentitySubset as IdentitySubset
# models # models
from architectures.Model import Model, Architecture from architectures.Model import Model, Architecture
from unlearning import LinearFiltration, WeightFiltration, CertifiedRemoval from unlearning.LinearFiltration import LinearFiltration
# WeightFiltration, CertifiedRemoval
# numbre of classes # numbre of classes
CLASS_SIZE = 20 CLASS_SIZE = 20
@@ -24,7 +25,7 @@ BATCH_SIZE = 32
SAMPLE_SIZE = 30 SAMPLE_SIZE = 30
# this is then (full_sample - test_sample) # this is then (full_sample - test_sample)
TRAINING_SMPLE = 28 TRAINING_SMPLE = 27
# learning rate # learning rate
LR_RATE = 0.0001 LR_RATE = 0.0001
@@ -96,68 +97,79 @@ print(f'> Constants : Classes = {CLASS_SIZE}, Batch = {BATCH_SIZE}, epochs = {EP
# MODEL PREPARATION # MODEL PREPARATION
# cuda if exists (it does here) # cuda if exists (it does here)
device = SetUp.get_device() device = SetUp.get_device()
for i in range(0,CLASS_SIZE):
# Create model using Factory # Create model using Factory
model = Model.create( model = Model.create(
arch = arch, arch = arch,
device = device, device = device,
size = CLASS_SIZE) size = CLASS_SIZE)
# we may need to load existing model or finetune # we may need to load existing model or finetune
model.train( model.train(
epochs = EPOCHS, epochs = EPOCHS,
loader = train_loader, loader = train_loader,
rate = LR_RATE) rate = LR_RATE)
# save. # save.
model.save(filename=arch.name.lower()) model.save(filename=arch.name.lower())
# done tuning # done tuning
print('Model saved!')
# EVALUATE # EVALUATE
te_transform = test_transform(RESOLUTION)
# Testing
test_data = IdentitySubset(
dataset = dataset,
indices=test_indices,
id_mapping=id_map,
transform=te_transform)
te_transform = test_transform(RESOLUTION) test_loader = DataLoader(
# Testing test_data,
test_data = IdentitySubset( batch_size=BATCH_SIZE,
dataset = dataset, shuffle=False)
indices=test_indices,
id_mapping=id_map,
transform=te_transform)
test_loader = DataLoader( print(f"Total test images for these {CLASS_SIZE} classes: {len(test_data)}")
test_data,
batch_size=BATCH_SIZE,
shuffle=False)
print(f"Total test images for these {CLASS_SIZE} classes: {len(test_data)}") # Evaluate
model.evaluate(
loader = test_loader,
mode="finetunned"
)
# Evaluate # test again
model.evaluate( reloaded = Model.create(
loader = test_loader) arch=arch,
device = device,
size = CLASS_SIZE
)
reloaded.load(arch = arch)
print("fine tunned model loaded")
# reloaded.evaluate(
# loader = test_loader
#)
# test again # Unlearning
reloaded = Model.create( FORGET_CLASS_IDX = i
arch=arch,
device = device, forget_test_loader, retain_test_loader = get_forget_retain_loaders(
size = CLASS_SIZE dataset=test_data,
forget_class_idx=FORGET_CLASS_IDX,
batch_size=BATCH_SIZE
) )
reloaded.load(arch = arch)
print("Evaluating loaded") #retain_test_loader = DataLoader(retain_test_loader.dataset, batch_size=BATCH_SIZE, shuffle=False)
reloaded.evaluate(
loader = test_loader # 3. Instantiate and apply the Linear Filtration rule
) filtration = LinearFiltration(target_class_idx=FORGET_CLASS_IDX)
filtration.apply(reloaded.model)
strategies_to_test = [ # 4. Final Performance Analysis
LinearFiltration(target_class_idx=12), print("\n--- Performance on Retained Classes")
WeightFiltration(target_class_idx=12), reloaded.evaluate(loader=retain_test_loader, mode="retain")
CertifiedRemoval(target_class_idx=12)
]
# Run the comparative benchmark seamlessly print("\n--- Performance on Forgotten Class")
execution_profiles = {} reloaded.evaluate(loader=forget_test_loader,mode="forget")
for strategy in strategies_to_test:
# Each iteration clones weights back to fine-tuned state before running
runtime = my_model.unlearn(strategy, forget_loader, retain_loader)
execution_profiles[strategy.__class__.__name__] = runtime

View File

@@ -43,10 +43,13 @@ class Model(ABC):
if self.device.type == 'cuda': torch.cuda.synchronize() if self.device.type == 'cuda': torch.cuda.synchronize()
print(f"Training completed in: {time.time() - start_time:.2f}s") print(f"Training completed in: {time.time() - start_time:.2f}s")
def evaluate(self, loader): def evaluate(self, loader):
self.model.eval() self.model.eval()
all_preds, all_labels = [], [] all_preds, all_labels = [], []
print("\nEvaluating...") print("\nEvaluating...")
with torch.no_grad(): with torch.no_grad():
for inputs, labels in loader: for inputs, labels in loader:
@@ -56,9 +59,11 @@ class Model(ABC):
all_preds.extend(predicted.cpu().numpy()) all_preds.extend(predicted.cpu().numpy())
all_labels.extend(labels.cpu().numpy()) all_labels.extend(labels.cpu().numpy())
accuracy = 100 * (np.array(all_preds) == np.array(all_labels)).sum() / len(all_labels) classes = sorted(list(set(all_labels)))
accuracy = 100 * (np.array(all_preds) == np.array(all_labels)).sum() / len(classes)
print(f"Test Accuracy: {accuracy:.2f}%") print(f"Test Accuracy: {accuracy:.2f}%")
print(classification_report(all_labels, all_preds, zero_division=0)) print(classification_report(all_labels, all_preds, labels=classes, zero_division=0))
def save(self, filename=None): def save(self, filename=None):
@@ -101,13 +106,109 @@ class Model(ABC):
start_time = time.time() start_time = time.time()
# Delegate the actual algorithmic weight/logit manipulation to the strategy # Delegate the actual algorithmic weight/logit manipulation to the strategy
strategy.apply(self.network, forget_loader, retain_loader) strategy.apply(self.model, forget_loader, retain_loader)
elapsed_time = time.time() - start_time elapsed_time = time.time() - start_time
print(f"{strategy.__class__.__name__} completed in {elapsed_time:.4f} seconds.") print(f"{strategy.__class__.__name__} completed in {elapsed_time:.4f} seconds.")
return elapsed_time 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, classes, report_dict)
def _log_to_csv(self, mode, accuracy, classes, report_dict):
"""Handles directory structures, file setups, and distinct CSV column formatting."""
arch_name = self.__class__.__name__.lower()
save_dir = Path("reports")
save_dir.mkdir(parents=True, exist_ok=True)
csv_path = save_dir / f"{arch_name}-{mode}.csv"
file_exists = csv_path.exists()
'''
# Structure payload and headers based on evaluation slice type
if mode == "forget":
headers = ["accuracy", "precision", "recall", "f1-score"]
target_cls_str = str(classes[0])
metrics = report_dict[target_cls_str]
row = [
f"{accuracy / 100.0:.4f}",
f"{metrics['precision']:.4f}",
f"{metrics['recall']:.4f}",
f"{metrics['f1-score']:.4f}"
]
else:
headers = [
"accuracy",
"macro_precision", "macro_recall", "macro_f1",
"weighted_precision", "weighted_recall", "weighted_f1"
]
row = [
f"{accuracy / 100.0:.4f}",
f"{report_dict['macro avg']['precision']:.4f}",
f"{report_dict['macro avg']['recall']:.4f}",
f"{report_dict['macro avg']['f1-score']:.4f}",
f"{report_dict['weighted avg']['precision']:.4f}",
f"{report_dict['weighted avg']['recall']:.4f}",
f"{report_dict['weighted avg']['f1-score']:.4f}"
]'''
headers = [
"accuracy",
"macro_precision", "macro_recall", "macro_f1",
"weighted_precision", "weighted_recall", "weighted_f1"
]
row = [
f"{accuracy / 100.0:.4f}",
f"{report_dict['macro avg']['precision']:.4f}",
f"{report_dict['macro avg']['recall']:.4f}",
f"{report_dict['macro avg']['f1-score']:.4f}",
f"{report_dict['weighted avg']['precision']:.4f}",
f"{report_dict['weighted avg']['recall']:.4f}",
f"{report_dict['weighted avg']['f1-score']:.4f}"
]
with open(csv_path, "a") as f:
if not file_exists:
f.write(",".join(headers) + "\n")
f.write(",".join(row) + "\n")
print(f">> Direct CSV metrics appended to {csv_path}")
# Using the factory patern here # Using the factory patern here

View File

@@ -0,0 +1,35 @@
execution_time_sec
0.001269
0.001227
0.001298
0.001281
0.001178
0.001392
0.001391
0.001338
0.001162
0.001355
0.001361
0.001241
0.001210
0.001152
0.001358
0.001250
0.001467
0.001248
0.001411
0.001470
0.001241
0.001366
0.001206
0.001339
0.001268
0.002847
0.001245
0.001299
0.001222
0.001274
0.001351
0.001401
0.001286
0.001214

View File

@@ -0,0 +1,21 @@
accuracy,macro_precision,macro_recall,macro_f1,weighted_precision,weighted_recall,weighted_f1
0.9333,0.9500,0.9333,0.9264,0.9500,0.9333,0.9264
0.9167,0.9425,0.9167,0.9111,0.9425,0.9167,0.9111
0.9333,0.9500,0.9333,0.9264,0.9500,0.9333,0.9264
0.8667,0.9050,0.8667,0.8625,0.9050,0.8667,0.8625
0.9000,0.9208,0.9000,0.8976,0.9208,0.9000,0.8976
0.9000,0.9208,0.9000,0.8926,0.9208,0.9000,0.8926
0.9667,0.9750,0.9667,0.9657,0.9750,0.9667,0.9657
0.9000,0.9308,0.9000,0.9012,0.9308,0.9000,0.9012
0.9833,0.9875,0.9833,0.9829,0.9875,0.9833,0.9829
0.9500,0.9625,0.9500,0.9486,0.9625,0.9500,0.9486
0.8667,0.9008,0.8667,0.8551,0.9008,0.8667,0.8551
0.9167,0.9375,0.9167,0.9093,0.9375,0.9167,0.9093
0.9000,0.9250,0.9000,0.8921,0.9250,0.9000,0.8921
0.9000,0.9333,0.9000,0.9024,0.9333,0.9000,0.9024
0.9500,0.9625,0.9500,0.9486,0.9625,0.9500,0.9486
0.9167,0.9333,0.9167,0.9148,0.9333,0.9167,0.9148
0.9167,0.9375,0.9167,0.9143,0.9375,0.9167,0.9143
0.9667,0.9750,0.9667,0.9657,0.9750,0.9667,0.9657
0.9333,0.9500,0.9333,0.9314,0.9500,0.9333,0.9314
0.9000,0.9350,0.9000,0.8957,0.9350,0.9000,0.8957
1 accuracy macro_precision macro_recall macro_f1 weighted_precision weighted_recall weighted_f1
2 0.9333 0.9500 0.9333 0.9264 0.9500 0.9333 0.9264
3 0.9167 0.9425 0.9167 0.9111 0.9425 0.9167 0.9111
4 0.9333 0.9500 0.9333 0.9264 0.9500 0.9333 0.9264
5 0.8667 0.9050 0.8667 0.8625 0.9050 0.8667 0.8625
6 0.9000 0.9208 0.9000 0.8976 0.9208 0.9000 0.8976
7 0.9000 0.9208 0.9000 0.8926 0.9208 0.9000 0.8926
8 0.9667 0.9750 0.9667 0.9657 0.9750 0.9667 0.9657
9 0.9000 0.9308 0.9000 0.9012 0.9308 0.9000 0.9012
10 0.9833 0.9875 0.9833 0.9829 0.9875 0.9833 0.9829
11 0.9500 0.9625 0.9500 0.9486 0.9625 0.9500 0.9486
12 0.8667 0.9008 0.8667 0.8551 0.9008 0.8667 0.8551
13 0.9167 0.9375 0.9167 0.9093 0.9375 0.9167 0.9093
14 0.9000 0.9250 0.9000 0.8921 0.9250 0.9000 0.8921
15 0.9000 0.9333 0.9000 0.9024 0.9333 0.9000 0.9024
16 0.9500 0.9625 0.9500 0.9486 0.9625 0.9500 0.9486
17 0.9167 0.9333 0.9167 0.9148 0.9333 0.9167 0.9148
18 0.9167 0.9375 0.9167 0.9143 0.9375 0.9167 0.9143
19 0.9667 0.9750 0.9667 0.9657 0.9750 0.9667 0.9657
20 0.9333 0.9500 0.9333 0.9314 0.9500 0.9333 0.9314
21 0.9000 0.9350 0.9000 0.8957 0.9350 0.9000 0.8957

View File

@@ -0,0 +1,21 @@
accuracy,macro_precision,macro_recall,macro_f1,weighted_precision,weighted_recall,weighted_f1
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
0.0000,0.0000,0.0000,0.0000,0.0000,0.0000,0.0000
1 accuracy macro_precision macro_recall macro_f1 weighted_precision weighted_recall weighted_f1
2 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
3 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
4 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
5 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
6 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
7 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
8 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
9 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
10 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
11 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
12 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
13 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
14 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
15 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
16 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
17 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
18 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
19 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
20 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000
21 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000

View File

@@ -0,0 +1,21 @@
accuracy,macro_precision,macro_recall,macro_f1,weighted_precision,weighted_recall,weighted_f1,
0.9474,0.9605,0.9474,0.9459,0.9605,0.9474,0.9459
0.9123,0.9395,0.9123,0.9064,0.9395,0.9123,0.9064
0.9298,0.9526,0.9298,0.9244,0.9526,0.9298,0.9244
0.8596,0.9000,0.8596,0.8553,0.9000,0.8596,0.8553
0.9123,0.9298,0.9123,0.9103,0.9298,0.9123,0.9103
0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
0.8947,0.9272,0.8947,0.8960,0.9272,0.8947,0.8960
1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000
0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
0.8772,0.9132,0.8772,0.8650,0.9132,0.8772,0.8650
0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
0.9298,0.9605,0.9298,0.9328,0.9605,0.9298,0.9328
0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
0.9298,0.9430,0.9298,0.9283,0.9430,0.9298,0.9283
0.9298,0.9474,0.9298,0.9278,0.9474,0.9298,0.9278
0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
0.9298,0.9474,0.9298,0.9278,0.9474,0.9298,0.9278
0.8947,0.9316,0.8947,0.8902,0.9316,0.8947,0.8902
1 accuracy,macro_precision,macro_recall,macro_f1,weighted_precision,weighted_recall,weighted_f1,
2 0.9474,0.9605,0.9474,0.9459,0.9605,0.9474,0.9459
3 0.9123,0.9395,0.9123,0.9064,0.9395,0.9123,0.9064
4 0.9298,0.9526,0.9298,0.9244,0.9526,0.9298,0.9244
5 0.8596,0.9000,0.8596,0.8553,0.9000,0.8596,0.8553
6 0.9123,0.9298,0.9123,0.9103,0.9298,0.9123,0.9103
7 0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
8 0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
9 0.8947,0.9272,0.8947,0.8960,0.9272,0.8947,0.8960
10 1.0000,1.0000,1.0000,1.0000,1.0000,1.0000,1.0000
11 0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
12 0.8772,0.9132,0.8772,0.8650,0.9132,0.8772,0.8650
13 0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
14 0.9123,0.9342,0.9123,0.9045,0.9342,0.9123,0.9045
15 0.9298,0.9605,0.9298,0.9328,0.9605,0.9298,0.9328
16 0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
17 0.9298,0.9430,0.9298,0.9283,0.9430,0.9298,0.9283
18 0.9298,0.9474,0.9298,0.9278,0.9474,0.9298,0.9278
19 0.9649,0.9737,0.9649,0.9639,0.9737,0.9649,0.9639
20 0.9298,0.9474,0.9298,0.9278,0.9474,0.9298,0.9278
21 0.8947,0.9316,0.8947,0.8902,0.9316,0.8947,0.8902

View File

@@ -1,29 +1,48 @@
import torch import torch
from Strategy import Strategy import torch.nn as nn
from .Strategy import Strategy
class NormalizingLinearFiltration(Strategy): class LinearFiltration(Strategy):
def __init__(self, target_class_idx): def __init__(self, target_class_idx: int):
super().__init__() # Automatically configures 'NormalizingLinearFiltration_metrics.txt'
self.target_class_idx = target_class_idx self.target_class_idx = target_class_idx
def apply(self, model, forget_loader, retain_loader): def _run(self, model: nn.Module) -> nn.Module:
model.eval() model.eval()
# Freeze parameters structurally
for param in model.parameters(): for param in model.parameters():
param.requires_grad = False param.requires_grad = False
with torch.no_grad(): with torch.no_grad():
# we modify only classification head W = model.fc.weight.data.clone()
# Shape: [num_classes, feature_dim]
W = model.fc.weight.data
# Compute the normalization transformation projection matrix (A)
# (In your full code, calculate A here matching Baumhauer et al.'s equations)
num_classes = W.shape[0] num_classes = W.shape[0]
A = torch.eye(num_classes, device=W.device)
# Mask/blend target class index distribution configurations here...
A[self.target_class_idx, :] = 0.0
# 3. Direct weight matrix override: W_filtered = A * W A = self._calculate_filtration_matrix(num_classes, self.target_class_idx, W.device)
sanitized_W = torch.mm(A, W) sanitized_W = torch.mm(A, W)
model.fc.weight.copy_(sanitized_W) model.fc.weight.copy_(sanitized_W)
return model
'''@staticmethod
def _calculate_filtration_matrix(num_classes: int, forget_class: int, device: torch.device) -> torch.Tensor:
A = torch.eye(num_classes, device=device)
num_remaining_classes = num_classes - 1
for j in range(num_classes):
if j == forget_class:
A[forget_class, j] = 0.0
else:
A[forget_class, j] = 1.0 / num_remaining_classes
return A'''
@staticmethod
def _calculate_filtration_matrix(num_classes: int, forget_class: int, device: torch.device) -> torch.Tensor:
A = torch.eye(num_classes, device=device)
num_remaining = num_classes - 1
# The row of the forgotten class should average the inputs of all other classes
for j in range(num_classes):
if j == forget_class:
A[forget_class, j] = 0.0
else:
A[forget_class, j] = 1.0 / num_remaining
return A

View File

@@ -0,0 +1,47 @@
import torch.nn as nn
import time
import os
class Strategy:
"""Abstract base class for unlearning algorithms with automated, strategy-specific logging."""
def __init__(self):
# Dynamically set file name based on the class name (e.g., 'NormalizingLinearFiltration.txt')
self.strategy_name = self.__class__.__name__
self.log_file = f"reports/{self.strategy_name}_metrics.txt"
self._initialize_log_file()
def _initialize_log_file(self):
"""Creates a unique log file for this strategy with a header if it doesn't exist."""
if not os.path.exists(self.log_file):
with open(self.log_file, "w") as f:
f.write("execution_time_sec\n")
def log_metric(self, execution_time: float):
"""Appends the execution time to this strategy's specific file."""
with open(self.log_file, "a") as f:
f.write(f"{execution_time:.6f}\n")
def apply(self, model: nn.Module) -> nn.Module:
"""
Wraps the unlearning execution with automated timing and strategy-specific logging.
DO NOT override this method in subclasses. Override _run instead.
"""
start_time = time.perf_counter()
# Execute core unlearning logic
processed_model = self._run(model)
end_time = time.perf_counter()
execution_time = end_time - start_time
# Log to the strategy's specific file
self.log_metric(execution_time)
print(f"[{self.strategy_name}] Completed in {execution_time:.6f} seconds. Saved to {self.log_file}")
return processed_model
def _run(self, model: nn.Module) -> nn.Module:
"""Subclasses implement their core unlearning logic here."""
raise NotImplementedError