ID3算法

前言:我參考了不少前輩寫的博客,有https://github.com/Dod-o/Statistical-Learning-Method_Codehttp://www.javashuo.com/article/p-pgblwlao-mo.html,都寫的很好,我在這裏從新寫一遍只是爲了整理本身學過的東西並加深本身的理解,方便之後回顧複習。對於初學的朋友,建議直接閱讀以上的兩個連接。html

數據集採用mnist,使用ID3來對圖片集進行分類,將每個像素做爲一個特徵,爲了提升預測的精確率,須要對圖片進行二值化處理。node

如下是ID3的源代碼。git

# encoding=utf-8

import cv2
import time
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score



# 二值化
def binaryzation(img):
    cv_img = img.astype(np.uint8) # astype用於改變數據的類型   uint8是無符號八位(bit)整型,表示範圍是[0, 255]的整數
    # 原圖片,閾值,最大值,劃分時採用的算法  大於50的點設置爲0,其餘的爲最大值1
    cv2.threshold(cv_img, 50, 1, cv2.THRESH_BINARY_INV, cv_img) 
    return cv_img

def binaryzation_features(trainset):
    features = []
    for img in trainset:
        img = np.reshape(img,(28,28)) # 28行*28列,每個元素表明一個像素值,元素的值是8位無符號整型
        cv_img = img.astype(np.uint8)
        img_b = binaryzation(cv_img)  # 此爲上面自定義的函數
        # hog_feature = np.transpose(hog_feature)
        features.append(img_b)
    features = np.array(features) # 將list轉化爲array
    features = np.reshape(features,(-1,feature_len)) # 我不知道能夠分紅多少行,可是個人須要是分紅feature_len列(feature_len = 784) 
    return features


import os
import struct
import numpy as np

def load_mnist(path, kind='train'):
    labels_path = os.path.join(path, '%s-labels.idx1-ubyte' % kind)
    images_path = os.path.join(path, '%s-images.idx3-ubyte' % kind)
    with open(labels_path, 'rb') as lbpath:
        magic, n = struct.unpack('>II', lbpath.read(8)) # 一個I表明4個字節,因此一共有8字節的頭部,分別存入變量magic和n中
        labels = np.fromfile(lbpath, dtype=np.uint8) # 一個字節一讀,並轉化爲8位無符號整型
    with open(images_path, 'rb') as imgpath:
        magic, num, rows, cols = struct.unpack('>IIII', imgpath.read(16))
        images = binaryzation_features(np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784))
        # images = np.fromfile(imgpath, dtype=np.uint8).reshape(len(labels), 784) # 除去16個字節的頭部以後,剩下的數據轉變爲8位無符號整型
    return images, labels
    # 訓練集一共有9912422字節,訓練集一共有60000個樣本,經過樣本數沒法推得有那麼多的字節,應該是通過壓縮了的



# 決策樹的定義
class Tree(object):
    def __init__(self,node_type,Class = None, feature = None):
        self.node_type = node_type  # 節點類型(internal或leaf)
        self.dict = {}              # dict的鍵表示特徵Ag的可能值ai,值表示根據ai獲得的子樹,{特徵值:子樹},採用字典的形式來描述樹型結構
        self.Class = Class          # 葉節點表示的類標記,如果內部節點則爲none
        self.feature = feature      # 表示當前的樹即將由第feature個特徵劃分(即第feature特徵是使得當前樹中信息增益最大的特徵)

    def add_tree(self,key,tree):    # key表明的特徵值,不一樣的key用於區分不一樣的子樹
        self.dict[key] = tree       # 一個key就是一條樹的分支

    def predict(self,features):     # features爲當前待預測的一個樣本
        # 當樣本中採樣不全,結果在測試集中出現了一個在樣本中從未出現過特徵值,則搜索會在內部節點就直接終止,self.class即是None
        if self.node_type == 'leaf' or (features[self.feature] not in self.dict): 
            return self.Class       #  predict最終返回的是樣本對應的類標記。
        tree = self.dict.get(features[self.feature]) # 找出第feature個特徵所對應的特徵值
        return tree.predict(features)  # 遞歸搜索


# 計算數據集x的經驗熵H(x)——每個類的樣本數量在全部類樣本數據中所佔的比重
def calc_ent(x):  # 類列
    # 樣本中有那些類(樣本中的類可能不全),x的類型是array,先將其轉變爲list,再變爲set
    x_value_list = set([x[i] for i in range(x.shape[0])]) # 造成不重複的類集合
    ent = 0.0
    for x_value in x_value_list:
        p = float(x[x == x_value].shape[0]) / x.shape[0]  # 當前類的數量 / 總的類的數量
        logp = np.log2(p)
        ent -= p * logp # 計算熵,熵越大,隨機變量的不肯定性就越大
    return ent


# 計算條件熵H(y/x)
def calc_condition_ent(x, y): # 特徵列,類列
    x_value_list = set([x[i] for i in range(x.shape[0])]) # 某一特徵中的全部不重複的特徵值
    ent = 0.0
    for x_value in x_value_list:
        # 從x這一array數組中依次遍歷,挑出其中與值x_value相同的,因爲train_test_split的影響,其結果會直接對應y中的對應類
        sub_y = y[x == x_value] 
        temp_ent = calc_ent(sub_y) # sub_y是擁有相同特徵值的不一樣的類的列表
        ent += (float(sub_y.shape[0]) / y.shape[0]) * temp_ent
    return ent


