데이터 한 그릇

자연어와 단어의 분산 표현 본문

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

자연어와 단어의 분산 표현

장사이언스 2022. 1. 13. 18:13

자연어 처리란?

한국어와 영어 등 우리가 사용하는 언어를 자연어(Natural Language) 라고 부름.

 

자연어 처리 : Natural Language Proccessing(NLP)

 

 

단어의 의미

문장안에 여러 단어들이 포함되어 있음.

단어는 의미의 최소 단위.

 

 

(이번장)

 

***"컴퓨터에게 단어의 의미 이해시키기" , 다른 말로 정확히 말하면 "컴퓨터가 단어의 의미를 잘 파악하는 표현 방법"***

 

 

구체적인 이번장과 다음장에서 알아볼 기술

 

 

(컴퓨터에게 단어의 의미를 이해시키는 방법 3가지)

 

 

  1. 시소러스를 활용한 기법(이번)
  2. 통계 기반 기법 (이번)
  3. 추론 기반 기법(word2vec) (다음)

 


시소러스

 

시소러스 방법(표현)을 이용해서 자연어 단어를 컴퓨터가 알아들을 수 있게 표현.

 

 

  1. 시소러스
    • 유의어 사전
    • 기본적으로 뜻이 같은 단어나, 유사한 단어들이 하나의 그룹으로 분류됨.
  2. 위계를 가지는 시소러스
    • object > motor vehicle >(car, go-kart, truck) -> car에서 분파되는 가지 (SUV, compact, hatch-back)

 

 

모든 단어에 대해서 유의어 집합을 만들고, 단어들의 관계를 그래프로 표현하여 단어 사이의 연결을 정의할 수 있다.

 

즉, 단어 네트워크(그래프) 를 통해서 단어간의 연결과 관계를 정의 내릴 수 있다.

 

이를 컴퓨터에게 가르치면 단어의 의미를 이해시켰다고 할 수 있다.

 

wordNet

자연어 처리에서 가장 유명한 시소러스는 wordNet.

 

wordNet을 사용하면 유의어를 얻거나, 단어 네트워크를 이용해 단어간 유사도를 구할 수 있다.

 

 

시소러스의 문제점

  1. 시대 변화에 따라서 단어의 의미가 새로 생기거나 변화한다.
    • 영어의 heavy 는 과거에 무겁다라는 의미만 있었고 현재는 어렵다라는 의미를 표현하기도 한다.
  2. 사람을 쓰는 비용이 크다.
  3. 단어의 미묘한 차이를 표현할 수 없다.
    • 빈티지와 레트로는 의미는 동일하지만 미묘한 표현 차이가 있다. 시소러스는 이를 표현할 수 없다.

시소러스의 이러한 문제점들을 해결하기 위해서 '통계 기반 기법'과 '신경망을 사용한 추론 기반 기법'을 알아볼 것.

  1. 시소러스의 문제점을 타파하기 위한 두 가지 방법
  • 통계 기반 기법
  • 추론 기반 기법(신경망 이용)

이 두 기법은 대량의 텍스트 데이터로부터 "단어의 의미" 를 자동으로 추출한다.

이 덕에 사람이 순수 레이블링 하는 중노동에서 해방.(시소러스 해방)

 


통계 기반 기법

 

 

말뭉치(Corpus) 사용

 

  • 자연어 처리를 목적으로 수집한 대량의 텍스트 데이터
  • 혹은 문서들의 집합이라고 표현

통계 기반 방법은 말뭉치로부터 자연어에 대한 지식을 자동으로 추출한다.

 

(말뭉치는 사람들이 적은 데이터이기 때문에, 자연어에 대한 정보가 자동으로 많이 담겨있다.)

 

 

*말뭉치 ->(추출) -> 자연어 지식 ※자동화*

 

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

자연어 기본적인 전처리 : 텍스트 데이터에서 단어들을 추출하고, 그 단어들 하나하나에 ID를 부여하고 그 값들을 LIST로 변환하는 작업

*Text -> 단어1,단어2,단어3.... -> 단어1_id, 단어2_id, 단어3_id,..... -> [단어2_id, 단어3_id,.....]*

 

#소문자로 전환

text = 'You say goodbye and I say hello'
text = text.lower()

