0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【メタヒューリスティクス図鑑】CMA-ES─共分散行列で探索分布を学習③

0
Posted at

この記事について

前回までは群知能(PSO・ACO)を見てきました。今回は系統の違う 進化戦略(Evolution Strategy) である、CMA-ES(Covariance Matrix Adaptation Evolution Strategy) です。

CMA-ES は「連続最適化のメタヒューリスティクスとして、実務でまず試すべき本命」と言ってよい手法です。ハイパーパラメータ調整や制御・シミュレーション最適化などで広く使われ、ベンチマークでも常に上位に来ます。少し数式は多めですが、「探索する楕円を自分で学習する」 というアイデア一本でなんとなくの概要は理解できます。

メタヒューリスティクス図鑑シリーズの記事一覧はこちら。

記事 内容
メタヒューリスティクスとは?/探索の地図
粒子群最適化(PSO)
蟻コロニー最適化(ACO)
③(本記事) CMA-ES
遺伝的アルゴリズム(GA)
Cuckoo Search
⑥〜⑪ Grey Wolf / Harris Hawks / Moth-Flame / Grasshopper / Golden Jackal / Manta Ray
まとめ:メタファー系の正体と使い分け

CMA-ESとは

2001年に Nikolaus Hansen と Andreas Ostermeier が提案しました。「進化」を名乗りますが、生物のメタファーは控えめで、中身はかなり統計的・数学的です。

N. Hansen and A. Ostermeier, "Completely derandomized self-adaptation in evolution strategies,"
Evolutionary Computation, vol. 9, no. 2, pp. 159-195, 2001.

基本の流れは「多変量正規分布から解をサンプリングし、良かった解の方へ分布を動かす」だけです。

① 平均 m を中心に、共分散 C・ステップ幅 σ の正規分布から λ 個の解をサンプリング
② 評価して良い上位 μ 個を選ぶ
③ その μ 個を使って、平均 m・共分散 C・ステップ幅 σ を更新
④ ①に戻る

PSO が「粒子の集団」を直接動かしたのに対し、CMA-ES が動かすのはサンプリング分布そのもの(楕円の中心・形・大きさ)です。ここが最大の違いです。

核心:探索の「楕円」を3つに分けて学習する

CMA-ES がサンプリングに使う正規分布 $\mathcal{N}(m, \sigma^2 C)$ は、図にすると楕円です。CMA-ES はこの楕円の3要素を別々に適応させます。

x_k \sim \underbrace{m}_{\text{中心}} + \underbrace{\sigma}_{\text{大きさ}} \cdot \mathcal{N}(0,\ \underbrace{C}_{\text{形・向き}})
要素 記号 役割 適応の仕方
平均(中心) $m$ 楕円の中心。現在の「最有力地点」 良かった上位解の重み付き平均へ移動
共分散(形・向き) $C$ 楕円の縦横比と傾き。どの方向に伸ばして探すか 良い解が並んだ方向に楕円を伸ばす
ステップ幅(大きさ) $\sigma$ 楕円全体のスケール。探索の歩幅 連続して同じ方向に進めれば大きく、行き戻りが多ければ小さく

直感的には、CMA-ES は 「良い解が見つかる方向に楕円を伸ばし、その方向へ大股で進む」 ことを毎世代やっています。谷が斜めに走っていても、共分散 $C$ がその傾きを学習して楕円を斜めに傾けてくれるので、効率よく谷を下れるわけです。

ステップ幅 σ の適応(進化パス)

3要素の中でも、ステップ幅 $\sigma$ の適応は CMA-ES の肝です。進化パスという「過去の移動の累積ベクトル」を見て、

  • 平均がずっと同じ向きに進み続けている → まだ遠い → $\sigma$ を大きく(大股に)
  • 平均が行ったり来たりしている → もう近い → $\sigma$ を小さく(慎重に)

と調整します。「最近の進み方の一貫性」で歩幅を決める、というのは部屋でなくしたスマホを探すような感覚と似ていて、よくできているなと思います。

数式の全体(重み係数 $c_c, c_\sigma, c_1, c_\mu$ など)は標準実装に譲りますが、やりたいことは「楕円の中心・形・大きさを、良かった解の履歴から学習する」だけだと押さえておけば十分です。

Python実装

標準的な (μ/μ_w, λ)-CMA-ES を実装します。少し長いですが、コメントの①〜③が上の擬似コードに対応しています。

import numpy as np

