Level 7: Transformer & LLM 원리
🤖

Level 7

Encoder & Decoder 구조

Transformer 전체 구조, Layer Norm, Residual Connection, FFN

60분
Encoder & Decoder 구조 강의 영상
강의 영상 보기 (새 탭에서 재생)YouTube

📓Google Colab에서 실습하기

이 레슨은 PyTorch/GPU가 필요합니다. 노트북을 다운로드 후 Google Colab에서 열어주세요.

학습 내용

Encoder & Decoder 구조: Transformer의 전체 그림

학습 목표

이 레슨을 완료하면:

  • Transformer의 Encoder-Decoder 전체 구조를 이해한다
  • Encoder와 Decoder 각각의 역할과 내부 구성을 설명할 수 있다
  • Residual Connection(잔차 연결)이 왜 필요한지 안다
  • Layer Normalization의 원리와 효과를 이해한다
  • Feed-Forward Network(FFN)의 역할을 설명할 수 있다
  • Cross-Attention이 Encoder와 Decoder를 어떻게 연결하는지 안다

핵심 메시지

"Transformer는 '이해하는 팀(Encoder)'과 '표현하는 팀(Decoder)'이 협력하는 시스템이다." Encoder가 입력을 깊이 이해하고, Decoder가 그 이해를 바탕으로 출력을 한 단어씩 만들어냅니다.


큰 그림: 왜 Encoder-Decoder인가?

비유: 동시통역사 국제회의에서 동시통역사가 일하는 과정을 떠올려보세요.

  1. 듣기 단계: 연사의 말을 끝까지 듣고 전체 의미를 파악합니다 (= Encoder)
  2. 말하기 단계: 파악한 의미를 다른 언어로 한 단어씩 표현합니다 (= Decoder)

통역사는 "듣기"와 "말하기"를 분리해서 처리합니다. Transformer도 마찬가지입니다.

원래 Transformer는 기계 번역을 위해 설계되었습니다. 영어 문장을 한국어로 번역하는 과정을 생각하면:

[영어 입력] "I love artificial intelligence"
     |
     v
  Encoder: 입력 문장 전체를 양방향으로 이해
     |          (모든 단어가 서로를 참조)
     v
  [문맥 표현] - 입력의 의미가 압축된 벡터들
     |
     v
  Decoder: 문맥 표현을 참고하면서 한국어를 한 단어씩 생성
     |
     v
[한국어 출력] "나는 인공지능을 좋아한다"

Transformer 전체 구조

Transformer는 크게 왼쪽의 Encoder오른쪽의 Decoder로 구성됩니다. 각각은 동일한 레이어를 여러 번(원래 논문에서는 6번) 쌓아올린 구조입니다.

구성 요소EncoderDecoder
Self-Attention양방향 (모든 토큰 참조)단방향 (Masked, 미래 토큰 차단)
Cross-Attention없음있음 (Encoder 출력 참조)
Feed-Forward있음있음
Add & Norm매 서브레이어마다매 서브레이어마다
레이어 반복N번 (보통 6)N번 (보통 6)

Encoder 상세: "입력을 깊이 이해하는 팀"

비유: 독서 토론 모임 책을 읽고 토론하는 모임을 상상해보세요. 모든 참가자(= 토큰)가 서로의 의견을 들으며 책의 내용을 더 깊이 이해합니다. 이것이 Encoder의 Self-Attention입니다.

Encoder 레이어 하나의 구성

Encoder 레이어 하나는 다음 두 단계로 이루어집니다:

단계 1: Multi-Head Self-Attention + Add & Norm

  • 입력의 모든 토큰이 서로를 양방향으로 참조합니다
  • "은행"이라는 단어가 "돈"과 "강" 중 어느 쪽과 관련있는지 주변을 봅니다

단계 2: Feed-Forward Network + Add & Norm

  • Attention의 결과를 비선형 변환하여 표현력을 높입니다
  • 각 토큰 위치마다 독립적으로 적용됩니다
