머신러닝·신경망

DL / 34번째 글

Attention: Seq2Seq 병목에서 Transformer로

고정 Context Vector의 병목을 Attention이 어떻게 푸는지, Bahdanau·Luong·Scaled Dot-Product가 어디서 갈리는지, 그리고 Attention을 붙이고도 RNN에 남아 있던 세 한계를 Transformer가 어떻게 한꺼번에 걷어냈는지 한 흐름으로 잇는다.

PALDYN Team35 MIN READ

지난 글에서 인코더-디코더 구조를 세웠다. 그리고 그 이음매에 손대지 않은 자리가 하나 남았다 — 인코더가 만든 벡터 하나가 디코더로 넘어가는 지점이다.

2015년 Bahdanau et al.이 내놓은 해법은 뜻밖에 단순했다. 압축을 하지 않는 것이다. 인코더가 만든 은닉 상태를 하나도 버리지 않고 전부 남겨 두고, 디코더는 토큰을 하나 만들 때마다 그중 지금 필요한 것을 골라 쓴다. 이것이 Attention 메커니즘이고, 여기서 딥러닝의 방향이 한 번 꺾인다. 2년 뒤 Vaswani et al.은 같은 장치를 그대로 둔 채 반대편, 그러니까 RNN 쪽을 빼 버렸다.

이 글은 그 두 걸음을 한 흐름으로 잇는다. 앞 절반은 Attention이 병목을 어떻게 푸는지와 점수 함수가 어디서 갈리는지를 다루고, 뒤 절반은 Attention을 붙이고도 RNN에 남아 있던 것이 무엇이었고 왜 결국 RNN을 통째로 뺐는지를 다룬다.

고정 크기 병목

Context Vector

기본 Seq2Seq에서 인코더는 입력 토큰을 하나씩 읽으며 은닉 상태를 갱신하고, 다 읽은 뒤 마지막 상태 하나만 디코더에 넘긴다. 그 벡터를 Context Vector라 부른다 — 입력 전체를 요약했다고 가정하는 벡터다. 중간에 만들어진 상태들은 계산되자마자 버려진다.

크기를 숫자로 놓아 보면 무리가 보인다. 은닉 차원이 256이면 Context Vector는 float32 기준 1킬로바이트다. 입력이 세 토큰짜리 「좋은 아침」이든 100 토큰짜리 계약서 한 문단이든 이 크기는 같다. 정보량은 길이에 따라 늘어나는데 그것을 받는 그릇은 고정이다.

더 나쁜 것은 고정 크기 자체보다 그 벡터가 최근 입력 쪽으로 기울어 있다는 점이다. 순환식은 매 스텝 이전 상태에 새 입력을 섞어 덮어쓴다. 100번째 토큰을 읽고 난 상태에는 100번째 입력의 흔적이 가장 진하게 남고, 첫 토큰의 흔적은 99번의 갱신을 지나며 옅어진다. 그래서 긴 문장에서 무너지는 것은 문장 전체가 아니라 주로 앞쪽이다.

길이별 품질 곡선

Bahdanau 논문의 유명한 그림 하나가 이것을 보여 준다. 번역 결과를 원문 문장 길이로 묶어 품질을 재는데, 여기서 길이의 단위는 단어 수다. 학습 문장 길이를 30 단어까지로 제한한 기본 Seq2Seq는 시험 문장이 그 30 단어 언저리를 넘어서는 순간부터 곡선이 뚝 떨어진다. 반면 같은 조건에서 Attention을 붙인 쪽은 50~60 단어에서도 곡선이 거의 평평하고, 학습 길이를 50까지 늘려 준 모델은 그 평평한 구간이 더 길게 이어진다.

읽을 때 조심할 것이 하나 있다. 이 그림에서 갈리는 것은 「모델이 30 단어를 처리할 수 있느냐」가 아니다. 둘 다 처리는 한다. 갈리는 것은 학습 때 본 적 없는 길이로 넘어갔을 때 품질이 버티느냐다. 압축하는 쪽은 못 버티고 압축하지 않는 쪽은 버틴다.

