Mastering Machine Learning With scikit-learn 中文版

461 77 25MB

Chinese Pages [155] Year 2016

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Mastering Machine Learning With scikit-learn 中文版

Citation preview

前言 这几年机器学习这种从经验学习的软件技术重现光明。在计算机诞生的早期,机器学习的概念已经出 现,各种理论天马行空,限于计算成本而未能普及。随着计算设备的普及,日常生活中越来越多的机 器学习应用,可以说它的成功开始变得习以为常。新应用如雨后春笋一般出现,很多都从机器学习中 获得动力。

在这本书里,我们将看到一些机器学习的模型和算法。我们会介绍一些常用的机器学习任务和模型的 效果评估方法。而这些模型和算法都是通过十分流行的Python机器学习库scikit-learn来完成,里面有 许多机器学习的模型和算法,每个API都简单易用。 本书特点主要有: 内容通俗易懂。本书只需要基本的编程和数学知识 案例实用。本书的案例都很容易上手,读者可以调整后解决自己的问题。

本书内容简介 第1章,机器学习基础 (http://muxuezi.github.io/posts/1-the-fundamentals-of-machinelearning.html),将机器学习定义成一种通过学习经验改善工作效果的程序研究与设计过程。其他章节 都以这个定义为基础,后面每一章里介绍的机器学习模型都是按照这个思路解决任务,评估效果。 第2章,线性回归 (http://muxuezi.github.io/posts/2-linear-regression.html),介绍线性回归模型,一 种解释变量和模型参数与连续的响应变量相关的模型。本章介绍成本函数的定义,通过最小二乘法求 解模型参数获得最优模型。 第3章,特征提取与处理 (http://muxuezi.github.io/posts/3-feature-extraction-andpreprocessing.html),介绍了常见的机器学习对象如文本,图像与分类变量的特征提取与处理方法。 第4章,从线性回归到逻辑回归 (http://muxuezi.github.io/posts/4-from-linear-regression-to-logisticregression.html),介绍广义线性回归模型如何解决分类任务。将逻辑回归模型与特征提取技术结合起 来实现一个垃圾短信分类器。 第5章,决策树——非线性回归与分类 (http://muxuezi.github.io/posts/5-nonlinear-classificationand-regression-with-decision-trees.html),介绍了一种回归和分类的非线性模型——决策树。用决 策树集成方法实现了一个网页广告图片屏蔽器。 第6章,K-Means聚类 (http://muxuezi.github.io/posts/6-clustering-with-k-means.html),介绍非监督 学习的K-Means聚类算法,并与逻辑回归组合起来实现一个照片分类器。 第7章,用PCA降维 (http://muxuezi.github.io/posts/7-dimensionality-reduction-with-pca.html),介 绍另一种非监督学习任务——降维。我们用主成分分析实现高维数据的可视化,建立一个脸部识别 器。

第8章,感知器 (http://muxuezi.github.io/posts/8-the-perceptron.html),介绍一种实时的,二元分类 器——感知器。后面两章都是针对感知器的缺点发展起来的。 第9章,从感知器到支持向量机 (http://muxuezi.github.io/posts/9-from-the-perceptron-to-supportvector-machines.html),介绍支持向量机,是一种有效的非线性回归与分类模型。我们用支持向量机 识别街景照片中的字母。 第10章,从感知器到人工神经网络 (http://muxuezi.github.io/posts/10-from-the-perceptron-toartificial-neural-networks.html),介绍了人工神经网络,是一种强大的有效的非线性回归与分类模 型。我们用人工神经网络识别手写数字。

机器学习基础 本章我们简要介绍下机器学习(Machine Learning)的基本概念。主要介绍机器学习算法的应用,监 督学习和无监督学习(supervised-unsupervised learning)的应用场景,训练和测试数据的用法,学 习效果评估方式。最后,我们介绍scikit-learn及其安装方法。

自计算机问世以来,计算机可以学习和模仿人类智慧的观点,可谓“引无数英雄竞折腰”。像Arthur C. Clarke的HAL(Heuristically programmed ALgorithmic computer) (https://en.wikipedia.org/wiki/HAL_9000)和Isaac Asimov的Sonny (https://en.wikipedia.org/wiki/I,_Robot_(film))那样的人工智能已经成为共识,通过学习经验获得新知 识和技能的软件程序也变得越来越多。我们用这机器学习程序发现我们会喜欢的新音乐,快速找出我 们想网购的鞋子。机器学习程序让我们通过命令控制手机,让恒温器自动调节温度。比人类更准确的 识别出潦草的手写邮箱地址,更安全的保护信用卡防止诈骗。从新药品调查到从网页寻找头条新闻, 机器学习软件逐渐成为许多产业的核心工具。机器学习已经进入长期以来一直被认为只有人类才能胜 任的领域,如杜克大学UNC(Duke)篮球队输给了北卡(UNC)的体育报道。 机器学习是设计和研究能够根据过去的经验来为未来做决策的软件,它是通过数据进行研究的程序。 机器学习的基础是归纳(generalize),就是从已知案例数据中找出未知的规律。机器学习的典型案 例就是垃圾邮件过滤。通过对数千份已经打上是否为垃圾标签的邮件进行观察经验,对新邮件进行过 滤。 人工智能研究领域的计算机科学家Arthur Samuel说,机器学习是“研究如何让计算机可以不需要明确 的程序也能具有学习能力”。在20世纪五六十年代,Samuel开发了下象棋程序。程序的规则非常简 单,要打败专业对手需要复杂的策略,但是通过几千局游戏的训练,程序学会了复杂的策略,可以打 败很多人类棋手。 计算机科学家Tom Mitchell对机器学习的定义更正式,“一个程序在完成任务T后获得了经验E,其表 现为效果P,如果它完成任务T的效果是P ,那么会获得经验E”。例如,假设你有一些图片,每个图 片里是一条狗或一只猫。程序可以通过观察图片来学习,然后它可以通过计算图片正确分类比例来评 估学习效果。 我们将使用Mitchell的定义来组织这一章的内容。首先,我们要介绍经验的类型,包括监督学习和无 监督学习。然后,我们介绍机器学习系统可以处理的常见任务。最后,我们介绍机器学习系统效果评 估方式。

从经验中学习 机器学习系统通常被看作是有无人类监督学习两种方式。监督学习问题是,从成对的已经标记好的输 入和输出经验数据作为一个输入进行学习,用来预测输出结果,是从有正确答案的例子中学习。而无 监督学习是程序不能从已经标记好的数据中学习。它需要在数据中发现一些规律。假如我们获取了人 的身高和体重数据,非监督学习的例子就是把数据点分成组别。一种程序可能是把数据分成男人与女 人,儿童与成人等不同组别。

再假设数据都标记了人的性别。那么,一种监督学习方式就是基于一个人的身高和体重数据来预测这 个人是男是女。后面我们会介绍监督学习与非监督学习的算法和案例。 监督学习与非监督学习可以看作机器学习的两端。还有一些中间类型,称为半监督学习,既包含监督 数据也有非监督数据,这类问题可以看作是介于监督学习与非监督学习之间的学习。半监督机器学习 案例是一种增强学习(Reinforcement Learning),问题可以通过决策来获得反馈,但是反馈与某一个 决策可能没有直接关系。例如,一个增强学习程序学习玩超级玛丽游戏,让它完成一级或超过一定分 数会获得奖励,如果失败一次会受到惩罚。但是,监督反馈与具体要执行的决策无关,避开板栗仔 (Goombas)或者跳过火轮圈。本书讨论的半监督学习将集中于监督与非监督学习,因为这些类型 包括机器学习的绝大多数问题。下一章,我们会介绍监督学习与非监督学习的更多细节。 监督学习是通过一个输入产生一个带标签的输出的经验数据对中进行学习。机器学习程序中输出结果 有很多名称,一些属于机器学习领域,另外一些是专用术语。本书中,我们把输出结果称为响应值 (response variable),不过输出结果还有其他名称,如因变量(dependent variables),回归值 (regressands),标准变量(criterion variables),测得变量(measured variables),解释变量 (explained variables),结果变量(outcome variables),实验变量(experimental variables), 标签(labels),和输出变量(output variables)。同理,输入变量也有很多名称。本书把输入变量 作为特征(features),它们测量的现象作为解释变量(explanatory variables)。输入变量的其他名 称有,预测值(predictors),解释变量(regressors),控制变量(controlled variables),操作便 利(manipulated variables)和显现变量(exposure variables)。响应变量和解释变量可能需要真 实的或不相关的数值。 构成监督学习经验的案例集合称为训练集(training set)。评估程序效果的案例集合称为测试集 (test set)。响应变量可以看成是解释变量构成问题的答案。监督学习问题从不同问题结合中学 习,就是说,监督学习程序输入是正确的答案,需要对类似的问题作出正确的反馈。

机器学习任务 常见的监督式机器学习任务就是分类(classification)和回归(regression)。分类认为需要学会从 若干变量约束条件中预测出目标变量的值,就是必须预测出新观测值的类型,种类或标签。分类的应 用包括预测股票的涨跌,新闻头条是政治新闻还是娱乐新闻。回归问题需要预测连续变量的数值,比 如预测新产品的销量,或者依据工作的描述预算工资水平等。与分类方式类似,回归问题需要监督学 习。 常见的无监督式机器学习任务是通过训练数据发现相关观测值的组别,称为类(clusters)。对应的 任务称为聚类(clustering),通过一些相似性度量方法把一些观测值分成同一类。聚类常用来分析 数据集。比如有一些影评数据,聚类算法可以分辨积极的和消极的影评。系统是不能给类加上“积 极”或“消极”的标签的;没有监督,系统只能通过相似性度量方法把观测值分成两类。聚类分析的应 用场景是用市场产品销售数据为客户分级。通过挖掘一组用户的共同属性,销售人员可以为这类客户 提供定制服务。聚类还被用于互联网广播服务,比如有一些歌曲,聚类算法能够按风格流派把歌曲分 组。通过不同的相似性度量方法,同样的聚类算法可能通过关键词来分组,也可能通过使用的乐器来 分组。 降维(Dimensionality reduction)是另一个常见的无监督学习任务。有些问题可能包含成千上万个解 释变量,处理起来非常麻烦。另外,有些解释变量属于噪音,也有些完全是无边的变量,这些影响都 会降低程序的归纳能力。降维是发现对响应变量影响最大的解释变量的过程。降维可以更容易的实现

数据可视化。如不同面积房子的价格数据可视化,房子的面积可以画在x轴,其价格可以画在y轴, 很容易实现可视化。再加一个解释变量,也很容易可视化房屋价格的回归问题,比如房间里卫生间的 数量可以画在z轴。但是,几千个解释变量的问题是不可能可视化的。

训练数据和测试数据 训练集里面的观测值构成了算法用来学习的经验数据。在监督学习问题中,每个观测值都由一个响应 变量和若干个解释变量组成。 测试集是一个类似的观测值集合,用一些度量标准来评估模型的运行效果。需要注意的是,测试集的 数据不能出现在训练集中。否则,很难评价算法是否从训练集中学到了归纳能力,或者仅仅只是简单 的记录了结果。归纳很好的程序能够用新数据有效地完成任务。相反,一个通过记忆训练数据来学习 复杂模型的程序,可能通过训练集准确预测响应变量的值,但是在处理新问题的时候由于没有归纳能 力会预测失败。 训练集的记忆称为过度拟合(over-fitting)。一个记住了观测值的程序不一定能够很好的完成工作, 因为它在记忆关系和结果的时候,把噪声也同时记住了。平衡记忆能力与归纳能力,或者说是过度拟 合与拟合不够,是许多机器学习算法面对的共同问题。后面的章节,我们会介绍正则化 (regularization),可以用来减轻许多模型的过度拟合程度。 除了训练集和测试集,还有一个观测值集合称为验证集(validation set或 hold-out set),有时候需 要用到。验证集用来调整超参数(hyperparameters)变量,这类变量控制模型是如何学习的。这个 程序也通过测试集来评估其真实的效果,验证集的效果不能用于评估其真实的效果,由于程序参数已 经用验证数据调整过了。通常会把监督学习的观测值分成训练、验证和测试集三部分。各部分的大小 没有要求,按实际观测值的规模来定。一般把50%以上的数据作为训练集,25%的数据做测试集, 剩下的作为验证集。

有的训练集只包含几百个观测值,有的可能有几百万。随着存储成本越来越便宜,网络连接范围不断 扩大,内置传感器的智能手机的普及,以及对隐私数据态度的转变都在为大数据新动力,千万甚至上 亿级别的训练集成为可能。本书不会涉及这类需要上百个机器并行计算才能完成的任务,许多机器学 习算法的能力会随着训练集的丰富变得更强大。但是,机器学习算法也有句老话“放入的是垃圾,出 来的也是垃圾”。一个学习了一大堆错误百出的教材的学生不会比只读一点好书的学生考得好。同 理,对一堆充满噪声、没有关联、或标签错误的数据进行学习的算法,也不会比只学习一小部分更有 代表性的训练集的算法效果更好。 许多监督学习的训练集都是手工准备的,或者半自动处理。建一个海量监督数据集需要耗费许多资 源。好在scikit-learn有些数据集,可以让开发者直接验证自己的模型。在开发阶段,尤其是训练集不 够的时候,交叉验证(cross-validation )的方法可以用相同的数据对算法进行多次训练和检验。在 交叉验证中,训练数据是分成N块的。算法用N-1块进行训练,再用最后一块进行测试。每块都被算 法轮流处理若干次,保证算法可以在训练和评估所有数据。下图就是5块数据的交叉验证方法:

数据集被等分成5块,从A标到E。开始的时候,模型用B到E进行训练,在A上测试。下一轮,在A, C,D和E上训练,用B进行测试。依次循环,直到每一块都测试过。交叉验证为模型的效果评估提供 了比只有一个数据集更准确的方法。

效果评估,偏差,方差 许多度量方法可以用于评估一个程序是否学会了有效处理任务。在监督学习问题中,很多效果度量标 准用来评估预测误差。有两种基本的预测误差:模型的偏差(bias)和方差(variance)。假设你有 很多训练集都是不一样的,但是都具有代表性。一个高偏差的模型会产生类似的误差,无论它使用哪 个训练集。模型偏离自己对真实关系假设的误差超过了模型在训练集训练的结果。模型有高偏差是固 定不变的,但是模型有高方差可能是灵活的,因为模型发觉了训练集里面的噪音部分。就是说,高方 差的模型是过度拟合了训练集数据,而一个模型有高偏差的时候,其实是拟合不够的表现。 偏差和方差就像飞镖射到靶子上。每个飞镖就是从不同数据集得出的预测结果。高偏差、低误差的模 型就是把飞镖扔到了离靶心很远的地方,但是都集中在一个位置。而高偏差、高误差的模型就是把飞 镖扔到了靶子上,但是飞镖离靶心也很远,而且彼此间很分散。低偏差、高误差的模型就是把飞镖扔 到了离靶心很近的地方,但是聚类效果不好。最后就是低偏差、低误差的模型,把飞镖扔到了离靶心 很近的地方,聚类效果也很好。如下图所示:

在理想情况下,模型具有低偏差和低误差,但是二者具有背反特征,即要降低一个指标的时候,另一 个指标就会增加。这就是著名的偏差-方差均衡(Bias-Variance Trade-off)。后面我们会介绍很多模型 的偏差-方差均衡特点。

无监督学习问题没有误差项要评估,其效果的是评估数据结构的一些属性。 大多数效果评估方法只能用于具体的任务。机器学习系统应该可以这样评估:用系统在真实世界中发 生错误的代价来表示效果评估方法。这看起来很明显,下面例子描述的是适用于一般任务而不只是具 体任务的效果评估方法。 有一个对肿瘤数据进行观察的机器学习系统分类任务,需要预测肿瘤是恶性的(malignant)还是良 性的(benign)。准确度,或者是正确分类的比例,就是对程序效果评价的直观度量方法。准确度能 够评价程序效果,不过它不能区分出,误把良性肿瘤分为恶性肿瘤,和误把恶性肿瘤分为良性肿瘤的 效果差异。在一些应用里,发生不同类型错误的代价是相同的。但是,在这个问题里面,没有识别出 恶性肿瘤的代价要比误把良性肿瘤分为恶性肿瘤的代价要大的多。

我们可以通过对每一种可能的预测结果进行评估来建立分类系统效果的不同评价方法。当系统正确地 识别出一个恶性肿瘤,这个预测叫真阳性(True positive);如果把一个良性肿瘤分成了一个恶性肿 瘤,叫假阳性(False positive);正确地识别出一个良性肿瘤叫真阴性(True negative);把一个恶性肿 瘤分成了一个良性肿瘤,叫假阴性(False negative)。这四个结果可以用来计算分类系统效果的评价 体系,包括准确率(accuracy),精确率(precision)和召回率(recall)三项指标。

准度率计算公式如下,TP是真阳性统计结果,TN是真阴性统计结果,FP是假阳性统计结果,FN是假 阴性统计结果: TP + TN ACC = TP + TN + FP + FN

精确率是被判断为恶性肿瘤中,真正为恶性肿瘤统计结果所占比例:

精确率是被判断为恶性肿瘤中,真正为恶性肿瘤统计结果所占比例: TP P = TP + FP

召回率是指真正为恶性肿瘤被分类系统判断出来的比例: TP R = TP + FN

在这个例子中,精确率是评估被系统判断为恶性肿瘤中的肿瘤里面,确实为恶性肿瘤的比例。而召回 率是评估真实的恶性肿瘤被系统正确判断出来的比例。 从精确率和召回率评估指标可以看出,高准确率的分类系统实际没有发现出所有的恶性肿瘤。如果绝 大多数肿瘤都是良性的,那么分类器没有预测出恶性肿瘤也可以获得极高的准确率。而一个具有低准 确率和高召回率的分类系统反而更好,因为它能够识别出更多恶性肿瘤。 许多其他效果评估方法都可以用于分类方法中,后面我们会介绍一些,包括多标签分类问题的评价标 准。下一章,我们会介绍一些回归问题的常用评价标准。

scikit-learn简介 自2007年发布以来,scikit-learn已经成为最给力的Python机器学习库(library)了。scikit-learn支持 的机器学习算法包括分类,回归,降维和聚类。还有一些特征提取(extracting features)、数据处 理(processing data)和模型评估(evaluating models)的模块。 作为Scipy库的扩展,scikit-learn也是建立在Python的NumPy和matplotlib库基础之上。NumPy可以 让Python支持大量多维矩阵数据的高效操作,matplotlib提供了可视化工具,SciPy带有许多科学计算 的模型。 scikit-learn文档完善,容易上手,丰富的API,使其在学术界颇受欢迎。开发者用scikit-learn实验不 同的算法,只要几行代码就可以搞定。scikit-learn包括许多知名的机器学习算法的实现,包括 LIBSVM和LIBLINEAR。还封装了其他的Python库,如自然语言处理的NLTK库。另外,scikit-learn内 置了大量数据集,允许开发者集中于算法设计,节省获取和整理数据集的时间。 scikit-learn可以不受任何限制,遵从自由的BSD授权。许多scikit-learn的算法都可以快速执行而且可 扩展,除了海量数据集以外。最后,scikit-learn稳定性很好,大部分代码都可以通过Python的自动化 测试(mock,nose等)。

安装scikit-learn 目前scikit-learn的稳定版本是0.16.1。用这个版本可以保证本书的代码可以正常运行,如果你之前装 过,可以检查一下版本:

In [3]: import sklearn sklearn.__version__ Out[3]: '0.16.1' 如果你还没安装,你可以通过pip安装,也可以从源代码安装。下面我们简单介绍一下Windows, Linux和Mac OS系统分别安装的过程,具体内容可以从官方网站 (http://scikitlearn.org/dev/install.html)找到。Python版本需要是Python (>= 2.6 or >= 3.3),NumPy (>= 1.6.1), SciPy (>= 0.9)。

Windows系统安装 强烈建议使用miniconda (http://conda.pydata.org/miniconda.html)安装,可以根据Windows系统版 本和想要的Python版本选择下载安装。 miniconda会自动安装scikit-learn的依赖包,如NumPy和SciPy等。双击安装文件安装miniconda,之 后在命令行工具输入下列指令: conda install scikit-learn 确实之后一会儿就安装完了,如果要更新版本,直接输入: conda update scikit-learn 如果已经装好了Python,Numpy和Scipy,可以用pip安装(Python 2.7.9和Python3.4自带): pip install -U scikit-learn 如果想从源代码编译scikit-learn,请参阅官方最新文档,可以从GitHub下载最新代码 (https://github.com/scikit-learn/ scikit-learn)。

Linux系统安装 Linux系统安装由你的发行版决定。最好的方法是用pip安装,也可以通过源代码编译安装。下面以 Ubuntu 14.10为例: pip安装很简单: sudo pip install -U scikit-learn 源代码安装需要一堆依赖: sudo apt-get install python-dev python-numpy python-numpy-dev pyth onsetuptools python-numpy-dev python-scipy libatlas-dev g++ 然后执行命令:

python setup.py install

Mac OS系统安装 直接用pip很简单: pip install -U numpy scipy scikit-learn

安装版本检验 装完之后,就可以像前面那样检验一下版本:

In [3]: import sklearn sklearn.__version__ Out[3]: '0.16.1' 要执行scikit-learn的单元测试,首先要安装nose库,然后运行: nosetests -v sklearn 这样scikit-learn就安装好了。

安装pandas和matplotlib库 本书还会用到pandas (http://pandas.pydata.org/pandas-docs/stable/index.html)库,是一个开源 Python数据分析工具。Windows,Linux和Mac OS系统都可以用pip安装: pip install pandas 如果安装了miniconda,也可以用conda命令安装。 在Debian-和Ubuntu-系统上也可以通过apt-get命令安装: apt-get install python-pandas matplotlib (http://matplotlib.org/)是一个图形库,可以很容易的创建点图,直方图,曲线图等不同样 式的图形。matplotlib的依赖关系和pandas一样,都需要Numpy。前面已经装过了,一样可以通过 pip或conda安装,在Debian-和Ubuntu-系统上也可以通过apt-get命令安装: apt-get install python-matplotlib Windows和Mac OS系统系统上也有可执行文件可以安装。

总结 本章,我们把机器学习定义成一种程序的设计和研究过程,其可以建立一种从一件任务的过往经验中 学习并改善处理能力的程序。我们讨论了经验监督的范围。一端是监督学习,程序从打上标签的输入 和输出数据中学习。另外一种是无监督学习,程序需要发现没有标签数据的内置结构。半监督学习同 时使用有标签和无标签的训练数据。 我们通过案例介绍了机器学习的常见问题。在分类任务中,程序需要从解释变量预测出响应变量的离 散数值。在回归任务中,程序从解释变量预测出响应变量的连续数值。无监督学习任务包括聚类和降 维,聚类是将观测值通过相似度评价方法分成不同的类,降维是将解释变量集合缩减为一个合成特性 集合,同时尽可能的保留数据的信息。我们还介绍了偏差-方差均衡和不同机器学习任务的效果评价 方法。 最后,我们介绍了scikit-learn的历史,目标和优点,以及scikit-learn和相关开发工具的安装过程。下 一章,我们就详细的介绍回归问题,用scikit-learn建立本书的第一个模型。

线性回归 本章介绍用线性模型处理回归问题。从简单问题开始,先处理一个响应变量和一个解释变量的一元问 题。然后,我们介绍多元线性回归问题(multiple linear regression),线性约束由多个解释变量构 成。紧接着,我们介绍多项式回归分析(polynomial regression问题),一种具有非线性关系的多元 线性回归问题。最后,我们介绍如果训练模型获取目标函数最小化的参数值。在研究一个大数据集问 题之前,我们先从一个小问题开始学习建立模型和学习算法。

一元线性回归 上一章我们介绍过在监督学习问题中用训练数据来估计模型参数。训练数据由解释变量的历史观测值 和对应的响应变量构成。模型可以预测不在训练数据中的解释变量对应的响应变量的值。回归问题的 目标是预测出响应变量的连续值。本章我们将学习一些线性回归模型,后面会介绍训练数据,建模和 学习算法,以及对每个方法的效果评估。首先,我们从简单的一元线性回归问题开始。 假设你想计算匹萨的价格。虽然看看菜单就知道了,不过也可以用机器学习方法建一个线性回归模 型,通过分析匹萨的直径与价格的数据的线性关系,来预测任意直径匹萨的价格。我们先用scikitlearn写出回归模型,然后我们介绍模型的用法,以及将模型应用到具体问题中。假设我们查到了部 分匹萨的直径与价格的数据,这就构成了训练数据,如下表所示: 训练样本 直径(英寸) 价格(美元) 1

6

7

2

8

9

3

10

13

4

14

17.5

5

18

18

我们可以用matplotlib画出图形:

In [1]: %matplotlib inline import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties font = FontProperties(fname=r"c:\windows\fonts\msyh.ttc", size=10)

In [2]: def runplt(): plt.figure() plt.title('匹萨价格与直径数据',fontproperties=font) plt.xlabel('直径(英寸)',fontproperties=font) plt.ylabel('价格(美元)',fontproperties=font) plt.axis([0, 25, 0, 25]) plt.grid(True) return plt plt = runplt() X = [[6], [8], [10], [14], [18]] y = [[7], [9], [13], [17.5], [18]] plt.plot(X, y, 'k.') plt.show()

上图中,'x'轴表示匹萨直径,'y'轴表示匹萨价格。能够看出,匹萨价格与其直径正相关,这与我们的 日常经验也比较吻合,自然是越大越贵。下面我们就用scikit-learn来构建模型。

In [14]: from sklearn.linear_model import LinearRegression # 创建并拟合模型 model = LinearRegression() model.fit(X, y) print('预测一张12英寸匹萨价格:$%.2f' % model.predict([12])[0]) 预测一张12英寸匹萨价格:$13.68 一元线性回归假设解释变量和响应变量之间存在线性关系;这个线性模型所构成的空间是一个超平面 (hyperplane)。超平面是n维欧氏空间中余维度等于一的线性子空间,如平面中的直线、空间中的 平面等,总比包含它的空间少一维。在一元线性回归中,一个维度是响应变量,另一个维度是解释变 量,总共两维。因此,其超平面只有一维,就是一条线。

上述代码中sklearn.linear_model.LinearRegression类是一个估计器(estimator)。估计 器依据观测值来预测结果。在scikit-learn里面,所有的估计器都带有fit()和predict()方 法。fit()用来分析模型参数,predict()是通过fit()算出的模型参数构成的模型,对解释变量 进行预测获得的值。因为所有的估计器都有这两种方法,所有scikit-learn很容易实验不同的模型。 LinearRegression类的fit()方法学习下面的一元线性回归模型: y =

α + βx

表示响应变量的预测值,本例指匹萨价格预测值,x 是解释变量,本例指匹萨直径。截距α 和相关系 数β 是线性回归模型最关心的事情。下图中的直线就是匹萨直径与价格的线性关系。用这个模型,你 可以计算不同直径的价格,8英寸$7.33,20英寸$18.75。 y

In [42]: plt = runplt() plt.plot(X, y, 'k.') X2 = [[0], [10], [14], [25]] model = LinearRegression() model.fit(X, y) y2 = model.predict(X2) plt.plot(X, y, 'k.') plt.plot(X2, y2, 'g-') plt.show()

一元线性回归拟合模型的参数估计常用方法是普通最小二乘法(ordinary least squares )或线性最小 二乘法(linear least squares)。首先,我们定义出拟合成本函数,然后对参数进行数理统计。

带成本函数的模型拟合评估 下图是由若干参数生成的回归直线。如何判断哪一条直线才是最佳拟合呢?

In [43]: plt = runplt() plt.plot(X, y, 'k.') y3 = [14.25, 14.25, 14.25, 14.25] y4 = y2 * 0.5 + 5 model.fit(X[1:-1], y[1:-1]) y5 = model.predict(X2) plt.plot(X, y, 'k.') plt.plot(X2, y2, 'g-.') plt.plot(X2, y3, 'r-.') plt.plot(X2, y4, 'y-.') plt.plot(X2, y5, 'o-') plt.show()

成本函数(cost function)也叫损失函数(loss function),用来定义模型与观测值的误差。模型预 测的价格与训练集数据的差异称为残差(residuals)或训练误差(training errors)。后面我们会用 模型计算测试集,那时模型预测的价格与测试集数据的差异称为预测误差(prediction errors)或训 练误差(test errors)。 模型的残差是训练样本点与线性回归模型的纵向距离,如下图所示:

In [64]: plt = runplt() plt.plot(X, y, 'k.') X2 = [[0], [10], [14], [25]] model = LinearRegression() model.fit(X, y) y2 = model.predict(X2) plt.plot(X, y, 'k.') plt.plot(X2, y2, 'g-') # 残差预测值 yr = model.predict(X) for idx, x in enumerate(X): plt.plot([x, x], [y[idx], yr[idx]], 'r-') plt.show()

我们可以通过残差之和最小化实现最佳拟合,也就是说模型预测的值与训练集的数据最接近就是最佳 拟合。对模型的拟合度进行评估的函数称为残差平方和(residual sum of squares)成本函数。就是 让所有训练数据与模型的残差的平方之和最小化,如下所示: n

SSres =



(yi

− f (x ))

2

i

i=1

其中,yi 是观测值,f (xi )是预测值。 残差平方和计算如下:

In [74]: import numpy as np print('残差平方和: %.2f' % np.mean((model.predict(X) - y) ** 2)) 残差平方和: 1.75 有了成本函数,就要使其最小化获得参数。

解一元线性回归的最小二乘法 通过成本函数最小化获得参数,我们先求相关系数β 。按照频率论的观点,我们首先需要计算x 的方 差和x 与y 的协方差。 方差是用来衡量样本分散程度的。如果样本全部相等,那么方差为0。方差越小,表示样本越集中, 反正则样本越分散。方差计算公式如下:



n

var(x) =

i=1

(xi

n

− x¯ )

2

−1

其中,x¯ 是直径x 的均值,xi 的训练集的第i个直径样本,n 是样本数量。计算如下:

In [73]: # 如果是Python2,加from __future__ import division xbar = (6 + 8 + 10 + 14 + 18) / 5 variance = ((6 - xbar)**2 + (8 - xbar)**2 + (10 - xbar)**2 + (14 - xbar)* *2 + (18 - xbar)**2) / 4 print(variance) 23.2 Numpy里面有var方法可以直接计算方差,ddof参数是贝塞尔(无偏估计)校正系数(Bessel's correction),设置为1,可得样本方差无偏估计量。

In [72]: print(np.var([6, 8, 10, 14, 18], ddof=1)) 23.2 协方差表示两个变量的总体的变化趋势。如果两个变量的变化趋势一致,也就是说如果其中一个大于 自身的期望值,另外一个也大于自身的期望值,那么两个变量之间的协方差就是正值。 如果两个变 量的变化趋势相反,即其中一个大于自身的期望值,另外一个却小于自身的期望值,那么两个变量之 间的协方差就是负值。如果两个变量不相关,则协方差为0,变量线性无关不表示一定没有其他相关 性。协方差公式如下:



n

cov(x, y) =

i=1

− x¯ )(y − y¯) n − 1

(xi

i

其中,x¯ 是直径x 的均值,xi 的训练集的第i个直径样本,y¯ 是价格y 的均值,yi 的训练集的第i个价格样 本,n 是样本数量。计算如下:

In [75]: ybar = (7 + 9 + 13 + 17.5 + 18) / 5 cov = ((6 - xbar) * (7 - ybar) + (8 - xbar) * (9 - ybar) + (10 - xbar) * (13 - ybar) + (14 - xbar) * (17.5 - ybar) + (18 - xbar) * (18 - ybar)) / 4 print(cov) 22.65 Numpy里面有cov方法可以直接计算方差。

In [76]: import numpy as np print(np.cov([6, 8, 10, 14, 18], [7, 9, 13, 17.5, 18])[0][1]) 22.65 现在有了方差和协方差,就可以计算相关系统β 了。

β= β=

cov(x, y) var(x)

22.65 = 0.9762931034482758 23.2

算出β 后,我们就可以计算α 了:

α = y¯ − βx¯ 将前面的数据带入公式就可以求出α 了:

α = 12.9 − 0.9762931034482758 × 11.2 = 1.9655172413793114 这样就通过最小化成本函数求出模型参数了。把匹萨直径带入方程就可以求出对应的价格了,如11英 寸直径价格$12.70,18英寸直径价格$19.54。

模型评估 前面我们用学习算法对训练集进行估计,得出了模型的参数。如何评价模型在现实中的表现呢?现在 让我们假设有另一组数据,作为测试集进行评估。 训练样本 直径(英寸) 价格(美元) 预测值(美元) 1

8

11

9.7759

2

9

8.5

10.7522

3

11

15

12.7048

4

16

18

17.5863

5

12

11

13.6811

有些度量方法可以用来评估预测效果,我们用R方(r-squared)评估匹萨价格预测的效果。R方也叫 确定系数(coefficient of determination),表示模型对现实数据拟合的程度。计算R方的方法有几 种。一元线性回归中R方等于皮尔逊积矩相关系数(Pearson product moment correlation coefficient 或Pearson's r)的平方。 这种方法计算的R方一定介于0~1之间的正数。其他计算方法,包括scikit-learn中的方法,不是用皮 尔逊积矩相关系数的平方计算的,因此当模型拟合效果很差的时候R方会是负值。下面我们用scikitlearn方法来计算R方。

首先,我们需要计算样本总体平方和,y¯ 是价格y 的均值,yi 的训练集的第i个价格样本,n 是样本数 量。 n

SStot =

SStot = (11

− 12.7)



(yi

− y¯)

2

i=1 2

+ (8.5

− 12.7)

2

+



+ (11

− 12.7)

2

= 56.8 然后,我们计算残差平方和,和前面的一样: n

SSres =

SSres = (11

− 9.7759)



(yi

− f (x ))

i=1

2

+ (8.5

− 10.7522)

2

2

i

+



+ (11

− 13.6811)

2

= 19.19821359

最后用下面的公式计算R方: 2

R

2

R

= 1



= 1



SSres SStot

19.19821359 = 0.6620032818661972 56.8

R方是0.6620说明测试集里面过半数的价格都可以通过模型解释。现在,让我们用scikit-learn来验证 一下。LinearRegression的score方法可以计算R方:

In [86]: # 测试集 X_test = [[8], [9], [11], [16], [12]] y_test = [[11], [8.5], [15], [18], [11]] model = LinearRegression() model.fit(X, y) model.score(X_test, y_test) Out[86]: 0.66200528638545175

多元线性回归 可以看出匹萨价格预测的模型R方值并不显著。如何改进呢?

回顾一下自己的生活经验,匹萨的价格其实还会受到其他因素的影响。比如,匹萨的价格还与上面的 辅料有关。让我们再为模型增加一个解释变量。用一元线性回归已经无法解决了,我们可以用更具一 般性的模型来表示,即多元线性回归。 y =

α+β

1

x1 +

β

2

x2 +

⋯ β +

n

xn

写成矩阵形式如下: Y = X

β

一元线性回归可以写成如下形式: ⎡ Y ⎤ 1 ⎢

⎢ Y2 ⎥ ⎢ ⎢





α + βX ⎢ α + βX ⎡



1

⎢ =

2







⎣ Y ⎦ n







⎡ 1







⎢ 1



α + βX

n

=



X1 ⎤ ⎥ X2 ⎥

⋮ ⋮







⎣ 1

⎥ ⎥

×

α [ β]

Xn ⎦

其中,Y 是训练集的响应变量列向量,β 是模型参数列向量。X 称为设计矩阵,是m × n 维训练集的 解释变量矩阵。m 是训练集样本数量,n 是解释变量个数。增加辅料的匹萨价格预测模型训练集如下 表所示: 训练样本 直径(英寸) 辅料种类 价格(美元) 1

8

11

9.7759

2

9

8.5

10.7522

3

11

15

12.7048

4

16

18

17.5863

5

12

11

13.6811

我们同时要升级测试集数据: 测试样本 直径(英寸) 辅料种类 价格(美元) 1

8

2

11

2

9

0

8.5

3

11

2

15

4

16

2

18

5

12

0

11

我们的学习算法评估三个参数的值:两个相关因子和一个截距。β 的求解方法可以通过矩阵运算来实 现。 Y = X

β

矩阵没有除法运算(详见线性代数相关内容),所以用矩阵的转置运算和逆运算来实现:

β = (X 通过Numpy的矩阵操作就可以完成:

T

X)

−1

X

T

Y

In [1]: from numpy.linalg import inv from numpy import dot, transpose X = [[1, 6, 2], [1, 8, 1], [1, 10, 0], [1, 14, 2], [1, 18, 0]] y = [[7], [9], [13], [17.5], [18]] print(dot(inv(dot(transpose(X), X)), dot(transpose(X), y))) [[ 1.1875 ] [ 1.01041667] [ 0.39583333]] Numpy也提供了最小二乘法函数来实现这一过程:

In [4]: from numpy.linalg import lstsq print(lstsq(X, y)[0]) [[ 1.1875 ] [ 1.01041667] [ 0.39583333]] 有了参数,我们就来更新价格预测模型:

In [1]: from sklearn.linear_model import LinearRegression X = [[6, 2], [8, 1], [10, 0], [14, 2], [18, 0]] y = [[7], [9], [13], [17.5], [18]] model = LinearRegression() model.fit(X, y) X_test = [[8, 2], [9, 0], [11, 2], [16, 2], [12, 0]] y_test = [[11], [8.5], [15], [18], [11]] predictions = model.predict(X_test) for i, prediction in enumerate(predictions): print('Predicted: %s, Target: %s' % (prediction, y_test[i])) print('R-squared: %.2f' % model.score(X_test, y_test)) Predicted: [ 10.06250019], Target: [11] Predicted: [ 10.28125019], Target: [8.5] Predicted: [ 13.09375019], Target: [15] Predicted: [ 18.14583353], Target: [18] Predicted: [ 13.31250019], Target: [11] R-squared: 0.77 增加解释变量让模型拟合效果更好了。后面我们会论述一个问题:为什么只用一个测试集评估一个模 型的效果是不准确的,如何通过将测试集数据分块的方法来测试,让模型的测试效果更可靠。不过现 在我们可以认为,匹萨价格预测问题,多元回归确实比一元回归效果更好。假如解释变量和响应变量 的关系不是线性的呢?下面我们来研究一个特别的多元线性回归的情况,可以用来构建非线性关系模 型。

多项式回归

多项式回归 上例中,我们假设解释变量和响应变量的关系是线性的。真实情况未必如此。下面我们用多项式回 归,一种特殊的多元线性回归方法,增加了指数项(x 的次数大于1)。现实世界中的曲线关系都是通 过增加多项式实现的,其实现方式和多元线性回归类似。本例还用一个解释变量,匹萨直径。让我们 用下面的数据对两种模型做个比较: 训练样本 直径(英寸) 价格(美元) 1

6

7

2

8

9

3

10

13

4

14

17.5

5

18

18

测试样本 直径(英寸) 价格(美元) 1

6

7

2

8

9

3

10

13

4

14

17.5

二次回归(Quadratic Regression),即回归方程有个二次项,公式如下: y =

α+β

1

x +

β

2

x

2

我们只用一个解释变量,但是模型有三项,通过第三项(二次项)来实现曲线关 系。PolynomialFeatures转换器可以用来解决这个问题。代码如下:

In [32]: import numpy as np from sklearn.linear_model import LinearRegression from sklearn.preprocessing import PolynomialFeatures X_train = [[6], [8], [10], [14], [18]] y_train = [[7], [9], [13], [17.5], [18]] X_test = [[6], [8], [11], [16]] y_test = [[8], [12], [15], [18]] regressor = LinearRegression() regressor.fit(X_train, y_train) xx = np.linspace(0, 26, 100) yy = regressor.predict(xx.reshape(xx.shape[0], 1)) plt = runplt() plt.plot(X_train, y_train, 'k.') plt.plot(xx, yy) quadratic_featurizer = PolynomialFeatures(degree=2) X_train_quadratic = quadratic_featurizer.fit_transform(X_train) X_test_quadratic = quadratic_featurizer.transform(X_test) regressor_quadratic = LinearRegression()

regressor_quadratic.fit(X_train_quadratic, y_train) xx_quadratic = quadratic_featurizer.transform(xx.reshape(xx.shape[0], 1)) plt.plot(xx, regressor_quadratic.predict(xx_quadratic), 'r-') plt.show() print(X_train) print(X_train_quadratic) print(X_test) print(X_test_quadratic) print('一元线性回归 r-squared', regressor.score(X_test, y_test)) print('二次回归 r-squared', regressor_quadratic.score(X_test_quadratic, y_ test))

[[6], [8], [10], [14], [18]] [[ 1 6 36] [ 1 8 64] [ 1 10 100] [ 1 14 196] [ 1 18 324]] [[6], [8], [11], [16]] [[ 1 6 36] [ 1 8 64] [ 1 11 121] [ 1 16 256]] 一元线性回归 r-squared 0.809726832467 二次回归 r-squared 0.867544458591 效果如上图所示,直线为一元线性回归(R方0.81),曲线为二次回归(R方0.87),其拟合效果更 佳。还有三次回归,就是再增加一个立方项(β 3 x 3 )。同样方法拟合,效果如下图所示:

In [33]: plt = runplt() plt.plot(X_train, y_train, 'k.') quadratic_featurizer = PolynomialFeatures(degree=2) X_train_quadratic = quadratic_featurizer.fit_transform(X_train) X_test_quadratic = quadratic_featurizer.transform(X_test) regressor_quadratic = LinearRegression() regressor_quadratic.fit(X_train_quadratic, y_train) xx_quadratic = quadratic_featurizer.transform(xx.reshape(xx.shape[0], 1)) plt.plot(xx, regressor_quadratic.predict(xx_quadratic), 'r-') cubic_featurizer = PolynomialFeatures(degree=3) X_train_cubic = cubic_featurizer.fit_transform(X_train) X_test_cubic = cubic_featurizer.transform(X_test) regressor_cubic = LinearRegression() regressor_cubic.fit(X_train_cubic, y_train) xx_cubic = cubic_featurizer.transform(xx.reshape(xx.shape[0], 1)) plt.plot(xx, regressor_cubic.predict(xx_cubic)) plt.show() print(X_train_cubic) print(X_test_cubic) print('二次回归 r-squared', regressor_quadratic.score(X_test_quadratic, y_ test)) print('三次回归 r-squared', regressor_cubic.score(X_test_cubic, y_test))

[[ 1 6 36 216] [ 1 8 64 512] [ 1 10 100 1000] [ 1 14 196 2744] [ 1 18 324 5832]] [[ 1 6 36 216] [ 1 8 64 512] [ 1 11 121 1331] [ 1 16 256 4096]] 二次回归 r-squared 0.867544458591 三次回归 r-squared 0.835692454062 从图中可以看出,三次回归经过的点更多,但是R方值却没有二次回归高。下面我们再看一个更高阶

从图中可以看出,三次回归经过的点更多,但是R方值却没有二次回归高。下面我们再看一个更高阶 的,七次回归,效果如下图所示:

In [34]: plt = runplt() plt.plot(X_train, y_train, 'k.') quadratic_featurizer = PolynomialFeatures(degree=2) X_train_quadratic = quadratic_featurizer.fit_transform(X_train) X_test_quadratic = quadratic_featurizer.transform(X_test) regressor_quadratic = LinearRegression() regressor_quadratic.fit(X_train_quadratic, y_train) xx_quadratic = quadratic_featurizer.transform(xx.reshape(xx.shape[0], 1)) plt.plot(xx, regressor_quadratic.predict(xx_quadratic), 'r-') seventh_featurizer = PolynomialFeatures(degree=7) X_train_seventh = seventh_featurizer.fit_transform(X_train) X_test_seventh = seventh_featurizer.transform(X_test) regressor_seventh = LinearRegression() regressor_seventh.fit(X_train_seventh, y_train) xx_seventh = seventh_featurizer.transform(xx.reshape(xx.shape[0], 1)) plt.plot(xx, regressor_seventh.predict(xx_seventh)) plt.show() print('二次回归 r-squared', regressor_quadratic.score(X_test_quadratic, y_ test)) print('七次回归 r-squared', regressor_seventh.score(X_test_seventh, y_test ))

二次回归 r-squared 0.867544458591 七次回归 r-squared 0.487942421984 可以看出,七次拟合的R方值更低,虽然其图形基本经过了所有的点。可以认为这是拟合过度(overfitting)的情况。这种模型并没有从输入和输出中推导出一般的规律,而是记忆训练集的结果,这样 在测试集的测试效果就不好了。

正则化

正则化 正则化(Regularization)是用来防止拟合过度的一堆方法。正则化向模型中增加信息,经常是一种 对抗复杂性的手段。与奥卡姆剃刀原理(Occam's razor)所说的具有最少假设的论点是最好的观点 类似。正则化就是用最简单的模型解释数据。 scikit-learn提供了一些方法来使线性回归模型正则化。其中之一是岭回归(Ridge Regression,RR, 也叫Tikhonov regularization),通过放弃最小二乘法的无偏性,以损失部分信息、降低精度为代价获 得回归系数更为符合实际、更可靠的回归方法。岭回归增加L2范数项(相关系数向量平方和的平方 根)来调整成本函数(残差平方和): p

n

RSSridge =



(yi

− x β) T

2

i

+

i=1

λ∑β

2 j

j=1

λ 是调整成本函数的超参数(hyperparameter),不能自动处理,需要手动调整一种参数。λ 增大, 成本函数就变大。 scikit-learn也提供了最小收缩和选择算子(Least absolute shrinkage and selection operator, LASSO),增加L1范数项(相关系数向量平方和的平方根)来调整成本函数(残差平方和): p

n

RSSlasso =

∑ i=1

(yi

− x β) T

i

2

+

λ∑β

j

j=1

LASSO方法会产生稀疏参数,大多数相关系数会变成0,模型只会保留一小部分特征。而岭回归还是 会保留大多数尽可能小的相关系数。当两个变量相关时,LASSO方法会让其中一个变量的相关系数 会变成0,而岭回归是将两个系数同时缩小。 scikit-learn还提供了弹性网(elastic net)正则化方法,通过线性组合L1和L2兼具LASSO和岭回归的 内容。可以认为这两种方法是弹性网正则化的特例。

线性回归应用案例 前面我们通过一个小例子介绍了线性回归模型。下面我们用一个现实的数据集来应用线性回归算法。 假如你去参加聚会,想喝最好的红酒,可以让朋友推荐,不过你觉得他们也不靠谱。作为科学控,你 带了pH试纸和一堆测量工具来测酒的理化性质,然后选一个最好的,周围的小伙伴都无语了,你亮 瞎了世界。 网上有相关的酒数据集可以参考,UCI机器学习项目的酒数据集收集了1599种酒的测试数据。收集完 数据自然要用线性回归来研究一下,响应变量是0-10的整数值,我们也可以把这个问题看成是一个分 类问题。不过本章还是把相应变量作为连续值来处理。

探索数据 scikit-learn作为机器学习系统,其探索数据的能力是不能与SPSS和R语言相媲美的。不过我们有 Pandas库,可以方便的读取数据,完成描述性统计工作。我们通过描述性统计来设计模型。Pandas 引入了R语言的dataframe,一种二维表格式异质(heterogeneous)数据结构。Pandas更多功能请 见文档 (http://pandas.pydata.org),这里只用一部分功能,都很容易使用。

首先,我们读取.csv文件生成dataframe:

In [5]: import pandas as pd df = pd.read_csv('mlslpic/winequality-red.csv', sep=';') df.head() Out[5]: free total fixed volatile citric residual chlorides sulfur sulfur density pH acidity acidity acid sugar dioxide dioxide

sulphates

0 7.4

0.70

0.00

1.9

0.076

11

34

0.9978

3.51 0.56

1 7.8

0.88

0.00

2.6

0.098

25

67

0.9968

3.20 0.68

2 7.8

0.76

0.04

2.3

0.092

15

54

0.9970

3.26 0.65

3 11.2

0.28

0.56

1.9

0.075

17

60

0.9980

3.16 0.58

4 7.4

0.70

0.00

1.9

0.076

11

34

0.9978

3.51 0.56

In [6]: df.describe() Out[6]: fixed acidity

volatile acidity

citric acid

residual sugar

chlorides

free sulfur dioxide

count 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 1599.000000 mean 8.319637

0.527821

0.270976

2.538806

0.087467

15.874922

std

1.741096

0.179060

0.194801

1.409928

0.047065

10.460157

min

4.600000

0.120000

0.000000

0.900000

0.012000

1.000000

25%

7.100000

0.390000

0.090000

1.900000

0.070000

7.000000

50%

7.900000

0.520000

0.260000

2.200000

0.079000

14.000000

75%

9.200000

0.640000

0.420000

2.600000

0.090000

21.000000

max

15.900000

1.580000

1.000000

15.500000

0.611000

72.000000

就这么简单,我们通过Dataframe.describe()方法获得了一堆描述性统计结 果。pd.read_csv()读取.csv文件。下面我们在用matplotlib看看,获得更直观的认识:

In [11]: plt.scatter(df['alcohol'], df['quality']) plt.xlabel('Alcohol') plt.ylabel('Quality') plt.title('酒精度(Alcohol)与品质( Quality)',fontproperties=font) plt.show()

散点图显示酒精度(Alcohol)与品质有比较弱的正相关特性,整体呈左下-右上趋势,也就是说酒精 度较高的酒具有较高的品质。挥发性酸度(volatile acidity)与品质呈现负相关特性:

In [12]: plt.scatter(df['volatile acidity'], df['quality']) plt.xlabel('Volatile Acidity') plt.ylabel('Quality') plt.title('挥发性酸度(volatile acidity)与品质( Quality)',fontproperties=f ont) plt.show()

这些图都可以显示出响应变量与解释变量的相关性;让我们构建一个多元线性相关模型表述这些相关

这些图都可以显示出响应变量与解释变量的相关性;让我们构建一个多元线性相关模型表述这些相关 性。如何决定哪个变量应该在模型中?哪个可以不在?通过Dataframe.corr()计算两两的关联矩 阵(correlation matrix)。关联矩阵进一步确认了酒精度与品质的正相关性,挥发性酸度与品质的负 相关性。挥发性酸度越高,酒喝着就越向醋。总之,我们就假设好酒应该具有酒精度高、挥发性酸度 低的特点,虽然这和品酒师的味觉可能不太一致。

拟合与评估模型 现在,我们把数据分成训练集和测试集,训练回归模型然后评估预测结果:

In [39]: import pandas as pd import matplotlib.pylab as plt from sklearn.linear_model import LinearRegression from sklearn.cross_validation import train_test_split df = pd.read_csv('mlslpic/winequality-red.csv', sep=';') X = df[list(df.columns)[:-1]] y = df['quality'] X_train, X_test, y_train, y_test = train_test_split(X, y) regressor = LinearRegression() regressor.fit(X_train, y_train) y_predictions = regressor.predict(X_test) print('R-squared:', regressor.score(X_test, y_test)) R-squared: 0.382777530798 开始和前面类似,加载数据,然后通过train_test_split把数据集分成训练集和测试集。两个分 区的数据比例都可以通过参数设置。默认情况下,25%的数据被分配给测试集。最后,我们训练模型 并用测试集测试。 R方值0.38表明38%的测试集数据都通过了测试。如果剩下的72%的数据被分到训练集,那效果就不 一样了。我们可以用交叉检验的方法来实现一个更完善的效果评估。上一章我们介绍过这类方法,可 以用来减少不同训练和测试数据集带来的变化。

In [33]: from sklearn.cross_validation import cross_val_score regressor = LinearRegression() scores = cross_val_score(regressor, X, y, cv=5) print(scores.mean(), scores) 0.290041628842 [ 0.13200871 0.31858135 0.34955348 0.369145 0.2809196 ] 这里cross_val_score函数可以帮助我们轻松实现交叉检验功能。cv参数将数据集分成了5份。每 个分区都会轮流作为测试集使用。cross_val_score函数返回模拟器score方法的结果。R方结果 是在0.13到0.37之间,均值0.29,是模拟器模拟出的结果,相比单个训练/测试集的效果要好。 让我们看看一些模型的预测品质与实际品质的图象显示结果:

In [46]: plt.scatter(y_test, y_predictions) plt.xlabel('实际品质',fontproperties=font) plt.ylabel('预测品质',fontproperties=font) plt.title('预测品质与实际品质',fontproperties=font) plt.show()

和假设一致,预测品质很少和实际品质完全一致。由于绝大多数训练数据都是一般品质的酒,所以这 个模型更适合预测一般质量的酒。

梯度下降法拟合模型 前面的内容都是通过最小化成本函数来计算参数的:

β = (X

T

X)

−1

X

T

Y

这里X 是解释变量矩阵,当变量很多(上万个)的时候,X T X 计算量会非常大。另外,如果X T X 的 行列式为0,即奇异矩阵,那么就无法求逆矩阵了。这里我们介绍另一种参数估计的方法,梯度下降 法(gradient descent)。拟合的目标并没有变,我们还是用成本函数最小化来进行参数估计。 梯度下降法被比喻成一种方法,一个人蒙着眼睛去找从山坡到溪谷最深处的路。他看不到地形图,所 以只能沿着最陡峭的方向一步一步往前走。每一步的大小与地势陡峭的程度成正比。如果地势很陡 峭,他就走一大步,因为他相信他仍在高出,还没有错过溪谷的最低点。如果地势比较平坦,他就走 一小步。这时如果再走大步,可能会与最低点失之交臂。如果真那样,他就需要改变方向,重新朝着 溪谷的最低点前进。他就这样一步一步的走啊走,直到有一个点走不动了,因为路是平的了,于是他 卸下眼罩,已经到了谷底深处,小龙女在等他。 通常,梯度下降算法是用来评估函数的局部最小值的。我们前面用的成本函数如下: n

SSres =

∑ i=1

(yi

− f (x )) i

2

可以用梯度下降法来找出成本函数最小的模型参数值。梯度下降法会在每一步走完后,计算对应位置 的导数,然后沿着梯度(变化最快的方向)相反的方向前进。总是垂直于等高线。 需要注意的是,梯度下降法来找出成本函数的局部最小值。一个三维凸(convex)函数所有点构成 的图行像一个碗。碗底就是唯一局部最小值。非凸函数可能有若干个局部最小值,也就是说整个图形 看着像是有多个波峰和波谷。梯度下降法只能保证找到的是局部最小值,并非全局最小值。残差平方 和构成的成本函数是凸函数,所以梯度下降法可以找到全局最小值。 梯度下降法的一个重要超参数是步长(learning rate),用来控制蒙眼人步子的大小,就是下降幅 度。如果步长足够小,那么成本函数每次迭代都会缩小,直到梯度下降法找到了最优参数为止。但 是,步长缩小的过程中,计算的时间就会不断增加。如果步长太大,这个人可能会重复越过谷底,也 就是梯度下降法可能在最优值附近摇摆不定。 如果按照每次迭代后用于更新模型参数的训练样本数量划分,有两种梯度下降法。批量梯度下降 (Batch gradient descent)每次迭代都用所有训练样本。随机梯度下降(Stochastic gradient descent,SGD)每次迭代都用一个训练样本,这个训练样本是随机选择的。当训练样本较多的时 候,随机梯度下降法比批量梯度下降法更快找到最优参数。批量梯度下降法一个训练集只能产生一个 结果。而SGD每次运行都会产生不同的结果。SGD也可能找不到最小值,因为升级权重的时候只用 一个训练样本。它的近似值通常足够接近最小值,尤其是处理残差平方和这类凸函数的时候。 下面我们用scikit-learn的SGDRegressor类来计算模型参数。它可以通过优化不同的成本函数来拟合 线性模型,默认成本函数为残差平方和。本例中,我们用波士顿住房数据的13个解释变量来预测房屋 价格:

In [3]: import numpy as np from sklearn.datasets import load_boston from sklearn.linear_model import SGDRegressor from sklearn.cross_validation import cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.cross_validation import train_test_split data = load_boston() X_train, X_test, y_train, y_test = train_test_split(data.data, data.targe t) scikit-learn加载数据集的方法很简单。首先我们用train_test_split分割训练集和测试集。

In [4]: X_scaler = StandardScaler() y_scaler = StandardScaler() X_train = X_scaler.fit_transform(X_train) y_train = y_scaler.fit_transform(y_train) X_test = X_scaler.transform(X_test) y_test = y_scaler.transform(y_test) 然后用StandardScaler做归一化处理,后面会介绍。最后我们用交叉验证方法完成训练和测试:

In [11]: regressor = SGDRegressor(loss='squared_loss') scores = cross_val_score(regressor, X_train, y_train, cv=5) print('交叉验证R方值:', scores) print('交叉验证R方均值:', np.mean(scores)) regressor.fit_transform(X_train, y_train) print('测试集R方值:', regressor.score(X_test, y_test)) 交叉验证R方值: [ 0.64102297 0.65659839 0.80237287 0.67294193 0.57322387] 交叉验证R方均值: 0.669232006274 测试集R方值: 0.787333341357

总结 本章我们介绍了三类线性回归模型。首先,通过匹萨价格预测的例子介绍了一元线性回归,一个解释 变量和一个响应变量的线性拟合。然后,我们讨论了多元线性回归,具有更一般形式的若干解释变量 和一个响应变量的问题。最后,我们讨论了多项式回归,一种特殊的多元线性模型,体系了解释变量 和响应变量的非线性特征。这三种模型都是一般线性模型的具体形式,在第4章,从线性回归到逻辑 回归(Logistic Regression)。 我们将残差平方差最小化为目标来估计模型参数。首先,通过解析方法求解,我们介绍了梯度下降 法,一种可以有效估计带许多解释变量的模型参数的优化方法。这章里的案例都很简单,很容易建 模。下一章,我们介绍处理不同类型的解释变量的方法,包括分类数据、文字、图像。

特征提取与处理 上一章案例中的解释变量都是数值,比如匹萨的直接。而很多机器学习问题需要研究的对象可能是分 类变量、文字甚至图像。本章,我们介绍提取这些变量特征的方法。这些技术是数据处理的前提—— 序列化,更是机器学习的基础,影响到本书的所有章节。

分类变量特征提取 许多机器学习问题都有分类的、标记的变量,不是连续的。例如,一个应用是用分类特征比如工作地 点来预测工资水平。分类变量通常用独热编码(One-of-K or One-Hot Encoding),通过二进制数来 表示每个解释变量的特征。 例如,假设city变量有三个值:New York, San Francisco, Chapel Hill。独热编码方式就是 用三位二进制数,每一位表示一个城市。 scikit-learn里有DictVectorizer类可以用来表示分类特征:

In [1]: from sklearn.feature_extraction import DictVectorizer onehot_encoder = DictVectorizer() instances = [{'city': 'New York'},{'city': 'San Francisco'}, {'city': 'Ch apel Hill'}] print(onehot_encoder.fit_transform(instances).toarray()) [[ 0. 1. 0.] [ 0. 0. 1.] [ 1. 0. 0.]] 会看到,编码的位置并不是与上面城市一一对应的。第一个city编码New York是[ 0. 1. 0.], 用第二个元素为1表示。相比用单独的数值来表示分类,这种方法看起来很直观。New York, San Francisco, Chapel Hill可以表示成1,2,3。数值的大小没有实际意义,城市并没有自然数顺 序。

文字特征提取 很多机器学习问题涉及自然语言处理(NLP),必然要处理文字信息。文字必须转换成可以量化的特 征向量。下面我们就来介绍最常用的文字表示方法:词库模型(Bag-of-words model)。

词库表示法

词库模型是文字模型化的最常用方法。对于一个文档(document),忽略其词序和语法,句法,将 其仅仅看做是一个词集合,或者说是词的一个组合,文档中每个词的出现都是独立的,不依赖于其他 词是否出现,或者说当这篇文章的作者在任意一个位置选择一个词汇都不受前面句子的影响而独立选 择的。词库模型可以看成是独热编码的一种扩展,它为每个单词设值一个特征值。词库模型依据是用 类似单词的文章意思也差不多。词库模型可以通过有限的编码信息实现有效的文档分类和检索。 一批文档的集合称为文集(corpus)。让我们用一个由两个文档组成的文集来演示词库模型:

In [2]: corpus = [ 'UNC played Duke in basketball', 'Duke lost the basketball game' ] 文集包括8个词:UNC, played, Duke, in, basketball, lost, the, game。文件的单词构成词汇表 (vocabulary)。词库模型用文集的词汇表中每个单词的特征向量表示每个文档。我们的文集有8个 单词,那么每个文档就是由一个包含8位元素的向量构成。构成特征向量的元素数量称为维度 (dimension)。用一个词典(dictionary)来表示词汇表与特征向量索引的对应关系。 在大多数词库模型中,特征向量的每一个元素是用二进制数表示单词是否在文档中。例如,第一个文 档的第一个词是UNC,词汇表的第一个单词是UNC,因此特征向量的第一个元素就是1。词汇表的最 后一个单词是game。第一个文档没有这个词,那么特征向量的最后一个元素就 是0。CountVectorizer类会把文档全部转换成小写,然后将文档词块化(tokenize)。文档词块 化是把句子分割成词块(token)或有意义的字母序列的过程。词块大多是单词,但是他们也可能是 一些短语,如标点符号和词缀。CountVectorizer类通过正则表达式用空格分割句子,然后抽取长 度大于等于2的字母序列。scikit-learn实现代码如下:

In [3]: from sklearn.feature_extraction.text import CountVectorizer corpus = [ 'UNC played Duke in basketball', 'Duke lost the basketball game' ] vectorizer = CountVectorizer() print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[1 1 0 1 0 1 0 1] [1 1 1 0 1 0 1 0]] {'unc': 7, 'played': 5, 'game': 2, 'in': 3, 'basketball': 0, 't he': 6, 'duke': 1, 'lost': 4} 让我们再增加一个文档到文集里:

In [6]: corpus = [ 'UNC played Duke in basketball', 'Duke lost the basketball game', 'I ate a sandwich' ] vectorizer = CountVectorizer() print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[0 1 1 0 1 0 1 0 0 1] [0 1 1 1 0 1 0 0 1 0] [1 0 0 0 0 0 0 1 0 0]] {'unc': 9, 'played': 6, 'game': 3, 'in': 4, 'ate': 0, 'basketba ll': 1, 'the': 8, 'sandwich': 7, 'duke': 2, 'lost': 5} 通过CountVectorizer类可以得出上面的结果。词汇表里面有10个单词,但a不在词汇表里面,是 因为a的长度不符合CountVectorizer类的要求。 对比文档的特征向量,会发现前两个文档相比第三个文档更相似。如果用欧氏距离(Euclidean distance)计算它们的特征向量会比其与第三个文档距离更接近。两向量的欧氏距离就是两个向量欧 氏范数(Euclidean norm)或L2范数差的绝对值: d =

∥x −x ∥ 0

1

向量的欧氏范数是其元素平方和的平方根:

∥x∥

=



‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ⋯ ‾ x

2

1

+ x

2

2

+

2

+ xn

scikit-learn里面的euclidean_distances函数可以计算若干向量的距离,表示两个语义最相似的 文档其向量在空间中也是最接近的。

In [15]: from sklearn.metrics.pairwise import euclidean_distances counts = vectorizer.fit_transform(corpus).todense() for x,y in [[0,1],[0,2],[1,2]]: dist = euclidean_distances(counts[x],counts[y]) print('文档{}与文档{}的距离{}'.format(x,y,dist)) 文档0与文档1的距离[[ 2.44948974]] 文档0与文档2的距离[[ 2.64575131]] 文档1与文档2的距离[[ 2.64575131]] 如果我们用新闻报道内容做文集,词汇表就可以用成千上万个单词。每篇新闻的特征向量都会有成千 上万个元素,很多元素都会是0。体育新闻不会包含财经新闻的术语,同样文化新闻也不会包含财经 新闻的术语。有许多零元素的高维特征向量成为稀疏向量(sparse vectors)。 用高维数据可以量化机器学习任务时会有一些问题,不只是出现在自然语言处理领域。第一个问题就 是高维向量需要占用更大内存。NumPy提供了一些数据类型只显示稀疏向量的非零元素,可以有效 处理这个问题。

第二个问题就是著名的维度灾难(curse of dimensionality,Hughes effect),维度越多就要求更大 的训练集数据保证模型能够充分学习。如果训练样本不够,那么算法就可以拟合过度导致归纳失败。 下面,我们介绍一些降维的方法。在第7章,PCA降维里面,我们还会介绍用数值方法降维。

停用词过滤 特征向量降维的一个基本方法是单词全部转换成小写。这是因为单词的大小写一般不会影响意思。而 首字母大写的单词一般只是在句子的开头,而词库模型并不在乎单词的位置和语法。 另一种方法是去掉文集常用词。这里词称为停用词(Stop-word),像a,an,the,助动词 do,be,will,介词on,around,beneath等。停用词通常是构建文档意思的功能词汇,其字面 意义并不体现。CountVectorizer类可以通过设置stop_words参数过滤停用词,默认是英语常用 的停用词。

In [16]: corpus = [ 'UNC played Duke in basketball', 'Duke lost the basketball game', 'I ate a sandwich' ] vectorizer = CountVectorizer(stop_words='english') print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[0 1 1 0 0 1 0 1] [0 1 1 1 1 0 0 0] [1 0 0 0 0 0 1 0]] {'unc': 7, 'played': 5, 'game': 3, 'ate': 0, 'basketball': 1, ' sandwich': 6, 'duke': 2, 'lost': 4} 这样停用词就没有了,前两篇文档依然相比其与第三篇的内容更接近。

词根还原与词形还原 停用词去掉之后,可能还会剩下许多词,还有一种常用的方法就是词根还原(stemming )与词形还 原(lemmatization)。 特征向量里面的单词很多都是一个词的不同形式,比如jumping和jumps都是jump的不同形式。词 根还原与词形还原就是为了将单词从不同的时态、派生形式还原。

In [17]: from sklearn.feature_extraction.text import CountVectorizer corpus = [ 'He ate the sandwiches', 'Every sandwich was eaten by him' ] vectorizer = CountVectorizer(binary=True, stop_words='english') print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[1 0 0 1] [0 1 1 0]] {'sandwich': 2, 'sandwiches': 3, 'ate': 0, 'eaten': 1} 这两个文档意思差不多,但是其特征向量完全不同,因为单词的形式不同。两个单词都是有一个动词 eat和一个sandwich,这些特征应该在向量中反映出来。词形还原就是用来处理可以表现单词意思 的词元(lemma)或形态学的词根(morphological root)的过程。词元是单词在词典中查询该词的 基本形式。词根还原与词形还原类似,但它不是生成单词的形态学的词根。而是把附加的词缀都去 掉,构成一个词块,可能不是一个正常的单词。词形还原通常需要词法资料的支持,比如WordNet和 单词词类(part of speech)。词根还原算法通常需要用规则产生词干(stem)并操作词块,不需要 词法资源,也不在乎单词的意思。 让我们分析一下单词gathering的词形还原:

In [1]: corpus = [ 'I am gathering ingredients for the sandwich.', 'There were many wizards at the gathering.' ] 第一句的gathering是动词,其词元是gather。后一句的gathering是名词,其词元 是gathering。我们用Python的NLTK(Natural Language Tool Kit) (http://www.nltk.org/install.html)库来处理。

In [8]: import nltk nltk.download() showing info http://www.nltk.org/nltk_data/ Out[8]: True NLTK的WordNetLemmatizer可以用gathering的词类确定词元。

In [3]: from nltk.stem.wordnet import WordNetLemmatizer lemmatizer = WordNetLemmatizer() print(lemmatizer.lemmatize('gathering', 'v')) print(lemmatizer.lemmatize('gathering', 'n')) gather gathering 我们把词形还原应用到之前的例子里:

In [6]: from nltk import word_tokenize from nltk.stem import PorterStemmer from nltk.stem.wordnet import WordNetLemmatizer from nltk import pos_tag wordnet_tags = ['n', 'v'] corpus = [ 'He ate the sandwiches', 'Every sandwich was eaten by him' ] stemmer = PorterStemmer() print('Stemmed:', [[stemmer.stem(token) for token in word_tokenize(docume nt)] for document in corpus]) Stemmed: [['He', 'ate', 'the', 'sandwich'], ['Everi', 'sandwich ', 'wa', 'eaten', 'by', 'him']] In [9]: def lemmatize(token, tag): if tag[0].lower() in ['n', 'v']: return lemmatizer.lemmatize(token, tag[0].lower()) return token lemmatizer = WordNetLemmatizer() tagged_corpus = [pos_tag(word_tokenize(document)) for document in corpus] print('Lemmatized:', [[lemmatize(token, tag) for token, tag in document] for document in tagged_corpus]) Lemmatized: [['He', 'eat', 'the', 'sandwich'], ['Every', 'sandw ich', 'be', 'eat', 'by', 'him']] 通过词根还原与词形还原可以有效降维,去掉不必要的单词形式,特征向量可以更有效的表示文档的 意思。

带TF-IDF权重的扩展词库 前面我们用词库模型构建了判断单词是个在文档中出现的特征向量。这些特征向量与单词的语法,顺 序,频率无关。不过直觉告诉我们文档中单词的频率对文档的意思有重要作用。一个文档中某个词多

序,频率无关。不过直觉告诉我们文档中单词的频率对文档的意思有重要作用。一个文档中某个词多 次出现,相比只出现过一次的单词更能体现反映文档的意思。现在我们就将单词频率加入特征向量, 然后介绍由词频引出的两个问题。 我们用一个整数来代码单词的频率。代码如下:

In [16]: from sklearn.feature_extraction.text import CountVectorizer corpus = ['The dog ate a sandwich, the wizard transfigured a sandwich, an d I ate a sandwich'] vectorizer = CountVectorizer(stop_words='english') print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[2 1 3 1 1]] {'wizard': 4, 'transfigured': 3, 'sandwich': 2, 'dog': 1, 'ate' : 0} 结果中第一行是单词的频率,dog频率为1,sandwich频率为3。注意和前面不同的 是,binary=True没有了,因为binary默认是False,这样返回的是词汇表的词频,不是二进制 结果[1 1 1 1 1]。这种单词频率构成的特征向量为文档的意思提供了更多的信息,但是在对比不 同的文档时,需要考虑文档的长度。 很多单词可能在两个文档的频率一样,但是两个文档的长度差别很大,一个文档比另一个文档长很多 倍。scikit-learn的TfdfTransformer类可以解决这个问题,通过对词频(term frequency)特征向 量归一化来实现不同文档向量的可比性。默认情况下,TfdfTransformer类用L2范数对特征向量归 一化: f (t, d) + 1 tf (t, d) =

∥x∥

是第d 个文档(document)第t个单词(term)的频率,∥ x ∥ 是频率向量的L2范数。另外,还 有对数词频调整方法(logarithmically scaled term frequencies),把词频调整到一个更小的范围, 或者词频放大法(augmented term frequencies),适用于消除较长文档的差异。对数词频公式如 下: f (t, d)

tf (t, d) = log(f (t, d) + 1)

TfdfTransformer类计算对数词频调整时,需要将参数sublinear_tf设置为True。词频放大公 式如下: f (t, d) + 1 tf (t, d) = 0.5 + maxf (w, d) : w

maxf (w, d) : w

∈d

∈ d 是文档d 中的最大词频。scikit-learn没有现成可用的词频放大公式,不过通过

CountVectorizer可以轻松实现。 归一化,对数调整词频和词频放大三支方法都消除文档不同大小对词频的影响。但是,另一个问题仍 然存在,那就是特征向量里高频词的权重更大,即使这些词在文集内其他文档里面也经常出现。这些 单词并没有突出代表单个文档的意思。比如,一个文集里大多数文档都是关于杜克大学篮球队的,那 么高频词就是basketball,Coach K,flop。这些词可以被看成是该文集的停用词,因为它们太 普遍对区分文档的意思没任何作用。逆向文件频率(inverse document frequency,IDF)就是用来 度量文集中单词频率的。 N

N idf (t, D) = log( 1 + |d

其中,N 是文集中文档数量,d 与逆向文件频率的乘积。

∈ D : t ∈ d|

)

∈ D : t ∈ d 表示包含单词t的文档数量。单词的TF-IDF值就是其频率

TfdfTransformer类默认返回TF-IDF值,其参数use_idf默认为True。由于TF-IDF加权特征向量 经常用来表示文本,所以scikit-learn提供了TfidfVectorizer类将CountVectorizer和 TfdfTransformer类封装在一起。

In [20]: from sklearn.feature_extraction.text import TfidfVectorizer corpus = [ 'The dog ate a sandwich and I ate a sandwich', 'The wizard transfigured a sandwich' ] vectorizer = TfidfVectorizer(stop_words='english') print(vectorizer.fit_transform(corpus).todense()) print(vectorizer.vocabulary_) [[ 0.75458397 0.37729199 0.53689271 0. 0. ] [ 0. 0. 0.44943642 0.6316672 0.6316672 ]] {'wizard': 4, 'transfigured': 3, 'sandwich': 2, 'dog': 1, 'ate' : 0} 通过TF-IDF加权之后,我们会发现在文集中较常见的词,如sandwich被调整了。

通过哈希技巧实现特征向量 前面我们是用包含文集所有词块的词典来完成文档词块与特征向量的映射的。这么做有两个缺点。首 先是文集需要被调用两次。第一次是创建词典,第二次是创建文档的特征向量。另外,词典必须储存 在内存里,如果文集特别大就会很耗内存。通过哈希表可以有效的解决这些问题。可以将词块用哈希 函数来确定它在特征向量的索引位置,可以不创建词典,这称为哈希技巧(hashing trick)。scikitlearn提供了HashingVectorizer来实现这个技巧:

In [27]: from sklearn.feature_extraction.text import HashingVectorizer corpus = ['the', 'ate', 'bacon', 'cat'] vectorizer = HashingVectorizer(n_features=6) print(vectorizer.transform(corpus).todense()) [[-1. [ 0. [ 0. [ 0.

0. 0. 0. 1.

0. 0. 0. 0.

0. 0. 1. 0. 0. -1. 0. 0.

0.] 0.] 0.] 0.]]

哈希技巧是无固定状态的(stateless),,它把任意的数据块映射到固定数目的位置,并且保证相同 的输入一定产生相同的输出,不同的输入尽可能产生不同的输出。它可以用并行,线上,流式传输创 20

建特征向量,因为它初始化是不需要文集输入。n_features是一个可选参数,默认值是2

,这里

设置成6是为了演示。另外,注意有些单词频率是负数。由于Hash碰撞可能发生,所以 HashingVectorizer用有符号哈希函数(signed hash function)。特征值和它的词块的哈希值带 同样符号,如果cats出现过两次,被哈希成-3,文档特征向量的第四个元素要减去2。如果dogs出 现过两次,被哈希成3,文档特征向量的第四个元素要加上2。 用带符号哈希函数可以把词块发生哈希碰撞的概率相互抵消掉,信息损失比信息损失的同时出现信息 冗余要好。哈希技巧的一个不足是模型的结果更难察看,由于哈希函数不能显示哪个词块映射到特征 向量的哪个位置了。

图片特征提取 计算机视觉是一门研究如何使机器“看”的科学,让计算机学会处理和理解图像。这门学问有时需要借 助机器学习。本章介绍一些机器学习在计算机视觉领域应用的基础技术。

通过像素值提取特征 数字图像通常是一张光栅图或像素图,将颜色映射到网格坐标里。一张图片可以看成是一个每个元素 都是颜色值的矩阵。表示图像基本特征就是将矩阵每行连起来变成一个行向量。光学文字识别 (Optical character recognition,OCR)是机器学习的经典问题。下面我们用这个技术来识别手写数 字。 scikit-learn的digits数字集包括至少1700种0-9的手写数字图像。每个图像都有8x8像像素构成。每 个像素的值是0-16,白色是0,黑色是16。如下图所示:

In [30]: %matplotlib inline from sklearn import datasets import matplotlib.pyplot as plt digits = datasets.load_digits() print('Digit:', digits.target[0]) print(digits.images[0]) plt.figure() plt.axis('off') plt.imshow(digits.images[0], cmap=plt.cm.gray_r, interpolation='nearest') plt.show() Digit: 0 [[ 0. 0. [ 0. 0. [ 0. 3. [ 0. 4. [ 0. 5. [ 0. 4. [ 0. 2. [ 0. 0.

5. 13. 9. 13. 15. 10. 15. 2. 0. 12. 0. 0. 8. 0. 0. 11. 0. 1. 14. 5. 10. 6. 13. 10.

1. 15. 11. 8. 9. 12. 12. 0.

0. 5. 8. 8. 8. 7. 0. 0.

0.] 0.] 0.] 0.] 0.] 0.] 0.] 0.]]

我们将8x8矩阵转换成64维向量来创建一个特征向量:

In [32]: digits = datasets.load_digits() print('Feature vector:\n', digits.images[0].reshape(-1, 64)) Feature vector: [[ 0. 0. 5. 13. 9. 10. 15. 5. 0. 0. 3. 15. 12. 0. 0. 8. 8. 0. 0. 0. 4. 11. 0. 1. 12. 7. 0. 0. 0. 0. 6. 13. 10.

1.

0.

0.

0.

0. 13. 15.

2.

0. 11.

8.

0.

0.

4.

5.

8.

0.

0.

9.

8.

0.

0.

0.

2. 14.

0.

0.

0.]]

5. 10. 12.

这样表示可以有效的处理一些基本任务,比如识别手写字母等。但是,记录每个像素的数值在大图像 处理时不太好用。一个100x100像素的图像其灰度图产生的特征向量是10000维度,而1920x1080像 素的图像是2073600。和TF-IDF特征向量不同,大部分图像都不是稀疏的。这种表示法的缺点不只是 特征向量的维度灾难,还有就是某个位置的学习结果在经过对图像的放缩,旋转或变换之后可能就不 对了,非常敏感,缺乏稳定性。另外,这种方法对图像的亮度也十分敏感。所以这种方法在处理照片 和其他自然景色图像时不怎么有用。现代计算机视觉应用通常手工实现特征提取,或者用深度学习自 动化解决无监督问题。后面我们会详细介绍。

对感兴趣的点进行特征提取 前面创建的特征矢量包含了图像的每个像素,既包含了图像特征的有用信息,也包含了一堆噪声。查 看图像后,我们会发现所有的图像都有一个白边,这些像素是没用的。人们不需要观察物体的每个属 性就可以很快的识别出很多物体。我们可以根据轮廓识别出汽车,并不需要观察后视镜,我们也可以 通过一个鼻子或嘴巴判断图像是一个人。这些直觉就可以用来建立一种表示图像大多数信息属性的方 法。这些有信息量的属性,称为兴趣点(points of interest),是由丰富的纹理包围,基本可以重建 图像。边缘(edges)和角点(corners)是两种常用的兴趣点类型。边是像素快速变化的分界线 (boundary),角是两条边的交集。我们用scikit-image库抽取下图的兴趣点:

In [3]: %matplotlib inline import numpy as np from skimage.feature import corner_harris, corner_peaks from skimage.color import rgb2gray import matplotlib.pyplot as plt import skimage.io as io from skimage.exposure import equalize_hist

def show_corners(corners, image): fig = plt.figure() plt.gray() plt.imshow(image) y_corner, x_corner = zip(*corners) plt.plot(x_corner, y_corner, 'or') plt.xlim(0, image.shape[1]) plt.ylim(image.shape[0], 0) fig.set_size_inches(np.array(fig.get_size_inches()) * 1.5) plt.show() mandrill = io.imread('mlslpic/3.1 mandrill.png') mandrill = equalize_hist(rgb2gray(mandrill)) corners = corner_peaks(corner_harris(mandrill), min_distance=2) show_corners(corners, mandrill)

上图就是兴趣点的提取结果。图片的230400个像素中,466个兴趣点被提取。这种提取方式更紧凑, 而且当图片的亮度发生统一变化时,这些兴趣点依然存在。

SIFT和SURF

尺度不变特征转换(Scale-Invariant Feature Transform,SIFT)是一种特征提取方法,相比前面使 用的方法,SIFT对图像的尺寸,旋转,亮度变化更不敏感。每个SIFT特征都是一个描述图片上某个区 域边缘和角点的向量。和兴趣点不同,SIFT还可以获取每个兴趣点和它周围点的综合信息。加速稳健 特征(Speeded-Up Robust Features,SURF)是另一个抽取图像兴趣点的方法,其特征向量对图像 的尺寸,旋转,亮度变化是不变的。SURF的算法可以比SIFT更快,更有效的识别出兴趣点。 两种方法的具体理论解释在数字图像处理类的教材中都有介绍,感兴趣的同学可以研究。这样 用mahotas库来应用SURF方法处理下面的图片。

和兴趣点抽取类似,抽取SURF只是机器学习中创建特征向量的第一步。训练集的每个实例都会抽取 不同的SURF。第六章的,K-Means聚类,我们会介绍聚类方法抽取SURF来学习特征,可以作为一 种图像分类方法。mahotas代码如下:

In [3]: import mahotas as mh from mahotas.features import surf image = mh.imread('mlslpic/3.2 xiaobao.png', as_grey=True) print('第一个SURF描述符:\n{}\n'.format(surf.surf(image)[0])) print('抽取了%s个SURF描述符' % len(surf.surf(image))) 第一个SURF描述符: [ 1.15299134e+02 5e+02 1.00000000e+00 9e-03 2.94268692e-03 6e-02 2.58778609e-02 0e-02 3.03768176e-02 6e-03 5.75169209e-03 1e-02 1.85200481e-02 6e-01 1.12794174e-01 6e-01 1.40583321e-01 9e-02 2.03376276e-02 5e-02 2.24247135e-02 6e-01 2.36547964e-01 2e-01 2.50125503e-01 7e-02 2.00617910e-02 5e-03 3.72417955e-03 8e-02 2.99748321e-02 1e-02 5.12284796e-02 0e-02 1.02035696e-02

2.56185453e+02

3.51230841e+00

3.3278648

1.75644866e+00 -2.94268692e-03

3.3073637

3.30736379e-03 -2.58778609e-02

3.2558706

3.25587066e-02 -3.03768176e-02

4.1821264

4.18212640e-02 -5.75169209e-03

7.6642226

7.66422266e-03 -1.85200481e-02

3.1052376

3.10523761e-02 -9.61023554e-02

2.5984281

2.59842816e-01 -6.66368114e-02

2.7200637

2.72006376e-01 -1.91014197e-02

5.2825059

5.28250599e-02 -2.24247135e-02

3.3510518

3.35105185e-02 -2.36547964e-01

3.1886736

3.18867366e-01 -2.49737941e-01

3.0064451

3.03596724e-01 -1.69936886e-02

3.8239856

3.82398567e-02 -3.72417955e-03

5.5324603

5.53246035e-03 -2.99748321e-02

4.7688436

4.76884368e-02 -5.12157923e-02

7.4261931

7.42619311e-02 -1.02035696e-02

1.1972964

1.19729640e-02]

抽取了588个SURF描述符

数据标准化 许多评估方法在处理标准化数据集时可以获得更好的效果。标准化数据均值为0,单位方差(Unit Variance)。均值为0的解释变量是关于原点对称的,特征向量的单位方差表示其特征值全身统一单 位,统一量级的数据。例如,假设特征向量由两个解释变量构成,第一个变量值范围[0,1],第二个 变量值范围[0,1000000],这时就要把第二个变量的值调整为[0,1],这样才能保证数据是单位方

差。如果变量特征值的量级比其他特征值的方差还大,这个特征值就会主导学习算法的方向,导致其 他变量的影响被忽略。有些机器学习算法会在数据不标准时引入很小的优化参数值。解释变量的值可 以通过正态分布进行标准化,减去均值后除以标准差。scikit-learn的scale函数可以实现:

In [1]: from sklearn import preprocessing import numpy as np X = np.array([ [0., 0., 5., 13., 9., 1.], [0., 0., 13., 15., 10., 15.], [0., 3., 15., 2., 0., 11.] ]) print(preprocessing.scale(X)) [[ 0. -0.70710678 -1.38873015 0.52489066 0.59299945 1.35873244] [ 0. -0.70710678 0.46291005 0.87481777 0.81537425 1.01904933] [ 0. 1.41421356 0.9258201 -1.39970842 -1.4083737 0.33968311]]

总结 本章我们介绍了特征提取的方法的基础知识,将不同类型的数据转换成特征向量方便机器学习算法研 究。首先,我们介绍了分类数据的独热编码方法,并用scikit-learn的DictVectorizer类实现。然 后,介绍了许多机器学习问题中常见的文档特征向量。通过词库模型将文档转换成词块的频率构成的 特征向量,用CountVectorizer类计算基本单词频次的二进制特征向量。最后,通过停用词过滤, 词根还原,词形还原进一步优化特征向量,并加入TF-IDF权重调整文集常见词,消除文档长度对特征 向量的影响。 之后,我们介绍了图像特征提取的方法。首先,我们介绍了一个关于的手写数字识别的OCR问题,通 过图像的像素矩阵扁平化来学习手写数字特征。这种方法非常耗费资源,于是我们引入兴趣点提取方 法,通过SIFT和SURF进行优化。 最后介绍了数据标准化的方法,确保解释变量的数据都是同一量级,均值为0的标准化数据。特征提 取技术在后面的章节中会不断使用。下一章,我们把词库模型和多元线性回归方法结合来实现文档分 类。

从线性回归到逻辑回归 在第2章,线性回归里面,我们介绍了一元线性回归,多元线性回归和多项式回归。这些模型都是广 义线性回归模型的具体形式,广义线性回归是一种灵活的框架,比普通线性回归要求更少的假设。这 一章,我们讨论广义线性回归模型的具体形式的另一种形式,逻辑回归(logistic regression)。

和前面讨论的模型不同,逻辑回归是用来做分类任务的。分类任务的目标是找一个函数,把观测值匹 配到相关的类和标签上。学习算法必须用成对的特征向量和对应的标签来估计匹配函数的参数,从而 实现更好的分类效果。在二元分类(binary classification)中,分类算法必须把一个实例配置两个类 别。二元分类案例包括,预测患者是否患有某种疾病,音频中是否含有人声,杜克大学男子篮球队在 NCAA比赛中第一场的输赢。多元分类中,分类算法需要为每个实例都分类一组标签。本章,我们会 用逻辑回归来介绍一些分类算法问题,研究分类任务的效果评价,也会用到上一章学的特征抽取方 法。

逻辑回归处理二元分类 普通的线性回归假设响应变量呈正态分布,也称为高斯分布(Gaussian distribution )或钟形曲线 (bell curve)。正态分布数据是对称的,且均值,中位数和众数(mode)是一样的。很多自然现象 都服从正态分布。比如,人类的身高就服从正态分布,姚明那样的高度极少,在99%之外了。 在某些问题里,响应变量不是正态分布的。比如,掷一个硬币获取正反两面的概率分布是伯努力分布 (Bernoulli distribution),又称两点分布或者0-1分布。表示一个事件发生的概率是P ,不发生的概 率是1 − P ,概率在{0,1}之间。线性回归假设解释变量值的变化会引起响应变量值的变化,如果响 应变量的值是概率的,这条假设就不满足了。广义线性回归去掉了这条假设,用一个联连函数(link function)来描述解释变量与响应变量的关系。实际上,在第2章,线性回归里面,我们已经用了联连 函数。普通线性回归作为广义线性回归的特例使用的是恒等联连函数(identity link function),将解释 变量的通过线性组合的方式来联接服从正态分布的响应变量。如果响应变量不服从正态分布,就要用 另外一种联连函数了。 在逻辑回归里,响应变量描述了类似于掷一个硬币结果为正面的概率。如果响应变量等于或超过了指 定的临界值,预测结果就是正面,否则预测结果就是反面。响应变量是一个像线性回归中的解释变量 构成的函数表示,称为逻辑函数(logistic function)。一个值在{0,1}之间的逻辑函数如下所示: 1 F (t) =

−t

1 + e

下面是t在{-6,6}的图形:

In [27]: %matplotlib inline import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties font = FontProperties(fname=r"c:\windows\fonts\msyh.ttc", size=10) In [18]: import numpy as np plt.figure() plt.axis([-6, 6, 0, 1]) plt.grid(True) X = np.arange(-6,6,0.1) y = 1 / (1 + np.e ** (-X)) plt.plot(X, y, 'b-');

在逻辑回归中,t是解释变量的线性组合,公式如下: 1 F (t) =

−(β

1 + e

0

β

)

β

0

+

x

对数函数(logit function)是逻辑函数的逆运算: F (x) g(x) = ln 1

− F (x)

=

+

β

x

定义了逻辑回归的模型之后,我们用它来完成一个分类任务。

垃圾邮件分类 经典的二元分类问题就是垃圾邮件分类(spam classification)。这里,我们分类垃圾短信。我们用 第三章介绍的TF-IDF算法来抽取短信的特征向量,然后用逻辑回归分类。 我们可以用UCI Machine Learning Repository (http://archive.ics.uci.edu/ml/datasets/SMS+Spam+Collection)的短信垃圾分类数据集(SMS Spam Classification Data Set)。首先,我们还是用Pandas做一些描述性统计:

In [21]: import pandas as pd df = pd.read_csv('mlslpic/SMSSpamCollection', delimiter='\t', header=None ) print(df.head()) print('含spam短信数量:', df[df[0] == 'spam'][0].count()) print('含ham短信数量:', df[df[0] == 'ham'][0].count()) 0 1 0 ham Go until jurong point, crazy.. Available only ... 1 ham Ok lar... Joking wif u oni... 2 spam Free entry in 2 a wkly comp to win FA Cup fina... 3 ham U dun say so early hor... U c already then say... 4 ham Nah I don't think he goes to usf, he lives aro... 含spam短信数量: 747 含ham短信数量: 4825 每条信息的前面已经被打上了标签。共5574条短信里面,4827条是ham,747条是spam。ham短信 用0标记,spam短信用1标记。观察数据会看到更多建模时需要的信息。下面的几条信息体现两种类 型的特征: Spam: Free entry in 2 a wkly comp to win FA Cup final tkts 21st May Text FA to 87121 to receive entry question(std txt rate)T&C's apply 08452810075over18's Spam: WINNER!! As a valued network customer you have been selected to receivea £900 prize reward! To claim call 09061701461. Claim code KL341. Valid 12 hours only. Ham: Sorry my roommates took forever, it ok if I come by now? Ham: Finished class where are you. 让我们用LogisticRegression类来预测:

In [1]: import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.cross_validation import train_test_split 首先,用pandas加载数据.csv文件,然后用train_test_split分成训练集(75%)和测试集 (25%):

In [36]: df = pd.read_csv('mlslpic/SMSSpamCollection', delimiter='\t', header=None ) X_train_raw, X_test_raw, y_train, y_test = train_test_split(df[1], df[0]) 然后,我们建一个TfidfVectorizer实例来计算TF-IDF权重:

In [3]: vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(X_train_raw) X_test = vectorizer.transform(X_test_raw) 最后,我们建一个LogisticRegression实例来训练模型。和LinearRegression类 似,LogisticRegression同样实现了fit()和predict()方法。最后把结果打印出来看看:

In [5]: classifier = LogisticRegression() classifier.fit(X_train, y_train) predictions = classifier.predict(X_test) In [25]: for i, prediction in enumerate(predictions[-5:]): print('预测类型:%s. 信息:%s' % (prediction, X_test_raw.iloc[i])) 预测类型:ham. 信息:Are u coming to the funeral home 预测类型:ham. 信息:Love isn't a decision, it's a feeling. If we could decide who to love, then, life would be much simpler, bu t then less magical 预测类型:ham. 信息:Dont think so. It turns off like randomlly w ithin 5min of opening 预测类型:spam. 信息:Hey happy birthday... 预测类型:ham. 信息:None of that's happening til you get here th ough 分类模型的运行效果如何?有线性回归的度量方法在这里不太适用了。我们感兴趣的是分类是否正确 (如第一章介绍的肿瘤预测问题),并不在乎它的决策范围。下面,我们来介绍二元分类的效果评估 方法。

二元分类效果评估方法 二元分类的效果评估方法有很多,常见的包括第一章里介绍的肿瘤预测使用的准确率(accuracy), 精确率(precision)和召回率(recall)三项指标,以及综合评价指标(F1 measure), ROC AUC 值(Receiver Operating Characteristic ROC,Area Under Curve,AUC)。这些指标评价的样本分 类是真阳性(true positives),真阴性(true negatives),假阳性(false positives),假阴性 (false negatives)。阳性和阴性指分类,真和假指预测的正确与否。 在我们的垃圾短信分类里,真阳性是指分类器将一个垃圾短信分辨为spam类。真阴性是指分类器将 一个正常短信分辨为ham类。假阳性是指分类器将一个正常短信分辨为spam类。假阴性是指分类器 将一个垃圾短信分辨为ham类。混淆矩阵(Confusion matrix),也称列联表分析(Contingency table)可以用来描述真假与阴阳的关系。矩阵的行表示实际类型,列表示预测类型。

In [30]: from sklearn.metrics import confusion_matrix import matplotlib.pyplot as plt y_test = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1] y_pred = [0, 1, 0, 0, 0, 0, 0, 1, 1, 1] confusion_matrix = confusion_matrix(y_test, y_pred) print(confusion_matrix) plt.matshow(confusion_matrix) plt.title('混淆矩阵',fontproperties=font) plt.colorbar() plt.ylabel('实际类型',fontproperties=font) plt.xlabel('预测类型',fontproperties=font) plt.show() [[4 1] [2 3]]

准确率 准确率是分类器预测正确性的评估指标。scikit-learn提供了accuracy_score来计算:

In [31]: from sklearn.metrics import accuracy_score y_pred, y_true = [0, 1, 1, 0], [1, 1, 1, 1] print(accuracy_score(y_true, y_pred)) 0.5 LogisticRegression.score()用来计算模型预测的准确率:

In [34]: import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.cross_validation import train_test_split, cross_val_score df = pd.read_csv('mlslpic/sms.csv') X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'] , df['label']) vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(X_train_raw) X_test = vectorizer.transform(X_test_raw) classifier = LogisticRegression() classifier.fit(X_train, y_train) scores = cross_val_score(classifier, X_train, y_train, cv=5) print('准确率:',np.mean(scores), scores) 准确率: 0.958373205742 [ 0.96291866 0.95334928 0.95813397 0. 96172249 0.95574163] 你的结果可能和这些数字不完全相同,毕竟交叉检验的训练集和测试集都是随机抽取的。准确率是分 类器预测正确性的比例,但是并不能分辨出假阳性错误和假阴性错误。在有些问题里面,比如第一章 的肿瘤预测问题中,假阴性与假阳性要严重得多,其他的问题里可能相反。另外,有时准确率并非一 个有效的衡量指标,如果分类的比例在样本中严重失调。比如,分类器预测信用卡交易是否为虚假交 易时,假阴性比假阳性更敏感。为了提高客户满意度,信用卡部门更倾向于对合法的交易进行风险检 查,往往会忽略虚假交易。因为绝大部分交易都是合法的,这里准确率不是一个有效的衡量指标。经 常预测出虚假交易的分类器可能有很高的准确率,但是实际情况可能并非如此。因此,分类器的预测 效果还需要另外两个指标:精确率和召回率。

精确率和召回率 第一章的肿瘤预测问题中精确率和召回率的定义我们已经介绍过。在本章的垃圾短信分类器中,精确 率是指分类器预测出的垃圾短信中真的是垃圾短信的比例: TP P = TP + FP

召回率在医学领域也叫做灵敏度(sensitivity),在本例中是指所有真的垃圾短信被分类器正确找出 来的比例。 TP R = TP + FN

精确率和召回率各自含有的信息都很少,它们对分类器效果的观察角度不同。精确率和召回率都不能 从表现差的一种分类器中区分出好的分类器。例如,假设一个测试集包括10个阳性和0个阴性结果。 分类器即使将每一个样本都预测为阳性,其召回率都是1: 10 R =

= 1 10 + 0

分类器如果将每一个样本都预测为阴性,或者只是预测出假阳性和真阴性,其召回率都是0。类似 的,一个分类器如果只预测一个样本,结果为阳性,而且这个样本确实为阳性,那么这个分类器就是 100%精确的了。 scikit-learn结合真实类型数据,提供了一个函数来计算一组预测值的精确率和召回率。

In [38]: import numpy as np import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.cross_validation import train_test_split, cross_val_score df = pd.read_csv('mlslpic/sms.csv') X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'] , df['label']) vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(X_train_raw) X_test = vectorizer.transform(X_test_raw) classifier = LogisticRegression() classifier.fit(X_train, y_train) precisions = cross_val_score(classifier, X_train, y_train, cv=5, scoring= 'precision') print('精确率:', np.mean(precisions), precisions) recalls = cross_val_score(classifier, X_train, y_train, cv=5, scoring='re call') print('召回率:', np.mean(recalls), recalls) 精确率: 0.99217372134 [ 0.9875 0.98571429 1. 1. 0.98765432] 召回率: 0.672121212121 [ 0.71171171 0.62162162 0.66363636 0. 63636364 0.72727273] 我们的分类器精确率99.2%,分类器预测出的垃圾短信中99.2%都是真的垃圾短信。召回率比较低 67.2%,就是说真实的垃圾短信中,32.8%被当作正常短信了,没有被识别出来。这些数据会不断变 化,因为训练集和测试集是随机抽取的。

计算综合评价指标 综合评价指标(F1 measure)是精确率和召回率的调和均值(harmonic mean),或加权平均值, 也称为F-measure或fF-score。 1

1 +

F1

1 =

F1

1 +

P

即 PR F1 = 2 P + R

R

综合评价指标平衡了精确率和召回率。一个二元分类模型,精确率和召回率为1,那么综合评价指标 为1。如果精确率或召回率为0,那么综合评价指标为0。scikit-learn也提供了计算综合评价指标的函 数。

In [39]: f1s = cross_val_score(classifier, X_train, y_train, cv=5, scoring='f1') print('综合评价指标:', np.mean(f1s), f1s) 综合评价指标: 0.800588878125 [ 0.82722513 0.76243094 0.7978142 1 0.77777778 0.83769634] 本例的综合评价指标是80%。由于精确率和召回率的差异比较小,所以综合评价指标的罚值也比较 小。有时也会用F0.5和F2,表示精确率权重大于召回率,或召回率权重大于精确率。

ROC AUC ROC曲线(Receiver Operating Characteristic,ROC curve)可以用来可视化分类器的效果。和准确 率不同,ROC曲线对分类比例不平衡的数据集不敏感,ROC曲线显示的是对超过限定阈值的所有预 测结果的分类器效果。ROC曲线画的是分类器的召回率与误警率(fall-out)的曲线。误警率也称假 阳性率,是所有阴性样本中分类器识别为阳性的样本所占比例: FP F = TN + FP

AUC是ROC曲线下方的面积,它把ROC曲线变成一个值,表示分类器随机预测的效果。scikit-learn 提供了计算ROC和AUC指标的函数

In [40]: import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.cross_validation import train_test_split, cross_val_score from sklearn.metrics import roc_curve, auc df = pd.read_csv('mlslpic/sms.csv') X_train_raw, X_test_raw, y_train, y_test = train_test_split(df['message'] , df['label']) vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(X_train_raw) X_test = vectorizer.transform(X_test_raw) classifier = LogisticRegression() classifier.fit(X_train, y_train) predictions = classifier.predict_proba(X_test) false_positive_rate, recall, thresholds = roc_curve(y_test, predictions[: , 1]) roc_auc = auc(false_positive_rate, recall) plt.title('Receiver Operating Characteristic') plt.plot(false_positive_rate, recall, 'b', label='AUC = %0.2f' % roc_auc) plt.legend(loc='lower right') plt.plot([0, 1], [0, 1], 'r--') plt.xlim([0.0, 1.0]) plt.ylim([0.0, 1.0]) plt.ylabel('Recall') plt.xlabel('Fall-out') plt.show()

网格搜索 在第二章我们曾经提到过超参数,是需要手动调节的参数,模型无法学习。比如,在我们的垃圾短信 分类模型中,超参数出现在TF-IDF中,用来移除太频繁和太稀缺单词的频率阈值,目前函数正则化的 权重值。在scikit-learn里面,超参数是在模型建立时设置的。在前面的例子中,我们没有

为LogisticRegression()设置参数,是因为用的都是默认值。但是有时候默认值不一定是最优 的。网格搜索(Grid search)就是用来确定最优超参数的方法。其原理就是选取可能的参数不断运 行模型获取最佳效果。网格搜索用的是穷举法,其缺点在于即使每个超参数的取值范围都很小,计算 量也是巨大的。不过这是一个并行问题,参数与参数彼此独立,计算过程不需要同步,所有很多方法 都可以解决这个问题。scikit-learn有GridSearchCV()函数解决这个问题:

In [5]: import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.grid_search import GridSearchCV from sklearn.pipeline import Pipeline from sklearn.cross_validation import train_test_split from sklearn.metrics import precision_score, recall_score, accuracy_score pipeline = Pipeline([ ('vect', TfidfVectorizer(stop_words='english')), ('clf', LogisticRegression()) ]) parameters = { 'vect__max_df': (0.25, 0.5, 0.75), 'vect__stop_words': ('english', None), 'vect__max_features': (2500, 5000, 10000, None), 'vect__ngram_range': ((1, 1), (1, 2)), 'vect__use_idf': (True, False), 'vect__norm': ('l1', 'l2'), 'clf__penalty': ('l1', 'l2'), 'clf__C': (0.01, 0.1, 1, 10), } grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, sc oring='accuracy', cv=3) df = pd.read_csv('mlslpic/sms.csv') X, y, = df['message'], df['label'] X_train, X_test, y_train, y_test = train_test_split(X, y) grid_search.fit(X_train, y_train) print('最佳效果:%0.3f' % grid_search.best_score_) print('最优参数组合:') best_parameters = grid_search.best_estimator_.get_params() for param_name in sorted(parameters.keys()): print('\t%s: %r' % (param_name, best_parameters[param_name])) predictions = grid_search.predict(X_test) print('准确率:', accuracy_score(y_test, predictions)) print('精确率:', precision_score(y_test, predictions)) print('召回率:', recall_score(y_test, predictions)) [Parallel(n_jobs=-1)]: Done 1 jobs [Parallel(n_jobs=-1)]: Done 50 jobs [Parallel(n_jobs=-1)]: Done 200 jobs [Parallel(n_jobs=-1)]: Done 450 jobs

| elapsed: | elapsed: | elapsed: | elapsed:

1.8s 10.1s 27.4s 54.2s

[Parallel(n_jobs=-1)]: Done 800 jobs [Parallel(n_jobs=-1)]: Done 1250 jobs [Parallel(n_jobs=-1)]: Done 1800 jobs [Parallel(n_jobs=-1)]: Done 2450 jobs [Parallel(n_jobs=-1)]: Done 3200 jobs

| elapsed: 1.6min | elapsed: 2.4min | elapsed: 3.4min | elapsed: 4.6min | elapsed: 6.0min

[Parallel(n_jobs=-1)]: Done 4050 jobs | elapsed: 10.6min [Parallel(n_jobs=-1)]: Done 4608 out of 4608 | elapsed: 11.7min finished Fitting 3 folds for each of 1536 candidates, totalling 4608 fit s 最佳效果:0.982 最优参数组合: clf__C: 10 clf__penalty: 'l2' vect__max_df: 0.25 vect__max_features: 2500 vect__ngram_range: (1, 2) vect__norm: 'l2' vect__stop_words: None vect__use_idf: True 准确率: 0.989956958393 精确率: 0.994252873563 召回率: 0.930107526882 GridSearchCV()函数的参数有待评估模型pipeline,超参数词典parameters和效果评价指 标scoring。n_jobs是指并发进程最大数量,设置为-1表示使用所有CPU核心进程。在Python3.4 中,可以写一个Python的脚本,让fit()函数可以在main()函数里调用,也可以在Python自带命令 行,IPython命令行和IPython Notebook运行。经过网格计算后的超参数在训练集中取得了很好的效 果。

多类分类 现实中有很多问题不只是分成两类,许多问题都需要分成多个类,成为多类分类问题(Multi-class classification)。比如听到一首歌的样曲之后,可以将其归入某一种音乐风格。这类风格就有许多 种。scikit-learn用one-vs.-all或one-vs.-the-rest方法实现多类分类,就是把多类中的每个类都作为二 元分类处理。分类器预测样本不同类型,将具有最大置信水平的类型作为样本类 型。LogisticRegression()通过one-vs.-all策略支持多类分类。下面,我们用它来分析一个多类 分类问题。 假设你想看电影,而你又非常讨厌看太次的电影。所以有品位的你可以在看每部电影之前都会看一堆 影评,不过你更讨厌看影评。那么下面我们就用好影评来给电影分类。 本例中,我们利用烂番茄(Rotten Tomatoes)网站影评短语数据对电影进行评价。每个影评可以归 入下面5个类项:不给力(negative),不太给力(somewhat negative),中等(neutral),有点给 力(somewhat positive), 给力(positive)。解释变量不会总是直白的语言,因为影评内容千差万 别,有讽刺的,否定的,以及其他语义的表述,语义并不直白,这些都会让分类充满挑战。数据集可 以从kaggle (https://www.kaggle.com/c/sentiment-analysis-on-movie-reviews)上下载。首先,我们 还是用Pandas简单探索一下:

In [28]: import zipfile # 压缩节省空间 z = zipfile.ZipFile('mlslpic/train.zip') df = pd.read_csv(z.open(z.namelist()[0]), header=0, delimiter='\t') In [30]: df.head() Out[30]: PhraseId SentenceId Phrase

Sentiment

0 1

1

A series of escapades demonstrating the adage ...

1

1 2

1

A series of escapades demonstrating the adage ...

2

2 3

1

A series

2

3 4

1

A

2

4 5

1

series

2

In [31]: df.count() Out[31]: PhraseId SentenceId Phrase Sentiment dtype: int64

156060 156060 156060 156060

Sentiment是响应变量,0是不给力(negative),4是给力(positive),其他以此类推。Phrase 列是影评的内容。影评中每句话都被分割成一行。我们不需要考虑PhraseId列和SentenceId列。

In [32]: df.Phrase.head(10) Out[32]: 0 A series of escapades demonstrating the adage ... 1 A series of escapades demonstrating the adage ... 2 A series 3 A 4 series 5 of escapades demonstrating the adage that what... 6 of 7 escapades demonstrating the adage that what is... 8 escapades 9 demonstrating the adage that what is good for ... Name: Phrase, dtype: object In [33]: df.Sentiment.describe() Out[33]: count 156060.000000 mean 2.063578 std 0.893832 min 0.000000 25% 2.000000 50% 2.000000 75% 3.000000 max 4.000000 Name: Sentiment, dtype: float64 In [34]: df.Sentiment.value_counts() Out[34]: 2 79582 3 32927 1 27273 4 9206 0 7072 dtype: int64

In [35]: df.Sentiment.value_counts()/df.Sentiment.count() Out[35]: 2 0.509945 3 0.210989 1 0.174760 4 0.058990 0 0.045316 dtype: float64 可以看出,近51%都是评价为2中等(neutral)的电影。可见,在这个问题里,准确率不是一个有信 息量的评价指标,因为即使很烂的分类器预测出中等水平的结果,其准确率也是51%。3有点给力 (somewhat positive)的电影占21%, 4给力(positive)的电影占6%,共占27%。剩下的21%就是 不给力(negative),不太给力(somewhat negative)的电影。用scikit-learn来训练分类器:

In [37]: import pandas as pd from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model.logistic import LogisticRegression from sklearn.cross_validation import train_test_split from sklearn.metrics import classification_report, accuracy_score, confus ion_matrix from sklearn.pipeline import Pipeline from sklearn.grid_search import GridSearchCV import zipfile pipeline = Pipeline([ ('vect', TfidfVectorizer(stop_words='english')), ('clf', LogisticRegression()) ]) parameters = { 'vect__max_df': (0.25, 0.5), 'vect__ngram_range': ((1, 1), (1, 2)), 'vect__use_idf': (True, False), 'clf__C': (0.1, 1, 10), } z = zipfile.ZipFile('mlslpic/train.zip') df = pd.read_csv(z.open(z.namelist()[0]), header=0, delimiter='\t') X, y = df['Phrase'], df['Sentiment'].as_matrix() X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.5) grid_search = GridSearchCV(pipeline, parameters, n_jobs=3, verbose=1, sco ring='accuracy') grid_search.fit(X_train, y_train) print('最佳效果:%0.3f' % grid_search.best_score_) print('最优参数组合:') best_parameters = grid_search.best_estimator_.get_params() for param_name in sorted(parameters.keys()): print('\t%s: %r' % (param_name, best_parameters[param_name])) [Parallel(n_jobs=3)]: Done 1 jobs | elapsed: 4.6s [Parallel(n_jobs=3)]: Done 50 jobs | elapsed: 1.8min [Parallel(n_jobs=3)]: Done 68 out of 72 | elapsed: 3.0min re maining: 10.6s [Parallel(n_jobs=3)]: Done 72 out of 72 | elapsed: 3.3min fi nished Fitting 3 folds for each of 24 candidates, totalling 72 fits 最佳效果:0.620 最优参数组合: clf__C: 10 vect__max_df: 0.25 vect__ngram_range: (1, 2) vect__use_idf: False

多类分类效果评估 二元分类里,混淆矩阵可以用来可视化不同分类错误的数据。每种类型的精确率,召回率和综合评价 指标(F1 score)可以计算,所有预测的准确率也可以计算。

In [38]: predictions = grid_search.predict(X_test) print('准确率:', accuracy_score(y_test, predictions)) print('混淆矩阵:', confusion_matrix(y_test, predictions)) print('分类报告:', classification_report(y_test, predictions)) 准确率: 0.635024990388 混淆矩阵: [[ 1178 1701 616 71 4] [ 990 5993 6030 563 30] [ 227 3231 32668 3520 143] [ 37 401 6642 8089 1305] [ 7 30 534 2397 1623]] 分类报告: precision recall f1-score 0 1 2 3 4

0.48 0.53 0.70 0.55 0.52

0.33 0.44 0.82 0.49 0.35

0.39 0.48 0.76 0.52 0.42

3570 13606 39789 16474 4591

avg / total

0.62

0.64

0.62

78030

support

我们通过网格搜索获得了最佳参数组合,最终的分类器是通过对开始的分类器不断优化得到的。

多标签分类和问题转换 前面我们讨论了二元分类,多类分类,还有一种分类问题是多标签分类(multi-label classification)。每个样本可以拥有全部类型的一部分类型。这样的例子太普遍了,比如统计班上同 学一周7天里哪天有空。每个同学都会在周一到周日这7天里,根据自己的情况分别打勾。再比如常见 的博客文章分类标签,一篇文章一般都有好几个标签等等。多标签分类问题一般有两种解决方法。 问题转化方法(Problem transformation)可以将多标签问题转化成单标签问题。 第一种转换方法是训 练集里面每个样本通过幂运算转换成单标签。比如下面数据里面每篇文章都带有若干标签。

转换方法就是用幂运算将多个类合并成一个类,比如样本1有Local和US类,新建一个标签 为Local^US类,这样多标签就变成单标签了。

这样原来5个标签现在变成了7个标签。这种幂运算虽然直观,但是并不实用,因为这样做多出来的标 签只有一小部分样本会用到。而且,这些标签只能在训练集里面学习这些类似,在测试集中依然无法 使用。 另外一种问题转换方法就是每个标签都用二元分类处理。每个标签的分类器都预测样本是否属于该标 签。我们的例子中需要5个二元分类器,第一个分类器预测样本是否应该被归入Local类,第二个分 类器预测样本是否应该被归入US类,以此类推。预测最后一步就是把这些分类结果求并集,如下图 所示。这个问题确保了单标签问题和多标签问题有同样的训练集,只是忽略了标签之间的关联关系。

多标签分类效果评估 多标签分类效果评估与多标签分类效果评估方式不同。最常用的手段是汉明损失函数(Hamming loss)和杰卡德相似度(Jaccard similarity)。汉明损失函数表示错误标签的平均比例,是一个函 数,当预测全部正确,即没有错误标签时,值为0。杰卡德相似度或杰卡德相指数(Jaccard index),是预测标签和真实标签的交集数量除以预测标签和真实标签的并集数量。其值在{0,1}之 间,公式如下:

∩ T rue| |Predicted ∪ T rue| |Predicted

J (Predicted, T rue) =

In [39]: import numpy as np from sklearn.metrics import hamming_loss, jaccard_similarity_score print(hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[0.0, 1. 0], [1.0, 1.0]]))) print(hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1. 0], [1.0, 1.0]]))) print(hamming_loss(np.array([[0.0, 1.0], [1.0, 1.0]]), np.array([[1.0, 1. 0], [0.0, 1.0]]))) print(jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.arr ay([[0.0, 1.0], [1.0, 1.0]]))) print(jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.arr ay([[1.0, 1.0], [1.0, 1.0]]))) print(jaccard_similarity_score(np.array([[0.0, 1.0], [1.0, 1.0]]), np.arr ay([[1.0, 1.0], [0.0, 1.0]]))) 0.0 0.25 0.5 1.0 0.75 0.5

