<jupyter_start><jupyter_text>Recommendation Model with Approximate Item MatchingThis notebook shows how to train a simple Neural Collaborative Filtering model for recommeding movies to users. We also show how learnt movie embeddings are stored in an appoximate similarity matching index, using Spotify's [Annoy library](https://github.com/spotify/annoy), so that we can quickly find and recommend the most relevant movies to a given customer. We show how this index to search for similar movies.In essense, this tutorial works as follows:1. Download the movielens dataset.2. Train a simple Neural Collaborative Model using TensorFlow custom estimator.3. Extract the learnt movie embeddings.4. Build an approximate similarity matching index for the movie embeddings.5. Export the trained model, which receives a user Id, and output the user embedding.The recommendation is served as follows:1. Receives a user Id2. Get the user embedding from the exported model3. Find the similar movie embeddings to the user embedding in the index4. Return the movie Ids of these embeddings to recommend  Setup<jupyter_code>!pip install annoy
import math
import os
import pandas as pd
import numpy as np
from datetime import datetime

import tensorflow as tf
from tensorflow import data

print "TensorFlow : {}".format(tf.__version__)

SEED = 19831060<jupyter_output>TensorFlow : 1.12.0<jupyter_text>1. Download Data<jupyter_code>DATA_DIR='data'
! wget http://files.grouplens.org/datasets/movielens/ml-latest-small.zip -P data/
! unzip data/ml-latest-small.zip -d data/
TRAIN_DATA_FILE = os.path.join(DATA_DIR, 'ml-latest-small/ratings.csv')
ratings_data = pd.read_csv(TRAIN_DATA_FILE)
ratings_data.describe()
ratings_data.head()
movies_data = pd.read_csv(os.path.join(DATA_DIR, 'ml-latest-small/movies.csv'))
movies_data.head()<jupyter_output><empty_output><jupyter_text>2. Build the TensorFlow Model  2.1 Define Metadata<jupyter_code>HEADER = ['userId', 'movieId', 'rating', 'timestamp']
HEADER_DEFAULTS = [0, 0, 0.0, 0]
TARGET_NAME = 'rating'
num_users = ratings_data.userId.max()
num_movies = movies_data.movieId.max()<jupyter_output><empty_output><jupyter_text>2.2 Define Data Input Function<jupyter_code>def make_input_fn(file_pattern, batch_size, num_epochs, 
                  mode=tf.estimator.ModeKeys.EVAL):
    
    def _input_fn():
        dataset = tf.data.experimental.make_csv_dataset(
            file_pattern=file_pattern,
            batch_size=batch_size,
            column_names=HEADER,
            column_defaults=HEADER_DEFAULTS,
            label_name=TARGET_NAME,
            field_delim=',',
            use_quote_delim=True,
            header=True,
            num_epochs=num_epochs,
            shuffle= (mode==tf.estimator.ModeKeys.TRAIN)
        )
        return dataset
    
    return _input_fn<jupyter_output><empty_output><jupyter_text>2.3 Create Feature Columns<jupyter_code>def create_feature_columns(embedding_size):
    
    feature_columns = []
    
    feature_columns.append(
        tf.feature_column.embedding_column(
            tf.feature_column.categorical_column_with_identity(
                'userId', num_buckets=num_users + 1), 
            embedding_size
        )
    )
    
    feature_columns.append(
        tf.feature_column.embedding_column(
            tf.feature_column.categorical_column_with_identity(
                'movieId', num_buckets=num_movies + 1),
            embedding_size
        )
    )
        
    return feature_columns<jupyter_output><empty_output><jupyter_text>2.4 Define Model Function<jupyter_code>def model_fn(features, labels, mode, params):
    
    feature_columns = create_feature_columns(params.embedding_size)
    user_layer = tf.feature_column.input_layer(
        features={'userId': features['userId']}, feature_columns=[feature_columns[0]])
    
    if mode != tf.estimator.ModeKeys.PREDICT:
        movie_layer = tf.feature_column.input_layer(
            features={'movieId': features['movieId']}, feature_columns=[feature_columns[1]])
        
        dot_product = tf.keras.layers.Dot(axes=1)([user_layer, movie_layer])
        logits = tf.clip_by_value(clip_value_min=0, clip_value_max=5, t=dot_product)

    predictions = None
    export_outputs = None
    loss = None
    train_op = None

    if mode == tf.estimator.ModeKeys.PREDICT:
        predictions = {'user_embedding': user_layer}
        export_outputs = {'predictions': tf.estimator.export.PredictOutput(predictions)}
    else:
        loss = tf.losses.mean_squared_error(labels, tf.squeeze(logits))
        train_op=tf.train.FtrlOptimizer(params.learning_rate).minimize(
            loss=loss, global_step=tf.train.get_global_step())

    loss = tf.losses.mean_squared_error(labels, tf.squeeze(logits))
    return tf.estimator.EstimatorSpec(
        mode=mode,
        loss=loss,
        train_op=train_op
    )<jupyter_output><empty_output><jupyter_text>2.5 Create Estimator<jupyter_code>def create_estimator(params, run_config):
    
    estimator = tf.estimator.Estimator(
        model_fn,
        params=params,
        config=run_config
    )
    
    return estimator<jupyter_output><empty_output><jupyter_text>2.6 Define Experiment<jupyter_code>def train_and_evaluate_experiment(params, run_config):
    
    # TrainSpec ####################################
    train_input_fn = make_input_fn(
        TRAIN_DATA_FILE,
        batch_size=params.batch_size,
        num_epochs=None,
        mode=tf.estimator.ModeKeys.TRAIN
    )
    
    train_spec = tf.estimator.TrainSpec(
        input_fn = train_input_fn,
        max_steps=params.traning_steps
    )
    ###############################################    
    
    # EvalSpec ####################################
    eval_input_fn = make_input_fn(
        TRAIN_DATA_FILE,
        num_epochs=1,
        batch_size=params.batch_size,
    )

    eval_spec = tf.estimator.EvalSpec(
        name=datetime.utcnow().strftime("%H%M%S"),
        input_fn = eval_input_fn,
        steps=None,
        start_delay_secs=0,
        throttle_secs=params.eval_throttle_secs
    )
    ###############################################

    tf.logging.set_verbosity(tf.logging.INFO)
    
    if tf.gfile.Exists(run_config.model_dir):
        print("Removing previous artefacts...")
        tf.gfile.DeleteRecursively(run_config.model_dir)
            
    print ''
    estimator = create_estimator(params, run_config)
    print ''
    
    time_start = datetime.utcnow() 
    print("Experiment started at {}".format(time_start.strftime("%H:%M:%S")))
    print(".......................................") 

    tf.estimator.train_and_evaluate(
        estimator=estimator,
        train_spec=train_spec, 
        eval_spec=eval_spec
    )

    time_end = datetime.utcnow() 
    print(".......................................")
    print("Experiment finished at {}".format(time_end.strftime("%H:%M:%S")))
    print("")
    time_elapsed = time_end - time_start
    print("Experiment elapsed time: {} seconds".format(time_elapsed.total_seconds()))
    
    return estimator<jupyter_output><empty_output><jupyter_text>2.7 Run Experiment with Parameters<jupyter_code>MODELS_LOCATION = 'models/movieles'
MODEL_NAME = 'recommender_01'
model_dir = os.path.join(MODELS_LOCATION, MODEL_NAME)

params  = tf.contrib.training.HParams(
    batch_size=265,
    traning_steps=1000,
    learning_rate=0.1,
    embedding_size=16,
    eval_throttle_secs=0,
)

run_config = tf.estimator.RunConfig(
    tf_random_seed=SEED,
    save_checkpoints_steps=10000,
    keep_checkpoint_max=3,
    model_dir=model_dir,
)

estimator = train_and_evaluate_experiment(params, run_config)<jupyter_output>INFO:tensorflow:Using config: {'_save_checkpoints_secs': None, '_session_config': allow_soft_placement: true
graph_options {
  rewrite_options {
    meta_optimizer_iterations: ONE
  }
}
, '_keep_checkpoint_max': 3, '_task_type': 'worker', '_train_distribute': None, '_is_chief': True, '_cluster_spec': <tensorflow.python.training.server_lib.ClusterSpec object at 0x121eaf850>, '_model_dir': 'models/movieles/recommender_01', '_protocol': None, '_save_checkpoints_steps': 10000, '_keep_checkpoint_every_n_hours': 10000, '_service': None, '_num_ps_replicas': 0, '_tf_random_seed': 19831060, '_save_summary_steps': 100, '_device_fn': None, '_experimental_distribute': None, '_num_worker_replicas': 1, '_task_id': 0, '_log_step_count_steps': 100, '_evaluation_master': '', '_eval_distribute': None, '_global_id_in_cluster': 0, '_master': ''}

