首发于 米狗的数据之路

基于矩阵分解的推荐技术

推荐系统与矩阵

推荐系统主要解决的是“人”与“物”的关系,我们可以把推荐看成一个二维矩阵,如下图所示,列为“人”,行为“物”,表中的数值为“人”对“物”喜爱程度的量化表示。推荐系统所要解决的问题就是给表格中未知的“?”填入合适的值。推荐系统最终的目标就是对于任意一个用户,预测出所有未评分物品的分值,并按分值从高到低的顺序将对应的物品推荐给用户。

对于矩阵分解,我们首先想到的是课本上学到的特征值矩阵分解和奇异值(SVD)矩阵分解。由于特征值矩阵分解要求是方阵,这一点对于现实的推荐来说太严格了,我们来看下SVD矩阵分解。

基于SVD的矩阵分解

奇异值分解,其具体描述为:对于一个m*n的矩阵M,则一定存在一个分解M=U∑V ,其中U是m*m的正交矩阵,V是n*n的正交矩阵,Σ是m*n的对角阵。其中,对角阵Σ还有一个特殊的性质,它的所有元素都非负,且依次减小。这个减小也特别快,在很多情况下,前10%的和就占了全部元素之和的99%以上,这就是说我们可以使用最大的k个值和对应大小的U、V矩阵来近似描述原始的评分矩阵,可以说是完美契合分解评分矩阵这个需求,

我们马上能得到一个解决方案:对原始评分矩阵M做奇异值分解,得到U、V及Σ,取Σ中较大的k类作为隐含特征,则此时M(m*n)被分解成U(m*k) Σ(k*k)V(k*n),接下来就可以直接使用矩阵乘法来完成对原始评分矩阵的填充。然而梦想是美好的,现实是残酷的,SVD使用有个前提条件,需要矩阵是稠密的,也就是说不允许待分解的矩阵存在着空值,而我们推荐系统需要解决的问题就是给矩阵中的空值做填空,这就与我们的问题冲突了。

基于Funk-SVD的矩阵分解

Simon Funk就提出了Funk-SVD算法,其主要思路是将原始评分矩阵M(m*n)分解成两个矩阵P(m*k)和Q(k*n),借鉴机器学习中监督学习的训练过程,把M矩阵中已知的分值作为目标值,选择均方差为模型的判别标准,经过迭代,得到一个使得整体均方差最小的P和Q。也就是,我们使用待确定的P和Q计算出一个M’,

总的损失就是,

最小化上面的损失SSE,就能以完成对原始评分矩阵的分解,在这之后只需要用计算M’的方式来完成对原始评分矩阵的填充即可。

这种方法也有一个高大上的名称,叫做隐语义模型(Latent factor model,LFM),其算法意义层面的解释为通过隐含特征(latent factor)将user兴趣与item特征联系起来。

对于原始评分矩阵R,我们假定一共有三类隐含特征,于是将矩阵R(3*4)分解成用户特征矩阵P(3*3)与物品特征矩阵Q(3*4)。考察user1对item1的评分,可以认为user1对三类隐含特征class1、class2、class3的感兴趣程度分别为P11、P12、P13,而这三类隐含特征与item1相关程度则分别为Q11、Q21、Q31。可以发现用户U对物品I最终的评分就是由各个隐含特征维度下U对I感兴趣程度的和,这里U对I的感兴趣程度则是由U对当前隐含特征的感兴趣程度乘上I与当前隐含特征相关程度来表示的。于是,现在的问题就变成了如何求出使得SSE最小的矩阵P和Q。

梯度下降法(Gradient Descent)是最常采用的方法之一,其核心思想非常简单,沿梯度下降的方向逐步迭代。梯度是一个向量,表示的是一个函数在该点处沿梯度的方向变化最快,变化率最大,而梯度下降的方向就是指的负梯度方向。

具体应用到上文中的目标函数

SSE是关于P和Q的多元函数,当随机选定U和I之后,需要枚举所有的k,并且对Pu,k以及 Qk,i求偏导数。整个式子中仅有 Pu,kQk,i 这一项与之相关,通过链式法则可知

改进的Funk-SVD的矩阵分解

和其他算法一样,我们从“过拟合”和“欠拟合”两个角度来看。

(1) 正则化

Funk-SVD的求解过程也是和一般的机器学习问题一样,也会存在过拟合的问题,因此我们也为其加上正则化项。具体做法就是在损失函数后面加入一个L2正则项,即

其中,λ为正则化系数,而整个求解过程依然可以使用随机梯度下降来完成。

(2) 偏置

观察式子

我们发现式子表明用户U对物品 I 的评分全部是由U和I之间的联系带来的,然而实际上,有很多性质是用户或者物品所独有的。比如某个用户习惯于差评,他给所有物品的得分都很低,这仅仅与用户自身有关。又比如某个物品非常完美,所有用户都会给出较高的分数,这也仅仅与物品自身有关。因此,只通过用户与物品之间的联系来预测评分是不合理的,同时也需要考虑到用户和物品自身的属性。于是,评分预测的公式也需要进行修正。不妨设整个评分矩阵的平均分为σ,用户U和物品I的偏置分别为 bu 和 bi ,那么此时的评分计算方法就变成了

将(1)和(2)结合下,我们就可以得到如下公式:

总结

矩阵分解有如下优点:

相对的,矩阵分解的缺点则有:

给出Funk-SVD的实现及使用示例:

import pickle
import numpy as np
import matplotlib.pyplot as plt


