텍스트의 벡터화
23년 이전 글/모두의연구소 아이펠

텍스트의 벡터화

단어 빈도를 이용한 벡터화

Bag of Words(BoW)

자연어 처리나 정보 검색에서 쓰이는 매우 간단한 단어 표현 방법 

[ 문서 내의 단어들의 빈도(frequency)를 파악하는 Bag of Words ]

# BoW 표현
doc1 = 'John likes to watch movies. Mary likes movies too.'
BoW1 = {"John":1, "likes":2, "to":1, "watch":1, "movies":2, "Mary":1, "too":1}
doc2 = 'Mary also likes to watch football games.'
BoW2 = {"Mary":1, "also":1, "likes":1, "to":1, "watch":1, "football":1, "games":1}

# 순서는 다르지만 본질적으로 같음
BoW = {"too":1, "Mary":1, "movies":2, "John":1, "watch":1, "likes":2, "to":1}
BoW1 = {"John":1, "likes":2, "to":1, "watch":1, "movies":2, "Mary":1, "too":1}

# 다 합쳐서 표현
doc3 = 'John likes to watch movies. Mary likes movies too. Mary also likes to watch football games.'
BoW3 = {"John":1, "likes":3, "to":2, "watch":2, "movies":2, "Mary":2, "too":1, "also":1, "football":1, "games":1};

 

Bag of Words 구현(텐서플로우 토크나이저)

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) # 단어장 생성
bow = dict(tokenizer.word_counts) # 각 단어와 각 단어의 빈도를 bow에 저장

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

# result
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

 

scikit-learn CountVectorizer 활용

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_) # 각 단어의 인덱스가 어떻게 부여되었는지

#result
print('Bag of Words : ', bow) # 코퍼스로부터 각 단어의 빈도수를 기록한다.
print('각 단어의 인덱스 :', vector.vocabulary_) # 각 단어의 인덱스가 어떻게 부여되었는지를 보여준다.


print('단어장(Vocabulary)의 크기 :', len(vector.vocabulary_))

#result
단어장(Vocabulary)의 크기 : 10

 

DTM과 코사인 유사도

DTM(Document-Term Matrix) 문서-단어 행렬로 여러 문서의 Bag of Words를 하나의 행렬로 구현한 것

문서는 행으로, 단어는 열로가지는 행렬이며 이 순서가 바뀔경우 TDM(Term-Document Matrix)로 부르기도 함

Doc1 = "Intelligent applications creates intelligent business processes"
Doc2 = "Bots are intelligent applications"
Doc3 = "I do business intelligence"

이 문서에 해당하는 DTM

[ 출처 : https://www.darrinbishop.com/blog/2017/10/text-analytics-document-term-matrix/ ]

행렬안에 불필요하게 0이 많은 모습.. 문서수가 많으면 통합 단어장의 크기도 커져서 DTM은 문서와 단어 벡터 대부분의 값이 0이 되는 성질이 있음

 

문서간 유사도

문서1 = I like dog
문서2 = I like cat
문서3 = I like cat I like cat

코사인 유사도에 대한 개념

 

1) 코사인 유사도(Cosine Similarity)

BoW에 기반한 단어 표현 방법인 DTM, TF-IDF, 또는 뒤에서 배우게 될 Word2Vec 등과 같이 단어를 수치화할 수 있는 방법을 이해했다면 이러한 표현 방법에 대 ...

wikidocs.net

import numpy as np
from numpy import dot
from numpy.linalg import norm

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(cos_sim(doc1, doc2)) #문서1과 문서2의 코사인 유사도
print(cos_sim(doc1, doc3)) #문서1과 문서3의 코사인 유사도
print(cos_sim(doc2, doc3)) #문서2과 문서3의 코사인 유사도

#result
0.6666666666666667
0.6666666666666667
1.0000000000000002

 

DTM 구현과 한계

from sklearn.feature_extraction.text import CountVectorizer

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_) # 각 단어의 인덱스가 어떻게 부여되었는지를 보여준다.

