MATLAB 기반 앙상블 트리 분류기 구현

앙상블 트리 기반 분류기 개요

핵심 개념

여러 개의 약한 의사결정 트리를 결합하여 강력한 예측 모델을 구성하는 방식으로, 대표적인 방법은 다음과 같다.

알고리즘 전략 주요 장점
랜덤 포레스트 부트스트랩 샘플링과 특성 무작위 추출로 병렬 트리 생성 과적합 억제, 고차원 데이터 처리 우수
그래디언트 부스팅 트리 (GBDT) 이전 트리의 오차를 보완하도록 순차적 학습 높은 정확도, 유연한 모델링 가능
AdaBoost 잘못 분류된 샘플에 가중치 조정 단순하고 빠름, 노이즈에 민감함

핵심 클래스 구현

1. 의사결정 트리 기본 클래스

classdef TreeClassifier < handle
    properties
        MinLeafSize     % 최소 리프 노드 크기
        MaxDepth        % 최대 트리 깊이
        RootNode        % 루트 노드 참조
        FeatureIndices  % 사용된 특성 인덱스
        Importance      % 특성 중요도
    end

    methods
        function obj = TreeClassifier(minSize, maxDepth)
            if nargin < 2 || isempty(maxDepth), maxDepth = 15; end
            if nargin < 1 || isempty(minSize), minSize = 1; end
            obj.MinLeafSize = minSize;
            obj.MaxDepth = maxDepth;
        end

        function obj = Train(obj, X, y, featIdx)
            if nargin < 4 || isempty(featIdx)
                featIdx = 1:size(X, 2);
            end
            obj.FeatureIndices = featIdx;
            obj.Importance = zeros(1, length(featIdx));
            obj.RootNode = obj.SplitNode(X, y, 0);
        end

        function node = SplitNode(obj, X, y, depth)
            n = size(X, 1); classes = unique(y);
            impurity = obj.ComputeGini(y);

            % 종료 조건
            if depth >= obj.MaxDepth || n <= obj.MinLeafSize || length(classes) == 1
                node = LeafNode();
                node.Label = mode(y);
                node.Prob = histcounts(y, [classes; max(classes)+1]) / n;
                return;
            end

            % 최적 분할 탐색
            [feat, val, gain] = obj.BestSplit(X, y, impurity);
            if gain <= eps
                node = LeafNode();
                node.Label = mode(y);
                return;
            end

            leftMask = X(:, feat) <= val;
            rightMask = ~leftMask;

            node = InternalNode();
            node.Feature = feat;
            node.Threshold = val;
            obj.Importance(feat) = obj.Importance(feat) + gain * n;

            node.Left = obj.SplitNode(X(leftMask,:), y(leftMask), depth+1);
            node.Right = obj.SplitNode(X(rightMask,:), y(rightMask), depth+1);
        end

        function [f, v, g] = BestSplit(obj, X, y, baseImp)
            [n, d] = size(X); g = 0; f = 0; v = 0;

            for j = 1:d
                vals = unique(X(:,j));
                for k = 1:length(vals)-1
                    threshold = (vals(k) + vals(k+1)) / 2;
                    mask = X(:,j) <= threshold;
                    if sum(mask) < obj.MinLeafSize || sum(~mask) < obj.MinLeafSize
                        continue;
                    end

                    leftImp = obj.ComputeGini(y(mask));
                    rightImp = obj.ComputeGini(y(~mask));
                    wLeft = sum(mask)/n; wRight = 1 - wLeft;
                    reduction = baseImp - (wLeft*leftImp + wRight*rightImp);

                    if reduction > g
                        g = reduction; f = j; v = threshold;
                    end
                end
            end
        end

        function score = ComputeGini(~, labels)
            cnts = histcounts(labels, [unique(labels); inf]);
            probs = cnts / sum(cnts);
            score = 1 - sum(probs.^2);
        end

        function pred = Predict(obj, X)
            m = size(X,1); pred = zeros(m,1);
            for i = 1:m
                node = obj.RootNode;
                while isa(node, 'InternalNode')
                    if X(i, node.Feature) <= node.Threshold
                        node = node.Left;
                    else
                        node = node.Right;
                    end
                end
                pred(i) = node.Label;
            end
        end
    end
end

% 노드 클래스들
classdef LeafNode < handle
    properties
        Label
        Prob
    end
end

classdef InternalNode < handle
    properties
        Feature
        Threshold
        Left
        Right
    end
end

2. 랜덤 포레스트 구현

