焼きなまし法(Simulated Annealing)の仕組みとPython実装

焼きなまし法の温度スケジュール・遷移確率の数理から、連続最適化と巡回セールスマン問題(TSP)へのPython実装例を紹介します。

はじめに

焼きなまし法(Simulated Annealing, SA)は、金属の焼きなまし(アニーリング)プロセスに着想を得たメタヒューリスティクス最適化手法です。高温から徐々に冷却することで、金属の結晶構造が最低エネルギー状態に向かうように、目的関数の大域的最適解を探索します。

SAの特徴は、改悪解を確率的に受容することで局所最適から脱出できる点です。温度が高い序盤では大胆な探索を行い、温度が低下するにつれて精緻な局所探索に移行します。

アルゴリズム

メトロポリス基準

SAの核となるのはメトロポリス基準です。現在の解 \(x\) から近傍解 \(x'\) への遷移を以下の確率で受容します。

\[P(\text{accept}) = \begin{cases} 1 & \text{if } \Delta E \leq 0 \\ \exp\left(-\frac{\Delta E}{T}\right) & \text{if } \Delta E > 0 \end{cases} \tag{1}\]

ここで \(\Delta E = f(x') - f(x)\) は目的関数値の変化量、\(T\) は現在の温度です。

  • \(\Delta E \leq 0\)(改善): 常に受容
  • \(\Delta E > 0\)(改悪): 温度 \(T\) に依存する確率で受容。\(T\) が高いほど受容確率が高い

ボルツマン分布との関係

温度 \(T\) で十分長い時間探索を続けると、解の分布はボルツマン分布に収束します。

\[P(x) \propto \exp\left(-\frac{f(x)}{T}\right) \tag{2}\]

\(T \to 0\) では最適解に集中します。

温度スケジュール

幾何冷却(最も一般的)

\[T_{k+1} = \alpha \cdot T_k, \quad 0.9 \leq \alpha < 1 \tag{3}\]

\(\alpha = 0.95\) が典型的な値です。

線形冷却

\[T_{k+1} = T_k - \delta \tag{4}\]

対数冷却(理論的保証あり)

\[T_k = \frac{T_0}{\ln(k + 1)} \tag{5}\]

大域最適解への収束が理論的に保証されますが、収束速度は極めて遅く実用的ではありません。

Python実装:連続最適化

Rastrigin関数(多峰性ベンチマーク)を焼きなまし法で最適化します。

import numpy as np
import matplotlib.pyplot as plt

def rastrigin(x):
    """Rastrigin関数"""
    return 10 * len(x) + np.sum(x**2 - 10 * np.cos(2 * np.pi * x))

def simulated_annealing(objective, dim, bounds=(-5.12, 5.12),
                        T_init=100.0, alpha=0.995, max_iter=10000):
    """焼きなまし法による連続最適化"""
    # 初期解
    x = np.random.uniform(*bounds, size=dim)
    best_x = x.copy()
    best_f = objective(x)
    current_f = best_f

    T = T_init
    history = [best_f]

    for i in range(max_iter):
        # 近傍解の生成(ガウスノイズ)
        sigma = 0.5 * (T / T_init)  # 温度に比例してステップ幅を調整
        x_new = x + np.random.normal(0, sigma, size=dim)
        x_new = np.clip(x_new, *bounds)

        new_f = objective(x_new)
        delta = new_f - current_f

        # メトロポリス基準
        if delta <= 0 or np.random.random() < np.exp(-delta / T):
            x = x_new
            current_f = new_f

            if current_f < best_f:
                best_x = x.copy()
                best_f = current_f

        T *= alpha
        history.append(best_f)

    return best_x, best_f, history

# --- 実行 ---
np.random.seed(42)
best_x, best_f, history = simulated_annealing(rastrigin, dim=10)

print(f"最良解の適応度: {best_f:.6f}")

# --- 収束曲線のプロット ---
plt.figure(figsize=(10, 5))
plt.plot(history)
plt.xlabel('Iteration')
plt.ylabel('Best Objective Value')
plt.title('Simulated Annealing on Rastrigin Function (10D)')
plt.yscale('log')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Python実装:巡回セールスマン問題(TSP)

SAは組合せ最適化にも有効です。TSPを2-opt近傍で解きます。

import numpy as np
import matplotlib.pyplot as plt

def total_distance(tour, cities):
    """巡回路の総距離を計算"""
    n = len(tour)
    dist = 0
    for i in range(n):
        dist += np.linalg.norm(cities[tour[i]] - cities[tour[(i + 1) % n]])
    return dist

def two_opt_swap(tour, i, j):
    """2-optスワップ: tour[i:j+1]を反転"""
    new_tour = tour.copy()
    new_tour[i:j+1] = tour[i:j+1][::-1]
    return new_tour

def sa_tsp(cities, T_init=1000.0, alpha=0.9995, max_iter=100000):
    """焼きなまし法によるTSP"""
    n = len(cities)
    tour = list(range(n))
    np.random.shuffle(tour)

    current_dist = total_distance(tour, cities)
    best_tour = tour.copy()
    best_dist = current_dist
    T = T_init
    history = [best_dist]

    for _ in range(max_iter):
        # ランダムな2-optスワップ
        i, j = sorted(np.random.choice(n, 2, replace=False))
        new_tour = two_opt_swap(tour, i, j)
        new_dist = total_distance(new_tour, cities)

        delta = new_dist - current_dist
        if delta <= 0 or np.random.random() < np.exp(-delta / T):
            tour = new_tour
            current_dist = new_dist
            if current_dist < best_dist:
                best_tour = tour.copy()
                best_dist = current_dist

        T *= alpha
        history.append(best_dist)

    return best_tour, best_dist, history

# --- 実行 ---
np.random.seed(42)
n_cities = 30
cities = np.random.rand(n_cities, 2) * 100

best_tour, best_dist, history = sa_tsp(cities)
print(f"最短距離: {best_dist:.2f}")

# --- 結果の可視化 ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 巡回路
tour_cities = cities[best_tour + [best_tour[0]]]
axes[0].plot(tour_cities[:, 0], tour_cities[:, 1], 'b-o', markersize=5)
axes[0].set_title(f'Best Tour (distance={best_dist:.2f})')
axes[0].set_aspect('equal')
axes[0].grid(True, alpha=0.3)

# 収束曲線
axes[1].plot(history)
axes[1].set_xlabel('Iteration')
axes[1].set_ylabel('Best Distance')
axes[1].set_title('SA Convergence on TSP')
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

他の最適化手法との比較

特性SAGACEM
解の数単一解集団ベース集団ベース
探索メカニズム確率的受容交叉・突然変異分布更新
離散問題得意得意不得意
パラメータ温度スケジュール交叉率・突然変異率エリート割合
並列化困難容易容易

関連記事

参考文献

  • Kirkpatrick, S., Gelatt, C. D., & Vecchi, M. P. (1983). “Optimization by Simulated Annealing”. Science, 220(4598), 671-680.
  • Metropolis, N., et al. (1953). “Equation of State Calculations by Fast Computing Machines”. The Journal of Chemical Physics, 21(6), 1087-1092.
  • Bertsimas, D., & Tsitsiklis, J. (1993). “Simulated annealing”. Statistical Science, 8(1), 10-15.