Experiment started at 19:14:20
.......................................
INFO:tensorflow:Not using Distribute Coordinator.
INFO:tensorflow:Running training and ev[...]<jupyter_text>3. Extract Movie Embeddings<jupyter_code>def find_embedding_tensor():
    with tf.Session() as sess:
        saver = tf.train.import_meta_graph(os.path.join(model_dir, 'model.ckpt-100000.meta'))
        saver.restore(sess, os.path.join(model_dir, 'model.ckpt-100000'))
        graph = tf.get_default_graph()
        trainable_tensors = map(str, graph.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES))
        for tensor in set(trainable_tensors):
            print tensor
            
find_embedding_tensor()
def extract_embeddings():
    
    with tf.Session() as sess:
        saver = tf.train.import_meta_graph(os.path.join(model_dir, 'model.ckpt-100000.meta'))
        saver.restore(sess, os.path.join(model_dir, 'model.ckpt-100000'))
        graph = tf.get_default_graph()
        weights_tensor = graph.get_tensor_by_name('input_layer_1/movieId_embedding/embedding_weights:0')
        weights = np.array(sess.run(weights_tensor))
    
    embeddings = {}
    for i in range(weights.shape[0]):
        embeddings[i] = weights[i]
    
    return embeddings

embeddings = extract_embeddings()<jupyter_output><empty_output><jupyter_text>4. Build Annoy Index<jupyter_code>from annoy import AnnoyIndex

def build_embeddings_index(num_trees):
    total_items = 0
    annoy_index = AnnoyIndex(params.embedding_size, metric='angular')
    for item_id in embeddings.keys():
        annoy_index.add_item(item_id, embeddings[item_id])
        total_items += 1
    print "{} items where added to the index".format(total_items)
    annoy_index.build(n_trees=num_trees)
    print "Index is built"
    return annoy_index

index = build_embeddings_index(100)
frequent_movie_ids = list(ratings_data.movieId.value_counts().index[:15])
movies_data[movies_data['movieId'].isin(frequent_movie_ids)]
def get_similar_movies(movie_id, num_matches=5):
    similar_movie_ids = index.get_nns_by_item(
        movie_id, num_matches, search_k=-1, include_distances=False)
    similar_movies = movies_data[movies_data['movieId'].isin(similar_movie_ids)].title
    return similar_movies
for movie_id in frequent_movie_ids:
    movie_title = movies_data[movies_data['movieId'] == movie_id].title.values[0]
    print "Movie: {}".format(movie_title)
    similar_movies = get_similar_movies(movie_id)
    print "Similar Movies:"
    print similar_movies
    print "--------------------------------------"<jupyter_output>Movie: Forrest Gump (1994)