总结 本章我们介绍了广义线性模型,是对普通线性回归中解释变量非正态分布情况的扩展。广义线性回归 模型通过联接方程将解释变量和响应变量联接起来,和普通线性回归不同,这个方程可能是非线性 的。我们重点介绍了逻辑联接方程,其图象是一种S曲线,对任意实数的返回值都在在{0,1}之间, 如群体生长曲线。 之后,我们介绍了逻辑回归,一种通过逻辑联接方程联接解释变量与呈伯努力分布的响应变量的关 系。逻辑回归可用于解决二元分类问题,我们用它研究了典型的垃圾短信分类问题。紧接着我们介绍 了多类分类问题,其类型空间超过两个,每个样本都有且仅有一种类型,我们用one-vs.-all策略研究 了通过影评对电影分类的问题。最后,我们介绍了多标签分类,其类型空间超过两个,每个样本都有 至少一种标签。介绍完广义线性模型的回归和分类问题,下一章我们就来介绍非线性模型的回归和分 类问题——决策树。

决策树——非线性回归与分类 前面几章,我们介绍的模型都是广义线性模型,基本方法都是通过联接方程构建解释变量与若干响应 变量的关联关系。我们用多元线性回归解决回归问题,逻辑回归解决分类问题。本章我们要讨论一种 简单的非线性模型,用来解决回归与分类问题,称为决策树(decision tree)。首先,我们将用决策 树做一个广告屏蔽器,可以将网页中的广告内容屏蔽掉。之后,我们介绍集成学习(lensemble learning)方法,通过将一系列学习方法集成使用,以取得更好的训练效果。

