혜온의 이것저것

[Chapter 2 자연어와 단어의 분산 표현] 4 통계 기반 개선하기 / 5 정리 본문

Deep Learning/밑바닥부터 시작하는 딥러닝2

[Chapter 2 자연어와 단어의 분산 표현] 4 통계 기반 개선하기 / 5 정리

혜온 :) 2022. 3. 18. 11:09

2.4 통계 기반 개선하기

2.4.1 상호정보량

앞 절에서 본 동시발생 행렬의 원소는 두 단어가 동시에 발생한 횟수를 나타낸다. 그러나 이 발생 횟수라는 것은 사실 그리좋은 특징은 아니다.

예를 들어서 'the'와 'car'의 동시발생을 생각해보면 두 단어의 동시발생 횟수는 아주 많다. 한편 'car'와 'drive'는 확실히 관련이 깊다. 빈도로만 따지만 'drive'보다 'the'가  고빈도 단어라서 'car'와 강한 관령성을 갖는다고 평가된다.

이 문제를 해결하기 위해 점별 상호정보량(PMI)이라는 척도를 사용한다.

 

PMI는 확률변수 x와 y에 대해 다음 식으로 정의된다.

P(x)는 x가 일어날 확률, P(y)는 y가 일어날 확률, P(x,y)는 x와 y가 동시에 일어날 확률을 뜻한다. 이 PMI가 높을수록 관련성이 높다는 의미이다.

이 식을 앞의 자연어 예에 적용하면 P(x)는 단어 x가 말뭉치에 등잘할 확률을 가리킨다.

예를 들면, 10,000개의 단어로 이뤄진 말뭉치에서 'the'가 100번 등장한다면 P("the")=100/10000=0.01이다.

"the"와 "car"가 10번 동시에 발생했다면 P("the","car")=10/10000=0.001이 된다.

 

동시발생 행렬을 사용하여 식을 다시 써보면 다음과 같다.

C는 동시발생 행렬, C(x,y)는 단어 x와 y가 동시발생하는 횟수, C(x), C(y)는 각각 단어 x와 y의 등장 횟수이다. 말뭉치에 포함되는 단어 수는 N이다.

이 식을 통해 동시발생 행렬로부터 PMI를 구할 수 있다.

말뭉치의 단어 수가 10,000이라 하고, "the"와 "car"와 "drive"가 각 1,000번, 20번, 10번 등장했다고 하자. 그리고 "the"와 "car"의 동시발생 수는 10회, "car"와 "drive"의 동시발생 수는 5회라고 가정하자.

동시발생 횟수의 관점에서는 "car"는 "drive"보다 "the"와 관련이 깊다고 나온다.

반면에 PMI관점에서는 "car"는 "the"보다 "drive"와의 관련성이 강해진다.

 

PMI도 한 가지 문제가 있다. 바로 두 단어의 동시발생 횟수가 0이면 -∞가 된다는 점이다. 이 문제를 피하기 위해 실제로 구현할 때는 양의 상호정보량(PPMI)을 사용한다. 음수일 때 0으로 취급하는 식이다.

동시발생 행렬을 PPMI행렬로 변환하는 함수를 구현해보자.