#단어 단위로 나누기

words = text.split(' ')
words

 

단어 단위로 나누어져서 다루기 쉬워진 것은 맞지만 있는 그대로 사용하기란 불편하다.

따라서 각 단어에 ID 를 부여한다.

그리고 ID의 리스트로 이용할 수 있도록 리스트에 그 값들을 담아준다.

 

#단어에 ID 붙이기

#각 단어에 id를 붙이고
#id가 value고 단어가 key 값인 딕셔너리를 만들고
#id가 key고 단어가 value인 딕셔너리를 만든다.

word_to_id = {}
id_to_word = {}

for word in words:
    if word not in word_to_id:
        new_id = len(word_to_id)
        word_to_id[word] = new_id
        id_to_word[new_id] = word
        
        
#word to id 딕셔너리를 array로 전환 (key : word, value는 id인 딕셔너리임)
import numpy as np
corpus = [word_to_id[w] for w in words]
corpus = np.array(corpus)
corpus

 

위의 과정을 한 번에 볼 수 있게 함수화 해보겠다.

 

#text lower
#text split으로 단어만 추출
#단어 하나하나에 id부여하기
#부여한 단어id들을 배열로 담아두기

def preprocessing(text):
    text = text.lower()
    text = text.replace('.',' .')
    words = text.split(' ')
    
    word_to_id = {}
    id_to_word = {}
    
    for word in words:
        if word not in word_to_id:
            new_id = len(word_to_id)
            word_to_id[word] = new_id
            id_to_word[new_id] = word
            
    corpus = [word_to_id[word] for word in words]
    corpus = np.array(corpus)
    
    return corpus, word_to_id, id_to_word

 

 

 

이 함수를 이용해서 한 번에 return 값 받기

 

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = preprocessing(text)
print(corpus)
print(word_to_id)
print(id_to_word)

 

 

분포 가설

단어를 벡터화 시키려는 노력은 계속 있어왔다.

 

중요한 기법의 거의 모두가 단 하나의 `간단한 아이디어` 에 뿌리를 두고 있다.

 

 

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

 

 

이를 `분포 가설(distribution hyphothesis)` 라고 부른다.

 

단어 자체에는 의미가 없고, 그 단어가 사용된 맥락(context)이 의미를 형성한다.

다시 의미를 이해하기 위해서 타이핑하면서 적어보면

단어 그 자체에는 의미가 없으며, 단어가 사용이된 맥락이 단어의 의미를 부여한다.

물론 의미가 같은 단어들은 같은 맥락에서 더 많이 등장.(선 맥락, x에 그 맥락에 따라 같은 단어 똑같이 더 많이 등장)

 

 

ex) I drink coffee, we drink beer

 

 

 

drink 가 사용이 됐는데, drink 단어 주변에는 음료가 등장하기 쉽다.

 

즉, x 빈칸이 있고 그 주변에 액체가 있으면 drink 가 등장할 가능성이 커진다.

그리고

 

 

ex) we guzzle beer, we guzzle wine

 

 

 

똑같은 맥락에서 drink 말고 guzzle 이 사용이 됐는데, 이럴 시 우리는 guzzle 과 drink 가 비슷한 단어라는 걸 알 수 있다.

 

즉, 주변 맥락에 따라서 어떤 단어가 나올 수 있는데, 똑같은 맥락에서 어떤 단어들이복수 사용 가능하면, 그 단어들 끼리는 유사성이 있음을 알 수 있다.

같은 맥락에서 사용된 단어들은 서로 유사성이 있다.

 

 

앞으로 이 책에서는 맥락 이라고 하면 주변 단어들을 일컫는다.

 

 

다시, 맥락이란 특정 단어를 중심으로 둔 주변 단어들.

 

동시발생 행렬

이런 분포 가설을 기초해서 단어를 벡터화 하는 방법을 생각해보자.

(분포가설 : 맥락에 따라서 단어의 의미가 정해진다.)

 

어떤 단어에 집중했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지 세어 집계하는 방법

 

집계해서 행렬을 만들면 이를 `co-occurrence-matrix` 라고 칭한다.

 

 

"You say goodbye and i say hello"

 

 

가 있다고 할 때,

 

