by museonghwang

경사하강법(Gradient Descent)을 이용한 비용(Cost) 최소화

|

경사하강법(Gradient Descent)

경사하강법 은 ‘데이터를 기반으로 알고리즘이 스스로 학습한다’는 머신러닝의 개념을 가능하게 만들어준 핵심 기법으로, '점진적으로' 반복적인 계산을 통해 $W$ 파라미터 값을 업데이트하면서 오류 값이 최소가 되는 $W$ 파라미터를 구하는 방식 입니다.

image


눈을 가린채 산 정상에서 아래로 내려간다고 가정했을때, 발을 뻗어서 현재 위치보다 무조건 낮은 곳으로 계속 이동하다 보면 마침내 지상에 도착할 수 있을 것입니다. 어떻게 보면 무식해 보이는 방법이지만 직관적이고 빠르게 비용 함수가 최소가 되는 $W$ 파라미터 값을 구할 수 있습니다.

경사하강법은 반복적으로 비용 함수의 반환 값, 즉 예측값과 실제 값의 차이가 작아지는 방향성을 가지고 $W$ 파라미터를 지속해서 보정(오류를 감소시키는 방향으로 $W$ 값을 계속 업데이트)해 나갑니다. 그리고 오류 값이 더 이상 작아지지 않으면 그 오류 값을 최소 비용으로 판단하고 그때의 $W$ 값을 최적 파라미터로 반환합니다.

경사하강법의 핵심“어떻게 하면 오류가 작아지는 방향으로 W 값을 보정할 수 있을까?” 입니다. 예를 들어 비용 함수가 다음 그림과 같은 포물선 형태의 2차 함수라면 경사하강법은 최초 $w$ 에서부터 미분을 적용한 뒤 이 미분 값이 계속 감소하는 방향으로 순차적으로 $w$ 를 업데이트합니다. 마침내 더 이상 미분된 1차 함수의 기울기가 감소하지 않는 지점을 비용 함수가 최소인 지점으로 간주 하고 그때의 $w$ 를 반환합니다.

image


$RSS(w)$ 는 변수가 $w$ 파라미터로 이뤄진 함수로 다음과 같이 작성할 수 있습니다.


[RSS(w)=\frac{1}{N}\sum^N_{i=1}(y_i-(w_0+w_1*x_i))^2]


$RSS(w)$ 를 미분해서 미분 함수의 최소값을 구하기 위해 $w_0, w_1$ 각 변수에 편미분을 적용해야 합니다. $RSS(w)$ 를 최소화하는 $w_0$ 와 $w_1$ 의 값은 각각 $RSS(w)$ 를 $w_0$, $w_1$ 으로 순차적으로 편미분을 수행해 얻을 수 있습니다.


[\frac{\partial RSS(w)}{\partial w_1} = \frac{2}{N}\sum^N_{i=1}-x_i(y_i-(w_0 + w_1x_i)) = -\frac{2}{N}\sum^N_{i=1}x_i(실제값_i-예측값_i)]

[\frac{\partial RSS(w)}{\partial w_0} = \frac{2}{N}\sum^N_{i=1}-(y_i-(w_0 + w_1x_i)) = -\frac{2}{N}\sum^N_{i=1}(실제값_i-예측값_i)]


$w_1, w_0$ 의 편미분 결과값을 반복적으로 보정하면서 $w_1, w_0$ 값을 업데이트하면 비용 함수 $RSS(w)$ 가 최소가 되는 $w_1, w_0$ 의 값을 구할 수 있습니다. 업데이트는 새로운 $w_1$ 을 이전 $w_1$ 에서 편미분 결과값을 마이너스($-$)하면서 적용 합니다. 또한 편미분 값이 너무 클 수 있기 때문에 보정 계수 $\eta$ 를 곱하는데, 이를 ‘학습률’ 이라고 합니다.


[w_1^* = w_1 - \eta \left( - \frac{2}{N}\sum^N_{i=1}x_i(실제값_i-예측값_i) \right) \ = w_1 + \eta \frac{2}{N}\sum^N_{i=1}x_i(실제값_i-예측값_i)]

[w_0^* = w_0 + \eta \frac{2}{N}\sum^N_{i=1}(실제값_i-예측값_i)]


요약하자면, 경사하강법은 $w_0, w_1$ 를 임의의 값으로 설정하고 첫 비용 함수의 값을 계산한 뒤, 새로운 $w_1$, 새로운 $w_0$ 을 반복적으로 적용하면서 비용 함수가 최소가 되는 값을 찾습니다.

지금까지 정리한 수식과 절차를 이용해 경사하강법을 구현하겠습니다. 간단한 회귀식인 y = 4X + 6 을 근사하기 위한 100개의 데이터 세트를 만들고, 여기에 경사하강법을 이용해 회귀계수 $w_0, w_1$ 을 도출하겠습니다.

import numpy as np
np.random.seed(0)
import matplotlib.pyplot as plt
%matplotlib inline

# y = 4X + 6 식을 근사(w1=4, w0=6). random 값은 Noise를 위해 만듬
X = 2 * np.random.rand(100,1)
y = 4*X + 6 + np.random.randn(100,1)

# X, y 데이터 셋 scatter plot으로 시각화
plt.scatter(X, y)

image


데이터는 y = 4X + 6 을 중심으로 무작위로 퍼져 있습니다. 다음으로 비용 함수를 정의하겠습니다. 비용 함수 get_cost() 는 실제 y 값과 예측된 y 값을 인자로 받아서 $\frac{1}{N}\sum^N_{i=1}x_i*(실제값_i-예측값_i)^2$ 을 계산해 반환합니다.

def get_cost(y, y_pred):
    N = len(y) 
    cost = np.sum(np.square(y - y_pred))/N
    return cost


$w_0$ 과 $w_1$ 의 값을 최소화 할 수 있도록 업데이트를 수행하는 get_weight_update() 함수를 생성하겠습니다. 예측 배열 y_prednp.dot(X, w1.T) + w0 으로 구합니다. 100개의 데이터 X(1,2,…,100) 이 있다면 예측값은 w0 + X(1)w1 + X(2)w1 +..+ X(100)*w1 이며, 이는 입력 배열 Xw1 배열의 내적과 동일합니다. 따라서 넘파이의 내적 연산인 dot() 를 이용해 예측 배열값을 계산합니다.

# w1 과 w0 를 업데이트 할 w1_update, w0_update를 반환. 
def get_weight_updates(w1, w0, X, y, learning_rate=0.01):
    N = len(y)
    
    # 먼저 w1_update, w0_update를 각각 w1, w0의 shape와 동일한 크기를 가진 0 값으로 초기화
    w1_update = np.zeros_like(w1)
    w0_update = np.zeros_like(w0)
    
    # 예측 배열 계산하고 예측과 실제 값의 차이 계산
    y_pred = np.dot(X, w1.T) + w0
    diff = y-y_pred
         
    # w0_update를 dot 행렬 연산으로 구하기 위해 모두 1값을 가진 행렬 생성 
    w0_factors = np.ones((N,1))

    # w1과 w0을 업데이트할 w1_update와 w0_update 계산
    w1_update = -(2/N)*learning_rate*(np.dot(X.T, diff))
    w0_update = -(2/N)*learning_rate*(np.dot(w0_factors.T, diff))    
    
    return w1_update, w0_update


다음은 get_weight_updates() 을 경사하강 방식으로 반복적으로 수행하여 w1w0 를 업데이트하는 함수인 gradient_descent_steps() 함수를 생성하겠습니다.

