机器学习(2):决策树+随机森林

一. 决策树

1. 决策树:

决策树算法借助于树的分支结构实现分类,决策树在选择分裂点的时候,总是选择最好的属性作为分类属性,即让每个分支的记录的类别尽可能纯。

常用的属性选择方法有信息增益(Information Gain),增益比例(gain ratio),基尼指数(Gini index)。

其基本思路是不断选取产生信息增益最大的属性来划分样例集和,构造决策树。

信息增益定义为按某种属性分裂后,结点与其子结点的信息熵之差。

信息熵是香农提出的,用于描述信息不纯度(不稳定性,不确定性),其计算公式是:

其中n为类别个数,Pi为子集合中不同类型(而二元分类即正样例和负样例)的样例的比例。信息熵表示将数据集S不同的类分开需要的信息量,其值越大表示越不纯。

这样信息增益可以定义为样本按照某属性划分时造成熵减少的期望,可以区分训练样本中正负样本的能力,其计算公式是:

上面公式实际上就是当前节点的不纯度减去子节点不纯度的加权平均数,权重由子节点记录数与当前节点记录数的比例决定。

--------------------------------------------------------------------------------------------------------

基本步骤:

# ==============================================
# 输入:
#        数据集
# 输出:
#        构造好的决策树(也即训练集)
# ==============================================
def 创建决策树:
    '创建决策树'
    
    if (数据集中所有样本分类一致):
        创建携带类标签的叶子节点
    else:
        寻找划分数据集的最好特征
        根据最好特征划分数据集
        for 每个划分的数据集:
            创建决策子树(递归方式)

 

构建决策树采用贪心算法,只考虑当前纯度差最大的属性来分裂。

使用信息增益的话其实是有一个缺点,那就是它偏向于具有大量值的属性--就是说在训练集中,某个属性所取的不同值的个数越多,那么越有可能拿它来作为分裂属性,而这样做有时候是没有意义的,

另外ID3不能处理连续分布的数据特征,于是就有了C4.5算法。CART算法也支持连续分布的数据特征。

2. 决策数有两大优点:

1)决策树模型可以读性好,具有描述性,有助于人工分析;

2)效率高,决策树只需要一次构建,反复使用,每一次预测的最大计算次数不超过决策树的深度。

3. 过渡拟合:

采用上面算法生成的决策树在事件中往往会导致过滤拟合。也就是该决策树对训练数据可以得到很低的错误率,但是运用到测试数据上却得到非常高的错误率。过渡拟合的原因有以下几点:

  • 噪音数据:训练数据中存在噪音数据,决策树的某些节点有噪音数据作为分割标准,导致决策树无法代表真实数据。
  • 缺少代表性数据:训练数据没有包含所有具有代表性的数据,导致某一类数据无法很好的匹配,这一点可以通过观察混淆矩阵(Confusion Matrix)分析得出。

优化方案1:修剪枝叶

决策树过渡拟合往往是因为太过“茂盛”,也就是节点过多,所以需要裁剪(Prune Tree)枝叶。裁剪枝叶的策略对决策树正确率的影响很大。主要有两种裁剪策略。

前置裁剪 在构建决策树的过程时,提前停止。那么,会将切分节点的条件设置的很苛刻,导致决策树很短小。结果就是决策树无法达到最优。实践证明这中策略无法得到较好的结果。

后置裁剪 决策树构建好后,然后才开始裁剪。采用两种方法:1)用单一叶节点代替整个子树,叶节点的分类采用子树中最主要的分类;

                                                                              2)将一个子树完全替代另外一颗子树。后置裁剪有个问题就是计算效率,有些节点计算后就被裁剪了,导致有点浪费。

 

优化方案2:K-Fold Cross Validation

首先计算出整体的决策树T,叶节点个数记作N,设i属于[1,N]。对每个i,使用K-Fold Validataion方法计算决策树,并裁剪到i个节点,计算错误率,最后求出平均错误率。

这样可以用具有最小错误率对应的i作为最终决策树的大小,对原始决策树进行裁剪,得到最优决策树。

 

优化方案3:Random Forest

Random Forest是用训练数据随机的计算出许多决策树,形成了一个森林。然后用这个森林对未知数据进行预测,选取投票最多的分类。实践证明,此算法的错误率得到了经一步的降低。

这种方法背后的原理可以用“三个臭皮匠定一个诸葛亮”这句谚语来概括。一颗树预测正确的概率可能不高,但是集体预测正确的概率却很高。

ID3(C++实现)  参考:http://blog.csdn.net/yangliuy/article/details/7322015#

#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <cmath>
using namespace std;
#define MAXLEN 6//输入每行的数据个数