def ppmi(C, verbose=False, eps=1e-8):
    M=np.zeros_like(C, dtype=np.float32)
    N=np.sum(C)
    S=np.sum(C,axis=0)
    total=C.shape[0]*C.shape[1]
    cnt=0
    
    for i in range(C.shape[0]):
        for j in range(C.shape[1]):
            pmi=np.log2(C[i,j]*N/(S[j]*S[i])+eps)
            M[i,j]=max(0,pmi)
            
            if verbose:
                cnt+=1
                if cnt%(total//100)==0:
                    print('%.1f%% 완료' % (100*cnt/total))
   return M

이 코드에서는 np.log2(0)이 음의 무한대가 되는 사태를 피하기 위해 eps라는 작은 값을 사용했다.

 

동시발생 행렬을 PPMI 행렬로 변환해보자.

import sys
sys.path.append('..')
import numpy as np
from common.util import preprocess, create_co_matrix,cos_similarity,ppmi

text='You say goodbye and I say hello.'
corpus, word_to_id, id_to_word=preprocess(text)
vocab_size=len(word_to_id)
C=create_co_matrix(corpus,vocav_size)
W=ppmi(C)

np.set_printoptions(precision=3)
print('동시발생 행렬')
print(C)
print('-'*50)
print('PPMI')
print(W)

PPMI행렬에도 여전히 큰 문제가 있다. 말뭉치의 어휘 수가 증가함에 따가 각 단어 벡터의 차원 수도 증가한다는 문제이다. 말뭉치의 어휘 수가 10만개 라면 그 벡터의 차원 수도 똑같이 10만이 된다. 10만 차원의 벡터를 다룬다는 것은 그다지 현실적이지 않다. 또한 이 행렬의 대부분이 0인 것을 알 수 있다. 각 원소의 중요도가 낮다는 뜻이다. 더구나 이런 벡터는 노이즈에 약하고 견고하지 못하다는 약점이 있다.

이 문제에 대처하고자 자주 수행하는 기법이 바로 벡터의 차원 감소이다.

 

2.4.2 차원 감소

차원감소는 문자 그대로 벡터의 차원을 줄이는 방법을 말한다. 그러나 단순히 줄이기만 하는 게 아니라, '중요한 정보'는 최대한 유지하면서 줄이는게 핵심이다. 아래의 그림처럼 데이터의 분포를 고려해 중요한 축을 찾는 일을 수행한다.

2차원 좌표로 표현된 데이터들을 좌표출 하나만으로 표시했다. 여기서 중요한 것은 가장 적합한 축을 찾아내는 일로, 1차우너 값만으로도 데이터의 본질적인 차이를 구별할 수 있어햐 한다. 이와 같은 작업은 다차원 데이터에 대해서도 수행할 수 있다.

 

차원을 감소시키는 방법은 여러 가지이지만, 여기서는 특잇값분해(SVD)를 이용할 것이다.

행렬X를 직교행렬U, V와 대각행렬S로 분해한다.

우리의 맥락에서는 U행렬을 단어공간으로 취급할 수 있다. 또한 S는 대각행렬로, 그 대각성분에는 특이값이 큰 순서로 나열되어 있다. 특이값이란, 쉽게 말해 해당 축의 중요도라고 간주할 수 있다. 그래서 중요도가 낮은 원소를 깎아내는 방법을 생각할 수 있다.

행렬 S에서 특잇값이 작다면 중요도가 낮다는 뜻이므로, 행렬 U에서 여분의 열벡터를 깎아내어 원래의 행렬을 근사할 수 있다.

단어의 PPMI행렬에 적용해보면, 행렬 X의 각 행에는 해당 단어 ID의 단어 벡터가 저장되어 있으며, 그 단어 벡터가 행렬 U'라는 차원 감소된 벡터로 표현되는 것이다.

 

2.4.2 SVD에 의한 차원 감소

SVD는 넘파이의 linalg모듈이 제공하는 scd 메서드로 실행할 수 있다.

import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_to_matrix, ppmi

text='You say goodbye and I say hello.'
corpus, word_to_id, id_to_word=preprocess(text)
vocab_size=len(id_to_Word)
C=create_co_matrix(corpus,vocab_size,window_size=1)
W=ppmi(C)

#SVD
U,S,V=np.linalg.svd(W)

이 코드에서 SVD에 의해 변환된 밀집벡터 표현은 변수U에 저장된다.

print(C[0])
# [0 1 0 0 0 0 0]

print(W[0])
# [0.	1.807	0.	0.	0.	0.	0.	]

print(U[0])	#SVD
# [3.409e-01	-1.110e-16	-1.205e-01	0.000e+00	-9.323e-01	2.226e-16]

이 결과에서 보듯 원래 희소벡터인 W[0]가 SVD에 의해서 밀집벡터 U[0]로 변했다.

그리고 이 밀집벡터의 차원을 감소시키려면, 예컨대 2차원 벡터로 줄이려면 단순히 처음의 두 원소를 꺼내면 된다.

print(U[0,:2])
# [3.409e-01	-1.110e-16]

각 단어를 2차원 벡터로 표현한 후 그래프로 그리려면 다음 코드를 추가하면 된다.

for word,word_id in word_to_id.items():
    plt.annoate(word,(U[word_id,0],U[word_id,1]))
    
plt.scatter(U[:,0],U[:,1],alpha=0.5)
plt.show()

'goodbye'와 'hello', 'you'와 'i'가 제법 가까이 있음 을 알 수 있다. 우리의 직관과 비슷하다.

하지만 지금 사용한 말뭉치가 아주 작아서 이 결과를 그대로 받아들이기엔 아직 석연치 않다. PTB 데이터셋이라는 더 큰 말뭉치를 사용하여 똑같은 실험을 수행해보자

 

2.4.4 PTB데이터셋

우리가 이용할 PTB말뭉치는 word2vec의 발명자인 토마스 미콜로프의 웹페이지에서 받을 수 있다.

이 PTB말뭉치는 텍스트 파일로 제공되며, 원래의 PTB문장에 몇가지 전처리를 해두었다. 또한 한 문장이 하나의 줄로 저장되어 있다. 이 책에서는 각 문장을 연결한 하나의 큰 시계열 데이터로 취급한다. 

import sys
sys.path.append('..')
from dataset import ptb

corpus, word_to_id, id_to_word=ptb.load_data('train')

print('말뭉치 크기:',len(corpus))
print('corpus[:30]:',corpus[:30])
print()
print('id_to_word[0]:',id_to_word[0])
print('id_to_word[1]:',id_to_word[1])
print('id_to_word[2]:',id_to_word[2])
print()
print("word_to_id['car']:",word_to_id['car'])
print("word_to_id['happy']:",word_to_id['happy'])
print("word_to_id['lexus']:",word_to_id['lexus'])

코드의 실행결과이다

corpus size: 929589
corpus[:30]: [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29]

id_to_word[0]: aer
id_to_word[1]: banknote
id_to_word[2]: berlitz

word_to_id['car']: 3856
word_to_id['happy']: 4428
word_to_id['lexus']: 7426

말뭉치를 다루는 방법은 지금까지와 같다. corpus에는 단어 ID목록이 저장된다. id_to_word는 단어 ID에서 단어로 변환하는 딕셔너리이고, word_to_id는 단어에서 단어ID로 변환하는 딕셔너리이다.

 

2.4.5 PTB 데이터셋 평가

큰 행렬에 SVD를 적용해야 하므로 고속 SVD를 이용하는 것이 좋다. 고속 SVD를 이용하려면 sklearn 모듈을 설치해야 한다.

import sys
sys.path.append('..')
import numpy as np
from common.util import most_similar, create_co_matrix, ppmi
from dataset import ptb

window_size=2
wordvec_size=100

corpus, word_to_id, id_to_word=ptb.load_data('train')
vocab_size=len(word_to_id)
print('동시발생 수 계싼 ...')
C=create_co_matrix(corpus, vocab_size,window_size)
print('PPMI 계산 ...')
W=ppmi(C,verbose=True)

print('SVD 계산 ...')
try:
    from sklearn.utils.extmath import randomized_Sd
    U,S,V=randomized_svd(W,n_components=wordvec_size,n_iter=5,random_state=None)
exept ImportError:
    U,S,V=np.linalg.svd(W)
    
word_vecs=U[:,:wordvec_size]

querys=['you','year','car','toyota']
for query in querys:
    most_similar(query,word_to_id,id_to_word,word_vecs,top=5)

결과를 보면, 우선 'you'라는 검색어에는 인칭대명사인 'i','we'가 상위를 차지했음을 알수 있다.

'year'의 연관어로는 'month','qurter'가, 'car'의 연관어로는 'auto','vehicle'등이 뽑혔다.

'toyota'와 관련된 단어로는 'nissan','honda','lexus'등 자동차 제조업체나 브랜드가 뽑힌 것도 확인할 수 있다.

이처럼 단어의 의미 혹은 문법적인 관점에서 비슷한 단어들이 가까운 벡터로 나타났다.

 

2.5 정리

- WordNet 등의 시소러스를 이용하면 유으어를 얻거나 단어 사이의 유사도를 측정하는 등 유용한 작업을 할 수 있다.

- 시소러스 기반 기법은 시소러스를 작성하는 데 엄청난 인적 자원이 듣다거나 새로운 단어를 대응하기 어렵다는 문제가 있다.

- 현재는 말뭉치를 이용해 단어를 벡터화하는 방식이 주로 쓰인다.

- 최근의 단어 벡터화 기법들은 대부분 '단어의 의미는 주변 단어에 의해 형성된다.'는 분포 가설에 기초한다.

- 통계 기반 기법은 말뭉치 안의 각 단어에 대해서 그 단어의 주변 단어의 빈도를 집계한다(동시발생 행렬).

- 동시바생 행렬을 PPMI 행렬로 변환하고 다시 차원을 감소시킴으로써, 거대한 '희소벡터'를 작은 '밀집벡터'로 변환할 수 있다.

- 단어의 벡터 공간에서는 의미가 가깡누 단어는 그 거리도 가까울 것으로 기대된다.

 

Comments