← Aung Kaung Myat/Walkthroughsプロジェクト解説
CPU-only robot-learning track · secondary project · repo ppo-from-scratchCPUのみのrobot learningトラック · サブプロジェクト · リポジトリ ppo-from-scratch

PPO Against LQR

PPO implemented from nothing, then measured against an LQR controller you can solve for with pen and paper. Both score a perfect 500.0 — PPO paid 62 144 environment steps for it, and its recoverable region is half the size, a difference the usual score cannot see.

強化学習をゼロから実装し、紙とペンで解けるLQR制御器と比較計測しました。どちらも500.0点満点ですが、PPOはそのために62 144環境ステップを費やし、復帰できる領域は半分の広さです。この差は通常のスコアには現れません。

PyTorch (CPU)NumPyMuJoCo no gymnasiumno stable-baselines3 no scipyno cleanrl
Environment環境
hand-written, 13/13 checks自作・検証13/13
Baseline基準制御器
LQR · 0 samplesサンプル0
PPO costPPOのコスト
62 144 steps · 16/16 seeds62 144ステップ · 16/16 seed
One training run学習1回あたり
11.4 s
Seeds per result結果あたりのseed数
16
Reward curves shown掲載したreward曲線
zero, on purpose0本(意図的)
00

At a glance要点

Five metrics, two controllers: where PPO matches the textbook answer, what it paid, and where it falls short.5つの指標、2つの制御器。PPOが教科書の答えに並ぶ点、支払ったコスト、及ばない点。

LQR vs PPO, metric by metric — same cart-pole, same termination testLQR vs PPO、指標ごとの比較 — 同じcart-pole、同じ終了判定
Each row is scaled to its own larger value. PPO values are the median of 16 seeds.各行は大きい方の値を基準にスケーリング。PPOの値は16 seedの中央値。
Samples to threshold (env steps)閾値到達までのサンプル数(環境ステップ)lower is better少ないほど良いLQR: 0 environment steps — solved in closed formPPO: 62 144 environment steps, median of 16 seeds (IQR 60 442–63 448)062 144Time to a working controller制御器が得られるまでの時間lower is better短いほど良いLQR: 866 Riccati sweeps in 22 msPPO: one 150 k-step training run, 11.4 s on one core22 ms11.4 sNominal return, of 500公称条件でのreturn(500点満点)tie — metric saturated同点 — 指標は飽和LQR: 500.0 of 500, 100 episodesPPO: 500.0 of 500, median of 16 seeds, worst seed 500.0500.0 = 500.0Recovery basin — critical θ̇₀復帰可能領域 — 臨界θ̇₀higher is better大きいほど良いLQR: critical θ̇₀ 2.169 rad/sPPO: critical θ̇₀ 1.082 rad/s, median of 16 seeds (best seed 2.004)2.169 rad/s1.082 rad/sRecovery basin — critical ẋ₀復帰可能領域 — 臨界ẋ₀higher is better大きいほど良いLQR: critical ẋ₀ 2.435 m/sPPO: critical ẋ₀ 0.824 m/s, median of 16 seeds (best seed 1.935)2.435 m/s0.824 m/s
LQRPPO, median of 16 seedsPPO(16 seedの中央値)
Numbers as a table数値を表で見る
Metric指標LQRPPO, medianPPO中央値PPO best seedPPO最良seed
steps to threshold閾値到達ステップ数062 144—
time to a controller制御器が得られるまでの時間22 ms11.4 s—
nominal return, of 500公称return(500点満点)500.0500.0500.0
critical θ̇₀臨界θ̇₀2.169 rad/s1.082 rad/s2.004 rad/s
critical ẋ₀臨界ẋ₀2.435 m/s0.824 m/s1.935 m/s
The one PPO ingredient that dominates — ratio clipping, 16 seeds eachPPOで支配的な唯一の要素 — ratio clip(各16 seed)
With clipping (baseline)clipあり(baseline) 16/16

solved · 62 144 steps median. Discrete and continuous alike.

