マテリアルズインフォマティクス(MI)入門⑨【ベイズ最適化による効率的材料探索】

マテリアルズインフォマティクス(MI)入門⑨【ベイズ最適化による効率的材料探索】

前回の第8回では、予測モデルを「探索エンジン」として利用し、数万件もの仮想実験を行う「ハイスループット・バーチャルスクリーニング(HTVS)」に挑戦しました。力まかせのランダム探索というアプローチではありましたが、私たちは既存のチャンピオンデータを超える、優れた新規材料候補を発見することに成功しました。

しかし、その成功の裏で、膨大な数の「ハズレ」の候補を計算していたことも事実です。もし、AIがより賢く、有望な領域に的を絞って探索してくれたなら、もっと効率的に、あるいはさらなる高みへと到達できるのではないでしょうか?

そこで第9回となる今回は、この「探索の効率」を劇的に向上させるための強力な武器、「ベイズ最適化(Bayesian Optimization)」を導入します。ベイズ最適化は、単なる闇雲な探索ではありません。これまでの探索結果から「次にどこを探索すれば、最も良い結果が得られそうか」を統計的に推論し、「活用(既知の有望領域の深掘り)」「探索(未知の領域への挑戦)」のバランスを自律的に取りながら、効率的に最適解に迫る洗練された手法です。

今回は、このベイズ最適化を実装するための強力なフレームワークOptunaを用い、前回のランダム探索とは一線を画す「賢い探索」を実践します。これにより、望ましい物性(出力)から、それを実現する材料組成(入力)を探索する、材料科学における究極の目標の一つである「逆問題」へのアプローチを体験しましょう。

動作検証済み環境

Google Colaboratory
Python 3.11.13
matminer==0.9.3
pandas==2.2.2
scikit-learn==1.6.1
matplotlib==3.10.0
catboost==1.2.8
optuna==3.6.1

この記事から学べること

  • ベイズ最適化の概念: 代理モデルと獲得関数を使い、「活用」と「探索」のバランスを取りながら効率的に最適解を探す仕組みを理解します。
  • 効率的な材料探索の実践: 闇雲な探索ではなく、AIが自律的に有望な候補を選択するプロセスを体験し、その効率性をランダム探索と比較します。
  • Optunaによる多目的ベイズ最適化: 強力な最適化ライブラリであるOptunaを使い、複数の物性(降伏強度と引張強さ)を同時に最大化する、より実践的な材料探索を簡単に実装する方法を学びます。

関連理論の解説

1. ベイズ最適化:賢い探索のメカニズム

ベイズ最適化は、主に2つの要素から構成される反復的なプロセスです。

  • 代理モデル (Surrogate Model): 実際の実験や複雑なシミュレーションの代わりとなる、軽量な予測モデルです。今回のケースでは、前回と同様に全データで学習させたCatBoostモデルがこれに該当します。このモデルは、与えられた組成に対して物性値を高速に予測します。
  • 獲得関数 (Acquisition Function): ベイズ最適化の「頭脳」にあたる部分です。代理モデルの予測結果(この組成は性能が高そうだ、という予測)とその不確かさ(この組成はデータが無く、よく分からない、という予測)の両方を考慮し、「次に評価すべき最も価値のある点」を決定します。これにより、単に性能が高いと予測される点ばかりを攻める(活用)だけでなく、モデルがまだよく分かっていないが大きなポテンシャルを秘めているかもしれない領域を試す(探索)という、戦略的な探索が可能になります。

この「代理モデルで予測 → 獲得関数で次の候補を決定」というサイクルを繰り返すことで、ベイズ最適化は評価回数を最小限に抑えながら、効率的に最適解へと収束していきます。

2. Optuna:最先端の最適化フレームワーク

Optunaは、ハイパーパラメータ最適化のために開発された、非常に強力で柔軟なPythonライブラリです。ベイズ最適化のような複雑なアルゴリズムも、数行のコードで直感的に実装できます。

  • Study: 最適化のタスク全体を管理するオブジェクトです。「どの物性を最大化(または最小化)したいか」といった目的を定義します。
  • Trial: 1回の試行(ある組成を評価するプロセス)を表すオブジェクトです。探索空間内で、次に試すべきパラメータの値を提案する役割を担います。
  • Sampler: どのアルゴリズムで次のパラメータを提案するかを決定します。今回は多目的最適化に対応したMOTPESamplerを使用し、パレートフロントを効率的に探索します。

実装方法

ワークフロー

