1. 程式人生 > >模式識別:分類迴歸決策樹CART的研究與實現

模式識別:分類迴歸決策樹CART的研究與實現

摘 要:本實驗的目的是學習和掌握分類迴歸樹演算法。CART提供一種通用的樹生長框架,它可以例項化為各種各樣不同的判定樹。CART演算法採用一種二分遞迴分割的技術,將當前的樣本集分為兩個子樣本集,使得生成的決策樹的每個非葉子節點都有兩個分支。因此,CART演算法生成的決策樹是結構簡潔的二叉樹。在MATLAB平臺上編寫程式,較好地實現了非剪枝完全二叉樹的建立、應用以及近似剪枝操作,同時把演算法推廣到多叉樹。

一、技術論述

1.非度量方法

在之前研究的多種模式分類演算法中,經常會使用到樣本或向量之間距離度量(distance metric)的方法。最典型的是最近鄰分類器,距離的概念是這個分類方法的根本思想所在。在神經網路中,如果兩個輸入向量足夠相似,則它們的輸出也很相似。總的來說,大多數的模式識別方法在研究這類問題中,由於特徵向量是實數資料,因此自然擁有距離的概念。

而在現實世界中,另外一類酚類問題中使用的是“語義資料”(nominal data),又稱為“標稱資料”或“名義資料”。這些資料往往是離散的,其中沒有任何相似性的概念,甚至沒有次序的關係。以下給出一個簡單的例子:

試用牙齒的資訊對魚和海洋哺乳動物分類。一些魚的牙齒細小而精緻(如巨大的鬚鯨),這種牙齒用於在海里篩濾出微小的浮游生物來吃;另一些有成排的牙齒(如鯊魚);一些海洋動物,如海象,有很長的牙齒;而另外一些,如魷魚,則根本沒有牙齒。這裡並沒有一個清楚的概念來表示關於牙齒的相似性或距離度量,打個比方,鬚鯨和海象的牙齒之間並不比鯊魚和魷魚之間更相似。本實驗的目的是將以往以實向量形式表示的模式,轉變成以非度量(nonmetric)的語義屬性來表示的模式。

2.判定樹

利用一系列的查詢回答來判斷和分類某模式是一種很自然和直觀的做法。有一個問題的提法依賴於前一個問題的回答。這種“問卷表”方式的做法對非度量資料特別有效,因為回答問題時的是“是/否”、“真/假”、“屬性值”等並不涉及任何距離測度概念。這些實際問題可以用有向的判定樹(decision tree)的形式表示。從結構上看,一棵樹的第一個節點又稱為根節點,存在於最上方,與其他節點通過分支實現有序的相連。繼續上述的構造過程,知道沒有後續的葉節點為止,下圖給出了一個判定樹的例項。

這裡寫圖片描述

從根節點開始,對模式的某一屬性的取值提問。與根節點相連的不同分支,對應這個屬性的不同取值,根據不同的結果轉向響應的後續子節點。這裡需要注意的是樹的各分支之間必須是互斥的,且各分支覆蓋了整個可能的取值空間。

在把當前到達的節點視為新的根節點,做同樣的分支判斷。不斷繼續這一過程直至到達葉節點。每個葉節點都擁有一個相應的類別標記,測試樣本被標記為它所到達的葉節點的類別標記。

決策樹演算法相比其他分類器(如神經網路)的優點之一是,樹中所體現的語義資訊,容易直接用邏輯表示式表示出。而樹分類器的另外一個有點是分類速度快。這是因為樹提供了一種很自然的嵌入人類專家的先驗知識的機制,在實際中,當問題較為簡單且訓練樣本較少的情況下,這類專家知識往往十分有效。

3.分類和迴歸樹(CART)演算法

綜合以上概念,這裡討論一個問題,即基於訓練樣本構造或“生成一棵判定樹”的問題。假設一個訓練樣本集D,該訓練集擁有類別標記,同時確定了一個用於判定模式的屬性集。對於一棵判定樹,其任務是把訓練樣本逐步劃分成越來越小的子集,一個理想的情況是每個子集中所有的樣本均擁有同種類別標記,樹的分類操作也到此結束,這類子集稱為“純”的子集。而一般情況下子集中的類別標記並不唯一,這時需要執行一個操作,要麼接受當前有“缺陷”的判決結果,停止繼續分類;要麼另外選取一個屬性進一步生長該樹,這一過程是一種遞迴結構的樹的生長過程。

若從資料結構的角度來看,資料表示在每個節點上,要麼該節點已經是葉節點(自身已擁有明確的類別標記),要麼利用另一種屬性,繼續分裂出子節點。分類和迴歸樹是僅有的一種通用的樹生長演算法。CART提供一種通用的框架,使用者可以將其例項化為各種不同的判定樹。

