STEP 14 / 18

첫 학습: 입력을 그대로 따라 쓰기

모든 부품이 모였다. 진짜 번역 데이터로 가기 전에 가장 쉬운 과제로 전체가 맞물려 돌아가는지 확인한다. 입력 숫자열을 그대로 출력하는 복사 과제다.

  • data_gen: 1~10 사이 난수 10개짜리 문장을 만든다. 첫 토큰은 시작 기호 1로 고정하고 src와 tgt를 똑같이 둔다.
  • example_simple_model: 사전 크기 11, 2층짜리 모델을 만들어 Adam + rate(warmup 400)로 20 epoch 학습한 뒤, greedy_decode로 [0, 1, ..., 9]를 따라 쓰게 한다.

10단계에서 같은 크기의 모델이 무작위 토큰을 내놓던 것과 비교해 보자.

원본 노트북은 이 예제의 실행 줄(execute_example(example_simple_model))을 주석 처리해 두어서 출력이 없다. 여기서는 CPU로 실제로 돌린 결과를 싣는다(시드 0).

실행 결과에서 볼 것

  • epoch마다 한 줄씩 손실이 찍힌다. 2.9에서 시작해 0.1 안팎까지 내려간다.
  • 학습률은 끝까지 오르기만 한다. 20 epoch × 20배치 = 400스텝이 warmup(400)과 같아서 내려가는 구간에 들어가지 못한다.
  • 마지막 출력 [0, 2, 1, 3, 4, ...]는 앞쪽 두세 칸이 틀린다. 원본 README에 적힌 실행 결과도 [0, 2, 3, 2, 4, ...]처럼 앞쪽이 틀린다.

왜 앞쪽이 틀릴까

data_gen은 항상 1로 시작하고 1~10만 쓰는 문장을 만든다. 그런데 테스트 입력 [0, 1, ..., 9]는 0으로 시작한다. 0은 이 과제에서 패딩 id(Batch(src, tgt, 0), padding_idx=0)라 학습 중 한 번도 입력으로 나온 적이 없는 토큰이다. 출력 아래쪽 실험에서 같은 설정으로 다시 학습한 모델에 학습 때와 같은 모양의 입력([1, 2, ..., 10])을 넣으면 완벽하게 따라 쓴다. 무작위 문장 100개 중에서는 88개를 정확히 복사했다. 모델이 못 배운 게 아니라 학습 때 본 적 없는 입력을 받은 것이다.

코드 변화

+54 −0 · 파일 전체 503줄

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

GitHub에서 보기 ↗
55import torch
66import torch.nn as nn
77from torch.nn.functional import log_softmax
8+from torch.optim.lr_scheduler import LambdaLR
89
910
1011def attention(query, key, value, mask=None, dropout=None):
447448class DummyScheduler:
448449 def step(self):
449450 None
451+
452+
453+def data_gen(V, batch_size, nbatches):
454+ "Generate random data for a src-tgt copy task."
455+ for i in range(nbatches):
456+ data = torch.randint(1, V, size=(batch_size, 10))
457+ data[:, 0] = 1
458+ src = data.requires_grad_(False).clone().detach()
459+ tgt = data.requires_grad_(False).clone().detach()
460+ yield Batch(src, tgt, 0)
461+
462+
463+def example_simple_model():
464+ V = 11
465+ criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
466+ model = make_model(V, V, N=2)
467+
468+ optimizer = torch.optim.Adam(
469+ model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9
470+ )
471+ lr_scheduler = LambdaLR(
472+ optimizer=optimizer,
473+ lr_lambda=lambda step: rate(
474+ step, model_size=model.src_embed[0].d_model, factor=1.0, warmup=400
475+ ),
476+ )
477+
478+ batch_size = 80
479+ for epoch in range(20):
480+ model.train()
481+ run_epoch(
482+ data_gen(V, batch_size, 20),
483+ model,
484+ SimpleLossCompute(model.generator, criterion),
485+ optimizer,
486+ lr_scheduler,
487+ mode="train",
488+ )
489+ model.eval()
490+ run_epoch(
491+ data_gen(V, batch_size, 5),
492+ model,
493+ SimpleLossCompute(model.generator, criterion),
494+ DummyOptimizer(),
495+ DummyScheduler(),
496+ mode="eval",
497+ )[0]
498+
499+ model.eval()
500+ src = torch.LongTensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
501+ max_len = src.shape[1]
502+ src_mask = torch.ones(1, 1, max_len)
503+ print(greedy_decode(model, src, src_mask, max_len=max_len, start_symbol=0))

실행 결과

