unlearning LF
This commit is contained in:
38
Data.py
38
Data.py
@@ -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
|
||||||
7
LinearFiltration_metrics.txt
Normal file
7
LinearFiltration_metrics.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
execution_time_sec
|
||||||
|
0.000996
|
||||||
|
0.030071
|
||||||
|
0.001182
|
||||||
|
0.001176
|
||||||
|
0.001229
|
||||||
|
0.001257
|
||||||
120
Tune.py
120
Tune.py
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
35
reports/LinearFiltration_metrics.txt
Normal file
35
reports/LinearFiltration_metrics.txt
Normal 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
|
||||||
21
reports/resnet50-finetunned.csv
Normal file
21
reports/resnet50-finetunned.csv
Normal 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
|
||||||
|
21
reports/resnet50-forget.csv
Normal file
21
reports/resnet50-forget.csv
Normal 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
|
||||||
|
21
reports/resnet50-retain.csv
Normal file
21
reports/resnet50-retain.csv
Normal 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,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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user