神经网络变得轻松(第五部分)OpenCL中的多线程计算
内容概述 1. MQL5 中如何组织多线程计算 2. 神经网络中的多线程计算 3. 利用 OpenCL 实现多线程计算 3.1. 前馈内核 3.2. 反向传播内核 3.3. 更新权重 3.4. 创建主程序的类 3.5. 创建基础神经元类来操控 OpenCL 3.6. CNet 类中的附加 4. 测试 结束语 链接 本文中用到的程序
概述
在之前的文章中,我们讨论过某些类型的神经网络实现。 如您所见,神经网络由大量相同类型的神经元组成,并在其中执行相同的操作。 然而,网络拥有的神经元越多,它消耗的计算资源也就越多。 结果就是,训练神经网络所需的时间呈指数增长,这是因为在隐藏层添加一个神经元,需要了解上一层和下一层中所有神经元的连接。 有一种减少神经网络训练时间的方法。 现代计算机的多线程功能可以同时计算多个神经元。 由于线程数量的增加,时间将可预见地大大减少。
1. MQL5 中如何组织多线程计算
MetaTrader 5 终端具有多线程体系架构。 终端中的线程分布受到严格控制。 根据文档,脚本和智能交易系统是在单独的线程中启动。 至于指示器,每个品种会提供单独的线程。 即时报价处理和历史记录同步于指标所在线程中执行。 这意味着终端只为每个智能交易系统分配一个线程。 某些计算可以在指标中执行,其可提供一个额外的线程。 然而,指标中过多的计算会减慢与即时报价数据处理相关的终端操作,这可能会导致针对市场状况的失控。 这种状况能对 EA 性能产生负面影响。
不过,有一个解决方案。 MetaTrader 5 开发人员为其提供了利用第三方 DLL 的能力。 在多线程体系结构上创建动态库会自动为函数库中实现的操作提供多线程支持。 在此,EA 操作以及与函数库之间的数据交换依然保留在智能交易系统的主线程之中。
第二个选项是利用 OpenCL 技术。 在这种情况下,我们可以用标准方法在支持该技术的处理器和视频卡上规划多线程计算。 对于此选项,程序代码不依赖所使用的设备。 该站点上有许多与 OpenCL 技术有关的出版物。 特别是,该主题在 [第五篇] 和 [第六篇] 文章里已有很好介绍。
因此,我决定使用 OpenCL。 首先,运用该技术时,用户不需要额外配置终端,并为第三方 DLL 设置权限。 其次,这样的智能交易系统可通过一个 EX5 文件在终端之间传送。 这允许将计算部分转移到视频卡,因视频卡通常在终端操作期间处于空闲状态。
2. 神经网络中的多线程计算
我们已选择了该技术。 现在,我们需要决定将计算部分拆分为线程的过程。 您还记得完全连接感知器算法吗? 信号顺序从输入层转至隐藏层,然后转至输出层。 没必要为每个层分配线程,因为计算必须按顺序执行。 直到收到来自上一层的结果之后,该层才能开始计算。 一层中独立神经元的计算不依赖该层中其他神经元的计算结果。 这意味着我们可为每个神经元分配单独的线程,并发送一整层的所有神经元进行并行计算。
深入到一个神经元的运算,我们可以研究把计算输入值与权重系数的乘积并行化的可能性。 不过,结果值的进一步求和,以及计算激活函数的数值被合并到一个线程当中。 我决定利用 vector 函数在单个 OpenCL 内核中实现这些操作。
类似的方法也用来拆分反馈线程。 其实现如下所示。 3. 利用 OpenCL 实现多线程计算
选择了基本方法后,我们就能够继续实现了。 我们从创建内核(可执行的OpenCL函数)开始。 根据以上逻辑,我们将创建 4 个内核。 3.1. 前馈内核。
与之前文章中讨论的方法类似,我们创建一个前馈推算内核 FeedForward 。
不要忘记内核是在每个线程中运行的函数。 调用内核时需设置此类线程的数量。 在内核内部的操作是特定循环内的嵌套操作;循环的迭代次数等于被调用线程的次数。如此,在前馈内核中,我们可以指定计算独立神经元状态的操作,并可从主程序调用内核时以指定神经元数量。
内核从参数中接收权重矩阵,输入数据数组和输出数据数组的引用,以及输入数组的元素数量,和激活函数类型。 请注意,OpenCL 中的所有数组都是一维的。 因此,如果在 MQL5 中将二维数组用做权重系数,则此处我们需要计算初始位置的位移,以便读取第二个、及后续神经元的数据。 __kernel void FeedForward(__global double *matrix_w, __global double *matrix_i, __global double *matrix_o, int inputs, int activation)
在内核的开头,我们获得线程的序列号,其可判定所计算神经元的序列号。 声明私密(内部)变量,包括向量变量 inp 和 weight 。 还要定义我们的神经元权重的位移。 { int i=get_global_id(0); double sum=0.0; double4 inp, weight; int shift=(inputs+1)*i;
接下来,组织一个循环来获取输入值与其权重的乘积的合计。 如上所述,我们用到 4 个元素 inp 和 weight 的向量来计算乘积合计。 然而,内核接收的所有数组并非都是 4 的倍数,因此缺少的元素应替换为零值。 注意输入数据向量中的一个 "1" - 它对应于贝叶斯偏差的权重。 for(int k=0; k<=inputs; k=k+4) { switch(inputs-k) { case 0: inp=(double4)(1,0,0,0); weight=(double4)(matrix_w[shift+k],0,0,0); break; case 1: inp=(double4)(matrix_i[k],1,0,0); weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],0,0); break; case 2: inp=(double4)(matrix_i[k],matrix_i[k+1],1,0); weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],0); break; case 3: inp=(double4)(matrix_i[k],matrix_i[k+1],matrix_i[k+2],1); weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],matrix_w[shift+k+3]); break; default: inp=(double4)(matrix_i[k],matrix_i[k+1],matrix_i[k+2],matrix_i[k+3]); weight=(double4)(matrix_w[shift+k],matrix_w[shift+k+1],matrix_w[shift+k+2],matrix_w[shift+k+3]); break; } sum+=dot(inp,weight); }
获得乘积之和后,计算激活函数,并将结果写入输出数据数组。 switch(activation) { case 0: sum=tanh(sum); break; case 1: sum=pow((1+exp(-sum)),-1); break; } matrix_o[i]=sum; } 3.2. 反向传播内核。
为反向传播误差梯度创建两个内核。 在第一个 CaclOutputGradient 中计算输出层误差。 它的逻辑很简单。 所获参考值在激活函数的数值范围进行常规化。 然后,将参考值和实际值之间的差乘以激活函数的导数。 将结果值写入梯度数组的相应单元格中。 __kernel void CaclOutputGradient(__global double *matrix_t, __global double *matrix_o, __global double *matrix_ig, int activation) { int i=get_global_id(0); double temp=0; double out=matrix_o[i]; switch(activation) { case 0: temp=clamp(matrix_t[i],-1.0,1.0)-out; temp=temp*(1+out)*(1-(out==1 ? 0.99 : out)); break; case 1: temp=clamp(matrix_t[i],0.0,1.0)-out; temp=temp*(out==0 ? 0.01 : out)*(1-(out==1 ? 0.99 : out)); break; } matrix_ig[i]=temp; }
在第二个内核中,在 CaclHiddenGradient 里计算隐藏层神经元的误差梯度。 内核构建类似于上述的前馈内核。 它还用到了向量运算。 区别在于前馈推算中以下一层的梯度向量替代前一层的输出值,并采用不同的权重矩阵。 而且,代替计算激活函数,结果合计是与激活函数导数的乘积。 内核代码给出如下。 __kernel void CaclHiddenGradient(__global double *matrix_w, __global double *matrix_g, __global double *matrix_o, __global double *matrix_ig, int outputs, int activation) { int i=get_global_id(0); double sum=0; double out=matrix_o[i]; double4 grad, weight; int shift=(outputs+1)*i; for(int k=0;k0) { if(CheckPointer(Weights)==POINTER_INVALID) { Weights=new CBufferDouble(); if(CheckPointer(Weights)==POINTER_INVALID) return false; } int count=(int)((numNeurons+1)*numOutputs); if(!Weights.Reserve(count)) return false; for(int i=0;i1 ? 1 : target<-1 ? -1 : target)-result[n]; error+=delta*delta; } error/= total; error = sqrt(error); recentAverageError+=(error-recentAverageError)/recentAverageSmoothingFactor; if(!neuron.calcOutputGradients(targetVals)) return;; //--- Calc Hidden Gradients CObject *temp=NULL; total=layers.Total(); for(int layerNum=total-2; layerNum>0; layerNum--) { CLayer *nextLayer=currentLayer; currentLayer=layers.At(layerNum); neuron=currentLayer.At(0); neuron.calcHiddenGradients(nextLayer.At(0)); } //--- CLayer *prevLayer=layers.At(total-1); for(int layerNum=total-1; layerNum>0; layerNum--) { currentLayer=prevLayer; prevLayer=layers.At(layerNum-1); neuron=currentLayer.At(0); neuron.updateInputWeights(prevLayer.At(0)); } }
针对 getResult 方法略微进行了一些修改。 if(CheckPointer(opencl)!=POINTER_INVALID && output.At(0).Type()==defNeuronBaseOCL) { CNeuronBaseOCL *temp=output.At(0); temp.getOutputVal(resultVals); return; }
附件中提供了所有方法和函数的完整代码。 4. 测试
采用与之前测试相同的条件,测试所创建类的操作。 已创建 Fractal_OCL EA 用于测试,它与先前创建的 Fractal_2 完全相同。 在 H1 时间帧,EURUSD 货币对上测试了神经网络的训练。 将 20 根烛条的数据输入到神经网络。 训练时采用最近两年的数据。 实验在支持 OpenCL 的 "Intel(R) Core(TM)2 Duo CPU T5750 @ 2.00GHz" 设备上运行。
在 5 小时 27 分钟的测试中,利用 OpenCL 技术的 EA 共执行了 75 个训练时期。 对于 12405 根烛条的区间,这平均需要 4 分 22 秒。 未利用 OpenCL 技术的同一智能交易系统,在同一台笔记本电脑上的相同神经网络体系结构下,每个时期平均要花费 40 分钟 48 秒。 如此,利用 OpenCL 可以令学习过程快 9.35 倍。
结束语
本文演示了利用 OpenCL 技术在神经网络中规划多线程计算的可能性。 测试表明,在同一 CPU 上,性能几乎提高了 10 倍。 期望利用 GPU 进一步提高算法性能 - 在这种情况下,将计算转移到兼容的 GPU 不需要修改智能交易系统代码。
总体而言,结果证明该方向的进一步发展具有良好的前景。
链接神经网络变得轻松 神经网络变得轻松(第二部分):网络训练和测试 神经网络变得轻松(第三部分):卷积网络 神经网络变得轻松(第四部分):循环网络 OpenCL: 通往并行世界的桥梁 OpenCL: 从初学到精通编程
本文中用到的程序
#
名称
类型
说明
1
Fractal_OCL.mq5
智能交易系统
利用 OpenCL 技术的含有分类神经网络(输出层中有 3 个神经元)的智能交易系统
2
NeuroNet.mqh
类库
用于创建神经网络的类库
3
NeuroNet.cl
代码库
OpenCL 程序代码库