達成 · 中央値62 144ステップ。離散・連続とも同じ。

Without clippingclipなし 8/16 → 0/16

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で順位付けしています。
01

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曲線を描いて終わります。 このプロジェクトは別の問いを立てます。その違いこそが本題です。

The question問い

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このページの用語

cart-poleA pole hinged on a cart. Push the cart left or right to keep the pole upright. The standard first control problem.台車にヒンジで取り付けたポール。台車を左右に押してポールを立て続けます。最初に扱う定番の制御問題です。
LQRLinear-quadratic regulator — the optimal controller for a linearised system, solved with matrix algebra. No learning involved.Linear-quadratic regulator。線形化したシステムに対する最適制御器で、行列計算で解きます。学習は一切ありません。
PPOProximal policy optimisation — a reinforcement-learning algorithm that limits how far each update is allowed to move the policy.Proximal policy optimisation。1回の更新でpolicyが動ける幅を制限する強化学習アルゴリズムです。
GAEGeneralised advantage estimation — how the algorithm estimates whether an action was better or worse than average.Generalised advantage estimation。あるactionが平均より良かったか悪かったかを推定する方法です。
episode / returnOne run from reset until failure or the time limit. The return is the total reward collected in it.リセットから失敗または制限時間までの1回の試行がepisodeです。returnはその間に得たrewardの合計です。
terminated vs truncatedTerminated means the pole actually fell. Truncated means the clock ran out while it was still up. Treating them the same corrupts the value estimate.terminatedはポールが実際に倒れたこと、truncatedはポールが立ったまま時間切れになったことを指します。両者を同じに扱うと価値推定が壊れます。
seedThe random-number starting point. One seed proves nothing, so results here are reported across sixteen.乱数の初期値。seed 1つでは何も証明できないため、ここでは16 seedで結果を報告しています。
basin of attractionThe set of starting states a controller can still recover from. Wider is better.制御器が復帰できる初期状態の集合。広いほど良いです。
02

The six steps6つのステップ

The six build steps, and what each had to produce before the next could start.

構築の6ステップと、次へ進む前に各ステップで出す必要があった成果。

step 1The environment環境 Cart-pole written from the published equations, verified against an independent solve of the Lagrangian.公開された式からcart-poleを実装し、Lagrangianを独立に解いた結果と照合して検証。 13/13 checks検証13/13
step 2LQR baselineLQRによるbaseline Linearise, iterate the Riccati recursion, project to two actions. Zero sample cost by construction.線形化し、Riccati漸化式を反復し、2つのactionに射影。構造上サンプルコストはゼロ。 7/7 checks · 500.0検証7/7 · 500.0
step 3PPO from scratchPPOをゼロから実装 GAE, advantage normalisation, the clipped surrogate, multi-epoch minibatching — each switchable.GAE、advantage正規化、clipped surrogate、複数epochのミニバッチ更新。いずれも個別にON/OFF可能。 358 lines · 11.4 s/run358行 · 11.4 s/回
step 4Seed studyseed別の検証 16 seeds, median and interquartile range, steps-to-threshold with censoring handled honestly.16 seed、中央値と四分位範囲。閾値到達ステップ数は打ち切り(censoring)を正直に扱って集計。 16/16 · 62 144 steps16/16 · 62 144ステップ
step 5AblationsAblation実験 One switch off at a time, 16 seeds each, permutation test for significance.スイッチを1つずつOFFにし、それぞれ16 seed。有意性は並べ替え検定で判定。 clipping dominatesclipの影響が最大
step 6Continuous actions連続action Gaussian policy on a MuJoCo plant proved identical to the numpy one. Does the ranking survive?numpy版と同一であることを証明したMuJoCoプラント上でGaussian policyを学習。順位は維持されるか。 16 seeds + 3 ablations16 seed + ablation 3種
03

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.py209 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.py456 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.py496 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.py360 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.py305 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.py291 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.py277 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.py148 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.py172 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.py49 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.xml42 · 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。
04

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結果
1Scalar 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
2aEnergy conserved under RK4, F = 0, 2 s at dt = 1e−4RK4でのエネルギー保存。F = 0、dt = 1e−4で2 s2.4e−14
2bHorizontal momentum conserved, same run水平方向の運動量保存(同じ実行)5.4e−15
3Pinned cart, divergence rate against the closed form √(3g/4l)台車固定時の発散率を閉形式√(3g/4l)と照合3.834058 vs 3.834058, rel err相対誤差 3.4e−11
4a–dTruncation 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完全一致
4eAction sign: bang-bang on θ + 0.5·θ̇ balancesactionの符号確認:θ + 0.5·θ̇によるbang-bangで倒立を維持500.0 / 500
5a–bSame seed gives a bitwise identical trajectory; the environment ignores the global numpy RNG同じseedでビット単位まで同一のtrajectory。環境はnumpyのグローバルRNGの影響を受けないexact完全一致
6Random-policy mean return against the published ≈22, over 10 000 episodesランダムpolicyの平均returnを文献値≈22と比較(10 000 episode)22.09 ± 0.12
7Golden-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桁まで再現します。