# 입력 인자 iters로 주어진 횟수만큼 반복적으로 w1과 w0를 업데이트 적용함. 
def gradient_descent_steps(X, y, iters=10000):
    # w0와 w1을 모두 0으로 초기화. 
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    
    # 인자로 주어진 iters 만큼 반복적으로 get_weight_updates() 호출하여 w1, w0 업데이트 수행. 
    for ind in range(iters):
        w1_update, w0_update = get_weight_updates(w1, w0, X, y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update

    return w1, w0


이제 gradient_descent_steps() 를 호출해 w1w0 을 구해보겠습니다. 그리고 최종적으로 예측값과 실제값의 RSS 차이를 계산하는 get_cost() 함수를 이용해 경사하강법의 예측 오류도 계산해 보겠습니다.

w1, w0 = gradient_descent_steps(X, y, iters=1000)
print("w1:{0:.3f} w0:{1:.3f}".format(w1[0,0], w0[0,0]))

y_pred = w1*X + w0
print('Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
[output]
w1:4.022 w0:6.162
Gradient Descent Total Cost:0.9935


실제 선형식인 y = 4X + 6 과 유사하게 w1 은 4.022, w0 는 6.162가 도출되었습니다. 예측 오류 비용은 약 0.9935입니다. 앞에서 구한 y_pred 에 기반해 회귀선을 그려 보겠습니다.

plt.scatter(X, y)
plt.plot(X,y_pred)

image


경사 하강법을 이용해 회귀선이 잘 만들어졌음을 알 수 있습니다. 일반적으로 경사 하강법은 모든 학습 데이터에 대해 반복적으로 비용함수 최소화를 위한 값을 업데이트하기 때문에 수행 시간이 매우 오래 걸린다는 단점 이 있습니다. 그 때문에 실전에서는 대부분 확률적 경사하강법(Stochastic Gradient Descent) 을 이용합니다.

확률적 경사하강법은 전체 입력 데이터로 $w$ 가 업데이트되는 값을 계산하는 것이 아니라 일부 데이터만 이용해 $w$ 가 업데이트되는 값을 계산하므로 경사 하강법에 비해서 빠른 속도를 보장 합니다.

확률적 경사하강법을 stochastic_gradient_descent_steps() 함수로 구현하겠습니다. gradient_descent_steps() 와 달리, 전체 X, y 데이터에서 랜덤하게 batch_size 만큼 데이터를 추출해 이를 기반으로 w1_update, w0_update 를 계산하는 부분만 차이가 있습니다.

def stochastic_gradient_descent_steps(X, y, batch_size=10, iters=1000):
    w0 = np.zeros((1,1))
    w1 = np.zeros((1,1))
    prev_cost = 100000
    iter_index = 0
    
    for ind in range(iters):
        np.random.seed(ind)
        
        # 전체 X, y 데이터에서 랜덤하게 batch_size만큼 데이터 추출하여 sample_X, sample_y로 저장
        stochastic_random_index = np.random.permutation(X.shape[0])
        sample_X = X[stochastic_random_index[0:batch_size]]
        sample_y = y[stochastic_random_index[0:batch_size]]
        
        # 랜덤하게 batch_size만큼 추출된 데이터 기반으로 w1_update, w0_update 계산 후 업데이트
        w1_update, w0_update = get_weight_updates(w1, w0, sample_X, sample_y, learning_rate=0.01)
        w1 = w1 - w1_update
        w0 = w0 - w0_update
    
    return w1, w0


이렇게 만들어진 stochastic_gradient_descent_steps() 를 이용해 w1, w0 및 예측 오류 비용을 계산해 보겠습니다.

w1, w0 = stochastic_gradient_descent_steps(X, y, iters=1000)
print("w1:",round(w1[0,0],3),"w0:",round(w0[0,0], 3))

y_pred = w1 * X + w0
print('Stochastic Gradient Descent Total Cost:{0:.4f}'.format(get_cost(y, y_pred)))
[output]
w1: 4.028 w0: 6.156
Stochastic Gradient Descent Total Cost:0.9937


지금까지 피처가 1개, 즉 독립변수가 1개인 단순 선형 회귀에서 경사하강법을 적용해 봤습니다. 피처가 여러 개인 경우도 1개인 경우를 확장해 회귀 계수를 유사하게 도출할 수 있습니다.

피처가 한 개인 경우의 예측값 $\hat{Y} = w_0 + w_1 * {X}$ 로 회귀 계수를 도출하며, 피처가 M개($X_1, X_2, …, X_{100}$) 있다면 그에 따른 회귀 계수도 $M + 1$(1개는 $w_0$) 개로 도출됩니다. 즉, $\hat{Y} = w_0 + w_1 * {X_1} + w_2 * {X_2} + … + w_{100} * {X_{100}}$ 과 같이 예측 회귀식을 만들 수 있습니다. 이렇게 회귀 계수가 많아지더라도 선형대수를 이용해 간단하게 예측값을 도출할 수 있습니다.

예제에서 입력 행렬 X 에 대해서 예측 행렬 y_prednp.dot(X, w1.T) + w0 을 이용해 계산했습니다. 마찬가지로 데이터의 개수가 N 이고 피처 M 개의 입력 행렬을 $X_{mat}$, 회귀 계수 $W_1, W_2, …, W_{100}$ 을 $W$ 배열로 표기하면 예측 행렬 $\hat{Y} = np.dot(X_{mat}, W^T)+w_0$ 로 구할 수 있습니다.

image


$w_0$ 를 Weight의 배열인 $W$ 안에 포함시키기 위해서 $X_{mat}$ 의 맨 처음 열에 모든 데이터의 값이 1인 피처 Feat 0 을 추가하겠습니다. 이제 회귀 예측값은 $\hat{Y} = X_{mat} * W^T$ 와 같이 도출할 수 있습니다.

image

Read more

회귀(regression)의 이해

|

지도학습은 크게 두 가지 유형(분류와 회귀)으로 나뉘는데, 이 두 가지 기법의 가장 큰 차이는 분류 는 예측값이 카테고리와 같은 이산형 클래스 값 이고, 회귀연속형 숫자 값 이라는 것입니다. 회귀(regression)에 대해 알아보겠습니다.

image


회귀(regression)

회귀(regression)여러 개의 독립변수와 한 개의 종속변수 간의 상관관계를 모델링하는 기법을 통칭 합니다.


[Y=W_1X_1+W_2X_2+…+W_n*X_n]


예를 들어 아파트의 방 개수, 방 크기, 주변 학군 등 여러 개의 독립변수에 따라 아파트 가격이라는 종속변수가 어떤 관계를 나타내는지를 모델링하고 예측하는 것입니다.

  • $Y$ : 종속변수, 아파트 가격을 의미
  • $X$ : 독립변수, 방 개수, 방 크기, 주변 학군 등을 의미
  • $W$ : 독립변수의 값에 영향을 미치는 회귀 계수(Regression coefficients)

머신러닝 관점에서 보면 독립변수는 피처 에 해당되며 종속변수는 결정 값 입니다. 머신러닝 회귀 예측의 핵심 은 주어진 피처와 결정 값 데이터 기반에서 학습을 통해 최적의 회귀 계수를 찾아내는 것 입니다.


회귀는 회귀 계수의 선형/비선형 여부, 독립변수의 개수, 종속변수의 개수에 따라 여러 가지 유형으로 나눌 수 있습니다.

독립변수 개수 회귀 계수의 결합
1개: 단일 회귀 선형: 선형 회귀
여러 개: 다중 회귀 비선형: 비선형 회귀

여러 가지 회귀 중에서 선형 회귀가 가장 많이 사용되며, 선형 회귀는 실제 값과 예측값의 차이(오류의 제곱 값)를 최소화하는 직선형 회귀선을 최적화하는 방식입니다. 대표적인 선형 회귀모델은 다음과 같습니다.

  • 일반 선형 회귀 : 예측값과 실제 값의 RSS(Residual Sum of Squares)를 최소화할 수 있도록 회귀 계수를 최적화한 모델.
  • 릿지(Ridge) : L2 규제를 추가한 회귀 모델. L2 규제는 상대적으로 큰 회귀 계수 값의 예측 영향도를 감소시키기 위해서 회귀 계수값을 더 작게 만드는 규제 모델.
  • 라쏘(Lasso) : L1 규제를 추가한 회귀 모델. L1 규제는 예측 영향력이 작은 피처의 회귀 계수를 0으로 만들어 회귀 예측 시 피처가 선택되지 않게 하는 규제 모델.
  • 엘라스틱넷(ElasticNet) : L2, L1 규제를 함께 결합한 모델입니다. L1 규제로 피처의 개수를 줄임과 동시에 L2 규제로 계수 값의 크기를 조정 하는 규제 모델.
  • 로지스틱 회귀(Logistic Regression) : 회귀라는 이름이 붙어 있지만, 사실은 분류에 사용되는 선형 모델.


단순 선형 회귀를 예로 들어 회귀를 살펴보겠습니다.


단순 선형 회귀(Simple linear regression)

단순 선형 회귀독립변수도 하나, 종속변수도 하나 인 선형 회귀입니다. 예를 들어, 주택 가격($\hat{Y}$)이 주택의 크기($X$)로만 결정된다고 할 때, 일반적으로 주택의 크기가 크면 가격이 높아지는 경향이 있기 때문에 주택 가격은 주택 크기에 대해 선형(직선 형태)의 관계로 표현할 수 있습니다. 즉, 특정 기울기와 절편을 가진 1차 함수식으로 모델링(독립변수가 1개인 단순 선형 회귀 모델) 할 수 있습니다.($\hat{Y}=w_0+w_1*X$)

image


실제 값($Y$)은 예측 값($\hat{Y}$)에서 실제 값만큼의 오류 값을 뺀(또는 더한) 값이 됩니다.($\hat{Y}=w_0+w_1*X+오류값$) 실제 값과 회귀 모델의 차이에 따른 오류 값을 남은 오류, 즉 잔차(오류 값, Error) 라고 지칭합니다.

최적의 회귀모델을 만든다는 것 은 바로 전체 데이터의 잔차(오류 값) 합이 최소가 되는 모델 을 만든다는 의미입니다. 동시에 오류 값 합이 최소가 될 수 있는 최적의 회귀 계수 를 찾는다는 의미도 됩니다.

image


오류 값은 $+$ 나 $-$ 가 될 수 있기에, 전체 데이터의 오류 합을 구하기 위해 단순히 더했다가는 뜻하지 않게 오류의 합이 크게 줄어들 수 있습니다. 따라서 보통 오류 합을 계산할 때는 오류 값의 제곱을 구해서 더하는 방식($RSS$, Residual Sum of Square)을 취합니다. 일반적으로 미분 등의 계산을 편리하게 하기 위해서 $RSS$ 방식으로 오류 합을 구합니다. 즉, $Error^2 = RSS$

image


$RSS$ 는 이제 변수가 $w_0, w_1$ 인 식으로 표현할 수 있으며, 이 $RSS$ 를 최소로 하는 $w_0, w_1$, 즉 회귀 계수를 학습을 통해서 찾는 것이 머신러닝 기반 회귀의 핵심 사항 입니다. $RSS$ 는 회귀식의 독립변수 $X$, 종속변수 $Y$ 가 중심 변수가 아니라, $w$ 변수(회귀 계수)가 중심 변수임을 인지하는 것이 매우 중요 합니다(학습 데이터로 입력되는 독립변수와 종속변수는 $RSS$ 에서 모두 상수로 간주). 일반적으로 $RSS$ 는 학습 데이터의 건수로 나누어서 다음과 같이 정규화된 식으로 표현됩니다.


[RSS(w_0, W_1) = \frac{1}{N} \sum_{k=1}^N (y_i - (w_0 + w_1 * x_i))^2]


회귀에서 이 $RSS$비용(Cost) 이며 $w$ 변수(회귀 계수)로 구성되는 $RSS$ 를 비용함수 또는 손실함수(loss function) 라고 합니다.

머신러닝 회귀 알고리즘 은 데이터를 계속 학습하면서 이 비용 함수가 반환하는 값(즉, 오류 값)을 지속해서 감소시키고, 최종적으로는 더 이상 감소하지 않는 최소의 오류 값을 구하는 것 입니다.

Read more

결정 트리(Decision Tree)

|

결정 트리(Decision Tree)

결정 트리(Decision Tree) 는 ML 알고리즘 중 직관적으로 이해하기 쉬운 알고리즘으로, 데이터에 있는 규칙을 학습을 통해 자동으로 찾아내 트리(Tree) 기반의 분류 규칙을 만드는 것 입니다. 따라서 데이터의 어떤 기준을 바탕으로 규칙을 만들어야 가장 효율적인 분류가 될 것인가가 알고리즘의 성능을 크게 좌우 합니다.


다음 그림은 결정 트리의 구조를 간략하게 나타낸 것으로, 규칙 노드(Decision Node) 는 규칙 조건이 되는 것이고, 리프 노드(Leaf Node) 는 결정된 클래스 값입니다. 그리고 새로운 규칙 조건마다 서브 트리(Sub Tree) 가 생성됩니다.

데이터 세트에 피처가 있고 이러한 피처가 결합해 규칙 조건을 만들 때마다 규칙 노드가 만들어집니다. 하지만 많은 규칙이 있다는 것은 곧 분류를 결정하는 방식이 더욱 복잡해진다는 얘기이고, 이는 곧 과적합으로 이어지기 쉽습니다. 즉, 트리의 깊이(depth)가 깊어질수록 결정 트리의 예측 성능이 저하될 가능성이 높습니다.

image


가능한 한 적은 결정 노드로 높은 예측 정확도를 가지려면 데이터를 분류할 때 최대한 많은 데이터 세트가 해당 분류에 속할 수 있도록 결정 노드의 규칙이 정해져야 합니다. 이를 위해서는 어떻게 트리를 분할(Split)할 것인가가 중요한데 최대한 균일한 데이터 세트를 구성할 수 있도록 분할하는 것이 필요 합니다.

다음 그림에서 가장 균일한 데이터 세트부터 순서대로 나열한다면 어떻게 될까요?

image


  • C -> B -> A
    • C의 경우 모두 검은 공으로 구성되므로 데이터가 모두 균일.
    • B의 경우 일부 하얀 공을 가지고 있지만, 대부분 검은 공으로 구성되어 다음으로 균일도가 높음.
    • A의 경우는 검은 공 못지않게 많은 하얀 공을 가지고 있어 균일도가 제일 낮음.
  • 데이터 세트의 균일도는 데이터를 구분하는 데 필요한 정보의 양에 영향을 미칩니다.
    • C : 하나의 데이터를 뽑았을 때 데이터에 대한 별다른 정보 없이도 ‘검은 공’이라고 쉽게 예측할 수 있음.
    • A : 상대적으로 혼잡도가 높고 균일도가 낮기 때문에 같은 조건에서 데이터를 판단하는 데 있어 더 많은 정보가 필요.


결정 노드정보 균일도가 높은 데이터 세트를 먼저 선택할 수 있도록 규칙 조건을 만듭니다. 즉, 정보 균일도가 데이터 세트로 쪼개질 수 있도록 조건을 찾아 서브 데이터 세트를 만들고, 다시 이 서브 데이터 세트에서 균일도가 높은 자식 데이터 세트 쪼개는 방식을 자식 트리로 내려가면서 반복하는 방식으로 데이터 값을 예측하게 됩니다.


정보의 균일도를 측정하는 대표적인 방법

  • 엔트로피를 이용한 정보 이득(Information Gain)지수
    • 엔트로피주어진 데이터 집합의 혼잡도 를 의미하는데, 서로 다른 값이 섞여 있으면 엔트로피가 높고, 같은 값이 섞여 있으면 엔트로피가 낮습니다. 정보 이득 지수는 1에서 엔트로피 지수를 뺀 값으로, 결정 트리는 이 정보 이득 지수로 분할 기준을 정합니다. 즉, 정보 이득이 높은 속성을 기준으로 분할합니다.
  • 지니 계수
    • 지니계수 는 0이 가장 평등하고 1로 갈수록 불평등합니다. 머신러닝에 적용될 때는 지니 계수가 낮을수록 데이터 균일도가 높은 것으로 해석 해 지니 계수가 낮은 속성을 기준으로 분할합니다.


결정 트리 알고리즘을 사이킷런에서 구현한 DecisionTreeClassifier 는 기본적으로 지니 계수를 이용해 데이터 세트를 분할합니다. 결정 트리의 일반적인 알고리즘은 데이터 세트를 분할하는 데 가장 좋은 조건, 즉 정보 이득이 높거나 지니 계수가 낮은 조건을 찾아서 자식 트리 노드에 걸쳐 반복적으로 분할 한 뒤, 데이터가 모두 특정 분류에 속하게 되면 분할을 멈추고 분류를 결정합니다.

image


결정 트리 모델의 특징

  • 장점
    • 정보의 ‘균일도’라는 룰을 기반으로 하고 있어서 알고리즘이 쉽고 직관적.
    • 정보의 균일도만 신경 쓰면 되므로 특별한 경우를 제외하고는 각 피처의 스케일링과 정규화 같은 전처리 작업이 필요 없음
  • 단점
    • 과적합으로 정확도가 떨어질 수 있음. 이를 극복하기 위해 트리의 크기를 사전에 제한하는 튜닝 필요.


결정 트리 모델의 시각화

Graphviz 패키지를 이용하여 결정 트리 알고리즘이 어떠한 규칙을 가지고 트리를 생성하는지 시각적으로 볼 수있습니다. 사이킷런은 Graphviz 패키지와 쉽게 인터페이스할 수 있도록 export_graphviz() API를 제공하며, 함수 인자로 학습이 완료된 Estimator, 피처의 이름 리스트, 레이블 이름 리스트를 입력하면 학습된 결정 트리 규칙을 실제 트리 형태로 시각화해 보여줍니다.

이렇게 결정 트리가 만드는 규칙을 시각화해보면 결정 트리 알고리즘을 더욱 쉽게 이해할 수 있습니다.


설치가 완료된 Graphviz 를 이용해 붓꽃 데이터 세트에 결정 트리, 즉 DecisionTreeClassifer 를 적용할 때 어떻게 서브 트리가 구성되고 만들어지는지 시각화해 보겠습니다.

from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
import warnings
warnings.filterwarnings('ignore')

# DecisionTree Classifier 생성
dt_clf = DecisionTreeClassifier(random_state=156)

# 붓꽃 데이터를 로딩하고, 학습과 테스트 데이터 셋으로 분리
iris_data = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    iris_data.data,
    iris_data.target,
    test_size=0.2,
    random_state=11
)

# DecisionTreeClassifer 학습. 
dt_clf.fit(X_train, y_train)


사이킷런의 트리 모듈은 Graphviz 를 이용하기 위해 export_graphviz() 함수를 제공합니다. export_graphviz()Graphviz 가 읽어 들여서 그래프 형태로 시각화할 수 있는 출력 파일을 생성합니다. export_graphviz() 에 인자로 학습이 완료된 estimator, output 파일 명, 결정 클래스의 명칭, 피처의 명칭을 입력해주면 됩니다.

from sklearn.tree import export_graphviz

# export_graphviz()의 호출 결과로 out_file로 지정된 tree.dot 파일을 생성함. 
export_graphviz(
    dt_clf,
    out_file="tree.dot",
    class_names=iris_data.target_names,
    feature_names=iris_data.feature_names,
    impurity=True,
    filled=True
)


이렇게 생성된 출력 파일 ‘tree.dot’ 을 다음과 같이 Graphviz 의 파이썬 래퍼 모듈을 호출해 결정 트리의 규칙을 시각적으로 표현할 수 있습니다.

import graphviz

# 위에서 생성된 tree.dot 파일을 Graphviz 읽어서 Jupyter Notebook상에서 시각화 
with open("tree.dot") as f:
    dot_graph = f.read()
graphviz.Source(dot_graph)

image


  • 리프(leaf) 노드
    • 최종 클래스(레이블) 값이 결정되는 노드
    • 더 이상 자식 노드가 없는 노드
    • 리프 노드가 되려면 오직 하나의 클래스 값으로 최종 데이터가 구성되거나 리프 노드가 될 수 있는 하이퍼 파라미터 조건을 충족하면 됩니다.
  • 브랜치(branch) 노드
    • 자식 노드가 있는 노드
    • 자식 노드를 만들기 위한 분할 규칙 조건을 가짐

위 그림에서 노드 내에 기술된 지표의 의미는 다음과 같습니다.

  • petal length(cm) <= 2.45와 같이 피처의 조건이 있는 것은 자식 노드를 만들기 위한 규칙 조건입니다. 이 조건이 없으면 리프 노드입니다.
  • gini는 다음의 value=[]로 주어진 데이터 분포에서의 지니 계수입니다.
  • samples는 현 규칙에 해당하는 데이터 건수입니다.
  • value = []는 클래스 값 기반의 데이터 건수입니다. 붓꽃 데이터 세트는 클래스 값으로 0, 1, 2를 가지고 있습니다. 만일 Value = [41, 40, 39]라면 클래스 값의 순서로 Setosa 41개, Vesicolor 40개, Virginica 39개로 데이터가 구성돼 있다는 의미입니다.

각 노드의 색깔은 붓꽃 데이터의 레이블 값을 의미 합니다. 주황색은 0: Setosa, 초록색은 1:Versicolor, 보라색은 2: Virginica 레이블을 나타냅니다. 색깔이 짙어질수록 지니 계수가 낮고 해당 레이블에 속하는 샘플 데이터가 많다는 의미 입니다.


결정 트리는 균일도에 기반해 어떠한 속성을 규칙 조건으로 선택하느냐가 중요한 요건 입니다. 중요한 몇 개의 피처가 명확한 규칙 트리를 만드는 데 크게 기여하며, 모델을 좀 더 간결하고 이상치(Outlier)에 강한 모델을 만들 수 있기 때문 입니다. 사이킷런은 결정 트리 알고리즘이 학습을 통해 규칙을 정하는 데 있어 피처의 중요한 역할 지표를 DecisionTreeClassifier 객체의 feature_importances_ 속성으로 제공합니다.

feature_importances_피처가 트리 분할 시 정보 이득이나 지니 계수를 얼마나 효율적으로 잘 개선시켰는지를 정규화된 값으로 표현한 것 으로 ndarray 형태로 값을 반환하며 피처 순서대로 값이 할당됩니다. 일반적으로 값이 높을수록 해당 피처의 중요도가 높다는 의미 입니다.

위 예제에서 fit() 으로 학습된 DecisionTreeClassifier 객체 변수인 df_clf 에서 feature_importances_ 속성을 가져와 피처별로 중요도 값을 매핑하고 이를 막대그래프로 표현해 보겠습니다.

import seaborn as sns
import numpy as np
%matplotlib inline

# feature importance 추출 
print("Feature importances:\n{0}".format(np.round(dt_clf.feature_importances_, 3)))

# feature별 importance 매핑
for name, value in zip(iris_data.feature_names, dt_clf.feature_importances_):
    print('{0} : {1:.3f}'.format(name, value))

# feature importance를 column 별로 시각화 하기 
sns.barplot(x=dt_clf.feature_importances_ , y=iris_data.feature_names)
[output]
Feature importances:
[0.025 0.    0.555 0.42 ]
sepal length (cm) : 0.025
sepal width (cm) : 0.000
petal length (cm) : 0.555
petal width (cm) : 0.420

image


여러 피처들 중 petal_length 가 가장 피처 중요도가 높음을 알 수 있습니다.


결정 트리 과적합(Overfitting)

결정 트리가 어떻게 학습 데이터를 분할해 예측을 수행하는지와 이로 인한 과적합 문제를 시각화 해 알아보겠습니다.

먼저 분류를 위한 데이터 세트를 임의로 만들어 보겠습니다. 사이킷런은 분류를 위한 테스트용 데이터를 쉽게 만들 수 있도록 make_classification() 함수를 제공합니다. 이 함수를 이용해 2개의 피처가 3가지 유형의 클래스 값을 가지는 데이터 세트를 만들고 이를 그래프 형태로 시각화하겠습니다. make_classification() 호출 시 반환되는 객체는 피처 데이터 세트와 클래스 레이블 데이터세트입니다.

from sklearn.datasets import make_classification
import matplotlib.pyplot as plt
%matplotlib inline

plt.title("3 Class values with 2 Features Sample data creation")

# 2차원 시각화를 위해서 feature는 2개, 결정값 클래스는 3가지 유형의 classification 샘플 데이터 생성. 
X_features, y_labels = make_classification(
    n_features=2,
    n_redundant=0,
    n_informative=2,
    n_classes=3,
    n_clusters_per_class=1,
    random_state=0
)

# plot 형태로 2개의 feature로 2차원 좌표 시각화, 각 클래스값은 다른 색깔로 표시됨. 
plt.scatter(
    X_features[:, 0],
    X_features[:, 1],
    marker='o',
    c=y_labels,
    s=25,
    cmap='rainbow',
    edgecolor='k'
)

image


각 피처가 X, Y축으로 나열된 2차원 그래프이며, 3개의 클래스 값 구분은 색깔로 돼 있습니다. 이제 X_featuresy_labels 데이터 세트를 기반으로 결정 트리를 학습하겠습니다. 첫 번째 학습 시에는 결정 트리 생성에 별다른 제약이 없도록 결정 트리의 하이퍼 파라미터를 디폴트로 한 뒤, 결정 트리 모델이 어떠한 결정 기준을 가지고 분할하면서 데이터를 분류하는지 확인할 것입니다.

먼저 결정 트리 생성에 별다른 제약이 없도록 하이퍼 파라미터가 디폴트인 Classifier를 학습하고 결정기준 경계를 시각화해 보겠습니다.

from sklearn.tree import DecisionTreeClassifier

# 특정한 트리 생성 제약없는 결정 트리의 Decsion Boundary 시각화.
dt_clf = DecisionTreeClassifier(random_state=156).fit(X_features, y_labels)
visualize_boundary(dt_clf, X_features, y_labels)

image


일부 이상치(Outlier) 데이터까지 분류하기 위해 분할이 자주 일어나서 결정 기준 경계가 매우 많아졌습니다. 결정 트리의 기본 하이퍼 파라미터 설정은 리프 노드 안에 데이터가 모두 균일하거나 하나만존재해야 하는 엄격한 분할 기준으로 인해 결정 기준 경계가 많아지고 복잡해졌습니다. 이렇게 복잡한모델은 학습 데이터 세트의 특성과 약간만 다른 형태의 데이터 세트를 예측하면 예측 정확도가 떨어지게 됩니다.

이번에는 min_samples_leaf = 6을 설정해 6개 이하의 데이터는 리프 노드를 생성할 수 있도록 리프노드 생성 규칙을 완화한 뒤 하이퍼 파라미터를 변경해 어떻게 결정 기준 경계가 변하는지 살펴보겠습니다.

# min_samples_leaf=6 으로 트리 생성 조건을 제약한 Decision Boundary 시각화
dt_clf = DecisionTreeClassifier(min_samples_leaf=6, random_state=156).fit(X_features, y_labels)
visualize_boundary(dt_clf, X_features, y_labels)

image


이상치에 크게 반응하지 않으면서 좀 더 일반화된 분류 규칙에 따라 분류됐음을 알 수 있습니다. 다양한 테스트 데이터 세트를 기반으로 한 결정 트리 모델의 예측 성능은 첫 번째 모델보다는 min_samples_leaf=6으로 트리 생성 조건을 제약한 모델이 더 뛰어날 가능성이 높습니다. 왜냐하면 테스트데이터 세트는 학습 데이터 세트와는 다른 데이터 세트인데, 학습 데이터에만 지나치게 최적화된 분류기준은 오히려 테스트 데이터 세트에서 정확도를 떨어뜨릴 수 있기 때문입니다.

Read more

ROC 곡선과 AUC

|

ROC 곡선과 AUC

ROC 곡선과 이에 기반한 AUC 스코어는 이진 분류의 예측 성능 측정에서 중요하게 사용되는 지표 입니다. ROC 곡선(Receiver Operation Characteristic Curve) 은 우리말로 수신자 판단 곡선 으로 불립니다. ROC 곡선은 FPR(False Positive Rate)이 변할 때 TPR(True Positive Rate)이 어떻게 변하는지를 나타내는 곡선, 즉 FPR의 변화에 따른 TPR의 변화를 곡선 형태 로 나타납니다.

TPR(True Positive Rate)민감도(재현율) 를 나타내고, 민감도에 대응하는 지표로 TNR(True Negative Rate)특이성(Specificity) 을 나타냅니다.

  • 민감도(TPR) : 실제값 Positive(양성)가 정확히 예측돼야 하는 수준 을 나타냄(질병이 있는 사람은 질병이 있는 것으로 양성 판정).
  • 특이성(TNR) : 실제값 Negative(음성)가 정확히 예측돼야 하는 수준 을 나타냄(질병이 없는 건강한 사람은 질병이 없는 것으로 음성 판정).


[TPR(True\ Positive\ Rate) = \frac{TP}{FN + TP}]

[TNR(True\ Negative\ Rate) = \frac{TN}{FP + TN}]

[FPR(False\ Positive\ Rate) = \frac{FP}{FP + TN} = 1 - TNR = 1 - 특이성]


image


다음은 ROC 곡선의 예입니다. 가운데 직선은 ROC 곡선의 최저 값(AUC는 0.5)으로, 동전을 무작위로 던져 앞/뒤를 맞추는 랜덤 수준의 이진 분류의 ROC 직선입니다. ROC 곡선이 가운데 직선에 가까울수록 성능이 떨어지는 것이며, 멀어질수록 성능이 뛰어난 것 입니다.

ROC 곡선FPR을 0부터 1까지 분류 결정 임계값(Positive 예측값을 결정하는 확률의 기준)을 변경하면서 TPR의 변화값을 구합니다.

  • FPR을 0으로 만드는 방법 : 분류 결정 임계값을 1로 지정
    • 임계값을 1로 지정하면 Postive 예측 기준이 매우 높기 때문에 분류기(Classifier)가 임계값보다 높은 확률을 가진 데이터를 Positive로 예측할 수 없기 때문에 FPR은 0이 됩니다.
    • 즉, 아예 Positive로 예측하지 않기 때문에 FP 값이 0이 되므로 자연스럽게 FPR은 0이 됩니다.
  • FPR을 1으로 만드는 방법 : 분류 결정 임계값을 0으로 지정
    • 임계값을 0으로 지정하면 분류기의 Positive 확률 기준이 너무 낮아서 다 Positive로 예측합니다.
    • 즉, 아예 Negative 예측이 없기 때문에 TN이 0이 되고 FPR 값은 1이 됩니다.


정리하면 분류 결정 임계값을 1부터 0까지 변화시키면서 FPR을 구하고 이 FPR 값의 변화에 따른 TPR 값을 구하는 것ROC 곡선 입니다.

사이킷런은 ROC 곡선을 구하기 위해 roc_curve() API 를 제공합니다.

  • roc_curve() API
    • y_true : 실제 클래스 값 array
    • y_score : Positive 칼럼의 예측 확률 배열
    • 반환값 : FPR, TPR, 임계값


roc_curve() API를 이용해 타이타닉 생존자 예측 모델의 FPR, TPR, 임계값 을 구해 보겠습니다.

from sklearn.preprocessing import LabelEncoder

# Null 처리 함수
def fillna(df):
    df['Age'].fillna(df['Age'].mean(), inplace=True)
    df['Cabin'].fillna('N', inplace=True)
    df['Embarked'].fillna('N', inplace=True)
    df['Fare'].fillna(0, inplace=True)
    return df

# 머신러닝 알고리즘에 불필요한 피처 제거
def drop_features(df):
    df.drop(['PassengerId', 'Name', 'Ticket'], axis=1, inplace=True)
    return df

# 레이블 인코딩 수행.
def format_features(df):
    df['Cabin'] = df['Cabin'].str[:1]
    features = ['Cabin', 'Sex', 'Embarked']
    for feature in features:
        le = LabelEncoder()
        le = le.fit(df[feature])
        df[feature] = le.transform(df[feature])
    return df

# 앞에서 설정한 데이터 전처리 함수 호출
def transform_features(df):
    df = fillna(df)
    df = drop_features(df)
    df = format_features(df)
    return df
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split 
from sklearn.linear_model import LogisticRegression

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
titanic_df = pd.read_csv('./titanic/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived', axis=1)
X_titanic_df = transform_features(X_titanic_df)
X_train, X_test, y_train, y_test = train_test_split(
    X_titanic_df,
    y_titanic_df,
    test_size=0.20,
    random_state=11
)
from sklearn.metrics import roc_curve

lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)

