神经网络变得简单(第 82 部分):常微分方程模型(NeuralODE)·进阶篇
(2/3)·离散隐藏层吃内存又累数值,这篇把 ODE 求解器当黑盒的端到端训练拆给你看
◍ 反向传播里的激活梯度拆解
在 MT5 的 OpenCL 内核里做神经网络训练,反向传播那一步最容易被忽略的是激活函数梯度怎么就地算。下面这段内核接收梯度张量、输入张量和权重,按三维全局 ID 定位每个样本-变量-时刻的单元,先把输出侧梯度乘权重累加成 sum,再做 NaN 防护。 sum 累加时用了 weight_step = dimension + 2 的偏移,说明权重矩阵每行多留了 2 个浮点位(偏置或辅助项),在 GPU 上跑时若 dimension=64,单样本权重跨度就是 66 个 float,显存排布得对齐。 激活类型用 switch 分流:case 0 是 Tanh,梯度乘 (1-out^2) 且下限截到 1e-4,避免饱和区直接归零;case 1 是 Sigmoid,乘 out*(1-out);case 2 是 LeakyReLU,负区梯度直接乘 0.01。外汇与贵金属行情噪声大,用 Tanh 时若输入不先归一化,sum 频繁触 clamp 会让梯度偏向边界,训练可能震荡。 把这段直接塞进你的 .cl 文件,用 clGetKernel 跑 dimension_out=32、variables=8 的小批,能肉眼看到 LeakyReLU 负区梯度恒为前向的 1% 这一手。
__global class="type">float *matrix_g, class=class="str">"cmt">///<[in] Gradient tensor __global class="type">float *matrix_i, class=class="str">"cmt">///<[in] Inputs tensor __global class="type">float *matrix_ig, class=class="str">"cmt">///<[out] Inputs Gradient tensor class="type">int dimension_out, class=class="str">"cmt">///< output dimension class="type">int activation class=class="str">"cmt">///< Input Activation type(class="macro">#ENUM_ACTIVATION) ) { class="type">int d = get_global_id(class="num">0); class="type">int dimension = get_global_size(class="num">0); class="type">int v = get_global_id(class="num">1); class="type">int variables = get_global_size(class="num">1); class="type">int i = get_global_id(class="num">2); class="type">int lenth = get_global_size(class="num">2); class="type">int shift = variables * i + v; class="type">int input_shift = shift * dimension + d; class="type">int output_shift = shift * dimension_out; class="type">int weight_step = (dimension + class="num">2); class="type">int weight_shift = (v * dimension_out) * weight_step + d; class="type">float sum = class="num">0; for(class="type">int k = class="num">0; k < dimension_out; k ++) sum += matrix_g[output_shift + k] * matrix_w[weight_shift + k * weight_step]; if(isnan(sum)) sum = class="num">0; class="type">float out = matrix_i[input_shift]; class="kw">switch(activation) { case class="num">0: out = clamp(out, -class="num">1.0f, class="num">1.0f); sum = clamp(sum + out, -class="num">1.0f, class="num">1.0f) - out; sum = sum * max(class="num">1 - pow(out, class="num">2), class="num">1.0e-4f); break; case class="num">1: out = clamp(out, class="num">0.0f, class="num">1.0f); sum = clamp(sum + out, class="num">0.0f, class="num">1.0f) - out; sum = sum * max(out * (class="num">1 - out), class="num">1.0e-4f); break; case class="num">2: if(out < class="num">0) sum *= class="num">0.01f; break;
前向传播里的核函数与张量路由
在 MT5 用 OpenCL 跑神经网络推理时,核心计算被拆成 __kernel 函数丢给 GPU。上面这段 FeedForwardNODEInpK 就是典型:它接收输入张量 matrix_i 和 6 组权重张量 K1~K6,再结合 matrix_beta 偏置,算出输出张量 matrix_o。
线程索引由 get_global_id(0) 拿到,每个线程处理一个样本点;初始 sum = matrix_i[i],随后对 b=0 到 5 做循环。若某路 beta 为 0 或 NaN,直接 continue 跳过——这能避免脏权重把整批推理带偏。
循环里用 switch(b) 把对应 K 张量的值取出来,6 个 case 分别映射 K1 至 K6。外汇与贵金属市场高杠杆、高风险,这类 GPU 核若 beta 未做 NaN 防护,回测时可能出现某根 XAUUSD 的 M15 K 线推理结果全 0 的幽灵信号。
把这段代码原样贴进 MT5 的 OpenCL 程序,改 matrix_beta 里某一个值为 0.0f,对比输出张量变化,就能验证跳过逻辑是否生效。
class="kw">default: break; } class=class="str">"cmt">//--- matrix_ig[input_shift] = sum; } __kernel class="type">void FeedForwardNODEInpK(__global class="type">float *matrix_i, class=class="str">"cmt">///<- [in] Inputs tensor __global class="type">float *matrix_k1, class=class="str">"cmt">///<- [in] K1 tensor __global class="type">float *matrix_k2, class=class="str">"cmt">///<- [in] K2 tensor __global class="type">float *matrix_k3, class=class="str">"cmt">///<- [in] K3 tensor __global class="type">float *matrix_k4, class=class="str">"cmt">///<- [in] K4 tensor __global class="type">float *matrix_k5, class=class="str">"cmt">///<- [in] K5 tensor __global class="type">float *matrix_k6, class=class="str">"cmt">///<- [in] K6 tenтor __global class="type">float *matrix_beta, class=class="str">"cmt">///<- [in] beta tensor __global class="type">float *matrix_o class=class="str">"cmt">///<- [out] Output tensor ) { class="type">int i = get_global_id(class="num">0); class="type">float sum = matrix_i[i]; for(class="type">int b = class="num">0; b < class="num">6; b++) { class="type">float beta = matrix_beta[b]; if(beta == class="num">0.0f || isnan(beta)) class="kw">continue; class=class="str">"cmt">//--- class="type">float val = class="num">0.0f; class="kw">switch(b) { case class="num">0: val = matrix_k1[i]; break; case class="num">1: val = matrix_k2[i]; break; case class="num">2: val = matrix_k3[i]; break; case class="num">3: val = matrix_k4[i]; break; case class="num">4:
「反向传播里的梯度分流写法」
上面这段 OpenCL 内核做的是隐藏层梯度回传:把输出梯度 matrix_og[i] 直接塞回输入梯度 matrix_ig[i],再沿 6 个 K 张量分支拆开。循环里 b 从 0 走到 5,每个分支拿 beta[b] 乘 grad,写进对应的 matrix_kNg。 值得盯的一点:beta 或 val 一旦是 NaN,代码立刻压成 0.0f,而不是让脏数据继续向后扩散。这种防御在 MT5 跑 GPU 计算时很实在——显存里的 NaN 会静默污染整批张量,回测结果看着正常其实早已失真。 switch 分支里 case 0 只示出了 k1 的写入,后面 1~5 是同样结构。你在自己的 EA 里接这套内核时,先确认 matrix_beta 长度确实是 6,否则 get_global_id(0) 越界会直接 kernel 崩溃。外汇与贵金属杠杆高,这类底层计算错误不会提示止损,只会让信号 quietly 失效。
val = matrix_k5[i];
break;
case class="num">5:
val = matrix_k6[i];
break;
}
if(val == class="num">0.0f || isnan(val))
class="kw">continue;
class=class="str">"cmt">//---
sum += val * beta;
}
matrix_o[i] = sum;
}
__kernel class="type">void HiddenGradientNODEInpK(__global class="type">float *matrix_ig, class=class="str">"cmt">///<[in] Inputs tensor
__global class="type">float *matrix_k1g, class=class="str">"cmt">///<[in] K1 tensor
__global class="type">float *matrix_k2g, class=class="str">"cmt">///<[in] K2 tensor
__global class="type">float *matrix_k3g, class=class="str">"cmt">///<[in] K3 tensor
__global class="type">float *matrix_k4g, class=class="str">"cmt">///<[in] K4 tensor
__global class="type">float *matrix_k5g, class=class="str">"cmt">///<[in] K5 tensor
__global class="type">float *matrix_k6g, class=class="str">"cmt">///<[in] K6 tensor
__global class="type">float *matrix_beta, class=class="str">"cmt">///<[in] beta tensor
__global class="type">float *matrix_og class=class="str">"cmt">///<[out] Output tensor
)
{
class="type">int i = get_global_id(class="num">0);
class=class="str">"cmt">//---
class="type">float grad = matrix_og[i];
matrix_ig[i] = grad;
for(class="type">int b = class="num">0; b < class="num">6; b++)
{
class="type">float beta = matrix_beta[b];
if(isnan(beta))
beta = class="num">0.0f;
class=class="str">"cmt">//---
class="type">float val = beta * grad;
if(isnan(val))
val = class="num">0.0f;
class="kw">switch(b)
{
case class="num">0:
matrix_k1g[i] = val;
break;◍ 梯度张量如何落进权重矩阵
这段 OpenCL 内核把 6 个时间步的梯度分别写回对应的全局矩阵:case 1 到 5 各管一个 k 值,matrix_k2g 到 matrix_k6g 按索引 i 接收 val,case 分支用 break 隔离避免穿透。 后面的 NODEF_UpdateWeightsAdam 内核一口气吃进 6 组梯度(matrix_gk1~gk6)和 4 组输入(matrix_ik1~ik4),注释标明权重矩阵 matrix_w 是 in,out,梯度与输入均为只读 const float 指针。 在 MT5 里把这段贴进自定义指标或 EA 的 OpenCL 调用层,先确认显卡驱动支持 float 全局内存对齐;若某次回测中 k6 梯度突然为 0,大概率上游 k6 分支未触发,可单步打印 matrix_k6g 验证。外汇与贵金属市场波动剧烈,这类 GPU 并行权重更新仅在策略回测与信号生成中提供概率倾向,实盘仍属高风险。
case class="num">1: matrix_k2g[i] = val; break; case class="num">2: matrix_k3g[i] = val; break; case class="num">3: matrix_k4g[i] = val; break; case class="num">4: matrix_k5g[i] = val; break; case class="num">5: matrix_k6g[i] = val; break; } } } __kernel class="type">void NODEF_UpdateWeightsAdam(__global class="type">float *matrix_w, class=class="str">"cmt">///<[in,out] Weights matrix __global const class="type">float *matrix_gk1, class=class="str">"cmt">///<[in] Tensor of gradients at k1 __global const class="type">float *matrix_gk2, class=class="str">"cmt">///<[in] Tensor of gradients at k2 __global const class="type">float *matrix_gk3, class=class="str">"cmt">///<[in] Tensor of gradients at k3 __global const class="type">float *matrix_gk4, class=class="str">"cmt">///<[in] Tensor of gradients at k4 __global const class="type">float *matrix_gk5, class=class="str">"cmt">///<[in] Tensor of gradients at k5 __global const class="type">float *matrix_gk6, class=class="str">"cmt">///<[in] Tensor of gradients at k6 __global const class="type">float *matrix_ik1, class=class="str">"cmt">///<[in] Inputs tensor __global const class="type">float *matrix_ik2, class=class="str">"cmt">///<[in] Inputs tensor __global const class="type">float *matrix_ik3, class=class="str">"cmt">///<[in] Inputs tensor __global const class="type">float *matrix_ik4, class=class="str">"cmt">///<[in] Inputs tensor
Adam 更新核里的张量寻址
这段 OpenCL 内核负责在 GPU 上并行更新一层全连接权值,用的是 Adam 优化器那套一阶、二阶动量。传入的 matrix_ik5、matrix_ik6 是两个输入张量,matrix_m 与 matrix_v 分别是需要维护的一阶、二阶动量矩阵,alpha 是步长 h,lenth 是输入样本数,l、b1、b2 对应学习率和两个动量衰减系数。 寻址部分先通过 get_global_id 拿到三维索引:d_in 是输入维度下标,d_out 是输出维度下标,v 是样本/变量块下标。weight_shift 用 (v*dimension_out + d_out)*dimension_in 把权值铺成连续偏移,input_step 则减掉 2,说明输入特征里末尾两个位置可能留作偏置或标签,不参与滑动。 核函数开头先把权值读进本地变量 weight,梯度累加器 g 置 0,随后对 lenth 个样本跑 for 循环求梯度。想在 MT5 里验证,可直接把这段内核塞进自定义神经层,打印 weight_shift 与 input_shift 看是否和维度对得上。外汇与贵金属杠杆高,这类 GPU 训练若用于实盘信号,回测过拟合概率不低,需谨慎。
__global const class="type">float *matrix_ik5, class=class="str">"cmt">///<[in] Inputs tensor __global const class="type">float *matrix_ik6, class=class="str">"cmt">///<[in] Inputs tensor __global class="type">float *matrix_m, class=class="str">"cmt">///<[in,out] Matrix of first momentum __global class="type">float *matrix_v, class=class="str">"cmt">///<[in,out] Matrix of seconfd momentum __global const class="type">float *alpha, class=class="str">"cmt">///< h const class="type">int lenth, class=class="str">"cmt">///< Number of inputs const class="type">float l, class=class="str">"cmt">///< Learning rates const class="type">float b1, class=class="str">"cmt">///< First momentum multiplier const class="type">float b2 class=class="str">"cmt">///< Second momentum multiplier ) { const class="type">int d_in = get_global_id(class="num">0); const class="type">int dimension_in = get_global_size(class="num">0); const class="type">int d_out = get_global_id(class="num">1); const class="type">int dimension_out = get_global_size(class="num">1); const class="type">int v = get_global_id(class="num">2); const class="type">int variables = get_global_id(class="num">2); const class="type">int weight_shift = (v * dimension_out + d_out) * dimension_in; const class="type">int input_step = variables * (dimension_in - class="num">2); const class="type">int input_shift = v * (dimension_in - class="num">2) + d_in; const class="type">int output_step = variables * dimension_out; const class="type">int output_shift = v * dimension_out + d_out; class="type">float weight = matrix_w[weight_shift]; class="type">float g = class="num">0; for(class="type">int i = class="num">0; i < lenth; i++) {
「梯度聚合与权重更新的底层写法」
这段逻辑出现在自定义神经元类的训练循环尾部,负责把不同维度的梯度按分支累加到 g,再走 Adam 风格的一阶矩、二阶矩估计。维度差 dimension_in - d_in 等于 1 时直接六矩阵求和,等于 2 时乘 alpha[0..5] 权重,其余情况则与输入侧矩阵逐项点乘,分支清晰但计算量随维度非线性放大。 mt 与 vt 用 b1、b2 做指数滑动平均,delta 里除以 sqrt(vt)+1e-37f 避免除零,再减 l1*sign(weight)+l2*weight 的惩罚项;只有 delta*g>0 才允许 clamp 到 [-MAX_WEIGHT, MAX_WEIGHT] 内更新,这一道闸能压住外汇与贵金属样本噪声下的权重抖动,但高频重训仍属高风险操作。 Init 里 weights 算式为 (iDimension+2)*iDimension*iVariables,iBuffersK 与 iInputsK 都硬性扩到 18,说明该层至少预留了 18 条核缓冲;若你改 dimension 或 variables 后报数组越界,先核对这里的上限而不是去动 OpenCL 内核。
class="type">int shift_g = i * output_step + output_shift; class="type">int shift_i = i * input_step + input_shift; class="kw">switch(dimension_in - d_in) { case class="num">1: g += matrix_gk1[shift_g] + matrix_gk2[shift_g] + matrix_gk3[shift_g] + matrix_gk4[shift_g] + matrix_gk5[shift_g] + matrix_gk6[shift_g]; break; case class="num">2: g += matrix_gk1[shift_g] * alpha[class="num">0] + matrix_gk2[shift_g] * alpha[class="num">1] + matrix_gk3[shift_g] * alpha[class="num">2] + matrix_gk4[shift_g] * alpha[class="num">3] + matrix_gk5[shift_g] * alpha[class="num">4] + matrix_gk6[shift_g] * alpha[class="num">5]; break; class="kw">default: g += matrix_gk1[shift_g] * matrix_ik1[shift_i] + matrix_gk2[shift_g] * matrix_ik2[shift_i] + matrix_gk3[shift_g] * matrix_ik3[shift_i] + matrix_gk4[shift_g] * matrix_ik4[shift_i] + matrix_gk5[shift_g] * matrix_ik5[shift_i] + matrix_gk6[shift_g] * matrix_ik6[shift_i]; break; } } class="type">float mt = b1 * matrix_m[weight_shift] + (class="num">1 - b1) * g; class="type">float vt = b2 * matrix_v[weight_shift] + (class="num">1 - b2) * pow(g, class="num">2); class="type">float delta = l * (mt / (sqrt(vt) + class="num">1.0e-37f) - (l1 * sign(weight) + l2 * weight)); if(delta * g > class="num">0) matrix_w[weight_shift] = clamp(matrix_w[weight_shift] + delta, -MAX_WEIGHT, MAX_WEIGHT); matrix_m[weight_shift] = mt; matrix_v[weight_shift] = vt; } class="type">bool CNeuronNODEOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint dimension, class="type">uint variables, class="type">uint lenth, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, dimension * variables * lenth, optimization_type, batch)) class="kw">return false; iDimension = dimension; iVariables = variables; iLenth = lenth; class="type">uint mult = class="num">2; class="type">uint weights = (iDimension + class="num">2) * iDimension * iVariables; if(ArrayResize(iBuffersK, class="num">18) < class="num">18) class="kw">return false; if(ArrayResize(iInputsK, class="num">18) < class="num">18) class="kw">return false;