隨機森林 RF 算法原理及實踐(二)

 上一節說過隨機森林(Random Forest,RF)算法是一種重要的基於Bagging 的集成學習算法,它可以用來做分類、迴歸等問題。下面就分類問題展開學習。

一、隨機森林算法模型

RF 在以決策樹爲基學習器構建 Bagging 集成的基礎上,進一步在決策樹的訓練過程中引入隨機屬性選擇,具體的就是傳統決策樹在選擇劃分屬性時是在當前節點的屬性集合(假設有 d 個屬性)中選擇一個最優的屬性;而在 RF 中,對基決策樹的每個節點,先從該節點的屬性集合中隨機選擇一個包含 k 個屬性的集合,然後再從這個子集中選擇一個最優屬性用於劃分。若 k=d,則基決策樹的構建和傳統決策樹相同,若 k=1,則是隨機選擇一個屬性進行劃分,一般推薦 k=log_{2}d

RF 其實質是對決策樹算法的一種改進,將多個決策樹合併在一起,每棵樹的建立依賴於一個獨立抽取的樣本,森林中每棵樹具有相同的分佈,分類誤差取決於每一棵樹的分類能力和他們之間的相關性。

隨機森林簡單、容易實現、計算開銷小,被譽爲“代表集成學習技術水平的方法“。在 Bagging 算法中基學習器的多樣性僅通過樣本擾動而不同,但是在 RF 算法中不僅通過樣本擾動還通過自屬性擾動,最終會使得集成的泛化能力通過個體學習器之間差異度的增加而進一步提升。在引入屬性擾動,隨機森林中個體學習器的性能往往降低,但是隨着個體學習器數目的增加,隨機森林往往通常會收斂到更低的泛化誤差。Bagging 使用的是確定性決策樹,在選擇劃分屬性時要對結點的所有屬性進行考察,而隨機森林使用的隨機型決策樹則只考察一個屬性子集。

RF的主要缺點有:

  1. 在某些噪音比較大的樣本集上,RF模型容易陷入過擬合;
  2. 取值劃分比較多的特徵容易對RF的決策產生更大的影響,從而影響擬合的模型的效果。

RF的主要優點有:

  1. 訓練可以高度並行化,對於大數據時代的大樣本訓練速度有優勢;
  2. 由於可以隨機選擇決策樹節點劃分特徵,這樣在樣本特徵維度很高的時候,仍然能高效的訓練模型;
  3. 由於採用了隨機採樣,訓練出的模型的方差小,泛化能力強;
  4. 相對於Boosting系列的Adaboost和GBDT, RF實現比較簡單;
  5. 對部分特徵缺失不敏感。

二、算法流程

  1. 假設訓練樣本的個數爲 m,則對每一棵決策樹的輸入樣本的個數都爲 m,這 m 個樣本是通過從訓練集中有放回地隨機抽取得到;
  2. 假設訓練樣本特徵的個數爲 n,對每一棵決策樹的樣本特徵是從 n 個特徵中隨機挑選 k 個,然後從這 k 個輸入特徵中選擇一個最優的進行分裂;
  3. 每棵樹都這樣分裂,直到該結點的所有訓練樣例都屬於同一類,決策樹分裂過程不需要剪枝。

三、Python 實現隨機森林訓練 

首先構建 CART 分類樹,predict 函數利用構建好的 CART 樹模型對樣本進行預測,pickle 模塊用於保存和導入訓練好的隨機森林RF 模型, 要計算 Gini 指數, 需要用到 pow 函數。保存到 tree.py 中:

from math import pow
class node:
    '''樹的節點的類
    '''
    def __init__(self, fea=-1, value=None, results=None, right=None, left=None):
        self.fea = fea  # 用於切分數據集的屬性的列索引值
        self.value = value  # 設置劃分的值
        self.results = results  # 存儲葉節點所屬的類別
        self.right = right  # 右子樹
        self.left = left  # 左子樹

def split_tree(data, fea, value):
    '''根據特徵fea中的值value將數據集data劃分成左右子樹
    input:  data(list):數據集
            fea(int):待分割特徵的索引
            value(float):待分割的特徵的具體值
    output: (set1,set2)(tuple):分割後的左右子樹
    '''
    set_1 = []
    set_2 = []
    for x in data:
        if x[fea] >= value:
            set_1.append(x)
        else:
            set_2.append(x)
    return (set_1, set_2)