# 레이블 값이 1일때의 예측 확률을 추출 
pred_proba_class1 = lr_clf.predict_proba(X_test)[:, 1] 

fprs, tprs, thresholds = roc_curve(y_test, pred_proba_class1)

# 반환된 임곗값 배열에서 샘플로 데이터를 추출하되, 임곗값을 5 Step으로 추출. 
# thresholds[0]은 max(예측확률)+1로 임의 설정됨. 이를 제외하기 위해 np.arange는 1부터 시작
thr_index = np.arange(1, thresholds.shape[0], 5)
print('샘플 추출을 위한 임곗값 배열의 index:', thr_index)
print('샘플 index로 추출한 임곗값: ', np.round(thresholds[thr_index], 2))

# 5 step 단위로 추출된 임계값에 따른 FPR, TPR 값
print('\n샘플 임곗값별 FPR: ', np.round(fprs[thr_index], 3))
print('샘플 임곗값별 TPR: ', np.round(tprs[thr_index], 3))
[output]
샘플 추출을 위한 임곗값 배열의 index: [ 1  6 11 16 21 26 31 36 41 46]
샘플 index로 추출한 임곗값:  [0.94 0.73 0.62 0.52 0.44 0.28 0.15 0.14 0.13 0.12]

샘플 임곗값별 FPR:  [0.    0.008 0.025 0.076 0.127 0.254 0.576 0.61  0.746 0.847]
샘플 임곗값별 TPR:  [0.016 0.492 0.705 0.738 0.803 0.885 0.902 0.951 0.967 1.   ]


