|
|
|
|
@ -0,0 +1,327 @@
|
|
|
|
|
# 第一章——具身智能到底在做什么
|
|
|
|
|
|
|
|
|
|
这一章先不训练大模型。我们从一个能手算、能写成 Python 的推物例子开始,理解具身智能最基本的工作方式:
|
|
|
|
|
|
|
|
|
|
> 观察环境,根据目标决定动作,执行动作,再根据新的观察调整下一步动作。
|
|
|
|
|
|
|
|
|
|
这就是“观察 → 决策 → 动作 → 反馈”的闭环。后续章节中的行为克隆、ACT、Diffusion Policy 和 VLA,虽然生成动作的方式不同,但最终都要进入这个闭环。
|
|
|
|
|
|
|
|
|
|
## 1. 具体问题:机器人怎样把方块推到目标位置
|
|
|
|
|
|
|
|
|
|
假设桌上有一个方块,我们希望机器人把它推到指定位置。机器人在每个时刻都需要回答三个问题:
|
|
|
|
|
|
|
|
|
|
1. **现在是什么情况?** 读取相机、关节或位置传感器,得到观察。
|
|
|
|
|
2. **下一步应该做什么?** 策略根据观察和任务目标选择动作。
|
|
|
|
|
3. **动作执行后发生了什么?** 重新观察环境,再决定下一步动作。
|
|
|
|
|
|
|
|
|
|
例如,机器人原本打算把方块向右推 2 厘米,但由于桌面摩擦较小,方块实际移动了 3 厘米。机器人如果重新观察,就能减小下一步的推动量;如果不看执行结果而继续照原计划行动,误差可能越来越大。
|
|
|
|
|
|
|
|
|
|
## 2. 直观解释:闭环为什么重要
|
|
|
|
|
|
|
|
|
|
可以把机器人想象成正在蒙眼走向一个目标的人:
|
|
|
|
|
|
|
|
|
|
- 每走一步睁眼看一次,再修正方向,是**闭环控制(closed-loop control)**;
|
|
|
|
|
- 开始前记住一串步伐,之后闭眼全部执行,是**开环控制(open-loop control)**。
|
|
|
|
|
|
|
|
|
|
这个类比只说明“是否利用执行后的反馈”。真实机器人还要处理传感器噪声、动作延迟、接触、摩擦和动力学等问题。
|
|
|
|
|
|
|
|
|
|
闭环并不保证一定成功。如果观察错误、策略能力不足或动作超出机器人能力,闭环仍然可能失败。它的意义是:策略有机会利用最新信息修正后续动作。
|
|
|
|
|
|
|
|
|
|
## 3. 五个核心概念
|
|
|
|
|
|
|
|
|
|
下标 \(t\) 表示第 \(t\) 个时间步(timestep)。
|
|
|
|
|
|
|
|
|
|
| 概念 | 符号 | 含义 | 推物例子 |
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
| 状态(State) | \(s_t\) | 足以描述环境当前情况及其后续演化的信息 | 推杆位置、方块位置、方块角度、速度等 |
|
|
|
|
|
| 观察(Observation) | \(o_t\) | 机器人通过传感器实际获得的信息 | 相机图像、关节角度、测得的位置 |
|
|
|
|
|
| 动作(Action) | \(a_t\) | 策略发送给环境或控制器的指令 | 推杆的目标坐标、机械臂关节目标 |
|
|
|
|
|
| 策略(Policy) | \(\pi\) | 根据观察和任务目标选择动作的规则或模型 | 根据方块和目标的位置决定向哪里推 |
|
|
|
|
|
| 状态转移(Transition) | \(f\) 或 \(P\) | 环境执行动作后如何变化 | 接触和摩擦使方块移动或旋转 |
|
|
|
|
|
|
|
|
|
|
### 3.1 状态不等于观察
|
|
|
|
|
|
|
|
|
|
状态包含环境演化所需的信息,观察则是机器人能够测到的信息,两者不能直接画等号。
|
|
|
|
|
|
|
|
|
|
以 PushT 推物任务为例,一个简化状态可以写成:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
s_t=
|
|
|
|
|
\begin{bmatrix}
|
|
|
|
|
x_t^{\text{agent}} & y_t^{\text{agent}} &
|
|
|
|
|
x_t^{\text{block}} & y_t^{\text{block}} &
|
|
|
|
|
\theta_t^{\text{block}}
|
|
|
|
|
\end{bmatrix}
|
|
|
|
|
\in\mathbb{R}^{5}
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
其中:
|
|
|
|
|
|
|
|
|
|
- \(x_t^{\text{agent}},y_t^{\text{agent}}\):推杆在平面中的位置;
|
|
|
|
|
- \(x_t^{\text{block}},y_t^{\text{block}}\):方块中心的位置;
|
|
|
|
|
- \(\theta_t^{\text{block}}\):方块的旋转角度。
|
|
|
|
|
|
|
|
|
|
一个状态向量的形状是 `(5,)`。如果一次处理 \(B\) 个状态,组成的批量张量形状就是 `(B, 5)`。
|
|
|
|
|
|
|
|
|
|
真实机器人可能只能看到相机图像,无法直接知道精确位置、速度或摩擦系数。因此观察可能不完整,也可能带有噪声。当一次观察不足以判断当前状态时,策略可能需要使用连续多帧观察或历史信息。
|
|
|
|
|
|
|
|
|
|
### 3.2 策略不一定是神经网络
|
|
|
|
|
|
|
|
|
|
策略 \(\pi\) 是从输入到动作的映射。它可以是:
|
|
|
|
|
|
|
|
|
|
- 人工编写的规则;
|
|
|
|
|
- 传统控制算法;
|
|
|
|
|
- 通过示范训练的神经网络;
|
|
|
|
|
- Diffusion Policy 或 VLA 等模型。
|
|
|
|
|
|
|
|
|
|
所以,“使用策略”不等于“已经进行了机器学习”。本章先用人工规则理解闭环,第三章再让模型从示范数据中学习策略。
|
|
|
|
|
|
|
|
|
|
## 4. 用公式表示完整闭环
|
|
|
|
|
|
|
|
|
|
先采用无噪声、确定性的简化模型:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
\begin{aligned}
|
|
|
|
|
o_t &= h(s_t),\\
|
|
|
|
|
a_t &= \pi(o_t,g),\\
|
|
|
|
|
s_{t+1} &= f(s_t,a_t),\\
|
|
|
|
|
o_{t+1} &= h(s_{t+1}).
|
|
|
|
|
\end{aligned}
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
公式中的符号分别表示:
|
|
|
|
|
|
|
|
|
|
- \(s_t\):执行动作前的环境状态;
|
|
|
|
|
- \(o_t\):策略实际获得的观察;
|
|
|
|
|
- \(g\):任务目标,例如方块的目标位置;
|
|
|
|
|
- \(a_t\):策略选择的动作;
|
|
|
|
|
- \(s_{t+1}\):执行动作后的新状态;
|
|
|
|
|
- \(h\):从真实状态产生传感器观察的过程;
|
|
|
|
|
- \(\pi\):根据观察和目标选择动作的策略;
|
|
|
|
|
- \(f\):环境的状态转移函数。
|
|
|
|
|
|
|
|
|
|
这里把目标 \(g\) 明确写进策略,因为同一个观察面对不同目标时,正确动作可能不同。另一种常见写法是把目标作为观察的一部分:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
\tilde{o}_t=[o_t,g],\qquad a_t=\pi(\tilde{o}_t).
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
真实环境存在不确定性,相同状态下执行相同动作,结果也可能略有不同。这时状态转移可以写成概率形式:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
s_{t+1}\sim P(\cdot\mid s_t,a_t).
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
\(P\) 表示:已知当前状态和动作时,下一个状态可能服从的概率分布。本章只需要知道不确定性存在,暂时不展开概率推导。
|
|
|
|
|
|
|
|
|
|
## 5. 反馈、奖励和成功信号不是一回事
|
|
|
|
|
|
|
|
|
|
闭环中的**反馈(feedback)**通常指执行动作后获得的新观察 \(o_{t+1}\)。**奖励(reward)**则是用数值评价动作结果的信号:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
r_t=R(s_t,a_t,s_{t+1}).
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
例如:
|
|
|
|
|
|
|
|
|
|
- 新观察:方块现在位于坐标 \((3.1,2.0)\);
|
|
|
|
|
- 奖励:方块更接近目标,得到 \(r_t=0.1\);
|
|
|
|
|
- 成功信号:方块进入目标区域,`success=True`。
|
|
|
|
|
|
|
|
|
|
模仿学习主要从专家的“观察—动作”示范中学习,不一定需要奖励;强化学习通常利用奖励改进策略。两者都会在环境中执行策略并获得新的观察。
|
|
|
|
|
|
|
|
|
|
## 6. 数学与手算例子:一维位置控制
|
|
|
|
|
|
|
|
|
|
为了手算,我们先把推物简化成一维位置控制:
|
|
|
|
|
|
|
|
|
|
- 当前位置 \(x_0=2\),目标位置 \(g=5\);
|
|
|
|
|
- 观察就是当前位置:\(o_t=x_t\);
|
|
|
|
|
- 动作是本步移动的距离:\(a_t\in[-1,1]\);
|
|
|
|
|
- 暂不考虑接触、摩擦、惯性和传感器噪声。
|
|
|
|
|
|
|
|
|
|
人工策略定义为:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
a_t=\operatorname{clip}\bigl(0.5(g-o_t),-1,1\bigr),
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
状态转移为:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
x_{t+1}=x_t+a_t.
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
`clip(value, -1, 1)` 表示把数值限制在 \([-1,1]\) 内。这里的 \(x_t\)、\(g\)、\(o_t\) 和 \(a_t\) 都是标量;如果统一写成一维张量,形状均为 `(1,)`。
|
|
|
|
|
|
|
|
|
|
第一步可以手算为:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
\begin{aligned}
|
|
|
|
|
o_0 &= x_0=2,\\
|
|
|
|
|
a_0 &= \operatorname{clip}(0.5\times(5-2),-1,1)=1,\\
|
|
|
|
|
x_1 &= x_0+a_0=2+1=3.
|
|
|
|
|
\end{aligned}
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
第二步为:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
\begin{aligned}
|
|
|
|
|
o_1 &= x_1=3,\\
|
|
|
|
|
a_1 &= \operatorname{clip}(0.5\times(5-3),-1,1)=1,\\
|
|
|
|
|
x_2 &= x_1+a_1=3+1=4.
|
|
|
|
|
\end{aligned}
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
## 7. 代码 Demo:观察、动作和反馈的循环
|
|
|
|
|
|
|
|
|
|
> **Demo 类型:原理演示。** 这段程序不是物理仿真,也没有训练神经网络,只用来展示闭环交互和人工策略。真实 PushT 的动作表示和物理过程会在第七章介绍。
|
|
|
|
|
|
|
|
|
|
将下面的代码保存为 `chapter01_closed_loop.py`,使用 Python 3 运行即可,不需要安装第三方库。
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def policy(observation, goal, gain=0.5):
|
|
|
|
|
"""根据当前位置和目标位置计算动作。"""
|
|
|
|
|
raw_action = gain * (goal - observation)
|
|
|
|
|
return max(-1.0, min(1.0, raw_action))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_episode(add_disturbance=False):
|
|
|
|
|
x = 2.0
|
|
|
|
|
goal = 5.0
|
|
|
|
|
|
|
|
|
|
for t in range(6):
|
|
|
|
|
# 1. 观察
|
|
|
|
|
observation = x
|
|
|
|
|
|
|
|
|
|
# 2. 决策
|
|
|
|
|
action = policy(observation, goal)
|
|
|
|
|
|
|
|
|
|
# 3. 执行动作,环境状态发生变化
|
|
|
|
|
next_x = x + action
|
|
|
|
|
|
|
|
|
|
# 在第 2 步执行后加入外部扰动
|
|
|
|
|
if add_disturbance and t == 1:
|
|
|
|
|
next_x -= 0.8
|
|
|
|
|
|
|
|
|
|
# 4. 记录结果。下一轮会重新观察 next_x
|
|
|
|
|
print(
|
|
|
|
|
f"t={t}, observation={observation:.4f}, "
|
|
|
|
|
f"action={action:.4f}, next_x={next_x:.4f}"
|
|
|
|
|
)
|
|
|
|
|
x = next_x
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print("无扰动:")
|
|
|
|
|
run_episode(add_disturbance=False)
|
|
|
|
|
|
|
|
|
|
print("\n加入扰动:")
|
|
|
|
|
run_episode(add_disturbance=True)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
实际运行结果如下:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
无扰动:
|
|
|
|
|
t=0, observation=2.0000, action=1.0000, next_x=3.0000
|
|
|
|
|
t=1, observation=3.0000, action=1.0000, next_x=4.0000
|
|
|
|
|
t=2, observation=4.0000, action=0.5000, next_x=4.5000
|
|
|
|
|
t=3, observation=4.5000, action=0.2500, next_x=4.7500
|
|
|
|
|
t=4, observation=4.7500, action=0.1250, next_x=4.8750
|
|
|
|
|
t=5, observation=4.8750, action=0.0625, next_x=4.9375
|
|
|
|
|
|
|
|
|
|
加入扰动:
|
|
|
|
|
t=0, observation=2.0000, action=1.0000, next_x=3.0000
|
|
|
|
|
t=1, observation=3.0000, action=1.0000, next_x=3.2000
|
|
|
|
|
t=2, observation=3.2000, action=0.9000, next_x=4.1000
|
|
|
|
|
t=3, observation=4.1000, action=0.4500, next_x=4.5500
|
|
|
|
|
t=4, observation=4.5500, action=0.2250, next_x=4.7750
|
|
|
|
|
t=5, observation=4.7750, action=0.1125, next_x=4.8875
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 8. 结果解读
|
|
|
|
|
|
|
|
|
|
前两步计算出的原始动作大于或等于 1,因此动作被限制在 1。这表示执行器每一步最多只能移动一个单位。
|
|
|
|
|
|
|
|
|
|
当位置进入目标附近后,动作逐步变小。定义位置误差:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
e_t=g-x_t.
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
当动作没有触发上下限时:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
\begin{aligned}
|
|
|
|
|
a_t &= 0.5e_t,\\
|
|
|
|
|
e_{t+1} &= g-x_{t+1}\\
|
|
|
|
|
&=g-(x_t+0.5e_t)\\
|
|
|
|
|
&=0.5e_t.
|
|
|
|
|
\end{aligned}
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
也就是说,误差每一步缩小一半。它会逐渐接近零,但有限步内通常不会恰好等于零。因此工程中通常使用容差判断成功,例如:
|
|
|
|
|
|
|
|
|
|
\[
|
|
|
|
|
|g-x_t|<\varepsilon,
|
|
|
|
|
\]
|
|
|
|
|
|
|
|
|
|
其中 \(\varepsilon\) 是允许误差。如果 \(\varepsilon=0.1\),无扰动实验执行 6 步后的误差为 \(5-4.9375=0.0625\),可以判定成功。
|
|
|
|
|
|
|
|
|
|
扰动实验中,第 2 步执行后的位置由 4.0 变成了 3.2。下一轮策略重新观察到 3.2,于是把动作从原本可能采用的 0.5 调整为 0.9。这正是反馈在闭环中的作用。
|
|
|
|
|
|
|
|
|
|
`gain=0.5` 是策略中的比例系数:数值较小通常移动得更慢;数值过大时可能反复越过目标甚至振荡。这个例子省略了惯性和时间单位,因此只能解释闭环原理,不能直接作为真实机器人的控制器。
|
|
|
|
|
|
|
|
|
|
## 9. 从原理 Demo 连接到机器人仿真
|
|
|
|
|
|
|
|
|
|
Gymnasium 是常见的仿真环境接口。一个完整回合(episode)通常写成:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
observation, info = env.reset(seed=0)
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
|
action = policy(observation)
|
|
|
|
|
next_observation, reward, terminated, truncated, info = env.step(action)
|
|
|
|
|
observation = next_observation
|
|
|
|
|
|
|
|
|
|
if terminated or truncated:
|
|
|
|
|
break
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
其中:
|
|
|
|
|
|
|
|
|
|
- `reset()`:开始一个新回合并返回初始观察;
|
|
|
|
|
- `step(action)`:执行一个动作并将环境推进一个时间步;
|
|
|
|
|
- `reward`:当前动作得到的奖励;
|
|
|
|
|
- `terminated`:任务因成功或失败等内部条件结束;
|
|
|
|
|
- `truncated`:任务因时间上限等外部条件结束;
|
|
|
|
|
- `info`:用于调试或评测的额外信息。
|
|
|
|
|
|
|
|
|
|
本章代码中的 `observation → policy → action → next_x`,与这个接口中的交互结构相同。区别在于,真实推物仿真会用物理引擎计算碰撞、摩擦和方块旋转。
|
|
|
|
|
|
|
|
|
|
## 10. 小练习
|
|
|
|
|
|
|
|
|
|
请先自己计算或修改代码,再核对运行结果。
|
|
|
|
|
|
|
|
|
|
1. 已知 \(x_1=3\),手算 \(a_1\) 和 \(x_2\)。代码输出是否一致?
|
|
|
|
|
2. 如果目标从 \(g=5\) 改为 \(g=-2\),为什么只写 \(a_t=\pi(o_t)\) 不足以完整描述这里的策略?
|
|
|
|
|
3. 将扰动从 `next_x -= 0.8` 改为 `next_x += 0.8`。下一步动作会变大还是变小?为什么?
|
|
|
|
|
4. 分别把 `gain` 改为 `0.2` 和 `1.2`,比较 6 步后的位置和误差。哪个更快?是否出现越过目标的情况?
|
|
|
|
|
5. 用自己的话解释:状态、观察、动作和策略分别是什么?反馈与奖励有什么区别?
|
|
|
|
|
|
|
|
|
|
完成本章后,你应该能够:
|
|
|
|
|
|
|
|
|
|
- 用自己的话解释“观察 → 决策 → 动作 → 反馈”的闭环;
|
|
|
|
|
- 区分状态和观察、反馈和奖励;
|
|
|
|
|
- 根据公式手算至少两个时间步;
|
|
|
|
|
- 运行 Demo,并解释扰动后动作为什么发生变化;
|
|
|
|
|
- 说明本章 Demo 是原理演示,不是真实机器人策略实验。
|
|
|
|
|
|
|
|
|
|
## 11. 参考资料与许可
|
|
|
|
|
|
|
|
|
|
- [Gymnasium:Basic Usage](https://gymnasium.farama.org/introduction/basic_usage/):`reset()`、`step()`、观察空间和动作空间的基础说明。
|
|
|
|
|
- [Gymnasium:Env API](https://gymnasium.farama.org/api/env/):环境、终止和截断信号的接口定义。
|
|
|
|
|
- [Hugging Face gym-pusht](https://github.com/huggingface/gym-pusht):后续完整推物实验使用的 Gymnasium 环境,上游项目采用 Apache-2.0 许可证。
|
|
|
|
|
- [Diffusion Policy 项目主页](https://diffusion-policy.cs.columbia.edu/):PushT 任务及扩散策略的代表性工作,后续第五章会展开介绍。
|
|
|
|
|
|
|
|
|
|
本章文字和一维控制代码为本教程原创。示例仅使用 Python 标准库,没有复制上游项目代码。引用外部项目时请同时遵守对应仓库的许可证。
|