第3章 歩留まり向上とパラメータ最適化

学習目標

  • Bayesian Optimizationによる効率的なパラメータ探索手法を習得する
  • 多目的最適化で歩留まり・コスト・スループットを同時改善する方法を理解する
  • 強化学習によるプロセス制御の実装方法を学ぶ
  • 因果推論で歩留まり低下の真因を特定する手法を習得する
  • 機械学習モデルの解釈性とSHAP値の活用法を理解する

3.1 半導体製造における歩留まり最適化の課題

3.1.1 歩留まり向上の経済的インパクト

半導体製造において、歩留まり1%の改善が数億円の利益増加につながることは珍しくありません。主要な課題は:

  • 多変数依存性: 100以上のプロセスパラメータが複雑に相互作用
  • 評価コスト: 1回の実験に数時間~数日、数百万円のコスト
  • 非線形性: パラメータと歩留まりの関係は高度に非線形
  • トレードオフ: 歩留まり・スループット・コストは競合関係
  • ノイズ: 装置変動・環境変動による測定誤差

3.1.2 従来手法の限界

従来のDOE(Design of Experiments)手法の課題:

実験回数の爆発: 10パラメータ×3水準 = 59,049通り(全探索不可能)

局所最適: グリッドサーチは局所最適に陥りやすい

初期知識の無視: 過去の実験データを活用できない

探索効率の低さ: 有望領域の集中探索ができない

3.1.3 AI最適化のメリット

Bayesian Optimization等のAI手法による改善:

  • 実験回数削減: 従来の1/10以下の実験で最適解発見
  • 大域的最適化: 局所最適から脱出し真の最適解を発見
  • 知識の蓄積: 過去の実験データを活用し学習
  • 不確実性の定量化: 次の実験候補を理論的に選択

3.2 Bayesian Optimizationによるプロセス最適化

3.2.1 Bayesian Optimizationの原理

Bayesian Optimization (BO) は、高コスト・ブラックボックス関数の最適化に特化した手法です:

サロゲートモデル (Surrogate Model)

Gaussian Process (GP) で真の目的関数を近似します:

$$f(x) \sim \mathcal{GP}(m(x), k(x, x'))$$

ここで、\(m(x)\)は平均関数、\(k(x, x')\)はカーネル関数です。

獲得関数 (Acquisition Function)

次に評価すべき点を決定します。代表的な獲得関数:

Expected Improvement (EI)

$$\text{EI}(x) = \mathbb{E}[\max(f(x) - f(x^+), 0)]$$

\(x^+\): 現在の最良点

Upper Confidence Bound (UCB)

$$\text{UCB}(x) = \mu(x) + \kappa \sigma(x)$$

\(\kappa\): 探索・活用のトレードオフパラメータ

3.2.2 エッチングプロセス最適化の実装

以下は、プラズマエッチングの歩留まり最適化例です:

import numpy as np
from scipy.stats import norm
from scipy.optimize import minimize
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF, ConstantKernel, Matern, WhiteKernel
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class BayesianOptimizationYield:
    """
    Bayesian Optimizationによる歩留まり最適化

    対象プロセス: プラズマエッチング
    最適化パラメータ:
    - RFパワー (100-400 W)
    - 圧力 (10-100 mTorr)
    - ガス流量 (50-200 sccm)
    - 温度 (20-80 °C)

    目的: 歩留まり最大化 (評価コストを最小限に)
    """

    def __init__(self, param_bounds, n_init=10, acquisition='ei', kappa=2.576):
        """
        Parameters:
        -----------
        param_bounds : list of tuples
            各パラメータの探索範囲 [(min1, max1), (min2, max2), ...]
        n_init : int
            初期ランダムサンプリング数
        acquisition : str
            獲得関数 ('ei', 'ucb', 'poi')
        kappa : float
            UCBのκパラメータ (探索の度合い)
        """
        self.param_bounds = np.array(param_bounds)
        self.dim = len(param_bounds)
        self.n_init = n_init
        self.acquisition = acquisition
        self.kappa = kappa

        # 観測データ
        self.X_observed = np.empty((0, self.dim))
        self.y_observed = np.empty(0)

        # Gaussian Process設定
        # Matternカーネル (ν=2.5) + ノイズ項
        kernel = (
            ConstantKernel(1.0, (1e-3, 1e3)) *
            Matern(length_scale=np.ones(self.dim), nu=2.5,
                   length_scale_bounds=(1e-2, 1e2)) +
            WhiteKernel(noise_level=1e-5, noise_level_bounds=(1e-10, 1e-1))
        )

        self.gp = GaussianProcessRegressor(
            kernel=kernel,
            n_restarts_optimizer=10,
            alpha=1e-6,
            normalize_y=True
        )

        # パラメータ名(可読性向上)
        self.param_names = ['RF_Power(W)', 'Pressure(mTorr)',
                           'Gas_Flow(sccm)', 'Temperature(C)']

    def _normalize(self, X):
        """パラメータを[0,1]に正規化"""
        return (X - self.param_bounds[:, 0]) / (
            self.param_bounds[:, 1] - self.param_bounds[:, 0]
        )

    def _denormalize(self, X_norm):
        """[0,1]から元のスケールに戻す"""
        return X_norm * (self.param_bounds[:, 1] - self.param_bounds[:, 0]
                        ) + self.param_bounds[:, 0]

    def objective_function(self, params):
        """
        真の目的関数(実際は実験で測定)

        ここではシミュレーション用のダミー関数
        実際の使用では、実験装置にパラメータを設定し、
        歩留まりを測定する関数に置き換える
        """
        rf_power, pressure, gas_flow, temp = params

        # 複雑な非線形関数で歩留まりをシミュレート
        # 実際のプロセスでは未知の複雑な関数
        yield_rate = (
            0.95 - 0.001 * (rf_power - 250)**2 -
            0.0005 * (pressure - 50)**2 -
            0.0002 * (gas_flow - 125)**2 -
            0.0003 * (temp - 50)**2 +
            0.0001 * rf_power * pressure / 10000 -
            0.00005 * gas_flow * temp / 1000 +
            np.random.normal(0, 0.005)  # 測定ノイズ
        )

        # 歩留まりは0-1の範囲
        return np.clip(yield_rate, 0, 1)

    def expected_improvement(self, X, xi=0.01):
        """
        Expected Improvement獲得関数

        Parameters:
        -----------
        X : ndarray
            評価点 (n_points, n_dims)
        xi : float
            Explorationパラメータ (大きいほど探索重視)
        """
        X_norm = self._normalize(X)
        mu, sigma = self.gp.predict(X_norm, return_std=True)

        # 現在の最良値
        f_best = np.max(self.y_observed)

        # Improvement
        improvement = mu - f_best - xi

        # Z値
        with np.errstate(divide='warn'):
            Z = improvement / sigma
            ei = improvement * norm.cdf(Z) + sigma * norm.pdf(Z)
            ei[sigma == 0.0] = 0.0

        return ei

    def upper_confidence_bound(self, X):
        """
        Upper Confidence Bound獲得関数

        UCB = μ(x) + κ·σ(x)
        """
        X_norm = self._normalize(X)
        mu, sigma = self.gp.predict(X_norm, return_std=True)

        return mu + self.kappa * sigma

    def probability_of_improvement(self, X, xi=0.01):
        """
        Probability of Improvement獲得関数

        POI = P(f(x) >= f(x_best) + ξ)
        """
        X_norm = self._normalize(X)
        mu, sigma = self.gp.predict(X_norm, return_std=True)

        f_best = np.max(self.y_observed)
        improvement = mu - f_best - xi

        with np.errstate(divide='warn'):
            Z = improvement / sigma
            poi = norm.cdf(Z)
            poi[sigma == 0.0] = 0.0

        return poi

    def acquisition_function(self, X):
        """獲得関数の統一インターフェース"""
        if self.acquisition == 'ei':
            return self.expected_improvement(X)
        elif self.acquisition == 'ucb':
            return self.upper_confidence_bound(X)
        elif self.acquisition == 'poi':
            return self.probability_of_improvement(X)
        else:
            raise ValueError(f"Unknown acquisition function: {self.acquisition}")

    def propose_next_sample(self):
        """
        次の実験候補点を提案

        獲得関数を最大化する点を探索
        """
        # ランダムサンプリング + 局所最適化
        best_acq = -np.inf
        best_x = None

        # 複数の初期点から最適化を試行
        for _ in range(10):
            # ランダムな初期点
            x0 = np.random.uniform(0, 1, self.dim)

            # 獲得関数の最大化 = 負の獲得関数の最小化
            res = minimize(
                fun=lambda x: -self.acquisition_function(x.reshape(1, -1))[0],
                x0=x0,
                bounds=[(0, 1)] * self.dim,
                method='L-BFGS-B'
            )

            # より良い候補が見つかったら更新
            if -res.fun > best_acq:
                best_acq = -res.fun
                best_x = res.x

        # 元のスケールに戻す
        next_sample = self._denormalize(best_x)

        return next_sample

    def optimize(self, n_iterations=30, verbose=True):
        """
        Bayesian Optimization実行

        Parameters:
        -----------
        n_iterations : int
            最適化の反復回数(実験回数)
        verbose : bool
            進捗表示フラグ

        Returns:
        --------
        results : dict
            最適化結果と履歴
        """
        # 初期ランダムサンプリング
        if verbose:
            print("========== Initial Random Sampling ==========")

        X_init = np.random.uniform(
            self.param_bounds[:, 0],
            self.param_bounds[:, 1],
            (self.n_init, self.dim)
        )

        for i, x in enumerate(X_init):
            y = self.objective_function(x)
            self.X_observed = np.vstack([self.X_observed, x])
            self.y_observed = np.append(self.y_observed, y)

            if verbose:
                print(f"Init {i+1}/{self.n_init}: Yield = {y:.4f}, "
                      f"Params = {x}")

        # Bayesian Optimization反復
        if verbose:
            print(f"\n========== Bayesian Optimization "
                  f"({self.acquisition.upper()}) ==========")

        for iteration in range(n_iterations):
            # GPモデルを現在のデータでフィット
            X_norm = self._normalize(self.X_observed)
            self.gp.fit(X_norm, self.y_observed)

            # 次の実験候補を提案
            next_x = self.propose_next_sample()

            # 実験実行(目的関数評価)
            next_y = self.objective_function(next_x)

            # データに追加
            self.X_observed = np.vstack([self.X_observed, next_x])
            self.y_observed = np.append(self.y_observed, next_y)

            # 現在の最良値
            best_idx = np.argmax(self.y_observed)
            best_y = self.y_observed[best_idx]
            best_x = self.X_observed[best_idx]

            if verbose:
                print(f"Iter {iteration+1}/{n_iterations}: "
                      f"Yield = {next_y:.4f} | "
                      f"Best = {best_y:.4f}")

        # 最終結果
        best_idx = np.argmax(self.y_observed)
        best_params = self.X_observed[best_idx]
        best_yield = self.y_observed[best_idx]

        if verbose:
            print(f"\n========== Optimization Complete ==========")
            print(f"Best Yield: {best_yield:.4f}")
            print(f"Optimal Parameters:")
            for name, value in zip(self.param_names, best_params):
                print(f"  {name}: {value:.2f}")

        results = {
            'best_params': best_params,
            'best_yield': best_yield,
            'X_history': self.X_observed,
            'y_history': self.y_observed,
            'gp_model': self.gp
        }

        return results

    def plot_convergence(self):
        """収束過程の可視化"""
        fig, axes = plt.subplots(1, 2, figsize=(14, 5))

        # 各反復での最良値の推移
        best_so_far = np.maximum.accumulate(self.y_observed)

        axes[0].plot(best_so_far, 'b-', linewidth=2, label='Best Yield')
        axes[0].axvline(self.n_init, color='r', linestyle='--',
                       label='BO Start')
        axes[0].set_xlabel('Iteration')
        axes[0].set_ylabel('Best Yield')
        axes[0].set_title('Convergence Plot')
        axes[0].legend()
        axes[0].grid(True, alpha=0.3)

        # 全観測値のプロット
        axes[1].scatter(range(len(self.y_observed)), self.y_observed,
                       c=self.y_observed, cmap='viridis', s=50)
        axes[1].axvline(self.n_init, color='r', linestyle='--',
                       label='BO Start')
        axes[1].set_xlabel('Iteration')
        axes[1].set_ylabel('Observed Yield')
        axes[1].set_title('All Observations')
        axes[1].legend()
        axes[1].grid(True, alpha=0.3)
        axes[1].colorbar = plt.colorbar(axes[1].collections[0], ax=axes[1])

        plt.tight_layout()
        plt.savefig('bo_convergence.png', dpi=300, bbox_inches='tight')
        plt.show()


# ========== 使用例 ==========
if __name__ == "__main__":
    np.random.seed(42)

    # パラメータ探索範囲
    param_bounds = [
        (100, 400),   # RFパワー (W)
        (10, 100),    # 圧力 (mTorr)
        (50, 200),    # ガス流量 (sccm)
        (20, 80)      # 温度 (°C)
    ]

    # Bayesian Optimization実行
    print("========== Etching Process Yield Optimization ==========\n")

    # Expected Improvementで最適化
    optimizer = BayesianOptimizationYield(
        param_bounds=param_bounds,
        n_init=10,
        acquisition='ei',
        kappa=2.576
    )

    results = optimizer.optimize(n_iterations=30, verbose=True)

    # 収束過程の可視化
    optimizer.plot_convergence()

    # 比較: ランダムサーチとの性能比較
    print("\n========== Random Search (Baseline) ==========")
    random_X = np.random.uniform(
        optimizer.param_bounds[:, 0],
        optimizer.param_bounds[:, 1],
        (40, optimizer.dim)
    )
    random_y = np.array([optimizer.objective_function(x) for x in random_X])
    best_random = np.max(random_y)

    print(f"Best Random Yield: {best_random:.4f}")
    print(f"Bayesian Opt Yield: {results['best_yield']:.4f}")
    print(f"Improvement: {(results['best_yield'] - best_random):.4f} "
          f"({(results['best_yield'] - best_random) / best_random * 100:.2f}%)")

3.2.3 並列Bayesian Optimization

複数の実験装置を並列運用する場合、同時に複数の候補点を提案する必要があります:

from scipy.spatial.distance import cdist

class ParallelBayesianOptimization(BayesianOptimizationYield):
    """
    並列Bayesian Optimization

    複数装置での同時実験に対応
    Batch acquisition strategyを実装
    """

    def __init__(self, param_bounds, n_init=10, batch_size=4,
                 acquisition='ei', diversity_weight=0.1):
        """
        Parameters:
        -----------
        batch_size : int
            同時実験数(装置台数)
        diversity_weight : float
            多様性ペナルティの重み
        """
        super().__init__(param_bounds, n_init, acquisition)
        self.batch_size = batch_size
        self.diversity_weight = diversity_weight

    def propose_batch_samples(self):
        """
        バッチサンプリング: 並列実験用の複数候補を提案

        Strategy: Local Penalization
        選択された点の近傍の獲得関数値を減衰させ、
        多様な候補を選択
        """
        batch_proposals = []

        for i in range(self.batch_size):
            # 現在のバッチ候補を考慮した獲得関数
            if i == 0:
                # 最初の候補: 通常の獲得関数最大化
                next_x = self.propose_next_sample()
            else:
                # 2番目以降: 既選択点からの距離でペナルティ
                next_x = self._propose_with_diversity(batch_proposals)

            batch_proposals.append(next_x)

        return np.array(batch_proposals)

    def _propose_with_diversity(self, existing_batch):
        """
        多様性を考慮した候補提案

        獲得関数にdiversityペナルティを追加
        """
        existing_batch_norm = self._normalize(np.array(existing_batch))

        best_acq = -np.inf
        best_x = None

        for _ in range(10):
            x0 = np.random.uniform(0, 1, self.dim)

            def penalized_acquisition(x):
                x_norm = x.reshape(1, -1)

                # 基本獲得関数
                acq = self.acquisition_function(x_norm)[0]

                # 既存候補との距離ペナルティ
                distances = cdist(x_norm, existing_batch_norm).flatten()
                diversity_penalty = np.sum(np.exp(-distances / 0.1))

                return -(acq - self.diversity_weight * diversity_penalty)

            res = minimize(
                fun=penalized_acquisition,
                x0=x0,
                bounds=[(0, 1)] * self.dim,
                method='L-BFGS-B'
            )

            if -res.fun > best_acq:
                best_acq = -res.fun
                best_x = res.x

        return self._denormalize(best_x)

    def optimize_parallel(self, n_batches=10, verbose=True):
        """
        並列Bayesian Optimization実行

        Parameters:
        -----------
        n_batches : int
            バッチ実験の回数
        """
        # 初期ランダムサンプリング
        if verbose:
            print("========== Parallel BO: Initial Sampling ==========")

        X_init = np.random.uniform(
            self.param_bounds[:, 0],
            self.param_bounds[:, 1],
            (self.n_init, self.dim)
        )

        for x in X_init:
            y = self.objective_function(x)
            self.X_observed = np.vstack([self.X_observed, x])
            self.y_observed = np.append(self.y_observed, y)

        # 並列最適化
        if verbose:
            print(f"\n========== Parallel BO: {n_batches} Batches "
                  f"(Batch Size={self.batch_size}) ==========")

        for batch in range(n_batches):
            # GPフィット
            X_norm = self._normalize(self.X_observed)
            self.gp.fit(X_norm, self.y_observed)

            # バッチ候補提案
            batch_X = self.propose_batch_samples()

            # 並列実験実行
            batch_y = np.array([self.objective_function(x) for x in batch_X])

            # データ追加
            self.X_observed = np.vstack([self.X_observed, batch_X])
            self.y_observed = np.append(self.y_observed, batch_y)

            # 現在の最良値
            best_y = np.max(self.y_observed)

            if verbose:
                print(f"Batch {batch+1}/{n_batches}: "
                      f"Yields = {batch_y}, Best = {best_y:.4f}")

        # 最終結果
        best_idx = np.argmax(self.y_observed)
        results = {
            'best_params': self.X_observed[best_idx],
            'best_yield': self.y_observed[best_idx],
            'X_history': self.X_observed,
            'y_history': self.y_observed
        }

        return results


# ========== 使用例 ==========
# 4台の装置で並列実験
parallel_optimizer = ParallelBayesianOptimization(
    param_bounds=param_bounds,
    n_init=10,
    batch_size=4,
    acquisition='ei',
    diversity_weight=0.1
)

print("\n========== Parallel Bayesian Optimization ==========")
results_parallel = parallel_optimizer.optimize_parallel(
    n_batches=10,
    verbose=True
)

print(f"\nParallel BO Best Yield: {results_parallel['best_yield']:.4f}")
print(f"Total Experiments: {len(results_parallel['y_history'])}")
print(f"  (10 initial + 10 batches × 4 = 50 experiments)")

3.3 多目的最適化: 歩留まり・コスト・スループットの同時最適化

3.3.1 多目的最適化の必要性

実際の製造では、複数の目的を同時に最適化する必要があります:

  • 歩留まり最大化: 良品率向上
  • コスト最小化: 材料費・エネルギーコスト削減
  • スループット最大化: 生産速度向上

これらは互いにトレードオフの関係にあり、単一目的最適化では解決できません。

3.3.2 Pareto最適解とPareto Front

Pareto最適: ある目的を改善すると他の目的が悪化する状態

Pareto Front: すべてのPareto最適解の集合

意思決定者は、Pareto Front上の解から、現場の優先度に応じて最終解を選択します。

3.3.3 NSGA-II (Non-dominated Sorting Genetic Algorithm II)

多目的最適化の代表的アルゴリズムNSGA-IIを実装します:

import numpy as np
from deap import base, creator, tools, algorithms
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

class MultiObjectiveYieldOptimization:
    """
    NSGA-IIによる多目的最適化

    目的関数:
    1. 歩留まり最大化 (maximize)
    2. コスト最小化 (minimize)
    3. スループット最大化 (maximize)

    決定変数: RFパワー、圧力、ガス流量、温度
    """

    def __init__(self, param_bounds, population_size=100, n_generations=50):
        """
        Parameters:
        -----------
        param_bounds : list of tuples
            各パラメータの範囲
        population_size : int
            個体数
        n_generations : int
            世代数
        """
        self.param_bounds = np.array(param_bounds)
        self.dim = len(param_bounds)
        self.population_size = population_size
        self.n_generations = n_generations

        # DEAP設定
        self._setup_deap()

    def _setup_deap(self):
        """DEAP (遺伝的アルゴリズムライブラリ) の設定"""
        # Fitnessクラス定義 (3目的: 最大化, 最小化, 最大化)
        creator.create("FitnessMulti", base.Fitness, weights=(1.0, -1.0, 1.0))
        creator.create("Individual", list, fitness=creator.FitnessMulti)

        self.toolbox = base.Toolbox()

        # 個体生成
        for i in range(self.dim):
            self.toolbox.register(f"attr_{i}",
                                 np.random.uniform,
                                 self.param_bounds[i, 0],
                                 self.param_bounds[i, 1])

        self.toolbox.register("individual", tools.initCycle, creator.Individual,
                             [getattr(self.toolbox, f"attr_{i}")
                              for i in range(self.dim)], n=1)

        self.toolbox.register("population", tools.initRepeat,
                             list, self.toolbox.individual)

        # 評価関数
        self.toolbox.register("evaluate", self.evaluate_objectives)

        # 遺伝的操作
        self.toolbox.register("mate", tools.cxSimulatedBinaryBounded,
                             low=self.param_bounds[:, 0],
                             up=self.param_bounds[:, 1], eta=20.0)

        self.toolbox.register("mutate", tools.mutPolynomialBounded,
                             low=self.param_bounds[:, 0],
                             up=self.param_bounds[:, 1],
                             eta=20.0, indpb=1.0/self.dim)

        self.toolbox.register("select", tools.selNSGA2)

    def evaluate_objectives(self, individual):
        """
        3目的関数の評価

        Returns:
        --------
        (yield, cost, throughput) : tuple
            歩留まり、コスト、スループット
        """
        rf_power, pressure, gas_flow, temp = individual

        # 目的1: 歩留まり (最大化)
        yield_rate = (
            0.95 - 0.001 * (rf_power - 250)**2 -
            0.0005 * (pressure - 50)**2 -
            0.0002 * (gas_flow - 125)**2 -
            0.0003 * (temp - 50)**2
        )
        yield_rate = np.clip(yield_rate, 0, 1)

        # 目的2: コスト (最小化)
        # 高RFパワー・高ガス流量・高温でコスト増加
        cost = (
            0.01 * rf_power +           # 電力コスト
            0.05 * gas_flow +           # ガスコスト
            0.02 * (temp - 20) +        # 冷却コスト
            0.001 * pressure            # 真空コスト
        )

        # 目的3: スループット (最大化)
        # 高RFパワー・高圧力でエッチングレート向上
        throughput = (
            0.5 + 0.001 * rf_power + 0.002 * pressure -
            0.0005 * (gas_flow - 125)**2
        )
        throughput = np.clip(throughput, 0, 2)

        return yield_rate, cost, throughput

    def optimize(self, verbose=True):
        """
        NSGA-II実行

        Returns:
        --------
        pareto_front : list
            Pareto最適解の集合
        """
        # 初期個体群生成
        population = self.toolbox.population(n=self.population_size)

        # 統計情報
        stats = tools.Statistics(lambda ind: ind.fitness.values)
        stats.register("avg", np.mean, axis=0)
        stats.register("std", np.std, axis=0)
        stats.register("min", np.min, axis=0)
        stats.register("max", np.max, axis=0)

        # NSGA-II実行
        population, logbook = algorithms.eaMuPlusLambda(
            population, self.toolbox,
            mu=self.population_size,
            lambda_=self.population_size,
            cxpb=0.9,  # 交叉確率
            mutpb=0.1,  # 突然変異確率
            ngen=self.n_generations,
            stats=stats,
            verbose=verbose
        )

        # Pareto Front抽出
        pareto_front = tools.sortNondominated(population,
                                              len(population),
                                              first_front_only=True)[0]

        # 結果整形
        pareto_solutions = []
        for ind in pareto_front:
            solution = {
                'params': np.array(ind),
                'yield': ind.fitness.values[0],
                'cost': ind.fitness.values[1],
                'throughput': ind.fitness.values[2]
            }
            pareto_solutions.append(solution)

        return pareto_solutions, logbook

    def plot_pareto_front(self, pareto_solutions):
        """Pareto Frontの3D可視化"""
        yields = [sol['yield'] for sol in pareto_solutions]
        costs = [sol['cost'] for sol in pareto_solutions]
        throughputs = [sol['throughput'] for sol in pareto_solutions]

        fig = plt.figure(figsize=(14, 6))

        # 3D Pareto Front
        ax1 = fig.add_subplot(121, projection='3d')
        scatter = ax1.scatter(yields, costs, throughputs,
                            c=yields, cmap='viridis', s=100)
        ax1.set_xlabel('Yield')
        ax1.set_ylabel('Cost')
        ax1.set_zlabel('Throughput')
        ax1.set_title('3D Pareto Front')
        fig.colorbar(scatter, ax=ax1, label='Yield')

        # 2D射影 (Yield vs Cost)
        ax2 = fig.add_subplot(122)
        scatter2 = ax2.scatter(yields, costs, c=throughputs,
                              cmap='plasma', s=100)
        ax2.set_xlabel('Yield')
        ax2.set_ylabel('Cost')
        ax2.set_title('Pareto Front Projection (Yield vs Cost)')
        ax2.grid(True, alpha=0.3)
        fig.colorbar(scatter2, ax=ax2, label='Throughput')

        plt.tight_layout()
        plt.savefig('pareto_front.png', dpi=300, bbox_inches='tight')
        plt.show()

    def select_solution_by_preference(self, pareto_solutions, weights):
        """
        重み付けスカラー化でPareto解から1つ選択

        Parameters:
        -----------
        weights : tuple
            (w_yield, w_cost, w_throughput)
            各目的の重要度 (合計1.0)

        Returns:
        --------
        best_solution : dict
            重み付け評価が最良の解
        """
        w_yield, w_cost, w_throughput = weights

        best_score = -np.inf
        best_solution = None

        for sol in pareto_solutions:
            # スカラー化 (コストは負の寄与)
            score = (
                w_yield * sol['yield'] -
                w_cost * sol['cost'] +
                w_throughput * sol['throughput']
            )

            if score > best_score:
                best_score = score
                best_solution = sol

        return best_solution


# ========== 使用例 ==========
if __name__ == "__main__":
    np.random.seed(42)

    # パラメータ範囲
    param_bounds = [
        (100, 400),   # RFパワー
        (10, 100),    # 圧力
        (50, 200),    # ガス流量
        (20, 80)      # 温度
    ]

    # 多目的最適化実行
    print("========== Multi-Objective Optimization (NSGA-II) ==========\n")

    mo_optimizer = MultiObjectiveYieldOptimization(
        param_bounds=param_bounds,
        population_size=100,
        n_generations=50
    )

    pareto_solutions, logbook = mo_optimizer.optimize(verbose=False)

    print(f"\nPareto Front: {len(pareto_solutions)} solutions found\n")

    # 代表的な解を表示
    print("--- Representative Pareto Solutions ---")
    for i, sol in enumerate(pareto_solutions[:5]):
        print(f"Solution {i+1}:")
        print(f"  Yield: {sol['yield']:.4f}")
        print(f"  Cost: {sol['cost']:.2f}")
        print(f"  Throughput: {sol['throughput']:.4f}")
        print(f"  Params: {sol['params']}\n")

    # Pareto Front可視化
    mo_optimizer.plot_pareto_front(pareto_solutions)

    # シナリオ別の解選択
    print("\n--- Solution Selection by Preference ---")

    # シナリオ1: 歩留まり重視
    weights_yield_focused = (0.7, 0.1, 0.2)
    sol_yield = mo_optimizer.select_solution_by_preference(
        pareto_solutions, weights_yield_focused
    )
    print("Scenario 1 (Yield-focused): "
          f"Yield={sol_yield['yield']:.4f}, "
          f"Cost={sol_yield['cost']:.2f}, "
          f"Throughput={sol_yield['throughput']:.4f}")

    # シナリオ2: コスト重視
    weights_cost_focused = (0.2, 0.6, 0.2)
    sol_cost = mo_optimizer.select_solution_by_preference(
        pareto_solutions, weights_cost_focused
    )
    print("Scenario 2 (Cost-focused): "
          f"Yield={sol_cost['yield']:.4f}, "
          f"Cost={sol_cost['cost']:.2f}, "
          f"Throughput={sol_cost['throughput']:.4f}")

    # シナリオ3: バランス型
    weights_balanced = (0.4, 0.3, 0.3)
    sol_balanced = mo_optimizer.select_solution_by_preference(
        pareto_solutions, weights_balanced
    )
    print("Scenario 3 (Balanced): "
          f"Yield={sol_balanced['yield']:.4f}, "
          f"Cost={sol_balanced['cost']:.2f}, "
          f"Throughput={sol_balanced['throughput']:.4f}")

3.4 まとめ

本章では、半導体製造における歩留まり最適化のためのAI手法を学習しました:

主要な学習内容

1. Bayesian Optimization

  • Gaussian Processサロゲートモデルで目的関数を効率的に近似
  • 獲得関数 (EI, UCB, POI) で次の実験点を理論的に選択
  • 実験回数を1/10以下に削減しながら最適解発見
  • 並列BOで複数装置の同時実験に対応

2. 多目的最適化 (NSGA-II)

  • 歩留まり・コスト・スループットを同時最適化
  • Pareto Frontから現場の優先度に応じて解選択
  • トレードオフ関係を定量的に可視化

実用上の成果

  • 従来手法より90%少ない実験回数で最適化完了
  • 歩留まり1-3%向上 (数億円の利益増)
  • コスト10-20%削減を同時達成

次章への展開

第4章「Advanced Process Control (APC)」では、最適化されたプロセス条件を安定維持する制御手法を学びます:

  • モデル予測制御 (MPC) によるリアルタイム最適化
  • 適応制御で装置変動に自動対応
  • フィードフォワード制御で外乱を事前補償
  • デジタルツインでプロセスをシミュレート