본문으로 이동
메뉴 여닫기
환경 설정 메뉴 여닫기
개인 메뉴 여닫기
로그인하지 않음
지금 편집한다면 당신의 IP 주소가 공개될 수 있습니다.
Document-Term Matrix; DTM, 문서-용어 행렬, 단어 문서 행렬(Term-Document Matrix, TDM)
문서를 행, 단어를 열로 두고 각 칸에 단어가 그 문서에 나온 횟수(또는 가중치)를 넣은 행렬

텍스트 마이닝에서 문장은 그대로 통계 모델에 넣을 수 없으므로, 문서마다 어떤 단어가 몇 번 나왔는지를 세어 숫자 행렬로 바꾼다. 단어의 순서는 버리고 출현 빈도만 남기므로 Bag of Words 표현이라고도 한다. 문서 단어 행렬(DTM)은 문서가 행, 단어가 열이고, 이를 전치한 단어 문서 행렬(TDM)은 단어가 행, 문서가 열이다. 두 행렬은 같은 정보를 담고 있으며 R tm 패키지는 두 형태를 모두 제공한다.

이 행렬은 군집 분석, 분류(나이브 베이즈, 서포트 벡터 머신), 연관어 분석, 감성 분석, 워드 클라우드 같은 이후 분석의 입력이 된다.

구분 행 열 R tm 함수 비고
문서 단어 행렬(DTM) 문서 단어 DocumentTermMatrix() 문서를 관측치, 단어를 변수로 보는 형태여서 분류·군집 모델 입력에 맞다
단어 문서 행렬(TDM) 단어 문서 TermDocumentMatrix() 단어별 빈도 합계나 단어 사이 연관성을 볼 때 편하다
  • 문서 수가 많아지면 단어 수(열)가 수만 개로 늘어나고, 한 문서에는 일부 단어만 나오므로 대부분의 칸이 0인 희소 행렬(sparse matrix)이 된다. 그래서 0이 아닌 칸만 저장하는 형식을 쓴다(tm은 slam 패키지의 simple triplet matrix, scikit-learn은 scipy CSR 행렬).
  • 희소성(sparsity)은 전체 칸 가운데 0인 칸의 비율이다. 거의 나오지 않는 단어를 지워 차원을 줄인다.

코퍼스와 전처리

편집 원본 편집

코퍼스(corpus, 말뭉치)는 분석 대상 문서의 모음이다. 문서 단어 행렬을 만들기 전에 다음 전처리를 한다.

전처리 내용 R tm
토큰화(tokenization) 문장을 단어(토큰) 단위로 나눈다. 한국어는 조사·어미가 붙으므로 형태소 분석으로 명사 등을 뽑는 경우가 많다 행렬 생성 시 기본 토크나이저 사용
소문자화 대소문자를 통일해 "Data"와 "data"를 같은 단어로 센다 tm_map(x, content_transformer(tolower))
문장 부호·숫자 제거 분석에 의미 없는 기호와 숫자를 지운다 removePunctuation, removeNumbers
불용어 제거 the, a, is, 그리고, 하지만처럼 자주 나오지만 의미 구분에 도움이 안 되는 단어를 지운다 removeWords, stopwords("english")
어간 추출(stemming) 어미를 잘라 같은 뿌리로 묶는다(analysis, analyses → analys). tm은 Porter 알고리즘(SnowballC)을 쓴다 stemDocument
공백 정리 여러 칸의 공백을 하나로 줄인다 stripWhitespace
  • 어간 추출과 비슷한 표제어 추출(lemmatization)은 사전을 이용해 기본형(better → good)으로 바꾼다.
  • 동의어·표기 변형을 하나로 합치는 사전 처리를 하기도 한다.

tm은 R의 대표적인 텍스트 마이닝 패키지로, 설계는 Feinerer·Hornik·Meyer(2008)의 논문에 설명되어 있다.

함수 기능
VectorSource(), DirSource() 문자 벡터·디렉터리를 문서 공급원(source)으로 지정
Corpus(), VCorpus() 코퍼스 생성. VCorpus는 메모리에 두는 휘발성 코퍼스이다
tm_map(x, FUN, ...) 코퍼스의 모든 문서에 변환 함수를 적용한다. 일반 문자열 함수는 content_transformer()로 감싼다
TermDocumentMatrix(), DocumentTermMatrix() 단어 문서 행렬·문서 단어 행렬 생성. control 인자로 가중치(weighting) 등을 지정한다
inspect() 코퍼스나 행렬의 내용 확인
findFreqTerms(x, lowfreq, highfreq) 출현 빈도가 lowfreq 이상 highfreq 이하인 단어 목록
findAssocs(x, terms, corlimit) 지정한 단어와 상관계수가 corlimit 이상인 단어(연관어)
removeSparseTerms(x, sparse) 희소성이 sparse 이상인 단어 제거
weightTf, weightTfIdf, weightBin 빈도, TF-IDF, 이진(있음/없음) 가중치
library(tm)
docs <- c("Data analysis needs clean data.",
          "Text mining turns text into data.",
          "The network analysis finds key nodes.")
