07. 딥러닝

 

07-1 인공 신경망

  • 시작하기 전에




    럭키백의 성공 이후 타깃 고객의 연령대를 대상으로 패션 럭키백을 진행해보려 한다!
    는 사실 패션 mnist 데이터를 사용하기 위한 밑밥..이지만 박해선님의 스토리 라인에 감탄!

 

<패션 MNIST>

시작하기에 앞서, 머신러닝/딥러닝을 입문할 때 사용하는 데이터셋이 있다. 머신러닝에서 붓꽃 데이터셋이 유명하듯, 딥러닝에서는 MNIST dataset이 유명하다. 이 데이터에는 손으로 적은 숫자로 이루어져 있는데, 패션 mnist는 숫자가 아닌 패션 아이템이 들어가있는 데이터셋이다.
from tensorflow import keras
(train_input, train_target), (test_input, test_target) = keras.datasets.fashion_mnist.load_data()

print(train_input.shape, train_target.shape)
print(test_input.shape, test_target.shape)

패션 MNIST 데이터셋은 워낙 유명하기 때문에 라이브러리를 통해 import하여 사용이 가능하다. load_date() 메서드는 훈련 데이터와 테스트 데이터를 나눠 반환하는 함수다. 크기를 확인해보면 6만개의 이미지를 활용한 훈련 세트와 1만개의 이미지를 활용한 테스트 세트로 이루어진 것을 알 수 있다.

 

6장에서 matplotlib으로 이미지를 출력했던 것처럼 샘플을 출력할 수 있다. 이렇게 샘플 이미지를 확인해보면 데이터를 이해하고 주어진 task를 해결할 때 방향 잡기가 훨씬 수월하다.

 

 

앞서 출력한 10개의 이미지를 레이블과 함께 출력할 수 있다. 위의 레이블과 출력된 값을 비교함으로써 제대로 labelling되었는지 확인할 수 있다. 또한 레이블마다 6,000개의 샘플이 들어있는 것을 확인할 수 있다.

 

<로지스틱 회귀로 패션 아이템 분류하기>

6만개의 데이터를 한꺼번에 훈련하는 것보다 샘플을 하나씩 꺼내서 훈련하는 방법이 더 효율적이지 않을까?

이럴 땐 4장에서 배웠던 경사하강법을 사용해볼 수 있다. 이번 실습에서 사용하는 데이터셋의 경우 각 픽셀이 0-255 사이의 정수값을 가지기 때문에 이를 255로 나누어 0-1사이의 값으로 정규화할 수 있다. 정확한 표준화 방법은 아니지만 이미지를 전처리할 때 널리 사용되는 방법이다.

train_scaled = train_input / 255.0
train_scaled = train_scaled.reshape(-1, 28*28)
print(train_scaled.shape)

SGDClassifier 는 2차원 입력을 다루지 못하기 때문에 1차원 배열로 만든 후 크기에 맞춰 지정하면 샘플의 갯수는 변하지 않고 데이터의 차원이 1차원으로 합쳐진다. 변환된 데이터의 크기를 확인해보면 784개의 픽셀로 이뤄진 6만개의 샘플이 준비된 것을 알 수 있다.

 

from sklearn.model_selection import cross_validate
from sklearn.linear_model import SGDClassifier
sc = SGDClassifier(loss = 'log', max_iter = 5, random_state=42)
scores = cross_validate(sc, train_scaled, train_target, n_jobs=-1)
print(np.mean(scores['test_score']))

경사하강법의 반복 횟수를 max_iter =5로 지정하여 성능을 확인해볼 수 있다.

 

 

첫번째 레이블인 티셔츠와 두번째 레이블인 바지에 대해 생각해본다면, 각 레이블마다 다른 가중치(weight) 값과 절편(bias) 값을 적용하여 계산해야 한다. 동일한 픽셀값을 사용하기 때문에 동일한 weight와 bias를 사용한다면 클래스의 구분이 어렵다.

티셔츠를 계산하기 위한 가중치,절편값과 바지를 계산하기 위한 가중치,절편값은 다르다. 회귀를 통해 각 모델 파라미터(가중치,절편)를 찾은 후에는 각 클래스에 대한 확률을 계산하여 얻을 수 있다.

 

<인공 신경망>

인공신경망(Artificial Neural Network, ANN)은 1장에서 배웠듯이, 딥러닝의 다른 이름이기도 하다. 그림으로 신경망을 나타낸다면 위와 같이 나타낼 수 있는데, z1, z2, ..., z10 의 항목들은 티셔츠, 바지,... 등의 항목이다. 여기서 클래스가 총 10개 이므로 z10까지 계산을 하고, 이를 바탕으로 클래스를 예측한다. 신경망의 최종 값을 만든다는 의미에서 출력층(output layer) 이라고 부른다.

인공신경망에서는 z 값을 계산하는 단위를 뉴런(neuron)이라 부르는데, 유닛(unit)이라 부르기도 한다. x1, x2, ..., x784 까지의 항목은 입력층(input layer) 이라 부르는데, 입력층은 픽셀값 자체이고 특별한 계산을 수행하지는 않는다.

z1을 만들기 위해 x1픽셀에 곱해지는 가중치는 w1,1 이라 쓰고 z2를 만들기 위해 x1픽셀에 곱해지는 가중치는 w1,2 이라고 표기하였다. 절편은 뉴런마다 하나씩이기 때문에 b1, b2 등으로 표기하였다.

인공 신경망의 발견으로 올라가자면, (생물학적) 뉴런은 수상 돌기로부터 신호를 받아 신호체에 모은다. 이 신호가 어떤 임계값(threshold)에 도달하면 축삭 돌기를 통해 다른 세포에 신호를 전달한다. (생물학적) 뉴런이 신호를 전달하는 과정에서 영감을 받아 구현한 것이 인공신경망이다. 인공 신경망은 우리 뇌에 있는 뉴런과 같지는 않지만 머신러닝 알고리즘이 해결하지 못했던 문제에서 좋은 성능을 발휘하는 새로운 종류의 머신러닝 알고리즘이다.

인공 신경망, 혹은 심층 신경망(Deep Neural Network, DNN)을 딥러닝이라고 부른다. 심층 신경망은 여러 개의 층을 가진 인공 신경망이다.

 

<텐서플로와 케라스>

텐서플로(tensorflow)는 구글이 오픈소스로 공개한 딥러닝 라이브러리다. 이 때를 기점으로 딥러닝에 관심을 가지는 개발자들이 폭발적으로 증가했고, 텐서플로 출시 이후 알파고가 이세돌9단을 이기며 더욱 폭발적으로 딥러닝 분야가 성장하였다.

텐서플로는 저수준 API와 고수준 API가 있는데, 케라스(keras)가 고수준 API다. 딥러닝 라이브러리가 머신러닝 라이브러리와 다른점은 GPU를 사용하여 인공 신경망을 훈련한다는 점이다. GPU는 벡터연산, 행렬연산에 최적화되어있기 때문에 곱셈과 덧셈계산이 많은 인공 신경망을 계산할 때 많이 사용한다.

케라스 라이브러리는 직접 GPU 연산을 수행하지는 않고, GPU 연산을 수행하는 라이브러리를 백엔드로 사용한다. 예를 들면 텐서플로가 케라스의 백엔드 중 한개이다. 그 외에도 Theano, CNTK 등의 케라스 백엔드 라이브러리가 있다. 구글은 Tensorflow 2.0 이후 대부분의 고수준 API를 정리하고 Keras API만 남겼다. 그래서 거의 Keras와 Tensorflow는 동일한 개념이라고 생각해도 무방하다. 개인적으로 이러한 역사(?) 를 좋아해서 정리해봤다. ㅎㅎㅎ

import tensorflow as tf
from tensorflow import keras

텐서플로에서 케라스를 사용하려면 위와 같이 임포트하여 사용할 수 있다.

 

<인공 신경망으로 모델 만들기>

로지스틱 회귀에서는 교차 검증을 사용하여 모델을 평가하지만, 인공 신경망에서는 교차 검증을 사용하지 않고 검증(validation) 세트를 별도로 덜어내어 사용한다. 그 이유는

첫째로, 딥러닝 데이터셋은 너무 크기 때문이다. 그래서 따로 검증 세트를 덜어내어 사용해도 검증 점수가 안정적이다.
둘째로, 교차 검증을 계산하는 시간이 너무 오래걸린다. 훈련하는 데만 해도 며칠이 걸릴 수 있는데, 검증까지 한다면..?

from sklearn.model_selection import train_test_split
train_scaled, val_scaled, train_target, val_target = train_test_split(train_scaled, train_target, test_size=0.2, random_state=42)

print(train_scaled.shape, train_target.shape)
print(val_scaled.shape, val_target.shape)

test_size=0.2 로 지정하여 훈련 세트의 20%를 검증 세트로 덜어내었다. 훈련 세트 48,000개와 검증 세트 12,000개로 나뉘었다!

밀집층(dense layer)

케라스의 레이어에는 다양한 층이 준비되어 있는데, 그 중 가장 기본은 밀집층(dense layer)이다. 바로 위의 그림 중 밀집층의 그림을 보면, 10개의 뉴런이 모두 연결된 것을 생각해본다면 784 개의 픽셀이기 때문에 총 7,840개의 연결된 선을 볼 수 있다. 양쪽의 뉴런을 모두 연결하기 때문에 완전 연결층(Fully Connected Layer) 이라고 부른다.

인공 신경망

dense = keras.layers.Dense(10, activation='softmax', input_shape=(784,))
model = keras.Sequential(dense)

밀집층을 만들기 위해 매개변수를 뉴런 개수를 10개로 지정하고, 뉴런에서 출력되는 값을 확률로 바꾸기 위해서는 softmax 함수를 사용한다. activation 매개변수에 함수를 지정할 수 있고, 만약 2개의 클래스를 분류하는 이진분류라면 sigmoid 함수를 사용할 수 있다.

Sequential 클래스를 사용하면 앞서 만든 밀집층의 객체 dense를 전달할 수 있다. 소프트맥스와 같이 뉴런의 선형 방정식 계산 결과에 적용되는 함수를 활성화 함수(activation function)라 부른다.

 

<인공 신경망으로 패션 아이템 분류하기>

model.compile(loss='sparse_categorical_crossentropy', metrics='accuracy')
print(train_target[:10])

 

케라스 모델에서 손실 함수의 종류를 지정해줘야한다. 이진 분류는 binary_crossentropy, 다중 분류는 sparse_categorical_crossentropy 로 사용한다.

 

이진 크로스 엔트로피 손실을 위해 -log(예측확률)에 타깃값(정답)을 곱할 수 있다. 이진 분류에서는 출력층의 뉴런이 하나이기 때문에 이 뉴런이 출력하는 확률값 a를 사용하여 양성 클래스와 음성 클래스에 대한 crossentropy를 계산할 수 있다.

두번째 뉴런의 활성화 출력만 남기려면 해당 두번째 원소만 1이고 나머지는 0으로 타깃값을 준비해야 한다. 이런 것을 원-핫 인코딩(one-hot encoding)이라 한다.

 

모델을 돌려보면 evaluate() 메서드가 fit() 메서드와 비슷한 출력을 보여주는 것을 알 수 있다.


# 기본미션

7-1.

Q1. 어떤 인공 신경망의 입력 특성이 100개이고 밀집층에 있는 뉴런 개수가 10개일 때 필요한 모델 파라미터의 개수는 몇 개일까? 
> 10개의 뉴런이 100개의 입력과 연결되기 때문에 1,000개의 가중치가 있고 뉴런마다 1개의 절편이 있기 때문에 1,010개의 모델 파라미터가 있다. 

Q2. 케라스의 Dense 클래스를 사용해 신경망의 출력층을 만들려고 한다. 이진 분류 모델이라면 activation 매개변수에 어떤 활성화함수를 지정해야 할까?
> 이진 분류일 경우 sigmoid를 사용한다.

Q3. 케라스 모델에서 손실함수와 측정지표를 지정하는 메서드는 무엇일까?
> compile() 메서드를 통해 loss 매개변수로 손실함수를 지정하고 metrics 매개변수에서 측정하는 지표를 지정할 수 있다.

Q4. 정수 레이블을 타깃으로 가지는 다중 분류 문제일 때 케라스 모델의 compile() 메서드에 지정할 손실함수로 적절한 것은 무엇일까?
> 타깃값이 다중일 경우에 'sparse_categorical_crossentropy' 를 사용한다.

 

# 선택미션

7-2

Q1. 모델의 add() 메서드 사용법이 올바른 것은 어떤 것일까?
> model.add(keras.layers.Dense(10, activation='relu')

Q2. 크기가 300 x 300인 입력을 케라스 층으로 펼치려고 할 때 어떤 층을 사용해야 할까?
> 입력의 차원을 일렬로 펼치려면 Flatten을 사용한다.

Q3. 이미지 분류를 위한 심층 신경망에 널리 사용되는 케라스의 활성화함수는 무엇일까?
> 'relu' 는 이미지 처리를 위해 자주 사용되는 함수이다.

Q4. 적응적 학습률을 사용하지 않는 옵티마이저는 무엇일까?
> 'sgd'는 모두 일정한 학습률을 사용한다.

 


● 마무리

이번 챕터에서는 딥러닝에서 사용하는 기본적인 개념에 대해 배웠다. 딥러닝은 머신러닝과 다른 부분이 다소 있고, '신경망'이라는 개념이 들어가기 때문에 확실히 입체적으로 접근할 수 있다. 하지만 더 깊게 들어가려면 수학적인 지식이 기본적으로 필요하기 때문에, 선수과목으로 선형대수나 통계/행렬을 배우는 것을 추천한다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter07_deep_learning.ipynb
0.05MB

06. 비지도 학습

 

06-1 군집 알고리즘

  • 시작하기 전에


과일의 여러가지 사진을 어떤 과일인지 찾기 위해 자동으로 분류하는 프로그램을 만들어 보려 한다.

 

<타깃을 모르는 비지도 학습>

chapter 2에서 배운 것처럼 타깃을 모르거나 타깃이 없을 때 사용하는 머신러닝 알고리즘을 비지도 학습(unsupervised learning)이라고 한다. 가르쳐 주지 않아도 데이터에 있는 무언가를 학습하도록 하기 위한 방법이 무엇이 있을까?

<과일 사진 데이터 준비하기>

!wget https://bit.ly/fruits_300_data -O fruits_300.npy

처음에 숫자 0 인줄 알았는데, 계속 에러가 나길래 봤더니 open 의 알파벳 O 였다...

import numpy as np
import matplotlib.pyplot as plt

fruits = np.load('fruits_300.npy')
print(fruits.shape)
print(fruits[0, 0, :])

넘파이 배열의 데이터를 가져와서 첫 번째 행을 출력하면 위와 같은 리스트가 나오는 것을 볼 수 있다.

 

plt.imshow(fruits[0], cmap='gray')
plt.show()

imshow() 메소드를 사용하여 cmap 매개변수를 gray로 지정하여 흑백사진으로 이미지를 출력하여 볼 수 있다. 0에 가까울수록 검게 나타나고 높은 값을 밝게 표시된다. 이미지를 이렇게 반전시킨 이유는 우리의 관심사가 바탕이 아닌 물체(과일)에 있기 때문이다. 또한 우리가 바라보는 것과 컴퓨터가 이미지를 인식하고 처리하는 방식은 다르다는 것을 염두에 두어야 한다.

 

plt.imshow(fruits[0], cmap='gray_r')
plt.show()

cmap 매개변수를 gray_r로 지정하여 이미지를 재반전 후 출력하면 우리 눈에 좀 더 편한 이미지가 출력된다. 

 

fig, axs = plt.subplots(1, 2)
axs[0].imshow(fruits[100], cmap='gray_r')
axs[1].imshow(fruits[200], cmap='gray_r')
plt.show()

바나나와 파인애플의 이미지도 위와같은 코드로 출력할 수 있다. subplot() 메서드를 활용하면 여러개의 그래프를 배열처럼 쌓을 수 있다. 

 

<픽셀값 분석하기>

100 x 100의 다차원 배열의 이미지를 펼쳐서 길이가 10,000인 1차원 배열로 만들 수 있다. 이렇게 만들면 데이터를 다룰 때 배열 계산을 훨씬 쉽게 진행할 수 있다.

 

apple = fruits[0:100].reshape(-1, 100*100)
pineapple = fruits[100:200].reshape(-1, 100*100)
banana = fruits[200:300].reshape(-1, 100*100)

print(apple.shape)

reshape() 메서드를 활용하여 두번째 차원과 세번째 차원을 합친 다음 과일의 배열의 크기를 확인해 볼 수 있다.

 

평균값을 확인해본다.

plt.hist(np.mean(apple, axis=1), alpha=0.8)
plt.hist(np.mean(pineapple, axis=1), alpha=0.8)
plt.hist(np.mean(banana, axis=1), alpha=0.8)
plt.legend(['apple', 'pineapple', 'banana'])
plt.show()

히스토그램을 그려 보면 바나나는 대체적으로 40 이하의 평균값을, 사과와 파인애플은 80 - 100 사이의 평균값을 띄는 것을 알 수 있다. 하지만 사과와 파인애플은 많이 겹쳐 있어서 그래프로만 보기에는 구분하기 쉽지 않다.

fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].bar(range(10000), np.mean(apple, axis=0))
axs[1].bar(range(10000), np.mean(pineapple, axis=0))
axs[2].bar(range(10000), np.mean(banana, axis=0))
plt.show()

