brunch

You can make anything
by writing

C.S.Lewis

by 워녁s토리 Aug 24. 2017

스팸인지 아닌지  맞춰 볼게

데이터분석을 통한 스팸메시지 감별 작업


  어느 순간부터 휴대폰이 스팸 메시지를 알아서 잘 걸러주고 있다. 스팸 의심 메시지라고 알려주거나, 스팸 메시지함으로 자동으로 옮겨준다. 덕분에 스팸 메시지를 신경 쓸 일이 줄어들었다. 그동안 이 고마운 서비스의 혜택은 잘 누리고 있으나, 누가 어떤 원리로 이 기술을 만들었는지 의문을 품어본 적이 없다. 이 기술이 만들어지는 원리에 대해서는 조금 더 곰곰이 생각을 해 보아야만 했는데, 빠르게 떠오른 몇 가지 생각은 다음과 같다. 


스팸을 구분하는 방식을 유추해보자면...


1. 다수의 사용자들이 스팸으로 등록한 번호는 스팸 발신자로 지정 

2. 불특정 다수에게 다량으로 송신되는 메시지는 스팸으로 분류

3. 메시지 내용을 분석하여 스팸으로 분류



데이터 사이언스를 공부하기 시작하면서 이렇게 당연시했던 기술에 대해서 생각해 보는 경우가 늘었다. 마침 방학이 시작했고, 각종 데이터셋을 모아둔 kaggle.com을 서핑하던 중 흥미로운 것을 발견했다. SMS SPAM Collection이라는 데이터셋을 본 것이다.

 https://www.kaggle.com/uciml/sms-spam-collection-dataset 


이 파일은 영어로 된 5574개의 SMS 메시지 내용을 가지고 있었고, 각 메시지마다 스팸이었는지 일반 메시지였는지 태그를 달아두었다. 데이터셋 자체가 그렇게 부담스럽지 않은 크기였고, 비정형-텍스트 데이터를 처리해 본 경험이 부족하였기에 바로 다운로드하여 도전해보았다. 


원본 데이터의 모습이다. 1열에는 spam or ham 태그,  2열에는 메시지 내용이 들어있다.


우선 앞서 생각해 두었던 스팸 메시지 분류 방식을 다시 살펴보자. 이 데이터셋에는 몇 명의 사용자들이 스팸으로 등록하였는지에 대한 정보가 없다. 뿐만 아니라 누구에게, 몇 명에게 메시지를 보냈는지 송신자와 수신자에 대한 기록 또한 없다. 따라서 1번과 2번의 방식대로 스팸 메시지를 분류하는 것은 안 되겠다. 따라서 3번 방식으로 데이터 분석을 진행해야 할 것이다. 나는 스팸 메시지에만 유독 자주 포함되는 단어들이 있을 것이라 생각했고, 그것들을 변수로 사용하여 분류하는 모델을 만들기로 정했다. 


본격적으로 분석을 진행하기 앞서 데이터셋을 잘 살펴보면, 사람들이 일상적으로 사용하는 채팅용어들이 많이 보이며 오탈자도 꽤나 많이 보인다. 이는 텍스트 분석을 어렵게 만드는 주요인인데, 다행히 이번 연구에서의 목적은 스팸의 여부만 구분하면 되기 때문에 큰 장애요소로 보이지 않는다. (적어도 특정 단어를 변수로 설정하여 분류하는 방법에 한해서)


V1열에서 spam으로 분류된 메시지들을 따로 떼어놓고 살펴보면, 스팸 메시지들의 특징이 보인다. 'Urgent!', 'Congrats' 등 이목을 끌기 위한 특정 단어들이 분포하여 있다. 여기서 힌트를 얻어서 머신러닝 모델을 만들고 스팸 메시지를 구분해낼 수 있겠다. 스팸 메시지에는 많이 들어있고 정상적인 메시지에는 거의 포함되어 있지 않은 그런 특별한 단어들을 찾아내어 분류를 해보자.


본격적인 모델링을 하기 앞서, 전처리를 해주었다. 굉장히 지루하고 귀찮은 작업이지만 전체 성능을 좌우할 만큼 중요한 단계이다. 정규표현식과 gsub 함수를 이용하여 대문자는 모두 소문자로 바꿔주고, 특수문자나 구두점을 모두 제거해주고 그리고 숫자나 제어문자를 모두 제거해주었다. (알고 보니 tm 패키지에 전처리를 해주는 함수들이 다 내장되어 있었다. removePuctuation 등의 명령어를 사용하면 훨씬 빠르고 편리하다.)


