!pip3 install tensorflow_hub

%%bash
pip install --upgrade tensorflow

"""
Explanation: Neural network hybrid recommendation system on Google Analytics data model and training
This notebook demonstrates how to implement a hybrid recommendation system using a neural network to combine content-based and collaborative filtering recommendation models using Google Analytics data. We are going to use the learned user embeddings from wals.ipynb and combine that with our previous content-based features from content_based_using_neural_networks.ipynb
Now that we have our data preprocessed from BigQuery and Cloud Dataflow, we can build our neural network hybrid recommendation model to our preprocessed data. Then we can train locally to make sure everything works and then use the power of Google Cloud ML Engine to scale it out.
We're going to use TensorFlow Hub to use trained text embeddings, so let's first pip install that and reset our session.
End of explanation
"""


# Import helpful libraries and setup our project, bucket, and region
import os
import tensorflow as tf
import tensorflow_hub as hub

PROJECT = "cloud-training-demos" # REPLACE WITH YOUR PROJECT ID
BUCKET = "cloud-training-demos-ml" # REPLACE WITH YOUR BUCKET NAME
REGION = "us-central1" # REPLACE WITH YOUR BUCKET REGION e.g. us-central1

# do not change these
os.environ["PROJECT"] = PROJECT
os.environ["BUCKET"] = BUCKET
os.environ["REGION"] = REGION
os.environ["TFVERSION"] = "1.13"

%%bash
gcloud config set project $PROJECT
gcloud config set compute/region $REGION

%%bash
if ! gsutil ls | grep -q gs://${BUCKET}/hybrid_recommendation/preproc; then
    gsutil mb -l ${REGION} gs://${BUCKET}
    # copy canonical set of preprocessed files if you didn't do preprocessing notebook
    gsutil -m cp -R gs://cloud-training-demos/courses/machine_learning/deepdive/10_recommendation/hybrid_recommendation gs://${BUCKET}
fi

"""
Explanation: Now reset the notebook's session kernel! Since we're no longer using Cloud Dataflow, we'll be using the python3 kernel from here on out so don't forget to change the kernel if it's still python2.
End of explanation
"""


from tensorflow.python.lib.io import file_io

