推荐系统及其实现

概述

个性化的推荐系统被应用于互联网生活中的方方面面,本篇文章中,将会利用电影评分数据,实现一个简单的电影个性化推荐系统,并实现推荐系统相关的协同过滤算法(Collaborative Filter Learning Algorithm)。这些电影评分数据集的评分范围为1-5,数据集中包含着943个用户(用n_u=943表示)和1682部电影(用n_m = 1682表示 )。

电影评分数据集的处理

这些电影评分数据集被保存在一个ex8_movies.mat的文件中,数据集中矩阵Y表示电影评分数据,用y^{(i,j)}表示,其值范围是(from 1 to 5),其含义是第j个用户对第i个电影的评分数数据,其大小为n_u * n_m,矩阵R是一个二进制值的标识矩阵,如果R^{(i,j)}=1表示第j个用户对第i部的电影进行了评价,若是为0,则表示没有该用户没有对此电影做出评价。协同过滤算法的适用目标是为用户还未做出评价的电影预测评分,也就是被R^{(i,j)}=0所标识的数据,这样能够使得算法能够将预测评分最高电影推荐给用户。

为了了解数据集,可以先求出对第一个电影的评分的均值,并利用python的绘图功能绘出所有评分数据的颜色分布,用以可视化的显示相关数据。具体实现代码如下所示:

#导入相关库
import matplotlib.pyplot as plt
import numpy as np
import scipy.io as scio
import scipy.optimize as opt

# 加载数据
data = scio.loadmat('ex8_movies.mat')
Y = data['Y']
R = data['R']
#求出第一个电影的平均值
average = np.mean(Y[0,np.where(R[0]==1)])
#可视化显示
plt.figure()
plt.imshow(Y)
plt.colorbar()
plt.xlabel('Users')
plt.ylabel('Movies')

最后,以上代码数据结果如下图所示:


除了电影评分数据集中的矩阵Y和矩阵R外,对于特征矩阵X和参数矩阵\theta有如解释:
矩阵XY的形式如下图所示:

X矩阵的第i行对应着第i部电影的特征向量x^{(i)},而参数矩阵的第j行对应着第j个用户的参数向量\theta^{(j)}x^{(i)}\theta^{(j)}都是n维向量,在本此算法实现中,令n=100,即也就是矩阵X的大小是100*n_m,而矩阵\theta的大小是100 *n_u.

协同过滤算法的实现

  • 算法介绍
    电影推荐系统的协同过滤算法需要考虑n维参数向量x^{(1)},……x^{(n_m)}和向量\theta^{(1)},……\theta^{(n_u)},此模型预测的第j个用户对第i个电影的评分可以用y^{(i,j)} = ({\theta^{(j)}})^{T}*x^{(i)}表示,给定一些用户对一些电影的评分数据集,可以通过协同过滤算法得到一系列参数向量x^{(1)},……x^{(n_m)}\theta^{(1)},……\theta^{(n_u)}能够对模型实现最好的拟合(最小化平方误差)。
  • 算法描述
    尽管与单变量线性回归的实现方式很相似,但还是略有不同,单变量线性回归的实现方式只需要通过梯度下降最小化损失函数的到一个参数向量\theta,而协同过滤算法需要得到两个参数,其实现方法可以简单总结为控制变量法,即给定参数矩阵X,求出参数矩阵\theta,再将参数矩阵\theta保持不变,求出参数矩阵X, 其实现方式如下公式所示,需要注意的是,为了防止过拟合,也要加入正则项。
    给定参数矩阵x^{(1)},……x^{(n_m)},预估参数矩阵\theta^{(1)},……\theta^{(n_u)}

