8.1 改进的推荐
回想一下上一章我们讲到哪里了:一个非常简单,比随机预测的效果要好,但并不是特别优秀的推荐系统。现在开始对它进行改进。首先,先看几个能抓住部分问题本质的想法。然后,我们要做的就是把多种方法融合在一起,而非某个单独方法,以便实现更好的最终效果。
我们将使用上一章中用到的电影推荐数据集;它包含一个矩阵,其中一个轴是用户,另一个轴是电影。它是一个稀疏矩阵,因为每个用户只会对一小部分电影进行评价。
8.1.1 使用二值推荐矩阵
从Netflix Challenge中可以得到一个有趣的结论,就是下面这个事后看起来很明显的想法:仅仅从“你给哪些电影评了分”这个信息里,我们就能了解到很多关于你的信息,甚至根本不用知道你究竟打了多少分。使用一个二值矩阵(1分表示用户对电影进行了评价,0分表示没有进行评价),我们就可以获得一个很好的预测效果。在事后这看起来非常有道理;我们不会完全随机地选择观看哪部电影,相反我们会选择那些自己已经有所期盼的电影。我们也不会随便选择一些电影去评分,而是只给那些我们特别有感想的电影打分(自然,这里面会有例外,但平均来看这很可能是真实的)。
我们用一幅图像把矩阵的值可视化地显示出来,其中每个评分是一个小方格。黑色代表缺少评分,而灰度级别代表了评分的数值。我们可以看到这个矩阵是稀疏的——大多数方块都是黑色的。我们还会发现一些用户点评过的电影比其他人多很多,而一些电影也比其他电影获得了更多的评分。
用来可视化数据的代码非常简单(你稍作修改就可以显示出比本书所能显示的更大的矩阵),如下所示:
from matplotlib import pyplot as plt imagedata = reviews[:200, :200].todense() plt.imshow(imagedata, interpolation='nearest')
下图就是这段代码的输出:
我们将会使用这个二值矩阵来对电影打分进行预测。大致算法(伪代码)如下所示。
1. 对于每个用户,根据用户间的相近程度对其他用户排序。在每一步里,我们都会使用这个二值矩阵,并把用户之间的相关性作为相近程度的衡量标准。(将这个二值矩阵解释为一组0和一组1,我们就能够进行计算了。)
2. 当我们需要给一个用户-电影数据对估算评分的时候,我们顺序地查找用户的近邻(在第一步中已定义)。当发现第一个对该电影的评分时,将它输出。
我们首先写一个NumPy函数来实现这个功能。NumPy里有一个np.corrcoeff 可以计算相关性。这是一个通用函数,可以用来计算n 维向量之间的相关性,即使需要的只是一个传统相关性。因此,要计算两个用户之间的相关性,我们需要这样调用:
corr_between_user1_and_user2 = np.corrcoef(user1, user2)[0,1]
事实上,我们想要计算一个用户和其他所有用户之间的相关性。也就是说我们将会多次用到这一操作,所以我们把它封装在一个函数里,叫做all_correlations :
import numpy as np def all_correlations(bait, target): ''' corrs = all_correlations(bait, target) corrs[i] is the correlation between bait and target[i] ''' return np.array( [np.corrcoef(bait, c)[0,1] for c in target])
现在可以用多种方法来使用它。一种简单方法是选择每个用户的最近近邻,也就是和该用户最相似的。我们将使用之前讨论过的相关性:
def estimate(user, rest): ''' estimate movie ratings for 'user' based on the 'rest' of the universe. ''' # user打分的二值版本 bu = user > 0 # test打分的二值版本 br = rest > 0 ws = all_correlations(bu,br) # 选择最高的100个值 selected = ws.argsort()[-100:] # 根据平均值估算 estimates = rest[selected].mean(0) # 我们需要纠正这些估算 # 基于一些电影得到的打分数量比别的电影多这个事实 estimates /= (.1+br[selected].mean(0))
与从数据集中所有用户那里得到的估算相比,这种方式可以使RMSE降低20%。和往常一样,如果只看那些进行过多次预测的用户,我们可以做得更好:如果用户排在评分活动的前半部分,那么预测误差会降低25%。
8.1.2 审视电影的近邻
在前一节里,我们审视了最相似的用户。我们还可以看一下哪些电影是最相似的。现在我们构建一个基于电影最邻近规则的推荐系统:在预测用户U对电影M评分的时候,这个系统所预测的U对M的评分,和它对最相似电影的评分是相同的。
因此,我们会按照两个步骤进行:第一步,我们计算出一个相似矩阵(该矩阵告诉我们哪些电影是最相似的);第二步,我们对每一个“用户-电影“对的评分进行估算。
我们用NumPy中的zeros 和ones 函数分配数组空间(分别初始化成0和1):
movie_likeness = np.zeros((nmovies,nmovies)) allms = np.ones(nmovies, bool) cs = np.zeros(nmovies)
现在,我们遍历所有电影:
for i in range(nmovies): movie_likeness[i] = all_correlations(reviews[:,i], reviews.T) movie_likeness[i,i] = -1
我们把对角元素设为-1 ;否则,对于任何电影来说,与之最相似的电影就是它自己。这是一个事实,却对我们没有任何帮助。这和在第2章中介绍最邻近分类方法时所使用的技巧是一样的。基于这个矩阵,我们很容易写一个函数来估算评分:
def nn_movie(movie_likeness, reviews, uid, mid): likes = movie_likeness[mid].argsort() # 逆序排列,使最受喜爱的电影排在前面 likes = likes[::-1] # 返回最相似电影的评分 for ell in likes: if reviews[u,ell] > 0: return reviews[u,ell]
注意 前面这个函数做得怎么样呢?还凑合:它的RMSE只有0.85。
前面这段代码并没有把交叉验证的细节给出来。尽管这样写在应用时效果不错,但对于测试,我们需要确保我们已经重新计算过喜好矩阵,并且没有使用过测试中的用户信息(否则,我们就污染了测试集,会得到一个对泛化能力过于乐观的估计)。遗憾的是,它的运行时间很长,而我们其实并不需要每个用户的全部矩阵信息,只需计算那些我们需要的东西就可以了。这会使代码比之前的要更复杂一些。在本书的网站上,你可以找到关于所有细节的代码。你还可以找到一个更快的all_correlation 函数的实现。
8.1.3 组合多种方法
现在我们把前一节里的几种方法组合在一起,做出一个新预测。例如,我们可以对各种方法的预测结果取平均值。一般来说这样做已经足够好了,但是我们不能想当然地认为两种方法的预测结果一样好,并且恰好具有相同的权重0.5。或许,其中一个是更好的。
我们尝试采用加权平均:在把预测结果相加之前,让每个预测乘以一个权重。那么我们又该如何设置最佳的权重呢?当然,我们可以从数据中学习出来!
提示 集成学习
我们使用的是机器学习中的一种通用技术,叫做集成学习 (ensemble learning);它并不仅仅适用于回归问题。我们学习出(一组)预测器的集成体,然后把它们组合在一起。有趣的是,我们可以把每个预测器的结果当做一个新特征。现在是基于训练数据把这些特征组合在一起,而这正是我们一直在做的事情。注意,这样做是为了解决回归问题,但是同样的推理在分类问题中也适用:你可以创建几个分类器和一个主分类器,主分类器会接收所有分类器的结果并给出一个最终的预测。组合基本分类器的不同方式,决定了集成学习的不同形式。在这里,我们复用了在学习预测器时所使用的训练数据。
有了这样一种组合多种方法的灵活方式,我们就可以尝试任何想法,并把它加到学习器的混合体中,然后让系统给出一个权重。我们还可以用这些权重来发现哪些想法比较好:如果它们得到了较高的权重,那意味着它们加入了有用的信息。而权重很低的想法甚至可以丢弃。
代码非常简单,如下所示:
# 引入前一个例子中所使用的代码 import similar_movie import corrneighbors import usermodel from sklearn.linear_model import LinearRegression es = [ usermodel.estimate_all() corrneighbors.estimate_all(), similar_movie.estimate_all(), ] coefficients = [] # 我们将进行留一交叉验证 for u in xrange(reviews.shape[0]): # 对所有用户的id es0 = np.delete(es,u,1) # u除外的所有用户 r0 = np.delete(reviews, u, 0) P0,P1 = np.where(r0 > 0) # 我们只关心实际预测结果 X = es[:,P0,P1] y = r0[r0 > 0] reg.fit(X.T,y) coefficients.append(reg.coef_) prediction = reg.predict(es[:,u,reviews[u] > 0].T) # 和以前一样衡量误差
得到的结果是,RMSE几乎为1。我们还可以分析一下coefficients 变量,看看预测器的效果如何:
print coefficients.mean(0) # 所有用户的平均值
这个数组的值是[ 0.25164062, 0.01258986, 0.60827019] 。基于最相似电影的方法,可以得到最高的权重(它是最佳的预测,所以这并不奇怪),同时我们在学习过程中还可以舍去基于相关性的方法,因为它对最终结果的影响非常小。
这种设置可以让人很容易加入一些额外的想法;例如,既然计算最相似电影是一个效果不错的预测器,那么在学习过程中使用5个最相似电影又会如何呢?我们可以修改前面的代码来生成k个最相似电影,然后用栈式的学习器来学习权重:
es = [ usermodel.estimate_all() similar_movie.estimate_all(k=1), similar_movie.estimate_all(k=2), similar_movie.estimate_all(k=3), similar_movie.estimate_all(k=4), similar_movie.estimate_all(k=5), ] # 剩下的代码跟以前一样
我们有很大的自由来生成新的机器学习系统。在这个例子里,最终结果并不会更好,但我们很容易测试这个新想法。
然而,我们必须谨慎,避免对数据过拟合。事实上,如果我们随机尝试很多东西,那么其中一些就会在这个数据集上效果不错但泛化能力不行。即使我们正在使用交叉验证,我们却并没有对设计决策进行交叉验证。为了获得一个较好的估计,在有很多数据的情况下,应该把一部分数据预留出来不要动,直到最终模型即将投入使用的时候。然后,用这份数据测试模型的效果,这样就可以得到模型在真实应用中预期效果的无偏差预测。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论