神经网络变得简单(第 70 部分):封闭式政策改进运算器(CFPI)·进阶篇
Critic 网络的层堆叠与初始化
在强化学习智能体里,Critic 负责给状态-动作对打分,这段逻辑就是把它的网络结构一层层塞进 CNet 对象。先清掉旧描述,再建输入层:类型用 defNeuronBaseOCL,节点数等于 LatentCount,激活函数关成 None,优化器统一挂 ADAM。 紧接着是拼接层(defNeuronConcatenate),window 设为上一层节点数 prev_count,step 绑定 NActions,激活走 LReLU。后面三层都是 defNeuronBaseOCL,节点保持 LatentCount、LReLU 激活,直到第 4 层把输出压到 NRewards 且激活为 None,完成价值估计头。 每一层都用 new CLayerDescription() 拿描述符,Add 失败就 delete 并 return false,避免内存泄漏。外层输入参数 Iterations 默认 1e6,意味着训练循环上限百万步,外汇与贵金属行情下过拟合风险高,实盘前建议在 MT5 策略测试器里先跑小样本验证。 下方全局对象 StateEncoder 与 Critic1 已声明,配合 Buffer[] 存轨迹,开 MT5 把这段贴进 EA 的 Critic 构建函数即可观察网络是否按预期组装。
descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Critic critic.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = LatentCount; descr.activation = None; descr.optimization = ADAM; if(!critic.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 = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count; descr.step = NActions; descr.optimization = ADAM; descr.activation = LReLU; if(!critic.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(!critic.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 = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!critic.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; descr.count = NRewards; descr.optimization = ADAM; descr.activation = None; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Input parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ input class="type">int Iterations = class="num">1e6; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ STrajectory Buffer[]; CNet StateEncoder; CNet Critic1;
「EA 初始化时神经网络模型的加载与维度校验」
MT5 的 OnInit 阶段先把历史学习数据通过 LoadTotalBase 读入,失败就直接返回 INIT_FAILED 并打出错误码,这一步卡住后续所有模型构建。 若三个网络文件(Enc.nnw、Crt1.nnw、Crt2.nnw)不存在或加载失败,代码会新建 actor / critic / encoder 三套描述对象,调用 CreateDescriptions 生成结构后分别 Create,任何一步失败均释放内存并终止初始化。 模型就位后,把 Critic1 的 OpenCL 上下文共享给 Critic2 与 StateEncoder,避免重复占用显存;随后用 getResults 与 GetLayerOutput 做四道维度断言:隐空间数须等于 LatentCount,编码器第 0 层输出须等于 HistoryBars * BarDescr,两个 Critic 输入须等于 LatentCount。 这些数字不吻合会在日志抛出 'scope does not match' 或 'Input size doesn't match' 并回 INIT_FAILED——实盘前务必确认 HistoryBars=120、BarDescr=5 这类宏与训练时一致,否则 EA 起不来。外汇与贵金属杠杆高,模型加载异常时勿手动强制跳过校验。
CNet Critic2; 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">float temp; if(!StateEncoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) || !Critic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) || !Critic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true)) { Print("Init new models"); CArrayObj *actor = new CArrayObj(); CArrayObj *critic = new CArrayObj(); CArrayObj *encoder = new CArrayObj(); if(!CreateDescriptions(actor, critic, encoder)) { class="kw">delete actor; class="kw">delete critic; class="kw">delete encoder; class="kw">return INIT_FAILED; } if(!Critic1.Create(critic) || !Critic2.Create(critic) || !StateEncoder.Create(encoder)) { class="kw">delete actor; class="kw">delete critic; class="kw">delete encoder; class="kw">return INIT_FAILED; } class="kw">delete actor; class="kw">delete critic; class="kw">delete encoder; class=class="str">"cmt">//--- } class=class="str">"cmt">//--- OpenCL = Critic1.GetOpenCL(); Critic2.SetOpenCL(OpenCL); StateEncoder.SetOpenCL(OpenCL); class=class="str">"cmt">//--- StateEncoder.getResults(Result); if(Result.Total() != LatentCount) { PrintFormat("The scope of the State Encoder does not match the latent size count(%d <> %d)", LatentCount, Result.Total()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- StateEncoder.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of State Encoder doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Critic1.GetLayerOutput(class="num">0, Result); if(Result.Total() != LatentCount) { PrintFormat("Input size of Critic1 doesn&class="macro">#x27;t match State Encoder output(%d <> %d)", Result.Total(), LatentCount); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Critic2.GetLayerOutput(class="num">0, Result); if(Result.Total() != LatentCount) {
◍ 初始化失败与网络落盘的细节
这段 MT5 智能交易系统的片段,暴露了两个工程上容易踩的点:初始化阶段若 Critic2 输入尺寸和状态编码器输出对不上,会直接 PrintFormat 报出具体维度差异并 return INIT_FAILED,这种显式维度校验能避免后续训练时张量形状崩溃。 OnDeinit 里只在 reason 不是 REASON_INITFAILED 和 REASON_RECOMPILE 时才把 StateEncoder、Critic1、Critic2 存成 .nnw 文件,意味着重编译或初始化失败不会覆盖上一轮权重;三个网络分别用 getRecentAverageError() 记录近期平均误差,方便你对比训练漂移。 Train 函数开头用 GetProbTrajectories(Buffer, 0.9) 拿到概率轨迹,循环里用 MathRand() 的平方除以 32767 的平方做非线性采样,把索引 i 限制在 Buffer[tr].Total - 3 以内,若 i<0 就回退一次迭代。外汇与贵金属市场高杠杆、高波动,这类强化学习模型在历史数据上表现不代表实盘概率优势,开 MT5 把这段接进自己的 EA 框架,先跑 Init 事件看维度报错有没有触发。
PrintFormat("Input size of Critic2 doesn&class="macro">#x27;t match State Encoder output(%d <> %d)", Result.Total(), LatentCount); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Gradient.BufferInit(AccountDescr, class="num">0); class=class="str">"cmt">//--- if(!EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init")) { PrintFormat("Error of create study event: %d", GetLastError()); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- if(!(reason == REASON_INITFAILED || reason == REASON_RECOMPILE)) { StateEncoder.Save(FileName + "Enc.nnw", class="num">0, class="num">0, class="num">0, TimeCurrent(), true); Critic1.Save(FileName + "Crt1.nnw", Critic1.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); Critic2.Save(FileName + "Crt2.nnw", Critic2.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); } class="kw">delete Result; class="kw">delete OpenCL; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Train function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); vector<class="type">float> rewards, rewards1, rewards2, target_reward; class="type">uint ticks = GetTickCount(); class=class="str">"cmt">//--- for(class="type">int iter = class="num">0; (iter < Iterations && !IsStopped()); iter ++) { class="type">int tr = SampleTrajectory(probability); class="type">int i = (class="type">int)((MathRand() * MathRand() / MathPow(class="num">32767, class="num">2)) * (Buffer[tr].Total - class="num">3)); if(i < class="num">0) { iter--; class="kw">continue; } class=class="str">"cmt">//--- Q-function study State.AssignArray(Buffer[tr].States[i].state); class="type">float PrevBalance = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">0]; class="type">float PrevEquity = Buffer[tr].States[MathMax(i - class="num">1, class="num">0)].account[class="num">1]; Account.Clear();
账户特征与时间周期的编码注入
这段逻辑把每笔状态的账户维度逐个塞进 Account 容器,前 8 项分别是余额变动率、当前余额占比、权益变动率、自由保证金、预付款、手续费占比等,全部除以 PrevBalance 做归一化,避免不同资金规模下量纲撕裂。 时间特征用了 4 个三角函数:以 2023.01.01 到 2024.01.01 的秒差(约 31536000 秒)为年周期,再叠加月线、周线、日线的 PeriodSeconds 周期,分别算 MathSin / MathCos。这样把绝对时间戳转成 [−1,1] 的循环向量,模型能感知季节节律而非单纯递增数字。 喂完特征后立刻走 StateEncoder.feedForward,失败就 PrintFormat 打函数名和行号并 break;随后 Actions 和双 Critic 网络同步前向,rewards 用 Buffer 里 i+1 与 i+2 的奖励差乘折扣因子 DiscFactor 做时序修正。外汇与贵金属杠杆高,这类特征工程只解决输入尺度,不预示任何收益。
Account.Add((Buffer[tr].States[i].account[class="num">0] - PrevBalance) / PrevBalance); Account.Add(Buffer[tr].States[i].account[class="num">1] / PrevBalance); Account.Add((Buffer[tr].States[i].account[class="num">1] - PrevEquity) / PrevEquity); Account.Add(Buffer[tr].States[i].account[class="num">2]); Account.Add(Buffer[tr].States[i].account[class="num">3]); Account.Add(Buffer[tr].States[i].account[class="num">4] / PrevBalance); Account.Add(Buffer[tr].States[i].account[class="num">5] / PrevBalance); Account.Add(Buffer[tr].States[i].account[class="num">6] / PrevBalance); class="type">class="kw">double time = (class="type">class="kw">double)Buffer[tr].States[i].account[class="num">7]; class="type">class="kw">double x = time / (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;); Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_MN1); Account.Add((class="type">float)MathCos(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_W1); Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); x = time / (class="type">class="kw">double)PeriodSeconds(PERIOD_D1); Account.Add((class="type">float)MathSin(x != class="num">0 ? class="num">2.0 * M_PI * x : class="num">0)); if(Account.GetIndex() >= class="num">0) Account.BufferWrite(); class=class="str">"cmt">//--- if(!StateEncoder.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Actions.AssignArray(Buffer[tr].States[i].action); if(Actions.GetIndex() >= class="num">0) Actions.BufferWrite(); class=class="str">"cmt">//--- if(!Critic1.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actions)) || !Critic2.feedForward(GetPointer(StateEncoder), -class="num">1, GetPointer(Actions))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } class=class="str">"cmt">//--- Critic1.getResults(rewards1); Critic2.getResults(rewards2); rewards.Assign(Buffer[tr].States[i + class="num">1].rewards); target_reward.Assign(Buffer[tr].States[i + class="num">2].rewards); rewards = rewards - target_reward * DiscFactor; Result.AssignArray(CAGrad(rewards - rewards1) + rewards1); if(!Critic1.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) ||
「双评论家网络的回传与训练监控」
这段逻辑跑在强化学习训练循环里,Critic1 与 Critic2 各自独立做反向传播,任何一次 backProp 或状态编码器的梯度回传失败,就立刻 PrintFormat 打出函数名和行号然后 break,避免脏梯度污染权重。 训练进度不是每一步都刷屏,而是用 GetTickCount 做了节流:距上次打印超过 500 毫秒才更新 Comment,显示两个 Critic 的近期平均误差,格式精确到 15.8f。默认 Iterations=10000、BatchSize=256,意味着全量跑完可能要上万次 tick 累加,MT5 策略测试器里开非可视化模式更稳。 循环结束后清空 Comment,并把 Critic1 / Critic2 的最终平均误差用 PrintFormat 落到日志(%10.7f 精度),随后调 ExpertRemove 自卸载——说明这是一次性离线训练 EA,不是常驻实盘策略。外汇与贵金属杠杆高,这类自训练模型仅用于回测研究,实盘接入前须重做样本外验证。
if(!StateEncoder.backPropGradient(GetPointer(Account), GetPointer(Gradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">break; } Result.AssignArray(CAGrad(rewards - rewards2) + rewards2); if(!Critic2.backProp(Result, GetPointer(Actions), GetPointer(Gradient)) || !StateEncoder.backPropGradient(GetPointer(Account), GetPointer(Gradient))) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">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", "Critic1", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic1.getRecentAverageError()); str += StringFormat("%-15s %class="num">5.2f%% -> Error %class="num">15.8f\n", "Critic2", iter * class="num">100.0 / (class="type">class="kw">double)(Iterations), Critic2.getRecentAverageError()); Comment(str); ticks = GetTickCount(); } } Comment(""); class=class="str">"cmt">//--- PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic1", Critic1.getRecentAverageError()); PrintFormat("%s -> %d -> %-15s %class="num">10.7f", __FUNCTION__, __LINE__, "Critic2", Critic2.getRecentAverageError()); ExpertRemove(); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Input parameters | class=class="str">"cmt">//+------------------------------------------------------------------+ input class="type">int Iterations = class="num">10000; input class="type">int BatchSize = class="num">256; CNet Actor; CNet Critic1; CNet Critic2; CNet StateEncoder; 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;
◍ 加载与维度校验这条链路
EA 初始化收尾阶段先把 StateEncoder、Critic1、Critic2 三个网络从 .nnw 文件读入,文件名统一拼成 FileName+"Enc.nnw" 这类形式。只要任意一个 Load 返回失败,终端会打印 Can't load Critic models 并以 INIT_FAILED 退出,说明模型文件缺失或路径不对。
Actor 若 Load 失败则走新建分支:用 CreateDescriptions 构造 actor/critic 描述数组,再 Actor.Create 建网;任一环节出错同样 INIT_FAILED,且务必 delete 掉临时数组避免内存泄漏。建完网后把 OpenCL 上下文从 Actor 取出并共享给 Critic1、Critic2、StateEncoder,推理阶段统一关掉 TrainMode(false)。
随后做硬维度对齐:Actor 输出节点数须等于 NActions,否则报 scope of the actor does not match the actions count;StateEncoder 第 0 层输出须等于 HistoryBars*BarDescr,这是输入状态的总特征数。取 StateEncoder 实际输出数记为 latent_state,Critic1、Critic2、Actor 的第 0 层输入都必须等于该值,偏差会分别打印 Input size of Critic1/2/Actor doesn't match output State Encoder。最后 Gradient.BufferInit(AccountDescr, 0) 收尾,外汇与贵金属行情高波动,模型维度对不齐时宁可 INIT_FAILED 也不要带错网络上线。
} class=class="str">"cmt">//--- load models class="type">float temp; if(!StateEncoder.Load(FileName + "Enc.nnw", temp, temp, temp, dtStudied, true) || !Critic1.Load(FileName + "Crt1.nnw", temp, temp, temp, dtStudied, true) || !Critic2.Load(FileName + "Crt2.nnw", temp, temp, temp, dtStudied, true)) { Print("Can&class="macro">#x27;t load Critic models"); class="kw">return INIT_FAILED; } if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true)) { Print("Init new models"); CArrayObj *actor = new CArrayObj(); CArrayObj *critic = new CArrayObj(); if(!CreateDescriptions(actor, critic, critic)) { class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } if(!Actor.Create(actor)) { class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } class="kw">delete actor; class="kw">delete critic; } OpenCL = Actor.GetOpenCL(); Critic1.SetOpenCL(OpenCL); Critic2.SetOpenCL(OpenCL); StateEncoder.SetOpenCL(OpenCL); class=class="str">"cmt">//--- StateEncoder.TrainMode(false); Critic1.TrainMode(false); Critic2.TrainMode(false); class=class="str">"cmt">//--- Actor.getResults(Result); if(Result.Total() != NActions) { PrintFormat("The scope of the actor does not match the actions count(%d <> %d)", NActions, Result.Total()); class="kw">return INIT_FAILED; } StateEncoder.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of State Encoder doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } StateEncoder.getResults(Result); class="type">int latent_state = Result.Total(); Critic1.GetLayerOutput(class="num">0, Result); if(Result.Total() != latent_state) { PrintFormat("Input size of Critic1 doesn&class="macro">#x27;t match output State Encoder(%d <> %d)", Result.Total(), latent_state); class="kw">return INIT_FAILED; } Critic2.GetLayerOutput(class="num">0, Result); if(Result.Total() != latent_state) { PrintFormat("Input size of Critic2 doesn&class="macro">#x27;t match output State Encoder(%d <> %d)", Result.Total(), latent_state); class="kw">return INIT_FAILED; } Actor.GetLayerOutput(class="num">0, Result); if(Result.Total() != latent_state) { PrintFormat("Input size of Actor doesn&class="macro">#x27;t match output State Encoder(%d <> %d)", Result.Total(), latent_state); class="kw">return INIT_FAILED; } Gradient.BufferInit(AccountDescr, class="num">0); class=class="str">"cmt">//---