Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Archives
Today
Total
관리 메뉴

cb

[혼공머신] Chapter 02 - 데이터 다루기 본문

ai - study

[혼공머신] Chapter 02 - 데이터 다루기

10011001101 2024. 1. 31. 16:37

본 게시물은 <혼자서 공부하는 머신러닝+딥러닝>의 Chapter 02: 데이터 다루기를 보고 정리한 글입니다. 원본 코드는 책의 저자인 박해선님의 깃허브 코드를 참고하시길 바랍니다.

 

GitHub - rickiepark/hg-mldl: <혼자 공부하는 머신러닝+딥러닝>의 코드 저장소입니다.

<혼자 공부하는 머신러닝+딥러닝>의 코드 저장소입니다. Contribute to rickiepark/hg-mldl development by creating an account on GitHub.

github.com


 

지도 학습과 비지도 학습이란?

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

 

지도학습은 데이터와 정답을 모두 제공하여 알고리즘이 정답을 맞히는 것을 학습한다. 여기서 데이터와 정답은 입력(input)타깃(target)이라고 칭하며, 둘을 합쳐 훈련 데이터(training data)라고 한다.

 

챕터 1에서 봤던 것과 같이, 생선의 길이 특성과 무게 특성을 입력이라 하고, 0과 1의 정답을 타깃이라고 한다.

 

지도학습이 입력과 타깃의 훈련 데이터가 필요한 반면, 비지도 학습은 타깃 없이 입력 데이터만 주어진다. 비지도 학습은 정답이 없기 때문에 무언가를 맞힐 수 없으므로 데이터를 파악하거나 변형하는 데에 도움을 준다. (비지도 학습에 대해서는 6장에서 좀 더 자세히 다룬다.)

 


 

훈련 세트와 테스트 세트

머신러닝의 정확한 평가를 위해서는 테스트 세트와 훈련 세트를 따로 준비해야 한다. 모델을 훈련한 데이터와 같은 데이터로 평가를 하게 되면, 모델은 당연히 모든 데이터를 제대로 분류해낼 것이다. 

 

이러한 문제는 크게 두 가지 방법으로 예방이 가능하다. 하나는 또 다른 데이터를 마련하는 방법, 하나는 이미 준비된 데이터에서 일부를 떼어내는 방법이 있다. 여기서 훈련에 사용할 데이터를 훈련 세트(training set), 평가에 사용할 데이터를 테스트 세트(test 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]

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

 

여기서, 하나의 생선 데이터를 샘플(sample)이라고 부른다.

 

마찬가지로 저번 챕터에서 사용한 KNeighborsClassifier 모델을 사용해 보자.

# 라이브러리 임포트
from sklearn.neighbors import KNeighborsClassifier

# 객체 생성
kn = KNeighborsClassifier()

 

모델의 객체까지 생성해 주었다면, 본격적으로 데이터를 분할해 보자,

 

이는 파이썬의 슬라이싱 연산자를 통해 쉽게 구현이 가능하다. 훈련 세트는 데이터의 앞 35개, 테스트 세트는 나머지로 구성해 주었다. 슬라이싱 연산은 인덱스의 범위를 지정하여 한 번에 여러 개의 원소를 선택할 수 있어 편리하다는 장점이 있다.

# 훈련 세트 구성
train_input = fish_data[:35]
train_target = fish_target[:35]

# 테스트 세트 구성
test_input = fish_data[35:]
test_target = fish_target[35:]

 

이렇게 나누어 준 데이터를 활용해 모델을 훈련시켜 보자.

# 훈련 및 평가
kn.fit(train_input, train_target)
kn.score(test_input, test_target)

 

어? 지난 챕터와 동일한 모델로 학습을 진행했음에도 불구하고 정확도가 0인 것을 확인할 수 있다. (위의 코드를 실행해 보면 0이라는 값이 반환됨)

왜 이런 문제가 발생할까?

 


 

샘플링 편향

