机器学习理论与实战

前面的近20篇博文已经牵扯到很多机器学习算法咯,已经吊足了胃口,决定从后面开始正式系统的学习机器学习理论,并尝试进入实战阶段,涵盖:

加州理工(caltech)的 Yaser Abu-Mostafa教授的机器学习,偏重于传统统计理论

斯坦福大学(Staford U)的Andrew Ng教授的机器学习,偏重于实用,直观理解

多伦多大学(Tronto U)的Geoffery Hinton教授的高级神经网络,偏重于神经网络和深度学习

斯坦福大学(Staford U)的Daphne Koller教授的概率图模型,偏重于推理和结构化学习

上面的作为理论指导,以《机器学习实战》(machine learning in action)作为主线,机器学习实战更适合码农,因此这个实用性不言而喻。


机器学习理论与实战(一)K近邻法

  机器学习分两大类,有监督学习(supervised learning)和无监督学习(unsupervised learning)。有监督学习又可分两类:分类(classification.)和回归(regression),分类的任务就是把一个样本划为某个已知类别,每个样本的类别信息在训练时需要给定,比如人脸识别、行为识别、目标检测等都属于分类。回归的任务则是预测一个数值,比如给定房屋市场的数据(面积,位置等样本信息)来预测房价走势。而无监督学习也可以成两类:聚类(clustering)和密度估计(density estimation),聚类则是把一堆数据聚成弱干组,没有类别信息;密度估计则是估计一堆数据的统计参数信息来描述数据,比如深度学习的RBM。

          根据机器学习实战讲解顺序,先学习K近邻法(K Nearest Neighbors-KNN)

          K近邻法是有监督学习方法,原理很简单,假设我们有一堆分好类的样本数据,分好类表示每个样本都一个对应的已知类标签,当来一个测试样本要我们判断它的类别是,就分别计算到每个样本的距离,然后选取离测试样本最近的前K个样本的标签累计投票,得票数最多的那个标签就为测试样本的标签。

例子(电影分类):


(图一)

(图一)中横坐标表示一部电影中的打斗统计个数,纵坐标表示接吻次数。我们要对(图一)中的问号这部电影进行分类,其他几部电影的统计数据和类别如(图二)所示:


(图二)

从(图二)中可以看出有三部电影的类别是Romance,有三部电影的类别是Action,那如何判断问号表示的这部电影的类别?根据KNN原理,我们需要在(图一)所示的坐标系中计算问号到所有其他电影之间的距离。计算出的欧式距离如(图三)所示:

(图三)

     由于我们的标签只有两类,那假设我们选K=6/2=3,由于前三个距离最近的电影都是Romance,那么问号表示的电影被判定为Romance。

代码实战(Python版本):

先来看看KNN的实现:

[python]  view plain copy
  1. from numpy import *  
  2. import operator  
  3. from os import listdir  
  4.   
  5.   
  6. def classify0(inX, dataSet, labels, k):  
  7.     dataSetSize = dataSet.shape[0]    #获取一条样本大小  
  8.     diffMat = tile(inX, (dataSetSize,1)) - dataSet  #计算距离  
  9.     sqDiffMat = diffMat**2    #计算距离  
  10.     sqDistances = sqDiffMat.sum(axis=1)   #计算距离  
  11.     distances = sqDistances**0.5   #计算距离  
  12.     sortedDistIndicies = distances.argsort()  #距离排序  
  13.     classCount={}            
  14.     for i in range(k):  
  15.         voteIlabel = labels[sortedDistIndicies[i]]    #前K个距离最近的投票统计  
  16.         classCount[voteIlabel] = classCount.get(voteIlabel,0) + 1  #前K个距离最近的投票统计  
  17.     sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)  #对投票统计进行排序  
  18.     return sortedClassCount[0][0]   #返回最高投票的类别  

下面取一些样本测试KNN:

[python]  view plain copy
  1. def file2matrix(filename):  
  2.     fr = open(filename)  
  3.     numberOfLines = len(fr.readlines())         #get the number of lines in the file  
  4.     returnMat = zeros((numberOfLines,3))        #prepare matrix to return  
  5.     classLabelVector = []                       #prepare labels return     
  6.     fr = open(filename)  
  7.     index = 0  
  8.     for line in fr.readlines():  
  9.         line = line.strip()  
  10.         listFromLine = line.split('\t')  
  11.         returnMat[index,:] = listFromLine[0:3]  
  12.         classLabelVector.append(int(listFromLine[-1]))  
  13.         index += 1  
  14.     return returnMat,classLabelVector  
  15.       
  16. def autoNorm(dataSet):  
  17.     minVals = dataSet.min(0)  
  18.     maxVals = dataSet.max(0)  
  19.     ranges = maxVals - minVals  
  20.     normDataSet = zeros(shape(dataSet))  
  21.     m = dataSet.shape[0]  
  22.     normDataSet = dataSet - tile(minVals, (m,1))  
  23.     normDataSet = normDataSet/tile(ranges, (m,1))   #element wise divide  
  24.     return normDataSet, ranges, minVals  
  25.      
  26. def datingClassTest():  
  27.     hoRatio = 0.50      #hold out 50%  
  28.     datingDataMat,datingLabels = file2matrix('datingTestSet2.txt')       #load data setfrom file  
  29.     normMat, ranges, minVals = autoNorm(datingDataMat)  
  30.     m = normMat.shape[0]  
  31.     numTestVecs = int(m*hoRatio)  
  32.     errorCount = 0.0  
  33.     for i in range(numTestVecs):  
  34.         classifierResult = classify0(normMat[i,:],normMat[numTestVecs:m,:],datingLabels[numTestVecs:m],3)  
  35.         print "the classifier came back with: %d, the real answer is: %d" % (classifierResult, datingLabels[i])  
  36.         if (classifierResult != datingLabels[i]): errorCount += 1.0  
  37.     print "the total error rate is: %f" % (errorCount/float(numTestVecs))  
  38.     print errorCount  

上面的代码中第一个函数从文本文件中读取样本数据,第二个函数把样本归一化,归一化的好处就是降低样本不同特征之间数值量级对距离计算的显著性影响

datingClassTest则是对KNN测试,留了一半数据进行测试,文本文件中的每条数据都有标签,这样可以计算错误率,运行的错误率为:the total error rate is: 0.064000


总结:

优点:高精度,对离群点不敏感,对数据不需要假设模型

缺点:判定时计算量太大,需要大量的内存

工作方式:数值或者类别


下面挑选一步样本数据发出来:




参考文献:machine learning in action

转载请注明来源:http://blog.csdn.net/cuoqu/article/details/9255377


 

机器学习理论与实战(二)决策树


  决策树也是有监督机器学习方法。 电影《无耻混蛋》里有一幕游戏,在德军小酒馆里有几个人在玩20问题游戏,游戏规则是一个设迷者在纸牌中抽出一个目标(可以是人,也可以是物),而猜谜者可以提问题,设迷者只能回答是或者不是,在几个问题(最多二十个问题)之后,猜谜者通过逐步缩小范围就准确的找到了答案。这就类似于决策树的工作原理。(图一)是一个判断邮件类别的工作方式,可以看出判别方法很简单,基本都是阈值判断,关键是如何构建决策树,也就是如何训练一个决策树。


(图一)

构建决策树的伪代码如下:

Check if every item in the dataset is in the same class:

       If so return the class label

       Else

             find the best feature to split the data

             split the dataset

             create a branch node

             for each split

                    call create Branch and add the result to the branch node

            return branch node

         原则只有一个,尽量使得每个节点的样本标签尽可能少,注意上面伪代码中一句说:find the best feature to split the data,那么如何find thebest feature?一般有个准则就是尽量使得分支之后节点的类别纯一些,也就是分的准确一些。如(图二)中所示,从海洋中捞取的5个动物,我们要判断他们是否是鱼,先用哪个特征?


(图二)

         

   为了提高识别精度,我们是先用“能否在陆地存活”还是“是否有蹼”来判断?我们必须要有一个衡量准则,常用的有信息论、基尼纯度等,这里使用前者。我们的目标就是选择使得分割后数据集的标签信息增益最大的那个特征,信息增益就是原始数据集标签基熵减去分割后的数据集标签熵,换句话说,信息增益大就是熵变小,使得数据集更有序。熵的计算如(公式一)所示:

(公式一)

       有了指导原则,那就进入代码实战阶段,先来看看熵的计算代码:

[python]  view plain copy
  1. def calcShannonEnt(dataSet):  
  2.     numEntries = len(dataSet)  
  3.     labelCounts = {}  
  4.     for featVec in dataSet: #the the number of unique elements and their occurance  
  5.         currentLabel = featVec[-1]  
  6.         if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0  
  7.         labelCounts[currentLabel] += 1  #收集所有类别的数目,创建字典  
  8.     shannonEnt = 0.0  
  9.     for key in labelCounts:  
  10.         prob = float(labelCounts[key])/numEntries  
  11.         shannonEnt -= prob * log(prob,2#log base 2  计算熵  
  12.     return shannonEnt  

         有了熵的计算代码,接下来看依照信息增益变大的原则选择特征的代码:


[python]  view plain copy
  1. def splitDataSet(dataSet, axis, value):  
  2.     retDataSet = []  
  3.     for featVec in dataSet:  
  4.         if featVec[axis] == value:  
  5.             reducedFeatVec = featVec[:axis]     #chop out axis used for splitting  
  6.             reducedFeatVec.extend(featVec[axis+1:])  
  7.             retDataSet.append(reducedFeatVec)  
  8.     return retDataSet  
  9.       
  10. def chooseBestFeatureToSplit(dataSet):  
  11.     numFeatures = len(dataSet[0]) - 1      #the last column is used for the labels  
  12.     baseEntropy = calcShannonEnt(dataSet)  
  13.     bestInfoGain = 0.0; bestFeature = -1  
  14.     for i in range(numFeatures):        #iterate over all the features  
  15.         featList = [example[i] for example in dataSet]#create a list of all the examples of this feature  
  16.         uniqueVals = set(featList)       #get a set of unique values  
  17.         newEntropy = 0.0  
  18.         for value in uniqueVals:  
  19.             subDataSet = splitDataSet(dataSet, i, value)  
  20.             prob = len(subDataSet)/float(len(dataSet))  
  21.             newEntropy += prob * calcShannonEnt(subDataSet)       
  22.         infoGain = baseEntropy - newEntropy     #calculate the info gain; ie reduction in entropy  
  23.         if (infoGain > bestInfoGain):       #compare this to the best gain so far    #选择信息增益最大的代码在此  
  24.             bestInfoGain = infoGain         #if better than current best, set to best  
  25.             bestFeature = i  
  26.     return bestFeature                      #returns an integer  

        从最后一个if可以看出,选择使得信息增益最大的特征作为分割特征,现在有了特征分割准则,继续进入一下个环节,如何构建决策树,其实就是依照最上面的伪代码写下去,采用递归的思想依次分割下去,直到执行完成就构建了决策树。代码如下:

[python]  view plain copy
  1. def majorityCnt(classList):  
  2.     classCount={}  
  3.     for vote in classList:  
  4.         if vote not in classCount.keys(): classCount[vote] = 0  
  5.         classCount[vote] += 1  
  6.     sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)  
  7.     return sortedClassCount[0][0]  
  8.   
  9. def createTree(dataSet,labels):  
  10.     classList = [example[-1for example in dataSet]  
  11.     if classList.count(classList[0]) == len(classList):   
  12.         return classList[0]#stop splitting when all of the classes are equal  
  13.     if len(dataSet[0]) == 1#stop splitting when there are no more features in dataSet  
  14.         return majorityCnt(classList)  
  15.     bestFeat = chooseBestFeatureToSplit(dataSet)  
  16.     bestFeatLabel = labels[bestFeat]  
  17.     myTree = {bestFeatLabel:{}}  
  18.     del(labels[bestFeat])  
  19.     featValues = [example[bestFeat] for example in dataSet]  
  20.     uniqueVals = set(featValues)  
  21.     for value in uniqueVals:  
  22.         subLabels = labels[:]       #copy all of labels, so trees don't mess up existing labels  
  23.         myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)  
  24.     return myTree                          

      用图二的样本构建的决策树如(图三)所示:


(图三)

有了决策树,就可以用它做分类咯,分类代码如下:

[python]  view plain copy
  1. def classify(inputTree,featLabels,testVec):  
  2.     firstStr = inputTree.keys()[0]  
  3.     secondDict = inputTree[firstStr]  
  4.     featIndex = featLabels.index(firstStr)  
  5.     key = testVec[featIndex]  
  6.     valueOfFeat = secondDict[key]  
  7.     if isinstance(valueOfFeat, dict):   
  8.         classLabel = classify(valueOfFeat, featLabels, testVec)  
  9.     else: classLabel = valueOfFeat  
  10.     return classLabel  

最后给出序列化决策树(把决策树模型保存在硬盘上)的代码:

[python]  view plain copy
  1. def storeTree(inputTree,filename):  
  2.     import pickle  
  3.     fw = open(filename,'w')  
  4.     pickle.dump(inputTree,fw)  
  5.     fw.close()  
  6.       
  7. def grabTree(filename):  
  8.     import pickle  
  9.     fr = open(filename)  
  10.     return pickle.load(fr)  
  11.       

优点:检测速度快

缺点:容易过拟合,可以采用修剪的方式来尽量避免

参考文献:machine learning in action


  贝叶斯决策一直很有争议,今年是贝叶斯250周年,历经沉浮,今天它的应用又开始逐渐活跃,有兴趣的可以看看斯坦福Brad Efron大师对其的反思,两篇文章:“Bayes'Theorem in the 21st Century”和“A250-YEAR ARGUMENT:BELIEF, BEHAVIOR, AND THE BOOTSTRAP”。俺就不参合这事了,下面来看看朴素贝叶斯分类器。

         有时我们想知道给定一个样本时,它属于每个类别的概率是多少,即P(Ci|X),Ci表示类别,X表示测试样本,有了概率后我们可以选择最大的概率的类别。要求这个概率要用经典贝叶斯公式,如(公式一)所示:


(公式一)

         (公式一)中的右边每项一般都是可以计算出的,例如(图一)中两个桶中分别装了黑色(Black)和灰色(Grey)的球。

(图一)

        假设Bucket A和BucketB是类别,C1和C2,当给定一个球时,我们想判断它最可能从哪个桶里出来的,换句话说是什么类别?这就可以根据(公式一)来算,(公式一)的右边部分的每项都可以计算出来,比如P(gray|bucketA)=2/4,P(gray|bucketB)=1/3,更严格的计算方法是:

        P(gray|bucketB) = P(gray andbucketB)/P(bucketB),

        而P(gray and bucketB) = 1/7,P(bucketB)= 3/7

        那么P(gray|bucketB)=P(gray and bucketB)/ P(bucketB)=(1/7)/(3/7)=1/3

      这就是朴素贝叶斯的原理,根据后验概率来判断,选择P(Ci|X)最大的作为X的类别Ci,另外朴素贝叶斯只所以被称为朴素的原因是,它假设了特征之间都是独立的,如(图二)所示:

(图二)

       尽管这个假设很不严密,但是在实际应用中它仍然很有效果,比如文本分类,下面就来看下文本分类实战,判断聊天信息是否是辱骂(abusive)信息(也就是类别为两类,是否辱骂信息),在此之前,先强调下,朴素贝叶斯的特征向量可以是多维的,上面的公式是一维的,二维的如(公式二)所示,都是相同的计算方法:

(公式二)

       对文本分类,首先的任务就是把文本转成数字向量,也就是提取特征。特征可以说某个关键字在文章中出现的次数(bag of words),比如垃圾邮件中经常出现“公司”,“酬宾”等字样,特征多样,可以根据所需自己建立特征。本例子中采用标记字(token)的方法,标记字可以是任何字符的组合,比如URL,单词,IP地址等,当然判断是否是辱骂信息大多数都是类似于单词的形式。下面来根据代码说下:

首先我们获取一些训练集:

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet():  
  4.     postingList=[['my''dog''has''flea''problems''help''please'],  
  5.                  ['maybe''not''take''him''to''dog''park''stupid'],  
  6.                  ['my''dalmation''is''so''cute''I''love''him'],  
  7.                  ['stop''posting''stupid''worthless''garbage'],  
  8.                  ['mr''licks''ate''my''steak''how''to''stop''him'],  
  9.                  ['quit''buying''worthless''dog''food''stupid']]  
  10.     classVec = [0,1,0,1,0,1]    #1 is abusive, 0 not  
  11.     return postingList,classVec  

        训练集是从聊天室里摘取的6句话,每句话都有一个标签0或者1,表示是否是辱骂信息(abusive or not abusive)。当然可以把每个消息看成是一个文档,只不过文档单词比这个多,但是一样的道理。接下来处理训练集,看看训练集有多少个不同的(唯一的)单词组成。代码如下:

[python]  view plain copy
  1. def createVocabList(dataSet):  
  2.     vocabSet = set([])  #create empty set  
  3.     for document in dataSet:  
  4.         vocabSet = vocabSet | set(document) #union of the two sets  
  5.     return list(vocabSet)  

        该函数返回一个由唯一单词组成的词汇表。接下来就是特征处理的关键步骤,同样先贴代码:

[python]  view plain copy
  1. def setOfWords2Vec(vocabList, inputSet):  
  2.     returnVec = [0]*len(vocabList)  
  3.     for word in inputSet:  
  4.         if word in vocabList:  
  5.             returnVec[vocabList.index(word)] = 1  
  6.         elseprint "the word: %s is not in my Vocabulary!" % word  
  7.     return returnVec  

        这个函数功能:输入词汇表和消息,通过逐个索引词汇表,然后看消息中的是否有对应的字在词汇表中,如果有就标记1,没有就标记0,这样就把每条消息都转成了和词汇表一样长度的有0和1组成的特征向量,如(图三)所示:


(图三)

        有了特征向量,我们就可以训练朴素贝叶斯分类器了,其实就是计算(公式三)右边部分的三个概率,(公式三)如下:

(公式三)

       其中w是特征向量。

代码如下:

[python]  view plain copy
  1. def trainNB0(trainMatrix,trainCategory):  
  2.     numTrainDocs = len(trainMatrix)  
  3.     numWords = len(trainMatrix[0])  
  4.     pAbusive = sum(trainCategory)/float(numTrainDocs)  
  5.     p0Num = ones(numWords); p1Num = ones(numWords)      #change to ones()   
  6.     p0Denom = 2.0; p1Denom = 2.0                        #change to 2.0  
  7.     for i in range(numTrainDocs):  
  8.         if trainCategory[i] == 1:  
  9.             p1Num += trainMatrix[i]  
  10.             p1Denom += sum(trainMatrix[i])  
  11.         else:  
  12.             p0Num += trainMatrix[i]  
  13.             p0Denom += sum(trainMatrix[i])  
  14.     p1Vect = log(p1Num/p1Denom)          #change to log()  
  15.     p0Vect = log(p0Num/p0Denom)          #change to log()  
  16.     return p0Vect,p1Vect,pAbusive  

      上面的代码中输入的是特征向量组成的矩阵,和一个由标签组成的向量,其中pAbusive是类别概率P(ci),因为只有两类,计算一类后,另外一类可以直接用1-p得出。接下来初始化计算p(wi|c1)和p(wi|c0)的分子和分母,这里惟一让人好奇的就是为什么分母p0Denom和p1Denom都初始化为2?这是因为在实际应用中,我们计算出了(公式三)右半部分的概率后,也就是p(wi|ci)后,注意wi表示消息中的一个字,接下来就是判断整条消息属于某个类别的概率,就要计算p(w0|1)p(w1|1)p(w2|1)的形式,这样如果某个wi为0,这样整个概率都为0,或者都很小连乘后会更小,甚至round off 0。这样就会影响判断,因此把他们转到对数空间中来做运算,对数在机器学习里经常用到,在保持单调的情况下避免因数值运算带来的歧义问题,而且对数可以把乘法转到加法运算,加速了运算。因此上面的代码中把所有的出现次数初始化为1,然后把分母初始为2,接着都是累加,在对数空间中从0还是1开始累加,最后比较大小不会受影响的。

最后贴出分类代码:

[python]  view plain copy
  1. def classifyNB(vec2Classify, p0Vec, p1Vec, pClass1):  
  2.     p1 = sum(vec2Classify * p1Vec) + log(pClass1)    #element-wise mult  
  3.     p0 = sum(vec2Classify * p0Vec) + log(1.0 - pClass1)  
  4.     if p1 > p0:  
  5.         return 1  
  6.     else:   
  7.         return 0  

     分类代码也是在对数空间中计算的后验概率,然后通过比较大小来判断消息属于那一类。


总结:

       优点:对小量数据很有效,可以处理多类

       缺点:很依赖于数据的准备

朴素贝叶斯在概率图模型里被划为判别模型(Discriminative model)


参考文献:

         [1] Machine learning in action.Peter Harrington 

         [2]Probabilistic graphical model.Daphne Koller

机器学习理论与实战(四)逻辑回归

   从这节算是开始进入“正规”的机器学习了吧,之所以“正规”因为它开始要建立价值函数(cost function),接着优化价值函数求出权重,然后测试验证。这整套的流程是机器学习必经环节。今天要学习的话题是逻辑回归,逻辑回归也是一种有监督学习方法(supervised machine learning)。逻辑回归一般用来做预测,也可以用来做分类,预测是某个类别^.^!线性回归想比大家都不陌生了,y=kx+b,给定一堆数据点,拟合出k和b的值就行了,下次给定X时,就可以计算出y,这就是回归。而逻辑回归跟这个有点区别,它是一种非线性函数,拟合功能颇为强大,而且它是连续函数,可以对其求导,这点很重要,如果一个函数不可求导,那它在机器学习用起来很麻烦,早期的海维赛德(Heaviside)阶梯函数就因此被sigmoid函数取代,因为可导意味着我们可以很快找到其极值点,这就是优化方法的重要思想之一:利用求导,得到梯度,然后用梯度下降法更新参数。

        下面来看看逻辑回归的sigmoid函数,如(图一)所示:


(图一)

            (图一)中上图是sigmoid函数在定义域[-5,5] 上的形状,而下图是在定义域[-60,60]上的形状,由这两个图可以看出,它比较适合做二类的回归,因为严重两级分化。Sigmoid函数的如(公式一)所示:

(公式一)

         现在有了二类回归函数模型,就可以把特征映射到这个模型上了,而且sigmoid函数的自变量只有一个Z,假设我们的特征为X=[x0,x1,x2…xn]。令

,当给定大批的训练样本特征X时,我们只要找到合适的W=[w0,w1,w2…wn]来正确的把每个样本特征X映射到sigmoid函数的两级上,也就是说正确的完成了类别回归就行了,那么以后来个测试样本,只要和权重相乘后,带入sigmoid函数计算出的值就是预测值啦,很简单是吧。那怎么求权重W呢?

          要计算W,就要进入优化求解阶段咯,用的方法是梯度下降法或者随机梯度下降法。说到梯度下降,梯度下降一般对什么求梯度呢?梯度是一个函数上升最快的方向,沿着梯度方向我们可以很快找到极值点。我们找什么极值?仔细想想,当然是找训练模型的误差极值,当模型预测值和训练样本给出的正确值之间的误差和最小时,模型参数就是我们要求的。当然误差最小有可能导致过拟合,这个以后再说。我们先建立模型训练误差价值函数(cost function),如(公式二)所示:

(公式二)

        (公式二)中Y表示训练样本真实值,当J(theta)最小时的所得的theta就是我们要求的模型权重,可以看出J(theta)是个凸函数,得到的最小值也是全局最小。对其求导后得出梯度,如(公式三)所示:

(公式三)

        由于我们是找极小值,而梯度方向是极大值方向,因此我们取负号,沿着负梯度方向更新参数,如(公式四)所示:

(公式四)

        按照(公式四)的参数更新方法,当权重不再变化时,我们就宣称找到了极值点,此时的权重也是我们要求的,整个参数更新示意图如(图二)所示:

(图二)

        原理到此为止逻辑回归基本就说完了,下面进入代码实战阶段:


[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet():  
  4.     dataMat = []; labelMat = []  
  5.     fr = open('testSet.txt')  
  6.     for line in fr.readlines():  
  7.         lineArr = line.strip().split()  
  8.         dataMat.append([1.0, float(lineArr[0]), float(lineArr[1])])  
  9.         labelMat.append(int(lineArr[2]))  
  10.     return dataMat,labelMat  
  11.   
  12. def sigmoid(inX):  
  13.     return 1.0/(1+exp(-inX))  

上面两个函数分别是加载训练集和定义sigmoid函数,都比较简单。下面发出梯度下降的代码:

[python]  view plain copy
  1. def gradAscent(dataMatIn, classLabels):  
  2.     dataMatrix = mat(dataMatIn)             #convert to NumPy matrix  
  3.     labelMat = mat(classLabels).transpose() #convert to NumPy matrix  
  4.     m,n = shape(dataMatrix)  
  5.     alpha = 0.001  
  6.     maxCycles = 500  
  7.     weights = ones((n,1))  
  8.     for k in range(maxCycles):              #heavy on matrix operations  
  9.         h = sigmoid(dataMatrix*weights)     #matrix mult  
  10.         error = (labelMat - h)              #vector subtraction  
  11.         weights = weights + alpha * dataMatrix.transpose()* error #matrix mult  
  12.     return weights  

         梯度下降输入训练集和对应标签,接着就是迭代跟新参数,计算梯度,然后更新参数,注意倒数第二句就是按照(公式三)和(公式四)来更新参数。

为了直观的看到我们得到的权重是否正确的,我们把权重和样本打印出来,下面是相关打印代码:

[python]  view plain copy
  1. def plotBestFit(weights):  
  2.     import matplotlib.pyplot as plt  
  3.     dataMat,labelMat=loadDataSet()  
  4.     dataArr = array(dataMat)  
  5.     n = shape(dataArr)[0]   
  6.     xcord1 = []; ycord1 = []  
  7.     xcord2 = []; ycord2 = []  
  8.     for i in range(n):  
  9.         if int(labelMat[i])== 1:  
  10.             xcord1.append(dataArr[i,1]); ycord1.append(dataArr[i,2])  
  11.         else:  
  12.             xcord2.append(dataArr[i,1]); ycord2.append(dataArr[i,2])  
  13.     fig = plt.figure()  
  14.     ax = fig.add_subplot(111)  
  15.     ax.scatter(xcord1, ycord1, s=30, c='red', marker='s')  
  16.     ax.scatter(xcord2, ycord2, s=30, c='green')  
  17.     x = arange(-3.03.00.1)  
  18.     y = (-weights[0]-weights[1]*x)/weights[2]  
  19.     ax.plot(x, y)  
  20.     plt.xlabel('X1'); plt.ylabel('X2');  
  21.     plt.show()  

打印的效果图如(图三)所示:

(图三)

       可以看出效果蛮不错的,小错误是难免的,如果训练集没有错误反而危险,说到这基本就说完了,但是考虑到这个方法对少量样本(几百的)还行,在实际中当遇到10亿数量级时,而且特征维数上千时,这种方法很恐怖,光计算梯度就要消耗大量时间,因此要使用随机梯度下降方法。随机梯度下降算法和梯度下降算法原理一样,只是计算梯度不再使用所有样本,而是使用一个或者一小批来计算梯度,这样可以减少计算代价,虽然权重更新的路径很曲折,但最终也会收敛的,如(图四)所示


(图四)

       下面也发出随机梯度下降的代码:

[python]  view plain copy
  1. def stocGradAscent1(dataMatrix, classLabels, numIter=150):  
  2.     m,n = shape(dataMatrix)  
  3.     weights = ones(n)   #initialize to all ones  
  4.     for j in range(numIter):  
  5.         dataIndex = range(m)  
  6.         for i in range(m):  
  7.             alpha = 4/(1.0+j+i)+0.0001    #apha decreases with iteration, does not   
  8.             randIndex = int(random.uniform(0,len(dataIndex)))#go to 0 because of the constant  
  9.             h = sigmoid(sum(dataMatrix[randIndex]*weights))  
  10.             error = classLabels[randIndex] - h  
  11.             weights = weights + alpha * error * dataMatrix[randIndex]  
  12.             del(dataIndex[randIndex])  
  13.     return weights  

       最后也给出一个分类的代码,只要把阈值设为0.5,大于0.5划为一类,小于0.5划为另一类就行了,代码如下:

[python]  view plain copy
  1. def classifyVector(inX, weights):  
  2.     prob = sigmoid(sum(inX*weights))  
  3.     if prob > 0.5return 1.0  
  4.     elsereturn 0.0  

总结:

        优点:计算量不高,容易实现,对现实数据也很容易描述

        缺点:很容易欠拟合,精度可能也会不高

参考文献:

    [1] machine learning in action. Peter Harrington

    [2] machine learning.Andrew Ng


机器学习理论与实战(五)支持向量机

   做机器学习的一定对支持向量机(support vector machine-SVM)颇为熟悉,因为在深度学习出现之前,SVM一直霸占着机器学习老大哥的位子。他的理论很优美,各种变种改进版本也很多,比如latent-SVM, structural-SVM等。这节先来看看SVM的理论吧,在(图一)中A图表示有两类的数据集,图B,C,D都提供了一个线性分类器来对数据进行分类?但是哪个效果好一些?


(图一)

        可能对这个数据集来说,三个的分类器都一样足够好了吧,但是其实不然,这个只是训练集,现实测试的样本分布可能会比较散一些,各种可能都有,为了应对这种情况,我们要做的就是尽可能的使得线性分类器离两个数据集都尽可能的远,因为这样就会减少现实测试样本越过分类器的风险,提高检测精度。这种使得数据集到分类器之间的间距(margin)最大化的思想就是支持向量机的核心思想,而离分类器距离最近的样本成为支持向量。既然知道了我们的目标就是为了寻找最大边距,怎么寻找支持向量?如何实现?下面以(图二)来说明如何完成这些工作。


(图二)

假设(图二)中的直线表示一个超面,为了方面观看显示成一维直线,特征都是超面维度加一维度的,图中也可以看出,特征是二维,而分类器是一维的。如果特征是三维的,分类器就是一个平面。假设超面的解析式为 ,那么点A到超面的距离为 ,下面给出这个距离证明:


(图三)

在(图三)中,青色菱形表示超面,Xn为数据集中一点,W是超面权重,而且W是垂直于超面的。证明垂直很简单,假设X’和X’’都是超面上的一点,


,因此W垂直于超面。知道了W垂直于超面,那么Xn到超面的距离其实就是Xn和超面上任意一点x的连线在W上的投影,如(图四)所示:

(图四)

而(Xn-X)在W上的投影可通过(公式一)来计算,另外(公式一)也一并完成距离计算:

(公式一)

     注意最后使用了配项法并且用了超面解析式 才得出了距离计算。有了距离就可以来推导我们刚开始的想法:使得分类器距所有样本距离最远,即最大化边距,但是最大化边距的前提是我们要找到支持向量,也就是离分类器最近的样本点,此时我们就要完成两个优化任务,找到离分类器最近的点(支持向量),然后最大化边距。如(公式二)所示:

(公式二)

        大括号里面表示找到距离分类超面最近的支持向量,大括号外面则是使得超面离支持向量的距离最远,要优化这个函数相当困难,目前没有太有效的优化方法。但是我们可以把问题转换一下,如果我们把 大括号里面的优化问题固定住 ,然后来优化外面的就很容易了,可以用现在的优化方法来求解,因此我们做一个假设,假设大括号里的分子 等于1,那么我们只剩下优化W咯,整个优化公式就可以写成(公式三)的形式:

(公式三)

        这下就简单了,有等式约束的优化,约束式子为 ,这个约束等式背后还有个小窍门,假设我们把样本 Xn 的标签设为 1 或者 -1 ,当 Xn 在超面上面(或者右边)时,带入超面解析式得到大于 0 的值,乘上标签 1 仍然为本身,可以表示离超面的距离;当 Xn 在超面下面(或者左边)时,带入超面解析式得到小于 0 的值,乘上标签 -1 也是正值,仍然可以表示距离,因此我们把通常两类的标签 0 1 转换成 -1 1 就可以把标签信息完美的融进等式约束中,(公式三)最后一行也体现出来咯。下面继续说优化 求解(公式四)的方法,在最优化中,通常我们需要求解的最优化问题有如下几类:

       (i)无约束优化问题,可以写为:

              min f(x);  

       (ii)有等式约束的优化问题,可以写为:

                  min f(x), 

                   s.t. h_i(x) = 0; i =1, ..., n 

        (iii)有不等式约束的优化问题,可以写为:

                min f(x), 

                 s.t. g_i(x) <= 0; i =1, ..., n

                h_j(x) = 0; j =1,..., m

       对于第(i)类的优化问题,常常使用的方法就是Fermat定理,即使用求取f(x)的导数,然后令其为零,可以求得候选最优值,再在这些候选值中验证;如果是凸函数,可以保证是最优解。

       对于第(ii)类的优化问题,常常使用的方法就是拉格朗日乘子法(LagrangeMultiplier),即把等式约束h_i(x)用一个系数与f(x)写为一个式子,称为拉格朗日函数,而系数称为拉格朗日乘子。通过拉格朗日函数对各个变量求导,令其为零,可以求得候选值集合,然后验证求得最优值。

       对于第(iii)类的优化问题,常常使用的方法就是KKT条件。同样地,我们把所有的等式、不等式约束与f(x)写为一个式子,也叫拉格朗日函数,系数也称拉格朗日乘子,通过一些条件,可以求出最优值的必要条件,这个条件称为KKT条件。

       而(公式三)很明显符合第二类优化方法,因此可以使用拉格朗日乘子法来对其求解,在求解之前,我们先对(公式四)做个简单的变换。最大化||W||的导数可以最小化||W||或者W’W,如(公式四)所示:

(公式四)

套进拉格朗日乘子法公式得到如(公式五)所示的样子:


(公式五)

        在(公式五)中通过拉格朗日乘子法函数分别对W和b求导,为了得到极值点,令导数为0,得到


 ,然后把他们代入拉格朗日乘子法公式里得到(公式六)的形式:

(公式六)

     (公式六)后两行是目前我们要求解的优化函数,现在只需要做个二次规划即可求出alpha,二次规划优化求解如(公式七)所示:


(公式七)

         通过(公式七)求出alpha后,就可以用(公式六)中的第一行求出W。到此为止,SVM的公式推导基本完成了,可以看出数学理论很严密,很优美,尽管有些同行们认为看起枯燥,但是最好沉下心来从头看完,也不难,难的是优化。二次规划求解计算量很大,在实际应用中常用SMO(Sequential minimal optimization)算法,SMO算法打算放在下节结合代码来说。


参考文献:

     [1]machine learning in action. Peter Harrington

     [2] Learning From Data. Yaser S.Abu-Mostafa

   

机器学习理论与实战(六)支持向量机

  上节基本完成了SVM的理论推倒,寻找最大化间隔的目标最终转换成求解拉格朗日乘子变量alpha的求解问题,求出了alpha即可求解出SVM的权重W,有了权重也就有了最大间隔距离,但是其实上节我们有个假设:就是训练集是线性可分的,这样求出的alpha在[0,infinite]。但是如果数据不是线性可分的呢?此时我们就要允许部分的样本可以越过分类器,这样优化的目标函数就可以不变,只要引入松弛变量即可,它表示错分类样本点的代价,分类正确时它等于0,当分类错误时,其中Tn表示样本的真实标签-1或者1,回顾上节中,我们把支持向量到分类器的距离固定为1,因此两类的支持向量间的距离肯定大于1的,当分类错误时肯定也大于1,如(图五)所示(这里公式和图标序号都接上一节)。


(图五)

       这样有了错分类的代价,我们把上节(公式四)的目标函数上添加上这一项错分类代价,得到如(公式八)的形式:

(公式八)

        重复上节的拉格朗日乘子法步骤,得到(公式九):

(公式九)

         多了一个Un乘子,当然我们的工作就是继续求解此目标函数,继续重复上节的步骤,求导得到(公式十):


(公式十)

         又因为alpha大于0,而且Un大于0,所以0<alpha<C,为了解释的清晰一些,我们把(公式九)的KKT条件也发出来(上节中的第三类优化问题),注意Un是大于等于0

       推导到现在,优化函数的形式基本没变,只是多了一项错分类的价值,但是多了一个条件,0<alpha<C,C是一个常数,它的作用就是在允许有错误分类的情况下,控制最大化间距,它太大了会导致过拟合,太小了会导致欠拟合。接下来的步骤貌似大家都应该知道了,多了一个C常量的限制条件,然后继续用SMO算法优化求解二次规划,但是我想继续把核函数也一次说了,如果样本线性不可分,引入核函数后,把样本映射到高维空间就可以线性可分,如(图六)所示的线性不可分的样本:

(图六)

         在(图六)中,现有的样本是很明显线性不可分,但是加入我们利用现有的样本X之间作些不同的运算,如(图六)右边所示的样子,而让f作为新的样本(或者说新的特征)是不是更好些?现在把X已经投射到高维度上去了,但是f我们不知道,此时核函数就该上场了,以高斯核函数为例,在(图七)中选几个样本点作为基准点,来利用核函数计算f,如(图七)所示:

(图七)

       这样就有了f,而核函数此时相当于对样本的X和基准点一个度量,做权重衰减,形成依赖于x的新的特征f,把f放在上面说的SVM中继续求解alpha,然后得出权重就行了,原理很简单吧,为了显得有点学术味道,把核函数也做个样子加入目标函数中去吧,如(公式十一)所示:


(公式十一)


        其中K(Xn,Xm)是核函数,和上面目标函数比没有多大的变化,用SMO优化求解就行了,代码如下:

[python]  view plain copy
  1. def smoPK(dataMatIn, classLabels, C, toler, maxIter):    #full Platt SMO  
  2.     oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)  
  3.     iter = 0  
  4.     entireSet = True; alphaPairsChanged = 0  
  5.     while (iter < maxIter) and ((alphaPairsChanged > 0or (entireSet)):  
  6.         alphaPairsChanged = 0  
  7.         if entireSet:   #go over all  
  8.             for i in range(oS.m):          
  9.                 alphaPairsChanged += innerL(i,oS)  
  10.                 print "fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  11.             iter += 1  
  12.         else:#go over non-bound (railed) alphas  
  13.             nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]  
  14.             for i in nonBoundIs:  
  15.                 alphaPairsChanged += innerL(i,oS)  
  16.                 print "non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  17.             iter += 1  
  18.         if entireSet: entireSet = False #toggle entire set loop  
  19.         elif (alphaPairsChanged == 0): entireSet = True    
  20.         print "iteration number: %d" % iter  
  21.     return oS.b,oS.alphas  

       下面演示一个小例子,手写识别。

      (1)收集数据:提供文本文件

      (2)准备数据:基于二值图像构造向量

      (3)分析数据:对图像向量进行目测

      (4)训练算法:采用两种不同的核函数,并对径向基函数采用不同的设置来运行SMO算法。

       (5)测试算法:编写一个函数来测试不同的核函数,并计算错误率

       (6)使用算法:一个图像识别的完整应用还需要一些图像处理的只是,此demo略。

      完整代码如下:

[python]  view plain copy
  1. from numpy import *  
  2. from time import sleep  
  3.   
  4. def loadDataSet(fileName):  
  5.     dataMat = []; labelMat = []  
  6.     fr = open(fileName)  
  7.     for line in fr.readlines():  
  8.         lineArr = line.strip().split('\t')  
  9.         dataMat.append([float(lineArr[0]), float(lineArr[1])])  
  10.         labelMat.append(float(lineArr[2]))  
  11.     return dataMat,labelMat  
  12.   
  13. def selectJrand(i,m):  
  14.     j=i #we want to select any J not equal to i  
  15.     while (j==i):  
  16.         j = int(random.uniform(0,m))  
  17.     return j  
  18.   
  19. def clipAlpha(aj,H,L):  
  20.     if aj > H:   
  21.         aj = H  
  22.     if L > aj:  
  23.         aj = L  
  24.     return aj  
  25.   
  26. def smoSimple(dataMatIn, classLabels, C, toler, maxIter):  
  27.     dataMatrix = mat(dataMatIn); labelMat = mat(classLabels).transpose()  
  28.     b = 0; m,n = shape(dataMatrix)  
  29.     alphas = mat(zeros((m,1)))  
  30.     iter = 0  
  31.     while (iter < maxIter):  
  32.         alphaPairsChanged = 0  
  33.         for i in range(m):  
  34.             fXi = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[i,:].T)) + b  
  35.             Ei = fXi - float(labelMat[i])#if checks if an example violates KKT conditions  
  36.             if ((labelMat[i]*Ei < -toler) and (alphas[i] < C)) or ((labelMat[i]*Ei > toler) and (alphas[i] > 0)):  
  37.                 j = selectJrand(i,m)  
  38.                 fXj = float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T)) + b  
  39.                 Ej = fXj - float(labelMat[j])  
  40.                 alphaIold = alphas[i].copy(); alphaJold = alphas[j].copy();  
  41.                 if (labelMat[i] != labelMat[j]):  
  42.                     L = max(0, alphas[j] - alphas[i])  
  43.                     H = min(C, C + alphas[j] - alphas[i])  
  44.                 else:  
  45.                     L = max(0, alphas[j] + alphas[i] - C)  
  46.                     H = min(C, alphas[j] + alphas[i])  
  47.                 if L==H: print "L==H"continue  
  48.                 eta = 2.0 * dataMatrix[i,:]*dataMatrix[j,:].T - dataMatrix[i,:]*dataMatrix[i,:].T - dataMatrix[j,:]*dataMatrix[j,:].T  
  49.                 if eta >= 0print "eta>=0"continue  
  50.                 alphas[j] -= labelMat[j]*(Ei - Ej)/eta  
  51.                 alphas[j] = clipAlpha(alphas[j],H,L)  
  52.                 if (abs(alphas[j] - alphaJold) < 0.00001): print "j not moving enough"continue  
  53.                 alphas[i] += labelMat[j]*labelMat[i]*(alphaJold - alphas[j])#update i by the same amount as j  
  54.                                                                         #the update is in the oppostie direction  
  55.                 b1 = b - Ei- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[i,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i,:]*dataMatrix[j,:].T  
  56.                 b2 = b - Ej- labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i,:]*dataMatrix[j,:].T - labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[j,:]*dataMatrix[j,:].T  
  57.                 if (0 < alphas[i]) and (C > alphas[i]): b = b1  
  58.                 elif (0 < alphas[j]) and (C > alphas[j]): b = b2  
  59.                 else: b = (b1 + b2)/2.0  
  60.                 alphaPairsChanged += 1  
  61.                 print "iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  62.         if (alphaPairsChanged == 0): iter += 1  
  63.         else: iter = 0  
  64.         print "iteration number: %d" % iter  
  65.     return b,alphas  
  66.   
  67. def kernelTrans(X, A, kTup): #calc the kernel or transform data to a higher dimensional space  
  68.     m,n = shape(X)  
  69.     K = mat(zeros((m,1)))  
  70.     if kTup[0]=='lin': K = X * A.T   #linear kernel  
  71.     elif kTup[0]=='rbf':  
  72.         for j in range(m):  
  73.             deltaRow = X[j,:] - A  
  74.             K[j] = deltaRow*deltaRow.T  
  75.         K = exp(K/(-1*kTup[1]**2)) #divide in NumPy is element-wise not matrix like Matlab  
  76.     elseraise NameError('Houston We Have a Problem -- \  
  77.     That Kernel is not recognized')  
  78.     return K  
  79.   
  80. class optStruct:  
  81.     def __init__(self,dataMatIn, classLabels, C, toler, kTup):  # Initialize the structure with the parameters   
  82.         self.X = dataMatIn  
  83.         self.labelMat = classLabels  
  84.         self.C = C  
  85.         self.tol = toler  
  86.         self.m = shape(dataMatIn)[0]  
  87.         self.alphas = mat(zeros((self.m,1)))  
  88.         self.b = 0  
  89.         self.eCache = mat(zeros((self.m,2))) #first column is valid flag  
  90.         self.K = mat(zeros((self.m,self.m)))  
  91.         for i in range(self.m):  
  92.             self.K[:,i] = kernelTrans(self.X, self.X[i,:], kTup)  
  93.           
  94. def calcEk(oS, k):  
  95.     fXk = float(multiply(oS.alphas,oS.labelMat).T*oS.K[:,k] + oS.b)  
  96.     Ek = fXk - float(oS.labelMat[k])  
  97.     return Ek  
  98.           
  99. def selectJ(i, oS, Ei):         #this is the second choice -heurstic, and calcs Ej  
  100.     maxK = -1; maxDeltaE = 0; Ej = 0  
  101.     oS.eCache[i] = [1,Ei]  #set valid #choose the alpha that gives the maximum delta E  
  102.     validEcacheList = nonzero(oS.eCache[:,0].A)[0]  
  103.     if (len(validEcacheList)) > 1:  
  104.         for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E  
  105.             if k == i: continue #don't calc for i, waste of time  
  106.             Ek = calcEk(oS, k)  
  107.             deltaE = abs(Ei - Ek)  
  108.             if (deltaE > maxDeltaE):  
  109.                 maxK = k; maxDeltaE = deltaE; Ej = Ek  
  110.         return maxK, Ej  
  111.     else:   #in this case (first time around) we don't have any valid eCache values  
  112.         j = selectJrand(i, oS.m)  
  113.         Ej = calcEk(oS, j)  
  114.     return j, Ej  
  115.   
  116. def updateEk(oS, k):#after any alpha has changed update the new value in the cache  
  117.     Ek = calcEk(oS, k)  
  118.     oS.eCache[k] = [1,Ek]  
  119.           
  120. def innerL(i, oS):  
  121.     Ei = calcEk(oS, i)  
  122.     if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):  
  123.         j,Ej = selectJ(i, oS, Ei) #this has been changed from selectJrand  
  124.         alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy();  
  125.         if (oS.labelMat[i] != oS.labelMat[j]):  
  126.             L = max(0, oS.alphas[j] - oS.alphas[i])  
  127.             H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])  
  128.         else:  
  129.             L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)  
  130.             H = min(oS.C, oS.alphas[j] + oS.alphas[i])  
  131.         if L==H: print "L==H"return 0  
  132.         eta = 2.0 * oS.K[i,j] - oS.K[i,i] - oS.K[j,j] #changed for kernel  
  133.         if eta >= 0print "eta>=0"return 0  
  134.         oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta  
  135.         oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)  
  136.         updateEk(oS, j) #added this for the Ecache  
  137.         if (abs(oS.alphas[j] - alphaJold) < 0.00001): print "j not moving enough"return 0  
  138.         oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])#update i by the same amount as j  
  139.         updateEk(oS, i) #added this for the Ecache                    #the update is in the oppostie direction  
  140.         b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,i] - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[i,j]  
  141.         b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.K[i,j]- oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.K[j,j]  
  142.         if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]): oS.b = b1  
  143.         elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]): oS.b = b2  
  144.         else: oS.b = (b1 + b2)/2.0  
  145.         return 1  
  146.     elsereturn 0  
  147.   
  148. def smoP(dataMatIn, classLabels, C, toler, maxIter,kTup=('lin'0)):    #full Platt SMO  
  149.     oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler, kTup)  
  150.     iter = 0  
  151.     entireSet = True; alphaPairsChanged = 0  
  152.     while (iter < maxIter) and ((alphaPairsChanged > 0or (entireSet)):  
  153.         alphaPairsChanged = 0  
  154.         if entireSet:   #go over all  
  155.             for i in range(oS.m):          
  156.                 alphaPairsChanged += innerL(i,oS)  
  157.                 print "fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  158.             iter += 1  
  159.         else:#go over non-bound (railed) alphas  
  160.             nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]  
  161.             for i in nonBoundIs:  
  162.                 alphaPairsChanged += innerL(i,oS)  
  163.                 print "non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  164.             iter += 1  
  165.         if entireSet: entireSet = False #toggle entire set loop  
  166.         elif (alphaPairsChanged == 0): entireSet = True    
  167.         print "iteration number: %d" % iter  
  168.     return oS.b,oS.alphas  
  169.   
  170. def calcWs(alphas,dataArr,classLabels):  
  171.     X = mat(dataArr); labelMat = mat(classLabels).transpose()  
  172.     m,n = shape(X)  
  173.     w = zeros((n,1))  
  174.     for i in range(m):  
  175.         w += multiply(alphas[i]*labelMat[i],X[i,:].T)  
  176.     return w  
  177.   
  178. def testRbf(k1=1.3):  
  179.     dataArr,labelArr = loadDataSet('testSetRBF.txt')  
  180.     b,alphas = smoP(dataArr, labelArr, 2000.000110000, ('rbf', k1)) #C=200 important  
  181.     datMat=mat(dataArr); labelMat = mat(labelArr).transpose()  
  182.     svInd=nonzero(alphas.A>0)[0]  
  183.     sVs=datMat[svInd] #get matrix of only support vectors  
  184.     labelSV = labelMat[svInd];  
  185.     print "there are %d Support Vectors" % shape(sVs)[0]  
  186.     m,n = shape(datMat)  
  187.     errorCount = 0  
  188.     for i in range(m):  
  189.         kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1))  
  190.         predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  
  191.         if sign(predict)!=sign(labelArr[i]): errorCount += 1  
  192.     print "the training error rate is: %f" % (float(errorCount)/m)  
  193.     dataArr,labelArr = loadDataSet('testSetRBF2.txt')  
  194.     errorCount = 0  
  195.     datMat=mat(dataArr); labelMat = mat(labelArr).transpose()  
  196.     m,n = shape(datMat)  
  197.     for i in range(m):  
  198.         kernelEval = kernelTrans(sVs,datMat[i,:],('rbf', k1))  
  199.         predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  
  200.         if sign(predict)!=sign(labelArr[i]): errorCount += 1      
  201.     print "the test error rate is: %f" % (float(errorCount)/m)      
  202.       
  203. def img2vector(filename):  
  204.     returnVect = zeros((1,1024))  
  205.     fr = open(filename)  
  206.     for i in range(32):  
  207.         lineStr = fr.readline()  
  208.         for j in range(32):  
  209.             returnVect[0,32*i+j] = int(lineStr[j])  
  210.     return returnVect  
  211.   
  212. def loadImages(dirName):  
  213.     from os import listdir  
  214.     hwLabels = []  
  215.     trainingFileList = listdir(dirName)           #load the training set  
  216.     m = len(trainingFileList)  
  217.     trainingMat = zeros((m,1024))  
  218.     for i in range(m):  
  219.         fileNameStr = trainingFileList[i]  
  220.         fileStr = fileNameStr.split('.')[0]     #take off .txt  
  221.         classNumStr = int(fileStr.split('_')[0])  
  222.         if classNumStr == 9: hwLabels.append(-1)  
  223.         else: hwLabels.append(1)  
  224.         trainingMat[i,:] = img2vector('%s/%s' % (dirName, fileNameStr))  
  225.     return trainingMat, hwLabels      
  226.   
  227. def testDigits(kTup=('rbf'10)):  
  228.     dataArr,labelArr = loadImages('trainingDigits')  
  229.     b,alphas = smoP(dataArr, labelArr, 2000.000110000, kTup)  
  230.     datMat=mat(dataArr); labelMat = mat(labelArr).transpose()  
  231.     svInd=nonzero(alphas.A>0)[0]  
  232.     sVs=datMat[svInd]   
  233.     labelSV = labelMat[svInd];  
  234.     print "there are %d Support Vectors" % shape(sVs)[0]  
  235.     m,n = shape(datMat)  
  236.     errorCount = 0  
  237.     for i in range(m):  
  238.         kernelEval = kernelTrans(sVs,datMat[i,:],kTup)  
  239.         predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  
  240.         if sign(predict)!=sign(labelArr[i]): errorCount += 1  
  241.     print "the training error rate is: %f" % (float(errorCount)/m)  
  242.     dataArr,labelArr = loadImages('testDigits')  
  243.     errorCount = 0  
  244.     datMat=mat(dataArr); labelMat = mat(labelArr).transpose()  
  245.     m,n = shape(datMat)  
  246.     for i in range(m):  
  247.         kernelEval = kernelTrans(sVs,datMat[i,:],kTup)  
  248.         predict=kernelEval.T * multiply(labelSV,alphas[svInd]) + b  
  249.         if sign(predict)!=sign(labelArr[i]): errorCount += 1      
  250.     print "the test error rate is: %f" % (float(errorCount)/m)   
  251.   
  252.   
  253. '''''#######******************************** 
  254. Non-Kernel VErsions below 
  255. '''#######********************************  
  256.   
  257. class optStructK:  
  258.     def __init__(self,dataMatIn, classLabels, C, toler):  # Initialize the structure with the parameters   
  259.         self.X = dataMatIn  
  260.         self.labelMat = classLabels  
  261.         self.C = C  
  262.         self.tol = toler  
  263.         self.m = shape(dataMatIn)[0]  
  264.         self.alphas = mat(zeros((self.m,1)))  
  265.         self.b = 0  
  266.         self.eCache = mat(zeros((self.m,2))) #first column is valid flag  
  267.           
  268. def calcEkK(oS, k):  
  269.     fXk = float(multiply(oS.alphas,oS.labelMat).T*(oS.X*oS.X[k,:].T)) + oS.b  
  270.     Ek = fXk - float(oS.labelMat[k])  
  271.     return Ek  
  272.           
  273. def selectJK(i, oS, Ei):         #this is the second choice -heurstic, and calcs Ej  
  274.     maxK = -1; maxDeltaE = 0; Ej = 0  
  275.     oS.eCache[i] = [1,Ei]  #set valid #choose the alpha that gives the maximum delta E  
  276.     validEcacheList = nonzero(oS.eCache[:,0].A)[0]  
  277.     if (len(validEcacheList)) > 1:  
  278.         for k in validEcacheList:   #loop through valid Ecache values and find the one that maximizes delta E  
  279.             if k == i: continue #don't calc for i, waste of time  
  280.             Ek = calcEk(oS, k)  
  281.             deltaE = abs(Ei - Ek)  
  282.             if (deltaE > maxDeltaE):  
  283.                 maxK = k; maxDeltaE = deltaE; Ej = Ek  
  284.         return maxK, Ej  
  285.     else:   #in this case (first time around) we don't have any valid eCache values  
  286.         j = selectJrand(i, oS.m)  
  287.         Ej = calcEk(oS, j)  
  288.     return j, Ej  
  289.   
  290. def updateEkK(oS, k):#after any alpha has changed update the new value in the cache  
  291.     Ek = calcEk(oS, k)  
  292.     oS.eCache[k] = [1,Ek]  
  293.           
  294. def innerLK(i, oS):  
  295.     Ei = calcEk(oS, i)  
  296.     if ((oS.labelMat[i]*Ei < -oS.tol) and (oS.alphas[i] < oS.C)) or ((oS.labelMat[i]*Ei > oS.tol) and (oS.alphas[i] > 0)):  
  297.         j,Ej = selectJ(i, oS, Ei) #this has been changed from selectJrand  
  298.         alphaIold = oS.alphas[i].copy(); alphaJold = oS.alphas[j].copy();  
  299.         if (oS.labelMat[i] != oS.labelMat[j]):  
  300.             L = max(0, oS.alphas[j] - oS.alphas[i])  
  301.             H = min(oS.C, oS.C + oS.alphas[j] - oS.alphas[i])  
  302.         else:  
  303.             L = max(0, oS.alphas[j] + oS.alphas[i] - oS.C)  
  304.             H = min(oS.C, oS.alphas[j] + oS.alphas[i])  
  305.         if L==H: print "L==H"return 0  
  306.         eta = 2.0 * oS.X[i,:]*oS.X[j,:].T - oS.X[i,:]*oS.X[i,:].T - oS.X[j,:]*oS.X[j,:].T  
  307.         if eta >= 0print "eta>=0"return 0  
  308.         oS.alphas[j] -= oS.labelMat[j]*(Ei - Ej)/eta  
  309.         oS.alphas[j] = clipAlpha(oS.alphas[j],H,L)  
  310.         updateEk(oS, j) #added this for the Ecache  
  311.         if (abs(oS.alphas[j] - alphaJold) < 0.00001): print "j not moving enough"return 0  
  312.         oS.alphas[i] += oS.labelMat[j]*oS.labelMat[i]*(alphaJold - oS.alphas[j])#update i by the same amount as j  
  313.         updateEk(oS, i) #added this for the Ecache                    #the update is in the oppostie direction  
  314.         b1 = oS.b - Ei- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[i,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[i,:]*oS.X[j,:].T  
  315.         b2 = oS.b - Ej- oS.labelMat[i]*(oS.alphas[i]-alphaIold)*oS.X[i,:]*oS.X[j,:].T - oS.labelMat[j]*(oS.alphas[j]-alphaJold)*oS.X[j,:]*oS.X[j,:].T  
  316.         if (0 < oS.alphas[i]) and (oS.C > oS.alphas[i]): oS.b = b1  
  317.         elif (0 < oS.alphas[j]) and (oS.C > oS.alphas[j]): oS.b = b2  
  318.         else: oS.b = (b1 + b2)/2.0  
  319.         return 1  
  320.     elsereturn 0  
  321.   
  322. def smoPK(dataMatIn, classLabels, C, toler, maxIter):    #full Platt SMO  
  323.     oS = optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)  
  324.     iter = 0  
  325.     entireSet = True; alphaPairsChanged = 0  
  326.     while (iter < maxIter) and ((alphaPairsChanged > 0or (entireSet)):  
  327.         alphaPairsChanged = 0  
  328.         if entireSet:   #go over all  
  329.             for i in range(oS.m):          
  330.                 alphaPairsChanged += innerL(i,oS)  
  331.                 print "fullSet, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  332.             iter += 1  
  333.         else:#go over non-bound (railed) alphas  
  334.             nonBoundIs = nonzero((oS.alphas.A > 0) * (oS.alphas.A < C))[0]  
  335.             for i in nonBoundIs:  
  336.                 alphaPairsChanged += innerL(i,oS)  
  337.                 print "non-bound, iter: %d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)  
  338.             iter += 1  
  339.         if entireSet: entireSet = False #toggle entire set loop  
  340.         elif (alphaPairsChanged == 0): entireSet = True    
  341.         print "iteration number: %d" % iter  
  342.     return oS.b,oS.alphas  

运行结果如(图八)所示:


(图八)

上面代码有兴趣的可以读读,用的话,建议使用libsvm。

参考文献:

    [1]machine learning in action. PeterHarrington

    [2] pattern recognition and machinelearning. Christopher M. Bishop

    [3]machine learning.Andrew Ng


机器学习理论与实战(七)Adaboost

 Adaboost也是一种原理简单,但很实用的有监督机器学习算法,它是daptive boosting的简称。说到boosting算法,就不得提一提bagging算法,他们两个都是把一些弱分类器组合起来来进行分类的方法,统称为集成方法(ensemble method),类似于投资,“不把鸡蛋放在一个篮子”,虽然每个弱分类器分类的不那么准确,但是如果把多个弱分类器组合起来可以得到相当不错的结果,另外要说的是集成方法还可以组合不同的分类器,而Adaboost和boosting算法的每个弱分类器的类型都一样的。他们两个不同的地方是:boosting的每个弱分类器组合起来的权重不一样,本节的Adaboost就是一个例子,而bagging的每个弱分类器的组合权重是相等,代表的例子就是random forest。Random forest的每个弱分类器是决策树,输出的类别有多个决策树分类的类别的众数决定。今天的主题是Adaboost,下面来看看Adaboost的工作原理:

         既然Adaboost的每个弱分类器的类型都一样,那么怎么组织安排每个分类器呢?如(图一)所示:


(图一)

       (图一)是Adaboost的原理示意图,左边矩形表示数据集,中间表示根据特征阈值来做分类,这样每一个弱分类器都类似于一个单节点的决策树,其实就是阈值判断而已,右边的三角形对每个弱分类器赋予一个权重,最后根据每个弱分类器的加权组合来判断总体类别。要注意一下数据集从上到下三个矩形内的直方图不一样,这表示每个样本的权重也发生了变化,样本权重的一开始初始化成相等的权重,然后根据弱分类器的错误率来调整每个弱分类器的全总alpha,如(图一)中的三角形所示,alpha 的计算如(公式一)所示:

(公式一)

        从(公式一)中也能感觉出来,弱分类器权重alpha和弱分类器分类错误率epsilon成反比,如果不能看出反比关系,分子分母同时除以epsilon就可以了,而ln是单调函数。这很make sense,当然分类器的错误率越高,越不能器重它,它的权重就应该低。同样的道理,样本也要区分对待,样本的权重要用弱分类器权重来计算,其实也是间接靠分类错误率,如(公式二)所示:

(公式二)

        其中D表示样本权重向量,有多少个样本就有多少个权重,下标i表示样本索引,而上标t表示上一次分类器训练迭代次数。这样一直更新迭代,一直到最大迭代次数或者整个分类器错误率为0或者不变时停止迭代,就完成了Adaboost的训练。但是这样就可以把样本分开了吗?下面从一组图解答这个问题,如(图二)所示:

(图二)

         由(图二)所示,每个弱分类器Hi可以要求不高的准确率,哪怕错误率是50%也可以接受,但是最后通过线性加权组合就可以得到一个很好的分类器,这点也可以通过错误率分析验证,有兴趣的可以看看:http://math.mit.edu/~rothvoss/18.304.3PM/Presentations/1-Eric-Boosting304FinalRpdf.pdf,想了解为什么alpha的计算如(公式一)的样子,可以看看:http://math.mit.edu/~rothvoss/18.304.3PM/Presentations/1-Eric-Boosting304FinalRpdf.pdf

这样Adaboost的原理基本分析完毕,下面进入代码实战阶段:

首先来准备个简单数据集:

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadSimpData():        
  4.     datMat = matrix([[ 1. ,  2.1],  
  5.         [ 2. ,  1.1],  
  6.         [ 1.3,  1. ],  
  7.         [ 1. ,  1. ],  
  8.         [ 2. ,  1. ]])  
  9.     classLabels = [1.01.0, -1.0, -1.01.0]  
  10.     return datMat,classLabels  

       上面有5个样本,接下来就是初始化每个样本的权重,刚开始相等的:

[python]  view plain copy
  1. D = mat(ones((5,1))/5)  

        有了样本和初始化权重,接下来的任务就是构建一个弱分类器,其实就是一个单节点决策树,找到决策树每个特征维度上对应的最佳阈值以及表示是大于阈值还是小于阈值为正样本的标识符。代码如下:

[python]  view plain copy
  1. def buildStump(dataArr,classLabels,D):  
  2.     dataMatrix = mat(dataArr); labelMat = mat(classLabels).T  
  3.     m,n = shape(dataMatrix)  
  4.     numSteps = 10.0; bestStump = {}; bestClasEst = mat(zeros((m,1)))  
  5.     minError = inf #init error sum, to +infinity  
  6.     for i in range(n):#loop over all dimensions  
  7.         rangeMin = dataMatrix[:,i].min(); rangeMax = dataMatrix[:,i].max();  
  8.         stepSize = (rangeMax-rangeMin)/numSteps  
  9.         for j in range(-1,int(numSteps)+1):#loop over all range in current dimension  
  10.             for inequal in ['lt''gt']: #go over less than and greater than  
  11.                 threshVal = (rangeMin + float(j) * stepSize)  
  12.                 predictedVals = stumpClassify(dataMatrix,i,threshVal,inequal)#call stump classify with i, j, lessThan  
  13.                 errArr = mat(ones((m,1)))  
  14.                 errArr[predictedVals == labelMat] = 0  
  15.                 weightedError = D.T*errArr  #calc total error multiplied by D  
  16.                 #print "split: dim %d, thresh %.2f, thresh ineqal: %s, the weighted error is %.3f" % (i, threshVal, inequal, weightedError)  
  17.                 if weightedError < minError:  
  18.                     minError = weightedError  
  19.                     bestClasEst = predictedVals.copy()  
  20.                     bestStump['dim'] = i  
  21.                     bestStump['thresh'] = threshVal  
  22.                     bestStump['ineq'] = inequal  
  23.     return bestStump,minError,bestClasEst  

          注意代码中有三个for循环,这三个for循环其实就是为了完成决策树的每个特征维度上对应的最佳阈值以及表示是大于阈值还是小于阈值为正样本的标识符,这三个要素。其中it,gt分别表示大于和小于,阈值的选择是靠增加步长来需找,最终三者的确定是靠决策树分类错误率最小者决定,每个决策树的分类代码如下,很简单,就是靠阈值判断:

[python]  view plain copy
  1. def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):#just classify the data  
  2.     retArray = ones((shape(dataMatrix)[0],1))  
  3.     if threshIneq == 'lt':  
  4.         retArray[dataMatrix[:,dimen] <= threshVal] = -1.0  
  5.     else:  
  6.         retArray[dataMatrix[:,dimen] > threshVal] = -1.0  
  7.     return retArray  

        有了弱分类器的构造代码,下面来看Adaboost的训练代码:

[python]  view plain copy
  1. def adaBoostTrainDS(dataArr,classLabels,numIt=40):  
  2.     weakClassArr = []  
  3.     m = shape(dataArr)[0]  
  4.     D = mat(ones((m,1))/m)   #init D to all equal  
  5.     aggClassEst = mat(zeros((m,1)))  
  6.     for i in range(numIt):  
  7.         bestStump,error,classEst = buildStump(dataArr,classLabels,D)#build Stump  
  8.         #print "D:",D.T  
  9.         alpha = float(0.5*log((1.0-error)/max(error,1e-16)))#calc alpha, throw in max(error,eps) to account for error=0  
  10.         bestStump['alpha'] = alpha    
  11.         weakClassArr.append(bestStump)                  #store Stump Params in Array  
  12.         #print "classEst: ",classEst.T  
  13.         expon = multiply(-1*alpha*mat(classLabels).T,classEst) #exponent for D calc, getting messy  
  14.         D = multiply(D,exp(expon))                              #Calc New D for next iteration  
  15.         D = D/D.sum()  
  16.         #calc training error of all classifiers, if this is 0 quit for loop early (use break)  
  17.         aggClassEst += alpha*classEst  
  18.         #print "aggClassEst: ",aggClassEst.T  
  19.         aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T,ones((m,1)))  
  20.         errorRate = aggErrors.sum()/m  
  21.         print "total error: ",errorRate  
  22.         if errorRate == 0.0break  
  23.     return weakClassArr,aggClassEst  

       上面的代码中训练过程主要任务就是完成(公式二)中的样本权重D和弱分类器权重alpha的更新,另外还要注意一下,代码中迭代了40次,每次都调用了buildStump,这就意味着创建了40个弱分类器。当模型收敛后,有了样本权重和弱弱弱分类器权重,最后就是对测试样本进行分类,分类代码如下:

[python]  view plain copy
  1. def adaClassify(datToClass,classifierArr):  
  2.     dataMatrix = mat(datToClass)#do stuff similar to last aggClassEst in adaBoostTrainDS  
  3.     m = shape(dataMatrix)[0]  
  4.     aggClassEst = mat(zeros((m,1)))  
  5.     for i in range(len(classifierArr)):  
  6.         classEst = stumpClassify(dataMatrix,classifierArr[i]['dim'],\  
  7.                                  classifierArr[i]['thresh'],\  
  8.                                  classifierArr[i]['ineq'])#call stump classify  
  9.         aggClassEst += classifierArr[i]['alpha']*classEst  
  10.         print aggClassEst  
  11.     return sign(aggClassEst)  

     考虑到有些做学术的为了比较不同机器学习算法的好坏,常常需要画ROC曲线,这里也给出画ROC的代码:

[python]  view plain copy
  1. def plotROC(predStrengths, classLabels):  
  2.     import matplotlib.pyplot as plt  
  3.     cur = (1.0,1.0#cursor  
  4.     ySum = 0.0 #variable to calculate AUC  
  5.     numPosClas = sum(array(classLabels)==1.0)  
  6.     yStep = 1/float(numPosClas); xStep = 1/float(len(classLabels)-numPosClas)  
  7.     sortedIndicies = predStrengths.argsort()#get sorted index, it's reverse  
  8.     fig = plt.figure()  
  9.     fig.clf()  
  10.     ax = plt.subplot(111)  
  11.     #loop through all the values, drawing a line segment at each point  
  12.     for index in sortedIndicies.tolist()[0]:  
  13.         if classLabels[index] == 1.0:  
  14.             delX = 0; delY = yStep;  
  15.         else:  
  16.             delX = xStep; delY = 0;  
  17.             ySum += cur[1]  
  18.         #draw line from cur to (cur[0]-delX,cur[1]-delY)  
  19.         ax.plot([cur[0],cur[0]-delX],[cur[1],cur[1]-delY], c='b')  
  20.         cur = (cur[0]-delX,cur[1]-delY)  
  21.     ax.plot([0,1],[0,1],'b--')  
  22.     plt.xlabel('False positive rate'); plt.ylabel('True positive rate')  
  23.     plt.title('ROC curve for AdaBoost horse colic detection system')  
  24.     ax.axis([0,1,0,1])  
  25.     plt.show()  
  26.     print "the Area Under the Curve is: ",ySum*xStep  

       到此位置,Adaboost的代码也介绍完了,最终程序的运行结果如(图三)所示:

(图三)

       而Adaboost的模型ROC运行曲线如(图四)所示:


(图四)

最近MIT的几个人证明了Adaboost可以用一阶梯度的角度来解释,详见链接


参考文献:

[1] machinelearning in action.

[2] http://www.robots.ox.ac.uk/~az/lectures/cv/adaboost_matas.pdf

机器学习理论与实战(八)回归

  按照《机器学习实战》的主线,结束有监督学习中关于分类的机器学习方法,进入回归部分。所谓回归就是数据进行曲线拟合,回归一般用来做预测,涵盖线性回归(经典最小二乘法)、局部加权线性回归、岭回归和逐步线性回归。先来看下线性回归,即经典最小二乘法,说到最小二乘法就不得说下线性代数,因为一般说线性回归只通过计算一个公式就可以得到答案,如(公式一)所示:


(公式一)

       其中X是表示样本特征组成的矩阵,Y表示对应的值,比如房价,股票走势等,(公式一)是直接通过对(公式二)求导得到的,因为(公式二)是凸函数,导数等于零的点就是最小点。

(公式二)

         不过并不是所有的码农能从(公式二)求导得到(公式一)的解,因此这里给出另外一个直观的解,直观理解建立起来后,后续几个回归就简单类推咯。从初中的投影点说起,如(图一)所示:

(图一)

       在(图一)中直线a上离点b最近的点是点b在其上的投影,即垂直于a的交点p。p是b在a上的投影点。试想一下,如果我们把WX看成多维的a,即空间中的一个超面来代替二维空间中的直线,而y看成b,那现在要使得(公式二)最小是不是就是寻找(图一)中的e,即垂直于WX的垂线,因为只有垂直时e才最小。下面来看看如何通过寻找垂线并最终得到W。要寻找垂线,先从(图二)中的夹角theta 说起吧,因为当cos(theta)=0时,他们也就垂直了。下面来分析下直线或者向量之间的夹角,如(图二)所示:


(图二)

       在(图二)中, 表示三角形 的斜边,那么:

       角beta也可以得到同样的计算公式,接着利用三角形和差公式得到(公式三):


(公式三)

        (公式三)表示的是两直线或者两向量之间的夹角公式,很多同学都学过。再仔细看下,发现分子其实是向量a,b之间的内积(点积),因此公式三变为简洁的(公式四)的样子:

(公式四)

接下来继续分析(图一)中的投影,为了方便观看,增加了一些提示如(图三)所示:

(图三)

        在(图三)中,假设向量b在向量a中的投影为p(注意,这里都上升为向量空间,不再使用直线,因为(公式四)是通用的)。投影p和a 在同一方向上(也可以反方向),因此我们可以用一个系数乘上a来表示p,比如(图三)中的 ,有了投影向量p,那么我们就可以表示向量e,因为根据向量法则, ,有因为a和e垂直,因此 ,展开求得系数x,如(公式五)所示:


(公式五)

(公式五)是不是很像(公式一)?只不过公式一的分母写成了另外的形式,不过别急,现在的系数只是一个标量数字,因为a,b都是一个向量,我们要扩展一下,把a从向量扩展到子空间,因为(公式一)中的X是样本矩阵,矩阵有列空间和行空间,如(图四)所示:

(图四)

        (图四)中的A表示样本矩阵X,假设它有两个列a1和a2,我们要找一些线性组合系数来找一个和(图三)一样的接受b 投影的向量,而这个向量通过矩阵列和系数的线性组合表示。求解的这个系数的思路和上面完全一样,就是寻找投影所在的向量和垂线e的垂直关系,得到系数,如(公式六)所示:

(公式六)

       这下(公式六)和(公式一)完全一样了,基于最小二乘法的线性回归也就推导完成了,而局部加权回归其实只是相当于对不同样本之间的关系给出了一个权重,所以叫局部加权,如(公式七)所示:

(公式七)

       而权重的计算可通过高斯核(高斯公式)来完成,核的作用就是做权重衰减,很多地方都要用到,表示样本的重要程度,一般离目标进的重要程度大些,高斯核可以很好的描述这种关系。如(公式八)所示,其中K是个超参数,根据情况灵活设置:

(公式八)

       (图五)是当K分别为1.0, 0.01,0.003时的局部加权线性回归的样子,可以看出当K=1.0时,和线性回归没区别:

(图五)

  而岭回归的样子如(公式九)所示:

(公式九)

        岭回归主要是解决的问题就是当XX’无法求逆时,比如当特征很多,样本很少,矩阵X不是满秩矩阵,此时求逆会出错,但是通过加上一个对角为常量lambda的矩阵,就可以很巧妙的避免这个计算问题,因此会多一个参数lambda,lambda的最优选择由交叉验证(cross-validation)来决定,加上一个对角不为0的矩阵很形象的在对角上抬高了,因此称为岭。不同的lambda会使得系数缩减,如(图六)所示:

(图六)

         说到系数缩减大家可能会觉得有奇怪,感觉有点类似于正则,但是这里只是相当于在(公式六)中增大分母,进而缩小系数,另外还有一些系数缩减的方法,比如直接增加一些约束,如(公式十)和(公式十一)所示:

(公式十)

(公式十一)

        当线性回归增加了(公式十)的约束变得和桥回归差不多,系数缩减了,而如果增加了(公式十一)的约束时就是稀疏回归咯,(我自己造的名词,sorry),系数有一些0。

有了约束后,求解起来就不像上面那样直接计算个矩阵运算就行了,回顾第五节说中支持向量机原理,需要使用二次规划求解,不过仍然有一些像SMO算法一样的简化求解算法,比如前向逐步回归方法:

前向逐步回归的伪代码如(图七)所示,也不难,仔细阅读代码就可以理解:

(图七)

 

     下面直接给出上面四种回归的代码:

     

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet(fileName):      #general function to parse tab -delimited floats  
  4.     numFeat = len(open(fileName).readline().split('\t')) - 1 #get number of fields   
  5.     dataMat = []; labelMat = []  
  6.     fr = open(fileName)  
  7.     for line in fr.readlines():  
  8.         lineArr =[]  
  9.         curLine = line.strip().split('\t')  
  10.         for i in range(numFeat):  
  11.             lineArr.append(float(curLine[i]))  
  12.         dataMat.append(lineArr)  
  13.         labelMat.append(float(curLine[-1]))  
  14.     return dataMat,labelMat  
  15.   
  16. def standRegres(xArr,yArr):  
  17.     xMat = mat(xArr); yMat = mat(yArr).T  
  18.     xTx = xMat.T*xMat  
  19.     if linalg.det(xTx) == 0.0:  
  20.         print "This matrix is singular, cannot do inverse"  
  21.         return  
  22.     ws = xTx.I * (xMat.T*yMat)  
  23.     return ws  
  24.   
  25. def lwlr(testPoint,xArr,yArr,k=1.0):  
  26.     xMat = mat(xArr); yMat = mat(yArr).T  
  27.     m = shape(xMat)[0]  
  28.     weights = mat(eye((m)))  
  29.     for j in range(m):                      #next 2 lines create weights matrix  
  30.         diffMat = testPoint - xMat[j,:]     #  
  31.         weights[j,j] = exp(diffMat*diffMat.T/(-2.0*k**2))  
  32.     xTx = xMat.T * (weights * xMat)  
  33.     if linalg.det(xTx) == 0.0:  
  34.         print "This matrix is singular, cannot do inverse"  
  35.         return  
  36.     ws = xTx.I * (xMat.T * (weights * yMat))  
  37.     return testPoint * ws  
  38.   
  39. def lwlrTest(testArr,xArr,yArr,k=1.0):  #loops over all the data points and applies lwlr to each one  
  40.     m = shape(testArr)[0]  
  41.     yHat = zeros(m)  
  42.     for i in range(m):  
  43.         yHat[i] = lwlr(testArr[i],xArr,yArr,k)  
  44.     return yHat  
  45.   
  46. def lwlrTestPlot(xArr,yArr,k=1.0):  #same thing as lwlrTest except it sorts X first  
  47.     yHat = zeros(shape(yArr))       #easier for plotting  
  48.     xCopy = mat(xArr)  
  49.     xCopy.sort(0)  
  50.     for i in range(shape(xArr)[0]):  
  51.         yHat[i] = lwlr(xCopy[i],xArr,yArr,k)  
  52.     return yHat,xCopy  
  53.   
  54. def rssError(yArr,yHatArr): #yArr and yHatArr both need to be arrays  
  55.     return ((yArr-yHatArr)**2).sum()  
  56.   
  57. def ridgeRegres(xMat,yMat,lam=0.2):  
  58.     xTx = xMat.T*xMat  
  59.     denom = xTx + eye(shape(xMat)[1])*lam  
  60.     if linalg.det(denom) == 0.0:  
  61.         print "This matrix is singular, cannot do inverse"  
  62.         return  
  63.     ws = denom.I * (xMat.T*yMat)  
  64.     return ws  
  65.       
  66. def ridgeTest(xArr,yArr):  
  67.     xMat = mat(xArr); yMat=mat(yArr).T  
  68.     yMean = mean(yMat,0)  
  69.     yMat = yMat - yMean     #to eliminate X0 take mean off of Y  
  70.     #regularize X's  
  71.     xMeans = mean(xMat,0)   #calc mean then subtract it off  
  72.     xVar = var(xMat,0)      #calc variance of Xi then divide by it  
  73.     xMat = (xMat - xMeans)/xVar  
  74.     numTestPts = 30  
  75.     wMat = zeros((numTestPts,shape(xMat)[1]))  
  76.     for i in range(numTestPts):  
  77.         ws = ridgeRegres(xMat,yMat,exp(i-10))  
  78.         wMat[i,:]=ws.T  
  79.     return wMat  
  80.   
  81. def regularize(xMat):#regularize by columns  
  82.     inMat = xMat.copy()  
  83.     inMeans = mean(inMat,0)   #calc mean then subtract it off  
  84.     inVar = var(inMat,0)      #calc variance of Xi then divide by it  
  85.     inMat = (inMat - inMeans)/inVar  
  86.     return inMat  
  87.   
  88. def stageWise(xArr,yArr,eps=0.01,numIt=100):  
  89.     xMat = mat(xArr); yMat=mat(yArr).T  
  90.     yMean = mean(yMat,0)  
  91.     yMat = yMat - yMean     #can also regularize ys but will get smaller coef  
  92.     xMat = regularize(xMat)  
  93.     m,n=shape(xMat)  
  94.     #returnMat = zeros((numIt,n)) #testing code remove  
  95.     ws = zeros((n,1)); wsTest = ws.copy(); wsMax = ws.copy()  
  96.     for i in range(numIt):  
  97.         print ws.T  
  98.         lowestError = inf;   
  99.         for j in range(n):  
  100.             for sign in [-1,1]:  
  101.                 wsTest = ws.copy()  
  102.                 wsTest[j] += eps*sign  
  103.                 yTest = xMat*wsTest  
  104.                 rssE = rssError(yMat.A,yTest.A)  
  105.                 if rssE < lowestError:  
  106.                     lowestError = rssE  
  107.                     wsMax = wsTest  
  108.         ws = wsMax.copy()  
  109.         #returnMat[i,:]=ws.T  
  110.     #return returnMat  
  111.   
  112. #def scrapePage(inFile,outFile,yr,numPce,origPrc):  
  113. #    from BeautifulSoup import BeautifulSoup  
  114. #    fr = open(inFile); fw=open(outFile,'a') #a is append mode writing  
  115. #    soup = BeautifulSoup(fr.read())  
  116. #    i=1  
  117. #    currentRow = soup.findAll('table', r="%d" % i)  
  118. #    while(len(currentRow)!=0):  
  119. #        title = currentRow[0].findAll('a')[1].text  
  120. #        lwrTitle = title.lower()  
  121. #        if (lwrTitle.find('new') > -1) or (lwrTitle.find('nisb') > -1):  
  122. #            newFlag = 1.0  
  123. #        else:  
  124. #            newFlag = 0.0  
  125. #        soldUnicde = currentRow[0].findAll('td')[3].findAll('span')  
  126. #        if len(soldUnicde)==0:  
  127. #            print "item #%d did not sell" % i  
  128. #        else:  
  129. #            soldPrice = currentRow[0].findAll('td')[4]  
  130. #            priceStr = soldPrice.text  
  131. #            priceStr = priceStr.replace('$','') #strips out $  
  132. #            priceStr = priceStr.replace(',','') #strips out ,  
  133. #            if len(soldPrice)>1:  
  134. #                priceStr = priceStr.replace('Free shipping', '') #strips out Free Shipping  
  135. #            print "%s\t%d\t%s" % (priceStr,newFlag,title)  
  136. #            fw.write("%d\t%d\t%d\t%f\t%s\n" % (yr,numPce,newFlag,origPrc,priceStr))  
  137. #        i += 1  
  138. #        currentRow = soup.findAll('table', r="%d" % i)  
  139. #    fw.close()  
  140.       
  141. from time import sleep  
  142. import json  
  143. import urllib2  
  144. def searchForSet(retX, retY, setNum, yr, numPce, origPrc):  
  145.     sleep(10)  
  146.     myAPIstr = 'AIzaSyD2cR2KFyx12hXu6PFU-wrWot3NXvko8vY'  
  147.     searchURL = 'https://www.googleapis.com/shopping/search/v1/public/products?key=%s&country=US&q=lego+%d&alt=json' % (myAPIstr, setNum)  
  148.     pg = urllib2.urlopen(searchURL)  
  149.     retDict = json.loads(pg.read())  
  150.     for i in range(len(retDict['items'])):  
  151.         try:  
  152.             currItem = retDict['items'][i]  
  153.             if currItem['product']['condition'] == 'new':  
  154.                 newFlag = 1  
  155.             else: newFlag = 0  
  156.             listOfInv = currItem['product']['inventories']  
  157.             for item in listOfInv:  
  158.                 sellingPrice = item['price']  
  159.                 if  sellingPrice > origPrc * 0.5:  
  160.                     print "%d\t%d\t%d\t%f\t%f" % (yr,numPce,newFlag,origPrc, sellingPrice)  
  161.                     retX.append([yr, numPce, newFlag, origPrc])  
  162.                     retY.append(sellingPrice)  
  163.         exceptprint 'problem with item %d' % i  
  164.       
  165. def setDataCollect(retX, retY):  
  166.     searchForSet(retX, retY, 8288200680049.99)  
  167.     searchForSet(retX, retY, 1003020023096269.99)  
  168.     searchForSet(retX, retY, 1017920075195499.99)  
  169.     searchForSet(retX, retY, 1018120073428199.99)  
  170.     searchForSet(retX, retY, 1018920085922299.99)  
  171.     searchForSet(retX, retY, 1019620093263249.99)  
  172.       
  173. def crossValidation(xArr,yArr,numVal=10):  
  174.     m = len(yArr)                             
  175.     indexList = range(m)  
  176.     errorMat = zeros((numVal,30))#create error mat 30columns numVal rows  
  177.     for i in range(numVal):  
  178.         trainX=[]; trainY=[]  
  179.         testX = []; testY = []  
  180.         random.shuffle(indexList)  
  181.         for j in range(m):#create training set based on first 90% of values in indexList  
  182.             if j < m*0.9:   
  183.                 trainX.append(xArr[indexList[j]])  
  184.                 trainY.append(yArr[indexList[j]])  
  185.             else:  
  186.                 testX.append(xArr[indexList[j]])  
  187.                 testY.append(yArr[indexList[j]])  
  188.         wMat = ridgeTest(trainX,trainY)    #get 30 weight vectors from ridge  
  189.         for k in range(30):#loop over all of the ridge estimates  
  190.             matTestX = mat(testX); matTrainX=mat(trainX)  
  191.             meanTrain = mean(matTrainX,0)  
  192.             varTrain = var(matTrainX,0)  
  193.             matTestX = (matTestX-meanTrain)/varTrain #regularize test with training params  
  194.             yEst = matTestX * mat(wMat[k,:]).T + mean(trainY)#test ridge results and store  
  195.             errorMat[i,k]=rssError(yEst.T.A,array(testY))  
  196.             #print errorMat[i,k]  
  197.     meanErrors = mean(errorMat,0)#calc avg performance of the different ridge weight vectors  
  198.     minMean = float(min(meanErrors))  
  199.     bestWeights = wMat[nonzero(meanErrors==minMean)]  
  200.     #can unregularize to get model  
  201.     #when we regularized we wrote Xreg = (x-meanX)/var(x)  
  202.     #we can now write in terms of x not Xreg:  x*w/var(x) - meanX/var(x) +meanY  
  203.     xMat = mat(xArr); yMat=mat(yArr).T  
  204.     meanX = mean(xMat,0); varX = var(xMat,0)  
  205.     unReg = bestWeights/varX  
  206.     print "the best model from Ridge Regression is:\n",unReg  
  207.     print "with constant term: ",-1*sum(multiply(meanX,unReg)) + mean(yMat)  


以上各种回归方法没有考虑实际数据的噪声,如果噪声很多,直接用上述的回归不是太好,因此需要加上正则,然后迭代更新权重

参考文献:

         [1] machine learning in action.Peter Harrington 

       [2]Linear Algebra and Its Applications_4ed.Gilbert_Strang

  前一节的回归是一种全局回归模型,它设定了一个模型,不管是线性还是非线性的模型,然后拟合数据得到参数,现实中会有些数据很复杂,肉眼几乎看不出符合那种模型,因此构建全局的模型就有点不合适。这节介绍的树回归就是为了解决这类问题,它通过构建决策节点把数据数据切分成区域,然后局部区域进行回归拟合。先来看看分类回归树吧(CART:Classification And Regression Trees),这个模型优点就是上面所说,可以对复杂和非线性的数据进行建模,缺点是得到的结果不容易理解。顾名思义它可以做分类也可以做回归,至于分类前面在说决策树时已经说过了,这里略过。直接通过分析回归树的代码来理解吧:

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet(fileName):      #general function to parse tab -delimited floats  
  4.     dataMat = []                #assume last column is target value  
  5.     fr = open(fileName)  
  6.     for line in fr.readlines():  
  7.         curLine = line.strip().split('\t')  
  8.         fltLine = map(float,curLine) #map all elements to float()  
  9.         dataMat.append(fltLine)  
  10.     return dataMat  
  11.   
  12. def binSplitDataSet(dataSet, feature, value):  
  13.     mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]  
  14.     mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][0]  
  15.     return mat0,mat1  

上面两个函数,第一个函数加载样本数据,第二个函数用来指定在某个特征和维度上切分数据,示例如(图一)所示:


(图一)

       注意一下,CART是一种通过二元切分来构建树的,前面的决策树的构建是通过香农熵最小作为度量,树的节点是个离散的阈值;这里不再使用香农熵,因为我们要做回归,因此这里使用计算分割数据的方差作为度量,而树的节点也对应使用使得方差最小的某个连续数值(其实是特征值)。试想一下,如果方差越小,说明误差那个节点最能表述那块数据。下面来看看树的构建代码:

[python]  view plain copy
  1. def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):#assume dataSet is NumPy Mat so we can array filtering  
  2.     feat, val = chooseBestSplit(dataSet, leafType, errType, ops)#choose the best split  
  3.     if feat == Nonereturn val #if the splitting hit a stop condition return val(叶子节点值)  
  4.     retTree = {}  
  5.     retTree['spInd'] = feat  
  6.     retTree['spVal'] = val  
  7.     lSet, rSet = binSplitDataSet(dataSet, feat, val)  
  8.     retTree['left'] = createTree(lSet, leafType, errType, ops)  
  9.     retTree['right'] = createTree(rSet, leafType, errType, ops)  
  10.     return retTree    

         这段代码中主要工作任务就是选择最佳分割特征,然后分割,是叶子节点就返回,不是叶子节点就递归的生成树结构。其中调用了最佳分割特征的函数:chooseBestSplit,前面决策树的构建中,这个函数里用熵来度量,这里采用误差(方差)来度量,同样先看代码:

[python]  view plain copy
  1. def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)):  
  2.     tolS = ops[0]; tolN = ops[1]  
  3.     #if all the target variables are the same value: quit and return value  
  4.     if len(set(dataSet[:,-1].T.tolist()[0])) == 1#exit cond 1  
  5.         return None, leafType(dataSet)  
  6.     m,n = shape(dataSet)  
  7.     #the choice of the best feature is driven by Reduction in RSS error from mean  
  8.     S = errType(dataSet)  
  9.     bestS = inf; bestIndex = 0; bestValue = 0  
  10.     for featIndex in range(n-1):  
  11.         for splitVal in set(dataSet[:,featIndex]):  
  12.             mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)  
  13.             if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN): continue  
  14.             newS = errType(mat0) + errType(mat1)  
  15.             if newS < bestS:   
  16.                 bestIndex = featIndex  
  17.                 bestValue = splitVal  
  18.                 bestS = newS  
  19.     #if the decrease (S-bestS) is less than a threshold don't do the split  
  20.     if (S - bestS) < tolS:   
  21.         return None, leafType(dataSet) #exit cond 2  
  22.     mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)  
  23.     if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN):  #exit cond 3  
  24.         return None, leafType(dataSet)  
  25.     return bestIndex,bestValue#returns the best feature to split on  
  26.                               #and the value used for that split  

这段代码的主干是:

遍历每个特征:

       遍历每个特征值:

               把数据集切分成两份

               计算此时的切分误差

               如果切分误差小于当前最小误差,更新最小误差值,当前切分为最佳切分

返回最佳切分的特征值和阈值

 

       尤其注意最后的返回值,因为它是构建树每个节点成分的东西。另外代码中errType=regErr 调用了regErr函数来计算方差,下面给出:

               
[python]  view plain copy
  1. def regErr(dataSet):  
  2.     return var(dataSet[:,-1]) * shape(dataSet)[0]  

       如果误差变化不大时(代码中(S - bestS)),则生成叶子节点,叶子节点函数是:

[python]  view plain copy
  1. def regLeaf(dataSet):#returns the value used for each leaf  
  2.     return mean(dataSet[:,-1])  

这样回归树构建的代码就初步分析完毕了,运行结果如(图二)所示:

(图二)

         数据ex00.txt在文章最后给出,它的分布如(图三)所示:

(图三)

       根据(图三),我们可以大概看出(图二)的代码的运行结果具有一定的合理性,选用X(用0表示)特征作为分割特征,然后左右节点各选了一个中心值来描述树回归。节点比较少,但很能说明问题,下面给出一个比较复杂数据跑出的结果,如(图四)所示:


(图四)

对应的数据如(图五)所示:



(图五)

            对于树的叶子节点和节点值的合理性,大家逐个对照(图五)来验证吧。下面简单的说下树的修剪,如果特征维度比较高,很容易发生节点过多,造成过拟合,过拟合(overfit)会出现high variance, 而欠拟合(under fit)会出现high bias,这点是题外话,因为机器学习理论一般要讲这些,当出现过拟合时,一般使用正则方法,由于回归树没有建立目标函数,因此这里解决过拟合的方法就是修剪树,简单的说就是使用少量的、关键的特征来判别,下面来看看如何修剪树:很简单,就是递归的遍历一个子树,从叶子节点开始,计算同一父节点的两个子节点合并后的误差,再计算不合并的误差,如果合并会降低误差,就把叶子节点合并。说到误差,其实前面的chooseBestSplit函数里有一句代码:

[python]  view plain copy
  1. #if the decrease (S-bestS) is less than a threshold don't do the split  
  2.    if (S - bestS) < tolS:   

         tolS 是个阈值,当误差变化不太大时,就不再分裂下去,其实也是修剪树的方法,只不过它是事前修剪,而计算合并误差的则是事后修剪。下面是其代码:

[python]  view plain copy
  1. def getMean(tree):  
  2.     if isTree(tree['right']): tree['right'] = getMean(tree['right'])  
  3.     if isTree(tree['left']): tree['left'] = getMean(tree['left'])  
  4.     return (tree['left']+tree['right'])/2.0  
  5.       
  6. def prune(tree, testData):  
  7.     if shape(testData)[0] == 0return getMean(tree) #if we have no test data collapse the tree  
  8.     if (isTree(tree['right']) or isTree(tree['left'])):#if the branches are not trees try to prune them  
  9.         lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])  
  10.     if isTree(tree['left']): tree['left'] = prune(tree['left'], lSet)  
  11.     if isTree(tree['right']): tree['right'] =  prune(tree['right'], rSet)  
  12.     #if they are now both leafs, see if we can merge them  
  13.     if not isTree(tree['left']) and not isTree(tree['right']):  
  14.         lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])  
  15.         errorNoMerge = sum(power(lSet[:,-1] - tree['left'],2)) +\  
  16.             sum(power(rSet[:,-1] - tree['right'],2))  
  17.         treeMean = (tree['left']+tree['right'])/2.0  
  18.         errorMerge = sum(power(testData[:,-1] - treeMean,2))  
  19.         if errorMerge < errorNoMerge:   
  20.             print "merging"  
  21.             return treeMean  
  22.         elsereturn tree  
  23.     elsereturn tree  

        说完了树回归,再简单的提下模型树,因为树回归每个节点是一些特征和特征值,选取的原则是根据特征方差最小。如果把叶子节点换成分段线性函数,那么就变成了模型树,如(图六)所示:

(图六)

      (图六)中明显是两个直线组成,以X坐标(0.0-0.3)和(0.3-1.0)分成的两个线段。如果我们用两个叶子节点保存两个线性回归模型,就完成了这部分数据的拟合。实现也比较简单,代码如下:

[python]  view plain copy
  1. def linearSolve(dataSet):   #helper function used in two places  
  2.     m,n = shape(dataSet)  
  3.     X = mat(ones((m,n))); Y = mat(ones((m,1)))#create a copy of data with 1 in 0th postion  
  4.     X[:,1:n] = dataSet[:,0:n-1]; Y = dataSet[:,-1]#and strip out Y  
  5.     xTx = X.T*X  
  6.     if linalg.det(xTx) == 0.0:  
  7.         raise NameError('This matrix is singular, cannot do inverse,\n\  
  8.         try increasing the second value of ops')  
  9.     ws = xTx.I * (X.T * Y)  
  10.     return ws,X,Y  
  11.   
  12. def modelLeaf(dataSet):#create linear model and return coeficients  
  13.     ws,X,Y = linearSolve(dataSet)  
  14.     return ws  
  15.   
  16. def modelErr(dataSet):  
  17.     ws,X,Y = linearSolve(dataSet)  
  18.     yHat = X * ws  
  19.     return sum(power(Y - yHat,2))  

       代码和树回归相似,只不过modelLeaf在返回叶子节点时,要完成一个线性回归,由linearSolve来完成。最后一个函数modelErr则和回归树的regErr函数起着同样的作用。

谢天谢地,这篇文章一个公式都没有出现,但同时也希望没有数学的语言,表述会清楚。


数据ex00.txt:

0.036098 0.155096

0.993349 1.077553

0.530897 0.893462

0.712386 0.564858

0.343554 -0.371700

0.098016 -0.332760

0.691115 0.834391

0.091358 0.099935

0.727098 1.000567

0.951949 0.945255

0.768596 0.760219

0.541314 0.893748

0.146366 0.034283

0.673195 0.915077

0.183510 0.184843

0.339563 0.206783

0.517921 1.493586

0.703755 1.101678

0.008307 0.069976

0.243909 -0.029467

0.306964 -0.177321

0.036492 0.408155

0.295511 0.002882

0.837522 1.229373

0.202054 -0.087744

0.919384 1.029889

0.377201 -0.243550

0.814825 1.095206

0.611270 0.982036

0.072243 -0.420983

0.410230 0.331722

0.869077 1.114825

0.620599 1.334421

0.101149 0.068834

0.820802 1.325907

0.520044 0.961983

0.488130 -0.097791

0.819823 0.835264

0.975022 0.673579

0.953112 1.064690

0.475976 -0.163707

0.273147 -0.455219

0.804586 0.924033

0.074795 -0.349692

0.625336 0.623696

0.656218 0.958506

0.834078 1.010580

0.781930 1.074488

0.009849 0.056594

0.302217 -0.148650

0.678287 0.907727

0.180506 0.103676

0.193641 -0.327589

0.343479 0.175264

0.145809 0.136979

0.996757 1.035533

0.590210 1.336661

0.238070 -0.358459

0.561362 1.070529

0.377597 0.088505

0.099142 0.025280

0.539558 1.053846

0.790240 0.533214

0.242204 0.209359

0.152324 0.132858

0.252649 -0.055613

0.895930 1.077275

0.133300 -0.223143

0.559763 1.253151

0.643665 1.024241

0.877241 0.797005

0.613765 1.621091

0.645762 1.026886

0.651376 1.315384

0.697718 1.212434

0.742527 1.087056

0.901056 1.055900

0.362314 -0.556464

0.948268 0.631862

0.000234 0.060903

0.750078 0.906291

0.325412 -0.219245

0.726828 1.017112

0.348013 0.048939

0.458121 -0.061456

0.280738 -0.228880

0.567704 0.969058

0.750918 0.748104

0.575805 0.899090

0.507940 1.107265

0.071769 -0.110946

0.553520 1.391273

0.401152 -0.121640

0.406649 -0.366317

0.652121 1.004346

0.347837 -0.153405

0.081931 -0.269756

0.821648 1.280895

0.048014 0.064496

0.130962 0.184241

0.773422 1.125943

0.789625 0.552614

0.096994 0.227167

0.625791 1.244731

0.589575 1.185812

0.323181 0.180811

0.822443 1.086648

0.360323 -0.204830

0.950153 1.022906

0.527505 0.879560

0.860049 0.717490

0.007044 0.094150

0.438367 0.034014

0.574573 1.066130

0.536689 0.867284

0.782167 0.886049

0.989888 0.744207

0.761474 1.058262

0.985425 1.227946

0.132543 -0.329372

0.346986 -0.150389

0.768784 0.899705

0.848921 1.170959

0.449280 0.069098

0.066172 0.052439

0.813719 0.706601

0.661923 0.767040

0.529491 1.022206

0.846455 0.720030

0.448656 0.026974

0.795072 0.965721

0.118156 -0.077409

0.084248 -0.019547

0.845815 0.952617

0.576946 1.234129

0.772083 1.299018

0.696648 0.845423

0.595012 1.213435

0.648675 1.287407

0.897094 1.240209

0.552990 1.036158

0.332982 0.210084

0.065615 -0.306970

0.278661 0.253628

0.773168 1.140917

0.203693 -0.064036

0.355688 -0.119399

0.988852 1.069062

0.518735 1.037179

0.514563 1.156648

0.976414 0.862911

0.919074 1.123413

0.697777 0.827805

0.928097 0.883225

0.900272 0.996871

0.344102 -0.061539

0.148049 0.204298

0.130052 -0.026167

0.302001 0.317135

0.337100 0.026332

0.314924 -0.001952

0.269681 -0.165971

0.196005 -0.048847

0.129061 0.305107

0.936783 1.026258

0.305540 -0.115991

0.683921 1.414382

0.622398 0.766330

0.902532 0.861601

0.712503 0.933490

0.590062 0.705531

0.723120 1.307248

0.188218 0.113685

0.643601 0.782552

0.520207 1.209557

0.233115 -0.348147

0.465625 -0.152940

0.884512 1.117833

0.663200 0.701634

0.268857 0.073447

0.729234 0.931956

0.429664 -0.188659

0.737189 1.200781

0.378595 -0.296094

0.930173 1.035645

0.774301 0.836763

0.273940 -0.085713

0.824442 1.082153

0.626011 0.840544

0.679390 1.307217

0.578252 0.921885

0.785541 1.165296

0.597409 0.974770

0.014083 -0.132525

0.663870 1.187129

0.552381 1.369630

0.683886 0.999985

0.210334 -0.006899

0.604529 1.212685

0.250744 0.046297


机器学习理论与实战(十)K均值聚类和二分K均值聚类

   接下来就要说下无监督机器学习方法,所谓无监督机器学习前面也说过,就是没有标签的情况,对样本数据进行聚类分析、关联性分析等。主要包括K均值聚类(K-means clustering)和关联分析,这两大类都可以说的很简单也可以说的很复杂,学术的东西本身就一直在更新着。比如K均值聚类可以扩展一下形成层次聚类(Hierarchical Clustering),也可以进入概率分布的空间进行聚类,就像前段时间很火的LDA聚类,虽然最近深度玻尔兹曼机(DBM)打败了它,但它也是自然语言处理领域(NLP:Natural Language Processing)的一个有力工具,有过辉煌的一段故事。而关联性分析又是另外一个比较有力的工具,它又称购物篮分析,我们可以大概可以体会到它的用途,挖掘目标之间的关联性,经典的故事就是啤酒和尿布的关联。另外多说一下,google最近的两大核心技术:深度学习和知识图,深度学习就不说了,而知识图就是挖掘关系的。找到了关系就找到了金矿,关系也可以用复杂网络(complex network)来建模。这些话题就打住吧,今天就来说下K均值聚类和二分K均值聚类。

       K均值聚类比较简单,再说原理之前,先来看个样本图,如(图一)所示:


(图一)

       假如(图一)中是我们的样本数据,每个样本都没有类别,我们想对他们简单的划下类,在(图一)中明显的有四“堆”数据,我们用什么方法能把他们分成四类呢?K均值聚类就是解决这种问题的(好腻 = =!),K均值聚类的原理如下:

随机的分配K个质心(上图中K为4)


如果样本中任意一个数据点归属的簇号(堆类别)发生改变

遍历每一个样本点

    遍历每一个质心

           计算数据点到质心的距离

    把数据点分配到距其最近的簇

    对每一个簇,重新计算簇中所有样本点的均值作为质心

 

K均值简单的一句话总结就是:更新质心,更新每个样本的所属的类别。按照上述更新规则,当没有样本的簇号发生改变了,迭代也就终止咯。下面就来看看代码吧:

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet(fileName):      #general function to parse tab -delimited floats  
  4.     dataMat = []                #assume last column is target value  
  5.     fr = open(fileName)  
  6.     for line in fr.readlines():  
  7.         curLine = line.strip().split('\t')  
  8.         fltLine = map(float,curLine) #map all elements to float()  
  9.         dataMat.append(fltLine)  
  10.     return dataMat  
  11.   
  12. def distEclud(vecA, vecB):  
  13.     return sqrt(sum(power(vecA - vecB, 2))) #la.norm(vecA-vecB)  
  14.   
  15. def randCent(dataSet, k):  
  16.     n = shape(dataSet)[1]  
  17.     centroids = mat(zeros((k,n)))#create centroid mat  
  18.     for j in range(n):#create random cluster centers, within bounds of each dimension  
  19.         minJ = min(dataSet[:,j])   
  20.         rangeJ = float(max(dataSet[:,j]) - minJ)  
  21.         centroids[:,j] = mat(minJ + rangeJ * random.rand(k,1))  
  22.     return centroids  
  23.       
  24. def kMeans(dataSet, k, distMeas=distEclud, createCent=randCent):  
  25.     m = shape(dataSet)[0]  
  26.     clusterAssment = mat(zeros((m,2)))#create mat to assign data points   
  27.                                       #to a centroid, also holds SE of each point  
  28.     centroids = createCent(dataSet, k)  
  29.     clusterChanged = True  
  30.     while clusterChanged:  
  31.         clusterChanged = False  
  32.         for i in range(m):#for each data point assign it to the closest centroid  
  33.             minDist = inf; minIndex = -1  
  34.             for j in range(k):  
  35.                 distJI = distMeas(centroids[j,:],dataSet[i,:])  
  36.                 if distJI < minDist:  
  37.                     minDist = distJI; minIndex = j  
  38.             if clusterAssment[i,0] != minIndex: clusterChanged = True  
  39.             clusterAssment[i,:] = minIndex,minDist**2  
  40.         print centroids  
  41.         for cent in range(k):#recalculate centroids  
  42.             ptsInClust = dataSet[nonzero(clusterAssment[:,0].A==cent)[0]]#get all the point in this cluster  
  43.             centroids[cent,:] = mean(ptsInClust, axis=0#assign centroid to mean   
  44.     return centroids, clusterAssment  

       代码也很简单,其中函数loadDataSet用来加载数据集,函数distEclud用来计算两个样本的距离,函数randCent为样本随机的分配K个质心(centroid),另外注意一下样本的质心维度和样本维度是一样的,这个应该没有异议,高维空间中的坐标,最后函数kMeans则是k均值聚类算法的核心步骤,和原理都是一一对应的。下面是运行结果:


(图二)

        从(图二)可以看出质心的更新,不过按照k-means的聚类规则很容易陷入局部最小,陷入局部最小说简单的就是随机初始的质心分布的不是太好,最后迭代终止后,两个质心有可能在同一堆数据上,而另外一个质心成了另外两堆离的近样本的唯一质心(如下面图三所示)。说的复杂一些就是马尔科夫岁机场中配置的代价函数不是好的目标函数(虽然这里没写出这个函数)。为了解决这个问题,有人提出了二分K均值聚类算法,该算法也比较简单,首先把所有样本作为一个簇,然后二分该簇,接着选择其中一个簇进行继续进行二分。选择哪一个簇二分的原则就是能否使得误差平方和(SSE: Sum of Squared Error)进可能的小。也就是说该算法有了个好的目标函数,SSE的计算其实就是距离和。下面来看看二分K均值聚类算法的代码:

[python]  view plain copy
  1. def biKmeans(dataSet, k, distMeas=distEclud):  
  2.     m = shape(dataSet)[0]  
  3.     clusterAssment = mat(zeros((m,2)))  
  4.     centroid0 = mean(dataSet, axis=0).tolist()[0]  
  5.     centList =[centroid0] #create a list with one centroid  
  6.     for j in range(m):#calc initial Error  
  7.         clusterAssment[j,1] = distMeas(mat(centroid0), dataSet[j,:])**2  
  8.     while (len(centList) < k):  
  9.         lowestSSE = inf  
  10.         for i in range(len(centList)):  
  11.             ptsInCurrCluster = dataSet[nonzero(clusterAssment[:,0].A==i)[0],:]#get the data points currently in cluster i  
  12.             centroidMat, splitClustAss = kMeans(ptsInCurrCluster, 2, distMeas)  
  13.             sseSplit = sum(splitClustAss[:,1])#compare the SSE to the currrent minimum  
  14.             sseNotSplit = sum(clusterAssment[nonzero(clusterAssment[:,0].A!=i)[0],1])  
  15.             print "sseSplit, and notSplit: ",sseSplit,sseNotSplit  
  16.             if (sseSplit + sseNotSplit) < lowestSSE:  
  17.                 bestCentToSplit = i  
  18.                 bestNewCents = centroidMat  
  19.                 bestClustAss = splitClustAss.copy()  
  20.                 lowestSSE = sseSplit + sseNotSplit  
  21.         bestClustAss[nonzero(bestClustAss[:,0].A == 1)[0],0] = len(centList) #change 1 to 3,4, or whatever  
  22.         bestClustAss[nonzero(bestClustAss[:,0].A == 0)[0],0] = bestCentToSplit  
  23.         print 'the bestCentToSplit is: ',bestCentToSplit  
  24.         print 'the len of bestClustAss is: ', len(bestClustAss)  
  25.         centList[bestCentToSplit] = bestNewCents[0,:].tolist()[0]#replace a centroid with two best centroids   
  26.         centList.append(bestNewCents[1,:].tolist()[0])  
  27.         clusterAssment[nonzero(clusterAssment[:,0].A == bestCentToSplit)[0],:]= bestClustAss#reassign new clusters, and SSE  
  28.     return mat(centList), clusterAssment  

       这个算法比K均值聚类算法效果好一些,比如(图三)是K均值算法在随机初始化不好的情况下聚类的效果示意图:

(图三)

         而用二分K均值聚类跑出的效果图如(图四)所示:

(图四)

 

       到此为止,关于K均值的聚类算法也就说完了,虽然有二分K均值聚类算法改进了K均值聚类算法的不足,但也不是没缺点,它们的共同的缺点就是必须事先确定K的值,现实中的数据我们有可能不知道K的值,如何确定K的值也是学术界一直在研究的问题,现在常用的解决办法是用层次聚类(Hierarchical Clustering),或者借鉴下LDA中的话题聚类分析。最后多说一句:谱聚类也是图像中的一个大成员,用途很广,典型的就是图像分割。

 

机器学习理论与实战(十一)关联规则分析Apriori

《机器学习实战》的最后的两个算法对我来说有点陌生,但学过后感觉蛮好玩,了解了一般的商品数据关联分析和搜索引擎智能提示的工作原理。先来看看关联分析(association analysis)吧,它又称关联规则学习(association rule learning),它的主要工作就是快速找到经常在一起的频繁项,比如著名的“啤酒”和“尿布”。试想一下,给我们一堆交易数据,每次的交易数据中有不同的商品,要我们从中发掘哪些商品经常被一起购买?当然穷举法也可以解决,但是计算量很大,这节的算法Apriori就是解决此类问题的快速算法。Apriori在拉丁语中表示“from before”(来自以前)的意思,在这里就是来自于子集的频繁信息咯,不懂,别着急。下面给出几个简单的交易数据,如(表一)所示:

交易号

商品

0

豆奶

生菜

 

 

1

生菜

尿布

葡萄酒

甜菜

2

豆奶

尿布

葡萄酒

橙汁

3

生菜

豆奶

尿布

葡萄酒

4

生菜

豆奶

尿布

橙汁

                                            (表一)    商品交易数据

 

     我们的目标就是找到经常在一起出现的频繁子集,集合哦。我们用大括号“{}”来表示集合。为了表示某个子集是否是频繁子集,我们需要用一些量化方法,光计数也不行,因为不同量的交易数据出现的次数差别很大,一般用支持度(support)和置信度(confident)这两个指标来量化频繁子集、关联规则。这两个指标的计算都很简单:

 

支持度=(包含该子集的交易数目)/总交易数目

比如{豆奶}的支持度为4/5,有四条交易数据都有豆奶,而{豆奶,尿布}的支持度为3/5。

 

置信度只是针对像{尿布}->{葡萄酒}的关联规则来定义的:

尿布到葡萄酒的置信度=支持度({尿布,葡萄酒})/支持度(尿布)

 

比如在(表一)中,{尿布,葡萄酒}的支持度为3/5,而尿布的支持度为4/5,那么尿布->葡萄酒的可信度为3/4=0.75。

 

尽管有了量化指标,但是要让我们在大规模的数据中计算所有的组合的支持度和关联规则的支持度工作量也很大,如(图一)所示:


(图一) 商品{0,1,2,3}所有可能的组合

 

      (图一)中只有四种商品,而其子集则有2^4-1=15个(空子集除外),每计算一个子集的两个指标都需要遍历一下数据,要遍历15次,如果有100种商品,则有1.26*10^30个子集,这个计算量很大,所幸的是研究人员发现了一种Apriori原理:Apriori原理是说如果某个子集不是频繁的,那么包含它的所有超集也不是频繁的,这样一下就砍掉了一大半,如(图二)所示:


(图二)  Apriori原理

 

        (图二)中{2,3}不是频繁的,那么所有包含它的超子集都不是频繁的,有了这个原则,就使得我们计算频繁子集的变成可行的。有了Apriori原理作为指导,我们还需要一些trick 来实现代码,这些trick就构成了Apriori算法:

先生成所有单个商品构成子集列表

然后计算每个子集的支持度

剔除不满足最小支持度的子集

接下来对满足要求的子集进行两两组合(但组合一定是某个交易的数据的子集)

然后再计算组合的支持度

再剔除,

依次按上述循环直到剔除完毕。

返回频繁子集

 

上述算法的伪代码如(图三)所示:

(图三)

下面直接给出代码,没有公式,就根据上面的伪代码,读者都可以尝试自己写代码:

[python]  view plain copy
  1. from numpy import *  
  2.   
  3. def loadDataSet():  
  4.     return [[134], [235], [1235], [25]]  
  5.   
  6. def createC1(dataSet):  
  7.     C1 = []  
  8.     for transaction in dataSet:  
  9.         for item in transaction:  
  10.             if not [item] in C1:  
  11.                 C1.append([item])  
  12.                   
  13.     C1.sort()  
  14.     return map(frozenset, C1)#use frozen set so we  
  15.                             #can use it as a key in a dict      
  16.   
  17. def scanD(D, Ck, minSupport):  
  18.     ssCnt = {}  
  19.     for tid in D:  
  20.         for can in Ck:  
  21.             if can.issubset(tid):  
  22.                 if not ssCnt.has_key(can): ssCnt[can]=1  
  23.                 else: ssCnt[can] += 1  
  24.     numItems = float(len(D))  
  25.     retList = []  
  26.     supportData = {}  
  27.     for key in ssCnt:  
  28.         support = ssCnt[key]/numItems  
  29.         if support >= minSupport:  
  30.             retList.insert(0,key)  
  31.         supportData[key] = support  
  32.     return retList, supportData  
  33.   
  34. def aprioriGen(Lk, k): #creates Ck  
  35.     retList = []  
  36.     lenLk = len(Lk)  
  37.     for i in range(lenLk):  
  38.         for j in range(i+1, lenLk):   
  39.             L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]  
  40.             L1.sort(); L2.sort()  
  41.             if L1==L2: #if first k-2 elements are equal  
  42.                 retList.append(Lk[i] | Lk[j]) #set union  
  43.     return retList  
  44.   
  45. def apriori(dataSet, minSupport = 0.5):  
  46.     C1 = createC1(dataSet)  
  47.     D = map(set, dataSet)  
  48.     L1, supportData = scanD(D, C1, minSupport)  
  49.     L = [L1]  
  50.     k = 2  
  51.     while (len(L[k-2]) > 0):  
  52.         Ck = aprioriGen(L[k-2], k)  
  53.         Lk, supK = scanD(D, Ck, minSupport)#scan DB to get Lk  
  54.         supportData.update(supK)  
  55.         L.append(Lk)  
  56.         k += 1  
  57.     return L, supportData  

另外多说一点关于关联规则的挖掘,有了频繁子集,就是挖掘关联规则,和频繁子集一样的道理,只不过改计算置信度(confident),计算方法也同上,如(图四)所示:

(图四) 关联规则挖掘

      (图四)和(图二)中的否定关系是反过来的,关联规则挖掘的原则是:如果某个关联规则不满足最小置信度,那么它的所有子集都要被舍去,都是不满足最小置信度的,这点并不是绝对的,读者可以根据自己的需求个性化修改。代码如下:

[python]  view plain copy
  1. def generateRules(L, supportData, minConf=0.7):  #supportData is a dict coming from scanD  
  2.     bigRuleList = []  
  3.     for i in range(1, len(L)):#only get the sets with two or more items  
  4.         for freqSet in L[i]:  
  5.             H1 = [frozenset([item]) for item in freqSet]  
  6.             if (i > 1):  
  7.                 rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)  
  8.             else:  
  9.                 calcConf(freqSet, H1, supportData, bigRuleList, minConf)  
  10.     return bigRuleList           
  11.   
  12. def calcConf(freqSet, H, supportData, brl, minConf=0.7):  
  13.     prunedH = [] #create new list to return  
  14.     for conseq in H:  
  15.         conf = supportData[freqSet]/supportData[freqSet-conseq] #calc confidence  
  16.         if conf >= minConf:   
  17.             print freqSet-conseq,'-->',conseq,'conf:',conf  
  18.             brl.append((freqSet-conseq, conseq, conf))  
  19.             prunedH.append(conseq)  
  20.     return prunedH  
  21.   
  22. def rulesFromConseq(freqSet, H, supportData, brl, minConf=0.7):  
  23.     m = len(H[0])  
  24.     if (len(freqSet) > (m + 1)): #try further merging  
  25.         Hmp1 = aprioriGen(H, m+1)#create Hm+1 new candidates  
  26.         Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf)  
  27.         if (len(Hmp1) > 1):    #need at least two sets to merge  
  28.             rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)  
  29.               
  30. def pntRules(ruleList, itemMeaning):  
  31.     for ruleTup in ruleList:  
  32.         for item in ruleTup[0]:  
  33.             print itemMeaning[item]  
  34.         print "           -------->"  
  35.         for item in ruleTup[1]:  
  36.             print itemMeaning[item]  
  37.         print "confidence: %f" % ruleTup[2]  
  38.         print       #print a blank line  

转载请注明来源: http://blog.csdn.net/marvin521/article/details/9751589


参考文献:

 [1] machine learning in action.Peter Harrington 




  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值