MPPI(Model Predictive Path Integral)の数理:クロスエントロピー法との統一的理解

Model Predictive Path Integral(MPPI)制御のアルゴリズムを、クロスエントロピー法(CEM)との比較を通じて解説し、Pythonで実装します。

MPPIとは

クロスエントロピー法(CEM)では、重点サンプリングにおけるサンプリング分布のパラメータを反復的に最適化する手法を紹介しました。

Model Predictive Path Integral(MPPI) は、確率最適制御に基づくサンプリングベースのモデル予測制御(MPC)アルゴリズムです。CEMとMPPIは、一見異なるアプローチに見えますが、実は重点サンプリング変分推論という共通の数理的枠組みで統一的に理解できます。

  • CEM: エリートサンプルの「ハード」な選択(上位 \(P\)%)
  • MPPI: コストに基づく指数関数的な「ソフト」な重み付け

参考文献:Williams, G., et al. (2017). “Information Theoretic MPC for Model-Based Reinforcement Learning.” ICRA 2017.

問題設定

離散時間の確率的力学系を考えます:

\[ \mathbf{x}_{t+1} = F(\mathbf{x}_t, \mathbf{u}_t) + \boldsymbol{\epsilon}_t, \quad \boldsymbol{\epsilon}_t \sim \mathcal{N}(\mathbf{0}, \boldsymbol{\Sigma}) \tag{1} \]

ここで \(\mathbf{x}_t\) は状態、\(\mathbf{u}_t\) は制御入力、\(\boldsymbol{\epsilon}_t\) はシステムノイズです。

制御入力列 \(\mathbf{U} = (\mathbf{u}_0, \mathbf{u}_1, \ldots, \mathbf{u}_{T-1})\) に対するコスト関数を:

\[ J(\mathbf{U}) = \phi(\mathbf{x}_T) + \sum_{t=0}^{T-1} q(\mathbf{x}_t, \mathbf{u}_t) \tag{2} \]

とします。\(\phi\) は終端コスト、\(q\) はステージコストです。

MPPIの導出

最適制御分布

確率最適制御の枠組みでは、制御列を確率変数とみなし、最適制御分布を以下のように定義します:

\[ p^*(\mathbf{V}) \propto \exp\left(-\frac{1}{\lambda} J(\mathbf{V})\right) \cdot p(\mathbf{V}) \tag{3} \]

ここで \(\mathbf{V}\) はノイズ付き制御列、\(\lambda > 0\) は温度パラメータ、\(p(\mathbf{V})\) は事前分布(現在のサンプリング分布)です。

温度 \(\lambda\) の役割:

  • \(\lambda \to 0\): 最小コストの制御列のみを選択(ハードな選択)
  • \(\lambda \to \infty\): すべての制御列を均等に重み付け

重み付き制御更新

\(N\) 個のサンプル \(\mathbf{V}^{(1)}, \ldots, \mathbf{V}^{(N)}\) を事前分布 \(p(\mathbf{V})\) から生成し、各サンプルのコスト \(J(\mathbf{V}^{(i)})\) を計算します。

重点サンプリングの重みは以下で計算されます:

\[ w^{(i)} = \frac{\exp\left(-\frac{1}{\lambda} J(\mathbf{V}^{(i)})\right)}{\sum_{j=1}^N \exp\left(-\frac{1}{\lambda} J(\mathbf{V}^{(j)})\right)} \tag{4} \]

最適制御入力は重み付き平均として求められます:

\[ \mathbf{u}_t^* = \sum_{i=1}^N w^{(i)} \mathbf{v}_t^{(i)} \tag{5} \]

CEMとの比較

CEMとMPPIは共に、現在の分布から制御列をサンプリングし、コスト情報に基づいて分布を更新するという構造を持ちます。

CEM(ハードな選択)

CEMでは上位 \(P\)% のエリートサンプルを選択し、分布パラメータを更新します:

\[ \boldsymbol{\mu}\_{\text{new}} = \frac{1}{|\mathcal{E}|} \sum_{i \in \mathcal{E}} \mathbf{V}^{(i)} \tag{6} \]\[ \boldsymbol{\Sigma}\_{\text{new}} = \frac{1}{|\mathcal{E}|} \sum_{i \in \mathcal{E}} (\mathbf{V}^{(i)} - \boldsymbol{\mu}\_{\text{new}})(\mathbf{V}^{(i)} - \boldsymbol{\mu}\_{\text{new}})^T \tag{7} \]

ここで \(\mathcal{E}\) はエリートサンプルの集合です。

MPPI(ソフトな重み付け)

MPPIでは全サンプルを指数関数的な重みで使用します:

\[ \boldsymbol{\mu}\_{\text{new}} = \sum_{i=1}^N w^{(i)} \mathbf{V}^{(i)} \tag{8} \]

統一的な理解:変分推論MPC

両手法は以下の変分推論の枠組みで統一的に理解できます:

\[ \min_q \, D(q(\mathbf{U}) \| p^*(\mathbf{U})) \tag{9} \]

ここで \(D\) はダイバージェンスの尺度です。

CEMMPPI
サンプル利用上位 \(P\)% のみ全サンプル
重み付け均等(0 or 1)指数関数的(連続的)
パラメータエリート率 \(P\)温度 \(\lambda\)
分散更新ありなし(通常)
数学的解釈ハード閾値ボルツマン分布

MPPIアルゴリズム

入力: 初期制御列 μ = (μ_0, ..., μ_{T-1}), サンプル数 N, 温度 λ, ノイズ分散 Σ
繰り返し:
  1. ノイズ付き制御列をN個サンプリング:
     ε_t^(i) ~ N(0, Σ),  V^(i) = μ + ε^(i)
  2. 各サンプルのコストを計算:
     J(V^(i)) = φ(x_T^(i)) + Σ_t q(x_t^(i), v_t^(i))
  3. 重みを計算:
     w^(i) = exp(-J(V^(i))/λ) / Σ_j exp(-J(V^(j))/λ)
  4. 制御入力を更新:
     μ_t ← μ_t + Σ_i w^(i) ε_t^(i)
  5. 最初の制御入力 μ_0 を適用し、制御列をシフト

Python実装

2次元のゴール到達問題でMPPIを実装します。

問題の定義

import numpy as np
import matplotlib.pyplot as plt

# ---- 問題設定 ----
n_state = 4     # 状態次元 [x, y, vx, vy]
n_ctrl = 2      # 制御次元 [ax, ay]
dt = 0.1        # 時間刻み
T = 20          # 予測ホライズン
N = 500         # サンプル数
lam = 1.0       # 温度パラメータ
sigma = 0.5     # 制御ノイズの標準偏差
goal = np.array([5.0, 5.0])  # ゴール位置

def dynamics(x, u):
    """線形力学モデル(質点の2次元運動)"""
    x_next = np.zeros(n_state)
    x_next[0] = x[0] + x[2] * dt  # px
    x_next[1] = x[1] + x[3] * dt  # py
    x_next[2] = x[2] + u[0] * dt  # vx
    x_next[3] = x[3] + u[1] * dt  # vy
    return x_next

def stage_cost(x, u):
    """ステージコスト"""
    pos = x[:2]
    dist = np.sum((pos - goal)**2)
    ctrl = 0.01 * np.sum(u**2)
    return dist + ctrl

def terminal_cost(x):
    """終端コスト"""
    pos = x[:2]
    return 10.0 * np.sum((pos - goal)**2)

MPPIコントローラ

