🌐 JP | 🇬🇧 EN

第4章: 実践チュートリアル

材料最適化の完全ワークフロー

📖 読了時間: 25-30分 📊 難易度: 中級 💻 コード例: 8 📝 演習: 2

学習目標

4.1 問題設定:三元合金の最適化

このチュートリアルでは、シミュレートされた三元合金系を最適化します。目標は、目的特性を最大化する組成(x1, x2, x3)を見つけることです。

このチュートリアルについて

実際のロボットシステムにアクセスできないため、既知の数学関数を使用して実験をシミュレートします。実際のアプリケーションでは、ロボットが実際の実験を行い、測定値を返します。

目的関数

シミュレートされた材料特性としてBranin関数を使用します:

$$f(x_1, x_2) = \left(x_2 - \frac{5.1}{4\pi^2}x_1^2 + \frac{5}{\pi}x_1 - 6\right)^2 + 10\left(1 - \frac{1}{8\pi}\right)\cos(x_1) + 10$$

コード例1: 目的関数の定義
import numpy as np

def branin(x1, x2):
    """
    Branin関数 - 材料特性をシミュレート。
    NIMOはデフォルトで最大化するため、符号を反転。
    """
    a = 1
    b = 5.1 / (4 * np.pi**2)
    c = 5 / np.pi
    r = 6
    s = 10
    t = 1 / (8 * np.pi)

    result = a * (x2 - b*x1**2 + c*x1 - r)**2 + s*(1-t)*np.cos(x1) + s
    return -result  # 最大化のために符号反転

# 関数のテスト
print(f"f(0, 0) = {branin(0, 0):.4f}")
print(f"f(5, 5) = {branin(5, 5):.4f}")

4.2 候補ファイルの作成

まず、探索する可能性のあるすべての候補を含むCSVファイルを作成する必要があります。

コード例2: 候補グリッドの生成
import pandas as pd
import numpy as np

# 候補のグリッドを作成
# x1: 0から15(16点)
# x2: 0から15(16点)
x1_values = np.linspace(0, 15, 16)
x2_values = np.linspace(0, 15, 16)

# すべての組み合わせを作成
candidates = []
for x1 in x1_values:
    for x2 in x2_values:
        candidates.append({
            'x1': x1,
            'x2': x2,
            'objective': np.nan  # 未テスト
        })

# DataFrameに変換して保存
df = pd.DataFrame(candidates)
df.to_csv('candidates.csv', index=False)

print(f"{len(df)}個の候補を作成しました")
print(df.head(10))

出力:

256個の候補を作成しました
    x1   x2  objective
0  0.0  0.0        NaN
1  0.0  1.0        NaN
2  0.0  2.0        NaN
3  0.0  3.0        NaN
4  0.0  4.0        NaN
5  0.0  5.0        NaN
6  0.0  6.0        NaN
7  0.0  7.0        NaN
8  0.0  8.0        NaN
9  0.0  9.0        NaN

4.3 最適化ループの実行

NIMOを使用して完全な最適化を実行しましょう:

コード例3: 完全な最適化ループ
import nimo
import pandas as pd
import numpy as np

def branin(x1, x2):
    """シミュレートされた材料特性(最大化のために符号反転)"""
    b = 5.1 / (4 * np.pi**2)
    c = 5 / np.pi
    result = (x2 - b*x1**2 + c*x1 - 6)**2 + 10*(1-1/(8*np.pi))*np.cos(x1) + 10
    return -result

def simulate_experiments(proposals_file, output_file):
    """目的値を計算してロボット実験をシミュレート"""
    df = pd.read_csv(proposals_file)
    df['objective'] = df.apply(lambda row: branin(row['x1'], row['x2']), axis=1)
    df.to_csv(output_file, index=False)
    return df

def update_candidates(candidates_file, results_file):
    """新しい実験結果で候補を更新"""
    candidates = pd.read_csv(candidates_file)
    results = pd.read_csv(results_file)

    for _, result in results.iterrows():
        mask = (candidates['x1'] == result['x1']) & (candidates['x2'] == result['x2'])
        candidates.loc[mask, 'objective'] = result['objective']

    candidates.to_csv(candidates_file, index=False)
    return candidates

# 設定
NUM_CYCLES = 10
PROPOSALS_PER_CYCLE = 3

# 最適化履歴を追跡
history = []

