본문 바로가기
대학공부/기계학습

실습 6차시: kNN, 랜덤포레스트, 앙상블, feature selection

by 진진리 2023. 10. 22.
728x90

1. K-Nearest Neighbor Classifier (KNN)

1-1. 데이터 포인트 간의 거리 - 2D

이번 실습에서는 KNN 모델로 영화 평가 분류기를 구현해볼 것입니다. 먼저 데이터 포인트들 간의 거리 개념부터 알아봅니다.

두 점이 서로 가깝거나 멀리 떨어져 있는 정도를 측정하기 위해 거리 공식을 사용할 것입니다.

이 예제의 경우 데이터의 차원은 다음과 같습니다.

  • 영화의 러닝타임
  • 영화 개봉 연도

스타워즈와 인디아나 존스를 예로 들겠습니다. 스타워즈는 125분이며 1977년에 개봉했습니다. 인디아나 존스는 115분이며 1981년에 개봉했습니다.

이 두 영화를 의미하는 두 데이터 포인트들의 거리는 아래와 같이 계산됩니다.

Practice 1

1. movie1과 movie2라는 두 리스트을 매개 변수로 사용하는 distance라는 함수를 선언합니다.

  • 각 리스트의 첫 번째 인덱스는 영화의 러닝타임이고 두 번째 인덱스는 영화의 개봉 연도입니다. distance 함수는 두 리스트 사이의 거리를 반환해야 합니다.
# distance 함수 구현하기
def distance(movie1, movie2):
    length_difference = (movie1[0] - movie2[0]) ** 2
    year_difference = (movie1[1] - movie2[1]) ** 2
    distance = (length_difference + year_difference) ** 0.5
    return distance

2. 아래 영화에 대해 distance 함수를 호출하여 거리를 비교해봅니다.

star_wars = [125, 1977]
raiders = [115, 1981]
mean_girls = [97, 2004]
# star_wars와 raiders의 거리
print(distance(star_wars, raiders))

# star_wars와 mean_girls의 거리
print(distance(star_wars, mean_girls))

# 스타워즈는 어느 영화와 더 비슷합니까?
# 정답: raiders

10.770329614269007

38.897300677553446

 

1-2. 데이터 포인트 간의 거리 - 3D

영화의 길이와 개봉일만을 기준으로 영화 평가 분류기를 만드는 것은 상당히 제한적입니다. 영화의 다른 속성을 데이터에 포함하여 세번째 차원을 추가해 보겠습니다.

추가된 세 번째 차원은 영화 예산입니다. 이제 영화를 나타내는 두 데이터 포인트들의 거리를 3차원으로 찾아야 합니다.

데이터의 차원이 삼차원보다 많아지면 시각화 하는 것은 어렵지만 그래도 거리를 구할 수 있습니다.

N차원의 데이터 포인트 A와 B 사이의 거리 공식은 다음과 같습니다.

여기서 A1−B1은 각 데이터 포인트의 첫 번째 feature 간의 차입니다. An−Bn은(는) 각 점의 마지막 feature 간의 차입니다.

이 공식을 이용하면 N차원 공간에서 한 데이터 포인트의 K-Nearest Neighbors를 찾을 수 있습니다.

이 거리를 사용하여 label되지 않은 데이터 포인트의 가장 가까운 이웃을 찾아 결과적으로 분류를 하게 됩니다.

Practice 2

1. distance 함수를 N개의 차원의 데이터의 거리를 반환하는 함수로 수정합니다.

# distance 함수 수정
def distance(movie1, movie2):
    squared_difference = 0
    for i in range(len(movie1)):
        squared_difference += (movie1[i] - movie2[i]) ** 2
    final_distance = squared_difference ** 0.5
    return final_distance

2. 위 활동에서 주어진 영화들에 영화 예산 속성이 추가되었습니다. 아래 영화에 대해 distance 함수를 호출하여 거리를 비교해봅니다.

star_wars = [125, 1977, 11000000]
raiders = [115, 1981, 18000000]
mean_girls = [97, 2004, 17000000]
# star_wars와 raiders의 거리
print(distance(star_wars, raiders))

# star_wars와 mean_girls의 거리
print(distance(star_wars, mean_girls))

# 스타워즈는 어느 영화와 더 비슷합니까?
# 정답: mean_girls

7000000.000008286

6000000.000126083

 

1-3. 척도가 다른 데이터: 정규화

이 활동에서는 K-Nearest Neighborhood 알고리즘의 첫 번째 단계를 구현합니다.

  1. 데이터를 정규화합니다.
  2. 가장 가까운 이웃인 k를 찾습니다.
  3. 이러한 이웃을 기준으로 새로운 포인트를 분류합니다.

우리가 영화 예산 속성을 추가하면 데이터의 범위가 변화한것을 확인할 수 있습니다.

