manipulate_docs.Rmd
Manipulate Documents
라 쓰고 텍스트
텍스트 데이터 정제
라 이야기 합니다.
신문 기사나 소설, 수필과 같은 잘 정리된 텍스트 문서와 뉴스 진행자들이 전하는 뉴스 멘트들은 맞춤법에 부합하는 품질 높은 텍스트 데이터들입니다.
실제로 텍스트 분석에 직면하면 환상은 저 먼 나라의 이야기가 되어 버립니다. 맞춤법, 띄어쓰기가 무시된 텍스트는 그나마 애교가 있는 수준입니다.
통화 내용을 STT(Speech to Text)기법으로 텍스트로 변환한 데이터는 변환기의 성능이 완벽하지 않아서 품질이 매우 낮습니다. 화자와 청자의 의도는 유추하여 이해할 수 있겠으나, 텍스트 분석이라는 기계를 시켜서 수행하는 데이터 분석에는 부족함이 많습니다.
카페나 블로그의 게시글, SNS 채널의 글은 맞춤법, 띄어쓰기에 취약하고, 신조어나 암호같은 줄임말, 완전하지 않은 문장들이 포함됩니다. 경우에 따라서는 수집 과정에서 기술적인 한계로, 불필요한 텍스트들이 포함되기도 합니다. 그래서 데이터 정제없이 분석할 수 없는 경우가 많습니다. 어떤 경우는 수집한 텍스트 데이터가 데이터 분석을 수행하려는 목적과 부합하지 않아서 제거해야할 경우도 있습니다.
이처럼 텍스트 데이터 분석은 일반적인 데이터 분석에 비해서 데이터 정제가 차지하는 비중은 매우 큽니다. 텍스트 데이터 정제 성능은 텍스트 데이터 분석 성능과 직결되기 때문입니다.
텍스트 데이터 분석은 보통 형태소분석을 통해서 품사를 태깅하고, 품사 기반으로 토큰화된 단어로 텍스트 분석을 수행합니다.
문제는 분석에 사용하는 형태소분석기가, 문법과 띄어쓰기에 부합되는 품질 좋은 양질의 텍스트 데이터를 학습해서 만들어진 모델을 이용한다는 점입니다. 그래서 형태소분석을 수행하는 데이터의 품질이 떨어진다면, 형태소분석의 결과도 만족스럽지 못합니다. 어찌 보면 이러한 점이 데이터 정제를 하는 가장 큰 이유 중에 하나입니다.
문서의 품질이 높은 경우에도 문제가 발생할 수 있습니다. 형태소분석기에 사용한 학습 데이터는 우리가 일상 생활에서 이야기하는 대화의 주제, 혹은 직업, 학문과 예술, 종교 등 여러 분야의 내용을 모두 담지 못합니다. 학습 데이터는 지극히 일부의 샘플링된 문장들이라는 점입니다. 그래서 통상적인 생활에서 발화되는 단어가 아닌 전문성이 필요한 영역의 단어를 이해하지 못합니다.
알파고
가 쏘아 올린 화두가 학계와 필드의 AI 혁신을
이끌었습니다. 아마도 5년전에는 대중들은 알파고
라는 단어에
익숙하지 못했을 겁니다.
이처럼 형태소분석기가 취약한 신조어나, 특정 영역에서 사용하는 전문용어들은 사용자 정의 사전에 등록해서 형태소분석기가 이를 이해할 수 있도록 도와줘야 합니다. 이러한 작업들도 광의적으로 데이터를 정제를 수행하는 덱트스 데이터의 조작(Manipulate Documents)입니다.
bitNLP의 텍스트 데이터 조작 기능을 정리하면 다음과 같습니다.
bitNLP는 대용량의 텍스트 데이터에서 상기 데이터 조작을 수행할 수 있도록 도와줍니다. 그래서 다음과 같은 방법으로 작업합니다.
메터 데이터 이름 | 메타 데이터 아이디 | 변수이름 | 변수 설명 |
---|---|---|---|
문서 필터링 | filter | rule_nm | 개별 필터 룰의 이름 |
pattern | 문서 필터링을 위한 패턴 매치 정규표현식 | ||
accept | allow/deny 여부, TRUE는 allow 패턴, FALSE는 deny 패턴 | ||
use | 개별 룰 사용여부, FALSE이면 미사용, TRUE인 건만 사용 | ||
텍스트 대체 | replace | rule_nm | 개별 대체 룰의 이름 |
rule_class | 텍스트 대체 룰의 그룹 이름 | ||
pattern | 텍스트 대체를 위한 패턴 매치 정규표현식 | ||
replace | 패턴에 매치된 텍스트를 대체할 텍스트 정의 | ||
use | 개별 룰 사용여부, FALSE이면 미사용, TRUE인 건만 사용 | ||
텍스트 연결 | concat | rule_nm | 개별 연결 룰의 이름 |
pattern | 텍스트 연결을 위한 패턴 매치 정규표현식 | ||
replace | 패턴에 매치된 텍스트를 대체할 텍스트 정의 | ||
use | 개별 룰 사용여부, FALSE이면 미사용, TRUE인 건만 사용 | ||
텍스트 분리 | split | rule_nm | 개별 분리 룰의 이름 |
pattern | 텍스트 분리를 위한 패턴 매치 정규표현식 | ||
replace | 패턴에 매치된 텍스트를 대체할 텍스트 정의 | ||
use | 개별 룰 사용여부, FALSE이면 미사용, TRUE인 건만 사용 | ||
텍스트 제거 | remove | rule_nm | 개별 제거 필터의 이름 |
pattern | 텍스트 제거를 위한 패턴 매치 정규표현식 | ||
use | 개별 룰 사용여부, FALSE이면 미사용, TRUE인 건만 사용 |
set_meta()
함수는 세션 안에서 bitNLP 패키지의 메타
데이터를 등록합니다.
다음의 set_meta()
함수의 원형을 보면 데이터 파일을 읽는
방법과 유사합니다.
set_meta(
id = c("filter", "replace", "remove", "concat", "split"),
filename,
sep = ",",
fileEncoding = "utf-8",
append = FALSE
)
bitNLP 패키지는 샘플 메타 데이터 파일을 제공하는데, 문서 필터링을 위한 샘플 메타 데이터 파일을 읽어 봅니다.
library(bitNLP)
meta_path <- system.file("meta", package = "bitNLP")
fname <- glue::glue("{meta_path}/preparation_filter.csv")
## 데이터 필터링 메타 신규 등록
set_meta("filter", fname, fileEncoding = "utf8")
get_meta()
함수는 세션 안에서 등록된 메타 데이터를
조회합니다.
## 기 등록된 데이터 필터링 메타 조회
get_meta("filter")
rule_nm1 신문기사
2 제품홍보
3 설문조사
4 출처
5 이벤트
6 방송
pattern1 (팍스넷|파이낸셜|연합|(PT)|오마이|경제)[[:space:]]*뉴스
2 ((입법|정치|교육)[[:space:]]*플랫폼)|맘마미아[[:space:]]*가계부[[:print:]]*인증샷|Playtex
3 좌담회|구글설문|채용대행업체
4 출처[[:space:]]*:|문의처보건복지콜센터
5 (증정|기념)이벤트|허니스크린|이벤트를[[:space:]]*진행
6 제작진|기억저장소|추모카페|블랙홀|푸드스튜디오|연금정보넷
accept use1 FALSE TRUE
2 FALSE TRUE
3 FALSE TRUE
4 FALSE TRUE
5 FALSE TRUE
6 FALSE TRUE
텍스트 데이터(문서들) 중에서 분석을 수행하려는 목적과 부합하지 않은
텍스트(문서)를 제거해야할 경우에는 filter_text()
를
사용합니다.
이미 앞에서 문서 필터링을 위한 메타 데이터 파일을 읽어들였습니다.
6개의 룰은 accept
값이 FALSE인 deny 룰입니다. 즉 해당 검색
패턴을 만족하는 텍스트 데이터를 제거하는 작업을 수행합니다.
버즈 데이터의 본문은 길이가 1000인 문자 벡터입니다. 이 벡터는 5개의 결측치를 포함하고 있습니다.
<- buzz$CONTENT
doc_content is.character(doc_content)
1] TRUE
[length(doc_content)
1] 1000
[
sum(is.na(doc_content))
1] 5 [
8개의 코어를 이용해서 필터링을 수행합니다.
as_logical = FALSE
을 지정하면 문자 벡터의 필터링을 수행할
수 있습니다.
<- filter_text(doc_content, as_logical = FALSE, mc.cores = 8)
doc_after_character : 방송 ──────────────────────────────────────────────────────── 3건 ──
── rejects: 설문조사 ──────────────────────────────────────────────────── 1건 ──
── rejects: 신문기사 ──────────────────────────────────────────────────── 1건 ──
── rejects: 이벤트 ────────────────────────────────────────────────────── 1건 ──
── rejects: 제품홍보 ──────────────────────────────────────────────────── 2건 ──
── rejects: 출처 ──────────────────────────────────────────────────────── 2건 ──
── rejects: Removing NA ─────────────────────────────────────────── 5건 ──
── Missing Check
length(doc_after_character)
1] 985 [
5개의 결측치와 6개의 룰에서 10개의 문서가 제거되어서 길이가 985인 문자 벡터가 만들어졌습니다.
tidytext 패키지를 이용해서 텍스트 데이터 분석을 수행한다면, 문자 벡터의 필터링이 아니라 문자 변수를 이용한 필터링을 수행해야 합니다.
다음처럼 as_logical
인수의 기본값인 TRUE를 사용합니다.
이 경우는 CONTENT
변수의 모든 원소에 대해서 allow 필터링
여부를 의미하는 논리 벡터를 만들어 반환합니다. 그러므로
dplyr
패키지의 filter
함수와 사용하여
필터링합니다.
library(dplyr)
%>%
buzz filter(filter_text(CONTENT, verbos = FALSE)) %>%
select(KEYWORD, SRC, CONTENT)
38;5;246m# A tibble: 985 × 3
[39m
[
KEYWORD SRC CONTENT 38;5;246m<chr>
[39m
[23m
[3m
[38;5;246m<chr>
[39m
[23m
[3m
[38;5;246m<chr>
[39m
[23m
[3m
[38;5;250m1
[39m 맞벌이 17,18년 베이비맘
[38;5;246m"
[39m지금 둘째 임신중인 어머니예요 첫째는 16년 1월생 둘…
[
[38;5;250m2
[39m 맞벌이 20대 수다방
[38;5;246m"
[39m저희 부부는 맞벌이인데요 남편 회사 사람들도 거의 다…
38;5;250m3
[39m 맞벌이 20대 수다방
[38;5;246m"
[39m신랑지출 제지출 구분해서 따로적으시나요 제가쓴돈은 …
[
[38;5;250m4
[39m 맞벌이 20대 수다방
[38;5;246m"
[39m너무 고민이 되서 하소연 할때 없어서 여기서 하소연 …
38;5;246m# … with 981 more rows
[39m
[
문서 안에 포함된 특정 텍스트를 다른 텍스트로 대체하기 위해서는
replace_text()
를 사용합니다. as_logical
인수만
없을 뿐 사용 방법은 filter_text()
와 유사합니다.
<- system.file("meta", package = "bitNLP")
meta_path <- glue::glue("{meta_path}/preparation_replace.csv")
fname set_meta("replace", fname, fileEncoding = "utf8")
# 등록된 문자열 대체 룰 확인하기
get_meta("replace")
rule_nm rule_class1 다중 구두점 구두점 대체
2 남편 유사단어 대체
3 베이비시터 유사단어 대체
4 텔레비전 유사단어 대체
5 CCTV 유사단어 대체
6 할머니 유사단어 대체
7 어머니 유사단어 대체
8 아버지 유사단어 대체
9 아들 유사단어 대체
10 딸 유사단어 대체
11 화이팅 유사단어 대체
12 모유량 유사단어 대체
13 베개 유사단어 대체
14 초산 유사단어 대체
15 급여 유사단어 대체
pattern replace use1 (\\.){2,} \\. TRUE
2 신랑|남편 남편 TRUE
3 베비시터|((육아|아이|아기)[[:space:]]*(도우미|돌보미)) 베이비시터 TRUE
4 TV|테레비|티브이|텔레비젼|티비 텔레비전 TRUE
5 (CC|cc|씨씨)[[:space:]]?(텔레비전|티비|tv|TV) CCTV TRUE
6 할(미|머님|무니|매) 할머니 TRUE
7 엄마|어머님|엄니|어무니 어머니 TRUE
8 아버님|아빠|아부지 아버지 TRUE
9 아들(래미|아이|애|내미|램) 아들 TRUE
10 딸(래미|아이|애|내미|램) 딸 TRUE
11 파이팅|홧팅|퐈이팅 화이팅 TRUE
12 모유[[:space:]]?[양|량] 모유량 TRUE
13 [베배][게개] 베개 TRUE
14 (첫|처음)[[:space:]]*출산 초산 TRUE
15 월급|봉급 급여 TRUE
남편
이라는 단어와 신랑
이라는 단어를 포함한
문장의 수는 각각 175개와 177개입니다. 그러나 이 두 단어는 동의어입니다.
그래서 텍스트 대체 룰에는 이 두 단어를 남편
이라는 하나의
단어로 표준화했습니다.
<- buzz$CONTENT
doc_content
::str_detect(doc_content, "남편") %>%
stringrsum(na.rm = TRUE)
1] 175
[
::str_detect(doc_content, "신랑") %>%
stringrsum(na.rm = TRUE)
1] 177 [
문서들에서 몇 개의 룰이 적용되는지 결과를 보면서 텍스트를 대체합니다.
신랑
이라는 단어가 남편
으로 대체되었음을 알 수
있습니다.
<- buzz %>%
buzz_after mutate(CONTENT = replace_text(CONTENT, verbos = TRUE))
: [구두점 대체] - 다중 구두점 ───────────────────────────────── 2건 ──
── Replace: [유사단어 대체] - CCTV ────────────────────────────────────── 3건 ──
── Replace: [유사단어 대체] - 급여 ────────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 남편 ──────────────────────────────────── 323건 ──
── Replace: [유사단어 대체] - 딸 ──────────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 모유량 ──────────────────────────────────── 1건 ──
── Replace: [유사단어 대체] - 베개 ────────────────────────────────────── 5건 ──
── Replace: [유사단어 대체] - 베이비시터 ──────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 아들 ────────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 아버지 ──────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 어머니 ──────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 초산 ────────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 텔레비전 ────────────────────────────────── 3건 ──
── Replace: [유사단어 대체] - 할머니 ──────────────────────────────────── 0건 ──
── Replace: [유사단어 대체] - 화이팅 ──────────────────────────────────── 0건 ──
── Replace
::str_detect(buzz_after$CONTENT, "남편") %>%
stringrsum(na.rm = TRUE)
1] 323
[
::str_detect(buzz_after$CONTENT, "신랑") %>%
stringrsum(na.rm = TRUE)
1] 0 [
띄어쓰기된 단어들을 하나의 단어로 묶어주기 위해서
concat_text()
를 사용합니다.
<- system.file("meta", package = "bitNLP")
meta_path <- glue::glue("{meta_path}/preparation_concat.csv")
fname set_meta("concat", fname, fileEncoding = "utf8")
# 등록된 문자열 결합 룰 확인하기
get_meta("concat")
rule_nm pattern replace use1 (하원도우미) 붙여쓰기 하원[[:space:]]+도우미 하원도우미 TRUE
2 (가사도우미) 붙여쓰기 가사[[:space:]]+도우미 가사도우미 TRUE
3 (산후도우미) 붙여쓰기 산후[[:space:]]+도우미 산후도우미 TRUE
4 (친정어머니) 붙여쓰기 친정[[:space:]]+어머니 친정어머니 TRUE
5 (베이비시터) 붙여쓰기 베이비[[:space:]]+시터 베이비시터 TRUE
6 (연말정산) 붙여쓰기 연말[[:space:]]+정산 연말정산 TRUE
7 (출산휴가) 붙여쓰기 출산[[:space:]]+휴가 출산휴가 TRUE
8 (시어머니) 붙여쓰기 시[[:space:]]+어머니 시어머니 TRUE
9 (육아휴직) 붙여쓰기 육아[[:space:]]+휴직 육아휴직 TRUE
일반적으로 복합명사를 정의하는 사례들입니다.
가사도우미
라는 단어는 가사
와
도우미
가 결합된 복합명사입니다. 그런데 두 단어가 띄어쓰기된
경우가 있습니다.
<- buzz$CONTENT
doc_content
::str_detect(doc_content, "가사도우미") %>%
stringrsum(na.rm = TRUE)
1] 1
[
::str_detect(doc_content, "가사[[:space:]]+도우미") %>%
stringrsum(na.rm = TRUE)
1] 22 [
문서들에서 몇 개의 룰이 적용되는지 결과를 보면서 텍스트를 연결합니다.
두 단어가 띄어쓰기된 가사 도우미
가 수정되었습니다.
<- buzz %>%
buzz_after mutate(CONTENT = concat_text(CONTENT, verbos = TRUE))
: (가사도우미) 붙여쓰기 ─────────────────────────────────────── 22건 ──
── Concat: (베이비시터) 붙여쓰기 ──────────────────────────────────────── 1건 ──
── Concat: (산후도우미) 붙여쓰기 ──────────────────────────────────────── 1건 ──
── Concat: (시어머니) 붙여쓰기 ────────────────────────────────────────── 1건 ──
── Concat: (연말정산) 붙여쓰기 ────────────────────────────────────────── 1건 ──
── Concat: (육아휴직) 붙여쓰기 ────────────────────────────────────────── 2건 ──
── Concat: (출산휴가) 붙여쓰기 ────────────────────────────────────────── 1건 ──
── Concat: (친정어머니) 붙여쓰기 ──────────────────────────────────────── 5건 ──
── Concat: (하원도우미) 붙여쓰기 ──────────────────────────────────────── 1건 ──
── Concat
::str_detect(buzz_after$CONTENT, "가사도우미") %>%
stringrsum(na.rm = TRUE)
1] 23
[
::str_detect(buzz_after$CONTENT, "가사[[:space:]]+도우미") %>%
stringrsum(na.rm = TRUE)
1] 0 [
이렇게 수정된 문서들이 형태소분석을 통해서 토큰화되어 분석을 수행한다면, 형태소분석기에도 복합명사가 등록되어 있어야 합니다. 안그러면 단어를 연경하여 복합명사를 만든어 놓아도 토큰화 과정에서 다시 분리됩니다.
다음처럼 mecab-ko의 사전에는 가사도우미
라는 명사가
등록되어 있지 않습니다. 이 경우에는 사용자 정의 사전으로 토큰화 과정에서
다시 분리되지 않도록 유도해야 합니다.
morpho_mecab("가사도우가 집안 청소를 했다.")
NNG NNG NNG NNG "가사" "도우" "집안" "청소"
묶어진 단어를 다시 분리할 경우에는 split_text()
를
사용합니다.
<- system.file("meta", package = "bitNLP")
meta_path <- glue::glue("{meta_path}/preparation_split.csv")
fname set_meta("split", fname, fileEncoding = "utf8")
# 등록된 문자열 분리 룰 확인하기
get_meta("split")
rule_nm1 (도우미) 유형 띄어쓰기
pattern replace use1 (하원|등하원|등원|입주|교포|가사|산후|보육|산모)(도우미) \\1 \\2 TRUE
가사도우미
를 주제로 하는 것이 아니라
도우미
를 주제로 분석하려 합니다. 도우미
가
들어간 복합명사를 분리해서 도우미
라는 독립된 단어를
만들고자 합니다. concat_text()
의 사례와는 반대의
경우입니다.
<- buzz$CONTENT
doc_content
::str_extract_all(doc_content, "(하원|등하원|등원|입주|교포|가사|산후|보육|산모)(도우미)") %>%
stringrunlist() %>%
na.omit() %>%
as.vector()
1] "산후도우미" "입주도우미" "입주도우미" "입주도우미" "입주도우미"
[6] "등원도우미" "등원도우미" "가사도우미" "등원도우미" "등원도우미"
[11] "보육도우미" [
도우미
가 들어간 복합명사들이 모두 분리되었습니다.
<- buzz %>%
buzz_after mutate(CONTENT = split_text(CONTENT, verbos = TRUE))
: (도우미) 유형 띄어쓰기 ──────────────────────────────────────── 6건 ──
── Split
::str_detect(buzz_after$CONTENT, "(하원|등하원|등원|입주|교포|가사|산후|보육|산모)(도우미)") %>%
stringrsum(na.rm = TRUE)
1] 0 [
문서 안에 불필요한 텍스트들이 포함되어 있을 수 있습니다. 그래서 문서
내에서 패턴 검색으로 불필요한 텍스트를 골라내어 제거할 수 있습니다.
remove_text()
를 사용합니다.
<- system.file("meta", package = "bitNLP")
meta_path <- glue::glue("{meta_path}/preparation_remove.csv")
fname set_meta("remove", fname, fileEncoding = "utf8")
# 등록된 문자열 제거 룰 확인하기
get_meta("remove")
rule_nm pattern use1 카페 안내문구 1 게시판[[:space:]]*이용전[[:print:]]*이동됩니다. TRUE
2 카페 안내문구 2 카페이용 전[[:print:]]*참고\\) TRUE
3 카페 안내문구 3 게시글 작성[[:print:]]*35756864 TRUE
4 카페 안내문구 4 흥부야[[:print:]]*기타 하고 싶은 말 TRUE
5 URL 문구 (http|www)([a-zA-Z0-9\\>\\/\\.\\:\\=\\&\\_])* TRUE
수집한 카페의 게시글에는 불필요한 텍스트들이 포함될 수 있습니다.
다음은 카페의 게시글을 작성할 때, 관리자가 미리 설정해 놓은 주의사항을 삭제하지 않고 게시글을 작성한 문서들을 조회한 사례입니다. 그리고 불필요한 주의사항을 제거한 후의 내용은 어느 정도 정제가 되었습니다.
<- buzz$CONTENT
doc_content
::str_detect(doc_content, "게시판[[:space:]]*이용전[[:print:]]*이동됩니다.") %>%
stringr
which1] 61 65 67 69 79 82 237 239 245 251 252 256 257 400 403 406 416 417 419
[20] 563 569 571 584 732 737 739 740 903 910 912 915 921 922
[
61]
doc_content[1] " 게시판 이용전반드시 카페규정 http://cafe.naver.com/imsanbu/28123090을 미리 숙지 당부드립니다.수다방 성격에 맞지않는 이탈글은 적합한 게시판으로 이동 또는 삭제예정게시판으로 이동됩니다.인천살구 아기는 15개월이에요 선천성기형인 이루공인데 수술하신분들은 어디서하셨나요 지금은 인천국제성모에서진료중이고 고름나기시작해서 수술을하게되면 아주대에서할까하는데 아기가 너무어려서 걱정입니다경험하신분들 조언좀해주세요"
[
::str_remove(doc_content[61], "게시판[[:space:]]*이용전[[:print:]]*이동됩니다.")
stringr1] " 인천살구 아기는 15개월이에요 선천성기형인 이루공인데 수술하신분들은 어디서하셨나요 지금은 인천국제성모에서진료중이고 고름나기시작해서 수술을하게되면 아주대에서할까하는데 아기가 너무어려서 걱정입니다경험하신분들 조언좀해주세요" [
remove_text()
로 불필요한 텍스트를 제거한 후에, 앞의
사례인 “게시판[[:space:]]*이용전[[:print:]]*이동됩니다.”를 조회했습니다.
해당 문장이 삭제되어 패턴 검색이 되지 않았습니다.
<- buzz %>%
buzz_after mutate(CONTENT = remove_text(CONTENT, verbos = TRUE))
: URL 문구 ─────────────────────────────────────────────────── 40건 ──
── Removes: 카페 안내문구 1 ──────────────────────────────────────────── 33건 ──
── Removes: 카페 안내문구 2 ───────────────────────────────────────────── 9건 ──
── Removes: 카페 안내문구 3 ──────────────────────────────────────────── 47건 ──
── Removes: 카페 안내문구 4 ──────────────────────────────────────────── 16건 ──
── Removes
::str_detect(buzz_after$CONTENT, "게시판[[:space:]]*이용전[[:print:]]*이동됩니다.") %>%
stringrsum(na.rm = TRUE)
1] 0 [