앞서, Decision Tree에 관한 개념과 알고리즘을 통해 구현해 보았습니다. 이번 블로그에서는 Decision Tree의 시각화 및 특성 중요도에 대하여 알아보고 결정 트리의 장단점에 대하여 알아보도록 하겠습니다.
트리 시각화 하기
트리 모듈의 export_graphviz 함수를 이용하면 트리를 시각화할 수 있습니다.
from sklearn.tree import export_graphviz
export_graphviz(tree, out_file="tree.dot", class_names=["악성","양성"],
feature_names=cancer.feature_names, impurity=False, filled=True)
with open("tree.dot") as f:
dot_graph = f.read()
display(graphviz.Source(dot_graph))
트리를 시각화 함에 따라 알고리즘의 예측을 쉽게 할 수 있게 됐습니다. 깊이가 4만 되었는데도 불구하고 매우 복잡한데, 보통은 10이 정도의 깊이를 사용합니다. samples는 각 노드에 있는 샘플의 수를 의미하며, value는 클래스당 샘플 수를 제공합니다.
트리의 특성 중요도 ( Feature Importance )
전체 트리를 살펴보는 것은 힘든 일입니다. 대신 트리가 어떻게 작동하는지 요약하는 속성들을 사용할 수 있습니다. 가장 널리 사용되는 속성은 트리를 만드는 결정에 각 특성이 얼마나 중요한지를 평가하는특성 중요도이 값은 0과 1 사이의 숫자로, 각 특성에 대해 0은 전혀 사용되지 않았다는 것을 의미하고, 1은 완벽하게 타깃 클래스를 예측했다는 뜻이 됩니다.
첫 번째 노드에서 사용한 특성인 worst radius 가 가장 중요한 특성으로 나타나는 것이 확인됩니다. 이 그래프는 첫 번째 노드에서 두 클래스( 악성, 양성 )를 꽤나 잘 나누고 있다는 것을 의미합니다.
feature_importance_의 값이 낮다고 아예 중요하지 않은 값은 아닙니다. 단지 트리가 그 특성을 선택하지 않은 것뿐이며 다른 특성이 동일한 정보를 지니고 있어서 일 수도 있습니다.
선형 모델의 계수와는 다르게 특성 중요도는 언제나 양수이며 특성이 어느 클래스를 지지하는지 ( 결정하는데 역할을 하는지 ) 알 수가 없습니다. 즉 특성 중요도의 값은 "worst radius"가 중요하다고 알려주지만, 이 특성이 양성을 의미하는지, 악성을 의미하는지는 알 수 없습니다.
사실 특성과 클래스 사이에는 간단하지 않은 관계가 있을 수 있기 때문에 다음 예를 들어 보겠습니다.
tree = mglearn.plots.plot_tree_not_monotone()
display(tree)
두 개의 특성과 두 개의 클래스를 가진 데이터셋을 표현하고 있습니다. X [1]에 있는 정보만 사용되었고, X [0]은 전혀 사용되지 않았습니다. 그런데 X [1]과 출력 클래스와의 관계는 단순히 비례나 반비례하지 않습니다. 즉 "X [1]의 값이 높으면 클래스 0이고 값이 낮으면 1"이라고 말할 수 없습니다.
결론적으로 데이터의 분포도와 트리에 사용된 특성을 참고하면서 트리에서 사용한 특성이 정말 중요한 특성인지, 혹시 놓친 특성은 없는지 살펴봐야 할 것 같습니다.
결정 트리 회귀
우리는 여기서 결정 트리를 가지고 분류에 대해서만 논하고 있지만 사실상 회귀에서도 비슷하게 적용됩니다. 회귀 트리의 사용법은 분류 트리와 매우 비슷합니다. 하지만 회귀를 위한 트리 기반의 모델을 사용할 때 확인 해 봐야 할 속성이 있습니다.
DecisionTreeRegressor( 그리고 모든 다른 트리 기반 회귀 모델 )는외삽(extrapolation), 즉 훈련 데이터의 범위 밖의 포인트에 대해 예측을 할 수 없습니다.
이번 포스팅에서는 의사결정 트리(Decision Tree)에 대하여 알아보도록 하겠습니다!
결정 트리(Decision Tree)
분류와 회귀 문제에 널리 사용하는 모델입니다. 기본적으로 결정 트리는 스무고개 놀이와 비슷합니다. 던지는 질문에 Yes / No를 결정해 문제를 해결합니다.
#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
from sklearn.model_selection import train_test_split
import platform
import graphviz
plt.rcParams['axes.unicode_minus'] = False
%matplotlib inline
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
font_name = font_manager.FontProperties(fname=path).get_name()
rc('font', family=font_name)
else:
print('Unknown system... sorry!')
mglearn.plots.plot_animal_tree()
4가지 동물( 매, 펭귄, 돌고래, 곰 )을 구분하는 모습을 보여주고 있습니다. 이 그림에서 트리의 노드는 질문이나 정답을 담은 네모 상자입니다.
마지막 정답이 들어있는 노드는 리프라고도 이야기합니다.
머신러닝 식으로 이야기하자면 "날개가 있나요"?, "날 수 있나요?", "지느러미가 있나요?"를 이용해 네 개의 클래스를 구분하는 모델을 만든 것입니다. 이런 모델을 직접 만드는 것이 아닌 지도 학습 방식으로 데이터로부터 학습할 수 있습니다.
결정 트리 만들기
2차원 데이터셋을 분류하는 결정트리를 만들어 봅시다. 이 데이터셋은 각 클래스에 데이터 포인트가 75개씩 존재합니다. 그리고 반달 두 개가 포개진듯한 모양을 하고 있습니다.
이 데이터셋을 two_moons라고 합니다.
결정 트리를 학습한다는 것은 정답에 가장 빨리 도달하는 예 / 아니오 질문 목록을 학습한다는 뜻입니다. 머신러닝에서는 이런 질문들을 테스트 라고합니다. ( 테스트 세트라고 생각하면 절대 안도미!)
위의 동물 예제처럼 예 / 아니오 형태의 특성으로 구성되지 않고, 2차원 데이터셋과 같이 연속된 특성으로 구성됩니다. 즉 "특성 𝑖는 값 𝑎 보다 큰가?"와 같은 형태입니다.
mglearn.plots.plot_tree_progressive()
알고리즘 고르기
트리를 만들 때 알고리즘은 가능한 모든 테스트에서 타깃값에 대해 가장 많은 정보를 가진 것을 고릅니다.
depth 1
트리의 깊이가 1인 상태에서의 테스트를 사려 보겠습니다. 데이터셋을 X [1] = 0.06에서 수평으로 나누는 것이 가장 많은 정보를 포함시키고 있습니다. 즉 이 직선이 0에 속한 포인트와 클래스 1에 속한 포인트를 가장 잘 나누고 있습니다. 파란 점은 클래스 0을 의미하며, 세모는 클래스 1을 의미합니다.
루트 노드라고도 이야기하는 맨 위 노드는 클래스 0에 속한 포인트 75개와 클래스 1에 속한 포인트 75개를 모두 포함한 전체 데이터셋을 의미합니다.
X [1] <= 0.6 테스트에 의해 true가 된 것들은 왼쪽 노드에 할당되고, 그렇지 않은 것들은 오른쪽에 할당됩니다.
왼쪽 노드에는 클래스 0에 속한 포인트는 2개, 클래스 1에 속한 포인트는 32개입니다. 오른쪽 노드에는 클래스 0에 속한 포인트는 48개, 클래스 1에 속한 포인트는 18개 있는 것으로 확인되네요
이 두 노드는 첫 번째 그림의 윗부분과 아랫부분으로 각각 분류됩니다.
대부분 잘 분류하기는 했지만 아직 완벽하게 분류하진 못한 것 같습니다.
depth 2
트리의 깊이가 2인 상태에서는 깊이가 1인 상태에서 보다 조금 더 각 클래스 별 정보를 영역에 담을 수 있도록 X [0] 값을 기준으로 왼쪽과 오른쪽 영역으로 나누고 있습니다.
이렇게 계속되는 테스트 ( 반복되는 프로세스 )는 각 노드가 테스트 하나씩을 가진 이진 결정 트리를 만들어 냅니다. 다르게 말하며 각 테스트는 하나의 축을 따라 데이터를 둘로 나누는 것이라고 생각해 볼 수도 있습니다. 이는 계층적으로 영역을 분할해 가는 알고리즘이라고 할 수 있습니다.
데이터를 분할하는 것은 각 분할된 영역이 ( 결정 트리의 리프 ) 한 개의 타깃값 ( 하나의 클래스나 하나의 회귀 분석 결과)을 가질 때까지 반복됩니다. 각 테스트는 하나의 특성에 대해서만 이루어지기 때문에 나누어진 영역은 항상 평행합니다. 이때 타깃 하나로만 이뤄진 리프 노드를 순수 노드(pure node)라고 합니다.
depth 9
순수 노드가 존재하는 최종 분할 트리라고 할 수 있는 깊이가 9인 상태를 보면 잘 보이지는 않지만 더 이상 분할 하지 않고 값을 결정 지어버리는 노드도 있습니다. 이 노드들이 위에서 이야기했던 순수 노드입니다.
결론
새로운 데이터 포인트에 대한 예측은 주어진 데이터 포인트가 특성을 분할한 영역들 중 어디에 놓이는지를 확인해 보면 됩니다.
타깃 값중 다수를 차지하는 노드
순수 노드로 결정 지어지는 경우
위의 두 가지로 예측결과를 지정합니다.
루트 노드에서 시작해 테스트의 결과에 따라 왼쪽 또는 오른쪽으로 트리를 탐색해 나가는 식으로 영역을 찾아낼 수 있습니다.
같은 방법으로 회귀 문제에도 트리를 사용할 수 있습니다. 예측을 하러면 각 노드의 테스트 결과에 따라 트리를 탐색해 나가고 새로운 데이터 포인트에 해당되는 리프 노드를 찾습니다. 찾은 리프 노드의 훈련 데이터 평균값이 이 데이터 포인트의 출력이 됩니다.
결정 트리 복잡도 제어
트리를 만들어 낼 때 모든 리프가 순수 노드가 될 때까지 진행한다면 모델이 매우 복잡해지고 훈련 데이터에 과대 적합합니다. 득 훈련 세트에 100% 정확하게 맞는다는 의미가 됩니다.
즉 순수 노드는 정확학 클래스의 리프노드라고 볼 수 있습니다. depth 9 일 때의 그림을 보시면 왼쪽 그래프가 굉장히 과대적합 되었다고 볼 수 있습니다.
클래서 1로 결정된 영역이 클래스 0에 속한 포인트들로 둘러싸인 것을 볼 수 있는데, 그 반대의 모습도 찾아볼 수 있습니다.
결정 경계가 클래스의 포인트들에서 멀리 떨어진 이상치(outlier)에 너무 민감해지기 때문이죠
과대적합 막기
과대적합을 막으려면 크게 두 가지로 전략을 세워야 합니다.
트리 생성을 일찍 중단(사전 가지치기 ( Pre pruning ))
트리를 다 만들고 데이터 포인트가 적은 노드를 삭제하거나 병합 (사후 가지치기( Post pruning ))
먼저 사전 가지치기 방법은 트리의 최대 깊이나 리프의 최대 개수를 제한하거나 노드가 분할하기 위한 포인트의 최소 개수를 지정하는 것입니다.
scikit-learn에서 결정트리는 DecisionTreeRegressor와 DecisionTreeClassifier에 구현되어 있습니다. 사전 가지치기 만을 지원합니다.
유방암 데이터 셋을 이용해 사전 가지치기의 효과에 대해서 알아보겠습니다. 기본값 설정으로 완벽한 트리 ( 모든 리프 노드가 순수 노드가 될 때까지 생성한 트리 ) 모델을 만들어 봅니다.
from sklearn.datasets import load_breast_cancer
from sklearn.tree import DecisionTreeClassifier
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, stratify=cancer.target, random_state=42)
tree = DecisionTreeClassifier(random_state = 0)
tree.fit(X_train, y_train)
print("훈련 세트 정확도 : {:.3f}".format(tree.score(X_train, y_train)))
print("테스트 세트 정확도 : {:.3f}".format(tree.score(X_test, y_test)))
생각 한대로 모든 리프가 순수 노드이기 때문에 훈련 세트의 정확도는 100%입니다. 즉 트리는 훈련 데이터의 훈련 데이터의 모든 레이블을 완벽하게 기억하고 있을 만큼 충분히 깊습니다.
테스트 세트의 정확도는 이전에 본 선형 모델에서의 정확도인 95% 보다 약간 낮습니다.
결정 트리의 깊이를 제한하지 않으면 트리는 무한정 깊어지고 복잡해질 수 있습니다. 따라서 가지치기를 하지 않은 트리는 과대적합 되기 쉽고 새로운 데이터에 잘 일반화되지 않습니다.
사전 가지치기 적용
이제 유방암 데이터셋을 위한 결정 트리에 사전 가지치기를 통해 트리가 깊어지는 것을 막아 보겠습니다. 방법은 트리가 훈련 데이터에 완전히 학습되기 전에 트리의 성장을 막아보는 것입니다. max_depth 매개변수를 이용하며, 유방암 데이터셋에서는 4 옵션을 줘서 막을 수 있습니다. 즉 연속된 질문을 4개로 제한하게 됩니다.
트리의 깊이를 제한하면 과대적합이 줄어듭니다. 이는 훈련 세트의 정확도를 떨어뜨리지만 테스트 세트의 성능을 개선시킵니다.
선형 모델은 매우 오래전 개발된 모델입니다. 선형 모델은 입력 특성에 대한 선형 함수를 만들어 예측을 수행합니다. 먼저 회귀의 선형 모델부터 알아보겠습니다
회귀의 선형 모델을 위한 일반화된 예측 함수는 다음과 같습니다.
𝑦̂ =𝑤[0]∗𝑥[0]+𝑤[1]∗𝑥[1]+...+𝑤[𝑝]∗𝑥[𝑝]+𝑏
위의 식에서 𝑥[0]부터𝑥[𝑝]까지는 하나의 데이터 포인트에 대한 특성을 나타내고, (특성의 수는 𝑝+1 )와𝑏는 모델이 학습해야 할 파라미터입니다. 𝑦̂ 는 모델이 만들어낸 예측값입니다.
그렇다면 특성이 한 개인 예측 함수는 어떻게 될까요? 다음과 같습니다.
𝑦̂ =𝑤[0]∗𝑥[0]+𝑏
위의 함수를 보면 예전 수학시간에 배운 직선의 방정식이 떠오릅니다. 𝑤[0]은 직선의 기울기( 계수(cofficient)라고도 합니다 )가 되고, 𝑏는 y 축과 만나는 절편( offset 또는 intercept )이 됩니다.
특성이 많아질수록 𝑤는 각 특성에 해당하는 기울기를 모두 가지게 됩니다. 즉 예측값은 입력특성에 𝑤의 가중치를 모두 더한 가중치의 합으로 볼 수 있겠습니다.
머신러닝에서 알고리즘이 주어진 데이터로부터 학습하는 파라미터라서 𝑤와 𝑏를 모델 파라미터라고 합니다.
나중에 살펴보겠지만 모델이 학습할 수 없어 사람이 직접 설정해 줘야 하는 파라미터를 하이퍼 파라미터(Hyper Parameter)라고 합니다.
wave 데이터셋을 이용해 눈으로 확인해 보겠습니다.
#필요 라이브러리 임포트
from IPython.display import display
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import mglearn
plt.rcParams['axes.unicode_minus'] = False
import platform
path = 'c:/Windows/Fonts/malgun.ttf'
from matplotlib import font_manager, rc
if platform.system() == 'Darwin':
rc('font', family='AppleGothic')
elif platform.system() == 'Windows':
font_name = font_manager.FontProperties(fname=path).get_name()
rc('font', family=font_name)
else:
print('Unknown system... sorry!')
# mglearn 샘플 그래프 보기
mglearn.plots.plot_linear_regression_wave()
그려진 그래프를 보면 직선의 방정식이 생각나는데요, 기울기는 대략 0.4, 절편은 0에 가깝게 나타나고 있는 것이 확인됩니다.
회귀 모델을 위한 선형 모델은 특성이 하나인 경우에는 직선, 두 개면 평면이 되며, 세 개 이상( 더 높은 차원 )이 되면 초평면(Hyperplane)이 되는 회귀 모델의 특징을 가지고 있습니다.
위의 그래프와 이전에 했었던 KNeighborsRegressor를 사용하여 만든 그래프를 비교해 보면 어떤가요? 직선을 지나는 점이 얼마 없는 것으로 보아 데이터의 상세정보를 대부분 잃어버린 것처럼 보이지 않나요?
사실 wave 같은 저 차원 데이터 셋에서는 타깃의 𝑦의 특성이 선형 조합이라고 생각하는 것은 매우 비현실 적인 가정입니다. 직선을 따라 데이터가 쭉 연결되지는 않으니까요.
하지만 1차원 데이터셋만 놓고 봐서 생긴 우리의 편견 일 수도 있습니다. 특성이 많은 데이터 셋이라면 선형 모델은 매우 훌륭한 성능을 발휘하며, 훈련 데이터보다 특성이 더 많은 경우엔 어떤 타깃 𝑦도 완벽하게 훈련 세트에 대해서 선형 함수로 모델링할 수 있습니다.
회귀를 위한 선형 모델은 다양합니다.
인기 있는 몇 가지 선형 모델을 살펴보려고 합니다. 이 모델들은 훈련 데이터로부터 모델 파라미터 𝑤와 𝑏를 학습하는 방법과 모델의 복잡도를 제어하는 방법에서 차이가 납니다.
선형 회귀 Linear Regression (최소제곱법)
선형 회귀(Linear Regression) 또는 최소 제곱법(OLS, Ordinary Least Squares)은 가장 간단하고 오래된 회귀용 선형 알고리즘이라고 할 수 있습니다.
선형 회귀는 훈련 세트에 있는 타깃 y 사이에 평균 제곱 오차(MSE - Mean Squared Error)를 최소화하는 모델 파라미터 𝑤와 𝑏를 찾습니다. 참고로 MSE는 다음과 같습니다.
위의 수식의 𝑛은 샘플의 개수입니다. 이를 𝐿2 norm을 적용했다라고 이야기 합니다.
위의 수식이 조금 불편 하긴 하지만 사실 MSE는 모델이 예측한 예측값( 𝑦𝑖^ )과 훈련 세트로 인해 훈련된 값 타깃값( 𝑦𝑖 )의 차이를 제곱하여 더한 후에 평균을 구한 것이라고 보면 됩니다. ( 샘플의 개수로 나눈 것 )
from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔
from sklearn.linear_model import LinearRegression
X, y = mglearn.datasets.make_wave(n_samples=60) # 60개의 샘플 데이터 준비
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) # 훈련, 테스트 데이터셋 준비
lr = LinearRegression().fit(X_train, y_train) # 선형 회귀 모델에 훈련!
모델이 데이터를 입력받고 훈련을 통해 알아내야 할 𝑤( 가중치 - weight 또는 계수 - cofficient )와 𝑏( 편향 - offset 또는 절편 - intercept ) 은 각각 coef와 intercept 변수에 들어 있습니다.
print("lr의 계수(weight 또는 cofficient) : {}".format(lr.coef_))
print("lr의 편향(offset 또는 intercept) : {}".format(lr.intercept_))
위의 결과에서 알 수 있는 사실 중 하나는 coef값은 NumPy 배열 형태이지만 intercept 값은 언제나 실수(float) 값 하나라는 사실입니다.
즉 coef 값은 각 특성 부여되는 가중치들의 배열이라는 사실을 알 수 있고, intercept 속성은 그 가중치에 대한 조정값이라는 점입니다.
wave 데이터 셋은 입력 특성이 하나밖에 없기 때문에 배열의 길이도 1이라는 사실을 알 수 있습니다.
단순히 선형 회귀(LinearRegression) 모델을 훈련시킨 후 점수를 확인해 보니 𝑅2 값이 66% 정도로 별로 좋지 않습니다. 또한 훈련 세트의 점수와 테스트 세트의 점수가 거의 비슷한 것을 확인할 수 있는데, 이는 과소적합인 상태를 의미합니다. 많이 생각할 필요 없이 우리가 테스트 용도로 사용한 wave 데이터 셋은 특성이 하나뿐인 1차원 데이터 셋이기 때문에 과대적합을 걱정하지 않아도 됩니다. 하지만 지금부터 사용해 볼 고차원 데이터 셋인 보스턴 주택 가격 데이터셋에 대해서는 선형 모델의 성능이 매우 높아지기 때문에 과대적합이 될 가능성이 있습니다!
X, y = mglearn.datasets.load_extended_boston()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state = 0)
lr = LinearRegression().fit(X_train, y_train)
print("훈련 세트 점수 : {:.2f}".format(lr.score(X_train, y_train)))
print("테스트 세트 점수 : {:.2f}".format(lr.score(X_test, y_test)))
테스트 결과 훈련 세트의 𝑅2는 95%지만, 테스트 세트의 점수는 61%입니다. 이는 모델이 과대적합되었다는 확실한 신호입니다. 따라서 우리는 모델의 일반화를 위해 복잡도를 제어할 수 있어야 하겠습니다. (각 특성별 가중치를 조절합니다.) 기본 선형 회귀 방식인 LinearRegression 모델로는 파라미터를 통해 복잡도를 제어 할 수 없으니, 여러 가지 회귀 모델을 알아보도록 하겠습니다.
릿지 Ridge 회귀
릿지(Ridge)도 회귀를 위한 선형 모델이므로 최소 제곱법 같은 예측 함수를 사용합니다. 릿지 회귀에서의 가중치(𝑤) 선택은 훈련 데이터를 잘 예측하기 위해서 뿐만 아니라 추가 제약 조건을 만족시키기 위한 목적도 있습니다. 즉 𝑤의 모든 원소가 0에 가깝게 만들어 가중치의 절댓값을 가능한 한 작게 한다는 뜻입니다. 결론적으로는 모든 특성이 예측(출력)에 주는 영향을 최소화하겠다는 뜻이 됩니다. 즉 𝑤에 의한 그래프의 기울기가 작아집니다. 이러한 제약 사향들을 규제(regularzation)이라고 합니다. 규제란 과대적합이 되지 않도록 모델을 강제로 제한한다는 뜻이 됩니다. 먼저 아무런 규제의 페널티가 없는 상황부터 살펴보겠습니다.
결과를 확인해 보니 훈련 세트에 대한 점수는 낮지만 테스트 세트에 대한 점수는 더 높네요. 선형 회귀(LinearRegression)는 보스턴 데이트 셋에 대해 과대적합 되지만 Ridge 회귀 방식은 더 자유로운 ( 특성에 제한이 걸리는 ) 모델이기 때문에 과대적합이 적어진다고 볼 수 있습니다.
따라서 모델의 적합도가 낮아졌다고 볼 수 있는데, 이렇게 모델의 복잡도가 낮아지면 훈련 세트에서의 성능은 나빠지지만 더욱 일반화된 모델이 됩니다.
우리는 테스트 세트에 대한 성능이기 때문에 보스턴 데이터셋에는 일반 선형 회귀보다는 Ridge 회귀가 어울리는 것 같네요
페널티
여기서 페널티에 대해 잠깐 이야기 하자면, 패널티란 예측 결과물에 대한 오차 범위 허용이라고 볼 수 있습니다. 이정도 오차는 괜찮아~ 라고 말 하는 것과 비슷합니다. 간단한 예로 택시의 예상 도착 시간을 예측 한다고 했을 때 예상 한 것 보다 1~2분 늦는 것은 별로가 문제가 되지 않으나, 10 ~ 20분 늦는 것은 예측이 매우 실패 했다 라고 판단 할 수 있습니다. 우리가 위에서 사용한 MSE(Mean Squad Error) 같은 공식이 예측값의 패널티에 대한 공식 같은 것이라고 판단하면 됩니다. MSE의 공식을 자세히 보면 오차 제곱( 타깃값 - 예측값의 제곱 )을 구하고 있는데, 이는 오차가 커지면 커질수록 더욱더 많은 페널티를 부여한다고 보면 됩니다. 보통 사용되는 다른 공식으로써 MAE(Mean Absolute Error)라는 것이 있습니다.
MSE와 다른 점은 MAE는 오차에 대한 페널티가 단순히 오차의 절대 값인 것을 확인할 수 있습니다. 이를 𝐿1 norm이라고 합니다. 즉 MSE는 오차가 커질수록 페널티가 커지고, MAE는 오차가 커질 수록 패널티가 일정 하다 라고 생각 해 볼 수 있겠습니다. 패널티를 계산하는 공식에는 RMSE, RMAE 등등의 여러가지 공식들이 있습니다.
Ridge 회귀에서의 패널티
Ridge는 모델을 단순하게 ( 계수 - 𝑤를 0에 가깝게 ) 해주고 훈련 세트와 테스트 세트 사이의 선능 사이를 절출 할 수 있는 방법을 제공합니다. 우리는 Ridge 회귀의 alpha 매개변수를 조절하여 모델의 성능을 단순화할 수 있습니다. 매개변수를 아무것도 넣지 않으면 alpha는 1.0을 기본적으로 사용하고 있었습니다. 참고로 alpha 값을 높이면 계수를 0으로 점점 가깝게 설정해 페널티의 효과가 커져 가중치가 차지하는 값이 0에 가까워져 모델이 단순해지고, alpha 값을 낮추면 점점 가중치가 높아져 모델이 복잡해집니다. Ridge 회귀를 사용할 때 최적화된 alpha 값은 사용하는 데이터셋에 따라 달라집니다. 바로 예를 들어 한번 보겠습니다. alpha 값을 10으로 조절한 경우입니다.
뭔가 굉장히 복잡해 보이네요, 먼저 X축은 coef_의 원소를 위치대로 나열한 것입니다. 즉 x=0은 첫 번째 특성, 약간 100을 넘는 값은 마지막 특성이라고 보면 될 것 같습니다.
y축은 계수의 수치를 나타내는데, alpha=10 일 때 ( 파란색 세모 ) 대부분의 계수는 -3부터 3 사이에 위치하는 것을 볼 수 있습니다. alpha=1 ( 주황색 네모 )의 계수의 크기는 조금 더 커져 가중치가 증가한 것이 확인됩니다. alpha=0.1 ( 초록색 세모 ) 같은 경우는 계수의 크기가 더 커져 가중치가 1일 때 보다 더 커진 것을 확인할 수 있고
alpha 값이 0인 ( 규제가 전혀 없는 ) LinearRegression 같은 경우는 그래프를 벗어나는 특성도 존재하는 것을 확인해 볼 수 있습니다.
Ridge에서 alpha 값은 고정되고, 데이터의 개수를 조절한다면?
규제의 효과를 확인해 보기 위해서 alpha를 1로 고정한 채로 훈련 데이터의 크기만 변화를 시켜 데이터 세트의 크기에 대해 훈련 효과를 확인해 보겠습니다.
보스턴 주택 가격 데이터세트에서 여러 가지 크기로 훈련시켜 LinearRegression과 Ridge(alpha=1)을 적용한 그래프를 확인 해 보겠습니다.
mglearn.plots.plot_ridge_n_samples()
릿지와 선형 회귀 모두 훈련 세트의 점수 ( 점선 )가 테스트 세트의 점수 ( 실선 ) 보다 높은 것을 확인할 수 있습니다.
여기서 살펴볼 수 있는 사실은 릿지 회귀에는 규제가 적용되기 때문에 훈련 데이터의 점수가 대체적으로 선형 회귀보다 낮은 것을 확인할 수 있습니다. 이는 데이터 세트의 개수가 적으면 적을수록 더 확연히 확인해 볼 수 있습니다.
데이터 크기가 400 미만에서는 선형 회귀는 무엇도 학습하지 못하고 있는 것이 확인됩니다.
즉 두 모델의 성능은 ( 테스트 데이터세트의 결과 ) 데이터가 많으면 많을수록 좋아지며, 마지막에는 선형 회귀가 릿지 회귀의 테스트 점수를 따라잡는 것이 확인됩니다.
정리하자면 데이터를 충분히 주게 되면 규제 자체는 덜 중요해져 릿지 회귀와 선형 회귀의 성능이 같아질 것이라는 것을 예상해 볼 수 있다는 점입니다.
또 하나 이상한 점은 주황색 점선 ( 훈련 데이터셋 점수 )가 점점 내려가는 것을 확인할 수 있는데, 이는 선형 회귀의 훈련 데이터셋에 대한 성능이 점점 감소하는 것을 의미합니다.
이는 데이터가 많아지면 많아질수록 모델이 데이터를 기억하거나 과대적합 하기 어려워진다는 것을 의미합니다.
라쏘 Lasso 회귀
라쏘 회귀 방식도 릿지와 비슷하게 계수에 규제를 걸어 계수를 0에 가깝게 만들기 위한 노력을 합니다. 릿지 회귀와의 차이점은 릿지 회귀는 L2 규제 방식을 사용하는데 비해 라쏘 회귀는 L1 규제 방식을 사용하며, 실제 어떤 계수는 실제로 0이 되기도 한다는 점입니다.
계수가 0이 되면 해당 특성은 전혀 상관이 없이 완전히 제외된다는 뜻이 됩니다.
어떻게 보면 라쏘 회귀는 특성 선택( feature selection )이 자동으로 이루어진다고도 볼 수 있습니다.
일부 계수를 0으로 만들면 모델을 이해하기가 쉬워지고 이 모델의 가장 중요한 특성이 무엇인지가 드러납니다.
확인 결과 라쏘는 훈련 세트와 테스트 세트의 점수가 전부 다 좋지 않습니다. 사용한 특성도 4개 정도밖에 안 되는 것으로 보아 과소적합이라고 생각할 수 있겠습니다.
릿지와 비슷하게 라쏘도 계수를 얼마나 강하게 보낼지를 조절하는 alpha 매개변수가 있습니다. 앞서 본 릿지에서는 기본적으로 alpha=1.0을 사용했었습니다.
과소적합을 줄이기 위해 alpha 값을 줄여보겠습니다. 하지만 이때 최대 반복 횟수를 의미하는 max_iter 값을 늘려 줘야 합니다.
max_iter는 내부적으로 Lasso의 학습 과정이 진행되는 최대 횟수를 의미합니다. 한 특성 씩 좌표축을 따라 최적화되는 좌표 하강법 방식을 사용하며 학습 과정이 max_iter에 지정된 횟수만큼 반복 횟수가 설정 되게 됩니다. lasso.niter를 이용해 반복 횟수를 확인할 수 있습니다.
동그라미 모양의 데이터들은 훈련 데이터입니다. 입니다. k-NN 모델 객체에 훈련시킨 데이터가 표시되어 있습니다.
별 모양의 데이터들은 테스트 데이터로써, 선을 이용해 각 훈련된 데이터 포인트에서 가장 가까운 이웃과 선이 연결되어 있습니다.
n_neighbors 파라미터의 값을 변경하면 가장 가까운 여러 개의 이웃이 선택되고, 둘 이상의 이웃이 선택되면 레이블( 분류 결과 - 그래프에서는 색상 )을 정하기 위해 투표를 합니다.
즉 테스트 포인트 하나에 대해서 클래스 0( 파란색 )에 속한 이웃이 몇 개인지, 클래스 1( 주황색 )에 속한 이웃이 몇 개인지를 셉니다. 그리고 이웃이 더 많은 클래스를 레이블로 지정합니다.
정리하자면 테스트 데이터에 대해 k-최근접 이웃 중 다수의 클래스가 레이블이 된다고 볼 수 있습니다. 이어서 3개의 최근접 이웃을 사용한 예를 보겠습니다.
mglearn.plots.plot_knn_classification(n_neighbors=3) # 이웃이 3개
이웃의 개수 ( n_neighbors 파라미터 )를 3개로 조절했더니 그래프 상에서는 별 모양의 테스트 데이터에 대해 3개의 훈련 데이터 포인트가 연결된 것을 확인할 수 있습니다.
또한 이웃을 한 개만 이용했을 때와, 세 개의 이웃을 사용했을 때 결과가 달라지는 것도 확인할 수 있습니다.
단순한 특성 1, 특성 2를 활용한 단순한 이진 분류 문제이지만, 우리가 k-NN 모델을 사용한다면 클래스가 다수인 데이터셋에도 같은 방법을 적용할 수 있습니다.
클래스가 여러 개일 때도 각 클래스에 속한 이웃이 몇 개인지를 헤아려 가장 많은 클래스를 예측값으로 사용합니다. 본격적으로 k-NN 알고리즘 적용에 대해 알아보겠습니다.
from sklearn.model_selection import train_test_split # 일반화 성능 평가를 위해 기존 데이터를 훈련 세트와 테스트 세트로 나눔
X, y = mglearn.datasets.make_forge()
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
모델을 만드는 방법은 간단합니다. k-NN 이웃 알고리즘은 scikit-learn에서 KNeighborsClassifier로 제공되고 있습니다
from sklearn.neighbors import KNeighborsClassifier #k-NN 분류 임포트
clf = KNeighborsClassifier(n_neighbors = 3) # 이웃의 갯수를 3개 갖는 모델 생성
이어서 훈련 세트를 사용해 KNeighborsClassifier 모델 객체( clf )를 훈련( fit ) 시켜 봅시다.
우리가 만든 모델의 정확도는 86%가 나왔습니다. 즉 모델이 테스트 데이터셋에 있는 샘플 중 86%를 정확히 예측한 것입니다.
이어서 이웃의 수에 따라서 k-NN 모델이 어떻게 데이터를 구분하는지 평면에 색을 칠해 경계를 나눠 보겠습니다.
이처럼 클래스 0과 클래스 1로 지정한 영역으로 나뉘는 경계를 결정 경계 (decision boundary)라고 합니다
fig, axes = plt.subplots(1, 5, figsize=(20,3))
for n_neighbors, ax in zip([1,3,6,9,26], axes):
# fit 메소드는 self 반환을 하기 때문에 객체 생성과 메소드를 한줄에 사용 할 수 있습니다.
clf = KNeighborsClassifier(n_neighbors=n_neighbors).fit(X, y)
mglearn.plots.plot_2d_separator(clf, X, fill=True, eps=0.5, ax=ax, alpha=.4)
mglearn.discrete_scatter(X[:,0], X[:,1],y, ax=ax)
ax.set_title("{} 이웃".format(n_neighbors))
ax.set_xlabel("특성 0")
ax.set_ylabel("특성 1")
axes[0].legend(loc=3)
<matplotlib.legend.Legend at 0x1 c14 acab38>
위의 그래프를 한번 분석해 보겠습니다.
이웃이 1개인 경우는 결정 경계가 훈련데이터에 가깝게 나눠지고 있습니다. 즉 모델의 복잡도가 증가했습니다. (과대적합 가능성)
이웃이 3개인 경우는 결정 경계가 1개일 때보다 약간 부드러워졌네요. 이는 모델의 복잡도가 감소한 것을 의미합니다.
이웃이 6개인 경우는 결정 경계가 클래스들이 위치한 것과 비슷한 구역을 각각 나눠 갖은것 같습니다.
이웃이 9개인 경우는 결정 경계가 6개 일 때 보다 훨씬 완만해집니다. 가운데쯤에 있는 특성 1 때문에 그런 것 같습니다. 모델이 점점 더 단순해지는 것 같네요( 과소 적합 가능성)
극단적으로 26개의 이웃을 가진 경우는 모든 데이터를 하나로 분석해 버립니다. k-nn은 이웃의 개수로 투표를 하기 때문인 것 같습니다.
위의 분석 내용과 그래프로 알 수 있는 사실은 모델의 개수가 너무 적으면 복잡도가 증가해 과대적합 가능성이 생겨버리고, 모델의 개수가 많으면 복잡도가 감소해 과소 적합 가능성이 생긴 다는 것을 알 수 있습니다.
유방암 데이터를 이용해 k-NN 알아보기
모델의 복잡도와 일반화 사이의 관계를 입증해 보도록 하겠습니다. 이웃의 개수에 따라 얼마나 k-NN이 잘 일반화되는지를 알아보겠습니다.
먼저 유방암 데이터에 대해 훈련 세트와 테스트 세트를 준비해 보겠습니다. 그러고 나서 이웃의 개수를 따로 하여 얼마나 정확도가 증가하고 알맞은 모델이 되는지 평가해 보겠습니다.
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer() #유방암 데이터 불러오기
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target,
stratify=cancer.target, random_state=66) # 테스트 세트와 훈련 세트 분할
training_accuracy = [] # 각 이웃 개수 별 훈련 세트에 대한 정확도를 저장할 리스트
test_accuracy = [] # 각 이웃 개수 별 테스트 세트에 대한 정확도를 저장할 리스트
#이웃의 개수 설정 (1개 ~ 10개 까지)
neighbors_settings = range(1, 11)
for n_neighbors in neighbors_settings:
#모델 생성하기
clf = KNeighborsClassifier(n_neighbors=n_neighbors)
clf.fit(X_train, y_train)
#훈련 세트 정확도 저장
training_accuracy.append(clf.score(X_train, y_train))
#일반화 정확도 저장
test_accuracy.append(clf.score(X_test, y_test))
plt.plot(neighbors_settings, training_accuracy, label="훈련 정확도") # 이웃 개수에 대한 훈련 세트 정확도 선 그래프 그리기
plt.plot(neighbors_settings, test_accuracy, label="테스트 정확도") # 이웃 개수에 대한 테스트 세트 정확도 선 그래프 그리기
plt.ylabel("정확도")
plt.xlabel("n_neighbors")
plt.legend()
<matplotlib.legend.Legend at 0x102 cdf5 f8>
그래프를 보면 과대적합과 과소적합의 특성을 알 수 있습니다.
이웃의 개수가 적으면 훈련세트의 정확도는 높지만 테스트 세트의 정확도는 낮고 - 과대적합
이웃의 개수가 많아지면 모델이 단순해지면서 훈련 정확도와 테스트 정확도가 동시에 낮아집니다. - 과소적합
여기서 알 수 있는 사실은 이웃이 1개인 경우에는 너무 모델을 복잡하게 만들어 낸다는 사실을 알 수 있네요, 반대로 이웃을 10개 사용했을 때는 모델이 너무 단순해서 정확도가 더욱더 나빠진다는 사실을 알 수 있게 됩니다.
가장 좋은 이웃은 몇 개일까요?테스트 세트의 정확도가 가장 높은 6개 정도의 이웃을 사용했을 때 가장 일반화가 잘 됐다라고 할 수 있겠습니다.
K-NN Regression
k-NN 알고리즘은 회귀 분석을 위해서도 사용합니다. wave 데이터셋을 이용해 알아보도록 하죠.
분류와 마찬가지로 mglearn 패키지에 wave 데이터 셋을 이용한 회귀 시각화 그래프가 이미 존재합니다. 먼저 이웃이 한 개인 경우의 회귀 결과입니다.
mglearn.plots.plot_knn_regression(n_neighbors=1)
먼저 파란색 원은 트레이닝 데이터, 초록색 별은 입력한 특성, 마지막으로 파란색 별은 입력한 특성에 대한 회귀 분석 결과입니다.
k-NN 회귀는 이웃의 개수를 다수로 지정하면 이웃 간의 평균이 그 예측값이 됩니다.
mglearn.plots.plot_knn_regression(n_neighbors=3)
이웃의 개수가 한 개일 때는 단순하게 제일 가까운 이웃만을 따져서 파란색 별의 위치가 결정되었지만, 이웃의 개수가 3개가 되면 가장 가까운 3개의 점을 찾아 그 평균을 회귀 결괏값으로 사용하는 것을 알 수 있습니다.
k-NN 회귀는 k-NN 분류와 비슷하게 사용할 수 있습니다. KNeighborsRegressor에 구현되어 있습니다.
from sklearn.neighbors import KNeighborsRegressor # k-NN 회귀를 위한 KNeighborsRegressor 임포트
X, y = mglearn.datasets.make_wave(n_samples=40)
#wave 데이터셋을 훈련 세트와 테스트 세트로 나누기
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
# 이웃의 수를 3으로 하여 모델 객체 생성하기
reg = KNeighborsRegressor(n_neighbors = 3)
# 훈련 데이터와 타깃을 사용하여 모델 학습 시키기
reg.fit(X_train, y_train)
print("테스트 세트 예측 :\n{}".format(reg.predict(X_test)))
이 역시 score 메소드를 이용해 점수를 알 수 있는데요, score 메소드는 회귀분석일 때는 $R^2$ 값을 반환합니다.
여기서 $R^2$ 값은 결정 계수 라고 하며 회귀 모델에서의 예측의 적합도를 0 ~ 1 사이의 값으로 계산한 것입니다. 공식은 다음과 같습니다. \begin {align} \\R^2=1-\frac {\sum{({y-\hat {y})^2}}}{\sum{({y-\bar {y})^2}}} \end {align}
의미하는 바는 다음과 같습니다. $y$ 는 타깃값이고, $\bar {y}$는 타깃값($y$ 들)의 평균이며, $\hat {y}$는 모델의 예측값입니다.
즉 $y$는 우리가 맞춰야 할 값( 훈련된 데이터 ), $\bar {y}$는 훈련된 데이터들의 평균값, $\hat {y}$는 우리가 만든 모델이 테스트 데이터에 대해 예측한 값입니다.
따라서 훈련데이터들(X_train)의 타깃값으로 사용한 y_train을 예측 데이터로 사용하게 되면 $R^2$의 공식에서 $\hat {y}$이 $\bar {y}$와 같게 되고, 분자와 분모가 같아져 $R^2$값이 0이 되게 됩니다.
1은 예측이 완벽한 경우, 0은 훈련 세트의 출력값인 y_train의 평균으로만 예측하는 모델의 경우입니다.
wave 데이터셋을 계속 이용하여 이웃의 개수(n_neighbors)에 따른 테스트 세트의 성능을 판단해 보겠습니다.
fig, axes = plt.subplots(1,4, figsize=(20,4))
#-3과 3 사이에 1000개의 데이터 만들기 -> 테스트 용도로 사용함
line = np.linspace(-3, 3, 1000).reshape(-1, 1)
for n_neighbors, ax in zip([1,3,5,9], axes):
# 이웃의 개수를 1,3,9로 하여 예측하는 모델을 만듭니다
reg = KNeighborsRegressor(n_neighbors=n_neighbors)
reg.fit(X_train, y_train)
ax.plot(line, reg.predict(line)) # 테스트 용도로 만든 데이터를 예측하고 예측 결과를 선으로 표현합니다.
ax.plot(X_train, y_train, '^', c=mglearn.cm2(0), markersize=8) # 훈련 데이터를 그래프에 표시합니다.
ax.plot(X_test, y_test, 'v', c=mglearn.cm2(1), markersize=8) # 테스트 데이터를 그래프에 표시합니다.
# 훈련 데이터의 점수와 테스트 데이터의 점수를 제목에 표현합니다.
ax.set_title("{} 이웃의 훈련 스코어: {:.2f} / 테스트 스코어 : {:.2f}".format(n_neighbors, reg.score(X_train, y_train), reg.score(X_test, y_test)))
ax.set_xlabel("특성")
ax.set_ylabel("타깃")
axes[0].legend(["모델 예측", "훈련 데이터/타깃", "테스트 데이터/타깃"], loc="best")
<matplotlib.legend.Legend at 0x1 c1744 d588>
첫 번째 그래프부터 확인해 보겠습니다.
이웃의 개수가 1개인 첫 번째 그래프는 모델을 예측한 라인들이 모두 훈련 데이터를 지나가는 것을 확인할 수 있습니다. 이는 훈련 세트의 각 데이터 포인트들이 예측에 주는 영향이 커서 매우 불안정한 예측이라고 할 수 있습니다.
이에 반에 이웃의 개수가 많아질수록 훈련 스코어가 점점 줄어드는 것을 볼 수 있는데 이는 이웃을 많이 사용할수록 훈련 데이터에는 잘 맞지 않을 수 있지만, 더 안정적으로 예측을 할 수 있게 됩니다.
이웃의 개수가 3개 일 때는 적절히 훈련 데이터와 테스트 데이터를 지나가는 모습을 볼 수 있으며, 그 이상의 이웃이 설정될수록 예측 라인들이 훈련데이터를 빗겨 지나가는 모습을 확인할 수 있습니다. (정확도가 점점 떨어집니다.)
장단점과 매개변수
우리는 지금까지 한 개의 매개변수(n_neighbors)만 사용해서 이웃의 개수를 조절하면서 모델의 성능을 체크해 보았습니다. 보통이면 두 개의 매개변수를 활용하는데, 다른 하나는 거리 조절 공식을 따로 준비하는 것입니다.
기본적으로 k-NN은 metic 매개변수를 활용하여 거리 측정 방식을 변경할 수 있으며, 기본값은 민코프스키 거리를 의미하는 'minkowski'가 설정되어 있습니다.
이 민코프스키 거리를 제곱하여 크기를 정하는 p의 기본값이 2일 때 k-NN의 기본 거리 측정 공식인 유클라디안 거리와 같게 됩니다.
k-NN의 장점은 이해하기가 매우 쉬운 모델이라는 점입니다. 또한 많이 조정할 필요 없이 좋은 성능을 발휘하여 k-NN 보다 더 복잡한 알고리즘을 적용하기 전에 테스트해볼 수 있는 좋은 시작점이 됩니다.
단점은 모델 자체는 매우 빠르게 만들어 볼 수 있지만, 훈련 세트가 매우 크면( 특성 또는 샘플 데이터의 개수가 많으면 ) 예측이 느려지게 됩니다. 또한 수백 개의 특성을 가진 데이터 셋에는 잘 동작하지 않고, 희소한 데이터셋 ( 특성값의 대부분이 0인 ) 데이터셋과는 특히 잘 작동할 수 없습니다.
위의 단점에 따라 k-NN은 예측이 느리고 많은 특성을 처리하는 능력이 부족하기에 현업에서는 잘 사용하지 않습니다.
사용 데이터는 scikit-learn 라이브러리에 있는 연습용 데모 데이터로써, 붓꽃의 품종을 꽃잎과 꽃받침의 크기에 따라서 분류해 놓은 데이터셋을 활용합니다.
가설 세우기
분석에 앞서 우리는 어떻게 데이터를 분석할지 가설을 세우는 것이 매우 중요합니다.
자, 여러분이 들판에서 붓꽃을 하나 발견했다고 가정합시다. 여러분들은 붓꽃에 대해서 잘 알지 못하기 때문에, 전문 식물학자가 측정한 데이터셋(우리가 활용할 데모 데이터셋)을 활용해야 합니다. 전문 식물학자가 측정해서 결과까지 내어 놓은 데이터셋은 데이터와 데이터에 따른 결과가 있을 것입니다.
붓꽃의 품종은 보통 setosa, versicolor, virgincia 이렇게 세 분류로 되어 있습니다. 예를 들어 꽃받침의 길이가 Xcm이고, 꽃잎의 길이가 Ycm 라면 virgincia라고 말한 것을 의미합니다.
문제 해결 하기
이러한 문제를 해결하기 위해서는 어떻게 해야 할까요?
여러분은 붓꽃의 품종 자체는 잘 알지 못하지만, 전문 식물학자가 작성해 놓은 데이터셋처럼 꽃받침, 꽃잎의 크기는 구할 수 있겠네요! 즉, 여러분이 채집한 붓꽃의 꽃받침, 꽃잎의 크기 ( 특성데이터 - feature )를 사용해 어떤 품종인지( 예측결과 - label )를 예측해야 합니다.
학습과 분류의 종류
여기서 우리가 사용할 데이터셋에는 품종을 정확하게 분류한 데이터를 가지고 있습니다. 따라서 지도 학습이라고 이야기할 수 있겠습니다.
몇 가지 선택사항( 품종 ) 중 하나를 선택해야 하는 문제입니다. 따라서 지금 예제는 분류(Classification)에 해당합니다. 여기서 출력될 수 있는 값들(여기서는 setosa, versicolor, virgincia)을 클래스(class)라고 합니다. 즉 측정한 데이터( feature )를 이용해 세 개의 클래스를 분류해야 한다고 볼 수 있겠습니다.
붓꽃 하나에 대한 기대 출력은 그 꽃의 품종이 됩니다. 이런 특정 데이터 포인트에 대해 기대할 수 있는 출력( 여러분이 채집한 꽃에 대한 품종 )을 레이블( label )이라고 합니다.
데이터를 통하여 한번 알아보도록 하겠습니다!
데이터 적재하기
scikit-learn의 datasets 모듈에 있는 iris 데이터를 불러와 봅시다.
from sklearn.datasets import load_iris
iris_dataset = load_iris() # python의 dict 클래스와 유사한 Bunch 클래스의 객체
# print(iris_dataset)<- 출력이 되는지 확인하기
print('iris_dataset의 키 : \n{}'.format(iris_dataset.keys())) # iris 데이터셋의 키 확인하기
# iris 데이터셋의 DESCR 키에는 간략한 설명이 들어있습니다.
print(iris_dataset['DESCR'][:193], "\n...")
# target_names 키는 우리가 예측할 붓꽃 품종의 이름을 문자열 배열로 가지고 있습니다.
print("타깃의 이름 : {}".format(iris_dataset['target_names']))
# feature_names 키는 각 특성을 설명하는 문자열 리스트 입니다.
print('특성의 이름 : {}'.format(iris_dataset['feature_names']))
# 실제 데이터는 target과 data 필드에 들어 있습니다.
# data는 feature 로써, 꽃받침과 꽃잎의 길이의 값이 들어 있습니다.
print('data의 타입 : {}'.format(type(iris_dataset['data'])))
# data 배열의 행은 각각 꽃에 대한 데이터입니다. 즉 4개의 측정치(꽃받침의 너비와 길이, 꽃잎의 너비와 길이)
print('data의 크기 : {}'.format(iris_dataset['data'].shape))
# data 배열의 실제 데이터를 확인 합니다. 5개의 데이터만 확인 해봅니다.
print('data의 처음 다섯 행 :\n{}'.format(iris_dataset['data'][:5]))
# target 배열은 붓꽃의 품종이 담겨 있습니다.
print('target의 타입 : {}'.format(type(iris_dataset['target'])))
print('target의 크기 : {}'.format(iris_dataset['target'].shape))
# 붓꽃의 종류는 0, 1, 2 형태의 정수로 들어 있습니다.
print('target:\n{}'.format(iris_dataset['target'])) # iris['target_names']에서 숫자의 의미를 파악 할 수 있습니다.
print('target mean : {}'.format(iris_dataset['target_names'])) # 0은 setosa, 1은 versicolor, 2는 virgincia 입니다.
신뢰할 수 있는 모델과 예측인지 확인하기
위의 데이터를 토대로 머신러닝 모델을 만들어서 위에 데이터셋에는 없는 새로운 데이터를 이용해 적용하기 전에 모델이 잘 작동하는지 검증하는 작업을 해봐야 합니다. 즉, 위의 iris_dataset이 신뢰할 수 있는 데이터 인지를 확인해봐야 하겠죠.
그렇다면 위의 데이터는 평가의 목적으로 사용할 수 있을까요? 애석하지만 위의 데이터는 평가용이 아닌 학습용도로 만들어 놓은 데이터 이기 때문에 iris_datasets의 데이터는 새로운 데이터로써 사용할 수 없는 상태입니다. 즉, 이미 위의 iris_datasets는 훈련에 의해 컴퓨터가 이미 다 알고 있는 데이터 이기 때문에 모든 데이터를 정확하게 맞출 수 있기 때문이죠. 이처럼 이미 컴퓨터가 데이터를 기억한다는 것은 모델의일반화가 잘 이루어져 있지 않았다( 새로운 데이터에 대해서는 잘 작동하지 않는다)라고 이야기합니다.
모델의 성능을 측정하기 위해서는( 정확도를 구하기 위해서는 ) 새로운 데이터가 필요합니다. 새로운 데이터를 당장에 만들어 내는 것은 조금은 번거롭고 어려운 작업입니다.
측정을 잘할 수 있는 성능테스트 하기
자, 우리가 만들 모델을 측정하기 위해 가장 간단한 방법은 무엇일까요? 바로 위의 데이터셋을 두 그룹으로 적절하게 나누는 것입니다.
첫 번째 세트는 머신러닝 모델을 만들 때 사용 합니다. 이를훈련 데이터 또는 훈련 세트라고 합니다.
두 번째 세트는 첫 번째 세트로 만들어낸 머신러닝 모델의 테스트 용도로써 사용합니다. 이를테스트 데이터, 테스트 세트 또는 홀드아웃 세트라고 합니다.
scikit-learn은 데이터셋을 적절하게 섞어서 나눠주는 train_test_split 함수를 제공하고 있습니다. 이 함수의 역할은 전체 행 중 75%가량을 레이블 데이터( feature에 의해 맞춰야 할 결과 값)와 함께 훈련용 데이터( feature )로 뽑습니다. 나머지 25% 정도의 데이터셋은 테스트 세트가 됩니다. 물론 상황에 따라 조절할 수 있습니다. ( test_size 매개변수 활용 )
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris_dataset['data'], iris_dataset['target'], random_state = 0)
print('X_train의 크기 : {}'.format(X_train.shape))
print('y_train의 크기 : {}'.format(y_train.shape))
train_test_split을 활용하여 데이터를 나눌 때는 랜덤 하게 나눠져야 합니다. iris_dataset ['target']을 이용해 확인해봤지만, 맨 뒤에 있는 데이터는 전부다 레이블이 2로 동일합니다.
세 클래스 중 하나만 포함한 테스트 세트를 사용하면 일반화가 잘 이루어지지 않겠죠. 따라서 테스트 세트를 적절하게 잘 섞어줄 수 있도록 난수(random)를 활용합니다.
기본적으로 train_test_split 함수에서 랜덤으로 셔플링(shuffle)을 해주지만, 실행할 때마다 실행 결과가 달라질 수 있기 때문에 seed의 개념을 이용해 랜덤값을 고정시킬 수 있습니다.( random_state = 0 또는 아무거나 들어가도 됩니다!)
print('X_test 크기 : {}'.format(X_test.shape))
print('y_test 크기 : {}'.format(y_test.shape))
데이터 시각화로 살펴보기
train_test_split과 같은 메소드를 이용해서 훈련용(X_train, y_train) 데이터와 테스트용(X_test, y_test) 데이터를 나눠 보았습니다. 실제 훈련과 테스트를 해보기 이전에 신뢰할 수 있는 데이터 인지 시각화를 통해 살펴보겠습니다.
이를 통해 우리는 비정상적인 값이나 특이한 값들을 눈으로 확인해 볼 수 있을 것입니다. pandas 도표 또는 jupyter notebook에서 문자로 보는 것보다 도표를 이용해 보는 것 이기 때문에 한눈에 보기도 쉽고 간편할 것입니다.
어떻게 시각화를 할까?
여러 종류의 데이터가 분포되어 있고, 여기에 대해서 결과물이 나오고 있는 형태입니다. 이때 우리가 생각해 볼 수 있는 내용은산점도(Scatter plot)를 활용한 방법이 좋습니다.
한 특성을 x축에 놓고, 다른 하나는 y축에 놓아 각 데이터 포인트를 하나의 점으로 나타내는 그래프입니다. 하지만 우리가 사용하고 있는 특성은 총 4가지(꽃받침, 꽃잎의 가로세로) 이기 때문에 하나의 그래프에 확인하기가 힘듭니다. 따라서산점도 행렬(Scatter matrix)을 이용해 확인해 보도록 하겠습니다.
먼저 그래프를 그리기 전에 numpy로 만들어 놓은 배열을 Pandas의 DataFrame으로 변경해야 합니다.
import matplotlib.pyplot as plt
import pandas as pd # pandas 모듈 임포트하기
import mglearn
%matplotlib inline
# 훈련용 데이터인 X_train 데이터를 이용해 데이터 프레임을 만들어 줍니다.
# y축이 될 열의 이름은 iris_datase.feature_names에 있는 문자열을 사용하겠습니다.
iris_dataframe = pd.DataFrame(X_train, columns=iris_dataset.feature_names)
pd.plotting.scatter_matrix(iris_dataframe, c=y_train, figsize = (15, 15), marker='o',
hist_kwds={'bins':20}, s = 60, alpha = .8, cmap=mglearn.cm3)
plt.show()
k-최근접 이웃 알고리즘
k-NN( k-Nearest Neighbors - k-최근접 이웃 ) 알고리즘은 새로운 데이터에 대한 예측이 필요할 때새로운 데이터와 가장 가까운k개의 훈련된 데이터 포인트들을 찾아냅니다. 즉 k는 하나의 이웃이 아닌 복수의 이웃이라는 것이죠.
이 이웃들의 클래스 중에서빈도가 가장 높은 클래스를 예측값으로 사용하게 됩니다.
덧붙여서 scikit-learn의 머신러닝 모델은 BaseEstimator라는 파이썬 클래스를 상속받아 구현되어 있습니다.
KNeighborsClassifier 클래스를 사용할 텐데, 이때 n_neighbors 매개변수는 이웃의 개수를 지정해 줍니다. 일단 가장 가까운 이웃만 하나 골라내기 위해서 n_neighbors를 1로 지정하겠습니다.
from sklearn.neighbors import KNeighborsClassifier #KNN 분류기
knn = KNeighborsClassifier(n_neighbors=1) # 이웃의 갯수를 1개로 강제 지정
위에서 만든 knn 객체는 훈련 데이터로 모델을 만들고 새로운 데이터 포인트에 대해 예측하는 알고리즘을 캡슐화하였습니다.
또한 알고리즘이 훈련 데이터로부터 추출한 정보까지 담아내고 있습니다. KNeighborsClassifier의 경우는 훈련 데이터 자체를 저장하고 있습니다.
훈련시키기 Fit
fit 메소드를 활용하면 드디어 모델을 만들어 낼 수 있습니다.
이때 훈련해야 할 데이터는 위에서 만들어 놓은 X_train과 y_train이 되어야겠죠?
knn.fit(X_train, y_train)
fit 메소드를 활용하면 머신러닝 알고리즘 객체 자체가 반환이 됩니다.
knn 객체를 생성할 때 n_neighbors 매개변수를 1로 설정하였는데, 결과를 보니 잘 된 것 같습니다.
매개변수의 종류가 상당히 많은데, 다음 포스트에서 바로 다뤄 보도록 하겠습니다!
데이터 신뢰도를 위한 예측
시각화를 통해 신뢰할 수 있는 데이터 인지 (각 데이터에 따라서 붓꽃의 종류가 잘 구분되었는지) 알아보았고, 알맞은 데이터라는 판단이 들어서 fit 메소드를 사용해 모델을 만들어 냈습니다.
이제 본격적으로 예측을 해볼 텐데요, 먼저 여러분이 수집한 붓꽃에 대한 데이터를 임의로 지정해 보도록 하겠습니다.
X_new 객체는 여러분이 수집한 붓꽃의 데이터가 들어있는 Numpy 배열입니다.
import numpy as np
X_new = np.array([[5,2.9,1,0.2]])
print("X_new.shape : {}".format(X_new.shape))
데이터가 잘 준비되었으면 이제 예측해 봅시다. 예측할 때는 predict 메소드를 사용합니다.
prediction = knn.predict(X_new) # 예측할 데이터인 X_new의 결과를 prediction으로 저장
print("예측: {}".format(prediction))
print("예측한 타깃의 이름: {}".format(iris_dataset['target_names'][prediction]))
잘 작동은 한 것 같습니다. 하지만 이 결과를 과연 신뢰할 수 있나요? 이때 필요한 것이 모델을 평가하는 것입니다.
앞서 X_train, y_train 말고도 X_test, y_test 데이터셋을 따로 만들어어 놓았습니다. 총데이터셋의 크기 중 25% 정도를 차지하는 이 데이터 셋들은 테스트, 즉 평가의 용도로 사용하게 됩니다.
여기에는 4가지 데이터와 정답이라고 할 수 있는 붓꽃의 품종까지 들어 있습니다. 우리는 이제 훈련한 머신러닝 모델과 정답이 들어있는 테스트 데이터를 이용해 예측하고, 얼마만큼 우리가 만든 모델이 붓꽃의 품종을 맞췄는지 알아볼 수 있습니다.
즉, 정확도를 계산하여 모델의 성능을 평가해 낼 수 있습니다.
y_pred = knn.predict(X_test) # 테스트 데이터셋을 넣어서 예측 하기
print("테스트 세트에 대한 예측값:\n {}".format(y_pred))
print('테스트 세트의 정확도: {:.2f}'.format(np.mean(y_pred == y_test))) # 정답이 들어있는 y_test와 knn 모델이 에측한 y_pred의 일치도
97퍼센트의 정답률이 나왔습니다. 꽤나 정확하게 붓꽃들을 구분했다고 볼 수 있습니다. 보통 정확도는 70% 이상이면 적합한 성능에 포함합니다. 물론, 높으면 높을수록 분석에 높은 확률을 주는 건 당연한 이야기겠죠~
앞으로 우리는 이렇게 신뢰할 수 있는 머신러닝 모델을 만들기 위해 성능을 높이는 방법과 모델을 튜닝할 때 주의할 점들을 알아봐야 합니다.