小坷笔记:强化学习零碎知识点笔记

强化学习:折扣回报与贝尔曼方程核心概念

1. 折扣回报 (Discounted Return) 的理论公式:
衡量一个状态的好坏,不仅看当下,还要看未来。

$$
G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \dots = \sum_{k=0}^{\infty} \gamma^k R_{t+k+1}
$$

  • $\gamma$ (Gamma):折扣率 (0 到 1 之间)。决定了智能体是“目光短浅”($\gamma$ 接近 0)还是“高瞻远瞩”($\gamma$ 接近 1)。

2. 解决“未知未来”的两大落地方法:

  • 蒙特卡洛方法 (Monte Carlo):必须等一个回合 (Episode) 彻底结束,拿到所有真实的 $R$ 序列后,再反向计算 $G_t$ 进行更新。属于“事后结算”。
  • 时序差分法 (Temporal Difference, TD):不需要等回合结束,走一步算一步。核心在于利用贝尔曼方程
    $$V(s_t) = \mathbb{E}[R_{t+1} + \gamma V(s_{t+1})]$$
    利用【当下真实的即时奖励 $R_{t+1}$】加上【下一个状态的预估价值 $\gamma V(s_{t+1})$】,来更新【当前状态的价值 $V(s_t)$】。

核心机制:经验回放 (Experience Replay)

1. 经验元组 (Transition Tuple)
智能体与环境交互的基本记录格式:$[s, a, r, s’]$

  • $s$: 当前状态 (State)
  • $a$: 动作 (Action)
  • $r$: 奖励 (Reward)
  • $s’$: 下一状态 (Next State)
    (注:有时候还会加上一个 $done$ 标志,记录游戏是否结束,变成 $[s, a, r, s’, done]$)

2. 机制流程

  • 存储 (Store):智能体在探索环境时,将每一步的 $[s, a, r, s’]$ 存入一个固定容量的队列 (Memory/Buffer) 中。如果存满了,新的经验会挤掉最老的经验。
  • 采样 (Sample):在训练更新网络时,从 Memory 中随机抽取 (Random Sample) 一批大小为 batch_size 的经验进行学习。

3. 为什么必须随机抽样?

  • 打破时间相关性 (Break Correlation):连续的样本之间高度相似,会导致神经网络训练产生震荡甚至崩溃。随机抽样能让送入网络的数据分布更加均匀。
  • 提高数据利用率:过去犯下的惨痛错误(稀有经验),可以在未来被多次随机抽中并反复复习,而不是经历一次就被遗忘了。

机器学习核心概念:偏差、方差与拟合状态

在机器学习中,模型的总误差可以分解为:$Error = Bias^2 + Variance + Noise$

1. 欠拟合 (Underfitting) $\rightarrow$ 高偏差 (High Bias)

  • 表现:模型在“训练集”和“测试集”上的表现都很差,准确率都不高。
  • 原因:模型过于简单(如用直线拟合曲线),表达能力不足,无法捕捉数据中的真实规律。
  • 形象比喻:瞄准镜歪了。预测结果集体偏离真实值。

2. 过拟合 (Overfitting) $\rightarrow$ 高方差 (High Variance)

  • 表现:模型在“训练集”上表现极好(甚至 100% 准确),但在“测试集”上表现极其糟糕。
  • 原因:模型过于复杂,把训练数据里的噪声和偶然特征也当成规律死记硬背了下来。泛化能力极差。
  • 形象比喻:手抖得厉害。稍微改变一下训练数据,训练出的模型就会剧烈变动,极不稳定。

3. 终极目标:偏差-方差权衡 (Bias-Variance Tradeoff)
我们想要的是一个既能看懂规律(低偏差),又不过度敏感(低方差)的模型,也就是打靶时既对准靶心,手又稳(子弹密集击中靶心)。

Advantage Actor-Critic(A2C)

