分类树与回归树的区别在样本的输出,如果样本输出是离散值,这是分类树;样本输出是连续值,这是回归树。分类树的输出是样本的类别,回归树的输出是一个实数。
分类模型:采用基尼系数的大小度量特征各个划分点的优劣。
回归模型:采用误差平方和度量。
输入:训练数据集D
输出:回归树T
计算各个特征各个值划分的两部分 D 1 和 D 2 D_1和D_2 D1和D2 的误差平方和,选择误差平方和最小的(如下式)作为最优特征和最优切分点
min A , a [ min c 1 ∑ x i ϵ D 1 ( A , a ) ( y i − c 1 ) 2 + min c 2 ∑ x i ϵ D 2 ( A , a ) ( y i − c 2 ) 2 ] \min_{A,a}\left[\min_{c_1}\sum_{x_i \epsilon D_1(A,a)}{(y_i-c_1)^2}+\min_{c_2}\sum_{x_i \epsilon D_2(A,a)}{(y_i-c_2)^2}\right] A,amin⎣⎡c1minxiϵD1(A,a)∑(yi−c1)2+c2minxiϵD2(A,a)∑(yi−c2)2⎦⎤
其中, c 1 c_1 c1为 D 1 D_1 D1的样本输出均值, c 2 c_2 c2为 D 2 D_2 D2的样本输出均值。
根据最优特征A和最优切分点a,将本节点的数据集划分成两部分 D 1 和 D 2 D_1和D_2 D1和D2 ,并给出相应的输出值
D 1 ( A , a ) = { ( x , y ) ∈ D ∣ A ( x ) < = a } D 2 ( A , a ) = { ( x , y ) ∈ D ∣ A ( x ) > a } D_1(A,a)= {\{(x,y)∈D|A(x)<=a\}}\\D_2(A,a)= {\{(x,y)∈D|A(x)>a\}} D1(A,a)={(x,y)∈D∣A(x)<=a}D2(A,a)={(x,y)∈D∣A(x)>a}
c 1 = a v e r a g e ( y i ∣ x i ϵ D 1 ( A , a ) ) c 2 = a v e r a g e ( y i ∣ x i ϵ D 2 ( A , a ) ) c_1=average(y_i|x_i\epsilon D_1(A,a))\\c_2=average(y_i|x_i\epsilon D_2(A,a)) c1=average(yi∣xiϵD1(A,a))c2=average(yi∣xiϵD2(A,a))
继续对两个子集调用1-2步骤,直到满足停止条件
生成回归树
对生成的CART回归树做预测时,用叶子节点的均值来作为预测的输出结果。
主要参考《机器学习实战》一书中的第9章树回归,来编写脚本的,因为我的环境是python3.6,所以按书本中的编写有几处会报错,故做了相关的修改,在代码中也一并标注出来了,大家需要注意下。
import numpy as np
def loadDataSet(fileName):
"""导入数据"""
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
# 使用python3会报错1,因为python3中map的返回类型是‘map’类,不能进行计算,需要将map转换为list
fltLine = list(map(float, curLine))
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):
"""
通过数组过滤切分数据集
:param dataSet: 数据集合
:param feature: 待切分的特征
:param value: 该特征的某个值
:return:
"""
# 使用python3会报错2,需要将书中脚本修改为以下内容
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]) * dataSet.shape[0]
def chooseBestSplit(dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
"""
遍历所有的特征及其可能的取值来找到使误差平方和最小化的切分特征及其切分点
:param dataSet: 数据集合
:param leafType: 建立叶节点的函数,该参数也决定了要建立的是模型树还是回归树
:param errType: 代表误差计算函数,即误差平方和计算函数
:param ops: 用于控制函数的停止时机,第一个是容许的误差下降值,第二个是切分的最少样本数
:return:最佳切分特征及其切分点
"""
tolS = ops[0]
tolN = ops[1]
# 如果所有值都相等,则停止切分,直接生成叶结点
if len(set(dataSet[:-1].T.tolist()[0])) == 1:
return None, leafType(dataSet)
m, n = dataSet.shape
S = errType(dataSet)
bestS = np.inf
bestIndex = 0
bestValue = 0
# 数据集中最后一列是标签,不是特征,所以这里是n-1
for featIndex in range(n - 1):
# set(dataSet[:,featIndex]) 使用python3会报错3,因为matrix类型不能被hash,需要修改为下面这句
for splitVal in set(dataSet[:, featIndex].T.tolist()[0]):
mat0, mat1 = binSplitDataSet(dataSet, featIndex, splitVal)
# 如果切分出的数据集小于切分最小样本数,则继续下一个
if mat0.shape[0] < tolN or mat1.shape[0] < tolN:
continue
newS = errType(mat0) + errType(mat1)
if newS < bestS:
bestS = newS
bestIndex = featIndex
bestValue = splitVal
# 如果误差减少不大,则停止切分,直接生成叶结点
if (S - bestS) < tolS:
return None, leafType(dataSet)
# 《机器学习实战》中,感觉下面这三句话多余(可以删了),因为在上面已经判断过了切分出的数据集很小的情况 #
# mat0, mat1 = binSplitDataSet(dataSet, bestIndex, bestValue) # 用最佳切分特征和切分点进行切分
# if mat0.shape[0] < tolN or mat1.shape[0] < tolN: # 如果切分出的数据集很小,则停止切分,直接生成叶结点
# return None, leafType(dataSet)
return bestIndex, bestValue # 返回最佳切分特征编号和切分点
def createTree(dataSet, leafType=regLeaf, errType=regErr, ops=(1, 4)):
"""
构建CART回归树
:param dataSet: 数据集,默认NumPy Mat
:param leafType: 建立叶节点的函数,该参数也决定了要建立的是模型树还是回归树
:param errType: 代表误差计算函数,即误差平方和计算函数
:param ops: 用于控制函数的停止时机,第一个是容许的误差下降值,第二个是切分的最少样本数
:return:
"""
feat, val = chooseBestSplit(dataSet, leafType, errType, ops)
# 如果feat为None, 则返回叶结点对应的预测值
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
绘制数据集ex00的散点图如下
对数据集ex00 构建回归树,输入以下语句测试下
dataMat = loadDataSet('./data/ex00.txt')
dataMat = np.mat(dataMat)
regTree = createTree(dataMat)
print(regTree)
运行结果如下
{'spInd': 0, 'spVal': 0.48813, 'left': 1.0180967672413792, 'right': -0.04465028571428572}
对数据集ex0构建回归树,输入以下语句测试下
dataMat = loadDataSet('./data/ex0.txt')
dataMat = np.mat(dataMat)
regTree = createTree(dataMat)
print(regTree)
运行结果如下
{'spInd': 0, 'spVal': 0.39435, 'left': {'spInd': 0, 'spVal': 0.582002, 'left': {'spInd': 0, 'spVal': 0.797583, 'left': 3.9871632, 'right': 2.9836209534883724}, 'right': 1.980035071428571}, 'right': {'spInd': 0, 'spVal': 0.197834, 'left': 1.0289583666666666, 'right': -0.023838155555555553}}
当决策树划分得太细时,会对数据产生过拟合,因此要通过剪枝来解决,降低决策树的复杂度来避免过拟合。剪枝(pruning)分为预剪枝和后剪枝,预剪枝(prepruning)是指在构造树的过程中就知道哪些节点可以剪掉,在函数chooseBestSplit中提前终止条件,其实也是在进行一种预剪枝操作 。 后剪枝(postpruning)是指构造出完整的决策树之后再来考查哪些子树可以剪掉。
使用后剪枝需要将数据集分为测试集和训练集。首先指定参数,使得构建出的树足够复杂,便于剪枝;然后从上而下找到叶节点,用测试集来判断这些叶节点合并是否能够降低测试误差,如果是的话就合并。
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['right'] + tree['left']) / 2.0
def prune(tree, testData):
"""
后剪枝
:param tree: 待剪枝的树
:param testData: 剪枝所需的测试数据
:return:
"""
# 如果没有测试数据,则对树进行塌陷处理
if testData.shape[0] == 0:
return getMean(tree)
# 检查左右分支是否是子树还是叶节点
if (isTree(tree['right']) or isTree(tree['left'])):
lSet, rSet = binSplitDataSet(testData, tree['spInd'], tree['spVal'])
# 如果是子树,递归调用prune()函数进行剪枝
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
errorMerge = sum(np.power(testData[:, -1] - treeMean, 2))
# 如果合并后的误差小于不合并的误差,则进行合并操作,反之不进行合并操作
if errorMerge < errorNoMerge:
print('Merging')
return treeMean
else:
return tree
else:
return tree
绘制数据集ex2的散点图如下
对数据集ex2 构建回归树并进行剪枝,输入以下语句测试下
dataMat2 = loadDataSet('./data/ex2.txt')
dataMat2 = np.mat(dataMat2)
mytree = createTree(dataMat2,ops=(0,1))
testMat = loadDataSet('./data/ex2test.txt')
testMat = np.mat(testMat)
prunedTree = prune(mytree,testMat)
print(prunedTree)
运行结果如下
Merging
Merging
...
Merging
{'spInd': 0, 'spVal': 0.499171, 'left': {'spInd': 0, 'spVal': 0.729397, 'left': {'spInd':
...
'left': {'spInd': 0, 'spVal': 0.053764, 'left': 1.7820349999999998, 'right': -13.731698}, 'right': 4.091626}}}}}}}}}}}
一般地,为了寻找最佳模型可以同时使用两种剪枝技术。
用树来对数据建模,除了把叶节点简单地设置为常数值之外,另一种方法是把叶节点设为分段线性函数,这里的分段线性(piecewise linear)是指模型由多个线性片段组成。
如上图,使用两条直线拟合会比使用一组常数来建模效果更好。 可以由[0.0,0.3]和[0.3,1.0]这两个区间的两条直线组成。决策树相比其他机器学习算法易于理解,而模型树的可解释性是它优于回归树的特性之一。模型树同时具备更高的预测准确度。
前面代码稍加修改就可以再叶节点生成线性模型而不是常数。createTree()里有两个参数leafType和errType从未改变过,回归树把这两个参数固定,而模型树需要对此略作修改。
为了寻找最佳切分,怎么计算误差?不能用回归树的误差计算方法了,这里需要对给定的数据集,先用线性的模型对它进行拟合,然后计算真实目标值和模型预测值之间的差距。最后求这些差值的平方和作为所需的误差。
def linearSolve(dataSet):
"""
将数据集格式化为目标变量Y和自变量X,并计算得到回归系数ws
:param dataSet:
:return: 回归系数ws,自变量X,目标变量Y
"""
m, n = dataSet.shape
# 将X和Y中的数据格式化
X = np.mat(np.ones((m, n)))
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 od ops")
ws = xTx.I * (X.T * Y)
return ws, X, Y
def modelLeaf(dataSet):
"""负责生产叶节点的模型,返回回归系数ws"""
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))
输入以下语句测试下
dataMat2 = loadDataSet('./data/exp2.txt')
dataMat2 = np.mat(dataMat2)
mytree = createTree(dataMat2, modelLeaf,modelErr,(1, 10))
print(mytree)
运行结果如下,
{'spInd': 0, 'spVal': 0.285477, 'left': matrix([[1.69855694e-03],
[1.19647739e+01]]), 'right': matrix([[3.46877936],
[1.18521743]])}
可以看出,生成的两个线性模型分别是 y = 3.468 + 1.1852 x 和 y = 0.0016985 + 11.96477 x y=3.468+1.1852x 和 y=0.0016985+11.96477x y=3.468+1.1852x和y=0.0016985+11.96477x。与用于生成该数据的真实模型( y = 3.5 + 1.0 x 和 y = 0 + 12 x y=3.5+1.0x和y=0+12x y=3.5+1.0x和y=0+12x)非常接近。
函数treeForeCast()自顶向下遍历整棵树,直到命中叶节点为止。一旦到达叶节点,它会在输入数据上调用modelEval()函数,该参数默认值是regTreeEval()。要对回归树叶节点预测,就调用regTreeEval()函数,要对模型树节点预测,调用modelTreeEval()函数。
def regTreeEval(model, inDat):
"""对回归树叶节点进行预测,返回一个预测值"""
return float(model)
def modelTreeEval(model, inDat):
"""对于模型树叶节点进行预测,返回一个预测值"""
n = inDat.shape[1]
X = np.mat(np.ones((1, n + 1)))
X[:, 1:n + 1] = inDat
return float(X * model)
def treeForeCast(tree, inData, modelEval=regTreeEval):
"""
对于输入的单个数据点或者行向量进行预测,返回一个浮点值
:param tree:生成树
:param inData:输入的单个测试数据
:param modelEval:指定数的类型,回归树还是模型树
:return:
"""
# 判断是否是树,如果不是,返回叶节点
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
数据集为人的智力水平和自行车的速度的关系(此数据纯属虚构),该数据散点图如下
输入下面语句,构建回归树和模型树,并比较 R 2 R^2 R2值
trainMat = np.mat(loadDataSet('./data/bikeSpeedVsIq_train.txt'))
testMat = np.mat(loadDataSet('./data/bikeSpeedVsIq_test.txt'))
print("-" * 20 + '回归树' + "-" * 20)
tree = createTree(trainMat, ops=(1, 20))
# print(tree)
yHat = createForeCast(tree, testMat[:, 0])
print("R平方为:",np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0, 1]) # 打印R平方,即相关系数
print("-" * 20 + '模型树' + "-" * 20)
tree = createTree(trainMat, modelLeaf, modelErr, ops=(1, 20))
# print(tree)
yHat = createForeCast(tree, testMat[:, 0], modelTreeEval)
print("R平方为:",np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0, 1]) # 打印R平方,即相关系数
我们知道, R 2 R^2 R2值越接近1.0越好,从上图可以看出,模型树的结果比回归树好。
下面看一下标准线性回归的效果,需要前面实现的线性方程组求解函数 linearSolve (),输入下面语句
print("-" * 20 + '标准线性回归' + "-" * 20)
ws, x, y = linearSolve(trainMat)
print("回归系数为:\n", ws)
for i in range(testMat.shape[0]):
yHat[i] = testMat[i, 0] * ws[1, 0] + ws[0, 0]
print("R平方为:",
np.corrcoef(yHat, testMat[:, 1], rowvar=0)[0, 1]) # 打印R平方,即相关系数
运行结果如下:
可以看到,标准线性回归方法在 R 2 R^2 R2值上的表现不如上面两种树回归方法。所以,树回归在预测复杂数据时会比简单的线性模型更有效。
机器学习从数据中提取有用的信息,能否将这些信息以易于人们理解的方式呈现非常重要。构建一个图像用户界面(GUI,Graphical User Interface)可以同时支持数据呈现和用户交互的方式。
python有很多GUI框架,其中一个易于使用的Tkinter,随python标准编译版本发布。Tkinter可以再Windows、Mac OS和大多数Linux平台上使用。
注意:python3.6版本是自动安装tkinter。
from tkinter import * # 导入tkinter包
root = Tk() # 会出现一个小窗口
# 在窗口上显示一些文字
myLabel = Label(root, text='Hello World')
myLabel.grid()
root.mainloop() # 启动时间循环,使该窗口在众多时间中可以响应鼠标点击、按钮和重绘等动作
运行结果如下
Tkinter的GUI是由一些小部件(Widget)组成的。小部件包括文本框(Text Box),按钮(Button),标签(刚才用的Label)、复选框(Check Button)等对象。其中,.grid()会把小部件安排到一个二维表格布局管理器中。用户可以设定每个小部件所在的行列位置,默认会显示在0行0列。
下面为集成Matplotlib和Tkinter的代码
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():
if tolN < 2:
tolN = 2
myTree = createTree(reDraw.rawDat, modelLeaf, modelErr, (tolS,tolN))
yHat = createForeCast(myTree,reDraw.testDat, modelTreeEval)
else:
myTree = createTree(reDraw.rawDat,ops=(tolS,tolN))
yHat = createForeCast(myTree,reDraw.testDat)
reDraw.a.scatter(reDraw.rawDat[:,0].flatten().A[0],reDraw.rawDat[:,1].flatten().A[0],s=5) # 绘制散点图
reDraw.a.plot(reDraw.testDat,yHat,linewidth=2.0) # 绘制连续曲线
reDraw.canvas.draw() # python3.6中用draw(),用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() # 获取输入框的值
reDraw(tolS,tolN) # 绘图
输入下面语句,将所需的小部件集成在一起构建树管理器界面
if __name__ == '__main__':
root = Tk() # 创建一个Tk类型的根部件
# 在Tk的GUI上放置一个画布,并用.grid()来调整布局
reDraw.f = Figure(figsize=(5, 4), dpi=100)
reDraw.canvas = FigureCanvasTkAgg(reDraw.f, master=root)
reDraw.canvas.draw()
reDraw.canvas.get_tk_widget().grid(row=0, columnspan=3)
# Label()标签,在窗口上会显示一些文字,.grid()设置行和列的位置
# rowspan和columnspan:跨行数和跨列数
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')
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关联的全局变量
reDraw.rawDat = np.mat(loadDataSet('./data/sine.txt'))
reDraw.testDat = np.arange(min(reDraw.rawDat[:, 0]),max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)
root.mainloop()
运行程序,默认情况下(即不勾选复选框),会给出一棵包含八个节点的回归树,如下
勾选复选框,然后点击“ReDraw”按钮会得到模型树结果,如下
数据集中会包含一些复杂的相互关系,使输入数据和目标变量之间存在非线性的关系。对于这种复杂关系的建模,一种可行的方式是使用树来对预测值分段,包括分段常数(回归树)和分段直线(模型树)。
CART算法可以用于构建二元树并处理离散型或连续型数据的切分。若使用不同的误差准则,就可以通过CART算法构建模型树和回归树。但是,该算法构建的树倾向于对数据过拟合,可采用剪枝的方法解决该问题。剪枝分为预剪枝(在树的构建过程中就就进行剪枝)和后剪枝(树构建完毕进行剪枝)。预剪枝更有效但需要用户定义一些参数。一般地,为了寻找最佳模型可以同时使用两种剪枝技术。
Tkinter是python的一个最常用的GUI工具包。利用Tkinter,可以轻松绘制各种部件并灵活安排它们的位置。另外,可以为Tkinter构造一个特殊的部件来显示Matplotlib绘出的图。所以,Matplotlib和Tkinter的集成可以构建出更强大的GUI。
书籍:《机器学习实战》、周志华的西瓜书《机器学习》
ID3算法
C4.5算法
CART分类树算法