交易中的神经网络:对比形态变换器(基础篇)
◍ 用变换器给价格形态做时序编码
传统 CNN 类模型在抓取 K 线局部形态时很顺手,但跨周期依赖往往漏掉。变换器(Transformer)靠自注意力机制,把一段行情序列里任意两根 K 线的关系都算一遍权重,适合处理没有明显局部边界的复杂形态。 在 MT5 里跑这类模型,输入通常是固定窗口的 OHLC 标准化序列。下面这段代码把最近 30 根收盘价做了 z-score 归一,喂给后续网络前先消除量纲差异。 外汇与贵金属杠杆高、滑点随机,变换器给出的形态匹配度只代表历史相似概率,实盘信号可能漂移,务必先开 MT5 策略测试器做样本外验证。
class="type">class="kw">double zscore(class="type">class="kw">double &arr[], class="type">int n) { class="type">class="kw">double mean=class="num">0, var=class="num">0; for(class="type">int i=class="num">0;i<n;i++) mean+=arr[i]; mean/=n; for(class="type">int i=class="num">0;i<n;i++) var+=(arr[i]-mean)*(arr[i]-mean); var/=n; class="type">class="kw">double sd=MathSqrt(var); for(class="type">int i=class="num">0;i<n;i++) arr[i]=(arr[i]-mean)/sd; class="kw">return sd; }
把烛条形态当分子基序看
做机器学习行情研判时,多数人盯着单根烛条的开放区间、实体长度这些原子属性,却放过了烛条形态——它是在相近市况里反复出现的稳定结构,透露的行为倾向比孤立烛条更值得挖。外汇与贵金属市场高杠杆、跳空频繁,单根信号容易被噪音盖掉,形态层面的统计反而更抗造。 之前试过 Molformer 那套,把原子和基序拼成一条序列喂给模型,结构信息确实进去了,但异类节点间的依赖不好拆。AMCT(原子-基序对比变换器)换了个路子:原子和基序本来就是同一分子的两种视角,训练时直接用对比学习让两者互相给监督信号,不用硬拼序列,统调过程自然发生,表征健壮性上来一截。 跨分子的雷同基序往往化学性质相近,说明同构基序该有一致表征。对比损失把不同分子里相同基序的统调拉到最大,产出的基序向量更易区分。落到 K 线,就是不同品种里相似的吞没、pin bar 结构,理应映射到邻近的向量区。 要挑出决定属性的关键基序,AMCT 接了个交叉注意力模块,把分子属性嵌入和基序表征做双向捕获。权重高的基序就是判属性的要害——对应到盘面上,就是哪类形态在当前品种里真正驱动了后续波动,开 MT5 把形态向量和后续 N 根收益做相关性,就能自己验证。
「双通道编码器怎么把分子拆开看」
AMCT 把分子先拆成原子序列和基序(motif)分段两套输入,各自送进独立的编码层拿嵌入。原子通路抓的是键合这类低层依赖,基序通路补的是片段间的高层结构信息——单看原子容易漏掉跨片段规律,并行跑才够用。 结构位置靠「节点中心化」塞进嵌入:每个原子或基序算一次中心化值,直接叠到对应嵌入上,不另起网络。同一分子的原子视图和基序视图本质是同一实体两面,训练时用 KL 散度强制两者统调,形成自监督信号。 光分子内统调不够,作者又加了基序水平对比损失:跨分子找雷同基序,把它们的表征拉近,把不同类的推远,借化学同构性补 intermolecular 一致性。 解码侧用属性感知交叉注意力——属性嵌入当 query,基序表征当 key-value,权重高的基序就是该性质的关键片段。最后线性投影出预测,训练总损失 = 原子-基序统调 + 基序对比 + 属性预测三项加和。
◍ 在 OpenCL 里并行算两条通路的误差梯度
AMCT 框架的原子(单根烛条)与基序(形态)两条通路需要互相比较表征差异,并把误差梯度反向分派到两边。最省事的做法是把一条通路的输出复制进另一条的梯度缓冲,再复用已有的 calcOutputGradients;但训练时要复制两份输出缓冲、且只能串行算梯度,开销偏大。 我们改在 OpenCL 端写一个小内核,一次性算出两条通路的误差梯度,免去不必要的显存拷贝。内核收 4 个缓冲指针:两个存原子/基序通路的前馈结果,两个存对应梯度;另带两个通路的激活函数指针。关键点在于两条通路必须用同一个激活函数——激活函数定义了层输出子空间,只有落在同一子空间才能正经比差异。 内核还带一个标志位,决定梯度是累加到原有缓冲、还是直接覆盖。它跑在一维任务空间,线程号直接映射到缓冲偏移。先局部存两份前馈结果、梯度清零;数值合法时就算偏差、乘激活函数导数值,再按标志写回全局缓冲。这是本框架对 OpenCL 程序的唯一新增代码。 基序通路单独抽到 CNeuronMotifEncoder 类,以 CNeuronRMAT 为基类。Init 里先卡输入序列长度,再用动态数组挂神经层指针;形态嵌入用卷积层提取,序列长于 10 根柱线就拆 3-元素形态,否则用 2-元素。步幅取小根柱线做重叠形态,单形态嵌入维度等于描述单柱的窗口大小。 进一步叠 5 或 3 根柱的更大形态,用 CNeuronMotifs 把两级形态嵌入与原始输入级联,再喂进 R-MAT。因变换器输出维度匹配输入,此时才能安全调基类 Init 定结果缓冲。解码器内层循环手建相对自注意力层与残差卷积,不能重用父类 Init 以免清掉形态层。 交叉注意力模块新建 CNeuronRelativeCrossAttention,第二个输入源生成键/值。第二源序列长度用于 K/V 卷积层,但各元素向量维可能不同,相对编码要求同子空间,所以加可训练的 cKVProjection 投影。前馈时第二源先投影,第一源出查询;距离系数用两源矩阵乘(其一转置)得到,再经 MLP 出 B_K、B_V 偏置与全局偏置。池化后接缩放 MLP 拉回原维度并做残差连。反向的 calcInputGradients 里在输出处插多样性损失,逼嵌入子空间散得更开,再逐层往 K/V/查询/偏置回传梯度。外汇与贵金属模型训练涉及高杠杆与滑点风险,回测结论仅代表历史样本,实盘可能失效。
双流误差梯度如何回灌到输入源
相对交叉注意力对象把结构矩阵的误差梯度拆成两条路径回传:一条经转置层下沉到第一个输入源,另一条走第二个源的投影模型。第一支在转置层级别要立刻叠加上残差连接的梯度,转置层缓冲区大小刚好,旧值可直接覆盖。 第二支之前已把梯度存进投影模型最后一层缓冲区,现在补入键、值实体的梯度:先替换接收对象的梯度缓冲区指针,再按顺序调各实体的梯度分派,把中间结果累加进去,随后反向遍历各层向下传播。 关键优化在于,把模型第一层梯度缓冲区指针替换成外部传入的缓冲区后,省掉了一次多余的数据复制——梯度落到第一层时自动写进外部系统空间。这一改动在 10 万次反向传播调用中约可减少一次完整矩阵拷贝的内存占用。 附带的 OpenCL 核函数 CalcAlignmentGradient 展示了双输出梯度的就地累加逻辑:当 add>0 时梯度叠加进原缓冲,否则覆盖。CNeuronMotifEncoder 继承 CNeuronRMAT 并覆写 Init,用于构建这类双流编码器。完整类源码见附件,下一篇再续。
__kernel <span class="keyword">class="type">void</span> CalcAlignmentGradient(__global <span class="keyword">const</span> <span class="keyword">class="type">float</span> *matrix_o1, __global <span class="keyword">const</span> <span class="keyword">class="type">float</span> *matrix_o2, __global <span class="keyword">class="type">float</span> *matrix_g1, __global <span class="keyword">class="type">float</span> *matrix_g2, <span class="keyword">const</span> <span class="keyword">class="type">int</span> activation, <span class="keyword">const</span> <span class="keyword">class="type">int</span> add) { <span class="keyword">class="type">int</span> i = get_global_id(<span class="number">class="num">0</span>); <span class="keyword">const</span> <span class="keyword">class="type">float</span> out1 = matrix_o1[i]; <span class="keyword">const</span> <span class="keyword">class="type">float</span> out2 = matrix_o2[i]; <span class="keyword">class="type">float</span> grad1 = <span class="number">class="num">0</span>; <span class="keyword">class="type">float</span> grad2 = <span class="number">class="num">0</span>; <span class="keyword">if</span>(!isnan(out1) && !isinf(out1) && !isnan(out2) && !isinf(out2)) { grad1 = Deactivation(out2 - out1, out1, activation); grad2 = Deactivation(out1 - out2, out2, activation); } <span class="keyword">if</span>(add > <span class="number">class="num">0</span>) { matrix_g1[i] += grad1; matrix_g2[i] += grad2; } <span class="keyword">else</span> { matrix_g1[i] = grad1; matrix_g2[i] = grad2; } } <span class="keyword">class</span> CNeuronMotifEncoder : <span class="keyword">class="kw">public</span> CNeuronRMAT { <span class="keyword">class="kw">public</span>: CNeuronMotifEncoder(<span class="keyword">class="type">void</span>) {}; ~CNeuronMotifEncoder(<span class="keyword">class="type">void</span>) {}; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Init(<span class="keyword">class="type">uint</span> numOutputs, <span class="keyword">class="type">uint</span> myIndex, COpenCLMy *open_cl, <span class="keyword">class="type">uint</span> window, <span class="keyword">class="type">uint</span> window_key, <span class="keyword">class="type">uint</span> units_count, <span class="keyword">class="type">uint</span> heads, <span class="keyword">class="type">uint</span> layers, ENUM_OPTIMIZATION optimization_type, <span class="keyword">class="type">uint</span> batch) <span class="keyword">class="kw">override</span>; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span> Type(<span class="keyword">class="type">void</span>) <span class="keyword">class="kw">override</span> <span class="keyword">const</span> { <span class="keyword">class="kw">return</span> defNeuronMotifEncoder; } };