1. 단어 표현 방법 대분류 - 국소 표현, 분산 표현
■ 단어 표현 방법은 크게 국소 표현(local representation)과 분산 표현(distributed representation)으로 나뉜다.
■ 분산 표현 방법은 어떤 단어를 표현할 때, 그 단어의 주변 단어를 고려하는 방법이고, 국소 표현 방법은 해당 단어 그 자체만 보고, 해당 단어와 특정값을 매핑하여 단어를 표현하는 방법이다.
■ 예를 들어 puppy(강아지), cute(귀여운), lovely(사랑스러운)라는 단어가 있을 때,
- 분산 표현은 puppy라는 단어가 cute, lovely라는 단어와 함께 자주 등장하면, puppy라는 단어는 cute, lovely한 느낌의 단어로 정의한다. 즉, 분산 표현은 '맥락'을 고려해서 단어의 뉘앙스를 표현할 수 있다.
- 국소 표현은 puppy, cute, lovely라는 단어에 숫자 1, 2, 3을 각각 매핑하여 의미를 부여하기 때문에 분산 표현과 달리, '맥락'을 고려하지 못하므로 단어의 뉘앙스를 표현할 수 없다.
■ 다음 그림은 단어 표현 방법을 분류한 그림이다.

- 국소 표현을 이산 표현(discrete representation), 분산 표현을 연속 표현(continuous represnetation)이라고도 부른다.
■ 위의 그림을 보면 단어의 표현 방법은 크게 국소 표현 방법과 분산 표현 방법으로 나뉘며,
- 국소 표현에는 원-핫 벡터로 단어를 표현하는 방법과 n-gram으로 단어를 표현하는 방법. 그리고 단어의 빈도수를 카운트하여 단어를 수치화하는 단어 표현 방법 Count Based가 있다.
- 분산 표현에는 예측 기반으로 단어의 뉘앙스를 표현하는 Word2Vec과 Word2Vec의 확장으로 예측과 카운트 방법을 모두 사용하는 글로브(Glove)가 있다.
2. 백 오브 워즈(Bag of Words)
■ BoW(Bag of Word)는 (입력) 단어들의 순서를 무시하고 집합으로 다루며, 단어들의 출현 빈도(frequency)에 기반해서 텍스트 데이터를 수치화하는 단어 표현 방법이다.
■ 그러므로, BoW를 만드는 과정은 두 가지 과정으로 생각할 수 있다.
- 마지막에 출현 빈도를 고려하기 위해, 각 단어에 고유한 정수 인덱스를 부여한다.
- 각 단어 토큰의 등장 횟수를 카운트한다.
from nltk.tokenize import word_tokenize
text = "You say goodbye and I say hello."
text = text.lower() # 소문자로 변환
text = text.replace('.', '') # 온점 제거
text
```#결과#```
'you say goodbye and i say hello'
````````````
word_tokens = word_tokenize(text)
print('토큰화 결과:', word_tokens)
```#결과#```
토큰화 결과: ['you', 'say', 'goodbye', 'and', 'i', 'say', 'hello']
````````````
word_to_index = {}
bow = []
for token in word_tokens:
if token not in word_to_index.keys(): # 한 번도 등장하지 않은 단어 토큰이면
word_to_index[token] = len(word_to_index) # 인덱스 부여
bow.insert(len(word_to_index)-1, 1)
else: # 재등장 하는 단어라면
index = word_to_index.get(token) # 해당 토큰(키)의 인덱스(값)을 가져와서
bow[index] += 1 # 해당 단어의 인덱스 위치에 +1
word_to_index
```#결과#```
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5}
````````````
bow
```#결과#```
[1, 2, 1, 1, 1, 1]
````````````
- BoW를 보면, 인덱스 1에 해당하는 'say'가 두 번 등장해서 BoW의 인덱스 1이 'say'의 빈도수를 나타내는 것을 볼 수 있다.
■ BoW는 이렇게 단어 표현을 각 단어의 빈도수를 수치화하여 나타내기 때문에, 주로 문서가 어떤 성격의 문서인지를 판단/분류하는 작업에 사용된다.
- 예를 들어 미분, 함수, 방정식같은 단어가 문서에서 자주 등장한다면, 수학 관련 문서로 분류할 수 있다.
■ BoW에 기반한 단어 표현 방법으로 DTM과 TF-IDF가 있다.
2.1 불용어 제거와 BoW
■ BoW를 사용해서 어떤 문서에 어떤 단어들이 중요한지 확인하고자 할 때, 별다른 의미를 갖지 않는 단어들인 불용어를 제거하는 것이 도움이 된다.
■ 사이킷런(sklearn)에서는 단어의 빈도수를 벡터로 만드는 CounterVectorize 클래스를 지원한다. 이 클래스를 사용하면 영어에 대해서는 쉽게 BoW를 만들 수 있으며, stop_words라는 인자에 불용어를 지정하면, 지정한 불용어는 제외하고 BoW를 만들 수 있다.
- CounterVectorizer 클래스는 띄어쓰기를 기준으로 단어를 자르는 낮은 수준의 토큰화를 진행하기 때문에 한국어 단어 토큰화에는 적합하지 않으나 영어에 대해서는 쉽게 토큰화를 진행할 수 있다.
- 또한, 길이가 2 이상인 문자에 대해서만 토큰으로 인식한다. 즉, 길이가 1인 문자는 토큰으로 고려하지 않는다.
■ 불용어 지정은 사용자가 정의한 불용어로 지정하거나 CounterVectorizer에서 제공하는 불용어, NLTK에서 제공하는 불용어 등을 지정할 수 있다.
from sklearn.feature_extraction.text import CountVectorizer
text = ["You say goodbye and I say hello."]
## 사용자 정의
stop_words = ['I', 'and', 'is']
vect = CountVectorizer(stop_words = stop_words)
print('BoW vector: ', vect.fit_transform(text).toarray())
print('vocabulary: ', vect.vocabulary_)
```#결과#```
BoW vector: [[1 1 2 1]]
vocabulary: {'you': 3, 'say': 2, 'goodbye': 0, 'hello': 1}
````````````
- index 0인 goodbye는 1번, index 1인 hello는 1번, index 2인 say는 2번, index 3인 you는 1번 등장했기 때문에 BoW = [[1 1 2 1]]이 되는 것을 확인할 수 있다.
## CountVectorizer에서 제공하는 불용어로 지정
vect = CountVectorizer(stop_words = 'english')
print('BoW vector: ', vect.fit_transform(text).toarray())
print('vocabulary: ', vect.vocabulary_)
```#결과#```
BoW vector: [[1 1 2]]
vocabulary: {'say': 2, 'goodbye': 0, 'hello': 1}
````````````
## NLTK에서 제공하는 불용어로 지정
from nltk.corpus import stopwords
nltk_stop_words = stopwords.words('english')
vect = CountVectorizer(stop_words = nltk_stop_words)
print('BoW vector: ', vect.fit_transform(text).toarray())
print('vocabulary: ', vect.vocabulary_)
```#결과#```
BoW vector: [[1 1 2]]
vocabulary: {'say': 2, 'goodbye': 0, 'hello': 1}
````````````
3. 문서 단어 행렬(Document-Term Matrix, DTM)
■ 문서 단어 행렬(DTM)은 다음과 같이 다수의 문서에서 등장하는 각 단어들의 빈도를 행렬로 표현한 것으로, 행렬의 행은 각 문서들을 나타내고 열은 각 단어들을 나타낸다.

