一、假设一个电影推荐问题,希望构建一个算法来预测每个用户可能会给他们没看过的电影打多少分,并以此作为推荐的依据。
引入一些标记:
n
u
n_{u}
nu代表用户的数量,
n
m
n_{m}
nm代表电影的数量,r(i,j):如果用户j给电影i评过分则r(i,j)=1,
y
(
i
,
j
)
y^{\left ( i,j \right )}
y(i,j)代表用户j给电影i的评分,
m
j
m_{j}
mj代表用户j评过分的电影总数。假设采用线性回归模型构建一个推荐系统算法,
θ
(
j
)
\theta ^{\left ( j \right )}
θ(j)为用户j的参数向量,
x
(
i
)
x^{\left ( i \right )}
x(i)为电影i的特征向量,对于用户j和电影i,预测评分为:
(
θ
(
j
)
)
T
x
(
i
)
\left (\theta ^{\left ( j \right )} \right )^{T}x^{\left ( i \right )}
(θ(j))Tx(i)。针对用户j,该线性回归模型的代价为预测误差的平方和加上正则化项(其中i:r(i,j)表示只计算那些用户j评过分的电影):
所有用户的代价函数为:
用梯度下降法来求解最优解,计算代价函数的偏导数后得到梯度下降的更新公式为:
二、协同过滤能够自行学习要使用的特征。如果拥有用户的参数,可以学习得出电影的特征,优化目标函数:
已知电影的特征量,可以学习参数θ;如果用户愿意提供参数θ,就能估计出各种电影的特征值。通过θ→x→θ→x→…这样的迭代过程,会得到更好的θ和x。重复这个过程,算法将会收敛到一组合理的电影特征以及一组合理的对不同用户的参数估计,这就是协同过滤。(观察大量用户的实际行为来协同地得到更佳的每个人对电影的评分值,每个用户都在帮助算法更好地进行特征学习。)
协同过滤算法可以将θ和x同时计算出来,优化目标函数为:
算法使用步骤如下:
1、初始化
x
(
1
)
x^{\left ( 1 \right )}
x(1),…,
x
(
n
m
)
x^{\left ( n_{m} \right )}
x(nm),
θ
(
1
)
\theta ^{\left ( 1 \right )}
θ(1),…,
θ
(
n
u
)
\theta ^{\left ( n_{u} \right )}
θ(nu)为小的随机值;
2、使用梯度下降算法最小化代价函数
3、训练完算法后,预测
(
θ
(
j
)
)
T
x
(
i
)
\left (\theta ^{\left ( j \right )} \right )^{T}x^{\left ( i \right )}
(θ(j))Tx(i)为用户j给电影i的评分。
三、以吴恩达机器学习课程练习材料实现,将协同过滤算法应用于电影评分数据集,根据新用户的评分为其推荐10部电影。代码实现来源参考:吴恩达机器学习作业Python实现(八):异常检测和推荐系统。
电影评分数据集由1到5的等级组成,总共有
n
u
n_{u}
nu=943个用户、
n
m
n_{m}
nm=1682部电影,因此Y是一个(1682, 943)的矩阵。矩阵R为二值指标矩阵,如果用户j对电影i进行评级,则R(i,j)=1,否则R(i,j)=0。
实现协同过滤的相关函数代码如下:
from scipy.io import loadmat
import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as opt
#展开参数
def serialize(X, Theta):
return np.r_[X.flatten(), Theta.flatten()]
#提取参数
def deserialize(seq, nm, nu, nf):
return seq[:nm * nf].reshape(nm, nf), seq[nm * nf:].reshape(nu, nf)
#代价函数
def cofiCostFunc(params, Y, R, nm, nu, nf, l):
X, Theta = deserialize(params, nm, nu, nf) #从一维的参数矩阵提取得到电影的特征矩阵和用户的特征矩阵
error = 0.5 * np.square((X @ Theta.T - Y) * R).sum() #误差项
reg1 = 0.5 * l * np.square(Theta).sum() #正则化项
reg2 = 0.5 * l * np.square(X).sum()
return error + reg1 + reg2
#梯度函数
def cofiGradient(params, Y, R, nm, nu, nf, l):
X, Theta = deserialize(params, nm, nu, nf) #从一维的参数矩阵提取得到电影的特征矩阵和用户的特征矩阵
X_grad = (X @ Theta.T - Y) * R @ Theta + l * X #梯度
Theta_grad = ((X @ Theta.T - Y) * R).T @ X + l * Theta
return serialize(X_grad, Theta_grad)
#标准化评分
def normalizeRatings(Y, R):
Ymean = (Y.sum(axis=1) / R.sum(axis=1)).reshape(-1, 1) #获取每部电影评分的均值
Ynorm = (Y - Ymean) * R #未评分的数据没有归一化
return Ynorm, Ymean
利用预先训练好的参数进行梯度检查的代码如下:
#梯度检查
def checkGradient(params, Y, myR, nm, nu, nf, l):
print('Numerical Gradient \t cofiGrad \t\t Difference')
grad = cofiGradient(params, Y, myR, nm, nu, nf, l) #梯度函数得到的梯度
e = 0.0001 #用微小的e计算数值梯度
nparams = len(params) #参数数量
e_vec = np.zeros(nparams)
for i in range(10): #随机选择参数向量的10个元素计算数值梯度
idx = np.random.randint(0, nparams) #随机选取元素
e_vec[idx] = e
loss1 = cofiCostFunc(params - e_vec, Y, myR, nm, nu, nf, l)
loss2 = cofiCostFunc(params + e_vec, Y, myR, nm, nu, nf, l)
numgrad = (loss2 - loss1) / (2 * e) #计算数值梯度
e_vec[idx] = 0 #还原元素
diff = np.linalg.norm(numgrad - grad[idx]) / np.linalg.norm(numgrad + grad[idx]) #计算数值梯度与理论梯度之间的差值
print('%0.15f \t %0.15f \t %0.15f' % (numgrad, grad[idx], diff))
mat = loadmat('recommender_movies.mat') #加载数据集
#print(mat.keys())
Y, R = mat['Y'], mat['R'] #Y是不同电影的不同用户的评分矩阵,R是二进制指示矩阵,表示用户是否对电影评分
#print(Y.shape,R.shape)
mat=loadmat('recommender_params.mat') #获得训练好的参数
#print(mat.keys())
X=mat['X'] #电影的特征矩阵
Theta=mat['Theta'] #用户的特征矩阵
#nu=int(mat['num_users']) #用户数量
#nm=int(mat['num_movies']) #电影数量
#nf=int(mat['num_features']) #特征数量
#print(X.shape,Theta.shape,nu,nm,nf)
nu=4;nm=5;nf=3 #减小数据集用来更快的测试代价函数的正确性
X=X[:nm,:nf]
Theta=Theta[:nu,:nf]
Y=Y[:nm,:nu]
R=R[:nm,:nu]
print("Checking gradient with lambda=1.5...")
checkGradient(serialize(X,Theta),Y,R,nm,nu,nf,l=1.5)
添加新用户评分并且训练模型对该新用户进行推荐的代码如下:
mat = loadmat('recommender_movies.mat') #加载数据集
#print(mat.keys())
Y, R = mat['Y'], mat['R'] #Y是不同电影的不同用户的评分矩阵,R是二进制指示矩阵,表示用户是否对电影评分
#print(Y.shape,R.shape)
#获得所有电影的名称列表
movies = []
with open('movie_id.txt', 'r', encoding='utf-8') as f:
for line in f:
movies.append(' '.join(line.strip().split(' ')[1:])) #strip去掉开头结尾的空格,split对空格切片,选取编号之后的所有字符串,再用jion空格连接成一个字符串
#初始化新用户评分
my_ratings = np.zeros((1682, 1))
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
Y = np.c_[Y, my_ratings] #添加新用户评分构成新的评分矩阵
R = np.c_[R, my_ratings != 0]
nm, nu = Y.shape #电影数量,用户数量
nf = 10 #特征维度
Ynorm,Ymean=normalizeRatings(Y,R) #标准化评分
X = np.random.random((nm, nf)) #随机初始化参数X矩阵、Theta矩阵
Theta = np.random.random((nu, nf))
params = serialize(X, Theta) #展开参数
l = 10 #正则化参数
res = opt.minimize(fun=cofiCostFunc,x0=params,args=(Ynorm, R, nm, nu, nf, l),method='TNC',jac=cofiGradient,options={'maxiter': 100}) #训练模型
ret = res.x #得到模型参数
fit_X, fit_Theta = deserialize(ret, nm, nu, nf) #提取参数
pred_mat = fit_X @ fit_Theta.T #所有用户的预测评分矩阵
pred = pred_mat[:, -1] + Ymean.flatten() #最后一个用户的预测分数,即添加的新用户
pred_sorted_idx = np.argsort(pred)[::-1] #排序并翻转,从大到小得到索引
print("Top recommendations for you:")
for i in range(10): #为新用户推荐10部电影
print('Predicting rating %0.1f for movie %s.' %(pred[pred_sorted_idx[i]], movies[pred_sorted_idx[i]]))
print("\nOriginal ratings provided:")
for i in range(len(my_ratings)): #新用户原来的评分
if my_ratings[i] > 0:
print('Rated %d for movie %s.' % (my_ratings[i], movies[i]))
最终推荐结果如下:
(结语个人日记:断断续续终于把吴恩达老师的机器学习课程练习实践都过了一边,结课啦!之前偶尔也学学Python常用方法语法,但发现在具体情景下实践一遍,真得比较容易理解和体会,也深切体会到Numpy在数据处理中的强大。老吴该寻找下一个学习模块了。)