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

+ Recent posts