티스토리 뷰

반응형
R을 통한 Machine Learning 구현 - (1)KNN

오늘부터 Machine Learning에 대해 다시 리마인드하는 마음으로
배웠던 Machine Learning을 R과 Python으로 구현하며 정리하도록 하겠다.



Knn 이론 설명

Knn은 K-nearest neighbors의 약자로, “최근접 이웃을 사용한 분류”라고 할 수 있다.
지도학습에서도 분류에 해당하는 방법이다.

보통 사람끼리의 관계이던 공간적인 관계이던,
비슷한 것끼리는 같은 특성을 가지며 가까이 지내려고 한다.

Knn은 이러한 데이터의 특성을 이용한 것이며 어떤 데이터의 라벨값을 정의할때
그 데이터의 가장 가까운 주변 반경 ε(입실론) 안의 데이터들의 라벨을 조사하여 다수결인 라벨로 정의하는 것이다.

<링크-KNN의 분류 원리를 잘 보여주는 그림>


위의 그림에서
주변반경 ε(입실론)을 5라고 설정했다면 → 라벨값은 세모3:별표2로 세모가 되고,
ε(입실론)을 10이라고 설정했다면 → 라벨값은 세모4:별표6로 별표가 된다.

ε(입실론)값이 너무 크면 분류를 잘 못하고,
ε(입실론)값이 너무 작으면 테스트 데이터에만 익숙해져 새로운 데이터를 분류를 잘 못하는 현상이 일어난다(오버피팅)

→ 적절한 k는 이원교차표를 통해 확인한다.


Data Set

Data Set 설명

Knn을 구현하기 위해 신체 특징을 통해 척추동물의 종류를 구별하는 zoo2.csv를 사용하였다. (Data Set 링크)

라벨값은 맨 마지막 컬럼인 type이며,
1(포유류), 2(조류), 3(파충류), 4(어류), 5(양서류), 6(곤충), 7(갑각류)로 나누어진다.

각 컬럼은 신체의 특징(fin: 발톱, tail: 꼬리 등)으로 구성되어 있으며,
leg는 Numeric type(다리가 몇 개인지)이며 이를 제외한 나머지 컬럼은 모두 boolean(Yes/no) type이다.


<zoo2.csv Data Set Column 설명>

Data Set Import

rm(list=ls()) #기존 변수 삭제 
zoo2<- read.csv("D:\\data\\zoo2.csv" , header = T, stringsAsFactors = F)
head(zoo2) #Data Set 확인
  animal.name hair feathers eggs milk airborne aquatic predator toothed
1    aardvark    1        0    0    1        0       0        1       1
2    antelope    1        0    0    1        0       0        0       1
3        bass    0        0    1    0        0       1        1       1
4        bear    1        0    0    1        0       0        1       1
5        boar    1        0    0    1        0       0        1       1
6     buffalo    1        0    0    1        0       0        0       1
  backbone breathes venomous fins legs tail domestic catsize type
1        1        1        0    0    4    0        0       1    1
2        1        1        0    0    4    1        0       1    1
3        1        0        0    1    0    1        0       0    4
4        1        1        0    0    4    0        0       1    1
5        1        1        0    0    4    1        0       1    1
6        1        1        0    0    4    1        0       1    1
str(zoo2) #Data Set type 확인
'data.frame':   101 obs. of  18 variables:
 $ animal.name: chr  "aardvark" "antelope" "bass" "bear" ...
 $ hair       : int  1 1 0 1 1 1 1 0 0 1 ...
 $ feathers   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ eggs       : int  0 0 1 0 0 0 0 1 1 0 ...
 $ milk       : int  1 1 0 1 1 1 1 0 0 1 ...
 $ airborne   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ aquatic    : int  0 0 1 0 0 0 0 1 1 0 ...
 $ predator   : int  1 0 1 1 1 0 0 0 1 0 ...
 $ toothed    : int  1 1 1 1 1 1 1 1 1 1 ...
 $ backbone   : int  1 1 1 1 1 1 1 1 1 1 ...
 $ breathes   : int  1 1 0 1 1 1 1 0 0 1 ...
 $ venomous   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ fins       : int  0 0 1 0 0 0 0 1 1 0 ...
 $ legs       : int  4 4 0 4 4 4 4 0 0 4 ...
 $ tail       : int  0 1 1 0 1 1 1 1 1 0 ...
 $ domestic   : int  0 0 0 0 0 0 1 1 0 1 ...
 $ catsize    : int  1 1 0 1 1 1 1 0 0 0 ...
 $ type       : int  1 1 4 1 1 1 1 4 4 1 ...
dim(zoo2) #Data Set size 확인
[1] 101  18




Knn 구현

첫 시도

라벨값의 분포가 어떻게 되어있는지 table함수와 ggplot으로 확인해보자.


 1  2  3  4  5  6  7 
