Word2vec

18 minute read

‘밑바닥부터 시작하는 딥러닝2’ 의 내용을 참조하여 작성하였음.

🐤 NLP의 Embedding

  • 사람의 언어를 기계가 이해할 수 있는 숫자 나열인 “Vector”로 바꾼 결과 또는 과정
  • 단어나 문장을 Vector로 변환하여 Vector Space로 끼워넣는다(“Embed”) => Embedding

🙊Word2vec?

  • 2013년 Google이 발표한 단어 임베딩 모델로, 단어를 벡터로 변환 하는 기법

    • Efficient Estimation of Word Representations in Vector Space

    ​ https://arxiv.org/pdf/1301.3781.pdf

    • Skip-gram과 CBOW라는 두 가지 방법론을 제시
    • Skip-gram; 중심에 있는 단어(target)로 주변 단어(context)를 예측하는 방법
    • CBOW; 주변에 있는 단어(context)들을 가지고 중심에 있는 단어(target)를 맞추는 방법

    • Distributed Representations of Words and Phrases and their Compositionality

      https://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf

      • Skip-gram, CBOW 두 모델을 바탕으로 네거티브 샘플링 등의 학습 최적화 기법을 제안
  • 말뭉치(corpus) -> 형태소분석 -> 100차원으로 학습 -> 단어벡터 사이의 유사도 계산 가능

  • 벡터 공간을 시각화 할 수 있음 (ex. t-SNE)

    source: imgur.com

  • 단어벡터 간 사칙연산으로 단어 유추 평가 가능

  • Word2Vec의 임베딩을 다른 딥러닝의 입력값(Input)으로 사용; Transfer Learning(전이 학습)

3.1 추론 기반 기법과 신경망

  • 단어를 벡터로 표현하는 방법은 분포 가설을 배경으로한 통계 기반 기법추론 기반 기법 두 부류로 나뉨
    • 분포 가설; “단어의 의미는 주변 단어에 의해 형성된다”는 가설 –> 단어의 동시발생 가능성을 얼마나 잘 모델링 하는가에 중요한 연구 포인트
  • 통계 기반 기법의 문제점 -> 극복 대안으로서의 추론 기반 기법, word2vec 전처리를 위한 신경망으로 단어 처리하는 예를 제시
3.1.1. 통계 기반 기법의 문제점
  • 통계 기반 기법
    • 주변 단어의 빈도를 기초로 단어를 표현
    • 말뭉치의 전체 통계(동시발생 행렬, PPMI 등) -> SVD(특이값 분해)를 적용(1회) -> 밀집벡터(Dense vector; 단어의 분산표현)
    • => BUT! 대규모 말뭉치를 다룰 때 문제 발생
  • 추론 기반 기법
    • 신경망에서의 미니배치 학습
      • 신경망이 한번에 미니배치의 학습 샘플씩 반복해서 학습하며 가중치를 갱신
  • https://imgur.com/ItHBS5I.jpg https://imgur.com/1JBytm3.jpg
    • 통계 기반 기법은 배치 학습으로 데이터를 한번에 처리
    • 추론 기반 기법은 미니 배치 학습으로 데이터의 일부를 사용하여 순차적으로 학습
      • 병렬 계산 등이 가능하여 학습 속도를 높일 수 있음
3.1.2 추론 기반 기법 개요

you __ goodbye and I say hello.

  • 주변 단어(Context word)가 있을 때 “ㅡㅡ”로 표현된 중심 단어(Target word)가 무엇인지 추론
  • 추론 문제를 반복하여 풀면서 단어의 출현 패턴을 학습하는 것
  • https://imgur.com/SINTF67.png
    • 맥락 정보를 입력 받아 각 단어의 출현 확률을 출력 -> 말뭉치를 사용해 모델이 올바른 추측을 내놓도록 학습 -> 학습의 결과로 단어의 분산 표현을 얻음
3.1.3. 신경망에서의 단어 처리
  • 단어를 고정 길이의 벡터로 변환 –> one-hot encoding 원-핫 인코딩으로 원-핫 벡터로 변환
    • 원-핫 인코딩?; 정수 인코딩을통해 인덱스를 부여한 후 숫자로 바뀐 단어들을 벡터화한 일종의 희소 표현(Sparse Representation)
    • 가지고 있는 텍스트에 단어의 개수 == 단어집합의 크기 → 단어의 개수가 늘어날 수록 비효율적
    • 단어집합의 크기를 벡터의 차원으로 보고, 표현하려는 True의 인덱스에 1, 아닌 경우에는 0을 부여. (0과 1로만 표현됨)
    • 연산의 편의성, 다양한 모델 적용, 정확도 향상 등의 이유로 사용
    • https://imgur.com/n3vXi7M.png
  • 단어를 벡터로 나타내고 신경망을 구성하는 계층들은 벡터를 처리 –> 단어를 신경망으로 처리할 수 있음 https://imgur.com/AmTrU0S.pnghttps://imgur.com/FuTGgCX.png
    • 완전연결계층(fully connected layer)로 이루어진 신경망, 편향을 사용하지 않는 행렬 곱 연산
    • 각 화살표마다의 가중치가 존재, 입력층 뉴런과의 가중합(weight sum)이 은닉층 뉴런이 됨
import numpy as np

