Textrank란?

참고로 PageRank 알고리즘은 다음과 같다.

Textrank 예제

문장의 중요도 계산

먼저 예제 실행에 필요한 패키지를 탑재하자.

library(dplyr)
## Warning: 패키지 'dplyr'는 R 버전 4.2.3에서 작성되었습니다
## 
## 다음의 패키지를 부착합니다: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidytext)
## Warning: 패키지 'tidytext'는 R 버전 4.2.3에서 작성되었습니다
library(textrank)
## Warning: 패키지 'textrank'는 R 버전 4.2.3에서 작성되었습니다
library(rvest)
## Warning: 패키지 'rvest'는 R 버전 4.2.3에서 작성되었습니다
library(ggplot2)
## Warning: 패키지 'ggplot2'는 R 버전 4.2.3에서 작성되었습니다

타임지의 어린이용 Fitbit tracker에 관한 기사를 가져와 분석해보자.2018년 3월에 나온 이 기사는 어린이용 핏빗이 새롭게 출시되었다는 사실과 어떤 특징이 있는지를 설명하는 내용으로 구성된다.

url <- "http://time.com/5196761/fitbit-ace-kids-fitness-tracker/"
article <- read_html(url) %>% 
  html_nodes('div[class="padded"]') %>% 
  html_text()

문서의 내용을 가져와 article에 저장한다. 현재 문서는 문장 단위로 구분되지 않았기 때문에 tidytext로 sentences별로 토큰화된 결과를 만들자. 그리고 각각의 토큰에 번호를 부여하기 위해 row_number() 함수를 사용한다.

article_sentences <- tibble(
  text=article
) %>% 
  unnest_tokens(sentence,text,token='sentences') %>% 
  mutate(sentence_id=row_number()) %>% 
  select(sentence_id,sentence)

문장의 중요도를 판별하기 위해 어떤 키워드를 봐야 하는지 정보를 줘야 한다. 여기서는 단순히 문장에 포함된 모든 단어들 중에서 stopwords를 제외한 단어들을 고려하도록 하자. 만약에 특히 주목해야 할 단어 집합이 정해진다면 그것을 제공하면 된다. 문장을 단어 단위로 토큰화하기 위해 unnest_tokens() 함수를 사용한다.

article_words <- article_sentences %>% 
  unnest_tokens(word,sentence) %>% 
  anti_join(stop_words,by='word')

이제 문장의 중요도를 계산하기 위해 textrank_sentneces() 함수를 사용할 준비가 되었다. 디폴트 결과는 가장 중요한 문장 5개를 추출하는 것이다.

article_summary <- textrank_sentences(
  data=article_sentences,
  terminology=article_words
)

결과를 살펴보자.

article_summary
## Textrank on sentences, showing top 5 most important sentences found:
##   1. fitbit is launching a new fitness tracker designed for children called the fitbit ace, which will go on sale for $99.95 in the second quarter of this year.
##   2. fitbit says the tracker is designed for children eight years old and up.
##   3. above all else, the ace is an effort to get children up and moving.
##   4. the most important of which is fitbit’s new family account option, which gives parents control over how their child uses their tracker and is compliant with the children’s online privacy protection act, or coppa.
##   5. but while fitbit’s default move goal is 30 minutes for adult users, the ace’s will be 60 minutes, in line with the world health organization’s recommendation that children between the ages of five and 17 get an hour of daily physical activity per day.

가장 중요한 문장 순서대로 3개를 골라보자. 문장 중요도 값은 textrank로 계산된다.

library(knitr)
## Warning: 패키지 'knitr'는 R 버전 4.2.3에서 작성되었습니다
library(kableExtra)
## Warning: 패키지 'kableExtra'는 R 버전 4.2.3에서 작성되었습니다
## 
## 다음의 패키지를 부착합니다: 'kableExtra'
## The following object is masked from 'package:dplyr':
## 
##     group_rows
article_summary[["sentences"]] %>% 
  arrange(desc(textrank)) %>% 
  slice(1:3) %>% 
  select(textrank,sentence) %>% 
  kable(format='html',align=c('r','l')) %>% 
  column_spec(1:2,border_left=T,border_right=T) %>% 
  kable_styling()