Encoder 레이어 1개의 흐름:

입력 (각 토큰의 벡터)
  |
  v
[Multi-Head Self-Attention] -- 모든 토큰이 서로 참조
  |
  v
[Add & Layer Norm] ----------- 잔차 연결 + 정규화
  |
  v
[Feed-Forward Network] ------- 비선형 변환 (확장 후 축소)
  |
  v
[Add & Layer Norm] ----------- 잔차 연결 + 정규화
  |
  v
출력 (다음 레이어의 입력이 됨)

이것을 N번(보통 6번) 반복합니다.

Encoder의 핵심 특성: 양방향 참조

Encoder의 Self-Attention에서는 마스킹이 없습니다. 즉, 모든 토큰이 다른 모든 토큰을 자유롭게 참조할 수 있습니다.

문장: "나는 은행에서 돈을 찾았다"

Encoder Attention 참조 가능 여부:
         나는  은행에서  돈을  찾았다
나는      O      O       O      O
은행에서   O      O       O      O
돈을      O      O       O      O
찾았다    O      O       O      O

모든 칸이 O = 양방향 참조!
"은행에서"는 뒤에 있는 "돈을"도 볼 수 있어서
금융 은행이라는 것을 정확히 파악합니다.

Decoder 상세: "한 단어씩 출력을 만들어내는 팀"

비유: 소설 작가 소설을 쓸 때 작가는 앞서 쓴 내용만 보면서 다음 문장을 씁니다. 아직 쓰지 않은 뒷부분을 미리 볼 수는 없습니다. 동시에 원작(= Encoder 출력)을 참고하면서 번역합니다.

Decoder 레이어 하나의 구성

Decoder는 Encoder보다 한 단계가 더 있어서, 총 세 단계입니다:

단계 1: Masked Multi-Head Self-Attention + Add & Norm

  • 지금까지 생성한 토큰들끼리 참조합니다
  • 미래 토큰은 볼 수 없도록 마스킹합니다

단계 2: Cross-Attention + Add & Norm

  • Decoder의 현재 상태로 Encoder의 출력을 참조합니다
  • 입력 문장의 어느 부분에 집중할지 결정합니다

단계 3: Feed-Forward Network + Add & Norm

  • 비선형 변환으로 표현력을 높입니다
Decoder 레이어 1개의 흐름: 출력 토큰들 (지금까지 생성된 것) | v [Masked Self-Attention] ------ 미래 토큰 차단, 과거만 참조 | v [Add & Layer Norm] | v [Cross-Attention] ------------ Query: Decoder 상태 | ^ Key, Value: Encoder 출력 | | | Encoder 출력이 여기로 들어옴 v [Add & Layer Norm] | v [Feed-Forward Network] | v [Add & Layer Norm] | v 출력 (다음 레이어의 입력) 이것을 N번(보통 6번) 반복합니다.

Masked Self-Attention: 미래를 볼 수 없게 막기

Decoder에서 "나는 밥을 먹는다"를 생성할 때, 각 단어는 자기 자신과 이전 단어만 참조할 수 있습니다.

"나는 밥을 먹는다" 생성 과정:

step 1: "나는" 생성  -- 참조할 이전 토큰 없음
step 2: "밥을" 생성  -- "나는"만 참조 가능
step 3: "먹는다" 생성 -- "나는", "밥을" 참조 가능

Masked Attention 행렬:
         나는  밥을  먹는다
나는      1     0     0
밥을      1     1     0
먹는다    1     1     1

1 = 참조 가능, 0 = 마스킹(참조 불가)

마스킹된 위치에는 -무한대 값을 넣어서
softmax 후 attention 가중치가 0이 됩니다.

왜 이렇게 할까요? 학습할 때와 실제 생성할 때의 조건을 동일하게 맞추기 위해서입니다. 실제 생성 시에는 미래 단어가 아직 없으니 볼 수 없고, 학습할 때도 마찬가지로 미래를 가려야 공정한 훈련이 됩니다.

