Open In Colab

트랜스포머가 나오게 된 배경

2017년 'Attention Is All You Need' 논문 이전 자연어 처리 분야는 RNN 기반 딥러닝을 사용했습니다.

RNN은 오래된 데이터를 잘 잊고, 이를 극복하려 나온 LSTM 조차도 장기 의존성 문제가 있습니다.

이는 자연어 처리 분야에서 문장의 길이가 길어지면 앞의 과거 정보가 마지막 시점까지 전달되지 못하는 현상을 유발합니다.

언어는 과거 정보가 중요한 경우가 상당히 많은데 장기 의존성 문제가 있다면 성능이 떨어지겠죠.

이런 단점을 극복하고자 트랜스포머가 등장했습니다.

셀프 어텐션

'A dog are the food because it was hungry'

위 문장에서 it는 dog와 food 중 dog를 의미한다는 것을 사람은 쉽게 알 수 있습니다.

컴퓨터 모델에서 it이 food가 아닌 dog라는 것을 이해시키기 위해선 어떻게 해야할까요.

image.png

문장 내용은 조금 틀리긴 하지만 위 그림을 참고해봅시다.

문장 내 모든 단어가 it 하고 상호작용을 하고 있습니다. 실제 문장이라는게 모든 단어가 연관되어 있으니깐요.

그 중 어느 단어가 it 단어와 상호작용이 강한지 혹은 약한지를 수치로 나타냅니다.

이 수치가 정확하다면 모델이 문장 구조를 이해하는데 큰 도움이 될 것입니다. 이것이 셀프 어텐션의 기초 이론입니다.

셀프 어텐선을 구체적으로 살펴보려고 하기 때문에 예시 문장을 보다 간단하게 I am good 으로 바꾸겠습니다.

예시 문장을 입력받게 되면 컴퓨터는 단어를 숫자로 인식해야 하기 때문에 변환을 해야 합니다.

사용하는 모든 단어를 모아서 원핫 인코딩을 할 순 있겠지만 행렬 사이즈가 너무 커지고 희소행렬이 되는 등 비효율적입니다.

그래서 주로 단어 임베딩을 많이 하는데 이 부분은 다음 시간에 자세히 다루겠습니다.

간단히 임베딩이란 각 단어마다 고유 벡터값이 있는 것을 말합니다. 벡터 길이는 정하기 나름이겠죠.

image.png

셀프 어텐션의 구조를 간단히 표현한 그림입니다. 왼쪽 부분 n은 단어 개수, c는 임베딩 벡터 길이 입니다.

3개의 같은 크기(임베딩 벡터 길이 X 임의에 차원 D) 가중치 행렬이 나옵니다.

단어 임베딩 행렬과 3개의 가중치 행렬을 행렬곱 해서 3개의 새로운 행렬(단어 길이(3) X 임의에 차원 D)을 만듭니다.

행렬은 각각 Q(쿼리), K(키), V(밸류) 행렬로 지칭합니다. 3개의 행렬 모두 행 기준으로 단어와 연관된 값이 순차적으로 저장될 것입니다.

예시를 통해 설명하면 단어 임베딩 행렬에서 1행 I, 2행 am, 3행 good 단어가 임베딩되있는데 출력된 3개의 행렬 또한 행 기준으로 1행 I, 2행 am, 3행 good 단어와 관련된 정보가 기록되어 있을 것 입니다.

image.png

구한 쿼리(Q) 행렬과 키(K) 행렬의 전치 행렬을 행렬곱합니다. 행렬 크기를 먼저 살펴보면 (단어수 n 임의에 차원 D) (임의에 차원 D 단어수 n) = (단어수 n 단어수 n)이 나옵니다.

여기서 쿼리(Q) 행렬은 현재 시점의 단어를 의미하고 키(K) 행렬은 attention을 구하고자 하는 대상 단어를 의미합니다.

예시문장으로 설명하면 Q 1행, 즉 I 단어를 사용한다고 합시다. 이 I단어를 기준으로 Q 1행이 K 1행과 내적한다면 I와 관계, K 2행과 내적한다면 am 단어와 관계, K 3행과 내적한다면 good 단어와 관계를 나타내는 수치가 나옵니다.

