본문 바로가기

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

2장 : 자연어와 단어의 분산 표현

2.1 자연어 처리란

Natural Language Processing(NLP): 우리의 말을 컴퓨터에게 이해시키기 위한 기술 

 

2.1.1 단어의 의미

우리의 말은 '문자'로 구성, 말의 의미는 '단어'로 구성 

단어 : 의미의 최소 단위


2.2 시소러스

: (기본적으로는) 유의어 사전

자연어 처리에 이용되는 시소러스에서는 단어 사이의 '상위와 하위' 혹은 '전체와 부분' 등 더 세세한 관계까지 정의

 

2.2.1 WordNet

: 자연어 처리 분야에서 가장 유명한 시소러스 

  • 유의어를 얻거나 단어 네트워크 이용 가능 
  • 단어 네크워크를 사용해 단어 사이의 유사도 구할 수 있음

 

2.2.2 시소러스의 문제점

  • 사람이 수작업으로 레이블링하는 방식
    • 결점

1) 시대 변화에 대응하기 어렵다.

2) 사람을 쓰는 비용은 크다.

3) 단어의 미묘한 차이를 표현할 수 없다. 
-> 통계 기반 기법추론 기반 기법을 사용하여 텍스트 데이터로부터 단어의 의미 자동으로 추출


2.3 통계 기반 기법

말뭉치 corpus : 대량의 텍스트 데이터, 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터 

 

2.3.1 파이썬으로 말뭉치 전처리하기 

전처리 preprocessing : 텍스트 데이터를 단어로 분할하고 그 분할된 단어들을 단어 ID목록으로 변환하는 일

##코퍼스 전처리 함수 
def preprocess(text):
    text = text.lower()
    text = text.replace('.', ' .')
    words = text.split(' ')  #공백기준으로 분할

    word_to_id = {}  #단어에서 단어ID로의 변환
    id_to_word = {}  #단어ID에서 단어로의 변환
    for word in words:
        if word not in word_to_id:  #새로운 ID와 단어 추가 
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word

    corpus = np.array([word_to_id[w] for w in words])

    return corpus, word_to_id, id_to_word
text = 'You say goodbye and I say hello'

corpus, word_to_id, id_to_word = preprocess(text)
print(corpus)  #단어 ID 목록 
print(word_to_id)
print(id_to_word)

-> 결과 

[0 1 2 3 4 1 5]
{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5}
{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello'}
 
 

2.3.2 단어의 분산 표현

: 단어의 의미를 정확하게 파악할 수 있는 벡터 표현 

  • 단어를 고정 길이의 밀집벡터로 표현 
  • 밀집벡터 : 원소가 0이 아닌 실수인 벡터 
  • 예) 3차원 분산 표현 : [0.21, -0.45, 0.83]

 

2.3.3 분포 가설

: 단어의 의미는 주변 단어에 의해 형성된다

  • 단어 자체에는 의미가 없고, 그 단어가 사용된 맥락이 의미를 형성
  • 맥락 : 주목하는 단어 주변에 놓인 단어 
  • 맥락의 크기 : 윈도우 크기 (window size)

 

 

2.3.4 동시발생 행렬

통계 기반 기법 : 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지를 세어 집계하는 방법

-> 동시발생 행렬  

-> 각 단어의 벡터 표현 

##말뭉치로부터 동시발생 행렬 만들어주는 함수
def create_co_matrix(corpus, vocab_size, window_size=1):
    corpus_size = len(corpus)
    co_matrix = np.zeros((vocab_size, vocab_size), dtype = np.int32)

    for idx, word_id in enumerate(corpus):  #인덱스와 word_id(corpus)로 이루어진 튜플 생성
        for i in range(1, window_size+1):
            left_idx = idx-i
            right_idx = idx +i

            if left_idx >=0:
                left_word_id = corpus[left_idx]
                co_matrix[word_id, left_word_id] +=1

            if right_idx < corpus_size:
                right_word_id = corpus[right_idx]
                co_matrix[word_id, right_word_id]+=1

    return co_matrix

 

