Я пытаюсь рассчитать log(a + b)
, учитывая log(a)
и log(b)
. Проблема в том, что log(a)
и log(b)
настолько отрицательны, что, когда я пытаюсь вычислить сами a
и b
, они переполняются, и я получаю log(0)
, который не определен.
Для log(a * b)
и log(a / b)
это не проблема, так как log(a * b) = log(a) + log(b)
и log(a / b) = log(a) - log(b)
. Есть ли аналогичный метод для вычисления log(a + b)
без необходимости самих a
и b
, избегая потери значимости?
Короче говоря, используйте следующее выражение:
fmax(la, lb) + log1p(exp(-fabs(lb - la)))
где я использую la
и lb
в качестве переменных, хранящих log(a)
и log(b)
соответственно, а имена функций взяты из языка C math.h
. Большинство других языков также будут иметь эти функции, но они могут называться по-другому, например. abs
и max
вместо fabs
и fmax
. (Примечание: это соглашения, которые я буду использовать в этом ответе.)
Если вы хотите получить более подробную информацию о том, откуда взялось вышеизложенное, как сложить более двух чисел и как вычесть, продолжайте читать.
Нет такого простого правила, как правила умножения и деления, но есть математическое тождество, которое поможет:log(a + b) = log(a) + log(1 + b/a)
Мы можем немного поиграть с этим идентификатором, чтобы получить следующее:
log(a + b) = log(a) + log(1 + b/a)
= log(a) + log(1 + exp(log(b) - log(a)))
= la + log(1 + exp(lb - la))
Здесь еще есть проблема. Если la
намного больше, чем lb
, у вас будет 1 + 0.000...000something
внутри log
. В мантиссе с плавающей запятой недостаточно цифр для хранения something
, поэтому вы просто получите log(1)
, полностью потеряв lb
.
К счастью, в стандартной библиотеке большинства языков программирования есть функция для решения именно этой задачи, log1p
, которая вычисляет логарифм единицы плюс ее аргумент. То есть log1p(x)
возвращает log(1 + x)
, но точно для очень маленьких x
.
Итак, теперь у нас есть:
log(a + b) = la + log1p(exp(lb - la))
Мы почти на месте. Есть еще одна вещь, которую следует учитывать. В общем, вы хотите, чтобы la
было больше, чем lb
. Это не всегда будет иметь значение, но иногда это даст вам дополнительную точность.* Если разница между lb
и la
действительно велика, это убережет вас от переполнения в exp(lb - la)
. В самом крайнем случае расчет работает, когда lb
равно отрицательной бесконечности (т.е. b
равно 0), но не когда la
равно.
Иногда вы будете знать, какой из них больше, и вы можете просто использовать его как la
. Но когда это может быть любой из них, вы можете использовать максимальное и абсолютное значение, чтобы обойти это:
log(a + b) = fmax(la, lb) + log1p(exp(-fabs(lb - la)))
Если вам нужно взять сумму более чем двух чисел, мы можем получить расширенную версию тождества выше:
log(a[0] + a[1] + a[2] + ...)
= log(a[0] * (a[0]/a[0] + a[1]/a[0] + a[2]/a[0] + ...))
= log(a[0]) + log(a[0]/a[0] + a[1]/a[0] + a[2]/a[0] + ...)
= la[0] + log(1 + exp(la[1]-la[0]) + exp(la[2]-la[0]) + ...)
Мы захотим использовать те же приемы, что и при сложении двух чисел. Таким образом, мы получаем наиболее точный ответ и максимально избегаем переполнения и недополнения. Во-первых, log1p
:
log(a[0] + a[1] + a[2] + ...)
= la[0] + log1p(exp(la[1]-la[0]) + exp(la[2]-la[0]) + ...)
Другое соображение заключается в том, какой операнд вытащить перед log1p
. До сих пор я использовал la[0]
для демонстрации, но вы хотите использовать тот, который лучше. Это по тем же причинам, по которым мы хотели la > lb
при добавлении двух чисел. Например, если бы la[1]
было наибольшим, вы бы сделали такой расчет:
log(a[0] + a[1] + a[2] + ...)
= la[1] + log1p(exp(la[0]-la[1]) + exp(la[2]-la[1]) + ...)
Поместив это в правильный код, он будет выглядеть примерно так (это C, но он должен хорошо переводиться на другие языки):
double log_sum(double la[], int num_elements)
{
// Assume index_of_max() finds the maximum element
// in the array and returns its index
int idx = index_of_max(la, num_elements);
double sum_exp = 0;
for (int i = 0; i < num_elements; i++) {
if (i == idx) {
continue;
}
sum_exp += exp(la[i] - la[idx]);
}
return la[idx] + log1p(sum_exp);
}
Это не было частью вопроса, но, тем не менее, это может быть полезно: вычитание в пространстве журнала может быть выполнено аналогичным образом. Основная формула такова:
log(a - b) = la + log(1 - exp(lb - la))
Обратите внимание, что это все еще предполагает, что la
больше, чем lb
, но для вычитания это еще важнее. Если la
меньше lb
, вы логарифмируете отрицательное число!
Как и в случае сложения, у этого есть проблема с точностью, которую можно исправить с помощью специализированных функций, но, как оказалось, есть два способа. Один использует ту же функцию log1p
, что и выше, но другой использует expm1
, где expm1(x)
возвращает exp(x) - 1
. Вот оба способа:
log(a - b) = la + log1p(-exp(lb - la))
log(a - b) = la + log(-expm1(lb - la))
Какой из них вы должны использовать, зависит от значения -(lb - la)
. Первый более точен, когда -(lb - la)
больше примерно 0,693 (т. е. log(2)
), а второй более точен, когда он меньше. Подробнее о том, почему это так и откуда взялось log(2)
, см. в этой заметке из проекта R, в которой оцениваются два метода.
Конечный результат будет выглядеть так:
(lb - la < -0.693) ? la + log1p(-exp(lb - la)) : la + log(-expm1(lb - la))
или в форме функции:
double log_diff(double la, double lb)
{
if (lb - la < -0.693) {
return la + log1p(-exp(lb - la));
} else {
return la + log(-expm1(lb - la));
}
}
* Есть что-то вроде приятного места для этого. Когда разница между la
и lb
невелика, ответ будет точным в любом случае. Когда разница слишком велика, результат всегда будет равен большему из двух, поскольку числа с плавающей запятой не обладают достаточной точностью. Но когда разница в самый раз, вы получите лучшую точность, когда la
больше.
Предположим без ограничения общности, что b<a.
log(a+b) = log(a) + log(1 + b/a)
= log(a) + b/a - 1/2(b/a)^2 + 1/3(b/a)^3 etc.
Члены, включающие b / a, могут быть вычислены в терминах известных величин, поскольку
b/a = exp(log(b) - log(a))
Если при вычислении b/a происходит потеря значимости, то log(a+b) = log(a) с машинной точностью.
Какой-то конкретный язык? Некоторые языки или языковые библиотеки имеют встроенную поддержку для этого, например logaddexp NumPy.