영화 개봉 연도와 영화 예산을 살펴보겠습니다. 어떠한 두 영화의 개봉 날짜의 최대 차이는 약 125년입니다((루미에르 브라더스는 1890년대에 영화를 만들고 있었습니다). 하지만, 어떠한 두 영화의 예산 차이는 수백만 달러가 될 수 있습니다.

거리 공식의 문제는 규모에 상관없이 모든 차원을 동등하게 취급한다는 것입니다. 이는 1년 차이가 나는 영화 개봉 년도를 1달러의 차이의 영화 예산과 같은 정도로 취급한다는 뜻입니다.

이 문제에 대한 해결책은 정규화입니다. 이는 모든 값은 0과 1 사이로 변환합니다.

이번 실습은 최소-최대 정규화(Min-Max Normalization)를 사용할 것입니다.

참고: scikit-learn https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.MinMaxScaler.html#sklearn.preprocessing.MinMaxScaler

 

sklearn.preprocessing.MinMaxScaler

Examples using sklearn.preprocessing.MinMaxScaler: Release Highlights for scikit-learn 0.24 Image denoising using kernel PCA Time-related feature engineering Univariate Feature Selection Scalable l...

scikit-learn.org

 

1-3-1. Normalization vs Standardization

Practice 3

# min_max_normalize 함수 구현
def min_max_normalize(lst):
    # 1.
    minimum = min(lst)
    maximum = max(lst)
    # 2.
    normalized = []
 
    for value in lst:
        normalized_num = (value - minimum) / (maximum - minimum)
        normalized.append(normalized_num)
 
    return normalized
release_dates = [1897, 1998, 2000, 1948, 1962, 1950, 1975, 1960,
                 2017, 1937, 1968, 1996, 1944, 1891, 1995, 1948,
                 2011, 1965, 1891, 1978]
from pprint import pprint

# 결과값 출력하기
pprint(min_max_normalize(release_dates))

[0.047619047619047616, 0.8492063492063492, 0.8650793650793651, 0.4523809523809524,

0.5634920634920635, 0.46825396825396826, 0.6666666666666666, 0.5476190476190477,

1.0, 0.36507936507936506, 0.6111111111111112, 0.8333333333333334,

0.42063492063492064, 0.0, 0.8253968253968254, 0.4523809523809524,

0.9523809523809523, 0.5873015873015873, 0.0, 0.6904761904761905]

 

1-4. Nearest Neighbors 찾기

K-Nearest Neighbors 알고리즘의 두번째 단계를 구현해보겠습니다.

  1. 데이터를 정규화합니다.
  2. 가장 가까운 이웃 k를 찾습니다.
  3. 이러한 이웃을 기준으로 새로운 포인트를 분류합니다.

Label되지 않은 데이터 포인터를 분류하기 위해서는 이 데이터 포인트와 가장 가까운 k개의 이웃을 찾아야 합니다. 가장 적합한 k를 찾는 방법도 있지만, 일단은 k=5로 정하도록 하겠습니다.

가장 가까운 5개의 이웃을 찾기 위해서는 label되지 않은 데이터 포인트를 데이터의 다른 모든 데이터 포인트와의 거리를 계산하여 비교합니다. 이번 실습에서 구현할 함수의 반환값은 label되지 않은 한 영화에 대해 데이터의 모든 영화와의 거리가 정렬된 리스트입니다.

반환값의 예시는 다음과 같습니다.

[
  [0.30, 'Superman II'],
  [0.31, 'Finding Nemo'],
  ...
  ...
  [0.38, 'Blazing Saddles']
]

이 예에서 label되지 않은 영화와 슈퍼맨2 영화와의 거리는 0.30입니다.

# 영화 데이터 받아오기
import urllib.request
import ast

movie_datasets = []
for line in urllib.request.urlopen(movie_dataset_url):
    movie_datasets.append(line.decode('utf-8'))

movie_dataset = ast.literal_eval(movie_datasets[0][16:])
movie_labels = ast.literal_eval(movie_datasets[1][15:])

 

Practice 4

1. 위에 주어진 영화 데이터 중 The Avengers 영화를 출력해봅니다. 각 영화의 세 가지 feature은 다음과 같습니다.

  • 정규화된 영화 예산(달러)
  • 정규화된 러닝타임(분)
  • 정규화된 개봉 연도

위 데이터의 label은 좋은 영화와 나쁜 영화를 나타냅니다. The Avengers 영화의 label은 1로, 좋은 영화입니다. 분류 기준은 IMDb에서 7.0 이상의 평가를 받으면 좋은 영화로 분류됩니다.

print(movie_dataset['The Avengers'])
print(movie_labels['The Avengers'])

[0.018009887923225047, 0.4641638225255973, 0.9550561797752809]

1

 

2. classify 함수를 선언하고 구현해봅니다.

# classify 함수를 구현
def classify(unknown, dataset, k):
    distances = []
    #Looping through all points in the dataset
    for title in dataset:
        movie = dataset[title]
        distance_to_point = distance(movie, unknown)
        #Adding the distance and point associated with that distance
        distances.append([distance_to_point, title])
    distances.sort()
    #Taking only the k closest points
    neighbors = distances[0:k]
   
    return neighbors

 

3. 아래 주어진 매개변수로 classify 함수를 테스트하고 결과를 출력합니다.

  • [.4, .2, .9]
  • movie_dataset
  • 5
# `classify` 함수를 테스트하고 결과를 출력합니다.
print(classify([.4, .2, .9], movie_dataset, 5))

[[0.08273614694606074, 'Lady Vengeance'], [0.22989623153818367, 'Steamboy'], [0.23641372358159884, 'Fateless'], [0.26735445689589943, 'Princess Mononoke'], [0.3311022951533416, 'Godzilla 2000']]

 

1-5. Neighbors 세기

이 활동에서는 K-Nearest Neighbor 알고리즘 중 마지막 단계를 구현합니다.

  1. 데이터를 정규화합니다.
  2. 가장 가까운 이웃인 k를 찾습니다.
  3. 이웃을 기준으로 새로운 포인트를 분류합니다.

이전 활동에서 어떠한 label되지 않은 데이터 포인트에 대해 가장 가까운 이웃 k개를 찾아 다음과 같은 형태로 리스트에 저장했습니다.

[
  [0.083, 'Lady Vengeance'],
  [0.236, 'Steamboy'],
  ...
  ...
  [0.331, 'Godzilla 2000']
]

이 리스트의 좋고 나쁜 영화의 갯수를 셈으로써 분류를 할 수 있습니다. 만약 좋은 영화로 분류된 이웃들의 갯수가 더 많아면, label되지 않은 영화는 좋은 영화로 분류됩니다. 반대의 경우에는 나쁜 영화로 분류됩니다.

만약 좋은 영화로 분류된 이웃들의 갯수와 나쁜 영화로 분류된 이웃들의 갯수가 같다면 가장 거리가 가까운 영화의 label을 선택하게 됩니다.

 

Practice 5

  1. 이 활동에서는 위의 classify 함수를 수정하여 unknown 데이터 포인트에 대한 분류값을 반환하겠습니다. labels 매개 변수를 classify함수에 추가합니다.
  2. num_good와 num_bad라는 두 변수를 만들고 각각 0으로 초기화합니다.
  3. labels과 title을 사용하여 각 영화의 label을 받아옵니다.
    • 해당 라벨이 0이면 num_bad에 1을 증가시킵니다.
    • 해당 라벨이 1이면 num_good에 1을 증가시킵니다.
  4. 이제 label되지 않은 영화를 분류할 수 있습니다. 
    • num_good가 num_bad보다 크면 1을 반환합니다.
    • 그렇지 않으면 0을 반환합니다.
# classify 함수 수정하기
# 1.
def classify(unknown, dataset, labels, k):
    distances = []
    for title in dataset:
        movie = dataset[title]
        distance_to_point = distance(movie, unknown)
        #Adding the distance and point associated with that distance
        distances.append([distance_to_point, title])
    distances.sort()
    #Taking only the k closest points
    neighbors = distances[0:k]
    # 2.
    num_good = 0
    num_bad = 0
    # 3.
    for neighbor in neighbors:
        title = neighbor[1]
        if labels[title] == 0:
            num_bad += 1
        elif labels[title] == 1:
            num_good += 1
    # 4.        
    if num_good > num_bad:
        return 1
    else:
        return 0

5. 다음 파라미터를 사용하여 classify 함수를 호출하여 결과를 출력해봅니다.

  • 분류하려는 영화는 [.4, .2, .9] 입니다.
  • 훈련 데이터는 movie_dataset 입니다.
  • 훈련 데이터의 label은 movie_labels입니다.
  • k는 5로 지정합니다.
# `classify` 함수를 테스트하고 결과를 출력합니다.
print(classify([.4, .2, .9], movie_dataset, movie_labels, 5))

1

 

1-6. 하나의 데이터 포인트 분류하기

위의 활동까지는 임의의 데이터인 [.4, .2, .9]에만 테스트 해보았습니다. 이번 활동에서는 실제 영화 Call Me By Your Name 데이터를 정규화하고 예측해 볼 것입니다.

 

1. 먼저 분류하고자 하는 영화가 훈련 데이터에 있지는 않은지 확인해야 합니다.

# "Call Me By Your Name"이 훈련 데이터에 있는지 확인하기
print("Call Me By Your Name" in movie_dataset)

False

 

2. 분류하고자 하는 영화가 훈련 데이터에 없는 것으로 확인 되면 my_movie 변수를 생성하고 영화 예산, 러닝타임, 개봉 연도의 순으로 데이터를 구성합니다.

Call Me By Your Name의 예산은 35만 달러, 런타임은 132분, 개봉 연도는 2017년입니다.

# my_movie 변수 생성하기
my_movie = [3500000, 132, 2017]

 

3. 주어진 normalize_dimension 함수를 사용하여 my_movie를 정규화합니다. normalized_my_movie라는 변수를 만들고 my_movie의 정규화된 값을 저장합니다. 결과를 출력해봅니다.

# x, y, z points array 받아오기
import urllib.request
import ast
import numpy as np

points = []
for line in urllib.request.urlopen(points_url):
    points.append(line.decode('utf-8'))

training_x = np.array(ast.literal_eval(points[0]))
training_y = np.array(ast.literal_eval(points[1]))
training_z = np.array(ast.literal_eval(points[2]))
def normalize_dimension(value, lst):
    minimum = min(lst)
    maximum = max(lst)
    return (value - minimum) / (maximum - minimum)

def normalize_point(pt):
    global training_x
    global training_y
    global training_z
    newx = normalize_dimension(pt[0], training_x)
    newy = normalize_dimension(pt[1], training_y)
    newz = normalize_dimension(pt[2], training_z)
    return [newx, newy, newz]
# my_movie 변수를 정규화합니다.
normalized_my_movie = normalize_point(my_movie)
print(normalized_my_movie)

[0.00028650338197026213, 0.3242320819112628, 1.0112359550561798]

 

4. normalized_my_movie, movie_dataset, movie_labels, k=5 매개 변수를 사용하여 "classify"를 호출하고 결과를 출력합니다.

# classify 함수를 호출합니다.
classify(normalized_my_movie, movie_dataset, movie_labels, 5)

# TODO: Call Me By Your Name 영화의 분류된 lable은 무엇인가요?
# 정답: good movie

1

 

1-7. scikit-learn으로 KNN 구현하기

사용 데이터: https://archive.ics.uci.edu/dataset/17/breast+cancer+wisconsin+diagnostic

사용 모델: https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.KNeighborsClassifier.html

import pandas as pd


df = pd.read_csv(data_url)
df.head()
# id 와 Unnamed: 32 column을 삭제한 후 column들의 리스트를 출력합니다.
df.drop(columns=['id', 'Unnamed: 32'], inplace=True)
df.columns

Index(['diagnosis', 'radius_mean', 'texture_mean', 'perimeter_mean', 'area_mean', 'smoothness_mean', 'compactness_mean', 'concavity_mean', 'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean', 'radius_se', 'texture_se', 'perimeter_se', 'area_se', 'smoothness_se', 'compactness_se', 'concavity_se', 'concave points_se', 'symmetry_se', 'fractal_dimension_se', 'radius_worst', 'texture_worst', 'perimeter_worst', 'area_worst', 'smoothness_worst', 'compactness_worst', 'concavity_worst', 'concave points_worst', 'symmetry_worst', 'fractal_dimension_worst'], dtype='object')

# 각 column에 na 값이 있지는 않은지 확인합니다.
df.isna().any()

diagnosis False

radius_mean False

texture_mean False

perimeter_mean False

area_mean False

smoothness_mean False

compactness_mean False

concavity_mean False

concave points_mean False

symmetry_mean False

fractal_dimension_mean False

radius_se False

texture_se False

perimeter_se False

area_se False

smoothness_se False

compactness_se False

concavity_se False

concave points_se False

...

# label이 어떻게 구성되어있나 봅니다.
df['diagnosis'].value_counts()

B 357

M 212

Name: diagnosis, dtype: int64

# 데이터를 X와 y로 나누고 train, test 셋으로 나눕니다.
from sklearn.model_selection import train_test_split

y = df['diagnosis']
X = df.drop(columns=['diagnosis'])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, shuffle=True)
# scikit-learn의 KNN classifier을 생성하고 훈련시킵니다.
from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier()
knn.fit(X_train, y_train)
# test 셋에 대하여 정확도를 출력해봅니다.
knn.score(X_test, y_test)

