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%以内)。すべての助けをありがとう!forすべての提案は役に立ちましたが、正しい答えとしてマークできるのは1つだけなので、配列の最適化を提案することと、ループヘッダーを事前に計算する唯一のことの両方について、@Durandalに渡します。

4

8 に答える 8

8

いくつかのヒント。

  • 最も内側のループで、CPUキャッシュをどのようにトラバースしているかを考え、最も外側の配列に順番にアクセスするようにマトリックスを再配置します。これにより、あちこちをジャンプするのではなく、順番にキャッシュにアクセスできるようになります。キャッシュヒットは、キャッシュミスよりも2桁速くなる可能性があります。たとえば、fWeightsを再構築して、次のようにアクセスできるようにします。

アクティベーション+=NeuronOutput [layer-1] [inputNeuron] * fWeights [layer-1] [neuron] [inputNeuron];

  • ループの外側(1回)で実行できる作業をループの内側(毎回)で実行しないでください。これをローカル変数に配置できる場合は、毎回[layer-1]ルックアップを実行しないでください。IDEはこれを簡単にリファクタリングできるはずです。

  • Javaの多次元配列は、Cの場合ほど効率的ではありません。実際には、1次元配列の複数のレイヤーです。コードを再構築して、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では、これによりパフォーマンスが向上するはずです。試すことができるもう1つのことは、次のように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にはインライン化に非常に厳しい制限があり、関数が大きくなる可能性があります)。ループインデックスをゼロに対してテストする方がローカル変数に対してチェックするよりも少し安価であるため(もちろん結果を変更しない場合)、ループを逆方向に実行する方が少し速くなる可能性があります(最も内側のループは再び有力な候補ですが、しないでください)フロートの追加a+b+cは潜在的にa+c + b)と同じではないため、出力はすべての場合で100%同一であると予想します。

于 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

高価な浮動小数点シグモイド伝達関数を整数ステップ伝達関数に置き換えます。

シグモイド伝達関数は、有機アナログシナプス学習のモデルであり、ステップ関数のモデルのようです。

これの歴史的な先例は、ヒントンが実際のシナプスに関する認知科学理論の第一原理から直接バックプロップアルゴリズムを設計したことです。これは、実際のアナログ測定に基づいており、シグモイドであることが判明しました。

しかし、シグモイド伝達関数はデジタルステップ関数の有機モデルのようであり、もちろん有機的に直接実装することはできません。

モデルをモデル化するのではなく、有機シグモイド伝達関数の高価な浮動小数点実装を、ステップ関数の直接デジタル実装(ゼロ未満= -1、ゼロより大きい= +1)に置き換えます。

ここに画像の説明を入力してください 脳はこれを行うことはできませんが、バックプロパゲーションはできます!

これにより、単一の学習反復のパフォーマンスが直線的かつ大幅に向上するだけでなく、ネットワークのトレーニングに必要な学習反復の数も削減されます。これは、学習が本質的にデジタルであるという証拠を裏付けるものです。

コンピュータサイエンスは本質的にクールであるという議論も支持しています。

于 2013-04-03T22:06:43.190 に答える
1

純粋にコード検査に基づいて、最も内側のループは3次元パラメーターへの参照を計算する必要があり、その多くが実行されます。配列の次元によっては、ループを繰り返すたびにメモリをジャンプする必要があるため、キャッシュの問題が発生する可能性があります。おそらく、寸法を再配置して、内側のループが現在よりも互いに近いメモリ要素にアクセスしようとすることができますか?

いずれの場合も、変更を加える前にコードのプロファイルを作成し、実際のボトルネックがどこにあるかを確認してください。

于 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 に答える