Tokenization using tidytext

자, 이제는 “tidy” 데이터가 무엇인지, 그 원리를 살펴보고 이를 기반으로 한 어휘 빈도수 분석에 효율적인 함수를 제공하는 “tidytext” 패키지를 소개해드리도록 하겠습니다. 특히, 이 패키지의 unnest_tokens() 함수는 텍스트 전처리에 매우 편리한 기능을 제공합니다. 특히, 트윗 텍스트의 경우에는 unnest_tweets() 함수로 편리하게 토큰화 과정이 가능합니다.

자 우선, tidytext 패키지와 텍스트 전처리에 필요한 stringr 패키지를 R세션에 설치해 봅시다.

### Corpus
library(tidyverse)
## -- Attaching packages ---------------------------------------------------------------------------------------------- tidyverse 1.3.0 --
## √ ggplot2 3.2.1     √ purrr   0.3.3
## √ tibble  2.1.3     √ dplyr   0.8.5
## √ tidyr   1.0.2     √ stringr 1.4.0
## √ readr   1.3.1     √ forcats 0.5.0
## -- Conflicts ------------------------------------------------------------------------------------------------- tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
library(stringr)
library(future.apply)
## Loading required package: future
plan(multicore)

load("covid_frames_tweets_sample.RData")

covid_frames_tweets
## # A tibble: 5,000 x 11
##    user_id status_id created_at          text  favorite_count retweet_count
##    <chr>   <chr>     <dttm>              <chr>          <int>         <int>
##  1 107920~ 12434792~ 2020-03-27 10:05:35 "요즘 ~              1             0
##  2 221873~ 12466013~ 2020-04-05 00:51:47 "정교모~              0             0
##  3 241668~ 12416792~ 2020-03-22 10:53:01 "신천지~              1             1
##  4 108008~ 12467265~ 2020-04-05 09:09:17 "평생 ~              0             0
##  5 111337~ 12448302~ 2020-03-31 03:34:05 "방탄소~              3             1
##  6 258016~ 12440690~ 2020-03-29 01:08:59 "#순천~              0             0
##  7 114023~ 12454298~ 2020-04-01 19:16:25 "@xo~              0             0
##  8 403278~ 12463508~ 2020-04-04 08:16:16 "잠시 ~              0             0
##  9 118952~ 12438192~ 2020-03-28 08:36:31 "@ja~              0             0
## 10 813409~ 12428154~ 2020-03-25 14:07:50 "코로나~              0             0
## # ... with 4,990 more rows, and 5 more variables: lang <chr>, date <dttm>,
## #   tweet <chr>, hashtag <chr>, nouns <chr>

앞서 우리는 텍스트 분석을 위한 전처리 과정을 거쳐왔습니다. 예를 들어, 문자열에서 HTML tags, URLs, 그리고 구두점, 숫자 등을 정규 표현을 이용해서 매칭하고 지워주거나 바꿔주는 전처리 작업을 했었죠. 그리고 이렇게 전처리 과정을 거친 텍스트를 단어 단위로 토큰화하기 위해 문자열을 쪼개는 작업을 해야 합니다.

tidytext 패키지의 unnest_tokens() 함수는 이 과정을 편리하게 수행할 수 있도록 해주는 기능을 수행합니다. apostrophe의 축약형을 제외한 텍스트의 구두점은 자동적으로 삭제하고, 빈칸을 기준으로 단어 단위로 텍스트를 쪼개서 그 결과값을 ‘tidy’ 데이터로 만들어 줍니다. 결국, 텍스트를 토큰 단위로 쪼개서 한 행에 하나의 토큰, 여기에선 한 단어씩 위치하게하는 데이터 프레임 형식으로 변환하는 기능을 수행하는 거죠.

library(tidytext)
tibble(text="텍스트 분석 난이도 초보자")
## # A tibble: 1 x 1
##   text                     
##   <chr>                    
## 1 텍스트 분석 난이도 초보자
unnest_tokens(tibble(text="텍스트 분석 난이도 초보자"), word, text)
## # A tibble: 4 x 1
##   word  
##   <chr> 
## 1 텍스트
## 2 분석  
## 3 난이도
## 4 초보자

tidy 데이터란 각 행에 하나의 토큰(여기에선 하나의 단어)만 위치하도록 구성된 테이블을 말합니다. 결국 tidy한 데이터를 만든다는 것은 토큰의 단위를 정하고 텍스트 데이터를 토큰화하여 각 행이 하나의 토큰으로 구성되게 만든다는 것이죠. 이렇게 tidy 데이터를 구성한다는 것은 각 행이 하나의 단어씩 가지고 있기 때문에 이후 기계 학습을 위한 텍스트 수치 변환 작업을 수행할 때 편리하게 연계 작업할 수 있다는 장점이 있습니다.

자, 다음의 R코드는 unnest_tokens() 함수를 이용해서 covid_frames_date라는 트윗 데이터를 토큰화한 과정을 보여주는데요.

covid_frames_tweets %>% arrange(created_at) # dplyr의 arrange() 함수는 해당 변수를 순차적으로 정렬 (작은 것부터); 시간의 경우 오래된 데이터부터 정렬
## # A tibble: 5,000 x 11
##    user_id status_id created_at          text  favorite_count retweet_count
##    <chr>   <chr>     <dttm>              <chr>          <int>         <int>
##  1 102014~ 12415455~ 2020-03-22 02:01:38 "대구와~              0             0
##  2 569707~ 12415475~ 2020-03-22 02:09:42 "교회도~              2             0
##  3 112777~ 12415630~ 2020-03-22 03:11:19 "신천지~              1             1
##  4 631022~ 12415789~ 2020-03-22 04:14:23 "신천지~              0             1
##  5 332861~ 12415887~ 2020-03-22 04:53:23 "신천지~              4             4
##  6 241044~ 12415931~ 2020-03-22 05:10:49 "신천지~              1             1
##  7 241044~ 12415934~ 2020-03-22 05:11:55 "신천지~              1             1
##  8 711645~ 12415941~ 2020-03-22 05:14:37 "<U+2757><U+FE0F>부~              1             3
##  9 115584~ 12416331~ 2020-03-22 07:49:46 "3월2~              0             0
## 10 812722~ 12416541~ 2020-03-22 09:13:11 "#우한~              0             0
## # ... with 4,990 more rows, and 5 more variables: lang <chr>, date <dttm>,
## #   tweet <chr>, hashtag <chr>, nouns <chr>
covid_frames_tweets %>% arrange(created_at) %>% unnest_tokens(word, nouns, token = "words") %>% slice(1:30)
## # A tibble: 30 x 11
##    user_id status_id created_at          text  favorite_count retweet_count
##    <chr>   <chr>     <dttm>              <chr>          <int>         <int>
##  1 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  2 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  3 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  4 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  5 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  6 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  7 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  8 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  9 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
## 10 569707~ 12415475~ 2020-03-22 02:09:42 교회도,~              2             0
## # ... with 20 more rows, and 5 more variables: lang <chr>, date <dttm>,
## #   tweet <chr>, hashtag <chr>, word <chr>
covid_frames_tweets %>% arrange(created_at) %>% unnest_tokens(word, nouns, token = "tweets") %>% slice(1:30) # dplyr의 slice() 함수는 특정 범위의 행(들)만 추출해서 보여주는 기능을 함
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
## # A tibble: 30 x 11
##    user_id status_id created_at          text  favorite_count retweet_count
##    <chr>   <chr>     <dttm>              <chr>          <int>         <int>
##  1 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  2 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  3 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  4 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  5 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  6 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  7 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  8 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  9 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
## 10 569707~ 12415475~ 2020-03-22 02:09:42 교회도,~              2             0
## # ... with 20 more rows, and 5 more variables: lang <chr>, date <dttm>,
## #   tweet <chr>, hashtag <chr>, word <chr>
covid_frames_tweets %>% arrange(created_at) %>% unnest_tweets(word, nouns) %>% slice(1:30)
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
## # A tibble: 30 x 11
##    user_id status_id created_at          text  favorite_count retweet_count
##    <chr>   <chr>     <dttm>              <chr>          <int>         <int>
##  1 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  2 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  3 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  4 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  5 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  6 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  7 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  8 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
##  9 102014~ 12415455~ 2020-03-22 02:01:38 대구와 ~              0             0
## 10 569707~ 12415475~ 2020-03-22 02:09:42 교회도,~              2             0
## # ... with 20 more rows, and 5 more variables: lang <chr>, date <dttm>,
## #   tweet <chr>, hashtag <chr>, word <chr>

이 코드는 covid_frames_tweets 데이터에 unnest_tokens()함수를 적용하여, nouns라는 변수의 명사 문자열을 단어 단위로 쪼개서 word라는 새로운 변수에 저장해주라는 명령을 수행합니다. 여기에서 토큰화 단위는 즉, 문자열을 쪼개고자 하는 기본 단위는 token이라는 인자를 “tweets”로 설정했죠. 이를 통해 단어 단위로 문자열을 토큰화하라는 설정이 취해진 것입니다.

