트랜스포머의 하이파라미터, 포지셔널 인코딩, 입력 이해

미남잉·2021년 9월 6일
0

참고 자료 출처: 딥러닝을 이용한 자연어 처리 입문

트랜스포머와 인코더-디코더, 트랜스포머의 인코더-디코더에 대한 간단한 설명인 여기로 가주세요!


목차

  1. 기존 seq2seq 모델의 한계
  2. 트랜스포머(Transformer)의 주요 하이퍼파라미터
  3. 트랜스포머(Transformer)
  4. 포지셔널 인코딩(Positional Encoding)
  5. 코드로 구현

기존 seq2seq 모델의 한계

트랜스포머에 대해 배우기 전 seq2seq를 배웠던 것을 기억하시나요? 기존 seq2seq 모델은 인코더-디코더 구조로 구성되어 있습니다. 여기서 인코더는 입력 시퀀스를 하나의 벡터 표현으로 압축하고, 디코더는 이 벡터 표현을 통해 출럭 시퀀스를 만들어 냅니다.

하지만 이 구조는 인코더가 입력 시퀀스를 하나의 벡터로 압축하는 과정에서 입력 시퀀스의 정보가 일부 손실된다는 단점이 있습니다!

이를 보정하기 위해 어텐션이 사용되었습니다. 어텐션을 RNN의 보정을 위한 용도가 아닌 어텐션으로 인코더와 디코더를 만들면 어떨까요?

이 아이디어가 트랜스포머(Transformer)입니다!


트랜스포머(Transformer)의 주요 하이퍼파라미터

트랜스포머(Transformer)의 하이퍼파라미터를 먼저 정의합니다. 아래에서 보여주는 수치는 트랜스포머를 제안한 논문에서 사용한 수치값으로 하이퍼파라미터는 사용자가 모델 설계시 임의로 변경할 수 있는 값입니다.

  • d_model = 512
    트랜스포머의 인코더와 디코더의 정해진 입력과 출력의 크기를 의미합니다. 임베딩 벡타의 차원 또한 d_model입니다. 각 인코더와 디코더가 다음 층의 인코더와 디코더로 값을 보낼 때도 이 차원을 유지합니다.

  • num_layers = 6
    트랜스포머에서 하나의 인코더와 디코더를 층으로 생각했을 때, 트랜스포머 모델에서 인코더와 디코더가 총 몇 층으로 구성되었는지를 의미합니다. 논문에서는 인코더와 디코더를 각각 6개 쌓았습니다.

  • num_heads = 8
    트랜스포머에서는 어텐션을 사용할 때, 1번 하는 것보더 여러 개로 분할해서 병렬로 어텐션을 수행합니다. 그리고 결괏값을 다시 하나로 합치는 방식인데요. 이때 병렬의 개수를 의미합니다.

  • d_ff = 2048
    트랜스포머 내부에는 피드 포워드 신경망이 존재합니다. 이때 은닉층의 크기를 의미합니다. 피드 포워드 신경망의 입력층과 출력층의 크기는 d_model입니다.


트랜스포머(Transformer)

자연어 처리 모델은 텍스트 문장을 입력 받으면 단어를 임베딩 벡터로 변환하는 벡터화 과정을 거칩니다. 트랜스포머도 그 점에서는 다른 모델들과 다르지 않습니다.

하지만 트랜스포머 모델의 입력 데이터 처리는 기존에 많이 사용되는 RNN 모델과 차이점이 하나 있습니다. 임베딩 벡터에 어떤 값을 더해준 뒤 입력으로 사용합니다. 그걸 '포지셔널 인코딩(positional Encoding)'에 해당하는 부분입니다.


포지셔널 인코딩(Positional Encoding)

트랜스포머의 내부를 이해하기 위해서는 트랜스포머의 입력에 대해 알아야 합니다. RNN이 자연어 처리에 유용했던 이유는 단어의 위치에 따라 단어를 순차적으로 입력받아서 처리하는 RNN의 특성으로 각 단어의 위치 정보(position information)를 가질 수 있다는 점에 있었습니다.