3.1 CART演算法的分支數目

節點處的一次判別稱為一個分支,它將訓練樣本劃分成子集。根節點處的分支對應於全部訓練樣本,氣候每一次判決都是一次子集劃分過程。一般情況下,節點的分支數目由樹的設計者確定,並且在一棵樹上可能有不同的值。從一個節點中分出去的分支數目有時稱為節點的分支率(branching ratio),這裡用B表示。這裡需要說明一個事實,即每個判別都可以用二值判別表示出來。由於二叉樹具有普適性,而且構造比較方便,因此被廣泛採用。

3.2 CART演算法中查詢的選取與節點不純度

在決策樹的設計過程中,一個重點是考慮在每個節點處應該選出測試或查詢哪一個屬性。我們知道對於數值資料,用判定樹的方法得到的分類邊界存在著較為自巨集觀的幾何解釋;而對於非數值資料,在節點處作查詢進而劃分資料的過程並沒有直接的幾何解釋。

構造樹的過程的一個基本原則是簡單。我們期望獲得的判定樹簡單緊湊,只有很少的節點。本著這一目標,應試圖尋找這樣一個查詢T,它能使後繼節點資料儘可能的“純”。這裡需要定義“不純度”的指標。用i(N)表示節點N的“不純度”,當節點上的模式資料均來自同一類別時,令i(N)=0;若類別標記均勻分佈時,i(N)應當比較大。一種最流行的測量稱為“熵不純度”(entropy impurity),又稱為資訊量不純度(information impurity):

這裡寫圖片描述

其中P(ωj)是節點N處屬於ωj類模式樣本數佔總樣本數的頻度。根據熵的特性,如果所有模式的樣本都來自同一類別,則不純度為零,否則則大於零。當且僅當所有類別以等概率出現時,熵值取最大值。以下給出另外幾種常用的不純度定義:

“平方不純度”,根據當節點樣本均來自單一類別時不純度為0的思想,用以下多項式定義不純度,該值與兩類分佈的總體分佈方差有關:

這裡寫圖片描述

“Gini不純度”,用於多類分類問題的方差不純度(也是當節點N的類別標記任意選取時對應的誤差率):

這裡寫圖片描述

當類別標記等概率時“Gini不純度”指標的峰度特性比“熵不純度”要好。

“誤分類不純度”,用於衡量節點N處訓練樣本分類誤差的最小概率:

這裡寫圖片描述

該指標在之前討論過的不純度指標中當等概率標記時具有最好的峰值特性。然而由於存在不連續的導數值,因而在連續引數空間搜尋最大值時會出現問題。

當給定一種不純度計算方法,另一個關鍵問題是:對於一棵樹,目前已生長到節點N,要求對該節點作屬性查詢T,應該如何選擇待查詢值s?一種思路是選擇那個能夠使不純度下降最快的那個查詢,不純度的下降公式可寫為:

這裡寫圖片描述

其中N_L和N_R分別表示左右子節點,i(N_L)和i(N_R)是相應的不純度。P_L是當查詢T被採納時,樹由N生長到N_L的概率。若採用熵不純度指標,則不純度的下降差就是本次查詢所能提供的資訊增益。由於二叉樹的每次查詢僅僅給出是/否的回答,所以每次分支所引起的熵不純度的下降差不會超過1位。

二、實驗步驟

訓練資料:

這裡寫圖片描述

編寫一個生成二叉分類樹的通用程式,並使用上表中的資料來訓練該樹,訓練過程中使用熵不純度進行分支。通過過程中的判決條件,使用treeplot函式畫出決策二叉樹,如下圖所示。

這裡寫圖片描述

用上述程式訓練好的非剪枝完全樹,分類下列模式:
{A,E,I,L,N},{D,E,J,K,N},{B,F,J,K,M},{C,D,J,L,N}

用上述程式訓練好的非剪枝完全樹,選擇其中的一對葉節點進行剪枝,使剪枝後樹的熵不純度的增量儘可能小。

三、實驗結果

使用未剪枝的樹進行分類:

這裡寫圖片描述

使用剪枝後的樹進行分類:

這裡寫圖片描述

四、MATLAB程式碼

主函式:

clear all; clc; close all;
%% 資料預處理 
% 訓練樣本
w1 = ['AEHKM'; 'BEILM'; 'AGILN'; 'BGHKM'; 'AGILM'];
w2 = ['BFILM'; 'BFJLN'; 'BEILN'; 'CGJKN'; 'CGJLM'; 'DGJKM'; 'BDILM'];
w3 = ['DEHKN'; 'AEHKN'; 'DEHLN'; 'DFJLN'; 'AFHKN'; 'DEJLM'; 'CFJLM'; 'DFHLM'];
w = [w1; w2; w3];   

C = [ones(5,1); 2*ones(7,1); 3*ones(8,1)]; % 分類標籤

% 資料範圍
Region = ['AD'; 'EG'; 'HJ'; 'KL'; 'MN'];

%測試樣本
T1 = 'AEILN';
T2 = 'DEJKN';
T3 = 'BFJKM';
T4 = 'CDJLM';


%字串矩陣 資料轉化為 相應的自然數矩陣,方便後續處理
w = w + 0;   % w=abs(w)
Region = abs(Region);

global Tree;
global Flag;

%% 非剪枝完全樹
%建立一二叉樹並用樣本進行訓練
Tree = CART_MakeBinaryTree(w, C, Region)
p=[0 1 2 3 4 4 3 2 8 9 10 10 9 8 1 15 15];
treeplot(p)%畫出二叉樹


%使用該分類樹
W1 = CART_UseBinaryTree(Tree,T1)
W2 = CART_UseBinaryTree(Tree,T2)
W3 = CART_UseBinaryTree(Tree,T3)
W4 = CART_UseBinaryTree(Tree,T4)   

%% B 剪枝

W1=CART_PruningBinaryTree(Tree,T1)
W2=CART_PruningBinaryTree(Tree,T2)
W3=CART_PruningBinaryTree(Tree,T3)
W4=CART_PruningBinaryTree(Tree,T4)  

%% 多叉樹分類
%AnyTree=CART_MakeAnyTree(w, C, Region)
%MultiTree=CART_MakeMultiTree(w, C, Region)
function Tree = CART_MakeBinarySortTree(Train_Samples, TrainingTargets, Region)
% 基於 熵不純度 遞迴地實現  非剪枝完全二叉樹
% 輸入變數:
%   Train_Samples:n個d維訓練樣本,為(n * d)的矩陣
%   TrainingTargets:對應的類別屬性,為(n * 1)的矩陣
%   Region:特徵向量維度順序下上限,為(d * 2)的矩陣(特徵值取離散的自然數區間,左小右大)
% 輸出變數:一個基本樹形節點 Tree  

% 基本樹形節點結構 
% 一:標籤(記錄當前節點判定所用的維度,表葉子時為空);
% 二:閾值(記錄當前所用維度判定之閾值,葉子節點時表類別);
% 三:左枝(小於等於閾值的待分目標 歸於此,表葉子時為空)
% 四:右枝(大於閾值的 歸於此,表葉子時為空)

[n,Dim] = size(Train_Samples); 
[t,m] = size(Region);
if Dim ~= t || m ~= 2
    disp('引數錯誤,請檢查');
    return;
end

%檢查類別屬性是否只有一個屬性,若是則當前為葉節點,否則需要繼續分
if ( length(unique(TrainingTargets)) == 1)
   Tree.Label    = [];
   Tree.Value    = TrainingTargets(1); 
   % 無左右子節點
   Tree.Right    = [];
   Tree.Left     = [];  
   Tree.Num = n;
   return;
end

% 如果兩個樣本 為兩類 直接設定為左右葉子
% 差異最大維度做為查詢專案
% 單獨處理此類情況,做為一種優化方法應對 後面提到的缺陷
if length(TrainingTargets) == 2
   [m,p] = max(abs(Train_Samples(1,:) -Train_Samples(2,:)));
   Tree.Label = p;
   Tree.Value = ((Train_Samples(1,p) +Train_Samples(2,p))/2);
   Tree.Num = n;
   BranchRight.Right    = [];
   BranchRight.Left     = [];
   BranchRight.Label    = [];
   BranchRight.Num =1;

   BranchLeft.Right    = [];
   BranchLeft.Left     = [];
   BranchLeft.Label    = [];   
   BranchLeft.Num =1;

   if Train_Samples(1,p) > Tree.Value
       BranchRight.Value   = TrainingTargets(1);
       BranchLeft.Value    = TrainingTargets(2);
   else
       BranchRight.Value   = TrainingTargets(2);
       BranchLeft.Value    = TrainingTargets(1);       
   end

   Tree.Right = BranchRight;
   Tree.Left = BranchLeft;   
   return;
end

%確定節點的標籤(當前節點判定所用的維度),熵不純度下降落差最大的維度當選

%依次計算各個維度當選之後所造成的不純度之和
    %每個維度中可選值中 最大值代表本維度
Dvp=zeros(Dim,2); %記錄每個維度中最大的不純度及相應的閾值
for k=1:Dim
    EI=-20*ones(Region(k,2)-Region(k,1)+1,1);
    Iei=0;
    for m = Region(k,1):Region(k,2)
        Iei =Iei +1;
        %計算臨時分類結果 去右邊的記為 1
        CpI = Train_Samples(:,k) > m;
        SumCpI = sum(CpI);
        if SumCpI == n || SumCpI == 0 %分到一邊去了,不妥,直接考察下一個
            continue;
        end
        CpI = [not(CpI),CpI];       
        EIt = zeros(2,1);
        %統計預計 新分到左右兩枝的類別及相應的比率,然後得出熵不純度
        for j = 1:2
            Cpt = TrainingTargets(CpI(:, j)); 

           if ( length(unique(Cpt)) == 1) %應對 hist() 在處理同一元素時所存在的異常問題
               Pw = 0;
           else
               Pw = hist(Cpt,unique(Cpt));
               Pw=Pw/length(Cpt);  % 被分到該類的比率
               Pw=Pw.*log2(Pw);                    
           end

            EIt(j) = sum(Pw);
        end
        Pr = length(Cpt)/n;

        EI(Iei) = EIt(1) *(1-Pr) + EIt(2) *Pr;
    end
    [maxEI, p] = max(EI);  
    NmaxEI = sum(EI == maxEI);
    if NmaxEI > 1  %如果最大值有多個,取中間那一個, 稍微改進了預設地只取第一個最大值的缺陷
        t = find(EI == maxEI); 
        p = round(NmaxEI /2);          
        p = t(p);
    end

    Dvp(k,1) = maxEI;
    Dvp(k,2) = Region(k,1) +p -1;
end

%更新節點標籤和閾值
[maxDv, p] = max(Dvp(:,1));  
    NmaxDv = sum(Dvp(:,1) == maxDv);
    if NmaxDv > 1  %如果最大值有多個,採用取值範圍較小的那一個維度屬性, 稍微改進了預設地只取第一個最大值的缺陷
        t = find(Dvp(:,1) == maxDv); 
        [D,p] = min(Region(t,2) -Region(t,1));
        p = t(p);
    end   

   Tree.Label = p;
   Tree.Value = Dvp(p,2);

%將訓練樣本分成兩類,對左右子節點分別形成二叉樹,需要進行遞迴呼叫
   CprI = Train_Samples(:,p) > Dvp(p,2);
   CplI = not(CprI);  
   Tree.Num = n; 
   Tree.Right = CART_MakeBinarySortTree(Train_Samples(CprI,:), TrainingTargets(CprI), Region);
   Tree.Left = CART_MakeBinarySortTree(Train_Samples(CplI,:), TrainingTargets(CplI), Region);
% 針對 由函式 CART_MakeBinaryTree()生成的二叉樹 Tree,給出 Test 類屬 W
function W = CART_UseBinarySortTree(tree, testSamples)

% 當前節點不存在左右子結點時,判定為葉節點並返回其類別屬性
if isempty(tree.Right) && isempty(tree.Left)
    W = tree.Value;
    return;
end

% 非葉節點的判決過程
if testSamples(tree.Label) > tree.Value
    W = CART_UseBinarySortTree(tree.Right,testSamples);
else
    W = CART_UseBinarySortTree(tree.Left,testSamples);
end
function W = CART_PruningBinarySortTree(tree, Samples)
% 針對 由函式 CART_MakeBinaryTree()生成的二叉樹 Tree,按樣本 Samples 遍歷Tree
%如果 發現有與葉子共父節點的長枝條,並且其數量不多於葉子中的樣本數,則將其父節點設為葉子,類別屬性取多數
%由於程式編寫上一時間沒能用Matlab語言實現對二叉樹的修改,所以沒有真實地修剪樹,但是起到的修剪之後的效果, 

%遇到葉子時,就可以返回其類別屬性
TempLift=tree.Left;
TempRight=tree.Right;

LeftEmpty =isempty(TempLift.Right) && isempty(TempLift.Left);
RightEmpty=isempty(TempRight.Right) && isempty(TempRight.Left);

if LeftEmpty && RightEmpty %遇到掛有 兩葉子節點,執行剪枝,擇多歸類

    if TempLift.Num > TempRight.Num 
        W=TempLift.Value;
    else
        W=TempRight.Value;
    end

    return;
elseif LeftEmpty || RightEmpty %遇到掛有 一個葉子 和 一個子父節點,比較,擇多歸類

    if LeftEmpty && (TempLift.Num > TempRight.Num /3)
        W=TempLift.Value;
        return;
    elseif RightEmpty && (TempRight.Num > TempLift.Num /3)
        W=TempRight.Value;
        return;
    end        
end

if Samples(tree.Label) > tree.Value
    if RightEmpty
        W = tree.Right.Value;
        return;
    else
        W = CART_PruningBinarySortTree(tree.Right,Samples);
    end
else
    if LeftEmpty
        W = tree.Left.Value;
        return;            
    else
        W = CART_PruningBinarySortTree(tree.Left,Samples);
    end
end

參考書籍:Richard O. Duda, Peter E. Hart, David G. Stork 著《模式分類》