c = np.array([[1, 0, 0, 0, 0 ,0 ,0]])
W = np.random.randn(7, 3)
layer = MatMul(W) # matmul에 가중치 설정
h = layer.forward(c) #순전파 수행
print(h)
  • 코드 실행
    • 완전연결계층의 계산은 행렬 곱으로 수행
    • 미니배치 처리를 고려하여 입력데이터 c는 2차원으로 지정 –> 최초의 차원(0번째 차원)에 각 데이터를 저장한 것
    • c와 W의 행렬곱은 결국 가중치의 행벡터 하나를 뽑아낸 것과 같음; 해당 위치의 행벡터가 추출
    • https://imgur.com/GHQYpax.png

3.2 단순한 word2vec

3.2.1 CBOW 모델의 추론 처리
  • CBOW; continuous bag-of-words

    • context(맥락)으로부터 target(중심)을 추측하는 용도의 신경망
    • context -> one-hot encoding -> 입력값으로 받음
  • 뉴런(모델)의 관점에서 CBOW

    • https://imgur.com/VCAoS4b.png
      • N개의 입력층 ~ 완전연결계층 ~ 은닉층 ~ 완전연결계층 ~ 출력층
      • 여러개의 입력층인 경우 전체의 평균을 구한 값이 은닉층 뉴런으로 흐름
      • 출력층 뉴런은 각 단어의 Score(점수)를 출력 -> 여기에 Softmax function을 적용하여 확률 도출
    • 입력층 다음의 완전연결계층의 가중치가 바로 단어분산표현
      • https://imgur.com/3ShMAM6.png
      • 학습을 진행할수록 맥락에서 출현하는 단어를 잘 추측하는 방향으로 분산표현들이 갱신
      • 단어의 의미를 다차원 공간에 벡터화한 것
    • 은닉층의 뉴런을 입력층의 뉴런수보다 적게하여 희소벡터를 밀집벡터로 만들어주는 것이 핵심
      • 은닉층의 정보(인코딩) –> 출력층의 점수(디코딩)
  • 계층 관점에서의 CBOW

    • image-20191109221901428
    • N개의 MatMul 계층(맥락으로 고려할 단어개수 N) ~~ 완전연결계층(weight sum) *0.5; 가중치 평균 ~~ 은닉층 ~~ MatMul계층~~ 출력층; 최종 score 출력
  • CBOW 모델 추론 과정

    ### CBOW 모델의 추론 처리
      
    # 샘플 맥락 데이터; one-hot encoding한 희소벡터
    c0 = np.array([1, 0, 0, 0, 0, 0, 0])
    c1 = np.array([0, 0, 1, 0, 0, 0, 0])
      
    #가중치 초기화
    W_in = np.random.randn(7, 3)
    W_out = np.random.randn(3, 7)
      
    # MatMul계층 생성; 맥락의 수 만큼 생성함(여기서는 2개)
    in_layer0 = MatMul(W_in) # 입력층은 가중치 W_in을 공유
    in_layer1 = MatMul(W_in)
    out_layer = MatMul(W_out) # 출력층의 MatMul 레이어는 1개만 생성
      
    #순전파
    h0 = in_layer0.forward(c0) # 중간데이터 계산
    h1 = in_layer1.forward(c1)
    h = 0.5 * (h0 + h1) #평균
    s = out_layer.forward(h) # 출력층 통과하여 score 도출
      
    print(s)
    
    • 활성화 함수를 사용치 않는 간단한 구성의 신경망
    • 입력층이 여러개(고려할 맥락의 개수만큼) 존재하며 입력층 간 가중치를 공유
3.2.2 CBOW 모델의 학습
  • 출력층의 score에 softmax function을 적용하여 확률로 변환 -> 확률과 정답 레이블로부터 교차 엔트로피 오차를 구함 -> 손실로 사용하여 학습 —> 올바른 중심단어를 예측할 수 있도록 가중치를 조정
    • 맥락이 주어졌을 때 중앙단어로 어떤 것이 출현하는 지 나타낸 것 ==> 정답에 해당하는 뉴런의 값이 가장 큼
  • 주어지는 학습 말뭉치에 나오는 단어 출현 패턴을 학습하므로 말뭉치에 따라 단어의 분산 표현도 달라짐
  • image-20191109225113344
    • 소프트맥스 계층과 교차 엔트로피 오차를 Softmax with Loss라는 하나의 계층으로 구현
3.2.3 word2vec의 가중치와 분산표현
  • 입력 측의 완전연결계층 가중치의 각 행이 각 단어의 분산표현
  • 출력 측의 완전연결계층 가중치에도 단어의 의미가 인코딩된 벡터가 저장되어있음
  • 출력측 가중치는 분산 표현이 열 방향(수직방향)으로 저장
  • image-20191109225817422
  • 보통 입력 측의 가중치만을 최종 단어의 분산 표현으로서 이용

3.3 학습 데이터 준비

  • “You say goodbye and I say hello”
