본문 바로가기

AI Theory/NLP

[GoingDeeper] 04. 단어 빈도, 텍스트 분포로 벡터화하기

[GoingDeeper] 04. 단어 빈도, 텍스트 분포로 벡터화하기


목차


자연어 처리에서 텍스트를 숫자 벡터로 변환하는 과정을 벡터화(Vectorization) 이라고 한다. 벡터화 방법은 크게 1.통계와 머신러닝을 활용한 방법2. 인공신경망을 활용한 방법 으로 나뉜다. 단어 빈도를 이용한 벡터화와 텍스트 분포를 이용한 벡터화의 경우 통계와 머신러닝을 활용한 벡터화에 속한다.


단어 빈도를 이용한 벡터화

1. Bag of words(BoW)

  • 자연어처리, 정보검색에서 쓰이는 간단한 표현 방법
  • 문서 내 단어들의 분포를 보고 문서의 특성을 파악하는 기법

the bag of word representation
문서내 단어들의 빈도를 파악하는 Bag of words (출처:모두연)

  • BoW 처리 과정
    • 어떤 한 문서에 등장하는 텍스트를 전부 단어 단위로 토큰화한다. 이때 중복 토큰은 제거하지 않는다.
    • 토큰들을 전부 가방에 넣고 무작위로 섞는다.
  • .위의 과정을 통해 BoW는 단어들의 순서는 무시하고 단어들의 빈도 정보는 보존한다.
  • BoW 예시
    • BoW는 단어 순서를 고려하지 않으므로 아래예시에서 BoW1과 BoW2는 동일하다.
    • 결국 doc1과 doc2는 다른 문장이지만 구분하지 못한다.
    • BoW는 어순에 따라 달라지는 의미를 반영하지 못한다는 한계를 갖는다.
doc1 = 'the snake ate the rabbit.'
doc2 = 'the rabbit ate the snake.'

# doc1 BoW: {"단어":등장횟수}
BoW1 = {"the":2, "snake":1, "ate":1, "rabbit":1}

#doc2 Bow
BoW2 = {"the":2, "rabbit":1, "ate":1, "snake":1}
  • keras Tokenizer를 활용한 BoW 구현
from tensorflow.keras.preprocessing.text import Tokenizer

sentence = ["John likes to watch movies. Mary likes movies too! Mary also likes to watch football games."]

tokenizer = Tokenizer()
tokenizer.fit_on_texts(sentence) # 주어진 문장의 단어장(vocabulary) 생성
bow = dict(tokenizer.word_counts) # 각 단어와 각 단어의 빈도를 bow에 저장

print("Bag of Words :", bow) # bow 출력
print('단어장(Vocabulary)의 크기 :', len(tokenizer.word_counts)) # 중복을 제거한 단어들의 개수

# >> Bag of Words : {'john': 1, 'likes': 3, 'to': 2, 'watch': 2, 'movies': 2, 'mary': 2, 'too': 1, 'also': 1, 'football': 1, 'games': 1}
# >> 단어장(Vocabulary)의 크기 : 10

단어장(vocabulary)?

  • bag of Words와는 다른 개념으로 자연어처리에 보편적으로 쓰이는 용어
  • 중복을 제거한 단어들의 집합
  • scikit-learn의 CountVectorizer를 활용한 BoW 구현
from sklearn.feature_extraction.text import CountVectorizer

sentence = ["John likes to watch movies. Mary likes movies too! Mary also likes to watch football games."]

vector = CountVectorizer()
bow = vector.fit_transform(sentence).toarray() # 각 단어의 빈도 저장

print('Bag of Words : ', bow) # 각 단어의 빈도만 출력
print('각 단어의 인덱스 :', vector.vocabulary_) # {"단어":단어의 빈도수}
print('단어장(Vocabulary)의 크기 :', len(vector.vocabulary_))

# >> Bag of Words :  [[1 1 1 1 3 2 2 2 1 2]]
# >> 각 단어의 인덱스 : {'john': 3, 'likes': 4, 'to': 7, 'watch': 9, 'movies': 6, 'mary': 5, 'too': 8, 'also': 0, 'football': 1, 'games': 2}
# >> 단어장(Vocabulary)의 크기 : 10