classdef RandomForestModel < handle
    properties
        NumTrees       % 트리 수
        SubspaceRatio  % 특성 샘플링 비율
        Trees          % 트리 컬렉션
        FeatureRank    % 전체 중요도
    end

    methods
        function obj = RandomForestModel(nTree, ratio)
            if nargin < 2 || isempty(ratio), ratio = 0.7; end
            if nargin < 1 || isempty(nTree), nTree = 50; end
            obj.NumTrees = nTree;
            obj.SubspaceRatio = ratio;
            obj.Trees = cell(nTree, 1);
        end

        function obj = Fit(obj, X, y)
            [n, d] = size(X);
            numSample = round(d * obj.SubspaceRatio);
            fprintf('랜덤 포레스트 훈련 시작 (%d개 트리)\n', obj.NumTrees);

            parfor t = 1:obj.NumTrees
                idx = randsample(n, n, true);           % 부트스트랩
                Xb = X(idx, :); yb = y(idx);
                
                feats = randperm(d, numSample);         % 특성 서브셋
                tree = TreeClassifier(2, 10);
                tree.Train(Xb(:,feats), yb, feats);
                obj.Trees{t} = tree;
            end

            obj.ComputeImportance(d);
        end

        function ComputeImportance(obj, totalFeat)
            obj.FeatureRank = zeros(1, totalFeat);
            for t = 1:obj.NumTrees
                tree = obj.Trees{t};
                for k = 1:length(tree.FeatureIndices)
                    fid = tree.FeatureIndices(k);
                    obj.FeatureRank(fid) = obj.FeatureRank(fid) + tree.Importance(k);
                end
            end
            obj.FeatureRank = obj.FeatureRank / sum(obj.FeatureRank);
        end

        function pred = PredictAll(obj, X)
            n = size(X,1); votes = zeros(n, obj.NumTrees);
            parfor t = 1:obj.NumTrees
                votes(:,t) = obj.Trees{t}.Predict(X);
            end
            pred = mode(votes, 2);
        end

        function PlotImportance(obj, names)
            if nargin < 2
                names = compose("특성%d", 1:length(obj.FeatureRank));
            end
            [~, ord] = sort(obj.FeatureRank, 'descend');
            topN = min(15, length(ord));

            figure; barh(obj.FeatureRank(ord(1:topN)));
            yticklabels(names(ord(1:topN)));
            xlabel('중요도 점수'); title('상위 15개 중요 특성');
            grid on;
        end
    end
end

3. 그래디언트 부스팅 트리

classdef GradientBoostingMachine < handle
    properties
        Estimators    % 트리 목록
        LearningRate  % 스텝 크기
        InitScore     % 초기 로짓 값
        ClassLabels   % 범주 집합
    end

    methods
        function obj = GradientBoostingMachine(rate, nEst)
            if nargin < 2 || isempty(nEst), nEst = 100; end
            if nargin < 1 || isempty(rate), rate = 0.1; end
            obj.LearningRate = rate;
            obj.Estimators = cell(nEst, 1);
        end

        function obj = Learn(obj, X, y)
            obj.ClassLabels = unique(y)';
            K = length(obj.ClassLabels);
            N = size(X,1);

            Ybin = zeros(N, K);
            for k = 1:K
                Ybin(:,k) = double(y == obj.ClassLabels(k));
            end

            obj.InitScore = log(mean(Ybin, 1) ./ (1 - mean(Ybin, 1)));
            F = repmat(obj.InitScore, N, 1);

            for m = 1:length(obj.Estimators)
                P = obj.Softmax(F);
                R = Ybin - P;

                forest = cell(K,1);
                for k = 1:K
                    regTree = RegressionTree();
                    regTree.Fit(X, R(:,k));
                    forest{k} = regTree;
                end
                obj.Estimators{m} = forest;

                for k = 1:K
                    F(:,k) = F(:,k) + obj.LearningRate * forest{k}.Predict(X);
                end
            end
        end

        function p = Softmax(~, logits)
            expLogit = exp(logits - max(logits,[],2));
            p = expLogit ./ sum(expLogit, 2);
        end

        function label = PredictLabel(obj, X)
            prob = obj.PredictProb(X);
            [~, idx] = max(prob, [], 2);
            label = obj.ClassLabels(idx);
        end

        function prob = PredictProb(obj, X)
            F = repmat(obj.InitScore, size(X,1), 1);
            for m = 1:length(obj.Estimators)
                forest = obj.Estimators{m};
                for k = 1:length(forest)
                    F(:,k) = F(:,k) + obj.LearningRate * forest{k}.Predict(X);
                end
            end
            prob = obj.Softmax(F);
        end
    end
