Word2vec
‘밑바닥부터 시작하는 딥러닝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)
-
단어벡터 간 사칙연산으로 단어 유추 평가 가능
-
Word2Vec의 임베딩을 다른 딥러닝의 입력값(Input)으로 사용; Transfer Learning(전이 학습)
3.1 추론 기반 기법과 신경망
- 단어를 벡터로 표현하는 방법은 분포 가설을 배경으로한 통계 기반 기법 과 추론 기반 기법 두 부류로 나뉨
- 분포 가설; “단어의 의미는 주변 단어에 의해 형성된다”는 가설 –> 단어의 동시발생 가능성을 얼마나 잘 모델링 하는가에 중요한 연구 포인트
- 통계 기반 기법의 문제점 -> 극복 대안으로서의 추론 기반 기법, word2vec 전처리를 위한 신경망으로 단어 처리하는 예를 제시
3.1.1. 통계 기반 기법의 문제점
- 통계 기반 기법
- 주변 단어의 빈도를 기초로 단어를 표현
- 말뭉치의 전체 통계(동시발생 행렬, PPMI 등) -> SVD(특이값 분해)를 적용(1회) -> 밀집벡터(Dense vector; 단어의 분산표현)
- => BUT! 대규모 말뭉치를 다룰 때 문제 발생
- 추론 기반 기법
- 신경망에서의 미니배치 학습
- 신경망이 한번에 미니배치의 학습 샘플씩 반복해서 학습하며 가중치를 갱신
- 신경망에서의 미니배치 학습
-
- 통계 기반 기법은 배치 학습으로 데이터를 한번에 처리
- 추론 기반 기법은 미니 배치 학습으로 데이터의 일부를 사용하여 순차적으로 학습
- 병렬 계산 등이 가능하여 학습 속도를 높일 수 있음
3.1.2 추론 기반 기법 개요
you __ goodbye and I say hello.
- 주변 단어(Context word)가 있을 때 “ㅡㅡ”로 표현된 중심 단어(Target word)가 무엇인지 추론
- 추론 문제를 반복하여 풀면서 단어의 출현 패턴을 학습하는 것
-
- 맥락 정보를 입력 받아 각 단어의 출현 확률을 출력 -> 말뭉치를 사용해 모델이 올바른 추측을 내놓도록 학습 -> 학습의 결과로 단어의 분산 표현을 얻음
3.1.3. 신경망에서의 단어 처리
- 단어를 고정 길이의 벡터로 변환 –> one-hot encoding 원-핫 인코딩으로 원-핫 벡터로 변환
- 원-핫 인코딩?; 정수 인코딩을통해 인덱스를 부여한 후 숫자로 바뀐 단어들을 벡터화한 일종의 희소 표현(Sparse Representation)
- 가지고 있는 텍스트에 단어의 개수 == 단어집합의 크기 → 단어의 개수가 늘어날 수록 비효율적
- 단어집합의 크기를 벡터의 차원으로 보고, 표현하려는 True의 인덱스에 1, 아닌 경우에는 0을 부여. (0과 1로만 표현됨)
- 연산의 편의성, 다양한 모델 적용, 정확도 향상 등의 이유로 사용
- 단어를 벡터로 나타내고 신경망을 구성하는 계층들은 벡터를 처리 –> 단어를 신경망으로 처리할 수 있음
- 완전연결계층(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의 행렬곱은 결국 가중치의 행벡터 하나를 뽑아낸 것과 같음; 해당 위치의 행벡터가 추출
3.2 단순한 word2vec
3.2.1 CBOW 모델의 추론 처리
-
CBOW; continuous bag-of-words
- context(맥락)으로부터 target(중심)을 추측하는 용도의 신경망
- context -> one-hot encoding -> 입력값으로 받음
-
뉴런(모델)의 관점에서 CBOW
-
- N개의 입력층 ~ 완전연결계층 ~ 은닉층 ~ 완전연결계층 ~ 출력층
- 여러개의 입력층인 경우 전체의 평균을 구한 값이 은닉층 뉴런으로 흐름
- 출력층 뉴런은 각 단어의 Score(점수)를 출력 -> 여기에 Softmax function을 적용하여 확률 도출
- 입력층 다음의 완전연결계층의 가중치가 바로 단어분산표현
- 학습을 진행할수록 맥락에서 출현하는 단어를 잘 추측하는 방향으로 분산표현들이 갱신
- 단어의 의미를 다차원 공간에 벡터화한 것
- 은닉층의 뉴런을 입력층의 뉴런수보다 적게하여 희소벡터를 밀집벡터로 만들어주는 것이 핵심
- 은닉층의 정보(인코딩) –> 출력층의 점수(디코딩)
-
-
계층 관점에서의 CBOW
- 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을 적용하여 확률로 변환 -> 확률과 정답 레이블로부터 교차 엔트로피 오차를 구함 -> 손실로 사용하여 학습 —> 올바른 중심단어를 예측할 수 있도록 가중치를 조정
- 맥락이 주어졌을 때 중앙단어로 어떤 것이 출현하는 지 나타낸 것 ==> 정답에 해당하는 뉴런의 값이 가장 큼
- 주어지는 학습 말뭉치에 나오는 단어 출현 패턴을 학습하므로 말뭉치에 따라 단어의 분산 표현도 달라짐
-
- 소프트맥스 계층과 교차 엔트로피 오차를 Softmax with Loss라는 하나의 계층으로 구현
3.2.3 word2vec의 가중치와 분산표현
- 입력 측의 완전연결계층 가중치의 각 행이 각 단어의 분산표현
- 출력 측의 완전연결계층 가중치에도 단어의 의미가 인코딩된 벡터가 저장되어있음
- 출력측 가중치는 분산 표현이 열 방향(수직방향)으로 저장
- 보통 입력 측의 가중치만을 최종 단어의 분산 표현으로서 이용
3.3 학습 데이터 준비
- “You say goodbye and I say hello”
3.3.1 맥락과 타깃
-
입력; context(맥락) / 정답 레이블; target(타깃)
- => 신경망에 맥락을 입력했을 때 타깃이 출현할 확률을 높이도록 학습
-
맥락과 타깃을 만드는 것을 말뭉치 안의 모든 단어에 대해 수행함
- 맥락:타깃은 다 대 일 관계
- 맥락-타깃으로 이루어진 각 행이 신경망의 입력값으로 사용됨
-
말뭉치 텍스트를 단어 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 원핫 표현으로 변환
-
- 맥락과 타깃을 단어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
-
-
- 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가 일어날 확률 -
- 말뭉치를 단어 시퀀스로 표기, t번째 단어에 대해 윈도우 크기가 1인 맥락
-
- Wt-1과 Wt+1이 주어졌을 때 타깃이 Wt가 될 확률
- CBOW가 모델링 하고 있는 수식
-
- Wt에 해당하는 원소만 1이고 나머지는 0인 희소벡터이므로 Wt이외의 일이 일어날 경우는 원핫 레이블 요소가 0
- => 단순히 확률에 음의 로그 가능도 를 취함; log를 취하고 마이너스를 붙임
- 말뭉치 전체로 확장 했을 때는 전체 합의 평균
- 이 값을 가능한 작게 만드는 것이 학습의 목적, W(가중치 매개변수)는 단어의 분산표현(밀집벡터)
3.5.2 skip-gram 모델
- CBOW에서 다루는 맥락과 타깃을 역전시킨 모델
-
- Skip-gram은 타깃으로부터 주변의 여러 단어(맥락)을 추측
-
- 입력층은 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; 통계기반기법을 기반으로 말뭉치 전체 통계정보를 손실함수에 도입, 추론기반기법의 미니배치 학습을 수행
chapter 4. word2vec 속도 개선
-
SimpleCBOW모델은 어휘 수가 많아지면 계산량도 커져 계산시간이 너무 오래 걸림 -> 속도 개선 필요
1️⃣ Embedding 계층 도입
2️⃣ 네거티브 샘플링이라는 손실 함수 도입
3️⃣ PTB 데이터셋(실용적 크기의 말뭉치)로 학습을 수행해 얻은 단어 분산표현을 평가
4.1 word2vec 개선 (1)
-
- 어휘 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
-
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)로 근사하는 것
- 출력층에 뉴런을 하나만 준비하여 타깃 단어의 score만 구하도록 함
- 은닉층과 출력 측의 가중치 행렬의 내적(dot-product)는 타깃에 해당하는 단어벡터만 추출 -> 단어벡터와 은닉층 뉴런과의 내적 계산만 수행
-
- 각 단어의 고유값의 단어벡터가 열로 저장되어 있고 타깃 단어벡터를 추출해서 은닉층뉴런과의 내적을 구함 -> 최종점수 -> 시그모이드 함수 -> 확률변환
- => 타깃 단어 하나에 주목해 그 점수만 계산하는 것이 핵심
4.2.3 시그모이드 함수와 교차 엔트로피 오차
-
시그모이드 함수로 확률로 변환하고 교차 엔트로피 오차로 손실함수 사용
cf) 다중 분류는 소프트맥스 함수 + 교차 엔트로피 오차
이진 분류는 시그모이드 함수 + 교차 엔트로피 오차
-
시그모이드 함수
-
시그모이드 출력 결과 확률 y로부터 교차 엔트로피 오차를 적용하여 손실값을 구함
-
- y == 신경망이 출력한 확률 / t == 정답 레이블 -> y-t == 추론결과에서 정답레이블을 뺀 값
- 오차가 크면 크게 학습, 작으면 작게 학습
-
cf; $\frac{\partial L}{\partial x}&=& y-t$가 유도되는 과정
4.2.4 다중 분류에서 이진 분류로(구현)
-
Embedding 계층과 dot 연산 처리를 합쳐 Embedding Dot 계층 도입
-
# 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에 가깝게 만들어야함
- 모든 부정적 예를 대상으로 하면 어휘 수가 늘어나게 됨 -> 당초 개선 목적과 대치 –> 부정적 예를 샘플링하여 사용
- 긍정적인 예를 타깃으로 한 경우의 손실을 구함 + 부정적 예를 선별하여 손실을 구함 = 최종 손실
-
- Sigmoid with Loss layer에 긍정적 예는 정답 레이블로 1을 입력, 부정적 예는 0을 입력함 => 각 데이터의 손실을 더해 최종 손실을 출력
4.2.6 네거티브 샘플링의 샘플링 기법
-
부정적 예를 어떻게 샘플링할 것인가?
- 말뭉치 통계 데이터를 기반으로 샘플링 -> 단어 빈도 기준으로 샘플링 –> 출현 횟수를 구해 확률분포로 나타내고 그에 따라 단어를 샘플링
- 희소한 단어를 처리하는 일은 중요도가 낮으므로 희소단어는 탈락되는 것이 좋음
-
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 단어분산표현을 사용하여 유추문제를 벡터의 덧셈, 뺄셈으로 풀 수 있음
-
- 단어벡터 공간에서 특정 단어와 가능한 가까워지는 단어를 찾아 유추 문제 해결
-
-
시제패턴, 단수/복수, 비교급 성질 등이 인코딩 되어 있음을 확인 -> 문법적 패턴도 파악 가능
4.4 word2vec 남은 주제
4.4.1 word2vec을 사용한 애플리케이션의 예
- 자연어 처리 분야에서 단어의 분산 표현 –> 다른 모델의 input으로 활용 -> 전이학습
- 학습을 미리 끝낸 단어 분산 표현을 이용하면 NLP 작업이 대체로 좋은 성능을 가짐
- 문장, 단어를 고정길이 벡터로 변환 -> 자연어처리에 머신러닝 기법을 적용할 수 있음
- Bag-of-words; 단어 순서를 고려치 않고 문장의 각 단어를 분산표현으로 변환해 합을 구하는 방법
- 순환신경망(RNN) 활용
4.4.2 단어 벡터 평가 방법
- 단어 분산 표현 평가는 실제 애플리케이션과 분리해 평가
- 단어의 유사성, 유추문제를 활용하여 평가
- 유사성 평가; 사람이 작성한 단어 유사도를 검증 세트를 사용해 평가
- 사람이 부여한 점수와 word2vec의 코사인 유사도 점수를 비교해 상관성을 봄
- 유추문제 활용 평가; 유추문제를 출제해 정답률로 측정
- 단어의 의미, 문법적 문제를 어느정도 이해하는지 측정 가능
4.5 정리
- Embedding 계층을 구현하고 네거티브 샘플링 기법을 도입해 일부만 처리하여 계산을 효율적으로 수행
- word2vec으로 얻은 단어 분산표현은 다양한 자연어처리 작업에 활용
Leave a comment