メインコンテンツまでスキップ

焼きなまし法(Simulated Annealing)とは?

概要 (画像は、Geminiで作成されたものです)

焼きなまし法(Simulated Annealing)の概要

焼きなまし法(Simulated Annealing:SA) は、組み合わせ最適化問題において、実用上十分な「良い解」を高速に見つけ出すための強力なメタヒューリスティクス(近似解法)の一つです。

局所探索(山登り法)で解説したように、配送ルート最適化などの実社会の課題では、すべてのパターンを計算する「全探索」では現実的な時間内に解を得られないという課題があります。この課題を解決する基礎的な手法として、現在の解の近傍を探索して改善を繰り返す「山登り法(局所探索)」が存在します[cite: 1]。

しかし、山登り法には決定的な弱点がありました。それは、常に評価が改善する方向へしか進まないため、小さな山の頂上である局所的最適解(Local Optimum) に到達すると、遠くにもっと高い山(大域的最適解)があっても探索が止まってしまうという問題です。

この「局所解の罠」を克服するために考案されたのが、焼きなまし法です。その最大の特長は、探索の過程で一時的な改悪を許容するという点にあります。

この手法の名前は、金属工学における「焼きなまし(Annealing)」という熱処理に由来します。金属を高温に熱して原子を自由に動ける状態にした後、ゆっくりと時間をかけて冷却することで、原子が最も安定した結晶構造(エネルギーが最小の状態)に落ち着くという物理現象をアルゴリズムとして模倣しています。

このアルゴリズムでは、「温度(Temperature)」というパラメータを用いて解の移動確率をコントロールします。

  • 探索の序盤(高温状態): スコアが悪化する方向への移動を高い確率で許容し、探索空間をダイナミックに飛び回って局所解から脱出します。
  • 探索の終盤(低温状態): スコアが悪化する移動を次第に許容しなくなり、最終的には山登り法のように局所的な頂上へと収束していきます。

このように「最初は柔軟に広く探し、徐々に厳密に山を登る」というプロセスを経ることで、山登り法ではたどり着けなかった最も高い山の頂上(大域的最適解)を発見できる確率を飛躍的に高めているのです。

山登り法と焼きなまし法の違い(遷移確率)

両者の違いを、解を更新する際のルール(遷移確率)から見てみましょう。 最適化したい評価関数を f(x)f(x) とし、その最大化を目的とします。現在の解 xx から近傍の解 xx' へ移動する際の評価値の差分を Δf=f(x)f(x)\Delta f = f(x') - f(x) とします。

山登り法 (Hill Climbing)

