Bag of Words

3 minute read

1. 단어를 세기(count)

문장 혹은 말뭉치(corpus) 들을 컴퓨터에게 주고 그 문장에 등장하는 단어를 세는 것이다.

13인의아해가도로로질주하오.
(길은막다른골목이적당하오.)

제1의아해가무섭다고그리오.
제2의아해도무섭다고그리오.
제3의아해도무섭다고그리오. 제4의아해도무섭다고그리오.

제13의아해도무섭다고그리오.
13인의아해는무서운아해와무서워하는아해와그렇게뿐이모였소.(다른사정은없는것이차라리나았소)

그중에1인의아해가무서운아해라도좋소.
그중에2인의아해가무서운아해라도좋소.
그중에2인의아해가무서워하는아해라도좋소.
그중에1인의아해가무서워하는아해라도좋소.

(길은뚫린골목이라도적당하오.)
13인의아해가도로로질주하지아니하여도좋소.

이상의 오감도라는 시가 있다.
이 시에서 각 단어들을 세어보자.

1 2 11 12 13 아해 무섭다고 그리오 무서운 무서워하는 좋소
2 2 1 1 4 13 20 13 13 3 2 5

이렇게 단어를 단어의 등장 순서와 관계 없이 그 단어가 등장한 횟수(빈도)를 나타내는 것을
백 오브 워즈(Bag of words)이라고 한다.
이렇게 나타낸 행렬로 단어를 임베딩을 하는 것이다.
마치 가방 안에 단어들이 들어가서 순서에 상관 없이 빈도만 체크한다 하여 Bag of words 라고 불린다.

이렇게 나타내면, 문학도가 아니더라도 이 시에서 중요한 의미를 가지는 단어로
‘아해’, ‘13’, ‘무섭다’ 의 단어가 있다는 사실을 유추해낼 수 있다.
시가 아닌 어떤 설명글이라고 한다면, 내용은 이해 못해도 많이 나오는 단어로 그 주제 혹은 주제어를 파악할 수 있다.

이 BoW 를 여러개를 묶어보도록 하자.

여기 네 문장이 있다.

문장 1 : 나는 쇼팽의 음악을 듣습니다.
문장 2 : 어제 음악 시간에 잤어요.
문장 3 : 나는 음악 듣는 것을 좋아합니다.
문장 4 : 음악은 역시 클래식 음악.

이 문장들의 BoW를 표로 나타내면 다음과 같다.

단어문장 문장 1 문장 2 문장 3 문장 4
나는 1 0 1 0
쇼팽 1 0 0 0
어제 0 1 0 0
음악 1 1 1 2
을(를) 1 0 1 0
0 0 0 1
역시 0 0 0 1
클래식 0 0 0 1
시간 0 1 0 0
잤어요 0 1 0 0
듣습니다 1 0 0 0
듣는 0 0 1 0
것을 0 0 1 0
좋아합니다 0 0 1 0

이를 단어-문서 행렬(Term-Document Matrix) 라고 한다.
이 행렬을 통해 문서 간의 유사도를 알 수 있다.
(혹은 Document-Term-Matrix 이나 지금은 단어가 행이기 때문에 단어-문서 행렬이라 부르겠다.)

이 행렬은 단어의 수 V, 문서의 수 D 로 V x D 차원의 행렬이다.
이때 음악이라는 단어의 3행을 보자(위부터 0행으로 카운트한다.)
3행은 [1, 1, 1, 2] 로, 다른 행들보다 값이 크다.

이 사실로 네 문장이 음악이라는 주제를 공유하며, 특히 4번째 문장에서 음악이라는 단어가 중요하게 쓰이는 것을 알 수 있다.
행렬의 각 값들은 TF (Term-Frequency) 라고 한다. 단어를 뜻하는 term 과 빈도를 나타내는 frequency 의 합성어이다.