41 20  5 13  4  8 10 



전체 Data Set에서 1(포유류)의 비중이 많은 편이다.
이럴때에는 train data를 편향된 데이터로 선정하지 않기 위해 Data를 섞어주자.

set.seed(12345) #복잡하게 랜덤 설정
idx<-sample(x=c("train", "test"), size=nrow(zoo2), 
            replace=TRUE, prob=c(7,3)) #7:3의 비율로 train, test 정함
zoo2$idx<-idx #새로운 컬럼추가
train<-zoo2[zoo2$idx=="train", ][,1:17] #label, idx컬럼을 제외하고 train 데이터를 만든다. 
train_label<-zoo2[zoo2$idx=="train", ][,18] #train데이터의 label 컬럼을 따로 뽑아준다. 
test<-zoo2[zoo2$idx=="test", ][,1:17] #train과 동일하게 test 적용 
test_label<-zoo2[zoo2$idx=="test", ][,18]
dim(train) #train Data Set Size 확인
[1] 66 17
dim(test) #test Data Set Size 확인
[1] 35 17



다른 컬럼들은 모두 boolean(0 or 1)이지만, legs는 number형태이다.
legs의 1과 fin의 1이 같은 값이 아니므로(각 컬럼의 범위가 다르므로) 잠재적으로 분류기에 문제가 된다.
이를 해결하기 위한 방법으로 데이터를 표준화 혹은 정규화를 통해 동일한 범위의 값으로 변경해야한다.

표준화는 기존 scale 함수를, 정규화는 직접 함수를 만들어 사용하면 되며, 공식은 아래와 같다.


표준화 공식(scale) 정규화 공식(기존 함수 없음)



표준화와 정규화를 사용해 실제 나온 정확도 차이는 보통 크지 않은 편이다.
이번에는 normalize 함수를 직접 만들어서 데이터를 정규화해주었다.

normalize<-function(x) {
  return( (x-min(x))/ ( max(x)-min(x)))
}
train<-as.data.frame(lapply(train[, 2:17], normalize)) #첫번째 컬럼(동물 이름이 나옴), 마지막 컬럼(라벨) 제외하고 정규화
test<-as.data.frame(lapply(test[, 2:17], normalize)) #첫번째 컬럼(동물 이름이 나옴), 마지막 컬럼(라벨) 제외하고 정규화
unique(train$legs) #legs컬럼의 값들이 변경된 것을 알 수 있다. 
[1] 0.500 0.000 0.250 0.750 1.000 0.625



이제 Knn을 구현해보자. class 라이브러리의 knn 함수를 이용하면 쉽게 knn을 구현할 수 있다.
(k값은 임의로 5로 설정)

knn( 훈련데이터, 테스트데이터, 훈련데이터라벨, k(입실론의 개수),
prob(예상값의 확률을 출력할 것인지), use.all(같은 거리에 있는 다수의 데이터 모두 사용할 것인지 여부 ) )
→ 테스트데이터의 라벨 예상값을 출력함

library(class)
result<- knn(train, test, train_label, k=5, prob=TRUE)
result
 [1] 1 1 4 1 4 1 4 1 1 2 2 1 2 2 7 2 1 2 1 2 4 5 1 1 1 1 4 4 4 7 2 5 1 1 7
attr(,"prob")
 [1] 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
 [8] 0.6000000 1.0000000 1.0000000 0.4285714 1.0000000 1.0000000 0.4285714
[15] 0.8333333 1.0000000 1.0000000 0.4285714 1.0000000 1.0000000 1.0000000
[22] 0.4285714 1.0000000 1.0000000 0.6000000 1.0000000 0.7000000 0.4000000
[29] 1.0000000 0.8333333 0.8888889 0.6666667 1.0000000 1.0000000 0.6000000
Levels: 1 2 4 5 6 7



위의 결과는 knn을 통해 test data의 label 예상값을 출력한 것이다.
결과를 더 알아보기 쉽게 test_label 값과 비교해보자.

head(data.frame(test_label, result))
  test_label result
1          1      1
2          1      1
3          4      4
4          1      1
5          4      4
6          1      1



틀린 것도 있지만, 예상이 맞은 것이 많은 편으로 보인다. 정확도와 이원교차표를 출력해보자.

# 정확도 확인
prop.table(table(ifelse(test_label==result,"o","x")))

        o         x 
0.6857143 0.3142857 
#이원교차표 생성
library(gmodels)
res<-CrossTable(x=test_label, y=result, prop.chisq=FALSE)  

 
   Cell Contents
|-------------------------|
|                       N |
|           N / Row Total |
|           N / Col Total |
|         N / Table Total |
|-------------------------|

 
Total Observations in Table:  35 

 
             | result 
  test_label |         1 |         2 |         4 |         5 |         7 | Row Total | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
           1 |        15 |         0 |         0 |         0 |         0 |        15 | 
             |     1.000 |     0.000 |     0.000 |     0.000 |     0.000 |     0.429 | 
             |     1.000 |     0.000 |     0.000 |     0.000 |     0.000 |           | 
             |     0.429 |     0.000 |     0.000 |     0.000 |     0.000 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
           2 |         0 |         4 |         0 |         0 |         0 |         4 | 
             |     0.000 |     1.000 |     0.000 |     0.000 |     0.000 |     0.114 | 
             |     0.000 |     0.500 |     0.000 |     0.000 |     0.000 |           | 
             |     0.000 |     0.114 |     0.000 |     0.000 |     0.000 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
           3 |         0 |         1 |         2 |         2 |         0 |         5 | 
             |     0.000 |     0.200 |     0.400 |     0.400 |     0.000 |     0.143 | 
             |     0.000 |     0.125 |     0.286 |     1.000 |     0.000 |           | 
             |     0.000 |     0.029 |     0.057 |     0.057 |     0.000 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
           4 |         0 |         0 |         5 |         0 |         0 |         5 | 
             |     0.000 |     0.000 |     1.000 |     0.000 |     0.000 |     0.143 | 
             |     0.000 |     0.000 |     0.714 |     0.000 |     0.000 |           | 
             |     0.000 |     0.000 |     0.143 |     0.000 |     0.000 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
           6 |         0 |         3 |         0 |         0 |         3 |         6 | 
             |     0.000 |     0.500 |     0.000 |     0.000 |     0.500 |     0.171 | 
             |     0.000 |     0.375 |     0.000 |     0.000 |     1.000 |           | 
             |     0.000 |     0.086 |     0.000 |     0.000 |     0.086 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|
Column Total |        15 |         8 |         7 |         2 |         3 |        35 | 
             |     0.429 |     0.229 |     0.200 |     0.057 |     0.086 |           | 
-------------|-----------|-----------|-----------|-----------|-----------|-----------|

 



knn의 정확도 높이는 방법

정확도가 68.57%로 높지 않은 편이다. knn에서 정확도를 높이는 방법은
1. 적정한 K의 값을 구하는 것
2. 표준화/정규화 중 사용하지 않은 방법 적용
두 가지가 있다.


우선 적정한 k의 값을 구하는 것을 살펴보면, 관례적으로 보통 k를 훈련데이터 개수의 제곱근으로 둔다.

훈련데이터가 66개 row이므로, 제곱근은 8.12이므로 8을 사용하였지만 결과의 정확도는 k가 5일때와 동일하였다.

이에 따라 최적의 k를 찾기 위해 함수를 구현해보았다. (본 Data Set의 row가 101개인 것을 안다고 가정)

library(gmodels) #이원교차표를 보기 위한 라이브러리
kvalue<-NULL
accuracy_k<-NULL
#k의 값과 정확도를 넣어줄 변수를 생성

for (i in c(1:50)){
  knn_i<-knn(train, test, train_label, k=i, prob=TRUE)
  kvalue<-c(kvalue, i) #k값 지속적으로 추가
  accuracy<-data.frame(prop.table(table(ifelse(test_label==knn_i,"o","x"))))[1,2]
  accuracy_k<-c(accuracy_k, accuracy) #정확도 추가
} 

valid_k<-data.frame(k = kvalue, accuracy = accuracy_k) #데이터프레임 생성

#k값에 따른 정확도 변화 그래프 생성
plot(formula = accuracy ~ k,
     data = valid_k,
     type = "o",
     pch = 20,
     main = "k값에 따른 정확도 변화")

#그래프에 k 라벨링하기
with(valid_k, text(accuracy ~ k, labels = rownames(valid_k), pos = 1, cex = 0.7))


이를 통해 k값은 1에서 증가할수록 점점 정확도가 떨어지는 것을 확인할 수 있다.
허나, k가 1이라면 가장 가까운 한가지 데이터로만 라벨값을 추측하는 것이므로,
왠만하면 1을 제외한 2나 3으로 진행하는 것이 좋겠다.

→ k가 1인 것을 1-NN이라고도 하는데,
거짓긍정이 추가되지만 거짓부정이 조금 줄어드는 장점이 있어 간혹 사용된다고 한다.



두번째로 정규화 대신 scale함수를 통해 표준화를 사용해보자.

※ 만약 만든 모델로 추후 다른 데이터를 분류할 때 현재 보유한 데이터의 최대/최소값을 넘는 데이터가
앞으로 생길 가능성이 있다면 무조건 z-점수 표준화를 사용한 scale을 사용해야한다. (정규화는 최대/최소값이 변경되므로 정확하지 않음)