3.3.1 맥락과 타깃
  • 입력; context(맥락) / 정답 레이블; target(타깃)

    • => 신경망에 맥락을 입력했을 때 타깃이 출현할 확률을 높이도록 학습
  • 맥락과 타깃을 만드는 것을 말뭉치 안의 모든 단어에 대해 수행함

    • page132image61531280.jpg
    • 맥락:타깃은 다 대 일 관계
    • 맥락-타깃으로 이루어진 각 행이 신경망의 입력값으로 사용됨
  • 말뭉치 텍스트를 단어 ID로 변환(고유값 부여)

    • ### 말뭉치 텍스트를 단어 ID로 변환
          
      def preprocess(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 = 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)
      print(id_to_word)
      
    • 맥락==2차원 행렬 -> 0번째 차원에는 각 맥락 데이터가 저장
  • contexts와 target 생성

    • ### 맥락과 타깃을 만드는 함수
          
      def create_contexts_target(corpus, window_size=1):
        # 단어ID의 배열(corpus), 맥락의 윈도우크기(window_size)
            
          # target 계산
          target = corpus[window_size:-window_size]
          contexts = []
              
          # contexts 계산
          for idx in range(window_size, len(corpus)-window_size):
              cs=[]
              for t in range(-window_size, window_size + 1):
                  if t == 0:
                      continue
                  cs.append(corpus[idx + t])
              contexts.append(cs)
          # 맥락과 타깃을 numpy 다차원 배열로 return      
          return np.array(contexts), np.array(target)
      
      contexts, target = create_contexts_target(corpus)
      print(contexts)
      print(target)
      
    • CBOW 모델의 입력값으로 사용
3.3.2 원핫 표현으로 변환
  • https://imgur.com/pfyMo5P.png
    • 맥락과 타깃을 단어ID에서 원핫표현으로 변환
    • 맥락(6,2) –> 원핫 표현; 다차원배열 (6,2,7)
  • 단어 ID목록과 어휘 수를 파라미터로 받아서 원핫인코딩 수행

    • def convert_one_hot(corpus, vocab_size):
          N = corpus.shape[0]
          
          if corpus.ndim == 1:
              one_hot = np.zeros((N, vocab_size), dtype=np.int32)
              for idx, word_id in enumerate(corpus):
                  one_hot[idx, word_id] = 1
          
          elif corpus.ndim == 2:
              C = corpus.shape[1]
              one_hot = np.zeros((N, C, vocab_size), dtype=np.int32)
              for idx_0, word_ids in enumerate(corpus):
                  for idx_1, word_id in enumerate(word_ids):
                      one_hot[idx_0, idx_1, word_id] = 1
          
          return one_hot
      
      ### 원핫 표현으로 변환
          
      text = 'You say goodbye and I say hello.'
      corpus, word_to_id, id_to_word = preprocess(text)
          
      contexts, target = create_contexts_target(corpus, window_size=1)
          
      vocab_size = len(word_to_id)
      target = convert_one_hot(target, vocab_size)
      contexts = convert_one_hot(contexts, vocab_size)
      
      print(contexts)
      print(target)
      

3.4 CBOW 모델 구현

  • SimpleCBOW 구현

    • ### 3.4 CBOW 모델 구현
      class SimpleCBOW:
          def __init__(self, vocab_size, hidden_size):
              V, H = vocab_size, hidden_size
              # 어휘수(vocab_size)와 은닉층의 뉴런수(hidden_size)를 파라미터로 받음
                  
              # 1) 가중치 초기화(각각 작은 무작위값으로 초기화)
              # numpy배열의 데이터타입을 astype('f')로 지정; 32bit 부동소수점수로 초기화 한 것
              W_in = 0.01 * np.random.randn(V, H).astype('f')
              W_out = 0.01 * np.random.randn(H, V).astype('f')
          
              # 2) 계층 생성
              # 맥락에서 사용하는 단어의 수(window_size)만큼 입력층을 쌓아야함
              # 각 입력층은 같은 가중치를 사용
              self.in_layer0 = MatMul(W_in)
              self.in_layer1 = MatMul(W_in)
              self.out_layer = MatMul(W_out)
              self.loss_layer = SoftmaxWithLoss()
          
              # 3) 모든 가중치와 기울기를 리스트에 모음
              layers = [self.in_layer0, self.in_layer1, self.out_layer]
              self.params, self.grads = [], []
              for layer in layers:
                  self.params += layer.params
                  self.grads += layer.grads
          
              # 4) 인스턴스 변수에 단어의 분산표현을 저장
              self.word_vecs = W_in
          
          # 순전파 메소드; 맥락과 타깃을 받아 손실을 반환
          def forward(self, contexts, target):
              #맥락은 3차원 numpy arr; ex) (6,2,7)
              #0번째 차원의 원소 수는 미니배치의 수만큼, 1번째 차원의 원소 수는 맥락의 윈도우크기, 2번째 차원은 원-핫 벡터
              #target의 형상은 2차원; ex) (6,7)
              h0 = self.in_layer0.forward(contexts[:, 0])
              h1 = self.in_layer1.forward(contexts[:, 1])
              h = (h0 + h1) * 0.5
              score = self.out_layer.forward(h)
              loss = self.loss_layer.forward(score, target)
              return loss
          
          # 역전파 메소드
          def backward(self, dout=1):
              # 1에서 시작해서 softmax with loss계층에 입력
              ds = self.loss_layer.backward(dout)
              # ds를 MatMul계층에 입력
              da = self.out_layer.backward(ds)
              da *= 0.5 
              # 'x' 역전파는 순전파의 입력을 서로 바꿔 기울기에 곱함
              # '+'역전파는 기울기를 그대로 통과시킴
              self.in_layer1.backward(da)
              self.in_layer0.backward(da)
              return None
      
  • https://imgur.com/xTAhesh.png

    • 1에서 시작해서 softmax with loss계층에 입력 -> ds -> 출력 MatMul계층으로 입력 -> 곱셈; 순전파 시 입력을 바꿔 기울기에 곱함 -> 덧셈; 기울기를 그대로 통과
