[Tree Based Model] 랜덤포레스트(Random Forests)

EDA

Decision Tree Data 사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Data import
target = 'vacc_h1n1_f'
train = pd.merge(pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train.csv'), 
                 pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/train_labels.csv')[target], left_index=True, right_index=True)
test = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/test.csv')
sample_submission = pd.read_csv('https://ds-lecture-data.s3.ap-northeast-2.amazonaws.com/vacc_flu/submission.csv')

# 훈련/검증 분리
from sklearn.model_selection import train_test_split
train, val = train_test_split(train, train_size=0.80, test_size=0.20, 
                              stratify=train[target], random_state=2)


train.shape, val.shape, test.shape
'''
((33723, 39), (8431, 39), (28104, 38))
'''

# 앞 전 함수 그대로 사용
def engineer(df):
    """특성을 엔지니어링 하는 함수입니다."""
    
#     높은 카디널리티를 가지는 특성을 제거합니다.
#     selected_cols = df.select_dtypes(include=['number', 'object'])
#     colnames = selected_cols.columns.tolist()
#     labels = selected_cols.nunique()
    
#     selected_features = labels[labels <= 30].index.tolist()
#     df = df[selected_features]
    
    # 새로운 특성을 생성합니다.
    behaviorals = [col for col in df.columns if 'behavioral' in col] 
    df['behaviorals'] = df[behaviorals].sum(axis=1)
    
    
    dels = [col for col in df.columns if ('employment' in col or 'seas' in col)]
    df.drop(columns=dels, inplace=True)
        
    return df


train = engineer(train)
val = engineer(val)
test = engineer(test)

# 특성/타겟 분리
features = train.drop(columns=[target]).columns

X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
X_test = test[features]

Random Forests

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
%%time
from category_encoders import OneHotEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.impute import SimpleImputer 
from sklearn.pipeline import make_pipeline

pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True), 
    SimpleImputer(), 
    RandomForestClassifier(n_jobs=-1, random_state=10, oob_score=True)
)
# n_jobs=-1 : 실행되고 있는 컴퓨터 환경에서 가용한 모든 프로세스를 사용한다는 옵션
#  오비 스코어 : 아래에서 다시 정리

pipe.fit(X_train, y_train)
print('검증 정확도: ', pipe.score(X_val, y_val))
'''
검증 정확도:  0.8265923378009726
CPU times: user 10 s, sys: 494 ms, total: 10.5 s
Wall time: 2.96 s
'''

랜덤포레스트는 앙상블(Ensemble) 방법

스크린샷 2021-08-18 10 24 53

  • 앙상블 : 한 종류의 데이터로 여러 머신러닝 학습모델(Week base learner, 작은 여러개의 모델, 기본모델)을 만들어 그 모델들의 예측결과를 다수결이나 평균을 내어 예측하는 방법(이론적으로 기본모델 몇가지 조건을 충족하는 여러 종류의 모델을 사용할 수 있다.)
  • 랜덤포레스트는 결정트리를 기본모델로 사용하는 앙상블 방법이다.
  • 결정트리들은 독립적으로 만들어지며 각각 랜덤으로 예측하는 성능보다 좋을 경우 랜덤포레스트는 결정트리보다 성능이 좋다.

How make 랜포’s 기본모델

스크린샷 2021-08-18 10 32 43

스크린샷 2021-08-18 10 33 15

  • 각각의 부트스트랩이라는 샘플을 통해 학습이 되는데 이렇게 모델링을 하고 합치는 과정을 배깅(Bagging, Bootstrap Aggregating)이라고 한다.

Bootstrap Sampling

스크린샷 2021-08-18 10 36 14

  • 앙상블에 사용하는 작은 모델들은 부트스트래핑(Bootstraping)이라는 샘플링 과정으로 얻은 부트스트랩세트를 사용해 학습한다.
  • 원본 데이터에서 샘플링을 할 때 복원추출을 한다는 것인데, 복원추출은 샘플을 뽑아 값을 기록하고 제자리에 돌려놓는 것을 말한다.
  • 샘플링을 특정한 수만큼 반복하면 하나의 부트스트렙 세트가 완성된다.
  • 복원추출이기 때문에 부트스트랩세트에는 같은 샘플이 반복될 수 있다.
  • Training Set에서 뽑히지 않은 샘플들이 Test Set에 있는데 이 Test Set을 기본모델을 검증할 때 사용하고 이 세트를 OB Set이라 한다.
  • 부트스트랩세트의 크기가 n이라 할 때 한 번의 추출과정에서 어떤 한 샘플이 추출 되지 않을 확률
\[\displaystyle \frac {n-1}{n}\]
  • n회 복원추출을 진행했을 때 그 샘플이 추출되지 않았을 확률
\[\displaystyle \left({\frac {n-1}{n}}\right)^{n}\]
  • n을 무한히 크게 했을 때 이 식
\[\displaystyle \lim _\left({1 - \frac {1}{n}}\right)^{n} = e^{-1} = 0.368\]
  • 참고
