Showing posts with label Recommender system. Show all posts
Showing posts with label Recommender system. Show all posts

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

Tensorflow Recommenders - How to retrieve candidate items?



Tensorflow Recommeners (TFRS), which is released in 2020 as part of Tensorflow ecosystem, provides entire stack for recommender systems including
  • Retrieval
  • Ranking
  • Post-ranking
where the Retrieval component shrinks item candidates from O(thousands of millions) to O(thousands), the Ranking component trims the candidates from O(thousands) to O(hundreds), and finally, the Post-ranking component narrows down the candidates from O(hundreds) to O(dozens).

In this post, we focus on the first part of TFRS tutorial, in which we would like to use the well-established MovieLens 100k dataset containing 100k ratings of movies from users. This tutorial requires TFRS installed (TFRS requires Tensorflow 2.x). You can install TFRS easily with:

pip install tensorflow-recommenders

Content

  • Prepare dataset
  • Build model
  • Train and evaluate the model
  • Export the model, load again and get retrieved items using 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 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
Let's load the MovieLens dataset from tensorflow_dataset.

# ----------------------------------------
# Prepare Movielens100k data
# ----------------------------------------
# Ratings data
ratings = tfds.load('movielens/100k-ratings', split='train')
# Features of all the available movies
movies = tfds.load('movielens/100k-movies', split='train')
We can print out some of the samples from ratings and movies.

# ----------------------------------------
# Explore dataset
# ----------------------------------------
n = 1
print('\nRatings:')
for r in ratings.take(n).as_numpy_iterator(): # as_numpy_iterator() Returns an iterator which converts all elements of the dataset to numpy.
    pprint.pprint(r)
    
print('\nMovies:')
for m in movies.take(n).as_numpy_iterator():
    pprint.pprint(m)
Output:

Ratings:
{'bucketized_user_age': 45.0,
 'movie_genres': array([7], dtype=int64),
 'movie_id': b'357',
 'movie_title': b"One Flew Over the Cuckoo's Nest (1975)",
 'raw_user_age': 46.0,
 'timestamp': 879024327,
 'user_gender': True,
 'user_id': b'138',
 'user_occupation_label': 4,
 'user_occupation_text': b'doctor',
 'user_rating': 4.0,
 'user_zip_code': b'53211'}

Movies:
{'movie_genres': array([4], dtype=int64),
 'movie_id': b'1681',
 'movie_title': b'You So Crazy (1994)'}
Now, we move on and prepare the dataset, which include the following steps:
  • Shuffle the dataset, and split train/test with 80000 and 20000 examples each.
  • Build up the user id and movie vocabularies for learning user/item embeddings later on
 

# ----------------------------------------
# Shuffle and prepare ML dataset 
# ----------------------------------------
tf.random.set_seed(42)

# Get user, item information only
ratings = ratings.map(lambda x: {
    'movie_title': x['movie_title'],
    'user_id': x['user_id']
})
shuffled = ratings.shuffle(100_000, seed=42, reshuffle_each_iteration=False)

train = shuffled.take(80_000)
test = shuffled.skip(80_000).take(20_000)

movie_titles = movies.map(lambda x: x['movie_title'])
user_ids = ratings.map(lambda x: x['user_id'])

# Convert to int ids
user_ids_vocabulary = tf.keras.layers.StringLookup(mask_token=None)
user_ids_vocabulary.adapt(ratings.map(lambda x: x["user_id"]))

movie_titles_vocabulary = tf.keras.layers.StringLookup(mask_token=None)
movie_titles_vocabulary.adapt(movie_titles)


Build model

To build a TFRS model, we need to prepare three things:
  • user model
  • item model
  • task (which is retrieval in this tutorial as one might expect)
user and item models are Tensorflow Keras models. The tfrs.metrics.FactorizedTopK computes metrics for across top K candidates surfaced by a retrieval model the ks parameter refers ot a sequence of values of k at which to perform retrieval evaluation..

# Define user and movie models.
user_model = tf.keras.Sequential([
    user_ids_vocabulary,
    tf.keras.layers.Embedding(user_ids_vocabulary.vocabulary_size(), 64)
])

item_model = tf.keras.Sequential([
    movie_titles_vocabulary,
    tf.keras.layers.Embedding(movie_titles_vocabulary.vocabulary_size(), 64)
])

# Define metrics
metrics = tfrs.metrics.FactorizedTopK(
    candidates=movie_titles.batch(128).map(item_model),
    ks=[5, 10, 20])

# Define task
# Will compile and calculate loss
task = tfrs.tasks.Retrieval(metrics=metrics)