决策树简介 决策树就是做出一个树状决策,就像猜猜看(Twenty Questions)的游戏。一个玩家(先知)选择一种 常见物品,但是事先不能透露给其他玩家(提问者)。提问者最多问20个问题,而先知只能回答: 是,否,可能三种答案。提问者的提问会根据先知的回答越来越具体,多个问题问完后,提问者的决 策就形成了一颗决策树。决策树的分支由可以猜出响应变量值的最短的解释变量序列构成。因此,在 猜猜看游戏中,提问者和先知对训练集的解释变量和响应变量都很了解,但是只有先知知道测试集的 响应变量值。 决策树通常是重复的将训练集解释变量分割成子集的过程,如下图所示。决策树的节点用方块表示, 用来测试解释变量。每个节点向下的边表示不同决策产生结果。训练集的样本由决策结果分成不同的 子集。例如,一个节点测试解释变量的值是否超过的限定值。如果没有超过,则进入该节点的右侧子 节点;如果超过,则进入左侧子节点。子节点的运行原理和前面的一样,直到终止条件(stopping criterion)满足才停止。在分类任务中,包含在叶子节点中的样本响应变量的值的平均值作为响应变 量的估计值。决策树建立之后,做决策的过程就是把测试样本放进决策树沿着边不断前进,直到一个 叶子被触及才停止前进。

