Stay Hungry Stay Foolish

혼공/머신러닝

2장 데이터 다루기

dev스카이 2022. 7. 22. 14:58

머신러닝 알고리즘은 크게 지도 학습(supervisid learning)과 비지도 학습(unsupervised learning)으로 나뉜다.

지도 학습 알고리즘이란?
- 정답(타깃)이 있으니 알고리즘이 정답을 맞히는 것을 학습한다.
- 훈련하기 위한 데이터와 정답이 필요하다.

비지도 학습 알고리즘이란? (6장에서 다룸)
- 타깃 없이 입력 데이터만 사용한다.
- 따라서 무언가를 맞힐 순 없지만 데이터를 잘 파악하거나 변형하는 데 도움을 준다.

지도 학습에서는 데이터를 입력(input), 정답을 타깃(target)이라고 하고, 이 둘을 합쳐 훈련 데이터(training data)라고 부른다. 머신러닝 알고리즘의 성능을 제대로 평가하려면 훈련 데이터와 평가에 사용할 데이터가 각각 달라야 한다. 가장 간단한 방법은 또 다른 데이터를 준비하거나 이미 준비된 데이터 중에서 일부를 떼어 내어 활용하는 것이다. 일반적으로 후자의 경우가 많다.

알고리즘의 정확한 평가를 위해 테스트 세트와 훈련 세트가 따로 준비되어야 한다.
- Test Set : 평가에 사용하는 데이터
- Train Set : 훈련에 사용되는 데이터


구글 코랩으로 머신러닝 실습하기

1장에 이어서 도미와 빙어를 이용해 실습하기


1. 생선 데이터 준비

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

이때 하나의 생선 데이터를 Sample이라고 부른다. 도미 35마리와 빙어 14마리를 합해서 총 49개의 샘플이 데이터에 들어있다. 여기서 35개를 훈련 세트로, 14개를 테스트 세트로 사용한다.
zip() : 나열된 리스트 각각에서 원소를 하나씩 꺼내 반환하는 함수다.

 

2. 훈련 세트와 테스트 세트

k-최근접 이웃 알고리즘을 구현할 클래스를 임포트하고, 객체를 만든다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()

전체 데이터에서 35개를 훈련 세트로, 14개를 테스트 세트로 나눠야 한다.
나누기 위해서는 index를 사용한다. -> 예시 : fish_data[4] - 3번째 샘플이 출력된다.
인덱스 외에도 slicing이라는 연산자를 사용할 수 있다. 인덱스와의 차이는 범위를 지정할 수 있다는 것이다.
슬라이싱을 사용하는 방법은 [인덱스:인덱스] -> fish_data[0:5] - 0번째~4번째 인덱스까지 5개의 샘플이 출력된다.
슬라이싱은 마지막 인덱스의 원소는 포함되지 않는다는 것에 주의한다. 처음과 마지막 인덱스는 생략해도 된다.

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

슬라이싱을 이용해 훈련 세트 35개와 테스트 세트 14개로 나눴다.

원하는 대로 나눴으니 모델을 훈련하고 평가해보자.

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

▣ fit() : 모델에 데이터를 전달해 규칙을 학습하는 과정을 뜻하는 훈련 메소드이다. 즉, 주어진 데이터로 알고리즘을 훈련함.
▣ score() : 모델을 평가하는 메서드로, 0~1 사이의 값을 반환한다. 1은 모든 데이터를 정확히 맞혔다는 것을 나타낸다.

그러나 결과는 0.0으로 정확도가 0%이다. 어떻게 된 것일까?
앞에서 훈련 세트에 도미만 포함시켰으므로 빙어 없이 모델을 훈련하면 당연하게 빙어를 올바르게 분류할 수가 없다. 따라서 골고루 섞이게 만들어야 한다. 이처럼 훈련 세트와 테스트 세트에 샘플이 골고루 섞여 있지 않고 샘플링이 한 쪽으로 치우쳤다는 것을 Sampling Bias, 샘플링 편향이라고 부른다.