Similar Movies:
55                  Mr. Holland's Opus (1995)
314                       Forrest Gump (1994)
1956    Open Your Eyes (Abre los ojos) (1997)
2372                   Green Mile, The (1999)
4867                    50 First Dates (2004)
Name: title, dtype: object
--------------------------------------
Movie: Shawshank Redemption, The (1994)
Similar Movies:
277          Shawshank Redemption, The (1994)
955                          Duck Soup (1933)
1956    Open Your Eyes (Abre los ojos) (1997)
2462              Boondock Saints, The (2000)
7466                King's Speech, The (2010)
Name: title, dtype: object
--------------------------------------
Movie: Pulp Fiction (1994)
Similar Movies:
257                                   Pulp Fiction (1994)
2226                                    Fight Club (1999)
2250                      Who Framed Roger Rabbit? (1988)
6310    Borat: Cultural Learnings of America for Make ...
Name: title, dtype: object
-------[...]<jupyter_text>5. Export the ModelThis needed to receive a userId and produce the embedding for the user.<jupyter_code>def make_serving_input_receiver_fn():
    return tf.estimator.export.build_raw_serving_input_receiver_fn(
        {'userId': tf.placeholder(shape=[None], dtype=tf.int32)}
    )

export_dir = os.path.join(model_dir, 'export')

if tf.gfile.Exists(export_dir):
    tf.gfile.DeleteRecursively(export_dir)
        
estimator.export_savedmodel(
    export_dir_base=export_dir,
    serving_input_receiver_fn=make_serving_input_receiver_fn()
)
import os

export_dir = os.path.join(model_dir, "export")
saved_model_dir = os.path.join(
    export_dir, [f for f in os.listdir(export_dir) if f.isdigit()][0])

print(saved_model_dir)

predictor_fn = tf.contrib.predictor.from_saved_model(
    export_dir = saved_model_dir,
)

output = predictor_fn({'userId': [1]})
print(output)<jupyter_output>models/movieles/recommender_01/export/1550083878
INFO:tensorflow:Restoring parameters from models/movieles/recommender_01/export/1550083878/variables/variables
{u'user_embedding': array([[-0.04079459, -0.06252338,  0.01964831, -0.03159623,  0.01765972,
         0.00015648,  0.0686218 ,  0.01872032,  0.04238764,  0.03700782,
         0.00166043,  0.00917281,  0.01879879, -0.01652114, -0.02870869,
         0.00668285]], dtype=float32)}<jupyter_text>Serve Movie Recommendations to a User<jupyter_code>def recommend_new_movies(userId, num_recommendations=5):
    watched_movie_ids = list(ratings_data[ratings_data['userId']==userId]['movieId'])
    
    user_emebding = predictor_fn({'userId': [userId]})['user_embedding'][0]
    similar_movie_ids = index.get_nns_by_vector(
        user_emebding, num_recommendations + len(watched_movie_ids), search_k=-1, include_distances=False)
    
    recommended_movie_ids = set(similar_movie_ids) - set(watched_movie_ids)
    similar_movies = movies_data[movies_data['movieId'].isin(recommended_movie_ids)].title
    return similar_movies
frequent_user_ids = list((ratings_data.userId.value_counts().index[-350:]))[:5] 
print recommend_movies(418)<jupyter_output>3857            Dangerous Lives of Altar Boys, The (2002)
8140    Wolf Children (Okami kodomo no ame to yuki) (2...
8429                                          Chef (2014)
9610                                     Cage Dive (2017)
9729                                         Bunny (1998)
Name: title, dtype: object
{'fraction_non_alphanumeric': 0.1266993306190049, 'fraction_numerical': 0.03388309985508246, 'mean_word_length': 3.4700801974090068, 'pattern_counts': {'":': 0, '<': 49, '<?xml version=': 0, '>': 49, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '421997', 'license': 'Apache-2.0', 'n_tokens_mistral': 5071, 'n_tokens_neox': 4658, 'n_words': 885, 'path': 'Experimental/Movielens Recommendation.ipynb', 'repo_name': 'ChunyuPCY/tf-estimator-tutorials', 'size': '14491'}