如何提高性能而不去并行我的反向人工神经网络

How to improve performance without going parallel for my backprop ANN

本文关键字:我的 人工神经网络 并行 何提 高性能      更新时间:2023-10-16

分析我的反向传播算法后,我了解到它占用了我60%的计算时间。在我开始寻找并行的替代方案之前,我想看看我还能做些什么。

配置activate(const double input[])函数只占用~5%的时间。gradient(const double input)功能实现如下:

inline double gradient(const double input) { return (1 - (input * input)); }

所讨论的训练函数:

void train(const vector<double>& data, const vector<double>& desired, const double learn_rate, const double momentum) {
        this->activate(data);
        this->calculate_error(desired);
        // adjust weights for layers
        const auto n_layers = this->config.size();
        const auto adjustment = (1 - momentum) * learn_rate;
        for (size_t i = 1; i < n_layers; ++i) {
            const auto& inputs = i - 1 > 0 ? this->outputs[i - 1] : data;
            const auto n_inputs = this->config[i - 1];
            const auto n_neurons = this->config[i];
            for (auto j = 0; j < n_neurons; ++j) {
                const auto adjusted_error = adjustment * this->errors[i][j];
                for (auto k = 0; k < n_inputs; ++k) {
                    const auto delta = adjusted_error * inputs[k] + (momentum * this->deltas[i][j][k]);
                    this->deltas[i][j][k] = delta;
                    this->weights[i][j][k] += delta;
                }
                const auto delta = adjusted_error * this->bias + (momentum * this->deltas[i][j][n_inputs]);
                this->deltas[i][j][n_inputs] = delta;
                this->weights[i][j][n_inputs] += delta;
            }
        }
    }
}

这个问题可能更适合https://codereview.stackexchange.com/.

如果你想训练/使用NN,你不能避免O(n^2)算法。但它非常适合矢量运算。例如,通过巧妙地使用SSE或AVX,你可以处理4或8个神经元块,并使用乘法-加法而不是两个单独的指令。

如果您使用现代编译器并仔细地重新制定算法并使用正确的开关,您甚至可以让编译器为您自动向量化循环,但您的里程可能会有所不同。

对于gcc,使用-O3或-ftree-vectorize激活自动矢量化。当然,您需要一个具有矢量功能的cpu,例如-march=core2 -mssse4.1或类似的内容,具体取决于目标cpu。如果您使用-ftree-vectorizer-verbose=2,您将得到详细的解释,为什么以及在哪里没有对循环进行矢量化。看一看http://gcc.gnu.org/projects/tree-ssa/vectorization.html

当然是直接使用编译器的内部函数。

你想从循环中消除条件:

const double lower_layer_output = i > 0 ? outputs[lower_layer][k] : input[k]; // input layer semantics

您可以通过提前计算第0次迭代(i==0的特殊情况)来消除此条件。

        deltas[i][j][k] = delta;
        weights[i][j][k] += delta;

你提到使用std::vector,所以这是vector的vector的vector?您的数据而不是将是连续的(除非每个向量是连续的)。考虑使用C风格的数组。

这些尺寸有多大?如果非常大,可能需要考虑一些缓存问题。例如,你不希望最后一个下标[k]刷新L1缓存。有时打破循环,一次处理较小范围的k个索引会有所帮助(条带开采)。

你也可以尝试稍微展开你的内部循环,例如尝试在循环内执行4或8个操作。分别增加4/8,并在另一个循环中处理任何余数。编译器可能已经这样做了。

正如其他人提到的,使用SIMD (SSE/AVX)可能是您可以获得最大收益的地方。你既可以使用编译器的内在特性(链接到Visual Studio,但gcc支持相同的语法),也可以用汇编编写(内联或其他)。正如你提到的,跨多核扩展是另一个方向。OpenMP可以帮你轻松地做到这一点。

有时候从你的代码中生成一个带注释的汇编清单是很有用的,这样可以尝试看看编译器在哪些地方做得不好。

这是一个很好的关于优化的通用资源。

我不确定编译器是否可以在您的情况下优化它,但是在较低的循环中将inverse_momentum * (learn_rate * errors[i][j])输出到循环"k"之外的变量可能会减少CPU上的负载。

顺便说一句,你是在分析一个发布二进制文件,而不是调试二进制文件,对吗?

我不喜欢valarray,但我有一种预感,这里有相当多的机会。

闪电战++(增强)似乎有一个更好的光环围绕网络,但我不知道:)

我自己开始做PoC,但是有太多的代码缺失

void activate(const double input[]) { /* ??? */ }
const unsigned int n_layers_ns;
const unsigned int n_layers;
const unsigned int output_layer_s;
const unsigned int output_layer;
T/*double?*/ bias = 1/*.0f?/;
const unsigned int config[];
double outputs[][];
double errors [][];
double weights[][][];
double deltas [][][];

现在,从逻辑上来看,至少数组的第一个(第0个)索引是由4个缺失的常量定义的。如果可以在编译时知道这些常量,它们将成为很有价值的类模板参数:

template <unsigned int n_layers_ns = 2,
          unsigned int n_layers = 3>
struct Backprop {
    void train(const double input[], const double desired[], const double learn_rate, const double momentum);
    void activate(const double input[]) { }
    enum _statically_known
    {
        output_layer = n_layers_ns - 1,
        output_layer_s = n_layers - 1, // output_layer with input layer semantics (for config use only)
        n_hidden_layers = output_layer - 1,
    };
    static const double bias = 1.0f;
    const unsigned int config[];
    double outputs[3][50];      // if these dimensions could be statically known, 
    double errors[3][50];       //        slap them in valarrays and 
    double weights[3][50][50];  //        see what the compiler does with that!
    double deltas[3][50][50];   // 
};
template <unsigned int n_layers_ns,
          unsigned int n_layers>
void Backprop<n_layers_ns, n_layers>::train(const double input[], const double desired[], const double learn_rate, const double momentum) {
    activate(input);
    // calculated constants
    const double inverse_momentum = 1.0 - momentum;
    const unsigned int n_outputs = config[output_layer_s];
    // calculate error for output layer
    const double *output_layer_input = output_layer > 0 ? outputs[output_layer] : input; // input layer semantics
    for (unsigned int j = 0; j < n_outputs; ++j) {
        //errors[output_layer][j] = f'(outputs[output_layer][j]) * (desired[j] - outputs[output_layer][j]);
        errors[output_layer][j] = gradient(output_layer_input[j]) * (desired[j] - output_layer_input[j]);
    }
    [... snip ...]

注意我是如何对第一个循环中的语句重新排序的,以使循环变得微不足道。现在,我可以想象最后几行变成

// calculate error for output layer
const std::valarray<double> output_layer_input = output_layer>0? outputs[output_layer] : input; // input layer semantics
errors[output_layer] = output_layer_input.apply(&gradient) * (desired - output_layer_input);

这将需要为输入设置适当的(g)片。我想不出这些要怎么标注尺寸。问题的关键在于,只要这些切片尺寸可以由编译器静态地确定,您就有可能节省大量的时间,因为编译器可以在FPU堆栈或使用SSE4指令集上将它们优化为矢量化操作。我想你会像这样声明你的输出:

std::valarray<double> rawoutput(/*capacity?*/);
std::valarray<double> outputs = rawoutput[std::slice(0, n_outputs, n_layers)]; // guesswork

(我希望权重和δ必须变成gslice,因为它们是三维的)

杂项(对齐、布局)

我意识到,如果数组的秩(维度)不是最优排序(例如,valarray中的第一个秩相对较小,例如8),可能不会有太大的增益。这可能会妨碍向量化,因为参与的元素可能分散在内存中,我认为优化需要它们相邻。

从这个角度来看,重要的是要认识到,排名的"最佳"排序最终仅取决于访问模式(因此,再次配置文件和检查)。

此外,优化的机会可能会受到不幸的内存对齐[1]的阻碍。在这种情况下,您可能希望将(val)数组的排序顺序转换为最接近的2的幂(或者更实际地说,是32字节的倍数)。

如果所有这些确实产生了很大的影响(首先配置文件/检查生成的代码!)我想支持

  • Blitz++或boost可能包含帮助器(分配器?)来强制对齐
  • 你的编译器将有属性(align和/或restrict类型)告诉它们它们可以假设输入指针的这些对齐方式
无关的

:

如果执行顺序不重要(即因子的相对数量级非常相似),而不是

inverse_momentum * (learn_rate * ???) 

你可以取

(inverse_momentum * learn_rate) * ???

并预先计算第一个子积。然而,从它明确地以这种方式排序的事实来看,我猜这会导致更多的噪音。

[1]免责声明:我实际上没有做任何分析,我只是把它放在那里,这样你就不会错过'though joint'(英语怎么说)