볼록함수의 쌍대
−log는 볼록함수다. 그리고 그 볼록성 하나에서 KL 발산(두 분포가 얼마나 다른지 재는 양, Σ p log(p/q))이 늘 0 이상이라는 사실이 나온다. 하지만 −log는 하나의 볼록함수일 뿐이다. 볼록함수 자체가 가진 구조를 이해하면, 훨씬 더 많은 것이 보인다.
젠센 부등식: 볼록한 그릇이 주는 부등식
“왜 구슬은 그릇 바닥을 찾는데 물결 모양 바닥에선 엉뚱한 데 멈추나?”
볼록 — 그릇의 기하학
그릇에 구슬을 놓으면 어디서 놓든 같은 바닥으로 굴러간다. 물결 모양 바닥에 놓으면 가장 가까운 골에 멈춘다. 그 골이 가장 깊은 골이라는 보장은 없다.
그릇의 성질을 식으로 쓰면 이렇다. 곡선 위의 두 점을 줄로 이으면, 줄은 곡선보다 아래로 내려가지 않는다.
왼쪽은 곡선의 높이, 오른쪽은 줄의 높이다. 두 번 미분할 수 있는 함수라면 f″(x) ≥ 0 과 같은 말이다. x², eˣ, −log x, x log x는 모두 이 시험을 통과한다. sin x나 x⁴ − 2x²는 통과하지 못한다. 줄이 곡선 아래로 파고드는 구간이 있다.
볼록이 주는 선물은 정확히 이것이다. 골짜기가 여러 개일 수 없다. 국소 최소는 곧 전역 최소다. 구슬이 멈춘 곳이 어디든, 그보다 낮은 곳은 없다.
"답이 하나"까지 말하려면 조건이 두 개 더 필요하다. 상수함수 f(x) = 0 도 볼록이다. 그런데 모든 점이 최소라서 답이 무수히 많다. eˣ 도 볼록이다. 그런데 왼쪽으로 한없이 내려가기만 하고 바닥이 없다. 엄격 볼록(줄이 두 끝점 말고는 곡선에 닿지 않음)이고 최솟값이 존재하면, 그때 최소점이 하나다.
불러오는 중…
파이썬
import numpy as np
funcs = {
"x²": lambda x: x**2,
"eˣ": np.exp,
"x⁴": lambda x: x**4,
"sin x": np.sin,
"x⁴ − 2x²": lambda x: x**4 - 2 * x**2,
}
rng = np.random.default_rng(0)
a, b = rng.uniform(-2, 2, (2, 100_000))
t = rng.uniform(0, 1, 100_000)
for name, f in funcs.items():
gap = (1 - t) * f(a) + t * f(b) - f((1 - t) * a + t * b) # 줄 높이 − 곡선 높이
print(f"{name:9s} 가장 작은 (줄 − 곡선) = {gap.min():+.4f} 볼록? {gap.min() >= -1e-12}")
# x² 가장 작은 (줄 − 곡선) = +0.0000 볼록? True
# eˣ 가장 작은 (줄 − 곡선) = +0.0000 볼록? True
# x⁴ 가장 작은 (줄 − 곡선) = +0.0000 볼록? True
# sin x 가장 작은 (줄 − 곡선) = -0.5128 볼록? False
# x⁴ − 2x² 가장 작은 (줄 − 곡선) = -0.9980 볼록? False
sin x와 x⁴ − 2x²만 음수가 나온다. 줄이 곡선 아래로 들어간 쌍이 있다는 뜻이다.
젠센 부등식 — 평균의 함수 vs 함수의 평균
“왜 들쭉날쭉함은 볼록한 비용을 늘리는가?”
비유 — 누진세
세금 T(소득)은 볼록함수다. 많이 벌수록 한 단위에 붙는 세율이 오른다. 5천만 원까지는 10%, 그 위로는 30%라고 하자.
두 사람이 합쳐 1억을 번다. 반반 5천씩 벌면 세금은 500만 + 500만 = 1,000만 원. 2천과 8천으로 갈리면 200만 + 1,400만 = 1,600만 원. 총소득은 같은데, 고르지 않을수록 세금 총액이 커진다. “두 사람의 세금 평균(800만) ≥ 평균 소득의 세금(500만).” 이것이 젠센 부등식이다.
(「소득을 합쳐 한 사람 이름으로 내면 더 많이 낸다」는 말도 누진세에서 참이지만, 그것은 f(a + b) ≥ f(a) + f(b) 라는 다른 성질(초가법성)이다. 젠센은 합치기가 아니라 고르기에 대한 말이다.)
왜 성립하는가. X가 두 값만 가지면 이것은 앞에서 본 「줄이 곡선 위에 있다」 그 자체다. 값이 여럿이면 줄 대신 여러 점의 무게중심을 쓰면 된다. 무게중심도 곡선 위쪽(볼록한 쪽의 안)에 있다. 두 변의 차이를 젠센 갭이라 부른다. X가 한 값에 몰려 있으면 갭은 0이고, 퍼질수록 커진다.
f(x) = x²이면 갭이 바로 분산이다: E[X²] − (E[X])² = Var(X) ≥ 0. KL 발산이 늘 0 이상이라는 증명도 같은 부등식을 f = −log 에 적용한 것이다.
불러오는 중…
파이썬
import numpy as np
rng = np.random.default_rng(0)
X = rng.normal(3, 2, 1_000_000) # 평균 3, 표준편차 2
for name, f in [("x²", lambda x: x**2), ("e^(x/4)", lambda x: np.exp(x / 4))]:
print(f"{name:8s} f(E[X]) = {f(X.mean()):.4f} E[f(X)] = {f(X).mean():.4f} 갭 = {f(X).mean() - f(X.mean()):.4f}")
print(f"분산 = {X.var():.4f}")
# x² f(E[X]) = 9.0120 E[f(X)] = 13.0174 갭 = 4.0054
# e^(x/4) f(E[X]) = 2.1181 E[f(X)] = 2.4004 갭 = 0.2824
# 분산 = 4.0054
x²의 갭이 분산과 소수 넷째 자리까지 같다(둘 다 4.0054, 표본을 뽑아 온 정규분포 자체의 분산인 모분산은 4).
ML에서: 평균을 먼저 내느냐, 나중에 내느냐
언어 모델을 평가하는 점수인 퍼플렉서티(perplexity)는 토큰 하나당 평균 손실(정답 토큰에 준 확률의 −log)에 exp 를 씌운 값이다. 모델이 다음 토큰을 평균 몇 개의 후보 가운데서 고르는 셈인지를 나타낸다. exp 도 볼록함수이므로, 문서마다 퍼플렉서티를 낸 뒤 평균하는 것과 손실을 먼저 평균한 뒤 exp 를 씌우는 것은 젠센 갭만큼 다르다. 얼마나, 어느 쪽으로 다른지는 아래 문제 3에서 직접 잰다.
수확
“볼록하면 골짜기가 하나다. 바닥이 한 점인지는 엄격 볼록과 최솟값의 존재가 정한다.”
“볼록성 하나가 부등식을 만든다. 정보이론의 부등식들이 여기서 나온다.”
문제 1. 왕복의 평균 속력
60 km 떨어진 도시까지 갈 때는 길이 막혀 시속 30 km 로, 돌아올 때는 시속 90 km 로 달렸다. (가) 왕복의 평균 속력은 얼마인가? (나) 1 km 를 가는 데 걸리는 시간 1/v 는 속력 v 의 볼록함수다. 두 구간의 「1 km당 걸린 시간」의 평균과, 시속 60 km 로 달렸을 때의 1 km당 시간을 견주어라. (다) 갈 때와 올 때 똑같이 한 시간씩 달렸다면 평균 속력은 얼마인가?
함께 풀기

