2. 数学基础回顾:马尔可夫决策过程(MDP)与状态、动作、奖励的定义

好,咱们正式开始啃第一块硬骨头——马尔可夫决策过程。

说实话,我当年刚接触强化学习时,觉得MDP就是个花哨的数学名词。直到我在实验室调一个四足机器人平衡控制器,死活跑不稳,才意识到:问题建模不对,后面全白费。MDP就是那个帮你把控制问题“翻译”成数学语言的工具。

2.1 为什么力矩控制离不开MDP?

你想想看,机器人控制本质上是个序列决策问题:当前该输出多大扭矩?下一秒关节位置会变成什么样?怎么让整个运动过程既平滑又省力?

MDP恰好提供了描述这类问题的标准框架。它把控制过程拆解成三个核心要素:状态(State)、动作(Action)、奖励(Reward)。说白了,就是告诉算法:你现在在哪(状态),你能做什么(动作),做得好不好(奖励)。

核心观点:MDP不是算法,而是问题的“坐标系”。坐标系建对了,后面的强化学习算法才能找到最优解。

2.2 MDP的五元组定义

一个标准的MDP用五元组 (S, A, P, R, γ) 来描述。我习惯把它理解成一张“决策地图”:

符号 名称 力矩控制中的含义
S 状态空间 关节角度、角速度、末端位置等传感器读数
A 动作空间 电机输出的扭矩值(连续量)或离散档位
P 状态转移概率 施加扭矩后,下一时刻关节状态的概率分布
R 奖励函数 跟踪误差、能耗、冲击度的加权评分
γ 折扣因子 0~1之间,控制算法“眼光”的长短

我的经验:刚开始做力矩控制时,我总把状态维度设得特别高,觉得“多测几个传感器总没错”。结果算法收敛极慢。后来才明白——状态空间要够用但别冗余。比如控制单腿摆动,关节角度+角速度通常就够了,没必要把电机温度也塞进去。

2.3 状态(State):你“看”到了什么?

状态是算法做决策的依据。在力矩控制里,状态通常来自编码器、IMU、力传感器等。

举个例子,控制一个二连杆机械臂画圆:

  • 关节空间状态: [θ₁, θ₂, ω₁, ω₂] —— 两个关节的角度和角速度
  • 任务空间状态: [x, y, vx, vy] —— 末端位置和速度
  • 混合状态: 两者都包含,再加一个目标轨迹点

我个人更推荐用关节空间+任务空间混合状态。为什么?因为纯关节空间会让算法“看不见”末端轨迹误差,纯任务空间又丢失了关节限位信息。我在做六轴机械臂力控时吃过这个亏——只用了末端位姿做状态,结果算法试图让关节转到物理极限之外,差点烧了电机。

避坑指南:状态必须满足马尔可夫性——当前状态包含了做出最优决策所需的全部历史信息。如果你发现算法“记不住”之前发生的事,那就得把历史帧也拼进状态里。我曾经在足式机器人上把过去3步的关节轨迹都堆进状态,效果立竿见影。

2.4 动作(Action):你能“做”什么?

动作就是控制器输出的指令。在力矩控制中,动作通常是电机目标扭矩或电流指令。

动作空间有两种常见设计:

  1. 连续动作空间:直接输出扭矩值,比如 [-10 N·m, 10 N·m]。精度高,但算法搜索难度大。
  2. 离散动作空间:把扭矩分成几个档位,比如 {-10, -5, 0, 5, 10}。简单粗暴,适合入门。

我个人的建议是:能用连续就别用离散。为什么?因为力矩控制对精度要求高,离散动作会让机器人出现“抖动”——扭矩在档位间跳变,关节像得了帕金森。我在做协作机器人拖动示教时,一开始用了离散动作,结果末端轨迹全是锯齿。换成连续动作后,曲线立刻平滑了。

# 连续动作空间示例(PyTorch)
import torch
import torch.nn as nn

class TorquePolicy(nn.Module):
    def __init__(self, state_dim, action_dim, max_torque):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, action_dim),
            nn.Tanh()  # 输出范围 [-1, 1]
        )
        self.max_torque = max_torque  # 最大扭矩限幅

    def forward(self, state):
        # 输出归一化到 [-max_torque, max_torque]
        return self.net(state) * self.max_torque

