STEP 02 / 18

보면 안 되는 칸 가리기

어텐션은 기본적으로 모든 토큰을 본다. 하지만 보면 안 되는 칸이 두 종류 있다.

  1. 미래 토큰: 번역문을 한 단어씩 만들 때, 학습 중에도 아직 만들지 않은 뒤쪽 단어를 보면 반칙이다.
  2. 패딩: 길이가 다른 문장을 한 배치로 묶으려고 채워 넣은 <blank> 자리는 의미가 없다.

그래서 attention에 mask 인자가 생긴다(diff의 빨간 줄 → 초록 줄). 가릴 칸의 점수를 softmax 전에 -1e9로 덮으면 softmax 뒤 그 칸의 가중치는 사실상 0이 된다.

subsequent_mask(size)는 미래를 가리는 마스크다. torch.triu(..., diagonal=1)로 대각선 위쪽을 1로 만든 뒤 == 0으로 뒤집어서, i번째 행은 0..i번째 열만 True인 아래 삼각 행렬을 만든다. 맨 앞의 1차원은 나중에 배치 차원에 맞춰 브로드캐스트하기 위한 것이다.

실행 결과에서 볼 것

  • 마스크를 건 가중치 행렬은 대각선 위가 전부 0이다.
  • 패딩 마스크를 걸면 패딩 열의 가중치가 0이 된다.
  • 차트는 원본 노트북의 example_mask다. 행(Masking)이 볼 수 있는 열(Window)만 노랗다.

코드 변화

+12 −1 · 파일 전체 22줄

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

GitHub에서 보기 ↗
11import math
22
33import torch
44
55
6−def attention(query, key, value):
6+def attention(query, key, value, mask=None):
77 "Compute 'Scaled Dot Product Attention'"
88 d_k = query.size(-1)
99 scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
10+ if mask is not None:
11+ scores = scores.masked_fill(mask == 0, -1e9)
1012 p_attn = scores.softmax(dim=-1)
1113 return torch.matmul(p_attn, value), p_attn
14+
15+
16+def subsequent_mask(size):
17+ "Mask out subsequent positions."
18+ attn_shape = (1, size, size)
19+ subsequent_mask = torch.triu(torch.ones(attn_shape), diagonal=1).type(
20+ torch.uint8
21+ )
22+ return subsequent_mask == 0

실행 결과

실행한 코드 demo.py 보기
1import altair as alt
2import pandas as pd
3import torch
4
5from show import show
6from transformer import attention, subsequent_mask
7
8torch.manual_seed(0)
9
10print("subsequent_mask(5) — True인 칸만 볼 수 있다")
11print(subsequent_mask(5)[0].int())
12print()
13
14x = torch.randn(1, 5, 8)
15_, p = attention(x, x, x, mask=subsequent_mask(5))
16print("마스크를 건 self-attention 가중치 — 대각선 위(미래)가 전부 0")
17print(p[0].round(decimals=2))
18print()
19
20# 패딩 마스크: 문장 길이를 맞추려고 채운 <blank> 자리를 가린다
21src = torch.tensor([[5, 7, 9, 2, 2]]) # 2 = <blank>
22pad_mask = (src != 2).unsqueeze(-2)
23print("패딩 마스크 (src != pad):", pad_mask.int().tolist())
24_, p = attention(x, x, x, mask=pad_mask)
25print("패딩 자리(마지막 두 열)의 가중치:", p[0, :, 3:].abs().max().item())
26print()
27
28
29# 원본 노트북의 example_mask
30def example_mask():
31 LS_data = pd.concat(
32 [
33 pd.DataFrame(
34 {
35 "Subsequent Mask": subsequent_mask(20)[0][x, y].flatten(),
36 "Window": y,
37 "Masking": x,
38 }
39 )
40 for y in range(20)
41 for x in range(20)
42 ]
43 )
44
45 return (
46 alt.Chart(LS_data)
47 .mark_rect()
48 .properties(height=250, width=250)
49 .encode(
50 alt.X("Window:O"),
51 alt.Y("Masking:O"),
52 alt.Color("Subsequent Mask:Q", scale=alt.Scale(scheme="viridis")),
53 )
54 .interactive()
55 )
56
57
58show(example_mask(), "mask")
subsequent_mask(5) — True인 칸만 볼 수 있다
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, 1]], dtype=torch.int32)

마스크를 건 self-attention 가중치 — 대각선 위(미래)가 전부 0
tensor([[1.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.2700, 0.7300, 0.0000, 0.0000, 0.0000],
        [0.0100, 0.0100, 0.9800, 0.0000, 0.0000],
        [0.0800, 0.1800, 0.4300, 0.3000, 0.0000],
        [0.0100, 0.0200, 0.0700, 0.0300, 0.8800]])

패딩 마스크 (src != pad): [[[1, 1, 1, 0, 0]]]
패딩 자리(마지막 두 열)의 가중치: 0.0
차트 불러오는 중…