# Get number of content ids from text file in Google Cloud Storage
with file_io.FileIO(tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocab_counts/content_id_vocab_count.txt*".format(BUCKET))[0], mode = 'r') as ifp:
    number_of_content_ids = int([x for x in ifp][0])
print("number_of_content_ids = {}".format(number_of_content_ids))

# Get number of categories from text file in Google Cloud Storage
with file_io.FileIO(tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocab_counts/category_vocab_count.txt*".format(BUCKET))[0], mode = 'r') as ifp:
    number_of_categories = int([x for x in ifp][0])
print("number_of_categories = {}".format(number_of_categories))

# Get number of authors from text file in Google Cloud Storage
with file_io.FileIO(tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocab_counts/author_vocab_count.txt*".format(BUCKET))[0], mode = 'r') as ifp:
    number_of_authors = int([x for x in ifp][0])
print("number_of_authors = {}".format(number_of_authors))

# Get mean months since epoch from text file in Google Cloud Storage
with file_io.FileIO(tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocab_counts/months_since_epoch_mean.txt*".format(BUCKET))[0], mode = 'r') as ifp:
    mean_months_since_epoch = float([x for x in ifp][0])
print("mean_months_since_epoch = {}".format(mean_months_since_epoch))

# Determine CSV and label columns
NON_FACTOR_COLUMNS = "next_content_id,visitor_id,content_id,category,title,author,months_since_epoch".split(',')
FACTOR_COLUMNS = ["user_factor_{}".format(i) for i in range(10)] + ["item_factor_{}".format(i) for i in range(10)]
CSV_COLUMNS = NON_FACTOR_COLUMNS + FACTOR_COLUMNS
LABEL_COLUMN = "next_content_id"

# Set default values for each CSV column
NON_FACTOR_DEFAULTS = [["Unknown"],["Unknown"],["Unknown"],["Unknown"],["Unknown"],["Unknown"],[mean_months_since_epoch]]
FACTOR_DEFAULTS = [[0.0] for i in range(10)] + [[0.0] for i in range(10)] # user and item
DEFAULTS = NON_FACTOR_DEFAULTS + FACTOR_DEFAULTS

"""
Explanation: <h2> Create hybrid recommendation system model using TensorFlow </h2>

Now that we've created our training and evaluation input files as well as our categorical feature vocabulary files, we can create our TensorFlow hybrid recommendation system model.
Let's first get some of our aggregate information that we will use in the model from some of our preprocessed files we saved in Google Cloud Storage.
End of explanation
"""


# Create input function for train and eval
def read_dataset(filename, mode, batch_size = 512):
    def _input_fn():
        def decode_csv(value_column):
            columns = tf.decode_csv(records = value_column, record_defaults = DEFAULTS)
            features = dict(zip(CSV_COLUMNS, columns))          
            label = features.pop(LABEL_COLUMN)         
            return features, label

        # Create list of files that match pattern
        file_list = tf.gfile.Glob(filename = filename)

        # Create dataset from file list
        dataset = tf.data.TextLineDataset(filenames = file_list).map(map_func = decode_csv)

        if mode == tf.estimator.ModeKeys.TRAIN:
            num_epochs = None # indefinitely
            dataset = dataset.shuffle(buffer_size = 10 * batch_size)
        else:
            num_epochs = 1 # end-of-input after this

        dataset = dataset.repeat(count = num_epochs).batch(batch_size = batch_size)
        return dataset.make_one_shot_iterator().get_next()
    return _input_fn

"""
Explanation: Create input function for training and evaluation to read from our preprocessed CSV files.
End of explanation
"""


# Create feature columns to be used in model
def create_feature_columns(args):
    # Create content_id feature column
    content_id_column = tf.feature_column.categorical_column_with_hash_bucket(
        key = "content_id",
        hash_bucket_size = number_of_content_ids)

    # Embed content id into a lower dimensional representation
    embedded_content_column = tf.feature_column.embedding_column(
        categorical_column = content_id_column,
        dimension = args["content_id_embedding_dimensions"])

    # Create category feature column
    categorical_category_column = tf.feature_column.categorical_column_with_vocabulary_file(
        key = "category",
        vocabulary_file = tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocabs/category_vocab.txt*".format(args["bucket"]))[0],
        num_oov_buckets = 1)

    # Convert categorical category column into indicator column so that it can be used in a DNN
    indicator_category_column = tf.feature_column.indicator_column(categorical_column = categorical_category_column)

    # Create title feature column using TF Hub
    embedded_title_column = hub.text_embedding_column(
        key = "title", 
        module_spec = "https://tfhub.dev/google/nnlm-de-dim50-with-normalization/1",
        trainable = False)

    # Create author feature column
    author_column = tf.feature_column.categorical_column_with_hash_bucket(
        key = "author",
        hash_bucket_size = number_of_authors + 1)

    # Embed author into a lower dimensional representation
    embedded_author_column = tf.feature_column.embedding_column(
        categorical_column = author_column,
        dimension = args["author_embedding_dimensions"])

    # Create months since epoch boundaries list for our binning
    months_since_epoch_boundaries = list(range(400, 700, 20))

    # Create months_since_epoch feature column using raw data
    months_since_epoch_column = tf.feature_column.numeric_column(
        key = "months_since_epoch")

    # Create bucketized months_since_epoch feature column using our boundaries
    months_since_epoch_bucketized = tf.feature_column.bucketized_column(
        source_column = months_since_epoch_column,
        boundaries = months_since_epoch_boundaries)

    # Cross our categorical category column and bucketized months since epoch column
    crossed_months_since_category_column = tf.feature_column.crossed_column(
        keys = [categorical_category_column, months_since_epoch_bucketized],
        hash_bucket_size = len(months_since_epoch_boundaries) * (number_of_categories + 1))

    # Convert crossed categorical category and bucketized months since epoch column into indicator column so that it can be used in a DNN
    indicator_crossed_months_since_category_column = tf.feature_column.indicator_column(
            categorical_column = crossed_months_since_category_column)

    # Create user and item factor feature columns from our trained WALS model
    user_factors = [tf.feature_column.numeric_column(key = "user_factor_" + str(i)) for i in range(10)]
    item_factors =  [tf.feature_column.numeric_column(key = "item_factor_" + str(i)) for i in range(10)]

    # Create list of feature columns
    feature_columns = [embedded_content_column,
    embedded_author_column,
    indicator_category_column,
    embedded_title_column,
    indicator_crossed_months_since_category_column] + user_factors + item_factors

    return feature_columns

"""
Explanation: Next, we will create our feature columns using our read in features.
End of explanation
"""


# Create custom model function for our custom estimator
def model_fn(features, labels, mode, params):
    # TODO: Create neural network input layer using our feature columns defined above

    # TODO: Create hidden layers by looping through hidden unit list

    # TODO: Compute logits (1 per class) using the output of our last hidden layer

    # TODO: Find the predicted class indices based on the highest logit (which will result in the highest probability)
    predicted_classes = 

  # Read in the content id vocabulary so we can tie the predicted class indices to their respective content ids
    with file_io.FileIO(tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocabs/content_id_vocab.txt*".format(BUCKET))[0], mode = "r") as ifp:
        content_id_names = tf.constant(value = [x.rstrip() for x in ifp])

    # Gather predicted class names based predicted class indices
    predicted_class_names = tf.gather(params = content_id_names, indices = predicted_classes)

    # If the mode is prediction
    if mode == tf.estimator.ModeKeys.PREDICT:
        # Create predictions dict
        predictions_dict = {
            "class_ids": tf.expand_dims(input = predicted_classes, axis = -1),
            "class_names" : tf.expand_dims(input = predicted_class_names, axis = -1),
            "probabilities": tf.nn.softmax(logits = logits),
            "logits": logits
        }

        # Create export outputs
        export_outputs = {"predict_export_outputs": tf.estimator.export.PredictOutput(outputs = predictions_dict)}

        return tf.estimator.EstimatorSpec( # return early since we"re done with what we need for prediction mode
          mode = mode,
          predictions = predictions_dict,
          loss = None,
          train_op = None,
          eval_metric_ops = None,
          export_outputs = export_outputs)

    # Continue on with training and evaluation modes

    # Create lookup table using our content id vocabulary
    table = tf.contrib.lookup.index_table_from_file(
        vocabulary_file = tf.gfile.Glob(filename = "gs://{}/hybrid_recommendation/preproc/vocabs/content_id_vocab.txt*".format(BUCKET))[0])

    # Look up labels from vocabulary table
    labels = table.lookup(keys = labels)

    # TODO: Compute loss using the correct type of softmax cross entropy since this is classification and our labels (content id indices) and probabilities are mutually exclusive
    loss = 

    # If the mode is evaluation
    if mode == tf.estimator.ModeKeys.EVAL:
        # Compute evaluation metrics of total accuracy and the accuracy of the top k classes
        accuracy = tf.metrics.accuracy(labels = labels, predictions = predicted_classes, name = "acc_op")
        top_k_accuracy = tf.metrics.mean(values = tf.nn.in_top_k(predictions = logits, targets = labels, k = params["top_k"]))
        map_at_k = tf.metrics.average_precision_at_k(labels = labels, predictions = predicted_classes, k = params["top_k"])

        # Put eval metrics into a dictionary
        eval_metric_ops = {
            "accuracy": accuracy,
            "top_k_accuracy": top_k_accuracy,
            "map_at_k": map_at_k}

        # Create scalar summaries to see in TensorBoard
        tf.summary.scalar(name = "accuracy", tensor = accuracy[1])
        tf.summary.scalar(name = "top_k_accuracy", tensor = top_k_accuracy[1])
        tf.summary.scalar(name = "map_at_k", tensor = map_at_k[1])
    
        return tf.estimator.EstimatorSpec( # return early since we"re done with what we need for evaluation mode
            mode = mode,
            predictions = None,
            loss = loss,
            train_op = None,
            eval_metric_ops = eval_metric_ops,
            export_outputs = None)

    # Continue on with training mode

    # If the mode is training
    assert mode == tf.estimator.ModeKeys.TRAIN

    # Create a custom optimizer
    optimizer = tf.train.AdagradOptimizer(learning_rate = params["learning_rate"])

    # Create train op
    train_op = optimizer.minimize(loss = loss, global_step = tf.train.get_global_step())

    return tf.estimator.EstimatorSpec( # final return since we"re done with what we need for training mode
        mode = mode,
        predictions = None,
        loss = loss,
        train_op = train_op,
        eval_metric_ops = None,
        export_outputs = None)

"""
Explanation: Now we'll create our model function
End of explanation
"""


# Create serving input function
def serving_input_fn():  
    feature_placeholders = {
        colname : tf.placeholder(dtype = tf.string, shape = [None]) \
        for colname in NON_FACTOR_COLUMNS[1:-1]
    }
    feature_placeholders["months_since_epoch"] = tf.placeholder(dtype = tf.float32, shape = [None])

    for colname in FACTOR_COLUMNS:
        feature_placeholders[colname] = tf.placeholder(dtype = tf.float32, shape = [None])

    features = {
        key: tf.expand_dims(tensor, -1) \
        for key, tensor in feature_placeholders.items()
    }

    return tf.estimator.export.ServingInputReceiver(features = features, receiver_tensors = feature_placeholders)

"""
Explanation: Now create a serving input function
End of explanation
"""


# Create train and evaluate loop to combine all of the pieces together.
tf.logging.set_verbosity(tf.logging.INFO)
def train_and_evaluate(args):
    estimator = tf.estimator.Estimator(
        model_fn = model_fn,
        model_dir = args["output_dir"],
        params = {
        "feature_columns": create_feature_columns(args),
        "hidden_units": args["hidden_units"],
        "n_classes": number_of_content_ids,
        "learning_rate": args["learning_rate"],
        "top_k": args["top_k"],
        "bucket": args["bucket"]
        }
    )

    train_spec = tf.estimator.TrainSpec(
        input_fn = read_dataset(filename = args["train_data_paths"], mode = tf.estimator.ModeKeys.TRAIN, batch_size = args["batch_size"]),
        max_steps = args["train_steps"])

    exporter = tf.estimator.LatestExporter(name = "exporter", serving_input_receiver_fn = serving_input_fn)

    eval_spec = tf.estimator.EvalSpec(
        input_fn = read_dataset(filename = args["eval_data_paths"], mode = tf.estimator.ModeKeys.EVAL, batch_size = args["batch_size"]),
        steps = None,
        start_delay_secs = args["start_delay_secs"],
        throttle_secs = args["throttle_secs"],
        exporters = exporter)

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

"""
Explanation: Now that all of the pieces are assembled let's create and run our train and evaluate loop
End of explanation
"""


# Call train and evaluate loop
import shutil

outdir = "hybrid_recommendation_trained"
shutil.rmtree(path = outdir, ignore_errors = True) # start fresh each time

arguments = {
    "bucket": BUCKET,
    "train_data_paths": "gs://{}/hybrid_recommendation/preproc/features/train.csv*".format(BUCKET),
    "eval_data_paths": "gs://{}/hybrid_recommendation/preproc/features/eval.csv*".format(BUCKET),
    "output_dir": outdir,
    "batch_size": 128,
    "learning_rate": 0.1,
    "hidden_units": [256, 128, 64],
    "content_id_embedding_dimensions": 10,
    "author_embedding_dimensions": 10,
    "top_k": 10,
    "train_steps": 1000,
    "start_delay_secs": 30,
    "throttle_secs": 30
}

train_and_evaluate(arguments)

"""
Explanation: Run train_and_evaluate!
End of explanation
"""


%%writefile requirements.txt
tensorflow_hub

%%bash
echo "bucket=${BUCKET}"
rm -rf hybrid_recommendation_trained
export PYTHONPATH=${PYTHONPATH}:${PWD}/hybrid_recommendations_module
python -m trainer.task \
    --bucket=${BUCKET} \
    --train_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/train.csv* \
    --eval_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/eval.csv* \
    --output_dir=${OUTDIR} \
    --batch_size=128 \
    --learning_rate=0.1 \
    --hidden_units="256 128 64" \
    --content_id_embedding_dimensions=10 \
    --author_embedding_dimensions=10 \
    --top_k=10 \
    --train_steps=1000 \
    --start_delay_secs=30 \
    --throttle_secs=60

"""
Explanation: Run on module locally
Now let's place our code into a python module with model.py and task.py files so that we can train using Google Cloud's ML Engine! First, let's test our module locally.
End of explanation
"""


%%bash
OUTDIR=gs://${BUCKET}/hybrid_recommendation/small_trained_model
JOBNAME=hybrid_recommendation_$(date -u +%y%m%d_%H%M%S)
echo $OUTDIR $REGION $JOBNAME
gsutil -m rm -rf $OUTDIR
gcloud ml-engine jobs submit training $JOBNAME \
    --region=$REGION \
    --module-name=trainer.task \
    --package-path=$(pwd)/hybrid_recommendations_module/trainer \
    --job-dir=$OUTDIR \
    --staging-bucket=gs://$BUCKET \
    --scale-tier=STANDARD_1 \
    --runtime-version=$TFVERSION \
    -- \
    --bucket=${BUCKET} \
    --train_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/train.csv* \
    --eval_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/eval.csv* \
    --output_dir=${OUTDIR} \
    --batch_size=128 \
    --learning_rate=0.1 \
    --hidden_units="256 128 64" \
    --content_id_embedding_dimensions=10 \
    --author_embedding_dimensions=10 \
    --top_k=10 \
    --train_steps=1000 \
    --start_delay_secs=30 \
    --throttle_secs=30

"""
Explanation: Run on Google Cloud AI Platform
If our module locally trained fine, let's now use of the power of AI Platform to scale it out on Google Cloud.
End of explanation
"""


%%writefile hyperparam.yaml
trainingInput:
    hyperparameters:
        goal: MAXIMIZE
        maxTrials: 5
        maxParallelTrials: 1
        hyperparameterMetricTag: accuracy
        params:
            - parameterName: batch_size
              type: INTEGER
              minValue: 8
              maxValue: 64
              scaleType: UNIT_LINEAR_SCALE
            - parameterName: learning_rate
              type: DOUBLE
              minValue: 0.01
              maxValue: 0.1
              scaleType: UNIT_LINEAR_SCALE
            - parameterName: hidden_units
              type: CATEGORICAL
              categoricalValues: ["1024 512 256", "1024 512 128", "1024 256 128", "512 256 128", "1024 512 64", "1024 256 64", "512 256 64", "1024 128 64", "512 128 64", "256 128 64", "1024 512 32", "1024 256 32", "512 256 32", "1024 128 32", "512 128 32", "256 128 32", "1024 64 32", "512 64 32", "256 64 32", "128 64 32"]
            - parameterName: content_id_embedding_dimensions
              type: INTEGER
              minValue: 5
              maxValue: 250
              scaleType: UNIT_LOG_SCALE
            - parameterName: author_embedding_dimensions
              type: INTEGER
              minValue: 5
              maxValue: 30
              scaleType: UNIT_LINEAR_SCALE

%%bash
OUTDIR=gs://${BUCKET}/hybrid_recommendation/hypertuning
JOBNAME=hybrid_recommendation_$(date -u +%y%m%d_%H%M%S)
echo $OUTDIR $REGION $JOBNAME
gsutil -m rm -rf $OUTDIR
gcloud ml-engine jobs submit training $JOBNAME \
    --region=$REGION \
    --module-name=trainer.task \
    --package-path=$(pwd)/hybrid_recommendations_module/trainer \
    --job-dir=$OUTDIR \
    --staging-bucket=gs://$BUCKET \
    --scale-tier=STANDARD_1 \
    --runtime-version=$TFVERSION \
    --config=hyperparam.yaml \
    -- \
    --bucket=${BUCKET} \
    --train_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/train.csv* \
    --eval_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/eval.csv* \
    --output_dir=${OUTDIR} \
    --batch_size=128 \
    --learning_rate=0.1 \
    --hidden_units="256 128 64" \
    --content_id_embedding_dimensions=10 \
    --author_embedding_dimensions=10 \
    --top_k=10 \
    --train_steps=1000 \
    --start_delay_secs=30 \
    --throttle_secs=30

"""
Explanation: Let's add some hyperparameter tuning!
End of explanation
"""


%%bash
OUTDIR=gs://${BUCKET}/hybrid_recommendation/big_trained_model
JOBNAME=hybrid_recommendation_$(date -u +%y%m%d_%H%M%S)
echo $OUTDIR $REGION $JOBNAME
gsutil -m rm -rf $OUTDIR
gcloud ml-engine jobs submit training $JOBNAME \
    --region=$REGION \
    --module-name=trainer.task \
    --package-path=$(pwd)/hybrid_recommendations_module/trainer \
    --job-dir=$OUTDIR \
    --staging-bucket=gs://$BUCKET \
    --scale-tier=STANDARD_1 \
    --runtime-version=$TFVERSION \
    -- \
    --bucket=${BUCKET} \
    --train_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/train.csv* \
    --eval_data_paths=gs://${BUCKET}/hybrid_recommendation/preproc/features/eval.csv* \
    --output_dir=${OUTDIR} \
    --batch_size=128 \
    --learning_rate=0.1 \
    --hidden_units="256 128 64" \
    --content_id_embedding_dimensions=10 \
    --author_embedding_dimensions=10 \
    --top_k=10 \
    --train_steps=10000 \
    --start_delay_secs=30 \
    --throttle_secs=30

"""
Explanation: Now that we know the best hyperparameters, run a big training job!
End of explanation
"""


