跳到主要內容

006互動

把一隻機器狗搬進瀏覽器:它的走路策略只是四次矩陣乘法

用 MuJoCo 的 WebAssembly 版和 Deep Robotics 公開的走路策略,在瀏覽器裡讓 Lite3 走起來,然後動手把它弄倒:蒙住它的感覺、推它、調壞它的馬達。

發布
閱讀時間
7 分鐘

前幾篇文章的模型都是在你的瀏覽器裡訓練出來的。這一篇不是。 下面這隻機器狗的大腦,是 Deep Robotics 用 GPU 在模擬器裡訓練好、再公開放在 GitHub 上的。真的 Lite3 機器狗身上跑的就是這種策略。

我做的事情只有兩件:把物理模擬器搬進瀏覽器,然後把那個大腦接上去。訓練它需要幾千隻狗同時在 GPU 裡摔上幾個小時,那不是一個網頁做得到的。但是「訓練好的東西到底是什麼、它靠什麼站著」,是可以在這裡動手拆開來看的。

模擬器加上機器狗的模型大約 4.5 MB,所以它不會自己載入,要你按下按鈕。

fig 01/lite3 / remote

物理引擎、機器狗的模型和它的走路策略。按了才會下載,全篇的儀器共用這一份。

拖曳,或先點一下再用方向鍵/WASD。放開就停。

前進速度
0.50 → –m/s
轉彎
0.00 → –rad/s
橫移
0.00 → –m/s

你要的 實際的

搖桿往上推是前進,左右是轉彎。虛線是你要的速度,實線是它實際的速度。

按下去之後,你會看到它以每秒半公尺左右的速度往前走。把搖桿推到底,它會以每秒 2 公尺跑起來;往下拉會倒退,勾選橫移還能像螃蟹一樣走。後面的儀器可以推它:150 牛頓,它踉蹌一下站穩;300 牛頓,它會四腳朝天。

沒有任何一行程式碼在處理「被推了要怎麼辦」。

大腦只有四次矩陣乘法

公開的策略檔是一個 758 KB 的 ONNX 檔。打開來看,裡面沒有任何特別的東西:

這隻狗的大腦
輸入45 個數字
隱藏層512 → 256 → 128,啟動函數是 ELU
輸出12 個數字,每個關節一個
參數總數189,324
記憶沒有。每一次都是從頭算

它比上一篇的 Transformer 大十幾倍,但結構簡單得多:四層全連接層,前三層後面各接一個 ELU。所以這裡不需要任何推論函式庫,CNN 那篇寫的 dense 再加上一個新的 elu,就是全部:

act(obs: Float32Array): Float32Array {
  let x = tensor(obs, [1, 45]);
  this.layers.forEach(({ w, b }, i) => {
    x = dense(x, w, b);
    if (i + 1 < this.layers.length) x = elu(x);
  });
  return x.data;
}

ELU 和 ReLU 只差在負的那一半:ReLU 直接歸零,ELU 是一條平滑地趨近 −1 的曲線,elu(x)=ex1\mathrm{elu}(x) = e^x - 1

我拿原本的 ONNX 檔算出來的動作當標準答案,和這段程式的輸出比對,差距在小數第七位。

這個網路每秒被呼叫 83 次,每次不到 0.2 毫秒。真正花時間的是物理。

它感覺得到什麼

那 45 個輸入是這隻狗對世界的全部認識。沒有相機,沒有地圖,它甚至不知道自己在哪裡、走了多快。

幾個數字是什麼真機上從哪裡來
3身體轉動的角速度陀螺儀
3重力的方向(從身體的角度看,哪邊是下面)IMU
3你下的指令:往前、往側邊、轉彎各要多快搖桿
12每個關節現在的角度馬達編碼器
12每個關節轉動的速度馬達編碼器
12它自己上一次輸出的動作記下來的

既然只有這六種感覺,最直接的問題就是:少了哪一種它會倒?下面每一組長條就是那一種感覺現在的讀數,按「蒙住」會把它歸零。

fig 02/lite3 / senses

物理引擎、機器狗的模型和它的走路策略。按了才會下載,全篇的儀器共用這一份。

站了
實際的
m/s
倒地次數

45 個輸入

陀螺儀
重力方向
指令
關節角度
關節速度
上一次的動作

12 個輸出

上面六組是網路看到的 45 個數字,最下面是它回答的 12 個。指令固定在每秒 0.5 公尺。

我量到的結果和我原本猜的很不一樣:

蒙住結果
陀螺儀幾乎沒差,照走
重力方向撐 0.8 秒後倒下
關節角度0.28 秒就倒
關節速度不會倒,但是暴衝:要它走 0.8,它跑到 2.5 公尺每秒
上一次的動作站得好好的,但一步都不走

最後兩個最有意思。關節速度是它用來煞車的:感覺不到腿在動,它就以為自己還沒出力,於是越踩越用力。而「上一次的動作」是這個沒有記憶的網路唯一的節拍器。走路是一個週期性的動作,網路要知道現在輪到哪隻腳,靠的就是看自己上一拍做了什麼。把它拿掉,每一拍看起來都像第一拍,它就永遠停在起步的姿勢。

它輸出的不是力

12 個輸出也不是「每個馬達出多少力」。它們是目標角度,而且是相對於站姿的偏移:

目標角度=站姿+輸出×縮放\text{目標角度} = \text{站姿} + \text{輸出} \times \text{縮放}

縮放是髖關節側擺 0.125、其他關節 0.25 弧度,所以網路輸出 1,關節大約移動 14 度。真正算出力矩的是一個每毫秒跑一次的 PD 控制器:

τ=Kp(目標角度現在角度)Kd角速度\tau = K_p(\text{目標角度} - \text{現在角度}) - K_d \cdot \text{角速度}

KpK_p 是彈簧有多硬,KdK_d 是阻尼有多黏。力矩最多 30 牛頓公尺,這是馬達的極限。

這樣分工的原因是時間尺度。網路一秒想 83 次,馬達一秒要被指揮 1000 次。中間那 11 毫秒,是彈簧在替它撐著。

代價是:網路是在 Kp=30K_p = 30Kd=1K_d = 1 的身體裡長大的,它學到的每一個動作都默默假設了這副彈簧。換掉彈簧,它不會知道:

fig 03/lite3 / pd gains

物理引擎、機器狗的模型和它的走路策略。按了才會下載,全篇的儀器共用這一份。

訓練時是 Kp 30、Kd 1
實際速度(指令 0.5)
m/s
機身高度
m

左前膝的角度(最近 2 秒)

網路要的 實際的

左前膝的力矩(上限 ±30 N·m)

虛線是網路要的膝蓋角度,實線是膝蓋實際的角度,下面是馬達為了追上去出的力矩。指令固定在每秒 0.5 公尺。

KpK_p 拉到 10,實線就追不上虛線了,它會慢慢趴下去。拉到 100,實線幾乎貼著虛線,它也走得比你要求的快。我量到的速度:

KpK_pKdK_d指令 0.5 時的實際速度
1010.01,腿太軟,趴在地上
2010.24
3010.47
6010.66
10010.80
300.20.62
3030.31

彈簧變硬,同樣的目標角度會被更用力地執行,步伐變大,它就走得比你要求的快。它沒有壞掉,只是不準了。真的機器人上,每一顆馬達的實際增益都和規格書差一點,這就是其中一種「模擬裡好好的,上真機就怪怪的」。

推它

開頭說過,沒有任何一行程式碼在處理被推的情況。所以直接推推看,每一下都是從側面推十分之一秒。

fig 04/lite3 / push

物理引擎、機器狗的模型和它的走路策略。按了才會下載,全篇的儀器共用這一份。

機身傾斜角度,0 到 45°,最近 6 秒

紀錄

還沒推過。同樣的力道多推幾次,結果不一定一樣:要看你推在它步伐的哪一個瞬間。

線是機身傾斜的角度。平常走路時不到 3 度;超過 30 度就算倒了。

我把力道從 100 牛頓掃到 400,每個力道在步伐的 8 個不同時間點、從兩邊各推一次:

力道往裡推,8 次裡倒幾次往外推
175 N 以下00
200 N10
225 N40
250 N74
275 N 以上88

中間那一段沒有一個乾淨的門檻。同樣 225 牛頓、同一個方向,我只是把推的時間點挪 60 毫秒,結果就從撐住變成倒地。兩邊也不對稱。在儀器裡用同樣的力道多推幾次,你會推出不一樣的結果。175 牛頓那一次,機身最多只歪了 6 度就回正了。

真實世界沒有這麼乾淨

模擬器裡,感測器沒有雜訊、訊號沒有延遲、地板摩擦係數剛好是 1。真機上三件事都不成立,所以我把它們一個一個弄髒。

fig 05/lite3 / real world

物理引擎、機器狗的模型和它的走路策略。按了才會下載,全篇的儀器共用這一份。

實際速度(指令 0.5)
m/s
著地的腳在滑
m/s

前進速度:你要的和實際的,最近 8 秒

三個滑桿可以同時拉。摩擦的滑桿是對數的:最左邊是橡膠地板,最右邊比冰滑十倍。

延遲。 讓網路看到的永遠是幾拍之前的世界:

延遲實際速度(指令 0.5)
0 ms0.47
24 ms0.42
48 ms0.34
72 ms0.26

到 72 毫秒都沒有倒,但越走越慢。

雜訊。 在感測器讀數上加隨機誤差:±0.2 以下看不出差別;±0.3 還站得住,但速度掉到 0.2 到 0.35;±0.4 是邊界,我用 10 組不同的亂數各走 10 秒,4 次倒了,剩下 6 次只剩每秒 0.1 到 0.25 公尺;±0.5 十次全倒;±0.8 不到半秒就倒。

摩擦。 這是我最意外的一個。把地板從橡膠(μ = 1)換成比冰還滑(μ = 0.05),速度只從 0.47 掉到 0.44。腳確實在滑,著地的腳平均滑動速度從每秒 0.12 公尺升到 0.27,但它照走。要到 μ = 0.01 才明顯走不動。

這三個結果放在一起,其實在說同一件事。這種策略訓練的時候,模擬器會故意亂調摩擦、亂推機器人、在感測器上加雜訊,這叫 domain randomization,目的就是讓它不要太相信模擬器。你在這裡弄不倒它的那些方式,多半是有人已經在訓練時替你試過了;你弄得倒它的方式(蒙住關節角度、把彈簧換軟),則是訓練時沒人想過要防的。

這裡沒有的東西

這篇文章只有「執行」,沒有「學習」。那個 758 KB 的檔案是怎麼來的,這裡一個字都還沒說:

  • 獎勵函數。 沒有人教它怎麼走。訓練時只有一個分數:速度跟上指令加分,摔倒、抖動、耗電、腳拖地扣分。走路的樣子是這個分數逼出來的。
  • 幾千隻狗。 訓練用的是 PPO,在 GPU 上同時模擬幾千隻狗,每一隻都在不同的地形和不同的摩擦係數上摔。
  • 只有平地。 官方的部署程式裡有樓梯地形,這裡沒有放。

要在瀏覽器裡重現訓練,需要的算力差了好幾個數量級。那是另一篇文章,而且需要一張 GPU。

授權與來源