막대그래프를 그려보니 과일마다 값이 높은 구간이 다른 것을 알 수 있다. 다시 평균값을 100 x 100 크기로 바꿔 출력해보도록 하겠다.

 

apple_mean = np.mean(apple, axis=0).reshape(100, 100)
pineapple_mean = np.mean(pineapple, axis=0).reshape(100, 100)
banana_mean = np.mean(banana, axis=0).reshape(100, 100)
fig, axs = plt.subplots(1, 3, figsize=(20, 5))
axs[0].imshow(apple_mean, cmap='gray_r')
axs[1].imshow(pineapple_mean, cmap='gray_r')
axs[2].imshow(banana_mean, cmap='gray_r')
plt.show()

각각의 이미지와 가까운 사진을 고르면 각 과일을 구분할 수 있지 않을까?

 

<평균값과 가까운 사진 고르기>

평균값과 가까운 사진을 고르기 위해 3장에서 배운 평균 절댓값 오차를 사용할 수 있다.

abs_mean은 각 샘플의 오차 평균이므로 크기가 300인 1차원 배열임을 알 수 있다.

 

apple_index = np.argsort(abs_mean)[:100]
fig, axs = plt.subplots(10, 10, figsize=(10, 10))
for i in range(10):
  for j in range(10):
    axs[i, j].imshow(fruits[apple_index[i*10 + j]], cmap='gray_r')
    axs[i, j].axis('off')
plt.show()

평균 값과 가장 오차가 적은 순서대로 이미지를 골라 10 x 10 배열로 출력한 결과다. axis() 매개변수를 'on'으로 설정하면 좌표값이 함께 출력된다. 여하튼, 이렇게 비슷한 샘플끼리 그룹으로 모으는 작업을 군집(clustering)이라고 한다. 군집은 대표적인 비지도 학습 작업 중 하나로, 군집 알고리즘에서 만든 그룹을 클러스터(cluster) 라고 한다.

 


06-2  k-평균

  • 시작하기 전에

    앞서 군집화를 할 때, 사과/파인애플/바나나 라는 것을 알고 있었지만, 실제 비지도 학습에서는 그러한 정보가 없이 진행된다. 이런 경우 k-평균(k-means) 군집 알고리즘을 통해 평균값을 자동으로 찾을 수 있다. 이 평균값이 클러스터의 중심에 위치하기 때문에 클러스터 중심(cluster center) 또는 센트로이드(centeroid) 라고 부른다.

<k-평균 알고리즘>

# 기본미션

k-평균 알고리즘의 작동 방식은 아래와 같다.

  1. 무작위로 k개의 클러스터 중심을 정한다.
  2. 각 샘플에서 가장 가까운 클러스터 중심을 찾아 해당 클러스터의 샘플로 지정한다.
  3. 클러스터에 속한 샘플의 평균값으로 클러스터 중심을 변경한다.
  4. 클러스터 중심에 변화가 없을 때까지 2번으로 돌아가 반복한다.

알고리즘 작동 방식을 그림으로 그리면 위와 같다. 클러스터 중심을 랜덤하게 지정한 후 중심에서 가장 가까운 샘플을 하나의 샘플로 묶으면(1) 과일이 분류된 것을(2) 확인할 수 있다. 클러스터에는 순서나 번호는 의미가 없다. 여기서 클러스터 중심을 다시 계산한 다음 가장 가까운 샘플을 묶으면(2) 분류된 것을(3) 확인할 수 있다. 여기서 3의 결과와 2의 결과가 동일하므로 클러스터에 변동이 없다는 것을 캐치하고 k-평균 알고리즘을 종료하면 된다.

<KMeans 클래스>

fruits = np.load('fruits_300.npy')
fruits_2d = fruits.reshape(-1, 100*100)

from sklearn.cluster import KMeans
km = KMeans(n_clusters=3, random_state=42)
km.fit(fruits_2d)

print(km.labels_)
print(np.unique(km.labels_, return_counts=True))

 

k-평균 알고리즘은 KMeans() 클래스에 구현되어 있는데, n_clusters 매개변수를 활용하여 클러스터의 갯수를 지정해줄 수 있다. 주의할 점은, 비지도 학습이기 때문에 fit() 메서드에서 타깃 데이터를 사용하지 않는다는 점이다. 배열을 출력하여 샘플이 0, 1, 2 중 어떤 레이블에 해당되는지 확인하고 각 클러스터의 샘플의 갯수를 출력해 보았다. 

def draw_fruits(arr, ratio=1):
  n = len(arr)
  rows = int(np.ceil(n/10))
  cols = n if rows < 2 else 10
  fig, axs = plt.subplots(rows, cols, figsize=(cols*ratio, rows*ratio), squeeze=False)

  for i in range(rows):
    for j in range(cols):
      if i*10 + j < n:
        axs[i, j].imshow(arr[i*10 + j], cmap='gray_r')
      axs[i, j].axis('off')
  plt.show()

draw_fruits() 함수를 만들고 레이블이 0인 과일 사진을 모두 그려볼 수 있다. 넘파이 자료형에서는 불리언 배열을 사용해 원소를 선택할 수 있고 이러한 것을 불리언 인덱싱(boolean indexing)이라고 한다. 더 자세한 내용은 4장에서 소개했었다. 즉, True인 원소만 추출하는 것이다.

레이블이 0인 클러스터는 파인애플이 다수이면서도, 사과와 바나나도 일부 섞인 것을 알 수 있다.

<클러스터 중심>

KMeans 클래스가 찾은 최종 중심은 cluster_centers_라는 속성에 저장되어 있다. 이 배열은 2d 이미지 샘플을 중심으로 이루어졌기 때문에 이미지로 출력하려면 100 x 100 크기로 다시 바꿔야 한다. 

print(km.transform(fruits_2d[100:101]))
print(km.predict(fruits_2d[100:101]))
print(km.n_iter_)

draw_fruits(fruits[100:101])

 

KMeans 클래스에서도 transform(), predict() 메서드가 있기 때문에 이를 활용하여 가장 가까운 클러스터 중심을 예측 클러스터로 출력하거나 예측할 수 있다. 또 n_iter_ 속성을 출력하여 반복횟수를 출력해볼 수 있다.

<최적의 k 찾기>

k-평균 알고리즘의 단점은 클러스터 개수를 사전에 지정해야 한다는 것이다. 적절한 k 값을 찾기 위한 몇 가지 도구가 있는데 엘보우(elbow) 방법이 대표적이다. k-평균 알고리즘은 클러스터 중심과 클러스터에 속한 샘플 사이의 거리를 잴 수 있다. 이 거리의 합을 이너셔라고 하는데, 클러스터에 속한 샘플이 얼마나 가깝게 모여 있는지를 나타내는 값이다.

일반적으로 클러스터 개수가 늘어나면 클래스터 각각의 크기는 줄어들기 때문에 이너셔도 줄어든다. 엘보우 방법은 클러스터 개수를 늘려가면서 이너셔의 변화를 관찰하여 최적의 클러스터 갯수를 찾는 방법이다.

inertia = []
for k in range(2, 7):
  km = KMeans(n_clusters=k, random_state=42)
  km.fit(fruits_2d)
  inertia.append(km.inertia)
plt.plot(range(2, 7), inertia)
plt.xlabel('k')
plt.ylavel('inertia')
plt.show()

 

이너셔를 그래프로 그리면 감소하는 속도가 꺾이는 지점이 있는데, 이 지점부터는 클러스터 개수를 늘려도 클러스터에 밀집된 정도가 크게 개선되지 않는다. 이 지점이 마치 팔꿈치 모양과도 같아서 엘보우 방법이라고 부른다고 한다.


06-3  주성분 분석

  • 시작하기 전에

    k-평균 알고리즘으로 나눠진 사진들을 군집이나 분류에 영향을 끼치지 않으면서 용량을 줄여서 저장할 방법이 없을까?


<차원과 차원 축소>

데이터가 가진 속성을 특성이라고 불렀는데, 머신 러닝에서는 이런 특성을 차원(dimension)이라 부른다. 앞서 다뤘던 과일 사진의 경우 10,000개의 픽셀 = 10,000개의 특성 = 10,000개의 차원이라는 뜻이다.

비지도 학습 작업 중 차원 축소(dimensionality reduction) 알고리즘이 있는데, 3장에서 차원이 많아지면 선형 모델의 성능이 높아지고 train model에 과적합 되기 쉽다는 것을 배웠다. 차원 축소는 데이터를 가장 잘 나타내는 특성을 선택하여 데이터의 크기를 줄이고 지도 학습 모델의 성능을 향상시킬 수 있는 방법이다. 

대표적인 차원 축소 알고리즘인 주성분 분석(Principal Component Analysis)PCA라고도 부른다.

 

<주성분 분석 소개>

주성분 분석은 데이터에 있는 분산이 큰 방향을 찾는 것이다. 분산은 데이터가 널리 퍼져있는 정도를 말하는데, 분산이 큰 방향을 데이터로 잘 표현하는 벡터라고 생각할 수 있다.

위와 같은 2차원 벡터가 있을 때 대각선 방향이 분산이 가장 크다고 볼 수 있다. 이 벡터를 주성분(principatl component) 이라 부른다. 

주성분 벡터의 원소 개수는 원본 데이터셋의 특성 개수와 같고, 주성분을 사용해 차원을 줄일 수 있다. S(4,2)를 주성분에 직각으로 투영하여 1차원 데이터 P(4,5)를 만들 수 있다. 주성분은 원본 차원과 같고 주성분으로 바꾼 데이터는 차원이 줄어든다.

첫번째 주성분을 찾고 이 벡터에 수직이고 분산이 가장 큰 다음 방향을 찾으면 두번째 주성분이 된다. 일반적으로 주성분은 원본 특성의 개수만큼 찾을 수 있다.

<PCA 클래스>

 

from sklearn.decomposition import PCA
pca = PCA(n_components=50)
pca.fit(fruits_2d)

print(pca.components_.shape)
print(fruits_2d.shape)

fruits_pca = pca.transform(fruits_2d)
print(fruits_pca.shape)

주성분 학습도 비지도 학습이기 때문에 fit() 메서드에 타깃값을 제공하지 않는다. 이 클래스가 찾은 주성분은 components_ 속성에 저장되어 있다. 이 배열의 크기를 확인해보니 n_components=50으로 지정했기 때문에 첫번째 차원이 50이고, 즉 50개의 주성분을 찾은 것을 알 수 있다. 두번째 주성분은 항상 원본 데이터의 특성 갯수인 10,000이다.

 

draw_fruits(pca.components_.reshape(-1, 100, 100))

 

원본 데이터와 차원이 같으므로 이미지처럼 출력해볼 수 있다. 위 주성분은 원본 데이터에서 가장 분산이 큰 방향을 순서대로 나타낸 것이다.

 

 

 

# 선택미션

6-3 확인문제 

2. 샘플개수가 1,000개 이고 특성이 100개인 데이터셋의 크기는 (1,000, 100) 이다. 이 데이터를 PCA를 사용해 10개의 주성분을 찾아 변환했을 때 변환된 데이터셋의 크기는 특성 개수만 바뀌므로 (1,000, 10) 이 된다.

3. 2번문제에서 설명된 분산이 가장 큰 주성분은 분산이 가장 큰 방향부터 찾기 때문에 첫번째 주성분이라고 볼 수 있다.


● 마무리

이번 챕터에서는 비지도 학습인 군집, 클러스터링, k-평균, 주성분(PCA) 분석을 배웠다. 비지도 학습에서 차원을 줄일 수 있을거라곤 생각도 못했는데 개인적으로 PCA라는 개념이 낯설면서도 신기하고 재미있고 유용했다. 

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter06_unsupervised_learning.ipynb
1.30MB

 

족장님, 한빛미디어 사랑합니다..♥


05. 트리 알고리즘

 

05-1 결정 트리

  • 시작하기 전에
    와인을 알코올, 당도, pH 값을 활용하여 분류해보려고 한다.

 

<로지스틱 회귀로 와인 분류하기>

import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
wine.info()

데이터를 불러온 후 info() 메서드를 사용하면 데이터프레임의 각 열의 데이터 타입과 누락된 데이터 여부를 알 수 있다. 6497개의 샘플 중 non-null count가 6497 이므로 누락된 값은 없는 것을 알 수 있다.

만약 누락된 값이 있다면 그 데이터를 버리거나 평균값으로 채운 후 사용할 수 있는데, 그렇게 하는 과정에서도 훈련 세트의 통계 값으로 테스트세트를 변환해야 한다. 즉, 훈련 세트의 평균 값으로 테스트 세트의 누락값을 채워야 한다.

wine.head()

.

head() 메서드는 데이터프레임의 처음 5개 샘플을 보여준다. 0이면 레드와인, 1이면 화이트와인이라고 한다.

 

wine.describe()

describe() 메서드는 열에 대한 간략한 통계를 출력해준다. 최솟값, 최댓값, 평균값 등을 볼 수 있다. 이를 통해 각 항목들의 스케일이 다른 것을 알 수 있고, 해당 데이터를 사용하기 전에 특성을 표준화해줘야 한다.

 

data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size=0.2, random_state=42)

print(train_input.shape, test_input.shape)

train_test_split() 메서드는 기본적으로 25%의 설정값으로 테스트 세트를 지정한다. 하지만 샘플 개수가 충분히 많다면 사이즈를 조절할 수 있다. test_size 매개변수를 활용하여 값을 지정할 수 있다. 5197개의 훈련, 1300개의 테스트 세트로 나뉘었다.

 

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target)) 

print(lr.coef_, lr.intercept_)

표준정규화를 사용한 후 로지스틱 회귀모델을 훈련해 본 결과 77~78의 훈련 점수가 나왔다. 둘의 점수가 모두 낮으니 과소적합이 된 것으로 추정된다. 회귀가 학습한 계수와 절편값을 출력한 결과를 들여다보려고 한다.

 

 

<설명하기 쉬운 모델과 어려운 모델>

 

앞서 확인한 계수와 절편값을 설명하기에는 너무 어렵다. 이 모델이 왜 저런 계수 값을 학습했는지 '이해'한다기보다는 '추측'에 가까울 것 같다. 숫자로는 이해할 수 없기 때문에 순서도를 그리며 설명한다면 조금 더 이해도가 높아지지 않을까?

 

<결정 트리>

결정트리(Decision Tree) 모델이란 마치 스무고개처럼 하나씩 질문을 던져 정답을 맞춰가는 모델이다. 즉, 진실(True)과 거짓(False)을 통해 예/아니오로 질문을 이어가며 정답을 찾아 학습한다. 데이터를 잘 나눌 수 있는 질문을 찾는다면 질문을 추가해서 분류 정확도를 높일 수 있다. 사이킷런이 결정트리 알고리즘을 제공한다.

 

from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(train_scaled, train_target)

print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))

결정트리 알고리즘을 활용했더니 훨씬 점수가 높아졌다. 하지만 훈련세트에만 높은 점수로 미루어 보아 과대적합이 발생한 것으로 추측된다. 사이킷런의 메서드를 통해 이 모델을 그림으로 표현할 수 있다.

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree
plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()

plot_tree() 메서드를 통해 트리를 그려볼 수 있다. 일반적으로 나무는 밑에서 위로 자라지만, 결정 트리는 위에서부터 아래로 탑다운(Top-down) 형태로 자란다. 맨 위의 파란색으로 동그라미한 노드루트 노드(root node)라 하고 화살표 모양으로 표시한 아래쪽에 주렁주렁 달린 노드리프 노드(leaf node)라고 한다.

회귀 모델의 경우 최종 도달한 리프 노드의 값의 평균이 새로운 샘플 x에 대한 예측값이라고 할 수 있다. 만약 분류 모델의 경우 k-최근접 이웃 알고리즘과 마찬가지로, 최종 조달한 라프 노드의 값 중 다수의 샘플들을 통해 예측할 수 있다. 

노드란 결정 트리를 구성하는 핵심 요소로, 훈련 데이터의 특성에 대한 테스트를 표현한다. 하지만 이렇게 보기엔 복잡하니 트리의 깊이를 제한해서 출력할 수 있다.

max_features 매개변수를 통해 사용할 특성의 갯수를 지정할 수 있다. 기본값은 None이라 모든 값을 사용하게 되는 것이고, 만약 특성의 갯수보다 작다고 하면 random하게 특성을 선택한다.

plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

plot_tree() 메서드를 사용하면 지정한만큼만 트리를 출력할 수 있다. max_depth 매개변수를 1로 준다면 루트 노트를 제외하고 추가적으로 하나의 노드를 더 확장하여 그린다. filled 매개변수는 클래스에 맞게 노드의 색을 칠할 수 있다. feature_names 매개변수에는 특성의 이름을 전달할 수 있다.

