1 셜록홈즈 소설 1

셜록홈즈 소설 데이터를 구텐베르그 프로젝트 웹사이트에서 다운로드 받아 텍스트 데이터 분석을 위해 데이터를 정제하고 단어구름을 통해 탐색적 데이터분석 작업을 수행하고 나서, 이를 TF-IDF로 중요한 단어를 식별하고 이를 토픽모형까지 연결시켜보자.

Julia Silge 블로그에 내용을 바탕으로 원본 텍스트데이터를 가져와서 이를 tidytext 방식으로 토픽모형(topic model)까지 구축하는 과정을 실습한다.

1.1 텍스트 데이터

gutenbergr 팩키지를 설치하고 나서 셜록홈즈 소설을 다운로드 받는다.

> library(tidyverse)
> library(gutenbergr)
> 
> sherlock_raw <- gutenberg_download(1661)
> 
> sherlock_raw
# A tibble: 12,648 x 2
   gutenberg_id text                             
          <int> <chr>                            
 1         1661 THE ADVENTURES OF SHERLOCK HOLMES
 2         1661 ""                               
 3         1661 by                               
 4         1661 ""                               
 5         1661 SIR ARTHUR CONAN DOYLE           
 6         1661 ""                               
 7         1661 ""                               
 8         1661 ""                               
 9         1661 "   I. A Scandal in Bohemia"     
10         1661 "  II. The Red-headed League"    
# ... with 12,638 more rows

1.2 텍스트 데이터 정제

1.2.1 스토리 단위 구분

ADVENTURE가 포함된 text를 기준으로 문장을 스토리 단위로 구분한다. 그리고 나서 스토리를 요인형 자료로 변환하여 다음 분석을 위한 준비를 한다. 전체 12개 스토리 단위로 구분됨이 확인된다.

> sherlock <- sherlock_raw %>%
+     mutate(story = ifelse(str_detect(text, "ADVENTURE"),
+                           text,
+                           NA)) %>%
+     tidyr::fill(story) %>%
+     filter(story != "THE ADVENTURES OF SHERLOCK HOLMES") %>%
+     mutate(story = factor(story, levels = unique(story)))
> 
> sherlock %>% 
+     count(story)
# A tibble: 12 x 2
   story                                          n
   <fct>                                      <int>
 1 ADVENTURE I. A SCANDAL IN BOHEMIA           1130
 2 ADVENTURE II. THE RED-HEADED LEAGUE         1109
 3 ADVENTURE III. A CASE OF IDENTITY            803
 4 ADVENTURE IV. THE BOSCOMBE VALLEY MYSTERY   1129
 5 ADVENTURE V. THE FIVE ORANGE PIPS            895
 6 ADVENTURE VI. THE MAN WITH THE TWISTED LIP  1106
 7 VII. THE ADVENTURE OF THE BLUE CARBUNCLE     979
 8 VIII. THE ADVENTURE OF THE SPECKLED BAND    1208
 9 IX. THE ADVENTURE OF THE ENGINEER'S THUMB    967
10 X. THE ADVENTURE OF THE NOBLE BACHELOR      1026
11 XI. THE ADVENTURE OF THE BERYL CORONET      1129
12 XII. THE ADVENTURE OF THE COPPER BEECHES    1143

1.2.2 스토리 → 단어

스토리를 기준으로 문장단위로 구분을 했으면, 다음 단계로 문장을 단어 단위로 잘라낸다. 이를 위해서 unnest_tokens() 함수를 사용한다. 그리고 불용어 사전에 등록된 stop_words를 사용해서 불용어를 제거하고 homes 단어도 너무 많이 출현되어 이를 제거하여 깔끔한 텍스트 데이터로 정제한다.

> library(tidytext)
> 
> tidy_sherlock <- sherlock %>%
+     mutate(line = row_number()) %>%
+     unnest_tokens(word, text) %>%
+     anti_join(stop_words) %>%
+     filter(word != "holmes")
> 
> tidy_sherlock %>%
+     count(word, sort = TRUE)
# A tibble: 7,437 x 2
   word        n
   <chr>   <int>
 1 time      151
 2 door      144
 3 matter    125
 4 house     123
 5 hand      120
 6 night     114
 7 heard     113
 8 found     108
 9 day       106
10 morning   102
# ... with 7,427 more rows

2 단어구름

wordcloud 팩키지 wordcloud() 함수로 전체에 대한 출현빈도가 높은 단어로 단어구름을 생성할 수 있다.

> library(wordcloud)
> library(ggrepel)
> 
> tidy_sherlock %>%
+     count(word, sort = TRUE) %>% 
+     with(wordcloud(word, n, max.words = 100,  min.freq = 1))

ggplot2를 사용해서 각 스토리(facet)별로 단어구름을 생성하여 탐색적으로 비교하는 것도 가능하다. 2

> tidy_sherlock %>% 
+     count(story, word, sort = TRUE) %>% 
+     filter(n > 10) %>% 
+     ggplot(aes(x = 1, y = 1, size = n, label = word)) +
+         geom_text_repel(segment.size = 0, force = 100, segment.color = 'white', segment.alpha = 0.01) +
+         scale_size(range = c(2, 10), guide = FALSE) +
+         scale_y_continuous(breaks = NULL) +
+         scale_x_continuous(breaks = NULL) +
+         labs(x = '', y = '', title = "Word Cloud of the Sherlock Holmes Stories") +
+         facet_wrap(~ story) +
+         # facet_grid(.~ story) +
+         theme_classic() +
+         theme(strip.text = element_text(color="red", size=16, lineheight=5.0),
+               plot.title = element_text(colour = "blue",
+               size = 18, hjust = 0.5, vjust = 0.8, angle = 0)) 