Cross-Attention: Encoder와 Decoder를 연결하는 다리

비유: 오픈북 시험 Decoder가 답을 쓸 때(= 출력 생성), Encoder가 정리해둔 노트(= Encoder 출력)를 펼쳐놓고 필요한 부분을 찾아보는 것과 같습니다.

Cross-Attention에서는 Query, Key, Value의 출처가 다릅니다:

요소출처역할
Query (Q)Decoder의 현재 상태"내가 지금 알고 싶은 것"
Key (K)Encoder의 출력"입력 문장의 각 부분 인덱스"
Value (V)Encoder의 출력"입력 문장의 각 부분 실제 내용"
번역 예시: "I love AI" -> "나는 AI를 좋아한다"

Decoder가 "좋아한다"를 생성할 때:
- Query: Decoder의 현재 상태 (지금까지 "나는 AI를" 생성함)
- Key/Value: Encoder가 분석한 "I", "love", "AI" 벡터들

Cross-Attention 결과:
  "I"    -> 가중치 0.1 (약간 관련)
  "love" -> 가중치 0.8 (높은 관련! "좋아한다"의 원문)
  "AI"   -> 가중치 0.1 (약간 관련)

=> "love"에 집중해서 "좋아한다"를 정확히 생성

Residual Connection (잔차 연결): 정보의 고속도로

비유: 고속도로 톨게이트 일반 도로만 있으면 신호등(= 레이어)을 하나씩 거칠 때마다 느려집니다. 고속도로(= Residual Connection)가 있으면 원래 정보가 빠르게 전달됩니다. "원본 정보가 사라지지 않도록 보장하는 지름길"입니다.

왜 필요한가?

깊은 신경망은 두 가지 심각한 문제가 있습니다:

문제 1: 기울기 소실 (Gradient Vanishing)

  • 역전파할 때 기울기가 레이어를 거칠수록 점점 작아집니다
  • 앞쪽 레이어는 거의 학습이 되지 않습니다

문제 2: 정보 손실

  • 여러 레이어를 통과하면서 원래 입력의 정보가 변형되어 사라질 수 있습니다

해결: x + f(x) 구조

Residual Connection의 핵심 아이디어:

일반 레이어: output = f(x) -- x가 변환되어 원본 사라짐 잔차 연결: output = x + f(x) -- 원본 x가 항상 보존됨

직관적으로: f(x)는 "원본에서 얼마나 바꿀지"만 학습하면 됩니다. 전체를 처음부터 만드는 것보다 훨씬 쉽습니다!

역전파 관점: d(output)dx=1+f(ˊx)\frac{d(\text{output})}{dx} = 1 + f\'(x) 항상 1이 더해지므로 기울기가 완전히 사라지지 않습니다!

실행해보기: Residual Connection 효과

python
import numpy as np np.random.seed(42) num_layers = 20 gradient = 1.0 # 1) Residual Connection 없이 print("=== Residual Connection 없이 ===") grad_no_res = gradient for i in range(num_layers): layer_grad = np.random.uniform(0.3, 0.9) grad_no_res *= layer_grad if (i + 1) % 5 == 0: print(f" {i+1}번째 레이어 후 기울기: {grad_no_res:.6f}") # 2) Residual Connection 있으면 print() print("=== Residual Connection 있으면 ===") grad_with_res = gradient for i in range(num_layers): layer_grad = np.random.uniform(0.3, 0.9) grad_with_res = 1.0 + layer_grad * grad_with_res if (i + 1) % 5 == 0: print(f" {i+1}번째 레이어 후 기울기: {grad_with_res:.4f}") print() print(f"잔차 연결 없이 20 레이어 후: {grad_no_res:.8f} (거의 0!)") print(f"잔차 연결 있으면 20 레이어 후: {grad_with_res:.4f} (건강한 크기)")

Layer Normalization: 값의 균형을 맞추는 조율사

