NIRVANA

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

AI

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

녜잉 2024. 3. 16. 00:05

 

시소러스

  • 유의어 사전, 뜻이 같은 단어 혹은 뜻이 비슷한 단어(유의어)가 한 그룹으로 분류되어 있는 것을 의미함 
  • 자연어 처리에 이용되는 시소러스에는 단어 사의 '상위와 하위' 혹은 '전체와 부분' 등, 더 세세한 관계까지 정의해둔 경우도 존재
  • 모든 단어에 대한 유의어 집합을 만든 다음, 단어들의 관계를 그래프로 표현하여 단어 사이의 연결을 정의 
    • 단어에 대한 동의어와 계층 구조의 관계 정의 가능 → '단어 네트워크' 생성 
    • '단어 네트워크'를 사용하여 컴퓨터에게 단어 사이의 관계를 가르침 

WordNet

  • 자연어 처리 분야에서 가장 유명한 시소러스, 워드넷을 사용하면 유의어를 얻거나 '단어 네트워크' 사용이 가능함 
    • 단어 네트워크를 사용해 단어 사이의 유사도를 구할 수도 있음 

문제점

  • 시대 변화에 대응하기 어려움 
    • '크라우드펀딩'와 같은 새로운 단어가 생성되고, 사용되지 않은 단어가 발생하지만 시소러스 방안은 이런 시대 변화에 대응하기 어려움 
  • 사람을 쓰는 비용은 크다
  • 단어의 미묘한 차이를 표현할 수 없다 
    • 빈티지와 레트로는 의미는 같지만 용법은 다름, 그러나 시소러스에서는 이런 미묘한 차이를 표현하기 어려움 

 

통계 기반 기법

  •  말뭉치에서 자동으로, 그리고 효율적으로 핵심을 추출하는 방법 
    • 말뭉치: 자연어 처리 연구나 애플리케이션을 염두에 두고 수집된 텍스트 데이터, 사람의 지식이 담긴 데이터 

 

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

text = 'You say goodbye and I say hello.'


text = text.lower() #문장 내 모든 대문자를 소문자로 변경 
text = text.replace('.', ' .')

words = text.split(' ') #공백을 기준으로 나눔

#단어를 ID의 리스트로 사용할 수 있도록 변경
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 

print(id_to_word)
print(word_to_id)

#단어 목록을 단어 ID 목록으로 변경 
import numpy as np

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

 

분산 표현 

  • 단어의 의미를 정확하게 파악할 수 있는 벡터 표현
  • 단어의 분산 표현은 단어를 고정 길이의 밀집벡터로 표현함 
    • 밀집벡터: 대부분의 원소가 0이 아닌 실수인 벡터를 의미 

분포 가설 

  • 단어의 의미는 주변 단어에 의해 형성됨
  • 단어 자체에는 의미가 없고, 그 단어가 사용된 '맥락(context)'이 의미를 형성함 
    • ex) "I drink beer",  "we drink wine" → drink 주변에 음료가 등장함을 알 수 있음
    • ex) "I guzzle beer", "we guzzle wine" → "guzzle"과 "drink"가 같은 맥락에서 사용됨을 알 수 있음 + 가까운 의미의 단어임을 알 수 있음 

맥락

  • 주변에 놓인 단어, 특정 단어를 중심에 둔 '주변 단어' 를 의미 
  • 윈도우 크기(window size): 맥락의 크기(주변 단어를 몇 개나 포함할지)를 결정 
    • 윈도우 크기가 2이면 좌우 두 단어씩 맥락에 포함, 윈도우 크기가 1이면 좌우 한 단어씩 맥락에 포함 

 

동시발생 행렬

통계기반 기법

  • 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는 지를 세어 집계하는 방법
import sys
sys.path.append('..')
import numpy as np
#from common.util import proccess

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

print(corpus)
#[0 1 2 3 4 1 5 6]

print(id_to_word)
#{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}

 

단어의 개수가 7개 임을 알 수 있음

단어의 맥락에 해당하는 단어의 빈도를 count → 윈도우 크기는 1, 단어 ID가 0인 "you"부터 시작 

 

" You say goodbye and I say hello ."

 

단어 you의 맥락은 say 하나 뿐이게 됨 → 이를 [0 1 0 0 0 0 0 0] 인 벡터로 표현 가능 

 

다른 단어들 역시 같은 방식으로 벡터 표현 가능

 

동시발생 행렬 기반 단어 벡터화

#동시발생 행렬 생성 
C = np.array([ 
    [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, 0, 0, 0, 1], 
    [0, 0, 0, 0, 0, 1, 0]
  
], dtype=np.int32)

#단어의 벡터 얻기

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

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

print(C[word_to_id['goodbye']])
#[0 1 0 1 0 0 0]

 

동시 발생 행렬 자동으로 얻기

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) #co_matrix를 0으로 채워진 2차원 배열로 초기화 