0.9590643274853801

 

1-8. 가장 적합한 k 찾기

n_neighbors_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
score = []

# 가장 적합한 k를 찾아봅니다.
for k in n_neighbors_list:
  knn = KNeighborsClassifier(n_neighbors=k)
  knn.fit(X_train, y_train)
  score.append(knn.score(X_test, y_test))
n_neighbors_list[np.argmax(score)]

9

import matplotlib.pyplot as plt

plt.plot(n_neighbors_list, score)

 

2. Random Forest

앙상블(ensemble)은 기존에 존재하는 기계학습 알고리즘으로 구축된 여러 모델의 예측을 결합하여 단일 모델에 비해 generalizability / robustness을 향상시키는 방법입니다.

앙상블 방법은 보통 두 가지 계열로 구분됩니다.

  • 평균화(averaging) 방법: 여러 개의 모델을 독립적으로 생성 후 예측을 평균화합니다. 결합 추정기는 분산(variance)이 감소하기 때문에 일반적으로 단일 기본 모델보다 낫습니다.
  • 부스팅(boosting) 방법: 기본 모델이 순차적으로 구축되고 결합된 모델의 편향(bias)을 줄이려고 합니다. 강력한 앙상블을 만들기 위해 몇 가지 약한 모델을 결합하는 방법입니다.

