티스토리 뷰

반응형
R을 통한 Machine Learning 구현 - (2)Naive Bayes


오늘은 Naive Bayes에 대해 알아보자.

Naive Bayes은 결과에 대한 전체 확률을 추정하기 위해 동시에 여러 속성 정보를 고려해야만 하는 문제에 가장 적합하다.
특히 TextMining 후 텍스트 분류(예: 스팸메일, 코딩 언어 분류)하는데 주로 사용된다. (한정된 데이터 범위, 큰 이상치나 오류가 없는 정도의 데이터)



Naive Bayes 이론 설명

Naive(순진한) Bayes 이론은 Data Set의 모든 특징(변수)이 동등하게 중요하고 독립적이라고 가정하기 때문에 이러한 이름이 붙여졌다.
이런 가정은 대부분의 실제 응용시에는 맞지 않지만, 그래도 높은 정확도의 효율을 보여준다.

어떠한 두 사건이 있을 때, 두 사건이 완전히 관련이 없다면 이를 독립사건이라 부른다.
→ 사건 B가 일어날 때 사건 A의 확률 P(A|B)는 사건 A의 확률 P(A)와 동일하다.

이는 단순히 한 사건의 결과를 아는 것으로는 다른 사건의 결과에 대한 어떤 정보도 제공하지 못한다는 것을 의미한다.

예시) 비가 오는 날씨 & 동전을 던졌을 때 앞면이 나오는 것


이를 다시 이야기하면, 종속 사건이 예측 모델링의 기반이 된다.
종속사건 간의 관계는 베이즈 정리(Bayes’Theorem)를 이용해 설명할 수 있으며, 아래의 공식과 같다.


P(A|B)는 ’사건 B가 발생한 경우 사건 A의 확률’로 읽으며, 이를 조건부 확률이라한다.
(A의 확률이 B가 발생한 것에 종속적이기 때문)

베이즈 정리는 P(A|B)의 추정이 P(A∩B)와 P(B)에 기반을 둬야 한다는 것으로,
P(A∩B)는 A와 B가 동시에 발생하는 빈도의 측정치며, P(B)는 B가 보통 발생하는 빈도의 측정치다.

아래에 변형된 공식은 P(A∩B)=P(B∩A)임을 이용하여 다시 배열한 것이다.
여기서 P(B|A)는 사후확률, P(A|B)를 우도(likelihood), P(B)를 사전확률, P(A)를 주변 우도라고 한다.


공식을 통해 복잡한 특징간의 확률을 구현하기에는 어려울 수 있으므로 우도표를 작성하는데, 간단한 예제로 이를 이해해보자.


예제) 스팸메시지를 분류하는 데이터에서 ’비아그라’와 ’주소삭제’라는 단어는 포함하고
’돈’과 ’식료품’이라는 단어는 포함하지 않은 메시지가 스팸일 확률은 어떻게 되는가?

▼ 위 예제의 우도표


→ 문제를 확률로 표현하자면 P(스팸 | 비아그라 ∩ ¬돈 ∩ ¬식료품 ∩ 주소삭제)와 동일하다. (¬은 아니다 라는 뜻의 기호)

이를 위 공식에 대입하면,

P(비아그라 ∩ ¬돈 ∩ ¬식료품 ∩ 주소삭제 | 스팸) × P(스팸)
—————————————————————————– 은 곧
P(비아그라 ∩ ¬돈 ∩ ¬식료품 ∩ 주소삭제)


P(비아그라|스팸) × P(¬돈|스팸) × P(¬식료품|스팸) × P(주소삭제|스팸) × P(스팸)
—————————————————————————————————————– 와 같다.
P(비아그라 ∩ ¬돈 ∩ ¬식료품 ∩ 주소삭제)


이 확률을 조금 더 쉽게 구하려면 동일한 조건일 때 스팸이 아닌 것(햄)이 나올 확률도 구하여 비교하면 된다.
(동일한 조건이므로 주변우도인 분모는 스킵하고 분자끼리만 비교)

