返回介绍

7.3 P 大于 N 的情形

发布于 2024-01-30 22:34:09 字数 7788 浏览 0 评论 0 收藏 0

这一节的标题用的是行话,现在我们来了解一下它的含义。从20世纪90年代起,首先是在生物医学领域,然后是在互联网领域,当P 大于N 的时候,就会出现一些问题。也就是说,特征个数P 大于样本个数N (这几个字母是这些概念的常用统计学速记写法),即“P 大于N ”问题。

例如,如果你的输入是一个文本集合,一个简单方法是把字典中每一个可能的词语都当做一个特征,然后在这些特征上进行回归(之后将会处理一个类似的问题)。在英语中,一共有20 000多个词汇(如果进行了一些词干处理的话,这只是公共词语的数目;如果保留了每个词语的词形,那么将会是这个数目的10倍以上)。如果只有几百或几千个样本,特征个数就超过了样本个数。

在这种情况下,由于特征数多于样本数,所以有可能会在训练数据中得到一个完美的拟合。这是一个数学上的事实:当你正在求解一组等式,而等式个数少于变量个数时,那你就可以找到一组训练误差为0的回归系数。(事实上你可以得到更多的完美解,无限多。)

然而,这里的主要问题是,0训练误差并不意味着你的模型泛化得好。事实上,它的泛化能力可能非常差。鉴于之前的正则化能给你带来一点额外的提升,现在就需要一个完全有意义的结果。

7.3.1 基于文本的例子

我们现在转向另一个例子,它来自于卡内基梅隆大学Noah Smith教授的研究团队所进行的一项研究。这项研究是基于对一个叫做“10-K reports”的公司文件进行挖掘,这份文件来自于美国证券交易委员会 (Securities and Exchange Commission,SEC)。对所有公开交易的公司来说,这份文件都具有法律授权。我们的目标是基于这份公开信息,来预测公司股票未来的波动性。在训练数据中,我们使用的实际上是历史数据,所以我们知道跟这些数据相关的结果。

这里一共有16 087个样本。每个特征和不同的词语相对应,一共150 360个。这些特征已经都预处理好了。我们拥有的特征比样本数要多。

这个数据集是SVMLight格式的,来自于多个数据源,包括本书的网站。scikit-learn能够读取这个格式的数据。SVMLight,顾名思义,是一个支持向量机的实现。它在scikit-learn里也可以使用。不过现在,我们只对它的数据格式感兴趣:

from sklearn.datasets import load_svmlight_file
data,target = load_svmlight_file('E2006.train')

在前面这段代码中,data 是一个稀疏矩阵(这是说,矩阵中多数元素都是0,因此在内存中只保存了非0元素),而target 是一个一维向量。我们可以看看target 的一些属性:

print('Min target value: {}'.format(target.min())) print('Max target value: {}'.format(target.max())) print('Mean target value: {}'.format(target.mean())) print('Std. dev. target: {}'.format(target.std()))

它会打印出如下数值:

Min target value: -7.89957807347 Max target value: -0.51940952694 Mean target value: -3.51405313669 Std. dev. target: 0.632278353911

我们可以看到数据范围在-7.9到-0.5之间。既然有了评估数据,就可以看看在使用OLS进行预测的时候都发生了什么。注意,所用的类和方法与之前完全一样:

from sklearn.linear_model import LinearRegression lr = LinearRegression(fit_intercept=True) lr.fit(data,target) p = np.array(map(lr.predict, data)) p = p.ravel() # p是一个(1,16087)的数组,我们想要使它扁平化 e = p-target # e是“误差”:预测值和真实值之间的差距 total_sq_error = np.sum(e*e) rmse_train = np.sqrt(total_sq_error/len(p)) print(rmse_train)

这个误差并不是正好为0,这是由于舍入有误差,但它已经非常接近0了:0.0025(比目标值的标准差要小得多)。

当使用交叉验证(这里的代码跟之前用在Boston例子中的代码非常相似)的时候,我们会得到很不一样的结果:0.78。不要忘了数据的标准差只有0.6。就是说如果我们总是“预测”平均值-3.5,那得到的均方根误差是0.6!所以,虽然在训练时使用OLS得到这样的误差无足重轻,但当我们进行泛化的时候,这个误差就太大了,这个预测实际上是有害的:每次简单预测平均值,就可以做得更好(就均方根误差而言)!。

提示 训练误差和泛化误差

当特征数目多于样本数目的时候,你用OLS可以一直得到0训练误差,但这并不表示你的模型在泛化中可以做得很好。事实上,你可能得到了0训练误差,但它是一个完全没有用处的模型。

一个自然的解决方案就是利用正则化对过拟合施加反作用。我们用弹性网络学习器来进行交叉验证循环,并将惩罚参数设置为1。现在,我们得到了0.4的RMSE。这比“预测均值”要好。在实际问题中,我们很难知道是否已经尝试了所有可能的方式,因为要做到完美预测几乎不可能。

7.3.2 巧妙地设置超参数(hyperparameter)

