안녕하세요. 휴먼스케이프 june입니다.
이전 시간에 이어서 이번엔 TF-IDF 를 이용해 벡터값을 추출해보겠습니다.
TF-IDF
TF-IDF는 단어의 중요도를 벡터값으로 활용합니다. 중요한 단어일수록 벡터값이 커지게됩니다.
TF-IDF는 다음 2가지의 전제를 가지고 있습니다.
- 특정 문서에서 중요 키워드는 자주 등장할 것이다.
- 여러 문서에 걸쳐서 등장하는 키워드는 중요 키워드가 아닌 일반적으로 사용하는 단어(
the
,a
,is
등)일 것이다.
word cloud를 상상해보면 이해가 쉽습니다.
- TF-IDF는 어떤 단어 w(word)가 문서 d(document) 내에서 얼마나 중요한지 나타내는 수치입니다.
- TF-IDF에서 TF는 Term frequency의 약어로, 단어의 문서 내에 출현한 횟수를 의미합니다.
- TF-IDF에서 IDF는 Inverse document frequency의 약어로, 그 단어가 출현한 문서의 숫자의 역수를 의미합니다.
TF는 단어가 문서에 출현한 횟수입니다. 따라서 그 숫자가 클수록 문서에서 중요한 단어일 확률이 높습니다.
하지만 the
, a
와 같은 단어도 TF값이 매우 크게 나타날 가능성이 높습니다.
이와같은 중요하지 않은 값을 배제하기 위해 IDF를 사용합니다.
IDF는 그 단어가 출현한 문서의 숫자를 의미하므로, 그 값이 클수록 여러 문장에서 일반적으로 쓰이는 단어일 확률이 큽니다.
따라서 IDF를 구해 TF에 곱함으로써 흔한 단어의 점수를 낮출 수 있습니다.
따라서 TF-IDF는 특정 문서에서 많이 나타난 단어를 구하는 알고리즘입니다.
이제 직접 데이터가 변하는걸 보며 tf-idf에 대해 이해해보도록 하겠습니다.
아래 5개의 document가 주어집니다.
- a b c d e
- a a b b c c
- a a a a a a
- c
- c d e
먼저 tf를 구해봅시다. tf는 단순히 해당 단어의 출현 횟수를 세면 됩니다.(bow와 동일합니다.)
+---+---+---+---+---+
| a | b | c | d | e |
+---+---+---+---+---+
| 1 | 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 0 | 0 |
| 5 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 | 0 |
| 0 | 0 | 1 | 1 | 1 |
+---+---+---+---+---+
다음은 idf를 구해봅시다. i는 역수를 뜻하니 df를 구하면 됩니다.
df는 각 단어가 몇개의 문서(document)에 츨현했는지를 세 구합니다.
(하나의 문서에서 여러번 출현했더라도 1로 카운팅하는게 핵심입니다.)
+---+---+---+---+---+
| a | b | c | d | e |
+---+---+---+---+---+
| 3 | 2 | 4 | 2 | 2 |
+---+---+---+---+---+
이제 위에서 구한 tf에 방금 구한 df를 나눠주면 tf-idf 벡터가 추출됩니다.
+-----+-----+------+-----+-----+
| a | b | c | d | e |
+-----+-----+------+-----+-----+
| 0.3 | 0.5 | 0.25 | 0.5 | 0.5 |
| 0.6 | 1 | 0.5 | 0 | 0 |
| 1.6 | 0 | 0 | 0 | 0 |
| 0 | 0 | 0.25 | 0 | 0 |
| 0 | 0 | 0.25 | 0.5 | 0.5 |
+-----+-----+------+-----+-----+
지금까지의 과정을 코드로 옮기면 다음과 같습니다.
def get_term_frequency(document, word_dict=None):
"""
하나의 문서에서 어떤 단어가 얼마나 나타났는지를 구함
예:
input: "this is a test sentence test"
output: ['this': 1, 'is': 1, 'a': 1, 'test': 2, 'sentence': 1, 'real': 1]
"""
if word_dict is None:
word_dict = {}
words = document.split()
for w in words:
w = w.replace(" ", "")
word_dict[w] = 1+(0 if word_dict.get(w) is None else word_dict[w])
return pd.Series(word_dict, dtype = 'object').sort_values(ascending=False)
def get_document_frequency(documents):
"""
각 단어가 몇개의 문서에 나타났는지를 구함
예:
input: ["this is a test sentence test", "real test", "this test is real"]
output: ['this': 2, 'is': 2, 'a': 1, 'test': 3, 'sentence': 1, 'real': 2]
"""
dicts = []
vocab = set([])
df = {}
for d in tqdm(documents):
tf = get_term_frequency(d)
dicts += [tf]
vocab |= set(tf.keys())
for v in tqdm(list(vocab)):
df[v] = 0
for d in dicts:
if d.get(v) is not None:
df[v] += 1
return pd.Series(df).sort_values(ascending=False)
def get_tfidf(docs):
"""
각 문서의 tf-idf 계산
"""
vocacb = {}
tfs = []
for d in docs:
vocab = get_term_frequency(d, vocacb)
tfs += [get_term_frequency(d)]
df = get_document_frequency(docs)
import numpy as np
stats = []
columns = ['word', 'frequency']+['doc{}'.format(i+1) for i in range(len(docs))]+['max']
for word, freq in vocab.items():
tfidfs = []
for idx in range(len(docs)):
if tfs[idx].get(word) is not None:
tfidfs += [tfs[idx][word] * np.log(len(docs) / df[word])]
else:
tfidfs += [0]
stats.append((word, freq, *tfidfs, max(tfidfs)))
return pd.DataFrame(stats,columns=columns).sort_values(by='max',ascending=False)
특이한 점은 tf-idf를 구하는 코드가 아래와 같이 나와있는데, 처음에 설명하지 않았던 np.log연산을 한다는 점입니다.
tfidfs += [tfs[idx][word] * np.log(len(docs) / df[word])]
간단히 df의 값을 줄이기 위해 나누기 연산을 수행한다고 이해하면 편합니다.
이와 관련해서 찾아보니 idf의 수치가 기하급수적으로 높아지는 것을 방지하기 위해 log를 붙여 tf값을 강조한다 라는 사실을 알 수 있었습니다.
영문 위키에서는 해당 식을 inverse document frequency smooth
라는 문장으로 표현합니다.
처음 설명에서 말한것처럼 tf-idf는 중요키워드에 더 큰 값을 할당하는 알고리즘입니다.
따라서 산출한 백터값을 정렬하면 각 문서에서의 중요 키워드를 산출할 수 있습니다.
여기까지 tf-idf에 대한 설명이었습니다.
사실 tf-idf는 사이킷-런 이라는 라이브러리에 구현되어 있어 아주 간단하게 구할 수 있습니다.
TfidfVectorizer라는 클래스에 fit_transform 메서드를 이용하면 위 구현한 코드와 같이 각 단어를 tf-idf 백터화 시켜 리턴합니다.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(docs)