机器学习(四)CART回归树(基础篇)
相关的决策树文章:
回归CART(Classification And Regression Trees)
之前的文章,我们学习了ID3决策树,和C4.5决策树以及CART分类树进行分类。决策树不断将数据切分成小数据,直到所有目标标量完全相同,或者数据不能再切分为止。决策树是一种贪心算法,它要在一定情况下做出最佳的选择。
1. ID3算法的弊端
回忆一下,决策树的树构建算法是ID3和C4.5。ID3和C4.5的做法是每次选取当前最佳的特征来分割数据,并按照该特征的所有可能取值来切分。也就是说,如果一个特征有4种取值,那么数据将被切分成4份。一旦按某特征切分后,该特征在之后的算法执行过程中将不会再起作用,所以有观点认为这种切分方式过于迅速。
除了切分过于迅速外,ID3算法还存在另一个问题,它不能直接处理连续型特征。只有事先将连续型特征离散化,才能在ID3算法中使用。但这种转换过程会破坏连续型变量的内在特性。
2. CART算法
与ID3算法相反,CART算法正好适用于连续型特征。CART算法使用二元切分法来处理连续型变量。而使用二元切分法则易于对树构建过程进行调整以处理连续型特征。具体的处理方法是:如果特征值大于给定值就走左子树,否则就走右子树。
CART算法有两步:
- 决策树生成:递归地构建二叉决策树的过程,基于训练数据集生成决策树,生成的决策树要尽量大;自上而下从根开始建立节点,在每个节点处要选择一个最好的属性来分裂,使得子节点中的训练集尽量的纯。不同的算法使用不同的指标来定义"最好":
- 决策树剪枝:用验证数据集对已生成的树进行剪枝并选择最优子树,这时损失函数最小作为剪枝的标准。
我们先看看决策树的生成:
在决策树的文章中,我们先根据信息熵的计算找到最佳特征切分数据集构建决策树。CART算法的决策树生成也是如此,实现过程如下:
- 使用CART算法选择特征
- 根据特征切分数据集合
- 构造树
函数一:根据特征切分数据集合
CART算法这里涉及到算法,实现起来复杂些,我们先挑个简单的,即根据特征切分数据集合。编写代码如下:
import numpy as np
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据特征切分数据
:param dataSet: 数据集
:param feature: 切分的特征
:param value: 该特征的值
:return:
"""
mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
return mat0, mat1
if __name__ == '__main__':
testMat = np.mat(np.eye(4))
mat0, mat1 = binSplitDataSet(testMat, 1, 0.5)
print('原始集合:\n', testMat)
print('mat0:\n', mat0)
print('mat1:\n', mat1)
我们看看结果:
在这里我们先创建了一个单位矩阵,然后根据切分的规则,对数据矩阵进行切分,可以看到函数根据特定的数据规则,对数据矩阵进行了切分。
从结果我们可以看出来,我们已经可以根据特征和特征值对数据进行切分了,mat0存放的大于指定特征值的矩阵,mat1存放的是小于指定特定值的矩阵。
CART算法
假设X与Y分别为输入和输出变量,并且Y是连续变量,给定训练集:
D
=
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
.
.
.
.
,
(
x
n
,
y
n
)
D={(x_1,y_1),(x_2,y_2),....,(x_n,y_n)}
D=(x1,y1),(x2,y2),....,(xn,yn)
其中,D表示整个数据集合,n为特征数
一个回归树对应着输入空间(即特征空间)的一个划分以及划分的单元上的输出值。假设已将输入空间划分为M个单元R1,R2,…Rm,并且在每一个单元Rm上有一个固定的输出值Cm,于是回归模型可表示为:
这样就可以计算模型输出值与实际值的误差:
我们希望每个单元上的Cm,可以的是这个误差平方误差最小化,当Cm为相应单元的所有实际值的均值时,可以得到最优
那么如何生成这些单元划分?
假设,我们选择变量 xj 为切分变量,它的取值 s 为切分点,那么就会得到两个区域:
当j和s固定时,我们要找到两个区域的代表值c1,c2使各自区间上的平方差最小:
前面几经直到C1, C2为区间上的平均:
那么对固定的 j 只需要找到最优的s,然后通过遍历所有的变量,我们可以找到最优的j,这样我们就可以得到最优对(j,s),并得到两个区间。
这样的回归树通常称为最小二乘回归树(least squares regression tree)。
上述过程表示的算法步骤为:
输入:训练数据集D;
输出:回归树
f
(
x
)
f(x)
f(x)
在训练数据即所在的输入空间中,递归地将每一个区域划分为两个子区域并决定每个子区域上地输出值,构建决策树
(1)选择罪域切分变量j与切分点s,求解
遍历变量j,对固定地切变量j扫描切分点s,选择使式达到最小值地对(j,s)
(2)用选定的对(j,s)划分区域并决定相应的输出值:
(3)继续对两个子区域调用步骤(1)(2),直至满足停止条件
(4)将输入空间划分为M个区域
R
1
,
R
2
,
.
.
.
R
M
,
R_1,R_2,...R_M,
R1,R2,...RM,生成决策树
除此之外,我们再定义两个参数,tolS和tolN,分别用于控制误差变化限制和切分特征最少样本数。这两个参数的意义是什么呢?就是防止过拟合,提前设置终止条件,实际上是在进行一种所谓的预剪枝(prepruning)操作,在下一小节会进行进一步讲解。
测试集下载:数据集下载
将ex2.txt文件保存在同一目录下。接下来我们看一下数据分布情况:
import matplotlib.pyplot as plt
import numpy as np
def loadDataSet(fileName):
"""
函数说明:加载数据
Parameters:
fileName - 文件名
Returns:
dataMat - 数据矩阵
Website:
https://www.cuijiahua.com/
Modify:
2017-12-09
"""
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float, curLine)) #转化为float类型
dataMat.append(fltLine)
return dataMat
def plotDataSet(filename):
"""
函数说明:绘制数据集
Parameters:
filename - 文件名
Returns:
无
Website:
https://www.cuijiahua.com/
Modify:
2017-11-12
"""
dataMat = loadDataSet(filename) #加载数据集
n = len(dataMat) #数据个数
xcord = []; ycord = [] #样本点
for i in range(n):
xcord.append(dataMat[i][0]); ycord.append(dataMat[i][1]) #样本点
fig = plt.figure()
ax = fig.add_subplot(111) #添加subplot
ax.scatter(xcord, ycord, s = 20, c = 'blue',alpha = .5) #绘制样本点
plt.title('DataSet') #绘制title
plt.xlabel('X')
plt.show()
if __name__ == '__main__':
filename = 'ex2.txt'
plotDataSet(filename)
运行结果如下:
我们用这个数据集测试我们的CART算法。
编写代码如下:
import numpy as np
import matplotlib.pyplot as plt
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据特征切分数据
:param dataSet: 数据集
:param feature: 切分的特征
:param value: 该特征的值
:return:
"""
mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
return mat0, mat1
def loadDataSet(fileName):
"""
函数说明:加载数据
Parameters:
fileName - 文件名
Returns:
dataMat - 数据矩阵
Website:
https://www.cuijiahua.com/
Modify:
2017-12-09
"""
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float, curLine)) # 转化为float类型
dataMat.append(fltLine)
return dataMat
def regLeaf(dataSet):
"""
函数说明:生成叶节点
:param dataSet: 数据集合
:return:
"""
return np.mean(dataSet[:, -1])
def regErr(dataSet):
"""
函数说明:误差估计函数
:param dataSet:
:return:
"""
return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType = regLeaf, errType = regErr, ops=(1,4)):
"""
函数说明:找到数据的最佳二元切分方式函数
:param dataSet:数据集合
:param leafType:生成叶节点
:param errType:误差估计函数
:param ops:用户定义的参数构成元组
:return:
"""
import types
#tolS允许的误差下降值,tolN切分的最少样本数
tolS = ops[0]; tolN = ops[1]
#如果当前所有值相等,则退出。(根据set的特性)
if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
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
if __name__ == '__main__':
myDat = loadDataSet('ex2.txt')
myMat = np.mat(myDat)
feat, val = chooseBestSplit(myMat, regLeaf, regErr, (1, 4))
print(feat)
print(val)
输出结果为:
可以看到,切分的最佳特征为第1列特征,最佳切分特征值为0.48813,这个特征值怎么选出来的?就是根据误差估计的大小,我们选择的这个特征值可以使误差最小化。
切分的特征和特征值我们已经选择好了,接下来就是利用选出的这两个变量创建回归树了。
创建方法很简单,我们根据切分的特征和特征值切分出两个数据集,然后将两个数据集分别用于左子树的构建和右子树的构建,直到无法找到切分的特征为止。因此,我们可以使用递归实现这个过程,编写代码如下:
import numpy as np
import matplotlib.pyplot as plt
def binSplitDataSet(dataSet, feature, value):
"""
函数说明:根据特征切分数据
:param dataSet: 数据集
:param feature: 切分的特征
:param value: 该特征的值
:return:
"""
mat0 = dataSet[np.nonzero(dataSet[:, feature] > value)[0], :]
mat1 = dataSet[np.nonzero(dataSet[:, feature] <= value)[0], :]
return mat0, mat1
def loadDataSet(fileName):
"""
函数说明:加载数据
Parameters:
fileName - 文件名
Returns:
dataMat - 数据矩阵
Website:
https://www.cuijiahua.com/
Modify:
2017-12-09
"""
dataMat = []
fr = open(fileName)
for line in fr.readlines():
curLine = line.strip().split('\t')
fltLine = list(map(float, curLine)) # 转化为float类型
dataMat.append(fltLine)
return dataMat
def regLeaf(dataSet):
"""
函数说明:生成叶节点
:param dataSet: 数据集合
:return:
"""
return np.mean(dataSet[:, -1])
def regErr(dataSet):
"""
函数说明:误差估计函数
:param dataSet:
:return:
"""
return np.var(dataSet[:, -1]) * np.shape(dataSet)[0]
def chooseBestSplit(dataSet, leafType = regLeaf, errType = regErr, ops=(1,4)):
"""
函数说明:找到数据的最佳二元切分方式函数
:param dataSet:数据集合
:param leafType:生成叶节点
:param errType:误差估计函数
:param ops:用户定义的参数构成元组
:return:
"""
import types
#tolS允许的误差下降值,tolN切分的最少样本数
tolS = ops[0]; tolN = ops[1]
#如果当前所有值相等,则退出。(根据set的特性)
if len(set(dataSet[:, -1].T.tolist()[0])) == 1:
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)):
"""
函数说明:树构建函数
Parameters:
dataSet - 数据集合
leafType - 建立叶结点的函数
errType - 误差计算函数
ops - 包含树构建所有其他参数的元组
Returns:
retTree - 构建的回归树
Website:
https://www.cuijiahua.com/
Modify:
2017-12-12
"""
# 选择最佳切分特征和特征值
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
if __name__ == '__main__':
myDat = loadDataSet('ex2.txt')
myMat = np.mat(myDat)
print(createTree(myMat))
运行结果如下图所示:
写到这里就结束了。