简介:本资源是面向强化学习初学者与Matlab实践者的Actor-Critic算法完整实现代码包,聚焦于连续/离散控制任务中的策略优化与价值评估协同训练问题。压缩包共10个文件,含7个核心.m脚本(如AC.m主训练逻辑、simulator.m环境交互、evaluate.m策略评估)及3个.asv备份文件,总大小仅5KB,轻量紧凑、结构清晰,便于逐模块理解Actor网络策略梯度更新与Critic网络状态价值拟合的协同机制。已有460人下载学习,适用于高校课程设计、科研原型验证及RL算法原理复现。读者可直接运行示例流程,掌握Matlab中基于深度神经网络构建Actor-Critic框架的关键步骤——包括网络定义、环境采样、优势函数计算、双网络交替更新及Adam优化器配置,无需额外依赖,开箱即用。
1. 项目概述:Actor-Critic网络的Matlab实现
最近在整理资料时,翻出了一个几年前用Matlab实现的Actor-Critic网络项目压缩包。对于刚接触强化学习的朋友来说,Actor-Critic(演员-评论家)算法是一个绕不开的经典架构,它巧妙地将策略梯度(Policy Gradient)和价值函数(Value Function)学习结合起来,解决了像REINFORCE这类“纯”策略梯度方法方差高、学习慢的问题。这个Matlab实现,最初是为了验证一个简单的连续控制问题(比如倒立摆)而写的,麻雀虽小,五脏俱全。今天把它拿出来拆解一下,不仅是为了分享代码,更是想聊聊在Matlab这个“非主流”的深度学习环境中,从零搭建一个强化学习智能体时会遇到哪些坑,以及如何用最“Matlab”的思维去解决问题。无论你是想用Matlab做算法原型验证,还是单纯想深入理解Actor-Critic的内部工作机制,这篇内容应该都能给你一些直接的参考。
2. 核心原理与架构设计思路
2.1 Actor-Critic算法思想拆解
要理解这个Matlab项目,首先得搞明白Actor-Critic到底在干什么。你可以把它想象成一个学徒(Actor)和一位教练(Critic)的合作过程。学徒负责行动,比如控制机器人的关节;教练不直接行动,而是在旁边观察,对学徒的每一个动作打分,评价这个动作在当下局面有多好。
- 演员(Actor): 它的核心是一个策略网络(Policy Network)。输入是环境的状态(State),比如倒立摆的角度、角速度,输出是一个动作(Action),比如施加在小车上的力。在连续动作空间中,它通常输出动作的概率分布参数(如高斯分布的均值和方差),然后依此采样得到具体动作。
- 评论家(Critic): 它的核心是一个价值网络(Value Network)。输入同样是环境的状态,有时也包括Actor采取的动作,输出是一个标量值,代表当前状态(或状态-动作对)的长期期望回报,即状态价值函数 V(s) 或动作价值函数 Q(s, a)。Critic的目标是尽可能准确地预测未来收益。
它们如何协作?Agent在环境中交互,产生轨迹数据 (s, a, r, s‘)。Critic根据这些数据更新自己的价值估计,使其更准确。然后,Actor利用Critic提供的“评价”来更新自己的策略:如果Critic认为某个动作导致了比预期更好的结果(优势为正),Actor就增加未来在类似状态下选择该动作的概率;反之则减少。这个“优势”通常用时序差分误差(TD Error)来近似,即δ = r + γ * V(s') - V(s)。这个δ就是Critic给Actor的“学习信号”。
在这个Matlab项目中,我选择实现了最经典的A2C(Advantage Actor-Critic)框架,使用状态价值函数V(s)作为Critic,优势函数通过TD Error计算。之所以选A2C而不是更复杂的A3C(异步版本)或DDPG(深度确定性策略梯度),是因为在单机Matlab环境下,A2C的结构最清晰,易于理解和调试,足以验证算法在经典控制问题上的有效性。
2.2 Matlab环境下的实现考量
用Matlab实现深度学习模型,和用Python(PyTorch/TensorFlow)思路有很大不同。Python生态有自动微分、动态计算图,写起来更像是在声明数学公式。而Matlab,尤其是早几年的版本,更偏向于矩阵运算和过程式编程。这就需要我们更“手动”一些。
- 神经网络构建: 项目没有使用较新的Deep Learning Toolbox的
layerGraph或dlnetwork(如果版本够新,用这些会方便很多),而是采用了更基础的feedforwardnet或手动矩阵运算来构建简单的多层感知机(MLP)。这虽然原始,但让你对网络前向传播、反向传播的每一步都清清楚楚。 - 自动微分 vs 手动梯度: 在策略梯度更新中,我们需要计算损失函数对策略网络参数的梯度。在Python里一行
loss.backward()搞定。在Matlab里,我们需要根据策略梯度定理,手动推导出梯度公式,并用矩阵运算实现。这恰恰是理解算法精髓的好机会。 - 数据管理与训练循环: 强化学习的数据是时序相关的、非独立同分布的。我们需要自己设计经验回放缓冲区(Replay Buffer)来存储和采样轨迹片段(trajectory)。在Matlab中,这通常用结构体数组(struct array)或元胞数组(cell array)来实现。
- 与仿真环境的交互: 项目通常需要连接一个仿真环境,如Cart-Pole(倒立摆)。Matlab有自己的Simulink可以建模,但为了轻量和聚焦算法,我更喜欢用预定义的动力学方程自己写一个简单的环境函数,输入状态和动作,输出下一状态和奖励。
这个项目的设计思路,就是在Matlab的编程范式内,清晰地复现A2C算法的数据流:环境交互收集数据 -> Critic网络评估优势 -> Actor网络依据优势更新策略 -> 循环。所有环节都暴露出来,没有黑盒。
3. 代码结构解析与核心模块详解
解压actor-critic网络 Matlab.zip后,你会看到几个主要的.m文件。我们来逐一拆解其功能和实现细节。
3.1 主训练脚本 (main_train.m)
这是整个项目的入口和控制中心。它通常包含以下步骤:
% 1. 初始化参数 env = createCartPoleEnv(); % 创建环境 actor = initActorNetwork(state_dim, action_dim); % 初始化Actor网络 critic = initCriticNetwork(state_dim); % 初始化Critic网络 hyperparams = setHyperparameters(); % 设置超参数:学习率、折扣因子等 % 2. 训练循环 for episode = 1:max_episodes state = env.reset(); episode_reward = 0; trajectory = []; % 用于存储本回合的(s, a, r, s')序列 while ~env.isDone() % 3. Actor根据状态选择动作 action = actor.select_action(state); % 4. 与环境交互 [next_state, reward, done] = env.step(action); % 5. 存储转移样本 trajectory = [trajectory; struct('state', state, 'action', action, ... 'reward', reward, 'next_state', next_state, 'done', done)]; state = next_state; episode_reward = episode_reward + reward; end % 6. 回合结束后,进行学习更新 [actor, critic] = a2c_update(actor, critic, trajectory, hyperparams); % 7. 记录和输出 fprintf('Episode %d, Total Reward: %.2f\n', episode, episode_reward); end关键点:
createCartPoleEnv: 这个函数封装了倒立摆的物理模型。状态是4维向量 [小车位置, 小车速度, 杆角度, 杆角速度]。动作是施加在小车上的力(离散的向左/向右,或连续的力值)。奖励函数设计为杆子保持直立的时间越长,奖励越高。actor.select_action: 这里体现了策略的网络输出。对于连续动作,Actor网络输出均值和方差,然后使用normrnd函数进行高斯采样,得到具体动作值。同时,为了探索,需要在采样时加入噪声,或者直接使用输出分布的方差。a2c_update: 这是核心的学习函数,我们稍后详细展开。
3.2 Actor与Critic网络定义 (networks.m)
在这个文件中,我们定义了Actor和Critic网络的结构。为了清晰,我采用了手动初始化权重和实现前向传播的方式。
function actor = initActorNetwork(state_dim, action_dim) % 定义一个简单的两层MLP作为策略网络 hidden_size = 128; % 第一层权重和偏置 actor.W1 = randn(state_dim, hidden_size) * sqrt(2/state_dim); % He初始化 actor.b1 = zeros(1, hidden_size); % 第二层(输出层)权重和偏置 - 输出均值和log_std actor.W2_mean = randn(hidden_size, action_dim) * sqrt(2/hidden_size); actor.b2_mean = zeros(1, action_dim); actor.W2_logstd = randn(hidden_size, action_dim) * sqrt(2/hidden_size); actor.b2_logstd = zeros(1, action_dim); actor.activation = @relu; % 使用ReLU激活函数 end function [action, log_prob] = actor_forward(actor, state) % 前向传播 h1 = state * actor.W1 + actor.b1; h1 = actor.activation(h1); mean = h1 * actor.W2_mean + actor.b2_mean; log_std = h1 * actor.W2_logstd + actor.b2_logstd; log_std = max(log_std, -20); % 防止log_std过小,数值不稳定 std = exp(log_std); % 采样动作 z = randn(size(mean)); action = mean + std .* z; % 计算该动作的对数概率(用于策略梯度) log_prob = -0.5 * sum(z.^2 + 2*log_std + log(2*pi), 2); endCritic网络的结构类似,但输出层只有一个神经元,输出标量状态价值V(s)。网络初始化方式相同。
注意: 这里手动实现网络,主要是为了教学透明。在实际应用中,如果Matlab版本支持(R2020a以后),强烈建议使用
fullyConnectedLayer,reluLayer等构建层图,并用dlarray和dlgradient进行自动微分,这会大大简化代码并提升效率。
3.3 核心更新算法 (a2c_update.m)
这是算法的引擎。它接收一个轨迹(或一批轨迹),计算优势,然后更新两个网络。
function [actor, critic] = a2c_update(actor, critic, trajectory, hyperparams) gamma = hyperparams.gamma; % 折扣因子 lr_actor = hyperparams.lr_actor; lr_critic = hyperparams.lr_critic; states = cat(1, trajectory.state); actions = cat(1, trajectory.action); rewards = cat(1, trajectory.reward); next_states = cat(1, trajectory.next_state); dones = cat(1, trajectory.done); % 1. 计算每个状态的价值 V(s) 和 V(s') values = critic_forward(critic, states); next_values = critic_forward(critic, next_states); % 2. 计算TD目标(Target)和TD误差(Advantage) % 对于终止状态,next_value为0 next_values(dones) = 0; targets = rewards + gamma * next_values; advantages = targets - values; % 这就是A(s,a)的近似 % 3. 更新Critic网络(最小化均方误差) critic_loss = mean((targets - values).^2); % 这里需要手动计算梯度并更新,简化示例使用梯度下降思想 % 实际更应用基于损失函数的梯度下降,以下为示意逻辑 [critic_grad] = compute_critic_gradient(critic, states, targets); % 需实现梯度计算函数 critic = update_critic_weights(critic, critic_grad, lr_critic); % 需实现权重更新函数 % 4. 更新Actor网络(最大化期望回报,使用策略梯度) % 策略梯度: grad J ≈ E[ grad log π(a|s) * A(s,a) ] [~, log_probs] = actor_forward(actor, states, actions); % 需要扩展函数以接收特定action计算log prob actor_loss = -mean(log_probs .* advantages); % 取负号,因为我们要最小化这个损失(等价于最大化J) [actor_grad] = compute_actor_gradient(actor, states, actions, advantages); % 需实现梯度计算函数 actor = update_actor_weights(actor, actor_grad, lr_actor); % 需实现权重更新函数 end核心要点:
- 优势估计: 这里使用了最简单的
TD Error作为优势函数A(s,a)的估计。更稳定的方法可以是使用GAE(广义优势估计),但在这个基础版本中,TD Error已经能工作。 - 损失函数: Critic的损失是价值预测的均方误差(MSE)。Actor的损失是策略梯度损失,即
-log_prob * advantage的均值。注意我们加了负号,因为在优化库中我们通常最小化损失函数。 - 手动梯度:
compute_critic_gradient和compute_actor_gradient是难点。对于MSE损失,Critic的梯度是2*(values - targets) * d(values)/d(W)。对于Actor,根据策略梯度定理,其梯度是advantage * d(log_prob)/d(W)。你需要根据网络结构,应用链式法则手动推导出这些梯度对每一层权重的表达式。这是Matlab实现中最繁琐但也最锻炼人的部分。
3.4 环境交互模块 (cartpole_env.m)
这个文件实现了倒立摆的动力学。使用欧拉积分法对微分方程进行离散化模拟。
function [next_state, reward, done] = cartpole_step(state, action) % 解析状态参数 x = state(1); x_dot = state(2); theta = state(3); theta_dot = state(4); force = action; % 物理常数 g = 9.8; mc = 1.0; mp = 0.1; l = 0.5; dt = 0.02; % 计算动力学(省略具体导数方程,参考经典cart-pole模型) % ... 这里是一系列根据牛顿力学推导的公式,计算角加速度theta_ddot和车加速度x_ddot theta_ddot = (g*sin(theta) + cos(theta)*((-force - mp*l*theta_dot^2*sin(theta))/(mc+mp))) / (l*(4/3 - (mp*cos(theta)^2)/(mc+mp))); x_ddot = (force + mp*l*(theta_dot^2*sin(theta) - theta_ddot*cos(theta))) / (mc+mp); % 欧拉积分更新状态 x_dot = x_dot + x_ddot * dt; x = x + x_dot * dt; theta_dot = theta_dot + theta_ddot * dt; theta = theta + theta_dot * dt; next_state = [x; x_dot; theta; theta_dot]; % 奖励和终止条件 reward = 1.0; % 只要没倒下,每一步都给1分 done = abs(x) > 2.4 || abs(theta) > pi/15; % 车超出界限或杆子倒下太多 end这个环境实现非常关键,它的数值稳定性和计算速度直接影响训练效率。dt(时间步长)不宜过大,否则模拟会失真。
4. 训练流程、参数调优与实操记录
4.1 完整的训练流程与可视化
在主训练循环中,除了更新网络,我们还需要监控训练过程。一个简单的做法是记录每回合的总奖励,并定期绘制学习曲线。
% 在主循环中添加记录 all_rewards = zeros(max_episodes, 1); for episode = 1:max_episodes % ... 交互与更新代码 ... all_rewards(episode) = episode_reward; % 每100回合绘制一次学习曲线 if mod(episode, 100) == 0 figure(1); plot(movmean(all_rewards(1:episode), 30), 'b-', 'LineWidth', 1.5); % 使用移动平均平滑曲线 xlabel('Episode'); ylabel('Smoothed Reward'); title('Training Progress'); grid on; drawnow; end end训练开始时,奖励会很低(杆子很快倒下)。随着学习进行,曲线应呈现上升趋势,并最终稳定在一个较高的值附近(例如,倒立摆能持续平衡200步以上)。
4.2 关键超参数解析与调优心得
Actor-Critic的性能对超参数非常敏感。以下是我在这个Matlab项目中反复调试得出的经验:
| 超参数 | 典型值/范围 | 作用与影响 | 调优心得 |
|---|---|---|---|
| 学习率 (lr_actor, lr_critic) | 1e-4 到 1e-3 | 控制网络权重更新步长。Actor和Critic通常需要不同的学习率。 | Critic的学习率通常应略大于或等于Actor的,因为需要Critic快速提供准确的价值估计,Actor才能有效学习。可以先从lr_critic=3e-4,lr_actor=1e-4开始尝试。学习率过大容易导致训练不稳定(奖励曲线剧烈震荡),过小则学习缓慢。 |
| 折扣因子 (gamma) | 0.95 到 0.99 | 衡量未来奖励的重要性。越接近1,智能体越有远见。 | 对于Cart-Pole这类回合制、目标明确的稀疏奖励任务,可以设得较高(如0.99)。如果任务奖励密集,可以适当降低。gamma过高可能导致训练初期不稳定,因为远期回报的不确定性大。 |
| 网络结构 | [128], [64,64] | Actor和Critic网络的隐藏层大小和层数。 | 不宜过深。对于简单任务如Cart-Pole,一层128个神经元或两层64个神经元足够。更深的网络不仅增加计算量,在Matlab手动反向传播时也更易出错。先从一个较小的网络开始。 |
| 探索策略 | 通过输出分布的方差控制 | 控制动作的随机性,影响探索与利用的平衡。 | 在连续动作空间中,探索主要通过策略网络输出的动作分布方差实现。可以设置一个初始方差(或log_std),并让其参与学习,也可以随时间衰减。实践中,让网络输出log_std并学习效果更好。注意用max函数限制其最小值,防止方差过小导致数值下溢。 |
| 回合长度/缓冲区 | 整个回合 | 每次用整个回合的数据进行更新。 | 这是A2C与使用经验回放(Replay Buffer)的DQN、DDPG的主要区别之一。A2C属于在线策略,通常使用当前策略采样的完整轨迹进行更新。在Matlab实现中,这简化了数据管理。 |
实操心得: 调试时,首先确保Critic能学。你可以先固定Actor的策略(比如随机策略),单独训练Critic几十个回合,观察它预测的V(s)是否随着接近回合结束而递减(因为快要失败了),并且值的大小是否合理。如果Critic都学不好,Actor得到的优势信号就是噪声。
4.3 训练过程常见现象与诊断
奖励曲线不上升,在低点徘徊:
- 可能原因: 学习率太小;网络结构能力不足;探索方差太小,智能体被困在局部最优;Critic完全学废了,给出的优势信号全是错的。
- 排查: 检查Critic的预测值
V(s),是否在合理范围(对于Cart-Pole,最大值应接近回合最大步数)。增大探索方差(初始log_std)。尝试增大学习率(特别是Critic的)。
奖励曲线剧烈震荡,时高时低:
- 可能原因: 学习率太大;回合数据相关性太强,导致每次更新方向变化剧烈(A2C的固有问题)。
- 排查: 显著降低学习率(一个数量级)。可以考虑引入多个环境并行采集数据(模拟A3C的思想),用不同批的数据平均化更新方向,这在Matlab中可以用
parfor循环尝试,但要注意数据同步。
训练后期性能突然崩溃(Collapse):
- 可能原因: 这是策略梯度方法的常见问题。策略更新步长不当,导致策略分布急剧变化,变得很差,而Critic来不及适应新的数据分布。
- 排查: 引入策略约束,如PPO(近端策略优化)中的裁剪(Clipping)机制。在我们的基础A2C中,可以尝试对Actor的更新梯度进行裁剪(
grad = max(min(grad, clip_value), -clip_value)),或者使用更小的Actor学习率。
5. 项目扩展、问题排查与进阶思考
5.1 从A2C到更高级算法的扩展思路
这个基础项目是一个完美的起点,你可以基于它实现更强大的算法:
- 引入经验回放(Replay Buffer): 将A2C改造成离线策略算法。增加一个缓冲区,存储大量
(s, a, r, s', done)转移样本。更新时从缓冲区中随机采样一批数据,打破数据间的相关性,能使训练更稳定。这需要修改a2c_update函数的数据来源。 - 实现PPO(近端策略优化): PPO是A2C的工业级升级版。核心是在Actor的损失函数中增加一个裁剪项,限制新旧策略的差异不能太大。你需要额外存储旧策略的概率,并在更新时计算概率比。这能有效防止训练崩溃。
- 尝试DDPG(深度确定性策略梯度): 如果动作空间是连续的,且希望得到确定性策略,DDPG是更好的选择。它需要引入目标网络(Target Network)和延迟更新来稳定训练。你需要将Actor改为直接输出确定性动作,并实现软更新(
θ_target = τ * θ + (1-τ) * θ_target)。
5.2 典型错误与调试技巧实录
在手动实现过程中,我踩过不少坑,这里记录几个典型的:
问题:梯度爆炸,网络输出很快变成NaN。
- 排查: 首先检查激活函数。ReLU在深度网络中可能导致梯度爆炸,可以尝试改用
tanh。其次,检查手动计算的梯度公式,特别是链式法则有没有写错。一个有效的调试方法是梯度检验(Gradient Checking):对某个参数施加一个微小扰动,计算损失函数的变化,与手动计算的梯度进行比较,两者应该非常接近。最后,检查优势A(s,a)的值是否过大,可以考虑对优势进行归一化(减去均值,除以标准差)。
- 排查: 首先检查激活函数。ReLU在深度网络中可能导致梯度爆炸,可以尝试改用
问题:智能体早期就学到“神风特攻队”策略,比如让车疯狂加速撞墙结束回合。
- 分析: 这可能是因为奖励函数设计有缺陷。在Cart-Pole中,如果只在失败时给一个大的负奖励,智能体可能发现“快速失败”比“艰难平衡”更容易获得可预测的回报(尽管是负的)。稀疏奖励问题。
- 解决: 改为每一步都给予小的正奖励(存活奖励),失败时给予一个终止惩罚。这样智能体才有动力延长存活时间。
问题:Matlab训练速度极慢。
- 分析: 手动实现的循环和矩阵运算,在Matlab中对于大规模数据效率不高。
- 优化:
- 向量化: 确保
actor_forward和critic_forward能一次性处理一批(batch)数据,而不是在循环中单个处理。 - 预分配数组: 在存储轨迹时,预先根据最大步长分配好数组空间,避免在循环中动态增长数组。
- 使用内置函数: 尽可能使用Matlab内置的矩阵运算函数,避免
for循环。 - 考虑迁移: 如果性能成为瓶颈,可以考虑将核心网络部分用MEX函数(C/C++)实现,或者直接转向使用Deep Learning Toolbox的自动微分功能。
- 向量化: 确保
5.3 项目总结与资源利用
这个用Matlab手搓的Actor-Critic项目,虽然性能上无法与PyTorch/TensorFlow的成熟实现相比,但其教育价值巨大。它强迫你理解算法每一个矩阵乘法和梯度计算的细节。当你亲手调试出一个能稳定平衡倒立摆的智能体时,那种对算法内在机理的透彻理解,是直接调用库函数无法比拟的。
对于想继续深入的朋友,我建议:
- 复现并调通: 先确保这个基础版本能在你的Matlab上运行起来,并成功训练Cart-Pole。
- 更换环境: 尝试将其应用到其他经典控制环境,如MountainCar、Pendulum。这需要你修改环境接口和状态/动作空间的定义。
- 算法升级: 选择上述一个扩展方向(如Replay Buffer或PPO裁剪),动手实现它,并观察训练稳定性和性能的提升。
- 利用新工具: 如果你使用的是较新版本的Matlab(R2020a+),强烈建议用Deep Learning Toolbox重构网络部分。使用
dlnetwork定义网络,用dlgradient计算梯度,代码会简洁优雅得多,并且能利用GPU加速。
最后,这个项目的所有代码都应该是模块化的,环境、网络、算法更新逻辑分离清晰。这样的结构不仅便于调试,也方便你未来将其作为模板,快速实验其他强化学习想法。在Matlab的天地里玩转强化学习,本身就是一种独特的乐趣和挑战。
本文还有配套的精品资源,点击获取