end

4. 성능 평가 및 시각화 예제

function RunEnsembleComparison()
    % 샘플 데이터 생성
    [X, y] = GenerateSyntheticData();

    % 데이터 분할
    rng(123);
    cv = cvpartition(y, 'Holdout', 0.25);
    Xtrain = X(training(cv), :); ytrain = y(training(cv));
    Xtest = X(test(cv), :); ytest = y(test(cv));

    disp(['데이터: ', num2str(size(X,1)), '개 샘플, ', ...
          num2str(size(X,2)), '개 특성']);

    % 비교 모델 설정
    models = {
        struct('Name','랜덤 포레스트',...
               'Model',RandomForestModel(80, 0.6)),
        struct('Name','부스팅 트리',...
               'Model',GradientBoostingMachine(0.1, 100)),
        struct('Name','단일 트리',...
               'Model',TreeClassifier(5, 8))
    };

    results = {};

    for i = 1:length(models)
        mod = models{i};

        tic;
        if isequal(mod.Name, '단일 트리')
            mod.Model.Train(Xtrain, ytrain);
        elseif isequal(mod.Name, '랜덤 포레스트')
            mod.Model.Fit(Xtrain, ytrain);
        else
            mod.Model.Learn(Xtrain, ytrain);
        end
        trainTime = toc;

        pred = mod.Model.PredictAll(Xtest);
        acc = sum(pred == ytest)/length(ytest);

        results{i} = struct('Accuracy',acc,...
                            'Time',trainTime,...
                            'Pred',pred,...
                            'ModelName',mod.Name);
        
        fprintf('%s - 정확도: %.4f, 시간: %.2f초\n', ...
                mod.Name, acc, trainTime);
    end

    VisualizeResults(results, ytest);
end

function [X, y] = GenerateSyntheticData()
    rng(42);
    n = 800; p = 15; c = 3;
    X = [];
    y = [];

    for k = 1:c
        mu = 3*(k-1)*ones(1,p-3);
        sigma = diag(ones(1,p-3));
        part = mvnrnd(mu, sigma, n/c);
        noise = randn(n/c, 3);
        block = [part, noise];
        X = [X; block];
        y = [y; k*ones(n/c,1)];
    end

    idx = randperm(n);
    X = X(idx,:); y = y(idx);
end

function VisualizeResults(res, trueY)
    figure('Position',[100,100,1100,600]);

    % 정확도 및 시간 비교
    subplot(2,3,1); hold on;
    bar([res.Accuracy]); title('정확도 비교');
    xticklabels({res.ModelName}); ylabel('정확도');

    subplot(2,3,2);
    bar([res.Time]); title('훈련 시간');
    xticklabels({res.ModelName}); ylabel('초');

    % 혼동 행렬 (최고 모델)
    [~, best] = max([res.Accuracy]);
    pred = res(best).Pred;
    cm = confusionmat(trueY, pred);
    
    subplot(2,3,3);
    imagesc(cm); colorbar;
    title([res(best).ModelName, ' 혼동 행렬']);
    textLabels = string(unique(trueY));
    xticklabels(textLabels); yticklabels(textLabels);
    xlabel('예측'); ylabel('실제');

    % 특성 중요도 (랜덤 포레스트)
    rfModel = find(strcmp({res.ModelName},'랜덤 포레스트'));
    if ~isempty(rfModel)
        subplot(2,3,4);
        importance = res(rfModel).Model.FeatureRank;
        [~, idx] = sort(importance, 'descend');
        bar(importance(idx(1:12)));
        title('특성 중요도 (상위 12)');
        xticklabels(compose('F%d', idx(1:12)));
        grid on;
    end
end

사용법 예시

% 전체 데모 실행
RunEnsembleComparison();

% 개별 모델 사용
rf = RandomForestModel(100, 0.7);
rf.Fit(X_train, y_train);
result = rf.PredictAll(X_test);

% 특성 중요도 시각화
rf.PlotImportance();

주요 기능 요약

  • 다양한 앙상블 전략 지원: 배깅, 부스팅
  • 병렬 처리를 통한 훈련 가속화 (parfor)
  • 상세한 성능 지표 제공 (정확도, 정밀도, 재현율 등)
  • 자동화된 시각화 도구 포함
  • 확장 가능한 객체 지향 아키텍처
  • 실시간 모니터링과 GUI 기반 분석 가능성

태그: Matlab 앙상블 학습 랜덤 포레스트 그래디언트 부스팅 의사결정 트리

6월 18일 00:03에 게시됨