트랜스포머·PEFT 2일 과정3장 · Attention과 Encoder15 / 18← 이전목차다음 →MetaContents

3장. Attention과 Encoder

실습 4 — Add & Norm 과 FFN 으로 Encoder 블록 완성하기

실습 3의 Multi-Head Attention 에 Add & Norm 과 FFN 을 붙여 Encoder 블록 하나를 함수로 만들고, 두 층을 쌓아 봅니다. 원본 노트북에는 코드가 없어 새로 쓴 코드입니다. 이 실습은 혼자서도 돌도록 필요한 함수를 처음부터 다시 정의합니다.

코드를 단계별로 따라가기 전에 실행 흐름을 한 장으로 봅니다.

실습 4 코드의 실행 흐름도. layer_norm 을 만들어 행 평균 0, 표준편차 1 을 확인하고, softmax 와 multi_head_attention 을 정의해 x 에 Attention 을 한 뒤 더하고 정규화해 x1 을 만든다. FFN 을 만들어 x1 에 더하고 정규화해 x2 를 만들고, 낱말마다 따로 계산되는지 True 를 확인한다. encoder_block 을 정의한 뒤 두 번 도는 반복으로 층마다 가중치를 만들고, 다시 층을 하나씩 도는 반복으로 out 을 블록에 통과시키며 (4, 8) 을 찍고, 끝으로 가중치 수 256, 552, 비율 0.68 을 찍는다
단계하는 일반복과 조건핵심 한 줄
1낱말마다 눈금을 맞추는 함수 만들고 시험없음(x - mean) / (std + eps)
2Multi-Head Attention 뒤 첫 번째 Add & Norm없음x1 = layer_norm(x + attn_out)
3FFN 뒤 두 번째 Add & Norm없음x2 = layer_norm(x1 + ffn(x1))
4블록 함수로 묶어 두 층 쌓기반복 두 번: 층 2개의 가중치 만들기(for _ in range(2)) → 층마다 통과시키기(for i, params in enumerate(layers))out = encoder_block(out, *params, h)
  • 반복 두 개: 4단계의 첫 마름모는 층마다 쓸 가중치 묶음을 layers 에 쌓고, 둘째 마름모는 그 묶음을 하나씩 꺼내 out 을 블록에 통과시킵니다. 둘째 반복에서 앞 층의 출력이 다음 층의 입력이 되는 것이 "쌓기"입니다.
  • 정의와 실행: def layer_norm, def multi_head_attention, def ffn, def encoder_block 은 요리법을 적어 두기만 하고, 계산은 그 아래 줄에서 부를 때 합니다.
  • 블록 안의 차례: 둘째 반복 안의 네모에 적은 "Attention → Add & Norm → FFN → Add & Norm" 이 2~3단계를 한 함수로 묶은 것입니다. 들어간 (4, 8) 이 그대로 나오므로 몇 층이든 쌓을 수 있습니다.

1단계. Layer Normalization 만들기

낱말마다 노트의 글씨 크기를 맞추는 함수를 만듭니다.

Python
import numpy as np

np.set_printoptions(precision=2, suppress=True)

def layer_norm(x, eps=1e-5):
    # 낱말(행)마다 평균과 표준편차를 구한다
    mean = x.mean(axis=-1, keepdims=True)
    std = x.std(axis=-1, keepdims=True)
    # eps: 표준편차가 0 일 때 0 으로 나누지 않게 더하는 아주 작은 수
    return (x - mean) / (std + eps)

x = np.array([[1.0, 2.0, 3.0, 6.0],
              [10.0, 20.0, 30.0, 60.0]])   # 둘째 줄은 첫째 줄의 10배
y = layer_norm(x)
print(y)
# 결과: 두 줄이 거의 같다 → 크기(눈금)가 달라도 같은 꼴로 맞춘다
print("행 평균:", y.mean(axis=1).round(2), " 행 표준편차:", y.std(axis=1).round(2))
# 결과: 행 평균: [0. 0.]  행 표준편차: [1. 1.]
출력
[[-1.07 -0.53  0.    1.6 ]
 [-1.07 -0.53  0.    1.6 ]]