unnest_tokens() 함수의 기능은 문자열을 단어 단위로 토큰화하라는 것 이외에 정규 표현식을 이용해서 텍스트를 토큰화하도록 할 수 있다는 점인데요. 따라서 텍스트를 쪼개는 토큰의 단위를 다양하게 설정할 수 있는 장점이 있습니다. 예를 들어서, 우리가 다루는 트윗 데이터에 경우, 텍스트에 우물 정자로 표시되는 해시태그나 골뱅이 모양으로 표시되는 트위터 핸들이 많이 포함되어 있는데요. 이러한 기호들은 트윗에서 그 자체로서 특별한 의미를 지니고 있기 때문에, 다른 구두점이나 기호들과는 차별해서 처리해 줘야 하는 경우가 있습니다. 따라서 해시태그와 트위터 핸들의 표시는 지우지 않는 토큰화 방법이 필요하고, 이 때 unnest_tweets()는 유용한 기능을 합니다.

자, 이러한 tidytext 방식의 토큰화를 거치면, 트윗 텍스트가 어휘 단위로 구분된 새로운 데이터를 얻게 되는데요. 이를 이용하면 어휘 빈도수 분석이 매우 용이합니다.

covid_word_count <- covid_frames_tweets %>% 
  unnest_tweets(word, nouns) %>% 
  count(word, sort = TRUE) 
## Using `to_lower = TRUE` with `token = 'tweets'` may not preserve URLs.
covid_word_count
## # A tibble: 8,115 x 2
##    word       n
##    <chr>  <int>
##  1 코로나  3467
##  2 문      2030
##  3 주세요  1976
##  4 알바    1975
##  5 만      1205
##  6 분      1090
##  7 시간    1073
##  8 저희    1006
##  9 남      1005
## 10 직원    1000
## # ... with 8,105 more rows
# covid_word_count는 데이터 프레임의 형식을 갖추고 있다. 변수는 두개. 관측값은 8,115. 즉, 트윗에서 나타나는 고유 단어의 수

covid_word_count %>% filter(str_length(word)>1)
## # A tibble: 7,375 x 2
##    word       n
##    <chr>  <int>
##  1 코로나  3467
##  2 주세요  1976
##  3 알바    1975
##  4 시간    1073
##  5 저희    1006
##  6 직원    1000
##  7 카톡     997
##  8 상대     996
##  9 비용     990
## 10 라인     987
## # ... with 7,365 more rows

dplyr의 count() 함수는 해당 변수 즉 벡터를 구성하는 값들의 빈도수를 계산합니다. sort 인자는 빈도수를 내림차순을 정렬할 수 있게 합니다.

Docuemnt-Term Matrix(DTM) or Docuemnt-Feature Matrix(DFM)

문서 분류 또는 군집화를 위한 기계학습 알고리즘은 행이 각 문서를 열은 각 특성(어휘)을 숫자로 표시하도록 구성된 2차원의 행렬에서 작동한다. 결국, 텍스트의 분류 또는 군집화를 위해서는 각 문서가 벡터로서 표현(representation)되도록 변환(transformation)하는 작업이 필요하고 우리는 이를 특성 추출(feature extraction) 또는 간략하게 벡터화(vectorization)라 부른다. 이 단계가 곧 텍스트 마이닝의 시작이다.

벡터? 모든 원소가 같은 데이터 속성을 갖는 1차원 데이터 구조

텍스트의 특성을 추출하는 작업은 대부분 문서(document) 단위로 이뤄진다. 각 문서의 내용(content)은 물론 길이, 저자, 출처, 게시 날짜 등의 메타 정보가 특성을 구성한다. 이러한 복수의 특성을 바탕으로 문서의 분류 및 군집화가 이뤄진다.

이러한 관점에서 언어는 일련의 어휘들이 조합이라기 보다는 고차원의 의미(semantic) 공간을 구성하는 접점들이다. 공간의 접점들은 촘촘하게 떼를 짓기도 띄엄띄엄 퍼져 있기도, 또는 일정하게 고루 분포되어 있을 수도 있다. 결국, 의미 공간에서 가깝게 표시되는 문서들은 비슷한 의미를 띄고 있고, 멀리 떨어져 위치하면 상당히 다른 의미를 보이는 것으로 이해될 수 있다. 의미 공간에 각 문서의 특성을 위치시키는 작업이 필요하고 이를 위해서는 의미 공간의 인코딩 작업이 필요하다.

의미 공간(semantic space)의 인코딩(부호화) 작업 중 가장 간단한 방법은 단어주머니(bag-of-words, BOW) 모델로서, 단어들에 의해 문서의 의미와 유사도가 측정될 수 있음을 보여준다. 즉, 비슷한 의미를 전단하는 문서들은 결국 비슷한 단어들로 구성되어 있따는 전제로서 의미 공간을 도출하는 것이다. 또한, 전혀 다른 주제(의미)를 표현하는 문서들과는 겹치는 단어들이 적을 것이다. 따라서 단어주머니 모델은 간단하지만 꽤 효과적인 의미 공간을 표현한다(=벡터화).

Words in Space

