R을 이용한 검색 랭킹과 검색 클러스터링 초간단 구현

KoNLP와 같이 쓰면 정말 좋은 R 패키지중에 tm이라는 아주 좋은 패키지가 있다. R에서 텍스트 분석을 한다면 이 패키지를 반드시 쓰게 되어 있다.

이 패키지의 가장 큰 장점은 텍스트를 숫자로 표현하는 대표적인 방법인 Term Document Matrix를 만들어 준다는 것이다. 이것으로 뭘 할지는 이후의 분석에 달려 있겠지만 일단 숫자로 변환된 텍스트는 다른 어떤 R패키지들을 활용하든지 적절한 통계적 분석 및 활용이 가능해지는 장점을 가진다.

이번 포스팅은 아주 작은 크기의 한글 검색 랭킹을 만들어 볼 것이다. 아마도 검색엔진의 랭킹이 어떻게 동작하는지 그 과정을 확인하는 작은 예제가 될 것 같다는 생각을 해본다.

얼마전에 r-bloggers에서 이번 주제와 상당히 유사한 영문 포스팅을 본것 같은데, 아마도 이 글을 쓴 중요한 동기가 되지 않았나 싶다.

배경 지식으로는 cosine 유사도에 대한 것이 전부이다. 이는 벡터스페이스 랭킹 모델을 사용할 거라는 의미이며, 자세한 내용은 관련 위키를 참고하길 바란다.

library(tm)
library(KoNLP)

docs <- 
    c("사랑은 달콤한 꽃이나 그것을 따기 위해서는 무서운 벼랑 끝까지 갈 용기가 있어야 한다.", 
    "진실한 사랑의 실체는 믿음이다.",
    "눈물은 눈동자로 말하는 고결한 언어.",
    "친구란 두 사람의 신체에 사는 하나의 영혼이다.",
    "흐르는 강물을 잡을수 없다면, 바다가 되어서 기다려라.",
    "믿음 소망 사랑 그중에 제일은 사랑이라.",
    "가장 소중한 사람은 가장 사랑하는 사람이다.",
    "사랑 사랑 사랑")

#편의상 검색어도 넣어준다. 
query <- "믿음을 주는 사랑"

names(docs) <- paste("doc", 1:length(docs), sep="")
docs <- c(docs, query=query)
docs.corp <- Corpus(VectorSource(docs))

#색인어 추출함수 
konlp_tokenizer <- function(doc){
  extractNoun(doc)
}

# weightTfIdf 함수 말고 다른 여러 함수들이 제공되는데 관련 메뉴얼을 참고하길 바란다. 
tdmat <- TermDocumentMatrix(docs.corp, control=list(tokenize=konlp_tokenizer,
                                                    weighting = function(x) weightTfIdf(x, TRUE),
                                                    wordLengths=c(1,Inf)))

tdmatmat <- as.matrix(tdmat)

# 벡터의 norm이 1이 되도록 정규화 
norm_vec <- function(x) {x/sqrt(sum(x^2))}
tdmatmat <- apply(tdmatmat, 2, norm_vec)

# 문서 유사도 계산 
docord <- t(tdmatmat[,9]) %*% tdmatmat[,1:8]

#검색 결과 리스팅 
orders <- data.frame(docs=docs[-9],scores=t(docord) ,stringsAsFactors=FALSE)
orders[order(docord, decreasing=T),]
##                                                                                     docs  scores
## doc6                                              믿음 소망 사랑 그중에 제일은 사랑이라. 0.38638
## doc8                                                                      사랑 사랑 사랑 0.34624
## doc2                                                      진실한 사랑의 실체는 믿음이다. 0.33481
## doc7                                          가장 소중한 사람은 가장 사랑하는 사람이다. 0.03595
## doc1 사랑은 달콤한 꽃이나 그것을 따기 위해서는 무서운 벼랑 끝까지 갈 용기가 있어야 한다. 0.02848
## doc3                                                 눈물은 눈동자로 말하는 고결한 언어. 0.00000
## doc4                                       친구란 두 사람의 신체에 사는 하나의 영혼이다. 0.00000
## doc5                                흐르는 강물을 잡을수 없다면, 바다가 되어서 기다려라. 0.00000

검색 결과 클러스터링

검색 결과를 스코어별로 클러스터링 하게 된다면 어떻게 표현될까?
아마도 아래 플로팅 결과가 그 힌트가 되지 않을까 한다. 물론 아래 함수는 유클리드언 거리를 클러스터링 하는데 사용했음에 유의하길 바란다.

fit <- hclust(dist(t(tdmatmat)), method = "ward")
plclust(fit)
rect.hclust(fit, k = 5)

plot of chunk unnamed-chunk-3

CC BY-NC 4.0 R을 이용한 검색 랭킹과 검색 클러스터링 초간단 구현 by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.

  • 受珉 李

    안녕하세요. 제일 밑에 있는 dist표가 무엇을 설명하는건가요?? 문장의 연관성에 따라 단계별로 배치되는 건가요??

  • gogamza

    triangular matrix형식으로 문서간의 거리를 계산해 결과를 리턴하는 함수 입니다. 기본으로 유클리드언 거리를 사용합니다.

  • 受珉 李

    근대 위에 예제와 같이 데이터가 아니고 텍스트일 경우에는 어떻게 삼각행렬 형식으로 거리를 계산한다는 거에요?? 단계별 클러스터를 보면 또 연관성이 커질수록 아래에 위치했는데, 어떻게 해석되는 건가요?? 제가 도저히 혼자서는 해석이 불가능해서…답변 부탁드리겠습니다. 그럼 좋은 하루 되세용~^^

  • gogamza

    tfidf로 계산된 문서들의 벡터간 거리를 계산한 결과를 상삼각행렬로 내보내고 이를 계층형 클러스터링을 수행해 시각화 한 결과입니다.

    사실 위 코드는 다양한 배경 지식을 요하는 코드라서 처음 접하는 것이라면 약간 시간 투자가 필요할듯 합니다.

  • 受珉 李

    네. 잘 알겠습니다. 위 분석이랑 관계가 없는 질문인데요. TM패키지에서 findassocs를 사용해 보셨나요?? findAssocs(dtm,”oil”,0.65)이렇게 한문서에서 분석을 진행하면 oil과 연관성이 0.65이상인 단어들이 표시되는데요, 이런 연관성들은 어떤 방법으로 계산이 되여 나오는건지 궁금합니다. 혹시 알고 계신다면 답변 부탁드립니다.

  • gogamza

    매뉴얼에도 뭘 사용했는지 나와 있지 않지만, 대략적으로 apriori rule에서 나오는 통계량에서 크게 벗어나지 않는 결과라 생각이 되네요.

  • 受珉 李

    안녕하세요. 제가 아직까지 위의 표가 이해가 되지 않는데요. 현재 보시면 doc2와 doc8, doc6, query가 하나에 묶여있는데요, 이것이 무엇을 설명할수 있나요? 또 왜 같이 묶여있을가요?유클리드언 거리를 사용한다고 햇는데 텍스트에서 유클리디언 거리는 무엇을 의미하나요??또 어떻게 구하나요??

  • 受珉 李

    만약에 도표에서의 height값을 볼려면 명령어는 어떻게 작성해야 하나요??

  • gogamza

    ?hclust 해보시면 detail부분에 관련 내용이 있습니다.

  • gogamza

    query와 유사도가 높은 문서가 doc2, doc8, doc6입니다.
    유클리드언 거리는 텍스트의 검색어를 인접행렬로 표현했을시 이들 검색어 벡터간의 거리를 각 검색어 벡터를 기반으로 유사도를 유클리드언 거리로 했다는 겁니다.

    위 코드를 보고 한꺼번에 모든 개념을 알기는 힘듦니다. 제가 추천하는 방법은 R의 명령어 메뉴얼을 읽어 보시고 관련 레퍼런스를 탐독하고 이후 코드를 수행하면서 데이터가 어떻게 변환되는지 살펴보는 것입니다.

    R, 검색 랭킹, 클러스터링을 모두 한꺼번에 위 코드에서 볼 수 있으나 모두 이해하기 위해서는 그만큼 시간 투자가 필요합니다.

  • ecmaster

    좋은 내용 감사드립니다.. 저도 실행을 해보는데 tdmat <- TermDocumentMatrix(docs.corp, …) 부분에서 Warning messages로 1: In preprocessing(sentence) : Input must be legitimate character! 와 같은 메시지가 9번 뜨고, 이후 스코어결과가 모두 0으로 나타납니다.. 무엇이 문제인지요?

  • ecmaster

    장시간 삽질^^ 끝 에 konlp_tokenizer 를 아래와 같이 수정하니 되는군요..
    konlp_tokenizer <- function(doc) { extractNoun(paste(doc, collapse = " ")) }

    버전 같은데서 차이가 있는 것인 지 모르겠습니다..
    어쨌든 많이 배우고 있습니다.. 계속 좋은 업데이트 부탁드립니다..

  • gogamza

    패키지들이 계속 업데이트 되기 때문에, 조그만 차이로 과거 코드가 동작하지 않을 경우가 많습니다. 그러나 모두 해결 가능한 문제긴 합니다.
    해결하셨다니 다행이네요.. ^^

  • 장원태

    마지막 검색결과 클러스팅에서 코드가

    fit <- hclust(dist(t(tdmatmat)),method = "ward.D")로 수정되어야 실행가능하더군요

    패키지가 업데이트 되면서 수정된것 같습니다~

  • 감사합니다. ^^

  • cindyre

    > orders <- data.frame(docs=docs[-9],scores=t(docord) ,stringsAsFactors=FALSE)
    # 이렇게 입력하였더니
    Error in data.frame(docs = docs[-9], scores = t(docord), stringsAsFactors = FALSE) :
    arguments imply differing number of rows: 10, 8
    이렇게 뜨네요..선생님 메뉴얼 보고 많이 공부하고 있는데 아직도 많이 부족해서 그런가 틀린부분을 찾기가 어렵네요…파일을 여니 row가 8로 입력되어 있습니다. 어디가 틀린지 잘 모르겠네요..

  • 정민호

    안녕하세요. 고감자님? 올려주신 포스팅을 보며 좀 더 공부하다 다른방법을 제안해 봅니다. 고감자님이 사용하신 방법은 가중치 tdm을 cosine 유사도로 계산하는 방식이였는대요, 차라리 가중치를 부여하지 않은 일반적인 빈도 tdm에 Tanimoto dist.를 적용하는것도 괜찮지 않을까요? 정리하자면, tdm에서 0의 빈도를 고려하지 않고 단어의 빈도가 발현된 부분에 좀 더 영향력을 주는 것이 바람직하지 않을까 해서 이렇게 여쭤봅니다.

  • 정민호

    findAssocs 함수는 correlation에 기반하여 만들어졌네요(관련링크 http://r.789695.n4.nabble.com/findAssocs-td3845751.html). dtm이 빈도로 이루어졌다면 findAssocs 함수보다 다른 유사도 계수를 적용하는 것이 좋지 않을까 생각됩니다.

  • 좋은 아이디어네요. 랭킹에 대한 검증은 골든셋으로 그 정도를 검증해야 맞습니다. 그런 뒤에 방안을 논의 하는게 올바른 접근 방식이죠… 그런 과정을 통한다면 말씀하셨던 방법 이외에도 다른 좋은 아이디어들이 나올 것으로 예상됩니다.