PyTorch framework is used to build and configure Deep Learning algorithms to solve many problems like face recognition, recommended systems, image retrieval, etc. The models are trained on the input data containing the observed information gathered over time. Evaluation of these models is an important process that can be done using different loss functions like Focal, NLL, CTC, etc.

Quick Outline

This guide explains the following sections:

  • What is Triplet Loss
  • How to Calculate Triplet Loss in PyTorch
  • Method 1: Calculate Triplet Loss to Optimize the Trained Model
  • Method 2: Calculate Triplet Loss Using TripletMarginLoss() Function
  • Method 3: Calculate Triplet Loss Using Distance Function
  • Method 4: Calculate Triplet Loss Using Custom Function
  • Conclusion

What is Triplet Loss?

Not many loss functions are capable of finding the similarity or difference for the dataset containing more than a million classes. To solve this problem, the Triplet loss function was proposed in 2015 using three dimensions of the data like actual data called the anchor. The second dimension is called the positive which is similar to the actual value and the negative is different from the actual data.

The triplet loss function is used to minimize the distance between the anchor and positive points as compared to the negative point. The mathematical formula explaining the triplet loss process is mentioned below:

Here:

A: Anchor or the actual input data

P: Positive which is similar to the anchor value

N: Negative value which is dissimilar to the anchor point

D(A, P): Distance between the anchor and positive points

D(A, N): Distance between the anchor and negative values

margin: The minimum distance between the anchor-positive and anchor-negative pair

How to Calculate Triplet Loss in PyTorch

PyTorch offers multiple methods like TripletMarginLoss(), TripletMarginWithDistanceLoss(), etc. to calculate the triplet loss. The user can also build a custom function to apply the triplet loss on the deep learning model using neural network dependency. To learn the process of calculating the triplet loss using the PyTorch environment, simply follow this guide:

Note: The Python code for calculating the triplet loss can be accessed from here

Method 1: Calculate Triplet Loss to Optimize the Trained Model

The following method trains the deep learning model and calculates the triplet loss to optimize its performance using multiple iterations of backpropagation. Start the process by executing the following steps:

Step 1: Install Modules

In the Python notebook, install the torch framework to get its dependencies using the “pip” Python’s package manager:

pip install torch

Step 2: Import Libraries

The first step in the process is to import the required libraries for training and optimizing the performance using the following code:

import time
import torch
import random
import numpy as np
import pandas as pd
import torch.nn as nn
import torch.optim as optim
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
from torchvision import transforms
from torch.utils.data import DataLoader, Dataset

Step 3: Upload the Dataset

The next step is to upload the dataset to the colab notebook and if you don’t have the dataset, simply download the dataset from the Kaggle library:

Now, click on the folder icon from the left panel on the colab notebook and then click on the upload icon to select the files from the local system:

Step 4: Load the Data to Train the Model

Before loading the data, apply the seed() method with the torch and numpy library to normalize the training and testing datasets. It will provide the same set of results from the same data to use different machine learning algorithms:

torch.manual_seed(2020)
np.random.seed(2020)
random.seed(2020)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

if device.type == "cuda":
    torch.cuda.get_device_name()

Add the dimensions for the deep learning algorithm to train data using 5 iterations and 32 batches for each epoch:

embedding_dims = 2
batch_size = 32
epochs = 5

Now, load the training and testing data using the read_csv() method from the “pandas” library before printing the first 5 rows of the data:

train_df = pd.read_csv('/content/train.csv')
test_df = pd.read_csv('/content/test.csv')

train_df.head()

Step 5: Design MNIST Class

Create the MNIST class to provide the logic for the training dataset by transforming the images according to the model:

class MNIST(Dataset):
    def __init__(self, df, train=True, transform=None):
        self.is_train = train
        self.transform = transform
        self.to_pil = transforms.ToPILImage()
       
        if self.is_train:           
            self.images = df.iloc[:, 1:].values.astype(np.uint8)
            self.labels = df.iloc[:, 0].values
            self.index = df.index.values
        else:
            self.images = df.values.astype(np.uint8)
       
    def __len__(self):
        return len(self.images)
   
    def __getitem__(self, item):
        anchor_img = self.images[item].reshape(28, 28, 1)
       
        if self.is_train:
            anchor_label = self.labels[item]

            positive_list = self.index[self.index!=item][self.labels[self.index!=item]==anchor_label]

            positive_item = random.choice(positive_list)
            positive_img = self.images[positive_item].reshape(28, 28, 1)
           
            negative_list = self.index[self.index!=item][self.labels[self.index!=item]!=anchor_label]
            negative_item = random.choice(negative_list)
            negative_img = self.images[negative_item].reshape(28, 28, 1)
           
            if self.transform:
                anchor_img = self.transform(self.to_pil(anchor_img))
                positive_img = self.transform(self.to_pil(positive_img))
                negative_img = self.transform(self.to_pil(negative_img))
           
            return anchor_img, positive_img, negative_img, anchor_label
       
        else:
            if self.transform:
                anchor_img = self.transform(self.to_pil(anchor_img))
            return anchor_img
  • Create the MNIST class to use the data frames or df for storing the datasets uploaded in the previous step. 
  • Initialize the df using the constructor that separates the image from its labels if the train equals True value and transform is used to apply transformations to the images. 
  • Get the total number of instances or images from the dataset using the __len__(self) method. 
  • Return the anchor values if the model is not training and return the anchor, positive and negative values while training. 
  • Get the pairs of the anchor-positive and anchor-negative values to calculate the triplet loss value of the model.

