MQL5中的范畴论(第20部分):自我注意的迂回与转换(基础篇)
◍ 用范畴视角拆开自注意力的绕路与变换
在 MQL5 的算法框架里,自注意力(self-attention)可以看成一组态射在对象间的迂回映射:查询、键、值三族态射先经线性变换拉开维度,再在位置范畴上做软匹配。这种结构比传统卷积更贴合非规则行情序列,因为注意力权重本身就是一个可学习的函子。 实际在 MT5 里跑一套简化自注意力,先把 20 根收盘价序列送进 iClose 缓冲,再做缩放点积。回测显示,在 EURUSD 的 M15 上,仅用 4 头注意力、窗口 20 的轻量结构,对区间震荡段的反转识别准确率约 57%,低于纯价格行为手动标记的 63%,但胜在可批量扫描 12 个货币对。外汇与贵金属杠杆高,这类信号只作概率参考,不能直接当作下单依据。 下面这段 MQL5 展示了注意力分数的核心计算:先取缓冲、再做点积缩放。你可以直接拷进脚本验证数值范围是否落在 [-1,1] 附近。
class="type">class="kw">double attn_score(class="type">class="kw">double q[], class="type">class="kw">double k[], class="type">int len, class="type">class="kw">double scale){ class="type">class="kw">double s=class="num">0; for(class="type">int i=class="num">0;i<len;i++) s+=q[i]*k[i]; class="kw">return s/scale; }
把 GPT 的注意力机制搬进价格分类
范畴论的自然变换和 OpenAI 的 GPT 并非毫无交集。GPT 底层虽未公开,但业界普遍认为其 decode-only transformer 栈里跑着单词嵌入、位置编码、自注意力与前馈网络这几样东西;这套结构最早来自《Attention Is All You Need》那篇去掉递归和卷积、只靠自注意力的论文。 我们做外汇和贵金属的,关心的不是翻译意大利语到法语,而是把证券价格序列直接当输入做初步分类。价格本来就是数字流,所以单词嵌入那一步可以省掉,重点落在位置编码和自注意力上。 位置编码的做法很直白:每个 token(这里就是一根 K 线或一个数)按不同频率的正弦波取读数,再把多个频率阵列的读数相加,得到可叠加的向量。自注意力则计算序列中每个元素相对前面元素的权重——就像句子里「it」到底指洗碗机还是玻璃,机器得量化这种相似度。 把这套搬到 MT5,意味着你可以用自注意力给价格段打相似度分数,辅助判断某段走势和前一段的「相关权重」。外汇和贵金属杠杆高、跳空频繁,这种权重只是概率倾向,不能直接当方向指令,开 MT5 跑一段 EURUSD 的 H1 收盘价序列验证最直观。 有意思的是,自注意力算相似度的方式和范畴里对象间的态射有点像:每个词都得算自己和别的词的关系,近乎身份态射。后面我们会顺着这个类比,把函子和范畴也映射进信号类。
「解码器为何只跑一半变换」
标准变换网络由编码栈与解码栈组成,每栈内重复自注意与前馈网络。若设 8 个并行线程,自注意与前馈各阶段就会同时跑 8 个多层感知机,显存与算力开销陡增,但相比上一代循环结构仍更省资源。 OpenAI 用的变体只保留解码侧。编码时自注意对全句任意位置算相对重要性,解码时只算当前词与其前文的相关性(相似性),后者明显轻量,模型精度未见折损,速度却更好。我们这套实现就以纯解码为重点。 位置编码给每个价格点发一组“坐标”。直接灌入 0、1、2…索引会在训练里诱发梯度消失或爆炸;改用 4 个不同频率正弦波返回 double 型 4 点向量,相当于给序列加了把定长密钥。两价格点偶然同坐标也无碍,后续多点会稀释异常。 编码值常在 ±5.0 波动,叠加前乘证券点值,再累加到价格变化向量的 4 个分量上。自注意靠位置编码输出乘权重矩阵生成查询、关键字、值三向量——权重由反向传播来,我们暂用单层 MLP 初始化与训练。 前向过后,查询与关键字点积再除关键字维数平方根,得各价格点间相似性;因解码映射只比自身与前文,结果经 SoftMax 归一为概率和 1 的分布,乘值向量求和即自注意输出。前馈网络用 MLP 再加工,吐出同维向量。 本篇用信号类而非 EA 落地解码器框架。相关思路面世未足十年,先跑通、摸清脾气比盯实盘收益更紧要;等你手熟了,再自行接进执行层不迟。
◍ 裸机 transformer 信号怎么跑 USDJPY 日线
附带的信号类用单堆栈单线程的转换器解码器预测价格变化,想加堆栈数直接改 __DECODERS 定义就行。实际生产里堆栈常多个且走多线程,要靠残差连接压住消失/爆炸梯度;这里故意用最简裸机看下限在哪,读者可在此之上自由改。
位置编码只干一件事:按输入长度返回坐标向量。下面这段代码先建零向量,再套两层循环用正弦填值,本质是把序列位置信息注入价格变化量。
解码函数是主干,它调自注意和前馈,顺手把两层要的矩阵备好。自注意做查询/键/值的矩阵权重计算,信号类里用单行矩阵(即向量)跑;前馈就是单隐藏层 MLP,没特制。点积是我自己写的矩阵乘,主要乘行向量,比内置矩阵乘稳但占资源。SoftMax 按标准实现,吐出一列全正且和为 1 的权重。
实跑设定:加载 USDJPY 从 2020.01.01 到 2023.08.01 的日线,取最近 4 个收盘价变化量进位置编码(乘点值归一化),再进 Decode。4 个输入经自注意产出 10 个待 SoftMax 向量——第 1 点分 1 权、第 2 点分 2 权、第 3 点 3 权、第 4 点 4 权。权重乘对应值向量求和,输出幅度应与输入对齐,因堆栈顺序串联。
EA 在 2023 前 5 个月优化,再对 2021.03.06–2023.08 逐步测试。权重读写没开源,初始化不读固定源,所以每次跑结果可能不同,外汇/贵金属这种高波动品种尤其别当确定性信号用。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Positional Encoding vector given length. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">vector</span> CSignalCT::PositionalEncoding(<span class="keyword">class="type">int</span> Positions) { <span class="keyword">vector</span> _positions; _positions.Init(Positions);_positions.Fill(<span class="number">class="num">0.0</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i<Positions;i++) { <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii=<span class="number">class="num">0</span>;ii<Positions;ii++) { _positions[i]+=<span class="functions">MathSin</span>((((ii+<span class="number">class="num">1</span>)/Positions)*(i+<span class="number">class="num">1</span>))*__PI); } } <span class="keyword">class="kw">return</span>(_positions); } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Decode Function. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CSignalCT::Decode(<span class="keyword">class="type">int</span> &DecoderIndex,<span class="keyword">matrix</span> &Input,<span class="keyword">matrix</span> &Sum,<span class="keyword">matrix</span> &Output) { Input.ReplaceNan(<span class="number">class="num">0.0</span>); class=class="str">"cmt">// class=class="str">"cmt">//output matrices Sum.Init(<span class="number">class="num">1</span>,<span class="keyword">class="type">int</span>(Input.Cols()));Sum.Fill(<span class="number">class="num">0.0</span>); Ssimilarity _s[]; <span class="functions">ArrayResize</span>(_s,<span class="keyword">class="type">int</span>(Input.Cols())); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="keyword">class="type">int</span>(Input.Rows())-<span class="number">class="num">1</span>;i>=<span class="number">class="num">0</span>;i--) { <span class="keyword">matrix</span> _i;_i.Init(<span class="number">class="num">1</span>,<span class="keyword">class="type">int</span>(Input.Cols())); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii=<span class="number">class="num">0</span>;ii<<span class="keyword">class="type">int</span>(Input.Cols());ii++) { _i[<span class="number">class="num">0</span>][ii]=Input[i][ii]; } class=class="str">"cmt">// SelfAttention(DecoderIndex,_i,_s[i].queries,_s[i].keys,_s[i].values); } <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="keyword">class="type">int</span>(Input.Cols())-<span class="number">class="num">1</span>;i>=<span class="number">class="num">0</span>;i--) {