本文始發於個人公眾號:

TechFlow,原創不易,求個關注

今天是

機器學習專題

的第26篇文章,我們一起聊聊另外一個整合學習模型,它就是大名鼎鼎的隨機森林。

隨機森林在業內名氣和使用範圍都很廣,曾經在許多演算法比賽當中拔得頭籌。另外,它也是一個透過組合多個弱分類器構建強分類器的經典模型,因此它在業內

廣受歡迎

本文基於決策樹相關的文章,沒有閱讀過的同學可以從

最上方的專輯

檢視過往決策樹相關的文章。

演算法原理

上一篇文章介紹AdaBoost的時候把整合學習的幾種思路大概介紹了一遍,整合學習主要是三種方法,

Bagging、Boosting和Stacking

。AdaBoost屬於Boosting,而隨機森林則屬於Bagging。

Bagging最大的特點就是“民主”,其中的思路很容易理解,針對同一份訓練資料我們會訓練出多個弱分類器來。當面臨一個新的樣本的時候,由這些分類器聚集在一起進行民主投票,

每一個分類器都是平等的,它們的權重一樣

。最後,根據所有這些弱分類器的結果整合出最後的結果來。比如說我們一共訓練了50個弱分類器,在面對一個樣本的時候,其中的35個給出類別1,15個給出類別0,那麼整個模型的結果就是類別1。

另外一點是,Bagging在訓練每一個分類器的時候所使用的資料並不是固定的,而是透過

有放回抽樣

選擇出來的。所以樣本在訓練集中出現的分佈也是各不相同的,有些樣本可能在同一個分類器中出現了多次,同樣也有一些樣本沒有出現過。單單有樣本隨機性還是不夠的,因為訓練樣本的分佈大體上是差不多的,雖然我們隨機取樣,也不會使得每一份訓練樣本會有很大的差距。所以為了保證每個分類器的側重點不同,擁有更強的隨機性,我們還可以從特徵入手,限制

每個分類器只能隨機使用部分特徵

。這樣得到的分類器每一個的特徵都各不相同,側重點也就不同,可以儘可能地增強隨機的效果,讓模型專注於資料。

隨機森林這個模型的名字當中隱藏了兩個關鍵點,分別是隨機和森林。隨機我們已經解釋過了,一方面是每一個分類器樣本的隨機,另外一個是分類器可以使用的特徵的隨機。而森林也很好理解,因為我們使用的分類器是決策樹,所以多棵決策“樹”組成的模型,自然就是森林了。

抓住這兩個特徵,隨機森林很好理解,也很好實現,畢竟決策樹模型我們之前已經實現過好幾次了。

程式碼實現

我們選擇決策樹當中最經典的CART演算法來實現決策樹,資料我們依然沿用上次AdaBoost模型當中乳腺癌預測的資料。

首先,我們還是一樣,先讀入資料:

import

numpy

as

np

import

pandas

as

pd

from

sklearn。datasets

import

load_breast_cancer

breast

=

load_breast_cancer

()

X

y

=

breast

data

breast

target

# reshape,將一維向量轉成二維

y

=

y

reshape

((

-

1

1

))

之後我們利用sklearn庫中的train_test_split工具將資料拆分成訓練資料和測試資料。

from

sklearn。model_selection

import

train_test_split

X_train

X_test

y_train

y_test

=

train_test_split

X

y

test_size

=

0。2

random_state

=

23

隨機森林的核心結構依然是決策樹,我們可以直接

沿用之前CART決策樹的程式碼

,只需要稍稍做一些小小的改動即可。首先是計算GIni指數和根據所選的特徵拆分資料的函式,這兩個函式不需要做任何改動,我們從之前的文章當中複製過來。

from

collections

import

Counter

def

gini_index

dataset

):

dataset

=

np

array

dataset

n

=

dataset

shape

0

if

n

==

0

return

0

counter

=

Counter

dataset

[:,

-

1

])

ret

=

1。0

for

k

v

in

counter

items

():

ret

-=

v

/

n

**

2

return

ret

def

split_gini

dataset

idx

threshold

):

left

right

=

[],

[]

n

=

dataset

shape

0

# 根據閾值拆分,拆分之後計算新的Gini指數

for

data

in

dataset

if

data

idx

<=

threshold

left

append

data

else

right

append

data

left

right

=

np

array

left

),

np

array

right

# 拆分成兩半之後,乘上所佔的比例

