本文引自图灵教育《机器学习系统设计》的第一章——Python机器学习入门。
如果你只想学习基础理论,那么这本书或许并不适合你。它并没有深入机器学习背后的数学细节,而是通过Python这样一种广泛应用的脚本语言,从数据处理,到特征工程,再到模型选择,把机器学习解决实际问题的过程一一呈现在你的面前。这本书的最大特点在于:易上手、实践性强、贴近应用。它可以让你在很短的时间内了解机器学习的基本原理,掌握机器学习工具,然后去解决实际问题。从文字、声音到图像,从主题模型、情感分析到推荐技术,本书所教给你的都是最实际的技术,让你从一个新手迅速成长为大咖。
参与方式:喜欢这本书,请在评论区留言,和大家分享你在学习Python过程中的一些经验和心得,根据评论质量和评论点赞数,前五名同学可获得本书。活动截止时间为11月6日(下周一晚8点)
机器学习(ML)就是教机器自己来完成任务,就这么简单。复杂性源于细节,而这很可能就是你要读这本书的原因。
也许你现在拥有过多的数据,却对这些数据缺少理解,你希望机器学习算法可以帮助解决这个难题。于是你随机找了一些算法开始钻研,但过了一段时间就感到困惑了:在无数的算法中应该选择哪一个呢?
或许你笼统地对机器学习感兴趣,也阅读过相关的博客和文章。机器学习中的任何东西看起来都那么不可思议、那么酷,所以你开始进行探索,把一些简单的数据放入一个决策树或者一个支持向量机。但是,成功将它应用到一些其他数据之后,你又心生疑惑:所有的设置都正确吗?你得到最优的结果了吗?怎么知道有没有更好的算法?或者,你的数据是否就是“正确的”?
欢迎加入机器学习的行列!我们作为本书的作者,也曾处在这个阶段,寻找过机器学习理论教材背后的真实故事。我们发现,很多东西都是标准教材中通常不会讲到的“魔术”。所以,从某种意义上说,我们在把这本书写给年轻的自己。它不仅是机器学习的快速入门书,而且还会把我们积累的经验教训传授给你。我们希望它还可以让你更顺畅地走进计算机科学中最令人兴奋的一个领域。
1.1 梦之队:机器学习与Python
机器学习的目标就是通过若干示例(怎样做或不做一个任务)让机器(软件)学会完成任务。假设每天早上当你打开电脑,都会做同样的事情:移动电子邮件,把属于某一特定主题的邮件放入同一个文件夹。过了一段时间,你感到厌烦了,开始琢磨是否可以让这种琐事自动完成。一种方法是分析你的大脑,将整理电子邮件时大脑思考过程中的规则记录下来。然而,这种方式相当麻烦,而且总不完美。你会漏掉一些规则,同时又会对另一些规则细致过头。另一种更好的、更加面向未来的方法是将这个过程自动化,即选择一组电子邮件元数据信息和邮件正文/文件夹名对,让算法据此选出最好的规则集。这些数据对就是你的训练数据,而生成的规则集(也叫做模型)以后能够应用到新的电子邮件上。这就是最简单的机器学习。
当然,机器学习(也常称作数据挖掘或预测分析)本身并不是一个全新的领域。正相反,它这些年来的成功可以归因于务实地采用了已经验证了的坚实技术,以及借鉴其他成功领域的真知灼见,例如统计学。统计学的目的是通过学习更多的潜在模式和关联关系,来帮助人类深入理解数据。对机器学习的成功应用了解得越多(你已经查看过kaggle.com了吧?),越会发现应用统计学是机器学习专家经常研究的一个领域。
本书后面将会介绍,构想出一个合适的机器学习(ML)方法,从来都不是一个瀑布式的过程。相反,你需要反复分析,在各色各样的机器学习算法中尝试不同版本的输入数据。这种探索方式非常适合Python。作为一门解释性高级编程语言,Python似乎就是专为尝试不同事物而设计的。更重要的是,用它进行这些尝试非常迅捷。无疑,它比C语言或其他类似的静态类型编程语言要慢一点。然而,它有着大量易用的库,而这些库往往是用C语言编写的,因此你不必为了敏捷性而牺牲速度。
1.2 这本书将教给你什么(以及不会教什么)
本书将全面展示不同应用领域正在使用的各种机器学习算法,以及使用它们时应当注意什么。然而,根据亲身经验,我们知道做这些很“酷”的事——使用和调整机器学习算法,比如支持向量机(SVM)、最邻近搜索(NNS),或者同时支持两者——其实只需要耗费一位优秀机器学习专家的一点儿时间。看看下面这个典型的工作流程,你就会发现绝大部分时间将花费在一些相当平凡的任务上:
(1) 读取和清洗数据;
(2) 探索和理解输入数据;
(3) 分析如何最好地将数据呈现给学习算法;
(4) 选择正确的模型和学习算法;
(5) 正确地评估性能。
在探索和理解输入数据的时候,我们需要一点统计学和基础数学知识。但当这样做的时候,你会发现,这些数学课上似乎十分枯燥的知识,用来处理有趣的数据时,其实真的很令人兴奋。
解读数据标志着旅程的开始。你面对诸如无效值或缺失值的问题时,会发现这更像是一种技艺而非一门精确的科学。这是一种非常有益的技艺,因为如果这部分做得正确,那么你的数据就能够适应更多的机器学习算法,从而成功的可能性大大提高。
数据在程序的数据结构中就绪之后,你要清楚自己正在跟何方神圣打交道。你有足够的数据来回答自己的问题吗?如果没有,也许应当考虑通过额外的途径来获取一些。或许你的数据过多?那么你可能要考虑怎样最有效地从中抽取样本。
你通常不会直接将数据输入机器学习算法,而是在训练前对部分数据进行提炼。很多时候,使用机器学习算法会让你得到性能提升的回报。一个简单算法在提炼后数据上的表现,甚至能够超过一个非常复杂的算法在原始数据上的效果。这部分机器学习流程叫做特征工程(feature engineering),通常是一个非常令人兴奋的有意思的挑战。你有创意和智慧,便会立即看到结果。
选择正确的学习算法并不只是尝试一下工具箱中的三四个算法那么简单(工具箱中会有很多的算法)。它更需要的是深思熟虑,来权衡性能和功能的不同需求。你是否会为了快速得到结果而牺牲质量,还是愿意投入更多的时间来得到最好的结果?你是否对未来的数据有一个清晰的认识,还是应该在这方面更保守一点?
最后,性能评估是怀有远大抱负的机器学习初学者最常犯错误的地方。有一些简单的错误,比如使用了与训练相同的数据来测试你的方法。但还有一些比较难的,例如,你使用了不平衡的训练数据。再说一次,数据决定了你的任务是成功还是失败。
我们看到,只有第(4)点是关于那些花哨的算法的。虽然如此,希望这本书可以使你相信,另外4个任务并不是简单的杂务,它们同等重要,或许还更加令人兴奋。我们希望读过本书之后,你可以真正爱上数据,而非学到的算法。
最后,我们并不想让机器学习算法的理论把你压垮,因为这方面已经有很多优秀的著作了(可以在附录A中找到我们的推荐)。相反,我们会在各节中直观地介绍各种基础方法——这对于你大致理解其中的思想已经足够了,并且能够确保你走好第一步。因此,这本书并不是机器学习“权威指南”,而更像是初学者的工具。我们希望它能够激发你的好奇心,并足以让你保持渴望,不断探索这个有趣的领域。
在本章的余下部分,我们将着手介绍Python的基础库NumPy和SciPy,并且使用Scikit-learn进行第一个机器学习训练。同时我们将介绍基本的ML概念,它们稍后将贯穿于全书。本书余下的各章会详细讲述之前介绍的5个步骤,同时突出介绍使用Python的机器学习方法在各种应用场景中的不同方面。
1.3 遇到困难的时候怎么办
本书中,我们会试图讲清楚每一个必要的想法,保证你能重现各个步骤。虽然如此,你仍然可能会遇到困难。其原因可能是软件包版本的古怪组合,可能是简单的拼写错误,也可能是理解上的问题。
在这种情况下,可以通过很多不同的途径来获取帮助。很有可能,你想问的问题早已有人提出,而且下面这些优质的问答网站已经给出了答案。
http://metaoptimize.com/qa
这个问答网站专注于机器学习主题。几乎所有的问题都会得到机器学习专家的高水平解答。即使你并没有问题,不时地翻阅这些问答也是一个很好的习惯。
http://stats.stackexchange.com
这个问答网站又叫交叉验证(Cross Validated),和MetaOptimized相似,但它更专注于统计方面的问题。
http://stackoverflow.com
这个问答网站与前面的相似,但还会更宽泛地讨论一些常规的编程主题。例如,一些软件包的问题,这些我们也会在本书中提到(SciPy和Matplotlib)。
Freenode的#machinelearning频道
这个互联网中转聊天(IRC)频道专门讨论机器学习主题。这是个机器学习方面的专业社区,虽然很小,但是非常活跃,十分有用。
http://www.TwoToReal.com
这是由本书作者制作的一个即时问答网站,来为你解答不适于上述任何网站的问题。如果你提交了一个问题,我们将会收到一条即时消息;只要我们当中有人在线,就会与你交谈解决。
正如一开始所述,本书试图帮助你快速开始机器学习之旅。因此,我们鼓励你构建自己的机器学习相关博客的列表,并且定期查阅。这是去了解什么可行、什么不可行的最佳方式。
在这里,唯一要着重指出的博客是http://blog.kaggle.com。这是举办过很多次机器学习比赛的Kaggle公司维护的博客(在附录A里可以找到更多链接)。通常,他们鼓励比赛优胜选手写文章,详细介绍他们是怎样着手解决难题的,什么样的策略不可行,以及他们是怎样想出获胜的策略的。如果你不想读其他的东西,没问题,但这个必须读。
1.4 开始
如果你已经安装了Python(2.7或更高版本),那么还需要安装NumPy和SciPy来处理数据,并需要安装Matplotlib对数据进行可视化。
1.4.1 NumPy、SciPy和Matplotlib简介
在讨论具体的机器学习算法之前,必须说一下如何最好地存储需要处理的数据。这很重要,因为多数高级学习算法,如果运行永远不会结束,对我们毫无用处。这可能仅仅是因为数据访问太慢了,也可能是因为这些数据的表示方式迫使操作系统一直做数据交换。再加上Python是一种解释性语言(尽管是高度优化过的),和C或者Fortran相比,这类语言对很多重数值算法来说运行缓慢。所以或许应该问一问究竟为什么有这么多科学家和公司,甚至在高度计算密集型领域内豪赌Python。
答案就是,在Python中很容易把数值计算任务交给下层的C或Fortran扩展包。这也正是NumPy和SciPy要做的事情(http://scipy.org/install.html)。在NumPy和SciPy这个组合中,NumPy提供了对高度优化的多维数组的支持,而这正是大多数新式算法的基本数据结构。SciPy则通过这些数组提供了一套快速的数值分析方法库。最后,用Python来绘制高品质图形,Matplotlib(http://matplotlib.org/)也许是使用最方便、功能最丰富的程序库了。
1.4.2 安装Python
幸运的是,所有主流操作系统,如Windows、Mac和Linux,都有针对NumPy、SciPy和Matplotlib的安装程序。如果你对安装过程不是很清楚,那么可能就需要安装Enthought Python发行版(https://www.enthought.com/products/epd_free.php)或者Python(x,y)(http://code.google.com/p/
pythonxy/wiki/Downloads),而这些已经包含在之前提到过的程序包里了。
1.4.3 使用NumPy和SciPy智能高效地处理数据
让我们快速浏览一下NumPy的基础示例,然后看看SciPy在NumPy之上提供了哪些东西。在这个过程中,我们将开始使用Matplotlib这个非凡的工具包进行绘图。
你可以在http://www.scipy.org/Tentative_NumPy_Tutorial上找到NumPy所提供的更多有趣示例。
你也会发现由IvanIdris所著的《Python数据分析基础教程:NumPy学习指南(第2版)》非常有价值。你还可以在http://scipy-lectures.github.com上找到辅导性质的指南,并到http://docs. scipy.org/doc/scipy/reference/tutorial访问SciPy的官方教程。
在本书中,我们使用1.6.2版本的NumPy和0.11.0版本的SciPy。
1.4.4 学习NumPy
让我们引入NumPy,并小试一下。对此,需要打开Python交互界面。
>>> import numpy
>>> numpy.version.full_version
1.6.2
由于我们并不想破坏命名空间,所以肯定不能做下面这样的事情:
>>> from numpy import *
这个numpy.array数组很可能会遮挡住标准Python中包含的数组模块。相反,我们将会采用下面这种便捷方式:
>>> import numpy as np
>>> a = np.array([0,1,2,3,4,5])
>>> a
array([0, 1, 2, 3, 4, 5])
>>> a.ndim
1
>>> a.shape
(6,)
这里只是采用了与在Python中创建列表相类似的方法来创建数组。不过,NumPy数组还包含更多关于数组形状的信息。在这个例子中,它是一个含有5个元素的一维数组。到目前为止,并没有什么令人惊奇的。
现在我们将这个数组转换到一个2D矩阵中:
>>> b = a.reshape((3,2))
>>> b
array([[0, 1],
[2, 3],
[4, 5]])
>>> b.ndim
2
>>> b.shape
(3, 2)
当我们意识到NumPy包优化到什么程度时,有趣的事情发生了。比如,它在所有可能之处都避免复制操作。
>>> b[1][0]=77
>>> b
array([[ 0, 1],
[77, 3],
[ 4, 5]])
>>> a
array([ 0, 1, 77, 3, 4, 5])
在这个例子中,我们把b的值从2改成77,然后立刻就会发现相同的改动已经反映在a中。当你需要一个真正的副本时,请记住这个。
>>> c =a.reshape((3,2)).copy()
>>> c
array([[ 0, 1],
[77, 3],
[ 4, 5]])
>>> c[0][0] = -99
>>> a
array([ 0, 1, 77, 3, 4,5])
>>> c
array([[-99, 1],
[ 77, 3],
[ 4, 5]])
这里,c和a是完全独立的副本。
NumPy数组还有一大优势,即对数组的操作可以传递到每个元素上。
>>> a*2
array([ 2, 4, 6, 8, 10])
>>> a**2
array([ 1, 4, 9, 16, 25])
Contrast that to ordinary Python lists:
>>> [1,2,3,4,5]*2
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5]
>>> [1,2,3,4,5]**2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for ** or pow(): 'list' and
'int'
当然,我们在使用NumPy数组的时候会牺牲Python列表所提供的一些敏捷性。像相加、删除这样的简单操作在NumPy数组中会有一点麻烦。幸运的是,这两种方式都可以使用。我们可以根据手头上的任务来选择最适合的那种。
1. 索引
NumPy的部分威力来自于它的通用数组访问方式。
除了正常的列表索引方式,它还允许我们将数组本身当做索引使用。
>>> a[np.array([2,3,4])]
array([77, 3, 4])
除了判断条件可以传递到每个元素这个事实,我们得到了一个非常方便的数据访问方法。
>>> a>4
array([False, False, True, False, False, True], dtype=bool)
>>> a[a>4]
array([77, 5])
这还可用于修剪异常值。
>>> a[a>4] = 4
>>> a
array([0, 1, 4, 3, 4, 4])
鉴于这是一个经常碰到的情况,所以这里有一个专门的修剪函数来处理它。如下面的函数调用所示,它将数组值超出某个区间边界的部分修剪掉。
>>> a.clip(0,4)
array([0, 1, 4, 3, 4, 4])
2. 处理不存在的值
当我们预处理刚从文本文件中读出的数据时,NumPy的索引能力就派上用场了。这些数据中很可能包含不合法的值,我们像下面这样用numpy.NAN做标记,来表示它不是真实数值。
c = np.array([1, 2, np.NAN, 3, 4]) # 假设已经从文本文件中读取了数据
>>> c
array([ 1., 2., nan, 3., 4.])
>>> np.isnan(c)
array([False, False, True, False, False], dtype=bool)
>>> c[~np.isnan(c)]
array([ 1., 2., 3., 4.])
>>> np.mean(c[~np.isnan(c)])
2.5
3. 运行时行为比较
让我们比较一下NumPy和标准Python列表的运行时行为。在下面这些代码中,我们将会计算从1到1000的所有数的平方和,并观察这些计算花费了多少时间。为了使评估足够准确,我们重复做了10 000次,并记录下总时间。
import timeit
normal_py_sec = timeit.timeit('sum(x*x for x in xrange(1000))',
number=10000)
naive_np_sec = timeit.timeit('sum(na*na)',
setup="import numpy as np; na=np.arange(1000)",
number=10000)
good_np_sec = timeit.timeit('na.dot(na)',
setup="import numpy as np; na=np.arange(1000)",
number=10000)
print("Normal Python: %f sec"%normal_py_sec)
print("Naive NumPy: %f sec"%naive_np_sec)
print("Good NumPy: %f sec"%good_np_sec)
Normal Python: 1.157467 sec
Naive NumPy: 4.061293 sec
Good NumPy: 0.033419 sec
我们观察到两个有趣的现象。首先,仅用NumPy作为数据存储(原始NumPy)时,花费的时间竟然是标准Python列表的3.5倍。这让我们感到非常惊奇,因为我们原本以为既然它是C扩展,那肯定要快得多。对此,一个解释是,在Python中访问个体数组元素是相当耗时的。只有当我们在优化后的扩展代码中使用一些算法之后,才能获得速度上的提升。一个巨大的提升是:当使用NumPy的dot()函数之后,可以得到25倍的加速。总而言之,在要实现的算法中,应该时常考虑如何将数组元素的循环处理从Python中移到一些高度优化的NumPy或SciPy扩展函数中。
然而,速度也是有代价的。当使用NumPy数组时,我们不再拥有像Python列表那样基本上可以装下任何数据的不可思议的灵活性。NumPy数组中只有一个数据类型。
>>> a = np.array([1,2,3])
>>> a.dtype
dtype('int64')
如果尝试使用不同类型的元素,NumPy会尽量把它们强制转换为最合理的常用数据类型:
>>> np.array([1, "stringy"])
array(['1', 'stringy'], dtype='|S8')
>>> np.array([1, "stringy", set([1,2,3])])
array([1, stringy, set([1, 2, 3])], dtype=object)
1.4.5 学习SciPy
在NumPy的高效数据结构之上,SciPy提供了基于这些数组的算法级应用。本书中任何一个数值分析方面的重数值算法,你都可以在SciPy中找到相应的支持。无论是矩阵运算、线性代数、最优化方法、聚类、空间运算,还是快速傅里叶变换,都囊括在这个工具包中了。因此在实现数值算法之前先查看一下SciPy模块,是一个好习惯。
为了方便起见,NumPy的全部命名空间都可以通过SciPy访问。因此从现在开始,我们会在SciPy的命名空间中使用NumPy的函数。通过比较这两个基础函数的引用,很容易就可以进行验证,例如:
>>> import scipy, numpy
>>> scipy.version.full_version
0.11.0
>>> scipy.dot is numpy.dot
True
各种各样的算法被分组到下面这个工具包中:
SciPy工具包 |
功 能 |
cluster |
层次聚类(cluster.hierarchy) 矢量量化 / K均值(cluster.vq) |
constants |
物理和数学常量 转换方法 |
fftpack |
离散傅里叶变换算法 |
integrate |
积分例程 |
interpolate |
插值(线性的、三次方的,等等) |
io |
数据输入和输出 |
linalg |
采用优化BLAS和LAPACK库的线性代数函数 |
maxentropy |
最大熵模型的函数 |
ndimage |
n维图像工具包 |
odr |
正交距离回归 |
optimize |
最优化(寻找极小值和方程的根) |
signal |
信号处理 |
(续)
SciPy工具包 |
功 能 |
sparse |
稀疏矩阵 |
spatial |
空间数据结构和算法 |
special |
特殊数学函数如贝塞尔函数(Bessel)或雅可比函数(Jacobian) |
stats |
统计学工具包 |
其中我们最感兴趣的是scipy.stats、scipy.interpolate、scipy.cluster和scipy. signal。为了简单起见,我们将会简要地探索stats包的一些特性,而其余的则在它们各自出现的章中进行解释。
1.5 我们第一个(极小的)机器学习应用
让我们亲自体验一下,看一看我们假想的互联网创业公司MLAAS。它通过HTTP向用户推销机器学习算法服务。但随着公司不断取得成功,要为所有Web访问请求都提供优质服务,就需要具备更好的基础设施。我们并不愿意分配过多的资源,因为这些资源非常昂贵。另一方面,如果没有足够的资源来为所有请求提供服务,我们也将会赔钱。现在的问题是,我们何时会到达目前基础设施的极限。这个极限我们估计是每小时100 000个请求。我们希望事先知道什么时候不得不申请更多的云端服务器来服务于所有请求,同时不必为未使用的服务器承担费用。
1.5.1读取数据
我们已经收集了上个月的Web统计信息,并把它们汇聚到了ch01/data/web_traffic.tsv(因为tsv包含以Tab字符分割的数字)。它们存储着每小时的访问次数。每一行包含连续的小时信息,以及该小时内的Web访问次数。
文件前面几行如下图所示:
使用SciPy的genfromtxt()很容易读取数据。
import scipy as sp
data = sp.genfromtxt("web_traffic.tsv",delimiter="\t")
必须使用Tab作为分隔符,确保可以正确读取各个列的数据。
经快速检验显示,我们已经正确地读取了数据。
>>> print(data[:10])
[[ 1.00000000e+00 2.27200000e+03]
[ 2.00000000e+00 nan]
[ 3.00000000e+001.38600000e+03]
[ 4.00000000e+001.36500000e+03]
[ 5.00000000e+001.48800000e+03]
[ 6.00000000e+001.33700000e+03]
[ 7.00000000e+001.88300000e+03]
[ 8.00000000e+002.28300000e+03]
[ 9.00000000e+001.33500000e+03]
[ 1.00000000e+011.02500000e+03]]
>>> print(data.shape)
(743, 2)
我们有743个二维数据点。
1.5.2 预处理和清洗数据
在SciPy中,为了处理起来更加便利,我们将各维度分成两个向量,其中每个向量的大小是743。第一个向量x包含小时信息,而另一个向量y包含某个小时内的Web访问数。这个切分过程是通过我们选择的某些列,由SciPy的特殊索引标记来完成的。
x = data[:,0]
y = data[:,1]
这里还有很多从SciPy数组中选取数据的方法。可以在http://www.scipy.org/ |
需要说明的是,y中仍然有一些项包含了无效值nan。但问题是,该如何处理这些无效值呢?让我们看一下有多少小时的数据中包含了无效值。
>>> sp.sum(sp.isnan(y))
8
我们看到743个项中只有8个值缺失了,因此把它们删除是可以承受的。记住,我们能够用另一个数组来索引SciPy的数组。sp.isnan(y)返回一个布尔型的数组,用来表示某个数组项中的内容是否是一个数字。我们可以使用~在逻辑上对数组取反,使我们可以在x和y中只选择y值合法的项。
x = x[~sp.isnan(y)]
y = y[~sp.isnan(y)]
为了获得对数据的第一印象,让我们用Matplotlib在散点图上将数据画出来。Matplotlib包含了pyplot包,它模仿了Matlab的接口——一个非常方便和易用的接口。(你可以在http://matplotlib. org/users/pyplot_tutorial.html上看到更多画图方面的教程。)
import matplotlib.pyplot as plt
plt.scatter(x,y)
plt.title("Web traffic over the last month")
plt.xlabel("Time")
plt.ylabel("Hits/hour")
plt.xticks([w*7*24 for w in range(10)],
['week %i'%w for w inrange(10)])
plt.autoscale(tight=True)
plt.grid()
plt.show()
在绘出的图上,可以看到虽然前面几个星期的流量差不多相同,但最后那个星期呈现出显著上升的趋势。
1.5.3 选择正确的模型和学习算法
现在我们已经对数据有了一个初步印象,那么回到起初的问题:服务器要用多长时间来处理进来的Web流量呢?要回答这个问题,必须先做到以下两点:
找到有噪数据背后真正的模型;
使用这个模型预测未来,以便及时找到我们的基础设施必须扩展的地方。
1. 在构建第一个模型之前
谈到模型,你可以把它想象成对复杂现实世界的简化的理论近似。它总会包含一些劣质内容,而这又叫做近似误差。这个误差将指引我们在无数选择中寻找正确的模型。我们用模型预测值到真实值的平方距离来计算这个误差。具体来说,对于一个训练好的模型函数f,按照下面这样来计算误差:
def error(f, x, y):
return sp.sum((f(x)-y)**2)
向量x和y包含我们之前提取的Web统计数据。这正是Scipy向量化函数(这里采用的是f(x))的美妙之处。在训练好的模型中,我们假定它把一个向量作为输入,并返回一个相同大小的向量。这样,我们就可以用它来计算与y之间的差距。
2. 从一条简单的直线开始
让我们假设另外一个例子,它的模型是一条直线。这里的挑战是如何在图中画出一条最佳的直线,使结果中的近似误差最小。SciPy的polyfit()函数正是用来解决这个问题的。给定数据x和y,以及期望的多项式的阶(直线的阶是1),它可以找到一个模型,能够最小化之前定义的误差函数。
fp1,residuals, rank, sv, rcond = sp.polyfit(x, y, 1, full=True)
polyfit()函数会把拟合的模型函数所使用的参数返回,即fp1;而且通过把full置成True,我们还可以获得更多逼近过程的背景信息。在这里面,我们只对残差感兴趣,而这正是近似误差。
>>> print("Model parameters: %s" % fp1)
Model parameters: [ 2.59619213 989.02487106]
>>> print(res)
[ 3.17389767e+08]
这里的意思是说,最优的近似直线如下面这个函数所示:
f(x)= 2.59619213 * x + 989.02487106
然后用poly1d()根据这些参数创建一个模型函数。
>>> f1 = sp.poly1d(fp1)
>>> print(error(f1, x, y))
317389767.34
我们已经利用full=True得到了更多的关于逼近过程的细节。正常来说,我们并不需要这个,只需要返回模型参数即可。
事实上,我们在这里只是做了曲线拟合。更多详细信息,请参考http://en. wikipedia.org/wiki/Curve_fitting。 |
现在用f1()画出第一个训练后的模型。在前述绘图命令之外,我们简单加入如下代码:
fx = sp.linspace(0,x[-1], 1000) # 生成X值用来作图
plt.plot(fx, f1(fx), linewidth=4)
plt.legend(["d=%i" % f1.order], loc="upperleft")
下面这个图中显示了我们第一个训练后的模型:
虽然前面4个星期的数据好像并没有偏离太多,但我们仍然可以清楚地看到,最初的直线模型假设是有问题的。此外,实际误差值317 389 767.34到底是好还是坏呢?
我们从来不拿误差的绝对值单独使用。然而当比较两个竞争的模型时,可以利用它们的绝对误差来判断哪一个更好。尽管第一个模型显然不是我们想要的,但它的工作流程却有一个重要作用:我们可以把它当做基线,直到找到更好的模型。无论将来构造出了什么样的模型,我们都会去和当前的基线做比较。
3. 一些高级话题
现在我们要用一个更复杂的模型来做拟合,来看一个阶数为2的多项式,看看它是否可以更好地“理解”我们的数据。
>>> f2p = sp.polyfit(x, y, 2)
>>> print(f2p)
array([ 1.05322215e-02, -5.26545650e+00, 1.97476082e+03])
>>> f2 = sp.poly1d(f2p)
>>> print(error(f2, x, y))
179983507.878
下面这个图表显示了之前训练好的模型(一阶直线),以及我们新训练出的更复杂的二阶模型(虚线):
这里的误差是179 983 507.878,几乎是直线模型误差的一半。这个效果看起来很不错,然而,它也是有代价的。我们现在得到了一个更复杂的函数,这意味着在polyfit()中多了一个参数需要调整。近似的多项式如下:
f(x)= 0.0105322215 * x**2 - 5.26545650 * x + 1974.76082
在这里,如果复杂性越大效果越好,那么为什么不进一步增加复杂性呢?让我们试一下阶数为3、10和100的函数。
数据越复杂,曲线对数据逼近得越好。它们的误差值似乎也反映出了同样的结果。
Error d=1: 317,389,767.339778
Error d=2: 179,983,507.878179
Error d=3: 139,350,144.031725
Error d=10: 121,942,326.363461
Error d=100: 109,318,004.475556
然而,如果近距离观察拟合出的曲线,我们就会开始对它们能否捕捉到真实的数据生成过程心生疑虑。换句话说,我们的模型是否真正代表了广大客户访问我们网站的行为呢?看看10阶和100阶的多项式,我们发现了巨大的震荡。似乎这些模型对数据拟合得太过了。它不但捕捉到了背后的数据生成过程,还把噪声也包含进去了,这就叫做过拟合(overfitting)。
在这里,我们有如下的选择。
选择其中一个拟合出的多项式模型。
换成另外一类更复杂的模型;样条(splines)?
从不同的角度思考数据,然后重新开始。
在上述这5个拟合模型中,1阶模型明显太过简单了,而10阶和100阶的模型显然是过拟合了。只有2阶和3阶模型似乎还比较匹配数据。然而,如果在数据的两个边界上进行预测,我们会发现它们的效果令人抓狂。
换成另外一类更复杂的模型似乎也是一个错误路线。那么什么样的论据会支持哪类模型呢?在这里,我们意识到,也许我们还没有真正理解数据。
4. 以退为进——另眼看数据
在此,我们退回去从另一个角度来看数据。似乎在第3周和第4周的数据之间有一个拐点。这让我们可以以3.5周作为分界点把数据分成两份,并训练出两条直线来。我们使用到第3周之前的数据来训练第一条线,用剩下的数据训练第2条线。
inflection = 3.5*7*24 # 计算拐点的小时数
xa = x[:inflection] # 拐点之前的数据
ya = y[:inflection]
xb = x[inflection:] # 之后的数据
yb = y[inflection:]
fa = sp.poly1d(sp.polyfit(xa, ya, 1))
fb = sp.poly1d(sp.polyfit(xb, yb, 1))
fa_error = error(fa, xa, ya)
fb_error = error(fb, xb, yb)
print("Error inflection=%f" % (fa + fb_error))
Error inflection=156,639,407.701523
我们在这两组数据的范围之内画出了这两个模型,如下图所示:
很明显,两条线组合起来似乎比之前我们做的任何模型都能更好地拟合数据。但组合之后的误差仍然高于高阶多项式的误差。我们最后能否相信这个误差呢?
换一个方式来问,相比于其他复杂模型,为什么仅在最后一周数据上更相信拟合的直线模型呢?这是因为我们认为它更符合未来数据。如果在未来时间段上画出模型,就可以看到这是非常正确的(d=1是我们最初的直线模型)。
10阶和100阶的模型在这里似乎并没有什么光明的未来。它们非常努力地对给定数据正确建模,但它们明显没法推广到将来的数据上。这个叫做过拟合。另一方面,低阶模型似乎也不能恰当地拟合数据。这个叫做欠拟合(underfitting)。
所以让我们公平地看待2阶或者更高阶的模型,并且试验一下如果只拟合最后一周数据的话,会有什么样的效果。毕竟,我们相信最后一周的数据比之前的数据更符合未来数据的趋势。下面这个有些迷幻的图表中给出了结果。这里更加明显地显示出过拟合问题是如何不好的。
然而,当模型只在3.5周及以后数据上训练时,从模型误差中判断,仍然应该选择最复杂的那个模型。
Error d=1: 22143941.107618
Error d=2: 19768846.989176
Error d=3: 19766452.361027
Error d=10: 18949339.348539
Error d=100: 16915159.603877
5. 训练与测试
如果有一些未来数据能用于模型评估,那么仅从近似误差结果中就应该可以判断出我们选择的模型是好是坏了。
尽管我们看不到未来的数据,但可以从现有数据中拿出一部分,来模拟类似的效果。例如,把一定比例的数据删掉,并使用剩下的数据进行训练,然后在拿出的那部分数据上计算误差。由于模型在训练中看不见拿出的那部分数据,所以就可以对模型的未来行为得到一个较为真实的预估。
只利用拐点时间后的数据训练出来的模型,其测试误差显现出了一个完全不同的境况。
Error d=1: 7,917,335.831122
Error d=2: 6,993,880.348870
Error d=3: 7,137,471.177363
Error d=10: 8,805,551.189738
Error d=100: 10,877,646.621984
结果显示在下图中:
看来最终的胜者已经一目了然。2阶模型的测试误差最低,而这个误差是在模型训练中未使用的那部分数据上评估得到的。这让我们相信,当未来数据到来时,不会遇到糟糕的意外。
6. 回答最初的问题
最终得到了一个模型,我们认为它可以最好地代表数据生成过程;现在,要获悉我们的基础设施何时到达每小时100 000次请求,已经是一个简单的事情了,只需要计算何时我们的模型函数到达100000这个值即可。
对于2阶模型,我们可以简单地计算出它的逆函数,并得到100 000上的结果。当然,我们还希望有一个可以适用于任何模型函数的方法。
可以这样做:从多项式中减去100000,得到另一个多项式,然后计算出它的根。如果提供了参数的初始值,SciPy的optimize模块有一个fsolve函数可以完成这项工作。假设这个胜出的2阶多项式是fbt2:
>>> print(fbt2)
2
0.08844 x - 97.31 x + 2.853e+04
>>> print(fbt2-100000)
2
0.08844 x - 97.31 x - 7.147e+04
>>> from scipy.optimize import fsolve
>>> reached_max = fsolve(fbt2-100000, 800)/(7*24)
>>> print("100,000 hits/hour expected at week %f"% reached_max[0])
100,000 hits/hour expected at week 9.827613
模型告诉我们,鉴于目前的用户行为和我们公司的推进力,还有一个月才会到达访问容量的界限。
当然,加入了我们的预测之后,会出现一定的不确定性。要获知真实的情况,需要更复杂的统计学知识来计算我们在望向更远处时所期望的方差。
对一些用户和潜在用户的动态行为,仍然无法准确地建模。但是,目前的预测对我们来说已经不错了。毕竟,现在可以对所有的耗时行为有所准备。如果能够对Web流量密切监控,我们就可以及时发现何时需要分配新的资源。
1.6 小结
恭喜你!你刚刚学到了两件重要的事情。其中最重要的是,你要明白,作为一名典型的机器学习践行者,你会在理解和提炼数据上花费大部分精力——这正是我们在第一个微小的机器学习示例中所做的。我们希望这个例子可以帮你把精力从算法转移到数据上来。在这之后,我们还一起了解了一下正确设置实验的重要性,其中至关重要的是,不要把训练数据和测试数据混在一起。
诚然,使用多项式拟合并不是机器学习领域最酷的事情。这个例子只是为了让你明白,不要让一些“闪闪发光”的算法分散你的注意力。这里包含上面总结的最重要的两点。
所以,让我们开始学习第2章的内容。我们将深入探究Scikit-learn这个令人惊奇的机器学习工具箱,并概述不同类型的学习算法,同时向你展示特征工程的美妙之处。
我们此前已经给大家送了不少Python学习书籍,比如此前很受大家欢迎的《Python编程:从入门到实践》《流畅的Python》《Python机器学习经典实例》,但仅仅阅读书籍距离成为一名Python高手还是有一段距离的,尤其是无监督的自学环境下。
为了帮助大家更快更高效地学习Python,CSDN学院特推出年度大课《从零基础到Python全栈工程师的成长之路》, 直播+录播,配备班主任和助教, 设置闯关制来监督学习效果,帮你在成为Python高手之路上更进一步。
☞ 点击阅读原文,查看课程详细信息。