
最优化理论与自动驾驶(四):iLQR原理、公式及代码演示
我们考虑一个离散时间的动态系统,其状态方程为:其中,是系统在时间步k的状态,是控制输入,描述系统的动态方程。我们的目标是找到控制序列其中,是阶段成本函数,是终端成本。由于LQR算法要求系统的状态方程为线性状态方程,成本函数为二次型,因此iLQR是通过牛顿高斯方法进行迭代逼近最优解。为了方便优化和递推,我们引入值函数,它表示从时刻k开始,给定当前状态,剩余时间内的最小代价。其定义如下:值函数依赖于当
(二)iLQR原理、公式推导以及代码演示
1. iLQR 原理
iLQR(迭代线性二次调节器)是一种用于求解非线性系统最优控制问题的算法。我们在第一部分已经介绍了LQR相关的内容,iLQR其实就是LQR的迭代版本。它在每个时间步通过一阶泰勒展开线性化系统状态方程,并对成本函数进行二阶近似,然后使用线性二次调节器(LQR)求解该近似问题。通过迭代调整策略,逐步逼近最优策略。iLQR属于微分动态规划(DDP)类算法的简化版本,特别适用于解决状态空间和控制输入较大的问题。
2. 应用场景
iLQR 应用于解决许多复杂的非线性系统优化控制问题,尤其是在机器人控制、轨迹规划、自主驾驶、航天器姿态控制等领域。它通常用于以下场景:
- 轨迹跟踪:机器人、无人机、车辆的轨迹规划与控制。
- 姿态控制:如航天器的姿态调整,精确控制其方向与姿态。
- 动态系统优化:如机械臂在复杂任务中的路径优化。
3. iLQR 问题定义
我们考虑一个离散时间的动态系统,其状态方程为:
其中,是系统在时间步k的状态,
是控制输入,
描述系统的动态方程。我们的目标是找到控制序列
最小化以下成本函数:
其中,是阶段成本函数,
是终端成本。由于LQR算法要求系统的状态方程为线性状态方程,成本函数为二次型,因此iLQR是通过牛顿高斯方法进行迭代逼近最优解。
4. iLQR 公式推导
1. 值函数的定义
为了方便优化和递推,我们引入值函数,它表示从时刻k开始,给定当前状态
,剩余时间内的最小代价。其定义如下:
值函数依赖于当前状态
,并反映了从当前状态到终端状态所需支付的最小累积代价。终端时刻的值函数为:
2. 值函数的贝尔曼递推方程
在动态规划(Dynamic Programming)中,值函数满足贝尔曼方程。贝尔曼方程是通过递推的方式,将问题从一个长时间的优化问题简化为一步步的局部优化问题。对于k时刻的值函数,我们有:
其中,。也就是说,值函数
是当前阶段成本
和未来值函数
之和的最小值。
3. 一阶近似与线性化
在iLQR算法中,核心思想是将非线性的动力学方程和成本函数
线性化和二次近似化。首先对
进行一阶泰勒展开,得到其线性化近似:
这里,和
分别是状态和控制输入的偏差,
和
是系统动力学方程的雅可比矩阵,分别表示状态和控制输入对下一时刻状态的影响。
同时,代价函数也进行二阶泰勒展开:
其中,,
,
,
,
是代价函数的梯度和海森矩阵。
4. Q函数定义与展开
为了简化推导,引入 Q 函数,它表示从时刻k开始,给定当前状态
和控制输入
时,立即执行该控制并在未来按照最优策略执行时的总成本。Q 函数定义为:
我们将对
和
进行二阶泰勒展开:
(式子1)
其中:
是Q函数的梯度和海森矩阵。
由于是由瞬时代价和未来的值函数组成的,因此我们可以分别对
和
的梯度和海森矩阵进行计算。具体来说:
5. 值函数递推公式
现在我们要根据 Q 函数来递推值函数。根据贝尔曼方程,值函数是对控制输入
优化后得到的最小代价,因此:
由于是二次形式的,这里不是假设,而是由于我们对系统的动力学和代价函数都进行了线性化和二次近似的必然结果。最优控制输入
可以通过对(式子1)关于
求导得到:
解得最优控制偏差:
将这个解代入,得到值函数
的递推公式。首先我们展开
:
现在将最优控制输入的表达式代入上式中:
5.1 值函数的梯度递推
为了得到的梯度
,我们只需要对上式中所有与
线性相关的项进行提取。注意到:
这个表达式说明当前时刻的值函数梯度是基于 Q 函数的状态梯度
和控制输入对状态的混合项
以及控制输入对输入的海森矩阵
来递推的。
5.2 值函数的海森矩阵递推
接下来我们推导值函数的海森矩阵
。注意到
的二次项为:
因此,值函数的海森矩阵递推公式为:
这个公式表示当前时刻值函数的海森矩阵是基于 Q 函数的状态二阶导数
和状态-控制混合导数
,并且通过控制输入的海森矩阵
进行修正。
6. 最优控制律的推导
根据我们推导的结果,最优控制输入的偏差为:
这意味着最优的控制律具有线性反馈形式:
其中:
是开环控制增量;
是状态反馈增益矩阵。
这两个矩阵描述了如何根据当前的状态偏差来调整控制输入,从而优化整个轨迹。有了新的轨迹后,在新轨迹附近再次进行近似优化,直到达到特定的阈值条件(如控制量变化小于阈值、代价函数变化量小于阈值或者达到最大迭代次数)停止本轮迭代。
7. 递推过程总结
后向传递 (Backward Pass):递推计算值函数的梯度和海森矩阵,进而得到新的最优控制律。
前向传递 (Forward Pass):利用新的控制律,更新控制输入和状态轨迹。
迭代更新:检查控制输入和代价函数的变化,如果满足收敛条件,则停止迭代,否则继续下一轮迭代。
1. 后向传递 (Backward Pass)
在后向传递过程中,从终端时刻N开始,递推计算值函数的梯度和海森矩阵
,并通过这些值函数信息来更新反馈增益和开环控制增量。
1.1 递推计算值函数
值函数的递推公式为:
- 梯度递推公式:
- 海森矩阵递推公式:
这些公式是通过 Q 函数对状态和控制
的二次展开推导得到的,反映了如何通过当前时刻的 Q 函数信息来更新值函数。
1.2 计算最优控制律
最优控制律的反馈形式为:
这个公式表示最优控制输入通过 Q 函数的梯度和海森矩阵来递推计算。
1.3 计算反馈增益和开环控制增量
最优控制律可以写成开环和闭环反馈的形式:
其中:
- 开环控制增量:
- 状态反馈增益:
通过这个反馈形式,后向传递完成后我们得到了更新控制输入所需的反馈增益和开环增量
。
2. 前向传递 (Forward Pass)
在前向传递过程中,利用后向传递得到的最优控制律,更新控制输入并通过系统的动力学方程更新状态轨迹。
2.1 更新控制输入
通过后向传递得到的反馈增益和开环控制增量
,我们可以更新控制输入:
其中:
是上一次迭代的控制输入。
是状态偏差。
2.2 更新状态轨迹
在得到更新后的控制输入后,我们利用系统的动力学方程
来更新状态轨迹:
这样,前向传递就会生成一条新的状态轨迹,该状态轨迹对应于新的控制输入序列
。
3. 迭代更新
3.1 收敛条件
在每次前向传递后,需要检查控制输入的变化或代价函数的变化,以判断是否继续迭代。常见的收敛条件包括:
控制输入的变化:检查两次迭代间控制输入的变化是否很小:
如果变化很小,说明控制输入已经趋于收敛。
代价函数的变化:计算新的代价函数,并比较与上一次迭代的代价函数
的差值:
如果代价函数的变化很小,则认为算法已经收敛。
最大迭代次数:在实际应用中,还可以设定一个最大迭代次数,当迭代次数超过这个值时,算法也会停止。
3.2 继续下一次迭代
如果收敛条件未满足,则继续迭代:
- 将新的状态和控制输入作为参考轨迹
,并再次进行后向传递,递推计算新的最优控制律。
- 利用新的控制律再次进行前向传递,生成新的状态轨迹和控制输入。
通过这种循环迭代,iLQR 逐步优化控制序列,直到找到最优解。
8. 注意事项
初始猜测:iLQR 需要一个初始的控制输入序列作为起点,初始猜测越好,收敛越快。
线性化近似:由于每次迭代使用一阶泰勒展开线性化系统动态,系统的非线性程度较高时,近似效果会较差,可能需要更多迭代。
二阶成本近似:成本函数的二次化近似也会影响最终的解的质量,尤其是在局部极小点附近。
收敛性问题:iLQR 的收敛性较强,但在某些高度非线性问题中,可能会陷入局部最优,或者在初始化较差时不收敛。
9. python代码示例
与上一章节相同,我们用一个简单的非线性系统,其状态和控制输入
满足以下非线性动力学方程:
,这里的非线性在于控制输入的影响是通过正弦函数进入系统的。我们希望通过控制输入
使状态
快速收敛到目标值 0,同时控制量尽可能小,因此二次成本函数:
。
python代码如下:
import numpy as np
# System dynamics (nonlinear)
def system_dynamics(x, u):
return x + np.sin(u)
# Cost function for a single step
def cost_function(x, u):
return 0.5 * (x**2 + u**2)
# Derivative of the cost function w.r.t. control input u (l_u)
def cost_u(u):
return u
# Derivative of the cost function w.r.t. state x (l_x)
def cost_x(x):
return x
# Second derivative of the cost function w.r.t. control input u (l_uu)
def cost_uu():
return 1
# Second derivative of the cost function w.r.t. state x (l_xx)
def cost_xx():
return 1
# Function to calculate the initial state trajectory based on control sequence
def compute_initial_trajectory(x0, u):
x = np.zeros(N+1)
x[0] = x0
for k in range(N):
x[k+1] = system_dynamics(x[k], u[k])
return x
# iLQR algorithm with different stopping conditions
def ilqr_with_conditions(x, u, iterations, epsilon_u, epsilon_J, epsilon_x):
prev_cost = np.inf
for i in range(iterations):
# Backward pass
V_x = np.zeros(N+1)
V_xx = np.zeros(N+1)
V_x[-1] = x[-1] # Terminal value for V_x
V_xx[-1] = 1 # Terminal value for V_xx (quadratic cost on terminal state)
du = np.zeros(N) # Control updates
# Backward pass: compute Q function and control update
for k in range(N-1, -1, -1):
# Compute Q-function terms
f_u = np.cos(u[k]) # Derivative of system dynamics w.r.t. u
Q_u = cost_u(u[k]) + f_u * V_x[k+1] # Q_u = l_u + f_u^T * V_x(k+1)
Q_uu = cost_uu() + f_u**2 * V_xx[k+1] # Q_uu = l_uu + f_u^T * V_xx(k+1) * f_u
Q_x = cost_x(x[k]) + V_x[k+1] # Q_x = l_x + f_x^T * V_x(k+1)
Q_xx = cost_xx() + V_xx[k+1] # Q_xx = l_xx + f_x^T * V_xx(k+1) * f_x
# Update control input
du[k] = -Q_u / Q_uu # Control update
V_x[k] = Q_x + Q_uu * du[k] # Update value function gradient
V_xx[k] = Q_xx # Update value function Hessian (V_xx)
# Forward pass: update trajectory using the new control inputs
x_new = np.zeros(N+1)
u_new = np.zeros(N)
x_new[0] = x0
for k in range(N):
u_new[k] = u[k] + du[k] # Update control
x_new[k+1] = system_dynamics(x_new[k], u_new[k]) # Update state
# Compute the total cost for the current trajectory
current_cost = np.sum([cost_function(x_new[k], u_new[k]) for k in range(N)])
# 1. Stop based on control input change
if np.max(np.abs(du)) < epsilon_u:
print(f"Stopped due to control input convergence at iteration {i}")
break
# Update for next iteration
x = x_new
u = u_new
prev_cost = current_cost
return x, u, i
if __name__ == "__main__":
# iLQR parameters
N = 3 # Number of time steps
x0 = 1 # Initial state
iterations = 50 # Maximum number of iterations
epsilon_u = 1e-3 # Tolerance for control input changes
epsilon_J = 1e-4 # Tolerance for cost function change
epsilon_x = 1e-4 # Tolerance for state trajectory change
# Initialize control sequence and state trajectory
u = np.zeros(N) # Initial control sequence
x = np.zeros(N+1) # State trajectory
x[0] = x0
# Compute initial trajectory
x_initial = compute_initial_trajectory(x0, u)
# Run iLQR with stopping conditions
x_final, u_final, num_iterations = ilqr_with_conditions(x_initial, u, iterations, epsilon_u, epsilon_J, epsilon_x)
# Output the final results and number of iterations
print(x_final, u_final, num_iterations)
执行结果如下:
Stopped due to control input convergence at iteration 11
[1. 0.44125242 0.18222331 0.0923361 ] [-0.59287488 -0.26201687 -0.0900087 ] 11
更多推荐
所有评论(0)