지금까지는 한 행당 하나의 문장이 들어가 있었는데, 이것을 스페이스바를 기준으로 단어 단위로 쪼개 준다. 그리고 새로운 벡터에 저장을 해주면 데이터셋에 포함된 전체 단어들이 하나의 변수에 저장이 된다. 약 8만 개의 단어가 모였는데, unique 명령어로 중복된 단어들을 제거해주면 8262개만 남는다. 훨씬 효율적이고 빠른 분석이 가능하게 만들었다.  


단어들로만 모인 벡터형 변수를 만들었고, 눈으로 훑어가며 스팸 메시지에 유독 많이 보이는 단어들을 몇 개 뽑아보았다. 아까 위에서 언급했듯이, "Congrats!"라는 단어를 사용하며 무언가 당첨되었다는 메시지가 있었다. 스팸의 냄새가 나는 것들이었고, 역시나 스팸으로 분류된 것이었다. 그렇게 눈으로 훑어 가며 스팸 메시지로 분류된 문서에서 많이 보이는 단어들을 직접 골라 선별했다. 그런데 아무리 생각해보아도, 이 작업은 분석자의 직관에 100% 의존하는 방법이며, 정확도 또한 높게 나올 리가 없었다. 더군다나 내가 일일이 직접 단어를 선별해야 했기에 효율적이지도 않았다. 


좀 더 컴퓨터스러운 방법이 없을까 고민을 하다가, 괜찮은 아이디어가 떠올랐다.

각 문서군에서 노출되는 단어의 빈도 차를 구하는 것이다. 

 

스팸 메시지에서의 특정 단어 노출 빈도 - 일반 메시지에서의 특정 단어 노출 빈도


스팸 메시지에서는 자주 발견되는 단어가 일반 메시지에서는 거의 보이지 않는다면, 그것은 스팸 여부를 분류하기에 최적의 단어가 되는 셈이다. 먼저 전체 문서에서 스팸메시지, 정상메시지만 따로 모여있는 문서군을 만들었다. 그리고 약 8000개의 단어를 순서대로, 노출 빈도를 비교해보았다. 


Dis <- data.frame(dif = (0:NROW(WL)), word = (0:NROW(WL)))

spam_spam <- filter(spam, spam$v1 == "spam")

spam_ham <- filter(spam, spam$v1 == "ham")

for(i in 1: NROW(WL)){

    sum_spam <- sum(str_detect(spam_spam$v2, WL[i]))

    sum_ham <- sum(str_detect(spam_ham$v2, WL[i]))

    Dis[i, ]$dif <- (sum_spam) - (sum_ham)

    Dis[i, ]$word <- WL[i] 

    cat(paste(round(i/NROW(WL), digits=3), "calculate.",paste(i,"times done.\n")))

    }


결과는 다음과 같다.


> head(arrange(Dis, desc(dif)), 50)

    dif       word

1  8262       8262

2   249         鶯

3   178        mob

4   162        txt

5   136         xt

6   133       free

7   132     mobile

8   123         pm

9   116      claim

10  106        box

11  104        ree

12   95        www

13   89      prize

14   83         cs

15   78         uk

16   77         iz

17   73       tone

18   70       stop

19   67    service

20   64        top

21   62       land

22   60       call

23   60        ppm

24   57      award

25   57        vic

26   56       cust

27   55       cash

28   55     urgent

29   55      rgent

30   54      reply

31   54      nokia

32   54        nok

33   53        sub

34   51      pobox

35   50       text

36   50 guaranteed

37   47    contact

38   46       lect

39   46       line

40   46        gua

41   46         rv

42   46        tex

43   45        opt

44   44   customer

45   43       subs

46   42       gent

47   42     custom

48   41    collect

49   40   ringtone

50   40        tcs


첫번째 열 dif가 특정단어가 스팸메시지군에서 나타나는 빈도 - 그 단어가 정상메시지군에서 나타나는 빈도를 나타낸다. 그런데 가장 많은 차이를 보여준 8262는 어쩌다 생긴 것인지 도저히 모르겠다...ㅜㅠ 저 8262라는 숫자는 단어를 담고 있는 벡터 WL 의 길이인데(즉 총 단어의 갯수)... 그만큼 하나씩 뭔가 입력되어 산출된 값인 듯하다. 


여하튼 이 단어들을 분석하고 판단하는 것이 연구자의 몫이라고 생각한다.