(30 + 90) / 2 = 60 이요. 시속 60 km 예요.

갈 때는 몇 시간 걸렸어요?

60 km 를 시속 30 으로 가니까 2시간, 올 때는 40분이에요. 120 km 를 2시간 40분에 달렸으니까… 시속 45 km 네요. 60 이 아니에요.

그럼 60 은 무엇을 평균한 값이었을까요?

속력을 평균한 값이요. 그런데 이 왕복은 거리를 똑같이 나눴으니까, 똑같은 비중으로 평균해야 하는 건 1 km당 걸린 시간이에요. 1/30 과 1/90 의 평균은 1/45 이고, 시속 60 km 의 1/60 보다 커요. 1/v 가 볼록이니까 「함수의 평균 ≥ 평균의 함수」, 젠센 부등식 그대로예요.

(다)는요?

한 시간씩이면 30 km 와 90 km 를 가서 120 km 를 2시간에 달린 거니까 시속 60 km 예요. 이번에는 속력의 산술평균이 맞아요. 거리를 똑같이 나눴는지 시간을 똑같이 나눴는지가 무엇을 평균할지를 정하네요.

수학 시간에 조화평균은 산술평균보다 크지 않다고 배웠는데, 45 가 30 과 90 의 조화평균이에요. 그 부등식도 젠센의 한 경우였네요.
문제 2. 젠센 갭은 분산이다
f(x) = x² 에 대해 젠센 갭 E[X²] − (E[X])² 가 분산임을 보여라. 평균 3, 표준편차 2 인 정규분포에서 표본 10개를 뽑아 갭을 계산하고 분산과 비교하라. 갭이 0이 되는 것은 언제인가?
함께 풀기

