Парадигмы разработки Парадигмы за горизонтом: вероятностное, дифференцируемое и квантовое программирование
0%

Парадигмы за горизонтом: вероятностное, дифференцируемое и квантовое программирование

Парадигмы за горизонтом: вероятностное, дифференцируемое и квантовое программирование

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

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

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

Общая рамка: что каждая отнимает и что даёт

Ровно та же оптика, что в обзорной статье трека — ограничение в обмен на гарантию.

Парадигма Отнимает Даёт взамен
Вероятностная детерминированный ответ и дешёвое исполнение неопределённость как первоклассную величину: не «оценка 0,7», а «0,7 плюс-минус столько, с такой вероятностью»
Дифференцируемая произвольный поток управления и дискретные решения автоматическую подгонку параметров под цель по градиенту, без ручного вывода производных
Квантовая наблюдаемость промежуточного состояния и копирование данных пространство состояний, растущее экспоненциально с числом кубитов, и алгоритмы, использующие интерференцию

Часть I. Вероятностное программирование

Идея: программа как генеративная модель

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

Прямой прогон такой программы — это сэмплирование: она порождает синтетические данные. Интересное начинается, когда вы добавляете наблюдения и просите движок «запустить программу назад».

# Псевдокод в стиле PyMC/Stan: описываем ПОРОЖДЕНИЕ данных, а не алгоритм.
#   p_A ~ Beta(1, 1)                  # что мы знали о конверсии A до эксперимента
#   p_B ~ Beta(1, 1)
#   k_A ~ Binomial(n_A, p_A)          # как из p_A получились наблюдения
#   k_B ~ Binomial(n_B, p_B)
# observe(k_A = 118, n_A = 2000)
# observe(k_B = 152, n_B = 2010)
# infer: P(p_B > p_A), а также распределение прироста p_B - p_A

Ни одной строчки про то, как считать. Есть модель и есть наблюдения; способ вывода — забота движка. Это ровно та же сделка «отдай управление, получи оптимизацию», что в декларативной парадигме, только вместо плана запроса движок выбирает стратегию вывода.

Как это считается на самом деле

Для бета-биномиальной модели ответ известен аналитически: апостериорное распределение — снова бета, Beta(1 + k, 1 + n - k). Это редкая удача (сопряжённые распределения), и она позволяет проверить численный метод на задаче с известным ответом.

import numpy as np

rng = np.random.default_rng(42)

n_a, k_a = 2000, 118          # показов и конверсий в контроле
n_b, k_b = 2010, 152          # в варианте

# 1) Сопряжённый случай: апостериор известен в закрытой форме.
post_a = rng.beta(1 + k_a, 1 + n_a - k_a, size=200_000)
post_b = rng.beta(1 + k_b, 1 + n_b - k_b, size=200_000)

p_b_better = float((post_b > post_a).mean())
uplift = post_b - post_a
lo, hi = np.quantile(uplift, [0.025, 0.975])

print(f"P(B лучше A) = {p_b_better:.3f}")
print(f"прирост конверсии: {uplift.mean():.4f}, 95% интервал [{lo:.4f}, {hi:.4f}]")
# P(B лучше A) = 0.983
# прирост конверсии: 0.0166, 95% интервал [0.0016, 0.0316]

Ответ здесь принципиально другого сорта, чем p-value < 0.05: это распределение прироста, из которого напрямую считается ожидаемая выручка и риск ошибки. Практическая сторона A/B-экспериментов разобрана в статье A/B-тестирование, теория — в вероятности и статистике.

Как только модель перестаёт быть сопряжённой (а это происходит на второй же реальной задаче), закрытой формы нет и нужен численный вывод. Вот честная мини-реализация алгоритма Метрополиса — того самого, на котором построены все MCMC-движки:

import numpy as np

def log_posterior(p, k, n):
    """Логарифм ненормированной апостериорной плотности: приор Beta(1,1) + биномиальное правдоподобие."""
    if not 0.0 < p < 1.0:
        return -np.inf                       # вне носителя — нулевая плотность
    return k * np.log(p) + (n - k) * np.log1p(-p)

def metropolis(k, n, steps=100_000, scale=0.01, seed=0):
    """Случайное блуждание с симметричным предложением. O(steps) времени, O(steps) памяти."""
    rng = np.random.default_rng(seed)
    p = 0.5
    lp = log_posterior(p, k, n)
    chain = np.empty(steps)
    accepted = 0
    for i in range(steps):
        proposal = p + rng.normal(0.0, scale)
        lp_new = log_posterior(proposal, k, n)
        # Симметричное предложение -> отношение сводится к отношению плотностей.
        if np.log(rng.random()) < lp_new - lp:
            p, lp = proposal, lp_new
            accepted += 1
        chain[i] = p
    return chain, accepted / steps

