Chapter

📖 読了時間: 20-25分 📊 難易度: 初級 💻 コード例: 0個 📝 演習問題: 0問

Chapter 3 Quality Enhancements

scikit-optimizeやBoTorchでの最小実装から始め、パラメータ設定の勘所を押さえます。制約や多目的への拡張の入口も示します。

💡 補足: ノイズの見積もりとスケール調整で安定化。反復回数は“少数精鋭”で設計します。

This file contains enhancements to be integrated into chapter-3.md

Code Reproducibility Section (add after section 3.1)

コード再現性の確保

環境設定の重要性:

すべてのコード例は以下の環境で動作確認されています:

# 必須ライブラリのバージョン
"""
Python: 3.8+
numpy: 1.21.0
scikit-learn: 1.0.0
scikit-optimize: 0.9.0
torch: 1.12.0
gpytorch: 1.8.0
botorch: 0.7.0
matplotlib: 3.5.0
pandas: 1.3.0
scipy: 1.7.0
"""

# 再現性確保のための設定
import numpy as np
import torch
import random

# 乱数シードの固定
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
torch.manual_seed(RANDOM_SEED)
random.seed(RANDOM_SEED)

# GPyTorchカーネル設定(推奨)
from gpytorch.kernels import RBF, MaternKernel, ScaleKernel

# RBFカーネル(最も一般的)
kernel_rbf = ScaleKernel(RBF(
    lengthscale_prior=None,  # データ駆動で最適化
    ard_num_dims=None  # Automatic Relevance Determination
))

# Maternカーネル(滑らかさ調整可能)
kernel_matern = ScaleKernel(MaternKernel(
    nu=2.5,  # 滑らかさパラメータ(1.5, 2.5, または inf(RBFと同等))
    ard_num_dims=None
))

print("環境設定完了")
print(f"NumPy version: {np.__version__}")
print(f"PyTorch version: {torch.__version__}")

インストール手順:

# 仮想環境の作成(推奨)
python -m venv bo_env
source bo_env/bin/activate  # Linuxmac
# bo_env\Scripts\activate  # Windows

# 必須パッケージのインストール
pip install numpy==1.21.0 scikit-learn==1.0.0 scikit-optimize==0.9.0
pip install torch==1.12.0 gpytorch==1.8.0 botorch==0.7.0
pip install matplotlib==3.5.0 pandas==1.3.0 scipy==1.7.0

# オプション: Materials Project API
pip install mp-api==0.30.0

# インストール確認
python -c "import botorch; print(f'BoTorch {botorch.__version__} installed')"

Practical Pitfalls Section (add after section 3.7)

3.8 実践的な落とし穴と対処法

落とし穴1: 不適切なカーネル選択

問題: カーネル選択が目的関数の性質と合っていない

症状: - 予測精度が低い - 探索効率が悪い - 局所最適に陥りやすい

解決策:

# カーネル選択ガイド
from gpytorch.kernels import RBF, MaternKernel, PeriodicKernel

def select_kernel(problem_characteristics):
    """
    問題の特性に応じたカーネル選択

    Parameters:
    -----------
    problem_characteristics : dict
        問題の特性を記述する辞書
        - 'smoothness': 'smooth' | 'rough'
        - 'periodicity': True | False
        - 'dimensionality': int

    Returns:
    --------
    kernel : gpytorch.kernels.Kernel
        推奨カーネル
    """
    if problem_characteristics.get('periodicity'):
        # 周期性がある場合
        return PeriodicKernel()

    elif problem_characteristics.get('smoothness') == 'smooth':
        # 滑らかな関数(材料物性など)
        return RBF()

    elif problem_characteristics.get('smoothness') == 'rough':
        # ノイズや不連続性がある
        return MaternKernel(nu=1.5)

    else:
        # デフォルト: Matern 5/2(汎用性高い)
        return MaternKernel(nu=2.5)

# 使用例
problem_specs = {
    'smoothness': 'smooth',
    'periodicity': False,
    'dimensionality': 4
}

recommended_kernel = select_kernel(problem_specs)
print(f"推奨カーネル: {recommended_kernel}")

カーネル比較実験:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, Matern

# テスト関数
def test_function(x):
    """ノイズのある複雑な関数"""
    return np.sin(5*x) + 0.5*np.cos(15*x) + 0.1*np.random.randn(len(x))

# データ生成
np.random.seed(42)
X_train = np.random.uniform(0, 1, 20).reshape(-1, 1)
y_train = test_function(X_train.ravel())

X_test = np.linspace(0, 1, 200).reshape(-1, 1)
y_true = test_function(X_test.ravel())

