02-2 데이터 전처리
앞에서 사용했던 도미와 빙어 데이터를 보면 도미는 길이와 무게가 큰 편이고 빙어는 작은 편이었다.
만약, 빙어처럼 작은 도미가 있거나 도미처럼 큰 빙어가 있다면 ?!
넘파이로 데이터 준비하기
먼저 앞에서 했던 데이터와는 다른 데이터를 준비하자.
노트북에서 새로운 셀을 만들어서 입력
1 | |
넘파이를 import 하고 넘파이의 column_stack() 함수를 이용해 fish_length와 fish_weight를 합치자.
column_stack() 함수는 전달받은 리스트를 일렬로 세운 다음 차례대로 나란히 연결해준다.
연결할 리스트는 튜플로 전달한다.
1 | |
리스트가 잘 연결됐는지 처음 3개의 데이터를 확인해보면
column_stack(A, B)의 A는 첫 번째 열로, B는 두 번째 열로 들어간 것을 알 수 있다.
동일하게 타깃 데이터도 만든다.
이전에는 0과 1을 갯수만큼 곱해서 입력해줬는데 이번엔 더 나은 방법인 ones()와 zeros() 함수를 이용해 각각 원하는 개수의 1과 0을 채운 배열을 만들어서 입력해보자.
예를 들면 np.ones(5)는
1 | |
1이 5개인 배열이 만들어진다.
같은 방식으로 zeros(N)는 0이 N개인 배열이 만들어지는 것이다.
이 두 함수를 이용해 1이 35개인 배열과 0이 14개인 배열을 만들 수 있고 concatenate()함수를 사용해 두 배열을 연결해보자.
1 | |
- column_stack()과 concatenate()의 차이점 ?
column_stack() 함수는 각 리스트를 열에 하나씩 넣고 concatenate() 함수는 앞 리스트 뒤에 뒷 리스트를 붙인다.
예를 들면 이렇게 표현할 수 있다. 👇
이제 훈련 세트와 테스트 세트를 나눠보자.
사이킷런으로 훈련 세트와 테스트 세트 나누기
훈련 세트와 테스트 세트를 비율에 맞게 나누기 위해 사이킷런의 train_test_split()함수를 사용한다.
이 함수에 나누고 싶은 리스트나 배열을 원하는 만큼 전달하면 된다.
사용하기에 앞서 train_test_split() 함수는 사이킷런의 model_selection 모듈 아래에 있어서 import 해준다.
1 | |
train_test_split()함수로 fish_data와 fish_target을 train_input, test_input, train_target, test_target으로 나눠야한다.
앞서 했던 random.seed()함수를 사용해 출력 결과와 책의 결과와 동일하게 해준 것 처럼 train_test_split()함수에는 자체적으로 랜덤 시드를 지정할 수 있는 random_state 매개변수가 존재한다.
앞에서 랜덤시드를 42로 설정한 것과 동일하게 random_state 매개변수를 42로 설정한다.
1 | |
이 함수는 기본적으로 25%를 테스트 세트로 떼어낸다.
잘 나누었는지 확인하기 위해 넘파이 배열의 shape 속성으로 입력 데이터의 크기를 출력해보자.
1 | |
출력 결과를 통해 훈련 데이터와 테스트 데이터가 각각 36, 13개로 나누어진 것을 확인할 수 있다.
튜플의 원소가 하나일 경우 원소 뒤에 콤마(,)를 추가한다.
입력 데이터(_input)의 ( , 2)는 2개의 열이 있는 2차원 배열을 의미하고, 타깃 데이터(_target)의 ( ,)는 1차원 배열을 의미한다.
테스트 데이터를 출력해 도미와 빙어가 잘 섞였는지 출력해보자.
1 | |
총 13개의 테스트 세트 중에 10개는 도미(1), 3개는 빙어(0)다.
그럴듯하게 섞여있지만 첫 번째 테스트 세트인 ‘1’ 이 영 찝찝하다.
원래 도미와 빙어의 개수는 35 : 14 이므로 두 생선의 비율은 2.5 : 1 인데 이 테스트 세트는 10 : 3 으로 비율이 3.3 : 1 이므로 비율이 맞지 않다.
즉, 샘플링 편향이 조금이지만 나타난 것이다.
이렇게 골고루 섞이지 않은 이유는 샘플 개수가 적어서 이런 일이 생긴 것이다.
다시 원래 비율대로 맞추기 위해서 train_test_split() 함수에 stratify 매개변수에 타깃 데이터를 전달해 비율에 맞게 데이터를 나눈다.
1 | |
아까 있던 찝찝했던 ‘1’은 사라지고 9개의 도미(1), 4개의 빙어(0)가 출력되고 비율은 2.25 : 1로 아까보단 2.5 : 1 과 근접한 비율이 되었다!
수상한 도미 한 마리
다시 k-최근접 이웃 훈련을 해보자.
1 | |
1.0이 나와서 정말 기분이 좋다.
테스트 세트의 도미와 빙어를 올바르게 분류했다는 걸 보여준다.
이제 다시 무게가 25이고 길이가 150인 도미의 데이터를 넣고 결과를 확인해보자.
1 | |
빙어라고 나왔다.. 내 세상이 무너졌다.
이 샘플과 다른 데이터를 함께 산점도로 그려 확인해보자.
1 | |
🔺가 25, 150인 데이터인데 그래프상 오른쪽 위로 뻗어 있는 도미 데이터에 더 가까운데 왜 왼쪽 아래의 빙어 데이터와 가깝다고 나오는 걸까?
k-최근접 이웃은 주변의 샘플 중에서 다수인 클래스를 예측으로 사용한다.
이 샘플의 주변 샘플을 알아보자.
KNeighborsClassifier 클래스는 주어진 샘플에서 가장 가까운 이웃을 찾아 주는 kneighbors() 메서드를 제공하는데 이 메서드는 이웃까지의 거리와 이웃 샘플의 인덱스를 반환한다.
KNeighborsClassifier 클래스의 이웃 개수인 n_neighbors의 기본값은 5이므로 5개의 이웃이 반환된다.
1 | |
kn.kneighbors([[25, 150]])을 출력한 결과를 통해 알 수 있듯이, [[25, 150]]과 가장 가까운 5개 이웃까지의 거리 배열과 5개 이웃 샘플의 인덱스가 반환됐다.
구한 indexes 배열을 사용해 훈련 데이터 중에서 이웃 샘플을 따로 구분해 그려보자.
1 | |
marker = ‘^’는 세모로, marker = ‘D’는 마름모로 표시된다.
초록색 🔷 5개가 🔺에 가장 가까운 샘플로 표시됐다.
🔷 5개 중 4개는 왼쪽 아래(빙어), 1개만 오른쪽 위(도미)에 포함되어 있으므로 빙어라고 예측한 것이다.
데이터를 확인해보자.
1 | |
타깃 데이터로도 확인해보자.
1 | |
빙어(0)가 압도적이다..
이렇게 본다면 이 샘플의 클래스를 빙어로 예측하는 것은 당연해 보인다.
산점도를 보면 도미와 더 가깝게 보이는데 왜 가장 가까운 이웃을 빙어라고 생각하는 걸까?
기준을 맞춰라
아까 kn.kneighbors()에서 구했던 distances 배열을 출력해보자.
1 | |
거리가 짧은 순으로 나열되어 있는데
92랑 130이 차이가 이렇게 나는게 맞나 ?
좀 이상하긴 하지만 산점도를 자세히 보면 x축은 칸 간격이 5씩이지만 y축은 200씩이다.
그래서 수직으로 조금만 떨어져 있어도 큰 값으로 계산이 될테니 수평적으로 가까운 도미 샘플보단
수평적으로 멀지만 수직적으로 가까운 빙어 샘플들이 이웃으로 선택된 것이다.
그렇다면 x축과 y축 간격을 맞춰서 다시 산점도를 그려보자 !
matplotlib에서 xlim()함수를 사용해 x축 범위를 지정해준다.
x + lim(it). 즉 경곗값을 지정해주는 함수. y축은 ylim()으로 하면 된다.
1 | |
x축과 y축의 범위를 동일하게 맞췄더니 산점도가 거의 일직선으로 나타났다.
이런 데이터들이면 x값(길이)보단 y값(무게)에만 영향을 받아서 오로지 y값(무게)만 고려 대상이 된다.
두 특성의 값이 놓인 범위가 매우 다른데 이것을 두 특성의 스케일이 다르다고도 말한다.
여기에서만 봐도 무게는 g 단위고 크기는 m 단위라 이런 현상이 생긴다.
데이터를 표현하는 기준이 다르면 알고리즘이 올바르게 예측할 수 없다.
특히 알고리즘이 거리 기반일 경우 그런데 우리가 사용한 k-최근접 이웃 알고리즘이 여기에 해당된다.
그렇기 때문에 특성값들을 일정한 기준으로 맞춰 줘야 하는데 이런 작업을 데이터 전처리(data preprocessing)이라고 한다.
가장 널리 사용하는 전처리 방법 중 하나는 표준점수(standard score) / z 점수이다.
표준점수는 각 특성값이 0에서 표준편차의 몇 배만큼 떨어져 있는지를 나타낸다.
이를 통해 실제 특성값의 크기와 상관없이 동일한 조건으로 비교할 수 있다.
계산하는 방법은 평균을 빼고 표준편차를 나누어 주는 것이다.
넘파이에서 제공하는 함수를 이용하자.
1 | |
mean()함수는 평균을 계산하고, std()함수는 표준편차를 계산한다.
train_input은 36개의 길이와 무게의 특성이 있는 배열이므로 각각의 평균과 표준편차가 계산이 된다.
이제 원본 데이터에서 평균을 빼고 표준편차로 나누어 표준점수로 변환하자.
1 | |
train_input의 모든 행에서 mean에 있는 두 평균값을 빼주고 std에 있는 두 표준편차로 모든 행을 나눈다.
이런 넘파이 기능을 브로드캐스팅(broadcasting)이라고 부른다.
참고로 브로드캐스팅은 넘파이 배열 사이에서 일어난다.
전처리 데이터로 모델 훈련하기
앞에서 표준점수로 변환한 train_scaled를 만들었으니 다시 산점도를 그려보자.
1 | |
어 음 샘플이 안녕히계세요 여러분을 시전해버렸다..
왜냐면 저 친구 혼자만 평균으로 빼고 표준편차로 나누지 않았기 때문이다.
샘플까지 변환해주고 다시 산점도를 그려보자.
1 | |
앞에서 평균과 표준편차로 변환하기 전의 산점도와 거의 비슷하다.
눈에 띄게 달라진 점은 x축과 y축이 범위가 -1.5 ~ 1.5로 바뀐 것이다.
두 특성이 비슷한 범위를 차지하고 있으니 이 데이터셋으로 k-최근접 이웃 모델을 다시 훈련해보자.
1 | |
금방 샘플로도 확인했듯이 평균과 표준편차로 값을 변환해주지 않으면 아까와 같은 상황(덩그러니 남겨진)이 반복될 것이다.
그렇기 때문에 테스트 세트 또한 변환해주자.
1 | |
잘 분류가 되었다.
이제 샘플이 잘 분류가 되는지 확인해보자.
1 | |
와! 드디어 도미(1)로 예측을 한다!!
마지막으로 kneighbors() 함수로 이 샘플의 k-최근접 이웃을 구한 다음 산점도로 그려보자.
가까운 이웃들이 도미로 나오는지 산점도로 확인해보자!
1 | |
가장 가까운 이웃인 초록색 🔷 5개가 오른쪽 위(도미)에서 나타났다 !
특성값의 스케일이 다르지만 안정적으로 예측할 수 있는 모델을 만드는 데에 성공했다 !!