이런식으로 한 단어를 기준으로 문장 내 모든 단어와의 관계가 얼마나 깊은지를 나타내는 수치를 구하는 행동을 모든 단어에서 수행합니다.

간단하게 설명하면 Q * T(K) 행렬에 결과로 1행 값은 I단어가 자신 포함 모든 단어와의 관련성 수치를 나타냅니다.

image.png

앞서 구한 Q * T(K) 행렬내 모든 값을 가중치 행렬에서 사용한 임의에 차원(D)의 제곱근 값으로 나눠줍니다.

이 행위에 목적은 안정적인 경사값을 얻기 위함입니다.

다음으로 소프트맥스 함수를 적용시킵니다. 이렇게 되면 임의에 행 백터의 합을 1로 만들 수 있습니다.

이는 해석이 상당히 용이해지는데요. 예시로 첫 행을 보면 단어 I는 자기자신과 몇 퍼센트, am 단어와 몇 퍼센트, good 단어와 몇 퍼센트 연관있는지 구할 수 있습니다.

여기서 몇 퍼센트는 전체 단어 내 몇퍼센트인지를 의미하니 직관적인 이해가 높아집니다.

다음 단계로 지금까지 구한 행렬에 아까 구한 밸류(V) 행렬을 곱해줍니다.

image.png

쓰던 예시문장과 조금 다른임을 감안하고 참고해주세요!

지금까지 구한 행렬의 특정 한 행(it 단어)을 관찰해봅시다. it과 관련이 높다면 큰 값(확률)을 갖고 낮다면 작은 값을 가집니다.

이 값들이 밸류(V) 행렬과 곱하게 되면 관련이 높은 단어는 높은 값과 곱해지고 낮은 단어는 낮은 값만 곱해집니다.

쉽게 얘기해서 관련이 높은 단어의 밸류(V) 행 값은 많이 적용되고, 관련이 낮은 단어의 밸류(V) 행 값은 적게 적용됩니다.

최종 결과는 어텐션 행렬(Z)로 크기는 (단어 개수 N * 임의에 차원 D)이 됩니다. 지금까지 셀프 어텐션에 대해 알아봤습니다.

멀티 헤드 어텐션

image.png

어텐션을 헤드 한 개만 사용한 형태에 대해서 알아봤는데 이를 확장하려고 합니다.

어텐션 결과의 정확도를 높이기 위해 방금 했던 셀프 어텐션을 통해 어텐션 행렬(Z)를 만드는 행위를 여러번 반복합니다.

당연히 Z의 버전을 여러개 만들면 Q, K, V 또한 새로운 버전을 만들어야 합니다. 이를 멀티 헤드 어텐션이라고 합니다.

어텐션 행렬 여러개를 구한 뒤 열 방향으로 연결한 후 새로운 가중치 행렬을 행렬곱하면 최종 출력값이 나오게 됩니다.

이때 어텐션 행렬(Z) 크기는 (단어 개수 N (임의에 차원 D 어텐션 행렬 개수)) 가 됩니다. 여기서 가중치의 크기는 (임의에 차원 어텐션 행렬 개수) 임베딩 백터 길이(c)를 해주기 때문에 최종 출력값의 크기는 (단어 개수 N * 임베딩 백터 길이 c)가 나오게 됩니다.

이 최종 출력값의 크기를 입력 값의 크기와 일치시킨 이유는 이 작업을 여러번 반복하기 위함입니다.

위치 인코딩

image.png

트랜스포머는 앞서 말한것 처럼 RNN 류의 기법을 사용하지 않기 때문에 단어의 순서 정보를 사용하지 않을 수 있습니다.

사실 단어의 순서 정보는 문장의 의미를 명확히 이해하기 위해선 상당히 중요합니다. 이를 위해서 위치 인코딩을 사용합니다.

위치 인코딩은 단어를 임베딩하여 입력하기 전 위치 인코딩 행렬값을 추가하여 입력 값 자체에 위치 정보를 다는 것을 의미합니다.

Attention Is All You Need 논문 저자는 위치 인코딩을 계산하는 데 사인파 함수를 이용했다고 합니다.

인코더

image.png

앞서 말했던 멀티 헤드 어텐션을 사용하는 하나의 인코더를 관찰하겠습니다. 먼저 그림에는 생략되었지만 입력값에 위치 인코딩을 더해줍니다.