What none of this provesこれらが証明しないこと

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つの判断

explicit Euler陽的Euler法
Position is advanced with the old velocity. Semi-implicit Euler is the better integrator and is deliberately not used, because the deliverable is a number ("475/500") that is only comparable to published results if the plant is the same plant. The cost is measured rather than hidden: after 25 controlled steps the two integrators differ by 7.8e−3 rad, which is 3.7 % of the failure angle.
位置は更新前の速度で進めます。半陰的Euler法の方が優れた積分器ですが、あえて使っていません。 成果物は「475/500」という数値であり、プラントが同一でなければ公表結果と比較できないからです。 そのコストは隠さず計測しました。制御下で25ステップ後、2つの積分器の差は7.8e−3 radで、失敗角度の3.7 %です。
no combined 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点に相当する、実際に起こるバグです。
no vectorised environmentベクトル化環境なし
The plan assumed the Python-loop environment would be the bottleneck. Measured: 374 000 steps/s, so a 200 k-step run spends 0.5 s total inside the environment — about 1 % of the budget. It was never written, and step 3 confirmed it was never needed.
当初の計画では、Pythonループの環境がボトルネックになると想定していました。計測すると374 000 steps/sで、 200 kステップの実行でも環境内で費やすのは合計0.5 s、全体の約1 %です。そのため実装せず、step 3で不要だったことも確認しました。
A comparison that was deliberately not reportedあえて報告しなかった比較

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ステップの上限)はすべて一致しています。

05

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結果
L1Analytic Ac, Bc against a central-difference Jacobian of the derivative解析的なAc, Bcを微分の中心差分Jacobianと照合3.78e−12
L2Ad = I + τ·Ac against a finite-difference Jacobian of the actual stepAd = I + τ·Acを実際のstepの有限差分Jacobianと照合7.57e−14
L3Converged P satisfies the discrete algebraic Riccati equation収束したPが離散代数Riccati方程式を満たすresidual残差 6.63e−10
L4Closed-loop spectral radius below 1閉ループのスペクトル半径が1未満0.983919
L5xᵀPx against the simulated linear closed-loop cost-to-goxᵀPxを線形閉ループのシミュレーションによるcost-to-goと照合max rel最大相対差 5.46e−12
L6Scaling both Q and R by c leaves the gain unchanged, c ∈ [1e−3, 1e3]QとRを両方c倍してもゲインは不変(c ∈ [1e−3, 1e3])5.81e−16
L7Mean return ≥ 475 over 100 consecutive episodes連続100 episodeの平均return ≥ 475500.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が自らの価値を示すべき基準値

Baseline performanceBaselineの性能 500.00

Mean return over 1000 episodes. Zero failures. Metric fully saturated.

1000 episodeの平均return。失敗ゼロ。指標は完全に飽和しています。

Baseline sample costBaselineのサンプルコスト 0 steps0ステップ