roc_curve() 의 결과를 살펴보면 임계값이 1에 가까운 값에서 점점 작아지면서 FPR 이 점점 커집니다. 그리고 FPR 이 조금씩 커질 때 TPR 은 가파르게 커짐을 알 수 있습니다. FPR 의 변화에 따른 TPR 의 변화를 ROC 곡선 으로 시각화해 보겠습니다.

import matplotlib.pyplot as plt
%matplotlib inline

def roc_curve_plot(y_test, pred_proba_c1):
    # 임곗값에 따른 FPR, TPR 값을 반환 받음. 
    fprs , tprs , thresholds = roc_curve(y_test ,pred_proba_c1)

    plt.figure(figsize=(14, 8))
    # ROC Curve를 plot 곡선으로 그림. 
    plt.plot(fprs, tprs, label='ROC')
    # 가운데 대각선 직선을 그림. 
    plt.plot([0, 1], [0, 1], 'k--', label='Random')
    
    # FPR X 축의 Scale을 0.1 단위로 변경, X,Y 축명 설정등   
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    plt.xlim(0,1); plt.ylim(0,1)
    plt.xlabel('FPR( 1 - Specificity )'); plt.ylabel('TPR( Recall )')
    plt.legend()
    plt.show()
    
roc_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1])

image