표본 10개로 갭이 2.1977 나왔어요. 그런데 np.var(X, ddof=1) 은 2.4419 예요. 안 맞아요. 젠센 갭이 분산이라는 게 근사인가요?

민준 학생이 갭을 계산할 때 E는 무엇으로 계산했어요?

np.mean 이요. 10으로 나눴어요.

ddof=1 은 무엇으로 나눠요?

9요. 아, 표본분산은 모분산을 추정하려고 n−1로 나누는 거고, 젠센 갭은 "이 10개 점의 분포"에 대한 기댓값이니까 n으로 나눠야 해요. np.var(X) 는 2.1977 로 딱 맞아요.

젠센 부등식은 어떤 분포에도 성립해요. 표본 10개로 만든 경험분포도 그중 하나고요. 갭이 0이 되는 건요?

x² 는 엄격 볼록이라 X가 한 값에 몰려 있을 때만이에요. 분산 0 이랑 같은 말이고요.

조교가 성적 평균 낼 때 결석생까지 분모에 넣었는지 안 넣었는지로 반 전체가 헷갈렸던 적 있어요. 분모가 무엇을 세는지부터 확인해야 하는 거네요.
문제 3. 문서마다의 퍼플렉서티를 평균하면
언어 모델을 토큰 수가 같은 문서 다섯 개로 평가했다. 문서마다 토큰 하나당 평균 손실(정답 토큰에 준 확률의 −log, 단위 nat)은 1.5, 2.0, 2.0, 2.5, 3.0 이었다. 퍼플렉서티는 평균 손실에 exp 를 씌운 값이다. (가) 문서마다 퍼플렉서티를 구해 평균한 값과, 다섯 문서를 한 덩어리로 보고 구한 퍼플렉서티를 견주어라. (나) 둘 가운데 어느 쪽이 언제나 크거나 같은가? 어떤 볼록함수의 젠센 부등식인가? (다) 두 값이 같아지는 것은 언제인가? 위의 젠센 위젯에서 「문제 3 불러오기」를 누르면 다섯 문서의 손실이 점으로 올라온다.
함께 풀기

돌려 봤어요. 문서마다 구하면 4.48, 7.39, 7.39, 12.18, 20.09 라서 평균이 10.31 이고, 한 덩어리로 보면 손실 평균이 2.2 니까 exp(2.2) = 9.03 이에요. 옆 팀은 10.31 로 보고했고 저는 9.03 이에요. 둘 중 하나는 버그죠?

같은 모델, 같은 문서예요. 두 계산에서 달라진 건 무엇이에요?

exp 를 씌운 순서요. 저는 손실을 먼저 평균하고 exp, 옆 팀은 exp 를 먼저 씌우고 평균했어요. exp 가 볼록이니까 f(E[X]) ≤ E[f(X)], 옆 팀 값이 언제나 크거나 같아요. 버그가 아니라 젠센 갭 1.28 이었어요.

아까 왕복 속력 문제랑 같네요. 평균을 먼저 내느냐 나중에 내느냐로 값이 달라졌어요.

(다)는요?

exp 는 엄격 볼록이니까 다섯 문서의 손실이 모두 같을 때만이요. 그런데 이상한 게 있어요. 문서 길이가 다르면 「한 덩어리」 퍼플렉서티는 토큰 수로 무게를 매겨 손실을 평균해야 해요. 그러면 두 계산은 exp 의 순서만이 아니라 무엇에 같은 비중을 주는지도 달라져요.

맞아요. 왕복 문제에서 거리에 같은 비중을 줄지 시간에 줄지가 답을 바꿨던 것과 같아요. 그래서 두 모델의 퍼플렉서티를 비교할 때는 둘이 같은 순서, 같은 비중으로 계산했는지부터 확인해야 해요.