2.5 奖励(Reward):你“做”得好不好?

奖励函数是强化学习的“指挥棒”。你设计什么奖励,算法就学会什么行为。力矩控制中常见的奖励项包括:

奖励项 公式示例 作用
跟踪误差 -||q_des - q_act||² 让关节跟随目标轨迹
能耗惩罚 -||τ||² 减少不必要的力矩输出
平滑性惩罚 -||τ_t - τ_{t-1}||² 抑制力矩突变
安全约束 -1000(越界时) 防止关节超限或碰撞

关键原则:奖励函数要稀疏但明确。别搞一堆小奖励让算法“猜”你的意图。我见过有人给力矩控制加了十几个奖励项,结果机器人学会了原地抖动——因为抖动能让每个小奖励都“蹭”到一点。后来我只保留跟踪误差和能耗两项,机器人反而跑得稳了。

2.6 MDP的“闭环”流程

理解了三个要素,咱们来看看MDP在力矩控制中是怎么转起来的。下面这张图是我自己画的标准流程:

力矩控制中的MDP闭环流程 状态 S_t 关节角度/角速度 策略 π(a|s) 神经网络/查表 动作 A_t 目标扭矩 τ 环境/机器人 动力学 + 传感器 奖励 R_t 跟踪误差 + 能耗 下一状态 S_{t+1} 更新后的关节状态 输入 输出 施加扭矩 产生 更新 时间步 t ← t+1 闭环解释 在每个时间步 t:根据当前状态 S_t 选择动作 A_t → 环境执行并返回 R_t 和 S_{t+1} → 进入下一时间步

这个闭环每走一圈,就是一个时间步。算法通过不断“试错”来优化策略,最终学会在什么状态下该输出多大扭矩。

2.7 折扣因子 γ:算法该“看”多远?

γ 决定了算法是“急功近利”还是“高瞻远瞩”。

  • γ = 0:只看眼前,只最大化当前奖励。适合纯跟踪控制。
  • γ = 0.9:兼顾近期和远期。力矩控制中常用这个值。
  • γ → 1:考虑无限远未来。适合需要规划长周期运动的场景。

我的经验:做力矩控制时,γ 别设太大。为什么?因为机器人动力学有延迟——你这一秒输出的扭矩,可能半秒后才体现在关节位置上。γ 太大,算法会“幻想”自己能瞬间改变未来,导致控制震荡。我一般从 0.95 开始调,如果发现抖动就降到 0.9。

2.8 一个完整的MDP实例

咱们用一个简单的单关节位置控制来串一遍:

# 单关节MDP定义示例
class SingleJointMDP:
    def __init__(self):
        # 状态:关节角度 θ (rad) 和角速度 ω (rad/s)
        self.state = [0.0, 0.0]
        # 目标位置
        self.target = 1.57  # 90度
        
    def step(self, torque):
        # 动作:扭矩 τ (N·m),范围 [-5, 5]
        # 简化动力学:τ = I * α,假设转动惯量 I = 1
        alpha = torque  # 角加速度
        dt = 0.01
        
        # 更新状态
        self.state[1] += alpha * dt  # 角速度
        self.state[0] += self.state[1] * dt  # 角度
        
        # 计算奖励:负的跟踪误差平方 - 能耗
        error = self.target - self.state[0]
        reward = -error**2 - 0.01 * torque**2
        
        # 判断是否终止
        done = abs(error) < 0.01
        
        return self.state, reward, done

这个例子虽然简单,但麻雀虽小五脏俱全。你可以在上面跑任何强化学习算法——DQN、PPO、SAC都行。

注意:实际机器人上千万别直接用这个简化动力学。真实系统有摩擦、柔性、延迟,你得用更精确的模型或直接上真实环境交互。我见过有人把简化模型训好的策略直接部署到真机上,结果机器人直接“抽风”——因为模型忽略了摩擦力矩。

好了,MDP的数学基础就聊到这儿。记住三个核心:状态告诉算法“你在哪”,动作告诉算法“你能做什么”,奖励告诉算法“做得好不好”。后面的章节,咱们会在这个框架上搭建真正的强化学习控制器。