\[\displaystyle e = \lim _\left(1+{\frac {1}{n}}\right)^{n}\]
  • 즉 데이터가 충분히 크다고 가정했을 때 한 부트스트랩세트는 표본의 63.2% 에 해당하는 샘플을 가진다.
  • 여기서 추출되지 않은 36.8%의 샘플이 Out Of Bag 샘플이며 이것을 사용해 모델을 검증할 수 있다.
1
2
3
4
pipe.named_steps['randomforestclassifier'].oob_score_
'''
0.8188180173768644
'''

Aggregation

  • Aggregation : 부트스트랩세트로 만들어진 기본모델(Weak learner, 작은 모델들)들을 합치는 과정
    • 회귀문제 : 기본모델 결과들의 평균
    • 분류문제 : 다수결로 가장 많은 모델들이 선택한 범주

Random Select

  • 랜덤포레스트는 기본모델들의 트리를 만들 때 무작위로 선택한 특성세트를 사용한다.
  • 기본모델 트리를 만들 때 일반 결정트리 알고리즘과 다른점
    • 회귀문제일 경우 기본모델 결과들의 평균으로 결과를 낸다.
    • 분류문제일 경우 다수결로 가장 많은 모델들이 선택한 범주로 예측한다.

순서형(Ordinal) 인코딩

  • 순서형 인코딩은 범주에 숫자를 mapping한다.
    • [a, b, c] 세 범주가 있다면 이것을 [1, 2, 3] 이렇게 숫자로 인코딩한다.
  • 트리구조 학습에서 원핫인코딩을 사용하면 문제가 있다.
    • 트리구조에서는 중요한 특성이 상위 노드에서 먼저 분할이 일어난다.
    • 범주 종류가 많은(high cardinality) 특성은 원핫인코딩으로 인해 상위노드에서 선택될 기회가 적어진다.
    • 원핫인코딩 영향을 안 받는 수치형 특성이 상위노드를 차지할 기회가 높아지고 전체적인 성능 저하가 생길 수 있다.
    • 트리모델에서는 순서정보가 상관없어서 순서형을 사용해도 괜찮다.
    • 만약 Feature가 30개라면 가치가 30개로 분산되어서 설명력이 적어진다.

Mapping 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from category_encoders import OrdinalEncoder

enc = OrdinalEncoder(handle_missing="value")
X = [['Male', 1, 'Yes'], ['Female', 3, 'No'], ['Female', 2, 'None']]
enc.fit(X)
'''
OrdinalEncoder(cols=[0, 2],
               mapping=[{'col': 0, 'data_type': dtype('O'),
                         'mapping': Male      1
Female    2
NaN      -2
dtype: int64},
                        {'col': 2, 'data_type': dtype('O'),
                         'mapping': Yes     1
No      2
None    3
NaN    -2
dtype: int64}])
'''

# transform 사용해서 데이터 주입
enc.transform([['Male',1,'No'],['Female', 10]])

스크린샷 2021-08-18 14 22 40

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enc.category_mapping
'''
[{'col': 0,
  'mapping': Male      1
  Female    2
  NaN      -2
  dtype: int64,
  'data_type': dtype('O')},
 {'col': 2,
  'mapping': Yes     1
  No      2
  None    3
  NaN    -2
  dtype: int64,
  'data_type': dtype('O')}]
'''

H1N1 Data에서 확인

  • 실행속도 변화 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
%%time

# ordinal encoding
pipe_ord = make_pipeline(
    OrdinalEncoder(), 
    SimpleImputer(), 
    RandomForestClassifier(random_state=10, n_jobs=-1, oob_score=True)
)

pipe_ord.fit(X_train, y_train)
print('검증 정확도', pipe_ord.score(X_val, y_val))
'''
검증 정확도 0.8254062388803226
CPU times: user 5.23 s, sys: 242 ms, total: 5.47 s
Wall time: 1.38 s
'''

# 특성의 수 비교(Onhot vs Ordinal)
print('Shape  before: ', X_train.shape)

# OneHotEncoder
enc = pipe.named_steps['onehotencoder']
encoded = enc.transform(X_train)
print('OneHot  shape: ', encoded.shape)

# OrdinalEncoder
enc = pipe_ord.named_steps['ordinalencoder']
encoded = enc.transform(X_train)
print('Ordinal shape: ', encoded.shape)
'''
Shape  before:  (33723, 32)
OneHot  shape:  (33723, 108)
Ordinal shape:  (33723, 32)
'''

Random Forests 특성 중요도 비교

  • 랜덤포레스트에서는 학습 후의 특성들의 중요도 정보(Gini importance)를 기본으로 제공한다.
  • 중요도는 노드들의 지니불순도(Gini impurity)를 가지고 계산한다.
  • 노드가 중요할수록 불순도가 크게 감소한다.
  • 노드는 한 특성의 값을 기준으로 분리되기 때문에 불순도를 크게 감소하는데 많이 사용된 특성이 중요도가 올라갈 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import matplotlib.pyplot as plt

# 특성 중요도(onehot)
rf = pipe.named_steps['randomforestclassifier']
colnames = pipe.named_steps['onehotencoder'].get_feature_names()
importances = pd.Series(rf.feature_importances_, colnames)

