Вопрос

$$f(x) = x anh(\log(1 + e^x))$$

Функцию (активация Миш) можно легко реализовать с использованием стабильного log1pexp без существенной потери точности.К сожалению, это требует больших вычислительных усилий.

Можно ли написать более прямую численно стабильную реализацию, которая будет работать быстрее?

Точность такая же хорошая, как x * std::tanh(std::log1p(std::exp(x))) было бы здорово.Строгих ограничений нет, но он должен быть достаточно точным для использования в нейронных сетях.

Распределение входных данных происходит от $[-\infty, \infty]$.Оно должно работать везде.

Это было полезно?

Решение

ОП указывает на конкретный выполнение принадлежащий mish функция активации для характеристик точности, поэтому мне пришлось сначала ее охарактеризовать.Эта реализация использует одинарную точность (float), стабильен и точен в положительной полуплоскости.В отрицательной полуплоскости, поскольку здесь используется logf вместо log1pf, относительная ошибка быстро возрастает $x o-\infty$.Потеря точности начинается примерно $-1$ и уже в $-16.6355324$ реализация ложно возвращает $0$, потому что $\exp(-16.6355324) = 2^{-24}$.

Той же точности и поведения можно добиться, используя простое математическое преобразование, исключающее $\mathrm{tahn}$, и учитывая, что графические процессоры обычно предлагают объединенное умножение-сложение (FMA), а также быстрое обратное преобразование, которое можно было бы использовать.Примерный код CUDA выглядит следующим образом:

__device__ float my_mishf (float x)
{
    float r;
    float e = expf (x);
    r = 1.0f / fmaf (fmaf (-0.5f, e, -1.0f), e, -1.0f);
    r = fmaf (r, x, x);
    return r;
}

Как и в случае с эталонной реализацией, на которую указывает ОП, она имеет превосходную точность в положительной полуплоскости, а в отрицательной полуплоскости ошибка быстро увеличивается, поэтому при $-16.6355324$ реализация ложно возвращает $0$.

Если есть желание решить эти проблемы точности, мы можем применить следующие наблюдения.Для достаточно малого $х$, $f(x) = x \exp(x)$ с точностью до чисел с плавающей запятой.Для float вычисление это справедливо для $х < -15$.За интервал $[-15,-1]$, мы можем использовать рациональное приближение $R(x)$ вычислить $f(x) := R(x)x\exp(x)$.Примерный код CUDA выглядит следующим образом:

__device__ float my_mishf (float x)
{
    float r;
    if (x >= -1.0f) {
        float e = expf (x);
        r = 1.0f / fmaf (fmaf (-0.5f, e, -1.0f), e, -1.0f);
        r = fmaf (r, x, x);
    } else {
        float eh = expf (0.5f * x);
        float p =        1.03628484e-3f;  //  0x1.0fa7e6p-10
        p = fmaf (p, x, -7.28869531e-3f); // -0x1.ddac04p-8
        p = fmaf (p, x,  3.47027816e-2f); //  0x1.1c4902p-5
        p = fmaf (p, x, -3.54762226e-1f); // -0x1.6b46cap-2
        p = fmaf (p, x,  8.58785570e-1f); //  0x1.b7b2bep-1
        p = fmaf (p, x, -1.38065982e+0f); // -0x1.6172ecp+0
        p = fmaf (p, x,  5.97694337e-1f); //  0x1.3204fep-1
        float q =        1.03527203e-3f;  //  0x1.0f63eep-10
        q = fmaf (q, x, -7.35638570e-3f); // -0x1.e21bacp-8
        q = fmaf (q, x,  3.28683928e-2f); //  0x1.0d4204p-5
        q = fmaf (q, x, -3.79927397e-1f); // -0x1.850bb0p-2 
        q = fmaf (q, x,  6.86127126e-1f); //  0x1.5f4c0ep-1
        q = fmaf (q, x, -1.81509292e+0f); // -0x1.d0a9eep+0
        q = fmaf (q, x,  1.00000000e+0f); //  0x1.000000p+0
        r = (1.0f / q) * p;
        if (x < -15.0f) r = 1.0f;
        r = r * x * eh * eh;
    }
    return r;
}

К сожалению, это точное решение достигается за счет значительного падения производительности.Если кто-то готов принять пониженную точность, сохраняя при этом плавно затухающий левый хвост, то следующая схема интерполяции, опять же основанная на $f(x) \приблизительно x\exp(x)$, восстанавливает большую часть производительности:

__device__ float my_mishf (float x)
{
    float r;
    float e = expf (x);
    if (x >= -6.0625f) {
        r = 1.0f / fmaf (fmaf (-0.5f, e, -1.0f), e, -1.0f);
        r = fmaf (r, x, x);
    } else {
        r = fmaf (-0.5f, e, 1.0f);
        r = r * x * e;
    }
    return r;
}

В качестве повышения производительности конкретной машины, expf() может быть заменено встроенным устройством __expf().

Другие советы

С помощью некоторых алгебраических манипуляций (как указано в ответе @orlp) мы можем вывести следующее:

$$f(x) = x anh(\log(1+e^x)) ag{1}$$ $$ = x\frac{(1+e^x)^2 - 1}{(1+ e^x)^2 + 1} = x\frac{e^{2x} + 2e ^ x}{e^{2x} + 2e ^ x + 2} ag{2}$$ $$ = x - \frac{2x}{(1 + e^ x)^2 + 1} ag{3}$$

Выражение $(3)$ отлично работает, когда $x$ является отрицательным с очень небольшой потерей точности.Выражение $(2)$ не подходит для больших значений $x$ поскольку термины будут расходиться как в числителе, так и в знаменателе.

Функция $(1)$ асимптотически достигает нуля как $x o-\infty$.Теперь , когда $x$ становится больше по величине, выражение $(3)$ пострадает от катастрофической отмены бронирования:два больших слагаемых, отменяющих друг друга, дают действительно небольшое число.Выражение $(2)$ больше подходит в этом диапазоне.

Это работает довольно хорошо до тех пор, пока $-18$ и за пределами которого вы теряете множество значимых цифр.

Давайте подробнее рассмотрим эту функцию и попытаемся приблизить $f(x)$ как $x o-\infty$.

$$f(x) = x \ frac{e^ {2x} + 2e ^ x}{e^{2x} + 2e ^ x + 2}$$

Тот Самый $e^{2x}$ будет на порядки меньше, чем $e^x$. $e^x$ будет на порядки меньше, чем $1$.Используя эти два факта, мы можем приблизить $f(x)$ Для:

$ f (x) \приблизительно x\frac{e^ x}{e ^ x+1}\приблизительно xe ^ x $

Результат:

$f(x) \приблизительно \begin{падежи} xe^x, & ext{если $x \le -18$} \\ x\frac{e ^ {2x} + 2e ^ x}{e ^{2x} + 2e ^ x + 2} & ext{если $-18 \lt x \le -0,6 $} \\ x - \frac{2x}{(1 + e ^ x) ^2 + 1}, & ext{в противном случае} \end{случаи} $

Быстрая реализация CUDA:

__device__ float mish(float x)
{
    auto e = __expf(x);
    if (x <= -18.0f)
        return x * e;    

    auto n = e * e + 2 * e;
    if (x <= -0.6f)
        return x * __fdividef(n, n + 2);

    return x - 2 * __fdividef(x, n + 2);
}

Редактировать:

Еще более быстрая и точная версия:

$f(x) \приблизительно \начало{случаи} x\frac{e^{2x} + 2e ^ x}{e ^{2x} + 2e ^ x + 2} & ext{$x \le -0.6$} \\ x - \frac{2x}{(1 + e ^ x) ^2 + 1}, & ext{в противном случае} \end{случаи} $

__device__ float mish(float x)
{
    auto e = __expf(value);
    auto n = e * e + 2 * e;
    if (value <= -0.6f)
        return value * __fdividef(n, n + 2);

    return value - 2 * __fdividef(value, n + 2);
}

Код: https://gist.github.com/YashasSamaga/8ad0cd3b30dbd0eb588c1f4c035db28c

$$\begin{массив}{c|c|c|c|} & ext{Time (float)} & ext{Time (float4)} & ext{L2 норма вектора ошибок} \\ \hline \текст {mish} & 1,49 мс & 1,39 мс & 2,4583e-05 \\hline ext {relu} & 1,47 мс & 1,39 мс & ext {N/A} \\ \hline \end {массив}$$

Нет необходимости выполнять логарифм.Если вы позволите $ p= 1+ \ exp (x) $ Тогда у нас есть $ f (x)= x \ cdot \ dfrac {p ^ 2-1} {p ^ 2 + 1} $ или, альтернативно $ f (x)= x - \ dfrac {2x} {p ^ 2 + 1} $ .

Мое впечатление состоит в том, что кто-то хотел умножить X путем функции f (x), которая проходит гладко от 0 до 1, и экспериментировал до тех пор, пока они не нашли выражения, используя элементарные функции, которые сделали это, без математической причины за выбором функций Отказ

После выбора параметра t, пусть $ p_t (x)= 1/2 + (3 / 4t) x - x ^ 3 / (4T ^ 3) $ , Тогда $ P_T (0)= 1/2 $ , $ p_t (t)= 1 $ , $ p_t (-t)= 0 $ и $ p_t '(t)= p_t' (- t)= 0 $ . Пусть g (x)= 0, если x <-t, 1, если x> +1 и $ p_t (x) $ если -t ≤ x ≤ + t. Это функция, которая плавно меняется от 0 до 1. Выберите другой параметр S, а вместо F (x) вычисляют X * G (x - S).

t= 3,0 и s= -0.3 соответствует данной функции вполне разумно и рассчитывается очень быстрее (что кажется важным). Это отличается конечно. Поскольку эта функция используется в качестве инструмента в какой-то проблеме, я бы хотел увидеть математическую причину, по которой оригинальная функция лучше .