corp <- VCorpus(VectorSource(docs))
corp <- tm_map(corp, content_transformer(tolower))
corp <- tm_map(corp, removePunctuation)
corp <- tm_map(corp, removeNumbers)
corp <- tm_map(corp, removeWords, stopwords("english"))
corp <- tm_map(corp, stripWhitespace)
# corp <- tm_map(corp, stemDocument)   # 어간 추출(SnowballC 필요)

tdm <- TermDocumentMatrix(corp)
inspect(tdm)
findFreqTerms(tdm, lowfreq = 2)
findAssocs(tdm, "data", 0.5)
dtm <- DocumentTermMatrix(corp, control = list(weighting = weightTfIdf))
dtm2 <- removeSparseTerms(DocumentTermMatrix(corp), 0.8)

Python scikit-learn 예시

편집 원본 편집

scikit-learn의 CountVectorizer는 토큰화, 소문자화(lowercase=True가 기본), 불용어 제거(stop_words="english")를 한 번에 처리해 문서 단어 행렬을 만든다. 결과는 scipy 희소 행렬이다.

import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

docs = ["Data analysis needs clean data.",
        "Text mining turns text into data.",
        "The network analysis finds key nodes."]
vec = CountVectorizer(lowercase=True, stop_words="english")
X = vec.fit_transform(docs)            # 문서 단어 행렬(희소 행렬)
print(X.shape, "0이 아닌 칸:", X.nnz)
dtm = pd.DataFrame(X.toarray(), index=["d1", "d2", "d3"],
                   columns=vec.get_feature_names_out())
print(dtm.T)                           # 전치하면 단어 문서 행렬

tf = TfidfVectorizer(stop_words="english")
T = tf.fit_transform(docs)
print(pd.DataFrame(T.toarray(), index=["d1", "d2", "d3"],
                   columns=tf.get_feature_names_out()).T.round(3))

scikit-learn 1.7.2로 실행한 결과를 표로 옮기면 다음과 같다. 행렬 크기는 (3, 11)이고 0이 아닌 칸은 13개이므로, 33칸 가운데 20칸(약 61%)이 0이다. "the", "into"는 불용어로 빠졌다.

단어 d1 빈도 d2 빈도 d3 빈도 d1 TF-IDF d2 TF-IDF d3 TF-IDF
analysis 1 0 1 0.344 0.000 0.355
clean 1 0 0 0.452 0.000 0.000
data 2 1 0 0.688 0.297 0.000
finds 0 0 1 0.000 0.000 0.467
key 0 0 1 0.000 0.000 0.467
mining 0 1 0 0.000 0.390 0.000
needs 1 0 0 0.452 0.000 0.000
network 0 0 1 0.000 0.000 0.467
nodes 0 0 1 0.000 0.000 0.467
text 0 2 0 0.000 0.780 0.000
turns 0 1 0 0.000 0.390 0.000
  • d2에서 text는 2번 나오고 다른 문서에는 없으므로 TF-IDF가 0.780으로 가장 크다. data는 d2에도 나오지만 두 문서에 걸쳐 있어 d2에서는 0.297로 낮아졌다.

단순 빈도는 여러 문서에 두루 나오는 흔한 단어를 과대평가한다. TF-IDF는 한 문서 안에서 자주 나오면서(TF) 다른 문서에는 잘 나오지 않는(IDF) 단어에 큰 가중치를 준다.

tfidf(t,d)=tf(t,d)×log⁡Ndf(t)

  • N은 전체 문서 수, df(t)는 단어 t가 나온 문서 수이다. 모든 문서에 나오는 단어는 log(N/N) = 0이 되어 가중치가 0이다.
  • 구현마다 세부 식이 다르다. tm의 weightTfIdf는 밑이 2인 로그를 쓰고 기본으로 문서 길이로 정규화한 TF를 곱한다. scikit-learn TfidfVectorizer는 기본값(smooth_idf=True)에서 idf = ln((1 + N) / (1 + df)) + 1을 쓰고, 문서 벡터를 L2 정규화한다. 그래서 같은 데이터라도 값이 다르다.
  • DTM은 문서×단어, TDM은 단어×문서라는 행·열 방향을 구분한다. tm의 TermDocumentMatrix()와 DocumentTermMatrix()도 이 방향을 따른다.
  • 전처리 순서와 각 단계의 tm 함수(tm_map, tolower, removePunctuation, removeNumbers, removeWords, stopwords, stemDocument, stripWhitespace)를 묻는다.
  • findFreqTerms는 빈도 기준 단어 찾기, findAssocs는 상관계수 기준 연관어 찾기, removeSparseTerms는 희소 단어 제거라는 차이를 기억한다.
  • TF-IDF의 의미(모든 문서에 나오는 단어는 가중치가 낮다)와 계산 원리를 이해한다.