The design consumed no environment interaction at all. Steps-to-threshold is 0 by construction.

設計に環境とのやり取りを一切使っていません。閾値到達ステップ数は構造上0です。

The framing this step deliversこのステップで決まる前提

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/s1.9632 rad/s
critical ẋ₀臨界ẋ₀2.4354 m/s1.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が隠すものを示す行

QSweeps反復回数ρReturnCritical θ̇₀臨界θ̇₀Max |x|
I₄8660.9839500.02.17 rad/s0.136 m
diag(1,1,10,10) pole-weightedポール重視8870.9842500.02.14 rad/s0.143 m
diag(10,10,1,1) cart-weighted台車重視6640.9786500.01.89 rad/s0.081 m
diag(0,0,1,1) pole onlyポールのみ2401.0000500.00.40 rad/s0.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つ目が壊れていることを示しています。

A bug worth writing down記録に値するバグ

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でも再び出てくる一般的な教訓は、スケールを計測していない量に対する絶対許容誤差は、収束判定にならないということです。

06

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で計測するのはまさにこの点です。

GAE(γ, λ)
Generalised advantage estimation. "How much better was this action than average?" can be estimated from one step (low variance, biased) or from the whole episode (unbiased, high variance). GAE interpolates between them with λ.
Generalised advantage estimation。「このactionは平均よりどれだけ良かったか」は、1ステップから推定する(低分散だがバイアスあり)ことも、 episode全体から推定する(不偏だが高分散)こともできます。GAEはλでその間を補間します。
advantage normalisationadvantage正規化
Standardise the advantages within each batch so the gradient scale does not depend on how large the rewards happen to be.
バッチごとにadvantageを標準化し、勾配のスケールがrewardの大きさに左右されないようにします。
the clipped surrogate
The core of PPO. The update is weighted by the ratio of new to old action probability; clipping that ratio to [1−ε, 1+ε] removes the incentive to move the policy far in one update.
PPOの中核です。更新は新旧のaction確率の比で重み付けされ、その比を[1−ε, 1+ε]にclipすることで、1回の更新でpolicyを大きく動かす動機をなくします。
multi-epoch minibatching複数epochのミニバッチ更新
Reuse each batch of experience for several optimisation epochs instead of throwing it away — which is only legal because of the clip.
集めた経験のバッチを捨てずに複数epochの最適化に再利用します。これが許されるのはclipがあるからです。

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/463 028[62 020, 63 428]
lr 1e−3, 8 epochs, 8 minibatches4/466 520[65 076, 69 772]
lr 1.5e−3, 10 epochs4/473 412[68 910, 78 664]
lr 1e−3, 10 epochs4/476 104[73 346, 85 050]
lr 2.5e−3, 4 epochs3/4112 104[83 832, 141 888]
lr 3e−4, 10 epochs2/4148 808—
lr 1e−3, 4 epochs1/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コストと、確認された予測

one 150 k-step run150 kステップの学習1回
11.4 s sequential — 13 187 environment steps per second
逐次実行で11.4 s — 毎秒13 187環境ステップ
64 runs64回の学習
344 s at four-way parallel, nice 10
4並列・nice 10で344 s
threadsスレッド数
torch.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 %です。ベクトル化した書き直しは不要で、実装もしていません。

07

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 PPOLQR
solved (≥475 over 100 consecutive episodes)達成(連続100 episodeで≥475)16/16yes達成
steps-to-threshold, median閾値到達ステップ数(中央値)62 1440
steps-to-threshold, IQR閾値到達ステップ数(IQR)[60 442, 63 448]—
greedy evaluation, 100 episodes, mediangreedy評価、100 episode、中央値500.0500.0
greedy evaluation, worst seedgreedy評価、最悪seed500.0—
The honest headline率直な結論

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

AblationSolved達成Steps medianステップ数中央値vs baseline対baselinePermutation p並べ替え検定 p
baseline16/1662 1441.00×—
no advantage normalisationadvantage正規化なし16/1664 8841.04×0.054
no GAE (λ = 1)GAEなし(λ = 1)16/1668 9481.11×0.0006
no ratio clippingratio clipなし8/16145 3682.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では次第に誤ったものになります。