일반적으로 ROC 곡선 자체는 FPRTPR의 변화 값을 보는 데 이용하며, 분류의 성능 지표로 사용되는 것은 ROC 곡선 면적에 기반한 AUC 값으로 결정합니다. AUC(Area Under Curve) 값은 ROC 곡선 밑의 면적을 구한 것으로서 일반적으로 1에 가까울수록 좋은 수치 입니다.

AUC 수치가 커지려면 FPR이 작은 상태에서 얼마나 큰 TPR을 얻을 수 있느냐가 관건 입니다. 가운데 직선에서 멀어지고 왼쪽상단 모서리 쪽으로 가파르게 곡선이 이동할수록 직사각형에 가까운 곡선이 되어 면적이 1에 가까워지는 좋은 ROC AUC 성능 수치를 얻게 됩니다. 가운데 대각선 직선은 랜덤 수준의(동전 던지기 수준) 이진 분류 AUC 값으로 0.5입니다. 따라서 보통의 분류는 0.5 이상의 AUC 값을 가집니다.

from sklearn.metrics import roc_auc_score

pred_proba = lr_clf.predict_proba(X_test)[:, 1]
roc_score = roc_auc_score(y_test, pred_proba)
print('ROC AUC 값: {0:.4f}'.format(roc_score))
[output]
ROC AUC 값: 0.8987


마지막으로 get_clf_eval() 함수에 roc_auc_score() 를 이용해 ROC AUC값을 측정하는 로직을 추가하는데, ROC AUC는 예측 확률값을 기반으로 계산되므로 이를 get_clf_eval() 함수의 인자로 받을 수 있도록 get_clf_eval(y_test, pred=None, pred_proba=None) 로 함수형을 변경해 줍니다. 이제 get_clf_eval() 함수는 정확도, 정밀도, 재현율, F1 스코어, ROC AUC 값까지 출력할 수 있습니다.

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score, recall_score, confusion_matrix
from sklearn.metrics import f1_score
from sklearn.metrics import roc_curve, roc_auc_score

def get_clf_eval(y_test, pred=None, pred_proba=None):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    f1 = f1_score(y_test, pred)
    # ROC-AUC 추가 
    roc_auc = roc_auc_score(y_test, pred_proba)
    
    print('오차 행렬')
    print(confusion)
    # ROC-AUC print 추가
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1: {3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
from sklearn.preprocessing import Binarizer

def get_eval_by_threshold(y_test, pred_proba_c1, thresholds):
    # thresholds list객체내의 값을 차례로 iteration하면서 Evaluation 수행.
    for custom_threshold in thresholds:
        binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_c1) 
        custom_predict = binarizer.transform(pred_proba_c1)
        print('\n임곗값:', custom_threshold)
        get_clf_eval(y_test, custom_predict, pred_proba_c1)

thresholds = [0.4 , 0.45 , 0.50 , 0.55 , 0.60]
pred_proba = lr_clf.predict_proba(X_test)
get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197, F1: 0.7576, AUC:0.8987

임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도: 0.7869, 재현율: 0.7869, F1: 0.7869, AUC:0.8987

임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705, F1: 0.7966, AUC:0.8987

임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도: 0.8654, 재현율: 0.7377, F1: 0.7965, AUC:0.8987

임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도: 0.8980, 재현율: 0.7213, F1: 0.8000, AUC:0.8987

Read more

confusion matrix 및 정밀도(Precision)와 재현율(Recall)

|

오차 행렬(confusion matrix, 혼동행렬)

이진 분류에서 성능 지표로 잘 활용되는 오차행렬(confusion matrix, 혼동행렬)학습된 분류 모델이 예측을 수행하면서 얼마나 헷갈리고(confused) 있는지를 함께 보여주는 지표 입니다. 즉, 이진 분류의 예측 오류가 얼마인지와 더불어 어떠한 유형의 예측 오류가 발생하고 있는지를 함께 나타내는 지표 입니다.

오차 행렬은 다음과 같이 4분면 행렬에서 실제 레이블 클래스 값과 예측 레이블 클래스 값이 어떠한 유형을 가지고 매핑 되는지를 나타내며, 예측 클래스와 실제 클래스의 값 유형에 따라 결정되는 TN, FP, FN, TP 형태로 오차 행렬의 4분면을 채울 수 있습니다. TN, FP, FN, TP 값을 다양하게 결합해 분류 모델 예측 성능의 오류가 어떠한 모습으로 발생하는지 알 수 있습니다.

image


