交易中的神经网络:时间序列的分段线性表示·进阶篇
(2/3)· 面对满屏 tick 数据算到卡顿?这篇进阶篇讲清双向分段线性表示怎么压缩时间序列
接上篇,我们继续深挖时间序列的紧凑表达。当你把整段 EURUSD 的逐笔报价直接喂给异常检测模型,内存和时延往往先崩,信号反而被淹。本篇聚焦 BPLR 算法,看它如何把长序列切成几段直线,留住结构、丢掉噪声。
◍ 转折点之间的线性斜率怎么算
在 GPU 内核里标记出当前柱为转折点(bttp=true)之后,先要回数前面已经出现过几个转折点,并定位上一个转折点的位置 prev_ttp 与索引 prev_in。代码用 pos 从 0 累加,每碰到一个 isttp 为真的单元就 pos++,这段循环在 transpose 开关下用不同的内存排布取 current_in,但逻辑一致。 只有当 pos 落在 1 到 lenth/3 之间(不含边界)时,才对 prev_ttp 到当前 i 这段距离做最小二乘线性回归。dist = i - prev_ttp,遍历 p 从 0 到 dist 累加 x、y、xy、xx,斜率 slope = (dist*sum_xy - sum_x*sum_y) / (dist*sum_xx - sum_x*sum_x),截距 intercept 同步算出,并把归一化长度 dist/lenth 写进 outputs 第三槽。 若 pos 恰好等于 lenth/3,则改用 dist = lenth - prev_ttp 跑同样的求和,覆盖从最后一个转折点到序列末尾的全段拟合。外汇与贵金属行情跳变频繁,这种局部斜率仅反映历史片段倾向,实盘需警惕过拟合与滑点风险。
bttp = true; class=class="str">"cmt">//--- isttp[shift_in] = (class="type">int)bttp; outputs[shift_in] = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); class=class="str">"cmt">//--- calc position class="type">int pos = -class="num">1; class="type">int prev_in = class="num">0; class="type">int prev_ttp = class="num">0; if(bttp) { pos = class="num">0; for(class="type">int p = class="num">0; p < i; p++) { class="type">int current_in = ((class="type">bool)transpose ? (p * variables + v) : (v * lenth + p)); if((class="type">bool)isttp[current_in]) { pos++; prev_ttp = p; prev_in = current_in; } } } class=class="str">"cmt">//--- cacl tendency if(pos > class="num">0 && pos < (lenth / class="num">3)) { class="type">class="kw">float sum_x = class="num">0; class="type">class="kw">float sum_y = class="num">0; class="type">class="kw">float sum_xy = class="num">0; class="type">class="kw">float sum_xx = class="num">0; class="type">int dist = i - prev_ttp; for(class="type">int p = class="num">0; p < dist; p++) { class="type">class="kw">float x = (class="type">class="kw">float)(p); class="type">class="kw">float y = inputs[prev_in + p * step_in]; sum_x += x; sum_y += y; sum_xy += x * y; sum_xx += x * x; } class="type">class="kw">float slope = (dist * sum_xy - sum_x * sum_y) / (dist > class="num">1 ? (dist * sum_xx - sum_x * sum_x) : class="num">1); class="type">class="kw">float intercept = (sum_y - slope * sum_x) / dist; class="type">int shift_out = ((class="type">bool)transpose ? ((pos - class="num">1) * class="num">3 * variables + v) : (v * lenth + (pos - class="num">1) * class="num">3)); outputs[shift_out] = slope; outputs[shift_out + class="num">1 * step_in] = intercept; outputs[shift_out + class="num">2 * step_in] = ((class="type">class="kw">float)dist) / lenth; } else { if(pos == (lenth / class="num">3)) { class="type">class="kw">float sum_x = class="num">0; class="type">class="kw">float sum_y = class="num">0; class="type">class="kw">float sum_xy = class="num">0; class="type">class="kw">float sum_xx = class="num">0; class="type">int dist = lenth - prev_ttp; for(class="type">int p = class="num">0; p < dist; p++) { class="type">class="kw">float x = (class="type">class="kw">float)(p); class="type">class="kw">float y = inputs[prev_in + p * step_in]; sum_x += x; sum_y += y; sum_xy += x * y; sum_xx += x * x;
分段线性回归的反向梯度怎么算
上面这段 OpenCL 内核是分段线性回归(PLR)层在训练时用的反向传播梯度函数,核心任务是把输出端的斜率、截距梯度,按当前样本落在哪一段回推到输入梯度。 先通过 do-while 定位:用 outputs 里存的第 3 个值(段长占比 × 序列长)累加 prev_in,直到覆盖当前全局下标 i 的那一段 pos 被找到;dist 至少取 1,避免除零。 接着只对该段长度 dist 预累加 sum_x 与 sum_xx(x 从 0 到 dist-1),不再遍历全序列,所以单点梯度计算量是 O(dist) 而非 O(lenth),在段数多时明显省算力。 梯度合并处用 grad_slope -= sum_x/dist*grad_intercept 先消去截距耦合,再除以 fmax(dist*sum_xx - sum_x*sum_x, 1) 得到纯斜率贡献;最终 grad 由截距项 grad_intercept/dist 加位置项 (dist*(i-prev_in)-sum_x)*grad_slope 合成,isnan/isinf 直接归零防止核崩溃。 在 MT5 里接自己的 OCL 网络时,若某段 dist 过短(如 1),分母保护会令 grad_slope 趋零,这时该段几乎只传截距梯度——调小分段阈值可能让更多短段出现,反向信号会变平滑但也可能丢细节。外汇与贵金属价格序列用这类层做特征提取时,高风险在于过拟合历史波动结构,实盘前务必用不同品种周期验证。
__kernel class="type">void PLRGradient(__global class="type">class="kw">float *inputs_gr, __global const class="type">class="kw">float *outputs, __global const class="type">class="kw">float *outputs_gr, const class="type">int transpose ) { const class="type">size_t i = get_global_id(class="num">0); const class="type">size_t lenth = get_global_size(class="num">0); const class="type">size_t v = get_global_id(class="num">1); const class="type">size_t variables = get_global_size(class="num">1); class=class="str">"cmt">//--- constants const class="type">int shift_in = ((class="type">bool)transpose ? (i * variables + v) : (v * lenth + i)); const class="type">int step_in = ((class="type">bool)transpose ? variables : class="num">1); const class="type">int shift_out = ((class="type">bool)transpose ? v : (v * lenth)); const class="type">int step_out = class="num">3 * step_in; class=class="str">"cmt">//--- calc position class="type">int pos = -class="num">1; class="type">int prev_in = class="num">0; class="type">int dist = class="num">0; do { pos++; prev_in += dist; dist = (class="type">int)fmax(outputs[shift_out + pos * step_out + class="num">2 * step_in] * lenth, class="num">1); } class="kw">while(!(prev_in <= i && (prev_in + dist) > i)); class=class="str">"cmt">//--- calc constants class="type">class="kw">float sum_x = class="num">0; class="type">class="kw">float sum_xx = class="num">0; for(class="type">int p = class="num">0; p < dist; p++) { class="type">class="kw">float x = (class="type">class="kw">float)(p); sum_x += x; sum_xx += x * x; } class=class="str">"cmt">//--- get output gradient class="type">class="kw">float grad_slope = outputs_gr[shift_out + pos * step_out]; class="type">class="kw">float grad_intercept = outputs_gr[shift_out + pos * step_out + step_in]; class=class="str">"cmt">//--- calc gradient grad_slope -= sum_x / dist * grad_intercept; grad_slope /= fmax(dist * sum_xx - sum_x * sum_x, class="num">1); class="type">class="kw">float grad = grad_intercept / dist; grad += (dist * (i - prev_in) - sum_x) * grad_slope; if(isnan(grad) || isinf(grad)) grad = class="num">0; class=class="str">"cmt">//--- save result inputs_gr[shift_in] = grad; }
「PLR 神经元的 OpenCL 初始化与前向传播骨架」
CNeuronPLROCL 是 MT5 中基于 OpenCL 的多项式线性回归神经元封装,类内先声明了 icIsTTP、iVariables、iCount 等整型成员,分别用于标记转置缓冲、输入窗口长度与单元数量,并预留了 feedForward、calcInputGradients 等虚函数接口,其中 updateInputWeights 直接返回 true,说明该层权重不在反向阶段更新。 构造函数仅把 bTranspose 置为 false,真正的资源申请在 Init 里完成:它先调用基类 CNeuronBaseOCL::Init,传入 window_in * units_count 作为展开后的输入维度,再把 iVariables 和 iCount 分别赋值为窗口与单元数,最后通过 OpenCL.AddBuffer 申请一块 sizeof(int) * Neurons() 大小的可读写缓冲,失败则返回 false。 feedForward 函数负责把上一层输出搬进 PLR 核。它先校验 OpenCL 指针与上游神经元输出缓冲非空,然后定义二维工作组:global_work_size 为 {iCount, iVariables},local_work_size 为 {iCount, 1},这意味着每个输入变量沿单元维度并行。接着用 SetArgumentBuffer 把上游输出索引与自身输出索引绑到 def_k_PLR 核的参数槽,任一绑定失败就打印错误码并退出。 在 MT5 终端里打开含此类的 EA 源码,把 window_in 从默认 12 改成 24 后重编译,能直接观察到 AddBuffer 申请的内存放翻约一倍,显存占用随窗口线性增长,外汇与贵金属模型训练属高风险,参数放大可能触发 CL_MEM 分配失败。
class="type">int icIsTTP; class="type">int iVariables; class="type">int iCount; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) { class="kw">return true; } class="kw">public: CNeuronPLROCL(class="type">void) : bTranspose(class="kw">false) {}; ~CNeuronPLROCL(class="type">void) {}; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint units_count, class="type">bool transpose, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronPLROCL; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronPLROCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint units_count, class="type">bool transpose, ENUM_OPTIMIZATION optimization_type, class="type">uint batch ) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_in * units_count, optimization_type, batch)) class="kw">return class="kw">false; iVariables = (class="type">int)window_in; iCount = (class="type">int)units_count; bTranspose = transpose; icIsTTP = OpenCL.AddBuffer(class="kw">sizeof(class="type">int) * Neurons(), CL_MEM_READ_WRITE); if(icIsTTP < class="num">0) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronPLROCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!OpenCL || !NeuronOCL || !NeuronOCL.getOutput()) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="type">uint global_work_offset[class="num">2] = {class="num">0}; class="type">uint global_work_size[class="num">2] = {iCount, iVariables}; class="type">uint local_work_size[class="num">2] = {iCount, class="num">1}; ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plr_inputs, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plr_outputs, getOutputIndex())) {
◍ 往 OpenCL 内核塞参数与搭编码器骨架
在 MT5 里跑 GPU 加速的行情模型,先得把内核参数一个个绑好。下面这段把缓冲区和标量往 def_k_PLR 内核灌:icIsTTP 缓冲、是否转置的 int、以及写死的步长 0.3(float)。任何一步 SetArgument/SetArgumentBuffer 失败就 printf 出函数名、错误码和行号并 return false,方便你直接在 Experts 日志里定位是哪一行 OpenCL 调用炸了。 参数绑完之后用 OpenCL.Execute(def_k_PLR, 2, global_work_offset, global_work_size, local_work_size) 真正派发计算,维度数传 2。若执行返回 false,同样打印错误后退出——外汇与贵金属杠杆品种上跑这套,显存或驱动异常都可能让 Execute 失败,属于高风险调试环节。 编码器网络结构用 CArrayObj 挂 CLayerDescription 来拼。输入层节点数 = HistoryBars * BarDescr,类型 defNeuronBaseOCL、激活 None、优化 ADAM;随后接一层 defNeuronBatchNormOCL,batch 设 1e4,节点数沿用上层;再接 defNeuronTransposeOCL,count=HistoryBars、window=BarDescr 做维度翻转。每层 new 出来加不进数组就 delete 并返回 false,避免内存泄漏。 开 MT5 把这段贴进你的 EA 源码,改 HistoryBars 和 BarDescr 两个宏,看日志里 0.3 步长下 PLR 内核是否一次过,就能验证你本地 OpenCL 环境通不通。
if(!OpenCL.SetArgumentBuffer(def_k_PLR, def_k_plt_isttp, icIsTTP)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgument(def_k_PLR, def_k_plr_transpose, (class="type">int)bTranspose)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } if(!OpenCL.SetArgument(def_k_PLR, def_k_plr_step, (class="type">class="kw">float)class="num">0.3)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); class="kw">return class="kw">false; } class=class="str">"cmt">//--- if(!OpenCL.Execute(def_k_PLR, class="num">2, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } 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 class="kw">false; } class=class="str">"cmt">//--- encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">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 class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronTransposeOCL; descr.count = HistoryBars; descr.window = BarDescr; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false;
编码器后段卷积与反归一化的堆叠
上面这段是编码器从第 4 层到第 9 层的实际装配逻辑,每层都用 new CLayerDescription() 建描述符,再交给 encoder.Add() 入栈,任何一步失败就 delete 并回 false。 第 4 层用 defNeuronConvOCL,window 取 HistoryBars、step 也等于 HistoryBars,window_out 压到 LatentCount,激活走 LReLU;第 5 层同样卷积但 window 与 step 均为 LatentCount,输出维度不变,激活换 SIGMOID。 第 6 层继续卷积,window_out 直接指向 NForecast,激活用 TANH 把输出限到 [-1,1],这一步基本决定了预测值的物理边界。第 7 层转置 defNeuronTransposeOCL 做形状还原,无激活。 第 8 层 defNeuronRevInDenormOCL 负责反归一化,count 写成 BarDescr*NForecast、layers=1,把网络内部尺度拉回价格量纲;第 9 层 defNeuronFreDFOCL 做频域处理,step 为 int(true),probability 设 0.7f,意味着约七成概率走该分支。 外汇与贵金属市场高杠杆、高波动,这类网络结构只是特征抽取框架,实盘信号倾向需结合样本外验证,复制代码到 MT5 跑一遍层数与维度打印最能看清张量怎么流动。
descr.type = defNeuronPLROCL; descr.count = HistoryBars; descr.window = BarDescr; descr.step = class="type">int(class="kw">false); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = HistoryBars; descr.step = HistoryBars; descr.window_out = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = LatentCount; descr.step = LatentCount; descr.window_out = LatentCount; descr.optimization = ADAM; descr.activation = SIGMOID; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = LatentCount; descr.step = LatentCount; descr.window_out = NForecast; descr.optimization = ADAM; descr.activation = TANH; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = NForecast; descr.activation = None; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr*NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers=class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; }