chain, acc_rate = metropolis(k=152, n=2010)
burn_in = chain[10_000:]                     # первые шаги выбрасываем: цепь ещё «идёт к цели»
print(f"среднее апостериора {burn_in.mean():.4f}, доля принятий {acc_rate:.2f}")
# среднее апостериора ~0.0761 при аналитическом 153/2012 = 0.0760 — совпадает

Три вещи, которые видно из этих двадцати строк и которые определяют весь опыт работы с вероятностным программированием:

  1. Стоимость. Одна оценка правдоподобия на шаг, шагов — десятки и сотни тысяч. Итоговая сложность — O(steps × стоимость модели). Модель с миллионом наблюдений и сложным правдоподобием считается минутами и часами, а не миллисекундами.
  2. Настройка. Слишком маленький scale — цепь ползёт и не исследует пространство; слишком большой — почти все предложения отвергаются. Современные движки (NUTS/HMC в Stan, PyMC, NumPyro) настраивают шаг сами и используют градиент правдоподобия — да, тот самый из части II.
  3. Проверка сходимости обязательна. Цепь не «падает с ошибкой», она молча возвращает неправильный ответ. Поэтому запускают несколько цепей и смотрят диагностики.

Где это работает в проде

  • Байесовские A/B-тесты и последовательные эксперименты — решение можно принимать по мере накопления данных, а не только в конце.
  • Рейтинги игроков. TrueSkill в Xbox Live — вероятностная модель навыка с неопределённостью; отсюда и «шкала уверенности» у нового игрока.
  • Маркетинг-микс и прогноз спроса — модели с иерархией по регионам и категориям, где данных на каждый сегмент мало и неопределённость надо честно переносить дальше.
  • Оценка надёжности и рисков — актуарные расчёты, отказы оборудования, оценки редких событий.
  • Инструменты. Stan, PyMC, NumPyro, Turing.jl, Gen.

Цена парадигмы честная: это медленно, требует статистической грамотности всей команды и даёт новый класс инцидентов — «модель не сошлась» вместо «сервис упал». Зато там, где решение принимается в условиях неопределённости и цена ошибки высока, никакая точечная оценка не заменит распределение.

Часть II. Дифференцируемое программирование

Идея: программа, у которой можно спросить производную

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

Автоматическое дифференцирование — это не численная разность ((f(x+h) - f(x)) / h, теряет точность и стоит O(n) вычислений) и не символьное дифференцирование (взрыв выражений). Это применение правила цепочки к записанному графу операций, с точностью до машинного эпсилон.

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

Режим Стоимость для f: R^n -> R^m Память Когда применяют
Прямой (forward) примерно n прогонов маленькая, потоковая мало входов, много выходов; якобиан-векторные произведения
Обратный (reverse) примерно m прогонов хранит промежуточные значения много входов, скалярный выход — то есть всё обучение

Отсюда и вся конструкция машинного обучения: параметров миллионы (n огромно), потеря одна (m = 1), поэтому используется обратный режим — он же обратное распространение ошибки.

Мини-реализация, которая помещается в голову

class Value:
    """Скаляр, который помнит, как он был получен. Достаточно для полноценного autodiff."""

    def __init__(self, data, children=(), op=""):
        self.data = data
        self.grad = 0.0
        self._backward = lambda: None     # как передать градиент родителям
        self._prev = set(children)
        self._op = op

    def __add__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data + other.data, (self, other), "+")

        def _backward():
            # d(a+b)/da = 1, d(a+b)/db = 1 — градиент проходит насквозь
            self.grad += out.grad
            other.grad += out.grad
        out._backward = _backward
        return out

    def __mul__(self, other):
        other = other if isinstance(other, Value) else Value(other)
        out = Value(self.data * other.data, (self, other), "*")

        def _backward():
            # d(a*b)/da = b, d(a*b)/db = a
            self.grad += other.data * out.grad
            other.grad += self.data * out.grad
        out._backward = _backward
        return out

    def relu(self):
        out = Value(self.data if self.data > 0 else 0.0, (self,), "relu")

        def _backward():
            # производная разрывна в нуле — берём субградиент
            self.grad += (out.data > 0) * out.grad
        out._backward = _backward
        return out

    def backward(self):
        """Топологическая сортировка графа и один обратный проход. O(V + E)."""
        order, visited = [], set()

        def build(v):
            if v not in visited:
                visited.add(v)
                for child in v._prev:
                    build(child)
                order.append(v)

        build(self)
        self.grad = 1.0                   # d(результат)/d(результат) = 1
        for v in reversed(order):
            v._backward()

