본문 바로가기
Programming/Machine Learning

머신러닝 - 훈련 세트와 테스트 세트

by IT learning 2021. 3. 30.
728x90

오늘은 훈련 세트와 테스트 세트, 지도 학습과 비지도 학습 등을 배워보겠다.

 

본 내용은 '혼자 공부하는 머신러닝 + 딥러닝' 교재를 이용하여 배운 것을 토대로 작성합니다.

 

위 머신러닝 공부를 할때 사용하는 IDE는 '구글 코랩' 입니다. 

코랩 사용법을 익히고 오시길 바랍니다.

 

또한 파이썬의 기초적인 지식을 알아야 합니다.

위 코드들은 각자의 파일이 아닌 하나의 파일에 작성된 코드들입니다.

 

 

시작하기 전에

저번시간에 우리는 도미와 빙어를 완벽하게 분류한 첫 번째 머신러님 모델을 제작했었다. 제작한 모델을 사장님께 드렸다.

그런데, 사장님이 모델을 보며 고개를 갸우뚱 했다. 뭔가가 이상한가 보다.

도미 35마리와 빙어 14마리를 모두 저장하고 맞추는 거라면 100%를 달성하는게 당연하지 않나?
어떤 것이 도미이고 빙어인지 알고 있는데 맞추지 못하는 것이 이상하잖소.

음..뭐가 잘못된 것일까? 다시 한번 생각해보며 오늘 배울 것을 시작해보자.

 

지도 학습과 비지도 학습

머신러닝 알고리즘은 크게 지도 학습(supervised learning)비지도 학습(unsupervised learning)으로 나눌 수 있다.

지도 학습 알고리즘은 훈련하기 위한 데이터정답이 필요하다.

 

저번 시간에 보았던 도미와 빙어의 예를 보면 생선의 길이와 무게를 알고리즘에 사용했다. 이 경우 정답은 도미인지 아닌지 여부이다.

지도 학습에서는 데이터와 정답을 입력타깃이라고 하고, 이 둘을 합쳐 훈련 데이터라고 부른다.

 

그리고 앞서 언급했듯이 입력으로 사용된 길이와 무게를 특성이라고 한다. 

도미와 빙어를 구분하는 데 사용한 길이와 무게가 특성인 것이다.

 

지도 학습은 정답(타깃)이 있으니 알고리즘이 정답을 맞히는 것을 학습한다. 예를 들어 도미인지 빙어인지 구분하는 것 말이다.

반면, 비지도 학습 알고리즘은 타깃 없이 입력 데이터만 사용한다. 이런 종류의 알고리즘은 정답을 사용하지 않으므로 무언가를 맞힐 수가 없다. 대신 데이터를 잘 파악하거나 변형하는데 도움을 준다.

 

[머신러닝 알고리즘은 지도 학습, 비지도 학습 외에 강화 학습을 또 다른 종류로 크게 분류 한다. 하지만 우리는 두 개만 알아보겠다.]

 

도미와 빙어를 구분하기 위해 사용한 k-최근접 이웃 알고리즘입력 데이터타깃(정답)을 사용했으므로 당연히 지도 학습 알고리즘이다. 이 알고리즘을 훈련하여 생선이 도미인지 아닌지를 판별하고, 이 모델이 훈련 데이터에서 도미를 100% 완벽하게 판단했다.

근데 무엇이 문제였을까?

 

훈련 세트와 테스트 세트

만약에 시험을 보기 전에 시험 문제와 정답을 알려주고 시험을 본다면 어떻게 될까? 정답을 외우는 노력을 하지 않는 이상 100점을 맞을 것이다. 머신러닝도 이와 마찬가지이다. 도미와 빙어의 데이터와 타깃을 주고 훈련한 다음, 같은 데이터로 테스트 한다면 모두 맞는게 정상이다. 연습 문제와 시험 문제가 달라야 올바르게 학생의 능력을 평가 할 수 있듯이, 머신러닝도 성능을 제대로 평가 하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 한다.

 

이렇게 하는 가장 간단한 방법은 평가를 위해 또 다른 데이터를 준비하거나, 이미 준비된 데이터 중에서 일부를 떼어 내어 활용하는 것이다. 보통 후자의 경우를 많이 사용한다. 평가에 사용하는 데이터를 테스트 세트, 훈련에 사용되는 데이터를 훈련 세트라고 부른다.

 

