diff --git a/chapter_rl_sys/control_code_ex.md b/chapter_rl_sys/control_code_ex.md new file mode 100644 index 0000000..0565e01 --- /dev/null +++ b/chapter_rl_sys/control_code_ex.md @@ -0,0 +1,113 @@ +## 控制系统案例 + +在上一章节中,我们初步了解了机器人的控制系统,同时也知道了机器学习在机器人控制系统这个领域有着很多有趣和有前景的研究方向。 +只不过由于控制系统的复杂性和这些研究的前瞻性,它们不太适合用来作为简单的案例。 + +与此同时,ROS作为一个成熟的机器人框架,它已经包含了很多成熟稳定的经典控制组件。 +这些控制组件和其他的成熟的功能模块一起组成了更大规模的功能模组,来完成更复杂的任务。 + +在这些更大规模的功能模组中,**Nav2**和**MoveIt2**可能是最常用的两个。 + +从名字上就可以看出来,这两个功能模组各自都是它们ROS1版本的继承者。 +Nav2是ROS Navigation Stack在ROS2中的继承者,专注于移动机器人的导航相关的功能,例如定位,路径规划等,并致力于用安全的方式将机器人从一点移动到另一点。 +MoveIt2是ROS MoveIt在ROS2中的继承者,致力于打造一个容易使用的机器人操纵平台。带机械臂的机器人都基本离不开它。 + +这两个模组都成熟,可靠,和容易使用。使用ROS框架开发机器人是基本上都会直接使用它们或者在它们已有功能的基础上做适合自己的自定义修改,以避免重复造轮子。 + +因此,在本章节中,我们将以Nav2为案例,来带领大家初步了解怎样使用一个大型的ROS2功能模组。 + +本章节的内容很大程度参考了Nav2的[英文官方文档](https://navigation.ros.org/),尤其是“Getting Started”这一章。对自己英文有信心的读者可以尝试阅读官方文档以了解更多细节。 + +本章没有额外的代码案例。 + +### 安装 + +首先,让我们通过Ubuntu的库管理器来安装Nav2相关的程序库。 + +```shell +sudo apt install ros-foxy-navigation2 ros-foxy-nav2-bringup +``` + +其中`ros-foxy-navigation2`是Nav2的核心程序库,而`ros-foxy-nav2-bringup`则是Nav2的一个启动案例。 +这个案例十分灵活,很多时候我们可以将其稍加修改后放到自己的项目中使用。 + +接下来让我们安装`turtlebot3`相关的一系列程序库。 +turtlebot系列是一个很成功的入门级移动机器人系列。 +而这一系列程序库则提供了和turtlebot3机器人相关的组件,其中包含了在模拟环境中使用虚拟turtlebot3机器人的相关功能组件。 + +```shell +sudo apt install "ros-foxy-turtlebot3*" +``` + +### 运行 + +在安装好上面的那些程序库后,我们就可以尝试使用Nav2了。 + +首先,让我们新开一个终端窗口,并执行以下命令。这些命令分别导入了ROS2框架,并设定好了我们要使用哪个Turtlebot3模型和在哪儿搜索虚拟世界(Gazebo)需要的模型。 + +```shell +source /opt/ros/foxy/setup.bash +export TURTLEBOT3_MODEL=waffle +export GAZEBO_MODEL_PATH=$GAZEBO_MODEL_PATH:/opt/ros/foxy/share/turtlebot3_gazebo/models +``` + +现在,我们一切就绪,可以下面这行命令来运行一个Nav2的演示程序。 + +```shell +ros2 launch nav2_bringup tb3_simulation_launch.py +``` + +其中`ros2 launch`命令是用来执行一个launch文件,而后者则是将很多需要启动的ROS2组件集合到一起来按计划启动的一个说明文件。 +一个机器人项目经常需要启动很多个不同的组件来配合完成任务。 +而如果每个组件都要新开一个窗口执行命令的话,整个机器人的启动将会变得十分繁琐。 +launch文件和`ros2 launch`命令就是来解决这个问题的。 +我们可以把整个ROS2项目想象成一个交响乐团,其中每个组件分别代表一个乐器。 +而launch文件就像是乐团的指挥,负责调配每个乐器应该在什么时候启动。 +总而言之,这是ROS2中一个非常使用的特性。 + +关于`ros2 launch`命令和launch文件的更多细节,感兴趣的读者可以查阅[官方英文文档](https://docs.ros.org/en/foxy/Tutorials/Launch/Creating-Launch-Files.html)。 + +成功运行上述命令之后,我们应该会看到两个新开的GUI窗口,分别对应`RViz`和`Gazebo`程序。 +其中`RViz`是ROS2框架的可视化接口,我们稍后将通过它来控制我们的虚拟机器人。 +而`Gazebo`则是一个用过创建和运行虚拟世界的软件。 +它独立于ROS2框架,但两者又互相紧密合作。 + +在`Gazebo`窗口中(如下图所示),我们应该能够看到一个三维的类六边形虚拟世界。 +这个世界中还有一个虚拟的Turtlebot3机器人。 +这个机器人发射出很多蓝色的射线。 +这些射线代表了机器人的激光雷达的读数射线。 +而激光雷达的读数则被Nav2用来在环境中定位机器人。 + +![Gazebo 截图 1](../img/ch13/ros2-gazebo-1.JPG) + +在`RViz`窗口中(如下图所示),我们应该能够看到虚拟世界的一个二维地图。 +地图上的白色部分是机器人可以到达的部分,而黑色则是检测到的障碍物或墙。 +如果你在左侧看到有红色的`Global Status: Error`错误的话,你的机器人并没有在RViz(即ROS2框架)中正确的定位。 +请在工具栏选择`2D Pose Estimate`并在RViz地图上机器人应该在的位置(以Gazebo中机器人的位置为准)更新好机器人的姿态。 + +![RViz 截图 1](../img/ch13/ros2-rviz-1.JPG) + +更新好机器人的姿态后,RViz应该和下图比较相似。 + +![RViz 截图 2](../img/ch13/ros2-rviz-2.JPG) + +这样一来,我们的机器人就准备好在虚拟事件中移动了。 + +请在RViz的工具栏中选择`Navigation2 Goal`按钮,并在地图上选择你想要Turtlebot3机器人最终所到达的位置和姿态。 +一旦选好了,你将会看到机器人开始向目标位置移动并最终到达目标。 + +RViz还提供了很多其它的Nav2功能的按钮,你可以通过Nav2和ROS2的官方英文文档来了解更多使用方法。 + +恭喜,你现在初步了解了怎样使用ROS2框架内的大型功能模组! + +#### 章节附录:在WSL中使用Nav2 + +有些读者可能是通过Windows下的WSL(Windows Subsystem for Linux)来运行ROS2的。 +如果是这种情况,这一章节中的图形界面程序,如RViz和Gazebo,可能会造成问题。 +这是因为WSL默认并不能打开图形界面程序。 + +幸运的是,我们可以更改设置来达到在WSL中运行图形界面程序这一点。 +[这篇笔记](https://github.com/rhaschke/lecture/wiki/WSL-install)介绍了其作者是如何在WSL中运行ROS2和图形界面的。其中第二点尤为值得注意。 +而[这篇笔记](https://github.com/cascadium/wsl-windows-toolbar-launcher#firewall-rules)则更为细致的介绍了在一般情况下怎样在WSL中运行图形界面程序。 + +这两篇笔记应该可以给读者足够的信息来解决上述所说的和RViz还有Gazebo相关的问题。唯一的缺点就是这两篇笔记都是英文的,对读者的英语水平有一定要求。 \ No newline at end of file diff --git a/chapter_rl_sys/index.md b/chapter_rl_sys/index.md index 9aa6148..dddc891 100644 --- a/chapter_rl_sys/index.md +++ b/chapter_rl_sys/index.md @@ -15,7 +15,11 @@ rl_sys_intro ros ros_code_ex perception +perception_code_ex planning +planning_code_ex control +control_code_ex +robot_safety summary ``` diff --git a/chapter_rl_sys/perception_code_ex.md b/chapter_rl_sys/perception_code_ex.md new file mode 100644 index 0000000..5c969d8 --- /dev/null +++ b/chapter_rl_sys/perception_code_ex.md @@ -0,0 +1,212 @@ +## 感知系统案例 + +在之前[机器人操作系统(ROS)的入门案例](./ros_code_ex.md)这一章节中,我们学习了怎样创建一个ROS2项目以及怎样使用ROS2框架下的节点,服务,动作等。然后,我们又在上一章节中初步了解了机器人的感知系统。 +在这一章节中,我们将通过一个简单的案例来演示怎样结合ROS2和深度学习框架PyTorch来完成一个我们设想的感知系统中的一个基本功能。 + +### 案例背景 + +假设我们想要帮某果园设计一款全自动摘菠萝机器人。 +这个机器人可能需要有一个智能移动底盘来负责在果园中移动,若干传感器(包括一个RGB摄像头)来检测菠萝,以及一个机械臂来负责摘取动作。 +在这个机器人需要完成的一长列各种功能中,它的感知系统必然需要能检测摄像头传感器的画面中央是否有一个菠萝。 +检测到了菠萝才会进入到摘取的环节。 + +这个检测在图像中央是否有菠萝存在的功能,就是我们机器人的感知系统中基础但必要的一个基本功能。 +幸运的是,随着现代卷积神经网络的发展,我们可以利用已存在的深度学习框架,如PyTorch,来快速的完成这个功能。 +而且一个简单的使用ImageNet进行预训练的AlexNet就以及足够了。 + +在之前的案例中,我们都是使用的ROS2框架下的程序库。在这个例子中,我们将开始了解如何在ROS2中使用框架外的Python库。 + +和之前的案例类似,本章节的案例所使用的代码可以在本书相关的[ROS2案例代码库](https://github.com/openmlsys/openmlsys-ros2)中的`src/object_detector`文件夹内找到。 + +### 项目搭建 + +让我们沿用之前已经搭建好的ROS2项目框架。 +我们只需在其中增加一个ROS2的Python库来实现我们想要的功能即可。 +因此,让我们回到`src`目录下并创建此Python库。 + +```shell +cd openmlsys-ros2/src +ros2 pkg create --build-type ament_python --node-name object_detector_node object_detector --dependencies rclpy std_msgs sensor_msgs cv_bridge opencv-python torch torchvision torchaudio +``` + +在创建好Python库后,别忘了将`package.xml`和`setup.py`中的`version`,`maintainer`,`maintainer_email`,`description`和`license`项都更新好。 + +紧接着,我们需要安装ROS2框架下的`image_publisher`库。 +这个库能帮助我们将一张图片模拟成像摄像头视频一样的图片流。 +在开发真是机器人时,我们可能可以在实机上检测我们的程序,但是对于这个案例,我们只能使用这个`image_publisher`库和若干选择好的图片来测试我们的程序。 +实际上,就算是开发实际机器人的功能,也最好在实际测试之前使用图片来做这个功能的单元测试。 + +我们只需通过ubuntu的`apt`来安装这个`image_publisher`库,因为作为一个常用的ROS2框架下的程序库,它已经被打包好以便通过ubuntu的包管理器来安装。 + +```shell +sudo apt install ros-foxy-image-publisher +``` + +关于`image_publisher`这个库更多的信息和使用方法,可以查看[它的文档](http://wiki.ros.org/image_publisher)。这是个针对早期ROS1版本的文档,但是因为这个库之后没有任何变化,文档中所有的功能都和我们所使用的ROS2版本中的一样。 + +接下来,让我们在ROS2项目的Python虚拟环境中安装`opencv-python`,`torch`,`torchvision`和`torchaudio`。 +例如使用`pipenv`的用户可能会执行`pipenv install opencv-python torch torchvision torchaudio`这条命令。 + +最后,让我们把下面这两张菠萝和苹果的图片保存在`openmlsys-ros2/data`下。 +我们将使用这两张图片来检测我们的程序可以检测到菠萝并且不会把菠萝当成苹果。 + +![菠萝图片](../img/ch13/ros-pineapple.jpg) + +:width:`256px` + +:label:`ros2-pineapple` + +![苹果图片](../img/ch13/ros-apple.jpg) + +:width:`256px` + +:label:`ros2-apple` + +### 添加代码 + +之前创建Python库的命令应该已经帮我们创建好了`src/object_detector/object_detector/object_detector_node.py`这个文件。现在让我们用以下内容来替换掉此文件中已有的内容。 + +```Python +import rclpy +from rclpy.node import Node + +from std_msgs.msg import Bool +from sensor_msgs.msg import Image +import cv2 +from cv_bridge import CvBridge + +import torch +import torchvision.models as models +from torchvision import transforms + + +class ObjectDetectorNode(Node): + + PINEAPPLE_CLASS_ID = 953 + + def __init__(self): + super().__init__('object_detector_node') + self.detection_publisher = self.create_publisher(Bool, 'object_detected', 10) + self.camera_subscriber = self.create_subscription( + Image, 'camera_topic', self.camera_callback, 10, + ) + self.alex_net = models.alexnet(pretrained=True) + self.alex_net.eval() + self.preprocess = transforms.Compose([ + transforms.ToPILImage(), + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ]) + self.cv_bridge = CvBridge() + self.declare_parameter('detection_class_id', self.PINEAPPLE_CLASS_ID) + self.get_logger().info(f'Detector node is ready.') + + def camera_callback(self, msg: Image): + self.get_logger().info(f'Received an image, ready to detect!') + detection_class_id = self.get_parameter('detection_class_id').get_parameter_value().integer_value + img = self.cv_bridge.imgmsg_to_cv2(msg) + input_batch = self.preprocess(img).unsqueeze(0) + img_output = self.alex_net(input_batch)[0] + detection = Bool() + detection.data = torch.argmax(img_output).item() == detection_class_id + self.detection_publisher.publish(detection) + self.get_logger().info(f'Detected: "{detection.data}", target class id: {detection_class_id}') + + +def main(args=None): + rclpy.init(args=args) + object_detector_node = ObjectDetectorNode() + rclpy.spin(object_detector_node) + object_detector_node.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() +``` + +可能有些细心的读者已经发现了,这段代码和我们之前创建ROS2节点的代码非常像。 +实际上这段代码就是创建了一个新的节点类来完成我们的功能。 + +这个节点类的实例将被赋予名字`object_detector_node`,同时它将订阅`camera_topic`这个主题并发布`Bool`类型的信息至`object_detected`主题。 +其中,`camera_topic`主题的内容是机器人的摄像头传感器所接收到的视频流,而我们将用`image_publisher`库和之前的图片来模拟这个视频流。 +而`object_detected`主题则将包含我们的检测结果以供机器人逻辑链的后续节点使用。 +如果我们检测到菠萝,则我们将发布`True`信息,否则就发布`False`信息。 + +下面,让我们关注这个新节点类中的一些新细节。 + +首先,我们引入了`cv_bridge.CvBridge`这个类。 +这个类是ROS2框架内的一个功能类,主要帮我们把图片在`opencv`/`numpy`格式和ROS2自己的`sensor_msgs.msg.Image`信息格式之间进行转换。 +在我们新的节点类中我们可以看到它的具体用法(即`self.cv_bridge = CvBridge()`和`img = self.cv_bridge.imgmsg_to_cv2(msg)`)。 + +然后,在新的节点类`ObjectDetectorNode`中,我们使用了`PINEAPPLE_CLASS_ID`这个类成员变量来保存我们想要识别的物体在ImageNet中的类别ID(class id)。这里`953`是菠萝在ImageNet中的具体类别ID。 + +再之后,我们通过PyTorch实例化了一个预训练好的AlexNet,并将其设置到`eval`状态。 +同时,我们声明了`detection_class_id`这个参数,以方便再运行时修改需要识别物体的类别ID(虽然这并不常用)。 + +最后,在`camera_topic`主题的回调函数`camera_callback`中,我们将收到的`Image`类型信息传换成`numpy`格式,然后调用AlexNet来进行物体识别,最后将识别结果以`Bool`的形式发布到`object_detected`主题上去,并进行日志记录。 + +至此,一个使用PyTorch和AlexNet来识别摄像头中是否有菠萝的节点类就完成了。 + +### 运行及检测 + +下面,让我们尝试运行我们新写好的节点类并用菠萝和苹果的图片来检测这个节点类是否运行正常。 + +首先,让我们编译这个新写的Python库。 + +```shell +cd openmlsys-ros2 +colcon build --symlink-install +``` + +在成功编译之后,我们可以新开一个终端窗口并执行下面的命令来运行一个节点类实例。 +记住,你可能需要先运行`source install/local_setup.zsh`来引入我们自己的ROS2项目。 + +```shell +ros2 run object_detector object_detector_node --ros-args -r camera_topic:=image_raw +``` + +如果你遇到`ModuleNotFoundError: No module named 'cv2'`这之类的问题,则代表ROS2命令没有成功识别你Python虚拟环境中的程序库。这时候你可以尝试进入你所用的虚拟环境后执行下面这个命令。 + +```shell +PYTHONPATH="$(dirname $(which python))/../lib/python3.8/site-packages:$PYTHONPATH" ros2 run object_detector object_detector_node --ros-args -r camera_topic:=image_raw +``` + +这个命令前面`PYTHONPATH="$(dirname $(which python))/../lib/python3.8/site-packages:$PYTHONPATH"`这一串的作用是将你目前Python环境的库添加到`PYTHONPATH`,这样ROS2命令的Python就可以找到你目前Python环境(即ROS2项目所对应的Python虚拟环境)中的Python库了。 + +当这个ROS2命令成功运行时,你应该能看到这行信息:`[INFO] [1655172977.491378700] [object_detector_node]: Detector node is ready.`。 + +另外,在这个ROS2命令中,我们使用了`--ros-args -r camera_topic:=image_raw`这一系列参数。 +这些参数是用来告诉ROS2将我们新节点类所使用的`camera_topic`主题重映射(remap)到`image_raw`这个主题上。 +这样一来,我们新节点类所有使用`camera_topic`主题的场合实际上都是在使用`image_raw`这个主题。 +使用主题名字重映射的好处是在于解耦合。 +对于每个新的ROS2程序库或者每个新的节点类,我们都可以自由的命名我们要使用的主题的名字,然后当它需要和其它组件组合起来发挥作用时,只需要使用重映射将两个不同组件所使用的不同主题名字连接起来,就可以达到数据在两个组件之间正常流通的效果。 +这实际上是ROS2框架的一个很实用的特性。 + +如果你想更深入的了解重映射相关的细节,可以阅读[这篇官方介绍](https://design.ros2.org/articles/static_remapping.html)。 + +在我们成功运行新节点后,让我们在一个新终端窗口中运行下面这行命令来测试它是否能检测到菠萝。同样的,你可能需要先运行`source install/local_setup.zsh`来引入我们自己的ROS2项目。 + +```shell +PYTHONPATH="$(dirname $(which python))/../lib/python3.8/site-packages:$PYTHONPATH" ros2 run image_publisher image_publisher_node data/ros-pineapple.jpg --ros-args -p publish_rate:=1.0 +``` + +上面这行命令将会使用`image_publisher`库和它的节点来以1Hz的频率将之前准备好的菠萝图片发布到`image_raw`这个主题上去。 +当这个`image_publisher_node`节点成功运行后,我们应当能在`object_detector_node`节点运行的终端窗口中看到类似`[INFO] [1655174212.930385900] [object_detector_node]: Detected: "True", target class id: 953`这样的信息来证明我们的节点类能够检测到菠萝。 + +接着让我们在`image_publisher_node`节点的窗口中使用`Ctrl+C`结束掉节点,然后使用下面这行命令来发布准备好的苹果图片。 + +```shell +PYTHONPATH="$(dirname $(which python))/../lib/python3.8/site-packages:$PYTHONPATH" ros2 run image_publisher image_publisher_node data/ros-apple.jpg --ros-args -p publish_rate:=1.0 +``` + +现在,在`object_detector_node`节点运行的终端窗口中我们应该看到的是类似`[INFO] [1655171989.912783400] [object_detector_node]: Detected: "False", target class id: 953`这样的信息来证明我们的节点类不会把苹果识别成菠萝。 + +### 小结 + +恭喜,你已经成功了解如何在ROS2项目中使用ROS2框架外的Python库了! +如果你使用Python虚拟环境,你可能需要额外的设置`PYTHONPATH`环境变量。 +另外,主题名字重映射(Name Remapping)是一个很有用的ROS2特性。 +你在以后的项目中很可能会经常用到它。 \ No newline at end of file diff --git a/chapter_rl_sys/planning_code_ex.md b/chapter_rl_sys/planning_code_ex.md new file mode 100644 index 0000000..2e3b8fb --- /dev/null +++ b/chapter_rl_sys/planning_code_ex.md @@ -0,0 +1,208 @@ +## 规划系统案例 + +在上一章节中,我们初步了解了机器人的规划系统。 +这一章节中,我们将通过一个简单的案例来演示怎样结合ROS2和机器学习框架scikit-learn来完成一个我们设想的规划系统中的一个基本功能。 +我们将使用和[感知系统案例](./perception_code_ex.md)这一章节类似的方法和结构来讲解本章节。 + +### 案例背景 + +假设我们想要帮某花园设计一款打理鸢尾花的园丁机器人。 +很“碰巧”的是,这个小花园里面正好只有经典的[鸢尾花数据集](https://scikit-learn.org/stable/auto_examples/datasets/plot_iris_dataset.html)中的那三种鸢尾花,而且已经有人帮我们完成了一个“魔术般的”ROS2感知组件来自动的检测目标鸢尾花的花萼长和宽以及花瓣长和宽(Sepal Length, Sepal Width, Petal Length and Petal Width:鸢尾花数据集所需要的4个输入维度)。 +同时因为机器人的性能限制,我们不能使用比较复杂的模型(例如神经网络)。 +这种情况下,我们可以尝试使用经典的机器学习模型,例如决策树,来接受感知组件的结果并识别鸢尾花的类别,然后用一个映射表(mapping table)来查找出我们应该为机器人规划怎样的行为去执行。 +当季节或情况改变时,花园的技术团队可以更新映射表来更改机器人的规划系统逻辑。 + +当然,上面的案例背景和解决方案都是为了生成一个简单的案例而设计的“非现实”的例子。 +大家在现实项目中遇到的案例应该会复杂的多。 +不过,我们任然希望这样一个简单的案例可以为大家带来些许价值。 + +让我们回到我们刚刚介绍的解决方案中。 +在之前的感知系统的案例中,我们选择使用ROS2节点类来处理感知任务。 +这是因为机器人会不断的接收到传感器的信号,而我们希望尽可能多的处理收到的信号。 +而对于我们这一章节的案例来说,因为我们不一定需要不间断的进行新的规划,同时每一次规划我们都期待有一个结果,所以使用ROS2服务可能会是一个更好的选择。 + +和之前的案例类似,本章节的案例所使用的代码可以在本书相关的[ROS2案例代码库](https://github.com/openmlsys/openmlsys-ros2)中的`src/action_decider`文件夹内找到。 + +### 项目搭建 + +让我们继续沿用之前已经搭建好的ROS2项目框架。 +和感知系统案例类似,我们只需在其中增加一个ROS2的Python库来实现我们想要的功能即可。 +因此,让我们回到`src`目录下并创建此Python库。 + +```shell +cd openmlsys-ros2/src +ros2 pkg create --build-type ament_python --node-name action_decider_node action_decider --dependencies rclpy std_msgs scikit-learn my_interfaces +``` + +我们将`my_interfaces`添加为依赖项是因为我们需要为新的ROS2服务创建对应的消息类型接口。 + +在创建好Python库后,别忘了将`package.xml`和`setup.py`中的`version`,`maintainer`,`maintainer_email`,`description`和`license`项都更新好。 + +接下来,让我们在ROS2项目的Python虚拟环境中安装`scikit-learn`。 +例如使用`pipenv`的用户可能会执行`pipenv install scikit-learn`这条命令。 + +### 添加消息类型接口 + +我们将要编写的新ROS2服务需要有它自己的服务消息接口。 +让我们借用已有的`my_interfaces`库来放置这个新接口。 + +首先,让我们在`openmlsys-ros2/src/my_interfaces/srv`中新建一个名为`IrisData.srv`的文件并用下面的内容填充它。 + +```text +float32 sepal_length +float32 sepal_width +float32 petal_length +float32 petal_width +--- +string action +``` + +我们可以看到,新的ROS2服务将会接受4个浮点值作为输入。 +这4个浮点值分别为鸢尾花的花萼的长和宽还有花瓣的长和宽。 +当规划完成后,服务会返回一个字符串。 +这个字符串将会是机器人需要执行的动作的名称。 + +我们还需要在`my_interfaces`库的`CMakeLists.txt`文件中的相应位置(`rosidl_generate_interfaces`函数的参数部分)添加一行新的内容: + +```cmake +"srv/IrisData.srv" +``` + +最后,别忘了在ROS2项目的根目录下执行`colcon build --packages-select my_interfaces`来重新编译`my_interfaces`这个库。 + +### 添加代码 + +之前创建Python库的命令应该已经帮我们创建好了`src/action_decider/action_decider/action_decider_node.py`这个文件。现在让我们用以下内容来替换掉此文件中已有的内容。 + +```Python +import os +import pickle + +import rclpy +from rclpy.node import Node + +from std_msgs.msg import String +from my_interfaces.srv import IrisData + +from sklearn.datasets import load_iris +from sklearn import tree + + +def main(args=None): + rclpy.init(args=args) + action_decider_service = ActionDeciderService() + rclpy.spin(action_decider_service) + action_decider_service.destroy_node() + rclpy.shutdown() + + +class ActionDeciderService(Node): + + IRIS_CLASSES = ['setosa', 'versicolor', 'virginica'] + + IRIS_ACTION_MAP = { + 'setosa': 'fertilise', + 'versicolor': 'idle', + 'virginica': 'prune', + } + + DEFAULT_MODEL_PATH = f'{os.path.dirname(__file__)}/../../../data/iris_model.pickle' + + def get_iris_classifier(self, model_path): + if os.path.isfile(model_path): + with open(model_path, 'rb') as model_file: + return pickle.load(model_file) + self.get_logger().info(f"Cannot find trained model at '{model_path}', will train a new model.") + iris = load_iris() + X, y = iris.data, iris.target + clf = tree.DecisionTreeClassifier() + clf = clf.fit(X, y) + with open(model_path, 'wb') as model_file: + pickle.dump(clf, model_file) + return clf + + def __init__(self): + super().__init__('iris_action_decider_service') + self.srv = self.create_service(IrisData, 'iris_action_decider', self.decide_iris_action_callback) + self.iris_classifier = self.get_iris_classifier(self.DEFAULT_MODEL_PATH) + self.get_logger().info('Iris action decider service is ready.') + + def decide_iris_action_callback(self, request, response): + iris_data = [request.sepal_length, request.sepal_width, request.petal_length, request.petal_width] + iris_class_idx = self.iris_classifier.predict([iris_data])[0] + iris_class = self.IRIS_CLASSES[iris_class_idx] + response.action = self.IRIS_ACTION_MAP[iris_class] + self.get_logger().info( + f'Incoming request\nsepal_length: {request.sepal_length}\nsepal_width: {request.sepal_width}' + f'\npetal_length: {request.petal_length}\npetal_width: {request.petal_width}' + f'\niris class: {iris_class}' + f'\ndecided action: {response.action}' + ) + + return response + + +if __name__ == '__main__': + main() +``` + +细心的读者可能已经发现了,这段代码和我们之前创建的使用ROS2服务的服务端节点类的代码非常像。 +实际上这段代码就是使用了同样的服务端节点类框架和一个新的服务来完成我们想要的功能。 + +这个服务端节点类的实例将被赋予名字`iris_action_decider_service`,它将提供一个名为`iris_action_decider`的服务并且这个服务期待`IrisData`格式的服务请求(即我们之前定义的消息类型接口的请求部分)。 +当服务计算完成后,它将把结果返回给请求发起方。 +这个结果是规划好的行为的名字并被封装到`IrisData`格式的服务结果中去(即我们之前定义的消息类型接口的结果部分)。 + +下面,让我们关注这个新节点类中的一些新细节。 + +首先,我们在新的服务端节点类`ActionDeciderService`中声明了三个类成员变量`IRIS_CLASSES`,`IRIS_ACTION_MAP`和`DEFAULT_MODEL_PATH`。 +它们分别表示鸢尾花的类别标签,鸢尾花类别至机器人行动名称的映射表,和默认存放训练好的决策树模型的路径。 + +当我们的服务端节点类初始化时,它将调用`get_iris_classifier()`来读取训练好的决策树模型。 +如果模型文件缺失,则会重新训练一个模型并保存。 +这里我们把训练模型的代码放到了同一个节点内。 +实际上,对于大型项目或大型模型,我们可以把模型训练和模型使用分开到不同的组件中去,并且它们可能在不同的时机运行。 + +当服务的回调函数`decide_iris_action_callback()`被调用时,服务将会使用训练好的模型和接收到的鸢尾花信息来预测鸢尾花的类别,然后通过查找映射表来决定机器人需要执行的动作。最后服务返回结果并进行日志记录。 + +至此,一个使用scikit-learn和决策树的简易“玩具级”规划组件就完成了。 + +### 运行及检测 + +下面,让我们尝试运行新写好的服务端节点类并检测它是否能正常运行。 + +首先,让我们编译这个新写的Python库。 + +```shell +cd openmlsys-ros2 +colcon build --symlink-install +``` + +在成功编译之后,我们可以新开一个终端窗口并执行下面的命令来运行一个节点类实例。 +记住,你可能需要先运行`source install/local_setup.zsh`来引入我们自己的ROS2项目。 + +```shell +ros2 run action_decider action_decider_node +``` + +如果你使用了Python虚拟环境,则可以尝试下面这条命令,而不是上面那条。背后具体的原因已在之前的案例章节叙述过。 + +```shell +PYTHONPATH="$(dirname $(which python))/../lib/python3.8/site-packages:$PYTHONPATH" ros2 run action_decider action_decider_node +``` + +当这个ROS2命令成功运行时,你应该能看到这行信息:`[INFO] [1655253519.693893500] [iris_action_decider_service]: Iris action decider service is ready.`。 + +在我们成功运行新的服务端节点后,让我们在一个新终端窗口中运行下面这行命令来测试新的服务是否能正常运行。同样的,你可能需要先运行`source install/local_setup.zsh`来引入我们自己的ROS2项目。 + +```shell +ros2 service call /iris_action_decider my_interfaces/srv/IrisData "{sepal_length: 1.0, sepal_width: 2.0, petal_length: 3.0, petal_width: 4.0}" +``` + +这里,我们用的`ros2 service call`命令是专门用来通过命令行调用一个ROS2服务的命令。其中服务请求的数据应该是字符串化的YAML格式数据。这个命令更多的信息可以通过`ros2 service call -h`来查阅。 + +一切顺利的话,执行完命令后不久,你应该就能在新窗口中很快看到类似这样的信息了:`response: my_interfaces.srv.IrisData_Response(action='prune')`。 + +### 小结 + +恭喜,你已经成功了解如何在ROS2项目中使用scikit-learn这样库并训练一个模型了! \ No newline at end of file diff --git a/chapter_rl_sys/robot_safety.md b/chapter_rl_sys/robot_safety.md new file mode 100644 index 0000000..962d6dd --- /dev/null +++ b/chapter_rl_sys/robot_safety.md @@ -0,0 +1,70 @@ +## 在机器人项目中安全的应用机器学习 + +机器人和机器学习都是有广阔前景和令人兴奋的前沿领域,而当它们结合在一起后,会变得更加迷人,并且有远大于1+1>2的效果。 +因此,当我们在机器人项目中应用机器学习时,我们很容易过于兴奋,尝试着用机器学习去做很多之前只能幻想的成果。 +然而,在机器人中应用机器学习和直接使用机器学习有着很多不同。 +其中很重要的一点不同就是,一般的机器学习系统更多的是在虚拟世界中造成直接影响,而机器人中的机器学习系统很容易通过机器人对物理世界造成直接影响。 +因此,**当我们在机器人项目中应用机器学习时,我们必须时刻关注系统的安全性**,保证无论是在产品开发时还是在产品上市后的使用期,开发者和用户的安全性都能得到可靠的保证。 +而且不仅商业项目要考虑安全性,开发个人项目是也需要确保安全性。 +没有人想因为安全性上的疏忽而对自己或朋友/同事造成无法挽回的遗憾。 + +以上这些并不是危言耸听,让我们设想以下这些情况。 + +假设你正在为你们公司开发一个物流仓库内使用的移动货运机器人,它被设计为和工人在同一工作环境内运行,以便在需要时及时帮工人搬运货物至目的地。 +这个机器人有一个视觉的行人识别系统,以便识别前方是否有人。 +当机器人在前进的过程中遇到障碍物的话,这个行人识别系统会参与决定机器人的行为。 +如果有人的话,机器人会选择绕大弯来避开行进道路上的行人障碍物;而如果没人的话,机器人可以绕小弯来避障。 +可是,如果某次这个行人识别系统检测失误,系统没有检测到前方的障碍物是一个正在梯子上整理货物的工人,所以选择小弯避障。 +而当机器人靠近时,工人才突然发现有个机器人正在靠近他,并因此受到惊吓跌落至机器人行进的正前方。如果我们考虑到物流仓库的货运机器人自重加载重一般至少是几百公斤,我们就知道万一真的因此发生碰撞,后果是不堪设想。 +如果真的发生这种情况,这个机器人产品的商业前景会毁于一旦,公司和负责人也会被追究相应责任(甚至法律意义上的责任)。更重要的是,对受害者所造成的伤害和自己心里的内疚会对双方的一生都造成严重的影响。 + +不仅是商业项目,假设你正在开发一个小型娱乐机械臂来尝试帮你完成桌面上的一些小任务,例如移动茶杯或打开关闭开关。 +你的这个机械臂也依赖于一个物体识别系统来识别任务目标。 +某次在移动茶杯时,机械臂没有识别到规划路线中有一个接线板,因此茶杯不小心摔倒并且水泼到接线板里引起短路。 +幸运的话可能只需要换一个接线板,而不幸的时候甚至可能会引起火灾或电击。 +我相信,没有人会想遇到这类突发事件。 + +因此,无论是在怎样的机器人项目中应用机器学习,我们都必须时刻关注和确保系统的安全性。 + +### 确保安全性的办法:谨慎的风险评估和独立的安全系统 + +#### 谨慎的风险评估 + +为了能够确保机器人和机器学习系统的安全性,我们首先要知道可能有哪些危险。 +我们可以通过风险评估(risk assessment)来做到这一点。 + +怎样完成一份风险评估网上已经有很多文章了,我们在这里就不过多的介绍。 +我们想要强调的是,对于发现的风险,我们需要尽可能的给出一个避免风险的方案(risk mitigation)。 +更重要的时,我们需要确保这些方案的具体执行,而不仅仅是流于表面的给出方案就完事。 +一份没有执行的方案等于没有方案。 + +#### 独立的安全系统 + +在了解了可能有哪些风险之后,我们可以通过设计一个独立的安全系统来规避掉风险中和机器人系统相关的那一部分。 + +具体来讲,这个安全系统应该独立于机器学习系统,并且处于机器人架构的底层和拥有足够或最高等级的优先级。 +实际上,这个安全系统不应该只针对机器学习系统,而是应该针对整个机器人的方方面面。 +或者换句话来说,当开发机器人项目时,必须要有一个足够安全且独立的安全系统。 +而针对于机器学习系统的安全性只是这个独立安全系统“足够安全”的部分体现罢了。 + +还是以之前的那个物流仓库移动货运机器人为例。 +如果机器人的轮子是有独立安全回路并且断电自动刹车的轮子,而机器人又有一个严格符合安全标准且也有安全回路的激光雷达来检测障碍物,同时这个激光雷达的安全回路直接连接至轮子的安全回路。 +这样一来,不管机器人是否检测到前方有人或突然有一个人闯入机器人行进路线,激光雷达都会检测到有异物,直接通过独立的安全回路将轮子断电并刹车,以确保不会发生碰撞。 +这样一个配置完全独立于任何控制逻辑,从而不受任何上层系统的影响。 +**而对于开发者来说,当我们有了一个可靠独立的安全系统,我们也可以放心的去使用最新的突破性技术,而不用担心新技术是否会造成不可预期的后果。** + +### 机器学习系统的伦理问题 + +除了上述讨论到的最根本的安全性问题,机器学习系统的伦理问题也会对机器人的使用造成影响。 + +例如训练数据集中人种类型不平衡这一类经典的伦理问题。 +让我们还是以之前的那个物流仓库移动货运机器人为例。 +如果我们的训练数据集只有亚洲人的图片,那么当我们想要开拓海外市场时,我们的海外用户很有可能会发现我们的机器人并不能很好的识别他们的工人。 +虽然独立的安全系统可以避免事故的发生,但是急停在工人面前肯定不是一个很好的用户体验。 +我们机器人的海外销量也会受到影响。 + +机器学习系统的伦理问题是目前比较火热的一个讨论领域。作为行业相关人员,我们需要了解这个方向上的最新进展。一方面是在系统设计的初期就把这些问题考虑进去,另一方面也是希望我们的成果能够给更多人带来幸福,而不是带去困扰。 + +### 小结 + +在这一章节中,我们稍微讨论了下怎样在机器人项目中安全的应用机器学习。我们确认了执行一份谨慎的风险检测和设计一个独立的安全系统是一个不错的办法。最后我们也稍微探讨了下机器学习系统的伦理问题。希望大家都能安全的在机器人项目中运用最新的机器学习技术! \ No newline at end of file diff --git a/chapter_rl_sys/ros_code_ex.md b/chapter_rl_sys/ros_code_ex.md index 678cbc2..3c3a409 100644 --- a/chapter_rl_sys/ros_code_ex.md +++ b/chapter_rl_sys/ros_code_ex.md @@ -23,6 +23,8 @@ pipenv --python $(/usr/bin/python3 -V | cut -d" " -f2) --site-packages 本章节中的案例有参考ROS2的[官方教程](https://docs.ros.org/en/foxy/Tutorials.html)。这个官方教程讲解的非常详细,非常适合初学者入门ROS2。如果读者对英文有自信的话,可以尝试阅读官方教程来了解更多ROS2的细节。 +另外,本章节的案例所使用的代码可以在本书相关的[ROS2案例代码库](https://github.com/openmlsys/openmlsys-ros2)中的`src/my_hello_world`和`src/my_interfaces`文件夹内找到。 + ### 安装ROS2 Foxy Fitzroy 在Ubuntu上安装ROS2相对简单,绝大多数情况跟随官方教程安装即可。 @@ -896,6 +898,4 @@ ros2 run my_hello_world my_sum_action_server ### 小结 -在本章节中,我们了解了怎样安装ROS2和在Python虚拟环境中进行ROS2项目的开发。然后我们通过一些案例来更加深入的了解了ROS2的一些核心概念,即节点,主题,参数,服务,和动作。 - -本章节所有的代码都托管在本章节相关的[GitHub库](https://github.com/openmlsys/openmlsys-ros2)中。欢迎大家使用! \ No newline at end of file +在本章节中,我们了解了怎样安装ROS2和在Python虚拟环境中进行ROS2项目的开发。然后我们通过一些案例来更加深入的了解了ROS2的一些核心概念,即节点,主题,参数,服务,和动作。 \ No newline at end of file diff --git a/img/ch13/ros-apple.jpg b/img/ch13/ros-apple.jpg new file mode 100644 index 0000000..c2710e1 Binary files /dev/null and b/img/ch13/ros-apple.jpg differ diff --git a/img/ch13/ros-pineapple.jpg b/img/ch13/ros-pineapple.jpg new file mode 100644 index 0000000..8e6c8f1 Binary files /dev/null and b/img/ch13/ros-pineapple.jpg differ diff --git a/img/ch13/ros2-gazebo-1.JPG b/img/ch13/ros2-gazebo-1.JPG new file mode 100644 index 0000000..0da9e5d Binary files /dev/null and b/img/ch13/ros2-gazebo-1.JPG differ diff --git a/img/ch13/ros2-rviz-1.JPG b/img/ch13/ros2-rviz-1.JPG new file mode 100644 index 0000000..20b9d73 Binary files /dev/null and b/img/ch13/ros2-rviz-1.JPG differ diff --git a/img/ch13/ros2-rviz-2.JPG b/img/ch13/ros2-rviz-2.JPG new file mode 100644 index 0000000..a9b5666 Binary files /dev/null and b/img/ch13/ros2-rviz-2.JPG differ