이는 데이터 샘플이 골고루 섞여 있지 않아 샘플링이 한쪽으로 치우쳐진 샘플링 편향(sampling bias)에 의해 일어난 문제이다.

 

우리가 원래 가지고 있던 데이터는 총 49개의 샘플로 구성되어 있다. 그 중 35개는 도미, 14개는 빙어 샘플이다. 하지만 위에서 훈련 세트와 테스트 세트를 나눌 때 35개의 도미 데이터를 훈련 세트로 구성하였고, 나머지 14개의 방어 샘플을 테스트 세트로 구성해버리고 말았다. 이 데이터를 사용하여 학습한 모델은 도미 데이터에 대해서만 훈련되었기 때문에, 어떤 데이터가 들어와도 데이터를 도미라고만 판단할 수밖에 없다.

 

특정 종류의 샘플이 과도하게 많은 샘플링 편향을 가지고 있으면, 제대로 된 지도 학습 모델을 만들 수 없다. 따라서 이를 해결하기 위해, 파이썬의 배열 라이브러리인 넘파이(numpy)를 사용하여 이전과 같은 문제를 해결한다.

 


 

넘파이(numpy)

넘파이는 고차원의 배열을 손쉽게 만들고 조작할 수 있는 도구를 많이 제공한다. 데이터를 쉽게 다루기 위해 먼저 파이썬 리스트를 넘파이 배열로 바꿔 주어야 한다. 이는 array()함수를 사용하여 손쉽게 구현이 가능하다.

# 라이브러리 임포트
import numpy as np

# 넘파이 배열로 변경
input_arr = np.array(fish_data)
target_arr = np.array(fish_target)

# 변경된 데이터 확인
print(input_arr)

 

코드의 마지막 줄은 데이터가 잘 변경되었는지 확인하기 위해 추가한 코드이다.

 

하지만 여기서 데이터의 특성이 많아지거나, 샘플의 수가 많아진다면 우리는 육안으로 데이터의 변경 사항을 한 눈에 확인하기 어렵다. 그렇기 때문에 넘파이에서는 배열의 차원을 확인할 수 있는 shape 속성을 제공해 준다.

 

파이썬 리스트에서 변경된 넘파이 배열은 각 데이터 하나하나를 행으로 가지기 때문에, 배열의 행은 곧 샘플 수를 나타낼 수 있다. 열은 샘플의 특성들에 해당하게 되므로, shape 속성을 출력하면 총 49개의 행, 2개의 열을 가지는 것을 볼 수 있을 것이다.

# 샘플 수, 특성 수 확인
print(input_arr.shape)

 

 

생선 데이터를 넘파이 배열로 준비했다면, 배열에서 랜덤하게 샘플을 선택해 훈련 세트와 테스트 세트로 데이터를 나눠 주어야 한다. 여기서, input_arr의 샘플과 targer_arr의 샘플은 같은 위치끼리 함께 선택해야 한다는 점 주의하자. (인풋 값과 정답 값은 서로 짝을 이루기 때문에, 셔플 과정에서 인풋 값에 해당하지 않는 정답이 짝지어지게 된다면 잘못된 데이터를 만드는 것과 동일하기 때문이다.)

 

먼저, 랜덤하게 데이터를 섞기 위해 랜덤 시드를 생성해 준다. 그런 다음 넘파이의 arrange() 함수를 사용하여 인덱스 리스트를 만들어 준다. arrange 함수는 매개변수로 들어오는 수 n이 있을 때, 0에서부터 n-1까지 1씩 증가하는 인덱스를 생성해 주는 함수이다. 여기서는 총 49개의 데이터 샘플을 무작위로 선택해 줄 것이기 때문에, 매개변수를 49로 설정해 주었다.

 

이후 shuffle() 함수를 사용하여 인덱스 배열을 섞으면 0부터 48까지의 숫자가 랜덤하게 나열되게 된다.

# 랜덤 시드 생성 및 인덱스 셔플링
np.random.seed(42)
index = np.arange(49)
np.random.shuffle(index)

 

 

