談談自己對隨機森林(Random Forest)的一點理解以及代碼註釋~

之前因爲做過隨機森林方面的項目,對隨機森林有過研究,但理論這塊還不是很深入,代碼倒是看了不少,這裏寫下這篇博客,說說對隨機森林的一些理解,以及附上了一份代碼註釋。

1. 隨機森林

隨機森林屬於非傳統式的機器學習算法,由多顆決策樹組成,每棵決策樹處理的是一個訓練樣本子集。訓練階段,通過決策樹的節點分裂來篩選特徵,層層對樣本進行細分,直至將每個訓練樣本子集分類正確,測試階段,直接基於訓練出的特徵進行樣本分類,所以測試速度較快(但訓練速度較慢)。屬於“傻瓜式”的策略(這點和adaboost很像很像),以下部分是標準隨機森林訓練階段的大致流程。


  1. 假如有N個樣本,則有放回的隨機選擇N個樣本(每次隨機選擇一個樣本,然後返回繼續選擇)。這選擇好了的N個樣本用來訓練一個決策樹,作爲決策樹根節點處的樣本。

  2. 當每個樣本有M個屬性時,在決策樹的每個節點需要分裂時,隨機從這M個屬性中選取出m個屬性,滿足條件m << M。然後從這m個屬性中採用某種策略(比如說信息增益)來選擇1個屬性作爲該節點的分裂屬性。

  3. 決策樹形成過程中每個節點都要按照步驟2來分裂,一直到不能夠再分裂爲止(就是收斂)。所謂不能再分裂,就是全部到達葉子節點,如果下一次該節點選出來的那一個屬性是剛剛其父節點分裂時用過的屬性,則該節點就是葉子節點。

  4. 按照步驟1~3建立大量的決策樹,便構成了隨機森林。

從上面的步驟可以看出,隨機森林的隨機性體現在每顆樹的訓練樣本是隨機的,樹中每個節點的分裂屬性集合也是隨機選擇確定的。有了這2個隨機的保證,隨機森林就不會產生過擬合的現象了。

值得注意的是,隨機森林的基本思想都一樣,但是在細節上卻有所不同,尤其是在節點分裂所採用規則,葉子節點確定那裏,有着很多種變種,也就具有不同的效果。說一個我自己遇到的,在確定葉子節點那塊,它不是採用屬性和父節點一致來確定,而是屬性爲空來確定,因爲它在每一個節點選擇一個屬性之後,都會將該屬性從候選屬性集中刪除,這樣就會造成葉子節點候選屬性集爲空的情況。

給出一份某位朋友在論壇中提問的代碼,關於文本分類的,剛好符合我上面描述的情況,我新添加了註釋,與朋友們分享,裏面肯定有很多問題,請不吝賜教啊!!!

2. 代碼

</pre><pre name="code" class="cpp">#include <iostream>
#include <fstream>
#include <sstream>
#include "random_forest.h"

using namespace std;

vector<decision_tree*>  alltrees;               // 森林(決策樹集合)
vector<TupleData>       trainAll,train,test;	// 樣本集
vector<int>	            attributes;	        // 屬性集(元素爲屬性序號)

int                     trainAllNum = 0;	
int                     testAllNum  = 0;	
int                     MaxAttr;	        // 屬性總數
int                     *ArrtNum;               // 屬性個數集(元素爲屬性最大值)
unsigned int            F;
int                     tree_num    = 100;      // 決策樹個數
const int               leafattrnum = -1;       // 葉子節點的屬性序號
int                     TP          = 0,
                        FN          = 0,
                        FP          = 0,
                        TN          = 0,
                        TestP       = 0,
                        TestN       = 0;

// 讀入數據
void init(char * trainname, char * testname)
{
    trainAllNum     = readData(trainAll, trainname);
    testAllNum      = readData(test, testname);
    calculate_attributes();
    double temp     = (double)trainAllNum;
    temp            = log(temp)/log(2.0);
    F               = (unsigned int)floor(temp+0.5)+1;
    if(F>MaxAttr) F = MaxAttr;
}

