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

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


 

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