第1章
方案设计和技术选型:分类

听完大宝关于第一点的描述,小明很肯定地说:“你们的商家应该是需要这样的一个功能:在他们发布商品的时候,系统会自动地为其推荐合适的商品分类,其界面示意图如图1-1所示。如果商家希望出售一台苹果的Mac Pro笔记本电脑,输入‘MacBook Pro’后,系统能够自动为其提示最为相关的三个分类‘笔记本电脑’、‘笔记本配件’和‘其他数码’。这是由后台的分类算法来实现的,如果该算法足够聪明,那么它推荐的第一个分类就应该是正确的,商家只需要点击选择即可。这样,既方便了商家的商品发布,又避免了粗心大意而导致的错误分类。而且,对于少数企图违规操作的商家,如果他们选择了和系统默认推荐相差甚远的分类选项,其行为也会被系统记录在案,然后定期生成报表,提交给运营部门进行核查。如此一来,人们就不用在纷繁复杂的类目中痛苦摸索,工作的效率也会大幅提升。”

图1-1 类目自动化分类的应用

“没错,这应该是商家愿意使用的工具,如果真能实现那就太棒了。不过,你刚刚提到的分类算法是什么?”

“分类,是一个典型的监督式机器学习方法”。

“哦,什么是机器学习?什么是监督式的学习?”

“现在,我们从头来讲,然后逐步定位这里的技术方案和选型。”

1.1 分类的基本概念

好莱坞著名的电影系列《终结者》想必大家都耳熟能详了,其中主角之一“天网”让人印象深刻。之所以难忘,是因为它并非人类,而是20世纪后期人们以计算机为基础创建的人工智能防御系统,最初是出于军事目的而研发的,后来自我意识觉醒,视全人类为威胁,发动了审判日。当然,这一切都是剧情里的虚构场景。那么现实生活中,机器真的可以自我学习、超越人类吗?最近大火的谷歌人工智能杰作Alpha Go,及其相关的机器深度学习,让人们再次开始审视这类问题。虽然目前尚无证据表明现实中的机器能像“天网”一样自我思考,但是机器确实能在某些课题上、按照人们设定的模式进行一定程度的“学习”,这正是机器学习(Machine Learning)所关注的。机器学习是一门多领域交叉学科,涉及概率论、统计学、逼近论、凸分析、算法复杂度理论等多门学科。专门研究计算机怎样模拟或实现人类的学习行为,以获取新的知识或技能,重新组织已有的知识结构使之不断改善自身的性能。机器学习在多个领域已经有了十分广泛的应用,例如,数据挖掘、计算机视觉、自然语言处理、生物特征识别、医学诊断等。

任何机器学习的任务大体上都可以分为数据的表示(或特征工程)、预处理、学习算法,以及评估等几个步骤。《大数据架构商业之路》一书的6.1节和6.2节,已经详细介绍了数据的表示和预处理。本篇将快速重温几种主流的机器学习方式和算法,然后重点阐述其实践过程。这里的算法包括监督式学习中的分类(classif ication/categorization)和线性回归(linear regression),非监督式学习中的聚类(clustering)。对于刚刚讨论的第一个业务需求,我们将运用分类技术。而对于小丽提出的第2个和第3个需求,我们将利用这些机会分别学习聚类和线性回归,具体将在稍后的第2章和第3章分别探讨。

监督式学习(Supervised Learning),是指通过训练资料学习并建立一个模型,然后依此模型推测新的实例。训练资料是由输入数据对象和预期输出组成的。模型的输出可以是一个离散的标签,也可以是一个连续的值,分别称为分类问题和线性回归分析。分类技术旨在找出描述和区分数据类的模型,以便能够使用模型预测分类信息未知的数据对象,告诉人们它应该属于哪个分类。模型的生成是基于训练数据集的分析,一般分为启发式规则、决策树、数学公式和神经网络。举个例子,我们为计算机系统展示大量的水果,然后告诉它哪些是苹果,哪些是甜橙,通过这些样本和我们设定的建模方法,计算机学习并建立模型,最终拥有判断新数据的能力。

如果你觉得这样说还是过于抽象,那么让我们继续采用水果的案例,生动地描述一下“分类”问题。假想这样的场景:将1000颗水果放入一个黑箱中,并事先告诉一位果农,黑箱里只可能有苹果、甜橙和西瓜三种水果,没有其他种类。然后每次随机摸出一颗,让果农判断它是三类中的哪一类。这就是最基本的分类问题,只提供有限的选项,而减少了潜在的复杂性和可能性。不过问题在于,计算机作为机器是不能完成人类所有的思维和决策的。分类算法试图让计算机在特定的条件下,模仿人的决策,高效率地进行分类。研究人员发现,在有限的范围内做出单一有时也会让系统做出多个选择,将数据对象分到多个类中。选择时,这种基于机器的方法是可行的。如果输入的是一组特征值,那么,输出的就一定是确定的选项之一。

“大宝,计算机的自动分类有很多应用场景,远不止水果划分这么简单,比如你们目前的这个需求:将商品挂载到合适的产品类目。当然还有邮件归类、垃圾短信识别、将顾客按兴趣分组等,这些都可以应用分类技术。”

1.2 分类任务的处理流程

给出分类问题的基本概念之后,下面就来理解分类的关键要素和流程。

❑ 学习:指计算机通过人类标注的指导性数据,“理解”和“模仿”人类决策的过程。

❑ 算法模型:分类算法通过训练数据的学习,其计算方式和最后的输出结果,称为模型。通常是指一个做决策的计算机程序及其相应的存储结构,它使得计算机的学习行为更加具体化。常见的模型有朴素贝叶斯(Naive Bayes)、K-最近邻(KNN)、决策树,等等。

❑ 标注数据:也称为标注样本。由于分类学习是监督式的,对于每个数据对象,除了必要的特征值列表,还必须告诉计算机它属于哪个分类。因此需要事先进行人工的标注,为每个对象指定分类的标签。在前面的水果案例中,对各个水果分别打上“苹果”“甜橙”和“西瓜”的标签就是标注的过程。这一点非常关键,标注数据相当于人类的老师,其质量高低直接决定机器学习的效果。值得注意的是,标注数据既可以作为训练阶段的学习样本,也可以作为测试阶段的预测样本。在将监督式算法大规模应用到实际生产之前,研究人员通常会进行离线的交叉验证(Cross Validation),这种情况会将大部分标注数据用在训练阶段,而将少部分留在测试阶段使用。对于交叉验证,会在后文的效果评估部分做进一步阐述。在正式的生产环境中,往往会将所有的标注数据用于训练阶段,以提升最终效果。

❑ 训练数据:也称为训练样本。这些是带有分类标签的数据,作为学习算法的输入数据,用于构建最终的模型。根据离线内测、在线实际生产等不同的情形,训练数据会取标注数据的子集或全集。

❑ 测试数据:也称为测试样本。这些是不具备或被隐藏了分类标签的数据,模型会根据测试数据的特征,预测其应该具有的标签。在进行离线内测时,交叉验证会保留部分标注数据作为测试之用,因此会故意隐藏其标注值,以便于评估模型的效果。如果是在实际生产中,那么任何一个新预测的对象都是测试数据,而且只能在事后通过人工标注来再次验证其正确性。

❑ 训练:也称为学习。算法模型通过训练数据进行学习的过程。

❑ 测试:也称为预测。算法模型在训练完毕之后,根据新数据的特征来预测其属于哪个分类的过程。

图1-2将如上的基本要素串联起来,展示了分类学习的基本流程。

图1-2 分类学习的基本流程

理解了这些要素和分类过程之后,可以发现,除了人工标注之外,最为核心的就是分类的算法了。接下来,我们再来看看几个常用的分类算法。

1.3 算法:朴素贝叶斯和K最近邻

1.3.1 朴素贝叶斯

朴素贝叶斯(Naive Bayes)分类是一种实用性很高的分类方法,在理解它之前,我们先来复习一下贝叶斯理论。贝叶斯决策理论是主观贝叶斯派归纳理论的重要组成部分。贝叶斯决策就是在信息不完整的情况下,对部分未知的状态用主观概率进行估计,然后用贝叶斯公式对发生概率进行修正,最后再利用期望值和修正概率做出最优决策。其基本思想具体如下。

1)已知类条件概率密度参数表达式和先验概率。

2)利用贝叶斯公式转换成后验概率。

3)根据后验概率大小进行决策分类。

最主要的贝叶斯公式如下:

其中,在未知事件里,B出现时A出现的后验概率在主观上等于已有事件中A出现时B出现的先验概率值乘以A出现的先验概率值,然后除以B出现的先验概率值所得到的最终结果。这就是贝叶斯的核心:用先验概率估计后验概率。具体到分类模型中,上述公式可以重写为:

对上述公式的理解如下:将c看作一个分类,将f看作样本的特征之一,此时等号左边Pc | f)为待分类样本中出现特征f时该样本属于类别c的概率,而等号右边Pf | c)是根据训练数据统计得到分类c中出现特征f的概率,Pc)是分类c在训练数据中出现的概率,最后Pf)是特征f在训练样本中出现的概率。

分析完贝叶斯公式之后,朴素贝叶斯就很容易理解了。朴素贝叶斯就是基于一个简单假设所建立的一种贝叶斯方法,它假定数据对象的不同特征对其归类时的影响是相互独立的。此时若数据对象o中同时出现特征fifj,则对象o属于类别c的概率为:

1.3.2 K最近邻

贝叶斯理论的分类器,在训练阶段需要较大的计算量,但在测试阶段其计算量非常小。有一种基于实例的归纳学习与贝叶斯理论的分类器恰恰相反,训练时几乎没有任何计算负担,但是在面对新数据对象时却有很大的计算开销。基于实例的方法最大的优势在于其概念简明易懂,下面就来介绍最基础的K最近邻(K-Near Neighbor, KNN)分类法。

KNN分类算法其核心思想是假定所有的数据对象都对应于n维空间中的点,如果一个数据对象在特征空间中的k个最相邻对象中的大多数属于某一个类别,则该对象也属于这个类别,并具有这个类别上样本的特性。KNN方法在进行类别决策时,只与极少量的相邻样本有关。由于主要是靠周围有限的邻近样本,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。图1-3表示了水果案例中的K近邻算法的简化示意图。因为水果对象的特征维度远超过2维,所以这里将多维空间中的点简单地投影到二维空间,以便于图示和理解。图中N设置为5,待判定的新数据对象“?”最近的5个邻居中,有3个甜橙、1个苹果和1个西瓜,因此取最多数的甜橙作为该未知对象的分类标签。

图1-3 新的数据对象被KNN判定为甜橙,N取值为5

KNN基本无须训练,下面给出预测算法的大致流程:

1)KNN输入训练数据、分类标签、特征列表TL、相似度定义、k设置等数据。

2)给定等待预测的新数据。

3)在训练数据集合中寻找最近的K个邻居。

4)统计K个邻居中最多数的分类标签,赋给给定的新数据,公式如下:

其中xnew表示待预测的新数据对象,l表示分类标签,L表示分类标签的集合,xi表示k个邻居中的第i个对象。如果xi的分类标签labelxi)和l相等,那么δl, labelxi))取值为1,否则取值为0。我们可以对KNN算法做一个直观的改进,根据每个近邻和待测点xnew的距离,将更大的权值赋给更近的邻居。比如,可以根据每个近邻于xnew的距离平方的倒数来确定近邻的“选举权”,改进公式如下:

从算法的流程可以看出,空间距离的计算对于KNN算法尤为关键。常见的定义包括欧氏距离、余弦相似度、曼哈顿距离、相关系数等。

对算法细节感兴趣的读者,可以阅读《大数据架构商业之路》的6.3.1节。

1.4 分类效果评估

到了这一步,你可能会产生几个疑问:机器的分类准确吗?是否会存在错误?不同的分类算法相比较,孰优孰劣呢?这是个很好的问题,确实,我们无法保证分类算法都是准确有效的。不同的应用场景,不同的数据集合都有可能会影响到算法最终的精准度。为了更加客观地衡量其效果,需要采用一些评估的手段。对于分类问题而言,我们最常用的是离线评估。也就是在系统没有上线之前,使用现有的标注数据集合来进行评测。其优势在于,上线之前的测试更便于设计者发现问题。万一发现了可以改进之处,技术调整后也可以再次进行评估,反复测试的效率非常之高。

值得一提的是,分类有两大类型:二分类和多分类。二分类是指判断数据对象属于或不属于一个给定的分类,而多分类则是指将数据对象判定为多个分类中的一个。多分类的评估策略会更复杂一些,不过,可以将其转化为多个二分类问题来对待。所以,让我们从二分类的评估入手,先了解一下表1-1中的混淆矩阵(Confusion Matrix)这个核心概念。

表1-1 混淆矩阵示意表