가장 대표적인 앙상블 모델은 랜덤 포레스트(random forest)이며, 이 모델은 기본 구성 요소로 결정 트리를 사용합니다.

결정 트리의 단점은 훈련 데이터에 과적합되는 경향이 있다는 것입니다. 랜덤 포레스트는 조금씩 다른 여러 결정 트리를 묶어 과적합 문제를 피할 수 있습니다. 랜덤 포레스트의 원리는, 각 트리는 비교적 예측을 잘 할 수 있지만 데이터의 일부에 과적합하는 경향을 가진다는 데 기초합니다. 잘 작동하되 서로 다른 방향으로 과적합된 트리를 많이 만들면 그 결과를 평균냄으로써 과적합된 양을 줄일 수 있습니다. 이렇게 하면 트리 모델의 예측 성능이 유지되면서 과적합이 줄어드는 것이 수학적으로 증명되었습니다.

2-1. Random Forest: Bagging (=Bootstrap Aggregation)

Bootstrap sample은 더 큰 샘플에서 "bootstrap"된 더 작은 샘플입니다. Bootstraping은 동일한 크기의 작은 샘플을 하나의 원래 샘플에서 대량으로 반복적으로 추출하는 리샘플링의 한 유형입니다.

통계에 대한 표본 분포를 생성하기 위해 모집단(population)에서 반복되지 않는 큰 표본을 그리는 것이 이상적입니다. 그렇지 않은 작은 데이터에서 bootstrap sample은 모집단 모수(population parameter)에 대해 상당히 좋은 근사치가 될 수 있습니다.

Decision tree은 훈련 데이터에 매우 민감합니다. 훈련 데이터를 약간 변경하면 트리 구조가 크게 달라질 수 있습니다. 랜덤 포레스트는 각 개별 트리가 데이터 세트에서 무작위로 대체하여 샘플을 추출할 수 있도록 하여 서로 다른 트리를 생성함으로써 이를 활용합니다. 이 과정을 bagging이라고 합니다.

 

2-2. Random Forest: Feature Randomness

일반적인 의사 결정 트리에서 노드를 분할할 때, 모든 feature을 고려하여 왼쪽 노드와 오른쪽 노드 사이에 불순도가 가장 낮도록 분할합니다. 반면에 랜덤 포리스트의 각 트리는 feature의 random subset에서만 선택할 수 있습니다. 이로 인해 모델의 트리 간에 훨씬 더 많은 변동이 발생하고 궁극적으로 트리 간 상관 관계가 낮아지고 더 다양화됩니다.

결론적으로, 이러한 과정을 통해 구성된 랜덤 포레스트는 bagging을 통해 각기 다른 데이터셋에 훈련됨과 동시에 각기 다른 feature을 사용한 여러개의 결정 트리로 이루어지게 됩니다.

 

 

2-3. Scikit-learn Random Forest

from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split

# two_moon 데이터셋을 가지고 실습합니다.
X, y = make_moons(n_samples=200, noise=0.25, random_state=42)
Xm_train, Xm_test, ym_train, ym_test = train_test_split(X, y, stratify=y, random_state=42)

# two_moon 데이터를 시각화해봅니다.
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.show()

# 랜덤 포레스트의 결정 경계를 시각화하는 함수입니다.
from matplotlib.colors import ListedColormap

def plot_decision_boundary(forest):
  figure = plt.figure(figsize=(36, 5))
  x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
  y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
  xx, yy = np.meshgrid(np.arange(x_min, x_max, .02), np.arange(y_min, y_max, .02))
  cm = plt.cm.RdBu
  cm_bright = ListedColormap(['#FF0000', '#0000FF'])

  for i in range(len(forest.estimators_)):
    ax = plt.subplot(1, 6, i+1)
    Z = forest.estimators_[i].predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
    Z = Z.reshape(xx.shape)
    ax.contourf(xx, yy, Z, cmap=cm, alpha=.8)
    ax.scatter(Xm_train[:, 0], Xm_train[:, 1], c=ym_train, cmap=cm_bright, edgecolors='k')
    ax.set_xticks(())
    ax.set_yticks(())

  ax = plt.subplot(1, 6, 6)
  Z = forest.predict_proba(np.c_[xx.ravel(), yy.ravel()])[:, 1]
  Z = Z.reshape(xx.shape)
  ax.contourf(xx, yy, Z, cmap=cm, alpha=.8)
  ax.scatter(Xm_train[:, 0], Xm_train[:, 1], c=ym_train, cmap=cm_bright, edgecolors='k')
  ax.set_xticks(())
  ax.set_yticks(())

 

scikit-learn RandomForestClassifier: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html

 

sklearn.ensemble.RandomForestClassifier

Examples using sklearn.ensemble.RandomForestClassifier: Release Highlights for scikit-learn 0.24 Release Highlights for scikit-learn 0.22 Comparison of Calibration of Classifiers Probability Calibr...

scikit-learn.org

from sklearn.ensemble import RandomForestClassifier

# 트리 5개로 구성된 랜덤 포레스트 모델을 만들어보겠습니다.
forest_m = RandomForestClassifier(n_estimators=5, random_state=2)
forest_m.fit(Xm_train, ym_train)
# 주어진 함수를 사용하여 다섯개의 트리와 랜덤 포레스트의 결정 경계를 시각화해봅니다.
plot_decision_boundary(forest_m)

다섯 개의 트리가 만든 결정 경계는 확연하게 다르다는 것을 알 수 있습니다. Bootstrap sampling 때문에 한쪽 트리에 나타나는 훈련 포인트가 다른 트리에는 포함되지 않을 수 있어 각 트리는 불완전합니다.

랜덤 포레스트는 개개의 트리보다는 덜 과적합되고 훨씬 좋은 결정 경계를 만들어줍니다. 실제 애플리케이션에서는 매우 많은 트리를 사용하기 때문에(수백, 수천 개) 더 부드러운 결정 경계가 만들어집니다.

scikit-learn에서 제공하는 유방암 데이터셋에 100개의 트리로 이뤄진 랜덤 포레스트를 적용해보겠습니다.

from sklearn.datasets import load_breast_cancer

# 유방암 데이터를 로드하고 train/test셋으로 나눕니다.
cancer = load_breast_cancer()
Xc_train, Xc_test, yc_train, yc_test = train_test_split(cancer.data, cancer.target, random_state=42)
# 데이터에 어떤 특성이 있는지 살펴봅니다.
cancer.feature_names

