얼마나 잘 맞히나: 루나가 놀라는 정도를 재기

이 장의 물음

언어 모델을 학습시키면 화면에 손실이라는 숫자가 계속 찍히고, 논문은 퍼플렉서티를 적고, 압축 대회는 바이트당 비트로 순위를 매긴다. 셋 다 「얼마나 잘 맞히나」를 재는 숫자라는데 크기도 단위도 제각각이다. 손실 2.28과 퍼플렉서티 9.81과 3.29비트가 같은 모델의 같은 성적이라면, 이 숫자들은 무엇을 잰 것이고 서로 어떻게 바뀔까?

숫자 하나가 정해지면 하고 싶은 일도 생긴다. 글자 바이그램과 낱말 바이그램처럼 맞히는 단위가 다른 두 모델을 한 줄에 세울 수 있을까? 앞 글자를 둘, 셋, 여섯으로 늘린 표는 얼마나 나아지고, 어디서 더 나아지지 않을까?

이 장은 다음 물음에 차례로 답한다.

크로스 엔트로피: 처음 보는 동화에 루나가 놀란 평균

둘째 주 실습 시간, 민준은 낱말 바이그램이 쓴 「Once upon a plan. Sue was magic!」을 선생님 화면에 띄우고 글자 바이그램의 옹알이보다 훨씬 말 같지 않으냐고 물었다. 선생님은 샘플을 끝까지 읽고 나서 고개를 저었다.

선생님 T13
선생님

샘플은 다시 뽑으면 또 달라져요. 읽는 대신, 루나가 얼마나 놀라는지 재 보세요.

이서연 S03
이서연

루나가… 놀라요?

선생님 T01
선생님

처음 보는 동화를 한 글자씩 보여 주고, 실제로 나온 글자에 루나가 확률을 얼마나 줬는지 보는 거예요. 낮게 줬던 글자가 나오면 많이 놀란 거고요.

이 장이 끝나면 루나가 처음 보는 동화에 얼마나 놀라는지를 바이트당 비트 하나로 적고, 앞 글자를 몇 개까지 보는 표가 가장 잘 맞히는지 알게 된다.

표를 센 학습 글로 점수를 매기면 학습 글의 긴 문장을 외운 표일수록 점수가 좋아지므로, 처음 보는 동화로 재야 한다. 우리가 바라는 루나는 외운 글을 되읊기보다 새 동화에서도 다음 글자를 잘 맞혀야 하니, 표를 셀 때 쓰지 않은 글로 잰다. 타이니스토리즈의 검증 파일(22,132,376 바이트)이 그 글이다.

글자 하나에 놀란 정도

바이그램 표로 공백 다음의 「the 」 네 글자를 맞혀 보자. 표가 이 네 글자에 준 확률은 0.1492, 0.3119, 0.5999, 0.3785였다. 확률이 1이면 조금도 놀랄 일이 없고, 확률이 작을수록 더 놀라므로, 놀란 정도를 −log₂ p로 재면 확률 1은 0비트, 확률 1/2는 1비트, 1/4은 2비트가 된다.

글자 앞 글자 표가 준 확률 놀란 정도 −log₂ p
t ␣ 0.1492 2.74비트
h t 0.3119 1.68비트
e h 0.5999 0.74비트
␣ e 0.3785 1.40비트

네 글자에 모두 6.56비트를 놀랐으니 글자 하나에 평균 1.64비트이고, 「q」 다음에 「u」가 오면 표가 준 확률이 1.000이라 놀람은 0비트다. 같은 계산을 검증 글의 글자 2천 2백만 개 모두에 대해 하고 평균을 내면, 그 모델이 새 동화를 얼마나 잘 맞히는지가 숫자 하나로 나온다. 민준은 이 평균이 정보이론 강의의 어디에 있었는지 기억해 냈다.

우리 실험에서 「실제 확률」은 검증 글에 실제로 나온 글자이고, 「믿은 확률」은 루나의 표다. 검증 글의 맞힐 글자가 N개일 때 루나가 놀란 평균은 다음과 같다.