3 소설 분석

3.1 tf-idf 단어

tf-idf 분석을 통해 12개 스토리 중에서 중요한 단어를 추출할 수 있게 도와준다. 많이 출현한다고 해서 중요한 단어가 아니라 상대적으로 중요한 단어를 식별할 수 있게 된다. tf-idf 분석은 토픽 모형개발로 진행하기 전에 중요한 역할을 수행하게 된다.

> # devtools::install_github("dgrtwo/drlib")
> library(drlib)
> 
> sherlock_tf_idf <- tidy_sherlock %>%
+     count(story, word, sort = TRUE) %>%
+     bind_tf_idf(word, story, n) %>%
+     arrange(-tf_idf) %>%
+     group_by(story) %>%
+     top_n(10) %>%
+     ungroup
> 
> sherlock_tf_idf %>%
+     mutate(word = reorder_within(word, tf_idf, story)) %>%
+     ggplot(aes(word, tf_idf, fill = story)) +
+     geom_col(alpha = 0.8, show.legend = FALSE) +
+     facet_wrap(~ story, scales = "free", ncol = 3) +
+     scale_x_reordered() +
+     coord_flip() +
+     theme(strip.text=element_text(size=11)) +
+     labs(x = NULL, y = "tf-idf",
+          title = "Highest tf-idf words in Sherlock Holmes short stories",
+          subtitle = "Individual stories focus on different characters and narrative elements")

3.2 토픽 모형

tf-idf 분석을 이어 토픽 모형으로 진행하는데 팩키지가 필요한데 과거 속도, 자바 의존성이 문제가 되어 번거러웠으나, stm 팩키지의 등장으로 C++로 작성되어 자바 의존성과 속도 문제가 모두 해결되고 전문가들의 평판도 좋다.

stm()함수는 인자로 문서-단어 행렬(document-term matrix) 혹은 quanteda 팩키지 희소 행렬(sparse matrix) 혹은 dfm 자료형을 전달하여야 한다. 깔끔한 텍스트 데이터로 정제된 것을 문서(스토리)-단어 행렬로 count(), cast_dfm() 함수를 연결하여 구현해낸다. 동일하게 count(), cast_sparse() 함수를 연결하여 희소행렬도 구현할 수 있다.

> library(quanteda)
> library(stm)
> 
> sherlock_dfm <- tidy_sherlock %>%
+     count(story, word, sort = TRUE) %>%
+     cast_dfm(story, word, n)
> 
> sherlock_sparse <- tidy_sherlock %>%
+     count(story, word, sort = TRUE) %>%
+     cast_sparse(story, word, n)

stm() 함수에 토픽을 6개(K = 6)로 정하여 토픽모형 결과를 객체에 전달하면 summary(topic_model)을 통해서 결과를 확인할 수 있으나, 후속 데이터 분석을 위해서 tidy() 함수로 데이터를 변환하고 나서 파이프 연산자를 결합하여 후속 분석 작업을 매끄럽게 진행한다.

> topic_model <- stm(sherlock_dfm, K = 6, 
+                    verbose = FALSE, init.type = "Spectral")
> 
> td_beta <- tidy(topic_model)
> 
> td_beta %>%
+     group_by(topic) %>%
+     top_n(10, beta) %>%
+     ungroup() %>%
+     mutate(topic = paste0("Topic ", topic),
+            term = reorder_within(term, beta, topic)) %>%
+     ggplot(aes(term, beta, fill = as.factor(topic))) +
+     geom_col(alpha = 0.8, show.legend = FALSE) +
+     facet_wrap(~ topic, scales = "free_y") +
+     coord_flip() +
+     scale_x_reordered() +
+     labs(x = NULL, y = expression(beta),
+          title = "Highest word probabilities for each topic",
+          subtitle = "Different words are associated with different topics")

다음으로 각 토픽별로 각 문서가 생성될 확률을 시각화해보자. 토픽 모형결과 산출된 결과값을 통해 가능하다.

> td_gamma <- tidy(topic_model, matrix = "gamma",                    
+                  document_names = rownames(sherlock_dfm))
> 
> td_gamma %>% filter(topic == 5, gamma > 0.5)
# A tibble: 3 x 3
  document                                 topic gamma
  <chr>                                    <int> <dbl>
1 VIII. THE ADVENTURE OF THE SPECKLED BAND     5 1.000
2 XI. THE ADVENTURE OF THE BERYL CORONET       5 1.000
3 XII. THE ADVENTURE OF THE COPPER BEECHES     5 1.000
> ggplot(td_gamma, aes(gamma, fill = as.factor(topic))) +
+   geom_histogram(alpha = 0.8, show.legend = FALSE) +
+   facet_wrap(~ topic, ncol = 3) +
+   labs(title = "Distribution of document probabilities for each topic",
+        subtitle = "Each topic is associated with 1-3 stories",
+        y = "Number of stories", x = expression(gamma))