神经网络变得简单(第 68 部分):离线优先引导政策优化(基础篇)
📘

神经网络变得简单(第 68 部分):离线优先引导政策优化(基础篇)

第 1/3 篇

「离线优先引导策略在 MT5 里的落地思路」

Dmitriy Gizlyk 在 2024 年 8 月 30 日发布的 MT5 交易系统文章里,把第 68 部分主题定为离线优先引导政策优化(Offline-Prioritized Guided Policy Optimization),核心是用历史离线数据先约束策略更新方向,再在引导阶段做小步修正,降低在线探索的踩坑成本。 外汇与贵金属市场高杠杆、跳空频繁,直接在线强化学习容易在极端行情里把权重推到不可恢复区;离线优先的做法相当于先拿已发生的 1 021 次浏览所指向的同类样本做策略先验,再有限度放开。 实际在 MT5 里验证时,建议先把历史 tick 导成离线缓冲,用 KL 散度卡住策略偏移上限,观察回测曲线是否比纯在线版更平滑。

◍ 用人类偏好替代环境奖励的 OPPO 思路

强化学习里,智能体靠最大化环境给的奖励来逼近最优政策,但奖励函数往往要人工精调,且真实目标常常无法被稀疏奖励充分表达。 论文《超越奖励:离线优先引导政策优化》提出 OPPO(Offline Priority-guided Policy Optimization),核心是用两条已跑完轨迹之间人类标注者的优先排序,替代环境奖励信号。 这意味着训练不再依赖环境即时回报,而是从离线轨迹中抽取人类判断;对交易策略搜索来说,相当于用「老手觉得哪条历史下单路径更稳」来引导模型,而不是等账户爆仓才算惩罚。外汇与贵金属杠杆高,这类偏好数据若取样偏差,可能放大回撤概率。

用高维上下文替掉标量奖励

传统离线优先引导学习常走两步:先用监督学习训出奖励函数,再拿任意离线 RL 算法去优化政策。问题在标量奖励会在复杂任务里形成信息瓶颈,政策优化容易卡在次优解;更麻烦的是离线政策会钻错误奖励函数的空子,跑出意料外的行为。 OPPO 把这两步并成一步:不单独训奖励,而是直接从离线优先数据集里学政策,同时建模离线缺失信息并做优先建模。它把政策写成上下文形式 π(A|S,Z),在高维空间 Z 里估算,而不是被压成一个数字。和标量收益比,Z 空间能装下更多任务相关信息,政策条件化在学出的优化上下文 Z' 上,更贴近人的优先判断。 作者用模型 Iθ 近似优先函数,Z=Iθ(τ) 就是优先上下文,编码器-解码器结构和离线模仿学习像,但因为没有专家演示,改靠优先标签抽回顾信息。优先建模目标里 z+ 是正轨迹上下文 Iθ(yτj+(1-y)τi),z- 是较差轨迹上下文 Iθ(yτi+(1-y)τj);底层假设是人在两条轨迹 τi、τj 间做两级比较:各自和假设最优 τ* 的相似度 l(z*,z+) 与 l(z*,z-),取更接近优先的那条。 优化这个目标会让最优上下文更靠 z+、更远离 z-。z* 来自始终优先于任何离线轨迹的 τ*,它的后验和 Iθ(•) 提取是交替更新的,训练稳定性由此来。编码器损失有两部分:和回顾信息误差比,以及和优先数据集给的二元标签对齐。更好估 z* 帮编码器抓人判定时真看重的特征,反过来好编码器又加速高维空间里找最优轨迹。

「用 MQL5 把轨迹优先写进结构体」