首先定义几个核心变量。$s$ 代表机器人当前的状态(例如各关节的位姿、速度、绳索张力),$a$ 代表输出的动作(例如电机的控制电压或目标力矩),$r$ 是环境给出的即时奖励,$\gamma$ 是用于折算未来收益的折扣因子(通常在 0.9 到 0.99 之间,决定了机器人有多看重长远收益)。

Critic 网络(价值网络)

Critic 网络的内部参数记作 $\phi$。它的任务是预测在状态 $s$ 下,未来能获得的总回报,即价值函数 $V_\phi(s)$。当机器人执行动作 $a$ 后,环境反馈了即时奖励 $r$,并进入下一个状态 $s’$。此时我们可以计算出一个“更真实的期望值”,即目标值(TD Target):
$$y = r + \gamma V_\phi(s’)$$
Critic 网络需要让自己的预测 $V_\phi(s)$ 尽可能逼近这个目标值 $y$。因此,Critic 的损失函数(Loss)通常采用均方误差:
$$L_C(\phi) = \frac{1}{2} (V_\phi(s) - y)^2$$
有了损失函数后,Critic 通过求导计算梯度,并以学习率 $\alpha_C$ 更新自身参数 $\phi$,使得下一次预测更准:
$$\phi \leftarrow \phi - \alpha_C \nabla_\phi L_C(\phi)$$

Actor 网络(策略网络)

Actor 网络的内部参数记作 $\theta$。它的输出是一个概率分布 $\pi_\theta(a|s)$,表示在状态 $s$ 下选择动作 $a$ 的概率。Actor 更新的基础是优势函数(Advantage Function),记作 $A(s, a)$。它衡量的是“当前动作实际带来的收益”与“Critic 预期的平均收益”之间的差值。公式直接利用了前面计算的 TD 误差:
$$A(s, a) = r + \gamma V_\phi(s’) - V_\phi(s)$$
如果 $A(s, a) > 0$,说明动作 $a$ 表现优于预期;反之则差于预期。Actor 的损失函数旨在最大化好动作的概率,由于通常使用梯度下降优化器,所以加上负号将其转化为最小化问题:
$$L_A(\theta) = - \log \pi_\theta(a|s) A(s, a)$$
Actor 根据这个损失函数计算梯度,并以学习率 $\alpha_A$ 更新自身参数 $\theta$:
$$\theta \leftarrow \theta - \alpha_A \nabla_\theta L_A(\theta)$$

完整训练闭环

下面是仿真环境中代码实际运行的流水线:
第一步(采集数据):Actor 接收机器人状态 $s$,根据概率 $\pi_\theta(a|s)$ 采样得到动作 $a$。第二步(环境交互):仿真环境执行动作 $a$,解算绳驱系统的动力学,返回即时奖励 $r$ 和新的状态 $s’$。第三步(计算优势值):Critic 根据 $r$ 和新老状态,计算出目标值 $y = r + \gamma V_\phi(s’)$,并得出优势值 $A(s, a) = y - V_\phi(s)$。第四步(更新 Critic):Critic 计算自身误差 $L_C(\phi)$,执行梯度下降更新参数 $\phi$,提升估值精度。第五步(更新 Actor):Actor 利用计算好的优势值 $A(s, a)$,计算策略误差 $L_A(\theta)$,执行梯度下降更新参数 $\theta$,优化控制策略。

可视化交互组件方便理解:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<div style="border: 1px solid #e2e8f0; padding: 20px; border-radius: 8px; font-family: sans-serif; background-color: #ffffff; color: #333;">
<h4 style="margin-top: 0; color: #1a202c;">Actor-Critic 更新逻辑演示</h4>

<div style="margin-bottom: 15px;">
<label>环境真实奖励 (Reward): <span id="r-val" style="font-weight: bold; color: #2b6cb0;">5</span></label>
<input type="range" id="r-slider" min="-10" max="10" value="5" step="1" style="width: 100%; margin-top: 8px;">
</div>

