推荐系统之矩阵分解MF原理及Python实现

矩阵分解基本原理

将mn维的共现矩阵R分解为mk维的用户矩阵U和k*n维的物品矩阵V相乘的形式。其中m是用户数量,n是物品数量,k是隐向量维度。k的大小决定了隐向量表达能力的强弱。k取值越小,隐向量的表达能力就越弱;反之,k取值越大,隐向量表达能力越强
实例:
在这里插入图片描述
基于用户矩阵U和物品矩阵V,用户u对物品i的预估评分为:
r ^ u i = q i T p u \hat{r}_{ui}=q_{i}^{T}p_{u} r^ui=qiTpu
参数说明:
qi:物品i的隐向量
pu:用户u的隐向量

用户矩阵U与物品矩阵V求解

在这里插入图片描述

实现矩阵分解Python代码

程序亲测有效,程序中附有详细说明

import numpy as np
import pandas as pd
import os
import time
import math


'''
说明: 使用的数据集 MovieLens 100K
数据集下载地址: https://grouplens.org/datasets/movielens/100k/
'''


'''
参数说明:
    R:用户-物品对应的共现矩阵 m*n
    P:用户因子矩阵 m*k
    Q:物品因子矩阵 k*n
    K:隐向量的维度 
    steps:最大迭代次数
    alpha:学习率
    Lambda:L2正则化的权重系数
'''


# 将矩阵R分解成P,Q
def matrix_factorization(R, P, Q, K, steps, alpha=0.05, Lambda=0.002):
    # 总时长
    sum_st = 0
    # 前一次的损失大小
    e_old = 0
    # 程序结束的标识
    flag = 1
    # 梯度下降结束条件1:满足最大迭代次数
    for step in range(steps):
        # 每次跌代开始的时间
        st = time.time()
        cnt = 0
        e_new = 0
        for u in range(1, len(R)):
            for i in range(1, len(R[u])):
                if R[u][i] > 0:
                    eui = R[u][i] - np.dot(P[u, :], Q[:, i])
                    for k in range(K):
                        temp = P[u][k]
                        P[u][k] = P[u][k] + alpha * eui * Q[k][i] - Lambda * P[u][k]
                        Q[k][i] = Q[k][i] + alpha * eui * temp - Lambda * Q[k][i]
        for u in range(1, len(R)):
            for i in range(1, len(R[u])):
                if R[u][i] > 0:
                    cnt += 1
                    e_new = e_new + pow(R[u][i] - np.dot(P[u, :], Q[:, i]), 2)
        e_new = e_new / cnt
        et = time.time()
        sum_st = sum_st + (et - st)
        # 第一次迭代不执行前后损失之差
        if step == 0:
            e_old = e_new
            continue
        # 梯度下降结束条件2:loss过小,跳出
        if e_new < 1e-3:
            flag = 2
            break
        # 梯度下降结束条件3:前后loss之差过小,跳出
        if (e_old - e_new) < 1e-10:
            flag = 3
            break
        else:
            e_old = e_new
    print(f'--------Summary---------\nThe type of jump out:{flag}\nTotal steps:{step+1}\nTotal time:{sum_st}\n'
          f'Average time:{sum_st / (step+1)}\nThe e is :{e_new}')
    return P, Q


#查看数据内容
def view_data():
    rtnames = ['user', 'item', 'score', 'time']
    data = pd.read_csv("D:\\YSA\\MovieLens\\ml-100k\\ML100K.txt", sep='\t', header=None, names=rtnames)
    # print(data.head())
    # 用户数量
    u_cnt = len(np.unique(data[rtnames[0]]))
    # 项目数量
    i_cnt = len(np.unique(data['item']))
    # 记录数量
    r_cnt = len(data)
    # 用户最小访问项目数量
    u_cnt_min = min(data['user'].value_counts().values)
    # 用户平均访问项目数量
    u_cnt_avg = r_cnt / u_cnt
    # 项目最小被访问量
    i_cnt_min = min(data['item'].value_counts().values)
    # 项目平均被访问量
    i_cnt_avg = r_cnt / i_cnt


# 分割数据集成训练集、测试集
def split_data(dformat):
    # 读取原始数据
    rating = pd.read_csv("D:\\YSA\\MovieLens\\ml-100k\\ML100K.txt", sep='\t', header=None, names=dformat)
    # 按照时间顺序排序
    rating.sort_values(by=['time'], axis=0, inplace=True)
    # 按照时间顺序值8:2,确定边界线
    boundary = rating['time'].quantile(0.8)
    # 按时间分界点切分数据,生成训练集
    train = rating[rating['time'] < boundary]
    # 训练集按用户、时间顺序排序
    train.sort_values(by=['user', 'time'], axis=0, inplace=True)
    # 按时间分界点切分数据,生成测试集
    test = rating[rating['time'] >= boundary]
    # 测试集按用户、时间顺序排序
    test.sort_values(by=['usr', 'time'], axis=0, inplace=True)
    data = pd.concat([train, test])
    # 确认目录是否存在
    if os.path.exists("D:\\YSA\\MovieLens\\ml-100k"):
        pass
    else:
        os.mkdir("D:\\YSA\\MovieLens\\ml-100k")
    # 将训练集、测试集写入文件中
    train.to_csv("D:\\YSA\\MovieLens\\ml-100k\\ML100K_Train.txt", sep=',', index=False, header=None)
    test.to_csv("D:\\YSA\\MovieLens\\ml-100k\\ML100K_test.txt", sep=',', index=False, header=None)
    print(f'split data complete!')