训练决策树 我们用Ross Quinlan发明的ID3(Iterative Dichotomiser 3,迭代二叉树3代)算法创建决策树,ID3是 最早用于决策树的算法之一。假设你有一些猫和狗的分类数据。但是不允许直接观察,你只能通过动 物特征的描述去做决策。对每个动物,你都会获得关于“是否喜欢玩球(play fetch)”和“是否经常发 脾气”,以及它最喜欢的食物三个问题的答案。 要正确分出新动物的种类,决策树需要对每条边的解释变量进行检查。每条边的下一个节点由测试结 果决定。例如,第一关节点可能问“是否喜欢玩球”,如果回答“YES”,则进入左节点,否则,如果回 答“NO”,则进入右节点。以此类推,最后一条边会指向一个叶子节点,那就是答案。下表是14个节 点的训练数据: 训练数据 是否喜欢玩球 是否经常发脾气 最喜欢的食物 种类 1

Yes

No

Bacon

Dog

2

No

Yes

Dog Food

Dog

3

No

Yes

Cat food

Cat

4

No

Yes

Bacon

Cat

5

No

No

Cat food

Cat

6

No

Yes

Bacon

Cat

7

No

Yes

Cat Food

Cat

8

No

No

Dog Food

Dog

9

No

Yes

Cat food

