作者:chen_h
微信號(hào) & QQ:862251340
微信公眾號(hào):coderpai
(一)機(jī)器學(xué)習(xí)中的集成學(xué)習(xí)入門
(二)bagging 方法
(三)使用Python進(jìn)行交易的隨機(jī)森林算法
(四)Python中隨機(jī)森林的實(shí)現(xiàn)與解釋
(五)如何用 Python 從頭開始實(shí)現(xiàn) Bagging 算法
決策樹是一種簡(jiǎn)單而強(qiáng)大的預(yù)測(cè)建模技術(shù),但它們存在高方差。這意味著在給定不同的訓(xùn)練數(shù)據(jù)的情況下,樹可以得到非常不同的結(jié)果。為了使決策樹更加健壯并實(shí)現(xiàn)更好性能,我們會(huì)采用集成學(xué)習(xí)方法,其中一種是 Bagging 方法。
在本教程中,您將了解如何使用 Python從頭開始使用決策樹的 bagging 過程。完成本教程后,您將了解:
- 如何創(chuàng)建數(shù)據(jù)集的自舉過程;
- 如何使用自舉模型進(jìn)行預(yù)測(cè);
- 如何將 bagging 算法應(yīng)用到你的預(yù)測(cè)模型中;
Bootstrap Aggregation 算法
Bootstrap 是一種有放回的數(shù)據(jù)采集方式。這還意味著一個(gè)新的數(shù)據(jù)集是從原來(lái)數(shù)據(jù)中進(jìn)行隨機(jī)采用得到的,并且會(huì)把數(shù)據(jù)進(jìn)行放回,然后進(jìn)行下一次采樣。
當(dāng)我們?cè)诠浪阋粋€(gè)非常龐大的數(shù)據(jù)集的時(shí)候,這種估算方式是非常好的。我們可以通過計(jì)算一個(gè)有限集合的均值從而來(lái)得到整個(gè)數(shù)據(jù)集的均值。這種方法我們一般都是和一些具有高方差的算法一起使用,比如決策樹。我們通過對(duì)每個(gè)自舉樣本進(jìn)行單獨(dú)模型計(jì)算,然后輸出多個(gè)模型結(jié)果的平均值。這種技術(shù)稱為 bootstrap 或者 bagging。
方差意味著算法的性能對(duì)訓(xùn)練數(shù)據(jù)敏感,高方差表明訓(xùn)練數(shù)據(jù)的變化越多,算法的性能就越差。我們可以通過訓(xùn)練許多樹并且取其預(yù)測(cè)的平均值,可以改善諸如未修剪的決策樹之類的高方差機(jī)器學(xué)習(xí)算法的性能。模型取得的結(jié)果通常會(huì)優(yōu)于單個(gè)決策樹的表現(xiàn)。
除了提高性能之外,bagging 的另一個(gè)好處是它不會(huì)過度擬合問題,我們可以通過繼續(xù)添加樹木,知道達(dá)到最佳性能。
Sonar 數(shù)據(jù)集
在本教程中我們使用的是 Sonar 數(shù)據(jù)集。這是一個(gè)描述聲吶信號(hào)從不同表面反彈的數(shù)據(jù)集。輸入數(shù)據(jù)是由 60 個(gè)特征數(shù)據(jù)組成的,輸出數(shù)據(jù)是一個(gè)二分類,來(lái)判斷物體表面是巖石還是金屬圓柱。數(shù)據(jù)一共有 208 條。這是一個(gè)非常簡(jiǎn)單的數(shù)據(jù)集。所有的輸入變量都是連續(xù)的,取值在 0 到 1 之間。輸出變量是 M(金屬圓柱) 和 R(巖石),我們需要將這個(gè)分類結(jié)果轉(zhuǎn)變成 1 和 0。數(shù)據(jù)我們通過 UCI Machine Learing 進(jìn)行下載。下載鏈接:https://archive.ics.uci.edu/ml/datasets/Connectionist+Bench+(Sonar,+Mines+vs.+Rocks)
實(shí)戰(zhàn)例子
本教程分為兩部分:
- Bootstrap 采樣;
- 聲吶數(shù)據(jù)分析;
這些步驟提供了數(shù)據(jù)采樣和算法編寫的基本功能,我們可以學(xué)習(xí)bagging算法是如何進(jìn)行基礎(chǔ)工作的。
1. Bootstrap 采樣
讓我們首先深入了解 bootstrap 方法的工作原理。
我們可以通過從數(shù)據(jù)集中隨機(jī)選擇行數(shù)據(jù),并將它們添加到新列表來(lái)創(chuàng)建數(shù)據(jù)集成為新樣本。我們可以針對(duì)固定數(shù)量的行重復(fù)進(jìn)行此操作,或者知道新數(shù)據(jù)集的大小與原始數(shù)據(jù)集的大小的比率達(dá)到我們的要求。我們每采集一次數(shù)據(jù),都會(huì)進(jìn)行放回,然后再次采集。
下面是一個(gè)名為 subsample() 的函數(shù),它實(shí)現(xiàn)了這個(gè)過程。隨機(jī)模塊中的 randrange() 函數(shù)用于選擇隨機(jī)行索引,以便在循環(huán)的每次迭代中添加到樣本中。樣本的默認(rèn)數(shù)量大小是原始數(shù)據(jù)集的大小。
def
subsample
(
dataset
,
ratio
=
1.0
)
:
sample
=
list
(
)
n_sample
=
round
(
len
(
dataset
)
*
ratio
)
while
len
(
sample
)
<
n_sample
:
index
=
randrange
(
len
(
dataset
)
)
sample
.
append
(
dataset
[
index
]
)
return
sample
我們可以使用這個(gè)函數(shù)來(lái)評(píng)估一個(gè)人造的數(shù)據(jù)集的平均值。
首先,我們創(chuàng)建一個(gè)包含 20 行,里面的數(shù)字時(shí) 0 到 9 之間的隨機(jī)值,并且我們計(jì)算他們的平均值。
然后,我們可以制作原始數(shù)據(jù)集的自舉樣本集,我們不斷重復(fù)這個(gè)過程,直到我們有一個(gè)均值列表,然后計(jì)算平均值。這個(gè)平均值跟我們整個(gè)樣本的平均值是非常接近的。
下面列出了一個(gè)完整的示例。
每個(gè)自舉樣本是原始樣本的 10 %,也就是 2 個(gè)樣本。然后,我們通過創(chuàng)建原始數(shù)據(jù)集的 1個(gè),10個(gè),100個(gè)自舉樣本,計(jì)算他們的平均值,然后平均所有這些估計(jì)的平均值來(lái)進(jìn)行實(shí)驗(yàn)。
from
random
import
seed
from
random
import
random
from
random
import
randrange
# Create a random subsample from the dataset with replacement
def
subsample
(
dataset
,
ratio
=
1.0
)
:
sample
=
list
(
)
n_sample
=
round
(
len
(
dataset
)
*
ratio
)
while
len
(
sample
)
<
n_sample
:
index
=
randrange
(
len
(
dataset
)
)
sample
.
append
(
dataset
[
index
]
)
return
sample
# Calculate the mean of a list of numbers
def
mean
(
numbers
)
:
return
sum
(
numbers
)
/
float
(
len
(
numbers
)
)
seed
(
1
)
# True mean
dataset
=
[
[
randrange
(
10
)
]
for
i
in
range
(
20
)
]
print
(
'True Mean: %.3f'
%
mean
(
[
row
[
0
]
for
row
in
dataset
]
)
)
# Estimated means
ratio
=
0.10
for
size
in
[
1
,
10
,
100
]
:
sample_means
=
list
(
)
for
i
in
range
(
size
)
:
sample
=
subsample
(
dataset
,
ratio
)
sample_mean
=
mean
(
[
row
[
0
]
for
row
in
sample
]
)
sample_means
.
append
(
sample_mean
)
print
(
'Samples=%d, Estimated Mean: %.3f'
%
(
size
,
mean
(
sample_means
)
)
)
運(yùn)行該示例將打印我們要估計(jì)的原始數(shù)據(jù)平均值。
然后我們可以從各種不同數(shù)量的自舉樣本中看到估計(jì)的平均值。我們可以看到,通過 100 個(gè)樣本,我們可以很好的估計(jì)平均值。
True
Mean
:
4.450
Samples
=
1
,
Estimated Mean
:
4.500
Samples
=
10
,
Estimated Mean
:
3.300
Samples
=
100
,
Estimated Mean
:
4.480
我們可以為每個(gè)子樣本創(chuàng)建一個(gè)模型,而不是簡(jiǎn)單的計(jì)算平均值。
接下來(lái),讓我們看看如何組合多個(gè) bootstrap 模型的預(yù)測(cè)。
2. 聲吶數(shù)據(jù)集案例研究
在這一節(jié)中,我們將隨機(jī)森林算法應(yīng)用于聲吶數(shù)據(jù)集。
首先,我們需要導(dǎo)入數(shù)據(jù)集,將字符串值轉(zhuǎn)換為數(shù)值型,并將輸出列從字符串轉(zhuǎn)換為 0 和 1 的整數(shù)值。這是通過輔助函數(shù) load_csv() ,str_column_to_float() 和 str_column_to_int() 來(lái)實(shí)現(xiàn)的,以便預(yù)處理數(shù)據(jù)集。
我們將使用 k-fold 交叉驗(yàn)證來(lái)估計(jì)學(xué)習(xí)模型在未知數(shù)據(jù)上的性能。這意味著我們將構(gòu)建和評(píng)估 k 個(gè)模型,并將性能估計(jì)為平均模型誤差。分類精度將評(píng)估每個(gè)模型,這些算法都在 cross_validation_split() ,accuracy_metric() 和 evaluate_algoritm() 函數(shù)中得到解決。
我們使用 CART 算法來(lái)實(shí)現(xiàn)我們的 bagging 過程,在實(shí)現(xiàn)的過程中我們還設(shè)計(jì)了一些輔助函數(shù):test_split() 函數(shù)將數(shù)據(jù)集拆分成組,gini_index() 用于評(píng)估拆分點(diǎn),get_split() 用于查找最佳拆分點(diǎn),to_terminal(),split() 和 build_tree() 用于創(chuàng)建單個(gè)決策樹,predict() 用于使用決策樹進(jìn)行預(yù)測(cè),并使用前一步驟中描述的 subsample() 函數(shù)來(lái)創(chuàng)建訓(xùn)練的子樣本訓(xùn)練集。
我們還開發(fā)了一個(gè) bagging_predict() 函數(shù),該函數(shù)負(fù)責(zé)使用每個(gè)決策樹進(jìn)行預(yù)測(cè)并將預(yù)測(cè)組合成單個(gè)返回值。這是 bagging 方法常用的一種模式。
最后,我們?cè)O(shè)計(jì)一個(gè)新的 bagging() 函數(shù),負(fù)責(zé)創(chuàng)建訓(xùn)練數(shù)據(jù)集的樣本,在每個(gè)樣本上訓(xùn)練決策樹,然后使用bagging() 列表對(duì)測(cè)試數(shù)據(jù)集進(jìn)行預(yù)測(cè)。
下面給出了完整代碼:
# Bagging Algorithm on the Sonar dataset
from
random
import
seed
from
random
import
randrange
from
csv
import
reader
# 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 integer
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
)
:
class_values
=
list
(
set
(
row
[
-
1
]
for
row
in
dataset
)
)
b_index
,
b_value
,
b_score
,
b_groups
=
999
,
999
,
999
,
None
for
index
in
range
(
len
(
dataset
[
0
]
)
-
1
)
:
for
row
in
dataset
:
# for i in range(len(dataset)):
# row = dataset[randrange(len(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
,
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
)
split
(
node
[
'left'
]
,
max_depth
,
min_size
,
depth
+
1
)
# process right child
if
len
(
right
)
<=
min_size
:
node
[
'right'
]
=
to_terminal
(
right
)
else
:
node
[
'right'
]
=
get_split
(
right
)
split
(
node
[
'right'
]
,
max_depth
,
min_size
,
depth
+
1
)
# Build a decision tree
def
build_tree
(
train
,
max_depth
,
min_size
)
:
root
=
get_split
(
train
)
split
(
root
,
max_depth
,
min_size
,
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
)
# Bootstrap Aggregation Algorithm
def
bagging
(
train
,
test
,
max_depth
,
min_size
,
sample_size
,
n_trees
)
:
trees
=
list
(
)
for
i
in
range
(
n_trees
)
:
sample
=
subsample
(
train
,
sample_size
)
tree
=
build_tree
(
sample
,
max_depth
,
min_size
)
trees
.
append
(
tree
)
predictions
=
[
bagging_predict
(
trees
,
row
)
for
row
in
test
]
return
(
predictions
)
# Test bagging on the sonar dataset
seed
(
1
)
# load and prepare data
filename
=
'sonar.all-data.csv'
dataset
=
load_csv
(
filename
)
# convert string attributes to integers
for
i
in
range
(
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
=
6
min_size
=
2
sample_size
=
0.50
for
n_trees
in
[
1
,
5
,
10
,
50
]
:
scores
=
evaluate_algorithm
(
dataset
,
bagging
,
n_folds
,
max_depth
,
min_size
,
sample_size
,
n_trees
)
print
(
'Trees: %d'
%
n_trees
)
print
(
'Scores: %s'
%
scores
)
print
(
'Mean Accuracy: %.3f%%'
%
(
sum
(
scores
)
/
float
(
len
(
scores
)
)
)
)
k 值為 5 時(shí)用于交叉驗(yàn)證,每次迭代評(píng)估的數(shù)據(jù)量為 208/5 = 41.6 或者直接使用 40 條進(jìn)行循環(huán)迭代。
構(gòu)建樹的最大深度,我們?cè)O(shè)為 6,每個(gè)節(jié)點(diǎn)為 2 的最小訓(xùn)練行數(shù)。訓(xùn)練數(shù)據(jù)集的樣本創(chuàng)建為原始數(shù)據(jù)集大小的 50% 。這是為了強(qiáng)制用于訓(xùn)練每棵樹的訓(xùn)練集子樣本中的某些變體。bagging 的默認(rèn)設(shè)置是使樣本數(shù)據(jù)集的大小與原始訓(xùn)練數(shù)據(jù)集的大小相匹配。
接下來(lái)我們打印每個(gè)類別的結(jié)果:
Trees
:
1
Scores
:
[
87.8048780487805
,
65.85365853658537
,
65.85365853658537
,
65.85365853658537
,
73.17073170731707
]
Mean Accuracy
:
71.707
%
Trees
:
5
Scores
:
[
60.97560975609756
,
80.48780487804879
,
78.04878048780488
,
82.92682926829268
,
63.41463414634146
]
Mean Accuracy
:
73.171
%
Trees
:
10
Scores
:
[
60.97560975609756
,
73.17073170731707
,
82.92682926829268
,
80.48780487804879
,
68.29268292682927
]
Mean Accuracy
:
73.171
%
Trees
:
50
Scores
:
[
63.41463414634146
,
75.60975609756098
,
80.48780487804879
,
75.60975609756098
,
85.36585365853658
]
Mean Accuracy
:
76.098
%
這種方法的一個(gè)難點(diǎn)是,即使我們構(gòu)建了一定深度的樹,但是 bagging 樹得到的結(jié)果也是非常相似的。但是我們希望在訓(xùn)練的過程中可以降低高方差。這是因?yàn)槲覀冊(cè)跇?gòu)造的過程中選擇了相同或者相似的分裂節(jié)點(diǎn),這是一種貪婪算法。
本教程試圖通過約束用于訓(xùn)練每棵樹的樣本大小來(lái)重新計(jì)算方差。更強(qiáng)大的技術(shù)是約束在創(chuàng)建每個(gè)分割點(diǎn)時(shí)對(duì)特征的評(píng)估。這是隨機(jī)森林中使用的方法。
擴(kuò)展
-
調(diào)整樹:調(diào)整樹的大小,深度,以及單個(gè)樹的配置;
-
bagging 中構(gòu)建不同的樹結(jié)構(gòu):我們可以通過使用不同的算法進(jìn)行平均預(yù)測(cè),比如貝葉斯,決策樹,神經(jīng)網(wǎng)絡(luò)等等;
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