array(['mean radius', 'mean texture', 'mean perimeter', 'mean area', 'mean smoothness', 'mean compactness', 'mean concavity', 'mean concave points', 'mean symmetry', 'mean fractal dimension', 'radius error', 'texture error', 'perimeter error', 'area error', 'smoothness error', 'compactness error', 'concavity error', 'concave points error', 'symmetry error', 'fractal dimension error', 'worst radius', 'worst texture', 'worst perimeter', 'worst area', 'worst smoothness', 'worst compactness', 'worst concavity', 'worst concave points', 'worst symmetry', 'worst fractal dimension'], dtype='<U23')

# 랜덤 포레스트 분류기를 유방암 데이터에 훈련시킵니다. n_estimators=100, random_state=42
forest_c = RandomForestClassifier(n_estimators=100, random_state=42)
forest_c.fit(Xc_train, yc_train)
print("Random Forest Training Set Accuracy: {:.3f}".format(forest_c.score(Xc_train, yc_train)))
print("Random Forest Test Set Accuracy: {:.3f}".format(forest_c.score(Xc_test, yc_test)))

Random Forest Training Set Accuracy: 1.000

Random Forest Test Set Accuracy: 0.965

 

랜덤 포레스트는 아무런 매개변수 튜닝 없이도 선형 모델이나 단일 결정 트리보다 높은 정확도를 내고 있습니다. 단일 결정 트리에서 한 것처럼 max_features 매개변수를 조정하거나 사전 가지치기를 할 수도 있습니다. 하지만 랜덤 포레스트는 기본 설정으로도 좋은 결과를 만들어줄 때가 많습니다.

결정 트리처럼 랜덤 포레스트도 특성 중요도를 제공하는데 각 트리의 특성 중요도를 취합하여 계산한 것입니다. 일반적으로 랜덤 포레스트에서 제공하는 특성 중요도가 하나의 트리에서 제공하는 것보다 더 신뢰할 만합니다.

def plot_imp(clf):
  sorted_idx = clf.feature_importances_.argsort()
  y_ticks = np.arange(0, len(cancer.feature_names))
  fig, ax = plt.subplots()
  ax.barh(y_ticks, clf.feature_importances_[sorted_idx])
  ax.set_yticklabels(cancer.feature_names[sorted_idx])
  ax.set_yticks(y_ticks)
  fig.tight_layout()
  plt.show()
from sklearn.tree import DecisionTreeClassifier

# 랜덤 포레스트와 비교하기 위해 결정트리 분류기를 생성하고 유방암 데이터에 훈련시킵니다.
dec_tree = DecisionTreeClassifier(random_state=42)
dec_tree.fit(Xc_train, yc_train)
print("Decision Tree Training Set Accuracy: {:.3f}".format(dec_tree.score(Xc_train, yc_train)))
print("Decision Tree Test Set Accuracy: {:.3f}".format(dec_tree.score(Xc_test, yc_test)))

Decision Tree Training Set Accuracy: 1.000

Decision Tree Test Set Accuracy: 0.951

# 하나의 트리에서 제공하는 feature importance를 살펴보겠습니다.
dec_tree.feature_importances_

array([0.                , 0.02601101, 0.                  , 0.                   , 0.                ,

            0.                 , 0.                   , 0.69593688, 0.                   , 0.                ,

            0.                 , 0.                   , 0.                   , 0.01277192, 0.00155458,

            0.                 , 0.00670697, 0.01702539, 0.                   , 0.                ,

            0.0877369 , 0.10787925 , 0.                , 0.03452044, 0.00985664,

            0.                 , 0.                   , 0.                    , 0.                   , 0. ])

# 결정 트리의 특성 중요도를 시각화해봅니다.
plot_imp(dec_tree)

# 랜덤포레스트의 feature importance를 살펴봅니다.
forest_c.feature_importances_

array([0.03971058, 0.01460399, 0.05314639, 0.04277978, 0.00816485,

            0.01140166, 0.08321459, 0.0902992 , 0.00443533, 0.00443395,

             0.01951684, 0.00459978, 0.00868228, 0.04355077, 0.00464415,

             0.0036549 , 0.00701442, 0.00504716, 0.00371411, 0.00658253,

             0.08127686, 0.01649014, 0.07138828, 0.12319232, 0.01033481,

             0.01580059, 0.03174022, 0.17229521, 0.01310266, 0.00518165])

#  랜덤 포레스트의 feature importance를 시각화해봅니다.
plot_imp(forest_c)

그림에서 알 수 있듯이 랜덤 포레스트에서는 단일 트리의 경우보다 훨신 많은 특성이 0 이상의 중요도 값을 갖습니다. 랜덤 포레스트를 만드는 무작위성은 알고리즘이 가능성 있는 많은 경우를 고려할 수 있도록 하므로, 그 결과 랜덤 포레스트가 단일 트리보다 더 넓은 시각으로 데이터를 바라볼 수 있습니다.

 

3. Ensemble Methods: Bagging and Boosting

3-1. Bagging (=Bootstrap Aggregation)

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.BaggingClassifier.html

 

sklearn.ensemble.BaggingClassifier

 

scikit-learn.org

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import BaggingClassifier

# Bagging을 사용하여 cancer 데이터셋에 LogisticRegression 모델을 100개 훈련하여 앙상블해봅니다.
bagging = BaggingClassifier(LogisticRegression(), n_estimators=100, oob_score=True, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)

BaggingClassifier은 분류기가 predict_proba() 메서드를 지원하는 경우 확률값을 평균하여 예측을 수행합니다. 그렇지 않은 분류기를 사용할 때는 가장 빈도가 높은 클래스 레이블이 예측 결과가 됩니다.

 

3-1-1. Out-of-Bag error (OOB)

Out-of-Bag error은 기계학습에서 bagging을 사용한 모델의 성능을 측정합니다. OOB error은 모델 훈련에 사용되지 않은 데이터에 대해 계산됩니다. OOB error은 bootstrap 훈련 데이터셋에 OOB 샘플이 포함되지 않은 결정 트리의 하위 집합만 사용하여 계산됩니다.

OOB error은 validation score와 다른 성능 측정치입니다. 때때로 데이터 세트의 크기가 충분하지 않기 때문에 validation dataset을 구성하기 어려운 경우가 있습니다. 대규모 데이터가 아니며, 모든 데이터를 훈련 데이터셋으로 사용하려는 경우 OOB error으로 모델의 성능을 판단할 수 있습니다.

print("Train set accuracy: {:.3f}".format(bagging.score(Xm_train, ym_train)))
print("Test set accuracy: {:.3f}".format(bagging.score(Xm_test, ym_test)))

#  OOB 오차를 출력합니다.
print("OOB sample score: {:.3f}".format(bagging.oob_score_))

Train set accuracy: 0.813

