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