Showing posts with label Tensorflow. Show all posts
Showing posts with label Tensorflow. Show all posts

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.

WARNING:tensorflow:Early stopping conditioned on metric `val_loss` which is not available. Available metrics are: xxx

Error message 

"WARNING:tensorflow:Early stopping conditioned on metric `val_loss` which is not available. Available metrics are: loss,mae,mse,mape"


Cause

This is normally due to the validation dataset is empty, so there is no associated val_loss for the empty validation data.

Preparing windowed dataset for time series forecasting



Windowed dataset is required in many forecasting methods, especially for machine learning-based approaches. As an example, given a time series, e.g., [1,3,5,7,9], and a window size of 2, the corresponding windowed dataset could be as follows, which can be used for training a machine learning model where each element in X represents windowed data while each element in y represents a corresponding label/target/next value to be predicted.

X = [[1,3], [3,5], [5,7]]
y = [[5], [7], [9]]

Importing packages


import tensorflow as tf
import numpy as np

print(tf.__version__)
print(np.__version__)

2.4.1
1.19.2  


For simplicity, here we create a sequence of numbers as our example time series data.


ts = np.arange(1, 100, 2)

array([ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61, 63, 65, 67, 69, 71, 73, 75, 77, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99])


Implementation

The window_dataset() below provides a functionality to produce the windowed dataset for a given time series (ts) and the window_size. We delve into some details of the code in the following.


def window_dataset(ts, window_size=2):
    """ Process the time series into windowed dataset 
    
    :parameter ts: time series data
    
    Return data, targets where data and targets are both list
        each element in data is history in a window, 
        each element in targets is the next value
    """
    data = list()
    targets = list()
    
    dataset = tf.data.Dataset.from_tensor_slices(ts)
    dataset = dataset.window(
        window_size+1, 
        shift=1, 
        drop_remainder=True
    )
    dataset = dataset.flat_map(lambda w: w.batch(window_size+1))
    dataset = dataset.map(lambda w: (w[:-1], w[-1:]))
    
    for (x, y) in dataset.as_numpy_iterator():
        data.append(x)
        targets.append(y)
        
    return data, targets


data, targets = window_dataset(ts)

for x,y in zip(data, targets):
    print(x,y)

[1 3] [5]
[3 5] [7]
[5 7] [9]
[7 9] [11]
[ 9 11] [13]
[11 13] [15]
[13 15] [17]
[15 17] [19]
[17 19] [21]
[19 21] [23]
[21 23] [25]
[23 25] [27]
[25 27] [29]
[27 29] [31]
[29 31] [33]
[31 33] [35]
[33 35] [37]
[35 37] [39]
[37 39] [41]
[39 41] [43]
[41 43] [45]
[43 45] [47]
[45 47] [49]
[47 49] [51]
[49 51] [53]
[51 53] [55]
[53 55] [57]
[55 57] [59]
[57 59] [61]
[59 61] [63]
[61 63] [65]
[63 65] [67]
[65 67] [69]
[67 69] [71]
[69 71] [73]
[71 73] [75]
[73 75] [77]
[75 77] [79]
[77 79] [81]
[79 81] [83]
[81 83] [85]
[83 85] [87]
[85 87] [89]
[87 89] [91]
[89 91] [93]
[91 93] [95]
[93 95] [97]
[95 97] [99]

Given the windowed dataset, we can fit any forecasting model. Here, we simply use a linear regression to fit the windowed dataset for illustration.

from sklearn.linear_model import LinearRegression

model = LinearRegression(fit_intercept=True)
model.fit(data, targets)
model.predict([[97,99]])

array([[101.]])


Details

Now, we move on to some details of the window_dataset() method. First step is creating a Tensorflow dataset. The from_tensor_slices(ts) creates a Dataset whose elements are slices of the given tensors.


dataset = tf.data.Dataset.from_tensor_slices(ts)
for d in dataset:
    print(d)
    break

tf.Tensor(1, shape=(), dtype=int64)