You 주변 단어는 say 하나 뿐. 그리고 1 빈도를 기록

say 주변 단어는 you, goodbye, I, hello 이고 you : 1, goodbye : 1, I : 1, hello : 1 빈도 기록

 

#동시행렬 만드는 자동화 함수

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

 

 


 

 

벡터 간 유사도

 

 

위에서 컴퓨터에 단어의 의미를 이해시키는 방법으로 시소러스 방법을 알아봤고, 시소러스 방법이 한계가 있어서 통계 기반 기법에 대해서 살펴봤다. 통계 기반 기법은 분포 가설에 근거하는데, 분포 가설은 단어의 의미가 그 자체로 형성되어 있지 않고 그 주변 맥락에 의해서 의미가 형성된다는 의미다.

 

 

 

그래서 단어간 동시발생 빈도를 카운트해서 동시발생 행렬을 만들었다. 이 과정을 통해서 `단어를 벡터로 만들었다.`

 

그렇다면 단어간에 유사도를 측정하기 위해 벡터간의 유사도를 측정해보자.

 

 

주로 "코사인 유사도" 사용해서 벡터간 유사도를 측정한다.

 

(벡터로 만들어진 단어, 그 벡터간의 유사도 측정)

 

 

만일 벡터 x = (x1, x2, x3, x4, x5, x6, x7, x8, x9, x10),

 

y = (y1,y2,y3,y4,y5,y6,y7,y8,y9,y10) 가 있다고 가정했을 때, `similarity(x,y) =` 공식은 다음과 같다.

두 벡터의 내적이 분자가 되고(각 원소 위치에 맞게 곱하고 그 값들을 더하기)

각 벡터 별로 원소 각각에 제곱을 하고 더한 이후에 루트를 씌우고 그 값들을 곱한 값이 분모가 된다.

이를 노름(norm) 이라고 부른다. 노름은 벡터의 크기를 나타낸다. 여기선 L2 노름을 계산한다.

 

 

L2노름(벡터의 크기) : 벡터의 각 원소를 제곱하고 더한 후 제곱근을 구한 값

 

 

 

결국, (벡터의 내적) / (x벡터의 크기)(y벡터의 크기)

 

코사인 유사도를 직관적으로 풀면, 두 벡터가 향하는 방향이 얼마나 비슷한가다.

두 벡터의 방향이 완전히 같다면 코사인 유사도가 1이 되며 완전히 반대라면 -1

 

import numpy as np

def cos_similarity(x,y, eps =1e-8):
    nx = x / np.sqrt(np.sum(x**2) + eps)
    ny = y / np.sqrt(np.sum(y**2) + eps)
    return np.dot(nx,ny)

text = "You say goodbye and I say hello"
corpus, word_to_id, id_to_word = preprocessing(text)
print(corpus)
print(word_to_id)
print(id_to_word
     )
vocab_size = len(word_to_id)
print(vocab_size)
C = create_co_matrix(corpus, vocab_size)
print(C[0])
print(C[1])
c_you = C[word_to_id['you']]
c_i = C[word_to_id['i']]
print(cos_similarity(c_you,c_i))

 

상위 5개의 유사도가 높은 단어 추출

 

#query : 검색단어
#word_matrix : 각 행에는 대응하는 단어들의 벡터들이 저장되어 있다고 가정
#top : 상위 몇 개까지 출력할지 설정

def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
    if query not in word_to_id:
        print("%s(을)를 찾을 수 없습니다.")
        return
    print('\n[query]' + query)
    query_id = word_to_id[query]
    query_vec = word_matrix[query_id]
    
    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) #query_vec 은 입력한 단어(고정)
        
    count = 0
    for i in (-1 * similarity).argsort():
        if id_to_word[i] == query:
            continue
        print('%s : %s' % (id_to_word[i], similarity[i]))
        
        count += 1
        
        if count >= top:
            return

 


통계 기반 기법 개선하기

앞장에서는, 단어간 동시발생빈도를 이용해서 동시발생행렬을 만들었다.

이를 통해서 단어를 벡터화 하는 건 성공했다.

상호정보량

두 단어가 동시에 발생한 횟수를 기반으로 단어를 벡터화 하는 건 사실 그리 좋은 기반이 아니다.

