遺伝的アルゴリズム(GA)の基礎とPython実装

遺伝的アルゴリズム(GA)の選択・交叉・突然変異の仕組みを解説し、連続値最適化問題(Rastrigin関数)へのPython実装例を紹介します。

はじめに

遺伝的アルゴリズム(Genetic Algorithm, GA)は、生物の進化メカニズムに着想を得たメタヒューリスティクス最適化手法です。解の集団(個体群)に対して選択交叉突然変異を繰り返し適用することで、目的関数の最適解を探索します。

GAは勾配情報を必要としないブラックボックス最適化手法であり、多峰性関数や離散最適化問題にも適用可能です。

アルゴリズムの概要

GAの基本的な流れは以下のとおりです。

  1. 初期化: \(N\) 個の個体をランダムに生成
  2. 適応度評価: 各個体の目的関数値を計算
  3. 選択: 適応度の高い個体を次世代の親として選択
  4. 交叉: 親のペアから子個体を生成
  5. 突然変異: 一定確率で個体の遺伝子を変化させる
  6. 世代交代: 新しい個体群で次世代を構成
  7. ステップ2-6を収束まで繰り返す

選択(Selection)

トーナメント選択

集団から \(k\) 個体をランダムに選び、その中で最も適応度の高い個体を親として選択します。\(k\) をトーナメントサイズと呼び、大きいほど選択圧が高くなります。

\[P(\text{最良個体が選択される}) = 1 - \left(1 - \frac{1}{N}\right)^k \tag{1}\]

ルーレット選択

適応度に比例した確率で個体を選択します。個体 \(i\) の選択確率は次のとおりです。

\[P(i) = \frac{f(x_i)}{\sum_{j=1}^{N} f(x_j)} \tag{2}\]

ここで \(f(x_i)\) は個体 \(i\) の適応度です。最小化問題の場合は適応度の変換(例: \(f' = f_{\max} - f\))が必要です。

交叉(Crossover)

BLX-\(\alpha\) 交叉(実数値最適化向け)

連続値最適化では、BLX-\(\alpha\) 交叉がよく使われます。親 \(p_1, p_2\) から子を生成する際、各次元 \(d\) について次のように計算します。

\[c_d = U\left[\min(p_{1,d}, p_{2,d}) - \alpha \cdot I_d, \; \max(p_{1,d}, p_{2,d}) + \alpha \cdot I_d\right] \tag{3}\]

ここで \(I_d = |p_{1,d} - p_{2,d}|\)、\(U[a, b]\) は一様分布、\(\alpha\) は典型的に 0.5 です。

突然変異(Mutation)

ガウス突然変異

連続値最適化では、ガウスノイズを加える突然変異が一般的です。

\[x'_d = x_d + \mathcal{N}(0, \sigma^2) \tag{4}\]

\(\sigma\) は突然変異の強度を制御します。世代が進むにつれて \(\sigma\) を減少させる適応的突然変異も有効です。

Python実装:Rastrigin関数の最適化

Rastrigin関数は多峰性のベンチマーク関数で、大域的最小値は \(f(\mathbf{0}) = 0\) です。

\[f(\mathbf{x}) = 10n + \sum_{i=1}^{n} \left[x_i^2 - 10\cos(2\pi x_i)\right] \tag{5}\]
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), axis=-1)

class GeneticAlgorithm:
    def __init__(self, dim, pop_size=100, bounds=(-5.12, 5.12),
                 crossover_rate=0.8, mutation_rate=0.1, mutation_sigma=0.5,
                 tournament_size=3):
        self.dim = dim
        self.pop_size = pop_size
        self.bounds = bounds
        self.crossover_rate = crossover_rate
        self.mutation_rate = mutation_rate
        self.mutation_sigma = mutation_sigma
        self.tournament_size = tournament_size

    def initialize(self):
        """ランダムな初期集団を生成"""
        low, high = self.bounds
        return np.random.uniform(low, high, (self.pop_size, self.dim))

    def tournament_selection(self, population, fitness):
        """トーナメント選択"""
        indices = np.random.randint(0, self.pop_size, size=self.tournament_size)
        best = indices[np.argmin(fitness[indices])]
        return population[best]

    def blx_alpha_crossover(self, parent1, parent2, alpha=0.5):
        """BLX-α交叉"""
        low = np.minimum(parent1, parent2) - alpha * np.abs(parent1 - parent2)
        high = np.maximum(parent1, parent2) + alpha * np.abs(parent1 - parent2)
        child = np.random.uniform(low, high)
        return np.clip(child, *self.bounds)

    def gaussian_mutation(self, individual):
        """ガウス突然変異"""
        mask = np.random.random(self.dim) < self.mutation_rate
        noise = np.random.normal(0, self.mutation_sigma, self.dim)
        individual[mask] += noise[mask]
        return np.clip(individual, *self.bounds)

    def run(self, objective, max_generations=200):
        """GAの実行"""
        population = self.initialize()
        best_history = []

        for gen in range(max_generations):
            fitness = objective(population)
            best_idx = np.argmin(fitness)
            best_history.append(fitness[best_idx])

            # 次世代の生成
            new_population = [population[best_idx].copy()]  # エリート保存
            while len(new_population) < self.pop_size:
                p1 = self.tournament_selection(population, fitness)
                p2 = self.tournament_selection(population, fitness)
                if np.random.random() < self.crossover_rate:
                    child = self.blx_alpha_crossover(p1, p2)
                else:
                    child = p1.copy()
                child = self.gaussian_mutation(child)
                new_population.append(child)

            population = np.array(new_population[:self.pop_size])

        fitness = objective(population)
        best_idx = np.argmin(fitness)
        return population[best_idx], fitness[best_idx], best_history

# --- 実行 ---
np.random.seed(42)
ga = GeneticAlgorithm(dim=10, pop_size=200, max_generations=300)
best_solution, best_fitness, history = ga.run(rastrigin, max_generations=300)

print(f"最良解の適応度: {best_fitness:.6f}")
print(f"最良解: {best_solution[:3]}... (最初の3次元)")

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

他の最適化手法との比較

手法集団ベース勾配不要離散問題特徴
GAYesYesYes交叉による探索空間の組み合わせ的探索
CEMYesYes困難確率分布の反復更新
PSOYesYes困難速度ベースの粒子移動
SANoYesYes温度による確率的受容

関連記事

参考文献

  • Goldberg, D. E. (1989). Genetic Algorithms in Search, Optimization, and Machine Learning. Addison-Wesley.
  • Eshelman, L. J., & Schaffer, J. D. (1993). “Real-coded genetic algorithms and interval-schemata”. Foundations of Genetic Algorithms, 2, 187-202.
  • Back, T. (1996). Evolutionary Algorithms in Theory and Practice. Oxford University Press.