1. 程式人生 > 其它 >如何在Python中從零開始實現隨機森林

如何在Python中從零開始實現隨機森林

決策樹可能會受到高度變異的影響,使得結果對所使用的特定測試資料而言變得脆弱。

根據您的測試資料樣本構建多個模型(稱為套袋)可以減少這種差異,但是樹本身是高度相關的。

隨機森林是套袋(方法)的延伸,除了基於多個測試資料樣本構建樹木之外,它還限制了可用於構建樹木的特徵,使得樹木間具有差異。這反過來可以提升演算法的表現。

在本教程中,您將瞭解如何在Python中從頭開始實現隨機森林演算法。

完成本教程後,您將知道:

  • 套袋決策樹和隨機森林演算法的區別。
  • 如何構造更多方差的袋裝決策樹。
  • 如何將隨機森林演算法應用於預測建模問題。

讓我們開始吧。

  • 2017年1月更新:將cross_validation_split()中fold_size的計算更改為始終為整數。修復了Python 3的問題。
  • 2017年2月更新:修復了build_tree中的錯誤。
  • 2017年8月更新:修正了基尼計算中的一個錯誤,增加了群組大小(基於邁克爾!)。

如何在Python中從頭開始實現隨機森林 圖片來自 InspireFate Photography,保留部分權利。

描述

本節簡要介紹本教程中使用的隨機森林演算法和Sonar資料集。

隨機森林演算法

決策樹涉及從資料集中(利用)貪婪選擇選取最佳分割點過程中的每一步。

如果不精簡(該演算法),此演算法容易使決策樹出現高方差。這種高方差(結果)可以通過建立包含測試資料集中(多個)不同的例項(問題的不同觀點)的多重樹,接著將例項所有的可能結果結合,這種方法簡稱為bootstrap聚合或套袋。

套袋的侷限性在於,它使用相同的貪婪演算法來建立每棵樹,這意味著在每棵樹中可能會選擇相同或非常相似的分割點,使得不同的樹非常相似(樹將被關聯)。這反過來又使他們的預測相似,從而縮減了最初尋求的差異。

我們可以通過貪婪演算法在建立樹時在每個分割點評估的特徵(行)來限制決策樹不同。這被稱為隨機森林演算法。

像裝袋一樣,測試資料集的多個樣本在被採集後,接著在每個樣本上訓練不同的樹。不同之處在於在每一點上,拆分是在資料中進行並新增到樹中的,且只考慮固定的屬性子集。

對於分類問題,我們將在本教程中討論的問題的型別——分割中輸入特點數的平方根值對為分割操作考慮的屬性個數的限制。

num_features_for_split = sqrt(total_input_features)

這一小變化的結果是樹之間變得更加不同(不關聯),作為結果會有更加多樣化的預測,這樣的結果往往好於一個單獨的樹或者單獨套袋得到的結果。

聲納資料集

我們將在本教程中使用的資料集是Sonar資料集。

這是一個描述聲納聲音從不同曲面反彈後返回(資料)的資料集。輸入的60個變數是聲吶從不同角度返回的力度值。這是一個二元分類問題,需要一個模型來區分金屬圓柱中的岩石。這裡有208個觀察物件。

這是一個很好理解的資料集。所有變數都是連續的且範圍一般是0到1。輸出變數是“Mine”字串中的“M”和“rock”中的“R”,需要轉換為整數1和0。

通過預測在資料集(“M”或“mines”)中觀測數最多的類,零規則演算法可以達到53%的準確度。

您可以在UCI Machine Learning repository瞭解關於此資料集的更多資訊。

下載免費的資料集,並將其放置在工作目錄中,檔案名為sonar.all-data.csv

教程

本教程分為2個步驟。

  1. 計算分割。
  2. 聲納資料集案例研究。

這些步驟為您需要將隨機森林演算法應用於自己的預測建模問題奠定了基礎。

1.計算分割

在決策樹中,通過利用最低成本找到指定屬性和該屬性的值方法來確定分割點。

