写在前面
SVD++
在LFM中,只考虑了用户特征向量和物品特征向量(显示反馈)和偏置向量,在SVD++中,进一步考虑了用户对其有过浏览/评分行为的商品的隐式反馈.
其中Nu是用户有过浏览等行为的物品组合,没有隐性反馈数据(比如浏览等)时,Nu和用户评过分的物品集合Ru就是重复的,此时一个数据用两次,给人两种不同模型的组合的感觉.
隐性反馈的理解
说说我自己的理解吧,感觉不像之前的LFM理解那么透彻,欢迎交流.
隐性反馈这部分,就是使用物品表示用户,可以看成是根据物品对用户进行简单画像。
某个电影本身就可以代表某种兴趣,看过这个电影代表这个人对某种电影感兴趣,或者说对这个电影的某些特质感兴趣.因此,不仅仅是显性评分,用户的隐性行为,本身就可以反映用户的喜好,但这种反映是隐性的,是一种弱反映.SVD++把隐性行为和显性行为结合起来,充分利用了现有的信息来获得用户的特征表达.
再看看[1]中的这张图:
一个物品的隐性反馈,是通过所有用户的信息来训练得到的.从这里我们可以看出,隐性反馈的用户特征表达,不像显性评分那样去尽可能拟合某个用户的评分,而是考虑了整个用户群体得到的,这就从整体把握了这个物品反馈。
从这个角度看,如果说显性评分的训练可能造成过拟合的问题,那么将和用户有关的隐性反馈综合起来后,就对用户得到了更好的特征表达,一定程度上避免了显性反馈的过拟合的问题;同时,当用户较多而物品较稀疏时,训练出的pu可能不够充分,但此时有了训练较成分的隐性反馈,可以一定程度上解决显性反馈的欠拟合问题。
这和[2]在多模态中用到的弱配对有一些相同的效果,利用图片和对应的文本进行分类的过程中,如果仅考虑一张图片和一条文本为一个样本,当然也行,但是过于关注单个样本很容易过拟合.但是如果将几张同类图片和对应的文本看成一个训练样本,这样训练出的模型更加具有泛化性,因为这种模型考虑了一个类别所对应的更多的图像和文本组合,可以get到更加通用的特征,而不仅仅考虑原先的单个图像和文本特征之间的关系.
代码实现
我们重点分析一下如何实现论文中SVD++的隐性反馈
def build_graph(self, mu, implicit_feedback):
mu = super(SVDPP, self).create_constants(mu)
self.users, self.items, self.ratings = super(SVDPP, self).create_placeholders()
N = self.create_implicit_feedback(implicit_feedback)
p_u, b_u, y_u = self.create_user_terms(self.users, N)
q_i, b_i = self.create_item_terms(self.items)
self.pred = self.create_prediction(mu, b_u, b_i, p_u, q_i, y_u)
这里的super(SVDPP, self).xxx
调用父类方法是python2中的写法,python3可以直接使用super().xxx.感兴趣可以看看[4][5].
mu是y的平均值,就是μ;y_u就是和用户u有关的隐性反馈.看一下y_u怎么来的:
def create_user_terms(self, users, N):
num_users = self.num_users
num_items = self.num_items
num_factors = self.num_factors
p_u, b_u = super(SVDPP, self).create_user_terms(users)
with tf.variable_scope('user'):
implicit_feedback_embeddings = tf.get_variable(
name='implicit_feedback_embeddings',
shape=[num_items, num_factors],
initializer=tf.zeros_initializer(),
regularizer=tf.contrib.layers.l2_regularizer(self.reg_y_u))
y_u = tf.gather(
tf.nn.embedding_lookup_sparse(
implicit_feedback_embeddings,
N,
sp_weights=None,
combiner='sqrtn'),
users,
name='y_u')
y_u是从implicit_feedback_embeddings中通过N进行lookup搜寻得到的,implicit_feedback_embeddings的shape=[num_items, num_factors]
,就是所有物品的隐性反馈;N记录了所有用户所对应的物品id,tf.nn.embedding_lookup_sparse
执行过后,就得到了所有用户的隐性反馈,tf.gather
通过users找到其隐性反馈.
里面的细节combiner='sqrtn’需要注意,look_up可以通过id找到每个用户的物品的隐性反馈向量,最后通过sqrtn
的方式来组合成这个用户的隐性反馈,"sqrtn" is the weighted sum divided by the square root of the sum of the squares of the weights.
这里我们不设权重,那就直接相加除以N(u)的平方根,也就是:
然后训练过程就不难了:
def run_train(self, x, y, epoches, batch_size, val_data):
train_gen = BatchGenerator(x, y, batch_size=batch_size)
steps_per_epoch = np.ceil(train_gen.length / batch_size).astype(int)
self.sess.run(tf.global_variables_initializer())
for i in range (1, epoches+1):
print('Epoch {} / {}'.format(i, epoches))
pbar = utils.Progbar(steps_per_epoch)
print('stpes_per_epoch', steps_per_epoch)
for step, batch in enumerate(train_gen.next(), start=1):
users = batch[0][:, 0]
items = batch[0][:, 1]
ratings = batch[1]
self.sess.run(self.optimizer,
feed_dict={
self.users: users,
self.items: items,
self.ratings: ratings})
pred = self.predict(batch[0])
update_values = [
('rmse', rmse(ratings, pred)),
('mae', mae(ratings, pred))]
if(val_data is not None and step == steps_per_epoch):
valid_x, valid_y = val_data
valid_pred = self.predict(valid_x)
update_values += [
('val_rmse', rmse(valid_y, valid_pred)),
('val_mae', mae(valid_y, valid_pred))]
pbar.update(step, values=update_values)
这里使用了keras中的可视化工具Progbar,还挺好用的,可以实时显示训练过程:
原python2代码:https://github.com/WindQAQ/tf-recsys
修改后代码:https://github.com/wyl6/Recommender-Systems-Samples/tree/master/RecSys Traditional/MF/SVD%2B%2B
参考
[1] https://www.cs.rochester.edu/twiki/pub/Main/HarpSeminar/Factorization_Meets_the_Neighborhood-_a_Multifaceted_Collaborative_Filtering_Model.pdf
[2] https://link.springer.com/content/pdf/10.1007%2Fs00500-018-3108-y.pdf
[3] https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=7917316
[4] https://www.runoob.com/python/python-func-super.html
[5] http://funhacks.net/explore-python/Class/super.html
更多精彩内容请移步公众号:推荐算法工程师