이 차이가 중요한 이유는 원인의 종류에 있다. 데이터를 더 넣거나 학습을 더 돌려서 좁힐 수 있는 격차가 아니다. 구조가 그어 놓은 상한이다. 그릇의 크기가 문장 길이와 무관하게 고정되어 있는 한, 어떤 길이부터는 반드시 넘친다. 고쳐야 할 것은 학습이 아니라 정보가 지나는 통로의 모양이었다.

쿼리·키·밸류

그래서 인코더 출력을 마지막 하나가 아니라 전부 들고 있기로 한다. 길이 TT 짜리 입력이면 (B,T,H)(B, T, H) 모양의 텐서가 통째로 남는다. 압축이 없으니 잃는 정보도 없다.

문제는 그다음이다. 디코더는 이 TT 개 중 무엇을 봐야 하는지 알아야 한다. Attention의 답은 「지금 상태에 비추어 각각이 얼마나 관련 있는지 점수를 매기고, 그 점수대로 섞는다」이다.

이 한 문장에 이름이 셋 붙는다. 먼저 디코더 상태 sts_t 가 쿼리(query)다 — 「지금 무엇을 찾고 있는가」를 적은 벡터다. 인코더 상태들은 두 역할을 겸한다. 쿼리와 대조해 점수를 매길 대상일 때는 키(key)이고, 점수가 정해진 뒤 실제로 꺼내 와 섞을 내용일 때는 밸류(value)다. 도서관에 비유하면 쿼리는 검색어, 키는 책등에 적힌 제목, 밸류는 책의 내용이다.

Bahdanau·Luong 시절에는 키와 밸류가 같은 벡터 hih_i 였다. 제목과 내용을 같은 것으로 쓴 셈이다. 둘을 갈라 서로 다른 벡터로 만드는 것은 뒤의 「순환 없는 Attention」 절에서 다시 나온다. 이름을 지금 붙여 두는 이유는 앞으로 나올 모든 점수 함수가 「쿼리와 키로 점수를 내고 밸류를 섞는다」는 같은 문장으로 읽히기 때문이다.

치르는 값은 메모리다. 벡터 하나 대신 T×HT \times H 를 들고 있어야 하고, 디코더 스텝마다 TT 번의 점수 계산이 붙는다. 2015년 기준으로도 감당할 만한 비용이었고, 얻는 것에 비하면 싼 편이었다.

Attention 메커니즘: 병목 문제 해결

Attention의 계산 절차

점수·정규화·가중합

절차는 세 걸음뿐이고 모든 Attention 변형이 이 뼈대를 공유한다. 점수를 매기고, 합이 1이 되도록 정규화하고, 그 비율대로 섞는다.

et,i=score(st,hi)αt,i=exp⁡(et,i)∑j=1Texp⁡(et,j)ct=∑i=1Tαt,i hi\begin{aligned} e_{t,i} &= \mathrm{score}(s_t, h_i) \\ \alpha_{t,i} &= \frac{\exp(e_{t,i})}{\sum_{j=1}^{T} \exp(e_{t,j})} \\ c_t &= \sum_{i=1}^{T} \alpha_{t,i} \, h_i \end{aligned}

소프트맥스가 하는 일을 숫자로 따라가 보면 성격이 분명해진다. 점수가 [2.0, 0.5, −1.0][2.0,\ 0.5,\ -1.0] 이면 지수를 취해 [7.39, 1.65, 0.37][7.39,\ 1.65,\ 0.37] 이 되고, 합 9.41로 나누면 가중치는 [0.786, 0.175, 0.039][0.786,\ 0.175,\ 0.039] 다. 점수 차이가 1.5밖에 안 되는데 가중치는 네 배 이상 벌어진다. 소프트맥스는 차이를 증폭한다 — 이 성질이 뒤에서 스케일링이 왜 필요한지를 설명하는 열쇠가 된다.

가중치가 음이 아니고 합이 1이므로 ctc_t 는 인코더 상태들의 볼록 결합(convex combination)이다. 볼록 결합은 여러 점을 비율대로 섞어 그 점들이 이루는 영역 안의 한 점을 만드는 것이다. 그래서 Context Vector는 인코더 상태들이 놓인 공간을 벗어나지 않는다. 디코더가 받는 것은 늘 「인코더가 실제로 본 것들의 어떤 조합」이지, 어디에도 없던 새 벡터가 아니다.