//多叉树的实现 
//1 广义表
//2 父指针表示法,适于经常找父结点的应用
//3 子女链表示法,适于经常找子结点的应用
//4 左长子,右兄弟表示法,实现比较麻烦
//5 每个结点的所有孩子用vector保存
//教训:数据结构的设计很重要,本算法采用5比较合适,同时
//注意维护剩余样例和剩余属性信息,建树时横向遍历考循环属性的值,
//纵向遍历靠递归调用

vector <vector <string> > state;//实例集
vector <string> item(MAXLEN);//对应一行实例集
vector <string> attribute_row;//保存首行即属性行数据
string endflag("end");//输入结束
string yes("yes");
string no("no");
string blank("");
map<string, vector < string > > map_attribute_values;//存储属性对应的所有的值
int tree_size = 0;
struct Node{//决策树节点
    string attribute;//属性值
    string arrived_value;//到达的属性值
    vector<Node *> childs;//所有的孩子
    Node(){
        attribute = blank;
        arrived_value = blank;
    }
};
Node * root;

//根据数据实例计算属性与值组成的map
void ComputeMapFrom2DVector(){
    unsigned int i, j, k;
    bool exited = false;
    vector<string> values;
    for (i = 1; i < MAXLEN - 1; i++){//按照列遍历
        for (j = 1; j < state.size(); j++){
            for (k = 0; k < values.size(); k++){
                if (!values[k].compare(state[j][i])) exited = true;
            }
            if (!exited){
                values.push_back(state[j][i]);//注意Vector的插入都是从前面插入的,注意更新it,始终指向vector头
            }
            exited = false;
        }
        map_attribute_values[state[0][i]] = values;
        values.erase(values.begin(), values.end());
    }
}

//根据具体属性和值来计算熵
double ComputeEntropy(vector <vector <string> > remain_state, string attribute, string value, bool ifparent){
    vector<int> count(2, 0);
    unsigned int i, j;
    bool done_flag = false;//哨兵值
    for (j = 1; j < MAXLEN; j++){
        if (done_flag) break;
        if (!attribute_row[j].compare(attribute)){
            for (i = 1; i < remain_state.size(); i++){
                if ((!ifparent&&!remain_state[i][j].compare(value)) || ifparent){//ifparent记录是否算父节点
                    if (!remain_state[i][MAXLEN - 1].compare(yes)){
                        count[0]++;
                    }
                    else count[1]++;
                }
            }
            done_flag = true;
        }
    }
    if (count[0] == 0 || count[1] == 0) return 0;//全部是正实例或者负实例
    //具体计算熵 根据[+count[0],-count[1]],log2为底通过换底公式换成自然数底数
    double sum = count[0] + count[1];
    double entropy = -count[0] / sum*log(count[0] / sum) / log(2.0) - count[1] / sum*log(count[1] / sum) / log(2.0);
    return entropy;
}

//计算按照属性attribute划分当前剩余实例的信息增益
double ComputeGain(vector <vector <string> > remain_state, string attribute){
    unsigned int j, k, m;
    //首先求不做划分时的熵
    double parent_entropy = ComputeEntropy(remain_state, attribute, blank, true);
    double children_entropy = 0;
    //然后求做划分后各个值的熵
    vector<string> values = map_attribute_values[attribute];
    vector<double> ratio;
    vector<int> count_values;
    int tempint;
    for (m = 0; m < values.size(); m++){
        tempint = 0;
        for (k = 1; k < MAXLEN - 1; k++){
            if (!attribute_row[k].compare(attribute)){
                for (j = 1; j < remain_state.size(); j++){
                    if (!remain_state[j][k].compare(values[m])){
                        tempint++;
                    }
                }
            }
        }
        count_values.push_back(tempint);
    }

    for (j = 0; j < values.size(); j++){
        ratio.push_back((double)count_values[j] / (double)(remain_state.size() - 1));
    }
    double temp_entropy;
    for (j = 0; j < values.size(); j++){
        temp_entropy = ComputeEntropy(remain_state, attribute, values[j], false);
        children_entropy += ratio[j] * temp_entropy;
    }
    return (parent_entropy - children_entropy);
}

int FindAttriNumByName(string attri){
    for (int i = 0; i < MAXLEN; i++){
        if (!state[0][i].compare(attri)) return i;
    }
    cerr << "can't find the numth of attribute" << endl;
    return 0;
}

//找出样例中占多数的正/负性
string MostCommonLabel(vector <vector <string> > remain_state){
    int p = 0, n = 0;
    for (unsigned i = 0; i < remain_state.size(); i++){
        if (!remain_state[i][MAXLEN - 1].compare(yes)) p++;
        else n++;
    }
    if (p >= n) return yes;
    else return no;
}