传统强化学习每步都存奖励,这里直接换成轨迹优先标签:只在轨迹结束时记一笔净利润,存进 STrajectory,SState 里的 rewards 数组被 scheduler 上下文数组取代,而且数组大小也改了,不是单纯改名。 优先级是挂在整条轨迹上的,不是单步动作;离线数据集里按 [0:1] 区间做轨迹两两比较,由智能系统打标签,不手动画棋盘表。作者只用了「轨迹盈利」这一个准则,但你完全可以自己加最大回撤、盈利因子和系数——准则集只影响训练结果,不影响算法实现。 结构字段一改,复制和读写这两个结构文件的方法也得跟着改,具体改动很琐碎,直接看附件代码最省事。下面这段是 SState 的骨架,注意 state 维度由 BarDescr * NBarInPattern 决定,action 长度等于 NActions。 模型端用两个 DT(决策转换器)分开训:Scheduler 学优先项,Agent 学行为策略,都带注意力机制。Scheduler 输入最后一根收盘烛条的价格、指标、账户持仓、时间戳和 Agent 上个动作,过批归一化、Embedding、两次 SoftMax 和注意力模块,输出 EmbeddingSize 长的潜向量;Agent 架构类似,多了卷积层降维,最后接 4 个全连接层,输出大小等于动作空间。 收集轨迹的 EA 用 ɛ-贪婪探索,OnTick 里等新柱、刷指标、组状态、跑 Scheduler 前馈拿上下文、再跑 Agent 前馈出动作,不加奖励只存轨迹;优先级在策略测试器 OnTester 里按验算有效性回填。训练 Scheduler 时从回放缓冲区按盈利采样正负两条轨迹,先顺向最小化优先轨迹偏差,再反向最大化负轨迹偏差(偏差还乘 0.5 往优先侧推),避开 GPT 顺序敏感带来的双模型合并坑。Agent 训法更简单:单轨迹采样,用回放里的上下文做监督,把预测动作和真实动作误差压到最小。 外汇和贵金属杠杆高、爆仓快,这套离线强化方案只是概率上提高策略可控性,实盘前务必在 MT5 策略测试器用历史数据跑通附件里的 Research / StudyScheduler / StudyAgent / Test 四个 mq5。

MQL5 / C++
class="kw">struct SState
  {
   class="type">float            state[BarDescr * NBarInPattern];
   class="type">float            account[AccountDescr];
   class="type">float            action[NActions];

◍ 状态结构与轨迹缓冲的底层定义

在 MT5 里做强化学习式的盯盘代理,第一步是把「状态」和「轨迹」固化成结构体。SState 内部用 float 型 scheduler[EmbeddingSize] 承载调度向量,Clear 方法靠 ArrayInitialize 把 state、account、action、scheduler 四个数组一次性归零,避免上一根 K 线的残留污染下一帧决策。 重载 operator= 时没有直接 memcpy,而是对四个数组分别 ArrayCopy,这样在回测中复制某一步状态做分支模拟时,内存是隔离的,不会改到原对象。STrajectory 则用一个 SState States[Buffer_Size] 环形缓冲装整条轨迹,外加 Total 与 Profit 双精度字段记录步数和累计盈亏。 CreateSchedulerDescriptions 是给神经网络调度器铺输入层的函数。输入层神经元数由 BarDescr * NBarInPattern + AccountDescr + TimeDescription + NActions 算出,激活函数设 None、优化器用 ADAM;若 scheduler 指针为空会先 new 一个 CArrayObj 再 Clear,保证每次重建描述时不带旧层配置。外汇与贵金属行情跳空频繁,这套结构在实盘前务必用策略测试器跑通 Save/Load 的二进制一致性。

MQL5 / C++
class="type">float                scheduler[EmbeddingSize];
class=class="str">"cmt">//---
                     SState(class="type">void);
class=class="str">"cmt">//---
class="type">bool                 Save(class="type">int file_handle);
class="type">bool                 Load(class="type">int file_handle);
class=class="str">"cmt">//---
class="type">void                 Clear(class="type">void)
   {
      ArrayInitialize(state, class="num">0);
      ArrayInitialize(account, class="num">0);
      ArrayInitialize(action, class="num">0);
      ArrayInitialize(scheduler, class="num">0);
   }
class=class="str">"cmt">//--- overloading
class="type">void                 class="kw">operator=(const SState &obj)
   {
      ArrayCopy(state, obj.state);
      ArrayCopy(account, obj.account);
      ArrayCopy(action, obj.action);
      ArrayCopy(scheduler, obj.scheduler);
   }
};
class="kw">struct STrajectory
  {
   SState             States[Buffer_Size];
   class="type">int                Total;
   class="type">class="kw">double             Profit;
class=class="str">"cmt">//---
                     STrajectory(class="type">void);
class=class="str">"cmt">//---
   class="type">bool               Add(SState &state);
   class="type">void               ClearFirstN(const class="type">int n);
class=class="str">"cmt">//---
   class="type">bool               Save(class="type">int file_handle);
   class="type">bool               Load(class="type">int file_handle);
class=class="str">"cmt">//--- overloading
   class="type">void               class="kw">operator=(const STrajectory &obj)
      {
       Total = obj.Total;
       Profit = obj.Profit;
       for(class="type">int i = class="num">0; i < Buffer_Size; i++)
          States[i] = obj.States[i];
      }
  };