■ 행과 열을 반대로. 즉, 행에는 각 문서에 등장하는 단어들을 나타내고 열에는 각 문서들을 나타내면 TDM이라고 부른다.
■ DTM 또는 TDM은 이렇게 행렬을 이용하여 서로 다른 문서들을 비교할 수 있다.
■ DTM의 원소들은 각 문서에 등장하는 단어들의 빈도수이므로, DTM은 각 문서에 대한 BoW를 하나의 행렬로 만든 것으로 볼 수 있다.
3.1 DTM의 한계
■ DTM은 위의 그림처럼 행렬이므로 간단하고 구현하기도 쉽다. 하지만, 문서의 개수가 많아지고 그에 따라 각 문서에 등장하는 단어들의 개수가 많아졌을 때, 몇 가지 단점이 있다.
■ DTM의 행은 각 문서, 열은 문서에 등장하는 각 단어들이므로 문서들이 개수가 많아졌을 때, 각 문서들이 고유한 단어들을 갖는다면,
■ 예를 들어 D1과 D2가 있고 D1에 D2에는 등장하지 않는 고유한 단어 50개, D2에는 D1에 등장하지 않는 고유한 단어 50개가 있다면
■ DTM이라는 행렬의 많은 원소들이 0의 값을 가질 것이다. 이는 문서의 개수가 많아지고 각 문서에서 고유한 단어의 등장 횟수가 증가할수록 DTM의 많은 원소들이 0의 값을 가질 것이다.
■ 이렇게 대부분의 원소(또는 값)가 0인 벡터 또는 행렬을 희소 벡터(sparse vector) 또는 희소 행렬(sparse matrix)라고 부른다. 이러한 희소 벡터와 희소 행렬은 희소하지 않은 벡터와 행렬에 비해 저장 공간을 낭비하고 있는 상태이며, 크기가 커질수록 계산 리소스를 증가시킬 수 있다.
■ 또한, DTM도 단순 빈도수에 기반하기 때문에 의미없는 불용어(the, is, a, an 등)가 의미있는 단어들보다 더 많이 등장하게 된다면 혹은 불필요한 단어들이 더 자주 등장한다면, 각 문서에 등장하는 단어들 중 어떤 단어가 중요한지 파악하기 어렵고, 유사한 문서들끼리 묶는 작업에서도 불용어나 불필요한 단어들을 기준으로 문서들이 매칭될 수 있다.
■ DTM의 불용어 또는 불필요한 단어로 인해 발생하는 문제점을 해결하기 위한 방법으로 TF-IDF가 있다.
4. TF-IDF(단어 빈도- 역문서 빈도, Term Frequency-Inverse Document Frequency) 표현
■ TF-IDF는 단어의 빈도(TF)와 역문서 빈도(IDF)를 사용하여 DTM(또는 문서) 내 등장하는 단어들의 중요도를 계산하고 가중치를 부여하는 방법이다. 이를 통해 어떤 단어가 DTM(또는 문서) 내에서 얼마나 중요한지 판단할 수 있다.
■ TF(단어 빈도, Term Frequency)는 특정 단어가 문서 내에 등장하는 빈도로, 이 갚이 높을수록 문서에서 중요하다고 간주할 수 있다. 그래서 TF는 등장 횟수에 비례하여 단어에 가중치를 부여한다.
■ IDF(역문서 빈도, Inverse Document-Frequency)는 TF와 반대로 드물게 등장하는 단어에 가중치를 부여하는데,
■ 예를 들어 법률 관련 문서 묶음이 있다고 했을 때,
- 문서 묶음 대부분에는 "계약", "의무", "책임"같은 단어가 자주 등장하고, 해당 단어들은 일반적인 법률 문서의 특징을 나타내므로 , 이런 상황에서는 TF(단어 빈도)가 적합하다.
- 하지만 자주 등장하지 않는 단어들 중에서도 "지적재산권법"같은 법률 문서의 특징을 나타내는 단어가 있을 것이다. 이런 상황에서는 IDF(역문서 빈도)가 적합하다.
■ 하지만, 여전히 남아 있는 단점은 단어의 순서가 무시된다는 점이다. 문장을 구성할 때, 특정 단어 (토큰)이 나올 확률은 이전 시점들의 단어 (토큰)에 영향을 받을 수밖에 없다. 카운트 기반의 표현 방식은 단어 (토큰)의 순서 정보를 전혀 담을 수 없기 때문에 새로운 문장을 생성해야 하는 상황에서는 사용하기 어렵다.
4.1 TF-IDF 계산
■ TF-IDF 값은 TF(단어 빈도)와 IDF(역문서 빈도)를 곱한 값이다. 문서를 d, 단어를 t, 문서의 총 개수를 n이라고 했을 때, TF, DF, IDF는 다음과 같이 정의할 수 있다.
- ① 단어 빈도 tf(d,t): 특정 문서 d에서 특정 단어 t의 등장 횟수
- 단어 빈도인 tf(d,t) 값을 산출하는 가장 간단한 방법은 단순히 문서 d에 나타나는 특정 단어 t의 총 빈도수를 사용하는 것이다.
- ② df(t): 특정 단어 t가 등장한 문서의 수
- df(t)는 특정 단어 t가 등장한 문서의 수이다. 예를 들어 다음과 같은 DTM이 있을 때,