//判断样例是否正负性都为label
bool AllTheSameLabel(vector <vector <string> > remain_state, string label){
    int count = 0;
    for (unsigned int i = 0; i < remain_state.size(); i++){
        if (!remain_state[i][MAXLEN - 1].compare(label)) count++;
    }
    if (count == remain_state.size() - 1) return true;
    else return false;
}

//计算信息增益,DFS构建决策树
//current_node为当前的节点
//remain_state为剩余待分类的样例
//remian_attribute为剩余还没有考虑的属性
//返回根结点指针
Node * BulidDecisionTreeDFS(Node * p, vector <vector <string> > remain_state, vector <string> remain_attribute){
    //if(remain_state.size() > 0){
    //printv(remain_state);
    //}
    if (p == NULL)
        p = new Node();
    //先看搜索到树叶的情况
    if (AllTheSameLabel(remain_state, yes)){
        p->attribute = yes;
        return p;
    }
    if (AllTheSameLabel(remain_state, no)){
        p->attribute = no;
        return p;
    }
    if (remain_attribute.size() == 0){//所有的属性均已经考虑完了,还没有分尽
        string label = MostCommonLabel(remain_state);
        p->attribute = label;
        return p;
    }

    double max_gain = 0, temp_gain;
    vector <string>::iterator max_it = remain_attribute.begin();
    vector <string>::iterator it1;
    for (it1 = remain_attribute.begin(); it1 < remain_attribute.end(); it1++){
        temp_gain = ComputeGain(remain_state, (*it1));
        if (temp_gain > max_gain) {
            max_gain = temp_gain;
            max_it = it1;
        }
    }
    //下面根据max_it指向的属性来划分当前样例,更新样例集和属性集
    vector <string> new_attribute;
    vector <vector <string> > new_state;
    for (vector <string>::iterator it2 = remain_attribute.begin(); it2 < remain_attribute.end(); it2++){
        if ((*it2).compare(*max_it)) new_attribute.push_back(*it2);
    }
    //确定了最佳划分属性,注意保存
    p->attribute = *max_it;
    vector <string> values = map_attribute_values[*max_it];
    int attribue_num = FindAttriNumByName(*max_it);
    new_state.push_back(attribute_row);
    for (vector <string>::iterator it3 = values.begin(); it3 < values.end(); it3++){
        for (unsigned int i = 1; i < remain_state.size(); i++){
            if (!remain_state[i][attribue_num].compare(*it3)){
                new_state.push_back(remain_state[i]);
            }
        }
        Node * new_node = new Node();
        new_node->arrived_value = *it3;
        if (new_state.size() == 0){//表示当前没有这个分支的样例,当前的new_node为叶子节点
            new_node->attribute = MostCommonLabel(remain_state);
        }
        else
            BulidDecisionTreeDFS(new_node, new_state, new_attribute);
        //递归函数返回时即回溯时需要1 将新结点加入父节点孩子容器 2清除new_state容器
        p->childs.push_back(new_node);
        new_state.erase(new_state.begin() + 1, new_state.end());//注意先清空new_state中的前一个取值的样例,准备遍历下一个取值样例
    }
    return p;
}

void Input(){
    string s;
    while (cin >> s, s.compare(endflag) != 0){//-1为输入结束
        item[0] = s;
        for (int i = 1; i < MAXLEN; i++){
            cin >> item[i];
        }
        state.push_back(item);//注意首行信息也输入进去,即属性
    }
    for (int j = 0; j < MAXLEN; j++){
        attribute_row.push_back(state[0][j]);
    }
}

void PrintTree(Node *p, int depth){
    for (int i = 0; i < depth; i++) cout << '\t';//按照树的深度先输出tab
    if (!p->arrived_value.empty()){
        cout << p->arrived_value << endl;
        for (int i = 0; i < depth + 1; i++) cout << '\t';//按照树的深度先输出tab
    }
    cout << p->attribute << endl;
    for (vector<Node*>::iterator it = p->childs.begin(); it != p->childs.end(); it++){
        PrintTree(*it, depth + 1);
    }
}

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

int main(){
    Input();
    vector <string> remain_attribute;

    string outlook("Outlook");
    string Temperature("Temperature");
    string Humidity("Humidity");
    string Wind("Wind");
    remain_attribute.push_back(outlook);
    remain_attribute.push_back(Temperature);
    remain_attribute.push_back(Humidity);
    remain_attribute.push_back(Wind);
    vector <vector <string> > remain_state;
    for (unsigned int i = 0; i < state.size(); i++){
        remain_state.push_back(state[i]);
    }
    ComputeMapFrom2DVector();
    root = BulidDecisionTreeDFS(root, remain_state, remain_attribute);
    cout << "the decision tree is :" << endl;
    PrintTree(root, 0);
    FreeTree(root);
    cout << endl;
    cout << "tree_size:" << tree_size << endl;
    system("pause");
    return 0;
}


