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

本文介绍了推荐系统中的两种经典方法:UserCF和ItemCF,以及基于梯度下降的矩阵分解。UserCF通过计算用户相似度进行推荐,而ItemCF则计算物品之间的相似度。此外,还详细展示了如何实现这两个方法,包括计算用户-物品共现矩阵,计算用户和物品的相似度,以及使用矩阵分解进行预测。
摘要由CSDN通过智能技术生成

生成用户-物品共现矩阵

以用户为行,电影为列生成共现矩阵,所采用数据为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

结果数据
在这里插入图片描述

UserCF

主要步骤:

  1. 计算用户的相似度,获得top-K个相似用户。
  2. 根据Top-K中的用户有待预测用户没有的物品,计算待预测用户对该物品的预测评分。
  3. 排序,最后获取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_ {p\epsilon P}^{}{(R_{i,p} - \bar {R_i})^2}}\sqrt {\sum_ {p\epsilon P}^{}{(R_{j,p} - \bar {R_j})^2}}} sim(i,j)=pϵP(Ri,pRiˉ)2 pϵP(Rj,pRjˉ)2 pP(Ri,pRiˉ)(Rj,pRjˉ)
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_{s\epsilon S }^{}{W_{u,s} * R_{s,p}}}{\sum_{s\epsilon S }^{}{W_{u,s}}} Ru,p=sϵSWu,ssϵSWu,sRs,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)]

ItemCF

  1. 计算两两物品之间的相似度,生成N * N的的相似度矩阵
  2. 获取特定用户行为数据中的正反馈物品列表(即有评分的数据)
  3. 根据物品相似矩阵,找出正反馈物品列表中的每一个物品的相似Top-k个物品。
  4. 总体排序去重,最后得出Top-N的推荐列表

相识度的计算方法与UserCF相同,评分预测有所不同:
R u , p = ∑ h ϵ H ( W p , h ) ( R u , h ) R_{u,p} = \sum_{h\epsilon 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就行。减少了空间的消耗。

  1. 确定目标函数和各个参数的迭代过程
  2. 分解原始共现矩阵,得到U,V
  3. 根据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} MmnUmkVkn
则用户u对物品i的预测股份公式为:
r ^ u , i = q i T ∗ p u \hat{r}_{u,i} = q_i^T * p_u r^u,i=qiTpu
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,i\epsilon K)}^{}{(r_{ui} - q_i^T*p_u )^2 + \lambda(||q_i||^2 + ||p_u||^2)} q,pmin(u,iϵK)(ruiqiTpu)2+λ(qi2+pu2)
λ ( ∣ ∣ q i ∣ ∣ 2 + ∣ ∣ p u ∣ ∣ 2 ) \lambda(||q_i||^2 + ||p_u||^2) λ(qi2+pu2)为正则化项是为了防止过拟合。引入梯度下降找到是的误差最小的参数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) qiqiγ((ruiqiTpu)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) pupuγ((ruiqiTpu)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 结果进行排序就行。

参考资料:
《深度学习推进系统》-王喆

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值