4장. LLM 을 완성하는 3단계와 Hugging Face
손실과 퍼플렉서티
객관식 시험에서 보기가 몇 개인지를 떠올려 보세요. 보기가 천 개면 찍어도 맞히기 어렵고, 두세 개로 좁힐 수 있으면 꽤 아는 것입니다. 언어모델의 실력을 "다음 토큰을 평균 몇 개 후보 사이에서 헷갈리나"로 나타낸 것이 퍼플렉서티입니다.
왼쪽 카드의 1,000 쯤은 보기 천 개 중에 찍는, 아무것도 모르는 상태입니다. 가운데 10 쯤은 열 개 정도로 좁힌 상태, 오른쪽 1 에 가까우면 거의 확신하는 상태입니다. 다만 1 에 아주 가까우면 글을 외웠을 수도 있으니 검증용 데이터의 값을 봅니다. 여기서 시험의 보기 수가 퍼플렉서티입니다.
손실에서 퍼플렉서티로
3장의 교차 엔트로피 손실을 e 의 지수로 올리면 퍼플렉서티가 됩니다.
| 평균 손실 | 퍼플렉서티 = e^손실 | 뜻 |
|---|---|---|
| 7.0 | 약 1,097 | 어휘 수천 개 가운데 거의 찍는다 |
| 2.3 | 약 10 | 열 개쯤으로 좁혔다 |
| 0.5 | 약 1.6 | 거의 맞힌다 |
import math
for loss in [7.0, 2.3, 0.5]:
print(loss, "→ 퍼플렉서티", round(math.exp(loss), 1))
# 학습 전의 모델은 어휘 전체를 고르게 고른다. 어휘가 3,000개면 손실은 log(3000)
print("어휘 3,000개를 고르게 고를 때의 손실:", round(math.log(3000), 2))손실 7.0, 2.3, 0.5 가 퍼플렉서티 1096.6, 10.0, 1.6 으로 바뀝니다. 학습 전 모델이 어휘 3,000개를 고르게 고르면 손실은 8.01 이고, 퍼플렉서티는 어휘 수인 3,000 이 됩니다. 실습 5에서 학습 전의 퍼플렉서티가 어휘 수 근처에서 출발하는지 확인합니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
math.exp(loss) | e(약 2.718)를 loss 번 곱한 값이다. exp 는 exponential(지수)의 줄임말이다. 손실을 퍼플렉서티로 바꾼다 |
math.log(3000) | exp 를 거꾸로 한 것이다. "e 를 몇 번 곱하면 3,000 이 되나"를 구한다. 퍼플렉서티를 손실로 되돌린다 |
round(값, 1) | 소수 첫째 자리까지 반올림해 찍는다. 값을 보기 좋게 할 뿐이다 |
이 코드가 뜻하는 것
시험 점수를 "틀린 정도"(손실)로 적어 두면 사람이 감을 잡기 어렵습니다. 그래서 "보기 몇 개 중에 찍는 셈인가"(퍼플렉서티)로 바꿔 읽습니다. exp 와 log 는 이 두 말을 서로 옮기는 통역사입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
for loss in [7.0, 2.3, 0.5] | 손실 세 가지를 차례로 꺼낸다 | 못하는 학생, 중간 학생, 잘하는 학생의 점수 |
math.exp(loss) | 손실을 퍼플렉서티로 바꾼다 | "보기 약 1,097개, 10개, 1.6개 중에 찍는 셈" |
math.log(3000) | 어휘 3,000개를 고르게 찍을 때의 손실을 구한다 | 아무것도 모를 때 받는 점수 |
손실이 1 줄 때마다 퍼플렉서티는 약 2.7배씩 줄어듭니다. 그래서 학습 초반에 손실이 8 에서 7 로만 내려가도 헷갈리는 후보 수는 크게 줄어듭니다.
작게 줄여서 눈으로 보기
평균 손실은 "정답 토큰에 모델이 준 확률"에서 나옵니다. 토큰 네 개에 준 확률을 설명용으로 정해 두고, 퍼플렉서티가 정말 "몇 개 중 하나"인지 확인해 봅니다.
import math
# 정답 토큰에 모델이 준 확률(설명용으로 정한 값)
probs = [0.5, 0.25, 0.1, 0.8]
losses = [-math.log(p) for p in probs] # 토큰마다 손실 = -log(확률)
avg = sum(losses) / len(losses) # 평균 손실
print("토큰별 손실:", [round(l, 3) for l in losses])
print("평균 손실:", round(avg, 3))
print("퍼플렉서티:", round(math.exp(avg), 3))
# 견주기: 확률의 역수(1/p, "몇 개 중 하나")를 곱해 네제곱근을 낸 값
inv = [1 / p for p in probs]
print("1/확률:", inv)
print("1/확률의 기하평균:", round(math.prod(inv) ** (1 / len(inv)), 3))토큰별 손실: [0.693, 1.386, 2.303, 0.223]
평균 손실: 1.151
퍼플렉서티: 3.162
1/확률: [2.0, 4.0, 10.0, 1.25]
1/확률의 기하평균: 3.162| 결과 | 읽는 법 |
|---|---|
| 토큰별 손실 | 확률 0.8 로 거의 맞힌 토큰은 손실 0.223 으로 작고, 0.1 밖에 주지 못한 토큰은 2.303 으로 큽니다 |
| 평균 손실, 퍼플렉서티 | 평균 손실 1.151 을 exp 하면 3.162 입니다. 평균 3개쯤의 후보 사이에서 헷갈린다는 뜻입니다 |
| 1/확률 | 확률 0.5 는 "2개 중 하나", 0.25 는 "4개 중 하나", 0.1 은 "10개 중 하나"로 맞힌 셈입니다 |
| 1/확률의 기하평균 | 네 값을 곱해 네제곱근을 낸 평균(기하평균)이 퍼플렉서티와 똑같은 3.162 입니다 |
마지막 두 줄이 같다는 것이 퍼플렉서티를 "평균 몇 개 후보 사이에서 헷갈리나"로 읽어도 되는 까닭입니다. math.prod 는 목록의 값을 모두 곱하는 함수입니다.
주의할 점
- 퍼플렉서티는 같은 토크나이저끼리만 견줄 수 있습니다. 토큰을 잘게 자르는 토크나이저일수록 맞히기 쉬워 숫자가 작게 나옵니다.
- 퍼플렉서티가 낮다고 답이 옳은 것은 아닙니다. 그럴듯하게 이어 쓰는 실력일 뿐이라, 질문에 맞는 답을 하는지는 실습 6처럼 따로 채점합니다.