잘 살펴보면, 3위에 mob, 7위에는 mobile 이 있고, 6위에는 free, 11위에는 ree라는 단어가 있다. 사전적 뜻으로 mob는 폭력배를 뜻하는 단어지만, 스팸과는 전혀 관계가 없는 단어이다. mob이라는 단어가 쓰인 문장들을 보면 mobile 의 줄임말로 쓰이고 있다는 것을 알 수 있다. 11위의 ree 같은 경우도 마찬가지이다. 아무런 의미가 없는 단어이지만 띄어쓰기, 오탈자 때문에 생긴 단어이다. 이런 부분들을 모두 감안하여 단어 선택을 해야 한다.


mobile 의 의미로 사용된 mob



그리고 22위인 call과 24위인 award를 살펴보자. 각각 60과 57로 3개의 차이만 있을 뿐인데, 구체적으로 한 번 확인해보아야 한다. 왜냐하면 스팸메시지군에서만 배타적으로 많이 나오는 단어가 더욱 좋기 때문이다. 


> sum(str_detect(spam_spam$v2, "award"))

[1] 58

> sum(str_detect(spam_ham$v2, "award"))

[1] 1

> sum(str_detect(spam_spam$v2, "call"))

[1] 348

> sum(str_detect(spam_ham$v2, "call"))

[1] 288


award는 spam으로 분류된 문서에서 58개, ham에서는 1개만 확인되었다.

반면 call은 spam 문서에서 348개나 확인되었고 ham에서도 288개가 확인되었다. 

분류모델을 사용할 때 call보다 award가 더욱 정확한 성능을 낼 것일 것이다. 따라서 이러한 부분을 모두 고려하여 변수로 사용할 단어들을 골라주어야 한다.  이를 반복하면서 총 12개의 단어들을 골라냈다. 그 단어들은 다음과 같다

 free, award, urgent, cash, mobile, tone, txt,  guaranteed, bonus, voucher , 鶯, www


이제 위의 단어들을 머신러닝 모델을 학습시키기 위한 변수로 추가시켜줘야 한다.


spam$free <- 0

for(i in 1: nrow(spam)){

    if(str_detect(spam$v2[i], "free")){

        spam$free[i] <- 1 

    }  else {

        spam$free[i] <- 0

    }

}


해당 행에 "free"라는 단어가 포함되어 있으면 1을 넣고 그렇지 않다면 0을 넣는 코드이다. 같은 코드를 반복하여 변수를 추가시키면 다음과 같은 데이터셋이 만들어진다. 확실히 spam인 메시지에서 1이 체크된 단어가 많아 보인다. 이런 1들이 나중에 스팸 메시지인지 아닌지 가려주는 역할을 한다. 



12개의 단어 포함여부가 변수로 추가된 데이터프레임 형식의 데이터셋.


아직 머신러닝은 시작도 안 했는데 벌써 다 한 것 같고 지친다. 


아래부터는 본격적인 기계학습을 이용한 분류작업 내용이다. 사용한 기법은 의사결정나무의 C5.0과 인공신경망 nnet을 사용하였다. 학습한 데이터만 잘 처리하고 새로운 데이터에 대해서는 낮은 정확도를 내는 과적합(Overfitting) 문제를 방지하기 위해 7:3의 비율로 학습데이터와 시험데이터로 나누었다. 


1. C50 의사결정 나무를 이용한 분류모델.


의사결정나무는 특정 분류 기준을 토대로 항목들을 나누는 것이다. 예를 들어 쓰레기를 분리수거 하는 의사결정나무를 만든다면, "쓰레기에서 나는 광택의 정도"라는 변수를 기준으로 병류, 병이 아닌 것들로 나눌 수 있겠다. 그리고 하위 노드에서 계속되는 기준에 따라 분류가 되는 원리이다. 분류 알고리즘에 따라 rpart, C50, ctree 등 다양한 패키지들이 존재하는데, 나는 C50 패키지를 이용하여 학습시켰다. 


plot 을 그려보려고 시도했지만...자꾸 원인 모르는 에러메시지가 나와서 보여주지 못한 것이 아쉽다.




2. 인공신경망을 이용한 분류모델


인공신경망은 인간 두뇌의 정보 처리방법을 모방하여 만든 기법이다. 딥러닝 분야의 토대가 되는 기술이라고도 한다. 인공신경망 기법의 단점은 '설명이 불가능하다'는 점이다. 원리는 인간의 두뇌를 모방하여 만들 수 있었지만, 어떻게 작동하고, 최적화를 할 수 있는가에 대한 부분은 여전히 학계에서 활발히 연구 중이다. 


