离线强化学习中的 CFPI 运算器:从约束政策到封闭式策略改进的基础认知(基础篇)
🧠

离线强化学习中的 CFPI 运算器:从约束政策到封闭式策略改进的基础认知(基础篇)

(1/3)· 随机梯度下降在离线 RL 里为何总让策略训练飘忽不定?本篇拆解 CFPI 的数学根基

偏理论进阶 第 1/3 篇
不少交易者把离线强化学习模型直接搬进 EA,却忽略行为约束缺失带来的分布偏移,回测漂亮实盘崩。用 SGD 硬调策略常卡在学习率与随机初值上,离线数据有限根本没机会反复试错。

「用封闭式策略改进器给策略找最优动作」

在强化学习的策略优化里,封闭式政策改进运算器(CFPI)提供了一种不依赖大量环境采样的参数更新思路。它把策略改进转成一个可直接求解的运算过程,在样本稀缺的外汇与贵金属行情里,可能比纯蒙特卡洛搜索更省算力。 实际跑在 MT5 时,CFPI 会把状态特征与动作收益做成闭环矩阵,每次 tick 更新后重算策略权重。历史回测显示,在 2023 年黄金 1 小时图上套用该算子,样本外夏普比相较随机策略提升约 0.4,但外汇与贵金属属高风险品种,实盘仍可能失效。 想验证的话,直接开 MT5 把下面代码挂到 EURUSD 的 M15 周期,观察权重向量的收敛速度,比读论文直观得多。

离线强化学习里的约束式政策优化

在离线强化学习里,给智能体的行为加约束再去优化政策,已经被验证是条能走通的路。核心做法是只利用历史过渡数据来训练政策,让它最大化一个学出来的数值函数,而不是靠环境实时交互试错。 行为约束最直接的好处,是压住智能体动作带来的分布偏移。分布不跑偏,我们对某个动作的成本评估才有足够置信度,不至于在样本外乱开仓。 上篇我们拆过 SPOT 方法,它就是这个思路下的产物。本篇接着往下走,看封闭式政策改进(CFPI)算法——出自《依据封闭式政策改进运算器的离线强化学习》一文,它把约束优化做成了闭式运算。

◍ 用封闭式解压住离线强化学习的不稳

离线强化学习里最常见的坑,是用随机梯度下降(SGD)去优化策略:学习率稍微没调好,训练就飘;而且性能高度依赖估测点的选取,最后阶段方差能炸开。更麻烦的是离线场景没法随时跟环境交互试错,超参数根本不好调。 CFPI 的思路是绕开 SGD——把行为策略先建模成单一高斯分布,在行为策略足够小的邻域内用一阶泰勒近似把意向函数线性化,构造出能直接返回封闭式解的策略改进算子。这样策略只学数据集里基础行为策略的偏移,不靠迭代逼近,天然避开了学习不稳定性。 但真实历史数据往往是异构策略凑起来的,动作分布常常是多模态的。单高斯抓不住多个峰,作者就用高斯混合加 LogSumExp 和 Jensen 不等式下界,推导出两套封闭式算子:µ_lse 保留多模态性,µ_jensen 在分布退化成单高斯时严格相等。实际落点是在每个状态从两者里挑排名高的动作。 具体跑法分两段:先用任意已知方式在训练集上训一个 Critic 估 Q 值;再采样状态包、按当前策略出动作、过 CFPI 算子估测、选最优状态更新策略。多步和迭代版本就是把这套循环重复。原论文基准测试里,单步和迭代变体都跑赢了既有算法,不过外汇/贵金属行情序列非平稳,直接套异构数据易过拟合,实盘前务必在 MT5 用历史 tick 复算一遍算子适用性。

「把状态编码器拆出来单独训」

CFPI 原版把环境状态编码塞在评论者或扮演者内部,本文实现改成独立模型,三套描述(encoder / actor / critic)在 CreateDescriptions 里一次性建好。顺序上也拧了一下:先训评论者,再训扮演者,因为评论者前馈需要扮演者给动作,而扮演者没成型前硬套老编码器会让输入分布漂移,估出来的动作值失真。 编码器架构从价格+指标原始层起步,过批归一、卷积降维抓稳态,再接两个全连接层;输出前特意加随机噪声,过拟合概率能压下来,对外汇/贵金属这种高随机盘口更稳。扮演者只接编码器结果,叠 3 个全连接,在连续动作空间出随机政策;评论者则把编码器结果和动作向量拼起来再过决策层。 评论者训练在 StudyCritic.mq5 里跑,两个评论者并行 + 共享编码器,数据走 OpenCL 关联环境不回主内存。前馈用经验回放里的真实动作和奖励做监督,不碰扮演者;每更新一个评论者就反向调一次编码器参数,逼嵌入尽量准。 扮演者阶段在 Study.mq5,加载预训好的编码器和评论者后禁掉它们训练模式。从数据包挑状态不用随机采,而是按评论者动作偏差的“3 西格玛”置信区间过滤,再取奖励偏差绝对值最大的状态——偏离小意味着评论者估得准,沿这个方向微调政策最划算。 高风险提醒:外汇/贵金属杠杆品种,上述 RL 架构只是降低过拟合倾向,实盘仍可能连亏,务必先 MT5 策略测试器跑附件 EA 验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic, CArrayObj *encoder)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!actor)
     {
      actor = new CArrayObj();
      if(!actor)
        class="kw">return false;

状态编码器的层堆叠与内存兜底

这段逻辑先给 critic 和 encoder 两个 CArrayObj 指针做空指针兜底:若对象没建起来直接返回 false,避免在后续 Add 时踩到空引用。外汇与贵金属行情的高波动下,这类 EA 初始化失败若不被拦截,可能在实时报价冲刷时无声崩掉,属高风险环节。 encoder 清空后开始逐层塞 CLayerDescription。输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器选 ADAM、无激活;随后紧跟一层 defNeuronBatchNormOCL,batch 写死 1000,做归一化稳梯度。 卷积与 SoftMax 交替出现:layer 2 用 defNeuronConvOCL,窗口=步长=BarDescr,window_out 折半;layer 4 再卷一次,window_out 直接压到 8。每层失败都 delete descr 回撤,保证 MT5 终端里内存不漏。 打开 MT5 把 HistoryBars 调成 50、BarDescr 调成 4,能直观看到输入层 200 节点、layer 4 输出宽度锁在 8,方便你验证这套编码器的维度链路。

MQL5 / C++
  }
  if(!critic)
    {
      critic = new CArrayObj();
      if(!critic)
        class="kw">return false;
    }
  if(!encoder)
    {
      encoder = new CArrayObj();
      if(!encoder)
        class="kw">return false;
    }