실행한 코드 demo.py 보기
1import torch
2
3from transformer import data_gen, example_simple_model
4
5torch.manual_seed(0)
6
7print("복사 과제의 데이터 한 배치 (앞 3문장)")
8b = next(data_gen(V=11, batch_size=3, nbatches=1))
9print(" src ", b.src.tolist())
10print(" tgt_y", b.tgt_y.tolist(), " ← src를 그대로 따라 쓰는 게 정답")
11print()
12
13print("원본의 example_simple_model 실행: N=2 모델, 20 epoch (CPU 약 2분)")
14print("한 epoch에 한 줄씩, 그 epoch 두 번째 배치의 손실이 찍힌다")
15print()
16example_simple_model()
17print()
18print("입력 [0, 1, ..., 9]를 그대로 따라 썼다면 학습 성공")
19print()
20print("─" * 60)
21print("왜 [0, 1, ..., 9]를 완벽하게 따라 쓰지 못할까?")
22print("학습 데이터는 항상 1로 시작하고 1~10만 쓴다. 0은 이 과제에서 패딩 id라 한 번도 입력으로 나온 적이 없다.")
23print("같은 설정으로 다시 학습해서 학습 때와 같은 모양의 입력을 넣어 본다.")
24print()
25
26import contextlib
27import io
28
29from torch.optim.lr_scheduler import LambdaLR
30
31from transformer import (
32 DummyOptimizer, DummyScheduler, LabelSmoothing, SimpleLossCompute,
33 greedy_decode, make_model, rate, run_epoch,
34)
35
36V = 11
37criterion = LabelSmoothing(size=V, padding_idx=0, smoothing=0.0)
38model = make_model(V, V, N=2)
39optimizer = torch.optim.Adam(model.parameters(), lr=0.5, betas=(0.9, 0.98), eps=1e-9)
40lr_scheduler = LambdaLR(optimizer=optimizer, lr_lambda=lambda step: rate(step, 512, 1.0, 400))
41for epoch in range(20):
42 model.train()
43 with contextlib.redirect_stdout(io.StringIO()):
44 run_epoch(data_gen(V, 80, 20), model, SimpleLossCompute(model.generator, criterion),
45 optimizer, lr_scheduler, mode="train")
46model.eval()
47
48
49def copy(seq):
50 src = torch.LongTensor([seq])
51 out = greedy_decode(model, src, torch.ones(1, 1, len(seq)), max_len=len(seq), start_symbol=seq[0])
52 return out[0].tolist()
53
54
55print(" 원본 테스트 [0..9] →", copy(list(range(10))))
56print(" 학습과 같은 [1..10] →", copy(list(range(1, 11))))
57ok = 0
58for _ in range(100):
59 seq = torch.randint(1, V, (10,)).tolist()
60 seq[0] = 1
61 ok += copy(seq) == seq
62print(f" 학습과 같은 분포의 무작위 문장 100개 중 완벽하게 따라 쓴 것: {ok}개")
복사 과제의 데이터 한 배치 (앞 3문장)
  src   [[1, 10, 4, 1, 4, 10, 8, 4, 8, 4], [1, 7, 7, 10, 9, 7, 7, 9, 5, 4], [1, 10, 2, 5, 5, 2, 10, 10, 10, 1]]
  tgt_y [[10, 4, 1, 4, 10, 8, 4, 8, 4], [7, 7, 10, 9, 7, 7, 9, 5, 4], [10, 2, 5, 5, 2, 10, 10, 10, 1]]  ← src를 그대로 따라 쓰는 게 정답

원본의 example_simple_model 실행: N=2 모델, 20 epoch (CPU 약 2분)
한 epoch에 한 줄씩, 그 epoch 두 번째 배치의 손실이 찍힌다

Epoch Step:      1 | Accumulation Step:   2 | Loss:   2.89 | Tokens / Sec:  2617.7 | Learning Rate: 5.5e-06
Epoch Step:      1 | Accumulation Step:   2 | Loss:   2.10 | Tokens / Sec:  2902.4 | Learning Rate: 6.1e-05
Epoch Step:      1 | Accumulation Step:   2 | Loss:   1.74 | Tokens / Sec:  2884.0 | Learning Rate: 1.2e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   1.39 | Tokens / Sec:  2839.5 | Learning Rate: 1.7e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.98 | Tokens / Sec:  2821.3 | Learning Rate: 2.3e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.61 | Tokens / Sec:  2695.7 | Learning Rate: 2.8e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.30 | Tokens / Sec:  2892.2 | Learning Rate: 3.4e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.25 | Tokens / Sec:  2861.2 | Learning Rate: 3.9e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.17 | Tokens / Sec:  2821.7 | Learning Rate: 4.5e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.15 | Tokens / Sec:  2907.0 | Learning Rate: 5.0e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.11 | Tokens / Sec:  2859.0 | Learning Rate: 5.6e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.13 | Tokens / Sec:  2853.5 | Learning Rate: 6.1e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.20 | Tokens / Sec:  2807.9 | Learning Rate: 6.7e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.17 | Tokens / Sec:  2822.6 | Learning Rate: 7.2e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.20 | Tokens / Sec:  2777.5 | Learning Rate: 7.8e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.11 | Tokens / Sec:  2836.7 | Learning Rate: 8.3e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.06 | Tokens / Sec:  2854.2 | Learning Rate: 8.9e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.20 | Tokens / Sec:  2859.5 | Learning Rate: 9.4e-04
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.08 | Tokens / Sec:  2889.9 | Learning Rate: 1.0e-03
Epoch Step:      1 | Accumulation Step:   2 | Loss:   0.14 | Tokens / Sec:  2726.9 | Learning Rate: 1.1e-03
tensor([[0, 2, 1, 3, 4, 5, 6, 7, 8, 9]])

입력 [0, 1, ..., 9]를 그대로 따라 썼다면 학습 성공

────────────────────────────────────────────────────────────
왜 [0, 1, ..., 9]를 완벽하게 따라 쓰지 못할까?
학습 데이터는 항상 1로 시작하고 1~10만 쓴다. 0은 이 과제에서 패딩 id라 한 번도 입력으로 나온 적이 없다.
같은 설정으로 다시 학습해서 학습 때와 같은 모양의 입력을 넣어 본다.

  원본 테스트  [0..9]  → [0, 3, 2, 3, 4, 5, 6, 7, 8, 9]
  학습과 같은 [1..10] → [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  학습과 같은 분포의 무작위 문장 100개 중 완벽하게 따라 쓴 것: 88개