Level 7
Encoder & Decoder 구조
Transformer 전체 구조, Layer Norm, Residual Connection, FFN

📓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인가?
비유: 동시통역사 국제회의에서 동시통역사가 일하는 과정을 떠올려보세요.
- •듣기 단계: 연사의 말을 끝까지 듣고 전체 의미를 파악합니다 (= Encoder)
- •말하기 단계: 파악한 의미를 다른 언어로 한 단어씩 표현합니다 (= Decoder)
통역사는 "듣기"와 "말하기"를 분리해서 처리합니다. Transformer도 마찬가지입니다.
원래 Transformer는 기계 번역을 위해 설계되었습니다. 영어 문장을 한국어로 번역하는 과정을 생각하면:
[영어 입력] "I love artificial intelligence"
|
v
Encoder: 입력 문장 전체를 양방향으로 이해
| (모든 단어가 서로를 참조)
v
[문맥 표현] - 입력의 의미가 압축된 벡터들
|
v
Decoder: 문맥 표현을 참고하면서 한국어를 한 단어씩 생성
|
v
[한국어 출력] "나는 인공지능을 좋아한다"
Transformer 전체 구조
Transformer는 크게 왼쪽의 Encoder와 오른쪽의 Decoder로 구성됩니다. 각각은 동일한 레이어를 여러 번(원래 논문에서는 6번) 쌓아올린 구조입니다.
| 구성 요소 | Encoder | Decoder |
|---|---|---|
| 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
- •비선형 변환으로 표현력을 높입니다
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)는 "원본에서 얼마나 바꿀지"만 학습하면 됩니다. 전체를 처음부터 만드는 것보다 훨씬 쉽습니다!
역전파 관점: 항상 1이 더해지므로 기울기가 완전히 사라지지 않습니다!
실행해보기: Residual Connection 효과
pythonimport 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의 작동 원리
- • (mean): 벡터 x의 평균
- • (variance): 벡터 x의 분산
- •: 학습 가능한 파라미터 (스케일과 이동)
- •: 0으로 나누는 것을 방지하는 아주 작은 값
실행해보기: Layer Normalization 직접 구현
pythonimport 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 Normalization | Layer Normalization |
|---|---|---|
| 정규화 축 | 배치(batch) 차원 | 특성(feature) 차원 |
| 배치 의존성 | 배치 크기에 영향 받음 | 배치 크기와 무관 |
| 시퀀스 처리 | 길이가 다르면 문제 | 길이가 달라도 OK |
| 주 사용처 | CNN (이미지) | Transformer (텍스트) |
Transformer에서 Layer Norm을 쓰는 이유: 문장마다 길이가 다르고, 배치 크기에 독립적이어야 하기 때문입니다.
Add & Norm: 잔차 연결과 정규화의 결합
Transformer의 모든 서브레이어(Attention, FFN) 뒤에는 반드시 Add & Norm이 붙습니다.
Add & Norm의 공식:
- •x: 서브레이어에 들어가는 입력
- •SubLayer(x): Attention 또는 FFN의 출력
- •x + SubLayer: 잔차 연결 (원본 보존)
- •LayerNorm: 값 정규화 (안정화)
Encoder 레이어에는 Add & Norm이 2번:
- •Self-Attention 뒤
- •FFN 뒤
Decoder 레이어에는 Add & Norm이 3번:
- •Masked Self-Attention 뒤
- •Cross-Attention 뒤
- •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의 확장-축소 과정
pythonimport 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 스타일 의사코드
pythonclass 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_model | 512 | 모델 차원 (벡터 크기) |
| d_ff | 2048 | FFN 중간 차원 (4배) |
| h (heads) | 8 | Attention 헤드 수 |
| N (layers) | 6 | Encoder/Decoder 레이어 수 |
| d_k | 64 | 각 헤드의 차원 (512/8) |
실행해보기: 미니 Transformer 흐름 시뮬레이션
pythonimport 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-Attention | Decoder가 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 등을 실행할 수 있습니다.