elevne's Study Note

NLP 공부 (5-1: Transformer) 본문

Machine Learning/NLP

NLP 공부 (5-1: Transformer)

elevne 2022. 10. 23. 12:18

Seq2Seq는 이전 글에서 언급한 것처럼 모델의 핵심적인 역할을 하는 Context Vector가 일종의 Bottle Neck 작용을 하여 성능적인 한계가 있는 모델이다. 이에 대한 대안으로 Transformer이라는 모델이 등장하였고 이는 현재에도 널리 사용되고 있는 모델이라고 한다. Transformer는 RNN을 사용하지 않지만, 이 또한 Seq2Seq와 마찬가지로 Encoder과 Decoder이 있다. Encoder에서 입력한 문장에 대한 정보와 Decoder에 입력한 문장 정보를 조합해서 Decoder 문장 다음에 나올 단어를 예측한다고 한다. 

 

 

 

Transformer에서는 Self-Attention이라는 것이 사용된다. Self-Attention이란 문장에서 각 단어 간 얼마나 관계가 있는지를 계산해서 반영하는 방법이다. 이 관계의 정도를 Attention-Score로 표현하는데, 데이터의 모든 단어에 대해 단어 간 Attention-score를 구해서 이를 하나의 테이블로 만든 것을 Attention-Map이라고 한다. 이 Attention map을 구축하기 위해 우선 텍스트의 의미 정보를 벡터로 표현한 후, 각 단어 벡터 간 내적을 통해 Attention score를 구하는 것이다. 

 

 

 

Attention Map이 준비되면, Map에 있는 Attention Score를 활용하여 문장에 있는 모든 단어에 대한 Context Vector를 구할 수 있다. 단어별로 돌아가면서 그 특정 단어에 대한 Attention Score를 문장의 각 단어에 부여하고 여기에 Softmax를 적용하여 Score를 확률값으로 표현한다. 그 후, 확률값으로 표현된 Attention Score과 각 단어벡터를 곱한 후, 모든 벡터를 더하면 선택한 단어의 Context Vector가 생성이 되는 것이다. 이 과정을 문장의 모든 단어에 대해서 반복하는 것이다.

 

 

 

 

Transformer

 

 

 

위 그림은 앞으로 구현해볼 Transformer의 전체 구조이다. 위 그림을 보게되면 우선 Multi-Head Attention이라는 것이 가장 많이 나오는 것을 확인할 수 있다. Multi-Head Attention이란 Scaled Dot-Product Attention이 중첩된 형태라고 한다. 그럼 여기서 또 Scaled Dot-Product Attention에 대해 알아보아야 했다.

 

 

 

Scaled Dot-Product Attention에서는 위에서 Self-Attention을 진행했던 것과 같이 특정 단어에 대해 다른 모든 단어와 내적을 하고 어텐션 맵을 구축하고 단어에 대한 Context Vector를 생성한다. 그 후에는 Self Attention과는 다르게, 내적한 값이 벡터의 차원이 커질수록 학습이 잘 안될 수 있기에, 벡터 크기에 반비례하도록 크기를 조정하는 과정을 거치게된다.

 

 

 

Scaled Dot Product Attention

 

 

 

Scaled Dot Product Attention은 아래와 같은 코드로 작성할 수 있다.

 

 

 

def scaledDotProductAttention(query, key, value):
  qk = tf.matmul(query, key, transpose_b=True)
  dk = tf.cast(tf.shape(key)[-1], tf.float32)
  scaled = qk / tf.math.sqrt(dk)
  attention_weights = tf.nn.softmax(scaled, axis=-1)
  result = tf.matmul(attention_weights, value)
  return attention_weights, result

 

 

 

Query, 즉 선택 단어벡터와 각 단어벡터를 곱해주기 위해 tf.matmul 함수에 transpose_b=True로 지정하여 Key 대신 Key의 전치행렬과 곱을 진행할 수 있게끔 하였다. 또, -1로 지정해준 softmax의 axis 값은 마지막차원을 지정한다는 뜻으로 Default 값으로 설정되어 있는 값이다.

 

 

 

