前言:数据挖掘和机器学习包含了许多的算法,算法的介绍往往是枯燥乏味的。本文中结合mahout和小例子还解释这些算法。因此我们先介绍一下mahout。
Hadoop是为了大数据而生的,在之前的学习中,我们也了解了Mapreduce程序的基本原理。但是,读者对如何将Hadoop应用到大数据还是没有一个清晰地认识。相信读者朋友们了解过数据挖掘的算法,这些算法如何与hadoop做一个无缝的结合,如何提高他们的复用性呢?像R语言和MatlAB以及其他的程序包实现了对数据挖掘算法的封装,用户不需要了解算法的具体实现,只需要将数据导入,调用算法,就能很快的得到结果,大大减轻了数据挖掘人员的负担。那hadoop有没有这样的包呢,封装常见的数据挖掘算法,提供给用户使用的API,让数据分析人员从繁重的代码工作中解脱出来。出于这样的目标,hadoop社区中的大神们就创建了mahout项目,该项目旨在提供数据挖掘的算法包,目前以及包含kmeans SVM等一系列的算法。
大家将自己想想成mahout的设计者,我们希望mahout有什么功能呢?
正确率高,当然这是最基本的要求。再好的算法封装,如果算法的效率不高,也不会被使用。
读取文件的类型足够多,能覆盖常见的数据文件类型。现在mahout能支持的数据类型包括,简单文本、attr和CVS文件。这些文件在数据挖掘中非常常见。
函数的使用简单。提供的接口便于理解和记忆。
完善的开发文档。文档的好用程度直接影响到开发者的使用感。
对硬件的要求不高。目前mahout的软件环境是JDK,maven。硬件环境,搭建hadoop的环境即可。
调用语言的接口不止一种。Hadoop的系列项目都是基于java开发的,对于不熟悉Java的开发人员也希望能通过C++、python等语言调用mahout的API。
能根据自己的需求修改算法达到算法的最优。
能提交源码到社区。作为开发人员最令人振奋的莫过于分享自己的代码,mahout是开源的,程序员在社区中碰撞出思想的火花。
由于mahout是以应用为导向的,因此上面的建设目标有一部分就是mahout的特点。Mahout是开源的提供数据挖掘算法的算法包,它是不断发展着的。它的繁荣需要广大的开发人员贡献自己的力量。
我们吹嘘了这么多mahout的好处,你肯定迫不及待的想要mahout中大显身手了。现在我们就再自己的机子上安装mahout来试一试吧。
下载二进制包解压安装。
现在mahout的官网提供了二进制包和源码包。这里我们选择二进制包直接解压。当然你可以选择mahout的源码包自己编译。由于mahout项目是一个不断发展者的项目,下载最新的源码同样会对您有很大的帮助。在本书编写时,最新的版本是0.9.
Tar -zxvf mahout-distribution-0.9.tar.gz
为了以后配置环境变量的方便,我们将解压以后的文件夹重命名为mahout。当然你也可以不这样修改。
2,配置环境变量
打开/etc/profile文件,输入下面三行内容。
Export MAHOUT_HOME=/home/hadoop/hadoop/mahout
Export CLASSPATH=:$CLASSPATH:$MAHOUT_HOME/lib
Export PATH=.:$PATH:$MAHOUT_HOME/bin
保存文件,并使其生效。
Source /etc/profile
3,检验是否安装成功
其实到这里mahout的安装已经完毕。我们想要检验一下是否安装成功,需要在hadoop环境下运行一个示例。
首先,启动hadoop集群。
Bin/start-all.sh
使用jps查看hadoop集群是否启动成功。
然后,输入mahout --help看是否列出了mahout的算法。如何显示算法则表明mahout安装成功。
在mahout包中有很多例子,其中一个是使用kmaens来对synthetic_control.data
这个数据集进行分类。
(1)数据准备
下载测试数据。这个数据是由600行X60列double型的数据组成, 意思是有600个元组,每个元组是一个时间序列。 http://archive.ics.uci.edu/ml/databases/synthetic_control/synthetic_control.data
将它放到本地文件夹中,这里我放在了mahout_home下。
(2)启动hadoop集群并将文件上传。
如何启动集群就不在赘述。将文件上传到/user/hadoop/testdata目录下。主要必须是这个目录,因此在mahout中这个目录已经写死了。首先创建testdata。
Hadoop fs -mkdir testdata
Hadoop fs -put /home/hadoop/hadoop/mahout/synthetic_control.data testdata
Hadoop fs -lsr 查看文件是否上传成功。
(3)运行程序
输入下面命令:Bin/hadoop jar /home/hadoop/hadoop/mahout/mahout-examples-0.9-job.jar org.apache.mahout.clustering.syntheticcontrol.kmeans.Job
将会运行kmeans程序,这个过程可能需要几分钟。在运行过程中留心观察会发现,这是个mapreduce程序。
(4)查看结果
运行完毕后,查看输出文件
Hadoop fs -lsr output
这时会出现很多文件,这些文件就是分类结果集。也就说明测试程序成功。这些文件的含义是聚类的结果。
在上一个章节中,我们安装了mahout并运行了一个mahout的k-means例子。在实际应用中,我们常常需要改动mahout的源码来适应自己的需求,因此构建mahout项目是必要的。对于现在的数据挖掘人员来说,不仅仅要了解算法,还要能将算法转化成程序,因此一些常用的开发工具也是需要掌握的,例如maven和ant,make等。玩转hadoop,要会配置hadoop集群、写java程序、使用linux,这对大部分程序员来说还是极富挑战性的。不过毕竟其乐无穷嘛,我们就笑着面对喽。
1、maven简介
Maven是apache的一个子项目,网站是http://maven.apache.org/。在基于java的项目开发中往往需要引入各种各样的jar包,而且这些包有不同的版本号,不同的包和版本可能造成包的冲突。在实际开发中,jar包得引入和冲突问题是困扰程序员的一大难题。Maven是这么一种工具,希望程序员在构建java项目中更简单,对版本的控制也更容易。
现在的apache maven是源于jakarta子项目的java项目管理及自动构建工具。使用maven能快速得对项目编译、打包、发布、测试等。在maven中一个常用的功能是管理版本的一致性。
Maven的代码包在windows和linux中是一样的。
2、Maven在windows中的安装
安装maven前,首先确认已经安装了JDK。注意JDK和maven的版本要匹配。具体参见官方文档。
下载maven:网址是:http://maven.apache.org/download.cgi,选择文件apache-maven-3.2.1-bin.zip。当然你也可以选择源码自己编译。
解压到本地:笔者这里解压到D:\tool\maven
添加maven的环境变量:
M2_HOME=D:\tool\maven
PATH:%M2_HOME%\bin
打开命令行模式检验是否安装成功:打开cmd,输入mvn -version 查看maven的版本号。出现如下信息,表明maven安装成功。
在eclipse中配置maven的插件
在eclipse的window->preferences->maven 设置maven的settings.xml文件的位置。设置完成后就能创建maven项目了。
3、Maven在linux中的安装
下载文件:apache-maven-3.2.1.tar.gz
解压到本地:这里,我将maven解压到/usr/local/apache-maven/apache-maven-3.2.1
配置环境变量:在/etc/profile中配置M2_HOME,PATH
M2_HOME=/usr/local/apache-maven/apache-maven-3.2.1
PATH=$M2_HOME/bin:$PATH
打开命令行模式是否安装成功:打开终端,输入mvn -version,如果显示maven的版本信息表明安装成功。
在linux的eclipse中设置maven插件:和在windows中配置maven插件是一样的。
1、下载mahout源码,解压到网站是mahout.apache.org
2、导入到eclipse中,选择import->existing maven project,选择解压的目录。
3、导入后显示如下的画面
4、可以安装普通maven项目对其编译、测试、打包、发布。
注意:这一步经常出现的问题。导入的项目pom.xml文件报错。右击mahout项目选择maven->update project ,将强制update 的选项打钩,错误就将消失。
报这个错的时候:Resource Path Location Type Plugin execution not covered by lifecycle configuration: org,右击错误选择,选择quick fix。在新打开的对话框中选择Permanently mark goal generate in pom.xml as ignored in Eclipse build点击finish。再重新update一下项目即可。
1、windows平台下构建maven项目
1.1用maven创建一个标准化的java项目
1.2导入项目到eclipse中
1.3增加mahout依赖,修改pom.xml文件
1.4下载依赖
2、Linux平台下构建maven项目
但凡是学过计算机和数学的人对神经网络这个名词就不会陌生。但是大部分人在相当长的时间内对神经网络的印象都停留在貌似和神经元有关的阶段。人的大脑如何发达,有上亿个神经元细胞,每时每刻都进行这大量的信息交换,但是就是这样我们还是不能理解按照我们大脑神经网络提出的人工神经网络算法。对于笔者也是,在过去的几年里,神经网络在我的脑海里就是一盆浆糊,我从没有过醍醐灌顶豁然开朗的感觉。“哎啊,原来是这样的!”的声音是我不止一次的期盼。
神经网络是一个典型的并行化的系统,它仿造人脑系统中神经元的工作系统来实现计算机中分类的计算。所以,神经网络最早是由心理学家提出来的。最早的有关神经网络的模型是有美国心理学家McCulloch和数理逻辑专家Pitts联合提出的M-P模型。看来交差学科才能创造出伟大的智慧啊。1949年,心理学家Hebb提出通过改变神经元连接强度达到学习的目的。随着,1957年感知器概念的提出迎来了神经网络的新高潮。但是,到了1969年由于当时的人工智能专家对神经网络抱有悲观态度,神经网络一度遭到冷落。后来由于大数据的发展,并行化的必要性,很多学者有想到了神经网络,并不断提出新的理论来改进它。比如美国波士顿大学的Grossberg和Carpenter提出了自适应的网络等等。
目前神经网络的应用领域非常广泛,在模式识别、信号处理、智能控制、数据挖掘等领域前途无量。
广义的神经网络是一个大的系统,它包含了原来的简单神经网络,还包括像模拟退火、蚁群算法这样的改进神经网络。神经网络按照网络拓扑结构可分为前向神经网络和反馈神经网络;按照网络性能可分为连续性神经网络和离散型神经网络、确定性神经网络和随机性神经网络。本书中着重介绍的是前向型神经网络中的BPNN。
不同的神经网络模型有不同的学习方式。分别是死记性学习、有监督的学习、无监督学习以及有监督和无监督的混合学习。BPNN采用的是有监督的学习,也就是说存在“教师”对网络的实际输出和“教师”指定的输出进行对比,得到一定范围的误差。然后系统通过误差函数进行权值的调整,使得误差函数达到最小值。
神经网络是最难理解的算法之一。很多人都只是听说,即是在自己的项目中用到了神经网络也不知道它的原理是什么。笔者在学习的过程中,也吃了不少苦头,原因是找不到问题的关键在哪里。
我们注意一下神经网络中的几个关键词。第一、有监督的学习。第二、神经元。第三、输入层、输出层、隐层。第四、反馈。
先说什么叫输入层、输出层和隐藏层。(神经网络有三层,输入层不是神经元,神经元只有两层)举个例子来说,我们小时候会玩的一种有奖游戏,有一个黑匣子,我们从上面投玻璃球(这可以当成是神经网络的输入层),我们不关心玻璃球在盒子里是怎么运动的(也就是神经网络的隐藏层),最后这个玻璃球会落入到相应的格子里(也就是神经网络的输出层)。也就是说,神经网络屏蔽了内部的计算,只是告诉我们结果。可能你没有玩过这个游戏,再举一个例子。
现在有这么一个感知器,它的功能是判断一个水果是苹果、香蕉还是橘子。我们输入是这个水果的颜色、味道、长度、形状特征。那我们输入了一个水果的特征,这个感知机告诉我们结果,至于这个计算过程是怎样的,我们没有兴趣知道,它是一系列的与特性向量有关的数学计算。
图一、屏蔽隐层的神经网络
对于人工神经网络来说,里面可能是这样的。主要我们也不知道隐层里面到底有几层。也不知道每层里面有几个神经元,各个神经元之间的关系。
图二、神经网络示意图
那么隐层是这么形成的呢?这就要说到神经网络的另外一个关键字,有监督的学习。所谓有监督的学习是在输入的时候已经告诉感知器这个是什么了。还拿上面的例子来说,就是在学习阶段,输入一个红色、甜、圆形、0.2kg的水果,告诉感知机这个就是苹果。再输入一个黄色、甜、长形、0.2kg的水果告诉它这个是香蕉。就这样通过大量的训练样本进行感知机的训练。在告诉感知机结果的同时,感知机也会有自己的判断。如果输入的是苹果,而感知机通过判断输出的是香蕉,感知机就会自动调整参数,直到误差控制在一定范围内。这个根据结果不断调整误差的过程,就叫做反馈。在BPNN中,因为误差是向前传播的,因此叫做前向反馈神经网络。这时隐藏层的就生成了,但是对我们来说这些都是我们看不见的东西。
那什么叫做感知机呢?通过上面的描述,感知机就是隐层所形成的神经网络。这个感知机概念的提出开始是单层的神经网络,随着多层次神经网络的发展,我们也将多层网络叫做感知机。感知机中包含若干神经元。不同的神经元组成不同的层次。
那什么叫做神经元呢?比较直观来说,上图中一个个圈就代表一个个神经元。每个神经元又有着复杂的内部结构。如下图所示。现在我们先不要纠结这些符号表示什么,大概有点印象即可。
图三、神经元内部结构
那么实际上最后的神经网络可能是这样子的。
图四、最终的神经网络
一个神经网络的形成大概就是这么一个过程,虽然实际上比这要复杂的多。这是一种不错的对于入门的人的一种直观认识。下面我们根据BPNN从专业的角度来分析什么是神经网络,以及如何实现一个神经网络了。大量的数学计算看起来让人头疼啊。
在前面我们引入了神经元的概念,见图三。由图可知,每个神经元只能有一个输出,但是有多个输入。其中图中,为第个神经元的内部状态,为神经元阀值,为输入信号,表示从第j个神经元到第个神经元连接的权值,说明神经元的每个输入都和一个权值相联系,正是这些权值决定了神经元网络的整体活跃性。权值的取值范围是【-1,1】,是浮点型数据。如果权值大于0,表示输入对神经元有激发(excitory)作用,反之权值为负数的话,对神经元有抑制(inhibitory)作用。当输入信号进入神经网络时,它们的值将会与它们对应的权值相乘,作为图中大圆的输入。大圆的‘核’是一个激励函数,它把所有的这些新的、经过权重调整后输入全部加起来,形成单个的激励值。激励值也是一个浮点数,且同样是可正可负的。根据激励值来产生函数的输出也即神经元的输出。如果激励值超过某个阀值,就会产生一个值为1的信号输出;如果激励值小于阀值,则输出0。由激励值产生的阶跃函数模型很明显是一个阶跃函数。
图五、阶跃函数
上述的假设可以描述为
上面的表达式表示的含义,是激励函数。H是状态转移函数。
由表达式可以看出,如果没有内部状态的话,,。
常用的神经元状态转移函数有:
(1)阶跃函数 1,
0,
(2)准则线函数
(3)Sigmoid函数
(4)双曲线正切函数
其实说了这么多我们还是不知道神经元是什么。
也许通过上面的介绍,你对神经元有了了解,也许你还是云里雾里,都没关系,先看下面的内容,可能是你豁然开朗。大脑中的生物神经细胞和其他的神经细胞连接在一起为大脑的信息处理共同努力。在人工神经网络中也是一样的,细胞需要连接起来,为了简化神经网络系统,现在广泛使用的是分层的神经网络。我们先看一下单层感知机的原理。
由图可知,神经网络共有三层。输入层中的每个输入都送到了隐藏层,作为该层每个神经元的输入;然后,从隐藏层的每个神经元的输出都被连接到它下一层(可能还是隐藏层还有可能是输出层)的每一个神经细胞。在一个神经网络中,一般来说有很多隐藏层,每个隐藏层中的神经元的个数也各不相同。也有的问题只要有一层就够了,甚至有的问题没有隐藏层都是可以的,你只要把那些输入直接连接到输出神经细胞就行了。神经网络的层数和每层中神经元的个数取决于要解决的问题的复杂性。显然,神经网络中层数和神经元的个数越多,神经网络工作的速度就越低。而且,不是网络越复杂,分类的精确度就越高,因此有异常数据的出现等。因此,在实际应用中,神经网络的规模要适当的小。
说了这么多,我想此时,可能你更加迷茫了。比如,每个神经元的阀值时一样的吗?每个神经元输出到下一层神经元的权值是相同的吗?下面我们举个例子来说吧。还是上面分水果的例子。a b c d代表的是颜色、味道、形状、大小。
在神经网络族中最常用的神经网络是BP神经网络。在《并行分布式处理》中对多层前馈的误差反向传播算法进行了深入研究,这使得BP神经网络在众多领域中得到广泛的应用。BP神经网络的主要思想是信号的向前传播和误差的后向传播。信号在正向传播中,输入信号通过隐藏层处理后传递给输出层。若输出层和预期的值不一样且大于误差可接受的范围之内,则进行误差反向传播过程。误差通过隐藏层向输入层传递,进行误差调整。通过不断调整各层之间的权值,直到输出误差达到可接受范围或者达到了最大学习次数为止。
BP神经网络可以有多个隐藏层。但是在《》一文中证明仅包含一个隐藏层的神经网络,只要有足够多的隐藏层神经元数目就能够以任意精度逼近一个连续的非线性函数,因此通常BP神经网络模型中只需要采用一个隐藏层就足够了。
BP神经网络的输入为,n为输入层神经元的个数;输出层为,m为输出层神经元的个数。隐藏层和输出层一般使用sigmoid函数作为激励函数。选用的sigmoid函数为上文介绍的。隐藏层的输入为,输出为。
在反向传播误差过程中,得到了BP神经网络的输出Y,其与预期的输出D之间存在误差,定义为 。由于 ,按照梯度最速下降法,权值和阀值的调整量和误差的梯度下降成正比。
Java实现怎么样的神经网络呢?我们的输入、输出是什么,算法的思想是什么。我们需要对纯数据的数据集分类,第一步需要做,我们的程序的目的是输入一个数据的时候能够自动判断它的类别。我们知道什么、不知道什么。
可以将每个神经元当成一个类,它的属性和方法有哪些呢?我们知道,每个神经元有多个输入信号和单一的输出信号,输出信号是由多个输入信号经过神经元。的转换函数变换后得到的。在每一个学习周期内,神经元必须根据反转回来的误差调整自己的权值。
单机版进行化的神经网络可以采取多线程的方式,使得每个神经元都继承线程。
我们上面只是简单的用代码介绍神经网络的基本原理,对开发一个可用的工具箱来说远远不够,接下来我们就使用一下神经网络的工具箱。
MATLAB 神经网络工具箱
Encog 神经网络工具箱
Mahout中的神经网络也是单机版的。在mahout-core中的org.apache.mahout.classifier.mlp的package中。在这个包中有三个类,这三个类分别是,能不能利用这三个类来写什么呢。
类名 |
解释 |
NeuralNetwork.java |
神经网络类 |
NeuralNetworkFunctions.java |
神经网络方法 |
MultilayerPerceptron.java |
多层感知机 |
在wiki上时这么解释多层感知机的:一个多层感知器(MLP)是一种前馈人工神经网络模型映射的输入数据集合上的一组适当的输出。一个MLP组成的有向图中的节点的多个层,每个层完全连接到下一个。除了输入节点,每个节点是一个神经元(或处理单元)与非线性激活函数。MLP利用监督学习技术称为反向传播算法训练网络。[ 1 ] [ 2 ] MLP是一个修改的标准线性感知器,可以区分的数据是不是线性可分的。这里我们就不再详细介绍了。
在mahout中没有神经网络的例子,是不是还没有开发好。我们自己写一个main函数吧。
马尔科夫过程是目前发展很快、应用很广的一种重要随机过程。它的特点是当前过程在t0时刻所处的状态为已知状态的条件下,过程在时刻t所处的状态仅与时刻t0所处的状态有关,而和过程t0时刻之前的状态无关,这种特性叫做无后效性。马尔科夫过程按照其状态和时间参数是连续的还是离散的,分成三类。(1)时间、状态都是离散的马尔科夫过程,成为马尔科夫链;(2)时间连续、状态离散的马尔科夫过程,称为连续时间的马尔科夫链;(3)时间、状态都连续的马尔科夫过程。马尔科夫链的应用十分广泛,包括文本分类、语音识别、信号处理等。在马尔科夫过程中常用的是马尔科夫链,马尔科夫链中最具代表性的是隐马尔科夫链。本文就具体介绍马尔科夫链和马尔科夫过程。
在概率论中马尔科夫链是这样定义的:由于马尔科夫链的状态和时间参数都是离散的,我们不妨假设:随机过程的状态空间是由有限个或可列多个状态构成,即状态空间为;过程只在时刻0,1,2,...发生状态转移,即参数集为。
将过程在时刻所处的状态记为,即。
假如随机过程在时刻处在任一状态的概率只与过程在时刻所处的状态有关,而与过程时刻以前所处的状态无关,即条件概率满足
则称随机过程为马尔科夫链,简称马氏链。(引自《概率统计和随机过程》)
总结:马尔科夫链是个离散过程。
在上面的介绍中,我们对马尔科夫链有了深刻的了解,隐马尔科夫链是马尔科夫链的一种,在语言识别、行为识别、文字识别等领域发挥着重要的作用。隐马尔可夫模型是马尔可夫链的一种,它的状态不能直接观察到,但能通过观测向量序列观察到,每个观测向量都是通过某些概率密度分布表现为各种状态,每一个观测向量是由一个具有相应概率密度分布的状态序列产生。所以,隐马尔可夫模型是一个双重随机过程----具有一定状态数的隐马尔可夫链和显示随机函数集。
举个例子来说明HMM。在wiki上的例子这么写道:Alice 和Bob是好朋友,但是他们离得比较远,每天都是通过电话了解对方那天作了什么.Bob仅仅对三种活动感兴趣:公园散步,购物以及清理房间.他选择做什么事情只凭当天天气.Alice对于Bob所住的地方的天气情况并不了解,但是知道总的趋势.在Bob告诉Alice每天所做的事情基础上,Alice想要猜测Bob所在地的天气情况.
Alice认为天气的运行就像一个马尔可夫链. 其有两个状态 “雨”和”晴”,但是无法直接观察它们,也就是说,它们对于Alice是隐藏的.每天,Bob有一定的概率进行下列活动:”散步”, “购物”, 或 “清理”. 因为Bob会告诉Alice他的活动,所以这些活动就是Alice的观察数据.这整个系统就是一个隐马尔可夫模型HMM.
Alice知道这个地区的总的天气趋势,并且平时知道Bob会做的事情.也就是说这个隐马尔可夫模型的参数是已知的。
在这个问题中有这么几个概念:
状态:{雨,晴}
状态转移矩阵:给定前一天天气情况下的当前天气概率。
向量:定义系统初始化时每一个状态的概率。假设雨晴的概率是{0.6,0.4}
在这些代码中,start_probability代表了Alice对于Bob第一次给她打电话时的天气情况的不确定性(Alice知道的只是那个地方平均起来下雨多些).在这里,这个特定的概率分布并非平衡的,平衡概率应该接近(在给定变迁概率的情况下){‘Rainy’: 0.571, ‘Sunny’: 0.429}。 transition_probability 表示马尔可夫链下的天气变迁情况,在这个例子中,如果今天下雨,那么明天天晴的概率只有30%.代码emission_probability 表示了Bob每天作某件事的概率.如果下雨,有 50% 的概率他在清理房间;如果天晴,则有60%的概率他在外头散步。
Alice和Bob通了三天电话后发现第一天Bob去散步了,第二天他去购物了,第三天他清理房间了。Alice现在有两个问题:这个观察序列“散步、购物、清理”的总的概率是多少?(注:这个问题对应于HMM的基本问题之一:已知HMM模型λ及观察序列O,如何计算P(O|λ)?) 最能解释这个观察序列的状态序列(晴/雨)又是什么?(注:这个问题对应HMM基本问题之二:给定观察序列O=O1,O2,…OT以及模型λ,如何选择一个对应的状态序列S = q1,q2,…qT,使得S能够最为合理的解释观察序列O?)
至于HMM的基本问题之三:如何调整模型参数, 使得P(O|λ)最大?这个问题事实上就是给出很多个观察序列值,来训练以上几个参数的问题。
针对评估问题:前向算法
解码问题:Viterbi算法
学习问题:Baum-Welch算法(向前向后算法)
隐马尔科夫链(HMM)中有五个重要的元素
1、 隐含状态
一个可以由马尔科夫过程描述的系统的真实状态。比如上面示例中的天气。
2、 可观测状态O
在这个马尔科夫过程中可视的状态。比如上面示例中的Bob的活动。
3、 初始状态概率矩阵
马尔科夫模型在时间t=1时的隐含状态的概率。对应上面示例中的天气的初始概率矩阵。
4、 隐含状态转移概率矩阵A
包含了一个隐含状态到另一个隐含状态的概率。
5、 观测状态转移概率矩阵B
包含了给定隐马尔科夫模型的某一个特殊的隐藏状态,观察到的某个观测状态的概率。
隐马尔科夫链(HMM)的三类问题
在上面的例子中,我们已经提到了这几个问题。即:给定HMM求一个观测序列的概率;搜索最有可能生成一个观测序列的隐含状态训练;给定观测序列生成一个隐马尔科夫链。这三个问题对应不同的应用场景,也有着不同的算法。
1、评估问题,也就是给定HMM求一个观测序列的概率。比如,对于一个场景来说,建立了不同的HMM模型,我们想要知道哪一个HMM最有可能产生这个给定的观测序列。在上面的示例中,有两个同学分别建立了HMM的模型,现在有这么一个观测序列{逛街、打扫房间、逛街},我们想要计算每个模型中观测到的观测序列的概率。现在业界使用前向算法(forward algorithm)来计算给定HMM后的一个观测序列的概率,并根据这个结果选择最合适的HMM模型。在语音识别中这种类型的问题发生在当一大堆数目的马尔科夫模型被使用,并且每一个模型都对一个特殊的单词进行建模时。一个观察序列从一个发音单词中形成,并且通过寻找对于此观察序列最有可能的隐马尔科夫模型(HMM)识别这个单词。
2、解码问题,也就是给定观察序列搜索最可能的隐藏状态序列。另一个相关问题,也是最感兴趣的一个,就是搜索生成输出序列的隐藏状态序列。在许多情况下我们对于模型中的隐藏状态更感兴趣,因为它们代表了一些更有价值的东西,而这些东西通常不能直接观察到。考虑上文的例子,Alice只能知道Bob的活动状态,但是他更想知道天气的情况,天气状态在这里就是隐藏状态。我们使用Viterbi 算法(Viterbi algorithm)确定(搜索)已知观察序列及HMM下最可能的隐藏状态序列。Viterbi算法(Viterbi algorithm)的另一广泛应用是自然语言处理中的词性标注。在词性标注中,句子中的单词是观察状态,词性(语法类别)是隐藏状态(注意对于许多单词,如wind,fish拥有不止一个词性)。对于每句话中的单词,通过搜索其最可能的隐藏状态,我们就可以在给定的上下文中找到每个单词最可能的词性标注。
3、学习问题,即根据观察序列生成隐马尔科夫模型。与HMM相关的问题中最难的,根据一个观察序列(来自于已知的集合),以及与其有关的一个隐藏状态集,估计一个最合适的隐马尔科夫模型(HMM),也就是确定对已知序列描述的最合适的(,A,B)三元组。当矩阵A和B不能够直接被(估计)测量时,前向-后向算法(forward-backward algorithm)被用来进行学习(参数估计),这也是实际应用中常见的情况。
我们的目标是计算给定隐马尔科夫模型HMM下的观察序列的概率。因此我们首先通过计算局部概率降低计算整个概率的复杂度,局部概率表示的是t时刻到达某个状态s的概率。t=1时,可以利用初始概率(来自于P向量)和观察概率Pr(observation|state)计算局部概率;而t>1时的局部概率可以利用t-时的局部概率计算。因此,这个问题是递归问题,观察序列的概率就是通过依次计算t=1,2,…,T时的局部概率,并且对于t=T时所有局部概率’s相加得到的。值得注意的是,用这种方式计算观察序列概率的时间复杂度远远小于计算所有序列的概率并对其相加(穷举搜索)的时间复杂度。
我们使用前向算法计算T长观察序列的概率:其中y的每一个是观察集合之一。局部(中间)概率(’s)是递归计算的,首先通过计算t=1时刻所有状态的局部概率:然后在每个时间点,t=2,… ,T时,对于每个状态的局部概率,由下式计算局部概率:也就是当前状态相应的观察概率与所有到达该状态的路径概率之积,其递归地利用了上一个时间点已经计算好的一些值。
最后,给定HMM,,观察序列的概率等于T时刻所有局部概率之和:
再重复说明一下,每一个局部概率(t > 2 时)都由前一时刻的结果计算得出。
对于“天气”那个例子,下面的图表显示了t = 2为状态为多云时局部概率的计算过程。这是相应的观察概率b与前一时刻的局部概率与状态转移概率a相乘后的总和再求积的结果:
如何计算观察序列的概率(Finding the probability of an observed sequence)呢?有下面两种方法。
1.穷举搜索( Exhaustive search for solution)
给定隐马尔科夫模型,也就是在模型参数(, A, B)已知的情况下,我们想找到观察序列的概率。还是考虑天气这个例子,我们有一个用来描述天气及与它密切相关的海藻湿度状态的隐马尔科夫模型(HMM),另外我们还有一个海藻的湿度状态观察序列。假设连续3天海藻湿度的观察结果是(干燥、湿润、湿透)——而这三天每一天都可能是晴天、多云或下雨,对于观察序列以及隐藏的状态,可以将其视为网格:网格中的每一列都显示了可能的的天气状态,并且每一列中的每个状态都与相邻列中的每一个状态相连。而其状态间的转移都由状态转移矩阵提供一个概率。在每一列下面都是某个时间点上的观察状态,给定任一个隐藏状态所得到的观察状态的概率由混淆矩阵提供。
可以看出,一种计算观察序列概率的方法是找到每一个可能的隐藏状态,并且将这些隐藏状态下的观察序列概率相加。对于上面那个(天气)例子,将有3^3 = 27种不同的天气序列可能性,因此,观察序列的概率是:
Pr(dry,damp,soggy | HMM) = Pr(dry,damp,soggy | sunny,sunny,sunny) + Pr(dry,damp,soggy | sunny,sunny ,cloudy) + Pr(dry,damp,soggy | sunny,sunny ,rainy) + . . . . Pr(dry,damp,soggy | rainy,rainy ,rainy)
用这种方式计算观察序列概率极为昂贵,特别对于大的模型或较长的序列,因此我们可以利用这些概率的时间不变性来减少问题的复杂度。
2.使用递归降低问题复杂度
给定一个隐马尔科夫模型(HMM),我们将考虑递归地计算一个观察序列的概率。我们首先定义局部概率(partial probability),它是到达网格中的某个中间状态时的概率。然后,我们将介绍如何在t=1和t=n(>1)时计算这些局部概率。
假设一个T-长观察序列是:
2a.局部概率(’s)
考虑下面这个网格,它显示的是天气状态及对于观察序列干燥,湿润及湿透的一阶状态转移情况。
我们可以将计算到达网格中某个中间状态的概率作为所有到达这个状态的可能路径的概率求和问题。
例如,t=2时位于“多云”状态的局部概率通过如下路径计算得出:
我们定义t时刻位于状态j的局部概率为at(j)——这个局部概率计算如下:
t ( j )= Pr( 观察状态 | 隐藏状态j ) x Pr(t时刻所有指向j状态的路径)
对于最后的观察状态,其局部概率包括了通过所有可能的路径到达这些状态的概率——例如,对于上述网格,最终的局部概率通过如下路径计算得出:
由此可见,对于这些最终局部概率求和等价于对于网格中所有可能的路径概率求和,也就求出了给定隐马尔科夫模型(HMM)后的观察序列概率。
第3节给出了一个计算这些概率的动态示例。
2b.计算t=1时的局部概率’s
我们按如下公式计算局部概率:
t ( j )= Pr( 观察状态 | 隐藏状态j ) x Pr(t时刻所有指向j状态的路径)
特别当t=1时,没有任何指向当前状态的路径。故t=1时位于当前状态的概率是初始概率,即Pr(state|t=1)=P(state),因此,t=1时的局部概率等于当前状态的初始概率乘以相关的观察概率:
所以初始时刻状态j的局部概率依赖于此状态的初始概率及相应时刻我们所见的观察概率。
2c.计算t>1时的局部概率’s
我们再次回顾局部概率的计算公式如下:
t ( j )= Pr( 观察状态 | 隐藏状态j ) x Pr(t时刻所有指向j状态的路径)
我们可以假设(递归地),乘号左边项“Pr( 观察状态 | 隐藏状态j )”已经有了,现在考虑其右边项“Pr(t时刻所有指向j状态的路径)”。
为了计算到达某个状态的所有路径的概率,我们可以计算到达此状态的每条路径的概率并对它们求和,例如:
计算所需要的路径数目随着观察序列的增加而指数级递增,但是t-1时刻’s给出了所有到达此状态的前一路径概率,因此,我们可以通过t-1时刻的局部概率定义t时刻的’s,即:
故我们所计算的这个概率等于相应的观察概率(亦即,t+1时在状态j所观察到的符号的概率)与该时刻到达此状态的概率总和——这来自于上一步每一个局部概率的计算结果与相应的状态转移概率乘积后再相加——的乘积。
注意我们已经有了一个仅利用t时刻局部概率计算t+1时刻局部概率的表达式。
现在我们就可以递归地计算给定隐马尔科夫模型(HMM)后一个观察序列的概率了——即通过t=1时刻的局部概率’s计算t=2时刻的’s,通过t=2时刻的’s计算t=3时刻的’s等等直到t=T。给定隐马尔科夫模型(HMM)的观察序列的概率就等于t=T时刻的局部概率之和。
2d.降低计算复杂度
我们可以比较通过穷举搜索(评估)和通过递归前向算法计算观察序列概率的时间复杂度。
我们有一个长度为T的观察序列O以及一个含有n个隐藏状态的隐马尔科夫模型l=(,A,B)。
穷举搜索将包括计算所有可能的序列:
对我们所观察到的概率求和——注意其复杂度与T成指数级关系。相反的,使用前向算法我们可以利用上一步计算的信息,相应地,其时间复杂度与T成线性关系。
注:穷举搜索的时间复杂度是,前向算法的时间复杂度是,其中T指的是观察序列长度,N指的是隐藏状态数目。
寻找最可能的隐藏状态序列,对于一个特殊的隐马尔科夫模型(HMM)及一个相应的观察序列,我们常常希望能找到生成此序列最可能的隐藏状态序列。同样求解这个问题有两种方法。
1.穷举搜索
我们使用下面这张网格图片来形象化的说明隐藏状态和观察状态之间的关系:
我们可以通过列出所有可能的隐藏状态序列并且计算对于每个组合相应的观察序列的概率来找到最可能的隐藏状态序列。最可能的隐藏状态序列是使下面这个概率最大的组合:
Pr(观察序列|隐藏状态的组合)
例如,对于网格中所显示的观察序列,最可能的隐藏状态序列是下面这些概率中最大概率所对应的那个隐藏状态序列:
Pr(dry,damp,soggy | sunny,sunny,sunny), Pr(dry,damp,soggy | sunny,sunny,cloudy), Pr(dry,damp,soggy | sunny,sunny,rainy), . . . . Pr(dry,damp,soggy | rainy,rainy,rainy)
这种方法是可行的,但是通过穷举计算每一个组合的概率找到最可能的序列是极为昂贵的。与前向算法类似,我们可以利用这些概率的时间不变性来降低计算复杂度。
2.使用递归降低复杂度
给定一个观察序列和一个隐马尔科夫模型(HMM),我们将考虑递归地寻找最有可能的隐藏状态序列。我们首先定义局部概率,它是到达网格中的某个特殊的中间状态时的概率。然后,我们将介绍如何在t=1和t=n(>1)时计算这些局部概率。
这些局部概率与前向算法中所计算的局部概率是不同的,因为它们表示的是时刻t时到达某个状态最可能的路径的概率,而不是所有路径概率的总和。
2a.局部概率’s和局部最佳途径
考虑下面这个网格,它显示的是天气状态及对于观察序列干燥,湿润及湿透的一阶状态转移情况:
对于网格中的每一个中间及终止状态,都有一个到达该状态的最可能路径。举例来说,在t=3时刻的3个状态中的每一个都有一个到达此状态的最可能路径,或许是这样的:
我们称这些路径局部最佳路径(partial best paths)。其中每个局部最佳路径都有一个相关联的概率,即局部概率或。与前向算法中的局部概率不同,是到达该状态(最可能)的一条路径的概率。
因而(i,t)是t时刻到达状态i的所有序列概率中最大的概率,而局部最佳路径是得到此最大概率的隐藏状态序列。对于每一个可能的i和t值来说,这一类概率(及局部路径)均存在。
特别地,在t=T时每一个状态都有一个局部概率和一个局部最佳路径。这样我们就可以通过选择此时刻包含最大局部概率的状态及其相应的局部最佳路径来确定全局最佳路径(最佳隐藏状态序列)。
2b.计算t=1时刻的局部概率’s
我们计算的局部概率是作为最可能到达我们当前位置的路径的概率(已知的特殊知识如观察概率及前一个状态的概率)。当t=1的时候,到达某状态的最可能路径明显是不存在的;但是,我们使用t=1时的所处状态的初始概率及相应的观察状态k1的观察概率计算局部概率;即
——与前向算法类似,这个结果是通过初始概率和相应的观察概率相乘得出的。
2c.计算t>1时刻的局部概率’s
现在我们来展示如何利用t-1时刻的局部概率计算t时刻的局部概率。
考虑如下的网格:
我们考虑计算t时刻到达状态X的最可能的路径;这条到达状态X的路径将通过t-1时刻的状态A,B或C中的某一个。
因此,最可能的到达状态X的路径将是下面这些路径的某一个
(状态序列),…,A,X
(状态序列),…,B,X
或 (状态序列),…,C,X
我们想找到路径末端是AX,BX或CX并且拥有最大概率的路径。
回顾一下马尔科夫假设:给定一个状态序列,一个状态发生的概率只依赖于前n个状态。特别地,在一阶马尔可夫假设下,状态X在一个状态序列后发生的概率只取决于之前的一个状态,即
Pr (到达状态A最可能的路径) .Pr (X | A) . Pr (观察状态 | X)
与此相同,路径末端是AX的最可能的路径将是到达A的最可能路径再紧跟X。相似地,这条路径的概率将是:
Pr (到达状态A最可能的路径) .Pr (X | A) . Pr (观察状态 | X)
因此,到达状态X的最可能路径概率是:
其中第一项是t-1时刻的局部概率,第二项是状态转移概率以及第三项是观察概率。
泛化上述公式,就是在t时刻,观察状态是kt,到达隐藏状态i的最佳局部路径的概率是:
这里,我们假设前一个状态的知识(局部概率)是已知的,同时利用了状态转移概率和相应的观察概率之积。然后,我们就可以在其中选择最大的概率了(局部概率)。
2d.反向指针,’s
考虑下面这个网格
在每一个中间及终止状态我们都知道了局部概率,(i,t)。然而我们的目标是在给定一个观察序列的情况下寻找网格中最可能的隐藏状态序列——因此,我们需要一些方法来记住网格中的局部最佳路径。
回顾一下我们是如何计算局部概率的,计算t时刻的’s我们仅仅需要知道t-1时刻的’s。在这个局部概率计算之后,就有可能记录前一时刻哪个状态生成了(i,t)——也就是说,在t-1时刻系统必须处于某个状态,该状态导致了系统在t时刻到达状态i是最优的。这种记录(记忆)是通过对每一个状态赋予一个反向指针完成的,这个指针指向最优的引发当前状态的前一时刻的某个状态。
形式上,我们可以写成如下的公式
其中argmax运算符是用来计算使括号中表达式的值最大的索引j的。
请注意这个表达式是通过前一个时间步骤的局部概率’s和转移概率计算的,并不包括观察概率(与计算局部概率’s本身不同)。这是因为我们希望这些’s能回答这个问题“如果我在这里,最可能通过哪条路径到达下一个状态?”——这个问题与隐藏状态有关,因此与观察概率有关的混淆(矩阵)因子是可以被忽略的。
2e.维特比算法的优点
使用Viterbi算法对观察序列进行解码有两个重要的优点:
1. 通过使用递归减少计算复杂度——这一点和前向算法使用递归减少计算复杂度是完全类似的。
2.维特比算法有一个非常有用的性质,就是对于观察序列的整个上下文进行了最好的解释(考虑)。事实上,寻找最可能的隐藏状态序列不止这一种方法,其他替代方法也可以,譬如,可以这样确定如下的隐藏状态序列:
其中
这里,采用了“自左向右”的决策方式进行一种近似的判断,其对于每个隐藏状态的判断是建立在前一个步骤的判断的基础之上(而第一步从隐藏状态的初始向量开始)。
这种做法,如果在整个观察序列的中部发生“噪音干扰”时,其最终的结果将与正确的答案严重偏离。
相反, 维特比算法在确定最可能的终止状态前将考虑整个观察序列,然后通过指针“回溯”以确定某个隐藏状态是否是最可能的隐藏状态序列中的一员。这是非常有用的,因为这样就可以孤立序列中的“噪音”,而这些“噪音”在实时数据中是很常见的。
3.小结
维特比算法提供了一种有效的计算方法来分析隐马尔科夫模型的观察序列,并捕获最可能的隐藏状态序列。它利用递归减少计算量,并使用整个序列的上下文来做判断,从而对包含“噪音”的序列也能进行良好的分析。
在使用时,维特比算法对于网格中的每一个单元(cell)都计算一个局部概率,同时包括一个反向指针用来指示最可能的到达该单元的路径。当完成整个计算过程后,首先在终止时刻找到最可能的状态,然后通过反向指针回溯到t=1时刻,这样回溯路径上的状态序列就是最可能的隐藏状态序列了。
1、维特比算法的形式化定义
维特比算法可以形式化的概括为:
对于每一个i,i = 1,… ,n,令:
——这一步是通过隐藏状态的初始概率和相应的观察概率之积计算了t=1时刻的局部概率。
对于t=2,…,T和i=1,…,n,令:
——这样就确定了到达下一个状态的最可能路径,并对如何到达下一个状态做了记录。具体来说首先通过考察所有的转移概率与上一步获得的最大的局部概率之积,然后记录下其中最大的一个,同时也包含了上一步触发此概率的状态。
令:
——这样就确定了系统完成时(t=T)最可能的隐藏状态。
对于t=T-1,…,1
令:
——这样就可以按最可能的状态路径在整个网格回溯。回溯完成时,对于观察序列来说,序列i1 … iT就是生成此观察序列的最可能的隐藏状态序列。
2.计算单独的’s和’s
维特比算法中的局部概率’s的计算与前向算法中的局部概率’s的很相似。下面这幅图表显示了’s和’s的计算细节,可以对比一下前向算法3中的计算局部概率’s的那幅图表:
唯一不同的是前向算法中计算局部概率’s时的求和符号()在维特比算法中计算局部概率’s时被替换为max——这一个重要的不同也说明了在维特比算法中我们选择的是到达当前状态的最可能路径,而不是总的概率。我们在维特比算法中维护了一个“反向指针”记录了到达当前状态的最佳路径,即在计算’s时通过argmax运算符获得。
总结(Summary)
对于一个特定的隐马尔科夫模型,维特比算法被用来寻找生成一个观察序列的最可能的隐藏状态序列。我们利用概率的时间不变性,通过避免计算网格中每一条路径的概率来降低问题的复杂度。维特比算法对于每一个状态(t>1)都保存了一个反向指针(),并在每一个状态中存储了一个局部概率()。
局部概率是由反向指针指示的路径到达某个状态的概率。
当t=T时,维特比算法所到达的这些终止状态的局部概率’s是按照最优(最可能)的路径到达该状态的概率。因此,选择其中最大的一个,并回溯找出所隐藏的状态路径,就是这个问题的最好答案。
关于维特比算法,需要着重强调的一点是它不是简单的对于某个给定的时间点选择最可能的隐藏状态,而是基于全局序列做决策——因此,如果在观察序列中有一个“非寻常”的事件发生,对于维特比算法的结果也影响不大。
这在语音处理中是特别有价值的,譬如当某个单词发音的一个中间音素出现失真或丢失的情况时,该单词也可以被识别出来。
根据观察序列生成隐马尔科夫模型
根据观察序列生成隐马尔科夫模型(Generating a HMM from a sequence of obersvations)
与HMM模型相关的“有用”的问题是评估(前向算法)和解码(维特比算法)——它们一个被用来测量一个模型的相对适用性,另一个被用来推测模型隐藏的部分在做什么(“到底发生了”什么)。可以看出它们都依赖于隐马尔科夫模型(HMM)参数这一先验知识——状态转移矩阵,混淆(观察)矩阵,以及向量(初始化概率向量)。
然而,在许多实际问题的情况下这些参数都不能直接计算的,而要需要进行估计——这就是隐马尔科夫模型中的学习问题。前向-后向算法就可以以一个观察序列为基础来进行这样的估计,而这个观察序列来自于一个给定的集合,它所代表的是一个隐马尔科夫模型中的一个已知的隐藏集合。
一个例子可能是一个庞大的语音处理数据库,其底层的语音可能由一个马尔可夫过程基于已知的音素建模的,而其可以观察的部分可能由可识别的状态(可能通过一些矢量数据表示)建模的,但是没有(直接)的方式来获取隐马尔科夫模型(HMM)参数。
前向-后向算法并非特别难以理解,但自然地比前向算法和维特比算法更复杂。由于这个原因,这里就不详细讲解前向-后向算法了(任何有关HMM模型的参考文献都会提供这方面的资料的)。
总之,前向-后向算法首先对于隐马尔科夫模型的参数进行一个初始的估计(这很可能是完全错误的),然后通过对于给定的数据评估这些参数的的价值并减少它们所引起的错误来重新修订这些HMM参数。从这个意义上讲,它是以一种梯度下降的形式寻找一种错误测度的最小值。
之所以称其为前向-后向算法,主要是因为对于网格中的每一个状态,它既计算到达此状态的“前向”概率(给定当前模型的近似估计),又计算生成此模型最终状态的“后向”概率(给定当前模型的近似估计)。 这些都可以通过利用递归进行有利地计算,就像我们已经看到的。可以通过利用近似的HMM模型参数来提高这些中间概率进行调整,而这些调整又形成了前向-后向算法迭代的基础。
注:关于前向-后向算法,原文只讲了这么多,后继我将按自己的理解补充一些内容。
要理解前向-后向算法,首先需要了解两个算法:后向算法和EM算法。后向算法是必须的,因为前向-后向算法就是利用了前向算法与后向算法中的变量因子,其得名也因于此;而EM算法不是必须的,不过由于前向-后向算法是EM算法的一个特例,因此了解一下EM算法也是有好处的,说实话,对于EM算法,我也是云里雾里的。好了,废话少说,我们先谈谈后向算法。
好了,后向算法就到此为止了,下一节我们粗略的谈谈EM算法。
前向-后向算法是Baum于1972年提出来的,又称之为Baum-Welch算法,虽然它是EM(Expectation-Maximization)算法的一个特例,但EM算法却是于1977年提出的。那么为什么说前向-后向算法是EM算法的一个特例呢?这里有两点需要说明一下。
第一,1977年A. P. Dempster、N. M. Laird、 D. B. Rubin在其论文“Maximum Likelihood from Incomplete Data via the EM Algorithm”中首次提出了EM算法的概念,但是他们也在论文的介绍中提到了在此之前就有一些学者利用了EM算法的思想解决了一些特殊问题,其中就包括了Baum在70年代初期的相关工作,只是这类方法没有被总结而已,他们的工作就是对这类解决问题的方法在更高的层次上定义了一个完整的EM算法框架。
第二,对于前向-后向算法与EM算法的关系,此后在许多与HMM或EM相关的论文里都被提及,其中贾里尼克(Jelinek)老先生在1997所著的书“Statistical Methods for Speech Recognition”中对于前向-后向算法与EM算法的关系进行了完整的描述,读者有兴趣的话可以找来这本书读读。
关于EM算法的讲解,网上有很多,这里我就不献丑了,直接拿目前搜索“EM算法”在Google排名第一的文章做了参考,希望读者不要拍砖:
EM 算法是 Dempster,Laind,Rubin 于 1977 年提出的求参数极大似然估计的一种方法,它可以从非完整数据集中对参数进行 MLE 估计,是一种非常简单实用的学习算法。这种方法可以广泛地应用于处理缺损数据,截尾数据,带有讨厌数据等所谓的不完全数据(incomplete data)。
假定集合Z = (X,Y)由观测数据 X 和未观测数据Y 组成,Z = (X,Y)和 X 分别称为完整数据和不完整数据。假设Z的联合概率密度被参数化地定义为P(X,Y|Θ),其中Θ 表示要被估计的参数。Θ 的最大似然估计是求不完整数据的对数似然函数L(X;Θ)的最大值而得到的:
L(Θ; X )= log p(X |Θ) = ∫log p(X ,Y |Θ)dY ;(1)
EM算法包括两个步骤:由E步和M步组成,它是通过迭代地最大化完整数据的对数似然函数Lc( X;Θ )的期望来最大化不完整数据的对数似然函数,其中:
Lc(X;Θ) =log p(X,Y |Θ) ; (2)
假设在算法第t次迭代后Θ 获得的估计记为Θ(t ) ,则在(t+1)次迭代时,
E-步:计算完整数据的对数似然函数的期望,记为:
Q(Θ |Θ (t) ) = E{Lc(Θ;Z)|X;Θ(t) }; (3)
M-步:通过最大化Q(Θ |Θ(t) ) 来获得新的Θ 。
通过交替使用这两个步骤,EM算法逐步改进模型的参数,使参数和训练样本的似然概率逐渐增大,最后终止于一个极大点。
直观地理解EM算法,它也可被看作为一个逐次逼近算法:事先并不知道模型的参数,可以随机的选择一套参数或者事先粗略地给定某个初始参数λ0 ,确定出对应于这组参数的最可能的状态,计算每个训练样本的可能结果的概率,在当前的状态下再由样本对参数修正,重新估计参数λ ,并在新的参数下重新确定模型的状态,这样,通过多次的迭代,循环直至某个收敛条件满足为止,就可以使得模型的参数逐渐逼近真实参数。
EM算法的主要目的是提供一个简单的迭代算法计算后验密度函数,它的最大优点是简单和稳定,但容易陷入局部最优。
有了后向算法和EM算法的预备知识,下一节我们就正式的谈一谈前向-后向算法。
隐马尔科夫模型(HMM)的三个基本问题中,第三个HMM参数学习的问题是最难的,因为对于给定的观察序列O,没有任何一种方法可以精确地找到一组最优的隐马尔科夫模型参数(A、B、)使P(O|)最大。因而,学者们退而求其次,不能使P(O|)全局最优,就寻求使其局部最优(最大化)的解决方法,而前向-后向算法(又称之为Baum-Welch算法)就成了隐马尔科夫模型学习问题的一种替代(近似)解决方法。
我们首先定义两个变量。给定观察序列O及隐马尔科夫模型,定义t时刻位于隐藏状态Si的概率变量为:
回顾一下第二节中关于前向变量at(i)及后向变量Bt(i)的定义,我们可以很容易地将上式用前向、后向变量表示为:
其中分母的作用是确保:
给定观察序列O及隐马尔科夫模型,定义t时刻位于隐藏状态Si及t+1时刻位于隐藏状态Sj的概率变量为:
该变量在网格中所代表的关系如下图所示:
同样,该变量也可以由前向、后向变量表示:
而上述定义的两个变量间也存在着如下关系:
如果对于时间轴t上的所有相加,我们可以得到一个总和,它可以被解释为从其他隐藏状态访问Si的期望值(网格中的所有时间的期望),或者,如果我们求和时不包括时间轴上的t=T时刻,那么它可以被解释为从隐藏状态Si出发的状态转移期望值。相似地,如果对在时间轴t上求和(从t=1到t=T-1),那么该和可以被解释为从状态Si到状态Sj的状态转移期望值。即:
上一节我们定义了两个变量及相应的期望值,本节我们利用这两个变量及其期望值来重新估计隐马尔科夫模型(HMM)的参数,A及B:
如果我们定义当前的HMM模型为,那么可以利用该模型计算上面三个式子的右端;我们再定义重新估计的HMM模型为,那么上面三个式子的左端就是重估的HMM模型参数。Baum及他的同事在70年代证明了因此如果我们迭代地的计算上面三个式子,由此不断地重新估计HMM的参数,那么在多次迭代后可以得到的HMM模型的一个最大似然估计。不过需要注意的是,前向-后向算法所得的这个结果(最大似然估计)是一个局部最优解。
关于前向-后向算法和EM算法的具体关系的解释,大家可以参考HMM经典论文《A tutorial on Hidden Markov Models and selected applications in speech recognition》,这里就不详述了。下面我们给出UMDHMM中的前向-后向算法示例,这个算法比较复杂,这里只取主函数,其中所引用的函数大家如果感兴趣的话可以自行参考UMDHMM。
类图,每个类的作用是
实现的原理是什么
在mahout中有个例子是使用HMM对文本进行分类。由于HMM的mahout代码是单机版的,所以跑在Hadoop集群上和跑在本地是没有差别的。
PosTagger.java |
|
|
BaumWelchTrainer.java |
后向-前向算法 |
|
HmmAlgorithm.java |
|
|
HmmEvaluator.java |
|
|
HmmModel.java |
|
|
HmmTrainer.java |
|
|
HmmUtils.java |
|
|
LossyHmmSerializer.java |
|
|
RandomSequenceGenertator.java |
|
|
ViterbiEvaluator.java |
维特比算法 |
|
在这些类中最关键的类是HmmAlgorithm.java,这个类中实现了三种HMM算法。
三部曲:
1、训练成HMMmodel
2、测试HMMMode
3、输入HMMModel和测试语句进行分类。
现在有这样的需求该怎么做。自己调用HMM的代码实现一个文本分类。
在分类算法中往往需要训练样本,它们是有监督的学习,这样做的成本比较高。而聚类算法是无监督的学习,在推荐系统中在对用户和商品继续分类时也经常用到这个算法。
聚类算法思想是将相近的点聚合在一起。举个例子来说,现在需要将淘宝用户分成K类,我们事先知道他们的行为特征,将这些行为抽象成特性向量。现在随机选择K个人,作为每个类别的中心点,对于其他人来说,根据计算他们到这K个人的距离(距离是通过特性向量计算的,标志的是两个之间的相似程度),将他们跟距离最短的(最相近的)人归为一类,形成一个新的集合。重新计算每个集合的中心值,将他作为新的该类的中心点。再次计算每个人的归属类别,直到所有的类别都不再发生变化为止。
聚类(Clustering)是将一些物理上的或者是抽象的对象进行分组的过程,聚类也是特殊的分类。聚类和分类的不同在于,分类是从上到下的分裂过程,聚类是从下向上的聚合过程。我们将聚类生成的组叫做簇(cluster),这些簇是数据对象的集合。好的聚类结果是,同一簇包含的数据集合相似度高,不同簇包含的数据集合之间的差异较大。相似度的表示方法有很多,比如空间坐标之间使用距离表示;抽象的事物之间使用特征向量表示,等等。
K-means是最常用的一种聚类算法,它使用均值作为新的中心点,因此叫做K-means算法。K-means算法以其易于理解、简单适应性广的特性在多个领域占有重要地位,特别是在一些统计分析软件例如SPSS中集成了K-means算法。
图一、K-means算法示意图
如图所示,现在我们需要将A,B,C,D,E五个点进行聚类,图中的无符号的两个点表示当前中心点。依次计算这五个点到这两个中心点的聚类,这时将A、B聚类成第一类别,将C、D、E聚类成第二类别;更新中心点,这时中心点变成图中新的无符号的两个圆点。进入下一次迭代过程,再次对这五个点进行聚类,此时A、B、C聚类成第一类别,D、E聚类成第二类别。再次更新中心点。重复上述迭代过程,直到中心点的位置不再改变(用收敛近似表示)。最终的聚类结果就是最后一次迭代过程的结果。
在k-means算法中,数据之间的相似度是非常重要的概念。两个数据之间的相似度是靠距离来衡量的,在对数据进行处理时往往是将数据转换成多维向量,在通过向量距离来进行相似度。设两个维向量和 分别表示两个对象,有多种形式的距离向量可以采用。
(1)闽科夫斯基距离
其中。闽科夫距离是无限个距离度量的概化。当它的值为1时,为曼哈顿距离;当它的值为2时为欧几里得距离;当它接近无穷大时是切比雪夫距离。
(2)曼哈顿距离
(3)欧几里得距离
(4)切比雪夫距离
上面几种运算公式是比较常见的,它们表示的是以哪种发生逼近中心点的。图下图中图(a)是哈弗曼距离的逼近方式,(b)代表的是闽科夫斯基距离,(c)代表的是欧几里得距离。至于他们的优缺点和应用范围不在本书的重点介绍范围之内,读者们可以自行查找资料,选择最合适的算法。
(a) (b) (c)
图二 几种距离公式的逼近方式
K-means算法中最重要的步骤之一是中心点的更新,k-means算法是用簇中的数据的均值来当做新的中心点。
个点的中心点的距离为
K-means算法的算法流程可以用下表表示。
表一、k-means算法流程图
输入:m个数据 |
过程: 1)随机选择K个数据作为聚类的K个中心。 2)对其他所有的数据分别求其到K个中心的距离,距离哪个中心点最近就将哪个归为一类。 3)更新聚类中心 4)重复(2)(3)知道聚类中心不再移动。 |
输出:分类结果 |
K-means算法非常简单,那么我们动手写个程序来近距离的接触一下K-means算法吧。
为了让读者更好的理解k-means算法,这里我们先用java实现一下。数据如下格式:每行数代表某个点的n为坐标向量,对这几个点进行聚类。它们被保存在文件中,文件中每行数字以’\t’隔开。在这里可以看出是空间坐标,三个数字分别表示(x,y,z)。要求将这些点分成三类,并将每一类包含的数据集合依次输出。
输入数据data.txt
0.3 0.2 0.4
0.4 0.2 0.4
0.5 0.2 0.4
5.0 5.2 5.4
6.0 5.2 6.4
4.0 5.2 4.4
10.3 10.4 10.5
10.3 10.4 10.5
10.3 10.5 10.6
由于这些点在存储已经计算它们之间的距离是比较麻烦,我们将它定义为一个类,叫做Node.它包括x,y,z三个成员变量以及两个构造函数分别是有参数和无参数的。为了方便计算两个点之间的距离,我们还定义了一个compareNode(Node node)函数,这里使用的距离公式是欧几里得距离公式。该类的定义如下。
public class Node {
private double x;
private double y;
private double z;
Node(double x,double y,double z){
this.x = x;
this.y = y;
this.z = z;
}
double CompareNode(Node node){
double result = (this.x -node.getX())*(this.x -node.getX())
+(this.y -node.getY())*(this.y -node.getY())+
(this.z -node.getZ())*(this.z -node.getZ());
result = Math.sqrt(result);
return result;
}
}
K-means算法是完全按照上述算法流程实现的。下面是算法的伪代码和关键程序。由于篇幅原理,不能列出全部源码。首先将全部的数据读入到节点链表(nodes)中,再构建一个类别链表数组(classes),它们存放每个类别的节点;为了找到该节点属于某个类别,需要将该节点与每个节点的距离保存在double数组(disnums)中;为了判断迭代是否结束,在更新中心点的同时,计算新中心点和原来中心点的距离,小于某个阀值时该值是收敛的,视为该中心点不再变化。但是要想停止迭代需要所有的中心点都不能变化。所以使用布尔数值stop来表示每个中心点是否收敛。
* 参数说明
* nodes 从文件中读入的节点放在链表中.
* classes 是一个链表数组,存放的是每个类别中的节点
* centernodes 存放中心节点的数组
* disnums存放每个节点与中心点的距离差,以便找到最小的那个
* stop 布尔数组,表示每个中心点是否停止迭代
* k 分组的个数
给出关键代码的伪代码如下表。
表二、K-means伪代码
输入:文件 |
初始化全部节点; 初始化中心点; do{ for(第一个节点->最后一个节点){ for(第一个中心点->最后一个中心点){ 计算节点到中心点的距离,将该节点与其最近的中心点归为一类; } 更新中心点; } }while(中心点在变化); |
输出:聚类结果 |
这里仅仅给出了程序主流程的代码,其中还调用了其他的函数,这些函数的实现详见附录。这段代码的含义是上述伪代码表示的内容。
void start(){
do{
/*
* 每次循环都需要将原来的链表清空
*/
classes = new ArrayList[k];
for(int i=0;i ArrayList classes[i] = numclass; stop[i]=false; } /* * 计算每个节点到每个中心点的距离,找出最小的距离将其加入到该类别, * 更新中心点 * 重复上面两个直到中心点不再变化为止。 */ for(int i =0;i disnums = new double[k]; for(int j=0;j double result = nodes.get(i).CompareNode(centernodes.get(j)); disnums[j]=result; } //加入相应的链表中 int location = minClass(disnums); classes[location].add(nodes.get(i)); } //更新中心点 for(int m=0;m Node newNode = new Node(); newNode = updataCenter(classes[m]); if(centernodes.get(m).CompareNode(newNode)<0.001){ //该中心点是否是收敛的 Stop[m]=true; } //更新中心点 centernodes.set(m, newNode); } }while(sstop() == false); } 运行程序,能对这些点进行分类,证明我们的思路是正确的。当然,本文中的例子只是实现该算法的一种形式,读者可自行开发实现它。单机版的k-means算法时间复杂度是 。表示聚类个数,表示数据集的大小,表示计算聚类的时间复杂度,表示迭代次数。可以看出k-means算法与数据量n有很大的关系,当n增加时,时间复杂度增加。 我们知道,k-means算法的时间复杂度和数据集大小相关。如何给k-means提速呢?一种办法是,将任务分发给不同的计算机,每个计算机只需完成自己的那一部分。总所周知,hadoop最大的优点是并行化处理。因此hadoop可以看成K-means的加速器。那么k-means中哪些步骤是可以并行化的呢,该如何实现并行化呢?在计算每个点到中心点的距离时可以在每台计算机上进行,只要我们知道目前该集群中的中心点,而在集群中,这些点可以依文件的形式存放在固定的IP地址上,这是很容易实现的。 我们将数据记录拆分给不同的datanode,每个datanode运行一个进程(taskTracker)来处理这些数据。每个进程都应该能读取到当前的聚类中心点的值,而且每个进程都应该能计算当前中心点的值,并将它贡献给当前集群。当前聚类的中心点是所有集群上的中心点,也就是说保持了数据的一致性。 为了实现上面的内容,我们设计一个共享文件,这个文件包含下面信息:迭代器数目、聚类id、中心点的值和已经分配的点的个数。我们将基于hadoop的K-means算法分成下面几个步骤。 步骤一:创建一个上面所述的共享文件。并将其上传到HDFS文件系统中,这个文件对集群中任意节点都是能够访问的。 步骤二:Map类设计。Map类的任务是k-means算法中的第二步,计算每个数据到每个中心点的距离,并将其归类到所属的簇中。既然我们的目标很明确,朝着这个方向努力就行了。要想求每个数据到每个中心点的距离就一定要知道每个中心点的值,还要知道每个数据。我们是将存放中心点的文件放到了HDFS上,很自然的想到在setup()函数中读出文件内容即可。而在MapReduce的设计中Map()函数就是逐条读入数据,因此我们在Map()函数中计算每个数据到每个中心点的距离。由于在Reduce类中需要计算新的中心点的值,我们需要将当前每个数据的所属簇Id记录下来。因此Map输出的 步骤三:Combine类设计。在设计Combine类之前先回忆一下Combine的作用,Combine是MapReduce分布式程序的提高性能的最有效地手段。影响MapReduce程序效率的除了计算资源还有网络资源,在Map程序做完将结果通过网络传给Reduce程序时,网络负载过重会增加网络时延,降低计算效率。Combine是在本地先做一次Reduce,相当于给网络传输的东西做了瘦身,大大降低了网络负载。既然Reduce的任务是对中心点进行更新,就需要对相同cluster Id的数据求均值。为了降低网络负载,Combine函数的目的是在本地计算每个簇的平均值。为了Reduce程序能计算均值,Combine还需要将该簇Id的数据集合的个数传递过去,Reduce就像是求加权平均数,必须知道每个datanode上的每个cluster的个数。它输出的 步骤四:Reduce类设计。Reduce类的目的就是更新中心点,也就是求均值。由于经过Combine传递过来的数据是每个datanode上的每个cluster Id对于的均值,做加权平均数即可。它输出的 我们举个例子讲解一下基于Hadoop的K-means算法的计算过程。数据和原来的数据一样。 表三、数据说明 x y z A 0.3 0.2 0.4 B 0.4 0.2 0.4 C 0.5 0.2 0.4 D 5.0 5.2 5.4 E 6.0 5.2 6.4 F 4.0 5.2 6.4 G 10.3 10.4 10.5 H 0.3 0.3 0.5 I 10.4 10.4 10.5 现在我们需要将它分成三类,也就是k=3,这三类的聚类类别id分别是cluster0,cluster1,cluster2.我们假设选取了(0.3,0.2,0.4)是cluster0的初始中心点;(5.0,5.2,5.4)是cluster1的聚类初始中心;(10.3,10.4,10.5)是cluster2的初始聚类中心。现在假设我们有3个进程,每个进程分配3条记录,进程1的记录分别是,进程2的记录分别是,进程3的记录是 刚开始,聚类文件应该是这样的。 Iteration No 0 Cluster Id Cluster Coordinates No.of Records Assigned 0 (0.3,0.2,0.4) 0 1 (5.0,5.2,5.4) 0 2 (10.3,10.4,10.5) 0 表四、K-means聚类的共享初始文件 第一次迭代的Map阶段输出结果 表五、进程0的Map阶段输出结果 Cluster Id Coordinate of records 0 A(0.3,0.2,0.4) 1 D(5.0,5.2,5.4) 2 G(10.3,10.4,10.5) 表六、进程1的Map阶段输出结果 Cluster Id Coordinate of records 0 B(0.4,0.2,0.4) 1 E(6.0,5.2,6.4) 0 H(0.3,0.3,0.5) 表七、进程2的Map阶段输出结果 Cluster Id Coordinate of records 0 C(0.5,0.2,0.4) 1 F(4.0,5.2,6.4) 2 I(10.4,10.4,10.5) 第一次迭代的Combine阶段输出结果是 表八、进程0的Combine阶段输出结果 Cluster Id No.of records Average of coordinates 0 1 (0.3,0.2,0.4) 1 1 (5.0,5.2,5.4) 2 1 (10.3,10.4,10.5) 表九、进程1的Combine阶段输出结果 Cluster Id No.of records Average of coordinates 0 2 (0.35,0.25,0.45) 1 1 (6.0,5.2,6.4) 表十、进程2的Combine阶段输出结果 Cluster Id No.of records Average of coordinates 0 1 (0.5,0.2,0.4) 1 1 (4.0,5.2,6.4) 2 1 (10.4,10.4,10.5) 在Reduce阶段计算中心点的值,下表是Cluster Id 为0的中心点的值的计算过程。 表十一、Reduce阶段计算中心点的辅助表 Processor Number Cluster Id No.of records Average of coordinates 0 0 1 (0.3,0.2,0.4) 1 0 2 (0.35,0.25,0.45) 2 0 1 (0.5,0.2,0.4) 计算得到下面的中心点。 同理计算Cluster Id为1,2的中心点。 表十二、计算Cluster Id为1的中心点的值 Processor Number Cluster Id No.of records Average of coordinates 0 1 1 (5.0,5.2,5.4) 1 1 1 (6.0,5.2,6.4) 2 1 1 (4.0,5.2,6.4) 表十三、计算Cluster Id为1的中心点的值 Processor Number Cluster Id No.of records Average of coordinates 0 2 1 (10.3,10.4,10.5) 2 2 1 (10.4,10.4,10.5) 第一次迭代后reduce程序的输出结果如下表。 Iteration No 1 Cluster Id Cluster Coordinates No.of Records Assigned 0 (0.375,0.225,0.425) 4 1 (5.0,5.2,6.07) 3 2 2 表十四、Reduce程序输出结果 第一次迭代结束,相信读者对MapReduce的过程有了了解,请自行写出第二次迭代过程的变化。当第次和第次迭代聚类结果不再发生变化时,MapReduce程序停止,得出聚类结果。 我们这里设计五个类,其中Node类是辅助数据结构,帮助处理特性向量之间的关系;KMeansMapper类继承了Mapper类,主要实现中心点文件的读入和将每个节点分配到相应的簇中;KMeansCombine类继承了Reduce类,主要的目的是减少网络负载;KMeansReducer类继承了Reducer类,它所作的事情就是更新中心点;KMeansDriver类是程序入口。 图三 基于Hadoop的K-means算法的类图 在hadoop上运行上述程序,注意该程序输入文件有两个,一个是共享文件,更外一个是待聚类数据文件。显然MapReduce程序比单机程序运行速度快,能够处理的数据量也更大。我们一直在强调MapReduce程序的在运行方便的优势,那么在编写单机程序和分布式MapReduce程序是有什么不同呢?我认为有下面几点需要考虑。 1)在设计MapReduce程序时需要考虑哪些部分是可以并行化处理的,哪些是串行化处理的。怎么才能实现并行化。而在单机程序中这些是不用考虑的。 2)充分发挥MapReduce程序的设计理念,比如在Reduce阶段会将key相同的合并,因此key和value的设计非常重要。 3)对辅助数据结构的设计也有些不同,学会利用MapReduce中的输入输出流。 K-means简单而且易于理解,但是它也有自己的缺点。 l 选取初始节点的方法不确定,比较常见的一种方式是随机选择K个点作为中心点。聚类结果很大程度上依赖初始节点的选取,所以聚类结果的好坏随机性较大,目前的解决方案是选取几组不同的值。 l 在聚类过程中,可能发生接近某一中心点的点集为空,那么该中心点的值不能更新。这种情况我们应该及时处理,但是由于底层操作对我们来说是透明的,我们往往不知道是否发生了这种情况,造成我们忽略了它的存在。 l 距离计算公式的选取也是影响聚类好坏的关键。 l 结果同样依赖K,而一般情况下,我们并不知道应该分成几类,现在是使用Canopy对聚类进行粗粒度处理,从而减少误差。 l 噪声数据对其的影响较大。假设一个点严重偏离各个聚类,加入该点是某个聚类的中心点发生了特别大的变化,造成聚类结果偏离正常值。 正是因为K-means算法的这些缺点,还有从海量数据挖掘的应用场景来看,我们还希望聚类算法有这样的能力: l 处理不同类型属性的能力。对一个事物来说,描述这个事物各个属性的类型可能不同,比如数值类型、boolean类型、分类类型等等。这时在采用原来的方法显得太牵强。 l 可扩展的能力。可能该算法在小数据集上时有效地,但是当数据量增大时,我们希望程序还是适用的。 l 处理高维度数据的能力。维度高的数据带来的问题就是计算量的增加。并且维度高是可能这个数据集是稀疏的、高度倾斜的。 l 发现任意簇的能力。本章中我们介绍的算法都是空间上的聚类的相似程度,但是在海量数据中存在的簇是任意形状的,也就是说,其他类型的相似度。我们希望发现不同的簇。 l 降低数据噪声影响的能力。数据集中难免包括缺失值、错误值等噪声数据。一方面我们希望程序是健壮的,另一方面,我们还希望能找出这些异常值,尤其在商业欺诈中尤为重要。 在对K-means算法进行改进也是围绕K-means算法的缺点来进行的。K-means算法的改进往往以下面四个方面进行: (1)改进K值的选择,我们知道K-means算法中K的选择至关重要,但是在K-means算法中K值的选择是随机的。现在有的算法是通过类的自动合并和分裂,得到较为合理的类别数目K,在进行K-means算法。在Mahout中就是采用这种方案,在使用K-means算法前,先使用Canopy算法求出适合的K值。还有的算法是利用统计学的知识,对K的取值进行F检验,从而得到合理的K值。 (2)改进初始中心点的选择,如果恰好我们选择的K个中心在一个极小的范围内,不仅仅会增加迭代的次数,还会只能达到局部最优解而不能达到全局最优解。现在有的算法根据最小生成树来选取K个聚类中心,从而避免上述的问题。 (3)减少每次迭代进行的计算,在K-means算法中每次产生新的中心点以后都要重新计算每个数据到每个聚类中心的距离。如果去掉不再可能发生变化的数据点的计算,将大大降低时间复杂度。 (4)对噪声数据进行预处理。至于如何进行数据清洗和预处理有专门的理论,这里就不再赘述。 在mahout中我们先看看官网上的例子。网址是http://mahout.apache.org/users/clustering/k-means-clustering.html。 Shell脚本的提纲,所谓的shell脚本是mahout中shell的脚本,我们可以查看它的源码 我们只要了解mahout的目录就行,聪明的你肯定很快就能找到上面运行的kmaens源码在哪里。 首先我们对K-means算法做简介。它是一种最常见的聚类算法,将输入的n个数据分成k类。算法思路是:先任意选择K个中心将其作为簇的中心,在依次将其他的求出算法步骤是: 算法的输入是类别k和收敛阀值。 1)选择k个中心点。 2)对任意一个样本,求其到k个中心点的距离,选择最近的一个将其归为一类。 3)更新中心值。一般使用新的簇的均值。 4)回到(2)继续,直到中心值收敛到设置的阀值。 从算法的描述上可以看出这种方法比较简单粗暴,它的优点是简洁快速,同时它的缺点也是显而易见的,分类的效果取决于K的选择和距离公式。在很多情况下我们是不知道分为几类是最合适的,因此往往需要设置不同的K来验证,十分不便。下面我们介绍另一种算法,它解决了K值这个问题。 Canopy算法是一种非常简单、快速但是不太精确的聚类算法。所有的对象都被看成多维空间中的一个点。这个算法使用一个快速计算的不太准确地距离计算公式和两个距离值T1,T2,其中T1>T2。算法的原来是首先将数据当成一个数据集合Set并从中随即取出一个数据。创造一个包含这个点的Canopy,对List中剩余的数据进行迭代。每个点,如果它到第一个点的距离小于T1,将这个点加入这个簇里面;相反,如果距离小于 理解完kmeans算法以后继续读源码吧。首先找到Job类。 对它就是/example/src/main/java/org/apache/mahout/clustering/syntheticcontrol/kmeans/job.java 代码太长了,这里我们只看核心代码块。 从Log信息可以看出,代码分为四个步骤: (1)准备输入数据 (2)运行Canopy得到初始聚类簇 (3)运行KMeans算法距离 (4)输出结果 由于我们关心的是KMeans算法,重点看KMeansDriver.run();9个参数分别是: (1)Configuration (2)Clusters Path 类型是Path (3)InputPath 类型是Path (4)OutPath 类型是Path (5)convergenceDelta double类型 (6)maxIterations, 最大迭代次数,整型。 (7) true, (8) 0.0, (9) False 你肯定要问k在哪里呢?(我感觉是因为使用了Canopy所以不需要k值了。) public static void run(Configuration conf, Path input, Path output, DistanceMeasure measure, double t1, double t2, double convergenceDelta, int maxIterations) throws Exception { Path directoryContainingConvertedInput = new Path(output, DIRECTORY_CONTAINING_CONVERTED_INPUT); log.info("Preparing Input"); InputDriver.runJob(input, directoryContainingConvertedInput, "org.apache.mahout.math.RandomAccessSparseVector"); log.info("Running Canopy to get initial clusters"); Path canopyOutput = new Path(output, "canopies"); CanopyDriver.run(new Configuration(), directoryContainingConvertedInput, canopyOutput, measure, t1, t2, false, 0.0, false); log.info("Running KMeans"); KMeansDriver.run(conf, directoryContainingConvertedInput, new Path(canopyOutput, Cluster.INITIAL_CLUSTERS_DIR + "-final"), output, convergenceDelta, maxIterations, true, 0.0, false); // run ClusterDumper ClusterDumper clusterDumper = new ClusterDumper(new Path(output, "clusters-*-final"), new Path(output, "clusteredPoints")); clusterDumper.printClusters(null); } 继续追踪程序。在KMeansDriver里面实际运行算法的是buildCluster()函数。参数是 (1)Configuration (2)Path (3)Path (4)Path (5)maxIterations (6)Delta (7)runSequential。这个参数的含义是运行的是否是序列文件。所谓序列文件是hadoop中以key/values形式存储的字节流文件。这里我们的数据不是sequencefile因此调用的是 ClusterIterator.iterateMR()方法。 public static Path buildClusters(Configuration conf, Path input, Path clustersIn, Path output, int maxIterations, String delta, boolean runSequential) throws IOException, InterruptedException, ClassNotFoundException { double convergenceDelta = Double.parseDouble(delta); List KMeansUtil.configureWithClusterInfo(conf, clustersIn, clusters); if (clusters.isEmpty()) { throw new IllegalStateException("No input clusters found in " + clustersIn + ". Check your -c argument."); } Path priorClustersPath = new Path(output, Cluster.INITIAL_CLUSTERS_DIR); ClusteringPolicy policy = new KMeansClusteringPolicy(convergenceDelta); ClusterClassifier prior = new ClusterClassifier(clusters, policy); prior.writeToSeqFiles(priorClustersPath); if (runSequential) { ClusterIterator.iterateSeq(conf, input, priorClustersPath, output, maxIterations); } else { ClusterIterator.iterateMR(conf, input, priorClustersPath, output, maxIterations); } return output; } 找到ClusterIterator类找到iterateMR()函数。参数 Configuration Path Path Path numIterations 迭代次数。 大家看到这段代码肯定是相当熟悉,在mapreduce程序中,这是驱动程序段,Mapper类是CIMapper。Reducer类是CIReducer。只要找到这两个类我们就知道如何使用MapReduce程序来实现KMeans算法。 public static void iterateMR(Configuration conf, Path inPath, Path priorPath, Path outPath, int numIterations) throws IOException, InterruptedException, ClassNotFoundException { ClusteringPolicy policy = ClusterClassifier.readPolicy(priorPath); Path clustersOut = null; int iteration = 1; while (iteration <= numIterations) { conf.set(PRIOR_PATH_KEY, priorPath.toString()); String jobName = "Cluster Iterator running iteration " + iteration + " over priorPath: " + priorPath; Job job = new Job(conf, jobName); job.setMapOutputKeyClass(IntWritable.class); job.setMapOutputValueClass(ClusterWritable.class); job.setOutputKeyClass(IntWritable.class); job.setOutputValueClass(ClusterWritable.class); job.setInputFormatClass(SequenceFileInputFormat.class); job.setOutputFormatClass(SequenceFileOutputFormat.class); job.setMapperClass(CIMapper.class); job.setReducerClass(CIReducer.class); FileInputFormat.addInputPath(job, inPath); clustersOut = new Path(outPath, Cluster.CLUSTERS_DIR + iteration); priorPath = clustersOut; FileOutputFormat.setOutputPath(job, clustersOut); job.setJarByClass(ClusterIterator.class); if (!job.waitForCompletion(true)) { throw new InterruptedException("Cluster Iteration " + iteration + " failed processing " + priorPath); } 根据MapReduce程序设计的一般步骤来解析。 分析数据格式 Mapper。首先读入数据,从上一次的MapReduce中读取聚类中心。然后确定计算聚类中心。将类写给reducer Reducer @Override protected void setup(Context context) throws IOException, InterruptedException { Configuration conf = context.getConfiguration(); String priorClustersPath = conf.get(ClusterIterator.PRIOR_PATH_KEY); classifier = new ClusterClassifier(); classifier.readFromSeqFiles(conf, new Path(priorClustersPath)); policy = classifier.getPolicy(); policy.update(classifier); super.setup(context); } @Override protected void map(WritableComparable> key, VectorWritable value, Context context) throws IOException, InterruptedException { Vector probabilities = classifier.classify(value.get()); Vector selections = policy.select(probabilities); for (Element el : selections.nonZeroes()) { classifier.train(el.index(), value.get(), el.get()); } } @Override protected void cleanup(Context context) throws IOException, InterruptedException { List ClusterWritable cw = new ClusterWritable(); for (int index = 0; index < clusters.size(); index++) { cw.setValue(clusters.get(index)); context.write(new IntWritable(index), cw); } super.cleanup(context); } @Override protected void reduce(IntWritable key, Iterable InterruptedException { Iterator Cluster first = iter.next().getValue(); // there must always be at least one while (iter.hasNext()) { Cluster cluster = iter.next().getValue(); first.observe(cluster); } List models.add(first); classifier = new ClusterClassifier(models, policy); classifier.close(); context.write(key, new ClusterWritable(first)); } @Override protected void setup(Context context) throws IOException, InterruptedException { Configuration conf = context.getConfiguration(); String priorClustersPath = conf.get(ClusterIterator.PRIOR_PATH_KEY); classifier = new ClusterClassifier(); classifier.readFromSeqFiles(conf, new Path(priorClustersPath)); policy = classifier.getPolicy(); policy.update(classifier); super.setup(context); } 在划分聚类中,形成的是一个个簇,在层次聚类中按数据分层建立簇,形成一棵以簇为结点的树,成为聚类图。如果按自底向上层分解,则称为凝聚的层次聚类;如果自上问下层次分解,就称为分裂的层次聚类。 凝聚的层次聚类采用自底向上的策略,开始时把每个对象作为一个单独的簇,然后逐次对各个簇进行适当合并,直到满足某个终止条件。 分裂的层次聚类采用自顶向下的策略,与凝聚的层次聚类相反,开始时将所有对象置于同一个簇中,然后逐次将簇分裂成更小的簇,直到满足某个条件为止。 (1)在算法初始阶段,每个数据都被看成一个簇。 (2)如果两个簇之间的距离最短,就将它们合并成一个新的簇。 (3)更新这个新的簇和其他簇之间的距离。往往是用距离矩阵表示的。 (4)重复(2)(3),直到所有的数据归到一个簇中。 在算法步骤二中,我们所说的簇之间的距离有这么几种计算方法。 (1)最小距离(但链接方法): (2)最大距离(完全链接方法): (3)平均聚类(平均链接方法): (4)均值距离(质心方法): 其中,为对象和的距离,是簇的平均值,是簇中对象的个数。 对象间的距离计算方法和前面介绍的一样,有欧式距离、曼哈顿距离、闵可夫斯基距离、马氏距离等。 在算法的步骤三中,计算每个簇到另外一个簇之间的距离,需要计算数据集中每个点到另外一个点的距离,这样它的时间复杂度就是。 以分解型层次聚类算法为例,其主要的思路是,在开始的时候,将每个对象归为一类,然后不断迭代,直到所有对象合并成一个大类(或者达到某个终止条件);在每轮迭代时,需要计算两两对象间的距离,合并距离最近的两个对象为一类。该算法需要计算两两对象间的距离,也就是说每个对象和其他对象均有关联,因而该问题不能被分解成若干个子问题,进而不能使用MapReduce解决。 KNN算法有样本和测试集,K-means算法没有样本和测试集 K-means算法的输入参数是K;然后将n个对象划分为K个聚类以便是的所获取的聚类满足:同一聚类中的对象相似度较高。 KNN算法的思路是,如果一个样本在特征空间中的k个最相近(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本就属于这个类别。KNN中所选择的邻居都是已经正确分类的对象。该方法虽然从原理上也依赖极限定理,但是在类别决策时,只与极小量的相邻样本有关。由于KNN方法主要依赖周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。 KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成正比。 该算法在分类时有个主要的不足是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。因此可以采用权值的方法(和该样本距离小的邻居权值大)来改进。该方法的另一个不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分。 KNN物以类聚人以群分 积极学习法和消极学习法 所谓积极学习法就是,先根据训练集构造出分类模型,根据分类模型对测试集进行分类。 消极学习法就是不建立学习模型。当给定训练元祖的时候,简单地存储训练数据,一直等到给定的一个测试元组。 消极学习法在提供训练元祖时只做少量工作,而在分类或者预测时做更多的工作,KNN就是一种简单的消极学习分类方法,它开始并不建立模型,而是对于给定的训练实例点和输入实例点,基于给定的邻居度量方式以及结合经验选取合适的K值,计算并且查找出给定输入实例点的K个最近邻训练实例点,然后基于某种给定的策略,利用着K个训练实例点的类来预测输入点实例点的类别 了解了KNN的核心思想,才能进行 KNN算法的伪代码 KNN算法的JAVA实现 KNN算法的MR实现 KNN算法的Hadoop分布式设计还是比较简单的。 KNN算法的正确度。最邻近算法的错误率是高于贝叶斯错误率的, KNN是一个典型的非参数法,是一个非常简单的、性能优秀的分类算法,KNN的改进也使得KNN的应用越来越广泛。 KNN算法需要解决的几个问题。第一,如何度量邻居之间的相识度。度量相识度的算法有我们之前使用的几种距离公式,如欧式距离、曼哈顿距离等,不知道大家有没有疑惑,笔者在学习的过程中常常这么想,数值距离很容易计算,但是如果是离散型的变量,比如高层、中层、底层等,该怎么计算呢? 第二个问题是,几个邻居才合适,根据经验来选择,经验来自于哪里,调参数。如果K选大的话,可能求出来的K最近邻集合可能包含了太多隶属于其他类别的样本点,最极端的就是K取训练的集合太小,此时无论输入实例是什么,都是简单的预测它属于在实训实例中最多的类,模型过于简单,忽略了训练实例中大量有用的信息。如果K选小的话,结果对噪声样本很敏感。交叉检验http://wenku.baidu.com/view/d8268490daef5ef7ba0d3cdd.html KNN算法的优缺点 缺点是需要存储全部训练样本,以及繁重的距离计算量 一种是对样本集进行组织和整理,分群分层、尽最大可能将计算压缩到在接近测试样本领域的小范围内,避免盲目的与训练样本集中每个样本进行距离计算。 另一种则是从原来的样本集中挑选出对分类计算有效的样本,使得样本总数合理的减少,以同时达到减少计算量,又减少存储量的双重效果。 快速搜索紧邻法 从最邻近算法到k-邻近算法 最邻近算法的定义,为了判定未知样本的类别,以全部的样本作为代表点,计算未知样本和每个样本的距离,将它和最近的样本分成一类。 显然,最邻近算法的缺点是对噪声数据过于敏感,为了解决这个问题,我们可以将位置样本周围的多个最近样本计算在内,扩大参与决策的样本量,避免个别数据直接决定决策结果。 K-最邻近算法是最邻近算法的一种改进,基本的思想是,选择未知样本一定范围内的确定的k个样本,该k个样本大多数属于某一类,那么未知样本就归为哪一类。还有一个问题,比如K=3,这三个最近邻居都各自属于不同的类型,该如何处理呢。这里可以取最近邻居。或者,k的大小要大于分类类别。实验表明,K的值越大,准确率越高。当然K值的增加导致了计算量的增大。所以在实际的应用中,我们应该做一下折中。 为了让读者更好的理解k-means算法,这里我们先用java实现一下。数据如下格式:每行数代表某个点的n为坐标向量,对这几个点进行聚类。它们被保存在文件中,文件中每行数字以’\t’隔开。在这里可以看出是空间坐标,三个数字分别表示(x,y,z)。要求将这些点分成三类,并将每一类包含的数据集合依次输出。 输入数据data.txt 0.3 0.2 0.4 0.4 0.2 0.4 0.5 0.2 0.4 5.0 5.2 5.4 6.0 5.2 6.4 4.0 5.2 4.4 10.3 10.4 10.5 10.3 10.4 10.5 10.3 10.5 10.6 由于这些点在存储已经计算它们之间的距离是比较麻烦,我们将它定义为一个类,叫做Node.它包括x,y,z三个成员变量以及两个构造函数分别是有参数和无参数的。为了方便计算两个点之间的距离,我们还定义了一个compareNode(Node node)函数,这里使用的距离公式是欧几里得距离公式。该类的定义如下。 public class Node { private double x; private double y; private double z; Node(double x,double y,double z){ this.x = x; this.y = y; this.z = z; } double CompareNode(Node node){ double result = (this.x -node.getX())*(this.x -node.getX()) +(this.y -node.getY())*(this.y -node.getY())+ (this.z -node.getZ())*(this.z -node.getZ()); result = Math.sqrt(result); return result; } } K-means算法是完全按照上述算法流程实现的。下面是算法的伪代码和关键程序。由于篇幅原理,不能列出全部源码。首先将全部的数据读入到节点链表(nodes)中,再构建一个类别链表数组(classes),它们存放每个类别的节点;为了找到该节点属于某个类别,需要将该节点与每个节点的距离保存在double数组(disnums)中;为了判断迭代是否结束,在更新中心点的同时,计算新中心点和原来中心点的距离,小于某个阀值时该值是收敛的,视为该中心点不再变化。但是要想停止迭代需要所有的中心点都不能变化。所以使用布尔数值stop来表示每个中心点是否收敛。 * 参数说明 * nodes 从文件中读入的节点放在链表中. * classes 是一个链表数组,存放的是每个类别中的节点 * centernodes 存放中心节点的数组 * disnums存放每个节点与中心点的距离差,以便找到最小的那个 * stop 布尔数组,表示每个中心点是否停止迭代 * k 分组的个数 给出关键代码的伪代码如下表。 表二、K-means伪代码 输入:文件 初始化全部节点; 初始化中心点; do{ for(第一个节点->最后一个节点){ for(第一个中心点->最后一个中心点){ 计算节点到中心点的距离,将该节点与其最近的中心点归为一类; } 更新中心点; } }while(中心点在变化); 输出:聚类结果 这里仅仅给出了程序主流程的代码,其中还调用了其他的函数,这些函数的实现详见附录。这段代码的含义是上述伪代码表示的内容。 void start(){ do{ /* * 每次循环都需要将原来的链表清空 */ classes = new ArrayList[k]; for(int i=0;i ArrayList classes[i] = numclass; stop[i]=false; } /* * 计算每个节点到每个中心点的距离,找出最小的距离将其加入到该类别, * 更新中心点 * 重复上面两个直到中心点不再变化为止。 */ for(int i =0;i disnums = new double[k]; for(int j=0;j double result = nodes.get(i).CompareNode(centernodes.get(j)); disnums[j]=result; } //加入相应的链表中 int location = minClass(disnums); classes[location].add(nodes.get(i)); } //更新中心点 for(int m=0;m Node newNode = new Node(); newNode = updataCenter(classes[m]); if(centernodes.get(m).CompareNode(newNode)<0.001){ //该中心点是否是收敛的 Stop[m]=true; } //更新中心点 centernodes.set(m, newNode); } }while(sstop() == false); } 运行程序,能对这些点进行分类,证明我们的思路是正确的。当然,本文中的例子只是实现该算法的一种形式,读者可自行开发实现它。单机版的k-means算法时间复杂度是 。表示聚类个数,表示数据集的大小,表示计算聚类的时间复杂度,表示迭代次数。可以看出k-means算法与数据量n有很大的关系,当n增加时,时间复杂度增加。 在数据挖掘中常常用到关联规则模型。关联规则挖掘的主要目的是找出数据集中的频繁模式(Frequent Pattern),即多次重复出现的模式和并发关系(Cooccurrence Relationship),即同时出现的关系,频繁和并发关系也称作关联。 这样说来,关联规则的模型比较抽象,举个最经典的案例。在电子商务中,往往通过分析顾客购物篮中的商品之间的关系,来挖掘顾客的消费习惯,从而来提高商品的成交量。 尿布—>啤酒【支持度=10%,置信度=65%】 这个规则表明:在所有的顾客中有10%的人同时买了尿布和啤酒,而在所有购买尿布的顾客中有65%的还购买了啤酒。因此,尿布和啤酒之间有关联。如果在销售过程中,将尿布和啤酒放在一起促销,将会提高营业额。 从上面的案例中可以看出,支持度和置信度是衡量关联规则强度的两个重要指标。那什么是规则?什么是关联规则呢?如何来度量一个关联规则的强度呢?规则可以理解为用户的行为。关联规则形如"如果…那么…(If…Then…)",前者为条件,后者为结果。例如一个顾客,如果买了面包,那么他也会购买牛奶。支持度和置信度是衡量关联规则强度的两个重要指标,它们分别反映所发现规则的有用性和确定性。 根据下面的例子来了解支持度和置信度的概念。 根据顾客购物清单判断橙汁和可乐之间是否存在关联规则。 支持度(Support)表示的是在所有事物集合中两件事情同时发生的概率。即Support(X->Y)=P(AB)。在上图中,同时购买橙汁和可乐的人有两条记录。因此,这条关联规则的支持度是2/5=40%。在商业实践中,只有大概率事件有商业价值。支持度太小,表示此规则代表的事件是偶然事件,是没有商业价值的。因此主要用来衡量规则的有用性。 置信度(Confidence)表示的是在当前事件发生的情况下,另一件事情发生的概率。即Confidence(X->Y)=P(Y|X)。在上图中,在购买了橙汁的记录有4条,购买了橙汁又同时购买了可乐的记录有2条。因此,这条关联规则的置信度是2/4=50%。在商业实践中,置信度太小,说明两个事件之间的关系不是很大,没有商业价值。置信度主要用来衡量规则的确定性(可预测性)。 在上述分析中,给出橙汁和可乐的关联规则: 橙汁->可乐【支持度=40%,置信度=50%】 我们可以认为购买了橙汁的人倾向于购买可乐,能把他们放在一起促销。 通过上面的例子,我们对关联规则有了初步的了解,下面相信介绍一下关联规则中的概念。 定义1项目与项集 设I={i1,i2,…,im}是m个不同项目的集合,每个ik(k=1,2,……,m)称为一个项目(Item)。 项目的集合 I 称为项目集合(Itemset),简称为项集。其元素个数称为项集的长度,长度为k的项集称为k-项集(k-Itemset)。 定义2 交易 每笔交易T(Transaction)是项集I上的一个子集,即TÍI,但通常TÌI。 对应每一个交易有一个唯一的标识——交易号,记作TID 交易的全体构成了交易数据库D,或称交易记录集D,简称交易集D。 交易集D中包含交易的个数记为|D|。 定义3 项集的支持度 对于项集X,XI,设定count(XT)为交易集D中包含X的交易的数量。 项集X的支持度support(X)就是项集X出现的概率,从而描述了X的重要性。 定义4 项集的最小支持度与频繁集 发现关联规则要求项集必须满足的最小支持阈值,称为项集的最小支持度(Minimum Support),记为supmin。 支持度大于或等于supmin的项集称为频繁项集,简称频繁集,反之则称为非频繁集。 通常k-项集如果满足supmin,称为k-频繁集,记作Lk 定义5 关联规则 关联规则(Association Rule)可以表示为一个蕴含式: R:X->Y 其中XI,YI,并且XY=. 例如:R:牛奶→面包 定义6 关联规则的支持度 对于关联规则R:X=>Y,其中XI,YI,并且XY=. 规则R的的支持度(Support)是交易集中同时包含X和Y的交易数与所有交易数之比。 定义7 关联规则的置信度 对于关联规则R:X=>Y,其中XI,YI,并且XY=F。 规则R的置信度(Confidence)是指包含X和Y的交易数与包含X的交易数之比 定义8 关联规则的最小支持度和最小置信度 关联规则的最小支持度也就是衡量频繁集的最小支持度(Minimum Support),记为supmin,它用于衡量规则需要满足的最低重要性。 关联规则的最小置信度(Minimum Confidence)记为confmin,它表示关联规则需要满足的最低可靠性。 定义9 强关联规则 如果规则R:X=>Y满足support(X=>Y)>=supmin且confidence(X=>Y)>=confmin,称关联规则X=>Y为强关联规则,否则称关联规则X=>Y为弱关联规则。 在挖掘关联规则时,产生的关联规则要经过supmin和confmin的衡量,筛选出来的强关联规则才能用于指导商家的决策。 在数据挖掘中关联规则算法有很多,最著名的是Apriori算法,该算法的思路如下: 1)生成所有的频繁项目集。一个频繁项目集合(Frequent Itemset)是一个支持度高于最小支持度阀值的项目集。 2)从频繁项目集中生成所有的可信关联规则。这里的可信关联规则是指置信度大于最小置信度阀值的规则。 先介绍一下求频繁项集的算法。 (1)扫描数据源,生成候选1项集和频繁1项集。 (2)从2项集开始,根据频繁K-1项集求频繁K项集。 在生成频繁K项集时,频繁K-1项集两两组合,判定是否可以连接,若能连接生成K项集。 对产生的候选频繁项集进行减枝,减枝的依据是频繁项集的子集肯定是频繁项集,非频繁项的超集一定是非频繁的。 扫描生成的候选频繁项集,去掉支持度小于最小支持度的候选项集。剩下的就是频繁项集。 循环终止的条件是当前K项集中只有一个项集。 为了更深刻的理解频繁K项集的计算方法,我们举个例子来重现一下它的计算过程。如下事物数据项TID按照字典顺序存放,共有十个事务,设最小支持度阀值为3,即最小支持度是0.3。根据Apriori算法生成频繁项集。 表一 事务数据库表DS TID 项的列表 T1 I1,I2,I5 T2 I2,I4 T3 I1,I2,I3,I5 T4 I1,I3 T5 I2,I3 T6 I1,I2,I3 T7 I2,I5 T8 I3,I4,I5 T9 I1,I3 T10 I1,I2,I5 、 使用Apriori算法挖掘频繁项集的算法的过程如下。 项集 支持度计数 {I1} 6 {I2} 7 {I3} 6 {I5} 5 对于有个项目的项集来说,共有个频繁模式。所以它的计算量是指数增长的。对于上面的例子来说,它的频繁项集有下面这10种。 频繁2项集 {I1,I2} {I1,I3} {I1,I5} {I2,I5} {I2,I3} 频繁3项集 {I1,I2,I5} 频繁1项集 {I1} {I2} {I3} {I5} 输入:数据源DS,最小支持度minSupport 输出:DS中所有的频繁项集L 步骤: (1) = findFP_1_itemsets(DS);//根据数据源找到频繁1项集 (2) (3) (4) (5) } (6) (7) (8) } (9) (10) (11) } (12)} 伪代码写完以后,算法的实现就变得简单的多了。这里需要注意的是,数据结构的选取和组合。存储项集和支持度计数的很明显是Key-value类型的,使用Map数据结构。在项集的排序中是按字典序排列的,而且是无重复的,使用Set数据结构。 根据上面的伪代码,我们大概已经知道了包含哪几个函数,每个函数的功能和参数是什么。画个类图来帮助大家理解一下。 这里的属性有minSupport表示的是最小支持度计数的阀值,它等于最小支持度*项集的个数。dataTrans是数据库事务,也就是项集,它是List 函数的伪代码 输入:文件路径 输出:dataTrans 类型是List 方法: (1)按行读取文件不为空){ (2)每行文件按“ ”分成字符串数值 (3) (4) (5) (6)} (7) 函数思想是扫描List,检查在Map中有没有出现过key为这个字符串,如果有将该key的value加1,如果没有将该string作为key加入Map中,并将它的value置为1。然后遍历Map,谁的value大于等于最小支持度阀值将加入到最终频繁1项集的Map中。的java代码如下: 输入: 输出:频繁1-项集类型是 /* * 第二步: * 生成频繁1项集,原理很简单,每行当成一个Set,整个文件当成一个List * 遍历每一个String,加入到Map中,在加入Map中时,如何ItemCount中还没有这个值置 1,如果有就将它的值加1. * 最后遍历大于minSupport的才是频繁1项集 * @return Map * @param List */ private Map Map Map for(Set for(String d:ds){ if(itemCount.containsKey(d)){ itemCount.put(d, itemCount.get(d)+1); }else{ itemCount.put(d, 1); } } } for(Map.Entry if(ic.getValue() >= minSupport){ result.put(ic.getKey(), ic.getValue()); } } System.out.print(result.toString()); return result; } 函数思想是遍历之前的k-1项集,两两组合生成k-项集。这里先将它的值赋值为0,后来再遍历数据库,对支持度进行计数。它的java代码如下: /* * 第三步: * 由频繁K-1项集生成频繁K项集,在构造K项集时,需要连接两个K-1项集,在连接的时候需要判断能不能连接 * 在连接完以后需要减枝,减枝完以后需要判断是否满足大于minSupport * @param preMap k-1项集它的形式是Map * @return k项集,形式还是一样的 * */ private Map Map //遍历两个k-1项集生成k项集 List for(Map.Entry preSetArray.add(preMapItem.getKey()); } int preSetLength = preSetArray.size(); for(int i=0; i for(int j =i+1; j String[] strA1 = preSetArray.get(i).toArray(new String[0]); String[] strA2 = preSetArray.get(j).toArray(new String[0]); if(isCanLink(strA1,strA2)){ Set for(String str:strA1){ set.add(str); } //连接成K项集,由于set是排序的,只需要在strA1的后面天上strA2的最后一个就行了 set.add((String) strA2[strA2.length - 1]); //需要减枝的话,减枝 if(!isCut(preMap,set)){ result.put(set, 0); } } } } return result; } 其他的函数就不再这里赘述了,详见程序源码。最后编写驱动函数。 public static void main(String[] args) throws IOException{ Apriori_FP apriori = new Apriori_FP(); String srcFile = "D:/workspace/suan/Apriori_guize/data.dat"; String shortFileName = srcFile.split("/")[4]; String targetFile = "D:/workspace/suan/Apriori_guize/" + shortFileName.substring(0, shortFileName.indexOf("."))+"_fp_threshold"; dataTrans = apriori.setDataTrans(srcFile); long totalItem = 0; FileWriter tgFileWriter = new FileWriter(targetFile); apriori.setMinSupport((int)(dataTrans.size() * 0.3)); //频繁1项集 Map //频繁1项集信息得加入支持度 Map for(Map.Entry Set fs.add(f1Item.getKey()); f1Map.put(fs, f1Item.getValue()); } totalItem += apriori.outPutMap(f1Map, tgFileWriter); Map do { result = apriori.getNextKItem(result); result = apriori.assertFP(result); System.out.print(result.toString()); totalItem += apriori.outPutMap(result, tgFileWriter); } while(result.size() != 0); tgFileWriter.close(); System.out.println("共有" + totalItem + "项频繁模式"); } 所有的工作都完成了,程序运行即可。运行结果为 {I3=6, I1=6, I2=7, I5=5}{[I1, I5]=3, [I2, I5]=4, [I1, I2]=4, [I1, I3]=4, [I2, I3]=3}{[I1, I2, I5]=3}{}共有10项频繁模式。这个结果跟我们的计算是一样的。 这个程序使用的数据量非常小,所有很快的运行出结果。读者朋友们可以对程序稍加修改,打印出程序的运行时间,然后不断增加项集中项目的个数,观察程序的运行时间。我们会发现程序的运行时间呈指数形式增长。Apriori 算法的出现在很大程度上提高了关联规则挖掘的效率,连接、剪枝策略可以在很大程度上降低候选项集的规模,但是对于较大规模的数据并且频繁项较多以及设置的支持度偏小的时候,该算法的效率明显降低。Apriori算法的缺点主要体现以下几个方面。 第一、频繁项集的产生涉及的计算量非常巨大,由频繁项集自连接生成候选项集层现指数增长,虽然在Apriori算法中候选项集的产生使用了相关的修剪方法,但是的长度仍然很大(特别是)。 第二、由于Apriori算法基于逐层搜索的思想,每求得一个候选k-项集后,必须再次扫描数据库以确定哪些项是频繁的,但是这种方法增加了I/O负担,且非常耗时。 第三、不能直接用于关系数据库的关系规则挖掘。不能直接处理数值型数据,该模型描述的是项目之间的频繁关系,各项目的地位是平等的,事实上不是这样的。 在上面的分析中,得出频繁3项集是,我们可以的出的关联规则有哪些呢?的非空真子集有,能得出的生成规则表见下表。 表二、生成规则表 规则 置信度 假设最小置信度是60%,那么强关联规则是,,,。这样做的结果是推荐更加准确,举个例子来说,买了手机的人,很可能买手机套,但是有人买了手机套,你给他推荐手机就不明智了。 表 频繁3-项集生成的强关联规则 规则 置信度 下面请读者朋友们自己计算出频繁2项集的生成规则表,与本书对照。频繁2-项集有 表 频繁2-项集的生成规则表 规则 置信度 去掉小于最小置信度的值,得出强关联规则表是 规则 置信度 将表 和表 合并得出强关联规则。 输入: 输出:形如的关联规则集 步骤: (1)for all { (2) (3) (4)} Procedure AP_GenRule(XK, K 是频繁项集,Hm:规则右边项的集合) (1)if (K>m+1) (2) for all (3) (4) (5) 输出规则: 支持度为置信度为conf (6) }else{ (7) Delete from ;} (8) } (9) 调用函数 (10) } 从事物数据库D中挖掘出频繁项集后,就可能比较容易的获取相应的关联规则,即满足置信度大于最小置信度的频繁项集产生强关联规则。由于规则是由频繁项集产生,所以每个规则自动满足最小支持度。代码请自行实现。 运用Apriori算法单机处理海量的数据时将耗费大量的时间和内存时间,这成为了Apriori算法的瓶颈。 以表的内容为例,讲诉算法的计算过程。这里需要提前声明的是局部最小支持度阀值为,也就是最小支持度*分块中的事物数。 假设最小的支持度阀值为3,即,共有三个进程来处理这些数据,其中进程1上面的数据如表,且它的最小支持度阀值是0.3*3=0.9 TID 项的列表 T1 I1,I2,I5 T2 I2,I4 T3 I1,I2,I3,I5 分发到进程2上的数见表 ,它的最小支持度的阀值也是0.9 TID 项的列表 T4 I1,I3 T5 I2,I3 T6 I1,I2,I3 同样,分发到进程3上的数据是:它的最小支持度的阀值是1.2 TID 项的列表 T7 I2,I5 T8 I3,I4,I5 T9 I1,I3 T10 I1,I2,I5 在Map阶段运行完以后,三个进程输出的结果分别是 combiner节点 通过对比局部的最小支持度阀值,得到3个局部频繁1-项集 进程1: 进程2: 进程3: 在局部使用传统的Apriori算法产生局部频繁3-项集 进程1: 进程2: 进程3:没有频繁3项集 Reduce(itemsetsk,list(supk))对局部的频繁3-项集的支持度计数进行累加。Reduce的输出结果见下表: 这里使用总的支持度的阀值来做比较,总的阀值时3,没有得到频繁项集,这肯定是不正确的。很明显的看出,这样做的坏处是容易丢失频繁项集,解决方案是将每个节点上的最小支持度的阀值设置的小一些。这个标准如何控制呢,目前笔者还没有想到有效的方法,可以采用不同的值进行测试吧。本书的程序采用80%的最小支持度。那么进程3的频繁3项集是,得出来的频繁3项集是。这样调整的话,说明我们的算法是有效的。 2,生成规则的过程 假设两条记录发送到2个Map节点,且将记录解析成形式。 Map函数对上述键值进行处理,输出<频繁项,规则>对。 工作节点1: 关联规则Apriori算法的实现主要有两个步骤:频繁项集的产生和规则的产生。其中频繁项集的产生比较复杂, 在MapReduce编程模式中,计算的输入和输出的结果的输出都是以键值对的形式存在。在上面的分析中,基于MapReduce的Apriori算法求频繁项集需要4个函数,Map 函数,Reduce函数,combiner函数,和Main函数 键值对的设计 其中key实现了WritableComparable接口,value实现了Writable接口,使得框架可以对其序列化并能够对Key执行排序。 算法分析:基于MapReduce的Apriori的算法与单机版有如下不同: (1)扫描事物数据库的次数减少,挖掘频繁项集只需2次扫描数据库就行了,减低了时间复杂度和空间复杂度。 (2)频繁项集的查找过程可以高度并行执行,通常情况下,Map的数量仅仅取决于输入文件的大小。 (3)MapReduce使得产生规则的算法比产生频繁项集时更为简单,它不需要循环调用Map类和Reduce类,只需要调用一次MapReduce过程即可。 关联规则的重点是求频繁项集。在第K次迭代是,它计算 第一步:使用MapReduce找到频繁1项集 第二步:设置k=1 第三步:如果找不到频繁K+1项集,转到第六步 第四步:根据频繁K项集找出K+1项集 第五步:如果K小于最大的迭代次数,k++,转到第三步。否则转到第六步 第六步:根据频繁项集L,总结关联规则。 正如上面介绍的,Map函数 如何评判是不是频繁项集呢,根据自信度和支持度。那么如何计算 假设三个机器上的三个进程之间的数据。 Apriori算法需要生成频繁项集的候选集,造成算法的时间复杂和空间复杂度都很大。针对这个问题,J.llan等人提出了不产生候选频繁项集的方法,该算法根据数据库构造频繁模式树,再通过树生成关联规则。 FP-tree算法也需要先设定最小的支持度阀值,根据此阀值进行频繁项集的筛选。它的基本思想是: (a) 遍历一次数据库,导出频繁1项集和支持度计数,并以降序排序。 (b) 构造FP-tree (c) 根据上一步得到的FP-tree,为1项集中的每一项构造条件FP-tree。 (d) 得到频繁项集 FP-tree算法的计算过程。我们还使用上面章节的Apriori数据。最小支持度阀值仍然是3. 表一 事务数据库表DS TID 项的列表 T1 I1,I2,I5 T2 I2,I4 T3 I1,I2,I3,I5 T4 I1,I3 T5 I2,I3 T6 I1,I2,I3 T7 I2,I5 T8 I3,I4,I5 T9 I1,I3 T10 I1,I2,I5 第一步:扫描事务数据库,找出频繁1项集,并降序排列。 然后对于每一条购买记录,按照频繁1项集的顺序重新排列。这是最后一次扫描数据库。 TID 项的列表 重新排列的项集 T1 I1,I2,I5 I2,I1,I5 T2 I2,I4 I2 T3 I1,I2,I3,I5 I2,I1,I3,I5 T4 I1,I3 I1,I3 T5 I2,I3 I2,I3 T6 I1,I2,I3 I2,I1,I3 T7 I2,I5 I2,I5 T8 I3,I4,I5 I3,I5 T9 I1,I3 I1,I3 T10 I1,I2,I5 I2,I1,I5 第二步:构造FP-tree。首先创建树的根节点,用null标记。其次,根据上表对每个记录数据创建一个分支。 扫描第一条记录 扫描第二条记录 扫描第三条记录 扫描第四条记录 中间过程略,最后生成的FP-tree为下图。其中左边的部分叫做头表,头表包含所有的频繁项集,并按照降序排列,表中的每个项目包含一个节点链表,指向树中和它同名的节点。也就是说,相同的项集时通过箭头连起来的。 第三步:从FP-tree中找出频繁项集 这时在内存中就生成了这样一个简明的数据结构,我们的下面的任务就是根据FP-tree中挖掘出频繁项集。遍历头表项中的每一项,依次得到频繁项集。 遍历头表的时候从头表的最后一项开始,在上面生成的FP-tree中,从I5开始。根据I5的节点链表,它共有3个节点路径,分别是 由于频繁项集的阀值是3,那么这棵树经过减枝以后,剩下的分支是(I2,I1:4),因此这棵条件FP树产生的频繁项集包括{ 同理求出剩余的头表的频繁项集。I3的条件FP树产生的频繁项目为{ 求完所有的,我们得到的频繁项集是,和我们使用apriori方法求得的频繁项集的结果是一样的。 图 构建FP-tree的算法流程图。 输入:数据源DS,最小支持度minSupport 输出:FP-tree 步骤: (1) = findFP_1_itemsets(DS);扫描数据库DS,得到频繁项集的集合F和每个频繁项的支持度,将F按照支持度降序排列,得到L1,因此这里的L1是排序的频繁1项集。 (2) (13) (14) (15) } (16) (17) (18) } (19) (20) (21) } (22)} 求频繁模式的伪代码 输入:一棵FP-tree 输出:所有的频繁项集 步骤: (1) if(Tree只包含但路径P)then (2) 对路径P中节点的每个组合 (3) 生成模式B并X,支持数=B中所有节点的最小支持度 (4) Else 对Tree头上的每个ai,do (5) { (6) 生成模式B=ai并x,支持度=ai.support; (7) 构造B的条件模式库和B的条件FP树TreeB; (8) If(TreeB!=空集) (9) Then call FP-Growth(TreeB,B) (10) } (11) } 由于FP-tree是一个树形的数据结果,我们需要构造一个TreeNode类,这个类里需要包含的属性有节点名称、支持度计数、父节点、子节点 这里的属性有minSupport表示的是最小支持度计数的阀值,它等于最小支持度*项集的个数。dataTrans是数据库事务,也就是项集,它是List 函数的伪代码 输入:文件路径 输出:dataTrans 类型是List 方法: (8)按行读取文件不为空){ (9)每行文件按“ ”分成字符串数值 (10) (11) (12) (13)} (14) 函数思想是扫描List,检查在Map中有没有出现过key为这个字符串,如果有将该key的value加1,如果没有将该string作为key加入Map中,并将它的value置为1。然后遍历Map,谁的value大于等于最小支持度阀值将加入到最终频繁1项集的Map中。的java代码如下: 输入: 输出:频繁1-项集类型是 /* * 第二步: * 生成频繁1项集,原理很简单,每行当成一个Set,整个文件当成一个List * 遍历每一个String,加入到Map中,在加入Map中时,如何ItemCount中还没有这个值置 1,如果有就将它的值加1. * 最后遍历大于minSupport的才是频繁1项集 * @return Map * @param List */ private Map Map Map for(Set for(String d:ds){ if(itemCount.containsKey(d)){ itemCount.put(d, itemCount.get(d)+1); }else{ itemCount.put(d, 1); } } } for(Map.Entry if(ic.getValue() >= minSupport){ result.put(ic.getKey(), ic.getValue()); } } System.out.print(result.toString()); return result; } 函数思想是遍历之前的k-1项集,两两组合生成k-项集。这里先将它的值赋值为0,后来再遍历数据库,对支持度进行计数。它的java代码如下: /* * 第三步: * 由频繁K-1项集生成频繁K项集,在构造K项集时,需要连接两个K-1项集,在连接的时候需要判断能不能连接 * 在连接完以后需要减枝,减枝完以后需要判断是否满足大于minSupport * @param preMap k-1项集它的形式是Map * @return k项集,形式还是一样的 * */ private Map Map //遍历两个k-1项集生成k项集 List for(Map.Entry preSetArray.add(preMapItem.getKey()); } int preSetLength = preSetArray.size(); for(int i=0; i for(int j =i+1; j String[] strA1 = preSetArray.get(i).toArray(new String[0]); String[] strA2 = preSetArray.get(j).toArray(new String[0]); if(isCanLink(strA1,strA2)){ Set for(String str:strA1){ set.add(str); } //连接成K项集,由于set是排序的,只需要在strA1的后面天上strA2的最后一个就行了 set.add((String) strA2[strA2.length - 1]); //需要减枝的话,减枝 if(!isCut(preMap,set)){ result.put(set, 0); } } } } return result; } 其他的函数就不再这里赘述了,详见程序源码。最后编写驱动函数。 public static void main(String[] args) throws IOException{ Apriori_FP apriori = new Apriori_FP(); String srcFile = "D:/workspace/suan/Apriori_guize/data.dat"; String shortFileName = srcFile.split("/")[4]; String targetFile = "D:/workspace/suan/Apriori_guize/" + shortFileName.substring(0, shortFileName.indexOf("."))+"_fp_threshold"; dataTrans = apriori.setDataTrans(srcFile); long totalItem = 0; FileWriter tgFileWriter = new FileWriter(targetFile); apriori.setMinSupport((int)(dataTrans.size() * 0.3)); //频繁1项集 Map //频繁1项集信息得加入支持度 Map for(Map.Entry Set fs.add(f1Item.getKey()); f1Map.put(fs, f1Item.getValue()); } totalItem += apriori.outPutMap(f1Map, tgFileWriter); Map do { result = apriori.getNextKItem(result); result = apriori.assertFP(result); System.out.print(result.toString()); totalItem += apriori.outPutMap(result, tgFileWriter); } while(result.size() != 0); tgFileWriter.close(); System.out.println("共有" + totalItem + "项频繁模式"); } 所有的工作都完成了,程序运行即可。运行结果为 {I3=6, I1=6, I2=7, I5=5}{[I1, I5]=3, [I2, I5]=4, [I1, I2]=4, [I1, I3]=4, [I2, I3]=3}{[I1, I2, I5]=3}{}共有10项频繁模式。这个结果跟我们的计算是一样的。 在Java实现的代码将整个事务数据库放在内存中,随着数据量的增大,继续将事务数据库存放在内存中是不切实际的。 MR实现的实现原理。在Map阶段做,在reduce阶段做, FP-Growth算法通过构造FP树映射事务数据,然后从FP树中构造个频繁项集的条件FP树,最后从条件FP树中包含该频繁项集的条件FP树的桥梁。然而在MR-FP算法中,无需构造整个事务集的FP树,仅需要将构造该频繁项集条件FP树的事物发送到其计算节点上,由该节点构造该频繁项集的条件FP树,并使用经典的FP-Growth算法得到包含该频繁项集并输出。 在mahout中没有apriori算法的实现,但还是提供了FP-tree算法的实现。二话不说,先用起来。 在mahout中上传测试数据到Hadoop集群。数据集的格式如下 f,g,d,e,b,c,a,j,k,h,i f,g,d,e,c,j,k,h,i f,d ..... Mahout中FP-Growth的入口类是FPGrowthDriver.java,它的参数是 FPGrowthDriver.main(arg); --minSupport (-s) 最小支持度 --input (-i) input 输入路径 -output (-o) 输出路径 --maxHeapSize (-k) (Optional) 挖掘最少k个item --numGroups (-g) 特征分组数 --method (-method) Sequential或mapreduce --encoding (-e) 默认UTF-8 --numTreeCacheEntries (-tc) 缓存大小,默认5 --splitterPattern (-regex) 默认"[ ,\t]*[,|\t][ ,\t]*" 运行命令: hadoop jar mahout-core-0.9.jar org.apache.mahout.fpm.pfpgrowth.FPGrowthDriver -i /bigdata/fpgrowth/FPdata.txt -o fpgrowth -k 4 -g 50 -method mapreduce -e UTF-8 -tc 5 -s 5 输出格式 结果文件 Key类型 Value类型 frequentpatterns Item (org.apache.hadoop.io.Text) 模式(org.apache.mahout.fpm.pfpgrowth.convertors.string.TopKStringPatarn 阅读mahout中fp-growth的源码来看它的MapReduce程序是如何实现的。 自决策树提出以来就受到了广泛的使用,特别是在金融预测、工程决策等领域。在金融中经常有这样的应用场景,根据用户信息预测该用户会不会购买某种服务。如下图,比如年龄大于50岁的客户一定不会购买该服务。20-30岁之间的客户,再根据收入情况判定,如果某个用户的年龄在20-30岁之间他的收入又大于5000元,那么他可能购买该服务。同理,一个用户在是34岁,他没有房产,那么他购买该服务的可能性很小。 决策树分类算法分为两步。首先根据训练数据构建决策树,然后根据决策树对输入的数据进行分类。因此该算法是有监督的学习算法。在构建决策树的过程中,又分为两个关键的步骤:建树和减枝。这些在后面的算法中具体介绍。我们来看图一这棵决策树。方框即内部结点表示属性,椭圆也就是叶子结点表示分类结果,斜线表示属性的值,也就是条件分支。决策树构建过程是不断地按广度优先递归,直到叶子结点属于某一个类为止。在预测的过程中,根据深度优先,按照条件分支,直到找到叶子结点即分类结果为止。 图一、银行服务决策树 聪明的读者肯定会想到,对于同一个问题,根据不同的属性选择能构造出不同的决策树。比如在图一中,我们选择年龄作为一个属性判别,如果我们选择收入作为一个属性判别,那么跟节点就是收入了,是一棵完全不同的决策树。我们希望决策树是简洁的,能用最少的属性、最少的分支来完成分类任务。所有在属性的选择中,决策树运用了贪心方法,选择信息增益作为属性的选择依据,每次都选择信息增益最大的。至于为什么选择信息增益作为选择标准,以及信息增益的计算方法在ID3算法中介绍。 简单来说,迭代两分器(Iterative Dichotomizer3,ID3)算法就是一种构造决策树的方法。它的思路非常简单,即在构建决策树的过程中,从根结点开始总是选择具有最大信息增益的属性作为当前结点的最佳测试属性。其具体方法是:检测所有的属性,选择信息增益最大的属性作为节点的判定条件,由该属性的不同取值向下建立分枝,再对各分枝的子集递归调用该方法往下建立决策树。 决策树学习的基本算法思想是贪心,即每次选择信息增益最大的属性。ID3算法中的属性是离散值,连续值的属性必须离散化。 信息熵是通信与信息论中一个重要概念,常常用它衡量一个随机变量取值的不确定性程度。在数据集合中,熵可以作为数据集合的不存度或者不规则程度的度量。所谓的不规则程度指的是集合元素之间依赖关系的强弱。设X使一个离散随机变量,它可能的取值为X的概率为P(x),那么定义 (1.1) 这里的H(X)就是随机变量X的熵,它是衡量随机取值的不确定性的度量。在随机试验之前,我们只了解个取值的概率分布,而做完随机试验以后,就确切地知道了取值,不确定完全消失了。这样,通过随机试验获取了信息,且该信息的数量恰好等于随机变量的熵,在这个意义上,熵可以作为信息的度量。 讲完信息熵,那信息增益是什么呢?信息熵刻画了任意样本集的纯度。设S是n个数据样本的集合,将样本集划分为C个不同的类Ci。每个类Ci含有的样本数为ni,则S划分为C个类的信息熵或期望信息为 (1.2) 其中,为中样本的属性属于第类的概率,即。信息以二进制编码,因此对数的底数是2。 假设给定样本集合含有10个样本,分为2类,类含有6个样本, 类含有4个样本。则 一个属性的信息增益,就是用这个属性对应样本分类而导致的熵的期望值下降。因此,ID3算法在每一个结点选择最大信息增益的属性。 假设属性A的所有不同值的集合为,是中属性A的值为的样本子集,即,在选择属性A后每一个分支节点上,对该结点的样本集分类的熵为。选择A导致的期望熵定义为每个子集的熵的加权和,权值为属于样本占原始样本的比例,即期望熵为 (1.4) 其中,是将中样本划分到c个类的信息熵。 属性A相对样本集合的信息熵增益定义为 (1.5) 是指因知道属性A的值后导致的熵的期望压缩。越大,说明选择测试属性A对分类提供的信息越多。 那么,我们为什么要属于信息增益作为选择属性的标准呢?决策树算法根据最佳属性测试选择标准对节点进行分裂,分裂标准反应样本划分的纯度,不存度越低,类分布就越倾斜,也就意味着,决策树高度增加,后续搜索的时间复杂度增加,这是我们非常不愿意看见的结果。因此,每次进行属性选择的时候都选择不存度最高的,也就是信息增益最高的。其实除了信息熵,基尼系数也常作为属性选择的标准。 例1.1我们采用金融数据来做分析。由于属性数量太多,计算量太大,我们选取部分属性和部分用户构造出下面的数据集。根据下列数据集构造决策树。 表1.1购买银行服务的样本 age income housing martial y 序号 20-30岁 <=5000 yes no no N1 20-30岁 <=5000 no no no N2 >50岁 >5000 yes yes no N3 30-50岁 <=5000 yes yes yes N4 30-50岁 >5000 yes yes yes N5 30-50岁 >5000 no no no N6 >50岁 >5000 no yes no N7 20-30岁 <=5000 yes no no N8 20-30岁 >5000 yes no yes N9 30-50岁 >5000 yes yes yes N10 30-50岁 >5000 no no yes N11 30-50岁 <=5000 no yes yes N12 >50岁 >5000 yes yes no N13 >50岁 <=5000 yes yes no N14 在实际场景中,年龄和收入其实是连续的变量,ID3算法是不能处理连续变量的,我们需要将连续的变量离散化。即将年龄分为20-30岁,30-50岁,50岁以上。收入分为大于5000,小于等于5000。 安装上面的算法步骤解题。我们需要选择一个属性作为根节点。如何选择这个属性呢?计算每个属性的信息增益,选择最大的那个。即计算,,,,选择最大的那个。下面以为例展示计算过程。 第一步:计算给定样本分类所需的期望信息即E(S)。 第二步:计算属性年龄的期望熵即。要想得到,需要得到属性为age的各个值的期望。,,,其中类中1个,中3个,故有 ,,其中类中5个,中1个,故有 ,其中类中0个,中4个,故有 因此属性age的期望熵为 同理可得 图二、决策树 数据集下载地址。数据集说明:银行客户是否会使用某种服务。客户的基本信息包括age(年龄),job(工作情况),marital(婚姻状况),default(默认),balance(收入),housing(是否有房产),loan(是否贷款),contact(职称),day(),mouth(),duration(),campaign(),pdays(),previous(),poutcome(),y(是否预订服务)。 数据结构设计。结点类TreeNode,TreeNode包括属性element,value和孩子节点,由于可能有多个孩子结点,所有孩子节点选用LinkedList。DecisionTree类。类中的方法有构造树,选择属性。 算法设计: 算法:Decision_Tree(samples,attribute_list) 输入:由离散值属性描述的训练样本集samples,候选属性集合attribute_list 输出:一棵决策树 方法: (1)创建节点N (2)If samples 都在同一类C中,then (3)返回N作为叶节点,以类C标记 (4)If attribute_list为空then (5)返回N作为叶节点,以samples中最普通的类标记 (6)选择attribute_list 中最高增益的属性test_attribute (7)以test_attribute标记节点N (8)For each test_attribute 的已知值v (9)由节点N分出一个对应test_attribute=v的分支 (10)令Sv为samples中test_attribute=v的样本集合 (11)If Sv为空 then (12)加上一个叶节点,以samples中最普遍的类标记 (13)Else 加上一个由Decision——tree返回的节点。 算法流程图 关键代码: 试验结果。 结果分析: 传统的决策树算法是基于内存计算的,整个决策树的生成过程以及数据计算都是在单台计算机的内存中完成的,已经不适用于海量数据的处理。因此开发基于hadoop MapReduce的决策树并行化程序成为必要。 在上面的讨论中发现,决策树算法的核心是属性的选择,选择属性的过程是该算法中最占用计算资源的阶段。因此,对这个阶段进行并行化处理是至关重要的。在本书中属性选择的度量标准是信息增益,信息增益计算是基于属性间相互独立的,因此,可以在MapReduce中并行计算得出各个属性的相关信息。最后,在主程序中计算信息增益,并选择最佳的分裂属性。 Map阶段 Map阶段针对海量数据集合按照划分条件进行划分,所谓的划分条件是该结点在决策树中的路径。 age income housing martial y 序号 20-30岁 <=5000 yes no no N1 20-30岁 <=5000 no no no N2 >50岁 >5000 yes yes no N3 30-50岁 <=5000 yes yes yes N4 30-50岁 >5000 yes yes yes N5 30-50岁 >5000 no no no N6 >50岁 >5000 no yes no N7 20-30岁 <=5000 yes no no N8 20-30岁 >5000 yes no yes N9 30-50岁 >5000 yes yes yes N10 30-50岁 >5000 no no yes N11 30-50岁 <=5000 no yes yes N12 >50岁 >5000 yes yes no N13 >50岁 <=5000 yes yes no N14 节点1 20-30岁 <=5000 yes no no N1 20-30岁 <=5000 no no no N2 >50岁 >5000 yes yes no N3 30-50岁 <=5000 yes yes yes N4 节点2 30-50岁 >5000 yes yes yes N5 30-50岁 >5000 no no no N6 >50岁 >5000 no yes no N7 20-30岁 <=5000 yes no no N8 20-30岁 >5000 yes no yes N9 节点3 30-50岁 >5000 yes yes yes N10 30-50岁 >5000 no no yes N11 30-50岁 <=5000 no yes yes N12 >50岁 >5000 yes yes no N13 >50岁 <=5000 yes yes no N14 Map阶段在构建树的过程中,构建数, ID3算法使用于属性集是离散值的情况下,而且当空值比较大的情况下,生成的决策树很倾斜。ID3算法的优点是,缺点是,针对它的缺点,有很有改进的算法出现。 ID4.5算法的核心思想和ID3算法是一样的,只是在构建决策树的过程中,ID4.5算法可以进行减枝,减少了算法的时间复杂度和空间复杂度。ID4.5还可以处理连续型的变量。适用的场景更广泛。 决策树的减枝分为两种,分别是同步减枝和滞后减枝。顾名思义,同步减枝就是在构建决策树的过程中减枝,滞后减枝是构建决策树结束后减枝。显然,同步减枝的效率更高。 (a)同步减枝 该方法在树构建的过程中,就判断给定的节点是否需要继续划分或分裂训练样本的子集来实现提前停止树的构建。一旦决定停止分枝,当前结点就标记为叶节点。进行判断时,可以利用信息增益方法或统计上的重要性检测等方法来对分枝生成情况进行评估,如果评估后一个节点上数据记录数将导致低于预定义的阀值,则要停止继续分裂,并将当前节点上包含记录的大多数类别对该节点进行标记。 (b)滞后减枝 该方法对决策树的构建阶段不加干扰,首先生成与训练集完全吻合的决策树。滞后减枝方法的输入为一个未减枝的决策树,输出的是经过减枝的决策树。一般情况下,滞后减枝方法从树的叶子结点开始减枝,逐步向根节点方向减枝。基于代价的复杂性减枝算法就是一种滞后剪枝方法。对于树中每个非叶子结点,计算该结点被剪枝后所发生的期望错位率,计算每个分支的错误率和每个分支的权重,计算该节点不被剪枝时期望的错误率;如果因为剪枝导致期望错误率变大,则放弃修剪。常见的剪枝标准有最小描述长度和最小期望错误码。前者是一种滞后剪枝算法,该标准对生成的决策树进行二位编码,编码所需二进制最小的树就是最佳剪枝数。后者计算节点上的子数被修剪后出现的期望错误率。 举个例子来讲。 第一步:下载数据集 第二步:编译文件 第三步:运行 用其他的数据集来做测试。用我们的数据该怎么运行呢。 建一个工程。加入mahout依赖 打包上传到集群上,运行 Mahout decision Forest的局限性。不支持太大的数据。不支持多个文件输入。Mahout的源码解析。N C类型,是不是分别进行了处理 在mahout中决策树的源码分为单机版和分布式版本,单机版在 分布式版本在partional目录下。在这个package介绍中这样写到, 在mahout的官网中这样写。这里我们先安装官网介绍的步骤进行mahout决策森林的使用。 第一步:下载数据集 我们还是使用我们Java实现的数据集,将其上传到hadoop的集群上。 第二步:编译Job文件 运行mvn claen install –Dskip Tests 运行代码 只有一个Map函数,每个节点上有数据,是主节点将这些数据分配到不同的节点上,还是在上传的时候上传到不同的节点上呢。完全不知所云 只有一个Map函数。随机森林 决策树有着良好的特性,比如训练时间复杂度低、预测的过程比较快速等。但是生成单决策树时树容易倾斜。后来出现了比较多的和决策树相关的算法,比如Boosting,Bagging等。他们的最终结果是生成N棵树,这样可以大大减少单决策树带来的问题。 随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的总数决定的。试验表明随机森林算法的速度准确率以及抗干扰能力都很好。 随机森林的优点有很多。它在大数据集上表现良好,它能够在不做特征选择的情况下处理高维度的数据,而且它在训练完成以后,能够给出那些特征比较重要。随机森林的并行化也比较简单。Mahout中也提供了随机森林的算法。在MapReduce程序中,每个Mapper构建一棵随机森林的子数。它允许用大数据集建构随机森林,只要内存放得下。 顾名思义随机森林就是用随机的方式建立一个森林,森林有很多的决策树组成,随机森林的每一棵决策树之间是没有关联的。当随机森林训练完成以后,新的样本输入时,就让森林中的每一棵决策树进行判断,首先看这个样本属于哪些类,然后看看哪一类选择的最多,就预测样本属于哪一类。 在建立每一棵决策树的过程中,有两点需要注意 - 采样与完全分裂。首先是两个随机采样的过程,random forest对输入的数据要进行行、列的采样。对于行采样,采用有放回的方式,也就是在采样得到的样本集合中,可能有重复的样本。假设输入样本为N个,那么采样的样本也为N个。这样使得在训练的时候,每一棵树的输入样本都不是全部的样本,使得相对不容易出现over-fitting。然后进行列采样,从M个feature中,选择m个(m << M)。之后就是对采样之后的数据使用完全分裂的方式建立出决策树,这样决策树的某一个叶子节点要么是无法继续分裂的,要么里面的所有样本的都是指向的同一个分类。一般很多的决策树算法都一个重要的步骤 - 剪枝,但是这里不这样干,由于之前的两个随机采样的过程保证了随机性,所以就算不剪枝,也不会出现over-fitting。 按这种算法得到的随机森林中的每一棵都是很弱的,但是大家组合起来就很厉害了。我觉得可以这样比喻随机森林算法:每一棵决策树就是一个精通于某一个窄领域的专家(因为我们从M个feature中选择m让每一棵决策树进行学习),这样在随机森林中就有了很多个精通不同领域的专家,对一个新的问题(新的输入数据),可以用不同的角度去看待它,最终由各个专家,投票得到结果。 GBRT是随机森林的改进算法,全称是Gradient Boost Regression Tree。Boost是"提升"的意思,一般Boosting算法都是一个迭代的过程,每一次新的训练都是为了改进上一次的结果。 原始的Boost算法是在算法开始的时候,为每一个样本赋上一个权重值,初始的时候,大家都是一样重要的。在每一步训练中得到的模型,会使得数据点的估计有对有错,我们就在每一步结束后,增加分错的点的权重,减少分对的点的权重,这样使得某些点如果老是被分错,那么就会被“严重关注”,也就被赋上一个很高的权重。然后等进行了N次迭代(由用户指定),将会得到N个简单的分类器(basic learner),然后我们将它们组合起来(比如说可以对它们进行加权、或者让它们进行投票等),得到一个最终的模型。 而Gradient Boost与传统的Boost的区别是,每一次的计算是为了减少上一次的残差(residual),而为了消除残差,我们可以在残差减少的梯度(Gradient)方向上建立一个新的模型。所以说,在Gradient Boost中,每个新的模型的简历是为了使得之前模型的残差往梯度方向减少,与传统Boost对正确、错误的样本进行加权有着很大的区别。 在分类问题中,有一个很重要的内容叫做Multi-Class Logistic,也就是多分类的Logistic问题,它适用于那些类别数>2的问题,并且在分类结果中,样本x不是一定只属于某一个类可以得到样本x分别属于多个类的概率(也可以说样本x的估计y符合某一个几何分布),这实际上是属于Generalized Linear Model中讨论的内容,这里就先不谈了,以后有机会再用一个专门的章节去做吧。这里就用一个结论:如果一个分类问题符合几何分布,那么就可以用Logistic变换来进行之后的运算。 假设对于一个样本x,它可能属于K个分类,其估计值分别为F1(x)…FK(x),Logistic变换如下,logistic变换是一个平滑且将数据规范化(使得向量的长度为1)的过程,结果为属于类别k的概率pk(x), 对于Logistic变换后的结果,损失函数为: 其中,yk为输入的样本数据的估计值,当一个样本x属于类别k时,yk = 1,否则yk = 0。 将Logistic变换的式子带入损失函数,并且对其求导,可以得到损失函数的梯度: 上面说的比较抽象,下面举个例子: 假设输入数据x可能属于5个分类(分别为1,2,3,4,5),训练数据中,x属于类别3,则y = (0, 0, 1, 0, 0),假设模型估计得到的F(x) = (0, 0.3, 0.6, 0, 0),则经过Logistic变换后的数据p(x) = (0.16,0.21,0.29,0.16,0.16),y - p得到梯度g:(-0.16, -0.21, 0.71, -0.16, -0.16)。观察这里可以得到一个比较有意思的结论: 假设gk为样本当某一维(某一个分类)上的梯度: gk>0时,越大表示其在这一维上的概率p(x)越应该提高,比如说上面的第三维的概率为0.29,就应该提高,属于应该往“正确的方向”前进 越小表示这个估计越“准确” gk<0时,越小,负得越多表示在这一维上的概率应该降低,比如说第二维0.21就应该得到降低。属于应该朝着“错误的反方向”前进 越大,负得越少表示这个估计越“不错误 ” 总的来说,对于一个样本,最理想的梯度是越接近0的梯度。所以,我们要能够让函数的估计值能够使得梯度往反方向移动(>0的维度上,往负方向移动,<0的维度上,往正方向移动)最终使得梯度尽量=0),并且该算法在会严重关注那些梯度比较大的样本,跟Boost的意思类似。 得到梯度之后,就是如何让梯度减少了。这里是用的一个迭代+决策树的方法,当初始化的时候,随便给出一个估计函数F(x)(可以让F(x)是一个随机的值,也可以让F(x)=0),然后之后每迭代一步就根据当前每一个样本的梯度的情况,建立一棵决策树。就让函数往梯度的反方向前进,最终使得迭代N步后,梯度越小。 这里建立的决策树和普通的决策树不太一样,首先,这个决策树是一个叶子节点数J固定的,当生成了J个节点后,就不再生成新的节点了。 算法的流程如下:(参考自treeBoost论文) 0. 表示给定一个初始值 1. 表示建立M棵决策树(迭代M次) 2. 表示对函数估计值F(x)进行Logistic变换 3. 表示对于K个分类进行下面的操作(其实这个for循环也可以理解为向量的操作,每一个样本点xi都对应了K种可能的分类yi,所以yi, F(xi), p(xi)都是一个K维的向量,这样或许容易理解一点) 4. 表示求得残差减少的梯度方向 5. 表示根据每一个样本点x,与其残差减少的梯度方向,得到一棵由J个叶子节点组成的决策树 6. 为当决策树建立完成后,通过这个公式,可以得到每一个叶子节点的增益(这个增益在预测的时候用的) 每个增益的组成其实也是一个K维的向量,表示如果在决策树预测的过程中,如果某一个样本点掉入了这个叶子节点,则其对应的K个分类的值是多少。比如说,GBDT得到了三棵决策树,一个样本点在预测的时候,也会掉入3个叶子节点上,其增益分别为(假设为3分类的问题): (0.5, 0.8, 0.1), (0.2, 0.6, 0.3), (0.4, 0.3, 0.3),那么这样最终得到的分类为第二个,因为选择分类2的决策树是最多的。 的意思为,将当前得到的决策树与之前的那些决策树合并起来,作为新的一个模型(跟6中所举的例子差不多) 各种算法的比较 决策树 随机森林 Boosting GBRT 是否需要全部的特性向量 速度 并行化 弱分类/强分类 说起SVM,不得不提的是统计学习理论的创始人Vapnik。他的书《statistical learning Theory》完整的阐述了统计机器学习的理论,代表这SLT理论的成熟。20世纪90年代中期,Vapnik等人得出了SVM理论。 在学术文献中支持向量机是这么定义的:支持向量机方法建立在统计学习理论的VC维理论和结构最小化原理基础上,根据有限的样本信息在模型的复杂性和学习能力之间寻求最佳折中,希望获取最好的推广能力。 因此在介绍支持向量机之前需要介绍一下统计学习理论。传统的统计模式识别方法只有在样本趋于无穷大的情况下,其性能才有理论上的保证。而海量的数据必然导致学习成本的提高,人们期望有一种学习了小样本以后就能得出可用性比较理想的分类器的自动学习方法。统计学习理论就是在此背景下诞生的。 统计学习理论(SLT,statistical learning Theory)是什么呢?它相较于传统的统计学理论,有什么优势呢?所谓统计学习理论就是希望学习的样本不多,得到的训练模型跟学习完所有的训练集得到的模型是一样的。这样学习的成本降低,效果却没有降低。与传统的统计理论相比,统计学习理论基本上不涉及测度的定义以及大数定律。统计学习理论为建立有限样本学习问题提供了一个统一的框架。它避免了人工神经网络等网络结构选择、过学习和前学习的以及局部极小等问题。 统计理论学习的核心是VC维(VC Dimension)概念,VC维是什么呢?它受什么的影响和影响了什么?导致哪些算法能用,哪些算法不能用?VC维是对函数类的一种度量,可以简单的理解为问题的复杂度,VC维度越高,一个问题就越复杂。在一定程度上VC维解决了维数灾难的问题。当然这个理解是不准确的。在下面我们将做详细的介绍。 什么是结构风险最小化。SVM的本质是构建了一个学习机模型,这个模型和真实的模型之间的误差就叫做结构风险,我们期望这个误差越小越好也就是结构风险最小化。 现在我们对支持向量机有了一个概念化的认识。 下面举个例子,在二维平面上将下面的图形分成两部分。我们很容易能做出这样一条线将平面中的黑点和白点分开。我们把它想象成三维空间,这条线变成了一个平面,将空间中的点分成了两部分。接着我们把它想象成一个n维空间,这个平面就变成了一个超平面将空间中的点分成了两部分。 因此SVM的分类思想是 (1)它是针对线性可分情况进行分析,对于线性不可分的情况,通过使用非线性映射算法将低维输入空间线性不可分的样本转化为高维特征空间使其线性可分,从而使得高维特征空间采用线性算法对样本的非线性特征进行线性分析成为可能。 (2)它基于结构风险最小化理论之上在特征空间中建构最优分割超平面,使得学习器得到全局最优化,并且在整个样本空间的期望风险以某个概率满足一定上界。 只要理解了线性可分的情况下SVM的原理就能理解其他复杂情况下的SVM工作原理。我们希望得到的一个最有超平面将空间中的点分成两部分。 线性分类器(也叫做感知机)是最简单也很有效的分类器形式,支持向量机最初也是从样本线性可分情况下寻求最优分类面的基础上发展而来的,从一个线性分类器中,可以看到SVM形成的思路,体现SVM的核心概念。 图3.1 线性分类器 图3.2 多条分类方案 如上两图(图3.1,图3.2)所示,可以很直观地看出分类器工作的原理,找到可以划分不同类别的直线(二维)、平面(三维)、超平面(多维)。我们令黑色的点 = -1, 白色的点 = +1,直线,这儿的、是向量。 更一般形式为: 当向量的维度为2的时候, 表示二维空间中的一条直线, 当的维度为3的时候,表示3维空间中的一个平面,当的维度为n > 3的时候,表示n维空间中的n-1维超平面。所以当有一个新的点需要预测属于哪个分类的时候,我们用,就可以预测了,表示符号函数,当> 0的时候, = +1, 当< 0的时候= -1。 大多数情况下,分类器处理的都是数据都是多维,最核心的问题是如何找到最优超平面。如下面两图所示(图3.4,图3.5)。 图3.4 分类方法1 图3.5 分类方法2 如何能使分类效果更好,从直观上讲,就是分割的间隙越大越好,把两个类别的点分得越开越好。两者的差别越大,意味着它们间的界限就越明显,所以图3.5所示的方法2比图3.4所示的方法1分类效果更好。那么如何才能找到最大间隔的哪个超平面?在SVM中,Maximum Marginal就是最大的边界,被圈出来的点就是支持向量。支持向量是线, 上的点。为什么选用这些点当成支持向量呢?其中是超平面的法向量,是超平面的常数项。根据解析几何中的概念在二维平面内表示一条直线,即的变形。支持向量表示离超平面最近的点,如果使得这些点之间的间隔最大,也就是最优超平面。 如图3.6所示(见下页),红线表示+1 calss边界,蓝线表示-1 class边界,黑线表示分类边界。令由二类别组成的样本集{(,),其中= 1,2,..},假如=-1,则表示属于第1类,=+1代表属于第2类,通过训练数据学习,构造一个决策函数,尽可能正确分类样本集,使两个超平面距离M最大。 图3.6 分类示意图1 图3.7 分类示意图2 设: 红线表示为+1, 蓝线表示为-1, 黑线表示为0. 则红色区域为,=1 则蓝色区域为,=-1 现在我们的目标就是寻找最优的分类超平面,即求。求得这个,求得最优超平要。想找最大的超平面,也就是找最大间隔。我们找支持向量之间的距离,使得它最小的 就是我们要找的结果。假设点1在线上即,点2在线线上即。它们之间的垂直距离就是我们想要求的间隔。 ;(式子 1) ;(式子2) 式子1-减去式子2得到式子3 (式子3) 式子两边同时除以单位法向量,得到式子4 (式子4) 得支持向量之间的间隔,确定最大,只需要确定最小,即求。为什么选择呢,是数学计算的方便。 最后求解分类问题的最优分类面问题转换为求解如下一元二次方程的最优化问题 (公式3.1) 求解这个最优化问题需要基本的拉格朗日乘子法。基本的拉格朗日乘子法是求函数在某些约束条件下的极值问题。它的基本思想是引入一个新的的参数(拉格朗日乘子),将约束条件和原函数联系在一起,使能配成与变量相等的等式方程,从而求出得到原函数极值的各个变量的解。 学过求解最优化问题的同学应该了解,最优化问题最关键的两部分是目标函数和约束函数。这里,目标函数是,约束函数是 约束函数的推导过程 首先建立拉格朗日函数,引进拉格朗日乘子。而拉格朗日函数的构造规则是用约束方程乘上非负的拉格朗日系数,然后再从目标函数中减去。因此我们得到的拉格朗日函数为: 其中 ,它就是拉格朗日乘子 然后对 求偏导数 原问题的对偶问题 求解优化问题变成了求解优化问题的对偶问题 对偶问题完全是根据训练集数据 subject to (=1,2,3...,为样本数)(公式3.2) 通过解公式(3.1)、(3.2)的优化问题可以得到该线性可分问题的最优分类平面,对整个训练样本的分类就可以转化为对支持向量的分类,所以说支持向量是整个样本集的特殊代表,它包含了该样本集的关键信息,支持向量机也因此得名。 上面我们介绍了二维平面内的SVM,在很多情况下,是非线性可分的。非线性可分的情况下可以采用高维映射的方法,将其转换成线性可分的。这这种情况下就引入了核函数。 这个函数就叫做核函数和惩罚函数,它们的作用是, SVM处理多分类问题。 上面讨论都是基于样本线性可分的前提下进行的,当训练样本在空间中线性不可分(如图3.8),即找不到一个超平面将它们完全分开,则优化问题(3.1)、(3.2)就无可行解,如何在样本线性不可分的情况下仍然能够找到大致划分样本类别的最优平面呢?为此Vapnik提出了软间隔的概念,这一概念的关键在于分类面是允许少量错分的,是在错分比率尽可能小的前提下优化问题的解。 图3.8 线性不可分示意图 如上图3.8所示,没有办法用一条直线去将其分成两个区域,每个区域只包含一种颜色的点。这种情况下的分类器,有两种方式,一种是用曲线去将其完全分开(如图3.9所示),曲线就是一种非线性的情况,跟下一节将谈到的核函数有一定的关系。 图3.9 曲线分类(线性不可分)示意图 另一种方式是还是用直线,不过不用去保证可分性,就是包容分错的情况,这就是软间隔。依照软间隔这种允许错分的思路,支持向量机引入了松弛变量的概念,原先对样本的约束条件变为: (公式3.3) (=1,2,3...) 为松弛变量,为样本数 (公式3.4) 从式(3.4)可知松弛变量是非负的,当分类结果小于1时,表示样本出现在图3.10的三条平行线之间,而允许样本出现在平行线中间就表示实验者放弃了对它们的精确分类,很显然这对分类器来说是一种损失,所换来的好处是分类面不必向这些样本点进行移动,从而得到较大的几何间隔。 图3.10 直线分类(线性不可分)示意图 在上图中,蓝色、红色的直线分别为支持向量所在的边界,绿色的线为决策函数,那些紫色的线表示分错的点到其相应的决策面的距离,这样我们可以在原函数上面加上一个惩罚函数,并且带上其限制条件为: (公式3.5) subject to , 公式3.5是在线性可分问题的基础上加上的惩罚函数部分,当在正确一边的时候,=0,R为全部的点的数目,C是一个由用户去指定的系数,表示对分错的点加入多少的惩罚,当C很大的时候,分错的点就会更少,但是过拟合的情况可能会比较严重。当C很小的时候,分错的点可能会很多,不过可能由此得到的模型也会不太正确,所以如何选择C是有很多学问的,不过在大部分情况下就是通过经验尝试得到的。这里所说的c就是惩罚系数。 前面两小节介绍的是线性可分和线性不可分的情况,因为两者都是基于在空间中至少能找到一个超平面使得样本能在容错范围类区分开的前提下进行训练的。但如果数据集本身不具有线性可分,即无法在输入空间中用线性判别函数将样本分开(如图3.11)。也就是说在线性不可分,即使是升维情况下仍然不可分的情况下,需要加入核函数(错误的看法)。 图3.11 非线性可分示意图 那么怎样才能建立分类面去分类?或者反过来理解如何使非线性可分转换成线性可分?前者在非线性的情况下很难找到合适的分类函数,那么可以考虑后者的想法。我们可以让空间从原本的线性空间变成一个更高维的空间,在这个高维的线性空间下,再用一个超平面进行划分。 当我们把这两个类似于椭圆形的点(如图3.11)映射到一个高维空间后,映射函数为: 用这个函数将上图的平面中的点映射到一个三维空间(),并且对映射后的坐标加以旋转之后就可以得到一个线性可分的点集了(如下图3.12,图3.13)。 图3.12 非线性三维旋转平面正面 图3.13非线性三维旋转平面侧面 我们将核函数定义为: (公式3.6) 公式3.6所做的事情就是将线性的空间映射到高维的空间,有很多种,下面是比较典型的两种: 多项式核函数 (公式3.7) 高斯核函数 (公式3.8) Libsvm中默认的核函数是RBF核函数。我们再来理解一下,引入核函数的目的是什么?核函数与什么有关?为什么这么做就能到达引入核函数的目的。 LIBSVM是台湾大学林智仁(Lin Chih-Jen)副教授等开发设计的一个简单、易于使用和快速有效的SVM模式识别与回归的软件包,他不但提供了编译好的可在Windows系列系统的执行文件,还提供了源代码,方便改进、修改以及在其它操作系统上应用;该软件对SVM所涉及的参数调节相对比较少,提供了很多的默认参数,利用这些默认参数可以解决很多问题;并提供了交互检验(Cross Validation)的功能。目前,LIBSVM拥有C、Java、Matlab、C#、Ruby、Python、R、Perl、Common LISP、Labview等数十种语言版本。最常使用的是C、Matlab、Java和命令行(C语言编译的工具)的版本。查看LIBSVM的README来做试验。 我们介绍一个LIBSVM最常用的两个类svm_train.java和svm_predict.java。 其中svm_train.java的用法和选择如下。-s 表示设置SVM的类型。0代表C-SVC,1代表nu-SVC等。-t 表示核函数的类型。其中,0表示线性核函数,3表示sigmoid核函数,4代表自定义核函数等等。下面的参数就不再一一介绍。 Usage: svm_train [options] training_set_file [model_file] options: -s svm_type : set type of SVM (default 0) 0 -- C-SVC 1 -- nu-SVC 2 -- one-class SVM 3 -- epsilon-SVR 4 -- nu-SVR -t kernel_type : set type of kernel function (default 2) 0 -- linear: u'*v 1 -- polynomial: (gamma*u'*v + coef0)^degree 2 -- radial basis function: exp(-gamma*|u-v|^2) 3 -- sigmoid: tanh(gamma*u'*v + coef0) 4 -- precomputed kernel (kernel values in training_set_file) -d degree : set degree in kernel function (default 3) -g gamma : set gamma in kernel function (default 1/num_features) -r coef0 : set coef0 in kernel function (default 0) -c cost : set the parameter C of C-SVC, epsilon-SVR, and nu-SVR (default 1) -n nu : set the parameter nu of nu-SVC, one-class SVM, and nu-SVR (default 0.5) -p epsilon : set the epsilon in loss function of epsilon-SVR (default 0.1) -m cachesize : set cache memory size in MB (default 100) -e epsilon : set tolerance of termination criterion (default 0.001) -h shrinking : whether to use the shrinking heuristics, 0 or 1 (default 1) -b probability_estimates : whether to train a SVC or SVR model for probability estimates, 0 or 1 (default 0) -wi weight : set the parameter C of class i to weight*C, for C-SVC (default 1) -v n : n-fold cross validation mode -q : quiet mode (no outputs) 另外,svm_predict.java用法介绍。它的主函数的参数一共有4个,其中第一个是可选参数。test_file表示测试集,model_file指的是svm_train训练的SVM的模型,output_file是预测结果数据集。 usage: svm_predict [options] test_file model_file output_file options: -b probability_estimates: whether to predict probability estimates, 0 or 1 (default 0); one-class SVM not supported yet LIBSVM 使用的一般步骤是: 1)按照LIBSVM软件包所要求的格式准备数据集;这里我们下载的数据集是UCI的UCI-breast-cancer数据集,训练样本和测试样本的基本格式是这样的 分别代表 类别 feature1索引:feature1值 feature2索引:feature2值 2)建立Java项目,将libsvm源码中的svm_train.java和svm_predict.java拷贝到项目中,并添加libsvm的jar包依赖。也可以不将这两个文件copy过去,为了观察源码进行的操作。 3)写主函数。注意,在参数设置中选用你想用的svm类型和核函数的类型以及其他的参数。 public class LibSVMTest { public static void main(String[] args) throws IOException { String[] trainArgs = {"-s","0","-t","0","-g","4","libsvmdata/UCI-breast-cancer-tra"};//directory of training file svm_train.main(trainArgs); String modelFile = "libsvmdata/UCI-breast-cancer-tra.model"; String[] testArgs = {"libsvmdata/UCI-breast-cancer-test", modelFile , "libsvmdata/UCI-breast-cancer-result"};//directory of test file, model file, result file svm_predict.main(testArgs); } } 4)运行函数,查看运行结果。这个结果说明准确率是69%。 optimization finished, #iter = 10000000 nu = 0.5288710696972564 obj = -1.3592340487368342E8, rho = -92293.78645272687 nSV = 343, nBSV = 339 Total nSV = 343 Accuracy = 69.23076923076923% (27/39) (classification) 5)不停的调整参数,找出最好的设置。采用最佳参数对整个训练集进行训练获取支持向量机模型; 6)利用获取的模型进行测试与预测。 总的来说,我们利用libsvm完成了一个简单的例子。同学们要想对libsvm有跟详细的了解,自行查阅资料。 本文使用的是分类算法是支持向量机(Support Vector Machine,SVM)。Vapnik等人在多年研究统计学习理论基础上对线性分类器提出了另一种设计最佳准则。其原理也从线性可分说起,然后扩展到线性不可分的情况。甚至扩展到使用非线性函数中去,这种分类器被称为支持向量机(Support Vector Machine,简称SVM)。 SVM的主要思想: (3)它是针对线性可分情况进行分析,对于线性不可分的情况,通过使用非线性映射算法将低维输入空间线性不可分的样本转化为高维特征空间使其线性可分,从而 使得高维特征空间采用线性算法对样本的非线性特征进行线性分析成为可能; (4)它基于结构风险最小化理论之上在特征空间中建构最优分割超平面,使得学习器得到全局最优化,并且在整个样本空间的期望风险以某个概率满足一定上界。 学习SVM的读者还需要了解一下如何利用拉格朗日乘子法解决带有等式或不等式约束的规划问题。 基本的拉格朗日乘子法是求函数在某些约束条件下的极值问题。它的基本思想是引入一个新的的参数(拉格朗日乘子),将约束条件和原函数联系在一起,使能配成与变量相等的等式方程,从而求出得到原函数极值的各个变量的解。 当时,在机器学习的理论中出现了偏倚方差均衡、容量控制和避免拟合。 支持向量机是一种需要训练和测试的分类方法,支持向量机中的几个重要概念。核函数、惩罚函数、线性分类器、非线性分类器。 在上一章节中,我们介绍了mahout的user-based协调过滤算法,并通过淘宝的例子做了简单的分析。那我们可不可以自己写个MapReduce程序呢,来实现user-based算法。 先对算法进行介绍,在上一小节中,我们已经知道user-based推荐算法的思想是找出与该用户喜欢相似的用户,将相似用户的商品推荐给该用户。内部是怎么实现的呢?下面我们进行详细的讲解。 假设有A,B,C,D四个用户,有1,2,3,4,5五件商品。他们之间的购买关系见下表。 表一,user和item之间的关系表 1 2 3 4 5 A √ √ B √ √ √ C √ √ √ D √ √ √ 在user-based算法中我们分为3步进行。 (1)找出K个与该用户相似的用户。 (2)找出这些用户对应的商品集合,过滤掉用户已经有的商品,并计算它们与该用户的喜欢期望值。 (3)选取N商品个推荐给用户。 第一步:如何找出K个相似的用户呢?它们是用什么指标来衡量的呢?在业界有这样的标准,通过计算用户的行为相似度来计算用户的兴趣相似度。用户的兴趣相似度有多种计算方式,普遍使用的有Jaccard公式。给定的用户u和用户v,令N(u)表示用户u曾经用过正反馈的物品集合,令N(v)表示用户V曾经有过正反馈的物品集合。那么兴趣相似度的表达式为: 或者通过余弦相似度计算: 以上表为例,先将用户对应的商品列出来。见下表二。我们计算A,B之间的相似度。 表二、用户和商品的对应表 我们需要计算不同的用户之间的相似度然后求最大的K个。那我们怎么做才能方便地求出他们两两之间的相似度呢?首先将它们转化成一个个的对儿。 1 2 3 统计成矩阵 A B C D A 0 1 2 1 B 1 0 1 2 C 2 1 0 2 D 1 2 2 0 对角矩阵。计算没两个用户的相似度。 求top2 A -》BC B-》AD C-》AD 第二步,计算期望。用户u对商品i的喜爱程度。相似的期望 其中,S(u,k)包含和用户u兴趣最接近的K个用户,N(i)是对物品i有关行为的用户集合,是用户u和用户v的兴趣相似度,代表用户v对物品i的兴趣,因为使用的是单一行为的隐反馈数据,所以所有的=1. 对于用户A来说,求P(A,1)S(A,2)={B,C},N(i)={A,C},所以 ,同理P(A,3),S(A,2)={B,C},N(3)={B,D},所以 ,同理P(A,4),S(A,2)={B,C},N(4)={C,D},所以,同理P(A,5)=0,因此对用户A来说,首先将商品4推荐给他,再讲商品3推荐给他。 伪代码 第三步,找出top N 即可。 显然,我们可以使用矩阵来存储两个用户同时购买一件商品的行为,但是在实际的应用场景中,用户的数量非常大,每个用户买得商品又特别少,每个人与另外一个人同时购买某件商品的概率也非常小,造成这个矩阵是稀疏矩阵,如果采用这种结构,程序的时间复杂度和空间复杂度都是非常大的。因此,我们采用三元组的形式存储形如《A,B,AB》,表示AB同时出现的次数。 1)TOP N。在user-based CF中两个用到top N,第一次出现在求K个最相近的用户,第二次是求前N个要推荐给用户的商品。我们先申请N个大小的对象数据,将,这样算法的时间复杂度为i*K。 在第一步中又分为几小步。 (1)输入的数据是按照userId,ItemId,type排行的,其中我们上面的行为默认是是购买,显然这里是不行的,如何转换用户行为和用户喜欢度呢。 (2)Job1,将输入数据转化成图2, (3)Job2,将转换成图3 (4)Job3,输出结束,进入第二个阶段。 Mahout的搭建 为什么要安装mahout呢,能不能不安装直接当成jar包使用呢。 1、 下载二进制文件。解压到mahout的安装目录 2、 配置环境变量。 在/etc/profile添加mahout 的安装目录 3、 启动Hadoop来测试 4、 Mahout –help 5、 Mahout的实例测试 下载文件 Mahout随机森林 决策树生成的过程 第一步:生成Describe 使用Describe来生成。 什么叫做回归呢?回归就是根据已经公式对未知参数的估计。举个例子来说,房价和房子的大小、地段位置、交通状况等有关。我们知道了这个计算公式,这时知道一个房子的情况就可以通过这个公式进行估价。网上有两种说法,有的说LR是线性回归,有的说是非线性回归,这可能是对线性的理解不同。其实准确的理解是LR模型是一种对数线性模型,可以当成广义上的线性模型。在上面的房价的例子中,我们可以看出它的属性是相互独立的,也就是说在特性向量相互独立的情况下比较适合使用LR模型,这和贝叶斯比较相像。 回归按照自变量的多少可以分为简单回归和多元回归。简单回归就是只有一个自变量的回归,多元回归就是多个变量。因此简单回归是多元回归的一个特殊情况。按照Y的取值分可以分为确定型回归和概率型回归。确定型回归比较常见的形式是多元线性回归,概率型回归就是它求的不是确切的未知参数的值,而是归到哪一类中的概率。Logistic regression是一种概率型回归。根据回归图形来说分为线性回归和非线性回归。 多元线性回归模型中回归方程是 我们需要做的是通过这些样本数据求出。我们是通过最小二乘法求解这些系数。这样就将这个问题转化成了一个最优化问题。至于最小二乘法读者朋友们自行查找资料。 Logistic回归模型是一种概率模型,它以某一事件发生与否的概率P为因变量,以影响P的因素为自变量建立的回归模型,分析某时间发生的概率与自变量之间的关系,是一种非线性模型。为什么不用线性回归做二分类呢?因此因变量的取值仅为0或1,不符合正态分布;另外使用线性回归,Y的预测值可能超出0,1之外,不好解释结果。因此也有这么一种说法,LR是归一化的线性回归。 函数g是logistic 函数也叫做sigmoid函数。函数的曲线是。因为,所以叫做S曲线。对于一个样例,我们求出他们的归到相应类别的概率值。 综合上面两个公式,得 这样,我们求得整个数据集上的似然函数。使得上面的值最小求得系数。 和线性回归相似,LR回归分析的过程,就是根据样本,求出各自变量的回归系数。由于LR是一种概率模型,通常使用的是最大似然法。在mahout中使用的是随机梯度下降法。那我们主要介绍一下随机梯度下降方法。 我们看线性回归模型中如何通过随机下降法求最小二乘法的最优解。 我们程序也需要一个机制去评估我们θ是否比较好,所以说需要对我们做出的h函数进行评估,一般这个函数称为损失函数(loss function)或者错误函数(error function),描述h函数不好的程度,在下面,我们称这个函数为J函数 在这儿我们可以做出下面的一个错误函数: 这个错误估计函数是去对x(i)的估计值与真实值y(i)差的平方和作为错误估计函数,前面乘上的1/2是为了在求导的时候,这个系数就不见了。 如何调整θ以使得J(θ)取得最小值有很多方法,其中有最小二乘法(min square),是一种完全是数学描述的方法,在stanford机器学习开放课最后的部分会推导最小二乘法的公式的来源,这个来很多的机器学习和数学书 上都可以找到,这里就不提最小二乘法,而谈谈梯度下降法。 梯度下降法是按下面的流程进行的: 1)首先对θ赋值,这个值可以是随机的,也可以让θ是一个全零的向量。 2)改变θ的值,使得J(θ)按梯度下降的方向进行减少。 为了更清楚,给出下面的图: 这是一个表示参数θ与误差函数J(θ)的关系图,红色的部分是表示J(θ)有着比较高的取值,我们需要的是,能够让J(θ)的值尽量的低。也就是深蓝色的部分。θ0,θ1表示θ向量的两个维度。 在上面提到梯度下降法的第一步是给θ给一个初值,假设随机给的初值是在图上的十字点。 然后我们将θ按照梯度下降的方向进行调整,就会使得J(θ)往更低的方向进行变化,如图所示,算法的结束将是在θ下降到无法继续下降为止。 当然,可能梯度下降的最终点并非是全局最小点,可能是一个局部最小点,可能是下面的情况: 上面这张图就是描述的一个局部最小点,这是我们重新选择了一个初始点得到的,看来我们这个算法将会在很大的程度上被初始点的选择影响而陷入局部最小点 下面我将用一个例子描述一下梯度减少的过程,对于我们的函数J(θ)求偏导J: 下面是更新的过程,也就是θi会向着梯度最小的方向进行减少。θi表示更新之前的值,-后面的部分表示按梯度方向减少的量,α表示步长,也就是每次按照梯度减少的方向变化多少。 一个很重要的地方值得注意的是,梯度是有方向的,对于一个向量θ,每一维分量θi都可以求出一个梯度的方向,我们就可以找到一个整体的方向,在变化的时候,我们就朝着下降最多的方向进行变化就可以达到一个最小点,不管它是局部的还是全局的。 用更简单的数学语言进行描述步骤2)是这样的: 倒三角形表示梯度,按这种方式来表示,θi就不见了,看看用好向量和矩阵,真的会大大的简化数学的描述啊。 根据下面的算法流程图来说,随机梯度下降法是这么进行的:首先对w进行赋值,随机赋值也行,全部指定为0也行。循环改变g(k)的值 http://www.autonlab.org/autonweb/14709/version/4/part/5/data/komarek:lr_thesis.pdf?branch=main&language=en上面有对LR的详细介绍。 Logistic方程 Logistic函数起源于。是生物学家做生物繁殖是发现的方程。 他的函数图像是 当x为0时,P(x)为0.5。 数据集:donut.txt 由donut.csv转换而来的。在mahout源码中有。为了方便存储样本,我们将样本定义成一个类Instance,它包含两个属性。 public class Instance { public int label; public double[] x; } 算法实现的代码交给Logistic来完成。它主要包含下面几个函数。算法流程图是 public static void main(String... args) throws Exception { Logistic logistic = new Logistic(5); List logistic.train(instances); List for(int i=0;i logistic.InstanceClassify(testinstances.get(i)); } logistic.writeFile(testinstances, "donut_test.txt"); } } 为了便于学习,我们将mahout官网上的翻译下来。http://mahout.apache.org/users/classification/logistic-regression.html 在mahout中有三个package和SGD相关,它们分别是 The vector encoding package Org.apache.mahout.vectorizer.enconders The SGD learning package Org.apache.mahout.classifier.sgd The evolutionary optimization system org.apache.mahout.ep 特征向量编码 因为SGD算法需要有固定长度的特征向量,因为提前建立一个字典的话是非常麻烦的,大部分SGD使用哈希过的特征向量编码系统。 基本的想法是,你创建一个向量,一个典型的randomaccesssparsevector,然后你用各种特征编码器逐步增加特征矢量。矢量的大小应足够大,以避免碰撞的哈希特征特征。 有多种类型的数据,专业编码器。你通常可以编码的字符串表示的要编码值或你可以编码字节级表示避免字符串转换。在continuousvalueencoder和constantvalueencoder的情况下,它也可能编码一个空值,通过真正的价值为重。这避免了数值解析完全在你从一个系统如Avro得到你的训练数据。 下图是特性向量编码的类图。 SGD学习 最简单的应用程序,您可以构建一个onlinelogisticregression和跑。通常情况下,虽然,它是好有运行性能估计伸出的数据。这样做,你应该使用一个crossfoldlearner保持一个稳定的五(默认)onlinelogisticregression对象。每次你通过训练样本的一crossfoldlearner,通过这个例子只有一个孩子作为训练并通过实例对最后一个孩子来评价当前的性能。孩子们被用于在圆罗宾时尚的评价,如果你使用的是默认的5分,所有的孩子都拿到80%的训练数据获取评价数据20%。 为了避免配置学习率的讨厌的需要,正则化参数和退火的时间表,你可以使用adaptivelogisticregression。这类维护一池crossfoldlearners和适应的学习速率和正则化在飞,你不需要。 这里是为classifiers.sgd包类图。正如你所看到的,twiddlable旋钮的数量是相当大的。一些例子,看trainnewsgroups实例代码。 在mahout 中使用逻辑回归的主要两个类是 原始数据的格式 进入mahout的bin目录 参数说明: input 输入数据 output 输出模型文件 target 预测的变量 输入数据要求第一行为变量名称 categories 预测变量的取值个数 predictors 参与建模的变量 types 预测变量的类型 Number、Word、text其中一个,如果是一样的,使用一个就可以 pass 训练时对输入数据测试的次数 feature 内部随机向量维度 rate 学习速率 如果输入数据大可以设置大一些 模型评估命令 ./mahout runlogistic --input /data/mahout-data/donut-test.csv --model /data/mahout-output/model2 --scores --auc --confusion input 测试数据 model 模型文件 scores 打印预测值和原始值的对比 auc 打印auc值 越接近1越好 confusion 打印模糊矩阵 写程序运行 使用 Mahout的数据集是 MapReduce实现 各类分类算法比较 神经网络 SVM 决策树 逻辑回归 朴素贝叶斯 支持多分类 线性可分 并行化 Mahout实现 1.1、贝叶斯的原理 1.1.1、条件概率、全概率和贝叶斯公式 在概率论中,我们将事件A发生的概率记做P(A)。但是有的情况下,我们需要考虑在事件B发生的概率下,事件A发生的概率,这种情况下记做P(A|B)。 u 条件概率公式 设A,B事件是两个事件,且P(B)> 0,那么 为在事件B发生的情况下,A发生的条件概率。 在现实生活中,遇到的问题往往不是这么简单的。但是我们可以将复杂的问题划分成若干个简单的问题。 u 定义划分 设随机试验E的样本空间为是样本空间S的一组事件,如果满足下面两个条件,就称为样本空间S的一个划分。 1)两两互不相容。 2)。 u 全概率公式 设随机试验E的样本空间S,是样本空间S的一个划分,则对于任意事件A,有 该公式称为全概率公式。该公式的证明非常简单,请自行证明。 根据条件概率和全概率公式很容易就能推到出贝叶斯公式。 u 贝叶斯公式 设随机试验E的样本空间为S,是样本S的一个划分,A是一事件,且,则有 证明:由条件概率和全概率公式推导而来, 。 1.1.2、贝叶斯原理 贝叶斯公式有着很重要的现实意义。比如,现在要判断一个学生是南京仙林大学城那个学校的,设候选的学校为,即是不相容的。我们凭借以往的经验估计出他是各个学校的概率,这通常称为先验概率。进一步考虑这个人的专业,比如师范类的专业还是理工类的专业,在专业确定下,他是各个学校的概率,这个概率是由贝叶斯公式计算得到的,通常称为是后验概率。根据后验概率,我们就能更准确地判断这个学生的所在学校了。 上面的例子可以看出一个分类的小问题,从上面的示例中可以看出,贝叶斯分类器的算法的关键是求出先验概率和后验概率。理解贝叶斯其实不难,举个例子来说。在仙林大学城,南邮的男生比例很大,同时南财的女生比例很大。这时,你看见一群女生走在街上,你的判断肯定是她们是南财的概率大。于是你将她们分到了南财那一类。贝叶斯就是根据结果来判断概率的方法。 2.2、几种贝叶斯算法 2.2.1、朴素贝叶斯 朴素贝叶斯(Naive Bayesian Classifier,NBC)是贝叶斯算法中最简单的算法,朴素贝叶斯之所以叫做“朴素”贝叶斯是因为它完全按照贝叶斯计算公式来分类,假设各个属性是条件独立的,互不影响、互不依赖。朴素贝叶斯虽然简单,但是其性能可以达到神经网络,决策树的水平。是文本分类中最常用的算法之一。 在使用朴素贝叶斯进行分类的时候,需要特别注意,它所选取的特征向量是相互独立的。因为,这里要满足划分定义的第一个条件。在实际应用中,往往达不到这个要求,这些变量之间往往有着连续,因此在进行数据挖掘之前可以进行特征向量的选取。这个条件好像是限制了朴素贝叶斯的使用范围,但是由于特性向量的压缩,减少了计算量,降低了构建贝叶斯网络的复杂性。使得贝叶斯分类器应用到更广的领域中。 在贝叶斯原理的介绍中,我们已经知道求解贝叶斯分类问题必不可少的步骤是先验概率和后验概率的计算。 朴素贝叶斯分类模型原理: 1)每个数据样本用一个n维特征向量表示,分别描述对n个属性样本的n个度量。 2)假定有m个类,给定一个未知的数据样本X(即没有类标号),分类法将预测X,属于具有最高后验概率(条件X下)的类。也就是说,朴素贝叶斯分类将未知的样本分类给类,当且仅当 就是求最大的后验概率。根据贝叶斯定量得到: 3)根据P(X)对于所有类为常数,只需要最大即可。如果类的先验概率未知,假设这些类是等概率的。先验概率可以用得到。其中是类中的训练样本数,而是训练样本总数。 4)计算,在计算这个值的时,如果给定具有许多属性的数据集,计算的开销会很大。为了节省开销,可以坐类条件独立的朴素假设。给定样本的类标号,假设该属性值相互独立,即在属性间,不存在依赖关系。这样的话, 概率可以由训练样本估值。 (a)如果是分类属性,则,其中是在属性上具有值的类的训练样本数,而是中的训练样本数。 (b)如果是连续属性值,则通过假设该属性服从高斯分布。 从上面我们可以总结朴素贝叶斯分类的步骤 1)求每个属性值的先验概率。 2)求最大后验概率。 朴素贝叶斯分类模型的特点: 1)算法逻辑简单,易于理解和实现。 2)分类过程中时间负责度和空间复杂度在可控范围内。 3)算法性能好。 尽管朴素贝叶斯模型有如此优点,但是它的局限性也是有的。注意我们假设各个属性之间是条件独立分布的,在实际应用中,这个往往是不能实现的。改进的贝叶斯也是解决这个问题的。 2.2.2、改进的贝叶斯算法 改进的贝叶斯分类算法是为了解决贝叶斯分类的独立假设条件。目前比较有名的算法有半朴素贝叶斯、选择贝叶斯、树增广朴素贝叶斯、贝叶斯网络等。 半朴素贝叶斯的思想是将相互依赖的属性放在一个属性集合中,形成一个新的属性,其他的运算跟朴素贝叶斯相同。 下面的图可以作为对比。 图一、朴素贝叶斯分类器 图二、半朴素贝叶斯 选择贝叶斯的思想是选择属性集的子集作为属性集,其他的和朴素贝叶斯一样。由此可见选择贝叶斯和半朴素贝叶斯有异曲同工之妙。都是使得每个属性是条件独立的。不过半朴素贝叶斯是全体属性,选择贝叶斯是局部代替整体。 图三、选择贝叶斯 上面两种方式都解决了独立性假设问题,但是并不是说,它们的分类准确性一定比朴素贝叶斯要好。对于半朴素贝叶斯来说,它构造了一个更复杂的训练数据集。表面上来看,系统越复杂,准确度越高,但是实际中复杂的系统会产生一个过分拟合的问题,即适用性不高,在分类器学习训练时性能很好,但是用到一个实例时,结果却不理想。另一个,算法的时间复杂度和空间复杂度也会相应的增加。对于选择贝叶斯来说,同样也存在相应的问题。子集的选择就是一个问题,如何保证子集选择的正确性是关键。 除了选择贝叶斯和半朴素贝叶斯,还有树增广朴素贝叶斯。树增广朴素贝叶斯的基本思想是:在朴素贝叶斯分类器的基础上,在属性之间增加连接弧线,以消除朴素贝叶斯分类器的条件独立假设。我们将这个弧线叫做扩展弧。如下图。 图四 树增广朴素贝叶斯 2.2.3、并行化的贝叶斯 随着海量数据的发展,大数据的处理也已经成为迫在眉睫的事情。大数据在单机内存处理已经远远不能满足现在的计算和时间需求了。 从朴素贝叶斯的算法来看,朴素贝叶斯的时间大多消耗在各个属性值的先验概率的计算上,而这些计算是可以分来的,因此我们可以将这一阶段进行并行化处理。 目前,Hadoop云计算的发展,为并行化计算带来了巨大的方便,已有人实现了基于Hadoop Mapreduce的朴素贝叶斯文本分类器。 Java实现本文分类 程序设计思路 将 哪些可以是并发的呢?计算每个 数据说明:csv文件格式,需要对这个文件格式的数据进行解析,现在有很多类似的包 程序目标: 设计思路: 辅助类设计: DataVector.java是一个文件中每行数据转化成一个DataVector,从上面可以看出,每个属性需要单独处理,因此同样需要将它抽象成一个数据结构。 选择的数据结构 算法流程: 伪代码: 关键代码: 试验结果: 结果分析:在程序设计中,所有的计算是基于内存的,当数据量很大时可能出现内存溢出。 使用基于Hadoop的实现能很好的解决这个问题。 基于Hadoop的朴素贝叶斯实现 Age income housing martial y 序号 20-30岁 <=5000 yes no no N1 20-30岁 <=5000 no no no N2 >50岁 >5000 yes yes no N3 30-50岁 <=5000 yes yes yes N4 30-50岁 >5000 yes yes yes N5 30-50岁 >5000 no no no N6 >50岁 >5000 no yes no N7 20-30岁 <=5000 yes no no N8 20-30岁 >5000 yes no yes N9 30-50岁 >5000 yes yes yes N10 30-50岁 >5000 no no yes N11 30-50岁 <=5000 no yes yes N12 >50岁 >5000 yes yes no N13 >50岁 <=5000 yes yes no N14 假设节点1上出现的 20-30岁 <=5000 yes no no N1 20-30岁 <=5000 no no no N2 >50岁 >5000 yes yes no N3 节点2上出现的 30-50岁 <=5000 yes yes yes N4 30-50岁 >5000 yes yes yes N5 30-50岁 >5000 no no no N6 节点3上出现的 >50岁 >5000 no yes no N7 20-30岁 <=5000 yes no no N8 20-30岁 >5000 yes no yes N9 在节点1上进行的数据转换是 Map阶段 Map<<20-30,no>2> Key value 0,20-30 ,no 1 1,《=5000,no 1 2,yes,no 1 3,no,no 1 0,20-30,no 1 1,<=5000,no 1 2,no,no 1 3,no,no 1 0,>50岁,no 1 1,>5000,no 1 2,yes,no 1 3,yes,no 1 节点2的Map阶段输出结果 Key value 0,30-50 ,yes 1 1,《=5000,yes 1 2,yes,yes 1 3,yes,yes 1 0,30-50,yes 1 1,>5000,yes 1 2,yes,yes 1 3,yes,yes 1 0,30-50岁,no 1 1,>5000,no 1 2,no,no 1 3,no,no 1 节点3的Map阶段输出结果 Key value 0,>50 ,no 1 1,>5000,no 1 2,no,no 1 3,yes,no 1 0,20-30,no 1 1,<=5000,no 1 2,yes,no 1 3,no,no 1 0,20-30岁,yes 1 1,>5000,yes 1 2,yes,yes 1 3,no,yes 1 Combiner阶段,在combiner阶段是按顺序排序了 Key value 0,20-30 ,no 2 0,>50岁,no 1 1,<=5000,no 2 1,>5000,no 1 2,no,no 1 2,yes,no 2 3,no,no 2 3,yes,no 1 Reduce阶段 Key value 0,30-50 ,no 1 0,30-50,yes 2 1,<=5000,yes 1 1,>5000,yes 1 1,>5000,no 1 2,no,no 1 2,yes,yes 2 3,no,no 1 3,yes,yes 2 Key value 0,20-30 ,no 1 0,20-30,yes 1 0,>50,no 1 1,<=5000,no 1 1,>5000,no 1 1,>5000,yes 1 2,no,no 1 2,yes,no 1 2,yes,yes 1 3,no,no 1 3,no,yes 1 3,yes,no 1 Reduce Key value 0,20-30 ,no 1 0,20-30,yes 1 0,>50,no 1 1,<=5000,no 1 1,>5000,no 1 1,>5000,yes 1 2,no,no 1 2,yes,no 1 2,yes,yes 1 3,no,no 1 3,no,yes 1 3,yes,no 1 接着进行分类器的设计即可,中间结果写文件、但是还有读到内存中进行处理,这时该怎么办,是要写新的类,进行处理吗。如何灵活的运用,二进制流的方法进行读写。 将中间结果写入文件,再次做出判断的时候需要将中间文件结果读入内存进行处理。 Hadoop的二进制流读写需要实现那些接口,进行那些处理。 1,先进行简单的处理,中间结果再使用辅助数据结构 2,先使用辅助数据结构然后进行二进制流读写,为了练习二进制流,使用第二种方案。 http://mahout.apache.org/users/classification/bayesian.html网址上记录着朴素贝叶斯的简单应用,我们试试看。 在命令行下使用贝叶斯。在前面的mahout介绍中,我们知道mahout提供一些cli命令行来进行数据的预处理、模型训练和数据集的测试。Mahout中有一个对文本进行分类的例子。 预处理:通过下面的命令将序列化的数据集处理成TF-IDF的词频对。命令如下。参数这-i 表示input,指的是文件的输入路径;-o 表示output,指的是TF-IDF的输出路径;-nv 表示namedVector 指的是输出的向量是否是重写过的向量,选项是true或者false。还有其他的参数,这里就不再一一赘述。读者朋友们可以参考mahout的官网。 Mahout seq2sparse -i ${PATH_TO_SEQUENCE_FILES} -o ${PATH_TO_TFIDF_VERCTORS} -nv -n 2 -wt tfidf 训练:预处理以后使用trainnb训练分类模型。默认情况下使用的是朴素贝叶斯模型,如果使用-c 参数指定的是CBayes模型。 Mahout trainnb -i ${PATH_TO_TFIDF_VERCTORS} -el -o ${PATH_TO_MODEL}/model -li ${PATH_TO_MODEL}/labelindex -ow -c 标签分配/测试:标签分配和测试可以通过mahout的testnb来实现。与上面的一样,-c 表示模型是CBayes。-seq 表示mahout testnb是序列化的。 Mahout testnb -i ${PATH_TO_TFIDF_TEST_VECTORS} -m ${PATH_TO_MODEL}/model -l ${PATH_TO_MODEL}/labelindex -ow -c -seq 接着看http://mahout.apache.org/users/classification/twenty-newsgroups.html上面的做法和讲解。 先确定mahout已经能够正常使用。运行mahout中的20 newsgroups example脚本。需要注意的是如果不是root用户,需要使用sudo命令。 $ ./example/bin/classify-20newsgroups.sh 运行完成以后,你将会看到下面的页面选项。 1.Complement Naive Bayes 2.Naive Bayes 3.Stochastic Gradient Descent 选1的话,将会执行下面的操作。 1、为数据集合所有的输入输出数据创建一个工作目录。 2、下载数据集到第一步创建的目录中。 3、将数据集转换成 4、预处理第三步得到的数据将其变成 5、将处理好的数据集分成训练集和测试集。 6、训练分类器 7、测试分类器 运行完成以后,输出的文件是这样的。 使用CBayes为20 newsgroup建立模型。 1、为数据集创建一个工作目录 $export WORK_DIR=/tmp/mahout-work-${user} $mkdir -p ${WORK_DIR} 2、下载20new-bydata.tar.gz,解压后copy到工作目录 $curl http://people.csail.mit.edu/jrennie/20Newsgroups/20news-bydata.tar.gz -o ${WORK_DIR}/20news-bydata.tar.gz $mkdir -p ${WORK_DIR}/20news-bydata $cd ${WORK_DIR}/20news-bydate && tar zxf ../20news-bydate.tar.gz && cd .. && cd .. $mkdir ${WORK_DIR}/20news-all $cp -R ${WORK_DIR}/20news-bydate/*/* ${WORK_DIR}/20news-all 如果在Hadoop的集群上运行,将文件上传到集群上。 $hadoop dfs -put ${WORK_DIR}/20news-all ${WORK_DIR}/20news-all 3、转换数据集成 $mahout seqdirectory -i ${WORK_DIR}/20news-all -o ${WORK_DIR}/20news-seq -ow 4、预处理第三步得到的数据将其变成 $ mahout seq2spare -i ${WORK_DIR}/20news-seq -o ${WORK_DIR}/20news-vectors -lnorm -nv -wt tfidf 5、将处理好的数据集分成训练集和测试集。 $ mahout split -i ${WORK_DIR}/20news-vectors/tfidf-vectors --trainingOutput ${WORK_DIR}/20news-train-vectors --testOutput ${WORK_DIR}/20news-test-vectors --randomSelectionPct 40 --overwrite --sequenceFiles -xm sequential 6、训练分类器 $mahout trainnb -i ${WORK_DIR}/20news-train-vectors -el -o ${WORK_DIR}/model -li ${WORK_DIR}/labelindex -ow -c 7、测试分类器 $mahout testnb -i ${WORK_DIR}/20news-test-vectors -m ${WORK_DIR}/model -l ${WORK_DIR}/labelindex -ow -o ${WORK_DIR}/20news-testing -c 不难发现,上面所做的事情就是classify-20newsgroup所做的事情,因此学习shell编程还是非常有必要的。 基于hadoop的K-means实现
算法思路
代码实现
结果分析
算法性能分析和评价
Mahout中对k-means的支持
如何读源码之Kmeans算法
层次聚类
合并层次聚类的步骤
KNN算法
KNN和K-means的区别
基于java的KNN实现
数据描述
算法思路
伪代码
关键代码
KNN的MapReduce
关联规则算法
1.1关联规则的引入
1.2关联规则的概念
1.3关联规则的算法Apriori
1.3.1求频繁项集的算法流程图
1.3.2求频繁项集的伪代码
1.3.3求频繁项集的java算法实现
1.3.4 求关联规则的算法流程图
1.3.5 求强关联规则的伪代码
1.3.6 求强关联规则的java实现
基于MapReduce的apriori
算法设计
生成频繁项集的阶段
FP-Tree算法
算法原理
FP-Tree算法的Java实现
FP-Tree求频繁项集的算法流程图
1.3.2求FP-tree的伪代码
1.3.3求频繁项集的java算法实现
FP-Tree算法的MR实现
Mahout中FP-Tree的实现
决策树
ID3算法
算法理论
Java程序实现ID3算法
Mapreduce程序实现ID3算法
ID3算法总结
ID4.5算法
在mahout中使用随机森林
Mahout决策树源码解析
随机森林与GBRT
支持向量机
最优超平面
软间隔(线性不可分)
核函数(非线性)
LIBSVM
本节小结
自己动手写推荐算法
程序设计
一、数据结构
二、算法
Mahout案例 电影推荐系统
逻辑回归模型
Java实现的Logistic regression
Mahout源码解析
贝叶斯
Java实现朴素贝叶斯
Mahout实现的朴素贝叶斯以及他们的应用
本节小结