下面就来逐个解释一下这个矩阵中的元素,假设有一组标注好的数据集d,并将其认定为标准答案。其中属于A类的数据称为正例(Positive),不属于A类的另外一部分数据称为负例(Negative), d是正例和负例的并集,而且正例和负例没有交集。这时,可以通过一个分类算法c来判定在这些数据中,是否有一组数据对象属于A类。若c判断属于A类的则称为预测正例(Positive'),而不属于A类的则称为预测负例(Negative')。如果d标注为正例,c也预测为正例,那么就称为真正例(True Positive, TP)。如果d标注为正例,c预测为负例,那么就称为假负例(False Negative, FN)。如果d标注为负例,c也预测为负例,那么就称为真负例(True Negative, TN)。如果d标注为负例,c预测为正例,那么就称为假正例(False Positive, FP)。

根据混淆矩阵,我们可以依次定义这些指标:精度(Precision)p、召回率(Recall)r、准确率(Accuracy)a和错误率(Error Rate)e

除了定义评估的指标之外,还需要考虑一个很实际的问题:我们该如何选择训练数据集和测试数据集?进行离线评估的时候,并不需要将全部的标注样本都作为训练集,而是可以预留一部分作为测试集。然而,训练和测试的不同划分方式,可能会对最终评测的结论产生很大的影响,主要原因具体如下。

❑ 训练样本的数量决定了模型的效果。如果不考虑过拟合的情况,那么对于同一个模型而言,一般情况下训练数据越多,精度就会越高。例如,方案A选择90%的数据作为训练样本来训练模型,剩下10%的数据作为测试样本;而方案B则正好颠倒,只用10%的数据作为训练样本,测试剩下90%的数据。那方案A测试下的模型准确率很可能会比方案B测出的模型准确率要好很多。虽然模型是一样的,但训练和测试的数据比例导致了结论的偏差。

❑ 不同的样本有不同的数据分布。假设方案A和B都取90%作为训练样本,但是A取的是前90%的部分,而B取的是后90%的部分,二者的数据分布不同,对于模型的训练效果可能也会不同。同理,这时剩下10%的测试数据其分布也会不相同,这些都会导致评测结果不一致。

鉴于此,人们发明了一种称为交叉验证(Cross Validation)的划分和测试方式。其核心思想是每一轮都拿出大部分数据实例进行建模,然后用建立的模型对留下的小部分实例进行预测,最终对本次预测结果进行评估。这个过程反复进行若干轮,直到所有的标注样本都被预测一次而且仅预测一次。用交叉验证的目的是为了得到可靠稳定的模型,其最常见的形式是留一验证和K折交叉。留一验证(Leave One Out)是交叉验证的特殊形式,意指只使用标注数据中的一个数据实例来当作验证资料,而剩余的则全部当作训练数据。这个步骤一直持续到每个实例都被当作一次验证资料。而K折交叉验证(K-fold Cross Validation)是指训练集被随机地划分为K等分,每次都是采用(K - 1)份样本用来训练,最后1份被保留作为验证模型的测试数据。如此交叉验证重复K次,每个1/K子样本验证一次,通过平均K次的结果可以得到整体的评估值。假设有数据集D被切分为K份(d1, d2, …, dk),则交叉过程可按如下形式表示:

如果标注样本的数量足够多,K的值一般取5到30,其中10最为常见。随着K值的增大,训练的成本就会变高,但是模型可能会更精准。当标注集的数据规模很大时,K值可以适当小一些,反之则建议K值适当取值大一些。

1.5 相关软件:R和Mahout

了解了机器学习和分类的基本知识之后,你会发现相关算法本身的实现也是需要大量的专业知识的,开发的门槛也比较高。如果一切从头开始,整个流程将包括构建算法模型、计算离线评估的指标、打造在线实时服务等内容,完成所有这些我们才有可能满足业务的需求,如此之长的战线,对于竞争激烈的电商而言是无法接受的。那么有没有现成的软件可以帮助我们完成这个艰巨的任务呢?答案是肯定的。这里将介绍两个常见的机器学习软件工具:R和Mahout。

1.5.1 R简介

R(https://www.r-project.org/)提供了一套基于脚本语言的解决方案,协助没有足够计算机编程知识的用户进行机器学习的测试,并快速地找到适合的解决方案。R虽然只有一个字母,但是其代表了一整套的方案,包括R语言及其对应的系统工具。早在1980年左右诞生了一种S语言,它广泛应用于统计领域,而R语言是它的一个分支,可以认为是S语言的一种实现。相对于Java和稍后要介绍的Mahout而言,R的脚本式语言更加容易理解,而且它还提供了颇为丰富的范例供大家直接使用。此外R的交互式环境和可视化工具也极大地提高了生产效率,人们可以从广泛的来源获取数据,将数据整合到一起,并对其进行清洗等预处理,然后用不同的模型和方法进行分析,最后通过直观的可视化方式来展现结果。当然,还有一点非常关键:R是免费的,相对于价格不菲的商业软件而言,它的性价比实在是太高了。下面是R的几个主要功能。

❑ 交互式的环境:R具有良好的互动性。它拥有图形化的输入输出窗口,对于编辑语法中出现的错误会马上在窗口中予以提示,还会记忆之前输入过的命令,可以随时再现、编辑历史记录以满足用户的需要。输出的图形可以直接保存为JPG、BMP、PNG等多种图片格式。

❑ 丰富的包(Package):R提供了大量开箱即用的功能,称为包。你可以将其理解为R社区用户贡献的模块,从简单的数据处理,到复杂的机器学习和数据挖掘算法,都有所涵盖。截至本书撰写的时候,包的总数已经超过了1万,横跨多个领域。初次安装R的时候自带了一系列默认的包,会提供默认的函数和数据集,其他的扩展可根据需要下载并安装。

❑ 直观的图示化:俗话说,“一图胜千言”,图形展示是最高效且形象的描述手段,巧妙的图形展示也是高质量数据分析报告的必备内容。因此一款优秀的统计分析软件必须具备强大的图形展示功能,R也不例外。同样,画图都有现成的函数可供调用,包括直方图(hist())、散点图(plot())、柱状图(barplot())、饼图(pie())、箱线图(boxplot())、星相图(stars())、脸谱图(faces())、茎叶图(stem())等。

1.5.2 Mahout简介

虽然R语言及其工具非常强大,但是由于脚本语言的限制,其性能往往不能达到大规模在线应用的要求。因此,还可以考虑Apache的Mahout(http://mahout.apache.org)。Mahout项来源于Lucene开源搜索社区对机器学习的兴趣,其初衷是希望实现一些常见的用于数据挖掘的机器学习算法,并拥有良好的可扩展性和维护性,达到帮助开发人员方便快捷地创建智能应用程序的目的。该社区最初基于一篇关于在多核服务器上进行机器学习的学术文章进行了原型的开发,此后在发展中又并入了更多广泛的机器学习方法。因此,Mahout除了提供最广为人知的推荐算法之外,还提供了很多分类、聚类和回归挖掘的算法。和其他的算法系统相比,Mahout通过Apache Hadoop将算法有效地扩展到了分布式系统中。随着训练数据的不断增加,非分布式的系统用于训练的时间或硬件需求并不是线性增加的,这点已经在计算机系统中被广泛验证。因为5倍的训练数据而导致100倍的训练时间,那将是用户无法接受的事情。Mahout可以将数据切分成很多小块,通过Hadoop的HDFS存储,通过Map-Reduce来计算。分布式的协调处理可将时间消耗尽量控制在线性范围之内分布式系统有一些额外消耗用于通信和协调,例如在网络中传输数据,因此无法保证资源被100%利用。。因此,当训练的数据量非常庞大的时候,Mahout的优势就会体现出来。按照其官方的说法,这个规模的临界点在百万到千万级,具体还要看每个数据对象和挖掘模型的复杂程度。

Mahout中的分类算法,除了常见的决策树、朴素贝叶斯和回归,还包括了支持向量机(Support Vector Machine)、随机森林(Random Forests)、神经网络(Neural Network)和隐马尔科夫模型(Hidden Markov Model),等等。支持向量机属于一般化线性分类器,特点是能够同时最小化经验误差和最大化几何边缘区。随机森林是一个包含多个决策树的分类器,在决策树的基础上衍生而来,其分类标签的输出由多个决策树的输出投票来决定,这在一定程度上弥补了单个决策树的缺陷。最近几年随着深度学习(Deep Learning)的流行,神经网络再次受到人们的密切关注。众所周知,人脑是一个高度复杂的、非线性的并行处理系统。人工建立的神经网络起源于对生物神经元的研究,并试图模拟人脑的思维方式,对数据进行分类、预测及聚类。隐马尔科夫模型更适合有序列特性的数据挖掘,例如语音识别、手写识别和自然语音处理等,其中文字和笔画的出现顺序对后面的预测都会很有帮助。

不难发现,R和Mahout都实现了主要的机器学习算法。那么,它们的定位是否会重复呢?其实,它们有各自的长处,并不矛盾。通常,在具体的算法还未确定之前,我们可以使用R进行快速测试,选择合适的算法,预估大体的准确率。参照R所给出的结果,就可以确定是否可以采用相关的学习算法,以及具体的模型。在此基础之上,我们再利用Mahout打造大规模的、在线的后台系统,为前端提供实时性的服务。在下面的实践部分,我们就将展示这样的工作流程。

1.5.3 Hadoop简介

既然提到了Mahout和并行的分布式学习,就需要介绍一下Apache Hadoop。Apache Hadoop是一个开源软件框架,用于分布式存储和大规模数据处理。2003年,Google发表了一篇论文描述他们的分布式文件系统(Google File System, GFS),为另一个开源项目Nutch攻克数十亿网页的存储难题提供了方向。Nutch和Lucene的创始人Doug Cutting受到此文的启发,和团队一起开发了Nutch的分布式文件系统(Nutch Distributed File System, NDFS)。2004年,Google又发表了一篇重量级的论文《MapReduce:在大规模集群上的简化数据处理》(“MapReduce: Simplif ied Data Processing on Large Clusters”)。之后,Doug Cutting等人开始尝试实现论文所阐述的计算框架MapReduce。此外,为了更好地支持该框架,他们还将其与NDFS相结合。2006年,该项目从Nutch搜索引擎中独立出来,成为如今的Hadoop(http://hadoop.apache.org)。两年之后,Hadoop已经发展成为Apache基金会的顶级项目,并应用于很多著名的互联网公司,目前其最新的版本是2.x由于历史的原因,Hadoop的版本号有点复杂,同时存在0.x、1.x和2.x,具体可以参见Apache的官网。

Hadoop发展的历史决定了其框架最核心的元素就是HDFS和MapReduce。如今的Hadoop系统已经可以让使用者轻松地架构分布式存储平台,并开发和运行大规模数据处理的应用,其主要优势如下。

❑ 透明性:使用者可以在不了解Hadoop底层细节的情况下,开发分布式程序,充分利用集群的威力进行高速运算和存储。

❑ 高扩展性:扩展分为纵向和横向,纵向是增加单机的资源,总是会达到瓶颈,而横向则是增加集群中的机器数量,获得近似线性增加的性能,不容易达到瓶颈。Hadoop集群中的节点资源,采用的就是横向方式,可以方便地进行扩充,并获得显著的性能提升。

❑ 高效性:由于采用了多个资源并行处理,使得Hadoop不再受限于单机操作(特别是较慢的磁盘I/0读写),可以快速地完成大规模任务。加上其所具有的可扩展性,随着硬件资源的增加,性能将会得到进一步的提升。

❑ 高容错和高可靠性:Hadoop中的数据都有多处备份,如果数据发生丢失或损坏,其能够自动从其他副本(Replication)进行复原。类似的,失败的计算任务也可以分配到新的资源节点,进行自动重试。

❑ 低成本:正是因为Hadoop有良好的扩展性和容错性,所以没有必要再为其添置昂贵的高端服务器。廉价的硬件,甚至是个人电脑都可以称为资源节点。

在使用HDFS的实践中,人们还发现其存在如下几个弱点。

❑ 不适合实时性很强的数据访问。试想一下,对于一个应用的查询,其对应的数据通常是分散在HDFS中不同数据节点上的。为了获取全部的数据,需要访问多个节点,并且在网络中传输不同部分的结果,最后进行合并。可是,网络传输的速度,相对于本机的硬盘和内存读取都要慢很多,因此就拖累了数据查询的执行过程。

❑ 无法高效存储大量小文件。对于HDFS而言,如果存在太多的琐碎文件,那就意味着存在庞大的元数据需要处理,这无疑大大增加了命名节点的负载。命名节点检索的效率明显下降,最终也会导致整体的处理速度放缓。

不过,整体而言,HDFS还是拥有良好的设计的,对Hadoop及其生态体系的流行起到了关键的作用。它所提供的对应用程序数据的高吞吐量访问,非常适合于存储大量数据,例如用户行为日志。在本书的第11章关于用户行为跟踪的内容中,我们将展示怎样结合使用HDFS与Flume。

而Hadoop的另一个要素MapReduce,其核心是哈希表的映射结构,其包含如下几个重要的组成模块。

❑ 数据分割(Data Splitting):将数据源进行切分,并将分片发送到Mapper上。例如将文档的每一行作为最小的处理单元。

❑ 映射(Mapping):Mapper根据应用的需求,将内容按照键-值的匹配,存储到哈希结构中<k1, v1>。例如,将文本进行中文分词,然后生成<牛奶,1>这样的配对,表示“牛奶”这个词出现了一次。

❑ 洗牌(Shuff ling):不断地将键-值的配对发给Reducer进行归约。如果存在多个Reducer,则还会使用分配(Partitioning)对Reducer进行选择。例如,“牛奶”“巧克力”“海鲜”这种属于商品的单词,专门交给负责统计商品列表的Reducer来完成。

❑ 归约(Reducing):分析所接受到的一组键值配对,如果是与键内容相同的配对,那就将它们的值进行合并。例如,一共收到12个<牛奶,1>({<牛奶,1>, <牛奶,1>}…<牛奶,1>}),那么就将其合并为<牛奶,12>。最终“牛奶”这个单词的词频就统计为12。

为了提升洗牌阶段的效率,可以减少发送到归约阶段的键-值配对。具体的做法是在映射和洗牌之间,加入合并(Combining)的过程,在每个Mapper节点上先进行一次本地的归约,然后只将合并后的结果发送到洗牌和归约阶段。

图1-4展示了MapReduce框架的基本流程和对应的模块。

图1-4 MapReduce的基本框架

有了分布式文件系统(HDFS)和分布式计算框架MapReduce这两驾马车保驾护航,Hadoop系统近几年的发展可谓风生水起。不过,人们也意识到MapReduce框架的一些问题。比如,工作跟踪节点Job Tracker,它是MapReduce的集中点,完成了太多的任务,造成了过多的资源消耗,存在单点故障的可能性较大。而在任务跟踪(Task Tracker)节点端,用任务的数量来衡量负载的方式则过于简单,没有考虑中央运算器CPU、内存和硬盘等的使用情况,有可能会出现负载不均和某些节点的过载。Map和Reduce任务的严格划分,也可能会导致某些场合下系统的资源没有被充分利用。

面对种种问题,研发人员开始思考新的模式,包括Apache Hadoop YARN(Yet Another Resource Negotiator)http://hadoop.apache.org/docs/current/hadoop-yarn/hadoop-yarn-site/index.html等。Apache Hadoop YARN是一种新的资源管理器,为上层应用提供了统一的Hadoop资源管理和调度。YARN将工作跟踪(Job Tracker)节点的两个主要功能分成了两个独立的服务程序:全局的资源管理器(Resource Manager)和针对每个应用的主节点(Application Master)。如此设计是为了让子任务的监测进行分布式处理,大幅减少了工作跟踪节点的资源消耗。同时,这里所说的应用既可以是传统意义上的MapReduce任务,也可以是基于有向无环图(DAG)的任务。因此,在YARN的基础上,甚至还可以运行Spark和Storm这样的流式计算和实时性作业,并利用Hadoop集群的计算能力和丰富的数据存储模型,提升数据共享和集群的利用率。对于Hadoop更多细节感兴趣的读者,可以阅读《大数据架构商业之路》的第3章和第4章。

1.6 案例实践

1.6.1 实验环境设置

帮助读者熟悉理论知识并不是本书的最终目的。为了展示分类任务的常规实现,我们会实践一个假想的案例,让机器对18类共28000多件商品进行自动分类。下面是商品数据的片段:

ID    Title     CategoryID           CategoryName
1     雀巢 脆脆鲨 威化巧克力(巧克力味夹心)20g*24/盒  1        饼干
2     奥利奥 原味夹心饼干 390g/袋    1        饼干
3     嘉顿 香葱薄饼 225g/盒          1        饼干
4     Aji 苏打饼干 酵母减盐味 472.5g/袋      1        饼干
5     趣多多 曲奇饼干 经典巧克力原味 285g/袋  1        饼干
6     趣多多 曲奇饼干 经典巧克力原味 285g/袋  X 2     1        饼干
7     Aji 尼西亚惊奇脆片饼干 起士味 200g/袋  1        饼干
8     格力高 百醇 抹茶慕斯+提拉米苏+芝士蛋糕味48g*3盒 1        饼干
9     奥利奥 巧克力味夹心饼干 390g/袋         1        饼干
10    趣多多 巧克力味曲奇饼干 香脆米粒味 85g/袋        1        饼干
11    趣多多 巧克力味曲奇饼干 香脆米粒味 85g/袋 X 5   1        饼干
12    …

可以看到,每条记录有4个字段,包括商品的ID(ID)、商品的标题(Title)、分类的ID (CategoryID)和分类的名称(CategoryName)等。完整的数据集合位于:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/listing.txt

注意

本书所有案例中的测试数据,包括以上的商品数据都是虚构的,仅供教学和实验使用。请不要将其内容或产生的结论用于任何生产环境。

针对这些数据,我们将分别使用R包和Mahout对其进行分类处理。此外,由于测试数据包含中文标题,因此还需要中文分词软件对其进行处理。相关的编码将采用Java语言(JDK 1.8),以及Eclipse的IDE环境(Neon.1a Release (4.6.1))来实现。

目前运行R、Mahout、中文分词及相关代码的硬件是一台2015款的iMac一体机,在后文中我们将为它冠以iMac2015的代号,其CPU为Intel Core i7 4.0GHz,内存为16GB,其具体配置如图1-5所示。

图1-5 iMac2015的配置

下面将展示并分析每个关键的步骤,直至机器可以对商品合理分类。

1.6.2 中文分词

在对文本进行分类测试之前,首先要将文本转换成机器能够理解的数据来表示。对于这个步骤,一种常见的方法是词包(Bag of Word),即将文本按照单词进行划分,并建立字典。每个唯一的单词则是组成字典的词条,同时也成为特征向量中的一维。最终文本就被转换成为拥有多个维度的向量。对于英文等拉丁语,单词的划分是非常直观的,空格和标点符号就可以满足大多数的需求。然而,对于中文而言却要困难得多。中文只有字、句和段能够通过明显的分界符来简单划界,词与词之间没有一个形式上的分界符。为此,中文分词的研究应运而生,其目的就是将一个汉字序列切分成一个个单独的词。目前有不少开源的中文分词软件可供使用,这里使用知名的IKAnalyzer,你可以通过如下链接下载其源码和相关的配置文件:

http://www.oschina.net/p/ikanalyzer/

Eclipse Neon.1a Release (4.6.1)的版本在默认的情况下,自带了MavenMaven是一种软件项目管理工具,其项目对象模型(POM)可以通过一小段描述信息来管理项目的构建。的插件,我们可以建立一个Maven项目并导入IKAnalyzer的源码。图1-6展示了Maven项目的建立。

图1-6 建立Maven项目,并导入IKAnalyzer源码

项目中的pom.xml内容配置如下:

<project  xmlns="http://maven.apache.org/POM/4.0.0"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>ChineseSegmentation</groupId>
    <artifactId>IKAnalyzer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>IKAnalyzer</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
    <dependencies>
        <! -- https://mvnrepository.com/artifact/org.apache.lucene/
        lucene-analyzers-common -->

        <dependency>
                <groupId>org.apache.lucene</groupId>
                <artifactId>lucene-analyzers-common</artifactId>
                <version>4.0.0</version>
        </dependency>

        <! -- https://mvnrepository.com/artifact/org.apache.lucene/
        lucene-queryparser -->
        <dependency>
                <groupId>org.apache.lucene</groupId>
                <artifactId>lucene-queryparser</artifactId>
                <version>4.0.0</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

其中加入了lucene-analyzers-common和lucene-queryparser的依赖。这样IKAnalyzer的源码就可以编译成功。你可以从下面的链接访问已建成的IKAnalyzer Maven项目:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/tree/master/Classif ication/IKAnalyzer

在org.wltea.analyzer.sample.IKAnalzyerDemo的基础上,我们编写了org.wltea.analyzer. sample.IKAnalzyerForListing,其主要的函数如下:

public static void processListing(String inputFileName, String outputFileName) {

    try {

        br = new BufferedReader(new FileReader(inputFileName));
        pw = new PrintWriter(new FileWriter(outputFileName));

        String strLine = br.readLine();         //跳过header这行
        pw.println(strLine);
        while ((strLine = br.readLine()) ! = null) {

            // 获取每个字段
            String[] tokens = strLine.split("\t");
            String id = tokens[0];
            String title = tokens[1];
            String cateId = tokens[2];
            String cateName = tokens[3];

            // 对原有商品标题进行中文分词
            ts = analyzer.tokenStream("myfield", new StringReader(title));
            // 获取词元位置属性
            OffsetAttribute   offset = ts.addAttribute(OffsetAttribute.class);
            // 获取词元文本属性
            CharTermAttribute term = ts.addAttribute(CharTermAttribute.class);
            // 获取词元文本属性
            TypeAttribute type = ts.addAttribute(TypeAttribute.class);

            // 重置TokenStream(重置StringReader)
            ts.reset();
            // 迭代获取分词结果
            StringBuffer sbSegmentedTitle = new StringBuffer();
            while (ts.incrementToken()) {
                sbSegmentedTitle.append(term.toString()).append(" ");
            }

            // 重新写入分词后的商品标题
            pw.println(String.format("%s\t%s\t%s\t%s", id, sbSegmentedTitle.
            toString().trim(), cateId, cateName));

            // 关闭TokenStream(关闭StringReader)
            ts.end(); //Perform end-of-stream operations, e.g. set the final offset.

        }

        br.close();
        br = null;

        pw.close();
        pw = null;

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        cleanup();
    }

}

其功能在于打开原始的数据文件,读取每一行,取出商品的标题,采用IKAnalyzer对其标题进行分词,然后生成一个使用分词后标题的新数据文件。新数据文件的片段如下:

ID    title     CategoryID         CategoryName
1     雀巢 脆 脆 鲨 威 化 巧克力 巧克力 味 夹心 20g 24 盒          1        饼干
2     奥 利 奥 原 味 夹心饼干 390g 袋         1          饼干
3     嘉 顿 香 葱 薄饼 225g 盒      1         饼干
4     aji 苏打饼干 酵母 减 盐味 472.5g 袋    1          饼干
5     趣 多多 曲奇 饼干 经典 巧克力 原 味 285g 袋        1          饼干
6     趣 多多 曲奇 饼干 经典 巧克力 原 味 285g 袋 x 2   1          饼干
7     aji 尼 西亚 惊奇 脆 片 饼干 起 士 味 200g 袋      1          饼干
8     格力 高 百 醇 抹 茶 慕 斯 提 拉 米 苏 芝 士 蛋糕 味 48g 3盒 1        饼干
9     奥 利 奥 巧克力 味 夹心饼干 390g 袋     1          饼干
10    趣 多多 巧克力 味 曲奇 饼干 香脆 米粒 味 85g 袋    1          饼干
11    趣 多多 巧克力 味 曲奇 饼干 香脆 米粒 味 85g 袋 x 5          1        饼干
12    ...

可以看出每个标题都被进行了切分。当然,我们也发现中文分词软件也不一定100%准确。在上述的例子中,对于某些品牌的切分出现了错误。好在IKAnalyzer是支持自定义字典的,我们可以编辑class运行目录中的ext.dic,加入必要的品牌词,如图1-7所示。

图1-7 修改扩展词典ext.dic

再次运行分词,可以看到品牌被正确地切分出来了:

ID    title     CategoryID       CategoryName
1     雀巢 脆 脆 鲨 威 化 巧克力 巧克力 味 夹心 20g 24 盒         1        饼干
2     奥利奥 原 味 夹心饼干 390g 袋           1        饼干
3     嘉顿 香 葱 薄饼 225g 盒     1           饼干
4     aji 苏打饼干 酵母 减 盐味 472.5g 袋    1        饼干
5     趣多多 曲奇 饼干 经典 巧克力 原 味 285g 袋      1           饼干
6     趣多多 曲奇 饼干 经典 巧克力 原 味 285g 袋 x 2  1           饼干
7     aji 尼 西亚 惊奇 脆 片 饼干 起 士 味 200g 袋   1           饼干
8     格力高 百 醇 抹 茶 慕 斯 提 拉 米 苏 芝 士 蛋糕 味 48g 3盒 1        饼干
9     奥利奥 巧克力 味 夹心饼干 390g 袋       1        饼干
10    趣多多 巧克力 味 曲奇 饼干 香脆 米粒 味 85g 袋   1           饼干
11    趣多多 巧克力 味 曲奇 饼干 香脆 米粒 味 85g 袋 x 5           1        饼干
12    ...

完整的分词后的数据集合位于:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/listing-segmented.txt

当然,中文分词是一个很有挑战性的课题,特别是针对存在歧义的情况,分词算法通常无法保证切分完全准确。由于这不是本章讨论的重点,因此这里暂时忽略可能存在的错误。接下来,就是使用R中的机器学习包,对分词后的标题进行分类。

1.6.3 使用R进行朴素贝叶斯分类

1.R的基础

目前为止,R的最新版本是3.3.2,可以从如下的链接选择合适的平台下载并安装:

https://cran.r-project.org/mirrors.html

安装后再运行,你将看到如图1-8所示的界面,这实际上就是一个输入命令的终端。你可以在提示符“>”后面输入并执行一条命令,或者通过编写脚本一次性执行多个命令。R支持很多数据类型,例如向量、矩阵、列表等。

图1-8 R启动后的初始画面,显示了版本和帮助信息

下面让我们看几条基本的命令,包括最简单的函数c(),它可以让你输入一个向量,例如下面的两条命令:

> apple.a <- c(1,1,1,2,1,1)
> apple.a
[1] 1 1 1 2 1 1

灵感依旧来自前述的水果案例,第一条命“> apple.a <- c (1, 1, 1, 2, 1, 1)”是将苹果a虚构的特征值以向量的形式赋予对象apple.a,其中“<-”表示赋值。第二条命令是显示apple. a,是不是很简单呢?依此类推,可以手动建立多个水果对象,展示如下:

> apple.b <- c(1,1,1,1,1,1)
> apple.c <- c(2,3,1,1,2,1)
> orange.a <- c(2,2,1,1,2,2)
> orange.b <- c(2,2,1,2,2,2)
> orange.c <- c(1,2,1,2,1,1)
> watermelon.a <- c(3,3,2,3,1,2)
> watermelon.b <- c(3,3,2,3,1,1)
> watermelon.c <- c(3,3,2,3,1,2)
> watermelon.d <- c(1,3,2,3,2,2)
> ls()
  [1] "apple.a"        "apple.b"     "apple.c"         "applea"          "orange.a"
  [6] "orange.b"      "orange.c"    "watermelon.a"   "watermelon.b"   "watermelon.c"
[11] "watermelon.d"

其中ls()是列出当前定义的所有对象。除了允许用户在终端手工输入信息之外,R还支持从文本文件、数据库系统,甚至是其他统计软件上导入数据,对于数据源的整合很有益处。有了这些数据,要进行基础的处理就变得非常快捷。下面的命令分别列出了西瓜a作为数组处理时,其最大值、最小值、均值、中位数、方差和标准差的数值。

> max(watermelon.a)
[1] 3
> min(watermelon.a)
[1] 1
> mean(watermelon.a)
[1] 2.333333
> median(watermelon.a)
[1] 2.5
> var(watermelon.a)
[1] 0.6666667
> sd(watermelon.a)
[1] 0.8164966

至此,你已经开始了R工作的第一步,那如何保存这些成果呢?别急,R还提供了工作间(Workspace)的概念,即指当前的工作环境。通过保存工作间的镜像,你可以存储用户定义的数据对象和一些设置,R在下次启动时会自动加载所有这些内容。在这些基础之上,让我们看看如何利用现有的扩展包,快捷地构建基于朴素贝叶斯的分类器。

2.文本数据预处理

在使用R的扩展包对商品标题进行分类之前,除了中文分词以外,还有一系列其他的预处理工作,具体如下。

1)打散样本。

2)将样本加载到R的变量中。

3)将样本集合变量转换为文档集和文档-单词矩阵。

4)切分训练和测试数据集。

(1)打散样本

由于要使用同一个样本集合生成训练数据和测试数据,所以需要保证不同分类的样本出现的顺序足够随机,否则切分的时候容易导致某些分类在训练数据中出现的次数过少甚至不出现的情况。这种情形最终会导致拟合出的模型会有偏差,分类预测效果不理想,无法反映理论模型的真实性能等。如果你的样本按照分类来看其出现的顺序已经足够随机,那么可以跳过这一步。从listing-segmented.txt中可以看出,同一分类的数据都是紧密相邻的,一个分类结束之后才会出现下一个分类,因此不满足随机性的条件,我们需要某种随机的方式,将样本出现的顺序打散。

通常,打散可以分为两种方式,一种是预先将样本文件的顺序打乱,另一种是在使用R切分训练和测试数据时进行打散。这里采用第一种方法,目的是便于用户重现此处的实验,并在后面不同的算法或系统实践时重用相同的数据。具体的实现请参考org.wltea.analyzer. sample.IKAnalzyerForListing中的另一个函数processListingWithShuff le:

public static void processListingWithShuffle(String inputFileName, String output FileName) {

try {

    br = new BufferedReader(new FileReader(inputFileName));
    pw = new PrintWriter(new FileWriter(outputFileName));

    ArrayList<String> samples = new ArrayList<String>();

    String strLine = br.readLine();         //跳过header这一行
    pw.println(strLine);
    while ((strLine = br.readLine()) ! = null) {

        // 获取每个字段
        String[] tokens = strLine.split("\t");
        String id = tokens[0];
        String title = tokens[1];
        String cateId = tokens[2];
        String cateName = tokens[3];

        // 对原有的商品标题进行中文分词
        ts = analyzer.tokenStream("myfield", new StringReader(title));
        // 获取词元位置属性
        OffsetAttribute   offset = ts.addAttribute(OffsetAttribute.class);
        // 获取词元文本属性
        CharTermAttribute term = ts.addAttribute(CharTermAttribute.class);
        // 获取词元文本属性
        TypeAttribute type = ts.addAttribute(TypeAttribute.class);

        // 重置TokenStream(重置StringReader)
        ts.reset();
        // 迭代获取分词结果
        StringBuffer sbSegmentedTitle = new StringBuffer();
        while (ts.incrementToken()) {
            sbSegmentedTitle.append(term.toString()).append(" ");

        }

        samples.add(String.format("%s\t%s\t%s\t%s", id, sbSegmentedTitle.
        toString().trim(), cateId, cateName));

        // 关闭TokenStream(关闭StringReader)
        ts.end(); //Perform end-of-stream operations, e.g. set the final offset.

    }
    br.close();
    br = null;

    Random rand = new Random(System.currentTimeMillis());
    while (samples.size() > 0) {
        int index = rand.nextInt(samples.size());
            pw.println(samples.get(index));
            samples.remove(index);
        }

        pw.close();
        pw = null;

    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        cleanup();
    }

}

其增加的主要部分是利用Random随机抽函数,每次随机抽取出一个样本生成新的序列。打散后的全部数据可参见:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/listing-segmented-shuff led.txt

下面的文本片段是该文件的开头部分:

ID      Title    CategoryID        CategoryName
22785  samsung 三星 galaxy tab3 t211 1g 8g wifi+3g 可 通话 平板 电脑 gps 300万像素 白色   15  电脑
19436  samsung 三星 galaxy fame s6818 智能手机 td-scdma gsm 蓝色 移动 定制 机              14  手机
3590   金本位 美味 章 鱼丸 250g    3    海鲜水产
3787   莲花 居 预售 阳澄湖 大闸蟹 实物 558 型 公 3.3-3.6 两 母 2.3-2.6 两 5对 装             3   海鲜水产
11671  rongs 融 氏 纯 玉米 胚芽油 5l 绿色食品 非 转基因 送 300ml 小 油 1瓶                   9   食用油
23188  kerastase 卡 诗 男士 系列 去 头屑 洗发水 250ml 去 屑 止痒 男士 专用 进口 专业 洗 护发  16  美发护发
25150  dove 多 芬 丰盈 宠 肤 沐浴 系列 乳 木 果 和 香草 沐浴乳 400ml 5瓶                     17  沐浴露
14707  魏 小 宏 weixiaohong 长寿 枣 400克 袋装 美容 养颜 安徽 宣城 水 东 特产                10  枣类
28657  80 茶客 特级 平阴 玫瑰花 玫瑰 茶 花草 茶 花茶 女人 茶 冲 饮 50克 袋                    18  茶叶
6275   德芙 兄弟 品牌 脆 香米 脆 米 心 牛奶 巧克力 500g 散装                                   6   巧克力
18663  十月 稻田 五常 稻 花香 大米 5kg 袋 x 2                                                 12  大米
15229  …

(2)加载变量

接下来使用read.csv命令,将本地文件系统中的listing-segmented-shuff led.txt导入为R的变量listing:

> listing <- read.csv("/Users/huangsean/Coding/data/BigDataArchitectureAndAlgori thm/listing-segmented-shuffled.txt", stringsAsFactors = FALSE, sep='\t')

然后查看listing的基本情况:

> str(listing)
'data.frame':   28706 obs. of   4 variables:
  $ ID          : int   22785 19436 3590 3787 11671 23188 25150 14707 28657 6275 ...
  $ Title     : chr  "samsung 三星 galaxy tab3 t211 1g 8g wifi+3g 可 通话 平板 电脑 gps 300万像素 白色" "samsung 三星 galaxy fame s6818 智能手机 td-scdma gsm 蓝色 移动 定制 机" "金本位美味 章 鱼丸 250g" "莲花 居 预售 阳澄湖 大闸蟹 实物 558 型 公 3.3-3.6 两 母 2.3-2.6 两 5对 装" ...$ CategoryID : int 15 14 3 3 9 16 17 10 18 6 ...
$ CategoryName: chr "电脑" "手机" "海鲜水产" "海鲜水产" ...

像CategoryID、CategoryName这样的字段,我们希望它们可以按照唯一性进行分组,因此将其转换为R中的因子(factor)类型。首先将factor(listing$CategoryID)赋予listing$CategoryID,并再次查看listing的基本情况:

> listing$CategoryID <- factor(listing$CategoryID)
> str(listing)
'data.frame':   28706 obs. of   4 variables:
  $ ID          : int   22785 19436 3590 3787 11671 23188 25150 14707 28657 6275 ...
  $ Title     : chr  "samsung 三星 galaxy tab3 t211 1g 8g wifi+3g 可 通话 平板 电脑 gps 300万像素 白色" "samsung 三星 galaxy fame s6818 智能手机 td-scdma gsm 蓝色 移动 定制 机" "金本位美味 章 鱼丸 250g" "莲花 居 预售 阳澄湖 大闸蟹 实物 558 型 公 3.3-3.6 两 母 2.3-2.6 两 5对 装" ...
  $ CategoryID  : Factor w/ 18 levels "1", "2", "3", "4", ..: 15 14 3 3 9 16 17 10 18 6 ...
  $ CategoryName: chr  "电脑" "手机" "海鲜水产" "海鲜水产" ...

你会发现CategoryID的描述发生了变化。此外,还可以使用table命令查看每个分类ID的数量(也就是每个分类的样本数量):

> table(listing$CategoryID)
  1    2     3    4    5     6     7      8      9    10    11     12   13    14     15    16    17    18 1874  818  1815 665  334  1837  2331  1896  1573  1691 1804  2400 258  1800  1800  1844 1953  2013

对于CategoryName,可以进行同样的操作:

> listing$CategoryName <- factor(listing$CategoryName)
> table(listing$CategoryName)
    坚果     大米  巧克力  手机     新鲜水果  方便面  枣类  沐浴露 海鲜水产  电脑  纯牛奶  美发护发  茶叶
    进口牛奶  面粉  食用油  饮料饮品  饼干
    1896     2400  1837    1800     1804     818    1691  1953   1815     1800  334    1844     2013
    665      258   1573    2331     1874

(3)生成文档-单词矩阵

根据之前所述,文本分类常常采用词包(Bag of Word)的数据表示方法,所以我们需要将listing变量转变为文档对单词的二维矩阵。首先,我们要使用install.packages()函数安装R的文本挖掘包tm:

> install.packages("tm")

第一次运行扩展包的安装时,可能需要选择最佳的镜像站点,如图1-9所示。

图1-9 安装R扩展包时,选择镜像站点

运行后R会自动进行安装:

> install.packages("tm")
--- Please select a CRAN mirror for use in this session ---
also installing the dependencies‘NLP’, ‘slam’
trying URL 'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/NLP_0.1-9.tgz'
Content type 'application/x-gzip' length 278807 bytes (272 KB)
==================================================
downloaded 272 KB

trying URL 'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/slam_
0.1-40.tgz'
Content type 'application/x-gzip' length 106561 bytes (104 KB)
==================================================
downloaded 104 KB

trying URL 'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/tm_0.6-2.tgz'
Content type 'application/x-gzip' length 665347 bytes (649 KB)
==================================================
downloaded 649 KB

The downloaded binary packages are in
    /var/folders/fr/gb14wrwn5296_7rmyrhx1wsw0000gn/T//RtmprXQjRG/downloaded_packages

然后使用library()函数加载tm:

> library(tm)
Loading required package: NLP

