9

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

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

/**
 * 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,到目前为止,我的代码比类似的 C 代码慢 25% 到 50%。我能做些什么来改善这种情况?

谢谢,

马丁·维博

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

编辑#1:在这个线程中实现了各种建议之后,我们的实现几乎和 C 版本一样快(在 ~2% 以内)。感谢所有的帮助!所有建议都很有帮助,但由于我只能将一个答案标记为正确答案,因此我会将其提供给 @Durandal 以建议数组优化并作为唯一一个预先计算for循环标头​​的答案。

4

8 回答 8

8

一些技巧。

  • 在最内层的循环中,考虑如何遍历 CPU 缓存并重新排列矩阵,以便按顺序访问最外层的数组。这将导致您按顺序访问缓存,而不是到处乱跳。缓存命中可以比缓存未命中快两个数量级。例如重组 fWeights 使其被访问为

激活 += 神经元输出[layer-1][inputNeuron] * fWeights[layer - 1][neuron][inputNeuron];

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

  • Java 中的多维数组不如 C 中的高效。它们实际上是多层单维数组。您可以重组代码,以便只使用一维数组。

  • 当您可以将结果数组作为参数传递时,不要返回新数组。(保存在每次调用时创建一个新对象)。

  • 与其在所有地方执行第 1 层,为什么不将第 1 层用作第 1 层并使用第 1 层 + 1 代替第 1 层。

于 2010-06-08T05:19:34.933 回答
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];

其他可能以牺牲可读性为代价提高速度的事情:手动内联 sigmoid() 函数(JIT 对内联有非常严格的限制,并且函数可能更大)。向后运行循环可能会稍微快一些(当然它不会改变结果),因为根据零测试循环索引比检查局部变量便宜一点(最里面的循环再次是一个潜在的候选者,但不要期望输出在所有情况下都是 100% 相同的,因为添加浮点数 a + b + c 可能与 a + c + b) 不同。

于 2010-06-08T11:13:26.840 回答
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 );
于 2010-06-08T00:09:25.123 回答
3

我要调查的第一件事是看看是否Math.exp会减慢您的速度。请参阅有关 Math.exp 近似值的这篇文章以获取本机替代方案。

于 2010-06-08T00:06:56.630 回答
3

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

sigmoid 传递函数是有机模拟突触学习的模型,它又似乎是阶跃函数的模型。

这方面的历史先例是 Hinton 直接根据关于真实突触的认知科学理论的第一原理设计了反向传播算法,而这些原理又基于真实的模拟测量,结果证明是 sigmoid。

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

与其对模型进行建模,不如用阶跃函数的直接数字实现(小于零 = -1,大于零 = +1)替换有机 sigmoid 传递函数的昂贵浮点实现。

在此处输入图像描述 大脑无法做到这一点,但反向传播可以!

这不仅线性且显着提高了单次学习迭代的性能,而且还减少了训练网络所需的学习迭代次数:支持学习本质上是数字化的证据。

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

于 2013-04-03T22:06:43.190 回答
1

纯粹基于代码检查,您的最内层循环必须计算对 3D 参数的引用,并且需要做很多工作。根据您的数组尺寸,您可能会遇到缓存问题,因为每次循环迭代都必须在内存中跳转。也许您可以重新排列维度,以便内部循环尝试访问比现在更接近的内存元素?

无论如何,在进行任何更改之前分析您的代码并查看真正的瓶颈在哪里。

于 2010-06-08T00:11:42.513 回答
1

我建议使用定点系统而不是浮点系统。在几乎所有使用 int 的处理器上都比 float 快。最简单的方法是将所有内容左移一定量(4 或 5 是好的起点)并将底部 4 位视为小数。

你最里面的循环正在做浮点数学,所以这可能会给你很大的推动力。

于 2010-06-08T00:14:44.240 回答
0

优化的关键是首先衡量时间花在哪里。调用 System.nanoTime() 包围算法的各个部分:

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

我猜想虽然使用 System.arraycopy() 会有所帮助,但您会在内部循环中找到真正的成本。

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

于 2010-06-08T00:24:15.967 回答