the car 라고 많이 표현되기 때문에 the, car 는 관련성이 높다고 나올 것.

car 와 drive 는 확실히 연관이 깊을 것, 하지만 the 와 car가 더 관련성이 높다고 나올 것.

왜냐하면 단순 등장 횟수만을 고려했기 때문에

**즉, car 와 drive가 훨씬 더 관련이 깊어보이지만, the 와 car 가 동시에 등장한 횟수가 더 많기 때문에 더 관련이 깊은 것으로 컴퓨터가 판별할 것**

 

 

`점별 상호정보량(Pointwise Mutal Information)` 척도를 사용할 것. (일명, PMI)

 

 

 

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

 

 

 

`PMI(x,y) = log(P(x,y) / P(x) * P(y))`

 

PMI 값이 높을수록 관련성이 높음을 의미한다. => x, y의 관련성이 높음을 의미한다.

 

 

만일 10000개의 단어로 이루어진 corpus 에서 단어 "the" 가 나온 횟수가 100일 때, P(x) = 100 / 10000,

 

"car" 와 "the" 가 동시에 나온 횟수가 10이면 P(x,y) = 10 / 10000 이다.

 

 

동시발생행렬을 기준으로 위의 식을 재바꿈한다면,

 

CORPUS 내에 출현한 단어의 개수 = N

P(x,y) = C(x,y) / N

P(x) = C(x) / N * C(y) / N

`log2( C(x,y) / N / C(x) / N * C(y) / N)``log2(C(x,y) * N) / C(x) * C(y)` 가 된다. (동시행렬을 기준으로 PMI 계산)

 

 

구체적인 수치 계산하기

 

 

예)

 

 

N = 10000,

(the, car, drive) = 1000,20,10,

C(the, car) = 10,

C(car, drive) = 5

(the, car) pmi 지수 :

`log2(10 * 10000 / 1000 * 20) = log2(100000 / 20000) ~~ 2.32`

2.32 값 도출된다.

 

 

(car, drive) pmi 지수 :

 

7.97

 

 

PMI 지수를 이용하면 car와 drive 가 더 유사성이 깊은걸로 도출된다.

 

본래 동시발생을 기준으로 관련성을 비교하면 the와 car 의 동시발생 빈도가 car, drive 보다 더 많기 때문에 더 관련성이 높다고 판정하지만, PMI 지수를 이용하면 car 와 drive 가 관련성이 더 깊음을 잘 보여준다.

 

 

PMI 도 issue 발생:

 

동시발생 횟수가 0이면 마이너스 무한대로 향한다는 것.

 

 

타파하기 위해서 **양의 상호정보량** 사용.(상호정보량 -> 양의 상호정보량)

 

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

 

 

만일 PMI 지수가 나왔는데 음수가 나오면 0으로 취급한다.

 

이제 두 단어 사이의 관련성을 0이상의 실수로 나타낼 수 있게 됐다.(PPMI 사용)

 

PPMI 행렬을 만들어보자 (x, y 단어의 관련성 수치로 채운)

 

#C = 동시발생행렬
#verbose 진행상활 출력 여부
#log2(0) 이 음의 무한대로 가는 걸 방지하기 위해서 eps 

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 + 1) == 0:
                    print("%.1f%% 완료" % (100 * cnt/total))
                    
    return M


text = 'You say goodbye and I say hello'
corpus, word_to_id, id_to_word = preprocessing(text)
vocab_size = len(word_to_id)

C = create_co_matrix(corpus, vocab_size)

W = ppmi(C)

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

 

차원 감소

PPMI 행렬의 문제

 

 

동시발생행렬의 형식을 이용해서 PPMI를 만들었는데, 문제점은 공간 낭비가 심하다는 점.

 

예를 들어서 어떤 CORPUS 가 단어가 10만개라고 했을 때 10만 * 10만의 행렬이 만들어지는데 이를 분석하기란 현실적이지 않다.

또한, 0 값이 많은데, 이 또한 공간낭비의 주범. 대부분의 원소가 중요하지 않음을 의미한다.

 

 

이런 issue 를 해결하기 위한 방법이 차원 감소 (decompositionality reduction).

 

문자 그대로 벡터의 차원을 줄이는 방법. 단순히 줄이는 게 아니라 중요한 정보를 유지한 상태에서 차원을 축소한다.

 

 

