Differential Privacy Applied in MNIST Dataset with Code

Nazmul Alom
Secure and Private AI Writing Challenge
7 min readJul 19, 2019

“Differential privacy is a constraint on the algorithms used to publish aggregate information about a statistical database which limits the disclosure of private information of records whose information is in the database.” — Wikipedia

MNIST Dataset

Here, We are going to apply Differential Privacy and PATE analysis on the MNIST dataset. We are not discussing any theory here, rather we will do some hands-on code on this dataset. And compare the Data Independent Epsilon and Data Dependent Epsilon from it.

First, let me briefly explain how this works. We need to train our dataset with private labels. But we will not make out labels private. Rather, we will acquire our labels from somewhere else. That’s why, we need some well-trained models, from where our dataset will get its labels. We can call them ‘Teacher Datasets’ and ‘Teacher Models’. So our dataset in ‘Student Dataset’. We will train our Student dataset on every Teacher model. And get their labels. We can anonymize the labels using Random Laplace Numbers from Numpy. And then we will get our best predictions from those labels and use them as our final labels. Finally, we can train our Student dataset with those final labels and test them with the Student Test dataset.

Now, we do not have any Teacher or Student dataset in MNIST. So we will divide the training dataset into subsets and use them as Teacher datasets. And we will use the test dataset as Student. We will split the Student dataset into training and testing part. So, we have the following steps to accomplish :

  1. Define the Teacher datasets
  2. Define the Student dataset and split it into training and testing datasets
  3. Define the Neural Network Model
  4. Train the Teacher datasets and find the Teacher models
  5. Using every trained Teacher models find predictions for the Student training dataset
  6. Then, get the private label for the Student training dataset
  7. PATE analyze the predictions and labels
  8. Replace the old labels from the Student training dataset with private labels
  9. Train the Student training dataset with new labels and test the accuracy with Student test dataset

We can start by importing all the necessary libraries and functions.

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets
import torchvision.transforms as transforms
from torch.utils.data import Subset

Downloading the dataset and transforming them into FloatTensor.

transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.5, ), (0.5,))])
train_data = datasets.MNIST(root=’data’, train=True, download=True, transform=transform)
test_data = datasets.MNIST(root=’data’, train=False, download=True, transform=transform)

We are going to divide the training dataset into ‘num_teachers’ pieces and call them teacher datasets. Then load them into different dataloaders.

batch_size = 32
num_teachers = 100
def teacher_loader_fn(train_data, num_teachers):
teacher_loaders = []
data_size = len(train_data) // num_teachers
for i in range(num_teachers):
indices = list(range(i*data_size, (i+1)*data_size))
subset = Subset(train_data, indices)
loader = torch.utils.data.DataLoader(subset, batch_size=batch_size)
teacher_loaders.append(loader)
return teacher_loaders
teacher_loaders = teacher_loader_fn(train_data, num_teachers)

Now, as I said earlier, we will use the test dataset as Student. We need to divide the test dataset into two parts; (i) Student training data, (ii) Student testing data. We will take most of the data for training. The length of the Student training dataset will be ‘num_student_train_set’ and rest will be used as a Student test dataset. And then load them into dataloaders.

num_student_train_set = 9000student_train_data = Subset(test_data, list(range(num_student_train_set)))
student_test_data = Subset(test_data, list(range(num_student_train_set, len(test_data))))
student_train_loader = torch.utils.data.DataLoader(student_train_data, batch_size=batch_size)
student_test_loader = torch.utils.data.DataLoader(student_test_data, batch_size=batch_size)

We are going to define the model, which we will be using for both Teachers and Student datasets. Here I am defining a simple model. But we can improve it later for a better approximation.

class Network(nn.Module):
def __init__(self):
super(Network, self).__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
self.conv2_drop = nn.Dropout2d()
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = F.dropout(x, training=self.training)
x = self.fc2(x)
x = F.log_softmax(x, dim=1)
return x

We are going to do the training in two steps. First, we are defining a ‘train’ method.

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
epochs=20
def train(model, trainloader, criterion, optimizer, epochs, print_every):
model.to(device)
running_loss = 0
for e in range(epochs):
model.train()
for images, labels in trainloader:
images, labels = images.to(device), labels.to(device)

