0%

本章使用pubullet完成常规机器人学相关计算,使用六自由度机械臂

配置与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import pybullet as p
import time
import pybullet_data
import numpy as np

physicsCilent = p.connect(p.DIRECT)

p.setAdditionalSearchPath(pybullet_data.getDataPath())
p.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 0)
planeId = p.loadURDF("plane.urdf")
robotId = p.loadURDF('E:/robo_prj/pybullet/examples/urdf/ur5.urdf',useFixedBase=True) #记得固定基座
p.setGravity(0, 0, -9.8)
p.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 1)

q0 = np.array([0, 0, 0, 0, 0, 0])
q1 = np.array([-1.5, -1.0, 1.0, -1.57, -1.57, -1.57])

controllable_joints = [i for i in range(p.getNumJoints(robotId)) if p.getJointInfo(robotId, i)[2] != p.JOINT_FIXED]

#################初始化角度##############################
zero_vec = [0.0] * len(controllable_joints)
kp = 1.0
kv = 1.0
p.setJointMotorControlArray(
robotId,
controllable_joints,
p.POSITION_CONTROL,
targetPositions=q1,
targetVelocities=zero_vec,
positionGains=[kp] * len(controllable_joints),
velocityGains=[kv] * len(controllable_joints)
)
for _ in range(100): # to settle the robot to its position
p.stepSimulation()

joint_state = p.getJointStates(robotId,controllable_joints)
joint_theta = [i[0] for i in joint_state]
joint_velocity = [i[1] for i in joint_state]
joint_torque = [i[3] for i in joint_state]
#########################################################

该部分代码完成了模型加载、重力设置、遍历可运动关节、设置当前角度为q1并计算关节角及其一阶二阶导

  • controllable_joints = [i for i in range(p.getNumJoints(robotId)) if p.getJointInfo(robotId, i)[2] != p.JOINT_FIXED]是常用的遍历可运动关节的操作
  • 对于当前情况,机器人是静止的,因此关节速度和加速度都为0

运动学

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
###############运动学正解#################################
link_state = p.getLinkState(robotId, controllable_joints[-1])
link_pos = link_state[0]
link_orn = link_state[1] #笛卡尔坐标姿态
link_R = np.array(p.getMatrixFromQuaternion(link_orn)).reshape(3, 3)
link_T = np.eye(4) #齐次变换矩阵
link_T[:3, :3] = link_R
link_T[:3, 3] = link_pos
print(f"q1下末端质心齐次矩阵为:{link_T}")
########################################################


###############运动学逆解#################################
joint_angle_solve = p.calculateInverseKinematics(robotId,
controllable_joints[-1],
targetPosition = link_pos,
targetOrientation = link_orn)
print(f"q1下运动学逆解为:{joint_angle_solve}")
########################################################
  • p.getLinkState会返回所查询关节对应子连杆的位姿等信息

    在pybullet中,

    1
    base → joint0 → link1 → joint1 → link2 → joint2 → link3

    对于joint2,其子连杆为link3,父连杆为link2

  • 默认的姿态表示是基于笛卡尔坐标系的,pybullet提供其与矩阵、欧拉坐标的转换api

雅可比矩阵相关

1
2
3
4
5
6
7
8
9
#################雅可比计算##############################
J_v, J_w = p.calculateJacobian(robotId,
controllable_joints[-1],
link_pos,
joint_theta,
zero_vec, zero_vec)
J = np.concatenate((np.asarray(J_v),np.asarray(J_w)),axis=0)
print(f"q1下末端的雅可比矩阵为{J}")
#######################################################

pybullet提供的雅可比函数默认分开返回线速度雅可比和角速度雅可比,需要输入所计算连杆的位置和所有关节的转角,关节速度和加速度默认0即可(形状需与机器人自由度DOF匹配)

机械臂控制(逆动力学)

大致流程:

  • 规划轨迹,得到q,qd,qdd
  • 逆动力学计算(pybullet使用牛顿欧拉法)
  • 控制电机