class="type">bool CreateSchedulerDescriptions(CArrayObj *scheduler)
  {
class=class="str">"cmt">//---
   CLayerDescription *descr;
class=class="str">"cmt">//---
   if(!scheduler)
     {
      scheduler = new CArrayObj();
      if(!scheduler)
         class="kw">return false;
     }
class=class="str">"cmt">//--- Scheduler
   scheduler.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 = (BarDescr * NBarInPattern +
AccountDescr + TimeDescription + NActions);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">1

把网络层逐个塞进调度器

下面这段是构建 OpenCL 神经网络的核心拼装过程,每一层都用 CLayerDescription 描述后交给 scheduler.Add。注意第二层 Embedding 的窗口数组直接用 ArrayCopy 从临时整型数组写入,维度组合是 BarDescr*NBarInPattern、AccountDescr、TimeDescription、NActions。 第一层 BatchNorm 把 batch 写死成 1000,优化器选 ADAM,激活函数留 None;第三层 SoftMax 的 step 用 prev_count*4,而 prev_count 来自上一层输出数量。第四层多头注意力把 count 扩到 prev_count*4,window_out 固定 32、layers 为 4、step 为 8,这是显存和计算量容易爆的地方。 第五层又接 SoftMax,step 回退到 prev_count;第六层 Base 层用 LReLU 激活、数量走 LatentCount;第七层 Base 层把 count 重置为 EmbeddingSize、激活 None。任何一层 Add 失败就 delete descr 并返 false,全部成功才返 true。 在 MT5 里跑这套,先确认 HistoryBars、EmbeddingSize、LatentCount 这些宏已在头文件定义,否则第四层 prev_count*4 会直接越界。外汇与贵金属杠杆高,这类模型只作概率参考,实盘前务必用历史数据验证过拟合程度。

MQL5 / C++
  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(!scheduler.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 = defNeuronEmbeddingOCL;
   prev_count = descr.count = HistoryBars;
     {
      class="type">int temp[] = {BarDescr * NBarInPattern, AccountDescr, TimeDescription, NActions};
      ArrayCopy(descr.windows, temp);
     }
   class="type">int prev_wout = descr.window_out = EmbeddingSize;
   if(!scheduler.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 = EmbeddingSize;
   descr.step = prev_count * class="num">4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.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 = defNeuronMLMHAttentionOCL;
   prev_count = descr.count = prev_count * class="num">4;
   descr.window = EmbeddingSize;
   descr.step = class="num">8;
   descr.window_out = class="num">32;
   descr.layers = class="num">4;
   descr.optimization = ADAM;
   if(!scheduler.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 = EmbeddingSize;
   descr.step = prev_count;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.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;
   descr.type = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.optimization = ADAM;
   descr.activation = LReLU;
   if(!scheduler.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 = EmbeddingSize;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!scheduler.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//---
   class="kw">return true;
   }

常见问题

把成交流程按盈亏比、回撤、持仓时间等维度人工标注好恶,存成偏好对,训练时直接拿偏好得分替掉传统标量奖励即可。
可以。把K线形态、订单簿厚度、波动率等多维状态打包进上下文向量,比单看一个平仓盈利数值更稳定,减少奖励噪声。
可以。小布能读取你的成交记录,按设定规则标注偏好并整理成结构化轨迹,你只需确认参数,不用手写缓冲代码。
状态只存必要浮点特征和上下文标识,轨迹缓冲用定长数组加优先权重字段,避免每步动态分配,回测更顺。
离线训练阶段逐层挂载不影响实盘;实盘只加载推理图,把更新频率降到分钟级,能避开卡顿。