여기서 그림이 담고 있는 정보를 하나씩 살펴보면 루트노드는 당도가 -0.239 이하인지에 대해 묻는 것이고, 작거나 같으면 좌측, 크면 우측으로 이동하는 것이다. 그에 따라 이동한 sample은 루트노드의 5197개 샘플 중 좌측이 2922개, 우측이 2275개다. 또한 plot_tree() 함수에서 filled=True로 지정하면 클래스마다 색상을 부여하고, 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표현한다고 한다.

결정트리에서는 리프 노트에서 가장 많은 클래스가 예측 클래스가 되고 만약 여기서 트리의 성장을 멈춘다면 좌측 샘플과 우측 샘플 모두 양성 클래스의 갯수가 많기 때문에 모두 양성 클래스로 예측이 될 수 있다.

노드 상자안에 gini라는 것이 있는데 이것에 대해 좀 더 자세히 알아보려 한다.

 

<불순도>

gini는 지니 불순도를 의미한다. 불순도는 결정 트리가 최적의 질문을 찾기 위한 기준이다. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값은 'gini'이다.  criterion 매개변수는 노드에서 데이터를 분할할 기준을 정하는 것인데 앞서 당도 -0.239를 기준으로 나눈 것 또한 criterion 매개변수에 지정한 지니 불순도(Gini impurity)를 사용한 것이다.

 

지니 불순도클래스의 비율을 제곱해서 더한 다음 1에서 빼는 것이 계산 방법이다. 즉, 음성 클래스의 비율을 제곱한 것과 양성 클래스의 비율을 제곱한 것을 더해서 1에서 빼면 된다.

앞서 나뉜 샘플의 불순도를 계산하면 0.367이 나오고, 만약 정확히 절반씩 나뉜다면 0.5의 지니 불순도가 나타나는 것을 알 수 있다. 노드에 하나의 클래스만 있다면 지니 불순도는 0이 되고 이러한 노드를 순수노드라고 부른다.

부모의 불순도와 자식의 불순도를 뺐을 때 그 차이가 가장 크게 나오는 방향으로 노드를 분할하는 것이 결정트리의 방향이다. 이 불순도의 차이를 정보 이득(information gain)이라고 부른다. 하지만 사이킷런에서 제공하는 또 다른 불순도 기준이 있는데, criterion='entropy'를 지정하여 엔트로피 불순도를 사용할 수 있다. 이 불순도도 클래스 비율을 사용하지만 제곱이 아닌 밑이 2인 로그를 사용한다.

노드를 순수하게 나룰수록 정보 이득이 커지는데, 앞선 트리에서는 무한하게 자람에 따라 과대적합이 발생한 것을 알 수 있다.

 

<가지치기>

결정 트리도 무한하게 자라지 않도록 가지치기가 필요하다. 왜냐하면 과대적합이 발생할 수 있기 때문이다. 보통은 트리의 최대 깊이를 지정함으로써 설정이 가능하다. 

dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))

max_depth 매개변수를 3으로 지정함으로써 루트 노드 아래로 최대 3개의 노드까지만 성장하도록 설정하였다. 이를 트리 그래프로 그려볼 수 있다.

plt.figure(figsize=(20,15))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

맨 위의 루트노드가 깊이 0, 가장 밑의 리프노드가 깊이 3이다. 깊이 1의 노드는 모두 당도를 기준으로 훈련 세트를 나눈다. 하지만 깊이 2의 노드는 맨 왼쪽만 당도(sugar)로 나누고, 왼쪽에서 두 번째 노드는 도수(alcohol)로 나누는 것을 알 수 있다. 오른쪽 두 노드는 pH를 기준으로 나눈다.

깊이 3의 노드 중 유일하게 붉은 빛을 내는 세 번째 노드만 음성 클래스가 더 많다. 이 노드에 도달해야만 레드 와인으로 인식을 하고, 조건은 알코올 도수가 0.454보다 작으며 당도는 -0.802와 -0.239 사이어야 한다.

하지만 당도가 음수로 된 것이 좀 이상하다... 결정 트리 알고리즘의 장점은 특성값의 스케일이 알고리즘에 영향을 미치지 않는다는 점이다. 따라서 표준화 전처리를 할 필요가 없다.

dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_input, train_target)
print(dt.score(train_input, train_target))
print(dt.score(test_input, test_target))

전처리 하기 전의 훈련 세트와 테스트 세트로 트리 모델을 다시 훈련하니 아까의 결과와 정확히 일치하는 것을 볼 수 있다.

 

plt.figure(figsize=(20,15))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

트리를 그려보면, 특성값을 표준점수로 바꾸지 않아 훨씬 이해하기 쉽다. 즉, 당도가 1.625 와 4.325 사이에 있으면서 알코올 도수가 11.025 와 같거나 작은 것이 레드 와인이고 그 외에는 전부 화이트 와인임을 알 수 있다.

결정 트리는 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 계산해준다. 

 

결정 트리 모델의 feature_importances_ 속성을 통해, 두번째 속성인 당도가 0.87 정도로 특성 중요도가 높은 것을 알 수 있다. 이들을 모두 더하면 1이 된다. 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산한다. 이를 활용하면 결정 트리 모델을 특성 선택에 활용할 수 있다.

# 기본미션

결정트리 예시


05-2 교차 검증과 그리드 서치

  • 시작하기 전에

max_depth를 바꾸면 성능의 변화가 있지 않을까? 또 일반화 성능을 올바르게 예측하려면 테스트 세트로 만들어서 테스트 세트로 평가하면 안 되지 않을까?

 

<검증 세트>

테스트 세트를 사용하지 않으면 모델의 과대/과소 적합 여부를 판단할 수 없다. 테스트 세트를 사용하지 않고 측정하는 법은 훈련 세트를 또 나누는 것이다. 이 데이터를 검증 세트(validation set)라고 부른다.

import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')

data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size=0.2, random_state=42)

sub_input, val_input, sub_target, val_target = train_test_split(train_input, train_target, test_size=0.2, random_state=42)

print(sub_input.shape, val_input.shape)

데이터를 읽어와서 테스트와 훈련 세트로 나누고 다시 훈련세트와 검증세트로 나눈다. test_size 매개변수를 0.2로 지정하여 train_input의 약 20%를 val_input으로 만들고 크기를 확인해보면 훈련 세트가 4,157개, 검증 세트가 1,040개로 나눠진 것을 확인할 수 있다.

from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)
print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target))

모델을 평가해보니 훈련 세트에 과적합이 된 것을 확인할 수 있다.

 

<교차 검증>

검증 세트를 너무 조금 떼어 놓으면 점수가 들쭉날쭉하고 불안정하다. 이럴 때 교차 검증(cross validation)을 이용하면 더욱 안정적인 점수를 얻고 더 많은 데이터를 사용할 수 있다. 교차 검증은 검증 세트를 떼어 평가하는 과정을 여러번 반복한다. 그 다음 이 점수를 평균하여 최종 검증 점수를 얻는다.

위의 그림은 3-폴드 교차 검증인데 훈련 세트를 세 부분으로 나눠서 교차 검증을 수행하는 것을 3-폴드 교차 검증이라고 한다. 통칭 k-fold 교차 검증(k-fold cross validation)이라고 하며, 훈련 세트를 몇 부분으로 나누냐에 따라 다르게 부른다.

from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)

cross_validate() 메서드를 통해 평가할 모델 객체를 매개변수로 전달한다. 이 함수는 딕셔너리를 반환하고, 처음 2개의 키는 각각 모델을 훈련하는 시간과 검증하는 시간을 의미한다. 각 키마다 5개의 숫자가 담겨 있고 기본적으로 5-폴드 교차 검증을 수행한다.

 

import numpy as np
print(np.mean(scores['test_score']))

교차 검증을 통해 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있다. 하지만 cross_validate() 함수는 훈련 세트를 섞어 폴드를 나누지 않기 때문에 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기(splitter)를 지정해야 한다.

from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))

splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))

KFold 클래스도 동일하게 사용할 수 있다. n_splits 매개변수는 몇 폴드 교차 검증을 할지 정한다. 

<하이퍼파라미터 튜닝>

모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 한다. 이것을 튜닝하는 작업은 먼저 라이브러리가 제공하는 기본값을 사용해 훈련을 한 다음 검증 세트의 점수나 교차 검증을 통해 조금씩 바꾸는 순서로 진행된다. 보통 모델은 최소 1-2개에서 5-6개까지의 매개변수를 제공한다.

from sklearn.model_selection import GridSearchCV
params = {'min_impurity_decrease' : [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

dt = gs.best_estimator_
print(dt.score(train_input, train_target))

결정 트리 모델에서 최적의 max_depth 값을 찾았을 때, 값을 고정하고 min_samples_split을 바꿔가며 최적의 값을 찾는 과정이 하이퍼파라미터 튜닝이 아니다. 왜냐하면 min_samples_split 매개변수의 값이 바뀌면 max_depth 의 값이 바뀌기 때문이다. 따라서 두 매개변수를 동시에 바꿔가며 최적의 값을 찾아야 한다.

매개변수가 많아질수록 문제는 더 복잡해지는데, 사이킷런에서 제공하는 그리드 서치(Grid Search)를 사용하면 편리하게 구현할 수 있다. 

fit() 메서드를 통해 결정트리 모델의 min_samples_split 값을 바꿔가며 실행했다. GridSearchCV의 cv 매개변수 기본값은 5이다. 값마다 5-폴드 교차 검증을 수행하기 때문에 결국 25개의 모델을 훈련하는 것이다. 이처럼 많은 모델을 훈련하기 때문에 n_jobs 매개변수로 CPU 코어수를 지정할 수 있다. 기본값은 1이고 -1로 지정하면 모든 코어를 사용한다. 그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어 있다.

 

print(gs.best_params_)
print(gs.cv_results_['mean_test_score'])

best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])

0.001이 가장 좋은 값으로 선택된 것을 알 수 있다. 또한 두번째 list에서는 첫번째 값이 가장 크다. argmax() 함수를 통해 가장 큰 값의 인덱스를 추출할 수 있고, 인덱스를 활용해 params 키에 저장된 매개변수를 출력할 수 있다. 이 값이 앞서 출력한 gs.best_params_ 와 동일한지 확인해보면 같은 것을 알 수 있다.

이 과정을 정리해보자면 다음과 같다.

1. 탐색할 매개변수를 지정한다.

2. 훈련 세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다. 이 조합은 그리드 서치 객체에 저장된다.

3. 그리드 서치는 최상의 매개변수에서 전체 훈련 세트를 사용하여 최종 모델을 훈련하다. 이 모델 또한 그리드 서치 객체에 저장된다.

params = {'min_impurity_decrease' : np.arange(0.0001, 0.001, 0.0001),
          'max_depth' : range(5, 20, 1),
          'min_samples_split' : range(2, 100, 10)}

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

print(gs.best_params_)

arange() 함수는 첫 매개변수 값에서 시작하여 두 번째 매개변수에 도달할 때까지 세 번째 매개변수를 계속 더한 배열을만든다. 0.0001에서 시작하여 0.001씩 더해서 0.001이 되도록 만드는 것이다. range() 함수는 정수만 사용할 수 있는데,  max_depth 를 5에서 20까지 1씩 증가시킨다. min_samples_split 은 2에서 100까지 10씩 증가시키는 것을 알 수 있다. 9개 * 15개 * 10개 이므로 총 1,350번의 교차 검증횟수가 진행되며 5-폴드가 기본이므로 6,750개의 모델이 수행된다.

최상의 교차검증 점수를 확인해보니 이러한 값이 나왔다.

 

<랜덤 서치>

매개변수의 범위나 간격을 미리 정하기 어렵거나 너무 많은 매개변수가 있을 때 그리드 서치 시간이 오래 걸린다. 그럴 때 랜덤 서치(Random Search)를 사용하면 좋다. 랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다.

이를 위해선 싸이파이(scipy) 라이브러리를 불러와야하는데 사이킷런에서는 넘파이와 사이파이를 많이 사용한다.

from scipy.stats import uniform, randint

rgen = randint(0, 10)
rgen.rvs(10)

uniform, randint 클래스는 주어진 범위에서 균등하게 값을 뽑는다(샘플링한다). randint() 는 정숫값을 뽑고, uniform() 은 실숫값을 뽑는다.

np.unique(rgen.rvs(1000), return_counts=True)

ugen = uniform(0, 1)
ugen.rvs(10)

난수 발생기와 유사한 원리로 이루어진다. 샘플링 횟수는 최대한 크게 하는 것이 좋다. 왜냐하면 샘플링 횟수가 많을수록 최적의 매개변수를 찾을 확률도 높아지기 때문이다.

 

params = {'min_impurity_decrease' : uniform(0.0001, 0.001),
          'max_depth' : randint(20, 50),
          'min_samples_split' : randint(2, 25),
          'min_samples_leaf' : randint(1, 25),}

from sklearn.model_selection import RandomizedSearchCV
gs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), params, n_iter=100, n_jobs=-1, random_state=42)
gs.fit(train_input, train_target)

print(gs.best_params_)

0.00001에서 0.001 사이의 실숫값을 샘플링하여 매개변수에 지정한다. 매개변수를 다시 100번 샘플링하여 교차 검증을 수행한 후 최적의 매개변수 조합을 찾아 출력하면 위와 같은 결과가 나온다.

테스트 세트의 성능을 확인하면 검증 세트에 대한 점수보다 조금 작은 것이 일반적이다. 앞으로 수동으로 매개변수를 바꾸는 대신에, 훨씬 쉽게 파라미터를 조절할 수 있게 되었다.


05-3 트리의 앙상블

<정형 데이터와 비정형 데이터>

데이터를 나눌 때 보통 정형 데이터와 비정형 데이터로 많이 나누는데, 정형 데이터(structured data)는 csv파일처럼 excel, db 등 정형화 플랫폼에 저장할 수 있는 데이터를 말한다. 행과 열이 잘 구분되는 나열된 데이터들이다. 보통 대부분의 데이터가 정형 데이터다. 하지만 정형화되지 않은 데이터도 존재하는데, 데이터베이스나 엑셀로 표현하기 어려운 텍스트 데이터, 이미지, 음악 등의 데이터들을 비정형 데이터(unstructured data)라고 부른다.

정형 데이터를 다루는 데 가장 뛰어난 성과를 내는 알고리즘이 앙상블 학습이다. 앙상블 알고리즘은 대부분 결정 트리를 기반으로 만들어졌다. 비정형 데이터를 다루기 위해서는 신경망 알고리즘을 많이 사용한다.

 

<랜덤 포레스트>

랜덤 포레스트(Random Forest)는 앙상블 학습의 대표 주자로 안정적인 성능 덕분에 널리 사용되고 있다. 결정 트리를 랜덤하게 만들어 결정 트리의 숲을 만들고 각 트리의 예측을 사용해 최종 예측을 만드는 것이다.

 

예를 들어 1,000개의 가방에서 100개씩 샘플을 뽑는다면 먼저 1개를 뽑고, 뽑았던 1개를 다시 가방에 넣는다. 이런 과정으로 샘플링을 진행한다면 중복된 샘플을 뽑을 수 있다. 이렇게 만들어진 샘플을 부트스트랩 샘플(bootstrap sample)이라고 부른다. 보통 중복을 허용하여 데이터를 샘플링하는 방식을 의미한다.

사이킷런의 랜덤포레스트는 기본적으로 100개의 결정트리를 이러한 방식으로 훈련한다. 랜덤하게 선택하기 때문에 훈련 세트에 과대적합되는 것을 막아주고 안정적인 성능을 얻을 수 있다.

 

 

# 선택미션

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
wine = pd.read_csv('https://bit.ly/wine_csv_data')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

train_input, test_input, train_target, test_target = train_test_split(data, target, test_size=0.2, random_state=42)

from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_jobs=-1, random_state=42)
scores = cross_validate(rf, train_input, train_target, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

cross_validate() 함수를 사용해 교차 검증을 하고 모든 CPU 코어를 사용한 다음 출력 결과를 보면 다소 훈련 세트에 과대적합된 것을 알 수 있다. 랜덤 포레스트는 결정트리의 앙상블이기 때문에 결정트리 클래스가 제공하는 모든 매개변수를 제공한다.

rf.fit(train_input, train_target)
print(rf.feature_importances_)

결정트리의 큰 장점 중 하나인 특성 중요도를 출력해보면, 앞서 출력한 특성 중요도와 다른 것을 알 수 있다. 그 이유는 램던 포레스트가 특성을 랜덤하게 선택하여 훈련하기 때문이다. 고로 과대적합을 줄이고 일반화 성능을 높이는 데 도움이 된다.

rf =  RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)
rf.fit(train_input, train_target)
print(rf.oob_score)

랜덤 포레스트에서 부트스트랩 샘플에 포함되지 않고 남았던 샘플을 OOB(out of bag) 샘플이라고 하는데, 이 샘플을 사용하여 결정트리를 평가할 수 있다. 마치 검증세트처럼 oob_score 매개변수를 True로 지정하여 점수를 출력해볼 수 있다.


● 마무리

