9

我正在尝试制作一个简单的前馈神经网络的Java端口。
这显然涉及到大量的数值计算,所以我试图尽可能优化我的中央循环。结果应该在float数据类型的限制内是正确的。Java:微优化数组操作

我当前的代码如下所示(错误处理&初始化删除):

/** 
* Simple implementation of a feedforward neural network. The network supports 
* including a bias neuron with a constant output of 1.0 and weighted synapses 
* to hidden and output layers. 
* 
* @author Martin Wiboe 
*/ 
public class FeedForwardNetwork { 
private final int outputNeurons; // No of neurons in output layer 
private final int inputNeurons;  // No of neurons in input layer 
private int largestLayerNeurons; // No of neurons in largest layer 
private final int numberLayers;  // No of layers 
private final int[] neuronCounts; // Neuron count in each layer, 0 is input 
           // layer. 
private final float[][][] fWeights; // Weights between neurons. 
            // fWeight[fromLayer][fromNeuron][toNeuron] 
            // is the weight from fromNeuron in 
            // fromLayer to toNeuron in layer 
            // fromLayer+1. 
private float[][] neuronOutput;  // Temporary storage of output from previous layer 


public float[] compute(float[] input) { 
    // Copy input values to input layer output 
    for (int i = 0; i < inputNeurons; i++) { 
     neuronOutput[0][i] = input[i]; 
    } 

    // Loop through layers 
    for (int layer = 1; layer < numberLayers; layer++) { 

     // Loop over neurons in the layer and determine weighted input sum 
     for (int neuron = 0; neuron < neuronCounts[layer]; neuron++) { 
      // Bias neuron is the last neuron in the previous layer 
      int biasNeuron = neuronCounts[layer - 1]; 

      // Get weighted input from bias neuron - output is always 1.0 
      float activation = 1.0F * fWeights[layer - 1][biasNeuron][neuron]; 

      // Get weighted inputs from rest of neurons in previous layer 
      for (int inputNeuron = 0; inputNeuron < biasNeuron; inputNeuron++) { 
       activation += neuronOutput[layer-1][inputNeuron] * fWeights[layer - 1][inputNeuron][neuron]; 
      } 

      // Store neuron output for next round of computation 
      neuronOutput[layer][neuron] = sigmoid(activation); 
     } 
    } 

    // Return output from network = output from last layer 
    float[] result = new float[outputNeurons]; 
    for (int i = 0; i < outputNeurons; i++) 
     result[i] = neuronOutput[numberLayers - 1][i]; 

    return result; 
} 

private final static float sigmoid(final float input) { 
    return (float) (1.0F/(1.0F + Math.exp(-1.0F * input))); 
} 
} 

我正在与-server选项的JVM,而截至目前我的代码是25%和慢50%之间比类似的C代码。我能做些什么来改善这种情况?

谢谢

马丁Wiboe

编辑#1:看到回复的大量后,我也许应该澄清在我们的场景中的数字。在典型的运行过程中,该方法将被称为约50,000次不同的输入。一个典型的网络会有numberLayers = 3层,分别有190,2和1个神经元。因此最内层的循环将具有大约2*191+3=385的迭代次数(当计数第0层和第1层中添加的偏置神经元时)

编辑1:在此线程中实现了各种建议之后,我们的实现实际上与C版一样快在〜2%之内)。感谢所有的帮助!所有的建议都很有帮助,但由于我只能将一个答案标记为正确的答案,因此我会将它提交给@Durandal,以提供阵列优化,并且是唯一预先计算for循环标题的答案。

+5

你分析过它吗?知道它花费大部分时间在哪里会很有趣。 – 2010-06-08 00:04:35

+0

同意分析。不要注意它,并猜测需要改进的地方。 – Donnie 2010-06-08 00:12:29

+1

这样的代码是否容易并行化?如果是这样的话,编写它的多线程版本将拥有大量时间的单线程版本。我一直在那里,用Java重写正确的多线程Quicksort。在16核心机器上观看是个喜悦:http://stackoverflow.com/questions/2210185(它压缩了默认的Java API排序算法**大**时间)。除此之外,我看到了一些微观优化,但是我对神经网络的了解不够充分。 (最近它已经很难买单CPU机器,例如我不知道苹果是否仍然销售单CPU CPU) – SyntaxT3rr0r 2010-06-08 00:18:54

