728x90
반응형
SMALL
728x90
반응형
SMALL

이번 블로그에서는 퍼셉트론에 대하여 알아보도록 하겠습니다.

 

퍼셉트론의 개념은 1957년도에 구현된 알고리즘이며, 신경망(딥러닝)의 기원이 되는 알고리즘이라고 할 수 있습니다. 즉, 퍼셉트론의 구조를 배워야만 신경망과 딥러닝으로 확장되는 아이디어를 생각해 낼 수 있는 중요한 기초 내용이 됩니다.

 

퍼셉트론이란?

퍼셉트론(인공 뉴런 이라고도 합니다)은 다수의 신호를 받아 하나의 신호를 출력합니다.

여기서 신호는 뇌과학에서는 일정한 자극, 신경망에서는 feature(특성)가 된다고 할 수 있는데요, 어떤 흐름을 타고 데이터(신호 - feature)가 들어와 처리된 결과를 다음 퍼셉트론으로 보내는 것으로 생각해 보면 됩니다.

보통 전류와 비교하게 되는데, 전류는 보통 실수값( ex : n볼트의 전류 )이 흐르지만, 지금 배워볼 우리의 퍼셉트론에서는 오로지 흐른다(1) / 흐르지 않는다.(0)으로 표현합니다.

이번 장에서는 1 => 흐른다 / 0 => 흐르지 않는다.로 표현하겠습니다.

 

신호가 퍼셉트론을 통과하는 과정

우리의 뇌는 여러가지 신호를 동시에 받아, 뇌에 있는 뉴런뉴런이라는 존재가 이 신호들을 처리합니다.

생물학적으로 뇌에 부담이 가지 않는 범위를 결정지어 주기 위해 신호별 가중치(weight)가 존재하는데요, 이 가중치가 존재하는 이유는 뇌가 받은 전기적 신호에 대해 적절하게 반응하기 위해 설정되어야 하기 때문입니다.

예를 들어 친구들과 게임을 하다가 이마를 맞았을 때( 입력 신호 ) 아픈 느낌이 점점 사라져야겠죠? 처음 입력받은 신호(자극)가 계속 사라지지 않고 유지되면 살 수가 없습니다. 따라서 처음 이마를 맞았을 때 받은 신호를 점점 줄여가는 식으로 가중치라는 것이 각 신호마다 곱해지게 됩니다.

이제는 퍼셉트론에 빗대어서 이야기해보겠습니다. 입력 신호( 입력 특성 )가 두 가지라고 가정해 보겠습니다. 그리고 이 신호들을 각각 x1, x2라고 가정해 보겠습니다.

퍼셉트론은 뇌의 뉴런과 대응되기 때문에 각각의 신호에 대한 가중치도 있다고 가정해 볼 수 있는데요. 이 각각의 가중치들을 w1, w2라고 가정하겠습니다.

 

본격적으로 퍼셉트론을 알아보자~!

입력 신호( x1, x2 )와 각 신호들에 대한 가중치( w1, w2 )가 준비되었습니다.신경망에서 입력 신호를 가중치와 함께 처리하여 출력하는 것을 뉴런 또는 노드라고 합니다. 즉 퍼셉트론 = 뉴런(노드)라고 이야기할 수 있습니다.

퍼셉트론에서 입력 신호를 받을 때는 각 가중치가 곱해지면서 받아집니다. 수식으로 표현하면 각 퍼셉트론마다 $w_1x_1 + w_2x_2$ 의 계산이 된다고 볼 수 있겠네요

 

퍼셉트론의 출력 결과는 흐른다(1) / 안 흐른다(0)

입력 특성과 가중치가 곱해져서 퍼셉트론에 흘러 들어오고, 거기에 대한 결과값이 나온 다는 것이 확인되었습니다.

여기서 중요한 것은, 각 퍼셉트론마다정해진 한계값이라는 것이 있습니다. 이것을 임계값( $\theta$ theta )이라고 합니다. 퍼셉트론로 들어온 입력 값들과 가중치가 곱해진 결과물의 합이 임계값 $\theta$를 넘어가면 0으로 출력, 넘어가지 않으면 1로 출력됩니다. 수식으로 표현하면 다음과 같습니다.

이를 반응 조건 계산식이라고 하겠습니다.

위의 수식을 일반화한 그래프를 그려 보도록 하겠습니다. 각 특성별 가중치는 임의로 0.5, 0.5로 설정하고, 임계값 𝜃는 2로 임의로 설정하겠습니다.

import matplotlib.pyplot as plt
import numpy as np
  • 사전에 알 수 있는 값 : 특성 값
  • 가중치는 아직 우리가 몰라요 : 모델이 학습하는 값

def func1(x1, x2):
    w1, w2, theta = 0.5, 0.5,  2 # 가중치를 각각 0.5, 0.5로 설정하고, 임계값은 2로 설정
    y = w1*x1 + w2*x2
    
    return y, y >= theta

# 입력 데이터 생성
x1 = np.linspace(-3, 7, 100)
x2 = np.linspace(-3, 7, 100)

y_value, y_result = func1(x1, x2)
plt.plot(y_value, y_result)
plt.yticks([0, 1])
plt.xticks([-3,2,7], [r'-$\infty$', r'$\theta$', r'-$\infty$'])
plt.xlabel(r"$w_1x_1+w_2x_2$")
plt.ylabel("y", rotation=0)
plt.show()

중간에 𝜃 를 기준으로 𝑤1𝑥1+𝑤2𝑥2의 결과가(y축) 0, 1로 결정지어지는 것이 확인됩니다. 또한 위 그래프를 함수로 일반화하면 다음과 같습니다. 이러한 함수를 단위 계단 함수(u)라고 합니다.

계산 식 𝑧의 결과가 계단 함수 u의 인자로 전달되었을 때 0보다 크면 1, 0보다 작으면 0으로 설정되는 것이 확인됩니다. 계단 함수 u(𝑧)의 그래프는 다음과 같습니다.

z = np.linspace(-3, 3, 100)
plt.plot(z, z > 0)
plt.yticks([0, 1])
plt.xticks([-3, 0, 3],[r'-$\infty$', r'$0$', r'-$\infty$'])
plt.xlabel(r"$z$")
plt.ylabel(r"$y=\mathcal{u}(z)$", rotation=0)
plt.show()

이항 법칙에 따라 반응 조건 계산식의 𝜃를 이항 하고, 계단 함수를 적용하면 다음과 같은 수식이 완성됩니다.

결과적으로 각 퍼셉트론의 입력 특성(x1, x2)과 가중치(w1, w2)를 이용한 결과를 계단 함수 u로 표현하기 위한 인자 𝑧


 

가 됩니다. 참고로 계단 함수의 인자로 들어가는 것을 퍼셉트론의 가중입력 이라고도 표현합니다. 계단 함수의 결과에 따라 𝑦의 결과물은 반응 조건 계산식 𝑤1𝑥1+𝑤2𝑥2𝜃 에 따라 항상 0 또는 1의 값만 가지게 됩니다.

 

단층 퍼셉트론 vs 다층 퍼셉트론

퍼셉트론은 이처럼 입력 값에 따른 가중치를 이용해서 출력 값을 내게 됩니다. 이번 예제는 하나의 퍼셉트론만 이용하는 단층 퍼셉트론의 한계에 대해 알아보고, 이를 해결할 수 있는 다층 퍼셉트론에 대해 알아봅니다.

 

AND Gate

논리 회로 게이트 문제를 이용해 알아보도록 하겠습니다. 먼저 가장 유명한 게이트죠? AND 게이트의 진리표를 확인해 보겠습니다. 

AND GATE 입력 𝑥1,  𝑥2, 출력 𝑦

AND 게이트의 특징은 입력값 𝑥1, 𝑥2가 둘 다 1일 때만 결과물이 1이 나옵니다. 이제 저희가 해야 할 일은 퍼셉트론에 적용되는 매개변수인 각각의 가중치들과 임계값을 정하는 일들입니다. AND 게이트의 매개변수의 조합은 무수히 많습니다. 여기는 𝑤1, 𝑤2를 각각 0.5로 설정하고, 𝜃는 0.7로 사용해 보겠습니다. 다음은 NAND와 OR 게이트입니다.

 

NAND Gate

NAND 게이트는 Not AND라는 뜻을 가지고 있으며, AND 게이트의 반대 동작을 하게 됩니다. 다음은 NAND 게이트의 진리표입니다.

NAND GATE 입력 𝑥1,  𝑥2, 출력 𝑦

입력 특성 𝑥1 𝑥2가 둘 다 1일 때만 0이고, 나머지 상황에 대해서는 모두 1이 출력되는 것이 확인됩니다. NAND 게이트의 가중치와 임계치는 AND 게이트 매개변수 부호의 반대를 넣어 주면 됩니다. 즉 위의 AND 게이트의 가중치와 임계값의 부호를 바꾼 NAND 게이트의 𝑤1, 𝑤2, 𝜃 는 각각 -0.5, -0.5, -0.7로 설정되면 됩니다.

 

OR Gate

OR 게이트는 입력 신호 중 하나 이상이 1 이상이면 출력이 1이 되는 게이트입니다. 진리표를 바로 확인해보겠습니다.

OR GATE 입력 𝑥1,  𝑥2, 출력 𝑦

각 진리표를 이용해 모두 퍼셉트론으로 AND, NAND, OR 게이트를 표현할 수 있습니다. 여기서 중요하게 생각해야 할 것은 퍼셉트론의 구조 자체는 AND, NAND, OR 게이트 모두 똑같다는 것입니다. 다른 점은 각 게이트의 매개변수(가중치와 임계값)만 다르다는 점입니다. 즉, 퍼셉트론의 매개변수 값만 적절히 조절하면 하나의 퍼셉트론이 AND, NAND, OR로 변신할 수 있다는 것입니다.

 

Gate의 퍼셉트론화

먼저 가장 기본 공식인 𝑤1𝑥1+𝑤2𝑥2𝜃를 함수와 시킨 AND게이트의 퍼셉트론입니다.

def AND(x1, x2):
    w1, w2, theta = 0.5, 0.5, 0.7
    tmp = w1 * x1 + w2 * x2 - theta
    
    if tmp <= 0 :
        return 0
    elif tmp > 0 : 
        return 1

AND(0, 0), AND(1, 0), AND(0, 1), AND(1, 1)

생각했던 대로 잘 동작하는 것 같습니다! NAND와 OR 게이트도 매개변수만 적절히 조절하면 똑같이 만들 수 있겠습니다만, 위의 함수를 약간만 손보도록 하겠습니다.

편향(bias) 도입

나름 𝑤1𝑥1+𝑤2𝑥2𝜃 같은 계산 식도 활용하기는 좋지만, 𝜃  𝑏로 치환하는 것이 더 보기도 좋고 일반적입니다. 즉, 우리가 앞으로 볼 퍼셉트론의 일반화된 식은


 

로 표현 됩니다. 결론을 내자면 퍼셉트론은 입력 신호에 가중치를 곱한 값과 편향(bias)을 합하여, 그 값이 0을 넘으면 1로, 그렇지 않으면 0이 출력된다고 일반화해볼 수 있겠습니다. 약간 프로그래밍 적으로, 입력 특성과 가중치를 두 개만 사용했지만, 여러 개의 입력 특성과 가중치가 있을 수 있으므로, 넘파이 배열을 이용해서 계산해보도록 하겠습니다. 위의 편향을 사용한 일반화된 공식에서 배열 곱에 대해 잠시 살펴보겠습니다. 위에서 구현한 AND 함수의 매개변수를 그대로 사용합니다.