트리 알고리즘의 대표인 결정 트리를 기반으로 한 랜덤 포레스트, 교차 검증과 그리드 설치, 앙상블까지 살펴볼 수 있었다. 사이킷런에서 제공하는 라이브러리를 통해 모델을 쉽게 사용하고 매개변수를 다루는 법부터 다양한 기본 모델을 배우는 시간이었다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter05_Tree_algorithm.ipynb
0.41MB

지난번 업로드했던 내용이 정리가 잘 되었는지 우수 혼공족으로 선정되었다. 항상 도움 많이받는 한빛미디어! 혼공족장님도 열일해주셔서 너무 감사하고,,백신 후유증은 괜찮으신가요? 사랑하는 한빛미디어 뼈를 묻겠습니다..♡


 

04. 다양한 분류 알고리즘

 

04-1 로지스틱 회귀

  • 시작하기 전에
    럭키백을 판매한다고 할 때, 고객에게 힌트를 주기 위해 확률을 구하는 문제가 있다.

    <럭키백의 확률>


    k-최근접 이웃 분류기를 통해 생선의 확률을 계산할 것이다.

    이웃한 샘플 중 다수의 항목들로 분류될 확률이 크다.
    import pandas as pd
    fish = pd.read_csv('https://bit.ly/fish_csv_data')
    fish.head()
    판다스의 데이터프레임으로 데이터를 읽는다. head() 메서드는 처음 5개 행을 출력해준다.

    첫번째 열만 타겟으로 만들고, 나머지 열을 특성으로 만들면 된다. 
print(pd.unique(fish['Species']))

 unique() 메서드를 통해 고유한 값을 추출해볼 수 있다.

 

# 여러 열 선택하여 넘파이배열로 바꿔 저장하기
fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
print(fish_input[:5])

# 타깃 데이터 넘파이배열로 저장하기
fish_target = fish['Species'].to_numpy()
print(fish_target[:5])

넘파이배열을 통해 데이터를 쉽게 정리할 수 있다.

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state=42)

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

앞의 챕터에서 했던 것처럼 훈련 데이터와 테스트 데이터를 StandardScaler 클래스를 사용해 표준화 전처리해준다.

<k-최근접 이웃 분류기의 확률 예측>

k-최근접 이웃 분류기를 통해 훈련 세트와 테스트 세트의 점수를 확인할 수 있다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier(n_neighbors=3)
kn.fit(train_scaled, train_target)
print(kn.score(train_scaled, train_target))
print(kn.score(test_scaled, test_target))

앞서 데이터 프레임을 만들 때 7개의 생선 종류가 들어있었기 때문에, 타깃 데이터에도 7개의 종류가 들어가 있다. 이렇게 타깃 데이터에 2개 이상의 클래스가 포함된 문제를 다중 분류(multi-class classification)라고 부른다.

이진 분류처럼 모델을 만들 수 있지만, 그렇게 한다면 True/False 값을 1/0으로 반환할 것이다. 다중 분류에서도 숫자로 바꿔 입력할 수 있지만 사이킷런에서는 문자열로 된 타깃값을 그대로 사용할 수 있다. 하지만, 그대로 사이킷런 모델에 전달하면 자동으로 '알파벳' 순으로 매겨지기 때문에 처음 입력했던 순서와 다를 수 있다.

# 정렬된 타깃값
print(kn.classes_)

# 처음5개 샘플 타깃값
print(kn.predict(test_scaled[:5]))

알파벳 순서와, 처음 입력했던 그대로의 순서로 다른 것을 확인할 수 있다. round() 메서드와 decimals 매개변수로 이 타깃값에 대한 확률을 추론해볼 수 있다. predict_proba() 메서드는 클래스의 확률을 출력한다.

import numpy as np
proba = kn.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=4))

모델이 계산한 확률이 가장 가까운 이웃의 비율이 맞는지 확인하기 위해 네번째 샘플을 선택하여 확인한다. predict() 메서드의 확률과 predict_proba() 메서드의 확률이 같아야 한다.

distances, indexes = kn.kneighbors(test_scaled[3:4])
print(train_target[indexes])

이 샘플의 이웃은 'Roach'가 한 개로 1/3 , 즉 0.3333이고 'Perch'는 2개 이므로 0.6667이 된다. 앞서 출력한 네 번째 샘플의 클래스 확률과 같은 것으로 확인할 수 있다. 하지만 생각해보니, 어차피 확률은 네 개 중에 한 개이다. 
0/3, 1/3, 2/3, 3/3 이 중에 있지 않을까? 만약 이렇게만 표기를 한다면 확률이라고 말하기 조금 애매한 부분이 있다.

 

<로지스틱 회귀>  

# 기본미션
로지스틱 회귀는 이름은 회귀지만 '분류' 모델이다. 이 알고리즘은 선형 회귀와 동일하게 선형 방정식을 학습한다.

여기서 각 특성의 앞에 붙는 숫자(스칼라)는 가중치(weight) 혹은 계수라고도 말한다. z는 어떤 값도 가능하지만 확률이 되려면 결과값은 0~1 또는 0~100% 사이의 값이 되어야 한다. 그러기 위해서 z가 아주 큰 음수이거나 아주 큰 양수일 때, 0 혹은 1로 바꿔주는 함수가 있는데 이를 활성화 함수(activation function)라고 한다. 주로 시그모이드 함수(sigmoid) 를 사용해서 이를 가능케 한다.

선형 방정식의 출력의 음수를 사용하여 자연 상수 e를 거듭제곱 하고 1을 더한 값의 역수를 취한다. 이런 복잡한 식을 거쳐 우측의 그림과 같은 그래프를 만들 수 있다. z가 무한하게 큰 음수일 경우 0에, 무한하게 큰 양수일 경우 1에 가까워 진다. z가 0일 때는 0.5가 된다. z가 어떤 값을 가지더라도 결과값은 0~1 사이에 있기 때문에 확률적으로 0~100% 의 확률을 보이는 것을 알 수 있다.

import matplotlib.pyplot as plt

z = np.arange(-5, 5, 0.1)
phi = 1 / (1 + np.exp(-z))
plt.plot(z, phi)
plt.xlabel('z')
plt.ylabel('phi')
plt.show()

이를 넘파이를 사용하여 그릴 수 있다. -5와 5 사이에서 0.1 간격으로 배열 z를 만든 다음 z 위치마다 시그모이드 함수(sigmoid)를 계산할 수 있다. 지수 함수는 np.exp() 함수를 사용한다. 이를 활용하여 로지스틱 회귀모델을 훈련할 수 있다.

이진 분류일 경우 시그모이드 함수의 출력이 0.5보다 크면 양성(positive/true), 0.5보다 작으면 음성(negative/false)으로 판단한다.

<로지스틱 회귀로 이진 분류 수행하기>  

넘파이 배열은 True, False 값을 전달하여 행을 선택할 수 있다. 이를 불리언 인덱싱(boolean indexing)이라고 부르기도 한다.

char_arr = np.array(['A', 'B', 'C', 'D', 'E'])
print(char_arr[[True, False, True, False, False]])

위와 같은 방법은 지정한 배열 중 True인 값만 출력하는 것이다.

bream_smelt_indexes = (train_target == 'Bream') | (train_target == 'Smelt')
train_bream_smelt = train_scaled[bream_smelt_indexes]
target_bream_smelt = train_target[bream_smelt_indexes]

도미와 빙어를 타겟값으로 두고 비교 연산자를 사용하여 도미와 빙어에 대한 행만 골라낼 수 있다.

 

from sklearn.linear_model import LogisticRegression
lr = LogisticRegression()
lr.fit(train_bream_smelt, target_bream_smelt)

## 예측값 출력
print(lr.predict(train_bream_smelt[:5]))
print(lr.predict_proba(train_bream_smelt[:5]))
print(lr.classes_)
print(lr.coef_, lr.intercept_)

훈련한 모델을 사용해 처음 5개 샘플을 예측하면 첫번째 출력값이 나온다. 이후 처음 5개 샘플의 예측값을 출력하면 샘플마다 2개의 확률이 출력한다. 첫번째 열이 음성(0)에 대한 확률이고 두번째 열이 양성(1)에 대한 확률이다. 그럼 두 개의 항목('bream', 'smelt' 중 어떤 것이 양성 클래스인지 확인해보면 알파벳 순이기 때문에 'smelt'가 양성임을 확인할 수 있다.

선형회귀와 마찬가지로 위의 계수들을 통해 특성에 대한 가중치(계수)가 위와 같이 설정되는 것을 확인할 수 있다. 로지스틱 회귀 모델로 z값을 계산해 볼수도 있다.

decisions = lr.decision_function(train_bream_smelt[:5])
print(decisions)

decision_funtion() 메서드를 사용하여 z값을 출력할 수 있다. 이 값을 시그모이드 함수에 통과시키면 확률을 얻을 수 있는데 사이파이(scipy) 라이브러리의 expit() 메서드를 사용하여 얻을 수 있다.

from scipy.special import expit
print(expit(decisions))

출력된 값을 통해 decision_function() 메서드는 양성 클래스에 대한 z값을 반환하는 것을 알 수 있다. decision_funciont()의 출력이 0보다 크면 시그모이드 함수의 값이 0.5보다 크므로 양성 클래스로 예측한다.

 

<로지스틱 회귀로 다중 분류 수행하기>  

로지스틱 회귀를 활용한 다중 분류도 이진 분류와 크게 다르지 않다. LogisticRegression 클래스는 max_iter 매개변수를 사용하여 반복 횟수를 지정하며 기본값은 100이다. 만약 반복 횟수가 부족하면 경고가 뜬다. 또한 릿지 회귀와 같이 계수의 제곱을 규제하는데, 이를 L2라고 부른다. L2 규제에서는 alpha를 사용한 규제를 했는데, LogisticRegression에서 규제를 제어하는 매개변수는 C이다. C는 alpha와 반대로 작을수록 규제가 커지고 기본값은 1이다.

lr = LogisticRegression(C=20, max_iter=1000)
lr.fit(train_scaled, train_target)
print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))

훈련 세트와 테스트 세트에 대한 점수가 높으면서도 과대적합이나 과소적합으로 치우친 것 같지 않다.

proba = lr.predict_proba(test_scaled[:5])
print(np.round(proba, decimals=3))

테스트 세트의 5개 샘플에 대한 예측을 하면 위와같이 출력이 된다. 첫번째 샘플의 경우 세번째 열의 3번째가 84.1%로 가장 높다.

 

print(lr.classes_)
print(lr.coef_.shape, lr.intercept_.shape)

이를 확인해보면, 첫번째 샘플은 세번째 열의 확률이 가장 높기 때문에 'perch'를 가장 높은 확률로 예측했고 두번째 샘플은 여섯번째 열의 확률이 높기 때문에 'smelt'를 높게 예측한 것을 알 수 있다. 또한 선형 방정식을 확인하여 계수를 출력해보면 5개의 특성을 사용하기 때문에 coef는 5개의 열이 있지만 행이 7개인 것으로 보인다. 즉, z를 7개를 계산한다는 것이다. 다중 분류는 클래스마다 z값을 하나씩 계산하고, 가장 높은 z값을 출력하는 클래스가 예측 클래스가 된다. 

확률을 계산할 때는 이진 분류에서는 시그모이드 함수(sigmoid)를 사용했지만, 다중 분류에서는 소프트맥스(softmax) 함수를 사용한다. 소프트맥스(softmax) 함수란 여러 선형 방정식의 출력값을 0~1 사이로 압축하는 역할을 하는 함수이다. 또한 전체 합이 1이 되도록 만들기 위해 지수 함수를 사용하기 때문에 '정규화된 지수 함수'라고 부르기도 한다.

 

소프트맥스 함수는 각각의 지수함수를 전체 지수함수의 합으로 나눈 다음 그 s1~sn까지의 값을 더한 것이다. 모두 더하게 되면 분자와 분모가 같아지므로 결과값이 1이 되기 때문에 정상적으로 계산된 것을 알 수 있다. 시그모이드 함수(sigmoid)소프트맥스(softmax) 함수는 신경망에서도 나오는 개념이기 때문에 확실히 짚어두고 개념을 아는 것이 중요하다.

 

decision = lr.decision_function(test_scaled[:5])
print(np.round(decision, decimals=2))

from scipy.special import softmax
proba = softmax(decision, axis=1)
print(np.round(proba, decimals=1))

이진 분류에서처럼 먼저 z값을 구한 후 softmax 함수로 계산할 수 있다. softmax() 메서드의 axis매개변수는 소프트맥스를 계산할 축을 지정하는 매개변수다. axis=1로 지정한다면 각 행(샘플)에 대한 소프트맥스를 계산한다. 만약 axis 매개변수를 따로 지정하지 않으면 배열 전체에 대한 소프트맥스 계산이 진행된다. 출력 결과를 앞서 구한 proba 배열과 비교하면 정확히 일치하는 것을 확인할 수 있다.


04-2 확률적 경사하강법

  • 시작하기 전에
    공급이 갑자기 늘면서 항목들이 추가되고, 그에 대한 샘플이 없는 경우에 어떻게 모델을 바꿔야할까? 

<점진적인 학습>

데이터가 추가될 때마다 일정 간격을 두고 샘플을 추려서 새로운 모델을 만들면 되는 걸까? 하지만 시간이 지날수록 데이터는 늘어나리 때문에 서버도 기하급수적으로 늘려야할 수도 있다.

또 다른 방법은 새로운 데이터를 추가할 때 이전 데이터를 버림으로써 훈련 데이터 크기를 일정하게 유지하는 것ㅇ이다. 하지만 데이터를 버릴 때 중요한 데이터가 포함되어있으면 안 된다. 

이렇게 데이터를 버리지 않고 새로운 데이터에 대해서만 훈련할 방법은 없을까? 이러한 훈련 방법을 점진적 학습이라고 부른다. 대표적인 점진적 학습 알고리즘은 확률적 경사 하강법(Stochastic Gradient Descent)이 있다.

 

<확률적 경사 하강법>  

확률적 경사 하강법(Stochastic Gradient Descent)은 최적화 방법 중 하나이다. '확률적' 이라는 말은 random하다는 것과 같다. '경사'는 말 그대로 경사, 기울기, 즉 가중치(weight)를 이야기한다. '하강법'은 '내려가는 방법'이기 때문에 합쳐서 경사를 따라 내려가는 방법을 의미한다. 

만약 우리가 산 꼭대기에서 하산을 하려고 하는데, 지도를 분실하거나 휴대폰의 배터리가 다 했다고 가정한다면 우리는 가장 빠르게 하산하기 위해 어떤 방법을 사용할까? 바로 경사가 가장 가파른 길을 선택할 것이다. 경사 하강법의 목적은 가장 빠르게 원하는 최소 지점에 도달하는 것이다.

 

한 걸음씩 내려올 때, 보폭이 너무 크면 최소 지점을 찾지 못할 수 있다. 또, 보폭이 너무 작다면 시간이 오래 걸릴 수 있다. 그렇기 때문에 가파른 길로 내려오되 적당한 보폭으로 내려와야 한다. 가장 경사가 가파른 길을 찾을 때는 전체 샘플이 아닌 '랜덤한' 샘플을 골라 내려올 것이다. 이처럼 훈련 세트에서 랜덤하게 샘플을 고르는 것이 확률적 경사 하강법이다.

만약 랜덤하게 샘플을 골라서 쓰다가 모든 샘플을 다 사용했는데, 내려오지 못했다면 다시 샘플을 채워넣고 랜덤하게 골라 내려오는 것이다. 이렇게 훈련 세트를 한 번씩 모두 사용하는 과정을 에포크(epoch)라고 부른다. 일반적으로 수십~수백번 이상의 에포크를 수행하는 편이다.

무작위로 내려오다가 실족하는 것이 걱정된다면 어떻게 해야할까? 그렇다면 랜덤 선택 시 1개가 아닌 여러 개의 샘플을 선택해서 내려갈 수 있다. 이렇게 여러 개의 샘플을 사용하는 것을 미니배치 경사 하강법(minibatch gradient descent)이라 한다.

한 번의 이동을 위해 전체 샘플을 사용할 수도 있는데 이를 배치 경사 하강법(batch gradient descent)라고 한다. 이는 가장 안정적이고 리스크가 적으르 수 있지만 그만큼 계산이 오래걸리고 컴퓨터 메모리를 많이 사용할 수 있다. 또, 데이터가 너무 많은 경우에는 모두 읽을 수 없을 수도 있다.

 

 

즉, 데이터의 성질이나 샘플의 갯수에 따라 경사 하강법의 종류가 나눠지며, 그 특성에 맞는 경사 하강법을 사용하면 된다.

 

<손실 함수>  

손실 함수(loss function)는 어떤 문제에서 알고리즘이 얼마나 별로인지, 즉 '나쁜 정도'를 측정하는 함수다. 그래서 값이 작을수록 좋은 것이고, 손실 값이 낮은 쪽으로 경사하강법을 진행하는 것이 좋다. 확률적 경사하강법이 최적화할 대상이라고 이해를 해도 좋다. 