정리를 해보면, 아까 우리는 모델을 훈련하기 위한 데이터를 사용해 이 모델의 정확도를 100%라고 평가했었다. 하지만 훈련에 사용한 데이터로 모델을 평가하는 것은 적절하지 않다. 훈련할 때 사용하지 않은 데이터평가해야 한다. 이를 위해 훈련 데이터에서 일부를 떼어 내어 테스트 세트로 사용하겠다.

 

먼저 저번처럼 도미와 빙어의 데이터를 합쳐 하나의 파이썬 리스트로 준비한다.

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

리스트 내포 구문과 zip() 함수를 이용했었다.

 

이때 하나의 생선 데이터를 샘플이라고 부른다. 도미와 빙어는 각각 35마리, 14마리가 있으므로 전체 데이터는 49개의 샘플이 존재한다.

사용하는 특성은 길이와 무게 2개이다. 이 데이터의 처름 35개를 훈련 세트로, 나머지 14개를 테스트 세트로 사용해보겠다.

 

먼저 사이킷런의 KNeighborsClassifier 클래스를 임포트하고 모델 객체를 만든다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()

이제 전체 데이터에서 처음 35개를 선택해야한다. 일반적으로 리스트처럼 배열의 요소를 선택할 때는 배열의 위치, 즉 인덱스를 지정한다.

예를 들어 fish_data의 다섯 번째 샘플을 출력하기 위해서는 다음과 같이 쓸 수 있다.

print(fish_data[4])

배열의 인덱스는 0부터 시작한다. 그래서 인덱스 4는 5번째 샘플이다.

 

파이썬 리스트는 인덱스 외에도 슬라이싱이라는 특별한 연산자를 제공한다. 슬라이싱은 콜론을 가운데 두고 인덱스의 범위를 지정하여

여러개의 원소를 선택해서 사용할 수 있다. 예를 들어 첫 번째 부터 다섯 번째까지의 샘플을 선택해 보겠다.

print(fish_data[0:5])

5개의 샘플이 선택되었다. 슬라이싱을 사용할 때는 마지막 인덱스의 원소는 포함되지 않는다는 점을 주의해야 한다.

예를 들어 위와 같이 0:5를 지정했다면 0~4까지의 5개의 원소만 선택되고 인덱스 5인 여섯 번째 원소는 선택되지 않는다.

만약 0부터 시작하는 슬라이싱의 경우 0을 생략하고 사용할 수 있다.

 

이와 비슷하게 마지막 원소까지 포함할 경우 두번째 인덱스를 생략할 수 있다.

예를 들어 마지막에서 5개의 샘플을 출력할 때 '44:49'와 같이 쓰지 않고 44:만 써도 된다.

print(fish_data[44:])

 

이를 응용하면 생선 데이터에서 처음 35개와 나머지 14개를 선택하는 것을 간단하게 처리할 수 있다.

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

슬라이싱 연산으로 인덱스 0~34까지 처음 35개 샘플을 훈련 세트로 선택했고, 인덱스 35~48까지 나머지 14개 샘플을 테스트 세트로 선택했다. 데이터를 준비했으니 훈련 세트로 fit() 메소드를 호출해 모델을 훈련하고, 데스트 세트로 score() 메소드를 호출해 평가해 보겠다.

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

????????? 정확도가 0.0이다. 저번 시간까지 완벽했던 머신러닝 모델이 갑자기 0%의 정확도 프로그램이 되었다. 무엇이 잘못된 것일까?

 

자, 훈련 세트와 테스트 세트에 들어가있는 것들을 생각해보자.

그렇다. 테스트 세트에 빙어만 들어가 있고, 훈련 세트에는 도미만 들어가있다.

상식적으로 훈련하는 데이터와 테스트하는 데이터에는 도미와 빙어가 골고루 섞여 있어야 한다.

일반적으로 훈련 세트와 테스트 세트에 샘플이 골고루 섞여 있지 않으면 샘플링이 한쪽으로 치우쳤다는 의미로 샘플링 편향(sampling bias)이라고 부른다.

 

이 프로그램에서는 훈련 세트에 도미만 있기 때문에 테스트 세트가 무엇이든 무조건 도미라고 분류한다. 

그런데 테스트 세트는 빙어만 있기 때문에 정답을 하나도 맞추지 못한다.

그렇다면 훈련 세트와 테스트 세트를 나누기 전에 데이터를 섞든지, 아니면 골고루 샘플을 뽑아서 훈련 세트와 테스트 세트를 만들어야 한다.

 

이런 작업을 간편하게 처리할 수 있도록 도움을 주는 파이썬 라이브러리에 대해 배워보겠다.

 

넘파이