# 異なるカーネルで比較
kernels = {
    'RBF': RBF(length_scale=0.1),
    'Matern 1.5': Matern(length_scale=0.1, nu=1.5),
    'Matern 2.5': Matern(length_scale=0.1, nu=2.5)
}

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

for ax, (name, kernel) in zip(axes, kernels.items()):
    gp = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=10)
    gp.fit(X_train, y_train)
    y_pred, y_std = gp.predict(X_test, return_std=True)

    ax.scatter(X_train, y_train, c='red', label='訓練データ')
    ax.plot(X_test, y_pred, 'b-', label='予測')
    ax.fill_between(X_test.ravel(), y_pred - 2*y_std, y_pred + 2*y_std,
                     alpha=0.3, color='blue')
    ax.set_title(f'カーネル: {name}')
    ax.legend()
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('kernel_comparison.png', dpi=150)
plt.show()

print("結論:")
print("  RBF: 滑らかな関数に最適")
print("  Matern 1.5: ノイズ耐性が高い")
print("  Matern 2.5: バランスが良い(推奨)")

落とし穴2: 初期化戦略の失敗

問題: 初期サンプリングが探索空間を十分カバーしていない

症状: - 探索が偏る - 重要な領域を見逃す - 収束が遅い

解決策: ラテン超方格サンプリング(LHS)

from scipy.stats.qmc import LatinHypercube

def initialize_with_lhs(n_samples, bounds, seed=42):
    """
    ラテン超方格サンプリングで初期点を生成

    Parameters:
    -----------
    n_samples : int
        サンプル数
    bounds : array (n_dims, 2)
        各次元の [lower, upper] 境界
    seed : int
        乱数シード

    Returns:
    --------
    X_init : array (n_samples, n_dims)
        初期サンプリング点
    """
    bounds = np.array(bounds)
    n_dims = len(bounds)

    # LHSサンプラー
    sampler = LatinHypercube(d=n_dims, seed=seed)
    X_unit = sampler.random(n=n_samples)

    # スケーリング
    X_init = bounds[:, 0] + (bounds[:, 1] - bounds[:, 0]) * X_unit

    return X_init

# 使用例: Li-ion電池組成の初期化
bounds_composition = [
    [0.1, 0.5],  # Li
    [0.1, 0.4],  # Ni
    [0.1, 0.3],  # Co
    [0.0, 0.5]   # Mn
]

X_init_lhs = initialize_with_lhs(
    n_samples=20,
    bounds=bounds_composition,
    seed=42
)

# 組成正規化
X_init_lhs = X_init_lhs / X_init_lhs.sum(axis=1, keepdims=True)

print("LHS初期化完了")
print(f"初期サンプル数: {len(X_init_lhs)}")
print(f"各次元のカバー範囲:")
for i, dim_name in enumerate(['Li', 'Ni', 'Co', 'Mn']):
    print(f"  {dim_name}: [{X_init_lhs[:, i].min():.3f}, "
          f"{X_init_lhs[:, i].max():.3f}]")

# ランダムサンプリングとの比較可視化
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# ランダムサンプリング
np.random.seed(42)
X_random = np.random.uniform(0, 1, (20, 2))

axes[0].scatter(X_random[:, 0], X_random[:, 1], s=100)
axes[0].set_title('ランダムサンプリング')
axes[0].set_xlabel('次元1')
axes[0].set_ylabel('次元2')
axes[0].grid(True, alpha=0.3)

# LHS
axes[1].scatter(X_init_lhs[:, 0], X_init_lhs[:, 1], s=100, c='red')
axes[1].set_title('ラテン超方格サンプリング(LHS)')
axes[1].set_xlabel('次元1 (Li)')
axes[1].set_ylabel('次元2 (Ni)')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('lhs_vs_random.png', dpi=150)
plt.show()

落とし穴3: ノイズのある観測への対応不足

問題: 実験ノイズを考慮していない

症状: - 同じ条件で結果が再現しない - モデルが過学習する - 最適点が不安定

解決策: ノイズを明示的にモデル化

import torch
from botorch.models import SingleTaskGP
from gpytorch.mlls import ExactMarginalLogLikelihood

def fit_gp_with_noise(X, y, noise_variance=0.01):
    """
    ノイズを考慮したガウス過程の学習

    Parameters:
    -----------
    X : Tensor (n, d)
        入力データ
    y : Tensor (n, 1)
        観測値(ノイズ含む)
    noise_variance : float
        観測ノイズの分散(事前知識から設定)

    Returns:
    --------
    gp_model : SingleTaskGP
        学習済みGPモデル
    """
    # ノイズ分散を設定してGP構築
    gp_model = SingleTaskGP(X, y, train_Yvar=torch.full_like(y, noise_variance))

    # 尤度最大化
    mll = ExactMarginalLogLikelihood(gp_model.likelihood, gp_model)
    from botorch.fit import fit_gpytorch_model
    fit_gpytorch_model(mll)

    return gp_model

