神经网络变得简单(第 58 部分):决策转换器(DT)·进阶篇
(2/3)· 当 GPT 思路撞上 MT5 策略,在途回报如何改写智能体动作生成逻辑
「嵌入层梯度回传的并行写法」
这段 OpenCL 内核处理的是嵌入层反向传播:每个线程先通过 do-while 累加 windows[emb] 找到当前 pos 所属的 emb 区间,定位到输出梯度和权重的偏移量 shift_out、shift_weights。 内层 for 循环把 outputs_gradient 与 weights 做 window_out 长度的点乘,得到原始 value;若 std[emb] 大于 0 则除以它做归一化,否则直接写回 inputs_gradient[pos],这一步标准差为 0 时跳过了缩放。 下方 EmbeddingUpdateWeightsAdam 内核刚拿到全局 id i 并初始化 emb=-1,就准备走同样的区间查找逻辑,配合传入的 l、b1、b2 三个标量做 Adam 更新——在 MT5 策略测试器里把 window_out 设成 16、b1=0.9、b2=0.999 跑一遍,能直接观察显存带宽是否成为瓶颈。 外汇与贵金属杠杆交易高风险,这类 GPU 内核仅用于离线训练验证,实盘信号须另行做样本外检验。
{
const class="type">int pos = get_global_id(class="num">0);
class="type">int emb = -class="num">1;
class="type">int count = class="num">0;
do
{
emb++;
count += windows[emb];
}
while(count <= pos);
const class="type">int shift_out = emb * window_out;
const class="type">int shift_weights = (pos + emb) * window_out;
class="type">class="kw">float value = class="num">0;
for(class="type">int i = class="num">0; i < window_out; i++)
value += outputs_gradient[shift_out + i] * weights[shift_weights + i];
class="type">class="kw">float s = std[emb];
if(s > class="num">0)
value /= s;
class=class="str">"cmt">//---
inputs_gradient[pos] = value;
}
__kernel class="type">void EmbeddingUpdateWeightsAdam(__global class="type">class="kw">float *weights,
__global const class="type">class="kw">float *gradient,
__global const class="type">class="kw">float *inputs,
__global class="type">class="kw">float *matrix_m,
__global class="type">class="kw">float *matrix_v,
__global class="type">int *windows,
__global class="type">class="kw">float *std,
const class="type">int window_out,
const class="type">class="kw">float l,
const class="type">class="kw">float b1,
const class="type">class="kw">float b2
)
{
const class="type">int i = get_global_id(class="num">0);
class="type">int emb = -class="num">1;◍ 嵌入层权重更新的 Adam 细节
在 MT5 的 OpenCL 神经网络实现里,嵌入层(Embedding)的权重更新走的是带 L1/L2 正则的 Adam 变体。下面这段循环先把索引 i 映射到具体的嵌入块与窗口偏移,再算梯度并维护一阶、二阶动量。 int count = 0; int shift = 0; do { emb++; shift = count; count += (windows[emb] + 1) * window_out; } while(count <= i); const int shift_out = emb * window_out; int shift_in = shift / window_out - emb; shift = (i - shift) / window_out; float weight = weights[i]; float g = gradient[shift_out] * inp / std[emb]; float mt = b1 * matrix_m[i] + (1 - b1) * g; float vt = b2 * matrix_v[i] + (1 - b2) * pow(g, 2); float delta = l * (mt / (sqrt(vt) + 1.0e-37f) - (l1 * sign(weight) + l2 * weight)); if(delta * g > 0) weights[i] = clamp(weights[i] + delta, -MAX_WEIGHT, MAX_WEIGHT); matrix_m[i] = mt; matrix_v[i] = vt; } 逐行看:emb 从 0 自增,直到累计 count 越过当前神经元索引 i,定位出落在第几个嵌入窗口;shift_out 是嵌入块的基础偏移,shift_in 与 shift 还原出块内相对位置。梯度 g 用 std[emb] 做标准化,避免不同窗口量纲不一致。 mt、vt 就是 Adam 的一阶和二阶动量,b1、b2 通常取 0.9 与 0.999 附近。delta 在动量商的基础上减掉 L1(sign)和 L2(weight)惩罚,学习率 l 控制步长;注意分母加了 1.0e-37f 这种极小量防除零,外汇与贵金属样本噪声大时,这种数值保护直接影响训练能否跑通。 类声明里 CNeuronEmbeddingOCL 继承自 CNeuronBaseOCL,保护成员 a_Windows[] 存各窗口长度,i_WindowOut 是输出窗口,WeightsEmbedding 与两个动量缓冲分离存储,方便在 GPU 上批量更新。想验证的话,在 MT5 策略测试器里挂一个用该类初始化的小网络,打印 weights[i] 前后差值,能直接看到 delta 符号截断(delta*g>0 才更新)带来的稀疏化效果。
class="type">int count = class="num">0; class="type">int shift = class="num">0; do { emb++; shift = count; count += (windows[emb] + class="num">1) * window_out; } while(count <= i); const class="type">int shift_out = emb * window_out; class="type">int shift_in = shift / window_out - emb; shift = (i - shift) / window_out; class="type">class="kw">float weight = weights[i]; class="type">class="kw">float g = gradient[shift_out] * inp / std[emb]; class="type">class="kw">float mt = b1 * matrix_m[i] + (class="num">1 - b1) * g; class="type">class="kw">float vt = b2 * matrix_v[i] + (class="num">1 - b2) * pow(g, class="num">2); class="type">class="kw">float delta = l * (mt / (sqrt(vt) + class="num">1.0e-37f) - (l1 * sign(weight) + l2 * weight)); if(delta * g > class="num">0) weights[i] = clamp(weights[i] + delta, -MAX_WEIGHT, MAX_WEIGHT); matrix_m[i] = mt; matrix_v[i] = vt; } class CNeuronEmbeddingOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">int a_Windows[]; class="type">int i_WindowOut; class="type">int i_StackSize; class="type">int i_WindowsBuffer; class="type">int i_STDBuffer; class=class="str">"cmt">//--- CBufferFloat WeightsEmbedding; CBufferFloat FirstMomentumEmbed; CBufferFloat SecondMomentumEmbed; class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronEmbeddingOCL(class="type">void); ~CNeuronEmbeddingOCL(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 stack_size, class="type">uint window_out, class="type">int &windows[]); class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); 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=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronEmbeddingOCL; } class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class="kw">virtual class="type">bool Clear(class="type">void); }; CNeuronEmbeddingOCL::CNeuronEmbeddingOCL(class="type">void) {
Embedding 层的 OpenCL 初始化与权重铺排
这段 CNeuronEmbeddingOCL::Init 负责在 GPU 侧把嵌入层真正立起来。进来先卡三道门槛:open_cl 指针无效、window_out 或 stack_size 为 0、windows 数组为空,任一不满足直接返 false,避免后续在空结构上分配显存。 神经元总数按 window_out * windows.Size() * stack_size 算,比如 window_out=10、windows 含 3 个窗口、stack_size=2,就是 60 个神经元交给基类 Init 用 ADAM 优化器接管。 权重数量不是简单相乘,而是遍历 windows 累加 (windows[i]+1)*window_out,再预留空间。初始化范围用 k = 1/sqrt(weights/window_out) 做缩放,每个权重取 k*(2*GenerateWeight()-1)*WeightsMultiplier,属于带方差约束的均匀初始化,能缓解嵌入矩阵过大时的梯度失衡。 显存端先后建了 WeightsEmbedding 及一阶、二阶动量缓冲,并单独把 a_Windows 和标准差缓冲通过 AddBuffer 推到 OpenCL 设备;i_WindowsBuffer 或 i_STDBuffer 建失败就回退。宏里 def_k_Embedding 定为 59、def_k_emb_inputs 为 0,是给后续 kernel 调用的固定偏移量,改这两个数可能直接改变输入拼接布局。 销毁路径在前面析构逻辑里:ArrayFree(a_Windows) 清本地数组,若 OpenCL 有效且缓冲句柄非负就 BufferFree,最后把句柄置 INVALID_HANDLE、i_WindowOut 和 i_StackSize 复位。开 MT5 把这段贴进自定义神经元类,调一下 windows 数组长度就能看到显存占用的线性变化。
ArrayFree(a_Windows); if(!!OpenCL) { if(i_WindowsBuffer >= class="num">0) OpenCL.BufferFree(i_WindowsBuffer); if(i_STDBuffer >= class="num">0) OpenCL.BufferFree(i_STDBuffer); } class=class="str">"cmt">//-- i_WindowsBuffer = INVALID_HANDLE; i_STDBuffer = INVALID_HANDLE; i_WindowOut = class="num">0; i_StackSize = class="num">1; } class="type">bool CNeuronEmbeddingOCL::Init(class="type">uint numOutputs,class="type">uint myIndex,COpenCLMy *open_cl,class="type">uint stack_size, class="type">uint window_out,class="type">int &windows[]) { if(CheckPointer(open_cl) == POINTER_INVALID || window_out <= class="num">0 || windows.Size() <= class="num">0 || stack_size <= class="num">0) class="kw">return false; if(!!OpenCL && OpenCL != open_cl) class="kw">delete OpenCL; class="type">uint numNeurons = window_out * windows.Size() * stack_size; if(!CNeuronBaseOCL::Init(numOutputs,myIndex,open_cl,numNeurons,ADAM,class="num">1)) class="kw">return false; class="type">uint weights = class="num">0; ArrayCopy(a_Windows,windows); i_WindowOut = (class="type">int)window_out; i_StackSize = (class="type">int)stack_size; for(class="type">uint i = class="num">0; i < windows.Size(); i++) weights += (windows[i] + class="num">1) * window_out; if(!WeightsEmbedding.Reserve(weights)) class="kw">return false; class="type">class="kw">float k = class="num">1.0f / sqrt((class="type">class="kw">float)weights / (class="type">class="kw">float)window_out); for(class="type">uint i = class="num">0; i < weights; i++) if(!WeightsEmbedding.Add(k * (class="num">2 * GenerateWeight() - class="num">1.0f)*WeightsMultiplier)) class="kw">return false; if(!WeightsEmbedding.BufferCreate(OpenCL)) class="kw">return false; if(!FirstMomentumEmbed.BufferInit(weights, class="num">0)) class="kw">return false; if(!FirstMomentumEmbed.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- if(!SecondMomentumEmbed.BufferInit(weights, class="num">0)) class="kw">return false; if(!SecondMomentumEmbed.BufferCreate(OpenCL)) class="kw">return false; i_WindowsBuffer = OpenCL.AddBuffer(class="kw">sizeof(class="type">int) * a_Windows.Size(),CL_MEM_READ_WRITE); if(i_WindowsBuffer < class="num">0 || !OpenCL.BufferWrite(i_WindowsBuffer,a_Windows,class="num">0,class="num">0,a_Windows.Size())) class="kw">return false; i_STDBuffer = OpenCL.AddBuffer(class="kw">sizeof(class="type">class="kw">float) * a_Windows.Size(),CL_MEM_READ_WRITE); if(i_STDBuffer<class="num">0) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="macro">#define def_k_Embedding class="num">59 class="macro">#define def_k_emb_inputs class="num">0
「Embedding 层的前向传参怎么绑 OpenCL 缓冲」
在 MT5 用 OpenCL 加速神经网络时,Embedding 层的前向计算靠一组宏常量来定位内核参数槽位。上面代码里 def_k_Embedding 主核用了 5 个参数索引:输入缓冲 0、输出缓冲 1、权重 2、窗口 3、标准差缓冲 4,另外 def_k_EmbeddingHiddenGradient(60)和 def_k_EmbeddingUpdateWeightsAdam(61)分别管反向梯度与 Adam 权重更新,后者一次要传 11 个参(权重、梯度、输入、m 矩阵、v 矩阵、窗口、std、输出窗口、学习率、b1、b2)。 CNeuronEmbeddingOCL::feedForward 里做的第一件事,是把上游神经元的输出索引绑到内核的输入参数槽。若 NeuronOCL 或 OpenCL 句柄为空直接返 false,这是避免空指针进 GPU 核的典型防御。 随后连续三次 SetArgumentBuffer:分别把输入缓冲(def_k_emb_inputs)、本层输出缓冲(def_k_emb_outputs)、以及标准差缓冲(def_k_emb_std,对应 i_STDBuffer)推给 def_k_Embedding 内核。任何一次绑定失败就 printf 打出函数名、GetLastError 和 __LINE__ 然后返 false——在 MT5 策略测试器里这类报错能直接定位到是哪一行 OpenCL 参数没挂上。 开 MT5 验证时,若你自建 Embedding 类却在前向阶段黑屏无输出,优先查这三个 SetArgumentBuffer 的返回值;外汇与贵金属品种上跑这类 GPU 网络仍属高风险,显存溢出或驱动不匹配都可能让推断静默失败。
class="macro">#define def_k_emb_outputs class="num">1 class="macro">#define def_k_emb_weights class="num">2 class="macro">#define def_k_emb_windows class="num">3 class="macro">#define def_k_emb_std class="num">4 class="macro">#define def_k_emb_stack_size class="num">5 class=class="str">"cmt">//--- class="macro">#define def_k_EmbeddingHiddenGradient class="num">60 class="macro">#define def_k_ehg_inputs_gradient class="num">0 class="macro">#define def_k_ehg_outputs_gradient class="num">1 class="macro">#define def_k_ehg_weights class="num">2 class="macro">#define def_k_ehg_windows class="num">3 class="macro">#define def_k_ehg_std class="num">4 class="macro">#define def_k_ehg_window_out class="num">5 class=class="str">"cmt">//--- class="macro">#define def_k_EmbeddingUpdateWeightsAdam class="num">61 class="macro">#define def_k_euw_weights class="num">0 class="macro">#define def_k_euw_gradient class="num">1 class="macro">#define def_k_euw_inputs class="num">2 class="macro">#define def_k_euw_matrix_m class="num">3 class="macro">#define def_k_euw_matrix_v class="num">4 class="macro">#define def_k_euw_windows class="num">5 class="macro">#define def_k_euw_std class="num">6 class="macro">#define def_k_euw_window_out class="num">7 class="macro">#define def_k_euw_learning_rate class="num">8 class="macro">#define def_k_euw_b1 class="num">9 class="macro">#define def_k_euw_b2 class="num">10 class="type">bool CNeuronEmbeddingOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || !OpenCL) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_Embedding, def_k_emb_inputs, NeuronOCL.getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Embedding, def_k_emb_outputs, getOutputIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Embedding, def_k_emb_std, i_STDBuffer)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; }
◍ Embedding 核的参数绑定与层描述结构
在 GPU 上跑 Embedding 层,先得把权重矩阵、窗口缓冲和栈长度塞进 OpenCL 核的参数槽。任何一次 SetArgumentBuffer 或 SetArgument 失败都直接返回 false,并在终端打印函数名、错误码和行号,方便在 MT5 里定位是哪一根线没接上。 核启动用二维工作项:global_work_size 设为 {i_WindowOut, a_Windows.Size()},local_work_size 设为 {i_WindowOut, 1}。这意味着每个输出窗口由一条包含 i_WindowOut 个线程的一维组处理,窗口数量由第二个维度摊开。若 Execute 返回失败,同样走 false 分支并报错。 Clear 方法负责把 Output 缓冲清零(BufferInit 到 0),若 OpenCL 上下文不存在则直接返回 true 跳过写回。这一轻量复位逻辑在反复训练迭代中能避免旧梯度污染。 CLayerDescription 类用普通字段描述一层网络:type 标神经元类型,count 是神经元数,window 与 window_out 分别是输入输出窗口尺寸,step 为滑动步长,layers 和 batch 控制堆叠与批大小,activation 与 optimization 枚举选定激活函数和优化器。把这些字段在代码里打印出来,就能在策略加载时核对网络拓扑是否符合预期。
if(!OpenCL.SetArgumentBuffer(def_k_Embedding, def_k_emb_weights, WeightsEmbedding.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgumentBuffer(def_k_Embedding, def_k_emb_windows, i_WindowsBuffer)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; } if(!OpenCL.SetArgument(def_k_Embedding, def_k_emb_stack_size, i_StackSize)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__,GetLastError(), __LINE__); class="kw">return false; } class="type">uint global_work_offset[class="num">2] = {class="num">0,class="num">0}; class="type">uint global_work_size[class="num">2] = {i_WindowOut,a_Windows.Size()}; class="type">uint local_work_size[class="num">2] = {i_WindowOut,class="num">1}; if(!OpenCL.Execute(def_k_Embedding, class="num">2, global_work_offset, global_work_size,local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__,GetLastError()); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronEmbeddingOCL::Clear(class="type">void) { if(!Output.BufferInit(Output.Total(),class="num">0)) class="kw">return false; if(!OpenCL) class="kw">return true; class=class="str">"cmt">//--- class="kw">return Output.BufferWrite(); } class CLayerDescription : class="kw">public CObject { class="kw">public: class=class="str">"cmt">/** Constructor */ CLayerDescription(class="type">void); class=class="str">"cmt">/** Destructor */~CLayerDescription(class="type">void) {}; class=class="str">"cmt">//--- class="type">int type; class=class="str">"cmt">///< Type of neurons in layer(\ref ObjectTypes) class="type">int count; class=class="str">"cmt">///< Number of neurons class="type">int window; class=class="str">"cmt">///< Size of class="kw">input window class="type">int window_out; class=class="str">"cmt">///< Size of output window class="type">int step; class=class="str">"cmt">///< Step size class="type">int layers; class=class="str">"cmt">///< Layers count class="type">int batch; class=class="str">"cmt">///< Batch Size ENUM_ACTIVATION activation; class=class="str">"cmt">///< Type of activation function(class="macro">#ENUM_ACTIVATION) ENUM_OPTIMIZATION optimization; class=class="str">"cmt">///< Type of optimization method(class="macro">#ENUM_OPTIMIZATION)
用代码堆出五层强化学习网络骨架
在 MT5 里搭一个能跑价格行为的智能体,第一步不是调参,而是把网络层描述对象塞进 CArrayObj。下面这段 CreateDescriptions 函数直接给出了五层结构,从输入到稀疏注意力全用 CLayerDescription 实例声明,复制进 EA 的神经网络模块就能编译验证。 输入层用 defNeuronBaseOCL,节点数由 NRewards + BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions 动态算出来,优化器选 ADAM、激活函数 None,相当于把账户状态、K线描述和动作空间一次性送进网络。 第二层接 BatchNorm(defNeuronBatchNormOCL),batch 写死 1000,用来稳初始分布;第三层 Embedding 把 HistoryBars 根 K 线压成 EmbeddingSize 维向量,windows 数组按 {BarDescr*NBarInPattern, AccountDescr, TimeDescription, NRewards, NActions} 切分特征通道。 第四层是重点:defNeuronMLMHSparseAttentionOCL 做多头稀疏注意力,step=4、window_out=16、layers=4,dropout 概率用 Sparse 常量控制,外汇与贵金属行情噪声大,这种稀疏结构可能降低过拟合概率。第五层回到全连接(defNeuronBaseOCL),LatentCount 个节点配 LReLU,留作策略隐变量。 把这段逻辑跑起来后,你只需改 HistoryBars 和 EmbeddingSize 两个量,就能观察 Agent 在 XAUUSD 15M 上信号频率的变化,高风险品种建议先开模拟盘验证。
class="type">class="kw">float probability; class=class="str">"cmt">///< Probability of neurons shutdown, only Dropout used class="type">int windows[]; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Copy(CLayerDescription *source); class=class="str">"cmt">//--- class="kw">virtual class="type">bool class="kw">operator= (CLayerDescription *source) { class="kw">return Copy(source); } }; class="type">bool CreateDescriptions(CArrayObj *agent) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!agent) { agent = new CArrayObj(); if(!agent) class="kw">return false; } class=class="str">"cmt">//--- Agent agent.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (NRewards + BarDescr*NBarInPattern + AccountDescr + TimeDescription + NActions); descr.activation = None; descr.optimization = ADAM; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = class="num">1000; descr.activation = None; descr.optimization = ADAM; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronEmbeddingOCL; prev_count = descr.count = HistoryBars; { class="type">int temp[] = {BarDescr*NBarInPattern,AccountDescr,TimeDescription,NRewards,NActions}; ArrayCopy(descr.windows,temp); } class="type">int prev_wout = descr.window_out = EmbeddingSize; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMLMHSparseAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = class="num">4; descr.probability = Sparse; descr.optimization = ADAM; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.optimization = ADAM; descr.activation = LReLU; if(!agent.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5