#result
[[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한계

  • 행과 열이 늘어날 수록 대부분의 값이 0을 가지므로 저장 공간 측면에서 낭비이며, 지나친 차원크기는 차원의 저주를 발생시킴
  • 단어 빈도에만 집중하는 방법의 한계. 예를들어 불용어 'the'가 자주 나오는데 이걸 토대로 문서가 비슷하다고 볼 순 없음

 

TF-IDF(Term Frequency-Inverse Document Frequency)

모든 문서에서 자주 등장하는 단어는 중요도가 낮으며, 특정 문서에서 자주 등장하는 단어는 중요도가 높다고 판단하는 방법

TF-IDF는 불용어처럼 중요도가 낮으면서 자주 나오는 단어들의 노이즈를 완화해주지만 이것이 DTM보다 항상 성능이 뛰어나다는 것은 아님

TF-IDF는 DTM을 만든뒤에 TF-IDF의 가중치를 DTM에 적용하는 방법으로 만들 수 있음

 

TF-IDF 계산

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

문서가 y, 단어가 x 일 때 TF-IDF의 수식

log항이 IDF 부분인데 역문서 빈도 IDF를 구해야함

DF는 문서빈도로, 문서 중 해당 단어가 나타난 값(ex: 5개 문서중 2개문서)

N은 전체 문서의 수 (ex: 5)

log(5/2)의 값이 나오며, 0.91629073187정도의 값이 됨

 

참조영상

 

TF-IDF 구현

from math import log
import pandas as pd
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()))
vocab.sort()
print('단어장의 크기 :', len(vocab))
print(vocab)

#result
단어장의 크기 : 13
['James', 'John', 'Mary', 'TV', 'also', 'and', 'football', 'games', 'likes', 'movies', 'to', 'too', 'watch']

IDF 구현에는 앞서 나온 식에서 조금씩 조정된 식을 사용하는데 이는 특정 단어가 전체 문서에서 등장하지 않을 경우 분모가 0이 되는 상황을 방지하기 위함

여기에서는 log항의 분모에 1, log항에 1값을 더해주는 것으로 조정했는데 이는 log항의 분자와 분모값이 동일 해졌을 때 log의 진수가 1이 됨에 따라서 IDF값이 0이 되는 것을 방지하기 위함

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
 
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)
tf_

각 단어의 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)
tfidf_

scikit-learn TFidfVectorizer 활용

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_

 

LSA와 LDA

LSA(Latent Semantic Analysis)

잠재 의미 분석으로 전체 코퍼스에서 문서 속 단어들 사이의 관계를 찾아내는 자연어 처리 정보 검색 기술로 단어와 단어 사이, 문서와 문서사이, 단어와 문서 사이 의미적 유사성 점수를 알 수 있음

 

특잇값 분해

LSA를 이해하기 위한 선형대수학의 특잇값 분해(Singular Value Decomposition)

참조링크

 

머신러닝 - 19. 고유값(eigenvalue), 고유벡터(eigenvector), 고유값 분해(eigen decomposition)

이번 시간에는 고유값과 고유 벡터, 그리고 고유값 분해에 대해 알아보겠습니다. 참고로 고유값, 고유 벡터를 이해하기 위해서는 행렬의 몇 가지 종류에 대해 알고 있어야 합니다. 본 글의 맨 아

bkshin.tistory.com

단위행렬은 주대각 성분이 모두 1이며, 나머지 성분은 0인 정사각행렬

n차 정사각형 행렬에 대해 어떤 행렬을 곱했을 때 결과가 단위행렬이 나온다면 이 어떤 행렬을 역행렬이라 함

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

특잇값 분해는 m x n 크기의 임의 사각행렬 A를 그림의 Full SVD와 같이 특이 벡터(singular vector)의 행렬과 특잇값(singular value)의 대각행렬로 분해하는 것

참조링크

 

 

3.4 특잇값 분해 — 데이터 사이언스 스쿨

정방행렬은 고유분해로 고윳값과 고유벡터를 찾을 수 있었다. 정방행렬이 아닌 행렬은 고유분해가 불가능하다. 하지만 대신 고유분해와 비슷한 특이분해를 할 수 있다. 1차원 근사 2차원 평면

datascienceschool.net

특잇값 가운데 가장 큰(중요한) t개만 남기고 해당 특잇값에 대응되는 특이 벡터들로 행렬 A를 근사하면 이는 절단된 특잇값 분해(Truncated SVD)라고 함

 

LSA와 Truncated SVD

