Post

[Python] ML-Random Forest

[Python] ML-Random Forest

Random Forest 개념

Random Forest는 여러 개의 Decision Tree를 독립적으로 학습시키고 그 결과를 다수결(분류) 또는 평균(회귀)으로 합치는 앙상블(Ensemble) 알고리즘

1
2
3
4
5
단일 Decision Tree의 문제:
→ 데이터 변화에 민감하고 과적합하기 쉬움

Random Forest의 해결책:
→ 다양한 트리를 만들고 결과를 합쳐 분산을 줄임

random_forest

두 가지 핵심 전략

1. Bagging (Bootstrap Aggregating)

1
2
3
원본 데이터에서 복원 추출(Bootstrap)로 서브셋 생성
→ 각 트리가 서로 다른 데이터로 학습
→ 트리마다 다른 패턴을 학습하여 다양성 확보

2. Feature Randomness (변수 무작위 선택)

1
2
3
각 노드 분할 시 전체 변수가 아닌 일부만 랜덤하게 사용
→ 트리 간 상관관계 낮춤
→ 더 독립적인 앙상블 구성

언제 사용하는가

사용해야 할 때

상황이유
빠르게 좋은 성능의 베이스라인이 필요할 때기본 파라미터만으로도 준수한 성능, 튜닝 부담 적음
결측값이 있고 전처리를 줄이고 싶을 때결측값에 비교적 강건, 스케일링 불필요
변수 중요도를 파악하고 싶을 때Feature Importance가 안정적이고 신뢰도 높음
과적합이 걱정될 때Bagging으로 분산 감소, Decision Tree보다 훨씬 안정적
데이터 크기가 중간 규모 (수만 ~ 수십만 건)병렬 학습으로 빠른 처리

사용하지 말아야 할 때

상황이유
실시간 예측 or 메모리가 제한적일 때수백 개의 트리를 메모리에 저장 → 무거움
최고 성능이 필요할 때XGBoost, LightGBM이 일반적으로 성능 우위(XGBoost, LightGBM은 오차를 점진적으로 개선해 나감)
선형 관계가 강한 데이터선형 모델(Logistic Regression, Ridge)이 더 적합

실무 활용 사례

1
2
3
4
금융 : 사기 거래 탐지 (불균형 데이터에서도 강건)
의료 : 환자 재입원 예측
제조 : 불량품 탐지, 예측 정비
마케팅 : 고객 이탈 예측 (빠른 배포 필요 시)

Parameters

ParametersExplanationDefault
n_estimators트리 개수100
max_depth각 트리의 최대 깊이None
max_features분할 시 사용할 변수 수 (sqrt 권장)sqrt
min_samples_split노드 분할 최소 샘플 수2
min_samples_leafLeaf 노드 최소 샘플 수1
random_state고정값None
n_jobs사용할 CPU 코어 개수None
  • n_estimators : 여러 개의 Decision Tree를 몇 개 만들지 결정하는 파라미터(보통 100 ~ 300데이터 크면 500 이상도 사용)
    • 값 변화별 효과
      • 클수록 → 성능 안정화 (Variance 감소), 과적합 ↓
      • 작을수록 → 모델 불안정, 성능 변동 큼
    • 단점 → 학습 시간 증가
  • max_depth : 각 트리의 최대 깊이를 제한하는 파라미터
    • Random Forest의 ‘랜덤성’을 결정하는 주요 파라미터
    • 값 변화별 효과
      • 클수록 → 복잡한 패턴 학습, 과적합 ↑
      • 작을수록 → 단순한 모델, 과적합 ↓
  • mxa_features : 각 노드에서 분할(Split)을 결정할 때, 전체 피처(Feature) 중 일부만 무작위로 골라서 그중 최적의 피처를 찾도록 제한하는 설정
    • 값 변화별 효과
      • 클수록 → 트리들이 비슷해짐, 과적합 ↑
      • 작을수록 → 트리 다양성 증가, 일반화 ↑ (너무 작으면 성능 ↓)
    • Options
      • int : 사용할 feature 개수를 직접 지정
      • float : (예: 5개)float (실수): 전체 피처 대비 비율로 지정합니다. (예: 0.3이면 전체의 30%)
      • sqrt or auto : 전체 피처 개수가 $M$일 때, $\sqrt{M}$ 개만 사용합니다. (분류 문제에서 권장)
      • log2 : $\log_2(M)$ 개를 사용
      • None : 모든 피처를 다 고려, 이는 Bagging 방식과 동일해지며 무작위성이 줄어듦.
  • min_samples_split : 노드를 분할하기 위해 필요한 최소 샘플 수
    • 값 변화별 효과
      • 클수록 → 분할 덜함 → 모델 단순 → 과적합 ↓
      • 작을수록 → 계속 분할 → 모델 복잡 → 과적합 ↑
    • Options
      • int : 최소 샘플 개수
      • floate : 전체 데이터 대비 비율
  • min_samples_leaf : leaf node(최종 노드)에 있어야 하는 최소 샘플 수
    • 값 변화별 효과
      • 클수록 → leaf가 커짐 → 부드러운 모델 → 과적합 ↓
      • 작을수록 → leaf가 작아짐 → 복잡한 모델 → 과적합 ↑
    • Options
      • int : 최소 샘플 개수
      • floate : 전체 데이터 대비 비율

