神经网络变得轻松(第三十四部分):全部参数化的分位数函数·进阶篇
(2/3)· 固定分位数假设拖慢训练又欠准,IQN 与 FQF 如何把选择权交还网络
◍ 分布式分位数网络的初始化与核函数落地
在 MT5 的 OpenCL 环境里搭 FQF(分位数回归)网络,第一步是把各子层对象逐一 Init 并指定激活函数。cSoftMax 与 cCosine 用 None 激活,而 cCosineEmbeding、cQuantile1 走 LReLU,cQuantile0 / cQuantile2 又回到 None——这种交错不是随意的,它对应分位数概率与特征映射的不同数值稳定性需求。 下面这段内核 FQF_Cosine 把 softmax 累积概率映射成余弦位置编码:result 先对前 i 项求和,再加当前项一半,最后 output = cos(i * π * result)。当你把 actions 设为 3、quantiles 设为 32 时,global_work_size[0] 会是 96,直接在 GPU 上并行算 96 条编码。 FQF_Output 内核则做分位数加权求和:对单个 action,遍历 total 个分位,result += quantiles[shift+i] * delta_taus[shift+i],输出该动作的预期值。外汇与贵金属行情跳空频繁,这类分布输出比单点预测更抗极端样本,但杠杆品种高风险,回测不等于实盘。 feedForward 里先跑 cFraction 再接 cSoftMax,随后用 OpenCL.SetArgumentBuffer 把输出缓冲绑到 def_k_FQF_Cosine 内核。若某层 Init 返回 false,整个前向直接中断——实盘前务必在策略测试器里单步看哪层索引越界。
if(!cSoftMax.Init(class="num">0, myIndex, open_cl, actions * quantiles, optimization, batch)) class="kw">return false; cSoftMax.SetHeads(actions); cSoftMax.SetActivationFunction(None); if(!cCosine.Init(numInputs, myIndex, open_cl, actions * quantiles, optimization, batch)) class="kw">return false; cCosine.SetActivationFunction(None); class=class="str">"cmt">//--- if(!cCosineEmbeding.Init(class="num">0, myIndex, open_cl, numInputs, optimization, batch)) class="kw">return false; cCosineEmbeding.SetActivationFunction(LReLU); if(!cQuantile0.Init(class="num">4 * actions * quantiles, myIndex, open_cl, numInputs, optimization, batch)) class="kw">return false; cQuantile0.SetActivationFunction(None); class=class="str">"cmt">//--- if(!cQuantile1.Init(actions * quantiles, myIndex, open_cl, class="num">4 * actions * quantiles, optimization, batch)) class="kw">return false; cQuantile1.SetActivationFunction(LReLU); class=class="str">"cmt">//--- if(!cQuantile2.Init(class="num">0, myIndex, open_cl, actions * quantiles, optimization, batch)) class="kw">return false; cQuantile2.SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void FQF_Cosine(__global class="type">float* softmax, __global class="type">float* output) { class="type">size_t i = get_global_id(class="num">0); class="type">size_t total = get_global_size(class="num">0); class="type">size_t action = get_global_id(class="num">1); class="type">int shift = action * total; class=class="str">"cmt">//--- class="type">float result = class="num">0; for(class="type">int it = class="num">0; it < i; it++) result += softmax[shift + it]; result += softmax[shift + i] / class="num">2.0f; output[shift + i] = cos(i * M_PI_F * result); } __kernel class="type">void FQF_Output(__global class="type">float* quantiles, __global class="type">float* delta_taus, __global class="type">float* output, class="type">uint total) { class="type">size_t action = get_global_id(class="num">0); class="type">int shift = action * total; class=class="str">"cmt">//--- class="type">float result = class="num">0; for(class="type">int i = class="num">0; i < total; i++) result += quantiles[shift + i] * delta_taus[shift + i]; output[action] = result; } class="type">bool CNeuronFQF::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cFraction.FeedForward(NeuronOCL)) class="kw">return false; if(!cSoftMax.FeedForward(GetPointer(cFraction))) class="kw">return false; { class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2]; global_work_size[class="num">1] = Output.Total(); global_work_size[class="num">0] = cSoftMax.Neurons() / global_work_size[class="num">1]; OpenCL.SetArgumentBuffer(def_k_FQF_Cosine, def_k_fqf_cosine_softmax, cSoftMax.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_Cosine, def_k_fqf_cosine_outputs, cCosine.getOutputIndex());
分位函数网络的 OpenCL 前向链路
这段实现把 FQF(分位数函数)网络的前向传播拆到 GPU 上跑。先执行 Cosine 核做周期嵌入,再走 Dropout 核做随机置零,最后由 FQF_Output 核汇总分位点和 tau 差分,输出分位预测。外汇与贵金属行情噪声大,这类网络在 MT5 上只适合做概率参考,实盘高风险。 Cosine 核调用时 global_work_size 设为 (cCosine.Neurons()+3)/4,相当于按 4 线程一组铺满神经元;若 Execute 返回 false,直接 printf 出 GetLastError() 并 return false,方便在专家日志里定位 OpenCL 设备异常。 Dropout 核把 cCosineEmbeding 的输出映射到 cQuantile0,维度参数用 cCosine.Neurons() 原样传入,工作组大小沿用上面那套除以 4 的算法。FQF_Output 核则把 cQuantile2 与 cSoftMax 的索引绑到输出缓冲,def_k_fqfout_total 用 cQuantile2.Neurons()/global_work_size[0] 算每个线程要归并的块数。 反向的 FQF_OutputGradient 核只露了头:用 get_global_id(0) 取线程号,get_global_size(0) 取总线程数,后面会按 i 和 total 做梯度散开。想验证的话,把这段贴进 MT5 的 OpenCL 管线,看专家日志里是否出现 kernel 执行报错。
if(!OpenCL.Execute(def_k_FQF_Cosine, class="num">2, global_work_offset, global_work_size)) { printf("Error of execution kernel FQF_Cosine: %d", GetLastError()); class="kw">return false; } } if(!cCosineEmbeding.FeedForward(GetPointer(cCosine))) class="kw">return false; { class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1] = {(cCosine.Neurons() + class="num">3) / class="num">4}; OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_input, NeuronOCL.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_map, cCosineEmbeding.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_Dropout, def_k_dout_out, cQuantile0.getOutputIndex()); OpenCL.SetArgument(def_k_Dropout, def_k_dout_dimension, (class="type">int)cCosine.Neurons()); if(!OpenCL.Execute(def_k_Dropout, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel Dropout: %d", GetLastError()); class="kw">return false; } } if(!cQuantile1.FeedForward(GetPointer(cQuantile0))) class="kw">return false; class=class="str">"cmt">//--- if(!cQuantile2.FeedForward(GetPointer(cQuantile1))) class="kw">return false; { class="type">uint global_work_offset[class="num">1] = {class="num">0}; class="type">uint global_work_size[class="num">1] = { Neurons() }; OpenCL.SetArgumentBuffer(def_k_FQF_Output, def_k_fqfout_quantiles, cQuantile2.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_Output, def_k_fqfout_delta_taus, cSoftMax.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_Output, def_k_fqfout_output, getOutputIndex()); OpenCL.SetArgument(def_k_FQF_Output, def_k_fqfout_total, (class="type">uint)(cQuantile2.Neurons() / global_work_size[class="num">0])); if(!OpenCL.Execute(def_k_FQF_Output, class="num">1, global_work_offset, global_work_size)) { printf("Error of execution kernel FQF_Output: %d", GetLastError()); class="kw">return false; } } class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void FQF_OutputGradient(__global class="type">float* quantiles, __global class="type">float* delta_taus, __global class="type">float* output_gr, __global class="type">float* quantiles_gr, __global class="type">float* taus_gr ) { class="type">size_t i = get_global_id(class="num">0); class="type">size_t total = get_global_size(class="num">0);
「分位数与余弦梯度的内核实现」
在 FQF(分位数全连接)网络的 OpenCL 后端里,梯度的反向传播被拆成了多个 __kernel 函数并行跑。get_global_id(0) 取分位数索引 i,get_global_id(1) 取动作索引 action,shift = action * total 把二维梯度摊平成一维偏移,这是典型的批处理写法。 FQF_QuantileGradient 里直接拿 quantiles_gr[shift+i] 乘以 taus_embeding 写回 state_gr,再乘 state_embeding 写回 taus_gr,相当于把分位层梯度分流到状态和分位嵌入两条支路。 FQF_CosineGradient 最重:先用 for 循环累加 softmax 前缀和 cumul,再算 -π·i·sin(π·i·(cumul+softmax/2))·output_gr 的本项贡献;随后遍历 i 之后的项,用 temp = cumul + softmax[it]/2 做加权,把后续分位点的梯度按 1/temp 比例回传。注意 M_PI_F 是 OpenCL 单精度 π,不是双精度 M_PI。 CNeuronFQF::calcInputGradients 负责把上面内核绑参:global_work_size 设为 { cSoftMax.Neurons()/Neurons(), Neurons() },也就是每个动作对应一组 softmax 神经元。在 MT5 里接好 OpenCL 设备后,改 Neurons() 数量会直接改变 global_work_size[1],可能让旧显卡因最大工作组维度超限而报错,调参前先看 clGetDeviceInfo 的 MAX_WORK_ITEM_SIZES。
__kernel class="type">void FQF_QuantileGradient(__global class="type">float* state_embeding, __global class="type">float* taus_embeding, __global class="type">float* quantiles_gr, __global class="type">float* state_gr, __global class="type">float* taus_gr ) { class="type">size_t i = get_global_id(class="num">0); class="type">size_t total = get_global_size(class="num">0); class="type">size_t action = get_global_id(class="num">1); class="type">int shift = action * total; class=class="str">"cmt">//--- class="type">float gradient = quantiles_gr[shift + i]; state_gr[shift + i] = gradient * taus_embeding[shift + i]; taus_gr[shift + i] = gradient * state_embeding[shift + i]; } __kernel class="type">void FQF_CosineGradient(__global class="type">float* softmax, __global class="type">float* output_gr, __global class="type">float* softmax_gr ) { class="type">size_t i = get_global_id(class="num">0); class="type">size_t total = get_global_size(class="num">0); class="type">size_t action = get_global_id(class="num">1); class="type">int shift = action * total; class="type">float cumul = class="num">0; for(class="type">int it = class="num">0; it < i; it++) cumul += softmax[shift + it]; class="type">float result = -M_PI_F * i * sin(M_PI_F * i * (cumul + softmax[shift + i] / class="num">2)) * output_gr[shift + i]; for(class="type">int it = i + class="num">1; it < total; it++) { cumul += softmax[shift + it - class="num">1]; class="type">float temp = cumul + softmax[shift + it] / class="num">2; result += -M_PI_F * it * sin(M_PI_F * it * temp) * output_gr[shift + it] * softmax[shift + it] / temp; } softmax_gr[shift + i] += result; } class="type">bool CNeuronFQF::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !Gradient || !Output) class="kw">return false; { class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2] = { cSoftMax.Neurons() / Neurons(), Neurons() }; OpenCL.SetArgumentBuffer(def_k_FQF_OutputGradient, def_k_fqfoutgr_quantiles, cQuantile2.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_OutputGradient, def_k_fqfoutgr_taus, cSoftMax.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_OutputGradient, def_k_fqfoutgr_output_gr, getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_OutputGradient, def_k_fqfoutgr_quantiles_gr, cQuantile2.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_OutputGradient, def_k_fqfoutgr_taus_gr, cSoftMax.getGradientIndex());
◍ 反向传播里的分位数梯度内核调用
FQF 模型的反向传播不是串行算完就完事,而是把各层梯度拆给不同的 OpenCL 内核并行跑。下面这段把输出梯度、分位数梯度和余弦梯度三个内核的调用顺序与参数绑定直接列出来,方便你在 MT5 里对照自己写的神经网络层。 先算隐藏层梯度:cQuantile1 拿 cQuantile2 的指针算隐藏梯度,cQuantile0 再拿 cQuantile1 的指针算,任何一步返回 false 就直接中断。这一步没走 GPU 内核,是 CPU 侧的梯度链式回传。 接着是 FQF_OutputGradient 内核,global_work_size 设成 {Neurons(), 1},也就是按神经元数量铺一维任务。若 Execute 返回失败,printf 打出 GetLastError() 的错误码并 return false,这种错误码在 MT5 终端里通常是 4001 以后的 OpenCL 相关异常。 FQF_QuantileGradient 内核绑定了 5 个缓冲区:状态嵌入、tau 嵌入、分位数梯度、状态梯度和 tau 梯度,分别用 SetArgumentBuffer 指到对应层的输出或梯度索引。它的 global_work_size 同样是 {cCosineEmbeding.Neurons(), 1},维度对齐嵌入层神经元数。 最后一个 FQF_CosineGradient 内核的 global_work_size 是 {cSoftMax.Neurons() / Neurons(), Neurons()},这里用了二维划分,X 维是 softmax 神经元除以总神经元数,Y 维是总神经元数。绑参时把 softmax 输出、余弦梯度和 softmax 梯度三个 buffer 接好,执行失败同样打印内核名和错误码退出。外汇与贵金属市场波动剧烈,这类 GPU 加速模型若用于实盘信号,回测与前向耗时必须先在模拟环境压测。
if(!OpenCL.Execute(def_k_FQF_OutputGradient, class="num">2, global_work_offset, global_work_size)) { printf("Error of execution kernel FQF_OutputGradient: %d", GetLastError()); class="kw">return false; } } if(!cQuantile1.calcHiddenGradients(GetPointer(cQuantile2))) class="kw">return false; if(!cQuantile0.calcHiddenGradients(GetPointer(cQuantile1))) class="kw">return false; { class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2] = { cCosineEmbeding.Neurons(), class="num">1 }; OpenCL.SetArgumentBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_state_enbeding, NeuronOCL.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_taus_embedding, cCosineEmbeding.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_quantiles_gr, cQuantile0.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_state_gr, NeuronOCL.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_QuantileGradient, def_k_fqfqgr_taus_gr, cCosineEmbeding.getGradientIndex()); if(!OpenCL.Execute(def_k_FQF_QuantileGradient, class="num">2, global_work_offset, global_work_size)) { printf("Error of execution kernel FQF_OutputGradient: %d", GetLastError()); class="kw">return false; } } if(!cCosine.calcHiddenGradients(GetPointer(cCosineEmbeding))) class="kw">return false; { class="type">uint global_work_offset[class="num">2] = {class="num">0, class="num">0}; class="type">uint global_work_size[class="num">2] = { cSoftMax.Neurons() / Neurons(), Neurons() }; OpenCL.SetArgumentBuffer(def_k_FQF_CosineGradient, def_k_fqfcosgr_softmax, cSoftMax.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_CosineGradient, def_k_fqfcosgr_output_gr, cCosine.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_FQF_CosineGradient, def_k_fqfcosgr_softmax_gr, cSoftMax.getGradientIndex()); if(!OpenCL.Execute(def_k_FQF_CosineGradient, class="num">2, global_work_offset, global_work_size)) { printf("Error of execution kernel FQF_CosineGradient: %d", GetLastError()); class="kw">return false; } } if(!cSoftMax.calcInputGradients(GetPointer(cFraction))) class="kw">return false; class=class="str">"cmt">//---
分位数网络权重回传与FQF类骨架
CNeuronFQF::updateInputWeights 负责把上游梯度按组件链回传:先更新 Fraction 子网络权重,再依次更新余弦嵌入层、分位数 0→1、分位数 1→2 的链接权重,任意一步失败立即返回 false,全过才返回 true。 这套链式更新意味着量化分层是嵌套依赖的——改了底层分位数 0 的权重,必须等 cQuantile1、cQuantile2 逐级回传成功,整层才算收敛;在 MT5 里若某次 return false,优先查 cCosine 指针是否有效。 外层 CFQF 类继承自 CNet,私有成员 iCountBackProp 记录反向传播计数,保护成员 iUpdateTarget 控制目标网络同步节奏,cTargetNet 持有独立目标网络实例。 公开接口里 feedForward 默认窗口 1、tem=true 走温度采样;backProp 折扣因子默认 0.9f,支持传入 nextState 做双网络更新;getAction / getSample 分别取贪心动作与训练抽样,getRecentAverageError 直接暴露近期平均误差供盯盘脚本读取。 Save 方法把 iUpdateTarget 作为元信息写入文件,Load 反向恢复;Type() 固定返回 defFQF,TrainMode 透传基类开关。外汇与贵金属行情下用此类做强化学习决策,需明确杠杆与滑点会带来显著回测偏差,实盘前务必在策略测试器跑至少 3 个月 Tick 级数据。
class="kw">return true; class="type">bool CNeuronFQF::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cFraction.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cCosineEmbeding.UpdateInputWeights(GetPointer(cCosine))) class="kw">return false; if(!cQuantile1.UpdateInputWeights(GetPointer(cQuantile0))) class="kw">return false; if(!cQuantile2.UpdateInputWeights(GetPointer(cQuantile1))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CFQF : class="kw">protected CNet { class="kw">private: class="type">uint iCountBackProp; class="kw">protected: class="type">uint iUpdateTarget; class=class="str">"cmt">//--- CNet cTargetNet; class="kw">public: CFQF(class="type">void); CFQF(CArrayObj *Description) { Create(Description); } class="type">bool Create(CArrayObj *Description); ~CFQF(class="type">void); class="type">bool feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true) { class="kw">return CNet::feedForward(inputVals, window, tem); } class="type">bool backProp(CBufferFloat *targetVals, class="type">float discount = class="num">0.9f, CArrayFloat *nextState = NULL, class="type">int window = class="num">1, class="type">bool tem = true); class="type">void getResults(CBufferFloat *&resultVals); class="type">int getAction(class="type">void); class="type">int getSample(class="type">void); class="type">float getRecentAverageError() { class="kw">return recentAverageError; } class="type">bool Save(class="type">class="kw">string file_name, class="type">class="kw">datetime time, class="type">bool common = true) { class="kw">return CNet::Save(file_name, getRecentAverageError(), (class="type">float)iUpdateTarget, class="num">0, time, common); } class="kw">virtual class="type">bool Save(class="kw">const class="type">int file_handle); class="kw">virtual class="type">bool Load(class="type">class="kw">string file_name, class="type">class="kw">datetime &time, class="type">bool common = true); class="kw">virtual class="type">bool Load(class="kw">const class="type">int file_handle); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defFQF; } class="kw">virtual class="type">bool TrainMode(class="type">bool flag) { class="kw">return CNet::TrainMode(flag); }
「反向传播与目标网络同步的细节」
CFQF 类的 backProp 方法在存在下一状态 buffer 时,会先让目标网络 cTargetNet 做一次前馈,再把返回结果按折扣因子叠加到当前目标值上。默认 discount = 0.9f,意味着未来奖励的折损偏慢,若你想让智能体更看重即时盈亏,可把这个参数调小到 0.5f 左右再跑 MT5 观察行为变化。 目标网络权重不是每次反向传播都更新,而是由 iUpdateTarget 计数控制:iCountBackProp 累计到阈值才调用 UpdateTarget 写盘(默认文件名 FQF.upd 或宏定义的 .nnw),否则只自增计数。这种延迟同步能缓解训练震荡,但阈值设太大可能导致策略滞后,外汇与贵金属市场高波动下建议从小批量开始验证。 getAction 直接取输出 buffer 的最大值索引作为确定性动作,而 getSample 走的是另一套随机路径:对输出做 softmax 再累积求和,用均值 0.5、标准差 0.5 的正态随机数做分层抽样。两者并存说明该类同时支持贪心执行与探索采样,复制下面代码到 MT5 的 CFQF 实现里即可核对你本地版本是否一致。
class="kw">virtual class="type">bool GetLayerOutput(class="type">uint layer, CBufferFloat *&result) { class="kw">return CNet::GetLayerOutput(layer, result); } class=class="str">"cmt">//--- class="kw">virtual class="type">void SetUpdateTarget(class="type">uint batch) { iUpdateTarget = batch; } class="kw">virtual class="type">bool UpdateTarget(class="type">class="kw">string file_name); }; class="type">bool CFQF::backProp(CBufferFloat *targetVals, class="type">float discount = class="num">0.9f, CArrayFloat *nextState = NULL, class="type">int window = class="num">1, class="type">bool tem = true) { class=class="str">"cmt">//--- if(!targetVals) class="kw">return false; if(!!nextState) { vectorf target; if(!targetVals.GetData(target) || target.Size() <= class="num">0) class="kw">return false; if(!cTargetNet.feedForward(nextState, window, tem)) class="kw">return false; cTargetNet.getResults(targetVals); if(!targetVals) class="kw">return false; target = target + discount * targetVals.Maximum(); if(!targetVals.AssignArray(target)) class="kw">return false; } if(iCountBackProp >= iUpdateTarget) { class="macro">#ifdef FileName if(UpdateTarget(FileName + ".nnw")) class="macro">#else if(UpdateTarget("FQF.upd")) class="macro">#endif iCountBackProp = class="num">0; } else iCountBackProp++; class="kw">return CNet::backProp(targetVals); } class="type">int CFQF::getAction(class="type">void) { CBufferFloat *temp; CNet::getResults(temp); if(!temp) class="kw">return -class="num">1; class=class="str">"cmt">//--- class="kw">return temp.Maximum(class="num">0, temp.Total()); } class="type">int CFQF::getSample(class="type">void) { CBufferFloat* resultVals; CNet::getResults(resultVals); if(!resultVals) class="kw">return -class="num">1; vectorf temp; if(!resultVals.GetData(temp)) { class="kw">delete resultVals; class="kw">return -class="num">1; } class="kw">delete resultVals; class=class="str">"cmt">//--- if(!temp.Activation(temp, AF_SOFTMAX)) class="kw">return -class="num">1; temp = temp.CumSum(); class="type">int err_code; class="type">float random = (class="type">float)Math::MathRandomNormal(class="num">0.5, class="num">0.5, err_code); if(random >= class="num">1) class="kw">return (class="type">int)temp.Size() - class="num">1; for(class="type">int i = class="num">0; i < (class="type">int)temp.Size(); i++) if(random <= temp[i] && temp[i] > class="num">0) class="kw">return i; class=class="str">"cmt">//--- class="kw">return -class="num">1; }