Test set accuracy: 0.880

OOB sample score: 0.807

from sklearn.tree import DecisionTreeClassifier

#결정 트리로 배깅을 수행하는 것보다 랜덤 포레스트를 사용하는 것이 편리하지만 여기서는 직접 결정 트리에 배깅을 적용해보겠습니다.
bagging = BaggingClassifier(DecisionTreeClassifier(), n_estimators=5, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)
# 랜덤 포레스트에서처럼 이 배깅 분류기에 있는 결정 트리의 결정 경계를 시각화해보겠습니다.
plot_decision_boundary(bagging)

결과 그래프는 랜덤 포레스트의 결정 경계와 매우 비슷합니다.

# `n_estimators=100`으로 늘려서 cancer 데이터셋에 훈련시켜보고 훈련 세트의 테스트 세트 성능을 확인해보겠습니다.
bagging = BaggingClassifier(DecisionTreeClassifier(), n_estimators=100, oob_score=True, n_jobs=-1, random_state=42)
bagging.fit(Xm_train, ym_train)
print("Train set accuracy: {:.3f}".format(bagging.score(Xm_train, ym_train)))
print("Test set accuracy: {:.3f}".format(bagging.score(Xm_test, ym_test)))
print("OOB sample score: {:.3f}".format(bagging.oob_score_))

Train set accuracy: 1.000

Test set accuracy: 0.900

OOB sample score: 0.900

 

배깅은 랜덤 포레스트와 달리 max_samples 매개변수에서 부트스트랩 샘플의 크기를 지정할 수 있습니다. 또한 랜덤 포레스트는 DecisionTreeClassifier(splitter=“best”)를 사용하도록 고정되어 있습니다. 결정 트리를 splitter=‘random’으로 설정하면 무작위로 분할한 후보 노드 중에서 최선의 분할을 찾습니다.

 

3-2. Boosting

Adaboost(=Adaptive Boosting)은 ensemble method의 boosting method 중 가장 유명한 방법입니다. 다른 boosting 방법들 중에는 XGBoost, GradientBoost, 그리고 BrownBoost 등이 있습니다. Adaboost은 일련의 단일 모델을 반복적으로 가중치가 부여된 데이터에 학습시킵니다. 그 다음 모든 예측은 가중 다수결 (혹은 합계)를 통해 결합되어 최종 예측을 생성합니다.

처음에는 가중치가 모든 데이터 포인트에 1/N으로 동일하게 설정됩니다. 순차적으로 훈련을 반복하면서 가중치는 개별적으로 수정되고, 단일 모델은 재조정된 데이터에 다시 훈련됩니다. 잘못 예측된 데이터는 가중치가 증가하며, 올바르게 예측된 데이터는 가중치가 감소합니다. 훈련이 반복될수록 예측하기 어려운 데이터가 더욱 많은 가중치를 받게 됩니다. 이러한 원리 때문에 boosting이라고 불립니다.

scikit-learn의 AdaBoostClassifier는 기본적으로 DecisionTreeClassifier(max_depth=1)를 사용하고 AdaBoostRegressor는 decisionTreeRegressor(max_depth=3)를 사용하지만 base_estimator 매개변수에서 다른 모델을 지정할 수도 있습니다. 순차적으로 학습해야 하기 때문에 n_jobs 매개변수를 지원하지 않습니다.

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostClassifier.html

 

sklearn.ensemble.AdaBoostClassifier

Examples using sklearn.ensemble.AdaBoostClassifier: Classifier comparison Discrete versus Real AdaBoost Multi-class AdaBoosted Decision Trees Plot the decision surfaces of ensembles of trees on the...

scikit-learn.org

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.AdaBoostRegressor.html

 

sklearn.ensemble.AdaBoostRegressor

Examples using sklearn.ensemble.AdaBoostRegressor: Decision Tree Regression with AdaBoost

scikit-learn.org

 

from sklearn.ensemble import AdaBoostClassifier

# AdaBoost를 two_moons 데이터에 적용해봅니다.
ada = AdaBoostClassifier(n_estimators=5, random_state=42)
ada.fit(Xm_train, ym_train)
# AdaBoost의 각 estimator의 결정 경계를 시각화해봅니다.
plot_decision_boundary(ada)

AdaBoostClassifier는 깊이가 1인 결정 트리를 사용하기 때문에 각 트리의 결정 경계가 하나의 직선입니다. 앙상블된 결정 경계도 다른 앙상블 모델에 비해 더 단순합니다.

# 이번에는 AdaBoost를 breast cancer 데이터에 적용해봅니다.
ada = AdaBoostClassifier(n_estimators=100, random_state=42)
ada.fit(Xc_train, yc_train)
print("Train set accuracy: {:.3f}".format(ada.score(Xc_train, yc_train)))
print("Test set accuracy: {:.3f}".format(ada.score(Xc_test, yc_test)))

Train set accuracy: 1.000

Test set accuracy: 0.944

# AdaBoost의 특성 중요도를 시각화해봅니다.
plot_imp(ada)

AdaBoost의 특성 중요도를 확인해보면 다른 모델에서 부각되지 않았던 compactness error 특성을 크게 강조하고 있습니다.

 

3-2-1. Bagging vs Boosting

 

4. Feature Selection

새로운 특성을 만드는 방법이 많으므로 데이터의 차원이 원본 특성의 수 이상으로 증가하기 쉽습니다. 그러나 특성이 추가되면 모델은 더 복잡해지고 과적합될 가능성도 높아집니다. 보통 새로운 특성을 추가할 때나 고차원 데이터셋을 사용할 때, 가장 유용한 특성만 선택하고 나머지는 무시해서 특성의 수를 줄이는 것이 좋습니다. 이렇게 하면 모델이 간단해지고 일반화 성능이 올라갑니다.

이를 위한 전략으로 1) 일변량 통계 (univariate statistics), 2) 모델 기반 선택 (model-based selection), 3) 반복적 선택 (iterative selection)이 있습니다. 이 방법들은 모두 지도 학습 방법이므로 최적값을 찾으려면 타깃값이 필요합니다. 그리고 데이터를 훈련 세트와 테스트 세트로 나눈 다음 훈련 데이터만 특성 선택에 사용해야 합니다.

 

4-1. 일변량 통계

일변량 통계에서는 개개의 특성과 타깃 사이에 중요한 통계적 관계가 있는지를 계산합니다. 그런 다음 깊게 관련되어 있다고 판단되는 특성을 선택합니다. 분산분석(ANOVA)이라고도 합니다.