또, Attention에는 Mask가 적용될 수 있는데 이를 위해서는 Subsequent Masked Attention에 대해 알아보아야 한다. RNN에서는 단어의 위치에 따라 단어를 순차적으로 입력받기에 단어의 위치 정보를 가질 수 있다는 장점이 있었지만, 위의 Attention 방식은 그렇지 않기에 다른 방식으로 단어의 위치정보를 반영할 필요가 있다. 이 때 사용될 수 있는 것이 Mask이고, 이 Mask를 적용해서 자신보다 뒤에 있는 단어를 참고하지 않도록 하는 것이다. 자기보다 뒤에 있는 관계정보를 보이지 않게 함으로써 이를 적용할 수 있다. 다음과 같은 코드로 마스킹을 적용할 수 있다.

 

 

 

def subsequentMask(size):
  mask = 1 - tf.linalg.band_part(tf.ones((size, size)), -1, 0)
  return mask

 

 

 

위 band_part 함수는 하, 상삼각행렬과 대각행렬을 만들 수 있는 함수이다. tf.ones에 동일한 size값을 두 번 넣어주고, 뒤에 두 개의 인자를 받게된다. 뒤의 두 수가 (-1,0) 순이면 하삼각행렬, (0,-1)이면 상삼각행렬, (0,0)이면 대각행렬이 생성되게 하는 함수이다. 이를 적용하여 위에 적은 scaledDotProductAttention 함수에 Mask를 적용할 수 있다. 다음과 같이 추가해주면 될 것이다.

 

 

 

def scaledDotProductAttention(query, key, value, mask):
  qk = tf.matmul(query, key, transpose_b=True)
  dk = tf.cast(tf.shape(key)[-1], tf.float32)
  scaled = qk / tf.math.sqrt(dk)
  if mask is not None:
    scaled += mask*-1e9
  attention_weights = tf.nn.softmax(scaled, axis=-1)
  result = tf.matmul(attention_weights, value)
  return attention_weights, result

 

 

 

위와 같이 처리하게되면 어텐션 맵의 아래쪽 삼각형 부분을 제외한 곳은 모두 매우 작은 음수값을 지니게 될 것이다. Softmax에 매우 작은 음수값을 넣었을 때는 0에 수렴하는 값이 나올 것이기에 마스킹된 부분은 참조되지 않게 될 것이다. Scaled Dot Product에 Mask까지 적용해보았으니 이를 중첩하여 사용한 Multi-Head Attention에 대해 알아볼 차례이다.

 

 

 

Multi-Head Attention

 

 

 

Self-Attention을 한 번만 적용하여 모델 학습을 진행하는 것도 물론 가능하지만 위 Transformer 모델에서는 Query, Key, Value에 대한 특징값을 헤드 수만큼 나눠서 Linear Layer(Dense)를 거치게 하고 Scaled Dot Product를 각각 따로 구해 다시 합치는 과정을 거친다. 이 과정에서 Query, Key, Value 벡터로 만들 입력은 동일하게 주어지지만, 이를 벡터로 선형변환 시킬 때 사용하는 파라티터를 각 Head 별로 다르게 만들어 학습한다고 한다. 이 합친 값을 다시 Linear Layer를 거치게하면 Multi-Head Attention 값이 도출되게 된다.

 

 

 

이러한 일련의 나누고 합치는 과정을 거치지 않아도 Multi-Head Attention을 사용한 것과 같은 결과가 나온다고 한다. 그럼에도 불구하고 복잡하게 이를 사용하는 이유는 여러 개의 병렬연산을 동시에 할 수 있게끔 하여 속도적 우위를 가져가기 위함이라고 한다.

 

 

 