행 평균: [0. 0.]  행 표준편차: [1. 1.]

2단계. Multi-Head Attention 과 첫 번째 Add & Norm

스터디 모임(Attention)의 결과를 원래 노트(x)에 더하고 눈금을 맞춥니다.

Python
def softmax(x):
    e = np.exp(x - x.max(axis=-1, keepdims=True))
    return e / e.sum(axis=-1, keepdims=True)

def multi_head_attention(x, W_Q, W_K, W_V, W_O, h):
    n, d_model = x.shape
    d_head = d_model // h
    # 실습 3과 같은 계산: 나누기 → head 별 Attention → 이어 붙이기 → W_O
    split = lambda M: M.reshape(n, h, d_head).transpose(1, 0, 2)
    Qh, Kh, Vh = split(x @ W_Q), split(x @ W_K), split(x @ W_V)
    weights = softmax(Qh @ Kh.transpose(0, 2, 1) / np.sqrt(d_head))
    concat = (weights @ Vh).transpose(1, 0, 2).reshape(n, d_model)
    return concat @ W_O

np.random.seed(2)
n, d_model, h = 4, 8, 2
x = np.random.randn(n, d_model)                     # 입력 (4, 8)
attn_W = [np.random.randn(d_model, d_model) * 0.5 for _ in range(4)]   # W_Q, W_K, W_V, W_O

attn_out = multi_head_attention(x, *attn_W, h)      # (4, 8)
x1 = layer_norm(x + attn_out)                       # Add(잔차) 뒤 Norm
print("attn_out:", attn_out.shape, " x1:", x1.shape)
# 결과: attn_out: (4, 8)  x1: (4, 8)
print("x1 첫 낱말:", x1[0])
출력
attn_out: (4, 8)  x1: (4, 8)
x1 첫 낱말: [-0.82 -0.19  0.16  0.46 -1.67 -0.58  1.8   0.83]

3단계. FFN 과 두 번째 Add & Norm

낱말마다 따로 개인 과외(FFN)를 받고, 다시 원래 노트에 더해 눈금을 맞춥니다.

Python
d_ff = 4 * d_model                     # 넓힐 폭 = 32 (원 논문 512 → 2048 과 같은 4배)
W1 = np.random.randn(d_model, d_ff) * 0.3
b1 = np.zeros(d_ff)                    # 편향(bias): 더하는 값. 처음엔 0
W2 = np.random.randn(d_ff, d_model) * 0.3
b2 = np.zeros(d_model)

def ffn(x):
    # 8 → 32 로 넓히고, ReLU(음수는 0) 로 비선형을 준 뒤, 32 → 8 로 줄인다
    hidden = np.maximum(0, x @ W1 + b1)
    return hidden @ W2 + b2

x2 = layer_norm(x1 + ffn(x1))          # Add(잔차) 뒤 Norm
print("x2:", x2.shape)
# 결과: x2: (4, 8)

# FFN 은 낱말마다 따로: 한 줄만 넣은 결과와 네 줄을 한꺼번에 넣은 결과의 그 줄이 같다
print("낱말마다 따로 계산되는가:", np.allclose(ffn(x1[2:3]), ffn(x1)[2:3]))
# 결과: True
출력
x2: (4, 8)
낱말마다 따로 계산되는가: True

4단계. 블록 함수로 묶어 두 층 쌓기

지금까지의 차례를 함수 하나(블록 하나)로 묶고, 같은 모양의 블록을 두 번 통과시킵니다.

Python
def encoder_block(x, attn_W, W1, b1, W2, b2, h):
    # 1) Multi-Head Attention → Add & Norm
    x = layer_norm(x + multi_head_attention(x, *attn_W, h))
    # 2) FFN → Add & Norm
    hidden = np.maximum(0, x @ W1 + b1)
    x = layer_norm(x + hidden @ W2 + b2)
    return x