Cat

10

Yes

No

Dog Food

Dog

11

Yes

No

Bacon

Dog

12

No

No

Cat food

Cat

13

Yes

Yes

Cat food

Cat

14

Yes

Yes

Bacon

Dog

从数据中我们发现,猫比狗更容易发脾气。大多数狗玩球,而猫不爱玩。狗更喜欢狗粮和培根,而猫 喜欢猫粮和培根。解释变量是否喜欢玩球和是否经常发脾气可以转换成二元特征值。解释变量最喜欢 的食物可以转换成一个具有三个可能值的分类变量,可以用热独编 码[1,0,0],[0,1,0],[0,0,1]表示,具体方法在第三章已经介绍过。通过上面的分析,我们可 以构建模型的规则。例如,一个动物如果经常发脾气且喜欢吃猫粮那就是猫,如果喜欢玩球且爱吃培 根就是狗。在这么小的训练集里,想手工逐条构建规则也是非常麻烦的事情。因此,下面我们来构建 决策树。

问题选择 和猜猜看一样,决策树也是通过对解释变量序列的逐条测试获取响应变量结果的。那么,哪个解释变 量应该先测试?直觉观察会发现,解释变量集合包含所有猫或者所有狗的测试,比既包含猫又包含狗 的解释变量集合的测试要好。如果子集成员种类不同,我们还是不能确定种类。我们还需要避免创建 那种测试,把单独的一只猫或一条狗分离出去,这种做法类似于猜猜看问题中前几轮就问非常具体的 问题。更一般的情形是,这些测试极少可以分出一个样本的种类,也不能降低分类不确定性。能够降 低分类不确定性的测试通常都是最好的测试。我们通常用熵(entropy)来度量信息的不确定性。 以比特(bits)为计量单位,熵量化了一个变量的不确定性。熵计算公式如下所示: n

H (X ) =





P(xi ) logb P(xi )

i=1

其中,n 是样本的数量,P(xi ) 是第i个样本的概率。b 一般取2 ,e 或10 。因为对数函数中真数小于1 则对数值为0,因此,公式前面加符号使熵为正数。 例如,一个硬币投掷一次事件发生后一般有两种可能:正面或反面。正面朝上的概率是0.5,反面朝 上的概率也是0.5。那么一个硬币投掷一次的结果这个变量的熵: H (X ) =

−(0.5 log

2

0.5 + 0.5 log2 0.5) = 1.0

也就是说,两个等概率的可能值,正面和反面,只需要一个比特。如果是两个硬币投掷一次事件发生 后一般有四种可能:正面正面,正面反面,反面反面,反面正面,每种可能的概率是0.25。其熵为: H (X ) =

−(0.25 log

2

0.25 × 4) = 2.0

如果硬币的两面相同,那么表示其可能值的变量熵为0比特,也就是说,结果是确定的,变量再也不 会产生新信息量了。熵还可以用小数值表示。比如,一个不正常的硬币,其正反面的材质不同,一边 重一边轻。导致其投掷后正面朝上的概率0.8,反面朝上概率0.2。那么其熵为: H (X ) =

−(0.8 log

2

0.8 + 0.2 log2 0.2) = 0.721928095

一个不正常的硬币投掷后其结果的熵是一个小数。虽然两种结果都有可能,但是因为其中一种可能性 更大,所有不确定性减小了。

下面让我们计算动物分类的熵。如果训练集数据中猫和狗数量是相等的,而且我们不知道动物的任何 其他信息,那么决策的熵是1。这就像普通硬币的结果一样,非猫即狗,两种可能概率一样。但是, 我们的训练数据里面,6条狗8只猫。如果我们不考虑其他信息,那么决策的熵就是: H (X ) =

−(

6 14

6 log2

8 +

14

14

8 log2

) = 0.985228136 14

由于猫更多,所以不确定性要小一些。现在让我们找出对分类最有用的解释变量,也就是找出对熵降 幅最大的解释变量。我们可以测试是否喜欢玩球这个解释变量,把测试分成两支,喜欢玩和不喜欢 玩,其结果如下图所示:

决策树通常都是用流程图显示决策过程的。最上面的方框是根节点,包括所有要测试的解释变量。在 根节点里我们还没有开始测试,所以变量的熵设为0.985228136。前面我们介绍过,将解释变量是否 喜欢玩球转换成二元变量,左子节点用0表示,右子节点用1表示。左子节点包括7只猫和2条狗都是 不喜欢玩球的数据。计算这时解释变量的熵: H (X ) =

−(

7 9

7 log2

2 +

9

9

2 log2

) = 0.764204507 9

右子节点包括1只猫和4条狗都是喜欢玩球的数据。计算这时解释变量的熵: H (X ) =

−(

1 5

1 log2

4 +

5

5

4 log2

) = 0.721928095 5

同理,我们也可以测试解释变量是否经常发脾气。不经常发脾气为左子节点,用0表示,经常发脾气 为右子节点,用1表示。

也可以对解释变量最喜欢的食物。对每一种食物都可以按照前面的思路进行测试:

信息增益 对解释变量最喜欢的食物的值是猫粮进行测试的结果是,右节点喜欢猫粮的动物中6只猫没有狗,其 熵为0,而做节点2只猫6条狗,其熵为0.8113比特。我们如何评估哪一个变量最大程度的降低了分类 的不确定性?子集熵的均值看起来像是一个合理的度量指标。本例中,猫粮测试这个子集熵的均值最 小。直观上看,这条测试也更有效,因为我们可以用它识别出几乎是一半样本。但是,实际上这么做 可能做导致决策局部最优值。例如,假设有一个子集的结果是两条狗没有猫,另一个子集的结果是4 条狗8只猫。第一个子集的熵是0,而第二个子集的熵0.918。那么平均熵是0.459,但是第二个子集包 含了绝大多数样本,而其熵接近1比特。

这就好像在猜猜看游戏中过早的问了太具体的问题,而我们的问题并没有消除许多可能性。因此,我 们要用一种方法来合理度量熵的降幅,这个方法称为信息增益(information gain)。信息增益是父 节点熵,用H (T )表示与其子节点熵的加权均值的差,计算公式如下: I G(T , a) = H (T )

{|x





v

其中,xa H ({x

∈vals(s)

∈ T |x

a

a

a

= v})

H ({x

|T |

∈ vals(s) 表示解释变量a 的样本x 。{|x ∈ T |x

∈ T |x

= v}|

= v}

∈ T |x

a

= v})

表示解释变量a 的值等于v 样本数量。

是解释变量a 的值等于v 样本熵。

下表就是本例信息增益的计算结果。可以看出,猫粮测试是最佳选择,因为其信息增益最大。

测试

父节点 熵

左子节点 熵

右子节点 熵

加权平均

信息增 益

是否喜欢玩球?

0.9852

0.7642

0.7219

0.7490 * 9/14 + 0.7219 * 5/14 = 0.7491

0.2361

是否经常发脾气?

0.9852

0.9183

0.8113

0.9183 * 6/14 + 0.8113 * 8/14 = 0.8571

0.1280

最喜欢的食物 = 猫 0.9852 粮

0.8113

0

0.8113 * 8 /14 + 0.0 * 6/14 = 0.4636

0.5216

最喜欢的食物 = 狗 0.9852 粮

0.8454

0

0.8454 * 11/14 + 0.0 * 3/14 = 0.6642

0.3210

最喜欢的食物 = 培 0.9852 根

0.9183

0.971

0.9183 * 9/14 + 0.9710 * 5/14 = 0.9371

0.0481

现在让我们增加其他的节点到决策树中。一个子节点只包含猫,另一个子节点还有2只猫和6条狗,我 们测试这个节点。同理,按照信息增益方法计算可以得到下表数据:

测试

父节点 熵

左子节点 熵

右子节点 熵

加权平均

信息增 益

是否喜欢玩球?

0.8113

1

0

1.0 * 4/8 + 0 * 4/8 = 0.5

0.3113

是否经常发脾气?

0.8113

0

1

0.0 * 4/8 + 1 * 4/8 = 0.5

0.3113

最喜欢的食物 = 狗 粮

0.8113

0.9710

0

0.9710 * 5/8 + 0.0 * 3/8 = 0.6069

0.2044

最喜欢的食物 = 培 根

0.8113

0

0.9710

0.0 * 3/8 + 0.9710 * 5/8 = 0.6069

0.2044

所有的测试都会出现熵为0的情况,但是从表中可以看出,解释变量是否喜欢玩球和是否经常发脾气 的信息增益相等且都是最大的。ID3算法会随机选择一个节点继续测试。我们选择是否经常发脾气这 个解释变量。它的右节点的8个动物分成左节点是4条狗,右节点是两只猫两只狗。如下图所示:

现在我们对剩下的解释变量进行信息增益计算,包括是否喜欢玩球?,最喜欢的食物 = 狗粮,最喜 欢的食物 = 培根,这些解释变量测试的结果都是一个节点是一只猫或一条狗,另一个节点是剩下的 动物。其信息增益计算结果如下表所示: 测试

父节点熵 左子节点熵 右子节点熵 加权平均 信息增益

是否喜欢玩球?

1

0.9183

0

0.688725 0.311275

最喜欢的食物 = 狗粮

1

0.9183

0

0.688725 0.311275

最喜欢的食物 = 培根

1

0

0.9183

0.688725 0.311275

我们随机选择是否喜欢玩球?这个解释变量来生成后面的节点,左节点包含一条狗,右节点包含两只 猫和一条狗。其他两个解释变量,最喜欢的食物 = 狗粮和最喜欢的食物 = 培根产生同样的结果, 左节点包含一条狗,右节点包含两只猫。然后,我们随机选择最喜欢的食物 = 狗粮进行测试,最终 胜出决策树如下图所示:

让我们用下表的测试集数据对决策树进行测试: 训练数据 是否喜欢玩球 是否经常发脾气 最喜欢的食物 种类 1

Yes

No

Bacon

Dog

2

Yes

Yes

Dog Food

Dog

3

No

Yes

Dog Food

Cat

4

No

Yes

Bacon

Cat

5

No

No

Cat food

Cat

让我们来找第一个动物的类型,它喜欢玩球,不经常发脾气,喜欢培根。沿着决策树往下走,根节点 测试不喜欢猫粮,因此进入左节点。又不经常发脾气,依然进入左节点,现在的叶子节点只有狗,因 此这个动物种类是狗。其他动物也按照同样的方法去查找,第三个动物是一只猫,根节点测试不喜欢 猫粮,进入左节点,然后经常发脾气,进入右节点,不喜欢玩球,进入左节点,喜欢狗粮,进入右节 点,因此该动物是猫。

这样我们就用ID3算法实现了一个决策树。还有很多算法也可以实现决策树,C4.5算法是ID3的改进 版,可以用来处理连续的解释变量并考虑特征值丢失。C4.5算法可以修剪(prune)决策树,修剪是 通过更少的叶节点来替换分支,以缩小决策树的规模。scikit-learn的决策树实现算法是 CART(Classification and Regression Trees,分类与回归树)算法,CART也是一种支持修剪的学 习算法。

基尼不纯度 前面我们用最大信息增益建立决策树。还有一个启发式方法是基尼不纯度(Gini impurity),度量一 个集合中每种类型的比例。基尼不纯度格式如下: j

Gini(t) = 1



∑ i

P(i|t)

2

−1

其中,j是类型的数量,t是节点样本的子集,P(i|t) 是从节点子集中选择一个类型i的概率。 可以看出,如果集合中只有一类,那么基尼不纯度值为0。和熵一样,当每个类型概率相同时,基尼 不纯度最大。此时,基尼不纯度的最大值有类型的数量决定: Gini max = 1



1 n

我们的例子有两种类型,所有基尼不纯度的最大值是0.5。scikit-learn研究决策树的算法,既支持信 息增益,也支持基尼不纯度。到底用哪种方法并没有规定,实际上,它们产生的结果类似。一般的决 策树都是两个都用,比较一下结果,哪个好用哪个。

scikit-learn决策树 下面让我们用scikit-learn的决策树来做一个广告屏蔽程序。这个程序可以预测出网页上的图片是广告 还是正常内容。被确认是广告的图片通过调整CSS隐藏。我们用互联网广告数据集(Internet Advertisements Data Set) (http://archive.ics.uci.edu/ml/datasets/Internet+Advertisements)来实现 分类器,里面包含了3279张图片。不过类型的比例并不协调,459幅广告图片,2820幅正常内容。决 策树学习算法可以从比例并不协调的数据集中生成一个不平衡的决策树(biased tree)。在决定是否 值得通过过抽样(over-sampling)和欠抽样(under-sampling)的方法平衡训练集之前,我们将用 不相关的数据集对模型进行评估。本例的解释变量就是图片的尺寸,网址链接里的单词,以及图片标 签周围的单词。响应变量就是图片的类型。解释变量已经被转换成特征向量了。前三个特征值表示宽 度,高度,图像纵横比(aspect ratio)。剩下的特征是文本变量的二元频率值。下面,我们用网格 搜索来确定决策树模型最大最优评价效果(F1 score)的超参数,然后把决策树用在测试集进行效果 评估。

In [24]: import pandas as pd from sklearn.tree import DecisionTreeClassifier from sklearn.cross_validation import train_test_split from sklearn.metrics import classification_report from sklearn.pipeline import Pipeline from sklearn.grid_search import GridSearchCV import zipfile # 压缩节省空间 z = zipfile.ZipFile('mlslpic/ad.zip') df = pd.read_csv(z.open(z.namelist()[0]), header=None, low_memory=False) explanatory_variable_columns = set(df.columns.values) response_variable_column = df[len(df.columns.values)-1] # The last column describes the targets explanatory_variable_columns.remove(len(df.columns.values)-1) y = [1 if e == 'ad.' else 0 for e in response_variable_column] X = df.loc[:, list(explanatory_variable_columns)] 首先,我们读取数据文件,然后解释变量和响应变量分开。

In [25]: X.replace(to_replace=' *\?', value=-1, regex=True, inplace=True) X_train, X_test, y_train, y_test = train_test_split(X, y) 我们把广告图片设为阳性类型,正文图片设为阴性类型。超过1/4的图片其宽带或高度的值不完整, 用空白加问号(“ ?”)表示,我们用正则表达式替换为-1,方便计算。然后我们用交叉检验对训练集 和测试集进行分割。

In [26]: pipeline = Pipeline([ ('clf', DecisionTreeClassifier(criterion='entropy')) ]) 我们创建了pipeline和DecisionTreeClassifier类的实例,将criterion参数设置 成entropy,这样表示使用信息增益启发式算法建立决策树。

In [27]: parameters = { 'clf__max_depth': (150, 155, 160), 'clf__min_samples_split': (1, 2, 3), 'clf__min_samples_leaf': (1, 2, 3) } 然后,我们确定网格搜索的参数范围。最后将GridSearchCV的搜索目标scoring设置为f1。

In [28]: grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, sc oring='f1') grid_search.fit(X_train, y_train) print('最佳效果:%0.3f' % grid_search.best_score_) print('最优参数:') best_parameters = grid_search.best_estimator_.get_params() for param_name in sorted(parameters.keys()): print('\t%s: %r' % (param_name, best_parameters[param_name])) predictions = grid_search.predict(X_test) print(classification_report(y_test, predictions)) [Parallel(n_jobs=-1)]: Done 1 jobs | elapsed: [Parallel(n_jobs=-1)]: Done 50 jobs | elapsed: [Parallel(n_jobs=-1)]: Done 75 out of 81 | elapsed: emaining: 4.0s [Parallel(n_jobs=-1)]: Done 81 out of 81 | elapsed: inished

20.3s 41.5s 51.4s r 53.1s f

Fitting 3 folds for each of 27 candidates, totalling 81 fits 最佳效果:0.899 最优参数: clf__max_depth: 160 clf__min_samples_leaf: 1 clf__min_samples_split: 3 precision recall f1-score support 0 1

0.97 0.88

0.98 0.83

0.98 0.86

709 111

avg / total

0.96

0.96

0.96

820

这个分类器发现了测试集中90%的广告,真广告中有88%被模型发现了,你运行的数据结果可能会 有不同。分类器的效果还可以,下面我们进一步改善模型的效果。

决策树集成 集成学习方法将一堆模型组合起来使用,比单个模型可以获取更好的效果。随机森林(random forest)是一种随机选取训练集解释变量的子集进行训练,获得一系列决策树的集合的方法。随机森 林通常用其决策树集合里每个决策树的预测结果的均值或众数作为最终预测值。scikit-learn里的随机 森林使用均值作为预测值。随机森林相比单一决策树,不太会受到拟合过度的影响,因为随机森林的 每个决策树都看不到训练集的全貌,只是训练一部分解释变量数据,不会记忆训练集的全部噪声。 下面我们用随机森林升级我们的广告屏蔽程序。把前面用的DecisionTreeClassifier替换 成RandomForestClassifier就可以了。和前面一样,我们仍然用网格搜索来探索最优超参数。

In [31]: import pandas as pd

import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.cross_validation import train_test_split from sklearn.metrics import classification_report from sklearn.pipeline import Pipeline from sklearn.grid_search import GridSearchCV

import zipfile # 压缩节省空间 z = zipfile.ZipFile('mlslpic/ad.zip') df = pd.read_csv(z.open(z.namelist()[0]), header=None, low_memory=False) explanatory_variable_columns = set(df.columns.values) response_variable_column = df[len(df.columns.values)-1] # The last column describes the targets explanatory_variable_columns.remove(len(df.columns.values)-1) y = [1 if e == 'ad.' else 0 for e in response_variable_column] X = df.loc[:, list(explanatory_variable_columns)] X.replace(to_replace=' *\?', value=-1, regex=True, inplace=True) X_train, X_test, y_train, y_test = train_test_split(X, y) pipeline = Pipeline([ ('clf', RandomForestClassifier(criterion='entropy')) ]) parameters = { 'clf__n_estimators': (5, 10, 20, 50), 'clf__max_depth': (50, 150, 250), 'clf__min_samples_split': (1, 2, 3), 'clf__min_samples_leaf': (1, 2, 3) } grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, sc oring='f1') grid_search.fit(X_train, y_train) print('最佳效果:%0.3f' % grid_search.best_score_) print('最优参数:') best_parameters = grid_search.best_estimator_.get_params() for param_name in sorted(parameters.keys()): print('\t%s: %r' % (param_name, best_parameters[param_name])) predictions = grid_search.predict(X_test) print(classification_report(y_test, predictions)) [Parallel(n_jobs=-1)]: Done 1 jobs | elapsed: 3.4s [Parallel(n_jobs=-1)]: Done 50 jobs | elapsed: 24.0s [Parallel(n_jobs=-1)]: Done 200 jobs | elapsed: 1.3min [Parallel(n_jobs=-1)]: Done 318 out of 324 | elapsed: 2.0min r emaining: 2.1s [Parallel(n_jobs=-1)]: Done 324 out of 324 | elapsed: 2.0min f inished Fitting 3 folds for each of 108 candidates, totalling 324 fits 最佳效果:0.914 最优参数: clf__max_depth: 50 clf__min_samples_leaf: 1 clf__min_samples_split: 1 clf__n_estimators: 20