在前面这个例子中,我们将惩罚参数设置为1。我们还可以把它设置为2(或者一半,或者200,或者2000万)。很自然,每次的结果都会不同。如果我们选了一个过大的值,那么就会欠拟合。在极端情况下,学习系统可以返回一个所有参数都为0的模型。如果我们选了一个太小的值,那么就会过拟合,这和OLS很相近,泛化能力较差。

该如何选择一个较好的值呢?在机器学习中经常会遇到这个问题:为学习方法设置参数。一种通用的解决方案是使用交叉验证。我们选择一组参数值,然后用交叉验证找出其中最优的一个。这需要更多的计算(如果用10折交叉验证,那么就需要10倍的计算),但它是可行的,而且没有偏向。

不过,必须小心谨慎。要想评估泛化能力,我们需要使用两层的交叉验证:第一层用来估计泛化能力,第二层用于获得较好的参数。这是说,例如将数据分成10折。我们从留存第一折数据开始,在剩下9折数据上学习。现在,再把数据分成10折,用来选择参数。一旦设置了参数,就在第一折上进行测试。然后,重复9次这样的操作。

上图显示了该如何将一个训练折拆分成子折。我们需要在所有其他数据折上重复这个过程。在这里,我们看到了5个外部数据折和5个内部数据折,不过外部和内部数据折的个数无需一模一样;你可以使用任何数字,只要把它们区分开即可。

这会增加计算量,但要把事情做正确,这是必要的。这里的问题是,如果使用了一部分数据来对模型做出任何决策,那么你已经把它污染了,就不能再用它来测试模型的泛化能力。这是一个微妙的地方,可能不是很明显。事实上,很多机器学习使用者都会在这里犯错误,从而高估了他们的系统,这是因为他们没有正确使用交叉验证。

幸运的是,Scikit-learn能很容易把这个事情做正确:它有一些类,名为LassoCV 、RidgeCV 和ElasticNetCV 。它们都对内部参数封装了交叉验证检查。这些代码与之前的代码完全类似,除了我们并不需要对alpha设置任何值。

from sklearn.linear_model import ElasticNetCV met = ElasticNetCV(fit_intercept=True) kf = KFold(len(target), n_folds=10) for train,test in kf: met.fit(data[train],target[train]) p = map(met.predict, data[test]) p = np.array(p).ravel() e = p-target[test] err += np.dot(e,e) rmse_10cv = np.sqrt(err/len(target))

这会带来很多计算,所以在你等待的时间里可以喝点咖啡。(等待的时间长短取决于你的计算机运行速度有多快。)

7.3.3 评分预测和推荐

如果你使用过任何近十年来的商业在线系统,可能已经看过这些推荐,例如亚马逊的“购买了X的客户还购买Y”。我们将会在下一章购物篮分析那一节里对此进行介绍。还有就是基于产品评分预测的应用,例如电影评分。

后面这个问题因为Netflix Challenge,一个来自Netflix的百万美金机器学习挑战,而广为人知。Netflix(在美国和英国很有名,但在其他地方并不出名)是一个影片租赁公司。一直以来,他们通过邮寄DVD为客户提供服务;但是最近,他们的业务专注于在线视频流服务。这项服务从一开始就有一个显著特点:用户可以给看过的电影评分,然后通过这些评分,系统会为用户推荐其他电影。通过这种模式,不但能知道用户看过哪些电影,还可以获悉他们对这些电影的印象分(包括负面印象)。

2006年,Netflix公开了很多用户对电影的评分数据,其目标是提升他们的内部评分推荐算法。任何人如果可以击败他们的算法,并且把效果提升10%以上,那么就可以赢得100万美金。在2009年,一个名为BellKor's Pragmatic Chaos 的国际团队做到了把效果提升10%以上,并获得了奖金。他们是在另一个队伍完成的20分钟之前实现的。另一个队伍叫做The Ensemble ,也做到了把效果提升10%以上!在这个持续多年的竞赛中,这是一个十分刺激的一线之差。

遗憾的是,由于法律原因,这个数据集不再对外公开(尽管数据是匿名的,但仍有可能暴露用户,并泄漏电影租赁的隐私信息)。不过,我们还可以使用一个具有相似性质的学术数据集。这个数据集来自于GroupLens——明尼苏达大学(University of Minnesota)的一个研究实验室。

提示 真实世界中的机器学习

关于Netflix奖,已经写了很多,你可能也学到了不少(本书会给你足够的信息让你起步,并理解这些问题)。赢得大奖所使用的技术,是一些高级机器学习算法的融合体,其中包括很多数据的预处理工作。例如,一些用户喜欢给所有电影都打很高的分数,而其他一些人的打分总是比较负面;如果在预处理中不把这些考虑进去,你的模型就会遇到困难。其他一些并不明显的归一化工作对于获得良好效果也是必要的:电影有多少年的历史,它得到了多少个评分,等等。好算法是一件好事情,但你一定要亲自动手调优你的方法,使之适应数据的特性。

我们可以把它形式化成一个回归问题,并应用本章里学到的方法。这个问题并不适合分类方法。我们当然也可以尝试学习一个5类别的分类器,一个类别对应一个可能的分数。这个方法有两个问题。