단어 w1은 모든 문서를 통틀어서 8회 등장하며, 총 3개의 문서 d1t1,d2t2,dJt12에서 등장한다. 그러므로 w1의 df(w1)=3이다.
- ③ idf(t): df(t)에 반비례하는 수
- idf(t)=log(n1+df(t))로 계산한다.
- n은 전체 문서의 수, df(t)는 특정 단어 t가 포함된 문서의 수이다.
- 분모에서 df(t)에 +1을 하는 이유는 분모가 0이 되는 것을 방지하기 위해서이다. 만약, 특정 단어 t가 전체 말뭉치에 존재하지 않는다면 df(t)=0이 되어 +1이 없다면 분모는 0이 된다.
- log를 취하지 않고 n1+df(t)식을 IDF 식으로 사용한다면, 전체 문서의 수 n이 커질수록 IDF 값은 기하급수적으로 커지게 된다.
- 특정 단어가 드문 경우 IDF 값이 지나치게 커지는 것을 방지하기 위해 log를 사용해서 n1+df(t) 값의 스케일을 조정한다.
■ TF-IDF 값은 tf와 idf의 곱이므로, 모든 문서에서 자주 등장하는(매우 흔한) 단어는 IDF값이 0또는 0에 가까운 값이 되므로 TF-IDF = TF × IDF를 통해 TF-IDF 값도 0 또는 매우 작은 값을 가지게 되어 해당 단어를 제외할 수 있다.
- 예를 들어 the나 a같은 불용어는 모든 문서에서 자주 등장하는 편이므로 불용어에 대한 TF-IDF 값은 다른 단어에 비해 낮은 값을 갖게 된다.
■ 반면, 특정 문서(또는 한 문서)에서만 자주 등장하는 단어는 IDF 값이 최댓값이나 최댓값과 거의 가까운 값을 가지므로 TF-IDF = TF × IDF를 통해 TF-IDF 값이 커지게 된다.
■ TF-IDF 값이 낮으면 중요도가 낮은 단어이며, TF-IDF 값이 크면 중요도가 높은 단어이다.
import pandas as pd
from math import log
docs = ["Dog bites man",
"Man bites dog",
"Dog eats meat",
"Man eats food"]
vocab = list(set(w.lower() for doc in docs for w in doc.split()))
vocab
```#결과#```
['dog', 'food', 'eats', 'meat', 'man', 'bites']
````````````
n = len(docs) # 전체 문서의 개수
## TF 값을 계산하는 함수
def tf(t, d):
return d.count(t)
## IDF 값을 계산하는 함수
def idf(t):
df = 0 # 특정 단어 t가 등장한 문서의 개수 초기화
for doc in docs:
df += t in doc # df 계산
return log(n/(1+df)) # idf 계산
## TF-IDF 값을 계산하는 함수
def tfidf(t, d):
return tf(t, d) * idf(t)
- def idf에서 df += t in doc의 과정은
- 각 문서(또는 문장)에 특정 단어 t가 있으면 't in doc'는 True를 반환한다.
- 불리언 값인 True/False는 덧셈에서 1/0이므로 각 문서(또는 문장)에 특정 단어 t가 있으면 df에 +1, 없으면 +0
result = []
for i in range(n): # 전체 문서에 대해
result.append([]) # TF-IDF 결과를 행렬로 나타내기 위해 차원 추가
d = docs[i]
for j in range(len(vocab)): # 전체 문서에서 등장하는 모든 단어에 대해
t = vocab[j]
result[-1].append(tf(t, d)) # 각 단어의 등장 횟수 계산
TF = pd.DataFrame(result, columns = vocab) # 각 단어의 등장 횟수에 대한 행렬을 데이터프레임으로, 이때 데이터프레임의 컬럼(열)은 각 단어
TF
- TF를 DTM 형태로 나타내면 다음과 같다.

