Showing posts with label machine learning. Show all posts
Showing posts with label machine learning. Show all posts

Mastercard Data Scientist Interview Questions




In this post, I'm going to share the interview experience for a data scientist position at Mastercard. Overall, the overall process is quite fast and efficient. 

Human Resource 

First, there was no HR round, and an HR communicated with me directly via email asking relevant questions such as:

  • Current base salary and other perks
  • Notification period
  • Expected salary 
  • Hybrid arrangement and if I'm OK with that 
  • ...

Technical Round

Then the HR scheduled the first round, which is a 1 hour session with three people for checking technical skills. There was no pair-coding session, and it was mainly organized as a set of questions from each of the three interviewers. 
  • Asked simple SQL and python questions and to talk about the solutions
  • Questions regarding ML basics
  • Tech stack match between the post and my experience
  • ...
After a few days, I got a call from the HR and checked my availability and fixed the interview time of the next round. It seems their internal process is quite fast.

Hiring manager round

The second (and final) round is a 45 minute session with the hiring manager and one principle individual contributor (IC). Overall, they would like to know if I could jump into the role to execute the new upcoming project.

  • Details about current role
  • Experience of leading (how many) people 
  • Practical experience of deploying ML in real-world settings
  • The scale of previous projects (e.g., how many end users, data scale)
  • How you would like to start structure the ML project?
  • What is the north star and how did you deliver?
  • ...

The final notification regarding the decision was also out quite quickly, on weekend as I recall. 

Overall, the interview process of Mastercard is quite efficient and fast, and jump directly into tech details and testing match between the post and candidate. I hope this post is helpful for you in case you are preparing a similar position for Mastercard. 

DocuSign Machine Learning Engineer Questions

In this post, I will share the interview experience for a DocuSign Machine Learning Engineer (MLE) post regarding the first round followed by the CV screening.

The first round is a technical screening by a lead MLE, and the round is about one hour long, and includes three parts:

  • Elaborating details on ML projects 
  • ML questions
  • Coding

The first part is about discussing one or two ML projects in your CV in detail. Based on your description, the interviewer can ask some detailed questions with respect to the project.

The second part is about a set of ML questions regarding NLP and deep learning, as DocuSign's main product is focused on NLP. Some questions regarding LLMs can also be expected given the current NLP trends.

Finally, it is coding session, which is typical leetcode-like one to solve a coding problem in the given time.

Hope it helps if you are interviewing similar posts at DocuSign!

Variational Autoencoders

Autoencoder is an unsupervised model - a deep neural network architecture - which contains an encoder and decoder. 
  • The encoder component serves as compressing the input to a lower-dimensional representation;
  • The decoder aims to reconstruct the compressed representation back to the original input. 

The architecture is pretty simple, with the number of neurons in the layers of the encoder part (blue below) decreases, and then starts increasing again in the decoder part (purple below).

Input image => Dense(256) => Dense(64) => Dense(2) => Dense(64) => Dense(246) => Output (reconstructed image)

As one might expect, the loss is between the input image/data and reconstructed one, as the part of the name auto (self-supervised) implies.

Variational AutoEncoder (VAE) is the probablistic twist of the Autoencoder. Instead of giving deterministic output of both encoder and decoder in Autoencoder, that in VAE give probability distributions. 

As Autoencoder, the loss of VAE contains the reconstruction loss between the input data and reconstructed data. In addition, it also has a regularization term - the KL divergence between the posterior distribution (output of the encoder) and a prior (usually using a simple isotropic Gaussian). 

In this post, we go through the implementation of VAE with Tensorflow, Tensorflow Probability, and Keras. The example below is from Probabilistic Deep Learning with TensorFlow 2 course from Coursera, which by the way, I am highly recommend if you want to get familiar with Tensorflow Probability module. 

Contents


Import required packages


import tensorflow as tf
import tensorflow_probability as tfp
import seaborn as sns
import matplotlib
import numpy as np
import matplotlib.pyplot as plt

from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.layers import Flatten, Dense, Reshape

print("tensorflow", tf.__version__)
print("tensorflow probability", tfp.__version__)
print("matplotlib", matplotlib.__version__)
print("numpy", np.__version__)
print("seaborn", sns.__version__)

tfd = tfp.distributions
tfpl = tfp.layers

tensorflow 2.8.0
tensorflow probability 0.14.0
matplotlib 3.8.0
numpy 1.26.0
seaborn 0.13.0


Fashion MNIST dataset

As we did in the Autoencoder post, we use the Fashion MNIST dataset is from Zalando. Zalando is a publicly traded German online retailer of shoes, fashion and beauty active across Europe. 

The dataset consists of a training set of 60,000 examples and a test set of 10,000 examples. Each example is a 28x28 grayscale image, associated with a label from 10 classes. We don't use those labels but only use images as we want to use Autoencoder to compress and reconstruct a given image. Let's get started.

# Fashion MNIST dataset

