英雄聯盟勝負預測--簡易肯德基上校

專注的阿熊發表於2022-07-07

# 定義決策樹類

class DecisionTree(object):

     def __init__(self, classes, features,

                  max_depth=10, min_samples_split=10,

                  impurity_t='entropy'):

         '''

         傳入一些可能用到的模型引數,也可能不會用到

         classes 表示模型分類總共有幾類

         features 是每個特徵的名字,也方便查詢總的共特徵數

         max_depth 表示構建決策樹時的最大深度

         min_samples_split 表示構建決策樹分裂節點時,如果到達該節點的樣本數小於該值則不再分裂

         impurity_t 表示計算混雜度(不純度)的計算方式,例如 entropy gini

         '''

         self.classes = classes

         self.features = features

         self.max_depth = max_depth

         self.min_samples_split = min_samples_split

         self.impurity_t = impurity_t

         self.root = None  # 定義根節點,未訓練時為空

     def impurity(self, data):

         '''

         計算某個特徵下的資訊增益

         :param data: numpy 一維陣列

         :return: 混雜度

         '''

         cnt = Counter(data)  # 計數每個值出現的次數

         probability_lst = [1.0 * cnt[i] / len(data) for i in cnt]

         if self.impurity_t == 'entropy':  # 如果是資訊熵

             return -np.sum([p * np.log2(p) for p in probability_lst if p > 0]), cnt  # 返回熵 和可能用到的資料次數 ( 方便以後使用 )

         return 1 - np.sum([p * p for p in probability_lst]), cnt  # 否則返回 gini 係數

     def gain(self, feature, label):

         '''

         計算某個特徵下的資訊增益

         :param feature: 特徵的值, numpy 一維陣列

         :param label: 對應的標籤, numpy 一維陣列

         :return: 資訊增益

         '''

         c_impurity, _ = self.impurity(label)  # 不考慮特徵時標籤的混雜度

         # 記錄特徵的每種取值所對應的陣列下標

         f_index = {}

         for idx, v in enumerate(feature):

             if v not in f_index:

                 f_index[v] = []

             f_index[v].append(idx)

         # 計算根據該特徵分裂後的不純度,根據特徵的每種值的數目加權和

         f_impurity = 0

         for v in f_index:

             # 根據該特徵取值對應的陣列下標 取出對應的標籤列表 比如分支 1 有多少個正負例 分支 2 ...

             f_l = label[f_index[v]]

             f_impurity += self.impurity(f_l)[0] * len(f_l) / len(label)  # 迴圈結束得到各分支混雜度的期望

         gain = c_impurity - f_impurity  # 得到 gain

         # 有些特徵取值很多,天然不純度高、資訊增益高,模型會偏向於取值很多的特徵比如日期,但很可能過擬合

         # 計算資訊增益率可以緩解該問題

         splitInformation = self.impurity(feature)[0]  # 計算該特徵在標籤無關時的不純度

         gainRatio = gain / splitInformation if splitInformation > 0 else gain  # 除數不為 0 時為資訊增益率

         return gainRatio, f_index  # 返回資訊增益率,以及每個特徵取值的陣列下標 ( 方便以後使用 )

     def expand_node(self, feature, label, depth, skip_features=set()):

         '''

         遞迴函式分裂節點

         :param feature: 二維 numpy n*m )陣列,每行表示一個樣本, n 行,有 m 個特徵

         :param label: 一維 numpy n )陣列,表示每個樣本的分類標籤

         :param depth: 當前節點的深度

         :param skip_features: 表示當前路徑已經用到的特徵

         在當前 ID3 演算法離散特徵的實現下,一條路徑上已經用過的特徵不會再用(其他實現有可能會選重複特徵)

         '''

         l_cnt = Counter(label)  # 計數每個類別的樣本出現次數

         if len(l_cnt) <= 1:  # 如果只有一種類別了,無需分裂,已經是葉節點

             return label[0]  # 只需記錄類別

         if len(label) < self.min_samples_split or depth > self.max_depth:  # 如果達到了最小分裂的樣本數或者最大深度的閾值

             return l_cnt.most_common(1)[0][0]  # 則只記錄當前樣本中最多的類別

         f_idx, max_gain, f_v_index = -1, -1, None  # 準備挑選分裂特徵

         for idx in range(len(self.features)):  # 遍歷所有特徵

             if idx in skip_features:  # 如果當前路徑已經用到,不用再算

                 continue

             f_gain, fv = self.gain(feature[:, idx], label)  # 計算特徵的資訊增益, fv 是特徵每個取值的樣本下標

             # if f_gain <= 0: # 如果資訊增益不為正,跳過該特徵

             #   continue

             if f_idx < 0 or f_gain > max_gain:  # 如果個更好的分裂特徵

                 f_idx, max_gain, f_v_index = idx, f_gain, fv  # 則記錄該特徵

             # if f_idx < 0: # 如果沒有找到合適的特徵,即所有特徵都沒有資訊增益

             #   return l_cnt.most_common(1)[0][0] # 則只記錄當前樣本中最多的類別

         decision = {}  # 用字典記錄每個特徵取值所對應的子節點, key 是特徵取值, value 是子節點

         skip_features = set([f_idx] + [f for f in skip_features])  # 子節點要跳過的特徵包括當前選擇的特徵

         for v in f_v_index:  # 遍歷特徵的每種取值

             decision[v] = self.expand_node(feature[f_v_index[v], :], label[f_v_index[v]],  # 取出該特徵取值所對應的樣本

                                            depth=depth + 1, skip_features=skip_features)  # 深度 +1 ,遞迴呼叫節點分裂

         # 返回一個元組,有三個元素

         # 第一個是選擇的特徵下標,第二個特徵取值和對應的子節點 ( 字典 ) ,第三個是到達當前節點的樣本中最多的類別

         return (f_idx, decision, l_cnt.most_common(1)[0][0])

     def traverse_node(self, node, feature):

         '''

         預測樣本時從根節點開始遍歷節點,根據特徵路由。

         :param node: 當前到達的節點,例如 self.root

         :param feature: 長度為 m numpy 一維陣列

         '''

         assert len(self.features) == len(feature)  # 要求輸入樣本特徵數和模型定義時特徵數目一致

         if type(node) is not tuple:  # 如果到達了一個節點是葉節點(不再分裂),則返回該節點類別

             return node

         fv = feature[node[0]]  # 否則取出該節點對應的特徵值, node[0] 記錄了特徵的下標

         if fv in node[1]:  # 外匯跟單gendan5.com 根據特徵值找到子節點,注意需要判斷訓練節點分裂時到達該節點的樣本是否有該特徵值(分支)

             return self.traverse_node(node[1][fv], feature)  # 如果有,則進入到子節點繼續遍歷

         return node[-1]  # 如果沒有,返回訓練時到達當前節點的樣本中最多的類別

     def fit(self, feature, label):

         '''

         訓練模型

         :param feature:feature 為二維 numpy n*m )陣列,每行表示一個樣本,有 m 個特徵

         :param label:label 為一維 numpy n )陣列,表示每個樣本的分類標籤

         '''

         assert len(self.features) == len(feature[0])  # 輸入資料的特徵數目應該和模型定義時的特徵數目相同

         self.root = self.expand_node(feature, label, depth=1)  # 從根節點開始分裂,模型記錄根節點

     def predict(self, feature):

         '''

         預測

         :param feature: 輸入 feature 可以是一個一維 numpy 陣列也可以是一個二維 numpy 陣列

         如果是一維 numpy m )陣列則是一個樣本,包含 m 個特徵,返回一個類別值

         如果是二維 numpy n*m )陣列則表示 n 個樣本,每個樣本包含 m 個特徵,返回一個 numpy 一維陣列

         '''

         assert len(feature.shape) == 1 or len(feature.shape) == 2  # 只能是 1 維或 2

         if len(feature.shape) == 1:  # 如果是一個樣本

             return self.traverse_node(self.root, feature)  # 從根節點開始路由

         return np.array([self.traverse_node(self.root, f) for f in feature])  # 如果是很多個樣本

     def get_params(self, deep):  # 要呼叫 sklearn cross_validate 需要實現該函式返回所有引數

         return {'classes': self.classes, 'features': self.features,

                 'max_depth': self.max_depth, 'min_samples_split': self.min_samples_split,

                 'impurity_t': self.impurity_t}

     def set_params(self, **parameters):  # 要呼叫 sklearn GridSearchCV 需要實現該函式給類設定所有引數

         for parameter, value in parameters.items():

             setattr(self, parameter, value)

         return self

# 定義決策樹模型,傳入演算法引數

DT = DecisionTree(classes=[0, 1], features=feature_names, max_depth=5, min_samples_split=10, impurity_t='gini')

DT.fit(x_train, y_train)  # 在訓練集上訓練

p_test = DT.predict(x_test)  # 在測試集上預測,獲得預測值

print(p_test)  # 輸出預測值

test_acc = accuracy_score(p_test, y_test)  # 將測試預測值與測試集標籤對比獲得準確率

print('accuracy: {:.4f}'.format(test_acc))  # 輸出準確率

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69946337/viewspace-2904738/,如需轉載,請註明出處,否則將追究法律責任。

相關文章