- 'bites'에 대한 IDF를 계산해보면, 전체 문서의 개수는 4이므로 n=4. 그리고 'bites'라는 단어는 2개의 문서에서 등장하므로 DF는 2이다.
- IDF를 계산하기 위해 사용하는 loge는 자연로그 ln이므로 'bites'에 대한 IDF 값은 ln(4 / (1 + 2)) = 0.287682072...
result = []
for i in range(len(vocab)): # 전체 문서에서 등장하는 모든 단어에 대해
t = vocab[i]
result.append(idf(t)) # IDF 계산
IDF = pd.DataFrame(result, index = vocab, columns = ['IDF'])
IDF.sort_values(by = 'IDF', ascending=False)

- 각 단어에 대한 IDF 값을 보면, 다른 단어에 비해 비교적 여러 문서에서 등장한 'eats'와 'bites'의 IDF 값이 작은 것을 볼 수 있다. 이렇게 IDF는 여러 문서에서 등장한 단어의 가중치를 낮추는 역할을 한다.
result = []
for i in range(n): # 전체 문서에 대해
result.append([])
d = docs[i]
for j in range(len(vocab)): # 전체 문서에서 등장하는 모든 단어에 대해
t = vocab[j]
result[-1].append(tfidf(t, d)) # TF-IDF 계산
TF_IDF = pd.DataFrame(result, columns = vocab)
TF_IDF