class MPPIController:
    def __init__(self):
        # 制御列の初期化(ゼロ)
        self.mu = np.zeros((T, n_ctrl))

    def compute_control(self, x0):
        """MPPIによる最適制御入力の計算"""
        # ステップ1: ノイズ付き制御列のサンプリング
        noise = np.random.randn(N, T, n_ctrl) * sigma
        V = self.mu[np.newaxis, :, :] + noise  # (N, T, n_ctrl)

        # ステップ2: 各サンプルのコスト計算
        costs = np.zeros(N)
        for i in range(N):
            x = x0.copy()
            for t in range(T):
                costs[i] += stage_cost(x, V[i, t])
                x = dynamics(x, V[i, t])
            costs[i] += terminal_cost(x)

        # ステップ3: 重みの計算(式4)
        # 数値安定性のためにコストの最小値を引く
        costs_shifted = costs - np.min(costs)
        weights = np.exp(-costs_shifted / lam)
        weights /= np.sum(weights)  # 正規化

        # ステップ4: 制御入力の更新(式5)
        weighted_noise = np.sum(
            weights[:, np.newaxis, np.newaxis] * noise, axis=0
        )
        self.mu += weighted_noise

        # 最適制御入力(最初のタイムステップ)
        u_opt = self.mu[0].copy()

        # 制御列のシフト(次のタイムステップの準備)
        self.mu = np.roll(self.mu, -1, axis=0)
        self.mu[-1] = 0.0  # 最後の要素をゼロに

        return u_opt

シミュレーション

np.random.seed(42)

# 初期状態
x = np.array([0.0, 0.0, 0.0, 0.0])
controller = MPPIController()

# シミュレーション
n_steps = 100
trajectory = [x.copy()]
controls = []

for step in range(n_steps):
    u = controller.compute_control(x)
    controls.append(u.copy())
    x = dynamics(x, u)
    trajectory.append(x.copy())

    # ゴール到達判定
    if np.linalg.norm(x[:2] - goal) < 0.1:
        print(f"Goal reached at step {step + 1}")
        break

trajectory = np.array(trajectory)
controls = np.array(controls)

# ---- 結果のプロット ----
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 軌道
axes[0].plot(trajectory[:, 0], trajectory[:, 1], "b-o",
             markersize=3, label="MPPI trajectory")
axes[0].plot(*goal, "r*", markersize=15, label="Goal")
axes[0].plot(0, 0, "gs", markersize=10, label="Start")
axes[0].set_xlabel("x")
axes[0].set_ylabel("y")
axes[0].set_title("MPPI - Goal Reaching")
axes[0].legend()
axes[0].grid(True)
axes[0].set_aspect("equal")

# 制御入力
axes[1].plot(controls[:, 0], label="$a_x$")
axes[1].plot(controls[:, 1], label="$a_y$")
axes[1].set_xlabel("Step")
axes[1].set_ylabel("Control input")
axes[1].set_title("Control inputs over time")
axes[1].legend()
axes[1].grid(True)

plt.tight_layout()
plt.savefig("mppi_result.png", dpi=150)
plt.show()

温度パラメータの影響

温度 \(\lambda\) は重みの集中度合いを制御します:

# 温度パラメータの影響を可視化
costs_example = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(8, 5))

for lam_val in [0.1, 0.5, 1.0, 5.0]:
    w = np.exp(-costs_example / lam_val)
    w /= np.sum(w)
    ax.plot(costs_example, w, label=f"$\\lambda={lam_val}$")

ax.set_xlabel("Cost $J$")
ax.set_ylabel("Weight $w$")
ax.set_title("Temperature parameter effect on MPPI weights")
ax.legend()
ax.grid(True)
plt.tight_layout()
plt.savefig("mppi_temperature.png", dpi=150)
plt.show()

\(\lambda\) が小さいほど低コストのサンプルに重みが集中し(CEMのハード選択に近づく)、\(\lambda\) が大きいほど均等な重み付けになります。

参考文献

  • Williams, G., Aldrich, A., & Theodorou, E. A. (2017). “Model Predictive Path Integral Control: From Theory to Parallel Computation.” Journal of Guidance, Control, and Dynamics, 40(2), 344-357.
  • Williams, G., et al. (2017). “Information Theoretic MPC for Model-Based Reinforcement Learning.” ICRA 2017.
  • Rubinstein, R. Y., & Kroese, D. P. (2013). The Cross-Entropy Method. Springer.