Оптимизация моего Backprop ANN - PullRequest
15 голосов
/ 19 апреля 2011

После профилирования моего алгоритма обратного распространения я понял, что он занимает 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 ]

7 голосов
/ 19 апреля 2011

Вы не можете избежать алгоритма O (n ^ 2), если хотите обучить / использовать NN. Но он идеально подходит для векторной арифметики. Например, при умном использовании SSE или AVX вы можете обрабатывать нейроны по 4 или 8 блоков и использовать умножение-сложение вместо двух отдельных инструкций.

Если вы используете современный компилятор, тщательно переформулируете алгоритм и используете правильные переключатели, вы можете даже заставить компилятор автоматически векторизовать циклы для вас, но ваш пробег может отличаться.

Для gcc, автовекторизация активируется с помощью -O3 или -ftree-vectorize. Конечно, вам нужен процессор, способный работать с вектором, что-то вроде -march = core2 -mssse4.1 или подобное, в зависимости от целевого процессора. Если вы используете -ftree-vectorizer-verbose = 2, вы получите подробные объяснения, почему и где петли не были векторизованы. Посмотрите на http://gcc.gnu.org/projects/tree-ssa/vectorization.html.

Лучше, конечно, напрямую использовать встроенные функции компилятора.

2 голосов
/ 23 апреля 2011

Вы хотите исключить условное изнутри вашего цикла здесь:

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, так что это вектор vector вектора?Ваши данные не будут смежными (за исключением того, что каждый вектор является смежным).Рассмотрите возможность использования массивов в стиле C.

Насколько велики эти размеры?Могут быть некоторые соображения кэширования, если они очень велики.Например, вы не хотите, чтобы последний индекс [k] очищал кэш L1.Иногда может помочь разрыв цикла для обработки меньшего диапазона k индексов за один раз ( добыча полосы ).

Вы также можете поэкспериментировать с развертыванием ваших внутренних циклов aнапример, попробуйте выполнить 4 или 8 операций внутри цикла.Увеличьте на 4/8 соответственно и обработайте любой остаток в другом цикле.Компилятор, возможно, уже делает это.

Как уже упоминали другие, использование SIMD (SSE / AVX), вероятно, является тем, где вы можете найти наибольшую выгоду.Вы можете использовать компилятор intrinsics (ссылка на Visual Studio, но gcc поддерживает тот же синтаксис) или написать в сборке (встроенный или другой).Как вы упомянули, масштабирование между несколькими ядрами - это другое направление. OpenMP может помочь вам сделать это без особых усилий.

Иногда бывает полезно сгенерировать аннотированный листинг сборки из вашего кода, чтобы попытаться увидеть, где находится компиляторне делает такой большой работы.

Этот является отличным общим ресурсом об оптимизации.

1 голос
/ 21 апреля 2011

Я не люблю valarray, но у меня есть догадка, что у тех, кто здесь есть, есть довольно широкие возможности.

Blitz ++ (boost), кажется, имеет лучшую ауру в сети, но я нене знаю:)

Я сам начинал работать на 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) индексы для массивов определяются четырьмя пропущенными константами.Если бы эти константы могли быть известны во время компиляции, это сделало бы параметры шаблона класса с большим значением:

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 они 3-мерные )

Разное (выравнивание, расположение)

Я понял, что, вероятно, не будет большого выигрыша, если ранги (измерения) массивов не будут оптимально упорядочены (например, первый ранг в valarray относительно мал, скажем, 8).Это может препятствовать векторизации, потому что участвующие элементы могут быть разбросаны в памяти, где, я полагаю, оптимизация требует, чтобы они были смежными.

В этом свете важно понимать, что «оптимальное» упорядочение рангов в конечном счете зависиттолько на шаблонах доступа (так что профилируйте и проверьте снова).

Кроме того, возможность оптимизации может быть затруднена неудачным выравниванием памяти [1].В этом свете вам может потребоваться переключить порядок рангов массива (val) и измерений круглого ранга на ближайшие степени 2 (или более того, например, кратные 32 байта).

Если все это действительно оказывает большое влияние (сначала профиль / проверять сгенерированный код!), Я бы предположил поддержку

  • , что Blitz ++ или boost могут содержать помощников (распределителей?) Для обеспечения выравнивания
  • ваш компилятор будет иметь атрибуты ( align и / или restrict вида), чтобы сообщить им, что они могут принять эти выравнивания для входных указателей

Не связано:

Если порядок исполнения не является критическим (т.е. относительные порядки величин факторов очень похожи), вместо

inverse_momentum * (learn_rate * ???) 

вы можете взять

(inverse_momentum * learn_rate) * ???

и предварительно рассчитайте первый субпродукт.Тем не менее, из-за того, что он явно упорядочен таким образом, я предполагаю, что это приведет к большему количеству шума.просто выбросить его туда, чтобы не пропустить «хоть и совместное» (как это для английского)

1 голос
/ 19 апреля 2011

Я не уверен, что компилятор может оптимизировать его в вашем случае, но выход inverse_momentum * (learn_rate * errors[i][j]) в переменную снаружи для цикла "k" в нижних циклах может снизить нагрузку на процессор.

Кстати, вы профилируете бинарный релиз, а не отладочный, не так ли?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...