BOW 방식의 벡터화는 해당 코퍼스의 모든 문서에 등장하는 모든 단어들을 모아 리스트로 만들어 각 문서가 어떤 단어들로 구성되어 있는지 파악하여, 각 문서별로 -> 각 단어의 출현 빈도수를 측정하는 것. 그리고 벡터 인코딩(벡터화)은 frequency(BOW), one-hot, TF-IDF 및 숫자로 표현(distributed representation) 등의 방법으로 산출 가능하다.

Encoding documents as vectors

Encoding documents as vectors

그럼 각 벡터의 숫자는 어떻게 정해지는가?

  1. Frequency
  2. One-hot encoding
  3. TF-IDF
  4. Distributed representations

각 문서의 벡터화를 위해서는 다음의 텍스트 전처리 과정이 필요하다. 1) 문자 표준화 (대문자, 영어 vs 한국어) 2) 구두점 제거 3) Stemming 또는 Lemmatization (벡터 단순화) 4) 불용어(stopwords) 제거

대부분의 자연어처리 패키지들은 (NLTK, Scikit-Learn, Gensim) 전처리 작업에 필요한 함수와 사전을 제공함.

의미 공간 인코딩 작업을 차례대로 살펴보자

load("covid_frames_tweets_sample_420.RData")
library(quanteda)
## Package version: 2.0.1
## Parallel computing: 2 of 6 threads used.
## See https://quanteda.io for tutorials and examples.
## 
## Attaching package: 'quanteda'
## The following object is masked from 'package:utils':
## 
##     View
txt <- covid_frames_tweets_sample$nouns

head(txt)
##                                                                                                                                                                                               언제 뵐까 각만 재고 있습니다..(*약속한 건 절대 잊지 않음!) 코로나 땜에 옴싹달싹하다 이케 됐네요 ㅠㅁㅠ @binu_4_lvlz 
##                                                                                                                                                                                                                                                                                            "각 약속 건 코로나 땜" 
##         @bornfreeonekiss 재중씨는 일본 호우재해 뒤 자원봉사로 와륵이나 흙부대를 나르는 노동을 정말 더운 중에 해 주셨고 코로나에 관해서도,조심하세요!라고 항상 경고해 주시는 것을 알고 있어요.싸다니는 사람들에게 분개해서 만우절 임펙트를 사용하는 형이 되었지만 우리들을 걱정해 주시는 마음만이라고 알고 있어요. 
##                                                                                                                                                                                   "재중 씨 일본 호우 재해 뒤 자원 봉사 와륵 흙 부대 노동 중 코로나 조심 경고 것 사람 분개 만우절 임 펙 트 사용 형 우리 걱정 마음" 
##                                                                                                                                                                                                          귀여워. 코로나 얼릉 꺼지시게.\n니놈들도 꺼지라\n#punish_nthroom\n#arrest_nthroom https://t.co/xShoMuDuBq 
##                                                                                                                                                                                                                                                                                                     "코로나 니놈" 
##                                                                                                                                       싸강개같다진짜코로나이자식언제잠잠해지냐\n#SAYNOTO_NTHROOM\n1학기연장하자니까이놈의대학교는눈치보면서일주일씩찔끔찔끔늘리는거보소조나빡치네제발한번에좀늘려이망할ㄷㅅㅇ대야 
##                                                                                                                                                                                                                                         "강개 코 나이 자식 학기 연장 이놈 대학교 일 주일 거보 소조 번 이망 ㄷ ㅅ" 
## #세종 #대전\n저희  스마일 밤 출장 샵 에서\n남 녀 섹 알바 직원 구함니다\n외로운 분들 상대로 \n알바 비용은 2시간 80만\n카톡:wn115문의 주세요\n라인:sk12238문의 주세요\n세종 대전 #확진자 #만남 #미녀 #대출 \n#코로나  #조건 #비키니 \n#야동사이트 #아가씨\n#ㅇㅕ성흥분ㅈㅔ\n#ㅂㅣㅇㅏ그ㄹㅏ https://t.co/XdHtnsz1TU 
##                                                                                                                                                                                                "저희 스마일 밤 출장 샵 남 녀 섹 알바 직원 구함 분 상대 알바 비용 시간 만 카톡 문 주세요 라인 문 주세요 세종 대전" 
##                                                                                                                                                                                                                             와아 교수님도 학교 오는 거 싫으셨나 봐 코로나 덕분에 &lt; 이렇게 말하심ㅋㅋㅋㅋㅋㅋㅋ 
##                                                                                                                                                                                                                                                                                     "교수 학교 거 코로나 덕분 말"
names(txt) <- 1:5000

Frequency Vector

간단한 인코딩 방식. 코퍼스 단어 리스트 중 각 문서에 등장하는 단어들의 횟수를 표현.