clf__n_estimators: 20 precision recall f1-score

support

0 1

0.98 0.94

0.99 0.90

0.99 0.92

712 108

avg / total

0.98

0.98

0.98

820

这个分类器发现了测试集中91%的广告,各类指标相比单一决策树都有明显改善。精确率和召回率都 提升到98%。

决策树的优劣势 和前面几章介绍过的模型相比,决策树的用法更简单。首先,决策树对数据没有零均值,均方差的要 求。而且可以容忍解释变量值的缺失,虽然现在的scikit-learn还没实现这一特点。决策树在训练的时 候可以忽略与任务无关的解释变量。 小型决策树很容易理解,而且可以通过scikit-learn的tree模块里的export_graphviz函数生成图 形,可视化效果好。决策树的分支都有着逻辑上的联接关系,很容易通过流程图画出来。另外,决策 树支持多输出任务,单一决策树可以用于多类分类,不需要使用one-versus-all策略。 和前面介绍过的模型一样,决策树是一种积极学习方法(eager learner),必须在它们可以用于预测 测试集任务时,先从训练集建立一个与后面的需求无关的模型,但是模型一旦建好它们可以很快的预 测出结果。相反,有些算法是消极学习方法(lazy learners),像K最近邻(K-Nearest Neighbor, KNN)分类算法,它们必须等到有了训练集数据的预测需求,才会开始学习整个数据的特征。消极学 习方法不需要花时间训练预测能力,但是比积极学习方法预测速度慢。

决策树比我们之前介绍的算法更容易拟合过度,因为它们可以通过精确的描述每个训练样本的特征, 构建出复杂的决策树,从而忽略了一般性的真实关联关系。有一些技术可以修正决策树的拟合过度。 修剪就是一个常用的策略,将决策树里一些最高的子节点和叶子节点剪掉,但是目前scikit-learn还没 有相应的实现。但是,类似的效果可以通过设置决策树最大深度,或者限定只有当决策树包含的训练 样本数量超过限定值时才创建子节点。DecisionTreeClassifier和DecisionTreeRegressor 类都有这样的参数可以设置。另外,随机森林决策树也可以消除拟合过度。 像ID3这样的决策树学习算法是贪婪的(greedy)。它们充分的学习有时会深陷于局部最优的美梦, 但是不能保证生成最优决策树。ID3通过选择解释变量序列进行测试。一个解释变量被选中是因为它 比其他解释变量更大幅度的降低了不确定性。但是,有可能全局最优的决策并非局部最优。 在我们的例子中,决策树的规模并不重要,因为我们可以获取所有节点。但是,在现实应用中,决策 树的规模被修剪以及其他技术限制。而决策树经过修剪后的不同形状会产生不同的效果。实际上,由 信息增益和基尼不纯度启发式方法计算出的局部最优决策通常都会生成一个可行的决策树。

总结 本章我们介绍了一个非线性模型——决策树,用来解决分类和回归问题。就像猜猜看游戏一样,决策 树也是由一些了问题构成一个测试实例。决策树的一个分支在遇到显示响应变量值的叶子节点时停

树也是由一些了问题构成一个测试实例。决策树的一个分支在遇到显示响应变量值的叶子节点时停 止。我们介绍了ID3算法,用来训练决策树,通过递归分割训练集,形成子集以减低响应变量的不确 定性。我们还介绍了集成学习方法,通过将一系列模型组合起来达到更好的学习效果。最后,我们用 随机森林方法对图片是广告还是网页正文进行了预测。下一章,我们将介绍第一种非监督学习方法: 聚类。

K-Means聚类 前面几章我们介绍了监督学习,包括从带标签的数据中学习的回归和分类算法。本章,我们讨论无监 督学习算法,聚类(clustering)。聚类是用于找出不带标签数据的相似性的算法。我们将介绍KMeans聚类思想,解决一个图像压缩问题,然后对算法的效果进行评估。最后,我们把聚类和分类算 法组合起来,解决一个半监督学习问题。

在第一章,机器学习基础中,我们介绍过非监督学习的目的是从不带标签的训练数据中挖掘隐含的关 系。聚类,或称为聚类分析(cluster analysis)是一种分组观察的方法,将更具相似性的样本归为一 组,或一类(cluster),同组中的样本比其他组的样本更相似。与监督学习方法一样,我们用n维向 量表示一个观测值。例如,假设你的训练数据如下图所示:

聚类算法可能会分成两组,用圆点和方块表示,如下图所示:

也可能分成四组,如下图所示:

聚类通常用于探索数据集。社交网络可以用聚类算法识别社区,然后向没有加入社区的用户进行推 荐。在生物学领域,聚类用于找出有相似模式的基因组。推荐系统有时也利用聚类算法向用户推荐产 品和媒体资源。在市场调查中,聚类算法用来做用户分组。下面,我们就用K-Means聚类算法来为一 个数据集聚类。

K-Means聚类算法简介 由于具有出色的速度和良好的可扩展性,K-Means聚类算法算得上是最著名的聚类方法。K-Means算 法是一个重复移动类中心点的过程,把类的中心点,也称重心(centroids),移动到其包含成员的平 均位置,然后重新划分其内部成员。K 是算法计算出的超参数,表示类的数量;K-Means可以自动分 配样本到不同的类,但是不能决定究竟要分几个类。K 必须是一个比训练集样本数小的正整数。有 时,类的数量是由问题内容指定的。例如,一个鞋厂有三种新款式,它想知道每种新款式都有哪些潜

在客户,于是它调研客户,然后从数据里找出三类。也有一些问题没有指定聚类的数量,最优的聚类 数量是不确定的。后面我们会介绍一种启发式方法来估计最优聚类数量,称为肘部法则(Elbow Method)。 K-Means的参数是类的重心位置和其内部观测值的位置。与广义线性模型和决策树类似,K-Means参 数的最优解也是以成本函数最小化为目标。K-Means成本函数公式如下: k

J =

∑ ∑ k=1 i

μ

∈C

|xi

−μ

2

k

|

k

是第k 个类的重心位置。成本函数是各个类畸变程度(distortions)之和。每个类的畸变程度等于 该类重心与其内部成员位置距离的平方和。若类内部的成员彼此间越紧凑则类的畸变程度越小,反 之,若类内部的成员彼此间越分散则类的畸变程度越大。求解成本函数最小化的参数就是一个重复配 置每个类包含的观测值,并不断移动类重心的过程。首先,类的重心是随机确定的位置。实际上,重 心位置等于随机选择的观测值的位置。每次迭代的时候,K-Means会把观测值分配到离它们最近的 类,然后把重心移动到该类全部成员位置的平均值那里。让我们用如下表所示的训练数据来试验一 下: k

样本 X0 X1 1

7

5

2

5

7

3

7

7

4

3

3

5

4

6

6

1

4

7

0

0

8

2

2

9

8

7

10

6

8

11

5

5

12

3

7

上表有两个解释变量,每个样本有两个特征。画图如下所示:

In [1]: %matplotlib inline import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties font = FontProperties(fname=r"c:\windows\fonts\msyh.ttc", size=10)

In [66]: import numpy as np X0 = np.array([7, 5, 7, 3, 4, 1, 0, 2, 8, 6, 5, 3]) X1 = np.array([5, 7, 7, 3, 6, 4, 0, 2, 7, 8, 5, 7]) plt.figure() plt.axis([-1, 9, -1, 9]) plt.grid(True) plt.plot(X0, X1, 'k.');

假设K-Means初始化时,将第一个类的重心设置在第5个样本,第二个类的重心设置在第11个样本.那 么我们可以把每个实例与两个重心的距离都计算出来,将其分配到最近的类里面。计算结果如下表所 示: 样本

X0 X1 与C1距离 与C2距离 上次聚类结果 新聚类结果 是否改变

1

7

5

3.16

2.00

None

C2

YES

2

5

7

1.41

2.00

None

C1

YES

3

7

7

3.16

2.83

None

C2

YES

4

3

3

3.16

2.83

None

C2

YES

5

4

6

0.00

1.41

None

C1

YES

6

1

4

3.61

4.12

None

C1

YES

7

0

0

7.21

7.07

None

C2

YES

8

2

2

4.47

4.24

None

C2

YES

9

8

7

4.12

3.61

None

C2

YES

10

6

8

2.83

3.16

None

C1

YES

11

5

5

1.41

0.00

None

C2

YES

12

3

7

1.41

2.83

None

C1

YES

C1重心

4

6

C2重心

5

5

新的重心位置和初始聚类结果如下图所示。第一类用X表示,第二类用点表示。重心位置用稍大的点 突出显示。

In [70]: C1 = [1, 4, 5, 9, 11] C2 = list(set(range(12)) - set(C1)) X0C1, X1C1 = X0[C1], X1[C1] X0C2, X1C2 = X0[C2], X1[C2] plt.figure() plt.title('第一次迭代后聚类结果',fontproperties=font) plt.axis([-1, 9, -1, 9]) plt.grid(True) plt.plot(X0C1, X1C1, 'rx') plt.plot(X0C2, X1C2, 'g.') plt.plot(4,6,'rx',ms=12.0) plt.plot(5,5,'g.',ms=12.0);

现在我们重新计算两个类的重心,把重心移动到新位置,并重新计算各个样本与新重心的距离,并根 据距离远近为样本重新归类。结果如下表所示: 样本

X0

X1

1

7

5

3.49

2.58

C2

C2

NO

2

5

7

1.34

2.89

C1

C1

NO

3

7

7

3.26

3.75

C2

C1

YES

4

3

3

3.49

1.94

C2

C2

NO

5

4

6

0.45

1.94

C1

C1

NO

6

1

4

3.69

3.57

C1

C2

YES

7

0

0

7.44

6.17

C2

C2

NO

8

2

2

4.75

3.35

C2

C2

NO

9

8

7

4.24

4.46

C2

C1

YES

10

6

8

2.72

4.11

C1

C1

NO

与C1距离 与C2距离 上次聚类结果 新聚类结果 是否改变

11

5

5

1.84

0.96

C2

C2

NO

12

3

7

1.00

3.26

C1

C1

NO

C1重心 3.80 6.40 C2重心 4.57 4.14 画图结果如下:

In [73]: C1 = [1, 2, 4, 8, 9, 11] C2 = list(set(range(12)) - set(C1)) X0C1, X1C1 = X0[C1], X1[C1] X0C2, X1C2 = X0[C2], X1[C2] plt.figure() plt.title('第二次迭代后聚类结果',fontproperties=font) plt.axis([-1, 9, -1, 9]) plt.grid(True) plt.plot(X0C1, X1C1, 'rx') plt.plot(X0C2, X1C2, 'g.') plt.plot(3.8,6.4,'rx',ms=12.0) plt.plot(4.57,4.14,'g.',ms=12.0);

我们再重复一次上面的做法,把重心移动到新位置,并重新计算各个样本与新重心的距离,并根据距 离远近为样本重新归类。结果如下表所示: 样本

X0

X1

1

7

5

2.50

5.28

C1

C1

NO

2

5

7

0.50

5.05

C1

C1

NO

3

7

7

1.50

6.38

C1

C1

NO

4

3

3

4.72

0.82

C2

C2

NO

5

4

6

1.80

3.67

C1

C1

NO

6

1

4

5.41

1.70

C2

C2

NO

与C1距离 与C2距离 上次聚类结果 新聚类结果 是否改变

7

0

0

8.90

3.56

C2

C2

NO

8

2

2

6.10

0.82

C2

C2

NO

9

8

7

2.50

7.16

C1

C1

NO

10

6

8

1.12

6.44

C1

C1

NO

11

5

5

2.06

3.56

C2

C1

YES

12

3

7

2.50

4.28

C1

C1

NO

C1重心 5.50 7.00 C2重心 2.20 2.80 画图结果如下:

In [76]: C1 = [0, 1, 2, 4, 8, 9, 10, 11] C2 = list(set(range(12)) - set(C1)) X0C1, X1C1 = X0[C1], X1[C1] X0C2, X1C2 = X0[C2], X1[C2] plt.figure() plt.title('第三次迭代后聚类结果',fontproperties=font) plt.axis([-1, 9, -1, 9]) plt.grid(True) plt.plot(X0C1, X1C1, 'rx') plt.plot(X0C2, X1C2, 'g.') plt.plot(5.5,7.0,'rx',ms=12.0) plt.plot(2.2,2.8,'g.',ms=12.0);

再重复上面的方法就会发现类的重心不变了,K-Means会在条件满足的时候停止重复聚类过程。通 常,条件是前后两次迭代的成本函数值的差达到了限定值,或者是前后两次迭代的重心位置变化达到 了限定值。如果这些停止条件足够小,K-Means就能找到最优解。不过这个最优解不一定是全局最优 解。

局部最优解

局部最优解 前面介绍过K-Means的初始重心位置是随机选择的。有时,如果运气不好,随机选择的重心会导致KMeans陷入局部最优解。例如,K-Means初始重心位置如下图所示:

K-Means最终会得到一个局部最优解,如下图所示。这些类可能没有实际意义,而上面和下面两部分 观测值可能是更有合理的聚类结果。为了避免局部最优解,K-Means通常初始时要重复运行十几次甚 至上百次。每次重复时,它会随机的从不同的位置开始初始化。最后把最小的成本函数对应的重心位 置作为初始化位置。

肘部法则 如果问题中没有指定K 的值,可以通过肘部法则这一技术来估计聚类数量。肘部法则会把不同K 值的 成本函数值画出来。随着K 值的增大,平均畸变程度会减小;每个类包含的样本数会减少,于是样本 离其重心会更近。但是,随着K 值继续增大,平均畸变程度的改善效果会不断减低。K 值增大过程 中,畸变程度的改善效果下降幅度最大的位置对应的K 值就是肘部。下面让我们用肘部法则来确定最 佳的K 值。下图数据明显可分成两类:

In [103]: import numpy as np cluster1 = np.random.uniform(0.5, 1.5, (2, 10)) cluster2 = np.random.uniform(3.5, 4.5, (2, 10)) X = np.hstack((cluster1, cluster2)).T plt.figure() plt.axis([0, 5, 0, 5]) plt.grid(True) plt.plot(X[:,0],X[:,1],'k.');

我们计算K 值从1到10对应的平均畸变程度:

In [104]: from sklearn.cluster import KMeans from scipy.spatial.distance import cdist K = range(1, 10) meandistortions = [] for k in K: kmeans = KMeans(n_clusters=k) kmeans.fit(X) meandistortions.append(sum(np.min(cdist(X, kmeans.cluster_centers_, ' euclidean'), axis=1)) / X.shape[0]) plt.plot(K, meandistortions, 'bx-') plt.xlabel('k') plt.ylabel('平均畸变程度',fontproperties=font) plt.title('用肘部法则来确定最佳的K值',fontproperties=font);

从图中可以看出,K 值从1到2时,平均畸变程度变化最大。超过2以后,平均畸变程度变化显著降 低。因此肘部就是K = 2 。下面我们再用肘部法则来确定3个类的最佳的K值:

In [136]: import numpy as np x1 = np.array([1, 2, 3, 1, 5, 6, 5, 5, 6, 7, 8, 9, 7, 9]) x2 = np.array([1, 3, 2, 2, 8, 6, 7, 6, 7, 1, 2, 1, 1, 3]) X = np.array(list(zip(x1, x2))).reshape(len(x1), 2) plt.figure() plt.axis([0, 10, 0, 10]) plt.grid(True) plt.plot(X[:,0],X[:,1],'k.');

In [137]: from sklearn.cluster import KMeans from scipy.spatial.distance import cdist K = range(1, 10) meandistortions = [] for k in K: kmeans = KMeans(n_clusters=k) kmeans.fit(X) meandistortions.append(sum(np.min(cdist(X, kmeans.cluster_centers_, ' euclidean'), axis=1)) / X.shape[0]) plt.plot(K, meandistortions, 'bx-') plt.xlabel('k') plt.ylabel('平均畸变程度',fontproperties=font) plt.title('用肘部法则来确定最佳的K值',fontproperties=font);

从图中可以看出,K 值从1到3时,平均畸变程度变化最大。超过3以后,平均畸变程度变化显著降 低。因此肘部就是K = 3 。

聚类效果评估 我们把机器学习定义为对系统的设计和学习,通过对经验数据的学习,将任务效果的不断改善作为一 个度量标准。K-Means是一种非监督学习,没有标签和其他信息来比较聚类结果。但是,我们还是有 一些指标可以评估算法的性能。我们已经介绍过类的畸变程度的度量方法。本节为将介绍另一种聚类 算法效果评估方法称为轮廓系数(Silhouette Coefficient)。轮廓系数是类的密集与分散程度的评价 指标。它会随着类的规模增大而增大。彼此相距很远,本身很密集的类,其轮廓系数较大,彼此集 中,本身很大的类,其轮廓系数较小。轮廓系数是通过所有样本计算出来的,计算每个样本分数的均 值,计算公式如下: ba s = max(a, b)

是每一个类中样本彼此距离的均值,b 是一个类中样本与其最近的那个类的所有样本的距离的均 值。下面的例子运行四次K-Means,从一个数据集中分别创建2,3,4,8个类,然后分别计算它们 的轮廓系数。 a

In [134]: import numpy as np from sklearn.cluster import KMeans from sklearn import metrics plt.figure(figsize=(8, 10)) plt.subplot(3, 2, 1) x1 = np.array([1, 2, 3, 1, 5, 6, 5, 5, 6, 7, 8, 9, 7, 9]) x2 = np.array([1, 3, 2, 2, 8, 6, 7, 6, 7, 1, 2, 1, 1, 3]) X = np.array(list(zip(x1, x2))).reshape(len(x1), 2) plt.xlim([0, 10]) plt.ylim([0, 10]) plt.title('样本',fontproperties=font) plt.scatter(x1, x2) colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'b'] markers = ['o', 's', 'D', 'v', '^', 'p', '*', '+'] tests = [2, 3, 4, 5, 8] subplot_counter = 1 for t in tests: subplot_counter += 1 plt.subplot(3, 2, subplot_counter) kmeans_model = KMeans(n_clusters=t).fit(X) for i, l in enumerate(kmeans_model.labels_): plt.plot(x1[i], x2[i], color=colors[l], marker=markers[l],ls='Non e') plt.xlim([0, 10]) plt.ylim([0, 10]) plt.title('K = %s, 轮廓系数 = %.03f' % (t, metrics.silhouette_scor e(X, kmeans_model.labels_,metric='euclidean')),fontproperties=font) d:\programfiles\Miniconda3\lib\site-packages\numpy\core\_method s.py:59: RuntimeWarning: Mean of empty slice. warnings.warn("Mean of empty slice.", RuntimeWarning)

很显然,这个数据集包括三个类。在K = 3 的时候轮廓系数是最大的。在K = 8 的时候,每个类的 样本不仅彼此很接近,而且与其他类的样本也非常接近,因此这时轮廓系数是最小的。

图像量化 前面我们用聚类算法探索了结构化数据集。现在我们用它来解决一个新问题。图像量化(image quantization)是一种将图像中相似颜色替换成同样颜色的有损压缩方法。图像量化会减少图像的存 储空间,由于表示不同颜色的字节减少了。下面的例子中,我们将用聚类方法从一张图片中找出包含 图片大多数颜色的压缩颜色调色板(palette),然后我们用这个压缩颜色调色板重新生成图片。这个 例子需要用mahotas图像处理库,可以通过pip install mahotas安装:

In [2]: import numpy as np from sklearn.cluster import KMeans from sklearn.utils import shuffle import mahotas as mh 首先,读入图片,然后将图片矩阵展开成一个行向量。

In [9]: original_img = np.array(mh.imread('mlslpic\6.6 tree.png'), dtype=np.float 64) / 255 original_dimensions = tuple(original_img.shape) width, height, depth = tuple(original_img.shape) image_flattened = np.reshape(original_img, (width * height, depth)) 然后我们用K-Means算法在随机选择1000个颜色样本中建立64个类。每个类都可能是压缩调色板中 的一种颜色。

In [10]: image_array_sample = shuffle(image_flattened, random_state=0)[:1000] estimator = KMeans(n_clusters=64, random_state=0) estimator.fit(image_array_sample) Out[10]: KMeans(copy_x=True, init='k-means++', max_iter=300, n_clusters= 64, n_init=10, n_jobs=1, precompute_distances='auto', random_state=0, tol= 0.0001, verbose=0) 之后,我们为原始图片的每个像素进行类的分配。

In [11]: cluster_assignments = estimator.predict(image_flattened) 最后,我们建立通过压缩调色板和类分配结果创建压缩后的图片:

In [12]: compressed_palette = estimator.cluster_centers_ compressed_img = np.zeros((width, height, compressed_palette.shape[1])) label_idx = 0 for i in range(width): for j in range(height): compressed_img[i][j] = compressed_palette[cluster_assignments[lab el_idx]] label_idx += 1 plt.subplot(122) plt.title('Original Image') plt.imshow(original_img) plt.axis('off') plt.subplot(121) plt.title('Compressed Image') plt.imshow(compressed_img) plt.axis('off') plt.show()

通过聚类学习特征 在下面的例子中,我们将聚类和分类组合起来研究一个半监督学习问题。你将对不带标签的数据进行 聚类,获得一些特征,然后用这些特征来建立一个监督方法分类器。 假设你有一只猫和一条狗。再假设你买了一个智能手机,表面上看是用来给人打电话的,其实你只是 用来给猫和狗拍照。你的照片拍得很棒,因此你觉得你的朋友和同事一定会喜欢它们。而你知道一部 分人只想看猫的照片,一部分人只想看狗的照片,但是要把这些照片分类太麻烦了。下面我们就做一 个半监督学习系统来分别猫和狗的照片。 回想一下第三章,特征抽取与处理的内容,有一个原始的方法来给图片分类,是用图片的像素密度值 或亮度值作为解释变量。和我们前面进行文本处理时的高维向量不同,图片的特征向量不是稀疏的。 另外,这个方法对图片的亮度,尺寸,旋转的变化都十分敏感。在第三章,特征抽取与处理里面,我 们还介绍了SIFT和SURF描述器,用来描述图片的兴趣点,这类方法对图片的亮度,尺寸,旋转变化 都不敏感。在下面的例子中,我们将用聚类算法处理这些描述器来学习图片的特征。每个元素将被编 码成从图片中抽取的,被分配到同一个类的描述器的数量。这种方法有时也称为视觉词袋(bag-offeatures)表示法,由于这个类的集合与词袋模型里的词汇表类似。我们将使用Kaggle's Dogs vs.

