ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [밑시딥2] word2vec
    A.I/Study 2023. 6. 26. 03:17

    추론 기반 기법과 신경망

    지금까지 연구되어온 단어를 벡터로 표현하는 방법 중 가장 성공적인 기법은 '통계 기반 기법'과 '추론 기반 기법'이라고 볼 수 있다.

    통계 기반 기법의 문제점

    이전에 언급된 통계 기반 기법은 주변 단어의 빈도를 기반으로 단어를 표현했다. 구체적인 방법으로는 동시발생 행렬을 만들고 그 행렬에 SVD를 적용하여 밀집벡터로 표현했다. 하지만, 이것은 대규모 말뭉치를 다룰때 문제가 발생한다.

    SVD는 n*n 행렬일 때, O(n^3)의 시간복잡도를 가지기 때문에 HPC 환경이라고 하더라도 비효율 적이다.

    이렇게 통계기반 학습은 학습 데이터를 통으로 한번에 처리하지만, 추론 기반 기법미니배치 학습을 이용하여, 학습 데이터의 일부를 사용하여 순차적으로 학습한다. 데이터를 잘게 나누기 때문에 아무리 코퍼스 규모가 크더라도 효율적으로 학습 시킬 수 있다.

    이는 GPU를 이용한 병렬 계산도 가능하게하여 학습 속도를 증가시킬 수 있다.

    추론 기반 기법 개요

    추론이란 아래의 그림처럼 주변 단어(맥락)이 주어졌을 때 "?"에 들어갈 단어를 추측하는 작업이다.

    추론 작업 예시

    이런 방법으로 추론 문제를 풀고 학습하는 것이 '추론 기반 기법'이 다루는 문제이다. 이 과정을 반복하면서 단어의 출현 패턴을 학습한다.

    신경망에서의 단어 처리

    학습에 사용하기전에 단어를 그대로 사용할 수 없으니 이를 고정 길이의 벡터로 변환해야한다. 이를 위한 방법으로 원핫 벡터(one-hot vector)로 변환하는 방법이 있다.

    원핫 벡터로 표현하는 법

    1. 어휘 수만큼의 원소를 갖는 벡터 준비
    2. 인덱스가 단어 ID와 같은 원소를 1로, 나머지를 0으로 설정

    이를 통해, 단어를 신경망으로 처리할 수 있게 됬다.

    완전 연결 계층 (Full connected layer)를 사용하여 변환하면 다음과 같이 코드를 작성할 수 있다.

    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)

    이 책에서 구현한 완전 연결 계층에서 편향을 생략한 경우 Matmul과 동일하다.

    단순한 word2vec

    word2vec에서 사용되는 모델은 CBOW (continuous bag-of-words) 모델와 Skip-gram 모델 등이 있다.

    CBOW 모델의 추론 처리

    CBOW 모델은 맥락으로부터 타깃(target)을 추측하는 용도의 신경망이다.
    입력으로 맥락(주변 단어 목록)을 받고 이를 통해 분산 표현을 출력한다.

    책에서 사용한 BOW의 대략적인 구조

    입력층이 2개 있고, 은닉층을 거쳐서 출력층에 도달하는 간단한 구조다.
    여기서 입력층이 2개인 이유는 맥락으로 고려할 단어를 2개로 지정했기 때문이다.
    최종 출력층은 각 단어에 대한 스코어를 출력하므로 마지막에 소프트맥스를 적용해야한다.

    은닉층의 경우 뉴런의 수를 입력층의 뉴런 수보다 적게 했는데 이렇게 할 경우 단어 예측에 필요한 정보를 간결하게 담게 되며 결과적으로 밀집백터 표현을 얻을 수 있다.

    이를 코드로 표현하면 다음과 같다.

    # 샘플 맥락 데이터
    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)

    CBOW 모델의 학습

    CBOW 모델의 학습에서는 올바른 예측을 할 수 있도록 가중치를 조정하는 일을 한다.
    다르게 해석하면, 다중 클래스 분류를 수행하는 신경망이라고 볼 수 있다.

    따라서 이 신경망을 학습하려면, 소프트맥스와 크로스 엔트로피 에러만 이용하면 된다.

    word2vec의 가중치와 분산 표현

    위에서 설명한 것처럼 word2vec에 사용되는 신경망에는 두 가지 가중치가 있다. 하나는 입력층에 사용한 W_in이고 또 하나는 출력층에 사용한 W_out이다. 만약 이 상황에서 단어의 분산표현으로는 어느쪽 가중치를 사용할 수 있을까.

    일단 가능한 선택지는 다음과 같다.

    1. 입력층의 가중치만 이용한다.
    2. 출력층의 가중치만 이용한다.
    3. 둘다 이용한다.

    word2vec (특히, skip-gram 모델)은 입력 측의 가중치만 이용하는 것이 대중적인 선택이라고 한다. 
    즉, 출력층의 가중치인 W_out을 버리고 입력층의 W_in만 분산표현으로서 사용한다는 의미이다.

    단, word2vec과 비슷한 GloVe와 같은 기법에서는 두 가중치를 더해서 사용하여 더 좋은 결과를 보여줬다.

    학습 데이터 준비

    맥락과 타깃

    word2vec에서 사용하는 입력은 맥락이다. 그리고 정답 레이블은 맥락 사이에 있는 타깃이다. 
    그리고 이 모델을 통해 해야할 일은 맥락을 입력했을 때, 타깃이 출현할 확률을 높이는 것이라고 볼 수 있다.

    코퍼스가 주어졌을 때, 맥락과 타깃을 만들어 내는 함수를 다음과 같이 구현 할 수 있다.

    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)

    원핫 표현으로 변환

    원핫 표현을 변환하게 되면, 맥락은 두개의 원핫 벡터를 가진 2차원 행렬이 되고, 타깃은 하나의 원핫 벡터가 될 것이다.

    코드로 표현하면 다음과 같다.

    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)

    CBOW 모델 구현

    SimpleCBOW 구조

    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') # 32bit float
            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() # softmax와 CEE
    
            # 모든 가중치와 기울기를 리스트에 모은다.
            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

    학습 코드 구현

    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 보충

    CBOW 모델과 확률

    맥락으로 W_t-1와 W_t+1가 주어지고, 타깃이 W_t가 될 확률은 다움과 같이 표현 가능하다.

    교차 엔트로피 에러를 기반으로 하나의 샘플에 대한 Loss 함수 수식 표현은 아래와 같이 표현 가능하다.

    이를 음의 로그 우도 (Negative log likelihood)라고 한다.

    이를 코퍼스 전체로 확장하면 다음과 같다.

    그냥 시그마를 통해 전체에 적용했다고 생각하면된다.

    skip-gram 모델

    skip-gram은 CBOW에서 맥락과 타깃을 역전시킨 모델이다.

    따라서 모델의 입력층은 하나이고, 출력층은 맥락의 수 만큼 존재한다. 
    따라서 각 출력층에서는 개별적으로 손실을 구하고 이를 모두 더한 값을 최종 손실로 한다.

    일단 확률로 표기하면 다음과 같다.

    모델의 확률 표기는 CBOW에서 타깃과 문맥의 위치만 바꿔서 뒤집으면 된다.
    (위 W_t-1과 W_t+1은 조건부 독립이라고 가정한다.)

    위 식을 아래와 같이 분해할 수 있다.

    이어서 Loss에 적용하면 아래와 같은 방법으로 유도할 수 있다.

    잘 보면 위에서 정의한 확률 함수를 그대로 적용하고 분해했다.
    로그 내에서 두 수의 곱은 밑이 같은 로그들의 합으로 변환가능하기 때문에 최종식이 저런 모습으로 유도됬다.

    이를 확장하여 시그마를 씌우면 이렇게된다.

    CBOW 모델과 skip-gram 모델 중, skip-gram이 더 나은 단어 분산 표현의 정밀도를 보여준다.
    나아가, 코퍼스의 규모가 커질수록 성능이 더 뛰어난 경향이 있다고 한다.

    하지만 학습 속도 측면에서는 Loss를 한번만 계산하기 때문에 CBOW가 더 빠르다.

    통계 기반 vs 추론 기반

    통계 기반 기법은 코퍼스 전체의 통계로 부터 1회 학습하여 단어의 분산 표현을 얻지만, 추론 기법은 미니배치 학습을 이용한다고 언급했었다.

    이 외에 어떻게 다른지 비교해보자면 아래와 같다.

    상황 1: 새 단어가 생겨서 분산 표현을 갱신해야하는 상황

    • 통계 기반 기법: 처음부터 다시 해야됨
    • 추론 기반 기법: 파인 튜닝으로 갱신

    상황 2: 분산 표현의 성격과 정밀도

    • 통계 기반 기법: 주로 단어의 유사성이 인코딩됨
    • 추론 기반 기법: 단어의 유사성과 복잡한 단어 사이의 패턴까지 파악 및 인코딩됨

    하지만, 이것이 추론 기반 기법이 더 우세하다는 의미는 아니다.

    또한 서로 관련되어 있다고도 볼 수 있는데, skip-gram의 경우 코퍼스 전체의 동시발생 행렬에 특수한 행렬 분해를 적용한 것과 같아 통계 기반 기법과 유사하다고 볼 수 있다.

    또한 GloVe의 경우 두 기법을 융합하여 더 나은 성능을 보여주었다.

    레퍼런스

    밑바닥부터 시작하는 딥러닝 2 (사이토 고키 지음, 개앞맵시 옮김)

    개앞맵시님의 밑시딥2 저장소: WegraLee/deep-learning-from-scratch-2: 『밑바닥부터 시작하는 딥러닝 ❷』(한빛미디어, 2019) (github.com)

Designed by Tistory.