이 방식의 핵심 요소는 일변량, 즉 각 특성이 독립적으로 평가된다는 점입니다. 따라서 다른 특성과 깊게 연관된 특성은 선택되지 않을 것입니다. 일변량 분석은 계산이 매우 빠르고 평가를 위해 모델을 만들 필요가 없습니다. 한편으로 이 방식은 특성을 선택한 후 적용하려는 모델에 상관없이 사용할 수 있습니다.

scikit-learn에서 일변량 분석으로 특성을 선택하려면 분류에서는 f-classif을, 회귀에서는 f_regression을 보통 선택하여 테스트하고, 계산한 p-value에 기초하여 특성을 제외하는 방식을 선택합니다. 이런 방식들은 매우 높은 p-value를 가진 특성(즉, 타깃값과 연관성이 작은 특성)을 제외할 수 있도록 임계값을 조정하는 매개변수를 사용합니다. 임계값을 계산하는 방법은 각각 다르며, 가장 간단한 SelectKBest는 고정된 k개의 특성을 선택하고, SelectPercentile은 지정된 비율만큼 특성을 선택합니다. 그럼 cancer 데이터셋에 분류를 위한 특성 선택을 적용해보겠습니다. 문제를 조금 복잡하게 하기위해 의미 없는 노이즈 특성을 데이터에 추가하겠습니다. 특성 선택이 이 의미 없는 특성을 식별해서 제거하는지 보겠습니다.

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectPercentile.html

 

sklearn.feature_selection.SelectPercentile

Examples using sklearn.feature_selection.SelectPercentile: Feature agglomeration vs. univariate selection Introducing the set_output API Column Transformer with Mixed Types SVM-Anova: SVM with univ...

scikit-learn.org

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.f_classif.html

 

sklearn.feature_selection.f_classif

Examples using sklearn.feature_selection.f_classif: Pipeline ANOVA SVM Univariate Feature Selection SVM-Anova: SVM with univariate feature selection

scikit-learn.org

 

from sklearn.datasets import load_breast_cancer
from sklearn.feature_selection import SelectPercentile, f_classif
from sklearn.model_selection import train_test_split

cancer = load_breast_cancer()

# 고정된 난수를 발생시킵니다.
rng = np.random.RandomState(42)
noise = rng.normal(size=(len(cancer.data), 50))
# 데이터에 노이즈 특성을 추가합니다. 처음 30개는 원본 특성이고 다음 50개는 노이즈입니다.
X_w_noise = np.hstack([cancer.data, noise])
# 원 데이터와 노이즈가 추가된 데이터의 크기를 비교해봅니다.
print(cancer.data.shape)
print(X_w_noise.shape)

(569, 30)

(569, 80)

# 데이터를 train/test셋으로 나눕니다. random_state=42, test_size=0.5
X_train, X_test, y_train, y_test = train_test_split(X_w_noise, cancer.target, random_state=42, test_size=0.5)
# f_classif(기본값)과 SelectPercentile을 사용하여 특성의 50%를 선택합니다.
select = SelectPercentile(score_func=f_classif, percentile=50)
select.fit(X_train, y_train)

# 훈련 세트에 적용합니다.
X_train_selected = select.transform(X_train)
print("X_train.shape", X_train.shape)
print("X_train_selected.shape", X_train_selected.shape)

결과에서 볼 수 있듯이 특성 개수가 80개에서 40개로 줄었습니다. (원본 특성의 50%)

# `get_support` 메서드는 선택된 특성을 boolean 값으로 표시해줍니다. 어떤 특성이 선택되었는지 확인해봅니다.
mask = select.get_support()
print(mask)

[ True True True True True True True True True True True False True True False True True True False False True True True True True True True True True True False False False True False True False False False False False False False True False False False True False False False False False False True False False False True False False False True False False False True True True False False False True True False False False False True True]

# 위 마스크를 시각화해봅니다.
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("feature number")
plt.yticks([0])

마스킹된 그래프에서 볼 수 있듯이 선택된 특성은 대부분 원본 특성이고 노이즈 특성이 거의 모두 제거되었습니다. 그러나 원본 특성이 완벽하게 복원된 것은 아닙니다. 전체 특성을 이용했을 때와 선택된 특성만 사용했을 때 로지스틱 회귀의 성능을 비교해보겠습니다.

from sklearn.linear_model import LogisticRegression

# 테스트 데이터 변환
X_test_selected = select.transform(X_test)

# LogisticRegression모델을 생성하고 원본 데이터에 훈련시킨 성능과 중요한 특성들이 선택된 데이터에 훈련시킨 성능을 비교해봅니다.
lr = LogisticRegression(max_iter=10000)
lr.fit(X_train, y_train)
print("전체 특성을 사용한 정확도", lr.score(X_test, y_test))
lr.fit(X_train_selected, y_train)
print("선택된 일부 특성을 사용한 정확도", lr.score(X_test_selected, y_test))

전체 특성을 사용한 정확도 0.9614035087719298

선택된 일부 특성을 사용한 정확도 0.9649122807017544

 

이 경우에서는 일부 원본 특성이 없더라도 노이즈 특성을 제거한 쪽의 성능이 더 높습니다. 이 예는 인위적으로 간단하게 만든 예제이고 실제 데이터에서의 결과는 보통 엇갈리는 경우도 많습니다. 하지만 너무 많은 특성때문에 모델을 만들기가 현실적으로 어려울 때 일변량 분석으로 사용하여 특성을 선택하면 큰 도움이 될 수 있습니다. 또는 많은 특성들이 확실히 도움이 안 된다고 생각될 때 사용할 수 있습니다.

 

4-1-1. Selecting K best features

SelectPercentile 말고도 K 개의 중요한 특성을 선택하는 방법이 있습니다. SelectKBest를 이용하면 가능합니다. SelectKBest는 사용자가 지정한 k개의 중요한 특성을 반환해줍니다. 다만 위에서 사용한 데이터에는 음의 값이 있기 때문에 SelectKBest를 사용할 수 없습니다. 따라서 이 예제는 Iris 데이터를 사용합니다.

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectKBest.html

 

sklearn.feature_selection.SelectKBest

Examples using sklearn.feature_selection.SelectKBest: Release Highlights for scikit-learn 1.1 Pipeline ANOVA SVM Univariate Feature Selection Concatenating multiple feature extraction methods Selec...

scikit-learn.org

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.chi2.html#sklearn.feature_selection.chi2

 

sklearn.feature_selection.chi2

Examples using sklearn.feature_selection.chi2: Column Transformer with Mixed Types

scikit-learn.org

 

from sklearn.datasets import load_iris

iris_X, iris_y = load_iris(return_X_y=True)
iris_X.shape

