Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ

в 14:42, , рубрики: autograd, python, pytorch, бесконечно малые, градиенты, дифференцирование, дуальные числа, математика, машинное обучение, нестандартный анализ

TL;DR

Когда вы пишете loss.backward(), ваш autograd делает то, что 200 лет считалось математической ересью: оперирует бесконечно малыми как настоящими числами. В 1960 году Абрахам Робинсон формализовал эту «ересь» в виде нестандартного анализа. Forward-mode автодифференцирование, на котором держатся JAX, PyTorch и пол-индустрии — это его обрезанная версия. В этой статье разберём гиперреалы и монады, реализуем дуальные числа в коде.

Проблема, о которой не говорят

Откройте любой учебник термодинамики. Найдите там первое начало:

dU=δQ−δA

Один значок прямой, другой — кривой. Спросите автора учебника, чем δQ отличается от dU — вам начнут объяснять про «полный» и «неполный» дифференциал. Спросите математика-аналитика, что вообще такое dx — он начнёт рассказывать про предельные переходы.

Проблема в том, что с точки зрения стандартного матанализа dU и δQ — это один и тот же объект. Оба определяются через предел. И тот, и другой стремится к нулю. Если у нас в руках только аппарат пределов, мы не можем сказать, чем интегрирование первого по замкнутому контуру отличается от интегрирования второго.

Физик это интуитивно понимает: δQ — маленький кусочек тепла, зависящий от пути. dU — приращение функции состояния, не зависящий. Но математически в стандартном анализе разница затаскивается в форму записи и определение интеграла, а не сидит в самих объектах. Дифференциалы как объекты в обычном анализе попросту отсутствуют — только пределы.

Краткая история запрещённой идеи

Лейбниц в 1684 году ввёл в анализ бесконечно малые — числа, которые больше нуля, но меньше любого положительного действительного. Производную он определял буквально: dy/dx, отношение двух бесконечно малых. Ньютон делал что-то похожее, кстати говоря.

Это работало. С помощью этого аппарата Эйлер посчитал такое, что до сих пор переоткрывают. Но был один нюанс: бесконечно малые непонятно как определить. Если ε > 0, но ε < r для любого положительного действительного r — то что это вообще такое за объект?

Беркли накинулся с философской критикой («призраки исчезнувших величин»). В XIX веке Коши, а потом более строго Вейерштрасс заменили бесконечно малые на пределы и (ε, δ)-определения. Получилось строго. Получилось тяжело. Получилось неудобно для физиков. Бесконечно малые формально из математики изгнали — точно так же, как когда-то изгоняли ноль и отрицательные числа.

Так оно и существовало до 1960 года.

В 1960-м Абрахам Робинсон опубликовал работу, в которой строго, в рамках теории моделей, построил расширение действительных чисел — гиперреалы *R — содержащее бесконечно большие и бесконечно малые числа как полноправные элементы поля. Бесконечно малые перестали быть «недочислами» и превратились в обычные числа, с которыми можно делать всё то же, что и с обычными.

Это направление называется нестандартным анализом. В России его очень развивала новосибирская школа Кутателадзе — серия монографий, заметная теоретическая глубина, своё видение того, как должна выглядеть аксиоматика расширения.

Что такое гиперреалы

Достаточно знать следующее.

*R — это поле, которое содержит обычные действительные числа R и при этом содержит:

  • бесконечно большие числа H, такие что H > n для любого натурального n;

  • бесконечно малые числа ε = 1/H, такие что 0 < |ε| < 1/n для любого натурального n.

Внутри действуют все обычные алгебраические правила. Можно складывать, умножать, делить. — тоже бесконечно малое. ε² — ещё меньше, чем ε. H + ε — бесконечно большое. И так далее.

Главное свойство: для любого конечного гиперреала x (то есть не бесконечно большого) существует единственное действительное число st(x), бесконечно близкое к нему. Это число называется стандартной частью x. Например, st(2 + ε) = 2, а st(3 + 5ε - 7ε²) = 3.

