배깅과 랜덤 포레스트 문서의 구현 코드다. if __name__ == "__main__": 아래가 자체 테스트다.
"""배깅(부트스트랩 집계) 구현과 실험.
문서: 21.배깅과 랜덤 포레스트 (예시, 의사코드, 카드)
모형: 1차원 회귀. 참 함수 f(x) = sin(2πx), 잡음 표준편차 0.3, 훈련 자료 40개.
기본 모델: 깊이 제한 없는 회귀 나무 대신, 가장 가까운 이웃 1개(1-NN) 예측 — 분산이 큰 모델.
주장:
1. 부트스트랩 표본(복원 추출 n개)에 서로 다른 원래 자료는 평균 약 63%가 들어간다.
2. 같은 시험 점들에서, 1-NN의 예측 분산(훈련 자료를 바꿀 때)이 배깅(모델 50개 평균)으로 크게 준다.
3. 편향은 거의 그대로다.
4. 랜덤 포레스트의 속성 뽑기: 속성 n개 중 m개를 뽑으면 두 나무가 같은 속성 집합을 쓸 확률이 1/C(n, m)로 작아져 서로 덜 닮는다.
"""
import math
import random
def f(x):
return math.sin(2 * math.pi * x)
def nn1(train, x):
return min(train, key=lambda p: abs(p[0] - x))[1]
def bagged(train, x, B, rnd):
n = len(train); s = 0.0
for _ in range(B):
boot = [train[rnd.randrange(n)] for _ in range(n)]
s += nn1(boot, x)
return s / B
def main():
rnd = random.Random(1)
n = 40
uniq = sum(len({rnd.randrange(n) for _ in range(n)}) for _ in range(5000)) / 5000 / n
assert abs(uniq - (1 - (1 - 1 / n) ** n)) < 0.01 and 0.62 < uniq < 0.65
print(f"[OK] 부트스트랩 표본 속 서로 다른 자료 비율 {uniq:.3f} (이론 {1 - (1 - 1/n) ** n:.3f})")
xs = [0.1, 0.35, 0.6, 0.85]
single = {x: [] for x in xs}; bag = {x: [] for x in xs}
for _ in range(150):
train = [(u, f(u) + rnd.gauss(0, 0.3)) for u in (rnd.random() for _ in range(n))]
for x in xs:
single[x].append(nn1(train, x)); bag[x].append(bagged(train, x, 50, rnd))
def var(v):
m = sum(v) / len(v); return sum((a - m) ** 2 for a in v) / len(v)
def bias2(v, x):
return (sum(v) / len(v) - f(x)) ** 2
vs = sum(var(single[x]) for x in xs) / len(xs); vb = sum(var(bag[x]) for x in xs) / len(xs)
bs = sum(bias2(single[x], x) for x in xs) / len(xs); bb = sum(bias2(bag[x], x) for x in xs) / len(xs)
assert vb < 0.7 * vs and bb < 0.02 and bs < 0.02
print(f"[OK] 1-NN 분산 {vs:.3f} -> 배깅 {vb:.3f}, 편향² {bs:.4f} -> {bb:.4f}")
assert math.comb(10, 3) == 120
print("[OK] 속성 10개 중 3개: 같은 조합일 확률 1/120")
print("ALL CHECKS PASSED")
if __name__ == "__main__":
main()