之前线性回归创建的模型需要拟合所有的样本点,但数据特征众多,关系复杂时,构建全局模型就很困难。之前构建决策树使用的算法是ID3。
ID3 的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有 4 种取值,那么数据将被切分成 4 份。一旦按照某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。另外一种方法是二元切分法,即每次把数据集切分成两份。如果数据的某特征值等于切分所要求的值,那么这些数据就进入树的左子树,反之则进入树的右子树。
除了切分过于迅速外, ID3 算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征转换成离散型,才能在 ID3 算法中使用。但这种转换过程会破坏连续型变量的内在性质。而使用二元切分法则易于对树构造过程进行调整以处理连续型特征。具体的处理方法是: 如果特征值大于给定值就走左子树,否则就走右子树。另外,二元切分法也节省了树的构建时间,但这点意义也不是特别大,因为这些树构建一般是离线完成,时间并非需要重点关注的因素。
CART 是十分著名且广泛记载的树构建算法,它使用二元切分来处理连续型变量。对 CART 稍作修改就可以处理回归问题。决策树中使用香农熵来度量集合的无组织程度。如果选用其他方法来代替香农熵,就可以使用树构建算法来完成回归。
回归树与分类树的思路类似,但是叶节点的数据类型不是离散型,而是连续型。
还有一点要说明,构建决策树算法,常用到的是三个方法: ID3, C4.5, CART.
三种方法区别是划分树的分支的方式:
工程上总的来说:
CART 和 C4.5 之间主要差异在于分类结果上,CART 可以回归分析也可以分类,C4.5 只能做分类;C4.5 子节点是可以多分的,而 CART 是无数个二叉子节点;
以此拓展出以 CART 为基础的 “树群” Random forest , 以 回归树 为基础的 “树群” GBDT 。
一棵树如果节点过多,表明该模型可能对数据进行了 “过拟合”。
通过降低决策树的复杂度来避免过拟合的过程称为 剪枝(pruning)
。在函数 chooseBestSplit() 中提前终止条件,实际上是在进行一种所谓的 预剪枝(prepruning)
操作。另一个形式的剪枝需要使用测试集和训练集,称作 后剪枝(postpruning)
。
顾名思义,预剪枝就是及早的停止树增长,在构造决策树的同时进行剪枝。
所有决策树的构建方法,都是在无法进一步降低熵的情况下才会停止创建分支的过程,为了避免过拟合,可以设定一个阈值,熵减小的数量小于这个阈值,即使还可以继续降低熵,也停止继续创建分支。但是这种方法实际中的效果并不好。
决策树构造完成后进行剪枝。剪枝的过程是对拥有同样父节点的一组节点进行检查,判断如果将其合并,熵的增加量是否小于某一阈值。如果确实小,则这一组节点可以合并一个节点,其中包含了所有可能的结果。合并也被称作 塌陷处理
,在回归树中一般采用取需要合并的所有子树的平均值。后剪枝是目前最普遍的做法。
用树来对数据建模,除了把叶节点简单地设定为常数值之外,还有一种方法是把叶节点设定为分段线性函数,这里所谓的 分段线性(piecewise linear)
是指模型由多个线性片段组成。
from __future__ import print_function
from Tkinter import *
from numpy import *
# 默认解析的数据是用tab分隔,并且是数值类型
# general function to parse tab -delimited floats
def loadDataSet(fileName):
"""loadDataSet(解析每一行,并转化为float类型)
Desc: 该函数读取一个以 tab 键为分隔符的文件,然后将每行的内容保存成一组浮点数
Args:
fileName 文件名
Returns:
dataMat 每一行的数据集array类型
Raises:
"""
# 假定最后一列是结果值
# assume last column is target value
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
# 将所有的元素转化为float类型
# map all elements to float()
# map() 函数具体的含义,可见 https://my.oschina.net/zyzzy/blog/115096
fltLine = map(float, curLine)
dataMat.append(fltLine)
return dataMat
def binSplitDataSet(dataSet, feature, value):
"""binSplitDataSet(将数据集,按照feature列的value进行 二元切分)
Description: 在给定特征和特征值的情况下,该函数通过数组过滤方式将上述数据集合切分得到两个子集并返回。
Args:
dataMat 数据集
feature 待切分的特征列
value 特征列要比较的值
Returns:
mat0 小于等于 value 的数据集在左边
mat1 大于 value 的数据集在右边
Raises:
"""
# # 测试案例
# print 'dataSet[:, feature]=', dataSet[:, feature]
# print 'nonzero(dataSet[:, feature] > value)[0]=', nonzero(dataSet[:, feature] > value)[0]
# print 'nonzero(dataSet[:, feature] <= value)[0]=', nonzero(dataSet[:, feature] <= value)[0]
# dataSet[:, feature] 取去每一行中,第1列的值(从0开始算)
# nonzero(dataSet[:, feature] > value) 返回结果为true行的index下标
mat0 = dataSet[nonzero(dataSet[:, feature] <= value)[0], :]
mat1 = dataSet[nonzero(dataSet[:, feature] > value)[0], :]
return mat0, mat1
# 返回每一个叶子结点的均值
# returns the value used for each leaf
# 我的理解是: regLeaf 是产生叶节点的函数,就是求均值,即用聚类中心点来代表这类数据
def regLeaf(dataSet):
return mean(dataSet[:, -1])
# 计算总方差=方差*样本数
# 我的理解是: 求这组数据的方差,即通过决策树划分,可以让靠近的数据分到同一类中去
def regErr(dataSet):
# shape(dataSet)[0] 表示行数
return var(dataSet[:, -1]) * shape(dataSet)[0]
# 1.用最佳方式切分数据集
# 2.生成相应的叶节点
def chooseBestSplit(dataSet,leafType=regLeaf,errType=regErr,ops=(1,4)):
'''
chooseBestSplit(用最佳方式切分数据集 和 生成相应的叶节点)
Args:
dataSet 加载的原始数据集
leafType 建立叶子点的函数
errType 误差计算函数(求总方差)
ops [容许误差下降值,切分的最少样本数]。
Returns:
bestIndex feature的index坐标
bestValue 切分的最优值
Raises:
'''
#ops=(1,4),非常重要,因为它决定了决策树划分停止的threshold值,被称为预剪枝(prepruning),其实也就是用于控制函数的停止时机。
# 之所以这样说,是因为它防止决策树的过拟合,所以当误差的下降值小于tolS,或划分后的集合size小于tolN时,选择停止继续划分。
# 最小误差下降值,划分后的误差减小小于这个差值,就不用继续划分
tolS=ops[0]
# 划分最小 size 小于,就不继续划分了
tolN=ops[1]
# 如果结果集(最后一列为1个变量),就返回退出
# .T 对数据集进行转置
# .tolist()[0] 转化为数组并取第0列
if len(set(dataSet[:,-1].T.tolist()[0]))==1:# 如果集合size为1,也就是说全部的数据都是同一个类别,不用继续划分。
# exit cond 1
return None,leafType(dataSet)
# 计算行列值
m,n=shape(dataSet)
# 无分类误差的总方差和
# the choice of the best feature is driven by Reduction in RSS error from mean
S=errType(dataSet)
# inf 正无穷大
bestS, bestIndex, bestValue = inf, 0, 0
# 循环处理每一列对应的feature值
for featIndex in range(n-1): # 对于每个特征
# [0]表示这一列的[所有行],不要[0]就是一个array[[所有行]],下面的一行表示的是将某一列全部的数据转换为行,然后设置为list形式
for splitVal in set(dataSet[:,featIndex].T.tolist()[0]):
# 对该列进行分组,然后组内成员的val值进行二元切分
mat0,mat1=binSplitDataSet(dataSet,featIndex,splitVal)
# 判断二元切分的方式的元素数量是否符合预期
if(shape(mat0)[0]", yHat[i, 0]
return yHat
# 测试数据集
testMat=mat(eye(4))
print(testMat)
print(type(testMat))
mat0,mat1=binSplitDataSet(testMat,1,0.5)
print(mat0,'\n-----------\n', mat1)
# 回归树
myDat=loadDataSet('data/9.RegTrees/data1.txt')
myMat=mat(myDat)
myTree=createTree(myMat)
print (myTree)
#1.预剪枝就是: 提起设置最大误差数和最少元素数
myDat = loadDataSet('data/9.RegTrees/data3.txt')
myMat=mat(myDat)
myTree=createTree(myMat,ops=(0,1))
print (myTree)
# 2. 后剪枝就是: 通过测试数据,对预测模型进行合并判断
myDatTest = loadDataSet('data/9.RegTrees/data3test.txt')
myMat2Test = mat(myDatTest)
myFinalTree = prune(myTree, myMat2Test)
print ('\n\n\n-------------------')
print (myFinalTree)
# 模型树求解
myDat = loadDataSet('data/9.RegTrees/data4.txt')
myMat = mat(myDat)
myTree = createTree(myMat, modelLeaf, modelErr)
print (myTree)
# 回归树 VS 模型树 VS 线性回归
trainMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_train.txt'))
testMat = mat(loadDataSet('data/9.RegTrees/bikeSpeedVsIq_test.txt'))
# 回归树
myTree1 = createTree(trainMat, ops=(1, 20))
print (myTree1)
yHat1 = createForeCast(myTree1, testMat[:, 0])
print ("--------------\n")
print (yHat1)
print ("ssss==>", testMat[:, 1])
# corrcoef 返回皮尔森乘积矩相关系数
# 描述XY相关程度
print("regTree:",corrcoef(yHat1,testMat[:,1],rowvar=0)[0,1])
# 模型树
myTree2 = createTree(trainMat, modelLeaf, modelErr, ops=(1, 20))
yHat2 = createForeCast(myTree2, testMat[:, 0], modelTreeEval)
print( myTree2)
print ("modelTree:", corrcoef(yHat2, testMat[:, 1],rowvar=0)[0, 1])
# 线性回归
ws, X, Y = linearSolve(trainMat)
print (ws)
m = len(testMat[:, 0])
yHat3 = mat(zeros((m, 1)))
for i in range(shape(testMat)[0]):
yHat3[i]=testMat[i,0]*ws[1,0]+ws[0,0]
print ("lr:", corrcoef(yHat3, testMat[:, 1],rowvar=0)[0, 1])
使用GUI
from __future__ import print_function
from Tkinter import *
from numpy import *
import matplotlib
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
matplotlib.use('TkAgg')
def test_widget_text(root):
mylabel = Label(root, text="helloworld")
# 相当于告诉 布局管理器(Geometry Manager),如果不设定位置,默认在 0行0列的位置
mylabel.grid()
# 最大为误差, 最大子叶节点的数量
def reDraw(tolS, tolN):
# clear the figure
reDraw.f.clf()
reDraw.a = reDraw.f.add_subplot(111)
# 检查复选框是否选中
if chkBtnVar.get():
if tolN < 2:
tolN = 2
myTree = createTree(reDraw.rawDat, regTrees.modelLeaf, regTrees.modelErr, (tolS, tolN))
yHat = createForeCast(myTree, reDraw.testDat, regTrees.modelTreeEval)
else:
myTree = createTree(reDraw.rawDat, ops=(tolS, tolN))
yHat = createForeCast(myTree, reDraw.testDat)
# use scatter for data set
reDraw.a.scatter(reDraw.rawDat[:, 0].A, reDraw.rawDat[:, 1].A, s=5)
# use plot for yHat
reDraw.a.plot(reDraw.testDat, yHat, linewidth=2.0, c='red')
reDraw.canvas.draw()
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
# 画新的tree
def drawNewTree():
# #get values from Entry boxes
tolN, tolS = getInputs()
reDraw(tolS, tolN)
def main(root):
# 标题
Label(root, text="Plot Place Holder").grid(row=0, columnspan=3)
# 输入栏1, 叶子的数量
Label(root, text="tolN").grid(row=1, column=0)
global tolNentry
tolNentry = Entry(root)
tolNentry.grid(row=1, column=1)
tolNentry.insert(0, '10')
# 输入栏2, 误差量
Label(root, text="tolS").grid(row=2, column=0)
global tolSentry
tolSentry = Entry(root)
tolSentry.grid(row=2, column=1)
# 设置输出值
tolSentry.insert(0,'1.0')
# 设置提交的按钮
Button(root, text="确定", command=drawNewTree).grid(row=1, column=2, rowspan=3)
# 设置复选按钮
global chkBtnVar
chkBtnVar = IntVar()
chkBtn = Checkbutton(root, text="Model Tree", variable = chkBtnVar)
chkBtn.grid(row=3, column=0, columnspan=2)
# 退出按钮
Button(root, text="退出", fg="black", command=quit).grid(row=1, column=2)
# 创建一个画板 canvas
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)
reDraw.rawDat = mat(loadDataSet('data/9.RegTrees/sine.txt'))
reDraw.testDat = arange(min(reDraw.rawDat[:, 0]), max(reDraw.rawDat[:, 0]), 0.01)
reDraw(1.0, 10)
# 创建一个事件
root = Tk()
# test_widget_text(root)
main(root)
# 启动事件循环
root.mainloop()