분류모델에서는 정확도(accuracy)를 많이 봤었는데, 이진 분류에서는 예측과 정답이 맞는지 여부에 따라 양성과 음성으로 나뉜다. 위의 경우는 정확도가 0.5, 50%임을 알 수 있다. 이를 손실함수로 표현할 수는 없을까?

 

만약 위의 경우라면 정확도가 0, 0.25, 0.5, 0.75, 1의 다섯가지만 가능하다. 정확도가 이렇게 듬성듬성하다면 경사하강법을 이용해 조금씩 움직일 수 없다. 경사는 연속적이어야하기 때문이다. 조금 더 기술적으로 이야기한다면, 선은 점의 연속으로 되어있고, 점이 조금 더 촘촘해서 선을 이루고 즉 미분이 가능해야 한다. 결론적으로, 손실함수는 미분이 가능해야 한다.

그렇다면 연속적인 손실 함수를 만드는 법은 무엇일까? 

 

<로지스틱 손실 함수>

로지스틱 손실 함수의 경우 예측값과 타깃값을 곱해서 사용할 수 있는데, 타깃이 0(false)인 경우는 곱하면 무조건 0이 되기 때문에 양성 클래스로 바꾸어 계산할 수 있다. 예측값과 타깃값 모두 1에서 빼서 변경이 가능하다. 

예측 확률의 범위는 0~1 사이인데 로그 함수는 이 사이에서 음수가 되므로 최종 손실값은 양수가 된다. 로그함수는 0에 가까울수록 아주 큰 음수가 되기 때문에 손실을 아주 크게 만들어 모델에 큰 영향을 끼칠 수 있다.


즉, 타깃값이 1(true)일때는 음수로 바꾼다음 로그함수 적용을 하고, 타깃값이 0(false)일때는 1에서 뺀 다음 음수로 바꾸고 로그함수를 적용한다.

양성 클래스일때 확률이 1에서 멀어질수록 손실은 큰 양수가 된다.  음성 클래스일 때 확률이 0에서 멀어질수록 손실은 큰 양수가 된다. 이 손실 함수를 로지스틱 손실 함수(logistic loss function) 혹은 이진 크로스엔트로피 손실 함수(binary cross-entropy loss function)이라고 한다. 다중 분류에서 사용하는 손실함수는 크로스엔트로피 손실 함수(cross-entropy loss function)라고 한다.

* 회귀에서 사용하는 손실함수

회귀의 손실함수로는 평균 절댓값 오차를 사용할 수 있다. 타깃에서 예측을 뺀 절댓값을 모든 샘플에 평균한 값이다. 혹은 평균 제곱 오차(mean squared error)를 많이 사용한다. 이는 타깃에서 예측을 뺀 값을 제곱한 다음 모든 샘플에 평균한 값이다. 이 값은 작을수록 좋은 모델이다.

 

<SGD Classifier>

확률적 경사하강법을 활용한 학습을 진행하기 전에, 데이터의 전처리를 진행해야 한다. 특성의 스케일값을 맞춘다.

import pandas as pd
fish = pd.read_csv('https://bit.ly/fish_csv_data')

fish_input = fish[['Weight', 'Length', 'Diagonal', 'Height', 'Width']].to_numpy()
fish_target = fish['Species'].to_numpy()
from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_input, fish_target, random_state=42)

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

 

스케일값을 맞춘 다음 사이킷런에서 제공하는 확률적 경사 하강법 분류용 클래스를 사용한다. 객체를 만들 때 2개의 매개변수를 지정한다. loss는 손실함수의 종류를 지정하는 것이고, 여기서는 log로 지정하여 로지스틱 손실함수를 지정했다. max_iter는 에포크 횟수를 지정하는 것이다.

from sklearn.linear_model import SGDClassifier

sc = SGDClassifier(loss='log', max_iter=10, random_state=42)
sc.fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

0.77정도의 정확도로 미루어 보아 훈련 에포크 횟수(10회)가 부족한 것으로 유추된다. ConvergenceWarning의 경우 모델이 충분히 수렴하지 않았다는 경고이다. 즉, max_iter의 값을 더 늘려주라는 친절한 안내메시지이다.

확률적 경사하강법은 점진적 학습이 가능하기 때문에 이어서 훈련이 가능하다. partial_fit() 메서드를 통해 1 에포크씩 이어서 훈련을 할 수 있다.

# 추가 학습
sc.partial_fit(train_scaled, train_target)
print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

한번의 에포크동안 학습을 더 실행한 결과 정확도가 향상된 것을 알 수 있다. 하지만 이렇게 무작정 한 번씩 반복할 수는 없으니 얼마나 더 반복해야하는지 횟수를 알 수는 없을까?

 

<에포크와 과대/과소적합>

확률적 경사하강법을 사용한 모델도 에포크 횟수에 따라 과대/과소적합이 발생할 수 있다. 에포크가 너무 많으면 과대적합, 에포크가 너무 적으면 과소적합이 될 수 있다.

위의 그래프는 에포크의 진행에 따른 모델의 정확도이다. 과대적합이 시작하기 전에 훈련을 멈추는 것을 조기 종료(early stopping)라고 한다. 예제에서 실습을 진행해보려고 한다.

# 선택미션

sc = SGDClassifier(loss='log', random_state=42)
train_score = []
test_score = []

classes = np.unique(train_target)
for _ in range(0, 300):
  sc.partial_fit(train_scaled, train_target, classes=classes)
  train_score.append(sc.score(train_scaled, train_target))
  test_score.append(sc.score(test_scaled, test_target))

# 산점도 그리기
plt.plot(train_score)
plt.plot(test_score)
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.show()

에포크마다 점수를 기록하기 위해 2개의 리스트를 준비한 후, 300번의 에포크를 반복한다. 그 다음 훈련에 대한 점수를 산점도로 나타내면 위와 같은 그래프로 나타난다. 위의 그래프를 통해, 100번째 epoch 이후로는 훈련 세트 점수테스트 세트 점수가 조금씩 벌어지는 것을 확인할 수 있다. 또한, 에포크 초기에는 과소적합으로 점수가 낮은 것을 알 수 있다. 결론적으로 100회가 적절한 epoch로 보인다.