# Проверка: f(a, b) = (a * b + b).relu(), a = -3, b = 4
a, b = Value(-3.0), Value(4.0)
f = (a * b + b).relu()
f.backward()
print(f.data, a.grad, b.grad)     # 0.0 0.0 0.0 — в отрицательной ветви ReLU градиента нет

Сорок строк дают ровно ту механику, что лежит в основе PyTorch и JAX: граф строится по ходу вычисления, обратный проход идёт по нему в обратном топологическом порядке. Всё остальное в промышленных фреймворках — производительность, работа с тензорами, распределённость и компиляция графа.

Где парадигма упирается в стену

  1. Недифференцируемые операции. argmax, сортировка, ветвление по данным, дискретный выбор — производной нет. Обходят релаксациями (Gumbel-softmax, «мягкий» argmax), оценщиками градиента для дискретных величин или прямым пропусканием градиента (straight-through).
  2. Память. Обратный режим обязан хранить промежуточные значения всего прямого прохода. Для глубокой сети это гигабайты; лечится градиентным чекпойнтингом — обмен памяти на время (пересчитать часть прямого прохода заново).
  3. Численная устойчивость. Наивные формулы взрываются: log(sum(exp(x))) считают через сдвиг на максимум, деление на почти-ноль убивает градиент. Это ровно те же проблемы, что в численных методах.
  4. Поток управления против компиляции. Динамический граф (PyTorch) удобно отлаживать; трассируемый и компилируемый (JAX) быстрее, но требует статических форм и запрещает произвольные if по значениям тензоров.

Не только нейросети

Стоит запомнить, что дифференцируемость — свойство программы, а не только модели:

  • Дифференцируемая физика — подбор параметров симуляции под наблюдаемое поведение.
  • Дифференцируемый рендеринг — восстановление геометрии и материалов сцены по изображениям.
  • Оптимизация систем — обучаемые контроллеры, параметры планировщиков, настройка гиперпараметров симуляций.
  • Научные вычисленияJAX, Zygote.jl, Enzyme дифференцируют обычный код, а не специальный DSL.

Практическая часть обучения моделей — в статьях Перцептрон и обратное распространение и Обучение и оптимизация; общий взгляд на постановку задач — в треке по машинному обучению.

Часть III. Квантовое программирование

Модель вычисления

Классический бит — 0 или 1. Кубит описывается парой комплексных амплитуд: состояние α|0⟩ + β|1⟩, где |α|² + |β|² = 1. Регистр из n кубитов описывается 2^n амплитудами — тридцать кубитов это миллиард комплексных чисел, и именно поэтому симуляция на классической машине упирается в память.

Три правила, которые определяют всё программирование под такую машину:

  1. Операции обратимы. Любой гейт — унитарное преобразование; вычисление можно прокрутить назад. Никакого «затереть переменную».
  2. Измерение разрушает. Прочитав кубит, вы получаете 0 или 1 с вероятностью |α|² или |β|², а суперпозиция схлопывается. Промежуточное состояние принципиально не наблюдаемо.
  3. Копировать нельзя. Теорема о запрете клонирования: универсального copy(qubit) не существует. Это ломает привычные приёмы вроде «сохраню на всякий случай».

Самое частое заблуждение: «квантовый компьютер перебирает все варианты параллельно». Суперпозицию действительно можно подготовить над всеми входами, но измерение вернёт один случайный результат. Полезные алгоритмы устроены иначе: они организуют интерференцию так, чтобы амплитуды неправильных ответов гасили друг друга, а правильного — усиливались. Квантовое программирование — это не про перебор, это про конструирование интерференции.

Схема Белла: суперпозиция, запутывание и измерение

Схема на картинке — «hello world» этой парадигмы: гейт Адамара переводит первый кубит в равную суперпозицию, CNOT связывает второй с первым, измерение даёт 00 или 11 примерно поровну — и никогда 01 или 10. Это запутанность: результаты скоррелированы, хотя каждый по отдельности случаен.

# Qiskit: схема — это данные, которые исполняются на симуляторе или на железе.
from qiskit import QuantumCircuit

qc = QuantumCircuit(2, 2)
qc.h(0)                 # суперпозиция на первом кубите
qc.cx(0, 1)             # запутывание: CNOT с управляющим 0 и целевым 1
qc.measure([0, 1], [0, 1])

# Результат — не значение, а гистограмма исходов по N прогонам ("выстрелам"):
# {'00': ~50%, '11': ~50%} — состояний '01' и '10' не будет.

Обратите внимание на программную модель: квантовая программа — это данные (схема), которые собирает и запускает классический хост, получая распределение исходов. Отладка — это статистика по прогонам и симулятор для маленьких размерностей.