回答

5

不考虑实际的数学,Java中的数组索引本身可能是一个性能问题。考虑到Java没有真正的多维数组,而是将它们作为数组的数组来实现。在最内层的循环中,你可以访问多个索引,其中一些实际上在那个循环中是不变的。数组访问的一部分可以循环外招:

final int[] neuronOutputSlice = neuronOutput[layer - 1]; 
final int[][] fWeightSlice = fWeights[layer - 1]; 
for (int inputNeuron = 0; inputNeuron < biasNeuron; inputNeuron++) { 
    activation += neuronOutputSlice[inputNeuron] * fWeightsSlice[inputNeuron][neuron]; 
} 

这可能是服务器JIT执行类似的代码不变运动,找出的唯一方法是改变和配置文件了。在客户JIT上,无论如何,这应该会提高性能。 你可以尝试的另一件事是预先计算的for循环的退出条件,如:

for (int neuron = 0; neuron < neuronCounts[layer]; neuron++) { ... } 
// transform to precalculated exit condition (move invariant array access outside loop) 
for (int neuron = 0, neuronCount = neuronCounts[layer]; neuron < neuronCount; neuron++) { ... } 

再次JIT可能已经为你做这个,所以配置文件是否有帮助。

是否有一个点与1.0F乘以这里躲开我?:

float activation = 1.0F * fWeights[layer - 1][biasNeuron][neuron]; 

可能潜在地在可读性成本提高速度,其他的事情:直列乙状结肠()函数手动(JIT的有一个非常内联的限制和功能可能会更大)。 向后运行一个循环可能会稍微快一些(它当然不会改变结果),因为测试循环索引的零点比检查局部变量便宜一点(最内层的循环再次是一个有效的候选项,但不要指望所有情况下的输出都是100%相同的,因为添加浮点数a + b + c可能不同于a + c + b)。

+1

数组和预计算似乎可以将总体运行时间提高25%:)谢谢。 – 2010-06-08 15:26:09

3

我会考虑的第一件事是看看如果Math.exp是放缓你。请参阅this post on a Math.exp approximation以获得本机选择。

+0

我在想整个'sigmoid()'函数的查找表可能是值得的,但很难说不知道在这个函数中花了多少时间。 – 2010-06-08 00:08:40

+0

几乎可以肯定,查找表会大大提高该函数的速度,并可能帮助您从C恢复到Java的25%的损失。如果您怀疑在那里花了多少时间,请使用一些分析工具来确定需要花费多长时间。但是由于它至少被计算层次为神经元时间,所以很可能这是一个可以轻易消除的瓶颈。 – drharris 2010-06-08 00:13:04

+0

我尝试过使用该近似值,但不幸的是结果太不准确。你知道有什么办法通过交换一些速度来提高准确性吗? @Brendan Long和drharris查找表很可能是一个选项 - 我将进行数百万次的计算。如何实现一个使用浮点数作为关键字的线程安全查找表? – 2010-06-08 00:53:41

5

一开始,不这样做:

// Copy input values to input layer output 
for (int i = 0; i < inputNeurons; i++) { 
    neuronOutput[0][i] = input[i]; 
} 

但这:

System.arraycopy(input, 0, neuronOutput[0], 0, inputNeurons); 
+0

+1是的,速度要快很多。 – naiad 2010-06-08 00:19:32

+0

当然,但是在这个算法中只有两个数组被拷贝,一个拷贝输入,一个拷贝结果呢?真正的成本更可能在嵌套for循环中。 – 2010-06-08 00:27:07

+0

@W和V - 确实如此,但这并不是瓶颈所在。 – CurtainDog 2010-06-08 00:36:34

1

纯粹基于代码检查,你内心最环路必须计算到三维引用参数和它的做法很多。取决于你的数组维度,你可能会遇到缓存问题,因为每次循环迭代都必须跳过内存。也许你可以重新排列维度,以便内部循环尝试访问比现在更接近彼此的内存元素?

在任何情况下,请在进行任何更改之前对您的代码进行概况分析,看看真正的瓶颈在哪里。

+0

分析一定会有所帮助。我将尝试切换fWeights [layer - 1] [inputNeuron] [神经元]中的最后两个索引,以便inputNeuron变化,它是第3个索引。 – 2010-06-08 01:01:21

1

我建议使用定点系统而不是浮点系统。几乎所有使用int的处理器都比float快。最简单的方法就是将所有剩下的东西都转移一定数量(4或5是好的起点),并将最低4位作为小数。

你最内层的循环正在做浮点数学,所以这可能会给你一个提升。

+0

通常,一个好的观点(实际上很多系统实际上需要固定的精度是错误的,因为他们天真地使用FP)。然而在这种情况下,我不认为sigmoid函数适合这种技术。 – CurtainDog 2010-06-08 00:35:23

+0

使用现代硬件,一个FP指令比定点执行相同操作所需的多个整数指令要快。 (特别是对于乘法,需要转换才能将点放在正确的位置; add/sub更便宜。) – 2016-11-20 03:46:07

+0

整数非常适合处理最初以整数开头的像素,因为每个元素常常有16位就足够了。因此,每个SIMD矢量与浮点数相比,您可以获得两倍的元素,并且还有一些SSE指令专为像素上经常需要的东西而设计。所以,当你有多个并行的16位元素时,使用整数是很有用的,如果它保存转换为/从浮动。对于其他情况,通常不值得。 – 2016-11-20 03:47:13

0

优化的关键是首先测量花费的时间。通过调用System来环绕算法的各个部分。nanoTime():

long start_time = System.nanoTime(); 
doStuff(); 
long time_taken = System.nanoTime() - start_time; 

我猜想,在使用System.arraycopy()会有点帮助,你会发现在内部循环的实际成本。

根据您的发现,您可能会考虑用整数算术替换浮点运算。

8

一些提示。

  • 在你最内层的循环中,考虑你是如何遍历你的CPU缓存并重新排列你的矩阵,以便顺序地访问最外层数组。这将导致您按顺序访问缓存,而不是在整个地方跳跃。高速缓存命中可以比高速缓存未命中快两个命令。 e.g重组fWeights所以它是作为

激活访问+ = neuronOutput [层-1] [inputNeuron] * fWeights [层 - 1] [神经元] [inputNeuron];

  • 在循环(一次)之外不能在循环内(每次)执行工作。每次可以将其放置在局部变量中时,不要执行[图层-1]查找。你的IDE应该能够轻松地重构它。

  • Java中的多维数组并不像它们在C中那样高效。它们实际上是多层单维数组。您可以重构代码,以便仅使用一维数组。当您可以将结果数组作为参数传递时,不返回新数组。 (保存每次通话创建一个新对象)。

  • 而不是在整个地方执行layer-1,为什么不使用layer1作为layer-1并使用layer1 + 1而不是layer。

+2

哇 - 优化数组访问将运行时间缩短了20%。谢谢。 – 2010-06-08 15:25:18

+0

交换'fWeights'的最后两个索引也将允许'activation + = neuronOutput [layer-1] [inputNeuron] * fWeights [layer-1] [inputNeuron] [neuron];'循环输入神经元以向量化SSE2或AVX(甚至FMA),如果Java(或您的特定JVM)有任何'-fast-math'选项。使用SIMD将单步访问切换为连续模式是一项巨大的优势。 – 2016-11-20 03:55:11

3

用整数阶跃传递函数替换昂贵的浮点S形传递函数。

sigmoid传递函数是一个有机模拟突触学习的模型,而这似乎是一个阶梯函数的模型。

这个历史的先例是,Hinton直接根据关于真正突触的认知科学理论的第一原理来设计后支持算法,后者又基于真实的模拟测量结果,其结果是sigmoid。

但是sigmoid传递函数似乎是数字阶跃函数的有机模型,当然不能直接实现有机模型。

与使用阶跃函数的直接数字实现(小于零= -1,大于零= +1)替换有机S形传递函数的昂贵的浮点实现,而不是建模模型。

enter image description here 大脑不能做到这一点,但backprop可以!

这不仅可以线性和急剧提高单个学习迭代的性能,还可以减少训练网络所需的学习迭代次数:支持学习本身具有数字化的证据。

也支持计算机科学天生酷的论点。