理解: 如果CART树处理离散型数据,叫做分类决策树,那么,引入基尼指数作为寻找最好的数据划分的依据,基尼指数越小,说明数据的“纯度越高”,随机森林的代码里边就运用到了基尼指数。如果CART树处理连续型数据时,叫做回归决策树,那么,引入了平方误差,首先,它使用二元切分来处理数据,得到两个子集,计算误差,找到最小误差,确定最佳切分的特征编号和特征值,然后进行建树。
构建回归树,需要给定某个误差计算方法,该函数会找到数据集上最佳的二元切分方式。另外,该函数还要确定什么时候停止划分,一旦停止划分会生成一个叶节点。这里引入reLeaf(),regErr()分别得到叶节点和总方差。叶节点的模型是目标变量的 均值,var()是均方差,所以需要乘以数据集的样本个数。
划分数据集时,如果找不到一个‘好’的二元切分,该函数返回None值并产生叶节点,叶节点的值也为None。
from numpy import *
#载入数据
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
#切分数据集为两个子集
# dataSet: 数据集合
# feature: 待切分的特征
# value: 该特征的某个值
#nonzero():得到数组非零元素的位置(数组索引)
def binSplitDataSet(dataSet, feature, value) :
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:]
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:]
return mat0, mat1
# 负责生成叶节点,当chooseBestSplit()函数确定不再对数据进行切分时,
# 将调用该regLeaf()函数来得到叶节点的模型,在回归树中,该模型其实就是目标变量的均值
def regLeaf(dataSet) :
return mean(dataSet[:, -1])
# 误差估计函数,该函数在给定的数据上计算目标变量的平方误差,这里直接调用均方差函数var
# 因为这里需要返回的是总方差,所以要用均方差乘以数据集中样本的个数
def regErr(dataSet) :
return var(dataSet[:, -1]) * shape(dataSet)[0]
# dataSet: 数据集合
# leafType: 给出建立叶节点的函数
# errType: 误差计算函数
# ops: 包含树构建所需其他参数的元组
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)) :
# 将数据集分成两个部分,若满足停止条件,chooseBestSplit将返回None和某类模型的值
# 若构建的是回归树,该模型是个常数。若是模型树,其模型是一个线性方程。
# 若不满足停止条件,chooseBestSplit()将创建一个新的Python字典,并将数据集分成两份,
# 在这两份数据集上将分别继续递归调用createTree()函数
feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
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
# 回归树的切分函数,构建回归树的核心函数。目的:找出数据的最佳二元切分方式。如果找不到
# 一个“好”的二元切分,该函数返回None并同时调用createTree()方法来产生叶节点,叶节点的
# 值也将返回None。
# 如果找到一个“好”的切分方式,则返回特征编号和切分特征值。
# 最佳切分就是使得切分后能达到最低误差的切分。
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1,4)) :
# tolS是容许的误差下降值
# tolN是切分的最小样本数
tolS = ops[0]; tolN = ops[1]
# 如果剩余特征值的数目为1,那么就不再切分而返回
if len(set(dataSet[:, -1].T.tolist()[0])) == 1 :
return None, leafType(dataSet)
# 当前数据集的大小
m,n = shape(dataSet)
# 当前数据集的误差
S = errType(dataSet)
bestS = inf; bestIndex = 0; bestValue = 0
for featIndex in range(n-1) :
# for splitVal in set(dataSet[:, featIndex]) :
for splitVal in set(dataSet[:,featIndex].T.tolist()[0]):
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)
if (shape(mat0)[0] < tolN) or (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)
# 检查切分后的子集大小,如果某个子集的大小小于用户定义的参数tolN,那么也不应切分。
if (shape(mat0)[0] < tolN) or (shape(mat1)[0] < tolN) :
return None, leafType(dataSet)
# 如果前面的这些终止条件都不满足,那么就返回切分特征和特征值。
return bestIndex, bestValue
通过降低决策树的复杂度来避免过拟合的过程叫剪枝,预剪枝和后剪枝的单个效果可能是不好的,一般来说,我们可以同时采用这两种剪枝方法。
理解:模型树和回归树的区别就是回归树的叶节点是一个常数值,而模型树的叶节点是分段线性函数,分段线性模型就是我们对数据集的一部分数据以某个线性模型建模,而另一份数据以另一个线性模型建模。
#模型树
# 主要功能:将数据格式化成目标变量Y和自变量X。X、Y用于执行简单的线性规划。
def linearSolve(dataSet) :
m,n = shape(dataSet)
X = mat(ones((m,n))); Y = mat(ones((m,1)))
X[:, 1:n] = dataSet[:, 0:n-1]; Y = dataSet[:, -1]
xTx = X.T*X
# 矩阵的逆不存在时会造成程序异常
if 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
# 与regLeaf()类似,当数据不需要切分时,它负责生成叶节点的模型。
def modelLeaf(dataSet) :
ws, X, Y = linearSolve(dataSet)
return ws
# 在给定的数据集上计算误差。与regErr()类似,会被chooseBestSplit()调用来找到最佳切分。
def modelErr(dataSet) :
ws, X, Y = linearSolve(dataSet)
yHat = X * ws
return sum(power(Y-yHat, 2))
# 为了和modeTreeEval()保持一致,保留两个输入参数
def regTreeEval(model, inDat) :
return float(model)
# 对输入数据进行格式化处理,在原数据矩阵上增加第0列,元素的值都是1
def modelTreeEval(model, inDat) :
n = shape(inDat)[1]
X = mat(ones((1, n+1)))
X[:, 1:n+1] = inDat
return float(X*model)
def isTree(obj):
return (type(obj).__name__=='dict')
# 在给定树结构的情况下,对于单个数据点,该函数会给出一个预测值。
# modeEval是对叶节点进行预测的函数引用,指定树的类型,以便在叶节点上调用合适的模型。
# 此函数自顶向下遍历整棵树,直到命中叶节点为止,一旦到达叶节点,它就会在输入数据上
# 调用modelEval()函数,该函数的默认值为regTreeEval()
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)
# 多次调用treeForeCast()函数,以向量形式返回预测值,在整个测试集进行预测非常有用
def createForeCast(tree, testData, modelEval=regTreeEval) :
m = len(testData)
yHat = mat(zeros((m,1)))
for i in range(m) :
yHat[i,0] = treeForeCast(tree, mat(testData[i]), modelEval)
return yHat
使用Tkinter工具构建图形用户界面:
from numpy import *
from tkinter import *
import regTrees as regTrees
import matplotlib
matplotlib.use('TkAgg') #设置后端TkAgg
#将TkAgg和matplotlib链接起来
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() : #检查选框model tree是否被选中
if tolN < 2 : tolN = 2
myTree = regTrees.createTree(reDraw.rawDat, regTrees.modelLeaf,
regTrees.modelErr, (tolS, tolN))
yHat = regTrees.createForeCast(myTree, reDraw.testDat, regTrees.modelTreeEval)
else :
myTree = regTrees.createTree(reDraw.rawDat, ops=(tolS, tolN))
yHat = regTrees.createForeCast(myTree, reDraw.testDat)
# reDraw.rawDat[:,0].A,需要将矩阵转换成数组
reDraw.a.scatter(reDraw.rawDat[:,0].A, reDraw.rawDat[:,1].A, s=5) # 绘制真实值
reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0) # 绘制预测值
reDraw.canvas.show()
#
def getInputs() :#获取输入
try : tolN = int(tolNentry.get()) #期望输入是整数
except : #清楚错误用默认值替换
tolN = 10
print ("enter Integer for tolN")
tolNentry.delete(0, END)
tolNentry.insert(0, '10')
try : tolS = float(tolSentry.get())
except : #期望输入是浮点数
tolS = 1.0
print ("enter Float for tolS")
tolSentry.delete(0, END)
tolSentry.insert(0, '1.0')
return tolN, tolS
#
def drawNewTree() :
# 取得输入框的值
tolN, tolS = getInputs() # 从输入文本框中获取参数
# 利用tolN,tolS,调用reDraw生成漂亮的图
reDraw(tolS, tolN) #绘制图
#布局GUI
root = Tk()
# 创建画布
Label(root, text='Plot Place Holder').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”按钮后,调用drawNewTree()函数
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.f = Figure(figsize=(5,4), dpi=100)
reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)
reDraw.canvas.show()
reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)
reDraw.rawDat = mat(regTrees.loadDataSet('ex00.txt'))
reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)
root.mainloop()