误差并不是等同的。例如,把5星电影评成4星与把5星电影评成1星的错误严重程度并不相同。

中间值是有意义的。即使我们的输出只有整数值,预测为4.7也是很有意义的。我们可以看到它跟4.2是不同的预测。

这两个因素加在一起说明,分类并不适合这个问题。回归的框架更有道理。

我们有两个选择:构建电影特定(movie-specific)模型或用户特定(user-specific)模型。在这个例子里,我们会先构建用户特定模型。这意味着,针对每个用户,我们把电影评分当做目标变量。而输入数据就是其他用户的打分。对于那些与我们的目标用户观影喜好较为相似的用户,模型会赋予他们的电影评分较高的权重分值;对于那些与我们的目标用户观影喜好完全相反的用户,模型会赋予他们的电影评分一个负值。

这就是到目前为止我们所开发的应用系统。你可以在本书网站上找到数据集以及读取数据的Python代码。在那里你还会找到包含更多信息的链接,包括原始的MovieLens网站。

数据读取部分使用基础Python即可,所以我们直接跳到学习训练那一部分。有一个稀疏矩阵,它的每个数据项,在有评分的时候,取值都是1到5(而大多数数据项都是0,这代表用户并没有对这些电影评分)。这一回,为了尝试多种回归方法,我们将会使用LassoCV 类:

from sklearn.linear_model import LassoCV
reg = LassoCV(fit_intercept=True, alphas=[.125,.25,.5,1.,2.,4.])

我们通过将一组明确的alpha值传递给构造器,从而对内部交叉验证所使用的参数值进行限制。你可能已经发现,这些值都是2的倍数,从1/8到4。现在我们要写一个函数,为用户i 学习一个模型:

# 将这个用户分离出来
u  = reviews[i]

我们只对用户u 打过分的电影感兴趣,所以我们需要构建这些电影的索引。在这里可以使用NumPy里面的一些技巧:用u.toarray() 从一个稀疏矩阵转换成一个正常的数组。然后,我们用ravel() 将它从一个行数组(这是一个二维数组,第一维是1)转换成一个简单一维数组。我们把它和0做比较,看看这个比较的结果在什么地方为真。得到的结果(ps )是一个索引数组;这些索引和该用户打过分的电影相对应:

u = u.array().ravel() ps, = np.where(u > 0) # 构建一个数组,索引是[0…N]之间除i以外的数值 us = np.delete(np.arange(reviews.shape[0]), i) x = reviews[us][:,ps].T

最后,我们只把用户打过分的电影挑选出来:

y = u[ps]

交叉验证过程跟之前一样。因为我们有很多用户,所以我们只进行4折验证(更多折会花费很长时间,而我们已经有足够多的训练数据了,它占数据的80%):

err = 0 kf = KFold(len(y), n_folds=4) for train,test in kf: # 现在按每个电影进行归一化 # 下面进行了解释 xc,x1 = movie_norm(x[train]) reg.fit(xc, y[train]-x1) # 在测试的时候也需要进行同样的归一化过程 xc,x1 = movie_norm(x[test]) p = np.array(map(reg.predict, xc)).ravel() e = (p+x1)-y[test] err += np.sum(e*e)

我们不会过多解释movie_norm 函数。这个函数会按每个电影进行归一化:一些电影在通常意义上比较好,会得到高于平均值的分数:

def movie_norm(x):
    xc = x.copy().toarray()

我们不能使用xc.mean(1) ,因为并不想有零计数的均值。我们只希望得到真实的平均分数:

x1 = np.array([xi[xi > 0].mean() for xi in xc])

在一些特定情况下,数据里并没有评分信息,我们就得到了一个NaN 值。我们用np.nan_to_num 把它替换成0,正如下面所做的那样:

x1 = np.nan_to_num(x1)

现在通过从非0项中减去均值对输入数据进行归一化:

for i in xrange(xc.shape[0]):
    xc[i] -= (xc[i] > 0) * x1[i]

这样做还会隐式地把用户未评分电影的分数设为0,而这个就是均值。最后,我们把归一化后的数组和均值返回回来:

return x,x1

你可能已经注意到了,我们把它转换成了一个正常(稠密的)数组。这种方式有一个附加的优点,就是它会使优化过程变得更加迅速:虽然Scikit-learn可以对稀疏数值处理得很好,但它对稠密数组会处理得更快(如果你能把它们装进内存的话;当你做不到的时候,你就必须使用稀疏数组)。

与简单猜测平均分数相比,这个方法有了80%的提高。这个结果并不是多么惊人,这只是一个开始。一方面,这是一个非常困难的问题,我们不能期待每次预测都能正确:当用户给出更多评价的时候,我们可以做得更好。另一方面,在这类任务上,回归并不是最锋利的工具。注意我们是怎样为每个用户训练一个完全独立的模型的。在下一章里,我们将在该问题上看到超越回归的其他方法。在那些模型中,我们将会更加智能地整合所有用户和电影的信息。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文