本文始發於個人公眾號:
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沒有的優勢,因為隨機性,使得我們可能會
發掘一些常規方法無法發現的特徵組合
或者是樣本當中獨特的資料分佈。這也是為什麼經過合適的調參之後,隨機森林往往能夠獲得非常好的效果。
關於隨機森林的介紹到這裡就結束了,如果喜歡本文,可以的話,請
點個關注
,給我一點鼓勵,也方便獲取更多文章。