# 計算信息增益
def calc_ent_grap(x,y):  # 特徵列,類列
    base_ent = calc_ent(y) # 計算數據集x的經驗熵H(x)
    condition_ent = calc_condition_ent(x, y) # 計算條件熵H(y/x)
    ent_grap = base_ent - condition_ent
    return ent_grap


# ID3算法
def recurse_train(train_set, train_label, features): # train_features, train_labels. 訓練集以及其對應的類列表  features爲特徵列表 
    LEAF = 'leaf'
    INTERNAL = 'internal'

    # 步驟1——若是訓練集train_set中的全部實例都屬於同一類Ck
    label_set = set(train_label)  # set能夠理解爲數據不重複的列表,且元素爲不可變對象
    if len(label_set) == 1:
        return Tree(LEAF,Class = label_set.pop())

    # 步驟2——處理特徵集爲空時的狀況
        # filter() 函數用於過濾序列,過濾掉不符合條件的元素,返回由符合條件元素組成的迭代器對象。
        # lambda x:x==i,train_label,x指向train_label,依次搜索train_label中的每個值,若是與i相等,則將其放入list中
        # 計算每個類出現的個數,並用元組存放結果[(類標號,類的數量), ...]
    class_len = [(i,len(list(  filter(lambda x:x==i,train_label)  )))   for i in range(class_num)] 
    (max_class, max_len) = max(class_len, key = lambda x:x[1]) # 返回列表中元組中的類數量最多的那個元組   x[1]類所對應的數量
        # 類, 類的數量 
    if len(features) == 0:  # features爲特徵集合
        return Tree(LEAF,Class = max_class)

    # 步驟3——計算信息增益,並選擇信息增益最大的特徵
    max_feature = 0  # 能產生最大信息增益的特徵
    max_gda = 0      # 信息增益的最大值
    D = train_label  # train_labels,訓練集對應的類列表
    for feature in features: # 從784個特徵中依次選取特徵計算器信息增益
        A = np.array(train_set[:,feature].flat) # 選擇訓練集中的第feature列的全部特徵值,.flat是將數組轉換爲1-D的迭代器
        gda=calc_ent_grap(A,D) # 計算信息增益
        if gda > max_gda:
            max_gda,max_feature = gda,feature

    # 步驟4——信息增益小於閾值,這裏採用先剪枝算法,防止過擬合
    if max_gda < epsilon:
        return Tree(LEAF,Class = max_class)

    # 步驟5——構建非空子集
    sub_features = list(filter(lambda x:x!=max_feature,features)) # 將已經使用過的特徵從特徵集中刪除
    tree = Tree(INTERNAL,feature=max_feature) # 當前樹節點是內部節點,將由feature特徵繼續劃分樣本集
    max_feature_col = np.array(train_set[:,max_feature].flat) # .flat是將數組轉換爲1-D的迭代器
        # 保存信息增益最大的特徵可能的取值 (shape[0]表示計算行數)
        # y.shape 返回的一個元組,表明 y 數據集的信息如(行,列) y.shape[0], 意思是:返回 y 中行的總數。
        # 這個值在 y 是單特徵的狀況下 和 len(y) 是等價的。即數據集中數據點的總數。
    # 依據特徵值的不一樣將樣本分到不一樣的子樹
    feature_value_list = set([max_feature_col[i] for i in range(max_feature_col.shape[0])]) 
    for feature_value in feature_value_list:
        index = []
        for i in range(len(train_label)):
            if train_set[i][max_feature] == feature_value:
                index.append(i)
        sub_train_set = train_set[index]     # 將特徵值同是feature_value的樣本放到一塊兒,造成一個新的子樹樣本
        sub_train_label = train_label[index] # 將樣本對應的類也封裝到一塊兒,一塊兒傳入子樹中
        sub_tree = recurse_train(sub_train_set,sub_train_label,sub_features) # 遞歸函數
        tree.add_tree(feature_value,sub_tree)      
    return tree

def train(train_set,train_label,features):
    return recurse_train(train_set,train_label,features)

def predict(test_set,tree):
    result = []
    for features in test_set:
        tmp_predict = tree.predict(features)
        result.append(tmp_predict) # result存儲測試集中的樣本對應的測試獲得的類
    return np.array(result) # 將list數組轉變爲array數組



class_num = 10     # MINST數據集有10種labels,分別是「0,1,2,3,4,5,6,7,8,9」
feature_len = 784  # MINST數據集每一個image有28*28=784個特徵(pixels)
epsilon = 0.001

if __name__ == '__main__':
    print("Start read data...")
    t1 = time.time()
    # basedir = os.path.dirname(__file__) 
    train_features,train_labels = load_mnist("data" , kind='train')
    test_features,test_labels = load_mnist("data", kind='t10k')
    t2 = time.time()
    print("讀取數據用時:" + str((t2-t1)))
    
    print('Start training...')
    tree = train(train_features, train_labels, list(range(feature_len))) # 0-783的一個數組,特徵集
    t3 = time.time()
    print("訓練數據用時:" + str((t3-t2)))

    print('Start predicting...')
    test_predict = predict(test_features,tree)  # test_features測試集,tree爲訓練好的模型,函數返回的類型爲np.array
    t4 = time.time()
    print("預測結果用時:" + str((t4-t3)))

    r = 0
    for i in range(len(test_predict)):
        if test_predict[i] == None: # 在樹的某一個分支處沒有其對應的特徵值
            test_predict[i] = 10 # 最終結果是不匹配,可是因爲須要比較,因此得將None變成數字,不存在10這個手寫數字
        else:
            if test_predict[i] == test_labels[i]:
                r = r + 1
    score = float(r)/float(len(test_predict))
    print("The accruacy score is %f" % score)
相關文章
相關標籤/搜索