机器学习算法——手动搭建决策树分类器(代码+作图)

决策树分类器原理(CART)

CART是决策树分类器的一种,它的独特之处在于可以处理分类问题也可以处理回归问题,具体处理哪类问题是根据关注的变量是类别型还是连续型而定的。
选择分裂属性的标准:
CART和ID3和C4.5在评价标准的选取上存在着差别。ID3使用的纯度衡量指标是信息增益,C4.5使用的是信息增益比,而CART使用的是Gini(分类)和均方差(回归)。对于回归问题,传统的信息熵标度确实不是一个很好的评价标准(相比于最小均方差),但是在分类问题中,为什么要用Gini系数代替信息增益比呢?这样做实际上是为了减少运算的复杂度,熵模型是对数运算而Gini系数是二次运算,同时基尼系数和熵之半的曲线是非常近似的,所以可以作为一个运算代价更小的替代指标。
离散属性的分裂
因为CART生成的决策树是一个二叉树,所以它会把该属性下的所有离散值分为两组,在所有可能的分组方案中找到Gini系数最小的那一个。
连续属性的分裂
CART对于连续型属性的处理方式和C4.5差不多,它是通过将所有出现在数据集中的值都作为一个类别,然后根据大小进行分割。最后,根据均方差最小原则来选取最优的分裂位置。
剪枝的标准
CART采用CCP(代价复杂度)剪枝方法。代价复杂度选择节点表面误差率增益值最小的非叶子节点,删除该非叶子节点的左右子节点,若有多个非叶子节点的表面误差率增益值相同小,则选择非叶子节点中子节点数最多的非叶子节点进行剪枝。
算法流程图
在这里插入图片描述

分类器实现

def pre_splite(index, value, dataset):
    """
    将一个数据集根据某一个属性的指定值进行划分
    @index: 指定属性
    @value: 指定划分的阈值
    @dataset: 数据集
    """
    left, right = [], []
    for row in dataset:
        if row[index] < value:
            left.append(row)
        else:
            right.append(row)
    return left, right

def gini_index(groups, classes):
    """
    计算被划分的子集的基尼系数
    @groups: 划分后的数据集
    @classes: 类别集合
    """
    # 计算全集的大小
    n_instances = float(sum([len(group) for group in groups]))
    # 求每一个子集的加权基尼系数
    gini = 0.0
    for group in groups:
        size = float(len(group))
        # 注意:分母不能为0
        if size == 0:
            continue
        score = 0.0
        for val in classes:
            p = [row[-1] for row in group].count(val) / size
            score += p * p
        gini += (1.0 - score) * (size / n_instances)
    return gini

def get_split(dataset):
    """
    找到最好的分割点
    @dataset: 数据集
    """
    class_values = list(set(row[-1] for row in dataset))
    b_index, b_value, b_score, b_groups = float('inf'), float('inf'), float('inf'), None
    for index in range(len(dataset[0])-1):
        for row in dataset:
            groups = pre_splite(index, row[index], dataset)
            gini = gini_index(groups, class_values)
            if gini < b_score:
                b_index, b_value, b_score, b_groups = index, row[index], gini, groups
    return {'index': b_index, 'value': b_value, 'groups': b_groups}   # 以dict形式返回结果

def to_terminal(group):
    """
    创造一个叶子节点输出值
    @groups: 组
    """
#     print(group)
    outcomes = [row[-1] for row in group]
    return max(set(outcomes), key=outcomes.count)

def split(node, max_depth, min_size, depth):
    """
    树的分叉
    @node: 当前所在的节点
    @max_depth: 最大深度
    @min_size: 停止拆分的数目(终止条件)
    @depth: 深度
    """
    left, right = node['groups']
#     print(left, right)
    del(node['groups'])     # 删掉
    if not left or not right:
        # 已经到达叶子
        node['left'] = node['right'] = to_terminal(left+right)
        return
    if depth >= max_depth:
        node['left'], node['right'] = to_terminal(left), to_terminal(right)
        return
    if len(left) <= min_size:
        node['left'] = to_terminal(left)
    else:
        node['left'] = get_split(left)
        split(node['left'], max_depth, min_size, depth+1)
    if len(right) <= min_size:
        node['right'] = to_terminal(right)
    else:
        node['right'] = get_split(right)
        split(node['right'], max_depth, min_size, depth+1)    # 递归调用,深度优先