對於分類問題,這個成本函式通常是基尼指數,它計算分割點建立的資料組的純度。基尼指數為0是完美純度,其中在兩類分類問題的情況下,將類別值完全分成兩組。

在決策樹中找到最佳分割點涉及到為每個輸入的變數評估訓練資料集中每個值的成本。

對於裝袋和隨機森林,這個程式是在測試資料集的樣本上執行的,並且是可替換的。更換取樣意味著同一行(資料)會不止一次的被選擇並將其新增到取樣中。

我們可以優化隨機森林的這個程式。我們可以建立一個輸入屬性樣本來考慮,而不是在搜尋中列舉輸入屬性的所有值。

這個輸入屬性的樣本可以隨機選擇而不需要替換,這意味著每個輸入屬性在查詢具有最低成本的分割點的過程中只被考慮一次。

下面是實現此過程的函式名稱get_split()。它將資料集和固定數量的輸入要素作為輸入引數進行評估,此資料集可能是實際測試資料集的一個樣本。

helper函式test_split()用於通過候選分割點拆分資料集,gini_index()用於根據建立的行組來計算給定拆分的花費。

我們可以看到,通過隨機選擇特徵索引並將其新增到列表(稱為特徵)來建立特徵列表,然後列舉該特徵列表並且將測試資料集中的特定值評估作為分割點。

# Select the best split point for a dataset
def get_split(dataset, n_features):
    class_values = list(set(row[-1] for row in dataset))
    b_index, b_value, b_score, b_groups = 999, 999, 999, None
    features = list()
    while len(features) < n_features:
        index = randrange(len(dataset[0])-1)
        if index not in features:
            features.append(index)
    for index in features:
        for row in dataset:
            groups = test_split(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}

現在我們知道如何修改決策樹演算法以便與隨機森林演算法一起使用,我們可以將它與一個bagging實現一起使用,並將其應用於現實生活中的資料集。

2.聲納資料集案例研究

在本節中,我們將把隨機森林演算法應用到聲納資料集。

該示例假定資料集的CSV副本位於當前工作目錄中,檔名為sonar.all-data.csv

首先載入資料集,將字串值轉換為數字,並將輸出列從字串轉換為0和1的整數值。這可以通過使用幫助器函式load_csv()str_column_to_float()str_column_to_int()來載入和預備資料集。

我們將使用k-fold交叉驗證來估計未知資料的學習模型的效能。這意味著我們將構建和評估k個模型,並將效能估計為平均模型誤差。分類準確性將用於評估每個模型。這些工具或是演算法在cross_validation_split()accuracy_metric()evaluate_algorithm()輔助函式中提供。