2.3.5 벡터 간 유사도

  • 유사도를 구하는 방법은 벡터의 내적이나 유클리드 거리 등 다양하지만 코사인 유사도를 자주 이용
  • 코사인 유사도 : 두 벡터가 가리키는 방향이 얼마나 비슷한가 (완전히 같으면 1, 반대이면 -1)

  • 분자에는 벡터의 내적
  • 분모에는 각 벡터의 노름 norm 
  • 노름 : 벡터의 크기 (L2노름 : 벡터의 각 원소를 제곱해 더한 후 다시 제곱근을 구해 계산)

-> 벡터를 정규화하고 내적을 구하는 것이 핵심

##코사인 유사도 함수
def cos_similarity(x,y, eps=1e-8):  #0으로 나누는 오류 막기위해 
    nx = x / np.sqrt(np.sum(x**2)+eps)  #x의 정규화
    ny = y/ np.sqrt(np.sum(y**2)+eps)  #y의 정규화
    return np.dot(nx, ny)
##you와 I의 유사도 구하는 코드
from common.util import preprocess, create_co_matrix, cos_similarity
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)

c0 = C[word_to_id['you']]  #you의 단어 벡터
c1 = C[word_to_id['i']]  #i의 단어 벡터
print(cos_similarity(c0, c1))

-> 0.7071067691154799

 

2.3.6 유사 단어의 랭킹 표시

: 어떤 단어가 검색어로 주어지면 그 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수 

##유사 단어의 랭킹 표시
def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):  #query :검색어(단어)
    #1. 검색어를 꺼낸다
    if query not in word_to_id:
        print('%s(을)를 찾을 수 없습니다.' %query)
        return

    print('\n[query] '+query)
    query_id = word_to_id[query]
    query_vec = word_matrix[query_id]

    #2. 코사인 유사도 계산
    vocab_size = len(id_to_word)
    similarity = np.zeros(vocab_size)
    for i in range(vocab_size):
        similarity[i] = cos_similarity(word_matrix[i], query_vec)

    #3. 코사인 유사도를 기준으로 내림차순으로 출력 
    count = 0
    for i in (-1*similarity).argsort():  
        #argsort() : 넘파이 배열의 원소를 오름차순으로 정렬 후 배열의 인덱스 반환
        if id_to_word[i] == query:
            continue
        print(' %s: %s' % (id_to_word[i], similarity[i]))

        count +=1
        if count >= top:
            return
##you를 검색어로 유사 단어 출력
from common.util import preprocess, create_co_matrix, most_similar

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)

most_similar('you', word_to_id, id_to_word, C, top=5)

-> [query] you

goodbye: 0.7071067691154799

i: 0.7071067691154799

hello: 0.7071067691154799

say: 0.0

and: 0.0


2.4 통계 기반 기법 개선하기

2.4.1 상호정보량

동시발생 행렬의 원소 - 두 단어가 동시에 발생한 횟수 나타냄
-> 좋은 특징이 아님 
예) the와 car, 단순히 등장 횟수만을 본다면 car는 drive보다 the와 관련성이 강하다고 나옴

=> 점별 상호정보량 pointwise mutual information (PMI) 척도 사용

  • P(x) : x가 일어날 확률
  • P(y) : y가 일어날 확률
  • P(x, y) : x와 y가 동시에 일어날 확률 

-> PMI 값이 높을수록 관련성이 높음

  • 동시발생 행렬로부터 PMI 구하는 식
  • N : 말뭉치에 포함된 단어 수 
  • 단어가 단독으로 출현하는 횟수가 분모에 고려되기 때문에 the의 PMI점수는 낮아짐 
  • 두 단어의 동시발생 횟수가 0이면 log20 = -무한이 되기 때문에 양의 상호정보량 (PPMI) 사용

  • PMI가 음수일 때는 0으로 취급
  • 단어 사이의 관련성을 0 이상의 실수로 나타낼 수 있음