# Building model all together
class MovielensModel(tfrs.Model):
    def __init__(self, user_model:tf.keras.Model, 
                 item_model: tf.keras.Model,
                 task = tf.keras.layers.Layer):
        super().__init__()
        self.item_model = item_model
        self.user_model = user_model
        self.task = task
        
    def compute_loss(self, features: Dict[Text, tf.Tensor], 
                     training=False) -> tf.Tensor:
        # Define how the loss is computed.

        user_embeddings = self.user_model(features["user_id"])
        item_embeddings = self.item_model(features["movie_title"])

        return self.task(user_embeddings, item_embeddings)





Train and evaluate the model

Given the defined model, we can compile and fit the model with our training dataset.

# Compile and fit
model = MovielensModel(user_model, item_model, task)
model.compile(optimizer=tf.keras.optimizers
              .Adagrad(learning_rate=0.1))

cached_train = train.shuffle(100_000).batch(8092).cache()
cached_test = test.batch(4096).cache()

model.fit(cached_train, epochs=3)
Output:

Epoch 1/3
10/10 [==============================] - 10s 695ms/step - factorized_top_k/top_5_categorical_accuracy: 0.0081 - factorized_top_k/top_10_categorical_accuracy: 0.0190 - factorized_top_k/top_20_categorical_accuracy: 0.0401 - loss: 70403.1754 - regularization_loss: 0.0000e+00 - total_loss: 70403.1754
Epoch 2/3
10/10 [==============================] - 6s 613ms/step - factorized_top_k/top_5_categorical_accuracy: 0.0183 - factorized_top_k/top_10_categorical_accuracy: 0.0370 - factorized_top_k/top_20_categorical_accuracy: 0.0740 - loss: 67763.8679 - regularization_loss: 0.0000e+00 - total_loss: 67763.8679
Epoch 3/3
10/10 [==============================] - 7s 664ms/step - factorized_top_k/top_5_categorical_accuracy: 0.0241 - factorized_top_k/top_10_categorical_accuracy: 0.0492 - factorized_top_k/top_20_categorical_accuracy: 0.0956 - loss: 66349.8991 - regularization_loss: 0.0000e+00 - total_loss: 66349.8991

Next, we can evaluate how well the trained model performs on our test set. return_dict parameter returns the results in a dictionary.

# Evaluation
model.evaluate(cached_test, return_dict=True)
Ouput:

5/5 [==============================] - 2s 185ms/step - factorized_top_k/top_5_categorical_accuracy: 0.0052 - factorized_top_k/top_10_categorical_accuracy: 0.0135 - factorized_top_k/top_20_categorical_accuracy: 0.0342 - loss: 31226.3392 - regularization_loss: 0.0000e+00 - total_loss: 31226.3392
{'factorized_top_k/top_5_categorical_accuracy': 0.005249999929219484,
 'factorized_top_k/top_10_categorical_accuracy': 0.013500000350177288,
 'factorized_top_k/top_20_categorical_accuracy': 0.03424999862909317,
 'loss': 28381.1328125,
 'regularization_loss': 0,
 'total_loss': 28381.1328125}
With our fitted model, we can use it for predictions or retrieving top-k items (movies) for a target user.

# ----------------------------------------
# Use trained model for predictions
# ----------------------------------------
# Use brute-force search to set up retrieval using the trained representations.
# BruteForce here means we do an exhaustive search on the neighbors of
# an embedding vector
index = tfrs.layers.factorized_top_k.BruteForce(model.user_model)
index.index_from_dataset(
    movie_titles.batch(100).map(lambda title: (title, model.item_model(title))))

# Get some recommendations for user id 42
_, titles = index(np.array(["42"]))
print(f"Top 3 recommendations for user 42: {titles[0, :3]}")
Output:

Top 3 recommendations for user 42: [b'Rudy (1993)' b'Father of the Bride Part II (1995)'
 b'Bridges of Madison County, The (1995)']


Export the model, load again and get retrieved items using the model

In practice, we might need to save a well-trained model and serve to the system. We can save the trained model above to the disk and reload it for inference - retrieving top-k items for a user. This should give us exactly the same results as above as one might expect.
# --------------------------------------
# Export the query model
# --------------------------------------
with tempfile.TemporaryDirectory() as tmp:
    path = os.path.join(tmp, "model")
    
    # Save the index
    index.save(path)
    
    # Load it back
    # Can also be done in TF Serving
    loaded = tf.keras.models.load_model(path)
    
    # Pass a user id, get recommendations
    scores, titles = loaded(["42"])
    
    print(f"Recommendations: {titles[0][:3]}")


In the next post, we look at how to utilize contextual features of users/items in the retrieval model.

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

References
[1]. https://github.com/tensorflow/recommenders
[2]. https://grouplens.org/datasets/movielens/