class MultiHeadAttention(tf.keras.layers.Layer):
  def __init__(self, **kwargs):
    super(MultiHeadAttention, self).__init__()
    self.numHeads = kwargs["numHeads"]
    self.dModel = kwargs["dModel"]
    assert self.dModel % self.numHeads == 0
    self.depth = self.dModel // self.numHeads
    self.wq = tf.keras.layers.Dense(self.dModel)
    self.wk = tf.keras.layers.Dense(self.dModel)
    self.wv = tf.keras.layers.Dense(self.dModel)
    self.dense = tf.keras.layers.Dense(self.dModel)
  def split_heads(self, data, batchSize):
    data = tf.reshape(data, (batchSize, -1, self.numHeads, self.depth))
    return tf.transpose(data, perm=[0, 2, 1, 3])
  def call(self, q, k, v, mask):
    batchSize = tf.shape(q)[0]
    q = self.wq(q)
    k = self.wk(k)
    v = self.wv(v)
    q = self.split_heads(q, batchSize)
    k = self.split_heads(k, batchSize)
    v = self.split_heads(v, batchSize)
    attention_weights, result = scaledDotProductAttention(q, k, v, mask)
    result = tf.transpose(result, perm=[0, 2, 1, 3])
    result = tf.reshape(result, (batchSize, -1, self.dModel))
    result = self.dense(result)
    return result, attention_weights

 

 

 

Class init 함수의 인자로 **kwargs를 받았는데 이는 인자를 여러개 넣을 때 사용하는 것이다. 함수의 인자로 * 혹은 ** 과 함께 변수명을 적어줄 때가 있다. (e.g., *args, **kwargs, *names) 우선 * 은 단순히 복수 개의 인자를 함수에 넣으려고 할 때 사용하는건데, 그 인자의 수가 정해져있지 않은 것이다. 몇 개든 함수의 인자로 넣어줄 수 있고 이는 튜플 형태로 함수 내에서 저장되어 사용된다. 이와 다르게 **kwargs는 Keyword Arguments의 줄임말로 키워드를 제공하는 건데, 키워드=특정값 형태로 함수를 호출할 수 있게끔 하는 것이다. *args와 비슷하지만 각기 다른 키워드로 받을 수 있는 점이 차이점이다.

 

 

 

다시 Multi-Head Attention으로 돌아오면, 우선 init 함수에서 어텐션 헤드 수, Dense layer을 지정해준다. Multi-Head Attention에서는 항상 차원 수가 헤드 개수만큼 딱 떨어져 나눠저야 한다고 한다. Assert문을 활용하여 나머지가 0이 아닐 경우 에러가 발생하도록 한다. 함수를 call 했을 때 split_heads 함수를 통해 우선 Head의 개수만큼 Quey, key, value를 분리할 수 있도록 한다. [배치*시퀀스*피처] => [배치*시퀀스*헤드*피처]의 형태로 reshape을 해준 후, 다시 원하는 출력 차원으로 바꾸기 위해 transpose에 perm=[0, 2, 1, 3]으로 주어 시퀀스와 헤드 차원만 교체해준다. 그 후, call 함수에서도 동일한 방식으로 transpose, reshape을 하며 다시 합치는 과정을 거치게된다.

 

 

 

출처:

https://wikidocs.net/31379

https://brunch.co.kr/@princox/180

https://welcome-to-dewy-world.tistory.com/108

https://glanceyes.tistory.com/entry/Transformer%EC%9D%98-Multi-Head-Attention%EA%B3%BC-Transformer%EC%97%90%EC%84%9C-%EC%93%B0%EC%9D%B8-%EB%8B%A4%EC%96%91%ED%95%9C-%EA%B8%B0%EB%B2%95#toc-link-4

https://github.com/NLP-kr/tensorflow-ml-nlp-tf2

'Machine Learning > NLP' 카테고리의 다른 글

NLP 공부 (5-3: Transformer)  (0) 2022.10.28
NLP 공부 (5-2: Transformer)  (0) 2022.10.25
NLP 공부 (4-2: Seq2Seq with Attention)  (0) 2022.10.22
NLP 공부 (4-1: Seq2Seq with Attention)  (0) 2022.10.21
NLP 공부 (3: 감성분석)  (1) 2022.10.18