神经网络变得简单(第 63 部分):决策转换器无监督预训练(PDT)·进阶篇
(2/3)· 当奖励标记稀缺、训练集难以扩展,PDT 如何仅凭过去轨迹让模型学会对未来推理
「编码器与未来嵌入的双路网络拼装」
这段代码在 MT5 的 OpenCL 环境里搭了两路神经网络:一路是主编码器(layer 4~6),另一路是 future_embedding 分支,专门处理未来窗口的嵌入表示。主编码器第 4、5 层都用 LatentCount 个 defNeuronBaseOCL 神经元,激活函数 LReLU,优化器 ADAM;第 6 层换成 defNeuronSoftMaxOCL 做多分类输出,count 等于 EmbeddingSize。 future_embedding 分支的输入层维度是 BarDescr * NBarInPattern * ValueBars,紧跟一个 batch=1000 的 BatchNorm 层做归一化。真正吃算力的是 layer 2:用了 MLMHSparseAttention(多头稀疏注意力),window=BarDescr*NBarInPattern、step=16、window_out=64、layers=4,概率模式为 Sparse。 每一层都先 new CLayerDescription,失败直接 return false;加进 planner 或 future_embedding 容器时若返回 false,则 delete descr 并退出。开 MT5 把 Sparse 改成 Dense 跑同结构,显存占用和收敛速度会有明显差异,外汇与贵金属数据噪声大,这类模型过拟合概率偏高,验证时务必用样本外切片。
class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!planner.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 = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!planner.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 = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; 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">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; descr.count = EmbeddingSize; descr.activation = None; descr.optimization = ADAM; if(!planner.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Future Embedding if(!future_embedding) { future_embedding = new CArrayObj(); if(!future_embedding) class="kw">return false; } class=class="str">"cmt">//--- future_embedding.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = BarDescr * NBarInPattern * ValueBars; descr.activation = None; descr.optimization = ADAM; if(!future_embedding.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(!future_embedding.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 = defNeuronMLMHSparseAttentionOCL; prev_count = descr.count = ValueBars; descr.window = BarDescr * NBarInPattern; descr.step = class="num">16; descr.window_out = class="num">64; descr.layers = class="num">4; descr.probability = Sparse; descr.optimization = ADAM; if(!future_embedding.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription()))
未来嵌入与账户价值网络的层描述拼装
这段逻辑在 MT5 的 OpenCL 神经网络封装里分两条线:future_embedding 负责把时序特征编码成 EmbeddingSize 维度的未来状态,value 网络则把账户描述 AccountDescr 维输入映射成价值估计。两者都靠 CLayerDescription 对象逐层 Add 进 CArrayObj,任何一层 new 失败或 Add 返回 false 就直接 delete 并退出,避免野指针。 future_embedding 从第 3 层起用 LatentCount 个 defNeuronBaseOCL 节点、LReLU 激活、ADAM 优化,连续堆两层;第 5 层节点数回到 EmbeddingSize 且激活为 None;第 6 层切换为 defNeuronSoftMaxOCL 做归一化输出。可见潜变量维度 LatentCount 是中间瓶颈,EmbeddingSize 是两端对齐的接口宽度。 value 网络入口先以 AccountDescr 作输入层(激活 None),随后接一个 batch=1000 的 defNeuronBatchNormOCL 批归一化层——这个 1000 的数值直接决定训练时滑动均值统计的窗口,调小可能让归一化对近期样本更敏感。再往后用 defNeuronConcatenate 把 LatentCount 维潜变量按步长 EmbeddingSize 拼接,并以 SIGMOID 压缩到 (0,1) 区间,供后续价值回归使用。 在 MT5 里把这段抄进 Expert Advisor 的神经网络初始化函数,改 batch 或 LatentCount 后重编译,能直观看到显存占用和训练收敛速度的变化。外汇与贵金属杠杆高,此类模型仅作概率参考,实盘前务必在策略测试器跑足量历史样本。
class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!future_embedding.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 = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!future_embedding.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 = defNeuronBaseOCL; descr.count = EmbeddingSize; descr.activation = None; descr.optimization = ADAM; if(!future_embedding.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 = defNeuronSoftMaxOCL; descr.count = EmbeddingSize; descr.activation = None; descr.optimization = ADAM; if(!future_embedding.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateValueDescriptions(CArrayObj *value) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!value) { value = new CArrayObj(); if(!value) class="kw">return false; } class=class="str">"cmt">//--- Value value.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 = AccountDescr; descr.activation = None; descr.optimization = ADAM; if(!value.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(!value.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 = LatentCount; descr.step = EmbeddingSize; descr.optimization = ADAM; descr.activation = SIGMOID; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; }
◍ 潜变量层与三网初始化落地
构建智能体网络时,第 3、4、5 层都走 CLayerDescription 动态描述。第 3 层与第 4 层类型均为 defNeuronBaseOCL,神经元数由 LatentCount 控制,优化器 ADAM,激活用 LReLU;第 5 层输出层神经元数等于 NRewards,激活设为 None,直接出原始分值。 若 LoadTotalBase 读不到训练数据,OnInit 会打印错误码并返回 INIT_FAILED,EA 不会启动。Agent、Planner、FutureEmbedding 三个 CNet 实例优先从 .nnw 文件加载,文件名后缀分别为 Act.nnw、Pln.nnw、FEm.nnw。 当三个网络任一 Load 失败,代码会新建 CArrayObj 描述容器,调用 CreateDescriptions 填充层结构,再走 Create 建网。这里若描述填充或建网失败同样返回 INIT_FAILED,实盘前应在 MT5 策略测试器里故意删掉一个 .nnw 验证该分支是否按预期报错。外汇与贵金属品种波动剧烈,此类模型网初始化失败可能导致信号中断,属高风险环节。
class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!value.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 = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!value.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 = defNeuronBaseOCL; descr.count = NRewards; descr.activation = None; descr.optimization = ADAM; if(!value.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } CNet Agent; CNet Planner; CNet FutureEmbedding; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ResetLastError(); if(!LoadTotalBase()) { PrintFormat("Error of load study data: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- load models class="type">class="kw">float temp; if(!Agent.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !Planner.Load(FileName + "Pln.nnw", temp, temp, temp, dtStudied, true) || !FutureEmbedding.Load(FileName + "FEm.nnw", temp, temp, temp, dtStudied, true)) { CArrayObj *agent = new CArrayObj(); CArrayObj *planner = new CArrayObj(); CArrayObj *future_embedding = new CArrayObj(); if(!CreateDescriptions(agent, planner, future_embedding)) { class="kw">delete agent; class="kw">delete planner; class="kw">delete future_embedding; class="kw">return INIT_FAILED; } if(!Agent.Create(agent) || !Planner.Create(planner) || !FutureEmbedding.Create(future_embedding)) { class="kw">delete agent; class="kw">delete planner; class="kw">delete future_embedding; class="kw">return INIT_FAILED; }
「训练循环里的样本抽取与张量初始化」
在 Train 函数里,迭代次数由 Iterations 控制,每次循环先用 MathRand() 除以 32767.0 把随机值映射到 [0,1),再乘 (total_tr-1) 选出一条训练样本 tr,total_tr 即 Buffer 数组长度。 第二条随机索引用了 MathRand() 的平方除以 32767 的平方,使 i 更偏向较小值,范围被夹在 Buffer[tr].Total - 2*HistoryBars - ValueBars 与 MathMin(Buffer[tr].Total, 20+ValueBars) 之间;若 i<0 则本次 iter 自减并跳过,避免越界。 进入有效样本后,Actions 被初始化为长度为 NActions 的全零 vector<float>,随后用 for 从 state=i 走到 MathMin(Buffer[tr].Total - ValueBars, i + HistoryBars*3),这段窗口长度直接决定每次喂给网络的历史步数。 开 MT5 把 HistoryBars 和 ValueBars 调小一档,能明显看到 i 的可选区间变窄、训练样本更集中近期行情,外汇与贵金属波动剧烈,这类参数改动须先在模拟盘验证风险。
class="type">void Train(class="type">void) { class="type">int total_tr = ArraySize(Buffer); class="type">uint ticks = GetTickCount(); class="type">bool StopFlag = false; for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped() && !StopFlag); iter ++) { class="type">int tr = (class="type">int)((MathRand() / class="num">32767.0) * (total_tr - class="num">1)); class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * MathMax(Buffer[tr].Total - class="num">2 * HistoryBars - ValueBars, MathMin(Buffer[tr].Total, class="num">20 + ValueBars))); if(i < class="num">0) { iter--; class="kw">continue; } Actions = vector<class="type">class="kw">float>::Zeros(NActions); for(class="type">int state = i; state < MathMin(Buffer[tr].Total - ValueBars, i + HistoryBars * class="num">3); state++) {
把账户状态塞进神经网络前的特征拼装
做强化学习训练时,喂给 Planner 的状态向量不是裸 K 线,而是把历史账户表现和时间周期编码后拼起来的复合特征。下面这段拼装逻辑直接决定了模型能不能学到‘仓位随净值波动’的非线性规律,外汇和贵金属的高杠杆环境下这种特征失真会直接放大回撤概率。 先看历史数据注入:State.AssignArray 把当前 state 的原始状态数组挂到输入容器,随后调 Planner.feedForward 做前向传播(batch=1,不训练)。如果返回 false,立刻打印函数名加行号、置 StopFlag 并 break——这意味着某根样本的状态维度对不上,整套训练循环会断。 账户描述那段是重点。PrevBalance / PrevEquity 取上一 state 的同名账户字段(state==0 时退化为自身初始值),之后连续 Add 了 8 个派生量:权益相对余额的变化率、权益余额比、权益相对权益的变化率,以及 account[2]~[6] 里原始字段和按余额归一化的三个量。注意 account[4]~[6] 全除以 PrevBalance,等于把绝对金额压成‘占初始余额比例’,避免不同账户规模导致数值尺度撕裂。 时间标签用了四个三角函数:以 2023 全年秒数、月线秒数、周线秒数、日线秒数为周期,把 account[7](时间戳)归一化后取 sin/cos。这样模型能隐式感知‘月末调仓’‘周五流动性变薄’这类周期,但 x 的基准写死 D'2024.01.01'-D'2023.01.01',换年份跑回测要手动改常数。 最后把上一动作 Actions 数组接在状态尾,Target 则取下一个 state 的向量并复制 ValueBars-1 次做多步前瞻,再送 FutureEmbedding 前向。ValueBars 若设 5,Target 长度就是单 state 的 5 倍,维度不对同样会让 feedForward 崩。
class=class="str">"cmt">//--- History data State.AssignArray(Buffer[tr].States[state].state); if(!Planner.feedForward(GetPointer(State), class="num">1, false)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } class=class="str">"cmt">//--- Account description class="type">class="kw">float PrevBalance = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">0] : Buffer[tr].States[state - class="num">1].account[class="num">0]); class="type">class="kw">float PrevEquity = (state == class="num">0 ? Buffer[tr].States[state].account[class="num">1] : Buffer[tr].States[state - class="num">1].account[class="num">1]); State.Add((Buffer[tr].States[state].account[class="num">0] - PrevBalance) / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">1] / PrevBalance); State.Add((Buffer[tr].States[state].account[class="num">1] - PrevEquity) / PrevEquity); State.Add(Buffer[tr].States[state].account[class="num">2]); State.Add(Buffer[tr].States[state].account[class="num">3]); State.Add(Buffer[tr].States[state].account[class="num">4] / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">5] / PrevBalance); State.Add(Buffer[tr].States[state].account[class="num">6] / PrevBalance); class=class="str">"cmt">//--- Time label class="type">class="kw">double x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)(D&class="macro">#x27;class="num">2024.01.class="num">01&class="macro">#x27; - D&class="macro">#x27;class="num">2023.01.class="num">01&class="macro">#x27;); State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1); State.Add((class="type">class="kw">float)MathCos(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1); State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x)); x = (class="type">class="kw">double)Buffer[tr].States[state].account[class="num">7] / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1); State.Add((class="type">class="kw">float)MathSin(class="num">2.0 * M_PI * x)); class=class="str">"cmt">//--- Prev action State.AddArray(Actions); class=class="str">"cmt">//--- Target Result.AssignArray(Buffer[tr].States[state + class="num">1].state); for(class="type">int s = class="num">1; s < ValueBars; s++) Result.AddArray(Buffer[tr].States[state + class="num">1].state); if(!FutureEmbedding.feedForward(Result, class="num">1, false)) {
◍ 训练循环里的容错与日志节奏
这段是强化学习智能体在 MT5 里跑训练迭代的核心收口逻辑。每一轮先取未来状态嵌入结果,再做策略前馈与规划器、策略网络的反向传播;任一环节返回 false 就立刻打印函数名与行号、置 StopFlag 并 break,避免脏梯度继续污染权重。 误差监控没有每 tick 都刷,而是用 GetTickCount() 判断距上次输出是否超过 500 毫秒,超了才用 Comment 同时打出 Agent 与 Planner 的进度百分比和近期平均误差(getRecentAverageError,精度 15.8f)。这种节流写法在回测长周期时能把图表卡顿概率压下来。 全部迭代结束后清空 Comment,并用 PrintFormat 以 10.7f 精度把两个网络的最终平均误差写到日志,随后调 ExpertRemove() 自卸载。外汇与贵金属行情下这类自训练 EA 实盘风险极高,训练误差低不代表样本外能存活,上真金前务必在策略测试器用历史数据先跑通。
PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } FutureEmbedding.getResults(Result); class=class="str">"cmt">//--- Policy Feed Forward if(!Agent.feedForward(GetPointer(State), class="num">1, false, Result)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } class=class="str">"cmt">//--- Planner Study if(!Planner.backProp(Result, NULL, NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } class=class="str">"cmt">//--- Policy study Actions.Assign(Buffer[tr].States[state].action); vector<class="type">class="kw">float> result; Agent.getResults(result); Result.AssignArray(CAGrad(Actions - result) + result); if(!Agent.backProp(Result, GetPointer(FutureEmbedding)) || !FutureEmbedding.backPropGradient((CBufferFloat *)NULL)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); StopFlag = true; break; } class=class="str">"cmt">//--- if(GetTickCount() - ticks > class="num">500) { class="type">class="kw">string str = StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Agent", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Agent.getRecentAverageError()); str += StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Planner", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Planner.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Agent", Agent.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Planner", Planner.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- }