①起源:Boosting算法
Boosting算法的目的是每次基於全部數據集,通過使用同一種分類器不同的抽取參數方法(如決策樹,每次都可以抽取不同的特征維度來剖分數據集)
訓練一些不同弱分類器(單次分類錯誤率>0.5),然后將其組合起來,綜合評估(默認認為每個分類器權重等價)進行分類。
AdaBoost算法進行了對其進行了改進。
一、每次訓練分類器時,給予每條數據用於計算誤差的不同權重D。
二、每個分類器,給予用於分類的不同權重alpha。
兩種權的使用,使得AdaBoost在組合分類器時,能夠根據當前的訓練情況做一些調整。
②弱分類器:單決策樹
單決策樹應該是AdaBoost中使用最多的弱分類器。硬傷是單決策樹是個0/1二類分類器。(實際是-1/+1二類)
一般決策樹是通過DFS,連續選擇不同維度多深度划分數據集。
單決策樹就是一般決策樹的1維版本,只搜dep=1就結束。
單決策樹有兩個好處:
一、單次分類效果很差(滿足弱分類器定義)
二、根據決策屬性不同,即便重復使用單決策樹,也可以誕生出分類效果不同的分類器。
單決策樹對於連續型數據,采用閾值(threshold)分類法。
首先確定所有數據中某個特征維度里的min/max范圍。
然后采用步進的方法(每次閾值=min+當前步數*步長)來確定閾值。
基於同一個閾值,又可以把小於/大於閾值的分到不同類(2種分法)。
所以單決策樹最多有【D*步數*2】種變形分類器。
②雙權問題
一、分類器權alpha
定義 $\alpha = \frac{1}{2} \ln \left ( \frac{1-\varepsilon }{\varepsilon } \right )$,其中$\varepsilon$為錯誤率。
P.S 實際Coding時,為防止$\varepsilon$為零,通常取max($\varepsilon$,eps)
$\alpha$這個函數比較有趣,求導之后,假設每次使用新的分類器,錯誤率都會變小,以錯誤率變小為x正軸,有圖像:
這個圖像告訴我們,AdaBoost對后續分類器,給的權是逐漸變大的,因為我們有理由相信,后續分類器更科學一點。
分類器權如何使用呢?假設當前分類器的二類分類結果向量[-1,1,-1,1],那么其加權之后就是$\alpha$ * [-1,1,-1,1]。
對於二類問題,將全部分類器的加權結果累加起來,通過判斷加權結果符號的正負判斷分類,因為$\alpha$的值域是R,所以只能看符號判斷分類。
二、數據權D
每條數據都有權D,該權實際是個概率權,總權和=1。
如果一條數據被正確分類,則$D_{i}^{new}= \frac{D_{i}^{old}*e^{-\alpha}}{Sum(D)}$
如果一條數據被錯誤分類,則$D_{i}^{new}= \frac{D_{i}^{old}*e^{\alpha}}{Sum(D)}$
正確分類的權會被減小,錯誤分類的權會被增大。有趣的是,這兩個式子如果單從$\alpha$角度來看,似乎有點問題。
如果某次$\alpha$為負,那么$D_{i}^{new}= \frac{D_{i}^{old}*e^{-\alpha}}{Sum(D)}$中$D_{i}^{new}$似乎好像是變大了。
但如果你把$\alpha$的定義式拉到$D_{i}^{new}$里面,就會發現:
$\left\{\begin{matrix}
D_{i}^{new}= \frac{1}{\sqrt{e}}*\frac{1-\varepsilon }{\varepsilon }
\\
D_{i}^{new}= \sqrt{e}*\frac{1-\varepsilon }{\varepsilon}
\end{matrix}\right.$
這樣,很明顯就可以看出,正確分類的權確實相對於錯誤分類的權被減小了。
數據權D的引入,主要是采用一種新的方式計算錯誤率。
一般的錯誤率$\varepsilon=\frac{errorExamples}{allExamples}$
但是AdaBoost里對錯誤率也進行加權了,$\varepsilon=\sum_{i=1}^{m}D_{i}*isError?1:0$
由於D是概率權,這樣算出來的也算是錯誤率。如果一條數據分錯了、又分錯了、還是分錯了,那么這條數據理應得到重視,其也會導致錯誤率比較大。
這種加權錯誤率對於在單決策樹里,從眾多變形分類器中,選擇一個錯誤率最低分類器,有很好的參考意義。
③算法過程
★訓練過程
while(true)
{
一、構建一個錯誤率最低的單決策樹分類器,並保存記錄該分類器全部屬性(α、閥值、選取維度、分類結果)
二、利用構建的單決策樹分類器計算α加權分類結果
若數據樣本分類全部正確(大數據情況下幾乎不可能)、分類正確率到達一定情況(多見於正確率收斂)則break
}
★測試過程
for(全部構建的分類器)
for(全部測試數據)
一、使用當前分類器分類
二、累計計算使用全部分類器的加權結果(記錄α的原因)
根據加權結果的正負判斷分類
不難發現,測試過程其實就是訓練過程的某一步提取出來的。
④代碼
#include "iostream" #include "fstream" #include "sstream" #include "math.h" #include "vector" #include "string" #include "cstdio" using namespace std; #define fin cin #define inf 1e10 #define D dataSet[0].feature.size() #define delta 100 int sign(double x) {return x<0?-1:1;} struct Data { vector<double> feature; int y; Data(vector<double> feature,int y):feature(feature),y(y) {} }; struct Stump { vector<int> classEst; int dim,symbol; double threshold,error,alpha; Stump() {} Stump(vector<int> classEst,int dim,double threshold,int symbol,double error):classEst(classEst),dim(dim),threshold(threshold),symbol(symbol),error(error) {} }; vector<Data> dataSet; void read() { ifstream fin("traindata.txt"); string line; double fea,cls; int id; while(getline(cin,line)) { vector<double> feature; stringstream sin(line); sin>>id; while(sin>>fea) feature.push_back(fea); cls=feature.back();feature.pop_back(); dataSet.push_back(Data(feature,cls==0?-1:1)); } } vector<int> stumpClassify(int d,double threshold,int symbol) { vector<int> ret(dataSet.size(),1); for(int i=0;i<dataSet.size();i++) { if(!symbol) {if(dataSet[i].feature[d]<=threshold) ret[i]=-1;} else {if(dataSet[i].feature[d]>threshold) ret[i]=-1;} } return ret; } Stump buildStump(vector<double> dataWeight) { int step=10; Stump bestStump; double minError=inf,weightError; for(int i=0;i<D;i++) { double minRange=inf,maxRange=-inf,stepSize; for(int j=0;j<dataSet.size();j++) { minRange=min(minRange,dataSet[j].feature[i]); maxRange=max(maxRange,dataSet[j].feature[i]); } stepSize=(maxRange-minRange)/step; for(int j=0;j<=step;j++) { double threshold=minRange+stepSize*j; for(int k=0;k<=1;k++) { vector<int> classEst=stumpClassify(i,threshold,k); weightError=0.0; for(int t=0;t<classEst.size();t++) if(classEst[t]!=dataSet[t].y) weightError+=dataWeight[t]; if(weightError<minError) { bestStump=Stump(classEst,i,threshold,k,weightError); minError=weightError; } } } } return bestStump; } vector<Stump> mainProcess() { int iter=0,repeat=0,last_cnt=-1; vector<double> dataWeight(dataSet.size(),1.0/dataSet.size()); vector<double> aggClassEst(dataSet.size(),0.0); vector<Stump> stumpList; while(1) { iter++; Stump nowStump=buildStump(dataWeight); double alpha=0.5*log((1.0-nowStump.error)/max(nowStump.error,1e-16)),DSum=0.0; nowStump.alpha=alpha; stumpList.push_back(nowStump); int err_cnt=0; for(int i=0;i<dataSet.size();i++) { double ret=-1*alpha*dataSet[i].y*nowStump.classEst[i]; dataWeight[i]=dataWeight[i]*exp(ret); DSum+=dataWeight[i]; } for(int i=0;i<dataSet.size();i++) { dataWeight[i]/=DSum; aggClassEst[i]+=alpha*nowStump.classEst[i]; if(sign(aggClassEst[i])!=dataSet[i].y) err_cnt++; } //cout<<err_cnt<<"/"<<dataSet.size()<<endl; if(err_cnt!=last_cnt) {last_cnt=err_cnt;repeat=0;} else repeat++; if(err_cnt==0||repeat>delta) break; } return stumpList; } void classify(vector<Stump> stumpList) { ifstream fin("testdata.txt"); dataSet.clear(); string line; double fea,cls; int id; while(getline(cin,line)) { vector<double> feature; stringstream sin(line); sin>>id; while(sin>>fea) feature.push_back(fea); cls=feature.back();feature.pop_back(); dataSet.push_back(Data(feature,cls==0?-1:1)); } vector<double> aggClassEst(dataSet.size(),0.0); for(int i=0;i<stumpList.size();i++) { vector<int> classEst=stumpClassify(stumpList[i].dim,stumpList[i].threshold,stumpList[i].symbol); for(int j=0;j<dataSet.size();j++) aggClassEst[j]+=(stumpList[i].alpha*classEst[j]); } for(int i=0;i<dataSet.size();i++) if(sign(aggClassEst[i])==-1) printf("%test #%d: origin: %d class %d\n",i,dataSet[i].y,-1); else printf("%test #%d: origin: %d class %d\n",i,dataSet[i].y,1); } int main() { read(); vector<Stump> stumpList=mainProcess(); classify(stumpList); }