导读
前面主要学习了推荐系统的基础,下面小编就带大家一起实际操作一下,这期干货满满,大家跟着小编好好学哦!
1. 基于矩阵分解的隐语义(LFM)模型算法的原理以及代码实现
我们浏览电影网站的时候,可以看到顶部一般会有很多分类,比如喜剧、动作、科幻、爱情、悬疑、恐怖等。对于我而言,我一般比较喜欢看喜剧、科幻、动作片等,因此如果网站给我推送这几种类型的电影,我是有较大概率去点击观看的。但是如果给推送言情、古装、穿越等题材的电影则根本激不起我的兴趣。因此我们首先可以对电影进行分类,接着得到用户对每一种电影类型的感兴趣度,然后挑出用户最感兴趣的那个类别,挑选出属于那个类别的电影推荐给用户。
我们其次来量化一下用户的感兴趣程度,使用0到1范围内的数字来代表用户对某种电影类型的感兴趣程度,从0到1依次代表喜欢程度加深。
假如我们现在有8个分类标签,分别是喜剧、动作、科幻、爱情、悬疑、恐怖、剧情、冒险,以用户A为例,A可能会做出以下评分:现在有两部电影,分别是《钢铁侠3》和《傲慢与偏见》,通过看电影介绍,我们知道《钢铁侠3》包含了动作、科幻、冒险3个标签,《傲慢与偏见》包含了剧情、爱情两个标签。
我们假设这两部电影对这8个标签的符合程度如下:我们可以计算出A对《钢铁侠3》的喜欢程度为:0.5x0.7+0.6x0.9+0.6x0.6=1.25。同理,A对《傲慢与偏见》的喜欢程度为:0.6x0.9+0.3x0.9=0.81。可以看到,用户A对《钢铁侠3》的喜爱程度要高于《傲慢与偏见》,故推荐《钢铁侠3》是一个比较明智的选择。
我们实际上收集到的数据一般是用户对物品的评分,以上述的电影为例,那么我们得到的原始数据集可能是这样的:那么我们有没有一种方法,能够通过原始数据集得到用户对每个类别的感兴趣程度以及每个物品的分类呢?为了解决这些问题,研究人员想到为什么我们不从数据出发,自动找到那些类。然后进行个性化推荐呢?于是,隐含语义分析技术出现了。
我将上面的表格数据用矩阵表示,并写成下面的形式:上图中,我们令R代表用户对电影的评分数据,这是我们已有的数据集。令P代表用户对电影类别的感兴趣程度,令Q为每部电影对每个类别的符合程度,因此可得R=P*Q。由于我们已有的数据集是R,要求P和Q,首先可能会想到SVD矩阵分解,但是有一个很大的问题,就是SVD分解要求矩阵是稠密的,也就是说矩阵的所有位置不能有空白。而我们的原始数据集有很多缺失值,因为不可能每个用户都会对每一件商品进行评分。
下面我们来看下LFM算法是如何解决这个问题的。令代表的是用户对物品的感兴趣程度,定义如下:
由于用户一般不会对全部商品评分,故上图R中有很多项没有数据。其中K是分类的个数,对比上图,图中的K值是8,因为我们将电影分成了8个类别,但是这里并没有指定具体的K值,因为在LFM算法中K是可以调整的,即我们可能会产生多个没有具体意义的类别,我想这也是为什么叫隐语义模型的一个原因吧。
以MovieLens数据集为例,演示下代码具体实现流程。
2. LFM模型算法的代码实现
2.1 准备训练数据
我们收集的原始数据是用户对电影的评分数据,但是这其中并没有用户对不喜欢的电影的评分(用户一般也不会去评分),为了训练我们的模型,我们需要准备一个训练数据集,这其中包含了每个用户喜欢的电影和不喜欢的电影,通过学习这个数据集,就可以获得上面的模型参数。那么在隐形反馈数据集上应用LFM算法的第一个问题就是如何给用户生成负样本,即用户不喜欢的电影。负样本的生成需要遵循以下原则:1.对每个用户,要保证正负样本数量均衡。
2.对每个用户采样负样本时,要选取那些很热门,而用户却没有行为的物品。
3.一般认为,物品很热门而用户却没有行为,更加代表用户对这个物品不感兴趣。因为对于冷门物品,用户可能压根没在网站中发现这个物品,所以谈不上是否感兴趣。
下面代码用于随机选择负样本,对于每个用户喜爱的电影集合,选择数量相等的不感兴趣电影:
def _select_negatives(self, movies):
"""
选择负样本
:param movies: 一个用户喜爱的电影集合
:return: 包含正负样本的电影样本集合
"""
ret = dict()
for i in movies: # 记录正样本,兴趣度为1
ret[i] = 1
number = 0
while number < len(movies):
# 从所有商品集合中随机选取一个当做负样本,兴趣度置为0
negative_sample = random.choice(self._item_pool)
if negative_sample in ret:
continue
ret[negative_sample] = 0
number += 1
return ret
2.2 初始化矩阵P和Q
采用均值为0,方差为1的高斯分布数据来初始化矩阵P和Q,代码如下:
def _init_matrix(self):
'''
初始化P和Q矩阵,同时选择高斯分布的随机值作为初始值
'''
print("start build latent matrix.")
# User-LF user_p 是一个 m x k 的矩阵,其中m是用户的数量,k是隐含类别的数量
self.user_p = dict()
for user in self._trainData.keys():
self.user_p[user] = np.random.normal(size=(self._k))
# Item-LF movie_q 是一个 n x k 的矩阵,其中n是电影的数量,k是隐含类别的数量
self.item_q = dict()
for movie in self._item_pool:
self.item_q[movie] = np.random.normal(size=(self._k))
2.3 随机梯度优化
采用随机梯度优化算法,在每次迭代中对每个用户的电影列表都重新选择负样本,并且更新参数:
def SGD(self):
# 随机梯度下降算法
alpha = self._alpha
for epoch in range(self._epochs):
print("start {0}th epoch training...".format(epoch))
for user, positive_movies in self._trainData.items():
# 每次迭代都对用户重新选择负样本
select_samples = self._select_negatives(positive_movies)
for movie, rui in select_samples.items():
# 使用模型去预测user对movie的相似度,并且得到与真实值之间的误差
eui = rui - self.predict(user, movie)
user_latent = self.user_p[user]
movie_latent = self.item_q[movie]
# 更新参数
self.user_p[user] += alpha * (eui * movie_latent - self._lmbda * user_latent)
self.item_q[movie] += alpha * (eui * user_latent - self._lmbda * movie_latent)
alpha *= 0.9 #使学习率线性减小
2.4 完整代码
import random
import numpy as np
from operator import itemgetter
from Utils import modelManager
class LFM(object):
def __init__(self, trainData, alpha, regularization_rate, number_LatentFactors=10, number_epochs=10):
self._trainData = trainData # User-Item表
self._alpha = alpha # 学习率
self._lmbda = regularization_rate # 正则化惩罚因子
self._k = number_LatentFactors # 隐语义类别数量
self._epochs = number_epochs # 训练次数
self._item_pool = self._getAllItems() # 所有物品集合
self._init_matrix()
def _getAllItems(self):
# 获取全体物品列表
print("start collect all items...")
items_pool = set()
for user, items in self._trainData.items():
for item in items:
items_pool.add(item)
return list(items_pool)
def _init_matrix(self):
'''
初始化P和Q矩阵,同时选择高斯分布的随机值作为初始值
'''
print("start build latent matrix.")
# User-LF user_p 是一个 m x k 的矩阵,其中m是用户的数量,k是隐含类别的数量
self.user_p = dict()
for user in self._trainData.keys():
self.user_p[user] = np.random.normal(size=(self._k))
# Item-LF movie_q 是一个 n x k 的矩阵,其中n是电影的数量,k是隐含类别的数量
self.item_q = dict()
for movie in self._item_pool:
self.item_q[movie] = np.random.normal(size=(self._k))
def predict(self, user, item):
# 通过公式 Rui = ∑P(u,k)Q(k,i)求出user对item的感兴趣程度
return np.dot(self.user_p[user], self.item_q[item])
def _select_negatives(self, movies):
"""
选择负样本
:param movies: 一个用户喜爱的电影集合
:return: 包含正负样本的电影样本集合
"""
ret = dict()
for i in movies: # 记录正样本,兴趣度为1
ret[i] = 1
number = 0
while number < len(movies):
# 从所有商品集合中随机选取一个当做负样本,兴趣度置为0
negative_sample = random.choice(self._item_pool)
if negative_sample in ret:
continue
ret[negative_sample] = 0
number += 1
return ret
def _loss(self):
C = 0.
for user, user_latent in self.user_p.items():
for movie, movie_latent in self.item_q.items():
rui = 0
for u, m in self._trainData.items():
if user == u:
if movie in m: # 如果movie出现在了user的喜爱列表里面,则rui=1
rui = 1
break
else:
continue
eui = rui - self.predict(user, movie)
C += (np.square(eui) +
self._lmbda * np.sum(np.square(self.user_p[user])) +
self._lmbda * np.sum(np.square(self.item_q[movie])))
return C
def SGD(self):
# 随机梯度下降算法
alpha = self._alpha
for epoch in range(self._epochs):
print("start {0}th epoch training...".format(epoch))
for user, positive_movies in self._trainData.items():
# 每次迭代都对用户重新选择负样本
select_samples = self._select_negatives(positive_movies)
for movie, rui in select_samples.items():
# 使用模型去预测user对movie的相似度,并且得到与真实值之间的误差
eui = rui - self.predict(user, movie)
# print("error : ", eui)
user_latent = self.user_p[user]
movie_latent = self.item_q[movie]
# 更新参数
self.user_p[user] += alpha * (eui * movie_latent - self._lmbda * user_latent)
self.item_q[movie] += alpha * (eui * user_latent - self._lmbda * movie_latent)
alpha *= 0.9
print("{0}td training finished, loss: {1}".format(epoch, self._loss()))
def recommend(self, user, N):
"""
给user推荐N个商品
:param user: 被推荐的用户user
:param N: 推荐的商品个数
:return: 按照user对推荐物品的感兴趣程度排序的N个商品
"""
recommends = dict()
for movie in self._item_pool:
recommends[movie] = self.predict(user, movie)
# 根据被推荐物品的相似度逆序排列,然后推荐前N个物品给到用户
return dict(sorted(recommends.items(), key=itemgetter(1), reverse=True)[:N])
def train(self):
train, test = movielens_loader.LoadMovieLensData("../Data/ml-1m/ratings.dat", 0.8)
print("train data size: %d, test data size: %d" % (len(train), len(test)))
lfm = lfm.LFM(train, 0.02, 0.01, 10, 30)
lfm.train()
print(lfm.user_p)
print(lfm.item_q)
modelManager.save("../Models/lfm.pkl", lfm.user_p, lfm.item_q)
2.5 测试代码
以下代码对测试集中3个用户进行了Top5推荐,即每个用户推荐5部电影,输出结果按照用户对电影的感兴趣程度从大到小排序。LFM算法学习率采用0.02,正则化参数0.01,隐类设置为10,迭代次数为30。代码如下:
from Utils import movielens_loader
from LFM import lfm
if __name__ == "__main__":
#######
# LFM
#######
model = modelManager.load("../Models/lfm.pkl", 3)
lfm.user_p = model[0]
lfm.item_q = model[1]
# 给测试集中的用户推荐5部电影
print(lfm.recommend(list(test.keys())[0], 5))
print(lfm.recommend(list(test.keys())[1], 5))
print(lfm.recommend(list(test.keys())[2], 5))
结果如下,输出了每个用户感兴趣程度最高的5部电影:对于以上代码有啥问题欢迎留言评论哦,小编看到了会及时回复!-The End-
文案:朱琪
指导老师:曹菁菁 赵强伟
排版:黄雅文 王雪