text: '''
  Created on Aug 9, 2016
  
  Keras Implementation of Generalized Matrix Factorization (GMF) recommender model in:
  He Xiangnan et al. Neural Collaborative Filtering. In WWW 2017.  
  
  @author: Xiangnan He (xiangnanhe@gmail.com)
  '''
  import argparse
  from time import time
  from datetime import datetime
  
  import numpy as np
  from keras import initializations
  from keras.layers import Embedding, Input, Dense, merge, Flatten
  from keras.models import Model
  from keras.optimizers import Adagrad, Adam, SGD, RMSprop
  from keras.regularizers import l2
  
  from Dataset import Dataset, get_train_instances
  from evaluate import evaluate_model
  from metrics_exporter import MetricsExporter
  
  
  #################### Arguments ####################
  def parse_args():
      parser = argparse.ArgumentParser(description="Run GMF.")
      parser.add_argument('--path', nargs='?', default='Data/',
                          help='Input data path.')
      parser.add_argument('--dataset', nargs='?', default='ml-1m',
                          help='Choose a dataset.')
      parser.add_argument('--epochs', type=int, default=100,
                          help='Number of epochs.')
      parser.add_argument('--batch_size', type=int, default=256,
                          help='Batch size.')
      parser.add_argument('--num_factors', type=int, default=8,
                          help='Embedding size.')
      parser.add_argument('--regs', nargs='?', default='[0,0]',
                          help="Regularization for user and item embeddings.")
      parser.add_argument('--num_neg', type=int, default=4,
                          help='Number of negative instances to pair with a positive instance.')
      parser.add_argument('--topk', type=int, default=10,
                          help='Number of @k.')
      parser.add_argument('--lr', type=float, default=0.001,
                          help='Learning rate.')
      parser.add_argument('--learner', nargs='?', default='adam',
                          help='Specify an optimizer: adagrad, adam, rmsprop, sgd')
      parser.add_argument('--verbose', type=int, default=1,
                          help='Show performance per X iterations')
      parser.add_argument('--out', type=int, default=1,
                          help='Whether to save the trained model.')
      return parser.parse_args()
  
  def init_normal(shape, name=None):
      return initializations.normal(shape, scale=0.01, name=name)
  
  def get_model(num_users, num_items, latent_dim, regs=[0,0]):
      # Input variables
      user_input = Input(shape=(1,), dtype='int32', name = 'user_input')
      item_input = Input(shape=(1,), dtype='int32', name = 'item_input')
  
      MF_Embedding_User = Embedding(input_dim = num_users, output_dim = latent_dim, name = 'user_embedding',
                                    init = init_normal, W_regularizer = l2(regs[0]), input_length=1)
      MF_Embedding_Item = Embedding(input_dim = num_items, output_dim = latent_dim, name = 'item_embedding',
                                    init = init_normal, W_regularizer = l2(regs[1]), input_length=1)   
      
      # Crucial to flatten an embedding vector!
      user_latent = Flatten()(MF_Embedding_User(user_input))
      item_latent = Flatten()(MF_Embedding_Item(item_input))
      
      # Element-wise product of user and item embeddings 
      predict_vector = merge([user_latent, item_latent], mode = 'mul')
      
      # Final prediction layer
      #prediction = Lambda(lambda x: K.sigmoid(K.sum(x)), output_shape=(1,))(predict_vector)
      prediction = Dense(1, activation='sigmoid', init='lecun_uniform', name = 'prediction')(predict_vector)
      
      model = Model(input=[user_input, item_input], 
                  output=prediction)
  
      return model
  
  
  if __name__ == '__main__':
      args = parse_args()
      num_factors = args.num_factors
      regs = eval(args.regs)
      num_negatives = args.num_neg
      learner = args.learner
      learning_rate = args.lr
      epochs = args.epochs
      batch_size = args.batch_size
      verbose = args.verbose
  
      topK = args.topk
      evaluation_threads = 1 #mp.cpu_count()
      print(("GMF arguments: %s" %(args)))
      model_out_file = 'pretrain/%s_GMF_%d.h5' %(args.dataset, num_factors)
  
      metrics = {}
      metrics['loss'] = []
      metrics['loss_eval'] = []
      metrics[f'hr@{topK}'] = []
      metrics['ndcg'] = []
      metrics['duration'] = []
      metrics['duration_eval'] = []
  
  
      # Loading data
      t1 = time()
      dataset = Dataset(args.path + args.dataset)
      train, testRatings, testNegatives = dataset.trainMatrix, dataset.testRatings, dataset.testNegatives
      num_users, num_items = train.shape
      print(("Load data done [%.1f s]. #user=%d, #item=%d, #train=%d, #test=%d" 
            %(time()-t1, num_users, num_items, train.nnz, len(testRatings))))
      
      # Build model
      model = get_model(num_users, num_items, num_factors, regs)
      if learner.lower() == "adagrad": 
          model.compile(optimizer=Adagrad(lr=learning_rate), loss='binary_crossentropy')
      elif learner.lower() == "rmsprop":
          model.compile(optimizer=RMSprop(lr=learning_rate), loss='binary_crossentropy')
      elif learner.lower() == "adam":
          model.compile(optimizer=Adam(lr=learning_rate), loss='binary_crossentropy')
      else:
          model.compile(optimizer=SGD(lr=learning_rate), loss='binary_crossentropy')
      #print(model.summary())
      
      # Init performance
      t1 = time()
      (hits, ndcgs) = evaluate_model(model, testRatings, testNegatives, topK, evaluation_threads)
      hr, ndcg = np.array(hits).mean(), np.array(ndcgs).mean()
      #mf_embedding_norm = np.linalg.norm(model.get_layer('user_embedding').get_weights())+np.linalg.norm(model.get_layer('item_embedding').get_weights())
      #p_norm = np.linalg.norm(model.get_layer('prediction').get_weights()[0])
      print(('Init: HR = %.4f, NDCG = %.4f\t [%.1f s]' % (hr, ndcg, time()-t1)))
      
      # Train model
      best_hr, best_ndcg, best_iter = hr, ndcg, -1
      for epoch in range(epochs):
          t1 = time()
          # Generate training instances
          user_input, item_input, labels = get_train_instances(train, num_negatives)
          
          # Training
          hist = model.fit([np.array(user_input), np.array(item_input)], #input
                           np.array(labels), # labels 
                           batch_size=batch_size, nb_epoch=1, verbose=1, shuffle=True)
          t2 = time()
          duration = t2-t1
          metrics['loss'].append( hist.history['loss'][0])
          metrics['duration'].append( duration)
  
          # Evaluation
          if epoch %verbose == 0:
              (hits, ndcgs) = evaluate_model(model, testRatings, testNegatives, topK, evaluation_threads)
              hr, ndcg, loss = np.array(hits).mean(), np.array(ndcgs).mean(), hist.history['loss'][0]
              metrics[f'hr@{topK}'].append(hr)
              metrics['ndcg'].append( ndcg)
              metrics['loss_eval'].append( loss)
              duration_eval = time()-t2
              metrics['duration_eval'].append(duration_eval)
  
              print(('Iteration %d [%.1f s]: HR = %.4f, NDCG = %.4f, loss = %.4f [%.1f s]' 
                    % (epoch,  duration, hr, ndcg, loss, duration_eval)))
              if hr > best_hr:
                  best_hr, best_ndcg, best_iter = hr, ndcg, epoch
                  if args.out > 0:
                      model.save_weights(model_out_file, overwrite=True)
  
      print(("End. Best Iteration %d:  HR = %.4f, NDCG = %.4f. " %(best_iter, best_hr, best_ndcg)))
      if args.out > 0:
          print(("The best GMF model is saved to %s" %(model_out_file)))
  
      hyperparams = {}
      hyperparams['num_factors'] = num_factors
      hyperparams['num_negatives'] = num_negatives
      hyperparams['regs'] = args.regs
      hyperparams['lr'] = args.lr
      hyperparams['learner'] = args.learner
  
      exporter = MetricsExporter()
      exporter.dataset_name  = args.dataset
      exporter.model_name = 'GMF'
      exporter.hyperparams = hyperparams
      exporter.export(metrics)