2. DTM(Document-Term Matrix)

  • BoW 기반의 문서-단어 행렬
  • 여러 문서의 BoW를 하나의 행렬로 구현해서 각 문서에 등장한 단어 빈도수를 하나의 행렬로 통합
  • 문서를 행으로, 단어를 열로 갖는다.(반대로 문서를 열로 단어를 행으로 한 행렬은 TDM(Term-Document Matrix))
  • 아래의 문서 3개로 DTM을 만들어보자
Doc 1: I like dog
Doc 2: I like cat
Doc 3: I like cat I like cat
  cat dog I like
doc1 0 1 1 1
doc2 1 0 1 1
doc3 2 0 2 2
  • DTM의 각 행을 문서 벡터(document vector), 각 열을 단어 벡터(word vector)라고 한다.
  • 문서 1개로 BoW를 만들었을 때보다 DTM은 각 행에 0이 많이 포함된다
  • 문서의 수가 많아질수록 통합 단어장의 크기가 커지며 DTM의 문서 벡터와 단어벡터 대부분의 값이 0이 된다
  • 여러 BoW를 통합해 만든 DTM로 각 문서간 유사도를 구할 수 있다.
  • 유사도를 구하는 방식은 다양하지만 보편적으로 코사인 유사도를 많이 사용한다.(아래 참고)
  • scikit-learn CountVector를 활용해 DTM 구현하기
from sklearn.feature_extraction.text import CountVectorizer

# 3개의 문서에 대해 DTM 만들기
corpus = [
    'John likes to watch movies',
    'Mary likes movies too',
    'Mary also likes to watch football games',    
]
vector = CountVectorizer()

print(vector.fit_transform(corpus).toarray()) # 코퍼스로부터 각 단어의 빈도수를 기록.
print(vector.vocabulary_) # 각 단어의 인덱스가 어떻게 부여되었는지를 보여준다.

# return 
"""
[[0 0 0 1 1 0 1 1 0 1]
 [0 0 0 0 1 1 1 0 1 0]
 [1 1 1 0 1 1 0 1 0 1]]
{'john': 3, 'likes': 4, 'to': 7, 'watch': 9, 'movies': 6, 'mary': 5, 'too': 8, 'also': 0, 'football': 1, 'games': 2}
"""
  • DTM의 한계점
    • 문서의 수, 단어의 수가 늘어날수록 DTM 내부의 값은 0이 대부분을 차지하게 되므로 저장 공간 측면에서 낭비이며 차원의 저주를 발생시킨다.
    • 단어의 빈도에만 집중한다는 점에서 한계가 있다. a, the, is 같은 경우 여러 문서내에서 빈도가 많겠지만 그렇다고 해서 유사한 문서라고 판단할 수 없기 때문이다.

이러한 DTM의 한계점을 극복하기 위해 각 단어의 중요도를 판단해 가중치를 주는 방식이 등장하였다.


cf. 코사인 유사도

  • 두 벡터간 유사도를 측정하는 방법 중 하나
  • 두 벡터의 방향이 얼마나 유사한지 나타내는 측정 지표
    • 두 벡터가 서로 독립적인 경우 코사인 유사도: 0

  • 두 벡터가 이루는 각도의 코사인 값으로 계산

(출처: 위키독스)

  • 문서간 코사인 유사도 계산하기
import numpy as np
from numpy import dot
from numpy.linalg import norm

# 위의 예제 DTM 벡터 구현
doc1 = np.array([0,1,1,1]) # 문서1 벡터
doc2 = np.array([1,0,1,1]) # 문서2 벡터
doc3 = np.array([2,0,2,2]) # 문서3 벡터

def cos_sim(A, B):
    return dot(A, B)/(norm(A)*norm(B))

print('{:.2f}'.format(cos_sim(doc1, doc2))) #문서1과 문서2의 코사인 유사도
print('{:.2f}'.format(cos_sim(doc1, doc3))) #문서1과 문서3의 코사인 유사도
print('{:.2f}'.format(cos_sim(doc2, doc3))) #문서2과 문서3의 코사인 유사도

