Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/main/beginner_source/knowledge_distillation_tutorial.py
Views: 712
# -*- coding: utf-8 -*-1"""2Knowledge Distillation Tutorial3===============================4**Author**: `Alexandros Chariton <https://github.com/AlexandrosChrtn>`_5"""67######################################################################8# Knowledge distillation is a technique that enables knowledge transfer from large, computationally expensive9# models to smaller ones without losing validity. This allows for deployment on less powerful10# hardware, making evaluation faster and more efficient.11#12# In this tutorial, we will run a number of experiments focused at improving the accuracy of a13# lightweight neural network, using a more powerful network as a teacher.14# The computational cost and the speed of the lightweight network will remain unaffected,15# our intervention only focuses on its weights, not on its forward pass.16# Applications of this technology can be found in devices such as drones or mobile phones.17# In this tutorial, we do not use any external packages as everything we need is available in ``torch`` and18# ``torchvision``.19#20# In this tutorial, you will learn:21#22# - How to modify model classes to extract hidden representations and use them for further calculations23# - How to modify regular train loops in PyTorch to include additional losses on top of, for example, cross-entropy for classification24# - How to improve the performance of lightweight models by using more complex models as teachers25#26# Prerequisites27# ~~~~~~~~~~~~~28#29# * 1 GPU, 4GB of memory30# * PyTorch v2.0 or later31# * CIFAR-10 dataset (downloaded by the script and saved in a directory called ``/data``)3233import torch34import torch.nn as nn35import torch.optim as optim36import torchvision.transforms as transforms37import torchvision.datasets as datasets3839# Check if GPU is available, and if not, use the CPU40device = torch.device("cuda" if torch.cuda.is_available() else "cpu")4142######################################################################43# Loading CIFAR-1044# ----------------45# CIFAR-10 is a popular image dataset with ten classes. Our objective is to predict one of the following classes for each input image.46#47# .. figure:: /../_static/img/cifar10.png48# :align: center49#50# Example of CIFAR-10 images51#52# The input images are RGB, so they have 3 channels and are 32x32 pixels. Basically, each image is described by 3 x 32 x 32 = 3072 numbers ranging from 0 to 255.53# A common practice in neural networks is to normalize the input, which is done for multiple reasons,54# including avoiding saturation in commonly used activation functions and increasing numerical stability.55# Our normalization process consists of subtracting the mean and dividing by the standard deviation along each channel.56# The tensors "mean=[0.485, 0.456, 0.406]" and "std=[0.229, 0.224, 0.225]" were already computed,57# and they represent the mean and standard deviation of each channel in the58# predefined subset of CIFAR-10 intended to be the training set.59# Notice how we use these values for the test set as well, without recomputing the mean and standard deviation from scratch.60# This is because the network was trained on features produced by subtracting and dividing the numbers above, and we want to maintain consistency.61# Furthermore, in real life, we would not be able to compute the mean and standard deviation of the test set since,62# under our assumptions, this data would not be accessible at that point.63#64# As a closing point, we often refer to this held-out set as the validation set, and we use a separate set,65# called the test set, after optimizing a model's performance on the validation set.66# This is done to avoid selecting a model based on the greedy and biased optimization of a single metric.6768# Below we are preprocessing data for CIFAR-10. We use an arbitrary batch size of 128.69transforms_cifar = transforms.Compose([70transforms.ToTensor(),71transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),72])7374# Loading the CIFAR-10 dataset:75train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transforms_cifar)76test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transforms_cifar)7778########################################################################79# .. note:: This section is for CPU users only who are interested in quick results. Use this option only if you're interested in a small scale experiment. Keep in mind the code should run fairly quickly using any GPU. Select only the first ``num_images_to_keep`` images from the train/test dataset80#81# .. code-block:: python82#83# #from torch.utils.data import Subset84# #num_images_to_keep = 200085# #train_dataset = Subset(train_dataset, range(min(num_images_to_keep, 50_000)))86# #test_dataset = Subset(test_dataset, range(min(num_images_to_keep, 10_000)))8788#Dataloaders89train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2)90test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=2)9192######################################################################93# Defining model classes and utility functions94# --------------------------------------------95# Next, we need to define our model classes. Several user-defined parameters need to be set here. We use two different architectures, keeping the number of filters fixed across our experiments to ensure fair comparisons.96# Both architectures are Convolutional Neural Networks (CNNs) with a different number of convolutional layers that serve as feature extractors, followed by a classifier with 10 classes.97# The number of filters and neurons is smaller for the students.9899# Deeper neural network class to be used as teacher:100class DeepNN(nn.Module):101def __init__(self, num_classes=10):102super(DeepNN, self).__init__()103self.features = nn.Sequential(104nn.Conv2d(3, 128, kernel_size=3, padding=1),105nn.ReLU(),106nn.Conv2d(128, 64, kernel_size=3, padding=1),107nn.ReLU(),108nn.MaxPool2d(kernel_size=2, stride=2),109nn.Conv2d(64, 64, kernel_size=3, padding=1),110nn.ReLU(),111nn.Conv2d(64, 32, kernel_size=3, padding=1),112nn.ReLU(),113nn.MaxPool2d(kernel_size=2, stride=2),114)115self.classifier = nn.Sequential(116nn.Linear(2048, 512),117nn.ReLU(),118nn.Dropout(0.1),119nn.Linear(512, num_classes)120)121122def forward(self, x):123x = self.features(x)124x = torch.flatten(x, 1)125x = self.classifier(x)126return x127128# Lightweight neural network class to be used as student:129class LightNN(nn.Module):130def __init__(self, num_classes=10):131super(LightNN, self).__init__()132self.features = nn.Sequential(133nn.Conv2d(3, 16, kernel_size=3, padding=1),134nn.ReLU(),135nn.MaxPool2d(kernel_size=2, stride=2),136nn.Conv2d(16, 16, kernel_size=3, padding=1),137nn.ReLU(),138nn.MaxPool2d(kernel_size=2, stride=2),139)140self.classifier = nn.Sequential(141nn.Linear(1024, 256),142nn.ReLU(),143nn.Dropout(0.1),144nn.Linear(256, num_classes)145)146147def forward(self, x):148x = self.features(x)149x = torch.flatten(x, 1)150x = self.classifier(x)151return x152153######################################################################154# We employ 2 functions to help us produce and evaluate the results on our original classification task.155# One function is called ``train`` and takes the following arguments:156#157# - ``model``: A model instance to train (update its weights) via this function.158# - ``train_loader``: We defined our ``train_loader`` above, and its job is to feed the data into the model.159# - ``epochs``: How many times we loop over the dataset.160# - ``learning_rate``: The learning rate determines how large our steps towards convergence should be. Too large or too small steps can be detrimental.161# - ``device``: Determines the device to run the workload on. Can be either CPU or GPU depending on availability.162#163# Our test function is similar, but it will be invoked with ``test_loader`` to load images from the test set.164#165# .. figure:: /../_static/img/knowledge_distillation/ce_only.png166# :align: center167#168# Train both networks with Cross-Entropy. The student will be used as a baseline:169#170171def train(model, train_loader, epochs, learning_rate, device):172criterion = nn.CrossEntropyLoss()173optimizer = optim.Adam(model.parameters(), lr=learning_rate)174175model.train()176177for epoch in range(epochs):178running_loss = 0.0179for inputs, labels in train_loader:180# inputs: A collection of batch_size images181# labels: A vector of dimensionality batch_size with integers denoting class of each image182inputs, labels = inputs.to(device), labels.to(device)183184optimizer.zero_grad()185outputs = model(inputs)186187# outputs: Output of the network for the collection of images. A tensor of dimensionality batch_size x num_classes188# labels: The actual labels of the images. Vector of dimensionality batch_size189loss = criterion(outputs, labels)190loss.backward()191optimizer.step()192193running_loss += loss.item()194195print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss / len(train_loader)}")196197def test(model, test_loader, device):198model.to(device)199model.eval()200201correct = 0202total = 0203204with torch.no_grad():205for inputs, labels in test_loader:206inputs, labels = inputs.to(device), labels.to(device)207208outputs = model(inputs)209_, predicted = torch.max(outputs.data, 1)210211total += labels.size(0)212correct += (predicted == labels).sum().item()213214accuracy = 100 * correct / total215print(f"Test Accuracy: {accuracy:.2f}%")216return accuracy217218######################################################################219# Cross-entropy runs220# ------------------221# For reproducibility, we need to set the torch manual seed. We train networks using different methods, so to compare them fairly,222# it makes sense to initialize the networks with the same weights.223# Start by training the teacher network using cross-entropy:224225torch.manual_seed(42)226nn_deep = DeepNN(num_classes=10).to(device)227train(nn_deep, train_loader, epochs=10, learning_rate=0.001, device=device)228test_accuracy_deep = test(nn_deep, test_loader, device)229230# Instantiate the lightweight network:231torch.manual_seed(42)232nn_light = LightNN(num_classes=10).to(device)233234######################################################################235# We instantiate one more lightweight network model to compare their performances.236# Back propagation is sensitive to weight initialization,237# so we need to make sure these two networks have the exact same initialization.238239torch.manual_seed(42)240new_nn_light = LightNN(num_classes=10).to(device)241242######################################################################243# To ensure we have created a copy of the first network, we inspect the norm of its first layer.244# If it matches, then we are safe to conclude that the networks are indeed the same.245246# Print the norm of the first layer of the initial lightweight model247print("Norm of 1st layer of nn_light:", torch.norm(nn_light.features[0].weight).item())248# Print the norm of the first layer of the new lightweight model249print("Norm of 1st layer of new_nn_light:", torch.norm(new_nn_light.features[0].weight).item())250251######################################################################252# Print the total number of parameters in each model:253total_params_deep = "{:,}".format(sum(p.numel() for p in nn_deep.parameters()))254print(f"DeepNN parameters: {total_params_deep}")255total_params_light = "{:,}".format(sum(p.numel() for p in nn_light.parameters()))256print(f"LightNN parameters: {total_params_light}")257258######################################################################259# Train and test the lightweight network with cross entropy loss:260train(nn_light, train_loader, epochs=10, learning_rate=0.001, device=device)261test_accuracy_light_ce = test(nn_light, test_loader, device)262263######################################################################264# As we can see, based on test accuracy, we can now compare the deeper network that is to be used as a teacher with the lightweight network that is our supposed student. So far, our student has not intervened with the teacher, therefore this performance is achieved by the student itself.265# The metrics so far can be seen with the following lines:266267print(f"Teacher accuracy: {test_accuracy_deep:.2f}%")268print(f"Student accuracy: {test_accuracy_light_ce:.2f}%")269270######################################################################271# Knowledge distillation run272# --------------------------273# Now let's try to improve the test accuracy of the student network by incorporating the teacher.274# Knowledge distillation is a straightforward technique to achieve this,275# based on the fact that both networks output a probability distribution over our classes.276# Therefore, the two networks share the same number of output neurons.277# The method works by incorporating an additional loss into the traditional cross entropy loss,278# which is based on the softmax output of the teacher network.279# The assumption is that the output activations of a properly trained teacher network carry additional information that can be leveraged by a student network during training.280# The original work suggests that utilizing ratios of smaller probabilities in the soft targets can help achieve the underlying objective of deep neural networks,281# which is to create a similarity structure over the data where similar objects are mapped closer together.282# For example, in CIFAR-10, a truck could be mistaken for an automobile or airplane,283# if its wheels are present, but it is less likely to be mistaken for a dog.284# Therefore, it makes sense to assume that valuable information resides not only in the top prediction of a properly trained model but in the entire output distribution.285# However, cross entropy alone does not sufficiently exploit this information as the activations for non-predicted classes286# tend to be so small that propagated gradients do not meaningfully change the weights to construct this desirable vector space.287#288# As we continue defining our first helper function that introduces a teacher-student dynamic, we need to include a few extra parameters:289#290# - ``T``: Temperature controls the smoothness of the output distributions. Larger ``T`` leads to smoother distributions, thus smaller probabilities get a larger boost.291# - ``soft_target_loss_weight``: A weight assigned to the extra objective we're about to include.292# - ``ce_loss_weight``: A weight assigned to cross-entropy. Tuning these weights pushes the network towards optimizing for either objective.293#294# .. figure:: /../_static/img/knowledge_distillation/distillation_output_loss.png295# :align: center296#297# Distillation loss is calculated from the logits of the networks. It only returns gradients to the student:298#299300def train_knowledge_distillation(teacher, student, train_loader, epochs, learning_rate, T, soft_target_loss_weight, ce_loss_weight, device):301ce_loss = nn.CrossEntropyLoss()302optimizer = optim.Adam(student.parameters(), lr=learning_rate)303304teacher.eval() # Teacher set to evaluation mode305student.train() # Student to train mode306307for epoch in range(epochs):308running_loss = 0.0309for inputs, labels in train_loader:310inputs, labels = inputs.to(device), labels.to(device)311312optimizer.zero_grad()313314# Forward pass with the teacher model - do not save gradients here as we do not change the teacher's weights315with torch.no_grad():316teacher_logits = teacher(inputs)317318# Forward pass with the student model319student_logits = student(inputs)320321#Soften the student logits by applying softmax first and log() second322soft_targets = nn.functional.softmax(teacher_logits / T, dim=-1)323soft_prob = nn.functional.log_softmax(student_logits / T, dim=-1)324325# Calculate the soft targets loss. Scaled by T**2 as suggested by the authors of the paper "Distilling the knowledge in a neural network"326soft_targets_loss = torch.sum(soft_targets * (soft_targets.log() - soft_prob)) / soft_prob.size()[0] * (T**2)327328# Calculate the true label loss329label_loss = ce_loss(student_logits, labels)330331# Weighted sum of the two losses332loss = soft_target_loss_weight * soft_targets_loss + ce_loss_weight * label_loss333334loss.backward()335optimizer.step()336337running_loss += loss.item()338339print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss / len(train_loader)}")340341# Apply ``train_knowledge_distillation`` with a temperature of 2. Arbitrarily set the weights to 0.75 for CE and 0.25 for distillation loss.342train_knowledge_distillation(teacher=nn_deep, student=new_nn_light, train_loader=train_loader, epochs=10, learning_rate=0.001, T=2, soft_target_loss_weight=0.25, ce_loss_weight=0.75, device=device)343test_accuracy_light_ce_and_kd = test(new_nn_light, test_loader, device)344345# Compare the student test accuracy with and without the teacher, after distillation346print(f"Teacher accuracy: {test_accuracy_deep:.2f}%")347print(f"Student accuracy without teacher: {test_accuracy_light_ce:.2f}%")348print(f"Student accuracy with CE + KD: {test_accuracy_light_ce_and_kd:.2f}%")349350######################################################################351# Cosine loss minimization run352# ----------------------------353# Feel free to play around with the temperature parameter that controls the softness of the softmax function and the loss coefficients.354# In neural networks, it is easy to include additional loss functions to the main objectives to achieve goals like better generalization.355# Let's try including an objective for the student, but now let's focus on their hidden states rather than their output layers.356# Our goal is to convey information from the teacher's representation to the student by including a naive loss function,357# whose minimization implies that the flattened vectors that are subsequently passed to the classifiers have become more *similar* as the loss decreases.358# Of course, the teacher does not update its weights, so the minimization depends only on the student's weights.359# The rationale behind this method is that we are operating under the assumption that the teacher model has a better internal representation that is360# unlikely to be achieved by the student without external intervention, therefore we artificially push the student to mimic the internal representation of the teacher.361# Whether or not this will end up helping the student is not straightforward, though, because pushing the lightweight network362# to reach this point could be a good thing, assuming that we have found an internal representation that leads to better test accuracy,363# but it could also be harmful because the networks have different architectures and the student does not have the same learning capacity as the teacher.364# In other words, there is no reason for these two vectors, the student's and the teacher's to match per component.365# The student could reach an internal representation that is a permutation of the teacher's and it would be just as efficient.366# Nonetheless, we can still run a quick experiment to figure out the impact of this method.367# We will be using the ``CosineEmbeddingLoss`` which is given by the following formula:368#369# .. figure:: /../_static/img/knowledge_distillation/cosine_embedding_loss.png370# :align: center371# :width: 450px372#373# Formula for CosineEmbeddingLoss374#375# Obviously, there is one thing that we need to resolve first.376# When we applied distillation to the output layer we mentioned that both networks have the same number of neurons, equal to the number of classes.377# However, this is not the case for the layer following our convolutional layers. Here, the teacher has more neurons than the student378# after the flattening of the final convolutional layer. Our loss function accepts two vectors of equal dimensionality as inputs,379# therefore we need to somehow match them. We will solve this by including an average pooling layer after the teacher's convolutional layer to reduce its dimensionality to match that of the student.380#381# To proceed, we will modify our model classes, or create new ones.382# Now, the forward function returns not only the logits of the network but also the flattened hidden representation after the convolutional layer. We include the aforementioned pooling for the modified teacher.383384class ModifiedDeepNNCosine(nn.Module):385def __init__(self, num_classes=10):386super(ModifiedDeepNNCosine, self).__init__()387self.features = nn.Sequential(388nn.Conv2d(3, 128, kernel_size=3, padding=1),389nn.ReLU(),390nn.Conv2d(128, 64, kernel_size=3, padding=1),391nn.ReLU(),392nn.MaxPool2d(kernel_size=2, stride=2),393nn.Conv2d(64, 64, kernel_size=3, padding=1),394nn.ReLU(),395nn.Conv2d(64, 32, kernel_size=3, padding=1),396nn.ReLU(),397nn.MaxPool2d(kernel_size=2, stride=2),398)399self.classifier = nn.Sequential(400nn.Linear(2048, 512),401nn.ReLU(),402nn.Dropout(0.1),403nn.Linear(512, num_classes)404)405406def forward(self, x):407x = self.features(x)408flattened_conv_output = torch.flatten(x, 1)409x = self.classifier(flattened_conv_output)410flattened_conv_output_after_pooling = torch.nn.functional.avg_pool1d(flattened_conv_output, 2)411return x, flattened_conv_output_after_pooling412413# Create a similar student class where we return a tuple. We do not apply pooling after flattening.414class ModifiedLightNNCosine(nn.Module):415def __init__(self, num_classes=10):416super(ModifiedLightNNCosine, self).__init__()417self.features = nn.Sequential(418nn.Conv2d(3, 16, kernel_size=3, padding=1),419nn.ReLU(),420nn.MaxPool2d(kernel_size=2, stride=2),421nn.Conv2d(16, 16, kernel_size=3, padding=1),422nn.ReLU(),423nn.MaxPool2d(kernel_size=2, stride=2),424)425self.classifier = nn.Sequential(426nn.Linear(1024, 256),427nn.ReLU(),428nn.Dropout(0.1),429nn.Linear(256, num_classes)430)431432def forward(self, x):433x = self.features(x)434flattened_conv_output = torch.flatten(x, 1)435x = self.classifier(flattened_conv_output)436return x, flattened_conv_output437438# We do not have to train the modified deep network from scratch of course, we just load its weights from the trained instance439modified_nn_deep = ModifiedDeepNNCosine(num_classes=10).to(device)440modified_nn_deep.load_state_dict(nn_deep.state_dict())441442# Once again ensure the norm of the first layer is the same for both networks443print("Norm of 1st layer for deep_nn:", torch.norm(nn_deep.features[0].weight).item())444print("Norm of 1st layer for modified_deep_nn:", torch.norm(modified_nn_deep.features[0].weight).item())445446# Initialize a modified lightweight network with the same seed as our other lightweight instances. This will be trained from scratch to examine the effectiveness of cosine loss minimization.447torch.manual_seed(42)448modified_nn_light = ModifiedLightNNCosine(num_classes=10).to(device)449print("Norm of 1st layer:", torch.norm(modified_nn_light.features[0].weight).item())450451######################################################################452# Naturally, we need to change the train loop because now the model returns a tuple ``(logits, hidden_representation)``. Using a sample input tensor453# we can print their shapes.454455# Create a sample input tensor456sample_input = torch.randn(128, 3, 32, 32).to(device) # Batch size: 128, Filters: 3, Image size: 32x32457458# Pass the input through the student459logits, hidden_representation = modified_nn_light(sample_input)460461# Print the shapes of the tensors462print("Student logits shape:", logits.shape) # batch_size x total_classes463print("Student hidden representation shape:", hidden_representation.shape) # batch_size x hidden_representation_size464465# Pass the input through the teacher466logits, hidden_representation = modified_nn_deep(sample_input)467468# Print the shapes of the tensors469print("Teacher logits shape:", logits.shape) # batch_size x total_classes470print("Teacher hidden representation shape:", hidden_representation.shape) # batch_size x hidden_representation_size471472######################################################################473# In our case, ``hidden_representation_size`` is ``1024``. This is the flattened feature map of the final convolutional layer of the student and as you can see,474# it is the input for its classifier. It is ``1024`` for the teacher too, because we made it so with ``avg_pool1d`` from ``2048``.475# The loss applied here only affects the weights of the student prior to the loss calculation. In other words, it does not affect the classifier of the student.476# The modified training loop is the following:477#478# .. figure:: /../_static/img/knowledge_distillation/cosine_loss_distillation.png479# :align: center480#481# In Cosine Loss minimization, we want to maximize the cosine similarity of the two representations by returning gradients to the student:482#483484def train_cosine_loss(teacher, student, train_loader, epochs, learning_rate, hidden_rep_loss_weight, ce_loss_weight, device):485ce_loss = nn.CrossEntropyLoss()486cosine_loss = nn.CosineEmbeddingLoss()487optimizer = optim.Adam(student.parameters(), lr=learning_rate)488489teacher.to(device)490student.to(device)491teacher.eval() # Teacher set to evaluation mode492student.train() # Student to train mode493494for epoch in range(epochs):495running_loss = 0.0496for inputs, labels in train_loader:497inputs, labels = inputs.to(device), labels.to(device)498499optimizer.zero_grad()500501# Forward pass with the teacher model and keep only the hidden representation502with torch.no_grad():503_, teacher_hidden_representation = teacher(inputs)504505# Forward pass with the student model506student_logits, student_hidden_representation = student(inputs)507508# Calculate the cosine loss. Target is a vector of ones. From the loss formula above we can see that is the case where loss minimization leads to cosine similarity increase.509hidden_rep_loss = cosine_loss(student_hidden_representation, teacher_hidden_representation, target=torch.ones(inputs.size(0)).to(device))510511# Calculate the true label loss512label_loss = ce_loss(student_logits, labels)513514# Weighted sum of the two losses515loss = hidden_rep_loss_weight * hidden_rep_loss + ce_loss_weight * label_loss516517loss.backward()518optimizer.step()519520running_loss += loss.item()521522print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss / len(train_loader)}")523524######################################################################525#We need to modify our test function for the same reason. Here we ignore the hidden representation returned by the model.526527def test_multiple_outputs(model, test_loader, device):528model.to(device)529model.eval()530531correct = 0532total = 0533534with torch.no_grad():535for inputs, labels in test_loader:536inputs, labels = inputs.to(device), labels.to(device)537538outputs, _ = model(inputs) # Disregard the second tensor of the tuple539_, predicted = torch.max(outputs.data, 1)540541total += labels.size(0)542correct += (predicted == labels).sum().item()543544accuracy = 100 * correct / total545print(f"Test Accuracy: {accuracy:.2f}%")546return accuracy547548######################################################################549# In this case, we could easily include both knowledge distillation and cosine loss minimization in the same function. It is common to combine methods to achieve better performance in teacher-student paradigms.550# For now, we can run a simple train-test session.551552# Train and test the lightweight network with cross entropy loss553train_cosine_loss(teacher=modified_nn_deep, student=modified_nn_light, train_loader=train_loader, epochs=10, learning_rate=0.001, hidden_rep_loss_weight=0.25, ce_loss_weight=0.75, device=device)554test_accuracy_light_ce_and_cosine_loss = test_multiple_outputs(modified_nn_light, test_loader, device)555556######################################################################557# Intermediate regressor run558# --------------------------559# Our naive minimization does not guarantee better results for several reasons, one being the dimensionality of the vectors.560# Cosine similarity generally works better than Euclidean distance for vectors of higher dimensionality,561# but we were dealing with vectors with 1024 components each, so it is much harder to extract meaningful similarities.562# Furthermore, as we mentioned, pushing towards a match of the hidden representation of the teacher and the student is not supported by theory.563# There are no good reasons why we should be aiming for a 1:1 match of these vectors.564# We will provide a final example of training intervention by including an extra network called regressor.565# The objective is to first extract the feature map of the teacher after a convolutional layer,566# then extract a feature map of the student after a convolutional layer, and finally try to match these maps.567# However, this time, we will introduce a regressor between the networks to facilitate the matching process.568# The regressor will be trainable and ideally will do a better job than our naive cosine loss minimization scheme.569# Its main job is to match the dimensionality of these feature maps so that we can properly define a loss function between the teacher and the student.570# Defining such a loss function provides a teaching "path," which is basically a flow to back-propagate gradients that will change the student's weights.571# Focusing on the output of the convolutional layers right before each classifier for our original networks, we have the following shapes:572#573574# Pass the sample input only from the convolutional feature extractor575convolutional_fe_output_student = nn_light.features(sample_input)576convolutional_fe_output_teacher = nn_deep.features(sample_input)577578# Print their shapes579print("Student's feature extractor output shape: ", convolutional_fe_output_student.shape)580print("Teacher's feature extractor output shape: ", convolutional_fe_output_teacher.shape)581582######################################################################583# We have 32 filters for the teacher and 16 filters for the student.584# We will include a trainable layer that converts the feature map of the student to the shape of the feature map of the teacher.585# In practice, we modify the lightweight class to return the hidden state after an intermediate regressor that matches the sizes of the convolutional586# feature maps and the teacher class to return the output of the final convolutional layer without pooling or flattening.587#588# .. figure:: /../_static/img/knowledge_distillation/fitnets_knowledge_distill.png589# :align: center590#591# The trainable layer matches the shapes of the intermediate tensors and Mean Squared Error (MSE) is properly defined:592#593594class ModifiedDeepNNRegressor(nn.Module):595def __init__(self, num_classes=10):596super(ModifiedDeepNNRegressor, self).__init__()597self.features = nn.Sequential(598nn.Conv2d(3, 128, kernel_size=3, padding=1),599nn.ReLU(),600nn.Conv2d(128, 64, kernel_size=3, padding=1),601nn.ReLU(),602nn.MaxPool2d(kernel_size=2, stride=2),603nn.Conv2d(64, 64, kernel_size=3, padding=1),604nn.ReLU(),605nn.Conv2d(64, 32, kernel_size=3, padding=1),606nn.ReLU(),607nn.MaxPool2d(kernel_size=2, stride=2),608)609self.classifier = nn.Sequential(610nn.Linear(2048, 512),611nn.ReLU(),612nn.Dropout(0.1),613nn.Linear(512, num_classes)614)615616def forward(self, x):617x = self.features(x)618conv_feature_map = x619x = torch.flatten(x, 1)620x = self.classifier(x)621return x, conv_feature_map622623class ModifiedLightNNRegressor(nn.Module):624def __init__(self, num_classes=10):625super(ModifiedLightNNRegressor, self).__init__()626self.features = nn.Sequential(627nn.Conv2d(3, 16, kernel_size=3, padding=1),628nn.ReLU(),629nn.MaxPool2d(kernel_size=2, stride=2),630nn.Conv2d(16, 16, kernel_size=3, padding=1),631nn.ReLU(),632nn.MaxPool2d(kernel_size=2, stride=2),633)634# Include an extra regressor (in our case linear)635self.regressor = nn.Sequential(636nn.Conv2d(16, 32, kernel_size=3, padding=1)637)638self.classifier = nn.Sequential(639nn.Linear(1024, 256),640nn.ReLU(),641nn.Dropout(0.1),642nn.Linear(256, num_classes)643)644645def forward(self, x):646x = self.features(x)647regressor_output = self.regressor(x)648x = torch.flatten(x, 1)649x = self.classifier(x)650return x, regressor_output651652######################################################################653# After that, we have to update our train loop again. This time, we extract the regressor output of the student, the feature map of the teacher,654# we calculate the ``MSE`` on these tensors (they have the exact same shape so it's properly defined) and we back propagate gradients based on that loss,655# in addition to the regular cross entropy loss of the classification task.656657def train_mse_loss(teacher, student, train_loader, epochs, learning_rate, feature_map_weight, ce_loss_weight, device):658ce_loss = nn.CrossEntropyLoss()659mse_loss = nn.MSELoss()660optimizer = optim.Adam(student.parameters(), lr=learning_rate)661662teacher.to(device)663student.to(device)664teacher.eval() # Teacher set to evaluation mode665student.train() # Student to train mode666667for epoch in range(epochs):668running_loss = 0.0669for inputs, labels in train_loader:670inputs, labels = inputs.to(device), labels.to(device)671672optimizer.zero_grad()673674# Again ignore teacher logits675with torch.no_grad():676_, teacher_feature_map = teacher(inputs)677678# Forward pass with the student model679student_logits, regressor_feature_map = student(inputs)680681# Calculate the loss682hidden_rep_loss = mse_loss(regressor_feature_map, teacher_feature_map)683684# Calculate the true label loss685label_loss = ce_loss(student_logits, labels)686687# Weighted sum of the two losses688loss = feature_map_weight * hidden_rep_loss + ce_loss_weight * label_loss689690loss.backward()691optimizer.step()692693running_loss += loss.item()694695print(f"Epoch {epoch+1}/{epochs}, Loss: {running_loss / len(train_loader)}")696697# Notice how our test function remains the same here with the one we used in our previous case. We only care about the actual outputs because we measure accuracy.698699# Initialize a ModifiedLightNNRegressor700torch.manual_seed(42)701modified_nn_light_reg = ModifiedLightNNRegressor(num_classes=10).to(device)702703# We do not have to train the modified deep network from scratch of course, we just load its weights from the trained instance704modified_nn_deep_reg = ModifiedDeepNNRegressor(num_classes=10).to(device)705modified_nn_deep_reg.load_state_dict(nn_deep.state_dict())706707# Train and test once again708train_mse_loss(teacher=modified_nn_deep_reg, student=modified_nn_light_reg, train_loader=train_loader, epochs=10, learning_rate=0.001, feature_map_weight=0.25, ce_loss_weight=0.75, device=device)709test_accuracy_light_ce_and_mse_loss = test_multiple_outputs(modified_nn_light_reg, test_loader, device)710711######################################################################712# It is expected that the final method will work better than ``CosineLoss`` because now we have allowed a trainable layer between the teacher and the student,713# which gives the student some wiggle room when it comes to learning, rather than pushing the student to copy the teacher's representation.714# Including the extra network is the idea behind hint-based distillation.715716print(f"Teacher accuracy: {test_accuracy_deep:.2f}%")717print(f"Student accuracy without teacher: {test_accuracy_light_ce:.2f}%")718print(f"Student accuracy with CE + KD: {test_accuracy_light_ce_and_kd:.2f}%")719print(f"Student accuracy with CE + CosineLoss: {test_accuracy_light_ce_and_cosine_loss:.2f}%")720print(f"Student accuracy with CE + RegressorMSE: {test_accuracy_light_ce_and_mse_loss:.2f}%")721722######################################################################723# Conclusion724# --------------------------------------------725# None of the methods above increases the number of parameters for the network or inference time,726# so the performance increase comes at the little cost of calculating gradients during training.727# In ML applications, we mostly care about inference time because training happens before the model deployment.728# If our lightweight model is still too heavy for deployment, we can apply different ideas, such as post-training quantization.729# Additional losses can be applied in many tasks, not just classification, and you can experiment with quantities like coefficients,730# temperature, or number of neurons. Feel free to tune any numbers in the tutorial above,731# but keep in mind, if you change the number of neurons / filters chances are a shape mismatch might occur.732#733# For more information, see:734#735# * `Hinton, G., Vinyals, O., Dean, J.: Distilling the knowledge in a neural network. In: Neural Information Processing System Deep Learning Workshop (2015) <https://arxiv.org/abs/1503.02531>`_736#737# * `Romero, A., Ballas, N., Kahou, S.E., Chassang, A., Gatta, C., Bengio, Y.: Fitnets: Hints for thin deep nets. In: Proceedings of the International Conference on Learning Representations (2015) <https://arxiv.org/abs/1412.6550>`_738739740