minJ(\theta^{(1)}……\theta^{(n_u)}) = \frac{1}{2}\sum_{j=1}^{n_u}\sum_{i:r(i,j)=1}^{n_m}((\theta^{(j)})^T*x^{(i)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum_{j=1}^{n_u}\sum_{k=1}^{n} (\theta_{k}^{(j)})^2

给定参数矩阵\theta^{(1)}……\theta^{(n_u)},预估参数矩阵参数阵x^{(1)},……x^{(n_m)}:

minJ(x^{(1)}……x^{(n_m)}) = \frac{1}{2}\sum_{i=1}^{n_m}\sum_{j:r(i,j)=1}^{n_u}((\theta^{(j)})^T*x^{(i)}-y^{(i,j)})^2+\frac{\lambda}{2}\sum_{i=1}^{n_m}\sum_{k=1}^{n} (x_{k}^{(i)})^2

综合以上,对两个参数矩阵同时进行最小化的公式如下所示:

minJ(x^{(1)},……x^{(n_m)},\theta^{(1)}……\theta^{(n_u)}) = \frac{1}{2}\sum_{(i,j):r(i,j) =1}((\theta^{(j)})^T*x^{(i)}-y^{(i,j)})^2 \frac{\lambda}{2}\sum_{j=1}^{n_u}\sum_{k=1}^{n} (\theta_{k}^{(j)})^2+\frac{\lambda}{2}\sum_{i=1}^{n_m}\sum_{k=1}^{n} (x_{k}^{(i)})^2

综合以上,对协同过滤算法的实现步骤可以有以下总结:

  1. 初始化x^{(1)},……x^{(n_m)}\theta^{(1)},……\theta^{(n_u)},并令其取得较小的随机值;

  2. 利用梯度下降算法最小化损失函数,并得到每一个参数向量,如下所示:

  • 算法实现
    1.在算法实现之前,需要对数据进行预处理操作,具体如下所示:
#加载训练前的数据
data = scio.loadmat('ex8_movieParams.mat')
X = data['X']
theta = data['Theta']
num_users = data['num_users']
num_movies = data['num_movies']
num_features = data['num_features']

#减少数据规模,能够使得数据算法运行的更快
num_users = 4
num_movies = 5
num_features = 3
X = X[0:num_movies, 0:num_features]
theta = theta[0:num_users, 0:num_features]
Y = Y[0:num_movies, 0:num_users]
R = R[0:num_movies, 0:num_users]
  1. 协同过滤损失函数的计算可如以下代码所示:
def cofi_cost_function(params, Y, R, num_users, num_movies, num_features, lmd):
    X = params[0:num_movies * num_features].reshape((num_movies, num_features))
    theta = params[num_movies * num_features:].reshape((num_users, num_features))
    cost = 0
    X_grad = np.zeros(X.shape)
    theta_grad = np.zeros(theta.shape)

    hypothesis = (np.dot(X, theta.T) - Y) * R

    cost = (1/2)*np.sum(hypothesis**2) + (lmd/2)*np.sum(theta**2) + (lmd/2)*np.sum(X**2)

    X_grad = np.dot(hypothesis, theta) + lmd * X
    theta_grad = np.dot(hypothesis.T, X) + lmd * theta

    #将两个梯度向量连接成为一维向量
    grad = np.concatenate((X_grad.flatten(), theta_grad.flatten()))

    return cost, grad
#对损失函数进行输出并评估模型性能
cost, grad = cofi_cost_function(np.concatenate((X.flatten(), theta.flatten())), Y, R, num_users, num_movies, num_features, 0)
print(cost)

注意:利用协同过滤算法得到的模型的梯度值之后,为了消除误差,需要对梯度值进行进一步检测,这个过程称之为梯度检测。具体的计算公式如下所示:
\frac{\partial}{\partial \theta}J(\theta) = \frac{J(\theta+\epsilon)-J(\theta-\epsilon)}{2\epsilon}

梯度检测的代码实现如下所示:

def compute_numerial_gradient(cost_func, theta):
    numgrad = np.zeros(theta.size)
    perturb = np.zeros(theta.size)

    e = 1e-4

    for p in range(theta.size):
        perturb[p] = e
        loss1, grad1 = cost_func(theta - perturb)
        loss2, grad2 = cost_func(theta + perturb)

        numgrad[p] = (loss2 - loss1) / (2 * e)
        perturb[p] = 0

    return numgrad

随机设置一些输入数据运行协同过滤算法和加载数据集运行协同过滤算法得到的梯度值如果比较接近,则说明梯度误差较小,该算法能得到最小的损失函数值并且能够收敛,其具体代码实现和运行结果如下所示:

def check_cost_function(lmd):

    # 创建随机的样本
    x_t = np.random.rand(4, 3) #4x3随机矩阵
    theta_t = np.random.rand(5, 3) #5x3随机矩阵

    Y = np.dot(x_t, theta_t.T)  # 4x5
    Y[np.random.rand(Y.shape[0], Y.shape[1]) > 0.5] = 0 #Y矩阵的随机位置的值大于0.5,则设为0
    R = np.zeros(Y.shape)
    R[Y != 0] = 1   #设置R矩阵部分值为1,认为某用户对某电影做出评价

    # 运行梯度检测
    x = np.random.randn(x_t.shape[0], x_t.shape[1])
    theta = np.random.randn(theta_t.shape[0], theta_t.shape[1])
    num_users = Y.shape[1]  #5
    num_movies = Y.shape[0]  #4
    num_features = theta_t.shape[1] #3
    
    #随机组的损失函数值和梯度计算
    def cost_func(p):
        return cofi_cost_function(p, Y, R, num_users, num_movies, num_features, lmd)

    numgrad = compute_numerial_gradient(cost_func, np.concatenate((x.flatten(), theta.flatten())))
    #数据集的损失函数值和梯度计算
    cost, grad = cofi_cost_function(np.concatenate((x.flatten(), theta.flatten())), Y, R, num_users, num_movies, num_features, lmd)

    print(np.c_[numgrad, grad])
  

    diff = np.linalg.norm(numgrad - grad) / np.linalg.norm(numgrad + grad)
    print('the relative difference will be small (less than 1e-9).\n'
          'Relative Difference: {:0.3e}'.format(diff))

设置\lambda = 1.5,运行以上代码得到的部分结果如下图所示,两者差值不大于10^{-9}.

电影推荐系统的实现

有了以上关于协同过滤模型的定义和实现,现在可以用以上算法实现一个电影推荐系统了,为了便于理解算法的运行过程,可以自己随机设置一些电影的评分数据和用户数据,具体代码实现如下所示:

  1. 加载电影评分数据
    电影评分数据的的以如下图所示的txt格式存在,逐行读取代码的格式如下所示:


def load_movie_list():
    movie_list = []
    with open("movie_ids.txt",encoding='utf-8') as f:
        lines = f.readlines()
        for line in lines:
            idx, *movie_name = line.split(' ')
            movie_list.append(' '.join(movie_name).rstrip())

    return movie_list

# 重新设置一些随机评分数据
movie_list = load_movie_list()
my_ratings = np.zeros(len(movie_list))
my_ratings[0] = 4
my_ratings[97] = 2
my_ratings[6] = 3
my_ratings[11] = 5
my_ratings[53] = 4
my_ratings[63] = 5
my_ratings[65] = 3
my_ratings[68] = 5
my_ratings[182] = 4
my_ratings[225] = 5
my_ratings[354] = 5
print('New user ratings:\n')
for i in range(my_ratings.size):
    if my_ratings[i] > 0:
        print('Rated {} for {}'.format(my_ratings[i], movie_list[i]))

以上代码运行结果如下所示:


  1. 实现电影推荐
    实现电影推荐之前,需要对电影评分数据做标准化处理。需要注意的是,为了减少算法运行时间,标准化处理数据之前,只考虑对此电影做出评价的用户的数据,而不考虑未对此电影做出评价的用户(即也就是{r(i,j)} = 1)的数据,这样可以减少数据规模,节省算法运行时间。具体实现代码如下所示:
data = scio.loadmat('ex8_movies.mat')
Y = data['Y']
R = data['R']

# Y 是一个1682x943的矩阵, 包含943个用户对1682个电影的评分哦数据(1-5)

#
# Y 是一个1682x943的矩阵, 当 R[i,j] = 1 认为用户j对电影i做出了评价
# 将自己的评分数据添加到数据集中
Y = np.c_[my_ratings, Y]
R = np.c_[(my_ratings != 0), R]

#标准化数据
Ynorm, Ymean = normalize_ratings(Y, R)

#特征向量
num_users = Y.shape[1]   #944
num_movies = Y.shape[0]   #1682
num_features = 10

# 设置初始参数
X = np.random.randn(num_movies, num_features)
theta = np.random.randn(num_users, num_features)

initial_params = np.concatenate([X.flatten(), theta.flatten()])

lmd = 10


def cost_func(p):
    return cofi_cost_function(p, Ynorm, R, num_users, num_movies, num_features, lmd)[0]


def grad_func(p):
    return cofi_cost_function(p, Ynorm, R, num_users, num_movies, num_features, lmd)[1]

theta, *unused = opt.fmin_cg(cost_func, fprime=grad_func, x0=initial_params, maxiter=100, disp=False, full_output=True)


X = theta[0:num_movies * num_features].reshape((num_movies, num_features))
theta = theta[num_movies * num_features:].reshape((num_users, num_features))

print('Recommender system learning completed')
print(theta)

input('Program paused. Press ENTER to continue')

运行结果如下图所示:


最后,利用得到的参数,实现电影推荐,其结果如下所示:

p = np.dot(X, theta.T)
my_predictions = p[:, 0] + Ymean

indices = np.argsort(my_predictions)[::-1]
print('\nTop recommendations for you:')
for i in range(10):
    j = indices[i]
    print('Predicting rating {:0.1f} for movie {}'.format(my_predictions[j], movie_list[j]))

print('\nOriginal ratings provided:')
for i in range(my_ratings.size):
    if my_ratings[i] > 0:
        print('Rated {} for {}'.format(my_ratings[i], movie_list[i]))

《吴恩达机器学习》笔记算是基本完成了,若此学习笔记有任何错误或者不当之处,欢迎各位大佬指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容