はじめに
画像生成や自然言語処理を中心に近年大きな盛り上がりを見せる機械学習ですが,ロボットの分野においても機械学習が応用される機会は増えてきています。従来のロボットは主にルールベースで,具体的にプログラムされた指示に従って動作することが一般的でしたが,機械学習を導入することで部分的ではあるもののロボットが環境からデータを学習し,より柔軟にタスクをこなせるようになりました。例えば,教師あり学習を用いた音声認識や画像認識は,ロボットシステムにおけるセンサー情報の処理に広く利用されています。また,強化学習はロボット自体の行動を最適化する手法として活発に研究が行われています。
一方で,人間のような会話を実現するChatGPT等の先進的なモデルが登場して以降大きな注目を集めている大規模言語モデル(Large Language Models,LLM)ですが,LLMがロボット制御において活用されている例は未だに限定的です。しかしLLMが持つ高い文脈理解力や思考力のようなものは,ロボットに応用することでこれまでよりも柔軟な動作を実現可能にすると考えられます。そこで今回は簡単なデモロボットを作成し,LLMがロボット制御に対してどの程度有効か,またどのような利用方法が考えられるかについて探ります。
ロボットの概要
ここでは今回作成したデモロボットの全体像を簡単に説明します。ProtoPediaの記事やGitHubも適宜参照してください。
ロボットの見た目
本体部分にはRT CORPORATIONの二輪移動ロボットRaspberry Pi Mouse V3を使用しており,外側のカバーは3Dプリンターで作成しています。
システムの概要
このロボットのシステムはROS 2 Humbleによって作成されており,ロボットの各機能はROS2ノードとして実装してあります。各ノードは以下のように接続されています。
下に各ノードの機能を簡単に説明します。
- vosk_node
vosk_node
は音声認識を行うノードで,ServiceServerとして実装されています。リクエストを受け取ると音声の入力待機状態となり,音声入力があれば文字列に変換して返します。 - yolo_node
yolo_node
は物体認識を行うノードで,ServiceServerとして実装されています。リクエストを受け取るとrealsense2_camera
ノードから送られてくるカメラ画像に映る物体を認識してその情報を返します。 - ar_node
ar_node
はロボットを検出するノードで,realsense2_camera
ノードから送られてくるカメラ画像からARマーカーを検出してその情報を送ります。 - gpt1_node
gpt1_node
はServiceServerとして実装されています。リクエストとして文字起こしされた音声を受け取ると,ChatGPTにそれがロボットへの命令であるか否かを判断させて,そうであればより簡潔な命令に分割・修正して返します。 - gpt2_node
gpt2_node
はServiceServerとして実装されています。リクエストとしてgpt1_node
,yolo_node
,ar_node
からの情報を受け取ると,それをChatGPTに送り,ロボットの目標座標を生成して返します。 - pos2vel_node
pos2vel_node
はアクションサーバーとして実装されており,ar_node
から送られてくるロボットの位置と方向の情報とgpt2_node
から送られてくるロボットの目標座標から,RasPiMouseへ送る速度指令を出力します。 - bt_node
bt_node
はBehaviorTreeを内包しており,各ServiceServerを呼び出すServiceClientを含んでいます。BehaviorTreeによって各機能を呼び出すタイミングと,送信するデータを制御しています。 - realsense2_camera
realsense2_camera
はIntelRealSense公式が配布しているパッケージで,RealSenseカメラから取得した映像をROSトピックとして配信します。
- raspimouse
raspimouse
は株式会社アールティが配布しているパッケージで,ラズパイマウスの各機能をROS2から制御できるようにします。
このロボットではgpt1_node
とgpt2_node
の2つのノードでLLMを利用しており,2段階でChatGPTを用いることで自然言語による命令をロボットに理解可能な表現まで落とし込んでいます。
ロボットの詳細
・音声認識(vosk_node)
音声認識は音声をテキストに変換する技術でありSTT(Speach to Text)とも呼ばれています。STTをするには様々なライブラリやAPIがありますが,無料であり,かつ高い精度が出るという事で今回はPython版のVOSKを使用しています。
こちらの記事を参考にして音声認識を行うプログラムをROS2のServiceServerとして実装しました。
・物体認識(yolo_node)
物体認識を行うために定番の手法としてYOLO(You Only Look Once)というものがありますが,YOLOは制作者や制作時期によって複数のバージョンが存在しており,今回はその中のYOLO-NASというバージョンを使用しました。
また、計算コストを抑えるために物体認識はServiceServerにリクエストがあったときのみ行うようにしています。
・ロボットの検出(ar_node)
ロボットの位置と方向を取得する方法はいくつか考えられますが、今回は簡単のためにロボットにARマーカーを貼り付け、ARマーカーの位置と方向を取得することでロボットの情報を取得しています。ARマーカーの読み取りにはOpenCVのArUcoを用いています。
color_image
という変数に画像が入っているとして,ARマーカーの検出は最小限以下のようなコードによって行うことができます。
from cv2 import aruco
dictionary = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
parameters = aruco.DetectorParameters()
detector = aruco.ArucoDetector(dictionary, parameters)
corners, ids, _ = detector.detectMarkers(color_image)
・命令の生成(gpt1_node, gpt2_node)
命令の解釈と生成には2段階でChatGPTを利用しています。
一つ目のChatGPTは音声認識によって文字起こしされたテキストを入力として,与えられたテキストがロボットへの命令であるかを判断し,そうであれば命令をより簡単な命令に分割してJOSN形式で出力します。また,GPT-4には出力を必ずJSON形式にするモードが実装されているため,それを利用しています。
ChatGPTには以下のようなプロンプトを与えています。
You will operate the robot.
Determine if the input string is an instruction to the robot, and if not, just output {“instruction”:["NULL"]}.
If it is a command for the robot, break it down into an array of simple commands that direct only the objects contained in <object_list> and output them.
# Object list
<object list> = ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush']
# Output format (JSON format)
{“instruction”:["instruction 1", "instruction 2", ...]}
# Example
input:
今日はいい天気ですね。
output:
{“instruction”:["NULL"]}
input:
スプーンとフォークを取ってきてください。
output:
{“instruction”:["Go to the spoon's location.", "Grab a spoon.", "Go to the fork locaotion.", "Grab a fork.", "Return to the first place."]}
input:
冷蔵庫の近くのリンゴをドーナツとバナナの中間に移動させて。
output:
{"instruction":["Go to the location of the apple near the refrigerator.", "Grab the apple", "Go to the middle of the donut and banana."]}
一般にプロンプトは英語で書いたほうが性能が良くなると言われています。また,入力と出力の例を書くことも性能向上に有効です。それ以外には,YOLOは認識できる物体の種類が使用する重みによって決まっているので,そのリストをObject list
として与えています。また,前述のJSONモードを利用するにはプロンプト内で出力をJSON形式とすることを明示する必要があります。
2つ目のChatGPTは、1つ目のChatGPTから出力された命令と,ロボットを含めた物体の位置情報を元にして、ロボットの目標位置を出力します。こちらでもJSONモードを利用して出力をJSON形式にしています。
ChatGPTには以下のようなプロンプトを与えています。
You will be operating a simple mobile robot.
You will be given a simple command, your coordinates, and the coordinates of surrounding objects, and you will output an array of coordinates in JSON format that the robot should move to.
If coordinates cannot be output, just output '{"coordinates": [{"x": -1, "y": -1 }]}'.
# Input format
instruction : <instruction to you>
object position : {<object name> : <object coordination>, ...}
your position : <your coordination>
# Output format (JSON format)
{“coordinates”:[{“x”:<x_position>, “y”:<y_position>}, …]}
# Example
input:
instruction : Move the banana to the location of the apple.
object position : [{"apple" : (200, 300)}, {"baseball" : (100, 150)}, {"fork" : (400, 20)}, {"banana" : (300, 100)}, {"refrigerator" : ( 0, 100)}, {"human" : (0, 0)}]
your position : (10, 10)
output:
{"coordinates": [{"x": 300, "y": 100 }, {"x":200, "y":300}]}
input:
instruction : Throw the yellow ball.
object position : [{"apple" : (200, 300)}, {"baseball" : (100, 150)}, {"fork" : (400, 20)}, {"banana" : (300, 100)}, {"refrigerator" : ( 0, 100)}, {"human" : (0, 0)}]
your position : (10, 10)
output:
{"coordinates": [{"x": -1, "y": -1 }]}
こちらのプロンプトも基本的に英語で記述し,また入出力の例を記述しています。
・全体制御(bt_node)
bt_node
はBehaviorTreeを内包しており,ROSノードとして実装された個々の機能はこれによって実行順序を制御されます。ROS2でBehaviorTreeを使う方法については別の記事で解説しているので参照してください。
BehaviorTreeのノード(≠ROSノード)は下の図のように接続されており,Root
から順に深度優先探索的にノードが実行されます。
プログラムの流れとしては,まずRoot
から順にノードが実行され,最初にVOSK
が呼び出されます。VOSK
はbt_node
に含まれるServiceClientを用いてvosk_node
のServiceServerを呼び出し,音声認識の結果を取得します。
その後,VOSK
はGPT1
へそのテキストデータを送り,GPT1
はそのデータをもとにgpt1_node
を呼び出します。ChatGPTが入力された文章はロボットへの命令ではないと判断した場合はGPT1
がFailureを返し,再びVOSK
ノードが呼び出されます。一方で,ChatGPTが入力された文章はロボットへの命令であると判断した場合は,命令をより簡潔な命令に分解した命令列が返されます。
次にYOLO
ノードが実行されると,yolo_node
のServiceServerを呼び出し,認識された物体の名称とスクリーン座標を取得します。次に実行されるGPT2
ノードはここで得られた情報に加えて,GPT1が取得した命令の一つ,ar_node
が配信しているロボットの座標の三つのデータをまとめてgpt2_node
へ送信し,それらの情報をもとにChatGPTが出力したロボットの目標座標のリストを取得します。
次にSendPos
ノードが実行され,目標座標を1つづつpos2vel_node
へ送ります。これらのノードが実行中にFailureを返した場合はVOSK
から再びノードが実行されます。
BehaviorTreeは以上の動作をTreeの構造に従い繰り返します。
・ロボットの操作(pos2vel_node)
ロボット(Raspberry Pi Mouse)を操作するには株式会社アールティが配布してるサンプルプログラムをそのまま利用しています。このプログラムでは速度制御によってロボットを操作するため,pos2vel_node
ではbt node
から送られてきた目標位置と,ar_node
から送られてくるロボットの位置と角度をもとにして,速度制御を行います。
具体的にはpos2vel_node
にはActionSreverが実装されており,これが呼び出されるとロボットの現在位置と目標位置から生成したベクトルのノルムに応じた速度の比例制御と、ロボットの方向とベクトルの向きに応じた角速度のPID制御を行います。
改善点
現在のシステムではYOLOの物体検出・認識精度がボトルネックとなっています。物体認識の精度を上げるための手法としては、YOLOのによって検出された画像をLLaVAやGPT-4Vなどのマルチモーダル大規模言語モデルに認識させる方法などが考えられますが処理速度とのトレードオフになりますので,使い分けが必要と考えられます。
また,現在はロボットの位置情報を得るためにARマーカーを利用していますが,これ以外にもモーションキャプチャーやSLAMなどの手法を用いることが考えられます。
終わりに
今回のロボットを作成した目的の一つは,近年急速に性能を向上させている大規模言語モデル(LLM)を,ロボットの制御にどのように応用できるかについてテストすることでしたが,結果として人とロボットを結びつけるツールとしてLLMが有効であることが確認できました。
また,今回のロボットはあくまでも技術実証レベルですが、その発展形として道案内ロボットなども考えられます。 具体例としては,博物館などで予めロボットに展示物を登録しておき,訪問者が展示物の場所をロボットに尋ねるとその場所まで案内してくれるというものです。 従来手法では決められた語句を用いてロボットに命令を与える必要がありましたが、LLMを用いることで自然言語での柔軟な命令が可能になると考えられます。
(この記事は研究室インターンで取り組みました:https://kojima-r.github.io/kojima/)