STEP 07 / 18

인코더: 같은 층을 6번

Transformer 구조

그림 왼쪽이 인코더다. 지금까지 만든 부품을 조립한다.

  • EncoderLayer: self-attention과 FFN을 각각 SublayerConnection으로 감싼 두 단계다. self-attention이므로 query, key, value에 모두 같은 x를 넣는다.
  • Encoder: EncoderLayer를 clones로 N개 복제해 차례로 통과시키고, 마지막에 LayerNorm을 한 번 더 건다. Pre-LN 구조에서는 마지막 층의 출력이 정규화되지 않은 상태라 이 정규화가 필요하다.

EncoderLayer는 어텐션과 FFN 객체를 생성자 인자로 받는다. 어떤 어텐션을 쓸지는 조립하는 쪽(9단계의 make_model)이 정한다.

실행 결과에서 볼 것

  • 6층 인코더의 파라미터는 약 1,890만 개다.
  • 층마다 가중치가 다른 객체다(clones가 deepcopy하기 때문).
  • 패딩 자리 값을 100배 크기의 난수로 바꿔도 진짜 토큰 4개의 출력은 완전히 같다(차이 0). 2단계의 마스크가 6층을 통과하는 동안 계속 지켜진다.

코드 변화

+31 −0 · 파일 전체 181줄

이전 단계의 transformer.py와 비교했다. 초록 줄이 새로 생긴 줄, 빨간 줄이 사라진 줄이다.

GitHub에서 보기 ↗
148148 def forward(self, x, sublayer):
149149 "Apply residual connection to any sublayer with the same size."
150150 return x + self.dropout(sublayer(self.norm(x)))
151+
152+
153+class EncoderLayer(nn.Module):
154+ "Encoder is made up of self-attn and feed forward (defined below)"
155+
156+ def __init__(self, size, self_attn, feed_forward, dropout):
157+ super(EncoderLayer, self).__init__()
158+ self.self_attn = self_attn
159+ self.feed_forward = feed_forward
160+ self.sublayer = clones(SublayerConnection(size, dropout), 2)
161+ self.size = size
162+
163+ def forward(self, x, mask):
164+ "Follow Figure 1 (left) for connections."
165+ x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
166+ return self.sublayer[1](x, self.feed_forward)
167+
168+
169+class Encoder(nn.Module):
170+ "Core encoder is a stack of N layers"
171+
172+ def __init__(self, layer, N):
173+ super(Encoder, self).__init__()
174+ self.layers = clones(layer, N)
175+ self.norm = LayerNorm(layer.size)
176+
177+ def forward(self, x, mask):
178+ "Pass the input (and mask) through each layer in turn."
179+ for layer in self.layers:
180+ x = layer(x, mask)
181+ return self.norm(x)

실행 결과

실행한 코드 demo.py 보기
1import copy
2
3import torch
4
5from transformer import Encoder, EncoderLayer, MultiHeadedAttention, PositionwiseFeedForward
6
7torch.manual_seed(0)
8
9d_model, N = 512, 6
10c = copy.deepcopy
11attn = MultiHeadedAttention(8, d_model)
12ff = PositionwiseFeedForward(d_model, 2048, 0.1)
13encoder = Encoder(EncoderLayer(d_model, c(attn), c(ff), 0.1), N).eval()
14
15print(f"인코더 층 {len(encoder.layers)}개, 층마다 서브층 {len(encoder.layers[0].sublayer)}개 (self-attn, FFN)")
16n = sum(p.numel() for p in encoder.parameters())
17print(f"인코더 파라미터 수: {n:,}")
18print("층마다 별도 가중치?", encoder.layers[0].self_attn.linears[0].weight is not encoder.layers[1].self_attn.linears[0].weight)
19print()
20
21x = torch.randn(1, 6, d_model)
22mask = torch.tensor([[[True, True, True, True, False, False]]]) # 뒤 2개는 패딩
23out = encoder(x, mask)
24print("입력", tuple(x.shape), "→ 출력", tuple(out.shape))
25print()
26
27print("패딩 자리 값을 마음대로 바꿔도 진짜 토큰의 출력은 그대로인가?")
28x2 = x.clone()
29x2[:, 4:] = torch.randn(1, 2, d_model) * 100
30out2 = encoder(x2, mask)
31print(f" 진짜 토큰 4개 출력 차이: {(out[:, :4] - out2[:, :4]).abs().max().item():.1e}")
32print(" → 마스크 덕분에 어텐션이 패딩을 보지 않는다")
인코더 층 6개, 층마다 서브층 2개 (self-attn, FFN)
인코더 파라미터 수: 18,915,328
층마다 별도 가중치? True

입력 (1, 6, 512) → 출력 (1, 6, 512)

패딩 자리 값을 마음대로 바꿔도 진짜 토큰의 출력은 그대로인가?
  진짜 토큰 4개 출력 차이: 0.0e+00
  → 마스크 덕분에 어텐션이 패딩을 보지 않는다