① 스팸인 우도: P(비아그라|스팸) * P(¬돈|스팸) * P(¬식료품|스팸) * P(주소삭제|스팸) * P(스팸)
= (4/20) * (10/20) * (20/20) * (12/20) * (20/100) = 0.012

② 햄인 우도: P(비아그라|햄) * P(¬돈|햄) * P(¬식료품|햄) * P(주소삭제|햄) * P(햄)
= (1/80) * (66/80) * (71/80) * (23/80) * (80/100) = 0.0021

결론적으로 어떠한 상황이던 메세지는 스팸 아니면 햄이므로, 위의 두 확률을 더하여 각각의 확률을 구해보자.

▼ 스팸일 확률
0.012/(0.012+0.0021) ≒ 85.1%



나이브 베이즈 알고리즘의 장/단점은 다음과 같다.

단순히 데이터를 입력하여 그것을 기반으로 어떤 그룹에 속할 것인지 판단하는 알고리즘으로서,
실무에서 높은 효율을 보이지만 인공지능처럼 실제 인간과 같을 정도나
한 치의 오류도 허용하면 안되는 부분에는 적용하기 어렵다.


Data Set

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

Data Set에 대한 설명은 링크를 참조하면 되겠다.

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



Naive Bayes 구현

e1071 패키지

Naive Bayes를 높은 정확도로 구현하기 위한 최적의 Data Set은 Factor type의 text로 구성된 것이다.
→ Data Set 내 데이터가 Factor가 아니면 사용할 수 없음

R에서는 오스트리아 수도 빈 비엔나 기술대학교 통계학과에서 개발한
e1071 패키지를 사용해 Naive Bayes를 구현할 수 있다.

Knn 구현 시와 동일하게 Data Shuffling & Sampling, Data Set인 zoo2.csv의 특성에 따라 정규화를 진행한다.

## Shuffling & Sampling  
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]

## 정규화 진행
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)) #첫번째 컬럼(동물 이름이 나옴), 마지막 컬럼(라벨) 제외하고 정규화



앞서 말했듯이 Naive Bayes 사용을 위해 사용할 데이터를 모두 factor로 변경한다.
(test_label은 알고리즘을 통해 나온 예상값과 비교하는데 사용하므로 factor로 변경하지 않아도 됨)

## 필요한 데이터 factor로 변경
for (i in (1:ncol(train))){
  train[,i]<-factor(train[,i])
}

for (i in (1:ncol(test))){
  test[,i]<-factor(test[,i])
}

train_label<-factor(train_label)
str(train) #factor로 변경되었는지 확인 
'data.frame':   66 obs. of  16 variables:
 $ hair    : Factor w/ 2 levels "0","1": 2 2 2 1 2 1 1 1 1 1 ...
 $ feathers: Factor w/ 2 levels "0","1": 1 1 1 1 1 2 1 1 1 2 ...
 $ eggs    : Factor w/ 2 levels "0","1": 1 1 1 2 1 2 2 2 2 2 ...
 $ milk    : Factor w/ 2 levels "0","1": 2 2 2 1 2 1 1 1 1 1 ...
 $ airborne: Factor w/ 2 levels "0","1": 1 1 1 1 1 2 1 1 1 2 ...
 $ aquatic : Factor w/ 2 levels "0","1": 1 1 1 2 1 1 1 2 2 1 ...
 $ predator: Factor w/ 2 levels "0","1": 2 1 1 1 2 1 2 2 2 2 ...
 $ toothed : Factor w/ 2 levels "0","1": 2 2 2 2 2 1 1 1 1 1 ...
 $ backbone: Factor w/ 2 levels "0","1": 2 2 2 2 2 2 1 1 1 2 ...
 $ breathes: Factor w/ 2 levels "0","1": 2 2 2 1 2 2 1 1 1 2 ...
 $ venomous: Factor w/ 2 levels "0","1": 1 1 1 1 1 1 1 1 1 1 ...
 $ fins    : Factor w/ 2 levels "0","1": 1 1 1 2 1 1 1 1 1 1 ...
 $ legs    : Factor w/ 6 levels "0","0.25","0.5",..: 3 3 3 1 3 2 1 3 5 2 ...
 $ tail    : Factor w/ 2 levels "0","1": 2 2 2 2 2 2 1 1 1 2 ...
 $ domestic: Factor w/ 2 levels "0","1": 1 1 2 2 1 2 1 1 1 1 ...
 $ catsize : Factor w/ 2 levels "0","1": 2 2 2 1 2 1 1 1 1 1 ...



이제 Naive Bayes를 구현하기 위해 e1071 패키지를 불러오고 naiveBayes 함수를 사용하자.
(laplace값은 임의로 1으로 설정)


naiveBayes( 훈련데이터, 훈련데이터라벨, laplace 값 설정) → 모델을 생성

predict( 생성한 모델, 테스트데이터 ) → 예상값을 출력

예상값과 실제 테스트데이터의 라벨인 test_label과 비교하고, 정확도 정도도 확인하자.

library(e1071)
model<-naiveBayes(train, train_label, laplace=1) # 모델생성
result<-predict(model, test) # 예상값 출력
head(data.frame(test_label, result), 10) # 예상값과 실제값 비교
   test_label result
1           1      1
2           1      1
3           4      4
4           1      1
5           4      4
6           1      1
7           4      4
8           1      4
9           1      1
10          2      2
result<-as.numeric(result) # 동일 여부를 파악하기 위해 result(예상값)을 factor에서 숫자형 데이터로 바꿔줌

prop.table(table(ifelse(test_label==result, 'O', 'X'))) # 같으면 'O'로 표기, 다르면 'X'로 표기

  O   X 
0.6 0.4 



60%의 낮은 정확도가 나왔다. 물론 laplace 추정량 변경을 통해 정확도를 높일 수 있지만,
default 값인 1일때 나온 정확도가 낮은 상황이므로 크게 개선되지 않을 것으로 판단되었다.

그래서 모든 데이터가 factor로 되어있으며 text로 구별할 수 있는 데이터를 찾다가
일반 버섯과 독버섯의 특징에 대한 Data Set인 mushroom.csv(출처-링크)로 Naive Bayes를 다시 구현해보았다.

  type cap_shape cap_surface cap_color bruises odor gill_attachment
1    e         x           s         y       t    a               f
2    e         b           s         w       t    l               f
3    p         x           y         w       t    p               f
4    e         x           s         g       f    n               f
5    e         x           y         y       t    a               f
6    e         b           s         w       t    a               f
  gill_spacing gill_size gill_color stalk_shape stalk_root
1            c         b          k           e          c
2            c         b          n           e          c
3            c         n          n           e          e
4            w         b          k           t          e
5            c         b          n           e          c
6            c         b          g           e          c
  stalk_surface_above_ring stalk_surface_below_ring stalk_color_above_ring
1                        s                        s                      w
2                        s                        s                      w
3                        s                        s                      w
4                        s                        s                      w
5                        s                        s                      w
6                        s                        s                      w
  stalk_color_below_ring veil_type veil_color ring_number ring_type
1                      w         p          w           o         p
2                      w         p          w           o         p
3                      w         p          w           o         p
4                      w         p          w           o         e
5                      w         p          w           o         p
6                      w         p          w           o         p
  spore_print_color population habitat