TN, FP, FN, TP예측 클래스와 실제 클래스의 Positive 결정값(값 1)과 Negative 결정 값(값 0)의 결합에 따라 결정 됩니다. 앞 문자 True/False예측값과 실제값이 ‘같은가/틀린가’를 의미 하고, 뒤 문자 Negative/Positive예측 결과 값이 부정(0)/긍정(1)을 의미 합니다.

  • TN : 예측을 Negative(0) 으로 예측, 정답 -> 실제 값은 Negative(0)
  • FP : 예측을 Positive(1) 으로 예측, 오답 -> 실제 값은 Negative(0)
  • FN : 예측을 Negative(0) 으로 예측, 오답 -> 실제 값은 Positive(1)
  • TP : 예측을 Positive(1) 으로 예측, 정답 -> 실제 값은 Positive(1)


다음 그림은 TN, FP, FN, TP 구분을 재미있게 표현한 그림입니다.

image


사이킷런은 오차 행렬을 구하기 위해 confusion_matrix() API 를 제공합니다.

MNIST 데이터의 10%만 True, 나머지 90%는 False인 불균형한 데이터 세트MyFakeClassifier 를 생성하고, MyFakeClassifier 의 예측 성능 지표를 confusion_matrix() 를 이용해 오차 행렬로 표현하겠습니다.

from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split
from sklearn.base import BaseEstimator
from sklearn.metrics import accuracy_score
import numpy as np
import pandas as pd

class MyFakeClassifier(BaseEstimator):
    def fit(self, X, y):
        pass
    
    # 입력값으로 들어오는 X 데이터 셋의 크기만큼 모두 0값으로 만들어서 반환
    def predict(self, X):
        return np.zeros((len(X), 1), dtype=bool)

# 사이킷런의 내장 데이터 셋인 load_digits()를 이용하여 MNIST 데이터 로딩
digits = load_digits()

# digits번호가 7번이면 True이고 이를 astype(int)로 1로 변환, 7번이 아니면 False이고 0으로 변환. 
y = (digits.target == 7).astype(int)
X_train, X_test, y_train, y_test = train_test_split(digits.data, y, random_state=11)

# 불균형한 레이블 데이터 분포도 확인. 
print('레이블 테스트 세트 크기 :', y_test.shape)
print('테스트 세트 레이블 0 과 1의 분포도')
print(pd.Series(y_test).value_counts())

# Dummy Classifier로 학습/예측/정확도 평가
fakeclf = MyFakeClassifier()
fakeclf.fit(X_train, y_train)
fakepred = fakeclf.predict(X_test)
print('\n모든 예측을 0으로 하여도 정확도는 : {:.3f}'.format(accuracy_score(y_test, fakepred)))
[output]
레이블 테스트 세트 크기 : (450,)
테스트 세트 레이블 0 과 1의 분포도
0    405
1     45
dtype: int64

모든 예측을 0으로 하여도 정확도는 : 0.900


from sklearn.metrics import confusion_matrix

# 앞절의 예측 결과인 fakepred와 실제 결과인 y_test의 Confusion Matrix출력
confusion_matrix(y_test, fakepred)
[output]
array([[405,   0],
       [ 45,   0]])


이진 분류의 TN, FP, FN, TP 는 상단 도표와 동일한 위치를 가지며, array에서 가져올 수 있습니다. 즉, TNarray[0,0] 로 405, FParray[0,1] 로 0, FNarray[1,0] 로 45, TParray[1,1] 로 0에 해당합니다.


MyFakeClassifieload_digits()target==7 인지 아닌지에 따라 클래스 값을 True/False 이진 분류 로 변경한 데이터 세트를 사용해 무조건 Negative 로 예측하는 Classifier 였고, 테스트 데이터 세트의 클래스 값 분포는 0이 405건, 1이 45건입니다.

따라서 TN 은 전체 450건 데이터 중 무조건 Negative 0으로 예측해서 True가 된 결과 405건, FP 는 Positive 1로 예측한 건수가 없으므로 0건, FN 은 Positive 1인 건수 45건을 Negative로 예측해서 False가 된 결과 45건, TP 는 Positive 1로 예측한 건수가 없으므로 0건입니다.

image


TN, FP, FN, TP 값은 Classifier 성능의 여러 면모를 판단할 수 있는 기반 정보를 제공 합니다. 이 값을 조합해 Classifier의 성능을 측정할 수 있는 주요 지표인 정확도(Accuracy), 정밀도(Precision), 재현율(Recall) 값을 알 수 있습니다.

정확도예측값과 실제 값이 얼마나 동일한가에 대한 비율만으로 결정 됩니다. 즉, 오차 행렬에서 True에 해당하는 값인 TN과 TP에 좌우 됩니다. 정확도는 오차 행렬상에서 다음과 같이 재정의될 수 있습니다.


[정확도 = \frac{예측\ 결과와\ 실제\ 값이\ 동일한\ 건수}{전체\ 데이터\ 수}=\frac{FN + TP}{TN + FP + FN + TP}]


일반적으로 불균형한 레이블 클래스를 가지는 이진 분류 모델 에서는 많은 데이터 중에서 중점적으로 찾아야 하는 매우 적은 수의 결과값에 Positive 를 설정해 1 값을 부여하고, 그렇지 않은 경우는 Negative 로 0 값을 부여하는 경우가 많습니다. 예를 들어 암 검진 예측 모델에서는 암이 양성일 경우 Positive 양성으로 1, 암이 음성일 경우 Negative 음성으로 값이 할당되는 경우가 일반적입니다.


불균형한 이진 분류 데이터 세트에서는 Positive 데이터 건수가 매우 작기 때문에 데이터에 기반한 ML 알고리즘은 Positive보다는 Negative로 예측 정확도가 높아지는 경향이 발생 합니다.

10,000건의 데이터 세트에서 9,900건이 Negative이고 100건이 Positive라면 Negative로 예측하는 경향이 더 강해져TN은 매우 커지고, TP는 매우 작아지게 됩니다. 또한 Negative로 예측할 때 정확도가 높기 때문에 FN(Negative로 예측할 때 틀린 데이터 수)이 매우 작고, Positive로 예측하는 경우가 작기 때문FP 역시 매우 작아집니다.


결과적으로 정확도 지표비대칭한 데이터 세트에서 Positive에 대한 예측 정확도를 판단하지 못한 채 Negative에 대한 예측 정확도만으로도 분류의 정확도가 매우 높게 나타나는 수치적인 판단 오류 를 일으키게 됩니다. 불균형한 데이터 세트에서 정확도만으로는 모델 신뢰도가 떨어질 수 있는 사례를 확인했습니다.


정밀도와 재현율

정밀도재현율Positive 데이터 세트의 예측 성능에 좀 더 초점을 맞춘 평가 지표 입니다. 정밀도와 재현율은 다음과 같은 공식으로 계산됩니다.


[정밀도 = \frac{TP}{FP + TP},\ 재현율 = \frac{TP}{FN + TP}]


정밀도예측을 Positive로 한 대상 중예측과 실제 값이 Positive로 일치한 데이터의 비율 을 뜻합니다. Positive 예측 성능을 더욱 정밀하게 측정하기 위한 평가 지표로 양성 예측도 라고도 불립니다.

정밀도가 상대적으로 더 중요한 지표인 경우실제 Negative 음성인 데이터 예측을 Positive 양성으로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우 입니다.

  • 스팸메일 여부를 판단하는 모델
    • 실제 Positive인 스팸 메일을 Negative인 일반 메일로 분류 -> 사용자가 불편함을 느끼는 정도.
    • 실제 Negative인 일반 메일을 Positive인 스팸 메일로 잘못 분류 -> 메일을 아예 받지 못하게 돼 업무에 차질이 생김.


재현율실제 값이 Positive인 대상 중예측과 실제 값이 Positive로 일치한 데이터의 비율 을 뜻합니다. 민감도(Sensitivity) 또는 TPR(True Positive Rate) 라고도 불립니다.

재현율이 상대적으로 더 중요한 지표인 경우실제 Positive 양성인 데이터 예측을 Negative로 잘못 판단하게 되면 업무상 큰 영향이 발생하는 경우 입니다.

  • 암 판단 모델
    • 실제 Negative인 건강한 환자를 암 환자인 Positive로 예측 -> 다시 한번 재검사를 하는 수준의 비용이 소모.
    • 실제 Positive인 암(양성) 환자를 Negative 음성으로 잘못 예측 -> Die.
  • 보험 사기 및 금융 사기 적발 모델
    • 정상 금융거래인 Negative를 금융사기인 Positive로 잘못 예측 -> 금융 사기인지 재확인 절차.
    • 실제 금융거래 사기인 Positive를 Negative로 잘못 예측 -> 회사에 미치는 손해가 클 것.


다시 한번 재현율과 정밀도의 공식을 살펴보겠습니다.

[정밀도 = \frac{TP}{FP + TP},\ 재현율 = \frac{TP}{FN + TP}]

  • 공통점
    • 재현율과 정밀도 모두 TP를 높이는 데 초점을 맞춤.
  • 차이점
    • 재현율 : FN(실제Positive, 예측 Negative)를 낮추는 데 초점을 맞춤.
    • 정밀도 : FP(실제Positive, 예측 Positive)를 낮추는 데 초점을 맞춤.


위와 같은 특성 때문에 재현율과 정밀도는 서로 보완적인 지표로 분류의 성능을 평가하는 데 적용 됩니다. 가장 좋은 성능평가는 재현율과 정밀도 모두 높은 수치를 얻는 것이며, 반면에 둘 중 어느 한 평가 지표에 편향되는 경우는 바람직하지 않습니다.

