안녕하세요! 은공지능 공작소 운영자 파이찬입니다.
오늘은 TF-IDF 벡터화에 대한 내용을 다루겠습니다.
자연어처리를 하다 보면 많이 등장하는 Feature extraction 기법입니다.
1. TF-IDF의 개념
TF-IDF의 풀네임은 Term Frequency - Inverse Document Frequency입니다.
TF, IDF의 의미를 각각 이해하시면 정확한 의미 파악이 가능합니다.
자세한 내용은 위의 그림을 참조해 주세요.
그럼 이 TF-IDF가 어떻게 사용되고,
어떤 맥락에서 등장하게 되었는지 이해해보도록 하겠습니다.
TF-IDF는 일종의 특징 추출(Feature extraction) 기법입니다.
특징 추출이란, 말 그대로 raw data에서 특징을 뽑아내는 것을 의미합니다.
좀 추상적으로 들릴 수도 있는데요, 한마디로 정의하자면 '수치화'입니다.
텍스트 데이터를 컴퓨터가 그대로 인식할 수는 없습니다.
컴퓨터가 인식하려면 이를 숫자의 형태로 바꾸어 주어야 합니다.
이렇게 숫자의 형태로 바꾸어 주는 과정에서,
어떻게 하면 사람이 인지하듯이, 데이터의 특징을 잘 담을 수 있을지 고민하게 되고
여러 가지 특징 추출 기법이 등장하는 것입니다.
여러가지 특징 추출 기법 중 가장 기본적이 count 기반 특징 추출기법입니다.
python scikit에서 CountVectorizer가 바로 이것입니다.
하지만 위의 그림에서 나오듯이, 이 CountVectorizer는 한계점이 존재합니다.
단어의 빈도수를 기반으로 하기 때문에,
조사, 관사 등의 의미 없는 단어에 높은 수치를 부여할 수 있다는 것입니다.
이러한 단점을 해결할 수 있는 것이 TF-IDF입니다.
일반적으로 TF-IDF가 CountVectorizer 보다 좋은 결과를 내놓는 것으로 알려져 있습니다.
이제 다시 한번 정확하게, TF, DF, IDF의 의미를 짚고 넘어가겠습니다.
위의 그림을 통해 의미를 이해하시면 됩니다.
+
유튜브 동영상에는 설명이 빠진 것이 있어 보충합니다.
TF-IDF에서 한 글자의 단어(Term)는 단어 사전에서 제외됩니다.
위의 문서들에 등장하는 단어 중 'I' 나 'a' 같은 단어에 이에 해당됩니다.
DF의 개념은 혼동될 수 있으니 조심하셔야 합니다.
TF가 '단어'의 빈도를 나타내었다면, 'DF'는 문서의 빈도를 나타냅니다.
위의 그림에서 home은 전체 문서에서 총 4번 등장합니다.
그렇다고 해서 DF = 4로 잡으시면 안 된다는 말씀입니다.
비록 단어의 등장 횟수는 4번이지만, 해당 단어를 가진 문서는 총 3개이기 때문에
여기서 home의 DF는 3으로 잡는 것이 정확합니다.
이러한 점을 숙지하시고 넘어가시면 되겠습니다.
마지막으로 IDF의 개념을 잡고 넘어가겠습니다.
IDF는 DF에 Inverse를 붙여준 것입니다.
수학에서 Inverse는 '역수'라는 의미를 가지고 있습니다.
역수는 분모와 분자를 뒤집는 것을 의미합니다.
예를 들어 3을 3/1이라고 표현하고, 분모와 분자를 뒤집는다면,
1/3이라고 할 수 있습니다. 이것이 역수의 개념입니다.
역수를 취해주면, 원래는 큰 값이 작은 값으로 확 줄어드는 효과를 낼 수 있습니다.
이러한 방식으로 많이 등장하는 단어에 패널티를 주기 때문에
DF에 Inverse를 붙여서 IDF라고 이름이 붙여졌습니다.
IDF를 구하는 수식은 위의 그림을 참조해주시면 됩니다.
from sklearn.feature_extraction.text import TfidfVectorizer
text = ['I go to my home my home is very large', # Doc[0]
'I went out my home I go to the market', # Doc[1]
'I bought a yellow lemon I go back to home'] # Doc[2]
tfidf_vectorizer = TfidfVectorizer() # TF-IDF 객체선언
이제 본격적으로 코드 실습을 시작하겠습니다.
설명자료에서 쓰인 문장들을 그대로 가져왔습니다. 총 3개의 문서가 담긴 데이터입니다.
모듈은 사이킷런의 Feature extraction을 사용합니다.
그중에서도 TfidfVectorizer라는 모듈을 사용하여 객체를 선언해주면 됩니다.
tfidf_vectorizer.fit(text) # 단어를 학습시킴
tfidf_vectorizer.vocabulary_ # 단어사전을 출력
sorted(tfidf_vectorizer.vocabulary_.items()) # 단어사전 정렬
output:
[('back', 0),
('bought', 1),
('go', 2),
('home', 3),
('is', 4),
('large', 5),
('lemon', 6),
('market', 7),
('my', 8),
('out', 9),
('the', 10),
('to', 11),
('very', 12),
('went', 13),
('yellow', 14)]
단어 사전을 정렬해서 출력한 모습입니다.
한 글자 짜리 단어들은 제외하고 단어 사전이 만들어졌습니다.
a, b, c, d 오름차순으로 순차적으로 인덱스가 할당된 모습입니다.
이제 Vectorizer는 이 사전을 참고하여, TF/DF/IDF 벡터화를 수행하게 됩니다.
2. TF, DF, IDF 벡터화 과정의 이해
tfidf_vectorizer.idf_
output:
array([1.69314718, 1.69314718, 1. , 1. , 1.69314718,
1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.69314718,
1.69314718, 1. , 1.69314718, 1.69314718, 1.69314718])
이제 TF, DF, IDF 벡터화 과정에 대해서 설명드리겠습니다.
그전에 IDF와 TF-IDF과정을 거친 결과가 어떻게 되는지 확인하고 넘어가겠습니다.
위의 결과는 IDF 벡터화의 결과물입니다.
tfidf_vectorizer.idf_.shape
output:
(15,)
코드의 뒤에 .shape를 붙여서 결과를 확인해 보십시오.
그러면 (15, ) 라는 결과가 출력이 됩니다.
이는 .vocabulary_ 명령어를 통해 확인한 단어 사전의 단어수와 일치합니다.
tfidf_vectorizer.transform(text).toarray()
output:
array([[0. , 0. , 0.2170186 , 0.4340372 , 0.36744443,
0.36744443, 0. , 0. , 0.55890191, 0. ,
0. , 0.2170186 , 0.36744443, 0. , 0. ],
[0. , 0. , 0.24902824, 0.24902824, 0. ,
0. , 0. , 0.42164146, 0.3206692 , 0.42164146,
0.42164146, 0.24902824, 0. , 0.42164146, 0. ],
[0.44514923, 0.44514923, 0.26291231, 0.26291231, 0. ,
0. , 0.44514923, 0. , 0. , 0. ,
0. , 0.26291231, 0. , 0. , 0.44514923]])
위의 결과는 TF-IDF 벡터화의 최종 결과물입니다.
각각의 Document 마다 array가 하나씩 할당되어, 총 3개의 결과물이 나왔습니다.
대괄호로 묶인 덩어리를 세어보면 3개가 있는 것을 확인할 수 있습니다.
이렇게 하여 IDF의 모습, Tf-IDF의 최종 결과물까지 확인해보았습니다.
저희의 목표는 어떻게 이런 결과가 나오게 되었는지를 자세히 살펴보는 것입니다.
즉 과정을 해부하는 것이지요!
지금부터 다시 PPT 자료를 통해,
TF 벡터화, DF 벡터화, IDF 벡터화에 대해 하나하나씩 자세히 파헤쳐보겠습니다.
왼쪽의 단어 사전은 fit 함수를 통해 만들어준 것입니다.
이를 쉽게 그림으로 표현해보면 오른쪽과 같이 됩니다.
이렇게 만들어준 단어 사전을 통해, TF가 벡터화되는 것입니다.
각 문서(Document)마다 하나씩 TF가 만들어집니다.
그러니 총 3개의 TF가 생성되는 것이죠.
각각의 TF 벡터에는 해당 단어들의 빈도가 숫자로 들어가게 됩니다.
예를 들어, Doc[0]의 home이라는 항을 살펴보겠습니다.
home이라는 단어는 Doc[0]에 총 2번 등장했기 때문에 2가 할당이 되는 방식입니다.
이런 방식으로 전체 TF 벡터가 만들어집니다.
이번에는 DF(Document Frequency)의 벡터화 과정에 대해 알아보겠습니다.
DF의 벡터화도 TF 벡터화와 비슷합니다.
다만, DF는 단어의 수가 아닌 '해당 단어가 포함된 문서의 수'라는 점을 기억해주시면 됩니다.
마지막으로 IDF(Inverse-Document Frequency) 벡터화 과정입니다.
IDF 벡터화는 위의 그림처럼 일종의 자연로그 변환을 거칩니다.
이런 방식으로 값이 높은 것들은 낮추고, 값이 낮은 것들은 높일 수 있습니다.
3. IDF 벡터화 해부
tfidf_vectorizer.idf_
output:
array([1.69314718, 1.69314718, 1. , 1. , 1.69314718,
1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.69314718,
1.69314718, 1. , 1.69314718, 1.69314718, 1.69314718])
이제 위와 같은 수치들을 직접 구해볼 것입니다.
코딩해서 계산한 과정이 맞다면 바로 위에 있는 결과와 같은 결과가 나오겠고,
제대로 IDF를 이해한 것이겠죠? ^^
IDF 벡터를 만들기 위해선 DF부터 만들어야 합니다.
DF 벡터는 직접 일일이 손코딩을 해줘야 합니다.
하지만 여러분은 그냥 아래 코드를 복붙해주세요~ 수고는 제가 합니다
import numpy as np
DF_vec = np.array([1, 1, 3, 3, 1,
1, 1, 1, 2, 1,
1, 3, 1, 1, 1])
DF 벡터를 위와 같이 만들어 주면 됩니다.
def idf_func(n, df):
import numpy as np
rst = np.log((1+n)/(1+df)) + 1
return rst
다음으로 DF를 IDF로 로그 변환해주는 함수를 만들어 줍니다.
함수의 인자는 n(총 문서의 개수)과 df 벡터를 넣어주었습니다.
idf_func(3, DF_vec)
output:
array([1.69314718, 1.69314718, 1. , 1. , 1.69314718,
1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.69314718,
1.69314718, 1. , 1.69314718, 1.69314718, 1.69314718])
tfidf_vectorizer.idf_
output:
array([1.69314718, 1.69314718, 1. , 1. , 1.69314718,
1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.69314718,
1.69314718, 1. , 1.69314718, 1.69314718, 1.69314718])
함수를 통해 구한 것과 idfVectorizer로 구한 값이 정확히 같게 나왔습니다.
포인트는 로그 변환 방식을 이해하는 것입니다.
이렇게 하여 IDF 벡터화 과정 해부가 끝났습니다.
+
해당 로그 변환은 IDF_smoothing을 거친 것입니다. (Default option)
이 IDF_smoothing에 대해서는 다음 포스팅에서 다루어 보도록 하겠습니다.
4. TF-IDF 벡터화 해부
tfidf_vectorizer.transform(text).toarray()
output:
array([[0. , 0. , 0.2170186 , 0.4340372 , 0.36744443,
0.36744443, 0. , 0. , 0.55890191, 0. ,
0. , 0.2170186 , 0.36744443, 0. , 0. ],
[0. , 0. , 0.24902824, 0.24902824, 0. ,
0. , 0. , 0.42164146, 0.3206692 , 0.42164146,
0.42164146, 0.24902824, 0. , 0.42164146, 0. ],
[0.44514923, 0.44514923, 0.26291231, 0.26291231, 0. ,
0. , 0.44514923, 0. , 0. , 0. ,
0. , 0.26291231, 0. , 0. , 0.44514923]])
이제 TF-IDF 벡터화를 해부해보겠습니다.
위의 벡터 수치들을 직접 만들어 보는 것이 저희의 목표입니다.
TF-IDF 벡터화는 쉽게 말해서 TF x IDF로 이루어져 있다고 설명을 드렸습니다.
그런데 여기까지 해도 결과는 실제 코드에서 나온 결과와 같지 않을 것입니다.
왜냐면 이 곱한 결과에 L2정규화까지 해야
위에 보이는 결과가 나오기 때문이지요. 그림으로 설명드리면 다음과 같습니다.
최종 TF-IDF는 위와 같이 크게 2단계를 거쳐서 산출됩니다.
1. TF벡터와 IDF벡터를 원소곱 해줍니다.
2. 위의 결과에 L2 정규화를 해줍니다.
L2 정규화에 대해서 간단히 설명드리겠습니다.
위의 하얀색 박스로 되어 있는 수식을 참고하시면 됩니다.
각각의 원소들을 제곱합에 루트를 씌워준 값으로 나눠준 것입니다.
이러한 방식으로 벡터화의 결과를 좀 완만하게 만들 수 있습니다.
이렇게 2단계를 거쳐서 최종 TF-IDF 벡터가 나오게 됩니다.
이제 이를 코딩 실습을 통해 이해해 보겠습니다.
tfidf_vectorizer.transform(text).toarray()[0]
array([0. , 0. , 0.2170186 , 0.4340372 , 0.36744443,
0.36744443, 0. , 0. , 0.55890191, 0. ,
0. , 0.2170186 , 0.36744443, 0. , 0. ])
저희의 목표는 위의 결과를 만드는 것입니다.
이는 첫 번째 Document에 해당하는 TF-IDF 벡터입니다.
count_vec = np.array([0, 0, 1, 2, 1,
1, 0, 0, 2, 0,
0, 1, 1, 0, 0])
먼저 TF 벡터부터 만들어 주겠습니다.
위의 Doc[0]에 해당하는 TF 벡터를 그대로 복붙 해가시길 바랍니다.
이는 첫 번째 문서에 나오는 단어 빈도수를 나타내는 벡터입니다.
tfidf_vectorizer.idf_
output:
array([1.69314718, 1.69314718, 1. , 1. , 1.69314718,
1.69314718, 1.69314718, 1.69314718, 1.28768207, 1.69314718,
1.69314718, 1. , 1.69314718, 1.69314718, 1.69314718])
그다음으로 해야 할 것은 IDF 벡터를 만들어주는 것입니다.
이것은 이미 이전에 저희가 다 해본 것이기 때문에,
tfidf_vectorizer가 만들어주는 결과물을 그냥 쓰도록 하겠습니다.
이제 위에서 만들어준 TF 벡터와 여기 있는 IDF 벡터를 곱해줘야 합니다.
np.multiply(count_vec, tfidf_vectorizer.idf_)
output:
array([0. , 0. , 1. , 2. , 1.69314718,
1.69314718, 0. , 0. , 2.57536414, 0. ,
0. , 1. , 1.69314718, 0. , 0. ])
위와 같은 방식으로 TF벡터와 IDF벡터를 곱해줍니다.
여기서 중요한 것은 np.multiply 함수를 쓰는 것입니다.
이는 벡터 각각의 원소들의 곱셈을 해주는 함수입니다.
(np.matmul과는 반드시 구분해서 써야 합니다)
이제 위에 나온 결과에 L2 정규화를 해주면
최종 TF-IDF 벡터를 구하게 됩니다. 거의 다 왔습니다!
from sklearn import preprocessing
tf_idf_before_l2 = np.multiply(count_vec, tfidf_vectorizer.idf_)
tf_idf_before_l2 = tf_idf_before_l2.reshape(1, -1)
tf_idf_after_l2 = preprocessing.normalize(tf_idf_before_l2, norm='l2')
위와 같이 사이킷런의 preprocessing 모듈을 이용하면
쉽게 L2 정규화를 할 수 있습니다.
먼저 np.multiply한 결과를 tf_idf_before_l2에 담아줍니다.
다음으로는 한 번 reshape를 해주는데, 이러한 과정을 거치는 이유는
L2 정규화의 범위를 전체로 잡아주기 위한 것입니다.
(그렇지 않으면 각 행 or 열을 기준으로 잡아버립니다)
마지막으로 preprocessing.normalize 함수를 통해 L2 정규화를 진행합니다.
제일 마지막에 norm = 'l2'라고 된 코드로 L2 정규화를 명시해주면 됩니다.
tf_idf_after_l2
output:
array([[0. , 0. , 0.2170186 , 0.4340372 , 0.36744443,
0.36744443, 0. , 0. , 0.55890191, 0. ,
0. , 0.2170186 , 0.36744443, 0. , 0. ]])
tfidf_vectorizer.transform(text).toarray()[0]
array([0. , 0. , 0.2170186 , 0.4340372 , 0.36744443,
0.36744443, 0. , 0. , 0.55890191, 0. ,
0. , 0.2170186 , 0.36744443, 0. , 0. ])
이제 tf_idf_after_l2의 수치가 제대로 나왔는지 비교를 해봅시다.
모든 값이 정확히 벡터라이저의 값과 일치하는 것을 확인하실 수 있습니다.
이런 식으로 TF벡터와 IDF벡터를 곱한 것에
L2정규화까지 거치는 것이 바로 TF-IDF 벡터를 산출하는 과정인 것입니다.
(사진: 은공지능 공작소 작업실)
수고하셨습니다. 오늘은 TF-IDF의 개념을 이해하고
각각의 과정들을 해부해보는 시간을 가졌습니다.
도움이 되셨다면 하단의 ♥ 버튼 클릭을 해주시길 바랍니다.
긴 포스팅 읽어주셔서 감사합니다.
반응형