3. TF-IDF

  • Term Frequency-Inverse Document Frequency의 약자
  • 각 단어의 중요도를 판단해서 가중치를 주는 방법
  • 모든 문서에서 자주 등장하는 단어는 중요도가 낮다고 판단하며, 특정 문서에서만 자주 등장하는 단어는 중요도가 높다고 판단한다.
  • 불용어처럼 중요도가 낮으면서 모든 문서에 등장하는 단어들이 노이즈가 되는 것을 완화해준다.
  • 그러나 TF-IDF가 항상 DTM보다 성능이 좋은 것은 아니다.
  • 먼저 DTM을 만든 뒤(TF행렬) TF-IDF 가중치(IDF)를 곱해서 TF-IDF 행렬을 완성한다

TF-IDF 수식 확인하기

[출처 : http://openuiz.blogspot.com/2018/11/tf-idf.html]

  • 위의 수식은 y는 문서, x는 단어라고 할 때의 TF-IDF 수식이다.
  • tf_(x,y)가 TF 행렬, 뒤의 log항이 IDF, 즉 역문서 빈도이다.
  • TF행렬은 DTM으로, IDF는 계산을 통해 구한다.
  • 모든 문서에 등장한 단어의 IDF가 가장 낮으며 1개의 문서에만 등장한 단어들이 가장 높은 IDF값을 갖는다.
  • EX1: 전체 문서 수가 5개이고 단어 'like'가 문서2에서 200번, 문서3에서 300번 등장하고 다른 문서에선 등장하지 않았을 때 'like'의 IDF를 구하라
    • 1. 문서빈도 DF와 전체 문서 수 N을 구한다.
      • DF(단어 'like'가 문서에서 몇개의 문서에서 등장했는지) = 2
      • N(전체 문서의 수): 5
    • 2. IDF = log(N/DF)
      • IDF = log(5/2) = 0.9162
  • EX2: 문서 2와 문서3에서 단어 like의 TF-IDF는?
    • 문서2에서의 TF-IDF = TF * log(N/DF) = 200 * log(5/2)
    • 문서3에서의 TF-IDF = TF*log(N/DF) = 300 * log(5/2)

TF-IDF 구현하기

from math import log
import pandas as pd

# 3개 문서 사용
docs = [
  'John likes to watch movies and Mary likes movies too',
  'James likes to watch TV',
  'Mary also likes to watch football games',  
]

# 통합 단어장 생성
vocab = list(set(w for doc in docs for w in doc.split())) # 길이 13인 단어장 생성됨
vocab.sort()

N = len(docs) # 총 문서의 수:3

#TF함수, IDF함수, TF-IDF 함수 생성하기
def tf(t, d):
    return d.count(t)

def idf(t):
    df = 0
    for doc in docs:
        df += t in doc    
    return log(N/(df + 1)) + 1 #df=0일 경우 분모가 0이므로 이를 방지하기 위해 분모에 1을 더한다

def tfidf(t, d):
    return tf(t,d)* idf(t)

# tf 함수를 사용해 DTM 생성
result = []
for i in range(N): # 각 문서에 대해서 아래 명령을 수행
    result.append([])
    d = docs[i]
    for j in range(len(vocab)):
        t = vocab[j]

        result[-1].append(tf(t, d))

tf_ = pd.DataFrame(result, columns = vocab) #DTM

# 각 단어의 IDF 계산
result = []
for j in range(len(vocab)):
    t = vocab[j]
    result.append(idf(t))

idf_ = pd.DataFrame(result, index = vocab, columns=["IDF"]) #IDF

# TF-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))

tfidf_ = pd.DataFrame(result, columns = vocab) #TF-IDF

scikit-learn TFidVectorizer 활용해 구현된 TF-IDF 사용하기

  • 위의 구현에서와 마찬가지로 log 분모가 0이되는 걸 막기 위해 임의의 숫자 1을 더한다
  • 또한 TFidVectorizer는 TF-IDF 결과에 L2 Norm도 추가로 수행한다.
from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
  'John likes to watch movies and Mary likes movies too',
  'James likes to watch TV',
  'Mary also likes to watch football games',  
]