3.4.1 학습 코드 구현
  • 학습 데이터를 신경망에 입력 -> 기울기를 구함 -> 가중치 매개변수를 순서대로 갱신

  • import matplotlib.pyplot as plt
      
    window_size = 1
    hidden_size = 5
    batch_size = 3
    max_epoch = 1000
      
    text = 'You say goodbye and I say hello.'
    corpus, word_to_id, id_to_word = preprocess(text)
      
    vocab_size = len(word_to_id)
    contexts, target = create_contexts_target(corpus, window_size)
    target = convert_one_hot(target, vocab_size)
    contexts = convert_one_hot(contexts, vocab_size)
      
    model = SimpleCBOW(vocab_size, hidden_size)
    optimizer = Adam() #Adam으로 매개변수 갱신
    trainer = Trainer(model, optimizer)
      
    trainer.fit(contexts, target, max_epoch, batch_size)
    trainer.plot()
    plt.show()
    
  • 가중치 매개변수 조회

    • # word_vecs; 각 행에 대응하는 단어ID의 분산표현(입력 MatMul 계층의 가중치)이 저장되어있음
      word_vecs = model.word_vecs
      for word_id, word in id_to_word.items():
          print(word, word_vecs[word_id])
      
  • 말뭉치가 작기 때문에 좋은 결과는 X

3.5 word2vec 보충

3.5.1 CBOW 모델과 확률
  • 동시확률 P(A,B); A와 B가 동시에 일어날 확률
  • 사후확률 P(A B); B라는 정보가 주어졌을 때 A가 일어날 확률
  • https://imgur.com/o1gaREB.png
    • 말뭉치를 단어 시퀀스로 표기, t번째 단어에 대해 윈도우 크기가 1인 맥락
  • https://imgur.com/4KQOHth.png
    • Wt-1과 Wt+1이 주어졌을 때 타깃이 Wt가 될 확률
    • CBOW가 모델링 하고 있는 수식
  • https://imgur.com/hhSdKrV.png
    • Wt에 해당하는 원소만 1이고 나머지는 0인 희소벡터이므로 Wt이외의 일이 일어날 경우는 원핫 레이블 요소가 0
    • => 단순히 확률에 음의 로그 가능도 를 취함; log를 취하고 마이너스를 붙임
    • 말뭉치 전체로 확장 했을 때는 전체 합의 평균
    • 이 값을 가능한 작게 만드는 것이 학습의 목적, W(가중치 매개변수)는 단어의 분산표현(밀집벡터)
3.5.2 skip-gram 모델
  • CBOW에서 다루는 맥락과 타깃을 역전시킨 모델
  • https://imgur.com/OgGt0ls
    • Skip-gram은 타깃으로부터 주변의 여러 단어(맥락)을 추측
  • https://imgur.com/PpjpvmO
    • 입력층은 1개, 출력층은 맥락의 수 만큼 존재 –> 각 출력층에서 개별적으로 손실을 구해 합산한 값이 최종 손실값
  • $P(w_{t-1}, w_{t+1} w_t)$
    • $w_t$로부터 $w_{t-1}, w_{t+1}$이 동시에 일어날 확률을 추측–> skip-gram이 모델링하는 수식
  • $P(w_{t-1}, w_{t+1} w_t)=P(w_{t-1} w_t)P(w_{t+1} w_t)$
    • 맥락의 단어 사이에 관련성이 없다고 가정(조건부 독립)하여 분해
    • 교차 엔트로피 오차에 적용하여 손실함수 유도
    • logxy = loge + logy라는 로그의 성질을 활용
    • 맥락별 손실을 구한 다음 모두 더함
  • $L=-\frac{1}{T}\displaystyle \sum_{t=1}^T (logP(w_{t-1} w_t)+logP(w_{t+1} w_t))$
    • 말뭉치 전체로 확장(합산 후 평균)
    • 모델의 맥락 수 만큼 추측하므로 손실함수는 각 맥락에서 구한 손실의 총합
  • 단어 분산 표현의 정밀도, 저빈도 단어나 유추문제 성능면에서 skip-gram 모델의 결과가 더 좋지만 손실을 맥락 수 만큼 구해야하기 때문에 계산 비용이 커 학습 속도가 느림
  • 반면 CBOW모델은 학습 속도가 빠름
3.5.3 통계 기반 vs. 추론 기반
  • 통계기반기법
    • 말뭉치 전체 통계로부터 1회 학습하여 단어의 분산표현을 얻음
    • 새로운 단어가 추가되어 단어의 분산표현을 갱신해야하는 경우 처음부터 계산해야함
    • 단어의 유사성이 인코딩
  • 추론기반기법
    • 말뭉치를 일부분씩 여러번 보면서 학습(미니배치)
    • 새로운 단어가 추가되었을 때 학습하던 가중치를 초기값으로 사용해 재학습 가능 -> 단어의 분산 표현을 효율적으로 갱신
    • 단어의 유사성 및 단어 간 복잡한 패턴 인코딩
  • 두 기법은 성능면에서 큰 차이는 없음
  • Skip-gram과 네거티브 샘플링을 이용한 모델은 말뭉치의 동시발생 행렬에 특수 행렬분해를 적용한 것 –> 통계기반기법과 추론기반기법이 서로 연관 있음
  • GloVe; 통계기반기법을 기반으로 말뭉치 전체 통계정보를 손실함수에 도입, 추론기반기법의 미니배치 학습을 수행