Что реально умеют алгоритмы

Алгоритм Выигрыш Практический смысл
Гровер, поиск в неструктурированном множестве квадратичный: около корня из N вместо N ускорение переборных задач; на реальном железе съедается накладными расходами
Шор, факторизация и дискретный логарифм экспоненциальный угроза RSA и эллиптической криптографии — при наличии большого отказоустойчивого компьютера
Симуляция квантовых систем экспоненциальный для ряда задач химия, материалы — исходная мотивация Фейнмана и самое вероятное первое применение
Вариационные схемы VQE и QAOA не доказан гибридные эвристики для оптимизации и химии на нынешнем шумном железе

Как выглядит реальность 2020-х

Текущая эпоха называется NISQ — шумные квантовые устройства промежуточного масштаба (термин ввёл Джон Прескилл в статье 2018 года). Кубиты декогерируют за микросекунды, гейты ошибаются, и глубина схемы ограничена этим сверху. Коррекция ошибок принципиально решает проблему, но платит накладными расходами: один логический кубит — это порядка тысячи физических при современных подходах вроде поверхностного кода.

Отсюда доминирующая программная модель — гибридная: классический оптимизатор крутит параметры короткой квантовой схемы.

Что из этого касается инженера уже сегодня — ровно один пункт, и он не про написание квантовых программ. Угроза «собрать шифротрафик сейчас, расшифровать потом» реальна для данных с долгим сроком секретности, поэтому в 2024 году NIST стандартизировал постквантовые алгоритмы (FIPS 203 ML-KEM, FIPS 204 ML-DSA, FIPS 205 SLH-DSA), и миграция на них — обычная инженерная задача с инвентаризацией, сроками и совместимостью. Практика — в статье Прикладная криптография и Транспортная безопасность.

Как эти три парадигмы возникали

Что общего и чему это учит

Вопрос Вероятностное Дифференцируемое Квантовое
Единица вычисления распределение значение вместе с градиентом амплитуда
Что пишет человек генеративную модель обычную программу из дифференцируемых операций схему унитарных операций
Что делает движок вывод по наблюдениям обратный проход по графу исполнение схемы и измерение
Главный ресурс время на сэмплирование память на активации глубина схемы до декогеренции
Как отлаживают R-hat, ESS, проверка предсказаниями нормы градиентов, поиск NaN, проверка конечными разностями симулятор, статистика исходов, томография
Главная ловушка молча не сошлось молча обучилось не тому шум съел сигнал

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

Типичные ошибки

  1. Использовать вероятностное программирование там, где хватает формулы. Для бета-биномиальной модели MCMC — это тысячекратный перерасход на задаче с закрытым решением.
  2. Игнорировать диагностику сходимости. Цепь без проверки R-hat даёт уверенный неправильный ответ, а не ошибку.
  3. Считать autodiff волшебством. Недифференцируемая операция в середине графа обнуляет градиент, и модель «учится» ровно нулю.
  4. Забывать про память в обратном режиме. Внезапный OOM на длинной последовательности — почти всегда сохранённые активации.
  5. Верить в «квантовый параллелизм». Без интерференции суперпозиция не даёт ничего: измерение вернёт один случайный вариант.
  6. Планировать «перевести систему на квантовые вычисления». Осмысленное действие сегодня — инвентаризация криптографии и план миграции на постквантовые алгоритмы.
  7. Смешивать классы задач. Вероятностная модель отвечает на вопрос о неопределённости, градиентная оптимизация — о подгонке параметров. Подменять одно другим — распространённый способ получить уверенную чушь.

Мини-итог

  1. Три парадигмы ломают общее допущение всего остального трека: значение перестаёт быть наблюдаемым. Это меняет не только семантику, но и инструменты отладки.
  2. Вероятностная программа описывает порождение данных; движок решает обратную задачу. Ответ — распределение, из которого считаются риск и выгода, а не одно число.
  3. MCMC стоит O(шагов × стоимость модели) и требует диагностик: несходимость не падает, а тихо врёт.
  4. Автоматическое дифференцирование — это правило цепочки по графу вычислений; обратный режим даёт градиент по миллионам параметров за стоимость нескольких прямых проходов, платя памятью.
  5. Дифференцируемость — свойство программы, а не только нейросети: физика, рендеринг, контроллеры.
  6. Квантовое вычисление — про интерференцию, а не про перебор; ограничения — необратимость измерения, запрет копирования, шум и стоимость коррекции ошибок.
  7. Единственное, что требует действий от обычного инженера сегодня, — миграция на постквантовую криптографию для данных с длинным сроком секретности.

Источники

Что дальше

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

Куда идти дальше, в зависимости от того, чего не хватает:

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов