优化我的Backprop ANN

时间:2011-04-19 08:44:58

标签: c++ optimization const

分析我的反向传播算法之后,我了解到它负责占用我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/。 对于那些感兴趣的人,可以在这里找到最小编译所需的代码:Backprop.cpp

4 个答案:

答案 0 :(得分:7)

如果您想训练/使用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

更好的是当然直接使用编译器内在函数。

答案 1 :(得分:2)

你想在这里消除循环内的条件:

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

你可以通过更早地计算第零次迭代(i == 0的特殊情况)来消除这种情况。

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

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

这些尺寸有多大?如果非常大,可能会有一些缓存注意事项。例如。你不希望最后一个下标[k]刷新L1缓存。有时打破循环以一次处理较小范围的k索引可以帮助(strip mining)。

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

正如其他人提到的那样使用SIMD(SSE / AVX)可能是你可以找到最多收益的地方。您可以使用编译器intrinsics(链接到Visual Studio,但gcc支持相同的语法)或写入程序集(内联或其他)。如您所述,跨多个核心进行扩展是另一个方向。 OpenMP可以帮助你在没有太多痛苦的情况下做到这一点。

有时从代码中生成带注释的assembly listing以尝试查看编译器在哪些方面做得不是很好。

This是优化的一般资源。

答案 2 :(得分:1)

我不确定编译器是否可以在您的情况下对其进行优化,但是将inverse_momentum * (learn_rate * errors[i][j])输出到外部的变量以在较低的循环中循环“k”可能会降低CPU的负载。

顺便说一句,你正在分析发布二进制文件而不是调试程序,不是你。

答案 3 :(得分:1)

我不喜欢valarray,但我预感到这里有很多机会。

Blitz ++(提升)似乎在网络上有更好的光环,但我不知道:)

我自己开始研究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 [][][];

现在,从代码逻辑上讲,至少第一个(rank-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

(我希望权重和增量必须成为gslices因为AFAICT它们是三维的

杂项(对齐,布局)

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

从这个角度来看,重要的是要意识到等级的“最优”排序最终仅取决于访问模式(因此,再次进行轮廓和检查)。

此外,优化的机会可能会因不幸的内存对齐而受到阻碍[1]。在这种情况下,您可能希望将(val)数组等级的圆形等级维度的顺序切换为最接近的2的幂(或实际上,或32个字节的倍数)。

如果这一切确实产生了很大的影响(配置文件/先检查生成的代码!)我会想象支持

  • Blitz ++或boost可能包含帮助器(分配器?)来强制对齐
  • 您的编译器将具有属性(对齐和/或限制类型)以告诉他们可以为输入指针采用这些对齐

相依:

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

inverse_momentum * (learn_rate * ???) 
你可以

(inverse_momentum * learn_rate) * ???

并预先计算第一个子产品。但是,由于它是以这种方式明确地订购的,我猜这会导致更多的噪音。

[1]免责声明:我实际上没有对此做过任何分析,我只是把它扔到那里所以你不要错过'虽然联合'(对于Engrish来说是怎样的)