Natural Language Processing Theory : 어텐션(Attention)
시퀀스-투-시퀀스(Sequence-to-Sequence)
순환 신경망(Recurrent Neural Network)은 문장을 읽고 분류하거나, 각 단어에 태그를 붙이는 과제를 잘 해결했습니다. 그러나 번역은 성격이 다릅니다. “나는 학교에 간다”는 네 개의 토큰이지만, 영어 번역 “I go to school”도 우연히 네 개일 뿐 일반적으로 입력과 출력의 길이가 다르고, 단어의 순서도 다릅니다. 한국어는 동사가 문장 끝에 오고 영어는 주어 바로 뒤에 오므로, 한 단어씩 순서대로 바꾸는 방식으로는 번역할 수 없습니다.
2014년 수츠케버(Ilya Sutskever) 연구진과 조경현 연구진은 이 문제를 두 개의 순환 신경망으로 해결했습니다. 하나는 입력 문장을 끝까지 읽어 의미를 요약하고, 다른 하나는 그 요약을 받아 출력 문장을 처음부터 생성합니다. 나열을 받아 다른 나열을 만든다는 뜻에서 이 구조를 시퀀스-투-시퀀스(Sequence-to-Sequence, Seq2Seq)라 합니다.
인코더(Encoder): 입력 문장의 토큰을 순서대로 읽으며 은닉 상태를 갱신합니다. 마지막 시점의 은닉 상태는 문장 전체를 요약한 벡터가 되며, 이를컨텍스트 벡터(Context Vector)라 합니다.디코더(Decoder): 컨텍스트 벡터를 첫 은닉 상태로 받아, 출력 문장을 한 토큰씩 생성합니다. 매 시점에 다음 토큰의 확률을 계산하고, 고른 토큰을 다음 시점의 입력으로 다시 넣습니다.
디코더는 문장의 시작을 알리는 특수 토큰 <sos>를 첫 입력으로 받아 생성을 시작하고, 문장의 끝을 뜻하는 <eos>를 출력하면 생성을 멈춥니다. 이 두 토큰 덕분에 출력 길이를 미리 정하지 않아도 모델이 스스로 언제 끝낼지 결정할 수 있습니다.
시퀀스-투-시퀀스는 번역뿐 아니라 요약, 질의응답, 대화처럼 입력과 출력이 모두 문장인 과제에 그대로 적용됩니다. 자연어 처리란?에서 대규모 언어 모델이 모든 과제를 “텍스트를 입력받아 텍스트를 출력하는 하나의 문제”로 통일했다고 설명했는데, 그 문제 설정의 출발점이 바로 이 구조입니다.
학습과 생성
디코더는 학습할 때와 실제로 문장을 생성할 때 입력이 다릅니다. 생성할 때는 직전에 자신이 출력한 토큰을 다음 입력으로 사용하지만, 학습 초기의 모델은 대부분 틀린 토큰을 출력합니다. 틀린 토큰을 다음 입력으로 넣으면 그 뒤의 예측도 줄줄이 어긋나, 정답 문장의 뒤쪽 부분을 제대로 학습할 수 없습니다.
그래서 학습할 때는 모델의 출력 대신 정답 문장의 토큰을 다음 입력으로 넣습니다. 이를 교사 강요(Teacher Forcing)라 하며, 매 시점이 올바른 문맥에서 출발하므로 학습이 빠르고 안정적입니다. 대신 학습 때는 항상 정답만 보다가 생성할 때 처음으로 자신의 실수를 입력으로 받게 되는 불일치가 생기는데, 이를 노출 편향(Exposure Bias)이라 합니다.
생성할 때는 매 시점 어휘 전체에 대한 확률이 나오며, 여기서 토큰을 고르는 방식을 디코딩(Decoding) 전략이라 합니다.
탐욕적 디코딩(Greedy Decoding): 매 시점 확률이 가장 높은 토큰 하나를 고릅니다. 빠르고 단순하지만, 지금 가장 좋아 보이는 선택이 문장 전체로는 최선이 아닐 수 있습니다. 첫 단어를 잘못 고르면 되돌릴 방법이 없습니다.빔 서치(Beam Search): 매 시점 확률이 높은 후보 문장을 여러 개 유지하며 생성합니다. 유지하는 후보의 수를빔 크기(Beam Size)라 하며, 각 후보를 한 토큰씩 늘린 뒤 전체 확률이 높은 것만 다시 남기는 과정을 반복합니다. 탐욕적 디코딩보다 느리지만 번역처럼 정답이 비교적 명확한 과제에서 더 좋은 문장을 찾습니다.
- Tip : 빔 서치는 문장 전체의 확률을 곱해서 비교하므로, 토큰이 많을수록 확률이 작아져 짧은 문장을 선호하는 경향이 있습니다. 그래서 실제로는 확률을 문장 길이로 나누어 보정하는 길이 정규화를 함께 사용합니다.
컨텍스트 벡터의 병목
시퀀스-투-시퀀스에서 인코더와 디코더를 잇는 유일한 통로는 컨텍스트 벡터 하나입니다. 입력 문장이 다섯 단어든 오십 단어든, 그 의미 전체를 수백 차원의 고정된 벡터 하나에 담아 디코더로 넘겨야 합니다. 이처럼 넓은 정보가 좁은 통로를 지나며 손실되는 지점을 병목(Bottleneck)이라 합니다.
실제로 2014년 조경현 연구진은 순환 신경망 기반 번역 모델의 성능이 문장이 길어질수록 급격히 떨어진다는 것을 보고했습니다. 순환 신경망에서 다룬 것처럼 마지막 은닉 상태에는 뒤쪽 단어의 정보가 진하게, 앞쪽 단어의 정보가 희미하게 남기 때문입니다. 인코더가 이미 첫 단어를 거의 잊은 상태에서, 디코더는 번역문의 첫 단어를 만들어야 합니다.
수츠케버 연구진은 입력 문장을 거꾸로 뒤집어 넣는 방법으로 이 문제를 일부 완화했습니다. 입력의 첫 단어가 인코더의 마지막에 읽히므로, 번역문의 첫 단어를 만들 때 필요한 정보가 컨텍스트 벡터에 진하게 남게 됩니다. 그러나 이는 문장의 다른 부분을 희생하는 임시방편이었습니다.
사람이 번역할 때를 생각해 보면 해결의 실마리가 보입니다. 사람은 원문을 한 번 읽고 그 기억만으로 번역문을 쓰지 않습니다. 번역문의 각 단어를 쓸 때마다 원문의 해당 부분을 다시 들여다봅니다. “school”을 쓸 때는 “학교”를, “go”를 쓸 때는 “간다”를 봅니다. 이 아이디어를 신경망으로 구현한 것이 어텐션(Attention)입니다.
어텐션(Attention)
어텐션(Attention)은 디코더가 토큰을 하나 생성할 때마다 인코더의 모든 시점의 은닉 상태를 다시 참고하되, 지금 생성하려는 토큰과 관련이 큰 위치에 더 많은 주의를 기울이는 방법입니다. 2014년 바다나우(Dzmitry Bahdanau), 조경현, 요슈아 벤지오(Yoshua Bengio)가 번역 모델에 처음 도입했습니다.
기존 시퀀스-투-시퀀스는 인코더의 마지막 은닉 상태만 사용하고 나머지는 버렸습니다. 어텐션은 인코더의 은닉 상태 \(\mathbf{h}_{1}, \mathbf{h}_{2}, \ldots, \mathbf{h}_{n}\)을 모두 보관해 두고, 디코더가 시점마다 이를 새롭게 조합해 그 시점만을 위한 컨텍스트 벡터를 만듭니다. 컨텍스트 벡터가 하나에서 출력 토큰마다 하나씩으로 바뀌는 것이며, 이로써 고정된 크기의 병목이 사라집니다.
디코더의 현재 은닉 상태를 \(\mathbf{s}_{t}\)라 할 때, 어텐션은 다음 세 단계로 계산됩니다.
\[e_{t,i} = \text{score}(\mathbf{s}_{t}, \mathbf{h}_{i}), \qquad \alpha_{t,i} = \frac{\exp(e_{t,i})}{\sum_{j=1}^{n} \exp(e_{t,j})}, \qquad \mathbf{c}_{t} = \sum_{i=1}^{n} \alpha_{t,i} \mathbf{h}_{i}\]- 점수 계산 : 디코더 상태 \(\mathbf{s}_{t}\)와 인코더의 각 은닉 상태 \(\mathbf{h}_{i}\)가 얼마나 관련 있는지 점수 \(e_{t,i}\)를 계산합니다. 점수를 계산하는 함수는 뒤에서 다룹니다.
- 가중치 계산 : 점수에
소프트맥스(Softmax)를 적용해 모두 더하면 1이 되는 확률 분포 \(\alpha_{t,i}\)로 바꿉니다. 이 값을어텐션 가중치(Attention Weight)라 하며, 각 입력 위치에 주의를 얼마나 나눠 줄지를 뜻합니다. - 가중합 : 인코더 은닉 상태를 어텐션 가중치로 가중 평균해 이번 시점의 컨텍스트 벡터 \(\mathbf{c}_{t}\)를 만듭니다.
마지막으로 디코더는 이 컨텍스트 벡터와 자신의 은닉 상태를 함께 사용해 다음 토큰을 예측합니다. 어떤 위치에 주의를 기울일지는 사람이 정하지 않으며, 번역 결과의 손실을 줄이는 방향으로 역전파를 통해 스스로 학습됩니다.
어텐션 계산 예제
간단한 수치로 계산 과정을 따라가 보겠습니다. 입력 문장이 세 개의 토큰으로 이루어져 있고, 설명을 위해 인코더의 은닉 상태를 2차원 벡터 \(\mathbf{h}_{1} = [1, 0]\), \(\mathbf{h}_{2} = [0, 1]\), \(\mathbf{h}_{3} = [1, 1]\)이라 하겠습니다. 디코더의 현재 상태와 비교한 점수가 각각 2.0, 1.0, 0.1로 계산되었다고 가정합니다.
먼저 점수에 소프트맥스를 적용합니다. \(e^{2.0} \approx 7.389\), \(e^{1.0} \approx 2.718\), \(e^{0.1} \approx 1.105\)이고, 합은 약 11.212입니다. 각 값을 합으로 나누면 어텐션 가중치는 다음과 같습니다.
\[\alpha = [0.659, \ 0.242, \ 0.099]\]점수가 가장 높은 첫 번째 토큰이 전체 주의의 약 66%를 가져가고, 세 번째 토큰은 10%도 받지 못합니다. 소프트맥스는 지수 함수를 사용하므로, 점수의 작은 차이를 큰 비율 차이로 증폭한다는 점을 확인할 수 있습니다. 그렇다고 세 번째 토큰을 완전히 무시하지는 않으며, 모든 위치가 0보다 큰 가중치를 받습니다.
이제 은닉 상태를 가중치로 더해 컨텍스트 벡터를 만듭니다.
\[\mathbf{c} = 0.659 \times [1, 0] + 0.242 \times [0, 1] + 0.099 \times [1, 1] = [0.758, \ 0.341]\]결과 벡터는 첫 번째 은닉 상태 \([1, 0]\)에 가장 가깝고, 나머지 상태의 정보가 조금씩 섞여 있습니다. 디코더가 다음 시점으로 넘어가면 디코더 상태가 바뀌므로 점수도 다시 계산되고, 이번에는 다른 토큰에 주의가 집중된 새 컨텍스트 벡터가 만들어집니다.
- Tip : 어텐션 가중치는 가중 평균이므로 컨텍스트 벡터는 언제나 인코더 은닉 상태들이 만드는 범위 안에 놓입니다. 어텐션은 새로운 정보를 만들어 내는 것이 아니라, 이미 있는 정보 중에서 지금 필요한 것을 골라 섞는 연산입니다.
점수 함수(Score Function)
디코더 상태와 인코더 상태가 얼마나 관련 있는지를 계산하는 방법에는 여러 가지가 있습니다. 대표적인 것은 다음 세 가지입니다.
\[\text{score}(\mathbf{s}, \mathbf{h}) = \begin{cases} \ \mathbf{s}^{\top} \mathbf{h} & \text{내적(Dot)} \\ \ \mathbf{s}^{\top} W \mathbf{h} & \text{일반(General)} \\ \ \mathbf{v}^{\top} \tanh \left( W_{1} \mathbf{s} + W_{2} \mathbf{h} \right) & \text{가산(Additive)} \end{cases}\]가산 어텐션(Additive Attention)은 바다나우 연구진이 처음 제안한 방식으로, 두 벡터를 각각 변환해 더한 뒤 작은 신경망을 통과시켜 점수를 얻습니다. 두 벡터의 차원이 달라도 사용할 수 있고 표현력이 높지만, 점수를 계산할 때마다 신경망 연산이 필요합니다. 제안자의 이름을 따 바다나우 어텐션(Bahdanau Attention)이라고도 합니다.
2015년 루옹(Minh-Thang Luong) 연구진은 더 단순한 내적(Dot) 방식과 일반(General) 방식을 비교했습니다. 내적 방식은 두 벡터의 내적을 그대로 점수로 사용하므로 추가 매개변수가 없고 매우 빠릅니다. 두 벡터가 같은 방향을 가리킬수록 점수가 커지므로, 단어 임베딩에서 다룬 코사인 유사도와 비슷한 직관으로 이해할 수 있습니다. 일반 방식은 그 사이에 학습 가능한 행렬 \(W\)를 넣어, 두 벡터가 서로 다른 공간에 있어도 비교할 수 있게 합니다. 이 두 방식을 묶어 곱셈 어텐션(Multiplicative Attention) 또는 루옹 어텐션(Luong Attention)이라 부릅니다.
곱셈 어텐션은 모든 위치의 점수를 행렬 곱 한 번으로 계산할 수 있어 GPU에서 매우 효율적입니다. 이 장점 때문에 이후의 모델은 대부분 내적 기반의 어텐션을 사용하게 됩니다.
어텐션 가중치의 해석
어텐션은 성능 향상뿐 아니라 모델이 무엇을 보고 판단했는지 들여다볼 수 있는 창을 제공했습니다. 출력 토큰을 행으로, 입력 토큰을 열로 두고 어텐션 가중치를 색의 진하기로 칠하면, 각 출력 단어를 만들 때 어느 입력 단어를 참고했는지 한눈에 보입니다. 이 그림을 어텐션 맵(Attention Map)이라 합니다.
바다나우 연구진의 영어-프랑스어 번역 실험에서, 어텐션 맵은 별도의 지도 없이도 두 언어의 단어 대응 관계를 정확히 찾아냈습니다. 대부분은 대각선을 따라 진하게 나타났지만, 영어의 “European Economic Area”가 프랑스어에서 “zone économique européenne”로 순서가 뒤집히는 부분에서는 대각선이 반대 방향으로 꺾였습니다. 모델이 어순의 차이를 스스로 학습했다는 증거입니다. 한국어와 영어처럼 어순이 크게 다른 언어 쌍에서는 이러한 꺾임이 훨씬 많이 나타납니다.
다만 어텐션 가중치를 모델의 판단 근거로 그대로 받아들이는 데는 주의가 필요합니다. 어텐션 가중치가 높은 위치를 바꿔도 결과가 거의 바뀌지 않거나, 서로 매우 다른 어텐션 분포가 같은 결과를 내는 경우도 있다는 연구가 보고되었습니다. 어텐션 맵은 모델의 동작을 짐작하는 데 유용한 단서이지만, 엄밀한 설명은 아닙니다.
쿼리, 키, 값(Query, Key, Value)
어텐션의 계산을 한 걸음 물러서서 보면, 이는 검색과 같은 구조를 가지고 있습니다. 도서관에서 책을 찾을 때 우리는 찾고 싶은 내용을 떠올리고(질의), 책마다 붙어 있는 제목이나 분류표와 비교한 뒤(색인), 관련 있는 책의 내용을 가져옵니다(내용). 어텐션도 마찬가지로 세 가지 역할로 나눌 수 있습니다.
쿼리(Query): 무엇을 찾고 있는지를 나타내는 벡터입니다. 시퀀스-투-시퀀스에서는 디코더의 현재 상태 \(\mathbf{s}_{t}\)가 쿼리입니다.키(Key): 각 위치가 어떤 내용을 가지고 있는지 알리는 벡터로, 쿼리와 비교해 점수를 계산하는 데 쓰입니다.값(Value): 점수에 따라 실제로 가져올 내용입니다. 가중합의 대상이 되는 벡터입니다.
지금까지의 어텐션에서는 인코더의 은닉 상태가 키와 값의 역할을 동시에 맡았습니다. 그러나 두 역할은 본질적으로 다릅니다. “이 위치를 찾아야 하는가”를 판단하는 데 필요한 정보와 “찾았을 때 무엇을 가져갈 것인가”에 필요한 정보는 같을 필요가 없습니다. 그래서 같은 은닉 상태에 서로 다른 가중치 행렬을 곱해 키와 값을 따로 만드는 방식이 등장했습니다.
쿼리, 키, 값을 각각 행렬 \(Q\), \(K\), \(V\)로 모으면, 모든 쿼리에 대한 어텐션을 한 번의 식으로 쓸 수 있습니다.
\[\text{Attention}(Q, K, V) = \text{softmax} \left( \frac{Q K^{\top}}{\sqrt{d_{k}}} \right) V\]\(Q K^{\top}\)는 모든 쿼리와 모든 키의 내적을 한 번에 계산한 점수 행렬이고, 소프트맥스를 거쳐 가중치가 되며, 마지막으로 \(V\)와 곱해 가중합을 얻습니다. 내적 점수를 키의 차원 \(d_{k}\)의 제곱근으로 나누기 때문에 이를 스케일드 닷-프로덕트 어텐션(Scaled Dot-Product Attention)이라 합니다.
\(\sqrt{d_{k}}\)로 나누는 이유는 차원이 클수록 내적 값이 커지기 때문입니다. 각 성분이 평균 0, 분산 1인 두 벡터의 내적은 분산이 차원 수 \(d_{k}\)에 비례해 커집니다. 점수의 크기가 커지면 소프트맥스가 한 위치에만 거의 1을, 나머지에는 거의 0을 주게 되며, 이 상태에서는 기울기가 매우 작아져 학습이 잘 되지 않습니다. 제곱근으로 나누면 점수의 분산이 차원과 무관하게 1 정도로 유지됩니다.
셀프 어텐션(Self-Attention)
지금까지의 어텐션은 디코더가 다른 문장인 인코더의 입력을 바라보는 방식이었습니다. 그런데 쿼리, 키, 값을 모두 같은 문장에서 만들면 어떻게 될까요? 문장의 각 토큰이 쿼리가 되어, 같은 문장의 모든 토큰을 키와 값으로 삼아 어텐션을 계산하는 것입니다. 이를 셀프 어텐션(Self-Attention)이라 합니다.
“그 동물은 너무 피곤해서 길을 건너지 않았다”에서 “피곤해서”가 누구의 상태인지 알려면 “동물”을 봐야 합니다. 셀프 어텐션에서는 “피곤해서”의 쿼리가 문장의 모든 토큰과 비교되고, “동물”과의 점수가 높게 나오면 “동물”의 정보를 많이 가져와 자신의 표현에 섞습니다. 결과적으로 각 토큰의 표현은 문장 안에서 관련 있는 다른 토큰들의 정보로 보강되며, 이것이 단어 임베딩에서 말한 문맥 임베딩을 만드는 또 하나의 방법이 됩니다.
셀프 어텐션은 순환 신경망의 한계를 정면으로 해결합니다. 첫째, 모든 토큰 쌍의 관계를 한 번의 행렬 곱으로 계산하므로 순서대로 처리할 필요가 없어 병렬 처리가 가능합니다. 둘째, 첫 단어와 마지막 단어가 중간 단어를 거치지 않고 직접 연결되므로, 거리가 멀어도 정보가 희미해지지 않습니다. 순환 신경망에서는 거리가 \(n\)인 두 토큰 사이에 \(n\)단계를 거쳐야 했지만, 셀프 어텐션에서는 한 단계면 충분합니다.
대신 대가도 있습니다. 모든 토큰 쌍의 점수를 계산하므로, 문장 길이가 \(n\)이면 점수 행렬의 크기가 \(n \times n\)이 되어 연산량과 메모리가 길이의 제곱에 비례해 늘어납니다. 또한 모든 위치를 동시에 보므로 순서 정보가 사라져, “개가 사람을 물었다”와 “사람이 개를 물었다”를 구분하려면 위치 정보를 따로 넣어 주어야 합니다.
2017년 바스와니(Ashish Vaswani) 연구진은 “Attention Is All You Need”라는 논문에서 순환 구조를 완전히 없애고 셀프 어텐션만으로 인코더와 디코더를 구성한 트랜스포머(Transformer)를 발표했습니다. 트랜스포머는 번역 성능과 학습 속도 모두에서 순환 신경망 기반 모델을 앞질렀고, 이후 거의 모든 자연어 처리 모델의 기반이 되었습니다. 다음 강좌에서는 트랜스포머의 구조를 자세히 다룹니다.
어텐션 정리
| 구분 | 쿼리 | 키 · 값 | 점수 계산 | 특징 |
|---|---|---|---|---|
| 기본 Seq2Seq | 없음 | 마지막 은닉 상태만 사용 | 없음 | 고정 크기 컨텍스트 벡터의 병목 |
| 가산 어텐션 | 디코더 상태 | 인코더 은닉 상태 | 작은 신경망 | 최초의 어텐션, 표현력이 높음 |
| 곱셈 어텐션 | 디코더 상태 | 인코더 은닉 상태 | 내적 또는 행렬을 낀 내적 | 빠르고 행렬 곱으로 병렬화 쉬움 |
| 스케일드 닷-프로덕트 | 별도 변환 | 별도 변환 | 내적을 \(\sqrt{d_{k}}\)로 나눔 | 키와 값의 역할 분리, 학습 안정 |
| 셀프 어텐션 | 같은 문장 | 같은 문장 | 스케일드 닷-프로덕트 | 병렬 처리, 먼 거리도 한 단계 |
어텐션은 고정된 요약 하나에 의존하지 말고, 필요할 때마다 원본을 다시 보자는 단순한 발상에서 출발했습니다. 처음에는 순환 신경망 기반 번역 모델의 병목을 보완하는 장치였지만, 쿼리·키·값이라는 일반적인 틀로 정리되면서 그 자체로 문장을 처리하는 핵심 연산이 되었습니다.
이 과정은 자연어 처리 모델의 무게 중심이 순서대로 기억을 쌓아 가는 방식에서 모든 위치의 관계를 한꺼번에 계산하는 방식으로 옮겨 간 전환점이었습니다. 오늘날의 대규모 언어 모델이 수천에서 수십만 개의 토큰을 입력으로 받아 처리할 수 있는 것도 이 전환 위에 서 있습니다.
- Tip : 어텐션의 계산량이 길이의 제곱에 비례한다는 점은 지금도 긴 문서를 다루는 모델의 가장 큰 제약입니다. 일부 위치만 골라 계산하는 희소 어텐션이나, 메모리 접근 순서를 최적화해 같은 결과를 더 빠르게 계산하는 방법 등이 꾸준히 연구되고 있습니다.
- Writer by : 윤대희
댓글 남기기