Machine Learning/K-최근접이웃 (KNN)

K-최근접 이웃(KNN) 알고리즘이란? - 3. 실전

Jay김 2020. 6. 9. 08:33

이번 편에서는 진짜 데이터를 상대로 Scikit-learn 패키지에 내장된 K 근접 이웃 알고리즘과 2편에서 우리가 직접 파이썬에서 써본 알고리즘을 사용해보고 비교해 보도록 하겠다.

일단 데이터부터 구해보자. 데이터는 UCI 기계학습 데이터 저장소에서 구할 수 있는 위스콘신 대학에서 진행한 연구에서 집계한 유방암 진단 데이터를 가져오도록 하겠다.[각주:1] 데이터 폴더(Data Folder) 링크에 들어가서 breast-cancer-wisconsin.data 파일을 다운로드하면 된다. 해당 파일을 열어보면 다음과 같이 CSV 형식의 데이터가 등장할 것이다. 

breast-cancer-wisconsin.names 파일에서 속성(attribute)에 관한 정보를 찾아보면 다음 리스트를 볼 수 있을 것이다.

여기서 1열(column)은 샘플 번호, 2 ~ 10열이 종양의 여러 피쳐(feature), 그리고 11열이 클래스 레이블(class label)에 해당되는 것을 확인할 수 있을 것이다. 2가 양성종양이고 4가 악성종양에 해당된다. 데이터 사용 편의를 위해 DATA 파일에 직접 헤더를 타이핑해주거나 pandas의 read_csv 함수를 이용해 헤더(header)를 추가해주자

breast-cancer-wisconsin.names 파일을 더 뒤져보면 16개의 데이터 누락 사례가 있으며 해당 데이터는 "?"로 표기가 되었음을 확인할 수 있다. 출처 표기 요청에 따라 아래에 데이터 출처도 따로 표기하도록 하겠다.

데이터 가공

다음 코드 블럭에 따라 몇 가지 패키지를 불러오고 데이터를 약간 가공하도록 하겠다. 일단 read_csv 함수로 파일을 불러온다. 그 뒤 누락된 정보, 그러니까 '?'로 표기된 데이터들을 -99,999로 변경해준다. 해당 작업은 손쉽게 알고리즘이 해당 데이터를 특이점(outlier)으로 판단해 계산에서 자동으로 제외하도록 도와준다. 이는 데이터가 누락된 행(row)을 통째로 제거해 귀중한 데이터를 잃는 방식을 대체한다. 여기서 inplace 매개변수를 사용하지 않는다면 다음과 같이 직접 데이터 프레임을 지정시킨 변수 df에 변형시킨 df를 다시 지정시켜 변경된 데이터를 저장해줘야 한다.

df = df.replace('?', -99999)

마지막으로 샘플 아이디 정보가 들어있는 열을 데이터에서 제외시킨다. drop 함수에 들어가는 매개변수들은 열 이름, 행(0) 또는 열(1), 그리고 원래 df를 변경시킴 순이다. 이는 샘플 아이디 정보 자체에 딱 봐도 종양이 악성인지 양성인지 판단할 수 있도록 도와주는 관련 정보도 없을뿐더러 KNN 알고리즘 자체의 예상 성적에 안 좋은 영향을 끼칠 수 있기 때문이다.  

import numpy as np
from sklearn import preprocessing, cross_validation, neighbors
import pandas as pd

df = pd.read_csv('breast-cancer-wisconsin.data')
df.replace('?', -99999, inplace = True)
df.drop(['id'], 1, inplace = True)

이제 데이터 프레임 df의 클래스 표기 정보가 들어있는 class 열을 Numpy 배열로 만들어 y 변수에 지정해주고 나머지 열들을 X 변수에 지정해준다.

다음으로 train_test_split 함수를 이용해 데이터를 1:4 비율로 훈련(train)과 시험(test) 자료로 나눠주도록 한다. 다음으로  scikit-learn 패키지 내장 K 근접이웃근접 이웃 함수인 KNeighborsClassifier() 함수를 '분류기' 변수에 지정해주고 훈련 데이터로 분류기를 fit 메서드(method)를 이용해 훈련해준다. 여기서 분류기라고 부르는 이유는 말 그대로 K 근접 이웃 알고리즘이 피쳐에 따라 각 관측 데이터를 양성 종양과 악성 종양으로 분류해주기 때문이다. 영어 코드로는 보통 classifier의 줄임말인 clf로 표기해준다.

마지막으로 시험 자료를 상대로 간단한 알고리즘 분류성능평가지표로 정확도(accuracy)를 출력해준다.

X = np.array(df.drop(['class'], 1))
y = np.array(df['class'])

X_훈련, X_시험, y_훈련, y_시험 = cross_validation.train_test_split(X, y, test_size = 0.2)

분류기 = neighbors.KNeighborsClassifier()
분류기.fit(x_훈련, y_훈련)

정확도 = 분류기.score(X_시험, y_시험)
print(정확도)

꽤 높은 정확도가 나올 것이다. 여기서 개인이 직접 실험해보고 싶으면 앞서 제외한 id 열까지 포함해서 다시 돌려보도록 하자. 정확도가 추락하는 것을 볼 수 있을 것이다.

분류기를 이용해 분류기가 관측한 적 없는 데이터를 분류해 보고 싶다면 새로 Numpy 배열을 만들어 분류기에 넣어주면 된다. 주어진 데이터 파일에서 Ctrl + F를 이용해 이미 해당 데이터가 없는지 확인해보고 시도하자.

신_관측개체 = np.array([4,2,1,1,1,2,3,2,1]).reshape(1, -1)


예측결과 = 분류기.predict(신_관측개체)
print(예측결과)

