0%

基于SVD++的协同过滤

基于SVD++的协同过滤理解和复现的一点记录。

程序入口

  • 日志对象;
  • 读取数据/分割数据集;
  • 创建SVD++模型对象/初始化参数;
  • 训练SVD++模型;
  • 预测评分并评估模型。
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
import os
from logger import Logger
from movielens import RatingData
from svdPlusPlus import SVDPlusPlus
import winsound

# 获取日志对象
logPath = os.path.join('logs', 'svdPP.log')
logger = Logger.getLogger(logPath)

# 读取数据集
filepath = os.path.join('ml-1m', 'ratings.dat')
#dataset = RatingData.getSmallRatingData(logger, filepath, 0.7, 0.3, False)
dataset = RatingData.getRatingDict(logger, filepath, 0.9, True)
trainset = dataset[RatingData.TRAIN_SET_KEY]
testset = dataset[RatingData.TEST_SET_KEY]
historyset = trainset
topicNum = 8
lr = 0.0006
gamma = 0.03
svdPlusPlus = SVDPlusPlus(logger, trainset, historyset, topicNum, lr, gamma)
svdPlusPlus.train(100, topN=10)
#svd预测
svdPlusPlus.evaluate(testset, topN=10)
winsound.Beep(500, 500)

SVD++模块

其他模块与 SVD博客 中的同名模块一致,只有svd模块改为svdPlusPlus模块SVDPlusPlus类继承SVD类++指加入了隐反馈信息(用户浏览、点击等反馈信息),我这里没有,故暂且把训练集也当做隐反馈信息,命名为历史数据集historyset)。

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
from svd import SVD
import numpy as np
import traceback

class SVDPlusPlus(SVD):
''' 基于SVD++的协同过滤算法 '''
def __init__(self, logger, trainset, historyset, topicNum, lr, gamma):
''' 初始化函数
Args:
logger:日志记录对象
trainset:训练集
historyset:历史数据集
topicNum:主题个数,奇异值数量
lr:学习率
gamma:正则项权重
'''
self.historyset = historyset
self.yItem = {}
super().__init__(logger, trainset, topicNum, lr, gamma)

def initModel(self):
''' 初始化模型参数:biasUser、biasItem、pUser、qItem、yItem '''
super().initModel()
for item in self.biasItem.keys():
self.yItem[item] = np.random.rand(self.topicNum) / 10

def train(self, iteration, topN):
''' 迭代多批次训练SVD模型
min power(rated - rate, 2)/2 + gamma * regularization/2
rate = mean + biasUser + biasItem + qItem * (pUser + yItemUserSum/sqrt(itemCounter))
itemCounter = sqrt(len(self.historyset[user].keys()))
yItemUserSum = sum(map(lambda key:self.yItem[key], self.historyset[user].keys()))
regularization = power(biasUser) + power(biasItem) + power(pUser) + power(qItem) + power(yItem)
故每次迭代:lr = learning rate
delta(biasUser) = lr * ((rate - rated) + gamma * biasUser)
delta(biasItem) = lr * ((rate - rated) + gamma * biasItem)
delta(qItem) = lr * ((rate - rated) * (pUser + yItemUserSum/itemCounter) + gamma * qItem)
delta(pUser) = lr * ((rate - rated) * qItem + gamma * pUser)
delta(yItem) = lr * (rate - rated) * qItem/itemCounter + gamma * yItem)
Args:
iteration:迭代次数
'''
self.logger.info('开始训练...')
sampleSize = len(self.trainMap) # 训练样本总数
sampleSeq = np.arange(sampleSize) # 训练样本号的训练队列
# 迭代训练
self.logger.info('训练前,误差为:')
self.evaluate(self.trainset, topN)
for i in range(iteration):
np.random.shuffle(sampleSeq) # 打乱训练样本号的训练队列
# 取样本号,逐样本训练
for j in sampleSeq:
user, item = self.trainMap[j]
rate = self.predict(user, item) # 预测得分,计算差异
rated = self.trainset[user][item]
itemCounter = np.sqrt(len(self.historyset[user].keys()))
yItemUserSum = sum(map(lambda key:self.yItem[key], self.historyset[user].keys()))
# 计算预测误差,并更新参数
rateDiff = rate - rated
self.biasUser[user] -= (self.lr * (rateDiff + self.gamma * self.biasUser[user]))
self.biasItem[item] -= (self.lr * (rateDiff + self.gamma * self.biasItem[item]))
self.qItem[item] -= (self.lr * (rateDiff * (self.pUser[user] + yItemUserSum/itemCounter) +
self.gamma * self.qItem[item]))
self.pUser[user] -= (self.lr * (rateDiff * self.qItem[item] +
self.gamma * self.pUser[user]))
# 针对该用户的历史评分物品,更新物品被浏览时,获得的偏好向量
for historyItem in self.historyset[user].keys():
self.yItem[historyItem] -= (self.lr * (rateDiff * self.qItem[item]/itemCounter +
self.gamma * self.yItem[historyItem]))

if i % 10 == 0:
self.logger.info('训练进度[{i}/{iteration}],训练误差为:'.format(i=i,
iteration=iteration))
self.evaluate(self.trainset, topN)
self.lr *= 0.5

self.logger.info('训练后,误差为:')
self.evaluate(self.trainset, topN)
self.logger.info('训练结束!')

def predict(self, user, item):
''' 测试SVD模型
rate = mean + biasUser + biasItem + qItem * (pUser + yItemUserSum/itemCounter)
itemCounter = sqrt(len(self.historyset[user].keys()))
yItemUserSum = sum(map(lambda key:self.yItem[key], self.historyset[user].keys()))
Args:
user:待预测评分的用户
item:待预测评分的物品
Returns:
rate:用户user对物品item的评分
'''
itemCounter = np.sqrt(len(self.historyset[user].keys()))
yItemUserSum = sum(map(lambda key:self.yItem[key], self.historyset[user].keys()))
try:
rate = (self.mean + self.biasUser[user] + self.biasItem[item] +
np.dot(self.qItem[item], self.pUser[user] + yItemUserSum/itemCounter))
except ZeroDivisionError as e:
traceback.print_exc()
print(user)
print(itemCounter)
raise e
return rate

总结

SVDSVD++速度对比

先简单对比评分估计的的公式的计算时间复杂度:

SVD

SVD++:

其中为用户感兴趣的历史物品。

对单个用户单个物品而言,忽略前三项的计算时间复杂度,同时,加上乘法的时间复杂度是加法的10倍,那么SVD的时间复杂度主要是的计算,大概是O(11*topicNum); SVD++则是的计算,大概是:O(11*topicNum)+O(topicNum)+O(counter*topicNum)+O(10*topicNum),其中

综上,SVD++的计算时间复杂度可粗略估计为SVD的10倍以上。但可以通过离线计算得到模型,降低时间复杂度的影响。

对偶算法

通过数据集的转置,实现对偶算法的实现。通过两者的结合,可提高精度。然而,其实推荐更多考虑的top N推荐的准确度,对于RMSE的精度,并不是多care。

感谢对原创的支持~