##다시 정규화전 데이터를 불러오기
train<-zoo2[zoo2$idx=="train", ][,1:17] #label, idx컬럼을 제외하고 train 데이터를 만든다. 
train_label<-zoo2[zoo2$idx=="train", ][,18] #train데이터의 label 컬럼을 따로 뽑아준다. 
test<-zoo2[zoo2$idx=="test", ][,1:17] #train과 동일하게 test 적용 
test_label<-zoo2[zoo2$idx=="test", ][,18]

##표준화
train<-as.data.frame(lapply(train[, 2:17], scale)) #첫번째 컬럼(동물 이름이 나옴), 마지막 컬럼(라벨) 제외하고 표준화
test<-as.data.frame(lapply(test[, 2:17], scale))

##knn구현 및 정확도 확인
library(class)
result<- knn(train, test, train_label, k=3, prob=TRUE)
prop.table(table(ifelse(test_label==result,"o","x"))) # 정확도 확인

        o         x 
0.6857143 0.3142857 


결과를 보니 (정규화, k=3)일때의 정확도인 74.29%보다 (표준화, k=3)일째의 정확도가 68.57%로 조금 더 낮았다.
본 Data Set에서는 표준화보다는 정규화가 더 정확도를 높이는데 도움이 되었다.



Weighted Nearest Neighbor Classification

정확도가 너무 낮은 문제가 있어 여기저기 찾아보니
k 관련 정확도/적절성을 해결하는 방법으로 k값을 크게 주고 가까운 값에 대하여 가중치를 주는 weighted voting 방법이 있었다.

해당 방법을 구현하는 알고리즘도 여러가지 였으며,
현재 weighted voting을 가장 잘 구현한 패키지라고 판단되는 FNN으로 다시 정확도를 확인해보았다.
(k=3으로 설정)

library(FNN)
ownn(train, test, train_label, testcl=test_label, k = 3, prob = FALSE, algorithm=c("kd_tree", "cover_tree", "brute"))
$k
[1] 3

$knnpred
 [1] 1 1 4 1 4 1 4 1 1 2 7 1 2 7 7 2 1 7 1 2 4 5 1 1 1 1 4 5 4 7 2 5 1 1 6
Levels: 1 2 4 5 6 7

$ownnpred
 [1] 1 1 4 1 4 1 4 1 1 2 7 1 2 7 7 2 1 7 1 2 4 5 1 1 1 1 7 5 4 7 2 5 1 1 6
Levels: 1 2 4 5 6 7

$bnnpred
 [1] 1 1 4 1 4 1 4 1 1 2 6 1 2 6 6 2 1 6 1 2 4 5 1 1 1 1 7 5 4 6 2 5 1 1 6
Levels: 1 2 4 5 6 7

$accuracy
      knn      ownn       bnn 
0.7142857 0.7142857 0.8571429 

결과를 보면 brute라는 알고리즘을 통해 생성된 모델은 정확도가 85.71%로 매우 높았다!
매우 향상된 결과를 보이는 알고리즘이다.


FNN 패키지의 설명을 보니 k값을 NULL로 놓는 것을 default로 설정하여 NULL로 실행해보았다.

ownn(train, test, train_label, testcl=test_label, k = NULL, prob = FALSE, algorithm=c("kd_tree", "cover_tree", "brute"))
$k
[1] 5

$knnpred
 [1] 1 1 4 1 4 1 4 1 1 2 7 1 2 7 7 2 1 7 1 2 4 5 1 1 1 1 4 2 4 7 2 5 1 1 7
Levels: 1 2 4 5 7

$ownnpred
 [1] 1 1 4 1 4 1 4 1 1 2 7 1 2 7 7 2 1 7 1 2 4 5 1 1 1 1 4 5 4 7 2 5 1 1 6
Levels: 1 2 4 5 6 7

$bnnpred
 [1] 1 1 4 1 4 1 4 1 1 2 7 1 2 7 7 2 1 7 1 2 4 5 1 1 1 1 4 5 4 7 2 5 1 1 6
Levels: 1 2 4 5 6 7

$accuracy
      knn      ownn       bnn 
0.6857143 0.7142857 0.7142857 


K의 값이 5로 자동 설정되어 아마 적당한 값들 중 정확도가 높은 k를 선별해주는 것으로 보인다.
추후 논문 등을 통해 살펴봐야겠다.



결론

  • zoo.csv로 knn을 구현해본 결과 본 Data Set은 knn을 통해 생성한 모델의 최고 높은 정확도가 약 85%였고,
    knn으로 모델을 만들기에는 적합하지 않은 데이터라고 판단된다.

  • 지도학습이지만, 단순하면서 빠른 분류방법인 knn은
    알맞은 Data Set일 경우 빠르고 강한 최적의 모델을 만들수 있다.

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함