n = 10
plt.figure(figsize=(10,n/4))
plt.title(f'Top {n} features with onehotencoder')
importances.sort_values()[-n:].plot.barh();


# 특성 중요도(ordinal)
rf_ord = pipe_ord.named_steps['randomforestclassifier']
importances_ord = pd.Series(rf_ord.feature_importances_, X_train.columns)

plt.figure(figsize=(10,n/4))
plt.title(f'Top {n} features with ordinalencoder')
importances_ord.sort_values()[-n:].plot.barh();

스크린샷 2021-08-18 14 36 22

  • 이 두가지 인코딩 방법이 선형 모델에 어떤 영향을 줄까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import StandardScaler

pipe_lr = make_pipeline(
    OneHotEncoder(), 
    SimpleImputer(), 
    LogisticRegression(n_jobs=-1)
)
pipe_lr.fit(X_train, y_train)

print('검증세트 정확도', pipe_lr.score(X_val, y_val))
'''
검증세트 정확도 0.8204246234135927
'''

pipe_lr = make_pipeline(
    OrdinalEncoder(), 
    SimpleImputer(),  
    LogisticRegression(n_jobs=-1)
)
pipe_lr.fit(X_train, y_train)

print('검증세트 정확도', pipe_lr.score(X_val, y_val))
'''
검증세트 정확도 0.797532914245048
'''
  • 순서형인코딩에서 주의해야 할 점은 범주들을 순서가 있는 숫자형으로 바꾸면 원래 그 범주에 없던 순서정보가 생긴다.
  • 순서형인코딩은 범주들 간의 분명한 순위가 있을 때 그 연관성에 맞게 숫자를 정해주는 것이 좋다.
  • 오디널인코더로 무작위로 수치를 인코딩했지만, 정확한 범주의 순위를 알고 있다면 mapping 파라미터를 이용해 지정해줄 수 있다.

RF모델이 DT모델모다 상대적으로 과적합을 피할 수 있는 이유

  • 랜덤포레스트의 랜덤성은 두가지에서 나온다.
    • 랜덤포레스트에서 학습되는 트리들은 배깅을 통해 만들어진다.(bootstrap = true)이때 각 기본트리에 사용되는 데이터가 랜덤으로 선택된다.
    • 각각 트리는 무작위로 선택된 특성들을 가지고 분기를 수행한다.(max_features = auto)
  • 결정트리는 데이터 일부에 과적합하는 경향이 있다.
    그래서 다르게 샘플링된 데이터로 과적합된 트리를 많이 만들고 그 결과를 평균 내 사용하는 모델이 랜덤포레스트이다.
  • 이렇게 하면 과적합이 줄고 성능이 유지된다.

비선형 관계의 데이터를 적합하는 모습

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
columns = ['mobility', 'density']
data = [[80.574, -3.067]
,[84.248, -2.981]
,[87.264, -2.921]
,[87.195, -2.912]
,[89.076, -2.84]
,[89.608, -2.797]
,[89.868, -2.702]
,[90.101, -2.699]
,[92.405, -2.633]
,[95.854, -2.481]
,[100.696, -2.363]
,[101.06, -2.322]
,[401.672, -1.501]
,[390.724, -1.46]
,[567.534, -1.274]
,[635.316, -1.212]
,[733.054, -1.1]
,[759.087, -1.046]
,[894.206, -0.915]
,[990.785, -0.714]
,[1090.109, -0.566]
,[1080.914, -0.545]
,[1122.643, -0.4]
,[1178.351, -0.309]
,[1260.531, -0.109]
,[1273.514, -0.103]
,[1288.339, 0.01]
,[1327.543, 0.119]
,[1353.863, 0.377]
,[1414.509, 0.79]
,[1425.208, 0.963]
,[1421.384, 1.006]
,[1442.962, 1.115]
,[1464.35, 1.572]
,[1468.705, 1.841]
,[1447.894, 2.047]
,[1457.628, 2.2]]

thurber = pd.DataFrame(columns=columns, data=data)
X_thurber = thurber[['mobility']]
y_thurber = thurber['density']

%matplotlib inline
import matplotlib.pyplot as plt
from ipywidgets import interact
from sklearn.ensemble import RandomForestRegressor
from sklearn.tree import DecisionTreeRegressor

def trees(max_depth=1, n_estimators=1):
    models = [DecisionTreeRegressor(max_depth=max_depth), 
              RandomForestRegressor(max_depth=max_depth, n_estimators=n_estimators)]
    
    for model in models:
        name = model.__class__.__name__
        model.fit(X_thurber, y_thurber)
        ax = thurber.plot('mobility', 'density', kind='scatter', title=name)
        ax.step(X_thurber, model.predict(X_thurber), where='mid')
        plt.show()
        
interact(trees, max_depth=(1,10,1), n_estimators=(10,50,10));

스크린샷 2021-08-18 14 44 21

랜덤포레스트 알고리즘

  • 랜덤포레스트의 의사코드(Pseudo code)

스크린샷 2021-08-18 14 45 22

0%