《機(jī)器學(xué)習(xí)(周志華)》學(xué)習(xí)筆記(八)

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)值w_i(0<w_i<1)。然后開始了n輪的訓(xùn)練,第一輪的時(shí)候每個(gè)樣本的權(quán)值都是均等的,假設(shè)訓(xùn)練數(shù)據(jù)集有m個(gè)樣本,則每個(gè)樣本權(quán)值都是\frac{1}{m}。用這些數(shù)據(jù)和權(quán)值可以訓(xùn)練出第一個(gè)學(xué)習(xí)器(一般是決策樹算法)。

用數(shù)學(xué)語言來描述的話,假設(shè)單個(gè)基學(xué)習(xí)器為h_t(\vec{x})其對(duì)應(yīng)的權(quán)重為\alpha_t,則集成后的學(xué)習(xí)器為

H(\vec{x}) = \sum_t{\alpha_t h_t(\vec{x})}

Q:那么AdaBoost中每個(gè)基分類器亦即對(duì)應(yīng)的權(quán)重如何求得?

我們現(xiàn)在只考察二分類任務(wù)。在開始探究AdaBoost的算法流程之前,首先要搞清楚幾個(gè)概念,或者說在西瓜書里容易混淆的符號(hào):

  • h_t(\vec{x})—— 基學(xué)習(xí)器,用于集成學(xué)習(xí)器的基本元素,最常用的是單層決策樹。
  • \alpha_t—— 集成學(xué)習(xí)器中每個(gè)基分類器的權(quán)重
  • H(\vec{x}) = sign(\sum_t{\alpha_t h_t(\vec{x})}) —— 集成學(xué)習(xí)器,多個(gè)基學(xué)習(xí)器預(yù)測(cè)結(jié)果的加權(quán)求和。
  • f(\vec{x})=y \in \{1, -1 \}—— 樣本x的真實(shí)值(真實(shí)標(biāo)記)。
  • D=\{(\vec{x_1},y_1),...(\vec{x_n},y_n) \}—— 訓(xùn)練數(shù)據(jù)集。
  • D_t(\vec{x})=w^{[t]} —— 第t輪訓(xùn)練中樣本x對(duì)應(yīng)的權(quán)重w。
  • D_t —— 原分布D在當(dāng)前樣本權(quán)重下D_t(\vec{x})形成的新分布。

AdaBoost是一個(gè)迭代算法,每一輪要做的事情是在當(dāng)前的樣本權(quán)重D_t下,用訓(xùn)練數(shù)據(jù)D訓(xùn)練出一個(gè)基學(xué)習(xí)器h_t,亦即對(duì)應(yīng)的學(xué)習(xí)器權(quán)重\alpha_t,最后確定下一輪學(xué)習(xí)的樣本權(quán)重D_{t+1}

先看基分類器h_t。

機(jī)器學(xué)習(xí)(更準(zhǔn)確一點(diǎn),參數(shù)化模型訓(xùn)練)的一個(gè)討論時(shí)設(shè)定一個(gè)損失函數(shù),然后最小化這個(gè)損失函數(shù),以獲得一組能使預(yù)測(cè)結(jié)果最好的模型參數(shù)。對(duì)于AdaBoost,損失函數(shù)被定義為:

指數(shù)損失函數(shù)

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

基學(xué)習(xí)器的損失函數(shù)

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

基學(xué)習(xí)器的損失函數(shù)(2)

再看基分類器權(quán)重\alpha_t。

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

alpha的計(jì)算方法

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

下一輪的樣本權(quán)重

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

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)練集T_1, T_2, T_3, ..., T_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è)屬性,西瓜書推薦k=log_2d。顯然,隨機(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)行許可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容