def label_uniq_cnt(data):
    '''統計數據集中不同的類標籤label的個數
    input:  data(list):原始數據集
    output: label_uniq_cnt(int):樣本中的標籤的個數
    '''
    label_uniq_cnt = {}
    for x in data:
        label = x[len(x) - 1]  # 取得每一個樣本的類標籤label
        if label not in label_uniq_cnt:
            label_uniq_cnt[label] = 0
        label_uniq_cnt[label] = label_uniq_cnt[label] + 1
    return label_uniq_cnt

def cal_gini_index(data):
    '''計算給定數據集的Gini指數
    input:  data(list):樹中
    output: gini(float):Gini指數
    '''
    total_sample = len(data)  # 樣本的總個數 
    if len(data) == 0:
        return 0   
    label_counts = label_uniq_cnt(data)  # 統計數據集中不同標籤的個數
     # 計算數據集的Gini指數
    gini = 0
    for label in label_counts:
        gini = gini + pow(label_counts[label], 2)
    gini = 1 - float(gini) / pow(total_sample, 2)
    return gini

def build_tree(data):
    '''構建樹
    input:  data(list):訓練樣本
    output: node:樹的根結點
    '''
    # 構建決策樹,函數返回該決策樹的根節點
    if len(data) == 0:
        return node()
    # 1、計算當前的Gini指數
    currentGini = cal_gini_index(data)
    bestGain = 0.0
    bestCriteria = None  # 存儲最佳切分屬性以及最佳切分點
    bestSets = None  # 存儲切分後的兩個數據集 
    feature_num = len(data[0]) - 1  # 樣本中特徵的個數
    # 2、找到最好的劃分
    for fea in range(0, feature_num):
        # 2.1、取得fea特徵處所有可能的取值
        feature_values = {}  # 在fea位置處可能的取值
        for sample in data:  # 對每一個樣本
            feature_values[sample[fea]] = 1  # 存儲特徵fea處所有可能的取值
        # 2.2、針對每一個可能的取值,嘗試將數據集劃分,並計算Gini指數
        for value in feature_values.keys():  # 遍歷該屬性的所有切分點
            # 2.2.1、 根據fea特徵中的值value將數據集劃分成左右子樹
            (set_1, set_2) = split_tree(data, fea, value)
            # 2.2.2、計算當前的Gini指數
            nowGini = float(len(set_1) * cal_gini_index(set_1) + \
                             len(set_2) * cal_gini_index(set_2)) / len(data)
            # 2.2.3、計算Gini指數的增加量
            gain = currentGini - nowGini
            # 2.2.4、判斷此劃分是否比當前的劃分更好
            if gain > bestGain and len(set_1) > 0 and len(set_2) > 0:
                bestGain = gain
                bestCriteria = (fea, value)
                bestSets = (set_1, set_2) 
    # 3、判斷劃分是否結束
    if bestGain > 0:
        right = build_tree(bestSets[0])
        left = build_tree(bestSets[1])
        return node(fea=bestCriteria[0], value=bestCriteria[1], \
                    right=right, left=left)
    else:
        return node(results=label_uniq_cnt(data))  # 返回當前的類別標籤作爲最終的類別標籤
def predict(sample, tree):
    '''對每一個樣本sample進行預測
    input:  sample(list):需要預測的樣本
            tree(類):構建好的分類樹
    output: tree.results:所屬的類別
    '''
    # 1、只是樹根
    if tree.results != None:
        return tree.results
    else:
    # 2、有左右子樹
        val_sample = sample[tree.fea]
        branch = None
        if val_sample >= tree.value:
            branch = tree.right
        else:
           branch = tree.left
        return predict(sample, branch)

訓練隨機森林模型:

# -*- coding: utf-8 -*-
"""
Created on Thu Mar 14 16:11:16 2019

@author: 2018061801
"""
import numpy as np
import random as rd
from math import log 
from tree import build_tree, predict
import pickle as pk
def load_data(file_name):
    '''導入數據
    input:  file_name(string):訓練數據保存的文件名
    output: data_train(list):訓練數據
    '''
    data_train = []
    f = open(file_name)
    for line in f.readlines():
        lines = line.strip().split("\t")
        data_tmp = []
        for x in lines:
            data_tmp.append(float(x))
        data_train.append(data_tmp)
    f.close()
    return data_train
