NTTドコモR&Dの技術ブログです。

【Physical AI】麻雀牌を切る動作をロボットアームに学習させてみた【模倣学習】

NTTドコモ クロステック開発部の畑元です。今回はロボットに関する記事です🤖

1. はじめに

近年、ロボット分野がかなり盛り上がっているようです。 工場の産業用ロボットだけでなく、家庭用・サービスロボットやヒューマノイド、そしてそれらを動かすPhysical AIと呼ばれる領域まで、一気に実用が加速しています。 従来はロボットに何かをさせようとすると、軌道計画・制御・認識をすべて手書きで実装する必要がありましたが、近年ではロボットを直感的に学習させる取り組みが身近になってきています。模倣学習(Imitation Learning)と呼ばれるアルゴリズムは、人間のデモンストレーションを教師データとしてロボットに行動ポリシーを学習させる手法であり、簡易性、学習速度、安全性の面で利点があります。

本記事では、ロボットアームを使って「麻雀牌を切る」動作を模倣学習で学習させてみたのでご紹介します🀄️ 人間にとっては簡単な手牌からの切り出し動作もロボットにとっては、牌の位置を把握→適切な姿勢でアプローチ→牌を落とさない程度の力で掴む→前に持っていく、といったように、意外と多くの要素が詰まった難しいタスクです。

大まかに以下のような流れで一連のパイプラインを追っていきます。

  1. ロボットアームとカメラを用意し、麻雀卓の上に作業環境を構築する
  2. 人間がテレオペで牌を切るデモ動作を複数回記録する
  3. 収集したデモデータを使ってポリシーモデルを学習する
  4. 学習済みポリシーをロボットアーム上で再生し、ロボットに牌を切らせる

2. 使用するハードウェア/ソフトウェア

SO-101

SO-101はHugging Faceによって開発された6軸ロボットアームキットです。リーダーアームフォロワーアームの2本からなり、リーダーアームを人間が操作、フォロワーアームを追従させて模倣学習を行うことができます。

本記事ではSO-101 Pro版を使っており、個人でも手に入れやすい価格帯(約4万円)で導入できるため、まずはロボットで模倣学習を試したいというケースにも適した機体です。

LeRobot

LeRobotはロボットの模倣学習・強化学習を行うためのオープンソースフレームワークです。学習デモの記録→ポリシーの学習→実機で動作という一連の流れをカバーしており、共通のインターフェースで扱うことができます。

https://github.com/huggingface/lerobot

SO-101はLeRobotに公式対応しているため、本記事で扱うように最小限のコードで模倣学習のパイプラインを回せるのが特徴です。

カメラ

視覚情報をデモや推論に活かすため、InnoMaker CAM-MIPIOV9281というUSBカメラを2台取り付けています。1台はフォロワーアームの手元カメラ(wrist)、もう1台はタスク範囲全体を見る鳥瞰カメラ(birdseye)として使用しています。この2視点を組み合わせることで、麻雀牌の位置や手先の細かい動きを認識できるようにしています。

GPU

実機のデータ取得や推論にはNVIDIA Jetson Orin Nano 8GBを使っています。Jetson Orin NanoはエッジAI開発向けの小型組み込みコンピューターであり、低消費電力ながら高性能な推論を実現できます。

使用した環境は以下の通りです。

  • JetPack: 6.2.1 (L4T 36.4.7)
  • OS: Ubuntu 22.04 LTS (Jetson Linux)
  • CUDA: 12.6
  • Python: 3.10
  • PyTorch: 2.5.0

3. 環境のセットアップ

LeRobotインストール

公式のドキュメントに従いLerobotをインストールしていきます。

(1) python=3.10で仮想環境を作成

conda create -y -n lerobot python=3.10
conda activate lerobot

(2) ffmpegのインストール

conda install ffmpeg -c conda-forge

(3) LeRobotのリポジトリをクローンし、ディレクトリに移動

git clone https://github.com/huggingface/lerobot.git
cd lerobot

(4) LeRobotライブラリのインストール

pip install -e .

SO-101の組み立て

今回は組み立て済みのものを使っているため、この工程は実施しませんでした。組み立てについては以下の記事で詳しく解説されています。

https://note.com/npaka/n/n8424d322ebd4

ポートの特定

SO-101をJetsonに接続すると、Ubuntu 上では"/dev/ttyACM0"や"/dev/ttyACM1"といったシリアルデバイスとして認識されます。そこで接続されているリーダーアームおよびフォロワーアームがどのポートに割り当てられているかを調べる必要があります。

まずはリーダーアームのポートを特定します。lerobot_find_port.pyを実行することで調べられます。

python ./src/lerobot/scripts/lerobot_find_port.py

"Remove the USB cable from your MotorsBus and press Enter when done."というメッセージが出るので、指示通りリーダーアームのUSBケーブルを抜いてEnterを押すと、"The port of this MotorsBus is '/dev/ttyACM0'"のように表示されるので、これをメモしておきます。その後、USBケーブルを挿し直してフォロワーアームについても同様にポートを調べます。

キャリブレーション

各関節のゼロ点と最小/最大角度を記録し、エンコーダ値と実際の角度の対応を揃えるためキャリブレーションを行います。

キャリブレーションはlerobot_calibrate.pyを実行します。まずはリーダーアームのキャリブレーションを行うため、引数teleop.portに先ほど調べたリーダーアームのポートを指定します。

python ./src/lerobot/scripts/lerobot_calibrate.py \
    --teleop.type=so101_leader \
    --teleop.port=/dev/ttyACM0 \
    --teleop.id=my_first_leader_arm

"Move my_first_leader_arm SO101Leader to the middle of its range of motion and press ENTER...."というメッセージが表示されます。指示通りリーダーアームを以下のようなミドルポジションにとってEnterを押します。

その後、各関節を可動範囲の最大まで動かして再度Enterを押します。これでリーダーアームのキャリブレーションが完了します。

同様にフォロワーアームについてもキャリブレーションを行います。

python ./src/lerobot/scripts/lerobot_calibrate.py \
    --robot.type=so101_follower \
    --robot.port=/dev/ttyACM1 \
    --robot.id=my_first_follower_arm

カメラの検出

ロボット本体の準備ができたら、次にカメラのデバイス番号(インデックス)を確認しておきます。lerobot_find_cameras.pyを実行することで確認できます。

python ./src/lerobot/scripts/lerobot_find_cameras.py opencv

今回は2台のカメラを利用しているので以下のような出力になります。

--- Detected Cameras ---
Camera #0:
  Name: OpenCV Camera @ /dev/video0
  Type: OpenCV
  Id: /dev/video0
  Backend api: V4L2
  Default stream profile:
    Format: 0.0
    Fourcc: YUYV
    Width: 640
    Height: 480
    Fps: 30.0
--------------------
Camera #1:
  Name: OpenCV Camera @ /dev/video2
  Type: OpenCV
  Id: /dev/video2
  Backend api: V4L2
  Default stream profile:
    Format: 0.0
    Fourcc: YUYV
    Width: 640
    Height: 480
    Fps: 30.0
--------------------

lerobot_find_cameras.pyを実行することで、カメラ画像が'outputs/captured_images'に保存されるため、インデックスとカメラの対応を確認します。今回の例では鳥瞰カメラが0、手元カメラが2となっています。

テレオペレーションで動作確認

実際にアームを動かして動作確認を行います。lerobot_teleoperate.pyを実行するとリーダーアームによるフォロワーアームの遠隔操作を行うことができます。robot.type, robot.portにフォロワーアームを指定、teleop.type, teleop.portの引数にリーダーアームを指定します。また、先ほど調べたカメラについてもrobot.camerasに設定しておきます。

python ./src/lerobot/scripts/lerobot_teleoperate.py \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM1 \
  --robot.id=my_first_follower_arm \
  --teleop.type=so101_leader \
  --teleop.port=/dev/ttyACM0 \
  --teleop.id=my_first_leader_arm \
  --robot.cameras='{
    "observation.images.birdseye": {
        "type": "opencv", 
        "index_or_path": 0, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }, 
    "observation.images.wrist": {
        "type": "opencv", 
        "index_or_path": 2, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }
  }' \
  --display_data=true

適当にリーダーアームを動かしてフォロワーアームが追従するか確認します。

youtu.be

ここでdisplay_dataをtrueにしておくことで、カメラ映像や関節状態をリアルタイムに確認することができます。

youtu.be

4. 学習用データ準備

実際に動作が確認できたら学習用のデモを記録していきます。LeRobotでは記録したデータセットをHugging Face Hubのリポジトリにアップロードして管理できるようになっており、クラウド上の動作データセットとしてバージョン管理、再利用、共有などが簡単にできるようになっています。

https://huggingface.co/

そのため、デモの記録を始める前にHugging Faceにログインしておきます。

huggingface-cli login

アクセストークンが要求されるのでHugging Faceで発行したものを入力します。ログイン完了後、Hugging Faceのユーザー名をHF_USER変数に保存しておきます。

HF_USER=$(hf auth whoami | head -n 1)

デモの記録はlerobot_record.pyを実行します。dataset.repo_idには任意のリポジトリ名を指定可能です。今回は"mahjong"に設定します。またdataset.episode_time_s, dataset.reset_time_s, dataset.num_episodesにそれぞれ1エピソードの時間、環境リセットの時間、記録するエピソード数を指定できます。

python ./src/lerobot/scripts/lerobot_record.py \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM1 \
  --robot.id=my_first_follower_arm \
  --teleop.type=so101_leader \
  --teleop.port=/dev/ttyACM0 \
  --teleop.id=my_first_leader_arm \
  --robot.cameras='{
    "observation.images.birdseye": {
        "type": "opencv", 
        "index_or_path": 0, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }, 
    "observation.images.wrist": {
        "type": "opencv", 
        "index_or_path": 2, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }
  }' \
  --dataset.repo_id="${HF_USER}/mahjong" \
  --dataset.single_task="discard" \
  --dataset.episode_time_s=30 \
  --dataset.reset_time_s=10 \
  --dataset.num_episodes=10

既存のリポジトリに追加でデモを記録する場合は、resumeをtrueにしておきます。

--resume=true

今回は以下のように右端の一索を切るデモを100エピソード記録しました。

youtu.be

5. 学習

デモデータが集まったら、ポリシー学習に入っていきます。Lerobot では、記録済みのデモデータを指定するだけで学習が行えるため、モデルの設計をあまり気にせず、素早く試すことができます。

学習処理には高性能なGoogle ColabのA100 GPUを使いました。Jetson Orin Nanoでも学習できないことはないですが、かなり時間がかかるので、学習はクラウド、推論はエッジという構成にしています。

また、学習の前にWandBにログインしておきます。

https://wandb.ai/site/ja/

WandBは、機械学習の学習ログを可視化・管理できるサービスです。Lerobotの学習スクリプトはWandBへのログ出力に対応しているため、lossや各指標の推移をリアルタイムで確認できます。

# condaインストール
!pip install -q condacolab
import condacolab
condacolab.install()

# LeRobotのインストール
!git clone https://github.com/huggingface/lerobot.git
%cd lerobot
!conda install ffmpeg -c conda-forge
!pip install -e .

# HuggingFaceログイン
!pip install huggingface_hub[cli]
from huggingface_hub import login
login()
HF_USER=$(huggingface-cli whoami --raw)

# wandbログイン
!wandb login

# 学習
!python src/lerobot/scripts/lerobot_train.py \
    --dataset.repo_id="${HF_USER}/mahjong" \
    --dataset.revision=main \
    --policy.type=act \
    --output_dir=outputs/train \
    --job_name=act_mahjong \
    --policy.device=cuda \
    --policy.repo_id="${HF_USER}/act_mahjong-policy" \
    --wandb.enable=true \
    --steps=100000

dataset.repo_idには学習データ取得時のリポジトリを指定します。また、stepsで学習ステップ数を調整可能で、今回は100,000ステップとしました。

100,000ステップの学習は数時間かかります。完了したらtrainデータの学習曲線をWandBで確認します。

train/l1_lossはミニバッチごとに振動しながら下がっていますが、トレンドとしては収束に向かっており、全体としてlossが綺麗に下がっていることがわかります。今回の結果を見る限り、モデルはデモの動きを徐々に再現できるようになっていると判断できます。

6. 推論

学習が完了したら、いよいよ実機での推論を実行します。colabで学習したポリシーはHugging Face Hubにアップロードされているので、Jetson Orin NanoからモデルにアクセスしてSO-101に動作を実行させることができます。

python src/lerobot/scripts/lerobot_record.py \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM1 \
  --robot.id=my_first_follower_arm \
  --robot.cameras='{
    "observation.images.birdseye": {
        "type": "opencv", 
        "index_or_path": 0, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }, 
    "observation.images.wrist": {
        "type": "opencv", 
        "index_or_path": 2, 
        "width": 1280, 
        "height": 800, 
        "fps": 10
    }
  }' \
  --dataset.repo_id="${HF_USER}/eval_mahjong" \
  --dataset.single_task="discard" \
  --dataset.episode_time_s=30 \
  --dataset.reset_time_s=10 \
  --dataset.num_episodes=1 \
  --dataset.video=false \
  --policy.device=cuda \
  --policy.path="${HF_USER}/act_mahjong-policy"

実際にSO-101を動かしてみた様子です。

youtu.be

若干たどたどしい感じはしますが、うまく牌を切る動作を実現することができました。

7. おわりに

本記事では、SO-101とLeRobotを使って麻雀牌を切る動作を学習する流れを紹介しました。模倣学習を使うことで複雑な動作も簡単に実現させることができました。

ただ、今回扱ったのは手牌の右端の牌を切る動作のみであり、データ数やバリエーションも十分ではありません。牌の並びが変わったり、位置が変わったりすると失敗することもあり、実用レベルにはまだまだ改良の余地が大きいです。モデルを強化しつつ、最善手を判断するAIと組み合わせると完璧な代打ちマシンも実現できそうですね。

SO-101ロボットアームや模倣学習を触ってみたいという方の参考になれば幸いです。