textrank sentence
0.1189734 fitbit is launching a new fitness tracker designed for children called the fitbit ace, which will go on sale for $99.95 in the second quarter of this year.
0.1048167 fitbit says the tracker is designed for children eight years old and up.
0.0790018 above all else, the ace is an effort to get children up and moving.

마찬가지로 가장 중요하지 않다고 판단한 문장 3개를 골라보자.

article_summary[["sentences"]] %>% 
  arrange(textrank) %>% 
  slice(1:3) %>% 
  select(textrank,sentence) %>% 
  kable(format='html',align=c("r","l")) %>% 
  column_spec(1:2,border_left = T,border_right = T) %>% 
  kable_styling()
textrank sentence
0.0153318 sign up for worth your time contact us at .
0.0242680 the $39.99 nabi compete, meanwhile, is sold in pairs so that family members can work together to achieve movement milestones.
0.0286615 more must-reads from time meet the newest class of next generation leaders the unwavering confidence of deion sanders why democrats didn’t save mccarthy the 100 best mystery and thriller books of all time inside one indian iphone factory the health benefits of nostalgia self-silencing is making women sick: essay want weekly recs on what to watch, read, and more?

신문기사에서 중요한 문장들이 주로 어디에 배치되어 있는지를 살펴보자. 영어의 글에서 보통 중요한 문장은 앞에 배치된 경향이 있다.

article_summary[["sentences"]] %>% 
  ggplot(aes(textrank_id,textrank,fill=textrank_id)) +
  geom_col() +
  scale_fill_viridis_c() +
  guides(fill='none') +
  labs(
    x="Sentence",
    y="Textrank score",
    title="4 most informative sentences appear within first half",
    subtitle="subtitle",
    caption="Business Data Lab"
  )

결과를 보면 글을 앞에 위치한 문장들의 중요도가 크지만, 중간 중간에 중요도가 높은 문장(주제문장)이 있음을 알 수 있다. 왜냐하면 글이 여러 단락으로 되어 있기 때문이다.

Extract relevant keywords

현재 예제에서 전체적으로 중요한 키워드가 무엇이지를 분석하자. 즉, 지금 핏빗 기사에서의 중요 키워드를 추리려 한다.

article_relevant_keywords <- textrank_keywords(article_words$word)

키워드 리스트 출력하기

article_relevant_keywords$terms
##  [1] "fitbit"      "children"    "ace"         "fitbit’s"   "tracker"    
##  [6] "time"        "fitness"     "family"      "move"        "minutes"    
## [11] "js"          "child"       "health"      "parents"     "kids"       
## [16] "activity"    "app"         "children’s" "watch"       "player"     
## [21] "fjs"         "smartwatch"  "purple"      "goal"        "compete"    
## [26] "company’s"  "control"     "called"      "day"         "account"    
## [31] "products"    "friendly"    "designed"    "letters"     "sanders"    
## [36] "themed"      "deion"       "democrats"   "skins"       "2"          
## [41] "confidence"  "save"        "minnie"      "jr"          "unwavering" 
## [46] "mccarthy"    "mouse"       "vivofit"     "leaders"     "100"        
## [51] "star"        "79.99"       "generation"  "mystery"     "wars"       
## [56] "garmin’s"   "sick"        "women"       "essay"       "contact"    
## [61] "silencing"   "class"       "thriller"    "versions"    "tempo"      
## [66] "ecommerce"   "choose"      "4830"        "a439"        "quarter"

바이그램(bigram) 조건과 frequency 2이상인 조건을 붙여서 결과보기

article_relevant_keywords$keywords %>% 
  filter(ngram==2) %>% 
  filter(freq > 1)

오스틴의 소설을 가지고 분석하기

tidytext의 예로 많이 등장하는 제인 오스틴의 소설들을 분석해보자.

library(janeaustenr)
## Warning: 패키지 'janeaustenr'는 R 버전 4.2.3에서 작성되었습니다
library(dplyr)
library(stringr)
## Warning: 패키지 'stringr'는 R 버전 4.2.3에서 작성되었습니다
library(textrank)
library(tidytext)
library(purrr)
## Warning: 패키지 'purrr'는 R 버전 4.2.3에서 작성되었습니다

책을 받아서, 책과 챕터 별로 문장을 끊어서 저장한다.

