《机器学习实战》读书笔记(第一部分:分类)

参考《机器学习实战》

一、KNN

原理:
步骤:

  1. 数据处理归一化(x-min)/(max-min)
  2. 计算已知类别数据集中的点与当前点之间的距离
  3. 选取与当前点距离最小的k个点
  4. 确定k个点所在类别出现的频率,返回频率最高的类别作为当前点的预测分类
def classify0(inX,dataSet,labels,k):
	dataSetSize=dataSet,shape[0]
	##tile函数用来复制一份inX数组的副本
	##dataSet是数据集,我们要求输入数据与数据集的欧氏距离
	diffMat=tile(inX,(dataSetSize,1))-dataSet
	sqDiffMat=diffMat**2
	##axis=1表示用列相加
	sqDistance=sqDiffMat.sum(axis=1)
	distance=sqDistance**0.5

	##对数据按从小到大的次序排序,选择距离最小的k个点
	sortedDistIndicies=distances.argsort()
	classCount={}
	
	for i in range(k):
		voteIlabel=labels[sortedDistIndicies[i]]
		##get的赋值语句,建立新的字典键对,统计每个label出现的频率
		##dict.get(key, default=None)
		##如果之前classCount[voteIlabel]有值就返回这个值
		##否则新建一个字典键对,默认返回0
		classCount[voteIlabel]=sorted(classCount.get(voteIlabel,0)+1
	##对ClassCount的内容进行排序,就可以找到距离最近的k个目标中
	##出现频率最高的标签啦
	sortedClassCount=sorted(classCount.iteritems(),key=operator.itemgetter(1),reverse=True)
	return sortedClassCount[0][0]
	

测试部分
已有数据的一部分数据作为训练样本,另一部分作为测试样本。
这里我们用错误率作为测试的依据。

对数据的预处理是很重要的
例子:手写数字识别系统

def handwritingClassTest():
	hwLabels=[]
	##listdir用于返回指定绝对地址中目录的条目
	##存储进trainingFileList这个列表中
	trainingFileList=listdir('trainingDigits')
	m=len(trainingFileList)
	##创建一个m行1024列的训练矩阵,每行存储一个数字的图像
	trainingMat=zeros({m,1024})
	for i in range(m):
		##区分当前行是哪个数字的图像
		fileNameStr=trainingFileList[i]
		##split函数以符号为基准切分字符串,返回切分结果列表
		fileStr=fileNameStr.split('.')[0]
		classNumStr=int(fileStr.split('_')[0])
		hwLabels.append(classNumStr)

		##img2vector:首先创建1×1024的NumPy数组,然后打开给定的文件,
		##循环读出文件的前32行,并将每行的头32个字符值存储在NumPy数组
		trainingMat[i,:]=img2vector('trainingDigits/%s'%fileNameStr')

	##下面用同样的方式处理测试集
	testFileList=listdir('testDigits')
	errorCount=0.0
	mTest=len(testFileList)
	for i in range(mTest)
		fileNameStr.testFileList[i]
		fileStr=fileNameStr.split('.')[0]
		classNumStr=int(fileStr.split('_')[0])
		vectorUnderTest=img2vector('testDigits/%s' % fileNameStr)
		##用上面的classify0去跑测试集和训练集
		classifierResult=classify0(vectorUnderTest,\trainingMat,hwLabels,3)
		##打印每一次测试集跑出的结果
		print" the classifier came back with: %d,the real answer is:%d"\ %(classifierResult,classNumStr)
		##统计计算错误率
		if(classifierResult != classNumStr):
			errorCount+=1.0
	print"\n the total number of errors is :%d" %errorCount
	print"\n the total error rate is %f"% (errorCount/float(mTest))
		

二、决策树

步骤:

  1. 检测是否属于一个分类
  2. 用信息论寻找划分数据集的最好特征
  3. 划分数据集
  4. 对每个划分子集继续1—3的操作

不同的划分数据集的算法是重点
划分数据集之前之后信息发生的变化称为信息增益。获得信息增益最高的特征就是最好的选择。
信息期望值计算公式:
− ∑ i = 1 n p ( x i ) l o g 2 p ( x i ) -\sum_{i=1}^np(x_i)log_2p(x_i) i=1np(xi)log2p(xi)
计算概率后计算信息熵的代码:

from math import log
def calcShannonEnt(dataSet)
	numEntries=len(dataSet)
	labelCounts={}
	##做一个键是类别,值是出现次数的字典
	for featVec in dataSet:
		currentLabel=featVec[-1]
		if currentLabel not in labelCounts.keys():
			labelCounts[currentLabel]=0
		labelCounts[currentLabel]+=1
	shannonEnt=0.0
	for key in labelCounts:
		prob=float(labelCounts[key])/numEntries
		shannonEnt-=prob*log(prob,2)
	return shannonEnt

ID3
按照给定特征划分数据集

##list.append(object) 向列表中添加一个对象object
##list.extend(sequence) 把一个序列seq的内容添加到列表中
def splitDataSet(dataSet,axis,value):
	retDataSet=[]
	for featVec in dataSet:
		if featVec[axis]==value
			##把已经用来分类的这个属性去掉
			reducedFeatVec=featVec[:axis]
			reducedFeatVec.extend(featVec[axis+1:])
			##添加到分类结果列表
			retDataSet.append(reducedFeatVec)
	return retDataSet

选择最好的数据集划分方式

def chooseBeatFeatureToSplit(dataSet):
	numFeatures=len(dataSet[0])-1
	baseEntropy=calcShannonEnt(dataSet)
	bestInfoGain=0.0
	bestFeature=-1
	
	for i in range(numFeatures):
		##将列表中第i个特征值有可能存在的值放入新的列表
		featList=[example[i] for examples in dataSet]
		##featlist去重
		uniqueVals=set(featList)
		newEntropy=0.0
		##对每一个唯一属性值划分数据集
		for value in uniqueVals:
			##按照给定特征划分数据集
			subDataSet=splitDataSet(data,i,value)
			##计算数据集的新熵值
			prob=len(subDataSet)/float(len(dataSet))
			newEntropy+=prob*calcShannonEnt(SubDataSet)
		##计算这样划分得到的信息增益,返回最好特征划分的索引值
		infoGain=baseEntropy-newEntropy
		if(infoGain>bestInfoGain):
			bestInfoGain=infoGain
			bestFeature=-1
		return bestFeature

在一棵决策树上,我们需要

  1. 得到数据
  2. 划分数据
  3. 在划分的子集上再次划分数据
  4. 如果程序遍历完所有划分数据集的属性(之后采用多数表决方法确定该叶子节点的分类)
    或者每个分支下的每个实例都有相同的分类
    则停止递归
def createTree(dataSet,labels):
	classList=[example[-1] for example in dataSet]
	##类别完全相同则停止划分
	if classList.count(classList[0])==len(classList)
		return classList[0]
	##遍历完所有特征,返回出现次数最多的类别
	if len(dataSet[0]==-1)
		return majorityCnt(classList)
	##选出最好的特征用于分割
	bestFeat=chooseBestFeatureToSplit(dataSet)
	bestFeatLabel=labels[bestFeat]
	myTree={bestFeatLabel:{}}
	##在集合里删除这个特征
	del(labels[bestFeat])
	##得到列表包含的所有属性值
	featValues=[example[bestFeat] for example in dataSet]
	uniqueVals=set(featValues)
	##对属性值中的每个属性创建子树
	for value in uniqueVals:
		subLabels=labels[:]
		myTree[bestFeatLabel][value]=createTree(splitDataSet\
		(dataSet,bestFeat,value),subLabels)
	return myTree
	

最终得到的结果:
一个子树:一个字典
一个叶子结点:一个类标签

绘图
获取叶节点的数目

def getNumLeafs(myTree):
	numLeafs=0
	firstStr=myTree.keys()[0]
	##找出树种第一个关键字
	secondDict=myTree[firstStr]
	for key in secondDict.keys()
		##type()判定测试节点的数据是否为字典
		##name属性表示类的名称
		if type(secondDict[key]).__name__=='dict':
			##递归调用getNumLeafs
			numLeafs+=getNumLeafs(secondDict[key])
		else
			numLeafs+=1
	return numLeafs

获取树的层数

def getTreeDepth(myTree):
	maxDepth=0
	firstStr=myTree.keys()[0]
	secondDict=myTree[firstStr]
	for key in secondDict.keys():
		if type(secondDict[key]).__name__=='dict':
			thisDepth=1+getTreeDepth(secondDict[key])
		else:
			thisDepth=1
		if thisDepth>maxDepth:
			maxDepth=thisDeoth
	return maxDepth

绘制树节点

import matplotlib.pyplot as plt
##sawtooth是将文本框的类型定义为锯齿形
decisionNode=dict(boxstyle="sawtooth",fc="0.8")
leafNode=dict(boxstyle="round4",fc="0.8")
arrow_args=dict(arrowstyle="<-")

def plotNode(nodeTxt,centerPt,parentPt.nodeType):
	##定义一个绘图区域,绘制一个节点
	createPlot.ax1.annotate(nodeTxt,xy=parentPt,\
	xycoords='axes fraction',xytext=centerPt,textcoords='axes fraction',\
	va="center",bbox=nodeType,arrowprops=arrow_args)

def createPlot():
	fig=plt.figure(1,facecolor='white')
	fig.clf()
	##subplot(pos, **kwargs)
	##pos是子块的索引
	##frameon表示是否绘制矩形边框
	createPlot.ax1=plt.subplot(111,frameon=False)
	##绘制带有标签的节点
	plotNode('决策节点',(0.5,0.1),(0.1,0.5),decisionNode)
	plotNode('叶节点',(0.8,0.1),(0.3,0.8),leafNode)
	

绘制整棵决策树

##在父子节点间填写文本信息
def plotMidText(cntrPt,parentPt,txtString):
	##找到一个中间位置
	xMid=(parentPt[0]-cntrPt[0])/2.0+cntrPt[0]
	yMid=(parentPt[1]-cntrPt[1])/2.0+cntrPt[1]
	createPlot,ax1,text(xMid,yMid,txtString)

def plotTree(myTree,parentPt,nodeTxt):
	##用之前的函数找出叶子节点的数量和深度
	##以此计算树的宽和高
	numLeafs=getNumLeafs(myTree)
	depth=getTreeDepth(myTree)
	firstStr=myTree.keys()[0]
	##用plotTree.xOff和plotTree.yOff将绘制的节点放在
	##相对合适的位置
	cntrPt=(PlotTree.xOff+(1.0+float(numLeafs))/2.0/plotTree.totalW,\
	plotTree.yOff)
	##标记该节点属性值,由此分支向下的节点必包含这一属性
	plotMidText(cntrPt,parentPt,nodeTxt)
	##绘制该节点
	plotNode(firstStr,cntrPt,parentPt,decisionNode)
	secondDict=myTree[firstStr]
	##因为树的层数增加了,按比例减小yOff值
	plotTree.yOff=plotTree.yOff-1.0/plotTree.totalD
	##绘制子节点,如果子节点是字典,就递归绘制
	##如果子节点是叶子,就直接绘制一个节点即可
	for key in secondDict.keys():
		if type(secondDict[key]).__name__=='dict':
			plotTree(secondDict[key],cntrPt,str(key))
		else:
			plotTree.xOff=plotTree.xOff+1.0/plotTree.totalW
			plotNode(secondDict[key],(plotTree.xOff,plotTree.yOff),cntrPt,leafNode)
			plotMidText((plotTree.xOff,plotTree.yOff),cntrPt,str(key))
	plotTree.yOff=plotTree.yOff+1.0/plotTree.totalD
	
	

createPlot应用于决策树的2.0版本
用整棵树的宽和高确定画树的位置

def createPlot(inTree)
	fig=plt.figures(1,facecolor='white')
	fig.clf()
	axprops=dict(xticks=[],yticks=[])
	createPlot.ax1=plt.subplot(111,frameon=False,**axprops)
	##计算总宽度,总高度,xOff,yOff
	plotTree.totalW=float(getNumLeafs(inTree))
	plotTree.totalD=float(getTreeDepth(inTree))
	plotTree.xOff=-0.5/plotTree.totalW
	plotTree.yOff=1.0
	##设置好参数后调用plotTree
	plotTree(inTree,(0.5,1.0),'')
	plt.show()

使用构建的决策树执行分类
递归比较测试数据和决策树上的数值,递归执行该过程直到进入叶子节点

def classify(inputTree,featLabels,testVec):
	firstStr=inputTree.keys()[0]
	secondDict=inputTree[firstStr]
	##将标签字符串转化为索引
	featIndex=featLabels.index(firstStr)
	for key in secondDict.keys():
		if testVec[featIndex]==key:
			if type(secondDict[key]).__name__=='dict'
				##继续递归分类
				classLbel=classify(secondDict[key],featLabels,testVec)
			else:
				##找到合适的分类啦
				classLabel=secondDict[key]
	return classLabel

用pickle序列化决策树,存储在磁盘中,下次用就不用再构造啦

##存储
def storeTree(inputTree,filename):
	import pickle
	fw=open(filename,'w')
	pickle.dump(inputTree,fw)
	fw.close()
##调用
def grabTree(filename)
	import pickle
	fr=open(filename)
	return pickle.load(fr)
	

三、朴素贝叶斯

数学基础:条件概率,贝叶斯公式
两个假设:

  1. 所有特征之间相互独立
  2. 每个特征同等重要

p ( c i ∣ w ) = p ( w 0 ∣ c i ) . . . p ( w n ∣ c i ) p ( c i ) p ( w ) p(c_{i}|w)=\frac{p(w_{0}|c_{i})...p(w_{n}|c_{i})p(c_{i})}{p(w)} p(ciw)=p(w)p(w0ci)...p(wnci)p(ci)

准备数据:从文本间构建词向量

def loadDataSet():
	postingList=[['my','dog','has','flea','problems','help','please'],\
	['maybe','not','take','him','to','dog','park','stupid'],\
	['my','dalmation','is','so','cute','I','love','him'],\
	['stop','posting','stupid','worthless','garbage'],\
	['mr','licks','ate','my','steak','how','to','stop','him'],\
	['quit','buying','worthless','dog','food','stupid']]
	classVec=[0,1,0,1,0,1]
	return postingList,classVec

def createVocabList(dataSet):
	##创建一个空集合
	vocabSet=set([])
	for document in dataSet:
		##求没有重复元素的并集
		vocabSet=vocabSet|set(document)
	return list(vocabSet)
	
##这个函数用来检测vocabList里是否有一段话里面的单词
##返回一个01词向量
def setOfWords2Vec(vocabList,inputSet):
	##创建元素全为0的向量
	returnVec=[0]*len(vocabList)
	for word in inputSet:
		if word in vocabList:
			returnVec[vocabList.index(word)]=1
		else:
			print"The word:%s is not in my volcabulary" % word
	return returnVec

	

朴素贝叶斯训练器
返回的概率将直接用于分类中

def trainNB0(trainMatrix,trainCategory):
	numTrainDocs=len(trainMatrix)
	numWords=len(trainMatrix[0])
	##求p(c)
	pAbusive=sum(trainCategory)/float(numTrainDocs)
	##初始化p(w0|c)和p(w1|c)
	p0Num=zeros(numWords)
	p1Num=zeros(numWords)
	p0Denom=0.0
	p1Denom=0.0
	##统计所有训练集中的词汇,一旦在文档中出现
	##该词对应的0,则pNum0++,该词对应的1,则pNum1++
	for i in range(numTrainDocs):
		if trainCategory[i]==1:
			p1Num+=trainMatrix[i]
			p1Denom+=sum(trainMatrix[i])
	p1Vect=p1Num/p1Denom
	p0Vect=p0Num/p0Denom
	return p1Vect,p0Vect,pAbusive

朴素贝叶斯分类函数

def classifyNB(vec2Classify,p0Vec):
	listOPosts,listClasses=loadDataSet()
	myVocabList=createVocabList(listOPosts)
	##numpy中对应相乘
	##词汇表的所有词的概率相加,并映射到log函数
	p1=sum(vec2Classify*p1Vec)+log(pClass1)
	p0=sum(vec2Classify*p0Vec)+log(pClass0)
	##分类为骂人句子和非骂人句子
	if p1>p0:
		return 1
	else
		return 0

测试一下

def testingNB()
	listOPosts,listClasses=loadDataSet()
	myVocabList=createVocabList(listOPosts)
	trainMat=[]
	for postinDoc in listOPosts:
		trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
	p0V,p1V,pAb=trainNB0(array(trainMat),array(listClasses))
	testEntry=['love','my','dalmation']
	##利用setOfWords2Vec进行词表到向量的转换
	thisDoc=array(setOfWords2Vec(myVocabList,testEntry))
	##进行classify并输出结果
	print testEntry,'classified as:',classifyNB(thisDoc,p0V,p1V,pAb)

将每个词出现与否作为一个特征,可以被描述为词集模型
对应之前的函数setOfWords2Vec
如果一个词在文档中出现不止一次,意味着会传达更多的信息,不能单纯用0和1衡量。于是引入了词袋模型

def bagOfWords2VecMN(vocabList,inputSet):
	returnVec=[0]*len(vocabList)
	for word in inputSet:
		if word in vocabList:
			returnVec[vocabList.index(word)]+=1
	return returnVec

使用朴素贝叶斯进行交叉验证

def textParse(bigString):
	import re
	##把字符串分割成词
	listOfTokens=re.split(r'\W*,bigString)
	##返回纯小写的词条,去掉少于两个字符的字符串
	return [tok.lower() for tok in listOfTokens if len(tok)>2]

def spamTest():
	docList=[]
	classList=[]
	fullText=[]
	for i in range(1,26):
		
		##打开要切分的文本		
		wordList=textParse(open('email/spam/%d.txt'%i).read())
		docList.append(wordList)
		fullText.extend(wordList)
		##这些词被分类为abusive
		classList.append(1)
		wordList=textParse(open('email/ham/%d.txt'%i).read())
		docList.append(wordList)
		fullList.extend(wordList)
		classList.append(0)
	##创建词条模型
	vocabList=createVocabList(docList)
	trainingSet=range(50)
	testSet=[]
	
	##接下来随机构造训练集,留存交叉验证
	for i in range(10):
	##trainingSet是0到49的整数列表
		randIndex=int(random.uniform(0,len(trainingSet)))
		testSet.append(trainingSet[randIndex])
		del(trainingSet[randIndex])
	trainMat=[]
	trainClasses=[]
	for docIndex in trainingSet:
		##对每封邮件构建词向量
		trainMat.append(setOfWords2Vec(vocabList,docList[docIndex]))
		trainClasses.append(classList[docIndex])
	##计算概率
	p0V,p1V,pSpam=trainNB0(array(trainMat),array(trainClasses))
	errorCount=0

	##遍历测试集,对每封邮件进行验证
	for docIndex in testSet:
		wordVector=setOfWords2Vec(vocabList,docList[docIndex])
		if classifyNB(array(wordVector),p0V,p1Vm,pSpam)!=classList	:
			errorCount+=1
	print'the error rate is',float(errorCount)/len(testSet)
		
	
		

使用RSS源

##计算最经常出现的30个单词
def calcMostFreq(vocabList,fullText)
	import operator
	freqDict=[]
	for token in vocabList:
		freqDict[token]=fullText.count(token)
	sortedFreq=sorted(freqDict.iteritems(),key=operator.itemgetter(1),reverse=True)
	return sortedFreq[:30]
##运用RSS源的spamtext
def localWords(feed1,feed0):
	import feedparser
	docList=[]
	classList=[]
	fullText=[]
	minLen=min(len(feed1['entries'][i]['summary']),len(feed0['entries'][i]['summary']))
	
	for i in range(minLen):
		wordList=textParse(feed0['entries'][i]['summary'])
		docList.append(wordList)
		fullText.extend(wordList)
		classList.append(0)
		
		wordList=textParse(feed1['entries'][i]['summary'])
		docList.append(wordList)
		fullText.extend(wordList)
		classList.append(1)
	##创建vocabList
	vocabList=createVocabList(docList)
	top30Words=calcMostFreq(vocabList,fullText)
	##去掉频率最高的30个单词
	##(出于实际应用考虑,因为广告词中重复的词是没用的)
	for pairW in top30Words:
		if pairW[0] in vocabList:
			vocabList.remove(pairW[0])
			
	trainingSet=range(2*minLen);

	##随机选出训练集
	for i in range(20):
	##uniform(x,y):随机生成一个在x,y之间的实数
		randIndex=int(random.uniform(0,len(trainingSet)))
		testSet.append(trainingSet[randIndex])
		del(trainingSet[randIndex])
	trainMat=[]
	trainClasses=[]
	for docIndex in trainingSet:
		trainMat.append(bagOfWords2VecMN(vocabList,docList[docIndex]))
		trainClasses.append(classList[docIndex])
	##利用trainNB0求classify所需的概率	
	p0V,p1V,pSpam=trainNB0(array(trainMat),array(trainClasses))
	errorCount=0
	for docIndex in testSet:
		wordVector=bagOfWords2VecMN(vocabList,docList[docIndex])
		if classifyNB(array(wordVector),p0V,p1V,pSpam)!=classList[docIndex]:
			errorCount+=1
	print'the error rate is:',float(errorCount)/len(testSet)
	return vocabList,p0V,p1V

四、Logistic回归

回归:寻找最佳的拟合系数
单位阶跃函数不能满足连续性。但如果我们把每个特征乘上一个回归系数,然后把所有结果值相加,总和代入sigmoid函数中。
大于0.5分为1类,小于0.5分为0类
sigmoid函数式和图像

梯度上升
z = w t x z=w^{t}x z=wtx

一般用梯度上升法求局部最大值,梯度下降法求局部最小值
在每一点处朝着梯度方向迭代(梯度方向就是偏导数方向)
w : = w + α ∇ w + f ( w ) w:=w+\alpha\nabla_{w}+f(w) w:=w+αw+f(w)
α \alpha α是向目标移动的步长
直到某个停止条件为止

def loadDataSet():
	dataMat=[]
	labelMat=[]
	fr=open('testSet.txt')
	for line in fr.readlines():
	##数据中,每行前两个是X1和X2,第三个是数据标签
		lineArr=line.strip().split()
		dataMat.append([1.0,float(lineArr[0]),float(lineArr[1])])
		labelMat.append(int(lineArr[2]))
	return dataMat,labelMat

##计算sigmoid函数
def sigmoid(inX):
	return 1.0/(1+exp(-inX))

def gradAscent(dataMatIn,classLabels):
	##转化为numpy矩阵数据类型
	dataMatrix=mat(dataMatIn)
	##转置为列向量
	labelMat=mat(classLabels).transpose()
	m,n=shape(dataMatrix)
	alpha=0.001
	maxCycles=500
	
	weights=ones(n,1)
	for k in range(maxCycles):
		h=sigmoid(dataMatrix*weights)
		##用sigmoid函数很好计算error
		error=(labelMat-h)
		##梯度上升迭代
		weights=weights+alpha*dataMatrix.transpose()*error
	return weights
	
	

matplotlib画出最佳拟合直线函数w2y=w1x+b

def plotBestFit(weights):
	import matplotlib.pyplot as plt
	dataMat,labelMat=loadDataSet()
	dataArr=array(dataMat)
	n=shape(dataArr)[0]
	xcord1=[]
	ycord1=[]
	xcord2=[]
	ycord2=[]
	for i in range(n):
	if int(labelMat[i])==1:
		xcord1.append(dataArr[i,1])
		ycord1.append(dataArr[i,2])
	fig=plt,figure()
	ax=fig.add_subplot(111)
	ax.scatter(xcord1,ycord1,s=30,c='red',marker='s')
	ax.scatter(xcord2,ycord2,s=30,c='green')
	x=arrange(-3.0,3.0,0.1)
	##反解出x2
	##0是两个类别的分界处
	##0=w0x0+w1x1+w2x2
	##这里w0=1(因为是一条直线)
	y=(-weights[0]-weights[1]*x)/weights[2]
	ax.plot(x,y)
	plt.xlabel('X1')
	plt.ylabel('X2')
	plt.show()

随机梯度上升算法
随机取数据集中的数据进行梯度上升算法,降低时间复杂度,是一个在线算法。

def stocGradAscent1(dataMatrix,classLabels):
	m,n=shape(dataMatrix)
	weights=ones(n)
	##j是迭代次数,i是样本点的下标
	for j in range(numIter):
		dataIndex=range(m)
		for i in range(m):
			##alpha每次迭代都会减小
			##有利于最后迭代次数降低,稳定在一个值,收敛的更快
			alpha=4/(1.0+j+i)+0.01
			##随机选择一个点更新参数
			randIndex=int(random.uniform(0,len(dataIndex))
			h=sigmoid(sum(dataMatrix[randIndex]*weights))
			error=classLabels[randIndex]-h
			weights=weights+alpha*error*dataMatrix[randIndex]
			##将用过的index删掉
			del(dataIndex[randIndex])
	return weights

示例:预测病马的死亡率
处理数据中缺失值的方法:

  • 使用可用特征的均值填补缺失值
  • 使用特殊值填补缺失值,如-1
  • 忽略有缺失值的样本
  • 使用相似样本的均值填补缺失值
  • 使用另外的机器学习算法预测缺失值

这里运用logistic回归,因此补0就好(sigmoid(0)=0.5),不会影响到分类函数
另外,分类的时候把测试集上的每个特征和回归系数相乘再相加,输入sigmoid函数,即可得到分类结果。

##分类函数
def classifyVector(inX,weights):
	prob=sigmoid(sum(inX*weights))
	if prob>0.5:
		return 1.0
	else:
		return 0.0

def colicTest():
	frTrain=open('horseColicTraining.txt')
	frTest=open('horseColicTest.txt')
	trainingSet=[]
	trainingLabels=[]
	##对训练集的处理
	for line in frTrain.readlines():
		currLine=line.strip().split('\t')
		lineArr=[]
		for i in range(21):
			lineArr.append(float(currLine[i]))
		trainingSet.append(lineArr)
		trainingLabels.append(float(currLine[21]))
	trainWeights=stocGradienAscent1(array(trainingSet),trainingLabels,500)

	##对测试集的处理
	errorCount=0
	numTestvec=0.0
	for line in frTest.readlines():
		numTestVec+=1.0
		currLine=line.strip().split('\t')
		lineArr=[]
		for i in range(21):
			lineArr.append(float(currLine[i]))
		if int(classifyVector(array(lineArr),trainWeights))!=int(currLine[21]):
			errorCount+=1
	errorRate=(float(errorCount)/numTestVec)
	print"the error rate of this test is %f" % errorRate
	return errorRate
	
def multiTest():
	numTests=10
	errorSum=0.0
	for k in range(numTests):
		errorSum+=colicTest()
	print"after %d iterations the average error rate is:%f" %(numTests,errorSum/float(numTests))

五、支持向量机

基本推导:
是个泛化错误率低的二分类问题
支持向量就是离分隔超平面最近的点。要最大化支持向量到分隔面的距离。
a r g m a x { m i n ( l a b e l ( w T x + b ) ) 1 ∣ ∣ w ∣ ∣ } argmax \{min(label(w^{T}x+b))\frac{1}{||w||}\} argmax{min(label(wTx+b))w1}
其中label是分类标签(-1、1)
约束条件:(共有i个)
l a b e l ∗ ( w T x + b ) ≥ 1.0 label*(w^{T}x+b)\geq1.0 label(wTx+b)1.0
下面引入拉格朗日乘数法求条件极值,解决一个凸优化问题
对w和b求偏导代入,得到:(这个不用讲了吧,高等数学)
L = ∑ i = 1 m α − 1 2 ∑ i , j = 1 m l a b e l i l a b e l j α i α j < x i x j > L=\sum_{i=1}^{m}\alpha-\frac{1}{2}\sum_{i,j=1}^{m}label^{i}label^{j}\alpha^{i}\alpha{j} L=i=1mα21i,j=1mlabelilabeljαiαj<xixj>
但是我们的约束条件是不等号,于是引入对偶问题,这里是一个强对偶关系
一般来说,minmax大于等于maxmin,只有在满足kkt条件的时候才会有两者相等。
条件为:

这篇博客讲的比较清晰:
我是对偶问题推导

m i n m a x [ ∑ i = 1 m α − 1 2 ∑ i , j = 1 m l a b e l i l a b e l j α i α j < x i x j > ] minmax[\sum_{i=1}^{m}\alpha-\frac{1}{2}\sum_{i,j=1}^{m}label^{i}label^{j}\alpha^{i}\alpha{j}] minmax[i=1mα21i,j=1mlabelilabeljαiαj<xixj>]

s t . α ≥ 0 , ∑ i = 1 m α i l a b e l i st.\alpha\geq0,\sum^{m}_{i=1}\alpha_{i}label^{i} st.α0,i=1mαilabeli
引入松弛变量c,允许有些分类在错误的一侧
一旦求出了所有的 α \alpha α,分隔超平面就很容易能表达出来,SVM的求解目的就在于求出满足条件的 α \alpha α
书上没有详细证明,可以参考这个教程:
我是SVM算法推导

SMO算法
上面有个遗留问题,就是在这样的带约束条件的凸优化问题中,怎么求解 α \alpha α
这里的 α \alpha α是一个向量,里面有m个 α 1 , . . . , α m \alpha_{1},...,\alpha_{m} α1,...,αm
找到 α \alpha α以后,w和b都能确定,整个超平面也就能够确定了。

在SMO算法中每次循环选择两个 α \alpha α进行优化

SMO算法中几个杂七杂八的函数:

def loadDataSet(fileName):
	dataMat=[]
	labelMat=[]
	fr=open(fileName)
	for line in fr.readlines:
		lineArr=line.strip().split('\t')
		dataMat.append(float(lineArr[0]),float(lineArr[1]))
		labelMat.append(float(lineArr[2]))
	return dataMat,labelMat

##在区间范围内随机选择一个整数
def selectJrand(i,m):
	j=i
	while(j==i)
		j=int(random.uniform(0,m))
	return j

##对太大太小的数值进行调整

```python
def clipAlpha(aj,H,L):
	if aj>H:
		aj=H
	if L>aj:
		aj=L
	return aj

简化版SMO算法:
代码中误差和alpha、eta计算公式的推导

def smoSimple(dataMatIn,classLabels,C,toler,maxIter):
	dataMatrix=mat(dataMatIn)
	labelMat=mat(classLabels).transpose()
	b=0
	m,n=shape(dataMatrix)
	alphas=mat(zeros(m,1))
	iter=0
	
	while(iter<maxIter):
		##记录alpha是否被优化过
		alphaParisChanged=0
		for i in range(m)##对于数据集中的每个数据向量
			##fXi是分类的类别,Ei是误差
			fXi=float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T))+b
			##.T是转置操作
			Ei=fXi-float(labelMat[i])
			##如果误差很大,则对alpha进行优化
			if((labelMat[i]*Ei<-toler)and(alphas[i]<C))or((labelMat[i]*Ei>toler)and(alphas[i]>0))
				j=selectJrand(i,m)
				fXj=float(multiply(alphas,labelMat).T*(dataMatrix*dataMatrix[j,:].T))+b
				Ej=fXj-float(labelMat[j])
				##新旧值的比较,将alpha[j]调整到0与C之间
				alphaIold=alphas[i].copy()
				alphaJold=alphas[j].copy()
				if(labelMat[i]!=labelMat[j]):
					L=max(0,alphas[j]-alphas[i])
					H=min(C,C+alphas[j]-alphas[i])
				else:
					L=max(0,alphas[j]+alphas[i]-C)
					H=min(C,alphas[j]+alphas[i])
				if L==H:
					print"L==H"
					continue
				##eta是alpha[j]的最佳修改量
				eta=2.0*dataMatrix[i:]*dataMatrix[j:].T-dataMatrix[i,:]*dataMatrix[i,:].T-dataMatrix[j,:]*dataMatrix[j,:].T
				if eta>=0:
					print"eta>=0"
					continue
				##如果eta==0计算新的alpha[j]
				alphas[j]-=labelMat[j]*(Ei-Ej)/eta
				##限定一下alphas[j]的范围
				alphas[j]=clipAlpha(alphas[j],H,L)
				if(abs(alpha[j]-alphaJold)<0.00001):
					print"j is not moving enough"
					continue
				##更新a[i]的值
				##a[j]减小a[i]就增大,数值相同方向相反
				alpha[i]+=labelMat[j]*labelMat[i]*(alphas[j]-alphaJold)*dataMatrix[j:]
				##为a[i]设置常数项
				b1=b-Ei-labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i:].T-labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i:]*dataMatrix[j:].T
				##为a[j]设置常数项
				b2=b-Ej-labelMat[i]*(alphas[i]-alphaIold)*dataMatrix[i:].T-labelMat[j]*(alphas[j]-alphaJold)*dataMatrix[i:]*dataMatrix[j:].T
				##统一常数项
				if(0<alphas[i])and(C>alphas[i]):
					b=b1
				elif(0<alphasp[j])and(C>alphas[j]):
					b=b2
				else:
					b=(b1+b2)/2.0
					alphaPairsChanged+=1
					print"iter:%d i:%d pairschanged:%d"%(iter,i,alphaPairsChanged)
		##检查alpha是否更新,更新了则重置iter为0
		##因为要在所有数据上遍历maxiter次,发现没有可更新的alpha后
		##整个程序才会完美结束
		if(alphaPairsChanged==0):
			iter+=1
		else:
			iter=0
		print"iteration number:%d"%iter
	return b,alphas
				
				
				 

完整Platt SMO算法加速优化
优化点:
建立非边界 α \alpha α值的缓存用于保存误差值,并且从中选择使步长(误差之差)最大的 α \alpha α

完整版中杂七杂八的函数

class optStruct:
##误差缓存
	def __init__(self,dataMatIn,classLabels,C,toler):
		self.X=dataMatIn
		self.labelMat=classLabels
		self.C=C
		self.tol=toler
		self.m=shape(dataMatIn)[0]
		self.alpha=mat(zeros(self,m,1))
		self.b=0
		self.eCache=mat(zeros(self.m,2))


def calcEk(oS,k):
##将算误差值的函数单独拎出来
	fXk=float(multiply(oS.alphas,oS.labelMat).T*(oS.X*oS.X[k:].T))+oS.b
	Ek=fXk-float(oS.labelMat[k])
	return Ek

def selectJ(i,oS,Ei):
	maxK=-1
	maxDeltaE=0 
	Ej=0
	oS.eCache[i]=[i,Ei]
	  
##选择具有最大步长的j
def selectJ(i,oS,Ei):
	maxK=-1
	maxDeltaE=0
	Ej=0
	oS.eCache[i]=[1,Ei]
	##非0  E值所对应的alpha值列表
	validEcacheList=nonzero(oS.eCache[:,0].A)[0]
	if(len(validEcacheList))>1:
		for k in validEcacheList:
			if k==i:
				continue
			Ek=calcEk(oS,k)
			deltaE=abs(Ei-Ek)
			##选取改变量最大的值
			if(deltaE>maxDeltaE):
				maxK=k
				maxDeltaE=deltaE
				Ej=Ek
		return maxK,Ej
	else:
		j=selectJrand(i,oS.m)
		Ej=calcEk(oS,j)
	return j,Ej

##计算误差值并存入缓存,对alpha进行优化的时候会用到
def updateEk(oS,k):
	Ek=calcEk(oS,k)
	oS.eCache[k]=[1,Ek]

完整版中内循环(用缓存存储误差,和之前的内循环算法没有差别)

def innerL(i,oS):
	Ei=calcEk(oS,i)
	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)):
	j,Ej=selectJ(i,oS,Ei)
	alphaIold=oS.alphas[i].copy()
	alphaJold=oS.alpha[j].copy()
	if(oS.labelMat[i]!=oS.labelMat[j]):
		L=max(0,oS.alphas[j]-oS.alphas[i])
		H=min(oS.C,oS.C+oS.alphas[j]-oS.alphas[i])
	else:
		L=max(0,oS.alphas[j]+oS.alphas[i]-oS.C)
		H=min(oS.C,oS.alphas[j]+oS.alphas[i])
	if L==H:
		print"L==H"
		return 0
	eta=2.0*oS.X[i,:]*oS.X[j,:].T-oS.X[i:]*oS.X[i:].T-oS.X[j,:]*oS.X[j,:].T
	if eta>=0:
		print"eta>=0"
		return 0
	oS.alphas[j]-=oS.labelMat[j]*(Ei-Ej)/eta
	oS.alphas[j]=clipAlpha(oS.alphas[j],H,L)
	##更新误差缓存
	updateEK(oS,j)
	if(abs(oS.alpha[j]-alphaJold)<0.00001):
		print"j not moving enough"
		return 0
	oS.alphas[i]+=oS.labelMat[j]*oS.labelMat[i]*(alphaJold-oS.alphas[j])
	##更新误差缓存
	updateEK(os,i)
	##下面和简单SMO都一样,只不过用了optStruct类,懒得打了
	b1=。。。。

完整版SMO外循环代码

def smoP(dataMatIn,classLabels,C,toler,maxIter,kTup=('lin',0)):
	oS=optStruct(mat(dataMatIn),mat(classLabels).transpose(),C,toler)
	iter=0
	entireSet=True
	alphaPairsChange=0
	##循环条件:没到最大迭代次数
	##在所有数据集上扫描/alpha的值已经被改变过
	while(iter<maxIter)and((alphaPairsChanged>0)or(entireSet)):
		alphaPairsChanged=0
		
		if entireSet:
		##遍历所有值
			for i in range(oS,m):
				##用内循环选alpha[j]
				alphaPairsChanged+=innerL(i,oS)
				print"fullSet,iter:%d i:%d,pairs changed %d" % (iter,i,alphaPairsChanged)
			iter+=1
		else:
			##寻找不在边界的alpha
			nonBoundIs=nonzero((os.alphas.A>0)*(os.alphas.A<C))[0]
			for i in nonBoundIs:
				alphaPairsChanged+=innerL(i,oS)
				print"non-bound,iter:%d i:%d, pairs changed %d" % (iter,i,alphaPairsChanged)
			iter+=1
		if entireSet:
			entireSet=False
		elif(alphaPairsChanged==0):
			entireSet=True
		print:"iteration number:%d" % iter
	return oS.b,oS.alphas	

计算w,就可以得到我们的分隔超平面

def calcWs(alphas,dataArr,classLabels):
	##将数组用mat转化为矩阵并转置
	X=mat(dataArr)
	labelMat=mat(classLabels).tanspose()
	m,n=shape(X)
	w=zeros((n,1))
	for i in range(m):
	##非零alpha对应的都是支持向量
	##所以最后乘进去的只有支持向量
		w+=multiply(alphas[i]*labelMat[i],X[i,:].T)
	return w

核函数
核函数完成从一个特征空间到另一个特征空间的映射
在SVM中我们可以把一个内积运算替换成核函数
SVM中常用径向基核函数
k ( x , y ) = e x p ( − ∣ ∣ x − y ∣ ∣ 2 2 σ 2 ) k(x,y)=exp(\frac{-||x-y||^{2}}{2\sigma^{2}}) k(x,y)=exp(2σ2xy2)
σ \sigma σ是个函数值跌落到0的速度参数
核转换函数:

def kernelTrans(X,A,kTup):
	##元祖kTup给出核函数的信息
	##kTup[0]是描述核函数类型的一个字符串
	##kTup[1]是delta
	m,n=shape(X)
	K=mat(zeros(m,1))
	if KTup[0]=='lin':
		K=X*A.T
	elif kTup[0]=='rbf':
		for j in range(m):
			deltaRow=X[j,:]-A
			K[j]=deltaRow*deltaRow.T
		K=exp(K/(-1*kTup[1]**2))
	else:
		raise NameError('Houston We Have a Problem That Kernel is not recognized')
	return K

对原本的optStruct类进行修改

class optStruct:
##误差缓存
	def __init__(self,dataMatIn,classLabels,C,toler,kTup):
		self.X=dataMatIn
		self.labelMat=classLabels
		self.C=C
		self.tol=toler
		self.m=shape(dataMatIn)[0]
		self.alpha=mat(zeros(self,m,1))
		self.b=0
		self.eCache=mat(zeros(self.m,2))
		self.K=mat(zeros(self.m,self.m))
		for i in range(self.m):
			self.K[:,i]=kernelTrans(self.X,self.X[i,:],kTup)
		

在训练集中,用K值改变b的结果
在测试集中,确定sigma的大小,并用该核函数构建一个分类器

def testRbf(k1=1.3):
	dataArr,labelArr=loadDataSet('testSetRBF.txt')
	b.alphas=smoP(dataArr,labelArr,200,0.0001,10000,('rbf',k1))
	datMat=mat(dataArr)
	labelMat=mat(labelArr).transpose()
	##找出alpha值非0的支持向量和他的类别标签
	svInd=nonzero(alphas.A>0)[0]
	sVs=datMat[svInd]
	labelSV=labelMar[svInd]
	print"there are %d Support Vectors"% shape(sVs)[0]
	m,n=shape(datMat)
	##对于训练数据集
	errorCount=0
	for i in range(m):
		##得到转换为核函数的数据
		kernelEval=kernelTrans(sVs,datMat[i,:],('rbf',k1))
		
		##核函数与alpha和类别标签求积
		predict=kernelEval.T*multiply(labelSV,alpha[svInd])+b
		if sign(predict)!=sign(labelArr[i]):
			errorCount+=1
	print"the training error rate id:%f"%(float(errorCount)/m)
	errorCount=0
	datMat=mat(dataArr)
	labelMat=mat(labelArr).transpose()
	m,n=shape(datMat)
	##对于测试集
	for i in range(m):
		kernelEval=kernelTrans(sVs,datMat[i,:],('rbf',k1))
		predict=kernelEval.T*multiply(labelSV,alphas[svInd])+b
		if sign(predict)!=sign(labelArr[i]):
			errorCount+=1
	print"the test error rate is:%f"%(float(errorCount)/m)

六、Adaboost

集成方法是对其他算法进行组合的一种方式
bagging:数据重新抽样构建分类器,如随机森林(多个决策树同时分类,看哪种结果多,详情见别人的随机森林博客)
boosting:boosting中的分类器权重并不相等,每个权重代表的是其对应分类器在上一轮迭代中的成功度
一开始,所有权重都初始化为相等的值
α \alpha α是根据每个弱分类器的错误率计算的
α = 1 2 l n ( 1 − ϵ ϵ ) \alpha=\frac{1}{2}ln(\frac{1-\epsilon}{\epsilon}) α=21ln(ϵ1ϵ)
算出 α \alpha α后,对权重向量D进行更新。

如果某个样本被正确分类
D ( i + 1 ) i = D i ( t ) e − α S u m ( D ) D^{(i+1)_{i}}=\frac{D^{(t)}_{i}e^{-\alpha}}{Sum(D)} D(i+1)i=Sum(D)Di(t)eα
如果未被正确分类就是 e α e^{\alpha} eα

不断迭代直到训练错误率到达指定值

单层决策树生成函数
构建一个弱分类器

def stumpClassify(dataMatrix,dimen,threshVal,threshIneq):
	retArray=ones((shape(dataMatrix[0],1)))
	##将所有不满足要求的设置为-1类
	if threshIneq=='lt':
		retArray[dataMatrix[:,dimen]<=threshVal]=-1.0
	else:
		retArray[dataMatrix[:,dimen]>threshVal]=-1.0
	return retArray

##找到数据集上最佳的单层决策树
def buildStump(dataArr,classLabels,D):
	dataMatrix=mat(dataArr)
	labelMat=mat(classLabels).T
	m,n=shape(dataMatrix)
	numSteps=10.0
	bestStump={}
	bestClasEst=mat(zeros((m,1)))
	minError=inf
	##遍历数据集的每一个特征
	for i in range(n):
		rangeMin=dataMatrix[:,i].min()
		rangeMax=dataMatrix[:,i].max()
		##用最大最小值计算步长
		stepSize=(rangeMax-rangeMin)/numSteps
		##对于步长中的每个值
		for j in range(-1,int(numSteps)+1):
			##考虑大于等于和小于等于两种情况
			for inequal in ['lt','gt']:	
				threshVal=(rangeMin+float(j)*stepSize)
				##计算分类预测结果
				predictedVals=stumpClassify(dataMatrix,i,threshVal,inequal)
				errArr=mat(ones((m,1)))
				##分类对了就为0,分类错了就为1
				errArr[predictedVals==labelMat]=0
				##计算加权错误率
				weightedError=D.T*errArr
				##找到加权错误率最小的
				if weightedError<minError:
					minError=weightedError
					bestClasEst=predictedVals.copy()
					bestStump['dim']=i
					bestStump['thresh']==threshVal
					bestStump['ineq']=inequal
		return bestStump,minError,bestClasEst
					

使用多个弱分类器构建Adaboost代码
任何分类器都可以作为基础分类器,本书讲到的任何一个算法都行

def adaBoostTrainDS(dataArr,classLabels,numIt=40):
	weakClassArr=[]
	m=shape(dataArr)[0]
	D=mat(ones((m,1))/m)
	aggClassEst=mat(zeros((m,1)))
	for i in range(numIt):
		bestStump,error,classEst=buildStump(dataArr,classLabels,D)
		print"D:",D.T
		##计算alpha值
		alpha=float(0.5*log((1.0-error)/max(error,1e-16)))
		##将alpha值加入bestStump字典中
		bestStump['alpha']=alpha
		##字典添加到弱分类器列表中
		weakClassArr.append(bestStump)
		print"ClassEst:",ClassEst.T
		##计算新权重D
		expon=multiply(-1*alpha*mat(classLabels).T,classEst)
		D=multiply(D,exp(expon))
		D=D/D.sum()
		##aggClassEst是每个数据点的类别估计的累积值
		##可以通过aggClassEst的符号来了解总类别
		aggClassEst+=alpha*classEst
		print "aggClassEst:",aggClassEst.T
		##计算累积错误率
		##sign()正数是1,负数是-1,0是0
		aggErrors=multiply(sign(aggClassEst)!=mat(classLabels).T,ones((m,1)))
		errorRate=aggErrors.sum()/m
		print"totalerror:",errorRate,"\n"
		##分类错误率为0,结束循环
		if errorRate==0.0:
			break
	return weakClassArr

Adaboost分类函数

def adaClassify(datToClass,classifierArr):
	dataMatrix=mat(datToClass)
	m=shape(dataMatrix)[0]
	aggClassEst=mat(zeros((m,1)))
	##遍历所有弱分类器
	for i in range(len(classifierArr)):
		##用stumpClassify对每个分类器估计一个值
		classEst=stumpClassify(dataMatrix,classifierArr[i]['dim'],classifierArr[i]['thresh'],classifierArr[i]['ineq'])
		##aggClassEst:每个数据点类别估计的累积值
		aggClassEst+=classifierArr[i]['alpha']*classEst
		print aggClassEst
	return sign(aggClassEst)

非均衡分类问题
有时候每个分类的代价不同,比如合法邮件案例中,宁愿误判不合法的邮件为合法邮件,也不要漏掉合法邮件造成损失。
代价敏感学习/对训练数据进行改造(欠抽样、过抽样)

正确率,召回率,ROC曲线
被西瓜书第二章折磨过的懂的都懂
《机器学习实战》读书笔记(第一部分:分类)_第1张图片
《机器学习实战》读书笔记(第一部分:分类)_第2张图片
ROC曲线下面的面积就是AUC,给出分类器的平均性能值,完美分类器的AUC是1.0,随机分类器的AUC是0.5

用代码画出ROC曲线
分类样例按照预测强度排序,依次作为正反例分割点,计算ROC曲线中对应的值

def plotROC(preStrengths,classLabels):
	import matplotlib.pyplot as plt
	cur=(1.0,1.0)
	ySum=0.0
	numPosClas=sum(array(classLabels)==1.0)
	##计算x轴和y轴上的步长
	yStep=1/float(numPosClas)
	xStep=1/float(len(classLabels)-numPosClas)
	##将分类器的预测强度排序
	sortedIndicies=predStrengths.argsort()
	fig=plt.figure()
	fig.clf()
	ax=plt.subplot(111)
	for index in sortedIndicies.tolist()[0]
		if classLabels[index]==1.0:
			delX=0
			delY=yStep
		else:
			delX=xStep
			delY=0
			##ySum计算AUC的值,对小矩形累加
			ySum+=cur[1]
		ax.plot([cur[0],cur[0]-delX],[cur[1],cur[1]-delY)
		cur=(cur[0]-delX,cur[1]-delY)
	ax.plot([0,1],[0,1],'b--')
	plt.xlabel('False Positive Rate')
	plt.ylabel('True Positive Rate')
	plt.title('ROC curve for AdaBoost')
	plt.show()
	##xStep是小矩形的宽
	print"the Area Under the Curve is:",ySum*xStep

你可能感兴趣的:(AI,机器学习,分类,人工智能)