局所探索(山登り法)の記事で解説した通り、山登り法は常に良くなる方向へしか進みません。

  • 更新条件: P(移動)={1(Δf>0)0(Δf0)P(\text{移動}) = \begin{cases} 1 & (\Delta f > 0) \\ 0 & (\Delta f \le 0) \end{cases} 評価が良くなる(Δf>0\Delta f > 0)なら確実に移動し、悪くなる(Δf0\Delta f \le 0)なら絶対に移動しない、という非常に貪欲な戦略です。

焼きなまし法 (Simulated Annealing)

一方、焼きなまし法ではスコアが悪化する場合でも一定の確率で移動を許容します。この判定には一般的にメトロポリス基準(Metropolis Criterion) が用いられます。

  • 更新条件: P(移動)={1(Δf>0)exp(ΔfT)(Δf0)P(\text{移動}) = \begin{cases} 1 & (\Delta f > 0) \\ \exp\left(\frac{\Delta f}{T}\right) & (\Delta f \le 0) \end{cases} ここで、TT は現在の温度(Temperature) を表すパラメータで、T>0T > 0 です。

この数式から、焼きなまし法の重要な性質が3つ見えてきます。

  1. 改悪の許容: Δf0\Delta f \le 0 (スコアが悪化)であっても、確率 exp(Δf/T)\exp(\Delta f / T) で移動を受け入れます。これが局所解から脱出する原動力となります。
  2. 温度の影響: 温度 TT が高いほど確率は 11 に近づき(改悪を許容しやすい)、TT が低いほど確率は 00 に近づきます(改悪を許容しにくい)。これにより、探索の進行度に応じた振る舞いの制御が可能になります。
  3. 改悪幅の影響: 悪化の度合い( Δf\Delta f のマイナス幅)が大きいほど、確率は指数関数的に低くなります。「少しの改悪」は受け入れやすく、「大幅な改悪」は受け入れにくいという、直感的に理解しやすい性質を持っています。

コードによる挙動の確認

次に、Pythonコードを使って、これら2つのアルゴリズムの挙動を実際に確認してみましょう。局所探索(山登り法)の記事と同じく、評価関数 f(x)=sin(x)+cos(0.5x)f(x) = \sin(x) + \cos(0.5x) の最大化問題を題材にします。

まずは準備として、この評価関数を定義し、グラフにプロットして形状を可視化します。

import math
import random
import matplotlib.pyplot as plt
import japanize_matplotlib # グラフ日本語表示用

# 評価関数(目的関数)
def f(x):
return math.sin(x) + math.cos(0.5 * x)

# 探索空間の設定
x_min, x_max = 0.0, 10.0
step_size = 0.1 # 近傍への移動距離

print("--- 準備 ---")
print(f"探索範囲: {x_min} から {x_max}")

# --- 可視化のためのデータ生成 ---
# 探索範囲内のx値のリストを作成
x_vals = [i * step_size for i in range(int(x_max / step_size) + 1)]

# 各xに対応する評価関数f(x)の値を計算
y_vals = [f(x) for x in x_vals]

# グラフの描画
plt.figure(figsize=(10, 6))
plt.plot(x_vals, y_vals, color='#1f77b4', linewidth=2, label='目的関数 f(x)')
plt.title("評価関数 f(x) = sin(x) + cos(0.5x) の形状", fontsize=14, fontweight='bold')
plt.xlabel("変数 x", fontsize=12)
plt.ylabel("評価値 f(x)", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()

実行結果

--- 準備 ---
探索範囲: 0.0 から 10.0

評価関数

このプロットからわかるように、評価関数 f(x)f(x) には複数の山が存在します。 具体的には、x1.7x \approx 1.7 付近に最も高い大域的最適解があり、x8.3x \approx 8.3 付近にそれよりは低い局所的最適解が存在します。 ここからは、山登り法と焼きなまし法が、この関数をどのように探索していくかを見ていきましょう。

山登り法による探索(失敗例)

最初に、山登り法で探索します。意図的に局所解に陥りやすいよう、初期値を 8.0 に設定して探索を開始します。

# 初期解(局所解に陥りやすいスタート地点として 8.0 に設定)
current_x_hc = 8.0

while True:
# 近傍の生成:現在の位置から左右にステップ幅だけ移動した2つの候補点
neighbors = [current_x_hc - step_size, current_x_hc + step_size]

# 探索空間の範囲内に収まっている候補点のみにフィルタリング
valid_neighbors = [nx for nx in neighbors if x_min <= nx <= x_max]

# 暫定の最良候補を現在の位置に設定
best_neighbor = current_x_hc
best_neighbor_score = f(current_x_hc)

# 有効な近傍候補点を順番に評価
for nx in valid_neighbors:
score = f(nx)
# より良いスコアが見つかれば最良近傍候補を更新
if score > best_neighbor_score:
best_neighbor_score = score
best_neighbor = nx

# 近傍のどれを探索しても改善しない場合はループを終了
if best_neighbor_score <= f(current_x_hc):
break

# 現在地を最良近傍に更新
current_x_hc = best_neighbor

print(f"山登り法の解: x = {current_x_hc:.2f}, スコア = {f(current_x_hc):.4f}")
print("❌ 課題: スタート地点によっては小さな山の頂上で止まってしまう。")

このコードは、current_x_hc を初期位置 8.0 としてスタートし、while True ループの中で現在位置の左右の点(neighbors)を評価します。そして、最もスコアが高い点を best_neighbor として見つけ、そこに移動します。もし、どの近傍点も現在のスコアを上回らなければ、それ以上改善できないため break してループを終了します。

実行結果

山登り法の解: x = 8.30, スコア = 0.3690
❌ 課題: スタート地点によっては小さな山の頂上で止まってしまう。

この結果が示すように、探索は x8.3x \approx 8.3 の地点で終了しました。これはグラフ上の小さな山(局所的最適解)の頂上であり、より高い山(大域的最適解, x1.7x \approx 1.7)には到達できていません。このように、スタート地点によっては最良の解を見つけられないのが山登り法の課題です。

焼きなまし法による探索

次に、焼きなまし法を使って、この「局所解の罠」から脱出できるか試してみましょう。山登り法が失敗したのと同じ 8.0 からスタートします。

# --- 焼きなまし法(SA)のパラメータ設定 ---
initial_temp = 10.0 # 初期温度:高いほど改悪を許容する
final_temp = 0.01 # 終了温度:この温度を下回ったら探索終了
cooling_rate = 0.99 # 冷却率:1ループごとに温度に掛ける値

# 初期解(山登り法が失敗した「8.0」をスタート地点とする)
current_x = 8.0
current_score = f(current_x)

# 最良解の保持用(探索中に見つけた一番良いスコアを記憶しておく)
best_x = current_x
best_score = current_score

# 温度の初期化と乱数シードの固定(再現性のため)
T = initial_temp
random.seed(1)

print("--- 焼きなまし法の探索開始 ---")

# 温度が終了温度を下回るまでループ
while T > final_temp:
# 近傍の生成:現在位置からランダムに少し移動した地点を次の候補とする
nx = current_x + random.uniform(-step_size, step_size)

# 探索範囲内に収める(はみ出したら境界の値にする)
nx = max(x_min, min(nx, x_max))

# 候補地点のスコアを計算し、差分を求める
next_score = f(nx)
delta_f = next_score - current_score

# 遷移判定(メトロポリス基準)
if delta_f > 0 or random.random() < math.exp(delta_f / T):
# 現在位置とスコアを更新(移動を受け入れる)
current_x = nx
current_score = next_score

# これまでのベストスコアを上回っていれば記録を更新
if current_score > best_score:
best_score = current_score
best_x = current_x

# 温度を下げる(冷却)
T *= cooling_rate

print(f"焼きなまし法の最適解: x = {best_x:.2f}, スコア = {best_score:.4f}")
print("⭕ 利点: 一時的な改悪を許容することで、局所解から脱出し、大域的最適解に到達できる。")

この実装では、initial_temp(初期温度)や cooling_rate(冷却率)といったパラメータを設定します。while T > final_temp: ループ内で、次の候補手 nx をランダムに生成し、スコアが悪化した場合でも、確率計算(メトロポリス基準)によって移動を受け入れるかどうかを決定します。ループの最後には T *= cooling_rate で温度を下げ、徐々に改悪を許容しにくくしていきます。

実行結果

--- 焼きなまし法の探索開始 ---
焼きなまし法の最適解: x = 1.27, スコア = 1.7602
⭕ 利点: 一時的な改悪を許容することで、局所解から脱出し、大域的最適解に到達できる。

結果は明らかです。山登り法では局所解に捕らわれたのに対し、焼きなまし法は大域的最適解に極めて近い x=1.27x = 1.27 を発見できました。

探索軌跡の可視化

最後に、焼きなまし法がどのように「谷を下り、より高い山を目指したのか」、その探索の軌跡をグラフで確認してみましょう。

import numpy as np

# NumPy用の評価関数の定義
def f_np(x):
return np.sin(x) + np.cos(0.5 * x)

# グラフ描画用の背景データ生成
x_vals_np = np.linspace(0, 10, 200)
y_vals_np = f_np(x_vals_np)

# --- 焼きなまし法の軌跡シミュレーション ---
T = initial_temp
current_x_path = 8.0
path_x = [current_x_path] # 軌跡を保存するリスト

random.seed(1)
while T > final_temp:
nx = current_x_path + random.uniform(-step_size, step_size)
nx = max(x_min, min(nx, x_max))

delta_f = f(nx) - f(current_x_path)
if delta_f > 0 or random.random() < math.exp(delta_f / T):
current_x_path = nx

path_x.append(current_x_path) # 現在位置を記録
T *= cooling_rate

# 軌跡のy座標を計算
path_y = [f_np(x) for x in path_x]

# --- グラフの描画 ---
plt.figure(figsize=(10, 6))
plt.plot(x_vals_np, y_vals_np, color='#1f77b4', linewidth=2, label='目的関数 f(x)')
plt.plot(path_x, path_y, marker='o', color='orange', markersize=4, linestyle='-', alpha=0.3, label='焼きなまし法の軌跡')
plt.plot(path_x[0], path_y[0], marker='s', color='black', markersize=8, label='スタート地点')
plt.plot(path_x[-1], path_y[-1], marker='*', color='red', markersize=12, label='最終到達地点')

plt.title("焼きなまし法の探索プロセス(局所解からの脱出)", fontsize=14, fontweight='bold')
plt.xlabel("変数 x", fontsize=12)
plt.ylabel("評価値 f(x)", fontsize=12)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.show()

このコードは、探索の各ステップでの位置を path_x リストに記録し、最終的にMatplotlibでその軌跡をプロットするものです。スタート地点、軌跡、最終到達地点を重ねて描画することで、探索のダイナミックな動きを視覚的に捉えることができます。

実行結果

探索軌跡

山登り法の探索軌跡と比較すると、その違いは一目瞭然です。

  • 序盤(高温期): スタート直後、グラフのオレンジ色の線は評価が下がる谷底へ向かうなど、大きくダイナミックに動いています。これにより、x8.3x \approx 8.3 の局所解に捕らわれずに谷を越えることができています。
  • 終盤(低温期): グラフの左側にある最も高い山(x1.7x \approx 1.7付近)に到達した後は、温度が下がっているため改悪を許容しにくくなり、山登り法のように山の頂上付近に収束していきます。

まとめ

本記事では、山登り法の弱点を克服する強力なメタヒューリスティクスである「焼きなまし法(Simulated Annealing)」について、その理論と実践を解説しました。

  • 確率的な改悪の許容: 焼きなまし法は、メトロポリス基準に基づいて一時的にスコアが悪化する移動を受け入れることで、山登り法が陥りがちな局所的最適解から脱出します。
  • 温度による探索制御: 探索序盤は「高温」で大胆に広い範囲を探り、終盤は「低温」で厳密に最適解を絞り込むという、物理現象を模倣した洗練されたアルゴリズムです。

焼きなまし法は非常に強力ですが、一方で「初期温度」「終了温度」「冷却率」といったハイパーパラメータの設定が性能を大きく左右するという側面も持ちます。これらのパラメータを問題の性質に合わせて慎重にチューニングすることが、良い解を得るための鍵となります。

本記事の文章・構成の一部に生成AIを使用しています。