交易中的神经网络:通过Adam-mini优化减少内存消耗(基础篇)
「用 Adam-mini 给 MT5 神经网络减负」
在 MT5 里跑神经网络策略,最容易被卡住的不是逻辑,而是显存和内存占用。标准 Adam 优化器要为每一个参数维护一阶、二阶动量,参数量一大,内存直接翻倍。 Adam-mini 的思路是把参数按结构分组,只在每组内共享二阶动量估计,省掉大量冗余状态。实测同样的全连接结构,Adam-mini 内存占用约为标准 Adam 的 45%–60%,训练步长几乎不变。 对做贵金属剥头皮的玩家,这意味着你能在本机用更大的 batch 做盘中再训练,而不必等云算力。外汇高杠杆下模型过拟合风险本就高,小内存方案只是让你更快验证想法,不等于信号更准。
Adam 的内存账本与 Adam-mini 的减法
做神经网络训练的人大多从 Adam 起步,它靠一阶、二阶矩估计给每个参数单独调学习率,收敛顺手。代价藏在显存里:矩估计的缓冲区体积和模型本身几乎等量,规模一大就得把活甩给 CPU,延迟和训练拖慢直接可见。 2024年7月的论文《Adam-mini: Use Fewer Learning Rates To Gain More》给了个务实的切口:把参数分块、每块只配一个学习率。实测内存占用比标准 Adam 砍掉45–50%,而收敛表现持平甚至略好。 对外汇贵金属相关的行情建模来说,这种轻量优化器意味着本地用小显存机器跑高频特征网络的概率更高,但模型信号仅作辅助,杠杆品种高风险不变。
◍ 块级学习率比逐参数更划算
Adam 里每个参数都带一个由二阶矩 v 派生的独立学习率。作者拆解后发现,Transformer 的 Hessian 矩阵近似块对角,且不同 Transformer 块的特征值分布差异明显,所以块与块之间确实需要不同学习率,但块内每个参数都单独配学习率属于过度供给。 他们在通用优化任务上做了对照:单一全局最优学习率打不过 Adam,这不出所料;但在一个密集 Hessian 子块内部,单一最优学习率反而能超过 Adam。结论很直接——把学习率放到块级,就能在省内存的同时不掉性能。 对一个 4 层 Transformer 的实验显示,用比 Adam 少得多的学习率数量,模型能达到相当甚至更优的表现。这就引出实问题:怎么不靠穷举网格搜索就把每块的学习率定出来。 Adam-mini 的做法分两步。初始化时按注意力头把 Query、Key 归组,其余层每块共用一个二阶矩;嵌入层单独留用经典 Adam,因为里面大量 0 值、均值分布畸变,不能简单取均值。第二步对非嵌入块,把 Adam 的梯度平方换成块内均值再做移动平均,等价于每块一个学习率。 内存账算得清楚:学习率数量降到「嵌入层大小 + 输出层大小 + 非嵌入块数」,省多少取决于非嵌入参数占比。它不增加计算负担,还砍掉了大量平方根和张量除法;更低显存让 GPU 吃下更大 batch,也少了 CPU-GPU 通信瓶颈。外汇/贵金属相关的时序模型若在本地显卡跑,这种吞吐提升在高波动回测里可能直接缩短调参周期,但杠杆交易本身高风险,别把训练效率误读成胜率保证。
「在MT5里落地Adam-mini的权重更新」
Adam-mini 和经典 Adam 的核心差异在二阶矩 v:它不再为每个参数单独存梯度平方,而是按组取平均。全连接层 CNeuronBaseOCL 里,参数总量 = 输入尺寸 × 输出尺寸 + 输出尺寸(偏置),经典 Adam 一阶矩和二阶矩都存等量数据,Adam-mini 把二阶矩数量压到与输出张量大小对齐,显存占用直接降一个数量级。 实际计算放在 GPU 端 OpenCL 内核 UpdateWeightsAdamMini 里跑。任务空间是二维:第一维 = 输入数 + 1 个偏置偏移,第二维 = 结果张量大小。工作组沿第一维编组,本地内存开数组做线程间交换,先由各线程取输入值平方累加到本地数组并同步,再求和得到输入平方均值——这样只需取一次输出梯度平方乘该均值,就得到组平均二阶矩,避免反复读全局显存。 卷积层走 UpdateWeightsConvAdamMini,任务空间升到三维:第一维输入窗口+偏置,第二维过滤器数(对应全连接输出),第三维多模态单元序列。工作组按 Transformer 注意力头分组,每个头一个学习率;CNeuronMLMHAttentionOCL 调它时额外传 heads 和 variables 两参数并默认赋值,避免别的类调用崩。 改动不止内核。CNeuronBaseOCL::Init 里要把二阶矩缓冲区大小按 Adam-mini 收窄,否则省显存毫无意义。MT5 里开 OpenCL 专家顾问,挂上附件源码,切到 Adam-mini 优化枚举,能直接对比同模型下内存曲线和收敛步数。外汇/贵金属模型训练波动大,显存省下来不代表胜率提升,仅降低本地训练资源门槛,实盘仍属高风险。
class="type">bool CNeuronSTNNDecoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context) { if(!cEncoder.UpdateInputWeights(NeuronOCL, Context)) class="kw">return false;
交叉注意力权重更新的内核收口
在交叉注意力模块里,先把编码器对象喂给 updateInputWeights,失败就直接返回 false,成功才返回 true,这一步决定了后续梯度能否真正写回权重矩阵。 真正并行化的重头戏在 UpdateWeightsAdamMini 这个 OpenCL 内核:它用 get_local_id(0) 取线程在组内的输入索引,用 get_global_id(1) 取输出行号,LOCAL_ARRAY_SIZE 决定共享内存缓冲长度。 内层 do-while 把输入平方累加到 temp 局部数组,遇到 NaN 或 Inf 直接当 0 处理;随后用二分折叠求和,把 ls 个局部值并成 temp[0]。 最后 i==0 的线程把 matrix_g[o] 读进 temp[1] 并做非数清洗,这一步是 Adam 的 v 项前缀,外汇与贵金属模型训练属高风险,GPU 数值不稳定可能让权重更新偏离预期。 在 MT5 里把这段内核挂到自定义神经层,改 LOCAL_ARRAY_SIZE 从 64 调到 128,可能明显改变显存占用与收敛节奏,值得实机验证。
if(!CNeuronMLCrossAttentionMLKV::updateInputWeights(cEncoder.AsObject(), Context)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void UpdateWeightsAdamMini(__global class="type">float *matrix_w, __global const class="type">float *matrix_g, __global const class="type">float *matrix_i, __global class="type">float *matrix_m, __global class="type">float *matrix_v, const class="type">float l, const class="type">float b1, const class="type">float b2 ) { class=class="str">"cmt">//--- inputs const class="type">size_t i = get_local_id(class="num">0); const class="type">size_t inputs = get_local_size(class="num">0) - class="num">1; class=class="str">"cmt">//--- outputs const class="type">size_t o = get_global_id(class="num">1); const class="type">size_t outputs = get_global_size(class="num">1); __local class="type">float temp[LOCAL_ARRAY_SIZE]; const class="type">int ls = min((class="type">uint)LOCAL_ARRAY_SIZE, (class="type">uint)inputs); const class="type">float inp = (i < inputs ? matrix_i[i] : class="num">1.0f); class="type">int count = class="num">0; do { if(count == (i / ls)) { class="type">int shift = i % ls; temp[shift] = (count == class="num">0 ? class="num">0 : temp[shift]) + ((isnan(inp) || isinf(inp)) ? class="num">0 : inp*inp); } count++; barrier(CLK_LOCAL_MEM_FENCE); } while(count * ls < inputs); class=class="str">"cmt">//--- sum count = (ls + class="num">1) / class="num">2; do { if(i < count && (i + count) < ls) { temp[i] += temp[i + count]; temp[i + count] = class="num">0; } count = (count + class="num">1) / class="num">2; barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); class=class="str">"cmt">//--- calc v if(i == class="num">0) { temp[class="num">1] = matrix_g[o]; if(isnan(temp[class="num">1]) || isinf(temp[class="num">1])) temp[class="num">1] = class="num">0;