実験データを機械学習で予測できても、「次にどの条件を試すか」は別の問題です。予測値が最も高い条件だけを選ぶと、既に調べた領域の周辺を繰り返し、未探索の有望な領域を見逃すことがあります。逆に、不確かさの大きい条件ばかり選ぶと、目的の性能に近づかないまま実験予算を使ってしまいます。
ベイズ最適化は、これまでの実験結果から目的特性を予測するモデルを作り、予測平均と不確かさを使って、次に測定する条件を決める方法です。本記事では、編集部が作った仮想のセラミックス強度データを使い、CSVの読み込みから候補の順位付け、実測結果の追加までをPythonで実行します。
結論は、目的・探索範囲・実行可能条件を先に決め、候補を1条件ずつ選び、測定結果を加えてモデルを更新することです。候補の提案は成功の保証ではありません。測定の品質、装置の限界、実験の実施可否は研究者が判断します。紹介する温度・時間・強度はすべて合成データで、実在する材料や工程の推奨条件ではありません。
1.ベイズ最適化は「次の測定」を決める方法
予測モデルと最適化の役割は違う
予測モデルは、入力した条件に対して特性を推定します。たとえば焼成温度と保持時間から強度を予測します。最適化は、そのモデルを利用して、許された候補の中から次に試す条件を選びます。条件を決める基準が獲得関数です。
本記事は、実験条件を入力し、測定された強度を目的変数とする、単一目的の最大化を扱います。条件の候補は有限の表として用意し、1回の更新で1条件を選びます。多目的最適化、カテゴリ変数、装置への自動送信は実装範囲に含めず、必要になる場面と限界を後半で説明します。
初期実験から追加実験へ進む流れ
| 段階 | 実施すること | 残す情報 |
|---|---|---|
| 目的設定 | 最大化する性能と探索可能な範囲を決める | 単位、上下限、装置刻み、予算 |
| 初期実験 | 条件空間の異なる場所を測る | 実験ID、条件、測定結果 |
| モデル学習 | 条件と特性の関係を推定する | モデル設定、測定ノイズの設定 |
| 候補選定 | 未実施候補を獲得関数で順位付けする | 平均、標準偏差、指標値、順位 |
| 実施判断 | 候補が実行可能かを確認する | 採用・見送り理由 |
| 更新・確認 | 実測値を追加し、再学習する | 実施履歴、最良条件の再測定結果 |
実験計画法との大きな違いは、実験結果を見ながら次の条件を更新する点です。ただし、実験計画法も逐次的に使えます。手法選択の比較は実験計画法とベイズ最適化の違いで整理しています。本記事では比較の繰り返しよりも、実際の候補選定に進みます。
公開研究例:薄膜の性能と変動を同時に考える
Shrivastavaらは、モリブデン薄膜のスパッタ成膜で、出力と圧力を探索する研究を報告しています。残留応力とシート抵抗だけでなく、圧力変動に対する応力の変化も目的設計へ取り入れています。[4]
ここから学べるのは、何を「良い条件」と呼ぶかを探索前に定義する重要性です。ピーク性能が高くても、小さな条件変動で品質が崩れる工程が望ましいとは限りません。本記事の強度最大化は、この研究の実装や結果を再現するものではありません。公開本文はarXivで確認しました。同ページに掲載誌と論文DOIが記載されています。対象装置を超えた一般的な性能向上率を保証する根拠には使いません。
2.ガウス過程回帰:平均と不確かさを一緒に予測する
予測平均と標準偏差を読む
ガウス過程回帰(GPR)は、条件と特性の関係を確率的に表す回帰モデルです。scikit-learnのGaussianProcessRegressorは、予測平均と標準偏差を返せます。[1] 平均はモデルが予想する値、標準偏差はその予測分布の広がりです。
例として、候補Aの予測が100 MPa、標準偏差が1 MPa、候補Bが98 MPa、標準偏差が6 MPaだったとします。Aは平均値が高く、Bは平均値では劣りますが、モデル上は高性能を示す可能性がより広く残っています。どちらを選ぶかは、現在の最良値や、探索を重視する程度で変わります。
標準偏差は、モデル・カーネル・データ・ノイズ設定を前提にした値です。未知の相変化、記録されていない原料差、装置の系統誤差を自動で検出したものではありません。小さい標準偏差だけで予測の正しさを断定しないことが重要です。
カーネルは条件間の類似性を表す
カーネルは、二つの入力条件がどの程度似ているかを表す関数です。似た条件では似た特性が得られる、という仮定をモデルへ与えます。カーネルの種類や長さ尺度によって、滑らかな変化を想定するか、短い距離で大きな変化を許すかが変わります。[1]
実習ではMatérnカーネルを使います。温度と時間は単位も数値幅も違うため、あらかじめ定めた上下限で0〜1に変換します。長さ尺度を変数ごとに持たせ、学習によって調整します。この選択がすべての材料に最適という意味ではありません。相転移や工程の切り替わりがあるなら、単一の滑らかなモデルでは表しにくい場合があります。
測定ノイズと潜在特性の不確かさを区別する
本コードは、観測値を「潜在する特性+独立な測定ノイズ」として扱います。ノイズの標準偏差を目的変数の単位で指定し、学習時のalphaへ分散として渡します。alphaによるノイズ設定は公式資料にも説明されています。[1]
出力列latent_stdは、潜在特性の予測標準偏差です。将来の測定値そのものの標準偏差ではありません。独立な同分散ノイズという仮定なら、測定値の分散にはノイズ分散も加わります。本例の0.5 MPaは合成データ生成に対応する値であり、実データでは反復測定や測定系の評価から決めてください。バッチ差や日付による系統変動を、この一つの数値だけで処理することはできません。
3.EI・PI・UCBは何を重視するのか
共通の記号と単位
最大化では、予測平均をμ、潜在特性の標準偏差をσ、現在の最良観測値をbとします。ξは「どれだけ上回れば改善とみなすか」の余裕量で、強度ならMPa単位です。κはUCBで不確かさを重視する大きさを表す、単位のない係数です。
| 手法 | 定義・仕組み | 読み方と注意点 |
|---|---|---|
| EI:Expected Improvement | E[max(f−b−ξ,0)] | 改善する量の期待値。改善しない場合は0として数える |
| PI:Probability of Improvement | P(f>b+ξ) | 改善する確率。改善幅そのものは評価しない |
| UCB:Upper Confidence Bound | μ+κσ | 平均に不確かさの加点をする。κを大きくすると探索寄りになる |
EIの定義と、単一候補の正規予測分布を使う実装はBoTorchの公式資料で確認できます。[2] 本記事のUCBは固定κの教育用ルールです。一定の信頼水準の区間や、一般的な収束保証を意味するものではありません。
候補3点を数値で比較する
現在の最良値を100 MPa、ξ=0、κ=2として計算します。ここは実験結果ではなく、獲得関数の違いを理解するための仮想計算です。
| 候補 | μ(MPa) | σ(MPa) | EI(MPa) | PI | UCB(MPa) |
|---|---|---|---|---|---|
| A | 101 | 0.5 | 1.004 | 0.977 | 102 |
| B | 99 | 3 | 0.763 | 0.369 | 105 |
| C | 98 | 6 | 1.525 | 0.369 | 110 |
PIならA、EIとこの設定のUCBならCを選びます。BとCは、最良値からの差をσで割った値が同じなので、PIが同じです。しかしCは予測分布の広がりが大きく、改善したときの幅を評価するEIは大きくなります。平均だけならAを選ぶため、獲得関数が必要な理由が分かります。
正規分布の予測を仮定し、z=(μ−b−ξ)/σとすると、EI=(μ−b−ξ)Φ(z)+σφ(z)、PI=Φ(z)です。Φは標準正規分布の累積分布関数、φはその確率密度です。コードのnorm.cdfとnorm.pdfがそれぞれ対応します。[2]
σがほぼ0の場合、EIはmax(μ−b−ξ,0)、PIは改善条件を満たすか否かとして扱います。ゼロで割る実装は避けます。有限候補の得点を比較するだけでも、極端に小さなEIが数値的に0になる場合があります。
教育用EIとLogEIの位置づけ
通常のEIは値や勾配が非常に小さくなり、最適化が難しくなることがあります。Amentらはその数値的問題とLogEIを扱っており、BoTorchの公式資料も通常のEIよりLogExpectedImprovementを推奨しています。[2][3]
本記事は式との対応が見えるよう、有限候補の通常EIを計算します。全候補のEIが0なら停止し、UCBへ無言で切り替えません。高度な探索でLogEIを使う場合は、log(EI)を単純に後付けするのではなく、数値的に安定した実装を利用します。ノイズが強い場合には、最良観測値を固定したEI自体の前提も見直し、ノイズを考慮する獲得関数へ進む必要があります。[2]
4.探索前に決める五つのこと
目的変数と探索範囲を固定する
強度最大化、抵抗最小化、目標値からの誤差最小化は異なる問題です。複数の目的を一つの点数にまとめるなら、重みや単位の処理によって結果が変わります。探索の途中で目的を変更した場合は、同じ履歴の連続結果として混ぜず、変更理由と設定を残します。
実習の探索範囲は温度1000〜1400、保持時間30〜150です。候補は温度20、時間10刻みの273条件です。単位はCSVの列名に持たせます。実データへの置き換え時には、コードのFEATURES、TARGET、LOWER、UPPERを一緒に変更します。許された条件表を先に作り、装置で実現できない条件は候補から除いてください。
初期点は一か所に集めない
初期8条件は低温・高温、短時間・長時間、中央付近を含む仮想配置です。「8件あれば十分」という推奨ではありません。必要数は次元、変化の複雑さ、ノイズ、制約によって異なります。変数が増えるほど、同じ件数では条件空間を調べられなくなります。
本コードは少なくとも4つの異なる条件と、両変数の変動を要求します。これは教材が動くための入力条件であって、統計的な十分性の基準ではありません。データが少ないときの基本的な判断は材料開発の少数データで機械学習を使う方法も参照してください。
反復測定と探索を分けて予算化する
新しい条件を調べる実験と、同じ条件で再現性を確かめる実験は役割が違います。本コードは観測済み条件を候補から除きますが、反復測定を不要と判断しているわけではありません。最良条件の確認実験や、測定ノイズを把握する反復は別枠で計画してください。
同じ試料を繰り返し測ることと、独立に作製した試料を測ることも区別します。条件以外に試料ID、バッチ、日付を実験記録へ残してください。学習データの評価設計は材料データで交差検証を設計する方法で扱っています。交差検証の誤差が小さくても、逐次探索で良い候補を選べることが自動で証明されるわけではありません。
5.PythonでCSVから次の実験条件を選ぶ
動作確認環境と実行方法
コードはPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、scikit-learn 1.8.0で実行確認しました。Matplotlibは図の保存に使います。公式Web資料は更新されるため、資料の最新版とこの検証環境は区別します。
python -m pip install numpy==2.3.5 pandas==2.2.3 scipy==1.17.0 scikit-learn==1.8.0 matplotlib==3.10.8
python bayesian_materials_search.py --output bo_results
引数にCSVを指定しないと、固定乱数で仮想データと候補表を作ります。初期の観測データは8行、候補表は273行です。既に測った8条件を除き、265候補を評価します。実験IDは各行で一意にし、反復測定にも別のIDを付けます。
experiment_id,temperature_c,hold_time_min,strength_mpa
E000,1020,40,実測した数値
E001,1380,40,実測した数値
上の「実測した数値」は列の意味を示す説明用表記です。そのまま実行用CSVへ入れず、配布する数値入りCSVを使ってください。候補用CSVにはcandidate_idと二つの条件列だけを入れます。候補の強度はまだ測っていないので、入力しません。
学習・採点コードの全体
次の完全スクリプトでは、入力を検証した後、温度・時間を固定範囲で正規化し、目的変数を平均0・標準偏差1へ変換します。ノイズ分散も同じ尺度へ変換してGPRを学習し、予測を元のMPa単位へ戻してから獲得関数を計算します。目的変数を最小化するときは符号を反転して効用を最大化します。
欠損値、無限値、範囲外条件、重複候補、不正ID、一定の目的変数を検出した場合は停止します。測れなかった実験を0 MPaとみなしたり、目的変数を自動補完したりしません。実施不能条件の理由を残した上で、別のデータ設計を行ってください。
完全なPythonコードを表示(保存名:bayesian_materials_search.py)
"""Educational, finite-candidate, single-objective Bayesian optimization.
Python 3.12; numpy, pandas, scipy, scikit-learn, matplotlib.
All demo data are synthetic, not recommended ceramic processing conditions.
"""
import argparse
import json
import warnings
from pathlib import Path
import numpy as np
import pandas as pd
import scipy
from scipy.stats import norm
import sklearn
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import ConstantKernel, Matern
from sklearn.exceptions import ConvergenceWarning
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
FEATURES = ['temperature_c', 'hold_time_min']
TARGET = 'strength_mpa'
LOWER = np.array([1000., 30.])
UPPER = np.array([1400., 150.])
SEED = 42
def acquisition(mean, std, best, xi=0.0, kappa=2.0):
mean, std = np.asarray(mean), np.asarray(std)
improvement = mean - best - xi
positive = std > 1e-12
z = np.divide(improvement, std, out=np.zeros_like(mean), where=positive)
ei = np.where(positive, improvement * norm.cdf(z) + std * norm.pdf(z),
np.maximum(improvement, 0.0))
pi = np.where(positive, norm.cdf(z), (improvement > 0).astype(float))
return np.maximum(ei, 0.0), pi, mean + kappa * std
def validate(observations, candidates):
if not {'experiment_id', TARGET, *FEATURES}.issubset(observations.columns):
raise ValueError('Observations need experiment_id, strength_mpa and both feature columns.')
if not {'candidate_id', *FEATURES}.issubset(candidates.columns):
raise ValueError('Candidates need candidate_id and both feature columns.')
observations, candidates = observations.copy(), candidates.copy()
for frame, id_col in [(observations, 'experiment_id'), (candidates, 'candidate_id')]:
if frame[id_col].isna().any() or frame[id_col].astype(str).str.strip().eq('').any():
raise ValueError('Empty IDs are not allowed.')
if frame[id_col].astype(str).duplicated().any():
raise ValueError('IDs must be unique within each CSV.')
for col in FEATURES + ([TARGET] if id_col == 'experiment_id' else []):
frame[col] = pd.to_numeric(frame[col], errors='raise')
values = frame[FEATURES].to_numpy(float)
if not np.isfinite(values).all():
raise ValueError('Feature values must be finite; no automatic imputation.')
if ((values < LOWER) | (values > UPPER)).any():
raise ValueError('A condition is outside the declared search bounds.')
if not np.isfinite(observations[TARGET]).all():
raise ValueError('Measured targets must be finite; failed experiments need separate handling.')
if len(observations) < 4 or len(observations[FEATURES].drop_duplicates()) < 4:
raise ValueError('This teaching script requires at least four distinct measured conditions.')
if observations[FEATURES].nunique().min() < 2:
raise ValueError('Both variables need variation in the initial data.')
if candidates[FEATURES].duplicated().any():
raise ValueError('Duplicate candidate conditions; canonicalize values using instrument resolution.')
# Exact matching requires canonical values/units (e.g., instrument ticks).
observed_keys = set(map(tuple, observations[FEATURES].to_numpy(float)))
keep = [tuple(row) not in observed_keys for row in candidates[FEATURES].to_numpy(float)]
candidates = candidates.loc[keep].reset_index(drop=True)
if candidates.empty:
raise ValueError('No unmeasured candidates remain.')
return observations, candidates
def propose(observations, candidates, *, minimize=False, noise_std=0.5,
method='ei', xi=0.0, kappa=2.0):
if not np.isfinite([noise_std, xi, kappa]).all() or noise_std <= 0 or xi < 0 or kappa < 0:
raise ValueError('noise_std > 0, xi >= 0 and kappa >= 0 are required.')
observations, candidates = validate(observations, candidates)
x = (observations[FEATURES].to_numpy(float) - LOWER) / (UPPER - LOWER)
xc = (candidates[FEATURES].to_numpy(float) - LOWER) / (UPPER - LOWER)
sign = -1.0 if minimize else 1.0
utility = sign * observations[TARGET].to_numpy(float)
center, scale = utility.mean(), utility.std(ddof=0)
if scale <= 1e-12:
raise ValueError('Constant targets: review measurements and initial design before optimization.')
kernel = ConstantKernel(1.0, (0.01, 100.0)) * Matern(
length_scale=[0.3, 0.3], length_scale_bounds=(0.03, 3.0), nu=2.5)
model = GaussianProcessRegressor(kernel=kernel, alpha=(noise_std / scale)**2,
normalize_y=False, n_restarts_optimizer=2, random_state=SEED)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter('always', ConvergenceWarning)
model.fit(x, (utility-center)/scale)
m, s = model.predict(xc, return_std=True)
m, s = m*scale+center, s*scale
ei, pi, ucb = acquisition(m, s, utility.max(), xi, kappa)
scores = {'ei': ei, 'pi': pi, 'ucb': ucb}[method]
if not np.isfinite(np.column_stack([m, s, ei, pi, ucb])).all():
raise ValueError('Non-finite predictions/acquisition values; do not select a candidate.')
if method == 'ei' and scores.max() <= 0:
raise ValueError('All EI scores are zero: review scaling/model or use a robust LogEI implementation.')
result = candidates.copy()
result['predicted_mean'] = sign*m
result['latent_std'] = s
result['ei'] = ei
result['pi'] = pi
result['ucb_utility'] = ucb
result['selection_score'] = scores
result = result.sort_values('selection_score', ascending=False, kind='stable').reset_index(drop=True)
result.insert(0, 'rank', np.arange(1, len(result)+1))
info = {'kernel': str(model.kernel_), 'noise_std_target_units': noise_std,
'direction': 'minimize' if minimize else 'maximize', 'method': method,
'xi_target_units': xi, 'kappa': kappa, 'seed': SEED,
'bounds': {'lower': LOWER.tolist(), 'upper': UPPER.tolist()},
'warnings': [str(w.message) for w in caught],
'note': 'latent_std excludes independent observation noise; demo does not validate real processes.'}
return result, info
def synthetic_truth(frame):
t, h = frame[FEATURES].to_numpy(float).T
return 70 + 30*np.exp(-((t-1260)/100)**2-((h-85)/35)**2) - 0.00005*(t-1200)**2
def make_demo():
candidates = pd.DataFrame([(t,h) for t in range(1000,1401,20)
for h in range(30,151,10)], columns=FEATURES)
candidates.insert(0, 'candidate_id', [f'C{i:03}' for i in range(len(candidates))])
initial = np.array([[1020,40],[1380,40],[1020,140],[1380,140],
[1200,90],[1100,90],[1300,90],[1200,50]], float)
observations = pd.DataFrame(initial, columns=FEATURES)
observations.insert(0, 'experiment_id', [f'E{i:03}' for i in range(len(initial))])
observations[TARGET] = synthetic_truth(observations) + np.random.default_rng(SEED).normal(0,0.5,len(initial))
return observations, candidates
def save_plots(observations, ranked, history, destination):
fig, ax = plt.subplots(figsize=(7,5))
pts = ax.scatter(ranked[FEATURES[0]], ranked[FEATURES[1]], c=ranked['ei'], cmap='viridis', s=35)
ax.scatter(observations[FEATURES[0]], observations[FEATURES[1]], c='white', edgecolors='black', s=65, label='Measured')
top = ranked.iloc[0]
ax.scatter(top[FEATURES[0]], top[FEATURES[1]], c='red', marker='*', s=200, label='Next candidate')
ax.set(xlabel='Temperature (synthetic, C)', ylabel='Hold time (synthetic, min)', title='Finite-candidate EI: synthetic example')
ax.legend(); fig.colorbar(pts, ax=ax, label='Expected improvement (MPa)')
fig.tight_layout(); fig.savefig(destination/'candidate_map.png', dpi=150); plt.close(fig)
if history:
hist = pd.DataFrame(history)
hist.to_csv(destination/'simulation_history.csv', index=False)
fig, ax = plt.subplots(figsize=(7,4))
ax.plot(hist['n_observed'], hist['best_observed'], 'o-')
ax.set(xlabel='Number of observations', ylabel='Best observed strength (synthetic, MPa)',
title='One synthetic run; no guaranteed improvement')
fig.tight_layout(); fig.savefig(destination/'progress.png', dpi=150); plt.close(fig)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--observations'); parser.add_argument('--candidates')
parser.add_argument('--output', default='bo_results')
parser.add_argument('--method', choices=['ei','pi','ucb'], default='ei')
parser.add_argument('--minimize', action='store_true')
parser.add_argument('--noise-std', type=float, default=0.5)
parser.add_argument('--xi', type=float, default=0.0)
parser.add_argument('--kappa', type=float, default=2.0)
parser.add_argument('--simulate', type=int, default=0, help='Demo-only number of additional synthetic experiments')
args = parser.parse_args()
if bool(args.observations) != bool(args.candidates):
parser.error('Supply both --observations and --candidates, or neither for the synthetic demo.')
if args.simulate < 0 or (args.simulate and args.observations):
parser.error('--simulate is non-negative and available only for the built-in synthetic demo.')
observations, candidates = (pd.read_csv(args.observations), pd.read_csv(args.candidates)) if args.observations else make_demo()
destination = Path(args.output); destination.mkdir(parents=True, exist_ok=True)
observations.to_csv(destination/'observations_input.csv', index=False)
candidates.to_csv(destination/'candidates_input.csv', index=False)
settings = dict(minimize=args.minimize, noise_std=args.noise_std, method=args.method, xi=args.xi, kappa=args.kappa)
first_ranked, first_info = propose(observations, candidates, **settings)
first_ranked.to_csv(destination/'ranked_candidates_initial.csv', index=False)
initial_observations = observations.copy()
best = lambda f: float(f[TARGET].min() if args.minimize else f[TARGET].max())
history = [{'n_observed':len(observations), 'best_observed':best(observations)}]
rng = np.random.default_rng(SEED+1)
for step in range(args.simulate):
ranked, info = propose(observations, candidates, **settings)
row = ranked.iloc[0]
added = pd.DataFrame([{ 'experiment_id':f'SIM{step:03}', **{c:row[c] for c in FEATURES}}])
added[TARGET] = synthetic_truth(added) + rng.normal(0,0.5,len(added))
observations = pd.concat([observations, added], ignore_index=True)
history.append({'n_observed':len(observations), 'best_observed':best(observations)})
ranked, info = propose(observations, candidates, **settings)
ranked.to_csv(destination/'ranked_candidates.csv', index=False)
ranked.head(1).to_csv(destination/'next_candidate.csv', index=False)
observations.to_csv(destination/'observations_updated.csv', index=False)
info.update({'python_note':'Tested environment recorded in article', 'numpy':np.__version__,
'pandas':pd.__version__, 'scipy':scipy.__version__, 'scikit_learn':sklearn.__version__,
'synthetic_demo':not bool(args.observations), 'simulation_steps':args.simulate,
'initial_fit_warnings':first_info['warnings']})
(destination/'run_settings.json').write_text(json.dumps(info, ensure_ascii=False, indent=2), encoding='utf-8')
save_plots(initial_observations, first_ranked, history if args.simulate else [], destination)
print(ranked.head(3).to_string(index=False))
if info['warnings']:
print('Model fitting warnings: inspect run_settings.json before using the proposal.')
print('Saved to', destination.resolve())
if __name__ == '__main__':
main()
出力を読む:順位1位だけで判断しない
初期データでEIを使った実行結果は、概ね次の値になりました。表示は丸めています。環境・最適化計算の差によって下位順位や値が変わる場合があるため、モデル設定と警告も保存します。
| 順位 | 候補ID | 温度 | 時間 | 予測平均 | 潜在標準偏差 | EI |
|---|---|---|---|---|---|---|
| 1 | C175 | 1260 | 90 | 94.913 | 2.724 | 1.244 |
| 2 | C188 | 1280 | 90 | 95.152 | 1.805 | 1.022 |
| 3 | C162 | 1240 | 90 | 93.924 | 2.669 | 0.756 |
1位の予測平均は2位より低いですが、不確かさと改善幅を考慮したEIは1位の方が高くなっています。ranked_candidates.csvが全順位、next_candidate.csvが次の1条件です。run_settings.jsonにはノイズ、方向、獲得関数、カーネル、警告、ライブラリ版を保存します。
この初期モデルでは、時間方向の長さ尺度が設定した下限に近いという警告が出ました。短い距離で変化する関係をモデルが選んだ可能性がありますが、少数点での不安定な学習も考えられます。警告が出ても計算結果は保存しますが、実施判断は保留して、初期点の配置、測定値、尺度、カーネル境界を確認してください。境界を広げるだけで解決したことにはなりません。
6.結果を追加して再実行する
自分のCSVを指定する
python bayesian_materials_search.py --observations my_experiments.csv --candidates my_candidates.csv --noise-std 0.5 --output round_01
noise-stdは目的変数と同じ単位です。自分の測定系に合う値へ変更します。xiも同じ単位、kappaは単位なしです。PIなら–method pi、UCBなら–method ucbを指定できます。抵抗などを最小化する場合は–minimizeを加えます。最小化でもselection_scoreは大きいほど選ばれます。predicted_meanは元の目的変数の単位・符号へ戻りますが、ucb_utilityは符号反転後の効用の指標です。
条件を実際に試したら、新しい実験IDと実測値を観測CSVへ追記し、同じ候補表で再実行します。観測済み条件が除かれるので、同じ条件の再提案を防げます。ただし浮動小数値の完全一致を使うため、温度の丸めや単位を統一してください。複雑な実務データでは装置刻みを整数のコードで管理する方が確実です。
順位表の上位3条件を同時に測っても、逐次3回と同じ効果にはなりません。ここで返す順位は、同じ時点のモデルがそれぞれを評価した値です。候補間の情報の重複を考えるバッチ獲得関数は実装していません。教材では1条件を選び、結果を見てから次の条件へ進みます。
合成データで8回の更新を確認する
python bayesian_materials_search.py --simulate 8 --output bo_simulation
このモードは仮想データ専用です。未知の真の関数を教材側に用意して、候補の実測を模擬します。実データCSVと一緒には指定できません。真の関数の値は、候補選定モデルには渡していません。
確認した一つの乱数系列では、初期8件の最良観測値が94.612、9件時点で99.336、10件時点で99.553となり、その後16件まで最良値は更新されませんでした。単調に上がるのは履歴上の最良観測値で、各追加実験の測定値ではありません。モデルが毎回改善する証拠や、ランダム探索より優れることの比較実験ではありません。
性能比較を行うなら、同じ初期点・候補集合・総実験予算で、ランダム探索などと複数乱数系列を比較します。実材料では同一の条件を完全に再現できるとは限らず、実験順や装置状態も結果に影響します。単一の見栄えの良い曲線だけで導入効果を結論づけないでください。
7.実データへの置き換えと拡張の判断
候補制約を先に確認する
装置の上下限以外にも、組み合わせによって実行できない条件があります。これらは候補表を作る段階で除きます。一方、実験して初めて分かる析出失敗や規格不適合は未知の制約です。単に削除して成功例だけで学習すると、失敗しやすい領域が隠れてしまいます。失敗状態と性能値を分けて記録し、必要に応じて制約を予測するモデルへ拡張します。
カテゴリ変数を0、1、2と置くと、存在しない距離や大小関係を与えることがあります。原料種や設備型式を扱うなら、適切な表現とモデルが必要です。組成比の合計が1という制約も、独立した矩形範囲だけでは表せません。まず実行可能な候補を作るか、制約付きの探索方法を選びます。本コードを列の追加だけで高次元・混合変数へ一般化しないでください。
ADと探索範囲は同じものではない
探索範囲は実験可能な条件の集合、AD(Applicability Domain)はモデルの予測を利用できる領域を判断するための考え方です。範囲内でも学習点から遠い条件はあり得ます。詳しくはモデルの適用範囲(AD)を参照してください。
既知領域で性能を判断するなら、AD内であることや外部検証を重視します。一方、新しい領域を調べる探索では、AD外の候補を測ること自体に意味があります。ただし、その候補を「高性能と確認された条件」と扱ってはいけません。モデル上の予想と、確認のための測定対象を区別します。
本コードはAD判定を自動実装していません。GPRの標準偏差だけをADラベルへ置き換えず、学習点との距離、条件の意味、既存記事で扱うAD評価などを別に確認します。安全な実行範囲の外へ出る理由として不確かさを利用することはありません。
8.失敗例、限界、停止条件
偶然高かった観測値に固執する
低ノイズを仮定したEIは、最良観測値を改善基準にします。偶然大きな誤差を含んだ最高値をbにすると、改善の見込みが不当に低くなることがあります。ノイズが強い場合は、反復測定、ノイズモデル、潜在性能の推定、ノイズを考慮する獲得関数を検討します。[2] 本教材のalpha設定だけで、ノイズのある最適化の問題すべてが解決されるわけではありません。
実験できる変数と説明変数を混同する
探索する入力は、実験前に設定できる変数にします。測定後にしか分からない結晶粒径を入力して、次の温度・時間を直接選ぶことはできません。粒径を中間特性として扱うなら、その生成関係も含めた別の設計が必要です。条件と結果、事前情報と事後情報を分けてください。
製薬の収率、装置測定の信号、製造工程の不良率でも、目的の定義が違えば探索の意味が変わります。不良率なら試行数、信号なら測定時間、収率なら測定法が必要です。連続・同分散の誤差を前提とする教材を、割合や検出限界のあるデータへ無条件で適用しないことが重要です。
停止は予算と検証を含めて決める
候補がなくなった、計算値が不正になった、入力範囲を外れた場合はコードが停止します。実務では、実験予算の上限、目標値と再現性の達成、装置状態の変化、測定品質の悪化も停止・見直し条件にします。
EIが小さくなったことだけで真の最適解に到達したとは言えません。モデルが過信している、候補表が粗い、重要な変数が抜けている可能性もあります。最良条件を独立試料で再測定し、必要なら近傍条件と変動への感度を確認して、探索結果を実装判断につなげます。
9.実務チェックリストとFAQ
実行前・実施判断・更新後の確認
- 目的変数、最大化・最小化、単位、予算を定めたか。
- 操作できる変数だけを探索し、装置刻みと実行不能条件を反映したか。
- 初期点が一か所に集中せず、反復測定の予算も確保したか。
- 測定ノイズの設定理由、バッチ・日付・試料情報を記録したか。
- 欠損目的変数や失敗実験を、正常な低性能値へ置き換えていないか。
- 予測平均、潜在標準偏差、獲得関数、学習警告を確認したか。
- 候補採用の理由と見送り理由を残し、実測値で更新したか。
- 最良観測値を再測定し、予測の確からしさと探索の判断を区別したか。
Q.何件あればベイズ最適化を始められますか?
万能な最低件数はありません。本教材の4条件以上は入力検証の基準です。少数点では長さ尺度やノイズの推定が不安定になりやすく、次元や反応の複雑さに応じた初期計画が必要です。件数だけでなく、条件空間の覆い方を確認します。
Q.予測が外れたら、そのデータを削除しますか?
まず測定、試料、単位、工程状態を確認します。誤記や測定不良と分かった場合は訂正履歴を残します。正しく測定された予想外の結果は、モデルが知らない変化の情報です。都合が悪いという理由だけで削除しません。
Q.同じ条件の反復データは入力できますか?
異なる実験IDなら入力できます。ただしノイズを独立・同分散として扱うため、相関する反復やバッチ差を自動で処理しません。反復の目的と試料の独立性を記録してください。候補側では観測済み条件を除くので、確認実験は別に計画します。
Q.PIとUCBも試して、一番良さそうな結果を選べますか?
比較はできますが、評価データを見ながら設定を都合よく選び直すと評価が楽観的になります。設定と選定理由を残し、公平な予算で複数試行を比べます。一つの乱数系列で最高値が出た手法を、普遍的な最良手法としないでください。
Q.連続最適化や多目的探索へ進むべき場面は?
候補数が非常に多い、カテゴリと連続変数が混在する、複数性能のトレードオフを扱う、未知の制約が重要といった場合です。本記事でデータの更新と指標の意味を理解した上で、対応するモデル・獲得関数へ拡張します。有限候補の単一目的コードとは検証範囲が変わります。
まとめ
ベイズ最適化は、平均予測だけでなく不確かさを使い、次の実験を選ぶ方法です。EI、PI、UCBは評価するものが違うため、目的・ノイズ・実験予算に合わせて選びます。まず有限の実行可能候補から1条件を提案し、実測値を追加して再計算する流れを再現してください。
実務で必要なのは、提案値を信じることではなく、入力、学習警告、測定品質、採用理由、確認実験を記録して判断を改善することです。合成データの成功例と実材料での効果を分け、最良条件の再現性まで確かめて、探索結果を次の開発判断へつなげます。
参考文献
[1]scikit-learn developers. Gaussian Processes, User Guide. 更新型公式資料、2026年10月7日閲覧。https://scikit-learn.org/stable/modules/gaussian_process.html (本コードの検証版は1.8.0)。
[2]BoTorch developers. Acquisition Function APIs. 更新型公式資料、2026年10月7日閲覧。https://botorch.readthedocs.io/en/stable/acquisition.html (ExpectedImprovement、LogExpectedImprovement、ノイズを考慮する獲得関数の説明)。
[3]Ament, S., Daulton, S., Eriksson, D., Balandat, M., Bakshy, E. Unexpected Improvements to Expected Improvement for Bayesian Optimization. 2023(参照arXiv版は2025年1月7日更新、v3). arXiv:2310.20708. https://arxiv.org/abs/2310.20708 DOI: https://doi.org/10.48550/arXiv.2310.20708
[4]Shrivastava, A., Kalaswad, M., Custer, J. O., Adams, D. P., Najm, H. N. Bayesian optimization for stable properties amid processing fluctuations in sputter deposition. 2024. Journal of Vacuum Science & Technology A, 42(3), 033408. 論文DOI:https://doi.org/10.1116/6.0003418 。参照本文はarXiv:2405.03092v1(2024年5月6日公開)。https://arxiv.org/abs/2405.03092 本文:https://arxiv.org/html/2405.03092v1 DOI: https://doi.org/10.48550/arXiv.2405.03092