H=−1N∑t=1Nlog⁡2p(xt∣x<t)\textcolor{#d62728}{H} = -\frac{1}{\textcolor{#3c4bff}{N}} \sum_{\textcolor{#a67a63}{t}=1}^{\textcolor{#3c4bff}{N}} \log_2 \textcolor{#c2408a}{p}(\textcolor{#1b9e77}{x}_{\textcolor{#a67a63}{t}} \mid \textcolor{#1b9e77}{x}_{<\textcolor{#a67a63}{t}})
H크로스 엔트로피: 글자 하나에 놀란 평균(비트)N검증 글에서 맞힐 글자 수xt, x<tt번째 글자와 그 앞의 글p(⋅∣⋅)모델이 실제 다음 글자에 준 확률\begin{array}{ll} \textcolor{#d62728}{H} & \text{크로스 엔트로피: 글자 하나에 놀란 평균(비트)} \\ \textcolor{#3c4bff}{N} & \text{검증 글에서 맞힐 글자 수} \\ \textcolor{#1b9e77}{x}_{\textcolor{#a67a63}{t}},\ \textcolor{#1b9e77}{x}_{<\textcolor{#a67a63}{t}} & \textcolor{#a67a63}{t}\text{번째 글자와 그 앞의 글} \\ \textcolor{#c2408a}{p}(\cdot \mid \cdot) & \text{모델이 실제 다음 글자에 준 확률} \end{array}

이렇게 처음 보는 글의 실제 다음 글자에 모델이 준 확률로 잰 평균 놀람을 크로스 엔트로피 (검증 글에 놀란 평균 / cross-entropy)라 한다. 로그를 씌운 확률의 합이라서, 글 전체의 확률을 곱하다 0으로 뭉개지는 일도 없다.

네 모델을 같은 글로 재기

네 모델을 비교한다. 바이트 256개에 모두 1/256을 주는 고른 분포, 학습 글에 나온 바이트 149가지에만 고르게 주는 고른 분포, 앞 글자를 보지 않고 바이트마다 학습 글에 나온 비율을 주는 유니그램, 그리고 바이그램이다. 모두 같은 검증 글의 같은 바이트 22,132,370개를 맞히게 했다.

그런데 표로 처음 보는 글을 맞히면 문제가 하나 생긴다. 학습 글에서 한 번도 세지 못한 이어짐이 검증 글에 나오면 표가 준 확률이 0이고, −log₂ 0은 무한대라서 평균 전체가 무한대가 된다. 그래서 칸마다 작은 수 α(알파)를 더해 둔다.

p(b∣a)=c(a,b)+α∑b′c(a,b′)+α V\textcolor{#c2408a}{p}(b \mid a) = \frac{\textcolor{#3c4bff}{c}(a, b) + \textcolor{#008787}{\alpha}}{\sum_{b'} \textcolor{#3c4bff}{c}(a, b') + \textcolor{#008787}{\alpha}\,\textcolor{#3c4bff}{V}}
c(a,b)학습 글에서 a 바로 뒤에 b 가 온 횟수α칸마다 더하는 작은 수(센 적 없는 이어짐에도 확률을 남긴다)V어휘 크기(바이트 256가지). 행의 칸 수만큼 더했으니 분모에도 αV\begin{array}{ll} \textcolor{#3c4bff}{c}(a, b) & \text{학습 글에서 } a \text{ 바로 뒤에 } b \text{ 가 온 횟수} \\ \textcolor{#008787}{\alpha} & \text{칸마다 더하는 작은 수(센 적 없는 이어짐에도 확률을 남긴다)} \\ \textcolor{#3c4bff}{V} & \text{어휘 크기(바이트 256가지). 행의 칸 수만큼 더했으니 분모에도 } \textcolor{#008787}{\alpha}\textcolor{#3c4bff}{V} \end{array}

이렇게 칸마다 같은 수를 더해 0을 없애는 방법을 add-α 스무딩 (칸마다 α 더하기 / add-α smoothing)이라 하고, α = 1이면 라플라스 스무딩이라고도 부른다. 스크립트에서는 아래 둘째 줄이 add-α 스무딩 식이고, 그 아래 줄이 크로스 엔트로피를 nat 로 잰 값이다.

# sources/surprise/measure.py (ngram_nats 에서, 설명 줄은 뺐다)
c_full, c_ctx = ngram_counts(train, val, n)
p = (c_full + alpha) / (c_ctx + alpha * V)
return float(-np.log(p).mean()), float((c_ctx > 0).mean())
학습 글 98,363,266 바이트, 검증 글 22,132,376 바이트
맞힐 바이트 22,132,370 개 (검증 글 앞 6 바이트 제외)

## 1. 기본 표 (nats = 바이트당 교차 엔트로피, bpb = 바이트당 비트, ppl = 퍼플렉서티)
모델                           nats      bpb       ppl
고른 분포 (256)                  5.5452   8.0000    256.00  
고른 분포' (나온 149 종류)           5.0039   7.2192    149.00  
유니그램 (α=1)                   3.0602   4.4150     21.33  
바이그램 (α=1)                   2.2833   3.2940      9.81  앞 글자가 학습 글에 나온 비율 1.0000
바이그램 (α=0.01)                2.2831   3.2938      9.81  

출력의 bpb 칸이 비트로 잰 크로스 엔트로피다(nats 칸과 ppl 칸은 다음 절에서 읽는다). 아무것도 모르는 고른 분포가 글자마다 8비트를 놀라고, 학습 글에 나온 149가지로만 좁혀도 7.22비트로 준다. 글자마다 흔한 정도를 아는 유니그램은 4.42비트, 바로 앞 글자를 보는 바이그램은 3.29비트다. 민준이 「옹알이」라고 부른 바이그램도 고른 분포보다 글자마다 4.7비트를 덜 놀란다. 바이그램에서는 α를 1로 두든 0.01로 두든 3.2940과 3.2938로 거의 같은데, 앞 글자 하나짜리 문맥은 모두 학습 글에서 수백만 번씩 세었기 때문이다.

ML에서: 학습 손실은 nat 로 잰 크로스 엔트로피

파이토치의 F.cross_entropy 가 돌려주는 손실은 위치마다 정답 토큰에 모델이 준 확률의 −ln 을 평균 낸 값이다. 밑이 e 라서 단위가 nat 일 뿐, 이 절의 크로스 엔트로피와 같은 것을 잰다. 학습 글로 재면 학습 손실, 검증 글로 재면 검증 손실이 된다. 파라미터를 막 무작위로 채운 모델은 모든 토큰에 거의 고르게 확률을 주므로, 첫 손실은 고른 분포의 값인 ln V 근처에서 시작한다. 바이트 256개짜리 루나라면 ln 256 = 5.5452다.

문제 1. 비 올 확률 100%

나흘 가운데 사흘은 비가 왔고 하루는 맑았는데, 예보관 셋은 나흘 내내 같은 말을 했다. 갑은 「비 올 확률 50%」, 을은 「90%」, 병은 「100%」. 날마다 실제 날씨에 예보관이 준 확률로 놀란 정도 −log₂ p 를 재서 나흘 평균을 내면, 누가 가장 덜 놀랐는가?

김민준 M01
김민준

갑은 비 오는 날도 맑은 날도 확률 0.5라 날마다 1비트, 평균 1비트예요. 을은 비 오는 사흘이 −log₂ 0.9 = 0.15비트씩이고, 맑은 날이 −log₂ 0.1 = 3.32비트예요. 평균하면 (0.46 + 3.32)/4 = 0.94비트요.

이서연 S01
이서연

병은 비 오는 날 0비트야. 맑은 날은 확률 0을 줬으니까… −log₂ 0이네.

선생님 T02
선생님

−log₂ 0은 얼마예요?

이서연 S06
이서연

무한대요. 사흘을 완벽하게 맞혔는데 하루 틀려서 평균이 무한대예요. 좀 억울한데요.

선생님 T01
선생님

억울해 보여도, 「절대 맑지 않다」고 장담한 예보가 틀린 거예요. 장담을 깬 사건이 얼마나 큰 놀람인지 이 잣대는 봐주지 않아요.

김민준 M07
김민준

그래서 표에 α를 더하는 거구나. 세지 못한 이어짐에 확률 0을 주면 검증 글에서 한 번만 나와도 평균이 무한대가 되니까, 작게라도 남겨 두는 거고요.

문제 2. 「q」 다음에 「u」가 아닌 글자가 오면

학습 글에서 「q」는 27,342번 나왔고 뒤에는 모두 「u」가 왔으며, 어휘는 256가지다. (가) α = 1로 스무딩하면 「q」 다음 「u」에 주는 확률과 놀람은? 「q」 다음에 다른 글자(예: 「a」)가 오면 몇 비트를 놀라는가? (나) α = 0.01이면? (다) 바이그램 전체로는 두 α의 바이트당 비트가 3.2940과 3.2938로 거의 같았다. 두 경우의 놀람이 이렇게 다른데 왜 평균은 거의 같은가?

김민준 M01
김민준

식에 넣어 볼게요. (가) 분모가 27,342 + 256 = 27,598이라 「u」는 27,343/27,598 = 0.9908, 놀람은 0.013비트예요. 「a」는 1/27,598이라 log₂ 27,598 = 14.75비트요.

이서연 S01
이서연

(나)는 분모가 27,342 + 2.56 = 27,344.56이야. 「u」는 0.99991이라 놀람이 0.0001비트쯤이고, 「a」는 0.01/27,344.56이라 21.38비트.

선생님 T01
선생님

α를 줄이면 흔한 글자에는 덜 놀라고, 처음 보는 글자에는 더 놀라네요. 그런데 평균은 왜 거의 같을까요?

이서연 S03
이서연

「q」 다음에 「u」가 아닌 글자가 검증 글에 몇 번이나 나올지가 관건 같아요. 거의 안 나오면 21비트든 14비트든 평균에 섞일 일이 없으니까요.

김민준 M08
김민준

그리고 「u」가 올 때도 0.013비트랑 0.0001비트라 둘 다 거의 0이에요. 행이 수만 번씩 세어져 있으면 α 1이나 0.01은 분모에 비해 너무 작아서 거의 티가 안 나고요.

선생님 T14
선생님

그래요. α가 크게 움직이는 건 몇 번 세지 못한 행이에요. 바이그램에는 그런 행이 거의 없지만, 앞 글자를 길게 보는 표에는 그런 행이 많아져요.

문제 3. 손실 곡선의 이정표

바이트 256개를 어휘로 쓰는 작은 신경망 루나를 학습시킨다. 이 절의 표에 있는 nat 값(고른 분포 5.5452, 유니그램 3.0602, 바이그램 2.2833)을 이정표로 삼아 답하라. (가) 파라미터를 막 무작위로 채운 첫 스텝에는 검증 손실이 얼마 근처여야 하는가? (나) 손실이 3.0602 아래로, 다시 2.2833 아래로 내려가면 루나가 각각 무엇까지 배웠다고 볼 수 있는가? (다) 첫 스텝의 손실이 5.5452보다 훨씬 크게 찍혔다면 어떻게 보아야 하는가?

이서연 S01
이서연

(가)는 고른 분포 근처요. 아직 아무것도 모르면 글자마다 거의 고르게 확률을 줄 테니 ln 256 = 5.5452 언저리겠죠.

김민준 M01
김민준

(나)는 3.0602 아래면 어느 글자가 흔한지는 안 거고, 2.2833 아래면 바로 앞 글자 하나를 보는 표보다 더 잘 맞히는 거니까 앞 글자를 둘 이상 쓰기 시작했다고 봐야겠네요.

선생님 T02
선생님

(다)는요?

김민준 M06
김민준

고른 분포보다 더 놀란 거잖아요. 아무것도 모르는 것보다 못한 건데… 무작위로 채운 모델이 왜 그렇게 돼요?

이서연 S08
이서연

고르게 찍으면 5.5452인데 그보다 크다는 건, 정답이 아닌 글자에 확률을 몰아서 장담했다는 뜻이야. 예보관 병처럼. 처음 점수들이 너무 크게 퍼져 있으면 그렇게 되지 않을까?

선생님 T01
선생님

그럴 수 있어요. 첫 손실이 ln V에서 크게 벗어나면 초기화나 출력 크기부터 의심해 보는 게 실무의 첫 점검이에요.