하지만 트랜스포머는 단어 입력을 순차적으로 받는 방식이 아닙니다. 단어의 위치 정보를 다른 방식으로 알려줄 필요가 있었고, 트랜스포머는 단어의 위치 정보를 얻기 위해서 각 단어의 임베딩 벡터에 위치 정보들을 더하여 모델의 입력으로 사용하는데, 이를 포지셔널 인코딩이라고 합니다.

입력으로 사용되는 임베딩 벡터들이 트랜스포머의 입력으로 사용되기 전 포지셔널 인코딩 값이 더해지는 것을 보여줍니다. 임베딩 벡터가 인코더의 입력으로 사용하기 전에 포지셔널 인코딩 값이 더해지는 과정을 시각화하면 위와 같습니다.

이렇게 해주는 이유는 트랜스포머는 입력을 받을 때, 문장에 있는 단어들을 1개씩 순차적으로 받는 것이 아니라, 문장에 있는 모든 단어를 한꺼번에 입력으로 받기 때문입니다. 트랜스포머가 RNN과 결정적으로 다른 점이 바로 이 부분입니다.

RNN은 어차피 문장을 구성하는 단어들이 어순 대로 모델에 입력되므로, 모델에게 따로 어순 정보를 알려줄 필요가 없습니다. 그러나 문장에 있는 모든 단어를 한 꺼 번에 문장 단위로 입력 받는 트랜스포머는 문장 속 단어의 위치가 바뀌면 구분 못 할 수도 있습니다.

그래서 그 단어가 문장의 몇 번째 어순에 위치하는 지를 모델에 추가로 알려주기 위해 단어의 임베딩 벡터에다가 위치 정보를 가진 벡터 값을 더해 모델의 입력으로 받습니다.

트랜스정보는 위치 정보를 가진 값을 만들기 위해 위의 함수를 사용합니다. 사인, 코사인 함수의 그래프를 생각해보면 요동치는 값의 형태를 생각해 볼 수 있는데, 트랜스포머는 사인, 코사인 함수의 값을 임베딩 벡터에 더해주며 단어의 순서 정보를 더해줍니다.

위의 함수 식에서

이와 같은 생소한 변수가 있습니다. 이 함수를 이해하려면 임베딩 벡터와 포지셔널 인코딩의 덧셈은 임베딩 벡터가 모여 만들어진 문장 벡터 행렬과 포지셔널 인코딩 행렬의 덧셈 연산을 통해 이루어진다는 점을 이해해야 합니다.

pos는 입력 문장에서 임베딩 벡터의 위치를 나타냅니다. i는 임베딩 벡터 내의 차원의 인덱스를 의미합니다. 위의 식에 따르면 임베딩 벡터 내의 각 차원의 인덱스가 짝수인 경우에는 사인 함수, 홀수인 경우에는 코사인 함수의 값을 사용합니다.

  • (pos, 2i)이면 사인 함수
  • (pos, 2i+1)이면 코사인 함수

d_model은 트랜스포머의 모든 층의 출력 차원을 의미하는 트랜스포머의 하이퍼파라미터입니다. 임베딩 벡터 또한 d_model의 차원을 가집니다.

위와 같은 포지셔널 인코딩 방법을 사용하면 순서 정보가 보존됩니다.

예를 들어, 각 임베딩 벡터에 포지셔널 인코딩값을 더하면 같은 단어라고 하더라도 문장 내의 위치에 따라 임베딩 벡터의 값이 달라집니다.

즉, 트랜스포머의 입력은 순서 정보가 고려된 임베딩 벡터입니다.

Q1. 한 문장에 같은 단어 A가 여러 번 등장하였다고 가정해보겠습니다. 임베딩 문장 행렬에 포지셔널 인코딩을 해주었을 때와 해주지 않았을 때, 트랜스포머가 임베딩 문장 행렬 내의 다수의 A 단어 벡터로부터 얻을 수 있는 정보의 차이는 어떤 것이 있을까요?

  • 같은 단어라고 하더라도 포지셔널 인코딩을 해준 경우엔, 임베딩 벡터값이 달라집니다. 그러므로 같은 단어라고 해도 각각 다른 위치에 등장했다는 사실을 모델에 알려줄 수 있습니다.

코드로 구현


필요한 패키지 import

import tensorflow as tf
import tensorflow_datasets as tfds
import os
import re
import numpy as np
import matplotlib.pyplot as plt


트랜스포머의 입력