前回の局所的HTVSのワークフローをベースに、探索部分をOptunaによるベイズ最適化に置き換えます。

  1. 環境構築:必要なライブラリのインストール
  2. 必要なライブラリのインポート
  3. matminerから実データセットの読み込み
  4. 全データの中からチャンピオン材料を特定
  5. 全データを用いて代理モデルを訓練
  6. 【NEW】Optunaによる局所的ベイズ最適化の実行
  7. 【NEW】探索結果の可視化とチャンピオン超え候補の特定

実行手順

  • 以下のコードブロック全体をコピーします。
  • Google Colaboratoryの新しいセルに貼り付けます。
  • セルが選択されていることを確認し、Shift キーと Enter キーを同時に押してコードを実行します。
# ===================================================================
# 0. 環境構築:必要なライブラリのインストール
# ===================================================================
!pip install matminer==0.9.3 scikit-learn==1.6.1 pandas==2.2.2
!pip install matplotlib==3.10.0 catboost==1.2.8
!pip install optuna==3.6.1
# ===================================================================
# 1. 必要なライブラリのインポート
# ===================================================================
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import optuna
from catboost import CatBoostRegressor
from matminer.datasets import load_dataset
# ===================================================================
# 2. matminerから実データセットの読み込み
# ===================================================================
print("ステップ2: matminerから実データセットを読み込み、前処理します...")
df = load_dataset("steel_strength")
df_clean = df.dropna().reset_index(drop=True)
print(f"データ数: {len(df_clean)}")
features = [ col for col in df_clean.columns if col not in [ "composition", "formula", "yield strength", "tensile strength", "elongation", ]
]
targets = ["yield strength", "tensile strength"]
X = df_clean[features]
y = df_clean[targets]
print("データ準備完了。")
# ===================================================================
# 3. 全データの中からチャンピオン材料を特定
# ===================================================================
print("\nステップ3: 全データの中からチャンピオン材料を特定します...")
y_sum = y.sum(axis=1)
champion_index = y_sum.idxmax()
champion_composition = X.loc[champion_index]
champion_properties = y.loc[champion_index]
print("\n【チャンピオン材料の組成と物性】")
print( f"降伏強度: {champion_properties['yield strength']} MPa" f"引張強さ: {champion_properties['tensile strength']} MPa"
)
# ===================================================================
# 4. 全データを用いて代理モデルを訓練
# ===================================================================
print("\nステップ4: 全データを用いて代理モデルを学習させます...")
model = CatBoostRegressor(random_seed=42, verbose=0, loss_function="MultiRMSE")
model.fit(X, y)
print("全データでのモデル学習が完了しました。")
# ===================================================================
# 5. チャンピオン材料周辺に探索空間を定義
# ===================================================================
print("\nステップ5: チャンピオン材料周辺の局所的な探索空間を定義します...")
# 前回より少し広い ±20% の範囲を探索
search_range_ratio = 0.20
comp_min = champion_composition * (1 - search_range_ratio)
comp_max = champion_composition * (1 + search_range_ratio)
comp_min[champion_composition == 0] = 0
comp_max[champion_composition == 0] = 0
# ===================================================================
# 6. 【NEW】Optunaによる局所的ベイズ最適化の実行
# ===================================================================
N_TRIALS = 500
print(f"\nステップ6: 局所探索ベイズ最適化を開始します... (試行回数: {N_TRIALS})")
def objective(trial): # 各元素の組成を探索空間内で提案させる composition = {} for element in features: composition[element] = trial.suggest_float( element, comp_min[element], comp_max[element] ) # 提案された組成で物性を予測 composition_df = pd.DataFrame([composition], columns=features) predicted_properties = model.predict(composition_df) # 2つの目的変数(物性値)を返す return predicted_properties[0, 0], predicted_properties[0, 1]
# 多目的最適化のStudyを作成(2つの目的を両方最大化)
study = optuna.create_study( directions=["maximize", "maximize"], sampler=optuna.samplers.MOTPESampler(seed=42)
)
study.optimize(objective, n_trials=N_TRIALS, show_progress_bar=True)
print("局所探索が完了しました。")
# ===================================================================
# 7. 【NEW】探索結果の可視化とチャンピオン超え候補の特定
# ===================================================================
print("\nステップ7: 探索結果を可視化します...")
# Optunaが自動で見つけたパレートフロント(非支配解)を取得
best_trials = study.best_trials
# チャンピオンの物性を超えた候補を特定
better_candidates_bo = [ t for t in best_trials if ( t.values[0] >= champion_properties["yield strength"] and t.values[1] > champion_properties["tensile strength"] ) or ( t.values[0] > champion_properties["yield strength"] and t.values[1] >= champion_properties["tensile strength"] )
]
print( f"\nベイズ最適化により、チャンピオンを超える新規候補を {len(better_candidates_bo)} 個発見。"
)
# 可視化
plt.figure(figsize=(12, 10))
# 全学習データを背景としてプロット
plt.scatter( y["tensile strength"], y["yield strength"], alpha=0.1, c="gray", label="All Training Data",
)
# ベイズ最適化の全探索点をプロット
all_bo_trials = np.array([t.values for t in study.trials])
plt.scatter( all_bo_trials[:, 1], all_bo_trials[:, 0], c="lightgreen", s=100, marker="o", alpha=0.3, label=f"All BO Candidates ({N_TRIALS} trials)",
)
# チャンピオン材料の点をプロット
plt.scatter( champion_properties["tensile strength"], champion_properties["yield strength"], c="blue", s=250, marker="o", edgecolors="white", zorder=10, label="Original Champion",
)
# チャンピオンを超えた候補をプロット
if len(better_candidates_bo) > 0: better_points_bo = np.array([t.values for t in better_candidates_bo]) plt.scatter( better_points_bo[:, 1], better_points_bo[:, 0], c="magenta", s=200, marker="*", edgecolors="black", zorder=11, label="BO: Superior Candidates", )
plt.xlabel("Tensile Strength (MPa)", fontsize=14)
plt.ylabel("Yield Strength (MPa)", fontsize=14)
plt.title( f"Local Bayesian Optimization around Champion", fontsize=16,
)
plt.legend(fontsize=12)
plt.grid(True)
plt.show()
# 新規有望材料の組成を表示
if len(better_candidates_bo) > 0: newly_discovered_params = [t.params for t in better_candidates_bo] newly_discovered_df = pd.DataFrame(newly_discovered_params) newly_discovered_df["predicted_yield_strength"] = [ t.values[0] for t in better_candidates_bo ] newly_discovered_df["predicted_tensile_strength"] = [ t.values[1] for t in better_candidates_bo ] print("\n【ベイズ最適化で発見された新規有望材料の組成】") print( newly_discovered_df.sort_values( "predicted_tensile_strength", ascending=False ).round(4) )
print("\n処理はすべて完了しました。")

実行結果と考察

ベイズ最適化による「賢い探索」の可視化

コードを実行して得られる上のグラフは、ベイズ最適化がどのように機能したかを示しています。

  • 灰色のプロット (All Training Data): 我々が持つ知識の全体像です。
  • 青い大きなプロット (Original Champion): 探索の出発点であり、超えるべき目標(引張強さ: 2570.0 MPa)です。
  • 緑色のプロット (All BO Candidates): これがベイズ最適化が試行した500の候補です。
  • マゼンタ色の星印 (BO: Superior Candidates): 緑のプロットの中から、特に元のチャンピオンを支配する(両方の特性で上回る)と判断された、選りすぐりの新規候補です。

発見された新規有望材料

ベイズ最適化は、元のチャンピオン(降伏強度: 2510.3 MPa, 引張強さ: 2570.0 MPa)を超える、3つの有望な候補を発見しました。

cmnsicrnimovnnbcowaltipredicted_yield_strengthpredicted_tensile_strength
20.00910.04770.04640.011917.44754.39770.01050.00.010213.18130.00.71471.50242533.91032602.4822
10.01120.04660.04710.012017.20414.41630.01070.00.011213.45030.00.69281.51642535.81552599.9633
00.01120.04640.04720.012017.15624.41500.01080.00.011213.48250.00.68951.51722535.85492599.5645

これらの候補は、前回発見した材料よりもさらに高い強度特性を持つ可能性を示唆しており、特に最も優れた候補は引張強さ2600 MPaの大台を超えるという目覚ましい結果となっています。同じ試行回数(500回)でも、より賢い探索アルゴリズムを用いることで、さらなる高みへと到達できることを示しています。

コードの詳細解説

今回の実装の核心部分である、Optunaを用いたベイズ最適化について、より深く掘り下げて解説します。

ステップ5: 探索空間の定義

search_range_ratio = 0.20

前回のランダム探索では探索範囲を±5%に設定しましたが、今回は±20%と広めに設定しています。これは、ベイズ最適化が闇雲に探索するのではなく、有望な領域を自ら見つけ出す「賢さ」を持っているため、より広い探索空間を与えても効率的に最適解にたどり着けるという信頼の表れです。これにより、AIがより大胆な改良案を発見する可能性が生まれます。

ステップ6: Optunaによる局所的ベイズ最適化の実行

このセクションが今回の核心部分です。

def objective(trial): # 各元素の組成を探索空間内で提案させる composition = {} for element in features: composition[element] = trial.suggest_float( element, comp_min[element], comp_max[element] ) # 提案された組成で物性を予測 composition_df = pd.DataFrame([composition], columns=features) predicted_properties = model.predict(composition_df) # 2つの目的変数(物性値)を返す return predicted_properties[0, 0], predicted_properties[0, 1]

objective(trial) 関数は、Optunaにおける「1回の試行(評価)」を定義します。**study.optimize**が呼び出すたびに、以下の処理が実行されます。

  1. trialオブジェクト: Optunaが内部的に管理する、今回の試行に関する情報を持つオブジェクトです。
  2. trial.suggest_float(...): このメソッドが、探索の「一手」を提案する部分です。内部ではサンプラー(今回はMOTPESampler)が、「これまでの結果を踏まえると、次はこの組成を試すのが最も良さそうだ」と判断した値を返します。**comp_mincomp_max**で定義された範囲内から、賢く値を選んできます。
  3. 代理モデルによる評価: 提案された組成(composition)を使って、学習済みのCatBoostモデル(model)で物性を高速に予測します。
  4. 返り値: 予測された2つの物性値(降伏強度と引張強さ)をタプルとして返します。Optunaはこの返り値を受け取り、サンプラーに情報をフィードバックして、次の**trial**でさらに良い提案ができるように学習していきます。
# 多目的最適化のStudyを作成(2つの目的を両方最大化)
study = optuna.create_study( directions=["maximize", "maximize"], sampler=optuna.samplers.MOTPESampler(seed=42)
)

**optuna.create_study(...)**この一行で、最適化タスク全体の設定を行います。

  • directions=["maximize", "maximize"]目的の定義です。**objective**関数が返す2つの値(タプルの1番目と2番目)を、両方とも最大化(maximize)することを目指す、とOptunaに伝えています。これにより、多目的最適化のタスクとして設定されます。
  • sampler=optuna.samplers.MOTPESampler(seed=42)探索アルゴリズムの選択です。ここがベイズ最適化の「頭脳」を指定する部分です。
    • MOTPESamplerMulti-objective Tree-structured Parzen Estimatorの略で、ベイズ最適化の中でも特に多目的最適化に強い性能を発揮するアルゴリズムです。
    • これを指定することで、Optunaは単なるランダムな値ではなく、獲得関数に基づいて計算された「次に試すべき最も有望な点」を提案するようになります。
    • **seed=42**は、乱数シードの固定です。これにより、何度実行しても同じ探索経路を辿るため、結果の再現性が保証されます。

ステップ7: 探索結果の特定

best_trials = study.best_trials

**study.optimizeが完了すると、Optunaは探索の過程で見つけたパレートフロント上の解(どの解にも支配されない優秀な解の集合)をstudy.best_trials**プロパティに自動で格納してくれます。これにより、第7回の記事のように自前でパレートフロントを計算する複雑な処理が不要になり、結果の分析に集中できるという大きなメリットがあります。

最後に

今回は、材料探索の効率を飛躍的に向上させる「ベイズ最適化」を、Optunaという強力なツールを用いて実践しました。闇雲な探索から脱却し、AIが自律的に有望な領域を判断して探索を進めることで、より少ない試行回数で、より優れた材料候補を発見できることを示しました。

これまでのシリーズを通して、私たちはデータの前処理に始まり、予測モデルの構築、そしてHTVSやベイズ最適化による未知材料の探索まで、マテリアルズインフォマティクスの基本的なワークフローを一気通貫で体験してきました。しかし、私たちが手にしたこの強力な予測・探索エンジンも、現状ではPythonコードを実行できる専門家だけが使える「閉じたツール」に過ぎません。

そこで入門編の最終回となる次回は、このMIの成果を、現場の材料開発者など、より多くの人々に「届ける」ための架け橋を架けます。テーマは「MIを届けよう!Streamlitで対話的なWebアプリ開発」です。今回構築したベイズ最適化モデルをバックエンドに、誰でも直感的に組成を入力し、物性を予測できるWebアプリケーションを構築します。専門家でないユーザーでもMIの力を最大限に活用できるツールを開発する、まさに実践的なゴールにご期待ください。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です