1. 从零理解MountainCarContinuous-v0与PPO算法
如果你刚开始接触强化学习,看到MountainCarContinuous-v0这个环境名可能会有点懵。简单来说,这就是一个“小车爬山”的模拟游戏。想象一下,你的车被困在一个U型山谷的底部,发动机马力不够,没法直接冲上右边的山顶。你的任务就是通过控制油门(给一个向左或向右的加速度),让小车通过左右摆动积累动量,最终“荡”到山顶。这个环境的难点在于,小车自身的动力不足以直接克服重力爬坡,必须学会“荡秋千”的策略,这非常考验智能体的探索和学习能力。
为什么这个环境在强化学习社区里这么经典?因为它有几个典型的挑战:稀疏奖励和连续动作空间。所谓稀疏奖励,就是小车在到达山顶之前,每一步得到的奖励都是负数(比如-0.1),只有成功登顶才会获得一个大的正奖励(比如+100)。这就像在茫茫大海里找一座小岛,大部分时间你都在“受罚”,只有找到目标才能“得救”,学习信号非常微弱。而连续动作空间意味着,你的控制输出不是一个简单的“左”或“右”的离散指令,而是一个在[-1, 1]区间内任意取值的实数,代表施加的力的大小和方向。这比离散动作复杂得多。
为了解决这类问题,我们请出了今天的主角:PPO(近端策略优化)算法。PPO是OpenAI在2017年提出的一种策略梯度算法,它之所以流行,核心在于两个字:“稳定”。在它之前,很多策略梯度方法像一匹难以驯服的野马,学习过程容易崩溃,参数更新步长稍微大一点,性能就可能一落千丈。PPO通过一个巧妙的“裁剪”机制,把每次策略更新的幅度限制在一个安全的范围内,从而保证了训练的稳定性。对于MountainCarContinuous-v0这种需要精细控制力度的环境,PPO的稳定特性让它成为了一个非常合适的选择。接下来,我们就一起动手,看看如何把这匹“好马”驯服,让它在这个经典环境里跑出好成绩。
2. 搭建你的第一个PPO智能体:代码骨架详解
纸上谈兵终觉浅,咱们直接上代码。下面我会带你一步步搭建PPO智能体的核心部分,并解释每一块“积木”的作用。别担心,我会尽量用大白话把原理讲清楚。
首先,智能体的大脑由两个神经网络组成:Actor(演员) 和 Critic(评论家)。你可以把Actor想象成赛车手,它负责做决策,根据当前的路况(状态)决定踩多大油门(动作)。而Critic则像副驾驶的教练,它不直接开车,而是评价赛车手当前所处位置的价值有多高,为后续的策略更新提供指导。
我们先来看看Actor网络是怎么构建的。它的输入是小车的状态(位置和速度两个数字),输出是一个动作分布。在连续动作空间里,我们通常假设这个分布是高斯分布(正态分布),所以网络需要输出两个东西:动作的均值(mu)和标准差(std)。均值决定了最可能采取的动作,标准差则代表了探索的随机性大小。一开始标准差可以大一些,让小车多尝试各种动作;随着学习进行,标准差会逐渐减小,策略趋于稳定。
import torch
import torch.nn as nn
import torch.nn.functional as F
def orthogonal_init(layer, gain=1.0):
"""正交初始化,一个小技巧,能让网络训练更稳定"""
nn.init.orthogonal_(layer.weight, gain=gain)
nn.init.constant_(layer.bias, 0)
class Actor(nn.Module):
def __init__(self, state_dim, action_dim):
super(Actor, self).__init__()
self.fc1 = nn.Linear(state_dim, 128)
self.fc2 = nn.Linear(128, 128)
self.mu_head = nn.Linear(128, action_dim)
# 将log_std定义为可学习参数,确保标准差总是正数
self.log_std = nn.Parameter(torch.zeros(action_dim))
# 使用正交初始化
orthogonal_init(self.fc1)
orthogonal_init(self.fc2)
orthogonal_init(self.mu_head, gain=0.01) # 输出层增益小一点
def forward(self, x):
x = torch.tanh(self.fc1(x))
x = torch.tanh(self.fc2(x))
mu = torch.tanh(self.mu_head(x)) # 用tanh把均值限制在[-1,1]之间,符合动作空间要求
std = torch.exp(self.log_std) # 取指数,得到正的标准差
return mu, std
def get_dist(self, state):
"""根据状态生成动作分布"""
mean, st


4166

被折叠的 条评论
为什么被折叠?



