「先生、曲率の信頼区間を出してみたら、不安定領域が消えました」
( 前回の記事 )
この話は、上記の話の続きです。
研究室のコーヒーブレイクで聞く、MCドロップアウトによる経営環境多様体の不確実性定量化
前回のあらすじ:
博士課程1年の留学生リーム・ストリームが研究室に現れた。彼女は経営環境多様体をデジタルツインにしてWorld Models AI Agentを動かすベンチャーを経営している。リルル先生は「静的パイプラインと動的Agentの弱点は同じ場所にある——因果推論・不確実性・説明可能性だ」と指摘し、3層ロードマップを提示した。今回はその第2層、不確実性の定量化の実装編。
翌週の金曜日。リルル先生の研究室。
テーブルの上に、見慣れないケーキが置かれている。
ユキ: これ何ですか? ふわふわ……。
リーム: ヴィクトリア・スポンジケーキです。イギリスの定番。ラズベリージャムとバタークリームを挟んであります。
リン: (一口食べて)……おいしい。先週のチーズケーキより好きかも。
ユキ: リンさん、自分で持ってきたのに。
リン: だから不本意だったって言ったでしょ。
第1話 「50回回したんですけど」
カイト: 先生、やってきました。MCドロップアウトで曲率の信頼区間を出す実装。
リルル先生: 結果は?
カイト: ……ちょっと困ったことになりました。
リルル先生: 見せて。
カイト: (ノートPCを開く)前回の話の通り、VAEのデコーダにドロップアウト層を入れて、推論時にも model.train() でドロップアウトを有効にしたまま50回推論を回しました。同じ潜在座標 $z$ に対して50回デコーダを通すと、毎回少しずつ異なる出力が得られる。その50回分で引き戻し計量を計算して、さらに曲率まで計算して、平均と標準偏差を出す。
import torch
import torch.nn as nn
import numpy as np
class VAEDecoderWithDropout(nn.Module):
"""ドロップアウト付きVAEデコーダ(MCドロップアウト用)"""
def __init__(self, latent_dim=6, hidden_dim=128, output_dim=30, p_drop=0.1):
super().__init__()
self.net = nn.Sequential(
nn.Linear(latent_dim, hidden_dim // 2),
nn.Tanh(),
nn.Dropout(p=p_drop), # ← ここがポイント
nn.Linear(hidden_dim // 2, hidden_dim),
nn.Tanh(),
nn.Dropout(p=p_drop), # ← ここも
nn.Linear(hidden_dim, output_dim),
)
def forward(self, z):
return self.net(z)
def mc_curvature_with_uncertainty(decoder, z, n_samples=50, d=2):
"""
MCドロップアウトでスカラー曲率の平均と標準偏差を計算する。
Args:
decoder: ドロップアウト付きVAEデコーダ(訓練済み)
z: 潜在空間上の点 (shape: [d])
n_samples: MCサンプル数
d: 潜在空間の次元
Returns:
mean_curvature: スカラー曲率の平均
std_curvature: スカラー曲率の標準偏差
"""
decoder.train() # ドロップアウトを有効にする(推論時も!)
curvatures = []
for _ in range(n_samples):
z_input = z.clone().requires_grad_(True)
# --- 引き戻し計量の計算 ---
f = decoder(z_input)
n_out = f.shape[-1]
J = torch.zeros(n_out, d)
for a in range(n_out):
grad = torch.autograd.grad(
f[a], z_input, retain_graph=True, create_graph=True
)[0]
J[a] = grad
g = J.T @ J # 引き戻し計量 g_{ij} = (J^T J)_{ij}
# --- 計量の偏微分 → クリストッフェル記号 ---
g_inv = torch.inverse(g)
dg = torch.zeros(d, d, d)
for j in range(d):
for l in range(d):
grad_g = torch.autograd.grad(
g[j, l], z_input, retain_graph=True, create_graph=True
)[0]
dg[:, j, l] = grad_g
Gamma = torch.zeros(d, d, d)
for k in range(d):
for i in range(d):
for j in range(d):
s = 0.0
for l in range(d):
s += g_inv[k, l] * (dg[i,j,l] + dg[j,i,l] - dg[l,i,j])
Gamma[k, i, j] = 0.5 * s
# --- 2次元の場合のガウス曲率(簡略版) ---
# 完全な実装ではdΓ/dzをさらにautogradで計算するが、
# ここでは有限差分で近似(MCドロップアウトのデモ目的)
eps = 1e-4
K = compute_gauss_curvature_fd(decoder, z_input.detach(), g, Gamma, d, eps)
curvatures.append(K)
curvatures = np.array(curvatures)
return curvatures.mean(), curvatures.std()
def compute_gauss_curvature_fd(decoder, z, g, Gamma, d, eps):
"""有限差分でガウス曲率を近似(2次元の場合)"""
# R^0_{101} の計算: dΓ^0_{01}/dz^1 - dΓ^0_{11}/dz^0 + ΓΓ - ΓΓ
# 有限差分で dΓ/dz を近似
det_g = torch.det(g).item()
if det_g < 1e-10:
return 0.0
# 簡略化: det(g) の変動からガウス曲率を近似
# K = -1/(2*sqrt(det(g))) * laplacian(log(det(g)))
# ここではMCドロップアウトの「仕組み」を示すのが目的なので
# 完全な曲率計算は省略し、det(g)ベースの近似を使用
log_det_g_center = np.log(max(det_g, 1e-10))
# 有限差分ラプラシアン
laplacian = 0.0
for i in range(d):
z_plus = z.clone()
z_minus = z.clone()
z_plus[i] += eps
z_minus[i] -= eps
det_plus = compute_det_g(decoder, z_plus, d)
det_minus = compute_det_g(decoder, z_minus, d)
log_det_plus = np.log(max(det_plus, 1e-10))
log_det_minus = np.log(max(det_minus, 1e-10))
laplacian += (log_det_plus - 2*log_det_g_center + log_det_minus) / (eps**2)
K = -0.5 / np.sqrt(max(det_g, 1e-10)) * laplacian
return K
def compute_det_g(decoder, z, d):
"""指定点での det(g) を計算"""
z = z.clone().requires_grad_(True)
decoder.eval() # ← ここでは確定的に計算
# 注意: MCの各サンプル内では同じドロップアウトマスクを使うべき
# ここではデモのため簡略化
decoder.train()
f = decoder(z)
n_out = f.shape[-1]
J = torch.zeros(n_out, d)
for a in range(n_out):
grad = torch.autograd.grad(f[a], z, retain_graph=True)[0]
J[a] = grad
g = J.T @ J
return torch.det(g).item()
カイト: で、これを潜在空間の格子点上で一括実行する関数も書きました。
def mc_curvature_map(decoder, grid_points, n_samples=50, d=2):
"""
格子点上でMCドロップアウト付きスカラー曲率マップを生成。
Returns:
means: 各格子点の曲率平均値
stds: 各格子点の曲率標準偏差
lowers: 95%信頼区間の下限(mean - 1.96*std)
uppers: 95%信頼区間の上限(mean + 1.96*std)
"""
means, stds = [], []
for z_np in grid_points:
z = torch.tensor(z_np, dtype=torch.float32)
mean_K, std_K = mc_curvature_with_uncertainty(decoder, z, n_samples, d)
means.append(mean_K)
stds.append(std_K)
means = np.array(means)
stds = np.array(stds)
lowers = means - 1.96 * stds
uppers = means + 1.96 * stds
return means, stds, lowers, uppers
リルル先生: 動いた?
カイト: 動きました。で、結果がこれです。
第2話 「不安定領域が消えた」
カイト: (画面を見せる)
前回までの曲率マップ(点推定版)では、国内市場と新興国市場の境界にスカラー曲率 $-2.3$ の不安定領域がはっきり見えてました。
でもMCドロップアウトで信頼区間を出したら——
格子点 (1.2, -0.8):
曲率平均: -2.3
曲率標準偏差: 1.8
95%信頼区間: [-5.8, +1.3]
カイト: 95%信頼区間が $[-5.8, +1.3]$ で、ゼロを跨いでるんです。つまり、この点が本当に不安定(負の曲率)なのか、実は安定(正の曲率)なのか、50回のサンプルでは判別できない。
ユキ: え……前回「ここが不安定領域です!」って自信満々に言ってたのに?
カイト: そうなんです。不安定領域だと思ってたところの半分くらいが、信頼区間を入れるとゼロを跨いで、**「不安定かどうか分からない」**になった。
(沈黙)
リルル先生: ……これ、すごく良い結果だよ。
カイト: え? 悪い結果じゃないんですか?
リルル先生: 考えてみて。もしこの信頼区間なしに、「曲率 $-2.3$ 、不安定領域です」って経営会議に報告してたら?
カイト: ……不安定領域を避けるために3億円の追加コストをかけて迂回ルートを取ったかもしれない。
リルル先生: でも実際には「不安定かどうか分からない」領域だった。3億円は無駄だった可能性がある。
リーム: ……あるいは逆のケースも。信頼区間が $[-5.8, -1.2]$ で完全に負の領域にあるなら、そこは「確信を持って不安定」と言える。そこを避けるための3億円は正当な投資です。
リルル先生: そう。信頼区間は「いくらの判断に使っていいか」を教えてくれる。
第3話 「50回じゃ足りない」
リン: カイト、50回で標準偏差は収束してるの?
カイト: ……実は、格子点によっては50回で収束してないところがあります。
リルル先生: 何回回した?
カイト: 50回です。
リルル先生: 500回回して。
カイト: えっ。
リルル先生: MCドロップアウトの収束性を確認する方法を教えるよ。サンプル数を10, 20, 50, 100, 200, 500と増やして、標準偏差の値がどう変わるかプロットする。
def check_mc_convergence(decoder, z, sample_counts, d=2):
"""
MCサンプル数を増やしたとき、曲率の標準偏差が収束するか確認。
Args:
sample_counts: [10, 20, 50, 100, 200, 500] など
Returns:
収束プロット用のデータ
"""
results = []
# 最大サンプル数で一気にサンプリング
max_n = max(sample_counts)
decoder.train()
all_curvatures = []
for _ in range(max_n):
z_input = z.clone().requires_grad_(True)
f = decoder(z_input)
n_out = f.shape[-1]
J = torch.zeros(n_out, d)
for a in range(n_out):
grad = torch.autograd.grad(
f[a], z_input, retain_graph=True, create_graph=True
)[0]
J[a] = grad
g = J.T @ J
det_g = torch.det(g).item()
all_curvatures.append(det_g) # 簡略: det(g)で代用
all_curvatures = np.array(all_curvatures)
for n in sample_counts:
subset = all_curvatures[:n]
results.append({
'n_samples': n,
'mean': subset.mean(),
'std': subset.std(),
})
return results
def plot_convergence(results):
"""収束プロットを描画"""
import matplotlib.pyplot as plt
ns = [r['n_samples'] for r in results]
means = [r['mean'] for r in results]
stds = [r['std'] for r in results]
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 左: 平均の収束
axes[0].plot(ns, means, 'b-o')
axes[0].set_xlabel('MC samples')
axes[0].set_ylabel('Mean curvature proxy')
axes[0].set_title('Convergence of mean')
axes[0].set_xscale('log')
# 右: 標準偏差の安定化
axes[1].plot(ns, stds, 'r-o')
axes[1].set_xlabel('MC samples')
axes[1].set_ylabel('Std of curvature proxy')
axes[1].set_title('Stabilization of uncertainty estimate')
axes[1].set_xscale('log')
plt.tight_layout()
plt.savefig('mc_convergence.png', dpi=150)
plt.show()
# 判定
if len(stds) >= 2:
last_two_ratio = abs(stds[-1] - stds[-2]) / max(stds[-2], 1e-10)
if last_two_ratio < 0.05:
print(f"✓ 標準偏差は収束しています(最後の2点の変動: {last_two_ratio:.1%})")
else:
print(f"✗ 標準偏差が未収束です(最後の2点の変動: {last_two_ratio:.1%})。サンプル数を増やしてください。")
カイト: これで収束判定すればいいんですね。
リルル先生: そう。標準偏差の変動が5%以内に収まったら収束と見なしていい。経験的には、6次元くらいの潜在空間なら200〜500回で収束することが多い。
リーム: 計算コストはどのくらいになりますか?
カイト: 格子点が1,000個で、各点500回回すと……50万回のデコーダ推論。
リルル先生: GPUなら数分。CPUだと数十分。一晩バッチで回せば問題ない規模。
第4話 リームの質問——「Agentの世界モデルにも同じことができますか?」
リーム: カイトさんの実装を見ていて思ったのですが、これと同じことを私のAgentの世界モデルにも適用できますか?
リルル先生: どういう意味?
リーム: 私のAgentはNeural ODEで世界モデル $dz/dt = f_\theta(z, u)$ を学習しています。このNeural ODEにもドロップアウトを入れて、MCドロップアウトで軌道の不確実性を定量化できないか、と。
リルル先生: できる。ただし注意点がある。
リーム: 何でしょう。
リルル先生: Neural ODEの場合、ドロップアウトの影響が時間方向に累積する。$t=0$ でのドロップアウトによる微小なズレが、$t=T$ では指数関数的に増幅されることがある。特に曲率が負の領域(不安定領域)では、このズレの増幅が激しい。
リン: それ、面白いですね。曲率が負の領域では、MCドロップアウトの標準偏差が大きくなる。つまり、不安定領域では不確実性も高い。
リルル先生: そう。直感的には当たり前だよね。「不安定な地形の上では、自分がどこにいるかの確信度も下がる」。
リーム: ということは、MCドロップアウトの標準偏差そのものが、一種のリスク指標になる?
リルル先生: いいところに気づいた。曲率マップが「地形の客観的な性質」を表すのに対して、MCドロップアウトの標準偏差マップは「その地形についての我々の知識の不確かさ」を表す。この2つは別物だけど、不安定領域では両方とも悪化する傾向がある。
def uncertainty_as_risk_indicator(means, stds):
"""
曲率の不確実性をリスク指標として分類する。
分類:
確信的安定: 曲率平均 > 0 かつ 信頼区間下限 > 0
確信的不安定: 曲率平均 < 0 かつ 信頼区間上限 < 0
判定不能: 信頼区間がゼロを跨ぐ
"""
lowers = means - 1.96 * stds
uppers = means + 1.96 * stds
classifications = []
for mean, lower, upper in zip(means, lowers, uppers):
if lower > 0:
classifications.append('確信的安定')
elif upper < 0:
classifications.append('確信的不安定')
else:
classifications.append('判定不能')
return classifications
カイト: 「判定不能」がたくさん出たら、それはデータが足りないとか、VAEの訓練が不十分ってことですか?
リルル先生: そう。「判定不能」の多さは、分析全体の信頼性の上限を示す。判定不能が8割だったら、その曲率マップに基づいて意思決定するのは危険。
リーム: 逆に言えば、「判定不能」を減らすために何をすべきかが明確になります。データを増やす、VAEのアーキテクチャを改善する、潜在次元を調整する——全部、「判定不能」の割合を下げる方向に効く。
リルル先生: そういうこと。品質チェックの指標が一つ増えたわけだ。
第5話 「信頼区間付き曲率マップ」の可視化
ユキ: 先生、結局どういう図ができるんですか? 見たい!
リルル先生: カイト、可視化コード書いた?
カイト: 書きました。
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
def plot_curvature_with_uncertainty(
grid_z1, grid_z2, means_2d, stds_2d,
confidence_level=0.95
):
"""
信頼区間付きスカラー曲率マップを3パネルで表示。
左: 曲率平均(従来の曲率マップ)
中: 標準偏差マップ(不確実性の空間分布)
右: 信頼度分類マップ(確信的安定/確信的不安定/判定不能)
"""
z_score = 1.96 if confidence_level == 0.95 else 2.576
lowers_2d = means_2d - z_score * stds_2d
uppers_2d = means_2d + z_score * stds_2d
# 分類マップ: +1=確信的安定, -1=確信的不安定, 0=判定不能
classification_2d = np.zeros_like(means_2d)
classification_2d[lowers_2d > 0] = 1 # 信頼区間が完全に正
classification_2d[uppers_2d < 0] = -1 # 信頼区間が完全に負
# それ以外は0(判定不能)のまま
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# --- 左: 曲率平均マップ ---
im0 = axes[0].contourf(
grid_z1, grid_z2, means_2d,
levels=20, cmap='RdBu_r'
)
axes[0].contour(
grid_z1, grid_z2, means_2d,
levels=[0], colors='black', linewidths=2
)
axes[0].set_title('Scalar Curvature (mean)', fontsize=13)
axes[0].set_xlabel('z¹'); axes[0].set_ylabel('z²')
plt.colorbar(im0, ax=axes[0], label='Scal(z)')
# --- 中: 標準偏差マップ ---
im1 = axes[1].contourf(
grid_z1, grid_z2, stds_2d,
levels=20, cmap='Oranges'
)
axes[1].set_title('Uncertainty (std)', fontsize=13)
axes[1].set_xlabel('z¹'); axes[1].set_ylabel('z²')
plt.colorbar(im1, ax=axes[1], label='σ(Scal)')
# --- 右: 分類マップ ---
cmap_class = mcolors.ListedColormap(['#2166ac', '#f0f0f0', '#b2182b'])
bounds = [-1.5, -0.5, 0.5, 1.5]
norm = mcolors.BoundaryNorm(bounds, cmap_class.N)
im2 = axes[2].imshow(
classification_2d,
extent=[grid_z1.min(), grid_z1.max(), grid_z2.min(), grid_z2.max()],
origin='lower', cmap=cmap_class, norm=norm, aspect='auto'
)
axes[2].set_title('Confidence Classification', fontsize=13)
axes[2].set_xlabel('z¹'); axes[2].set_ylabel('z²')
# カスタム凡例
from matplotlib.patches import Patch
legend_elements = [
Patch(facecolor='#b2182b', label='Confidently Stable'),
Patch(facecolor='#f0f0f0', edgecolor='gray', label='Indeterminate'),
Patch(facecolor='#2166ac', label='Confidently Unstable'),
]
axes[2].legend(handles=legend_elements, loc='lower right', fontsize=9)
plt.suptitle(
f'Curvature Map with {confidence_level*100:.0f}% Confidence Intervals '
f'(MC Dropout, N={n_samples} samples)',
fontsize=14
)
plt.tight_layout()
plt.savefig('curvature_with_uncertainty.png', dpi=150, bbox_inches='tight')
plt.show()
# 統計
total = classification_2d.size
n_stable = (classification_2d == 1).sum()
n_unstable = (classification_2d == -1).sum()
n_indeterminate = (classification_2d == 0).sum()
print(f"\n=== 信頼度分類の統計 ===")
print(f"確信的安定: {n_stable:4d} / {total} ({n_stable/total*100:.1f}%)")
print(f"確信的不安定: {n_unstable:4d} / {total} ({n_unstable/total*100:.1f}%)")
print(f"判定不能: {n_indeterminate:4d} / {total} ({n_indeterminate/total*100:.1f}%)")
if n_indeterminate / total > 0.5:
print("\n⚠️ 警告: 判定不能が50%を超えています。")
print(" データ追加、VAE再訓練、または潜在次元の見直しを検討してください。")
ユキ: 3つの図が並ぶんですね。左が従来の曲率マップ、真ん中が「どこが不確かか」のマップ、右が「確信を持って言えるか」の分類マップ。
カイト: 右の分類マップが一番重要で、赤が「確信を持って安定」、青が「確信を持って不安定」、灰色が「判定不能」です。
リルル先生: 経営者に見せるなら、右の図だけでいい。「赤い領域は安全、青い領域は危険、灰色は判断できない。灰色が多い場合はデータが不足しています」——これで伝わる。
左パネル(Scalar Curvature Mean): 従来の曲率マップ。赤が正の曲率(安定)、青が負の曲率(不安定)。黒の破線がScal=0の等高線です。中心の国内市場領域が安定で、右下に強い不安定領域が見えます。
中パネル(Uncertainty): MCドロップアウトによる標準偏差マップ。データが密な中心部は不確実性が低く(黄色)、周辺部やデータが疎な領域は不確実性が高い(赤)。曲率が急変する境界付近でも不確実性が高くなっています。
右パネル(Confidence Classification): 本記事の核心(ハイライト)の図。赤が「確信的安定」(信頼区間が完全に正)、青が「確信的不安定」(信頼区間が完全に負)、灰色が「判定不能」(信頼区間がゼロを跨ぐ)。統計を見ると**判定不能が90%**を占めており、まさにカイトが「不安定領域が消えた」と驚いた結果が再現されています。リルル先生の言う「これは良い結果——過信に気づけた」がこの図から読み取れます。
ユキ: あ、先生、一つ聞いていいですか。この3つの図の横軸 z^1と縦軸 z^2って、具体的に何なんですか? 売上とか為替レートとか、そういう意味があるんですか?
リルル先生: いい質問。答えは「直接的には、特定のビジネス変数に対応しない」。
ユキ: え? じゃあ何なんですか?
リルル先生: VAEのエンコーダが、30個の経営指標を6次元の潜在空間に圧縮したときの座標だよ。この図では6次元のうち2次元だけを取り出して描いてる。
カイト: つまり、z^1 と z^2はVAEが学習した「本質的な因子」の第1成分と第2成分ってことですね。
リルル先生: そう。主成分分析のPC1、PC2に近い感覚だけど、VAEは非線形な圧縮だから、PC1・PC2より複雑な構造を捉えている。
ユキ: でも、z^1 が何を意味するか分からないと、図を見ても「ふーん」で終わっちゃいません?
リルル先生: 鋭い。実はそれが、4話目でやったSHAP分解の意義なんだよ。z^1 や z^2そのものに名前はつけられないけど、曲率マップの各点で「この曲率を構成している元のビジネス変数の寄与度」をSHAPで分解すれば、「この領域が不安定な 理由は為替変動が40%、原材料価格が30%」と、元の30変数の言葉で説明できる。
リーム: つまり、横軸と縦軸の意味を直接解釈しようとするのではなく、図上の各点の性質を元の変数で説明するのが正しいアプローチですね。
リルル先生: その通り。地図にたとえると、z^1 と z^2は緯度・経度のようなもの。緯度35度・経度139度そのものには意味がないけど、その座標を見れば「ああ、東京だ」と分かる。同じように、潜在座標 (z^1,z^2)=(1.5,-0.8)そのものに意味はないけど、その点の曲率やSHAP値を見れば「ここは為替リスクが高い不安定領域だ」と分かる。
ユキ: なるほど。座標は住所で、大事なのはそこに何があるか。
リルル先生: 完璧な理解。
第6話 「信頼区間がない分析は、天気予報で降水確率を言わないのと同じ」
リーム: 少し整理させてください。前回まで、このパイプラインは曲率マップを「点推定」で出していました。つまり「曲率は $-2.3$ です」としか言っていなかった。
リルル先生: そう。
リーム: MCドロップアウトを入れたことで、「曲率は $-2.3 \pm 1.8$ です。95%信頼区間は $[-5.8, +1.3]$ でゼロを跨いでいるため、この点が不安定かどうかの判定は不確実です」と言えるようになった。
リルル先生: その通り。
リーム: これは……天気予報の話に似ていますね。「明日は雨です」と言うのと「明日の降水確率は70%です」と言うのでは、意思決定の質が全然違います。
リルル先生: いいたとえだ。
リーム: 「雨です」だけなら、傘を持っていくか持っていかないかの二択。でも「70%」なら、折りたたみ傘にするか、予定を変更するか、リスクに応じた判断ができる。
リルル先生: 経営判断も同じ。「不安定領域です」だけなら、「3億円かけて迂回する」か「無視する」の二択。でも「不安定である確率95%、不確実性 $\pm 0.5$」なら「3億円の迂回投資は正当化される」、「不安定である確率60%、不確実性 $\pm 2.0$」なら「まずデータを追加して確信度を上げてから判断する」——リスクに応じた段階的な判断ができる。
ユキ: 信頼区間がないと、全部「やるかやらないか」の白黒になっちゃうんですね。
リルル先生: そう。信頼区間がない分析は、降水確率を言わない天気予報。意思決定に使うには不十分。
第7話 リームのAgent——「不確実な領域では慎重に探索する」
リーム: 先生、先ほどの話をAgentに組み込む方法を考えました。
リルル先生: 聞かせて。
リーム: Agentが多様体上で行動を計画するとき、曲率の信頼区間を使って探索戦略を切り替えるんです。
class UncertaintyAwareAgent:
"""不確実性を考慮したWorld Models Agent(概念実装)"""
def __init__(self, world_model, curvature_map, uncertainty_map):
self.world_model = world_model
self.curvature_map = curvature_map # 曲率平均
self.uncertainty_map = uncertainty_map # 曲率標準偏差
def plan_action(self, current_state, goal_state, n_simulations=1000):
"""
不確実性を考慮した行動計画。
確信的安定領域: 大胆に行動(exploit)
確信的不安定領域: 慎重に回避(avoid)
判定不能領域: 情報収集を優先(explore)
"""
best_action = None
best_score = -float('inf')
for _ in range(n_simulations):
# 候補行動をサンプリング
candidate_action = self.sample_action()
# 世界モデルで軌道をシミュレーション
trajectory = self.world_model.simulate(
current_state, candidate_action
)
# 各軌道点での曲率と不確実性を取得
curvatures = [self.curvature_map(z) for z in trajectory]
uncertainties = [self.uncertainty_map(z) for z in trajectory]
# スコアリング
score = self.compute_score(
trajectory, goal_state, curvatures, uncertainties
)
if score > best_score:
best_score = score
best_action = candidate_action
return best_action
def compute_score(self, trajectory, goal, curvatures, uncertainties):
"""
行動のスコアリング。
= 目標への近さ
- 不安定領域通過のペナルティ
- 判定不能領域通過の情報収集コスト
"""
# 目標への到達度(リーマン距離で測る)
goal_reward = -riemannian_distance(trajectory[-1], goal)
# 不安定領域ペナルティ
instability_penalty = 0
for K, sigma in zip(curvatures, uncertainties):
lower = K - 1.96 * sigma
upper = K + 1.96 * sigma
if upper < 0:
# 確信的不安定 → 大きなペナルティ
instability_penalty += abs(K) * 10.0
elif lower < 0 < upper:
# 判定不能 → 中程度のペナルティ(情報不足のコスト)
instability_penalty += sigma * 3.0
# 確信的安定 → ペナルティなし
return goal_reward - instability_penalty
リーム: ポイントは、「判定不能」領域を不安定とも安定とも扱わないことです。代わりに「情報不足のコスト」として中程度のペナルティを課す。これにより、Agentは判定不能領域を無闇に通過するのではなく、まず情報を集めて判定不能を解消してから通過する行動を選好するようになります。
リン: それ、強化学習の「探索と活用のトレードオフ」と同じ構造ですね。UCB(Upper Confidence Bound)アルゴリズムと似てる。
リーム: はい。不確実性が高い領域の価値を楽観的に見積もる(探索を促進する)か、悲観的に見積もる(安全を優先する)かは、ビジネスの文脈に応じて切り替えます。
リルル先生: 探索企業(スタートアップ)は楽観的に、安定企業(大企業)は悲観的に。
リーム: まさにそうです。この楽観度パラメータを顧客企業のリスク許容度に応じて調整するのが、私のサービスの差別化ポイントの一つです。
第8話 品質チェックポイント
リルル先生: じゃあ、今回の品質チェックポイントを整理しよう。
カイト、書いて。
□ MCドロップアウトの収束チェック
・サンプル数を [50, 100, 200, 500] と増やして、
標準偏差の変動が5%以内に収まるか?
→ 収束していなければサンプル数を増やす。
□ 判定不能の割合
・格子点全体のうち「判定不能」が何%か?
→ 50%超なら、データ追加またはVAE再訓練を検討。
→ 80%超なら、曲率マップに基づく意思決定は控える。
□ 信頼区間幅の妥当性
・信頼区間が極端に広い点(σ > |mean| × 3)が多くないか?
→ 多い場合、ドロップアウト率の調整(p=0.1→0.05)を試す。
□ 既知イベントとの整合性(前回チェックの更新版)
・「確信的不安定」と分類された領域は、
過去の既知危機(リーマンショック等)と一致するか?
→ 一致しなければ、第9章(データ収集)に戻る。
リルル先生: 前回までの品質チェックに「信頼区間の収束」「判定不能の割合」が追加された。これは第2層(信頼性担保)が第1層(多様体構成)のチェック体制を強化してるってこと。
第9話 次回予告
リルル先生: 今日の話をまとめると——
ユキ: MCドロップアウトで曲率に信頼区間をつけたら、「不安定だと思ってた領域の半分が判定不能だった」。これは悪い結果じゃなくて、「どこまで信じていいか」が分かるようになったという良い結果。
リルル先生: 完璧。
カイト: そして、リームさんのAgentは信頼区間を使って「確信があるところでは大胆に、不確かなところでは慎重に」探索する。
リーム: 信頼区間がなければ、Agentは全ての領域を同じ確信度で扱ってしまう。それは危険です。
リルル先生: 次回は第2層の2番目——説明可能性。SHAPで「曲率 $-2.3$ のうち、為替リスクの寄与は40%」を出す。これができると、経営者に「何が危険の原因か」を元のビジネス変数で説明できるようになる。
ユキ: リームさん、来週も来ますか?
リーム: もちろん。顧客企業への報告書を、この「信頼区間付き曲率マップ+SHAP分解」の形式で出したいんです。来週のSHAPの実装は、私にとって直接的にビジネスに使えるものになるはずです。
リルル先生: じゃあ来週の金曜。ケーキは——
リーム: 今度はスコーンにしてみます。クロテッドクリーム付きで。
ユキ: イギリスの研究室、最高。
まとめ(真面目版)
| 論点 | 結論 |
|---|---|
| MCドロップアウトの効果 | 曲率に95%信頼区間が付き、「確信的安定」「確信的不安定」「判定不能」の3分類が可能に |
| 「不安定領域の消失」の意味 | 悪い結果ではない。信頼区間なしの分析が過信だったことの発見 |
| 必要なサンプル数 | 50回では不十分な場合あり。収束判定を行い、200〜500回程度が目安 |
| Agentへの応用 | 信頼区間を使って「確信度に応じた探索戦略」を実現。不確実な領域では慎重に行動 |
| 品質チェックの更新 | 「判定不能の割合」が新たなチェック項目として追加 |
次回予告: 「先生、SHAPで曲率を分解してみたんですけど、Shapley値がマイナスの変数ってどう解釈するんですか?」「曲率を上げてる(安定化してる)変数だよ。つまりそれはリスク要因じゃなくて安定化要因」「えっ、安定化要因も見えるんだ!」—— 説明可能性の実装編。スコーンとクロテッドクリームはリーム持参。
📚 Zenn Book『ジオメトリック インテリジェンス』について
本連載の出発点(起点)になったZenn Book『ジオメトリック インテリジェンス』は、以下をご参照ください。
カイトたちが言及している「経営環境多様体アプローチ」のより厳密な数学的定式化、およびVAEやNeural ODEを用いた全ステップのPython実装(約27万字)については、上記のZenn Bookに完全収録しています。