original_books <- austen_books() %>% 
  group_by(book) %>% 
  mutate(
    linenumber=row_number(),
    chapter=cumsum(str_detect(text,
                              regex("^chapter [\\divxlc]",
                                    ignore_case=TRUE)))) %>% 
      ungroup()
original_books

이제 토큰 데이터를 구해보자.

tidy_books <- original_books %>% 
  unnest_tokens(word,text) %>% 
  filter(chapter>0)
tidy_books

word 별로 book, linenumber, chapter 데이터가 뒤따라 붙었다.

그런데 책 제목을 제외하고 챕터 1부터 데이터를 봐야 하기 때문에 chapter>0 조건을 붙였다.

또한, 다음과 같이 chapter라는 단어와 단순한 숫자는 제외했다. 그리고 stopwords도 제거한다.

tidy_books <- tidy_books %>% 
  anti_join(stop_words) %>% 
  anti_join(tibble(
    word=c("chapter",1:10)
  ))
## Joining with `by = join_by(word)`
## Joining with `by = join_by(word)`

결과를 다시보자.

tidy_books

각각의 챕터에 대하여 각 챕터에서 어떤 단어가 핵심 키워드인지를 추출해보자. 우리는 ngram=2 옵션으로 bigram 데이터를 추출하고 n=10으로 가장 중요한 10개 키워드만을 추려보기로 한다.

tidy_books_chapter_keywords <- seq(min(tidy_books$chapter),
                                   max(tidy_books$chapter)) %>% 
  map(function(chapter_position) {
    bow = tidy_books %>% filter(chapter == chapter_position)
    tr_result = textrank_keywords(bow$word)
    tr_result$keywords %>% 
      filter(ngram==2) %>% 
      arrange(desc(freq)) %>% 
      slice_head(n=10) %>% 
      mutate(chapter=chapter_position)
  })

이제 해당 결과를 그림으로 살펴보자. 먼저 1에서 4번 챕터까지.

tidy_books_chapter_keywords %>% 
  bind_rows() %>% 
  filter(chapter %in% 1:4) %>% 
  group_by(chapter) %>% 
    arrange(desc(freq)) %>% 
    mutate(ranking=row_number()) %>% 
  ungroup() %>% 
  ggplot(aes(x=reorder_within(keyword,-freq,chapter,sep="..."),y=freq,fill=ranking)) +
  geom_bar(stat='identity') +
  scale_fill_viridis_c() +
  guides(fill='none') +
  theme_minimal() +
  theme(axis.text.x=element_text(angle=-90)) +
  labs(x="Bigram keyword",
       y="Frequency",
       title="Textrank keyword extraction example",
       subtitle = "Bigram, Austine books, chapter = 1 ~ 4",
       caption = "Business Data Lab, 2023") +
  facet_grid(~chapter, scales="free_x")

다음으로 5번에서 8번 챕터다.

tidy_books_chapter_keywords %>% 
  bind_rows() %>% 
  filter(chapter %in% 5:8) %>% 
  group_by(chapter) %>% 
    arrange(desc(freq)) %>% 
    mutate(ranking=row_number()) %>% 
  ungroup() %>% 
  ggplot(aes(x=reorder_within(keyword,-freq,chapter,sep="..."),y=freq,fill=ranking)) +
  geom_bar(stat='identity') +
  scale_fill_viridis_c() +
  guides(fill='none') +
  theme_minimal() +
  theme(axis.text.x=element_text(angle=-90)) +
  labs(x="Bigram keyword",
       y="Frequency",
       title="Textrank keyword extraction example",
       subtitle = "Bigram, Austine books, chapter = 5 ~ 8",
       caption = "Business Data Lab, 2023") +
  facet_grid(~chapter, scales="free_x")

챕터별 가장 중요한 주인공의 이름이 등장하거나 서로 간의 관계들을 볼 수 있는 결과들이 들어있다.

bigram 분석의 결과를 바탕으로 네트워크 분석을 수행할 수 있다 (숙제).

한편, 단순히 sir와 같은 단어는 별 의미가 없어 보이기 때문에 이를 제거하고 분석을 다시 할 필요도 있어 보인다.

노트

2023년 10월 7일 작성.

김태경