스텝별 Context Vector

기본 Seq2Seq에서 Context Vector는 하나였다. Attention에서는 디코더 스텝마다 다르다. 출력 길이가 mm, 입력 길이가 nn 이면 점수 계산이 m×nm \times n 번 일어나고, 서로 다른 mm 개의 Context Vector가 만들어진다.

이 「다시 만든다」가 왜 필요한지는 어순이 다른 언어쌍에서 바로 드러난다. 프랑스어 「le chat noir」를 영어 「the black cat」로 옮길 때, 두 번째 출력 토큰 black은 세 번째 입력 noir를, 세 번째 출력 cat은 두 번째 입력 chat을 봐야 한다. 참조 순서가 교차한다. 고정 Context Vector 하나로는 이 교차를 벡터 내부의 어딘가에 접어 넣은 채 디코더가 알아서 풀어내기를 바라야 했다. Attention은 그냥 스텝마다 다른 곳을 본다.

Bahdanau와 Luong은 이 ctc_t 를 쓰는 자리가 다르다. Bahdanau는 디코더 셀에 들어가기 전에 입력과 함께 넣고, Luong은 디코더 상태를 먼저 만든 뒤 그 상태와 ctc_t 를 이어 붙여 출력층으로 보낸다. 구현상 차이지만 결과적으로 Luong 쪽이 디코더 루프를 단순하게 만들어 이후 구현들이 더 많이 따랐다.

정렬 행렬

α\alpha 를 스텝별로 쌓으면 m×nm \times n 짜리 행렬이 된다. 이것을 히트맵으로 그린 것이 Attention 논문마다 실리는 정렬 행렬(alignment matrix)이다. 밝은 칸이 「그 출력 토큰을 만들 때 그 입력 토큰을 봤다」는 뜻이다.

어순이 비슷한 언어쌍에서는 밝은 칸이 대각선을 따라 단조롭게 흐르고, 어순이 다른 자리에서는 그 대각선이 국소적으로 뒤집힌 블록으로 나타난다. 학습 라벨로 정렬을 준 적이 없는데도 통계적 기계 번역이 따로 모델링하던 단어 정렬이 부산물로 떨어진다는 것이 당시로서는 인상적인 결과였다.

정렬 행렬의 두 모양

다만 해석에는 선이 있다. 가중치가 큰 자리가 곧 인과적으로 중요한 자리는 아니다. 같은 출력을 내는 서로 다른 가중치 분포가 여럿 존재할 수 있고, 실제로 가중치를 흔들어도 예측이 잘 바뀌지 않는 경우가 보고됐다. 정렬 행렬은 모델을 들여다보는 단서이지 근거는 아니다.

점수 함수

Bahdanau의 덧셈 점수

Bahdanau의 점수 함수는 작은 신경망 하나다. 쿼리와 키를 각각 선형 변환해 더하고, tanh를 통과시킨 뒤 벡터 하나로 내적해 스칼라를 얻는다.

import torch
import torch.nn as nn
import torch.nn.functional as F

class BahdanauAttention(nn.Module):
    def __init__(self, enc_hid, dec_hid):
        super().__init__()
        self.W_s = nn.Linear(dec_hid, dec_hid, bias=False)
        self.W_h = nn.Linear(enc_hid, dec_hid, bias=False)
        self.v   = nn.Linear(dec_hid, 1, bias=False)

    def forward(self, s, enc_outputs):
        # s: (B, dec_hid), enc_outputs: (B, T, enc_hid)
        score = self.v(torch.tanh(
            self.W_s(s.unsqueeze(1)) + self.W_h(enc_outputs)
        )).squeeze(-1)                       # (B, T)
        alpha = F.softmax(score, dim=-1)
        context = (alpha.unsqueeze(-1) * enc_outputs).sum(1)
        return context, alpha

