노트북은 예측 → 계산 → 대조 세 부분으로 고정한다. 새 개념은 도입하지 않는다. 본문에서 이미 유도한 것을 수치로 확인할 뿐이다.
1. 예측 (코드를 쓰기 전에)¶
아래 칸을 먼저 채운다. 계산하지 않는다.
계산 계획: numpy만. 격자 300점, 다음 기 자본을 격자 위에서 고르는 유한상태 가치함수 반복 — . 보간 없음. . 정지 규칙 . , . 반복 루프를 직접 쌓고 패키지 호출로 답을 내지 않는다.
수렴률 — 연속 갱신량의 비 는 이 커지면 0.96으로 수렴한다. 오차 반감기는 약 17회다. 정지까지 반복 횟수는 약 340회이고, 사후 상계 계수 24로 진짜 오차는 이하다.
예측: ____
민감도 — 으로 바꾸면 정지까지 반복 횟수는 약 배, 130회 근처다. 반감기는 6.6회다.
예측: ____
포락선 조건과 닫힌 해 — 정지 후 (i) 정책 와 의 최대 상대오차는 격자 간격 규모(1–2%); (ii) 과 (eq-w12-17)의 최대 절대 차는 10-3 아래 — 격자 오차가 반복 오차를 지배한다; (iii) 유한차분 와 의 비는 격자 내부에서 , 양 끝에서 어긋난다.
예측: ____
2. 계산¶
패키지 호출로 답을 내지 않는다. 격자·보수 행렬·벨만 연산자·반복 루프를 차례로 직접 쌓는다.
# 도구와 한국어 글꼴, 그리고 이 회차의 파라미터를 쌓는다
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import font_manager
CAND = ['Apple SD Gothic Neo', 'AppleGothic', 'NanumGothic', 'Noto Sans CJK KR', 'Malgun Gothic']
AVAIL = {f.name for f in font_manager.fontManager.ttflist}
FONT = next((f for f in CAND if f in AVAIL), 'DejaVu Sans')
plt.rcParams['font.family'] = FONT
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['mathtext.fontset'] = 'cm'
ALPHA = 0.4 # 자본분배율
BETA = 0.96 # 할인인자 = 압축계수
TOL = 1e-6 # 정지 규칙
NGRID = 300 # 격자점 수
print('글꼴:', FONT)
print(f'alpha={ALPHA}, beta={BETA}, 격자 {NGRID}점, 정지 기준 {TOL:g}')글꼴: Apple SD Gothic Neo
alpha=0.4, beta=0.96, 격자 300점, 정지 기준 1e-06
# 격자 위 소비 c = k_i^alpha - k_j 와 그 효용을 행렬로 쌓는다 (delta=1)
def build(n, lo=0.01, hi=1.0, alpha=ALPHA):
k = np.linspace(lo, hi, n)
y = k ** alpha # 처분가능 자원 y(k) = k^alpha
c = y[:, None] - k[None, :] # (i,j)칸의 소비
u = np.where(c > 0, np.log(np.where(c > 0, c, 1.0)), -np.inf)
return k, u
k300, u300 = build(NGRID)
n_feas = np.isfinite(u300).sum(axis=1)
print(f'격자 간격 = {k300[1] - k300[0]:.5f}')
print(f'실현가능한 선택 수: k=0.010에서 {n_feas[0]}개, k=1.000에서 {n_feas[-1]}개')
print(f'효용의 범위: {u300[np.isfinite(u300)].min():.3f} ~ {u300[np.isfinite(u300)].max():.3f} (유한상태이므로 유계)')격자 간격 = 0.00331
실현가능한 선택 수: k=0.010에서 45개, k=1.000에서 299개
효용의 범위: -11.514 ~ -0.010 (유한상태이므로 유계)
# 벨만 연산자 한 번과 그 반복을 직접 쌓는다
def bellman(V, u, beta):
M = u + beta * V[None, :] # (i,j)칸의 후보값
return M.max(axis=1), M.argmax(axis=1)
def vfi(u, beta, tol=TOL, maxit=20000):
V = np.zeros(u.shape[0]) # V_0 = 0
gaps = []
for _ in range(maxit):
Vn, pol = bellman(V, u, beta)
gaps.append(float(np.max(np.abs(Vn - V)))) # 상한 노름 갱신량
V = Vn
if gaps[-1] < tol:
break
return V, pol, np.array(gaps)
_, _, g1 = vfi(u300, BETA, tol=np.inf, maxit=1) # 첫 갱신 하나만 본다
n_apriori = np.log(TOL * (1 - BETA) / g1[0]) / np.log(BETA)
print(f'한 번 갱신한 뒤 ||TV_0 - V_0|| = {g1[0]:.4f}')
print(f'사전 상계 ||V_0 - V|| <= ||TV_0 - V_0||/(1-beta) = {g1[0] / (1 - BETA):.1f}')
print(f'오차를 {TOL:g} 아래로 보증하는 n = ln(TOL(1-beta)/||TV_0-V_0||)/ln(beta) '
f'= {n_apriori:.1f} -> {np.ceil(n_apriori):.0f}회')한 번 갱신한 뒤 ||TV_0 - V_0|| = 1.9072
사전 상계 ||V_0 - V|| <= ||TV_0 - V_0||/(1-beta) = 47.7
오차를 1e-06 아래로 보증하는 n = ln(TOL(1-beta)/||TV_0-V_0||)/ln(beta) = 433.1 -> 434회
# beta=0.96에서 갱신량의 비·반감기·사후 상계를 쌓는다 (예측 1)
V96, pol96, gaps96 = vfi(u300, BETA)
ratio96 = gaps96[1:] / gaps96[:-1]
half96 = np.log(2) / -np.log(BETA)
post96 = BETA / (1 - BETA) * gaps96[-1]
print(f'[예측 1] 정지까지 반복 = {len(gaps96)}회')
print(f'[예측 1] 갱신비: n=1 {ratio96[0]:.4f} | n=5 {ratio96[4]:.4f} | '
f'n=10 {ratio96[9]:.4f} | n=100 {ratio96[99]:.4f} | 마지막 {ratio96[-1]:.4f}')
print(f'[예측 1] 오차 반감기 = ln2/(-ln beta) = {half96:.2f}회')
print(f'[예측 1] 마지막 갱신량 {gaps96[-1]:.3e}, 사후 상계 계수 {BETA / (1 - BETA):.0f} '
f'-> 진짜 오차 <= {post96:.2e}')
print(f'[예측 1] 사전 상계가 요구하는 n = {np.ceil(n_apriori):.0f}회 vs 실제 정지 {len(gaps96)}회 '
f'— 첫 갱신 하나로 미리 잡은 횟수가 보수적으로 크다')[예측 1] 정지까지 반복 = 343회
[예측 1] 갱신비: n=1 0.7831 | n=5 0.9457 | n=10 0.9598 | n=100 0.9600 | 마지막 0.9600
[예측 1] 오차 반감기 = ln2/(-ln beta) = 16.98회
[예측 1] 마지막 갱신량 9.886e-07, 사후 상계 계수 24 -> 진짜 오차 <= 2.37e-05
[예측 1] 사전 상계가 요구하는 n = 434회 vs 실제 정지 343회 — 첫 갱신 하나로 미리 잡은 횟수가 보수적으로 크다
# 같은 격자에서 beta만 바꿔 반복 횟수를 다시 센다 (예측 2)
BETA2 = 0.90
V90, pol90, gaps90 = vfi(u300, BETA2)
half90 = np.log(2) / -np.log(BETA2)
print(f'[예측 2] beta=0.90 정지까지 반복 = {len(gaps90)}회 (beta=0.96은 {len(gaps96)}회)')
print(f'[예측 2] 횟수 비 = {len(gaps90) / len(gaps96):.3f}, '
f'이론값 ln0.96/ln0.90 = {np.log(0.96) / np.log(0.90):.3f}')
print(f'[예측 2] 반감기 = {half90:.2f}회, 마지막 갱신비 = {gaps90[-1] / gaps90[-2]:.4f}')[예측 2] beta=0.90 정지까지 반복 = 134회 (beta=0.96은 343회)
[예측 2] 횟수 비 = 0.391, 이론값 ln0.96/ln0.90 = 0.387
[예측 2] 반감기 = 6.58회, 마지막 갱신비 = 0.9000
# 닫힌 해 (eq-w12-17)을 쌓아 격자 해와 맞댄다 (예측 3-i, 3-ii)
def closed(k, beta, alpha=ALPHA):
B = alpha / (1 - alpha * beta)
A = (np.log(1 - alpha * beta)
+ alpha * beta / (1 - alpha * beta) * np.log(alpha * beta)) / (1 - beta)
return A + B * np.log(k), (1 - alpha * beta) * k ** alpha, A, B
Vc, cc, A, B = closed(k300, BETA)
c96 = k300 ** ALPHA - k300[pol96] # 격자 정책이 고른 소비
rel96 = np.abs(c96 - cc) / cc
kstar = (ALPHA * BETA) ** (1 / (1 - ALPHA))
print(f'[예측 3-i] 정책 최대 상대오차 = {rel96.max() * 100:.2f}% (격자 간격 {k300[1] - k300[0]:.5f})')
print(f'[예측 3-ii] max|V_n - V| = {np.max(np.abs(V96 - Vc)):.2e} '
f'(A={A:.2f}, B={B:.4f}, ||V||_inf={np.abs(Vc).max():.2f})')
print(f'정상상태 k* = {kstar:.4f}, 닫힌 해의 c*(k*) = {(1 - ALPHA * BETA) * kstar ** ALPHA:.4f}')[예측 3-i] 정책 최대 상대오차 = 1.39% (격자 간격 0.00331)
[예측 3-ii] max|V_n - V| = 3.81e-04 (A=-27.03, B=0.6494, ||V||_inf=30.02)
정상상태 k* = 0.2029, 닫힌 해의 c*(k*) = 0.3254
# 유한차분 V'와 u'(c) g_k 의 비를 쌓는다 (예측 3-iii)
h = k300[1] - k300[0]
dV = np.empty_like(V96) # 유한차분 V'를 직접 쌓는다
dV[1:-1] = (V96[2:] - V96[:-2]) / (k300[2:] - k300[:-2]) # 내부는 중심차분, 오차 O(h^2)
dV[0] = (V96[1] - V96[0]) / h # 양 끝은 한쪽 차분이라 정확도가
dV[-1] = (V96[-1] - V96[-2]) / h # 한 차수 낮다 — 오차 O(h)
env = (1.0 / c96) * ALPHA * k300 ** (ALPHA - 1) # u'(c)*g_k, delta=1이므로 g_k=f'(k)
ratio = dV / env
inner = slice(5, -5)
print(f'[예측 3-iii] 내부(양 끝 5점 제외) 비 범위 = '
f'{ratio[inner].min():.4f} ~ {ratio[inner].max():.4f}')
print(f'[예측 3-iii] 내부에서 |비-1|<0.01인 점의 비율 = {np.mean(np.abs(ratio[inner] - 1) < 0.01) * 100:.1f}%')
print(f'[예측 3-iii] 양 끝: k=0.010에서 {ratio[0]:.3f}, k=1.000에서 {ratio[-1]:.3f}')
off = np.flatnonzero(np.abs(ratio - 1) > 0.01)
print(f'[예측 3-iii] 격자 전체에서 |비-1|>0.01인 점 = {off.size}개, '
f'k = {k300[off].min():.3f}~{k300[off].max():.3f}')
i_s = int(np.argmin(np.abs(k300 - kstar)))
print(f'정상상태 근처 k={k300[i_s]:.4f}: V\'={dV[i_s]:.3f}, u\'(c)g_k={env[i_s]:.3f}, '
f'닫힌 해 B/k={B / k300[i_s]:.3f}')[예측 3-iii] 내부(양 끝 5점 제외) 비 범위 = 0.9930 ~ 1.0136
[예측 3-iii] 내부에서 |비-1|<0.01인 점의 비율 = 99.7%
[예측 3-iii] 양 끝: k=0.010에서 0.875, k=1.000에서 1.002
[예측 3-iii] 격자 전체에서 |비-1|>0.01인 점 = 4개, k = 0.010~0.030
정상상태 근처 k=0.2020: V'=3.209, u'(c)g_k=3.209, 닫힌 해 B/k=3.214
# 격자를 600점으로 늘려 격자 오차와 반복 오차를 갈라 본다
k600, u600 = build(600)
V600, pol600, gaps600 = vfi(u600, BETA)
Vc600, cc600, _, _ = closed(k600, BETA)
c600 = k600 ** ALPHA - k600[pol600]
print('격자 | 반복 | max|V_n-V| | 정책 최대 상대오차 | 마지막 갱신량')
print(f'{300:4d} | {len(gaps96):4d} | {np.max(np.abs(V96 - Vc)):10.2e} | '
f'{np.max(rel96) * 100:16.2f}% | {gaps96[-1]:.2e}')
print(f'{600:4d} | {len(gaps600):4d} | {np.max(np.abs(V600 - Vc600)):10.2e} | '
f'{np.max(np.abs(c600 - cc600) / cc600) * 100:16.2f}% | {gaps600[-1]:.2e}')
print('반복 횟수는 격자가 아니라 beta의 것이고, 격자를 늘리면 격자 오차만 준다')격자 | 반복 | max|V_n-V| | 정책 최대 상대오차 | 마지막 갱신량
300 | 343 | 3.81e-04 | 1.39% | 9.89e-07
600 | 343 | 7.63e-05 | 0.70% | 9.89e-07
반복 횟수는 격자가 아니라 beta의 것이고, 격자를 늘리면 격자 오차만 준다
# 갱신량의 감쇠를 semi-log로 그린다
fig, ax = plt.subplots(figsize=(7.2, 4.2))
ax.semilogy(np.arange(1, len(gaps96) + 1), gaps96, color='C0', label=r'$\beta=0.96$')
ax.semilogy(np.arange(1, len(gaps90) + 1), gaps90, color='C1', label=r'$\beta=0.90$')
ax.axvline(half96, color='C0', ls=':', lw=1)
ax.axvline(half90, color='C1', ls=':', lw=1)
ax.axhline(TOL, color='gray', ls='--', lw=0.8)
ax.set_yticks([1e0, 1e-2, 1e-4, 1e-6])
ax.set_yticklabels([r'$10^{0}$', r'$10^{-2}$', r'$10^{-4}$', r'$10^{-6}$'])
ax.text(half96 + 4, 3e-6, f'반감기 {half96:.1f}회', color='C0', fontsize=9)
ax.text(half90 + 4, 1e-6, f'반감기 {half90:.1f}회', color='C1', fontsize=9)
ax.set_xlabel('반복 횟수 n')
ax.set_ylabel(r'$\|V_{n+1}-V_n\|_\infty$')
ax.set_title(r'갱신량의 기하급수 감쇠 — semi-log 기울기는 $\ln\beta$')
ax.legend()
fig.tight_layout()
plt.show()
# 가치함수·정책·포락선 비를 한 줄에 그린다
fig, axes = plt.subplots(1, 3, figsize=(12.6, 3.8))
axes[0].plot(k300, V96, color='C0', lw=2, label=r'격자 해 $V_n$')
axes[0].plot(k300, Vc, 'r--', lw=1, label=r'닫힌 해 $A+B\ln k$')
axes[0].set_xlabel('$k$')
axes[0].set_ylabel('가치 [효용]')
axes[0].legend()
axes[1].plot(k300, c96, color='C0', lw=2, label=r'격자 정책 $c_n$')
axes[1].plot(k300, cc, 'r--', lw=1, label=r'$(1-\alpha\beta)k^{\alpha}$')
axes[1].set_xlabel('$k$')
axes[1].set_ylabel('소비 [수량]')
axes[1].legend()
axes[2].plot(k300, ratio, color='C0', lw=1)
axes[2].axhline(1.0, color='r', ls='--', lw=1, label='비 = 1')
axes[2].axhspan(0.99, 1.01, color='gray', alpha=0.2, label=r'$\pm1\%$')
axes[2].set_ylim(0.85, 1.06)
axes[2].set_xlabel('$k$')
axes[2].set_ylabel(r"$V'/(u'(c)\,g_k)$")
axes[2].set_title('포락선 비', fontsize=10)
axes[2].legend(fontsize=8)
fig.tight_layout()
plt.show()
# 예측 3항목에 대응하는 수치를 한 곳에 모은다
print(f'예측 1 | 갱신비 -> {ratio96[-1]:.4f} | 반감기 {half96:.2f}회 | '
f'정지 {len(gaps96)}회 | 진짜 오차 <= {post96:.2e}')
print(f'예측 2 | beta=0.90 정지 {len(gaps90)}회 | 횟수 비 {len(gaps90) / len(gaps96):.3f} | '
f'반감기 {half90:.2f}회')
print(f'예측 3 | 정책 최대 상대오차 {rel96.max() * 100:.2f}% | '
f'max|V_n-V| {np.max(np.abs(V96 - Vc)):.2e} | '
f'내부 포락선 비 {ratio[inner].min():.3f}~{ratio[inner].max():.3f} | '
f'양 끝 왼쪽 {ratio[0]:.3f} / 오른쪽 {ratio[-1]:.3f}')예측 1 | 갱신비 -> 0.9600 | 반감기 16.98회 | 정지 343회 | 진짜 오차 <= 2.37e-05
예측 2 | beta=0.90 정지 134회 | 횟수 비 0.391 | 반감기 6.58회
예측 3 | 정책 최대 상대오차 1.39% | max|V_n-V| 3.81e-04 | 내부 포락선 비 0.993~1.014 | 양 끝 왼쪽 0.875 / 오른쪽 1.002
3. 대조¶
예측과 계산이 어긋난 지점을 적는다. 어느 쪽이 틀렸는지 판정한다.
| 예측 | 결과 | 어긋남 | 원인 |
|---|---|---|---|
본문 확인: 어긋났으면 (eq-w12-12)·(eq-w12-15)·(eq-w12-17)로 돌아간다.