二足歩行ロボットを作る(4)
横の弱さを追いかけたら、バグ2つに行き着いた
制御の再設計から機体寸法まで手を広げた末に、原因は自分のコードでした
この記事に出てくる数値・結論は、すべてMuJoCo上のシミュレーションで測ったものです。実機による検証は経ていません。実際にロボットを組み立てて確かめた結果ではなく、計算機の中で起きたことである点をご承知おきください。
この連載は「専門家として正しさを保証する記事」ではなく、開発の実況記録です。うまくいかなかったことや、後から誤りだと分かったこともそのまま書いています。
前回のおさらいと、今回の道のり
前回、その場足踏みだけで横方向に耐えられる余裕の97%を使い切っていることが分かりました。押されて耐える力を測ったら14N。しかし足踏みするだけで、その97%ぶんの揺れを自分で作っていたわけです。
今回は、この横方向の弱さを根本から直そうとした回です。結論から書きます。
制御の再設計、機体寸法の見直し、アクチュエータの増強——ひととおり試した末に、原因は自分が書いたコードのバグ2つでした。しかも片方は、同じプロジェクトで一度文書化していた罠を、二度踏んだものです。
消費率は68%まで下がりました。ただし、そこに至る道のりのほとんどは遠回りです。その遠回りにも意味があったので、順に書きます。
ハードの設計まで疑ったのに、犯人は自分のコードだったやつだ…。
まず、立てた仮説が外れた
出発点の見立てはこうでした。「片足を上げるとき、その足を振る動きの反動が、体を横に傾けているのではないか」。前回、足を上げるだけで必ず傾きが出ることは分かっていたので、素直な推測です。
この仮説が正しいなら、検証は簡単です。足をゆっくり振れば、反動は小さくなるはず。足を運ぶ時間を0.10秒から1.0秒まで、10倍のレンジで振って測りました。
| 足を運ぶ時間 | 誘発される傾きのピーク |
|---|---|
| 0.10秒 | 0.06rad(例外的に小さい) |
| 0.20〜1.0秒 | 0.11〜0.14rad(ほぼ一定) |
10倍ゆっくり動かしても、傾きはほとんど変わりませんでした。仮説は外れです。
ただ、この測定は別のことを教えてくれました。傾きのピークが出るタイミングが、足を運ぶ時間によらず常に「足が浮いてから0.1〜0.2秒以内」だったのです。
足を振っている最中ずっと起きているのではなく、浮いた瞬間に決まっている。ここから原因が見えました。両足で立っている状態から片足になった瞬間、骨盤の位置が「支えている足の真上」からずれていれば、そのずれがそのまま倒れる方向の力になります。足首や股関節がそれを抑え込むのに0.1〜0.15秒かかる——という説明です。
仮説は外れましたが、外れ方が原因を教えてくれました。「時間を変えても変わらない」という結果は、「時間に依存しない何か」を探せという指示になります。10倍のレンジで振ったから、はっきり否定できました。
骨盤の位置そのものを制御する
原因が「浮いた瞬間の位置のずれ」なら、対策は素直です。骨盤の左右位置そのものを制御対象にする。傾きの角度を見て後から戻すのではなく、位置を直接合わせにいきます。
そのために、関節の角度を逆算する仕組み(逆運動学)を、任意の部位・任意の関節の組み合わせに使えるよう一般化しました。前回まで足を運ぶためだけに使っていたものを、汎用の道具に格上げした形です。
つまずき: 親は、子に動かされない
実装して動かしたところ、計算結果が毎回ほぼゼロになりました。何も動きません。
理由は、構造そのものにありました。
ロボットの体は、骨盤を根元として脚がぶら下がる木のような構造で表現されています。骨盤が親で、股関節・膝・足首はその子です。この構造で「脚の関節を動かしたら骨盤がどれだけ動くか」を計算すると、答えは構造上つねにゼロになります。子は親を動かせない、という前提で組み立てられているからです。
しかし現実には、脚を動かせば体は動きます。足が地面に固定されているからです。
逆から解く
そこで、発想を逆にしました。
- 骨盤を「行きたい位置」に固定してしまう(計算上だけ)
- その状態で、足が今ある場所に一致するような関節角を逆算する
足は動かない、骨盤が動く——という現実の因果を、計算の上で再現したわけです。これで動くようになりました。
シミュレータの内部表現が持つ前提(木構造)と、現実の物理(足が固定されている)が食い違う場面でした。ツールが返す「ゼロ」は故障ではなく、ツールが答えられる問いを間違えていた、という話です。
単発では効いた。連続では3歩で転んだ
できあがった仕組みを、まず「押されたら一歩踏み出す」制御に組み込みました。こちらは改善しました。
次に、本来の目的だったその場足踏みに組み込みました。
3〜4歩で転倒するようになりました。
設定を最も控えめにしても——「傾きをゼロに保つだけ」の弱い設定にしても——転びます。試しにこの新しい仕組みを完全に迂回して何もしないようにすると、それだけで30歩以上の安定した足踏みが戻りました。
単発では有用なのに、連続で使うと有害。直感に反する結果です。
このときの私の診断はこうでした。「毎回計算し直しながら連続実行すると、何かがうまく噛み合わない」。原因は特定できていませんでしたが、足の実測位置を計算の基準に使っているので、実行誤差が混入して蓄積しているのではないか、と疑いました。
時間の制約もあり、その場足踏みではこの機能を無効化するという判断で撤退しました。中途半端な決着です。
この「連続実行すると噛み合わない」という診断は、後で誤りだと判明します。連続かどうかは関係ありませんでした。ただしそれが分かるのは、このあと長い遠回りをしてからです。
遠回り: 機体の形を変えれば直るのか
制御で直せないなら、機体の形はどうか。ここから設計そのものを疑い始めます。試したのは3つです。
| 試したこと | 結果 |
|---|---|
| 股関節の横幅を広げて、足の間隔を稼ぐ | 逆効果。自分で生む揺れの大きさが、足の間隔の半分にほぼ比例していた |
| 重い部品を低い位置に置いて、重心を下げる | 効かない。効くのは重心の高さではなく脚の長さだった |
| 横方向のアクチュエータを強くする | 効かない。そもそも上限に達していなかった |
3つ目が象徴的でした。「力が足りないのでは」と疑ってトルクを2倍にしたのですが、何も変わりませんでした。測ってみると、そもそも上限まで使い切っていなかったのです。足りないものを増やしても意味がありません。
唯一改善が出たのは脚を短くする案でした。骨盤の高さを340mmから260mmにすると、横方向の消費率が96%から77%に下がります。
ただしこれには裏がありました。同時に前後方向の消費率が53%から85%に悪化していたのです。4方向の最悪値で見れば96%→85%。余裕が生まれたのではなく、ボトルネックが横から前後へ移っただけでした。しかも両隣の寸法では転倒する、崖のような点です。設計の基準にはできません。
この遠回りは無駄ではありませんでした。機体の形では直らないと分かったことで、「原因は制御アルゴリズムの側にある」という切り分けが裏づけられたからです。否定的な結果を3つ積んで、探す場所が確定しました。
12歩では足りなかった
機体の形を試していたとき、もう一度同じ罠を踏みました。
各案の評価は、その場足踏み12歩ぶんの揺れで測っていました。それまでずっとそうしてきたからです。その基準で見ると、脚を90mmまで短くした案が消費率30%という劇的な改善に見えました。97%が30%です。飛び上がりました。
念のため、歩数を伸ばして測り直しました。
| 変種 | 12歩 | 40歩 | 100歩 |
|---|---|---|---|
| 基準の機体 | 5.99° | 5.99°(完走) | 完走 |
| 脚を短くした案 | 2.06° | 3.83°(完走) | 64歩目で転倒 |
| 別の寸法案 | 3.94° | 35歩目で転倒 | 転倒 |
12歩時点の2.06°は、まだ発散し切っていない途中の状態をピークだと読んでいただけでした。40歩まで見れば3.83°に増えています。もう一方の案に至っては、12歩では最良候補に見えて、35歩目で転びます。
これは、第2回で書いた「転倒判定の待機時間が0.8秒では短すぎた」というのと、まったく同じ構造の罠です。あのときは待機時間の側で踏み、今回は歩数の側で踏みました。
「観測する期間が短いと、途中の状態を最終的な結果だと誤認する」——文章にすればひとことです。それでも、待機時間で一度痛い目を見た後に、歩数で同じことをやりました。教訓を「待機時間の話」として覚えていて、「観測期間一般の話」として一般化できていなかったわけです。
以後、評価の既定を200歩に変更し、12歩で出した数値はすべて破棄しました。あわせて待機時間の側も、2.5秒と6.0秒で測り直して1つも数値が動かないことを確認しています。
戻ってきて、真因はバグ2つだった
「原因は制御アルゴリズムの側にある」と切り分けたうえで、先ほど撤退した場所に戻ってきました。
まず、撤退時の見立て——「足の実測位置を計算の基準に使っているため、誤差が蓄積している」——を直接検証しました。蓄積が原因なら、基準を計画値に差し替えれば直るはずです。
| 足を運ぶ時間 | 基準=実測位置(従来) | 基準=計画位置 |
|---|---|---|
| 0.40秒 | 1歩で転倒 | 1歩で転倒(改善なし) |
| 0.18秒 | 20歩完走 | 4歩で転倒(悪化) |
直りません。それどころか悪化しました。さらに決定的だったのが次の2つです。
- 単発の1歩でも転ぶ(足を運ぶ時間が0.22秒以上のとき)。蓄積するための歩数が存在しません
- 毎歩この処理を走らせても、最初の0.10秒で打ち切れば20歩完走する。繰り返すこと自体は無害です
「連続実行が悪い」という診断は、ここで完全に否定されました。本当に効いていた変数は「1回の片足支持で、この処理が動き続ける時間」でした。0.11秒なら生き残り、0.15秒で死ぬ。そういう閾値です。
バグ1: 支えている脚が、脱力していた
1つ目のバグです。こちらが致命的でした。
関節角を逆算する処理は、今の実測角を出発点にして計算します。返ってくる答えは「実測角 + わずかな補正」です。そして旧実装は、その答えでバランス制御が出していた指令を上書きしていました。
ここに問題があります。位置制御の関節が出すトルクは、「指令した角度」と「今の角度」の差に比例します。差がゼロならトルクもゼロです。
つまり実測角を基準にした指令を出すと、その関節はほとんどトルクを出しません。骨盤が目標に達した瞬間、トルクはゼロになります。片足で立っている最中に、全体重を支えている唯一の脚が脱力するわけです。
同じ傾き角のときに支持脚が出していた復元トルクを測りました。
| 傾き角 | バランス制御のみ | 旧実装(上書き) | 修正後(加算) |
|---|---|---|---|
| -2.0° | -2.02 N·m | -0.35 N·m | -2.40 N·m |
| -3.0° | -2.24 N·m | -0.54 N·m | -2.85 N·m |
| -4.0° | -2.50 N·m | -0.70 N·m | -3.22 N·m |
復元トルクの68〜83%が消えていました。支えが実質的に効かない状態なので、傾きは0.1秒ほどの時定数で発散します。「0.11秒なら生き残り、0.15秒で死ぬ」という閾値とぴたり整合しました。
これは、前回の前進歩行の調査で「失敗機序その1」としてすでに文書化していた罠とまったく同じものでした。「位置制御の脚に現在角を指令すると脱力する。位置制御では、荷重を支えているのは誤差そのものだから」——自分でそう書いていたのに、別の場所で同じことをやっていました。
バグ2: 潰したはずの冗長性が、場所を変えて残っていた
2つ目です。これは、少し苦い話になります。
再設計のとき、私はこう書いていました。「位置だけを目標にすると、傾きが拘束されない冗長な系になってしまう。だから位置と傾きの2つを同時に目標にして、ちょうど解ける系にした」。
ところが実装が実際に選んでいた2つの目標は、支持足の左右位置(y)と上下位置(z)でした。
ここで問題になるのは、股関節と足首の横方向の関節は、足を上下に動かせないということです。感度を計算すると、こうなります。
| 目標 | 股関節ロール | 足首ロール |
|---|---|---|
| 左右位置(y) | +0.320 | +0.020 |
| 上下位置(z) | 0.000 | 0.000 |
上下位置の行が、きれいにゼロです。動かせないものを目標にしていたので、目標を2つ並べても実質1つしかありませんでした。
つまり傾きは、再び拘束されていなかったのです。
冗長性を潰すための再設計が、場所を変えて同じ冗長性を作り直していました。「直した」と報告した内容が、実装のうえでは成立していなかったことになります。しかも報告の文章は正しいのです——「2つの目標で拘束する」という方針は正しく、選んだ目標が間違っていました。方針が正しいと、実装を疑いにくくなります。
正しい組み合わせ(左右位置と傾き)にすると、感度の計算はきちんと2方向に効く形になりました。両方のバグを修正しています。
結果と、前回の診断の訂正
2つのバグを直した結果です。
| 指標 | 前回 | 今回 |
|---|---|---|
| その場足踏みの横方向 消費率 | 96% | 68% |
| 押されて耐えられる限界(横) | 14.0N | 14.0N(不変) |
第3回では97%と書きました。あれは12歩で測った値です。同じ機体・同じ設定を200歩で測り直すと96%でした。上の表は、前も後も200歩で揃えてあります。
この記事で「12歩では足りなかった」と書いたばかりですが、その教訓は、自分の結論の数字にも当たっていました。1ポイントの違いなので話の筋は変わりませんが、同じ条件で測り直した2つを並べる形に直しました。
分母は1Nも変わっていません。耐えられる力は同じままで、自分で生む揺れのほうが小さくなったということです。それでも、96%から68%は大きな差です。押されたときに使える余地が、ようやく3割ほど戻りました。
側方の限界は、制御の設定をいろいろ変えても14.0Nのまま動きません。理由は構造にあります。側方に押されたとき、この機体は踏み出しません。足首だけで受け止めています。「踏み出しが間に合うかどうか」という一番きわどい仕組みを通らないので、設定の影響を受けないわけです。
前方はそうではありません。踏み出しで受けるので、設定しだいで大きく動きます(条件によっては12N台まで落ちます)。1つの数字で「限界はここ」と書けるものではないので、前方の限界値はこの表に載せていません。
前回の診断は誤りでした
あわせて訂正します。前回「単発では有効でも、連続実行すると噛み合わない」と書きましたが、これは間違いでした。
連続かどうかは無関係で、単発の1歩でも、足を運ぶ時間が0.22秒を超えれば転倒します。効いていたのは「1回の片足支持でこの処理が動き続ける時間」であって、繰り返しではありませんでした。
「連続実行が原因」と考えていたから、誤差の蓄積を疑いました。そして蓄積を疑ったから、機体の形にまで手を広げる遠回りをしました。最初の診断のひとつのずれが、その後の探索方向を丸ごと決めていたことになります。
原因が分からないまま撤退するとき、「たぶんこれだろう」を書き残すのは危険だと分かりました。次に戻ってきたとき、それが出発点になってしまうからです。「連続実行で悪化する。原因は未特定」とだけ書いておけば、遠回りは短くなったかもしれません。分からないことは、分からないと書く。推測を混ぜると、推測が記録になります。
次回予告
今回の道のりを振り返ります。
- 立てた仮説が外れた(ただし外れ方が原因を教えてくれた)
- 制御を再設計したが、その場足踏みには組み込めず撤退
- 機体の形を3通り疑って、すべて否定的な結果(それが切り分けになった)
- 測定の罠を形を変えて二度踏んだ(12歩では足りなかった)
- 真因は自分のコードのバグ2つ。片方は文書化済みの罠の再発
横方向の消費率は96%から68%へ。ようやく「歩く」に進める土台ができました。
そして次回、この機体はついに前に進みます。ただし、そこで測った数字がまた新しい問題を突きつけてきます。立っているときに測った強さは、歩いている最中には無かったのです。