https://imgur.com/zkyMH9I

chapter 4. word2vec 속도 개선


  • SimpleCBOW모델은 어휘 수가 많아지면 계산량도 커져 계산시간이 너무 오래 걸림 -> 속도 개선 필요

    1️⃣ Embedding 계층 도입

    2️⃣ 네거티브 샘플링이라는 손실 함수 도입

    3️⃣ PTB 데이터셋(실용적 크기의 말뭉치)로 학습을 수행해 얻은 단어 분산표현을 평가

4.1 word2vec 개선 (1)

  • https://imgur.com/ub8yRCh
    • 어휘 100만개, 은닉층 뉴런이 100개인 CBOW 모델
    • 입력층 원-핫 표현과 가중치 행렬의 곱 계산에서 병목 발생
      • 원-핫 표현이 희소벡터이므로 어휘 100만개에 따라 100만개의 벡터가 됨 -> 메모리 리소스 🤷‍♀️ -> 곱 연산에서 많은 리소스가 소모됨
      • Embedding 계층 도입으로 해결
    • 은닉층과 가중치 행렬의 곱, softmax계층 계산에서 병목 발생
      • 은닉층*가중치행렬에서도 리소스가 많이 잡히는데 softmax 계층에서 다루는 어휘가 많아짐에 따라 계산량이 증가
      • 네거티브 샘플링을 도입하여 해결
4.1.1 Embedding 계층
  • 입력 측의 연산에서 수행하는 것은 행렬의 특정 행을 추출하는 것
  • => 원-핫 변환 및 MatMul계층의 행렬 곱 연산을 삭제해도 무방
  • 가중치 매개변수로부터 단어ID에 해당하는 행(벡터)를 추출하는 계층으로 대체(Embedding 계층) -> 단어 임베딩(분산표현)을 저장
4.1.2 Embedding 계층 구현
  • ndim==2, numpy arr의 경우 W[i]로 특정행을 추출하는 방법을 이용

    • class Embedding:
          def __init__(self, W):
              self.params = [W]
              self.grads = [np.zeros_like(W)]
              self.idx = None
          
          def forward(self, idx):
              W, = self.params
              self.idx = idx
              out = W[idx] # 단순히 가중치의 특정행만 추출
              return out
          
          def backward(self, dout):
              dW, = self.grads 
              dW[...] = 0 # dW의 형상을 유지한 채 원소를 0으로 덮어씀
              np.add.at(dW, self.idx, dout)# 앞층의 기울기를 가중치 기울기 dW의 특정행에 설정
              return None
      
    • https://imgur.com/hnGYeKF.jpg

    • idx 배열의 원소 중 행번호가 같은 원소가 있는 경우-> 할당이 아닌 더하기가 필요

      • def backward(self, dout):
        		dW, = self.grads
            dW[...] = 0
                  
            for i word_id in enumerate(self.idx):
              	dW[word_id] += dout[i]
            #np.add.at(dW, self.idx, dout) #numpy의 내장메소드; 처리속도 빠름
                  
            return None
        

4.2 word2vec 개선 (2)

4.2.1 은닉층 이후 계산의 문제점
  • 거대한 행렬 계산으로 인한 리소스 부족 문제
    • 은닉층의 뉴런과 가중치 행렬의 곱
    • Softmax 계층의 계산
      • $y_k = \frac{exp(s_k)}{\displaystyle\sum_{i=1}^{1000000} exp(s_i)}$
      • 분모의 값을 얻기 위해 exp계산을 100만번 수행해야하는 구조
  • 가벼운 계산이 필요
4.2.2 다중분류에서 이진분류로
  • 네거티브 샘플링
    • 다중분류(multi-classification)을 이진분류(binary classification)로 근사하는 것
    • https://imgur.com/HWYU0Ht
    • 출력층에 뉴런을 하나만 준비하여 타깃 단어의 score만 구하도록 함
    • 은닉층과 출력 측의 가중치 행렬의 내적(dot-product)는 타깃에 해당하는 단어벡터만 추출 -> 단어벡터와 은닉층 뉴런과의 내적 계산만 수행
  • https://imgur.com/PsnIGNm
    • 각 단어의 고유값의 단어벡터가 열로 저장되어 있고 타깃 단어벡터를 추출해서 은닉층뉴런과의 내적을 구함 -> 최종점수 -> 시그모이드 함수 -> 확률변환
    • => 타깃 단어 하나에 주목해 그 점수만 계산하는 것이 핵심
4.2.3 시그모이드 함수와 교차 엔트로피 오차
  • 시그모이드 함수로 확률로 변환하고 교차 엔트로피 오차로 손실함수 사용

    cf) 다중 분류는 소프트맥스 함수 + 교차 엔트로피 오차

    ​ 이진 분류는 시그모이드 함수 + 교차 엔트로피 오차

  • 시그모이드 함수

    • https://imgur.com/gudh9Lf
  • 시그모이드 출력 결과 확률 y로부터 교차 엔트로피 오차를 적용하여 손실값을 구함

  • https://imgur.com/JEWf1rW

    • y == 신경망이 출력한 확률 / t == 정답 레이블 -> y-t == 추론결과에서 정답레이블을 뺀 값
    • 오차가 크면 크게 학습, 작으면 작게 학습
    • cf; $\frac{\partial L}{\partial x}&=& y-t​$가 유도되는 과정