// 初始化訓練樣本子集
void sub_init()
{
    // 選取決策樹的訓練樣本集合
    RandomSelectData(trainAll, train);

    // 計算樣本屬性個數
    calculate_ArrtNum();
}

// 讀數據
int readData(vector<TupleData> &data, const char* fileName)
{
    ifstream fin;
    fin.open(fileName);
    string line;

    int datanum=0;

    // 每行數據作爲一個樣本
    while(getline(fin,line))
    {
        TupleData d;
        istringstream stream(line);
        string str;

        // 設置每個樣本的標籤和內容
        while(stream>>str)
        {
            if(str.find('+')==0)
            {
                d.label='+';
            }
            else if(str.find('-')==0)
            {
                d.label='-';
            }
            else
            {
                int j=stringtoint(str);
                d.A.push_back(j);
            }
        }

        data.push_back(d);	
        datanum++;
    }

    fin.close();
    return datanum;
}

// 生成根節點的訓練樣本子集
void RandomSelectData(vector<TupleData> &data, vector<TupleData> &subdata)
{
    int index;
    subdata.clear();
    int d = 0;
    while (d < trainAllNum)
    {
        index = rand() % trainAllNum;
        subdata.push_back(data.at(index));
        d++;
    }
}

// 計算屬性序列
void calculate_attributes()
{
    // 每個樣本必須具有相同的屬性個數
    TupleData d = trainAll.at(0);
    MaxAttr = d.A.size();
    attributes.clear();

    // 建立屬性集合attributes,元素爲屬性序號
    for (int i = 0; i < MaxAttr; i++)
    {
        attributes.push_back(i);
    }

    // 初始化屬性最大值序列,元素爲屬性最大值
    ArrtNum = new int[MaxAttr];
}

// 字符串轉化爲int
int stringtoint(string s)
{
    int sum=0;
    for(int i=0; s[i]!='\0';i++)
    {
        int j=int(s[i])-48;
        sum=sum*10+j;
    }
    return sum;
}

// 計算ArrtNum元素值
void calculate_ArrtNum()
{
    for(int i = 0; i < MaxAttr; i++) ArrtNum[i] = 0;

    // ArrtNum元素值爲屬性最大值
    for (vector<TupleData>::const_iterator it = train.begin(); it != train.end(); it++)	
    {
        int i = 0;

        for (vector<int>::const_iterator intt=(*it).A.begin(); intt!=(*it).A.end();intt++)
        {
            int valuemax=(*intt)+1;
            if(valuemax>ArrtNum[i]) ArrtNum[i]=valuemax;
            i++;
        }
    }
}

// 計算熵
double Entropy(double p, double s)
{
    double n = s - p;
    double result = 0;
    if (n != 0)
        result += - double(n) / s * log(double(n) / s) / log(2.0);
    if (p != 0)
        result += double(-p) / s * log(double(p) / s) / log(2.0);
    return result;
}

// 訓練一棵決策樹
int creat_classifier(decision_tree *&p, const vector<TupleData> &samples, vector<int> &attributes)
{
    if (p == NULL)
        p = new decision_tree();

    // 根據樣本真實類別,輸出葉子節點類別
    if (Allthesame(samples, '+'))
    {
        p->node.label = '+';
        p->node.attrNum = leafattrnum;
        p->childs.clear();
        return 1;
    }
    if (Allthesame(samples, '-'))
    {
        p->node.label = '-';
        p->node.attrNum = leafattrnum;
        p->childs.clear();
        return 1;
    }
    // 如果屬性序列爲空,當前節點就爲葉子節點
    if (attributes.size() == 0)
    {
        p->node.label = Majorityclass(samples);
        p->node.attrNum = leafattrnum;
        p->childs.clear();
        return 1;
    }

    // 計算當前節點的最優屬性
    p->node.attrNum = BestGainArrt(samples, attributes);

    // 中間節點無標籤
    p->node.label = ' ';

    // 計算子節點候選屬性集合,候選集合元素越來越少
    vector<int> newAttributes;
    for (vector<int>::iterator it = attributes.begin(); it != attributes.end(); it++)
        if ((*it) != p->node.attrNum)
            newAttributes.push_back((*it));

    // 初始化樣本子集,建立maxvalue個樣本子集,也就說明該節點有maxvalue個子節點
    // 爲什麼不建立一個閾值,進行二分類?
    int maxvalue = ArrtNum[p->node.attrNum];
    vector<TupleData>* subSamples = new vector<TupleData>[maxvalue];
    for (int i = 0; i < maxvalue; i++)
        subSamples[i].clear();

    // 將樣本集合分爲樣本子集
    for (vector<TupleData>::const_iterator it = samples.begin(); it != samples.end(); it++)
    {
        // 對樣本進行分類,分別分到maxvalue個子節點中
        // p->node.attrNum是當前節點的最優屬性序號
        // (*it).A.at(p->node.attrNum)正是子節點的序號
        // 基於當前節點最優屬性,計算當前樣本的歸類
        subSamples[(*it).A.at(p->node.attrNum)].push_back((*it));
    }

    decision_tree *child;
    for (int i = 0; i < maxvalue; i++)
    {
        child = new decision_tree;

        if (subSamples[i].size() == 0)
            child->node.label = Majorityclass(samples);
        else
            creat_classifier(child, subSamples[i], newAttributes);

        p->childs.push_back(child);
    }
    delete[] subSamples;
    return 0;
}

