这几天完成了树回归的相关学习,这一部分内容挺多,收获也挺多,刚刚终于完成了全部内容,非常开心。
树回归这一章涉及了CART,CART树称作(classify and regression tree) 分类与回归树,既可以用于分类,也可以用于回归。这正是前面决策树没有说到的内容,在这里补充一下。正好也总结一下我们学的3种决策树。
ID3:用信息增益来选择特性进行分类,只能处理分类问题。缺点是往往偏向于特性种类多的特性进行分解,比如特性A有2种选择,特性B有3种选择,混乱度差不多的情况下,ID3会偏向特性B进行选择,这样会使得树变得复杂,增加了建树难度,节点的增加也使得过拟合情况的出现。因为这个缺点,我们发明了ID3的优化版C4.5。
C4.5:用信息增益率来选择特性进行分类,信息增益和特性种类的个数多少正好抵消,信息增益率可以客观的表示选择某一特性标签的混乱程度。
CART:用基尼指数(类似于熵的概念)来选择特性进行分类。用总方差进行回归预测。上述两种算法分割特性是一旦选择了该特性,该特性便不再起作用,由于该分割过于‘迅速’,不能更好的利用特性的价值,CART算法横空出世。
在以上的ID3和C4.5,只能处理分类问题,在遇到标签为连续型数值便无法处理,所以CART的优势便体现出来,CART可以进行回归分析,预测回归值,称作树回归,CART可以进行剪枝操作避免过拟合,分为预剪枝和后剪枝,后剪枝后面会有,预剪枝为修改函数的参数ops。也可以把叶节点的回归数值改成线性回归,称作模型树。回归效果略优于简单的线性回归(最后的三种回归结果我们可以看到)。
step1:
读取数据:
#读取数据 def loadDataSet(filename): feanum = len(open(filename).readline().strip().split('\t')) datamat = [] f = open(filename) for i in f.readlines(): line = i.strip().split('\t') l = [] for j in range(feanum): l.append(float(line[j])) datamat.append(l) return datamat
step2:
递归创建回归树:
#创建树(ops两个参数第一个表示最小允许误差,第二个为样本最小容量) def createTree(dataset, leaftype = regLeaf, errortype = regError, ops = [1, 4]): tree = {} fea, val = chooseBestFeatrue(dataset, leaftype, errortype, ops) if fea == None: return val tree['fea'] = fea tree['val'] = val lefttree, righttree = splitDataSet(dataset, fea, val) tree['left'] = createTree(lefttree, leaftype, errortype, ops) tree['right'] = createTree(righttree, leaftype, errortype, ops) return tree
step3:
回归树中的分割子集函数:
#分类函数 def splitDataSet(Data, featrue, val): mat0 = [] mat1 = [] data = mat(Data) m = shape(data)[0] for i in range(m): if data[i, featrue] > val: mat0.append(data[i].tolist()[0]) else: mat1.append(data[i].tolist()[0]) return mat0, mat1
step4:
回归树中的查找最佳特性函数:
停止条件:
1:分类的子集数量小于4个
2:误差值降低量小于最小允许值
3:标签值都为重复值,无需再次分解,返回为叶子节点。
#寻找最佳分类特性 def chooseBestFeatrue(dataset, leaftype = regLeaf, errortype = regError, ops = [1, 4]): d = mat(dataset) leastnum = ops[1] leasterr = ops[0] m, n = shape(d) if len(set(d[ : , -1].T.tolist()[0])) == 1: return None, leaftype(d) errorsum = errortype(d) bestfea = -1 bestval = -1 besterror = inf for i in range(n - 1): for temp in set(d[ : , i].T.tolist()[0]): mat0, mat1 = splitDataSet(d, i, temp) if shape(mat0)[0] < leastnum or shape(mat1)[0] < leastnum: continue tempsumerror = errortype(mat0) + errortype(mat1) if tempsumerror < besterror: bestfea = i bestval = temp besterror = tempsumerror if (errorsum - besterror) < leasterr: print errorsum, besterror return None, leaftype(d) mat0, mat1 = splitDataSet(d, bestfea, bestval) if shape(mat0)[0] < leastnum or shape(mat1)[0] < leastnum: return None, leaftype(d) return bestfea, bestval
step5:
相关函数:
#计算平方误差 def regError(dataset): t = mat(dataset) return var(t[ : , -1]) * shape(t)[0] #计算叶子节点的值(叶子节点的平均值) def regLeaf(dataset): t = mat(dataset) return mean(t[ : , -1])
#判断节点是否为树 def isTree(tree): return isinstance(tree, dict) #节点值的平均值 def getMean(tree): if isTree(tree['left']): tree['left'] = getMean(tree['left']) if isTree(tree['right']): tree['right'] = getMean(tree['right']) return (tree['right'] + tree['left']) / 2.0
我们看一下效果:
图1:
结果为:
图2:
结果为:
我们看到符合我们的预期。
step6:
回归树的剪枝:这里的剪枝和算法中的剪枝不同,算法中的剪枝为剪去不必要的节点,这里的剪枝为把繁琐的节点合并。目的位避免CART过拟合。分为预剪枝和后剪枝,
此为后剪枝,预剪枝为修改函数的参数ops。
这里有两个细节:
1:当没有数据进入树节点的时候,我们把这个树合并为一个节点。
2:当该节点的左节点和右节点都为值时,我们判断是分开好还是合并好。
为取得更好的剪枝效果,应该预剪枝和后剪枝一起用。
#回归树剪枝 def pruneTree(tree, testdata): if shape(testdata)[0] == 0: return getMean(tree) if isTree(tree['left']) or isTree(tree['right']): lset, rset = splitDataSet(testdata, tree['fea'], tree['val']) if isTree(tree['left']): tree['left'] = pruneTree(tree['left'], lset) if isTree(tree['right']): tree['right'] = pruneTree(tree['right'], rset) if (not isTree(tree['left'])) and (not isTree(tree['right'])): l, r = splitDataSet(testdata, tree['fea'], tree['val']) lset = mat(l) rset = mat(r) e1 = 0.0 e2 = 0.0 #书中忽略了这一点可能会有一个集合没有元素 if shape(lset)[1] != 0: e1 = sum(power(lset[ : , -1] - tree['left'], 2)) if shape(rset)[1] != 0: e2 = sum(power(rset[ : , -1] - tree['right'], 2)) errornomerge = e1 + e2 average = (tree['left'] + tree['right']) / 2.0 test = mat(testdata) errormerge = sum(power(test[ : , -1] - average, 2)) if errormerge < errornomerge: print 'merging' return average else: return tree else: return tree
step7:
模型树:
修改了回归树中的参数函数和加入了叶节点的线性回归函数。
#模型树线性回归函数(简单的线性回归函数,矩阵逆不存在时出错) def lineSolve(data): linedata = mat(data) m, n = shape(linedata) x = mat(ones((m, n))) x[ : , 1 : n] = linedata[ : , 0 : n - 1] y = linedata[ : , -1] xtemp = x.T * x if linalg.det(xtemp) == 0.0: print 'error,没有逆矩阵' return w = xtemp.I * x.T * y return x, y, w #模型树的误差 def modError(dataset): x, y, w = lineSolve(dataset) yhat = x * w return sum(power(yhat - y, 2)) #模型树的叶子节点 def modLeaf(dataset): x, y, w = lineSolve(dataset) return w
图为:
结果为:
叶子节点的值为w,x * w得到y的预测值。
step8:
树回归的预测函数:
#回归值(树回归) def regValue(value, dataset): return value #回归值(模型树回归) def modValue(value, dataset): data = mat(dataset) n = shape(data)[1] x = mat(ones((1, n + 1))) x[ : , 1 : n + 1] = data yhat = x * value return yhat #树回归预测 def predictTree(tree, t, valuetype = regValue): testdata = mat(t) m, n = shape(testdata) yhat = mat(zeros((m, 1))) for i in range(m): yhat[i, 0] = predictValue(tree, testdata[i], valuetype) return yhat #回归预测值 def predictValue(tree, test, valuetype = regValue): if not isTree(tree): return valuetype(tree, test) if test[tree['fea']] > tree['val']: return predictValue(tree['left'], test, valuetype) else: return predictValue(tree['right'], test, valuetype)
接下来我们进行3种回归的比较:
图为:
相关系数越接近1,相关性越大。
1:CART树回归:
相关系数为:
2:模型树:
相关系数为:
3:线性回归
相关系数为:
可见大小顺序为 模型树 > 回归树 > 线性回归
终于完成了监督学习的相关学习部分,SVM还不懂,有机会学习一下SVM的应用,明天开始学习无监督学习,先学习聚类算法。加油!