개요
지난 글 끝에서 던진 질문이 이번 글의 출발점이다.
RNN이 하던 “정보 전달”을 Attention이 더 잘한다면, 굳이 RNN이 필요할까?
2017년에 나온 논문 제목이 그 답을 대놓고 말한다. “Attention Is All You Need” — Attention만 있으면 된다.
이번 글에서는 그 주장이 어떻게 성립하는지, 그리고 그렇게 만든 구조인 Transformer를 정리한다. 지금 우리가 쓰는 거의 모든 언어 모델이 이 구조 위에 있다.
교육 자료는 대외비라 슬라이드 이미지나 예제 데이터는 싣지 않는다. 개념 흐름만 내가 이해한 방식으로 재구성했고, 그림은 전부 생성했다.
1. RNN을 빼야 하는 두 가지 이유
Attention을 붙였어도 RNN은 여전히 뼈대로 남아 있었다. 그런데 RNN에는 구조적인 문제가 둘 있다.
① 멀리 있는 단어까지 가는 데 단계가 필요하다
RNN은 정보를 옆으로 한 칸씩 넘긴다. 30단어 떨어진 단어의 정보를 쓰려면 30번을 거쳐야 한다. 거치는 동안 신호가 희미해지는 게 앞에서 본 기울기 소실이다.
② 병렬 처리가 안 된다
이쪽이 실무적으로 더 아프다. RNN은 1번 단어를 계산해야 2번 단어를 시작할 수 있다. 순서를 지켜야 하니 한꺼번에 계산할 수가 없다.
지난 시리즈에서 GPU가 강한 이유를 “서로 독립인 계산 수천 개를 동시에” 처리하기 때문이라고 정리했는데, RNN은 그 강점을 전혀 못 쓴다. 데이터가 커질수록 손해가 커진다.
계산기가 아무리 많아도 앞사람 답을 받아야 내 계산을 시작할 수 있다면 줄을 서서 기다리는 수밖에 없다. RNN이 딱 그 상태다.
Attention은 이 두 문제가 없다. 거리에 상관없이 한 번에 연결되고, 각 단어의 계산이 서로 독립적이다. 그래서 이런 결론이 나온다.
RNN을 빼고 Attention만 남기자.
2. Self-Attention
지난 글의 Attention은 번역문 쪽에서 원문 쪽을 보는 구조였다. 서로 다른 두 문장 사이의 관계였다.
Self-Attention은 이걸 한 문장 안으로 가져온다.
Self-Attention = 문장 안의 각 단어가 같은 문장의 다른 단어들을 보고 자기 표현을 다시 만드는 것
그림의 예를 보자. “거기”라는 단어가 무엇을 가리키는지 알려면 “카페”를 봐야 한다. 이 관계는 번역과 아무 상관이 없다. 같은 문장 안에서 필요한 정보다.
계산 방식은 지난 글과 똑같다. Query·Key·Value를 만들고, 점수를 매기고, 소프트맥스로 비중을 만들고, 그 비중대로 섞는다. 달라진 건 비교 대상이 같은 문장 안에 있다는 것뿐이다.
더 쉬운 비유로
단체 대화방에서 누가 “그거 어떻게 됐어?”라고 물었다고 하자. “그거”가 뭔지 알려면 위로 스크롤해서 앞 대화를 봐야 한다.
이때 우리는 대화를 처음부터 순서대로 다시 읽지 않는다. 관련 있어 보이는 메시지로 바로 건너뛴다. Self-Attention이 하는 게 그거다.
얻는 것
| RNN | Self-Attention | |
|---|---|---|
| 멀리 있는 단어까지 | 단어 수만큼 단계 | 한 걸음 |
| 계산 순서 | 앞에서부터 차례로 | 전부 동시에 |
| 병렬 처리 | 불가능 | 가능 |
3. 그런데 그대로는 안 된다 — 세 가지 구멍
Self-Attention만 덜렁 쓰면 곤란한 일이 생긴다. 세 가지 구멍이 있고, 각각 해결책이 붙는다.
① 순서를 모른다
Self-Attention은 단어끼리 얼마나 관련 있는지만 계산한다. 누가 먼저 나왔는지는 전혀 보지 않는다.
그러니 “형이 동생을 업었다”와 “동생이 형을 업었다”가 완전히 똑같이 계산된다. 2편에서 짚었던 문제가 통째로 돌아온 것이다. RNN은 적어도 순서대로 읽기라도 했는데, 이건 아예 순서 개념이 없다.
해결 — Positional Encoding(위치 인코딩)
단어 벡터에 “몇 번째 자리인지”를 나타내는 값을 더해서 넣어준다.
이러면 같은 단어라도 첫 번째 자리에 있을 때와 세 번째 자리에 있을 때 입력값 자체가 달라진다. 순서 정보가 데이터에 실려 들어가는 셈이다.
만드는 방법은 두 가지가 있다. 주기가 다른 파형을 조합해 만드는 방식과, 위치 벡터도 학습으로 배우게 하는 방식이다.
좌석 번호를 붙이는 것과 같다. 사람이 서 있는 순서를 따로 기억하지 않아도, 표에 자리 번호가 찍혀 있으면 누가 어디 있었는지 알 수 있다.
② 표현력이 약하다
Self-Attention이 하는 계산은 결국 가중 평균이다. 값에 비중을 곱해 더하는 것뿐이다.
지난 시리즈에서 이걸 이미 봤다. 곱하고 더하기만 하면 아무리 층을 쌓아도 결국 하나의 선형 변환이라는 이야기였다. 꺾어주는 무언가가 없으면 복잡한 패턴을 못 만든다.
해결 — Feed-Forward Network 추가
Attention을 통과한 각 단어 벡터에 작은 신경망을 하나씩 더 건다. 지난 시리즈에서 본 그 신경망이다. 넓혔다가 다시 좁히는 두 층으로 되어 있고, 중간에 ReLU 같은 활성화 함수가 들어간다.
FFN = 벡터를 넓게 펼친다 → ReLU로 꺾는다 → 다시 원래 크기로 줄인다
넓혔다가 줄이는 이유가 있다. 좁은 상태에서 바로 꺾으면 표현할 수 있는 모양이 제한된다. 일단 넓은 공간에 펼쳐놓고 꺾으면 훨씬 다양한 패턴을 만들 수 있고, 그 결과를 다시 원래 크기로 정리해서 다음 층에 넘긴다. 보통 4배 정도로 넓혔다가 되돌린다.
역할이 완전히 나뉜다는 게 핵심이다.
- Attention — 단어들 사이의 정보를 섞는다. “누가 누구와 관련 있나”
- FFN — 단어 하나하나를 깊이 가공한다. “이 단어를 어떻게 해석할까”
회의로 치면 Attention은 서로 의견을 주고받는 시간이고, FFN은 각자 자리로 돌아가 들은 내용을 소화하는 시간이다. 둘 다 있어야 회의가 굴러간다.
그리고 이 층은 단어마다 완전히 독립적으로 적용된다. 옆 단어를 볼 필요가 없으니 전부 동시에 계산할 수 있다. 병렬 처리를 조금도 방해하지 않는다.
참고로 Transformer 파라미터의 상당 부분이 이 FFN에 들어 있다. 이름은 수수한데 모델 용량의 큰 몫을 차지하는 층이다.
③ 미래를 훔쳐본다
문장을 생성할 때 생기는 문제다.
언어 모델은 왼쪽에서 오른쪽으로 한 단어씩 만들어 나간다. 그런데 Self-Attention은 기본적으로 문장 안의 모든 단어를 동시에 본다. 아직 만들지도 않은 뒤쪽 단어까지 보게 된다.
이러면 학습이 의미가 없어진다. 답을 보고 답을 맞히는 셈이니까.
해결 — Masked Self-Attention
지금 위치보다 뒤에 있는 단어의 점수를 아주 작은 값으로 만들어버린다.
소프트맥스를 통과하면 그 칸의 비중이 사실상 0이 된다. 결과적으로 왼쪽 아래 삼각형만 살아남는다.
시험 문제를 풀 때 답안지를 덮어두는 것과 같다. 뒷장을 볼 수 있으면 연습이 되지 않는다.
4. Transformer — 세 가지를 다 붙이면
세 가지 해결책을 전부 붙이고 나면 우리가 아는 Transformer 블록이 된다.
블록 하나를 아래에서부터 읽으면 이렇다.
- 단어 임베딩 + 위치 정보 — 1편의 임베딩에 자리 번호를 더한다
- Multi-Head Attention — 단어끼리 서로 본다
- Add & Norm — 안전장치 (뒤에서 설명)
- Feed-Forward — 비선형 변환
- Add & Norm — 다시 안전장치
그리고 이 블록을 여러 개 쌓아 올린다. 층이 깊을수록 더 복잡한 관계를 잡아낸다.
Multi-Head — 여러 관점으로 동시에
같은 단어라도 주목할 이유가 여러 가지다.
- 문법적으로 이어지는 단어
- 이 대명사가 가리키는 대상
- 시제나 말투를 결정하는 단어
Attention을 하나만 쓰면 한 가지 관점밖에 못 잡는다. 그래서 여러 개를 동시에 돌리고 결과를 합친다. 이게 Multi-Head Attention이다.
한 장면을 여러 사람이 각자 다른 관심사로 보고 있다고 생각하면 된다. 누구는 대사를, 누구는 표정을, 누구는 배경을 본다. 각자 본 것을 모으면 훨씬 풍부한 이해가 된다.
머리 개수를 늘릴수록 좋아질 것 같지만 그렇지도 않다. 일정 수준을 넘으면 비슷한 걸 보는 중복 머리가 생긴다. 지난 시리즈에서 Adam을 정리하며 나온 결론과 같다. 기본값은 출발점이지 정답이 아니다.
점수 크기 조절 — Scaled Dot-Product
작지만 중요한 장치가 하나 더 있다.
Query와 Key를 곱해 점수를 낼 때, 벡터가 길어질수록 점수도 자연히 커진다. 점수가 너무 크면 소프트맥스 결과가 한 칸에 0.99가 몰리는 극단적인 모양이 된다.
이러면 나머지 단어는 사실상 무시되고, 학습 신호도 거의 흐르지 않는다.
그래서 점수를 일정한 값으로 나눠서 크기를 눌러준다. 별것 아닌 나눗셈 같지만, 이거 하나로 학습 안정성이 크게 달라진다.
Add & Norm — 깊게 쌓기 위한 안전장치
층을 깊게 쌓으면 학습이 불안정해진다. 이걸 잡아주는 두 장치가 붙는다. 이름은 붙어 다니지만 하는 일은 전혀 다르다.
Add — 입력을 그대로 더한다
Residual Connection(잔차 연결)이라고 부른다. 하는 일은 정말 이게 전부다.
출력 = 입력 + 층이 계산한 결과
층의 결과만 다음으로 넘기는 게 아니라, 들어온 입력을 옆으로 빼서 그대로 더해준다.
이게 왜 필요할까? 두 가지 효과가 있다.
① 층이 할 일이 쉬워진다
입력이 더해지니, 층은 전체를 새로 만들 필요가 없다. “이 입력에서 무엇을 더하거나 빼야 하는가”만 배우면 된다.
문서를 고치는 상황과 같다. 처음부터 다시 쓰라고 하면 원래 있던 좋은 내용까지 날아갈 위험이 있다. 고칠 부분만 표시하라고 하면 훨씬 쉽고 안전하다. Residual은 후자로 만들어준다.
만약 이 층이 딱히 할 일이 없다면? 아무것도 안 더하면 입력이 그대로 통과한다. 층을 쌓아서 손해 볼 일이 없어지는 것이다.
② 역전파 신호가 살아서 돌아간다
이쪽이 더 중요하다. 지난 시리즈에서 역전파는 거슬러 가며 값을 곱해 나간다고 정리했고, 그래서 깊어지면 신호가 사라지는 문제가 있었다.
Residual이 있으면 더하기로 이어진 지름길이 생긴다. 층을 통과하는 길에서 신호가 약해져도, 더해진 쪽 길로는 신호가 거의 그대로 돌아간다.
LSTM의 cell state와 발상이 똑같다. 그때도 “곱셈과 덧셈만 지나가는 길”을 따로 뚫어서 신호가 멀리까지 살아남게 했다. 여기서는 그 길을 시간 방향이 아니라 층 방향으로 뚫은 것이다.
덕분에 Transformer는 층을 수십 개씩 쌓아도 학습이 된다. 깊게 쌓을 수 있게 만든 장치라고 보면 된다.
Norm — 값의 크기를 정돈한다
Layer Normalization(층 정규화)이다. 이건 완전히 다른 문제를 푼다.
층을 거칠수록 벡터 안의 값들이 어떤 건 아주 커지고 어떤 건 아주 작아진다. 값의 크기가 층마다 들쭉날쭉해지면 학습이 불안정해진다. 학습률 하나로 감당이 안 되기 때문이다.
그래서 각 단어 벡터의 값들을 평균 0, 크기 1 근처로 다시 맞춰준다.
지난 시리즈에서 클러스터링을 다룰 때 스케일링을 했던 것과 같은 이유다. 나이(20~60)와 연봉(3000~10000)을 같이 쓰면 연봉이 나이를 압도해버리니 크기를 맞춰줬다.
여기서는 그 작업을 층을 지날 때마다 반복하는 셈이다. 값이 한쪽으로 쏠려 커지기 전에 계속 정돈해주는 것이다.
정리하면 이렇다.
| 장치 | 푸는 문제 | 하는 일 |
|---|---|---|
| Add | 깊어지면 학습 신호가 사라진다 | 입력을 더해 지름길을 만든다 |
| Norm | 층마다 값 크기가 들쭉날쭉해진다 | 크기를 일정하게 맞춘다 |
둘 다 성능을 올리는 장치가 아니라, 깊게 쌓아도 학습이 되게 만드는 장치다. 화려하진 않은데 이게 없으면 Transformer는 몇 층 못 올라간다.
5. 인코더와 디코더
Transformer도 Seq2Seq처럼 인코더와 디코더로 나뉜다. 차이는 딱 하나다.
| 마스킹 | 볼 수 있는 범위 | |
|---|---|---|
| 인코더 | 없음 | 문장 전체 (앞뒤 모두) |
| 디코더 | 있음 | 자기보다 앞쪽만 |
인코더는 이해가 목적이라 앞뒤 문맥을 다 봐도 된다. 디코더는 생성이 목적이라 뒤를 보면 안 된다.
번역처럼 입력과 출력이 모두 있는 작업에서는 둘을 함께 쓴다. 이때 디코더는 자기 문장만 보는 게 아니라 인코더가 만든 표현도 참조하는데, 이걸 Cross-Attention이라고 한다. 지난 글에서 본 원래의 Attention이 여기 그대로 들어간다.
정리하면 Transformer 안에는 Attention이 두 종류로 들어간다.
- Self-Attention — 자기 문장 안을 본다
- Cross-Attention — 상대 문장을 본다
이 인코더·디코더 구분이 다음 글에서 아주 중요해진다. BERT는 인코더만, GPT는 디코더만 떼어 쓰는 모델이기 때문이다.
6. 결과
Transformer는 나오자마자 번역에서 최고 성능을 찍었다. 그런데 더 중요한 건 다른 지점이었다.
성능만 오른 게 아니라, 학습 비용이 오히려 줄었다.
순서 제약이 없으니 전부 한꺼번에 계산할 수 있고, GPU를 제대로 쓸 수 있게 됐다. 같은 시간에 훨씬 많은 데이터를 학습할 수 있게 된 것이다.
이게 결정적이었다. 대규모 데이터로 오래 학습하는 것이 현실적으로 가능해졌고, 그게 다음 글에서 다룰 사전학습 시대를 열었다.
지금 이름을 아는 모델은 거의 다 Transformer 계열이다. 구조 하나가 분야 전체를 갈아치운 셈이다.
정리
- RNN을 뺀 이유 — 멀리 갈수록 신호가 약해지고, 병렬 처리가 안 된다
- Self-Attention = 한 문장 안에서 단어끼리 서로를 보는 것. 거리 무관, 전부 동시 계산
- 그대로는 안 되는 세 가지와 해결
- 순서를 모른다 → Positional Encoding (자리 번호를 더해준다)
- 표현력이 약하다 → Feed-Forward 층 (단어별로 넓혔다 줄이며 비선형성 추가)
- 미래를 본다 → Masked Self-Attention (뒤쪽을 막는다)
- Multi-Head — 여러 관점으로 동시에 보고 합친다. 많다고 계속 좋아지진 않는다
- 점수 나누기(Scaled) — 소프트맥스가 한쪽으로 쏠리는 걸 막는다
- Add — 입력을 그대로 더해 역전파 지름길을 만든다. LSTM의 cell state와 같은 발상
- Norm — 층마다 값 크기를 일정하게 맞춰 학습을 안정시킨다
- FFN — Attention이 섞은 정보를 단어마다 따로 소화한다. 넓혔다 줄이며 비선형성 확보
- 인코더는 앞뒤 다 보고, 디코더는 앞만 본다. 이 차이가 다음 글의 모델 분화로 이어진다
- 성능도 올랐지만 학습 비용이 줄어든 것이 더 큰 변화였다
다음 글에서
다음 글에서는 사전학습을 다룬다. Transformer를 어디에 어떻게 학습시켜서 BERT·T5·GPT가 갈라졌는지, 그리고 파인튜닝 없이도 새 일을 해내는 In-Context Learning이 어떻게 등장했는지 정리한다.
한줄 평
- 나는 디코더가 아니다. 리코더다. 어렵다. 화이팅해라.