다음으로 입력값을 멀티 헤드 어텐션에 넣어서 최종 출력값을 제출합니다. 그 후 add와 Norm 과정을 거치게 됩니다.

add는 다르게 표현하면 잔차 연결(Residual connection) 과정 입니다. 멀티 헤드 어텐션 출력값에 입력값(X)를 더해줍니다. 두 행렬의 크기가 동일하기 때문에 더하기 연산이 가능합니다.

잔차 연결 과정은 하위 층에서 학습된 정보가 데이터 처리 과정에서 손실되는 것을 방지합니다.

다음으로 Norm은 다르게 표현하면 층 정규화(Layer Normalization) 과정입니다. 행 기준으로 값들을 정규화 시켜 출력합니다.

그 다음 피드포워드 네트워크 층을 통과시킵니다. 논문에서는 2개의 전결합층과 Relu 활성화 함수를 사용했습니다. 물론 출력하는 행렬 크기는 일정해야합니다.

이후 add와 norm 과정을 한번 더 적용시키면 첫번째 인코더 과정이 끝납니다. 인코더 출력 행렬의 크기가 입력 행렬의 크기와 같다는 걸 잘 기억해야합니다.

이러한 인코딩 과정을 여러번 수행합니다. 앞 인코더의 출력값을 다음 인코더의 입력값으로 계속 사용합니다. 논문에서는 총 6번 수행했다고 하네요.

디코더

image.png

'나는 차를 가지고 있습니다' 문장으로 'I have a car' 문장을 생성하는 변역기를 만든다고 가정합시다.

첫 인코더에 '나는 차를 가지고 있습니다' 을 넣고 인코더들을 계속해서 거치면서 만들어진 최종 출력값을 디코더의 입력값에 사용하게 됩니다.

여기서 먼저 알아야할 것은 문장의 시작을 알리는 단어토큰으로 'sos' 이 있고, 문장의 끝을 나타내는 단어토큰으로 'eos' 가 있습니다.

실제 모델을 사용할때 입력문장 '나는 차를 가지고 있습니다'을 인코더에 넣어 출력값을 만들면 그 출력값과 'sos' 토큰을 디코더에 넣을때 다음 값인 'I' 단어가 나와야합니다.

다음으로 인코더 출력값과 'sos'토큰, 또 'I'단어를 디코더에 넣으면 'am' 단어가 나옵니다. 이런식으로 다음 단어를 찾는 식으로 진행됩니다.

디코더에 넣어서 나온 단어들을 또 추가해서 다시 디코더에 넣어 다음 단어를 찾는 행위를 반복하다가 'eos' 단어 토큰이 나오면 중단하고 변역을 마칩니다. 이 부분 또한 중요한 것이 문장 변역시 입력된 문장과 출력된 문장간 길이는 다를 수 있다는 점을 고려했다는 것 입니다.

주의할 점은 디코더에 단어를 넣을때도 마찬가지로 문장 순서 또한 중요하기 때문에 인코더와 마찬가지로 위치 인코딩 값을 넣어줍니다.

이러한 디코더 모델을 만들기 위해 어떻게 학습이 진행되는지 살펴봅시다.

image.png

하나의 디코더는 3개의 서브 층으로 구성됩니다. 인코더와 다르게 첫번째 서브 층에 마스크 멀티 헤드 어텐션 부분이 추가 됬는데요.

앞서 든 문장 번역 예시로 학습을 진행하면 타겟 문장 'I have a car'을 예측하는데 단어 단위로 보면 미리 다음 단어를 안 상태로 어텐션을 진행하면 안됩니다. 이걸 위해 마스크 멀티 헤드 어텐션 부분이 존재하는데 뒤에 더 자세히 설명하겠습니다.

타겟 문장은 앞서 말한 것 처럼 한 단어씩 늘려서 입력하여 다음 단어를 추론하는데, 타겟 문장은 마스크 멀티 헤드 어텐션의 입력값으로 넣고, 인코더에서 나온 출력값과 마스크 멀티 헤드 어텐션의 출력값을 동시에 디코더 두번째 서브층인 멀티 헤드 어텐션에 넣습니다.