4.2.4 다중 분류에서 이진 분류로(구현)
  • https://imgur.com/bQ99S1f

  • Embedding 계층과 dot 연산 처리를 합쳐 Embedding Dot 계층 도입

    • https://imgur.com/MNbTzWy

    • # dot product(내적)과 embedding 계층을 합친 계층; 은닉층 이후 처리를 한번에 수행
      class EmbeddingDot:
          def __init__(self, W):
              self.embed = Embedding(W)   # embedding계층
              self.params = self.embed.params # 매개변수
              self.grads = self.embed.grads   # 기울기
              self.cache = None   # 순전파시의 계산결과를 잠시 유지하기 위한 변수
          
          # 순전파; 은닉층 뉴런(h), 단어 ID의 넘파이배열(idx)
          def forward(self, h, idx):
              # idx: 단어ID의 배열 -> 한꺼번에 처리하는 미니배치 처리를 가정하였음
              target_W = self.embed.forward(idx)
              out = np.sum(target_W * h, axis=1) #내적 계산, 행을 기준으로 수행
          
              self.cache = (h, target_W)
              return out
          
          def backward(self, dout):
              h, target_W = self.cache # 순전파 계산결과를 불러옴
              dout = dout.reshape(dout.shape[0], 1)
          
              dtarget_W = dout * h
              self.embed.backward(dtarget_W)
              dh = dout * target_W
              return dh
      
4.2.5 네거티브 샘플링
  • 기존의 방법은 긍정적인 예만 학습하고 부정적인 예(타깃 외의 단어)에 대한 지식은 X
  • 타깃단어에 대해서는 sigmoid 출력값을 1에 가깝게, 타깃 외 단어에 대해서는 출력값을 0에 가깝게 만들어야함
    • https://imgur.com/dY6xW7T
  • 모든 부정적 예를 대상으로 하면 어휘 수가 늘어나게 됨 -> 당초 개선 목적과 대치 –> 부정적 예를 샘플링하여 사용
  • 긍정적인 예를 타깃으로 한 경우의 손실을 구함 + 부정적 예를 선별하여 손실을 구함 = 최종 손실
  • https://imgur.com/q7v8kD4
    • Sigmoid with Loss layer에 긍정적 예는 정답 레이블로 1을 입력, 부정적 예는 0을 입력함 => 각 데이터의 손실을 더해 최종 손실을 출력
4.2.6 네거티브 샘플링의 샘플링 기법
  • 부정적 예를 어떻게 샘플링할 것인가?

    • 말뭉치 통계 데이터를 기반으로 샘플링 -> 단어 빈도 기준으로 샘플링 –> 출현 횟수를 구해 확률분포로 나타내고 그에 따라 단어를 샘플링
    • https://imgur.com/kOSbZsI
    • 희소한 단어를 처리하는 일은 중요도가 낮으므로 희소단어는 탈락되는 것이 좋음
  • np.random.choice()를 활용하여 무작위 샘플링

  • # 확률분포에 따라 네거티브샘플링 하기
    np.random.choice(10)
    np.random.choice(10)
      
    words=['you', 'say', 'goodbye', 'I', 'hello', '.']
    np.random.choice(words)
      
    # 5개만 무작위로 샘플링(중복O)
    np.random.choice(words, size=5)
      
    # 5개만 무작위로 샘플링(중복X)
    np.random.choice(words, size=5, replace=False)
      
    # 확률분포에 따라 샘플링
    p=[0.5, 0.1, 0.05, 0.2, 0.05, 0.1]
    np.random.choice(words, p=p)
    
  • 확률분포의 각 요소에 0.75를 제곱함으로써 출현확률이 낮은 단어를 버리지 않게함

    • p =[0.7, 0.29, 0.01]
      new_p = np.power(p, 0.75)
      new_p /= np.sum(new_p)
      print(new_p)
      
  • 유니그램 샘플러; 한 단어를 대상으로 확률분포로 만듦

    • # 유니그램; 하나의 연속된 단어 -> 한 단어를 대상으로 확률분포를 만듦
      class UnigramSampler:
          def __init__(self, corpus, power, sample_size):
              # 단어ID목록(corpus), 확률분포에 제곱할 값(power; default==0.75), 부정 예 샘플링을 수행할 횟수(sample_size)
              self.sample_size = sample_size
              self.vocab_size = None
              self.word_p = None
          
              counts = collections.Counter()
              for word_id in corpus:
                  counts[word_id] += 1
          
              vocab_size = len(counts)
              self.vocab_size = vocab_size
          
              self.word_p = np.zeros(vocab_size)
              for i in range(vocab_size):
                  self.word_p[i] = counts[i]
          
              self.word_p = np.power(self.word_p, power)
              self.word_p /= np.sum(self.word_p)
          
          # target으로 지정한 단어를 긍정적 예로 해석하여 그 외의 단어ID를 샘플링함(부정적예시를 선택)
          def get_negative_sample(self, target):
              batch_size = target.shape[0]
          
              if not GPU:
                  negative_sample = np.zeros((batch_size, self.sample_size), dtype=np.int32)
          
                  for i in range(batch_size):
                      p = self.word_p.copy()
                      target_idx = target[i]
                      p[target_idx] = 0
                      p /= p.sum()
                      negative_sample[i, :] = np.random.choice(self.vocab_size, size=self.sample_size, replace=False, p=p)
              else:
                  # GPU(cupy)로 계산할 때는 속도를 우선한다.
                  # 부정적 예에 타깃이 포함될 수 있다.
                  negative_sample = np.random.choice(self.vocab_size, size=(batch_size, self.sample_size),
                                                     replace=True, p=self.word_p)
          
              return negative_sample
      
      # 유니그램 샘플러 사용하기
      corpus = np.array([0, 1, 2, 3, 4, 1, 2, 3])
      power = 0.75
      sample_size = 2
          
      sampler = UnigramSampler(corpus, power, sample_size)
      target = np.array([1,3,0]) # 긍정적 예로 3개의 데이터를 미니배치로 사용
      negative_sample = sampler.get_negative_sample(target)
      print(negative_sample)
      