Recommender Systems: Online Business Metrics

Image by Freepik

 

Content

  • What is CTR?
  • What is CVR?
  • What is CPM and eCPM?
  • What is RPM?
  • What is GMV?


CTR (Click Through Rate)

CTR indicates the ratio showing how often people who see your ad or free product listing end up clicking it. Click-through rate (CTR) can be used to gauge how well your keywords and ads, and free listings, are performing.

CTR is the number of clicks that your ad receives divided by the number of times your ad is shown: clicks ÷ impressions = CTR. For example, if you had 5 clicks and 100 impressions, then your CTR would be 5%.

CTP (Click Through Probability)

CTP sometimes used as CTR measured as below:
# of unique visitors who click/# of unique visitors to page


CVR (Conversion Rate)

CVR, or conversion rate, in in-app advertising is the percentage of users who saw an app-install ad, clicked on it, and converted through some pre-specified action. CVR tells app advertisers how many users their ad converted.

Different from the CTR task, the samples available for this task is sparse and usually around 1% of the samples in the CTR task.

CPM (Cost Per Thousand/Mile) and eCPM

Cost per thousand (CPM), also called cost per mille, is a marketing term used to denote the price of 1,000 advertisement impressions on one web page. If a website publisher charges \$2.00 CPM, that means an advertiser must pay \$2.00 for every 1,000 impressions of its ad.

On the other hand, eCPM is concerned with the revenue generated by a thousand impressions. In marketing, CPM is crucial since it helps advertisers determine which platforms are viable for publishing ads. However, the effective cost per thousand ad impressions indicates the publisher’s ad revenue.

RPM (Revenue Per Mile)

Revenue per mille (RPM) is the estimated earnings that accrue for every 1000 impressions received (in Latin, mille means thousand), a commonly used measurement in radio, television, newspaper, magazine, out-of-home, and online advertising.

MIT-Harvard CINCS / Hamilton Institute Seminar: Inclusive Search and Recommendations from Pinterest by Dr Nadia Fawaz

From https://medium.com/pinterest-engineering/powering-inclusive-search-recommendations-with-our-new-visual-skin-tone-model-1d3ba6eeffc7


The topic of this week's MIT-Harvard CINCS  / Hamilton Institute Seminar was about "Inclusive Search and Recommendations" from Dr Nadia Fawaz, Pinterest.


Nadia Fawaz is a research scientiest and tech lead at Pinterest, and she gave an interesting talk on inclusive search and recommendations with interesting examples of this line of efforts at Pinterest when they are building their ML-based search and recommendation systems.


Why this problem is important? It has been clear that in addition to the benefits of ML systems for a wide range of domains, there are also some critical problems have been noticed such as our learned language models could be biased (e.g., "Man is to Doctor as Woman is to Nurse"). This is mainly due to the training data we collected and used to train a ML system is biased. Without considering those biases, ML loop will enhance the bias instead of eliminating it. Nadia Fawaz mentioned during her talk that bias mainly comes from demographic features such as age, skintone, gender, etc., and the talk is focused on Pinterest's efforts to build inclusive search and recommendations with skintone as an example.


Example of Pinterest inclusive AI solution for skintone [medium article]. Motivated by a top request from Pinners where they want to feel represented in the product, they built the first version of skin tone ranges, an inclusive search feature, in 2018. This aims to provide more inclusive inspirations to be recommended in search as well as allow Pinners to choose ranges for recommendations/search results. Some important aspects in terms of building inclusive service are such as:

  • data balanced across a wide range of groups (e.g., a wide range of skin tones)
  • error analysis should be tailed down to each group (so that the system does not perform well on only some specific groups while performing poor on other groups)
  • improve fairness and reduce potential bias in other ML models (e.g., incorporating fairness into objective functions)
  • also as one might expect, to achieve the goal of inclusive ML services, multidisciplinary efforts and a lot of labeling works required from domain experts.

Interestingly, at the time of writing this post, there is a comprehensive survey on "Fairness on Ranking" available on Arxiv which is highly relevant to the same topic discussed in this post.


RecSys Related Libraries List

fastFM (Factorization Machines, Python)
lodreclib (LODRecSys, SPrank etc., Java)

To co-curate the list of recommender system libraries, I created a github repository which contains the list of libraries. Please feel free to send requests to add/update the information.

EKAW2016 Travel Report

From 19-24, November, I attended 20th International Conference on Knowledge Engineering and Knowledge Management at Bologna, Italy. It's a biannual conference on Knowledge Engineering along with the K-CAP conference.