首先定义如下轨迹规划函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def getTrajectory(thi, thf, tf, dt):
desired_position, desired_velocity, desired_acceleration = [], [], []
t = 0
while t <= tf:
th = thi + ((thf - thi) / tf) * (t - (tf / (2 * np.pi)) * np.sin((2 * np.pi / tf) * t))
dth = ((thf - thi) / tf) * (1 - np.cos((2 * np.pi / tf) * t))
ddth = (2 * np.pi * (thf - thi) / (tf * tf)) * np.sin((2 * np.pi / tf) * t)
desired_position.append(th)
desired_velocity.append(dth)
desired_acceleration.append(ddth)
t += dt
desired_position = np.array(desired_position)
desired_velocity = np.array(desired_velocity)
desired_acceleration = np.array(desired_acceleration)
return desired_position, desired_velocity, desired_acceleration

该函数根据始末关节角规划n个关节空间点

位置控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sim_time = 2
dt = 1e-3
q,qd,qdd = getTrajectory(q0,q1,tf=sim_time,dt=dt)

#不设置阻尼
for link_idx in range(9):
p.changeDynamics(robotId, link_idx, linearDamping=0.0, angularDamping=0.0, jointDamping=0.0)
p.changeDynamics(robotId, link_idx, maxJointVelocity=200)


n = 0
kp = 1
kv = 1
while n < q.shape[0]:
#位置控制
p.setJointMotorControlArray(robotId,controllable_joints,p.POSITION_CONTROL,
targetPositions = list(q[n]),
targetVelocities = list(qd[n]),
positionGains=[kp] * len(controllable_joints),
velocityGains=[kv] * len(controllable_joints)
)
p.stepSimulation()
print(tau)
time.sleep(dt)
n += 1
p.disconnect()

查阅官方手册可知,输入模式为p.POSITION_CONTROL时,误差函数如图:

因此,为达到最佳控制效果,我们最好额外输入角速度项,也就是qd

速度控制

只需将控制电机函数改为:

1
2
3
4
p.setJointMotorControlArray(robotId,controllable_joints,p.VELOCITY_CONTROL,
targetVelocities = list(qd[n]),
forces = [200]*len(controllable_joints),
)

由误差函数可知,单速度项控制非常简单,因此其效果也不好,一般在机械臂中不使用

力控制

简单版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
p.setPhysicsEngineParameter(fixedTimeStep=dt, numSolverIterations=100, numSubSteps=10)
#重要,此处fixedTimeStep应与getTrajectory和p.setTimeStep(dt)一致!!!!
...
...
...
#初始化力控制
p.setJointMotorControlArray(robotId,controllable_joints,p.VELOCITY_CONTROL,
forces = [0]*len(controllable_joints))
n = 0
while n < q.shape[0]:

tau = p.calculateInverseDynamics(robotId,list(q[n]),list(qd[n]),list(qdd[n]))
p.setJointMotorControlArray(robotId,controllable_joints,p.TORQUE_CONTROL,
forces = tau,
)
p.stepSimulation()
print(tau)
time.sleep(dt)
n += 1
p.disconnect()

如果在此之前使用过位置控制,需要先进行一次初始化力控制(不知道为什么),然后就是一定要注意在创建环境时使用如下代码保证物理引擎计算稳定性:

1
2
p.setTimeStep(dt)
p.setPhysicsEngineParameter(fixedTimeStep=dt, numSolverIterations=100, numSubSteps=10)

带反馈版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
while n < q.shape[0]:
joint_states = p.getJointStates(robotId,controllable_joints)
q_actual = np.array([state[0] for state in joint_states])
qd_actual = np.array([state[1] for state in joint_states])


q_e = q[n] - q_actual
qd_e = qd[n] - qd_actual

aq = qdd[n] + 400 * q_e + 40 * qd_e

tau = p.calculateInverseDynamics(robotId,list(q_actual),list(qd_actual),list(aq))
p.setJointMotorControlArray(robotId,controllable_joints,p.TORQUE_CONTROL,
forces = tau,
)
p.stepSimulation()
print(tau)
time.sleep(dt)
n += 1

实际上,对于逆动力学或者p.calculateInverseDynamics函数来说,我们在构建动力学方程时,都应该使用系统当前位置、当前速度和期望加速度,因为在机器人控制系统中,加速度项才是真正的唯一前馈项,而要构建动力学方程中的M,C,G矩阵则需要当前系统的位置和速度(即q_actual,qd_actual决定动力学方程内容,aq根据当前动力学方程输出在我们期望的加速度下对应的关节力,以车为例,即我们要控制车速,最终只能通过改变加速度来控制,而要达到期望加速度,我们就需要构建动力学方程将期望加速度映射到电机输出力上)