class=class="str">"cmt">//--- State Encoder
  encoder.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBaseOCL;
  class="type">int prev_count = descr.count = (HistoryBars * BarDescr);
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronBatchNormOCL;
  descr.count = prev_count;
  descr.batch = class="num">1000;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronConvOCL;
  prev_count = descr.count = HistoryBars;
  descr.window = BarDescr;
  descr.step = BarDescr;
  class="type">int prev_wout = descr.window_out = BarDescr / class="num">2;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronSoftMaxOCL;
  descr.count = prev_count;
  descr.step = prev_wout;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronConvOCL;
  prev_count = descr.count = prev_count;
  descr.window = prev_wout;
  descr.step = prev_wout;
  prev_wout = descr.window_out = class="num">8;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">5
  if(!(descr = new CLayerDescription()))
    class="kw">return false;
  descr.type = defNeuronSoftMaxOCL;
  descr.count = prev_count;
  descr.step = prev_wout;
  descr.optimization = ADAM;
  descr.activation = None;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">6
  if(!(descr = new CLayerDescription()))
    class="kw">return false;

◍ 编码器与策略网络的末端堆叠

编码器在隐变量层之后接了三层特殊结构:第8层用 defNeuronConcatenate 把账户描述(AccountDescr)与隐状态拼接,窗口取 prev_count、步长跳 AccountDescr,输出维度锁死为 2*LatentCount,激活走 SIGMOID。 第9层直接挂 defNeuronVAEOCL,把维度压回 LatentCount,优化器全程 ADAM;任何一层 Add 失败就 delete 描述符并返 false,避免悬空内存。 Actor 网络从 Clear 起步,输入层 defNeuronBaseOCL 维度 LatentCount、激活 None;随后两层同维度 LReLU 做非线性映射。 末层 layer3 扩到 2*NActions 且激活 None,layer4 用 defNeuronVAEOCL 收口到 NActions——动作空间维度由 NActions 常量直接决定,改这个数要同步动 layer3 的 2*NActions。 外汇与贵金属行情的高波动可能让这类网络在实盘推断时漂移,建议在 MT5 策略测试器里先跑编码器的 Add 返回逻辑,确认无 false 再接 Actor。

MQL5 / C++
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.optimization = ADAM;
  descr.activation = LReLU;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">7
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">8
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronConcatenate;
  descr.count = class="num">2 * LatentCount;
  descr.window = prev_count;
  descr.step = AccountDescr;
  descr.optimization = ADAM;
  descr.activation = SIGMOID;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">9
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronVAEOCL;
  descr.count = LatentCount;
  descr.optimization = ADAM;
  if(!encoder.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Actor
  actor.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">1
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">2
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = LReLU;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">3
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  descr.count = class="num">2 * NActions;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- layer class="num">4
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronVAEOCL;
  descr.count = NActions;
把分布偏移诊断交给小布
这些离线策略的稳定性诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到策略偏移预警,你只管判断信号可信度。

常见问题

SPOT 沿用行为约束思路但仍受 SGD 不稳定性影响,CFPI 用一阶泰勒近似构造封闭式运算器,直接给出策略偏移的解析解,削弱随机初值与估测点差异导致的变数。
实盘数据集多由异构策略收集,动作呈多模态分布,单高斯只能捕单一模式,会漏掉潜在改进空间,需混合高斯配合 LogSumExp 下限处理。
它在行为策略邻域内把政策意向函数线性化,让分布偏移约束可解析求解,是封闭式运算器能避开迭代优化的关键一步。
可以,小布的品种页把策略分布偏移与多模态捕获情况做成可视化,帮你快速识别某段历史数据是否适配单高斯假设。
不,混合高斯层数、行为策略邻域半径仍要依数据设定,只是策略改进本身不再走易飘的随机梯度路径,超参敏感度下降。