Decision Tree, Bagging과 OOBDecision Tree, Bagging and OOB
학습지표
Decision Tree의 높은 variance를 줄이기 위해 Bagging은 데이터에 randomness를 주고, Random Forest는 feature 후보에도 randomness를 추가하며, Extra Trees는 분할 threshold까지 randomness를 추가한다.
Decision Tree는 비선형 관계와 변수 사이의 상호작용을 자연스럽게 표현한다. 전처리가 비교적 적고, 사람이 규칙을 따라갈 수 있다는 장점도 있다.
그러나 한 가지 약점이 크다. 학습 표본이 조금만 달라져도 Tree 구조와 예측이 크게 달라질 수 있다.
이번 글에서는 이 문제에서 출발해 다음 질문에 답한다.
- Decision Tree는 왜 흔들리는가?
- Bagging은 왜 Tree를 여러 개 만드는가?
- Bootstrap과 Aggregation은 각각 무슨 역할을 하는가?
- OOB는 어디서 생기고 무엇을 평가하는가?
1. Decision Tree는 왜 불안정할까?
1.1 상위 분할 하나가 전체 구조를 바꾼다
Decision Tree는 각 노드에서 데이터를 가장 잘 나누는 질문을 탐욕적(Greedy Algorithm)으로 선택한다.
petal width < 0.8인가?
최적 후보들의 점수가 비슷하다면 학습 데이터 몇 개만 추가되거나 빠져도 선택되는 분할이 달라질 수 있다. 상위 노드의 분할이 바뀌면 자식 노드가 받는 데이터도 달라지므로 이후 가지 구조가 연쇄적으로 변한다.
원본 데이터 D → Tree A → Decision Boundary A
조금 바뀐 데이터 D' → Tree B → Decision Boundary B
이 성질을 통계적인 언어로는 variance가 높다고 표현한다.
여기서 variance는 데이터 열의 분산이 아니다.
같은 모집단에서 학습 표본을 다시 뽑아 모델을 재학습한다면 예측이 얼마나 달라지는가?
깊은 Tree는 복잡한 패턴을 잡아 bias가 낮을 수 있지만, 표본의 우연한 변화와 잡음까지 따라가기 쉬워 variance가 커질 수 있다.
복잡하고 깊은 Tree: bias가 낮을 수 있지만 variance가 커질 수 있음
단순하고 얕은 Tree: variance는 작아질 수 있지만 bias가 커질 수 있음
이는 절대적인 법칙이라기보다 모델 복잡도를 조절할 때 나타나는 대표적인 trade-off다.
1.2 Tree 하나를 단순하게 만들면 되지 않을까?
max_depth, min_samples_leaf 같은 제약으로 Tree를 단순하게 만들면 흔들림을 줄일 수 있다. 그러나 지나치게 제한하면 중요한 비선형 관계까지 놓쳐 underfitting이 생길 수 있다.
Bagging은 다른 질문을 던진다.
개별 Tree의 표현력을 크게 포기하지 않으면서 여러 Tree의 흔들림을 결합 과정에서 상쇄할 수는 없을까?
이 질문에 대한 답이 Bootstrap Aggregating, 줄여서 Bagging이다.
2. Bagging: 서로 다르게 흔들리게 한 뒤 평균낸다
Bagging은 같은 종류의 기본 모델을 서로 다른 Bootstrap 표본에 학습하고 예측을 결합한다.
원본 학습 데이터
├─ Bootstrap 표본 1 → Tree 1 ─┐
├─ Bootstrap 표본 2 → Tree 2 ─┼→ 평균 또는 투표
└─ Bootstrap 표본 3 → Tree 3 ─┘
Tree들은 앞 Tree의 결과를 다음 Tree가 이어받지 않는다. 따라서 순차 의존 없이 별도로 학습할 수 있고 병렬화하기 쉽다.
이를 “Tree들이 통계적으로 독립적이다”라고 표현하면 정확하지 않다. 같은 원본 데이터와 Feature를 공유하므로 예측 오류는 서로 상관될 수 있다. 이 상관 문제는 다음 글의 Random Forest가 다룬다.
2.1 같은 Tree를 복사하는 것만으로는 부족하다
같은 데이터, 같은 알고리즘, 같은 설정으로 Tree를 100개 만들면 동일하거나 매우 비슷한 Tree가 만들어질 수 있다.
같은 학습 데이터
├─ Tree 1 ─┐
├─ Tree 2 ─┤ 비슷한 예측
└─ Tree 3 ─┘
같은 실수를 하는 모델 100개를 모아도 평균의 효과는 제한된다. Bagging은 먼저 학습 표본을 바꿔 각 모델에 서로 다른 학습 경험을 제공한다.
3. Bootstrap Sampling: 서로 다른 학습 표본 만들기
Bootstrap은 원본 학습 데이터에서 **복원 추출(with replacement)**하는 방법이다.
원본 데이터가 다음과 같다고 하자.
[A, B, C, D, E]
크기가 5인 Bootstrap 표본은 다음처럼 만들어질 수 있다.
표본 1: [A, A, C, D, E]
표본 2: [B, B, C, C, E]
표본 3: [A, B, D, D, D]
한 번 뽑은 관측치를 다시 뽑을 수 있으므로 중복이 생긴다. 반대로 어떤 관측치는 한 번도 선택되지 않을 수 있다.
max_samples=0.8의 정확한 의미
학습 데이터가 1,000개이고 다음과 같이 설정했다고 하자.
max_samples=0.8
bootstrap=True
각 모델은 원본의 80%에 해당하는 800번을 복원 추출한다. 서로 다른 관측치 800개가 반드시 포함된다는 뜻은 아니다. 중복 때문에 실제 고유 관측치 수는 800개보다 적다.
Bootstrap의 목적은 개별 Tree 하나를 반드시 더 정확하게 만드는 것이 아니다.
Tree들을 서로 다르게 흔들리게 만들어, 결합할 때 오차가 일부 상쇄될 조건을 만드는 것이다.
4. Aggregation: 흔들림을 안정성으로 바꾸기
Bootstrap 표본 D_1, D_2, ..., D_B로 모델 f_1, f_2, ..., f_B를 학습했다고 하자.
회귀에서는 예측값을 평균한다.
- = Tree 개수
- = b번째 Tree가 입력 에 대해 낸 예측값
- = 모든 Tree 예측을 다 더함
- = Tree 개수로 나눔
- = Bagging의 최종 예측
예를 들어 실제값이 50이고 다섯 Tree의 예측이 다음과 같다면:
Tree 1 → 43
Tree 2 → 58
Tree 3 → 48
Tree 4 → 55
Tree 5 → 46
평균 → 50
개별 Tree의 오차 방향이 완전히 같지 않다면 평균 과정에서 일부 오차가 상쇄된다. 그래서 Bagging을 대표적인 variance reduction 기법이라고 부른다.
분류의 결합 방식은 설명 층위를 구분할 필요가 있다.
- Breiman의 원래 Bagging 설명: 가장 많은 표를 받은 클래스를 선택하는 plurality vote
- scikit-learn
BaggingClassifier: 기본 모델이predict_proba()를 제공하면 클래스 확률을 평균하고, 제공하지 않으면 voting 사용
즉 “분류는 무조건 hard voting”이라고 외우기보다 사용하는 구현의 결합 방식을 확인해야 한다.
5. OOB: Bootstrap에서 자연스럽게 생기는 내부 추정치
원본 데이터가 N개이고 크기가 N인 Bootstrap 표본을 만든다고 하자. 특정 관측치 하나가 한 번도 선택되지 않을 확률은 다음과 같다.
따라서 표준적인 Bootstrap 표본 하나에는 평균적으로 다음과 같은 구성이 나타난다.
한 번 이상 등장한 고유 관측치: 약 63.2%
한 번도 등장하지 않은 관측치: 약 36.8%
한 Tree의 Bootstrap 표본에 포함되지 않은 관측치를 그 Tree의 Out-of-Bag(OOB) 표본이라고 한다.
5.1 OOB 평가는 어떻게 만들어질까?
각 학습 관측치에 대해 그 관측치를 보지 않고 학습한 Tree들의 예측만 모은다.
관측치 x_i
↓
x_i를 학습에 사용하지 않은 Tree만 선택
↓
그 Tree들의 예측을 집계
↓
y_i와 비교 → OOB 성능 추정치
OOB는 별도의 모델 재학습 없이 얻을 수 있는 편리한 신호다. 그러나 역할을 과장하면 안 된다.
- 36.8%는
N개에서N번 복원 추출할 때의 근사값이다. max_samples가 달라지면 OOB 비율도 달라진다.- OOB는 원래 학습 풀 내부에서 만든 추정치다.
- 최종 독립 테스트 세트인 final holdout과 같지 않다.
- 모든 상황에서 Cross Validation을 대체한다는 규칙도 아니다.
OOB는 “공짜 테스트 세트”보다 Bootstrap 과정에서 얻는 내부 보조 성능 추정치라고 부르는 편이 정확하다.
6. scikit-learn으로 Bagging과 OOB 확인하기
6.1 데이터 준비
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=42,
)
6.2 단일 Decision Tree
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(random_state=42)
tree.fit(X_train, y_train)
print("Decision Tree test accuracy:", tree.score(X_test, y_test))
한 번의 정확도만으로 모델의 안정성을 판단할 수는 없다. 여러 데이터 분할에서 예측이나 성능이 얼마나 달라지는지도 확인해야 한다.
6.3 BaggingClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(random_state=42),
n_estimators=100,
max_samples=1.0,
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42,
)
bagging.fit(X_train, y_train)
print("Bagging test accuracy:", bagging.score(X_test, y_test))
print("Bagging OOB accuracy:", bagging.oob_score_)
| 파라미터 | 의미 |
|---|---|
estimator | 반복해서 학습할 기본 모델 |
n_estimators | 기본 모델 수 |
max_samples | 각 모델을 위해 뽑을 표본 수 또는 비율 |
bootstrap=True | 표본을 복원 추출 |
oob_score=True | OOB 표본으로 내부 점수 계산 |
n_jobs=-1 | 사용 가능한 CPU 코어 활용 |
random_state | 무작위 과정 재현 |
scikit-learn 1.2 이전 코드에서는 estimator 대신 base_estimator가 보일 수 있다. 현재 API에서는 estimator를 사용한다.
Iris처럼 작고 쉬운 데이터에서는 두 모델의 한 번의 test accuracy가 같을 수도 있다. 이 결과 하나로 “Bagging이 항상 더 좋다”거나 “효과가 없다”고 결론 내리면 안 된다.
7. 이번 글의 핵심
Decision Tree
학습 표본이 조금 달라질 때 예측이 크게 바뀔 수 있는 고분산 모델이다.
Bagging
불안정한 모델을 버리는 대신 서로 다른 Bootstrap 표본에서 다르게 흔들리게 만들고, 예측을 집계해 흔들림을 줄인다.
OOB
각 모델이 학습 중 보지 않은 표본의 예측만 모아 만든 학습 풀 내부의 보조 성능 추정치다.
Bagging은 Tree뿐 아니라 여러 기본 추정기에 적용할 수 있다. 다만 표본 변화에 민감한 고분산 모델에서 특히 유용해 Decision Tree와 자주 결합한다. Tree 수를 늘리면 예측은 안정되는 경향이 있지만 개선은 포화되고 계산 비용은 증가한다.
그러나 Bootstrap을 사용해도 강력한 Feature 때문에 Tree들이 계속 비슷한 분할을 선택할 수 있다. Tree들의 오류가 비슷하면 평균 효과도 제한된다.
다음 글에서는 이 문제를 해결하기 위해 노드별 Feature 후보를 무작위화하는 Random Forest와 분할 임계값까지 무작위화하는 Extra Trees를 비교한다.
참고 자료
Learning goals
To reduce the high variance of a Decision Tree, Bagging adds randomness to the data, Random Forest also adds randomness to the feature candidates, and Extra Trees adds randomness all the way down to the split threshold.
A decision tree naturally expresses non-linear relationships and interactions between variables. It needs relatively little preprocessing, and people can follow its rules.
But it has one big weakness: if the training sample changes even slightly, the tree’s structure and predictions can change a lot.
Starting from this problem, this post answers the following questions.
- Why does a decision tree wobble?
- Why does bagging build many trees?
- What do bootstrap and aggregation each do?
- Where does OOB come from, and what does it evaluate?
1. Why are decision trees unstable?
1.1 One top-level split changes the whole structure
At each node, a decision tree greedily (Greedy Algorithm) chooses the question that splits the data best.
petal width < 0.8인가?
If the best candidates have similar scores, adding or removing just a few training examples can change which split is chosen. When a split near the top changes, the data its child nodes receive changes too, so the branch structure below changes in a cascade.
원본 데이터 D → Tree A → Decision Boundary A
조금 바뀐 데이터 D' → Tree B → Decision Boundary B
In statistical language, this property is described as high variance.
Here, variance does not mean the variance of a data column.
If we drew a new training sample from the same population and retrained the model, how much would the predictions change?
A deep tree can have low bias because it captures complex patterns, but it easily follows chance variation and noise in the sample as well, so its variance can grow.
복잡하고 깊은 Tree: bias가 낮을 수 있지만 variance가 커질 수 있음
단순하고 얕은 Tree: variance는 작아질 수 있지만 bias가 커질 수 있음
This is less an absolute law than a typical trade-off that appears when you tune model complexity.
1.2 Couldn’t we just make one tree simpler?
Constraints like max_depth and min_samples_leaf make a tree simpler and can reduce the wobble. But restricting it too much can miss important non-linear relationships and cause underfitting.
Bagging asks a different question.
Without giving up much of each tree’s expressive power, can we cancel out the wobble of many trees when we combine them?
The answer to that question is Bootstrap Aggregating, or Bagging for short.
2. Bagging: let them wobble differently, then average
Bagging trains base models of the same kind on different bootstrap samples and combines their predictions.
원본 학습 데이터
├─ Bootstrap 표본 1 → Tree 1 ─┐
├─ Bootstrap 표본 2 → Tree 2 ─┼→ 평균 또는 투표
└─ Bootstrap 표본 3 → Tree 3 ─┘
No tree inherits the result of the previous one. So they can be trained separately without sequential dependency, which makes them easy to parallelize.
Saying “the trees are statistically independent” would not be accurate, though. They share the same original data and features, so their prediction errors can be correlated. That correlation problem is what Random Forest, in the next post, deals with.
2.1 Copying the same tree is not enough
If you build 100 trees with the same data, the same algorithm and the same settings, you may get identical or very similar trees.
같은 학습 데이터
├─ Tree 1 ─┐
├─ Tree 2 ─┤ 비슷한 예측
└─ Tree 3 ─┘
Collecting 100 models that make the same mistakes gives averaging only a limited effect. Bagging first changes the training sample, giving each model a different learning experience.
3. Bootstrap sampling: building different training samples
Bootstrap draws from the original training data with replacement.
Suppose the original data looks like this.
[A, B, C, D, E]
A bootstrap sample of size 5 might come out like this.
표본 1: [A, A, C, D, E]
표본 2: [B, B, C, C, E]
표본 3: [A, B, D, D, D]
Because an observation that was drawn can be drawn again, duplicates appear. Conversely, some observations may never be selected.
What max_samples=0.8 actually means
Suppose there are 1,000 training examples and you set the following.
max_samples=0.8
bootstrap=True
Each model draws 800 times with replacement, 80% of the original size. This does not mean 800 distinct observations are guaranteed to be included. Because of duplicates, the actual number of unique observations is less than 800.
The goal of bootstrap is not necessarily to make any single tree more accurate.
It is to make the trees wobble differently, creating the conditions for some of their errors to cancel out when combined.
4. Aggregation: turning wobble into stability
Suppose models f_1, f_2, ..., f_B were trained on bootstrap samples D_1, D_2, ..., D_B.
For regression, the predictions are averaged.
- = number of trees
- = the prediction of the b-th tree for input
- = the sum of all tree predictions
- = divide by the number of trees
- = bagging’s final prediction
For example, if the true value is 50 and five trees predict the following:
Tree 1 → 43
Tree 2 → 58
Tree 3 → 48
Tree 4 → 55
Tree 5 → 46
평균 → 50
As long as the individual trees’ errors don’t all point the same way, some of the error cancels out in the averaging. That is why bagging is called a classic variance reduction technique.
For classification, the way predictions are combined depends on which level you are describing.
- Breiman’s original description of bagging: a plurality vote that picks the class with the most votes
- scikit-learn
BaggingClassifier: averages class probabilities if the base model providespredict_proba(), and uses voting otherwise
So rather than memorizing “classification is always hard voting”, check how the implementation you use combines predictions.
5. OOB: an internal estimate that comes naturally from bootstrap
Suppose there are N original examples and we build a bootstrap sample of size N. The probability that a particular observation is never selected is:
So a standard bootstrap sample has, on average, the following composition.
한 번 이상 등장한 고유 관측치: 약 63.2%
한 번도 등장하지 않은 관측치: 약 36.8%
The observations not included in a tree’s bootstrap sample are that tree’s Out-of-Bag (OOB) sample.
5.1 How is the OOB evaluation built?
For each training observation, collect only the predictions of the trees that were trained without seeing it.
관측치 x_i
↓
x_i를 학습에 사용하지 않은 Tree만 선택
↓
그 Tree들의 예측을 집계
↓
y_i와 비교 → OOB 성능 추정치
OOB is a convenient signal you get without retraining any models. But its role should not be overstated.
- 36.8% is the approximation for drawing
Ntimes with replacement fromNexamples. - If
max_sampleschanges, the OOB ratio changes too. - OOB is an estimate built inside the original training pool.
- It is not the same as a final holdout, the final independent test set.
- Nor is there a rule that it replaces cross-validation in every situation.
It is more accurate to call OOB an auxiliary internal performance estimate obtained from the bootstrap process than a “free test set”.
6. Checking bagging and OOB with scikit-learn
6.1 Preparing the data
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X,
y,
test_size=0.2,
stratify=y,
random_state=42,
)
6.2 A single decision tree
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(random_state=42)
tree.fit(X_train, y_train)
print("Decision Tree test accuracy:", tree.score(X_test, y_test))
You cannot judge a model’s stability from a single accuracy number. You also need to check how much the predictions or performance change across different data splits.
6.3 BaggingClassifier
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(random_state=42),
n_estimators=100,
max_samples=1.0,
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42,
)
bagging.fit(X_train, y_train)
print("Bagging test accuracy:", bagging.score(X_test, y_test))
print("Bagging OOB accuracy:", bagging.oob_score_)
| Parameter | Meaning |
|---|---|
estimator | The base model trained repeatedly |
n_estimators | Number of base models |
max_samples | Number or fraction of samples drawn for each model |
bootstrap=True | Draw samples with replacement |
oob_score=True | Compute an internal score on the OOB samples |
n_jobs=-1 | Use all available CPU cores |
random_state | Make the random process reproducible |
In code from before scikit-learn 1.2 you may see base_estimator instead of estimator. The current API uses estimator.
On small, easy data like Iris, the two models may get the same test accuracy on a single run. You should not conclude from that one result that “bagging is always better” or that “it has no effect”.
7. Key takeaways
Decision Tree
A high-variance model whose predictions can change a lot when the training sample changes slightly.
Bagging
Instead of discarding the unstable model, it makes models wobble differently on different bootstrap samples and aggregates their predictions to reduce the wobble.
OOB
An auxiliary performance estimate inside the training pool, built from each model’s predictions on the samples it did not see during training.
Bagging can be applied to many base estimators, not just trees. But it is especially useful for high-variance models that are sensitive to changes in the sample, so it is often paired with decision trees. Adding more trees tends to stabilize predictions, but the improvement saturates while the computation cost grows.
However, even with bootstrap, a strong feature can keep the trees choosing similar splits. If the trees make similar errors, the effect of averaging is limited.
In the next post, to solve this problem, I compare Random Forest, which randomizes the feature candidates at each node, and Extra Trees, which randomizes the split thresholds as well.