sc = SGDClassifier(loss='log', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

SGDClassifier에서 반복횟수를 100회로 지정한 후 훈련을 진행해보았다. SGDClassifier는 일정 에포크동안 성능이 향상되지 않으면 자동으로 멈춘다. tol 매개변수에서 향상될 최솟값을 지정할 수 있다. None으로 지정한다면 자동으로 멈추지 않고 max_iter만큼 무조건 반복된다.

* 확률적 경사하강법을 사용한 회귀모델은 SGDRegressor이다.

SGDClassifier의 loss 매개변수의 기본값은 'hinge'이다. 힌지 손실(hinge loss)서포트 벡터 머신(support vector machine)이라 불리는 알고리즘을 위한 손실 함수이다. 이는 또 다른 머신러닝 알고리즘이다. 서포트 벡터 머신에서는 다른 포스팅에서 딥하게 설명하도록 하겠다. SGDClassifier는 여러 종류의 손실함수를 loss 매개변수에 지정하여 알고리즘에 사용할 수 있다.

# 힌지 손실을 사용한 훈련
sc = SGDClassifier(loss='hinge', max_iter=100, tol=None, random_state=42)
sc.fit(train_scaled, train_target)

print(sc.score(train_scaled, train_target))
print(sc.score(test_scaled, test_target))

log함수가 아닌 힌지함수를 사용하여 loss값을 지정해주었더니 점수가 달라졌다.

 

 


● 마무리

다양한 회귀, 분류 모델에 사용되는 알고리즘을 통해 배웠다. 점진적 학습을 할 수 있는 확률적 경사 하강법, 로지스틱 회귀 모델 등으로 요즘처럼 데이터가 큰 빅데이터를 다룬다면 아주 유용할 것이다. 손실함수가 어떤 것인지와 적당한 에포크 횟수를 캐치하는 방법도 학습했다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter04_Various_classification_algorithms.ipynb
0.05MB

03. 회귀 알고리즘과 모델 규제

 

03-1 k-최근접 이웃 회귀

  • 시작하기 전에

농어의 무게를 예측하는 모델을 만들어보려고 한다.

<k- 최근접 이웃 회귀>

지도 학습 알고리즘은 분류와 회귀(regression)로 나뉜다. 분류는 샘플을 클래스 중 하나로 분류하는 문제이고, 회귀는 분류가 아니라 임의의 어떤 숫자를 예측하는 문제이다. 즉, 정해진 클래스, 타깃 값(정답)이 없는 것을 예측하여 임의의 수치를 출력하는 것이다. 회귀는 19세기 통계학자인 프랜시스 골턴이 처음 사용한 단어로, 두 변수 사이의 상관관계를 분석하는 방법회귀라고 불렀다.

k-최근접 이웃 분류 알고리즘은 샘플에 가장 가까운 샘플 k개를 선택한 다음 그들의 클래스를 확인하여 예측하는 것이다.

X를 추측할때 네모가 더 많기 때문에 동그라미보단 네모로 예측하는 것이다.

X의 타깃값을 예측할 때는 이웃 샘플의 타깃값의 평균으로 구할 수 있다. 고로, 80으로 예측하는 것이다.

 

<데이터 준비>

훈련데이터를 깃허브에서 복사한 다음, 데이터의 산점도를 그려볼 수 있다.

import matplotlib.pyplot as plt
plt.scatter(perch_length, perch_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

하나의 특성을 사용하기 때문에 특성 데이터를 x축에 두고, 타깃 데이터를 y축에 놓고 산점도를 그린 결과다. 결과에 따르면, 농어의 길이가 길어짐에 따라 무게도 늘어나는 것을 알 수 있다.

이제 이 데이터를 훈련 세트와 테스트 세트로 나눌 것이다.

여기서 사용하는 random_state의 시드 값을 설정해줄 때 보통 0이나 1이 아닌 42를 관용적으로 설정해주길래 이유를 찾아봤더니 은하수를 여행하는 히치하이커를 위한 안내서 책에서 "삶과 우주, 그리고 모든 것에 대한 답은 42입니다." 라는 문장이 있어서 그 이후로 밈처럼 사용되었다고 한다...

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

print(train_input.shape, test_input.shape)

여기서 perch_length의 특성이 1차원 배열이고 train_input, test_input도 1차원 배열인데 이를 2차원 배열로 바꾼다면 아래와 같이 진행된다.  reshape() 메서드는 크기를 바꿀 수 있는 메서드로, 넘파이에서 제공하는 메서드다. 즉, 바꾸려는 배열의 크기를 지정할 수 있다. 첫 번째 크기에 나머지 원소 개수로 모두 채우려면 -1, 두 번째 크기를  1로 하려면 train_input.shape(-1, 1)처럼 하면 된다.

 

<결정계수(R²)>

사이킷런에서 k-최근접 이웃 회귀 알고리즘을 구현한 클래스는 kNeighborsRegressor로 kNeighborsClassifier와 매우 유사하다. fit() 메서드를 통해 회귀 모델을 훈련할 수 있다. 

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor()

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.score(test_input, test_target))

여기서 나온 0.992~의 숫자는 결정계수(Coefficient of determination)라고 부른다. R²(알스퀘어)라고도 부르는데, 자세히 설명해 둔 블로그가 있어서 참고하였다. 계산식은 아래와 같다.

즉, 타깃의 평균 정도를 예측하는 수준이라면 R²는 0에 가까워지고, 예측이 타깃에 가까워지면 1에 가까운 값이 된다. 사이킷런의 score()메서드의 값은 높을수록 좋다. 분류 모델에서의 정확도나, 회귀 모델에서의 결정계수 모두 동일하다.

하지만 직관적으로 숫자만 보고 어느정도의 결정계수인지 가늠하기 어렵기 때문에 다른 값으로 치환해서 살펴볼 수도 있다.

from sklearn.metrics import mean_absolute_error

# 테스트 세트에 대한 예측
test_prediction = knr.predict(test_input)

# 테스트 세트에 대한 평균 절댓값 오차 계산
mae = mean_absolute_error(test_target, test_prediction)
print(mae)

mean_absolute_error() 메서드는 타깃과 예측이 절댓값 오차를 평균하여 반환한다. 즉, 이 결과값은 결과에서 예측이 평균 19g정도 타깃값과 다르다는 것이다.

 

<과대적합 vs 과소적합>

위에서는 훈련 세트로 훈련을 하고 테스트 세트로 평가했는데, 만약 훈련 세트로 평가를 한다면 어떤 결과가 나올까?

print(knr.score(train_input, train_target))

보통 테스트 세트를 활용한 모델의 점수가 더 높고 훈련 세트를 활용한 모델의 점수가 더 낮게 나오지만, 이 경우엔 반대로 된 것을 확인할 수 있다. 

훈련 세트에서 점수가 좋았는데 테스트 세트의 점수가 굉장히 나쁘다면 모델이 훈련세트에 과대적합(overfitting) 되었다고 한다. 즉, 훈련 세트에만 잘 맞는 모델이라 테스트 세트 혹은 실전에서 사용할 때 잘 동작하지 않을 수 있다. 이는 복잡도가 높다고 할 수도 있다.

반대로 훈련 세트보다 테스트 세트의 점수가 높거나 두 점수 모두 낮은 경우는 과소적합(underfitting) 되었다고 말한다. 즉, 모델이 너무 단순하여 적절히 훈련되지 않은 경우이다. 이는 복잡도가 낮다고 할 수 있다. 이럴 때는 훈련 세트의 크기가 너무 작거나, 데이터가 중요한 특징을 담고 있지 않은 경우일 수 있다.

위의 경우도 과소적합이다. 이를 해결하기 위해서 모델을 조금 더 복잡하게, 즉 훈련 세트에 더 잘 맞게 만들면 테스트 세트의 점수가 낮아질 것이다. 이웃의 개수인 k를 줄인다면 훈련 세트의 패턴에 더욱 민감해지고, k의 개수를 늘린다면 데이터 전반적인 특징을 따를 것이다. 기본 k값인 5를 3으로 줄여서 실행해보려 한다.

# 이웃의 개수 3개로 설정
knr.n_neighbors = 3

# 모델 재훈련
knr.fit(train_input, train_target)


print(knr.score(train_input, train_target))  # 훈련 세트의 결정계수
print(knr.score(test_input, test_target))  # 테스트 세트의 결정계수

테스트 세트의 점수가 훈련 세틑의 점수보다 낮아졌고, 둘의 차이도 크지 않아 과소적합이 해결된 것을 알 수 있다.

이처럼 최적의 k값을 찾기 위한 과정이 필요하고, 스스로 정의하는 이러한 매개변수를 보통 '하이퍼파라미터'라고 부른다.

파라미터(Parameter)란 모델의 구성요소이자 데이터로부터 학습되는 것이다. 즉, 머신러닝 훈련 모델에 의해 요구되는 변수이다. 머신러닝 훈련 모델의 성능은 파라미터에 의해 결정된다.

그 중에서도 하이퍼파라미터(Hyperparameter)란 최적의 훈련 모델을 구현하기 위해 설정하는 변수이다. 이는 모델 학습 과정에 반영되며, 학습을 시작하기 전에 미리 수동으로 값을 결정하는 것이다. 하이퍼 파라미터는 절대적인 최적값이 존재하지 않기 때문에 사용자가 직접 모델에 대한 이해를 통해 값을 설정하고 결정해야 한다. 좋은 모델을 만들기 위해서는 하이퍼파라미터를 잘 튜닝(컨트롤)해야 한다. 그러기 위해서는 학습 알고리즘, 모델의 구조 등 총체적인 이해가 필요하다.

 

<확인문제>  # 기본미션 

# k-최근접 이웃 회귀 객체 만들기
knr = KNeighborsRegressor()

# 5 ~ 45까지 x 좌표 만들기
x = np.arange(5, 45).reshape(-1, 1)

# n = 1, 5, 10일 때 예측 결과 그래프로 그리기
for n in [1, 5, 10]:

# 모델 훈련하기
  knr.n_neighbors = n
  knr.fit(train_input, train_target)

# 지정한 범위 x에 대한 예측 구하기
  prediction = knr.predict(x)

# 훈련 세트와 예측 결과 그래프로 그리기
  plt.scatter(train_input, train_target)
  plt.plot(x, prediction)
  plt.title('n_neighbors = {}'.format(n))
  plt.xlabel('length')
  plt.ylabel('weight')
  plt.show()

 


03-2 선형회귀

  • 시작하기 전에

print(knr.predict([[50]]))

길이가 50cm인 농어의 무게를 예측했을 때, 예측 결과값은 1,033g이지만 실제로는 1.5kg에 달하는 농어였다. 이렇게 예측값과 실제값의 차이가 많이 나는 이유는 무엇일까?

<k-최근접 이웃의 한계>

훈련데이터를 깃허브에서 복사한다.

perch_length = np.array([8.4, 13.7, 15.0, 16.2, 17.4, 18.0, 18.7, 19.0, 19.6, 20.0, 21.0,
       21.0, 21.0, 21.3, 22.0, 22.0, 22.0, 22.0, 22.0, 22.5, 22.5, 22.7,
       23.0, 23.5, 24.0, 24.0, 24.6, 25.0, 25.6, 26.5, 27.3, 27.5, 27.5,
       27.5, 28.0, 28.7, 30.0, 32.8, 34.5, 35.0, 36.5, 36.0, 37.0, 37.0,
       39.0, 39.0, 39.0, 40.0, 40.0, 40.0, 40.0, 42.0, 43.0, 43.0, 43.5,
       44.0])
perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

훈련 세트와 테스트 세트로 데이터를 나눈다.

from sklearn.model_selection import train_test_split

# 훈련 세트와 테스트 세트로 나누기
train_input, test_input, train_target, test_target = train_test_split(
perch_length, perch_weight, random_state=42)

# 훈련 세트와 테스트 세트를 2차원 배열로 바꾸기
train_input = train_input.reshape(-1, 1)
test_input = test_input.reshape(-1, 1)

 

최근접 이웃 개수를 3으로 하는 모델을 훈련한다.

from sklearn.neighbors import KNeighborsRegressor

knr = KNeighborsRegressor(n_neighbors=3)

# k-최근접 이웃 회귀 모델 훈련
knr.fit(train_input, train_target)

print(knr.predict([[50]]))

이 모델은 무게를 1,033g 정도로 예측했다. 문제점을 찾기 위해 산점도를 표시하는데, kneighbors() 메서드를 사용할 것이다.

import matplotlib.pyplot as plt

# 50cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[50]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 50cm 농어 데이터
plt.scatter(50, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

길이가 50cm이고 무게가 1,033g인 농어는 삼각형으로 표시되고 그 주변의 샘플은 다이아몬드로 표시된다. 산점도에 따르면, 길이가 길어질수록 무게가 증가하는 경향이 있다. 하지만 50cm의 농어에서 가장 가까운 샘플은 45cm의 샘플이기 때문에, k-최근접 이웃 알고리즘에 따르면 이들의 무게의 평균을 예측 무게값으로 출력할 수 밖에 없다. 

print(np.mean(train_target[indexes]))

새로운 샘플이 훈련 세트의 범위를 벗어나면 엉뚱한 값을 예측할 수 있다. 예를 들어 길이가 50cm가 아니라 100cm, 200cm 여도 농어의 무게는 여전히 1,033g으로 예측될 것이다.

다시 한 번 그래프를 통해 확인해보려고 한다.

# 100cm 농어의 이웃 구하기
distances, indexes = knr.kneighbors([[100]])

# 훈련 세트의 산점도 그리기
plt.scatter(train_input, train_target)

# 훈련 세트 중 이웃 샘플만 그리기
plt.scatter(train_input[indexes], train_target[indexes], marker='D')

# 100cm 농어 데이터
plt.scatter(100, 1033, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

이런 식이면 농어가 아무리 커도 무게는 늘어나지 않을 것이다. k-최근접 이웃이 아닌 다른 알고리즘으로 이 문제를 해결할 수 있을까?

 

<선형 회귀>

선형 회귀(Linear Regression)는 대표적인 회귀 알고리즘 중 하나로, 특성이 하나인 경우 어떤 직선을 학습하는 알고리즘이다. 즉, 그 특성을 가장 잘 나타낼 수 있는 직선을 학습하는 것이다.

사이킷런의 LinearRegression 클래스를 사용하여 선형회귀 알고리즘을 구현할 수 있다. 이 클래스에도 fit(), score(), predict() 메서드가 있다.

from sklearn.linear_model import LinearRegression
lr = LinearRegression()

# 선형 회귀 모델 훈련
lr.fit(train_input, train_target)

# 50cm 농어 예측
print(lr.predict([[50]]))

k-최근접 이웃 회귀와 달리 선형 회귀는 50cm 농어의 무게를 1,241g으로 예측했다. 

직선을 그리려면 기울기와 절편이 있어야 한다. 이 선형회귀 클래스가 이 데이터에 가장 잘 맞는 a와 b를 찾은 것이다. 클래스가 찾은 a와 b는 lr 객체의 coef_와 intercept_ 속성에 저장되어 있다.

 

#선택미션

Q. 모델 파라미터에 대해 설명하기

A. coef_intercept_를 머신러닝 알고리즘이 찾은 값이라는 의미로 모델 파라미터(model parameter)라 부른다. coef_는 기울기로, 계수(coefficient) 또는 가중치(weight)라고 부른다. 모델 기반 학습에서 모델이 찾은 정보는 모델 파라미터에 저장되고, 선형 회귀에서는 방정식의 계수가 여기에 해당된다.

많은 머신러닝 알고리즘의 훈련 과정은 최적의 모델 파라미터를 찾는 것과 같다. 이를 모델 기반 학습이라고 부른다. 앞서 사용한 k-최근접 이웃 알고리즘처럼 모델 파라미터가 없는 경우의 훈련은 사례 기반 학습이라고 부른다.

 

농어의 길이 15~50까지를 직선으로 그릴 때, 앞서 구하나 기울기와 절편을 사용하여 두 점을 잇고, 산점도로 그려볼 수 있다.

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~50 1차방정식 그래프 그리기
plt.plot([15, 50], [15 * lr.coef_ + lr.intercept_, 50 * lr.coef_ + lr.intercept_])

# 50cm 농어 데이터
plt.scatter(50, 1241.8, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

이 직선이 선형 회귀 알고리즘이 데이터셋에서 찾은 최적의 직선이다. 길이가 50cm인 농어의 예측값 역시 이 직선의 연장선 상에 있음을 알 수 있다. 훈련 세트와 테스트 세트에 대한 R² 점수도 확인해보겠다.

둘의 결정계수 점수의 차이가 나는 것을 발견할 수 있다. 그럼 이게 훈련 세트에 과적합된걸까? 어디가 문제이니 살펴보니 그래프 왼쪽 아래가 좀 이상해보이기도 한다.

<다항 회귀>

선형 회귀가 만든 직선은 왼쪽 아래로 쭉 뻗어있다. 선형 회귀에 따르면 길이가 짧다면 무게가 0g 이하로 내려갈 것이다. 그래서 최적의 직선을 찾기보다 최적의 곡선을 찾는 것이 더욱 바람직할 것이다.

이런 2차방정식 그래프르르 그리려면 길이를 제곱한 항이 훈련 세트에 추가되어야 한다. 이를 위해서는 넘파이를 사용하면 된다.

train_poly = np.column_stack((train_input ** 2, train_input))
test_poly = np.column_stack((test_input ** 2, test_input))

print(train_poly.shape, test_poly.shape)

column_stack() 함수는 1차원 배열을 2차원 배열에 열로 쌓는 함수이다. 새로 만든 데이터의 크기가 위와같이 나온다.

train_poly를 사용해 선형 회귀 모델을 다시 훈련한 후, 2차 방정식의 a,b,c값을 추론할 수 있다. 2차 방정식 그래프를 위해 훈련 세트에 제곱 항을 추가했지만 타깃 값은 그대로 사용한다.

lr = LinearRegression()
lr.fit(train_poly, train_target)

print(lr.predict([[50 ** 2, 50]]))

선형회귀에서 예측한 것보다 훨씬 높은 무게가 예측된 것을 확인할 수 있다. 여기서 다시 계수와 절편을 출력할 수 있다.

이 모델은 위와 같은 그래프를 학습한 것이다. 이런 방정식을 다항식(polynomial)이라 부르며 다항식을 사용한 선형 회귀를 다항 회귀(polynomial regression)라 부른다.

여기서 발견한 절편 a,b,c를 통해 산점도를 그려볼 수 있다.

# 구간별 직선을 그리기 위해 15~49 정수배열 만들기
point = np.arange(15, 50)

# 훈련 세트 산점도 그리기
plt.scatter(train_input, train_target)

# 15~49 2차 방정식 그래프 그리기
plt.plot(point, 1.01 * point ** 2 - 21.6 * point + 116.05)

# 50cm 농어 데이터
plt.scatter(50, 1574, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

무게가 음수로 나올 일도 없고 앞선 모델보다 훨씬 안정적으로 보인다. 결정계수를 확인해보겠다.

아까보단 차이가 많이 줄었지만 여전히 테스트 세트의 점수가 높은 것으로 미루어 보아 과소적합이 있는 것 같다.


03-3 특성 공학과 규제

  • 시작하기 전에

다항 회귀에서 해결하지 못한 과소적합으로 추측되는 문제는, 더 고차항을 넣어서 해결해야 하는 걸까? 아니면 길이와 무게 데이터가 아닌 높이와 두께 데이터를 사용해 볼 수 있을까?

<다중 회귀>

앞서 사용한 선형 회귀는 하나의 특성을 사용했지만, 사실 선형 회귀는 특성이 많을 수록 엄청난 효과를 낸다. 여러 개의 특성을 사용한 선형 회귀를 다중 회귀(multiple regression)라 부른다.

좌측의 그림처럼 1개의 특성을 사용하면 직선을 학습하고, 우측의 그림처럼 2개의 특성을 학습하면 평면을 학습한다. 즉, 타깃값과 함께 3차원 공간을 형성하고 곧 평면이 된다. 특성이 3개인 경우는 어떻게 될까?

우리는 3차원 이상의 공간은 그리거나 상상할 수는 없지만 특성이 적다고 무조건 성능이 안 좋은 것은 아니다. 또, 주어진 특성 외에도 특성들을 연산하여 새로운 특성으로 만들 수도 있다. 이렇게 기존의 특성을 사용하여 새로운 특성을 뽑아내는 작업을 특성 공학(feature engineering)이라 부른다.

<데이터 준비>

농어의 특성이 두께, 길이, 높이 3개로 늘어났고, 복사해서 붙여넣는 방식은 너무 피곤하다. 이럴 경우 판다스를 사용하여 데이터를 내려받아 데이터프레임에 저장할 수 있다. 그 다음 넘파이 배열로 변환하여 사용할 수 있고, 판다스에서는 보통 csv 형태의 파일을 많이 사용한다.

import pandas as pd
df = pd.read_csv('https://bit.ly/perch_csv_data')
perch_full = df.to_numpy()
print(perch_full)

 

이렇게 긴 데이터를 판다스를 통해 쉽게 불러올 수 있다. 타깃데이터는 기존처럼 깃허브에서 복사한다. 

perch_weight = np.array([5.9, 32.0, 40.0, 51.5, 70.0, 100.0, 78.0, 80.0, 85.0, 85.0, 110.0,
       115.0, 125.0, 130.0, 120.0, 120.0, 130.0, 135.0, 110.0, 130.0,
       150.0, 145.0, 150.0, 170.0, 225.0, 145.0, 188.0, 180.0, 197.0,
       218.0, 300.0, 260.0, 265.0, 250.0, 250.0, 300.0, 320.0, 514.0,
       556.0, 840.0, 685.0, 700.0, 700.0, 690.0, 900.0, 650.0, 820.0,
       850.0, 900.0, 1015.0, 820.0, 1100.0, 1000.0, 1100.0, 1000.0,
       1000.0])

 

그 다음 훈련 세트와 테스트 세트로 나누고 특성 공학을 사용하여 새로운 특성을 만든다.

from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(
perch_full, perch_weight, random_state=42)

 

<사이킷런의 변환기>

사이킷런에서 제공하는 전처리를 위한 클래스들을 변환기(transformer)라고 부르는데, 이 트랜스포머들은 모두 fit(), transform() 메서드를 제공한다.

이번에는 PolynomialFeatures 사용할 것이다. transform(변환)메서드는 fit(훈련)을 한 이후에 사용할 수 있다. 혹은 두 개를 붙인 fit_transform() 메서드를 사용해도 된다.

from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures()
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

fit() 메서드는 새롭게 만들 특성 조합을 찾고 transform() 메서드는 실제로 데이터를 변환한다. 변환기는 타깃 데이터 없이 입력 데이터를 변환하기 때문에 fit() 메서드에 입력 데이터만 전달한다.

poly = PolynomialFeatures(include_bias=False)
poly.fit([[2, 3]])
print(poly.transform([[2, 3]]))

사실 선형 방정식의 절편을 항상 값이 1인 특성과 곱해지는 계수라 할 수 있기 떄문에, 절편을 위한 항을 제거하고 특성의 제곱과 특성끼리 곱한 항만 사용할 것이다.

 

이제 다시 훈련 데이터를 활용하여 학습에 적용할 수 있다.

poly = PolynomialFeatures(include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
print(train_poly.shape)

 

get_feature_names() 메서드는 9개의 특성의 내용을 알려준다. 하지만, 에러가 떠서 찾아보니 사이킷런 블로그에 의하면 get_feature_names_out() 메서드를 대신 사용해달라고 한다.

poly.get_feature_names_out()

'x0'는 첫번째 특성, 'x0^2'는 첫번째 특성의 제곱, 'x0 x1'은 첫번째 특성과 두번째 특성의 곱을 나타낸다고 한다.

 

이제 테스트 세트를 변환하여 다중회귀 모델을 훈련할 것이다.

test_poly = poly.transform(test_input)

 

<다중 회귀 모델 훈련하기>

다중 회귀 모델을 훈련하는 것은 선형 회귀 모델을 훈련하는 것과 같지만 특성의 개수가 여러개일 뿐이다. 

from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(train_poly, train_target)
print(lr.score(train_poly, train_target))
print(lr.score(test_poly, test_target))

특성이 증가함으로 인해서 테스트 세트와 훈련 세트 모두의 점수가 높게 나온 것을 확인할 수 있다. 특성을 더 많이 추가해보려고 한다. degree 매개변수를 사용하여 5제곱까지의 특성을 만들어 출력해보려 한다.

poly = PolynomialFeatures(degree=5, include_bias=False)
poly.fit(train_input)
train_poly = poly.transform(train_input)
test_poly = poly.transform(test_input)
print(train_poly.shape)

특성이 55개나 만들어진 것이다. 이를 통해 다시 훈련을 해보겠다.

점수가 음수가 나와버렸다... 특성의 개수를 크게 늘리면 선형 모델은 거의 완벽하게 학습할 수 있을 정도로 강력해진다. 하지만 훈련 세트에 너무 과대적합되기 때문에 테스트 세트에서는 형편없는 점수가 나올 수 있다.

 

<규제>

규제(regularization)는 모델이 훈련 세트를 너무 과도하게 학습하지 못하도록(과대적합하지 않도록) 하는 것이다. 즉, 선형 회귀모델의 경우 특성에 곱해지는 계수(기울기)의 크기를 작게 만드는 것이다.

좌측은 훈련 세트를 과도하게 학습했고, 우측은 보다 보편적으로 학습한 것을 알 수 있다.

특성의 스케일이 정규화되지 않으면 곱해지는 계수 값도 차이가 난다. 그렇기 때문에 규제를 적ㅇ굥하기 전에 정규화를 먼저해야 한다. 사이킷런의 StandardScaler 클래스를 이용할 수 있다.

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_poly)
train_scaled = ss.transform(train_poly)
test_scaled = ss.transform(test_poly)

 

선형 회귀 모델에 규제를 추가한 모델을 릿지(ridge)라쏘(lasso)라고 부르는데, 규제 방법이 다르다. 릿지는 계수를 제곱한 값을 기준으로 규제하고, 라쏘는 계수의 절댓값을 기준으로 규제한다. 보통 릿지를 더 선호하는 편이고, 두 알고리즘 모두 계수의 크기를 줄인다. 하지만 라쏘는 아예 0으로 만들 수도 있다.

 

<릿지 회귀>

사이킷런에서 제공하는 모델이라 편하게 릿지 회귀 알고리즘을 사용할 수 있다.

from sklearn.linear_model import Ridge
ridge = Ridge()
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

 

점수가 정상인 것을 알 수 있다. 릿지와 라쏘 모델을 사용할 때 alpha라는 매개변수를 통해 규제의 강도를 조절할 수 있다. 값이 커질수록 규제의 강도도 세진다. 적절한 alpha 값을 찾는 법은 alpha 값에 대한 결정계수 값의 그래프를 그려보는 것이다. 

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 릿지모델 만들기
    ridge = Ridge(alpha=alpha)
    # 릿지모델 훈련하기
    ridge.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(ridge.score(train_scaled, train_target))
    test_score.append(ridge.score(test_scaled, test_target))

 

이제 그래프를 그려볼 것이다. alpha 값을 10배씩 늘렸기 때문에 이대로 그래프를 그리면 왼쪽이 너무 촘촘해진다. 따라서, 동일한 간격으로 나타내기 위해 log함수로 바꿔서 표현할 것이다. np.log() 혹은 np.log10() 함수를 사용할 수 있다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

위가 훈련세트, 아래가 테스트세트의 그래프이다. 

 

좌측을 보면 두 데이터의 점수차가 아주 큰 것을 통해, 훈련 세트에만 잘 맞는 과대적합의 모습을 볼 수 있다. 반대로 우측을 보면 둘의 점수가 모두 낮아지는 과소적합의 모습을 볼 수 있다. 따라서 적절한 alpha 값은 둘의 거리가 가까우면서도 테스트 세트의 점수가 가장 높은 -1(0.1)임을 알 수 있다.

# 10의 -1제곱인 0.1을 alpha값으로 지정함
ridge = Ridge(alpha=0.1)
ridge.fit(train_scaled, train_target)
print(ridge.score(train_scaled, train_target))
print(ridge.score(test_scaled, test_target))

결과값을 통해 균형잡힌 모델임을 알 수 있다.

 

<라쏘 회귀>

라쏘 회귀도 릿지와 구현 방법은 비슷하다.

from sklearn.linear_model import Lasso
lasso = Lasso()
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

결과가 나쁘지 않지만, 릿지에서 했던 것처럼 최적의 alpha 값을 찾아보려 한다.

train_score = []
test_score = []

alpha_list = [0.001, 0.01, 0.1, 1, 10, 100]
for alpha in alpha_list:
    # 라쏘모델 만들기
    lasso = Lasso(alpha=alpha, max_iter=10000)
    # 라쏘모델 훈련하기
    lasso.fit(train_scaled, train_target)
    # 훈련 점수와 테스트 점수 저장
    train_score.append(lasso.score(train_scaled, train_target))
    test_score.append(lasso.score(test_scaled, test_target))

경고문구가 뜨지만, 정상작동되는 것이다. 반복 횟수가 부족할 때 경고가 발생할 수 있다.

다시 그래프를 그린다.

# 그래프 그리기 (로그)
plt.plot(np.log10(alpha_list), train_score)
plt.plot(np.log10(alpha_list), test_score)
plt.xlabel('alpha')
plt.ylabel('R^2')
plt.show()

좌측은 과대적합, 우측은 과소적합되는 걸로 미루어보아 최적의 alpha 값은 1, 즉 10임을 알 수 있다. 이로 다시 훈련을 해볼 수 있다.

# 10의 1제곱인 10을 alpha값으로 지정함
lasso = Lasso(alpha=10)
lasso.fit(train_scaled, train_target)
print(lasso.score(train_scaled, train_target))
print(lasso.score(test_scaled, test_target))

릿지와 마찬가지로 과대적합을 많이 줄이며 테스트 세트의 성능을 높인 것을 알 수 있다. 라쏘 모델은 계수 값을 0으로 만들 수 있기 때문에 얼마나 0이 되었는지 찾아볼 수 있다.

55개의 특성 중 40개가 0으로 변하고, 15개의 특성만 사용한 것을 알 수 있다. 라쏘 모델은 유용한 특성을 골라내는 데에 사용하기도 좋을 듯 하다.

 


● 마무리

다양한 선형 회귀 알고리즘을 통해 과대적합 문제와 과소적합 문제를 해결할 수 있다. 또한, 특성을 추가할수록 좋은 성능을 내는 선형 회귀 모델에 발맞춰 특성을 만들어보기도 하였다. 랏쏘와 릿지 규제를 사용하여 모델에 제약을 주기도 할 수 있다. 또한, 규제의 양을 조절하는 매개변수인 alpha 값도 직접 찾아볼 수 있다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

 

Chapter03_Regression_algorithms_and_model_regularization.ipynb
0.17MB

02. 데이터 다루기

 

02-1 훈련 세트와 테스트 세트

  • 시작하기 전에


1강에서 사용한 k-최근접 이웃알고리즘은 알고리즘을 찾는다기 보다는 입력한 49개의 데이터에 대한 최근접 이웃을 찾는 것이기 때문에 성공율이 100%일수밖에 없다.

 

<지도학습과 비지도학습>



입력(데이터)과 타깃(정답)이 있는 것이 지도학습, 타깃데이터가 없고 입력만 있는 것이 비지도학습이라고 한다. 강화학습은 모델이 어떤 행동을 수행한 다음 주변의 환경에서 행동의 결과를 피드백을 받아 개선해나가며 수행하는 방식이다.
k-최근접 이웃 알고리즘도 지도학습의 한 종류이다.


<훈련 세트와 테스트 세트>

연습문제와 시험문제가 같다면, 정답률은 100%일수밖에 없다. 따라서 머신러닝 프로그램의 성능을 제대로 평가하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 한다.

평가에 사용하는 데이터를 테스트 세트(test set), 훈련에 사용되는 데이터를 훈련 세트(train set)라고 부른다. 만약 데이터가 부족하다면, 일부를 떼어 내어 훈련시키고 남은 것으로 테스트를 진행해도 된다.

생선의 리스트를 복사하여 사용한다.

fish_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0, 9.8, 
                10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
fish_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0, 6.7, 
                7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

이제 각각의 항목들을 2차원 리스트로 묶는 작업을 진행할 것이다.

fish_data = [[l, w] for l, w in zip(fish_length, fish_weight)]
fish_target = [1]*35 + [0]*14

전체 49개 데이터 중 35개 데이터를 훈련데이터, 14개 데이터를 테스트데이터로 K최근접이웃알고리즘을 사용할 것이다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()

데이터 중에서 35개를 인덱스와 슬라이싱을 통해 접근한다.

# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_input = fish_data[:35]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_target = fish_target[:35]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
test_input = fish_data[35:]
# 훈련 세트로 입력값 중 0부터 34번째 인덱스까지 사용
train_target = fish_target[35:]

 연산을 통해 0~34까지의 인덱스( [:35] )를 훈련세트로, 인덱스 35~48까지의 인덱스 ( [35:] )를 테스트 세트로 선택했다. 이제 fit()메서드를 통해 모델을 훈련하고, score()메서드를 통해 평가한다.

kn = kn.fit(train_input, train_target)
kn.score(test_input, test_target)

이게 어찌된 걸까? 정확도가 0%다.. 도대체 어떤 것이 문제인걸까?

 

<샘플링 편향>

훈련/테스트 데이터를 나누려면 두 개의 항목이 골고루 섞이도록 우측 그림처럼 해야한다. 방금 전의 사례처럼, 데이터(샘플링)가 한 쪽으로 치우친 현상을 샘플링 편향(sampling bias)이라고 부른다. 따라서 이를 해결하기 위해서는 데이터를 나누기 전에 데이터를 섞거나 골고루 뽑아야 한다. 이런 작업을 편하게 하기 위해 파이썬 라이브러리인 넘파이를 사용하려고 한다.

 

<넘파이>

넘파이(numpy)는 파이썬의 대표적인 배열 라이브러리다. 넘파이를 활용하여 데이터를 2차원 배열로 변형할 수 있다.

import numpy as np
input_arr = np.array(fish_data)
target_arr = np.array(fish_target)

넘파이의 array() 함수에 파이썬 리스트를 넣고 출력하면 2차원 배열 형태로 출력이 된다.

print(input_arr)

 

shape 속성을 사용하면 배열의 크기를 알 수 있다.

print(input_arr.shape)

49개의 샘플과 2개의 특성이 있음을 알 수 있다.

 

 

입력과 타깃은 다르기 때문에, 하나는 인덱스로, 하나는 랜덤하게 섞어서 사용하기로 한다.

np.random.seed(42)
index = np.arange(49)
np.random.shuffle(index)

arange함수에 정수N을 전달하면 0에서부터 N-1까지 1씩 증가하는 배열을 만든다음, random함수와 shuffle 함수로 무작위로 섞는다.

print(index)
print(input_arr[[1,3]])

 

그다음 인덱스를 통해 제대로 섞였는지 출력해볼 수 있다.

 

train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]
test_input = input_arr[index[35:]]
test_target = target_arr[index[35:]]

만든 배열의 처음 35개를 랜덤하게 훈련세트로 만들고, 그 이후의 것은 테스트세트로 만든다.

이를 산점도로 만든다.

import matplotlib.pyplot as plt
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(test_input[:,0], test_input[:,1])
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

데이터가 랜덤하게 잘 섞인 것을 확인할 수 있다.

 

<두 번째 머신러닝 프로그램>

앞서 만든 데이터로 k-최근접 이웃 모델을 훈련시켜보려고 한다. fit() 메서드를 실행할 때마다 이전에 학습한 모든 것을 잃어버리기 때문에, 앞서 만든 것을 그대로 사용하려고 한다.

kn = kn.fit(train_input, train_target)

 

훈련세트를 훈련시켰으니, 이제 테스트하려고 한다.

kn.score(test_input, test_target)

100%의 정확도를 보이는 것을 알 수 있다. predict() 메서드를 통해 예측 결과와 실제 타깃 확인을 해 볼 수 있다.

kn.predict(test_input)
test_target

예측 결과와 테스트 세트의 정답이 일치하는 것을 확인할 수 있다. predict 메서드가 반환하는 것은 넘파이 배열이다. 사이킷런 모델에서 사용하는 입출력은 모두 넘파이 배열이다.

 

<선택미션>

 


02-2 데이터전처리

  • 시작하기 전에
    길이가 25cm, 무게 150g인 생선이 빙어라고 예측을 하는 사례가 발생했다.

<넘파이로 데이터 준비하기>

다시 데이터를 준비하여 프로그램을 만든다. 앞서 사용했던 생선의 리스트를 복사하여 사용한다.

column_stack 함수를 통해 리스트를 일렬로 세운 다음 나란히 연결한다. 아래처럼 [1,2,3] 리스트와 [4,5,6]을 하나씩 연결하면 하나씩 연결되어 2차원 리스트가 만들어진 것을 확인할 수 있다.

np.column_stack(([1,2,3], [4,5,6]))

 

두 리스트를 연결한다.

fish_data = np.column_stack((fish_length, fish_weight))
print(fish_data[:5])

 

그 다음 타깃데이터를 만드는데, 앞에서처럼 [1], [0]을 곱하지 않고 쉽게 넣을 수 있다. ones() 함수와 zeros() 함수를 사용하면 원하는 개수의 1과 0을 채운 배열을 만들 수 있다.

 

concatenate() 함수를 사용하여 배열을 연결할 수 있다. column_stack() 함수에서 처음 일렬로 배열했던 것처럼 일렬의 리스트 배열을 결과값으로 얻을 수 있다.

fish_target = np.concatenate((np.ones(35), np.zeros(14)))
print(fish_target)

 

<사이킷런으로 훈련 세트와 테스트 세트 나누기>

앞서 넘파이를 사용하지 않고 사이킷런으로 좀 더 손쉽게 데이터를 나눌 수도 있다. 사이킷런의 train_test_split() 함수를 사용하기 위해서는 먼저 import 해줘야 한다.

from sklearn.model_selection import train_test_split

 

그 다음 나누고 싶은 리스트나 배열을 원하는 만큼 전달하면 된다. random_state 매개변수는 자체적으로 랜덤 시드를 지정할 수 있는 함수이다.

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, random_state=42)

 

이 함수는 기본적으로 25%를 테스트 세트로 떼어 낸다.

 

잘 나뉘었는지 확인을 하고 잘 섞였는지 확인한다.

print(train_input.shape, test_input.shape)
print(train_target.shape, test_target.shape)
print(test_target)

 

도미와 빙어의 비율이 35:14이기 때문에 2.5:1의 비율을 가지고 있다. 하지만 테스트 세트의 비율은 3.3:1이기 때문에 샘플링 편향이 나타나는 것을 알 수 있다. 특정 클래스의 갯수가 적으면 이러한 일이 발생할 수 있다. 

이를 해결하기 위해 train_test_split 함수를 사용하여 stratify 매개변수에 타깃 데이터를 전달하여 비율을 맞춘다.

train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42)
print(test_target)

빙어가 한 개 증가하여 3개에서 총 4개가 된 것을 확인할 수 있다. 덕분에 비율도 2.25:1로 바뀌었다.

 

<수상한 도미 한 마리>

다시 만든 데이터로 앞서 발생했던 오류를 해결하기 위해 결과를 확인해보려고 한다.

kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

 

길이 25cm, 무게 150의 도미를 넣어본다.

print(kn.predict([[25,150]]))

 

헉! 왜 빙어로 예측을 하는걸까? 이를 확인하기 위해 산점도를 그려보려고 한다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')  # marker 매개변수는 모양을 지정
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

분명 산점도를 보면 도미데이터에 더 가까운데, 왜 빙어로 구분한걸까? k-최근접 이웃은 주변의 샘플 중에서 '다수'인 클래스를 예측으로 사용한다. 여기서 사용하는 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환한다. 기본값이 5이기 때문에 5개의 이웃이 반환된다.

그럼, 이웃 샘플을 다시 구분해서 산점도를 그려보도록 한다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

 

5개의 이웃 중 4개가 빙어임을 확인해볼 수 있다. 분명히 육안으로는 도미가 가까운데 왜 그런걸까? 거리를 확인해보기 위해 거리를 출력해본다.

print(distances)

 

<기준을 맞춰라>

빨간색 거리가 92인데, 파란색 거리가 130~138인건 아무리 봐도 이상하다. 왜 그런걸까?

그 이유는 x축은 범위가 좁고(10-40), y축은 범위가 넓기(0~1000) 때문이다. 그래서 y축으로는 조금만 멀어도 거리가 아주 큰 값으로 계산이 되는 것이다. 이를 눈으로 명확히 하기 위해 x축의 범위를 지정하는 xlim() 함수를 사용한다.

plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(25, 150, marker='^')
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker='D')
plt.xlim((0, 1000))
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

 

x축과 y축의 동일하게 맞췄더니 데이터가 일직선의 모양으로 나타난다. 두 특성(길이/무게)의 값이 놓인 범위가 매우 다르다. 이를 두 특성의 스케일이 다르다고 말한다. 이렇게 기준이 다르면 알고리즘이 올바르게 예측할 수 없기 때문에, 특성값을 일정한 기준으로 맞춰줘야 한다. 보통 이러한 작업을 데이터 전처리라고 한다.

 

가장 널리 사용하는 방법은 표준점수이다. 표준점수는 각 특성값이 0에서 표준편차의 몇 배만큼 떨어져있는지를 나타낸다. 이를 계산하려면 평균을 빼고 표준편차를 나누면 된다. 넘파이의 mean() 함수를 통해 평균을, std() 함수를 통해 표준편차를 계산할 수 있다. axis는 중심선으로, 0을 기준으로 그래프를 그리려면 axis=0으로 해줘야 한다.

mean = np.mean(train_input, axis=0)
std = np.std(train_input, axis=0)
print(mean, std)

이제 평균을 빼고 표준점수로 변환하는 작업이 필요하다. 넘파이에서는 이 기능을 제공하는데, 이러한 기능을 브로드캐스팅(broadcasting)이라고 한다.

 

<전처리 데이터로 모델 훈련하기>

앞서 변환한 표준점수를 산점도로 그려본다.

plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(25, 150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

샘플을 제외한 나머지 데이터가 뭉쳐있는 것을 확인할 수 있다. 훈련 세트를 평균으로 빼고 표준편차로 나눴기 때문에 값의 범위가 달라져서 발생하는 현상이다. 따라서 다시 샘플을 변환하여 산점도를 그려야 한다.

new = ([25, 150] - mean) /std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

앞서 봤던 산점도와 거의 유사하지만 x축,y축의 범위가 바뀐 것을 확인할 수 있다. 다시 이웃모델을 찾아본다.

kn = kn.fit(train_scaled, train_target)
test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)

모든 테스트 샘플을 완벽하게 분류한 것을 알 수 있다. 다시 25cm, 150g의 생선을 제대로 분류하는지 확인한다.

print(kn.predict([new]))

도미로 제대로 분류하는 것을 알 수 있다. 이제 다시 k-최근접 이웃 알고리즘을 활용하여 산점도를 그려본다.

distances, indexes = kn.kneighbors([new])
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(new[0], new[1], marker='^')
plt.scatter(train_scaled[indexes,0], train_scaled[indexes,1], marker='D')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

25cm, 150g의 샘플근접 이웃은 제대로 도미로 보이고, 따라서 도미로 예측하는 것을 확인할 수 있다.


● 마무리

데이터를 확인할 때 스케일이 같은지, 다른지를 확인하며 전처리 처리를 해야한다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter02_train_and_test_data.ipynb
0.09MB

박해선님의 혼자 공부하는 머신러닝+딥러닝 책과 유튜브 영상을 참고하여 공부하며 정리하였습니다. 

 


01. 나의 첫 머신러닝

 

01-1 인공지능과 머신러닝, 딥러닝

  • 시작하기 전에

책의 그림을 통해 인공지능의 역사를 볼 수 있다.

 

<인공지능이란>

참고사이트 : http://scimonitors.com/aiai기획②-인공지능-발달과정-튜링부터-구글-알파고-ibm/

 

Science Monitor

AI, Quantum Computing Science & Technology Magazine

scimonitors.com

1) 인공지능 태동기(1943-1956) : 인공지능에 대한 꿈이 부풀어오르던 초기시기, 튜링 테스트의 등장.