# theta를 -b로 치환시킨 AND 게이트
def AND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.7
    
    tmp = np.sum(w*x) + b # 계산
    
    if tmp <= 0:
        return 0
    else:
        return 1
    
AND(0, 0), AND(1, 0), AND(0, 1), AND(1, 1)

간단히 𝜃가 편향 𝑏로 치환된 것을 확인할 수 있습니다!

여기서 중요한 점은 가중치와 편향의 기능은 다르다는 것인데요, 가중치의 역할은 입력 신호가 결과에 주는 영향력(중요도)을 조절하는 매개변수고, 편향은 퍼셉트론이 얼마나 쉽게 활성화( 1로 출력 ) 되는가를 조정하는 매개변수입니다. 즉 편향이 크면 퍼셉트론의 흥분도가 커져 쉽게 활성화되고( 민감 ), 편향이 작으면 흥분도가 낮아 활성화가 잘 안 된다(둔감) 고 생각하면 됩니다. 이어서 NAND와 OR Gate를 모두 구현해 보도록 하겠습니다.

def NAND(x1, x2):
    x = np.array([x1, x2])
    w = np.array([-0.5, -0.5]) # AND 게이트의 매개 변수 부호를 반대로
    b = 0.7 
    
    tmp = np.sum(w * x) + b
    if tmp <= 0:
        return 0
    else:
        return 1

NAND(0, 0), NAND(1, 0), NAND(0, 1), NAND(1, 1)

def OR(x1, x2):
    x = np.array([x1, x2])
    w = np.array([0.5, 0.5])
    b = -0.2 # 적절하게 편향만 조절하여 OR 게이트를 구현!
    tmp = np.sum(w*x) + b
    
    if tmp <= 0:
        return 0
    else:
        return 1

OR(0, 0), OR(1, 0), OR(0, 1), OR(1, 1)

단층 퍼셉트론

여태 AND, NAND, OR 게이트를 이야기하다 갑자기 단층 퍼셉트론이라는 주제로 바뀌었습니다.

하지만 여러분들이 구현한 위의 함수들이 곧 퍼셉트론이 된다는 사실! 그렇다면 단층 퍼셉트론이 어떤 것인지 알아보죠, 위의 각 퍼셉트론들은 각각 입력값을 받아 각자의 역할을 수행합니다. 즉 한 번의 입력에 한번의 출력이 이루어 진다는 이야기가 되겠죠. 이처럼 한번의 입력, 한번의 결과를 내는 퍼셉트론들을 단층 퍼셉트론이라고 합니다.

 

단층 퍼셉트론의 한계, XOR 게이트 구현

XOR 게이트를 하나의 퍼셉트론에서 구현할 수 있을까요? XOR 게이트란 배타적 논리합이라는 논리회로로써𝑥1 𝑥2 중 한쪽의 값만 1일 때 결과가 1이 됩니다. 즉 𝑥1, 𝑥2 가 똑같은 입력이라면 그 결과가 0이 된다고 볼 수 있습니다. 다음은 XOR 게이트의 진리표입니다.

XOR GATE 입력 𝑥1, 𝑥2, 출력 𝑦

지금까지 AND, NAND, OR 게이트를 만들었던 것처럼 XOR 게이트도 하나의 퍼셉트론으로 처리할 수 있을까요?

 

정답은 XOR 게이트는 단일 퍼셉트론으로 처리할 수 없다!

임의로 OR 게이트의 매개변수 𝑤1,𝑤2, 𝑏 = ( 1.0, 1.0, -0.5 )로 설정했다고 생각해 보겠습니다. 𝑥1+𝑥2−0.52−0.5라는 식으로 표현가능 할 것 같습니다. 간단히 결과만 살펴보면 𝑥1 또는 𝑥2 가 1만 넘어가면 결과가 양수가 되기 때문에 좌표평면에 그래프를 그려보면 완벽하게 직선 그래프를 이용해 간단히 그려질 것 같습니다.

  • XOR는 어떻게 구현해야 할까
  • XOR : 배타적 논리합
# OR 시각화

plt.figure(figsize=(4,4))

plt.scatter([0],[0], marker='o')
plt.scatter([1,0,1],[0,1,1], marker='^')
plt.xticks([0, 0.5, 1])
plt.yticks([0, 0.5, 1])
plt.xlim((-0.2,1.2))
plt.ylim((-0.2,1.2))
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$', rotation=0)

plt.show()

직선 한 개를 그어서 위의 점들을 두 분류(동그라미, 세모)로 나눌 수 있을까요? 당연히 할 수 있습니다..! 이처럼 OR 게이트를 만들면 직선으로 1 또는 0으로 나눠지는 구간을 그려줄 수 있습니다.

 

하지만 XOR은 이야기가 좀 다릅니다!

XOR 진리표를 이용해 그래프를 찍어 보면 다음과 같습니다.

# XOR 시각화
plt.figure(figsize=(4,4))

plt.scatter([0,1],[0,1], marker='o')
plt.scatter([1,0],[0,1], marker='^')
plt.xticks([0, 0.5, 1])
plt.yticks([0, 0.5, 1])
plt.xlim((-0.2,1.2))
plt.ylim((-0.2,1.2))
plt.xlabel(r'$x_1$')
plt.ylabel(r'$x_2$', rotation=0)

plt.show()

XOR은 위의 그래프처럼 교차 되어있는 형태 이기 때문에 하나의 직선으로 점들을 구분하기는 불가능합니다.

단층 퍼셉트론의 한계가 보이시나요?

퍼셉트론 자체가 직선 하나의 영역으로 나눈 영역만 설정할 수 있다는 한계가 있습니다. 위의 XOR 같은 경우는 곡선 또는 여러 번의 직선을 그어야 구분할 수 있는데요, 이때 직선 하나로만 영역을 나누는 것을 선형으로 표현하고, 그 외에 곡선이나 끊긴 직선들은 비선형으로 표현합니다. 따라서 단층 퍼셉트론으로는 XOR 게이트를 표현할 수 없다. 즉 단층 퍼셉트론으로는 비선형 영역을 분리할 수 없다로 이해할 수 있겠습니다.

어떻게 해결하는가?

단층 퍼셉트론의 층을 쌓으면 된다!

각 게이트에 대한 중요한 이야기를 다루지 않기 때문에, 결과적으로만 이해해보겠습니다. XOR 게이트는 지금까지 위에서 이야기했었던 AND, NAND, OR 게이터를 적절하게 조합하면 구현해 낼 수 있습니다. 방법은 바로 NAND의 결과와 OR의 결과를 AND 게이트의 결과물로 사용하면 됩니다. 진리표로 확인해 보겠습니다.

입력&nbsp; 𝑥1, &nbsp; 𝑥2 &nbsp;=> NAND 결과 𝑠1/ OR 결과 𝑠2/ AND 결과 𝑦

 

𝑥1 𝑥2의 입력 값에 따라 출력되는 결과물 𝑦의 값이 위에 작성했던 XOR 진리표와 일치하는 것을 볼 수 있습니다. XOR 게이트를 함수로 구현하면 다음과 같습니다.

def XOR(x1, x2):
    s1 = NAND(x1, x2)
    s2 = OR(x1, x2)
    y = AND(s1, s2)
    
    return y

XOR(0, 0), XOR(1, 0), XOR(0, 1), XOR(1, 1)

정확하게 XOR이 구현된 것이 확인됩니다!

입력값 𝑥1, 𝑥2에 대해 각각 다른 류의 퍼셉트론인 NAND, OR 퍼셉트론을 통과시켰고, 그 결과를 다시 하나의 퍼셉트론인 AND 게이트에 통과시켰습니다. 이처럼 두 개 이상의 퍼셉트론 층이 생기는 구조를 다층 퍼셉트론이라고 합니다.

 

정리하자면 NAND, OR 게이트는 𝑥1, 𝑥2를 전달받아 각자의 작업을 한 후 최종적으로 AND 게이트에 부품을 전달하는 역할만 수행하게 되고 AND 게이트는 전달받은 부품으로 최종 결과물을 수행해 내는 역할을 하게 됩니다. 위처럼 2층 이상의 다층 퍼셉트론을 이용하면 단층 퍼셉트론으로는 표현하지 못한 것을 층을 늘려 구현할 수 있습니다. 퍼셉트론은 층을 쌓아 더 다양한 것을 표현해 낼 수 있습니다.

 

지금까지 단층 & 다층 퍼셉트론에 대하여 알아보았습니다. 다음 포스팅에서는 신경망과 활성화 함수에 대하여 알아보도록 하겠습니다~ ;)

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 딥러닝에서 필연적으로 마주하게 되는 기본 Numpy에 대하여 알아보도록 하겠습니다.

 

numpy는 내부적으로 C로 구현이 되어 있으며, 수치에 관련된 프로그래밍을 할 때 일반 python보다 훨씬 빠릅니다.

특히, 배열 연산에 특화되어 있습니다.

한번 예제를 통하여 바로 알아보도록 하겠습니다!

 
import numpy as np

# 일반 파이썬 리스트를 numpy 배열화 ( numpy 배열 : ndarray )
py_list = [1, 2, 3]
arr1 = np.array(py_list)
arr1

# ndarray 타입 확인하기
type(arr1)

py_list = [ [1, 2, 3],
            [4, 5, 6] ]

arr2 = np.array(py_list)
type(arr2)

numpy에서는 숫자 배열을 만들기 위한 여러 가지 함수를 제공합니다.

np.arrange()
# np.arange 함수는 파이썬의 range 함수와 매우 흡사하다.
arr = np.arange(10) # 0 ~ 9 까지 들어있는 숫자 배열을 생성
arr

arr = np.arange(1, 10) # 1 ~ 9 까지 들어있는 숫자 배열을 생성
arr

arr = np.arange(1, 10, 2) # 1부터 9까지 2씩 띄워가면서 숫자 배열 생성
arr


다차원 배열 만들기
  • np.zeros
  • np.ones
  • np.full
np.zeros: 지정한 차원에 모든 원소를 0으로 채워주는 함수
np.zeros(5) # 1차원 배열에 5개의 0을 채운다.

np.zeros((4, 5)) # 4행 5열(2차원 배열)의 배열에 0을 채운다.

np.zeros((3, 4, 5))

np.ones: 지원한 차원에 모든 원소를 1로 채워주는 함수
np.ones(5)

np.ones((4,5))

np.ones((4, 5)) * 3

np.full: 지정한 차원에 모든 원소를 원하는 수로 채우는 함수
np.full((3,4),7)


단위행렬 만들기
  • np.eye
  • 단위 행렬 : 대각선 방향의 원소가 1이고, 나머지 원소는 0인 행렬
np.eye(3)

np.eye(3, 4)


구간 나누기
  • np.linspace
np.linspace(1, 10, 3) # 1부터 10까지 2개의 구간 만들기

np.linspace(1, 10, 4)


Random을 사용하기 위한 numpy
  • 보통 랜덤한 배열은 딥러닝에서 매개변수의 초기화를 하기 위해 사용
  • 딥러닝에서는 일반적으로 Xavier 초깃값, He 초깃값, 정규분포 초깃값을 계산해 준다.
np.random.rand: 완전한 랜덤 만들기 (정규분포나 균등분포가 아닌 랜덤값)
np.random.randn(10)

np.random.uniform: 균등분포 랜덤값 만들기
np.random.uniform(1.0, 3.0, size=(4, 5)) # 1.0 ~ 3.0 까지 균등분포 랜덤값 생성


정수 랜덤 만들기
np.random.randint
np.random.randint(1, 100, size=(5, )) # 1차원 배열의 원소 5개에 1 ~ 100까지의 랜덤값으로 채우기

