神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)(基础篇)
用决策转换器给策略做无监督预训练
决策转换器(Decision Transformer)原本用于离线强化学习,核心是把历史轨迹切成(状态, 动作, 回报)三元组,靠掩码语言建模方式做序列预测。放到 MT5 环境里,我们可以不依赖奖励信号先做无监督预训练(PDT),让模型先吃透行情状态到动作的映射分布。 具体做法是用一段滑窗把每根 K 线附近的市场特征当作状态,把账户在此后的下单动作当作标签,回报字段先置零或填归一化净值变化。这样在没人工标注胜率目标时,网络也能学到‘某种形态倾向于某种手数’的先验。 外汇与贵金属杠杆高、滑点突变频繁,这种预训练只降低过拟合概率,不保证样本外稳定,实盘前务必在 MT5 策略测试器用 2020—2023 年数据回测验证。
◍ PDT 怎么绕开奖励标记做预训练
决策转换器(DT)原本依赖带奖励的标注数据做长期训练,但实际场景里奖励常常难拿,标注集也很难撑起训练规模。2023 年 5 月那篇《决策转换器的未来条件下无监督预训练》提出的预训练决策转换器(PDT),核心就是用次优、无奖励的轨迹先离线跑预训练,再靠在线交互优调目标任务。 PDT 只拿过去信息去学未来轨迹的嵌入空间,外加一个未来先验条件;它通过条件化动作预测,逼模型具备“对未来推理”的通用能力,这一步本身不挂钩任何具体任务奖励。 到了下游任务优调阶段,可以给每个未来嵌入挂一个回报预测网络,把嵌入和奖励关联起来,框架就能轻量适配新条件。外汇与贵金属行情里这类无奖励轨迹很多,用 PDT 预训练再优调,可能比从头标数据省掉一大半准备成本,但模型输出只是概率倾向,实盘仍属高风险。
「用潜在状态替掉回报指南针」
PDT 沿用了决策转换器(DT)的骨架:先读历史状态与已执行动作,再预测下一步动作。但它塞进一个额外机制——允许只用未标注轨迹做预训练,完全不碰回报(RTG)。
| 原本 DT 序列里那个“在途回报”相当于空间里的指南针,PDT 作者用潜在状态向量 Z 把它换掉。预训练阶段实际并行训三个模型:扮演者(经典 DT,看前序轨迹出动作)、目标预测模型 P(• | S_t)(由当前状态推 Z)、未来编码器 G(• | τ_{t+1:t+k})(把后续一段轨迹压成 Z)。后两者吃不同数据但都吐 Z,构成当前态与未来态间的自动编码结构,Z 直接当扮演者的目标称谓。 |
|---|
训练次序有讲究:先绑未来编码器与扮演者,让模型“望”出规划边界 k 步,把后续轨迹信息压缩进 Z,借此逼出不受环境奖励限制、技能面更宽的初版策略;再训目标预测模型,找当前态与未来嵌入的依赖。 这套路把“奖励”和“目标结果”拆开,打开了大规模持续预学习的口子;当智能体跑偏时,行为不一致的问题也淡了。外汇与贵金属行情里这类离线预训练若直接搬用,需注意实盘分布漂移的高风险,仅可作为策略原型验证。
| 下游阶段不能只靠 P(Z | S_t) 采样,因为它没编码任何任务专属回报。原文补了一个奖励预测模型 F(• | Z,S_t) 与未来编码器联训,让梯度回传调 Z 里的奖励编码,才把“回报最大化”真正钉进行为里。 |
|---|
在 MT5 里搭三模型骨架
预训练决策转换器(PDT)落到 MQL5,核心是先定义扮演者、计划者、未来编码器这三套网络的描述。CreateDescriptions 方法接收三个动态数组指针,把每一层的 CLayerDescription 塞进去,后面 Train 方法才会照此建图。 扮演者用 4 层转换器、每层 16 个自注意力头,后面接 2 个卷积层抓稳定形态,再叠 3 个全连接层,输出维度等于动作空间。这里有个实操坑:反向传播要把梯度传去未来编码器,所以源数据被切两路,一路走批量归一化进主缓冲,一路带未来嵌入进串联层,少切一路梯度就断链。 计划者只看 1 根柱线的形态和指标,信息量极小,但它的产出会以嵌入形式喂回扮演者内层,重点不在深历史而在局部变化。未来编码器稍重,源数据塞了后续几根蜡烛的价和指标(不含账户与动作),同样 4 层转换器+16 头,输出端 SoftMax 归一化。奖励预测模型放到单独的 CreateValueDescriptions,优调阶段才接进来。 下面这段是 CreateDescriptions 的入口骨架,注意 agent 指针为空时才 new,真在 EA 里跑要先确认三个数组都已实例化,否则返回 false 直接中断初始化。
class="type">bool CreateDescriptions(CArrayObj *agent, CArrayObj *planner, CArrayObj *future_embedding) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!agent) { agent = new CArrayObj(); if(!agent) class="kw">return false; }
◍ 用 CLayerDescription 拼出行情特征代理网络
在 MT5 里搭一个面向价格行为的轻量智能体,第一步是把网络层用 CLayerDescription 逐个塞进 agent 对象。下面这套结构直接决定了小布盯盘时能从 K 线、账户状态、时间标签里提取多少特征,外汇与贵金属波动剧烈,参数错了模型可能完全学不到有效信号。 输入层先用 defNeuronBaseOCL,节点数由 BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions 算出,激活函数设 None、优化器用 ADAM。紧接着接一层 defNeuronBatchNormOCL 做批量归一,batch 写死 1000,能缓解不同品种量纲差异带来的训练抖动。 第三层用 defNeuronConcatenate 把前层输出和 EmbeddingSize 维度的嵌入拼起来,step 设为 EmbeddingSize;第四层 defNeuronEmbeddingOCL 按 HistoryBars 展开,用 ArrayCopy 把五个窗口长度写进 descr.windows,window_out 固定为 EmbeddingSize。 真正吃算力的在第五层:defNeuronMLMHSparseAttentionOCL 把节点扩到上一层 5 倍,window_out=64、layers=4、step=16,概率模式选 Sparse,多头稀疏注意力适合抓长周期贵金属走势里的偶发结构。最后两层 defNeuronConvOCL 用 LReLU 激活,第二层 window_out 压到 16,逐步把高维特征收敛成可行动向量。 开 MT5 把这段代码原样丢进 EA 初始化,先打印各层 prev_count 确认维度链没断;外汇和贵金属杠杆高,模型过拟合时回测漂亮实盘也可能亏,调 batch 和 Sparse 概率前务必用历史数据跑一遍。
class=class="str">"cmt">//--- Agent agent.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(!agent.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(!agent.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 = defNeuronConcatenate; descr.count = prev_count + EmbeddingSize; descr.step = EmbeddingSize; descr.optimization = ADAM; descr.activation = None; if(!agent.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 = defNeuronEmbeddingOCL; prev_count = descr.count = HistoryBars; { class="type">int temp[] = {BarDescr * NBarInPattern, AccountDescr, TimeDescription, NActions, EmbeddingSize}; ArrayCopy(descr.windows, temp); } class="type">int prev_wout = descr.window_out = EmbeddingSize; if(!agent.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 = defNeuronMLMHSparseAttentionOCL; prev_count = descr.count = prev_count * class="num">5; descr.window = EmbeddingSize; descr.step = class="num">16; descr.window_out = class="num">64; descr.layers = class="num">4; descr.probability = Sparse; descr.optimization = ADAM; if(!agent.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 = defNeuronConvOCL; descr.count = prev_count; descr.window = EmbeddingSize; descr.step = EmbeddingSize; descr.window_out = EmbeddingSize; descr.optimization = ADAM; descr.activation = LReLU; if(!agent.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 = defNeuronConvOCL; descr.count = prev_count; descr.window = EmbeddingSize; descr.step = EmbeddingSize; descr.window_out = class="num">16; descr.optimization = ADAM; descr.activation = LReLU; if(!agent.Add(descr)) {
「收尾的潜变量层与规划器输入装配」
上面这段把智能体(agent)尾部从第 7 层到第 10 层逐层挂上:第 7、8、9 层都是 defNeuronBaseOCL 类型、节点数等于 LatentCount、用 LReLU 激活加 ADAM 优化;第 10 层节点数改成 NActions、激活换 SIGMOID,输出动作概率。 每一层都先 new 一个 CLayerDescription,若 agent.Add 失败就 delete 掉 descr 并返回 false,避免内存泄漏。第 8 层里 prev_count 被同步赋成 LatentCount,供后续维度推算使用。 planner 对象若为空才新建 CArrayObj,随后 Clear 清空旧结构,开始拼规划器的网络:输入层节点数 = BarDescr * NBarInPattern、无激活;第 1 层用 defNeuronBatchNormOCL 且 batch=1000 做批归一化;第 2 层回到 defNeuronBaseOCL、LatentCount 节点、LReLU。 在 MT5 里把 LatentCount 从默认 64 调到 128,可能让第 7–9 层表达能力上升,但显存占用和训练耗时也会明显增加,外汇与贵金属行情下模型过拟合概率偏高,属高风险实验。
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; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!agent.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 = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!agent.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 = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NActions; descr.activation = SIGMOID; descr.optimization = ADAM; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } if(!planner) { planner = new CArrayObj(); if(!planner) class="kw">return false; } class=class="str">"cmt">//--- Planner planner.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = BarDescr * NBarInPattern; descr.activation = None; descr.optimization = ADAM; if(!planner.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(!planner.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.optimization = ADAM; descr.activation = LReLU; if(!planner.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription()))