넘파이는 파이썬의 대표적인 배열 라이브러리이다. 앞서 파이썬의 리스트로 2차원 리스트를 표현할 수 있지만, 고차원 리스트를 표현 하려면 매우 번거롭다. 넘파이는 고차원 배열을 손쉽게 만들고 조작할 수 있는 간편한 도구들을 많이 제공해준다.

 

넘파이는 보통의 xy좌표계와는 달리 시작점이 왼쪽 아래가 아니고 왼쪽 위부터 시작한다. 

그럼 생선 데이터를 2차원 넘파이 배열로 변환해보자.

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

print(input_arr)        # 2차원 넘파이 배열로 변환
print(input_arr.shape)  # 배열의 크기를 알려주는 shape 속성

파이썬 리스트를 넘파이 배열로 바꾸기는 정말 쉽다. 넘파이 array() 함수에 파이썬 리스트를 전달하면 끝이다.

input_arr배열을 출력하면,

[[  25.4  242. ]
 [  26.3  290. ]
 [  26.5  340. ]
 [  29.   363. ]
 [  29.   430. ]
 [  29.7  450. ]
 [  29.7  500. ]
 [  30.   390. ]
 [  30.   450. ]
 [  30.7  500. ]
	.
    	.
        .
 [  12.     9.8]
 [  12.2   12.2]
 [  12.4   13.4]
 [  13.    12.2]
 [  14.3   19.7]
 [  15.    19.9]]

 

기대했던 것과 동일한 형태인 2차원 리스트로 표현이 된다. 넘파이는 친절하게 배열의 차원을 구분하기 쉽도록 행과 열을 가지런히 출력한다. 

 

눈으로 확인하는 것 외에도 넘파일 배열 객체는 배열의 크기를 알려주는 shape 속성을 제공한다.

(49, 2)

샘플수 : 49 특성 수: 2 개로 정확히 나온다.

 

이제 생선 데이터를 넘파이 배열로 준비했으니 이 배열에서 랜덤하게 샘플을 선택해 훈련 세트와 테스트 세트로 만들 차례다.

여기에서는 배열을 섞은 후 나누는 방식 대신에 무작위로 샘플을 고르는 방법을 사용하겠다.

 

한 가지 주의할 점은 input_arr와 target_arr에서 같은 위치는 함께 선택되어야 한다는 점이다. 예를 들어 input_arr의 두 번째 값은 훈련 세트로 가고, target_arr의 두번 째 값은 테스트 세트로 가면 안되지 않겠는가? 타깃이 샘플과 함께 이동하지 않으면 올바르게 훈련될 수 없다.

 

이렇게 하려면 훈련 세트와 테스트 세트로 나눌 인덱스값을 잘 기억해야 한다.

그런데 항상 인덱스값을 기억할 수는 없으니 다른 방법이 필요하다.

아예 인덱스를 섞은 다음 input_arr와 target_arr 에서 샘플을 선택하면 무작위로 훈련 세트를 나누는 셈이 된다.

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

넘파이 arange() 함수를 사용하면 0에서부터 48까지 1씩 증가하는 인덱스를 간단히 만들 수 있다. 그 다음 이 인덱스를 랜덤하게 섞는다.

 

# 넘파이에서 무작위 결과를 만드는 함수들은 실행할 때마다 다른 결과를 만든다. 일정한 결과를 얻으려면 초기에 랜덤 시드를 지정하면 된다. 필자와 동일한 실습 결과를 얻을 수 있도록 랜덤 시드를 42로 지정했다.

 

넘파이 random 패키지 아래에 있는 shuffle() 함수는 주어진 배열을 무작위로 섞는다.

np.random.shuffle(index)
[13 45 47 44 17 27 26 25 31 19 12  4 34  8  3  6 40 41 46 15  9 16 24 33
 30  0 43 32  5 29 11 36  1 21  2 37 35 23 39 10 22 18 48 20  7 42 14 28
 38]

0부터 48까지 정수가 잘 섞였다. 이제 랜덤하게 섞인 인덱스를 사용해 전체 데이터를 훈련 세트와 테스트 세트로 나누어 보자.

 

넘파이는 슬라이싱 외에 배열 인덱싱이란 기능을 제공한다. 배열 인덱싱은 1개의 인덱스가 아닌 여러 개의 인덱스로 한 번에 여러 개의 원소를 선택할 수 있다. 예를 들어 다음처럼 input_arr에서 두 번째와 네 번째 샘플을 선택하여 출력 할 수 있다.

print(input_arr[[1,3]])

 

