機器學習算法——手動搭建決策樹分類器(代碼+作圖)

決策樹分類器原理(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()

在這裏插入圖片描述

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