上一章节讨论了数学中的SVD、机器学习中的SVD以及推荐系统中的SVD系列算法,今天我们用python来实战一下,知道原理,究竟该如何实现。
LFM
首先是最简单的LFM(即 Funk SVD),首先原理我们是知道的,LFM的基本思想是使用两个矩阵P、Q的乘积来近似原始评分矩阵的。如何实现呢?(完整代码可以直接在我的github中下载或复制:https://github.com/flyChineseBoy/RCLearn)
首先第一步,定义我们的Model class,定义有哪些一开始就可以给定的属性(我这里使用users:包括所有的用户的列表,items:包括所有物品的列表,F:兴趣因子数来初始化class,同时使用这三个属性随机初始化P、Q矩阵)
def __init__(self, users, items,F):
self.P = {}
self.Q = {}
self.user_items = {}
self.mse_result = []
self.F = F
self.users = users
self.items = items
a = 1
for user in users:
self.P[user] = [random.random() for i in range(F)]
for item in items:
self.Q[item] = [random.random() for i in range(F)]
有了基础的P、Q矩阵,我们其实已经可以做预测了(当然现在不可能预测的准),下面定义预测(或推荐)方法:
def predict(self,user, item):
return sum(self.P[user][f]*self.Q[item][f] for f in range(self.F))
def recommend(self,user,item):
return sum(self.P[user][f] * self.Q[item][f] for f in range(self.F))
接下来才是最重要的步骤,如何拟合P、Q矩阵?这里直接使用各种书籍都推荐的SGD来更新:
'''
输入:
user_items:(user-items字典)
users: 包含所有user的列表
items: 包含所有item的列表
N:SGD迭代数
alpha:alpha
_lambda:正则化系数
'''
def train(self,user_items,steps,alpha,_lambda):
self.user_items = user_items
# 去除可能存在的未评分元素
pop_elems = []
new_user_items = {}
for user,items in new_user_items.items():
users = {}
for item,rui in items.items():
users[item] = rui
if(new_user_items[user][item]==0):
pop_elems.append((user,item))
new_user_items[user] = users
for user,items in pop_elems:
new_user_items[user].pop(item)
#更新参数
for step in range(steps):
for user,items in new_user_items.items():
for item,rui in items.items():
eui = rui - self.predict(user,item)
for f in range(self.F):
self.P[user][f] +=alpha * (eui * self.Q[item][f] - _lambda * self.P[user][f])
self.Q[item][f] +=alpha *(eui * self.P[user][f] - _lambda * self.Q[item][f])
以上,最简单的LFM就完成了。做了一个简单的预测,如下:
Basic SVD
Basic SVD是LFM的升级版,在LFM的基础上考虑了用户偏置项和物品偏置项,这两个是什么意思呢?
举例来说,用户A和用户B评分喜好不同,A喜欢就打5分,不喜欢也不会低于3分,但是B比较保守,喜欢只有4分,不喜欢2分3分,实际上,他们的喜好是差不多了,但是因为个人打分习惯的不同而导致出差别,用户偏置项就是用来消除这种差别。
那么物品偏置项自然就是用来消除物品与物品之间的差别,也可以说这个物品本质就值这个分数,比如一个院线烂片,无论是什么类型的都改变不了大家不喜欢它的本质。
第一步,与之前一样,我们先来定义初始化方法,(与上面一样,因为两个偏置项也可以在这里随机初始化,但是我感觉一开始将偏置项置为user或item的平均值比较合适,所以放在了train方法,输入数据之后再初始化)
第二步,预测方法,与之前不同的是,加上了三个值,全局平均数mu、user偏置项、item偏置项:
def __init__(self, users, items,F):
self.P = {}
self.Q = {}
self.mu=0
self.bu = 0
self.bi = 0
self.user_items = {}
self.mse_result=[]
self.F = F
self.users = users
self.items = items
for user in users:
self.P[user] = [random.random() for i in range(F)]
for item in items:
self.Q[item] = [random.random() for i in range(F)]
第三步,训练。处理数据-->初始化偏置项->更新参数:
def train(self,user_items,steps,alpha,_lambda):
self.user_items = user_items
# 去除可能存在的未评分元素
pop_elems = []
new_user_items = {}
for user,items in user_items.items():
users = {}
for item,rui in items.items():
users[item] = rui
if(user_items[user][item]==0):
pop_elems.append((user,item))
new_user_items[user] = users
for user,item in pop_elems:
new_user_items[user].pop(item)
#初始化mu、bi、bu
user_ruis = {}
item_ruis = {}
for user,items in new_user_items.items():
for item,rui in items.items():
if(item not in item_ruis):
item_ruis[item] = [rui]
else:
item_ruis[item].append(rui)
if(user not in user_ruis):
user_ruis[user] = [rui]
else:
user_ruis[user].append(rui)
self.bu = {user:np.mean(ruis) for user,ruis in user_ruis.items()}
self.bi = {item:np.mean(ruis) for item,ruis in item_ruis.items()}
print(self.bu)
print(self.bi)
self.mu = np.mean([ruis for item,ruis in self.bu.items()])
print(self.mu)
# 更新参数
for step in range(steps):
for user,items in user_items.items():
for item,rui in items.items():
eui = rui - self.predict(user,item)
self.bu[user] += alpha*(eui-_lambda*self.bu[user])
self.bi[item] +=alpha*(eui-_lambda*self.bi[item])
for f in range(self.F):
self.P[user][f] +=alpha * (eui * self.Q[item][f] - _lambda * self.P[user][f])
self.Q[item][f] +=alpha *(eui * self.P[user][f] - _lambda * self.Q[item][f])
if(step % 10 ==0):
self.mse_result.append(self.mse())
至此完成,在LFM的基础上增加为BasicSVD没有什么难度,仅仅是多加了两个偏置项。
SVD++
SVD++进一步在BasicSVD的基础上对用户进行更详细的描绘,再次为每一个物品i关联一个因子向量yi(之前有了qi)。
在这里我的理解时:用户(user)评分某一个电影(item)这一行为即代表这一电影有某个元素吸引着用户,无关评分,这是一种隐式反馈(如果有其他的数据,比如用户点击了某个电影或搜索了某个电影,这也是隐式反馈)。那么我们就可以从用户评分过的电影集合中再抽取出一些隐式特征来。
,这里的R(u)为用户u评分过的item集合。
下面来书写方法:
第一步,与之前一样,我们先来定义初始化方法,(这里的初始化方法有一些变化,同时初始化了y)
def __init__(self,F,users,items):
self.P = {}
self.Q = {}
self.mu=0
self.bu = 0
self.bi = 0
self.user_items = {}
self.mse_result=[]
self.y = {}
self.F = F
self.users = users
self.items = items
for user in users:
self.P[user] = [random.random() for i in range(F)]
for item in items:
self.Q[item] = [random.random() for i in range(F)]
self.y[item] = [random.random() for i in range(F)]
第二步,预测方法,增加新的因子向量:
'''
得到公式中关于y的部分:|y|^2,∑yj/|y|^2
输入:
user:用户标识
返回:
nu_sqrt,nu_tmp
'''
def get_y(self,user):
nu_len = len(user_items[user])
nu_sqrt = math.pow(nu_len,2)
nu_tmp = np.sum([self.y[item] for item,rui in user_items[user].items()],axis=0)/nu_sqrt
return nu_sqrt,nu_tmp
'''
预测 mu + bi + bu + qi的转置 * (pu + nu_tmp)
输入:
user:用户标识
item:物品标识
返回:
rui:预测分数
'''
def predict(self,user,item):
nu_sqrt,nu_tmp = self.get_y(user)
return self.mu+self.bu[user]+self.bi[item]+np.sum(
(np.array(self.P[user])+np.array(nu_tmp))*(np.array(self.Q[item]))
)
第三步,training:
def train(self,user_items,steps,alpha,_lambda):
# 去除输入数据中可能存在的未评分元素
self.user_items = user_items
pop_elems = []
new_user_items = {}
for user,items in user_items.items():
users = {}
for item,rui in items.items():
users[item] = rui
if(user_items[user][item]==0):
pop_elems.append((user,item))
new_user_items[user] = users
for user,item in pop_elems:
new_user_items[user].pop(item)
#初始化mu、bi、bu
user_ruis = {}
item_ruis = {}
for user,items in new_user_items.items():
for item,rui in items.items():
if(item not in item_ruis):
item_ruis[item] = [rui]
else:
item_ruis[item].append(rui)
if(user not in user_ruis):
user_ruis[user] = [rui]
else:
user_ruis[user].append(rui)
self.bu = {user:np.mean(ruis) for user,ruis in user_ruis.items()}
self.bi = {item:np.mean(ruis) for item,ruis in item_ruis.items()}
self.mu = np.mean([ruis for item,ruis in self.bu.items()])
# 更新参数
for step in range(steps):
for user,items in user_items.items():
for item,rui in items.items():
eui = rui - self.predict(user,item)
self.bu[user] += alpha*(eui-_lambda*self.bu[user])
self.bi[item] +=alpha*(eui-_lambda*self.bi[item])
nu_sqrt,nu_tmp = self.get_y(user)
for f in range(self.F):
self.P[user][f] +=alpha * (eui * self.Q[item][f] - _lambda * self.P[user][f])
self.Q[item][f] +=alpha *(eui * (self.P[user][f]+nu_tmp) - _lambda * self.Q[item][f])
for item_02,rui_02 in items.items():
self.y[item_02][f] +=alpha*(eui - nu_sqrt - _lambda*self.y[item_02][f])
if(step % 10 ==0):
self.mse_result.append(self.mse())
#alpha = alpha*0.9