1                 n          n       g
2                 n          n       m
3                 k          s       u
4                 n          a       g
5                 k          n       g
6                 k          n       m
'data.frame':   5342 obs. of  23 variables:
 $ type                    : Factor w/ 2 levels "e","p": 1 1 2 1 1 1 1 2 1 1 ...
 $ cap_shape               : Factor w/ 6 levels "b","c","f","k",..: 6 1 6 6 6 1 1 6 1 6 ...
 $ cap_surface             : Factor w/ 4 levels "f","g","s","y": 3 3 4 3 4 3 4 4 3 4 ...
 $ cap_color               : Factor w/ 10 levels "b","c","e","g",..: 10 9 9 4 10 9 9 9 10 10 ...
 $ bruises                 : Factor w/ 2 levels "f","t": 2 2 2 1 2 2 2 2 2 2 ...
 $ odor                    : Factor w/ 8 levels "a","c","f","l",..: 1 4 6 5 1 1 4 6 1 4 ...
 $ gill_attachment         : Factor w/ 1 level "f": 1 1 1 1 1 1 1 1 1 1 ...
 $ gill_spacing            : Factor w/ 2 levels "c","w": 1 1 1 2 1 1 1 1 1 1 ...
 $ gill_size               : Factor w/ 2 levels "b","n": 1 1 2 1 1 1 1 2 1 1 ...
 $ gill_color              : Factor w/ 10 levels "b","e","g","h",..: 5 6 6 5 6 3 6 7 3 3 ...
 $ stalk_shape             : Factor w/ 2 levels "e","t": 1 1 1 2 1 1 1 1 1 1 ...
 $ stalk_root              : Factor w/ 5 levels "?","b","c","e",..: 3 3 4 4 3 3 3 4 3 3 ...
 $ stalk_surface_above_ring: Factor w/ 3 levels "f","k","s": 3 3 3 3 3 3 3 3 3 3 ...
 $ stalk_surface_below_ring: Factor w/ 4 levels "f","k","s","y": 3 3 3 3 3 3 3 3 3 3 ...
 $ stalk_color_above_ring  : Factor w/ 7 levels "","b","e","g",..: 7 7 7 7 7 7 7 7 7 7 ...
 $ stalk_color_below_ring  : Factor w/ 8 levels "","b","e","g",..: 7 7 7 7 7 7 7 7 7 7 ...
 $ veil_type               : Factor w/ 2 levels "","p": 2 2 2 2 2 2 2 2 2 2 ...
 $ veil_color              : Factor w/ 2 levels "","w": 2 2 2 2 2 2 2 2 2 2 ...
 $ ring_number             : Factor w/ 3 levels "","o","t": 2 2 2 2 2 2 2 2 2 2 ...
 $ ring_type               : Factor w/ 5 levels "","e","f","l",..: 5 5 5 2 5 5 5 5 5 5 ...
 $ spore_print_color       : Factor w/ 7 levels "","h","k","n",..: 4 4 3 4 3 3 4 3 3 4 ...
 $ population              : Factor w/ 7 levels "","a","c","n",..: 4 4 5 2 4 4 5 6 5 4 ...
 $ habitat                 : Factor w/ 8 levels "","d","g","l",..: 3 5 7 3 3 5 5 3 5 3 ...
[1] 5342   23



mushroom.csv의 라벨은 첫번째 컬럼인 type이고, 독이 있다면 “p(poisonous)”, 식용이면 “e(edible)”의 값을 가진다.
라벨값의 분포가 어떻게 되어있는지 table함수와 ggplot으로 확인해보자.


   e    p 
3510 1832 



이번 Data Set은 정규화할 필요는 없으니 간단한 정제작업 후
Shuffling과 Sampling으로 train, test data를 나누었다.

## 정제작업 
mr[which(mr[12]=='?'), 12] <-NA #'?'로 되어있는 것을 NA로 바꿔준다. 
for (i in (2:23)){
  mr[which(mr[,i]==''),i]<-NA # 데이터가 없는 것을 NA로 바꿔준다. 
}

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



Naive Bayes를 구현해보았다. 이번엔 보기 편하게 이원교차표로 결과를 확인해보았다.

library(e1071)
model<-naiveBayes(train, train_label, laplace=1) # 모델생성
result<-predict(model, test) # 예상값 출력

library(gmodels) #이원교차표 사용을 위한 패키지 불러오기 
va<-CrossTable(result, test_label ) #이원교차표 생성

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|           N / Col Total |
|         N / Table Total |
|-------------------------|

 
Total Observations in Table:  1613 

 
             | test_label 
      result |         e |         p | Row Total | 