np.random.randint(1, 100, size=(5, 1)) # 2차원 배열에 랜덤 정수 채우기

np.random.randint(1, 100, size=(3, 4, 5))


정수 랜덤 샘플링 하기
np.random.choice
  • 랜덤 샘플링 : 일정한 범위 내에서 필요한 정수를 랜덤으로 추출
  • 미니배치를 만들 때 사용
np.random.choice(100, size=(3, 4))

# 임의의 배열을 만들어서 그 안에서 랜덤하게 추출할 수 있도록 함
arr = np.arange(1, 6)

np.random.choice(arr, size=(2, 2), replace=False) # replace가 True면 원소를 중복 추출, False면 중복 추출 X


인덱스와 슬라이싱
arr = np.arange(1, 11).reshape(2, 5)
arr

arr[:, 2] # 2차원 배열 내에서 1차원 배열은 전체 선택, 0차원 스칼라값은 2번째 인덱스에 있는 것을 선택

arr = np.arange(1, 37).reshape(3, 4, 3)
arr

arr[:, :2, :2]


배열의 차원과 형상
  • 형상( shape )
  • 차원수( ndim )
arr = np.arange(12).reshape(3, 4)
arr

arr.shape

arr.ndim


차원수 확장하기
  • 항상 같은 예시는 아니지만, 보통은 이미지 분석( CNN )을 하기 위해 데이터를 전처리 하면서 사용된다.
  • tensor flow는 tf.newaxis를 활용, numpy는 np.newaxis를 활용해서 추가 차원을 만들어 낼 수 있다.
arr

arr.shape

tmp_arr = arr[np.newaxis, :, :, np.newaxis] # arr의 첫 번째와 마지막에 차원을 1씩 확장
tmp_arr

# 실제 데이터를 다룰 때는 실제 데이터를 보는 것이 아닌, shape만 보자..
tmp_arr.shape

# 각 차원이 1인 차원 삭제하기
tmp_arr2 = np.squeeze(tmp_arr)
tmp_arr2.shape

spread 연산자 활용하기
  • 슬라이싱 도중에 특정 차원부터는 전체를 선택할 때 사용한다.
tmp_arr3 = arr[np.newaxis, ..., np.newaxis] # ... -> spread 연산자
tmp_arr3.shape


배열의 형상 (shape) 바꾸기
  • np.newaxis를 활용해서 축을 추가한다. ( 차원수를 늘린다. )

평탄화

  • ravel : 원본 배열을 평탄화시킨참조배열을 만들어 낸다. ( View 또는 Reference를 만들었다고 표현한다. )
  • flatten : 평탄화시킨 복사된배열을 만들어 낸다.

형상 변환

  • reshape
ravel 사용하기
x = np.arange(15).reshape(3, 5)
x.shape

print("원본 x : \n{}".format(x))

temp = np.ravel(x)
print("ravel로 평탄화된 x의 shape : {}".format(x.shape))
print("ravel로 평탄화된 x : \n{}".format(temp))

ravel함수는 원본의 참조본을 만들어 낸다.

print(temp[0])

temp[0] = 100
print(x)

flatten 사용하기
# flatten은 복사본을 만들어 낸다.
y = np.arange(15).reshape(3, 5)
temp2 = y.flatten()
print("원본 y : \n{}".format(y))
print("flatten으로 평탄화된 y의 shape : {}".format(temp2.shape))
print("flatten으로 평탄화된 y : \n{}".format(temp2))

temp2[0] = 100 # flatten을 활용해서 y의 복사본을 만들어 냈기 때문에 원본 y에는 영향을 미치지 않는다.
print(temp2)
print(y)

reshape 사용하기
x = np.arange(20)
x

reshape 팁 : reshape 함수 내의 모든 숫자를 곱했을 때 스칼라 원소의 개수와 같기만 하면 된다.

x.reshape(2, 5, 2)

-1 reshape()에 넣으면 남는 숫자를 자동으로 계산해 준다.

x.reshape(2, 2, -1) # 2 * 2 는 4니까, -1은 5를 의미하게 된다.

x.reshape(2, -1, 2)

x.reshape(3, -1) # 오류... 한쪽 차원의 수가 3이면 원소의 개수인 20만큼 딱 떨어지지 않기 때문에 오류


브로드 캐스팅
  • 차원 수가 다른 배열끼리의 연산
  • 차원이 달라도 0차원 스칼라의 개수가 똑같으면 연산이 가능
  • 저 차원의 배열을 고차원으로 확장
x = np.arange(20).reshape(4, 5)
y = np.arange(20, 40).reshape(4, 5)

print(x)
print()
print(y)

x.shape, y.shape

x + y

x - y

x * y

원소의 개수와 차원이 같으면 자연스럽게 원소끼리 연산을 할 수 있다.

x * 2

x는 2차원, 2는 0차원 스칼라 값이다. 2의 차원이 곱해지는 배열인 x shape으로 확장된다.

# shape이 다를 때의 연산
a = np.arange(12).reshape(4, 3) # ( 4, 3 )
b = np.arange(100, 103) #  (3, )
c = np.arange(1000, 1004) # (4, )
d = b.reshape(1, 3) # (1, 3)

# b와 d의 다른점 : b는 1차원, d는 2차원
a + b

a + c

a + d


전치행렬 만들기
  • 행렬 A의 행과 열의 인덱스를 바꾼 것을 전치행렬이라고 한다.
  • 𝐴(𝑖,𝑗)의 위치를 𝐴(𝑗,𝑖)로 바꾼 것
  • 보통 Transpose 했다고 이야기한다.
e = np.arange(6).reshape(2, 3)
e

# ndarray에서 T만 호출해 주면 된다.
e.T

특히 역전파에 사용된다!

지금까지 파이썬을 활용하여 딥러닝을 위한 numpy의 기초를 총 정리 해보았습니다.

다음 블로그에서는 퍼셉트론에 관하여 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

지난 앙상블 모델에 이어 이번 블로그에서는 서포트 벡터 머신에 대하여 다루어 보도록 하겠습니다!

2023.06.06 - [Programming/특성 공학] - [Machine Learning] 앙상블 모델(Random Forest & Gradient Boosting)

 

[Machine Learning] 앙상블 모델(Random Forest & Gradient Boosting)

앞선 결정 트리의 개념에 더하여 이번 블로그에서는 앙상블 모델에 대하여 알아보도록 하겠습니다. 앙상블(ensemble)이란? 앙상블은 여러 머신러닝 모델을 연결하여 더 강력한 모델은 만드는 데에

yuja-k.tistory.com

이전에 분류용 선형 모델에서 잠시 LinearSVC를 잠깐 사용해 보았습니다.

2023.05.16 - [Programming/특성 공학] - [Machine Learning] 다중 선형 분류

 

[Machine Learning] 다중 선형 분류

이번 포스팅은 다중 선형 분류에 대하여 알아보도록 하겠습니다 선형 이진 분류 또는 선형 회귀에 대해 알고 싶은 분들은 이전 포스팅들을 참고해 주세요~! 2023.05.09 - [Programming/특성 공학] - [Machi

yuja-k.tistory.com

지금 해볼 SVM에 포함되는 모델인데요, SVM은 선형 모델과 다르게 단순히 선이나 평면으로 분류를 하지 않는 더 복잡한 모델을 만들 수 있도록 확장한 것입니다.

서포트 벡터 머신도 마찬가지로 분류, 회귀에 모두 사용할 수 있습니다.

다른 모델과 다르게 SVM은 수학적 의미가 너무나 복잡하기 때문에 다루지는 않겠습니다. 사용법과 원리에 대해서만 이야기해 보겠습니다.

선형 모델과 비선형 특성

직선과 초평면은 유연하지 못하다는 것을 우리는 선형 모델을 통해 이미 확인해 보았습니다. 즉 저 차원 데이터에서는 선형 모델이 매우 제한적이게 되는데요, 선형 모델을 유연하게 만드는 방법은 특성끼리 곱하거나 특성 자체를 거듭제곱하는 식으로 특성을 추가하는 것입니다.

#필요 라이브러리 임포트
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
%matplotlib inline
plt.rcParams['axes.unicode_minus'] = False

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!')
from sklearn.datasets import make_blobs
X, y = make_blobs(centers=4, random_state=8)
y = y % 2

mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("특성 0")
plt.ylabel("특성 1")

선형 모델은 직선으로만 데이터 포인트를 나누기 때문에 이러한 데이터셋에는 잘 들어맞지 않습니다.

from sklearn.svm import LinearSVC
linear_svm = LinearSVC().fit(X, y)

mglearn.plots.plot_2d_separator(linear_svm, X)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)

plt.xlabel("특성 0")
plt.ylabel("특성 1")

이번에는 특성 1의 제곱을 새로운 특성으로 추가해서 입력 특성을 확장해 보겠습니다. 즉 기존 특성 0, 특성 1만 있는 것이 아닌 [ 특성 0, 특성 1, 특성 1 제곱 ] 데이터가 추가됩니다.

X_new = np.hstack([X, X[:, 1:] ** 2])

from mpl_toolkits.mplot3d import Axes3D, axes3d
figure = plt.figure()

#3차원 그래프
ax = Axes3D(figure, elev=-152, azim=-26)

# y == 0인 포인트를 그리고 다음 y == 1 인 포인트를 그리기
mask = y == 0
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b', cmap=mglearn.cm2, s=60, edgecolor='k')
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^', cmap=mglearn.cm2, s=60, edgecolor='k')

ax.set_xlabel("특성 0")
ax.set_ylabel("특성 1")
ax.set_zlabel("특성 1 ** 2")

새로운 데이터셋에서는 선형 모델과 3차원 공간의 평면을 사용해 두 클래스를 구별할 수 있을 것 같습니다. 선형 모델을 만들어 확인해 보겠습니다.

linear_svm_3d = LinearSVC().fit(X_new, y)
coef, intercept = linear_svm_3d.coef_.ravel(), linear_svm_3d.intercept_

# 선형 결정 경계 그리기
figure = plt.figure()
ax = Axes3D(figure, elev=-152, azim=-26)
xx = np.linspace(X_new[:, 0].min() - 2, X_new[:, 0].max() + 2, 50)
yy = np.linspace(X_new[:, 1].min() - 2, X_new[:, 1].max() + 2, 50)

XX, YY = np.meshgrid(xx, yy)
ZZ = (coef[0] * XX + coef[1] * YY + intercept) / -coef[2]
ax.plot_surface(XX, YY, ZZ, rstride=8, cstride=8, alpha=0.3)
ax.scatter(X_new[mask, 0], X_new[mask, 1], X_new[mask, 2], c='b', cmap=mglearn.cm2, s=60, edgecolor='k')
ax.scatter(X_new[~mask, 0], X_new[~mask, 1], X_new[~mask, 2], c='r', marker='^', cmap=mglearn.cm2, s=60, edgecolor='k')

ax.set_xlabel("특성 0")
ax.set_ylabel("특성 1")
ax.set_zlabel("특성 1 ** 2")

3차원으로 봐도 평면으로 결정 경계가 이루어지는 것을 볼 수 있습니다! SVM 모델을 사용하면 더 이상 선형이 아닌 직선 보다 타원에 가까운 비선형 모델을 만들어 냅니다.

ZZ = YY ** 2
dec = linear_svm_3d.decision_function(np.c_[XX.ravel(), YY.ravel(), ZZ.ravel()])
plt.contourf(XX, YY, dec.reshape(XX.shape), levels = [dec.min(), 0, dec.max()], cmap=mglearn.cm2, alpha=0.5)

