神经网络变得简单(第 84 部分):可逆归一化(RevIN)·进阶篇
◍ 反向归一化核里的梯度回传细节
在 MT5 的 OpenCL 神经网络推理中,RevInHiddenGraddient 这个核负责把输出梯度反算回输入梯度,相当于训练时的反向归一化步骤。 代码里先通过 get_global_id(0) 拿到当前神经元索引 n,再用 (n * optimization == 0 ? 7 : 9) % options_size 算出偏移 shift——这个三元表达式说明优化开关会让参数读取位置在 7 和 9 之间切换,实际部署时若 optimization 参数配错,梯度可能读到错误的方差项。 variance 取自 options[shift+1],k 取自 options[shift+3],当 k 不为零时才做 sqrt(variance) * output_gr[n] / k 的缩放;若结果为 NaN 则强制归零,避免 GPU 上浮点异常扩散到整层。 激活函数反向分三种:case 0 是 Tanh 类(钳制到 ±1 后乘 1−x²),case 1 是 Sigmoid 类(钳制到 0~1 后乘 x(1−x)),case 2 是带 0.01 负半轴的 Leaky 类。改激活类型时,直接调 activation 入参即可,不用动主体逻辑。 外层调度里 defNeuronRevInDenormOCL 会检查 layers 总数和图层类型,若目标层不是 defNeuronBatchNormOCL 就 delete temp 并返 false——这意味着归一化层与反归一化层必须配对,否则网络构建直接失败。外汇与贵金属模型用这套逻辑时,注意 GPU 驱动版本差异可能导致 CLayer 类型校验行为偏移,属高风险调试项。
printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void RevInHiddenGraddient(__global class="type">float *inputs, __global class="type">float *inputs_gr, __global class="type">float *options, __global class="type">float *output_gr, class="type">int options_size, class="type">int optimization, class="type">int activation) { class="type">int n = get_global_id(class="num">0); class="type">int shift = (n * optimization == class="num">0 ? class="num">7 : class="num">9) % options_size; class="type">float variance = options[shift + class="num">1]; class="type">float inp = inputs[n]; class="type">float k = options[shift + class="num">3]; class="type">float res = class="num">0; if(k != class="num">0) res = sqrt(variance) * output_gr[n] / k; if(isnan(res)) res = class="num">0; class="kw">switch(activation) { case class="num">0: res = clamp(res + inp, -class="num">1.0f, class="num">1.0f) - inp; res = res * (class="num">1 - pow(inp == class="num">1 || inp == -class="num">1 ? class="num">0.99999999f : inp, class="num">2)); break; case class="num">1: res= clamp(res + inp, class="num">0.0f, class="num">1.0f) - inp; res = res * (inp == class="num">0 || inp == class="num">1 ? class="num">0.00000001f : (inp * (class="num">1 - inp))); break; case class="num">2: if(inp < class="num">0) res *= class="num">0.01f; break; class="kw">default: break; } class=class="str">"cmt">//--- inputs_gr[n] = res; } case defNeuronRevInDenormOCL: if(desc.layers>=layers.Total()) { class="kw">delete temp; class="kw">return false; } if(((CLayer *)layers.At(desc.layers)).At(class="num">0).Type()!=defNeuronBatchNormOCL) { class="kw">delete temp;
反向归一化层的实例化与网络读取
在神经网络描述符的 switch 分支里,遇到 defNeuronRevInDenormOCL 类型时会先校验 OpenCL 指针有效性,无效则直接 return false。随后用 new 创建 CNeuronRevINDenormOCL 对象,若指针无效将 result 置 false;Init 传入 iOutputs、index、OpenCL、层数 1、索引 -1 与 NULL 父层,成功就把对象挂到 m_data[index] 并返回 true,失败则 delete 回收。 前一个分支对 revin 的初始化更重:需传入 outputs、0、opencl、desc.count、desc.layers 以及取到的具体层指针,任何一步 Add 或 Init 失败都会顺序 delete temp 与 revin 并回 false,避免半初始化对象泄漏。 CNet::Load 从文件句柄读结构时,先用 FileReadInteger(file_handle, INT_VALUE) 取数组长度 num,非 0 才循环 new CLayer(0, file_handle, opencl) 并调 Layer.Load;任一层 Load 失败就 break,意味着权重文件损坏时网络只加载到断点层。 开 MT5 把这段贴进 EA 的神经网络模块,故意传一个 POINTER_INVALID 的 OpenCL 上下文,能复现第二分支直接返回 false 且不产生 revin 对象的行为。
class="kw">return false; } revin = new CNeuronRevINDenormOCL(); if(!revin) { class="kw">delete temp; class="kw">return false; } if(!revin.Init(outputs, class="num">0, opencl, desc.count, desc.layers, ((CLayer *)layers.At(desc.layers)).At(class="num">0))) { class="kw">delete temp; class="kw">delete revin; class="kw">return false; } if(!temp.Add(revin)) { class="kw">delete temp; class="kw">delete revin; class="kw">return false; } break; case defNeuronRevInDenormOCL: if(CheckPointer(OpenCL) == POINTER_INVALID) class="kw">return false; revin = new CNeuronRevINDenormOCL(); if(CheckPointer(revin) == POINTER_INVALID) result = false; if(revin.Init(iOutputs, index, OpenCL, class="num">1, -class="num">1, NULL)) { m_data[index] = revin; class="kw">return true; } class="kw">delete revin; break; class="type">bool CNet::Load(const class="type">int file_handle) { ........ ........ class=class="str">"cmt">//--- read array length num = FileReadInteger(file_handle, INT_VALUE); class=class="str">"cmt">//--- read array if(num != class="num">0) { for(i = class="num">0; i < num; i++) { class=class="str">"cmt">//--- create new element CLayer *Layer = new CLayer(class="num">0, file_handle, opencl); if(!Layer.Load(file_handle)) break;
「反归一化层与编码器描述体的衔接细节」
在加载网络结构时,若第 0 层被识别为反归一化类型(defNeuronRevInDenormOCL),需要回链到指定的归一化层做参数初始化。代码先取该层索引 l = revin.GetNormLayer(),一旦 layers.At(l) 为空或对应神经元不是 defNeuronBatchNormOCL,就直接 delete Layer 并 break,避免挂上不兼容的结构。 初始化调用 revin.Init(...) 的最后一个参数是归一化层神经元指针 neuron,若返回失败同样释放 Layer 退出。这意味着反向结构强依赖前向 BatchNorm 层存在,否则整个网络描述作废,返回 layers.Total() == num 判定成败。 编码器构建从清空数组开始,输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器选 ADAM 且无激活。第一层紧跟 BatchNorm,batch 尺寸取 MathMax(1000, GPTBars),即至少 1000 条;第二层起用 Embedding 类型承接。 实盘里调 GPTBars 会直接改 BatchNorm 的批大小,过小可能让归一化统计抖动,外汇与贵金属波动下高风险,建议开 MT5 把 GPTBars 从默认改到 2000 以上观察层输出方差变化。
if(Layer.At(class="num">0).Type() == defNeuronRevInDenormOCL) { CNeuronRevINDenormOCL *revin = Layer.At(class="num">0); class="type">int l = revin.GetNormLayer(); if(!layers.At(l)) { class="kw">delete Layer; break; } CNeuronBaseOCL *neuron = ((CLayer *)layers.At(l)).At(class="num">0); if(neuron.Type() != defNeuronBatchNormOCL) { class="kw">delete Layer; break; } if(!revin.Init(revin.getConnections(), class="num">0, opencl, revin.Neurons(), l, neuron)) { class="kw">delete Layer; break; } } if(!layers.Add(Layer)) break; } } FileClose(file_handle); class=class="str">"cmt">//--- result class="kw">return (layers.Total() == num); } class="macro">#define NForecast class="num">6 class=class="str">"cmt">//Number of forecast class="type">bool CreateEncoderDescriptions(CArrayObj *encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.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.activation = None; descr.optimization = ADAM; if(!encoder.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 = MathMax(class="num">1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.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; {
◍ 编码器堆叠里的层定义套路
这段贴的是行情特征编码器后半段的层描述装配逻辑,从第三层一路叠到第十二层,每层都先 new 一个 CLayerDescription,填完参数再丢进 encoder.Add;任意一步失败就 delete 并 return false,保证对象不泄漏。 第三层用 defNeuronConvOCL,节点数直接拉到 prev_count*5,滑动步长和窗口都设为 prev_wout,输出窗口重写回 EmbeddingSize;第四层转 defNeuronPEOCL,窗口扩成 prev_wout*5 但不再改输出宽。 第五到第十层是 5 次循环,统一挂 defNeuronConformerOCL,step 固定为 4、内部 layers=5、window_out=EmbeddingSize,相当于叠了 5 个轻量 transformer 块做时序聚合。 第十一层收口到 defNeuronBaseOCL,count 变成 NForecast*BarDescr,激活用 TANH、优化用 ADAM;第十二层接 defNeuronRevInDenormOCL 做反归一化,activation 设 None、layers=1。外汇与贵金属市场杠杆高、滑点跳空频繁,这类结构只是特征抽取骨架,实盘信号质量仍要在 MT5 用历史Tick重跑验证。 CreateDescriptions 函数开头对 actor / critic 做了空指针兜底,为空就 new 一个 CArrayObj 再继续,避免上层调用方漏传容器导致后续 Add 崩掉。
class="type">int temp[] = {class="num">4, class="num">1, class="num">1, class="num">1, class="num">2}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; class="type">int prev_wout = descr.window_out = EmbeddingSize / class="num">2; if(!encoder.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; descr.count = prev_count * class="num">5; descr.step = descr.window = prev_wout; prev_wout = descr.window_out = EmbeddingSize; if(!encoder.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 = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout * class="num">5; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5-class="num">10 for(class="type">int i = class="num">0; i < class="num">5; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConformerOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = EmbeddingSize; descr.layers = class="num">5; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } } class=class="str">"cmt">//--- layer class="num">11 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NForecast*BarDescr; descr.activation = TANH; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">12 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronRevInDenormOCL; prev_count = descr.count = prev_count; descr.activation = None; descr.optimization = ADAM; descr.layers=class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } 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) {
Actor 网络的层定义与张量维度
在 MT5 里搭强化学习策略网络时,Actor 的层级结构直接决定信号生成的复杂度。下面这段构建逻辑把输入层到输出层逐层铺开,每一层都用 CLayerDescription 描述类型、神经元数和激活函数。 输入层承接账户状态特征,节点数等于 AccountDescr,激活选 None,优化器统一用 ADAM;紧接着的嵌入层把维度压到 EmbeddingSize,改用 SIGMOID 做非线性映射。 中间第 2 到 4 层是交叉注意力结构,循环 3 次生成。每层 units 设为 {1, NForecast}、windows 设为 {EmbeddingSize, BarDescr},输出窗口 window_out=16、滑动步长 step=4,无激活函数。这类设置让网络在 16 根 K 线的局部视窗内做跨序列关注,对外汇 1H 级别噪声有一定过滤倾向。 后续第 5 层回到基础全连接,LatentCount 个 SIGMOID 节点;第 6 层输出 2*NActions 个无激活节点,供均值方差拆分;第 7 层用 VAE 类型收口到 NActions 维动作空间。外汇与贵金属杠杆高,这套结构过拟合概率不低,建议先在 EURUSD 上用小样本回测验证梯度是否发散。
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 = AccountDescr; 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 = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2-class="num">4 for(class="type">int i = class="num">0; i < class="num">3; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {class="num">1, NForecast}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, BarDescr}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; 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">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; 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 = defNeuronBaseOCL; descr.count = class="num">2 * NActions; 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">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronVAEOCL; descr.count = NActions; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Critic critic.Clear();
「评论家网络的层堆叠与采样温度」
这段构建逻辑把 critic 网络从输入到输出逐层挂进容器,输入层直接吃 NActions 个动作维度,激活留 None、优化器统一走 ADAM,没有任何特征变换。第二层把维度压到 EmbeddingSize 并套 SIGMOID,算是给后续交叉注意力做嵌入准备。 中间 layer 2-4 是三段循环,类型换成 defNeuronCrossAttenOCL,units 设 {1, NForecast}、windows 设 {EmbeddingSize, BarDescr},window_out=16、step=4。这意味着每步滑 4 根 bar、吐 16 维输出,跨注意力在预测窗和 bar 描述上同时扫。 尾段 layer 5-6 是两个 LatentCount 维的 SIGMOID 隐层,layer 7 用 None 激活映射到 NRewards 个奖励维度,整网无池化、纯前馈加交叉注意力。 Train 里只露了一行:probability = GetProbTrajectories(Buffer, 0.9),0.9 是轨迹采样的温度系数。调低它会更贪心、调高则更偏探索,外汇与贵金属市场高波动下建议先拿 0.9 跑通再微调。
class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions; 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 = defNeuronBaseOCL; prev_count = descr.count = EmbeddingSize; descr.activation = SIGMOID; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2-class="num">4 for(class="type">int i = class="num">0; i < class="num">3; i++) { if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCrossAttenOCL; { class="type">int temp[] = {class="num">1, NForecast}; ArrayCopy(descr.units, temp); } { class="type">int temp[] = {EmbeddingSize, BarDescr}; ArrayCopy(descr.windows, temp); } descr.window_out = class="num">16; descr.step = class="num">4; 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">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!critic.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.activation = SIGMOID; descr.optimization = ADAM; if(!critic.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 = NRewards; descr.activation = None; descr.optimization = ADAM; if(!critic.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">void Train(class="type">void) { class=class="str">"cmt">//--- vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9); class=class="str">"cmt">//---