# 获取本地数据
def getData(path, dformat):
    # 读取用户-共现矩阵数据
    rating = np.loadtxt(path+"\\Basic_MF\\rating.txt", delimiter=',', dtype=float)
    # 读取训练集数据
    trainData = pd.read_csv(path+"\\ML100K_Train.txt", sep=',', header=None, names=dformat)
    # 读取测试集数据
    testData = pd.read_csv(path+"\\ML100K_test.txt", sep=',', header=None, names=dformat)
    data = pd.concat([trainData, testData])
    # 总用户数量
    all_user = np.unique(data['user'])
    # 总项目数量
    all_item = np.unique(data['item'])
    return rating, trainData, testData, all_user, all_item


# 生成用户-物品矩阵并保存到本地文件中
def getUserItem(path, train, all_user, all_item):
    train.sort_values(by=['user, item'], axis=0, inplace=True)
    # 用户-项目共现矩阵行数
    num_user = len(all_user)+1
    # 用户-项目共现矩阵列数
    num_item = len(all_item)+1
    # 用户-项目共现矩阵初始化
    rating_mat = np.zeros([num_user, num_item], dtype=int)
    # 用户-项目共现矩阵赋值
    for i in range(len(train)):
        user = train.iloc[i]['user']
        item = train.iloc[i]['item']
        score = train.iloc[i]['score']
        rating_mat[user][item] = score
    # 判断文件夹是否存在
    if os.path.exists(path+"\\BasicMF"):
        pass
    else:
        os.mkdir(path+"\\BasicMF")
    # 保存用户-项目共现矩阵到文件
    np.savetxt(path+"\\BasicMF\\rating.txt", rating_mat, fmt='%d', delimiter=',', newline='\n')
    print(f'generate rating matrix complete!')


#	训练
def train(path, rating, K, steps):
    R = rating
    M = len(R)
    N = len(R[0])
    # 用户矩阵初始化
    P = np.random.normal(loc=0, scale=0.01, size=(M, K))
    # 项目矩阵初始化
    Q = np.random.normal(loc=0, scale=0.01, size=(K, N))
    P, Q = matrix_factorization_BiasSVD(R, P, Q, K, steps)
    # 判断文件夹是否存在
    if os.path.exists(path + "\\Basic_MF"):
        pass
    else:
        os.mkdir(path + "\\Basic_MF")
    # 将P,Q保存到文件
    np.savetxt(path+"\\Basic_MF\\userMatrix.txt", P, fmt="%.6f", delimiter=',', newline='\n')
    np.savetxt(path+"\\Basic_MF\\itemMatrix.txt", Q, fmt="%.6f", delimiter=',', newline='\n')
    print("train complete!")


# 生成topk推荐列表
def topK(dic, k):
    keys = []
    values = []
    for i in range(k):
        key, value = max(dic.items(), key=lambda x: x[1])
        keys.append(key)
        values.append(value)
        dic.pop(key)
    return keys, values


# 测试
def test(path, trainData, testData, all_item, k):
    # 读取用户矩阵
    P = np.loadtxt(path+"\\Basic_MF\\userMatrix.txt", delimiter=',', dtype=float)
    # 读取项目矩阵
    Q = np.loadtxt(path+"\\Basic_MF\\itemMatrix.txt", delimiter=',', dtype=float)
    # 测试集中的用户集合
    testUser = np.unique(testData['user'])
    # 测试集的长度
    test_lenght = len(testData)

    Hits = 0
    MRR = 0
    NDCG= 0
    # 开始时间
    st = time.time()
    for user_i in testUser:
        # 测试集第i个用户在训练集已访问的项目
        visited_list = list(trainData[trainData['user'] == user_i]['item'])
        # 没有训练数据,跳过
        if len(visited_list) == 0:
            continue
        # 测试集第i个用户的访问项目并去重
        test_list = list(testData[testData['user'] == user_i]['item'].drop_duplicates())
        # 测试集第i个用户的访问项目中去除该用户在训练集已访问的项目
        test_list = list(set(test_list) - set(test_list).intersection(set(visited_list)))
        # 测试集第i个用户的访问项目为空,跳过
        if len(test_list) == 0:
            continue
        # 生成测试集第i个用户未访问的项目:评分对
        poss = {
    
    }
        for item in all_item:
            if item in visited_list:
                continue
            else:
                poss[item] = np.dot(P[user_i, :], Q[:, item])
        # 生成测试集第i个用户的推荐列表
        ranked_list, test_score = topK(poss, k)
        # 命中测试集第i个用户访问项目的列表
        h = list(set(test_list).intersection(set(ranked_list)))
        Hits += len(h)
        for item in test_list:
            for i in range(len(ranked_list)):
                if item == ranked_list[i]:
                    MRR += 1 / (i+1)
                    NDCG += 1 / (math.log2(i+1+1))
                else:
                    continue
    HR = Hits / test_lenght
    MRR /= test_lenght
    NDCG /= test_lenght
    # 结束时间
    et = time.time()
    print("HR@10:%.4f\nMRR@10:%.4f\nNDCG@10:%.4f\nTotal time:%.4f" % (HR, MRR, NDCG, et-st))


if __name__ == '__main__':
    rtnames = ['user', 'item', 'score', 'time']
    path = "D:\\YSA\\MovieLens\\ml-100k"
    rating, trainData, testData, all_user, all_item = getData(path, rtnames)
    # train(path, rating, 30, 10)
	test(path, trainData, testData, all_item, 10)

参考

1.https://blog.csdn.net/qq_34862636/article/details/105432139

猜你喜欢

转载自blog.csdn.net/shiaiao/article/details/109119588