# 使用例: ノイズのある実験データ
np.random.seed(42)
X_obs = np.random.rand(15, 4)
X_obs = X_obs / X_obs.sum(axis=1, keepdims=True)

# 真の容量 + 実験ノイズ
y_true = 200 + 150 * X_obs[:, 0] + 50 * X_obs[:, 1]
noise = np.random.randn(15) * 10  # 実験ノイズ σ=10 mAh/g
y_obs = y_true + noise

# PyTorchテンソルに変換
X_tensor = torch.tensor(X_obs, dtype=torch.float64)
y_tensor = torch.tensor(y_obs, dtype=torch.float64).unsqueeze(-1)

# ノイズを考慮してGP学習
gp_noisy = fit_gp_with_noise(X_tensor, y_tensor, noise_variance=100.0)

print("ノイズを考慮したGP学習完了")
print(f"観測ノイズ標準偏差: 10 mAh/g")
print(f"モデル化ノイズ分散: 100.0 (mAh/g)²")

ノイズレベルの推定:

def estimate_noise_level(X, y, n_replicates=3):
    """
    複製実験からノイズレベルを推定

    Parameters:
    -----------
    X : array (n, d)
        実験条件
    y : array (n,)
        観測値
    n_replicates : int
        各条件での複製実験数

    Returns:
    --------
    noise_std : float
        推定ノイズ標準偏差
    """
    # 同一条件の複製実験を抽出
    unique_X, indices = np.unique(X, axis=0, return_inverse=True)

    variances = []
    for i in range(len(unique_X)):
        replicates = y[indices == i]
        if len(replicates) >= 2:
            variances.append(np.var(replicates, ddof=1))

    if len(variances) == 0:
        print("警告: 複製実験がありません。デフォルト値を使用")
        return 1.0

    noise_std = np.sqrt(np.mean(variances))
    return noise_std

# 使用例
noise_std_estimated = estimate_noise_level(X_obs, y_obs)
print(f"推定ノイズ標準偏差: {noise_std_estimated:.2f} mAh/g")

落とし穴4: 制約処理の不備

問題: 制約違反を適切に扱っていない

症状: - 実行不可能な材料を提案 - 最適化が収束しない - 無駄な実験が多い

解決策: 制約付き獲得関数

from botorch.acquisition import ConstrainedExpectedImprovement

def constrained_bayesian_optimization_example():
    """
    制約付きベイズ最適化の実装例

    制約:
    1. 組成の合計 = 1.0 (±2%)
    2. Co含量 < 0.3 (コスト制約)
    3. 安定性: formation energy < -1.5 eV/atom
    """
    # 初期データ
    n_initial = 10
    X_init = initialize_with_lhs(n_initial, bounds_composition, seed=42)
    X_init = X_init / X_init.sum(axis=1, keepdims=True)  # 正規化

    # 目的関数と制約の評価
    y_capacity = []
    constraints_satisfied = []

    for x in X_init:
        # 容量予測(目的関数)
        capacity = 200 + 150*x[0] + 50*x[1]
        y_capacity.append(capacity)

        # 制約チェック
        co_constraint = x[2] < 0.3  # Co < 0.3
        stability = -2.0 - 0.5*x[0] - 0.3*x[1]
        stability_constraint = stability < -1.5  # 安定

        all_satisfied = co_constraint and stability_constraint
        constraints_satisfied.append(1.0 if all_satisfied else 0.0)

    X_tensor = torch.tensor(X_init, dtype=torch.float64)
    y_tensor = torch.tensor(y_capacity, dtype=torch.float64).unsqueeze(-1)
    c_tensor = torch.tensor(constraints_satisfied, dtype=torch.float64).unsqueeze(-1)

    # ガウス過程モデル(目的関数)
    gp_objective = SingleTaskGP(X_tensor, y_tensor)
    mll_obj = ExactMarginalLogLikelihood(gp_objective.likelihood, gp_objective)
    from botorch.fit import fit_gpytorch_model
    fit_gpytorch_model(mll_obj)

    # ガウス過程モデル(制約)
    gp_constraint = SingleTaskGP(X_tensor, c_tensor)
    mll_con = ExactMarginalLogLikelihood(gp_constraint.likelihood, gp_constraint)
    fit_gpytorch_model(mll_con)

    # 制約付きEI獲得関数
    best_f = y_tensor.max()
    acq_func = ConstrainedExpectedImprovement(
        model=gp_objective,
        best_f=best_f,
        objective_index=0,
        constraints={0: [None, 0.5]}  # 制約満足確率 > 0.5
    )

    print("制約付きベイズ最適化セットアップ完了")
    print(f"初期実行可能解: {sum(constraints_satisfied)}/{n_initial}")

    return gp_objective, gp_constraint, acq_func