optimizer.zero_grad()

output = model(images)
loss = criterion(output, labels)
loss.backward()
optimizer.step()

running_loss += loss.item()

Then, we are training the Teacher dataloaders with that method and getting respective teacher models in return.

def train_teachers(num_teachers, teacher_loaders):
models = []
for t in range(num_teachers):
print(“Training teacher {}”.format(t+1))
model = Network()
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.003)
train(model, teacher_loaders[t], criterion, optimizer, epochs, print_every)
models.append(model)
return models
teacher_models = train_teachers(num_teachers, teacher_loaders)

Defining ‘predict’ method to evaluate the teacher models with the Student train loader.

def predict(model, dataloader):
outputs = torch.zeros(0, dtype=torch.long).to(device)
model.to(device)
model.eval()
for images, labels in dataloader:
images, labels = images.to(device), labels.to(device)
output = model(images)
ps = torch.argmax(torch.exp(output), dim=1)
outputs = torch.cat((outputs, ps))

return outputs

Defining a method to find the predictions using the ‘predict’ method and returning the predictions as a numpy array. These are the predictions, that we get by running our Student Train dataset on every Teacher model. So, the shape of this ‘preds’ will be (num_teachers, num_student_train_set).
[(100, 9000) in this case]

def prediction_fn(models, data_loader):
preds = torch.torch.zeros((len(models), num_student_train_set), dtype=torch.long)
for i, model in enumerate(models):
results = predict(model, data_loader)
preds[i] = results
return preds.numpy()
preds = prediction_fn(teacher_models, student_train_loader)

Now, getting the private training labels for the Student training dataset according to every prediction. We are using ‘np.random.laplace’ for randomizing the data. And finally, get the labels for our Student training dataset. It’s shape is (num_student_train_set, ).
[(9000, ) in this case]

epsilon = 0.2def get_student_labels(preds, epsilon):
labels = np.array([]).astype(int)
for image_preds in np.transpose(preds):
label_counts = np.bincount(image_preds, minlength=10)
beta = 1 / epsilon
for i in range(len(label_counts)):
label_counts[i] += np.random.laplace(0, beta, 1)
new_label = np.argmax(label_counts)
labels = np.append(labels, new_label)
return labels
student_labels = get_student_labels(preds, epsilon)
Exhausted

PATE Analysis:

from syft.frameworks.torch.differential_privacy import patedata_dep_eps, data_ind_eps = pate.perform_analysis(teacher_preds=preds, indices=student_labels, noise_eps=epsilon, delta=1e-5)print(“Data Independent Epsilon:”, data_ind_eps)
print(“Data Dependent Epsilon:”, data_dep_eps)

The result is:

Data Independent Epsilon: 1451.5129254649705 
Data Dependent Epsilon: 1.47499221208096

Now, we are going to remove the labels from student training data loaders and add these new labels respectively:

def student_loader(student_train_loader, labels):
student_iterator = iter(student_train_loader)
for i, (data, _) in enumerate(student_iterator):
student_train_label = torch.from_numpy(labels[i*len(data):(i+1)*len(data)])
yield data, student_train_label

Using the same model, we are now Training the Student Train dataset and Testing the Student Test dataset and finding the accuracy:

student_model = Network()
criterion = nn.NLLLoss()
optimizer = optim.Adam(student_model.parameters(), lr=0.001)
student_model.to(device)
epochs=20
steps = 0
running_loss = 0
for e in range(epochs):
student_model.train()
train_loader = student_loader(student_train_loader, student_labels)
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
steps += 1
optimizer.zero_grad()
output = student_model(images)
loss = criterion(output, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
if steps % 50 == 0:
test_loss = 0
accuracy = 0
student_model.eval()
with torch.no_grad():
for images, labels in student_test_loader:
images, labels = images.to(device), labels.to(device)
log_ps = student_model(images)
test_loss += criterion(log_ps, labels).item()

ps = torch.exp(log_ps)
top_p, top_class = ps.topk(1, dim=1)
equals = top_class == labels.view(*top_class.shape)
accuracy += torch.mean(equals.type(torch.FloatTensor))
student_model.train()
print(“Epoch: {}/{}.. “.format(e+1, epochs), “Training Loss: {:.3f}.. “.format(running_loss/len(student_train_loader)), “Test Loss: {:.3f}.. “.format(test_loss/len(student_test_loader)), “Test Accuracy: {:.3f}”.format(accuracy/len(student_test_loader)))
running_loss = 0

The final result is:

Epoch: 1/20...  Training Loss: 1.186...  Test Loss: 0.336...  Test Accuracy: 0.916 
Epoch: 2/20... Training Loss: 0.484... Test Loss: 0.227... Test Accuracy: 0.935
Epoch: 3/20... Training Loss: 0.371... Test Loss: 0.205... Test Accuracy: 0.940
... ... ... ... ... ... ... ... ... ... ...... ... ... ... ... ... ... ... ... ... ...... ... ... ... ... ... ... ... ... ... ...Epoch: 18/20... Training Loss: 0.186... Test Loss: 0.155... Test Accuracy: 0.960
Epoch: 19/20... Training Loss: 0.185... Test Loss: 0.161... Test Accuracy: 0.964
Epoch: 20/20... Training Loss: 0.173... Test Loss: 0.153... Test Accuracy: 0.959

Now we want to know that, if we do this, how much our accuracy gets affected. For this, we will train the MNIST dataset and test it as usual with the same model, specified above, with the same number of epochs. Let’s see what happens.

trainloader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)
testloader = torch.utils.data.DataLoader(test_data, batch_size=64, shuffle=True)
model = Network()
criterion = nn.NLLLoss()
optimizer = optim.Adam(model.parameters(), lr=0.003)
epochs = 20
steps = 0
train_losses, test_losses = [], []
for e in range(epochs):
running_loss = 0
for images, labels in trainloader:

optimizer.zero_grad()

log_ps = model(images)
loss = criterion(log_ps, labels)
loss.backward()
optimizer.step()

running_loss += loss.item()

else:
test_loss = 0
accuracy = 0

with torch.no_grad():
model.eval()
for images, labels in testloader:
log_ps = model(images)
test_loss += criterion(log_ps, labels)

ps = torch.exp(log_ps)
top_p, top_class = ps.topk(1, dim=1)
equals = top_class == labels.view(*top_class.shape)
accuracy += torch.mean(equals.type(torch.FloatTensor))

model.train()

train_losses.append(running_loss/len(trainloader))
test_losses.append(test_loss/len(testloader))
print("Epoch: {}/{}.. ".format(e+1, epochs),
"Training Loss: {:.3f}.. ".format(train_losses[-1]),
"Test Loss: {:.3f}.. ".format(test_losses[-1]),
"Test Accuracy: {:.3f}".format(accuracy/len(testloader)))

The result is:

Epoch: 1/20.. Training Loss: 0.454.. Test Loss: 0.083.. Test Accuracy: 0.975 
Epoch: 2/20.. Training Loss: 0.251.. Test Loss: 0.069.. Test Accuracy: 0.981
Epoch: 3/20.. Training Loss: 0.211.. Test Loss: 0.064.. Test Accuracy: 0.980
... ... ... ... ... ... ... ... ... ... ...... ... ... ... ... ... ... ... ... ... ...... ... ... ... ... ... ... ... ... ... ...Epoch: 18/20.. Training Loss: 0.136.. Test Loss: 0.031.. Test Accuracy: 0.990
Epoch: 19/20.. Training Loss: 0.133.. Test Loss: 0.034.. Test Accuracy: 0.990
Epoch: 20/20.. Training Loss: 0.140.. Test Loss: 0.041.. Test Accuracy: 0.987

From this result, we can see that the accuracy is slightly different than the previous one. Actually, this one is better than the previous one. But we can compromise this little amount of accuracy in exchange for the privacy of our data. We also have to keep in mind that, the amount of data in the previous model was only 1000 and we have 60000 data here. So if we could afford this much of data there, the result would have been much better.

That’s all for now. Please add a response below for any kind of correction, advice or anything. I’ll be writing on the very basics of privacy and Federated Learning. Hope to see you there.

[A huge thanks to ‘Udacity Secure and Privacy AI Challenge’ course for giving me the opportunity to learn this.]

--

--