[ 출처 : https://www.analyticsvidhya.com/blog/2018/10/stepwise-guide-topic-modeling-latent-semantic-analysis/ ]

LSA는 DTM이나 TF-IDF 행렬 등에 Truncated SVD를 수행하며 분해해서 얻은 행렬3개

문서들와 관련된 의미 표현 행렬, 단어들과 관련된 의미 표현 행렬, 각 의미의 중요도 표현 행렬

m이 문서의 수, n이 단어의 수 일때 Truncated SVD를 통해 얻은 행렬 Uk는 m x k의 크기를 가지며 문서의 수에 해당하는 m의 크기는 줄지 않고, Uk의 각 행은 각 문서를 표현하는 문서 벡터임

VkT는 k X n의 크기를 가지는 행렬로 이 행렬의 각 열은 단어를 나타내는 n 차원의 단어벡터가 됨 A에서 단어 벡터의 크기는 m이었는데 VkT에선 k의 크기를 가지게 되었으니 벡터의 차원이 저차원으로 축소되며, 이때 잠재된 의미를 끌어내는 방법이 워드 임베딩

DTM이나 TF-IDF 행렬에 Truncated SVD를 수행하고 얻은 VkT행렬의 k 열은 전체 코퍼스로부터 얻어낸 k개의 주요 주제로 간주도 가능

 

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.download('punkt')
nltk.download('wordnet')
nltk.download('stopwords')
import os
csv_filename = os.getenv('HOME')+'/aiffel/topic_modelling/data/abcnews-date-text.csv'
urllib.request.urlretrieve("https://raw.githubusercontent.com/franciscadias/data/master/abcnews-date-text.csv", 
                           filename=csv_filename)

data = pd.read_csv(csv_filename, error_bad_lines=False)
data.shape

#result
(1082168, 2)

data.head()

# headline만 저장
text = data[['headline_text']].copy()

#중복 체크
text.nunique() # 중복을 제외하고 유일한 시퀀스를 가지는 샘플의 개수를 출력

#result
headline_text    1054983
dtype: int64

#중복 제거
text.drop_duplicates(inplace=True) # 중복 샘플 제거
text.reset_index(drop=True, inplace=True)
text.shape

#result
(1054983, 1)

데이터 정제, 정규화

# 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.head()

동일하면서 다른 표현을 가지는 단어를 하나의 단어로 통합하는 단어 정규화 과정, 길이가 1~2인 단어를 제거

# 단어 정규화. 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])

#result
0     [aba, decide, community, broadcast, licence]
1    [act, fire, witness, must, aware, defamation]
2       [call, infrastructure, protection, summit]
3            [air, staff, aust, strike, pay, rise]
4    [air, strike, affect, australian, travellers]
Name: headline_text, dtype: object

 

역토큰화 및 DTM생성

DTM을 생성하는 CountVectorizer 또는 TF-IDF 행렬을 생성하는 TfidVectorizer의 입력으로 사용하기 위해 토큰화 과정을 역으로 돌리는 역토큰화 수행(detokenization)

# 역토큰화 (토큰화 작업을 역으로 수행)
detokenized_doc = []
for i in range(len(text)):
    t = ' '.join(text[i])
    detokenized_doc.append(t)

train_data = detokenized_doc

#result
['aba decide community broadcast licence',
 'act fire witness must aware defamation',
 'call infrastructure protection summit',
 'air staff aust strike pay rise',
 'air strike affect australian travellers']

CountVectorizer 사용한 DTM 생성

# 상위 5000개의 단어만 사용
c_vectorizer = CountVectorizer(stop_words='english', max_features = 5000)
document_term_matrix = c_vectorizer.fit_transform(train_data)

print('행렬의 크기 :',document_term_matrix.shape)
행렬의 크기 : (1054983, 5000)
# 문서수 x 단어집합

 

scikit-learn TruncatedSVD

참조링크

https://scikit-learn.org/stable/modules/generated/sklearn.decomposition.TruncatedSVD.html#sklearn.decomposition.TruncatedSVD.fit_transform

 

sklearn.decomposition.TruncatedSVD

Examples using sklearn.decomposition.TruncatedSVD: Hashing feature transformation using Totally Random Trees Hashing feature transformation using Totally Random Trees, Manifold learning on handwrit...

scikit-learn.org

from sklearn.decomposition import TruncatedSVD

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

print(lsa_model.components_.shape)

#result
(10, 5000)

terms = c_vectorizer.get_feature_names() # 단어 집합. 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)

#result
Topic 1: [('police', 0.74637), ('man', 0.45362), ('charge', 0.21079), ('new', 0.1409), ('court', 0.11146)]
Topic 2: [('man', 0.69419), ('charge', 0.30054), ('court', 0.16763), ('face', 0.11476), ('murder', 0.10638)]
Topic 3: [('new', 0.83666), ('plan', 0.23664), ('say', 0.18268), ('govt', 0.11105), ('council', 0.10946)]
Topic 4: [('say', 0.73824), ('plan', 0.3585), ('govt', 0.16957), ('council', 0.13094), ('urge', 0.07511)]
Topic 5: [('plan', 0.73175), ('council', 0.17649), ('govt', 0.14029), ('urge', 0.08755), ('water', 0.07104)]
Topic 6: [('govt', 0.54527), ('court', 0.24193), ('fund', 0.23791), ('urge', 0.22731), ('face', 0.13834)]
Topic 7: [('charge', 0.55868), ('court', 0.42866), ('face', 0.32573), ('murder', 0.14436), ('plan', 0.09852)]
Topic 8: [('win', 0.58246), ('court', 0.35312), ('kill', 0.20069), ('australia', 0.16082), ('crash', 0.1328)]
Topic 9: [('win', 0.54102), ('charge', 0.46396), ('council', 0.17023), ('cup', 0.07653), ('australia', 0.07556)]
Topic 10: [('council', 0.51473), ('kill', 0.44969), ('crash', 0.37022), ('charge', 0.1675), ('car', 0.1549)]

 

LDA

잠재 디리클레 할당이라 불리며 토픽 모델링의 다른 대표 알고리즘

LDA는 문서들이 토픽의 혼합으로 구성되어 있으며, 토픽은 확률 분포에 기반하여 단어를 생성한다고 가정하며

LDA는 이 가정에 따라 단어들의 분포로부터 문서가 생성되는 과정을 역추적해 문서 토픽을 찾아냄

 

LDA 시뮬레이션

 

 

Latent Dirichlet Allocation Topic Modeling by David Lettier

 

lettier.com

 

1. Add Document를 다수의 단어로 구성된 다수의 문서 추가

2. Topics 값을 원하는 값으로 지정하고 몇개의 주제를 얻을 것인지 정하는 하이퍼파라미터

3. Run LDA를 누르면 두개의 행렬이 나옴

첫번째 행렬은 단어 집합의 단어들이고 열은 Topic

두번째 행렬의 행은 문서이고 열은 Topic

 

LDA의 두가지 결과

LDA는 각 토픽의 단어 분포와 각 문서의 토픽 분포를 추정

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

Topics라고 적힌 부분을 보면 그중 초록색 토픽에는 brain이라는 단어가 등장할 확률이 0.04

그림 중앙에 Documents에 노란색, 분홍색, 하늘색 토픽이라는 세가지 토픽이 존재

Topic proportions and assignments 아래 막대그래프는 문서에 존재하는 토픽 비율을 시각화한 그래프로

이 문서엔 세가지 토픽이 존재함, 노란색 토픽 비중이 가장 큼

 

LDA의 가정

문서를 작성하기 위한 주제와, 주제를 위한 단어를 설정한다는 가정

 

 

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

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

wikidocs.net

LSA는 DTM을 차원 축소하여 축소 차원에서 근접 단어를 토픽으로 묶는 반면 LDA는 단어가 특정 토픽에 존재할 확률과 문서에 특정 토픽이 존재할 확률을 결합 확률로 추정하여 토픽을 추출

 

55. 텍스트 데이터 분석 [토픽 모델] - (4) LDA 모형

 

serviceapi.nmv.naver.com

 

LDA 실습

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

# TF-IDF 행렬의 크기를 확인해봅시다.
print('행렬의 크기 :', tf_idf_matrix.shape)

#result
행렬의 크기 : (1054983, 5000)

 

#scikit-learn LDA Model
from sklearn.decomposition import LatentDirichletAllocation

lda_model = LatentDirichletAllocation(n_components=10, learning_method='online', random_state=777, max_iter=1)
lda_model.fit_transform(tf_idf_matrix)

#result
array([[0.0335099 , 0.0335099 , 0.0335099 , ..., 0.17024867, 0.0335099 ,
        0.0335099 ],
       [0.03365631, 0.03365631, 0.03365631, ..., 0.03365631, 0.03365631,
        0.03365631],
       [0.25184095, 0.0366096 , 0.0366096 , ..., 0.0366096 , 0.0366096 ,
        0.0366096 ],
       ...,
       [0.26687206, 0.02914502, 0.02914502, ..., 0.13007484, 0.02916018,
        0.28739608],
       [0.10378115, 0.02637829, 0.12325014, ..., 0.02637829, 0.02637829,
        0.02637829],
       [0.03376055, 0.03376055, 0.2255442 , ..., 0.03376055, 0.03376055,
        0.03376055]])


print(lda_model.components_.shape)
#result
(10, 5000)

전체 코퍼스로부터 얻은 10개 토픽과 각 토픽에서 단어의 비중을 확인

# LDA의 결과 토픽과 각 단어의 비중을 출력합시다
terms = tfidf_vectorizer.get_feature_names() # 단어 집합. 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)

#result
Topic 1: [('australia', 9359.06334), ('sydney', 5854.97288), ('attack', 4784.76322), ('change', 4193.63035), ('year', 3924.88997)]
Topic 2: [('government', 6344.07413), ('charge', 5947.12292), ('man', 4519.7974), ('state', 3658.16422), ('live', 3625.10473)]
Topic 3: [('australian', 7666.65651), ('say', 7561.01807), ('police', 5513.22932), ('home', 4048.38409), ('report', 3796.04446)]
Topic 4: [('melbourne', 5298.35047), ('south', 4844.59835), ('death', 4281.78433), ('china', 3214.44581), ('women', 3029.28443)]
Topic 5: [('win', 5704.0914), ('canberra', 4322.0963), ('die', 4025.63057), ('open', 3771.65243), ('warn', 3577.47151)]
Topic 6: [('court', 5246.3124), ('world', 4536.86331), ('country', 4166.34794), ('woman', 3983.97748), ('crash', 3793.50267)]
Topic 7: [('election', 5418.5038), ('adelaide', 4864.95604), ('house', 4478.6135), ('school', 3966.82676), ('2016', 3955.11155)]
Topic 8: [('trump', 8189.58575), ('new', 6625.2724), ('north', 3705.40987), ('rural', 3521.42659), ('donald', 3356.26657)]
Topic 9: [('perth', 4552.8151), ('kill', 4093.61782), ('break', 2695.71958), ('budget', 2596.93268), ('children', 2586.01957)]
Topic 10: [('queensland', 5552.68506), ('coast', 3825.32603), ('tasmanian', 3550.75997), ('shoot', 3185.71575), ('service', 2695.21462)]

 

텍스트 분포를 이용한 비지도 학습 토크나이저

영어의 토큰화

en_text = "The dog ran back to the corner near the spare bedrooms"
print(en_text.split())

#result
['The', 'dog', 'ran', 'back', 'to', 'the', 'corner', 'near', 'the', 'spare', 'bedrooms']

한국어의 토큰화

kor_text = "사과의 놀라운 효능이라는 글을 봤어. 그래서 오늘 사과를 먹으려고 했는데 사과가 썩어서 슈퍼에 가서 사과랑 오렌지 사 왔어"
print(kor_text.split())

['사과의', '놀라운', '효능이라는', '글을', '봤어.', '그래서', '오늘', '사과를', '먹으려고', '했는데', '사과가', '썩어서', '슈퍼에', '가서', '사과랑', '오렌지', '사', '왔어']

형태소 분석기를 통한 토큰화

from konlpy.tag import Okt

tokenizer = Okt()
print(tokenizer.morphs(kor_text))

#result
['사과', '의', '놀라운', '효능', '이라는', '글', '을', '봤어', '.', '그래서', '오늘', '사과', '를', '먹으려고', '했는데', '사과', '가', '썩어서', '슈퍼', '에', '가서', '사과', '랑', '오렌지', '사', '왔어']

단어 미등록 문제

print(tokenizer.morphs('모두의연구소에서 자연어 처리를 공부하는 건 정말 즐거워'))

#result
['모두', '의', '연구소', '에서', '자연어', '처리', '를', '공부', '하는', '건', '정말', '즐거워']

 

soynlp

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

다운로드 한 말뭉치를 문서단위로 분리

from soynlp import DoublespaceLineCorpus

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

#result
30091

3개의 문서 테스트 출력

i = 0
for document in corpus:
  if len(document) > 0:
    print(document)
    i = i+1
  if i == 3:
    break
    
#result
19  1990  52 1 22
오패산터널 총격전 용의자 검거 서울 연합뉴스 경찰 관계자들이 19일 오후 서울 강북구 오패산 터널 인근에서 사제 총기를 발사해 경찰을 살해한 용의자 성모씨를 검거하고 있다 성씨는 검거 당시 서바이벌 게임에서 쓰는 방탄조끼에 헬멧까지 착용한 상태였다 독자제공 영상 캡처 연합뉴스  서울 연합뉴스 김은경 기자 사제 총기로 경찰을 살해한 범인 성모 46 씨는 주도면밀했다  
경찰에 따르면 성씨는 19일 오후 강북경찰서 인근 부동산 업소 밖에서 부동산업자 이모 67 씨가 나오기를 기다렸다 이씨와는 평소에도 말다툼을 자주 한 것으로 알려졌다  
이씨가 나와 걷기 시작하자 성씨는 따라가면서 미리 준비해온 사제 총기를 이씨에게 발사했다 총알이 빗나가면서 이씨는 도망갔다 그 빗나간 총알은 지나가던 행인 71 씨의 배를 스쳤다  성씨는 강북서 인근 치킨집까지 이씨 뒤를 쫓으며 실랑이하다 쓰러뜨린 후 총기와 함께 가져온 망치로 이씨 머리를 때렸다  이 과정에서 오후 6시 20분께 강북구 번동 길 위에서 사람들이 싸우고 있다 총소리가 났다 는 등의 신고가 여러건 들어왔다  5분 후에 성씨의 전자발찌가 훼손됐다는 신고가 보호관찰소 시스템을 통해 들어왔다 성범죄자로 전자발찌를 차고 있던 성씨는 부엌칼로 직접 자신의 발찌를 끊었다  용의자 소지 사제총기 2정 서울 연합뉴스 임헌정 기자 서울 시내에서 폭행 용의자가 현장 조사를 벌이던 경찰관에게 사제총기를 발사해 경찰관이 숨졌다 19일 오후 6시28분 강북구 번동에서 둔기로 맞았다 는 폭행 피해 신고가 접수돼 현장에서 조사하던 강북경찰서 번동파출소 소속 김모 54 경위가 폭행 용의자 성모 45 씨가 쏜 사제총기에 맞고 쓰러진 뒤 병원에 옮겨졌으나 숨졌다 사진은 용의자가 소지한 사제총기  신고를 받고 번동파출소에서 김창호 54 경위 등 경찰들이 오후 6시 29분께 현장으로 출동했다 성씨는 그사이 부동산 앞에 놓아뒀던 가방을 챙겨 오패산 쪽으로 도망간 후였다  김 경위는 오패산 터널 입구 오른쪽의 급경사에서 성씨에게 접근하다가 오후 6시 33분께 풀숲에 숨은 성씨가 허공에 난사한 10여발의 총알 중 일부를 왼쪽 어깨 뒷부분에 맞고 쓰러졌다  김 경위는 구급차가 도착했을 때 이미 의식이 없었고 심폐소생술을 하며 병원으로 옮겨졌으나 총알이 폐를 훼손해 오후 7시 40분께 사망했다  김 경위는 외근용 조끼를 입고 있었으나 총알을 막기에는 역부족이었다  머리에 부상을 입은 이씨도 함께 병원으로 이송됐으나 생명에는 지장이 없는 것으로 알려졌다  성씨는 오패산 터널 밑쪽 숲에서 오후 6시 45분께 잡혔다  총격현장 수색하는 경찰들 서울 연합뉴스 이효석 기자 19일 오후 서울 강북구 오패산 터널 인근에서 경찰들이 폭행 용의자가 사제총기를 발사해 경찰관이 사망한 사건을 조사 하고 있다  총 때문에 쫓던 경관들과 민간인들이 몸을 숨겼는데 인근 신발가게 직원 이모씨가 다가가 성씨를 덮쳤고 이어 현장에 있던 다른 상인들과 경찰이 가세해 체포했다  성씨는 경찰에 붙잡힌 직후 나 자살하려고 한 거다 맞아 죽어도 괜찮다 고 말한 것으로 전해졌다  성씨 자신도 경찰이 발사한 공포탄 1발 실탄 3발 중 실탄 1발을 배에 맞았으나 방탄조끼를 입은 상태여서 부상하지는 않았다  경찰은 인근을 수색해 성씨가 만든 사제총 16정과 칼 7개를 압수했다 실제 폭발할지는 알 수 없는 요구르트병에 무언가를 채워두고 심지를 꽂은 사제 폭탄도 발견됐다  일부는 숲에서 발견됐고 일부는 성씨가 소지한 가방 안에 있었다
테헤란 연합뉴스 강훈상 특파원 이용 승객수 기준 세계 최대 공항인 아랍에미리트 두바이국제공항은 19일 현지시간 이 공항을 이륙하는 모든 항공기의 탑승객은 삼성전자의 갤럭시노트7을 휴대하면 안 된다고 밝혔다  두바이국제공항은 여러 항공 관련 기구의 권고에 따라 안전성에 우려가 있는 스마트폰 갤럭시노트7을 휴대하고 비행기를 타면 안 된다 며 탑승 전 검색 중 발견되면 압수할 계획 이라고 발표했다  공항 측은 갤럭시노트7의 배터리가 폭발 우려가 제기된 만큼 이 제품을 갖고 공항 안으로 들어오지 말라고 이용객에 당부했다  이런 조치는 두바이국제공항 뿐 아니라 신공항인 두바이월드센터에도 적용된다  배터리 폭발문제로 회수된 갤럭시노트7 연합뉴스자료사진
from soynlp.word import WordExtractor

word_extractor = WordExtractor()
word_extractor.train(corpus)
word_score_table = word_extractor.extract()

#result
training was done. used memory 2.496 Gb
all cohesion probabilities was computed. # words = 223348
all branching entropies was computed # words = 361598
all accessor variety was computed # words = 361598

 

soynlp의 응집확률(cohesion probability)

응집 확률은 내부 문자열이 얼마나 응집하여 자주 등장하는지 판단하는 척도

문자열을 문자 단위로 분리하여 내부 문자열을 만드는 과정에서 왼쪽부터 순서대로 문자를 추가하며 각 문자열이 주어졌을 때 그 다음 문자가 나올 확률을 계산하여 누적곱을 한 값

이 값이 높을수록 전체 코퍼스에서 이 문자열 시퀸스는 하나의 단어로 등장할 가능성이 높음

word_score_table["반포한"].cohesion_forward
#result
0.08838002913645132

word_score_table["반포한강"].cohesion_forward
#result
0.19841268168224552

word_score_table["반포한강공"].cohesion_forward
#result
0.2972877884078849

word_score_table["반포한강공원"].cohesion_forward
#result
0.37891487632839754

word_score_table["반포한강공원에"].cohesion_forward
#result
0.33492963377557666

 

soynlp의 브랜칭 엔트로피

브랜칭 엔트로피는 확률 분포의 엔트로피 값을 사용

word_score_table["디스"].right_branching_entropy
#result
1.6371694761537934

word_score_table["디스플"].right_branching_entropy
#result
-0.0

#디스플 이후의 값이 0
word_score_table["디스플레"].right_branching_entropy
-0.0

#디스플레이 이후 다른 값이 나올 수 있으므로
word_score_table["디스플레이"].right_branching_entropy
#result
3.1400392861792916

 

soynlp LTokenizer

L토크나이저 사용, L 토크나이저는 L토큰 + R토큰으로 나누되 점수가 가장 높은 L 토큰을 찾아내는 분리 기준을 가지고 있음

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)

#result
[('국제사회', '와'), ('우리', '의'), ('노력', '들로'), ('범죄', '를'), ('척결', '하자')]

 

최대 점수 토크나이저

최대 점수 토크나이저(MAXScoreTokenizer)는 띄어쓰기가 되어 있지 않은 문장에서 점수가 높은 글자 시퀸스를 순차적으로 찾아내는 토크나이저

from soynlp.tokenizer import MaxScoreTokenizer

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

#result
['국제사회', '와', '우리', '의', '노력', '들로', '범죄', '를', '척결', '하자']

 

반응형