/*测试数据:
Day Outlook Temperature Humidity Wind PlayTennis
1 Sunny Hot High Weak no
2 Sunny Hot High Strong no
3 Overcast Hot High Weak yes
4 Rainy Mild High Weak yes
5 Rainy Cool Normal Weak yes
6 Rainy Cool Normal Strong no
7 Overcast Cool Normal Strong yes
8 Sunny Mild High Weak no
9 Sunny Cool Normal Weak yes
10 Rainy Mild Normal Weak yes
11 Sunny Mild Normal Strong yes
12 Overcast Mild High Strong yes
13 Overcast Hot Normal Weak yes
14 Rainy Mild High Strong no
end
*/

 

--------------------------------------------------------------------------------------------------------

2. 建立决策树的关键,即在当前状态下选择哪个属性作为分类依据。

   根据不同的目标函数,建立决策树主要有一下三种算法。

       基于信息论的决策树算法有ID3、CART和C4.5等算法,其中C4.5和CART两种算法从ID3算法中衍生而来。

   CART和C4.5支持数据特征为连续分布时的处理,主要通过使用二元切分来处理连续型变量,即求一个特定的值-分裂值:特征值大于分裂值就走左子树,或者就走右子树。这个分裂值的选取的原则是使得划分后的子树中的“混乱程度”降低,具体到C4.5和CART算法则有不同的定义方式。

  ID3算法由Ross Quinlan发明,建立在“奥卡姆剃刀”的基础上:越是小型的决策树越优于大的决策树(be simple简单理论)。ID3算法中根据信息论的信息增益评估和选择特征,每次选择信息增益最大的特征做判断模块。ID3算法可用于划分标称型数据集,没有剪枝的过程,为了去除过度数据匹配的问题,可通过裁剪合并相邻的无法产生大量信息增益的叶子节点(例如设置信息增益阀值)。使用信息增益的话其实是有一个缺点,那就是它偏向于具有大量值的属性--就是说在训练集中,某个属性所取的不同值的个数越多,那么越有可能拿它来作为分裂属性,而这样做有时候是没有意义的,另外ID3不能处理连续分布的数据特征,于是就有了C4.5算法。CART算法也支持连续分布的数据特征。

  C4.5是ID3的一个改进算法,继承了ID3算法的优点。C4.5算法用信息增益率来选择属性,克服了用信息增益选择属性时偏向选择取值多的属性的不足在树构造过程中进行剪枝;能够完成对连续属性的离散化处理;能够对不完整数据进行处理。C4.5算法产生的分类规则易于理解、准确率较高;但效率低,因树构造过程中,需要对数据集进行多次的顺序扫描和排序。也是因为必须多次数据集扫描,C4.5只适合于能够驻留于内存的数据集。

  CART算法的全称是Classification And Regression Tree,采用的是Gini指数(选Gini指数最小的特征s)作为分裂标准,同时它也是包含后剪枝操作。ID3算法和C4.5算法虽然在对训练样本集的学习中可以尽可能多地挖掘信息,但其生成的决策树分支较大,规模较大。为了简化决策树的规模,提高生成决策树的效率,就出现了根据GINI系数来选择测试属性的决策树算法CART。

3. 其他目标:

   1)信息增益率: 信息增益比率度量是用ID3算法中的信息增益Gain(D,X)和分裂信息度量SplitInformation(D,X)来共同定义的。

                     分裂信息度量SplitInformation(D,X)就相当于特征X的信息熵。
 C4.5 (在ID3中用信息增益选择属性时偏向于选择分枝比较多的属性值,即取值多的属性,在C4.5中由于除以SplitInformation(D,X)=H(X),可以削弱这种作用。)

   2)Gini系数:

4. 基尼系数,熵,分类误差之间的关系:

 

从上图可以看出,基尼系数和熵之半的曲线非常接近,仅仅在45度角附近误差稍大。因此,基尼系数可以做为熵模型的一个近似替代。而CART分类树算法就是使用的基尼系数来选择决策树的特征。

同时,为了进一步简化,CART分类树算法每次仅仅对某个特征的值进行二分,而不是多分,这样CART分类树算法建立起来的是二叉树,而不是多叉树。这样一可以进一步简化基尼系数的计算,二可以建立一个更加优雅的二叉树模型。

 