(X_train, y_train), (X_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()

# If using Bernoulli, we can scale by simply dividing the max value 
#X_train = X_train.astype('float32')/256.
#X_test = X_test.astype('float32')/256.
# If we use Beta distribution defined within interval (0,1), 
# we need to scale images to the range (avoiding 0)
X_train = X_train.astype('float32')/256. + 0.5/256
X_test = X_test.astype('float32')/256. + 0.5/256

print(X_train.shape)

class_names = np.array([
    'T-shirt/top', 
    'Trouser/pants', 
    'Pullover shirt', 
    'Dress',
    'Coat', 
    'Sandal', 
    'Shirt', 
    'Sneaker', 
    'Bag',
    'Ankle boot'
])
(60000, 28, 28)

# Show some examples of the data

n_examples = 1000
example_images = X_test[0:n_examples]
example_labels = y_test[0:n_examples]

fig, axes = plt.subplots(1, 5, figsize=(15, 4))
for i in range(len(axes)):
    axes[i].imshow(example_images[i], cmap='binary')
    axes[i].set_title(class_names[example_labels[i]])
    axes[i].axis('off')



Encoder

The same as Autoencoder post, we keep the encoded dimention as 2, i.e., the output of the encoder is 2-dimentional vector - 2 random variables from probability distributions. And as mentioned earlier, we have a prior distribution for the KL divergence loss between it and the posterior distribution (the output distribution from encoder for those 2 random variables).

encoded_dim = 2

# Identity covariance matrix by default
prior = tfd.MultivariateNormalDiag(
    loc=tf.zeros(encoded_dim)
)

Here, we list three different versions for the encoder part. You can focus on first version only and then come back to investigate other versions if you are interested in learning more.

The posterior distribution is also multivariate Gaussian, but its parameters will be learned during training. The KLDivergenceAddLoss is a bypass layer, but will automatically add KL divergence loss between the prior and posterior to the main loss - the reconstruction loss.

# Encoder version 1
# Feel free to skip other versions and come back later

encoder = Sequential([
    Flatten(input_shape=(28, 28)),
    Dense(256, activation='relu'),
    Dense(64, activation='relu'),
    Dense(tfpl.MultivariateNormalTriL.params_size(encoded_dim)),
    tfpl.MultivariateNormalTriL(encoded_dim),
    tfpl.KLDivergenceAddLoss(prior)
])

print(encoder.losses, end='\n\n')
print(encoder(example_images), end='\n\n')
print(encoder.losses)

[tf.Tensor 'kl_divergence_add_loss_1/kldivergence_loss/batch_total_kl_divergence:0' shape=() dtype=float32]

tfp.distributions._TensorCoercible("sequential_2_multivariate_normal_tri_l_1_tensor_coercible", batch_shape=[1000], event_shape=[2], dtype=float32)

[tf.Tensor: shape=(), dtype=float32, numpy=0.89796853]

The second version gives more details of some parameters in the KLDivergenceAddLoss.

# Encoder version 2: with some arguments

encoder = Sequential([
    Flatten(input_shape=(28, 28)),
    Dense(256, activation='relu'),
    Dense(64, activation='relu'),
    Dense(tfpl.MultivariateNormalTriL.params_size(encoded_dim)),
    tfpl.MultivariateNormalTriL(encoded_dim),
    tfpl.KLDivergenceAddLoss(
        prior,
        use_exact_kl=False,
        weight=1.5,
        test_points_fn=lambda d: d.sample(10),
        test_points_reduce_axis=0
    )
])
  • weight: what multiple of KL divergence to be added to the loss, useful for implementing 𝛽-VAE, where 𝛽 indicates the weight of KL divergence.
  • test_points_fn: Receives batch of distributions, returns tensor of samples of shape (n_sample, batch_size, dim_z). These samples are converted to scalar value. $z_{ij}$ is the $i$-th sample for the observation $x_j$ (is at (i,j,:) in the tensor of samples) is mapped to $\log q(z_{ij})|x_j-\log p(z_{ij})$. This implies the tensor of samples returned by test_points_fn is converted into a tensor of values with a shape (n_samples, batch)
  • test_points_reduce_axis: to compute the loss added to the model, this arg indicates axis to average over (reduce_mean)
We can do exactly the same thing without using the KLDivergenceAddLoss. Instead, we can declare the KLDivergenceRegularizer as below and specify the activity_regularizer parameter in the MultivariateNormalTriL directly.

# Encoder version 3: Using KLDivergenceRegularizer

divergence_regularizer = tfpl.KLDivergenceRegularizer(
    prior,
    use_exact_kl=False,
    test_points_fn=lambda d: d.sample(10),
    test_points_reduce_axis=0
)

encoder = Sequential([
    Flatten(input_shape=(28, 28)),
    Dense(256, activation='relu'),
    Dense(64, activation='relu'),
    Dense(tfpl.MultivariateNormalTriL.params_size(encoded_dim)),
    tfpl.MultivariateNormalTriL(
        encoded_dim, 
        activity_regularizer=divergence_regularizer
    ),
])
We can first look at some encoded images before training. As one might expect, there is no clusters exhibit as the encoder is before any training.

pretrain_example_encodings = encoder(example_images).mean().numpy()

# Plot encoded examples before training 

f, ax = plt.subplots(1, 1, figsize=(7, 7))
sns.scatterplot(x=pretrain_example_encodings[:, 0],
                y=pretrain_example_encodings[:, 1],
                hue=class_names[example_labels], ax=ax,
                palette=sns.color_palette("colorblind", 10));
ax.set_xlabel('Encoding dimension 1'); ax.set_ylabel('Encoding dimension 2')
ax.set_title('Encodings of example images before training')



Decoder

For decoder part, we also use two different versions/options for modeling the output distribution. First one uses Bernoulli distribution and the second one uses Beta distribution. Tensorflow Probability provides an IndependentBernoulli layer which we can directly use for the first version. For the second one, as there is no such independent Beta layer implemented, we bake one from scratch using the DistributionLambda layer.

# Decoder version 1: Using IndependentBernoulli

decoder = Sequential([
    Dense(64, activation='relu', input_shape=(encoded_dim,)),
    Dense(256, activation='relu'),
    Dense(28*28),
    tfpl.IndependentBernoulli((28, 28))
])

# Decoder version 2: Using Independent Beta Distribution
# Since there is no IndependentBeta layer, bake one from scratch

decoder = Sequential([
    Dense(64, activation='relu', input_shape=(encoded_dim,)),
    Dense(256, activation='relu'),
    Dense(28*28*2, activation='exponential'), # non-nengative for Beta distribution params
    Reshape((28, 28, 2)),
    tfpl.DistributionLambda(
        lambda t: tfd.Independent(
            tfd.Beta(
                concentration1=t[..., 0],
                concentration0=t[..., 1]
            )
        )
    )
])

VAE

Finally, we use both encoder and decoder to build our VAE model. For the loss part, as we mentioned earlier, the KLDivergenceAddLoss layer already automatically add the KL/regularization loss to the main loss. Here we only need to specify the log loss.

vae = Model(
    inputs=encoder.inputs, 
    outputs=decoder(encoder.outputs)
)

def log_loss(x_true, p_x_given_z):
    return -tf.reduce_sum(p_x_given_z.log_prob(x_true))

vae.compile(loss=log_loss,)
vae.fit(
    x=X_train, 
    y=X_train,
    validation_data=(X_test, X_test),
    epochs=10,
    batch_size=32
)

WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
1874/1875 [============================>.] - ETA: 0s - loss: -55572.9219WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
WARNING:tensorflow:@custom_gradient grad_fn has 'variables' in signature, but no ResourceVariables were used on the forward pass.
1875/1875 [==============================] - 34s 16ms/step - loss: -55578.1367 - val_loss: -62465.1016
Epoch 2/10
1875/1875 [==============================] - 28s 15ms/step - loss: -64829.3750 - val_loss: -65042.4648
Epoch 3/10
1875/1875 [==============================] - 29s 15ms/step - loss: -67461.9844 - val_loss: -70393.7500
Epoch 4/10
1875/1875 [==============================] - 29s 15ms/step - loss: -69034.2734 - val_loss: -66066.8672
Epoch 5/10
1875/1875 [==============================] - 29s 15ms/step - loss: -70065.8438 - val_loss: -68278.2109
Epoch 6/10
1875/1875 [==============================] - 29s 15ms/step - loss: -70965.3438 - val_loss: -69826.4219
Epoch 7/10
1875/1875 [==============================] - 29s 15ms/step - loss: -71667.5000 - val_loss: -72257.3984
Epoch 8/10
1875/1875 [==============================] - 29s 15ms/step - loss: -72232.3594 - val_loss: -68337.1094
Epoch 9/10
1875/1875 [==============================] - 29s 16ms/step - loss: -72632.1641 - val_loss: -71673.4922
Epoch 10/10
1875/1875 [==============================] - 30s 16ms/step - loss: -72972.3281 - val_loss: -72791.6250

Results

First, we can plot some examples using the trained encoder this time.

# Generate an example reconstruction

example_reconstruction = vae(example_images).mean().numpy().squeeze()

# Plot the example reconstructions

fig, axs = plt.subplots(2, 6, figsize=(16, 5))

for j in range(6):
    axs[0, j].imshow(example_images[j, :, :].squeeze(), cmap='binary')
    axs[1, j].imshow(example_reconstruction[j, :, :], cmap='binary')
    axs[0, j].axis('off')
    axs[1, j].axis('off')

Finally, we can look at whether those encoded images exhibit some clusters after training. As we can observer from the right figure, some clusters can be found which contains images in the same or similar label.

# Compute example encodings after training

posttrain_example_encodings = encoder(example_images).mean().numpy()

# Compare the example encodings before and after training

f, axs = plt.subplots(nrows=1, ncols=2, figsize=(15, 7))
sns.scatterplot(
    x=pretrain_example_encodings[:, 0],
    y=pretrain_example_encodings[:, 1],
    hue=class_names[example_labels], ax=axs[0],
    palette=sns.color_palette("colorblind", 10)
)
sns.scatterplot(
    x=posttrain_example_encodings[:, 0],
    y=posttrain_example_encodings[:, 1],
    hue=class_names[example_labels], 
    ax=axs[1],
    palette=sns.color_palette("colorblind", 10)
)

axs[0].set_title('Encodings of example images before training');
axs[1].set_title('Encodings of example images after training');

for ax in axs: 
    ax.set_xlabel('Encoding dimension 1')
    ax.set_ylabel('Encoding dimension 2')
    ax.legend(loc='upper right')



In this post, we introduced Variational AutoEncoder, which is the probablistic twist of Autoencoder. In contrast to the Autoencoder, it is designed or trained to generate images, and it is not deterministic as the Autoencoder (the output of encoder and decoder given an input image). For example, VAE allows sampling from the distributions in the encoder and decoder and will lead to different results for a given image.

Autoencoders

Autoencoder is an unsupervised model - a deep neural network architecture - which contains an encoder and decoder. The encoder component serves as compressing the input to a lower-dimensional representation while the decoder aims to reconstruct the compressed representation back to the original input. 

The architecture is pretty simple, with the number of neurons in the layers of the encoder part (blue below) decreases, and then starts increasing again in the decoder part (purple below).

Input image => Dense(256) => Dense(64) => Dense(2) => Dense(64) => Dense(246) => Output (reconstructed image)

As one might expect, the loss is between the input image/data and reconstructed one, as the part of the name auto (self-supervised) implies.

In this post, we go through the implementation of Autoencoder with Tensorflow and Keras. The example below is from Probabilistic Deep Learning with TensorFlow 2 course from Coursera, which by the way, I am highly recommend if you want to get familiar with Tensorflow Probability module. However, for Autoencoder, we don't necessarily need the Tensorflow Probability module (The module is useful when implementing Variational AutoEncoder, a generative variant of Autoencoder). 

Contents

  • Import required packages
  • Fashion MNIST dataset
  • Encoder
  • Decoder
  • Encoding results after training
  • Autoencoder reconstructed results
  • Import required packages

    
    import tensorflow
    import matplotlib
    import seaborn as sns
    import numpy as np
    import matplotlib.pyplot as plt
    
    from tensorflow.keras.models import Sequential, Model
    from tensorflow.keras.layers import Dense, Flatten, Reshape
    
    print(tensorflow.__version__)
    print(matplotlib.__version__)
    print(np.__version__)
    print(sns.__version__)
    print(matplotlib.__version__)
    
    
    2.1.0
    3.0.3
    1.18.3
    0.9.0
    3.0.3

    Fashion MNIST dataset

    Fashion MNIST dataset is from Zalando - a publicly traded German online retailer of shoes, fashion and beauty active across Europe. The dataset consists of a training set of 60,000 examples and a test set of 10,000 examples. Each example is a 28x28 grayscale image, associated with a label from 10 classes. We don't use those labels but only use images as we want to use Autoencoder to compress and reconstruct a given image. Let's get started.
    
    # Load Fashion MNIST
    
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.fashion_mnist.load_data()
    x_train = x_train.astype('float32')/255.
    x_test = x_test.astype('float32')/255.
    class_names = np.array([
        'T-shirt/top', 
        'Trouser/pants', 
        'Pullover shirt', 
        'Dress',
        'Coat', 
        'Sandal', 
        'Shirt', 
        'Sneaker', 
        'Bag',
        'Ankle boot'
    ])
    
    print(x_train.shape)
    
    
    (60000, 28, 28)

    We can have a look on some of those images.

    
    # Display a few examples
    
    n_examples = 1000
    example_images = x_test[0:n_examples]
    example_labels = y_test[0:n_examples]
    
    f, axs = plt.subplots(1, 5, figsize=(15, 4))
    for j in range(len(axs)):
        axs[j].imshow(example_images[j], cmap='binary')
        axs[j].axis('off')
    
    


    Encoder

    Now we move on to the implementation of the encoder part of Autoencoder. The encoder simply flattens the input image and goes through two Dense layers followed by another Dense layer with desired encoding dimensionality, which is 2 here.

    We can check the compressed or encoded images using this encoder. Note as the encoder has not been trained yet, we should see those encoded images from different class are not distinguishable in the encoding space.

    
    # Define the encoder
    
    encoded_dim = 2
    encoder = Sequential([
        Flatten(input_shape=(28, 28)),
        Dense(256, activation='sigmoid'),
        Dense(64, activation='sigmoid'),
        Dense(encoded_dim)
    ])
    
    # Encode examples before training
    
    pretrain_example_encodings = encoder(example_images).numpy()
    
    # Plot encoded examples before training 
    
    f, ax = plt.subplots(1, 1, figsize=(7, 7))
    sns.scatterplot(pretrain_example_encodings[:, 0],
                    pretrain_example_encodings[:, 1],
                    hue=class_names[example_labels], ax=ax,
                    palette=sns.color_palette("colorblind", 10));
    ax.set_xlabel('Encoding dimension 1'); ax.set_ylabel('Encoding dimension 2')
    ax.set_title('Encodings of example images before training');
    



    Decoder

    Given the 2-dim encoded images, the decoder part tries to reconstruct the input image. And we can use the encoder and deconder that we've just defined to define the Autoencoder. Afterwards, we compile and fit the model as we usually do for training the Autoencoder.
    
    # Define the decoder
    
    decoder = Sequential([
        Dense(64, activation='sigmoid', input_shape=(encoded_dim,)),
        Dense(256, activation='sigmoid'),
        Dense(28*28, activation='sigmoid'),
        Reshape((28, 28))
    ])
    
    # Compile and fit the model
    
    autoencoder = Model(
        inputs=encoder.input,
        outputs=decoder(encoder.output)
    )
    
    # Specify loss - input and output is in [0., 1.], so we can use a binary cross-entropy loss
    autoencoder.compile(loss='binary_crossentropy')
    
    # Fit model - highlight that labels and input are the same
    autoencoder.fit(
        x=x_train, 
        y=x_train,
        epochs=10,
        batch_size=32
    )
    
    Train on 60000 samples
    Epoch 1/10 60000/60000 [==============================] - 76s 1ms/sample - loss: 0.4078
    Epoch 2/10 60000/60000 [==============================] - 74s 1ms/sample - loss: 0.3510
    Epoch 3/10 60000/60000 [==============================] - 75s 1ms/sample - loss: 0.3395
    Epoch 4/10 60000/60000 [==============================] - 78s 1ms/sample - loss: 0.3342
    Epoch 5/10 60000/60000 [==============================] - 78s 1ms/sample - loss: 0.3308
    Epoch 6/10 60000/60000 [==============================] - 78s 1ms/sample - loss: 0.3284
    Epoch 7/10 60000/60000 [==============================] - 77s 1ms/sample - loss: 0.3264
    Epoch 8/10 60000/60000 [==============================] - 74s 1ms/sample - loss: 0.3248
    Epoch 9/10 60000/60000 [==============================] - 70s 1ms/sample - loss: 0.3234
    Epoch 10/10 60000/60000 [==============================] - 84s 1ms/sample - loss: 0.3226

    Encoding results after training

    Now the Autoencoder has been trained. We can again check the encoded/compressed images to see if those encoded/compressed representations of images exhibit some interesting patterns ideally according to their categories.
    
    # Compute example encodings after training
    
    posttrain_example_encodings = encoder(example_images).numpy()
    
    # Compare the example encodings before and after training
    
    f, axs = plt.subplots(nrows=1, ncols=2, figsize=(15, 7))
    sns.scatterplot(pretrain_example_encodings[:, 0],
                    pretrain_example_encodings[:, 1],
                    hue=class_names[example_labels], ax=axs[0],
                    palette=sns.color_palette("colorblind", 10));
    sns.scatterplot(posttrain_example_encodings[:, 0],
                    posttrain_example_encodings[:, 1],
                    hue=class_names[example_labels], ax=axs[1],
                    palette=sns.color_palette("colorblind", 10));
    
    axs[0].set_title('Encodings of example images before training');
    axs[1].set_title('Encodings of example images after training');
    
    for ax in axs: 
        ax.set_xlabel('Encoding dimension 1')
        ax.set_ylabel('Encoding dimension 2')
        ax.legend(loc='upper right')
    



    As we can see from the figure, after training, images belong to the same or similar categories such as "Ankle boot" and "Sneaker" tend to be clustered together.

    Autoencoder reconstructed results

    Here we can reconstruct some images using the trained Autoencoder, which shows the reconstructed images are reasonably close to the given images.
    
    # Compute the autoencoder's reconstructions
    
    reconstructed_example_images = autoencoder(example_images)
    
    # Evaluate the autoencoder's reconstructions
    
    f, axs = plt.subplots(2, 5, figsize=(15, 4))
    for j in range(5):
        axs[0, j].imshow(example_images[j], cmap='binary')
        axs[1, j].imshow(reconstructed_example_images[j].numpy().squeeze(), cmap='binary')
        axs[0, j].axis('off')
        axs[1, j].axis('off')
    



    In this post, we introduced Autoencoder, which trains the encoder and decoder parts via "self-supervised" way by minimizing the reconstruction loss. Although Autoencoder can be useful for compression and reconstruction, it is not designed or trained to generate images. VAE (Variational Autoencoder) is the probablistic twist of Autoencoder for that purpose, which we will look into in another post.

    Self-Organizing Maps (SOM)

    Self-Organizing Maps (SOM) are one of the clustering techniques under the branch of unsupervised learning. It was invented by Teuvo Kohonen in 1982, and therefore, sometimes called as Kohonen map.

    In this post, we introduce the concept of SOM and the how the algorithm works in Python using an intuitive example. By the end of the post, you will learn about (1) what is SOM, (2) how is it working, and (3) how to implement it in Python.

  • Introduction to Self-Organizing Maps (SOM)
  • Importing required packages
  • Creating training data and initializing SOM cells
  • SOM implementation
  • Results
  • Introduction to Self-Organizing Maps (SOM)

    SOM represents can be treated as a form of artificial neural network that constructs a map based on the examples of training dataset. This map typically takes the form of a 2D rectangular grid of weights, although it can also be expanded to include 3D or higher-dimensional models.

    Each cell in the map can be thought as n-dimentional vector containing learnable weights, where n is also the same as the dimension of an example or the number of features in the example. For example, a training example $x_1$ and a cell $w_{ij}$ of SOM: $$x_1=\{f_{11}, \cdots, f_{1n}\}$$ $$w_{ij}=\{f_{ij1}, \cdots, f_{ijn}\}$$

    Initialization

    For the simplicity, we use the 2D case as our example. To get started, we need to pre-define the shape of SOM - axb - where a and b indicate the number of rows and columns of SOM respectively. In other words, we have axb cells and each cell can be thought as a cluster represented as a n-dimentional vector or weights. As one might expect, we need to initialize those weights. This can be done with a random initialization strategy or using training examples.

    Training

    Those initalized weights of SOM need to be trained/updated with training examples based on predefined number of epochs as we do in other machine learning methods. One big difference here is we don't define a loss function and use backpropagation to update the weights of SOM. Instead, SOM uses Competitive Learning which is a form of unsupervised learning, where constituent elements compete to produce a satisfying result, and only one gets to win the competition. 

    More specifically, for a given trainining instance x, we determine the best-matching cell/unit (refered as BMU) using a distance metric such as Euclidean distance (as both x and cell weight vector are n dimenitonal) who win the competition. Afterwards, we update the weights of the BMU as well as those of its neighboring cells, e.g., those on the left, right, above, and below. An update at time $t+1$ for the weight vector $w_{ij}$ of SOM can be described as: $$w_{ij}^{t+1} \leftarrow w_{ij}^{t} + \eta f(i,j,r_{t}) (x-w_{ij}^t)$$ where $\eta$ is the learning rate, (i,j) indicates the index of the cell in SOM, and $r_{t}$ refers to the radius to determine neighboring cells. $f(\cdot)$ is the neighborhood distance function. Simply put, a training example x pulls the weights of these cells towards it.

    To summarize, the training can be illustrated as below:
    • Initialize all grid weights of the SOM 
    • Repeat until convergence or the predefined maximum epochs 
      • Shuffle the training examples 
      • For each training instance x, we get the best matching unit BMU, and then update the weights of BMU and its neighboring cells
    One thing to note is that both the learning rate and the radius are decayed at each epoch during training. This means the weight updates and the number of neighboring cells is getting smaller in each epoch. In the following, we illustrate the concept of SOM in Python.

    Importing required packages

    First, let's import some required Python packages such as numpy and matplotlib.
    
    import numpy as np
    import matplotlib
    import matplotlib.pyplot as plt
    
    print(np.__version__)
    print(matplotlib.__version__)
    
    

    1.19.5
    3.3.4



    Creating training data and initializing SOM cells

    Here, we create a training data containing 3000 examples, and each example is 3-dimentional vector representing Red, Green, and Blue component value in the RGB color space. Then, we also create $10 \times 10$ SOM in which each cell is also 3-dimentional vector.
    
    m_som = 10
    n_som = 10
    n_x = 3000
    
    rand = np.random.RandomState(777)
    
    # Create training data
    x_train = rand.randint(
        low=0,
        high=255,
        size=(n_x, 3)
    )
    print(x_train.shape)
    
    # Initialize SOM cells
    som = rand.randint(
        low=0,
        high=255,
        size=(m_som, n_som, 3)
    )
    print(som.shape)
    
    

    (3000, 3)
    (10, 10, 3)

    I like this example as we can visualize it and see the generated training examples and the initialized SOM.

    
    fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
    ax[0].imshow(x_train.reshape(50, 60, 3))
    ax[0].set_title('Training data')
    ax[1].imshow(som)
    ax[1].set_title('Initialized SOM')
    
    

    SOM implementation

    Now, we can implement two functions, getBMU() for retrieving the BMU of SOM and update_weights() to update the weights of cells for a given training example as we discussed before.

    Getting BMU cell

    
    def getBMU(x, som):
        """ Return cell index g,h of som clostest to x """
        square_distance = (np.square(x - som)).sum(axis=2)
        return np.unravel_index(
            np.argmin(square_distance, axis=None), 
            square_distance.shape
        )
    
    getBMU(x_train[0], som)
    
    

    (8, 1)

    Updating the weights of SOM cells

    
    def update_weights(
        som, 
        x, 
        lr, 
        radius_sq, 
        bmu, 
        step=3
    ):
        """
        Update the weights of the SOM cells with a training example
    
        :parameter som: SOM
        :parameter x: a training example
        :parameter lr: learing rate
        :parameter radius_sq: radius
        :parameter bmu: (g,h) coordinates of bmu
        :parameter step: to determine neighborhood
        """
        g, h = bmu
        #if radius is close to zero then only BMU is changed
        if radius_sq < 1e-3:
            som[g,h,:] += lr * (x - som[g,h,:])
            return som
        # Change all cells in a small neighborhood of BMU based on step 
        for i in range(max(0, g-step), min(som.shape[0], g+step)):
            for j in range(max(0, h-step), min(som.shape[1], h+step)):
                dist_sq = np.square(i - g) + np.square(j - h)
                dist_func = np.exp(-dist_sq / 2 / radius_sq)
                som[i,j,:] += lr * dist_func * (x - som[i,j,:])   
        return som  
    
    

    Training: Learning the weights of SOM cells

    Now we can start training SOM using those two implemented functions. Here, we train 10 epochs with a decay of 0.1 for both learning rate and radius.
    
    def train(
        som, 
        x_train, 
        learn_rate = 0.1, 
        radius_sq = 1, 
        lr_decay = .1, 
        radius_decay = .1, 
        epochs = 10
    ):
        """ Training SOM cell weights """
        learn_rate_0 = learn_rate
        radius_0 = radius_sq
        for epoch in np.arange(0, epochs):
            rand.shuffle(x_train)      
            for x in x_train:
                g, h = getBMU(som, x)
                som = update_weights(
                        som.astype(float), 
                        x, 
                        learn_rate, 
                        radius_sq, 
                        (g,h)
                )
            # Update learning rate and radius
            learn_rate = learn_rate_0 * np.exp(-epoch * lr_decay)
            radius_sq = radius_0 * np.exp(-epoch * radius_decay)            
        return som
    
    

    Results

    We can also visualize the change of weights in SOM after certain epochs to investigate how the weights of SOM have been changing during our training process.
    
    fig, ax = plt.subplots(
        nrows=1, 
        ncols=4, 
        figsize=(15, 4), 
    )
    
    total_epochs = 0
    for epochs, i in zip([1, 4, 5, 10], range(0,4)):
        total_epochs += epochs
        som = train(som, x_train, epochs=epochs)
        ax[i].imshow(som.astype(int))
        ax[i].title.set_text('Epochs = ' + str(total_epochs))
    
    


    References

    TypeError: Descriptors cannot not be created directly.

     2023-10-19 10:01:41.895250: W tensorflow/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory

    2023-10-19 10:01:41.895278: I tensorflow/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.

    Traceback (most recent call last):

      File "t01.py", line 7, in <module>

        import tensorflow.compat.v2 as tf

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/__init__.py", line 37, in <module>

        from tensorflow.python.tools import module_util as _module_util

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/python/__init__.py", line 37, in <module>

        from tensorflow.python.eager import context

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/python/eager/context.py", line 29, in <module>

        from tensorflow.core.framework import function_pb2

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/core/framework/function_pb2.py", line 16, in <module>

        from tensorflow.core.framework import attr_value_pb2 as tensorflow_dot_core_dot_framework_dot_attr__value__pb2

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/core/framework/attr_value_pb2.py", line 16, in <module>

        from tensorflow.core.framework import tensor_pb2 as tensorflow_dot_core_dot_framework_dot_tensor__pb2

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/core/framework/tensor_pb2.py", line 16, in <module>

        from tensorflow.core.framework import resource_handle_pb2 as tensorflow_dot_core_dot_framework_dot_resource__handle__pb2

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/core/framework/resource_handle_pb2.py", line 16, in <module>

        from tensorflow.core.framework import tensor_shape_pb2 as tensorflow_dot_core_dot_framework_dot_tensor__shape__pb2

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/tensorflow/core/framework/tensor_shape_pb2.py", line 36, in <module>

        _descriptor.FieldDescriptor(

      File "/home/parklize/Documents/code/tfp/venv/lib/python3.8/site-packages/google/protobuf/descriptor.py", line 561, in __new__

        _message.Message._CheckCalledFromGeneratedFile()

    TypeError: Descriptors cannot not be created directly.

    If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc >= 3.19.0.

    If you cannot immediately regenerate your protos, some other possible workarounds are:

     1. Downgrade the protobuf package to 3.20.x or lower.

     2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower).


    More information: https://developers.google.com/protocol-buffers/docs/news/2022-05-06#python-updates

    =======================================
    To solve the issue, I needed to downgrade protobuf using pip
    $ pip install protobuf==3.20.*

    Note: the * above is not to be taken literally, it's called a "wildcard". You put your own number in there as needed, as in 3.20.1, 3.20.5, etc. See https://stackoverflow.com/questions/72899948/how-to-downgrade-protobuf for details.

    Building Machine Learning Apps with Hugging Face: LLMs to Diffusion Modeling

    The seminar "Building Machine Learning Apps with Hugging Face: LLMs to Diffusion Modeling" from a few weeks ago from Hugging Face provides some interesting insights on LLMs to Diffusion Modeling.


    History of MLOps

    - Software is eating the world

    - Deep learning is eating the world

    - Transformers are eating the Deep learning

    Underthehood, transfer learning is the key player for transformers eating the deep learning.


    Transfer learning

    There are many pre-trained models which have been trained on large-scale data that we cannot do by ourselves. But thankfully, we can use the output (the pre-trained models) for our tasks.

    So nowadays, we can easily use those models, e.g., via Hugging Face, by following steps:

    1. Identify the task matching the problem

    - Text, Images, Speech

    2. Pick a pre-trained SOTA 

    - no need to build, label, and clean a large dataset

    - 2 lines of code to download and test a model

    3. If needed, fine-tune it on your dataset

            - build customized layers on top of pre-trained models for our needs

            - only train the parameters of the customized layers


    Mission of Hugging Face: Democratize Good ML

            - open source, community, ethics-first


    Some facts about Hugging Face

    - Diffusers is growing faster than Transformers 

    - 150k free public models

    - Over 50% of Hub models are private 

    - 1M model downloads/day

    - How we make money? Sell compute services, expert support

    - Get started with /tasks

    - Bloomz from Bloom: an instruction fine-tuned open source LLM 

    - Fast ControlNet: Guide image generation

    - Mac app for that (Diffusers)

    - Free diffusion course from Hugging Face (git)

    - Javascript client - huggingface.js


    Building ML with Hugging Face

    - Explore models

    - Manage your models (every model is on github)

    - Collaborate

    - Use


    from transformers import pipeline

    - pipeline - encapsulize all - text in text out


    AutoTrain

    - allows to train based on your data (pricing)


    Spaces

    - allows you to create your app 


    Deploy models at scale

    - headache about GPUs, CPUs, Docker containers etc.

    - Inference Endpoints (we made models plug and play)


    LLMs 3types

    - Bloom, GPT

    - Bloomz, GPT-3: intruction guided 

    - ChatGPT: Human feedback (RL-based)


    Pinecone: vector database company

    ModelScope from text to generate video







    DeepCTR: Hello World



    In this post, we look at a "hello world" example of DeepCTR library. DeepCTR is a project that introduces classic CTR (Click Through Rate) prediction model and implements popular network designed for CTR prediction task. 

    What’s more, it provides a great number of experiments on open data and provide as benchmark. It also provides ready-to-go implementations of many SOTA (State of the Art) CTR prediction models in the literature, which is super convenient to use.


    Import libraries required

    
    import pandas as pd
    import numpy as np
    import pandas_profiling
    import random
    import tensorflow as tf
    
    from sklearn.preprocessing import LabelEncoder
    from deepctr.models import DeepFM
    from deepctr.feature_column import SparseFeat, DenseFeat, get_feature_names
    
    # For reproducible experiments
    random.seed(200)
    np.random.seed(200)
    %load_ext line_profiler
    


    Frappe dataset

    We use the Frappe dataset, which is a small dataset for quick experimentation and testing of different models. It has been used for context-aware app recommendation, which contains 96,203 app usage logs of users under different contexts. The eight context variables are all categorical, including weather, city, daytime and so on.
    data = pd.read_csv('datasets/Frappe/Mobile_Frappe/frappe/frappe.csv', sep='\t')
    data['target'] = 1
    
    num_users = len(data["user"].unique())
    items = data["item"].unique()
    num_items = len(items)
    print(f'distinct users: {num_users}')
    print(f'distinct items: {num_items}')
    print(f'sparsity: {len(data)/(num_users*num_items)}')
    
    sparse_features = [
        'user',
        'item',
        'daytime',
        'weekday',
        'isweekend',
        'homework',
        'cost',
        'weather',
        'country',
        'city'
    ]
    
    # Shuffle
    data = data.sample(frac=1,random_state=0)
    # pandas_profiling.ProfileReport(data)
    data.loc[23225]
    
    Output:
    
    distinct users: 957
    distinct items: 4082
    sparsity: 0.024626555814783357
    


    Data preparation

    Here we follow the benchmark setting from DeepCTR documentation page. After one-hot encoding of the features, we obtain 5,382 features. As all logs should be considered as positive sample when making CTR prediction, we construct two negative instances for each log through randomly replacing the item variable with other item. The data is randomly split into training data (70%), validation data (20%), test data (10%) before constructing negative instances.
    
    def get_gt(user):
        """ Get ground truth of user """
        return data[data['user']==user]['item'].values
    
    ##### 70%, 20%, 10% split
    num_train, num_val = int(len(data)*0.7), int(len(data)*0.2)
    num_test = len(data) - num_train - num_val
    print(num_train, num_val, num_test)
    
    train_data = data.iloc[0:num_train]
    val_data = data.iloc[num_train:num_train+num_val]
    test_data = data.iloc[num_train+num_val:]
    
    def neg_sampling(d, n):
        """ Get n negative samples for each pos samples """
        neg_samples = []
        
        def _get_neg_list():
            neg_list = []
            for r in d.iterrows():
                user = r[1]['user']
                num_sampled = 0
                while num_sampled != n:
                    sampled_items = np.random.choice(items, size=n, replace=False)
        #             print(sampled_items)
                    num_sampled = sum([x not in get_gt(user) for x in sampled_items])
                for i in sampled_items:
                    neg_ex = r[1].copy()
                    neg_ex['item'] = i
                    neg_ex['target'] = 0
        #             d = d.append(neg_ex)
                    neg_list.append(neg_ex)
        #         break
            return neg_list
        
        neg_samples += (_get_neg_list())
        neg_df = pd.concat(neg_samples, axis=1).transpose()
        d = d.append(neg_df)
        d = d.sample(frac=1,random_state=0)
        
        return d
    
    num_neg = 2
    train_data = neg_sampling(train_data, num_neg)
    print('train neg sampling is finished')
    val_data = neg_sampling(val_data, num_neg)
    print('val neg sampling is finished')
    test_data = neg_sampling(test_data, num_neg)
    test_data
    
    Encoding categorical values
    
    for f in sparse_features:
        print(f)
        lbe = LabelEncoder()
        data[f] = lbe.fit_transform(data[f])
        train_data[f] = lbe.transform(train_data[f])
        val_data[f] = lbe.transform(val_data[f])
        test_data[f] = lbe.transform(test_data[f])
    
    Prepare for the model input format with respect to training, validation, and testing data.
    
    fixlen_feature_columns = [
        SparseFeat(feat, vocabulary_size=data[feat].max()+1, embedding_dim=4) \
            for feat in sparse_features 
    ]
    feature_names = get_feature_names(fixlen_feature_columns)
    print(feature_names)
    
    train_model_input = {
        name:train_data[name].astype('float32').values \
        for name in feature_names
    }
    val_model_input = {
        name:val_data[name].astype('float32').values \
        for name in feature_names
    }
    test_model_input = {
        name:test_data[name].astype('float32').values \
        for name in feature_names
    }
    


    Using DeepFM without Early Stopping strategy

    Here we use the DeepFM (Deep Factorization Machines) model for training with the prepared dataset mentioned above. We don't use any early stopping here, and trian 40 epochs.
    
    model = DeepFM([], fixlen_feature_columns, task='binary')
    
    model.compile('adam',
        'binary_crossentropy',
        metrics='binary_crossentropy')
    
    # Early stopping
    earlystopping_callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5)
    
    # Best model checkpoint
    checkpoint_filepath = '/tmp/checkpoint'
    model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
        filepath=checkpoint_filepath,
        save_weights_only=True,
        monitor='val_loss',
        mode='min',
        save_best_only=True)
    
    
    history = model.fit(train_model_input,
        train_data['target'].astype('float32').values,
        batch_size=256,
        epochs=40,
        verbose=2,
        validation_data=(val_model_input, 
                         val_data['target'].astype('float32').values),
    #     callbacks=[
    #         earlystopping_callback, 
    #         model_checkpoint_callback
    #     ]
    )
    

    Check the performance on the test set

    
    from sklearn.metrics import log_loss, roc_auc_score, accuracy_score
    
    pred_ans = model.predict(test_model_input, batch_size=256)
    
    print('log loss', log_loss(test_data['target'].astype('float32').values, pred_ans, eps=1e-7))
    print('auc', roc_auc_score(test_data['target'].astype('float32').values, pred_ans))
    
    During the 40 epochs, the log loss start decreasing and increasing again, ending up to the output below. Output:
    
    log loss 0.2621498030364543
    auc 0.9733840688051586
    



    Using DeepFM with Early Stopping strategy

    
    model = DeepFM([], fixlen_feature_columns, task='binary')
    
    model.compile('adam',
        'binary_crossentropy',
        metrics='binary_crossentropy')
    
    # Early stopping
    earlystopping_callback = tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=5)
    
    # Best model checkpoint
    checkpoint_filepath = '/tmp/checkpoint'
    model_checkpoint_callback = tf.keras.callbacks.ModelCheckpoint(
        filepath=checkpoint_filepath,
        save_weights_only=True,
        monitor='val_loss',
        mode='min',
        save_best_only=True)
    
    
    history = model.fit(train_model_input,
        train_data['target'].astype('float32').values,
        batch_size=256,
        epochs=40,
        verbose=2,
        validation_data=(val_model_input, 
                         val_data['target'].astype('float32').values),
         callbacks=[
             earlystopping_callback, 
             model_checkpoint_callback
         ]
    )
    
    This time we apply an early stopping strategy with a patience of 5 steps. That is, if there is no decrease in the 5 concecutive steps during trating, the training process will be stopped early before 40 epochs. Output:
    
    log loss 0.19507425489108865
    auc 0.9737500710457143
    
    We can observe that our log loss and AUC have been improved with early stopping strategy compared to that without using early stopping.

    Tensorflow Recommenders 5 - Use DCN (Deep Cross Network) as the ranking model



    In the previous tutorials, we have looked at retrieval model, ranking model, leveraging contextual features, and training both retrieval and ranking models in a multi-task learning scheme. 

    In this post, we look at a more recent state-of-the-art ranking model - Deep Cross Network (DCN) - as our ranking model. The other settings are exactly the same as the previous tutorial for training both retrieval and ranking models in a multi-task learning scheme, except the introduction of DCN, and updated Movielens model with that in this tutorial.


    Content

    • Deep Cross Network (DCN)
    • Prepare dataset
    • User model
    • Movie model
    • Movielens model
    • Train and evaluate the model


    Deep Cross Network (DCN)

    Cross features such as whether banana and milk are bought together play important role in recommender systems due to the challenges such as the feature space normally large and sparse, and therefore manually engineering features are challenging, and also, traditional neural networks are not work well on this by implicitly modeling feature interactions.

    DCN was first introduced in 2017 with its updated version DCN V2 recently. The essense of DCN is the cross network:

    $$x_{i+1} = x_0 \odot (W \times x_i + b) + x_i$$

    where $x_0$ is the input features (an embedding layer) and $x_{i+1}$ is the $i+1$-th layer output of the cross net.

    The cross network can be multiple layers, and can also be stacked on top of an embedding layer in parallel with a DNN. Alternatively, it can also stack on top of the embedding layer and followed by a DNN.

    In the MovielensModel we use later on, we will use the second design where the DNN comes after the DCN with the keras Functional API.

    Prepare dataset

    First, let's import packages that we need, and print out Tensorflow and TFRS versions for reference.
    
    from typing import Dict, Text # for typing hint
    
    import pprint
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfds
    import tensorflow_recommenders as tfrs
    
    print(tf.__version__)
    print(tfrs.__version__)
    
    Output:
    
    2.9.1
    v0.7.0
    

    Load the Movielens 100k dataset.
    
    # Load the movielens dataset
    ratings = tfds.load('movielens/100k-ratings', split='train')
    ratings = ratings.map(lambda x: {
        'movie_title': x['movie_title'],
        'user_id': x['user_id'],
        'user_rating': x['user_rating'],
        'timestamp': x['timestamp']
    })
    
    movies = tfds.load('movielens/100k-movies', split='train')
    movies = movies.map(lambda x: x['movie_title'])
    
    timestamps = np.concatenate(list(
        ratings.map(lambda x: x['timestamp']).batch(100)))
    max_timestamp = timestamps.max()
    min_timestamp = timestamps.min()
    
    timestamp_buckets = np.linspace(
        min_timestamp, max_timestamp, num=1000)
    
    unique_movie_titles = np.unique(np.concatenate(list(movies.batch(1000))))
    unique_user_ids = np.unique(np.concatenate(list(ratings.batch(1_000).map(
        lambda x: x['user_id']))))
    
    print(len(unique_movie_titles), len(unique_user_ids))
    
    Output:
    
    1664 943
    

    User Model

    We use UserModel as the same one defined in the previous tutorial in which we can optionally choose to use the timestamps as part of our user model.
    
    class UserModel(tf.keras.Model):
        # User embedding will be user id + ts + normalized ts embeddings
    
        def __init__(self, use_timestamps):
            super().__init__()
            
            self._use_timestamps = use_timestamps
            
            # User id embedding
            self.user_embedding = tf.keras.Sequential([
                tf.keras.layers.StringLookup(
                    vocabulary=unique_user_ids,
                    mask_token=None),
                tf.keras.layers.Embedding(len(unique_user_ids)+1, 32)
            ])
            
            if use_timestamps:
                # Use timestamp
                self.timestamp_embedding = tf.keras.Sequential([
                    tf.keras.layers.Discretization(timestamp_buckets.tolist()),
                    tf.keras.layers.Embedding(len(timestamp_buckets)+1, 32)
                ])
    
                # Normalized timestamp
                self.normalized_timestamp = tf.keras.layers.Normalization(axis=None)
                self.normalized_timestamp.adapt(timestamps)
            
        
        def call(self, inputs):
            if not self._use_timestamps:
                return self.user_embedding(inputs['user_id'])
            
            return tf.concat([
                self.user_embedding(inputs['user_id']),
                self.timestamp_embedding(inputs['timestamp']),
                tf.reshape(self.normalized_timestamp(inputs['timestamp']),(-1,1))
            ], axis=1) 
    

    Movie Model

    Similarly, we use the MovieModel as the same one we used in the previous tutorial in which we optionally use the title text as part of the movie embeddings.
    
    class MovieModel(tf.keras.Model):
        # Movie embedding: title text + id 
        
        def __init__(self, use_title_text):
            super().__init__()
            max_tokens = 10_000
            
            self._use_title_text = use_title_text
            
            self.title_embedding = tf.keras.Sequential([
                tf.keras.layers.StringLookup(
                    vocabulary=unique_movie_titles, mask_token=None),
                tf.keras.layers.Embedding(len(unique_movie_titles)+1, 32)
            ])
            
            if use_title_text:
                self.title_vectorizer = tf.keras.layers.TextVectorization(
                    max_tokens=max_tokens)
                self.title_vectorizer.adapt(movies)
    
                self.title_text_embedding = tf.keras.Sequential([
                    self.title_vectorizer,
                    tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
                    tf.keras.layers.GlobalAveragePooling1D()
                ])
            
            
        def call(self, inputs):
            if not self._use_title_text:
                return self.title_embedding(inputs)
            
            return tf.concat([
                self.title_embedding(inputs),
                self.title_text_embedding(inputs)
            ], axis=1)
    

    Movielens Model

    So far, the UserModel and MovieModel are exactly the same as in the previous tutorial, and nothing new. Now we move on to define our new MovielensModel which allows us to train both the retrieval and ranking tasks together in a multi-task training scheme. 

    We can see that two tasks are defined in the __init__() method, and in the compute_loss() we are calculating the loss as the total of both tasks with equal contribution (with their corresponding weights self.rating_weight and self.retrieval_weight as 0.5 respectively).
    
    class MovielensModel(tfrs.models.Model):
        
        def __init__(self, use_timestamps=True, use_title_text=True):
            super().__init__()
            
            self.rating_weight = 0.5
            self.retrieval_weight = 0.5
            
            # User and Movie models
            self.user_model = tf.keras.Sequential([
                UserModel(use_timestamps),
                tf.keras.layers.Dense(32)
            ])
            self.movie_model = tf.keras.Sequential([
                MovieModel(use_title_text),
                tf.keras.layers.Dense(32)
            ])
            
    	# Ranking model with DCN using keras Functional API
            x0 = tf.keras.Input(shape=(64,))
            x1 = tfrs.layers.dcn.Cross()(x0, x0)
            x2 = tfrs.layers.dcn.Cross()(x0, x1)
            d1 = tf.keras.layers.Dense(256, activation='relu')(x2)
            d2 = tf.keras.layers.Dense(64, activation='relu')(d1)
            output = tf.keras.layers.Dense(1)(d2)
            self.rating_model = tf.keras.Model(inputs=x0, outputs=output)
        
            # Multi-tasks
            self.rating_task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
                loss=tf.keras.losses.MeanSquaredError(),
                metrics=[tf.keras.metrics.RootMeanSquaredError()]
            )
            self.retrieval_task: tf.keras.layers.Layer = tfrs.tasks.Retrieval(
                metrics=tfrs.metrics.FactorizedTopK(
                    candidates=movies.batch(128).map(self.movie_model)
                )
            )
                
        def call(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
            user_embeddings = self.user_model({
                'user_id': features['user_id'],
                'timestamp': features['timestamp']
            })
            movie_embeddings = self.movie_model(
                features['movie_title']
            )
            return (
                user_embeddings, 
                movie_embeddings,
                self.rating_model(tf.concat([
                    user_embeddings,
                    movie_embeddings
                ], axis=1))
            )
            
        def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
            user_embeddings, movie_embeddings, rating_predictions = self.call(features)
            # Retrieval loss
            retrieval_loss = self.retrieval_task(user_embeddings, movie_embeddings)
            # Rating loss
            rating_loss = self.rating_task(
                labels=features['user_rating'],
                predictions=rating_predictions
            )
            
            # Combine two losses with hyper-parameters (to be tuned)
            return (self.rating_weight * rating_loss \
                    + self.retrieval_weight * retrieval_loss)
    





    Train and evaluate the model

    We use 80% of the dataset for training, and the rest (20%) for testing.
    
    # -------------------------------
    # Experiment
    # -------------------------------
    # Prepare data
    tf.random.set_seed(7)
    shuffled = ratings.shuffle(100_000, seed=7,
                    reshuffle_each_iteration=False)
    
    train = shuffled.take(80_000)
    test = shuffled.skip(80_000).take(20_000)
    
    cached_train = train.shuffle(100_000).batch(2048).cache()
    cached_test = test.batch(4096).cache()
    
    
    model = MovielensModel(use_timestamps=True, use_title_text=True)
    model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
    
    model.fit(cached_train, epochs=3)
    
    train_acc = model.evaluate(
        cached_train, return_dict=True)['factorized_top_k/top_100_categorical_accuracy']
    test_acc = model.evaluate(
        cached_test, return_dict=True)['factorized_top_k/top_100_categorical_accuracy']
    
    print(f'Top-100 accuracy (train): {train_acc:.2f}')
    print(f'Top-100 accuracy (test): {test_acc:.2f}')
    
    Output:
    
    Top-100 accuracy (train): 0.34
    Top-100 accuracy (test): 0.25
    
    Evaluation results of all metrics on the test set.
    
    model.evaluate(cached_test, return_dict=True)
    
    Output:
    
    {'root_mean_squared_error': 1.0266695022583008,
     'factorized_top_k/top_1_categorical_accuracy': 0.001449999981559813,
     'factorized_top_k/top_5_categorical_accuracy': 0.010900000110268593,
     'factorized_top_k/top_10_categorical_accuracy': 0.025450000539422035,
     'factorized_top_k/top_50_categorical_accuracy': 0.13050000369548798,
     'factorized_top_k/top_100_categorical_accuracy': 0.2531999945640564,
     'loss': 13955.5576171875,
     'regularization_loss': 0,
     'total_loss': 13955.5576171875}
    
    As a quick comparision, the results on the test set without DCN in the previous tutorial was as follows, in which the RMSE (Root Mean Squared Error) has been decreased using DCN.
    
    {'root_mean_squared_error': 1.0596544742584229,
     'factorized_top_k/top_1_categorical_accuracy': 0.00139999995008111,
     'factorized_top_k/top_5_categorical_accuracy': 0.011549999937415123,
     'factorized_top_k/top_10_categorical_accuracy': 0.025450000539422035,
     'factorized_top_k/top_50_categorical_accuracy': 0.1335500031709671,
     'factorized_top_k/top_100_categorical_accuracy': 0.24815000593662262,
     'loss': 13961.8505859375,
     'regularization_loss': 0,
     'total_loss': 13961.8505859375}
    

    We can get the 5 movies in the test set for user 42 with a certain timestamp, and sort them based on their scores in a descending order.
    
    test_ratings = {}
    for m in test.take(5):
    #     print(m['movie_title'].numpy())
        _, _, test_ratings[m['movie_title'].numpy()] = \
            model(
                {'user_id':np.array(['42']), 
                 'timestamp':np.array([892839492]), 
                 'movie_title': np.array([m['movie_title'].numpy()])
                }
            )
        
    for m in sorted(test_ratings, key=test_ratings.get, reverse=True):
        print(m)
    
    Output:
    
    b'Chasing Amy (1997)'
    b'Top Gun (1986)'
    b'Twister (1996)'
    b'Event Horizon (1997)'
    b'Batman Forever (1995)'
    

    We can look into the scores of those movies in test_ratings
    
    for r in test_ratings: 
        print(r, test_ratings[r].numpy()[0][0])
    
    Output:
    
    b'Top Gun (1986)' 3.278518
    b'Chasing Amy (1997)' 3.3716574
    b'Batman Forever (1995)' 2.8443925
    b'Twister (1996)' 3.1891446
    b'Event Horizon (1997)' 3.0244293
    

    Finally, let's change the timestamp of user 42 for those movies to check if the predicted scores/ratings change. When we change the timestamp from 892839492 to 879024327, we can see the ratings change accordingly as below: Output:
    
    b'Top Gun (1986)' 3.3720827
    b'Chasing Amy (1997)' 3.408927
    b'Batman Forever (1995)' 3.0349593
    b'Twister (1996)' 3.162705
    b'Event Horizon (1997)' 3.058013
    

    More TFRS tutorials can be found at https://parklize.blogspot.com/p/tensorflow.html

    References

    Tensorflow Recommenders 4 - Multi-task learning: How to train retrieval and ranking models together?



    In the previous tutorials, we have looked at retrieval model, ranking model, and leveraging contextual features for those models. In this post, we look at how to train the two models (retrieval and ranking) together as a multi-task learning problem.

    Content

    • Prepare dataset
    • User model
    • Movie model
    • Movielens model
    • Train and evaluate the model

    Prepare dataset

    First, let's import packages that we need, and print out Tensorflow and TFRS versions for reference.
    
    from typing import Dict, Text # for typing hint
    
    import pprint
    import numpy as np
    import tensorflow as tf
    import tensorflow_datasets as tfds
    import tensorflow_recommenders as tfrs
    
    print(tf.__version__)
    print(tfrs.__version__)
    
    Output:
    
    2.9.1
    v0.7.0
    

    Load the Movielens 100k dataset.
    
    # Load the movielens dataset
    ratings = tfds.load('movielens/100k-ratings', split='train')
    ratings = ratings.map(lambda x: {
        'movie_title': x['movie_title'],
        'user_id': x['user_id'],
        'user_rating': x['user_rating'],
        'timestamp': x['timestamp']
    })
    
    movies = tfds.load('movielens/100k-movies', split='train')
    movies = movies.map(lambda x: x['movie_title'])
    
    timestamps = np.concatenate(list(
        ratings.map(lambda x: x['timestamp']).batch(100)))
    max_timestamp = timestamps.max()
    min_timestamp = timestamps.min()
    
    timestamp_buckets = np.linspace(
        min_timestamp, max_timestamp, num=1000)
    
    unique_movie_titles = np.unique(np.concatenate(list(movies.batch(1000))))
    unique_user_ids = np.unique(np.concatenate(list(ratings.batch(1_000).map(
        lambda x: x['user_id']))))
    
    print(len(unique_movie_titles), len(unique_user_ids))
    
    Output:
    
    1664 943
    

    User Model

    We use UserModel as the same one defined in the previous tutorial in which we can optionally choose to use the timestamps as part of our user model.
    
    class UserModel(tf.keras.Model):
        # User embedding will be user id + ts + normalized ts embeddings
    
        def __init__(self, use_timestamps):
            super().__init__()
            
            self._use_timestamps = use_timestamps
            
            # User id embedding
            self.user_embedding = tf.keras.Sequential([
                tf.keras.layers.StringLookup(
                    vocabulary=unique_user_ids,
                    mask_token=None),
                tf.keras.layers.Embedding(len(unique_user_ids)+1, 32)
            ])
            
            if use_timestamps:
                # Use timestamp
                self.timestamp_embedding = tf.keras.Sequential([
                    tf.keras.layers.Discretization(timestamp_buckets.tolist()),
                    tf.keras.layers.Embedding(len(timestamp_buckets)+1, 32)
                ])
    
                # Normalized timestamp
                self.normalized_timestamp = tf.keras.layers.Normalization(axis=None)
                self.normalized_timestamp.adapt(timestamps)
            
        
        def call(self, inputs):
            if not self._use_timestamps:
                return self.user_embedding(inputs['user_id'])
            
            return tf.concat([
                self.user_embedding(inputs['user_id']),
                self.timestamp_embedding(inputs['timestamp']),
                tf.reshape(self.normalized_timestamp(inputs['timestamp']),(-1,1))
            ], axis=1) 
    

    Movie Model

    Similarly, we use the MovieModel as the same one we used in the previous tutorial in which we optionally use the title text as part of the movie embeddings.
    
    class MovieModel(tf.keras.Model):
        # Movie embedding: title text + id 
        
        def __init__(self, use_title_text):
            super().__init__()
            max_tokens = 10_000
            
            self._use_title_text = use_title_text
            
            self.title_embedding = tf.keras.Sequential([
                tf.keras.layers.StringLookup(
                    vocabulary=unique_movie_titles, mask_token=None),
                tf.keras.layers.Embedding(len(unique_movie_titles)+1, 32)
            ])
            
            if use_title_text:
                self.title_vectorizer = tf.keras.layers.TextVectorization(
                    max_tokens=max_tokens)
                self.title_vectorizer.adapt(movies)
    
                self.title_text_embedding = tf.keras.Sequential([
                    self.title_vectorizer,
                    tf.keras.layers.Embedding(max_tokens, 32, mask_zero=True),
                    tf.keras.layers.GlobalAveragePooling1D()
                ])
            
            
        def call(self, inputs):
            if not self._use_title_text:
                return self.title_embedding(inputs)
            
            return tf.concat([
                self.title_embedding(inputs),
                self.title_text_embedding(inputs)
            ], axis=1)
    

    Movielens Model

    So far, the UserModel and MovieModel are exactly the same as in the previous tutorial, and nothing new. Now we move on to define our new MovielensModel which allows us to train both the retrieval and ranking tasks together in a multi-task training scheme. 

    We can see that two tasks are defined in the __init__() method, and in the compute_loss() we are calculating the loss as the total of both tasks with equal contribution (with their corresponding weights self.rating_weight and self.retrieval_weight as 0.5 respectively).
    
    class MovielensModel(tfrs.models.Model):
        
        def __init__(self, use_timestamps=True, use_title_text=True):
            super().__init__()
            
            self.rating_weight = 0.5
            self.retrieval_weight = 0.5
            
            # User and Movie models
            self.user_model = tf.keras.Sequential([
                UserModel(use_timestamps),
                tf.keras.layers.Dense(32)
            ])
            self.movie_model = tf.keras.Sequential([
                MovieModel(use_title_text),
                tf.keras.layers.Dense(32)
            ])
            
            # Ranking model
            self.rating_model = tf.keras.Sequential([
                # Multiple dense layers
                tf.keras.layers.Dense(256, activation='relu'),
                tf.keras.layers.Dense(64, activation='relu'),
                # Prediction layer
                tf.keras.layers.Dense(1)
            ])
        
            # Multi-tasks
            self.rating_task: tf.keras.layers.Layer = tfrs.tasks.Ranking(
                loss=tf.keras.losses.MeanSquaredError(),
                metrics=[tf.keras.metrics.RootMeanSquaredError()]
            )
            self.retrieval_task: tf.keras.layers.Layer = tfrs.tasks.Retrieval(
                metrics=tfrs.metrics.FactorizedTopK(
                    candidates=movies.batch(128).map(self.movie_model)
                )
            )
                
        def call(self, features: Dict[Text, tf.Tensor]) -> tf.Tensor:
            user_embeddings = self.user_model({
                'user_id': features['user_id'],
                'timestamp': features['timestamp']
            })
            movie_embeddings = self.movie_model(
                features['movie_title']
            )
            return (
                user_embeddings, 
                movie_embeddings,
                self.rating_model(tf.concat([
                    user_embeddings,
                    movie_embeddings
                ], axis=1))
            )
            
        def compute_loss(self, features: Dict[Text, tf.Tensor], training=False) -> tf.Tensor:
            user_embeddings, movie_embeddings, rating_predictions = self.call(features)
            # Retrieval loss
            retrieval_loss = self.retrieval_task(user_embeddings, movie_embeddings)
            # Rating loss
            rating_loss = self.rating_task(
                labels=features['user_rating'],
                predictions=rating_predictions
            )
            
            # Combine two losses with hyper-parameters (to be tuned)
            return (self.rating_weight * rating_loss \
                    + self.retrieval_weight * retrieval_loss)
    





    Train and evaluate the model

    We use 80% of the dataset for training, and the rest (20%) for testing.
    
    # -------------------------------
    # Experiment
    # -------------------------------
    # Prepare data
    tf.random.set_seed(7)
    shuffled = ratings.shuffle(100_000, seed=7,
                    reshuffle_each_iteration=False)
    
    train = shuffled.take(80_000)
    test = shuffled.skip(80_000).take(20_000)
    
    cached_train = train.shuffle(100_000).batch(2048).cache()
    cached_test = test.batch(4096).cache()
    
    
    model = MovielensModel(use_timestamps=True, use_title_text=True)
    model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
    
    model.fit(cached_train, epochs=3)
    
    train_acc = model.evaluate(
        cached_train, return_dict=True)['factorized_top_k/top_100_categorical_accuracy']
    test_acc = model.evaluate(
        cached_test, return_dict=True)['factorized_top_k/top_100_categorical_accuracy']
    
    print(f'Top-100 accuracy (train): {train_acc:.2f}')
    print(f'Top-100 accuracy (test): {test_acc:.2f}')
    
    Output:
    
    Top-100 accuracy (train): 0.34
    Top-100 accuracy (test): 0.25
    
    Evaluation results of all metrics on the test set.
    
    model.evaluate(cached_test, return_dict=True)
    
    Output:
    
    {'root_mean_squared_error': 1.0596544742584229,
     'factorized_top_k/top_1_categorical_accuracy': 0.00139999995008111,
     'factorized_top_k/top_5_categorical_accuracy': 0.011549999937415123,
     'factorized_top_k/top_10_categorical_accuracy': 0.025450000539422035,
     'factorized_top_k/top_50_categorical_accuracy': 0.1335500031709671,
     'factorized_top_k/top_100_categorical_accuracy': 0.24815000593662262,
     'loss': 13961.8505859375,
     'regularization_loss': 0,
     'total_loss': 13961.8505859375}
    

    We can get the 5 movies in the test set for user 42 with a certain timestamp, and sort them based on their scores in a descending order.
    
    test_ratings = {}
    for m in test.take(5):
    #     print(m['movie_title'].numpy())
        _, _, test_ratings[m['movie_title'].numpy()] = \
            model(
                {'user_id':np.array(['42']), 
                 'timestamp':np.array([892839492]), 
                 'movie_title': np.array([m['movie_title'].numpy()])
                }
            )
        
    for m in sorted(test_ratings, key=test_ratings.get, reverse=True):
        print(m)
    
    Output:
    
    b'Chasing Amy (1997)'
    b'Top Gun (1986)'
    b'Twister (1996)'
    b'Event Horizon (1997)'
    b'Batman Forever (1995)'
    

    We can look into the scores of those movies in test_ratings
    
    for r in test_ratings: 
        print(r, test_ratings[r].numpy()[0][0])
    
    Output:
    
    b'Top Gun (1986)' 3.278518
    b'Chasing Amy (1997)' 3.3716574
    b'Batman Forever (1995)' 2.8443925
    b'Twister (1996)' 3.1891446
    b'Event Horizon (1997)' 3.0244293
    

    Finally, let's change the timestamp of user 42 for those movies to check if the predicted scores/ratings change. When we change the timestamp from 892839492 to 879024327, we can see the ratings change accordingly as below: Output:
    
    b'Top Gun (1986)' 3.3720827
    b'Chasing Amy (1997)' 3.408927
    b'Batman Forever (1995)' 3.0349593
    b'Twister (1996)' 3.162705
    b'Event Horizon (1997)' 3.058013
    

    More TFRS tutorials can be found at https://parklize.blogspot.com/p/tensorflow.html

    References