How to calculate epsilon for DP-SGD while applying using Func torch

Pradeep_Kumar_Ragu_C: <p>I’m training a simple NN with DP-SGD. I calculated per sample gradient using functorch, and added noise. I’m able to train the model with noise. But how do you calculate the privacy budget (epsilon)? and how to train the model with fixed privacy budget like OPACUS?</p>
<p>my current code<br>
import torch<br>
import torch.nn as nn<br>
import torch.nn.functional as F<br>
from torch.utils.data import DataLoader, Dataset<br>
import pandas as pd<br>
from sklearn.model_selection import train_test_split<br>
from sklearn.preprocessing import LabelEncoder, StandardScaler<br>
import matplotlib.pyplot as plt<br>
import math</p>
<h1><a name="p-455461-set-random-seed-for-reproducibility-1" class="anchor" href="#p-455461-set-random-seed-for-reproducibility-1"></a>Set random seed for reproducibility</h1>
<p>torch.manual_seed(0)</p>
<h1><a name="p-455461-load-and-preprocess-the-dataset-2" class="anchor" href="#p-455461-load-and-preprocess-the-dataset-2"></a>Load and preprocess the dataset</h1>
<p>car_data = pd.read_csv(‘/content/car.data’, header=None)</p>
<h1><a name="p-455461-preprocessing-steps-3" class="anchor" href="#p-455461-preprocessing-steps-3"></a>Preprocessing steps:</h1>
<h1><a name="p-455461-assume-the-last-column-is-the-target-and-the-rest-are-features-4" class="anchor" href="#p-455461-assume-the-last-column-is-the-target-and-the-rest-are-features-4"></a>Assume the last column is the target, and the rest are features.</h1>
<p>features = car_data.iloc[:, :-1]<br>
targets = car_data.iloc[:, -1]</p>
<h1><a name="p-455461-convert-categorical-features-and-targets-into-numerical-values-5" class="anchor" href="#p-455461-convert-categorical-features-and-targets-into-numerical-values-5"></a>Convert categorical features and targets into numerical values</h1>
<p>encoder = LabelEncoder()<br>
features = features.apply(encoder.fit_transform)<br>
targets = encoder.fit_transform(targets)</p>
<h1><a name="p-455461-scale-the-features-6" class="anchor" href="#p-455461-scale-the-features-6"></a>Scale the features</h1>
<p>scaler = StandardScaler()<br>
features = scaler.fit_transform(features)</p>
<h1><a name="p-455461-split-into-train-and-test-7" class="anchor" href="#p-455461-split-into-train-and-test-7"></a>Split into train and test</h1>
<p>X_train, X_test, y_train, y_test = train_test_split(features, targets, test_size=0.2, random_state=42)</p>
<h1><a name="p-455461-convert-data-to-pytorch-tensors-8" class="anchor" href="#p-455461-convert-data-to-pytorch-tensors-8"></a>Convert data to PyTorch tensors</h1>
<p>X_train_tensor = torch.tensor(X_train, dtype=torch.float32)<br>
y_train_tensor = torch.tensor(y_train, dtype=torch.long)<br>
X_test_tensor = torch.tensor(X_test, dtype=torch.float32)<br>
y_test_tensor = torch.tensor(y_test, dtype=torch.long)</p>
<h1><a name="p-455461-create-a-custom-dataset-class-9" class="anchor" href="#p-455461-create-a-custom-dataset-class-9"></a>Create a custom dataset class</h1>
<p>class CarDataset(Dataset):<br>
def <strong>init</strong>(self, X, y):<br>
self.X = X<br>
self.y = y</p>
<pre><code>def __len__(self):
    return len(self.X)

def __getitem__(self, idx):
    return self.X[idx], self.y[idx]
