- 生成用户-物品共现矩阵
- UserCF
- ItemCF
- 基于梯度下降的矩阵分解
以用户为行,电影为列生成共现矩阵,所采用数据为movieLens-100k
#读取原始数据
def loadData(path,names=False):
data = pd.read_table(path,header=None,sep="t",names=names)
data = data.drop("time",axis=1)
return data
原始数据
#生成共现矩阵
def transformData(data):
#生成空矩阵,逐渐往里面填值
#这个方法非常的耗时间,记得保存结果,所以采用双指针
#注意movie_id的最大值是比data_rating.shape[1]大的,说明电影id并不全是连续的
GXdata = pd.Dataframe(0.0,columns=list(range(1,max(data_rating["movie_id"]))) , index = list(range(1,max(data_rating["user_id"]))))
#循环data_rating赋值
'''
for i in range(data_rating.shape[0]):
uid = data_rating.iloc[i,0]
mid = data_rating.iloc[i,1]
rate = data_rating.iloc[i,2]
#直接赋值
GXdata.loc[uid,mid] = rate
'''
start = time.time()
#双指针
j = data.shape[0]
for i in range(data.shape[0] // 2):
if (i == j):
break
uid_l = data.iloc[i,0]
mid_l = data.iloc[i,1]
rate_l = data.iloc[i,2]
j -= 1
uid_r = data.iloc[j,0]
mid_r = data.iloc[j,1]
rate_r = data.iloc[j,2]
#直接赋值
GXdata.loc[uid_l,mid_l] = rate_l
GXdata.loc[uid_r,mid_r] = rate_r
if time.time() - start > 120:
break
return np.mat(GXdata)
%%time= 20.6s
结果数据
主要步骤:
- 计算用户的相似度,获得top-K个相似用户。
- 根据Top-K中的用户有待预测用户没有的物品,计算待预测用户对该物品的预测评分。
- 排序,最后获取TOP-N的推荐列表。
计算相似度有余弦相似系数和皮尔逊相似系数,这里采用皮尔逊相关系数计算相似度:
s
i
m
(
i
,
j
)
=
∑
p
P
(
R
i
,
p
−
R
i
ˉ
)
(
R
j
,
p
−
R
j
ˉ
)
∑
p
ϵ
P
(
R
i
,
p
−
R
i
ˉ
)
2
∑
p
ϵ
P
(
R
j
,
p
−
R
j
ˉ
)
2
sim(i,j) = frac{sum_{p P}^{}{(R_{i,p} - bar {R_i})} (R_{j,p} - bar {R_j})}{ sqrt {sum_ {pepsilon P}^{}{(R_{i,p} - bar {R_i})^2}}sqrt {sum_ {pepsilon P}^{}{(R_{j,p} - bar {R_j})^2}}}
sim(i,j)=∑pϵP(Ri,p−Riˉ)2
∑pϵP(Rj,p−Rjˉ)2
∑pP(Ri,p−Riˉ)(Rj,p−Rjˉ)
R
i
,
p
R_{i,p}
Ri,p代表用户 i 对物品 p 的评价,
R
i
,
p
ˉ
bar {R_{i,p}}
Ri,pˉ代表用户 i 对所有物品的评分。
得到前k个用户后,利用用户的相似度与相似用户的评分加权平均获得目标用户的评分预测。
R
u
,
p
=
∑
s
ϵ
S
W
u
,
s
∗
R
s
,
p
∑
s
ϵ
S
W
u
,
s
R_{u,p} = frac {sum_{sepsilon S }^{}{W_{u,s} * R_{s,p}}}{sum_{sepsilon S }^{}{W_{u,s}}}
Ru,p=∑sϵSWu,s∑sϵSWu,s∗Rs,p
W
u
,
s
W_{u,s}
Wu,s使用户u与用户s的相似度,
R
s
,
p
R_{s,p}
Rs,p用户s对物品p的评分。在最后获得用户u的预测评分后,排序推荐。
#皮尔逊
def piErXun(u1,u2,m1,m2):
#m1,m2是平均值
#u1,u2是mat
#例如 u1 = matrix[[0.1,0.2,0.3]]
fenzi = 0.0 #分子
fenmu_1 = 0.0 #分母
fenmu_2 = 0.0
for p in range(u1.shape[1]):
fenzi += (u1[0,p] - m1)*(u2[0,p] - m2)
fenmu_1 += (u1[0,p] - m1)**2
fenmu_2 += (u2[0,p] - m2)**2
assert fenmu_1 > 0.0;
assert fenmu_2 > 0.0;
return fenzi / (np.sqrt(fenmu_1)*np.sqrt(fenmu_2))
#根据uid计算与uid相似的n个用户
#注意有用户从没有打过分
#注意id属性与index差1
def simUsers(data,uid,n):
sim_dict = {}
u1 = data[uid-1]
m1 = np.mean(u1)
for i in range(data.shape[0]):
if i == uid-1:
continue
u2 = data[i]
m2 = np.mean(u2)
if(m1 == 0.0 or m2 == 0.0):
continue
sim_dict[i+1] = piErXun(u1,u2,m1,m2)
sim_dict = sorted(sim_dict.items(),key=lambda x:x[1] ,reverse=True)
return sim_dict[:n]
获取的topk相似用户格式:(user_id,sim_rate)
[(916, 0.48794913303276216),
(738, 0.4711048400073322),
(864, 0.4652827485719093),
(457, 0.457111312198188),
(268, 0.45710496926467037),
(823, 0.45480511185283184),
(92, 0.44542969870214305),
(514, 0.4453589932403556),
(435, 0.4448146761911006),
(521, 0.4414895704371854)]
#获得推荐
def getTopN(data,uid,k,N):
sim_list = simUsers(uid,k)
R={}
sid = [] #相似用户id
sid_r = [] #相似用户相似度
#遍历sim_list,获取相似用户id
for item in sim_list:
sid.append(item[0])
sid_r.append(item[1])
#遍历所有相似用户对同一物品的打分
#注意id属性与index相差1
for m_index in range(data.shape[1]):
#temp作为验证,如果所用相似用户都没有对物品p打分,则跳过
temp = 0.0
for i in sid:
temp += data[i-1,m_index]
if temp == 0.0:
continue
fenzi = 0.0
for s in range(len(sid)):
w = sim_list[s][1] #相似度
r = data[sid[s]-1,m_index] #相似用户的评分
fenzi += w*r
R[m_index+1] = fenzi / np.sum(sid_r)
R_list = sorted(R.items(),key=lambda x : x[1] ,reverse=True)
return R_list[:N]
结果数据格式:(movie_id,pred_rate)
[(50, 4.853019903084822),
(174, 4.7570347839825375),
(173, 4.500476600347472),
(168, 4.340274579343471),
(56, 4.299741435055532),
(98, 4.258438814588287),
(100, 4.195530901885508),
(12, 4.190947260864641),
(181, 4.151045871349933),
(172, 4.1184228618907355)]
- 计算两两物品之间的相似度,生成N * N的的相似度矩阵
- 获取特定用户行为数据中的正反馈物品列表(即有评分的数据)
- 根据物品相似矩阵,找出正反馈物品列表中的每一个物品的相似Top-k个物品。
- 总体排序去重,最后得出Top-N的推荐列表
相识度的计算方法与UserCF相同,评分预测有所不同:
R
u
,
p
=
∑
h
ϵ
H
(
W
p
,
h
)
(
R
u
,
h
)
R_{u,p} = sum_{hepsilon H}^{}{(W_{p,h})(R_{u,h})}
Ru,p=hϵH∑(Wp,h)(Ru,h)
H是用户评分的物品集合,
W
p
,
h
W_{p,h}
Wp,h是物品p与物品h的相似度,
R
u
,
h
R_{u,h}
Ru,h是用户u对物品h的评分。
参照userCF的格式实现ItemCF主要代码:
#需要转置以适应相似度的计算
def Simitems(data,mid,k,H):
sim_dict = {}
m1 = data[:,mid-1].T
mean_1 = np.mean(m1)
for i in range(data.shape[1]):
if i == mid-1:
continue
#新增
#如果遍历到我的物品id没有在H中则跳过
if i+1 not in H:
continue
m2 = data[:,i].T
mean_2 = np.mean(m2)
if(mean_1 == 0.0 or mean_2 == 0.0):
continue
sim_dict[i+1] = piErXun(m1,m2,mean_1,mean_2)
sim_dict = sorted(sim_dict.items(),key=lambda x:x[1] ,reverse=True)
return sim_dict[:k]
输出相似物品的格式为:movie_id,sim_rate
[(20, 0.3468836171749395),
(190, 0.3307832057541817),
(713, 0.32322768977079996),
(52, 0.3155875913057789),
(582, 0.30364775830684687),
(1115, 0.28013338640311797),
(847, 0.26913435730393254),
(83, 0.2661533573470109),
(652, 0.2594745844252282),
(387, 0.2588421063414054),
(277, 0.2565579931526503),
(489, 0.2554860245820769),]
def getItemsTopN(data,uid,k,N):
R={}
H = []
#注意mid并不连续,data是共现矩阵
#注意id属性与index相差1
#获取用户的正反馈物品列表H
for i in range(data.shape[1]):
if data[uid-1,i] != 0.0:
H.append(i+1) #index+1变为mid
#遍历H,又是一个很耗时间的循环
#分析:如果用户评分为0则乘以相似度也为0,所以直接滤过用户没有评分的movie,改进Simitems,新增H参数
for mid in H:
#寻找k个相似物品,主要耗时间的地方
sim_movie_list = Simitems(data,mid,k,H)
#遍历sim_movie_list计算预估评分
for smid,srate in sim_movie_list:
w = srate #mid与smid的相似度
r = data[uid-1,smid-1] #用户对相似物品的评分,注意有可能是0
#会有相似的重合物品,不用管,相似物品直接多个累计和计算
if smid not in R.keys():
R[smid] = 0.0
R[smid] += w*r
#排序
R_list = sorted(R.items(),key=lambda x:x[1],reverse=True)
return R
输出数据格式为:(mid , sim(相似度))
[(174, 115.60181846851796),
(204, 99.9275867037441),
(82, 98.31279014448701),
(172, 69.53792735764443),
(195, 68.83252826645494),
(96, 64.21457459796717),
(161, 64.0777873016334),
(168, 62.61665393465903),
(176, 57.493175149912446),
(202, 53.59897101952933)]
矩阵分解是将共现矩阵M分为两个矩阵U与V的乘积,M是稀疏矩阵,U和V是密集矩阵。U代表用户矩阵,V代表物品矩阵。若要计算用户对物品的预测评分,将U中的一行乘以V就行。减少了空间的消耗。
- 确定目标函数和各个参数的迭代过程
- 分解原始共现矩阵,得到U,V
- 根据U,V获得推荐结果
假设原来的得共现矩阵维度为MN,设用户矩阵和物品矩阵的维度分别为MK和K*N,所以近似得到:
M
m
∗
n
≈
U
m
∗
k
∗
V
k
∗
n
M_{m*n} approx U_{m*k}*V_{k*n}
Mm∗n≈Um∗k∗Vk∗n
则用户u对物品i的预测股份公式为:
r
^
u
,
i
=
q
i
T
∗
p
u
hat{r}_{u,i} = q_i^T * p_u
r^u,i=qiT∗pu
p
u
p_u
pu是用户u在用户矩阵U中的一行,
q
i
T
q_i^T
qiT是物品i在物品矩阵中对应的一列
有了目标函数就可以得出误差函数为:
m
i
n
q
,
p
∑
(
u
,
i
ϵ
K
)
(
r
u
i
−
q
i
T
∗
p
u
)
2
+
λ
(
∣
∣
q
i
∣
∣
2
+
∣
∣
p
u
∣
∣
2
)
underset{q,p}{min} sum_{(u,iepsilon K)}^{}{(r_{ui} - q_i^T*p_u )^2 + lambda(||q_i||^2 + ||p_u||^2)}
q,pmin(u,iϵK)∑(rui−qiT∗pu)2+λ(∣∣qi∣∣2+∣∣pu∣∣2)
λ
(
∣
∣
q
i
∣
∣
2
+
∣
∣
p
u
∣
∣
2
)
lambda(||q_i||^2 + ||p_u||^2)
λ(∣∣qi∣∣2+∣∣pu∣∣2)为正则化项是为了防止过拟合。引入梯度下降找到是的误差最小的参数q,p。则误差函数分别对p,q求偏导得出p,q的迭代式子为:
q i ← q i − γ ( ( r u i − q i T p u ) p u − λ q i ) q_i leftarrow q_i - gamma((r_{ui} - q_i^Tp_u )p_u - lambda q_i) qi←qi−γ((rui−qiTpu)pu−λqi)
p
u
←
p
u
−
γ
(
(
r
u
i
−
q
i
T
p
u
)
q
i
−
λ
p
u
)
p_u leftarrow p_u - gamma((r_{ui} - q_i^Tp_u )q_i - lambda p_u)
pu←pu−γ((rui−qiTpu)qi−λpu)
γ
gamma
γ为全局学习率。
def getRandUandV(M, k=200, lr=0.01, alpha=0.1, steps=100):
# steps 为迭代次数
# lr为学习率
# alpha为正则化系数
m, n = M.shape
# 初始化U,V,这里写作P--用户,Q--物品
P = np.random.rand(m, k)
Q = np.random.rand(n, k).T
R = np.zeros_like(M)
# 注意id属性与index相差1
# 遍历P的每一行去与Q相乘,得到近似的M,再过程之中计算误差与梯度
# 计算下遍历次数 : 100 * m * n = 100 * 1681 * 943 = 161880300 我算个锤子
error = []
for _ in range(steps):
err = 0.0
for i in range(P.shape[0]):
# uid = i+1
u = P[i]
R[i] = np.matmul(u,Q)
e_u = np.sum(np.power(u, 2))
# 遍历Q的每一列
for j in range(Q.shape[1]):
# 计算误差
# 正则化常数为
z = alpha * (np.sum(np.power(Q[:, j], 2) + e_u))
err += (np.power((M[i,j] - R[i,j]), 2) + z)
# 更新P,Q,顺序不可颠倒
#P[i]更新次数多于Q[:,j]
P[i] -= lr * ((M[i,j] - R[i,j])*Q[:, j] - alpha * P[i])
Q[:, j] -= lr * ((M[i,j] - R[i,j])*u - alpha * Q[:, j])
#如果err小于阈值可以中止
error.append(err)
return R,P,Q
最后推荐只需要用U[mid] * V 结果进行排序就行。
参考资料:
《深度学习推进系统》-王喆