5. 决策树C4.5算法的改进

 ID3算法有四个主要的不足,一是不能处理连续特征,第二个就是用信息增益作为标准容易偏向于取值较多的特征,最后两个是缺失值处理的问和过拟合问题。

 昆兰在C4.5算法中改进了上述4个问题。

 1)对于第一个问题,不能处理连续特征, C4.5的思路是将连续的特征离散化。

 2)对于第二个问题,信息增益作为标准容易偏向于取值较多的特征的问题。我们引入一个信息增益率的变量 ,它是信息增益和特征熵的比值。(启发式方法,从信息增益高于平均值的特征中选择信息增益率最高的特征)

 3)对于第三个缺失值处理的问题,主要需要解决的是两个问题,一是在样本某些特征缺失的情况下选择划分的属性,二是选定了划分属性,对于在该属性上缺失特征的样本的处理。

 4)对于第4个问题,C4.5引入了正则化系数进行初步的剪枝。

C4.5虽然改进或者改善了ID3算法的几个主要的问题,仍然有优化的空间。

    1)由于决策树算法非常容易过拟合,因此对于生成的决策树必须要进行剪枝。剪枝的算法有非常多,C4.5的剪枝方法有优化的空间。思路主要是两种,一种是预剪枝,即在生成决策树的时候就决定是否剪枝。

              另一个是后剪枝,即先生成决策树,再通过交叉验证来剪枝。后面在下篇讲CART树的时候我们会专门讲决策树的减枝思路,主要采用的是后剪枝加上交叉验证选择最合适的决策树。

    2)C4.5生成的是多叉树,即一个父节点可以有多个节点。很多时候,在计算机中二叉树模型会比多叉树运算效率高。如果采用二叉树,可以提高效率。

    3)C4.5只能用于分类,如果能将决策树用于回归的话可以扩大它的使用范围。

    4)C4.5由于使用了熵模型,里面有大量的耗时的对数运算,如果是连续值还有大量的排序运算。如果能够加以模型简化可以减少运算强度但又不牺牲太多准确性的话,那就更好了。

6. CART分类树算法的最优特征选择方法

    我们知道,在ID3算法中我们使用了信息增益来选择特征,信息增益大的优先选择。在C4.5算法中,采用了信息增益比来选择特征,以减少信息增益容易选择特征值多的特征的问题。

           但是无论是ID3还是C4.5,都是基于信息论的熵模型的,这里面会涉及大量的对数运算。能不能简化模型同时也不至于完全丢失熵模型的优点呢?

           有!CART分类树算法使用基尼系数来代替信息增益比,基尼系数代表了模型的不纯度,基尼系数越小,则不纯度越低,特征越好。这和信息增益(比)是相反的。

          对于CART分类树连续值的处理问题,其思想和C4.5是相同的,都是将连续的特征离散化。唯一的区别在于在选择划分点时的度量方式不同,C4.5使用的是信息增益,则CART分类树使用的是基尼系数。

   下面我们看看CART分类树建立算法的具体流程

    算法输入是训练集D,基尼系数的阈值,样本个数阈值。

    输出是决策树T。

    我们的算法从根节点开始,用训练集递归的建立CART树。

    1) 对于当前节点的数据集为D,如果样本个数小于阈值或者没有特征,则返回决策子树,当前节点停止递归。

    2) 计算样本集D的基尼系数,如果基尼系数小于阈值,则返回决策树子树,当前节点停止递归。

    3) 计算当前节点现有的各个特征的各个特征值对数据集D的基尼系数,对于离散值和连续值的处理方法和基尼系数的计算见第二节。缺失值的处理方法和上篇的C4.5算法里描述的相同。

    4) 在计算出来的各个特征的各个特征值对数据集D的基尼系数中,选择基尼系数最小的特征A和对应的特征值a。

            根据这个最优特征和最优特征值,把数据集划分成两部分D1和D2,同时建立当前节点的左右节点,做节点的数据集D为D1,右节点的数据集D为D2.

    5) 对左右的子节点递归的调用1-4步,生成决策树。

       对于生成的决策树做预测的时候,假如测试集里的样本A落到了某个叶子节点,而节点里有多个训练样本。则对于A的类别预测采用的是这个叶子节点里概率最大的类别。

 

 7. 总结:

    首先我们看看决策树算法的优点:

    1)简单直观,生成的决策树很直观。

    2)基本不需要预处理,不需要提前归一化,处理缺失值。

    3)使用决策树预测的代价低。

    4)既可以处理离散值也可以处理连续值。很多算法只是专注于离散值或者连续值。

    5)可以处理多维度输出的分类问题。

    6)相比于神经网络之类的黑盒分类模型,决策树在逻辑上可以得到很好的解释。

    7)可以交叉验证的剪枝来选择模型,从而提高泛化能力。

    8) 对于异常点的容错能力好,健壮性高。

  我们再看看决策树算法的缺点:

    1)决策树算法非常容易过拟合,导致泛化能力不强。可以通过设置节点最少样本数量和限制决策树深度来改进。

    2)决策树会因为样本发生一点点的改动,就会导致树结构的剧烈改变。这个可以通过集成学习之类的方法解决。

    3)寻找最优的决策树是一个NP难的问题,我们一般是通过启发式方法,容易陷入局部最优。可以通过集成学习之类的方法来改善。

    4)有些比较复杂的关系,决策树很难学习,比如异或。这个就没有办法了,一般这种关系可以换神经网络分类方法来解决。

    5)如果某些特征的样本比例过大,生成决策树容易偏向于这些特征。这个可以通过调节样本权重来改善。

 

