第三章使用决策树进行分类,其不断将数据切分为小数据,直到目标变量完全相同,或者数据不能再分为止,决策树是一种贪心算法,要在给定的时间内做出最佳选择,但并不关心能否达到全局最优。这种树构建算法是ID3,每次选取当前最佳的特征来分割数据,并按照该特征的所有可能值来切分。一旦按某特征切分后,该特征在之后的算法中将不会再起作用,所以有观点认为这种切分方式过于迅速。
另外一种方法是二元切分法,即每次把数据集切成2份,如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入右子树。
第八章的回归是一种全局回归模型,它设定了一个模型,不管是线性还是非线性的模型,然后拟合数据得到参数,现实中会有些数据很复杂,肉眼几乎看不出符合那种模型,因此构建全局的模型就有点不合适。这节介绍的树回归就是为了解决这类问题,它通过构建决策节点把数据数据切分成区域,然后局部区域进行回归拟合。
CART算法使用二元切分来处理连续型变量。
优点:可以对复杂和非线性的数据建模
缺点:结果不易理解
一般流程:
收集数据:任意方法
准备数据:采用数值型的数据,标称型的数据应该映射成二值型数据
分析数据:绘出数据的二维可视化显示结果,以字典方式生成树
训练算法:大部分时间都花费在叶节点树模型的构建上
测试算法:使用测试数据上的R**2值来分析模型的结果
使用算法:使用训练出的树做预测,还可以做很多其他事情
import numpy as np
def loadDataSet(fileName):
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float, curLine))
dataMat.append(fltLine)
return dataMat
#将数据集分成两个
def binSplitDataSet(dataSet, feature, value):
# np.nonzero(a),返回数组a中非零元素的索引值数组
# np.nonzero(dataSet[:, feature] > value)[0]=1,
# 下面一行代码表示mat0=dataSet[1,:]即第一行所有列
mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
return mat0, mat1
def regLeaf(dataSet):#生成叶结点
return np.mean(dataSet[:,-1])# 目标变量的均值
def regErr(dataSet):
return np.var(dataSet[:,-1]) * np.shape(dataSet)[0]#目标变量的总方差
#找到数据集切分的最佳位置,生成相应的叶节点
def chooseBestSplit(dataSet, leafType = regLeaf, errType = regErr, ops = (1,4)):
"""
dataSet - 数据集合
leafType - 生成叶结点
regErr - 误差估计函数
ops - 用户定义的参数构成的元组
Returns:
bestIndex - 最佳切分特征
bestValue - 最佳特征值
"""
#tolS允许的误差下降值,tolN切分的最少样本数
tolS = ops[0]; tolN = ops[1]
#如果当前所有值相等,则退出。(根据set的特性)
if len(set(dataSet[:,-1].T.tolist()[0])) == 1: #tolist()[0]变为矩阵,set()创建集合
return None, leafType(dataSet)
#统计数据集合的行m和列n
m, n = np.shape(dataSet)
#默认最后一个特征为最佳切分特征,计算其误差估计
S = errType(dataSet)
#分别为最佳误差,最佳特征切分的索引值,最佳特征值
bestS = float('inf'); bestIndex = 0; bestValue = 0
#遍历所有特征列
for featIndex in range(n - 1):
#遍历所有特征值
for splitVal in set(dataSet[:,featIndex].T.A.tolist()[0]):
#根据特征和特征值切分数据集
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)
#如果数据少于tolN,则退出
if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN): continue
#计算误差估计
newS = errType(mat0) + errType(mat1)
#如果误差估计更小,则更新特征索引值和特征值
if newS < bestS:
bestIndex = featIndex
bestValue = splitVal
bestS = newS
#如果误差减少不大则退出
if (S - bestS) < tolS:
return None, leafType(dataSet)
#根据最佳的切分特征和特征值切分数据集合
mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue)
#如果切分出的数据集很小则退出
if (np.shape(mat0)[0] < tolN) or (np.shape(mat1)[0] < tolN):
return None, leafType(dataSet)
#返回最佳切分特征和特征值
return bestIndex, bestValue
def createTree(dataSet, leafType = regLeaf, errType = regErr, ops = (1, 4)):#构建回归树
#选择最佳切分特征和特征值
feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
#r如果没有特征,则返回特征值
if feat == None: return val
#回归树
retTree = {}
retTree['spInd'] = feat
retTree['spVal'] = val
#分成左数据集和右数据集
lSet, rSet = binSplitDataSet(dataSet, feat, val)
#创建左子树和右子树
retTree['left'] = createTree(lSet, leafType, errType, ops)
retTree['right'] = createTree(rSet, leafType, errType, ops)
return retTree
myDat = loadDataSet('ex0.txt')
myMat = np.mat(myDat)
print(createTree(myMat))
ex0数据集:
ex00数据集:
一棵树如果结点过多,表明该模型可能对数据进行了“过拟合”,如何判断是否过拟合,前面已经介绍了使用测试集上的某种交叉验证的方法来发现过拟合,决策树一样。
通过降低树的复杂度来避免过拟合的过程称为剪枝(pruning)。我们也已经提到,设置tolS和tolN就是一种预剪枝操作。另一种形式的剪枝需要使用测试集和训练集,称作后剪枝(postpruning)。本节将分析后剪枝的有效性,但首先来看一下预剪枝的不足之处。
如果将ops改为(0,1),结果将为每个样本都分配一个叶节点:
如果用另一组数据ex2:
这组数据与ex00十分相似,但是y轴是ex00的100倍,如果用ops(1,4)来构建,ex00的结果为
只有2个叶节点,而放到ex2上:
则会出现无数个叶节点,说明这个判断依据是不合适的。停止条件tolS对误差的数量级十分敏感,如果在选项中花费时间并对上述误差容忍度取平方值,也许能得到仅有2个子节点组成的树:
ops(10000,4):
但是,如果靠试来找到合适的值并不是一个好方法,事实上,我们常常不确定要寻找什么样的结果。这正是机器学习所关注的内容,计算机应该可以给出总体的概貌。
使用后剪枝方法需要将数据集分成测试集和训练集。首先指定参数,使得构建出的树足够大、足够复杂,便于剪枝。接下来从上而下找到叶结点,用测试集来判断这些叶结点合并是否能降低测试集误差。如果是的话就合并。
基于已有的树切分测试数据:
如果存下任一子集是一棵树,则在该子集递归剪枝过程
计算将当前两个叶节点合并后的误差
计算不合并的误差
如果合并会降低误差的话,就将叶节点合并
#判断是否是一棵树
def isTree(obj):
return (type(obj).__name__ =='dict') #返回布尔类型
def getMean(tree):
if (isTree(tree['right'])):
tree['right'] = getMean(tree['right'])
if (isTree(tree['left'])):
tree['left'] = getMean(tree['left'])
return (tree['left'] + tree['right'])/2.0
def prune(tree, testData):
"""
tree:待剪枝的树
testData:剪枝所需要的测试数据
"""
if np.shape(testData)[0] == 0: return getMean(tree) #确认数据集是否为空
if (isTree(tree['right']) or isTree(tree['left'])):
lSet, rSet = binSplitDataSet(testData,tree['spInd'], tree['spVal'])
if isTree(tree['left']):
tree['left'] = prune(tree['left'], lSet)
if isTree(tree['right']):
tree['right'] = prune(tree['right'],rSet)
if not isTree(tree['left']) and not isTree(tree['right']):
lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
errorNoMerge = sum(np.power(lSet[:, -1] - tree['left'], 2)) + sum(np.power(rSet[:,-1] - tree['right'],2))
treeMean = (tree['left'] + tree['right'])/2.0
errorMerge = sum(np.power(testData[:, -1] - treeMean, 2))
if errorMerge < errorNoMerge:
print('合并')
return treeMean
else:
return tree
else:
return tree
myDat = loadDataSet('ex2.txt')
myMat2 = np.mat(myDat)
mytree=createTree(myMat2)
myDatTest = loadDataSet('ex2test.txt')
myMat2Test = np.mat(myDatTest)
print(prune(mytree,myMat2Test))
结果:
可以看到,虽然剪掉了大部分枝,但是并没有只剩下两部分,说明后剪枝可能没有预剪枝有效。一般的,为了寻求最佳模型可以同时使用两种剪枝技术。
因为树回归每个节点是一些特征和特征值,选取的原则是根据特征方差最小。
用树来对数据建模,除了把叶节点设定为常数值之外,还可以把叶节点设置为分段线性函数,这就是模型树。如下图可用2条直线模型来拟合。
前面的代码稍加修改就可以在叶节点生成线性模型而不是常数值,下面将利用数生成算法对数据进行切分,且每份切分数据都能很容易的被线性模型表示。该算法的关键在于误差的计算。
def linearSolve(dataSet): #将数据集分成自变量x集和目标变量y
m,n = np.shape(dataSet)
X = np.mat(np.ones((m,n))); Y = np.mat(np.ones((m,1)))
X[:,1:n] = dataSet[:,0:n-1]; Y = dataSet[:,-1]
xTx = X.T*X
if np.linalg.det(xTx) == 0.0:
raise NameError('This matrix is singular, cannot do inverse,\n\
try increasing the second value of ops')
ws = xTx.I * (X.T * Y)
return ws,X,Y
def modelLeaf(dataSet):#生成叶节点模型
ws,X,Y = linearSolve(dataSet)
return ws
def modelErr(dataSet):#生成实际值与估计值之间的总误差
ws,X,Y = linearSolve(dataSet)
yHat = X * ws
return np.sum(np.power(Y - yHat,2))
myDat = loadDataSet('exp2.txt')
myMat2 = np.mat(myDat)
print(createTree(myMat2,modelLeaf,modelErr,(1,10)))
结果
可见该代码以0.285477为界创建2个模型,与真实模型十分接近。
以这个数据集为例
def regTreeEval(model, inDat):#回归树类型
return float(model)
def modelTreeEval(model, inDat):#模型树类型,对输入数据进行格式化处理,在原数据矩阵上增加第0列,然后计算并返回预测值
n = np.shape(inDat)[1]
X = np.mat(np.ones((1,n+1)))
X[:,1:n+1]=inDat
return float(X*model)
def treeForeCast(tree, inData, modelEval=regTreeEval):
if not isTree(tree): return modelEval(tree, inData)
if inData[tree['spInd']] > tree['spVal']:
if isTree(tree['left']): return treeForeCast(tree['left'], inData, modelEval)
else: return modelEval(tree['left'], inData)
else:
if isTree(tree['right']): return treeForeCast(tree['right'], inData, modelEval)
else: return modelEval(tree['right'], inData)
def createForeCast(tree, testData, modelEval=regTreeEval):
m=len(testData)
yHat = np.mat(np.zeros((m,1)))
for i in range(m):
yHat[i,0] = treeForeCast(tree, np.mat(testData[i]), modelEval)
return yHat
回归树误差:
trainMat = np.mat(loadDataSet('bikeSpeedVsIq_train.txt'))
testMat = np.mat(loadDataSet('bikeSpeedVsIq_test.txt'))
myTree = createTree(trainMat,ops=(1,20))
yHat = createForeCast(myTree,testMat[:,0])
print(np.corrcoef(yHat, testMat[:,1], rowvar=0)[0, 1])
0.964085231822215
模型树误差:
trainMat = np.mat(loadDataSet('bikeSpeedVsIq_train.txt'))
testMat = np.mat(loadDataSet('bikeSpeedVsIq_test.txt'))
myTree = createTree(trainMat,modelLeaf,modelErr,ops=(1,20))
yHat = createForeCast(myTree,testMat[:,0],modelTreeEval)
print(np.corrcoef(yHat, testMat[:,1], rowvar=0)[0, 1])
0.9760412191380619
由于越接近1越好,这里模型树比回归树拟合效果更好。
下面用线性回归拟合:
trainMat = np.mat(loadDataSet('bikeSpeedVsIq_train.txt'))
testMat = np.mat(loadDataSet('bikeSpeedVsIq_test.txt'))
ws,X,Y = linearSolve(trainMat)
m=len(testMat)
yHat = np.mat(np.zeros((m,1)))
for i in range(np.shape(testMat)[0]):
yHat[i] = testMat[i,0]*ws[1,0]+ws[0,0]
print(np.corrcoef(yHat, testMat[:,1], rowvar=0)[0, 1])
0.9434684235674767
可见线性回归在该问题上不如两种树回归。
机器学习提供了一些强大的工具,能从未知数据中抽取出有用的信息。因此,能否将这些信息以易于人们理解的方式呈现十分重要。再者,假如人们可以直接与算法和数据交互,将可以比较轻松地进行解释。如果仅仅只是绘制出一幅静态图像,或者只是在Python命令行中输出一些数字,那么对结果做分析和交流将非常困难。如果能让用户不需要任何指令就可以按照他们自己的方式来分析数据,就不需要对数据做出过多解释。其中一个能同时支持数据呈现和用户交互的方式就是构建一个 图形用户界面(GUI,Graphical User Interface),tkiner是易于使用的GUI框架。
import numpy as np
from tkinter import *
import regTree
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
def reDraw(tolS,tolN):
reDraw.f.clf() # 清除之前的图像
reDraw.a = reDraw.f.add_subplot(111)
if chkBtnVar.get():
if tolN < 2: tolN = 2
myTree=regTree.createTree(reDraw.rawDat, regTree.modelLeaf,regTree.modelErr, (tolS,tolN))
yHat = regTree.createForeCast(myTree, reDraw.testDat, regTree.modelTreeEval)
else:
myTree=regTree.createTree(reDraw.rawDat, ops=(tolS,tolN))
yHat = regTree.createForeCast(myTree, reDraw.testDat)
reDraw.a.scatter(np.array(reDraw.rawDat[:,0]),np.array(reDraw.rawDat[:,1]),s= 5) # 离散型散点图
reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0) # 构建yHat的连续曲线
reDraw.canvas.draw()
# 理解用户输入并防止程序崩溃
def getInputs():
try: tolN = int(tolNentry.get())
except:
tolN = 10
print("tolN请输入整数")
tolNentry.delete(0, END)
tolNentry.insert(0,'10')
try: tolS = float(tolSentry.get())
except:
tolS = 1.0
print("tolS请输入小数")
tolSentry.delete(0, END)
tolSentry.insert(0,'1.0')
return tolN,tolS
def drawNewTree():
tolN,tolS = getInputs() # 从输入框中获取值
reDraw(tolS,tolN) # 生成图
#构建树管理器界面小部件
root=Tk()
# 创造画布
reDraw.f = Figure(figsize=(5,4), dpi=100)
# 调用Agg,把Agg呈现在画布上
# Agg是一个C++的库,可以从图像创建光栅图
reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)
reDraw.canvas.draw()
reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)
Label(root, text="tolN").grid(row=1, column=0)
tolNentry = Entry(root)
tolNentry.grid(row=1, column=1)
tolNentry.insert(0,'10')
Label(root, text="tolS").grid(row=2, column=0)
tolSentry = Entry(root)
tolSentry.grid(row=2, column=1)
tolSentry.insert(0,'1.0')
# 初始化与reDraw()关联的全局变量
Button(root, text="ReDraw", command=drawNewTree).grid(row=1, column=2, rowspan=3)# 按钮整数值
chkBtnVar = IntVar()# 复选按钮
chkBtn = Checkbutton(root, text="Model Tree", variable = chkBtnVar)
chkBtn.grid(row=3, column=0, columnspan=2)
reDraw.rawDat = np.mat(regTree.loadDataSet('sine.txt'))
reDraw.testDat = np.arange(min(reDraw.rawDat[:,0]),max(reDraw.rawDat[:,0]),0.01)
reDraw(1.0, 10)
root.mainloop()
与回归树相比,模型树有更好的预测效果。
tolN是切分的最少样本数!
tolS是允许的最小误差