비유: 오케스트라 지휘자 어떤 악기는 너무 크게, 어떤 악기는 너무 작게 연주합니다. 지휘자가 음량 균형을 맞춰주듯, Layer Norm은 벡터 값들의 균형을 맞춥니다.

왜 정규화가 필요한가?

레이어를 여러 번 거치면 값이 점점 커지거나 작아질 수 있습니다.

레이어 1 출력: [0.5, -0.3, 0.8]        -- 적절한 범위
레이어 3 출력: [5.2, -3.1, 8.4]        -- 값이 커지기 시작
레이어 6 출력: [152.3, -89.7, 243.1]   -- 폭발!

이러면 학습이 매우 불안정해집니다.

Layer Norm의 작동 원리

LayerNorm(x)=γxμσ2+ϵ+β\text{LayerNorm}(x) = \gamma \cdot \frac{x - \mu}{\sqrt{\sigma^2 + \epsilon}} + \beta

  • μ\mu (mean): 벡터 x의 평균
  • σ2\sigma^2 (variance): 벡터 x의 분산
  • γ,β\gamma, \beta: 학습 가능한 파라미터 (스케일과 이동)
  • ϵ\epsilon: 0으로 나누는 것을 방지하는 아주 작은 값

실행해보기: Layer Normalization 직접 구현

python
import numpy as np def layer_norm(x, gamma=None, beta=None, eps=1e-5): """Layer Normalization 직접 구현""" mean = np.mean(x) variance = np.var(x) x_norm = (x - mean) / np.sqrt(variance + eps) if gamma is not None and beta is not None: x_norm = gamma * x_norm + beta return x_norm # 불안정한 값 x_unstable = np.array([152.3, -89.7, 243.1, -15.6]) print("정규화 전:", x_unstable) print(f" 평균: {np.mean(x_unstable):.1f}, 표준편차: {np.std(x_unstable):.1f}") x_normalized = layer_norm(x_unstable) print() print("정규화 후:", np.round(x_normalized, 4)) print(f" 평균: {np.mean(x_normalized):.6f}, 표준편차: {np.std(x_normalized):.4f}") gamma = np.array([1.5, 1.5, 1.5, 1.5]) beta = np.array([0.5, 0.5, 0.5, 0.5]) x_scaled = layer_norm(x_unstable, gamma, beta) print() print("gamma/beta 적용 후:", np.round(x_scaled, 4)) print("-> 값이 안정적인 범위 안에 있으면서도 학습 가능!")

Batch Norm vs Layer Norm

비교 항목Batch NormalizationLayer Normalization
정규화 축배치(batch) 차원특성(feature) 차원
배치 의존성배치 크기에 영향 받음배치 크기와 무관
시퀀스 처리길이가 다르면 문제길이가 달라도 OK
주 사용처CNN (이미지)Transformer (텍스트)

Transformer에서 Layer Norm을 쓰는 이유: 문장마다 길이가 다르고, 배치 크기에 독립적이어야 하기 때문입니다.


Add & Norm: 잔차 연결과 정규화의 결합

Transformer의 모든 서브레이어(Attention, FFN) 뒤에는 반드시 Add & Norm이 붙습니다.

Add & Norm의 공식:

output=LayerNorm(x+SubLayer(x))\text{output} = \text{LayerNorm}(x + \text{SubLayer}(x))

  • x: 서브레이어에 들어가는 입력
  • SubLayer(x): Attention 또는 FFN의 출력
  • x + SubLayer: 잔차 연결 (원본 보존)
  • LayerNorm: 값 정규화 (안정화)

Encoder 레이어에는 Add & Norm이 2번:

  1. Self-Attention 뒤
  2. FFN 뒤

Decoder 레이어에는 Add & Norm이 3번:

  1. Masked Self-Attention 뒤
  2. Cross-Attention 뒤
  3. FFN 뒤