# メイン最適化ループ
for cycle in range(NUM_CYCLES):
    print(f"\n{'='*50}")
    print(f"サイクル {cycle + 1}/{NUM_CYCLES}")
    print('='*50)

    # ステップ1: 候補を選択
    if cycle == 0:
        method = "RE"  # 最初のサイクルはランダム
        print("初期データ収集のためにランダム探索を使用")
    else:
        method = "PHYSBO"  # 以降のサイクルはベイズ最適化
        print("ベイズ最適化を使用")

    nimo.selection(
        method=method,
        input_file="candidates.csv",
        output_file="proposals.csv",
        num_objectives=1,
        num_proposals=PROPOSALS_PER_CYCLE,
        re_seed=42 + cycle if method == "RE" else None,
        physbo_seed=42 if method == "PHYSBO" else None
    )

    # ステップ2: 選択された候補を表示
    proposals = pd.read_csv("proposals.csv")
    print(f"\n選択された候補:")
    print(proposals[['x1', 'x2']])

    # ステップ3: 実験をシミュレート(実際の使用ではロボットが行う)
    results = simulate_experiments("proposals.csv", "results.csv")
    print(f"\n実験結果:")
    print(results)

    # ステップ4: 候補ファイルを更新
    candidates = update_candidates("candidates.csv", "results.csv")

    # 見つかった最良値を追跡
    tested = candidates[candidates['objective'].notna()]
    best_value = tested['objective'].max()
    best_idx = tested['objective'].idxmax()
    best_x1 = tested.loc[best_idx, 'x1']
    best_x2 = tested.loc[best_idx, 'x2']

    history.append({
        'cycle': cycle + 1,
        'best_value': best_value,
        'best_x1': best_x1,
        'best_x2': best_x2,
        'num_tested': len(tested)
    })

    print(f"\n現時点の最良値: f({best_x1:.2f}, {best_x2:.2f}) = {best_value:.4f}")

# 最終サマリーを表示
print("\n" + "="*50)
print("最適化完了")
print("="*50)
print(f"総実験数: {history[-1]['num_tested']}")
print(f"発見した最良値: {history[-1]['best_value']:.4f}")
print(f"最良組成: x1={history[-1]['best_x1']:.2f}, x2={history[-1]['best_x2']:.2f}")

4.4 結果の可視化

最適化の進捗を可視化しましょう:

コード例4: 最適化履歴のプロット
import matplotlib.pyplot as plt

# 履歴をDataFrameに変換
history_df = pd.DataFrame(history)

# サイクルごとの最良値をプロット
plt.figure(figsize=(10, 6))
plt.plot(history_df['cycle'], history_df['best_value'], 'b-o', linewidth=2, markersize=8)
plt.xlabel('サイクル', fontsize=12)
plt.ylabel('最良目的値', fontsize=12)
plt.title('最適化の進捗', fontsize=14)
plt.grid(True, alpha=0.3)
plt.savefig('optimization_history.png', dpi=150, bbox_inches='tight')
plt.show()

print("保存しました: optimization_history.png")
コード例5: サンプリング点の可視化
import matplotlib.pyplot as plt
import numpy as np

# 最終的な候補を読み込み
candidates = pd.read_csv('candidates.csv')
tested = candidates[candidates['objective'].notna()]
untested = candidates[candidates['objective'].isna()]

# 真の関数の等高線図を作成
x1_grid = np.linspace(0, 15, 100)
x2_grid = np.linspace(0, 15, 100)
X1, X2 = np.meshgrid(x1_grid, x2_grid)
Z = np.vectorize(branin)(X1, X2)

plt.figure(figsize=(12, 5))

# 左: サンプリング点付きの等高線
plt.subplot(1, 2, 1)
plt.contourf(X1, X2, Z, levels=20, cmap='viridis')
plt.colorbar(label='目的値')
plt.scatter(tested['x1'], tested['x2'], c='red', s=100, edgecolors='white', label='テスト済み')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('目的関数ランドスケープ上のテスト点')
plt.legend()

# 右: テスト値の分布
plt.subplot(1, 2, 2)
plt.scatter(tested['x1'], tested['x2'], c=tested['objective'], s=100, cmap='viridis', edgecolors='black')
plt.colorbar(label='測定値')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('テスト点での測定値')

plt.tight_layout()
plt.savefig('sampling_visualization.png', dpi=150, bbox_inches='tight')
plt.show()

4.5 アルゴリズムの比較

異なるアルゴリズムの性能を比較しましょう:

コード例6: アルゴリズム比較
import nimo
import pandas as pd
import numpy as np

def run_optimization(method, num_cycles=10, proposals_per_cycle=3, seed=42):
    """指定されたメソッドで最適化を実行し、履歴を返す"""
    # 候補をリセット
    x1_values = np.linspace(0, 15, 16)
    x2_values = np.linspace(0, 15, 16)
    candidates = []
    for x1 in x1_values:
        for x2 in x2_values:
            candidates.append({'x1': x1, 'x2': x2, 'objective': np.nan})
    pd.DataFrame(candidates).to_csv('candidates.csv', index=False)

    history = []
    for cycle in range(num_cycles):
        # 最初のサイクルは常にREを使用
        current_method = "RE" if cycle == 0 else method

        nimo.selection(
            method=current_method,
            input_file="candidates.csv",
            output_file="proposals.csv",
            num_objectives=1,
            num_proposals=proposals_per_cycle,
            re_seed=seed + cycle,
            physbo_seed=seed
        )

        # シミュレートして更新
        simulate_experiments("proposals.csv", "results.csv")
        candidates_df = update_candidates("candidates.csv", "results.csv")

        tested = candidates_df[candidates_df['objective'].notna()]
        best_value = tested['objective'].max()
        history.append({'cycle': cycle + 1, 'best_value': best_value})

    return pd.DataFrame(history)

# 異なるメソッドを比較
methods = ['PHYSBO', 'BLOX', 'RE']
results = {}

for method in methods:
    print(f"{method}を実行中...")
    results[method] = run_optimization(method)

# 比較をプロット
plt.figure(figsize=(10, 6))
for method, history in results.items():
    plt.plot(history['cycle'], history['best_value'], '-o', label=method, linewidth=2)

plt.xlabel('サイクル', fontsize=12)
plt.ylabel('最良目的値', fontsize=12)
plt.title('アルゴリズム比較', fontsize=14)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.savefig('algorithm_comparison.png', dpi=150, bbox_inches='tight')
plt.show()

4.6 NIMOの組み込み可視化の使用

NIMOは組み込みの可視化関数を提供しています:

コード例7: NIMO可視化ツール
import nimo
from nimo.visualization import plot_history, plot_distribution

# 最適化履歴をプロット
fig1 = plot_history(
    input_file="candidates.csv",
    num_objectives=1
)
fig1.savefig('nimo_history.png', dpi=150)

# テスト点の分布をプロット
fig2 = plot_distribution(
    input_file="candidates.csv",
    num_objectives=1
)
fig2.savefig('nimo_distribution.png', dpi=150)

print("保存しました: nimo_history.png, nimo_distribution.png")

4.7 最適化状態の保存と読み込み

コード例8: 最適化の保存と再開
import shutil
import os

def save_checkpoint(checkpoint_name):
    """現在の最適化状態を保存"""
    checkpoint_dir = f"checkpoints/{checkpoint_name}"
    os.makedirs(checkpoint_dir, exist_ok=True)
    shutil.copy("candidates.csv", f"{checkpoint_dir}/candidates.csv")
    print(f"チェックポイントを{checkpoint_dir}に保存しました")

def load_checkpoint(checkpoint_name):
    """チェックポイントから最適化状態を読み込み"""
    checkpoint_dir = f"checkpoints/{checkpoint_name}"
    shutil.copy(f"{checkpoint_dir}/candidates.csv", "candidates.csv")
    print(f"チェックポイントを{checkpoint_dir}から読み込みました")

# 使用例
# 5サイクル後:
save_checkpoint("cycle_5")

# 後で再開するには:
# load_checkpoint("cycle_5")
# サイクル6から最適化を続行...

演習

演習1: 独自の最適化を実行

最適化コードを以下のように変更してください:

  1. 16x16の代わりに20x20グリッドを使用
  2. 10サイクルの代わりに15サイクル実行
  3. 3つの代わりに各サイクル5つの提案を選択

結果を比較してください:高解像度グリッドはより良い解を見つけますか?

演習2: 異なる獲得関数

異なるPHYSBO獲得関数を使用して最適化を3回実行してください:

  1. physbo_score="EI"(Expected Improvement)
  2. physbo_score="PI"(Probability of Improvement)
  3. physbo_score="TS"(Thompson Sampling)

最適化曲線を一緒にプロットしてください。この問題に対してどの獲得関数が最も優れていますか?

まとめ

免責事項

本コンテンツは教育目的で提供されています。NIMOはNIMSによって開発・保守されています。