이외에 디코더 입력값에 위치 인코더 값을 더해주는것과 add & norm 층과 피드포워드 네트워크 층은 인코더와 동일합니다. 또 인코더와 마찬가지로 여러개의 디코더를 연이어서 붙힙니다.

디코더의 최종 출력 값은 다음 단어를 예측하는 일이기 때문에 softmax 함수를 이용해 알맞는 단어를 출력해줍니다.

image.png

마스크 멀티 헤드 어텐션 부분에 대해 좀 더 자세히 알아보겠습니다. 한 단어씩 예측하는 모델이기 때문에 다음 단어는 모르는 것이 당연합니다.

어텐션 단계에서도 마찬가지 인데요. all 단어만 있었던 디코더 입력 시기에는 밑에 단어는 알지 못합니다. all 단어 중심 어텐션에서 밑에 단어를 고려한 어텐션을 하는 것 자체가 모순이죠.

일반 멀티 헤드 어텐션과 과정은 동일하나 쿼리(Q) 행렬과 키(K) 전치 행렬 간 행렬 곱한 행렬을 봅시다. 행 기준 단어와 다른 단어간 연관도 정도를 구하는데 이 부분에서 자기 단어보다 우측 단어(열 기준)는 몰랐던 단어이기 때문에 어텐션 값을 구하지 않습니다.

그림에서 보다싶이 이 과정에서 우 삼각 행렬 값에 마이너스 무한대 값을 가진 행렬을 더해줍니다. 그 다음 과정이 시그모이드 함수에 적용하는 것인데 마이너스 무한대 값은 0으로 변환되겠네요. 즉 뒷 단어 값이 현 단어와의 상관관계가 0%라는 것을 표현합니다.

이 과정 이외에 나머지 과정은 일반 멀티 헤드 어텐션과 동일합니다.

image.png

디코더의 두번째 서브층인 멀티 헤드 어텐션 관련 설명입니다. 인코더 내 멀티 헤드 어텐션에서는 입력값(X)에서 쿼리(Q), 키(K), 밸류(V) 행렬을 모두 만들었는데요. 디코더 멀티 헤드 어텐션에서는 조금 다릅니다.

퀴리(Q) 행렬은 디코더의 첫번째 서브층, 마스크 멀티 헤드 어텐션의 출력값에서 추출되고, 키(K)와 밸류(V) 행렬은 인코더의 최종 출력물 행렬로 부터 추출 됩니다.

이렇게 연산을 하는 이유는 멀티 헤드 어텐션을 진행하는 과정에서 쿼리(Q) 행렬과 키(K) 전치 행렬의 행렬곱을 구할때 확인할 수 있습니다. 행렬 곱 내 행 부분이 쿼리(Q) 행렬의 행 부분과 일치하고, 행렬곱 내 열 부분은 키(K) 행렬의 열 부분과 일치합니다. 이를 해석하면 타겟 값으로 입력된 문장 내 모든 단어('나는 차를 가지고 있습니다')와 인코더에서 출력된 입력 문장의 모든 단어('I have a car') 간 어텐션 연산이 진행됩니다.

그 뒤 밸류(V) 행렬과 행렬 곱 연산을 진행하면 타겟 값으로 입력된 단어별로 인코더에서 출력된 입력 문장의 모든 단어의 연관도에 비례하여 값이 더해져서 출력값을 배출합니다.

지금 다룬 내용을 아래 그림과 함께 보시면 이해가 잘 되실 것 같아요.

image.png

느낀점

'구글 BERT의 정석' 책과 'attention is all you need' 논문을 참고하여 트랜스포머 모델구조를 관찰했습니다.

그냥 어텐션을 사용했다, 자연어 처리에 기초 모델이다로 알고 있었던 개념을 정확히 알 수 있어 좋은 기회였던 것 같습니다.

개인적으로 자연어 처리 부분을 주로 다루는 것이 목표인데 다소 지루하지만 꼭 필요했던 과정인 것 같습니다.

개념이 다소 복잡해 이해하는데 정말 시간을 많이 들였는데, 그래도 모델 구조만큼은 떳떳하게 이해했다고 말 할수 있어서 뿌듯하네요.

설명이 부족한 부분은 댓글로 질문 써주시면 최대한 답변해보겠습니다. 감사합니다.