# Reference 1. [飞行机械臂仿真环境](https://mubu.com/doc/o1TkigWxq) 2. [遥操作与模仿学习](https://mubu.com/doc/fHZt1nQhZK) 3. 课外阅读:[MimicGen系统](https://mp.weixin.qq.com/s/O1OVokb-K2WVPxmCOAf2yQ):利用人类示教用于机器人操作学习的数据生成系统,[UMI](https://mp.weixin.qq.com/s/caes5225uDozYo8Oai1czg):通用操纵界面,可将人类演示的技能直接转移到可部署的机器人策略中,[对比偏好学习](https://mp.weixin.qq.com/s/I43POxN4WKRySxgeqxUolA),遥操作机器人:[ALOHA机器人](https://mp.weixin.qq.com/s/wAkbO9NSi_Tg5Z5MwLwPFA),[ALOHA2机器人](https://mp.weixin.qq.com/s/hjHBf0VX6s_GNKL4AsYMFg) 4. 可能用到的虚拟机搭建平台:virtual box7.0虚拟机,[虚拟机镜像:roslearn1.ova](http://192.168.35.165:8090/nextcloud/index.php/apps/files/?dir=/23%E7%A7%8B-%E6%99%BA%E8%83%BD%E6%97%A0%E4%BA%BA%E6%9C%BA&fileid=166462)从实验室云盘下载 5. 相关论文:在Ubuntu下遥操作文件夹中,百度网盘中也有备份 6. 这个是那个机械臂课程里面讲到的一个行为克隆强化学习的示例,可能比上面那个纯强化学习策略,更依赖示教数据(遥操作更有意义),也可以参考看看。 https://ai.googleblog.com/2022/04/efficiently-initializing-reinforcement.html 如何使用先验策略有效地初始化强化学习? https://news.qq.com/rain/a/20220906A04CVX00 D4RL的Adroit数据集原始环境安装CSDN: https://blog.csdn.net/konodiodaaa/article/details/131599877 GITHUB: https://github.com/vikashplus/Adroit https://github.com/aravindr93/hand_dapg/ # My Study ## 学习资料 多旋翼飞行器:《多旋翼飞行器设计与控制》([配套资料](https://rflysim.com/doc/zh/C/1.Multicopter_Design.html)) 机械臂:主要了解齐次变换矩阵和DH参数即可 深度强化学习:《深度强化学习》([配套资料](https://github.com/wangshusen/DRL?tab=readme-ov-file)) ## 总体思路 1. **先机械后智能**:先掌握飞行器动力学与控制,再进入强化学习和模仿学习。 2. **理论+仿真+编程**并行:不要只读书,必须结合公式推导、编程实现和仿真实验。 3. **由浅入深,逐步交叉**:先分开学,再将飞行器控制和模仿学习结合起来,最后进入飞行机械臂(飞行器+机械臂)的复杂控制问题。 ------ ## 学习计划(前 6 个月) ### **第一阶段:打好基础 (0-2 个月)** 目标:掌握飞行器动力学、控制基础和强化学习基本概念。 **任务** 1. 《多旋翼飞行器设计与控制》 - 第1-3章:飞行器建模(动力学、运动学、状态空间模型) - 第4-5章:姿态控制与位置控制(PID、LQR) - 配合推导公式,**用 MATLAB/Python 建立一个四旋翼动力学仿真模型**。 2. 《深度强化学习》 - 第1-3章:强化学习基础(MDP、值函数、策略迭代) - 第4-5章:Q-learning、DQN - 实现经典环境(CartPole、MountainCar)的强化学习算法(建议用 Python + PyTorch)。 **产出** - 一个四旋翼简化模型的仿真程序(Python 或 MATLAB)。 - 一个能跑通 OpenAI Gym 环境的 DQN 算法。 ------ ### **第二阶段:进阶与交叉 (2-4 个月)** 目标:将传统控制与强化学习方法联系起来,理解模仿学习的核心思想。 **任务** 1. 《多旋翼飞行器设计与控制》 - 第6-8章:先进控制(非线性控制、鲁棒控制)。 - 尝试在仿真中加入外部扰动(如风力),验证控制器性能。 2. 《深度强化学习》 - 第6-9章:策略梯度方法(REINFORCE、Actor-Critic、PPO)。 - 初步了解模仿学习(行为克隆、逆强化学习)。 **实践** - 用 PPO 训练一个四旋翼的姿态/位置保持控制器(先在简化环境里)。 - 对比传统 PID 和 RL 控制效果。 **产出** - 一个四旋翼姿态控制 RL 算法实验报告。 - 一份 RL 和传统控制对比的仿真数据。 ------ ### **第三阶段:结合飞行机械臂 (4-6 个月)** 目标:进入博士课题核心,理解飞行器与机械臂的耦合动力学,并在此基础上引入模仿学习。 **任务** 1. 学习飞行机械臂动力学建模(可查阅论文/教材补充,书中不一定有)。 - 建立耦合模型:飞行器 + 机械臂关节动力学。 - 重点理解重心偏移、扰动对飞行稳定性的影响。 2. 模仿学习 - 行为克隆(BC):用专家数据训练控制策略。 - GAIL(生成对抗模仿学习)等方法。 3. 实践 - 在仿真环境(Gazebo + ROS / Mujoco / PyBullet)中搭建飞行机械臂模型。 - 收集专家示范(比如人工遥控、传统控制器轨迹)。 - 用模仿学习训练机械臂操作任务(如抓取或物体移动)。 **产出** - 一个飞行机械臂的仿真平台(ROS/Gazebo 或 Mujoco)。 - 一份模仿学习实验报告:用专家演示训练飞行机械臂完成任务。 ------ ## 学习方法建议 1. **读书方式**:每章不要死读,重点在于公式推导 + 代码实现。 2. **编程习惯**:用 GitHub 建立仓库,把每个阶段的代码和报告上传,方便迭代和复盘。 3. **科研习惯**:每个月写一份总结文档(学到什么,遇到什么问题,怎么解决)。 4. **论文积累**:从第 2 个月开始,每周至少读 2 篇相关论文(飞行机械臂、模仿学习) ## 1、搭建机械臂仿真环境 参考[Unitree机械臂仿真环境搭建](https://blog.csdn.net/eyas3/article/details/135505749?spm=1001.2014.3001.5502),最后运行效果: ![w73j8-6xasa](./Picture/遥操作和模仿学习/w73j8-6xasa.gif) ## 2、使用panda-gym和stable-baseline3完成一个demo 完成机械臂强化学习的一个demo,使用panda-gym和stable baseline3完成,参考: https://zhuanlan.zhihu.com/p/491909457 https://blog.csdn.net/qq_47997583/article/details/125009605 运行效果:[DDPG](https://www.bilibili.com/video/BV1SZBfYQELC/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b),[TD3](https://www.bilibili.com/video/BV1SZBfYQE4J/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b),[SAC](https://www.bilibili.com/video/BV1SZBfYQETq/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b) ## 3、使用panda-gym完成unitree z1机械臂仿真训练 [panda-gym2.0.0 docs](/home/mickey/APP/panda-gym-2.0.0/docs/build/html/index.html) 学习panda-gym自定义环境一节,按照文档提示成功导入urdf文件创建宇树机械臂模型,这里要注意导入urdf文件时要连同其所在的文件夹一起导入才行,否则会报错。之后在调用PandaReach-v2任务时出错,已向panda-gym开发者请求帮助。 2024.12.26代码修改完成,具体修改方法为: 1. 添加机器人模型:在```/home/mickey/miniforge3/envs/panda-gym/lib/python3.6/site-packages/panda_gym/envs/robots```文件夹下添加z1.py ```python import numpy as np from gym import spaces from panda_gym.envs.core import PyBulletRobot from panda_gym.pybullet import PyBullet class z1(PyBulletRobot): """Panda robot in PyBullet. Args: sim (PyBullet): Simulation instance. block_gripper (bool, optional): Whether the gripper is blocked. Defaults to False. base_position (np.ndarray, optionnal): Position of the base base of the robot, as (x, y, z). Defaults to (0, 0, 0). control_type (str, optional): "ee" to control end-effector displacement or "joints" to control joint angles. Defaults to "ee". """ def __init__( self, sim: PyBullet, # block_gripper: bool = False, base_position: np.ndarray = np.array([0.0, 0.0, 0.0]), control_type: str = "ee", ) -> None: # self.block_gripper = block_gripper self.control_type = control_type n_action = ( 3 if self.control_type == "ee" else 6 ) # control (x, y z) if "ee", else, control the 6 joints # n_action += 0 if self.block_gripper else 1 action_space = spaces.Box(-1.0, 1.0, shape=(n_action,), dtype=np.float32) super().__init__( sim, body_name="z1", file_name="/home/mickey/mickey_sync/project/my_panda_gym/z1_description/xacro/z1.urdf", base_position=base_position, action_space=action_space, joint_indices=np.array( [1, 2, 3, 4, 5, 6] ), # list of the indices, as defined in the URDF joint_forces=np.array( [30.0, 60.0, 30.0, 30.0, 30.0, 30.0] ), # force applied when robot is controled (Nm) ) # self.fingers_indices = np.array([9, 10]) self.neutral_joint_values = np.array([0.00, 0.00, 0.00, 0.00, 0.00, 0.00]) self.ee_link = 6 # self.sim.set_lateral_friction(self.body_name, self.fingers_indices[0], lateral_friction=1.0) # self.sim.set_lateral_friction(self.body_name, self.fingers_indices[1], lateral_friction=1.0) # self.sim.set_spinning_friction(self.body_name, self.fingers_indices[0], spinning_friction=0.001) # self.sim.set_spinning_friction(self.body_name, self.fingers_indices[1], spinning_friction=0.001) def set_action(self, action: np.ndarray) -> None: action = action.copy() # ensure action don't change action = np.clip(action, self.action_space.low, self.action_space.high) if self.control_type == "ee": ee_displacement = action[:3] target_arm_angles = self.ee_displacement_to_target_arm_angles( ee_displacement ) else: arm_joint_ctrl = action[:6] target_arm_angles = self.arm_joint_ctrl_to_target_arm_angles(arm_joint_ctrl) # if self.block_gripper: # target_fingers_width = 0 # else: # fingers_ctrl = action[-1] * 0.2 # limit maximum change in position # fingers_width = self.get_fingers_width() # target_fingers_width = fingers_width + fingers_ctrl # target_angles = np.concatenate((target_arm_angles, [target_fingers_width / 2, target_fingers_width / 2])) target_angles = target_arm_angles self.control_joints(target_angles=target_angles) def ee_displacement_to_target_arm_angles( self, ee_displacement: np.ndarray ) -> np.ndarray: """Compute the target arm angles from the end-effector displacement. Args: ee_displacement (np.ndarray): End-effector displacement, as (dx, dy, dy). Returns: np.ndarray: Target arm angles, as the angles of the 7 arm joints. """ ee_displacement = ee_displacement[:3] * 0.05 # limit maximum change in position # get the current position and the target position ee_position = self.get_ee_position() target_ee_position = ee_position + ee_displacement # Clip the height target. For some reason, it has a great impact on learning target_ee_position[2] = np.max((0, target_ee_position[2])) # compute the new joint angles target_arm_angles = self.inverse_kinematics( link=self.ee_link, position=target_ee_position, orientation=np.array([1.0, 0.0, 0.0, 0.0]), ) target_arm_angles = target_arm_angles[:6] # remove fingers angles return target_arm_angles def arm_joint_ctrl_to_target_arm_angles( self, arm_joint_ctrl: np.ndarray ) -> np.ndarray: """Compute the target arm angles from the arm joint control. Args: arm_joint_ctrl (np.ndarray): Control of the 7 joints. Returns: np.ndarray: Target arm angles, as the angles of the 7 arm joints. """ arm_joint_ctrl = arm_joint_ctrl * 0.05 # limit maximum change in position # get the current position and the target position current_arm_joint_angles = np.array( [self.get_joint_angle(joint=i) for i in range(6)] ) target_arm_angles = current_arm_joint_angles + arm_joint_ctrl return target_arm_angles def get_obs(self) -> np.ndarray: # end-effector position and velocity ee_position = np.array(self.get_ee_position()) ee_velocity = np.array(self.get_ee_velocity()) # new add obs = np.concatenate((ee_position, ee_velocity)) # fingers opening # if not self.block_gripper: # fingers_width = self.get_fingers_width() # obs = np.concatenate((ee_position, ee_velocity, [fingers_width])) # else: # obs = np.concatenate((ee_position, ee_velocity)) return obs def reset(self) -> None: self.set_joint_neutral() def set_joint_neutral(self) -> None: """Set the robot to its neutral pose.""" self.set_joint_angles(self.neutral_joint_values) # def get_fingers_width(self) -> float: # """Get the distance between the fingers.""" # finger1 = self.sim.get_joint_angle(self.body_name, self.fingers_indices[0]) # finger2 = self.sim.get_joint_angle(self.body_name, self.fingers_indices[1]) # return finger1 + finger2 def get_ee_position(self) -> np.ndarray: """Returns the position of the ned-effector as (x, y, z)""" return self.get_link_position(self.ee_link) def get_ee_velocity(self) -> np.ndarray: """Returns the velocity of the end-effector as (vx, vy, vz)""" return self.get_link_velocity(self.ee_link) ``` 2. 在```/home/mickey/miniforge3/envs/panda-gym/lib/python3.6/site-packages/panda_gym/envs/panda_tasks```下添加z1的任务: ```python import numpy as np from panda_gym.envs.core import RobotTaskEnv from panda_gym.envs.robots.z1 import z1 from panda_gym.envs.tasks.reach import Reach from panda_gym.pybullet import PyBullet # import sys # sys.path.append("/home/mickey/mickey_sync/project/panda-gym/envs/robots") # from z1 import z1 # sys.path.append("/home/mickey/mickey_sync/project/panda-gym/envs/tasks") # from reach import Reach # import gym # from gym.wrappers import TimeLimit class Z1ReachEnv(RobotTaskEnv): """Reach task wih Panda robot. Args: render (bool, optional): Activate rendering. Defaults to False. reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". """ def __init__( self, render: bool = False, reward_type: str = "sparse", control_type: str = "ee", ) -> None: sim = PyBullet(render=render) robot = z1( sim, # block_gripper=True, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type, ) task = Reach( sim, reward_type=reward_type, get_ee_position=robot.get_ee_position ) super().__init__(robot, task) ``` 3. 在```/home/mickey/miniforge3/envs/panda-gym/lib/python3.6/site-packages/panda_gym/__init__.py```文件夹下注册环境 ```python /home/mickey/miniforge3/envs/panda-gym/lib/python3.6/site-packages/panda_gym/__init__.py中添加以下内容: register( id="Z1Reach{}{}-v1".format(control_suffix, reward_suffix), entry_point="panda_gym.envs:Z1ReachEnv", kwargs=kwargs, max_episode_steps=50, ) ``` 运行效果如下: [DDPG](https://www.bilibili.com/video/BV1oLCnYvEsU/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b),[TD3](https://www.bilibili.com/video/BV1iLCnYvEQH/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b),[SAC](https://www.bilibili.com/video/BV1iLCnYvE5Z/?vd_source=63856ba65bb4b008f5c3f3b0c598ac4b) * 需要解决的问题:改用Unitree z1机械臂的urdf文件后,强化学习训练效果成功率无法达到100%,只有90%左右,如下图: ![z1_tensorboard](./Picture/遥操作和模仿学习/z1_tensorboard.png) * 虽然TD3和DDPG算法的成功率比SAC算法高,但是DDPG和TD3都有较大抖动,SAC稳定性则较好,后续考虑重新重新学习一些新的强化学习算法用于训练机械臂,而不仅仅局限于stable-baseline3中提供的算法。 * 后续计划 * 整理当前代码环境,融合到pypi包中,并上传github,并跟导师商量看是否能发一篇论文。 * 使用当前平台进行机械臂模仿学习的仿真。 ---- ## TODO - [x] 导入大四轴模型(完成时间:2025.9.24) - [ ] 修改vehicles.py代码,实现python调用无人机起飞 - [ ] 研究如何通过ros发布轨迹信息实现大四轴在isaacsim中的运行 - [ ] 研究如何通过ros控制机械臂在isaacsim中的运行 - [ ] 研究模仿学习算法 ## 2025.9.16 在isaacsim中成功使无人机飞行,降落,详细配置方法: https://github.com/PegasusSimulator/PegasusSimulator ## 2025.9.23 由于PegasusSimulator集成了px4,所以考虑将大四轴模型先导入px4的gazebo模型路径下,然后通过python接口调用 将模型导入px4 gazebo模型路径下的操作: ```bash # px4 gazebo模型路径:/home/leona/sim_wt/PX4-Autopilot/Tools/simulation/gazebo-classic/sitl_gazebo-classic/models # 需要编写model.config文件,从现有的模板中复制粘贴一个,修改文件名即可 gedit model.config # 进入launch文件夹,路径:/home/leona/sim_wt/PX4-Autopilot/launch,编写新模型的launch文件,从现有的模版复制粘贴进行修改 gedit big_four.launch # 在px4中注册airframe配置 cd /home/leona/sim_wt/PX4-Autopilot/ROMFS/px4fmu_common/init.d-posix/airframes # 修改大四轴的参数配置,没有就新建一个 gedit 1112_gazebo-classic_big_four # 大四轴参数配置开始 #!/bin/sh # # @name Big Quadrotor 0904 # # @type Quadrotor X # @class Copter # # @output MAIN1 motor1 # @output MAIN2 motor2 # @output MAIN3 motor3 # @output MAIN4 motor4 # # @maintainer Your Name # . ${R}etc/init.d/rc.mc_defaults # 设置机型为多旋翼 param set-default CA_AIRFRAME 0 # 设置为四旋翼 param set-default CA_ROTOR_COUNT 4 # 电机1 (右前: CCW) param set-default CA_ROTOR0_PX 0.60093 param set-default CA_ROTOR0_PY 0.60093 param set-default CA_ROTOR0_KM 0.05 # 电机2 (右后: CW) param set-default CA_ROTOR1_PX -0.60093 param set-default CA_ROTOR1_PY -0.60093 param set-default CA_ROTOR1_KM 0.05 # 电机3 (左后: CCW) param set-default CA_ROTOR2_PX 0.60093 param set-default CA_ROTOR2_PY -0.60093 param set-default CA_ROTOR2_KM -0.05 # 电机4 (左前: CW) param set-default CA_ROTOR3_PX -0.60093 param set-default CA_ROTOR3_PY 0.60093 param set-default CA_ROTOR3_KM -0.05 # 设置PWM输出功能(将飞控输出映射到电机) param set-default PWM_MAIN_FUNC1 101 # 电机1 (MAIN1输出) param set-default PWM_MAIN_FUNC2 102 # 电机2 (MAIN2输出) param set-default PWM_MAIN_FUNC3 103 # 电机3 (MAIN3输出) param set-default PWM_MAIN_FUNC4 104 # 电机4 (MAIN4输出) # 大四轴参数配置结束 # 编辑同目录下的CmakeLists.txt文件 px4_add_romfs_airframe(1112_gazebo-classic_big_four) # 回到主目录,make编译 cd /home/leona/sim_wt/PX4-Autopilot make # 测试 roslaunch px4 big_four.launch ``` 可以在gazebo里打开,但是在isaacsim中调用报错,后续还应继续修改代码 ## 2025.9.24 本来以为pegasus simulator是把gazebo中的sdf文件导入isaacsim,结果搞了半天发现还是一样地用usd文件,其用的是它自身存储中的usd文件,路径:```/home/leona/mickey/PegasusSimulator/extensions/pegasus.simulator/pegasus/simulator/assets/Robots```,将之前的大四轴模型copy一份到该路径下,打开模型进行修改 * 在defaultPrim中右键,add—>physics—>articulation root,如果没有看到该选项,则在右下角的Property一栏查看是否已经添加,如果还是没有,就需要在其他xform处寻找是否已经添加过articulation root,找到并删除,重新在defaultPrim中添加,记住,**一定要在defaultPrim中添加** * 选中所有的link,右键,add—>physics—>Rigid Body with Colliders Preset(或者Colliders Preset) * 删除不必要的平面,以防无人机无法起飞 * 直接修改源代码```/home/leona/mickey/PegasusSimulator/examples/1_px4_single_vehicle.py```,添加以下代码,并把之前的相关代码注释掉 ```bash # ROBOTS = {"Iris": ROBOTS_ASSETS + "/Iris/iris.usd"} #, "Flying Cube": ROBOTS_ASSETS + "/iris_cube.usda"} # 大四轴 ROBOTS = {"big_four": ROBOTS_ASSETS + "/big_four/big_four.usd"} ``` 修改完之后,通过isaacsim中的pegasus simulator扩展load scene和load vehicle,可以成功使无人机起飞,但是飞行状态十分扭曲,应该是px4中的飞行参数给的不对。 通过代码调用: 复制```/home/leona/mickey/PegasusSimulator/examples/1_px4_single_vehicle.py```到project文件夹,修改其中代码: ```python mavlink_config = PX4MavlinkBackendConfig({ "vehicle_id": 0, "px4_autolaunch": True, "px4_dir": self.pg.px4_path, "px4_vehicle_model": 'big_four' # 修改 }) config_multirotor.backends = [PX4MavlinkBackend(mavlink_config)] Multirotor( "/World/quadrotor", ROBOTS['big_four'], # 修改 0, [0.0, 0.0, 0.07], Rotation.from_euler("XYZ", [0.0, 0.0, 0.0], degrees=True).as_quat(), config=config_multirotor, ) ``` 修改完以上代码后,可以通过python成功在isaacsim中打开大四轴,但是无法飞行,初步找到解决方法是修改```/home/leona/mickey/PegasusSimulator/extensions/pegasus.simulator/pegasus/simulator/logic/vehicles/vehicle.py```其中的代码,因为它的源代码中默认为机体名称为/body,而大四轴模型的机体坐标为/base_link,大致看了一下需要修改的地方还挺多,不太好改,明天继续。 # 相关文献阅读笔记 ## 1、[模仿学习简介教程(南京大学)](https://alist-org.github.io/pdf.js/web/viewer.html?file=http%3A%2F%2Flocalhost%3A5244%2Fp%2Fubuntu%2Fimportant%2F%25E5%25AD%25A6%25E4%25B9%25A0%25E8%25B5%2584%25E6%2596%2599%2F%25E9%2581%25A5%25E6%2593%258D%25E4%25BD%259C%25E5%2592%258C%25E6%25A8%25A1%25E4%25BB%25BF%25E5%25AD%25A6%25E4%25B9%25A0%2F%25E8%25AF%25BE%25E9%25A2%2598-%25E6%25A8%25A1%25E4%25BB%25BF%25E5%25AD%25A6%25E4%25B9%25A0%2Fbook%2F%25E6%25A8%25A1%25E4%25BB%25BF%25E5%25AD%25A6%25E4%25B9%25A0%25E7%25AE%2580%25E6%25B4%2581%25E6%2595%2599%25E7%25A8%258B.pdf%3Fsign%3DdGjS4dRV7MwXxawrJmM-Roue96WNE0W7UMvrYaHbBEs%3D%3A0) * **2.1.1 马尔可夫链相关笔记** [简述马尔可夫链.html](https://openlist.speedtest.mingqian.online/@s/xIc4DRmK/%E7%AE%80%E8%BF%B0%E9%A9%AC%E5%B0%94%E5%8F%AF%E5%A4%AB%E9%93%BE.html) * **2.1.2 马尔可夫决策过程** 原文:除此之外,我们用奖励函数 $r : S × A → R$ 来衡量某个状态-动作对的好坏。 注释: 1. **什么是 $S \times A$?** $s$ 是状态空间,表示系统可能处于的所有状态。$A$是动作空间,表示智能体在每个状态下可以采取的所有可能动作。$S \times A$表示所有状态-动作对的集合,形式上是“某个状态下采取某个动作”。比如:如果一个机器人在某个房间(状态)内选择往北走(动作),这就形成了一个状态-动作对$(s,a)$。 2. **什么是$r : S × A → R$ ?** 奖励函数$r$是一个从状态-动作对映射到实数的函数,它的意思是:你在某个状态$s$下做了一个动作$a$,会立刻获得一个数值型的奖励$r(s,a)$。这个奖励数值衡量了该行为的好坏:如果$r(s,a)$很高,说明这个动作在这个状态下是“好”的(系统鼓励这种行为)。如果$r(s,a)$很低(甚至为负),说明这种行为不受欢迎。