1.隨機森林原理介紹
隨機森林,指的是利用多棵樹對樣本進行訓練并預測的一種分類器。該分類器最早由Leo Breiman和Adele Cutler提出,并被注冊成了商標。簡單來說,隨機森林就是由多棵CART(Classification And Regression Tree)構成的。對于每棵樹,它們使用的訓練集是從總的訓練集中有放回采樣出來的,這意味著,總的訓練集中的有些樣本可能多次出現在一棵樹的訓練集中,也可能從未出現在一棵樹的訓練集中。在訓練每棵樹的節點時,使用的特征是從所有特征中按照一定比例隨機地無放回的抽取的,根據Leo Breiman的建議,假設總的特征數量為M,這個比例可以是sqrt(M),1/2sqrt(M),2sqrt(M)。
因此,隨機森林的訓練過程可以總結如下:
(1)給定訓練集S,測試集T,特征維數F。確定參數:使用到的CART的數量t,每棵樹的深度d,每個節點使用到的特征數量f,終止條件:節點上最少樣本數s,節點上最少的信息增益m
對于第1-t棵樹,i=1-t:
(2)從S中有放回的抽取大小和S一樣的訓練集S(i),作為根節點的樣本,從根節點開始訓練
(3)如果當前節點上達到終止條件,則設置當前節點為葉子節點,如果是分類問題,該葉子節點的預測輸出為當前節點樣本集合中數量最多的那一類c(j),概率p為c(j)占當前樣本集的比例;如果是回歸問題,預測輸出為當前節點樣本集各個樣本值的平均值。然后繼續訓練其他節點。如果當前節點沒有達到終止條件,則從F維特征中無放回的隨機選取f維特征。利用這f維特征,尋找分類效果最好的一維特征k及其閾值th,當前節點上樣本第k維特征小于th的樣本被劃分到左節點,其余的被劃分到右節點。繼續訓練其他節點。有關分類效果的評判標準在后面會講。
(4)重復(2)(3)直到所有節點都訓練過了或者被標記為葉子節點。
(5)重復(2),(3),(4)直到所有CART都被訓練過。
利用隨機森林的預測過程如下:
對于第1-t棵樹,i=1-t:
(2)重復執行(1)直到所有t棵樹都輸出了預測值。如果是分類問題,則輸出為所有樹中預測概率總和最大的那一個類,即對每個c(j)的p進行累計;如果是回歸問題,則輸出為所有樹的輸出的平均值。
注:有關分類效果的評判標準,因為使用的是CART,因此使用的也是CART的平板標準,和C3.0,C4.5都不相同。
對于分類問題(將某個樣本劃分到某一類),也就是離散變量問題,CART使用Gini值作為評判標準。定義為Gini=1-∑(P(i)*P(i)),P(i)為當前節點上數據集中第i類樣本的比例。例如:分為2類,當前節點上有100個樣本,屬于第一類的樣本有70個,屬于第二類的樣本有30個,則Gini=1-0.7×07-0.3×03=0.42,可以看出,類別分布越平均,Gini值越大,類分布越不均勻,Gini值越小。在尋找最佳的分類特征和閾值時,評判標準為:argmax(Gini-GiniLeft-GiniRight),即尋找最佳的特征f和閾值th,使得當前節點的Gini值減去左子節點的Gini和右子節點的Gini值最大。
對于回歸問題,相對更加簡單,直接使用argmax(Var-VarLeft-VarRight)作為評判標準,即當前節點訓練集的方差Var減去減去左子節點的方差VarLeft和右子節點的方差VarRight值最大。
2.OpenCV函數使用
OpenCV提供了隨機森林的相關類和函數。具體使用方法如下:
(1)首先利用CvRTParams定義自己的參數,其格式如下
CvRTParams::CvRTParams(intmax_depth,intmin_sample_count,floatregression_accuracy,booluse_surrogates,intmax_categories,constfloat* priors,boolcalc_var_importance,intnactive_vars,intmax_num_of_trees_in_the_forest,floatforest_accuracy,inttermcrit_type)
大部分參數描述都在http://docs.opencv.org/modules/ml/doc/random_trees.html上面有,說一下沒有描述的幾個參數的意義
booluse_surrogates:是否使用代理,指的是,如果當前的測試樣本缺少某些特征,但是在當前節點上的分類or回歸特征正是缺少的這個特征,那么這個樣本就沒法繼續沿著樹向下走了,達不到葉子節點的話,就沒有預測輸出,這種情況下,可以利用當前節點下面的所有子節點中的葉子節點預測輸出的平均值,作為這個樣本的預測輸出。
const float*priors:先驗知識,這個指的是,可以根據各個類別樣本數量的先驗分布,對其進行加權。比如:如果一共有3類,第一類樣本占整個訓練集的80%,其余兩類各占10%,那么這個數據集里面的數據就很不平均,如果每類的樣本都加權的話,就算把所有樣本都預測成第一類,那么準確率也有80%,這顯然是不合理的,因此我們需要提高后兩類的權重,使得后兩類的分類正確率也不會太低。
floatregression_accuracy:回歸樹的終止條件,如果當前節點上所有樣本的真實值和預測值之間的差小于這個數值時,停止生產這個節點,并將其作為葉子節點。
后來發現這些參數在決策樹里面有解釋,英文說明在這里http://docs.opencv.org/modules/ml/doc/decision_trees.html#cvdtreeparams
具體例子如下,網上找了個別人的例子,自己改成了可以讀取MNIST數據并且做分類的形式,如下:
#include <cv.h>//opencv general include file
#include <ml.h>//opencv machine learning include file
#include <stdio.h>
usingnamespacecv;//OpenCV API is in the C++ "cv" namespace
/******************************************************************************/
//global definitions (for speed and ease of use)
//手寫體數字識別
#defineNUMBER_OF_TRAINING_SAMPLES 60000
#defineATTRIBUTES_PER_SAMPLE 784
#defineNUMBER_OF_TESTING_SAMPLES 10000
#defineNUMBER_OF_CLASSES 10
//N.B. classes are integer handwritten digits in range 0-9
/******************************************************************************/
//loads the sample database from file (which is a CSV text file)
inlinevoidrevertInt(int&x)
{
x=((x&0x000000ff)<<24)|((x&0x0000ff00)<<8)|((x&0x00ff0000)>>8)|((x&0xff000000)>>24);
};
intread_data_from_csv(constchar* samplePath,constchar*labelPath, Mat data, Mat classes,
intn_samples )
{
FILE* sampleFile=fopen(samplePath,"rb");
FILE* labelFile=fopen(labelPath,"rb");
intmbs=0,number=0,col=0,row=0;
fread(&mbs,4,1,sampleFile);
fread(&number,4,1,sampleFile);
fread(&row,4,1,sampleFile);
fread(&col,4,1,sampleFile);
revertInt(mbs);
revertInt(number);
revertInt(row);
revertInt(col);
fread(&mbs,4,1,labelFile);
fread(&number,4,1,labelFile);
revertInt(mbs);
revertInt(number);
unsignedchartemp;
for(intline =0; line < n_samples; line++)
{
//for each attribute on the line in the file
for(intattribute =0; attribute < (ATTRIBUTES_PER_SAMPLE +1); attribute++)
{
if(attribute <ATTRIBUTES_PER_SAMPLE)
{
//first 64 elements (0-63) in each line are the attributes
fread(&temp,1,1,sampleFile);
//fscanf(f, "%f,", &tmp);
data.at<float>(line, attribute) = static_cast<float>(temp);
//printf("%f,", data.at<float>(line, attribute));
}
elseif(attribute ==ATTRIBUTES_PER_SAMPLE)
{
//attribute 65 is the class label {0 ... 9}
fread(&temp,1,1,labelFile);
//fscanf(f, "%f,", &tmp);
classes.at<float>(line,0) = static_cast<float>(temp);
//printf("%f\n", classes.at<float>(line, 0));
}
}
}
fclose(sampleFile);
fclose(labelFile);
return1;//all OK
}
/******************************************************************************/
intmain(intargc,char**argv )
{
for(inti=0; i< argc; i++)
std::cout<<argv[i]<<std::endl;
//lets just check the version first
printf ("OpenCV version %s (%d.%d.%d)\n",
CV_VERSION,
CV_MAJOR_VERSION, CV_MINOR_VERSION, CV_SUBMINOR_VERSION);
//定義訓練數據與標簽矩陣
Mat training_data =Mat(NUMBER_OF_TRAINING_SAMPLES, ATTRIBUTES_PER_SAMPLE, CV_32FC1);
Mat training_classifications= Mat(NUMBER_OF_TRAINING_SAMPLES,1, CV_32FC1);
//定義測試數據矩陣與標簽
Mat testing_data =Mat(NUMBER_OF_TESTING_SAMPLES, ATTRIBUTES_PER_SAMPLE, CV_32FC1);
Mat testing_classifications= Mat(NUMBER_OF_TESTING_SAMPLES,1, CV_32FC1);
//define all the attributes as numerical
//alternatives are CV_VAR_CATEGORICAL or CV_VAR_ORDERED(=CV_VAR_NUMERICAL)
//that can be assigned on a per attribute basis
Mat var_type= Mat(ATTRIBUTES_PER_SAMPLE +1,1, CV_8U );
var_type.setTo(Scalar(CV_VAR_NUMERICAL) );//all inputs are numerical
//this is a classification problem (i.e. predict a discrete number of class
//outputs) so reset the last (+1) output var_type element to CV_VAR_CATEGORICAL
var_type.at<uchar>(ATTRIBUTES_PER_SAMPLE,0) =CV_VAR_CATEGORICAL;
doubleresult;//value returned from a prediction
//加載訓練數據集和測試數據集
if(read_data_from_csv(argv[1],argv[2], training_data, training_classifications, NUMBER_OF_TRAINING_SAMPLES) &&
read_data_from_csv(argv[3],argv[4], testing_data, testing_classifications, NUMBER_OF_TESTING_SAMPLES))
{
/********************************步驟1:定義初始化Random Trees的參數******************************/
floatpriors[] = {1,1,1,1,1,1,1,1,1,1};//weights of each classification for classes
CvRTParamsparams= CvRTParams(20,//max depth
50,//min sample count
0,//regression accuracy: N/A here
false,//compute surrogate split, no missing data
15,//max number of categories (use sub-optimal algorithm for larger numbers)
priors,//the array of priors
false,//calculate variable importance
50,//number of variables randomly selected at node and used to find the best split(s).
100,//max number of trees in the forest
0.01f,//forest accuracy
CV_TERMCRIT_ITER |? ? CV_TERMCRIT_EPS//termination cirteria
);
/****************************步驟2:訓練 Random Decision Forest(RDF)分類器*********************/
printf("\nUsing training database: %s\n\n", argv[1]);
CvRTrees* rtree =newCvRTrees;
booltrain_result=rtree->train(training_data, CV_ROW_SAMPLE, training_classifications,
Mat(), Mat(), var_type, Mat(),params);
//float train_error=rtree->get_train_error();
//printf("train error:%f\n",train_error);
//perform classifier testing and report results
Mat test_sample;
intcorrect_class =0;
intwrong_class =0;
intfalse_positives [NUMBER_OF_CLASSES] = {0,0,0,0,0,0,0,0,0,0};
printf("\nUsing testing database: %s\n\n", argv[2]);
for(inttsample =0; tsample < NUMBER_OF_TESTING_SAMPLES; tsample++)
{
//extract a row from the testing matrix
test_sample =testing_data.row(tsample);
/********************************步驟3:預測*********************************************/
result= rtree->predict(test_sample, Mat());
printf("Testing Sample %i -> class result (digit %d)\n", tsample, (int) result);
//if the prediction and the (true) testing classification are the same
//(N.B. openCV uses a floating point decision tree implementation!)
if(fabs(result - testing_classifications.at<float>(tsample,0))
>=FLT_EPSILON)
{
//if they differ more than floating point error => wrong class
wrong_class++;
false_positives[(int) result]++;
}
else
{
//otherwise correct
correct_class++;
}
}
printf("\nResults on the testing database: %s\n"
"\tCorrect classification: %d (%g%%)\n"
"\tWrong classifications: %d (%g%%)\n",
argv[2],
correct_class, (double) correct_class*100/NUMBER_OF_TESTING_SAMPLES,
wrong_class, (double) wrong_class*100/NUMBER_OF_TESTING_SAMPLES);
for(inti =0; i < NUMBER_OF_CLASSES; i++)
{
printf("\tClass (digit %d) false postives? ? %d (%g%%)\n", i,
false_positives[i],
(double) false_positives[i]*100/NUMBER_OF_TESTING_SAMPLES);
}
//all matrix memory free by destructors
//all OK : main returns 0
return0;
}
//not OK : main returns -1
return-1;
}
MNIST樣本可以在這個網址http://yann.lecun.com/exdb/mnist/下載,改一下路徑可以直接跑的。
3.如何自己設計隨機森林程序
有時現有的庫無法滿足要求,就需要自己設計一個分類器算法,這部分講一下如何設計自己的隨機森林分類器,代碼實現就不貼了,因為在工作中用到了,因此比較敏感。
首先,要有一個RandomForest類,里面保存整個樹需要的一些參數,包括但不限于:訓練樣本數量、測試樣本數量、特征維數、每個節點隨機提取的特征維數、CART樹的數量、樹的最大深度、類別數量(如果是分類問題)、一些終止條件、指向所有樹的指針,指向訓練集和測試集的指針,指向訓練集label的指針等。還要有一些函數,至少要有train和predict吧。train里面直接調用每棵樹的train方法即可,predict同理,但要對每棵樹的預測輸出做處理,得到森林的預測輸出。
其次,要有一個sample類,這個類可不是用來存儲訓練集和對應label的,這是因為,每棵樹、每個節點都有自己的樣本集和,如果你的存儲每個樣本集和的話,需要的內存實在是太過巨大了,假設樣本數量為M,特征維數為N,則整個訓練集大小為M×N,而每棵樹的每層都有這么多樣本,樹的深度為D,共有S棵樹的話,則需要存儲M×N×D×S的存儲空間。這實在是太大了。因此,每個節點訓練時用到的訓練樣本和特征,我們都用序號數組來代替,sample類就是干這個的。sample的函數基本需要兩個就行,第一個是從現有訓練集有放回的隨機抽取一個新的訓練集,當然,只包含樣本的序號。第二個函數是從現有的特征中無放回的隨機抽取一定數量的特征,同理,也是特征序號即可。
然后,需要一個Tree類,代表每棵樹,里面保存樹的一些參數以及一個指向所有節點的指針。
最后,需要一個Node類,代表樹的每個節點。
需要說明的是,保存樹的方式可以是最普通的數組,也可是是vector。Node的保存方式同理,但是個人不建議用鏈表的方式,在程序設計以及函數處理上太麻煩,但是在省空間上并沒有太多的體現。
目前先寫這么多,最后這部分我還會再擴充一些。