#만약 한 개 이상의 새로운 관측 개체를 넣고자 한다면 다음 메서드를 사용하면 된다.
np.array([4,2,1,1,1,2,3,2,1], [4,2,1,1,1,2,3,2,1]).reshape(len(신_관측개체), -1)

밑바닥 코드 적용

2편에서 만든 코드를 데이터에 적용해보도록 하겠다. 앞에서와 마찬가지로 df에 read_csv로 데이터를 불러와준다. 다만 우리가 만든 K 최근접이웃 알고리즘은 약간 다르게 데이터를 받아들이므로 직접 tolist() 메서드를 이용해 데이터 프레임에서 리스트로 이루어진 리스트로 변형시킨 뒤 훈련 자료와 시험자료로 나눠주도록 하겠다.

다음으로 혹시라도 있을 편향을 막기 위해 데이터를 셔플해준다. train_test_split은 훈련 자료와 시험 자료로 데이터를 나누기 전에 데이터를 셔플해준다.

import numpy as np
from math import sqrt
import warnings
from collections import Counter
import pandas as pd
import random

def k최근접이웃(데이터, 예상, k=3):
	if len(데이터) >= k:
    		warnings.warn('k 매개변수가 전체 투표 그룹보다 적습니다!')
 
    거리모음 = []
    for 그룹 in 데이터:
    	for 피쳐 in 데이터[그룹]:
            유클리드거리 = np.linalg.norm(np.array(피쳐) - np.array(예상))
            거리모음.append([유클리드거리, 그룹])
            
    투표 = [i[1] for i in sorted(거리모음)[:k]]
    투표결과 = Counter(투표).most_common(1)[0][0]
    return 투표결과
    
    
df = pd.read_csv("breast-cancer-wisconsin.data")
df.replace('?', -99999, inplace = True)
df.drop(['id'], 1, inplace = True)

#데이터 프레임 전체가 float 타입 데이터가 되도록 해줌
데이터셋 = df.astype(float).values.tolist()

#데이터를 섞어줌
random.shuffle(데이터셋)



k최근접이웃(데이터셋, 새_데이터, 3)

우리가 만든 K 최근접이웃 알고리즘은 데이터가 딕셔너리(dictionary)라는 가정을 하고 있다. 만약 알고리즘의 원리가 기억나지 않는다면 2편으로 가서 초반에 데이터셋을 어떻게 정의했는지 보고오도록 하자.

시험자료크기(test data size)을 0.2로 설정하고 인덱스 슬라이싱으로 1:4 비율로 훈련 자료와 시험 자료 리스트로 나누어준다. 그 후 훈련자료와 시험자료를 for 반복문으로 딕셔너리에 각 클래스 레이블에 맞춰 추가해준다. 여기서 이해가 안간다면 '데이터셋' 리스트가 리스트로 이루어진 리스트, 그러니까 [[4,2,1,1,1,2,3,2,1,2], [4,2,1,1,1,2,3,2,1,4]]와 같은 리스트라는 것을 떠올리자.

해당 리스트의 원소는 한개의 관측개체의 피쳐와 레이블 정보가 담긴 리스트이며 이 리스트의 -1 인덱스가 리스트의 마지막 숫자인데 이것이 곧 레이블이다. 이건 또한 훈련집합과 시험집합 딕셔너리의 키(key)이기 때문에 우리는 이를 이용해 각 관측개체에 해당하는 리스트들을 각 집합의 올바른 값(value) 리스트에 지정해주는 것이다. 다시 말하자면, 앞에 예를 들은 [4,2,1,1,1,2,3,2,1,2]은 2:[[4,2,1,1,1,2,3,2,1,2]]로 배정되고 [4,2,1,1,1,2,3,2,1,4]은 4:[[4,2,1,1,1,2,3,2,1,4]]로 배정된다.

이제 우리가 만든 K최근접이웃 알고리즘에 차례대로 훈련 데이터 딕셔너리, 분류해야 되는 시험집합에 포함된 리스트, 그리고 투표에 참여하는 이웃 수를 지정해준 뒤, 투표 결과가 각 시험집합 관측 개체의 진짜 레이블과 일치하는지 확인해준다. 만약 하나의 관측 개체 분류가 2 혹은 4로 제대로 분류 되었다면 제대로 분류된 사례를 세는 '일치' 변수를 1 증가시키고 총 분류 개수를 세는 '총개수' 변수도 1 증가 시켜준다.

마지막으로 '일치' 변수를 '총개수' 변수로 나눠서 해당 알고리즘의 정확도를 계산해주면 된다.

훈련자료크기 = 0.2
훈련집합 = {2:[], 4:[]}
시험집합 = {2:[], 4:[]}
훈련자료 = 데이터셋[:-int(시험자료크기*len(데이터셋))]
시험자료 = 데이터셋[-int(시험자료크기*len(데이터셋)):]

for i in 훈련자료:
	훈련집합[i[-1]].append(i[:-1])
    
for i in 시험자료:
	시험집합[i[-1]].append(i[:-1])
    
일치 = 0 
총개수 = 0

for 그룹 in 시험집합:
	for 데이터 in 시험집합[그룹]:
    	투표 = k최근접이웃(훈련집합, 데이터, k=5)
        if 그룹 == 투표:
        	일치 += 1
        총개수 += 1
print('정확도:', 일치/총개수)

다음 편에선 K 지정에 따라 정확도가 어떻게 바뀌는지를 포함한 세부사항을 다루도록 하겠다.

데이터 출처: 1. O. L. Mangasarian and W. H. Wolberg: "Cancer diagnosis via linear 
      programming", SIAM News, Volume 23, Number 5, September 1990, pp 1 & 18.

[Copyright ⓒ 블로그채널 무단전재 및 재배포 금지]