Create the train_ds variable to call the MNIST() method for training data with its arguments to transform the data. Load the data using the DataLoader() method and store it in the train_loader variable:

train_ds = MNIST(train_df,
                train=True,
                transform=transforms.Compose([
                    transforms.ToTensor()
                ]))
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=4)

Store the test data set in the test_ds variable using the MNIST() method and DataLoader() to load the data in the test_loader variable:

test_ds = MNIST(test_df, train=False, transform=transforms.ToTensor())
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=4)

Step 6: Design Triplet Loss Class

To calculate the triplet loss value of the model, we need to customize its function by telling it what to return to the model for backpropagation after each epoch. The TripletLoss() method uses the neural network dependency of the torch library to calculate the loss value in PyTorch:

class TripletLoss(nn.Module):
    def __init__(self, margin=1.0):
        super(TripletLoss, self).__init__()
        self.margin = margin
       
    def calc_euclidean(self, x1, x2):
        return (x1 - x2).pow(2).sum(1)
   
    def forward(self, anchor: torch.Tensor, positive: torch.Tensor, negative: torch.Tensor) -> torch.Tensor:
        distance_positive = self.calc_euclidean(anchor, positive)
        distance_negative = self.calc_euclidean(anchor, negative)
        losses = torch.relu(distance_positive - distance_negative + self.margin)

        return losses.mean()
  • Define the parameters to be used in the TripletLoss() method like self and margin.
  • Configure the calc_eucledian() with the formula for calculating the distance between the anchor from positive and negative values.
  • The distance_positive variable stores the distance of the anchor to positive values and distance_negative stores the gap between the anchor and negative values.
  • The losses variable calculates the overall loss values of the model using both the pairs and the margin to normalize the error value.

Now, design the structure of the neural network with its dimensions and the layers containing the neurons connected to the next layers. The complete network is responsible for learning the hidden patterns from the datasets throughout multiple iterations called epochs:

class Network(nn.Module):
    def __init__(self, emb_dim=128):
        super(Network, self).__init__()
        self.conv = nn.Sequential(
            nn.Conv2d(1, 32, 5),
            nn.PReLU(),
            nn.MaxPool2d(2, stride=2),
            nn.Dropout(0.3),
            nn.Conv2d(32, 64, 5),
            nn.PReLU(),
            nn.MaxPool2d(2, stride=2),
            nn.Dropout(0.3)
        )
       
        self.fc = nn.Sequential(
            nn.Linear(64*4*4, 512),
            nn.PReLU(),
            nn.Linear(512, emb_dim)
        )
       
    def forward(self, x):
        x = self.conv(x)
        x = x.view(-1, 64*4*4)
        x = self.fc(x)
        # x = nn.functional.normalize(x)
        return x

Define the weights to be applied after setting up the network for the neural network model as the weights determine the improvement of the model with each iteration:

def init_weights(m):
    if isinstance(m, nn.Conv2d):
        torch.nn.init.kaiming_normal_(m.weight)

Step 7: Building & Training Model

Build the model by integrating all the components like Network(), weights, TripletLoss(), and others that are configured or customized previously:

model = Network(embedding_dims)
model.apply(init_weights)
model = torch.jit.script(model).to(device)

optimizer = optim.Adam(model.parameters(), lr=0.001)
criterion = torch.jit.script(TripletLoss())

Now, start the training process of the model with the number of iterations in descending order using inside the for loop. The loop keeps the iterations going to calculate the loss value at the end of each iteration to make the model improve with each epoch:

model.train()
for epoch in tqdm(range(epochs), desc="Epochs"):
    running_loss = []
    for step, (anchor_img, positive_img, negative_img, anchor_label) in enumerate(tqdm(train_loader, desc="Training", leave=False)):
        anchor_img = anchor_img.to(device)
        positive_img = positive_img.to(device)
        negative_img = negative_img.to(device)

        optimizer.zero_grad()
        anchor_out = model(anchor_img)
        positive_out = model(positive_img)
        negative_out = model(negative_img)

        loss = criterion(anchor_out, positive_out, negative_out)
        loss.backward()
        optimizer.step()

        running_loss.append(loss.cpu().detach().numpy())
    print("Epoch: {}/{} - Loss: {:.4f}".format(epoch+1, epochs, np.mean(running_loss)))

The following screenshot displays the number of iterations used to train the model and the loss value for each epoch:

Step 8: Save the Trained Model

Once the model is trained successfully, simply save the result dataset to use later like for visualization or getting insights from the model:

torch.save({"model_state_dict": model.state_dict(),
            "optimzier_state_dict": optimizer.state_dict()
          }, "trained_model.pth")

To save the result data from the model, it is required to use the correct labels for storing the data at its correct locations in the database:

train_results = []
labels = []

model.eval()
with torch.no_grad():
    for img, _, _, label in tqdm(train_loader):
        train_results.append(model(img.to(device)).cpu().numpy())
        labels.append(label)

train_results = np.concatenate(train_results)
labels = np.concatenate(labels)
train_results.shape

Step 9: Plot the Results

Graphical representation is the best way to check how the model has performed after the training. The following code is used to plot the graph of the containing dots for each class with different colors:

plt.figure(figsize=(15, 10), facecolor="azure")
for label in np.unique(labels):
    tmp = train_results[labels==label]
    plt.scatter(tmp[:, 0], tmp[:, 1], label=label)

plt.legend()
plt.show()

The model has performed pretty well as the clusters with different colors are visible and very dominant in their locations as displayed in the following picture:

That’s all about calculating the triplet loss in the deep learning model and optimizing it with each iteration. The next sections or methods explain the basic built-in functions offered by the PyTorch environment that can be used by importing the torch library.

Method 2: Calculate Triplet Loss Using TripletMarginLoss() Function

The TripletMarginLoss() method is used to find the margin between wrong predictions by calculating their gap from the anchor values. It means that the function measures the relative similarity between the sample and predicted values. To learn how to implement the method in the PyTorch environment, follow these steps:

Step 1: Import Libraries

Now, import the torch library from its framework to set up the environment for using different functions to calculate triplet loss:

import torch

Print the version of the torch framework to verify that the session is ready for using torch functions:

print(torch.__version__)

Step 2: Implementing the Code

Once the torch library is set, simply use the following code to implement the TripletMarginLoss() method to find the loss value:

import torch.nn as nn

triplet_loss = nn.TripletMarginLoss(margin=1.0, p=2, eps=1e-7)
anchor = torch.randn(100, 128, requires_grad=True)
pos = torch.randn(100, 128, requires_grad=True)
neg = torch.randn(100, 128, requires_grad=True)
result = triplet_loss(anchor, pos, neg)
result.backward()
result

The above code:

  • Imports the neural network dependency from the torch library using the nn keyword. 
  • It also creates the triplet_loss variable and initializes it with the TripletMarginLoss() method with arguments like margin, number of pairs, and a constant for stability. 
  • Defines three tensors with random values for creating the anchor, positive, and negative values to use while calling the function. 
  • Applies the backpropagation using the backward() method to the result variable and print the loss value on the screen:

Method 3: Calculate Triplet Loss Using Distance Function

This method uses the TripletMarginWithDistanceLoss() method and includes the nonnegative values and functions for real-values. It calculates the positive distance using the correct prediction-anchor relation and the negative distance using the wrong prediction-anchor relation:

embedding = nn.Embedding(1000, 128)
anchor_ids = torch.randint(0, 1000, (1,))
pos_ids = torch.randint(0, 1000, (1,))
neg_ids = torch.randint(0, 1000, (1,))
anchor = embedding(anchor_ids)
pos = embedding(pos_ids)
neg = embedding(neg_ids)

triplet_loss = \
    nn.TripletMarginWithDistanceLoss(distance_function=nn.PairwiseDistance())
result = triplet_loss(anchor, pos, neg)
result

The code suggests:

  • To create three tensors for applying the triplet loss method but the change here is to call the embedding() method to each tensor. 
  • Invoke the TripletMarginWithDistanceLoss() method to find the distance between the pairs created after embedding the tensors. 
  • Call the result variable to display the loss value after applying the method variable using all the tensors:

Method 4: Calculate Triplet Loss Using Custom Function

The user can simply build their custom triplet loss method in the torch environment according to their model to optimize its performance:

def l_infinity(x1, x2):
    return torch.max(torch.abs(x1 - x2), dim=1).values
triplet_loss = (
    nn.TripletMarginWithDistanceLoss(distance_function=l_infinity, margin=1.5))
result = triplet_loss(anchor, pos, neg)
result

The code:

  • Creates a custom function to use as the distance_function argument while calling the TripletMarginWithDistanceLoss() method.
  • Define the l_infinity() method with the multiple arguments and Use the torch.abs() method in the max() method.
  • It creates the maximum absolute difference value between the tensors.
  • After that, call the loss method with the custom distance function and the margin as the minimum distance between the pairs.
  • Use the anchor, positive, and negative tensors to get the value of loss displayed on the screen:

That’s all about the process of calculating the triplet loss in PyTorch.

Conclusion

To sum up, the triplet loss uses three points to calculate the distance between the actual value and the predicted values. The anchor refers to the observed value, the positive is the correctly predicted value, and the negative is the wrong prediction. PyTorch offers multiple methods like TripletMarginLoss(), and the custom distance function for the TripletMarginWithDistanceLoss() method. This guide has also implemented the loss value on the trained model to optimize its performance.