-------------|-----------|-----------|-----------|
           e |      1055 |        59 |      1114 | 
             |   145.440 |   275.735 |           | 
             |     0.947 |     0.053 |     0.691 | 
             |     0.999 |     0.106 |           | 
             |     0.654 |     0.037 |           | 
-------------|-----------|-----------|-----------|
           p |         1 |       498 |       499 | 
             |   324.689 |   615.568 |           | 
             |     0.002 |     0.998 |     0.309 | 
             |     0.001 |     0.894 |           | 
             |     0.001 |     0.309 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1056 |       557 |      1613 | 
             |     0.655 |     0.345 |           | 
-------------|-----------|-----------|-----------|

 
print(paste0(round(sum(diag(va$prop.tbl))*100, 3),'%')) #정확도 출력
[1] "96.28%"



무려 96.28%가 나왔다. 대단한 효율을 보여주지만 더 높일 수 있다.



Laplace 추정량

앞서 naiveBayes 함수에서 laplace라는 파라미터가 있었다. 이는 라플라스 추정량을 의미하는데,
이론 설명 시 우도를 구하는 공식을 참고해야 한다.

P(비아그라|스팸) × P(¬돈|스팸) × P(¬식료품|스팸) × P(주소삭제|스팸) × P(스팸)
—————————————————————————————————————– 라는 식에서
P(비아그라 ∩ ¬돈 ∩ ¬식료품 ∩ 주소삭제)

만약 P(비아그라|스팸)이 0이라면, 우도는 0이 된다. (모두 곱하기이므로)

이러한 경우 나중에 Data Set이 추가되더라도 스팸메일일때 비아그라 단어가 들어간 사건은 고려되지 않는다.
즉, 다른 증거를 거부하기 때문에 이를 해결하기 위해 프랑스 수학자 피에르 시몬 라플라스가
빈도표의 각 합계에 작은 숫자를 더해서 확률이 0이 되지 않게 하였고, 이를 라플라스 추정량이라 한다.

1로 설정하였던 라플라스 추정량을 더 작은 값인 0.01로 수정하여 모델을 생성해보자.

library(e1071)
model<-naiveBayes(train, train_label, laplace=0.1) # 모델생성
result<-predict(model, test) # 예상값 출력

library(gmodels) #이원교차표 사용을 위한 패키지 불러오기 
va<-CrossTable(result, test_label ) #이원교차표 생성

 
   Cell Contents
|-------------------------|
|                       N |
| Chi-square contribution |
|           N / Row Total |
|           N / Col Total |
|         N / Table Total |
|-------------------------|

 
Total Observations in Table:  1613 

 
             | test_label 
      result |         e |         p | Row Total | 
-------------|-----------|-----------|-----------|
           e |      1056 |        11 |      1067 | 
             |   182.915 |   346.784 |           | 
             |     0.990 |     0.010 |     0.662 | 
             |     1.000 |     0.020 |           | 
             |     0.655 |     0.007 |           | 
-------------|-----------|-----------|-----------|
           p |         0 |       546 |       546 | 
             |   357.456 |   677.690 |           | 
             |     0.000 |     1.000 |     0.338 | 
             |     0.000 |     0.980 |           | 
             |     0.000 |     0.338 |           | 
-------------|-----------|-----------|-----------|
Column Total |      1056 |       557 |      1613 | 
             |     0.655 |     0.345 |           | 
-------------|-----------|-----------|-----------|

 
print(paste0(round(sum(diag(va$prop.tbl))*100, 3),'%')) #정확도 출력
[1] "99.318%"


정확도가 96.28%에서 99.32%로 3.04%가 올라가 100%에 가까운 수치가 되었다.



결론

  • Naive Bayes는 모든 변수를 고려하면서 빠르고 높은 정확도를 보여주는 알고리즘임을
    독버섯 구분을 99.32%의 정확도를 나타내는 것으로 확인하였다.

  • Text 구분하는 Data Set / Laplace 추정량을 낮게 주었을 때 그 정확도가 더욱 커짐을 확인할 수 있었다.



반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함