There were around 150 participants from worldwide. Regarding submissions, there were 226 abstracts which resulted in 171 final submissions in total. 539 reviews were submitted for those papers and 42 out of 142 research papers have been accepted. Based on further quality assessment, the organizers also divided 42 papers into long presentations (17.3%) and short presentations for presentations during the conference.

Keynotes:

The first keynote was given by Chris Welty from Google research. He talked about how current AI systems are losing information with one label ground truth for training themselves (e.g, a song might be in different genres or not in the options you provided for getting ground truth data with a survey). He pointed out current simplified world for AI, which consists of black and white, while the reality is much complex. To achieve better ground truth labeling, he also introduced solutions such as using the wise crowd with diversity-enabled labeling for training AI systems.

The second keynote was given by Francesca Rossi from IBM research. She talked about AI has the capabilities to make sense of the huge volume of data (text, images, videos, etc.) that surrounds us in our everyday private and professional life, and to transform it into knowledge to be exploited to make better and more informed decisions that could help solving global societal problems such as those in healthcare, transportation, and climate. To achieve these goals, and in order to fully exploit the potential of AI, we need to build intelligent machines that behave ethically and create symbiotic partnerships with humans. So rather than considering/making AI for Decision Making Systems, we need to consider/make it as Decision Support Systems.

The conference sessions are very diverse, from data management to NLP as well as Entity Recognition, Crowdsourcing, ontology related topics etc.

My presentation:

I presented a User Modeling work considering different dimensions studied in the literature for investigating their synergetic effect on User Modeling.


UMAP2016EA


About

This post provides supplemental material and information about the poster "Analyzing MOOC Entries of Professionals on LinkedIn for User Modeling and Personalized MOOC Recommendations: a first look". Available online: 


Poster:



Dataset 


namenumber of recordsdescription
users.sql56685668 learner profiles from LinkedIn who have been taken any Coursera MOOCs
coruseRecordsV1.sql15744course records extracted from user profiles
eduExperience.sql11085educational experience of learners
workExperience.sql32801work experience of learners
skills.sql159291skills of learners



Descriptive statistics: the dataset is about analyzed MOOC learner profiles from LinkedIn, which consists of 15,744 MOOC entries from 5,668 professionals. Each professional took 3 courses on average with the majority of learners (87%) having less than or equal to 5 MOOCs. Interestingly, the learner with the largest number of MOOCs had 114 of them. The distribution of genders and degrees of learners is as below:



If we assume that course entries in LinkedIn are courses that have been completed by users, the distribution of degrees are similar to the study [1] which provides the distribution of learners who completed their course.

Verified certifications: Instead of just taking MOOCs on Coursera and getting statements of accomplishment, learners can also purchase verified certifications for some courses that meet certain criteria. A verified certification provides proof that learners have completed their online courses. In such cases, varied certifications can also be added to LinkedIn parallels with their varied serial numbers. We found that around 26% of certifications in our collected profiles are verified while 74% of the certifications are unverified. 

Course tracks. We found that course tracks can be identified by exploring learning activities of users in the OSN. Formally, we can define a course track as a set of courses that were taken together more than n times where n is a threshold. The course relationships can be represented by weighted undirected networks like in the figure below. 



Nodes denote courses and the ties among courses denote the frequency of two courses taken together. In this context, a course track is a clique (or complete graph that has an edge joining each pair of nodes) within the course relationships network, with the weight of each tie in the clique is higher than the threshold n. Course tracks can be constructed based on the cliques within the course relationships network. As one might expect, the higher of the value n, the stronger the relationships a course track must hold with less number of cliques meeting the criteria. Indeed, 60 maximal cliques (a clique in maximal if it cannot be extended to a larger clique) can be found with a threshold of 10 while 16 maximal cliques can be found with a threshold of 20. 

We evaluated these tracks and found that two of the course tracks provided by Coursera can be identified in those cliques through this approach. A course track, called a specialization in Coursera, is a targeted sequence of courses from an institution taken together to earn a specialization certificate. The first course track from Coursera is a specialization of "Data Science" which consists of 9 courses from Johns Hopkins University, and the second one is a specialization of "Business Foundations" provided by the University of Pennsylvania. In practice, these ground truth course tracks can also be used for identifying the threshold n, which is the highest value that does not break the ground truth course tracks. In our case, 27 maximal cliques can be found including the two golden truth course tracks with the value of 13 for the threshold. Interestingly, when we look at the maximal clique that contains the "Data Science" course track (Figure 2), we found that "Machine Learning", "Introduction to Data Science" and "Computing for Data Analysis" are also being taken frequently with 9 courses in the "Data Science" course track in practice. This indicates that new course tracks can be constructed on top of existing tracks by exploring learning activities of users from the OSN.

[1]. T. Balch. MOOC student demographics. Retrieved Apr, 28:2013, 2013.