Word2Vec의 대중화 이전에, 텍스트의 분포를 활용하여 텍스트를 벡터화하는 아이디어를 들여다보자.
텍스트를 벡터화하는 방법으로는 (1) 통계와 머신러닝 활용, (2) 인공 신경망을 활용하는 두가지 방법이 있다.
이번엔 전자의 방법으로 가보자
BoW란, 단어들의 순서는 전혀 고려하지 않고, 단어들의 출현 빈도(frequency)에만 집중하는 텍스트 데이터의 수치화 표현 방법이다. 텍스트를 전부 단어 단위로 토큰화 하고, 단어 사용 횟수를 카운트 한다.
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}
어순이 달라지더라도 같은 문장으로 취급한다는 한계가 있다
# Keras Tokenizer로 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) # 텍스트를 리스트 형태로 단어장 생성 (중복 X)
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
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
scikit-learn의 CountVectorizer 빈도수만 나올 뿐이다.
Keras의 토크나이저를 사용하는 것이 보통이다
DTM이란, 여러 문서의 Bag of Words를 하나의 행렬로 구현한 것이다.
즉, 각 문서에 등장한 단어의 빈도수를 하나의 행렬로 통합한 것이다.
Doc 1: Intelligent applications creates intelligent business processes
Doc 2: Bots are intelligent applications
Doc 3: I do business intelligence
위 문장들로 만들어진 DTM
row는 문서 벡터(document vector), column은 단어 벡터(word vector)
문서 수가 많아지면 단어장이 커져서 희소벡터가 되어버린다.
# 코사인 유사도
# 문서1 : I like dog
# 문서2 : I like cat
# 문서3 : I like cat I like cat
# 위 문장의 DTM에서 코사인 유사도 계산
# https://wikidocs.net/24603
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의 코사인 유사도
0.6666666666666667
0.6666666666666667
1.0000000000000002
문서1
과 문서2
의 코사인 유사도는 0.67, 문서1
과 문서3
의 코사인 유사도도 0.67
문서2
와 문서3
의 유사도는 1 (모든 단어의 빈도수가 동일하게 증가했기 때문)
코사인 유사도는 벡터의 크기가 아니라 벡터의 방향(패턴)에 초첨을 둔다
# CountVectorizer로 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_) # 각 단어의 인덱스가 어떻게 부여되었는지를 보여준다.
[[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}
the
가 많이 있다고 해서 유사한 문장 X TF-IDF
(Term Frequency-Inverse Document Frequency)는 모든 문서에서 자주 등장하는 단어는 중요도를 낮게 보고, 특정 문서에서만 자주 등장하는 단어는 중요도를 높게 본다. 마치 불용어를 제외하고 보듯이. -> IDF 항에서 이 역할을 수행
하지만 이것이 DTM보다 성능이 항상 좋지는 않다.
DTM을 만든 뒤 TF-IDF 가중치를 DTM에 적용
사실 DTM 자체가 이미 TF (Term Frequency)
tf 뒤에 곱해지는 log항이 IDF.
전체 문서의 수가 5개라고 해봅시다. 그리고 단어 'like'가 문서2에서 200번, 문서 3에서 300번 등장했다고 해봅시다. 다른 문서에서 단어 'like'
는 등장하지 않았습니다. 이때, 단어 'like'의 IDF는 몇일까요?
($tf1=200,tf2=300, N=500, df=200 $)
그러면 여기서 문서2와 문서3의 단어 'like'
의 TF-IDF의 값은?
위에꺼 계산 이거 맞아??
https://www.bloter.net/newsView/blt201609280001
희소벡터를 해결하기 위해 특이값분해를 통해 축소시킨다.
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',
]
# DTM의 열을 만들기 위해 문서 3개의 단어가 모두 들어간 통합 단어장을 만든다.
vocab = list({w for doc in docs for w in doc.split()}) # set comprehension으로 중복 단어 제거
vocab.sort()
print('단어장의 크기 :', len(vocab))
print(vocab)
N = len(docs)
N
단어장의 크기 : 13
['James', 'John', 'Mary', 'TV', 'also', 'and', 'football', 'games', 'likes', 'movies', 'to', 'too', 'watch']
3
# TF-IDF 함수를 만드는 데, log 항에는 분모 1을 더해준다 (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 # log항에 또 1을 더해준다. 분자와 분모값이 같아져서 0이 되는 것을 방지
def tf_idf(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 삽입
tf_ = pd.DataFrame(result, columns= vocab)
tf_
James | John | Mary | TV | also | and | football | games | likes | movies | to | too | watch | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | 1 | 0 | 0 | 1 | 0 | 0 | 2 | 2 | 2 | 1 | 1 |
1 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
2 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 1 |
result = []
for j in range(len(vocab)):
t = vocab[j]
result.append(idf(t))
idf_ = pd.DataFrame(result, index = vocab, columns=["IDF"])
idf_
IDF | |
---|---|
James | 1.405465 |
John | 1.405465 |
Mary | 1.000000 |
TV | 1.405465 |
also | 1.405465 |
and | 1.405465 |
football | 1.405465 |
games | 1.405465 |
likes | 0.712318 |
movies | 1.405465 |
to | 0.712318 |
too | 1.405465 |
watch | 0.712318 |
result = []
for i in range(N):
result.append([])
d = docs[i]
for j in range(len(vocab)):
t = vocab[j]
result[-1].append(tf_idf(t,d))
tfidf_ = pd.DataFrame(result, columns = vocab)
tfidf_
James | John | Mary | TV | also | and | football | games | likes | movies | to | too | watch | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.000000 | 1.405465 | 1.0 | 0.000000 | 0.000000 | 1.405465 | 0.000000 | 0.000000 | 1.424636 | 2.81093 | 1.424636 | 1.405465 | 0.712318 |
1 | 1.405465 | 0.000000 | 0.0 | 1.405465 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.712318 | 0.00000 | 0.712318 | 0.000000 | 0.712318 |
2 | 0.000000 | 0.000000 | 1.0 | 0.000000 | 1.405465 | 0.000000 | 1.405465 | 1.405465 | 0.712318 | 0.00000 | 0.712318 | 0.000000 | 0.712318 |
사이킷런에서 DTM을 만들 때 CountVectorizer
를 사용했듯이,
TF-IDF를 자동으로 계산하여 출력하는 TfidfVectorizer 사용 가능하다.
여기에선 log항의 분자에도 1을 더해주며, 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_
also | and | football | games | james | john | likes | mary | movies | to | too | tv | watch | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.000000 | 0.321556 | 0.000000 | 0.000000 | 0.000000 | 0.321556 | 0.379832 | 0.244551 | 0.643111 | 0.189916 | 0.321556 | 0.000000 | 0.189916 |
1 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.572929 | 0.000000 | 0.338381 | 0.000000 | 0.000000 | 0.338381 | 0.000000 | 0.572929 | 0.338381 |
2 | 0.464997 | 0.000000 | 0.464997 | 0.464997 | 0.000000 | 0.000000 | 0.274634 | 0.353642 | 0.000000 | 0.274634 | 0.000000 | 0.000000 | 0.274634 |
LSA(Latent Semantic Analysis) 는 전체 코퍼스에서 문서 속 단어들 사이의 관계를 찾아내는 자연어 처리 정보 검색 기술
LSA를 사용하면 단어와 단어 사이, 문서와 문서 사이, 단어와 문서 사이의 의미적 유사성 점수를 찾아낼 수 있다.
고유값(eigenvalue), 고유벡터(eigenvector), 고유값 분해(eigen decomposition)
특잇값 분해
Singular value decomposition의 목적
LSA는 DTM이나 TF-IDF 행렬 등에 Truncated SVD를 수행
Truncated SVD를 수행하면 행렬 Σ의 대각 원솟값 중에서 상윗값 t개만 남게 되며, U행렬과 V행렬의 k열까지만 남는다. 이로 인해 세 행렬에서 값(정보)의 손실이 일어나 기존의 행렬 A를 정확히 복구할 수는 없다.
여기서 k는 하이퍼파라미터. t를 크게 잡으면 기존의 행렬 A로부터 다양한 의미를 가져갈 수 있지만, 노이즈를 제거하려면 k를 작게 잡아야 한다.
여기서 얻은 는 각각 '문서들과 관련된 의미들을 표현한 행렬', '단어들과 관련된 의미를 표현한 행렬' , '각 의미의 중요도를 표현한 행렬' 이라고 해석할 수 있다.
Uk는 mXk의 크기를 가지는데 m은 문서 벡터로, 줄어들지 않는다.
VkT는 kXn으로 저차원으로 축소되었다. k열은 전체 코퍼스로부터 얻어낸 k개의 주요 주제(topic)라고 할 수 있다.
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')
[nltk_data] Downloading package punkt to /aiffel/nltk_data...
[nltk_data] Unzipping tokenizers/punkt.zip.
[nltk_data] Downloading package wordnet to /aiffel/nltk_data...
[nltk_data] Unzipping corpora/wordnet.zip.
[nltk_data] Downloading package stopwords to /aiffel/nltk_data...
[nltk_data] Unzipping corpora/stopwords.zip.
True
# 데이터 다운로드
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)
('/aiffel/aiffel/topic_modelling/data/abcnews-date-text.csv',
<http.client.HTTPMessage at 0x7f31e2fe6690>)
data = pd.read_csv(csv_filename, error_bad_lines=False)
data.shape
# 108만개의 샘플 존재
(1082168, 2)
data.head()
publish_date | headline_text | |
---|---|---|
0 | 20030219 | aba decides against community broadcasting lic... |
1 | 20030219 | act fire witnesses must be aware of defamation |
2 | 20030219 | a g calls for infrastructure protection summit |
3 | 20030219 | air nz staff in aust strike for pay rise |
4 | 20030219 | air nz strike to affect australian travellers |
# 헤드라인만 가져오자
text = data[['headline_text']].copy()
text.head()
headline_text | |
---|---|
0 | aba decides against community broadcasting lic... |
1 | act fire witnesses must be aware of defamation |
2 | a g calls for infrastructure protection summit |
3 | air nz staff in aust strike for pay rise |
4 | air nz strike to affect australian travellers |
text.nunique() # 중복을 제외하고 유일한 시퀀스를 가지는 샘플의 개수를 출력
headline_text 1054983
dtype: int64
text.drop_duplicates(inplace=True) # 중복 샘플 제거
text.reset_index(drop=True, inplace=True)
text.shape
(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()
headline_text | |
---|---|
0 | [aba, decides, community, broadcasting, licence] |
1 | [act, fire, witnesses, must, aware, defamation] |
2 | [g, calls, infrastructure, protection, summit] |
3 | [air, nz, staff, aust, strike, pay, rise] |
4 | [air, nz, strike, affect, australian, travellers] |
# 단어 정규화. 3인칭 단수 표현 -> 1인칭 변환, 과거형 동사 -> 현재형 동사 등을 수행한다.
# WordNetLemmatizer().lemmatize로 동일한 단어이지만 다른 표현을 가지는 단어들을 하나로 통합(단어 정규화)
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])
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을 생성하는 CountVectorizer
또는 TF-IDF 행렬을 생성하는 TfidfVectorizer
의 입력으로 사용하기 위해서는 토큰화 과정을 역으로 되돌리는 역토큰화(detokenization)
text
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]
...
1054978 [compliment, womans, smile, guide]
1054979 [white, house, defend, trump, tweet]
1054980 [winter, close, tasmania, snow, ice, fall]
1054981 [womens, world, cup, australia, win, despite, ...
1054982 [youtube, stunt, death, foreshadow, tweet]
Name: headline_text, Length: 1054983, dtype: object
# 역토큰화 (토큰화 작업을 역으로 수행)
detokenized_doc = []
for i in range(len(text)):
t = ' '.join(text[i])
detokenized_doc.append(t)
train_data = detokenized_doc
train_data[:5]
['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']
# 상위 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)
Truncated SVD를 통해 LSA를 수행한다.
토픽의 수 10 = 하이퍼파라미터 k. $$ 행렬 V_k^T 가 k * (단어의 수)의 크기 $$를 가지도톡 DTM에 TruncatedSVD를 수행
from sklearn.decomposition import TruncatedSVD
n_topics = 10
lsa_model = TruncatedSVD(n_components = n_topics)
lsa_model.fit_transform(document_term_matrix)
array([[ 1.20376737e-02, -3.81908022e-03, 1.81480609e-02, ...,
2.79256277e-03, -9.93909980e-04, 1.24848449e-02],
[ 2.90955010e-02, -1.09056248e-02, 1.80152176e-02, ...,
-5.38805396e-03, 1.28468980e-02, -7.26340264e-03],
[ 5.05735116e-03, -2.00901941e-03, 9.65353416e-03, ...,
-5.28652194e-03, -3.01980860e-03, 2.67820340e-03],
...,
[ 2.96924830e-02, 4.83505650e-03, 2.49322624e-02, ...,
3.50457171e-02, -6.91731142e-03, 3.45635228e-03],
[ 6.16084902e-02, 7.94331605e-04, 1.35989941e-01, ...,
1.07899084e+00, -6.19802463e-01, -3.47817540e-01],
[ 7.06307670e-02, 3.09586671e-02, 3.65633899e-03, ...,
8.62574723e-02, 6.14413597e-04, 1.42147004e-02]])
# 위 코드로 없는 TruncatedSVD를 통해 얻은 행렬의 크기 확인
print(lsa_model.components_.shape)
(10, 5000)
terms = c_vectorizer.get_feature_names() # 단어 집합. 5,000개의 단어가 저장됨.
# 각 주제에서 n개씩 단어 출력
# 토픽 모델링
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.45355), ('charge', 0.2109), ('new', 0.14089), ('court', 0.11144)]
Topic 2: [('man', 0.69418), ('charge', 0.3003), ('court', 0.16799), ('face', 0.11374), ('murder', 0.10701)]
Topic 3: [('new', 0.83671), ('plan', 0.23631), ('say', 0.18335), ('govt', 0.11207), ('council', 0.1097)]
Topic 4: [('say', 0.73696), ('plan', 0.36221), ('govt', 0.1653), ('council', 0.13156), ('fund', 0.07648)]
Topic 5: [('plan', 0.72884), ('council', 0.17292), ('govt', 0.15001), ('urge', 0.09461), ('water', 0.07344)]
Topic 6: [('govt', 0.52766), ('court', 0.28817), ('fund', 0.22782), ('urge', 0.18901), ('face', 0.17731)]
Topic 7: [('charge', 0.5379), ('court', 0.43098), ('face', 0.33173), ('plan', 0.14738), ('murder', 0.12647)]
Topic 8: [('win', 0.71685), ('court', 0.22828), ('crash', 0.2076), ('kill', 0.17477), ('australia', 0.11384)]
Topic 9: [('court', 0.5989), ('accuse', 0.12775), ('kill', 0.12475), ('face', 0.11953), ('crash', 0.1135)]
Topic 10: [('council', 0.77771), ('kill', 0.19497), ('crash', 0.1333), ('report', 0.08672), ('water', 0.05911)]
위키독스
잠재 디리클레 할당. 다른 토픽 모델링. LDA는 문서들이 토픽들의 혼합으로 구성되어 있으며, 토픽들은 확률 분포에 기반하여 단어들을 생성한다고 가정. 그리고 데이터가 주어지면, LDA는 이 가정에 따라 단어들의 분포로부터 문서가 생성되는 과정을 역추적해 문서의 토픽을 찾아낸다.
즉, LDA는 각 토픽의 단어 분포와 각 문서의 토픽 분포를 추정한다.
# 상위 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)
행렬의 크기 : (1054983, 5000)
from sklearn.decomposition import LatentDirichletAllocation
# 토픽 개수 10개
lda_model = LatentDirichletAllocation(n_components=10, learning_method='online', random_state=777, max_iter=1)
lda_model.fit_transform(tf_idf_matrix)
array([[0.0335099 , 0.69841093, 0.0335099 , ..., 0.0335099 , 0.0335099 ,
0.0335099 ],
[0.03365631, 0.03365631, 0.03365631, ..., 0.03365631, 0.03365631,
0.03365631],
[0.0366096 , 0.0366096 , 0.0366096 , ..., 0.67051361, 0.0366096 ,
0.0366096 ],
...,
[0.02914502, 0.02914502, 0.14077174, ..., 0.02914502, 0.02914502,
0.26688721],
[0.02637829, 0.12325014, 0.02638944, ..., 0.21422895, 0.02637829,
0.0996168 ],
[0.03376121, 0.03376055, 0.03376055, ..., 0.03376055, 0.50437083,
0.03376055]])
# LDA로 얻는 행렬 크기
print(lda_model.components_.shape)
(10, 5000)
# 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)
Topic 1: [('new', 6839.37079), ('government', 6344.47105), ('election', 5419.59529), ('adelaide', 4864.1739), ('home', 4048.41226)]
Topic 2: [('say', 7929.44145), ('change', 4193.89405), ('year', 3924.88995), ('live', 3625.10473), ('market', 3541.15301)]
Topic 3: [('australian', 7667.75985), ('south', 4846.36918), ('perth', 4552.99622), ('2016', 3955.96018), ('open', 3771.68108)]
Topic 4: [('trump', 8187.32772), ('school', 3966.83143), ('jail', 3245.22756), ('women', 3029.28982), ('life', 2998.44275)]
Topic 5: [('police', 5589.78951), ('melbourne', 5299.84238), ('warn', 3577.31091), ('rural', 3521.5736), ('hospital', 3106.7779)]
Topic 6: [('world', 4536.54893), ('sydney', 4406.58731), ('country', 4167.71984), ('years', 3581.99631), ('man', 3520.89397)]
Topic 7: [('charge', 5946.75892), ('day', 5062.31785), ('house', 4481.76928), ('murder', 4065.57534), ('crash', 3793.50261)]
Topic 8: [('australia', 7253.84683), ('attack', 4787.62503), ('north', 3706.11788), ('state', 3658.17043), ('west', 2890.04311)]
Topic 9: [('kill', 4093.71098), ('die', 4025.52754), ('woman', 3983.65518), ('death', 3465.80304), ('drug', 3050.84164)]
Topic 10: [('queensland', 5552.50636), ('canberra', 4323.24), ('win', 4315.15603), ('coast', 3825.82182), ('tasmanian', 3552.17616)]
비지도 학습 토크나이저로 있는 형태소 분석기
# 영어 토큰화 슬쩍 해보자
# 띄어쓰기만 해도 잘 된다
en_text = "The dog ran back to the corner near the spare bedrooms"
print(en_text.split())
['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))
['사과', '의', '놀라운', '효능', '이라는', '글', '을', '봤어', '.', '그래서', '오늘', '사과', '를', '먹으려고', '했는데', '사과', '가', '썩어서', '슈퍼', '에', '가서', '사과', '랑', '오렌지', '사', '왔어']
Okt로 했을 땐 그나마 잘 분리된다. 하지만 등록된 단어가 아닌 새로운 단어를 인식하기는 어렵다.
print(tokenizer.morphs('모두의연구소에서 자연어 처리를 공부하는 건 정말 즐거워'))
['모두', '의', '연구소', '에서', '자연어', '처리', '를', '공부', '하는', '건', '정말', '즐거워']
'모두의연구소'는 하나의 단어이나, 전부 분리되어버렸다!
이것을 해결하기 위해, 텍스트 데이터에서 특정 문자 시퀀스가 함께 자주 등장하는 빈도가 높고, 앞뒤로 조사 또는 완전히 다른 단어가 등장하는 것을 고려해서 해당 문자 시퀀스를 형태소라고 판단하는 형태소 분석기가 있는데, 이것이 soynlp
!
예를 들어, '모두의연구소'라는 문자열이 자주 연결되어 등장한다면 형태소라고 판단하고, '모두의연구소'라는 단어 앞, 뒤에 '최고', 'AI', '실력'과 같은 독립된 다른 단어들이 계속해서 등장한다면 '모두의연구소'를 형태소로 파악하는 식
soynlp는 품사 태깅, 형태소 분석 등을 지원하는 한국어 형태소 분석기
비지도 학습으로 형태소 분석을 하며, 데이터에 자주 등장하는 단어들을 형태소로 분석한다.
내부적으로 단어 점수표로 동작하며, 이 점수는 응집 확률(cohesion probability) 과 브랜칭 엔트로피(branching entropy)으로 활용한다.
# 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)
('/aiffel/aiffel/topic_modelling/data/2016-10-20.txt',
<http.client.HTTPMessage at 0x7f31d66c50d0>)
from soynlp import DoublespaceLineCorpus
# 말뭉치에 대해서 다수의 문서로 분리
corpus = DoublespaceLineCorpus(txt_filename)
len(corpus)
30091
# 상위 3개만 출력
i = 0
for document in corpus:
if len(document) > 0:
print(document)
i = i+1
if i == 3:
break
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 연합뉴스자료사진
soynlp는 비지도학습 형태소 분석기이기에, 기존의 형태소 분석기와는 달리 학습 과정이 필요하다.
내부적으로는, 전체 코퍼스로부터 응집 확률과 브랜칭 엔트로피 단어 점수표를 만드는 과정!
# WordExtractor.extract()를 통해서 전체 코퍼스에 대해 단어 점수표를 계산
from soynlp.word import WordExtractor
word_extractor = WordExtractor()
word_extractor.train(corpus)
word_score_table = word_extractor.extract()
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
내부 문자열(substring)이 얼마나 응집하여 자주 등장하는지를 판단하는 척도다. 응집 확률을 문자열을 문자 단위로 분리하여 내부 문자열을 만드는 광정에서, 왼쪽부터 순서대로 문자를 추가하면서 각 문자열이 주어졌을 때 그다음 문자가 나올 확률을 계산하여 누적 곱을 한 값이다. 이 값이 높을수록 전체 코퍼스에서 이 문자열 시퀀스는 하나의 단어로 등장할 가능성이 높다.
print("반포한 : ",word_score_table["반포한"].cohesion_forward)
print("반포한강 : ",word_score_table["반포한강"].cohesion_forward)
print("반포한강공 : ",word_score_table["반포한강공"].cohesion_forward)
print("반포한강공원 : ",word_score_table["반포한강공원"].cohesion_forward, "< 가장 높음")
print("반포한강공원에 : ",word_score_table["반포한강공원에"].cohesion_forward)
반포한 : 0.08838002913645132
반포한강 : 0.19841268168224552
반포한강공 : 0.2972877884078849
반포한강공원 : 0.37891487632839754 < 가장 높음
반포한강공원에 : 0.33492963377557666
브랜칭 엔트로피(Branching Entropy) 는 확률 분포의 엔트로피값을 사용한다.
주어진 문자열에서 다음 문자가 등장할 수 있는 가능성을 판단하는 척도!
print("디스 : ", word_score_table["디스"].right_branching_entropy)
print("디스플 : ", word_score_table["디스플"].right_branching_entropy)
print("디스플레 : ", word_score_table["디스플레"].right_branching_entropy)
print("디스플레이 : ", word_score_table["디스플레이"].right_branching_entropy, "< 조사나 다른 단어 등장 확률 증가")
# 브랜칭 엔트로피 증가
디스 : 1.6371694761537934
디스플 : -0.0
디스플레 : -0.0
디스플레이 : 3.1400392861792916 < 조사나 다른 단어 등장 확률 증가
띄어쓰기 단위로 잘 나뉜 문장은 L 토크나이저(LTokenizer)를 사용하면 좋다.
한국어는 띄어쓰기 단위로 나눈 어절 토큰이 주로 L 토큰 + R 토큰의 형식을 가질 때가 많다.
ex) '공원에' -> '공원' + '에'
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)
[('국제사회', '와'), ('우리', '의'), ('노력', '들로'), ('범죄', '를'), ('척결', '하자')]
최대 점수 토크나이저(MaxScoreTokenizer)는 띄어쓰기가 되어 있지 않은 문장에서 점수가 높은 글자 시퀀스를 순차적으로 찾아내는 토크나이저
from soynlp.tokenizer import MaxScoreTokenizer
maxscore_tokenizer = MaxScoreTokenizer(scores=scores) # 응집 확률
maxscore_tokenizer.tokenize("국제사회와우리의노력들로범죄를척결하자")
['국제사회', '와', '우리', '의', '노력', '들로', '범죄', '를', '척결', '하자']