# 층마다 가중치를 따로 만든다(실제 모델도 층마다 다른 가중치)
np.random.seed(3)
layers = []
for _ in range(2):
    layers.append(([np.random.randn(d_model, d_model) * 0.5 for _ in range(4)],
                   np.random.randn(d_model, d_ff) * 0.3, np.zeros(d_ff),
                   np.random.randn(d_ff, d_model) * 0.3, np.zeros(d_model)))

out = x
for i, params in enumerate(layers):
    out = encoder_block(out, *params, h)
    print(f"{i + 1}층 통과 뒤 shape:", out.shape)
# 결과: 1층, 2층 모두 (4, 8) → 모양이 같아 몇 층이든 쌓을 수 있다

# 블록 하나의 파라미터 수
attn_params = 4 * d_model * d_model                       # 256
ffn_params = d_model * d_ff + d_ff + d_ff * d_model + d_model   # 552
print("Attention:", attn_params, " FFN:", ffn_params, " FFN 비율:", round(ffn_params / (attn_params + ffn_params), 2))
# 결과: Attention: 256  FFN: 552  FFN 비율: 0.68 → 블록의 약 2/3
출력
1층 통과 뒤 shape: (4, 8)
2층 통과 뒤 shape: (4, 8)
Attention: 256  FFN: 552  FFN 비율: 0.68

이 실습이 뜻하는 것

노트의 눈금을 맞추는 도구를 만들고(1단계), 스터디 모임 결과를 원래 노트에 더해 정리한 뒤(2단계), 개인 과외로 낱말마다 소화하고 다시 정리해(3단계), 이 차례를 블록 하나로 묶어 두 번 반복한(4단계) 과정입니다. 여기서 원래 노트가 x, 스터디 모임이 multi_head_attention, 개인 과외가 ffn, 눈금 맞추기가 layer_norm 입니다.

단계출력뜻
1. LayerNorm행 평균 0, 표준편차 1크기가 10배 달라도 같은 꼴
2. Attention + Add & Normx1: (4, 8)낱말끼리 정보를 주고받음
3. FFN + Add & Normx2: (4, 8), True낱말마다 따로 가공
4. 두 층 쌓기1층, 2층 모두 (4, 8), FFN 비율 0.68모양이 같아 계속 쌓을 수 있음

한 줄로 줄이면, Encoder 블록은 "주고받기 → 더하고 맞추기 → 따로 소화 → 더하고 맞추기"이고, 들어간 모양 그대로 나와 N 층을 쌓습니다.

코드 설명

  • layer_norm 은 axis=-1(낱말 하나의 칸들)로 평균과 표준편차를 냅니다. 낱말끼리 섞지 않고 한 줄씩 맞춥니다.
  • x + attn_out 의 + 가 잔차 연결(Add)입니다. 원래 x 가 그대로 더해지므로 Attention 이 정보를 망쳐도 원본이 남습니다.
  • np.maximum(0, ...) 이 ReLU 입니다. 음수를 0 으로 바꿔 직선 두 개를 이은 꺾인 함수가 되게 합니다. 이 꺾임이 없으면 W1, W2 두 번 곱한 것이 행렬 하나와 다를 바 없습니다.
  • 3단계의 np.allclose 가 True 인 것은 FFN 이 낱말 사이를 섞지 않는다는 확인입니다. 낱말 사이를 섞는 것은 Attention 뿐입니다.
  • 이 블록은 원 논문의 Post-LN 차례입니다. Pre-LN 으로 바꾸려면 x + 서브층(layer_norm(x)) 꼴로 정규화를 앞에 둡니다.
← 이전 절3장 · 실습 3 — Multi-Head 로 나누고 합치기다음 절 →3장 · 프로젝트 1 — '그것'이 가리키는 낱말 찾기