4.2.7 네거티브 샘플링 구현
# 초기화 메소드
class NegativeSamplingLoss:
    def __init__(self, W, corpus, power=0.75, sample_size=5):
      	#출력측 가중치 W, 말뭉치(단어id list) corpus, 
        #확률분포에 제곱할 값 power, 부정적 예 샘플링 횟수 sample 
        self.sample_size = sample_size # 부정적 샘플링 횟수를 인스턴스변수에 저장
        self.sampler = UnigramSampler(corpus, power, sample_size)
        self.loss_layers = [SigmoidWithLoss() for _ in range(sample_size + 1)] #sigmoid with loss 계층을 저장 + 1(0번째 계층; 긍정적예)
        self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)] # embedding dot 계층을 저장 + 1(0번째 계층; 긍정적예)
				
        # 각 계층에서 사용할 가중치 매개변수와 기울기를 배열로 저장
        self.params, self.grads = [], []
        for layer in self.embed_dot_layers:
            self.params += layer.params
            self.grads += layer.grads

    def forward(self, h, target):
      	# 은닉층 뉴런 h, 긍정적 예 target
        batch_size = target.shape[0]
        
        #부정적 예를 샘플링하여 저장
        negative_sample = self.sampler.get_negative_sample(target)

        # 긍정적 예 순전파
        score = self.embed_dot_layers[0].forward(h, target) #Embedding Dot 계층의 forward score
        correct_label = np.ones(batch_size, dtype=np.int32)
        loss = self.loss_layers[0].forward(score, correct_label) # sigmoid with loss 계층으로 흘려 loss를 구함

        # 부정적 예 순전파
        negative_label = np.zeros(batch_size, dtype=np.int32)
        for i in range(self.sample_size):
            negative_target = negative_sample[:, i]
            score = self.embed_dot_layers[1 + i].forward(h, negative_target)
            loss += self.loss_layers[1 + i].forward(score, negative_label)

        return loss

    def backward(self, dout=1):
        dh = 0
        for l0, l1 in zip(self.loss_layers, self.embed_dot_layers):
            dscore = l0.backward(dout)
            dh += l1.backward(dscore)

        return dh

4.3 개선판 word2vec 학습

  • PTB 데이터셋을 사용해서 학습하고 실용적인 단어 분산표현 얻기
4.3.1 CBOW 모델 구현
  • 클래스 출력측 가중치는 입력측 가중치와 같은 형상 -> 단어 벡터가 행 방향에 배치 –> Embedding 계층 때문

  • # CBOW 표현
    # coding: utf-8
    import sys
    from common.np import *  # import numpy as np
    # from common.layers import Embedding
      
    class CBOW:
        def __init__(self, vocab_size, hidden_size, window_size, corpus):
          	# vocab_size;어휘수, hidden_size; 은닉층의 뉴런 수,
            # corpus; 단어 ID 목록, window_size; 맥락의 크기(타깃을 중심으로 좌우로 +@)
            V, H = vocab_size, hidden_size
      
            # 가중치 초기화
            W_in = 0.01 * np.random.randn(V, H).astype('f')
            W_out = 0.01 * np.random.randn(V, H).astype('f')
      
            # 계층 생성
            self.in_layers = []
            for i in range(2 * window_size):
                layer = Embedding(W_in)  # Embedding 계층 사용
                self.in_layers.append(layer)
            self.ns_loss = NegativeSamplingLoss(W_out, corpus, power=0.75, sample_size=5)
      
            # 모든 가중치와 기울기를 배열에 모음
            layers = self.in_layers + [self.ns_loss]
            self.params, self.grads = [], []
            for layer in layers:
                self.params += layer.params
                self.grads += layer.grads
      
            # 인스턴스 변수에 단어의 분산표현을 저장
            self.word_vecs = W_in
      
        def forward(self, contexts, target):
          	# 맥락과 타깃을 단어ID로 받음(SimpleCBOW는 희소벡터로 변환하여 사용)
            h = 0
            for i, layer in enumerate(self.in_layers):
                h += layer.forward(contexts[:, i])
            h *= 1 / len(self.in_layers)
            loss = self.ns_loss.forward(h, target)
            return loss
      
        def backward(self, dout=1):
            dout = self.ns_loss.backward(dout)
            dout *= 1 / len(self.in_layers)
            for layer in self.in_layers:
                layer.backward(dout)
            return None
    