tfidfv = TfidfVectorizer().fit(corpus)
vocab = list(tfidfv.vocabulary_.keys()) # 단어장을 리스트로 저장
vocab.sort() # 단어장을 알파벳 순으로 정렬

# TF-IDF 행렬에 단어장을 데이터프레임의 열로 지정하여 데이터프레임 생성
tfidf_ = pd.DataFrame(tfidfv.transform(corpus).toarray(), columns = vocab)
tfidf_

TF-IDF의 한계점

DTM과 TF-IDF는 결국 BoW 방식을 기반으로 하고 있어 근본적으로 단어의 의미를 벡터로 표현하지 못한다는 한계가 있다.


4. LSA(Latent Sematic Analysis, 잠재 의미 분석)

  • 특정 단어의 의미와 관련된 문서 집합(주제)를 찾고 싶을 때 적절한 방법(topic modeling)
  • 전체 corpus에서 문서 속 단어들 사이의 관계를 찾아내는 자연어 처리 정보 검색 기술
  • LSA를 통해 단어와 단어, 문서와 문서, 단어와 문서 사이의 의미적 유사성 점수를 찾아낼 수 있으며 이 방법은 단순히 특정 단어들의 빈도를 계산하는 것보다 더 효과적이다.
  • LSA를 이해하기 위해서는 SVD(특잇값 분해)를 이해해야 한다.

cf. 특이값 분해(Singular Value Decomposition, SVD)