<div style="margin-bottom: 15px;">
<label>Critic 预期打分 (Prediction): <span id="p-val" style="font-weight: bold; color: #c53030;">2</span></label>
<input type="range" id="p-slider" min="-10" max="10" value="2" step="1" style="width: 100%; margin-top: 8px;">
</div>

<div style="margin-bottom: 20px;">
<label>Actor 动作概率 (Probability): <span id="prob-val" style="font-weight: bold; color: #276749;">0.5</span></label>
<input type="range" id="prob-slider" min="0.01" max="0.99" value="0.5" step="0.01" style="width: 100%; margin-top: 8px;">
</div>

<div style="background-color: #f7fafc; padding: 15px; border-radius: 6px; border: 1px solid #edf2f7; font-size: 15px; line-height: 1.6;">
<div style="margin-bottom: 8px;"><strong>1. 优势值 (Advantage)</strong> = Reward - Prediction = <span id="adv-out" style="font-weight:bold;">3</span></div>
<div style="margin-bottom: 8px;"><strong>2. Critic Loss</strong> = 优势值的平方 = <span id="c-loss-out" style="font-weight:bold;">9.00</span></div>
<div style="margin-bottom: 8px;"><strong>3. Actor Loss</strong> = -ln(Probability) × Advantage = <span id="a-loss-out" style="font-weight:bold;">2.08</span></div>
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px dashed #cbd5e0;">
<strong>4. Actor 更新方向:</strong> <span id="dir-out" style="font-weight: bold; font-size: 16px;">计算中...</span>
</div>
</div>
</div>

<script>
// 获取 DOM 元素
const rSlider = document.getElementById('r-slider');
const pSlider = document.getElementById('p-slider');
const probSlider = document.getElementById('prob-slider');

// 核心计算与更新函数
const updateUI = () => {
const r = parseFloat(rSlider.value);
const p = parseFloat(pSlider.value);
const prob = parseFloat(probSlider.value);

// 更新滑块数值显示
document.getElementById('r-val').innerText = r;
document.getElementById('p-val').innerText = p;
document.getElementById('prob-val').innerText = prob.toFixed(2);

// 计算逻辑
const adv = r - p;
const cLoss = Math.pow(adv, 2);
const aLoss = -Math.log(prob) * adv;

// 更新计算结果
document.getElementById('adv-out').innerText = adv;
document.getElementById('c-loss-out').innerText = cLoss.toFixed(2);
document.getElementById('a-loss-out').innerText = aLoss.toFixed(2);

// 更新方向结论
const dirOut = document.getElementById('dir-out');
if (adv > 0) {
dirOut.innerText = "鼓励该动作 (因为优势值 > 0)";
dirOut.style.color = "#38a169"; // 绿色
} else if (adv < 0) {
dirOut.innerText = "抑制该动作 (因为优势值 < 0)";
dirOut.style.color = "#e53e3e"; // 红色
} else {
dirOut.innerText = "策略保持不变 (优势值为 0)";
dirOut.style.color = "#718096"; // 灰色
}
};

// 绑定事件监听
rSlider.addEventListener('input', updateUI);
pSlider.addEventListener('input', updateUI);
probSlider.addEventListener('input', updateUI);

// 初始化渲染
updateUI();
</script>

Actor-Critic 与 A2C 算法区别

1. 基础版:Actor-Critic (AC)

  • 评分标准:使用绝对价值(如 $Q$ 值)来更新策略。
  • 缺点:无法区分是“动作选得好”还是“当前状态本身就好”。导致策略梯度更新时方差极大 (High Variance),训练非常不稳定。

2. 进阶版:Advantage Actor-Critic (A2C)

  • 评分标准:引入优势函数 $A(s,a)$。
  • 核心公式:$A(s,a) = Q(s,a) - V(s)$
    • $A > 0$:该动作比平均水平好,增加该动作的概率。
    • $A < 0$:该动作比平均水平差,降低该动作的概率。
  • 优点:相当于引入了一个“基线 (Baseline)”。剥离了状态本身带来的好坏,只评估动作的相对好坏。大幅降低了方差 (Reduced Variance),让训练变得极其稳定和高效。

np.array_equal

比较numpy数组尽量不用==,要用np.array_equal。
两个 numpy 数组用==时,不会像普通变量 / 列表那样返回「单个 True/False」,而是对两个数组中对应位置的元素逐一比较 ,返回一个和原数组形状完全相同的布尔数组。

1
2
3
4
5
6
7
8
9
import numpy as np
# 任务1的位置:[5,6],任务2的位置:[5,7],任务3的位置:[5,6]
loc1 = np.array([5,6])
loc2 = np.array([5,7])
loc3 = np.array([5,6])

# 数组用==,逐元素比较,返回布尔数组
print(loc1 == loc2) # 输出 [ True False] → x相等,y不相等
print(loc1 == loc3) # 输出 [ True True] → x、y都相等

np.array_equal(a, b)是专门判断两个数组是否完全相等的函数,核心特性:

  1. 先判断两个数组的形状是否一致(比如都是 2 维、都是 N×2),形状不同直接返回 False;
  2. 形状一致则逐元素比较,所有元素都相等才返回单个 True,否则返回单个 False;
  3. 返回值是单个布尔值,可以直接用在if判断里,完美解决数组比较的问题。
1
2
np.array_equal(loc1, loc2)  # False(元素不全等)
np.array_equal(loc1, loc3) # True(所有元素相等,形状也一致)

@dataclass、default_factory 与 Lambda

1. @dataclass 装饰器

来源from dataclasses import dataclass (Python 3.7+)
定义:一个用于简化类定义的装饰器,专门用于创建主要存储数据的类(Data Class)。
核心功能:自动生成 __init____repr____eq__ 等样板代码,让代码极度简洁。

写法对比

  • 传统写法(手动挡)
    1
    2
    3
    4
    5
    6
    class Point:
    def __init__(self, x, y):
    self.x = x
    self.y = y
    def __repr__(self):
    return f"Point(x={self.x}, y={self.y})"
  • Dataclass 写法(自动挡)
    1
    2
    3
    4
    @dataclass
    class Point:
    x: int
    y: int

2. field(default_factory=…)

核心痛点:可变默认参数陷阱
在 Python 类中,绝对不能直接使用可变对象(如 list, dict, set)作为类变量的默认值。
错误写法:items: list = []
后果:所有该类的实例会共享同一个列表内存地址。修改 A 对象的列表,B 对象的列表也会随之改变。
为了解决共享问题,我们需要告诉 Python:不要使用现成的对象,而是每次实例化时,调用一个“工厂函数”现场创建一个新对象。
示例:

1
taskTypes: List[str] = field(default_factory=lambda: ["search", "fire", "facility"])

ROS 2 与 Gazebo 桥接器 (ros_gz_bridge) 语法

在 Launch 文件中配置 parameter_bridge 时,字符串格式非常严谨,核心公式为:
话题名称@ROS数据类型<方向号>Gazebo数据类型

1. 符号含义:

  • @:分隔符,分隔话题名称和数据类型。
  • ]:单向通信,数据从 ROS 2 流向 Gazebo(例如:下发控制指令、推力)。
  • [:单向通信,数据从 Gazebo 流向 ROS 2(例如:获取传感器数据、Odometry 里程计位置)。
  • @== (有些版本支持双向):双向通信。

2. 核心代码结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from launch_ros.actions import Node

# 1. 定义桥接规则列表
bridge_params = [
'/topic_name@ros_type]gz_type', # ROS -> Gazebo
]

# 2. 创建 Node 节点
bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
arguments=bridge_params,
output='screen'
)

为什么 VS Code 找不到代码引用/不高亮?

问题现象