Bootstraping 

Bootstraping的名称来自成语“pull up by your own bootstraps”,意思是依靠你自己的资源,称为自助法,它是一种有放回的抽样方法。
pull up by your own bootstraps”即“通过拉靴子让自己上升”,意思是“不可能发生的事情”。后来意思发生了转变,隐喻“不需要外界帮助,仅依靠自身力量让自己变得更好”。

Bagging

 

二. 随机森林:

随机森林就是通过集成学习的思想将多棵树集成的一种算法,它的基本单元是决策树,而它的本质属于机器学习的一大分支——集成学习(Ensemble Learning)方法。

简单来说,随机森林就是由多棵CART(Classification And Regression Tree)构成的。对于每棵树,它们使用的训练集是从总的训练集中有放回采样出来的,这意味着,总的训练集中的有些样本可能多次出现在一棵树的训练集中,也可能从未出现在一棵树的训练集中。在训练每棵树的节点时,使用的特征是从所有特征中按照一定比例随机地无放回的抽取的,根据Leo Breiman的建议,假设总的特征数量为M,这个比例可以是sqrt(M),1/2sqrt(M),2sqrt(M)。

随机森林的名称中有两个关键词,一个是“随机”,一个就是“森林”。“森林”我们很好理解,一棵叫做树,那么成百上千棵就可以叫做森林了,这样的比喻还是很贴切的,其实这也是随机森林的主要思想--集成思想的体现。“随机”的含义我们会在下边部分讲到。

其实从直观角度来解释,每棵决策树都是一个分类器(假设现在针对的是分类问题),那么对于一个输入样本,N棵树会有N个分类结果。而随机森林集成了所有的分类投票结果,将投票次数最多的类别指定为最终的输出,这就是一种最简单的 Bagging 思想。

bagging+决策树=随机森林

 随机森林的特点

  我们前边提到,随机森林是一种很灵活实用的方法,它有如下几个特点:

 

  • 在当前所有算法中,具有极好的准确率/It is unexcelled in accuracy among current algorithms;
  • 能够有效地运行在大数据集上/It runs efficiently on large data bases;
  • 能够处理具有高维特征的输入样本,而且不需要降维/It can handle thousands of input variables without variable deletion;
  • 能够评估各个特征在分类问题上的重要性/It gives estimates of what variables are important in the classification;
  • 在生成过程中,能够获取到内部生成误差的一种无偏估计/It generates an internal unbiased estimate of the generalization error as the forest building progresses;
  • 对于缺省值问题也能够获得很好得结果/It has an effective method for estimating missing data and maintains accuracy when a large proportion of the data are missing
  • ... ...

  实际上,随机森林的特点不只有这六点,它就相当于机器学习领域的Leatherman(多面手),你几乎可以把任何东西扔进去,它基本上都是可供使用的。在估计推断映射方面特别好用,以致都不需要像SVM那样做很多参数的调试。具体的随机森林介绍可以参见随机森林主页:Random Forest

 

 集成学习

集成学习通过建立几个模型组合的来解决单一预测问题。它的工作原理是生成多个分类器/模型,各自独立地学习和作出预测。这些预测最后结合成单预测,因此优于任何一个单分类的做出预测。

随机森林是集成学习的一个子类,它依靠于决策树的投票选择来决定最后的分类结果。你可以在这找到用python实现集成学习的文档:Scikit 学习文档

将若干个弱分类器的分类结果进行投票选择,从而组成一个强分类器,这就是随机森林bagging的思想。

(关于bagging的一个有必要提及的问题:bagging的代价是不用单棵决策树来做预测,具体哪个变量起到重要作用变得未知,所以bagging改进了预测准确率但损失了解释性。)。

 