이를 해결하기 위해 데이터를 섞든지 아니면 골고루 샘플을 뽑아서 훈련 세트와 데스트 세트를 만들어야 한다. 이런 작업을 간편하게 처리하기 위해 numpy 라이브러리를 사용한다.
넘파이 : 파이썬의 대표적인 배열 라이브러리로, 고차원의 배열을 손쉽게 만들고 조작할 수 있는 간편한 도구를 제공한다.

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

array() : 넘파이 함수에 파이썬 리스트를 전달한다. 즉, 리스트를 배열로 변환한다.
shape 속성 : 배열의 크기를 알려준다. 이 명령을 사용하면 (샘플 수, 특성 수)를 출력한다. 위의 결과는 (49, 2)이다.

이제 데이터를 섞자!
주의할 점은 입력 데이터와 타깃 데이터가 같이 선택되어야 한다는 것이다. 방법은 인덱스를 섞은 다음 입력과 타깃 배열에서 샘플을 선택하면 무작위로 훈련 세트를 나누는 셈이 된다.

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

random.seed() : 무작위 결과를 만드는 함수들은 실행할 때마다 다른 결과를 만든다. 일정한 결과를 얻으려면 초기에 랜덤시드를 지정한다.
arange() : 함수에 정수 N을 전달하면 0에서부터 N-1까지 1씩 증가하는 배열을 만든다.
shuffle() : 주어진 배열을 무작위로 섞는다.

index 결과

위의 인덱스를 사용해 훈련 세트와 테스트 세트로 나눠보자!
넘파이는 슬라이싱 외에 Array Indexing이라는 기능을 제공한다. 1개의 인덱스가 아닌 여러 개의 인덱스를 한 번에 여러 개의 원소를 선택할 수 있다.

print(input_arr[[1,3]]) #2번째와 4번째 샘플을 출력
train_input = input_arr[index[:35]] #처음부터 35개를 선택
train_target = target_arr[index[:35]] #처음부터 35개를 선택

랜덤하게 섞은 인덱스에서 35개의 샘플을 선택해 훈련 세트로 만들었다.

print(input_arr[13], train_input[0])

train_input의 첫 번째 원소는 input_arr의 열 네 번째 원소가 들어 있다.

나머지 테스트 세트도 만들어보자!

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

이제 모든 데이터가 준비됐다. 그럼 산점도로 확인해보자.

3. 산점도로 확인

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차원 배열은 행과 열 인덱스를 콤마로 나누어 지정한다.

파란색은 훈련 세트, 주황색은 테스트 세트이다. 의도한 대로 잘 섞인 결과를 볼 수 있다.

4. k-Nearest Neighbors 모델을 훈련시켜 평가하기

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

결과는 1.0이다. 테스트 세트에 있는 모든 생성을 맞혔다는 뜻이다.

그럼 이제 테스트 세트의 예측 결과와 실제 타깃을 확인해보자!

kn.predict(test_input)
test_target #구글 코랩은 마지막 코드 결과를 자동으로 출력해줘서 print()함수를 사용 안 함

predict() : 새로운 데이터의 정답을 예측하는 메서드이다. 2차원 리스트를 전달해야 한다.

test_input과 test_target의 결과

테스트 세트에 대한 예측 결과가 정답과 일치한 것을 볼 수 있다.


문제점
그러나 위의 모델에서 길이가 25cm, 무게가 150g인 도미를 넣어보면 빙어라고 예측한다. 모델을 다시 확인해보자.

 

1. 도미와 빙어 데이터 준비하기

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개의 리스트로 구성된 생선 데이터를 배열로 만든다. 연결할 2개의 리스트는 파이썬 튜플로 전달한다.
※ 파이썬 튜플은 리스트와 매우 비슷한데, 차이점은 한 번 만들어진 튜플은 수정할 수 없다.