The Tensorflow dataset provides a method - window() - which a dataset of "windows". According to the documentation, each "window" is a dataset that contains a subset of elements of the input dataset. These are finite datasets of size size (or possibly fewer if there are not enough input elements to fill the window and drop_remainder evaluates to False). As the subset is still a dataset, we use list() and as_numpy_iterator() which returns an iterator which converts all elements of the dataset to numpy.


window_size = 2
dataset = dataset.window(
    window_size+1, 
    shift=1, 
    drop_remainder=True
)
for d in dataset:
    # Each d will be sub-dataset of the dataset
    print(list(d.as_numpy_iterator()))
    break

[1, 3, 5]


Here, we grab all data in each sub-dataset and flattens the result.


# Maps .batch across each sub-dataset
dataset = dataset.flat_map(lambda w: w.batch(window_size+1))
for d in dataset:
    print(d)
    break

tf.Tensor([1 3 5], shape=(3,), dtype=int64)


Finally, we split the data part and target one for each element in the dataset, and each pair will be a training example for training a forecasting model.


dataset = dataset.map(lambda w: (w[:-1], w[-1:]))
for (x, y) in dataset.as_numpy_iterator():
    print(x, y)
    break

[1 3] [5]
That's it for preparing windowed dataset for time series forecasting. Although we used Tensorflow to implement the preprocessing step, you can also try to use other ways to implement the same functionality as long as you can derive the same output.

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.

    Tensorflow Probability: module 'collections' has no attribute 'Sequence'

      File "/Users/judau/miniconda/lib/python3.10/site-packages/tensorflow_probability/python/layers/distribution_layer.py", line 171, in _fn

        value_is_seq = isinstance(d.dtype, collections.Sequence)

    AttributeError: Exception encountered when calling layer "multivariate_normal_tri_l" (type MultivariateNormalTriL).

    module 'collections' has no attribute 'Sequence' 

    ################################################

    The issue seems related to the `collections.abc` package which has been available since Python 3.3, and `collections.Mapping` and `collections.Sequence` are gone as of Python 3.10. So if you are using Python 3.10+, it might cause this type of error as discussed in the following thread.

    https://github.com/tensorflow/probability/commit/76ff71ba27a5a035fa6220e6132744ac89a56fdf#

    Move uses of collections.Mapping and collections.Sequence to `col…
    …lections.abc`.
    
    The `collections.abc` package has been available since Python 3.3, and `collections.Mapping` and `collections.Sequence` are gone as of Python 3.10.
    

    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.

    Tensorflow Error - TypeError: bases must be types





    It worked just fine for me. Tensorflow and protobuf versions are incompatible in my case.
    
    pip uninstall protobuf
    pip install protobuf==3.20.1
    
    Source: https://stackoverflow.com/questions/72779449/google-visions-python-client-quickstart-throws-typeerror-bases-must-be-types

    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

    Tensorflow Recommenders - How to rank candidate items?



    In the previous posts, we have discussed (1) how to retrieve candidate items and (2) how to use contextual features for building models. As mentioned in the first post, the Retrieval component shrinks item candidates from O(thousands of millions) to O(thousands), and the Ranking component trims the candidates from O(thousands) to O(hundreds).

    In this post, we look at how to build ranking models. Differing from the retrieval stage, we will keep ratings (explicit feedback) in this time. And as we don’t have efficiency constraints like in the retrieval stage as the ranking model normally will work on retrieved items only from the retrieval stage, we can use a deeper model for ranking.

    Content
    • Load the Movielens 100k dataset
    • Ranking model
    • Movielens model
    • Compile and training
    • Getting ranked list of recommended items


    Load the Movielens 100k dataset


    
    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
    
    Let's load the MovieLens 100k dataset, but this time we will also use the ratings (also called explicit feedback) which is different from the retrieval model in the previous post.
    
    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']
    })
    
    tf.random.set_seed(42)
    shuffled = ratings.shuffle(100_000, 
                               seed=42, 
                               reshuffle_each_iteration=False)
    
    train = shuffled.take(80_000)
    test = shuffled.skip(20_000).take(20_000)
    
    movie_titles = ratings.batch(1_000_000) \
                    .map(lambda x: x['movie_title'])
    user_ids = ratings.batch(1_000_000) \
                    .map(lambda x: x['user_id'])
        
    unique_movie_titles = np.unique(np.concatenate(list(movie_titles)))
    unique_user_ids = np.unique(np.concatenate(list(user_ids)))
    


    Ranking Model

    Here we define the ranking model with deeper neural networks compared to the retrieval model.
    
    class RankingModel(tf.keras.Model):
        
        def __init__(self):
            super().__init__()
            embedding_dimension = 32
            
            # Compute embeddings for users
            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, embedding_dimension)
            ])
            
            # Compute embeddings for movies
            self.movie_embedding = tf.keras.Sequential([
                tf.keras.layers.StringLookup(
                    vocabulary=unique_movie_titles, mask_token=None),
                tf.keras.layers.Embedding(
                    len(unique_movie_titles)+1, embedding_dimension)
            ])
            
            # Rating model for predict ratings
            self.ratings = 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)
            ])
            
            
        def call(self, inputs):
            user_id, movie_titles = inputs
            
            user_embedding = self.user_embedding(user_id)
            movie_embedding = self.movie_embedding(movie_titles)
            
            return self.ratings(
                tf.concat([user_embedding, movie_embedding], axis=1))
    
    We can test the defined and untrained ranking model as it is for getting the prediction score given a user id (42) and a movie (One Flew Over the Cuckoo's Nest (1975)).
    
    RankingModel()((["42"], ["One Flew Over the Cuckoo's Nest (1975)"])).numpy()
    
    Output:
    
    array([[0.03740937]], dtype=float32)
    





    Movielens Model

    Now we can move on to define the MovielensModel using the defined ranking model, defining task and the compute_loss() function. As you might expect, we are using the mean squared error (MSE) as our loss below, and RMSE (Root MSE) as our metrics.
    
    class MovielensModel(tfrs.models.Model):
        
        def __init__(self):
            super().__init__()
            # Setup models in the init method
            self.ranking_model = RankingModel()
            self.task = tfrs.tasks.Ranking(
                loss = tf.keras.losses.MeanSquaredError(),
                metrics = [tf.keras.metrics.RootMeanSquaredError()]
            )
            
        def compute_loss(self, features: Dict[Text, tf.Tensor],
                        training=False) -> tf.Tensor:
            # Implement the compute_loss method
            # taking into the raw features
            # returning the loss
            rating_predictions = self.ranking_model(
                (features['user_id'], features['movie_title']))
            
            # The task computes the loss and the metrics
            return self.task(labels=features['user_rating'], 
                            predictions=rating_predictions)
    


    Compile and Training

    Compile and fit using the training set.
    
    model = MovielensModel()
    model.compile(optimizer=tf.keras.optimizers.Adagrad(learning_rate=0.1))
    
    cached_train = train.shuffle(100_000).batch(8192).cache()
    cached_test = test.batch(4096).cache()
    
    model.fit(cached_train, epochs=3)
    

    Getting ranked list of recommended items

    Finally, we can get a ranked list of recommended items based on predicted scores of items using our trained model. In practice, we will sort candidates only from the retrieval stage.
    
    test_ratings = {}
    for m in test.take(5):
        test_ratings[m['movie_title'].numpy()] = \
            RankingModel()((['42'], [m['movie_title']]))
        
    for m in sorted(test_ratings, key=test_ratings.get, reverse=True):
        print(m)
    
    Output:
    
    b'Man Without a Face, The (1993)'
    b'Maverick (1994)'
    b'Unstrung Heroes (1995)'
    b'Shining, The (1980)'
    b'Free Willy (1993)'
    

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

    References

    Tensorflow Recommenders - How to use contextual features for building models?



    In the previous post, we have looked at Tensorflow Recommeners (TFRS) for (1) preparing dataset, (2) building a retrieval model, (3) training and evaluating the model, and (4) exporting the model, loading again and getting retrieved items using the model.

    In this post, we show how to use contextual features related to users/items [1, 2]. To this end, we consider one contextual features for users, and one for items:
    • User model: in addition to user id embeddings, we use the timestamps of movie ratings which include discretized timestamps and its normalized values. 
    • Movie model: in addition to movie embedding, we use the text of a movie title using its embeddings

    Content: We briefly compare three versions of retrieval models in the following by enabling or disabling those additional optional contextual features:
    • Pure user and movie models without any contextual features
    • User model with the consideration of timestamps
    • Using all contextual features for both user and movie models
     

    Load the dataset

     
    
    from typing import Dict, Text # for typing hint
     
    import os
    import pprint
    import numpy as np
    import tempfile
    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
    

    We can load the Movielens 100k dataset including contextual information such as timestamps and movie titles of ratings. Timestamps are split into 1000 buckets and the indices of a timestamp in those buckets will be used when building our user model.
    
    # 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 
    We have 1664 unique movies and 943 unique users in the dataset. 

    tiemstamp_buckets are looks as below:
    
    [8.74724710e+08 8.74743291e+08 8.74761871e+08 8.74780452e+08
     8.74799032e+08 8.74817613e+08 8.74836193e+08 8.74854774e+08
     8.74873354e+08 8.74891935e+08 8.74910515e+08 8.74929096e+08
     8.74947676e+08 8.74966257e+08 8.74984837e+08 8.75003418e+08
     ...
    



    User model

    As we can see from below, the use_timestamps parameter can control whether to consider the contextual feature (timestamps) or not.
    
    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

    Similar to the user model, we use the use_title_text parameter to control whether we would like to consider the contextual feature (title text) or not.
    
    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

    Finally, we can define the MovielensModel by using the predefined user model and movie model and compute_loss function.
    
    
    class MovielensModel(tfrs.models.Model):
        
        def __init__(self, use_timestamps, use_title_text):
            super().__init__()
            self.query_model = tf.keras.Sequential([
                UserModel(use_timestamps),
                tf.keras.layers.Dense(32)
            ])
            self.candidate_model = tf.keras.Sequential([
                MovieModel(use_title_text),
                tf.keras.layers.Dense(32)
            ])
            # Define task
            self.task = tfrs.tasks.Retrieval(
                metrics=tfrs.metrics.FactorizedTopK(
                    candidates=movies.batch(128) \
                        .map(self.candidate_model),
                )
            )
            
        # Define compute loss
        def compute_loss(self, features, training=False):
            query_embeddings = self.query_model({
                'user_id': features['user_id'],
                'timestamp': features['timestamp']
            })
            movie_embeddings = self.candidate_model(
                features['movie_title'])
            return self.task(query_embeddings, movie_embeddings)
            
    






    Experiment

    Now, we are ready to run some experiments to compare the three models mentioned at the beginning of this post:
    • Pure user and movie models without any contextual features
    • User model with the consideration of timestamps
    • Using all contextual features for both user and movie models
    
    # -------------------------------
    # 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()
    


    Baseline1: user model without timestamp features, movie model without title text

    
    
    # Baseline: user model without timestamp features, movie model without title text
    model = MovielensModel(use_timestamps=False, use_title_text=False)
    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.27
    Top-100 accuracy (test): 0.21
    

    Baseline2: user model with timestamp features, movie model without title text

    
    tf.keras.backend.clear_session()
    
    model = MovielensModel(use_timestamps=True, use_title_text=False)
    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.24
    

    Finally, we use both timestamps for user model and the text of movie title for movie model.

    
    tf.keras.backend.clear_session()
    
    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.35
    Top-100 accuracy (test): 0.25
    

    For the sake of not running too long, we set epochs as 3 but you can definitely try it out with larger values. Under the current settings, it is interesting to see the performance increases with enabling contextual features one by one.

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

    References