神经网络变得轻松(第四十七部分):连续动作空间·进阶篇
(2/3)· 仅靠买卖持有三选项的训练只是玩具,资金管理缺位如何撑起实盘策略
◍ 神经网络前向传播与梯度回传的指针守卫
这段 CNet 成员函数展示了 OpenCL 加速神经网络在 MT5 里的典型容错写法:前向 feedForward 从 layers.At(0) 取首神经元,逐层调用 neuron.FeedForward 把上一层输出喂给下一层,任意一层指针无效或更新失败立即 delete second 并返回 false,成功才返回 true。 反向 backPropGradient 先校验 layers 与 opencl 非空,再从最后一层向前循环到 layerNum=0 计算隐藏层梯度(calcHiddenGradients),随后由后向前调用 UpdateInputWeights 更新权重;循环里只要 CheckPointer 返回 POINTER_INVALID 或神经元方法返回 false 就直接退出,避免悬空指针污染显存缓冲。 在 MT5 策略测试器里接这段逻辑时,留意 del_second 标志:它为 true 时会释放传入的第二个输入缓冲,若你在外部复用同一 CBufferFloat 对象,误设该标志可能导致后续 bar 计算读到已释放内存。外汇与贵金属模型训练本身属高风险操作,过拟合后实盘胜率可能显著低于回测。
{
if(del_second)
class="kw">delete second;
class="kw">return false;
}
CNeuronBaseOCL *neuron = layer.At(class="num">0);
layer = layers.At(class="num">0);
if(!layer)
{
if(del_second)
class="kw">delete second;
class="kw">return false;
}
if(layer.At(class="num">0) != neuron)
if(!layer.Update(class="num">0, neuron))
{
if(del_second)
class="kw">delete second;
class="kw">return false;
}
for(class="type">int l = class="num">1; l < layers.Total(); l++)
{
layer = layers.At(l);
neuron = layer.At(class="num">0);
layer = layers.At(l - class="num">1);
if(!neuron.FeedForward(layer.At(class="num">0), second))
{
if(del_second)
class="kw">delete second;
class="kw">return false;
}
}
class=class="str">"cmt">//---
if(del_second)
class="kw">delete second;
class="kw">return true;
}
class="type">bool CNet::backPropGradient(CBufferFloat *SecondInput = NULL, CBufferFloat *SecondGradient = NULL)
{
if(
! layers ||
! opencl)
class="kw">return false;
CLayer *currentLayer = layers.At(layers.Total() - class="num">1);
CNeuronBaseOCL *neuron = NULL;
if(CheckPointer(currentLayer) == POINTER_INVALID)
class="kw">return false;
class=class="str">"cmt">//--- Calc Hidden Gradients
class="type">int total = layers.Total();
for(class="type">int layerNum = total - class="num">2; layerNum >= class="num">0; layerNum--)
{
CLayer *nextLayer = currentLayer;
currentLayer = layers.At(layerNum);
if(CheckPointer(currentLayer) == POINTER_INVALID)
class="kw">return false;
neuron = currentLayer.At(class="num">0);
if(!neuron || !neuron.calcHiddenGradients(nextLayer.At(class="num">0), SecondInput, SecondGradient))
class="kw">return false;
}
CLayer *prevLayer = layers.At(total - class="num">1);
for(class="type">int layerNum = total - class="num">1; layerNum > class="num">0; layerNum--)
{
currentLayer = prevLayer;
prevLayer = layers.At(layerNum - class="num">1);
neuron = currentLayer.At(class="num">0);
if(!neuron.UpdateInputWeights(prevLayer.At(class="num">0), SecondInput))
class="kw">return false;
}
class="type">bool result=false;
for(class="type">int layerNum = class="num">0; layerNum < total; layerNum++)
{
currentLayer = layers.At(layerNum);
CNeuronBaseOCL *temp = currentLayer.At(class="num">0);「Actor 网络的四层卷积堆叠」
在强化学习交易代理里,Actor 负责输出动作策略,其结构描述由 CreateDescriptions 函数填充。若传入的 actor 指针为空,代码会 new 一个 CArrayObj 并做空指针回退,保证后续 Add 操作不崩。 输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,激活设为 None,优化用 ADAM;紧跟的 BatchNorm 层 batch 写死 1000,对输入做归一化,能缓解外汇小时线均值漂移带来的梯度问题。 随后两层 defNeuronConvOCL 是重点:第一层 window=2、window_out=8、步长 1,把 prev_count 减 1;第二层 window=8、步长 8、window_out=8,相当于对特征做 8 倍降采样。最后一层全连接 256 节点、LReLU 激活。 每层 Add 失败都会 delete descr 并返回 false,MT5 里跑这套若报 false,优先查前层 count 是否算错——比如 prev_count 在卷积后没更新就会让维度对不上。贵金属与外汇杠杆高,模型结构错配可能放大回测虚高,实盘前务必小样本验证。
class="type">bool CreateDescriptions(CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) 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; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.window = class="num">0; 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 = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; 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">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; prev_count = descr.count = prev_count - class="num">1; descr.window = class="num">2; descr.step = class="num">1; descr.window_out = class="num">8; 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 = defNeuronConvOCL; prev_count = descr.count = prev_count; descr.window = class="num">8; descr.step = class="num">8; descr.window_out = class="num">8; 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">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.optimization = ADAM; descr.activation = LReLU; if(!actor.Add(descr)) { class="kw">delete descr;
Actor 与 Critic 的深层网络拼装
这段逻辑在 MT5 里把强化学习的双网络逐层堆起来:Actor 从第 5 层到第 9 层,分别用 128、256、256、256、6 个神经元收口,最后一层 6 个输出单元大概率对应六类动作空间。 Critic 侧先清结构,再建输入层 256 节点、随后第 1 层用 Concatenate 把前层 256 维窗口与步长 6 的动作拼接成 128 维,第 2 层回到 128 个 LReLU 基元。 每层都先 new 描述符、失败即 delete 并返回 false,这种写法在 EA 初始化阶段若显存或对象池紧张会直接中断,开 MT5 跑前先确认终端版本对 OCL 类支持。 外汇与贵金属行情下用此类网络做决策属高风险,过拟合与滑点可能让回测倾向失真,参数只作验证用。
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 = class="num">128; 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">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = class="num">256; descr.window = prev_count; descr.step = AccountDescr; descr.optimization = ADAM; descr.activation = LReLU; if(!actor.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; descr.count = class="num">256; 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">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; 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">9 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = class="num">6; descr.optimization = ADAM; descr.activation = LReLU; 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 = class="num">256; descr.window = class="num">0; 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 = class="num">128; descr.window = prev_count; descr.step = class="num">6; 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 = class="num">128; descr.activation = LReLU;
◍ 判别器后几层与初始化里的模型装载
判别网络在第三、四层继续用 ADAM 优化器堆叠。第三层为 128 个 LReLU 激活的基元神经元,第四层退化为 1 个无激活神经元,输出标量评分;任何一层 Add 失败就 delete 描述符并回 false,避免悬空对象。 宏 LatentLayer 被定义为 6,暗示潜变量维度,后续采样或噪声注入会用到这个常量,改它等于改网络瓶颈宽度。 OnInit 里先把 RSI(周期 RSIPeriod)、CCI(CCIPeriod)、ATR(ATRPeriod)、MACD(快/慢/信号周期) 全部按当前图表品种与时间框架创建,并统一 BufferResize 到 HistoryBars。若四个指标任一创建或扩容失败,直接 INIT_FAILED,EA 不会上线交易。 模型装载段尝试从 FileName 指向的路径读 Act.nnw 与 Crt.nnw,同时把同一份权重塞进 TargetActor / TargetCritic,做双网络影子拷贝;若文件缺失则现场 new 出 actor 与 critic 数组并调 CreateDescriptions 重建拓扑。外汇与贵金属杠杆高,这类未训练模型直接跑实盘可能瞬间回撤,建议先开 MT5 用策略测试器跑空载看 OnInit 日志。
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 = class="num">128; 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 = class="num">1; 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="macro">#define LatentLayer class="num">6 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!Symb.Name(_Symbol)) class="kw">return INIT_FAILED; Symb.Refresh(); class=class="str">"cmt">//--- if(!RSI.Create(Symb.Name(), TimeFrame, RSIPeriod, RSIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- if(!CCI.Create(Symb.Name(), TimeFrame, CCIPeriod, CCIPrice)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- if(!ATR.Create(Symb.Name(), TimeFrame, ATRPeriod)) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- if(!MACD.Create(Symb.Name(), TimeFrame, FastPeriod, SlowPeriod, SignalPeriod, MACDPrice)) class="kw">return INIT_FAILED; if(!RSI.BufferResize(HistoryBars) || !CCI.BufferResize(HistoryBars) || !ATR.BufferResize(HistoryBars) || !MACD.BufferResize(HistoryBars)) { PrintFormat("%s -> %d", __FUNCTION__, __LINE__); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- if(!Trade.SetTypeFillingBySymbol(Symb.Name())) class="kw">return INIT_FAILED; class=class="str">"cmt">//--- load models class="type">float temp; if(!Actor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !Critic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true) || !TargetActor.Load(FileName + "Act.nnw", temp, temp, temp, dtStudied, true) || !TargetCritic.Load(FileName + "Crt.nnw", temp, temp, temp, dtStudied, true)) { CArrayObj *actor = new CArrayObj(); CArrayObj *critic = new CArrayObj(); if(!CreateDescriptions(actor, critic))
「初始化与退出时的网络闭环校验」
EA 初始化阶段对 actor / critic 网络做硬性维度校验:动作空间必须严格等于 6,否则直接返回 INIT_FAILED。这段代码里 Result.Total() != 6 的判定,就是防止你改了输出层却忘了同步动作枚举。 输入状态维度也有约束:Actor 第 0 层输出必须等于 HistoryBars * BarDescr,Critic 第 0 层输入必须等于 Actor 隐层(LatentLayer)输出维度。任何一处对不上,Init 就终止,不会带着错配网络跑实盘。 OnDeinit 里会把 TargetActor / TargetCritic 用 Tau 系数软更新回主网络,并落盘为 Act.nnw / Crt.nnw。注意这里存的是 target 网络权重,不是在线训练的 actor——回测中断后重载,倾向从 target 恢复策略稳定性。 外汇与贵金属杠杆高、滑点跳空频繁,这类 RL 模型在 MT5 实盘前务必用历史数据多轮验证维度与权重文件一致性,可能避免初始化静默失败。
class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } if(!Actor.Create(actor) || !Critic.Create(critic) || !TargetActor.Create(actor) || !TargetCritic.Create(critic)) { class="kw">delete actor; class="kw">delete critic; class="kw">return INIT_FAILED; } class="kw">delete actor; class="kw">delete critic; class=class="str">"cmt">//--- } COpenCLMy *opencl = Actor.GetOpenCL(); Critic.SetOpenCL(opencl); TargetActor.SetOpenCL(opencl); TargetCritic.SetOpenCL(opencl); Actor.getResults(Result); if(Result.Total() != class="num">6) { PrintFormat("The scope of the actor does not match the actions count(%d <> %d)", class="num">6, Result.Total()); class="kw">return INIT_FAILED; } ActorResult = vector<class="type">float>::Zeros(class="num">6); class=class="str">"cmt">//--- Actor.GetLayerOutput(class="num">0, Result); if(Result.Total() != (HistoryBars * BarDescr)) { PrintFormat("Input size of Actor doesn&class="macro">#x27;t match state description(%d <> %d)", Result.Total(), (HistoryBars * BarDescr)); class="kw">return INIT_FAILED; } class=class="str">"cmt">//--- Actor.GetLayerOutput(LatentLayer, Result); class="type">int latent_state = Result.Total(); Critic.GetLayerOutput(class="num">0, Result); if(Result.Total() != latent_state) { PrintFormat("Input size of Critic doesn&class="macro">#x27;t match latent state Actor(%d <> %d)", Result.Total(), latent_state); class="kw">return INIT_FAILED; } PrevBalance = AccountInfoDouble(ACCOUNT_BALANCE); PrevEquity = AccountInfoDouble(ACCOUNT_EQUITY); FirstBar = true; Gradient.BufferInit(AccountDescr, class="num">0); Gradient.BufferCreate(opencl); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- TargetActor.WeightsUpdate(GetPointer(Actor), Tau); TargetCritic.WeightsUpdate(GetPointer(Critic), Tau); TargetActor.Save(FileName + "Act.nnw", Actor.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); TargetCritic.Save(FileName + "Crt.nnw", Critic.getRecentAverageError(), class="num">0, class="num">0, TimeCurrent(), true); class="kw">delete Result; } class="type">void OnTick() { class=class="str">"cmt">//---
把网络输出变成下单手数与止损位
新 K 线触发后先回拷历史 Rate 数组并翻转序列,再逐个 Refresh 指标与品种对象,这一步卡住就直接 return,避免用脏数据喂网络。 如果是非首根 K 线,先用 Target 网络做前向、用余额差加 Critic 估值算 reward;当账户余额没动且多空信号都为 0 时,reward 再扣 1,用来惩罚“看了半天不下单”的惰性策略。外汇与贵金属杠杆高,这种惩罚项只是概率上引导模型更积极,不保证任何收益。 主网络前向跑完,Critic 结果会人为加一个绝对值的 0.0001 倍噪声再反传,相当于给梯度加点扰动做探索;随后把 Account 快照存进 PrevAccount 并建 OpenCL 缓冲,供下一根 K 线算梯度用。 真正下单前,把 Actor 输出的 vector 转成 lot:buy_lot 用 ActorResult[0] 除以最小手数四舍五入,sell_lot 取 [3];买单价差保护 stops 取 StopsLevel 与 1 的较大值乘 Point。买 TP 是 Ask 加 [1]、买 SL 是 Ask 减 [2],均按品种 Digits 归一化。打开 MT5 把这段接在 EA 的 OnTick 末尾,就能看到神经网络决策如何落地成具体挂单价。
if(!IsNewBar()) class="kw">return; class=class="str">"cmt">//--- class="type">int bars = CopyRates(Symb.Name(), TimeFrame, iTime(Symb.Name(), TimeFrame, class="num">1), HistoryBars, Rates); if(!ArraySetAsSeries(Rates, true)) class="kw">return; class=class="str">"cmt">//--- RSI.Refresh(); CCI.Refresh(); ATR.Refresh(); MACD.Refresh(); Symb.Refresh(); Symb.RefreshRates(); if(!FirstBar) { if(!TargetActor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account))) class="kw">return; if(!TargetCritic.feedForward(GetPointer(TargetActor), LatentLayer, GetPointer(TargetActor))) class="kw">return; TargetCritic.getResults(Result); class="type">float reward = (class="type">float)(account[class="num">0] - PrevBalance + Result[class="num">0]); if(account[class="num">0] == PrevBalance) if((buy_value + sell_value) == class="num">0) reward -= class="num">1; Result.Update(class="num">0, reward); if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(PrevAccount), GetPointer(Gradient))) class="kw">return; } if(!Actor.feedForward(GetPointer(State), class="num">1, false, GetPointer(Account))) class="kw">return; if(!Critic.feedForward(GetPointer(Actor), LatentLayer, GetPointer(Actor))) class="kw">return; if(!FirstBar) { Critic.getResults(Result); Result.Update(class="num">0, Result.At(class="num">0) + MathAbs(Result.At(class="num">0) * class="num">0.0001f)); Critic.TrainMode(false); if(!Critic.backProp(Result, GetPointer(Actor)) || !Actor.backPropGradient(GetPointer(Account), GetPointer(Gradient))) class="kw">return; Critic.TrainMode(true); } FirstBar = false; PrevAccount.AssignArray(GetPointer(Account)); PrevAccount.BufferCreate(Actor.GetOpenCL()); PrevBalance = account[class="num">0]; PrevEquity = account[class="num">1]; vector<class="type">float> temp; Actor.getResults(temp); class="type">float delta = MathAbs(ActorResult - temp).Sum(); ActorResult = temp; class=class="str">"cmt">//--- class="type">class="kw">double min_lot = Symb.LotsMin(); class="type">class="kw">double stops = MathMax(Symb.StopsLevel(), class="num">1) * Symb.Point(); class="type">class="kw">double buy_lot = MathRound((class="type">class="kw">double)ActorResult[class="num">0] / min_lot) * min_lot; class="type">class="kw">double sell_lot = MathRound((class="type">class="kw">double)ActorResult[class="num">3] / min_lot) * min_lot; class="type">class="kw">double buy_tp = NormalizeDouble(Symb.Ask() + ActorResult[class="num">1], Symb.Digits()); class="type">class="kw">double buy_sl = NormalizeDouble(Symb.Ask() - ActorResult[class="num">2], Symb.Digits());