fish_data = np.column_stack((fish_length, fish_weight))
print(fish_data[:5]) #2개의 리스트가 잘 연결되었는지 5개의 데이터를 확인

column_stack() : 전달받은 리스트를 일렬로 세운 후 차례대로 나란히 연결하는 넘파이 함수이다.

fish_data 배열 결과

동일한 방법으로 타깃 데이터도 만든다. 위에서는 여러 번 곱해서 만들었지만, 넘파이는 더 간단한 방법이 있다.
※ 데이터가 클수록 파이썬 리스트보다는 넘파이 배열을 사용하는 게 효율적이다.

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

▣ ones(), zeros() : 각각 원하는 개수의 1과 0을 채운 배열을 만든다.
▣ concatenate() : 첫 번째 차원을 따라 배열을 연결하는 함수다.
※ concatenate() vs column_stack() : 배열이 생성될 때 차원의 차이가 있다.

fish_target 결과

 

2. 훈련 세트와 테스트 세트 나누기

위에서는 넘파이 배열의 인덱스를 직접 섞어서 나누었지만, 사이킷런에서 더 간단한 방법이 제공된다. 알고리즘뿐만 아니라 다양한 유틸리티 도구를 제공해준다.

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

 

▣ shape : 넘파이 배열의 속성으로 입력 데이터의 크기를 출력한다.

▣ train_test_split() : 전달되는 리스트나 배열을 비율에 맞게 훈련 세트와 테스트 세트로 나누어 주는 함수다. 나누기 전엔 알아서 섞어 준다. 나누고 싶은 리스트나 배열을 원하는 만큼 전달하면 된다. 이 함수 내에 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 있다. 기본적으로 25%를 테스트 세트로 떼어 낸다.

결과

도미와 빙어가 잘 섞였는지 확인해보자.

print(test_target)

test_target 결과

13개의 테스트 세트 중 10개 도미, 3개가 빙어다. 그러나 원래 두 생선의 비율이 2.5:1인데, 위의 결과는 3.3:1로 빙어의 비율이 모자라다. 샘플링 편향이 나타났다. 샘플링 편향이 일어나면 모델이 일부 샘플을 올바르게 학습할 수 없는 문제점이 생긴다. 이를 해결하기 위해서는 클래스 비율에 맞게 데이터를 나누도록 해야 한다.

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

stratify : 클래스 비율에 맞게 데이터를 나누는 매개변수이다. 훈련 데이터가 작거나 특정 클래스의 샘플 개수가 적을 때 유용하다.

test_target 결과

그 결과 테스트 세트의 비율이 2.25:1이 됐다. 비율을 동일하게 맞출 순 없지만 근접한 비율이 됐다.

 

 

3. k-Nearest Neighbors 모델을 훈련시켜 평가하기

※ k-최근접 이웃은 훈련 데이터를 저장하는 것이 훈련의 전부다.

from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

결과는 1.0으로, 테스트 세트의 도미와 빙어를 완벽하게 분류했다.

이제 처음에 문제가 생겼던 새로운 도미 데이터를 넣고 결과를 확인해보자.

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

그러나 결과는 [0.]이 출력됐다. 원래는 1이 나와야 정상이다.

그렇다면 어디서 문제가 생긴건지 산점도로 확인해보자.

import matplotlib.pyplot as plt
distances, indexes = kn.kneighbors([[25,150]])

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()
▣ kneighbors() : 주어진 샘플에서 가장 가까운 이웃을 찾아 주는 메서드로, KNeighborsClassifier 클래스에서 제공한다. 클래스의 이웃 개수는 기본적으로 5개가 주어진다.

초록색 마름모는 삼각형 샘플에 가장 가까운 5개의 샘플이다. 직관적으로 봤을 때 1개는 도미의 데이터와 더 가깝다. 그러나 나머지 샘플 4개는 모두 빙어로 표시된다. 확실하게 알기 위해 거리를 확인해보자.

