协同过滤与矩阵分解推荐系统

本文最后更新于:2021年10月28日 晚上

协同过滤与矩阵分解

推荐系统

在如今的大数据时代,我们不断地在与推荐系统打交道。在音乐播放器中听几首喜欢的歌,不一会儿播放器就开始向你推荐类似的歌;打开B站刷几条视频,马上就有与之类似的视频出现在了你的首页。毫不夸张地说,我们已经处于一种广告效率最大化的环境之中,与之同来的是一个个的信息茧房,我们确实看到了当时我们喜欢的东西,但是不久之后我们也会为之感到无聊,于此同时我们对一些事物的偏见也会加深,人与人之间的隔阂也会加剧,推荐系统,我看还是随机点好(笑😂)。


影评推荐

今天我们来实现一个经典的推荐系统,该系统根据目标用户的影评(评分)情况来为他推荐他可能喜欢的电影。

协同过滤

我们很容易想到根据用户已经有的评价来找到与之相似的用户,然后根据相似用户的打分情况来推荐给目标用户。但是我们并不是总能找到这样的相似用户,而且按照协同滤波理论,我们需要维持一个 rUser×Itemr_{User \times Item}大小的矩阵。显然这里的ItemItemUserUser的数目都是非常巨大的,而且随着时间会不断增加。更不优雅的是这个矩阵显而易见是稀疏的,维持这样的矩阵会产生非常不必要的开销。

矩阵分解

矩阵分解的核心思想是将矩阵rr分解成两个矩阵的乘积p×qTp \times q^T,其中pp为用户矩阵,大小为User×kUser \times kqq为物品矩阵,大小为Item×kItem \times k,其中,kk为隐因子,我个人的理解就是用户之间、物品之间差异的表示。这样一来我们就将原来的平方空间复杂度转化成了线性空间复杂度,而且最为关键的,现在我们有了每个用户对所有物品评价的数据的近似值,有了这些数据,我们就能很方便的进行推荐了。

但是一般来讲我们是无法直接得到p×qT=r^=rp \times q^T = \hat{r} = r矩阵,我们只能对原数据进行拟合,使得r^\hat{r}在已有的数据集上尽可能地接近rr。这里我们使用SGD算法进行拟合。

同时我们注意到对不同的用户,有些可能会偏好打低分,而有些喜欢打高分,对不同的物品也是如此,我们必须减去这些影响才能得到用户之间、物品之间的差距。

r^ui=μ+bu+bi+puqiTerr=ruir^uiMSE=(u,i)K(ruir^ui)2+α(bu2+bi2+pu2+qi2)SGDMSEbu+=2err2αbubi+=2err2αbipu+=2errqi2αpuqi+=2errpu2αqipuqip:nuser×kq:nitem×kpu:1×kqi:1×k\hat{r}_{ui} = \mu + b_u + b_i + p_u q_i^T\\ err = r_{ui} - \hat{r}_{ui}\\ 为了防止过拟合,加入正则项\\ MSE = \sum_{(u,i) \in K} (r_{ui} - \hat{r}_{ui})^2 + \alpha (||b_u||^2 + ||b_i||^2 + ||p_u||^2 + ||q_i||^2)\\ 应用SGD算法,对MSE中的各个变量求偏导\\ b_u' += 2*err-2\alpha b_u\\ b_i' += 2*err-2\alpha b_i\\ p_u' += 2*err*q_i-2\alpha p_u\\ q_i' += 2*err*p_u-2\alpha q_i\\ 注意这里p_u'、q_i'的更新不存在维度问题,p:n_{user}\times k、q:n_{item}\times k\\ p_u:1\times k、q_i:1\times k


实验数据