def choose_samples(data, k):
    '''
    input:  data(list):原始數據集
            k(int):選擇特徵的個數
    output: data_samples(list):被選擇出來的樣本
            feature(list):被選擇的特徵index
    '''
    m, n = np.shape(data)  
    # 樣本的個數和樣本特徵的個數
    # 1、選擇出k個特徵的index
    feature = []
    for j in range(k):
        feature.append(rd.randint(0, n - 2))  # n-1列是標籤
    # 2、選擇出m個樣本的index
    index = []
    for i in range(m):
        index.append(rd.randint(0, m - 1))
    # 3、從data中選擇出m個樣本的k個特徵,組成數據集data_samples
    data_samples = []
    for i in range(m):
        data_tmp = []
        for fea in feature:
            data_tmp.append(data[index[i]][fea])
        data_tmp.append(data[index[i]][-1])
        data_samples.append(data_tmp)
    return data_samples, feature

def random_forest_training(data_train, trees_num):
    '''構建隨機森林
    input:  data_train(list):訓練數據
            trees_num(int):分類樹的個數
    output: trees_result(list):每一棵樹的最好劃分
            trees_feature(list):每一棵樹中對原始特徵的選擇
    '''
    trees_result = []  # 構建好每一棵樹的最好劃分
    trees_feature = []
    n = np.shape(data_train)[1]  # 樣本的維數
    if n > 2:
        k = int(log(n - 1, 2)) + 1 # 設置特徵的個數
    else:
        k = 1
    # 開始構建每一棵樹
    for i in range(trees_num):
        # 1、隨機選擇m個樣本, k個特徵
        data_samples, feature = choose_samples(data_train, k)
        # 2、構建每一棵分類樹
        tree = build_tree(data_samples)
        # 3、保存訓練好的分類樹
        trees_result.append(tree)
        # 4、保存好該分類樹使用到的特徵
        trees_feature.append(feature)
    return trees_result, trees_feature

def split_data(data_train, feature):
    '''選擇特徵
    input:  data_train(list):訓練數據集
            feature(list):要選擇的特徵
    output: data(list):選擇出來的數據集
    '''
    m = np.shape(data_train)[0]
    data = []
    for i in range(m):
        data_x_tmp = []
        for x in feature:
            data_x_tmp.append(data_train[i][x])
        data_x_tmp.append(data_train[i][-1])
        data.append(data_x_tmp)
    return data

def get_predict(trees_result, trees_fiture, data_train):
    m_tree = len(trees_result)
    m = np.shape(data_train)[0]
    result = []
    for i in range(m_tree):
        clf = trees_result[i]
        feature = trees_fiture[i]
        data = split_data(data_train, feature)
        result_i = []
        for i in range(m):
            result_i.append(list(predict(data[i][0:-1], clf).keys())[0])
        result.append(result_i)
    final_predict = np.sum(result, axis=0)
    return final_predict

def cal_correct_rate(data_train, final_predict):
    m = len(final_predict)
    corr = 0.0
    for i in range(m):
        if data_train[i][-1] * final_predict[i] > 0:
            corr += 1
    return corr / m

def save_model(trees_result, trees_feature, result_file, feature_file):
    # 1、保存選擇的特徵
    m = len(trees_feature)
    f_fea = open(feature_file, "w")
    for i in range(m):
        fea_tmp = []
        for x in trees_feature[i]:
            fea_tmp.append(str(x))
        f_fea.writelines("\t".join(fea_tmp) + "\n")
    f_fea.close()  
    # 2、保存最終的隨機森林模型
    with open(result_file, 'wb+') as f:
        pk.dump(trees_result, f)

if __name__ == "__main__":
   # 1、導入數據
    print ("----------- 1、load data -----------")
    data_train = load_data("D:/anaconda4.3/spyder_work/test3.txt")
    # 2、訓練random_forest模型
    print ("----------- 2、random forest training ------------")
    trees_result, trees_feature = random_forest_training(data_train, 50)
    # 3、得到訓練的準確性
    print ("------------ 3、get prediction correct rate ------------")
    result = get_predict(trees_result, trees_feature, data_train)
    corr_rate = cal_correct_rate(data_train, result)
    print ("\t------correct rate: ", corr_rate)
    # 4、保存最終的隨機森林模型
    print ("------------ 4、save model -------------")
    save_model(trees_result, trees_feature, "result_file", "feature_file")

結果如下:

Reloaded modules: tree
----------- 1、load data -----------
----------- 2、random forest training ------------
------------ 3、get prediction correct rate ------------
        ------correct rate:  1.0
------------ 4、save model -------------

可以看出,隨機森林的準確率爲100%

注: 我使用 Python3.5 , 測試部分可以自己去試試

訓練數據鏈接https://github.com/zhaozhiyong19890102/Python-Machine-Learning-Algorithm/blob/master/Chapter_5%20Random%20Forest/data.txt

 

 

參考文獻:趙志勇《python 機器學習算法》(程序)

周志華《機器學習》

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