Q:有些學(xué)習(xí)器強(qiáng),有些學(xué)習(xí)器弱,有沒有方法讓弱的學(xué)習(xí)器戰(zhàn)勝強(qiáng)的學(xué)習(xí)器?
所謂“弱學(xué)習(xí)器”,就是指性能比起隨機(jī)猜測(cè)高一點(diǎn)的學(xué)習(xí)器,比如弱分類器就是指分類準(zhǔn)確率比50%高一點(diǎn)的分類器。俗話說:“三個(gè)臭皮匠,賽過諸葛亮”。我們可以通過某種“合作制度”,使得多個(gè)弱學(xué)習(xí)器結(jié)合起來一起進(jìn)行分類(回歸),獲得甚至高于強(qiáng)學(xué)習(xí)器的學(xué)習(xí)性能。這被稱為集成學(xué)習(xí)(ensemble learning)。
Q:用于集成學(xué)習(xí)的每個(gè)個(gè)體學(xué)習(xí)器如何生成?
集成學(xué)習(xí)的個(gè)體學(xué)習(xí)器有兩類生成方式:串行生成和并行生成。串行生成指學(xué)習(xí)器一個(gè)接一個(gè)生成,后面的個(gè)體學(xué)習(xí)器基于前面的個(gè)體學(xué)習(xí)器的情況生成;并行生成是所有用到的學(xué)習(xí)器一起生成,不分先后。顯然串行方式相對(duì)來說比并行方式更耗時(shí)間。串行生成的代表算法是Boosting算法家族中的AdaBoost,并行生成的代表算法是Bagging算法,而其變體算法Random Rorest 最為著名和常用。
Q:AdaBoost算法的基本思想是怎樣的?
首先我們想一下現(xiàn)實(shí)生活中的經(jīng)歷——我們?cè)趯W(xué)習(xí)的時(shí)候,尤其是初高中學(xué)習(xí)模式中,總是會(huì)有聽課-測(cè)驗(yàn)-總結(jié)測(cè)驗(yàn)結(jié)果-重點(diǎn)學(xué)習(xí)測(cè)驗(yàn)中做錯(cuò)的知識(shí)點(diǎn)這樣的學(xué)習(xí)模式。
Boosting算法的基本思想和上面的學(xué)習(xí)模式很像。Boosting算法要做的事情就是用同一個(gè)訓(xùn)練數(shù)據(jù)集,訓(xùn)練出n個(gè)弱學(xué)習(xí)器,然后把這n個(gè)弱學(xué)習(xí)器整合成一個(gè)強(qiáng)學(xué)習(xí)器。其學(xué)習(xí)過程也是:首先根據(jù)原始的數(shù)據(jù)集訓(xùn)練出第一個(gè)學(xué)習(xí)器,然后根據(jù)學(xué)習(xí)器的學(xué)習(xí)結(jié)果,看看這個(gè)學(xué)習(xí)器在哪些樣本上犯了錯(cuò)。然后下一輪訓(xùn)練時(shí)給在上一輪犯錯(cuò)的樣本給予更多的關(guān)注,訓(xùn)練出一個(gè)新的學(xué)習(xí)器。
AdaBoost是Boosting算法家族中著名的算法之一。算法會(huì)對(duì)訓(xùn)練數(shù)據(jù)集里每一個(gè)樣本賦予一個(gè)權(quán)值。然后開始了n輪的訓(xùn)練,第一輪的時(shí)候每個(gè)樣本的權(quán)值都是均等的,假設(shè)訓(xùn)練數(shù)據(jù)集有m個(gè)樣本,則每個(gè)樣本權(quán)值都是
。用這些數(shù)據(jù)和權(quán)值可以訓(xùn)練出第一個(gè)學(xué)習(xí)器(一般是決策樹算法)。
用數(shù)學(xué)語言來描述的話,假設(shè)單個(gè)基學(xué)習(xí)器為其對(duì)應(yīng)的權(quán)重為
,則集成后的學(xué)習(xí)器為
Q:那么AdaBoost中每個(gè)基分類器亦即對(duì)應(yīng)的權(quán)重如何求得?
我們現(xiàn)在只考察二分類任務(wù)。在開始探究AdaBoost的算法流程之前,首先要搞清楚幾個(gè)概念,或者說在西瓜書里容易混淆的符號(hào):
-
—— 基學(xué)習(xí)器,用于集成學(xué)習(xí)器的基本元素,最常用的是單層決策樹。
-
—— 集成學(xué)習(xí)器中每個(gè)基分類器的權(quán)重
-
—— 集成學(xué)習(xí)器,多個(gè)基學(xué)習(xí)器預(yù)測(cè)結(jié)果的加權(quán)求和。
-
—— 樣本x的真實(shí)值(真實(shí)標(biāo)記)。
-
—— 訓(xùn)練數(shù)據(jù)集。
-
—— 第t輪訓(xùn)練中樣本x對(duì)應(yīng)的權(quán)重w。
-
—— 原分布
在當(dāng)前樣本權(quán)重下
形成的新分布。
AdaBoost是一個(gè)迭代算法,每一輪要做的事情是在當(dāng)前的樣本權(quán)重下,用訓(xùn)練數(shù)據(jù)
訓(xùn)練出一個(gè)基學(xué)習(xí)器
,亦即對(duì)應(yīng)的學(xué)習(xí)器權(quán)重
,最后確定下一輪學(xué)習(xí)的樣本權(quán)重
。
先看基分類器。
機(jī)器學(xué)習(xí)(更準(zhǔn)確一點(diǎn),參數(shù)化模型訓(xùn)練)的一個(gè)討論時(shí)設(shè)定一個(gè)損失函數(shù),然后最小化這個(gè)損失函數(shù),以獲得一組能使預(yù)測(cè)結(jié)果最好的模型參數(shù)。對(duì)于AdaBoost,損失函數(shù)被定義為:

那么第t輪訓(xùn)練中,基學(xué)習(xí)器應(yīng)當(dāng)能訓(xùn)練目標(biāo)就是和前面t-1個(gè)學(xué)習(xí)器一起令總體損失最?。?/p>

將上面公式展開,再變形,就能得到下面新的損失函數(shù),含義是在當(dāng)前樣本權(quán)重下使得分類器的損失最小。也可以理解為在新分布下最小化錯(cuò)誤率。本質(zhì)上就是關(guān)注前面幾輪都誤判的樣本,使得這一輪生成的分類器
能盡可能正確預(yù)測(cè)這些困難的樣本。

再看基分類器權(quán)重。
從上面分析可知,(t>2)的訓(xùn)練數(shù)據(jù)不再是呈原分布D,而是經(jīng)過權(quán)重
調(diào)整過后的新分布
。則
應(yīng)該使得
最小化指數(shù)損失函數(shù)

最后決定下一輪的樣本權(quán)重。權(quán)重更新的指導(dǎo)思想就是,這一輪被誤判的樣本,要在下一輪獲得更多的關(guān)注:

如此我們就有知道了訓(xùn)練一個(gè)AdaBoost集成學(xué)習(xí)器的全流程。貼一張AdaBoost算法總體流程的偽代碼:

更多關(guān)于Adaboost算法的知識(shí),比如應(yīng)用于回歸問題的AdaBoost,可以看看這篇博文。
Q:Bagging和Random Forest算法的大致內(nèi)容如何?
Bagging算法的思想很簡(jiǎn)單——假設(shè)現(xiàn)在有含m個(gè)樣本的樣本數(shù)據(jù)集D。對(duì)D進(jìn)行m次有放回抽樣,得到一個(gè)含m個(gè)樣本的訓(xùn)練數(shù)據(jù)集T。根據(jù)概率,D中有約63%的樣本被抽到T中(部分會(huì)重復(fù)出現(xiàn))。進(jìn)行數(shù)次這樣的操作,得到數(shù)個(gè)訓(xùn)練集。用這n個(gè)訓(xùn)練集可以訓(xùn)練出n個(gè)不同的個(gè)體學(xué)習(xí)器。
Random Forest算法是基于Bagging算法的擴(kuò)展——以決策樹為個(gè)體學(xué)習(xí)器的集成學(xué)習(xí)器算法。除此以外,一般的,單個(gè)的決策樹在構(gòu)建樹的節(jié)點(diǎn)時(shí),會(huì)在所有可選屬性中選擇最優(yōu)的屬性作為節(jié)點(diǎn)的劃分屬性。而隨機(jī)森林中的決策樹在構(gòu)建樹的節(jié)點(diǎn)時(shí),會(huì)先在所有可選屬性中隨機(jī)選擇k個(gè)屬性出來,然后選出這k個(gè)屬性中最優(yōu)的屬性作為當(dāng)前節(jié)點(diǎn)的劃分屬性。假設(shè)數(shù)據(jù)集一共有d個(gè)屬性,西瓜書推薦。顯然,隨機(jī)森林中的每一棵性能都比一般的決策樹性能弱,但是許多棵這樣的決策樹形成森林后,其總體性能則大大強(qiáng)于一般的決策樹了。
Q:現(xiàn)在有了多個(gè)個(gè)體學(xué)習(xí)器,有哪些“合作規(guī)則”讓多個(gè)弱學(xué)習(xí)器一起工作?
對(duì)于分類任務(wù),最簡(jiǎn)單的就是投票法,看看哪一個(gè)類別得到更多學(xué)習(xí)器的支持,就把待分類任務(wù)歸到哪一個(gè)類別,如果同時(shí)有兩個(gè)或以上類別得到同樣多票數(shù),則從這幾個(gè)類別中隨機(jī)選一個(gè),這叫相對(duì)投票法。也有絕對(duì)投票法,即把待分類樣本歸屬到得票多于半數(shù)的那一個(gè)類別。如果沒有一個(gè)類別得票多于半數(shù),則拒絕本次預(yù)測(cè),即待分類樣本不歸屬到任何一個(gè)類別。另外還有加權(quán)投票法,給每一個(gè)學(xué)習(xí)器加一個(gè)權(quán)值,表示各個(gè)學(xué)習(xí)器票數(shù)的重要度,再進(jìn)行絕對(duì)或相對(duì)投票。
對(duì)于預(yù)測(cè)任務(wù),或者說輸出是連續(xù)值的任務(wù),最簡(jiǎn)單的是用平均法,有簡(jiǎn)單平均法,即各個(gè)學(xué)習(xí)器的結(jié)果直接取平均;也可以用加權(quán)平均法,同樣給各個(gè)個(gè)體學(xué)習(xí)器加一個(gè)權(quán)值,然后將各個(gè)結(jié)果乘上相應(yīng)權(quán)值再取平均。
當(dāng)數(shù)據(jù)量足夠多時(shí),可以考慮更加強(qiáng)大的合作規(guī)則——學(xué)習(xí)法,亦即把各個(gè)個(gè)體學(xué)習(xí)器的輸出結(jié)果當(dāng)作一個(gè)新的數(shù)據(jù)集,通過一個(gè)新的學(xué)習(xí)算法得到一個(gè)新的學(xué)習(xí)器。如果把前兩種合作規(guī)則看作是一群人討論得出結(jié)果,那么“學(xué)習(xí)法”就是一群人把他們的知識(shí)經(jīng)驗(yàn)全部傳授給一個(gè)學(xué)生,由這個(gè)接受了多人功力的學(xué)生(實(shí)際上就是一個(gè)強(qiáng)學(xué)習(xí)器了)來做判斷。
學(xué)習(xí)法最典型的算法是stacking算法。假設(shè)現(xiàn)在有一個(gè)數(shù)據(jù)集D,先把數(shù)據(jù)集分成兩部分,一部分用來訓(xùn)練初級(jí)學(xué)習(xí)器(個(gè)體學(xué)習(xí)器),可以用于boosting或者bagging的訓(xùn)練方式。另一部分則作為輸入,喂給訓(xùn)練出來的每個(gè)個(gè)體學(xué)習(xí)器,從而得到輸出,形成一個(gè)新的數(shù)據(jù)集——這個(gè)新的數(shù)據(jù)集中每個(gè)樣本有n個(gè)屬性和1個(gè)標(biāo)記,對(duì)應(yīng)n個(gè)個(gè)體學(xué)習(xí)器的輸出結(jié)果,以及原來的喂給個(gè)體學(xué)習(xí)器的數(shù)據(jù)集中每個(gè)樣本的標(biāo)記。
用這個(gè)新的數(shù)據(jù)集作為訓(xùn)練集,訓(xùn)練出一個(gè)新的學(xué)習(xí)器,這個(gè)學(xué)習(xí)器就是最終的集成學(xué)習(xí)器。這個(gè)最終的學(xué)習(xí)器一般使用線性回歸算法來訓(xùn)練。
Talk is cheap, show me the code!
下面的代碼是一個(gè)只支持二分類任務(wù)的AdaBoost集成分類器。
"""
Simple AdaBoost Classifier
:file: ensemble.py
:author: Richy Zhu
:email: rickyzhu@foxmail.com
"""
import copy
import numpy as np
class MyAdaBoostClassifier:
def __init__(self, base_learner=None, max_learners=100):
'''
Create an AdaBoosted ensemble classifier
(currently only do binary classification)
Parameters
----------
base_learner: object
weak learner instance that must support sample weighting when
training suppport fit(X, y, sample_weight), predict(X),
and score(X, y) methods. By default use `DecisionTreeClassifier`
from scikit-learn package.
max_learners: int
maximum number of ensembled learners
'''
if base_learner is None:
from sklearn.tree import DecisionTreeClassifier
base_learner = DecisionTreeClassifier(max_depth=1)
# base learner must support sample weighting when training
self.learners = [copy.deepcopy(base_learner) for k in range(max_learners)]
self.alphas = [0] * max_learners # weights of each learner
def fit(self, X, y):
'''
Build an AdaBoosted ensemble classifier
Parameters
----------
X: ndarray of shape (m, n)
sample data where row represent sample and column represent feature
y: ndarray of shape (m,)
labels of sample data
Returns
-------
self
trained model
'''
# weights of each training sample
weights = np.ones(len(X)) * (1/len(X))
for k in range(len(self.learners)):
self.learners[k].fit(X, y, sample_weight=weights)
y_hat = self.learners[k].predict(X)
err_rate = 1 - self.learners[k].score(X,y)
if err_rate > 0.5:
break
alpha = 0.5 * np.log((1-err_rate)/(err_rate))
self.alphas[k] = alpha
weights = weights * np.exp(-alpha * y * y_hat)
weights /= sum(weights)
self.learners = self.learners[:k+1]
self.alphas = np.array(self.alphas[:k+1]).reshape([-1,1])
return self
def predict(self, X, func_sign=None):
'''
Make prediction by the trained model.
Parameters
----------
X: ndarray of shape (m, n)
data to be predicted, the same shape as trainning data
func_sign: function
sign function that convert weighted sums into labels
Returns
-------
C: ndarray of shape (m,)
Predicted class label per sample.
'''
if func_sign is None:
def func_sign(x):
return np.where(x<0, -1, 1)
Y = []
for learner in self.learners:
Y.append(learner.predict(X))
Y = np.array(Y)
# weighted sum of result from each classifier
y = np.sum(Y * self.alphas, axis=0)
return func_sign(y)
測(cè)試代碼如下,對(duì)于比scikit-learn的模型效果更好,我也很驚奇。但sklearn的模型能處理更穩(wěn)定,也能處理更多情況,這一點(diǎn)還是要服產(chǎn)品級(jí)代碼。
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from ensemble import *
print('\nAdaBoost')
print('---------------------------------------------------------------------')
X, y = make_classification(n_samples=1000, n_features=4,
n_informative=2, n_redundant=0,
random_state=0, shuffle=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
y_train[y_train==0] = -1
y_test[y_test==0] = -1
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(max_depth=1)
myada = MyAdaBoostClassifier(tree)
myada.fit(X_train, y_train)
print('My AdaBoost:', accuracy_score(y_test, myada.predict(X_test)))
from sklearn.ensemble import AdaBoostClassifier
skada = AdaBoostClassifier(n_estimators=100, random_state=0)
skada.fit(X_train, y_train)
print('Sk AdaBoost:', accuracy_score(y_test, skada.predict(X_test)))
結(jié)果如下
$ python ensemble_examples.py
AdaBoost
---------------------------------------------------------------------
My AdaBoost: 0.952
Sk AdaBoost: 0.936
相比于AdaBoost那看上去比較復(fù)雜的數(shù)學(xué)公式實(shí)現(xiàn),Random Forest的機(jī)制更加清晰簡(jiǎn)單,代碼編寫也更加容易(順利...)
"""
Simple Random Forest, use decision tree model in scikit-learn package.
:file: ensemble.py
:author: Richy Zhu
:email: rickyzhu@foxmail.com
"""
import copy
import numpy as np
from collections import Counter
class MyRandomForestClassifier:
def __init__(self, n_learners=100):
from sklearn.tree import DecisionTreeClassifier
base_tree = DecisionTreeClassifier(max_features='log2')
self.learners = [copy.deepcopy(base_tree) for k in range(n_learners)]
def fit(self, X, y):
'''
Build an random forest classifier
Parameters
----------
X: ndarray of shape (m, n)
sample data where row represent sample and column represent feature
y: ndarray of shape (m,)
labels of sample data
Returns
-------
self
trained model
'''
for learner in self.learners:
# sampling len(X) times with replacement
new_index = np.random.choice(range(len(X)), len(X), replace=True)
new_X = X[new_index]
new_y = y[new_index]
learner.fit(new_X, new_y)
return self
def predict(self, X):
'''
Make prediction by the trained model.
Parameters
----------
X: ndarray of shape (m, n)
data to be predicted, the same shape as trainning data
func_sign: function
sign function that convert weighted sums into labels
Returns
-------
C: ndarray of shape (m,)
Predicted class label per sample.
'''
Y = []
y = []
for learner in self.learners:
Y.append(learner.predict(X))
Y = np.array(Y)
for i in range(Y.shape[1]):
counter = Counter(Y[:, i])
y.append(counter.most_common(1)[0][0])
return np.array(y)
測(cè)試一下
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from ensemble import *
print('\nRandom Forest')
print('---------------------------------------------------------------------')
X, y = make_classification(n_samples=1000, n_features=4,
n_informative=2, n_redundant=0,
random_state=0, shuffle=False)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
y_train[y_train==0] = -1
y_test[y_test==0] = -1
myforest = MyRandomForestClassifier()
myforest.fit(X_train, y_train)
# print(myforest.predict(X_test))
print('My forest:', accuracy_score(y_test, myforest.predict(X_test)))
from sklearn.ensemble import RandomForestClassifier
skforest = RandomForestClassifier()
skforest.fit(X_train, y_train)
print('SK forest:', accuracy_score(y_test, skforest.predict(X_test)))
結(jié)果如下
$ python ensemble_examples.py
Random Forest
---------------------------------------------------------------------
My forest: 0.952
SK forest: 0.96
更多代碼請(qǐng)參考https://github.com/qige96/programming-practice/tree/master/machine-learning
本作品首發(fā)于簡(jiǎn)書 和 博客園平臺(tái),采用知識(shí)共享署名 4.0 國際許可協(xié)議進(jìn)行許可。