나는 12개의 단어들을 input 변수로 놓고, 스팸& 정상메시지를 output 변수로 두었다. 그리고 히든노드는 10개로 설정한 후 단층 인공신경망을 만들어보았다. 인공신경망의 plot은 아래와 같다.


인공신경망의 모습



3. 의사결정나무와 인공신경망의 결과


좌측이 의사결정나무의 혼동행렬(Confusion Matrix) 모습이고 우측이 인공신경망의 결과이다. 


결과를 분석해보자.


각각 약 94%, 98% 의 정확도를 나타냈다. 특히 인공신경망은 98%의 Accuracy와 100%의 Specificity를 자랑한다. 정말 놀라운 수치이다. 이는 스팸메시지를 정상메시지라고 분류할 확률은 0%인 것이다. 다시 말해 광고문자는 100% 걸러내어 사용자를 귀찮게 하지 않는다는 뜻이다. 하지만 Sensitivity지수를 보면 차이가 있다. 의사결정나무에서의 민감도는 99%인 반면 인공신경망에서는 97%이다. 여전히 둘 다 높은 수치이지만 이는 큰 차이를 나타낸다. 여기서 민감도가 나타내는 것은 정상인 메시지 중 정상인 것으로 분류된 수이다. 즉 민감도 수치가 낮으면 정상인 메시지를 스팸으로 분류할 경우가 많다는 것을 의미한다. 내 친구가 보낸 메시지가 스팸메시지함에 들어가 있어서 보지 못하는 낭패가 생기면 곤란하다. 따라서 나는 의사결정나무의 성능이 더 좋다고 평가하고 싶다. 





4. 한계 및 보완해야 할 점


높은 정확도를 내고도 불안한 부분이 많다. 우선 변수를 만들고 전처리 하는 과정에서 연구자의 손을 너무 많이 탄다. 단어를 선택하는 과정에서 연구자의 의도가 개입되었다고 할 수도 있겠다. 단어 빈도 차이 1위부터 30위까지 전부 다 변수로 사용하면 좋았겠지만....띄어쓰기, 오탈자의 문제 때문에 연구자가 직접 선별할 수밖에 없었다. 하지만 이는 나중에 데이터셋 조작에 대한 시비가 붙을 가능성이 있다고 생각한다.


무엇보다도 내 능력에 대한 한계를 지적할 수밖에 없을 텐데.... 비정형 텍스트를 전처리하는 지식이 딱 거기까지 였던 것이다. 스팸메시지에 많이 포함된 단어들을 변수로 만드는 방법은 '자동화'부분과 어울리지 않다. 그냥 센서의 역할 정도밖에 못한다고 생각된다. 내가 직접 12개의 단어를 선별했다는 것은 나쁘게 보면 연구자의 개입이라 할 수 있기 때문이다. 더 좋은 정확도를 내기 위해 인위적인 선택을 했기 때문이다. 데이터분석에서는 이런 개입을 최소화 하는 것이 좋다. 그리고 과적합 문제에 대한 우려가 있다. 예를 들어 free, award, cash 라는 단어가 포함된 메시지는 꼼짝없이 스팸메시지로 분류될 것이다. 하지만 친구나, 가족이 보낸 문자라면 큰 문제가 될 것이다. 이러한 방식이 고작 5000여개의 메시지 자료에서는 유효하여 높은 정확도를 냈지만, 스팸 전송 업체들이 이 로직을 간파하고 다른 단어를 쓴다면 걸러낼 방법이 없어지는 것이다. 즉 과적합 문제가 생긴다는 것이다. 

 이것보다 진일보한 방식으로는 TF-IDF(Term Frequency- Inverse Document Frequency)라는 텍스트 마이닝 기법이 있다. 이는 여러 문서군에서 한 단어의 특정 문서에서의 중요도를 나타내 주는데, 단어들의 가중치를 알려준다. 통계적으로 어떤 단어가 중요한 지 알아낼 수 있는 방법이다. 아마 정확도는 내가 했던 방식보다 낮아지겠지만, 연구자의 개입을 최소화하고 과적합문제를 줄이는 데에는 훨씬 좋을 것이다. 






# 한창 공부 중인 학부생입니다. 피드백은 항상 환영합니다!


# kaggle.com 에서 일부 아이디어를 따왔습니다.


# 모델의 성능을 판별하는 지표 Accuracy, Sensitivity(Recall), Specificty, f-score 등은 나중에 한꺼번에 정리하여 포스팅할 예정입니다.  저도 은근히 헷갈리는 부분이 많고 가끔 잊어먹기 때문에 정리해놓고 자주 볼 생각입니다 :).





브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari