1. 从LQR到深度强化学习:一条被低估的进阶路线
很多人学强化学习,一上来就扎进策略梯度、PPO、SAC这些热门算法里,跑几个MuJoCo环境,看到reward曲线往上走就觉得自己入门了。但真到了要自己设计控制器、调参调到怀疑人生的时候,才发现底层那套最优控制的理论框架根本没搭起来。CS285的lec10到lec12这三讲,恰好卡在一个非常关键的位置——它把经典最优控制里的LQR(线性二次调节器)和深度强化学习里的策略优化串了起来。如果你跳过LQR直接看后面的actor-critic,很多推导你会觉得“凭空冒出来”,但补上LQR这块基石之后,整个逻辑链条会顺畅得多。
这篇文章面向的是已经学过一点强化学习基础(比如知道MDP、值函数、策略梯度大概怎么回事),但在最优控制和深度RL结合处卡壳的人。我会把lec10到lec12的核心内容拆开,重点讲清楚LQR为什么重要、它和RL的桥梁在哪里、以及实操中怎么把这些理论落地成能跑的代码。文章里涉及的公式推导我会尽量用直觉解释,参数选择会给具体计算过程,踩过的坑也会如实说。你不需要有控制理论背景,但需要愿意动手推一推公式、跑一跑实验。
先说结论:LQR不是“过时的经典控制”,它是理解现代深度RL中策略优化、值函数近似、甚至模型预测控制的一把钥匙。lec10讲LQR和轨迹优化,lec11讲从最优控制到RL的过渡,lec12讲策略梯度和actor-critic。这三讲连起来看,你会发现很多深度RL里的设计选择,其实在LQR框架下早有答案。
2. LQR基础:为什么它值得你花时间啃
2.1 LQR到底在解决什么问题
LQR要解决的问题非常具体:你有一个线性系统,状态转移是 ( x_{t+1} = A x_t + B u_t ),你想找一个控制序列 ( u_0, u_1, \dots, u_{T-1} ),让整个轨迹上的累积代价最小。代价函数是二次型的:
[ J = \sum_{t=0}^{T-1} (x_t^T Q x_t + u_t^T R u_t) + x_T^T Q_f x_T ]
这里 ( Q ) 和 ( Q_f ) 是半正定矩阵,惩罚状态偏离原点;( R ) 是正定矩阵,惩罚控制量的大小。这个设定看起来简单,但它有两个极其重要的性质:第一,最优控制律是状态的线性函数 ( u_t = K_t x_t + k_t );第二,这个问题有解析解,可以通过动态规划反向递推求出 ( K_t ) 和 ( k_t )。
我刚开始学的时候觉得这太理想化了——真实系统哪有线性的?但后来才明白,LQR的价值不在于直接控制真实系统,而在于它提供了一个可解析分析的最优控制基准。深度RL里的很多算法,本质上是在用神经网络去逼近LQR给出的那个最优策略,只不过系统是非线性的、代价函数是任意的。
2.2 反向递推:LQR的求解过程拆解
LQR的求解用的是动态规划的思想,从终点往回推。定义值函数 ( V_t(x) ) 为从时刻 ( t ) 的状态 ( x ) 出发,到终点为止的最小代价。由于代价是二次型、系统是线性的,可以假设 ( V_t(x) = x^T P_t x + 2 p_t^T x + c_t )。这个假设不是拍脑袋,而是因为二次型在线性变换下保持二次型,所以值函数的形式可以预先确定。
反向递推的步骤是这样的:
- 初始化 ( P_T = Q_f ),( p_T = 0 ),( c_T = 0 )。
- 对于 ( t = T-1 ) 到 ( 0 ),计算: [ K_t = (R + B^T P_{t+1} B)^{-1} B^T P_{t+1} A ] [ k_t = (R + B^T P_{t+1} B)^{-1} B^T p_{t+1} ] [ P_t = Q + A^T P_{t+1} A - A^T P_{t+1} B (R + B^T P_{t+1} B)^{-1} B^T P_{t+1} A ] [ p_t = A^T p_{t+1} - A^T P_{t+1} B (R + B^T P_{t+1} B)^{-1} B^T p_{t+1} ]
- 得到最优控制 ( u_t = -K_t x_t - k_t )。
这个递推过程在代码里就是一个for循环,但有几个细节容易出错。第一,矩阵求逆那一步,( R + B^T P_{t+1} B ) 必须是对称正定的,否则求逆会出问题。实际实现时可以用Cholesky分解或者直接调numpy的linalg.solve,不要显式求逆。第二,如果 ( Q ) 和 ( R ) 的量级差太多,数值稳定性会变差,建议先做归一化。第三,当 ( T ) 很大时,( P_t ) 会收敛到一个稳态值 ( P_\infty ),这时候可以用稳态LQR,不用每一步都递推。
注意:LQR的反向递推要求系统是可镇定的(stabilizable),也就是说所有不稳定的模态都能被控制输入影响。如果这个条件不满足,递推出来的 ( P_t ) 可能发散。
2.3 有限时域与无限时域LQR的区别
lec10里花了很大篇幅讲有限时域LQR,也就是 ( T ) 是固定的。这种情况下 ( K_t ) 是时变的,越靠近终点,控制增益越大,因为你要在有限步内把状态拉回原点。无限时域LQR则是 ( T \to \infty ),这时候 ( K_t ) 收敛到常数矩阵 ( K_\infty ),可以用代数Riccati方程直接求解。
实际做RL实验时,大部分环境是无限时域的(或者episode长度不固定),所以稳态LQR更常用。但有限时域LQR的推导是理解动态规划的基础,不能跳过。我建议你先手推一遍有限时域的递推,再用scipy或者自己写代码验证,最后看无限时域怎么从有限时域取极限得到。
2.4 LQR与轨迹优化的关系
LQR本质上是一个轨迹优化问题,只不过它有解析解。更一般的轨迹优化问题(非线性系统、非二次代价)没有解析解,需要用数值方法,比如打靶法(shooting method)或者配点法(collocation)。lec10里提到的iLQR(iterative LQR)就是把非线性系统在 nominal trajectory 附近线性化,然后用LQR求解,再更新轨迹,反复迭代。
iLQR的每一步是这样的:
- 在当前轨迹 ( \bar{x}_t, \bar{u}_t ) 处线性化动力学:( A_t = \frac{\partial f}{\partial x} ),( B_t = \frac{\partial f}{\partial u} )。
- 在轨迹处二次化代价函数,得到 ( Q_t, R_t, Q_f )。
- 用LQR反向递推求出最优控制增量 ( \delta u_t )。
- 前向传播更新轨迹,加入线搜索保证代价下降。
- 重复直到收敛。
这个流程和深度RL里的策略迭代非常像——都是在当前策略下评估,然后改进策略。区别在于iLQR用的是局部线性二次近似,而深度RL用的是神经网络全局近似。理解了iLQR,你再看TRPO、PPO里的信赖域和线搜索,会觉得似曾相识。
3. 从最优控制到强化学习:lec11的桥梁作用
3.1 为什么需要从最优控制过渡到RL
LQR和iLQR都假设你知道系统动力学 ( f(x, u) )。但现实中很多场景下,你根本不知道动力学方程,只能通过和环境交互采样来学习。这就是RL要解决的问题。lec11的核心就是讲清楚:当你不知道动力学时,怎么把最优控制的思想转化成RL算法。
这里有一个关键的概念转换:在最优控制里,你优化的是控制序列 ( u_{0:T-1} );在RL里,你优化的是策略 ( \pi_\theta(u|x) )。策略是一个从状态到动作分布的映射,它可以是随机的,也可以是确定的。这个转换看起来简单,但它带来了两个新问题:第一,策略的参数量可能很大(比如神经网络),怎么高效优化?第二,采样是有噪声的,怎么保证估计的梯度方向是对的?
3.2 值函数、Q函数与优势函数
lec11里重新定义了值函数和Q函数,但这次是在RL的语境下。值函数 ( V^\pi(x) ) 是从状态 ( x ) 出发,按照策略 ( \pi ) 采样得到的期望累积回报。Q函数 ( Q^\pi(x, u) ) 是在状态 ( x ) 执行动作 ( u ),然后按照 ( \pi ) 继续的期望累积回报。优势函数 ( A^\pi(x, u) = Q^\pi(x, u) - V^\pi(x) ) 衡量的是动作 ( u ) 相对于平均水平的优劣。
这三个函数的关系是RL算法的核心。策略梯度的公式可以写成:
[ \nabla_\theta J(\theta) = \mathbb{E}{x \sim \rho^\pi, u \sim \pi\theta} [\nabla_\theta \log \pi_\theta(u|x) A^\pi(x, u)] ]
这个公式的直觉是:增加高优势动作的概率,降低低优势动作的概率。但这里有一个坑——优势函数的估计是有偏的,如果估计不准,策略梯度就会把策略带偏。所以后面才有GAE(广义优势估计)这种技巧来平衡偏差和方差。
3.3 策略迭代与值迭代的RL版本
在最优控制里,策略迭代是:给定策略,计算值函数;然后基于值函数改进策略。在RL里,这个流程变成了:
- 用当前策略 ( \pi_\theta ) 采样一批轨迹。
- 用这些轨迹估计优势函数 ( \hat{A}^\pi(x, u) )。
- 用策略梯度更新 ( \theta )。
- 重复。
这个流程和iLQR的迭代非常像,但有一个本质区别:iLQR每次迭代都重新线性化,而RL的策略更新是在参数空间里走一小步。这个“一小步”的大小就是学习率,它决定了算法的稳定性和收敛速度。学习率太大,策略会崩;太小,收敛太慢。lec11里提到的信赖域方法(比如TRPO)就是用来控制这个步长的。
3.4 模型学习:从数据中估计动力学
如果你既想用最优控制的思路,又不知道动力学,一个自然的想法是:先从数据里学一个动力学模型 ( \hat{f}(x, u) ),然后用iLQR或者MPC去控制。这就是基于模型的RL(model-based RL)。lec11里讨论了模型学习的几种方式:一种是直接回归 ( x_{t+1} - x_t ) 作为目标,另一种是预测状态分布。直接回归的损失函数是均方误差,但要注意,模型误差会在多步预测中累积,所以训练时最好用多步预测的损失。
模型学习的一个常见坑是:神经网络模型在训练数据分布内很准,但一旦策略跑到分布外,模型预测就会错得离谱。这时候用模型规划出来的控制量可能完全不可行。解决办法是加入不确定性估计,比如用ensemble或者贝叶斯神经网络,当模型不确定时降低对模型的信任。
4. 策略梯度与Actor-Critic:lec12的核心内容
4.1 策略梯度的推导与直觉
lec12从策略梯度开始讲。策略梯度的推导有两种方式:一种是似然比方法(REINFORCE),另一种是路径导数方法。似然比方法不要求动力学可微,适用性更广,但方差大;路径导数方法要求动力学可微,方差小但有偏。CS285主要讲似然比方法,因为它在深度RL里更常用。
似然比方法的核心公式是:
[ \nabla_\theta J(\theta) = \mathbb{E}{\tau \sim \pi\theta} [\sum_{t=0}^{T-1} \nabla_\theta \log \pi_\theta(u_t|x_t) \hat{A}_t] ]
其中 ( \hat{A}t ) 是优势函数的估计。这个公式的推导用到了log-derivative trick:( \nabla\theta \pi_\theta = \pi_\theta \nabla_\theta \log \pi_\theta )。这个技巧把对概率的梯度转化成了对log概率的梯度,使得我们可以用采样来估计梯度。
实操中,( \hat{A}t ) 的估计方式决定了算法的性能。最简单的估计是 ( \hat{A}t = \sum{t'=t}^{T-1} r{t'} - b ),其中 ( b ) 是基线。基线的作用是降低方差,不影响期望。更好的估计是GAE:
[ \hat{A}t^{GAE} = \sum{l=0}^{T-t-1} (\gamma \lambda)^l \delta_{t+l} ]
其中 ( \delta_t = r_t + \gamma V(x_{t+1}) - V(x_t) ) 是TD误差。( \lambda ) 控制偏差和方差的权衡,( \lambda=0 ) 时就是TD误差,方差小但偏差大;( \lambda=1 ) 时就是蒙特卡洛回报,无偏但方差大。实际中 ( \lambda ) 取0.95到0.99之间比较常见。
4.2 Actor-Critic架构的设计选择
Actor-Critic就是把策略(actor)和值函数(critic)分开表示,actor负责选动作,critic负责评估动作的好坏。lec12里讨论了几个设计选择:
第一,actor和critic是否共享网络参数。共享参数可以减少参数量、加速训练,但可能导致两个任务互相干扰。实际中,如果状态空间是图像,通常共享卷积层;如果是低维状态,通常分开。
第二,critic是学 ( V(x) ) 还是 ( Q(x, u) )。学 ( V(x) ) 的话,优势函数需要 ( Q(x, u) - V(x) ),而 ( Q(x, u) ) 可以用 ( r + \gamma V(x') ) 来估计。学 ( Q(x, u) ) 的话,优势函数直接是 ( Q(x, u) - V(x) ),但 ( V(x) ) 需要额外估计。两种方式各有优劣,实际中A2C、PPO通常学 ( V(x) ),而DDPG、SAC通常学 ( Q(x, u) )。
第三,策略是随机还是确定。随机策略(比如高斯策略)天然有探索能力,但方差大;确定策略(比如DDPG)需要额外加噪声来探索,但样本效率高。lec12主要讲随机策略,因为它的策略梯度推导更直接。
4.3 方差缩减技巧实操
策略梯度的方差是最大的敌人。除了GAE,还有几个常用的方差缩减技巧:
- 基线(baseline):用 ( V(x) ) 作为基线,优势函数变成 ( Q(x, u) - V(x) )。基线不改变期望,但能显著降低方差。
- 奖励归一化:把奖励减去滑动平均、除以滑动标准差。这个技巧在连续控制任务里几乎是标配。
- 优势归一化:在一个batch内把优势函数归一化到零均值、单位方差。这个技巧在PPO里很常用。
- 并行采样:用多个环境并行采样,然后拼成一个batch。这样batch内的样本更独立,梯度估计更稳。
我实测下来,奖励归一化和优势归一化对训练稳定性的提升最明显。特别是奖励归一化,如果不做,不同任务的奖励量级差很多,学习率根本没法统一设。
4.4 从策略梯度到PPO:信赖域的实际实现
lec12最后提到了信赖域方法,也就是TRPO和PPO。TRPO的约束是KL散度小于某个阈值,保证策略更新不会太大。PPO把约束软化成了惩罚项或者clip,实现更简单。
PPO的clip目标函数是:
[ L^{CLIP}(\theta) = \mathbb{E}_t [\min(r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t)] ]
其中 ( r_t(\theta) = \frac{\pi_\theta(u_t|x_t)}{\pi_{\theta_{old}}(u_t|x_t)} ) 是概率比。clip的作用是:当概率比超出 ( [1-\epsilon, 1+\epsilon] ) 时,梯度不再推动策略往那个方向走。这个设计比TRPO简单得多,但效果差不多。
实操中,PPO有几个关键参数:clip范围 ( \epsilon ) 通常取0.1到0.2;GAE的 ( \lambda ) 取0.95;每个batch更新多少次(n_epochs)通常取3到10;学习率用3e-4到1e-3。这些参数不是随便设的,背后都有偏差-方差权衡的考量。
5. 实操复现:从LQR到PPO的完整流程
5.1 环境搭建与依赖选择
要复现lec10到lec12的内容,你需要一个能跑LQR和深度RL的环境。我建议用Python,依赖如下:
pip install numpy scipy matplotlib gymnasium torchgymnasium是OpenAI Gym的维护版本,接口更稳定。torch用来实现神经网络。如果你不想装gymnasium,也可以自己写一个简单的线性系统环境,LQR部分不需要gym。
对于LQR实验,我建议先从一个简单的双积分器开始:
[ A = \begin{bmatrix} 1 & \Delta t \ 0 & 1 \end{bmatrix}, \quad B = \begin{bmatrix} 0 \ \Delta t \end{bmatrix} ]
状态是位置和速度,控制是加速度。( Q = \text{diag}(1, 0.1) ),( R = 0.01 )。这个系统足够简单,但能展示LQR的所有关键特性。
5.2 LQR反向递推的代码实现
下面是一个有限时域LQR的实现:
import numpy as np def lqr_finite(A, B, Q, R, Qf, T): n = A.shape[0] m = B.shape[1] P = [None] * (T + 1) K = [None] * T P[T] = Qf for t in range(T - 1, -1, -1): S = R + B.T @ P[t+1] @ B K[t] = np.linalg.solve(S, B.T @ P[t+1] @ A) P[t] = Q + A.T @ P[t+1] @ A - A.T @ P[t+1] @ B @ K[t] return K, P这个实现里,我用np.linalg.solve而不是显式求逆,数值上更稳定。K[t]是时刻 ( t ) 的最优控制增益,控制律是 ( u_t = -K[t] @ x_t )。
验证的时候,你可以跑一个闭环仿真,看状态是否收敛到零。如果发散,检查 ( Q ) 和 ( R ) 是否正定,以及系统是否可镇定。
5.3 从LQR到iLQR的扩展
iLQR的实现要复杂一些,因为需要线性化和二次化。下面是一个简化版的iLQR核心循环:
def ilqr(f, cost, x0, U, T, n_iter=50): U = U.copy() for it in range(n_iter): # 前向传播 X = [x0] for t in range(T): X.append(f(X[t], U[t])) # 反向递推 K, k = backward_pass(f, cost, X, U) # 前向更新 X_new, U_new = forward_pass(f, X, U, K, k) # 线搜索 U = line_search(f, cost, X, U, X_new, U_new) return U这个框架和深度RL的策略迭代非常像:前向传播对应采样,反向递推对应策略评估,前向更新对应策略改进。区别在于iLQR用的是解析梯度,而深度RL用的是随机梯度。
5.4 PPO在连续控制任务上的实现要点
PPO的实现有几个关键模块:
第一,网络结构。actor输出高斯分布的均值和标准差,critic输出状态值。均值用tanh激活限制在动作范围内,标准差用softplus保证正数。
第二,采样和更新。每个iteration采样一批轨迹,计算GAE,然后对actor和critic分别做梯度更新。critic的损失是值函数的均方误差,actor的损失是PPO的clip目标。
第三,超参数。我常用的配置是:learning_rate=3e-4,gamma=0.99,lambda=0.95,clip_epsilon=0.2,n_epochs=10,batch_size=64。这些参数在MuJoCo任务上表现稳定。
一个常见的坑是:如果奖励没有归一化,critic的损失会很大,梯度会爆炸。解决办法是在环境wrapper里做奖励归一化,用滑动平均和滑动标准差。
5.5 实验结果对比与分析
我跑了一个简单的对比实验:在双积分器系统上,分别用LQR、iLQR和PPO控制。LQR直接给出解析最优解,累积代价最低;iLQR迭代几次后接近LQR;PPO需要更多样本,但最终也能达到接近LQR的性能。
这个对比说明了一个重要观点:当系统是线性的、代价是二次的,LQR是最优的,深度RL没有必要。深度RL的价值在于处理非线性系统和非二次代价。但理解LQR能帮你判断深度RL算法的性能上限——如果PPO在双积分器上都跑不过LQR,那说明实现有问题。
6. 常见问题与排查技巧实录
6.1 LQR递推发散的排查思路
LQR反向递推发散通常有三个原因:第一,系统不可镇定,也就是存在不稳定模态不受控制输入影响。检查方法:计算PBH矩阵 ( [\lambda I - A, B] ) 的秩,对所有不稳定特征值 ( \lambda ) 都要满秩。第二,( R ) 不是正定的,导致 ( R + B^T P B ) 不可逆。检查 ( R ) 的特征值是否都大于零。第三,数值误差累积,特别是 ( T ) 很大时。解决办法是用稳态LQR,或者用平方根滤波。
6.2 策略梯度方差过大的处理
策略梯度方差过大表现为:训练曲线剧烈震荡,不同随机种子结果差异巨大。处理方法:第一,加基线,用 ( V(x) ) 作为基线。第二,用GAE,( \lambda ) 取0.95左右。第三,奖励归一化。第四,增大batch size。第五,用并行环境采样。我通常同时用前四个,效果就很明显了。
6.3 PPO训练不收敛的常见原因
PPO不收敛的原因很多,我列一个速查表:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 策略熵迅速降到零 | 熵正则系数太小 | 增大entropy_coef到0.01 |
| 值函数损失震荡 | 学习率太大 | 降低critic学习率 |
| 奖励不上升 | 优势估计有偏 | 检查GAE的lambda和gamma |
| 动作饱和 | 动作范围没限制 | 用tanh压缩均值 |
| 训练后期崩溃 | clip范围太大 | 减小clip_epsilon到0.1 |
6.4 模型学习中的分布偏移问题
基于模型的RL最大的坑是分布偏移:模型在训练数据上很准,但策略跑到新区域后,模型预测完全错误。解决办法:第一,用ensemble估计不确定性,当不确定性高时降低模型的使用权重。第二,用短rollout,不要用模型预测太长的轨迹。第三,定期用真实环境数据重新训练模型。第四,用保守的模型预测,比如对预测的状态加惩罚。
6.5 从仿真到实物的迁移注意事项
如果你想把LQR或RL算法用到真实系统上,有几个注意事项:第一,仿真里的动力学和真实系统有差距,LQR的增益需要重新调。第二,真实系统有延迟和噪声,需要在代价函数里加惩罚项。第三,安全约束很重要,LQR可以加约束变成约束LQR,RL可以用安全层过滤动作。第四,先从简单的PD控制开始,逐步过渡到LQR和RL。
我个人在实际操作中的体会是,LQR那套东西看起来数学味很重,但真正写代码实现一遍之后,你会发现它比深度RL简单得多,而且能给你很多直觉。比如为什么PPO要clip、为什么GAE要用lambda、为什么值函数要归一化,这些问题在LQR框架下都有对应的解释。lec10到lec12这三讲,值得反复看几遍,每遍都会有新的收获。