mglearn.discrete_scatter(X[:, 0], X[:, 1], y)

plt.xlabel("특성 0")
plt.ylabel("특성 1")

커널 기법 활용하기

앞서 비선형 특성을 추가하여 선형 모델을 조금 더 강력하게 만들어 보았습니다. 이전처럼 확실하게 어떤 특성에 비선형을 적용시켜야 할지 알고 있으면 좋겠지만, 보통은 어떤 특성을 추가해야 할지 모르고, 그렇다고 특성을 많이 추가하면 연산 비용(cpu, 메모리 사용 등)이 커지게 됩니다.

다행히도 수학적 기교를 통해 새로운 특성을 많이 만들지 않고도 고차원에서 분류기를 학습시킬 수 있습니다. 이를 커널 기법(kernel trick)이라고 합니다.

실제로 데이터를 확장하지 않고 확장된 특성에 대한 데이터 포인트들의 거리( 스칼라 곱 )를 계산합니다.

SVM에서 데이터를 고차원 공간에 매핑하는 방법은 두 가지가 있습니다. 차수를 더 늘린다

  • 원래 특성의 가능한 조합을 지정된 차수까지 모두 계산(특성 1 제곱 * 특성 2 5제곱 등..) -> 다항식 커널
  • RBF(radial basis function ) -> 가우시안 커널

가우시안 커널은 차원이 무한한 특성 공간에 매핑하는 것으로, 모든 차수의 모든 다항식을 고려한다고 이해하면 됩니다. 하지만 특성의 중요도는 테일러급수 전개 때문에 고차항이 될수록 줄어듭니다.

복잡한가요? 실제로 수학적인 이론은 중요하지 않지만, RBF 커널을 사용한 SVM이 결정을 만드는 방법은 비교적 쉽게 요약이 가능합니다.

본격적 SVM 이해하기

학습이 진행되는 동안 SVM은 훈련 데이터 포인트가 두 클래스 사이의 결정 경계를 구분하는데 얼마나 중요한지를 배우게 됩니다.

일반적으로 훈련 데이터의 일부만 결정경계를 만드는데 영향을 주게 되는데, 바로 두 클래스 사이의 경계에 위치한 데이터 포인트들입니다.
이러한 데이터들을 서포트 벡터(support vector)라고 하며, 서포트 벡터 머신이 여기서 유래하였습니다.

새로운 데이터 포인트에 예측하려면 각 서포트 벡터와의 거리를 측정합니다. 분류에 대한 결정은 서포트 벡터까지의 거리에 기반하며 서포트 벡터의 중요도는 훈련 과정에서 학습하게 됩니다.(SVC객체의 dual_coef_ 속성에 저장)

데이터 포인트 사이의 거리는 가우시안 커널에 의해 계산됩니다.

𝑘(𝑥1,𝑥2)=exp(−𝛾||𝑥1−𝑥2||2) 수식으로 표현이 가능한데요, 여기서 𝑥1과 𝑥2는 데이터 포인트이며, ||𝑥1−𝑥2||는 유클리디안 거리, $\gamma$는 가우시안 커널의 폭을 제어하는 매개변수입니다.

이어서 forge 데이터셋에 SVM을 학습시켜 보겠습니다.

from sklearn.svm import SVC
X, y = mglearn.tools.make_handcrafted_dataset()
svm = SVC(kernel="rbf", C=10, gamma=0.1).fit(X, y)

mglearn.plots.plot_2d_separator(svm, X, eps=0.5)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)

#서포트 벡터
sv = svm.support_vectors_
# dual_coef_의 부호에 의해 서포트 벡터의 클래스 레이블이 결정됨
sv_labels = svm.dual_coef_.ravel() > 0
mglearn.discrete_scatter(sv[:, 0], sv_labels, s = 15, markeredgewidth=3)
plt.xlabel("특성 0")
plt.ylabel("특성 1")

위 그림에서 결정경계는 검은색 선으로, 서포트 벡터는 굵은 테두리를 가진 데이터 포인트로 그려 보았습니다.

SVM의 핵심 매개변수는 C와 gamma 값으로, 자세히 살펴보겠습니다

SVM 매개변수 튜닝

1) 𝛾매개변수의 역할

gamma 매개변수는 𝛾 로 가우시안 커널 폭의 역수에 해당합니다. gamma 매개변수가 하나의 훈련 샘플에 미치는 영향의 범위를 결정합니다.
작은 값은 넓은 영역을 뜻하며 큰 값이라면 영향이 미치는 범위가 제한적입니다.

정리하자면 가우시안 커널의 반경이 클수록 훈련 샘플의 영향 범위도 커지게 됩니다. gamma는 0보다 범위가 커야 하기 때문에 가우시안 커널 함수의 값의 범위는 𝑒0 ~ 𝑒−∞ 사이입니다 즉 1~0 사이라고 볼 수 있는데요, 따라서 gamma 값이 적어질수록 데이터 포인트의 영향 범위가 커집니다.

 

2) C 매개변수의 역할

 

C 매개변수는 선형 모델에서 사용한 것과 비슷한 규제 매개변수입니다. 이 매개변수는 각 포인트의 중요도(dual_coef_ 값)를 제한합니다.

C 매개변수를 다르게 했을 때 어떻게 변경되는지 살펴보겠습니다.

fig, axes = plt.subplots(3, 3, figsize=(15, 10))

for ax, C in zip(axes, [-1, 0, 3]) :
    for a, gamma in zip(ax, range(-1, 2)):
        mglearn.plots.plot_svm(log_C = C, log_gamma=gamma, ax=a)
        
axes[0, 0].legend(["클래스 0", "클래스 1", "클래스 0 서포트 벡터", "클래스 1 서포트 벡터"],
                  ncol=4, loc=(.9, 1.2))

왼쪽에서 오른쪽으로 가면서 gamma 매개변수를 0.1에서 10으로 증가시킨 것부터 확인하면, gamma 값은 가우시안 커널의 반경을 크게 하여 많은 포인트들이 가까이 있는 것을 고려하는 모습이 보입니다.

그래서 왼쪽에 있는 결정 경계는 매우 부드러워지고, gamma값이 커질수록 결정 경계는 하나하나의 데이터 포인트에 민감해져 결정 경계를 각각 그리는 것이 확인됩니다.

작은 gamma 값이 결정 경계를 천천히 바뀌게 하므로 모델의 복잡도를 낮추게 됩니다. 반면에 gamma 값이 커지면 더욱더 복잡해집니다.

위에서 아래로는 C 매개변수를 0.1에서 1000까지 증가시켰습니다. 선형 모델에서처럼 작은 C는 매우 제약이 큰 모델을 만들고 각 데이터 포인트의 영향력이 작습니다. 왼쪽 위의 결정경계는 거의 선형에 가까우며 잘못 분류된 데이터 포인트가 경계에 영향을 주지 않는 것을 확인할 수 있습니다. 왼쪽 아래에서 볼 수 있듯이 C값을 증가시키면 이 포인트들이 모델에 큰 영향을 주게 되며 결정경계를 휘어서 정확하게 분류합니다.


RBF 커널 SVM을 유방암 데이터셋에 적용해 보겠습니다. 기본값 C=1, gamma=1/n_ features를 사용해 보도록 하겠습니다.

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, random_state=0)

svc = SVC()
svc.fit(X_train, y_train)

print("훈련 세트 정확도: {:.2f}".format(svc.score(X_train, y_train)))
print("테스트 세트 정확도: {:.2f}".format(svc.score(X_test, y_test)))

훈련 세트에서는 완벽한 점수를 냈지만, 테스트 세트에서는 63% 의 정확도기 때문에 상당히 과대적합 된 것 같습니다. SVM은 꽤나 잘 작동하는 편이긴 하지만, 매개변수 설정과 데이터 스케일에 매우 민감합니다. 특히 입력 특성의 범위가 비슷해야 합니다. 각 특성의 최솟값과 최댓값을 로그 스케일로 나타내 보겠습니다.

plt.boxplot(X_train, manage_xticks=False)
plt.yscale("symlog")
plt.xlabel("특성 목록")
plt.ylabel("특성 크기")

각 특성의 최댓값 최솟값을 확인해 보면 자릿수 자체가 다른 것이 확인됩니다. 이것이 선형 모델등에서도 문제가 될 수 있지만, 커널 SVM에서는 영향이 매우 큽니다. 이 문제를 해결하기 위해서는 각 특성들에 대해 전처리(pre processing) 작업이 필요합니다.

SVM을 위한 유방암 데이터셋 전처리

위의 문제를 해결하는 방법은 각 특성 값의 범위가 비슷해지도록 조정하는 것입니다. 커널 SVM에서는 모든 특성의 값을 0과 1 사이로 맞추는 방법을 사용합니다. MinMaxScaler 메소드를 사용할 수 있지만, 여기서는 우리가 직접 구현해 보겠습니다.

MinMaxScaler 메소드의 공식은 다음과 같습니다.

# 훈련 세트에서 특성별 최솟값 계산
min_on_training = X_train.min(axis=0)

# 훈련 세트에서 특성별 (최댓값 - 최솟값) 범위 계산
range_on_training = (X_train - min_on_training).max(axis=0)

# 훈련 데이터에 최솟값을 빼고 범위로 나누면
# 각 특성에 대해 최솟값은 0, 최댓값은 1입니다.

X_train_scaled = (X_train - min_on_training) / range_on_training

print("특성별 최소 값 :\n{}".format(X_train_scaled.min(axis=0)))
print("특성별 최대 값 :\n{}".format(X_train_scaled.max(axis=0)))​
# 테스트 세트에도 같은 작업을 작용하지만
# 훈련 세트에서 계산한 최솟값과 범위를 사용합니다.

X_test_scaled = (X_test - min_on_training) / range_on_training​
svc = SVC()
svc.fit(X_train_scaled, y_train)

print("훈련 세트 정확도:{:.3f}".format(svc.score(X_train_scaled, y_train)))
print("테스트 세트 정확도:{:.3f}".format(svc.score(X_test_scaled, y_test)))​

전처리 과정을 통했더니 결과가 매우 크게 달라지는 것이 확인됩니다. 하지만 100% 정확도에서 조금 멀어졌지만 두 결과가 매우 비슷하기 때문에 확실히 과소적합된 상태라고 생각됩니다. 여기서 C나 gamma 값을 증가시켜 조금 더 복잡한 모델로 만들어 볼 수 있을 것 같습니다.

svc = SVC(C=1000)
svc.fit(X_train_scaled, y_train)
print("훈련 세트 정확도:{:.3f}".format(svc.score(X_train_scaled, y_train)))
print("테스트 세트 정확도:{:.3f}".format(svc.score(X_test_scaled, y_test)))

C 값을 증가시켰더니 모델의 성능이 97.2%로 상승하였습니다!

장단점과 매개변수

커널 서포트 벡터 머신은 매우 강력한 모델이며, 다양한 데이터셋에서 잘 작동합니다. SVM은 데이터의 특성이 몇 개 안 되더라도 복잡한 결정 경계를 만들 수 있으며 저 차원과 고차원의 데이터에 모두 잘 작동합니다.

하지만 샘플이 매우 많을 때는 잘 맞지 않습니다. 즉 10,000 개의 이상의 데이터셋에서는 잘 작동하지만, 100,000개 이상의 데이터셋에는 잘 작동하지 않을 수 있다는 측면이 있습니다. (속도와 메모리 관점에서..)