// 計算節點處的信息增益
int BestGainArrt(const vector<TupleData> &samples, vector<int> &attributes)
{
    int attr, 
        bestAttr = 0,
        p = 0,
        s = (int)samples.size();

    // 計算正樣本個數
    for (vector<TupleData>::const_iterator it = samples.begin(); it != samples.end(); it++)
    {
        if ((*it).label == '+')
            p++;
    }

    double infoD;
    double bestResult = 0;

    // 計算初始熵
    infoD = Entropy(p, s);

    vector<int> m_attributes;

    // 隨機確定候選屬性集
    RandomSelectAttr(attributes, m_attributes);

    // 遍歷屬性(即主題),通過信息增益篩選最優屬性
    for (vector<int>::iterator it = m_attributes.begin(); it != m_attributes.end(); it++)
    {
        attr            = (*it);
        double result   = infoD;

        // 第attr個屬性的最大屬性值
        int maxvalue    = ArrtNum[attr];

        // 正負樣本集
        int* subN       = new int[maxvalue];
        int* subP       = new int[maxvalue];
        int* sub        = new int[maxvalue];

        for (int i = 0; i < maxvalue; i++)
        {
            subN[i] = 0;
            subP[i] = 0;
            sub[i]  = 0;
        }

        // 基於特定屬性,對當前訓練樣本進行分類
        // 屬性計算這一步的確沒有,屬性值直接存儲在樣本中
        for (vector<TupleData>::const_iterator jt = samples.begin(); jt != samples.end(); jt++)
        {
            if ((*jt).label == '+')
                subP[(*jt).A.at(attr)] ++;
            else
                subN[(*jt).A.at(attr)] ++;
            sub[(*jt).A.at(attr)]++;
        }

        // 計算特定屬性下信息增益(相對熵)
        double SplitInfo = 0;
        for(int i = 0; i < maxvalue; i++)
        {
            double partsplitinfo;
            partsplitinfo   = -double(sub[i])/s*log(double(sub[i])/s)/log(2.0);
            SplitInfo       = SplitInfo+partsplitinfo;
        }

        double infoattr = 0;
        for (int i = 0; i < maxvalue; i++)
        {
            double partentropy;
            partentropy     = Entropy(subP[i], subP[i] + subN[i]);
            infoattr        = infoattr+((double)(subP[i] + subN[i])/(double)(s))*partentropy;
        }
        result = result - infoattr;
        result = result / SplitInfo;

        // 尋找最優屬性
        if (result > bestResult)
        {
            bestResult      = result;
            bestAttr        = attr;
        }
        delete[] subN;
        delete[] subP;
        delete[] sub;
    }

    if (bestResult == 0)
    {
        bestAttr=attributes.at(0);
    }
    return bestAttr;
}

