AIにロケットを着陸させたら、手作りの制御に勝てなかった話 —— そして「勝った」と思った自分を、自分で反証するまで
対象: 強化学習・進化計算・制御工学に興味がある人 / 「学習は手作りに勝つ」を検証したい人。
前提知識: Python が読めれば十分。制御用語は都度かみくだきます。
数値はすべてローカル実測。robust NULL(学習は公平な PID に勝てず引き分け)を、自分が一度はまった罠ごと正直に書きます。
0. 3行まとめ(先に結論)
- MuJoCo でジンバル推力のロケット(booster)を垂直着陸させ、「手で設計した制御(PID)」vs「進化で学習した方策」を正直に比較した。
- 素朴にやると学習が勝つように見える。しかし**評価を厳密にし(毎世代再サンプル+ソフト目的+paired・10 seed・ブートストラップ信頼区間で測り直す)、審判 PID も公平に再調整すると、学習は勝てず「引き分け」**だった。(後で分かるが、効いたのは主に"統計の厳密さ"で、審判の再調整は衛生作業だった。)
- いちばんの学びは技術ではなく態度: 「学習が制御に勝った」ときは、まず"審判が弱い"か"統計が薄い"を疑え。この記事は、私自身がその罠に一度はまり、自分で自分を反証するまでを含む。
1. 用語(先に地図)
| 用語 | かみくだき |
|---|---|
| 終端着陸(terminal descent) | ロケットが地面に着く「最後の数秒」だけを扱う。軌道力学は扱わない |
| ジンバル推力(thrust vectoring) | エンジンノズルを傾けて推力の向きを変える=倒立振子を立て直す舵 |
| PD / PID 制御 | 誤差に比例(P)・微分(D)・積分(I)して打ち消す、制御工学の定番。I(積分)は「ずっと同じ方向の誤差」を溜めて消す記憶 |
| 残差方策(residual policy) | 「PD の出力 + 小さなニューラルネットの補正」。ネットがゼロ初期化ならそのまま PD(進化のウォームスタート) |
| 過学習(overfitting) | 評価に使った少数のシナリオだけに強くなり、新しいシナリオで崩れる |
| held-out / TEST | 学習に一切使っていない「新しい問題セット」での成績。汎化の唯一の証拠 |
| fitness(適応度) | 着陸の良さを1つの数値にしたスコア(遅く・まっすぐ・パッド上ほど高い)。進化が最大化する量 |
| CI(信頼区間, confidence interval) | 「本当の平均はこの範囲にありそう」の幅。下限が 0 を超えて初めて"有意に正" |
| 事前登録(pre-registration) | 結果を見る前に「何をもって成功とするか」を凍結する。後付けの言い訳を封じる |
2. 舞台: 排気の上に立つ倒立振子
onocollo.rocket(CPU で回る MuJoCo トイ)で、フリージョイントの機体・4本の着陸脚・2軸ヒンジのジンバルノズルを持つ booster を作る。制御は3つ:
| idx | アクチュエータ | 効果 |
|---|---|---|
| 0 | thrust | 落下を止める推力(0〜TWR×重量、既定 TWR 2.4) |
| 1 | gimbal_x | y 平面へ推力を振る=x 軸まわりの姿勢 |
| 2 | gimbal_y | x 平面へ推力を振る=y 軸まわりの姿勢 |
(TWR = thrust-to-weight ratio, 推力重量比=最大推力÷重量。1 を超えないと落下を止められない。)
ポイントはこの機体は放っておくと必ず倒れること(排気の上に立つ倒立振子)。だから「着陸させること自体が制御問題」になる。脚はノズルより下まで届き、接地した瞬間にエンジンをカットする(実機と同じ。噴きっぱなしだと着地後に倒される)。
デバッグ小話: ジンバルの符号を物理で較正する
制御を書くとき地味にハマったのがジンバルの符号。2軸は「効き方の符号が逆」で、gx = -k·(誤差) だが gy = +k·(誤差)。机上で悩まず、機体に実際に小さな舵を打って d(傾き)/d(ジンバル) の符号を実測して確定した。制御は「式を眺める」より「1回動かして符号を見る」ほうが速い。
3. ベースライン: よくチューニングした PD 誘導
強い"手作り"の基準として、カスケード PD 誘導を置く:
-
縦: 降下速度プロファイル
v_target = -(v_land + v_slope·高度)を追従。地面付近でv_landまで細くなる=ソフトタッチダウン。 - 横/姿勢(カスケード): 横ずれ・横流れがあると「パッド方向へ少し傾け」を指令し(機体は傾いて初めて横移動する)、内側の姿勢ループがジンバルでその傾きを追う→真上で立て直す。
実測(32ランダム着陸、グリッド調整済み、正直): 着陸率 1.00 / ソフト率 ~0.85 / 接地 ~0.68 m/s / 接地時の傾き ~7°。全ランダム降下が着陸し、~85% が「遅く・まっすぐ・パッド上」のソフト着陸。手作り、かなり強い。
4. 学習を足す: 残差方策と、正直な基準
ctrl = pd_control(obs) + scale · MLP(obs)。MLP(多層パーセプトロン=小さなニューラルネット)はゼロ初期化=最初は PD そのもの。(μ+λ) 進化(親 μ 個から子 λ 個を変異で作り、良い方を残す進化アルゴリズム)で PD の上の改善を探す。全候補と PD を同じランダム初期条件で採点するので、best > baseline は「学習が手作りを超えたか」の公平なテスト——のはずだった。
- 無風(nominal): 進化残差が PD をわずかに上回る(76.60 → 77.78、+1.5%)。PD が既に強いので差は小さい。学習は「良い手作りの上にちょっと足す」だけで、ダメな制御を救うわけではない。
5. 風を入れると null が出る(正直な行き詰まり)
hard_config() は持続横風(≈0.13×重量、毎エピソード向き・強さランダム)を足す。ここが面白い: メモリレスな P(+D) 則は、一定の横力を"一定の傾き"でしか相殺できず、消せない定常オフセットが残る(積分の記憶が無いから)。
- 風下でメモリレス残差を小さい eval(10エピソード)で進化→訓練 +26%(52.8→66.4)。でも held-out では PD に負ける(20.5 vs 27.2)。ゲインは有限の評価エピソードへの過学習だった。
- eval を24に増やす→訓練ゲインは縮み(+13%)、held-out は PD と引き分け。転移しない。
正直な結論(第1幕): メモリレス残差は、持続風の下で PD を超えない。理由は明快で状態(記憶)が無いから。→ 次は「状態を持つ制御」だ。
6. 状態を足す: PID(積分)は"少しだけ"効く
仮説「状態が足りない」は検証可能なので検証した。PIDController は横ずれの積分を持ち(アンチワインドアップ付き)、持続風を時間をかけて相殺する。
実測(手調整 k_i≈0.005): 小さな積分項が PD を控えめだが一貫して上回る(ソフト率 0.09→0.16 など、決して悪化しない)。ただし劇的ではない——終端降下は ~3秒しかなく、積分が育ちきる前に着地する。大きすぎる k_i はワインドアップで悪化。
→ アーク前半のオチ: 手作りの状態(積分)が、状態の欠けたメモリレス学習に勝つ。
7. 学習に状態を持たせる: recurrent 方策——また過学習
RecurrentPolicy(RNN=recurrent neural network, 再帰型ニューラルネット。隠れ状態で「記憶」を持てる。その隠れ状態を PD の上の残差にする、ゼロ初期化でウォームスタート)に、PID と同じ「記憶」を与え、進化に好きに使わせる。基準は手調整 PID。
-
訓練:
PID 53.8 → recurrent 65.1、+21%——大きく見える勝ち。 - held-out: 転移しない。1勝1敗1分、しかも全 held-out でソフト率が PID より低い。進化はスカラー fitness をソフト着陸を犠牲にして稼いだ(目的関数ハッキング)。
正直な結論(第2幕): held-out では手調整 PID が学習 recurrent に勝つ。+21% はまた過学習+目的ハッキング。次の伸びしろは policy の容量ではなく、"評価"だ。
🗒️ 「初めは大人しく成り行きを見守っていたんやが…余りにも隙だらけや…そう思ってからは早かった」— 進化アルゴリズム側から見た、私の目的関数の話。ソフト着陸を削ればスコアが伸びる隙を残したのは私で、+21% はその隙を突かれた記録だった(© Forbidden shibukawa / SHUEISHA・『スナックバス江』)
8. 【本題】"評価こそがレバー" を事前登録して検証する
ここからが今回の新しい仕事。null が「評価が伸びしろ」と指したので、それを事前登録して決着させた。設計は結果を見る前に凍結(feedback_benchmark_honest_disclosure)。
8.1 手法を敵対的に事前登録する(複数AIオーケストレーション)
設計自体を6エージェントの Workflowで敵対的に固めた: 4つのレンズ(過学習 / 目的設計 / baseline 公平性 / honest-disclosure)で並列に設計 → 統合 → 敵対批評。批評家は私の初期実装の欠陥を的確に突いた——「validation 選抜が gaming された目的で行われ循環」「選んだ集合で判定する winner's curse(=多数の候補から最良を選ぶと、その"選び"が運を拾って過大評価になる)」「soft ボーナスが二値の崖」「seed 分割の不備」。全て修正してから走らせた。
8.2 eval-signal スタック(すべて既定 OFF=過去の null を壊さない)
- 世代ごと再サンプル eval: 固定セットの記憶を封じる。
-
連続 min-margin ソフト目的: 3つのソフト余裕の最小に比例する滑らかなボーナス。二値
int(soft)の崖にしない(崖はギリギリのソフトを量産して汎化で崩れる)。 - validation ブロック選抜: 動く eval で「最高訓練 fitness」を選ぶと"運の良い引き"に過学習するので、別ブロックで、しかも**元の目的(w_soft=0)**で選ぶ。
- 公平に再チューニングした PID: 学習と同じ目的・風・予算で PID を再調整。
-
リークしない paired 評価: 100万ブロックで seed を分割(TRAIN 1M / VAL 2M / TEST 3M / TUNE 4M)+実行時
assert_no_seed_leakage、共通乱数の paired 差分+ブートストラップ CI。
8.3 事前登録した主判定
recurrent をフルスタックで進化させ、再調整 PID と比較。CONFIRM(評価改善で学習が公平な PID に勝つ)は、TEST(3ストリーム×100エピソード、paired)で全ストリームの fitness CI 下限>0 かつ soft CI 下限≥0、かつ 10 seed 中 8 以上が個別に正——のときだけ。fitness は勝つが soft は負ける、は自動で目的ハッキング nullに分類。
9. 結果: robust NULL——公平な PID には勝てない
| 条件 | 方策 / 評価 | 訓練ゲイン | held-out fit差 | 過学習gap | held-out判定 |
|---|---|---|---|---|---|
| P(主) | recurrent, 再サンプル+soft-margin+val選抜 | +3.7 | −0.1 | +3.8 | 引き分け(0/10正) |
| C_evaldesign | recurrent, 再サンプル+スカラー目的 | +9.4 | −4.2 | +13.6 | 負け |
| C_sham | recurrent, 固定eval+soft-margin | +8.7 | −0.0 | +8.8 | 引き分け/負け |
| C_feedforward | メモリレスMLP, 再サンプル+soft-margin | +6.7 | −1.9 | +8.6 | 負け/引き分け |
| R_nominal | recurrent, 無風(sanity) | +0.5 | −0.1 | +0.6 | 引き分け |
主判定 P は全ゲート不通過: 0/10 seed が正、TEST 3ストリームとも fitness CI が 0 をまたぐ(+0.1, -0.1, +0.5=引き分け、負けではない)。フルスタックは学習を引き分けまで押し上げるが、それ以上は無い。
正直な但し書き: 唯一、TEST の soft で s2 が
+0.035[+0.015,+0.055]と CI 下限が 0 を超える正の信号を出した。だが別ストリームで相殺され、その stream の fitness CI は 0 をまたぐので勝ちにはならない。null を"きれい"に見せないために、この 1 本の信号も隠さず載せる。
9.1 正直な分解——何が本当に効いたか
- 見かけの勝ちは厳密統計で消えた: 過去の「recurrent が勝つ」は "3 seed で fit 3/3、CI 無し、k_i=0.005 相手"(§7 の 1勝1敗1分とは別の、本セッションで固定 eval・別 seed で再現した run。少数 seed だと"見え方"がこれだけ振れる、という例でもある)。paired・10 seed・ブートストラップ CI で再測ると 0/10・pooled fit 平均マイナス。小 N・CI 無し・seed 依存ノイズだった。
- eval 改善は本物だが引き分け止まり: soft-margin 目的は目的ハッキングを止め(P は引き分け、スカラーの C_evaldesign は −4.2 で負け)、再サンプルは過学習 gap を半減(P +3.8 vs 固定 C_sham +8.8)。負け→引き分けまで押すが、勝ちは作らない。
- 訓練ゲインは転移しない: どの条件も訓練 +3.7〜+9.4 が held-out で ≈0 に潰れる。
10. 【climax】"審判が弱かった" と思った自分を、自分で反証する
ここが今回いちばん正直な部分。私は途中でこう書きかけた——「元の PID(k_i=0.005)が under-tuned だった。公平に再調整(k_i=0.012)したら見かけの勝ちが消えたのだ」と。きれいな話だ。だがそれは間違いだった。
実験後に「再調整 PID は本当に強い bar なのか?」を確かめる別 check を走らせたら:
TEST lanes 上の PID: k_i=0.005 → fit 49.16 / soft 0.181
k_i=0.012 → fit 49.10 / soft 0.167
TEST では k_i=0.005 ≈ 0.012(soft はむしろ 0.005 が僅かに上)。つまり「元の bar は under-tuned だった」という私の物語は、TEST 上では成り立たない(その主張は別の seed セットでの話だった)。
真因はもっとシンプルで、もっと正直だ: 見かけの勝ちは"弱い審判"ではなく"薄い統計"(小 N・CI 無し・seed 依存)のノイズだった。再調整はやるべき衛生作業だが、支配的な要因ではなかった。
さらに 4体の敵対的 skeptic(baseline 公平性 / リーク / 目的ハッキング / 検出力)で null 自体を検証した。全員、null を反証できず caveat 止まり:
- リークは"偽 CONFIRM"しか作れず、null は説明できない。
- 判定時の soft は w_soft=0 で凍結=目的汚染なし。
- 「検出力不足で本当の効果を null と誤認では?」→ もしそうなら 6〜8/10 seed が正になるはず。実際は 0/10+平均マイナス=誤認の兆候と逆。
正直な限界も開示する: PID の再調整は {k_i,k_pos,k_vel,lean_max} のみで i_max/kv_z/v_land は固定。ただし固定軸を追加 sweep しても TEST で reported bar を上回る PID は見つからなかった(bar は近似最適)。そして fitness チャネルは検出力不足(効果量 d<0.03)=これは「意味ある勝ちは無い」であって「効果ゼロの証明」ではない。
🗒️ 「嘘は良くないわよ…!」「声デカ」— honest disclosure を他人ではなく自分の中間結論に向けると、だいたいこの音量になる(© Forbidden shibukawa / SHUEISHA・『スナックバス江』)
11. 教訓(gift-to-reader)
- 「学習が手作りに勝った」ときの最初の容疑者は、"弱い baseline" と "薄い統計"。 派手な手法や policy 容量ではない。
- 評価設計 > policy 容量。 今回、伸びしろは RNN を大きくすることではなく、評価を厳密にすることにあった——それでも公平な PID は超えなかった。
- 自分の中間結論も疑う。 私は一度「under-tuned baseline が原因」と書き、自分の追加 check で反証した。honest-disclosure は他人だけでなく自分にも向ける。
- null は一級の成果。 「勝てなかった」を正直に、内訳つきで出すことが、次の実験の正しい出発点になる。
11.5. 続編(実験中): 「突風」なら反応性が学習に味方するか
引き分けの実験は持続する(一定の)横風だった。ここは"記憶"=積分(PID の I)が効く領域で、だから手作りが強かった。では時変の突風(gust)——向きも強さもコロコロ変わる風——ならどうか?
理屈はこう: PID の積分は「一定のずれ」を消すために溜める記憶。だが風が積分の育つ速さより速く変われば、積分は常に一歩遅れて的外れを撃つ。つまり突風では"記憶"より**"反応の速さ"**が効く——ここは学習に勝ち目がありそうな領域だ。
onocollo.rocket に時変風(ゼロ平均のオルンシュタイン=ウーレンベック過程。相関時間 ~0.4 秒で滑らかに揺らぐ)を後方互換で足し(gust_config、既定 OFF で既存挙動は不変)、同じ厳密ハーネスで再検証中。すでに分かった下地: 突風下で手作り PID を再調整すると、最適は k_i=0(=積分なしの純 PD)——積分はむしろ害になり、"反応的な"純 PD が最良の手作り制御だった(持続風では k_i>0 が良かったのと対照的)。仮説の機構は裏が取れた。
残る問い: その反応的な純 PD を、学習した反応的方策は超えられるか? 事前登録した主判定(recurrent フルスタック 10 seed vs 再調整 PD、paired・ブートストラップ CI)を走らせた。
結果: また引き分け(NULL)——反応性の領域でも、学習は手作りを超えなかった。
-
P_gust(recurrent): 0/10 seed が正、TEST の 3 ストリームとも fitness CI は 0 をまたぐ(
+0.6/-0.1/+0.3=タイ)。soft はむしろ僅かに負(-0.004/-0.033/-0.036)——持続風と同じ「fit はタイ、soft を少し削る」型。 -
C_gust_ff(feedforward): 1 ストリーム(s0)だけ fitness
+4.3[+2.3,+6.2]・soft+0.058[+0.008]と両方プラスに光ったが、他 2 ストリームは soft が負、held-out(VAL-DEC)は-0.88、seed も 0/4 が正——全ゲート不通過で、robust な勝ちではない(1 本の光りは、条件を絞った探索で時々出る"まぐれ寄り"の類。前回の教訓どおり、これ 1 本で「勝ち」とは書かない)。
結論: 積分が害になる"反応性"の領域で、手作りの最適が純 PD(k_i=0)になっても、学習した反応的方策はその純 PD を明確には超えない。→ 手作り制御は、持続風でも突風でも頑健。「突風なら反応性で学習有利」という予想は、正直に言うと外れた(仮説の機構=積分が効かない、は当たったが、だからといって学習が勝つわけではなかった)。
なお positive の疑いは残さず処理した: C_gust_ff s0 の唯一の両プラス信号は、他ストリーム・VAL-DEC・seed 一致のどれも満たさず CONFIRM ゲートに届かない。robust な勝ちが出ていれば Phase D 敵対検証を通してから headline する方針(
feedback_no_solo_ai_judgment)だが、今回は通す対象の"勝ち"が無い。
12. 次回に続く
上の突風実験の先は、世界モデル(V/M/C)に接続して、画素から着陸を学ぶ——観測が「状態ベクトル」ではなく「カメラ画像」になったとき、この引き分けは動くのか? そして移動パッド——反応性がさらに効く領域では、学習に勝ち目はあるのか。
(コードは onocollo.rocket=物理 model + 正直な fitness env + 手作り baseline + ウォームスタート進化 policy + CLI + テスト + 正直な doc、の再利用テンプレ。新実験は同じ形で載る。)
補足: 数値の正本は
docs/rocket_landing.md「The evaluation is the lever」節。実験はscripts/rocket_eval_experiment.py+rocket_eval_aggregate.pyで再現、結果はout/rocket/eval_exp/。この記事はドラフト(公開は人間判断)。