참고: Pre-Norm vs Post-Norm 원래 논문은 Post-Norm을 사용했지만, 최근 모델들은 Pre-Norm (서브레이어 앞에 Norm)을 더 많이 씁니다. Pre-Norm이 학습이 더 안정적이라는 연구 결과가 있기 때문입니다.


Feed-Forward Network (FFN): 각 토큰을 개별 변환

비유: 개인 과외 Self-Attention이 "그룹 토론"이라면, FFN은 "개인 과외"입니다. 토론에서 다른 사람들의 의견을 들은 후(Attention), 각자 혼자서 깊이 생각하는 시간(FFN)을 갖습니다.

FFN의 구조

FFN(x) = Linear2(Activation(Linear1(x)))

차원 변화:
  입력:  d_model (512)
    |
    v -- Linear1: 512 -> 2048 (4배 확장)
  중간:  d_ff (2048) + ReLU 활성화
    |
    v -- Linear2: 2048 -> 512 (원래 크기로 축소)
  출력:  d_model (512)

왜 확장했다 축소할까?

  • 더 높은 차원에서 복잡한 패턴을 포착한 후
  • 다시 원래 크기로 압축하여 정보를 정제합니다

FFN의 중요한 특성

핵심: FFN은 각 토큰 위치에 "독립적으로" 적용됩니다!

문장: ["나는", "밥을", "먹는다"]

  • Self-Attention: 세 단어가 서로를 참조 (상호작용)
  • FFN: 각 단어가 독립적으로 변환 (개별 처리)

모든 위치에 동일한 FFN을 적용하지만, 입력 벡터가 다르므로 출력도 달라집니다.

실행해보기: FFN의 확장-축소 과정

python
import numpy as np np.random.seed(42) d_model = 8 d_ff = 32 # 4배 확장 W1 = np.random.randn(d_model, d_ff) * 0.1 b1 = np.zeros(d_ff) W2 = np.random.randn(d_ff, d_model) * 0.1 b2 = np.zeros(d_model) def relu(x): return np.maximum(0, x) def ffn(x): hidden = relu(x @ W1 + b1) output = hidden @ W2 + b2 return hidden, output x = np.array([0.5, -0.3, 0.8, 0.1, -0.6, 0.4, -0.2, 0.7]) hidden, output = ffn(x) print(f"입력 크기: {x.shape[0]} (d_model={d_model})") print(f"중간 크기: {hidden.shape[0]} (d_ff={d_ff}, {d_ff//d_model}배 확장)") print(f"출력 크기: {output.shape[0]} (d_model={d_model})") print() print(f"입력 벡터: {np.round(x, 3)}") print(f"출력 벡터: {np.round(output, 3)}") print() print(f"ReLU 후 0인 뉴런: {np.sum(hidden == 0)}/{d_ff}") print("-> ReLU가 일부 뉴런을 꺼서 희소한 표현을 만듭니다")

전체 Transformer 한눈에 보기

PyTorch 스타일 의사코드

python
class Transformer: def __init__(self): self.src_embedding = Embedding(src_vocab, d_model=512) self.tgt_embedding = Embedding(tgt_vocab, d_model=512) self.pos_encoding = PositionalEncoding(d_model=512) self.encoder_layers = [EncoderLayer() for _ in range(6)] self.decoder_layers = [DecoderLayer() for _ in range(6)] self.output_projection = Linear(d_model, tgt_vocab) def forward(self, source, target): # === Encoder === enc = self.pos_encoding(self.src_embedding(source)) for layer in self.encoder_layers: enc = layer(enc) # 내부: Self-Attention -> Add&Norm -> FFN -> Add&Norm # === Decoder === dec = self.pos_encoding(self.tgt_embedding(target)) for layer in self.decoder_layers: dec = layer(dec, encoder_output=enc) # 내부: Masked Self-Attn -> Add&Norm # -> Cross-Attn(enc 참조) -> Add&Norm # -> FFN -> Add&Norm # === 출력 === logits = self.output_projection(dec) return softmax(logits)