4.3.2 CBOW 모델 학습 코드
  • # coding: utf-8
    import sys
    # sys.path.append('..')
    import numpy as np
    from common import config
    # GPU에서 실행하려면 아래 주석을 해제하세요(CuPy 필요).
    # ===============================================
    # config.GPU = True
    # ===============================================
    import pickle
    # from common.trainer import Trainer
    # from common.optimizer import Adam
    # from cbow import CBOW
    # from skip_gram import SkipGram
    from common.util import to_cpu, to_gpu
    from dataset import ptb
      
      
    # 하이퍼파라미터 설정
    window_size = 5	#윈도우 크기(맥락 크기)
    hidden_size = 100 #은닉층 뉴런 수
    batch_size = 100
    max_epoch = 10
      
    # 데이터 읽기
    corpus, word_to_id, id_to_word = ptb.load_data('train')
    vocab_size = len(word_to_id)
      
    contexts, target = create_contexts_target(corpus, window_size)
    if config.GPU:
        contexts, target = to_gpu(contexts), to_gpu(target)
      
    # 모델 등 생성
    model = CBOW(vocab_size, hidden_size, window_size, corpus)
    # model = SkipGram(vocab_size, hidden_size, window_size, corpus)
    optimizer = Adam()
    trainer = Trainer(model, optimizer)
      
    # 학습 시작
    trainer.fit(contexts, target, max_epoch, batch_size)
    trainer.plot()
      
    # 나중에 사용할 수 있도록 필요한 데이터 저장
    word_vecs = model.word_vecs
    if config.GPU:
        word_vecs = to_cpu(word_vecs)
    params = {}
    params['word_vecs'] = word_vecs.astype(np.float16)
    params['word_to_id'] = word_to_id
    params['id_to_word'] = id_to_word
    pkl_file = 'cbow_params.pkl'  # or 'skipgram_params.pkl'
    with open(pkl_file, 'wb') as f:
        pickle.dump(params, f, -1)
      
    
4.3.3 CBOW 모델 평가
  • most_similar() 메소드로 거리가 가까운 단어를 뽑음

    • # coding: utf-8
      import sys
      sys.path.append('..')
      from common.util import most_similar, analogy
      import pickle
          
          
      pkl_file = 'cbow_params.pkl'
      # pkl_file = 'skipgram_params.pkl'
          
      with open(pkl_file, 'rb') as f:
          params = pickle.load(f)
          word_vecs = params['word_vecs']
          word_to_id = params['word_to_id']
          id_to_word = params['id_to_word']
          
      # 가장 비슷한(most similar) 단어 뽑기
      querys = ['you', 'year', 'car', 'toyota']
      for query in querys:
          most_similar(query, word_to_id, id_to_word, word_vecs, top=5)
          
      # 유추(analogy) 작업
      print('-'*50)
      analogy('king', 'man', 'queen',  word_to_id, id_to_word, word_vecs)
      analogy('take', 'took', 'go',  word_to_id, id_to_word, word_vecs)
      analogy('car', 'cars', 'child',  word_to_id, id_to_word, word_vecs)
      analogy('good', 'better', 'bad',  word_to_id, id_to_word, word_vecs)
      
  • 비슷한 단어를 모으고 복잡한 패턴을 파악

  • word2vec 단어분산표현을 사용하여 유추문제를 벡터의 덧셈, 뺄셈으로 풀 수 있음

    • https://imgur.com/mfWgX1h
      • 단어벡터 공간에서 특정 단어와 가능한 가까워지는 단어를 찾아 유추 문제 해결
  • 시제패턴, 단수/복수, 비교급 성질 등이 인코딩 되어 있음을 확인 -> 문법적 패턴도 파악 가능

4.4 word2vec 남은 주제

4.4.1 word2vec을 사용한 애플리케이션의 예

  • 자연어 처리 분야에서 단어의 분산 표현 –> 다른 모델의 input으로 활용 -> 전이학습
    • 학습을 미리 끝낸 단어 분산 표현을 이용하면 NLP 작업이 대체로 좋은 성능을 가짐
  • 문장, 단어를 고정길이 벡터로 변환 -> 자연어처리에 머신러닝 기법을 적용할 수 있음
    • Bag-of-words; 단어 순서를 고려치 않고 문장의 각 단어를 분산표현으로 변환해 합을 구하는 방법
    • 순환신경망(RNN) 활용
    • https://imgur.com/iFYqt28
4.4.2 단어 벡터 평가 방법
  • 단어 분산 표현 평가는 실제 애플리케이션과 분리해 평가
  • 단어의 유사성, 유추문제를 활용하여 평가
  • 유사성 평가; 사람이 작성한 단어 유사도를 검증 세트를 사용해 평가
    • 사람이 부여한 점수와 word2vec의 코사인 유사도 점수를 비교해 상관성을 봄
  • 유추문제 활용 평가; 유추문제를 출제해 정답률로 측정
    • https://imgur.com/PjGhUZa
    • 단어의 의미, 문법적 문제를 어느정도 이해하는지 측정 가능

4.5 정리

  • Embedding 계층을 구현하고 네거티브 샘플링 기법을 도입해 일부만 처리하여 계산을 효율적으로 수행
  • word2vec으로 얻은 단어 분산표현은 다양한 자연어처리 작업에 활용

https://imgur.com/82Zj3Qy

Tags: ,

Categories:

Updated:

Leave a comment