def build_tree(train, max_depth, min_size):
    """
    创建决策树
    @train: 训练集
    @max_depth: 最大深度
    @min_size: 停止拆分的数目(终止条件)
    """
    train = list(train.reshape((-1,5)))
    root = get_split(train)
    split(root, max_depth, min_size, 1)  # 激活递归
    return root

def tree_prediction(node, row):
    """
    使用决策树来进行预测
    @node: 一个新的数据点
    @row: 当前所在层
    """
    if row[node['index']] < node['value']:
        if isinstance(node['left'], dict):
            # 如果左子树尚未到达叶子则继续递归
            return tree_prediction(node['left'], row)
        else:
            return node['left']
    else:
        if isinstance(node['right'], dict):
            return tree_prediction(node['right'], row)
        else:
            return node['right']

def decision_tree(train, test, max_depth, min_size):
    """
    决策树算法
    @train: 训练集
    @test: 测试集
    @max_depth: 最大深度
    @min_size: 终止条件
    """
    tree = build_tree(train, max_depth, min_size)
    predictions = []
    for row in test:
        prediction = tree_prediction(tree, row)
        predictions.append(prediction)
    return(predictions)

def print_tree(node, depth=0):
    """
    暴力打印结果
    @node: 节点
    @depth: 当前深度
    """
    if isinstance(node, dict):
        print('%s[X%d < %.3f]' % ((depth*' ', (node['index']+1), node['value'])))
        print_tree(node['left'], depth+1)
        print_tree(node['right'], depth+1)
    else:
        print('%s[%s]' % ((depth*' ', node)))
        

使用鸢尾花数据集检验

seed(1)
filename = 'iris.csv'
dataset = pd.read_csv(filename).values
str_column_to_int(dataset, len(dataset[0])-1)
max_depth = 10
min_size = 1
n_folds = 3
scores = evaluate_algorithm(dataset, decision_tree, n_folds, max_depth, min_size)
print('某个折上的准确率: %s' % scores)
print('算法的平均准确率: %.3f%%' % (sum(scores)/float(len(scores))))

结果:

{'Iris-versicolor': 0, 'Iris-virginica': 1, 'Iris-setosa': 2}
某个折上的准确率: [92.0, 98.0, 96.0]
算法的平均准确率: 95.333%

因为交叉检验了三次,所以有三棵决策树生成。
使用sklearn进行可视化(自己造图实在是太困难了):

from sklearn import tree
from sklearn.datasets import load_iris
import pydotplus
from IPython.display import Image, display
# 载入数据
iris = load_iris()
# 构建决策树
clf = tree.DecisionTreeClassifier(max_depth=5)# 深度为4层时有1个结果仍然模糊,5层就足够
clf = clf.fit(iris.data, iris.target)
# 数据可视化
dot_data = tree.export_graphviz(clf,
                                out_file = None,
                                feature_names = iris.feature_names,
                                class_names = iris.target_names,
                                filled=True,
                                rounded=True
                               )
graph = pydotplus.graph_from_dot_data(dot_data)
display(Image(graph.create_png()))

在这里插入图片描述

fig = make_subplots(rows=1, cols=2, subplot_titles=("Change folds", "Change minimum size"))
scores, index, acc = [], [], []
max_depth = 10
min_size = 1
for i in range(2, 22):
    score = evaluate_algorithm(dataset, decision_tree, i, max_depth, min_size)
    scores.append(list(score))
    acc.append(sum(score)/float(len(score)))
    index.append([i for j in range(i)])
fig.append_trace(go.Scatter(x=[i + 2 for i in range(20)], y=acc,
                    mode='lines+markers',
                    name='mean'), row=1, col=1)
fig.append_trace(go.Scatter(x=sum(index, []), y=sum(scores, []),
                    mode='markers',
                    name='each'), row=1, col=1)
scores, index, acc = [], [], []
for j in range(1, 11):
    score = evaluate_algorithm(dataset, decision_tree, 5, max_depth, j)
    scores.append(list(score))
    acc.append(sum(score)/float(len(score)))
    index.append(j)
fig.append_trace(go.Scatter(x=[i + 1 for i in range(10)], y=acc,
                    mode='lines+markers',
                    name='mean-acc'), row=1, col=2)
fig.update_layout(height=600, width=1200, template='none')
fig.update_yaxes(title_text="Accuracy", row=1, col=1)
fig.update_yaxes(title_text="Accuracy", row=1, col=2)
fig.update_xaxes(title_text="Folds Num", row=1, col=1)
fig.update_xaxes(title_text="Minimum Size", row=1, col=2)
fig.show()

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章