怎么分——熵与Gini指数
熵,表示信息量的期望,含义是混乱程度,也是对随机变量编码所需的最小比特数。请参考之前的文章。
信息增益建立在熵之上,是选择某特征之后熵减少的多少(熵减少即信息增加),等于信息熵-条件熵,
基尼不纯度 ,它表示是分错的概率的期望。Gini不纯度其实可以看作是熵的近似值,形式一样没有取对数更容易计算,二分类时,都是概率取0.5时达到最大值。Gini不纯度是一种不等性度量,取值[0,1],当数据完全相等时取0.
K表示类别数目,那么对于数据集(样本集合)D,它的基尼不纯度:
D经过特征A后分裂为两个子集,新的不纯度就是两个子集不纯度的加权和(因为Gini不纯度本来也是加权和):.我们的目标就是使得分裂后的不纯度最小。
ID3
ID3是最早的决策树,使用信息增益作为分裂准则。特点是特征必须是离散的,离散意味着特征是类别特征,那就也意味着特征不会被复用(该特征下的结果只能是属于或者不属于,而像连续特征可以换个阈值重新使用)。而既然是类别特征,当然类别越多分得越细(ID3可能是多叉树),所以ID3会倾向于选择类别较多的特征(直接念身份证号得了)。但其实不是说特征类别更多带来的增益就一定更大,从熵的公式看,他们极有可能是一样的,,只是可能性更大,即便带来的增益相等,类别过多将原有数据集划分为更多更小的子集,极容易过拟合。
此外ID3没有考虑缺失特征带来的影响。
ID3的作者Quinlan继续提出了C4.5。特征类别过多时这个特征自身的熵会升高(称为属性熵split info),所以可以将信息增益调整为信息增益率=信息增益/属性熵,这样就限制了属性熵的值不能过高。其实信息增益率可以看作是信息增益与代价的比(属性熵越大代价越大),核心目的仍然是为了让信息增益最大(在特征为连续值时,只使用信息增益)。信息增益率又有可能偏向于类别较少的特征,所以C4.5真正的做法是先选取信息增益高于均值的几种,在这几种里面再按照信息增益率选取。
对于特征值缺失的问题,意思是对于某一类别特征,有一些样本我们不知道它到底属于哪一类。所以在计算增益时就只以不缺失的数据来计算;当正好选取了缺失的特征后,对于不知道它属于哪一类的样本,就按照概率进行分类。
CART
CART树最大特定是既可以用于分类也可以用于回归。首先它是二叉树,递归划分,对于分类可以化为one vs others,对于连续特征,则选定某一特征的一个值,按大小分为两类。CART使用Gini不纯度代替熵,避免了对数运算。
怎么剪——剪枝
决策树在训练过程中采取贪婪策略的方法进行分裂,很容易过拟合。控制深度,叶子节点数的方法又有点武断(这种方法其实叫做预剪枝)。
关于后剪枝方法,C4.5使用悲观剪枝PEP(Pessimostic Error Pruning),CART树中可以使用代价复杂度的方法进行剪枝CCP。
PEP的好处是决策树的生成和剪枝使用相同的训练集。它比较的是待剪去的子树和新的叶子结点处的误判率。前者使用公式:
,表示该子树每个叶子结点的误判个数,L表示有多少个叶子结点,N表示该子树根节点处的总数。
而新叶子节点处的误判率就很好算了,误判数目/总数。新叶子节点误判率总是高于子树误判率,当差距小于一个阈值时就认为可以剪枝。这个阈值通过子树的标准差来确定:正确判断与误判看作二项分布,那么
CCP涉及到代价,那自然是在追求准确度的基础上增加一项正则项,正则项通过叶子节点的数目表示树结构的复杂度。所以代价复杂度函数就是:
= 错误率 + 叶子结点个数
假设在节点处剪枝,以为根节点的子树为,剪枝前后的代价复杂度函数分别为,
解释一下从(1)到(2)的推导。有两点问题首先要清楚,树的误差体现在其叶子节点的加权误差();剪枝的过程本质是用一个节点代替其底部更深的所有节点。所以剪枝前后的结构对应的误差等价于节点处的误差和以为根的树的叶子节点的误差:。而剪枝前后的树的复杂度变化就是叶子节点个数的变化,剪枝之后叶子结点数首先减少了个,但剪枝处的节点成为新的一个叶子节点,所以叶子节点的变化情况是
所以在确定了之后,所需要做的就是尝试删除一个个节点之下的树,寻找使得剪枝前后代价函数变化最小的那个节点。当然,剪枝之后还是要在测试集中测试,选择最优的作为最终的决策树。这也就是所谓的生成子树序列+测试集上的交叉验证。
但是,在更多的资料中,会令(2)等于0,可以得到。这是认为剪枝的充分条件是剪枝前后的代价函数相等,剪枝前后的代价函数可以相等,但是误差肯定是增大的,所以分子大于0,所以>0。有无限多,我们不能一个个试,但是子树的结构是有限的。仔细看,其实可以代表剪枝带来的增益,分子越小,损失变化越小;分母越大,表示叶子节点减小得越多。所以真正的做法是遍历节点剪枝,得到最小的剪枝处,迭代多次直至剪枝之后只剩下根节点,就可以得到子树构成的序列和对应的构成的序列,将这些序列对应的模型再代入测试集验证。
怎么用——sklearn实战
傻瓜式操作,只需要创建一个实例类,然后对训练数据执行fit()操作。
在tree的实例类中可以指定我们分裂所使用的度量方法,可以是entropy,可以是gini(默认)
训练的数据应该是在csv文件中指定特征后的数据
这里的score不是指决策树预测的概率值,因为输入样本落入某节点必须是属于某一类的(硬分类)。注意到这个函数同时需要输入标签值,它返回的其实只是一个值,意为预测的准确率accuracy。
from sklearn import tree
clf = tree.DecisionTreeClassifier(criterion="entropy")# criterion选择entropy,这里表示选择ID3算法
clf = clf.fit(Xtrain, Ytrain)
score = clf.score(Xtest, Ytest) #返回预测的准确度accuracy
可视化
out_file指定生成的dot文件位置。dot文件是微软开发的用于页面布局的文件,可以使用Word打开。打开之后可以发现它保存的就是各个节点的信息:该节点的信息增益,颜色,所属类别等。
import graphviz
dot_data = tree.export_graphviz(clf, out_file=".\Tree.dot"
,feature_names = feature_name
,class_names=["琴酒","雪莉","贝尔摩德"]
,filled=True
,rounded=True
)
汉字支持
dot文件内有一个关键字fontname,它指定了字符的编码格式,所以我们为了支持汉字,核心就是将原有的编码格式Helvetica(在拉丁文中意为“瑞士的”)改为微软雅黑。可以使用代码自动修改。
import re
# 打开 dot_data.dot,修改 fontname="支持的中文字体"
f = open("./Tree.dot", "r+", encoding="utf-8")
open('./Tree_utf8.dot', 'w', encoding="utf-8").write(re.sub(r'fontname=helvetica', 'fontname="Microsoft YaHei"', f.read()))
f.close()
在修改之后可以open,得到一个TextIOWarpper类型的变量,对其取read()得到文件中的字符串,类型为str。然后可以使用 pydotplus.graph_from_dot_data保存:
with open((r'./Tree_utf8.dot',"r",encoding='utf-8') as f:
text = f.read()
graph = pydotplus.graph_from_dot_data(text)
graph.write_png("试试.png")
graph.write_pdf("iris.pdf")
或者使用命令行保存:
dot -Tjpg Tree.dot -o tree.jpg
怎么解读画出的图像呢。
基尼不纯度gini应该是越来越小的。每个圆角矩形代表一个节点,在每个节点中alue的维度和标签的种类数一致,三种类别中符合以上条件的数目。他们的数目之和即这个节点的samples值,而每层的samples之和就是测试集的总数。有的节点的value中,只有一个非零值,代表只有该类符合之上的条件,所以gini不纯度也是0,这时候该节点就成为叶子节点,不会继续向下生长。决策树会倾向于一直生长(除非指定深度),直至使得每个叶子节点的gini等于0,所以容易过拟合,极端情况就是为每个样本量身定制了一份判断条件。所以为了防止过拟合,除了限制生长的深度,还可以指定叶子所需的最小样本数,样本数再小就没必要再生长了。
Reference:
1.sklearn手册https://sklearn.apachecn.org/docs/0.21.3/11.html
2.少年阿斌https://www.cnblogs.com/wqbin/p/11689709.html
3.https://zhuanlan.zhihu.com/p/85731206
4.CCPhttp://mlwiki.org/index.php/Cost-Complexity_Pruning
5.https://zhuanlan.zhihu.com/p/76667156