# 実行
gp_obj, gp_con, acq = constrained_bayesian_optimization_example()

End-of-Chapter Checklist (add before "演習問題")

3.9 章末チェックリスト

✅ ガウス過程の理解

確認問題:

Q: RBFカーネルとMaternカーネルの違いは何ですか?
A: RBFは無限回微分可能(非常に滑らか)、Maternはパラメータνで
   滑らかさを調整可能。ノイズがある場合はMatern (ν=2.5) が推奨。

✅ 獲得関数の選択

選択ガイド:

一般的な最適化     → EI (バランス良い)
探索重視の初期     → UCB (κ=2~3)
安全重視          → PI (保守的)
バッチ最適化      → q-EI, q-KG
多目的最適化      → EHVI (Hypervolume)

✅ 多目的最適化

実装チェック:

# 以下を実装できますか?
def is_pareto_optimal(objectives):
    """
    Pareto最適解を判定する関数
    objectives: (n_points, n_objectives)
    """
    # あなたの実装
    pass

# 正解は演習問題3を参照

✅ バッチベイズ最適化

バッチサイズ選択:

実験装置数: n台
→ バッチサイズ: n(最大活用)

計算コスト制約あり
→ バッチサイズ: 3~5(実用的)

探索初期
→ バッチサイズ: 大きめ(多様性重視)

収束期
→ バッチサイズ: 小さめ(精密化)

✅ 制約処理

制約処理チェックリスト:

□ 組成制約(合計=1.0)を正規化で処理
□ 境界制約をboundsパラメータで設定
□ 非線形制約をペナルティ関数で表現
□ 実行可能解が見つからない場合の対処法を準備
□ 制約満足確率を可視化

✅ 実装スキル(GPyTorch/BoTorch)

コード実装確認:

# このコードが理解できますか?
from botorch.models import SingleTaskGP
from botorch.fit import fit_gpytorch_model
from gpytorch.mlls import ExactMarginalLogLikelihood
from botorch.acquisition import ExpectedImprovement
from botorch.optim import optimize_acqf

# GPモデル構築
gp = SingleTaskGP(X_train, y_train)
mll = ExactMarginalLogLikelihood(gp.likelihood, gp)
fit_gpytorch_model(mll)

# EI最大化
EI = ExpectedImprovement(gp, best_f=y_train.max())
candidate, acq_value = optimize_acqf(
    EI, bounds=bounds, q=1, num_restarts=10
)

# 各行の意味を説明できますか?

✅ 実験設計との統合

実験計画テンプレート:

1. 目的設定
   - 最適化する特性: ________
   - 制約条件: ________
   - 実験予算: ________ 回

2. 初期化
   - 初期サンプル数: ________
   - サンプリング法: LHS / Random
   - 予想実験期間: ________

3. 最適化戦略
   - 獲得関数: ________
   - カーネル: ________
   - バッチサイズ: ________

4. 終了条件
   - 最大実験回数: ________
   - 目標性能: ________
   - 改善率閾値: ________

✅ トラブルシューティング

よくあるエラーと対処法:

エラー: "RuntimeError: cholesky_cpu: U(i,i) is zero"
→ 原因: 数値不安定性
→ 対処: GPモデルにjitter追加
   gp = SingleTaskGP(X, y, covar_module=...).double()
   gp.likelihood.noise = 1e-4

エラー: "All points violate constraints"
→ 原因: 制約が厳しすぎる
→ 対処: 段階的制約緩和、初期LHSサンプリング

警告: "Optimization failed to converge"
→ 原因: 獲得関数最適化の失敗
→ 対処: num_restarts増加、raw_samples増加

合格基準

各セクションで80%以上のチェック項目をクリアし、 実装確認コードが理解できれば、次章へ進む準備完了です。

最終確認問題: 1. Li-ion電池正極材料の最適化問題を定式化できますか? 2. 3目的(容量、電圧、安定性)の最適化を実装できますか? 3. 実験回数50回でPareto最適解10個を見つけられますか?

全てYESなら、第4章「アクティブラーニングと実験連携」へ進みましょう!

免責事項