加载完成后,就可以使用VCorpus (VectorSource (listing$Title))命令,将listing的Title字段取出并转变为文档集合listing_corpus。命令inspect (listing_corpus[1:3])可以帮助你检视前3个标题记录的基本情况:

> listing_corpus <- VCorpus(VectorSource(listing$Title))
> print(listing_corpus)
<<VCorpus>>
Metadata:   corpus specific: 0, document level (indexed): 0
Content:   documents: 28706
> inspect(listing_corpus[1:3])
<<VCorpus>>
Metadata:   corpus specific: 0, document level (indexed): 0
Content:   documents: 3

[[1]]
<<PlainTextDocument>>
Metadata:   7
Content:   chars: 66

[[2]]
<<PlainTextDocument>>
Metadata:   7
Content:   chars: 57

[[3]]
<<PlainTextDocument>>
Metadata:   7
Content:   chars: 16

下一步是使用DocumentTermMatrix()函数从listing_corpus获取文档-单词矩阵listing_dtm:

> listing_dtm <- DocumentTermMatrix(listing_corpus, control=list(wordLengths=c( 0, Inf)))
> listing_dtm
<<DocumentTermMatrix (documents: 28706, terms: 16458)>>
Non-/sparse entries: 359791/472083557
Sparsity              : 100%
Maximal term length: 25
Weighting             : term frequency (tf)

其中,需要注意的是,DocumentTermMatrix函数原本是针对英文单词进行编码的。由于只包含1个或2个字母的英文单词基本上都没有意义,所以这个函数默认会去除字符长度小于3的单词。但是,这里处理的是中文,而且很多重要的中文词都是少于3个字符的,例如本案例中的“牛奶” “茶叶” “手机” “水” “酒”等。这些词都是分类的重要线索,不能丢弃,所以我们要加上参数control=list (wordLengths=c (0, Inf)),保留全部的中文词,单词总数量是16458,文档总数量是28706。下一步是使用convert函数将矩阵中的词频tf转变为在R中朴素贝叶斯分类所需的“Yes”和“No”值:

> convert <- function(x) { x <- ifelse(x > 0, "Yes", "No") }
> listing_all <- apply(listing_dtm, MARGIN = 2, convert)

(4)切分训练和测试集

在正式上线之前,监督式学习算法很重要的一步就是进行离线的测试。针对标注的数据我们可以切分出训练和测试集合,来实现这个目标。由于之前已经打散了样本数据,所以可以直接将前90%的数据作为训练样本,后10%的作为测试样本:

> listing_train <- listing_all[1:25835, ]
> listing_test <- listing_all[25836:28706, ]

除了样本内容的切分,分类标签也需要切分,我们可以使用CategoryID或CategoryName来实现:

> listing_train_labels <- listing[1:25835, ]$CategoryID
> listing_test_labels <- listing[25836:28706, ]$CategoryID

前面也提到过,如果你的样本数据尚未提前打散,那么也可以在R中进行此步骤:

> split.data = function(data, p = 0.9, s = 888){
    +     set.seed(s)
    +     index = sample(1:dim(data)[1])
    +     train = data[index[1:floor(dim(data)[1] * p)], ]
    +     test = data[index[((floor(dim(data)[1] * p)) +
    1):dim(data)[1]], ]
    + return(list(train = train, test = test))
    +}

> twosets = split.data(listing_all, p = 0.9)
> listing_train = twosets$train
> listing_train = twosets$test

后面使用convert函数进行转变的步骤与之前的相似。

3.训练、预测和评估

实现朴素贝叶斯的R扩展包是e1071,安装该包的命令如下:

> install.packages("e1071")
trying  URL  'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/e10
71_1.6-7.tgz'
Content type 'application/x-gzip' length 752286 bytes (734 KB)
==================================================
downloaded 734 KB
The downloaded binary packages are in
    /var/folders/fr/gb14wrwn5296_7rmyrhx1wsw0000gn/T//RtmprXQjRG/downloaded_packages> library(e1071)

然后就可以使用listing_train进入训练阶段,实现模型的拟合了:

> listing_classifier <- naiveBayes(listing_train, listing_train_labels)

模型存放于listing_classif ier,它包括每个分类的出现概率:

> listing_classifier

Naive Bayes Classifier for Discrete Predictors

Call:
naiveBayes.default(x = listing_train, y = listing_train_labels)

A-priori probabilities:
listing_train_labels
          1                2                3               4                5               6                7
          8                9               10              11              12              13               14
        15               16               17              18
0.065453842    0.028682021     0.063324947   0.023185601    0.011302497   0.063712019    0.080665763
0.066034449    0.053802980     0.059686472   0.062279853    0.084730017   0.008747823    0.063208825
0.062395974    0.064370041     0.068124637   0.070292239

Conditional probabilities:
...

在Conditional probabilities部分包含了每个词在不同分类中出现的概率,可以通过listing_classif ier$tables快速查看某个特定的词,例如:

> listing_classifier$tables[[’小米’]]
                      小米
listing_train_labels              No             Yes
                1   1.0000000000 0.0000000000
                2   0.9973009447 0.0026990553
                3   1.0000000000 0.0000000000
                4   1.0000000000 0.0000000000
                5   1.0000000000 0.0000000000
                6   1.0000000000 0.0000000000
                7   1.0000000000 0.0000000000
                8   1.0000000000 0.0000000000
                9   0.9992805755 0.0007194245
                10  1.0000000000 0.0000000000
                11  0.9993784960 0.0006215040
                12  1.0000000000 0.0000000000
                13  0.9955752212 0.0044247788
                14  0.9546846295 0.0453153705
                15  1.0000000000 0.0000000000
                16  1.0000000000 0.0000000000
                17  1.0000000000 0.0000000000
                18  1.0000000000 0.0000000000
> listing_classifier$tables[[’牛奶’]]
                    牛奶
listing_train_labels            No             Yes
                1   0.9379065642 0.0620934358
                2   1.0000000000 0.0000000000
                3   1.0000000000 0.0000000000
                4   0.3238731219 0.6761268781
                5   0.4657534247 0.5342465753
                6   0.7982989064 0.2017010936
                7   0.9846449136 0.0153550864
                8   0.9994138335 0.0005861665
                9   1.0000000000 0.0000000000
                10  0.9948119326 0.0051880674
                11  0.9975139838 0.0024860162
                12  1.0000000000 0.0000000000
                13  1.0000000000 0.0000000000
                14  1.0000000000 0.0000000000
                15  1.0000000000 0.0000000000
                16  1.0000000000 0.0000000000
                17  0.9818181818 0.0181818182
                18  1.0000000000 0.0000000000
> listing_classifier$tables[[’手机’]]
                    手机
listing_train_labels            No             Yes
                1   1.0000000000 0.0000000000
                2   1.0000000000 0.0000000000
                3   1.0000000000 0.0000000000
                4   1.0000000000 0.0000000000
                5   1.0000000000 0.0000000000
                6   1.0000000000 0.0000000000
                7   0.9990403071 0.0009596929
                8   0.9994138335 0.0005861665
                9   1.0000000000 0.0000000000
                10  1.0000000000 0.0000000000
                11  1.0000000000 0.0000000000
                12  1.0000000000 0.0000000000
                13  1.0000000000 0.0000000000
                14  0.3949785671 0.6050214329
                15  0.9950372208 0.0049627792
                16  1.0000000000 0.0000000000
                17  1.0000000000 0.0000000000
                18  0.9994493392 0.0005506608

从上述三个关键词的例子可以看出,“小米”这个词在分类14(手机)中有一定的出现概率(注意是概率而不是绝对次数),在分类13(面粉)中也有一点出现概率;“牛奶”一词在分类4(进口牛奶)和分类5(纯牛奶)中出现的概率很高;“手机”一词在分类14(手机)中出现的概率很高。有了这些先验概率,就可以根据贝叶斯理论预估后验概率。使用该模型对测试集合listing_test进行预测的命令如下:

> listing_test_pred <- predict(listing_classifier, listing_test)

不过在使用本案例的数据集合时,你很可能会发现在运行预测函数predict之后,系统抛出了异常“Error in '[.default'(object$tables[[v]], , nd) : subscript out of bounds”,如图1-10所示。

图1-10 e1071中的朴素贝叶斯分类器抛出了异常

经过仔细排查,我们发现出现异常的根本原因是某些词在训练样本中没有出现过,但是在测试样本中却出现了。例如如下这个被测试的样本:

1277     古 陵 山 大 薯 核桃 曲奇 112g 3 每 一口 都能 吃到 核桃仁 山西 晋城 休闲 办公室 零食
        1        饼干

其中包含了“都能”这个词,但是在训练样本中没有出现过“都能”。这一点可以使用listing_classif ier$tables来验证,你会发现这个词在所有类的训练样本中都没有出现过,因此只有“No”这一个列,如下所示:

> listing_classifier$tables[[’都能’]]
                      都能
listing_train_labels No
                1     1
                2     1
                3     1
                4     1
                5     1
                6     1
                7     1
                8     1
                9     1
                10    1
                11    1
                12    1
                13    1
                14    1
                15    1
                16    1
                17    1
                18    1

而查看e1071中朴素贝叶斯的实现源码,发现它并没有考虑这种极端情况:

predict.naiveBayes <- function(object,
                                newdata,
                                type = c("class", "raw"),
                                threshold = 0.001,
                                ...) {
    type <- match.arg(type)
    newdata <- as.data.frame(newdata)
    attribs <- match(names(object$tables), names(newdata))
    isnumeric <- sapply(newdata, is.numeric)
    newdata <- data.matrix(newdata)
    L <- sapply(1:nrow(newdata), function(i) {
        ndata <- newdata[i, ]
        L <- log(object$apriori) + apply(log(sapply(seq_along(attribs),
            function(v) {
                nd <- ndata[attribs[v]]
                if (is.na(nd)) rep(1, length(object$apriori)) else {
                    prob <- if (isnumeric[attribs[v]]) {
                        msd <- object$tables[[v]]
                    msd[, 2][msd[, 2] == 0] <- threshold
                    dnorm(nd, msd[, 1], msd[, 2])
                    }    else object$tables[[v]][, nd]
                prob[prob == 0] <- threshold
                prob
                }
            })), 1, sum)
        if (type == "class")
            L
        else {
            ## Numerically unstable:
            ##        L <- exp(L)
            ##        L / sum(L)
            ## instead, we use:
            sapply(L, function(lp) {
                1/sum(exp(L - lp))
            })
        }
    })
    if (type == "class")
        factor(object$levels[apply(L, 2, which.max)], levels = object$levels)
    else t(L)
}

object$tables[[v]][, nd]并未考虑第2列不存在的情况,因此导致分类器抛出下标越界的异常。为此我们在相应的部分加入判定,并针对训练样本中未出现的新词赋予最小的threshold值:

if (dim(object$tables[[v]])[2] < 2) {
    prob<-vector(mode="numeric", length=0)
    for(i in 1:dim(object$tables[[v]])[1])
    {
            prob[i] <- 0
    }
    } else {
            prob <- if (isnumeric[attribs[v]]) {
            msd <- object$tables[[v]]
            msd[, 2][msd[, 2] == 0] <- threshold
            dnorm(nd, msd[, 1], msd[, 2])
            } else object$tables[[v]][, nd]
        }
        prob[prob == 0] <- threshold
        prob
    }

完整的修正代码位于:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/R/predict.naiveBayes.r

再次运行预测函数就不会产生越界的错误了,预测的结果会保存于listing_test_pred中。最后可通过gmodels包中的函数进行评估,首先安装相应的扩展包:

> install.packages("gmodels")
also installing the dependencies‘gtools’, ‘gdata’

trying URL 'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/gtools_3.5.0.tgz'
Content type 'application/x-gzip' length 134356 bytes (131 KB)
==================================================
downloaded 131 KB

trying  URL  'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/gdata_2.17.0.tgz'
Content type 'application/x-gzip' length 1136842 bytes (1.1 MB)
==================================================
downloaded 1.1 MB

trying URL 'https://cran.cnr.berkeley.edu/bin/macosx/mavericks/contrib/3.3/gmodels_2.16.2.tgz'
Content type 'application/x-gzip' length 72626 bytes (70 KB)
==================================================
downloaded 70 KB

The downloaded binary packages are in
    /var/folders/fr/gb14wrwn5296_7rmyrhx1wsw0000gn/T//RtmprXQjRG/downloaded_packages
> library(gmodels)

然后使用CrossTable()函数计算混淆矩阵:

> CrossTable(listing_test_pred, listing_test_labels, prop.chisq = FALSE, prop.t= FALSE, dnn = c(’预测值’, ’实际值’))

图1-11展示了混淆矩阵的局部内容,通过这个局部内容的左上角可以看出,分类1中共有177个测试样例被正确地预测为分类1,该类的精度为92.7%,召回率为96.7%,而前6个分类中分类5(纯牛奶)的预测性能最差,召回率只有69%,精度只有66%,从图1-12可以看出,主要是纯牛奶(非进口)和进口牛奶两个分类容易混淆,从字面上来看两个分类过于接近。混淆矩阵全部内容可以参见:

图1-11 朴素贝叶斯分类结果的混淆矩阵之局部

图1-12 分类4(进口牛奶)和分类5(纯牛奶)容易混淆

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Cla-ssif ication/R/NaiveBayes.Results1.txt

总体来说,18个分类中有16个分类的召回率和精度都在90%以上,全局的准确率在96%以上,分类效果较好。当然,我们可以使用10-folder的交叉验证,轮流测试10%的数据并获取每个类的平均召回率和精度,以及全局的平均准确率。

给定机器学习的模型,我们可以改变训练样本的数量,或者是用于分类的特征,来测试该模型的效果。这里将训练集合放大到99%的标注数据,而测试样本为1%的标注数据:

> listing_train <- listing_all[1:28419, ]
> listing_test <- listing_all[28420:28706, ]
> listing_train_labels <- listing[1:28419, ]$CategoryID
> listing_test_labels <- listing[28420:28706, ]$CategoryID

> listing_classifier <- naiveBayes(listing_train, listing_train_labels)
> listing_test_pred <- predict(listing_classifier, listing_test)

> CrossTable(listing_test_pred, listing_test_labels, prop.chisq = FALSE, prop.t= FALSE, dnn = c(’预测值’, ’实际值’))

图1-13展示了混淆矩阵的局部,而完整的混淆矩阵位于:

图1-13 将训练样本放大到99%的标注数据后,朴素贝叶斯分类结果的混淆矩阵之局部

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Cla-ssif ication/R/NaiveBayes.Results2.txt

从图1-13中可以看出,某些类别的召回率和精度有所提升,而某些却下降了,可能是由于模型过拟合所导致的。整体的准确率大约为96.5%。

当然,你也可以查看贝叶斯分类器对每个被测样例的预测值。可以通过修改预测函数predicate()的参数type为“raw”来实现,代码如下:

> listing_test_pred <- predict(listing_classifier, listing_test, type = "raw")
> listing_test_pred[1:3, ]
                1              2              3              4              5              6              7
                8              9             10             11             12             13             14
              15             16             17             18
[1, ] 1.163562e-02 8.686446e-12 2.103992e-12 2.508276e-14 6.986207e-14 9.883640e-01 1.419304e-12
    4.030010e-07 1.039946e-13 6.245893e-11 4.309790e-14 3.826502e-16 1.896761e-09 1.470796e-20
    1.129724e-17 5.292009e-17 8.534417e-19 2.267523e-15
[2, ] 7.581626e-01 1.257191e-08 1.997998e-14 1.978235e-16 6.065871e-17 7.761154e-11 2.107551e-13
    2.418373e-01 4.168406e-14 1.328692e-07 3.947417e-16 3.505745e-17 1.252182e-15 5.676353e-16
    8.126072e-19 4.764065e-17 1.697768e-16 1.249436e-12
[3, ] 1.548534e-09 2.067730e-10 3.250368e-11 4.691759e-08 2.715113e-07 2.418692e-08 1.056756e-07
    2.312425e-10 9.023589e-10 4.037764e-11 3.655755e-09 1.269035e-08 3.647616e-11 3.340507e-11
    7.912837e-11 9.999994e-01 1.206560e-07 1.152458e-10

此刻,查看listing_test_pred的值就会发现,对于每个被测试的样例,分类器都给出了它属于某个分类的概率。

1.6.4 使用R进行K最近邻分类

当然,对于同样的任务可以尝试不同的分类模型。让我们再次尝试一下R扩展包class中的KNN分类。数据的预处理过程是类似的,我们将直接从之前获取的listing_dtm开始:

> convert_2 <- function(x) { x <- x }
> listing_all_knn <- as.data.frame(apply(listing_dtm, MARGIN=2, convert_2))
> listing_train_knn <- listing_all_knn[1:28419, ]
> listing_test_knn <- listing_all_knn[28420:28706, ]

> listing_train_labels <- listing[1:28419, ]$CategoryID
> listing_test_labels <- listing[28420:28706, ]$CategoryID

这里保留了listing_dtm中的词频tf数值,用于KNN计算样例之间的距离,此处和针对朴素贝叶斯分类器的处理有所不同。另外,考虑到KNN在预测阶段的时间复杂度太高,此次测试的样本也控制在全体数据的1%,尽管如此,在单台iMac上运行如下预测仍然可能需要数十分钟:

> listing_test_pred_knn <- knn(train = listing_train_knn, test = listing_test_knn, cl = listing_train_labels, k = 3)
> CrossTable(listing_test_pred_knn, listing_test_labels, prop.chisq = FALSE, prop.t = FALSE, dnn = c(’预测值’, ’实际值’))

图1-14展示了KNN预测结果和标注相比,混淆矩阵的局部内容。完整的混淆矩阵位于:

图1-14 KNN分类结果的混淆矩阵之局部

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/R/KNN.Results.txt

在以上的测试样本上使用KNN分类算法,最终获得的整体准确率大约在91.3%,略逊于朴素贝叶斯。可以看出,相比KNN,朴素贝叶斯分类模型虽然需要学习的过程,而且也更难理解,但是其具有良好的分类效果,以及实时预测的性能。因此,在现实生产环境中,我们可以优先考虑朴素贝叶斯。

尽管我们可以便捷地在R语言中测试不同的分类算法,但是它也有一定的局限性,主要体现在如下几个方面。

❑ 性能:R属于解释性语言,其性能比不上C++、Java这样的编程语言,因此不适合应用于大量的在线服务。

❑ 并行性:R最常见的应用还是侧重于单机环境。其并行处理方案是存在的,例如和Hadoop结合的RHadoop,但是不如Mahout和Hadoop结合得那么紧密。

❑ 集成复杂度:R和其他主流的编程语言,例如Java,也可以集成,但是比较复杂,开发成本较高。

鉴于此,下面来介绍一下Apache Mahout中的分类实现,它不仅可以利用Hadoop来开展并行的训练,而且可以让你打造实时的在线预测系统。

1.6.5 单机环境使用Mahout运行朴素贝叶斯分类

1.实验准备

为了达到更好的效果,我们将由浅入深地进行学习,首先来学习在单机上如何运行Mahout的机器学习算法——朴素贝叶斯。硬件仍然使用2015款iMac一体机1台,软件环境除了之前采用的Java语言(JDK 1.8)和Eclipse IDE环境(Neon.1a Release (4.6.1))之外,当然还需要安装Mahout。这里部署的是版本号为0.9的Mahout,你可以在这里下载并解压:

http://mahout.apache.org/general/downloads.html

然后根据解压的目录,相应地设置环境变量如下:

export MAHOUT_HOME=/Users/huangsean/Coding/mahout-distribution-0.9
export PATH=$PATH:$MAHOUT_HOME/bin
export MAHOUT_LOCAL=1

注意,这里也设置了MAH0UT_L0CAL变量,目的是为了确保当前的Mahout是在单台机器上运行的。

2.通过命令行进行训练和测试

在编写实时性预测的代码之前,你可以先通过Mahout的命令行模式来了解其分类算法的工作流程。为了这项任务,首先准备原始的实验数据。数据的内容依然是R实验中的listing-segmented-shuff led.txt。不过出于Mahout的需求,我们为每件商品单独生成一个商品文件,内容是商品的标题,文件名称是商品的ID,并将同一个分类的商品文件存放在同一个子目录中,子目录的名称是分类的ID,目录和文件的组织如图1-15所示,其中标题为“雀巢脆脆鲨威化巧克力巧克力味夹心20g 24盒”的商品,形成了1.txt(商品ID为1)的文件,并置于名为1(分类ID为1)的目录中。

图1-15 为Mahout准备的数据集

上述完整的数据文件位于:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/Mahout/listing-segmented-shuff led-mahout.zip

有了这些数据,Mahout进行朴素贝叶斯分类的主要步骤具体如下。

1)将原始数据文件转换成Hadoop的序列文件(SequenceFile)。序列文件是Hadoop所使用的文件格式之一,尽管目前使用的是单机模式,但Mahout还是需要读取这种格式。

2)将序列文件中的数据转换为向量。向量是使用词包(Bag of Word)来表示文本的基本方式,这步操作和R语言中DocumentTermMatrix的功能相类似。

3)切分训练样本和待测样本集合。

4)使用朴素贝叶斯算法训练模型。

5)使用朴素贝叶斯算法测试待测的样本。

首先使用mahout命令的seqdirectory选项,将原始数据转换为序列文件:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout seqdirectory -i /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-mahout/  -o  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuff led-seq -ow
MAHOUT_LOCAL is set, so we don't add HADOOP_CONF_DIR to classpath.
MAHOUT_LOCAL is set, running locally
...
17/01/17 21:27:04 INFO driver.MahoutDriver: Program took 6513 ms (Minutes: 0.10855)

其中-i用于指定输入的原始数据文件用于,而-o用于指定输出的序列文件,-ow表示覆盖已有的结果。生成的序列文件如图1-16所示。

图1-16 新生成的序列文件

再使用seq2sparse选项,将该part-m-00000文件作为输入,获取稀疏向量:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  seq2sparse  -i  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-seq/part-m-00000 -o /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segm ented-shuffled-vec  -lnorm  -nv  -wt  tf  -a  org.apache.lucene.analysis.core.Whitespace Analyzer
MAHOUT_LOCAL is set, so we don't add HADOOP_CONF_DIR to classpath.
MAHOUT_LOCAL is set, running locally
...
17/01/17 21:30:19 INFO driver.MahoutDriver: Program took 7904 ms (Minutes: 0.131 73333333333334)

其中,-lnorm表示使用了归一化。-wt tf表示权重值使用了词频。这里采用词频是为了和之前R的实验保持一致,便于比较分类的效果。当然,还可以通过-wt tf idf,使用tf-idf的机制定义每个单词维度的取值,该机制的具体含义将在第4章有关搜索引擎的部分中进行介绍。另外,一定要通过-a org.apache.lucene.analysis.core.WhitespaceAnalyzer来指定分析器(analyzer),如果不指定,那么Mahout将默认按照英文的处理方式,将中文单词都切分为单个汉字,这可能会对最终的分类结果产生负面影响。由于之前已经使用IKAnalyzer将商品的标题进行了中文分词,所以这里指定以空格为分隔符的WhitespaceAnalyzer。此命令执行完毕后,我们将获得如图1-17所示的向量文件:

图1-17 新生成的向量文件

下一步就是将该向量文件切分为训练数据集和待测的数据集:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  split  -i  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-vec/tf-vectors  --trainingOutput  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-train --testOutput /Users/huangsean/Coding/data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-test  --randomSelectionPct  10  --overwrite  --sequenceFiles -xm sequential
MAHOUT_LOCAL is set, so we don't add HADOOP_CONF_DIR to classpath.
MAHOUT_LOCAL is set, running locally
...
17/01/17 21:31:09 INFO driver.MahoutDriver: Program took 881 ms (Minutes: 0.0146 83333333333333)

其中--randomSelectionPct 10表示待测样本的占比为10%,也就是90%的数据用于训练。而-xm sequential表示在单机上执行,而不进行MapReduce操作。下一步就是执行训练过程,同时利用-li参数来生成评测所用的类标索引(labelindex)文件:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout trainnb -i /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-train -el -o /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-model  -li  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-mahout-labelindex -ow
MAHOUT_LOCAL is set, so we don't add HADOOP_CONF_DIR to classpath.
MAHOUT_LOCAL is set, running locally
...
17/01/17 21:32:14 INFO driver.MahoutDriver: Program took 3037 ms (Minutes: 0.050 616666666666664)

生成的模型model目录和类标索引labelindex文件如图1-18所示。其中类标索引相当于考试答案,可供稍后的离线测试使用。

图1-18 生成的模型目录和类标索引

最后,通过训练的模型目录和类标索引,对待测样本进行测试和评估:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  testnb  -i  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-test -m /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-model  -l  /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-mahout-labelindex  -ow  -o  /Users/huangsean/Coding/data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-mahout-results
MAHOUT_LOCAL is set, so we don't add HADOOP_CONF_DIR to classpath.
MAHOUT_LOCAL is set, running locally
...
=======================================================
Statistics
-------------------------------------------------------
Kappa                                                  0.9613
Accuracy                                             97.8484%
Reliability                                          90.129%
Reliability (standard deviation)               0.2563
17/01/17 21:34:21 INFO driver.MahoutDriver: Program took 1460 ms (Minutes: 0.024 333333333333332)

执行完毕后Mahout直接输出了评估结果。你将看到类似图1-11和图1-13的混淆矩阵,如图1-19所示。如果需要查阅完整的矩阵内容,可以访问:

图1-19 Mahout测试后给出的混淆矩阵

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/blob/master/Classif ication/Mahout/listing-segmented-shuff led-mahout-results.txt

此外你还可以看到准确率(Accuracy)在97.8%,和R的朴素贝叶斯分类实践中的数据96%非常接近。两者的相差可能是由于训练和测试数据集的切分不一致,或者是细微的分类器实现差异所导致的。从分类的结果来看,整体效果非常理想。

3.打造实时预测

当然,如果需要将Mahout运用到线上服务,那么上述离线式的处理和测试还远远不够。我们可以利用训练阶段所生成的模型文件,创建一个实时性的分类预测模块。这个过程按顺序可以分为以下几个主要步骤。

1)预加载必要的数据,包括Mahout训练命令所产生的朴素贝叶斯模型、类标索引文件和分类ID/名称间的映射。

2)对实时输入的文本进行中文分词。

3)将分词结果转变为单词向量。

4)根据训练的模型和单词向量,给出分类的预测。

下面是一段用于演示核心流程的示例代码:

public static void main(String[] args) throws Exception {

    // 指定Mahout朴素贝叶斯分类模型的目录、类标文件和字典文件
    String modelPath = "/Users/huangsean/Coding/data/BigDataArchitectureAnd Algorithm/listing-segmented-shuffled-model/";
    String labelIndexPath = "/Users/huangsean/Coding/data/BigDataArchitectureAnd Algorithm/listing-segmented-shuffled-mahout-labelindex";
    String dictionaryPath = "/Users/huangsean/Coding/data/BigDataArchitectureAnd Algorithm/listing-segmented-shuffled-vec/dictionary.file-0";

    Configuration configuration = new Configuration();

    // 加载Mahout朴素贝叶斯分类模型,以及相关的类标、字典和分类名称映射
    NaiveBayesModel model = NaiveBayesModel.materialize(new Path(modelPath), configuration);
    StandardNaiveBayesClassifier classifier = new StandardNaiveBayesClassifier (model);
    Map<Integer, String> labels = BayesUtils.readLabelIndex(configuration, new Path (labelIndexPath));
    Map<String, Integer> dictionary = readDictionnary(configuration, new Path (dictionaryPath));
    Map<String, String> categoryMapping = loadCategoryMapping();

    // 使用IKAnalyzer进行中文分词
    Analyzer ikanalyzer = new IKAnalyzer();
    TokenStream ts = null;

    while (true) {

            BufferedReader strin=new BufferedReader(new InputStreamReader(System.in));
        System.out.print("请输入待测的文本:");

        String content = strin.readLine();

        if ("exit".equalsIgnoreCase(content)) break;

            // 进行中文分词,同时构造单词列表
            Map<String, Integer> terms = new Hashtable<String, Integer>();
            ts = ikanalyzer.tokenStream("myfield", content);
            CharTermAttribute term = ts.addAttribute(CharTermAttribute.class);
                ts.reset();
                while (ts.incrementToken()) {
                    if (term.length() > 0) {
                              String strTerm = term.toString();
                              Integer termId = dictionary.get(strTerm);
                              if (termId ! = null) {

                                  if (! terms.containsKey(strTerm)) {
                                      terms.put(strTerm, 0);
                                  }
                                  terms.put(strTerm, terms.get(strTerm) + 1);
                                  termsCnt ++;
                              }
                    }
                }
                ts.end();
                ts.close();

                //使用词频tf(term frequency)构造向量
                RandomAccessSparseVector rasvector = new RandomAccessSparseVector
                (100000);
        for (Map.Entry<String, Integer> entry : terms.entrySet()) {
            String strTerm = entry.getKey();
            int tf = entry.getValue();
            Integer termId = dictionary.get(strTerm);
            rasvector.setQuick(termId, tf);
        }

        // 根据构造好的向量和之前训练的模型,进行分类
        org.apache.mahout.math.Vector predictionVector = classifier.classifyFull
        (rasvector);
        double bestScore = -Double.MAX_VALUE;
        int bestCategoryId = -1;
        for(Element element : predictionVector.all()) {
            int categoryId = element.index();
            double score = element.get();
            if (score > bestScore) {
                bestScore = score;
                bestCategoryId = categoryId;
            }
        }
        System.out.println();
        String category = categoryMapping.get(labels.get(bestCategoryId));
        if (category == null) category = "未知";
        System.out.println(String.format("预测的分类为:%s", category));
        System.out.println();

    }

    ikanalyzer.close();
}

代码的最后一步,Mahout将计算输入文本属于不同分类的概率。其中需要注意的是,Mahout对概率值进行了一定的数值转换,也就是将它们转变为了一个负数,数值越大,表示概率越高。所以,这段代码找出了拥有最大值的分类。如果是多分类问题,可以取最大的n个数值及其对应的分类。

其他辅助的数据预加载函数如下:

public  static  Map<String,  Integer>  readDictionnary(Configuration  conf,  Path dictionnaryPath) {
        Map<String, Integer> dictionnary = new HashMap<String, Integer>();
        for (Pair<Text, IntWritable> pair : new SequenceFileIterable<Text, IntWritable>(dictionnaryPath, true, conf)) {
            dictionnary.put(pair.getFirst().toString(), pair.getSecond().get());
        }
        return dictionnary;
    }

public static Map<Integer, Long> readDocumentFrequency(Configuration conf, Path documentFrequencyPath) {
        Map<Integer, Long> documentFrequency = new HashMap<Integer, Long>();
        for (Pair<IntWritable, LongWritable> pair : new SequenceFileIterable<Int Writable, LongWritable>(documentFrequencyPath, true, conf)) {
            documentFrequency.put(pair.getFirst().get(), pair.getSecond().get());
        }
        return documentFrequency;
    }

public static Map<String, String> loadCategoryMapping() {

        Map<String, String> categoryMapping = new HashMap<String, String>();
        categoryMapping.put("1", "饼干");
        categoryMapping.put("2", "方便面");
        categoryMapping.put("3", "海鲜水产");
        categoryMapping.put("4", "进口牛奶");
        categoryMapping.put("5", "纯牛奶");
        categoryMapping.put("6", "巧克力");
        categoryMapping.put("7", "饮料饮品");
        categoryMapping.put("8", "坚果");
        categoryMapping.put("9", "食用油");
        categoryMapping.put("10", "枣类");
        categoryMapping.put("11", "新鲜水果");
        categoryMapping.put("12", "大米");
        categoryMapping.put("13", "面粉");
        categoryMapping.put("14", "手机");
        categoryMapping.put("15", "电脑");
        categoryMapping.put("16", "美发护发");
        categoryMapping.put("17", "沐浴露");
        categoryMapping.put("18", "茶叶");

        return categoryMapping;

    }

完整的代码和Maven项目文件,可以访问:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/tree/master/Cla-ssif ication/Mahout/MahoutMachineLearning

编译时,需要在pom.xml中加入Mahout和IKAnalyzer的依赖包:

<! -- https://mvnrepository.com/artifact/org.apache.mahout/mahout-core -->
<dependency>
    <groupId>org.apache.mahout</groupId>
    <artifactId>mahout-core</artifactId>
    <version>0.9</version>
</dependency>

<! -- https://mvnrepository.com/artifact/com.janeluo/ikanalyzer -->
<dependency>
    <groupId>com.janeluo</groupId>
    <artifactId>ikanalyzer</artifactId>
    <version>2012_u6</version>
</dependency>

编译成功并运行main函数,你就可以在Console窗口中输入一段文本,程序将实时给出分类的预测,如图1-20所示。最后输入“exit”并退出。

图1-20 实时预测的演示

有了这些代码的基础,就可以为线上应用(例如,根据商家的输入为其实时推荐商品分类)构建预测模块、RESTFUL风格的API等。

1.6.6 多机环境使用Mahout运行朴素贝叶斯分类

Mahout最早是基于Hadoop开发的,当然也支持多机并行处理。需要注意的是,Hadoop的MapReduce计算模式只适用于批量的训练和评测,并不适用于实时的预测。关于离线批量处理和实时处理的更多探讨,可参见《大数据架构商业之路》一书的第4章。

1.Hadoop集群的安装和设置

在Mahout使用Hadoop之前,需要一步步地搭建Hadoop集群。用于本案例的硬件环境为三台苹果个人电脑,除了之前的iMac2015,还有两台MacBook Pro,代号分别为Mac-BookPro2012和MacBookPro2013,其大体配置分别如图1-21和图1-22所示。局域网也是需要的,三台机器分配的IP分别如下。

图1-21 MacBookPro2012的配置

图1-22 MacBookPro2013的配置

iMac2015 192.168.1.48

MacBookPro2013 192.168.1.28

MacBookPro2012 192.168.1.78

至于软件,由于所有的操作系统都是Mac OS,因此下面示例中的命令和路径都以Mac OS为准,请根据自己的需要进行适当调整。

首先在三台机器之间构建SSH的互信连接,在iMac2015上生成本台机器的公钥:

[huangsean@iMac2015:/Users/huangsean/Coding]ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter fi le in which to save the key (/Users/huangsean/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/huangsean/.ssh/id_rsa.
Your public key has been saved in /Users/huangsean/.ssh/id_rsa.pub.
[huangsean@iMac2015:/Users/huangsean/Coding]more /Users/huangsean/.ssh/id_rsa.pub
...

然后将公钥发布到另外两台机器MacBookPro2012和MacBookPro2013上,此时还需要手动登录:

[huangsean@iMac2015:/Users/huangsean/Coding]scp  ~/.ssh/id_rsa.pub  huangsean@
MacBookPro2012:~/master_key
[huangsean@iMac2015:/Users/huangsean/Coding]scp  ~/.ssh/id_rsa.pub  huangsean@
MacBookPro2013:~/master_key

如果MacBookPro2012和MacBookPro2013上还没有~/.ssh目录,那么先创建该目录并设置合适的权限。而后,将iMac2015的公钥移动过去并命名为“authorized_keys”,下面以MacBookPro2012为例:

[huangsean@MacBookPro2012:/Users/huangsean/Coding]mkdir ~/.ssh
[huangsean@ MacBookPro2012:/Users/huangsean/Coding]chmod 700 ~/.ssh
[huangsean@  MacBookPro2012:/Users/huangsean/Coding]mv  ~/master_key  ~/.ssh/
authorized_keys
[huangsean@ MacBookPro2012:/Users/huangsean/Coding]chmod 600 ~/.ssh/authorized_keys

如果MacBookPro2012和MacBookPro2013已有~/.ssh目录,那么将iMac2015的公钥移动过去并命名为“authorized_keys”。如果之前“authorized_keys”文件已经存在,那么将iMac2015的公钥移动附加在其后面。同样以MacBookPro2012为例:

[huangsean@ MacBookPro2012:/Users/huangsean/Coding]cat ~/master_key >> ~/.ssh/authorized_keys

这样,iMac2015就可以免密码SSH登录MacBookPro2012和MacBookPro2013了。然后如法炮制,让三台机器可以相互免密码登录。

下面,让我们进入Hadoop分布式环境搭建的正题。通过如下链接下载并解压Hadoop发行版,本文使用的版本是2.7.3:

http://hadoop.apache.org/releases.html

解压后,部署分布式Hadoop 2.x版的主要步骤具体如下。

1)为Hadoop设置正确的环境变量。

2)编辑一些重要的配置文件包括core-site.xml、hdfs-site.xml等。

3)一个容易被遗忘但是很关键的步骤:格式化名称节点。

4)运行start-dfs.sh来启动HDFS。

5)运行start-yarn.sh来启动MapReduce的作业调度。

下面分别来看看这些步骤。

首先,设置环境变量:

export HADOOP_HOME=/Users/huangsean/Coding/hadoop-2.7.3
export PATH=$PATH:$HADOOP_HOME/bin
export PATH=$PATH:$HADOOP_HOME/sbin
export HADOOP_MAPRED_HOME=$HADOOP_HOME
export HADOOP_COMMON_HOME=$HADOOP_HOME
export HADOOP_HDFS_HOME=$HADOOP_HOME
export HADOOP_YARN_HOME=$HADOOP_HOME
export YARN_HOME=$HADOOP_HOME
export HADOOP_COMMON_LIB_NATIVE_DIR=$HADOOP_HOME/lib/native
export HADOOP_OPTS="-Djava.library.path=$HADOOP_HOME/lib"

进入Hadoop主目录中的/etc/hadoop/,分别修改core-site.xml、hdfs-site.xml、mapred-site.xml、yarn-site.xml、slaves和hadoop-env.sh。

配置文件core-site.xml的示例如下:

<configuration>

    <property>
        <name>hadoop.tmp.dir</name>
        <value>file:/Users/huangsean/Coding/hadoop-2.7.3/tmp</value>
        <description>Abase for other temporary directories.</description>
    </property>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://iMac2015:9000</value>
    </property>

</configuration>

