[ML] 혼공머닝 1편 k-Nearest Neighbors(k-최근접 이웃)
카테고리: ML
💡 ‘혼자 공부하는 머신러닝 딥러닝(박해선 저)’ 책을 읽고 공부한 내용을 요약한 페이지입니다.
책에 나오는 코드를 그대로 쓰지 않고, 코드와 파라미터를 변형하고 조정해가며 다른 결과를 출력하며 공부했습니다.
01. K-Nearest Neighbor(K-최근접 이웃)
01-1. 도미, 빙어 데이터로 K-최근접 이웃 분류하기
생선 중에 무엇이 도미이고 무엇이 빙어인지 알려주는 프로그램을 작성해보자.
# 도미 데이터 준비하기
bream_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]
bream_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]
# 빙어 데이터 준비하기
smelt_length = [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]
smelt_weight = [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 matplotlib.pyplot as plt #과학계산용 그래프 그리는 패키지 '맷플롯립'
plt.scatter(bream_length, bream_weight)
plt.xlabel('length') #x축 설정
plt.ylabel('weight') #y축 설정
plt.show()
- k-최근접 이웃(k-Nearest Neighbors) 알고리즘: 어떤 데이터에 대해 답을 구할 때 주위의 다른 데이터를 보고 다수를 차지하는 것을 정답으로 사용하는 알고리즘.
length = bream_length + smelt_length
weight = bream_weight + smelt_weight
fish_data = [[l,w] for l, w in zip(length, weight)] #리스트 내포 구문
#이러면 2차원 리스트가 만들어짐!
fish_target = [1]*35 + [0]*14 #도미는 1로, 빙어는 0으로 놓는다.
print(fish_target)
#k최근접 이웃 알고리즘을 구현한 클래스
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier() #클래스의 객체 만들어주기
kn.fit(fish_data, fish_target) #fit메서드는 training을 시켜준다.
kn.score(fish_data, fish_target) #score메서드는 모델을 평가하는 점수(정확도,accuracy)를 알려준다.
kn.predict([[30,600]]) #predict메서드는 새로운 데이터의 정답을 예측한다.
#가까운 몇 개의 데이터를 사용할지는 n_neighbors 매개변수로 정함!
kn49 = KNeighborsClassifier(n_neighbors=49) #주변의 49개의 데이터를 사용한다는 의미
kn49.fit(fish_data, fish_target)
kn49.score(fish_data, fish_target)
01-1 핵심 키워드
- 특성(feature): 데이터를 표현하는 하나의 성질
- k-최근접 이웃 알고리즘: 어떤 데이터에 대해 답을 구할 때 주위의 다른 데이터를 보고 다수를 차지하는 것을 정답으로 사용하는 알고리즘. 가장 간단한 머신러닝 알고리즘 중 하나
- 정확도(accuracy) = 정확히 맞힌 개수 / 전체 데이터 개수
01-1 핵심 패키지와 함수
- matplotlib
- scatter(): 산점도를 그리는 맷플롯립 함수
- scikit-learn
KNeighborsClassifier()
: k-최근접 이웃 분류 모델을 만드는 사이킷런 클래스fit()
은 사이킷런 모델을 훈련할 때 사용하는 메서드predict()
는 사이킷런 모델을 훈련하고 예측할 때 사용하는 메서드score()
은 훈련된 사이킷런 모델의 성능을 측정하는 메서드
02 데이터 다루기
02-1 훈련 세트와 테스트 세트
도미와 빙어 데이터/타깃을 주고 훈련한 다음 같은 데이터로 테스트한다면 모두 맞힐 것이 당연하다. 연습 문제와 시험 문제가 달라야 올바르게 모델의 성능을 평가할 수 있다. 따라서 평가에 사용할 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]
fish_data = [[l,w] for l,w in zip(fish_length, fish_weight)]
fish_target = [1]*35 + [0]*14
#총 49개의 샘플 가운데 처음 35개를 훈련세트로, 나머지 14개를 테스트세트로 쓴다.
from sklearn.neighbors import KNeighborsClassifier
kn = KNeighborsClassifier()
#총 49개의 샘플 가운데 처음 35개를 훈련세트로, 나머지 14개를 테스트세트로 쓴다.
train_input = fish_data[:35] #슬라이싱으로 0부터 34번째 인덱스까지
train_target = fish_target[:35]
test_input = fish_data[35:]
test_target = fish_target[35:]
kn = kn.fit(train_input, train_target)
kn.score(test_input, test_target)
#score를 확인해보니 0.0으로 나왔다. 왜일까?
#샘플링이 편향된 것! 나머지 14개를 테스트 세트로 떼어놓으면 훈련 세트에는 빙어가 하나도 없다!
#따라서 훈련 세트와 테스트 세트를 적절히 분류하는 것이 매우 중요.
넘파이는 파이썬의 대표적인 배열(array) 라이브러리로, 고차원의 배열을 손쉽게 만들 수 있다.
import numpy as np
input_arr = np.array(fish_data)
target_arr = np.array(fish_target)
#훈련 세트와 테스트 세트를 적절히 섞기 위한 대책
np.random.seed(42)
index = np.arange(49) #0부터 48까지 정수 배열
np.random.shuffle(index) #index를 무작위로 섞음
print(index)
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.show()
파란색이 훈련세트, 주황색이 테스트세트이다. 골고루 잘 섞인 것을 볼 수 있다.
kn = kn.fit(train_input, train_target)
kn.score(test_input, test_target)
kn.predict(test_input)
02-1 핵심 키워드
- 지도학습: 입력과 타깃을 전달하여 모델을 훈련한 다음 새로운 데이터를 예측하는 데 사용
- 비지도학습: 타깃 데이터가 없는 학습
- 훈련세트: 모델을 훈련할 때 사용하는 데이터. 보통 훈련 세트가 클수록 좋음
- 테스트세트: 모델을 평가할 때 사용하는 데이터. 보통 전체 데이터의 20~30%.
02-1 핵심 패키지와 함수
- numpy
seed()
: 넘파이에서 난수를 생성하기 위한 정수 초기값을 지정arange()
: 일정한 간격의 정수 또는 실수 배열을 만듦. 기본 간격은 1shuffle
: 주어진 배열을 랜덤하게 섞음.
02-2 데이터 전처리(표준화)
fish_data = np.column_stack((fish_length, fish_weight))
# cloumn_stack은 전달받은 리스트를 일렬로 세운 다음 차례대로 연결
fish_target = np.concatenate((np.ones(35), np.zeros(14)))
# ones은 1을 n개만큼 배열로 만들고 zeros는 0을 n개만큼 배열로 만듦
사이킷런(train_test_split
함수)으로 훈련 세트와 테스트 데이터 나누기
from sklearn.model_selection import train_test_split
train_input, test_input, train_target, test_target = train_test_split(fish_data, fish_target, stratify=fish_target, random_state=42)
#straify매개변수에 타깃데이터를 전달하면 클래스 비율에 맞게 데이터를 나눔
print(test_target)
print(kn.predict([[25,150]])) #길이가 25이고 무게가 150인 생선을 예측
#원래대로라면 도미로 예측해야 하는데 빙어로 예측해버림. 왜일까?
distance, indexes = kn.kneighbors([[25,150]])
print(distance, indexes)
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker ='D')
plt.scatter(25,150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
삼각형이 예측대상이고, 주황색이 인접한 5개의 샘플이다. 가깝기론 우측상단의 도미들이 더 가까워보이는데, 훨씬 멀어보이는 빙어가 인접한 샘플로 분류되었다.
위같은 착시가 일어나는 것은 y축의 범위가 x축에 비해 훨씬 넓기 때문이다. 두 축의 범위를 같게 해보자.
plt.scatter(train_input[:,0], train_input[:,1])
plt.scatter(train_input[indexes,0], train_input[indexes,1], marker ='D')
plt.scatter(25,150, marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.xlim((0,1000)) #x축의 범위를 1000으로 넓혀 y축과 같은 스케일로 만들어보자
plt.show()
이제야 납득이 가는 실제 거리
두 특성의 scale이 다르기 때문에 생긴 일이다. 스케일이 상이하다면 k-최근접 알고리즘은 올바른 예측을 할 수 없다. 때문에 특성값을 전처리(preprocessing) 해줘야 한다. 가장 널리 사용하는 전처리 방법 중 하나는 표준점수(standard score) 이다.
mean = np.mean(train_input, axis=0) #평균
std = np.std(train_input, axis=0) #표준편차
train_scaled = (train_input-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()
kn.fit(train_scaled, train_target)
kn.score(test_scaled, test_target)
print(kn.predict([new]))
distance, indexes = kn.kneighbors([new])
new = ([25, 150] - mean)/std
plt.scatter(train_scaled[:,0], train_scaled[:,1])
plt.scatter(train_scaled[indexes,0], train_scaled[indexes,1], marker='D')
plt.scatter(new[0],new[1], marker='^')
plt.xlabel('length')
plt.ylabel('weight')
plt.show()
표준점수로 전처리해준 후엔 알고리즘이 제대로 작동하게 된다.
02-2 핵심 키워드
- 데이터 전처리(data-preprocessing): 훈련데이터를 주입하기 전 가공하는 단계
- 표준점수: 특성에 평균을 빼고 표준편차로 나눔. 반드시 훈련세트의 평균과 표준편차로 테스트 세트를 바꿔야 함
- 브로드캐스팅: 크기가 다른 넘파이 배열에서 자동으로 사칙 연산을 모든 행이나 열로 확장하여 수행하는 기능
02-2 핵심 패키지와 함수
- scikit-learn
train_test_split()
: 훈련세트, 테스트세트를 나누는 함수. test_size 매개변수로 테스트세트의 비율 지정 가능.kneighbors()
: 입력한 데이터의 가장 가까운 이웃을 찾아 거리와 이웃 샘플의 인덱스를 반환.