Module reclab.recommenders.llorma.llorma_lib.llorma_g
The package for the Global LLORMA recommender. Code modified from https://github.com/JoonyoungYi/LLORMA-tensorflow
Expand source code
"""
The package for the Global LLORMA recommender.
Code modified from https://github.com/JoonyoungYi/LLORMA-tensorflow
"""
import os
import random
import warnings
import numpy as np
import tensorflow as tf
from .anchor import AnchorManager
from .train_utils import get_train_op, init_latent_mat, init_session
tf.compat.v1.disable_eager_execution()
class Llorma():
"""Local Low Rank Matrix Approximation Model
Parameters
----------
max_user : int
Maximum number of users in the environment
max_item : int
Maximum number of items in the environment
n_anchor : int, optional
number of anchor-points, by default 10
pre_rank : int, optional
latent-dimension of the matrix-factorization model
used for pre-training, by default 5
pre_learning_rate : float, optional
learning rate used to fit the global pre-train model,
by default 2e-4
pre_lambda_val : float, optional
regularization parameter for pre-training,
by default 10
pre_train_steps : int, optional
number of gradient steps used for pretraining,
by default 100
rank : int, optional
latent-dimension of the local models, by default 10
learning_rate : float, optional
learning rate used to fit local models, by default 1e-2
lambda_val : float, optional
regularization parameter for the local models,
by default 1e-3
train_steps : int, optional
number of train epochs for fitting local models,
by default 1000
batch_size : int, optional
the batch size used when fitting local models,
by default 1024
use_cache : bool, optional
If True use old saved models of the pre-train step,
by default True
result_path : str, optional
directory name where model data will be saved,
by default 'results'
kernel_fun : callable, optional
kernel function used for similarity,
by_default None
"""
def __init__(self, max_user, max_item, n_anchor=10, pre_rank=5,
pre_learning_rate=2e-4, pre_lambda_val=10,
pre_train_steps=100, rank=10, learning_rate=1e-2,
lambda_val=1e-3, train_steps=1000, batch_size=1024,
use_cache=True, result_path='results', kernel_fun=None):
""" Initialize a LLORMA recommender
"""
self.max_user = max_user
self.max_item = max_item
self.n_anchor = n_anchor
self.pre_rank = pre_rank
self.pre_learning_rate = pre_learning_rate
self.pre_lambda_val = pre_lambda_val
self.pre_train_steps = pre_train_steps
self.rank = rank
self.learning_rate = learning_rate
self.lambda_val = lambda_val
self.train_steps = train_steps
self.batch_size = batch_size
self.use_cache = use_cache
self.result_path = result_path
self.kernel_fun = kernel_fun
self.user_latent_init = None
self.item_latent_init = None
self.batch_manager = None
self.anchor_manager = None
self.session = None
self.model = None
self.pre_model = None
self.model = None
def reset_data(self, train_data, valid_data, test_data):
""" Reset the data of a recommender by instantiating a
new BatchManager or modifying the existing one
Parameters
----------
train_data : Array-like, shape (N_train,3)
Training data, each row is of the form
(user_id, item_id, rating)
valid_data : Array-like, shape (N_valid, 3)
Validation data, each row is of the form
(user_id, item_id, rating)
test_data : Array-like, shape (N_test, 3)
Test data, each row is of the form
(user_id, item_idm rating)
"""
if not self.batch_manager:
self.batch_manager = BatchManager(train_data, valid_data, test_data)
else:
self.batch_manager.update(train_data, valid_data, test_data)
N_ratings = self.batch_manager.train_data.shape[0]
if N_ratings < self.n_anchor:
warnings.warn("The data has fewer ratings than anchor points: {}<{}".format(
N_ratings, self.n_anchor))
self.n_anchor = N_ratings
def init_pre_model(self):
""" Initialize TF variables, loss, objective and
optimizer for the global pre-model
"""
u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u')
i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i')
r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r')
p_factor = init_latent_mat(self.max_user,
self.pre_rank,
self.batch_manager.mu,
self.batch_manager.std)
q_factor = init_latent_mat(self.max_item,
self.pre_rank,
self.batch_manager.mu,
self.batch_manager.std)
p_lookup = tf.nn.embedding_lookup(p_factor, u_var)
q_lookup = tf.nn.embedding_lookup(q_factor, i_var)
r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), 1)
reg_loss = tf.add_n([tf.reduce_sum(tf.square(p_factor)),
tf.reduce_sum(tf.square(q_factor))])
loss = tf.reduce_sum(tf.square(r_var - r_hat)) + self.pre_lambda_val * reg_loss
rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat)))
optimizer = tf.compat.v1.train.MomentumOptimizer(self.pre_learning_rate, 0.9)
train_ops = [
optimizer.minimize(loss, var_list=[p_factor]),
optimizer.minimize(loss, var_list=[q_factor])
]
return {
'u': u_var,
'i': i_var,
'r': r_var,
'train_ops': train_ops,
'loss': loss,
'rmse': rmse,
'p': p_factor,
'q': q_factor,
}
def init_model(self):
""" Initialize TF variables, loss, objective and
optimizer for the local models
"""
u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u')
i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i')
r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r')
k_var = tf.compat.v1.placeholder(tf.float64, [None, self.n_anchor], name='k')
k_sum = tf.reduce_sum(k_var, axis=1)
# init weights
all_p_factors, all_q_factors, r_hats = [], [], []
for _ in range(self.n_anchor):
p_factor = init_latent_mat(self.max_user,
self.rank,
self.batch_manager.mu,
self.batch_manager.std)
q_factor = init_latent_mat(self.max_item,
self.rank,
self.batch_manager.mu,
self.batch_manager.std)
all_p_factors.append(p_factor)
all_q_factors.append(q_factor)
p_lookup = tf.nn.embedding_lookup(p_factor, u_var)
q_lookup = tf.nn.embedding_lookup(q_factor, i_var)
r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), axis=1)
r_hats.append(r_hat)
r_hat = tf.reduce_sum(tf.multiply(k_var, tf.stack(r_hats, axis=1)), axis=1)
r_hat = tf.where(tf.greater(k_sum, 1e-2), r_hat, tf.ones_like(r_hat) * 3)
rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat)))
optimizer = tf.compat.v1.train.GradientDescentOptimizer(self.learning_rate)
loss = tf.reduce_sum(tf.square(r_hat - r_var)) + self.lambda_val * tf.reduce_sum(
[tf.reduce_sum(tf.square(p_or_q)) for p_or_q in all_p_factors + all_q_factors])
train_ops = [get_train_op(optimizer, loss, [p, q])
for p, q in zip(all_p_factors, all_q_factors)]
return {
'u': u_var,
'i': i_var,
'r': r_var,
'k': k_var,
'train_ops': train_ops,
'rmse': rmse,
'r_hat': r_hat,
}
def _get_rmse_pre_model(self, cur_session, pre_model):
""" Helper method to compute RMSE of the pre-model
Parameters
----------
cur_session : obj: tf.session
TensorFlow session to use for computation
pre_model : Dict-like
Dictionary of TF variables, train operations
Typically the output of self.init_pre_model()
Returns
-------
(float, float)
The validation and test set RMSE
"""
valid_rmse = cur_session.run(
pre_model['rmse'],
feed_dict={
pre_model['u']: self.batch_manager.valid_data[:, 0],
pre_model['i']: self.batch_manager.valid_data[:, 1],
pre_model['r']: self.batch_manager.valid_data[:, 2]
})
test_rmse = cur_session.run(
pre_model['rmse'],
feed_dict={
pre_model['u']: self.batch_manager.test_data[:, 0],
pre_model['i']: self.batch_manager.test_data[:, 1],
pre_model['r']: self.batch_manager.test_data[:, 2]
})
return valid_rmse, test_rmse
def _get_rmse_model(self, cur_session, model, valid_k, test_k):
""" Compute the RMSE for the ensamble model of local models
Parameters
----------
cur_session : obj: tf.session
TensorFlow session to use for computation
model : Dict-like
Dictionary of TF variables, train operations
Typically the output of self.init_model()
valid_k : Array-like, shape (N_valid,)
Kernel weight values for each user-item pair
test_k : Array-like, shape (N_test,)
Kernel weight values for each user-item pair
Returns
-------
(float, float)
The validation and test set RMSE
"""
valid_rmse = cur_session.run(
model['rmse'],
feed_dict={
model['u']: self.batch_manager.valid_data[:, 0],
model['i']: self.batch_manager.valid_data[:, 1],
model['r']: self.batch_manager.valid_data[:, 2],
model['k']: valid_k,
})
test_rmse = cur_session.run(
model['rmse'],
feed_dict={
model['u']: self.batch_manager.test_data[:, 0],
model['i']: self.batch_manager.test_data[:, 1],
model['r']: self.batch_manager.test_data[:, 2],
model['k']: test_k,
})
return valid_rmse, test_rmse
def pre_train(self): # noqa: R0914
"""Pre-train a Matrix Factorization model for the full data
"""
# if self.use_cache:
# # check if the pre-train factor are already initialized from a previous iteration
# if (self.user_latent_init is not None) and (self.item_latent_init is not None):
# return
# if self.pre_model is None:
# self.pre_model = self.init_pre_model()
tf.compat.v1.reset_default_graph()
self.pre_model = self.init_pre_model()
pre_model = self.pre_model
pre_session = tf.compat.v1.Session()
pre_session.run(tf.compat.v1.global_variables_initializer())
min_valid_rmse = float('Inf')
random_model_idx = random.randint(0, 1000000)
file_path = '{}/pre-model-{}.ckpt'.format(self.result_path, random_model_idx)
train_data = self.batch_manager.train_data
u_vec = train_data[:, 0]
i_vec = train_data[:, 1]
r_vec = train_data[:, 2]
#saver = tf.train.Saver()
for itr in range(self.pre_train_steps):
for train_op in pre_model['train_ops']:
pre_session.run((train_op, pre_model['loss'], pre_model['rmse']),
feed_dict={pre_model['u']: u_vec,
pre_model['i']: i_vec,
pre_model['r']: r_vec})
if (itr+1)%10==0:
valid_rmse, _ = self._get_rmse_pre_model(pre_session, pre_model)
print('Pre-train step: {}, train_error:{}'.format(itr+1, valid_rmse))
# if valid_rmse <= min_valid_rmse:
# min_valid_rmse = valid_rmse
# min_valid_iter = itr
# #saver.save(pre_session, file_path)
#saver.restore(pre_session, file_path)
p_factor, q_factor = pre_session.run(
(pre_model['p'], pre_model['q']),
feed_dict={
pre_model['u']: self.batch_manager.train_data[:, 0],
pre_model['i']: self.batch_manager.train_data[:, 1],
pre_model['r']: self.batch_manager.train_data[:, 2]
})
if not os.path.exists(self.result_path):
os.makedirs(self.result_path)
np.save('{}/pre_train_p.npy'.format(self.result_path), p_factor)
np.save('{}/pre_train_q.npy'.format(self.result_path), q_factor)
pre_session.close()
self.user_latent_init = p_factor
self.item_latent_init = q_factor
def train(self): # noqa: R0914
""" Train the LLORMA recommender
"""
self.pre_train()
#tf.reset_default_graph()
self.model = self.init_model()
model = self.model
self.anchor_manager = AnchorManager(self.n_anchor,
self.batch_manager,
self.user_latent_init,
self.item_latent_init,
self.kernel_fun)
session = init_session()
local_models = [
LocalModel(session, model, anchor_idx, self.anchor_manager, self.batch_manager)
for anchor_idx in range(self.n_anchor)
]
train_k = _get_local_k(local_models, kind='train')
valid_k = _get_local_k(local_models, kind='valid')
test_k = _get_local_k(local_models, kind='test')
min_valid_rmse = float('Inf')
min_valid_iter = 0
train_data = self.batch_manager.train_data
#saver = tf.train.Saver()
for itr in range(self.train_steps):
file_path = '{}/model-{}.ckpt'.format(self.result_path, itr)
for start_m in range(0, train_data.shape[0], self.batch_size):
end_m = min(start_m + self.batch_size, train_data.shape[0])
u_vec = train_data[start_m:end_m, 0]
i_vec = train_data[start_m:end_m, 1]
r_vec = train_data[start_m:end_m, 2]
k_vec = train_k[start_m:end_m, :]
results = session.run(
[model['rmse']] + model['train_ops'],
feed_dict={
model['u']: u_vec,
model['i']: i_vec,
model['r']: r_vec,
model['k']: k_vec,
})
if (itr+1)%10==0:
valid_rmse, test_rmse = self._get_rmse_model(session, model,
valid_k, test_k)
print("Train step:{}, train error: {}, test error: {}".format(itr+1, test_rmse, valid_rmse))
# if valid_rmse < min_valid_rmse:
# min_valid_rmse = valid_rmse
# min_valid_iter = itr
# #saver.save(session, file_path)
# #saver.restore(session, file_path)
# if itr >= min_valid_iter + 100:
# break
self.session = session
return(session, model)
def predict(self, user_items):
"""Given user-item pairs predict the rating
Parameters
----------
user_items : Array-like, shape (N, 2)
Each row is an (user, item) pair
Returns
-------
np.ndarray, shape (N,)
Predicted ratings
"""
session = self.session
model = self.model
predict_k = np.stack(
[
self.anchor_manager.get_k(anchor_idx, user_items)
for anchor_idx in range(len(self.anchor_manager.anchor_idxs))
],
axis=1)
predict_k = np.clip(predict_k, 0.0, 1.0)
predict_k = np.divide(predict_k, np.sum(predict_k, axis=1, keepdims=1))
predict_k[np.isnan(predict_k)] = 0
predict_r_hat = session.run(
model['r_hat'],
feed_dict={
model['u']: user_items[:, 0],
model['i']: user_items[:, 1],
model['k']: predict_k
})
return predict_r_hat
class LocalModel: # noqa: R0903
"""LocalModel
Parameters
----------
session : obj: tf.session
TF session
model : Dict-like
Dictionary of TF variables, train operations
Typically the output of self.init_pre_model()
anchor_idx : int
Id of the anchor point for the local model
anchor_manager : obj: AnchorManager
batch_manager : obj: BatchManager
"""
def __init__(self, session, model, anchor_idx, anchor_manager,
batch_manager):
""" Instantiate a local model
"""
self.session = session
self.model = model
self.batch_manager = batch_manager
self.anchor_idx = anchor_idx
self.anchor_manager = anchor_manager
#print('>> update k in anchor_idx [{}].'.format(anchor_idx))
self.train_k = anchor_manager.get_train_k(anchor_idx)
self.valid_k = anchor_manager.get_valid_k(anchor_idx)
self.test_k = anchor_manager.get_test_k(anchor_idx)
def _get_local_k(local_models, kind='train'):
"""Get kernel weights for the local models
Parameters
----------
local_models : Array-like
A list of LocalModel objects
kind : str, optional
type of data to get kernel weights for,
possible values are: 'train', 'valid', 'test'
by default 'train'
Returns
-------
np.ndarray, shape (N_local_models, N_ratings)
Matrix of kernel weights for each local model
for each user-item pair in the train/valid/test data
"""
k = np.stack(
[
getattr(local_model, '{}_k'.format(kind))
for local_model in local_models
],
axis=1)
k = np.clip(k, 0.0, 1.0)
k = np.divide(k, np.sum(k, axis=1, keepdims=1))
k[np.isnan(k)] = 0
return k
class BatchManager: # noqa: R0903
"""BatchManager Class to manage the train-valid-test datasets
Parameters
----------
train_data : Array-like, shape [N_train, 3]
Each row is of the form (user_id, item_id, rating)
valid_data : Array-like, shape [N_valid, 3]
Each row is of the form (user_id, item_id, rating)
test_data : Array-like, shape [N_test, 3]
Each row is of the form (user_id, item_id, rating)
"""
def __init__(self, train_data, valid_data, test_data):
"""Instantiate a BatchManager
"""
self.train_data = train_data
self.valid_data = valid_data
self.test_data = test_data
self._set_params()
def _set_params(self):
"""Private method to set the number of users, number of items,
mean and standard deviation attributes
"""
self.n_user = int(
max(
np.max(self.train_data[:, 0]),
np.max(self.valid_data[:, 0]), np.max(self.test_data[:,
0]))) + 1
self.n_item = int(
max(
np.max(self.train_data[:, 1]),
np.max(self.valid_data[:, 1]), np.max(self.test_data[:,
1]))) + 1
self.mu = np.mean(self.train_data[:, 2])
self.std = np.std(self.train_data[:, 2])
def update(self, train_data, valid_data=None, test_data=None):
""" Update the data
Parameters
----------
train_data : Array-like, shape [N_train, 3]
Each row is of the form (user_id, item_id, rating)
valid_data : [Array-like, shape [N_valid, 3], optional
Each row is of the form (user_id, item_id, rating),
by default None
test_data : Array-like, shape [N_test, 3], optional
Each row is of the form (user_id, item_id, rating)
by default None
"""
self.train_data = train_data
if valid_data is not None:
self.valid_data = valid_data
if test_data is not None:
self.test_data = test_data
self._set_params()
Classes
class BatchManager (train_data, valid_data, test_data)
-
BatchManager Class to manage the train-valid-test datasets
Parameters
train_data
:Array-like, shape [N_train, 3]
- Each row is of the form (user_id, item_id, rating)
valid_data
:Array-like, shape [N_valid, 3]
- Each row is of the form (user_id, item_id, rating)
test_data
:Array-like, shape [N_test, 3]
- Each row is of the form (user_id, item_id, rating)
Instantiate a BatchManager
Expand source code
class BatchManager: # noqa: R0903 """BatchManager Class to manage the train-valid-test datasets Parameters ---------- train_data : Array-like, shape [N_train, 3] Each row is of the form (user_id, item_id, rating) valid_data : Array-like, shape [N_valid, 3] Each row is of the form (user_id, item_id, rating) test_data : Array-like, shape [N_test, 3] Each row is of the form (user_id, item_id, rating) """ def __init__(self, train_data, valid_data, test_data): """Instantiate a BatchManager """ self.train_data = train_data self.valid_data = valid_data self.test_data = test_data self._set_params() def _set_params(self): """Private method to set the number of users, number of items, mean and standard deviation attributes """ self.n_user = int( max( np.max(self.train_data[:, 0]), np.max(self.valid_data[:, 0]), np.max(self.test_data[:, 0]))) + 1 self.n_item = int( max( np.max(self.train_data[:, 1]), np.max(self.valid_data[:, 1]), np.max(self.test_data[:, 1]))) + 1 self.mu = np.mean(self.train_data[:, 2]) self.std = np.std(self.train_data[:, 2]) def update(self, train_data, valid_data=None, test_data=None): """ Update the data Parameters ---------- train_data : Array-like, shape [N_train, 3] Each row is of the form (user_id, item_id, rating) valid_data : [Array-like, shape [N_valid, 3], optional Each row is of the form (user_id, item_id, rating), by default None test_data : Array-like, shape [N_test, 3], optional Each row is of the form (user_id, item_id, rating) by default None """ self.train_data = train_data if valid_data is not None: self.valid_data = valid_data if test_data is not None: self.test_data = test_data self._set_params()
Methods
def update(self, train_data, valid_data=None, test_data=None)
-
Update the data
Parameters
train_data
:Array-like, shape [N_train, 3]
- Each row is of the form (user_id, item_id, rating)
valid_data
:[Array-like, shape [N_valid, 3]
, optional- Each row is of the form (user_id, item_id, rating), by default None
test_data
:Array-like, shape [N_test, 3]
, optional- Each row is of the form (user_id, item_id, rating) by default None
Expand source code
def update(self, train_data, valid_data=None, test_data=None): """ Update the data Parameters ---------- train_data : Array-like, shape [N_train, 3] Each row is of the form (user_id, item_id, rating) valid_data : [Array-like, shape [N_valid, 3], optional Each row is of the form (user_id, item_id, rating), by default None test_data : Array-like, shape [N_test, 3], optional Each row is of the form (user_id, item_id, rating) by default None """ self.train_data = train_data if valid_data is not None: self.valid_data = valid_data if test_data is not None: self.test_data = test_data self._set_params()
class Llorma (max_user, max_item, n_anchor=10, pre_rank=5, pre_learning_rate=0.0002, pre_lambda_val=10, pre_train_steps=100, rank=10, learning_rate=0.01, lambda_val=0.001, train_steps=1000, batch_size=1024, use_cache=True, result_path='results', kernel_fun=None)
-
Local Low Rank Matrix Approximation Model
Parameters
max_user
:int
- Maximum number of users in the environment
- max_item : int
- Maximum number of items in the environment
n_anchor
:int
, optional- number of anchor-points, by default 10
pre_rank
:int
, optional- latent-dimension of the matrix-factorization model used for pre-training, by default 5
pre_learning_rate
:float
, optional- learning rate used to fit the global pre-train model, by default 2e-4
pre_lambda_val
:float
, optional- regularization parameter for pre-training, by default 10
pre_train_steps
:int
, optional- number of gradient steps used for pretraining, by default 100
rank
:int
, optional- latent-dimension of the local models, by default 10
learning_rate
:float
, optional- learning rate used to fit local models, by default 1e-2
lambda_val
:float
, optional- regularization parameter for the local models, by default 1e-3
train_steps
:int
, optional- number of train epochs for fitting local models, by default 1000
batch_size
:int
, optional- the batch size used when fitting local models, by default 1024
use_cache
:bool
, optional- If True use old saved models of the pre-train step, by default True
result_path
:str
, optional- directory name where model data will be saved, by default 'results'
kernel_fun
:callable
, optional- kernel function used for similarity, by_default None
Initialize a LLORMA recommender
Expand source code
class Llorma(): """Local Low Rank Matrix Approximation Model Parameters ---------- max_user : int Maximum number of users in the environment max_item : int Maximum number of items in the environment n_anchor : int, optional number of anchor-points, by default 10 pre_rank : int, optional latent-dimension of the matrix-factorization model used for pre-training, by default 5 pre_learning_rate : float, optional learning rate used to fit the global pre-train model, by default 2e-4 pre_lambda_val : float, optional regularization parameter for pre-training, by default 10 pre_train_steps : int, optional number of gradient steps used for pretraining, by default 100 rank : int, optional latent-dimension of the local models, by default 10 learning_rate : float, optional learning rate used to fit local models, by default 1e-2 lambda_val : float, optional regularization parameter for the local models, by default 1e-3 train_steps : int, optional number of train epochs for fitting local models, by default 1000 batch_size : int, optional the batch size used when fitting local models, by default 1024 use_cache : bool, optional If True use old saved models of the pre-train step, by default True result_path : str, optional directory name where model data will be saved, by default 'results' kernel_fun : callable, optional kernel function used for similarity, by_default None """ def __init__(self, max_user, max_item, n_anchor=10, pre_rank=5, pre_learning_rate=2e-4, pre_lambda_val=10, pre_train_steps=100, rank=10, learning_rate=1e-2, lambda_val=1e-3, train_steps=1000, batch_size=1024, use_cache=True, result_path='results', kernel_fun=None): """ Initialize a LLORMA recommender """ self.max_user = max_user self.max_item = max_item self.n_anchor = n_anchor self.pre_rank = pre_rank self.pre_learning_rate = pre_learning_rate self.pre_lambda_val = pre_lambda_val self.pre_train_steps = pre_train_steps self.rank = rank self.learning_rate = learning_rate self.lambda_val = lambda_val self.train_steps = train_steps self.batch_size = batch_size self.use_cache = use_cache self.result_path = result_path self.kernel_fun = kernel_fun self.user_latent_init = None self.item_latent_init = None self.batch_manager = None self.anchor_manager = None self.session = None self.model = None self.pre_model = None self.model = None def reset_data(self, train_data, valid_data, test_data): """ Reset the data of a recommender by instantiating a new BatchManager or modifying the existing one Parameters ---------- train_data : Array-like, shape (N_train,3) Training data, each row is of the form (user_id, item_id, rating) valid_data : Array-like, shape (N_valid, 3) Validation data, each row is of the form (user_id, item_id, rating) test_data : Array-like, shape (N_test, 3) Test data, each row is of the form (user_id, item_idm rating) """ if not self.batch_manager: self.batch_manager = BatchManager(train_data, valid_data, test_data) else: self.batch_manager.update(train_data, valid_data, test_data) N_ratings = self.batch_manager.train_data.shape[0] if N_ratings < self.n_anchor: warnings.warn("The data has fewer ratings than anchor points: {}<{}".format( N_ratings, self.n_anchor)) self.n_anchor = N_ratings def init_pre_model(self): """ Initialize TF variables, loss, objective and optimizer for the global pre-model """ u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u') i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i') r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r') p_factor = init_latent_mat(self.max_user, self.pre_rank, self.batch_manager.mu, self.batch_manager.std) q_factor = init_latent_mat(self.max_item, self.pre_rank, self.batch_manager.mu, self.batch_manager.std) p_lookup = tf.nn.embedding_lookup(p_factor, u_var) q_lookup = tf.nn.embedding_lookup(q_factor, i_var) r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), 1) reg_loss = tf.add_n([tf.reduce_sum(tf.square(p_factor)), tf.reduce_sum(tf.square(q_factor))]) loss = tf.reduce_sum(tf.square(r_var - r_hat)) + self.pre_lambda_val * reg_loss rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat))) optimizer = tf.compat.v1.train.MomentumOptimizer(self.pre_learning_rate, 0.9) train_ops = [ optimizer.minimize(loss, var_list=[p_factor]), optimizer.minimize(loss, var_list=[q_factor]) ] return { 'u': u_var, 'i': i_var, 'r': r_var, 'train_ops': train_ops, 'loss': loss, 'rmse': rmse, 'p': p_factor, 'q': q_factor, } def init_model(self): """ Initialize TF variables, loss, objective and optimizer for the local models """ u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u') i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i') r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r') k_var = tf.compat.v1.placeholder(tf.float64, [None, self.n_anchor], name='k') k_sum = tf.reduce_sum(k_var, axis=1) # init weights all_p_factors, all_q_factors, r_hats = [], [], [] for _ in range(self.n_anchor): p_factor = init_latent_mat(self.max_user, self.rank, self.batch_manager.mu, self.batch_manager.std) q_factor = init_latent_mat(self.max_item, self.rank, self.batch_manager.mu, self.batch_manager.std) all_p_factors.append(p_factor) all_q_factors.append(q_factor) p_lookup = tf.nn.embedding_lookup(p_factor, u_var) q_lookup = tf.nn.embedding_lookup(q_factor, i_var) r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), axis=1) r_hats.append(r_hat) r_hat = tf.reduce_sum(tf.multiply(k_var, tf.stack(r_hats, axis=1)), axis=1) r_hat = tf.where(tf.greater(k_sum, 1e-2), r_hat, tf.ones_like(r_hat) * 3) rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat))) optimizer = tf.compat.v1.train.GradientDescentOptimizer(self.learning_rate) loss = tf.reduce_sum(tf.square(r_hat - r_var)) + self.lambda_val * tf.reduce_sum( [tf.reduce_sum(tf.square(p_or_q)) for p_or_q in all_p_factors + all_q_factors]) train_ops = [get_train_op(optimizer, loss, [p, q]) for p, q in zip(all_p_factors, all_q_factors)] return { 'u': u_var, 'i': i_var, 'r': r_var, 'k': k_var, 'train_ops': train_ops, 'rmse': rmse, 'r_hat': r_hat, } def _get_rmse_pre_model(self, cur_session, pre_model): """ Helper method to compute RMSE of the pre-model Parameters ---------- cur_session : obj: tf.session TensorFlow session to use for computation pre_model : Dict-like Dictionary of TF variables, train operations Typically the output of self.init_pre_model() Returns ------- (float, float) The validation and test set RMSE """ valid_rmse = cur_session.run( pre_model['rmse'], feed_dict={ pre_model['u']: self.batch_manager.valid_data[:, 0], pre_model['i']: self.batch_manager.valid_data[:, 1], pre_model['r']: self.batch_manager.valid_data[:, 2] }) test_rmse = cur_session.run( pre_model['rmse'], feed_dict={ pre_model['u']: self.batch_manager.test_data[:, 0], pre_model['i']: self.batch_manager.test_data[:, 1], pre_model['r']: self.batch_manager.test_data[:, 2] }) return valid_rmse, test_rmse def _get_rmse_model(self, cur_session, model, valid_k, test_k): """ Compute the RMSE for the ensamble model of local models Parameters ---------- cur_session : obj: tf.session TensorFlow session to use for computation model : Dict-like Dictionary of TF variables, train operations Typically the output of self.init_model() valid_k : Array-like, shape (N_valid,) Kernel weight values for each user-item pair test_k : Array-like, shape (N_test,) Kernel weight values for each user-item pair Returns ------- (float, float) The validation and test set RMSE """ valid_rmse = cur_session.run( model['rmse'], feed_dict={ model['u']: self.batch_manager.valid_data[:, 0], model['i']: self.batch_manager.valid_data[:, 1], model['r']: self.batch_manager.valid_data[:, 2], model['k']: valid_k, }) test_rmse = cur_session.run( model['rmse'], feed_dict={ model['u']: self.batch_manager.test_data[:, 0], model['i']: self.batch_manager.test_data[:, 1], model['r']: self.batch_manager.test_data[:, 2], model['k']: test_k, }) return valid_rmse, test_rmse def pre_train(self): # noqa: R0914 """Pre-train a Matrix Factorization model for the full data """ # if self.use_cache: # # check if the pre-train factor are already initialized from a previous iteration # if (self.user_latent_init is not None) and (self.item_latent_init is not None): # return # if self.pre_model is None: # self.pre_model = self.init_pre_model() tf.compat.v1.reset_default_graph() self.pre_model = self.init_pre_model() pre_model = self.pre_model pre_session = tf.compat.v1.Session() pre_session.run(tf.compat.v1.global_variables_initializer()) min_valid_rmse = float('Inf') random_model_idx = random.randint(0, 1000000) file_path = '{}/pre-model-{}.ckpt'.format(self.result_path, random_model_idx) train_data = self.batch_manager.train_data u_vec = train_data[:, 0] i_vec = train_data[:, 1] r_vec = train_data[:, 2] #saver = tf.train.Saver() for itr in range(self.pre_train_steps): for train_op in pre_model['train_ops']: pre_session.run((train_op, pre_model['loss'], pre_model['rmse']), feed_dict={pre_model['u']: u_vec, pre_model['i']: i_vec, pre_model['r']: r_vec}) if (itr+1)%10==0: valid_rmse, _ = self._get_rmse_pre_model(pre_session, pre_model) print('Pre-train step: {}, train_error:{}'.format(itr+1, valid_rmse)) # if valid_rmse <= min_valid_rmse: # min_valid_rmse = valid_rmse # min_valid_iter = itr # #saver.save(pre_session, file_path) #saver.restore(pre_session, file_path) p_factor, q_factor = pre_session.run( (pre_model['p'], pre_model['q']), feed_dict={ pre_model['u']: self.batch_manager.train_data[:, 0], pre_model['i']: self.batch_manager.train_data[:, 1], pre_model['r']: self.batch_manager.train_data[:, 2] }) if not os.path.exists(self.result_path): os.makedirs(self.result_path) np.save('{}/pre_train_p.npy'.format(self.result_path), p_factor) np.save('{}/pre_train_q.npy'.format(self.result_path), q_factor) pre_session.close() self.user_latent_init = p_factor self.item_latent_init = q_factor def train(self): # noqa: R0914 """ Train the LLORMA recommender """ self.pre_train() #tf.reset_default_graph() self.model = self.init_model() model = self.model self.anchor_manager = AnchorManager(self.n_anchor, self.batch_manager, self.user_latent_init, self.item_latent_init, self.kernel_fun) session = init_session() local_models = [ LocalModel(session, model, anchor_idx, self.anchor_manager, self.batch_manager) for anchor_idx in range(self.n_anchor) ] train_k = _get_local_k(local_models, kind='train') valid_k = _get_local_k(local_models, kind='valid') test_k = _get_local_k(local_models, kind='test') min_valid_rmse = float('Inf') min_valid_iter = 0 train_data = self.batch_manager.train_data #saver = tf.train.Saver() for itr in range(self.train_steps): file_path = '{}/model-{}.ckpt'.format(self.result_path, itr) for start_m in range(0, train_data.shape[0], self.batch_size): end_m = min(start_m + self.batch_size, train_data.shape[0]) u_vec = train_data[start_m:end_m, 0] i_vec = train_data[start_m:end_m, 1] r_vec = train_data[start_m:end_m, 2] k_vec = train_k[start_m:end_m, :] results = session.run( [model['rmse']] + model['train_ops'], feed_dict={ model['u']: u_vec, model['i']: i_vec, model['r']: r_vec, model['k']: k_vec, }) if (itr+1)%10==0: valid_rmse, test_rmse = self._get_rmse_model(session, model, valid_k, test_k) print("Train step:{}, train error: {}, test error: {}".format(itr+1, test_rmse, valid_rmse)) # if valid_rmse < min_valid_rmse: # min_valid_rmse = valid_rmse # min_valid_iter = itr # #saver.save(session, file_path) # #saver.restore(session, file_path) # if itr >= min_valid_iter + 100: # break self.session = session return(session, model) def predict(self, user_items): """Given user-item pairs predict the rating Parameters ---------- user_items : Array-like, shape (N, 2) Each row is an (user, item) pair Returns ------- np.ndarray, shape (N,) Predicted ratings """ session = self.session model = self.model predict_k = np.stack( [ self.anchor_manager.get_k(anchor_idx, user_items) for anchor_idx in range(len(self.anchor_manager.anchor_idxs)) ], axis=1) predict_k = np.clip(predict_k, 0.0, 1.0) predict_k = np.divide(predict_k, np.sum(predict_k, axis=1, keepdims=1)) predict_k[np.isnan(predict_k)] = 0 predict_r_hat = session.run( model['r_hat'], feed_dict={ model['u']: user_items[:, 0], model['i']: user_items[:, 1], model['k']: predict_k }) return predict_r_hat
Methods
def init_model(self)
-
Initialize TF variables, loss, objective and optimizer for the local models
Expand source code
def init_model(self): """ Initialize TF variables, loss, objective and optimizer for the local models """ u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u') i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i') r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r') k_var = tf.compat.v1.placeholder(tf.float64, [None, self.n_anchor], name='k') k_sum = tf.reduce_sum(k_var, axis=1) # init weights all_p_factors, all_q_factors, r_hats = [], [], [] for _ in range(self.n_anchor): p_factor = init_latent_mat(self.max_user, self.rank, self.batch_manager.mu, self.batch_manager.std) q_factor = init_latent_mat(self.max_item, self.rank, self.batch_manager.mu, self.batch_manager.std) all_p_factors.append(p_factor) all_q_factors.append(q_factor) p_lookup = tf.nn.embedding_lookup(p_factor, u_var) q_lookup = tf.nn.embedding_lookup(q_factor, i_var) r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), axis=1) r_hats.append(r_hat) r_hat = tf.reduce_sum(tf.multiply(k_var, tf.stack(r_hats, axis=1)), axis=1) r_hat = tf.where(tf.greater(k_sum, 1e-2), r_hat, tf.ones_like(r_hat) * 3) rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat))) optimizer = tf.compat.v1.train.GradientDescentOptimizer(self.learning_rate) loss = tf.reduce_sum(tf.square(r_hat - r_var)) + self.lambda_val * tf.reduce_sum( [tf.reduce_sum(tf.square(p_or_q)) for p_or_q in all_p_factors + all_q_factors]) train_ops = [get_train_op(optimizer, loss, [p, q]) for p, q in zip(all_p_factors, all_q_factors)] return { 'u': u_var, 'i': i_var, 'r': r_var, 'k': k_var, 'train_ops': train_ops, 'rmse': rmse, 'r_hat': r_hat, }
def init_pre_model(self)
-
Initialize TF variables, loss, objective and optimizer for the global pre-model
Expand source code
def init_pre_model(self): """ Initialize TF variables, loss, objective and optimizer for the global pre-model """ u_var = tf.compat.v1.placeholder(tf.int64, [None], name='u') i_var = tf.compat.v1.placeholder(tf.int64, [None], name='i') r_var = tf.compat.v1.placeholder(tf.float64, [None], name='r') p_factor = init_latent_mat(self.max_user, self.pre_rank, self.batch_manager.mu, self.batch_manager.std) q_factor = init_latent_mat(self.max_item, self.pre_rank, self.batch_manager.mu, self.batch_manager.std) p_lookup = tf.nn.embedding_lookup(p_factor, u_var) q_lookup = tf.nn.embedding_lookup(q_factor, i_var) r_hat = tf.reduce_sum(tf.multiply(p_lookup, q_lookup), 1) reg_loss = tf.add_n([tf.reduce_sum(tf.square(p_factor)), tf.reduce_sum(tf.square(q_factor))]) loss = tf.reduce_sum(tf.square(r_var - r_hat)) + self.pre_lambda_val * reg_loss rmse = tf.sqrt(tf.reduce_mean(tf.square(r_var - r_hat))) optimizer = tf.compat.v1.train.MomentumOptimizer(self.pre_learning_rate, 0.9) train_ops = [ optimizer.minimize(loss, var_list=[p_factor]), optimizer.minimize(loss, var_list=[q_factor]) ] return { 'u': u_var, 'i': i_var, 'r': r_var, 'train_ops': train_ops, 'loss': loss, 'rmse': rmse, 'p': p_factor, 'q': q_factor, }
def pre_train(self)
-
Pre-train a Matrix Factorization model for the full data
Expand source code
def pre_train(self): # noqa: R0914 """Pre-train a Matrix Factorization model for the full data """ # if self.use_cache: # # check if the pre-train factor are already initialized from a previous iteration # if (self.user_latent_init is not None) and (self.item_latent_init is not None): # return # if self.pre_model is None: # self.pre_model = self.init_pre_model() tf.compat.v1.reset_default_graph() self.pre_model = self.init_pre_model() pre_model = self.pre_model pre_session = tf.compat.v1.Session() pre_session.run(tf.compat.v1.global_variables_initializer()) min_valid_rmse = float('Inf') random_model_idx = random.randint(0, 1000000) file_path = '{}/pre-model-{}.ckpt'.format(self.result_path, random_model_idx) train_data = self.batch_manager.train_data u_vec = train_data[:, 0] i_vec = train_data[:, 1] r_vec = train_data[:, 2] #saver = tf.train.Saver() for itr in range(self.pre_train_steps): for train_op in pre_model['train_ops']: pre_session.run((train_op, pre_model['loss'], pre_model['rmse']), feed_dict={pre_model['u']: u_vec, pre_model['i']: i_vec, pre_model['r']: r_vec}) if (itr+1)%10==0: valid_rmse, _ = self._get_rmse_pre_model(pre_session, pre_model) print('Pre-train step: {}, train_error:{}'.format(itr+1, valid_rmse)) # if valid_rmse <= min_valid_rmse: # min_valid_rmse = valid_rmse # min_valid_iter = itr # #saver.save(pre_session, file_path) #saver.restore(pre_session, file_path) p_factor, q_factor = pre_session.run( (pre_model['p'], pre_model['q']), feed_dict={ pre_model['u']: self.batch_manager.train_data[:, 0], pre_model['i']: self.batch_manager.train_data[:, 1], pre_model['r']: self.batch_manager.train_data[:, 2] }) if not os.path.exists(self.result_path): os.makedirs(self.result_path) np.save('{}/pre_train_p.npy'.format(self.result_path), p_factor) np.save('{}/pre_train_q.npy'.format(self.result_path), q_factor) pre_session.close() self.user_latent_init = p_factor self.item_latent_init = q_factor
def predict(self, user_items)
-
Given user-item pairs predict the rating
Parameters
user_items
:Array-like, shape (N, 2)
- Each row is an (user, item) pair
Returns
np.ndarray, shape (N,)
- Predicted ratings
Expand source code
def predict(self, user_items): """Given user-item pairs predict the rating Parameters ---------- user_items : Array-like, shape (N, 2) Each row is an (user, item) pair Returns ------- np.ndarray, shape (N,) Predicted ratings """ session = self.session model = self.model predict_k = np.stack( [ self.anchor_manager.get_k(anchor_idx, user_items) for anchor_idx in range(len(self.anchor_manager.anchor_idxs)) ], axis=1) predict_k = np.clip(predict_k, 0.0, 1.0) predict_k = np.divide(predict_k, np.sum(predict_k, axis=1, keepdims=1)) predict_k[np.isnan(predict_k)] = 0 predict_r_hat = session.run( model['r_hat'], feed_dict={ model['u']: user_items[:, 0], model['i']: user_items[:, 1], model['k']: predict_k }) return predict_r_hat
def reset_data(self, train_data, valid_data, test_data)
-
Reset the data of a recommender by instantiating a new BatchManager or modifying the existing one
Parameters
train_data
:Array-like, shape (N_train,3)
- Training data, each row is of the form (user_id, item_id, rating)
valid_data
:Array-like, shape (N_valid, 3)
- Validation data, each row is of the form (user_id, item_id, rating)
test_data
:Array-like, shape (N_test, 3)
- Test data, each row is of the form (user_id, item_idm rating)
Expand source code
def reset_data(self, train_data, valid_data, test_data): """ Reset the data of a recommender by instantiating a new BatchManager or modifying the existing one Parameters ---------- train_data : Array-like, shape (N_train,3) Training data, each row is of the form (user_id, item_id, rating) valid_data : Array-like, shape (N_valid, 3) Validation data, each row is of the form (user_id, item_id, rating) test_data : Array-like, shape (N_test, 3) Test data, each row is of the form (user_id, item_idm rating) """ if not self.batch_manager: self.batch_manager = BatchManager(train_data, valid_data, test_data) else: self.batch_manager.update(train_data, valid_data, test_data) N_ratings = self.batch_manager.train_data.shape[0] if N_ratings < self.n_anchor: warnings.warn("The data has fewer ratings than anchor points: {}<{}".format( N_ratings, self.n_anchor)) self.n_anchor = N_ratings
def train(self)
-
Train the LLORMA recommender
Expand source code
def train(self): # noqa: R0914 """ Train the LLORMA recommender """ self.pre_train() #tf.reset_default_graph() self.model = self.init_model() model = self.model self.anchor_manager = AnchorManager(self.n_anchor, self.batch_manager, self.user_latent_init, self.item_latent_init, self.kernel_fun) session = init_session() local_models = [ LocalModel(session, model, anchor_idx, self.anchor_manager, self.batch_manager) for anchor_idx in range(self.n_anchor) ] train_k = _get_local_k(local_models, kind='train') valid_k = _get_local_k(local_models, kind='valid') test_k = _get_local_k(local_models, kind='test') min_valid_rmse = float('Inf') min_valid_iter = 0 train_data = self.batch_manager.train_data #saver = tf.train.Saver() for itr in range(self.train_steps): file_path = '{}/model-{}.ckpt'.format(self.result_path, itr) for start_m in range(0, train_data.shape[0], self.batch_size): end_m = min(start_m + self.batch_size, train_data.shape[0]) u_vec = train_data[start_m:end_m, 0] i_vec = train_data[start_m:end_m, 1] r_vec = train_data[start_m:end_m, 2] k_vec = train_k[start_m:end_m, :] results = session.run( [model['rmse']] + model['train_ops'], feed_dict={ model['u']: u_vec, model['i']: i_vec, model['r']: r_vec, model['k']: k_vec, }) if (itr+1)%10==0: valid_rmse, test_rmse = self._get_rmse_model(session, model, valid_k, test_k) print("Train step:{}, train error: {}, test error: {}".format(itr+1, test_rmse, valid_rmse)) # if valid_rmse < min_valid_rmse: # min_valid_rmse = valid_rmse # min_valid_iter = itr # #saver.save(session, file_path) # #saver.restore(session, file_path) # if itr >= min_valid_iter + 100: # break self.session = session return(session, model)
class LocalModel (session, model, anchor_idx, anchor_manager, batch_manager)
-
LocalModel
Parameters
session
:obj: tf.session
- TF session
model
:Dict-like
- Dictionary of TF variables, train operations Typically the output of self.init_pre_model()
anchor_idx
:int
- Id of the anchor point for the local model
anchor_manager
:obj: AnchorManager
batch_manager
:obj: BatchManager
Instantiate a local model
Expand source code
class LocalModel: # noqa: R0903 """LocalModel Parameters ---------- session : obj: tf.session TF session model : Dict-like Dictionary of TF variables, train operations Typically the output of self.init_pre_model() anchor_idx : int Id of the anchor point for the local model anchor_manager : obj: AnchorManager batch_manager : obj: BatchManager """ def __init__(self, session, model, anchor_idx, anchor_manager, batch_manager): """ Instantiate a local model """ self.session = session self.model = model self.batch_manager = batch_manager self.anchor_idx = anchor_idx self.anchor_manager = anchor_manager #print('>> update k in anchor_idx [{}].'.format(anchor_idx)) self.train_k = anchor_manager.get_train_k(anchor_idx) self.valid_k = anchor_manager.get_valid_k(anchor_idx) self.test_k = anchor_manager.get_test_k(anchor_idx)