因此我们若要引入反馈控制,也应该仅对期望加速度项进行调整,因此真正的动力学方程才是:

1
tau = p.calculateInverseDynamics(robotId,list(q_actual),list(qd_actual),list(aq))

来源:(PyBullet笔记(二)从hello world开始的杂谈(引擎连接,URDF模型加载,查看信息) - 知乎,开源好人一生平安!

连接引擎

使用pybullet的第一件事就是连接物理引擎,整个pybullet的结构可以理解为客户端和服务端,客户端发送指令,服务端来执行。为了让我们在客户端编写的脚本能够被解释,并在物理引擎运行整个环境,需要使用pybullet的connect方法。

1
2
3
4
5
6
import pybullet as p
import time
import pybullet_data

# 连接物理引擎
physicsCilent = p.connect(p.GUI)

connect函数接受一个参数,代表用户选择连接的物理引擎服务器(physics server),可选的有pybullet.GUIpybullet.DIRECT ,返回一个数字代表服务器的ID。这两个物理引擎执行的内容,返回的结果等方面完全一致,唯一区别是,GUI可以实时渲染场景到gui上,而DIRECT则不会且不允许用户调用内置的渲染器,(即不进行画面渲染等可视化,在RL训练时,这些不必要操作会使训练变慢)也不允许用户调用外部的openGL,VR之类的硬件特征。

关闭服务器(引擎)

与gym操作相同,存在一个断开与服务器连接的函数:p.disconnect()

调试配置

ui界面配置

在使用GUI引擎时,可以通过一些操作对渲染ui界面进行配置(如不显示界面的控件、禁用cpu核显渲染等)这些操作通常都通过p.configureDebugVisualizer(..., 0 or 1)实现,例如:

1
2
p.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)
p.configureDebugVisualizer(p.COV_ENABLE_TINY_RENDERER, 0)

绘制辅助线

1
2
3
4
5
6
7
8
9
froms = [[1, 1, 0], [-1, 1, 0], [-1, 1, 3], [1, 1, 3]]
tos = [[-1, 1, 0], [-1, 1, 3], [1, 1, 3], [1, 1, 0]]
for f, t in zip(froms, tos):
p.addUserDebugLine(
lineFromXYZ=f,
lineToXYZ=t,
lineColorRGB=[0, 1, 0],
lineWidth=2
)

添加文字

1
2
3
4
5
6
p.addUserDebugText(
text="Destination",
textPosition=[0, 1, 3],
textColorRGB=[0, 1, 0],
textSize=1.2,
)

添加控件

此处为在debug界面添加机器人某关节的运动控制,p.addUserDebugParameter会返回控件id,用p.readUserDebugParameter即可读取该id对应具体值

注:当rangeMin>rangeMax时,控件会从默认滑块变为按钮,按按钮的次数会放反映为累加值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
v_id = p.addUserDebugParameter(
paramName="V",
rangeMin=-50,
rangeMax=50,
startValue=0
)
f_id = p.addUserDebugParameter(
paramName="F",
rangeMin=-10,
rangeMax=10,
startValue=0
)

p.setJointMotorControl2( bodyUniqueId=robot_id,
jointIndices=joint_id,
controlMode=p.VELOCITY_CONTROL,
targetVelocities=p.readUserDebugParameter(v_id),
forces=p.readUserDebugParameter(f_id))

添加合成摄像机视角

使用p.getCameraImage获取ui界面左侧三个窗口的视角,不具体展开了,用到了再说

移除debug配置

  • removeAllUserParameters:移除所有的滑块和按钮类控件。
  • removeUserDebugItem:接受一个代表debug textdebug line的ID,并移除该ID的debug text或者debug line
  • removeAllUserDebugItems:移除所有的debug text和debug line

