단어의 의미는 문장에 함께 사용된 단어에 따라 달라집니다. 단어의 빈도를 분석하면 중요한 단어가 무엇인지는 알 수 있지만, 단어가 어떤 맥락에서 사용됐는지는 알 수 없습니다. 텍스트의 맥락을 이해하려면 단어의 관계를 이용해 의미망(semantic network)을 만들고 단어들이 어떻게 연결되는지 살펴봐야 합니다. 이번 수업에서는 의미망을 이용해 단어들의 관계를 분석해 그 의미를 포착하는 방법을 알아봅니다.
‘손-장갑’, ‘머리-모자’ 처럼 관계가 깊은 단어들이 있습니다. 이와 같은 단어 간의 관계를 살펴보는 분석 방법을 동시 출현 단어 분석(공기어 분석; co-occurrence analysis)이라고 합니다. 동시 출현 단어를 이용해 텍스트에 어떤 단어가 함께 사용되었는지 살펴보고 네트워크 그래프를 만드는 방법을 알아보겠습니다.
코로나 백신 관련 기사의 헤드라인에서 AZ와 화이자에 대한 동시 출현 단어 분석
library(tidyverse)
## -- Attaching packages --------------------------------------- tidyverse 1.3.0 --
## v ggplot2 3.3.3 v purrr 0.3.4
## v tibble 3.1.0 v dplyr 1.0.4
## v tidyr 1.1.2 v stringr 1.4.0
## v readr 1.3.1 v forcats 0.5.1
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
library(readxl)
library(lubridate)
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
cv <- read_excel("bigkinds_corona_vaccine.xlsx", sheet = 1)
cv_hls_type <- cv %>%
select(DATE, COMPANY, HEADLINE, FEATURE) %>%
filter(!duplicated(HEADLINE)) %>%
mutate(DATE = ymd(DATE)) %>%
mutate(HEADLINE = str_remove_all(HEADLINE, "\\[[[:print:]]+\\]|\\<[[:print:]]+\\>")) %>%
filter(str_detect(HEADLINE, "아스트라|AZ|화이자")) %>%
mutate(type = ifelse(str_detect(HEADLINE, "AZ|아스트라"),
"AZ", "Pfizer"))
cv_hls_type
## # A tibble: 1,006 x 5
## DATE COMPANY HEADLINE FEATURE type
## <date> <chr> <chr> <chr> <chr>
## 1 2021-03-22 중부일보 " 고령층에도 본격화된 AZ 백신 접종"~ az,유럽,고령층,코로나,65세,불안감,코로~ AZ
## 2 2021-03-22 중앙일보 "AZ \"코로나백신, 美 임상서 효과 79~ 아스트라제네카,미국,코로나19,임상시험,고령~ AZ
## 3 2021-03-22 중앙일보 "\"AZ 접종 후 희귀 혈전 발생 20대 ~ 혈전증,az,서은숙,코로나19,정맥동,대응요~ AZ
## 4 2021-03-22 세계일보 "예방접종위 “AZ 백신과 혈전 연관성 없어~ az,예방접종위,접종위,아스트라제네카,뉴시스~ AZ
## 5 2021-03-22 조선일보 "文, 아스트라 접종 하루 전날 “백신 가짜~ 아스트라제네카,부동산,일자리,정상회의,경계심~ AZ
## 6 2021-03-22 조선일보 " 예방접종위 “아스트라, 혈전과 연관 없어~ 아스트라제네카,혈전증,코로나,예방접종위,접종~ AZ
## 7 2021-03-22 동아일보 "예방접종위 “AZ 백신과 혈전 연관성 없다~ 아스트라제네카,전문위,예방접종전문위,코로나,~ AZ
## 8 2021-03-22 세계일보 "AZ 백신 신뢰 회복하고 대상자 불안감 해~ az,대상자,종사자,요양병원,전문가,불안감,~ AZ
## 9 2021-03-22 조선일보 "존슨 英총리 “저도 아스트라 백신 맞았어요~ 존슨,아스트라제네카,유럽,주요국,이탈리아,정~ AZ
## 10 2021-03-22 동아일보 "같은 75세 이상인데 요양병원 환자는 AZ~ 아스트라제네카,요양병원,75세,종사자,고령층~ AZ
## # ... with 996 more rows
한국어의 경우 텍스트의 의미 파악에 유용한 단어의 품사는 명사입니다. 하지만 명사의 의미는 문장에 함께 사용된 형용사와 동사에 따라 달라집니다. 동시 출현 단어 분석은 단어가 사용된 맥락을 살펴보는 게 중요하므로 명사뿐 아니라 형용사와 동사도 함께 추출해야 합니다.
library(tidytext)
library(RcppMeCab)
cv_hls_pos <- cv_hls_type %>%
select(HEADLINE, type) %>%
rowid_to_column() %>%
unnest_tokens(input = HEADLINE,
output = word,
token = pos,
drop = F)
## Warning: Outer names are only allowed for unnamed scalar atomic inputs
cv_hls_pos
## # A tibble: 14,339 x 4
## rowid HEADLINE type word
## <int> <chr> <chr> <chr>
## 1 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 고령/nng
## 2 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 층/xsn
## 3 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 에/jkb
## 4 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 도/jx
## 5 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 본격/xr
## 6 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 화/xsn
## 7 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 된/xsv+etm
## 8 1 " 고령층에도 본격화된 AZ 백신 접종" AZ az/sl
## 9 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 백신/nng
## 10 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 접종/nng
## # ... with 14,329 more rows
cv_hls_nouns <- cv_hls_pos %>%
filter(str_detect(word, "[/]n|[/]sl")) %>% # 명사 품사만 선택
mutate(word = str_remove(word, "/.*$")) # 품사 태그 제거
cv_hls_nouns
## # A tibble: 8,534 x 4
## rowid HEADLINE type word
## <int> <chr> <chr> <chr>
## 1 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 고령
## 2 1 " 고령층에도 본격화된 AZ 백신 접종" AZ az
## 3 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 백신
## 4 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 접종
## 5 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ az
## 6 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 코로나~
## 7 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 백신
## 8 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 美
## 9 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 임상
## 10 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 효과
## # ... with 8,524 more rows
cv_hls_va <- cv_hls_pos %>%
filter(str_detect(word, "[/]vv|[/]va")) %>% # 동사, 형용사 품사만 선택
mutate(word = str_replace(word, "/.*$", "다")) # "/"로 시작 문자를 "다"로 바꾸기
cv_hls_va
## # A tibble: 534 x 4
## rowid HEADLINE type word
## <int> <chr> <chr> <chr>
## 1 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 없다
## 2 4 "예방접종위 “AZ 백신과 혈전 연관성 없어 계속 접종해야”" AZ 없다
## 3 5 "文, 아스트라 접종 하루 전날 “백신 가짜뉴스 경계심 가져달라”"~ AZ 가져다~
## 4 6 " 예방접종위 “아스트라, 혈전과 연관 없어...접종 계속해야”" AZ 없다
## 5 7 "예방접종위 “AZ 백신과 혈전 연관성 없다” 결론 접종 권고 결정"~ AZ 없다
## 6 9 "존슨 英총리 “저도 아스트라 백신 맞았어요”" AZ 맞다
## 7 10 "같은 75세 이상인데 요양병원 환자는 AZ, 일반인은 화이자" AZ 같다
## 8 11 "丁총리 “아스트라 안전 문제없다 결론” 전문가 “접종이 더 이익”"~ AZ 없다
## 9 12 "내일 아스트라 접종, 고혈압 당뇨 있는 부모님 괜찮을까" AZ 있다
## 10 12 "내일 아스트라 접종, 고혈압 당뇨 있는 부모님 괜찮을까" AZ 괜찮다~
## # ... with 524 more rows
cv_hls <- bind_rows(cv_hls_nouns, cv_hls_va) %>%
filter(str_count(word) >= 2) %>%
arrange(rowid)
cv_hls
## # A tibble: 7,351 x 4
## rowid HEADLINE type word
## <int> <chr> <chr> <chr>
## 1 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 고령
## 2 1 " 고령층에도 본격화된 AZ 백신 접종" AZ az
## 3 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 백신
## 4 1 " 고령층에도 본격화된 AZ 백신 접종" AZ 접종
## 5 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ az
## 6 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 코로나~
## 7 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 백신
## 8 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 임상
## 9 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 효과
## 10 2 "AZ \"코로나백신, 美 임상서 효과 79% 혈전 위험 증가 없어\"" AZ 혈전
## # ... with 7,341 more rows
pairwise_count()
토큰화한 텍스트를 이용해 단어의 동시 출현 빈도를 구하겠습니다. widyr
패키지의 pairwise_count()
를 이용하면 동시 출현 빈도를 구할 수 있습니다. pairwise_count()
에는 다음과 같은 파라미터를 입력합니다.
item
: 단어. 여기서는 word
를 입력합니다.feature
: 텍스트 구분 기준. 여기서는 rowid
를 입력합니다.sort=T
: 빈도가 높은 순으로 출력 결과를 정렬합니다.pairwise_count()
는 한 단어를 기준으로 함께 사용된 모든 단어의 빈도를 구하기 때문에 출력 결과가 “코로나-백신”, “백신-코로나”와 같이 순서를 바꿔가며 같은 빈도를 지니는 두개의 행으로 구성되는 특징이 있습니다.
#install.packages("widyr")
library(widyr)
pair <- cv_hls %>%
pairwise_count(item = word,
feature = rowid,
sort = T)
## Warning: `distinct_()` was deprecated in dplyr 0.7.0.
## Please use `distinct()` instead.
## See vignette('programming') for more help
## Warning: `tbl_df()` was deprecated in dplyr 1.0.0.
## Please use `tibble::as_tibble()` instead.
pair
## # A tibble: 23,894 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 화이자 백신 459
## 2 백신 화이자 459
## 3 접종 백신 278
## 4 백신 접종 278
## 5 코로나 백신 264
## 6 백신 코로나 264
## 7 아스트라 백신 232
## 8 백신 아스트라 232
## 9 화이자 코로나 196
## 10 코로나 화이자 196
## # ... with 23,884 more rows
save(pair, file="pair.RData")
filter()
를 이용하면 특정 단어와 같은 헤드라인에서 자주 함께 등장하는 단어가 무엇인지 알 수 있습니다.
pair %>% filter(item1 == "화이자")
## # A tibble: 902 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 화이자 백신 459
## 2 화이자 코로나 196
## 3 화이자 접종 183
## 4 화이자 승인 82
## 5 화이자 효과 72
## 6 화이자 모더 62
## 7 화이자 맞다 43
## 8 화이자 예방 36
## 9 화이자 시작 35
## 10 화이자 아스트라 33
## # ... with 892 more rows
pair %>% filter(item1 == "아스트라")
## # A tibble: 592 x 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 아스트라 백신 232
## 2 아스트라 접종 136
## 3 아스트라 코로나 62
## 4 아스트라 맞다 42
## 5 아스트라 승인 40
## 6 아스트라 효과 35
## 7 아스트라 이상 33
## 8 아스트라 화이자 33
## 9 아스트라 제네 25
## 10 아스트라 영국 20
## # ... with 582 more rows
동시 출현 빈도를 이용해 단어의 관계를 네트워크 형태로 표현한 것을 _동시 출현 네트워크(co-occurrence network)_라고 합니다. 동시 출현 네트워크를 이용하면 단어들이 어떤 맥락에서 함께 사용되었는지 이해할 수 있습니다.
as_tbl_graph()
동시 출현 네트워크를 만들려면 동시 출현 빈도 데이터를 ’네트워크 그래프 데이터’로 변환해야 합니다. tidygraph
패키지의 as_tbl_graph()
함수를 이용하면 네트워크 그래프 데이터를 만들 수 있습니다. 네트워크가 너무 복잡하지 않도록 pair
에서 25회 이상 등장한 단어만 추출해 네트워크 그래프 데이터를 만들어 보겠습니다. graph_pair
를 출력하면 단어를 나타내는 노드(node, 꼭짓점) 27개와 단어를 연결하는 엣지(edge, 선) 124개로 구성되어있음을 알 수 있습니다. 그래프를 만들 때 이 값들을 활용합니다.
#install.packages("tidygraph")
library(tidygraph)
##
## Attaching package: 'tidygraph'
## The following object is masked from 'package:stats':
##
## filter
graph_pair <- pair %>%
filter(n >= 25) %>%
as_tbl_graph()
graph_pair
## # A tbl_graph: 27 nodes and 124 edges
## #
## # A directed simple graph with 1 component
## #
## # Node Data: 27 x 1 (active)
## name
## <chr>
## 1 화이자
## 2 백신
## 3 접종
## 4 코로나
## 5 아스트라
## 6 az
## # ... with 21 more rows
## #
## # Edge Data: 124 x 3
## from to n
## <int> <int> <dbl>
## 1 1 2 459
## 2 2 1 459
## 3 3 2 278
## # ... with 121 more rows
ggraph()
ggraph
패키지의 ggraph()
를 이용하면 네트워크 그래프를 만들 수 있습니다.
ggraph()
에 graph_pair
를 입력한 다음 geom_edge_link()
를 추가해 단어를 엣지로 연결합니다.geom_node_point()
를 추가해 단어를 노드로 구성합니다.geom_node_text()
에 aes(label = name)
을 입력해 노드에 단어를 표시합니다.#install.packages("ggraph")
library(ggraph)
ggraph(graph_pair) +
geom_edge_link() + #엣지
geom_node_point() + #노드
geom_node_text(aes(label = name)) #텍스트트
## Using `stress` as default layout
네트워크 그래프를 보기 좋게 수정하겠습니다. 우선, 노드의 한글을 표현하는 데 사용할 폰트를 설정합니다.
#install.packages("showtext")
library(showtext)
## Warning: package 'showtext' was built under R version 4.0.5
## Loading required package: sysfonts
## Warning: package 'sysfonts' was built under R version 4.0.5
## Loading required package: showtextdb
## Warning: package 'showtextdb' was built under R version 4.0.5
font_add_google(name = "Nanum Gothic",
family = "nanumgothic")
showtext_auto()
함수의 파라미터를 사용해 엣지와 노드의 색깔, 크기, 텍스트 위치 등을 수정합니다. ggraph()
의 layout
은 네트워크의 형태를 정하는 기능을 합니다. layout
을 정하면 난수를 이용해 매번 다른 모양의 그래프를 만드므로 set.seed()
로 난수를 고정해 항상 같은 모양의 그래프를 만들도록 합니다.
set.seed(210524) # 오늘 날짜와 같은 임의의 숫자 조합으로 난수 고정
ggraph(graph_pair, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(color = "lightcoral", # 노드 색깔
size = 5) + # 노드 크기
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph() # 배경 삭제
형태소 분석 결과 잘못 토큰화 된 단어들을 수정하겠습니다. 또한 “az”와 “아스트라”와 같이 의미가 비슷한 단어가 개별 노드로 되어 있어 해석이 복잡해지는 문제를 수정해 보겠습니다. 표현은 다르지만 의미가 비슷한 단어를 유의어(synonym)라 합니다. 유의어를 한 단어로 통일하면 네트워크 구조가 간결해지고 단어의 관계가 좀 더 분명하게 드러납니다. cv_hls
에서 토큰을 수정하고 pair
에서 유의어를 통일한 다음 네트워크 그래프 데이터를 다시 만들겠습니다. 출력한 그래프를 보면 네트워크 구조가 간결해졌음을 알 수 있습니다.
# 토큰 오류 수정 및 유의어 처리
cv_hls <- cv_hls %>%
mutate(word = ifelse(str_detect(word, "아스트라"),
"아스트라제네카", word),
word = ifelse(str_detect(word, "모더"),
"모더나", word)) %>%
filter(word != "제네") %>%
mutate(word = ifelse(word == "아스트라제네카",
"az", word))
cv_hls %>% count(word, sort=T)
## # A tibble: 1,343 x 2
## word n
## <chr> <int>
## 1 백신 820
## 2 화이자 569
## 3 az 484
## 4 접종 425
## 5 코로나 281
## 6 승인 123
## 7 효과 106
## 8 맞다 98
## 9 이상 82
## 10 모더나 65
## # ... with 1,333 more rows
# 단어 동시 출현 빈도 구하기
pair <- cv_hls %>%
pairwise_count(item = word,
feature = rowid,
sort = T)
# 네트워크 그래프 데이터 만들기
graph_pair <- pair %>%
filter(n >= 25) %>%
as_tbl_graph()
# 네트워크 그래프 만들기
set.seed(210524) # 오늘 날짜와 같은 임의의 숫자 조합으로 난수 고정
ggraph(graph_pair, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(color = "lightcoral", # 노드 색깔
size = 5) + # 노드 크기
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph()
pfizer_pair <- pair %>%
filter(item1=="화이자"|item2=="화이자")
az_pair <- pair %>%
filter(item1=="az"|item2=="az")
pfizer_graph <- pfizer_pair %>%
filter(n >= 25) %>%
as_tbl_graph()
az_graph <- az_pair %>%
filter(n >= 25) %>%
as_tbl_graph()
# 화이자 네트워크 그래프 만들기
set.seed(210524) # 오늘 날짜와 같은 임의의 숫자 조합으로 난수 고정
ggraph(pfizer_graph, layout = "fr") + # 레이아웃
geom_edge_link(color = "gray50", # 엣지 색깔
alpha = 0.5) + # 엣지 명암
geom_node_point(color = "lightcoral", # 노드 색깔
size = 5) + # 노드 크기
geom_node_text(aes(label = name), # 텍스트 표시
repel = T, # 노드밖 표시
size = 5, # 텍스트 크기
family = "nanumgothic") + # 폰트
theme_graph()