Контекст здесь — это компьютерное зрение и функция активации для обучения нейронных сетей.

Скорее всего, этот код будет выполняться на графическом процессоре.Хотя производительность будет зависеть от распределения типичных входных данных, вообще говоря, важно избегать ветвей в коде графического процессора.Дивергенция деформации может значительно снизить производительность вашего кода.Например, Документация по набору инструментов CUDA говорит:

Примечание: Высокий приоритет:Избегайте разных путей выполнения в пределах одной деформации.Инструкции управления потоком (if, switch, do, for, while) могут существенно повлиять на пропускную способность инструкций, заставляя потоки одной и той же деформации расходиться;то есть следовать разным путям выполнения.Если это произойдет, разные пути выполнения должны выполняться отдельно;это увеличивает общее количество инструкций, выполняемых для этой деформации....Для ветвей, включающих всего несколько инструкций, расхождение деформации обычно приводит к незначительным потерям производительности.Например, компилятор может использовать предикацию, чтобы избежать реального ветвления.Вместо этого все инструкции планируются, но код условия или предикат для каждого потока определяет, какие потоки выполняют инструкции.Потоки с ложным предикатом не записывают результаты, а также не оценивают адреса и не читают операнды.

Две реализации без ветвей

ответ ОП имеет короткие ветки, поэтому в некоторых компиляторах может произойти предсказание ветвления.Еще я заметил, что вполне приемлемо вычислять экспоненту один раз за вызов.То есть я понимаю ответ ОП, говорящий, что один вызов экспоненты не является «дорогим» или «медленным».

В этом случае я бы предложил следующий простой код:

__device__ float mish(float x)
{
    float expx = __expf(x);
    return x / (1.0f + 2.0f / (expx * (2.0f + expx)));
}

У него нет ветвей, одна экспонента, одно умножение и два деления.Деление зачастую обходится дороже, чем умножение, поэтому я также опробовал этот код:

__device__ float mish(float x)
{
    float expx = __expf(x);
    float psi = expx * (2.0f + expx);
    return x * (psi / (2.0f + psi));
}

Здесь нет ветвей, одна экспонента, два умножения и одно деление.

Относительная ошибка

Я вычислил относительную точность log10 этих двух реализаций плюс ответ ОП.Я вычислил интервал (-100 100) с шагом 1/1024, затем вычислил текущий максимум для 51 значения (чтобы уменьшить визуальный беспорядок, но при этом создать правильное впечатление).В качестве примера достаточно вычисления первой реализации с двойной точностью.Экспонента имеет точность с точностью до одного ULP, а арифметических операций всего несколько;остальных битов более чем достаточно, чтобы сделать дилемму создателя стола маловероятной.Таким образом, мы, скорее всего, сможем вычислить правильно округленные опорные значения одинарной точности.

Log10 relative error

Зеленый:первая реализация.Красный:вторая реализация.Синий:Реализация ОП.Синий и красный перекрываются в большей части своего диапазона (слева около -20).

Примечание для ОП:вам нужно изменить значение отсечки больше -5, если вы хотите сохранить полную точность.

Производительность

Вам придется протестировать эти две реализации, чтобы увидеть, какая из них быстрее.Они должны быть как минимум такими же быстрыми, как OP, и я подозреваю, что они будут намного быстрее из-за отсутствия ветвей.Однако, если они недостаточно быстры для вас, вы можете сделать больше.

Важный вопрос:

Какое распределение типичных входных значений вы ожидаете увидеть?Будут ли значения равномерно распределены по всему диапазону, в котором функция эффективно вычислима?Или они почти все время будут группироваться около 0?Если да, то с какой дисперсией/разбросом?

Асимптотику можно улучшить.

Слева ОП использует x * expx с отсечкой -18.Это отсечение можно увеличить примерно до -15,5625 без потери точности.Затратив одно дополнительное умножение, вы можете использовать x * expx * (1.0f - 0.5f * expx) и порог около -4,875.Примечание:умножение на 0,5 можно оптимизировать до вычитания 1 из показателя степени, поэтому я здесь это не учитываю.

Справа можно ввести еще одну асимптотику.Если x > 8.75, просто return x.За немного больше затрат вы могли бы сделать x * (1.0f - 2.0f * __expf(-2.0f * x)) когда x > 6.0.

Интерполяция

Для центральной части диапазона (-4,875, 6,0) можно использовать таблицу интерполянтов.Если их диапазоны расположены на одинаковом расстоянии друг от друга, вы можете использовать одно деление для вычисления прямого индекса таблицы (без ветвления).Расчет такой таблицы потребует некоторых усилий, но в зависимости от ваших потребностей оно того стоит:несколько умножений и сложений мощь быть дешевле, чем экспоненциальный.Тем не менее, разработчики экспоненты в библиотеке, вероятно, потратили много времени и усилий на то, чтобы сделать свою экспоненту правильной и быстрой.Также функция «миш» не предоставляет никаких возможностей для уменьшения дальности.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с cs.stackexchange
scroll top