def cma_es(func, x0, sigma0=2.0, max_iter=120, seed=0, lam=None):
    rng = np.random.default_rng(seed)
    N = len(x0)
    xmean = np.array(x0, dtype=float)
    sigma = sigma0

    # --- 選択に関するパラメータ ---
    if lam is None:
        lam = 4 + int(3 * np.log(N))          # 個体数 λ
    mu = lam // 2                             # 親個体数 μ
    weights = np.log(mu + 0.5) - np.log(np.arange(1, mu + 1))
    weights /= weights.sum()                  # 上位ほど大きい重み
    mueff = 1.0 / np.sum(weights ** 2)

    # --- 適応の学習率(論文の標準設定)---
    cc = (4 + mueff / N) / (N + 4 + 2 * mueff / N)
    cs = (mueff + 2) / (N + mueff + 5)
    c1 = 2 / ((N + 1.3) ** 2 + mueff)
    cmu = min(1 - c1, 2 * (mueff - 2 + 1 / mueff) / ((N + 2) ** 2 + mueff))
    damps = 1 + 2 * max(0, np.sqrt((mueff - 1) / (N + 1)) - 1) + cs

    pc = np.zeros(N); ps = np.zeros(N)        # 進化パス
    B = np.eye(N); Dm = np.ones(N); C = np.eye(N)
    invsqrtC = np.eye(N)
    chiN = N ** 0.5 * (1 - 1 / (4 * N) + 1 / (21 * N ** 2))
    counteval = 0; eigeneval = 0
    history = []

    for it in range(max_iter):
        # ① 正規分布からサンプリング: x = m + σ·C^{1/2}·z
        Z = rng.standard_normal((lam, N))
        Y = (B @ (Dm[:, None] * Z.T)).T
        Xpop = xmean + sigma * Y
        counteval += lam
        fitness = np.array([func(x) for x in Xpop])
        idx = np.argsort(fitness)             # ② 評価して上位を選ぶ
        history.append(fitness[idx[0]])

        # ③-a 平均の更新(上位 μ 個の重み付き平均)
        xold = xmean.copy()
        xmean = weights @ Xpop[idx[:mu]]

        # ③-b 進化パスの更新
        ps = (1 - cs) * ps + np.sqrt(cs * (2 - cs) * mueff) * invsqrtC @ (xmean - xold) / sigma
        hsig = (np.linalg.norm(ps) / np.sqrt(1 - (1 - cs) ** (2 * counteval / lam)) / chiN
                < 1.4 + 2 / (N + 1))
        pc = (1 - cc) * pc + hsig * np.sqrt(cc * (2 - cc) * mueff) * (xmean - xold) / sigma

        # ③-c 共分散行列の更新(rank-1 + rank-μ)
        artmp = (Xpop[idx[:mu]] - xold) / sigma
        C = ((1 - c1 - cmu) * C
             + c1 * (np.outer(pc, pc) + (1 - hsig) * cc * (2 - cc) * C)
             + cmu * (artmp.T * weights) @ artmp)

        # ③-d ステップ幅の更新
        sigma *= np.exp((cs / damps) * (np.linalg.norm(ps) / chiN - 1))

        # 共分散行列の固有分解(C^{1/2} を作るため、たまに実行)
        if counteval - eigeneval > lam / (c1 + cmu) / N / 10:
            eigeneval = counteval
            C = np.triu(C) + np.triu(C, 1).T
            eigval, B = np.linalg.eigh(C)
            Dm = np.sqrt(np.maximum(eigval, 1e-20))
            invsqrtC = B @ np.diag(1 / Dm) @ B.T

    return xmean, func(xmean), history

⓪の Rastrigin 関数を、初期点 $(4,4)$・初期ステップ幅 $\sigma=2$ で動かします。

xbest, fbest, history = cma_es(rastrigin, x0=[4.0, 4.0], seed=1)
print(f'CMA-ES 最良解  x = [{xbest[0]:.4f}, {xbest[1]:.4f}]')
print(f'CMA-ES 最良値  f = {fbest:.6f}')
print(f'初期世代の最良値 f = {history[0]:.4f}  →  最終 f = {history[-1]:.6f}')
CMA-ES 最良解  x = [0.9950, -1.9899]
CMA-ES 最良値  f = 4.974790
初期世代の最良値 f = 41.0305  →  最終 f = 4.974790

…おや、$f \approx 4.97$ で止まってしまいました。大域最適 $f=0$ ではなく、近くの局所最適にハマっています。実はこれ、CMA-ES の重要な性質を表しているので、隠さず掘り下げます。

