ChatGPTと画像認識で実機ロボットを制御してみる

# はじめに
　画像生成や自然言語処理を中心に近年大きな盛り上がりを見せる機械学習ですが，ロボットの分野においても機械学習が応用される機会は増えてきています。従来のロボットは主にルールベースで，具体的にプログラムされた指示に従って動作することが一般的でしたが，機械学習を導入することで部分的ではあるもののロボットが環境からデータを学習し，より柔軟にタスクをこなせるようになりました。例えば，教師あり学習を用いた音声認識や画像認識は，ロボットシステムにおけるセンサー情報の処理に広く利用されています。また，強化学習はロボット自体の行動を最適化する手法として活発に研究が行われています。

　一方で，人間のような会話を実現するChatGPT等の先進的なモデルが登場して以降大きな注目を集めている大規模言語モデル（Large Language Models，LLM）ですが，LLMがロボット制御において活用されている例は未だに限定的です。しかしLLMが持つ高い文脈理解力や思考力のようなものは，ロボットに応用することでこれまでよりも柔軟な動作を実現可能にすると考えられます。そこで今回は簡単なデモロボットを作成し，LLMがロボット制御に対してどの程度有効か，またどのような利用方法が考えられるかについて探ります。




# ロボットの概要
　ここでは今回作成したデモロボットの全体像を簡単に説明します。[ProtoPediaの記事](https://protopedia.net/prototype/5014)や[GitHub](https://github.com/Azuma413/voice_gpt.git)も適宜参照してください。

### ロボットの見た目
|![unnamed.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/770e5705-ce7a-13f7-1385-dc2117544e5e.jpeg)|![unnamed (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/24e1cb63-797c-c40e-4dd6-09feb3a44ec7.jpeg)|
|---|---|

　本体部分には[RT CORPORATION](https://rt-net.jp/)の二輪移動ロボット[Raspberry Pi Mouse V3](https://rt-net.jp/products/raspberrypimousev3/)を使用しており，外側のカバーは3Dプリンターで作成しています。

### システムの概要
　このロボットのシステムはROS 2 Humbleによって作成されており，ロボットの各機能はROS2ノードとして実装してあります。各ノードは以下のように接続されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/e8bdb20d-325e-e275-d2fd-f20a73b0a870.png)

　下に各ノードの機能を簡単に説明します。
- 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トピックとして配信します。

https://github.com/IntelRealSense/realsense-ros.git

- raspimouse
`raspimouse`は株式会社アールティが配布しているパッケージで，ラズパイマウスの各機能をROS2から制御できるようにします。

https://github.com/rt-net/raspimouse2

　このロボットでは`gpt1_node`と`gpt2_node`の２つのノードでLLMを利用しており，２段階でChatGPTを用いることで自然言語による命令をロボットに理解可能な表現まで落とし込んでいます。









# ロボットの詳細
### ・[音声認識(vosk_node)](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover/chat_rover/vosk_node.py)
音声認識は音声をテキストに変換する技術でありSTT(Speach to Text)とも呼ばれています。STTをするには様々なライブラリやAPIがありますが，無料であり，かつ高い精度が出るという事で今回はPython版の[VOSK](https://alphacephei.com/vosk/)を使用しています。

https://tam5917.hatenablog.com/entry/2022/11/03/074937

こちらの記事を参考にして音声認識を行うプログラムをROS2のServiceServerとして実装しました。


### ・[物体認識(yolo_node)](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover/chat_rover/yolo_node.py)
物体認識を行うために定番の手法としてYOLO(You Only Look Once)というものがありますが，YOLOは制作者や制作時期によって複数のバージョンが存在しており，今回はその中の[YOLO-NAS](https://docs.ultralytics.com/ja/models/yolo-nas/)というバージョンを使用しました。
また、計算コストを抑えるために物体認識はServiceServerにリクエストがあったときのみ行うようにしています。

![Screenshot from 2024-01-25 16-18-10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/1119fb68-4133-52e1-1da0-ed374e776a38.png)


### ・[ロボットの検出(ar_node)](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover/chat_rover/ar_node.py)
ロボットの位置と方向を取得する方法はいくつか考えられますが、今回は簡単のためにロボットにARマーカーを貼り付け、ARマーカーの位置と方向を取得することでロボットの情報を取得しています。ARマーカーの読み取りにはOpenCVのArUcoを用いています。

![Screenshot from 2024-01-25 16-24-34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/fe5156f9-33bd-55b2-0de2-1d6ace42ea5c.png)


`color_image`という変数に画像が入っているとして，ARマーカーの検出は最小限以下のようなコードによって行うことができます。
```py
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形式にするモード](https://platform.openai.com/docs/guides/text-generation/json-mode)が実装されているため，それを利用しています。
　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形式とすることを明示する必要があります。

[__ソースコード__](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover/chat_rover/gpt1_node.py)



　２つ目のChatGPTは、１つ目の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 }]}
```
　こちらのプロンプトも基本的に英語で記述し，また入出力の例を記述しています。

[__ソースコード__](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover/chat_rover/gpt2_node.py)






### ・[全体制御(bt_node)](https://github.com/Azuma413/voice_gpt/tree/main/chat_rover_bt)
　`bt_node`は[BehaviorTree](https://www.behaviortree.dev/)を内包しており，ROSノードとして実装された個々の機能はこれによって実行順序を制御されます。ROS2でBehaviorTreeを使う方法については[別の記事](https://qiita.com/hirekatsu0523/items/0315b22d05224b3d8866)で解説しているので参照してください。
　BehaviorTreeのノード(≠ROSノード)は下の図のように接続されており，`Root`から順に深度優先探索的にノードが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3231080/2072e37d-88fa-54b6-55b8-42e3232cff32.png)

　プログラムの流れとしては，まず`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)](https://github.com/Azuma413/voice_gpt/blob/main/chat_rover_bt/src/pos2vel_node.cpp)

　ロボット(Raspberry Pi Mouse)を操作するには株式会社アールティが配布してる[サンプルプログラム](https://github.com/rt-net/raspimouse2)をそのまま利用しています。このプログラムでは速度制御によってロボットを操作するため，`pos2vel_node`では`bt
node`から送られてきた目標位置と，`ar_node`から送られてくるロボットの位置と角度をもとにして，速度制御を行います。
　具体的には`pos2vel_node`にはActionSreverが実装されており，これが呼び出されるとロボットの現在位置と目標位置から生成したベクトルのノルムに応じた速度の比例制御と、ロボットの方向とベクトルの向きに応じた角速度のPID制御を行います。


# 改善点
　現在のシステムではYOLOの物体検出・認識精度がボトルネックとなっています。物体認識の精度を上げるための手法としては、YOLOのによって検出された画像を[LLaVA](https://llava-vl.github.io/)やGPT-4Vなどのマルチモーダル大規模言語モデルに認識させる方法などが考えられますが処理速度とのトレードオフになりますので，使い分けが必要と考えられます。
 　また，現在はロボットの位置情報を得るためにARマーカーを利用していますが，これ以外にもモーションキャプチャーやSLAMなどの手法を用いることが考えられます。
 

# 終わりに
　今回のロボットを作成した目的の一つは，近年急速に性能を向上させている大規模言語モデル（LLM）を，ロボットの制御にどのように応用できるかについてテストすることでしたが，結果として人とロボットを結びつけるツールとしてLLMが有効であることが確認できました。
　また，今回のロボットはあくまでも技術実証レベルですが、その発展形として道案内ロボットなども考えられます。 具体例としては，博物館などで予めロボットに展示物を登録しておき，訪問者が展示物の場所をロボットに尋ねるとその場所まで案内してくれるというものです。 従来手法では決められた語句を用いてロボットに命令を与える必要がありましたが、LLMを用いることで自然言語での柔軟な命令が可能になると考えられます。

 (この記事は研究室インターンで取り組みました：https://kojima-r.github.io/kojima/)