원래 Transformer 하이퍼파라미터

파라미터설명
d_model512모델 차원 (벡터 크기)
d_ff2048FFN 중간 차원 (4배)
h (heads)8Attention 헤드 수
N (layers)6Encoder/Decoder 레이어 수
d_k64각 헤드의 차원 (512/8)

실행해보기: 미니 Transformer 흐름 시뮬레이션

python
import numpy as np np.random.seed(42) d_model = 4 seq_len = 3 vocab = ["나는", "밥을", "먹는다"] # 1단계: 임베딩 embeddings = np.random.randn(seq_len, d_model) * 0.5 print("1) 토큰 임베딩:") for i, word in enumerate(vocab): print(f" {word}: {np.round(embeddings[i], 3)}") # 2단계: 위치 인코딩 pos_enc = np.zeros((seq_len, d_model)) for pos in range(seq_len): for i in range(d_model): if i % 2 == 0: pos_enc[pos, i] = np.sin(pos / 10000**(i/d_model)) else: pos_enc[pos, i] = np.cos(pos / 10000**((i-1)/d_model)) input_vectors = embeddings + pos_enc print() print("2) 위치 인코딩 추가 후:") for i, word in enumerate(vocab): print(f" {word}: {np.round(input_vectors[i], 3)}") # 3단계: Self-Attention (간소화) scores = input_vectors @ input_vectors.T scores = scores / np.sqrt(d_model) attn_weights = np.exp(scores) / np.exp(scores).sum(axis=-1, keepdims=True) attn_output = attn_weights @ input_vectors print() print("3) Self-Attention 가중치:") print(" 나는 밥을 먹는다") for i, word in enumerate(vocab): weights_str = " ".join([f"{w:.3f}" for w in attn_weights[i]]) print(f" {word}: {weights_str}") # 4단계: Residual + LayerNorm residual = input_vectors + attn_output mean = residual.mean(axis=-1, keepdims=True) std = residual.std(axis=-1, keepdims=True) normalized = (residual - mean) / (std + 1e-5) print() print("4) Add & Norm 후:") for i, word in enumerate(vocab): print(f" {word}: {np.round(normalized[i], 3)}") print() print("전체 흐름: 임베딩 -> Self-Attention -> Add&Norm -> FFN -> Add&Norm") print("이것이 Encoder 레이어 1개의 과정입니다!")

핵심 요약

구성 요소역할비유
Encoder입력 전체를 양방향으로 이해독서 토론 (서로 참조)
Decoder한 단어씩 출력 생성소설 작가 (앞만 보고 쓰기)
Cross-AttentionDecoder가 Encoder를 참조오픈북 시험
Residual Connection원본 정보 보존, 기울기 유지고속도로 (지름길)
Layer Normalization값 안정화, 학습 촉진지휘자 (음량 조절)
Feed-Forward Network토큰별 비선형 변환개인 과외 (개별 심화)
Add & Norm잔차 연결 + 정규화 결합매 단계의 안전장치

학습 체크리스트

  • Encoder와 Decoder의 역할 차이를 설명할 수 있다
  • Masked Self-Attention이 왜 필요한지 이해한다
  • Cross-Attention에서 Q, K, V가 각각 어디서 오는지 안다
  • Residual Connection이 기울기 소실을 어떻게 방지하는지 설명할 수 있다
  • Layer Normalization이 왜 Batch Norm 대신 쓰이는지 안다
  • FFN이 "확장 후 축소"하는 이유를 이해한다
  • Transformer 전체 흐름을 처음부터 끝까지 따라갈 수 있다

레슨 정보

레벨
Level 7: Transformer & LLM 원리
예상 소요 시간
60분
참고 영상
YouTube 링크

💡실습 환경 안내

이 레벨은 PyTorch/GPU가 필요하여 Google Colab 사용을 권장합니다.

Colab은 무료 GPU를 제공하여 PyTorch, CNN, Transformer 등을 실행할 수 있습니다.