在 VS Code 中选中一个函数(比如 step),但在其他类中调用它的地方(比如 self.bandit.step())却没有高亮显示。右键点击“查找所有引用”也毫无反应,让人误以为这个函数没被用过。

根本原因:Python 的“动态类型”特性

Python 是一门非常自由的语言,声明变量时不需要指定类型。
当你写下 def __init__(self, bandit): 时,VS Code 的代码分析器(Pylance/IntelliSense)并不知道传入的 bandit 到底是个什么对象(是数字?是字符串?还是老虎机?)。因为不知道身份,VS Code 为了避免报错,干脆就不进行跨文件/跨类的高亮关联。

终极解决办法:类型提示 (Type Hint)

在定义参数时,顺手给它“贴个标签”,明确告诉 VS Code 它的真实身份。

  • 修改前(VS Code 无法识别):
    1
    def __init__(self, bandit):
  • 修改后(VS Code 瞬间变聪明):
    1
    def __init__(self, bandit: BernoulliBandit): 
    (加上 : BernoulliBandit 后,VS Code 瞬间就能把两个类关联起来,代码高亮、Ctrl + 点击 跳转、自动补全全部复活!)

备用方案:暴力搜索法

如果是在阅读别人写的老代码(没有类型提示),千万别依赖高亮来判断函数有没有被调用。请直接使用:

  1. 单文件搜索Ctrl + F
  2. 全局搜索(最管用)Ctrl + Shift + F,在整个工程文件夹里直接搜索函数名。

rsl_rl

1. 简介

rsl_rl 是一个基于 PyTorch 实现的强化学习库,由 ETH Zurich 的 RSL 实验室开发。它专门为同步的大规模并行采样而设计,通常作为 Isaac GymIsaac Lab 的后端算法库。

2. 核心特点

  • 极致的训练速度:通过 GPU 端的向量化环境采样,可以在几十分钟内完成传统 RL 需要几天才能完成的训练量。
  • PPO 算法优化:内置了高度优化的 PPO (Proximal Policy Optimization) 算法,非常适合处理高维连续动作空间(如足式机器人的关节电机控制)。
  • 轻量化架构:相比于 Stable Baselines3 (SB3) 或 Ray Rllib,它的代码结构非常精简,开发者可以轻松修改网络结构或 Loss 函数。
  • 足式机器人适配:内置了处理刚体动力学任务中常见的观察值、特权信息(Privileged Information)和周期性奖励函数的逻辑。

3. 算法原理:PPO 简述

rsl_rl 主要实现的是 Actor-Critic 架构的 PPO 算法。其目标函数公式如下:

$$J^{CLIP}(\theta) = \hat{\mathbb{E}}_t \left[ \min(r_t(\theta) \hat{A}_t, \text{clip}(r_t(\theta), 1-\epsilon, 1+\epsilon) \hat{A}_t) \right]$$

其中:

  • $r_t(\theta)$ 是新旧策略的概率比。
  • $\hat{A}_t$ 是优势函数(Advantage Function)。
  • $\epsilon$ 是裁剪超参数,防止策略更新步长过大。

4. 关键文件结构

在使用 rsl_rl 时,你通常会接触到以下核心类:

  • OnPolicyRunner: 负责管理整个训练循环(存储、评估、存盘)。
  • PPO: 算法核心,处理梯度更新和 Loss 计算。
  • ActorCritic: 定义神经网络架构(通常包括 Actor 网络和 Critic 网络)。
  • RolloutStorage: 在 GPU 上直接存储经验轨迹的数据结构。