파라미터가 셋 붙는 대신 얻는 것이 둘 있다. 하나는 표현력이다. 비선형을 한 번 거치므로 단순한 유사도로는 잡히지 않는 관계도 점수에 담을 수 있다. 다른 하나는 차원의 자유다. W_s와 W_h가 각각 맞춰 주므로 인코더와 디코더의 은닉 차원이 달라도 된다 — 양방향 인코더의 출력이 2H2H 이고 디코더가 HH 인 흔한 구성을 그대로 이을 수 있다.

값은 속도로 치른다. TT 개 위치 각각에 대해 덧셈과 tanh를 거쳐야 하므로 행렬 곱 한 번으로 끝나지 않는다. 시퀀스가 길어질수록 이 비용이 눈에 띈다.

Luong의 내적 점수

Luong et al.은 같은 해에 훨씬 단순한 점수를 제안했다. 쿼리와 키를 그냥 내적하는 것이다.

class LuongAttention(nn.Module):
    def forward(self, s, enc_outputs):
        # s: (B, 1, hid), enc_outputs: (B, T, hid)
        score = torch.bmm(s, enc_outputs.transpose(1, 2)).squeeze(1)
        alpha = F.softmax(score, dim=-1)
        context = torch.bmm(alpha.unsqueeze(1), enc_outputs).squeeze(1)
        return context, alpha

추가 파라미터가 하나도 없고, 계산 전체가 배치 행렬 곱 두 번으로 끝난다. 대신 조건이 붙는다 — 쿼리와 키의 차원이 같아야 한다. 차원이 다르면 사이에 행렬 하나를 껴서 s⊤Whs^\top W h 로 맞추는 general 변형을 쓴다.

표현력이 줄었는데도 성능이 크게 떨어지지 않았다는 것이 이 논문의 실질적 발견이었다. 그리고 파라미터가 없다는 점 덕분에 이 점수 함수는 뒤에 층을 수십 개 쌓을 때 훨씬 유리해진다. Transformer가 이쪽을 고른 이유가 여기에 있다.

스케일링

내적을 그대로 쓰면 차원이 커질 때 문제가 생긴다. 두 벡터의 성분이 평균 0, 분산 1이고 서로 독립이라고 두면, 길이 dkd_k 짜리 내적의 분산은 dkd_k 가 된다. dk=64d_k = 64 면 표준편차가 8이라는 뜻이다.

점수가 ±8\pm 8 범위로 흩어지면 무슨 일이 벌어지는지 앞에서 본 소프트맥스의 증폭 성질을 대입하면 된다. 점수가 [8, 0, 0][8,\ 0,\ 0] 일 때 가중치는 [0.9993, 0.00034, 0.00034][0.9993,\ 0.00034,\ 0.00034] 로 사실상 원핫이다. 원핫에 가까운 소프트맥스 출력에서는 기울기가 0에 가깝다 — 학습이 시작하자마자 한 자리에 못 박히고 거기서 움직이지 않는다.

dk=8\sqrt{d_k} = 8 로 나누면 같은 점수가 [1, 0, 0][1,\ 0,\ 0] 이 되고 가중치는 [0.576, 0.212, 0.212][0.576,\ 0.212,\ 0.212] 다. 여전히 첫 자리가 우세하지만 나머지도 살아 있고 기울기가 흐른다. 스케일링은 성능을 올리는 장치가 아니라 학습이 죽지 않게 막는 장치다. 차원이 작을 때는 있으나 마나지만, dkd_k 가 64를 넘어가는 순간부터는 없으면 안 된다.

Attention 종류별 점수 계산

종류 점수 함수 추가 파라미터 성격
Additive (Bahdanau) v⊤tanh⁡(Wss+Whh)v^\top \tanh(W_s s + W_h h) Ws,Wh,vW_s, W_h, v 표현력이 가장 크고 차원이 달라도 된다
Dot-Product (Luong) s⊤hs^\top h 없음 가장 단순하고 빠르며 차원이 같아야 한다
Scaled Dot-Product QK⊤/dkQK^\top / \sqrt{d_k} WQ,WK,WVW_Q, W_K, W_V 큰 차원에서 기울기가 살아 있다

RNN의 한계

