Transformer Block
블록 하나가 하는 일은 두 가지다. 다른 토큰들을 참조해서 정보를 모으고(attention), 그 결과를 비선형 변환한다(FFN).
블록 구조
Transformer block(= layer) 하나는 다음 순서로 구성된다.
flowchart TD
IN["입력 벡터"] --> LN1["Layer Norm"]
LN1 --> ATT["Multi-Head Self-Attention"]
ATT --> ADD1["+ 입력 (Residual)"]
ADD1 --> LN2["Layer Norm"]
LN2 --> FFN["FFN"]
FFN --> ADD2["+ ADD1 출력 (Residual)"]
ADD2 --> OUT["출력 벡터"]
이 구조가 N번 반복된다. Llama-3 8B는 32번, 70B는 80번.
각 블록은 독립된 weight를 갖고 있으므로 블록이 많을수록 모델이 크고 느리다.
반복 횟수는 모델 구조의 하이퍼파라미터이고, weight 는 각 블록 안에 들어 있는 학습된 파라미터 값이다.
예를 들어 Llama-3 8B 는 32개 Block 을 가지는 것이고, 각 Block 은 자기만의 Attention weight, FFN Weight 를 가진다.
즉 서로 다른 weight 묶음이 들어 있는 block 이 쌓여있다고 상상하는게 더 맞겠다.
Self-Attention
Attention은 “이 토큰이 다른 토큰들 중 어디를 얼마나 봐야 하는가”를 계산하는 메커니즘이다.
각 토큰 위치에서 세 개의 벡터가 만들어진다.
- Query (Q): “나는 무엇을 찾고 있는가”
- Key (K): “나는 어떤 정보를 갖고 있는가”
- Value (V): “내가 제공할 실제 정보”
Q와 K의 내적(dot product)으로 유사도 점수를 매기고, softmax로 정규화해서 가중치를 만들고, 그 가중치로 V를 가중합한다.
"서울은 한국의 수도이다"
"수도"의 Q가 "한국"의 K와 높은 점수 → "한국"의 V를 많이 참조
"수도"의 Q가 "이다"의 K와 낮은 점수 → "이다"의 V는 거의 무시
이 과정이 시퀀스의 모든 토큰 위치에서 동시에 일어난다. 결과적으로 각 토큰은 문맥에서 중요한 토큰들의 정보를 흡수한 새로운 벡터가 된다.
Q, K, V는 같은 입력 벡터에 서로 다른 weight 매트릭스를 곱해서 만든다.
self-attention이라 부르는 이유는 Q, K, V가 모두 같은 시퀀스의 토큰들로부터 오기 때문이다.
Multi-Head Attention
Attention을 한 번만 하면 “한 가지 관점”으로만 토큰 간 관계를 본다.
실제로는 문법적 관계, 의미적 관계, 위치적 관계 등 여러 관점이 동시에 필요하다.
그래서 Q, K, V를 여러 세트(head)로 나눠 각각 독립적으로 attention을 수행한 다음 결과를 합친다.
여기서 중요한 변형이 GQA(Grouped Query Attention)다.
원래 MHA(Multi-Head Attention)에서는 Q, K, V head 수가 모두 같다.
GQA에서는 K, V head를 줄여서 여러 Q head가 하나의 K, V 쌍을 공유한다.
Llama-3 8B는 Q head 32개, KV head 8개로 4:1 비율이다.
이 설계가 inference에서 결정적인 이유는 KV cache 크기를 줄이기 때문이다.
Self-Attention 과 Multi-Head Attention
Self-Attention 은 “무엇을 보느냐”에 대한 말이다.
Q, K, V 가 모두 같은 시퀀스에서 나온다.
예를 들어 “나는 밥을 먹었다” 에서 “먹었다” 토큰이 “나는”, “밥을” 같은 앞 토큰들을 참고하는 것이다.
Multi-Head Attention 은 “attention 을 몇 벌 병렬로 하느냐”에 대한 말이다.
flowchart LR
X["같은 입력 토큰 벡터"] --> H1["Head 1<br/>문법 관계"]
X --> H2["Head 2<br/>의미 관계"]
X --> H3["Head 3<br/>위치 관계"]
X --> HN["..."]
H1 --> CAT["결과 결합"]
H2 --> CAT
H3 --> CAT
HN --> CAT
CAT --> OUT["새 토큰 벡터"]
그래서 실제 Transformer 에서는 보통 Multi-Head Self-Attention 을 쓴다.
Self-Attention 을 여러 Head 로 나눠서 병렬로 수행한다.
FFN
Attention이 “토큰 간 정보 교환”이라면, FFN은 “토큰 하나의 표현을 변형”하는 단계다.
Attention을 지나온 토큰 벡터에는 주변 토큰에서 가져온 정보가 섞여 있다.
FFN은 그 벡터를 한 번 더 가공해서 “이 토큰을 다음 단계에서 어떻게 표현할지”를 정리한다.
다른 토큰을 다시 보지는 않는다. 토큰 하나하나에 같은 방식의 작은 처리기를 따로 적용한다고 보면 된다.
예를 들어 다음 문장을 보자.
나는 사과를 먹었다
Attention을 지난 "먹었다" 벡터에는 "나는", "사과" 같은 주변 토큰에서 가져온 정보가 섞여 있다.
FFN은 여기서 다른 토큰을 다시 조회하지 않는다. 대신 "먹었다" 벡터 자체를 한 번 더 가공해서, 다음 블록이 쓰기 좋은 표현으로 정리한다.
사람이 이해하기 쉽게 말하면 “이 토큰은 동작을 나타내고, 앞의 사과와 연결된 서술어다” 같은 방향의 신호를 더 또렷하게 만드는 단계에 가깝다.
흐름은 넓혔다가, 걸러내고, 다시 줄이는 구조다.
flowchart LR
H["토큰 벡터<br/>4096칸"] --> UP["넓은 작업 공간으로 펼침<br/>14336칸"]
UP --> ACT["중요한 신호는 살리고<br/>약한 신호는 줄임"]
ACT --> DOWN["다시 원래 크기로 압축<br/>4096칸"]
여기서 “선형 변환”은 벡터 안의 숫자들을 섞어서 새로운 숫자 묶음을 만드는 계산이다.
예를 들어 4096칸짜리 벡터를 14336칸짜리 벡터로 바꾸면, 모델은 더 넓은 임시 공간에서 여러 특징 조합을 만들어볼 수 있다.
그다음 “비선형 활성화 함수”가 들어간다. 이름은 어렵지만 역할은 게이트에 가깝다.
모든 값을 그대로 통과시키지 않고, 어떤 신호는 강하게 남기고 어떤 신호는 약하게 만든다. SiLU, GeLU는 이런 게이트 역할을 하는 함수 이름이다.
마지막으로 다시 선형 변환을 해서 원래 hidden size로 압축한다. 그래서 FFN은 “펼치기 → 걸러내기 → 압축하기” 단계라고 이해하면 된다.
Llama-3 8B 기준으로 hidden size 4096이 intermediate size 14336까지 확장됐다가 다시 4096으로 돌아온다.
FFN의 weight가 전체 모델 파라미터의 약 2/3를 차지한다.
Attention이 transformer의 대표 개념이지만, 파라미터의 대부분은 FFN에 있다.
Residual Connection과 Layer Norm
블록을 32~80개 쌓으면 깊이가 수십 층이다. 이렇게 깊은 네트워크에서는 두 가지 문제가 생긴다.
- 기울기 소실: 역전파 시 gradient가 층을 거칠수록 작아져 학습이 안 된다
- 표현 붕괴: 값이 층을 지나면서 발산하거나 사라진다
Residual connection(잔차 연결)은 블록의 입력을 출력에 더해주는 것이다. 블록이 학습하는 것은 “원래 값과의 차이”뿐이므로, 아무것도 못 배우면 최소한 입력을 그대로 통과시킨다. gradient도 덧셈 연산을 통해 직통 경로로 흐른다.
Layer Norm은 각 토큰의 벡터 값을 정규화해서 스케일을 안정시킨다. 블록의 앞(Pre-LN)에 두느냐 뒤(Post-LN)에 두느냐 차이가 있는데, 최근 모델은 대부분 Pre-LN을 쓴다. 위의 mermaid 다이어그램이 Pre-LN 구조다.