##양의 상호정보량 PPMI 
#동시발생 행렬을 PPMI 행렬로 변환하는 함수
def ppmi(C, verbose=False, eps = 1e-8):
    M = np.zeros_like(C, dtypes=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)
            #음의 무한대로 빠지지 않기 위해 eps 더해줌
            M[i,j] = max(0, pmi)  #음수일 때는 0으로 취급

            if verbose:  #진행상황 출력
                cnt +=1
                if cnt % (total//100+1) ==0:
                    print('%.1f%% 완료' % (100*cnt/total))

    return M
##변환해보기
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, vocab_size)
W = ppmi(C)  #각 원소는 0이상의 실수

np.set_printoptions(precision=3)  #유효 자릿수를 세 자리로 표시
print('동시발생 행렬')
print(C)
print('-'*50)
print('PPMI')
print(W)

-> 

동시발생 행렬
[[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.   ]]

 

  • 말뭉치의 어휘 수가 증가함에 따라 각 단어 벡터의 차원 수도 증가한다는 문제 
  • 원소 대부분이 0 -> 각 원소의 중요도가 낮음
  • 노이즈에 약하고 견고하지 못하다는 약점
    -> 벡터의 차원 감소

 

2.4.2 차원 감소

dimentionality reduntion : 문자 그대로 벡터의 차원을 줄이는 방법

  • 중요한 정보는 유지하면서 줄이는 게 핵심
  • 데이터의 분포를 고려해 중요한 축을 찾는 일을 수행
  • 원소 대부분이 0인 행렬 : 희소행렬 
  • 차원 감소의 핵심은 희소벡터에서 중요한 축을 찾아내어 더 적은 차원으로 다시 표현하는 것
  • 차원 감소의 결과로 원래의 희소벡터는 대부분이 0이 아닌 값으로 구성된 밀집벡터로 변환

차원을 감소시키는 방법은 여러가지, 우리는 특잇값분해 singular value decomosition (SVD) 이용

  • 임의의 행렬 x를 U, S, V라는 세 행렬의 곱으로 분해
  • U, V는 직교행렬 (orthogonal matrix) : 열벡터는 서로 직교
  • S는 대각행렬 (diagonal matrix) : 대각성분 외에는 모두 0인 행렬

  • S의 대각성분에는 특잇값이 큰 순서로 나열 
  • 특잇값 : 해당 축의 중요도  -> 중요도가 낮은 원소를 깎아냄

  • 행렬S에서 특잇값이 작다면 행렬 U에서 여분의 열벡터를 깎아내어 원래의 행렬을 근사
    -> 행렬 X의 각 행에는 해당 단어 ID의 단어 벡터가 저장 -> 단어 벡터가 행렬 U라는 차원 감소된 벡터로 표현 

 

2.4.3 SVD에 의한 차원 감소

##SVD에 의한 차원 감소
import matplotlib.pyplot as plt

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)

#SVD - 넘파이의 linalg모듈이 제공하는 svd메소드 사용 
U, S, V = np.linalg.svd(W)
print(C[0])  #동시발생 행렬
print(W[0])  #PPMI 행렬
print(U[0])  #SVD
print(U[0, :2])  #각 단어를 2차원 벡터로 줄임

-> 희소벡터인 W가 밀집벡터U로 변함

[0 1 0 0 0 0 0]
[0.    1.807 0.    0.    0.    0.    0.   ]
[ 3.409e-01 -1.110e-16 -1.205e-01 -4.163e-16 -9.323e-01 -1.110e-16
 -2.426e-17]
[ 3.409e-01 -1.110e-16]

 

2.4.4 PTB 데이터셋