SVM의 또 다른 단점은 데이터의 전처리와 매개변수 설정에 생각보다 신경을 많이 써야 한다는 점입니다. 따라서 요즘에는 전처리가 거의 필요 없는 랜덤 포스트나 그래디언트 부스팅과 같은 트리 기반 모델을 애플리케이션에 많이 도입합니다. 또한 SVM은 수학 적인 관점에서도 모델을 설명하는 것 자체가 매우 어렵습니다.

!! BUT!!

모든 특성이 비슷한 단위이고( 예를 들어 픽셀의 컬러 강도 등...) 스케일이 비슷하면 SVM을 시도해 볼만합니다.

커널 SVM에서 중요한 매개변수는 규제 매개변수 C이고, 어떤 커널을 사용할지와 각 커널에 따른 매개변수입니다. 우리는 RBF 커널만 살펴봤지만 다른 커널도 많이 있습니다.

RBF 커널은 가우시안 커널 폭의 역수인 gamma 매개변수 하나를 가지고 있습니다. gamma와 C 모두 모델의 복잡도를 조정하며 둘 다 큰 값이 더 복잡한 모델을 만들어 냅니다. 따라서 연관성이 많은 이 두 개의 매개변수를 잘 설정하려면 C와 gamma를 함께 조정하는 것이 좋습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

앞선 결정 트리의 개념에 더하여 이번 블로그에서는 앙상블 모델에 대하여 알아보도록 하겠습니다.

앙상블(ensemble)이란?

앙상블은 여러 머신러닝 모델을 연결하여 더 강력한 모델은 만드는 데에 그 의의가 있습니다. 이번 블로그를 통해 저희가 알아볼 앙상블 모델은

  • 랜덤 포레스트(Random Forest Classifier)
  • 그래디언트 부스팅(Gradient Boosting Classifier)

이며, 결정 트리를 이용하여 알아보도록 하겠습니다!


랜덤 포레스트(RandomForest)

결정 트리의 가장 큰 단점 중 하나는 과대적합 될 경향이 있다는 것입니다. 랜덤 포레스트는 결정 트리의 과대적합 가능성을 막아주기 위해 생각해 볼 수 있는 방법입니다.

먼저 포레스트는 말 그대로 숲을 의미하듯이, Decision Tree를 여러개 만드는 기법을 의미합니다.

각 트리는 모델별로 예측을 잘할 테지만, 과대적합 경향을 가진다는데 기초하며, 서로 다른 방향으로 과대적합된 트리를 많이 만들면 그 결과를 평균냄으로써 과대적합 양을 줄이자 라는 겁니다.

이렇게 되면 트리 모델의 예측 성능은 그대로 유지되면서 과대적합이 줄어든 다는 것이 수학적으로 증명되었습니다.

말 그대로 숲(forest)을 이루기 위해서는 나무(tree)를 많이 만들어야 합니다. 각각의 트리는 타깃 예측을 잘해야 하고 다른 트리와 비슷한 예측을 하지 않도록 구별되어야 합니다.

랜덤은 말 그대로 무작위성을 의미합니다. 숲을 이루는 나무들이 각각 달라지도록 무작위성을 주입한다는 뜻입니다.

방식은 두 가지가 있습니다.

  • 트리를 생성할 때 사용하는 데이터 포인트(특성)를 무작위로 선택
  • 분할 테스트에서 특성을 무작위로 선택하는 방법
랜덤 포레스트 구축하기

랜덤 포레스트 모델을 구축할 때 먼저 트리의 개수를 정해야 합니다. RandomForestClassifier나 RandomForestRegressor 클래스의 n_estimators 매개변수를 이용할 수 있습니다.

예제에서는 트리가 10개라고 가정하고 시작합니다.

이 각각의 트리들은 완전히 독립적으로 만들어져야 하므로 알고리즘은 각 트리가 고유하게 만들어지도록 무작위 한 선택을 해야 합니다. 이때 트리를 만들기 위해 데이터의 부트스트랩 샘플(bootstrap sample)을 생성합니다.

매개변수로 n_samples 개의 데이터 포인트 중 무작위로 n_samples 횟수만큼 반복 추출 하게 되는데, 이때 한 샘플이 여러 번 추출될 수 있습니다.

결론적으로 트리를 만들어야 할 샘플 자체는 원본 데이터셋 크기와 같지만, 어떤 데이터 포인트는 누락될 수 있고 어떤 데이터 포인트는 중복되어 들어갈 수 있습니다.

예를 들어 ['a', 'b', 'c', 'd']에서 부트스트랩 샘플을 만든다면

  • ['b', 'd', 'd', 'c']
  • ['a', 'd', 'd', 'b']

같은 샘플이 만들어질 수 있다는 이야기입니다.

이렇게 만들어진 데이터셋을 이용하여 n_estimators 개수만큼 트리를 생성하게 됩니다. 하지만 우리가 본 결정 트리의 알고리즘과 조금 다른데요, 각 노드에서 전체 특성을 대상으로 최선의 테스트를 찾는 것이 아닌 알고리즘이 각 노드에서 후보 특성을 무작위로 선택한 후 이 후보들 중에서 최선의 테스트를 찾습니다.

몇 개의 특성을 고를지는 max_features 매개변수로 조절할 수 있습니다.

후보 특성을 무작위로 고르는 것은 매 노드마다 반복되므로 트리의 각 노드는 다른 후보 특성들을 사용하여 테스트를 만듭니다. 결론적으로 부트스트랩 샘플링은 랜덤 포레스트의 트리가 조금씩 다른 데이터셋을 이용해 만들어지도록 합니다. 이는 각 트리가 서로 무작위적으로 다른 특성, 다른 데이터셋을 가지게 되어 랜덤 포레스트의 모든 트리가 서로 달라지게 합니다.

이 방식에서 핵심이 되는 매개변수는 max_features 입니다. 값에 따른 특징들은 다음과 같습니다.

  • n_features ( 총특성의 개수 )와 같다면 모든 트리에서 모든 특성을 고려하기 때문에 무작위성이 들어가지 않는다.(단, 부트스트랩 샘플링은 적용 )
  • 1로 설정하면 트리는 테스트할 특성을 고를 필요가 없게 되어 그냥 무작위로 선택한 특성의 임계값만을 찾게 됩니다.

정리하자면 max_features의 개수가 많아질수록 랜덤 포레스트가 각 트리들은 구성 자체가 굉장히 비슷해지고 max_feature의 개수가 적어지면 트리는 데이터를 결정하기 위해 트리들의 깊이가 깊어지게 됩니다.

max_features와 n_samples 매개변수를 적절히 조절하여 트리들을 만들면 각 트리 들은 나름대로의 예측을 하게 되고 결과를 냅니다.

랜덤 포레스트는 회귀에서는 예측들의 평균을 내고, 분류에서는 각 트리들에서 제공한 예측 값에 대한 가능성 있는 출력 레이블의 확률을 평균 내어 가장 높은 확률을 가진 클래스가 예측값이 됩니다.

본격적으로 two_moon 데이터셋을 가지고 트리 5개로 구성된 랜덤 포레스트 모델을 만들어 보겠습니다.

#필요 라이브러리 임포트
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!')
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_moons

X, y = make_moons(n_samples=100, noise=0.25, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)

forest = RandomForestClassifier(n_estimators=5, random_state=2)
forest.fit(X_train, y_train)

랜덤 포레스트에 의해 만들어진 트리들은 estimator_ 속성에 저장됩니다. 각 트리에서 학습된 결정 경계와 이를 취합해 만든 결정 경계를 함께 시각화해 보겠습니다.

fig, axes = plt.subplots(2, 3, figsize=(20, 10))
for i, (ax, tree) in enumerate(zip(axes.ravel(), forest.estimators_)):
    ax.set_title("트리 {}".format(i))
    mglearn.plots.plot_tree_partition(X, y, tree, ax=ax)

mglearn.plots.plot_2d_separator(forest, X, fill=True, ax=axes[-1, -1], alpha=.4)
axes[-1, -1].set_title("랜덤 포레스트")
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)

다섯 개의 결정 트리가 만들어낸 경계는 서로 다른 경계를 만들어 낸다는 것이 확인됩니다. 부트스트랩 샘플링 때문에 한쪽 트리에 나타나는 훈련 포인트가 다른 트리에는 포함되지 않을 수 있어 각 트리는 불완전합니다만, 랜덤 포레스트는 각 트리들의 평균값을 취합하기 때문에 각 트리들보다 훨씬 과대적합이 덜 되고 좋은 결정 경계를 만들어 낸다는 것이 확인됩니다.

유방암 데이터셋을 RandomForest로

이번엔 유방암 데이터셋에 100개의 트리로 이뤄진 랜덤 포레스트를 적용해 보겠습니다.

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, random_state=0)

forest = RandomForestClassifier(n_estimators=100, random_state=0)
forest.fit(X_train, y_train)

print("훈련 세트 정확도 : {:.3f}".format(forest.score(X_train, y_train)))
print("테스트 세트 정확도 : {:.3f}".format(forest.score(X_test, y_test)))

확인 결과 테스트 세트 정확도가 꽤나 높게 나오는 것이 확인됩니다. 단일 결정 트리에서 max_features 매개변수를 따로 조정하거나 사전 가지치기를 할 수도 있습니다만, 여러 튜닝을 하지 않아도 랜덤 포레스트는 꽤나 높은 결과를 얻을 수 있습니다.

결정 트리처럼 랜덤 포레스트도 특성 중요도를 제공합니다. 일반적으로 랜덤 포레스트에서 제공하는 특성 정확도가 단일 트리에서 제공하는 특성 보다 더 신뢰가 갈 것 같네요.

def plot_feature_importances_cancer(model):
    n_features = cancer.data.shape[1]
    plt.barh(range(n_features), model.feature_importances_, align='center')
    plt.yticks(np.arange(n_features), cancer.feature_names)
    plt.xlabel("특성 중요도")
    plt.ylabel("특성")
    plt.ylim(-1, n_features)
    
plot_feature_importances_cancer(forest)

단순히 그래프만 봐도 랜덤 포레스트에서는 단일 트리의 경우보다 훨씬 많은 특성이 중요도를 갖습니다. 단일 결정 트리의 worst radius도 랜덤 포레스트에서는 중요하게 생각하고 있고, 랜덤 포레스트는 더 추가해서 worst perimeter도 매우 중요한 특성으로 생각하는 것 같네요

결론적으로 랜덤 포레스트가 단일 결정 트리보다 훨씬 더 넓은 시각으로 데이터를 바라볼 수 있다는 것을 알 수 있습니다.

장단점과 매개변수

랜덤 포레스트는 회귀, 분류 가리지 않고 오늘날 매우 많이 사용하는 머신러닝 알고리즘입니다. 랜덤 포레스트는 성능이 매우 뛰어나고 딱히 매개변수 튜닝을 하지 않아도 잘 작동하며 데이터의 스케일을 맞춰줄 필요가 없습니다.

하지만 랜덤 포레스트의 모델을 만들 때는 트리의 개수가 많다면 시간이 약간 느려지기도 합니다만, n_job 매개변수를 통해 작업할 cpu개수를 지정하여 병렬 처리도 가능합니다. 참고로 n_job = -1 매개변수를 사용하면 cpu의 모든 코어를 사용하여 병렬처리 합니다.

랜덤 포레스트를 사용할 때 주의해야 할 점은 말 그래도 랜덤이기 때문에 random_state 매개변수에 값을 지정하지 않는다면 랜덤 포레스트로 모델을 생성할 때마다 다른 트리가 만들어져 전혀 다른 모델이 만들어지기도 합니다. 트리가 많을수록 random_state 값의 변화에 따른 변동이 적어집니다.
즉 항상 같은 모델을 만들어야 한다면 random_state를 지정해 주어야만 합니다.

