4. 策略梯度方法:REINFORCE、Actor-Critic 算法,为什么适合连续动作空间

聊到强化学习在投资组合中的应用,很多朋友第一反应就是DQN。嗯,DQN确实经典,但它有个硬伤——只能处理离散动作。你想想看,在真实交易中,我们调整仓位是0.1%、0.5%还是2.3%?这些连续值DQN根本搞不定。

这时候,策略梯度方法就派上用场了。我个人习惯把这类方法称为「端到端的策略学习器」——它直接输出动作的概率分布,而不是先评估每个动作的价值再选。说白了,就是让神经网络自己学会「在什么情况下该下多重的注」。

4.1 为什么连续动作空间需要策略梯度?

先讲个我踩过的坑。几年前我在做期货CTA策略时,想用DQN来控制仓位。结果动作空间设了10档:0%, 10%, 20%... 100%。训练出来的策略永远在极端值之间跳来跳去——要么满仓,要么空仓。为什么?因为DQN的本质是argmax,它只会选Q值最高的那个离散动作。

连续动作空间的核心挑战在于:

  • 维度灾难:如果每个资产有100档仓位,10个资产就是100^10种组合,DQN根本存不下
  • 动作相关性:仓位从10%调到11%和调到90%,在DQN看来是两个完全独立的动作,学不到「微调」的概念
  • 泛化能力:策略梯度直接学习策略函数π(a|s),天然支持连续输出

核心洞察:策略梯度方法不计算Q值,而是直接优化策略参数θ,使得累积回报的期望最大化。这意味着它可以在连续空间中「平滑地」调整动作。

4.2 REINFORCE 算法:最朴素的策略梯度

REINFORCE,也叫蒙特卡洛策略梯度。它的思路非常直白:用完整的episode回报来更新策略。公式长这样:

∇θ J(θ) = E[ Σ ∇θ log π(a_t|s_t) * G_t ]

其中 G_t = Σ γ^(k-t) * r_k  (从t时刻开始的折扣累积回报)

我在项目中第一次实现REINFORCE时,犯了个低级错误——忘了对回报做归一化。结果训练曲线抖得像心电图。后来我加了个技巧:用当前batch的回报均值做baseline,方差瞬间降下来了。

REINFORCE的优缺点很明显:

优点 缺点
实现简单,逻辑清晰 方差大,训练不稳定
无偏估计 只能离线更新(完整episode后)
适合 episodic 任务 样本效率低

实战技巧:在投资组合场景中,我建议把每个交易周期(比如一个月)当作一个episode。这样G_t就是该周期内的累计收益率,物理意义很清晰。

4.3 Actor-Critic:让策略梯度真正可用

REINFORCE的问题在于,它用完整的episode回报来评估动作好坏。这就像你打完一场篮球赛,才说「刚才那个投篮选择不好」——太晚了!

Actor-Critic的思路是:一边做动作(Actor),一边实时评价(Critic)。Critic学一个价值函数V(s)或Q(s,a),用来替代REINFORCE中的G_t。这样每个时间步都能更新,方差大幅降低。

我画个图帮你理解:

状态 s_t Actor 网络 π(a|s; θ) Critic 网络 V(s; w) 动作 a_t (仓位调整) TD 误差 δ = r + γV(s') - V(s) 环境 共享状态 r_t, s_{t+1} TD误差反馈 Actor-Critic 架构图 Actor生成动作,Critic评估动作,TD误差驱动更新

看到这个架构图你就明白了:Actor负责「怎么做」,Critic负责「做得怎么样」。Critic给出的TD误差δ = r + γV(s') - V(s),就是Actor的「老师」——如果δ为正,说明这个动作比预期好,下次多选它;反之则少选。

4.4 为什么Actor-Critic适合连续动作空间?

这个问题我经常被问到。其实答案就藏在Actor网络的输出层设计里。

对于连续动作空间,Actor网络的输出层通常是一个高斯分布的参数——均值和方差。比如:

class Actor(nn.Module):
    def __init__(self, state_dim, action_dim):
        super().__init__()
        self.fc = nn.Sequential(
            nn.Linear(state_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU()
        )
        self.mean_head = nn.Linear(64, action_dim)
        self.log_std_head = nn.Linear(64, action_dim)
    
    def forward(self, state):
        x = self.fc(state)
        mean = torch.tanh(self.mean_head(x))  # 仓位在[-1, 1]之间
        log_std = self.log_std_head(x)
        std = torch.exp(log_std.clamp(-20, 2))
        return mean, std

这样设计的好处是:

  • 输出是连续的:均值可以是0.372这样的任意值,不是离散档位
  • 自带探索:通过标准差控制探索程度,不像DQN需要ε-greedy
  • 可微的采样:使用重参数化技巧,让采样过程可导,梯度能回传

我曾经踩过的坑:在训练初期,Actor的标准差会快速坍缩到0,导致策略失去探索能力。解决方案是给log_std加一个熵正则项,强制保留一定的随机性。具体做法是在loss里加上 -β * H(π),其中H是策略的熵。

4.5 投资组合中的实战配置

在实际项目中,我通常这样配置Actor-Critic:

组件 配置 说明
状态空间 过去N天的收益率、波动率、相关性矩阵 我习惯用20天窗口,太长会滞后
动作空间 各资产的权重向量,和为1 用Softmax或归一化保证约束
奖励函数 夏普比率 + 最大回撤惩罚 纯收益率会导致过度冒险
Critic网络 3层MLP,256-128-64 输出标量V(s)
学习率 Actor: 3e-4, Critic: 1e-3 Critic学得快一点,给Actor稳定反馈

我的个人习惯:在训练初期,我会把Actor的学习率设得比Critic小一个数量级。这样Critic先学会准确评估,Actor再慢慢调整策略。就像你先让裁判熟悉规则,再让运动员上场——顺序很重要。

4.6 代码实战:最小化Actor-Critic

给你一个可以直接跑的最小实现。这个版本去掉了所有花哨技巧,只保留核心逻辑:

import torch
import torch.nn as nn
import torch.optim as optim

class ActorCritic:
    def __init__(self, state_dim, action_dim, lr_actor=3e-4, lr_critic=1e-3):
        self.actor = Actor(state_dim, action_dim)
        self.critic = Critic(state_dim)
        self.opt_actor = optim.Adam(self.actor.parameters(), lr=lr_actor)
        self.opt_critic = optim.Adam(self.critic.parameters(), lr=lr_critic)
        self.gamma = 0.99
    
    def update(self, state, action, reward, next_state, done):
        # 计算TD目标
        with torch.no_grad():
            target = reward + (1 - done) * self.gamma * self.critic(next_state)
        
        # 更新Critic
        current = self.critic(state)
        critic_loss = nn.MSELoss()(current, target)
        self.opt_critic.zero_grad()
        critic_loss.backward()
        self.opt_critic.step()
        
        # 更新Actor
        td_error = target - current  # 这就是Critic给Actor的反馈
        mean, std = self.actor(state)
        dist = torch.distributions.Normal(mean, std)
        log_prob = dist.log_prob(action).sum(dim=-1)
        actor_loss = -(log_prob * td_error.detach()).mean()
        
        self.opt_actor.zero_grad()
        actor_loss.backward()
        self.opt_actor.step()
        
        return critic_loss.item(), actor_loss.item()

这段代码虽然短,但包含了Actor-Critic的全部精髓。你把它嵌入到投资组合的回测框架里,就能跑起来了。

4.7 总结一下

策略梯度方法之所以适合连续动作空间,根本原因在于它直接学习策略的分布参数,而不是枚举所有动作的价值。REINFORCE是基础,但方差太大;Actor-Critic通过引入价值函数作为baseline,大幅提升了稳定性。

在实际项目中,我建议你从Actor-Critic入手,先跑通一个最小版本,再逐步加入GAE、PPO等改进。记住:在投资组合中,连续仓位调整带来的边际收益,往往比离散动作高出几个百分点——这就是策略梯度的价值所在。

一句话总结:策略梯度让AI学会「该下多重的注」,而不是「该下注还是不下注」——这对投资组合管理来说,是天壤之别。

专注资料整理