Attention은 병목을 풀었지만, 그 시점의 모델에서 인코더와 디코더 자체는 여전히 RNN이었다. 2017년 이전까지 남아 있던 것이 무엇인지 셋으로 정리한다.

곱셈 사슬과 기울기 소실

BPTT(Backpropagation Through Time)는 시간축으로 펼친 계산 그래프에 역전파를 그대로 돌리는 것이다. 자세한 전개는 RNN 기초에 있고, 여기서는 결과의 모양만 본다.

∂L∂h1=∂L∂hT∏t=2TWh⊤ diag(σ′(⋅))\frac{\partial L}{\partial h_1} = \frac{\partial L}{\partial h_T} \prod_{t=2}^{T} W_h^\top \, \mathrm{diag}(\sigma'(\cdot))

기울기가 첫 스텝까지 가려면 같은 행렬을 T−1T-1 번 통과해야 한다. 곱셈 사슬이라는 것이 핵심이다. WhW_h 의 최대 특잇값이 0.9면 100 스텝 뒤에 0.9100≈2.7×10−50.9^{100} \approx 2.7 \times 10^{-5} 이고, 500 스텝이면 1.3×10−231.3 \times 10^{-23} 이다. 부동소수점이 표현은 하지만 학습에 아무 영향을 못 준다.

LSTM은 셀 상태에 덧셈 경로를 두어 이것을 완화했다. GRU는 그 게이트를 셋에서 둘로 줄여 같은 발상을 더 가볍게 구현한 변형인데, 완화의 성격은 다르지 않다. 두 구조 모두 그 덧셈 경로에 게이트가 곱해지기 때문이다. 게이트가 0.99라는 좋은 값을 계속 유지해도 1000 스텝이면 0.991000≈4.3×10−50.99^{1000} \approx 4.3 \times 10^{-5} 다. LSTM·GRU가 완화지 해결이 아니라는 말이 이 뜻이다 — 소실이 일어나는 길이를 수십에서 수백 스텝으로 밀어냈을 뿐 곱셈 사슬 자체는 그대로다. 활성 함수와 초기화를 포함한 더 일반적인 논의는 기울기 소실과 폭발에 있다.

Attention이 이 문제를 일부 우회하기는 한다. 디코더에서 인코더 상태로 가는 기울기는 α\alpha 를 타고 직접 흐르므로 순환 사슬을 건너뛴다. 그러나 인코더 안에서 h1h_1 부터 h500h_{500} 까지 가는 경로는 손대지 않았다. 병목은 사라졌는데 사슬은 남았다.

RNN의 한계: 기울기 소실과 병렬화 불가

순차 처리와 병렬화 장벽

두 번째 한계는 수식이 아니라 정의에서 나온다. hth_t 는 ht−1h_{t-1} 이 계산된 뒤에만 구할 수 있다. 이것은 최적화로 우회할 수 있는 구현 문제가 아니라 모델의 정의 자체다.

# RNN: T 스텝을 순서대로 — 앞 스텝이 끝나야 다음이 시작된다
for t in range(T):
    h = rnn_cell(x[:, t], h)

# Self-Attention: 모든 위치를 한 번에
out = attention(Q, K, V)          # (B, T, d)

1000 토큰이면 커널을 1000번 순서대로 띄워야 한다. 그런데 각 스텝이 하는 일은 GPU 기준으로 우스울 만큼 작다 — 배치 32에 은닉 256이면 행렬 곱 하나가 200만 번 곱셈 정도이고, 현대 GPU는 이 정도를 채우지 못한 채 대부분의 시간을 커널을 띄우고 동기화하는 데 쓴다. 연산량이 아니라 직렬 깊이가 벽이다.

Attention 자체는 완전히 병렬이지만 그 입력을 만드는 인코더가 순차이므로 전체는 여전히 O(T)O(T) 의 직렬 깊이를 가진다. 같은 하드웨어에서 Transformer가 수십 배 빨리 학습하는 이유가 바로 이 자리다.

장거리 의존성의 경로 길이

장거리 의존성은 멀리 떨어진 두 토큰이 서로를 참조해야 하는 상황이다. 「내가 어제 서점에서 우연히 발견한 그 두꺼운 책은 생각보다 재미있었다」에서 주어 「책은」과 서술어 「재미있었다」 사이에는 아무 관계도 없는 토큰이 여럿 끼어 있다.

단방향 RNN에서 t=1t=1 의 정보가 t=500t=500 에 닿으려면 중간 상태 498개를 지나야 한다. 그리고 각 단계에서 은닉 상태는 새 입력과 섞여 덮어쓰이므로 원래 정보는 조금씩 희석된다. 기울기 소실과 원인이 같아 보이지만 방향이 반대다 — 기울기 소실은 학습 신호가 뒤에서 앞으로 못 가는 것이고, 이쪽은 정보가 앞에서 뒤로 못 가는 것이다.

희석이 얼마나 진행되는지는 손으로 확인해 볼 수 있다. 길이 500짜리 무작위 입력을 RNN에 넣고 첫 스텝의 출력과 마지막 스텝의 출력이 이루는 코사인 유사도를 재 보면 된다. 두 벡터가 같은 정보를 공유하고 있다면 유사도가 유의미하게 남아 있어야 하는데, 실제로는 0 근처에서 요동한다. 499번의 덮어쓰기를 지나며 첫 토큰의 흔적이 사라진 것이다. 뒤에서 볼 Self-Attention에서는 사정이 다르다 — 첫 위치의 값이 마지막 위치의 계산식에 가중치 하나를 사이에 두고 그대로 등장한다. 희석될 중간 단계가 아예 없으니 유사도를 재 볼 것도 없다.

인코더를 양방향으로 감싸면 상황이 나아지지만 흔히 말하듯 「경로가 절반」인 것은 아니다. 양방향 RNN은 순방향 층과 역방향 층을 따로 돌려 이어 붙이는 구조인데, 순방향 층에서 1번 위치의 정보가 nn 번 위치로 가는 경로는 여전히 n−1n-1 스텝이다. 절반이 되는 것은 시퀀스 전체를 대표하는 벡터를 만들 때나, 양쪽 끝에서 동시에 출발해 만나는 가운데 위치에 한해서다. 양방향으로 감싸도 차수는 그대로 O(n)O(n) 이고 상수만 사정이 좋아진다.

구조 t=1→t=nt=1 \to t=n 경로 n=500n = 500 일 때
단방향 RNN n−1n - 1 499
양방향 RNN (가운데 위치 기준) 약 n/2n/2 약 250
Attention (디코더 → 인코더) 1 1
Self-Attention (인코더 내부) 1 1 — 아직 없던 줄

이 표에서 2015년까지 채워진 것은 셋째 줄까지다. Attention은 디코더와 인코더 사이의 경로를 O(1)O(1) 로 만들었지만, 인코더 내부와 디코더 내부는 첫째 줄과 둘째 줄에 그대로 남겨 두었다. 병목이 사라진 뒤에도 인코더가 500 스텝을 굴러야 한다는 사실은 변하지 않는다. 넷째 줄을 누가 채울 것인가 — 여기서 다음 걸음이 나온다.

순환의 제거

Self-Attention

Attention이 인코더와 디코더 사이에서 통한다면 같은 시퀀스 안에서도 통하지 않을 이유가 없다. 쿼리·키·밸류를 모두 같은 시퀀스에서 뽑는 것이 Self-Attention이다. 각 위치가 나머지 모든 위치를 직접 보고 자기 표현을 갱신한다.

Vaswani et al.이 여기서 한 결단은 간단하다. Self-Attention이 인코더 내부의 정보 전달을 맡을 수 있다면 순환은 더 이상 필요 없다. 남은 것은 쿼리·키·밸류를 각각 선형 변환으로 만들어 앞에서 본 스케일드 내적을 적용하는 일뿐이다.

import math

def scaled_dot_product_attention(Q, K, V, mask=None):
    d_k = Q.size(-1)
    scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k)
    if mask is not None:
        scores = scores.masked_fill(mask, float('-inf'))
    alpha = F.softmax(scores, dim=-1)
    return alpha @ V, alpha          # (B, T, d_v), (B, T, T)