랜덤 포레스트의 단점은 텍스트 데이터 같은 차원(feature)이 많고 희소한 데이터에는 잘 작동하지 않습니다. 이런 모델에는 오히려 선형 모델이 더 어울립니다.

랜덤 포레스트는 큰 데이터셋에도 잘 동작하고, CPU 개수까지 조절할 수 있으나, 선형 모델보다 더 많은 메모리를 사용하며 훈련과 예측이 느려집니다. 즉 속도와 메모리 사용에 제약이 있는 애플리케이션을 제작한다면 랜덤 포레스트 보다 선형 모델이 더 어울릴 수 있습니다.

중요한 매개변수로써 n_estimators, max_feature 및 max_depth 같은 사전 가지치기 옵션이 있습니다. 트리의 개수를 결정짓는 n_estimators의 값은 높을수록 트리를 많이 만들기 때문에 과대적합 가능성이 적어지지만 메모리와 긴 훈련시간이 필요하게 됩니다.

보통은 "가용한 시간과 메모리만큼 많이" 만드는 것이 좋습니다.

max_features는 각 트리가 얼마나 무작위가 될지를 결정하며 작은 max_features는 마찬가지로 과대적합을 줄여줍니다. 일반적으로는 그냥 기본값을 사용하는 것이 좋습니다.


그래디언트 부스팅(GradientBoosting)

마찬가지로 앙상블 모델로써 랜덤 포레스트와 비슷하게 여러 개의 결정 트리를 만듭니다. 이름은 회귀 트리지만 분류와 회귀 모두 사용 가능합니다.

랜덤 포레스트와의 차이점은 그래디언트 부스팅 회귀 트리에는 무작위성이 없다는 점입니다. 그래디언트 부스팅은 이전 트리의 오차를 보완하는 방식으로 순차적으로 트리를 만들어 냅니다. 무작위성을 포기하는 대신에 강력한 사전 가지치기를 사용합니다.

그래디언트 부스팅 회귀 트리에는 보통 1~5개 정도의 깊이를 가진 결정 트리를 사용하기 때문에 메모리를 적게 사용하고 예측도 빠릅니다. 이러한 얕은 트리 같은 간단한 모델을 약한 학습기(weak learner)라고도 하는데, 그래디언트 부스팅 회귀 트리는 이러한 약한 학습기를 여러개 연결합니다.

각각의 트리는 데이터의 일부에 대해서만 예측을 잘 수행할 수 있어서 트리가 많이 추가될수록 성능이 좋아지게 됩니다.

랜덤 포레스트 보다는 매개변수 설정에 대해 조금 민감하긴 하지만 잘 조정하면 더 높은 정확도를 제공합니다.

앙상블에서 사용하는 대표적인 매개변수인 사전 가지치기, 트리 개수 조절 외에도 그래디언트 부스팅에서 중요한 매개변수는 이전 트리의 오차를 얼마나 강하게 보정할 것인지를 제어하는 learning_rate를 잘 조절해야 합니다. 학습률이 커지면 보정을 강하게 하기 때문에 복잡한 모델을 만들어 냅니다. n_estimators 값을 커지게 하면 앙상블에 트리가 더 많이 추가되어 모델의 복잡도가 커지고 훈련 세트에서의 실수를 바로잡을 기회가 더 많아집니다.

마찬가지로 유방암 데이터셋을 이용해 GradientBoostingClassifier를 사용해 보겠습니다. 기본값인 깊이가 3인 트리 100개와 학습률 0.1을 사용합니다.

from sklearn.ensemble import GradientBoostingClassifier

X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state = 0)

gbrt = GradientBoostingClassifier(random_state=0)
gbrt.fit(X_train, y_train)

print("훈련 세트 정확도 : {:.3f}".format(gbrt.score(X_train, y_train)))
print("테스트 세트 정확도 : {:.3f}".format(gbrt.score(X_test, y_test)))

훈련 세트의 정확도가 100% 가 나왔기 때문에 과대적합된 거 같습니다. 과대적합을 만들기 위해 그래디언트 부스팅의 복잡도를 줄이기 위하여 트리의 최대 깊이를 줄여서 사전 가지치기를 강하게 하거나 학습률을 낮출 수 있습니다.

gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)

print("훈련 세트 정확도 : {:.3f}".format(gbrt.score(X_train, y_train)))
print("테스트 세트 정확도 : {:.3f}".format(gbrt.score(X_test, y_test)))

gbrt = GradientBoostingClassifier(random_state=0, learning_rate=0.01)
gbrt.fit(X_train, y_train)

print("훈련 세트 정확도 : {:.3f}".format(gbrt.score(X_train, y_train)))
print("테스트 세트 정확도 : {:.3f}".format(gbrt.score(X_test, y_test)))

위의 두 방식 모두 모델의 정확도를 감소시키기 대문에 훈련 세트의 정확도가 낮아지기 시작합니다. 학습률을 낮추는 것은 테스트 세트의 성능을 조금밖에 개선시키지 못했으나, 트리의 최대 깊이를 낮추는 것은 모델 성능 향상에 매우 큰 기여를 하였습니다.

랜덤 포레스트처럼 특성의 중요도를 시각화해보겠습니다. 트리를 100개나 사용했기 때문에 깊이가 1인 트리더라도 모든 트리를 전부 분석하기에는 쉽지는 않습니다.

gbrt = GradientBoostingClassifier(random_state=0, max_depth=1)
gbrt.fit(X_train, y_train)

plot_feature_importances_cancer(gbrt)

랜덤 포레스트의 특성 중요도와 비슷하게 등장하는데요, 랜덤 포레스트와 다르게 그래디언트 부스팅은 일부 특성은 완전히 무시하는 것이 보입니다.

비슷한 종류의 데이터에서 그래디언트 부스팅과 랜덤 포레스트 둘 다 잘 작동합니다만, 보통 더 안정적인 랜덤 포레스트를 먼저 사용합니다. 랜덤 포레스트가 잘 작동하더라도 예측 시간이 중요하거나 머신러닝 모델에서 마지막 성능까지 쥐어짜야 할 때 그래디언트 부스팅을 사용하면 도움이 됩니다.

장단점과 매개변수

마찬가지로 그래디언트 부스팅 결정 트리 역시 지도 학습에서 강력하고 널리 사용하는 모델 중 하나입니다. 단점은 매개변수에 너무 민감하다는 데에 있고, 훈련 시간이 꽤나 길다는 것입니다.

하지만 다른 트리 기반 모델처럼 특성의 스케일을 조정하지 않아도 되고 이진 특성이 연속적인 특성에서도 잘 동작합니다. 하지만 트리 기반 모델의 특성상 희소한 고차원 데이터에는 잘 작동하지 않습니다.

그래디언트 부스팅에서 중요한 매개변수는 n_estimators와 learning_rate가 중요합니다. 이 두 매개변수는 매우 깊게 연관되어 있으며 learing_rate를 낮추면 비슷한 복잡도의 모델을 만들기 위해서 더 많은 트리를 추가해야 합니다. n_estimators가 클수록 좋은 랜덤 포레스트와 다르게 그래디언트 부스팅에서 n_estimators 매개변수를 크게 하면 모델이 복잡해지고 과대적합될 가능성이 높아지게 됩니다.

일반적인 관례상 가용한 시간과 메모리 한도 내에서 n_estimators를 맞추고 나서 적절한 learning_rate를 찾는 것입니다.

또한 트리의 복잡도를 낮추기 위해 max_depth( 또는 max_leaf_nodes )를 조절해야 하는데, 통상적으로 그래디언트 부스팅에서는 max_depth를 작게 설정하여 트리의 깊이가 5보다 깊어지지 않게 하는 것이 좋습니다.

 

다음 포스팅에서는 서포트 백터 머신(SVM)에 대하여 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

2023.05.23 - [Programming/특성 공학] - [Machine Learning] 의사결정 트리(Decision Tree)

 

[Machine Learning] 의사결정 트리(Decision Tree)

이번 포스팅에서는 의사결정 트리(Decision Tree)에 대하여 알아보도록 하겠습니다! 결정 트리(Decision Tree) 분류와 회귀 문제에 널리 사용하는 모델입니다. 기본적으로 결정 트리는 스무고개 놀이와

yuja-k.tistory.com

앞서, 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은 완벽하게 타깃 클래스를 예측했다는 뜻이 됩니다.

print("특성 중요도 :\n{}".format(tree.feature_importances_))

def plot_feature_importances_cancer(model):
    n_features = cancer.data.shape[1]
    plt.barh(range(n_features), model.feature_importances_, align='center')
    plt.yticks(np.arange(n_features), cancer.feature_names)
    plt.xlabel("특성 중요도")
    plt.ylabel("특성")
    plt.ylim(-1, n_features)

plot_feature_importances_cancer(tree)

첫 번째 노드에서 사용한 특성인 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), 즉 훈련 데이터의 범위 밖의 포인트에 대해 예측을 할 수 없습니다.

import os
ram_prices = pd.read_csv(os.path.join(mglearn.datasets.DATA_PATH, "ram_price.csv"))

plt.semilogy(ram_prices.date, ram_prices.price)
plt.xlabel("년")
plt.ylabel("가격 ($/Mbyte)")

Text(0,0.5,'가격 ($/Mbyte)')

y축은 로그 스케일로써 선형적으로 그리기 좋기 때문에 비교적 예측이 쉬워집니다.

날짜 특성 하나만으로 램의 2000년도 전까지의 데이터로부터 2000년도 이후의 가격을 예측해 보겠습니다. 여기서는 간단한 두 모델인 DecisionTreeRegressor와 LinearRegression을 비교해 보도록 하겠습니다.

그래프 표현식을 위해 전체 데이터셋에 대해 예측을 수행하였지만, 테스트 데이터셋의 비교가 관심 대상입니다.

from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression

# 2000년 이전을 훈련 데이터로, 2000년 이후를 테스트 데이터로
data_train = ram_prices[ram_prices.date < 2000]
data_test  = ram_prices[ram_prices.date >= 2000]

# 가격 예측을 위해 날짜 특성만을 이용합니다.
X_train = data_train.date[:, np.newaxis]

# 데이터와 타깃 관계를 간단하기 위해 로그 스케일로 바꿉니다.
y_train = np.log(data_train.price)

tree = DecisionTreeRegressor().fit(X_train, y_train)
linear_reg = LinearRegression().fit(X_train, y_train)

# 예측은 전체 기간에 대해서 수행합니다.
X_all = ram_prices.date[:, np.newaxis]

pred_tree = tree.predict(X_all)
pred_lr   = linear_reg.predict(X_all)

# 예측한 값의 로그 스케일을 되돌립니다.
price_tree = np.exp(pred_tree)
price_lr   = np.exp(pred_lr)
plt.semilogy(data_train.date, data_train.price, label="훈련 데이터")
plt.semilogy(data_test.date, data_test.price, label="테스트 데이터")
plt.semilogy(ram_prices.date, price_tree, label="트리 예측")
plt.semilogy(ram_prices.date, price_lr, label="선형 회귀 예측")
plt.legend()

두 모델은 확연한 차이를 보이고 있습니다. 선형 모델 ( 빨간 선 )은 우리가 이미 알고 있는 대로 데이터를 근사하여 직선을 2000년 이후 데이터를 꽤나 정확히 예측하고 있습니다. 하지만 트리 모델은 훈련 데이터를 ( 2000년도 이전 ) 완벽하게 예측합니다.