이번에는 오차 행렬 및 정밀도, 재현율을 모두 구해서 예측 성능을 평가해 보겠습니다. 사이킷런은 정밀도 계산을 위해 precision_score(), 재현율 계산을 위해 recall_score()를 API로 제공합니다.

from sklearn.metrics import accuracy_score, precision_score , recall_score , confusion_matrix

def get_clf_eval(y_test , pred):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    print('오차 행렬')
    print(confusion)
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}'.format(accuracy, precision, recall))


이제 로지스틱 회귀 기반으로 타이타닉 생존자를 예측하고 confusion matrix, accuracy, precision, recall 평가를 수행합니다.

import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split 
from sklearn.linear_model import LogisticRegression

# 원본 데이터를 재로딩, 데이터 가공, 학습데이터/테스트 데이터 분할. 
titanic_df = pd.read_csv('./titanic/train.csv')
y_titanic_df = titanic_df['Survived']
X_titanic_df= titanic_df.drop('Survived', axis=1)
X_titanic_df = transform_features(X_titanic_df)
X_train, X_test, y_train, y_test = train_test_split(
    X_titanic_df,
    y_titanic_df,
    test_size=0.20,
    random_state=11
)

lr_clf = LogisticRegression(solver='liblinear')
lr_clf.fit(X_train, y_train)
pred = lr_clf.predict(X_test)
get_clf_eval(y_test, pred)
[output]
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705


정밀도/재현율 트레이드오프(Trade-off)

분류 결정 임계값(Threshold) 을 조정해 정밀도 또는 재현율의 수치를 높일 수 있습니다. 하지만 정밀도와 재현율은 상호 보완적인 평가 지표이기 때문에 정밀도/재현율은 트레이드오프(Trade-off) 를 가집니다.

사이킷런의 분류 알고리즘은 예측 데이터가 특정 레이블(Label, 결정 클래스 값)에 속하는지를 계산하기 위해 먼저 개별 레이블별로 결정 확률을 구합니다. 그리고 예측 확률이 큰 레이블값으로 예측 하게 됩니다. 가령 이진 분류 모델에서 특정 데이터가 0이 될 확률이 10%, 1이 될 확률이 90%로 예측됐다면 최종 예측은 더 큰 확률을 가진, 즉 90% 확률을 가진 1로 예측합니다. 일반적으로 이진 분류에서는 이 임곗값을 0.5, 즉 50%로 정하고 이 기준 값보다 확률이 크면 Positive, 작으면 Negative로 결정합니다.


사이킷런은 개별 데이터별로 예측 확률을 반환하는 메서드인 predict_proba() 를 제공합니다.

  • predict_proba()
    • 반환 결과가 예측 결과 클래스값이 아닌 예측 확률 결과.
    • 학습이 완료된 사이킷런 Classifier 객체에서 호출 가능.
    • 이진 분류에서 첫 번째 칼럼은 0 Negative의 확률, 두 번째 칼럼은 1 Positive의 확률.


앞 예제의 타이타닉 생존자 데이터를 학습한 LogisiticRegression 객체에서 predict_proba() 메서드와 predict() 메서드의 결과를 비교해 보겠습니다.

pred_proba = lr_clf.predict_proba(X_test)
pred = lr_clf.predict(X_test)
print('pred_proba()결과 Shape : {0}'.format(pred_proba.shape))
print('\npred_proba array에서 앞 3개만 샘플로 추출 \n:', pred_proba[:3])

# 예측 확률 array와 예측 결과값 array를 concatenate 하여 예측 확률과 결과값을 한눈에 확인
pred_proba_result = np.concatenate([pred_proba, pred.reshape(-1, 1)], axis=1)
print('\n두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 \n', pred_proba_result[:3])
[output]
pred_proba()결과 Shape : (179, 2)

pred_proba array에서 앞 3개만 샘플로 추출 
: [[0.44935227 0.55064773]
 [0.86335512 0.13664488]
 [0.86429645 0.13570355]]

두개의 class 중에서 더 큰 확률을 클래스 값으로 예측 
 [[0.44935227 0.55064773 1.        ]
 [0.86335512 0.13664488 0.        ]
 [0.86429645 0.13570355 0.        ]]


반환 결과인 ndarray는 0과 1에 대한 확률을 나타내므로 첫 번째 칼럼 값과 두 번째 칼럼 값을 더하면 1이 됩니다. 두 개의 칼럼 중에서 더 큰 확률 값으로 predict() 메서드가 최종 예측하고 있습니다.

predict() 메서드는 predict_proba() 메서드에 기반해 생성된 API로, predict_proba() 호출 결과로 반환된 배열에서 분류 결정 임계값보다 큰 값이 들어 있는 칼럼의 위치를 받아 최종적으로 예측 클래스를 결정하는 API 입니다. 이는 사이킷런이 어떻게 정밀도/재현율 트레이드오프를 구현했는지를 이해할 수 있으며, 사이킷런은 분류 결정 임계값을 조절해 정밀도와 재현율의 성능 수치를 상호 보완적으로 조정할 수 있습니다.


사이킷런의 Binarizer 클래스 를 이용하여 정밀도/재현율 트레이드오프 방식을 이해해 보겠습니다.

다음 예제는 threshold 변수를 특정 값으로 설정하고 Binarizer 클래스 를 객체로 생성합니다. 생성된 Binarizer 객체fit_transform() 메서드를 이용하여 넘파이 ndarray 를 입력하면 입력된 ndarray 의 값을 지정된 threshold 보다 같거나 작으면 0값으로, 크면 1값으로 변환해 반환합니다.

from sklearn.preprocessing import Binarizer

X = [[ 1, -1,  2],
     [ 2,  0,  0],
     [ 0,  1.1, 1.2]]

# threshold 기준값보다 같거나 작으면 0을, 크면 1을 반환
binarizer = Binarizer(threshold=1.1)                     
print(binarizer.fit_transform(X))
[output]
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 0. 1.]]


입력된 X 데이터 세트에서 Binarizerthreshold 값이 1.1보다 같거나 작으면 0, 크면 1로 변환됨을 알 수 있습니다. 각 클래스별 예측 확률값에 분류 결정 임계값(threshold)을 지정한 Binarizer 클래스 를 통해 최종 예측값을 구하는 predict() 를 만들겠습니다.

from sklearn.preprocessing import Binarizer

#Binarizer의 threshold 설정값. 분류 결정 임곗값임.  
custom_threshold = 0.5

# predict_proba() 반환값의 두번째 컬럼, 즉 Positive 클래스 컬럼 하나만 추출하여 Binarizer를 적용
pred_proba_1 = pred_proba[:,1].reshape(-1, 1)

binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1) 
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)
[output]
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705


해당 코드로 계산된 평가 지표는 앞 예제의 타이타닉 데이터로 학습된 로지스틱 회귀 Classifier 객체에서 호출된 predict() 로 계산된 지표 값과 정확히 같습니다. predict()predict_proba() 에 기반함을 알 수 있습니다.


만일 이 분류 결정 임계값을 0.4로 낮춰보겠습니다.

# Binarizer의 threshold 설정값을 0.4로 설정. 즉 분류 결정 임곗값을 0.5에서 0.4로 낮춤  
custom_threshold = 0.4
pred_proba_1 = pred_proba[:,1].reshape(-1, 1)
binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_1) 
custom_predict = binarizer.transform(pred_proba_1)

get_clf_eval(y_test, custom_predict)
[output]
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197


임계값을 낮추니 재현율 값이 올라가고 정밀도가 떨어졌습니다.

이유가 뭘까요? 분류 결정 임계값Positive 예측값을 결정하는 확률의 기준 이 됩니다. 확률이 0.5가 아닌 0.4부터 Positive로 예측을 더 너그럽게 하기 때문에 임계값 값을 낮출수록 True 값이 많아지게 됩니다.

image


Positive 예측값이 많아지면 상대적으로 재현율 값이 높아 집니다. 양성 예측을 많이 하다 보니 실제 양성을 음성으로 예측하는 횟수가 상대적으로 줄어들기 때문 입니다.

image


임계값이 0.5에서 0.4로 낮아지면서 TP 가 47에서 50으로 늘었고 FN 이 14에서 11로 줄었습니다. 그에 따라 재현율이 0.770에서 0.820으로 좋아졌습니다. 하지만 FP 는 10에서 21로 늘면서 정밀도가 0.825에서 0.704로 많이 나빠졌습니다. 그리고 정확도도 0.866에서 0.821로 나빠졌습니다.


이번에는 임계값을 0.4에서부터 0.6까지 0.05씩 증가시키며 평가 지표를 조사하겠습니다.

# 테스트를 수행할 모든 임곗값을 리스트 객체로 저장. 
thresholds = [0.4, 0.45, 0.50, 0.55, 0.60]

def get_eval_by_threshold(y_test, pred_proba_c1, thresholds):
    # thresholds list객체내의 값을 차례로 iteration하면서 Evaluation 수행.
    for custom_threshold in thresholds:
        binarizer = Binarizer(threshold=custom_threshold).fit(pred_proba_c1) 
        custom_predict = binarizer.transform(pred_proba_c1)
        print('\n임곗값:', custom_threshold)
        get_clf_eval(y_test, custom_predict)

get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197

임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도: 0.7869, 재현율: 0.7869

임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705

임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도: 0.8654, 재현율: 0.7377

임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도: 0.8980, 재현율: 0.7213


재현율을 향상시키면서 다른 수치를 어느 정도 감소하는 희생을 해야 한다면 임계값 0.45가 가장 적당해 보입니다. 지금까지 임계값 변화에 따른 평가 지표 값을 알아보는 코드를 작성했는데, 사이킷런은 이와 유사한 precision_recall_curve() API 를 제공합니다.

  • precision_recall_curve()
    • y_true : 실제 클래스값 배열
    • probas_pred : Positive 칼럼의 예측 확률 배열