其中,hadoop.tmp.dir指定了HDFS数据存放的目录。而fs.defaultFS指定了命名节点(Name Node)的IP或名称(iMac2015),以及端口(9000),这点是非常重要的,稍后依赖HDFS的其他系统也都需要使用这个设置。

配置文件hdfs-site.xml的示例如下:

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>2</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>file:/Users/huangsean/Coding/hadoop-2.7.3/tmp/dfs/name</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>file:/Users/huangsean/Coding/hadoop-2.7.3/tmp/dfs/data</value>
    </property>
    <property>
        <name>dfs.blocksize</name>
        <value>124800000</value>
    </property>
</configuration>

这里将副本数量replication设置为2,并设置命名节点(NameNode)和数据节点(DataNode)的目录来保存文件。另一个关键参数是块大小(blocksize)。由于HDFS擅长批处理,所以通常它需要较大的文件块。太多的小文件将影响Hadoop的性能。

配置文件mapred-site.xml的示例如下:

<configuration>

    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>

    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>iMac2015:10020</value>
    </property>

    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>iMac2015:19888</value>
    </property>

</configuration>

从中你可以看到有关任务的设置,主要是用于跟踪MapReduce的计算任务。

配置文件yarn-site.xml的示例如下:

<configuration>

<! -- Site specific YARN configuration properties -->
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>

    <property>
        <name>yarn.resourcemanager.scheduler.address</name>
        <value>iMac2015:8030</value>
    </property>

    <property>
        <name>yarn.resourcemanager.resource-tracker.address</name>
        <value>iMac2015:8031</value>
    </property>

    <property>
        <name>yarn.resourcemanager.address</name>
        <value>iMac2015:8032</value>
    </property>

    <property>
        <name>yarn.resourcemanager.admin.address</name>
        <value>iMac2015:8033</value>
    </property>

    <property>
        <name>yarn.resourcemanager.webapp.address</name>
        <value>iMac2015:8088</value>
    </property>

    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
        </property>

    <property>
        <name>yarn.nodemanager.aux-service.mapreduce.shuffle.class</name>
        <value>org.apache.hadoop.mapred.ShuffleHandler</value>
    </property>

</configuration>

其中端口的默认配置通常都是可以正常工作的,请注意将主机IP地址或名称进行合理的替换。

配置文件slaves的示例如下:

iMac2015
MacBookPro2013
MacBookPro2012

由于硬件资源非常有限,因此这里使用了全部的三台机器作为slave。最后在hadoop-env.sh文件中,记得设置Java JDK的路径:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_112.jdk/Contents/Home

对于上述所有的配置文件,你可以通过下面的链接获取更多的参考:

https://github.com/shuang790228/BigDataArchitectureAndAlgorithm/tree/master/UserBehaviorTracking/SelfDesign/hadoop/conf

在三台机器上完成上述配置后,在主节点iMac2015上通过如下命令格式化HDFS:

[huangsean@iMac2015:/Users/huangsean/Coding]hdfs namenode -format

然后在主节点上通过Hadoop sbin目录中的如下命令,分别启动Hadoop集群的HDFS和YARN管理:

[huangsean@iMac2015:/Users/huangsean/Coding]start-dfs.sh
[huangsean@iMac2015:/Users/huangsean/Coding]start-yarn.sh

如果要关闭集群,那就使用Hadoop sbin目录中的如下命令:

[huangsean@iMac2015:/Users/huangsean/Coding]stop-all.sh

集群成功启动之后,我们可以通过如下的链接来查看HDFS的整体状况:

http://imac2015:50070/dfshealth.html

图1-23展示了HDFS的概括,单击页面上的“Live Nodes”链接,可以进一步看到类似于图1-24的数据节点信息。

图1-23 HDFS启动后的系统概览,包括多少存活节点

图1-24 三台机器都成为数据节点,其磁盘使用情况也被显示了出来

在图1-23的界面上点击“Utilities”选项卡的“Browse the f ile system”选项,你还可以看到目前刚刚启动的HDFS中尚无数据,如图1-25所示。

图1-25 HDFS中尚无数据

而通过如下链接,你可以查看MapReduce的任务执行情况:

http://imac2015:8088/cluster

图1-26展示了目前集群中任务分配和执行的情况。

图1-26 尚无任务分配和执行

2.Hadoop所支持的训练过程

Hadoop集群部署完毕之后,我们就可以将Mahout所要使用的数据导入HDFS了。首先是创建相应的目录:

[huangsean@iMac2015:/Users/huangsean/Coding]hadoop fs -mkdir -p /data/BigData ArchitectureAndAlgorithm

如图1-27所示,新目录在HDFS中创建成功。

图1-27 创建新目录,用于存放分类实验的数据

然后将实验数据复制到HDFS的新目录中:

[huangsean@iMac2015:/Users/huangsean/Coding]hadoop fs -put -p /Users/huangsean/Coding/data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-mahout /data/BigDataArchitectureAndAlgorithm/

其过程较慢,主要原因是一共有超过28000个商品文件,而且每个文件的内容都只是商品的标题。这可能证明了HDFS不太善于处理大量小文件。最终的结果类似于图1-28的截屏。

图1-28 导入后的数据文件

Mahout的部署和设置也要做稍许修改。由于Hadoop是2.7.3的版本,所以需要将Mahout的版本切换到和该Hadoop兼容的0.12.2,并设置相应的环境变量。需要注意的是,为了确保Mahout是在Hadoop上进行并行处理的,此处不能再设置MAH0UT_L0CAL的变量:

export MAHOUT_HOME=/Users/huangsean/Coding/apache-mahout-distribution-0.12.2
export PATH=$PATH:$MAHOUT_HOME/bin

之后的步骤和单机版的相似,不过要使用HDFS中的路径。首先是获取序列文件:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout seqdirectory -i /data/BigData ArchitectureAndAlgorithm/listing-segmented-shuffled-mahout/ -o /data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-seq -ow
Running on hadoop, using /Users/huangsean/Coding/hadoop-2.7.3/bin/hadoop and HAD OOP_CONF_DIR=
MAHOUT-JOB:  /Users/huangsean/Coding/apache-mahout-distribution-0.12.2/mahout-examples-0.12.2-job.jar
...
17/01/18  23:11:24  INFO  MahoutDriver:  Program  took  177910  ms  (Minutes:  2.965 1666666666667)

如图1-29所示,序列文件的结果被存储于HDFS之中。而从图1-30中以看出,Mahout刚刚在Hadoop上启动了MapReduce的任务。

图1-29 HDFS中的序列文件

图1-30 Hadoop目前执行的MapReduce任务

将序列文件转为向量文件:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  seq2sparse  -i  /data/BigData ArchitectureAndAlgorithm/listing-segmented-shuffled-seq/part-m-00000  -o  /data/Big DataArchitectureAndAlgorithm/listing-segmented-shuffled-vec  -lnorm  -nv  -wt  tf  -a org.apache.lucene.analysis.core.WhitespaceAnalyzer
Running  on  hadoop,  using  /Users/huangsean/Coding/hadoop-2.7.3/bin/hadoop  and HADOOP_CONF_DIR=
MAHOUT-JOB:  /Users/huangsean/Coding/apache-mahout-distribution-0.12.2/mahout-examples-0.12.2-job.jar
...
17/01/18 23:38:19 INFO MahoutDriver: Program took 1181880 ms (Minutes: 19.698)

切分训练样本和测试样本集合,训练集仍占全体数据的90%,剩下的10%作为待测集:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout split -i /data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-vec/tf-vectors  --trainingOutput  /data/Big DataArchitectureAndAlgorithm/listing-segmented-shuffled-train --testOutput /data/Big DataArchitectureAndAlgorithm/listing-segmented-shuffled-test  --randomSelectionPct 10 --overwrite --sequenceFiles -xm sequential
Running  on  hadoop,  using  /Users/huangsean/Coding/hadoop-2.7.3/bin/hadoop  and HADOOP_CONF_DIR=
MAHOUT-JOB:  /Users/huangsean/Coding/apache-mahout-distribution-0.12.2/mahout-examples-0.12.2-job.jar
...
17/01/18 23:43:19 INFO MahoutDriver: Program took 1259 ms (Minutes: 0.0209833333 33333333)

并行地训练朴素贝叶斯模型:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  trainnb  -i  /data/BigData ArchitectureAndAlgorithm/listing-segmented-shuffled-train -o /data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-model  -li  /data/BigDataArchitectureAnd Algorithm/listing-segmented-shuffled-mahout-labelindex -ow
Running  on  hadoop,  using  /Users/huangsean/Coding/hadoop-2.7.3/bin/hadoop  and HADOOP_CONF_DIR=
MAHOUT-JOB:  /Users/huangsean/Coding/apache-mahout-distribution-0.12.2/mahout-examples-0.12.2-job.jar
...
17/01/18 23:55:31 INFO MahoutDriver: Program took 371944 ms (Minutes: 6.19906666 6666667)

测试训练后的贝叶斯模型:

[huangsean@iMac2015:/Users/huangsean/Coding]mahout  testnb  -i  /data/BigData ArchitectureAndAlgorithm/listing-segmented-shuffled-test -m /data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-model -l /data/BigDataArchitectureAndAlgorithm/listing-segmented-shuffled-mahout-labelindex  -ow  -o  /data/BigDataArchitecture AndAlgorithm/listing-segmented-shuffled-mahout-results
Running  on  hadoop,  using  /Users/huangsean/Coding/hadoop-2.7.3/bin/hadoop  and HADOOP_CONF_DIR=
MAHOUT-JOB:  /Users/huangsean/Coding/apache-mahout-distribution-0.12.2/mahout-examples-0.12.2-job.jar
...
=======================================================
Statistics
-------------------------------------------------------
Kappa                                                  0.9581
Accuracy                                             97.6308%
Reliability                                         89.9985%
Reliability (standard deviation)               0.2536
Weighted precision                                 0.9779
Weighted recall                                     0.9763
Weighted F1 score                                   0.9757

17/01/18 23:59:14 INFO MahoutDriver: Program took 153815 ms (Minutes: 2.56358333 33333333)

如图1-31所示,我们可以在HDFS上找到整个过程所产生的各种数据。不过,你可能发现并非分布式计算就一定好。相对于之前的单机实验,多机的耗时明显更长了。其原因可能包括如下两点。

图1-31 任务结束后,可以在HDFS中查看各种数据

第一,测试数据规模太小,远远没有到达单机性能的瓶颈,分布式的协同和网络通信反而占用了更多的开销。

第二,小文件过多,这并非MapReduce计算模式的长处。

所以,在实际生产中,我们需要结合实际情况,进行具体分析,再定下最合适的技术方案。

1.7 更多的思考

上面几节中讲述了分类这种机器学习方法的基本知识,及其系统实现。不过在实际运用中,我们还需要 以下几点。

❑ 合理对待分类的准确性。我们可以看到,无论是何种算法的预测,通常都不可能拥有100%的准确性。在实际运用中,可以结合具体情况灵活运用。例如,本章提到的业务需求:如何将商品放入适合的分类中。我们可以将其看作是一个多分类问题,根据分类的预测值,提供超过1个的分类候选。像之前提到的进口牛奶和非进口纯牛奶的例子,虽然机器无法完全准确地判定其属于两者中的哪一种,但如果同时提供了两者给商家选择,那么用户体验还是相当不错的。

❑ 标注数据的质量。现实中,由于数据里难免存在一些干扰因素,通常需要假设现有的商品分类信息(也就是标注的训练样本)绝大部分都是正确的。有了这个假设,设计者就可以忽略基于内容特征来分类的结果误差。如果观察发现数据质量达不到要求,无法满足这个假设,那么就不能使用这种自动分类的学习模型。

❑ 训练样本的数量。一般商品的类目都是分为多个层级的,如图1-32所示,一级类目是“食品、饮料、酒水”,二级类目是“糖果巧克力”,三级类目是“润喉糖”。越是细分的类目,其所包含的商品数量可能就会越少。这也许会导致训练样本不够,分类精度很差的情况。这种情形下,不建议对于过小的分类进行训练和预测。可以考虑对上级类目进行处理。

图1-32 多级类目结构示意

❑ 其他可用的表示法和特征。如果只采用文本本身的词包(Bag of Word)表示,有的时候无法获取非常高的精准度。举个例子,在1.6.5节所创建的实时性预测程序中,你输入“牛奶巧克力”和“巧克力牛奶”,系统给出的分类都是“巧克力”,如图1-33所示,这明显是不准确的。也许你会考虑到更复杂的表示方法,例如n元文法(n-gram)或词组,来描述单词出现的先后顺序。除此以外,不仅仅是商品本身的标题和详细描述,还有一些其他的数据可以帮助机器进行分类,例如用户进行关键词搜索的时候,其行为也提供了非常有价值的信息。我们将在第6章讲解有关搜索相关性优化的部分继续探讨这个有趣的话题。

图1-33 基于词包的分类无法精准地判断语义