At a glance要点
Five metrics, two controllers: where PPO matches the textbook answer, what it paid, and where it falls short.5つの指標、2つの制御器。PPOが教科書の答えに並ぶ点、支払ったコスト、及ばない点。
Numbers as a table数値を表で見る
| Metric指標 | LQR | PPO, medianPPO中央値 | PPO best seedPPO最良seed |
|---|---|---|---|
| steps to threshold閾値到達ステップ数 | 0 | 62 144 | — |
| time to a controller制御器が得られるまでの時間 | 22 ms | 11.4 s | — |
| nominal return, of 500公称return(500点満点) | 500.0 | 500.0 | 500.0 |
| critical θ̇₀臨界θ̇₀ | 2.169 rad/s | 1.082 rad/s | 2.004 rad/s |
| critical ẋ₀臨界ẋ₀ | 2.435 m/s | 0.824 m/s | 1.935 m/s |
solved · 62 144 steps median. Discrete and continuous alike.
達成 · 中央値62 144ステップ。離散・連続とも同じ。
discrete: 145 368 steps, 2.34×, p = 0.0001 · continuous (MuJoCo): no seed reaches threshold, 28.2 % of actions saturated.
離散:145 368ステップ、2.34×、p = 0.0001 · 連続(MuJoCo):閾値に達したseedはゼロ、actionの28.2 %が飽和。
- 01Both controllers score a perfect 500.0. LQR gets there by iterating the Riccati recursion by hand (866 sweeps vs 923 predicted, 22 ms) at zero sample cost; PPO, written without gymnasium, stable-baselines3 or cleanrl, matches it after a median 62 144 environment steps on 16/16 seeds, 11.4 s per run.どちらの制御器も500.0点満点。LQRはRiccati漸化式を手で反復し(866回、理論予測923回、22 ms)、サンプルコストゼロで到達。PPOはgymnasium・stable-baselines3・cleanrlを使わずに実装し、16/16 seedで中央値62 144環境ステップ(学習1回11.4 s)をかけて並びました。
- 02PPO's recovery basin is half to a third of LQR's. Critical θ̇₀ 1.082 vs 2.169 rad/s, critical ẋ₀ 0.824 vs 2.435 m/s at identical nominal return — the difference the score cannot see, and the seeds spread from 0.831 to 2.004 rad/s.PPOの復帰可能領域はLQRの半分〜3分の1。公称returnは同じでも、臨界θ̇₀は1.082 vs 2.169 rad/s、臨界ẋ₀は0.824 vs 2.435 m/s。スコアには現れない差で、seed間でも0.831〜2.004 rad/sとばらつきます。
- 03Ratio clipping is the only ingredient that dominates. Without it 8/16 seeds solve at 2.34× the steps (p = 0.0001); GAE is worth 11 %, advantage normalisation is not resolved at the 5 % level — and eight seeds had those two in the wrong order.支配的な要素はratio clipだけ。clipなしでは達成8/16、ステップ数2.34×(p = 0.0001)。GAEの効果は11 %、advantage正規化は5 %水準で判別できず、8 seedではこの2つの順位が逆でした。
- 04The conclusion survives continuous actions. On a MuJoCo plant proved identical to 1.4e−14, a Gaussian policy solves in 63 748 steps, and removing the clip turns from a slowdown into a 0/16 failure mode.結論は連続actionでも維持。1.4e−14の精度で同一と証明したMuJoCoプラント上で、Gaussian policyは63 748ステップで達成。clipを外すと学習の遅れではなく0/16の失敗モードになります。
- 05Everything hand-written and verified; no reward curve anywhere. The environment passes 13/13 checks (Lagrangian 2.6e−15, divergence rate 3.4e−11); because return saturates, every result ranks on steps-to-threshold and basin instead.すべて自作・検証済み。reward曲線は1本もなし。環境は検証13/13(Lagrangian 2.6e−15、発散率3.4e−11)。returnは飽和するため、すべての結果は閾値到達ステップ数とbasinで順位付けしています。
In plain words概要
Two controllers for the same cart-pole: one solved with linear algebra, one learned from nothing.
同じcart-poleに対する2つの制御器。一方は線形代数で解き、もう一方はゼロから学習します。
Cart-pole is the "hello world" of reinforcement learning: a pole hinged on a cart, and a controller that can push the cart left or right. Let it go and the pole falls. Keep it upright for 500 steps and you have "solved" the task.
cart-poleは強化学習の「hello world」です。台車にヒンジで取り付けたポールがあり、制御器は台車を左右に押せます。 放っておけばポールは倒れます。500ステップ立て続ければタスクを「解いた」ことになります。
Almost every tutorial trains a policy on it, plots a reward curve that goes up, and stops. This project asks a different question, and the difference is the whole point:
ほとんどのチュートリアルはpolicyを学習させ、右肩上がりのreward曲線を描いて終わります。 このプロジェクトは別の問いを立てます。その違いこそが本題です。
What does a learned policy actually buy over a controller you can solve for in closed form — and what does it cost in samples?
閉形式で解ける制御器に対して、学習したpolicyは実際に何をもたらすのか。そしてサンプル数でどれだけのコストを払うのか。
Because cart-pole has an answer that does not require learning at all. Linearise the physics around upright, solve one matrix equation, and out comes an optimal linear feedback controller. It consumes zero environment steps. It scores a perfect 500.0 out of 500 on the first try, and on every try after that.
cart-poleには、学習をまったく必要としない答えがあるからです。倒立点まわりで物理を線形化し、行列方程式を1つ解けば、 最適な線形フィードバック制御器が得られます。消費する環境ステップはゼロです。 初回から500点満点中500.0を出し、その後も毎回同じです。
So the learned policy cannot beat the baseline on the usual metric. It can only match it. That framing forces two better questions, which is what the project measures: how many samples does matching cost, and is there any axis on which the two controllers actually differ?
つまり通常の指標では、学習したpolicyはbaselineに勝てず、並ぶことしかできません。この前提から、より良い2つの問いが生まれます。 並ぶまでに何サンプル必要か、そして2つの制御器が実際に異なる軸はあるのか。このプロジェクトが計測しているのはこの2点です。
Nothing is imported外部実装は一切使わない
No gymnasium, no stable-baselines3, no rllib, no cleanrl copy-paste, no scipy. The environment is hand-written from the 1983 paper's equations. The discrete Riccati equation is solved by iterating the recursion rather than calling a library, because understanding that recursion is the point — it is the exact same object PPO's critic estimates by regression, except here it can be computed exactly.
gymnasium、stable-baselines3、rllib、cleanrlからのコピー、scipyはいずれも使っていません。環境は1983年の論文の式から自作しました。 離散Riccati方程式もライブラリを呼ばず、漸化式を反復して解いています。この漸化式を理解することが目的だからです。 これはPPOのcriticが回帰で推定しているものとまったく同じ対象ですが、ここでは厳密に計算できます。
Terms used on this pageこのページの用語
The six steps6つのステップ
The six build steps, and what each had to produce before the next could start.
構築の6ステップと、次へ進む前に各ステップで出す必要があった成果。
Every file, and what it is for全ファイルとその役割
Every source file, how big it is, and the single job it owns.
各ソースファイルの規模と、それぞれが担う唯一の役割。
| Fileファイル | Lines行数 | What it does役割 |
|---|---|---|
| cartpole.py | 209 | The plant and the environment, numpy only. The four physical parameters, the closed-form
accelerations, the explicit-Euler step, energy and momentum functions used by the checks, and an
environment class with separate terminated and truncated flags and its
own random generator.プラントと環境(numpyのみ)。4つの物理パラメータ、閉形式の加速度、陽的Euler法の1ステップ、
検証で使うエネルギー・運動量の関数、そしてterminatedとtruncatedを別フラグで持ち、
独自の乱数生成器を備えた環境クラス。 |
| verify_env.py | 456 | Thirteen checks in about six seconds on one core. Independent Lagrangian solve, energy and momentum conservation under RK4, the closed-form divergence rate, every termination and truncation contract, determinism, RNG isolation, the published random-policy return, and a golden-trajectory regression fixture.1コアで約6秒、13項目の検証。Lagrangianの独立解、RK4でのエネルギー・運動量保存、閉形式の発散率、 terminationとtruncationの全仕様、決定性、RNGの分離、文献値どおりのランダムpolicyのreturn、 そしてgolden trajectoryによる回帰テスト。 |
| lqr.py | 496 | The baseline. Analytic linearisation, finite-difference Jacobian cross-checks, exact discretisation, the Riccati recursion, the gain, the sign projection to two actions, seven verification checks, the basin bisection, and the Q-sensitivity sweep.baseline本体。解析的な線形化、有限差分Jacobianとの照合、厳密な離散化、Riccati漸化式、ゲイン、 2つのactionへの符号射影、7項目の検証、basinの二分探索、Q感度スイープ。 |
| lqr_baseline.json | — | The baseline's numbers, written out so later steps compare against a file rather than a memory.baselineの数値。後のステップが記憶ではなくファイルと比較できるよう書き出したもの。 |
| ppo.py | 360 | PPO itself. Separate 64-64 tanh actor and critic, orthogonal initialisation, GAE, the clipped surrogate, multi-epoch minibatch updates, the truncation bootstrap, and command-line switches that turn each of the four PPO ingredients off individually.PPO本体。独立した64-64 tanhのactorとcritic、直交初期化、GAE、clipped surrogate、複数epochのミニバッチ更新、 truncation時のbootstrap、そしてPPOの4要素をそれぞれ個別にOFFにできるコマンドラインスイッチ。 |
| ppo_continuous.py | 305 | The Gaussian-policy version. Imports compute_gae and layer_init from
ppo.py rather than copying them, so the two implementations cannot drift apart. Only
the head changes.Gaussian policy版。compute_gaeとlayer_initはコピーせずppo.pyからimportしているため、
2つの実装がずれることはありません。変わるのは出力層(head)だけです。 |
| mj_cartpole.py | 291 | The MuJoCo environment for step 6, plus six verification checks — including the one that proves the two simulators describe the same rigid body to 1.4e−14.step 6用のMuJoCo環境と6項目の検証。2つのシミュレータが同じ剛体を1.4e−14の精度で記述していることを証明する検証も含みます。 |
| study.py | 277 | The experiment driver. Seed lists (0–15 for reporting, 100–103 for hyperparameter search, kept disjoint), parallel launching, censoring, and a permutation test written without scipy.実験ドライバ。seedリスト(報告用は0–15、ハイパーパラメータ探索用は100–103で重複なし)、並列起動、censoring処理、 scipyを使わずに書いた並べ替え検定。 |
| compare.py | 148 | LQR against all sixteen trained checkpoints on the axis that can actually separate them: the basin of attraction, by bisection, plus a widened-initial-condition sweep.LQRと学習済みcheckpoint 16個すべてを、実際に差が出る軸で比較します。二分探索によるbasin of attractionと、 初期条件を広げたスイープです。 |
| visualize.py | 172 | Watch it. MuJoCo is used as a renderer only — mj_forward for kinematics,
never mj_step — and the numpy plant supplies the state. Letting MuJoCo integrate would
silently be a different plant.動作の可視化。MuJoCoは描画専用で、運動学にはmj_forwardを使い、mj_stepは一切使いません。
状態はnumpyのプラントが供給します。MuJoCoに積分させると、気付かないうちに別のプラントになってしまうからです。 |
| boxcheck.py | 49 | Refuses to start training above a 1-minute load average of 4.0, and prints the offending process. Called by every entry point that trains, not just the study driver — for a reason recorded below.1分間のload averageが4.0を超えていれば学習の開始を拒否し、原因のプロセスを表示します。 study driverだけでなく学習を行うすべてのエントリポイントから呼ばれます。理由は後述します。 |
| cartpole.xml · cartpole_mj.xml | 42 · 60 | Two MJCF models. The first is for visualisation only. The second sets inertias by hand
(inertiafromgeom="false") so its rigid body is provably identical to the numpy one.2つのMJCFモデル。1つ目は可視化専用です。2つ目は慣性を手で設定(inertiafromgeom="false")しており、
その剛体がnumpy版と同一であることを証明できます。 |
| golden_traj.npz | — | 201 frozen states. A regression fixture, not a correctness proof — it stops a refactor during steps 3–5 from moving the environment silently.固定した201個の状態。正しさの証明ではなく回帰テスト用で、step 3–5のリファクタリング中に環境が気付かないうちに変わるのを防ぎます。 |
| runs/ · runs_c/ | — | Checkpoints and per-run JSON for the discrete and continuous studies.離散版・連続版studyのcheckpointと、各runのJSON。 |
Step 1 — the environment, and why it is hand-writtenStep 1 — 環境と、自作する理由
Why the environment is hand-written rather than imported, and how it is checked against a saved trajectory.
環境をimportせず自作した理由と、保存済みtrajectoryとの照合方法。
The reason理由
PPO will fail to learn several times before step 5 is finished. Every time it does, the first question is algorithm or environment. That question is only cheap to answer if the environment was verified before any reinforcement learning existed. This one was.
step 5が終わるまでに、PPOは何度も学習に失敗します。そのたびに最初に問うのはアルゴリズムか、環境かです。 この問いに安く答えられるのは、強化学習を書く前に環境が検証済みだった場合だけです。この環境はそうしてあります。
The dynamicsダイナミクス
Barto, Sutton & Anderson (1983), appendix. θ is measured from upright; positive θ leans the pole toward +x.
Barto, Sutton & Anderson(1983)の付録に基づきます。θは鉛直上向きから測り、正のθでポールは+x方向に傾きます。
temp = (F + m·l·θ̇²·sin θ) / (M + m)
θ̈ = (g·sin θ − cos θ · temp) / (l·(4/3 − m·cos²θ/(M+m)))
ẍ = temp − m·l·θ̈·cos θ / (M + m)
Two conventions hide inside those three lines and both are classic bug sources. l = 0.5 is
the half-length, not the full pole length. And the 4/3 is
1 + I_cm/(m·l²) for a uniform rod, whose moment of inertia about its own centre is
m(2l)²/12 = m·l²/3. Getting the half-length convention wrong is the most common bug in a
hand-written cart-pole, so one check exists purely to catch it.
この3行には2つの規約が隠れており、どちらも典型的なバグの原因です。l = 0.5はポール全長ではなく半分の長さです。
また4/3は一様な棒に対する1 + I_cm/(m·l²)で、棒自身の重心まわりの慣性モーメントは
m(2l)²/12 = m·l²/3です。半長の規約を間違えるのは自作cart-poleで最も多いバグなので、これを検出するためだけの検証を1つ用意しています。
Thirteen checks13項目の検証
| # | Check検証内容 | Result結果 |
|---|---|---|
| 1 | Scalar formulas against an independent solve of the Lagrangian mass matrix, 10 000 random states, |θ| up to πスカラー式をLagrangian質量行列の独立解と照合。ランダムな10 000状態、|θ|は最大π | max rel diff最大相対差 2.6e−15 |
| 2a | Energy conserved under RK4, F = 0, 2 s at dt = 1e−4RK4でのエネルギー保存。F = 0、dt = 1e−4で2 s | 2.4e−14 |
| 2b | Horizontal momentum conserved, same run水平方向の運動量保存(同じ実行) | 5.4e−15 |
| 3 | Pinned cart, divergence rate against the closed form √(3g/4l)台車固定時の発散率を閉形式√(3g/4l)と照合 | 3.834058 vs 3.834058, rel err相対誤差 3.4e−11 |
| 4a–d | Truncation fires exactly at the cap with terminated false; stepping a finished episode raises; reward is 1.0 on every step including the terminal one; the bounds are |x|>2.4 and |θ|>12°truncationは上限ちょうどでterminatedがfalseのまま発生。終了済みepisodeをstepすると例外。rewardは終端を含む全ステップで1.0。境界は|x|>2.4と|θ|>12° | exact完全一致 |
| 4e | Action sign: bang-bang on θ + 0.5·θ̇ balancesactionの符号確認:θ + 0.5·θ̇によるbang-bangで倒立を維持 | 500.0 / 500 |
| 5a–b | Same seed gives a bitwise identical trajectory; the environment ignores the global numpy RNG同じseedでビット単位まで同一のtrajectory。環境はnumpyのグローバルRNGの影響を受けない | exact完全一致 |
| 6 | Random-policy mean return against the published ≈22, over 10 000 episodesランダムpolicyの平均returnを文献値≈22と比較(10 000 episode) | 22.09 ± 0.12 |
| 7 | Golden-trajectory regression, 201 statesgolden trajectoryによる回帰テスト(201状態) | bitwise identicalビット単位で一致 |
Check 1 is the strong one. The mass matrix is assembled from the Lagrangian and inverted with
np.linalg.solve — it shares no algebra with the hand-eliminated formulas the environment
uses, so agreement to 2.6e−15 rules out sign errors and bad elimination. Check 3 reaches outside the
code entirely: √(3g/4l) = 3.834058 s⁻¹ is a number you can derive on paper, and the simulator
reproduces it to eleven digits.
最も強い検証は1です。質量行列をLagrangianから組み立ててnp.linalg.solveで解くため、環境が使う手計算で消去した式とは
代数を一切共有しません。したがって2.6e−15での一致は、符号ミスや消去の誤りがないことを示します。検証3はコードの外側に根拠を持ちます。
√(3g/4l) = 3.834058 s⁻¹は紙の上で導出できる値で、シミュレータはこれを11桁まで再現します。
gymnasium is not installed, so there is no bit-diff against the reference implementation. Checks 1–3 prove the equations are the published ones and internally consistent; check 6 is the only externally published number reproduced. Check 7 is a regression fixture, not a correctness proof.
gymnasiumはインストールしていないため、参照実装とのビット単位の比較はしていません。検証1–3は式が文献どおりで内部的に整合していることを証明し、 外部で公表された数値を再現したのは検証6だけです。検証7は回帰テストであり、正しさの証明ではありません。
Three decisions worth arguing with議論の余地がある3つの判断
donedoneを統合しないterminated and truncated are kept separate and the environment refuses to
expose a merged flag. Collapsing them makes the value target at the 500-step cap treat a perfectly
healthy upright state as worth zero future return — a real bug worth roughly 100 points of final
performance.terminatedとtruncatedは分けたまま保持し、環境は統合したフラグを公開しません。
両者を1つにすると、500ステップの上限で正常に立っている状態の価値目標が将来return 0として扱われます。
最終性能で約100点に相当する、実際に起こるバグです。A full-episode Euler-vs-RK4 comparison is absent, and check 3 is the reason: at λ = 3.834 s⁻¹, any
difference between two integrators is amplified by e³⁸ ≈ 3e16 over a ten-second episode. That number
would be about Lyapunov exponents, not about integrators. An early draft reported it anyway and got
max |Δθ| = 12.9 rad, which is how the mistake was caught.
episode全体でのEuler対RK4の比較は載せていません。理由は検証3にあります。λ = 3.834 s⁻¹では、2つの積分器のわずかな差も
10秒のepisodeでe³⁸ ≈ 3e16倍に増幅されます。その数値が示すのは積分器の差ではなくLyapunov指数です。
初期の草稿ではそれでも報告してしまい、max |Δθ| = 12.9 radという値が出たことで誤りに気付きました。
Complete list of deviations from the standard CartPole-v1: observations are float64 not float32; stepping a finished episode raises rather than warning; the two done flags are never merged. Everything else — constants, equations, integrator and its ordering, initial distribution, thresholds, reward, 500-step cap — matches.
標準のCartPole-v1との相違点は次がすべてです。observationがfloat32ではなくfloat64であること、終了済みepisodeのstepは警告ではなく例外になること、 2つのdoneフラグを統合しないこと。それ以外(定数、式、積分器とその順序、初期分布、閾値、reward、500ステップの上限)はすべて一致しています。
Step 2 — the baseline that costs nothingStep 2 — コストゼロのbaseline
LQR from first principles: linearisation, discretisation, the Riccati recursion, and what it costs to run.
第一原理からのLQR。線形化、離散化、Riccati漸化式、そして実行コスト。
What LQR isLQRとは
Linear-Quadratic Regulator. If a system is linear and the cost you want to minimise is quadratic, the
optimal controller is a matrix multiplication: u = −Kx. The gain K is
computed from the model, not learned from data. Cart-pole is not linear, but near upright it is close
enough — so linearise, solve, and use the result.
Linear-Quadratic Regulatorの略です。システムが線形で、最小化したいコストが二次形式なら、最適制御器は行列の掛け算u = −Kxになります。
ゲインKはデータから学習するのではなく、モデルから計算します。cart-poleは線形ではありませんが、倒立点付近では十分に線形に近いため、
線形化して解き、その結果を使います。
Linearising線形化
Dropping second-order terms about the origin, with D = l(4/3 − m/M):
原点まわりで2次以上の項を落とし、D = l(4/3 − m/M)とすると次のようになります。
θ̈ = (g/D)·θ − (1/(M·D))·F
ẍ = F/M − (m·l/M)·θ̈
Ac[3,2] = g/D = 15.7756 s⁻², and √15.7756 = 3.9719 s⁻¹ is the growth rate of the
free cart-pole — slightly faster than the pinned-cart 3.834058 s⁻¹ measured in step 1, because
a free cart lets the base slide out from under the pole. Two related numbers, both derivable, both
matching.
Ac[3,2] = g/D = 15.7756 s⁻²であり、√15.7756 = 3.9719 s⁻¹が台車が自由なcart-poleの発散率です。
step 1で計測した台車固定時の3.834058 s⁻¹よりわずかに速いのは、台車が自由だとポールの根元が下から滑り出てしまうためです。
関連する2つの数値が、どちらも導出可能で、どちらも一致しています。
Discretisation is exact here — and that is the payoff for step 1's integrator choiceここでは離散化が厳密 — step 1の積分器選択が効いてくる
Every acceleration in euler_step() is evaluated at the old state, so the plant
genuinely is s' = s + τ·f(s,u) and therefore
euler_step()内の加速度はすべて更新前の状態で評価されるため、プラントはまさにs' = s + τ·f(s,u)であり、したがって
Ad = I + τ·Ac Bd = τ·Bc
with no approximation at all. A semi-implicit or exact zero-order-hold plant would need a matrix exponential here. It was verified against a finite-difference Jacobian of the actual step function anyway: max absolute difference 7.57e−14.
となり、近似は一切ありません。半陰的、あるいは厳密なゼロ次ホールドのプラントであれば、ここで行列指数関数が必要になります。 念のため実際のstep関数の有限差分Jacobianとも照合しました。最大絶対差は7.57e−14です。
Riccati by iteration, not by library callRiccatiはライブラリではなく反復で解く
P_{k+1} = Q + Adᵀ P_k Ad − Adᵀ P_k Bd (R + Bdᵀ P_k Bd)⁻¹ Bdᵀ P_k Ad, P₀ = Q
K = (R + Bdᵀ P Bd)⁻¹ Bdᵀ P Ad, u = −Kx
P_k is the cost-to-go matrix with k steps of horizon remaining, so this is
literally value iteration for a quadratic value function. It is the same object PPO's critic
estimates by regression — the difference is that here it can be computed exactly, which is the
whole reason to write the recursion out longhand.
P_kは残りホライズンがkステップのときのcost-to-go行列なので、これは文字どおり二次形式の価値関数に対するvalue iterationです。
PPOのcriticが回帰で推定しているものと同じ対象です。違いは、ここでは厳密に計算できることであり、漸化式を手で書き下す理由はまさにそこにあります。
Converged in 866 sweeps / 22 ms. The theory says the convergence rate is
ρ(Ad−BdK)² = 0.96810 per sweep, predicting about 923 sweeps to reach 1e−13 relative.
Measured 866. That agreement is the check that the recursion is the textbook one and not something that
merely happens to converge.
866回の反復・22 msで収束しました。理論上の収束率は1反復あたりρ(Ad−BdK)² = 0.96810で、
相対誤差1e−13に達するまで約923回と予測されます。実測は866回です。この一致が、漸化式が教科書どおりのものであり、
たまたま収束しただけの別物ではないことの確認になります。
K = [−0.910126, −2.132488, −30.594563, −7.841506]
ρ(Ad − Bd·K) = 0.983919
Seven checks7項目の検証
| # | Check検証内容 | Result結果 |
|---|---|---|
| L1 | Analytic Ac, Bc against a central-difference Jacobian of the derivative解析的なAc, Bcを微分の中心差分Jacobianと照合 | 3.78e−12 |
| L2 | Ad = I + τ·Ac against a finite-difference Jacobian of the actual stepAd = I + τ·Acを実際のstepの有限差分Jacobianと照合 | 7.57e−14 |
| L3 | Converged P satisfies the discrete algebraic Riccati equation収束したPが離散代数Riccati方程式を満たす | residual残差 6.63e−10 |
| L4 | Closed-loop spectral radius below 1閉ループのスペクトル半径が1未満 | 0.983919 |
| L5 | xᵀPx against the simulated linear closed-loop cost-to-goxᵀPxを線形閉ループのシミュレーションによるcost-to-goと照合 | max rel最大相対差 5.46e−12 |
| L6 | Scaling both Q and R by c leaves the gain unchanged, c ∈ [1e−3, 1e3]QとRを両方c倍してもゲインは不変(c ∈ [1e−3, 1e3]) | 5.81e−16 |
| L7 | Mean return ≥ 475 over 100 consecutive episodes連続100 episodeの平均return ≥ 475 | 500.0 (min 500, max 500) |
L5 is the one that matters most: it checks P against something
P was not used to compute. Roll the linear closed loop forward, accumulate
xᵀQx + uᵀRu, and compare the total to x₀ᵀPx₀. Agreement to 5e−12 means the
converged matrix really is the cost-to-go and not just a fixed point of some recursion that was typed in.
最も重要なのはL5です。Pの計算に使っていないものとPを照合するからです。
線形閉ループを前に進めてxᵀQx + uᵀRuを累積し、その合計をx₀ᵀPx₀と比べます。
5e−12での一致は、収束した行列が本当にcost-to-goであり、入力した何らかの漸化式の不動点にすぎないわけではないことを意味します。
Bridging a continuous controller to two discrete actions連続値の制御器を2つの離散actionにつなぐ
The environment offers ±10 N. LQR returns a real number. The bridge is a design decision, not a detail:
環境が受け付けるのは±10 Nで、LQRが返すのは実数です。この橋渡しは細部ではなく設計判断です。
action = 1 if −Kx > 0 else 0
Bang-bang control with an LQR-derived switching surface. Only the sign of −Kx
reaches the plant, so the policy is invariant to any positive rescaling of K, and therefore
to scaling the whole cost — which is exactly what check L6 confirms.
LQRから導いた切替面を持つbang-bang制御です。プラントに届くのは−Kxの符号だけなので、policyはKの正のスケーリングに対して不変であり、
したがってコスト全体のスケーリングにも不変です。検証L6が確認しているのはまさにこの点です。
The number PPO has to justify itself againstPPOが自らの価値を示すべき基準値
Mean return over 1000 episodes. Zero failures. Metric fully saturated.
1000 episodeの平均return。失敗ゼロ。指標は完全に飽和しています。
The design consumed no environment interaction at all. Steps-to-threshold is 0 by construction.
設計に環境とのやり取りを一切使っていません。閾値到達ステップ数は構造上0です。
PPO cannot beat this. The metric is saturated. The most it can do is match it, and the only honest axes left are sample cost — where LQR wins by construction — and everything the LQR does not have, which is independence from the model.
PPOはこれに勝てません。指標が飽和しているからです。できるのはせいぜい並ぶことで、残る正直な比較軸は、 LQRが構造上勝つサンプルコストと、LQRが持たないもの、すなわちモデルからの独立性だけです。
Return is a saturating metric — measured, not assertedreturnは飽和する指標 — 主張ではなく計測
An early claim in the repository was that the cost weight R cannot matter because it only
rescales K. That is false — the Riccati equation is not linear in R — and
sweeping R from 0.01 to 100 rotates the normalised gain direction by 0.130. The check caught
it. The interesting part is what happened next: that rotation moved the measured return by
0.0. Every one of those controllers scores 500.0.
リポジトリの初期には「コスト重みRはKをスケーリングするだけなので影響しない」という主張がありました。
これは誤りです。Riccati方程式はRについて線形ではなく、Rを0.01から100までスイープすると正規化したゲインの向きが0.130回転します。
検証がこれを見つけました。興味深いのはその先で、この回転による計測returnの変化は0.0でした。どの制御器も500.0を出します。
So return cannot rank controllers on this task once they are all merely good enough. Steps 4 and 5 therefore rank on steps-to-threshold, and this repository does not contain a single reward curve.
つまりこのタスクでは、どの制御器も一定以上の性能になった時点で、returnでは順位を付けられません。 そのためstep 4と5は閾値到達ステップ数で順位を付けており、このリポジトリにはreward曲線が1本もありません。
What can tell controllers apart is the basin — how large a disturbance they can recover from — found by bisecting on the initial state one axis at a time:
制御器を区別できるのはbasin、つまりどれだけ大きな外乱から復帰できるかです。初期状態を1軸ずつ二分探索して求めます。
| Perturbation from upright倒立状態からの摂動 | Bang-bang (±10 N only)Bang-bang(±10 Nのみ) | Continuous force, clipped to ±10 N連続値の力(±10 Nでclip) |
|---|---|---|
| critical θ̇₀臨界θ̇₀ | 2.1693 rad/s | 1.9632 rad/s |
| critical ẋ₀臨界ẋ₀ | 2.4354 m/s | 1.9593 m/s |
| critical θ₀臨界θ₀ | 0.2094 rad = the termination limit= 終了判定の限界 | 0.2094 rad |
Bang-bang has the larger basin than the same law with a proportional actuator of equal strength. Not a paradox: recovering from a large disturbance is a time-optimal problem, and the time-optimal solution to a bounded-input problem is bang-bang. The linear law under-commands near the edge because it is minimising a quadratic cost, not maximising survival. Angle finds nothing — every angle inside the termination limit is recoverable, which is why the sweep runs on velocity.
同じ制御則を同じ強さの比例アクチュエータで動かした場合よりも、bang-bangの方がbasinは広くなります。 これは矛盾ではありません。大きな外乱からの復帰は時間最適問題であり、入力に上限がある問題の時間最適解こそbang-bangだからです。 線形の制御則は生存の最大化ではなく二次コストの最小化をしているため、境界付近で指令が不足します。角度では差が出ません。 終了判定の限界内の角度はすべて復帰可能なので、スイープは速度で行っています。
Q sensitivity — and the row that shows what return hidesQ感度 — returnが隠すものを示す行
| Q | Sweeps反復回数 | ρ | Return | Critical θ̇₀臨界θ̇₀ | Max |x| |
|---|---|---|---|---|---|
I₄ | 866 | 0.9839 | 500.0 | 2.17 rad/s | 0.136 m |
diag(1,1,10,10) pole-weightedポール重視 | 887 | 0.9842 | 500.0 | 2.14 rad/s | 0.143 m |
diag(10,10,1,1) cart-weighted台車重視 | 664 | 0.9786 | 500.0 | 1.89 rad/s | 0.081 m |
diag(0,0,1,1) pole onlyポールのみ | 240 | 1.0000 | 500.0 | 0.40 rad/s | 0.245 m |
The last row is the interesting one. With no cost on cart position or velocity the cart mode is uncontrolled: ρ = 1.0000 exactly, the closed loop is only marginally stable, and the cart drifts. It still scores 500.0, because 500 steps is not long enough to drift 2.4 m. Return says these four designs are identical. The spectral radius and the basin say the fourth is broken.
注目すべきは最後の行です。台車の位置にも速度にもコストがないため台車のモードは制御されず、ρはちょうど1.0000、 閉ループは安定限界にとどまり、台車はドリフトします。それでも500.0を出すのは、500ステップでは2.4 mドリフトするには足りないからです。 returnはこの4つの設計を同一と判定します。スペクトル半径とbasinは、4つ目が壊れていることを示しています。
The Riccati loop originally used an absolute tolerance of 1e−14. P has entries
near 7e3, so machine epsilon times 7e3 is about 1.5e−12 — the tolerance was below the float64
resolution of P itself and could only ever be met by luck. Q = I₄ landed on an
exact fixed point and converged; Q = diag(10,10,1,1) chattered in the last bit for 200 000
sweeps and looked exactly like a stabilisability failure. It was not. The general lesson, which comes
back in step 3: an absolute tolerance on a quantity whose scale you have not measured is not a
convergence criterion.
Riccatiのループは当初、1e−14の絶対許容誤差を使っていました。Pの要素は7e3前後なので、マシンイプシロン×7e3は約1.5e−12です。
つまり許容誤差はP自体のfloat64分解能を下回っており、満たされるかどうかは運次第でした。Q = I₄は厳密な不動点に到達して収束しましたが、
Q = diag(10,10,1,1)は最下位ビットで200 000回振動し続け、可安定性の破綻とまったく同じに見えました。実際はそうではありません。
step 3でも再び出てくる一般的な教訓は、スケールを計測していない量に対する絶対許容誤差は、収束判定にならないということです。
Step 3 — PPO, written outStep 3 — PPOを書き下す
PPO written out in full — GAE, the clipped objective, and the truncation bootstrap that is easy to get wrong.
PPOの完全な実装。GAE、clipped objective、そして間違えやすいtruncation時のbootstrap。
The four ingredients4つの要素
PPO is policy gradient plus four specific additions. Each is written longhand here, and each can be switched off from the command line — which is what step 5 measures.
PPOはpolicy gradientに4つの特定の追加要素を加えたものです。ここではそれぞれを手で書き下しており、 いずれもコマンドラインからOFFにできます。step 5で計測するのはまさにこの点です。
Architecture: separate 64-64 tanh actor and critic trunks, orthogonal initialisation (gain √2, 0.01 on the policy head, 1.0 on the value head), Adam with eps = 1e−5, linear learning-rate anneal, gradient-norm clipping at 0.5, and 8 environments × 128 steps = 1024-step batches.
構成:独立した64-64 tanhのactorとcritic、直交初期化(ゲイン√2、policy headは0.01、value headは1.0)、eps = 1e−5のAdam、 学習率の線形減衰、勾配ノルムのclip 0.5、8環境 × 128ステップ = 1024ステップのバッチ。
The truncation bootstrap — what step 1 was fortruncation時のbootstrap — step 1の意味
At the 500-step cap the pole has not fallen. The episode was cut by the clock.
Bootstrapping V(s_final) there is correct; treating it as terminal teaches the critic that
surviving 500 steps is worth zero future return. That is the single most common silent bug in a
hand-written PPO, and it is only avoidable here because step 1 refused to merge the two done flags.
500ステップの上限では、ポールは倒れていません。episodeは時間で打ち切られただけです。
そこでV(s_final)をbootstrapするのが正解で、終端として扱うと「500ステップ生き延びることの将来returnはゼロ」とcriticに教えてしまいます。
これは自作PPOで最も多い、表に出ないバグです。ここで回避できるのは、step 1で2つのdoneフラグの統合を拒否したからにほかなりません。
Implemented by folding the bootstrap into the reward of the truncated step,
r_t += γ·V(s_final), and then treating that step as an episode boundary for GAE — which it
is, since the next observation comes from a fresh reset.
実装では、bootstrapをtruncatedステップのrewardにr_t += γ·V(s_final)として組み込み、そのステップをGAE上のepisode境界として扱います。
次のobservationは新たなリセットから得られるので、実際に境界です。
Hyperparameters were searched — on seeds that are not the reported onesハイパーパラメータ探索 — 報告用とは別のseedで実施
The first configuration written down (lr 3e−4, 4 epochs) never reached threshold in 150 k steps, and reported a clip fraction of 0.000 on every single update: the policy was moving so little per update that the clip never engaged. That is worth stating rather than quietly deleting, because a PPO whose clip never engages is not PPO — it is A2C with extra arithmetic, and it would have made the step-5 clipping ablation come out as "makes no difference".
最初に書いた設定(lr 3e−4、4 epoch)は150 kステップで一度も閾値に到達せず、すべての更新でclip fractionが0.000でした。 1回の更新でpolicyがほとんど動かず、clipが一度も効いていなかったのです。これは黙って消すのではなく明記する価値があります。 clipが効かないPPOはPPOではなく、余計な計算をしているだけのA2Cであり、step 5のclip ablationが「差なし」という結果になっていたはずだからです。
The search ran on seeds 100–103, disjoint from the study seeds 0–15.
探索はseed 100–103で行い、study用のseed 0–15とは重複させていません。
| Configuration設定 | Solved達成 | Steps-to-threshold, median閾値到達ステップ数(中央値) | IQR |
|---|---|---|---|
| lr 2e−3, 10 epochs — adopted10 epoch — 採用 | 4/4 | 63 028 | [62 020, 63 428] |
| lr 1e−3, 8 epochs, 8 minibatches | 4/4 | 66 520 | [65 076, 69 772] |
| lr 1.5e−3, 10 epochs | 4/4 | 73 412 | [68 910, 78 664] |
| lr 1e−3, 10 epochs | 4/4 | 76 104 | [73 346, 85 050] |
| lr 2.5e−3, 4 epochs | 3/4 | 112 104 | [83 832, 141 888] |
| lr 3e−4, 10 epochs | 2/4 | 148 808 | — |
| lr 1e−3, 4 epochs | 1/4 | >150 000 | — |
| lr 3e−4, 4 epochs — first attempt4 epoch — 最初の試行 | 0/4 | >150 000 | — |
With the adopted configuration the clip fraction runs at 0.03–0.05 instead of 0.000.
採用した設定では、clip fractionは0.000ではなく0.03–0.05で推移します。
Cost, and a prediction confirmedコストと、確認された予測
nice 10nice 10で344 storch.set_num_threads(1), deliberately. The networks are 64×64;
intra-op threading costs more in synchronisation than it saves, and this machine shares eight threads
with a detection-training job that has priority.torch.set_num_threads(1)としています。ネットワークは64×64で、演算内のマルチスレッド化は削減分より同期コストの方が大きくなります。
またこのマシンの8スレッドは、優先度の高い物体検出の学習ジョブと共有しています。Step 1 predicted the environment would not be the bottleneck — 374 k steps/s standalone against 13 k inside training. Confirmed: the environment is about 3 % of the wall clock. The vectorised rewrite was never needed and was never written.
step 1では、環境はボトルネックにならないと予測していました(単体で374 k steps/s、学習中は13 k)。結果はそのとおりで、 環境が占めるのは実時間の約3 %です。ベクトル化した書き直しは不要で、実装もしていません。
Steps 4 and 5 — seeds and ablationsStep 4・5 — seedとablation
Sixteen seeds, four ablations, and a permutation test written without scipy.
16 seed、4種のablation、そしてscipyを使わずに書いた並べ替え検定。
The seed studyseed別の検証
| Baseline PPO | LQR | |
|---|---|---|
| solved (≥475 over 100 consecutive episodes)達成(連続100 episodeで≥475) | 16/16 | yes達成 |
| steps-to-threshold, median閾値到達ステップ数(中央値) | 62 144 | 0 |
| steps-to-threshold, IQR閾値到達ステップ数(IQR) | [60 442, 63 448] | — |
| greedy evaluation, 100 episodes, mediangreedy評価、100 episode、中央値 | 500.0 | 500.0 |
| greedy evaluation, worst seedgreedy評価、最悪seed | 500.0 | — |
PPO matches the baseline and pays 62 144 environment steps for what the model gave away free. That is not an argument against PPO — it is a precise statement of what PPO is buying, which is independence from the model, and of what that costs on a plant where the model happens to be exact.
PPOはbaselineに並びますが、モデルがタダでくれたものに62 144環境ステップを支払っています。これはPPOを否定する主張ではありません。 PPOが手に入れているもの(モデルからの独立性)と、たまたまモデルが厳密なプラントでそれにかかるコストを、正確に述べたものです。
Censoring. A run that never reaches threshold has no steps-to-threshold.
Dropping those and taking the median of the survivors is the standard way to make a bad configuration
look good, so they are entered as budget + 1 instead. The median is then correct whenever
fewer than half the runs fail, and reports as >150000 when more than half do. Solved
counts are printed next to every median.
Censoringの扱い。閾値に到達しなかったrunには閾値到達ステップ数がありません。それらを除いて残りの中央値を取るのは、
悪い設定を良く見せる典型的な手口です。そのため代わりにbudget + 1として記録します。こうすると失敗が半数未満なら中央値は正しく、
半数を超えれば>150000と報告されます。すべての中央値の横に達成数を併記しています。
Ablations — one switch at a time, 16 seeds eachAblation — スイッチを1つずつ、各16 seed
| Ablation | Solved達成 | Steps medianステップ数中央値 | vs baseline対baseline | Permutation p並べ替え検定 p |
|---|---|---|---|---|
| baseline | 16/16 | 62 144 | 1.00× | — |
| no advantage normalisationadvantage正規化なし | 16/16 | 64 884 | 1.04× | 0.054 |
| no GAE (λ = 1)GAEなし(λ = 1) | 16/16 | 68 948 | 1.11× | 0.0006 |
| no ratio clippingratio clipなし | 8/16 | 145 368 | 2.34× | 0.0001 |
Ratio clipping is the only one that actually matters, and it matters by a lot. Half the seeds never reach threshold without it. GAE is worth 11 % of the sample budget here, and advantage normalisation is not distinguishable from noise at the 5 % level on this task.
実際に効いているのはratio clipだけで、その効果は非常に大きいものです。clipがないとseedの半数が閾値に到達しません。 GAEの効果はここではサンプル予算の11 %分で、advantage正規化はこのタスクでは5 %水準でノイズと区別できません。
That ordering is task-specific and the reason is visible in the setup. Cart-pole episodes are short and γ = 0.99 over 128-step rollouts, so λ = 1 costs much less variance than it would on a long-horizon task. Clipping, by contrast, is what makes ten epochs on the same batch legal at all — without it the objective is a correct policy gradient for the first epoch and an increasingly wrong one for the next nine.
この順位はタスク固有のもので、理由は設定から読み取れます。cart-poleのepisodeは短く、128ステップのrolloutでγ = 0.99なので、 λ = 1による分散の増加は長ホライズンのタスクよりはるかに小さく済みます。一方clipは、同じバッチで10 epoch回すことをそもそも成立させるものです。 clipがなければ、目的関数は最初のepochでは正しいpolicy gradientですが、残り9 epochでは次第に誤ったものになります。
The study ran at eight seeds first. It gave: no GAE 1.03×, no advantage normalisation 1.11×. At sixteen: no GAE 1.11×, no advantage normalisation 1.04×. The ranking of the two small ablations is exactly reversed. Both runs were honest, both used the same code, and eight seeds — already above the usual "at least five" — was enough to get the order backwards. The conclusion that survived was only the large one. Small effects need either more seeds or an explicit statement that they were not resolved, and the p-values are reported so a reader can see which is which.
studyは最初8 seedで実施し、GAEなし1.03×、advantage正規化なし1.11×という結果でした。16 seedではGAEなし1.11×、advantage正規化なし1.04×です。 効果の小さい2つのablationの順位が完全に逆転しました。どちらの実行も正当で同じコードを使っており、 一般的な「最低5 seed」を上回る8 seedでも順位を取り違えるには十分だったということです。残った結論は効果の大きいものだけでした。 小さな効果には、seedを増やすか、判別できなかったと明記するかのどちらかが必要です。どれがどちらに当たるか読者が判断できるよう、p値を併記しています。
LQR against PPO, where it countsLQR対PPO — 差が出る指標で比べる
Where the learned policy beats the optimal linear controller, and where it plainly does not.
学習したpolicyが最適線形制御器に勝る点と、明らかに及ばない点。
Both controllers score 500.0/500. Step 2 already showed return saturates and cannot rank anything competent, so the comparison runs on the basin instead — the same bisection and the same widened initial-condition sweep, same plant, same termination test, LQR against all sixteen trained checkpoints.
どちらの制御器も500.0/500です。step 2で示したとおりreturnは飽和しており、一定以上の性能の制御器には順位を付けられません。 そこで比較はbasinで行います。同じ二分探索、同じく初期条件を広げたスイープ、同じプラント、同じ終了判定で、LQRと学習済みcheckpoint 16個すべてを比べます。
| LQR | PPO, median over 16 seedsPPO(16 seedの中央値) | PPO best seedPPO最良seed | |
|---|---|---|---|
| nominal return, 100 episodes公称条件でのreturn(100 episode) | 500.0 | 500.0 | 500.0 |
| critical θ̇₀臨界θ̇₀ | 2.169 rad/s | 1.082 rad/s | 2.004 rad/s |
| critical ẋ₀臨界ẋ₀ | 2.435 m/s | 0.824 m/s | 1.935 m/s |
| Failures out of 200 episodes, initial velocities widened200 episode中の失敗数(初期速度を拡大) | LQR | PPO medianPPO中央値 | PPO worst seedPPO最悪seed |
|---|---|---|---|
| U(−0.5, 0.5) | 0 | 1 | 22 |
| U(−1.0, 1.0) | 0 | 48 | 83 |
| U(−1.5, 1.5) | 6 | 88 | 115 |
| U(−2.0, 2.0) | 28 | 116 | 138 |
| U(−2.5, 2.5) | 61 | 140 | 158 |
PPO's basin is roughly half the LQR's on angular rate and a third on cart velocity, at identical nominal return. It learned the initial-state distribution it was trained on — U(−0.05, 0.05) on every state — and nothing outside it, because nothing outside it was ever sampled and the reward never asked. A policy that holds the nominal task with a small basin is a policy that falls over the first time a real plant hands it a disturbance the training distribution did not contain. Fixing that is what domain randomisation is for, and this measurement is the thing domain randomisation would have to move.
公称returnは同じでも、PPOのbasinは角速度でLQRの約半分、台車速度で約3分の1です。 PPOが学習したのは学習時の初期状態分布(全状態でU(−0.05, 0.05))だけで、その外側は何も学んでいません。外側は一度もサンプルされず、rewardもそれを求めなかったからです。 公称タスクはこなせてもbasinが狭いpolicyは、実機のプラントが学習分布にない外乱を与えた最初の瞬間に倒れます。 それを直すのがdomain randomisationの役割であり、この計測値こそdomain randomisationが動かさなければならない指標です。
The same finding, watchable in three windows同じ結果を3つのウィンドウで観察する
Same disturbance, applied from the exact origin, no random initialisation:
ランダム初期化なしで、厳密な原点から同じ外乱を与えます。
python visualize.py --controller lqr --zero-init --theta-dot0 1.5
python visualize.py --controller ppo --ckpt runs/study/baseline_seed2.pt --zero-init --theta-dot0 1.5
python visualize.py --controller ppo --ckpt runs/study/baseline_seed0.pt --zero-init --theta-dot0 1.5
| Controller制御器 | Outcome at θ̇₀ = 1.5 rad/sθ̇₀ = 1.5 rad/sでの結果 | Peak |θ|最大|θ| |
|---|---|---|
| LQR | survives 500 steps500ステップ維持 | 46 % of the limit限界の46 % |
| PPO seed 2 | falls at step 18 (0.36 s)18ステップ目で転倒(0.36 s) | 113 % |
| PPO seed 0 | survives 500 steps500ステップ維持 | 52 % |
Both findings in one experiment: the learned policy is worse than the model-based one at the median, and the spread across seeds is enormous — critical θ̇₀ is 2.004 rad/s for seed 0 and 0.831 rad/s for seed 2, from identical hyperparameters and an identical step budget. All three score 500.0 on the nominal task and are indistinguishable by return.
1つの実験で2つの結果が見えます。学習したpolicyは中央値でモデルベースの制御器より劣り、しかもseed間のばらつきが非常に大きいことです。 ハイパーパラメータもステップ予算も同一なのに、臨界θ̇₀はseed 0で2.004 rad/s、seed 2で0.831 rad/sです。3つとも公称タスクでは500.0を出し、returnでは区別できません。
--zero-init matters. Without it the random ±0.05 initialisation is still
present, and a negative θ₀ paired with a positive θ̇₀ is the pole rotating back towards upright —
easier, not harder. Every basin number is bisected from the origin, so reproducing them needs the flag.
And baseline_seed0 is the best of the sixteen, not a typical one; it is the default
checkpoint only because it is the first.
--zero-initは重要です。これがないと±0.05のランダム初期化が残り、負のθ₀と正のθ̇₀の組み合わせではポールが倒立方向へ戻る回転になるため、
難しくなるどころか易しくなります。basinの数値はすべて原点から二分探索したものなので、再現にはこのフラグが必要です。
またbaseline_seed0は16個の中で最良のものであり、典型例ではありません。デフォルトのcheckpointになっているのは、単に最初の1つだからです。
Step 6 — continuous actions on MuJoCoStep 6 — MuJoCo上の連続action
The same algorithm on continuous actions and MuJoCo physics, and the saturation problem that exposed.
同じアルゴリズムを連続actionとMuJoCoの物理で動かした結果と、そこで露呈した飽和の問題。
Two things change from steps 1–5 at once — the physics engine and the action space — so both are pinned down separately before anything is concluded.
step 1–5から物理エンジンとaction空間の2つが同時に変わるため、結論を出す前にそれぞれを個別に確定させます。
The plant is identical, verified to 1.4e−14プラントは同一 — 1.4e−14で検証
cartpole_mj.xml sets the inertias by hand rather than letting MuJoCo derive them from a
geometry primitive. A capsule adds hemispherical caps and a cylinder adds a 3r²/12 term; the
4/3 in the numpy plant is exactly the uniform-thin-rod assumption. Deriving inertia from the
geometry would have made the two plants nearly the same object, which is the worst possible
state to be in.
cartpole_mj.xmlでは、MuJoCoに形状プリミティブから慣性を導出させず、手で設定しています。capsuleなら半球の端部が加わり、
cylinderなら3r²/12の項が加わりますが、numpy版プラントの4/3はまさに一様な細い棒の仮定です。
形状から慣性を導出すると2つのプラントはほぼ同じものになってしまい、それが考えうる最悪の状態です。
Check V1 compares MuJoCo's qacc against the numpy accelerations over 5000 random states:
max absolute difference 1.42e−14. The two simulators describe the same rigid body.
Anything that differs from here on is the integrator or the action space, and cannot be blamed on the
model.
検証V1では、ランダムな5000状態についてMuJoCoのqaccをnumpyの加速度と比較し、最大絶対差は1.42e−14でした。
2つのシミュレータは同じ剛体を記述しています。ここから先に生じる差は積分器かaction空間によるもので、モデルのせいにはできません。
The integrator is different, and that is not reconcilable積分器は異なり、それは揃えられない
MuJoCo's Euler is semi-implicit: velocity first, then position advanced with the new velocity. The numpy plant uses the explicit ordering because that is what the reference cart-pole does.
MuJoCoのEuler法は半陰的で、先に速度を更新し、その新しい速度で位置を進めます。numpy版プラントは陽的な順序を使っています。 参照実装のcart-poleがそうしているからです。
| Energy drift, zero control, θ₀ = 0.5 radエネルギードリフト(制御なし、θ₀ = 0.5 rad) | at 50 steps50ステップ | at 500 steps500ステップ |
|---|---|---|
| MuJoCo semi-implicitMuJoCo(半陰的) | +6.69 % | +9.85 % |
| explicit Euler (step 1)陽的Euler法(step 1) | +0.43 % | +145.0 % |
Semi-implicit Euler is worse in the short run and dramatically better in the long run: its energy error oscillates with bounded amplitude, while the explicit version's grows without bound. Neither is "the correct plant" — they are two discretisations of the same continuous system, which is why step 6 gets its own baseline instead of borrowing step 4's numbers. One-step gap between them: median |Δθ| 5.84e−3 rad, 3.35 % of the failure angle.
半陰的Euler法は短期的には劣り、長期的には圧倒的に優れています。エネルギー誤差は有界な振幅で振動するのに対し、陽的版の誤差は際限なく増えます。 どちらも「正しいプラント」ではなく、同じ連続系の2通りの離散化です。だからこそstep 6はstep 4の数値を借りず、独自のbaselineを持ちます。 両者の1ステップあたりの差は、|Δθ|の中央値で5.84e−3 rad、失敗角度の3.35 %です。
The step-2 gain transfers unchanged — the smallest honest sim-to-real argument herestep 2のゲインはそのまま移植できる — このページで最小かつ正直なsim-to-realの論拠
| Step-2 gain K applied to the MuJoCo plantstep 2のゲインKをMuJoCoプラントに適用 | Mean return, 100 episodes平均return(100 episode) |
|---|---|
| bang-bang, sign only, ±10 Nbang-bang(符号のみ、±10 N) | 500.0 (min 500) |
| continuous, clipped to ±10 N連続値(±10 Nでclip) | 500.0 (min 500) |
The gain was computed against the numpy plant, in step 2, before this file existed. A model-based design survived being moved to a different integrator at zero cost, because it depends on the plant and not on the discretisation. That is the one thing the learned policy in step 4 was never asked to do.
このゲインは、このファイルが存在する前のstep 2でnumpy版プラントに対して計算したものです。モデルベースの設計は、 別の積分器に移してもコストゼロでそのまま機能しました。依存しているのがプラントであって離散化ではないからです。 これこそ、step 4の学習したpolicyには一度も求められなかったことです。
The Gaussian policy, and a bias that is reported rather than hiddenGaussian policyと、隠さずに報告するバイアス
Only the head changes: Categorical(logits) becomes Normal(μ(s), exp(log σ))
with log σ a free parameter, log-probability summed over action dimensions, and Gaussian
differential entropy. The entropy coefficient defaults to 0.0 here against 0.01 in the
discrete version, because Gaussian differential entropy is unbounded below and can go negative, so an
entropy bonus pushes σ up with no floor — the same coefficient does not mean the same thing in the two
files.
変わるのは出力層(head)だけです。Categorical(logits)がNormal(μ(s), exp(log σ))になり、log σは自由パラメータ、
対数確率はaction次元で総和を取り、エントロピーはGaussianの微分エントロピーを使います。エントロピー係数のデフォルトは離散版の0.01に対しここでは0.0です。
Gaussianの微分エントロピーは下に有界でなく負にもなりうるため、エントロピーボーナスはσを下限なしに押し上げてしまうからです。
同じ係数でも、2つのファイルでは意味が異なります。
The Gaussian has support on all of ℝ; the actuator saturates at ±1. The action is clipped at the environment boundary while the log-probability is computed on the unclipped sample, so every out-of-range sample is credited with a density it did not act under. The honest alternatives are a tanh-squashed policy with the change-of-variables correction, or a Beta policy on the bounded interval; neither is used. Instead every run reports its saturation fraction, so the size of the problem is visible in the results table.
Gaussianの台はℝ全体ですが、アクチュエータは±1で飽和します。actionは環境の境界でclipされる一方、対数確率はclip前のサンプルで計算されるため、 範囲外のサンプルはすべて、実際には作用していない確率密度で評価されます。正しい代替策は、変数変換の補正付きのtanh-squashed policyか、有界区間上のBeta policyですが、 どちらも使っていません。代わりに各runで飽和率を報告し、問題の大きさが結果表で見えるようにしています。
The search table makes the case for why initialising σ matters on a bounded action space. At
init_log_std = 0, σ = 1 on a ±1 action range means 37 % of sampled actions are
outside the actuator range — the policy spends most of its probability mass on actions the plant
cannot execute, and it costs roughly 50 % more environment steps. The adopted setting, −1.0, ends at
σ = 0.133 with 0.5 % saturation.
探索結果の表は、有界なaction空間でσの初期化が重要である理由を示しています。init_log_std = 0では±1のaction範囲に対してσ = 1となり、
サンプルしたactionの37 %がアクチュエータの範囲外になります。policyは確率質量の大半をプラントが実行できないactionに費やし、
環境ステップが約50 %余分にかかります。採用した設定−1.0では、最終的にσ = 0.133、飽和率0.5 %です。
Seed study and ablations, continuous連続版のseed studyとablation
| Solved達成 | Steps medianステップ数中央値 | IQR | Greedy evalgreedy評価 | |
|---|---|---|---|---|
discrete, ppo.py (step 4)離散、ppo.py(step 4) | 16/16 | 62 144 | [60 442, 63 448] | 500.0 |
continuous, ppo_continuous.py連続、ppo_continuous.py | 16/16 | 63 748 | [62 008, 65 636] | 500.0 |
Moving from two discrete actions to a one-dimensional Gaussian costs 2.6 % of the sample budget on this task, and the interquartile ranges overlap. That is smaller than expected, and it is not evidence that continuous control is free in general — it is evidence that on a plant this small, with the action range matched to the force the discrete policy was already applying, the extra difficulty of learning a mean and a standard deviation is nearly paid for by the finer control authority.
2つの離散actionから1次元のGaussianに移ると、このタスクではサンプル予算が2.6 %増え、四分位範囲は重なります。予想より小さな差ですが、 連続制御が一般にコストなしである証拠ではありません。これほど小さなプラントで、action範囲を離散policyが既に加えていた力に合わせた場合には、 平均と標準偏差の両方を学習する難しさが、より細かな制御能力でほぼ相殺される、という証拠にすぎません。
| Ablation | Solved達成 | Steps medianステップ数中央値 | vs baseline対baseline | p | Final σ最終σ | Saturation飽和率 |
|---|---|---|---|---|---|---|
| baseline | 16/16 | 63 748 | 1.00× | — | 0.136 | 0.5 % |
| no advantage normalisationadvantage正規化なし | 16/16 | 64 664 | 1.01× | 0.73 | 0.144 | 0.1 % |
| no GAE (λ = 1)GAEなし(λ = 1) | 16/16 | 71 760 | 1.13× | 0.0032 | 0.218 | 0.8 % |
| no ratio clippingratio clipなし | 0/16 | >150 000 | 2.35× | 0.0008 | 0.088 | 28.2 % |
Without clipping the discrete policy still gets half its seeds to threshold. The Gaussian policy gets
none. The mechanism is visible in the two right-hand columns: no_clip ends
with the smallest σ of any configuration (0.088) and by far the largest saturation
(28.2 %, up to 80.6 % on the worst seed). A Gaussian with σ = 0.088 only puts 28 % of its mass outside
[−1, 1] if the mean sits at roughly ±0.95 — the mean has been driven onto the actuator rail while the
standard deviation collapsed around it. With an importance ratio unbounded above on a continuous
density, one favourable minibatch moves the mean as far as the gradient points, and the nine remaining
epochs on that batch reinforce a policy that is already off the plant's control range. A categorical
ratio cannot do this — it is bounded by 1/π_old(a) on a two-element support, and the worst
it can do is become deterministic between two executable actions.
clipがなくても、離散policyではseedの半数が閾値に到達します。Gaussian policyでは1つも到達しません。
その仕組みは右側の2列に表れています。no_clipは全設定の中で最終σが最も小さく(0.088)、飽和率は群を抜いて最も大きい
(28.2 %、最悪seedでは80.6 %)です。σ = 0.088のGaussianが質量の28 %を[−1, 1]の外に置くのは、平均が約±0.95にある場合だけです。
つまり平均がアクチュエータの上限に張り付き、その周りで標準偏差がつぶれています。連続の確率密度ではimportance ratioに上限がないため、
都合の良いミニバッチが1つあれば平均は勾配の向きにどこまでも動き、そのバッチでの残り9 epochが、既にプラントの制御範囲外にあるpolicyをさらに強化します。
categoricalのratioではこれは起こりません。2要素の台では1/π_old(a)で上限があり、最悪でも実行可能な2つのactionの間で決定的になるだけです。
So the conclusion is narrower than "clipping matters more in continuous control". It is: on an action space where the policy can place mass outside the actuator range, removing the clip is not a slowdown, it is a failure mode.
したがって結論は「連続制御ではclipがより重要」よりも限定的です。policyがアクチュエータの範囲外に確率質量を置けるaction空間では、 clipを外すことは学習の遅れではなく、失敗モードになる、ということです。
What this does not prove, and one process failureこの結果が証明しないことと、1つのプロセス上の失敗
The limits of the comparison, plus one mistake in the process worth recording.
比較の限界と、記録しておくべきプロセス上の誤り。
- No real exploration problem. Cart-pole rewards every timestep and a random policy already scores 22. The ablation ordering would very likely change on a sparse-reward task.
- The ablations are one-at-a-time. Interactions were not measured.
- One hyperparameter configuration was searched over eight candidates on four seeds. A different configuration could change the ablation magnitudes — though the clipping result is large enough that it is unlikely to flip.
no_clipfailing is measured at this learning rate and epoch count. Clipping matters because of the ten epochs; at one epoch it would matter much less.- The basin comparison uses the greedy policy. The stochastic policy used during training has a different, smaller basin.
- Step 6 changes the action space, not the task. The plant, the reward and the horizon are the same, so it tests whether the conclusion is an artefact of the categorical head — not whether it holds anywhere else.
- 本当の意味での探索問題がない。cart-poleは毎タイムステップrewardを与え、ランダムpolicyでも既に22点を取ります。 sparse rewardのタスクでは、ablationの順位はおそらく変わります。
- ablationは1つずつ。要素間の相互作用は計測していません。
- ハイパーパラメータ設定は1つで、4 seed・8候補から探索したものです。別の設定ではablationの大きさが変わる可能性があります。 ただしclipの結果は十分に大きいため、逆転する可能性は低いと考えます。
no_clipの失敗はこの学習率とepoch数で計測したもの。clipが重要なのは10 epochだからで、1 epochなら影響はずっと小さくなります。- basinの比較はgreedy policyで行っている。学習中に使う確率的policyのbasinは別物で、より狭くなります。
- step 6で変わるのはaction空間であり、タスクではない。プラント、reward、ホライズンは同じなので、 検証しているのは結論がcategorical headによる見かけの効果かどうかであって、他の条件でも成り立つかどうかではありません。
Box etiquette, and the time the guard failedマシン共有のルールと、ガードが機能しなかった件
This machine shares eight threads with a higher-priority detection-training job. Every entry point
that starts training calls boxcheck.require_quiet_box() and refuses above a 1-minute load
average of 4.0.
このマシンの8スレッドは、優先度の高い物体検出の学習ジョブと共有しています。学習を開始するエントリポイントはすべて
boxcheck.require_quiet_box()を呼び、1分間のload averageが4.0を超えていれば開始を拒否します。
That guard originally lived only in study.py, and it did not help. The step-6
hyperparameter sweep was launched from a bare shell loop, which never called study.py, and
it ran straight through a training job using 770 % CPU. A guard on the front door is not a
guard, so it now lives in its own module and is called by every training entry point alike,
and it prints the offending process.
当初このガードはstudy.pyの中にしかなく、役に立ちませんでした。step 6のハイパーパラメータスイープは素のシェルループから起動したため
study.pyを一度も通らず、CPU 770 %を使う学習ジョブの最中にそのまま走ってしまいました。正面玄関だけのガードはガードではありません。
そのため現在は独立したモジュールにして、学習のエントリポイントすべてから同じように呼び、原因のプロセスを表示するようにしています。
The consequences are stated rather than buried:
影響は伏せずに明記します。
- The step-6 search wall-clock numbers are contaminated and are not reported. The learning outcomes are not: runs are seeded and single-threaded, and two identical configurations submitted under different tags during that window returned bitwise-identical steps-to-threshold — which is the evidence that contention moved the clock and nothing else.
- A throughput figure measured under load was re-measured and proved wrong: contended 73 962 steps/s, quiet box 145 458 steps/s — 1.97×. So MuJoCo is 2.6× slower than the numpy environment rather than the 5× first recorded. A throughput number carries the load average it was taken under, or it carries nothing.
- Steps 1–5 are clean. The 64-run study finished six minutes before the detection job started, and the guard passed at launch.
- step 6の探索の実時間の数値は汚染されているため報告していません。学習結果は汚染されていません。runはseed固定かつシングルスレッドで、 その時間帯に別タグで投入した同一設定2つが、ビット単位まで同一の閾値到達ステップ数を返しました。競合で変わったのは時間だけで、それ以外は何も変わっていない証拠です。
- 負荷下で計測したthroughputは再計測で誤りと判明しました。競合時73 962 steps/s、静かな状態で145 458 steps/sで、1.97×の差です。 したがってMuJoCoはnumpy環境より、最初に記録した5×ではなく2.6×遅いだけです。throughputの数値は、計測時のload averageとセットでなければ意味がありません。
- step 1–5は汚染されていません。64回のstudyは物体検出ジョブ開始の6分前に終わっており、起動時にガードも通過しています。
Running it実行方法
Setup, the commands in order, and the runtimes to expect on a CPU.
セットアップ、実行するコマンドの順序、CPUでの想定実行時間。
git clone https://github.com/AungKaung1928/ppo-from-scratch.git
cd ppo-from-scratch
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
python verify_env.py # step 1, ~6 s, one core
python lqr.py # step 2, ~30 s, one core
python ppo.py --seed 0 # step 3, one 150k-step run, ~11 s
python study.py --mode all # steps 3-5, 96 runs, ~8 min, 4-way parallel
python compare.py # LQR vs PPO basin comparison, ~3 min
python mj_cartpole.py # step 6 environment, 6/6 checks, ~2 min
python study.py --family continuous --mode ablations --out runs_c # ~7 min
python visualize.py --controller lqr # MuJoCo viewer
python visualize.py --controller lqr --record out/frames # offscreen, no window
numpy and CPU torch are the only hard dependencies. mujoco is
imported lazily and is needed only by the step-6 environment and the visualiser — the cart-pole plant
itself is numpy, so every number in the repository reproduces without a physics engine installed at all.
必須の依存はnumpyとCPU版torchだけです。mujocoは遅延importで、必要なのはstep 6の環境と可視化ツールだけです。
cart-poleのプラント自体はnumpyなので、リポジトリ内のすべての数値は物理エンジンをまったくインストールせずに再現できます。
visualize.py opens a window, so it needs a rendering backend: set MUJOCO_GL to
whatever your machine has (glfw, egl, osmesa).
--record writes frames offscreen and needs no window.
visualize.pyはウィンドウを開くため、描画バックエンドが必要です。MUJOCO_GLをマシンで使えるもの
(glfw、egl、osmesa)に設定してください。--recordはオフスクリーンでフレームを書き出すため、ウィンドウは不要です。
The short versionまとめ
The whole project compressed into one paragraph.
プロジェクト全体を1段落に凝縮。
A cart-pole environment hand-written from the 1983 equations and verified thirteen ways — including against an independent solve of the Lagrangian (2.6e−15) and a closed-form divergence rate derivable on paper (3.4e−11). An LQR baseline built by linearising, discretising exactly, and iterating the Riccati recursion by hand, converging in 866 sweeps against a theory prediction of 923, scoring a perfect 500.0 at zero sample cost. PPO then written from scratch — no gymnasium, no stable-baselines3, no cleanrl — and matched to that baseline for 62 144 environment steps across 16/16 seeds. Ablations over sixteen seeds with a permutation test show ratio clipping is the only ingredient that dominates (2.34×, 8/16 solved without it), while GAE is worth 11 % and advantage normalisation is not resolved at the 5 % level. Because return saturates and cannot rank competent controllers, the real comparison runs on the basin of attraction, where PPO recovers from roughly half the angular-rate disturbance and a third of the cart velocity that LQR does, at identical nominal score. A stretch step repeats it with a Gaussian policy on a MuJoCo plant proved identical to 1.4e−14, where removing the clip stops being a slowdown and becomes a 0/16 failure mode. No reward curve appears anywhere in the repository.
1983年の式から自作し、13通りの方法で検証したcart-pole環境。検証にはLagrangianの独立解との照合(2.6e−15)と、紙の上で導出できる閉形式の発散率との照合(3.4e−11)を含みます。 線形化、厳密な離散化、Riccati漸化式の手動反復で構築したLQR baselineは、理論予測923回に対して866回で収束し、サンプルコストゼロで満点の500.0を出します。 続いてPPOをゼロから実装し(gymnasium、stable-baselines3、cleanrlは不使用)、16/16 seedで62 144環境ステップをかけてbaselineに並びました。 16 seedのablationと並べ替え検定から、支配的な要素はratio clipだけであり(2.34×、clipなしでは8/16のみ達成)、GAEの効果は11 %、advantage正規化は5 %水準で判別できないことが分かりました。 returnは飽和して一定以上の制御器に順位を付けられないため、実際の比較はbasin of attractionで行いました。公称スコアは同じでも、PPOが復帰できる外乱はLQRに比べ角速度で約半分、台車速度で約3分の1です。 発展ステップとして、1.4e−14の精度で同一と証明したMuJoCoプラント上でGaussian policyを用いて同じ比較を繰り返し、そこではclipを外すことが学習の遅れではなく0/16の失敗モードになりました。 リポジトリにはreward曲線が1本もありません。