앞의 두 절에서 각각 나온 것이 여기 한 줄에 모여 있다. 앞에서 쿼리·키·밸류에 이름을 붙일 때 「키와 밸류가 같은 벡터 hih_i 였다」고 적어 두었는데, 갈라지는 자리가 바로 여기다. WQW_Q·WKW_K·WVW_V 셋을 따로 두면 같은 입력에서 서로 다른 세 벡터가 나오고, 그러면 「무엇으로 점수를 매길지」와 「무엇을 꺼내 올지」가 분리된다. 제목만 보고 고른 다음 내용을 가져오는 것과 같다. 여기에 dk\sqrt{d_k} 가 붙어 큰 차원에서도 기울기가 산다. 그리고 반환값 α\alpha 의 모양이 (B,T,T)(B, T, T) 라는 점에 주목할 만하다 — 모든 위치 쌍에 대한 가중치가 한 번에 나온다. h1h_1 에서 h500h_{500} 까지의 경로 길이가 498에서 1이 되는 순간이다.

위치 인코딩

RNN을 빼면 공짜로 딸려 오던 것 하나도 함께 사라진다. 순서다. Self-Attention은 순열 동변성(permutation equivariance)을 갖는다 — 입력 위치를 섞으면 출력도 똑같이 섞일 뿐, 각 토큰이 받는 값 자체는 변하지 않는다. 가중합에는 순서 개념이 없기 때문이다. 섞어도 출력이 통째로 그대로인 성질은 불변(invariance)이라 부르고 그건 다른 이야기다 — 여기서는 출력이 입력을 따라 같이 섞이므로 동변성이 맞는 이름이다.