</code></pre>
<h1><a name="p-455461-create-dataloader-for-training-and-testing-10" class="anchor" href="#p-455461-create-dataloader-for-training-and-testing-10"></a>Create DataLoader for training and testing</h1>
<p>batch_size = 64<br>
train_dataset = CarDataset(X_train_tensor, y_train_tensor)<br>
test_dataset = CarDataset(X_test_tensor, y_test_tensor)</p>
<p>train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)<br>
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)</p>
<h1><a name="p-455461-define-the-nn-model-for-tabular-data-11" class="anchor" href="#p-455461-define-the-nn-model-for-tabular-data-11"></a>Define the NN model for tabular data</h1>
<p>class NN(nn.Module):<br>
def <strong>init</strong>(self, input_dim, output_dim):<br>
super(NN, self).<strong>init</strong>()<br>
self.fc1 = nn.Linear(input_dim, 128)<br>
self.fc2 = nn.Linear(128, 64)<br>
self.fc3 = nn.Linear(64, output_dim)</p>
<pre><code>def forward(self, x):
    x = F.relu(self.fc1(x))
    x = F.relu(self.fc2(x))
    x = self.fc3(x)
    return x
</code></pre>
<h1><a name="p-455461-loss-function-12" class="anchor" href="#p-455461-loss-function-12"></a>Loss function</h1>
<p>def loss_fn(predictions, targets):<br>
return F.cross_entropy(predictions, targets)</p>
<h1><a name="p-455461-set-device-13" class="anchor" href="#p-455461-set-device-13"></a>Set device</h1>
<p>device = torch.device(“cuda” if torch.cuda.is_available() else “cpu”)</p>
<h1><a name="p-455461-initialize-the-model-optimizer-and-other-hyperparameters-14" class="anchor" href="#p-455461-initialize-the-model-optimizer-and-other-hyperparameters-14"></a>Initialize the model, optimizer, and other hyperparameters</h1>
<p>input_dim = X_train.shape[1]<br>
output_dim = len(set(y_train))<br>
model = SimpleMLP(input_dim, output_dim).to(device)<br>
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)</p>
<h1><a name="p-455461-dp-sgd-parameters-15" class="anchor" href="#p-455461-dp-sgd-parameters-15"></a>DP-SGD parameters</h1>
<p>max_grad_norm = 1.0<br>
target_epsilon = 1.0  # Privacy budget target<br>
delta = 1e-5          # Delta value<br>
noise_multiplier = 20  # noise multiplier</p>
<h1><a name="p-455461-helper-functions-for-dp-sgd-16" class="anchor" href="#p-455461-helper-functions-for-dp-sgd-16"></a>Helper functions for DP-SGD</h1>
<p>def compute_per_sample_gradients(model, loss_fn, data, targets):<br>
model.zero_grad()<br>
outputs = model(data)<br>
loss = loss_fn(outputs, targets)<br>
per_sample_grads = torch.autograd.grad(loss, model.parameters(), create_graph=True)<br>
return per_sample_grads</p>
<p>def clip_gradients(gradients, max_norm):<br>
total_norm = torch.norm(torch.stack([torch.norm(g.detach(), p=2) for g in gradients]))<br>
clip_coef = max_norm / (total_norm + 1e-6)<br>
clip_coef_clamped = torch.clamp(clip_coef, max=1.0)<br>
return [g.detach() * clip_coef_clamped for g in gradients]</p>
<p>def add_noise(gradients, noise_multiplier, max_norm):<br>
noised_gradients = <span class="chcklst-box fa fa-square-o fa-fw"></span><br>
for grad in gradients:<br>
noise = torch.normal(0, noise_multiplier * max_norm / batch_size, grad.shape, device=grad.device)<br>
noised_gradients.append(grad + noise)<br>
return noised_gradients</p>
<p>def train_step(model, optimizer, data, targets):<br>
per_sample_grads = compute_per_sample_gradients(model, loss_fn, data, targets)<br>
clipped_grads = clip_gradients(per_sample_grads, max_grad_norm)<br>
noised_grads = add_noise(clipped_grads, noise_multiplier, max_grad_norm)</p>
<pre><code>optimizer.zero_grad()
for param, noised_grad in zip(model.parameters(), noised_grads):
    param.grad = noised_grad
