「모델 파라미터 하나당 학습 토큰 20개」는 아마 지금 가장 자주 인용되는 스케일링 규칙일 것이다. 출처는 Chinchilla 논문(arXiv 2203.15556)이고, 그 논문이 최적 배분을 구한 방법 중 하나가 손실 함수를 직접 적합해서 푸는 파라메트릭 모델링(Approach 3)이다. 계수는 공개돼 있다. 그러면 그 계수를 넣고 직접 풀면 20이 나와야 한다.
넣어 봤다. 59.1이 나왔다.
이 글은 그 숫자가 어디서 어긋났는지를 추적한 기록이다. 결론부터 적으면 세 가지다.
- 우리 최적화 코드는 맞다. 같은 계수로 Gopher 예산을 풀면 B가 나오고, 지수도 논문이 보고한 ·와 소수 셋째 자리까지 맞는다.
- 20이라는 값은 Approach 3이 아니라 Approach 2(IsoFLOP)의 성질이다. 논문 부록 표에 두 열이 나란히 인쇄돼 있는데, Approach 2 열의 토큰/파라미터는 19.2~29.2로 거의 평평하고 Approach 3 열은 23.0에서 142.6까지 6.20배 커진다. 표에 이미 답이 적혀 있었다.
- 재현 논문(arXiv 2404.10102)의 재적합 계수를 쓰면 18.4가 나와 20 근처로 돌아온다. 그런데 그 논문이 함께 보고한 표준오차를 그대로 흘리면 Gopher 예산에서 이 비의 95% 구간이 [0.02, 2470]이다. 20도 59도 전부 그 안이다.
덤으로 하나 더 잡혔다. 논문 부록 표의 175B 행에서 인쇄된 FLOPs가 자기 표의 토큰 수와 정확히 10배 어긋난다.
스케일링 법칙이 무엇을 주장하는지는 스케일링 법칙이 맡는다. 이 글은 계수를 넣고 푸는 산수만 맡는다.
파라메트릭 모델
손실 함수
Chinchilla의 파라메트릭 모델은 손실을 파라미터 수 과 토큰 수 의 함수로 놓는다.
는 아무리 키워도 남는 바닥, 나머지 두 항은 각각 모델을 키워서 줄이는 몫과 데이터를 늘려서 줄이는 몫이다. 여기에 학습 연산량을 로 근사하는 제약을 걸고 을 최소화하면 예산 에서의 최적 배분 가 나온다. 6ND 근사는 파라미터 하나를 학습시키는 데 토큰당 6 FLOPs가 든다고 보는 관례적인 어림이다.
해석해
제약을 대입하면 변수가 하나로 줄어든다.
을 풀면 해석해가 나온다.
이 식은 두 가지를 바로 알려 준다. 첫째, 이고 다. 둘째, 토큰/파라미터 비는 라서 가 정확히 0.5일 때만 예산과 무관한 상수가 된다. 이 사실이 이 글의 나머지 전부를 설명한다.
아래에서는 해석해와 scipy 수치해를 둘 다 계산해서 서로 대조한다. 한쪽이 틀리면 상대차가 벌어지므로 최적화 코드 자체의 검산이 된다.
Approach 3 계수
pip install numpy scipy
python optimum.py
import numpy as np
from scipy.optimize import minimize_scalar
# L(N, D) = E + A/N^alpha + B/D^beta 의 계수 세 벌
FITS = {
"Hoffmann A3": dict(E=1.6934, A=406.4, alpha=0.3392, B=410.7, beta=0.2849),
"printed 2dp": dict(E=1.69, A=406.4, alpha=0.34, B=410.7, beta=0.28),
"Besiroglu": dict(E=1.8172, A=482.01, alpha=0.3478, B=2085.43, beta=0.3658),
}
GOPHER = 5.76e23
def numeric(f, C): # C = 6ND 제약 아래 수치 최소화
def L(lnN):
N = np.exp(lnN)
return f["E"] + f["A"] * N ** -f["alpha"] + f["B"] * (6 * N / C) ** f["beta"]
r = minimize_scalar(L, bounds=(np.log(1e6), np.log(1e15)), method="bounded",
options={"xatol": 1e-12})
return np.exp(r.x), r.fun
def closed(f, C): # dL/dN = 0 을 손으로 푼 해
a, b = f["alpha"], f["beta"]
N = (a * f["A"] * C ** b / (b * f["B"] * 6 ** b)) ** (1 / (a + b))
return N, C / (6 * N)
print("=== 1. Gopher 예산 C = 5.76e23 에서의 최적 배분 ===")
for name, f in FITS.items():
Nn, L = numeric(f, GOPHER)
N, D = closed(f, GOPHER)
print(f"{name:12s} N*={N/1e9:7.2f}B D*={D/1e12:5.2f}T D*/N*={D/N:6.1f} L={L:.4f}"
f" 해석해와 수치해의 상대차 {abs(Nn-N)/N:.1e}")
print("\n=== 2. 지수 N* ∝ C^a, D* ∝ C^b ===")
for name, f in FITS.items():
a, b = f["alpha"], f["beta"]
print(f"{name:12s} a = {b/(a+b):.4f} b = {a/(a+b):.4f} D*/N* ∝ C^{1-2*b/(a+b):+.4f}")
print("\n=== 3. 예산을 6자릿수 흔들면 D*/N* 는 상수인가 ===")
print(f"{'C (FLOPs)':>10} |" + "".join(f"{n:>19}" for n in FITS))
for C in [1e21, 1e22, 1e23, 5.76e23, 1e24, 1e25, 1e26, 1e27]:
cells = ""
for f in FITS.values():
N, D = closed(f, C)
cells += f"{N/1e9:12.1f}B{D/N:6.1f}"
print(f"{C:10.2e} |" + cells)
계수 세 벌의 출처는 이렇다. printed 2dp는 원논문 부록 D.2의 식 (10)에 실제로 인쇄된 값이다 — 그 식은 에 , , 로, 지수가 두 자리까지만 적혀 있다. Hoffmann A3의 0.3392·0.2849는 원논문에 없다 — 재현 논문(arXiv 2404.10102) Table 1이 「Hoffmann et al.」 열로 함께 싣는 전체 자릿수값이고, 이 글에서 원논문 계수라고 부르는 것은 그쪽이다. Besiroglu는 같은 표의 재적합 열이다.
실제 출력이다.
=== 1. Gopher 예산 C = 5.76e23 에서의 최적 배분 ===
Hoffmann A3 N*= 40.31B D*= 2.38T D*/N*= 59.1 L=1.9184 해석해와 수치해의 상대차 2.5e-09
printed 2dp N*= 32.19B D*= 2.98T D*/N*= 92.6 L=1.9307 해석해와 수치해의 상대차 2.2e-08
Besiroglu N*= 72.25B D*= 1.33T D*/N*= 18.4 L=1.9744 해석해와 수치해의 상대차 1.2e-07
=== 2. 지수 N* ∝ C^a, D* ∝ C^b ===
Hoffmann A3 a = 0.4565 b = 0.5435 D*/N* ∝ C^+0.0870
printed 2dp a = 0.4516 b = 0.5484 D*/N* ∝ C^+0.0968
Besiroglu a = 0.5126 b = 0.4874 D*/N* ∝ C^-0.0252
=== 3. 예산을 6자릿수 흔들면 D*/N* 는 상수인가 ===
C (FLOPs) | Hoffmann A3 printed 2dp Besiroglu
1.00e+21 | 2.2B 34.0 1.8B 50.1 2.8B 21.6
1.00e+22 | 6.3B 41.5 5.2B 62.6 9.0B 20.4
1.00e+23 | 18.1B 50.7 14.6B 78.2 29.4B 19.2
5.76e+23 | 40.3B 59.1 32.2B 92.6 72.2B 18.4
1.00e+24 | 51.9B 62.0 41.3B 97.7 95.9B 18.1
1.00e+25 | 148.3B 75.7 116.8B 122.1 312.1B 17.1
1.00e+26 | 424.4B 92.5 330.5B 152.6 1015.9B 16.1
1.00e+27 | 1214.2B 113.1 934.9B 190.7 3307.3B 15.2
코드 검산
논문은 Gopher 예산 FLOPs에서 Approach 3의 최적 모델 크기를 40B로 보고한다. 우리가 계수만 넣고 푼 값은 40.31B다. 지수도 논문 Table 2의 Approach 3 행이 , 인데 우리 값이 0.4565와 0.5435다. 해석해와 수치해의 상대차도 아래다. 최적화 코드에는 문제가 없다.
그러니 59.1이라는 값도 계산 실수가 아니다. 같은 코드가 40.31B를 맞혔다.
토큰/파라미터 비
3번 표가 이 글의 핵심이다. 예산을 에서 까지 여섯 자릿수 흔들면 Hoffmann 계수의 토큰/파라미터 비는 34.0에서 113.1까지 3.3배 커진다. 상수가 아니다.
이유는 앞에서 유도한 그대로다. 라서 이고, 지수가 0이 아니면 예산이 커질수록 비가 커진다. 반대로 재적합 계수는 으로 0.5에 훨씬 가깝고 지수가 라 21.6에서 15.2로 거의 움직이지 않는다. 「20토큰/파라미터」가 예산과 무관한 규칙으로 성립하려면 가 0.5여야 한다. 원논문 Approach 3의 계수는 그 조건을 만족하지 않는다.
반올림 민감도
printed 2dp 행이 왜 있는지가 여기서 나온다. 원논문만 읽고 이 계산을 하려는 사람이 손에 쥘 수 있는 계수는 그 행뿐이다.
그 두 자리로 풀면 같은 Gopher 예산에서 가 40.31B에서 32.19B로 내려가고 토큰/파라미터는 59.1에서 92.6으로 뛴다. 소수 넷째 자리 두 개가 결과를 57% 움직인다. 이건 계수가 나쁘다는 뜻이 아니라, 가 급이라 에서 의 미세한 차이가 곱셈으로 증폭된다는 뜻이다. 가 0.0049만 커져도 는 1.3배가 된다.
부록 Table A3
여기서 방향을 바꿨다. 우리가 계수로 푼 값을 논문의 서술과 맞추는 대신, 논문이 부록에 인쇄한 표를 그대로 읽기로 했다. 부록 Table A3은 모델 크기별로 Approach 2와 Approach 3이 각각 예측한 FLOPs와 토큰 수를 두 열로 나란히 싣는다. 두 열의 토큰/파라미터를 직접 나눠 보면 된다.
pip install numpy
python table.py
import numpy as np
# arXiv 2203.15556 Appendix Table A3 을 그대로 옮긴 것 (N, A2 FLOPs, A2 토큰, A3 FLOPs, A3 토큰)
ROWS = [(4e8, 1.84e19, 7.7e9, 2.21e19, 9.2e9), (1e9, 1.20e20, 2.00e10, 1.62e20, 2.71e10),
(1e10, 1.32e22, 2.195e11, 2.46e22, 4.101e11), (6.7e10, 6.88e23, 1.7e12, 1.71e24, 4.1e12),
(1.75e11, 4.54e24, 4.3e12, 1.26e24, 1.20e13), (2.8e11, 1.18e25, 7.1e12, 3.52e25, 2.01e13),
(5.2e11, 4.19e25, 1.34e13, 1.36e26, 4.35e13), (1e12, 1.59e26, 2.65e13, 5.65e26, 9.41e13),
(1e13, 1.75e28, 2.92e14, 8.55e28, 1.4255e15)]
print("=== 논문 표에 이미 적혀 있는 D/N ===")
print(f"{'N':>9} | {'Approach 2':>10} | {'Approach 3':>10}")
for N, _, D2, _, D3 in ROWS:
print(f"{N:9.2e} | {D2/N:10.1f} | {D3/N:10.1f}")
r2 = [D2 / N for N, _, D2, _, _ in ROWS]
r3 = [D3 / N for N, _, _, _, D3 in ROWS]
print(f"{'폭(최대/최소)':>9} | {max(r2)/min(r2):10.2f} | {max(r3)/min(r3):10.2f}")
print("\n=== 표에서 지수를 회귀하면 (175B 행은 아래 감사에서 걸러 제외) ===")
keep = [r for r in ROWS if r[0] != 1.75e11]
for tag, ci, cd in [("Approach 2", 1, 2), ("Approach 3", 3, 4)]:
C = np.array([r[ci] for r in keep]); N = np.array([r[0] for r in keep])
D = np.array([r[cd] for r in keep])
a = np.polyfit(np.log(C), np.log(N), 1)[0]
b = np.polyfit(np.log(C), np.log(D), 1)[0]
print(f"{tag}: a = {a:.4f} b = {b:.4f} -> D/N ∝ C^{b-a:+.4f}")
al, be = 0.3392, 0.2849
print(f"Approach 3 계수 (alpha={al}, beta={be}) 에서 나오는 값: "
f"a = {be/(al+be):.4f} b = {al/(al+be):.4f} -> D/N ∝ C^{1-2*be/(al+be):+.4f}")
print("\n=== 인쇄된 (N, FLOPs, 토큰) 세 값을 6ND 로 대조 ===")
for N, C2, D2, C3, D3 in ROWS:
for tag, C, D in [("A2", C2, D2), ("A3", C3, D3)]:
r = 6 * N * D / C
if not 0.9 < r < 1.1:
print(f" N={N:9.2e} {tag}: 6ND = {6*N*D:9.2e} 인쇄된 FLOPs = {C:9.2e} 비 = {r:.3f}")
print(" (그 밖의 모든 칸은 비가 0.9~1.1 안에 든다)")
=== 논문 표에 이미 적혀 있는 D/N ===
N | Approach 2 | Approach 3
4.00e+08 | 19.2 | 23.0
1.00e+09 | 20.0 | 27.1
1.00e+10 | 21.9 | 41.0
6.70e+10 | 25.4 | 61.2
1.75e+11 | 24.6 | 68.6
2.80e+11 | 25.4 | 71.8
5.20e+11 | 25.8 | 83.7
1.00e+12 | 26.5 | 94.1
1.00e+13 | 29.2 | 142.6
폭(최대/최소) | 1.52 | 6.20
=== 표에서 지수를 회귀하면 (175B 행은 아래 감사에서 걸러 제외) ===
Approach 2: a = 0.4899 b = 0.5101 -> D/N ∝ C^+0.0201
Approach 3: a = 0.4586 b = 0.5410 -> D/N ∝ C^+0.0823
Approach 3 계수 (alpha=0.3392, beta=0.2849) 에서 나오는 값: a = 0.4565 b = 0.5435 -> D/N ∝ C^+0.0870
=== 인쇄된 (N, FLOPs, 토큰) 세 값을 6ND 로 대조 ===
N= 1.75e+11 A3: 6ND = 1.26e+25 인쇄된 FLOPs = 1.26e+24 비 = 10.000
(그 밖의 모든 칸은 비가 0.9~1.1 안에 든다)
두 열의 비
첫 표가 답이다. Approach 2 열은 19.2에서 29.2까지 1.52배 안에서 움직이고, Approach 3 열은 23.0에서 142.6까지 6.20배로 벌어진다. 20 근처에 머무는 것은 IsoFLOP 쪽이다. 파라메트릭 적합 쪽은 67B 행에서 이미 61.2이고, 우리가 같은 계수로 푼 59.1과 거의 같은 자리다.
두 번째 출력은 이 대응을 지수로 확인한다. 논문 표에서 회귀한 Approach 2의 가 0.4899인데 논문 Table 2가 보고한 값이 0.49다. Approach 3은 회귀값 0.4586, 논문 보고값 0.46, 그리고 우리가 계수에서 유도한 값 0.4565다. 셋이 소수 둘째 자리까지 맞는다. 표와 계수와 우리 계산이 같은 것을 가리키고 있다는 확인이다.
가 0.49면 이라 여섯 자릿수를 가도 1.5배 안이고, 0.4586이면 이라 6배가 된다. 표의 두 열이 갈리는 것은 지수 0.03 차이 하나 때문이다.
175B 행 오타
세 번째 출력은 표의 세 값 가 서로 를 만족하는지를 전 칸에 대해 확인한 것이다. 18개 칸 중 17개는 비가 0.959~1.004로 들어오는데 175B 행의 Approach 3만 정확히 10.000이다. 인쇄된 는 같은 행의 토큰 수 12.0조와 맞지 않고, 여야 앞뒤가 맞는다. 지수 하나가 빠진 자리다.
우리 쪽 원인부터 지웠다. 옮겨 적는 실수라면 다른 칸에도 나와야 하는데 나머지 17칸이 전부 4% 안에 든다. 단위 착각이라면 열 전체가 어긋나야 하는데 같은 열의 위아래 행이 멀쩡하다. 10.000이라는 딱 떨어지는 비는 반올림으로 나올 수 있는 모양이 아니다. 그래서 표 쪽의 오타로 본다. 이 행을 회귀에서 뺀 이유이기도 하다.
재적합 계수
재현 논문(arXiv 2404.10102)은 원논문 그림에서 데이터 점을 복원해 같은 함수를 다시 적합했고, 그 계수로는 Gopher 예산에서 18.4가 나온다. 20 근처다. 그런데 이 논문은 원논문과 달리 표준오차를 함께 보고한다. 그 오차를 그대로 흘려 보면 어떻게 되는지가 마지막 실험이다.
pip install numpy
python uncertainty.py
import numpy as np
rng = np.random.default_rng(0)
GOPHER, M = 5.76e23, 200000
# Besiroglu et al. Table 1 재적합: 추정값과 표준오차
SE = dict(A=(482.01, 124.58), alpha=(0.3478, 0.02), B=(2085.43, 1293.23), beta=(0.3658, 0.02))
valid = lambda d: (d["A"] > 0) & (d["B"] > 0) & (d["alpha"] > 0) & (d["beta"] > 0)
def ratio(d, C=GOPHER):
A, a, B, b = d["A"], d["alpha"], d["B"], d["beta"]
N = (a * A * C ** b / (b * B * 6 ** b)) ** (1 / (a + b))
return C / (6 * N) / N
draws = {k: rng.normal(m, s, M) for k, (m, s) in SE.items()}
ok = valid(draws)
r = ratio({k: v[ok] for k, v in draws.items()})
q = np.percentile(r, [2.5, 25, 50, 75, 97.5])
print(f"표본 {M}개 중 네 계수가 모두 양수인 것 {ok.sum()}개 ({100*ok.mean():.1f}%)")
print(f"점추정 D*/N* = {ratio({k: np.array([v[0]]) for k, v in SE.items()})[0]:.2f}")
print(f"중앙값 {q[2]:.2f} IQR [{q[1]:.2f}, {q[3]:.2f}] 95% 구간 [{q[0]:.2f}, {q[4]:.2f}]")
for t in [20, 30, 59.1]:
print(f" D*/N* > {t:4.1f} 일 확률 = {100*(r > t).mean():5.1f}%")
print("\n계수를 하나씩만 흔들었을 때의 95% 구간")
for k in SE:
d = {n: np.full(M, SE[n][0]) for n in SE}
d[k] = draws[k]
m = valid(d)
lo, hi = np.percentile(ratio({n: v[m] for n, v in d.items()}), [2.5, 97.5])
print(f" {k:5s} ±{SE[k][1]:8.2f} -> [{lo:6.2f}, {hi:6.2f}] 폭 {hi-lo:7.2f}")
print("\nHoffmann 계수에서 D*/N* 를 59.1 -> 20 으로 옮기려면 예산이 몇 배여야 하는가")
e = 1 - 2 * 0.2849 / (0.3392 + 0.2849)
print(f" D*/N* ∝ C^{e:+.4f} 이므로 필요한 배수 = (20/59.1)^(1/{e:.4f}) = {(20/59.1)**(1/e):.2e}")
표본 200000개 중 네 계수가 모두 양수인 것 189416개 (94.7%)
점추정 D*/N* = 18.39
중앙값 17.75 IQR [2.71, 101.19] 95% 구간 [0.02, 2470.08]
D*/N* > 20.0 일 확률 = 48.2%
D*/N* > 30.0 일 확률 = 42.1%
D*/N* > 59.1 일 확률 = 32.1%
계수를 하나씩만 흔들었을 때의 95% 구간
A ± 124.58 -> [ 5.82, 133.18] 폭 127.36
alpha ± 0.02 -> [ 1.43, 187.61] 폭 186.18
B ± 1293.23 -> [ 0.04, 175.23] 폭 175.18
beta ± 0.02 -> [ 1.32, 335.51] 폭 334.18
Hoffmann 계수에서 D*/N* 를 59.1 -> 20 으로 옮기려면 예산이 몇 배여야 하는가
D*/N* ∝ C^+0.0870 이므로 필요한 배수 = (20/59.1)^(1/0.0870) = 3.91e-06
오차 전파 구간
점추정은 18.39로 20 근처가 맞다. 그런데 계수를 각자의 표준오차만큼 흔들면 중앙값 17.75에 IQR이 [2.71, 101.19]이고 95% 구간이 [0.02, 2470.08]이다. 원논문 계수가 준 59.1보다 큰 값이 나올 확률이 32.1%다. 다시 말해 재적합 계수의 자기 오차만으로도 20과 59는 서로 구별되지 않는다.
지수의 증폭
무엇이 이 폭을 만드는지는 그다음 출력이 말해 준다. 하나만 흔들어도 구간 폭이 334다. 의 표준오차(1293)가 추정값(2085)의 62%나 되는 것이 눈에 먼저 띄지만, 실제로 폭을 키우는 것은 가 아니라 지수 쪽이다. 앞서 반올림 두 자리가 59.1을 92.6으로 옮긴 것과 같은 메커니즘이다 — 에서 가 이라 의 0.02가 배로 증폭되고, 그것이 다시 제곱으로 에 실린 뒤 비에서 제곱으로 들어간다.
예산 역산
마지막 줄은 반대 방향의 확인이다. Hoffmann 계수를 그대로 두고 예산만 조정해서 59.1을 20으로 끌어내리려면 예산이 지금의 배, 즉 26만분의 1이어야 한다. 예산을 어떻게 세든 이 간극은 안 메워진다. 6ND 근사 대신 다른 FLOPs 회계를 써서 예산이 10% 달라지는 정도로는 비가 1% 남짓 움직일 뿐이다.
꺾이는 지점
공짜와 손해의 경계
논문에서 그대로 가져다 쓸 수 있는 것은 이라는 지수까지다. 여기까지는 세 경로(계수에서 유도, 표에서 회귀, 논문 Table 2 보고값)가 소수 둘째 자리까지 일치하므로 공짜다. 토큰/파라미터 비부터가 손해다 — 같은 계수에서 예산을 여섯 자릿수 옮기면 3.3배, 인쇄된 두 자리로 반올림하면 57%, 재적합의 표준오차를 흘리면 두 자릿수가 움직인다. 「20토큰/파라미터」를 예산과 무관한 상수로 쓰는 순간부터 근거가 없다.
실무 적용
실무로 옮기면 이렇다. 자기 예산이 Gopher 급(~)이 아니라면 20을 그대로 대입하지 말고 위 스크립트에 자기 를 넣어 다시 풀어야 한다. 예산이 작을수록() 재적합 계수도 21.6으로 올라가고, 원논문 계수를 믿으면 34.0이다.
한계
계산만의 검증
전부 산수다. 우리는 모델을 하나도 학습시키지 않았고 손실을 하나도 측정하지 않았다. 이 글이 검증한 것은 「공개된 계수를 논문이 명시한 제약에 넣고 풀면 무엇이 나오는가」뿐이다. 계수 자체가 맞는지, 즉 그 함수가 실제 학습 곡선을 잘 설명하는지는 이 글에서 아무것도 말하지 않는다.
6ND 근사
는 근사다. 어텐션 항과 임베딩 항을 빼고 세는 어림이고, 논문 부록의 더 자세한 FLOPs 회계와는 몇 %에서 십몇 % 정도 차이가 난다. 다만 앞서 봤듯 그 정도 차이는 비를 1% 남짓 움직일 뿐이라 이 글의 결론을 바꾸지 못한다.
계수 상관
표준오차 전파에서 계수 사이의 상관을 무시했다. 재현 논문은 표준오차는 주지만 공분산 행렬은 주지 않는다. 실제로는 와 , 와 가 강하게 상관돼 있을 것이고, 그렇다면 위의 95% 구간 [0.02, 2470]은 실제보다 넓다. 그래서 이 구간을 「진짜 불확실성은 이만큼이다」로 읽으면 안 되고 「보고된 오차만으로 좁혀지지 않는다」로만 읽어야 한다. 상관을 넣으면 얼마나 좁아지는지는 공분산이 공개돼야 답할 수 있다.
Approach 1 제외
Approach 1은 다루지 않았다. 부록 Table A3이 Approach 2와 3만 나란히 싣기 때문이다. Approach 1의 (67B, 1.5조 토큰)만으로는 열 전체의 기울기를 볼 수 없어 표에서 뺐다.
재현 기록
측정 환경
| 항목 | 값 |
|---|---|
| OS | Linux 6.18.5 x86_64 (컨테이너) |
| CPU | Intel Xeon @ 2.80GHz, 4코어 |
| Python | 3.11.15 |
| numpy | 2.4.6 |
| scipy | 1.17.1 |
| 측정일 | 2026-08-19 |
세 스크립트를 각각 5회 반복 실행했다. optimum.py 0.420.47초, 0.22초이고, 다섯 번의 표준출력이 바이트 단위로 전부 동일했다. 몬테카를로에는 table.py 0.09초, uncertainty.py 0.21default_rng(0)으로 시드를 고정했다. 위 시간은 이 컨테이너 값이고 하드웨어가 다르면 바뀐다 — 결론에 쓰는 것은 비율뿐이다.
논문 원문은 ar5iv.labs.arxiv.org/html/2203.15556에서 받았다. 2022년 논문이라 arxiv.org/html/에는 HTML 전문이 없다. 부록 표는 HTML의 해당 <table>을 직접 읽어 옮겼다.
자기검사 수정
초안의 몬테카를로는 B > 0만 걸렀다. 그러면 나 가 음수인 표본에서 음수의 실수 제곱이 되어 nan이 섞이는데, numpy는 RuntimeWarning 한 줄만 찍고 계속 간다. np.percentile이 전부 nan을 반환했고 그 위에서 계산한 (r > 20).mean()은 nan을 False로 세어 48.2%라는 그럴듯한 숫자를 그대로 냈다. 경고를 무시했다면 중앙값 칸만 비어 있고 확률 칸은 멀쩡해 보이는 표를 실었을 것이다. 네 계수 모두에 양수 조건을 걸어 고쳤고, 걸러진 비율(5.3%)을 출력에 남겼다.
또 하나. 처음에는 표의 열 이름을 한글로 두었는데 f-string의 폭 지정은 글자 수로 세고 터미널은 한글을 두 칸으로 그린다. 열이 어긋나 보여 printed 2dp 같은 영문 키로 바꿨다. 숫자는 그대로다.
읽어주셔서 감사합니다. 😊