트리의 복잡도에 제한을 두지 않아서 저체 데이터셋을 모두 기억하기 때문입니다. 그러나 모델이 가진 데이터 범위 밖으로 나가면 단순히 마지막 포인트를 이용해 예측하는 것이 전부입니다.

트리 모델은 훈련 데이터 밖의 새로운 데이터를 예측할 능력이 없습니다. 이는 모든 트리 기반 모델의 공통적인 단점입니다.

장단점과 매개변수

결정 트리에서의 모델 복잡도를 조절하는 매개변수는 트리가 완전히 만들어지기 전에 멈추는 사전 가지치기 매개변수입니다. 우리는 실제 하나만 써 보았지만 3개의 매개변수가 있습니다.

  • max_depth
  • max_leaf_nodes
  • min_sample_leaf

위의 매개변수 중 하나만 조절해도 과대적합을 막는데 충분합니다.

결정 트리의 장점

이전에 소개한 다른 알고리즘보다 나은 점은 총 두 가지로 정리가 가능합니다.

  • 1) 만들어진 모델을 쉽게 시각화 할 수 있어서 비 전문가도 이해하기가 쉽습니다. ( 너무 크지 않다면...)
  • 2) 데이터의 크기게 구애 받지 않습니다.

각 특성이 개별적으로 처리되어 데이터를 분할하는데 데이터 스케일의 영향을 받지 않으므로 결정 트리에서는 특성의 정규화나 표준화 같은 전처리 과정이 필요가 없습니다.

특히 특성의 스케일이 서로 다르거나 이진 특성과 연속적인 특성이 혼합되어 있을 때도 잘 작동합니다.

결정 트리의 단점

매개변수를 조절해 사전 가지치기를 사용함에도 불구하고 과대적합되는 경향이 있어 일반화 성능이 좋지 못합니다. 따라서 다음에 해볼 랜덤포레스트 - 앙상블 방법을 단일 결정 트리의 대안으로 흔히 사용합니다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅에서는 의사결정 트리(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개로 제한하게 됩니다.

트리의 깊이를 제한하면 과대적합이 줄어듭니다. 이는 훈련 세트의 정확도를 떨어뜨리지만 테스트 세트의 성능을 개선시킵니다.

tree = DecisionTreeClassifier(max_depth=4, 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)))

다음 블로그에서는 앞서 다룬 Tree의 시각화 및 특성 중요도에 대하여 알아보고 결정 트리의 장단점에 대하여 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

이번 포스팅은 다중 선형 분류에 대하여 알아보도록 하겠습니다

선형 이진 분류 또는 선형 회귀에 대해 알고 싶은 분들은 이전 포스팅들을 참고해 주세요~!

2023.05.09 - [Programming/특성 공학] - [Machine Learning] 선형 이진 분류

 

[Machine Learning] 선형 이진 분류

지난 선형 회귀 포스팅에 이어 이번 포스팅에서는 선형 이진 분류에 대하여 알아보도록 하겠습니다. 2023.05.02 - [Programming/특성 공학] - [Machine Learning] 선형 회귀 [Machine Learning] 선형 회귀 이번 블로

yuja-k.tistory.com

2023.05.02 - [Programming/특성 공학] - [Machine Learning] 선형 회귀

 

[Machine Learning] 선형 회귀

이번 블로그는 선형 모델에 대하여 알아보도록 합시다. 선형 모델은 매우 오래전 개발된 모델입니다. 선형 모델은 입력 특성에 대한 선형 함수를 만들어 예측을 수행합니다. 먼저 회귀의 선형

yuja-k.tistory.com

다중 클래스 분류란?

로지스틱 회귀는 기본적으로 softmax 함수를 이용하여 다중 분류를 지원하나, 다른 많은 모델들은 태생적으로 이진 분류만을 지원합니다. 즉 다중 클래스(Multi-class)를 지원하지 않습니다.

이진 분류 알고리즘을 다중 클래스 알고리즘으로 확장하는 방법은 일대다(One vs Rest 또는 One vs All) 방법입니다. 일대다 방식은 각 클래스를 다른 모든 클래스와 구분하도록 이진 분류 모델을 학습시킵니다.

결국 클래스의 개수만큼 이진 분류 모델이 만들어지며, 예측할 때 이렇게 만들어진 모든 이진 분류기가 작동하여 가장 높은 점수를 내는 분류기의 클래스를 예측값으로 선택하게 됩니다.

지금까지 해왔던 것과 마찬가지로 다중 클래스 분류도 어쨌든 이진 분류를 기반으로 생각해야 하기 때문에 방정식 자체가 똑같습니다.

다중 클래스 로지스틱 회귀의 수학은 다른 모델들의 위의 일대다 방식과는 조금 다릅니다. 하지만 여기서도 클래스마다 계수 벡터와 절편을 만들며, 예측 방법도 같습니다.

 

𝑖번째 데이터 포인트 𝑋𝑖의 출력 𝑌𝑖가 클래스 𝑐일 확률 𝑃𝑟(𝑌𝑖=𝑐)는 𝐾개의 클래스에 대한 각각의 계수를 데이터 포인트에 곱하여 자연상수(𝑒)에 지수함수를 적용한 합으로써 클래스에 대한 값을 나누어 계산합니다.

위의 함수를 소프트맥스 함수 표현식이라 하며, 수식의 간소화를 위해 계수벡터에 절편 𝑏가 포함 되어 있는 것으로 나타냅니다.

따라서 다중 클래스 로지스틱 회귀에서도 계수 벡터와 절편이 존재합니다.

세 개의 클래스를 가진 간단한 데이터셋에 일대다 방식을 적용시켜 보겠습니다. 이 데이터셋은 2차원이며 각 클래스의 데이터는 정규분포(가우시안 분포)를 따릅니다.

#필요 라이브러리 임포트
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
plt.rcParams['axes.unicode_minus'] = False

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!')
from sklearn.datasets import make_blobs

X, y = make_blobs(random_state=42)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
plt.xlabel("특성 0")
plt.ylabel("특성 1")
plt.legend(["클래스 0", "클래스 1", "클래스 2"])

이 데이터셋으로 먼저 LinearSVC 분류기를 훈련해 보겠습니다.

from sklearn.svm import LinearSVC
linear_svm = LinearSVC().fit(X, y)
print("계수 배열의 크기 : ", linear_svm.coef_.shape)
df_coef = pd.DataFrame(columns=["특성 0","특성 1"] ,data=linear_svm.coef_)
df_coef

print("절편 배열의 크기 : ", linear_svm.intercept_.shape)
df_intercept = pd.DataFrame(columns=["절편"], data = linear_svm.intercept_)
df_intercept

coef_ 배열의 크기는 (3,2)입니다. coef_의 행은 세 개의 클래스가 각각 대응하는 계수 벡터들을 각각 담고 있으며, 열은 각 특성에 따른 계수 값을 가지고 있습니다. intercept_는 각 클래스의 절편을 담아낸 1차원 벡터입니다.

세 개의 이진분류기가 만드는 경계를 시각화해보겠습니다.

mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
line = np.linspace(-15, 15) # -15 ~ 15 까지 50개의 수열을 생성

for coef, intercept, color in zip(linear_svm.coef_, linear_svm.intercept_, mglearn.cm3.colors):
    plt.plot(line, -(line * coef[0] + intercept) / coef[1], c=color) # 판별 함수 사용

plt.ylim(-10, 15)
plt.xlim(-10, 8)
plt.xlabel("특성 0")
plt.ylabel("특성 1")
plt.legend(['클래스 0', '클래스 1', '클래스 2', '클래스 0 경계','클래스 1 경계', '클래스 2 경계'], loc=(1.01, 0.3))

각 훈련 데이터의 클래스들에 대한 결정 경계를 그려 보았습니다. 파란색 선은 클래스 0으로, 주황색 선은 클래스 1로, 초록색 선은 클래스 2로 경계합니다. 각 경계에서 겹치는 부분들은 조금 더 가까운 쪽으로 예측을 하게 됩니다.

중앙의 삼각형 영역은 모든 데이터 포인트가 나머지로 분류한 곳으로써 분류 공식의 결과가 가장 높은 클래스가 포인트로써 지정되게 됩니다.

조금 더 알아보기 쉽도록 경계면을 그려 보겠습니다.

mglearn.plots.plot_2d_classification(linear_svm, X, fill=True, alpha=.7)
mglearn.discrete_scatter(X[:, 0], X[:, 1], y)
line = np.linspace(-15, 15)

for coef, intercept, color in zip(linear_svm.coef_, linear_svm.intercept_, mglearn.cm3.colors):
    plt.plot(line, -(line * coef[0] + intercept) / coef[1], c=color)

plt.legend(['클래스 0', '클래스 1', '클래스 2', '클래스 0 경계', '클래스 1 경계', '클래스 2 경계'], 
           loc=(1.01, 0.3))
plt.xlabel('특성 0')
plt.ylabel('특성 1')

선형 모델의 주요 매개변수는 회귀 모델은 alpha, 분류 모델 (LinearSVC, LogisticRegression)에서는 C입니다. alpha의 값이 클수록, C 값이 작아질수록 모델은 단순해집니다.

회귀 모델은 이러한 매개변수를 조절하는 것이 꽤나 중요하며 보통 C와 alpha는 로그 스케일 ( 10배 단위로 조절 하는 것. 0.01, 0.1, 1, 10 등 )로 최적치를 정하게 됩니다.

또한 L1 규제와 L2 규제를 정해야 하는데 어떠한 규제를 사용해야 할지 결정 지어 주는 상황은 다음으로 정리해 볼 수 있겠습니다.

  • 중요한 특성이 많지 않으면 L1 규제를 사용해 예측에 사용하지 않도록 만들 수 있습니다.
  • 모델의 해석이 중요한 요소일 때도 ( 특정 요소가 매우 중요한 요소일 경우 ) L1 규제를 사용할 수 있습니다.
  • 중요한 특성이 많으면 L2 규제를 사용해 약간이라도 예측에 영향을 미치도록 만들 수 있습니다.

선형 모델은 학습 속도가 빠르고 예측도 매우 빠릅니다. 매우 큰 데이터셋이던, 희소한 데이터셋이던 잘 작동합니다.

참고로 수십만 ~ 수백만 개의 샘플로 이뤄진 대용량 데이터 셋이라면 기본 설정보다 더 빨리 처리할 수 있도록 LogisticRegression과 Ridge에 solver='sag' 옵션을 부여해 줄 수 있습니다.

아니면 SGDClassifier 또는 SDGRegressor를 사용해 볼 수도 있습니다.

선형 모델은 샘플에 비해 특성이 많을 때 잘 작동하며 다른 모델로 학습하기 어려운 매우 큰 데이터셋에도 선형 모델을 많이 사용합니다.

하지만 저 차원의 데이터셋에서는 ( 특성이 많이 없는 ) 데이터셋에서는 다른 모델들의 일반화 성능이 더 좋습니다. 

 

다음 블로그에서는 의사결정 트리에 대하여 다뤄보도록 하겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

지난 선형 회귀 포스팅에 이어 이번 포스팅에서는 선형 이진 분류에 대하여 알아보도록 하겠습니다.

2023.05.02 - [Programming/특성 공학] - [Machine Learning] 선형 회귀

 

[Machine Learning] 선형 회귀

이번 블로그는 선형 모델에 대하여 알아보도록 합시다. 선형 모델은 매우 오래전 개발된 모델입니다. 선형 모델은 입력 특성에 대한 선형 함수를 만들어 예측을 수행합니다. 먼저 회귀의 선형

yuja-k.tistory.com

