1. 자연어 처리
- 자연어(Natural Language): 한국어와 영어 등 우리가 평소에 쓰는 말
- 자연어 처리(Natural Language Processing, NLP): 언어를 컴퓨터에게 이해시키기 위한 기술
- 단어의 의미를 잘 파악하는 표현 방법
- 시소러스를 활용한 기법
- 통계 기반 기법
- 추론 기반 기법(word2vec)
- 시소러스(Thesaurus)
- 시소러스: 유의어 사전
- 동의어나 유의어가 한 그룹으로 분류
- 상·하위, 전체와 부분 등의 관계까지 정의
- 모든 단어가 레이블링 되어 있다면, '단어 네트워크'를 이용하여 컴퓨터에게 단어 사이의 관계를 가르칠 수 있음
- ex) WordNet, K-Thesaurus 등
- 시소러스의 문제점
- 시대 변화에 대응하기 어려움
- 사람이 쓰는 비용이 큼
- 단어의 미묘한 차리를 표현할 수 없음
- 통계 기반 기법
- 어떤 단어에 주목했을 때, 그 주변에 어떤 단어가 몇 번이나 등장하는지를 세어 집계하는 방법
- ex) 검색 기반 방법혼이 발전 되어 나온 방법
- 번역 데이터를 모으는 일은 쉬운 일이 아니며 보통은 1천만 문장쌍이 있어야 쓸만한 성능이 나옴
- 도메인(분야: 뉴스, 특허, 웹채팅, SNS 등)에 굉장히 의존적이며 뉴스 도메인 데이터로 학습한 모델을 SMS 번역에 적용하면 심각한 성능저하 발생
import numpy as np
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:' + str(corpus))
print('word_to_id:' + str(word_to_id))
print('id_to_word:' + str(id_to_word))
# 출력 결과
# say는 첫 say에서 id가 1번으로 부여되어 뒤에 나온 say에 새로운 id 부여없이 1로 사용
corpus:[0 1 2 3 4 1 5 6]
word_to_id:{'you': 0, 'say': 1, 'goodbye': 2, 'and': 3, 'i': 4, 'hello': 5, '.': 6}
id_to_word:{0: 'you', 1: 'say', 2: 'goodbye', 3: 'and', 4: 'i', 5: 'hello', 6: '.'}
- 파이썬으로 말뭉치 전처리하기
- 단어의 분산 표현
- '단어의 의미'를 정확하게 파악할 수 있는 벡터 표현
- 단어를 고정 길이의 '밀집벡터'로 표현
- 밀집벡터: 대부분의 원소가 0이 아닌 실수인 벡터 (ex/ (R, G, B) = (170, 33, 22))
- 분포 가설
- 단어의 의미는 주변 단어에 의해 형성
- 단어 자체에는 의미가 없고, 그 단어가 사용된 '맥락'이 의미를 형성
- 맥락: 주목하는 단어 주변에 놓인 단어
- 맥락의 크기(주변 단어를 몇 개나 포함할 지) = 윈도우 크기
- 동시발생 행렬
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
C = create_co_matrix(corpus, len(word_to_id))
C
# 출력 결과
array([[0, 1, 0, 0, 0, 0, 0, 0],
[1, 0, 1, 0, 1, 1, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 0],
[0, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0]])
- 벡터 간 유사도
- 벡터의 내적, 유클리드 거리등
- 코사인 유사도(cosine similarity)를 자주 이용
$$ similarity(x, y) = \frac{x \cdot y}{\left || x \right || \left || y \right ||} = \frac{x_{1}y_{1} + \cdots + x_{n}y_{n}}{\sqrt{x_{1}^{2} + \cdots + x_{n}^{2}} \sqrt{y_{1}^{2} + \cdots + y_{n}^{2}}} $$
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)
c0 = C[word_to_id['you']] # 'you'의 단어 벡터: [0 1 0 0 0 0 0]
c1 = C[word_to_id['i']] # 'i'의 단어 벡터: [0 1 0 1 0 0 0]
print(cos_similarity(c0, c1))
# 출력 결과
0.7071067758832467
- 통계 기반 기법 개선하기
- 유사 단어 랭킹 표시
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)
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
most_similar('he', word_to_id, id_to_word, C, top = 5)
# 출력 결과
'he(을)를 찾을 수 없습니다.
most_similar('you', word_to_id, id_to_word, C, top = 5)
# 출력 결과
[query]you
goodbye: 0.7071067758832467
i: 0.7071067758832467
hello: 0.7071067758832467
say: 0.0
and: 0.0
- 직관적으로 'you' & 'hello', 'you' & 'goodbye'는 관련이 없어 보일 수 있음
- 이는 말뭉치의 크기가 너무 작기 때문
- 상호정보량(점별 상호정보량(Pointwise Mutual Information, PMI)
\( PMI(x, y) = log_{2} \frac{P(x, y)}{P(x)P(y)} = log_{2} \frac{\frac{C(x, y)}{N}}{\frac{C(x)}{N}\frac{C(y)}{N}}=log_{2}\frac{C(x, y) \cdot N}{C(x)C(y)} \)- x, y가 일어날 확률 / x가 일어날 확률, y가 일어날 확률
- 양의 상호정보량(Positive PMI, PPMI)
\( PPMI(x, y)=max(0, PMI(x, y)) \)
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[i] * S[j]) + eps)
M[i, j] = max(0, pmi)
if verbose:
cnt += 1
if cnt % (total //100) == 0:
print('%.1f%% 완료'%(100 * cnt / total))
return M
C = create_co_matrix(corpus, len(word_to_id), window_size = 1)
W = ppmi(C)
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. ]]
- PPMI 행렬의 문제점
- 말뭉치의 어휘수가 증가함에 따라 단어 벡터의 차원 수도 증가
- 원소 대부분이 0(각 원소의 중요도가 낮음
- 노이즈에 약하고 견고하지 못함
- 차원 감소 - 특이값 분해(Singular Value Decomopsition, SVD)
import sys
sys.path.append('..')
import numpy as np
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)
U, S, V = np.linalg.svd(W)
print('차원 감소된 U')
print(U[:, :2])
for word, word_id in word_to_id.items():
plt.annotate(word, (U[word_id, 0], U[word_id, 1]))
plt.scatter(U[:, 0], U[:, 1], alpha = 0.5)
plt.show()
# 출력 결과
차원 감소된 U
[[-1.110e-16 3.409e-01]
[-5.976e-01 0.000e+00]
[-5.551e-17 4.363e-01]
[-4.978e-01 1.665e-16]
[-3.124e-17 4.363e-01]
[-3.124e-17 7.092e-01]
[-6.285e-01 -1.943e-16]]
2. Word2Vec
- 단어를 벡터로 표현하는 방법
- 통계 기반 기법: 배치학습(많은 데이터를 한번에 처리), 학습 데이터에 대해 SVD을 통해 저차원으로 변환
- 추론 기반 기법: 미니 배치 학습 가능(전체를 다 하지 않고 필요한 부분만 분리시켜 학습, 나눠서 처리하므로 나눠서 할 수 있고 GPU 병렬 계산 등이 가능)
- 신경망에서의 단어 처리
- 추론 기반 기법의 주된 작업은 추론
- 모델로 사용하는 것은 신경망
- '?'를 추론하기 위해 맥락을 모델에 넣어 '?' 자리에 올 수 있는 단어의 확률분포 값을 추론해서 알려줌
- 신경망은 단어를 그대로 처리할 수 없음
- 원-핫 표현으로 변환(벡터의 원소 중 하나만 1이고 나머지는 모두 0인 벡터
- 신경망의 입력층은 뉴런의 수 고정
- 단어 하나를 완전연결계층을 통해 변환
- 완전연결계층: 인접하는 계층의 모든 뉴런과 연결되어 있는 신경망
- C와 W의 곱은 가중치의 행벡터 한 줄과 같음
- matmul로도 수행가능
import numpy as np
c = np.array([1, 0, 0, 0, 0, 0, 0]) # 입력
W = np.random.randn(7, 3) # 가중치
h = np.matmul(c,W) # 중간 노드
print(h)
# 출력 결과
[-0.473 -1.132 -1.333]
- CBOW(continuous bag-of-word) 모델
- 주변 단어들로부터 중앙 단어를 추측하는 용도의 신경망
- CBOW의 입력은 맥락
- 맥락에 포함시킬 단어가 N개면 입력층도 N개
- 입력층이 여러 개일 경우 은닉층은 전체의 평균을 하면 됨
- 출력층에 소프트맥스 함수를 적용하여 확률을 얻을 수 있음
- 계층 관점에서 CBOW 모델의 신경망 구성
- you와 goodbye 사이에 무엇이 올지 추론
- 가중치 계산할 때 행렬곱한 뒤, 0.5를 곱함(두 개의 평균을 취하기 위해)
- 그 후 가중치 출력을 통해 점수를 계산
- CBOW 모델의 파이썬 구현
- 이때 사용한 common.layers import MatMul 파일은 '밑바닥부터 시작하는 딥러닝' github에서 다운받음
( https://github.com/WegraLee/deep-learning-from-scratch-2 )
- 이때 사용한 common.layers import MatMul 파일은 '밑바닥부터 시작하는 딥러닝' github에서 다운받음
import numpy as np
from common.layers import MatMul
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)
in_layer0 = MatMul(W_in)
in_layer1 = MatMul(W_in)
out_layer = MatMul(W_out)
h0 = in_layer0.forward(c0)
h1 = in_layer1.forward(c1)
h = 0.5 * (h0 + h1)
s = out_layer.forward(h)
print(s)
# 출력 결과
[[ 0.216 0.096 0.06 0.536 0.389 -0.392 -0.217]]
- CBOW 모델의 학습에서 하는 일: 올바른 학습을 할 수 있게 가중치 조절
- 다루고 있는 모델은 다중 클래스 분류를 수행하는 신경망(softmax와 cross entropy error)만 이용하면 됨
- word2vec의 가중치와 분산 표현
- Word2vec에서 사용되는 가중치는 2개가 존재
- W(in) 입력층 완전 연결계층의 가중치
- W(out) 출력층 완전 연결계층의 가중치
- 최종적으로 이용하는 단어의 분산표현으로는 어느 가중치를 선택해야 할까?
→ 가장 대중적인 선택: W(in) 입력층 완전 연결계층의 가중치만 이용하는 것
- 맥락과 타겟
- 타겟: 맥락에 둘러 싸인 중앙의 단어(정답 레이블)
- 말뭉치 → 단어 ID 변환
def create_contexts_target(corpus, window_size = 1):
# 문장의 첫단어부터 마지막 단어까지가 타겟
target = corpus[window_size:- window_size]
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)
return np.array(contexts), np.array(target)
contexts, target = create_contexts_target(corpus, window_size = 1)
print(contexts)
print(target)
# 출력 결과
[[0 2]
[1 3]
[2 4]
[3 1]
[4 5]
[1 6]]
[1 2 3 4 1 5]
- 맥락과 타겟 → 원-핫 표현으로 변환
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] = i
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
- CBOW 모델 구현
- 생성자레서 어휘 수와 뉴런 수를 입력 받음
- 가중치 두 개 생성
- astype(f): float형으로 타입 변환
- MatMul 계층 2개, 출력층 1개, Softmax with Loss 계층 1개 생성
- 매개변수와 기울기를 인스턴스 변수에 모음
- 이때 사용한 common.layers import SoftmaxWithLoss 파일은 '밑바닥부터 시작하는 딥러닝' github에서 다운받음
( https://github.com/WegraLee/deep-learning-from-scratch-2 )
from common.layers import MatMul
from common.layers import SoftmaxWithLoss
class SimpleCBOW:
def __init__(self, vocab_size, hidden_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(H, V).astype('f')
# 계층 생성
self.in_layer0 = MatMul(W_in)
self.in_layer1 = MatMul(W_in)
self.out_layer = MatMul(W_out)
self.loss_layer = SoftmaxWithLoss()
# 모든 가중치와 기울기를 리스트에 모으기
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
# 인스턴스 변수에 단어의 분산 표현 저장
self.word_vecs = W_in
def forward(self, contexts, target):
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):
ds = self.loss_layer.backward(dout)
da = self.out_layer.backward(ds)
da *= 0.5
self.in_layer1.backward(da)
self.in_layer0.backward(da)
return None
- 학습코드 구현
from common.trainer import Trainer
from common.optimizer import Adam
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()
trainer = Trainer(model, optimizer)
trainer.fit(contexts, target, max_epoch, batch_size)
trainer.plot()
word_vecs = model.word_vecs
for word_id, word in id_to_word.items():
print(word, word_vecs[word_id])
- word2vec 보충
- 동시확률: A와 B가 동시에 일어날 확률
- 사후확률: 사건이 일어난 후의 확률
- 타깃이 \( W_{t} \)가 될 확률은 수식으로
\( P(w_{t}|w_{t-1}, w_{t+1}) \) - 손실함수도 간결하게 표현 가능
\( L=-logP(w_{t}|w_{t-1}, w_{t+1}) \) - 말 뭉치 전체로 확장
\( L=-\frac{1}{T}\sum_{t=1}^{T}logP(w_{t}|w_{t-1}, w_{t+1}) \)
- skip-gram 모델
- Word2vec은 CBOW 모델, skip-gram 모델 두 개 제안
- skip-gram모델은 CBOW에서 다루는 맥락과 타겟을 역전시킨 모델
- 입력층은 하나, 출력의 수는 맥락의 수만큼 존재
- 개별 손실들을 모두 더한 값이 최종 손실이 됨
- skip-gram 모델의 확률 표기
- \( W_{t} \)가 주어졌을 때, \( W_{t-1} \)과 \( W_{t+1} \)이 동시에 일어날 확률
\( P(w_{t-1}, w_{t+1}|w_{t}) \) - 조건부 독립
\( P(w_{t-1}, w_{t+1}|w_{t})=P(w_{t-1}|w_{t})P(w_{t+1}|w_{t}) \) - skip-gram 모델의 손실 함수
\( L=-logP(w_{t-1}, w_{t+1}|w_{t} \\ \quad =-logP(w_{t-1}|w_{t})P(w_{t+1}|w_{t}) \\ \quad =-(logP(w_{t-1}|w_{t})+logP(w_{t+1}|w_{t})) \) - 말뭉치 전체의 손실함수
\( L=-\frac{1}{T} \sum_{t=1}^{T}(logP(w_{t-1}|w_{t})+logP(w_{t+1}|w_{t})) \)
- \( W_{t} \)가 주어졌을 때, \( W_{t-1} \)과 \( W_{t+1} \)이 동시에 일어날 확률
- CBOW모델과 SKip-gram모델을 비교했을 때,
단어 분산 표현의 정밀도 면에서 skip-gram모델이 유리- 말뭉치가 커질수록 성능이 뛰어남
- 학습속도는 CBOW모델보다 느림
3. Word2Vec 속도 개선
- word2vec의 문제점: 말뭉치에 포함된 어휘 수가 많아지면 계산량도 커짐
- word2vec의 개선방향
- Embedding이라는 새로운 개층 도입
- 네거티브 샘플링이라는 새로운 손실함수 도입
- word2vec 개선 1
- 입력측 가중치(W_in)와의 행렬곱으로 은닉층 계산
- 출력측 가중치(W_out)와의 행렬곱으로 각 단어의 점수 구함
- softmax 함수를 적용해 단어의 확률을 얻어 손실 구함
- CBOW 모델의 문제점
- 입력층의 원-핫 표현과 가중치 행렬(W_in)의 곱 계산
→ Embedding 계층을 도입해서 해결 - 은닉층과 가중치 행렬(W_out)의 곱 및 softmax 계층의 계산
→ 네거티브 샘플링을 도입해서 해결
- 입력층의 원-핫 표현과 가중치 행렬(W_in)의 곱 계산
- Embedding 계층
- 맥락 c와 가중치 W의 곱으로 해당 위치의 행 벡터 추출 → 결과적으로 행렬의 특정 행을 추출하는 것
import numpy as np
W = np.arange(21).reshape(7, 3)
W
# 출력 결과
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14],
[15, 16, 17],
[18, 19, 20]])
W[0]
# 출력 결과
array([0, 1, 2])
W[2]
# 출력 결과
array([6, 7, 8])
idx = np.array([1, 0, 3, 0])
W[idx]
# 출력 결과
array([[ 3, 4, 5],
[ 0, 1, 2],
[ 9, 10, 11],
[ 0, 1, 2]])
- Embedding의 순전파, 역전파 구현
- Embedding 계층 backward의 문제점
- 아래 그림에서 첫번째와 세번째가 중복으로 0번 인덱스에 들어가야 하는데 어떻게 처리해야 하나
→ 중복문제 해결을 위해, 할당이 아닌 '더하기'를 해주어야 함
- 아래 그림에서 첫번째와 세번째가 중복으로 0번 인덱스에 들어가야 하는데 어떻게 처리해야 하나
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
# backward의 중복 문제 해결을 위해 더하기를 해줌
np.add.at(dW, self.idx, dout)
# 혹은 다음과 같은 for문 사용
# for i, word_id in enumerate(self.idx):
# dW[word_id] += dout[i]
return None
- word2vec 개선 2
- 은닉층 이후에 계산이 오래 걸리는 곳(은닉측의 뉴런과 가중치 행렬(W_out)의 곱 / softmax 계층의 계산)
(\( y_{k}=\frac{exp(s_{k}}{\sum_{i=1}^{1000000}exp(s_{i})} \))
- 다중 분류에서 이진 분류로(네거티브 샘플링)
- 출력층의 가중치 W_out에서는 각 단어 ID의 단어 벡터가 열로 저장되어 있음
- 아래의 예에서 'say'에 해당하는 단어 벡터를 추출
→ 그 벡터와 은닉층 뉴런과의 내적을 구함
다중 분류 | 이진 분류 | |
출력층 | softmax | sigmoid |
손실 함수 | cross entropy error | cross entropy error |
- 시그모이드 함수와 교차 엔트로피 오차
- 시그모이드 함수
\( y=\frac{1}{1+exp(-x)} \) - 교차 엔트로피 오차
\( L=-(tlogy+(1-t)log(1-y)) \)
t가 1이면 정답은 'Yes' → -log y 출력
t가 1이면 정답은 'No' → -log (1-y) 출력
- 시그모이드 함수
- 정답 레이블이 1이라면 y가 1(100%)에 가까워질수록 오차가 줄어듦
반대로, y가 1로부터 멀어지면 오차가 커짐 - 오차가 크면 '크게' 학습하고, 오차가 작으면 '작게' 학습하게 됨
- 다중 분류 구현
- 이진 분류 구현
- 은닉층 뉴런 h와 출력층의 가중치 W_out에서 'say'에 해당하는 단어 벡터와의 내적을 계산
- 출력을 Sigmoid with Loss 계층에 입력해 최종 손실을 얻음
- Embedding Dot 계층을 도입
class EmbeddingDot:
def __init__(self, W):
self.embed = Embedding(W)
self.params = self.embed.params # 매개변수 저장
self.grads = self.embed.grads # 기울기 저장
self.cache = None
def forward(self, h, idx):
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
idx = [0, 3, 1]
embed = Embedding(W)
target_W = embed.forward(idx)
out = np.sum(target_W * h, axis = 1)
target_W
# 출력 결과
array([[ 0, 1, 2],
[ 9, 10, 11],
[ 3, 4, 5]])
out
# 출력 결과
array([0.383, 7.621, 2.796])
- 정답은 1에 가깝게, 오답은 0에 가깝게 만들어야 함
- 부정적인 예에서는 오답이 무엇인지 알 수 없음
- 네거티브 샘플링을 통해 부정적인 예를 전부 학습하지 않고 일부만 샘플링하여 학습
- 긍정적인 예(say): Sigmoid with Loss 계층에 정답 레이블로 '1'을 입력함
- 부정적인 예(hello와 I): Sigmoid with Loss 계층에 정답 레이블로 '0'을 입력함
- 네거티브 샘플링의 샘플링 기법
- 말뭉치에서 각 단어의 출현 횟수를 구해 '확률분포'로 샘플링
import numpy as np
# 무작위 샘플링
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)
- \( P'(w_{i}-\frac{P(w_{i})^0.75}{\sum_{j}^{n}P(w_{j})^0.75} \)
- 기본 확률분포에 0.75를 제곱하는 방법을 추천
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)
# 출력 결과
[0.642 0.332 0.027]
- UnigramSampler: 네거티브 샘플링 말뭉치에서 단어의 확률분포를 만들고, 0.75 제곱한 후 np.random.choice()를 사용해 부정적 예를 샘플링하는 클래스
# UnigramSampler 클래스
import collections
GPU=False
class UnigramSampler:
def __init__(self, corpus, power, 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) # 전체 합이 1이 되도록 확률 설정
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() # 정답 부분의 확률을 0으로 만들어 버렸으므로 남은 확률들의 합이 1이되도록 하기 위함
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])
negative_sample = sampler.get_negative_sample(target)
print(negative_sample)
# 출력 결과
[[2 3]
[0 2]
[1 3]]
- 네거티브 샘플링 손실함수 클래스 구현
from common.layers import SigmoidWithLoss
class NegativeSamplingLoss:
def __init__(self, W, corpus, power = 0.75, sample_size = 5):
self.sample_size = sample_size
self.sampler = UnigramSampler(corpus, power, sample_size)
self.loss_layers = [SigmoidWithLoss for _ in range(sample_size + 1)]
self.embed_dot_layers = [EmbeddingDot(W) for _ in range(sample_size + 1)]
self.params, self.grads = [], []
for layer in self.embed_dot_layers:
self.params += layer.params
self.grads += layer.grads
def forward(self, h, target):
batch_size = target.shape[0]
negative_sample = self.sampler.get_negative_sample(target)
# 긍정적인 예 순전파
score = self.embed_dot_layers[0].forward(h, target)
correct_label = np.ones(batch_size, dtype = np.int32)
loss = self.loss_layers[0].forward(score, correct_label)
# 부정적인 예 순전파
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 I0, I1 in zip(self.loss_layers, self.embed_dot_layers):
dscore = I0.backward(dout)
dh += I1.backward(dscore)
return dh
- 개선된 word2vec 학습
import numpy as np
class CBOW:
def __init__(self, vocab_size, hidden_size, window_size, corpus):
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)
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):
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
# CBOW 모델 학습
from common import config
import pickle
from common.trainer import Trainer
from common.optimizer import Adam
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)
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'
with open(pkl_file, 'wb') as f:
pickle.dump(params, f, -1)
- word2vec의 단어 분산 표현을 사용하면 유추문제를 덧셈과 뺄셈으로 풀 수 있음
- king → ?에서 ?를 man → woman의 관계로부터 유추
- analogy 함수 사용
# 유추 문제
from common.util import analogy
pkl_file = 'cbow_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']
# 가장 비슷한 단어 뽑기
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)
- 자연어 처리 분야에서 단어의 분산 표현이 중요한 이유
- 전이 학습 가능
- 단어를 고정 길이 벡터로 변환해줌(bag of words)
- word2vec을 사용한 애플리케이션의 예
- 메일 자동 분류 시스템 만들기
- 데이터(메일) 수집
- 수동으로 레이블링 작업(3단계의 감정을 나타내는 레이블)
- 학습된 word2vec을 이용해 메일을 벡터로 변환
- 감정분석을 수행하는 어떤 분류 시스템(SVM이나 신경망 등)에 벡터화된 메일과 감정 레이블을 입력하여 학습 수행
- 단어 벡터 평가 방법
- 모델에 따라 정확도가 다름(말뭉치에 따라 적합한 모델 선택
- 일반적으로 말뭉치가 클수록 결과가 좋음(항상 데이터가 많은 게 좋음)
- 단어 벡터 차원 수는 적당한 크기가 좋음(너무 커도 정확도가 나빠짐)
- 정리
- Embedding 계층은 단어의 분산 표현을 담고 있으며, 순전파 시 지정한 단어 ID의 벡터를 추출
- Word2vec은 어휘 수 증가에 비례항 계산량도 증가하므로, 근사치로 계산하는 빠른 기법을 사용하면 좋음
- 네거티브 샘플링은 부정적 예를 몇 개 샘플링한느 기법으로, 이를 이요하면 다중 분류를 이진 분류처럼 취급
- word2vec으로 얻은 단어의 분산 표현에는 단어의 의미가 녹아들어 있으며, 비슷한 맥락에서 사용되는 단어는 벡터 공간에서 가까이 위치함
- word2vec은 전이 학습 측면에서 특히 중요하며, 그 단어의 분산 표현은 다양한 자연어 처리 작업에 이용할 수 있음
'Python > Deep Learning' 카테고리의 다른 글
[딥러닝-텐서플로우] 텐서플로우 기초 (0) | 2023.04.25 |
---|---|
[딥러닝 기초] RNN(순환신경망) (0) | 2023.03.27 |
[딥러닝 기초] CNN(합성곱 신경망)(2) (0) | 2023.03.23 |
[딥러닝 기초] CNN(합성곱 신경망)(1) (0) | 2023.03.23 |
[딥러닝 기초] 딥러닝 학습 기술 (2) (0) | 2023.03.22 |