optimizer.step()
</code></pre>
<p>def test(model, device, test_loader):<br>
model.eval()<br>
test_loss = 0<br>
correct = 0<br>
with torch.no_grad():<br>
for data, target in test_loader:<br>
data, target = data.to(device), target.to(device)<br>
output = model(data)<br>
test_loss += F.cross_entropy(output, target, reduction=‘sum’).item()<br>
pred = output.argmax(dim=1, keepdim=True)<br>
correct += pred.eq(target.view_as(pred)).sum().item()<br>
test_loss /= len(test_loader.dataset)<br>
accuracy = 100. * correct / len(test_loader.dataset)</p>
<pre><code>return test_loss, accuracy
</code></pre>
<p>def compute_epsilon(steps, noise_multiplier, batch_size, dataset_size, delta):<br>
“”“Compute epsilon using a simplified formula.”“”<br>
q = batch_size / dataset_size<br>
T = steps * q<br>
c = math.sqrt(2 * math.log(1.25 / delta))<br>
return c * math.sqrt(T) / noise_multiplier</p>
<h1><a name="p-455461-training-loop-17" class="anchor" href="#p-455461-training-loop-17"></a>Training loop</h1>
<p>steps = 0<br>
losses = <span class="chcklst-box fa fa-square-o fa-fw"></span><br>
epsilons = <span class="chcklst-box fa fa-square-o fa-fw"></span><br>
train_accuracies = <span class="chcklst-box fa fa-square-o fa-fw"></span></p>
<p>for epoch in range(50):  # Specify number of epochs here<br>
model.train()<br>
epoch_loss = 0<br>
correct_train = 0<br>
total_train = 0</p>
<pre><code>for batch_idx, (data, target) in enumerate(train_loader):
    data, target = data.to(device), target.to(device)

    # Perform a training step
    train_step(model, optimizer, data, target)
    steps += 1

    # Compute loss for the current batch
    outputs = model(data)
    loss = loss_fn(outputs, target)
    epoch_loss += loss.item()

    # Compute the number of correct predictions in the training batch
    pred = outputs.argmax(dim=1, keepdim=True)  # Get the index of the max log-probability
    correct_train += pred.eq(target.view_as(pred)).sum().item()
    total_train += target.size(0)

# Compute average loss over the training data
losses.append(epoch_loss / len(train_loader))

# Calculate training accuracy
train_accuracy = 100. * correct_train / total_train
train_accuracies.append(train_accuracy)

# Evaluate on test set
test_loss, test_accuracy = test(model, device, test_loader)

print(f'Epoch {epoch+1}: Train Loss: {epoch_loss / len(train_loader):.4f}, '
      f'Train Accuracy: {train_accuracy:.2f}%, '
      f'Test Loss: {test_loss:.4f}, Test Accuracy: {test_accuracy:.2f}%')

# Compute privacy spent
epsilon = compute_epsilon(steps, noise_multiplier, batch_size, len(train_dataset), delta)
epsilons.append(epsilon)
print(f"Current privacy guarantee: ε = {epsilon:.2f} at δ = {delta}")

# Stop if the current epsilon exceeds the target epsilon
if epsilon &gt;= target_epsilon:
    print("Target privacy budget reached. Stopping training.")
    break
</code></pre>
<h1><a name="p-455461-final-privacy-guarantee-18" class="anchor" href="#p-455461-final-privacy-guarantee-18"></a>Final privacy guarantee</h1>
<p>epsilon = compute_epsilon(steps, noise_multiplier, batch_size, len(train_dataset), delta)<br>
print(f"Final privacy guarantee: ε = {epsilon:.2f} at δ = {delta}")</p>