Deep Learning/from scratch II

[밑바닥부터 시작하는 딥러닝 2] - 자연어와 단어의 분산 표현 II

해파리냉채무침 2024. 2. 13. 19:26

통계 기반 기법 개선

상호정보량

만약 the car drive 라는 단어가 있으면, the와 car 과의 연관성 보다, car과 drive가 더 관련성이 깊다고 생각할 것입니다. 하지만 the가 더 많이 등장한다면, the와의 관련성이 더 높다고 생각할 것이다. 이러한 문제를 해결하기 위해 점별 상호정보량(PMI) 이라는 척도를 사용한다. 

https://velog.io/@a01152a/NLP

P(x)는 x가 일어날 확률, P(y)는 y가 일어날 확률, P(x,y)는 x,y가 동시에 일어날 확률이다. 

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

 

만약 the 가 1000번, car가 20번, drive가 10번 등장했을 때, the와 car의 동시발생은 10회, car과 drive의 동시발생은 5회라고 가정한다.

PMI('the','car') = log2(10*10000)/(1000 *20) ∽2.32

PMI('car','drive') = log2(5*10000)/(20 *10) ∽7.97

 

이렇게 하면 car은 the보다 drive와의 관련성이 더 많아진다. the가 자주 출현했기 때문에 pmi 점수가 낮아진 것이다.

만약 동시발생 횟수가 0이면, log0 = -∞가 되기 때문에 실제 구현시는 양의 상호정보량(PPMI)을 사용함.음수일 때는 0으로 간주한다.

 

PPMI(x,y) = max(0,PMI(x,y))

 

def ppmi(C, verbose=False, eps = 1e-8):  
    M = np.zeros_like(C, dtype=np.float32) #동시발생행렬 
    N = np.sum(C) #C(x)의 합
    S = np.sum(C, axis=0) #C(y)의 합
    total = C.shape[0] * C.shape[1] C(x) * C(y)
    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) #pmi 
            M[i, j] = max(0, pmi) #ppmi

            if verbose:#진행사항 출력여부
                cnt += 1
                if cnt % (total//100 + 1) == 0:
                    print('%.1f%% done' % (100*cnt/total))
    return M

 

C는 동시발생 행렬, verbose는 진행상황 출력여부를 결정한다. verbose=True면, 중간마다 진행 상황을 알려준다.

 

PPMI 행렬 변환 모델 구현

import sys
sys.path.append('..')
import numpy as np
from 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, vocab_size)
W = ppmi(C)

np.set_printoptions(precision=3)  
print('co-occurrence matrix')
print(C)
print('-'*50)
print('PPMI')
print(W)

co-occurrence matrix
[[0 1 0 0 0 0 0]
 [1 0 1 0 1 1 0]
 [0 1 0 1 0 0 0]
 [0 0 1 0 1 0 0]
 [0 1 0 1 0 0 0]
 [0 1 0 0 0 0 1]
 [0 0 0 0 0 1 0]]
--------------------------------------------------
PPMI
[[0.    1.807 0.    0.    0.    0.    0.   ]
 [1.807 0.    0.807 0.    0.807 0.807 0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.    1.807 0.    1.807 0.    0.   ]
 [0.    0.807 0.    1.807 0.    0.    0.   ]
 [0.    0.807 0.    0.    0.    0.    2.807]
 [0.    0.    0.    0.    0.    2.807 0.   ]]

 

ppmi 행렬을 보면 원소 대부분이 0이다. 각 숫자는 원소의 중요도를 의미한다. 말뭉치 어휘 수가 증가함에 따라 각 단어 벡터 차원수도 증가한다. 이러한 벡터의 약점은 노이즈에 약하고 견고하지 못한다. 이를 해결하기 위해 벡터의 차원 감소를 수행한다. 

차원감소

차원감소는 벡터의 차원을 줄이도록 한다. 중요한 정보는 최대한 유지하면서 줄여야 하는데, 데이터 분포를 고려하여 축을 찾아야 한다. 원소 대부분이 0인 희소행렬을 차원을 줄여 원소 대부분이 0이 아닌값으로 밀집벡터로 변환한다.

차원감소의 방법으로 특이값 분해(SVD)를 이용한다.

https://velog.io/@a01152a/NLP

U와 V는 직교행렬이고, 두 열벡터는 서로 직교한다.

U : 공간의 축(기저) 형성

S: 대각행렬, 대각성분 특이값(특이값이란 해당 축의 중요도를 의미)

중요도가 낮은 원소(특이값이 작은 원소)를 깎아내는 방법을 생각할 수 있다.

만약 행렬 S에서 특이값이 작다면 중요도가 낮다는 뜻으로 행렬 U에서의 열벡터를 깎아서 원래의 행렬을 근사할 수 있다.

 

SVD에 의한 차원감소 구현

import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt
from common.util import preprocess, create_co_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)
print(C[0])
print(W[0])
print(U[0])

[0 1 0 0 0 0 0] #동시발생행렬
[0.        1.8073549 0.        0.        0.        0.        0.       ] #ppmi 행렬
[ 3.4094876e-01 -1.1102230e-16 -1.2051624e-01 -3.3306691e-16 #svd
 -9.3232495e-01  0.0000000e+00  1.9584388e-17]

 

다음과 같이 밀집 벡터 변환이 되었다.

 

PTB 데이터셋

본격적인 말뭉치를 이용하기 위해선 PTB를 사용한다. 

PTB 말뭉치는 텍스트 파일로 되어있고, 몇개의 전처리가 되어있다. 희소한 단어에 [unk]라고 되어있거나, 구체적인 숫자를 N으로 대체하였다. 

ptb 코드는 깃허브에서 가져왔다. 

https://github.com/oreilly-japan/deep-learning-from-scratch-2/blob/master/dataset/ptb.py

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


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

print('corpus size:', 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'])

결과는 이미지로 가져왔다.

PTB  데이터셋 평가

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('counting  co-occurrence ...')
C = create_co_matrix(corpus, vocab_size, window_size)
print('calculating PPMI ...')
W = ppmi(C, verbose=True)

print('calculating SVD ...')
try:
    # truncated SVD (fast!)
    from sklearn.utils.extmath import randomized_svd
    U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
                             random_state=None)
except ImportError:
    # SVD (slow)
    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)

해당 코드는 sklearn 모듈을 이용해 고속 svd를 이용하였다. np.linalg.svd를 사용하면 시간이 오래걸린다.

randomized_svd()는 무작위 수를 사용한 truncated SVD로, 특이값이 큰것들만 계산해서 기본 SVD 보다 훨씬 빠르다

결과는 이미지로 가져왔다.

https://velog.io/@cksgh0984/%EB%B0%91%EB%B0%94%EB%8B%A5%EB%B6%80%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%94%A5%EB%9F%AC%EB%8B%9D-2-Chapter-2.-%EC%9E%90%EC%97%B0%EC%96%B4%EC%99%80-%EB%8B%A8%EC%96%B4%EC%9D%98-%EB%B6%84%EC%82%B0-%ED%91%9C%ED%98%84

결과를 보면, 단어의 의미, 문법적인 관점에서 비슷한 단어들이 가까운 벡터로 나타난 것을 알 수 있다.