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可以!

這不僅可以線性和急劇提高單個學習迭代的性能,還可以減少訓練網絡所需的學習迭代次數:支持學習本身具有數字化的證據。

也支持計算機科學天生酷的論點。