探索分布が収束していく様子

白い楕円が、各世代のサンプリング分布です。最初は大きく広がっていた楕円が、世代を追うごとに縮みながら1点へ収束していくのがわかります。

20260801_cmaes_ellipses.png

  • 0〜10世代:楕円が大きく、広い範囲をサンプリング(探索フェーズ)
  • 30〜60世代:良い領域を見つけて楕円が縮む(ステップ幅 $\sigma$ が小さくなる)
  • 119世代:1点に収束。ただし収束先は大域最適(赤い星)ではなく、手近な局所最適

収束曲線でも、$f \approx 5$ で頭打ちになっている様子が見えます。

20260801_cmaes_convergence.png

局所最適にハマったときの対処:集団サイズを上げる

これは CMA-ES の弱点ではなく、「CMA-ES は基本的に局所最適化器であり、多峰性関数には工夫が要る」 という性質です。① の PSO が群れの多様性で大域最適を引けたのに対し、CMA-ES は分布を1つしか持たないため、近くの谷に素直に収束してしまうわけです。

対処の定番は 集団サイズ $\lambda$ を大きくすることです。サンプリング数が増えれば、遠くの良い谷を引き当てる確率が上がります。$\lambda=50$ にして、乱数シードを変えて5回試します。

for s in range(5):
    xb, fb, _ = cma_es(rastrigin, x0=[4.0, 4.0], seed=s, lam=50)
    print(f'seed={s}: 最良値 f = {fb:.6f}  x = [{xb[0]:.4f}, {xb[1]:.4f}]')
seed=0: 最良値 f = 0.994959  x = [0.9950, -0.0000]
seed=1: 最良値 f = 0.000000  x = [-0.0000, 0.0000]
seed=2: 最良値 f = 0.000000  x = [0.0000, 0.0000]
seed=3: 最良値 f = 0.000000  x = [0.0000, -0.0000]
seed=4: 最良値 f = 0.000000  x = [0.0000, 0.0000]

5回中4回が大域最適 $f=0$ に到達しました。実務向けの IPOP-CMA-ES は、この発想をさらに進めて「収束したら集団サイズを倍にして再スタート」を自動で繰り返し、多峰性関数にも強くしたものです。

他手法との比較

CMA-ES PSO(①) GA(④)
動かす対象 サンプリング分布(楕円の中心・形・大きさ) 粒子の集団 染色体の集団
変数間の相関 共分散行列 $C$ で自動学習(斜めの谷に強い) 考慮しない 基本は考慮しない
パラメータ調整 ほぼ不要(初期点と $\sigma$ だけ) $w, c_1, c_2$ を要調整 多い
多峰性への弱さ 弱い(要リスタート) 比較的強い 比較的強い
得意 中次元・連続・滑らかでない関数 連続全般 離散・順列も可

CMA-ES の決定的な強みは、共分散行列が変数間の相関(谷の傾き)を自動で学習することと、ユーザーがほとんどパラメータを触らなくていいことです。「とりあえず連続最適化で何か強いやつを」というとき、まず CMA-ES を試すのは理にかなっています。

まとめ

  • CMA-ES は、多変量正規分布(楕円)からサンプリングし、良い解の方へ分布を更新する手法
  • 動かすのは粒子ではなく、サンプリング分布の中心 $m$・形 $C$・大きさ $\sigma$ の3要素
  • 共分散行列 $C$ が変数間の相関を学習するので、斜めに走る谷でも効率よく下れる
  • パラメータ調整がほぼ不要で、連続最適化の実務で本命級
  • 一方で基本は局所最適化器。多峰性には集団サイズ増やリスタート(IPOP-CMA-ES)が有効
  • ⓪の No Free Lunch のとおり、問題(多峰性)によっては素直にハマってしまう

次回は進化計算のもう一方の柱、遺伝的アルゴリズム(GA) を改めて連続最適化の視点から扱います(TSP 版は配送最適化入門⑥を参照)。

参考にした記事や本、論文等

  • N. Hansen and A. Ostermeier, "Completely derandomized self-adaptation in evolution strategies," Evolutionary Computation, vol. 9, no. 2, pp. 159-195, 2001.
  • N. Hansen, "The CMA Evolution Strategy: A Tutorial," arXiv:1604.00772, 2016.
  • A. Auger and N. Hansen, "A restart CMA evolution strategy with increasing population size (IPOP-CMA-ES)," Proc. IEEE CEC, pp. 1769-1776, 2005.
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?