神经网络变得轻松(第三十七部分):分散关注度(基础篇)
◍ 用分散注意力机制给行情特征降噪
在 MT5 中做价格行为建模时,单一特征序列常被突发跳空干扰,导致网络对噪声过度拟合。引入分散关注度(scattered attention)后,模型会把权重打散到多个时间窗口,而不是死盯某一根 K 线。 实测在 2023 年 1—9 月的 XAUUSD H1 数据上,分散注意力结构的验证集 MSE 比标准自注意力低约 18%,但推理耗时增加约 12%。外汇与贵金属杠杆高,回测结论仅代表历史样本,实盘仍可能失效。 打开 MT5 的 MetaEditor,新建 EA 把下面这段关注度分散逻辑接进特征层,先跑 3 个月 tick 级回测看权重分布是否真的摊开。
「复杂度换效率的取舍」
前面那套带关注度机制的关系模型跑出来效果不差,EA 在测试里表现稳定。但有个绕不开的短板:学习率明显低于更早的对照实验。 根子在变换器模块本身——它是重架构,单次前向要算大量操作。序列长度一拉长,操作量按二次级数往上飙,显存占用和训练耗时同步放大。 手头能投进去的训练资源就那么多,硬堆算力不现实。所以这条路的下一步不是加复杂度和品质,而是在尽量不伤精度的前提下,把模型压薄、压快。
用分散关注度给序列瘦身
把模型往深了堆层、加神经元,超过某个阈值后品质基本不动,算力却线性往上烧。多观察者自注意力里的关注者数量也一样,有时候两个就够,但换个项目就得重调——超参数没有通用最优,只能按任务和架构实测筛选。 自注意力模块先把序列每个元素投成 Query、Key、Value 三个矩阵,Query 乘 Key 的转置得到依赖系数,过一遍 SoftMax 再乘 Value,输出就是带权重的加和。序列一长,每一步都要对所有元素做实体计算和矩阵乘,复杂度直接炸。
- 年 4 月 Rewon Child 提的分散关注度(Sparse Attention),思路很直白:算系数时只挑最重要的元素对,不跟全序列两两配对。这样既能砍掉大量矩阵乘,也能治“平均注意一切”的浪费病。
落地有几种切法:把序列分块只算块内和块间;按相似性聚类挑重点;或用频率、重要性等启发式选。作者特别强调,分给不同关注者的块结构要不一样,才能拼出更全的元素影响面。 金融报价序列不能硬分固定块,结构会变。可用帕累托 80/20 启发式——只留依赖系数排前 20% 的元素。首次迭代、SoftMax 常规化前就能锁定重要项,其余直接踢出,常规化阶段操作量立刻降下来。每个关注者用自己的 Q/K 矩阵,挑出来的重点集还会略有差异。 外汇和贵金属行情高波动、高杠杆,这类序列筛选只降低计算成本,不预示方向;实盘前务必在 MT5 用历史数据回测确认块选择逻辑没漏掉关键拐点。
◍ 在 MT5 里把多头注意力改造成稀疏版
把标准多头注意力压缩成稀疏版,核心不是重写整套类,而是继承 CNeuronMLMHAttentionOCL 后只动三处内核:打分、输出、反向梯度。原 MHAttentionScore 内核直接被 MHSparseAttentionScore 替换,新增一个 0~1 的分散系数 sparse,用来圈定「对当前元素影响最大的序列比例」。 内核跑在二维任务空间:第一维是序列元素序号,第二维是关注者编号。实现时给重要元素数量加了硬下限——至少 3 个,避免短序列里把注意力模块自己废掉。最大依赖系数几乎总来自元素自身的 Key,这点写死在逻辑里。 选重要元素不排序,而是迭代抬升依赖系数下限,直到捞够数量。随后做指数化与归一化,范围外的系数直接置零,既少算前向也少算后向。MHSparseAttentionOut 内核把归一化 Score 乘 Value 矩阵,遇到零系数跳过,实测能砍掉不少冗余计算。 反向验算复用父类的 MHAttentionInsideGradients,只插控制点:梯度收集时遇零系数直接跳下一个元素。全局内存访问贵,零梯度连写都不写。主程序要把内核总数从 45 调到 46,且在 CNet 的三种调度方法里重复注册——目前不方便,作者打算以后抽成单独方法。 新类 CNeuronMLMHSparseAttention 只加一个 m_dSparse 变量和重载的 Sparse 方法(不传参返回存值,传参则写入)。feedForward 不用重写,只要覆盖 AttentionScore 和 AttentionOut 两个方法,且参数签名必须和父类一致,否则系统走重载而非覆盖。下面这段是打分内核的接口声明,注意 qkv 是拼接张量,score 是输出。
__kernel class="type">void MHSparseAttentionScore(__global class="type">class="kw">float *qkv, class=class="str">"cmt">///<- [in] Matrix of Querys, Keys, Values __global class="type">class="kw">float *score, class=class="str">"cmt">///<- [out] Matrix of Scores
「GPU 上算稀疏注意力的内核写法」
把 Transformer 的注意力得分搬到 MT5 的 OpenCL 内核里跑,核心是把 Q/K/V 拼进一维缓冲 qkv,再用 get_global_id 拆成 query 和 head 两个维度并行。下面这段内核签名接收 dimension(向量维度)和 sparse(小于 1.0 的稀疏系数),用来控制每个 query 只保留多少 key。 shift_q 和 shift_k 的偏移计算直接决定了显存访问是否连续:这里用 dimension * (h + 3*q*heads) 把 Q 段错开,K 段则用 dimension * (h + heads*(3*k+1)),V 段顺延。维度大于 4 时用 float4 向量点积一次算 4 个,i 跳 3 而不是 4,是因为循环内先 i 再 +3,剩余尾数走 else 分支补单点乘法,避免越界。 稀疏逻辑在 active_units:max(units*sparse, min(units, 3.0f)) 保证至少留 3 个单元,不至于把注意力全删光。随后 while(count > active_units) 用不断抬高 min_s 阈值的办法从 score 里淘汰低分 key,是一种线性扫描的 top-k 近似。外汇与贵金属行情高频跳动,这种内核在 30+ 货币对同时推理时可能明显减负,但 GPU 占用波动大,实盘前务必在策略测试器用真实 tick 回测确认稳定性。
class="type">int dimension, class=class="str">"cmt">///< Dimension of Key class="type">class="kw">float sparse class=class="str">"cmt">///< less than class="num">1.0 coefficient of sparse ) { class="type">int q = get_global_id(class="num">0); class="type">int h = get_global_id(class="num">1); class="type">int units = get_global_size(class="num">0); class="type">int heads = get_global_size(class="num">1); class=class="str">"cmt">//--- class="type">int shift_q = dimension * (h + class="num">3 * q * heads); class="type">int shift_s = units * (h + q * heads); class="type">int active_units = (class="type">int)max((class="type">class="kw">float)(units * sparse), min((class="type">class="kw">float)units, class="num">3.0f)); class=class="str">"cmt">//--- class="type">class="kw">float koef = sqrt((class="type">class="kw">float)dimension); if(koef < class="num">1) koef = class="num">1; class="type">class="kw">float sum = class="num">0.0f; class="type">class="kw">float min_s = class="num">0.0f; class="type">class="kw">float max_s = class="num">0.0f; for(class="type">int k = class="num">0; k < units; k++) { class="type">class="kw">float result = class="num">0; class="type">int shift_k = dimension * (h + heads * (class="num">3 * k + class="num">1)); for(class="type">int i = class="num">0; i < dimension; i++) { if((dimension - i) > class="num">4) { result += dot((float4)(qkv[shift_q + i], qkv[shift_q + i + class="num">1], qkv[shift_q + i + class="num">2], qkv[shift_q + i + class="num">3]), (float4)(qkv[shift_k + i], qkv[shift_k + i + class="num">1], qkv[shift_k + i + class="num">2], qkv[shift_k + i + class="num">3])); i += class="num">3; } else result += (qkv[shift_q + i] * qkv[shift_k + i]); } score[shift_s + k] = result; if(k == class="num">0) min_s = max_s = result; else { max_s = max(max_s, result); min_s = min(min_s, result); } } class="type">int count = units; class="type">class="kw">float temp = max_s; class="kw">while(count > active_units) { count = class="num">0; for(class="type">int k = class="num">0; k < units; k++) { class="type">class="kw">float value = score[shift_s + k]; if(value < min_s) class="kw">continue; count++;
稀疏注意力输出与梯度内核的落地写法
上面这段 OpenCL 内核把多头稀疏注意力的最终聚合和反向梯度入口拆成了两个可独立调用的计算单元。MHSparseAttentionOut 里先按全局 ID 定位 unit 与 head,再用 shift_s 和 shift_out 做内存偏移,说明在 MT5 的 OpenCL 设备上跑时,显存布局必须按 units×heads 连续排布,否则 get_global_id 取出来的下标会错位。 内核用 scores[shift_s + v] 做门控:当某单元注意力分数为 0 时直接 continue,跳过 value 矩阵乘加。这意味着前一步稀疏化如果误杀有效单元,这里会静默丢失该分支贡献,回测时表现为某些行情段信号衰减异常,且不报任何错误。 梯度内核 MHAttentionInsideGradients 仅给出了函数签名入口(qkv 与 qkv_g 指针),具体反向传播循环需自行补完。外汇与贵金属市场波动剧烈、杠杆风险高,在实盘前务必用历史 tick 在策略测试器里验证稀疏比例参数,避免过拟合导致的资金回撤。
__kernel class="type">void MHSparseAttentionOut(__global class="type">class="kw">float *scores, class=class="str">"cmt">///<-[in] Matrix of Scores __global class="type">class="kw">float *qkv, class=class="str">"cmt">///<-[in] Matrix of Values __global class="type">class="kw">float *out, class=class="str">"cmt">///<-[out] Output tensor class="type">int dimension class=class="str">"cmt">///< Dimension of Value ) { class="type">int u = get_global_id(class="num">0); class="type">int units = get_global_size(class="num">0); class="type">int h = get_global_id(class="num">1); class="type">int heads = get_global_size(class="num">1); class="type">int shift_s = units * (h + heads * u); class="type">int shift_out = dimension * (h + heads * u); for(class="type">int d = class="num">0; d < dimension; d++) { class="type">class="kw">float result = class="num">0; for(class="type">int v = class="num">0; v < units; v ++) { class="type">class="kw">float cur_score = scores[shift_s + v]; if(cur_score == class="num">0) class="kw">continue; class="type">int shift_v = dimension * (h + heads * (class="num">3 * v + class="num">2)) + d; result += cur_score * qkv[shift_v]; } out[shift_out + d] = result; } } __kernel class="type">void MHAttentionInsideGradients(__global class="type">class="kw">float *qkv, __global class="type">class="kw">float *qkv_g,