문서 간의 유사도를 점검하려면 TF 만으로는 부족하지 싶다.
어떤 단어가 몇 개의 문장에서 나타났는지 구하면, 문장 간의 유사도를 더 정확히 알 수 있다.
이를 DF 라고 한다. (Document-Frequency)
예를 들어 음악이라는 단어는 네 문장에서 다 나타났으므로 4라고 할 수 있다.

이 TDM 의 값인 TF 와 DF 를 곱하면, 여러 문서에 등장한 단어에 더 가중치를 둘 수 있다. (주: TDM 과 DF 의 행렬 곱셈이 아닌 원소별 곱셈이다.)

그러나 만일 어떤 단어가 모든 문서에서 많이 등장한다면, 이를 특별한 단어라고 볼 수 없다.
예를 들어 소설책들을 분석한다면, 1인칭 시점인 소설에서 자주 등장하는 ‘나는’ 이라는 단어는
문장과 소설을 해석하는데 크게 중요한 단어가 아니다.

그래서 그냥 DF를 곱하는 것이 아니라, 전체 문장 수를 DF 만큼 나눈 만큼 곱하기로 한다.
이때 그냥 DF를 가져가면 값이 매우 커지므로 log 을 취해주기로 하자.
이를 IDF (Inversion-Document Frequency) 라고 한다.
단어를 t, 전체 문장 수를 N 이라할 때 어떤 단어 t의 IDF 는 log(N/(1+df(t)) 이다.
0번 등장하는 경우를 고려해 분모에 1을 더해주었다.
이 IDF를 TF에 곱한 행렬은 다음과 같은 코드로 구한다.

import numpy as np
import pandas as pd

data = {
    '나는' : np.array([1,0,1,0]),
    '쇼팽' : np.array([1,0,0,0]),
    '어제' : np.array([0,1,0,0]),
    '음악' : np.array([1,1,1,2]),
    '을(를)' : np.array([1,0,1,0]),
    '은' : np.array([0,0,0,1]),
    '역시' : np.array([0,0,0,1]),
    '클래식' : np.array([0,0,0,1]),
    '시간' : np.array([0,1,0,0]),
    '잤어요' : np.array([0,1,0,0]),
    '듣는' : np.array([0,0,1,0]),
    '것을' : np.array([0,0,1,0]),
    '좋아합니다' : np.array([0,0,1,0])
}

tdm = pd.DataFrame.from_dict(data, orient="index")
dm = np.array(np.sum([tf > 0 for tf in tdm.values], axis=1))[:, np.newaxis]
idm = np.log(len(tdm)/(1+dm))
tf_idm = pd.DataFrame((tdm.values * idm), columns=['문장1', '문장2', '문장3', '문장4'], index=data.keys())
print(tf_idm)

위 코드의 출력은 이러하다.

image

그냥 df만을 곱했다면 결과는 이렇다.

image

그냥 df 를 곱했을 때보다, idf 를 곱하니까 여러 문장에서 등장한 단어들의 빈도(중요도)가 낮아졌다.
지금의 예는 문장이 매우 적고 단순해서 그렇지, 이를 많은 데이터에 적용시키면 ‘나는’이나 ‘을’, ‘한다’ 같은
문서의 주제 파악에 별 도움이 안 되는 단어들에 대해선 값이 0에 가까워지게 된다.

위 행렬을 TF-IDF 행렬이라고 한다.
단, 이러한 BoW나 TF-IDF 같은 모델의 단점으로는 행렬이 sparse matrix 일 확률이 높다는 점이다.
자주 나오는 단어 몇 개 말고는 거의 다 0일 것이므로, 공간, 시간 상 낭비가 있다.
그래도 컴퓨터가 실제 언어를 이해하게 하는데는 어렵지만 임베딩이 간편하다는 장점이 있어
실제 문서의 주제, 카테고리 등을 분류하는데 많이 쓰인다.
오히려 단순한 문서 분류 작업에서 2, 3번째 같은 모델을 적용하면 더 시간이 많이 들 것이다.