はじめに
最近、「フィジカルAI」という言葉をよく見かけるようになりました。AIを使ってロボットなどの実際の機械を動かす技術ですが、具体的にどのような仕組みで実現するのか、興味がありました。
そこで、以前製作した二輪倒立振子を使い、機械学習による制御に挑戦してみました。今回行ったのは、次のような実験です。
- M5Stack Core2とMicroPythonで二輪倒立振子をPD制御する
- PD制御中の傾斜角度、角速度、モータ指令値を記録する
- PC上のPythonでニューラルネットワークを学習させる
- 学習したニューラルネットワークをCore2に組み込む
- PD制御をAI制御に置き換えて倒立させる
最終的には、ニューラルネットワークの出力だけで倒立に成功しました。 今回は、実験の手順とプログラムの概要を紹介します。
1. 使用した機器と開発環境
使用した主な機器は次のとおりです。
| 機器 | 用途 |
|---|---|
| M5Stack Core2(M5GO-BOTTOM2 V1.3 取付) | センサ計測・制御演算 |
| Roller485 Lite ×2 | 車輪駆動 |
| 車輪(直径約80 mm)×2 | 移動・倒立 |
| フレーム(LEGOブロック) | 本体の支持 |
| PC(Windows 11) | プログラム作成・機械学習 |
Core2には加速度センサとジャイロセンサが内蔵されています。今回は、これらのセンサを使って傾斜角度と角速度を求めました。
モータはI2C通信で制御し、アドレスは 0x64 と 0x65 に設定しました。
開発環境には、Core2側でUIFlow2のMicroPython、PC側でThonnyとPythonを使用しました。機械学習には scikit-learn を使用しています。
2. まずはPD制御で倒立させる
いきなりAIに倒立方法を学習させるのは難しいため、最初に一般的なPD制御で倒立できる状態にしました。
今回の制御式は次のとおりです。
$$
u=-K_p\theta-K_d\omega
$$
ここで、$u$はモータ指令値、$\theta$は傾斜角度(°)、$\omega$は角速度(°/s)、$K_p$、$K_d$は制御ゲインです。実験では、次の値を使用しました。
Kp = 40.0
Kd = 1.0
MAX_CURRENT = 400
制御部分は非常に簡単です。
u = -Kp * theta - Kd * gx
u = max(-MAX_CURRENT, min(MAX_CURRENT, u))
current = int(u)
motor1.set_motor_max_current(current)
motor2.set_motor_max_current(-current)
左右のモータは取り付け方向が逆なので、指令値の符号を反転させています。
なお、MAX_CURRENT は今回のプログラムで使用する指令値の上限であり、実際の電流を測定して確認した値ではありません。
傾斜角度の推定
加速度センサから求めた角度と、ジャイロセンサの角速度を積分した角度を、相補フィルタで組み合わせています。
accel_angle = math.degrees(math.atan2(ay, az))
alpha = TAU / (TAU + dt)
angle = (
alpha * (angle + gx * dt)
+ (1.0 - alpha) * accel_angle
)
theta = angle - angle_offset
ここで、TAU = 0.5 としました。
今回の機体では、前後方向の傾斜に対応する加速度成分として ay と az、角速度として gx を使用しています。機体の取り付け方向によって、使用する軸や符号は変わります。この方法により、MicroPythonによるPD制御で倒立させることができました。
3. PD制御のデータ取得
次に、PD制御の動作をAIに学習させるため、教師データを取得しました。記録するのは、次の3項目です。
- 傾斜角度
theta_deg - 角速度
omega_deg_s - PD制御によるモータ指令値
command
制御周期を5 msとして、1,000件、約5秒間のデータを取得しました。取得したCSVファイルは次の形式です。
time_ms,theta_deg,omega_deg_s,command
5,0.55126,0.25299,-22
10,0.55286,0.37506,-22
15,0.55489,0.31403,-22
20,0.55479,-0.11322,-22
25,0.54757,-1.27289,-20
なお、制御ループ内で毎回CSVファイルに書き込むと、書き込み処理によって制御周期が乱れる可能性があります。そこで、測定中はデータをRAMに保存し、モータを停止してからCSVファイルに書き出す方法としました。
4. ニューラルネットワークによる学習
本来、AIに倒立方法を一から学習させるには、多くの試行錯誤が必要になります。しかし、実機で試行錯誤を繰り返すと、転倒によって機器を破損するおそれがあります。
そこで今回は、すでに倒立できるPD制御の動作をAIに学習させる方法を採用しました。これなら、実機を安定して動作させながら教師データを取得でき、比較的安全に機械学習による制御を試すことができます。
取得したCSVファイルをPCに移し、Pythonで機械学習を行いました。今回の目的は、傾斜角度と角速度を入力すると、PD制御と同じようなモータ指令値を出力するAIを作ることです。ニューラルネットワークの構成は次のようにしました。
| 層 | ニューロン数 |
|---|---|
| 入力層 | 2 |
| 中間層 | 4 |
| 出力層 | 1 |
非常に小さなネットワークです。
学習プログラム
scikit-learn の MLPRegressor を使用しました。以下は学習処理を簡略化した例です。
import pandas as pd
import numpy as np
from sklearn.neural_network import MLPRegressor
from sklearn.metrics import mean_absolute_error
df = pd.read_csv("pd_training.csv")
X = df[["theta_deg", "omega_deg_s"]].to_numpy()
y = df["command"].to_numpy()
# 入出力をスケーリング
X = X / np.array([5.0, 200.0])
y_scaled = y / 400.0
# 最初の800件を学習用、残り200件を評価用
X_train, X_test = X[:800], X[800:]
y_train, y_test = y_scaled[:800], y_scaled[800:]
model = MLPRegressor(
hidden_layer_sizes=(4,),
activation="tanh",
solver="lbfgs",
alpha=0.00001,
max_iter=3000,
random_state=0
)
model.fit(X_train, y_train)
prediction = model.predict(X_test) * 400.0
mae = mean_absolute_error(y[800:], prediction)
print("MAE:", mae)
実際の実験では複数の初期乱数を試し、評価用データで誤差が小さかったモデルを採用しました。そのモデルの平均絶対誤差(MAE)は、モータ指令値に換算して約0.34となりました。これは、AIがPD制御の指令値を非常によく近似できたことを示しています。ただし、今回のデータに対する評価であり、あらゆる姿勢や外乱で同じ精度が得られるわけではありません。
5. 学習済みAIのMicroPythonへの組み込み
PCで学習したニューラルネットワークをCore2上で動かすため、学習済みの重みとバイアスを取り出しました。
scikit-learn のモデルは、そのままMicroPythonで実行するのではなく、計算部分だけをMicroPythonで実装します。今回のネットワークは入力2個、中間層4個、出力1個なので、計算量はそれほど多くありません。実際の推論処理は次のようになります。
def ai_predict(theta, omega):
x0 = theta / 5.0
x1 = omega / 200.0
v = AI_B2
for j in range(4):
h = math.tanh(
x0 * AI_W1[0][j]
+ x1 * AI_W1[1][j]
+ AI_B1[j]
)
v += h * AI_W2[j]
return v * 400.0
AI_W1、AI_B1、AI_W2、AI_B2 には、PCで学習した重みとバイアスを設定します。これにより、Core2単体でニューラルネットワークによる推論が可能になります。
なお、機械学習はPCで行っており、Core2では学習済みのニューラルネットワークを使った推論のみを実行しています。
6. PD制御とAI制御を比較する
最初からAIにモータ制御を任せるのではなく、まずはPD制御で倒立させながら、同時にAIの指令値も計算しました。この段階では、AIの出力はモータに反映させません。実験したところ、AIとPD制御の指令値は非常によく一致しました。
そこで、次に両者の指令値を混合する方法を試しました。
$$
u=(1-\lambda)u_{\mathrm{PD}}+\lambda u_{\mathrm{AI}}
$$
AIの割合を10%、50%と増やして実験したところ、どちらも床上で倒立できました。AI50%の実験では、途中で手で押して外乱を与えましたが、AIはPD制御に近い指令を出していました。
7. AI100%での倒立
最後に、モータ指令をAIの出力だけで決めるようにしました。
ai_u = ai_predict(theta, gx)
ai_u = max(-MAX_CURRENT, min(MAX_CURRENT, ai_u))
command = int(ai_u)
motor1.set_motor_max_current(command)
motor2.set_motor_max_current(-command)
これで、モータ指令値はニューラルネットワークの出力だけで決定されます。ただし、安全確認のため、PD制御の指令値も並行して計算し、AIの指令値との差が大きくなった場合には停止する監視機能を残しています。また、傾斜角度が20°を超えた場合や制御周期に異常が発生した場合にも停止するようにしました。
最初は2秒間の試験を行い、AI100%で倒立できることを確認しました。
そこで、次に60秒間の連続倒立に挑戦しました。最初は60秒間のデータをすべてRAMに記録しようとしましたが、MicroPythonのメモリ不足が発生しました。そこで、CSV記録を省略し、制御終了後に集計結果だけを表示するように変更しました。その結果、AI100%制御で60秒間の連続倒立に成功しました。実験中は、指で車体を細かく押して繰り返し外乱を与えました。
結果の一例です。
| 項目 | 結果 |
|---|---|
| 制御時間 | 60秒 |
| 制御周期 | 10 ms |
| 制御回数 | 6,000回 |
| 最大制御周期 | 10.012 ms |
| 最大傾斜角度 | 約5.22° |
| 最大モータ指令値 | 260 |
| AIとPDの平均指令差 | 約0.36 |
| AIとPDの最大指令差 | 約2.43 |
繰り返し外乱を与えた状態でも倒立を維持できました。なお、外乱の大きさを測定していないため、PD制御とAI制御の外乱抑制性能を定量的に比較した結果ではありません。
8. 今回の実験で分かったこと
今回の実験では、PD制御の動作データを学習させることで、ニューラルネットワークによる倒立制御を実現できました。特に興味深かったのは、中間層のニューロンがわずか4個という小さなネットワークでも、実際のロボットを制御できたことです。
ただし、今回のAIはPD制御の動作を模倣したものです。AIが試行錯誤によって倒立方法を発見したわけではありません。また、PD制御よりも優れた制御性能を獲得したわけでもありません。
今回の取り組みは、一般に「模倣学習(Behavior Cloning)」と呼ばれる考え方に近いものです。機械学習によって従来の制御器の入出力関係を再現し、実際の機械の制御に利用できることを確認した実験といえます。
9. おわりに
M5Stack Core2とMicroPythonを用いて、二輪倒立振子の機械学習による制御を試しました。今回の実験を通じて
- マイコンによる制御データの取得
- Pythonによるニューラルネットワークの学習
- 学習済みモデルのマイコンへの実装
- AIによるリアルタイム制御
という一連の流れを体験できました。
AIによる機械制御というと難しそうですが、今回のように、まず従来の制御方法をAIに学習させることで、比較的取り組みやすい実験にできると感じました。
今後は、制御器の模倣だけでなく、モータ指令と姿勢変化の関係をAIに学習させるなど、別の方法にも挑戦してみたいと思います。
なお、倒立振子は転倒や予期しない移動の危険があります。実験する場合は、車輪を浮かせた状態で動作方向と停止機能を確認し、外部からモータ電源を遮断できるようにするなど、安全に十分注意してください。
参考資料
プログラム・データ等