2) 인공지능 황금기(1956-1974) : 다트먼스 AI 컨퍼런스에서 인공지능에 대한 장밋빛 전망 발표, 퍼셉트론의 등장

  • 퍼셉트론 : 인공신경망의 기본이 되는 알고리즘으로 1957년에 발표된 개념.
  • 1959년 고양이 뇌에 전기자극을 준 후 눈에 어떤 자극이 발생하는지 슬라이드를 통해 시각 피질연구. 이를 통해 인간의 뇌에도 시각세포가 반응을 해서 단계적으로 복잡한 사물을 인식한다는 것이 확인되었다. CNN의 시초가 된 연구로 알고 있다. 

3) 1차 AI 겨울(1974-1980) : 주 원인은 컴퓨터 성능의 한계. 문제를 풀 수 없기 때문에 인기가 사라짐.

4) AI붐(1980-1987) : 전문가 시스템 발견

5) 2차 AI 겨울(1987-1993) : 전문가 시스템 실패

6) 1998년 LeNet-5의 발견이후로 인공지능 분야가 흥행하였다. 합성곱 신경망을 사용하여 최초로 숫자를 인식하는 모델을 만들었다. 2012년 ImageNet 데이터를 사용하는 경진대회에서 AlexNet이라는 합성곱 신경망을 통해 괄목한 성장을 보여주었다. AlexNet이후로 나온 대부분의 알고리즘은 이미지 관련 작업에서는 합성곱 신경망을 사용하고 있다.