每棵树的按照如下规则生成:

       有了树我们就可以分类了,但是森林中的每棵树是怎么生成的呢?

  1)如果训练集大小为N,对于每棵树而言,随机且有放回地从训练集中的抽取N个训练样本(这种采样方式称为bootstrap sample方法),作为该树的训练集;

  从这里我们可以知道:每棵树的训练集都是不同的,而且里面包含重复的训练样本(理解这点很重要)。

  为什么要随机抽样训练集?(add @2016.05.28)

  如果不进行随机抽样,每棵树的训练集都一样,那么最终训练出的树分类结果也是完全一样的,这样的话完全没有bagging的必要;

  为什么要有放回地抽样?(add @2016.05.28)

  我理解的是这样的:如果不是有放回的抽样,那么每棵树的训练样本都是不同的,都是没有交集的,这样每棵树都是"有偏的",都是绝对"片面的"(当然这样说可能不对),也就是说每棵树训练出来都是有很大的差异的;而随机森林最后分类取决于多棵树(弱分类器)的投票表决,这种表决应该是"求同",因此使用完全不同的训练集来训练每棵树这样对最终分类结果是没有帮助的,这样无异于是"盲人摸象"。

  2)如果每个样本的特征维度为M,指定一个常数m<<M,随机地从M个特征中选取m个特征子集,每次树进行分裂时,从这m个特征中选择最优的;

  3)每棵树都尽最大程度的生长,并且没有剪枝过程。

  一开始我们提到的随机森林中的“随机”就是指的这里的两个随机性。两个随机性的引入对随机森林的分类性能至关重要。由于它们的引入,使得随机森林不容易陷入过拟合,并且具有很好得抗噪能力(比如:对缺省值不敏感)。

随机森林分类效果(错误率)与两个因素有关:

  • 森林中任意两棵树的相关性:相关性越大,错误率越大;
  • 森林中每棵树的分类能力:每棵树的分类能力越强,整个森林的错误率越低。

  减小特征选择个数m,树的相关性和分类能力也会相应的降低;增大m,两者也会随之增大。所以关键问题是如何选择最优的m(或者是范围),这也是随机森林唯一的一个参数

 

袋外错误率(oob error)

  上面我们提到,构建随机森林的关键问题就是如何选择最优的m,要解决这个问题主要依据计算袋外错误率oob error(out-of-bag error)。

  随机森林有一个重要的优点就是,没有必要对它进行交叉验证或者用一个独立的测试集来获得误差的一个无偏估计。它可以在内部进行评估,也就是说在生成的过程中就可以对误差建立一个无偏估计。

  我们知道,在构建每棵树时,我们对训练集使用了不同的bootstrap sample(随机且有放回地抽取)。所以对于每棵树而言(假设对于第k棵树),大约有1/3的训练实例没有参与第k棵树的生成,它们称为第k棵树的oob样本。

  而这样的采样特点就允许我们进行oob估计,它的计算方式如下:

  (note:以样本为单位)

  1)对每个样本,计算它作为oob样本的树对它的分类情况(约1/3的树);

  2)然后以简单多数投票作为该样本的分类结果;

  3)最后用误分个数占样本总数的比率作为随机森林的oob误分率。

         oob误分率是随机森林泛化误差的一个无偏估计,它的结果近似于需要大量计算的k折交叉验证。

  (文献原文:Put each case left out in the construction of the kth tree down the kth tree to get a classification. In this way, a test set classification is obtained for each case in about one-third of the trees. At the end of the run, take j to be the class that got most of the votes every time case n was oob. The proportion of times that j is not equal to the true class of n averaged over all cases is the oob error estimate. This has proven to be unbiased in many tests.)

 

更多有关随机森林的代码:

  1)Fortran版本

  2)OpenCV版本

  3)Matlab版本

  4)R版本

 AdaBoost

AdaBoost,是英文"Adaptive Boosting"(自适应增强)的缩写,是一种机器学习方法,由Yoav Freund和Robert Schapire提出
AdaBoost方法的自适应在于:前一个分类器分错的样本会被用来训练下一个分类器。

AdaBoost方法对于噪声数据和异常数据很敏感。但在一些问题中,AdaBoost方法相对于大多数其它学习算法而言,不会很容易出现过拟合现象。

 w(样本权重)-->e(误差)-->α(分类器权重)

 三. GDBT