- 예를 들어 'bites'의 경우, 문서 1과 문서 2에서 TF값이 1이므로 IDF 값과 Tf = 1이 곱해져 TF-IDF 값이 0.287682가 되는 것을 볼 수 있다.
- 만약, 문서 1에서 'bites'의 TF 값이 2였다면 'bites'의 IDF 값과 TF = 2가 곱해져 TF-IDF 값은 0.575364가 될 것이다.
■ 사이킷런의 CountVectorizer를 사용하여 DTM을 만들 수 있다.
from sklearn.feature_extraction.text import CountVectorizer
docs = ["Dog bites man",
"Man bites dog",
"Dog eats meat",
"Man eats food"]
vector = CountVectorizer()
print(vector.fit_transform(docs).toarray()) # 각 단어의 빈도수
print(vector.vocabulary_) # 각 단어와 맵핑된 인덱스
```#결과#```
[[1 1 0 0 1 0]
[1 1 0 0 1 0]
[0 1 1 0 0 1]
[0 0 1 1 1 0]]
{'dog': 1, 'bites': 0, 'man': 4, 'eats': 2, 'meat': 5, 'food': 3}
````````````
- 여기서 DTM의 첫 번째 열은 index 0에 해당하는 'bites'이다. 'bites'는 첫 번째 문서와 두 번째 문서에서 각각 1번 등장했기 때문에 1행 1열과 2행 1열의 값이 1이 되는 것을 볼 수 있다.
- 두 번째 열은 index 1에 해당하는 'dog'이다.
■ 사이킷런의 TfidfVectorizer를 사용하면 TF-IDF를 계산할 수 있다. 단, 여기서의 TF-IDF는 위에서 함수를 정의하여 계산한 TF-IDF 값과 다른 값을 가진다.
- 그 이유는 사이킷런에서는 IDF를 계산할 때, log(1+n1+df)으로 로그항의 분자에도 1을 더해준다.
- 그리고 TF-IDF를 구하기 위해 TF와 IDF를 곱할 때, IDF에 +1을 하여 TF × ( IDF + 1)을 계산한다.
- 또한, 각각의 TF-IDF의 값에 L2 정규화한 값을 나누어 값을 조정한다.
from sklearn.feature_extraction.text import TfidfVectorizer
docs = ["Dog bites man",
"Man bites dog",
"Dog eats meat",
"Man eats food"]
tf_idf = TfidfVectorizer().fit(docs)
print(tf_idf.transform(docs).toarray())
print(tf_idf.vocabulary_)
```#결과#```
[[0.65782931 0.53256952 0. 0. 0.53256952 0. ]
[0.65782931 0.53256952 0. 0. 0.53256952 0. ]
[0. 0.44809973 0.55349232 0. 0. 0.70203482]
[0. 0. 0.55349232 0.70203482 0.44809973 0. ]]
{'dog': 1, 'bites': 0, 'man': 4, 'eats': 2, 'meat': 5, 'food': 3}
````````````
'자연어처리' 카테고리의 다른 글
원-핫 인코딩, 워드 임베딩 (0) | 2025.02.28 |
---|---|
유사도 (0) | 2025.02.28 |
언어 모델(Language Model) (0) | 2025.02.25 |
텍스트 표준화, 토큰화, 어휘 사전(단어 집합) 인덱싱 (0) | 2025.02.24 |
텐서(Tensor) (1) | 2025.01.20 |