return

left

shape

0

/

n

*

gini_index

left

+

right

shape

0

/

n

*

gini_index

right

然後是拆分資料集,我們根據特徵和閾值將資料集拆分成兩個部分。和上面的split_gini函式類似,只是split_gini函式計算的是拆分之後的Gini指數,而我們現在開發的是

將資料集拆分

的功能。

from

collections

import

defaultdict

def

split_dataset

dataset

idx

thread

):

splitData

=

defaultdict

list

# 否則根據閾值劃分,分成兩類大於和小於

for

data

in

dataset

splitData

data

idx

<=

thread

append

np

delete

data

idx

))

return

list

splitData

values

()),

list

splitData

keys

())

再然後是獲取所有閾值以及選擇最佳的特徵和拆分閾值的函式,這兩個函式的邏輯和之前也完全一樣,我們也複製過來。

def

get_thresholds

X

idx

):

# numpy多維索引用法

new_data

=

X

[:,

idx

-

1

]]

tolist

()

# 根據特徵值排序

new_data

=

sorted

new_data

key

=

lambda

x

x

0

],

reverse

=

True

base

=

new_data

0

][

1

threads

=

[]

for

i

in

range

1

len

new_data

)):

f

l

=

new_data

i

# 如果label變化則記錄

if

l

!=

base

base

=

l

threads

append

f

return

threads

def

choose_feature_to_split

dataset

):

n

=

len

dataset

0

])

-

1

m

=

len

dataset

# 記錄最佳Gini,特徵和閾值

bestGini

=

float

‘inf’

feature

=

-

1

thred

=

None

for

i

in

range

n

):

threds

=

get_thresholds

dataset

i

for

t

in

threds

# 遍歷所有的閾值,計算每個閾值的Gini

gini

=

split_gini

dataset

i

t

if

gini

<

bestGini

bestGini

feature

thred

=

gini

i

t

return

feature

thred

再然後就是建樹的部分了,這裡我們做了一點小小的調整。因為我們

只使用了一部分訓練資料以及一部分特徵

訓練的決策樹,所以一般不會出現過擬合的情況,所以我去掉了防止過擬合的預剪枝邏輯。另外一個變動是加上了backup的設定,因為特徵和資料的稀疏,

可能會出現某個節點只有一個分支的情況

。為了防止這種情況,我在每個節點都儲存了一個backup,它代表的當前節點的資料中出現次數最多的那個類別。

def

create_decision_tree

dataset

):

dataset

=

np

array

dataset

# 如果都是一類,那麼直接返回類別

counter

=

Counter

dataset

[:,

-

1

])

if

len

counter

==

1

return

dataset

0

-

1

# 如果已經用完了所有特徵

if

dataset

shape

1

==

1

return

counter

most_common

1

)[

0

][

0

# 記錄最佳拆分的特徵和閾值

fidx

th

=

choose_feature_to_split

dataset

node

=

{

‘threshold’

th

‘feature’

fidx

‘backup’

counter

most_common

1

)[

0

][

0

]}

split_data

vals

=

split_dataset

dataset

fidx

th

for

data

val

in

zip

split_data

vals

):

node

val

<=

th

=

create_decision_tree

data

return

node

再然後是兩個工具函式,第一個是單棵子樹對於單個樣本的預測函式,第二個是單個子樹對於批次樣本的預測函式。整體的邏輯和之前大同小異,我們直接看程式碼即可:

def

subtree_classify

node

data

):

key

=

node

‘feature’

pred

=

None

thred

=

node

‘threshold’

# 如果當前的分支沒有出現過,那麼返回backup

if

data

key

<

thred

not

in

node

return

node

‘backup’

if

isinstance

node

data

key

<

thred

],

dict

):

pred

=

subtree_classify

node

data

key

<

thred

],

data

else

pred

=

node

data

key

<

thred

# 防止pred為空,挑選一個葉子節點作為替補

if

pred

is

None

for

key

in

node

if

not

isinstance

node

key

],

dict

):

pred

=

node

key

break

return

pred

def

subtree_predict

node

X

):

y_pred

=

[]

# 遍歷資料,批次預測

for

x

in

X

y

=

subtree_classify

node

x

y_pred

append

y

return

np

array

y_pred

到這裡其實都是決策樹的部分,決策樹實現了之後,構建森林的部分非常簡單。只做了一件事,就是