: 펜 트리뱅크 penn Treddbank (PTB) 말뭉치

  • 주어진 기법의 품질을 측정하는 벤치마크로 자주 이용
  • 이 책에서는 각 문장을 연결한 하나의 큰 시계열 데이터로 취급할 것
  • 각 문장 끝에 <eos>라는 특수 문자 삽입
##PTB데이터셋
import sys
sys.path.append('..')
from dataset import ptb

corpus, word_to_id, id_to_word = ptb.load_data('train')
#데이터 읽기, train/test/valid 중 하나 지정 가능

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'])

-> 

말뭉치 크기 : 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

 

2.4.4 PTB 데이터셋 평가

  • 큰 행렬에 SVD 적용해야 하므로 고속 SVD 이용
  • sklearn 모듈
##PTB 데이터셋 평가

window_size = 2
wordvec_size = 100

corpus, word_to_id, id_to_word = ptb.load_data('train')
#데이터 읽기, train/test/valid 중 하나 지정 가능
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:
    # truncated SVD
    from sklearn.utils.extmath import randomized_svd  
    #sklearn의 고속SVD, 무작위 수를 사용, 특잇값이 큰 것들만 계산하여 기본적인 SVD보다 훨씬 빠름
    U, S, V = randomized_svd(W, n_components=wordvec_size, n_iter=5,
                             random_state=None)
except:
    # SVD
    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)

-> 

[query] you
 i: 0.6880272626876831
 do: 0.6079217195510864
 we: 0.5786764025688171
 anybody: 0.5776432752609253
 else: 0.5051592588424683

[query] year
 earlier: 0.6934074759483337
 month: 0.6774067282676697
 last: 0.636110246181488
 next: 0.6313115954399109
 quarter: 0.608620822429657

[query] car
 auto: 0.717846691608429
 luxury: 0.6236147284507751
 truck: 0.5415768027305603
 corsica: 0.5339930057525635
 vehicle: 0.5269615054130554

[query] toyota
 motor: 0.7085002660751343
 honda: 0.6650682687759399
 nissan: 0.6501489877700806
 motors: 0.6489919424057007
 lexus: 0.5389389991760254

결과 매번 다름 (Truncated SVD는 무작위 수를 사용하기 때문)


2.5 정리

  • WordNet 등의 시소러스를 이용하면 유의어를 얻거나 단어 사이의 유사도를 측정하는 등 유용한 작업을 할 수 있다.
  • 시소러스 기반 기법은 시소러스를 작성하는 데 엄청난 인적 자원이 든다거나 새로운 단어에 대응하기 어렵다는 문제가 있다.
  • 현재는 말뭉치를 이용해 단어를 벡터화하는 방식이 주로 쓰인다.
  • 최근의 단어 벡터화 기법들은 대부분 '단어의 의미는 주변 단어에 의해 형성된다'는 분포 가설에 기초한다.
  • 통계 기반 기법은 말뭉치 안의 각 단어에 대해서 그 단어의 주변 단어의 빈도를 집계한다 (동시발생 행렬).
  • 동시발생 행렬을 PPMI행렬로 변환하고 다시 차원을 감소시킴으로써, 거대한 '희소벡터'를 작은 '밀집벡터'로 변환할 수 있다.
  • 단어의 벡터 공간에서는 의미가 가까운 단어는 그 거리도 가까울 것으로 기대된다. 

책 참고 : 밑바닥부터 시작하는 딥러닝 (한빛미디어)

 

 

 

 

 

 

 

 

 

'딥러닝 > 밑바닥부터 시작하는 딥러닝 2' 카테고리의 다른 글

6장 : 게이트가 추가된 RNN  (0) 2023.09.26
5장 : 순환 신경망(RNN)  (0) 2023.09.26
4장 : word2vec 속도 개선  (0) 2023.09.23
3장 : word2vec  (0) 2023.09.21
1장 : 신경망 복습  (0) 2023.09.20