class PositionalEncoding(tf.keras.layers.Layer):

def __init__(self, position, d_model):
    super(PositionalEncoding, self).__init__()
    self.pos_encoding = self.positional_encoding(position, d_model)

def get_angles(self, position, i, d_model):
    angles = 1 / tf.pow(
        10000, (2*(i//2)) / tf.cast(d_model, tf.float32)
    )
    return position * angles

def positional_encoding(self, position, d_model):
    angle_rads = self.get_angles(
        position = tf.range(position, dtype=tf.float32)[:, tf.newaxis], 
        i = tf.range(d_model,dtype=tf.float32)[tf.newaxis,:],
        d_model = d_model
    )
    
    sines = tf.math.sin(angle_rads[:, 0::2])
    cosines = tf.math.cos(angle_rads[:, 1::2])

    angle_rads = np.zeros(angle_rads.shape)
    angle_rads[:, 0::2] = sines
    angle_rads[:, 1::2] = cosines
    pos_encoding = tf.constant(angle_rads)
    pos_encoding = pos_encoding[tf.newaxis, ...]
    
    print(pos_encoding.shape)
    return tf.cast(pos_encoding, tf.float32)

def call(self, inputs):
    return inputs + self.post_encoding[:, :tf.shape(inputs)[1], :]

50 × 128의 크기를 가지는 포지셔널 인코딩 행렬을 시각화하여 어떤 형태를 가지는지 확인해봅시다. 이는 입력 문장의 단어가 50개이면서, 각 단어가 128차원의 임베딩 벡터를 가질 때 사용할 수 있는 행렬입니다.


# 문장의 길이 50, 임베딩 벡터의 차원 128
sample_pos_encoding = PositionalEncoding(50, 128)

plt.pcolormesh(sample_pos_encoding.pos_encoding.numpy()[0], cmap='RdBu')
plt.xlabel('Depth')
plt.xlim((0,128))
plt.ylabel('Position')
plt.colorbar()
plt.show()
(1, 50, 128)



Q2. 임베딩 벡터의 차원이 256이고 최대 문장의 길이가 30인 텍스트를 입력으로 하는 트랜스포머를 구현한다고 하였을 때, 적절한 포지셔널 인코딩 행렬의 크기를 추측해보고 위에 구현한 포지셔널 인코딩 레이어를 사용해 표현해 보세요.

  • (1, 30, 256)
  • 위의 코드에서 50, 512 대신 30, 256을 입력으로 하여 행렬을 만들면 정답입니다. 즉 PositionalEncoding(30,256)로 표현할 수 있습니다.

논문 - 포지셔널 인코딩 표현

실제 논문에서 제시된 그림에서는 다음과 같이 포지셔널 인코딩을 표현하였습니다.

profile
Tistory로 이사갔어요

2개의 댓글

comment-user-thumbnail
2022년 2월 25일

글 참고하다가 PositionalEncoding 클래스에서 빠진 부분이 있어 다른데서 찾아보았습니다!

class PositionalEncoding(tf.keras.layers.Layer):

def __init__(self, position, d_model):
    super(PositionalEncoding, self).__init__()
    self.pos_encoding = self.positional_encoding(position, d_model)

def get_angles(self, position, i, d_model):
    angles = 1 / tf.pow(
        10000, (2*(i//2)) / tf.cast(d_model, tf.float32)
    )
    return position * angles

def positional_encoding(self, position, d_model):
    angle_rads = self.get_angles(
        position = tf.range(position, dtype=tf.float32)[:, tf.newaxis], 
        i = tf.range(d_model,dtype=tf.float32)[tf.newaxis,:],
        d_model = d_model
    )
    
    sines = tf.math.sin(angle_rads[:, 0::2])
    cosines = tf.math.cos(angle_rads[:, 1::2])

    angle_rads = np.zeros(angle_rads.shape)
    angle_rads[:, 0::2] = sines
    angle_rads[:, 1::2] = cosines
    pos_encoding = tf.constant(angle_rads)
    pos_encoding = pos_encoding[tf.newaxis, ...]
    
    print(pos_encoding.shape)
    return tf.cast(pos_encoding, tf.float32)

def call(self, inputs):
    return inputs + self.post_encoding[:, :tf.shape(inputs)[1], :]
1개의 답글