ex) 이차원 좌표 x,y 에 점이 찍혀있다고 가정, ax 방향으로 plot 들이 찍혀있을 때, 1차원 직선으로 좌표에 그림을 그려서 예측할 수 있다.

 

 

특잇값분해

 

 

차원을 감소하는 방식 중 특잇값분해(singular value decomposition, SVD) 사용.

 

특잇값분해는 임의의 행렬을 세 개의 행렬 곱으로 분해한다.

 

 

`X = USV^t`

 

 

 

이 식을 통해서 U,S,V 행렬들로 X행렬을 분해한 걸 알 수 있다.

 

여기서 U와 V는 `직교행렬` 이고, 그 열벡터는 서로 직교한다.

그리고 S는 `대각행렬`.

 

U : 직교행렬

S : 대각행렬

V : 직교행렬

대각행렬의 대각 값들은 특잇값, 이 특잇값은 해당 축의 중요도를 의미한다.

대각행렬의 대각 특잇값을 이용해서 본래 X 행렬이 차원 축소가 된다.

 

text = 'You say goodbye and I say hello'
corpus, word_to_id, id_to_word = preprocessing(text)
vocab_size = len(id_to_word)

C =create_co_matrix(corpus, vocab_size, window_size = 1)
w = ppmi(C)
U,S,V = np.linalg.svd(w)
print('차원축소 전 ppmi 행렬은 여전히 희소행렬 : ',w[0])
print('차원축소 후 행렬을 분해하고 U 의 벡터를 확인하면 밀집행렬 : ',U[0])

print(U[:,0:2])

정리

지금까지 자연어 처리 기초에 대해서 살펴봤다.

자연어 처리란, 우리의 자연어를 컴퓨터가 이해시킬 수 있게 하는 과정을 의미한다.

이번 장에서는 컴퓨터에게 단어의 의미를 이해할 수 있게 단어를 표현하는 방식에 대해서 살펴봤다.

 

먼저 시소러스 방식이 있는데, 이는 단어의 유사어를 위계를 지어서 나타내어 컴퓨터에게 그 의미를 전달하는 방식이다. 시소러스 방식은 수작업이라는 점에서 문제점을 가지고 있다.

 

따라서 그 대안으로 통계 분석 기법이 사용되는데, 이 기법의 기본적인 원리는 하나의 단어의 의미는 그 자체로 형성되지 않고 그 주변 맥락에 의해서 형성이 된다는 것이다.

 

따라서 CORPUS 가 들어오면 그 CORPUS를 단어별로 나누고 동시행렬 C를 만든다.

동시행렬 C를 만들게 되면 단어를 동시발생 빈도기반의 벡터로 표현할 수 있다.

 

그렇다면 단어간 유사도를 계산하기 위해서 코사인 유사도를 사용할 수 있다.

코사인 유사도는 두 벡터간의 유사도를 측정하는 방식인데, 벡터의 방향이 같은지 다른지에 따라서 그 값이 변화한다.

 

그런데 문제가 발생한다. 만일 동시발생빈도기반으로 행렬을 만들고 두 단어의 관련성을 따지면, THE, CAR, DRIVE 셋 단어 중에서 THE와 CAR가 더 관련이 깊은 것으로 컴퓨터가 이해한다. 이는 오직 빈도를 기준으로만 따졌기 때문에 나타난 현상이다.

따라서 그 대안으로 PMI 기법이 나왔고, PMI 기법이 0 미만의 값에 대해서 -무한의 값을 기록하기 때문에 PPMI 기법이 나왔다.

 

상호정보량을 기반으로한 PPMI 행렬의 문제점은 희소행렬이라는 점과 차원이 너무 클 수 있다는 점이다.

따라서 이러한 문제를 해결하기 위해,차원축소를 하여 차원을 줄이게 된다.

 

`동시발생 행렬 -> PPMI -> 차원축소`

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

어텐션(Attention)  (0) 2022.01.25
게이트가 추가된 RNN  (0) 2022.01.21
순환신경망(RNN)  (0) 2022.01.20
word2vec 개선  (4) 2022.01.19
word2vec  (0) 2022.01.18
Comments