비슷한 방식으로 리스트 대신 넘파이 배열을 인덱스로 전달할 수도 있다.

앞서 만든 index 배열의 처음 35개를 input_arr 와 target_arr에 전달하여 랜덤하게 35개의 샘플을 훈련 세트로 만들겠다.

train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]
print(input_arr[13],train_input[0])

만들어진 index의 첫 번째 값은 13이다. 따라서 train_arr의 첫 번째 원소는 input_arr의 열네 번째 원소가 들어가 있을 것이다.

[ 32. 340.] [ 32. 340.]

정확하게 일치한다.

 

이번에는 나머지 14개를 테스트 세트로 만들어 보겠다.

test_input = input_arr[index[35:]]
test_target = target_arr[index[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()

2차원 배열은 행과 열 인덱스를 콤마로 나누어 지정한다. 슬라이싱 연산자로 처음부터 마지막 원소까지 모두 선택하는 경우 시작과 종료 인덱스를 모두 생략 할 수 있다.

위 코드의 그래프이다.

파란색이 훈련 세트이고 주황색이 테스트 세트이다. 양쪽에 도미와 빙어가 모두 섞여 있다. 의도한 대로 잘 만들어진 것 같다.

이제 모델을 다시 훈련 시켜보자!

 

두 번째 머신러닝 프로그램

앞서 만든 훈련 세트와 테스트 세트로 k-최근접 이웃 모델을 훈련시켜보자.

fit() 메소드를 실행 할 때마다 이전에 학습한 모든 것을 잃어버린다.

따라서 이전에 모델을 그대로 두고 싶다면 새로운 객체를 만들어야 한다. 하지만 우리는 그냥 만들어진 객체를 사용하겠다.

kn = kn.fit(train_input, train_target)

원래 있던 객체에 fit() 메소드를 이용해 랜덤으로 섞은 훈련 세트로 학습을 시킨 후

kn.score(test_input, test_target)

위 훈련이 잘 되었는지 확인하는 score() 메소드를 실행시켜보자.

1.0

100%의 정확도로 테스트 세트에 있는 모든 생선들을 맞혔다. predict() 메소드로 테스트 세트의 예측 결과와 실제 타깃을 확인해 보겠다.

kn.predict(test_input)
array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])
test_target
array([0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0])

# 코랩은 셀의 마지막 코드 결과를 자동으로 출력해 주기 때문에 print() 함수를 사용하지 않아도 된다.

 

테스트 세트에 대한 예측 결과가 정답과 일치하다. predict() 메소드의 출력 결과가 test_target의 출력과 동일하게 array() 로 감싸 있는 것을 유심히 보자. 이 값은 넘파이 배열을 의미한다. 즉 predict() 메소드가 반환하는 값은 단순한 파이썬 리스트가 아니라 넘파이 배열이다.

 

사실 사이킷런 모델의 입력과 출력은 모두 넘파이 배열이다.

 

훈련 모델 평가

저번에 완벽한 모델을 만들어서 보고했었지만, 무언가 수상한 점을 발견했다. 알고리즘이 도미와 빙어를 모두 외우고 있다면 같은 데이터로 모델을 평가하는 것은 이상하다고 생각했던 것 이다.

 

모델을 훈련할 때 사용한 데이터로 모델의 성능을 평가하는 것은 정답을 미리 알려주고 문제를 푸는 꼴이니 말이다. 공정하게 점수를 매기기 위해선 훈련할 때의 데이터와 테스트 할 때의 데이터가 다르게 진행 되어야 한다.

 

따라서 훈련 데이터훈련 세트테스트 세트로 나누었다. 훈련 세트로는 모델을 훈련 시키고, 테스트 세트는 훈련 시킨 모델을 테스트 시켰다. 하지만 테스트 세트를 그냥 무작성 나누어서는 안된다. 도미와 빙어를 구분하는게 목적이기에 샘플링 편향이 이루어지지 않게 랜덤으로 나누어 훈련 세트와 테스트 세트에 뿌려야 한다.

 

도미와 빙어를 골고루 섞어 나누기 위해 파이썬의 다차원 배열 라이브러리인 넘파이를 사용해 보았다.

넘파이는 파이썬의 리스트와 비슷하지만 고차원의 큰 배열을 효과적으로 다룰 수 있고, 다양한 도구를 많이 제공한다.

그래서 우리는 넘파이의 shuffle() 함수를 사용해 배열의 인덱스를 섞었다.

 

결과는 성공. 테스트 세트에서 100%의 정확도를 달성했다.

728x90

댓글

IT_learning's Commit