5. 典型工作流

  1. 环境定义:在 Isaac Gym/Lab 中定义机器人的 URDF、传感器和奖励函数。
  2. 配置参数:通过 Python 的 config 类定义超参数(如 learning_rate, num_steps_per_env, entropy_coef)。
  3. 启动训练
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
        from rsl_rl.runners import OnPolicyRunner
    # 初始化环境
    env = VecEnv(...)
    # 初始化训练器
    runner = OnPolicyRunner(env, train_cfg, log_dir)
    # 开始学习
    runner.learn(num_learning_iterations=1000, init_with_graceful_stop=True)

    # 变量占位符与拆包赋值
    ## 1. 语法现象:`_, var = function()`
    在 Python 中,当一个函数返回多个值(以元组形式)时,如果我们只需要其中的某一个或某几个,可以使用 `_` 作为占位符。

    ### 示例代码
    ```python
    def get_user_info():
    # 返回 (姓名, 年龄, 职业)
    return "wkh", 23, "算法工程师"

    # 只需要姓名,忽略其他信息
    name, _, _ = get_user_info()

    # 或者使用 * 忽略剩余所有
    name, *_ = get_user_info()

跑RL训练流程

不要在带界面的情况下跑训练(极慢),标准操作分为三步:

Step 1: 纯后台训练 (Train)

通过 –headless 剥离渲染,将 100% 的算力交给张量计算和物理引擎。

1
2
3
4
5
6
# 激活环境并挂载 C++ 底层库隔离区或者其他配置
conda activate LeggedGym
export LD_LIBRARY_PATH=/home/wkh/lessons/legged_gym/isaac_libs:$LD_LIBRARY_PATH

# 无头模式启动训练
python legged_gym/scripts/train.py --task=a1 --num_envs=64 --headless

Step 2: 数据监控 (Monitor)

代码会自动在 logs 目录下按时间戳存档。使用 TensorBoard 查看“大脑”发育情况。

1
2
# 建议使用绝对路径启动,防止找不到数据
tensorboard --logdir=/home/wkh/lessons/legged_gym/logs

核心指标: Train/mean_reward(平均奖励,需稳步上升)和 Episode/length(存活时间,需逐渐变长)。注意: 数据非实时写入,需让程序运行几分钟后刷新网页。

Step 3: 前台可视化验收 (Play)

加载最新训练好的模型权重(只做前向推理,算力压力极小),弹窗查看实际表现。

1
2
# 运行前确保处于 Xorg 桌面环境
python legged_gym/scripts/play.py --task=a1

pip install -e . (可编辑安装)

一句话总结:在当前目录下,以“开发者可编辑模式”将项目安装到当前的 Conda 环境中。它的本质是建立一个指向源代码的快捷方式,而不是复制文件。

-e (全称 –editable):代表“可编辑模式” (Editable mode)。

. :代表“当前终端所在的目录”。(前提:该目录下必须存在 setup.py 或 pyproject.toml 等项目构建文件)。

  • 普通安装(没有 -e): 比如你 pip install numpy,系统会把 numpy 的代码复制一份,扔进你的 Conda 环境的一个深层文件夹(site-packages)里。

  • 可编辑安装(加了 -e): 系统不会复制代码,它只是在你的 Conda 环境里建立了一个快捷方式,指向你当前下载的这个 rsl_rl 文件夹。

为什么要用 -e:因为如果原作者代码有 Bug,或者你想修改底层算法,由于系统是指向这个文件夹的,你只要在这个文件夹里修改了代码保存,你的环境里会立刻生效,不需要重新 pip install

rsl_rl 为例:

  1. 你使用 git clone 下载了 rsl_rl 的源代码文件夹到你的电脑上。

  2. 你在终端 cd rsl_rl 进入该文件夹。

  3. 执行 pip install -e .

结果: 此时在当前环境内, Python 已经认识了 rsl_rl 这个包。无论你的终端以后切换到电脑的哪个目录下运行代码,只要遇到 import rsl_rl,系统都会通过快捷方式,飞回你最初 clone 下来的那个源码文件夹去读取逻辑。

注意:绝对不能移动源代码文件夹! 如果你把下载的 rsl_rl 文件夹剪切到了另一个硬盘或目录,快捷方式就会断裂。当你再次运行代码时,会直接报错 ModuleNotFoundError: No module named 'rsl_rl'