선형 모델은 '분류'에서도 사용됩니다.

먼저 이진 분류부터 확인해 보겠습니다! 이진 분류를 위한 방정식은 다음과 같습니다.

위의 방정식은 선형 회귀와 매우 비슷한데, 이러한 특성들의 가중치 합을 그냥 사용하는 대신, 그 결과가 임계치라고 할 수 있는 0과 비교하여 0보다 작으면 클래스를 -1로 분류하고, 0보다 크면 +1로 예측합니다.

위의 규칙은 모든 선형 모델에서 동일하게 작동하며, 여기에서도 𝑤와 𝑏를 찾기 위한 여러 가지 방법들이 있습니다.

먼저 회귀에서는 𝑦̂ 이 특성의 선형 함수가 되었었습니다. 분류용 선형 모델에서는 결정 경계가 입력 특성의 선형 함수입니다.

즉 이진 선형 분류기는 선, 평면, 초평면을 사용하여 두 개의 클래스를 구분하는 분류기라고 생각하면 됩니다.

선형 모델을 학습시키기 위한 알고리즘은 다양한데, 다음의 두 방법으로 구분합니다.

.

  • 특정 계수(𝑤)와 절편(𝑏)의 조합이 훈련 데이터에 얼마나 잘 맞는지 측정하는 방법
  • 사용할 수 있는 규제가 있는지, 있다면 어떤 방식인지

각 알고리즘마다 훈련세트를 잘 학습하는지를 측정하는 방법은 각기 다릅니다. 불행하게도 수학적이고 기술적인 이유로, 알고리즘이 만드는 잘못된 분류의 수를 최소화하도록 𝑤와 𝑏를 조정하는 것은 불가능합니다. 손실이 일어난다고 할 수 있고, 완벽히는 아니지만 손실을 최소화하는 방법이 여럿 존재 합니다.

 

위에서 이야기한 첫 번째 목록( 손실 함수 loss function 라 합니다. )에 대한 차이는 별로 중요하지는 않습니다. 직접 잘못된 결과를 나타내는 0-1 손실 함수는 완전한 계단 함수입니다. 따라서 대신 사용할 수 있는 함수( surrogate loss function )를 사용하여 대신 최적화 합니다.

우리가 가장 널리 사용 할 수 있는 함수는 두 가지가 있습니다.

  • 로지스틱 회귀 (logistic Regression) - linear_model.LogisticRegression에 구현되어 있음 - 이진 분류에서 logistic 손실 함수 사용, 다중 분류에서 교차 엔트로피( cross-entropy ) 손실 함수 사용
  • 서포트 벡터 머신 ( Support Vector Machine - SVM ) - 제곱 힌지 ( squared hinge ) 손실 함수 사용

LogisticRegression은 회귀(Regression)가 들어가 있기는 하지만 회귀 알고리즘이 아닌 분류 알고리즘 이기 때문에 LinearRegression(선형 회귀)와 혼동하면 안 됩니다.

이진 분류용 모델인 forge 데이터 세트를 이용하여 LogisticRegression과 LinearSVC 모델을 각각 만들어 이 선형 모델들이 만들어낸 결정 경계를 확인해 보겠습니다.

참고로 두 모델 전부다 L2 규제를 사용하며, 차이점은 사용하는 손실 함수가 다르다는 것뿐입니다.

#필요 라이브러리 임포트
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
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')
from sklearn.linear_model import LogisticRegression
from sklearn.svm import LinearSVC

X, y = mglearn.datasets.make_forge()

fig, axes = plt.subplots(1, 2, figsize=(10, 3))

for model, ax in zip([LinearSVC(), LogisticRegression()], axes):
    clf = model.fit(X, y)
    mglearn.plots.plot_2d_separator(clf, X, fill=False, eps=0.5, ax=ax, alpha=.7)
    mglearn.discrete_scatter(X[:, 0], X[:, 1], y, ax=ax)
    ax.set_title("{}".format(clf.__class__.__name__))
    ax.set_xlabel("특성 0")
    ax.set_ylabel("특성 1")
    
axes[0].legend()

먼저 forge 데이터셋의 첫 번째 특성을 x축에, 두 번째 특성을 y축에 배치시켰습니다.

LinearSVC와 LogisticRegression으로 만들어낸 결정 경계가 직선으로 표현되어 아래쪽은 특성 0으로, 위쪽은 특성 1로 구분한 것이 확인됩니다.
즉, 새롭게 추가될 데이터가 직선을 기준으로 위로 놓이게 되면 특성 1을 의미하고, 아래쪽으로 놓이면 특성 0으로 분류할 것이라고 분류될 것입니다. 두 모델은 비슷한 결정 경계를 만들었는데, 각각 똑같이 두 개의 포인트를 잘못 구분한 것이 확인됩니다.


각 모델의 규제의 강도를 줄이는 매개변수는 C 값을 활용합니다. 여기서 C 값이 높아지면 규제가 감소하고, C값이 낮아지면 규제가 증가합니다.

즉, C값을 높이 지정하면 훈련 세트에 최대한 가깝게 맞추기 위해 노력하고, C값을 낮추면 계수(𝑤)를 0에 가깝게 맞추도록 노력하게 됩니다.

다르게 설명할 수도 있는데, C값이 낮아지면 데이터 포인트 중 다수에 맞추려고 노력하고, C 값을 높이면 개개의 데이터 포인트를 정확히 분류하려고 노력합니다.

 

다음은 C값에 따라서 LinearSVC의 결정경계가 달라지는 모양을 알아봅니다.

mglearn.plots.plot_linear_svc_regularization()

왼쪽 그림은 너무 낮은 C값 때문에 규제가 많이 적용되었습니다. 잘 보면 다수의 데이터 포인트에 몰린 쪽으로 직선이 그어져 있는 것이 확인됩니다. 즉 다수의 포인트에 맞추려고 노력했다는 증거가 될 수 있겠네요

중간 그림은 C값이 조금 커져서 잘못 분류한 샘플에 굉장히 민감해진 것이 확인됩니다. 잘못 분류된 데이터 쪽으로 결정 경계가 그어져 있는 것이 확인되나요?

오른쪽 그림에서 C값을 아주 크게 설정했더니 결정 경계가 엄청나게 기울어졌고 클래스 0의 모든 데이터를 완벽하게 구분한 것이 확인됩니다.

forge 데이터셋의 모든 포인트를 직선으로 완벽히 구분하는 것은 불가능하기에 클래스 1의 포인트 한 개는 여전히 잘못 구분하였습니다.

오른쪽 그림의 모델은 모든 데이터 포인트를 정확하게 분류하려고 애썼지만 클래스의 전체적인 배치를 잘 파악하지 못한 것 같습니다. 즉 과대적합 되었다고 볼 수 있겠네요

회귀와 비슷하게 선형 모델은 낮은 차원의 데이터에서는 결정 경계가 직선 또는 평면이어서 매우 제한적인 것처럼 보입니다. 하지만 고차원에서는 분류에 대한 선형 모델이 매우 강력해지며 특성이 많아지면 과대적합 되지 않도록 규제를 조절하는 것이 중요합니다.

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=42)
logreg = LogisticRegression().fit(X_train, y_train)

print("훈련 세트 점수 : {:.3f}".format(logreg.score(X_train, y_train)))
print("테스트 세트 점수 : {:.3f}".format(logreg.score(X_test, y_test)))

기본값 C=1이 훈련 세트와 테스트 세트 양쪽에 95% 정도로 매우 좋은 정확도를 보이긴 하지만 두 세트의 성능이 매우 비슷합니다. 따라서 과소적합인 것 같으니 제약을 더 풀어주기 위해 C를 증가시켜 보겠습니다.

logreg100 = LogisticRegression(C=100).fit(X_train, y_train)

print("훈련 세트 점수 : {:.3f}".format(logreg100.score(X_train, y_train)))
print("테스트 세트 점수 : {:.3f}".format(logreg100.score(X_test, y_test)))

C=100을 사용하니 훈련 세트의 정확도가 높아졌고 테스트 세트의 정확도도 조금 증가하였습니다. 이는 복잡도가 높은 모델일수록 성능이 좋아진다는 것을 의미합니다.

반대로 규제를 더 강하게 하기 위해 C=0.01을 사용해 보겠습니다.

logreg001 = LogisticRegression(C=0.01).fit(X_train, y_train)

print("훈련 세트 점수 : {:.3f}".format(logreg001.score(X_train, y_train)))
print("테스트 세트 점수 : {:.3f}".format(logreg001.score(X_test, y_test)))

C=1 인 상태에서 과소 적합이었는데, 여기서 더 규제가 강해져 계수가 미치는 영향도가 줄어들었기 때문에 더욱더 과소적합 되었다고 볼 수 있겠습니다.

규제 매개변수 C 설정을 세 가지로 다르게 하여 계수를 시각화해서 확인해 보겠습니다.

plt.plot(logreg.coef_.T, 'o', label="C=1")
plt.plot(logreg100.coef_.T, '^', label="C=100")
plt.plot(logreg001.coef_.T, 'v', label="C=0.01")
plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90)
plt.hlines(0, 0, cancer.data.shape[1])
plt.ylim(-5, 5)
plt.xlabel("특성")
plt.ylabel("계수 크기")
plt.legend()

위의 그림을 잘 살펴보면 C 매개변수가 특성에 미치는 영향을 파악할 수 있는데, 릿지 회귀와 비슷한 모양새를 보이고 있습니다.

규제를 강하게 할수록( C 값이 감소할수록 ) 계수들을 0에 가깝게 만들지만 완전한 0으로 만들지는 않는 것이 보이고 있습니다.

그리고 세 번째 계수인 mean perimeter를 보면 C=100, C = 1 일 때는 음수지만 C=0.0001 이면 양수가 되며, C = 1일 때보다 절댓값이 더 큰 것을 확인할 수 있습니다.

이와 같이 모델을 해석하면 계수가 클래스와 특성의 연관성을 알려줄 수도 있습니다.

다른 예로 texture error 특성은 악성인 샘플과 영향이 깊다는 것도 알 수 있습니다. ( 음수 ) 하지만 mean perimeter는 계수의 부호가 바뀜에 따라서 양성이나 악성의 신호 모두가 될 수 있다고도 판단할 수 있습니다.

다음은 L1 규제를 사용한 LogisticRegression입니다.

for C, marker in zip([0.001, 1, 100], ['o', '^', 'v']):
    lr_l1 = LogisticRegression(C=C, penalty="l1").fit(X_train, y_train)
    print("C={:.3f} 인 로지스틱 회귀의 훈련 정확도 : {:.2f}".format(C, lr_l1.score(X_train, y_train)))
    print("C={:.3f} 인 로지스틱 회귀의 테스트 정확도 : {:.2f}".format(C, lr_l1.score(X_test, y_test)))
    plt.plot(lr_l1.coef_.T, marker, label="C={:.3f}".format(C))

plt.xticks(range(cancer.data.shape[1]), cancer.feature_names, rotation=90)
plt.hlines(0, 0, cancer.data.shape[1])
plt.xlabel("특성")
plt.ylabel("계수 크기")

plt.ylim(-5, 5)
plt.legend(loc=3)

이진 분류에서의 선형 모델과 회귀에서의 선형 모델 사이에는 유사점이 굉장히 많습니다.

penalty 매개변수를 활용하여 전체 특성을 사용할지( L2 규제 ), 일부 특성만을 사용할지 ( L1 규제 )를 사용할지를 결정할 수 있습니다.

penalty="l2" 또는 penalty="l1"을 설정할 수 있습니다.

728x90
반응형
LIST

+ Recent posts