실습 — Titanic 생존자 예측

I. Library & Data Load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# warning ignore 
import warnings
warnings.filterwarnings(action = 'ignore')

# Data Preprocessing 
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.preprocessing import StandardScaler, LabelEncoder

# Visualization 
import seaborn as sns
import matplotlib.pyplot as plt
plt.rcParams['font.family'] ='AppleGothic'  # mac 한글 깨짐 현상 해결

# Model Definition
from sklearn.ensemble import RandomForestClassifier

# Evaluation
from sklearn.metrics import roc_curve, accuracy_score, confusion_matrix, roc_auc_score
1
2
3
# Data Load
titanic = pd.read_csv("./Data/Titanic.csv")
titanic
1
2
3
4
5
6
7
8
9
10
11
12
13
	Survived	Pclass	Name	Sex	Age	SibSp	Parch	Ticket	Fare	Cabin	Embarked
0	0	3	Braund, Mr. Owen Harris	male	22.0	1	0	A/5 21171	7.2500	NaN	S
1	1	1	Cumings, Mrs. John Bradley (Florence Briggs Th...	female	38.0	1	0	PC 17599	71.2833	C85	C
2	1	3	Heikkinen, Miss. Laina	female	26.0	0	0	STON/O2. 3101282	7.9250	NaN	S
3	1	1	Futrelle, Mrs. Jacques Heath (Lily May Peel)	female	35.0	1	0	113803	53.1000	C123	S
4	0	3	Allen, Mr. William Henry	male	35.0	0	0	373450	8.0500	NaN	S
...	...	...	...	...	...	...	...	...	...	...	...
886	0	2	Montvila, Rev. Juozas	male	27.0	0	0	211536	13.0000	NaN	S
887	1	1	Graham, Miss. Margaret Edith	female	19.0	0	0	112053	30.0000	B42	S
888	0	3	Johnston, Miss. Catherine Helen "Carrie"	female	NaN	1	2	W./C. 6607	23.4500	NaN	S
889	1	1	Behr, Mr. Karl Howell	male	26.0	0	0	111369	30.0000	C148	C
890	0	3	Dooley, Mr. Patrick	male	32.0	0	0	370376	7.7500	NaN	Q
891 rows × 11 columns

II. Preprocessing

II-I. Feature Engineering

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 가족 변수 추가
titanic['FamSize'] = titanic['SibSp'] + titanic['Parch']  

# 분석에 사용할 변수만 선택
use_cols = ['Survived', 'Pclass', 'Sex', 'Age', 'FamSize', 'Fare', 'Embarked']

# 결측값 제거
titanic = titanic[use_cols].dropna(subset = ['Age'])

# 변수 형태 변경
titanic[['Survived', 'Pclass', 'Sex', 'Embarked']] = titanic[['Survived', 'Pclass', 'Sex', 'Embarked']].astype('category')
titanic['Age'] = titanic['Age'].astype('int')

# One-Hot-Encoding
titanic = pd.get_dummies(titanic, columns = ['Pclass', 'Sex', 'Embarked'], drop_first = True)
titanic
1
2
3
4
5
6
7
8
9
10
11
12
13
	Survived	Age	FamSize	Fare	Pclass_2	Pclass_3	Sex_male	Embarked_Q	Embarked_S
0	0	22	1	7.2500	False	True	True	False	True
1	1	38	1	71.2833	False	False	False	False	False
2	1	26	0	7.9250	False	True	False	False	True
3	1	35	1	53.1000	False	False	False	False	True
4	0	35	0	8.0500	False	True	True	False	True
...	...	...	...	...	...	...	...	...	...
885	0	39	5	29.1250	False	True	False	True	False
886	0	27	0	13.0000	True	False	True	False	True
887	1	19	0	30.0000	False	False	False	False	True
889	1	26	0	30.0000	False	False	True	False	False
890	0	32	0	7.7500	False	True	True	True	False
714 rows × 9 columns

II-II. 수치형 변수 시각화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 수치형 변수 시각화
def numberic_plot(df, target):
    g = sns.PairGrid(df, hue = target)  # 주어진 데이터 컬럼에 대한 모든 조합을 만들어주는 빈 틀을 위한 코드        
    g.map_diag(sns.histplot)            # 삼각행렬의 중간 부분
    g.map_lower(sns.scatterplot)        # 아래 부분
    
    # 상관 계수 행렬을 구하고 상관 계수 값 표시
    corr_matrix = df.corr()
    for i, j in zip(*plt.np.triu_indices_from(g.axes, k = 1)):                                        # np.triu_indices_from : 삼각행렬의 위쪽 삼각형의 인덱스 (k = 0 : 대각 행렬 포함, 1 : 제외)
        g.axes[i, j].annotate(f"corr : {corr_matrix.iloc[i, j]:.2f}",                                 # 상관계수
                              (0.5, 0.5), xycoords = "axes fraction", ha = 'center', va = 'center',   # 중앙 정렬
                              fontsize = 12,                                                          # 글자 크기
                              color = 'black')                                                        # 글자 색  
    g.add_legend()  # 범례 표시
    plt.show()

Columns = ['Age', 'FamSize', 'Fare', 'Survived']  # 수치형 변수
numberic_plot(titanic[Columns], 'Survived')

rf_수치형 변수 시각화

II-III. Train & Test Split

1
2
3
4
5
y = titanic['Survived']
X = titanic.drop(['Survived'], axis = 1)

# 75 : 25 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 0)

III. Model Train

1
2
3
4
5
6
7
8
9
10
11
RF = RandomForestClassifier(
    n_estimators = 200,     # 생성할 트리 개수
    max_depth = 10,         # 각 트리의 최대 깊이
    max_features = 'sqrt',  # 분할 시 사용할 변수 
    min_samples_split = 2,  # 노드 분할 최소 샘플 수 
    min_samples_leaf = 1,   # Leaf 노드의 최소 샘플 수
    random_state = 0,       # random seed
    n_jobs = None           # 사용할 CPU 코어 개수 
)

RF.fit(X_train, y_train)

IV. OOB Score (Out-of-Bag Score)

Bagging에서 각 트리는 복원 추출로 데이터를 선택하기 때문에, 선택되지 않은 약 37%의 데이터가 생깁니다. 이를 OOB(Out-of-Bag) 샘플이라 하며, 별도의 검증셋 없이 성능을 추정할 수 있음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RF_oob = RandomForestClassifier(
    n_estimators = 200,     # 생성할 트리 개수
    max_depth = 10,         # 각 트리의 최대 깊이
    max_features = 'sqrt',  # 분할 시 사용할 변수 
    min_samples_split = 2,  # 노드 분할 최소 샘플 수 
    min_samples_leaf = 1,   # Leaf 노드의 최소 샘플 수
    random_state = 0,       # random seed
    n_jobs = None,          # 사용할 CPU 코어 개수 
    oob_score = True,       # OOB Score 활성화
)
RF_oob.fit(X_train, y_train)

print(f"OOB Score : {RF_oob.oob_score_ * 100 : .2f}%")
# OOB Score는 교차 검증 점수와 유사하게 해석 가능
1
OOB Score : 80.37%

V. Variable Importance Visualization

1
2
3
4
5
6
importances = pd.Series(RF.feature_importances_, index = X.columns)
importances.sort_values().plot(kind = 'barh', figsize = (8, 5))
plt.title('Feature Importance')
plt.xlabel('중요도')
plt.tight_layout()
plt.show()

rf_feature_important

VI. Evaluation Score

1
2
3
4
5
6
7
8
9
pred = RF.predict(X_test)
cfx = confusion_matrix(y_test, pred)                     # Confusion Matrix
sensitivity = cfx[0, 0] / (cfx[0, 0] + cfx[0, 1])  # 민감도 계산
specificity = cfx[1, 1] / (cfx[1, 0] + cfx[1, 1])  # 특이도 계산

print(f"정확도(accuracy) : {accuracy_score(y_test, pred) * 100 :.2f}%")
print(f"Confusion_Matrix :\n{cfx}")
print(f"민감도(sensitivity) : {sensitivity * 100 :.2f}%")
print(f"특이도(specificity) : {specificity * 100 :.2f}%")
1
2
3
4
5
6
정확도(accuracy) : 79.89%
Confusion_Matrix :
[[88 15]
 [21 55]]
민감도(sensitivity) : 85.44%
특이도(specificity) : 72.37%

VII. Roc Curve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fpr, tpr, thresholds = roc_curve(y_test, pred)

J = tpr - fpr
ix = np.argmax(J)             # 가장 큰 원소의 위치(최대값의 인덱스)
best_thresh = thresholds[ix]

#plot roc and best threshold
sens, spec = tpr[ix], 1 - fpr[ix]

# plot the roc curve for the model
plt.plot([0,1], [0,1], linestyle = '--', markersize = 0.01, color = 'black')  # 중간 기준 선
plt.plot(fpr, tpr, marker = '.', color = 'black', markersize = 0.01, label = "Ridge AUC = %.2f" % roc_auc_score(y_test, pred))
plt.scatter(fpr[ix], tpr[ix], marker = '+', s = 100, color = 'r', 
            label = f"Best threshold = {best_thresh:.3f}, \nSensitivity = {sens:.3f}, \nSpecificity = {spec:.3f}")

# Title & Axis Labels
plt.title("ROC Curve")
plt.xlabel("False Positive Rate(1 - Specificity)")
plt.ylabel("True Positive Rate(Sensitivity)")
plt.legend(loc = 4)

# show the plot
plt.show()

rf_roc_curve


Bagging vs Boosting

Random Forest가 속한 Bagging과 다음 포스팅부터 다룰 Boosting을 비교합니다.

 Bagging (Random Forest)Boosting (AdaBoost, XGBoost…)
학습 방식병렬순차
각 트리 관계독립적이전 결과에 의존
목적분산 감소편향 감소
과적합상대적으로 강함노이즈에 민감할 수 있음
속도빠름 (병렬 가능)느림

장단점

장점

  • Decision Tree 대비 과적합에 강함
  • 변수 중요도 제공 → 해석 가능
  • 스케일링 불필요
  • 결측값에 비교적 강건

단점

  • 트리 수가 많아지면 메모리/시간 비용 증가
  • 개별 트리보다 해석이 어려움
  • Boosting 계열보다 성능이 낮을 수 있음
This post is licensed under CC BY 4.0 by the author.