{"id":136,"date":"2020-08-30T10:34:36","date_gmt":"2020-08-30T10:34:36","guid":{"rendered":"https:\/\/machine-learning.webcloning.com\/2020\/08\/30\/building-a-customized-recommender-system-in-amazon-sagemaker\/"},"modified":"2020-08-30T10:34:36","modified_gmt":"2020-08-30T10:34:36","slug":"building-a-customized-recommender-system-in-amazon-sagemaker","status":"publish","type":"post","link":"https:\/\/salarydistribution.com\/machine-learning\/2020\/08\/30\/building-a-customized-recommender-system-in-amazon-sagemaker\/","title":{"rendered":"Building a customized recommender system in Amazon SageMaker"},"content":{"rendered":"<div id=\"\">\n<p><a href=\"https:\/\/en.wikipedia.org\/wiki\/Recommender_system\" target=\"_blank\" rel=\"noopener noreferrer\">Recommender systems<\/a> help you tailor customer experiences on online platforms. <a href=\"https:\/\/aws.amazon.com\/personalize\/\" target=\"_blank\" rel=\"noopener noreferrer\">Amazon Personalize<\/a> is an artificial intelligence and machine learning service that specializes in developing recommender system solutions. It automatically examines the data, performs feature and algorithm selection, optimizes the model based on your data, and deploys and hosts the model for real-time recommendation inference. However, if you need to access trained models\u2019 weights, you may need to build your recommender system from scratch. In this post, I show you how to train and deploy a customized recommender system in TensorFlow 2.0, using a <a href=\"https:\/\/arxiv.org\/abs\/1708.05031\" target=\"_blank\" rel=\"noopener noreferrer\">Neural Collaborative Filtering<\/a> (NCF) (He et al., 2017) model on <a href=\"https:\/\/aws.amazon.com\/sagemaker\/\" target=\"_blank\" rel=\"noopener noreferrer\">Amazon SageMaker<\/a>.<\/p>\n<h2>Understanding Neural Collaborative Filtering<\/h2>\n<p>A recommender system is a set of tools that helps provide users with a personalized experience by predicting user preference amongst a large number of options. Matrix factorization (MF) is a well-known approach to solving such a problem. Conventional MF solutions exploit explicit feedback in a linear fashion; explicit feedback consists of direct user preferences, such as ratings for movies on a five-star scale or binary preference on a product (like or not like). However, explicit feedback isn\u2019t always present in datasets. NCF solves the absence of explicit feedback by only using implicit feedback, which is derived from user activity, such as clicks and views. In addition, NCF utilizes multi-layer perceptron to introduce non-linearity into the solution.<\/p>\n<h2>Architecture overview<\/h2>\n<p>An NCF model contains two intrinsic sets of network layers: embedding and NCF layers. You use these layers to build a neural matrix factorization solution with two separate network architectures, generalized matrix factorization (GMF) and multi-layer perceptron (MLP), whose outputs are then concatenated as input for the final output layer. The following diagram from the original paper illustrates this architecture.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-14989\" src=\"https:\/\/d2908q01vomqb2.cloudfront.net\/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59\/2020\/08\/20\/customized-recommender-sagemaker-1.jpg\" alt=\"\" width=\"800\" height=\"487\"><\/p>\n<p>In this post, I walk you through building and deploying the NCF solution using the following steps:<\/p>\n<ol>\n<li>Prepare data for model training and testing.<\/li>\n<li>Code the NCF network in TensorFlow 2.0.<\/li>\n<li>Perform model training using Script Mode and deploy the trained model using Amazon SageMaker hosting services as an endpoint.<\/li>\n<li>Make a recommendation inference via the model endpoint.<\/li>\n<\/ol>\n<p>You can find the complete code sample in the <a href=\"https:\/\/github.com\/aws-samples\/amazon-sagemaker-custom-recommender-system\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub repo<\/a>.<\/p>\n<h2>Preparing the data<\/h2>\n<p>For this post, I use the <a href=\"https:\/\/grouplens.org\/datasets\/movielens\/\" target=\"_blank\" rel=\"noopener noreferrer\">MovieLens<\/a> dataset. MovieLens is a movie rating dataset provided by GroupLens, a research lab at the University of Minnesota.<\/p>\n<p>First, I run the following code to download the dataset into the <code>ml-latest-small<\/code> directory:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-bash\">%%bash  \r\n# delete the data directory if it already exists  \r\nrm -r ml-latest-small  \r\n  \r\n# download movielens small dataset  \r\ncurl -O http:\/\/files.grouplens.org\/datasets\/movielens\/ml-latest-small.zip  \r\n  \r\n# unzip into data directory\r\nunzip ml-latest-small.zip  \r\nrm ml-latest-small.zip  <\/code><\/pre>\n<\/div>\n<p>I only use <code>rating.csv<\/code>, which contains explicit feedback data, as a proxy dataset to demonstrate the NCF solution. To fit this solution to your data, you need to determine the meaning of a user liking an item.<\/p>\n<p>To perform a training and testing split, I take the latest 10 items each user rated as the testing set and keep the rest as the training set:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def train_test_split(df, holdout_num):  \r\n    \"\"\" perform training testing split \r\n     \r\n    @param df: dataframe \r\n    @param holdhout_num: number of items to be held out per user as testing items \r\n     \r\n    @return df_train: training data\r\n    @return df_test testing data\r\n     \r\n    \"\"\"  \r\n    # first sort the data by time  \r\n    df = df.sort_values(['userId', 'timestamp'], ascending=[True, False])  \r\n      \r\n    # perform deep copy to avoid modification on the original dataframe  \r\n    df_train = df.copy(deep=True)  \r\n    df_test = df.copy(deep=True)  \r\n      \r\n    # get test set  \r\n    df_test = df_test.groupby(['userId']).head(holdout_num).reset_index()  \r\n      \r\n    # get train set  \r\n    df_train = df_train.merge(  \r\n        df_test[['userId', 'movieId']].assign(remove=1),  \r\n        how='left'  \r\n    ).query('remove != 1').drop('remove', 1).reset_index(drop=True)  \r\n      \r\n    # sanity check to make sure we're not duplicating\/losing data  \r\n    assert len(df) == len(df_train) + len(df_test)  \r\n      \r\n    return df_train, df_test  \r\n<\/code><\/pre>\n<\/div>\n<p>Because we\u2019re performing a binary classification task and treating the positive label as if the user liked an item, we need to randomly sample the movies each user hasn\u2019t rated and treat them as negative labels. This is called <em>negative sampling<\/em>. The following function implements the process:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def negative_sampling(user_ids, movie_ids, items, n_neg):  \r\n    \"\"\"This function creates n_neg negative labels for every positive label \r\n     \r\n    @param user_ids: list of user ids \r\n    @param movie_ids: list of movie ids \r\n    @param items: unique list of movie ids \r\n    @param n_neg: number of negative labels to sample \r\n     \r\n    @return df_neg: negative sample dataframe \r\n     \r\n    \"\"\"  \r\n      \r\n    neg = []  \r\n    ui_pairs = zip(user_ids, movie_ids)  \r\n    records = set(ui_pairs)  \r\n      \r\n    # for every positive label case  \r\n    for (u, i) in records:  \r\n        # generate n_neg negative labels  \r\n        for _ in range(n_neg):  \r\n            j = np.random.choice(items)  \r\n            # resample if the movie already exists for that user  \r\n            While (u, j) in records:  \r\n                j = np.random.choice(items)  \r\n            neg.append([u, j, 0])  \r\n              \r\n    # convert to pandas dataframe for concatenation later  \r\n    df_neg = pd.DataFrame(neg, columns=['userId', 'movieId', 'rating'])  \r\n      \r\n    return df_neg  \r\n<\/code><\/pre>\n<\/div>\n<p>You can use the following code to perform the training and testing splits, negative sampling, and store the processed data in <a href=\"http:\/\/aws.amazon.com\/s3\" target=\"_blank\" rel=\"noopener noreferrer\">Amazon Simple Storage Service<\/a> (Amazon S3):<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">import os  \r\nimport boto3  \r\nimport sagemaker  \r\nimport numpy as np  \r\nimport pandas as pd  \r\n  \r\n# read rating data    \r\nfpath = '.\/ml-latest-small\/ratings.csv'    \r\ndf = pd.read_csv(fpath)    \r\n    \r\n# perform train test split    \r\ndf_train, df_test = train_test_split(df, 10)    \r\n   \r\n# create 5 negative samples per positive label for training set    \r\nneg_train = negative_sampling(    \r\n    user_ids=df_train.userId.values,     \r\n    movie_ids=df_train.movieId.values,    \r\n    items=df.movieId.unique(),    \r\n    n_neg=5    \r\n)    \r\n    \r\n# create final training and testing sets    \r\ndf_train = df_train[['userId', 'movieId']].assign(rating=1)    \r\ndf_train = pd.concat([df_train, neg_train], ignore_index=True)    \r\n    \r\ndf_test = df_test[['userId', 'movieId']].assign(rating=1)    \r\n  \r\n# save data locally first    \r\ndest = 'ml-latest-small\/s3'    \r\n!mkdir {dest}  \r\ntrain_path = os.path.join(dest, 'train.npy')    \r\ntest_path = os.path.join(dest, 'test.npy')    \r\nnp.save(train_path, df_train.values)    \r\nnp.save(test_path, df_test.values)    \r\n    \r\n# store data in the default S3 bucket  \r\nsagemaker_session = sagemaker.Session()  \r\nbucket_name = sm_session.default_bucket()  \r\nprint(\"the default bucket name is\", bucket_name)  \r\n  \r\n# upload to the default s3 bucket\r\nsagemaker_session.upload_data(train_path, key_prefix='data')    \r\nsagemaker_session.upload_data(test_path, key_prefix='data')    \r\n<\/code><\/pre>\n<\/div>\n<p>I use the Amazon SageMaker session\u2019s default bucket to store processed data. The format of the default bucket name is sagemaker-<em>{region}<\/em>\u2013<em>{aws-account-id}<\/em>.<\/p>\n<h2>Coding the NCF network<\/h2>\n<p>In this section, I implement GMF and MLP separately. These two components\u2019 input are both user and item embeddings. To define the embedding layers, we enter the following code:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def _get_user_embedding_layers(inputs, emb_dim):  \r\n    \"\"\" create user embeddings \"\"\"  \r\n    user_gmf_emb = tf.keras.layers.Dense(emb_dim, activation='relu')(inputs)  \r\n    user_mlp_emb = tf.keras.layers.Dense(emb_dim, activation='relu')(inputs)  \r\n    return user_gmf_emb, user_mlp_emb  \r\n  \r\ndef _get_item_embedding_layers(inputs, emb_dim):  \r\n    \"\"\" create item embeddings \"\"\"  \r\n    item_gmf_emb = tf.keras.layers.Dense(emb_dim, activation='relu')(inputs)  \r\n    item_mlp_emb = tf.keras.layers.Dense(emb_dim, activation='relu')(inputs)  \r\n    return item_gmf_emb, item_mlp_emb  \r\n<\/code><\/pre>\n<\/div>\n<p>To implement GMF, we multiply the user and item embeddings:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def _gmf(user_emb, item_emb):  \r\n    \"\"\" general matrix factorization branch \"\"\"  \r\n    gmf_mat = tf.keras.layers.Multiply()([user_emb, item_emb])  \r\n    return gmf_mat  <\/code><\/pre>\n<\/div>\n<p>The authors of <a href=\"https:\/\/arxiv.org\/abs\/1708.05031\" target=\"_blank\" rel=\"noopener noreferrer\">Neural Collaborative Filtering<\/a> show that a four-layer MLP with 64-dimensional user and item latent factor performed the best across different experiments, so we implement this structure as our MLP:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def _mlp(user_emb, item_emb, dropout_rate):  \r\n    \"\"\" multi-layer perceptron branch \"\"\"  \r\n    def add_layer(dim, input_layer, dropout_rate):  \r\n        hidden_layer = tf.keras.layers.Dense(dim, activation='relu')(input_layer)  \r\n        if dropout_rate:  \r\n            dropout_layer = tf.keras.layers.Dropout(dropout_rate)(hidden_layer)  \r\n            return dropout_layer  \r\n        return hidden_layer  \r\n  \r\n    concat_layer = tf.keras.layers.Concatenate()([user_emb, item_emb])  \r\n    dropout_l1 = tf.keras.layers.Dropout(dropout_rate)(concat_layer)  \r\n    dense_layer_1 = add_layer(64, dropout_l1, dropout_rate)  \r\n    dense_layer_2 = add_layer(32, dense_layer_1, dropout_rate)  \r\n    dense_layer_3 = add_layer(16, dense_layer_2, None)  \r\n    dense_layer_4 = add_layer(8, dense_layer_3, None)  \r\n    return dense_layer_4  \r\n<\/code><\/pre>\n<\/div>\n<p>Lastly, to produce the final prediction, we concatenate the output of GMF and MLP as the following:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def _neuCF(gmf, mlp, dropout_rate):  \r\n    \"\"\" final output layer \"\"\"  \r\n    concat_layer = tf.keras.layers.Concatenate()([gmf, mlp])  \r\n    output_layer = tf.keras.layers.Dense(1, activation='sigmoid')(concat_layer)  \r\n    return output_layer  \r\n<\/code><\/pre>\n<\/div>\n<p>To build the entire solution in one step, we create a graph building function:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def build_graph(user_dim, item_dim, dropout_rate=0.25):  \r\n    \"\"\" neural collaborative filtering model  \r\n     \r\n    @param user_dim: one hot encoded user dimension \r\n    @param item_dim: one hot encoded item dimension \r\n    @param dropout_rate: drop out rate for dropout layers \r\n     \r\n    @return model: neural collaborative filtering model graph \r\n     \r\n    \"\"\"  \r\n  \r\n    user_input = tf.keras.Input(shape=(user_dim))  \r\n    item_input = tf.keras.Input(shape=(item_dim))  \r\n  \r\n    # create embedding layers  \r\n    user_gmf_emb, user_mlp_emb = _get_user_embedding_layers(user_input, 32)  \r\n    item_gmf_emb, item_mlp_emb = _get_item_embedding_layers(item_input, 32)  \r\n  \r\n    # general matrix factorization  \r\n    gmf = _gmf(user_gmf_emb, item_gmf_emb)  \r\n  \r\n    # multi layer perceptron  \r\n    mlp = _mlp(user_mlp_emb, item_mlp_emb, dropout_rate)  \r\n  \r\n    # output  \r\n    output = _neuCF(gmf, mlp, dropout_rate)  \r\n  \r\n    # create the model\r\n    model = tf.keras.Model(inputs=[user_input, item_input], outputs=output)  \r\n  \r\n    return model  \r\n<\/code><\/pre>\n<\/div>\n<p>I use the Keras <code>plot_model<\/code> utility to verify the network architecture I just built is correct:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\"># build graph  \r\nncf_model = build_graph(n_user, n_item)  \r\n  \r\n# visualize and save to a local png file  \r\ntf.keras.utils.plot_model(ncf_model, to_file=\"neural_collaborative_filtering_model.png\")  <\/code><\/pre>\n<\/div>\n<p>The output architecture should look like the following diagram.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-14990\" src=\"https:\/\/d2908q01vomqb2.cloudfront.net\/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59\/2020\/08\/20\/customized-recommender-sagemaker-2.jpg\" alt=\"\" width=\"600\" height=\"1114\"><\/p>\n<h2>Training and deploying the model<\/h2>\n<p>For instructions on deploying a model you trained using an instance on Amazon SageMaker, see <a href=\"https:\/\/aws.amazon.com\/blogs\/machine-learning\/deploy-trained-keras-or-tensorflow-models-using-amazon-sagemaker\/\" target=\"_blank\" rel=\"noopener noreferrer\">Deploy trained Keras or TensorFlow models using Amazon SageMaker<\/a>. For this post, I deploy this model using Script Mode.<\/p>\n<p>We first need to create a Python script that contains the model training code. I compiled the model architecture code presented previously and added additional code required in the <code>ncf.py<\/code>, which you can use directly. I also implemented a function for you to load training data; to load testing data, the function is the same except the file name is changed to reflect the testing data destination. See the following code:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">def _load_training_data(base_dir):  \r\n    \"\"\" load training data \"\"\"  \r\n    df_train = np.load(os.path.join(base_dir, 'train.npy'))  \r\n    user_train, item_train, y_train = np.split(np.transpose(df_train).flatten(), 3)  \r\n    return user_train, item_train, y_train  \r\n<\/code><\/pre>\n<\/div>\n<p>After downloading the training script and storing it in the same directory as the model training notebook, we can initialize a TensorFlow estimator with the following code:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">ncf_estimator = TensorFlow(  \r\n    entry_point='ncf.py',  \r\n    role=role,  \r\n    train_instance_count=1,  \r\n    train_instance_type='ml.c5.2xlarge',  \r\n    framework_version='2.1.0',  \r\n    py_version='py3',  \r\n    distributions={'parameter_server': {'enabled': True}},  \r\n    hyperparameters={'epochs': 3, 'batch_size': 256, 'n_user': n_user, 'n_item': n_item}  \r\n)  \r\n<\/code><\/pre>\n<\/div>\n<p>We fit the estimator to our training data to start the training job:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\"># specify the location of the training data  \r\ntraining_data_uri = os.path.join(f's3:\/\/{bucket_name}', 'data')  \r\n  \r\n# kick off the training job  \r\nncf_estimator.fit(training_data_uri)  \r\n<\/code><\/pre>\n<\/div>\n<p>When you see the output in the following screenshot, your model training job has started.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-14991\" src=\"https:\/\/d2908q01vomqb2.cloudfront.net\/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59\/2020\/08\/20\/customized-recommender-sagemaker-3.jpg\" alt=\"\" width=\"800\" height=\"164\"><\/p>\n<p>When the model training process is complete, we can deploy the model as an endpoint. See the following code:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\">predictor = ncf_estimator.deploy(initial_instance_count=1,   \r\n                                 instance_type='ml.c5.xlarge',   \r\n                                 endpoint_type='tensorflow-serving')  \r\n<\/code><\/pre>\n<\/div>\n<h2>Performing model inference<\/h2>\n<p>To make inference using the endpoint on the testing set, we can invoke the model in the same way by using <a href=\"https:\/\/www.tensorflow.org\/tfx\/guide\/serving\" target=\"_blank\" rel=\"noopener noreferrer\">TensorFlow Serving<\/a>:<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\"># Define a function to read testing data  \r\ndef _load_testing_data(base_dir):  \r\n    \"\"\" load testing data \"\"\"  \r\n    df_test = np.load(os.path.join(base_dir, 'test.npy'))  \r\n    user_test, item_test, y_test = np.split(np.transpose(df_test).flatten(), 3)  \r\n    return user_test, item_test, y_test  \r\n  \r\n# read testing data from local  \r\nuser_test, item_test, test_labels = _load_testing_data('.\/ml-latest-small\/s3\/')  \r\n  \r\n# one-hot encode the testing data for model input  \r\nwith tf.Session() as tf_sess:  \r\n    test_user_data = tf_sess.run(tf.one_hot(user_test, depth=n_user)).tolist()  \r\n    test_item_data = tf_sess.run(tf.one_hot(item_test, depth=n_item)).tolist()  \r\n      \r\n# make batch prediction  \r\nbatch_size = 100  \r\ny_pred = []  \r\nfor idx in range(0, len(test_user_data), batch_size):  \r\n    # reformat test samples into tensorflow serving acceptable format  \r\n    input_vals = {  \r\n     \"instances\": [  \r\n         {'input_1': u, 'input_2': i}   \r\n         for (u, i) in zip(test_user_data[idx:idx+batch_size], test_item_data[idx:idx+batch_size])  \r\n    ]}  \r\n   \r\n    # invoke model endpoint to make inference  \r\n    pred = predictor.predict(input_vals)  \r\n      \r\n    # store predictions  \r\n    y_pred.extend([i[0] for i in pred['predictions']])  \r\n<\/code><\/pre>\n<\/div>\n<p>The model output is a set of probabilities, ranging from 0 to 1, for each user-item pair that we specify for inference. To make final binary predictions, such as like or not like, we need to apply a threshold. For demonstration purposes, I use 0.5 as a threshold; if the predicted probability is equal or greater than 0.5, we say the model predicts the user will like the item, and vice versa.<\/p>\n<div class=\"hide-language\">\n<pre><code class=\"lang-python\"># combine user id, movie id, and prediction in one dataframe  \r\npred_df = pd.DataFrame([  \r\n    user_test,  \r\n    item_test,  \r\n    (np.array(y_pred) &gt;= 0.5).astype(int)],  \r\n).T  \r\n  \r\n# assign column names to the dataframe  \r\npred_df.columns = ['userId', 'movieId', 'prediction']  \r\n<\/code><\/pre>\n<\/div>\n<p>Finally, we can get a list of model predictions on whether a user will like a movie, as shown in the following screenshot.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-14992\" src=\"https:\/\/d2908q01vomqb2.cloudfront.net\/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59\/2020\/08\/20\/customized-recommender-sagemaker-4.jpg\" alt=\"\" width=\"300\" height=\"119\"><\/p>\n<h2>Conclusion<\/h2>\n<p>Designing a recommender system can be a challenging task that sometimes requires model customization. In this post, I showed you how to implement, deploy, and invoke an NCF model from scratch in Amazon SageMaker. This work can serve as a foundation for you to start building more customized solutions with your own datasets.<\/p>\n<p>For more information about using built-in Amazon SageMaker algorithms and <a href=\"https:\/\/aws.amazon.com\/personalize\" target=\"_blank\" rel=\"noopener noreferrer\">Amazon Personalize<\/a> to build recommender system solutions, see the following:<\/p>\n<p>To further customize the Neural Collaborative Filtering network, <a href=\"https:\/\/www.ijcai.org\/Proceedings\/2017\/0447.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">Deep Matrix Factorization<\/a> (Xue et al., 2017) model can be an option. For more information, see <a href=\"http:\/\/aws-de-media.s3.amazonaws.com\/images\/AWS_Summit_2018\/June6\/Doppler\/Build%20Your%20Recommendation%20Engines%20on%20AWS%20Today.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">Build a Recommendation Engine on AWS Today<\/a>.<\/p>\n<hr>\n<h3>About the Author<\/h3>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignleft size-full wp-image-14998\" src=\"https:\/\/d2908q01vomqb2.cloudfront.net\/f1f836cb4ea6efb2a0b1b99f41ad8b103eff4b59\/2020\/08\/20\/ray-li-100.jpg\" alt=\"\" width=\"100\" height=\"135\">Taihua (Ray) Li is a data scientist with AWS Professional Services. He holds a M.S. in Predictive Analytics degree from DePaul University and has several years of experience building artificial intelligence powered applications for non-profit and enterprise organizations. At AWS, Ray helps customers to unlock business potentials and to drive actionable outcomes with machine learning. Outside of work, he enjoys fitness classes, biking, and traveling.<\/p>\n<p>\u00a0<\/p>\n<p>\u00a0<\/p>\n<p>\u00a0<\/p>\n<p>\u00a0<\/p>\n<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>https:\/\/aws.amazon.com\/blogs\/machine-learning\/building-a-customized-recommender-system-in-amazon-sagemaker\/<\/p>\n","protected":false},"author":0,"featured_media":137,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[3],"tags":[],"_links":{"self":[{"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/posts\/136"}],"collection":[{"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/comments?post=136"}],"version-history":[{"count":0,"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/posts\/136\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/media\/137"}],"wp:attachment":[{"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/media?parent=136"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/categories?post=136"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/salarydistribution.com\/machine-learning\/wp-json\/wp\/v2\/tags?post=136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}