print(distances)

▣ distances : 이웃 샘플까지의 거리가 담겨 있는 배열이다. 위에서 kneighbors()메서드에 반환했다.

결과

결과를 보면 첫 번째 샘플까지의 거리가 92이고, 나머지 4개는 그것보다 더 거리가 멀어야 직관적으로 봤을 때 정상이다. 그러나 그렇지 않다. 거리 비율이 이상한 걸 볼 수 있다. 이유는 무게와 길이 비율이 처음부터 일치하지 않기 때문이다. x축은 범위가 좁고, 그에 비해 y축은 범위가 아주 넓다.

그럼 비율을 동일하게 맞추자.

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()

▣ xlim(), ylim() : x축과 y축 비율을 지정할 수 있는 함수다.

그 결과, 산점도가 수직선으로 나타났다. 이처럼 두 특성(길이,무게)의 값이 놓인 범위가 다른데, 이를 두 특성의 스케일(scale)이 다르다고도 말한다. 따라서 특성값을 일정한 기준으로 맞춰 줘야 하는데 이를 데이터 전처리(data processing)라고 한다.

가장 널리 사용하는 전처리 방법 중 하나는 표준점수이다.(z 점수라고도 부른다.) 표준점수는 각 특성값이 평균에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다. 계산하는 방법은 평균을 빼고 표준편차를 나눈다.

더보기

⊙ 분산은 데이터에서 평균을 뺀 값을 모두 제곱한 다음 평균을 내어 구한다.

표준편차는 분산의 제곱근으로 데이터가 분산된 정도를 나타낸다.

표준점수는 각 데이터가 원점에서 몇 표준편차만큼 떨어져 있는지를 나타낸다.

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

▣ mean() : 평균을 계산하는 함수로, 넘파이에서 제공한다.
▣ std() : 표준편차를 계산하는 함수로, 넘파이에서 제공한다.
특성마다 값의 스케일이 다르므로 평균과 표준편차는 각 특성별로 계산해야 한다. 이를 위해 axis = 0으로 지정한다. 그러면 행을 따라 각 열의 통계 값을 계산하게 된다.

각 특성마다 평균은 [ 27.29722222 454.09722222]이고, 표준편차는 [ 9.98244253 323.29893931]이다.

이제 원본 데이터에서 평균을 빼고 표준편차로 나눠 표준점수로 변환해보자.

train_scaled = (train_input - mean) / std



넘파이는 원본 데이터의 모든 행에서 두 평균값을 빼주고, 두 표준편차를 다시 모든 행에 적용한다. 이런 기능을 브로드캐스팅이라고 한다.
broadcasting : 조건을 만족하면 모양이 다른 배열 간의 연산을 가능하게 해 주는 기능이다.

 

4. 전처리 데이터로 모델 훈련하기

표준점수로 변환했으니 샘플을 다시 산점도로 그려보자!

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()

※ new = ([25,150]- mean)/std 코드를 추가한 이유는 x축과 y축 범위를 동일하게 맞추기 위해서이다.

 
산점도 결과를 보면 훈련 데이터의 두 특성이 비슷한 범위를 차지하고 있다. 
 
그럼 이제 모델을 훈련한 후 평가해보자.
kn.fit(train_scaled, train_target)
test_scaled = (test_input - mean)/std
kn.score(test_scaled, test_target)
print(kn.predict([new]))

모델의 평가 결과는 1이고, 도미 데이터를 넣고 테스트한 결과 1이 도출됐다.

마지막으로 산점도를 그려보자.

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()

그 결과, 도미 샘플과 가장 가까운 5개 샘플이 모두 도미이다. 이처럼 스케일이 다른 특성을 잘 처리해야 올바른 값을 도출할 수 있다.

'혼공 > 머신러닝' 카테고리의 다른 글

3장 회귀 알고리즘과 모델 규제  (0) 2022.08.13
1장 머신러닝이란?  (0) 2022.07.08