Survey를 진행하다보면 주관식 문항에 대해 워드클라우드를 요청하는 고객사가 정말 많다.
워드클라우드는 해석에 그렇게 좋지 않은 그래프임에도 무언가 꽉찬 느낌에 화려하여 보고서에서 한 장 차지하기 딱 좋아보여서 그런 것 같다.
근래 가장 많은 도움을 받고 있는 책인 ‘Do it! 쉽게 배우는 R 데이터 분석(김영우, 2017)’에 따르면 워드클라우드는 분석 결과를 정확하게 표현하는데는 정확하지 않다고 한다. 본문 중 워드클라우드에 대한 구체적인 평가 내용은 아래와 같다.
워드 클라우드는 디자인이 아름다워서 자주 사용되지만 분석 결과를 정확하게 표현하는 데는 적합하지 않다. 단어 빈도를 크기와 색으로 표현하므로 ‘어떤 단어가 몇 번 사용 되었는지’ 정확히 알 수 없고, 단어 배치가 산만해서 ‘어떤 단어가 다른 단어보다 얼마나 더 많이 사용되었는지’ 비교가 어렵다. 텍스트를 아름답게 표현하는 게 아니라 분석 결과를 정확하게 표현하는 목적이라면 워드 클라우드보다는 막대 그래프를 이용하는게 좋다.
고객사에게 해석의 어려움에 대해 설명 드려도 일단 해달라는 요청도 많다. 그래서 어떻게든 조금의 해석이 이루어질 수 있도록 하려다보면 전처리에 굉장히 심혈을 기울여야 한다.
“자연어처리 분야가 당연히 전처리를 꼼꼼히 잘 해야하는 것 아니냐”라고 묻는 분도 계시겠지만 워드클라우드를 일단 만들어 달라는 고객사의 요청에 부응하기 위해서는 인사이트의 실마리가 필요하다. 또 그 실마리를 찾기 위해서는 ‘진짜 정말로’ 전처리를 어떻게든 잘 해내야 한다.
그러다보니 데이터를 분석하면서 전처리에 평소보다 더 많은 시간을 할애하게 되었고 공부했다. 공부하며 얻게 된 지식의 일부를 명확하게 정리하고자 본 글을 쓰게 되었다.
(아 물론 이 글의 주제는 NLP의 매우 기본적인 내용이다)
주제는 Stemming & Lemmatization이다.
고객사에게 인사이트를 제공하기 위한 실마리를 찾는다는 것은 다른 말로 조사 참여자들의 응답 속에서 “의미를 찾는 것”이라 할 수 있다. 따라서 텍스트 안에서 객관적인 의미를 갖는 단어들을 추출해야 한다.
그 의미를 갖는 단어들을 최대한 일반화하여 요약하는 것을 정규화(normalization)라고 한다. 그 기법에는 크게 2가지가 있는데 그것이 바로 이 글의 주제인 Stemming(어간 추출)과 Lemmatization(표제어 추출)이다.
어간(stem)의 사전적 의미는 다음과 같다.
활용어가 활용할 때에 변하지 않는 부분. ‘보다’, ‘보니’, ‘보고’에서 ‘보-’와 ‘먹다’, ‘먹니’, ‘먹고’에서 ‘먹-’ 따위이다.
출처: 네이버 사전
자연어처리 분야의 관점에서 의미는 다음과 같다.
단어의 의미를 담고 있는 단어의 핵심 부분
본 프로젝트는 통계 프로그램 R을 활용하였다. 어간 추출에 대한 정확한 개념을 모를 때는 구글링에서 얻은 코드를 막 썼다가 엉망진창인 워드클라우드가 나오기도 했다.
아래는 실제 사용했던 코드이다. 특정 목적에 따라 설계된 설문조사였기 때문에 각 문항을 별도로 분석해야했다. 따라서 1개의 문항을 별도의 객체로 생성하고 말뭉치(corpus)화 했다.
말뭉치란 “언어 연구를 위하여 컴퓨터가 텍스트를 가공·처리·분석할 수 있는 형태로 모아 놓은 자료의 집합”이다(한국민족문화대백과사전). R의 tm 라이브러리에서 문서를 관리하는 기본적인 단위이기도 하다.
Q1 <- df$Q101 #Q1문항을 별도의 객체로 생성
docs < - Corpus(VectorSource(text_101)) #말뭉치 생성
어간추출과 직접적으로 관련된 함수는 tm 라이브러리의 tm_map 함수이다. tm_map 함수를 사용하여 단어의 원형이 되는 어간을 추출하였다.
tm_map(처리하고자 하는 텍스트, 함수)
tm_map 함수 내 텍스트를 처리하는 함수는 ‘stemDocument’를 사용하였는데 이 함수는 Poter의 어간 추출 알고리즘을 사용한다. 해당 알고리즘의 자세한 내용은 아래 논문을 참고하길 바란다.
원문: Porter, Martin F. "An algorithm for suffix stripping." Program (1980).
toSpace <- content_transformer(function (x , pattern ) gsub(pattern, " ", x))
docs <- tm_map(docs, toSpace, "/")
docs <- tm_map(docs, toSpace, "@")
docs <- tm_map(docs, toSpace, "\\|")
docs <- tm_map(docs, toSpace, "-")
docs <- tm_map(docs, toSpace, ":")
#library(dplyr)
docs <- docs %>%
tm_map(removeNumbers) %>% #숫자 제거
tm_map(removePunctuation) %>% #구두점 제거
tm_map(stripWhitespace) #공백 제거
docs <- tm_map(docs, content_transformer(tolower)) #소문자로 변환 -> 사전(Dictionary)의 내용과 대조 목적
docs <- tm_map(docs, removeWords, stopwords("english")) #띄어쓰기와 시제 제거
#원하는 단어 직접 제거
docs <- tm_map(docs, removeWords, c("직접 제거할 단어"))
# Text stemming: 어간 추출 추출하는 작업
docs_101 <- tm_map(docs, stemDocument)
tm_map(docs, stemDocument)
#함수 정의
stemCompletion_mod <- function(x,dict=dictCorpus) {
PlainTextDocument(stripWhitespace(paste(stemCompletion(unlist(strsplit(as.character(x)," ")),dictionary=dict, type="shortest"),sep="", collapse=" ")))
}
docs <- lapply(docs_101, stemCompletion_mod, dict=docs)
docs <- VCorpus(VectorSource(docs))
Term Document Matrix(이하 TDM)란 다수의 문서에 등장하는 각 단어들의 빈도를 행렬로 나타낸 것을 의미한다. TDM을 matrix로 변환한 뒤 모든 문서, 즉 모든 열에 카운팅된 각 키워드의 빈도를 합산하여 내림차순으로 정렬하였다. 이후 word와 freq로 명명된 2개의 변수가 있는 ‘df101’이라는 빈도 순위표를 생성하였다.
어간 추출을 했음에도 키워드들이 딱 떨어지지 않아 보다 더 명확하게 정규화하기 위해 어간추출로 나온 테이블에 ‘표제어추출’을 적용하였다.
표제어 추출은 R패키지 texstem의 lemmatize_word 함수를 사용하였다. dictionary 옵션에는 lexicon 외에도 많은 사전이 존재하니 도움말을 참고해보길 바란다.
dtm <- TermDocumentMatrix(docs) #TDM으로 변환
matrix <- as.matrix(dtm) #Matrix로 인식하도록 변환
words <- sort(rowSums(matrix),decreasing=TRUE) #1:n 열을 합산한 뒤 내림차순 정렬
df101 <- data.frame(word = names(words),freq=words) #변수명 설정
#표제어 추출
df101 <- df101 %>%
mutate(word101=lemmatize_words(df101$word, dictionary = lexicon::available_data())) %>%
group_by(word=word101) %>%
summarise(freq=sum(freq)) %>%
arrange(-freq)
dtm$dimnames$Terms <-lemmatize_words(dtm$dimnames$Terms)
본 글에서는 어간추출, 표제어 추출의 개념을 간략하게 설명하고 바로 적용해볼 수 있는 코드를 소개하였다.
매우 기본적인 과정이지만 개념과 R코드를 일목요연하게 정리해놓은 글은 거의 보이지 않아 직접 글을 작성하게 되었다.
주관식 문항의 데이터를 처리 과정에서 고통 받는 리서치 연구원에게 1이라도 도움이 되길 희망한다.