文章 "神经网络变得轻松(第五部分):OpenCL 中的多线程计算"

 

新文章 神经网络变得轻松(第五部分):OpenCL 中的多线程计算已发布:

我们早前已经讨论过某些类型的神经网络实现。 在所研究的网络中,每个神经元都重复相同的操作。 逻辑上进一步应利用现代技术提供的多线程计算功能来加快神经网络学习过程。 本文介绍了一种可能的实现方式。

我们已选择了该技术。 现在,我们需要决定将计算部分拆分为线程的过程。 您还记得完全连接感知器算法吗? 信号顺序从输入层转至隐藏层,然后转至输出层。 没必要为每个层分配线程,因为计算必须按顺序执行。 直到收到来自上一层的结果之后,该层才能开始计算。 一层中独立神经元的计算不依赖该层中其他神经元的计算结果。 这意味着我们可为每个神经元分配单独的线程,并发送一整层的所有神经元进行并行计算。  

完全连接感知器

深入到一个神经元的运算,我们可以研究把计算输入值与权重系数的乘积并行化的可能性。 不过,结果值的进一步求和,以及计算激活函数的数值被合并到一个线程当中。 我决定利用 vector 函数在单个 OpenCL 内核中实现这些操作。

类似的方法也用来拆分反馈线程。 其实现如下所示。

作者:Dmitriy Gizlyk

 

这篇文章对于一个不了解 OpenCL 的人来说非常复杂,我试图理解其中的精髓,我阅读了您在您的位置上标注的文章,.....。我什么也没看懂,或者说只看懂了一部分。

你的文章可能很精彩,但也太复杂了,我认为你需要考虑 OpenCL 的基本原理,也许还需要使用 OpenCL.mqh 等内置库,因为没有这些库,你就无法理解,文章缺乏讨论就是证明。我不理解你的代码,因为其中引用了许多我不知道的库。

我不明白的第二个问题是如何在优化模式 下使用 OpenCL。如果多个 OpenCL 实例同时访问视频卡????,会发生什么情况?在 OpenCL 中,有很多方法可以加快计算速度,即使是同一件事。例如,去掉 double 改用 int。等等。....问题很多。

我建议从多线程编程代码中一个非常简单的例子开始。假设有一个感知器,它有 n 个输入,我们需要分别编写一个简单的代码来计算 [-1,1] 范围内的归一化函数,并使用 OpenCL 通过 tanh 计算神经元的值。OpenCL 中的代码本身应该有注释。再考虑一个非常简单的交易智能交易系统,在测试器中使用一个指标对历史记录进行优化,也是一个简单的指标,即只给出一个值,如 RSI 或 CCI。也就是说,一切都要尽可能简化,不要有任何阴谋诡计,只要尽可能简单。我认为,将来将其扩展到许多感知器并编写自己的智能交易系统也不成问题。

感谢您的文章。

 
这篇文章正是您所需要的。感谢作者。我第一次就理解了神经元的并行计算。只是还需要 "成熟"。)
 
Реter Konow:
这篇文章正是您所需要的。感谢作者。我第一次就理解了神经元的并行计算。只是还需要 "成熟"。)

嗯,一个还没成熟,另一个 .....但我想每个人都想成熟。

我同意这篇文章很有必要

我正在努力理解它。

也许你可以写我让你写的东西,如果这对你来说很容易的话。
 
Boris Egorov:

嗯,一个还没熟,一个是 .....我想每个人都想要一个。

我同意这是一篇好文章。

我正在努力理解它。

如果对你来说很容易的话,也许你可以写我要求你写的东西。
据我所知,你不需要在优化模式 下使用 OpenCL,因为优化本身就是多线程的。 神经网络是通过训练优化的,而不是通过内部测试仪。

网络中的多线程对于加快处理一层的信息量(数据)和将接力棒传给下一层是必要的。

每个神经元都在自己的线程中处理自己的 "一块 "值空间,而不会让其他神经元等待。这就是在 NS 中使用 OpenCL 技术提供的多线程的优势。
 
所有这一切都很有趣,尽管我不太可能重复,但令我困惑的问题是:如果训练中只增加了一个核心,10 倍的增长从何而来?
 
Aleksey Vyazmikin:
所有这一切都很有趣,虽然我不太可能重复,但我对这个问题感到困惑--如果只增加一个内核参与训练,为什么会增加 10 倍?

据我所知,在 OpenCL 上进行训练时,会同时对多个指标进行并行计算。

 
Aleksey Vyazmikin:
所有这些都很有趣,虽然我不太可能重复,但我对这个问题感到困惑--如果在训练中只多用了一个内核,为什么会增加 10 倍?

内核 不是物理或逻辑内核,而是在处理器或显卡的所有逻辑内核 上并行执行的固件。

 

以下是我不明白的几点

__kernel void FeedForward(__global double *matrix_w,
                          __global double *matrix_i,
                          __global double *matrix_o,
                          int inputs, 
                          int activation)
  {
   int i=get_global_id(0); //这一行有什么作用?
   double sum=0.0;
   double4 inp, weight;
   int shift=(inputs+1)*i; //这一行有什么作用?
   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;
  }

我认为有必要向 "远方 "的人解释一下,最主要的是我不明白代码中的所有内容都是在哪里初始化的,又是在哪里调用的。

 
Boris Egorov:

以下是一些我不明白的地方

我认为有必要向 "远方 "的人解释一下,最主要的是,我不明白代码中所有东西都是在哪里初始化的,所有东西都是在哪里调用的。

日安,鲍里斯。
你附上了内核代码。正如马克西姆在上文所写,这是一个在微处理器或显卡内核上执行的微程序(取决于初始化上下文)。关键在于同时调用多个此类程序,并在所有内核上并行执行。每个程序都使用自己的数据。

int i=get_global_id(0); //这一行有什么作用?

这一行只是从并行运行副本池中获取微程序的序列号。在代码中,该序列号用于确定需要处理的数据块。在这种情况下,它将与层中神经元的数量相对应。

此外,为了使微程序内部的操作更加并行,还使用了矢量变量--这是一个固定大小的小数组。在本例中,向量的维度为 4,即包含 4 个元素的数组。

double4 inp, weight;

但输入数组的大小并不总是 4 的倍数。因此,为了避免错误的数组引用,使用了 swith,缺失的值用 "0 "填充。在这种情况下,每个神经元的权重数组的维度都 比输入元素的维度大 1 个元素,这个元素被用作贝叶斯偏置。早些时候,我们为此目的使用了一个额外的神经元,其中我们只修正了权重,而没有重新计算输出值,输出值始终等于 "1"。在这里,我并没有在输入数组中不断添加 "1",而是直接在代码中写入,因此输入数组的大小不会改变。

           inp=(double4)(1,0,0,0); //这一行有什么作用?
           weight=(double4)(matrix_w[shift+k],0,0,0);

dot 函数返回两个向量的标量乘积,也就是说,在我们的例子中,我们在一行中计算了 4 个输入值的权重乘积之和。

sum+=dot(inp,weight); //这一行有什么作用?
 
Maxim Dmitrievsky:

内核 不是物理或逻辑内核,而是在处理器或显卡的所有逻辑内核 上并行运行的固件

所以,这不是新闻--以前是一个内核,负载在它上面,现在是两个内核,负载减半....。最有可能的是,变化更为显著,对比并不正确。