RNN에서는 순서가 구조에 새겨져 있었다. tt 번째 입력은 반드시 tt 번째로 들어가고 그 사실이 상태에 남는다. 그것을 버렸으니 순서를 다른 방법으로 넣어 줘야 한다. Transformer는 위치마다 다른 벡터를 만들어 입력 임베딩에 더한다 — 위치 인코딩이다.

이 장치가 없으면 「나는 너를 좋아한다」와 「너는 나를 좋아한다」가 모델에게 같은 입력이 된다. 순서가 뜻을 바꾸는 언어에서 이것은 치명적이므로, 위치 인코딩은 선택 사항이 아니라 RNN 제거의 필수 대가다. 사인·코사인 방식과 학습형 방식의 차이는 Transformer 기초에서 다룬다.

차수의 교환

셋을 나란히 놓으면 무엇이 정말 달라졌는지 보인다.

한계 RNN·LSTM·GRU의 대응 Transformer의 답
기울기 소실 LSTM·GRU 게이트로 완화 잔차 연결과 정규화 층으로 경로 확보
순차 처리 우회 불가 Self-Attention으로 완전 병렬
장거리 의존성 게이트로 일부 완화 모든 쌍이 경로 길이 1
계산 복잡도 길이에 비례 길이의 제곱에 비례 — 새 부담

마지막 줄이 청구서다. 모든 위치 쌍을 계산하므로 점수 행렬의 크기가 n2n^2 이다. n=1,000n = 1{,}000 이면 100만 칸이고 n=10,000n = 10{,}000 이면 1억 칸이라, 길이를 열 배 늘리면 메모리가 백 배가 된다. RNN이라면 열 배로 끝났을 일이다.

그러니 정확히 말하면 Transformer는 세 한계를 없앤 것이 아니라 차수를 바꿔치기했다. 경로 길이와 직렬 깊이를 O(n)O(n) 에서 O(1)O(1) 로 내리고, 그 대가로 계산과 메모리를 O(n)O(n) 에서 O(n2)O(n^2) 으로 올렸다. 이 거래가 이득이었던 이유는 GPU가 잘하는 것이 큰 행렬 곱이고 못하는 것이 긴 직렬 대기이기 때문이다. 하드웨어의 모양이 아키텍처의 승부를 갈랐다. 뒤에 나온 FlashAttention·Sliding Window·Linear Attention은 전부 이 청구서를 깎으려는 시도이고, 효율적인 트랜스포머가 그 계보를 다룬다.

RNN vs LSTM vs Transformer 비교

Attention 이후의 지형

연도별 계보