class Funk_SVD(object):
	"""
	implement Funk_SVD
	"""
	def __init__(self, path,USER_NUM,ITEM_NUM,FACTOR):
		super(Funk_SVD, self).__init__()
		self.path = path
		self.USER_NUM=USER_NUM
		self.ITEM_NUM=ITEM_NUM
		self.FACTOR=FACTOR
		self.init_model()


	def load_data(self,flag='train',sep='\t',random_state=0,size=0.8):
		'''
		flag- train or test
		sep- separator of data
		random_state- seed of the random 
		size- rate of the train of the test
		'''
		np.random.seed(random_state)
		with open(self.path,'r') as f:
			for index,line in enumerate(f):
				if index==0:
					continue
				rand_num=np.random.rand()
				if flag=='train':
					if  rand_num < size:
						u,i,r,t=line.strip('\r\n').split(sep)
						yield (int(u)-1,int(i)-1,float(r))
				else:
					if rand_num >= size:
						u,i,r,t=line.strip('\r\n').split(sep)
						yield (int(u)-1,int(i)-1,float(r))

	def init_model(self):
		self.P=np.random.rand(self.USER_NUM,self.FACTOR)/(self.FACTOR**0.5)
		self.Q=np.random.rand(self.ITEM_NUM,self.FACTOR)/(self.FACTOR**0.5)

	
	def train(self,epochs=5,theta=1e-4,alpha=0.02,beta=0.02):#500
		'''
		train the model
		epochs- num of iterations
		theta- therehold of iterations
		alpha- learning rate
		beta- parameter of regularization term
		'''
		old_e=0.0
		self.cost_of_epoch=[]
		for epoch in range(epochs):#SGD
			print("current epoch is {}".format(epoch))
			current_e=0.0
			train_data=self.load_data(flag='train') #reload the train data every iteration(generator)
			for index,d in enumerate(train_data): 
				u,i,r=d
				pr=np.dot(self.P[u],self.Q[i])
				err=r-pr 
				current_e+=pow(err,2) #loss term
				self.P[u]+=alpha*(err*self.Q[i]-beta*self.P[u])
				self.Q[i]+=alpha*(err*self.P[u]-beta*self.Q[i])
				current_e+=(beta/2)*(sum(pow(self.P[u],2))+sum(pow(self.Q[i],2))) #正则项
			self.cost_of_epoch.append(current_e)
			print('cost is {}'.format(current_e))
			if abs(current_e - old_e) < theta:
				break
			old_e=current_e
			alpha*=0.9


	def predict_rating(self,user_id,item_id):
		'''
		predict rating for target user of target item

		user- the number of user(user_id=xuhao-1)
		item- the number of item(item_id=xuhao-1)
		'''
		pr=np.dot(self.P[user_id],self.Q[item_id])
		return pr

	def recommand_list(self,user,k=10):
		'''
		recommand top n for target user
		for rating prediction,recommand the items which socre is higer than 4/5 of max socre
		'''
		user_id=user-1
		user_items={}
		for item_id in range(self.ITEM_NUM):
			user_had_look = {}
			user_had_look = self.user_had_look_in_train()
			if item_id in user_had_look[user]:
			   continue
			pr=self.predict_rating(user_id,item_id)
			user_items[item_id]=pr
		items=sorted(user_items.items(),key=lambda x:x[1],reverse=True)[:k]
		return items
    
	def user_had_look_in_train(self):
		user_had_look = {}
		train_data=self.load_data(flag='train')
		for index,d in enumerate(train_data):
			u,i,r=d
			user_had_look.setdefault(u,{})
			user_had_look[u][i] = r
		return user_had_look


	def test_rmse(self):
		'''
		test the model and return the value of rmse
		'''
		rmse=.0
		num=0
		test_data=self.load_data(flag='test')
		for index,d in enumerate(test_data):
			num=index+1
			u,i,r=d
			pr=np.dot(self.P[u],self.Q[i])
			rmse+=pow((r-pr),2)
		rmse=(rmse/num)**0.5
		return rmse
	
	def show(self):
		'''
		show figure for cost and epoch
		'''
		nums=range(len(self.cost_of_epoch))
		plt.plot(nums,self.cost_of_epoch,label='cost value')
		plt.xlabel('# of epoch')
		plt.ylabel('cost')
		plt.legend()
		plt.show()
		pass

	def save_model(self):
		'''
		save the model to pickle,P,Q and rmse
		'''
		data_dict={'P':self.P,'Q':self.Q}
		f=open('funk-svd.pkl','wb')
		pickle.dump(data_dict,f)
		pass

	def read_model(self):
		'''
		reload the model from local disk
		'''
		f=open('funk-svd.pkl','rb')
		model=pickle.load(f)
		self.P=model['P']
		self.Q=model['Q']
		pass

if __name__=="__main__":
	mf=Funk_SVD(ml-100k/u.data',943,1682,50)#path,user_num,item_num,factor
	mf.train()
	mf.save_model()
	rmse=mf.test_rmse()
	print("rmse:",rmse)
	user_items=mf.recommand_list(3)
	print(user_items)

代做工资流水公司珠海公司流水代办莆田离职证明查询兰州开企业流水打印潍坊代开转账银行流水南阳打入职银行流水临沂企业流水打印开具哈尔滨办理背调流水济宁背调流水开具绵阳车贷流水代办沧州个人流水制作沈阳打工资流水扬州签证工资流水样本唐山贷款流水代开许昌银行流水单公司沈阳个人流水代办泰州查对公账户流水青岛转账银行流水办理泉州自存银行流水代办柳州离职证明报价南通打自存银行流水威海转账流水开具邢台购房银行流水开具遵义代办房贷收入证明绍兴流水样本泉州房贷工资流水 打印济南打在职证明惠州流水账单费用潮州工作收入证明样本孝感贷款工资流水 制作舟山入职工资流水公司香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

代做工资流水公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化