获取键盘、鼠标事件

  • getKeyboardEvents:默认无输入即可,能够返回一个字典,字典中为当前时刻被按下去的按键的ID(key)以及它的状态(value)。其中,一般的按键的ID(key)就是它的小写字母Unicode码,而value则固定为三种状态:KEY_IS_DOWN, KEY_WAS_TRIGGERED 和 KEY_WAS_RELEASED。KEY_WAS_TRIGGERED 会在该按键刚刚被按下去后触发,并将按钮状态设为KEY_IS_DOWN;只要按键被一直按着,那么KEY_IS_DOWN就会一直触发;按钮松开,KEY_WAS_RELEASED会被触发。

    当无键盘事件时,该函数返回空字典(也就是说当有按键触发时,返回一个“按键”:p.KEY_WAS_TRIGGERED

    对于特殊按键的key,如下:

  • getMouseEvents:获取鼠标事件,包括移动和点击,具体用到了再展开

说明 按键ID常量
F1到F12 B3G_F1 … B3G_F12
上下左右方向键 B3G_LEFT_ARROW, B3G_RIGHT_ARROW, B3G_UP_ARROW, B3G_DOWN_ARROW
同一页向上/下,页尾,起始页 B3G_PAGE_UP, B3G_PAGE_DOWN, B3G_PAGE_END, B3G_HOME
删除,插入,Alt,Shift,Ctrl,Enter,Backspace,空格 B3G_DELETE, B3G_INSERT, B3G_ALT, B3G_SHIFT, B3G_CONTROL, B3G_RETURN, B3G_BACKSPACE, B3G_SPACE

加载模型

直接加载urdf模型

1
2
3
4
5
6
7
8
9
10
# 设置环境重力加速度
p.setGravity(0, 0, -10)

# 加载URDF模型,此处是加载蓝白相间的陆地
planeId = p.loadURDF("plane.urdf")

# 加载机器人,并设置加载的机器人的位姿
startPos = [0, 0, 1]
startOrientation = p.getQuaternionFromEuler([0, 0, 0])
boxId = p.loadURDF("r2d2.urdf", startPos, startOrientation)

pybullet提供了非常方便的函数loadURDF来加载外部的urdf文件,返回值是创建的模型对象的ID(每个加载的模型在服务器中都使用唯一的ID),接受的参数有8个,只有第一个是必填参数(urdf文件绝对路径),第二个参数为机器人起始位置,第三个为起始姿态,对于姿态,可以调用p.getQuaternionFromEuler使用欧拉角定义其姿态

另外,也可以使用p.resetBasePositionAndOrientation重置位姿

通过3D文件创建模型

createVisualShape负责创建视觉模型,createCollisionShape负责创建碰撞箱模型,而createMultiBody则是负责将视觉模型和碰撞箱模型整合在一起形成一个完整的物理模型对象,并可以加入一些额外的参数,比如质量,转动惯量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 创建过程中不渲染
p.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 0)

# 创建视觉模型和碰撞箱模型时共用的两个参数
shift = [0, -0.02, 0]
scale = [1, 1, 1]

# 创建视觉形状
visual_shape_id = p.createVisualShape(
shapeType=p.GEOM_MESH,
fileName="duck.obj",
rgbaColor=[1, 1, 1, 1],
specularColor=[0.4, 0.4, 0],
visualFramePosition=shift,
meshScale=scale
)

#碰撞模型
collision_shape_id = p.createCollisionShape(
shapeType=p.GEOM_MESH,
fileName="duck_vhacd.obj",
collisionFramePosition=shift,
meshScale=scale
)

#使用createMultiBody将两者结合在一起
p.createMultiBody(
baseMass=1,
baseCollisionShapeIndex=collision_shape_id,
baseVisualShapeIndex=visual_shape_id,
basePosition=[0, 0, 2],
useMaximalCoordinates=True
)

# 创建结束,重新开启渲染
p.configureDebugVisualizer(p.COV_ENABLE_RENDERING, 1)

对于创建的所有模型id,均可重复调用,因此可以多次使用p.createMultiBody创建多个一样的鸭子

开始模拟

步进模拟

1
2
3
4
# 开始一千次迭代,也就是一千次交互,每次交互后停顿1/240
for i in range(1000):
p.stepSimulation()
time.sleep(1 / 240)

利用正向动力学进行步进模拟。由于计算中模拟物理过程还是离散得模拟的,因此,使用stepSimulation可以看成是进行一次迭代步。可以理解为gym中的env.step(action),使用time.sleep可以方便观察

实时模拟