Gradient Boost Decision Tree:

   GBDT是一个应用很广泛的算法,可以用来做分类、回归。在很多的数据上都有不错的效果。GBDT这个算法还有一些其他的名字,比如说MART(Multiple Additive Regression Tree),GBRT(Gradient Boost Regression Tree),Tree Net等,其实它们都是一个东西(参考自wikipedia – Gradient Boosting),发明者是Friedman

   Gradient Boost其实是一个框架,里面可以套入很多不同的算法。

   Boost是"提升"的意思,一般Boosting算法都是一个迭代的过程,每一次新的训练都是为了改进上一次的结果。

  

   原始的Boost算法是在算法开始的时候,为每一个样本赋上一个权重值,初始的时候,大家都是一样重要的。在每一步训练中得到的模型,会使得数据点的估计有对有错,我们就在每一步结束后,增加分错的点的权重,减少分对的点的权重,这样使得某些点如果老是被分错,那么就会被“严重关注”,也就被赋上一个很高的权重。然后等进行了N次迭代(由用户指定),将会得到N个简单的分类器(basic learner),然后我们将它们组合起来(比如说可以对它们进行加权、或者让它们进行投票等),得到一个最终的模型。

   而Gradient Boost与传统的Boost的区别是,每一次的计算是为了减少上一次的残差(residual),而为了消除残差,我们可以在残差减少的梯度(Gradient)方向上建立一个新的模型。所以说,在Gradient Boost中,每个新的模型的简历是为了使得之前模型的残差往梯度方向减少,与传统Boost对正确、错误的样本进行加权有着很大的区别。

 

下面举个例子:

假设输入数据x可能属于5个分类(分别为1,2,3,4,5),训练数据中,x属于类别3,则y = (0, 0, 1, 0, 0),假设模型估计得到的F(x) = (0, 0.3, 0.6, 0, 0),则经过Logistic变换后的数据p(x) = (0.16,0.21,0.29,0.16,0.16),y - p得到梯度g:(-0.16, -0.21, 0.71, -0.16, -0.16)。观察这里可以得到一个比较有意思的结论:

假设gk为样本当某一维(某一个分类)上的梯度:

gk>0时,越大表示其在这一维上的概率p(x)越应该提高,比如说上面的第三维的概率为0.29,就应该提高,属于应该往“正确的方向”前进

越小表示这个估计越“准确”

gk<0时,越小,负得越多表示在这一维上的概率应该降低,比如说第二维0.21就应该得到降低。属于应该朝着“错误的反方向”前进

越大,负得越少表示这个估计越“不错误 ”

总的来说,对于一个样本,最理想的梯度是越接近0的梯度。所以,我们要能够让函数的估计值能够使得梯度往反方向移动(>0的维度上,往负方向移动,<0的维度上,往正方向移动)最终使得梯度尽量=0),并且该算法在会严重关注那些梯度比较大的样本,跟Boost的意思类似

得到梯度之后,就是如何让梯度减少了。这里是用的一个迭代+决策树的方法,当初始化的时候,随便给出一个估计函数F(x)(可以让F(x)是一个随机的值,也可以让F(x)=0),然后之后每迭代一步就根据当前每一个样本的梯度的情况,建立一棵决策树。就让函数往梯度的反方向前进,最终使得迭代N步后,梯度越小。

这里建立的决策树和普通的决策树不太一样,首先,这个决策树是一个叶子节点数J固定的,当生成了J个节点后,就不再生成新的节点了。

 

算法流程为:

0. 表示给定一个初始值

1. 表示建立M棵决策树(迭代M次)

2. 表示对函数估计值F(x)进行Logistic变换

3. 表示对于K个分类进行下面的操作(其实这个for循环也可以理解为向量的操作,每一个样本点xi都对应了K种可能的分类yi,所以yi, F(xi), p(xi)都是一个K维的向量,这样或许容易理解一点)

4. 表示求得残差减少的梯度方向

5. 表示根据每一个样本点x,与其残差减少的梯度方向,得到一棵由J个叶子节点组成的决策树

6. 为当决策树建立完成后,通过这个公式,可以得到每一个叶子节点的增益(这个增益在预测的时候用的)

每个增益的组成其实也是一个K维的向量,表示如果在决策树预测的过程中,如果某一个样本点掉入了这个叶子节点,则其对应的K个分类的值是多少。比如 说,GBDT得到了三棵决策树,一个样本点在预测的时候,也会掉入3个叶子节点上,其增益分别为(假设为3分类的问题):

(0.5, 0.8, 0.1),  (0.2, 0.6, 0.3),  (0.4, 0.3, 0.3),那么这样最终得到的分类为第二个,因为选择分类2的决策树是最多的。

7. 的意思为,将当前得到的决策树与之前的那些决策树合并起来,作为新的一个模型(跟6中所举的例子差不多)

 

posted @ 2016-11-25 17:06  静悟生慧  阅读(2558)  评论(0编辑  收藏  举报