Cats competition (https://www.kaggle.com/c/dogs-vs-cats/data)里面的1000张猫图片和1000张狗 图片数据。注意,图片有不同的尺寸;由于我们的特征向量不用像素表示,所有我们也不需要将所有 图片都缩放成同样的尺寸。我们将训练其中60的图片,然后用剩下的40图片来测试:

In [ ]: import numpy as np import mahotas as mh from mahotas.features import surf from sklearn.linear_model import LogisticRegression from sklearn.metrics import * from sklearn.cluster import MiniBatchKMeans import glob 首先,我们加载图片,转换成灰度图,再抽取SURF描述器。SURF描述器与其他类似的特征相比, 可以更快的被提取,但是从2000张图片中抽取描述器依然是很费时间的。

In [ ]: all_instance_filenames = [] all_instance_targets = [] for f in glob.glob('cats-and-dogs-img/*.jpg'): target = 1 if 'cat' in f else 0 all_instance_filenames.append(f) all_instance_targets.append(target) surf_features = [] counter = 0 for f in all_instance_filenames: print('Reading image:', f) image = mh.imread(f, as_grey=True) surf_features.append(surf.surf(image)[:, 5:]) train_len = int(len(all_instance_filenames) * .60) X_train_surf_features = np.concatenate(surf_features[:train_len]) X_test_surf_feautres = np.concatenate(surf_features[train_len:]) y_train = all_instance_targets[:train_len] y_test = all_instance_targets[train_len:] 然后我们把抽取的描述器分成300个类。用MiniBatchKMeans类实现,它是K-Means算法的变种, 每次迭代都随机抽取样本。由于每次迭代它只计算这些被随机抽取的一小部分样本与重心的距离,因 此MiniBatchKMeans可以更快的聚类,但是它的畸变程度会更大。实际上,计算结果差不多:

In [ ]: n_clusters = 300 print('Clustering', len(X_train_surf_features), 'features') estimator = MiniBatchKMeans(n_clusters=n_clusters) estimator.fit_transform(X_train_surf_features) 之后我们为训练集和测试集构建特征向量。我们找出每一个SURF描述器的类,用Numpy的 binCount()进行计数。下面的代码为每个样本生成一个300维的特征向量:

In [ ]: X_train = [] for instance in surf_features[:train_len]: clusters = estimator.predict(instance) features = np.bincount(clusters) if len(features) < n_clusters: features = np.append(features, np.zeros((1, n_clusterslen(features))) ) X_train.append(features) X_test = [] for instance in surf_features[train_len:]: clusters = estimator.predict(instance) features = np.bincount(clusters) if len(features) < n_clusters: features = np.append(features, np.zeros((1, n_clusterslen(features))) ) X_test.append(features) 最后,我们在特征向量和目标上训练一个逻辑回归分类器,然后估计它的精确率,召回率和准确率:

In [ ]: clf = LogisticRegression(C=0.001, penalty='l2') clf.fit_transform(X_train, y_train) predictions = clf.predict(X_test) print(classification_report(y_test, predictions)) print('Precision: ', precision_score(y_test, predictions)) print('Recall: ', recall_score(y_test, predictions)) print('Accuracy: ', accuracy_score(y_test, predictions)) 半监督学习系统仅仅使用像素密度作为特征向量,就获得了比逻辑回归分类器更好的精确率和召回 率。而且,我们的特征向量只有300维,比100x100像素图片的10000维要小得多。

总结 本章,我们介绍了我们的第一个无监督学习方法:聚类。聚类是用来探索无标签数据的结构的。我们 介绍了K-Means聚类算法,重复将样本分配的类里面,不断的更新类的重心位置。虽然K-Means是无 监督学习方法,其效果依然是可以度量的;用畸变程度和轮廓系数可以评估聚类效果。我们用KMeans研究了两个问题。第一个问题是图像量化,一种用单一颜色表示一组相似颜色的图像压缩技 术。我们还用K-Means研究了半监督图像分类问题的特征。 下一章,我们将介绍另一种无监督学习任务——降维(dimensionality reduction)。和我们前面介绍 过的半监督猫和狗图像分类问题类似,降维算法可以在尽量保留信息完整性的同时,降低解释变量集 合的维度。

用PCA降维 本章我们将介绍一种降维方法,PCA(Principal Component Analysis,主成分分析)。降维致力于 解决三类问题。第一,降维可以缓解维度灾难问题。第二,降维可以在压缩数据的同时让信息损失最 小化。第三,理解几百个维度的数据结构很困难,两三个维度的数据通过可视化更容易理解。下面, 我们用PCA将一个高维数据降成二维,方便可视化,之后,我们建一个脸部识别系统。

PCA简介 在第三章,特征提取与处理里面,涉及高维特征向量的问题往往容易陷入维度灾难。随着数据集维度 的增加,算法学习需要的样本数量呈指数级增加。有些应用中,遇到这样的大数据是非常不利的,而 且从大数据集中学习需要更多的内存和处理能力。另外,随着维度的增加,数据的稀疏性会越来越 高。在高维向量空间中探索同样的数据集比在同样稀疏的数据集中探索更加困难。 主成分分析也称为卡尔胡宁-勒夫变换(Karhunen-Loeve Transform),是一种用于探索高维数据结 构的技术。PCA通常用于高维数据集的探索与可视化。还可以用于数据压缩,数据预处理等。PCA可 以把可能具有相关性的高维变量合成线性无关的低维变量,称为主成分( principal components)。 新的低维数据集会经可能的保留原始数据的变量。 PCA将数据投射到一个低维子空间实现降维。例如,二维数据集降维就是把点投射成一条线,数据集 的每个样本都可以用一个值表示,不需要两个值。三维数据集可以降成二维,就是把变量映射成一个 平面。一般情况下,n 维数据集可以通过映射降成k 维子空间,其中$k 假如你是一本养花工具宣传册的摄影师,你正在拍摄一个水壶。水壶是三维的,但是照片是二维的, 为了更全面的把水壶展示给客户,你需要从不同角度拍几张图片。下图是你从四个方向拍的照片:

第一张图里水壶的背面可以看到,但是看不到前面。第二张图是拍前面,可以看到壶嘴,这张图可以 提供了第一张图缺失的信息,但是壶把看不到了。从第三张俯视图里无法看出壶的高度。第四张图是 你打算放进目录的,水壶的高度,顶部,壶嘴和壶把都清晰可见。 PCA的设计理念与此类似,它可以将高维数据集映射到低维空间的同时,尽可能的保留更多变量。 PCA旋转数据集与其主成分对齐,将最多的变量保留到第一主成分中。假设我们有下图所示的数据 集:

数据集看起来像一个从原点到右上角延伸的细长扁平的椭圆。要降低整个数据集的维度,我们必须把 点映射成一条线。下图中的两条线都是数据集可以映射的,映射到哪条线样本变化最大?

显然,样本映射到虚线的变化比映射到点线的变化。实际上,这条虚线就是第一主成分。第二主成分 必须与第一主成分正交,也就是说第二主成分必须是在统计学上独立的,会出现在与第一主成分垂直 的方向,如下图所示:

后面的每个主成分也会尽量多的保留剩下的变量,唯一的要求就是每一个主成分需要和前面的主成分 正交。 现在假设数据集是三维的,散点图看起来像是沿着一个轴旋转的光盘。

这些点可以通过旋转和变换使光盘完全变成二维的。现在这些点看着像一个椭圆,第三维上基本没有 变量,可以被忽略。 当数据集不同维度上的方差分布不均匀的时候,PCA最有用。如果是一个球壳行数据集,PCA不能有 效的发挥作用,因为各个方向上的方差都相等;没有丢失大量的信息维度一个都不能忽略。

PCA计算步骤 在介绍PCA的运行步骤之前,有一些术语需要说明一下。

方差,协方差和协方差矩阵 方差(Variance)是度量一组数据分散的程度。方差是各个样本与样本均值的差的平方和的均值:



n

s

2

=

i=1

(Xi

n

− X¯ )

2

−1

协方差(Covariance)是度量两个变量的变动的同步程度,也就是度量两个变量线性相关性程度。如 果两个变量的协方差为0,则统计学上认为二者线性无关。注意两个无关的变量并非完全独立,只是 没有线性相关性而已。计算公式如下:



n

(



¯ )(



¯)



n

cov(X , Y ) =

i=1

(Xi

− X¯ )(X − Y¯ ) n − 1 i

如果协方差不为0,如果大于0表示正相关,小于0表示负相关。当协方差大于0时,一个变量增大是 另一个变量也会增大。当协方差小于0时,一个变量增大是另一个变量会减小。协方差矩阵 (Covariance matrix)由数据集中两两变量的协方差组成。矩阵的第(i, j) 个元素是数据集中第i和第j 个元素的协方差。例如,三维数据的协方差矩阵如下所示: ⎡ cov(x , x ) 1 1 C =

cov(x1 , x2 )

cov(x1 , x3 ) ⎤

cov(x2 , x1 )

cov(x2 , x2 )

cov(x2 , x3 )

⎣ cov(x , x ) 3 1

cov(x3 , x2 )

cov(x3 , x3 ) ⎦

⎢ ⎢

⎥ ⎥

让我们计算下表数据的协方差矩阵: X1 X2 2

0

X3 −1.4

2.2 0.2 −1.5 2.4 0.1 1.9

0

−1 −1.2

三个变量的样本均值分别是2.125,0.075和-1.275。用Numpy计算协方差矩阵如下:

In [1]: import numpy as np X = [[2, 0, -1.4], [2.2, 0.2, -1.5], [2.4, 0.1, -1], [1.9, 0, -1.2]] print(np.cov(np.array(X).T)) [[ 0.04916667 0.01416667 0.01916667] [ 0.01416667 0.00916667 -0.00583333] [ 0.01916667 -0.00583333 0.04916667]]

特征向量和特征值 向量是具有大小(magnitude)和方向(direction)的几何概念。特征向量(eigenvector)是一个矩 阵的满足如下公式的非零向量: A



ν ⃗ = λν ⃗

其中,ν 是特征向量,A 是方阵,λ 是特征值。经过A 变换之后,特征向量的方向保持不变,只是其大 小发生了特征值倍数的变化。也就是说,一个特征向量左乘一个矩阵之后等于等比例放缩(scaling) 特征向量。德语单词eigen的意思是属于...或...专有( belonging to or peculiar to);矩阵的特征向量 是属于并描述数据集结构的向量。

特征向量和特征值只能由方阵得出,且并非所有方阵都有特征向量和特征值。如果一个矩阵有特征向 量和特征值,那么它的每个维度都有一对特征向量和特征值。矩阵的主成分是其协方差矩阵的特征向 量,按照对应的特征值大小排序。最大的特征值就是第一主成分,第二大的特征值就是第二主成分, 以此类推。 让我们来计算下面矩阵的特征向量和特征值: 1 A =

[2

−2 −3 ]

根据前面的公式A 乘以特征向量,必然等于特征值乘以特征向量。我们建立特征方程求解:

− λI )ν⃗ = 0 ∣ 1 −2 λ |A − λ ∗ I | = ∣ − ∣ [ 2 −3 ] [ 0 (A

∣ ∣=0 λ ]∣ 0

从特征方程可以看出,矩阵与单位矩阵和特征值乘积的矩阵行列式为0:

∣ 1−λ ∣ ∣[ 2

∣ −2 ∣ = (λ + 1)(λ + 1) = 0 −3 − λ ] ∣

矩阵的两个特征值都等于-1。现在再用特征值来解特征向量。 A

ν ⃗ = λν ⃗

首先,我们用特征方程: (A

− λI )ν⃗ = 0

把数据带入: 1 ( [2

−2 λ − −3 ] [ 0

0

λ ])

ν⃗ =

1 [

−λ 2

−2 1 − λ ν⃗ = [ −3 − λ ] 2

ν −2 −3 − λ ] [ ν

1,1

1,2

]

= 0

我们把特征值代入方程: 1 [

− (−1) 2

ν −2 −3 − (−1) ] [ ν

2

1,1

1,2

]

=

[2

ν −2 −2 ] [ ν

1,1

1,2

]

= 0

可以重新整理成如下方程: ⧵begin{Bmatrix} 2⧵nu{1,1} + -(2\nu{1,2})=0 ⧵ 2⧵nu{1,1} + -(2\nu{1,2})=0 ⧵ ⧵end{Bmatrix} 任何满足方程的非零向量都可以作为特征向量: $$ ⧵begin{bmatrix} 1 & -2 ⧵ 2 & -3 ⧵ ⧵end{bmatrix} ⧵begin{bmatrix} 1 ⧵ 1 ⧵

⧵end{bmatrix} 1 [1]

= [

$$

−1  − 1  ]

PCA需要单位特征向量,也就是L2范数等于1的特征向量:

∥x∥

=



‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ ⋯ ‾ x

2

1

+ x

2

2

2

+

+ xn

那么把前面的特征向量带入可得:

∥ 1 ∥ ∥ ∥ ∥ [1] ∥

=

√‾1‾‾‾‾‾ + 1‾ = √2 ‾ 2

2

于是单位特征向量是:



1 [1]

0.70710678

/√2 =

[ 0.70710678 ]

我们可以通过Numpy检验我们手算的特征向量。eig函数返回特征值和特征向量的元组:

In [8]: import numpy as np w, v = np.linalg.eig(np.array([[1, -2], [2, -3]])) print('特征值:{}\n特征向量:{}'.format(w,v)) 特征值:[-0.99999998 -1.00000002] 特征向量:[[ 0.70710678 0.70710678] [ 0.70710678 0.70710678]]

用PCA降维 让我们用PCA方法把下表二维数据降成一维: X1 X2 0.9

1

2.4 2.6 1.2 1.7 0.5 0.7 0.3 0.7 1.8 1.4 0.5 0.6 0.3 0.6 2.5 2.6 1.3 1.1 PCA第一步是用解释变量减去样本均值: X1

X2

0.9 - 1.17 = -0.27

1 - 1.3 = -0.3

2.4 - 1.17 = 1.23

2.6 - 1.3 = 1.3

1.2 - 1.17 = 0.03

1.7 - 1.3 = 0.4

0.5 - 1.17 = -0.67 -0.7 - 1.3 = 0.6 0.3 - 1.17 = -0.87 -0.7 - 1.3 = 0.6 1.8 - 1.17 = 0.63

1.4 - 1.3 = 0.1

0.5 - 1.17 = -0.67 0.6 - 1.3 = -0.7 0.3 - 1.17 = -0.87 0.6 - 1.3 = -0.7 2.5 - 1.17 = 1.33

2.6 - 1.3 = 1.3

1.3 - 1.17 = 0.13 1.1 - 1.3 = -0.2 然后,我们计算数据的主成分。前面介绍过,矩阵的主成分是其协方差矩阵的特征向量,按照对应的 特征值大小排序。主成分可以通过两种方法计算。第一种方法是计算数据协方差矩阵。因为协方差矩 阵是方阵,所有我们可以用前面的方法计算特征值和特征向量。第二种方法是用数据矩阵的奇异值分 解(singular value decomposition)来找协方差矩阵的特征向量和特征值的平方根。我们先介绍第一 种方法,然后介绍scikit-learn的PCA实现,也就是第二种方法。上述数据集的解释变量协方差矩阵如 下: 0.6867777778 C =

[ 0.6066666667

0.6066666667 0.5977777778 ]

用前面介绍过的方法,特征值是1.250和0.034,单位特征向量是: 0.73251454 [ 0.68075138

0.68075138 0.73251454 ]

下面我们把数据映射到主成分上。第一主成分是最大特征值对应的特征向量,因此我们要建一个转换 矩阵,它的每一列都是主成分的特征向量。如果我们要把5维数据降成3维,那么我们就要用一个3维 矩阵做转换矩阵。在本例中,我们将把我们的二维数据映射成一维,因此我们只需要用特征向量中的 第一主成分。最后,我们用数据矩阵点乘转换矩阵。下面就是第一主成分映射的结果: ⎡

−0.27

−0.3 ⎤







⎢ ⎢ ⎢

1.23

1.3





0.03

0.4 ⎥ ⎥ 0.6 ⎥





−0.67 ⎢ −0.87 ⎢ ⎢ ⎢

0.63

−0.67 ⎢ −0.87 ⎢ ⎢ ⎢ ⎢ ⎣

1.33 0.13

0.6

⎥ ⎥

0.73251454



= ⎢ ⎢









通过Numpy的dot函数计算如下:



−0.08233391 ⎥ ⎢ −0.22883682 ⎥

−0.7 ⎥ −0.7 ⎥ −0.2 ⎦

0.29427599







1.3

⎥ 1.78596968



0.1 ⎥ [ 0.68075138 ] ⎥

−0.40200434 ⎤



0.5295593



−0.96731071 ⎥ ⎥ ⎢ −1.11381362 ⎥ ⎢



1.85922113

⎥ ⎥

−0.04092339 ⎦

In [12]: a = [[-0.27, -0.3], [1.23, 1.3], [0.03, 0.4], [-0.67, 0.6], [-0.87, 0.6], [0.63, 0.1], [-0.67, -0.7], [-0.87, -0.7], [1.33, 1.3], [0.13, -0.2]] b = [[0.73251454], [0.68075138]] np.dot(a,b) Out[12]: array([[-0.40200434], [ 1.78596968], [ 0.29427599], [-0.08233391], [-0.22883682], [ 0.5295593 ], [-0.96731071], [-1.11381362], [ 1.85922113], [-0.04092339]]) 许多PCA的实现方法,包括scikit-learn的实现方法都是用奇异值分解计算特征值和特征向量。SVD计 算公式如下: X = U



V

T

列向量U 称为数据矩阵的左奇异值向量,V 称为数据矩阵的右奇异值向量,∑ 的对角线元素是它的奇 异值。矩阵的奇异值向量和奇异值在一些信号处理和统计学中是十分有用的,我们只对它们与数据矩 阵特征向量和特征值相关的内容感兴趣。具体来说,左奇异值向量就是协方差矩阵的特征向量,∑ 的对角线元素是协方差矩阵的特征值的平方根。计算SVD超出本书范围,不过用SVD找特征向量的方 法与通过协方差矩阵解析方法类似,详细内容见线性代数教程。

用PCA实现高维数据可视化 二维或三维数据更容易通过可视化发现模式。一个高维数据集是无法用图形表示的,但是我们可以通 过降维方法把它降成二维或三维数据来可视化。 Fisher1936年收集了三种鸢尾花分别50个样本数据(Iris Data):Setosa、Virginica、Versicolour。 解释变量是花瓣(petals)和萼片(sepals)长度和宽度的测量值,响应变量是花的种类。鸢尾花数 据集经常用于分类模型测试,scikit-learn中也有。让我们把iris数据集降成方便可视化的二维数 据:

In [5]: %matplotlib inline import matplotlib.pyplot as plt from sklearn.decomposition import PCA from sklearn.datasets import load_iris 首先,我们导入鸢尾花数据集和PCA估计器。PCA类把主成分的数量作为超参数,和其他估计器一 样,PCA也用fit_transform()返回降维的数据矩阵:

In [6]: data = load_iris() y = data.target X = data.data pca = PCA(n_components=2) reduced_X = pca.fit_transform(X) 最后,我们把图形画出来:

In [9]: red_x, red_y = [], [] blue_x, blue_y = [], [] green_x, green_y = [], [] for i in range(len(reduced_X)): if y[i] == 0: red_x.append(reduced_X[i][0]) red_y.append(reduced_X[i][1]) elif y[i] == 1: blue_x.append(reduced_X[i][0]) blue_y.append(reduced_X[i][1]) else: green_x.append(reduced_X[i][0]) green_y.append(reduced_X[i][1]) plt.scatter(red_x, red_y, c='r', marker='x') plt.scatter(blue_x, blue_y, c='b', marker='D') plt.scatter(green_x, green_y, c='g', marker='.') plt.show()

降维的数据如上图所示。每个数据集中三个类都用不同的符号标记。从这个二维数据图中可以明显看 出,有一个类与其他两个重叠的类完全分离。这个结果可以帮助我们选择分类模型。

PCA脸部识别 现在让我们用PCA来解决一个脸部识别问题。脸部识别是一个监督分类任务,用于从照片中认出某个 人。本例中,我们用剑桥大学AT&T实验室的Our Database of Faces数据集 (http://www.cl.cam.ac.uk/research/dtg/attarchive/facedatabase.html),这个数据集包含40个人每个 人10张照片。这些照片是在不同的光照条件下拍摄的,每张照片的表情也不同。照片都是黑白的,尺 寸为92 x 112像素。虽然这些图片都不大,但是每张图片的按像素强度排列的特征向量也有10304 维。这些高维数据的训练可能需要很多样本才能避免拟合过度。而我们样本量并不大,所有我们用 PCA计算一些主成分来表示这些照片。

我们可以把照片的像素强度矩阵转换成向量,然后用所有的训练照片的向量建一个矩阵。每个照片都 是数据集主成分的线性组合。在脸部识别理论中,这些主成分称为特征脸(eigenfaces)。特征脸可 以看成是脸部的标准化组成部分。数据集中的每张脸都可以通过一些标准脸的组合生成出来,或者说 是最重要的特征脸线性组合的近似值。

In [1]: from os import walk, path import numpy as np import mahotas as mh from sklearn.cross_validation import train_test_split from sklearn.cross_validation import cross_val_score from sklearn.preprocessing import scale from sklearn.decomposition import PCA from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report X = [] y = [] 下面我们把照片导入Numpy数组,然后把它们的像素矩阵转换成向量:

In [6]: for dir_path, dir_names, file_names in walk('mlslpic/att-faces/'): for fn in file_names: if fn[-3:] == 'pgm': image_filename = path.join(dir_path, fn) X.append(scale(mh.imread(image_filename, as_grey=True).reshap e(10304).astype('float32'))) y.append(dir_path) X = np.array(X) 然后,我们用交叉检验建立训练集和测试集,在训练集上用PCA:

In [3]: X_train, X_test, y_train, y_test = train_test_split(X, y) pca = PCA(n_components=150) 我们把所有样本降到150维,然后训练一个逻辑回归分类器。数据集包括40个类;scikit-learn底层会 自动用one versus all策略创建二元分类器:

In [4]: X_train_reduced = pca.fit_transform(X_train) X_test_reduced = pca.transform(X_test) print('训练集数据的原始维度是:{}'.format(X_train.shape)) print('PCA降维后训练集数据是:{}'.format(X_train_reduced.shape)) classifier = LogisticRegression() accuracies = cross_val_score(classifier, X_train_reduced, y_train) 训练集数据的原始维度是:(300, 10304) PCA降维后训练集数据是:(300, 150) 最后,我们用交叉验证和测试集评估分类器的性能。分类器的平均综合评价指标(F1 score)是 0.88,但是需要花费更多的时间训练,在更多训练实例的应用中可能会更慢。

In [7]: print('交叉验证准确率是:{}\n{}'.format(np.mean(accuracies), accuracies)) classifier.fit(X_train_reduced, y_train) predictions = classifier.predict(X_test_reduced) print(classification_report(y_test, predictions)) 交叉验证准确率是:0.823104855161 [ 0.84210526 0.79 0.8372093 ] precision recall f1-score mlslpic/att-faces/s1 mlslpic/att-faces/s10 mlslpic/att-faces/s11 mlslpic/att-faces/s12 mlslpic/att-faces/s13 mlslpic/att-faces/s14 mlslpic/att-faces/s15 mlslpic/att-faces/s17 mlslpic/att-faces/s18 mlslpic/att-faces/s19 mlslpic/att-faces/s2 mlslpic/att-faces/s20 mlslpic/att-faces/s21 mlslpic/att-faces/s22 mlslpic/att-faces/s23 mlslpic/att-faces/s24 mlslpic/att-faces/s25 mlslpic/att-faces/s26 mlslpic/att-faces/s27 mlslpic/att-faces/s28 mlslpic/att-faces/s29 mlslpic/att-faces/s3 mlslpic/att-faces/s30 mlslpic/att-faces/s31 mlslpic/att-faces/s32 mlslpic/att-faces/s33 mlslpic/att-faces/s34 mlslpic/att-faces/s35 mlslpic/att-faces/s36 mlslpic/att-faces/s37 mlslpic/att-faces/s38 mlslpic/att-faces/s39 mlslpic/att-faces/s4 mlslpic/att-faces/s40 mlslpic/att-faces/s5 mlslpic/att-faces/s6 mlslpic/att-faces/s7 mlslpic/att-faces/s8 mlslpic/att-faces/s9 avg / total

总结

0.93

support

1.00 1.00 1.00 1.00 1.00 0.33 1.00 1.00 1.00 1.00 0.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.00 1.00 1.00 1.00 0.75 1.00 0.00 0.75 1.00 0.50 1.00 1.00 1.00 1.00 0.00 0.80 1.00 1.00 1.00 1.00

1.00 1.00 0.83 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.00 0.50 1.00 1.00 1.00 0.75 0.00 1.00 1.00 1.00 0.17 1.00 1.00 1.00 0.00 0.80 1.00 1.00 1.00 1.00

1.00 1.00 0.91 1.00 1.00 0.50 1.00 1.00 1.00 1.00 0.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 1.00 0.00 0.67 1.00 1.00 0.86 0.86 0.00 0.86 1.00 0.67 0.29 1.00 1.00 1.00 0.00 0.80 1.00 1.00 1.00 1.00

0.88

0.88

100

1 2 6 2 3 2 4 2 2 2 0 2 3 3 1 3 4 4 3 1 2 3 3 3 4 1 3 2 1 6 2 2 1 1 5 2 2 4 3

本章,我们介绍了降维问题。高维数据不能轻易可视化。估计器训练高维数据集时,也可能出现维度 灾难。我们通过主成分分析法缓解这些问题,将可能解释变量具有相关性的高维数据集,通过将数据 映射到一个低维子空间,降维成一个线性无关的低维数据集。我们用主成分分析将四维的鸢尾花数据 集降成二维数据进行可视化,还建立了一个脸部识别系统。下一章,我们将回到监督学习方法,介绍 一种分类算法——感知器(perceptron),本书的最后两章都是建立在感知器的基础上。

感知器 前面,我们介绍了广义线性模型,用联接方程描述解释变量、超参数和响应变量的线性关系。这一 章,我们将介绍另一种线性模型,称为感知器(perceptron)。感知器是一种研究单个训练样本的二 元分类器,训练较大的数据集很有用。而且,感知器和它的不足激发了我们后面两种将介绍的模型。

感知器是Frank Rosenblatt在1957年就职于Cornell航空实验室(Cornell Aeronautical Laboratory)时发 明的,其灵感源自于对人脑的仿真。大脑是由处理信息的神经元(neurons)细胞和连接神经元细胞 进行信息传递的突触(synapses)构成。据说人脑有1千亿神经元和10万亿突触构成。神经元的组成 如下图所示,主要包括树突(Dendrites),细胞核(Cell Body)和轴突(Axon)。树突从一个神经 元接受电信号。信号在细胞核里处理,然后通过轴突将处理过的信号传递给另一个神经元。

一个神经元可以看作是将一个或多个输入处理成一个输出的计算单元。一个感知器函数类似于一个神 经元;它接受一个或多个输入,处理他们然后返回一个输出。咋看这样的模型,就像人脑千亿神经元 的一个孤胆英雄,无用武之地。但是,有两个理由使得我们有必要介绍它。首先,神经元可以实时 (online),错误驱动(error-driven)的学习,神经元可以通过一一个的训练样本不断更新参数,而非一次 性使用整套数据。实时学习可能有效的处理内存无法容纳的大数据。其次,理解感知器的工作原理是 后两章算法学习的基础,包括支持向量机(support vector machines)和人工神经网络(artificial neural networks)。感知器通常用下面的图形表示:

,x2 和x3 是输入单元。每个输入单元分别代表一个特征。感知器通常用另外一个输入单元代表一 个常用误差项,但是这个输入单元在图形中通常被忽略了。中间的圆圈是一个计算单元,类似神经元 的细胞核。连接输入单元和计算单元的边类似于树突。每条边是一个权重,或者是一个参数。参数容 易解释,如果某个解释变量与阳性类型(positive class)相关,其权重为正,某个解释变量与阴性类 型(negative class)相关,其权重为负。连接计算单元和输出单元的边类似轴突。 x1

激励函数 感知器通过使用激励函数(activation function )处理解释变量和模型参数的线性组合对样本分类, 计算公式如下所示。解释变量和模型参数的线性组合有时也称为感知器的预激励(preactivation)。 n

y =

ϕ(∑ w x i

+ b)

i

i=1

其中,wi 是模型参数,b 是常误差项,ϕ() 是激励方程。常用的激励方程有几种。Rosenblatt最初的 感知器用的是阶跃函数(Heaviside step function或unit step function)作为激励函数。函数公式如 下所示: 1, x > 0 g(x) =

{ 0, x

≤ 0}

如果加权解释变量的和加上常误差项之和大于0,则激励方程返回1,此时感知器就把样本归类为阳 性。否则,激励方程返回0,感知器就把样本归类为阴性。阶跃函数图形如下所示:

另一个常用的激励函数是逻辑S形(logistic sigmoid )激励函数。这个激励函数的梯度分布可以更有 效的计算,在处理后面的ANN算法时十分有效。其计算公式如下: 1 g(x) =

−x

1 + e

其中,x 是加权输入的和。这个模型与第四章的逻辑方程类似,是解释变量值与模型参数的线性组 合,与逻辑回归模型是一样的。虽然用逻辑S形激励函数的感知器与逻辑回归是一样的,但是要估计 的参数不同。

感知器学习算法 感知器学习算法首先需要将权重设置为0或很小的随机数,然后预测训练样本的类型。感知器是一种 错误驱动(error-driven)的学习算法。如果感知器是正确的,算法就继续处理下一个样本。如果感 知器是错误的,算法就更新权重,重新预测。权重的更新规则如下: wi (t + 1) = wi (t) +

α(d − y (t))x j

j

j,i

,0

≤i≤n

对每个训练样本来说,每个解释变量的参数值增加α(dj − yj (t))xj,i ,dj 是样本j的真实类型,yj (t)是 样本j的预测类型,xj,i 是第i个样本j的解释变量的值,α 是控制学习速率的超参数。如果预测是正确 的,dj − yj (t) 等于0,α(dj − yj (t))xj,i 也是0,此时,权重不更新。如预测是错误的,权重会按照学 习速率,dj − yj (t) 与解释变量值的乘积增加。 这里的更新规则与梯度下降法中的权重更新规则类似,都是朝着使样本得到正确分类更新,且更新的 幅度是由学习速率控制的。每遍历一次训练样本称为完成了一世代(epoch)。如果学习完一世代 后,所有的样本都分类正确,那么算法就会收敛(converge)。学习算法不能保证收敛;后面的章 节,我们会介绍线性不可分数据集,是不可能收敛的。因此,学习算法还需要一个超参数,在算法终 止前需要更新的最大世代数。

感知器二元分类 下面我们来解决一个分类案例。假设你想从一堆猫里分辨幼猫(kitten)和成年猫(adult cats)。数 据集只有两个解释变量:用来睡觉的天数比例,闹脾气的天数比例。训练数据由下面四个样本构成: 样本 用来睡觉的天数比例 闹脾气的天数比例 幼猫还是成年猫? 1

0.2

0.1

幼猫

2

0.4

0.6

幼猫

3

0.5

0.2

幼猫

4

0.7

0.9

成年猫

下面的散点图表面这些样本是可以线性分离的:

In [2]: %matplotlib inline import matplotlib.pyplot as plt from matplotlib.font_manager import FontProperties font = FontProperties(fname=r"c:\windows\fonts\msyh.ttc", size=10)

In [3]: import numpy as np X = np.array([ [0.2, 0.1], [0.4, 0.6], [0.5, 0.2], [0.7, 0.9] ]) y = [0, 0, 0, 1] markers = ['.', 'x'] plt.scatter(X[:3, 0], X[:3, 1], marker='.', s=400) plt.scatter(X[3, 0], X[3, 1], marker='x', s=400) plt.xlabel('用来睡觉的天数比例',fontproperties=font) plt.ylabel('闹脾气的天数比例',fontproperties=font) plt.title('幼猫和成年猫',fontproperties=font) plt.show()

我们的目标是训练一个感知器可以用两个解释变量分辨猫的类型。我们用阳性表示幼猫,用阴性表示 成年猫。用感知网络图(preceding network diagram)可以呈现感知器训练的过程。 我们的感知器有三个输入单元。x1 是常误差项,x2 和x3 是两个特征的输入项。我们的感知器的计算 单元用一个阶跃函数表示。本例中,我们把最大的训练世代数设置为10;如果算法经过10世代没有 收敛,就会停止返回当时的权重值。为了简化,我们把训练速率设置为1。首先,我们把所有的权重 设置为0。第一代的训练结果如下表所示: 世代 1 样本 初始权重 0 1

xi

激励函数值

0, 0, 0

1.0, 0.2, 0.1

1.0 * 0 + 0.2 * 0 + 0.1 * 0 = 0.0

1.0, 0.2,

1.0, 0.4,

1.0 * 1.0 + 0.4 * 0.2 + 0.6 * 0.1 =

预测值,目标 是否正 值 确 0, 1

False

1, 1

True

升级权重 1.0, 0.2, 0.1 1.0, 0.2,

1

1.0, 0.2, 0.1

1.0, 0.4, 0.6

1.0 * 1.0 + 0.4 * 0.2 + 0.6 * 0.1 = 1.14

1, 1

True

1.0, 0.2, 0.1

2

1.0, 0.2, 0.1

1.0, 0.5, 0.2

1.0 * 1.0 + 0.5 * 0.2 + 0.2 * 0.1 = 1.12

1, 1

True

1.0, 0.2, 0.1

3

1.0, 0.2, 0.1

1.0, 0.7, 0.9

1.0 * 1.0 + 0.7 * 0.2 + 0.9 * 0.1 = 1.23

1, 0

False

0, -0.5, -0.8

开始所有权重为0。第一个变量的解释变量加权之和为0,则激励函数值为0,因此样本的预测结果为 阴性,即幼猫样本是成年猫类型。预测错误,所以我们要根据规则升级权重。我们将每个输入单元的 权重增加,增加幅度为学习速率,真实类型与预测类型的差异值与对应解释变量的值的乘积。 然后用更新的权重预测第二个样本类型。这次解释变量加权之和为1.14,激励函数值为1,真实类型 为1,所以类型判断正确。于是继续对第三个样本进行预测,解释变量加权之和为1.12,激励函数值 为1,真实类型为1,所以类型判断正确。再对第四个样本进行预测,这次解释变量加权之和为1.23, 激励函数值为1,真实类型为0,所以类型判断错误。于是我们更新权重,这样就完成了第一代的训练 集样本分类。感知器没有收敛,只有一半样本预测正确。第一代训练完成,决策边界如下图所示:

注意决策边界在整个世代中不断移动;在某个世代结束后由权重构成的决策边界不一定必然与前一世 代产生同样的预测值。由于我们还没超过10个世代,所以我们还可以继续训练样本。第二世代的计算 过程如下表所示: 世代 2 样本

初始权重

xi

激励函数值

预测值,目标 是否正 值 确

升级权重

0

0, -0.5, -0.8

1.0, 0.2, 0.1

1.00 + 0.2-0.5 + 0.1*-0.8 = -0.18

0, 1

False

1, -0.3, -0.7

1

1, -0.3, -0.7

1.0, 0.4, 0.6

1.01.0 + 0.4-0.3 + 0.6*-0.7 = 0.46

1, 1

True

1, -0.3, -0.7

2

1, -0.3, -0.7

1.0, 0.5, 0.2

1.01.0 + 0.5-0.3 + 0.2*-0.7 = 0.71

1, 1

True

1, -0.3, -0.7

3

1, -0.3, -0.7

1.0, 0.7, 0.9

1.01.0 + 0.7-0.3 + 0.9*-0.7 = 0.16

1, 0

False

0, -1, -1.6

第2世代开始用的是第1世代的权重。这个世代里有两个训练样本被预测错误。权重升级两次,但是这 个世代结束时的决策边界与上个世代结束时的决策边界类似。

这个世代结束是算法依然没有收敛,所有我们要继续训练。第3个世代的训练结果如下表所示: 世代 3 样本

初始权重

0 1

预测值,目标 是否正 值 确

xi

激励函数值

升级权重

0, -1, -1.6

1.0, 0.2, 0.1

1.00 + 0.2-1.0 + 0.1*-1.6 = -0.36

0, 1

False

1,-0.8, -1.5

1,-0.8, -1.5

1.0, 0.4, 0.6

1.01.0 + 0.4-0.8 + 0.6*-1.5 = -0.22

0, 1

False

2, -0.4, -0.9

2

2, -0.4, -0.9

1.0, 0.5, 0.2

1.02.0 + 0.5-0.4 + 0.2*-0.9 = 1.62

1, 1

True

2, -0.4, -0.9

3

2, -0.4, -0.9

1.0, 0.7, 0.9

1.02.0 + 0.7-0.4 + 0.9*-0.9 = 0.91

1, 0

False

1, -1.1, -1.8

感知器这个世代比前面世代预测的效果更差。第3个世代的决策边界如下图所示:

感知器继续更新权重进行第4代和第5代的训练,仍然有预测错误的样本。直到第6代,所有的样本都 预测正确了,此时算法达到了收敛状态。第6个世代的训练结果如下表所示: 世代 4 样本 初始权重

xi

激励函数值

预测值,目标 值

是否正 确

升级权重

0

2, -1, -1.5

1.0, 0.2, 0.1

1.02 + 0.2-1 + 0.1*-1.5 = 1.65

1, 1

True

2, -1, -1.5

1

2, -1, -1.5

1.0, 0.4, 0.6

1.02 + 0.4-1 + 0.6*-1.5 = 0.70

1, 1

True

2, -1, -1.5

2

2, -1, -1.5

1.0, 0.5, 0.2

1.02 + 0.5-1 + 0.2*-1.5 = 1.2

1, 1

True

2, -1, -1.5

3

2, -1, -1.5

1.0, 0.7, 0.9

1.02 + 0.7-1 + 0.9*-1.5 = -0.05

0, 0

True

2, -1, -1.5

第6个世代的决策边界如下图所示:

感知器解决文档分类 scikit-learn 提供了感知器功能。和我们用过的其他功能类似,Perceptron类的构造器接受超参数设 置。Perceptron类有fit_transform()和predict()方法。Perceptron类还提供了 partial_fit()方法,允许分类器训练流式数据(streaming data)并做出预测。 在下面的例子中,我们训练一个感知器对20个新闻类别的数据集进行分类。这个数据集从20个网络 新闻网站收集了近2万篇新闻。这个数据集经常用来进行文档分类和聚类实验;scikit-learn提供了下 载和读取数据集的简便方法。我们将训练一个感知器识别三个新闻类别:rec.sports.hockey, rec.sports.baseball和rec.auto。scikit-learn的Perceptron也支持多类分类,使用one versus all策略为训练集中的每个类型训练分类器。我们将用TF-IDF加权词袋来表示新闻文 档。partial_fit()方法可以连接HashingVectorizer在内存有限的情况下训练较大的流式数 据:

In [6]: from sklearn.datasets import fetch_20newsgroups from sklearn.metrics import f1_score, classification_report from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import Perceptron categories = ['rec.sport.hockey', 'rec.sport.baseball', 'rec.autos'] newsgroups_train = fetch_20newsgroups(subset='train', categories=categori es, remove=('headers', 'footers', 'quotes')) newsgroups_test = fetch_20newsgroups(subset='test', categories=categories , remove=('headers', 'footers', 'quotes')) vectorizer = TfidfVectorizer() X_train = vectorizer.fit_transform(newsgroups_train.data) X_test = vectorizer.transform(newsgroups_test.data) classifier = Perceptron(n_iter=100, eta0=0.1) classifier.fit_transform(X_train, newsgroups_train.target) predictions = classifier.predict(X_test) print(classification_report(newsgroups_test.target, predictions)) precision

recall f1-score

support

0 1 2

0.85 0.85 0.89

0.92 0.81 0.86

0.89 0.83 0.87

396 397 399

avg / total

0.86

0.86

0.86

1192

首先,我们用fetch_20newsgroups()下载并读取数据。和其他内建数据集一致,这个函数返回的 对象包括data,target和target_names属性。我们还去掉了每篇文章的页眉,页脚和引用文 献。保留那些让分类更容易的解释变量。我们用TfidfVectorizer生成TF-IDF矢量,训练感知器, 然后用测试集评估效果。没有用网格搜索优化超参数,感知器的平均精确率,召回率和综合评价指标 达到0.86。

感知器的不足 虽然我们的例子中感知器的分类效果不错,但是模型仍有一些不足。带阶跃激励函数的感知器线性模 型并非通用的函数近似器(universal function approximators);有一些函数特征是无法表现的。具 体来说,线性模型只能学习如何近似线性可分(linearly separable)数据集的函数。我们介绍过的分 类器都找到一个超平面将阳性类型与阴性类型区分开来,如果没有一个超平面可以区分两种类型,问 题就不是线性可分的。 线性不可分函数的一个简单例子就是逻辑运算异或(XOR),也称为互斥析取(exclusive disjunction)。异或是当一个输入为1另一个输入为0是,输出结果为1,否则为0。异或的结果如下图 所示,当结果为1,样本用圆圈表示,当结果为0,样本用菱形表示:

这种情况下,不可能用一条直线将圆圈和菱形分开。假设这四个样本都是定在板上的钉子,你用一条 橡皮筋把两个阳性类型的样本连起来,再用另一条橡皮筋把两个阴性类型的样本连起来。这两条橡皮 筋称为凸包(convex hull),或者包含一个集合内所有点的外壳,这个集合内任意两点的连线都在这 个外壳的内部。相比低维空间,这种特征更可能在高维空间实现线性可分。例如,在使用词袋模型分 类文本分类问题时,高维特征向量更容易实现线性可分。 后面两章,我们将介绍处理线性不可分数据集的方法。第一个方法是核心化算法(kernelization),将线 性不可分数据集映射到高维空间变成线性可分数据集。核心化算法可以用在许多场合,包括感知器, 但是最适合的场景是支持向量机,我们下一章将会介绍。支持向量机也提供了一些找超平面的技术, 以最小的误差分离线性不可分数据集。第一个方法建立了一种感知器有向图,其模型称为人工神经网 络,是一种通用函数近似器,我们将在本书的最后一章介绍它。

总结 本章,我们介绍了感知器。源自神经元知识,感知器是一个二元分类线性模型。感知器将解释变量和 权重的线性组合作为激励函数的输入,通过激励函数的结果预测样本的类型。带逻辑S形激励函数的 感知器就和逻辑回归模型一样,只是感知器用一种实时的错误驱动算法计算权重参数。感知器可以有 效的解决一下问题。和我们介绍过的其他线性模型一样,感知器并非通用函数近似器;它只能用一个 超平面分类两种类型。有一些线性不可分数据集,不存在一个超平面来正确的区分所有样本。在后面 的章节,我们将介绍两种处理线性不可分数据集的模型:支持向量机,将线性不可分数据集映射到高 维空间变成线性可分数据集;人工神经网络,一种带感知器有向图的通用函数近似器。

从感知器到支持向量机 上一章我们介绍了感知器。作为一种二元分类器,感知器不能有效的解决线性不可分问题。其实在第 二章,线性回归里面已经遇到过类似的问题,当时需要解决一个解释变量与响应变量存在非线性关系 的问题。为了提高模型的准确率,我们引入了一种特殊的多元线性回归模型,多项式回归。通过对特 征进行合理的组合,我们建立了高维特征空间的解释变量与响应变量的线性关系模型。 随着特征空间的维度的不断增多,在用线性模型近似非线性函数时,上述方法似乎依然可行,但是有 两个问题不可避免。首先是计算问题,计算映射的特征,操纵高维的向量需要更强大的计算能力。然 后是与算法归纳有关的问题,特征空间的维度的不断增多会导致维度灾难。从高维的特征变量中学 习,要避免拟合过度,就需要呈指数级增长的训练数据。 这一章,我们将介绍一种强大的分类和回归模型,称为支持向量机(support vector machine, SVM)。首先,我们将学习高维空间的特征映射。然后,我们将介绍,在处理被映射到高维空间的数 据时,支持向量机是如何缓解那些计算与综合问题的。有许多书整本整本的介绍SVM,相关的优化算 法需要比前面章节里介绍其他算法更多的数学知识。我们不再用前面那些章节的小例子来演示算法, 而是通过直观的案例来介绍scikit-learn如何有效的使用SVM去解决问题。

核与核方法 感知器是用超平面作决策边界对阳性和阴性类型进行分类的。其决策边界公式如下: $$f(x)=+b$$ 预测是通过下面的格式计算得出: h(x) = sign(f (x))

其中,$是内积 w^Tx$。为了与SVM的概念一致,我们要对上面的概念做一些调整。 我们可以把模型写成另一种形式,其证明过程忽略。下面的模型表达式称为对偶型(dual form)。 前面介绍的表达式称为原型(primal form): $$f(x)=+b=⧵sum a_iy_i+b$$ 对偶型与原型的最重要区别就是原型计算模型参数(model parameters )内积,测试样本的特征向 量,而对偶型计算训练样本(training instances)的内积,测试样本的特征向量。总之,我们将利用 对偶型的这个特性来解决线性不可分问题。首先,我们必须定义高维空间特征映射。 在第二章,线性回归介绍多元回归时,我们将特征映射成与响应变量线性相关的高维空间。映射将原 来的特征进行组合,通过建立二次项增加特征的数量。这些综合特征允许我们用线性模型表示非线性 函数。通常,映射表达式如下所示: x

→ϕ →

(x)

ϕ:R

d

D

R

下图中左边显示了一个线性不可分数据集。而右边就是将数据集映射到更高维空间后线性可分的结 果。

让我们回到决策边界的对偶型,观察特征向量只以点积呈现的情况。我们可以用下面的方法将数据映 射到一个高维空间: $$f(x)=⧵sum a_iy_i+b f(x)=⧵sum a_iy_i+b$$ 这个映射允许我们表述更复杂的模型,但是它也引入了计算和综合相关的问题。映射特征向量并计算 它们的点积需要极大的计算能力。 注意在第二个方程里,我们将特征向量映射到高维空间中,特征向量仍然是以点积呈现。点积是标 量,如果一个标量已经被计算出来,我们就不需要去映射对应的特征向量了,这样我们在计算点击和 映射特征向量这些事情上省点儿事儿。 有一种方法叫做核方法( kernel trick)。核是一个考虑原始特征向量的函数,返回对应的映射后特 征向量点积相同的值。核不直接将特征向量映射到高维空间,或者计算映射后特征向量点积。而且通 过一组更有效的计算步骤得出同样的值。核更正式定义如下: K (x, z) =


下面我们演示核是如何运行的。假设我们有两特征向量,x 和z : x = (x1 , x2 ) z = (z 1 , z 2 )

在我们的模型里面,我们想用下面的转换函数将特征向量映射到高维空间:

ϕ(x) = x

2

映射后的标准化特征向量的点积等价于:
=< (x

2

1

,x

2

2



2

2



, √2 x1 x2 ), (z , z , √2 z 1 z 2 ) > 1 2

而下面公式的核可以产生与映射后的特征向量的点积同样的结果: $$K(x,z)={}^2={(x_1z_1+x_2z_2)}^2=x_1^2z_1^2+2x_1z_1x_2z_2+x_2^2z_2^2 K(x,z)=$$

让我们把数值带入公式计算一下,让结果更清楚: x = (4, 9) z = (3, 3)

$$K(x,z)={}^2= {(4⧵times3+9⧵times3)}^2=4^2⧵times3^2+2⧵times4⧵times3⧵times9⧵times3+9^2⧵times3^2=1521 ==1521$$ 与< ϕ(x), ϕ(z) > 映射后的特征向量的点积计算结果相同,但是不需要将特征向量映射到高 维空间,这样减少了计算操作。这个例子只用了二维特征向量。具有中等数量特征的数据集经过映射 后的特征空间将具有巨大的维度。scikit-learn提供了一些常用的核,包括多项式(polynomial),S 形曲线(sigmoid),正态分布(Gaussian)和线性(linear)核。多项式核公司如下: K (x, z)





k

K (x, x ) = (1 + x × x )

平方核,就是多项式核的参数k 是2的形式,在自然语言处理中经常用到。 S形曲线核公式如下。γ 和r 是可以通过交叉检验进行调整的超参数: ′

K (x, x ) = tanh


正态分布核是处理非线性问题的首选。正态分布核是一种径向基函数(radial basis function) (https://zh.wikipedia.org/wiki/%E5%BE%84%E5%90%91%E5%9F%BA%E5%87%BD%E6%95%B0%E6% 映射后的特征空间里的超平面形成的决策边界与原始特征空间的超平面形成的决策边界类似。正态分 布核产生的特征空间可以有无限维,这点在其他核中是不可能有的。正态分布核公式如下: ′

K (x, x ) = exp(



∥ x, x ∥ ′

2

σ

2

2

)

其中,σ 是一个超参数。当使用SVM时,通常缩放特征是很重要的,但是使用正态分布核时缩放特征 尤其重要。 选择哪种核是很复杂的事情。理想情况下,一个核会以一种有益于解决问题的方式度量样本间的相似 度。核经常用于SVM,也可用于任何能够用两个特征向量点积进行表述的模型,包括逻辑回归,感知 器和主成分分析。下面,我们将解决由映射到高维特征空间引起的第二个问题:综合。

最大化间隔分类与支持向量 下图显示了两种线性可分的类型的样本集和三种可能的决策边界。所有的决策边界都可以把样本集分 成阳性与阴性两种类型,感知器可以学习任何一种边界。那么,哪个决策边界对测试集数据的测试效 果最好呢?

观察图中三条决策边界,我们会直观的认为点线是最佳边界。实线决策边界接近许多阳性类型样本。 测试集中如果包含第一个解释变量x1 比较小的阳性样本,这个样本的类型将预测错误。虚线决策边 界与大多数训练样本都很远,但是它接近一个阳性类型样本和一个阴性类型样本。下图提供了评估决 策边界效果的不同视角:

假设上面画的这条线是一个逻辑回归分类器的决策边界。样本A远离决策边界,较高的概率预测它属 于阳性类型。样本B仍然可以预测它属于阳性类型,但是概率可能比远离决策边界的样本A要低。最 后,样本C可能以较低的概率预测它属于阳性类型,甚至训练数据集的一点儿小变化都会改变预测结 果。最可靠的预测是远离决策边界的样本。我们可以用函数间隔(functional margin)来估计预测的 置信水平。训练集的函数间隔公式如下: f unct = min(yi f (xi ))

$$f(x) = +b$$ 其中,yi 是样本的真实类型。样本A的函数间隔比样本C的函数间隔小。如果样本C预测错了,其函数 间隔就会是负数。函数间隔为1的样本集称为支持向量(support vectors)。仅这些样本都足以定义 决策边界;预测测试集样本的类型时,其他的样本可以不用。函数间隔有关的概念是空间间隔 (geometric margin),或称为分离支持向量的带空间的最大宽度。空间间隔等于标准化的函数间 隔。有必要用w 标准化函数间隔,因为间隔在训练时是不确定的。当w 是一个单位向量时,空间间隔 等于函数间隔。现在我们可以把最佳决策边界定义成最大空间间隔。可以通过下面的约束条件求解最 大空间间隔时的模型参数: $$min ⧵frac 1 n subject to: y_i(+b) ⧵ge 1$$ 支持向量机的一个有用的属性是这个优化问题是凸包形,其唯一的局部最小值也是全局最小值。这里 省略证明过程,前面的优化问题可以用对偶型调整核函数写出如下形式: W(

α) = ∑ α − i

i

1 2 ∑

α α y y K (x , x ) i

j

i

j

i,j

n

subjectto :

∑ i=1

yi

α

i

= 0

i

j

i=1

subjectto :

α ≥0 i

求最大化空间间隔的参数的约束是:所有的阳性样本的函数间隔最小为1,所有的阳性样本的函数间 隔最多为-1,是一个二次规划问题(quadratic programming problem)。这类问题通常的解法是用 序列极小优化(Sequential Minimal Optimization,SMO)算法。SMO算法把优化问题分解成一系列 最小化子问题,可以通过解析方法解决。

scikit-learn文字识别 下面我们用SVM来解决分类问题。近几年,SVM已经成功解决了文字识别问题。就是给一张图片, 分类器需要预测出上面的文字。文字识别是OCR(optical character-recognition,光学识别)系统的 一部分。当用原始的像素强度矩阵表示特征向量时,很小的图片也会产生很高维的向量。如果类型是 线性不可分,必须被映射到高维空间时,特征空间的维度会变得更庞大。SVM可以有效的处理这些问 题。首先,我们用scikit-learn 训练SVM识别手写数字。然后,我们再用SVM识别照片上的字母。

识别手写数字 MNIST手写数字数据库(Mixed National Institute of Standards and Technology database)包含 70000张手写数字图片。这些数字是通过美国国家统计局的员工和美国高校的学生收集的。每张图片 都是28x28的灰度图。让我们取一些图看看:

In [1]: %matplotlib inline import matplotlib.pyplot as plt from sklearn.datasets import fetch_mldata import matplotlib.cm as cm digits = fetch_mldata('MNIST original', data_home='data/mnist').data counter = 1 for i in range(1, 4): for j in range(1, 6): plt.subplot(3, 5, counter) plt.imshow(digits[(i - 1) * 8000 + j].reshape((28, 28)), cmap=cm. Greys_r) plt.axis('off') counter += 1 plt.show()

首先,我们加载数据。scikit-learn提供了fetch_mldata函数下载数据,然后读入对象。然后,我们 创建subplot分别显示0,1,2的5个样本。 MNIST数据集可以分成60000张图片的训练集和10000张图片的测试集。这个数据集经常用于估计机 器学习模型的效果;训练模型前总需要一点预处理,所以这个数据集很受欢迎。让我们用scikit-learn 建一个分类器来预测图片的数字。 首先,导入需要用的模块:

In [1]: from sklearn.datasets import fetch_mldata from sklearn.pipeline import Pipeline from sklearn.preprocessing import scale from sklearn.cross_validation import train_test_split from sklearn.svm import SVC from sklearn.grid_search import GridSearchCV from sklearn.metrics import classification_report 然后用fetch_mldata函数导入数据。再放大特征值,然后把图像调整到原点为中心的位置。再用 交叉检验分割数据集:

交叉检验分割数据集:

In [2]: data = fetch_mldata('MNIST original', data_home='data\mnist') X, y = data.data, data.target X = X/255.0*2 - 1 X = scale(X) X_train, X_test, y_train, y_test = train_test_split(X, y) 之后,我们实例化一个SVC类的对象,就是支持向量分类器。这个对象的API和scikit-learn的其他估 计器差不多;分类器用fit函数训练,然后用predict函数预测结果。如果你看过SVC的文档内容, 你会发现它的参数与其他估计器要多一个。SVC最有意思的超参数是kernel,C和gamma。kernel 是核函数类型。scikit-learn提供了线性,多项式,S形曲线,和RBF(径向基函数)核。C是控制正则 化的参数,类似逻辑回归里的λ 超参数。gamma是多项式,S形曲线,和RBF核的相关系数。超参数 的设置很难恰到好处,所以我们用网格搜索来计算:

In [ ]: # clf = SVC(kernel='rbf', C=2.8, gamma=.0073) pipeline = Pipeline([ ('clf', SVC(kernel='rbf', gamma=0.01, C=100)) ]) parameters = { 'clf__gamma': (0.01, 0.03, 0.1, 0.3, 1), 'clf__C': (0.1, 0.3, 1, 3, 10, 30), } # TODO is refit true by default? grid_search = GridSearchCV(pipeline, parameters, n_jobs=-1, verbose=1, sc oring='accuracy', refit=True) grid_search.fit(X_train, y_train) print('最佳效果:%0.3f' % grid_search.best_score_) print('最优参数集:') best_arameters = grid_search.best_estimator_.get_params() for param_name in sorted(parameters.keys()): print('\t%s: %r' % (param_name, best_parameters[param_name])) predictios = grid_search.predict(X_test) print(classification_report(y_test, predictions))

Fitting 3 folds for each of 30 candidates, totalling 90 fits [Parallel(n_jobs=2)]: Done 1 jobs | elapsed: 7.7min [Parallel(n_jobs=2)]: Done 50 jobs | elapsed: 201.2min [Parallel(n_jobs=2)]: Done 88 out of 90 | elapsed: 304.8min remaining: 6.9min [Parallel(n_jobs=2)]: Done 90 out of 90 | elapsed: 309.2min finish ed Best score: 0.966 Best parameters set: clf__C: 3 clf__gamma: 0.01 precision recall f1-score support

0.0 0.98 0.99 0.99 1758 1.0 0.98 0.99 0.98 1968 2.0 0.95 0.97 0.96 1727 3.0 0.97 0.95 0.96 1803 4.0 0.97 0.98 0.97 1714 5.0 0.96 0.96 0.96 1535 6.0 0.98 0.98 0.98 1758 7.0 0.97 0.96 0.97 1840 8.0 0.95 0.96 0.96 1668 9.0 0.96 0.95 0.96 1729 avg / total 0.97 0.97 0.97 17500

模型预测的最佳综合指标是0.97,增大训练样本量可以继续改善效果,不过对硬件的要求依然很高, 在我的电脑上i5 cpu 16G RAM上跑了309分钟。

自然图片文字识别 现在,我们再做一个更具挑战性的任务。我们将从自然图片中识别英文字母。Chars74K数据集 (http://www.ee.surrey.ac.uk/CVSSP/demos/chars74k)是T. E. de Campos,B. R. Babu和M. Varma 为《Character Recognition in Natural Images》提供的74000张图片,包括0到9的数字和26个英文字 母。下面是小写字母z的三个示例图片:

这个数据集包含不同的图片类型。本例使用从印度的Bangalore拍摄的街景里抽取的7705张文字图 片。与MNIST数据集不同,Chars74K数据集里面的这些图片中的文字具有不同的字体,颜色和变 化。数据下载完成后,我们将用English/Img/GoodImg/Bmp/里面的图片。首先我们导入模块

In [1]: import os import numpy as np import mahotas as mh from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.cross_validation import train_test_split from sklearn.grid_search import GridSearchCV from sklearn.metrics import classification_report 然后我们用mahotas库将图片转换成同样大小和颜色。与MNIST数据集不同,Chars74K数据集里面 的图片大小不同,我们将图片转换成一边为30px的图片。

In [ ]: X = [] y = [] for path, subdirs, files in os.walk('data/English/Img/GoodImg/Bmp/'): for filename in files: f = os.path.join(path, filename) target = filename[3:filename.index('-')] img = mh.imread(f, as_grey=True) if img.shape[0]