"no GAE" = λ 1.0「GAEなし」= λ 1.0
GAE(γ, 1) is the plain Monte-Carlo advantage. Implementing the ablation as a single number change means the comparison is about GAE rather than about two different implementations.
GAE(γ, 1)は単純なMonte-Carlo advantageそのものです。ablationを数値1つの変更として実装しているので、 比較の対象は2つの異なる実装ではなくGAEそのものになります。
significance有意性
Steps-to-threshold is right-skewed and censored, so a t-test is the wrong tool. A two-sided permutation test on the difference of medians is used instead — exhaustive when the number of splits allows, 100 000 Monte-Carlo resamples otherwise. It assumes only exchangeability under the null, which is exactly what "this ablation changed nothing" means. Written without scipy.
閾値到達ステップ数は右に歪んでおり、censoringもあるため、t検定は不適切です。代わりに中央値の差に対する両側の並べ替え検定を使います。 分割数が許せば全数、そうでなければ100 000回のMonte-Carlo再標本化です。仮定するのは帰無仮説の下での交換可能性だけで、 これはまさに「このablationは何も変えなかった」という意味です。scipyは使っていません。
Why the seed count is 16 and not 8seed数が8ではなく16である理由

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値を併記しています。

08

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個すべてを比べます。

LQRPPO, median over 16 seedsPPO(16 seedの中央値)PPO best seedPPO最良seed
nominal return, 100 episodes公称条件でのreturn(100 episode)500.0500.0500.0
critical θ̇₀臨界θ̇₀2.169 rad/s1.082 rad/s2.004 rad/s
critical ẋ₀臨界ẋ₀2.435 m/s0.824 m/s1.935 m/s
Failures out of 200 episodes, initial velocities widened200 episode中の失敗数(初期速度を拡大)LQRPPO medianPPO中央値PPO worst seedPPO最悪seed
U(−0.5, 0.5)0122
U(−1.0, 1.0)04883
U(−1.5, 1.5)688115
U(−2.0, 2.0)28116138
U(−2.5, 2.5)61140158
The result worth keeping from the whole projectプロジェクト全体で最も残すべき結果

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 |θ|最大|θ|
LQRsurvives 500 steps500ステップ維持46 % of the limit限界の46 %
PPO seed 2falls at step 18 (0.36 s)18ステップ目で転倒(0.36 s)113 %
PPO seed 0survives 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つだからです。

09

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つのファイルでは意味が異なります。

Action clipping is biased, and the size of the bias is measuredactionのclipにはバイアスがあり、その大きさを計測

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ステップ数中央値IQRGreedy evalgreedy評価
discrete, ppo.py (step 4)離散、ppo.py(step 4)16/1662 144[60 442, 63 448]500.0
continuous, ppo_continuous.py連続、ppo_continuous.py16/1663 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が既に加えていた力に合わせた場合には、 平均と標準偏差の両方を学習する難しさが、より細かな制御能力でほぼ相殺される、という証拠にすぎません。

AblationSolved達成Steps medianステップ数中央値vs baseline対baselinepFinal σ最終σSaturation飽和率
baseline16/1663 7481.00×—0.1360.5 %
no advantage normalisationadvantage正規化なし16/1664 6641.01×0.730.1440.1 %
no GAE (λ = 1)GAEなし(λ = 1)16/1671 7601.13×0.00320.2180.8 %
no ratio clippingratio clipなし0/16>150 0002.35×0.00080.08828.2 %
The ordering survives; the magnitude of the top effect does not順位は維持され、最大の効果の大きさは維持されない

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を外すことは学習の遅れではなく、失敗モードになる、ということです。

10

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_clip failing 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分前に終わっており、起動時にガードも通過しています。
11

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はオフスクリーンでフレームを書き出すため、ウィンドウは不要です。

12

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本もありません。