Вокруг каждой действительной точки клубится облако бесконечно близких к ней гиперреалов — это и есть монада этой точки в смысле Лейбница. Монада — это окрестность, состоящая из самой точки и всех бесконечно малых отклонений от неё. Топологическая интуиция тут принципиально другая: вместо «точки сколь угодно близко» появляется «целое облако точек, каждая из которых уже неотличима от центра».

Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ - 1

Аксиома Архимеда не работает — и это фича

Аксиома Архимеда: для любых двух положительных чисел a и b найдётся натуральное n, такое что n·a > b. В обычной математике это очевидно: складывай a достаточно много раз — рано или поздно перешагнёшь любое b.

В *R это не работает. Если ε бесконечно мало, а b = 1, то n·ε остаётся бесконечно малым для любого натурального n. Никогда не дотянет до единицы. Это не баг — это базовое свойство нестандартного континуума.

Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ - 2

И, кстати, это не такая уж экзотика. Когда в физике одновременно требуют dt → 0 и t → ∞, и волнуются, как именно эти пределы связаны — фактически вопрос про не-архимедову структуру.

Производная без пределов

В стандартном анализе:

Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ - 3

В нестандартном анализе:

Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ - 4

где ε — конкретное бесконечно малое, а st(…) берёт стандартную часть.

Никаких пределов. Никаких дельта-эпсилон

Для f(x) = x²:

(f(x + ε) - f(x)) / ε
  = ((x + ε)² - x²) / ε
  = (x² + 2xε + ε² - x²) / ε
  = (2xε + ε²) / ε
  = 2x + ε

st(2x + ε) = 2x. Готово. Никакого предела не брали — только алгебра в гиперреалах плюс взятие стандартной части. Производная играет роль объекта.

Дуальные числа: бесконечно малые, в которые поверил инженер

Полный нестандартный анализ — штука с серьёзной теоретико-модельной начинкой, и реализовать его на компьютере целиком невозможно. Но есть его обрезанная версия, которая прекрасно реализуется и которой пользуется буквально вся индустрия.

Это дуальные числа.

Дуальное число — это пара (a, b), которую обычно записывают как a + bε, где ε² = 0, но ε ≠ 0.

Это как комплексные числа, только вместо i² = -1 мы постулируем ε² = 0. Получается коммутативное кольцо (не поле, но нам и не надо).

Сложение и умножение работают по правилам:

(a + bε) + (c + dε) = (a + c) + (b + d)ε
(a + bε) · (c + dε) = ac + (ad + bc)ε + bd·ε²
                    = ac + (ad + bc)ε        ← ε² = 0

И теперь фокус. Подставим x + ε в любую гладкую функцию и формально разложим в ряд Тейлора:

f(x + ε) = f(x) + f'(x)·ε + ½ f''(x)·ε² + ...
         = f(x) + f'(x)·ε                   ← всё, что с ε², обнуляется

Вычисляем функцию в дуальной точке — на выходе получаем пару (значение, производная) бесплатно. Никаких разностных схем, ни потери точности из-за деления маленького на маленькое. Точная производная, посчитанная одним прогоном алгоритма.

Запрещённая математика в твоём autograd: бесконечно малые, дуальные числа и нестандартный анализ - 5

Реализация в NumPy

Делаем класс дуальных чисел:

import numpy as np

class Dual:
    """Дуальное число: real + dual·ε, где ε² = 0."""
    __slots__ = ("real", "dual")

    def __init__(self, real, dual=0.0):
        self.real = np.asarray(real, dtype=float)
        self.dual = np.asarray(dual, dtype=float)

    def __repr__(self):
        return f"Dual({self.real}, {self.dual}ε)"

    def __add__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(self.real + other.real, self.dual + other.dual)

    def __radd__(self, other):
        return self + other

    def __sub__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(self.real - other.real, self.dual - other.dual)

    def __rsub__(self, other):
        return Dual(other) - self

    def __mul__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(
            self.real * other.real,
            self.real * other.dual + self.dual * other.real,
        )

    def __rmul__(self, other):
        return self * other

    def __truediv__(self, other):
        other = other if isinstance(other, Dual) else Dual(other)
        return Dual(
            self.real / other.real,
            (self.dual * other.real - self.real * other.dual) / (other.real ** 2),
        )

    def __pow__(self, n):
        # n — обычное число
        return Dual(self.real ** n, n * self.real ** (n - 1) * self.dual)

    def __neg__(self):
        return Dual(-self.real, -self.dual)