[출처 : https://wikidocs.net/24949]

  • 정방행렬은 고유분해로 고윳값과 고유벡터를 찾고, 정방행렬이 아닌 행렬은 특이값분해를 한다. 물론 정방행렬로 특이값분해를 할 수 있다.
  • m*n 크기의 사각행렬 A를 특이벡터(singular vector)의 행렬과 특이값의 대각행렬로 분해하는 것

cf. Truncated SVD(절단된 특잇값 분해)

cf. Truncated SVD(절단된 특잇값 분해)와 LSA

  • 특잇값 중 가장 큰(중요한) t개만 남기고 해당 특잇값에 대응되는 특이벡터들로 행렬 A를 근사(approximate)
  • 이때 t는 하이퍼파라미터로 t를 크게 잡으면 기존 행렬 A에서 다양한 의미를 가져갈 수 있고, t를 작게 잡으면 노이즈를 제거할 수 있다.
  • 결국 truncated SVD를 수행하면 행렬 Σ의 대각 원소값 중 상위값 t개만 남게 되며 U행렬과 V행렬의 t열까지만 남게되고 이는 정보 손실을 발생시켜 기존의 행렬 A를 완벽히 복구할 수 없게 된다.

LSA는 DTM, TF-IDF 행렬 등에 truncated SVD를 수행하여 3개의 분해된 행렬을 얻는다. 이때 각각의 행렬들은 다음과같이 해석한다.(t = k)

  • U
    • 크기: (m, k)
    • m: 문서들의 수, k:남길 특잇값의 개수
    • 문서들과 관련된 의미들을 표현한 행렬
  • S: (k, k)
    • 단어들과 관련된 의미를 표현한 행렬
  • V^T:
    • 크기: (k, n)
    • n: 단어의 수
    • 각 의미의 중요도를 표현한 행렬
    • 각 열이 각 단어를 나타내는 n차원의 단어 벡터
    • k열은 전체 corpus에서 얻어낸 k개의 주요 topic이라고 볼 수 있다.

LSA 구현

import pandas as pd
import numpy as np
import urllib.request
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer

# NLTK 데이터셋 다운로드
nltk.download('punkt')
nltk.download('wordnet')
nltk.download('stopwords')

# 영어 텍스트 데이터 로드
data = pd.read_csv(csv_filename, on_bad_lines='skip') # air nz staff in aust strike for pay rise 형태


# 텍스트 데이터만 별도 저장
text = data[['headline_text']].copy()

# 데이터 중복 확인 및 제거
text.nunique()
text.drop_duplicates(inplace=True) # 중복 샘플 제거
text.reset_index(drop=True, inplace=True)


# 데이터 정제 및 정규화
# NLTK 토크나이저를 이용해서 토큰화
text['headline_text'] = text.apply(lambda row: nltk.word_tokenize(row['headline_text']), axis=1)

# 불용어 제거
stop_words = stopwords.words('english')
text['headline_text'] = text['headline_text'].apply(lambda x: [word for word in x if word not in (stop_words)])
# text 한 행당 데이터 형태 : [air, nz, staff, aust, strike, pay, rise]

# 단어 정규화. 3인칭 단수 표현 -> 1인칭 변환, 과거형 동사 -> 현재형 동사 등을 수행한다.
text['headline_text'] = text['headline_text'].apply(lambda x: [WordNetLemmatizer().lemmatize(word, pos='v') for word in x])

# 길이가 1 ~ 2인 단어는 제거.
text = text['headline_text'].apply(lambda x: [word for word in x if len(word) > 2])
print(text[:5])
# [air, staff, aust, strike, pay, rise] 형태


# 역토큰화 및 DTM 생성
## 이후 countvectorizer(DTM 생성), TfidVectorizer(TF-IDF 행렬 생성)의 입력으로 사용됨
detokenized_doc = []
for i in range(len(text)):
    t = ' '.join(text[i])
    detokenized_doc.append(t)

train_data = detokenized_doc # 전처리 최종 결과 저장
#  'air staff aust strike pay rise',

# countvectorizer로 DTM 생성
c_vectorizer = CountVectorizer(stop_words='english', max_features = 5000) # 상위 5000개의 단어만 사용
document_term_matrix = c_vectorizer.fit_transform(train_data) #크기: 문서의 수(데이터프레임행수) * 단어 집합의 크기(5000)


# Truncated SVD로 LSA 수행. 이때 t(토픽 수) = 10
from sklearn.decomposition import TruncatedSVD

n_topics = 10
lsa_model = TruncatedSVD(n_components = n_topics)
lsa_model.fit_transform(document_term_matrix)

# 행렬 V의 크기 확인
print(lsa_model.components_.shape) # (10,5000) 즉 (K, 단어의수)

# 각 행을 전체 코퍼스의 k개 주제(topic)로 판단하고 각 주제에서 n개씩 단어 출력
terms = c_vectorizer.get_feature_names_out() # 단어 집합. 5,000개의 단어가 저장됨.

def get_topics(components, feature_names, n=5):
    for idx, topic in enumerate(components):
        print("Topic %d:" % (idx+1), [(feature_names[i], topic[i].round(5)) for i in topic.argsort()[:-n - 1:-1]])
get_topics(lsa_model.components_, terms)

"""
Topic 1: [('police', 0.74637), ('man', 0.45354), ('charge', 0.2108), ('new', 0.1409), ('court', 0.11144)]
Topic 2: [('man', 0.69417), ('charge', 0.30008), ('court', 0.16726), ('face', 0.11649), ('murder', 0.10662)]
Topic 3: [('new', 0.83667), ('plan', 0.23632), ('say', 0.18291), ('govt', 0.11111), ('council', 0.10943)]
Topic 4: [('say', 0.73756), ('plan', 0.3599), ('govt', 0.16535), ('council', 0.13094), ('urge', 0.07603)]
Topic 5: [('plan', 0.72898), ('council', 0.17829), ('govt', 0.14759), ('urge', 0.07722), ('fund', 0.06133)]
Topic 6: [('govt', 0.55169), ('urge', 0.26786), ('court', 0.25394), ('fund', 0.19663), ('win', 0.16523)]
Topic 7: [('charge', 0.52425), ('court', 0.46413), ('face', 0.35807), ('murder', 0.12274), ('plan', 0.1069)]
Topic 8: [('win', 0.52304), ('court', 0.40622), ('kill', 0.21367), ('crash', 0.18104), ('face', 0.09614)]
Topic 9: [('win', 0.68451), ('charge', 0.39921), ('australia', 0.11035), ('sydney', 0.0739), ('open', 0.07209)]
Topic 10: [('council', 0.81516), ('kill', 0.15187), ('charge', 0.11251), ('crash', 0.09251), ('rise', 0.07724)]
"""

5. LDA(Latent Dirichlet Allocation, 잠재 디리클레 할당)

  • LSA와 함께 Topic modeling의 대표적인 알고리즘
  • 모든 문서들에는 주제가 있고 주제를 위해 어떤 단어들을 사용했다고 가정한다.
  • 문서들이 토픽들의 혼합으로 구성되어 있으며, 토픽들은 확률 분포에 기반하여 단어들을 생성한다고 가정
  • 데이터가 주어지면 LDA는 위의 가정에 따라 단어들의 분포로부터 문서가 생성되는 과정을 역추적해 문서의 토픽을 찾아냄

[출처 : https://noduslabs.com/cases/tutorial-lda-text-mining-network-analysis/]

  • 위의 그림에서 topics는 총 노랑, 핑크, 초록, 파랑 4가지이다
  • 노란색 토픽일 때 gene 단어가 등장할 확률은 0.04이다
  • Documents에서 각 topic에 맞는 단어들이 다음과 같이 사용되었다.
  • 사용된 단어들의 분포를 수직 그래프로 확인해보면 노란색 토픽의 단어들 비중이 가장 크므로 document는 노란색 토픽일 가능성이 높다

LDA 수행과정

  1. 토픽의 개수 k 결정. LDA는 토픽 k개가 M개의 전체 문서에 걸쳐 분포돼 있다고 가정함
  2. 모든 단어를 k개 토픽들 중 하나에 랜덤으로 할당함. 한 단어가 한 문서에서 2회이상 등장하였을 때 각 단어가 서로 다른 토픽에 할당되었을 수도 있다.
  3. 어떤 문서의 각 단어 w는 자신은 잘못된 토픽에 할당되어있으나 다른 단어들은 올바른 토픽에 할당되 있다고 가정함. 이에 따라 단어 w를 아래 2가지 기준에 따라 재할당
    1. p(topic t | document d) : 문서 d의 단어들 중 토픽 t에 해당하는 단어들의 비율
    2. p(word w | topic t) : 각 토픽들 t에서 해당 단어 w의 분포
  4. 모든 문서의 모든 단어에 대해 3을 반복

 

LDA 실습

# 역토큰화까지 LSA와 동일
import pandas as pd
import numpy as np
import urllib.request
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer

# NLTK 데이터셋 다운로드
nltk.download('punkt')
nltk.download('wordnet')
nltk.download('stopwords')

# 영어 텍스트 데이터 로드
data = pd.read_csv(csv_filename, on_bad_lines='skip') # air nz staff in aust strike for pay rise 형태


# 텍스트 데이터만 별도 저장
text = data[['headline_text']].copy()

# 데이터 중복 확인 및 제거
text.nunique()
text.drop_duplicates(inplace=True) # 중복 샘플 제거
text.reset_index(drop=True, inplace=True)


# 데이터 정제 및 정규화
# NLTK 토크나이저를 이용해서 토큰화
text['headline_text'] = text.apply(lambda row: nltk.word_tokenize(row['headline_text']), axis=1)

# 불용어 제거
stop_words = stopwords.words('english')
text['headline_text'] = text['headline_text'].apply(lambda x: [word for word in x if word not in (stop_words)])
# text 한 행당 데이터 형태 : [air, nz, staff, aust, strike, pay, rise]

# 단어 정규화. 3인칭 단수 표현 -> 1인칭 변환, 과거형 동사 -> 현재형 동사 등을 수행한다.
text['headline_text'] = text['headline_text'].apply(lambda x: [WordNetLemmatizer().lemmatize(word, pos='v') for word in x])

# 길이가 1 ~ 2인 단어는 제거.
text = text['headline_text'].apply(lambda x: [word for word in x if len(word) > 2])
print(text[:5])
# [air, staff, aust, strike, pay, rise] 형태


# 역토큰화 및 DTM 생성
## 이후 countvectorizer(DTM 생성), TfidVectorizer(TF-IDF 행렬 생성)의 입력으로 사용됨
detokenized_doc = []
for i in range(len(text)):
    t = ' '.join(text[i])
    detokenized_doc.append(t)

train_data = detokenized_doc # 전처리 최종 결과 저장
#  'air staff aust strike pay rise',

# countvectorizer로 DTM 생성
c_vectorizer = CountVectorizer(stop_words='english', max_features = 5000) # 상위 5000개의 단어만 사용
document_term_matrix = c_vectorizer.fit_transform(train_data) #크기: 문서의 수(데이터프레임행수) * 단어 집합의 크기(5000)



# LDA 실습
## 1. TfidVectorizer 이용해 TF-IDF 행렬 생성
tfidf_vectorizer = TfidfVectorizer(stop_words='english', max_features=5000) # 상위 5,000개의 단어만 사용
tf_idf_matrix = tfidf_vectorizer.fit_transform(train_data)

## 2. scikit-learn LDA Model 로 모델 학습
from sklearn.decomposition import LatentDirichletAllocation

### n_components = 토픽 개수 = 10
lda_model = LatentDirichletAllocation(n_components=10, learning_method='online', random_state=777, max_iter=1)
lda_model.fit_transform(tf_idf_matrix)

# LDA의 결과 토픽과 각 단어의 비중을 출력합시다
terms = tfidf_vectorizer.get_feature_names_out() # 단어 집합. 5,000개의 단어가 저장됨.

def get_topics(components, feature_names, n=5):
    for idx, topic in enumerate(components):
        print("Topic %d:" % (idx+1), [(feature_names[i], topic[i].round(5)) for i in topic.argsort()[:-n-1:-1]])

get_topics(lda_model.components_, terms)
  • 첫 번째 행렬 : 첫 번째 행렬의 행은 단어 집합의 단어들이고, 열은 Topic입니다.
  • 두 번째 행렬 : 두 번째 행렬의 행은 문서이고, 열은 Topic입니다

 


텍스트 분포를 이용한 벡터화: soynlp

띄어쓰기 단위의 단어 분포가 아닌 텍스트 자체의 분포를 이용해 토큰화를 수행할 수 있다.

교착어인 한국어 특성상 띄어쓰기만으로 토큰화를 하는 것은 부족하기에 형태소 분석기를 사용한다.

그러나 기존의 형태소 분석기는 등록된 단어를 기준으로 형태소를 분류하므로 새롭게 만들어진 단어를 인식하기 어렵다는 단점이 있다.

 

이러한 문제를 해결하기 위해 텍스트 데이터에서 특정 문자 시퀀스가 함께 자주 등장하는 빈도가 높고, 앞뒤로 조사 또는 완전히 다른 단어가 등장하는 것을 고려해서 해당 문자 시퀀스를 형태소라고 판단하는 형태소 분석기, soynlp가 등장하였다.

 

예를 들어, '모두의연구소'라는 문자열이 자주 연결되어 등장한다면 형태소라고 판단하고, '모두의연구소'라는 단어 앞, 뒤에 '최고', 'AI', '실력'과 같은 독립된 다른 단어들이 계속해서 등장한다면 '모두의연구소'를 형태소로 파악하는 방식의 형태소 분석기이다.

 


soynlp

  • 품사 태깅, 형태소 분석 등을 지원하는 한국어 형태소 분석기
  • 비지도 학습으로 형태소 분석을 하며 데이터에 자주 등장하는 단어들을 형태소로 분석한다
  • 내부적으로 단어 점수표로 동작하하며, 이 점수는 응집 확률(cohesion probability)  브랜칭 엔트로피(branching entropy) 를 활용한다.

 

응집확률(cohesion probability)

  • 내부 문자열(substring)이 얼마나 응집하여 자주 등장하는지를 판단하는 척도
  •  문자열을 문자 단위로 분리하여 내부 문자열을 만드는 과정에서, 왼쪽부터 순서대로 문자를 추가하면서 각 문자열이 주어졌을 때 그다음 문자가 나올 확률을 계산하여 누적 곱을 한 값
  • 높을수록 전체 코퍼스에서 이 문자열 시퀀스는 하나의 단어로 등장할 가능성이 높다

 브랜칭 엔트로피(branching entropy)

  • 확률 분포의 엔트로피값을 사용해 주어진 문자열에서 다음 문자가 등장할 가능성을 판단하는 척도
  • 랜칭 엔트로피의 값은 하나의 완성된 단어에 가까워질수록 문맥으로 인해 정확히 예측할 수 있게 되므로 점차 줄어든다.
  • '디스플레이'라는 단어를 예측한다고 해보자
word_score_table["디스"].right_branching_entropy #1.6371694761537934
word_score_table["디스플"].right_branching_entropy #-0.0
word_score_table["디스플레이"].right_branching_entropy #3.1400392861792916
  • "디스플" 다음 "레"가 오는 것이 명백하므로 브랜칭 엔트로피는 0이다.
  • 단어가 끝나면 그 경계 부분부터 새로운 조사가 들어올 수 있기 때문에 그 경계부분부터 브랜칭 엔트로피값이 증가한다

soynlp는 이렇게 계산한 점수를 사용해 두가지 문자열 토큰화 방법을 수행한다

  •  LTokenizer: 단어를 L토큰 + R토큰으로 나누며 점수가 가장 높은 L토큰을 찾아냄
  • 최대점수 토크나이저: 띄어쓰기가 되어 있지 않은 문장에서 점수가 높은 글자 시퀀스를 순차적으로 찾아내는 토크나이저

 

soynlp 실습

# 1. soynlp에서 제공하는 예제 말뭉치 다운로드
import urllib.request

txt_filename = os.getenv('HOME')+'/aiffel/topic_modelling/data/2016-10-20.txt'

urllib.request.urlretrieve("https://raw.githubusercontent.com/lovit/soynlp/master/tutorials/2016-10-20.txt",\
                            filename=txt_filename)
                            

# 2. 다운로드한 말뭉치 분리
from soynlp import DoublespaceLineCorpus

# 말뭉치에 대해서 다수의 문서로 분리
corpus = DoublespaceLineCorpus(txt_filename)


#3. 비지도학습 수행해 전체 corpus에서 응집확률, 브랜칭 엔트로피 단어 점수표 생성
from soynlp.word import WordExtractor

word_extractor = WordExtractor()
word_extractor.train(corpus)
word_score_table = word_extractor.extract() # 단어 점수표 계산


# 4. 단어별 응집확률 계산
word_score_table["반포한"].cohesion_forward #0.08838002913645132
word_score_table["반포한강"].cohesion_forward #0.19841268168224552
word_score_table["반포한강공"].cohesion_forward # 0.2972877884078849
word_score_table["반포한강공원"].cohesion_forward # 0.37891487632839754
word_score_table["반포한강공원에"].cohesion_forward #0.33492963377557666
# 가장 응집확률이 가장 높은 "반포한강공원"이 하나의 단어일 확률이 높다


# 5. 브랜칭 엔트로피 계산
word_score_table["디스"].right_branching_entropy
word_score_table["디스플"].right_branching_entropy


# 6. 띄어쓰기 단위로 나뉜 문장을 Ltokenizer 사용해 분리
from soynlp.tokenizer import LTokenizer

scores = {word:score.cohesion_forward for word, score in word_score_table.items()}
l_tokenizer = LTokenizer(scores=scores)
l_tokenizer.tokenize("국제사회와 우리의 노력들로 범죄를 척결하자", flatten=False)
## [('국제사회', '와'), ('우리', '의'), ('노력', '들로'), ('범죄', '를'), ('척결', '하자')]


# 7. 최대점수 토크나이저
from soynlp.tokenizer import MaxScoreTokenizer

maxscore_tokenizer = MaxScoreTokenizer(scores=scores)
maxscore_tokenizer.tokenize("국제사회와우리의노력들로범죄를척결하자")
## ['국제사회', '와', '우리', '의', '노력', '들로', '범죄', '를', '척결', '하자']

Reference

https://wikidocs.net/30708

 

19-02 잠재 디리클레 할당(Latent Dirichlet Allocation, LDA)

토픽 모델링은 문서의 집합에서 토픽을 찾아내는 프로세스를 말합니다. 이는 검색 엔진, 고객 민원 시스템 등과 같이 문서의 주제를 알아내는 일이 중요한 곳에서 사용됩니다. 잠재 디…

wikidocs.net