(150, 4)

from sklearn.feature_selection import SelectKBest, chi2

# SelectKBest와 chi2 테스트를 사용하여 iris data에서 두개의 가장 중요한 특성을 선택합니다.
X_new = SelectKBest(chi2, k=2).fit_transform(iris_X, iris_y)
print(X_new.shape)

(150, 2)

 

4.2 모델 기반 특성 선택

모델 기반 특성 선택은 지도 학습 머신러닝 모델을 사용하여 특성의 중요도를 평가해서 가장 중요한 특성들만 선택합니다. 특성 선택에 사용되는 지도 학습 모델은 최종적으로 사용할 지도 학습 모델과 같을 필요는 없습니다. 특성 선택을 위한 모델은 각 특성의 중요도를 측정하여 순서를 매길 수 있어야 합니다. 결정 트리와 이를 기반으로 한 모델은 각 특성의 중요도가 담겨있는 feature_importance_ 속성을 제공합니다. 일변량 분석과는 반대로 모델 기반 특성 선택은 한번에 모든 특성을 고려하므로 (사용된 모델이 상호작용을 잡아낼 수 있다면) 상호작용 부분을 반영할 수 있습니다. 모델 기반의 특성 선택은 SelectFromModel에 구현되어 있습니다.

scikit-learn: https://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectFromModel.html

 

sklearn.feature_selection.SelectFromModel

Examples using sklearn.feature_selection.SelectFromModel: Model-based and sequential feature selection

scikit-learn.org

 

from sklearn.feature_selection import SelectFromModel
from sklearn.ensemble import RandomForestClassifier

# RandomForestClassifier와 SelectFromModel을 이용하여 중요한 특성을 선택합니다.
select = SelectFromModel(RandomForestClassifier(n_estimators=100, random_state=42), threshold='median')

SelectFromModel은 지도 학습 모델로 계산된 중요도가 지정한 임계치보다 큰 모든 특성을 선택합니다. 일변량 분석으로 선택한 특성과 결과를 비교하기 위해 절반 가량의 특성이 선택될 수 있도록 중간값을 임계치로 사용하겠습니다. 트리 100개로 만든 랜덤포레스트 분류기를 사용해 특성 중요도를 계산합니다. 이는 매우 복잡한 모델이고 일변량 분석보다는 훨씬 강력한 방법입니다.

# 모델을 데이터에 훈련시키고 선택된 특성의 shape을 출력합니다.
select.fit(X_train, y_train)
X_train_l1 = select.transform(X_train)
print("X_train.shape:", X_train.shape)
print("X_train_l1.shape:", X_train_l1.shape)

X_train.shape: (284, 80)

X_train_l1.shape: (284, 40)

# 선택된 특성을 같은 방식으로 그려보겠습니다.
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("featue number")

이번에는 두 개를 제외한 모든 원본 특성이 선택되었습니다. 특성을 40개 선택하도록 지정했으므로 일부 노이즈 특성도 선택되었습니다.

# LogisticRegression 모델로 성능이 얼마나 향상되었는지 확인해봅니다.
X_test_l1 = select.transform(X_test)
score = LogisticRegression(max_iter=10000).fit(X_train_l1, y_train).score(X_test_l1, y_test)
print("test score", score)

test score 0.9508771929824561

 

3.3 반복적 특성 선택

반복적 특성 선택(iterative feature selection)에서는 특성의 수가 각기 다른 일련의 모델이 만들어집니다. 기본적으로 두가지 방법이 있습니다.

  • 특성을 하나도 선택하지 않은 상태로 시작해서 어떤 종료 조건에 도달할 때 까지 하나씩 추가하는 방법
  • 모든 특성을 가지고 시작해서 어떤 종료 조건이 될 때까지 특성을 하나씩 제거해가는 방법

일련의 모델이 만들어지기 때문에 이 방법은 앞서 소개한 방법들보다 계산 비용이 월씬 많이 듭니다. 재귀적 특성 제거 (Recursive Feature Elimination, RFE)가 이런 방법의 하나입니다. 이 방법은 모든 특성으로 시작해서 모델을 만들고 특성 중요도가 가장 낮은 특성을 제거합니다. 그런 다음 제거한 특성을 빼고 나머지 특성 전체로 새로운 모델을 만듭니다. 이런 식으로 미리 정의한 특성 개수가 남을 때까지 계속합니다. 이를 위해 모델 기반 선택에서처럼 특성 선택에 사용할 모델은 특성의 중요도를 결정하는 방법을 제공해야 합니다. 다음은 앞에서와 같은 랜덤 포레스트 모델을 사용합니다.

 

from sklearn.feature_selection import RFE

# RFE를 생성하고 데이터에 훈련시킵니다.
select = RFE(RandomForestClassifier(n_estimators=100, random_state=42), n_features_to_select=40)
select.fit(X_train, y_train)
# 선택된 특성을 시각화합니다.
mask = select.get_support()
plt.matshow(mask.reshape(1, -1), cmap='gray_r')
plt.xlabel("feature number")

일변량 분석이나 모델 기반 선택보다 특성 선택이 나아졌지만, 여전히 특성 한개를 놓쳤습니다. 랜덤포레스트 모델은 특성이 누락될 때마다 다시 학습하므로 40번이나 실행됩니다. 그래서 이 코드를 실행하면 모델 기반 선택보다 훨씬 오래 걸립니다.

# RFE를 사용해서 특성을 선택했을 때 로지스틱 회귀의 정확도를 확인해보겠습니다.
X_train_rfe = select.transform(X_train)
X_test_rfe = select.transform(X_test)

score = LogisticRegression(max_iter=10000).fit(X_train_rfe, y_train).score(X_test_rfe, y_test)
print("test score", score)

test score 0.9578947368421052

# 또한 RFE에 사용된 모델을 이용해서도 예측을 할 수 있습니다. 이 경우 선택된 특성만 사용됩니다.
score = select.score(X_test, y_test)
print(score)

0.9508771929824561

 

RFE안에 있는 랜덤 포레스트의 성능이 이 모델에서 선택한 특성으로 만든 로지스틱 회귀의 성능과 비슷합니다. 다른 말로 하면, 특성 선택이 제대로 되면 선형 모델의 성능은 랜덤 포레스트와 견줄만 합니다.

머신 러닝 알고리즘에 어떤 입력값을 넣을지 확신이 안선다면 특성 자동 선택이 도움이 될 수 있습니다. 또한 예측속도를 높이거나 해석하기 더 쉬운 모델을 만드는 데 필요한 만큼 특성의 수를 줄이는 데도 효과적입니다.