setRealTimeSimulation函数直接将物理引擎渲染的时间和RTC(real time clock)同步,这样做,就不需要使用stepSimualtion显式地执行模拟步了。引擎会根据RTC自动执行模拟步。这对于实时展示很有利:

1
2
3
4
p.setRealTimeSimulation(1)
p.setTimeStep(1/240)
while 1:
pass

查看机器人信息

位姿查看

1
2
# 获取位置与方向四元数
cubePos, cubeOrn = p.getBasePositionAndOrientation(boxId)

关节信息查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
joint_num = p.getNumJoints(robot_id)
print("r2d2的节点数量为:", joint_num)

print("r2d2的信息:")
for joint_index in range(joint_num):
info_tuple = p.getJointInfo(robot_id, joint_index)
print(f"关节序号:{info_tuple[0]}\n\
关节名称:{info_tuple[1]}\n\
关节类型:{info_tuple[2]}\n\ #4表示该关节为固定关节
机器人第一个位置的变量索引:{info_tuple[3]}\n\
机器人第一个速度的变量索引:{info_tuple[4]}\n\
保留参数:{info_tuple[5]}\n\
关节的阻尼大小:{info_tuple[6]}\n\
关节的摩擦系数:{info_tuple[7]}\n\
slider和revolute(hinge)类型的位移最小值:{info_tuple[8]}\n\
slider和revolute(hinge)类型的位移最大值:{info_tuple[9]}\n\
关节驱动的最大值:{info_tuple[10]}\n\
关节的最大速度:{info_tuple[11]}\n\
节点名称:{info_tuple[12]}\n\
局部框架中的关节轴系:{info_tuple[13]}\n\
父节点frame的关节位置:{info_tuple[14]}\n\
父节点frame的关节方向:{info_tuple[15]}\n\
父节点的索引,若是基座返回-1:{info_tuple[16]}\n\n")

p.getNumJoints返回关节数量,p.getJointInfo返回指定关节的信息

控制关节电机

pybullet中控制机器人关节电机的API主要有两个:setJointMotorControl2setJointMotorControlArray,这两个的用法基本一样,不同的是前者调用一次只能设置一台关节电机的参数,后者调用一次则可以设置一组关节电机的参数。

  • setJointMotorControl2有三个必选参数:(setJointMotorControlArray将对应参数替换为列表)
    • 被控机器人id
    • 被控关节id(即关节序号索引info_tuple[0])
    • 控制模式(可选POSITION_CONTROL, VELOCITY_CONTROL, TORQUE_CONTROL and PD_CONTROL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import pybullet as p
import time
import pybullet_data

# 连接物理引擎
physicsCilent = p.connect(p.GUI)

# 添加资源路径
p.setAdditionalSearchPath(pybullet_data.getDataPath())

# 设置环境重力加速度
p.setGravity(0, 0, -10)

# 加载URDF模型,此处是加载蓝白相间的陆地
planeId = p.loadURDF("plane.urdf")
plane_Pos,_ = p.getBasePositionAndOrientation(planeId)
# 加载机器人,并设置加载的机器人的位姿
startPos = [0, 0, plane_Pos[2]+0.5]
startOrientation = p.getQuaternionFromEuler([0, 0, 0])
boxId = p.loadURDF("r2d2.urdf", startPos, startOrientation)

available_joints_indexes = [i for i in range(p.getNumJoints(boxId)) if p.getJointInfo(boxId, i)[2] != p.JOINT_FIXED]
wheel_joints_indexes = [i for i in available_joints_indexes if "wheel" in str(p.getJointInfo(boxId, i)[1])]

target_v = 10 # 电机达到的预定角速度(rad/s)
max_force = 10 # 电机能够提供的力,这个值决定了机器人运动时的加速度,想禁用电机给0即可


for i in range(1000):
p.stepSimulation()
p.setJointMotorControlArray(
bodyUniqueId=boxId,
jointIndices=wheel_joints_indexes,
controlMode=p.VELOCITY_CONTROL,
targetVelocities=[target_v for _ in wheel_joints_indexes],
forces=[max_force for _ in wheel_joints_indexes]
)
time.sleep(1 / 240) # 模拟器一秒模拟迭代240步


# 断开连接
p.disconnect()

需要注意的是,当需要电机反转时,应该设定速度为负值,而力仍为正值

相机跟踪

p.resetDebugVisualizerCamera通过设置相机欧拉角,和实时坐标,可以使其跟踪机器人一起运动:

1
2
3
4
5
6
7
location, _ = p.getBasePositionAndOrientation(boxId)
p.resetDebugVisualizerCamera(
cameraDistance=3,
cameraYaw=110,
cameraPitch=-30,
cameraTargetPosition=location
)

如果想实现更复杂的跟踪(如机器人第一视角),可以结合p.getJointState(),p.getLinkState()通过获取多种坐标实现

状态保存与加载

  • saveState:将目前模拟器的状态保存到内存中,让这段程序后面可以随时读取内存中的这个模拟器状态,然后载入这个存档,因此saveState只需要指定模拟器环境ID,返回一个状态ID
  • saveBullet:将状态保存到磁盘上,需要接受模拟器ID和路径
  • restoreState:读取状态,上述两种都用此方法读,具体用法搜官方文档

碰撞检测

为了简化碰撞检测,通常使用规则的几何物体代替机器人实际碰撞模型用于检测碰撞,在pybullet中,则使用AABB包围盒作为一个碰撞检测的长方体。AABB包围盒的各条边都与坐标轴平行,那么我们只需要选取两个位于体对角线上的点就可以确定这个长方体,并且当一个点位于这两点之间时,则可以视作发生碰撞

  • getAABB:默认返回基于世界坐标系的,指定机器人的AABB对角点(两个tuple)

  • getOverlappingObjects:输入AABB对角点,返回与这个AABB模型发生碰撞的模型id和对应具体link的id

    注:其返回的是n个二元tuple,当未发生碰撞时,始终返回((1,-1),),代表模型自己与自己重合,基于这个机制,可以通过以下方法判断是否碰撞:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    while True:
    p.stepSimulation()
    P_min, P_max = p.getAABB(robot_id)
    id_tuple = p.getOverlappingObjects(P_min, P_max)
    if len(id_tuple) > 1:
    for ID, _ in id_tuple:
    if ID == robot_id:
    continue
    else:
    print(f"hit happen! hit object is {p.getBodyInfo(ID)}")
    sleep(1 / 240)

其余相关函数:

  • getContactPoints:返回与一个物体接触的所有接触点
  • getClosestPoints:返回两个物体距离最近的点
  • setCollisionFilterGroupMask:忽略模型与模型之间碰撞
  • setCollisionFilterPair:忽略关节之间碰撞

强化学习概念

强化学习是一种解决控制任务(也称为决策问题)的框架,通过构建智能体,这些智能体通过与环境互动、试错并接收奖励(正面或负面)作为独特的反馈来从环境中学习。

强化学习框架

在下图中,以机器人为例,agent则为机器人的控制中心,environment则可以为机器人的各个关节电机

当控制中心(agent)得到关节电机(environment)0时刻的角度(state)时,会控制其进行转动(action),在转动过后,t1时刻会产生一个新的角度,如果该转角达到了预期,则会产生一个奖励(reward)

因此,强化学习的目标应该是最大化累积奖励,称为预期回报的最大化

状态和观测空间

  • 状态(state)是对世界状态的完整描述,没有隐藏信息
  • 观测(observation)是对状态的部分描述

行动空间

行动空间是环境中所有可能行动(action)的合集,分为离散和连续行动。

  • 离散空间:可能的行动数量有限(例如游戏的移动只有上左下右)
  • 连续空间:可能的行动数量无限(例如汽车的移动方向)

奖励与折扣

强化学习中的唯一反馈是累积奖励,而在较早时间步上奖励更有可能发生,为了表达对不同时间步奖励的关心程度,在积累奖励中引入折扣率gamma:

gamma大多数情况介于0.95与0.99之间,越大代表折扣越小,即更关心长期奖励,反之成立。

任务类型

  • 情景式任务:存在起始与终结点的任务
  • 持续式任务:没有终止状态的任务

探索与利用

  • 利用:利用已知信息来最大化奖励
  • 探索:通过随机行动探索环境,获取更多的信息

简单来说,探索就是风险更大的获取奖励的方式,相比利用,可能获得更大奖励,但也可能获得更大惩罚(负奖励值),因此,必须定义一个有助于权衡二者的规则

强化学习的目标

我们需要得到一个函数,该函数在得到当前环境状态(state)时,会给出最优的行动(action)

  • 基于策略的方法:直接学习一个策略函数,该函数将定义每个状态到最佳动作的直接映射(或概率分布)
  • 基于价值的方法:学习一个价值函数,该函数将每个状态映射到对应的一个预期价值,因此行动策略即“走向价值最高的状态”

Gymnasium

Gymnasium是一个用于强化学习创建环境的库

Gymnasium 的核心是 Env,一个表示强化学习理论中马尔可夫决策过程(MDP)的高级 Python 类(注意:这不是一个完美的重构,缺少 MDP 的几个组件)。该类为用户提供了开始新情节、采取行动和可视化智能体当前状态的能力。

1
2
3
4
import gymnasium as gym

env = gym.make('CartPole-v1') #通过调用make函数返回一个env类,此处为倒立摆环境
observation, info = env.reset() #初始化环境

上述代码创建了一个倒立摆模型,现在我们可以通过env.step()对环境执行动作(action):

1
2
3
4
5
6
while not episode_over:
#随机动作,对于倒立摆来说,其动作空间只有左0或右1,也就是说其动作空间为离散空间
action = env.action_space.sample()
observation, reward, terminated, truncated, info = env.step(action)
total_reward += reward
episode_over = terminated or truncated

对于step的返回如下,对于每个环境,其返回的各元素具体内容都不同:

  • observation:新状态 (st+1),取决于环境,对于倒立摆,其为(4,),分别为小车位置、速度、杆角度、角速度
  • reward:执行动作后获得的奖励
  • terminated:指奖励是否到达阈值
  • truncated:指环境是否因超出边界而结束,对于倒立摆,杆角度过大、小车位置超出屏幕或时间到达上限都会结束
  • info:一个提供额外信息的字典(取决于环境)。

月球车实践

首先创建对应环境,对于月球车,其奖励方案较为复杂,具体可查阅月球着陆器 - Gymnasium 文档 - Gymnasium 文档

1
2
3
4
5
6
7
8
import gymnasium as gym

lunar = gym.make("LunarLander-v3")
lunar.reset()
print("Observation Space Shape", lunar.observation_space.shape) #环境形状
print("Sample observation", lunar.observation_space.sample())#随机取一个环境观测值
print("Action Space Shape", lunar.action_space) #动作空间形状
print("Action Space Sample", lunar.action_space.sample()) #随机取动作空间

现在我们已经有了环境、可执行的动作以及奖励,就差引入agent进行强化学习,这里我们引入stable_baselines3库中的PPO深度强化学习方法:

1
2
3
4
model = sb3.PPO('MlpPolicy', lunar, verbose=1,device='cpu')
#PPO算法在cpu的表现上更好
model.learn(total_timesteps=1000000)
model.save("ppo")

对于强化学习,其总学习时长不再由epoch决定,有以下几个关键的训练参数:

  • total_timesteps:指整个训练过程中模型一共会与环境交互多少个时间步

  • n_step:每个rollout的长度,可以将其理解为训练数据集的大小,只不过在强化学习中数据集在不断更新,数据集更新次数为total_timesteps\n_step

    (注:当在rollout的过程中如果环境终止而时间步未达到n_step,则会立即reset环境继续采样)

  • batchsize:每次梯度更新的数据长度,即将n_step切分为n_step/batchsize份,与常规深度学习同理

  • n_epoch:对于每个数据集,其都会重复利用n_epoch次,但每次数据集都会被打乱

综上所述,深度强化学习的训练相比于一般深度学习,多了一个类似“更新数据集的操作”(即rollout),除此之外其他的操作是类似的

由于笔者暂时未接触其他强化学习算法,因此以上结论均只针对PPO

可视化

在训练完成后,我们可以通过动画观察模型训练效果:(设置render_mode="human"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import gymnasium as gym
from stable_baselines3 import PPO

env = gym.make("LunarLander-v3", render_mode="human")
model = PPO.load("ppo", env=env,device="cpu")

obs, info = env.reset()
total_reward = 0

for _ in range(1000): #随便取的值,可能会执行多次episode
action, _ = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, info = env.step(action)
total_reward += reward
if terminated or truncated:
obs, info = env.reset()
print(f"episode reward:{total_reward}")
total_reward = 0
env.close()