Skip to content

Latest commit

 

History

History
314 lines (274 loc) · 12.4 KB

2018-12-13-决策树-coding.md

File metadata and controls

314 lines (274 loc) · 12.4 KB
title date categories tags mathjax
决策树-coding
2018-12-13 08:18:27 -0800
Coding
Code
Decision Tree
true

1. 数据集说明

1.1 多分类数据集-酒类

  • 数据来源:sklearn.datasets.load_wine
  • 数据集形状:总计178个样本,每个样本由13个属性表示,以及target表示酒类型0-2,三种类别不是均等的,class_0 (59), class_1 (71), class_2 (48),所有属性值均为number,详情可调用load_wine()['DESCR']了解每个属性的具体含义;
  • 数据集划分:随机选出20%数据作为测试集,不做验证集要求;
  • 性能度量:accuracy或ROC。

1.10. 决策树

  • 对于拥有大量特征的数据决策树会出现过拟合的现象。获得一个合适的样本比例和特征数量十分重要,因为在高维空间中只有少量的样本的树是十分容易过拟合的。
  • 考虑事先进行降维(PCA,ICA,使您的树更好地找到具有分辨性的特征。
  • 通过export功能可以可视化您的决策树。使用 max_depth=3 作为初始树深度,让决策树知道如何适应您的数据,然后再增加树的深度。
  • 请记住,填充树的样本数量会增加树的每个附加级别。使用max_depth来控制输的大小防止过拟合。
  • 通过使用 min_samples_split 和 min_samples_leaf 来控制叶节点上的样本数量。当这个值很小时意味着生成的决策树将会过拟合,然而当这个值很大时将会不利于决策树的对样本的学习。所以尝试 min_samples_leaf=5 作为初始值。如果样本的变化量很大,可以使用浮点数作为这两个参数中的百分比。两者之间的主要区别在于 min_samples_leaf 保证叶结点中最少的采样数,而 min_samples_split 可以创建任意小的叶子,尽管在文献中 min_samples_split 更常见。
  • 在训练之前平衡您的数据集,以防止决策树偏向于主导类.可以通过从每个类中抽取相等数量的样本来进行类平衡,或者优选地通过将每个类的样本权重 (sample_weight) 的和归一化为相同的值。还要注意的是,基于权重的预修剪标准 (min_weight_fraction_leaf) 对于显性类别的偏倚偏小,而不是不了解样本权重的标准,如 min_samples_leaf。
  • 如果样本被加权,则使用基于权重的预修剪标准 min_weight_fraction_leaf 来优化树结构将更容易,这确保叶节点包含样本权重的总和的至少一部分。
  • 所有的决策树内部使用 np.float32 数组 ,如果训练数据不是这种格式,将会复制数据集。
  • 如果输入的矩阵X为稀疏矩阵,建议您在调用fit之前将矩阵X转换为稀疏的csc_matrix ,在调用predict之前将 csr_matrix 稀疏。当特征在大多数样本中具有零值时,与密集矩阵相比,稀疏矩阵输入的训练时间可以快几个数量级。

2. 借助Pandas使用Python实现决策树

#coding=utf-8
# ==============================================================================
"""使用Python实现决策树
版本:
    Python:3.6.7
参考:
    https://github.com/apachecn/AiLearning
"""
#%%
from math import log
import pickle

from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
import pandas as pd

pd.set_option('precision', 4)

def load_dataset():
    """加载数据集以及特征名称
    Returns:
        train_data: 构造决策树的训练集
        test_data: 用于剪枝和测试的测试集
        features: 数据特征名称
    """
    raw_data = load_wine()
    data = raw_data['data']
    target = raw_data['target']

    print(raw_data['DESCR'])
    features = raw_data['feature_names']
    x_train, x_test, y_train, y_test = train_test_split(
        data, target, test_size=0.2, random_state=2018)

    # 使用Pandas便于计算和处理数据
    train_data = pd.DataFrame(x_train, columns=raw_data['feature_names'])
    test_data = pd.DataFrame(x_test, columns=raw_data['feature_names'])
    # 将label列添加进DataFrame中
    train_data['label'] = y_train
    test_data['label'] = y_test
    # 调用describe获取数据信息
    # train_data.describe()
    return train_data, test_data, features

def cal_ent(dataframe):
    """计算以label为目标的信息熵
    Args:
        dataframe: 数据集DataFrame
    Returns:
        ent_data: 信息熵的值
    """
    ent_data = 0.0
    # value_counts返回label列下的值和对应的出现次数,Series类型
    value_counts = dataframe['label'].value_counts()
    # Series可以使用items()遍历
    for _, value in value_counts.items():
        prob = value / len(dataframe)
        ent_data += - prob * log(prob, 2)
    return ent_data

def split_data(dataframe, feature, threshold):
    """根据阈值划分数据集
    对于连续值的特征,采用插队的方式选出threshold,将数据集划分为小于threshold和
    大于threshold的两部分。
    Args:
        dataframe: 数据集DataFrame
        feature: 特征名称str
        threshold: 特征阈值float
    Returns:
        greater_data: 大于阈值的DataFrame
        less_data: 小于阈值的DataFrame
    """
    greater_data = dataframe[dataframe[feature] > threshold].reset_index(drop=True)
    less_data = dataframe[dataframe[feature] < threshold].reset_index(drop=True)
    return greater_data, less_data

def choose_best_feature(dataframe, features):
    """根据ID3算法选择信息增益最大的特征
    对于特征为连续值的情况,每一个特征用过之后可以在后面继续作为划分特征。
    Args:
        dataframe: 数据集DataFrame
        features: 所有特征名称list
    Returns:
        best_feature: 最优划分特征名称
        best_threshold: 最优划分特征的最优阈值
    """
    base_ent = cal_ent(dataframe)
    best_feature = None
    best_threshold = 0.0
    best_info_gain = 0.0
    for feature in features:
        # print('当前特征为:{}'.format(feature))
        sorted_values = sorted(dataframe[feature].values)
        for i in range(len(sorted_values) - 1):
            threshold = round((sorted_values[i] + sorted_values[i + 1]) / 2, 4)
            greater_data, less_data = split_data(dataframe, feature, threshold)
            prob_g = len(greater_data) / len(dataframe)
            prob_l = len(less_data) / len(dataframe)
            tmp_ent = - prob_g * cal_ent(greater_data) - prob_l * cal_ent(less_data)
            info_gain = base_ent - tmp_ent
            if info_gain > best_info_gain:
                best_feature = feature
                best_info_gain = info_gain
                best_threshold = threshold
            # print(('当前阈值为:{:.4f},'
            #     '信息增益为:{:.4f},'
            #     '最佳信息增益为:{:.4f}').format(
            #     threshold,
            #     info_gain,
            #     best_info_gain))
    return best_feature, best_threshold

def majority_label(dataframe):
    """返回DataFrame中label列下出现次数最多的标签
    Args:
        dataframe: 数据集DataFrame
    Returns:
        出现次数最多的标签以及标签种类数量
    """
    # value_counts默认降序,以label为index,以出现次数为value
    value_counts = dataframe['label'].value_counts()
    return value_counts.index.tolist()[0], len(value_counts)

def create_tree(dataframe, features, min_samples_split=3):
    """递归构建决策树
    Args:
        dataframe: 数据集DataFrame
        features: 所有特征名称list
        min_samples_split: 最小划分子集
    Returns:
        my_tree: 决策树dict
    """
    label, label_category_count = majority_label(dataframe)
    # 停止条件1:DataFrame中所有label都相同
    if label_category_count is 1:
        return label
    # 停止条件2:DataFrame包含的样本数小于min_samples_split
    if len(dataframe) < min_samples_split:
        return label

    best_feature, best_threshold = choose_best_feature(dataframe, features)
    tree_node = best_feature + '>' + str(best_threshold)
    my_tree = {tree_node: {}}

    greater_data, less_data = split_data(dataframe, best_feature, best_threshold)

    my_tree[tree_node]['true'] = create_tree(greater_data, features, min_samples_split)
    my_tree[tree_node]['false'] = create_tree(less_data, features, min_samples_split)
    # print(my_tree)
    return my_tree

def classify(input_tree, test_data):
    """对testData进行分类
    Args:
        input_tree: 构建的决策树dict
        test_data: 测试数据dict
    Returns:
        label: testData对应的预测标签
    """
    get_node = list(input_tree.keys())[0]
    feature = get_node.split('>')[0]
    threshold = float(get_node.split('>')[1])

    branches = input_tree[get_node]
    key = 'true' if test_data[feature] > threshold else 'false'

    sub_tree = branches[key]
    if isinstance(sub_tree, dict):
        label = classify(sub_tree, test_data)
    else:
        label = sub_tree
    return label

def save_tree(input_tree, filename):
    """保存决策树"""
    with open(filename, 'wb') as file:
        pickle.dump(input_tree, file)

def load_tree(filename):
    """加载决策树"""
    with open(filename, 'rb') as file:
        return pickle.load(file)

def test_accuracy(input_tree, test_data):
    """测试决策树对测试集的准确率"""
    length = len(test_data)
    score = 0.0
    for i in range(length):
        result = classify(input_tree, test_data.loc[i]) == int(test_data.loc[i]['label'])
        score += int(result) / length
    return round(score, 6)

def test_major(majority, test_data):
    """测试集标签为majority的准确率"""
    length = len(test_data)
    score = 0.0
    for i in range(length):
        result = majority == test_data.loc[i]['label']
        score += int(result) / length
    return round(score, 6)

def post_pruning_tree(input_tree, dataframe, test_data):
    """后剪枝操作
    以递归的方式进行后剪枝
    Args:
        input_tree: 输入需要进行后剪枝的决策树
        dataframe: 训练数据集
        test_data: 测试数据集
    Returns:
        后剪枝决策树
    """
    get_node = list(input_tree.keys())[0]
    feature = get_node.split('>')[0]
    threshold = float(get_node.split('>')[1])

    branches = input_tree[get_node]

    g_data, l_data = split_data(dataframe, feature, threshold)
    g_data_test, l_data_test = split_data(test_data, feature, threshold)
    # 分支判断
    if isinstance(branches['true'], dict):
        input_tree[get_node]['true'] = post_pruning_tree(branches['true'], g_data, g_data_test)
    if isinstance(branches['false'], dict):
        input_tree[get_node]['false'] = post_pruning_tree(branches['false'], l_data, l_data_test)

    majority, _ = majority_label(dataframe)
    # 递归返回
    if test_accuracy(input_tree, test_data) > test_major(majority, test_data):
        return input_tree
    return majority

def main():
    """主函数"""
    train_data, test_data, features = load_dataset()
    tree = create_tree(train_data, features, min_samples_split=2)
    print('未剪枝决策树准确率:{}'.format(test_accuracy(tree, test_data)))
    pruned_tree = post_pruning_tree(tree, train_data, test_data)
    print('后剪枝决策树准确率:{}'.format(test_accuracy(pruned_tree, test_data)))
    # 保存决策树
    save_tree(pruned_tree, 'my_tree.txt')
    # 从文件中读取决策树
    tree_loaded = load_tree('my_tree.txt')
    print('加载剪枝决策树准确率:{}'.format(test_accuracy(tree_loaded, test_data)))

if __name__ == '__main__':
    main()

未剪枝决策树准确率:0.583333 后剪枝决策树准确率:0.722222 加载剪枝决策树准确率:0.722222

3. 使用sklearn的DecisionTreeClassifier

#coding=utf-8
# ==============================================================================
"""使用sklearn实现决策树
版本:
    Python:3.6.7
参考:
    https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html#sklearn.tree.DecisionTreeClassifier
"""
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn import tree

raw_data = load_wine()
X_train, X_test, y_train, y_test = train_test_split(
    raw_data.data, raw_data.target, test_size=0.2, random_state=2019)
clf = tree.DecisionTreeClassifier(criterion='entropy')
clf.fit(X_train, y_train)

print('测试集准确率:{:.4f}'.format(clf.score(X_test, y_test)))

测试集准确率:0.9167