|
|
트랜스포머 — 어텐션, 셀프 어텐션, 멀티헤드, 위치 인코딩, 인코더-디코더
— 어텐션의 등장
앞서 순환 신경망(RNN)은 순차 데이터를 시간 순서대로 처리하며 맥락을 은닉 상태에 담아 전달하는 구조였다. 트랜스포머는 이 순환 구조를 완전히 버리고 어텐션만으로 구성한 모델이다. 이 글에서는 어텐션이 무엇을 대체했고 어떤 역할을 하는지부터, 셀프 어텐션·멀티헤드·위치 인코딩을 거쳐 트랜스포머 전체 구조까지 정리한다.
1. 어텐션의 등장
■ 인코더-디코더의 콘텍스트 벡터 병목
어텐션이 없던 시절의 인코더-디코더(Seq2Seq)는 인코더가 입력 전체를 마지막 은닉 상태 하나, 즉 콘텍스트 벡터에 압축해 디코더에 넘겼다. 디코더는 단어를 생성하는 매 순간 이 하나의 벡터만 참고했다.
문제는 여기서 발생한다. 예를 들어 "나는 너를 사랑해"를 "I love you"로 번역할 때, "love"를 생성하는 시점에는 입력의 "사랑해"에 집중해야 하고 "you"를 생성할 때는 "너를"에 집중해야 한다. 그러나 콘텍스트 벡터 하나에는 이 모든 정보가 뭉뚱그려져 있어 매번 같은 요약만 참고하게 된다. 입력이 길수록 앞부분 정보는 이 벡터에 도달하기까지 흐려진다.
■ 어텐션 — 매 순간 필요한 곳에 집중
어텐션은 이 병목을 해결한다. 방식은 두 가지다.
첫째, 인코더의 마지막 은닉 상태 하나만 넘기지 않고 모든 단계의 은닉 상태를 전부 보관한다.
둘째, 디코더가 단어를 생성할 때마다 이 은닉 상태들 중 지금 필요한 것을 동적으로 골라 본다. 예를 들어 "love"를 생성하는 시점에는 각 입력과의 연관도를 계산해 "사랑해"에 해당하는 은닉 상태에 높은 가중치를 주고, 그 가중치대로 은닉 상태들을 가중합한다. 이 가중합 결과가 그 시점의 콘텍스트가 된다.
핵심은 콘텍스트가 고정된 하나가 아니라, 생성하는 단어마다 새로 계산된다는 점이다. 이 계산에는 세 요소가 쓰인다.
· 쿼리(Query, q) : 지금 기준이 되는 것. 디코더의 현재 상태.
· 키(Key, k) : 각 대상의 식별용 이름표. 인코더의 은닉 상태들.
· 밸류(Value, v) : 연관도에 따라 실제로 가져올 데이터.
디코더의 q를 인코더의 모든 k와 비교해 연관도를 구하고, 그 비율로 v를 가중합해 콘텍스트를 만든다.
■ 순환 구조를 버린 트랜스포머
위 단계에서는 인코더와 디코더가 여전히 RNN이며, 어텐션은 그 위에 얹힌 보조 장치였다. 트랜스포머는 여기서 한 발 더 나아가 순환 구조 자체를 제거하고 어텐션만으로 모델을 구성했다.
RNN은 단어를 순서대로 거치며 맥락을 쌓기 때문에 멀리 떨어진 단어일수록 정보가 흐려지고 병렬 처리도 불가능했다. 어텐션은 모든 단어를 한 번에 보고 각 단어 쌍의 연관도를 직접 계산하므로, 거리에 관계없이 정보가 흐려지지 않고 순환 없이도 맥락을 만들 수 있다. 즉 RNN이 순차적으로 하던 "맥락 담긴 표현 만들기"를 어텐션이 대체한 것이다.
▶ 핵심 한 줄 : 어텐션은 고정된 요약 하나에 의존하는 대신 매 순간 전체를 보고 필요한 곳에 직접 집중하며, 트랜스포머는 이 어텐션을 주력으로 삼아 순환 구조를 걷어냈다.
2. 셀프 어텐션과 Q·K·V
■ 셀프 어텐션이란
앞 장의 어텐션은 디코더가 인코더를 참고하는, 즉 서로 다른 두 문장 사이의 어텐션이었다. 셀프 어텐션(self-attention)은 하나의 문장 안에서 단어들끼리 서로 얼마나 연관되는지를 보는 방식이다. 쿼리·키·밸류를 모두 같은 입력(자기 자신)에서 뽑기 때문에 '셀프'라 부른다.
예를 들어 "그 동물은 길을 안 건넜다, 그것(it)이 피곤했기 때문에"에서 'it'이 '동물'을 가리킨다는 것을, it의 쿼리를 문장 내 모든 단어의 키와 비교해 알아낸다. 이렇게 각 단어가 문맥을 자기 표현 안으로 끌어들이는 것이 셀프 어텐션이며, 이것이 트랜스포머의 핵심이다.
■ Wq·Wk·Wv — 학습되는 변환 행렬
셀프 어텐션에서 q, k, v는 하늘에서 떨어지는 값이 아니라 입력에 세 개의 가중치 행렬을 곱해 만든다.
q = 입력 × Wq
k = 입력 × Wk
v = 입력 × Wv
여기서 입력은 단어를 숫자 벡터로 바꾼 임베딩(embedding)이다. 예를 들어 512차원 벡터다. 차원이 큰 이유는 단어의 의미를 여러 숫자의 조합(분산 표현)으로 담아야 하기 때문이며, 차원이 클수록 표현력이 높지만 연산량도 늘어 적절한 값을 택한다.
이 Wq·Wk·Wv가 어텐션이 실제로 학습하는 실체다. 입력마다 계산되어 바뀌는 q·k·v와 달리, Wq·Wk·Wv는 학습으로 고정되는 파라미터이며 역전파로 갱신된다. 즉 RNN 층이 하던 역할을 이 세 행렬이 대체한다.
k와 v가 나뉘어 있는 이유도 여기서 드러난다. 같은 입력을 Wk로 뽑은 것이 k, Wv로 뽑은 것이 v로, k는 쿼리와의 연관 정도를 판단하는 이름표이고 v는 연관도에 따라 실제로 넘겨줄 내용물이다. 역할이 다르므로 서로 다른 행렬로 만든다.
■ 셀프 어텐션의 계산 과정
한 단어 'it'의 새 표현을 만드는 과정을 따라가면 다음과 같다.
먼저 입력을 Wq·Wk·Wv로 변환해 모든 단어의 q, k, v를 만든다. 그다음 it의 쿼리 q를 문장 내 모든 단어의 키 k와 내적해 연관도(어텐션 점수)를 구한다.
q_it · k_동물 = 0.7
q_it · k_길 = 0.1
q_it · k_피곤 = 0.6
...
이 점수를 소프트맥스로 0~1 사이 비율(합이 1)로 바꾼 뒤, 그 비율대로 각 단어의 밸류 v를 가중합한다.
it의 새 표현 = 0.7·v_동물 + 0.1·v_길 + 0.6·v_피곤 + ...
이 과정을 거치면 원래 대명사에 불과하던 'it'의 표현에 '동물'과 '피곤'의 정보가 섞여 들어가, 맥락이 담긴 표현이 된다. 그리고 이 계산은 특정 단어만이 아니라 문장 내 모든 단어에 대해 동시에(병렬로) 일어난다.
여기서 q·k는 "얼마나 볼지" 비율을 정하는 단계이고, v는 그 비율대로 "실제로 무엇을 가져올지"를 담당하는 단계다. RNN이 멀리 떨어진 단어의 정보를 여러 단계 거쳐 전달하며 흐려뜨렸던 것과 달리, 셀프 어텐션은 q와 k를 거리에 관계없이 직접 내적하므로 정보가 흐려지지 않는다.
(실제 구현에서는 내적값을 벡터 차원의 제곱근으로 나누는 스케일링을 적용하며, 단어를 하나씩이 아니라 문장 전체를 행렬로 쌓아 한 번에 계산한다. 이 때문에 정식 명칭은 Scaled Dot-Product Attention이다.)
▶ 핵심 한 줄 : 셀프 어텐션은 입력을 Wq·Wk·Wv로 q·k·v로 바꾸고, q·k로 정한 비율대로 v를 섞어 맥락 담긴 표현을 만든다.
3. 멀티헤드 어텐션
■ 어텐션 하나로는 관점이 하나뿐
셀프 어텐션은 q·k로 연관도를 계산하는데, 이때 연관도를 재는 잣대는 Wq·Wk 한 세트로 정해진다. 그런데 단어 사이의 관계는 한 종류가 아니다. 앞의 'it'만 봐도 '동물'을 가리키는 지시 관계, '피곤'과 이어지는 인과 관계, 문장 내 문법적 역할 등 여러 관계가 동시에 존재한다.
주의할 점은, 모든 단어 쌍을 다 계산하는 것과 여러 관점으로 보는 것은 다르다는 것이다. 셀프 어텐션은 모든 단어를 보긴 하지만, 연관도를 재는 잣대(Wq·Wk)가 하나이므로 한 종류의 관계만 포착한다. 지시 관계에 맞춰진 잣대는 인과 관계를 놓친다.
■ 여러 헤드를 병렬로
이를 해결하기 위해 셀프 어텐션을 여러 개 병렬로 수행한다. 각각을 헤드(head)라 하며, 헤드마다 독립된 Wq·Wk·Wv를 가진다. 그 결과 헤드마다 서로 다른 관계에 특화되어, 여러 관점을 동시에 확보한다. 어느 헤드가 어떤 관계를 볼지는 사람이 정하지 않고 학습으로 분화된다.
계산은 차원을 나눠 수행한다. 예를 들어 모델 차원이 512이고 헤드가 8개면, 각 헤드는 512차원 입력을 각자의 Wq·Wk·Wv로 64차원으로 변환한다.
헤드1 : 입력(512) → q1·k1·v1 (각 64차원) → 어텐션 → 출력1(64)
헤드2 : 입력(512) → q2·k2·v2 (각 64차원) → 어텐션 → 출력2(64)
...
헤드8 : ... → 출력8(64)
여기서 512를 물리적으로 잘라 나누는 것이 아니라, 각 헤드가 512 전체를 자기 방식으로 64차원으로 압축한다는 점에 유의한다. 64 × 8 = 512로 맞추므로 단일 헤드로 512를 처리하는 것과 연산량이 비슷하면서, 8가지 관점을 얻는다.
■ 헤드 종합
각 헤드가 64차원 결과를 내면, 이들을 이어붙여(concat) 다시 512차원으로 만들고, 마지막에 학습되는 행렬 Wo로 한 번 더 변환한다.
출력1(64) + 출력2(64) + ... + 출력8(64) → concat → 512 → × Wo → 최종 출력(512)
이어붙이기는 여러 헤드의 결과를 나란히 모으는 것이고, Wo는 이들을 어떻게 조합할지를 학습한다. 각 관점을 그대로 유지하는 것이 아니라 종합하는 것이 목적이며, 얼마나 섞고 얼마나 살릴지도 Wo가 학습으로 결정한다. 쪼개는 Wq·Wk·Wv와 합치는 Wo는 서로 다른 역할의 독립된 파라미터다.
입력과 출력이 모두 512차원으로 유지되므로, 이 멀티헤드 어텐션 층을 여러 겹 쌓을 수 있다.
▶ 핵심 한 줄 : 멀티헤드 어텐션은 연관도를 재는 잣대를 여러 개 두어 여러 관점을 병렬로 확보하고, Wo로 종합한다.
4. 위치 인코딩
■ 순환을 버리면 순서가 사라진다
RNN은 단어를 시간 순서대로 하나씩 입력받았기 때문에 순서 정보가 자동으로 담겼다. 첫 번째 은닉 상태는 첫 단어, 두 번째는 두 번째 단어를 처리한 결과라는 것이 저절로 정해졌다.
그러나 셀프 어텐션은 모든 단어를 동시에 처리한다. q·k로 연관도를 계산할 때 각 단어가 몇 번째인지에 대한 정보가 전혀 들어가지 않는다. 즉 트랜스포머에게 문장은 순서 없는 단어의 집합으로 보인다. "개가 사람을 물었다"와 "사람이 개를 물었다"는 단어 구성이 같으므로 구분되지 않는다. 순환 구조를 버려 병렬 처리와 직접 연결을 얻은 대신, 순서 정보를 잃은 것이다.
■ 위치 벡터를 더한다
이를 해결하기 위해 단어 임베딩에 위치 정보를 담은 벡터를 더한다. 이것이 위치 인코딩(positional encoding)이다.
최종 입력 = 단어 임베딩 + 위치 벡터
이어붙이는 것이 아니라 같은 크기의 벡터를 더하는 것으로, 결과적으로 하나의 벡터에 "무엇인지(의미)"와 "어디에 있는지(위치)"가 함께 담긴다.
위치를 표현하는 나이브한 방법에는 문제가 있다. 정수 0, 1, 2, ...를 그대로 더하면 문장이 길수록 값이 커져 단어 임베딩을 압도하고, 0~1로 정규화하면 문장 길이마다 단어 사이 간격이 달라져 일관성이 없다.
■ sin/cos 파동
트랜스포머는 위치를 여러 주기의 sin, cos 값들의 조합으로 표현한다. 위치 벡터의 각 차원마다 다른 주파수의 파동을 사용한다.
짝수 차원 : sin(pos / 10000^(2i/d))
홀수 차원 : cos(pos / 10000^(2i/d))
이는 십진법의 자릿수와 유사하다. 일의 자리는 매 숫자마다 바뀌고(빠르게 변함), 십의 자리는 10마다, 백의 자리는 100마다 바뀐다(느리게 변함). 위치 인코딩도 벡터의 앞쪽 차원은 빠르게 진동하는 파동을, 뒤쪽 차원은 느리게 진동하는 파동을 사용한다. 자릿수 하나로는 0~9만 구분되지만 여러 자릿수를 조합하면 많은 수를 유일하게 표현하듯, 여러 주기의 파동을 조합하면 각 위치가 고유한 벡터를 갖는다. 정수처럼 값이 끊기지 않고 부드럽게 이어진다는 점만 다르다.
이 방식의 이점은 세 가지다. 값이 항상 -1~1 사이라 단어 임베딩을 압도하지 않고, 삼각함수의 성질 덕분에 상대적 위치를 계산하기 쉬우며, 공식으로 값이 정해지므로 학습이 필요 없다. sin/cos 값은 위치를 공식에 넣으면 바로 나오는 고정값으로, Wq·Wk·Wv 같은 학습 파라미터가 아니다. (위치 벡터를 학습시키는 방식도 있으나 원논문은 이 고정 방식을 사용한다.)
의미와 위치를 더하면 정보가 섞일 것 같지만, 고차원 공간에서는 두 정보가 겹치지 않을 여유가 충분하고, 이어지는 Wq·Wk·Wv가 학습을 통해 필요한 정보를 구분해 쓰는 법을 익히므로 문제되지 않는다.
▶ 핵심 한 줄 : 순환을 버려 사라진 순서 정보를, 각 차원에 다른 주기의 파동을 넣는 위치 인코딩으로 되채운다.
5. 트랜스포머 전체 구조
앞의 부품들(셀프 어텐션, 멀티헤드, 위치 인코딩)을 조립하면 트랜스포머가 된다. 트랜스포머는 입력을 이해하는 인코더와 출력을 생성하는 디코더로 구성되며, 각각 동일한 블록을 여러 층(보통 6층) 쌓는다.
■ FFN과 잔차 연결
블록을 보기 전에 두 부품을 먼저 짚는다.
FFN(Feed-Forward Network)은 되먹임 없이 앞으로만 흐르는 기본 신경망이다. 구조는 선형변환 → ReLU → 선형변환의 두 층으로, 차원을 크게 늘렸다가(예: 512 → 2048) 다시 줄인다(2048 → 512).
FFN(x) = W2 · ReLU(W1·x + b1) + b2
어텐션이 단어들 사이에 정보를 주고받는 소통이라면, FFN은 그렇게 섞인 정보를 각 단어가 개별적으로 한 번 더 가공하는 역할이다. 다른 단어와 무관하게 단어마다 독립적으로 통과한다. 중간 차원을 넓히는 이유는, 넓은 공간에서 비선형 변환을 거쳐야 더 복잡한 특징을 뽑아낼 수 있기 때문이다. W1·W2 모두 학습된다.
잔차 연결(residual connection)은 층의 출력에 입력을 그대로 더하는 지름길이다.
출력 = F(x) + x
CNN의 ResNet에서 쓰인 것과 같은 구조로, 역전파 시 그래디언트가 이 덧셈 경로로 흘러 층을 깊이 쌓아도 흐려지지 않게 한다. 앞서 LSTM이 셀 상태에 W 곱 없이 이전 값을 더해 그래디언트 소실을 막은 것과 같은 발상이다. 트랜스포머의 어텐션 층과 FFN 층은 모두 이 잔차 연결로 감싸지며, 함께 층 정규화(Layer Normalization)를 적용해 학습을 안정시킨다.
■ 인코더
인코더 블록은 멀티헤드 셀프 어텐션과 FFN으로 이루어지며, 각각 잔차 연결과 정규화로 감싼다.
입력 임베딩 + 위치 인코딩
→ 멀티헤드 셀프 어텐션 (+ 잔차·정규화)
→ FFN (+ 잔차·정규화)
→ (이 블록을 6층 반복)
→ 인코더 출력 (입력을 이해한 표현)
셀프 어텐션으로 단어들의 관계를 파악하고 FFN으로 각 단어를 개별 가공하는 과정을 반복하며, 층이 깊어질수록 표현이 정교해진다.
■ 디코더
디코더 블록에는 어텐션이 두 종류 들어간다.
첫째는 마스크드 셀프 어텐션이다. 디코더는 출력을 한 단어씩 생성하므로, 각 단어를 처리할 때 아직 생성되지 않은 미래 단어를 보면 안 된다. 학습 시에는 정답 문장 전체를 입력하기 때문에 그대로 두면 미래 단어가 보여 부정확한 학습이 되므로, 미래 위치의 연관도를 가려(mask) 각 단어가 자기 앞쪽만 보게 한다.
둘째는 크로스 어텐션이다. 여기서 쿼리는 디코더의 현재 상태에서, 키와 밸류는 인코더 출력에서 온다. 두 개의 서로 다른 문장을 교차해 참고하므로 크로스라 부른다. 디코더가 출력 단어를 생성할 때 입력의 어느 부분을 봐야 할지를 계산하는 부분으로, 앞 장에서 본 인코더-디코더 어텐션과 동일한 역할이다.
출력 임베딩 + 위치 인코딩
→ 마스크드 셀프 어텐션 (+ 잔차·정규화)
→ 크로스 어텐션 (q=디코더, k·v=인코더) (+ 잔차·정규화)
→ FFN (+ 잔차·정규화)
→ (이 블록을 6층 반복)
→ 선형 변환 → 소프트맥스 → 다음 단어 확률
셀프 어텐션과 크로스 어텐션의 차이는 q·k·v의 출처에 있다. 셀프 어텐션은 셋 모두 같은 문장에서 나오고, 크로스 어텐션은 q는 디코더, k·v는 인코더에서 나온다. 인코더는 셀프 어텐션만 쓰고, 디코더는 마스크드 셀프 어텐션과 크로스 어텐션을 모두 쓴다.
■ 출력
디코더의 마지막 출력은 선형 변환과 소프트맥스를 거쳐, 전체 어휘(예: 5만 단어) 각각에 대한 확률로 바뀐다. 이 중 가장 확률이 높은 단어를 다음 단어로 선택하고, 이 과정을 반복해 문장을 완성한다.
트랜스포머에서 학습되는 것은 Wq·Wk·Wv, 헤드를 종합하는 Wo, FFN의 가중치, 단어 임베딩까지 대부분이며(위치 인코딩만 고정), 이 모두는 최종 출력의 손실 하나로부터 역전파를 통해 함께 학습된다.
▶ 핵심 한 줄 : 트랜스포머는 인코더(셀프 어텐션 + FFN)로 입력을 이해하고, 디코더(마스크드 셀프 + 크로스 어텐션 + FFN)로 출력을 한 단어씩 생성하며, 모든 층을 잔차 연결로 감싸 깊게 쌓는다.
참고자료 : Vaswani et al., "Attention Is All You Need" (2017)
|
|