void RandomSelectAttr(vector<int> &data, vector<int> &subdata)
{
    int index;
    unsigned int dataNum=data.size();
    subdata.clear();
    if(dataNum<=F)
    {
        for (vector<int>::iterator it = data.begin(); it != data.end(); it++)
        {
            int attr = (*it);
            subdata.push_back(attr);
        }
    }
    else
    {
        set<int> AttrSet;
        AttrSet.clear();
        while (AttrSet.size() < F)
        {
            index = rand() % dataNum;
            if (AttrSet.count(index) == 0)
            {
                AttrSet.insert(index);
                subdata.push_back(data.at(index));
            }
        }
    }
}

bool Allthesame(const vector<TupleData> &samples, char ch)
{
    for (vector<TupleData>::const_iterator it = samples.begin(); it != samples.end(); it++)
        if ((*it).label != ch)
            return false;
    return true;
}

// 確定節點中哪個類別樣本個數最多
char Majorityclass(const vector<TupleData> &samples)
{
    int p = 0, n = 0;
    for (vector<TupleData>::const_iterator it = samples.begin(); it != samples.end(); it++)
        if ((*it).label == '+')
            p++;
        else
            n++;
    if (p >= n)
        return '+';
    else
        return '-';
}

// 測試階段
char testClassifier(decision_tree *p, TupleData d)
{
    // 抵達葉子節點
    if (p->node.label != ' ')
        return p->node.label;

    // 節點處最優屬性
    int attrNum = p->node.attrNum;

    // 錯誤樣本
    if (d.A.at(attrNum) < 0)
        return ' ';

    // 確定分支
    return testClassifier(p->childs.at(d.A.at(attrNum)), d);
}

void testData()
{
    for (vector<TupleData>::iterator it = test.begin(); it != test.end(); it++)
    {
        printf("新樣本\n");
        if((*it).label=='+') TestP++;
        else TestN++;

        int p = 0, n = 0;

        for(int i = 0; i < tree_num; i++)
        {
            if(testClassifier(alltrees.at(i), (*it))=='+')  p++;
            else n++;
        }

        if(p>n)
        {
            if((*it).label=='+') TP++;
            else FP++;
        }
        else
        {
            if((*it).label=='+') FN++;
            else TN++;
        }
    }
}

void freeClassifier(decision_tree *p)
{
    if (p == NULL)
        return;
    for (vector<decision_tree*>::iterator it = p->childs.begin(); it != p->childs.end(); it++)
    {
        freeClassifier(*it);
    }
    delete p;
}

void freeArrtNum()
{
    delete[] ArrtNum;
}

void showResult()
{
    cout << "Train size:	"<< trainAllNum<<endl;
    cout << "Test size:	"<<testAllNum<<endl;	
    cout << "True positive:	" << TP << endl;
    cout << "False negative:	"<< FN<<endl;
    cout << "False positive:	"<<FP<<endl;
    cout << "True negative:	"<<TN<<endl;
}

int main(int argc, char **argv)
{
    char * trainfile=argv[1];
    char * testfile=argv[2];

    srand((unsigned)time(NULL)); 

    // 初始化樣本
    init("1.txt", "2.txt");

    // 訓練階段
    for(int i = 0; i < tree_num; i++)
    {
        printf("第 %d 棵決策樹訓練開始\n", i);

        // 每棵樹的訓練樣本子集
        sub_init();

        // 訓練每棵決策樹
        decision_tree * root=NULL;
        creat_classifier(root, train, attributes);

        // 建立森林
        alltrees.push_back(root);

        printf("第 %d 棵決策樹訓練完畢\n", i);
    }

    // 測試階段
    testData();

    for (vector<decision_tree *>::const_iterator it = alltrees.begin(); it != alltrees.end(); it++)
    {
        freeClassifier((*it));
    }

    freeArrtNum();

    showResult();

    system("pause");
    return 0;
}

3. 總結

隨機森林的基本思想就是決策樹的基本思想,目前應用很廣,比如說人臉對齊算法中的《Face Alignment at 3000 FPS via Regressing Local Binary Features》,正是利用RF進行特徵點定位,效果很好並且速度十分快,在實際應用中有很大優勢。


發佈了42 篇原創文章 · 獲贊 193 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章