넘파이는 배열 인덱싱(array indexing)이라는 기능도 제공한다. 이는 1개의 인덱스가 아닌 여러 개의 인덱스로 한 번에 여러 개의 원소를 선택할 수 있는 기능이다.

 

또, 넘파이 배열을 인덱스로 전달할 수도 있다. 이를 활용하여 랜덤하게 섞인 인덱스 값에 해당하는 번째의 데이터를 훈련 세트, 테스트 세트로 분할해 준다. 아래 코드에서는 0에서부터 34번째 인덱스 값번째에 있는 데이터를 훈련 세트로 구성하였고, 나머지 데이터를 테스트 세트로 구성해 주었다.

# 배열 인덱싱을 통한 여러 원소 선택
print(input_arr[[1,3]])

# 0~34번째 인덱스에 해당하는 데이터를 훈련 세트로 구성
train_input = input_arr[index[:35]]
train_target = target_arr[index[:35]]

# 나머지 데이터를 테스트 세트로 구성
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()

 

이 코드를 실행시키면 파란색 점과 주황색 점이 골고루 섞여 있는 것을 확인할 수 있다. 여기서 파란색 점은 훈련 세트를 의미하고, 주황색 점은 테스트 세트를 의미한다.


두 번째 머신러닝 프로그램

훈련 세트와 테스트 세트가 모두 준비되었으니 챕터 1에서 사용하였던 k-최근접 이웃 모델을 활용해 머신러닝 모델을 훈련해 보자. 아래 코드는 저번 챕터에서 사용한 코드와 동일한 코드이다.

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

 

이를 실행시켜 보면, 100% 정확도로 모든 생선을 맞히는 것을 확인할 수 있다!

 

prerdict() 메소드를 사용하여 예측 결과와 실제 타킷이 일치하는지 확인해 보자. 이는 단순히 결과값을 출력하여 같은 값을 가지는지 육안으로 확인하면 된다. (데이터의 사이즈가 작기 때문에 가능)

kn.predict(test_input)
test_target

 

다행히 모든 예측값이 타겟 데이터와 일치하는 것을 볼 수 있다. 

 


데이터 전처리

앞서 우리는 파이썬 배열을 이용하여 훈련 세트와 테스트 세트를 나눠 보았다. 하지만 이 방법은 아주 번거롭다. 따라서 사이킷런에서는 train_test_split()이라는 함수를 제공한다. 이는 데이터를 전달하였을 때 비율에 맞게 훈련 세트와 테스트 세트를 나눠 주는 역할을 한다. 나누기 전에 알아서 데이터를 섞어 주므로 아주 편리하게 데이터를 전처리할 수 있다.

 

앞에서 사용한 생선 데이터를 그대로 이용해 보겠다.

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]


import numpy as np

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

fish_data = np.column_stack((fish_length, fish_weight))

print(fish_data[:5])

print(np.ones(5))

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

print(fish_target)

 

먼저, train_test_split()함수는 model_selection 모듈 아래에 있기 때문에 모듈을 임포트해 주어야 한다. 이후 나누고 싶은 배열을 원하는 만큼 전달하고, 매개변수를 지정해 주면 끝이다.

# 모듈 임포트
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)

 

이 함수는 기본적으로 데이터의 25%를 테스트 세트로 떼어내 준다.

 

하지만 테스트 데이터를 출력해 보면, 빙어의 비율이 조금 모자란 것을 확인할 수 있다. 이처럼 무작위로 데이터를 나누었을 때, 샘플이 골고루 섞이지 않는 문제가 나타날 수 있다. 이는 train_test_spllit() 함수의 stratify 매개변수를 통해 해결할 수 있다. stratify에 타겟 데이터를 전달하면, 클래스 비율에 맞게 데이터를 나눠 준다.

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

 


 

수상한 도미 한 마리

앞서 마련한 데이터로 k-최근접 이웃을 훈련해 보자. 이 과정은 1장에서 했던 것과 동일하다.

from sklearn.neighbors import KNeighborsClassifier

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

 

