토픽 모델링 #1 LDA
토픽 모델링
텍스트의 핵심 주제를 찾아 비슷한 내용끼리 분류하는 분석 방법
다량의 텍스트를 분석할 때 유용
(1) 토픽 모델을 이용하면
- 단어가 어떤 토픽에 등장할 확률이 더 높은지 알 수 있다
- 단어 등장 확률을 보고 토픽의 핵심 단어를 알 수 있다
- 문서가 어떤 토픽에 등장할 확률이 높은지 알 수 있다
- 확률을 이용해 문서를 토픽별로 분류할 수 있다 → 다량의 문서 분석할 때 특히 유용
- 문서가 어떤 주제로 구성되는지 파악할 수 있다
(2) LDA 모델
LDA(Latent Dirichlet Allocation, 잠재 디리클레 할당)
가장 널리 사용되는 토픽 모델링 알고리즘
< LDA 모델의 가정 >
- 1. 토픽은 여러 단어의 혼합으로 구성된다
한 토픽에 여러 단어가 서로 다른 확률로 포함
같은 단어가 여러 토픽에 서로 다른 확률로 포함
- 2. 문서는 토픽들의 혼합으로 구성된다
문서에는 여러 토픽의 단어가 서로 다른 비율로 들어 있음
단어 확률이 더 높은 쪽으로 문서 분류
LDA 모델 만들기
1. 기본적인 전처리
중복 문서 제거하기:
dplyr::distinct() 중복 문서가 있으면 계산량 늘어나 모델 만드는 시간 오래 걸림
한 토픽에 내용이 똑같은 문서가 여러 개 들어 있는 문제 생김
짧은 문서 제거하기:
토픽 모델은 여러 문서에 공통으로 사용된 단어를 이용해 만듦
짧은 문서는 다른 문서와 공통으로 사용된 단어가 적어 모델 만드는 데 적합하지 않음
library(multilinguer) library(KoNLP) useNIADic() library(readr) library(dplyr) library(stats) library(stringr) library(textclean) |
# 1-1 데이터 불러오기
raw_news_comment <- read_csv("C:/news_comment_parasite.csv") %>% mutate(id = row_number()) |
row_number() : 각 행의 행번호. 각 행에 일련번호를 붙일 때 유용.
문서를 토픽별로 분류하는 작업을 할 때 문서 구분 기준이 필요하므로 댓글 고유 번호 부여
#1-2 기본 전처리
news_comment <- raw_news_comment %>% mutate(reply = str_replace_all(reply , "[^가-힣]"," "), reply = str_squish(reply)) %>% |
#1-3 중복 댓글 제거
distinct(reply, .keep_all = T) %>% |
distinct : 해당 데이터 프레임의 모든 열에서 동일 값을 갖는 행 데이터를 제거
.keep_all = T : 다른 컬럼을 남긴다는 의미
#1-4 짧은 문서 제거
3단어 이상 추출
news_comment <- raw_news_comment %>% mutate(reply = str_replace_all(reply , "[^가-힣]"," "), reply = str_squish(reply)) %>% distinct(reply, .keep_all = T) %>% filter(str_count(reply, boundary("word") >= 3)) |
#2. 명사 추출하기
문서의 주제는 명사로 결정되므로 명사 추출해 모델 만드는 경우가 많음
댓글에 중복 사용된 단어 제거: 문서에 같은 단어 여러 번 사용되면 내용 관계없이 사용 빈도 때문 에 특정 토픽으로 분류될 가능성 높음
#2-1 명사추출
comment <- news_comment %>% unnest_tokens(input = reply, output = word, token = extractNoun, drop = F) %>% filter(str_count(word) > 1) %>% |
#2-2 댓글 내 중복 제거
group_by(id) %>% distinct(word, .keep_all = T) %>% ungroup() %>% select(id, word) |
comment # A tibble: 21,835 x 2 # id word # <int> <chr> # 1 1 우리 # 2 1 행복 # 3 2 시국 # 4 2 감사 # 5 2 진심 # 6 3 우리나라 # 7 3 영화감독 # 8 3 영감 # 9 3 봉감독님 # 10 3 공동각본쓴 # # ... with 21,825 more rows # # i Use `print(n = ...)` to see more rows |
#3. 빈도 높은 단어 제거하기
빈도가 매우 높은 단어가 포함된 상태로 토픽 모델을 만들면 대부분의 토픽에 똑같은 단어가 주요 단어로 등장해 토픽의 특징을 파악하기 어려우므로 제거
count_word <- comment %>% add_count(word) %>% filter(n <= 200) %>% select(-n) count_word # # A tibble: 18,239 x 2 # id word # <int> <chr> # 1 1 우리 # 2 1 행복 # 3 2 시국 # 4 2 감사 # 5 2 진심 # 6 3 우리나라 # 7 3 영화감독 # 8 3 영감 # 9 3 봉감독님 # 10 3 공동각본쓴 # # ... with 18,229 more rows # # i Use `print(n = ...)` to see more rows |
#4. 불용어 제거 & 유의어 처리
#4-1 불용어, 유의어 확인
불용어(Stop word): 단어 텍스트 해석에 도움이 되지 않으므로 제거해야 함
count_word %>% count(word, sort = T) %>% print(n = 200) # A tibble: 6,179 x 2 # word n # <chr> <int> # 1 조국 186 # 2 블랙리스트 174 # 3 대박 170 # 4 한국 165 # 5 세계 142 # 6 수상 137 # 7 미국 128 # 8 들이 126 # 9 역사 106 # 10 정치 106 # # ... with 6,169 more rows |
빈도 높은 단어 추출해 불용어 확인, 표현은 다르지만 의미가 비슷한 유의어가 있는지 확인
#4-2 불용어 목록 만들기
stopword <- c("들이", "하다", "하게", "하면", "해서", "이번", "하네", "해요", "이것", "니들", "하기", "하지", "한거", "해주", "그것", "어디", "여기", "까지", "이거", "하신", "만큼") |
분석에서 제외할 단어 "들이" , "하다" , "하게" 처럼 의미를 알 수 없는 단어들
stop word에 불용어 넣기
#4-3 불용어 제거하고 유의어 수정하기
count_word <- count_word %>% filter(!word %in% stopword) %>% mutate(word = recode(word, "자랑스럽습니" = "자랑", "자랑스럽" = "자랑", "자한" = "자유한국당", "문재" = "문재인", "한국의" = "한국", "그네" = "박근혜", "추카" = "축하", "정경" = "정경심", "방탄" = "방탄소년단")) |
dplyr::recode() : 특정 값을 다른 값으로 수정
!word %in% stopword : word가 아닌것 stopword안에서
#5 DTM 만들기
DTM(Document-Term Matrix, 문서 단어 행렬): 행은 문서, 열은 단어로 구성해 빈도를 나타낸 행렬
#5-1 문서별 단어 빈도 구하기
count_word_doc <- count_word %>% count(id, word, sort = T) count_word_doc # A tibble: 17,785 x 3 # id word n # # 1 35 한국 2 # 2 1173 한국 2 # 3 1599 한국 2 # 4 1762 한국 2 # 5 2240 한국 2 # 6 2307 방탄소년단 2 # 7 2733 한국 2 # 8 2984 박근혜 2 # 9 1 우리 1 # 10 1 행복 1 # # ... with 17,775 more rows # # i Use `print(n = ...)` to see more rows |
sort =T : 오름차순 정렬
sort =(decreasing = TRUE) : 내림차순 정렬
cf) sort()는 정렬된 값을 순서대로 보여주는 반면에, order()는 데이터 크기의 색인을 제공
#5-2 DTM 만들기
tidytext::cast_dtm()
document : 문서 구분 기준
term : 단어
value : 단어 빈도
library(tm) library(tidytext) # cast_dtm 함수 사용하기 위한 라이브러리 dtm_comment <- count_word_doc %>% cast_dtm(document = id, term = word, value = n) dtm_comment # <></documenttermmatrix (documents: 3563, terms: 6157)> # Non-/sparse entries: 17785/21919606 # Sparsity : 100% # Maximal term length: 35 # Weighting : term frequency (tf) |
cast_dtm()
어떤 함수들은 tidy format이 아닌 dtm format을 input으로 필요로 하는 케이스가 있을 수도 있다.
tidytext 라이브러리에서는 tidy format을 dtm format으로 변환해주는 함수
cf) tidy()
tidytext 라이브러리는 DTM 객체를 직접 사용할 수 없지만 이를 tidy data frame 형태로 변환해주는 함수를 제공
#6. LDA
topicmodels::LDA()
k : 토픽 수
method : 샘플링 방법. // 일반적으로 깁스 샘플링( "Gibbs" ) 가장 많이 사용
control = list(seed = )) : 난수 고정
# 6-1 토픽 모델 만들기
library(topicmodels) lda_model <- LDA(dtm_comment, k = 8, method = "Gibbs", control = list(seed = 1234)) |
토픽 수에는 정해진 정답이 없기 때문에 k값을 바꿔가며 여러 모델을 만든 다음 결과를 비교해 결정
# 6-2 모델 내용 확인
glimpse(lda_model) ## Formal class 'LDA_Gibbs' [package "topicmodels"] with 16 slots ## ..@ seedwords : NULL ## ..@ z : int [1:17604] 8 8 4 3 7 4 3 1 1 1 ... ## ..@ alpha : num 6.25 ## ..@ call : language LDA(x = dtm_comment, k = 8, method = "Gibbs", control = list(seed = 1234)) ## ..@ Dim : int [1:2] 3563 6157 ## ..@ control :Formal class 'LDA_Gibbscontrol' [package "topicmodels"] with 14 slots ## ..@ k : int 8 ## ..@ terms : chr [1:6157] "한국" "자랑" "방탄소년단" "박근혜" ... ## ..@ documents : chr [1:3563] "35" "206" "566" "578" ... ## ..@ beta : num [1:8, 1:6157] -7.81 -10.22 -10.25 -5.83 -10.25 ... ## ..@ gamma : num [1:3563, 1:8] 0.151 0.15 0.11 0.114 0.11 ... ## ..@ wordassignments:List of 5 ## .. ..$ i : int [1:17592] 1 1 1 1 1 1 1 1 1 1 ... ## .. ..$ j : int [1:17592] 1 98 99 100 101 102 103 104 105 106 ... ## .. ..$ v : num [1:17592] 8 4 3 7 4 3 7 2 8 6 ... ## .. ..$ nrow: int 3563 ## .. ..$ ncol: int 6157 ## .. ..- attr(*, "class")= chr "simple_triplet_matrix" ## ..@ loglikelihood : num -128790 |
@ beta : num [1:8, 1:6157] : 단어가 각 토픽에 등장할 확률/ 6157개 단어로 모델
@ gamma : num [1:3563, 1:8] : 문서가 각 토픽에 등장할 확률/ 3563개 문서로 모델 생성