STEP 11 / 18

학습용 배치: 한 칸 밀기와 마스크

여기서부터 2부, 학습이다. 첫 부품은 문장쌍 배치를 학습에 필요한 형태로 바꾸는 Batch 클래스다.

teacher forcing: 학습할 때는 디코더에 정답 문장을 통째로 넣고, 모든 위치에서 동시에 "다음 토큰"을 맞히게 한다. 그러려면 정답을 한 칸 밀어야 한다.

  • tgt = 정답에서 마지막 토큰을 뺀 것 → 디코더 입력
  • tgt_y = 정답에서 첫 토큰을 뺀 것 → 맞혀야 할 답

i번째 위치는 tgt[0..i]를 보고 tgt_y[i]를 맞힌다. 이때 i번째 위치가 tgt[i+1..]를 보면 답을 베끼는 셈이므로 미래 마스크가 필요하다. make_std_mask는 패딩 마스크와 subsequent_mask를 AND로 합친다.

ntokens는 패딩이 아닌 정답 토큰 수다. 손실을 이 값으로 나눠 토큰당 평균 손실을 만든다.

실행 결과에서 볼 것

  • tgt와 tgt_y가 한 칸 어긋나 있다.
  • tgt_mask는 아래 삼각 행렬인데, 패딩 열(마지막 열)이 추가로 0이다.

코드 변화

+22 −0 · 파일 전체 313줄

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

GitHub에서 보기 ↗
289289 [ys, torch.zeros(1, 1).type_as(src.data).fill_(next_word)], dim=1
290290 )
291291 return ys
292+
293+
294+class Batch:
295+ """Object for holding a batch of data with mask during training."""
296+
297+ def __init__(self, src, tgt=None, pad=2): # 2 = <blank>
298+ self.src = src
299+ self.src_mask = (src != pad).unsqueeze(-2)
300+ if tgt is not None:
301+ self.tgt = tgt[:, :-1]
302+ self.tgt_y = tgt[:, 1:]
303+ self.tgt_mask = self.make_std_mask(self.tgt, pad)
304+ self.ntokens = (self.tgt_y != pad).data.sum()
305+
306+ @staticmethod
307+ def make_std_mask(tgt, pad):
308+ "Create a mask to hide padding and future words."
309+ tgt_mask = (tgt != pad).unsqueeze(-2)
310+ tgt_mask = tgt_mask & subsequent_mask(tgt.size(-1)).type_as(
311+ tgt_mask.data
312+ )
313+ return tgt_mask

실행 결과

실행한 코드 demo.py 보기
1import torch
2
3from transformer import Batch
4
5# 0 = <s>, 1 = </s>, 2 = <blank>(패딩)
6src = torch.tensor([[0, 11, 12, 13, 1, 2]])
7tgt = torch.tensor([[0, 21, 22, 1, 2, 2]])
8b = Batch(src, tgt, pad=2)
9
10print("src ", b.src.tolist())
11print("src_mask ", b.src_mask.int().tolist(), " (패딩만 가림)")
12print()
13print("tgt ", tgt.tolist())
14print("b.tgt ", b.tgt.tolist(), " ← 디코더 입력: 마지막 토큰을 뺀 것")
15print("b.tgt_y ", b.tgt_y.tolist(), " ← 정답: 첫 토큰을 뺀 것 (한 칸 앞으로 민 것)")
16print()
17print("i번째 입력을 보고 i번째 정답을 맞힌다:")
18for i, (a, y) in enumerate(zip(b.tgt[0].tolist(), b.tgt_y[0].tolist())):
19 print(f" 위치 {i}: {b.tgt[0, : i + 1].tolist()} 까지 보고 → {y}")
20print()
21print("tgt_mask = 미래 가리기 AND 패딩 가리기")
22print(b.tgt_mask[0].int())
23print("ntokens (패딩이 아닌 정답 수):", b.ntokens.item())
src       [[0, 11, 12, 13, 1, 2]]
src_mask  [[[1, 1, 1, 1, 1, 0]]]  (패딩만 가림)

tgt       [[0, 21, 22, 1, 2, 2]]
b.tgt     [[0, 21, 22, 1, 2]]  ← 디코더 입력: 마지막 토큰을 뺀 것
b.tgt_y   [[21, 22, 1, 2, 2]]  ← 정답: 첫 토큰을 뺀 것 (한 칸 앞으로 민 것)

i번째 입력을 보고 i번째 정답을 맞힌다:
  위치 0: [0] 까지 보고 → 21
  위치 1: [0, 21] 까지 보고 → 22
  위치 2: [0, 21, 22] 까지 보고 → 1
  위치 3: [0, 21, 22, 1] 까지 보고 → 2
  위치 4: [0, 21, 22, 1, 2] 까지 보고 → 2

tgt_mask = 미래 가리기 AND 패딩 가리기
tensor([[1, 0, 0, 0, 0],
        [1, 1, 0, 0, 0],
        [1, 1, 1, 0, 0],
        [1, 1, 1, 1, 0],
        [1, 1, 1, 1, 0]], dtype=torch.int32)
ntokens (패딩이 아닌 정답 수): 3