隨機樣本和特徵,然後用隨機出的樣本和特徵建立新的決策樹

並進行記錄。

首先我們來看隨機樣本和特徵的函式:

def

get_random_sample

X

y

):

rnd_idx

=

np

random

choice

range

X

shape

0

]),

350

replace

=

True

ft_idx

=

np

random

choice

range

X

shape

1

]),

15

replace

=

False

x_ret

=

X

rnd_idx

][:,

ft_idx

# 將類別拼接到X當中

x_ret

=

np

hstack

((

x_ret

y

rnd_idx

]))

# 對特徵的下標進行排序

ft_idx

sort

()

return

x_ret

np

array

ft_idx

在這個函式當中,我們設定了每次訓練新的決策樹的時候,使用的樣本數量是350條,特徵數量是15個。透過這種方式保證構建的決策樹的隨機性。

隨機樣本搞定了之後,構建森林的程式碼就很簡單了,透過一重迴圈建立樹並記錄即可:

trees_num

=

20

subtrees

=

[]

for

i

in

range

trees_num

):

X_rnd

features

=

get_random_sample

X_train

y_train

node

=

create_decision_tree

X_rnd

# 記錄下創建出來的決策樹與用到的特徵

subtrees

append

((

node

features

))

我們在建立樹的時候隨機使用了一些特徵,我們在

後續預測的時候需要用同樣的特徵去預測

。所以我們需要記錄下每一棵樹用到了哪些特徵值,並且決策樹當中記錄的只是特徵的下標,所以我們在記錄之前需要先對這些特徵進行排序。

這些函式都實現之後就是predict方法了,根據Bagging的定義,對於每一個樣本,我們都需要獲取每一棵決策樹的分類。然後選擇其中數量最多的那個作為最終的預測結果,這段邏輯也不復雜,相信大家觀看程式碼都能想明白。

def

predict

X

trees

):

y_pred

=

[]

for

x

in

X

ret

=

[]

for

tree

features

in

trees

ret

append

subtree_classify

tree

x

features

]))

ret

=

Counter

ret

y_pred

append

ret

most_common

1

)[

0

][

0

])

return

np

array

y_pred

最後,我們在測試集上驗證一下模型的效果:

機器學習——動手從決策樹實現隨機森林

準確率65。7%,看起來並不高,和我們預想中有些差距,我們來看下森林當中單棵樹的效果:

機器學習——動手從決策樹實現隨機森林

會發現有些樹的準確率只有30%多,都沒有達到50%的理論值。這當中的原因很多,一方面是我們選擇特徵的隨機性,導致了我們可能選出來一些效果比較差的特徵。另外一方面和我們訓練的樣本數量也有關係,在這個例子當中,我們

可以使用的樣本數量太少了

,所以隨機性很大,偏差自然也不小。

另外我們可以看下我們呼叫sklearn當中的隨機森林的效果,我們同樣設定森林中決策樹的數量是40,並且選擇Gini指數作為劃分樣本的依據。

機器學習——動手從決策樹實現隨機森林

我們可以看到它的準確率甚至比我們自己開發的還要低,這也是很正常的,畢竟

模型不是普適的

,會存在一些場景下不太適用或者是效果不好的情況,這是非常正常的。

總結

隨機森林模型最大的特點是隨機,其實其中的原理和AdaBoost非常接近,我們透過隨機樣本和特徵來保證模型的隨機性,以及保證這樣訓練得到的模型是一個弱分類器。弱分類器除了效果弱之外,還有很大的一個特點就是

不能過擬合

,也就是說它們都是欠擬合的模型,自然不可能過擬合。我們透過整合大量弱分類器獲得強大的分類效果。

和AdaBoost比起來,

隨機森林的隨機性更強

,並且對於引數的依賴更高,森林中決策樹的數量,每一棵決策樹需要使用的特徵數量,以及剪枝的策略等等。這些因素都會影響到最終模型的效果,但它也有AdaBoost沒有的優勢,因為隨機性,使得我們可能會

發掘一些常規方法無法發現的特徵組合

或者是樣本當中獨特的資料分佈。這也是為什麼經過合適的調參之後,隨機森林往往能夠獲得非常好的效果。

關於隨機森林的介紹到這裡就結束了,如果喜歡本文,可以的話,請

點個關注

,給我一點鼓勵,也方便獲取更多文章。

機器學習——動手從決策樹實現隨機森林