我們也將使用適合套袋包括輔助功能分類和迴歸樹(CART)演算法的實現)test_split(拆分資料集分成組,gini_index()來評估分割點,我們修改get_split()函式中討論在前一步中,to_terminal()split()build_tree()用於建立單個決策樹,預測()使用決策樹進行預測,subsample()建立訓練資料集的子取樣,以及bagging_predict()用決策樹列表進行預測。

開發了一個新的函式名稱random_forest(),首先根據訓練資料集的子樣本建立一個決策樹列表,然後使用它們進行預測。

正如我們上面所說的,隨機森林和袋裝決策樹之間的關鍵區別是對樹的建立方式中的一個小的改變,這裡是在get_split()函式中。

完整的例子如下所示。

# Random Forest Algorithm on Sonar Dataset
from random import seed
from random import randrange
from csv import reade
from math import sqrt
 
# Load a CSV file
def load_csv(filename):
    dataset = list()
    with open(filename, 'r') as file:
        csv_reader = reader(file)
        for row in csv_reader:
            if not row:
                continue
            dataset.append(row)
    return dataset
 
# Convert string column to float
def str_column_to_float(dataset, column):
    for row in dataset:
        row[column] = float(row[column].strip())
 
# Convert string column to intege
def str_column_to_int(dataset, column):
    class_values = [row[column] for row in dataset]
    unique = set(class_values)
    lookup = dict()
    for i, value in enumerate(unique):
        lookup[value] = i
    for row in dataset:
        row[column] = lookup[row[column]]
    return lookup
 
# Split a dataset into k folds
def cross_validation_split(dataset, n_folds):
    dataset_split = list()
    dataset_copy = list(dataset)
    fold_size = int(len(dataset) / n_folds)
    for i in range(n_folds):
        fold = list()
        while len(fold) < fold_size:
            index = randrange(len(dataset_copy))
            fold.append(dataset_copy.pop(index))
        dataset_split.append(fold)
    return dataset_split
 
# Calculate accuracy percentage
def accuracy_metric(actual, predicted):
    correct = 0
    for i in range(len(actual)):
        if actual[i] == predicted[i]:
            correct += 1
    return correct / float(len(actual)) * 100.0
 
# Evaluate an algorithm using a cross validation split
def evaluate_algorithm(dataset, algorithm, n_folds, *args):
    folds = cross_validation_split(dataset, n_folds)
    scores = list()
    for fold in folds:
        train_set = list(folds)
        train_set.remove(fold)
        train_set = sum(train_set, [])
        test_set = list()
        for row in fold:
            row_copy = list(row)
            test_set.append(row_copy)
            row_copy[-1] = None
        predicted = algorithm(train_set, test_set, *args)
        actual = [row[-1] for row in fold]
        accuracy = accuracy_metric(actual, predicted)
        scores.append(accuracy)
    return scores
 
# Split a dataset based on an attribute and an attribute value
def test_split(index, value, dataset):
    left, right = list(), list()
    for row in dataset:
        if row[index] < value:
            left.append(row)
        else:
            right.append(row)
    return left, right
 
# Calculate the Gini index for a split dataset
def gini_index(groups, classes):
    # count all samples at split point
    n_instances = float(sum([len(group) for group in groups]))
    # sum weighted Gini index for each group
    gini = 0.0
    for group in groups:
        size = float(len(group))
        # avoid divide by zero
        if size == 0:
            continue
        score = 0.0
        # score the group based on the score for each class
        for class_val in classes:
            p = [row[-1] for row in group].count(class_val) / size
            score += p * p
        # weight the group score by its relative size
        gini += (1.0 - score) * (size / n_instances)
    return gini
 
# Select the best split point for a dataset
def get_split(dataset, n_features):
    class_values = list(set(row[-1] for row in dataset))
    b_index, b_value, b_score, b_groups = 999, 999, 999, None
    features = list()
    while len(features) < n_features:
        index = randrange(len(dataset[0])-1)
        if index not in features:
            features.append(index)
    for index in features:
        for row in dataset:
            groups = test_split(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}
 
# Create a terminal node value
def to_terminal(group):
    outcomes = [row[-1] for row in group]
    return max(set(outcomes), key=outcomes.count)
 
# Create child splits for a node or make terminal
def split(node, max_depth, min_size, n_features, depth):
    left, right = node['groups']
    del(node['groups'])
    # check for a no split
    if not left or not right:
        node['left'] = node['right'] = to_terminal(left + right)
        return
    # check for max depth
    if depth >= max_depth:
        node['left'], node['right'] = to_terminal(left), to_terminal(right)
        return
    # process left child
    if len(left) <= min_size:
        node['left'] = to_terminal(left)
    else:
        node['left'] = get_split(left, n_features)
        split(node['left'], max_depth, min_size, n_features, depth+1)
    # process right child
    if len(right) <= min_size:
        node['right'] = to_terminal(right)
    else:
        node['right'] = get_split(right, n_features)
        split(node['right'], max_depth, min_size, n_features, depth+1)
 
# Build a decision tree
def build_tree(train, max_depth, min_size, n_features):
    root = get_split(train, n_features)
    split(root, max_depth, min_size, n_features, 1)
    return root
 
# Make a prediction with a decision tree
def predict(node, row):
    if row[node['index']] < node['value']:
        if isinstance(node['left'], dict):
            return predict(node['left'], row)
        else:
            return node['left']
    else:
        if isinstance(node['right'], dict):
            return predict(node['right'], row)
        else:
            return node['right']
 
# Create a random subsample from the dataset with replacement
def subsample(dataset, ratio):
    sample = list()
    n_sample = round(len(dataset) * ratio)
    while len(sample) < n_sample:
        index = randrange(len(dataset))
        sample.append(dataset[index])
    return sample
 
# Make a prediction with a list of bagged trees
def bagging_predict(trees, row):
    predictions = [predict(tree, row) for tree in trees]
    return max(set(predictions), key=predictions.count)
 
# Random Forest Algorithm
def random_forest(train, test, max_depth, min_size, sample_size, n_trees, n_features):
    trees = list()
    for i in range(n_trees):
        sample = subsample(train, sample_size)
        tree = build_tree(sample, max_depth, min_size, n_features)
        trees.append(tree)
    predictions = [bagging_predict(trees, row) for row in test]
    return(predictions)
 
# Test the random forest algorithm
seed(2)
# load and prepare data
filename = 'sonar.all-data.csv'
dataset = load_csv(filename)
# convert string attributes to integers
for i in range(0, len(dataset[0])-1):
    str_column_to_float(dataset, i)
# convert class column to integers
str_column_to_int(dataset, len(dataset[0])-1)
# evaluate algorithm
n_folds = 5
max_depth = 10
min_size = 1
sample_size = 1.0
n_features = int(sqrt(len(dataset[0])-1))
for n_trees in [1, 5, 10]:
    scores = evaluate_algorithm(dataset, random_forest, n_folds, max_depth, min_size, sample_size, n_trees, n_features)
    print('Trees: %d' % n_trees)
    print('Scores: %s' % scores)
    print('Mean Accuracy: %.3f%%' % (sum(scores)/float(len(scores))))

使用k值5進行交叉驗證,給定每個倍數值為208/5 = 41.6或者在每次迭代中剛好超過40個記錄被計算。

構建深度樹的最大深度為10,每個節點的最小訓練行數為1。訓練資料集樣本的建立大小與原始資料集相同,這是隨機森林演算法的預設期望值。

在每個分割點處考慮的特徵的數量被設定為sqrt(num_features)或者sqrt(60)= 7.74被保留為7個特徵。

對一套有著3種不同數量的樹木(示例)進行評測在此過程中進行比較,結果表明隨著更多樹木的新增,(處理)技能也隨之提升。

執行該示例將列印每個摺疊的分數和每個配置的平均分數。

Trees: 1
Scores: [56.09756097560976, 63.41463414634146, 60.97560975609756, 58.536585365853654, 73.17073170731707]
Mean Accuracy: 62.439%
 
Trees: 5
Scores: [70.73170731707317, 58.536585365853654, 85.36585365853658, 75.60975609756098, 63.41463414634146]
Mean Accuracy: 70.732%
 
Trees: 10
Scores: [82.92682926829268, 75.60975609756098, 97.5609756097561, 80.48780487804879, 68.29268292682927]
Mean Accuracy: 80.976% 

擴充套件

本節列出了您可能有興趣探索的關於本教程的擴充套件。

  • 演算法優化。發現教程中使用的配置有一些試驗和錯誤,但沒有進行優化。嘗試更多的樹木,不同數量的特徵,甚至不同的樹形配置來提高效能。
  • 更多的問題。將該技術應用於其他分類問題,甚至將其應用於迴歸,具有新的成本函式和結合樹預測的新方法。

你有沒有嘗試這些擴充套件? 在下面的評論中分享你的經驗。

評論

在本教程中,您瞭解瞭如何從頭開始實現隨機森林演算法。

具體來說,你瞭解到:

  • 隨機森林和Bagged決策樹的區別。
  • 如何更新決策樹的建立以適應隨機森林過程。
  • 如何將隨機森林演算法應用於現實世界的預測建模問題。