2015년 구글에서 TensorFlow라는 라이브러리를 발표하였고, 이후 대중적으로 많은 개발자들이 DL/ML분야를 연구하게 되었고, 2016년 구글에서 알파고를 발표하며 이세돌과의 바둑 대국이 있었다. 이후, 전세계적으로 인공지능 분야에 큰 붐이 일어났다. 그 이후에도 많은 알고리즘과 사건이 있고 최근 많은 성장을 하였다.

 

 

인공지능을 한마디로 표현한다면 사람처럼 학습, 생각, 추론할 수 있는 컴퓨터 시스템/알고리즘이다.

마치 영화 <her>의 사만다처럼 지능을 가진 시스템. 마치 사람과도 같아서 <터미네이터>의 스카이넷처럼 사람과 구분하기 어려운 수준인데, 이를 강한 인공지능(Strong AI)라고 부른다. 아직은 영화지만, 현실에서 불가능하다고만은 말할 수 없는 일이다.

 

지금의 인공지능은 사람의 일을 도와주고 반복할 수 있는 작업을 행하는 등, 편리한 생활을 누릴 수 있도록 보조적인 역할을 한다.

현재 주위에서 볼 수 있는 자율주행자동차 테슬라, 아이폰의 Siri(음성비서), 음악추천, 기계번역 등은 약한 인공지능(Week AI)라고 말할 수 있다. 아직은 인공 일반지능에 언제 도달할 지 그 시기를 알 수 없지만, 그 가능성에 대해서는 대체적으로 긍정적이다.

 

<머신러닝이란>

머신러닝은 인공지능의 하위 분야이다. 여러 하위 분야 중에서도 소프트웨어를 담당하는 핵심 분야이다. 규칙을 프로그래밍하지 않아도 자동적으로 데이터에서 규칙을 학습하는 알고리즘을 연구하는 분야이다.

대표적인 머신러닝 라이브러리는 사이킷런(scikit-learn)이다. 사이트에 접속하여 라이브러리에 대한 정보를 얻을 수 있다.

https://scikit-learn.org/stable/

 

scikit-learn: machine learning in Python — scikit-learn 1.0.2 documentation

Model selection Comparing, validating and choosing parameters and models. Applications: Improved accuracy via parameter tuning Algorithms: grid search, cross validation, metrics, and more...

scikit-learn.org

사이킷런은 파이썬 API를 사용하고 있다. 많은 알고리즘이 들어있으며, 그 중에서도 자주 사용되는 알고리즘을 알아두면 모델을 짤 때 유용하다. 사이킷런은 오픈서스 라이브러리이기 때문에, 파이썬 코드를 다룰 수 있다면 누구나 쉽게 머신러닝 알고리즘을 제품에 활용할 수 있다. 

사이킷런의 공개 이후로 많은 개발자가 머신러닝 알고리즘에 접근할 수 있게 되었으며, 이를 통해 프로그래머가 직접 알고리즘을 구현할 필요가 없게 되었다.

 

 

<딥러닝이란>

 

딥러닝은 인공신경망의 다른 이름으로, 머신러닝의 하위 분야이다. 머신러닝의 다양한 알고리즘 중 한 부류를 딥러닝, 혹은 인공신경망이라고 부를 수 있다. 종종 사람들은 둘을 크게 구분하지 않고 사용한다.

딥러닝은 알파고를 통해 유명해졌고, 딥러닝에도 머신러닝의 사이킷런처럼 라이브러리가 있다.

주로 널리 사용되는 라이브러리는 구글의 텐서플로우(TensorFlow)이다. 또 Facebook(meta)에서 출시한 파이토치(PyTorch)도 요새 많은 인기를 얻고 있다. 이 둘도 오픈소스이기 때문에 누구나 쉽게 사용할 수 있으며, 인공 신경망 알고리즘을 전문적으로 다루며 파이썬 API를 사용한다.

 

 


 

01-2 코랩과 주피터 노트북

  • 시작하기 전에
    머신러닝을 학습하기 위한 환경은 두 가지로 나눠볼 수 있다.
    1) 로컬컴퓨터 : 파이썬, 사이킷런, 텐서플로 등을 설치해야하며 이를 설치하기 위해서는 아나콘다, 도커 등의 가상환경이 필요하다. 아나콘다를 설치하면 대부분의 라이브러리들이 자동으로 설치되기 때문에 파이썬, 사이킷런 등을 설치할 필요가 없다. 또한 GPU가 필요한 경우가 있기 때문에 로컬에서 고성능의 컴퓨터를 이용하기에는 부담되는 경우가 많다.

    2) 코랩 : 구글에서 제공하는 주피터노트북 개발환경으로, 구글 계정만 있으면 누구나 쉽게 사용이 가능하다.

<구글 코랩>

머신 러닝은 컴퓨터 사양이 중요한데, 코랩을 사용하면 컴퓨터 성능과 별개로 프로그램을 이용할 수 있다는 장점이 있다. 구글 계정이 있다면 코랩(Colab)에 접속하여 사용이 가능하다. 

코랩 웹사이트에 접속하면 위와같은 화면이 뜨는데, 내가 작업하던 파일을 불러와서 다시 하거나, 드라이브나 깃허브의 파일, 혹은 업로드를 하여 작업을 진행할 수 있다.

 

<텍스트 셀>

텍스트 셀은 텍스트를 입력할 수 있는  창이고, HTML과 마크다운을 혼용해서 사용할 수 있다. 

코랩에 접속했을 때 뜨는 기본 소개 문구에서 위의 셀을 더블클릭하면 더욱 자세하게 볼 수 있다.

왼쪽의 창에서 텍스트를 수정하면 우측 미리보기 창에서 수정된 결과를 바로 확인할 수 있다.

 

<코드 셀>

이처럼 세번째에 위치한 셀이 코드셀이다. 결과는 코드가 끝난 다음 셀에 표시된다.

 

<노트북>

코랩은 구글이 주피터를 커스터마이징한 것으로, 구글 클라우드의 가상 서버를 사용한다. 우측 상단의 RAM과 디스크가 나와있는 부분에 마우스를 가져다대면 런타임 정보를 알려준다.

그림 우측의 화살표를 누르고 리소스보기를 누르면 리소스와 RAM 의 연결사태를 확인할 수 있다. 만약 연결이 끊겼다면 [연결] 버튼을 통해 연결할 수 있다. 

코랩 서버의 메모리는 약 12기가, 디스크 공간은 100기가로 구글 계정만 있다면 무료로 가상서버를 활용할 수 있다는 장점이 있다. 하지만 무료 서비스이기 때문에, 무한정으로 사용할 수 있지는 않고 하나의 노트북을 12시간 이상 실행할 수 없다. 만약 그 이상의 시간동안 사용하고 싶다고 하면 월 1만원 가량의 구독료로 코랩프로(Colab Pro)를 구독하면 된다.

 

 

* 새 노트북 만들기

 

[파일] -[새 노트]를 클릭하여 새로운 노트북을 만들 수 있다.

기본 이름은 Untitled숫자.ipynb 형태이고, 클릭하여 본인이 원하는 이름으로 바꿀 수 있다.

 

코드 셀에 코드를 입력한 후 실행할 수 있다. 코드셀 좌측의 플레이 아이콘을 클릭하거나 Ctrl+Enter키를 통해 실행할 수 있다. (macOS는 cmd+Enter)

 

 

 

노트북은 자동으로 구글 드라이브의 [Colab Notebooks] 폴더에 저장이 된다. 드라이브에서 바로 코랩으로 연결할 수도 있고, 드라이브 내에서 파일명을 변경하는 것 또한 가능하다.

 

 


 

01-3 마켓과 머신러닝

  • 시작하기 전에


    생선을 판매하는 한빛 마켓의 물류 센터에서, 생선을 자동으로 분류해주는 머신러닝 프로그램을 만들기로 한다.

 

 

<생선 분류 문제>

생선의 크기가 30-40cm라면 '도미'라고 알려주는 그런 프로그램을 만들어 보도록 한다.

 

전통적인 프로그램은 가이드와 규칙을 정해두고 프로그램을 만든다. 하지만 만약 생선이 30cm 이상이라고 무조건 도미라고 말할 수 있을까? 고래처럼 원래 몸집이 큰 경우에는 다른 기준을 적용해야 한다. 이처럼 절대 변하지 않는 기준을 정하기 어렵거나, 가이드나 규칙을 정하기 어려운 경우에는 머신러닝 프로그램을 통해 적용할 수 있다. 

프로그램을 만들기에 앞서, 도미와 빙어를 분류하는 프로그램을 만들기로 한다.

 

  •  도미 데이터 준비하기


코랩에서 [BreamAndSmelt] 노트를 만든다. 박해선님의 깃허브에서 도미 데이터를 복사할 수 있다. 해당 데이터는 캐글에 공개된 데이터셋이라고 한다.

 

bream_length = [25.4, 26.3, 26.5, 29.0, 29.0, 29.7, 29.7, 30.0, 30.0, 30.7, 31.0, 31.0, 
                31.5, 32.0, 32.0, 32.0, 33.0, 33.0, 33.5, 33.5, 34.0, 34.0, 34.5, 35.0, 
                35.0, 35.0, 35.0, 36.0, 36.0, 37.0, 38.5, 38.5, 39.5, 41.0, 41.0]
bream_weight = [242.0, 290.0, 340.0, 363.0, 430.0, 450.0, 500.0, 390.0, 450.0, 500.0, 475.0, 500.0, 
                500.0, 340.0, 600.0, 600.0, 700.0, 700.0, 610.0, 650.0, 575.0, 685.0, 620.0, 680.0, 
                700.0, 725.0, 720.0, 714.0, 850.0, 1000.0, 920.0, 955.0, 925.0, 975.0, 950.0]​



리스트에서 첫번째 도미의 길이는 25.4cm, 무게는 242.0g인 것을 알 수 있다. 이러한 특징들을 특성(Feature)이라 부를 수 있다. 이 데이터를 통해 길이를 x축, 무게를 y축으로 하는 그래프를 그려 각 도미들을 점으로 표시할 수 있다.

파이썬에서 자주 사용되는 패키지는 맷플롯립(matplotlib)이다.

import matplotlib.pyplot as plt # matplotlib의 pyplot 함수를 plt로 줄여서 사용

plt.scatter(bream_length, bream_weight)
plt.xlabel('length')  # x축은 길이
plt.ylabel('weight')  # y축은 무게
plt.show()​

 

코랩의 코드셀에 위의 코드를 입력하면 산점도를 확인할 수 있다.


산점도를 통해 유추할 수 있는 사실은 생선의 길이가 길수록 무게가 많이 나간다는 것이다. 이처럼 일직선에 가까운 형태로 나타나는 모습을 선형적(linear)이라고 말한다.

  • 빙어 데이터 준비하기

마찬가지로 박해선님의 깃허브에서 빙어 데이터를 복사해서 사용할 수 있다.

smelt_length = [9.8, 10.5, 10.6, 11.0, 11.2, 11.3, 11.8, 11.8, 12.0, 12.2, 12.4, 13.0, 14.3, 15.0]
smelt_weight = [6.7, 7.5, 7.0, 9.7, 9.8, 8.7, 10.0, 9.9, 9.8, 12.2, 13.4, 12.2, 19.7, 19.9]

해당 데이터를 사용해 도미데이터와 비교를 할 수 있다.

plt.scatter(bream_length, bream_weight)
plt.scatter(smelt_length, smelt_weight)
plt.xlabel('length')
plt.ylabel('weight')
plt.show()

 

주황색이 빙어, 파란색이 도미임을 알 수 있다. 또 도미는 무게와 길이에 선형적인 관계가 있는 반면, 빙어는 길이가 늘어나도 무게가 크게 증가하지 않는 것을 확인할 수 있다.

 

 

<첫 번째 머신러닝 프로그램>

여기서는 k-최근접 이웃(k-Nearest Neighbors) 알고리즘을 사용하기 위해 두 생선의 무게와 길이 데이터를 합친다.

length = bream_length + smelt_length
weight = bream_weight + smelt_weight

 

사이킷런 패키지를 사용하기 위해 2차원 리스트를 만들려면 zip() 함수를 사용하면 된다.

fish_data = [[l, w] for l, w in zip(length, weight)]

 

만든 2차원 리스트를 출력한다.

print(fish_data)

 

해당 2차원 리스트가 완성된 후, 준비할 데이터는 정답 데이터이다. 즉, 데이터만으로 어떤 것이 도미이고, 빙어인지 정답을 알려주는 것이다.

fish_target = [1] * 35 + [0] * 14
print(fish_target)

 

결과값은 아래와 같다.

 

이제 k-최근접 이웃 알고리즘 클래스를 넣어둔 패키지를 import한다.

from sklearn.neighbors import KNeighborsClassifier

 

클래스의 객체를 만든다.

kn = KNeighborsClassifier()

 

파이썬의 fit() 메서드를 통해 도미를 찾기 위한 기준을 훈련(학습)시킨다.

kn.fit(fish_data, fish_target)

 

이제 객체kn이 얼마나 잘 훈련되었는지 평가할 수 있다. score() 메서드를 통해 정답률을 알 수 있다. 0~1의 값으로만 결과값이 반환된다. 0.5면 50%를 맞춘 것이고 1이면 100%를 맞힌 것이다. 이를 정확도라고 부른다.

kn.score(fish_data, fish_target)

 

결과물이 1.0으로 나온것으로 보아 100% 학습에 성공했음을 알 수 있다.

 

<k-최근접 이웃 알고리즘>

k-최근접 이웃 알고리즘이란 어떤 데이터에 대해 답을 구할 때 주위의 다른 데이터를 보고 다수를 차지하는 것을 정답으로 사용하는 것이다. 주위의 데이터로 현재 데이터를 판단하는 것이다.

예를 들어 길이가 30cm, 무게가 600g인 삼각형을 보면 근처에 도미가 있기 때문에, 도미인 것으로 추론할 수 있다. predict() 메서드를 통해 모델을 훈련, 예측할 수 있다. fit() 메서드는 두 매개변수로 훈련에 사용하라 특성과 정답 데이터를 전달한다.

kn.predict([[30, 600]])
print(kn._fit_X)

 

해당 데이터를 기존 만들어둔 49개의 데이터를 활용하여 적용한다면, 49개의 데이터 중 도미가 35개 이므로 결과값은 무조건 도미라고 나올 것이다.

kn49 = KNeighborsClassifier(n_neighbors=49)
kn49.fit(fish_data, fish_target)
kn49.score(fish_data, fish_target)
print(35/49)

둘의 결과값이 동일한 것으로 미루어 보아 같은 매개변수를 49가 아닌 기본값으로 설정하는 것이 좋다는 것을 알 수 있다.

 


<마무리>

첫 머신러닝 프로그램을 통해 사이킷런의 메서드를 사용하고, k-최근접 이웃 알고리즘의 특징을 알아보았다.

코랩에서 실행한 파일을 공유드리니, 필요하신 분은 다운받아 사용하시면 됩니다.

Chapter01-BreamAndSmelt.ipynb
0.04MB

한빛미디어에서 주최하는 혼공학습단에 지원하였고, 혼공학습단 7기에 선정되었다.

도서 별 커리큘럼이 있고, 혼공 시리즈 도서 중 하나를 선택하여 미션을 성공하면 되는 간단한 활동이다.

 

 

미션을 완수하면 마일리지 2만원과 더불어 우수 학습자에게는 백화점 상품권까지 증정한다고 한다.

내가 선택한 책은 박해선님의 혼자 공부하는 머신러닝+딥러닝이다.

이미 봤던 책이지만 리마인드 겸 블로그에 정리를 하려던 찰나에,

좋은 컨텐츠를 제공해주신 한빛미디어에게 감사하다.

 

커리큘럼은 위와 같다.

설 연휴 주를 제외한 6주간의 활동(1/10~2/27) 동안 진행이 되기 때문에, 

혹시나 신청을 못했더라도 같은 진도로 진행하면 좋을 듯 하다.

 

 

https://youtu.be/J6wehCO_c58

 

박해선님의 유튜브 강의를 통해 도움을 받는다면,

혼공머신 책과 함께 혼자 공부할 수 있다.

 

내일부터 시작되는 혼공학습단 7기 화이팅!

 

 

 

 

+ Recent posts