线性回归模型需要拟合所有样本(局部加权线性回归除外),当数据拥有众多特征且特征间关系复杂时,构建全局模型就显得太难了。一种可行的方法是将数据集切分成很多份易建模的数据,然后利用线性回归技术建模。如果首次切分后仍难以拟合线性模型就继续切分,在这种切分模式下,树结构和回归法相当有用。
CART(Classification And Regression Trees,分类回归树)算法,即可用于分类,也可用于回归。其中的树剪枝技术用于防止树的过拟合。
决策树不断将数据切分成小数据集,直到所有目标变量完全相同,或者数据不能再切分为止。决策树是一种贪心算法,它要在给定的时间内做出最佳选择,但不关心能否达到全局最优。
前面介绍的决策树构建算法是ID3。ID3的做法是每次选取当前最佳的特征来分隔数据,并按照该特征的所有可能来切分。这种切分过于迅速,且不能处理连续性特征。另外一种方法是二元切分法,它易于对树构建过程进行调整以处理连续性特征。
CART是十分著名的树构建算法,它使用二元切分来处理连续性变量,对其稍作修改就可处理回归问题。CART算法也使用一个字典来存储树的数据结构,该字典含:
CART可构建两种书:回归树(regression tree),其每个叶节点包含单个值;模型树(model tree),其每个叶节点包含一个线性方程。创建树的函数createTree()的伪代码大致如下:
找到最佳的待切分特征 :
如果该节点不能再分,将该节点存为叶节点
执行二元切分
在右子树调用createTree()方法
在左子树调用createTree()方法
回归树假设叶节点是常数值,这种策略认为数据中的复杂关系可用树结构来概括。为了构建以分段常数为叶节点的树,需要度量数据的一致性。使用ID3算法构建的决策树进行分类,会在给定节点时计算数据的混乱度。计算连续性数值的混乱度是非常简单的,首先计算所有数值的均值,然后计算每条数据的值到均值的差值。为了对正负差值同等看待,一般使用绝对值或平方值来代替上述差值。这里使用的是总方差(平方误差的总值),总方差=均方差*样本数。
构建树首先要实现如何切分数据集,使用函数chooseBestSplit()函数切分数据集。给定误差计算方法,该函数寻找数据集上的最佳二元切分方式,一旦停止切分会生成一个叶节点。它遍历所有的特征及其可能的取值来找到使误差最小化的切分阈值。函数的伪代码如下:
对每个特征 :
对每个特征值 :
将数据集切分成两份
计算切分的误差
如果当前误差小于当前最小误差,那么将当前切分设定为最佳切分并更新最小误差
返回最佳切分的特征和阈值
切分停止的三个条件:
决策树也可使用测试集上某种交叉验证技术发现过拟合。通过降低决策树的复杂度来避免过拟合的过程称为剪枝(pruning),在chooseBestSplit()中提前终止条件,实际上是一种所谓的预剪枝(prepruning)操作。另一种剪枝需要使用测试集和训练集,称为后剪枝(postpruning)。
树构建算法对输入参数tolS和tolN(预剪枝)非常敏感。停止条件tolS对误差的数量级十分敏感。通过不断修改停止条件来得到合理结果不是好办法,事实上,我们常常不确定需要寻找什么样的结果,而这正是机器学习所关注的内容,计算机应可给出总体的概貌。
后剪枝,使用测试集来对树进行剪枝,不需要用户指定参数,是一种更理想化的剪枝方法。使用后剪枝,需要将数据集分为测试集和训练集。首先指定参数,使得构建的树足够大和复杂,便于剪枝。接着,从上到下找到叶节点,用测试集来判断将这些叶节点合并是否能够降低测试误差,如果是的话就合并,合并也称为塌陷处理,在回归树中一般采用取需要合并的所有子树的平均值。函数prune()的伪代码如下:
基于已有的树切分测试数据 :
如果存在任一子集是一棵树,则在该子集递归剪枝过程
计算将当前两个叶节点合并后的误差
计算不合并的误差
如果合并会降低误差的话,那就将叶节点合并
后剪枝可能不如预剪枝有效,一般,为了需求最佳模型可同时使用两种剪枝技术。
用树建模,除了把叶节点简单地设定为常数值外,还可把叶节点设定为分段线性函数,这里的分段线性是指模型由多个线性片段组成。模型树的可解析性是它由于回归的特点之一,它还具有更高的预测准确度。
模型树、回归树以及其他模型中那个更好,可使用相关系数 R2 来衡量。具体使用numpy.corrcoef(yHat, y, rowvar=0)
来求解。
>>> from Tkinter import *
>>> root = Tk()
>>> myLabel = Label(root, text="Hello World")
>>> myLabel.grid()
>>> root.mainloop()
Tkinter的GUI由一些小部件(Widget)组成。小部件,指的是文本框(TextBox)、按钮(Button)、标签(Label)和复选按钮(CheckButton)等对象。上例中myLabel的.grid()方法把myLabel的位置告诉了布局管理器。详细的实例见代码treeExplore.py
可以将Matplotlib绘制的图像放在GUI上,通过修改Matplotlib后端达到在Tkinter的GUI上绘图的目的。Matplotlib的构建程序包含一个前端,即面向用户的一些代码,如plot()和scatter()方法。Matplotlib同时也创建了一个后端,用于实现绘图和不同应用间的接口。通过改变后端可将图像绘制在PNG、PDF、SVG等格式的文件上。
接下来,设置Matplotlib的后端为TkAgg(Agg是一个C++的库,可从图像创建光栅图),TkAgg可在所选的GUI框架上调用Agg,把Agg呈现在画布上。可在Tk的GUI上放置一个画布,并用.grid()来调整布局。
具体的集成代码,请参照treeExplore.py,treeExplore.py绘制的图形如下所示
函数 | 功能 |
---|---|
map(function, sequence[, sequence, …]) -> list | 根据提供的函数对指定序列做映射,简单的示例,map(float, curLine):对curLine中的元素作浮点转换 |
numpy.var(a) | 计算样本集的方差 |
pow(x, y[, z]) | 函数是计算x的y次方,如果z在存在,则再对结果进行取模,其结果等效于pow(x,y) %z,pow() 通过内置的方法直接调用,内置方法会把参数作为整型,而 math 模块则会把参数转换为 float。 |
numpy.linalg.det(xTx) | 求方阵的行列式 |
xTx.I | 对矩阵求逆 |
corrcoef(yHat, y, rowvar=0) | 求解yHat,y的相关系数,也称为 R2 值 |
# coding=utf-8
from numpy import *
def loadDataSet(fileName) :
dataMat = []
fr = open(fileName)
for line in fr.readlines() :
curLine = line.strip().split('\t')
fltLine = map(float, curLine)
dataMat.append(fltLine)
return dataMat
# dataSet: 数据集合
# feature: 待切分的特征
# value: 该特征的某个值
def binSplitDataSet(dataSet, feature, value) :
mat0 = dataSet[nonzero(dataSet[:,feature] > value)[0],:][0]
mat1 = dataSet[nonzero(dataSet[:,feature] <= value)[0],:][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]) :
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)
# 在给定树结构的情况下,对于单个数据点,该函数会给出一个预测值。
# 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
# coding=utf-8
from numpy import *
from Tkinter import *
import ml.regTrees as regTrees
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 = 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)
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('c:\python27\ml\\sine.txt'))
reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)
root.mainloop()
>>> import ml.regTrees
>>> from numpy import *
>>> testMat = mat(eye(4))
>>> testMat
matrix([[ 1., 0., 0., 0.],
[ 0., 1., 0., 0.],
[ 0., 0., 1., 0.],
[ 0., 0., 0., 1.]])
>>> mat0, mat1 = regTrees.binSplitDataSet(testMat, 1, 0.5)
>>> mat0
matrix([[ 0., 1., 0., 0.]])
>>> mat1
matrix([[ 1., 0., 0., 0.],
[ 0., 0., 1., 0.],
[ 0., 0., 0., 1.]])
# 运行二元切分代码
>>> reload(regTrees)
'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> myData = regTrees.loadDataSet('c:\python27\ml\\ex00.txt')
>>> myMat = mat(myData)
>>> regTrees.createTree(myMat)
{'spInd': 0, 'spVal': matrix([[ 0.48813]]), 'right': -0.044650285714285719, 'left': 1.0180967672413792}
# 多次切分的列子
>>> myData1 = regTrees.loadDataSet('c:\python27\ml\\ex0.txt')
>>> myMat1 = mat(myData1)
>>> regTrees.createTree(myMat1)
{'spInd': 1, 'spVal': matrix([[ 0.39435]]), 'right': {'spInd': 1, 'spVal': matri
x([[ 0.197834]]), 'right': -0.023838155555555553, 'left': 1.0289583666666666}, '
left': {'spInd': 1, 'spVal': matrix([[ 0.582002]]), 'right': 1.980035071428571,
'left': {'spInd': 1, 'spVal': matrix([[ 0.797583]]), 'right': 2.9836209534883724
, 'left': 3.9871631999999999}}}
# 后剪枝
>>> reload(regTrees)
'ml.regTrees' from 'C:\Python27\ml\regTrees.pyc'>
>>> myData2 = regTrees.loadDataSet('c:\python27\ml\\ex2.txt')
>>> myMat2 = mat(myData2)
>>> from numpy import *
>>> myMat2 = mat(myData2)
>>> regTrees.createTree(myMat2)
>>> regTrees.createTree(myMat2)
{'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': {'spInd': 0, 'spVal': matr
......
0, 'spVal': matrix([[ 0.958512]]), 'right': 112.42895575000001, 'left': 105.2486
2350000001}}}}
>>> regTrees.createTree(myMat2, ops=(10000, 4))
{'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': -2.6377193297872341, 'left
': 101.35815937735848}
>>> myTree = regTrees.createTree(myMat2, ops=(0,1))
>>> myDataTest = regTrees.loadDataSet('c:\python27\ml\\ex2test.txt')
>>> myMat2Test = mat(myDataTest)
>>> regTrees.prune(myTree, myMat2Test)
Merging
Merging
Merging
......
Merging
{'spInd': 0, 'spVal': matrix([[ 0.499171]]), 'right': {'spInd': 0, 'spVal': matr
......
rix([[ 0.965969]]), 'right': {'spInd': 0, 'spVal': matrix([[ 0.956951]]), 'right
': 111.2013225, 'left': {'spInd': 0, 'spVal': matrix([[ 0.958512]]), 'right': 13
5.83701300000001, 'left': {'spInd': 0, 'spVal': matrix([[ 0.960398]]), 'right':
123.559747, 'left': 112.386764}}}, 'left': 92.523991499999994}}}}
# 模型树
>>> reload(regTrees)
'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> myMat2 = mat(regTrees.loadDataSet('c:\python27\ml\\exp2.txt'))
>>> regTrees.createTree(myMat2, regTrees.modelLeaf, regTrees.modelErr, (1,10))
{'spInd': 0, 'spVal': matrix([[ 0.285477]]), 'right': matrix([[ 3.46877936],
[ 1.18521743]]), 'left': matrix([[ 1.69855694e-03],
[ 1.19647739e+01]])}
# 树回归与标准回归的比较
>>> reload(regTrees)
'ml.regTrees' from 'C:\Python27\ml\regTrees.py'>
>>> trainMat = mat(regTrees.loadDataSet('c:\python27\ml\\bikeSpeedVsIq_train.txt'))
>>> testMat = mat(regTrees.loadDataSet('c:\python27\ml\\bikeSpeedVsIq_test.txt'))
>>> myTree = regTrees.createTree(trainMat, ops=(1,20))
>>> yHat = regTrees.createForeCast(myTree, testMat[:,0])
>>> corrcoef(yHat, testMat[:,1], rowvar=0)
array([[ 1. , 0.96408523],
[ 0.96408523, 1. ]])
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.96408523182221384
>>> myTree = regTrees.createTree(trainMat, regTrees.modelLeaf, regTrees.modelErr
, (1,20))
>>> yHat = regTrees.createForeCast(myTree, testMat[:,0], regTrees.modelTreeEval)
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.97604121913806285
# 标准回归
>>> ws, X, Y = regTrees.linearSolve(trainMat)
>>> ws
matrix([[ 37.58916794],
[ 6.18978355]])
>>> for i in range(shape(testMat)[0]) :
... yHat[i] = testMat[i,0]*ws[1,0] + ws[0,0]
...
>>> corrcoef(yHat, testMat[:,1], rowvar=0)[0,1]
0.94346842356747584
# 用Tkinter创建GUI
>>> from Tkinter import *
>>> root = Tk()
>>> myLabel = Label(root, text="Hello World")
>>> myLabel.grid()
# 为了程序的完整,还应输入下面命令,此命令将启动时间循环,使该窗口在众多事件中可以响应鼠标点击、按键和重绘动作
>>> root.mainloop()