Дальше — элементарные функции. Для каждой мы вручную записываем её разложение в дуальной арифметике, используя её производную:

def sin(x):
    if isinstance(x, Dual):
        return Dual(np.sin(x.real), np.cos(x.real) * x.dual)
    return np.sin(x)

def cos(x):
    if isinstance(x, Dual):
        return Dual(np.cos(x.real), -np.sin(x.real) * x.dual)
    return np.cos(x)

def exp(x):
    if isinstance(x, Dual):
        e = np.exp(x.real)
        return Dual(e, e * x.dual)
    return np.exp(x)

def log(x):
    if isinstance(x, Dual):
        return Dual(np.log(x.real), x.dual / x.real)
    return np.log(x)

И обёртка для вычисления производной:

def derivative(f, x):
    """Производная f в точке x — одним прогоном."""
    return f(Dual(x, 1.0)).dual

Тестируем на чём-нибудь нетривиальном:

def f(x):
    return sin(x ** 2) * exp(-x) + log(1 + x ** 2)

x0 = 1.3
print("f'(1.3) через дуальные числа:    ", derivative(f, x0))

Сверяем с центральной разностью:

h = 1e-6
fd = (f(x0 + h) - f(x0 - h)) / (2 * h)
print("f'(1.3) через центральную разность:", fd)

Числа совпадут до плавающей точности. Только дуальная версия посчитала точно (до ошибок округления самой арифметики), а центральная разность — приближённо, с ошибкой O(h²) плюс чувствительностью к выбору h.

Где это реально полезно за пределами «вау-факта»

Несколько мест, где гиперреальная (или дуальная) интуиция работает на практике.

Численные методы. Когда вы сравниваете центральную разность с дуальными числами, они дают одинаковый ответ при сопоставимой стоимости — но разностная схема ломается на функциях, чувствительных к ошибкам округления (вычитание близких чисел в числителе). Дуальные не ломаются. Это используют в CFD-симуляциях для построения якобиана и в оптимизации, где нужны очень точные градиенты.

Чувствительностный анализ. В моделях с большим числом параметров иногда хочется получить одновременно значение функции и её производную по одному выбранному направлению — без полного градиента. Forward-mode AD — это ровно оно, и он дешевле обратного режима, когда выходов много, а входов мало.

Гессианы и высшие производные. Если вложить дуальные числа сами в себя — Dual(Dual(...)) — получается машинерия для второй производной. Расширение на «многоуровневые ε» даёт высшие порядки. По сути это вторая аппроксимация нестандартного анализа.

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

Парадоксы стандартной теории множеств. Парадокс Банаха–Тарского — как разрезать шар на пять кусков и собрать из них два таких же шара — это сигнал, что с обычной аксиоматикой не всё благополучно на бесконечности. Альтернативные конструкции вещественной прямой — повод задуматься, какая из них вообще описывает физическую реальность, а не идеализированный континуум.

К чему это все?

Бесконечно малые два с половиной века считались либо метафорой, либо ошибкой. Физики ими тихо пользовались, математики делали вид, что их не существует, и переписывали всё через пределы. В 1960 году Робинсон показал, что они не метафора. Это нормальные числа в нормальном поле, просто в большем, чем R.

А в 2026-м ваш autograd считает на их обрезанной версии градиенты для трансформеров.

Иногда математика возвращается с того света неожиданным образом.

Автор: inkedsymon

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js