지금까지의 흐름을 연도로 늘어놓으면 각 걸음이 앞 걸음의 한계에 답하고 있다는 것이 보인다.

연도 사건 앞 단계의 무엇에 답했나
2013 Word2Vec 단어를 벡터로 놓는 자리
2014 Seq2Seq 길이가 다른 입출력을 잇는 구조
2015 Attention (Bahdanau, Luong) 고정 Context Vector의 병목
2017 Transformer 순차 처리와 경로 길이
2018 BERT 양방향 사전학습과 전이
2019~ GPT-2, GPT-3, PaLM, LLaMA 규모와 일반화

한 줄 건너뛸 때마다 「그 전에 남아 있던 것」이 무엇이었는지를 물으면 다음 줄이 나온다. Transformer는 하늘에서 떨어진 아이디어가 아니라 Attention이 절반만 푼 문제의 나머지 절반이었다.

상태 공간 모델

그렇다고 RNN이 사라진 것은 아니다. 오히려 Transformer가 비싼 자리에서 순환의 장점이 되살아난다. 추론 시 메모리가 그 자리다. RNN은 상태 벡터 하나만 들고 다음 토큰을 만들 수 있어 문장 길이와 무관하게 O(1)O(1) 이다. Transformer는 지금까지의 모든 위치에 대한 키와 밸류를 캐시로 들고 있어야 하므로 길이에 비례해 메모리가 는다. 긴 대화를 이어 가는 서비스에서 이 차이는 그대로 비용이 된다.

그래서 2023년 이후 Mamba를 비롯한 상태 공간 모델(S4·SSM) 계열이 다시 주목받았다. 상태 공간 모델은 시퀀스를 상태 하나가 시간에 따라 굴러가는 선형 시스템으로 보고, 그 시스템의 계수를 학습하는 방식이다. 발상은 「학습은 Transformer처럼 병렬로, 추론은 RNN처럼 상태 하나로」다. 순환의 형태를 유지하되 시간축을 따라 병렬로 펼칠 수 있는 형태로 바꾼 것이라, 이 글에서 본 두 번째 한계만 정확히 겨냥해 걷어낸 셈이다. 온디바이스 추론과 스트리밍처럼 메모리 상한이 빡빡한 자리에서 특히 쓸모가 있다.

여기까지 오면서 우리는 hih_i 를 「인코더가 만든 상태」라고만 부르고 그 안에 무엇이 들었는지는 묻지 않았다. Attention이 정하는 것은 어느 벡터를 얼마나 볼 것인가이지 그 벡터가 애초에 어떤 공간에 놓여 있는가가 아니다. 그런데 점수를 내적으로 매긴다는 것은 이미 「방향이 비슷하면 의미가 비슷하다」는 가정을 깔고 있다는 뜻이다. 다음 글은 그 앞단으로 간다 — 단어 하나를 벡터 하나에 대응시키는 자리, 그리고 그 공간에서 거리와 방향이 왜 뜻을 갖게 되는지가 주제다.


읽어주셔서 감사합니다. 😊

LATEST

머신러닝·신경망의 최신 글

머신러닝·신경망2026.05.08

문맥적 임베딩: ELMo부터 BERT까지

정적 임베딩의 다의어 문제를 해결하는 문맥적 임베딩의 원리, ELMo의 양방향 LSTM 레이어 표현, BERT의 트랜스포머 기반 서브워드 임베딩 추출법을 수식과 코드로 완전히 해설한다.

12 MIN
머신러닝·신경망2026.05.08

FastText: 부분 단어로 OOV를 정복하다

FastText가 문자 n-gram 기반의 부분 단어 모델로 OOV 문제를 해결하는 방법, 한국어 형태론에서의 강점, 실전 학습과 추론 코드를 완전히 해설한다.

11 MIN
머신러닝·신경망2026.05.08

GloVe: 전역 공기 통계로 단어 벡터를 만들다

GloVe가 공기 행렬의 전역 통계와 국소 문맥 창의 장점을 결합하는 방법, 목적 함수의 수학적 의미, 사전 학습 벡터 활용법을 깊이 있게 다룬다.

11 MIN