여기서 새로운 도미 데이터를 넣고 잘 예측하는지 살펴보기 위해, 아래의 코드를 사용해 보자.

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

 

하지만 모델은 새로운 데이터를 빙어로 분류하고 만다... 왜일까? 산점도를 통해 살펴보자.

import matplotlib.pyplot as plt

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

 

 

새로운 샘플은 marker 매개변수를 삼각형으로 나타난다. 샘플이 도미 데이터에 더 가깝게 나타났음에도 불구하고 빙어로 분류한 이유는 뭘까?

 

그는 k-최근접 이웃의 원리와 밀접한 연관이 있다. k-최근접 이웃은 주어진 샘플에서 가장 가까운 이웃 샘플 k개를 참고한다.

아래 코드를 사용해 산점도의 가까운 5개의 샘플을 확인해 보자.

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

이는 가장 가까운 5개 샘플 중 4개가 빙어에 해당하고 있음을 밝혀 준다. 산점도로만 봤을 때에는 도미와 가깝게 보이는 반면, 가까운 k개의 데이터를 확인했을 때는 빙어가 많이 존재한다.

 


 

기준을 맞춰라

산점도를 다시 살펴  보면, 첫 번째 샘플까지의 거리는 92이고 가장 가까운 샘플들은 모두 130, 138로 나타난다. 육안으로 보기에 도미가 훨씬 가까워 보이는데 왜 이런 현상이 나타날까?

 

이는 x, y축의 범위가 다르기 때문이다. x축의 범위는 (10~40)이고 y축의 범위는 (0~100)으로 나타나니 y축으로 조금만 멀어져도 거리가 아주 크게 계산돼버리는 문제가 발생한다.

 

이를 눈으로 확인하기 위해 x축의 범위를 동일하게 0~1000으로 맞춰 보자.

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

 

범위를 동일하게 맞추니 모든 데이터가 수직으로 늘어선 형태가 외었다. 두 특성의 값이 놓인 범위가 매우 다른데, 이를 두 특성의 스케일(scale)이 다르다고도 말한다. k-최근접 이웃은 샘플 간의 거리에 큰 영향을 받기 때문에 일정한 기준으로 맞춰 주는 것이 중요하다. 이 과정을 데이터 전처리(data preprocessing)라고 칭하며, 표준 점수(standard score)을 가장 널리 사용한다.

 

표준 점수를 계산하기 위해서는 평균을 빼고 표준편차를 나누어 주면 된다. 넘파이가 제공하는 함수를 통해 계산해 보자.

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

train_scaled = (train_input - mean) / std

 


 

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

전처리된 데이터를 다시 산점도로 그려 보자.

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

하지만 여기서도 그래프가 이상하게 나타나는 것을 알 수 있다. 이는 훈련 세트의 mean, std를 이용해 변환해야 하기 때문이다. 

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

이와 같이 새로운 데이터도 정규화를 해 주면 그래프가 제대로 그려지는 것을 볼 수 있다. 테스트 세트도 마찬가지로 정규화해 준 후 평가해야 한다는 점 잊지 말자.

 

이제, 테스트 세트의 스케일을 변환하고 모델을 평가해 보자.

test_scaled = (test_input - mean) / std
kn.score(test_scaled, test_target)
print(kn.predict([new]))

 

전처리된 데이터에 대해서는 우수한 저확도를 보이는 것을 알 수 있다.

 

학습한 모델의 다시 산점도를 그리게 되면, 가장 가까운 5개의 샘플을 도미에서 뽑아냈다는 사실을 기대할 수 있다. 아래의 코드를 통해 확인해 보자.

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

가까운 데이터를 잘 뽑아낸 것을 확인할 수 있다.

 


 

마무리

이번 챕터에서는 훈련된 모델을 평가하기 위해 데이터를 어떻게 분할하는지, 스케일이 다른 특성을 어떻게 처리하는지에 대해 배워 보았다. 모델을 학습시킬 때에는 데이터가 성능에 아주 큰 영향을 끼친다는 점 꼭 유의해 두자!