输入:影评字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
critics = {'Lisa Rose': 
{'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5,
'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5,
'The Night Listener': 3.0},
'Gene Seymour':
{'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5,
'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0,
'You, Me and Dupree': 3.5},
'Michael Phillips':
{'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0,
'Superman Returns': 3.5, 'The Night Listener': 4.0},
'Claudia Puig':
{'Snakes on a Plane': 3.5, 'Just My Luck': 3.0,
'The Night Listener': 4.5, 'Superman Returns': 4.0,
'You, Me and Dupree': 2.5},
'Mick LaSalle':
{'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0,
'You, Me and Dupree': 2.0},
'Jack Matthews':
{'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0,
'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5},
'Toby':
{'Snakes on a Plane': 4.5, 'You, Me and Dupree': 1.0, 'Superman Returns': 4.0}}

目标,为Toby进行推荐。

我们进行模块化设计,以下为矩阵分解推荐系统的代码:

首先我们对要用的变量进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def __init__(self, k):
'''
:param k: 隐因子数量
'''
self.k = k
self.Pu = np.random.random(size=(1, k))
self.Qi = np.random.random(size=(1, k))
self.Bu = np.random.random(1)
self.Bi = np.random.random(1)
self.Users = []
self.Items = []
self.userItemDic = {}
self.sampleSet = np.array([])
self.mu = 0

定义添加数据集的函数,为了能多次添加数据,我们每次都会更新模型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def addUserItemDic(self, UIDic: dict):
# 先将训练集变回list,方便append数据
self.sampleSet = list(self.sampleSet)
for user in UIDic.keys():
if user not in self.userItemDic.keys():
self.userItemDic[user] = {}
self.Users.append(user)
for item in UIDic[user]:
if item not in self.userItemDic[user].keys():
self.userItemDic[user][item] = UIDic[user][item]
self.Items.append(item)
self.sampleSet.append((user, item, UIDic[user][item]))
# 将训练集变回np数组,方便使用fancy index
self.sampleSet = np.array(self.sampleSet)
# 更新总体均值
self.mu = 0
userMeanScore = []
for user in self.userItemDic.keys():
userMeanScore.append(np.mean(list(self.userItemDic[user].values())))
self.mu = np.mean(userMeanScore)
# 使用set来清除重复项,方便后续做索引
self.Users = list(set(self.Users))
self.Items = list(set(self.Items))
# 更新其它基本参数
self.Pu = np.random.random(size=(len(self.Users), self.k))
self.Qi = np.random.random(size=(len(self.Items), self.k))
self.Bu = np.random.random(len(self.Users))
self.Bi = np.random.random(len(self.Items))

定义预测函数:

1
2
3
def pred(self, user, item):
iUser, iItem = self.Users.index(user), self.Items.index(item)
return np.float(self.mu + self.Bu[iUser] + self.Bi[iItem] + self.Pu[iUser].dot(self.Qi[iItem].T))

使用SGD的训练函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def train(self, epoch=5, n=100, trainTestRate=0.8, lr=0.1, alpha=0.1):
trainMSEs = []
testMSEs = []
for i in range(epoch):
# 按比例随机抽取样本集与数据集
numOfTrain = int(trainTestRate * self.sampleSet.shape[0])
randomIndexSampleSet = np.random.choice(self.sampleSet.shape[0], self.sampleSet.shape[0], replace=False)
trainSet = self.sampleSet[randomIndexSampleSet[:numOfTrain]]
testSet = self.sampleSet[randomIndexSampleSet[numOfTrain:]]
MSE = 0
for user, item, score in trainSet:
err = float(score) - self.pred(user, item)
MSE += err ** 2
iUser, iItem = self.Users.index(user), self.Items.index(item)
self.Bu[iUser] += lr * (err - alpha * self.Bu[iUser])
self.Bi[iItem] += lr * (err - alpha * self.Bi[iItem])
self.Pu[iUser] += lr * (err * self.Qi[iItem] - alpha * self.Pu[iUser])
self.Qi[iItem] += lr * (err * self.Pu[iUser] - alpha * self.Qi[iItem])
testMSE = 0
for user, item, score in testSet:
err = float(score) - self.pred(user, item)
testMSE += err ** 2
MSE /= numOfTrain
testMSE /= (self.sampleSet.shape[0] - numOfTrain)
trainMSEs.append(MSE)
testMSEs.append(testMSE)
print('epoch:%d\tMSE:%f\t testMSE:%f' % (i, MSE, testMSE))
return trainMSEs, testMSEs

最后,定义推荐函数:

1
2
3
4
5
6
def recommend(self, user):
items = []
for item in self.Items:
items.append((item, self.pred(user, item)))
items = sorted(items, key=lambda d: d[1], reverse=True)
return items

以上就是推荐系统的全部定义,下面我们来测试推荐系统性能:

1
2
3
4
5
6
7
8
movieRecommend = RecommendSys(k=3)
movieRecommend.addUserItemDic(critics)
trainMse, testMse = movieRecommend.train(epoch=100, n=20, trainTestRate=0.7)
print(movieRecommend.recommend('Toby'))
plt.plot(trainMse, c='r', label='TrainMse')
plt.plot(testMse, c='b', label='TestMse')
plt.legend()
plt.show()

k=3、epoch=100、n=20、trainTestRate=0.7的条件下我们得到了如下结果:

得到的Toby推荐评分矩阵为:

Snakes on a Plane 4.369445
Superman Returns 3.865079
The Night Listener 3.511469
Lady in the Water 3.047306
Just My Luck 2.428205
You, Me and Dupree 1.211615

我们继续在不同k的情况下继续实验:

k = 5
k = 10
k = 1

显然k越大数据变化越稳定,但是模型占据的空间就更大了,且模型更容易过拟合。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!