#반복문을 통해 말뭉치의 모든 단어 각각에 대해 윈도우에 포함된 주변 단어를 세어 나감 
  for idx, word_id in enumerate(corpus):
    for i in range(1, window_size+1):
      left_idx = idx - i
      right_idx = idx +1

      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

 

 

벡터 간 유사도

  • 단어 벡터의 유사도를 나타낼 때는 코사인 유사도를 자주 사용함 

#코사인 유사도 파이썬 함수 표현

def cos_similarity(x, y , eps=1e-8): #0d이 나누어지는 상황을 방지하기 위해 엡실론 값을 더해줌 
  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의 유사도 구하기

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

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

C = create_co_matrix(corpus, vocab_size)

c0 = C[word_to_id['you']]
c1 = C[word_to_id['i']]

print(cos_similarity(c0, c1))

 

 

유사 단어의 랭킹 표시 

  • 어떤 단어가 검색어로 주어질 때, 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수 만들기 
#어떤 단어가 검색어로 주어질 때, 검색어와 비슷한 단어를 유사도 순으로 출력하는 함수 만들기 

def most_similar(query, word_to_id, id_to_word, word_matrix, top=5):
  #검색어 꺼내기 

  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]

  #코사인 유사도 계산 
  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)

  #코사인 유사도를 기준, 내림차순 출력
  #argsort: 배열의 원소를 오름차순으로 정렬 후 배열의 인덱스 반환 
  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
import sys
sys.path.append('..')
import numpy as np
#from common.util import proccess, create_co_matrix, cos_similarity

text = 'You say goodbye and I say hello.'
corpus, word_to_id, id_to_word = proccess(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)

 

 

통계 기반 기법 개선하기 

상호정보량 

  • 발생 횟수는 좋은 특징이 될 수 없음 
    • 문장에서 'the'와 'car'의 동시발생을 생각할 때, "...the car"라는 문구가 자주 나오게 되므로 두 단어의 동시발생 횟수는 많음 그러나 car는 dirve와 더 연관성이 높음 → 등장 횟수만 보면 the와 car의 관련성이 높게 나옴
  • 점별 상호정보량(PMI)을 사용, 문제를 해결 
    • PMI 값이 높을 수록 관련성이 높다는 뜻 

 

  • PMI는 두 단어의 동시 발생 횟수가 0일 경우 log_2 0 = -∞ 이 됨 → 양의 상호정보량(PPMI)를 사용

 

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

 

  • PMI가 음수일 경우 0 취급
def ppmi(C, verbose=False, eps=1e-8): #vervose: 진행상황 출력 여부 결정 플래그
  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

 

PPMI 단점

  • 말뭉치의 어휘 수 증가에 따라 단어 벡터의 차원 수도 증가함 
  • (지금까지 기준으로) 벡터의 원소 대부분이 0으로 이루어짐 → 각 원소의 중요도가 낮음 
    • 대부분이 0으로 이루어진 벡터(희소행렬-희소벡터) 의 경우 노이즈에 약하고 견고하지 못함
    • 차원 감소를 통해 문제 해결

 

차원 감소

  • 중요한 정보를 최대한 유지하면서 벡터의 차원을 줄이는 방법
  • 희소벡터에서 중요한 축을 찾아내어 더 작은 차원으로 다시 표현하는 것을 의미  
    • 데이터의 분포를 고려, 중요한 '축'을 찾는 일을 수행 
    • 1차원 값만으로도 데이터의 본질적인 차이를 구별할 수 있는 적합한 축을 찾아야 함 
  • 차원 감소의 결과로 원래의 희소벡터는 원소 대부분이 0이 아닌 값으로 구성된 '밀집 벡터'로 변환됨 

 

특이값 분해(Singular Value Decomposition, SVD)

X = USV^t

  • 임의의 행렬을 세 행렬의 곱을 분해
  • U, V: 직교 행렬, 열벡터가 서로 직교
    • 직교 행렬은 어떠한 공간의을 형성할 수 있으므로, 행렬 U를 단어 공간으로 취급할 수 있음
  • S: 대각 행렬
    • S는 대각행렬 → 대각 성분에는 특잇값이 큰 순서로 나열되어 있음 
  • 특잇값: "해당 축"의 중요한 요소
  • 행렬 S에서 특잇값이 작다면 중요도가 낮다는 뜻이므로, 행렬 U에서 여분의 열벡터를 깎아내어 원래의 행렬을 근사할 수 있음
    • 우리의 문제에 적용해본다면, 행렬  X의 각 행에는 해당 단어 ID의 단어 벡터가 저장되어 있으며, 그 단어 벡터가 행렬 U`라는 차원 감소된 벡터로 표현되는 것 

 

SVD에 의한 차원감소 

import sys
sys.path.append('..')
import numpy as np
import matplotlib.pyplot as plt

#from common.util import proccess, create_co_matrix, cos_similarity

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

C = create_co_matrix(corpus, vocab_size)
W  = ppmi(C)

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

print(C[0])
print(W[0])
print(U[0])
print(U[0, :2])