ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Диффузия на практике: расписание шума и проверка обучающих примеров

Автор: · Обновлено

В этом практикуме вы проверите генератор зашумлённых обучающих примеров: составите расписание beta, вычислите alpha_bar и сравните два способа получения x_t. Результат — работающий файл Python с проверками среднего, дисперсии и индексации шага. Нейросеть и GPU не нужны.

В [уроке о мультимодальности](/lessons/without-university/generative-ai-foundations/generative-ai-11) диффузия показана как семейство генеративных методов. [Научный разбор DDPM](/research/ddpm-denoising-distribution-reverse-process) объясняет обратную цепочку и неоднозначность восстановления. Здесь сосредоточимся на подготовке корректного входа и целевого шума для обучения: ошибка на этом этапе может испортить эксперимент ещё до первого обновления весов.

Различайте beta одного шага и накопленное зашумление

На каждом шаге берётся доля сигнала alpha_t = 1 − beta_t. Для t шагов накопленная доля равна произведению alpha_bar_t = alpha_1 × ... × alpha_t. Это не 1 − сумма beta и не значение alpha последнего шага. Даже маленькое beta при многократном применении постепенно ослабляет исходный сигнал.

Для фиксированного x_0 прямое распределение имеет среднее sqrt(alpha_bar_t) × x_0 и дисперсию 1 − alpha_bar_t. Корни нужны потому, что дисперсия масштабируется квадратом коэффициента. Коэффициент шума 1 − alpha_bar вместо его квадратного корня создаст другую дисперсию.

В учебном опыте x_0 всегда равно 2.0. Поэтому проверяем именно условное распределение при фиксированном исходном значении. Если выбирать каждый раз разные x_0, общая дисперсия полученной смеси включает ещё и разброс исходных данных; сравнивать её только с 1 − alpha_bar неправильно.

Проверьте распределение двумя независимыми способами

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

Сохраните код как forward_diffusion.py и запустите python3 forward_diffusion.py. Оба генератора имеют фиксированный seed, чтобы эксперимент можно было повторить.

import math
import random
import statistics

x0 = 2.0
betas = [0.02] * 60
alpha_bar = [1.0]  # Индекс 0: шум ещё не добавлен.
for beta in betas:
    assert 0.0 < beta < 1.0
    alpha_bar.append(alpha_bar[-1] * (1.0 - beta))

assert alpha_bar[0] == 1.0
assert all(a > b for a, b in zip(alpha_bar, alpha_bar[1:]))
assert not math.isclose(alpha_bar[-1], 1.0 - betas[-1])

direct_rng = random.Random(29)
chain_rng = random.Random(30)

def direct(t):
    noise = direct_rng.gauss(0.0, 1.0)
    return math.sqrt(alpha_bar[t]) * x0 + math.sqrt(1.0 - alpha_bar[t]) * noise

def chain(t):
    x = x0
    for beta in betas[:t]:
        x = math.sqrt(1.0 - beta) * x + math.sqrt(beta) * chain_rng.gauss(0.0, 1.0)
    return x

assert direct(0) == x0
assert chain(0) == x0
n = 20000
t = 60
expected_mean = math.sqrt(alpha_bar[t]) * x0
expected_variance = 1.0 - alpha_bar[t]
for name, sample in (("direct", direct), ("chain", chain)):
    values = [sample(t) for _ in range(n)]
    mean = statistics.mean(values)
    variance = statistics.pvariance(values)
    assert abs(mean - expected_mean) < 6 * math.sqrt(expected_variance / n)
    assert abs(variance - expected_variance) < 8 * expected_variance * math.sqrt(2 / n)
    print(name, round(mean, 3), round(variance, 3))
print("expected", round(expected_mean, 3), round(expected_variance, 3))

Ожидаемое среднее примерно 1.091, дисперсия примерно 0.702. Оценки direct и chain немного отличаются из-за случайной выборки. Проверки используют широкие статистические допуски; это проверка реализации для данного эксперимента, не математическое доказательство равенства распределений. Независимая формула ожидаемых моментов позволяет поймать одинаково выглядящие, но неверно масштабированные примеры.

Не сравнивайте массивы значений поэлементно: отдельный шум прямой формулы объединяет влияние всех прошлых шагов и не равен шуму последнего шага цепочки. При подготовке target для модели нужно сохранять именно тот epsilon, который использован в прямой формуле.

Целевой шум должен соответствовать входу

Следующий небольшой файл создаёт один вход и проверяет, что из него вместе с сохранённым epsilon можно восстановить исходное число. Это диагностическое тождество. Модель при обучении получает x_t и номер t; доступ к x_0 здесь нужен программе, которая готовит пару.

import math
import random

rng = random.Random(7)
x0 = -1.5
alpha_bar_t = 0.4
epsilon = rng.gauss(0.0, 1.0)
xt = math.sqrt(alpha_bar_t) * x0 + math.sqrt(1 - alpha_bar_t) * epsilon
restored = (xt - math.sqrt(1 - alpha_bar_t) * epsilon) / math.sqrt(alpha_bar_t)
assert math.isclose(restored, x0, abs_tol=1e-12)

# Новый шум не является правильным target для уже созданного xt.
unrelated_noise = rng.gauss(0.0, 1.0)
wrong = (xt - math.sqrt(1 - alpha_bar_t) * unrelated_noise) / math.sqrt(alpha_bar_t)
assert not math.isclose(wrong, x0, abs_tol=1e-3)
print("Сохранённый epsilon согласован с xt; повторная генерация — нет")

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

Что делать, если проверка не проходит

Если среднее неверное, проверьте масштаб сигнала, произведение alpha и номер шага. Если неверна дисперсия при правильном среднем, проверьте корни и стандартное отклонение генератора шума. Если t = 0 уже меняет исходное число, соглашение об индексах нарушено. Первая запись alpha_bar[0] = 1 в нашем коде служит явной точкой отсчёта; чужая реализация может использовать другой формат массива коэффициентов.

Не исправляйте сбой увеличением допуска наугад. Выведите alpha_bar, теоретические моменты и измеренные значения для t = 1, 10 и 60. В систематической ошибке отклонение обычно сохраняет направление при увеличении числа выборок. Случайный разброс, напротив, должен уменьшаться при повторении опыта с большим n.

Наше расписание из 60 шагов оставляет alpha_bar около 0.298: исходный сигнал ещё заметен. Поэтому нельзя объявить конечное распределение стандартным нормальным и без проверки использовать его как начальное распределение генерации. Для достаточно длинной цепочки доля сигнала может стать малой, но это нужно посчитать для выбранных beta, а не вывести из слова «диффузия».

Практическое задание

Замените постоянное beta линейным расписанием от 0.001 до 0.03, сохранив 60 шагов. До запуска предположите, увеличится ли остаточный сигнал. Затем сравните прямую формулу и цепочку по тем же двум метрикам. Зафиксируйте расписание, seed, n, условное x_0 и соглашение об индексах в отчёте.

После этого намеренно уберите квадратный корень из коэффициента шума прямого способа. Найдите проверку, которая обнаружит дефект, и объясните её через дисперсию. Готовность к следующему этапу означает, что вы умеете получить согласованную пару (x_t, epsilon), проверить выбранный уровень шума и назвать ограничения опыта. Обучение обратного предсказателя начинается уже после этой проверки.

Источники