STEP 12 / 18

손실 함수: 너무 확신하지 않게

정답 토큰에 확률 1을 주는 one-hot 대신, 정답에 0.9, 나머지에 0.1을 나눠 준 분포를 목표로 학습한다. 논문은 이를 label smoothing($\epsilon_{ls}=0.1$)이라고 부른다. 모델이 지나치게 확신하지 않게 되어 perplexity는 조금 나빠지지만 정확도와 BLEU는 좋아진다.

LabelSmoothing: 목표 분포 true_dist를 만든 뒤 모델의 로그 확률과의 KL divergence를 손실로 쓴다. - 모든 칸을 smoothing / (size - 2)로 채운다. 2를 빼는 이유는 정답 칸과 패딩 칸을 제외하기 때문이다. - 정답 칸(scatter_)에 confidence = 1 - smoothing을 넣는다. - 패딩 열은 0으로 만들고, 정답 자체가 패딩인 행(문장 뒤 채움)은 통째로 0으로 만들어 손실에서 뺀다.

SimpleLossCompute: generator를 거쳐 손실을 계산하고 토큰 수로 나눈다. 두 값을 돌려준다. 하나는 기록용 합계(sloss.data * norm)이고, 하나는 backward()를 부를 텐서(sloss)다.

실행 결과에서 볼 것

  • 원본의 example_label_smoothing: 정답 칸 0.6, 나머지 0.133씩, 패딩이 정답인 3번째 행은 전부 0이다.
  • 원본의 penalization_visualization: 정답 확률을 계속 키우면 손실이 내려가다가 다시 올라간다. 지나친 확신에 벌점을 준다는 뜻이다.

코드 변화

+43 −0 · 파일 전체 356줄

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

GitHub에서 보기 ↗
311311 tgt_mask.data
312312 )
313313 return tgt_mask
314+
315+
316+class LabelSmoothing(nn.Module):
317+ "Implement label smoothing."
318+
319+ def __init__(self, size, padding_idx, smoothing=0.0):
320+ super(LabelSmoothing, self).__init__()
321+ self.criterion = nn.KLDivLoss(reduction="sum")
322+ self.padding_idx = padding_idx
323+ self.confidence = 1.0 - smoothing
324+ self.smoothing = smoothing
325+ self.size = size
326+ self.true_dist = None
327+
328+ def forward(self, x, target):
329+ assert x.size(1) == self.size
330+ true_dist = x.data.clone()
331+ true_dist.fill_(self.smoothing / (self.size - 2))
332+ true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)
333+ true_dist[:, self.padding_idx] = 0
334+ mask = torch.nonzero(target.data == self.padding_idx)
335+ if mask.dim() > 0:
336+ true_dist.index_fill_(0, mask.squeeze(), 0.0)
337+ self.true_dist = true_dist
338+ return self.criterion(x, true_dist.clone().detach())
339+
340+
341+class SimpleLossCompute:
342+ "A simple loss compute and train function."
343+
344+ def __init__(self, generator, criterion):
345+ self.generator = generator
346+ self.criterion = criterion
347+
348+ def __call__(self, x, y, norm):
349+ x = self.generator(x)
350+ sloss = (
351+ self.criterion(
352+ x.contiguous().view(-1, x.size(-1)), y.contiguous().view(-1)
353+ )
354+ / norm
355+ )
356+ return sloss.data * norm, sloss

실행 결과

실행한 코드 demo.py 보기
1import altair as alt
2import pandas as pd
3import torch
4
5from show import show
6from transformer import LabelSmoothing
7
8
9# 원본 노트북의 example_label_smoothing
10def example_label_smoothing():
11 crit = LabelSmoothing(5, 0, 0.4)
12 predict = torch.FloatTensor(
13 [
14 [0, 0.2, 0.7, 0.1, 0],
15 [0, 0.2, 0.7, 0.1, 0],
16 [0, 0.2, 0.7, 0.1, 0],
17 [0, 0.2, 0.7, 0.1, 0],
18 [0, 0.2, 0.7, 0.1, 0],
19 ]
20 )
21 crit(x=predict.log(), target=torch.LongTensor([2, 1, 0, 3, 3]))
22 print("정답 [2, 1, 0, 3, 3]에 대한 목표 분포 (smoothing=0.4, 0번 = 패딩)")
23 print(crit.true_dist.round(decimals=3))
24 print("→ 정답 칸 0.6, 나머지 0.4를 패딩 아닌 칸 3개에 0.133씩. 정답이 패딩인 3번째 행은 통째로 0")
25 print()
26 LS_data = pd.concat(
27 [
28 pd.DataFrame(
29 {
30 "target distribution": crit.true_dist[x, y].flatten(),
31 "columns": y,
32 "rows": x,
33 }
34 )
35 for y in range(5)
36 for x in range(5)
37 ]
38 )
39
40 return (
41 alt.Chart(LS_data)
42 .mark_rect(color="Blue", opacity=1)
43 .properties(height=200, width=200)
44 .encode(
45 alt.X("columns:O", title=None),
46 alt.Y("rows:O", title=None),
47 alt.Color(
48 "target distribution:Q", scale=alt.Scale(scheme="viridis")
49 ),
50 )
51 .interactive()
52 )
53
54
55show(example_label_smoothing(), "label_smoothing")
56
57
58# 원본 노트북의 penalization_visualization
59def loss(x, crit):
60 d = x + 3 * 1
61 predict = torch.FloatTensor([[0, x / d, 1 / d, 1 / d, 1 / d]])
62 return crit(predict.log(), torch.LongTensor([1])).data
63
64
65def penalization_visualization():
66 crit = LabelSmoothing(5, 0, 0.1)
67 loss_data = pd.DataFrame(
68 {
69 "Loss": [loss(x, crit) for x in range(1, 100)],
70 "Steps": list(range(99)),
71 }
72 ).astype("float")
73
74 return (
75 alt.Chart(loss_data)
76 .mark_line()
77 .properties(width=350)
78 .encode(
79 x="Steps",
80 y="Loss",
81 )
82 .interactive()
83 )
84
85
86print("정답 확률을 점점 키울 때 손실 — 너무 확신하면 오히려 손실이 다시 오른다")
87show(penalization_visualization(), "penalization")
정답 [2, 1, 0, 3, 3]에 대한 목표 분포 (smoothing=0.4, 0번 = 패딩)
tensor([[0.0000, 0.1330, 0.6000, 0.1330, 0.1330],
        [0.0000, 0.6000, 0.1330, 0.1330, 0.1330],
        [0.0000, 0.0000, 0.0000, 0.0000, 0.0000],
        [0.0000, 0.1330, 0.1330, 0.6000, 0.1330],
        [0.0000, 0.1330, 0.1330, 0.6000, 0.1330]])
→ 정답 칸 0.6, 나머지 0.4를 패딩 아닌 칸 3개에 0.133씩. 정답이 패딩인 3번째 행은 통째로 0
차트 불러오는 중…
정답 확률을 점점 키울 때 손실 — 너무 확신하면 오히려 손실이 다시 오른다
차트 불러오는 중…