precision_recall_curve() 를 이용해 타이타닉 예측 모델의 임계값별 정밀도와 재현율을 구해 보겠습니다. 일반적으로 0.11 ~ 0.95 정도의 임계값을 담은 넘파이 ndarray이 임계값에 해당하는 정밀도 및 재현율 값을 담은 넘파이 ndarray 를 반환합니다.

from sklearn.metrics import precision_recall_curve

# 레이블 값이 1일때의 예측 확률을 추출 
pred_proba_class1 = lr_clf.predict_proba(X_test)[:, 1] 

# 실제값 데이터 셋과 레이블 값이 1일 때의 예측 확률을 precision_recall_curve 인자로 입력 
precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_class1)
print('반환된 분류 결정 임곗값 배열의 Shape:', thresholds.shape)
print('반환된 precisions 배열의 Shape:', precisions.shape)
print('반환된 recalls 배열의 Shape:', recalls.shape)

print("\nthresholds 5 sample:", thresholds[:5])
print("precisions 5 sample:", precisions[:5])
print("recalls 5 sample:", recalls[:5])

# 반환된 임계값 배열 로우가 147건이므로 샘플로 10건만 추출하되, 임곗값을 15 Step으로 추출. 
thr_index = np.arange(0, thresholds.shape[0], 15)
print('\n샘플 추출을 위한 임계값 배열의 index 10개:', thr_index)
print('샘플용 10개의 임곗값: ', np.round(thresholds[thr_index], 2))

# 15 step 단위로 추출된 임계값에 따른 정밀도와 재현율 값 
print('\n샘플 임계값별 정밀도: ', np.round(precisions[thr_index], 3))
print('샘플 임계값별 재현율: ', np.round(recalls[thr_index], 3))
[output]
반환된 분류 결정 임곗값 배열의 Shape: (165,)
반환된 precisions 배열의 Shape: (166,)
반환된 recalls 배열의 Shape: (166,)

thresholds 5 sample: [0.01974988 0.06956414 0.08402808 0.08474207 0.08920161]
precisions 5 sample: [0.34078212 0.34269663 0.34463277 0.34659091 0.34857143]
recalls 5 sample: [1. 1. 1. 1. 1.]

샘플 추출을 위한 임계값 배열의 index 10개: [  0  15  30  45  60  75  90 105 120 135 150]
샘플용 10개의 임곗값:  [0.02 0.11 0.13 0.14 0.16 0.24 0.32 0.45 0.62 0.73 0.87]

샘플 임계값별 정밀도:  [0.341 0.372 0.401 0.44  0.505 0.598 0.688 0.774 0.915 0.968 0.938]
샘플 임계값별 재현율:  [1.    1.    0.967 0.902 0.902 0.902 0.869 0.787 0.705 0.492 0.246]


추출된 임계값 샘플 10개에 해당하는 정밀도 값과 재현율 값을 살펴보면 임계값이 증가할수록 정밀도값은 동시에 높아지나 재현율 값은 낮아짐을 알 수 있습니다. 정밀도와 재현율 곡선을 시각화해 보겠습니다.

import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%matplotlib inline

def precision_recall_curve_plot(y_test, pred_proba_c1):
    # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출. 
    precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1)
    
    # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시
    plt.figure(figsize=(14, 8))
    
    threshold_boundary = thresholds.shape[0]
    plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision')
    plt.plot(thresholds, recalls[0:threshold_boundary],label='recall')
    
    # threshold 값 X 축의 Scale을 0.1 단위로 변경
    start, end = plt.xlim()
    plt.xticks(np.round(np.arange(start, end, 0.1), 2))
    
    # x축, y축 label과 legend, 그리고 grid 설정
    plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value')
    plt.legend(); plt.grid()
    plt.show()
    
precision_recall_curve_plot( y_test, lr_clf.predict_proba(X_test)[:, 1] )

image


임계값이 낮을수록 많은 수의 양성 예측으로 인해 재현율 값이 극도로 높아지고 정밀도 값이 극도로 낮아지며, 임계값이 증가할수록 재현율 값이 낮아지고 정밀도 값이 높아지는 반대의 양상 이 됩니다.


정밀도와 재현율의 맹점

앞에서도 봤듯이 Positive 예측의 임계값을 변경함에 따라 정밀도와 재현율의 수치가 변경됩니다. 임곗값의 변경은 업무 환경에 맞게 두 개의 수치를 상호 보완할 수 있는 수준에서 적용돼야 합니다. 그렇지 않고 단순히 하나의 성능 지표 수치를 높이기 위한 수단으로 사용돼서는 안 됩니다. 다음은 정밀도 또는 재현율 평가 지표 수치 중 하나를 극단적으로 높이는 방법이지만 숫자놀음에 불과한 방법입니다.

정밀도가 100%가 되는 방법

확실한 기준이 되는 경우만 Positive로 예측하고 나머지는 모두 Negative로 예측합니다. 예를 들어 환자가 80세 이상이고 비만이며 이전에 암 진단을 받았고 암 세포의 크기가 상위 0.1% 이상이면 무조건 Positive, 다른 경우는 Negative 로 예측하는 겁니다.

$정밀도 = TP / (TP + FP)$ 입니다. 전체 환자 1000명 중 확실한 Positive 징후만 가진 환자는 단 1명이라고 하면 이 한 명만 Positive 로 예측하고 나머지는 모두 Negative 로 예측하더라도 FP 는 0, TP 는 1이 되므로 정밀도는 1/(1+0)으로 100% 가 됩니다.

재현율이 100%가 되는 방법

모든 환자를 Positive로 예측하면 됩니다. $재현율 = TP / (TP + FN)$ 이므로 전체 환자 1000명을 전부 Positive 로 예측하는 겁니다. 이 중 실제 양성인 사람이 30명 정도라도 TN 이 수치에 포함되지 않고 FN 은 아예 0이므로 재현율은 30/(30+0)으로 100% 가 됩니다.


이처럼 정밀도와 재현율 성능 수치도 어느 한쪽만 참조하면 극단적인 수치 조작이 가능합니다. 따라서 정밀도 또는 재현율 중 하나만 스코어가 좋고 다른 하나는 스코어가 나쁜 분류는 성능이 좋지 않은 분류로 간주할 수 있습니다. 정밀도와 재현율의 수치가 적절하게 조합돼 분류의 종합적인 성능 평가에 사용될 수 있는 평가 지표가 필요합니다.


F1 스코어

F1 스코어(Score)정밀도와 재현율을 결합한 지표 입니다. F1 스코어는 정밀도와 재현율이 어느 한쪽으로 치우치지 않는 수치를 나타낼 때 상대적으로 높은 값 을 가집니다. F1 스코어의 공식은 다음과 같습니다.

image


만일 A 예측 모델의 경우 정밀도가 0.9, 재현율이 0.1로 극단적인 차이가 나고, B 예측 모델은 정밀도가 0.5, 재현율이 0.5로 정밀도와 재현율이 큰 차이가 없다면 A 예측 모델의 F1 스코어 는 0.18이고, B예측 모델의 F1 스코어 는 0.5로 B 모델이 A모델에 비해 매우 우수한 F1 스코어를 가지게 됩니다.

사이킷런은 F1 스코어를 구하기 위해 f1_score() API 를 제공합니다. 이를 이용해 학습/예측한 로지스틱 회귀 기반 타이타닉 생존자 모델의 F1 스코어를 구해 보겠습니다.

from sklearn.metrics import f1_score

f1 = f1_score(y_test, pred)
print('F1 스코어: {0:.4f}'.format(f1))
[output]
F1 스코어: 0.7966


이번에는 타이타닉 생존자 예측에서 임계값을 변화시키면서 F1 스코어를 포함한 평가 지표를 구해 보겠습니다.

def get_clf_eval(y_test, pred):
    confusion = confusion_matrix(y_test, pred)
    accuracy = accuracy_score(y_test, pred)
    precision = precision_score(y_test, pred)
    recall = recall_score(y_test, pred)
    
    # F1 스코어 추가
    f1 = f1_score(y_test,pred)
    print('오차 행렬')
    print(confusion)
    # f1 score print 추가
    print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1:{3:.4f}'.format(accuracy, precision, recall, f1))

thresholds = [0.4 , 0.45 , 0.50 , 0.55 , 0.60]
pred_proba = lr_clf.predict_proba(X_test)
get_eval_by_threshold(y_test, pred_proba[:,1].reshape(-1, 1), thresholds)
[output]
임곗값: 0.4
오차 행렬
[[97 21]
 [11 50]]
정확도: 0.8212, 정밀도: 0.7042, 재현율: 0.8197, F1:0.7576

임곗값: 0.45
오차 행렬
[[105  13]
 [ 13  48]]
정확도: 0.8547, 정밀도: 0.7869, 재현율: 0.7869, F1:0.7869

임곗값: 0.5
오차 행렬
[[108  10]
 [ 14  47]]
정확도: 0.8659, 정밀도: 0.8246, 재현율: 0.7705, F1:0.7966

임곗값: 0.55
오차 행렬
[[111   7]
 [ 16  45]]
정확도: 0.8715, 정밀도: 0.8654, 재현율: 0.7377, F1:0.7965

임곗값: 0.6
오차 행렬
[[113   5]
 [ 17  44]]
정확도: 0.8771, 정밀도: 0.8980, 재현율: 0.7213, F1:0.8000

Read more