File size: 10,408 Bytes
0ba9d09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
"""The schedule and the Gaussian corruption it defines.

Once we choose a noising rate, the variance-preserving path is fixed. Its accumulated rate gives
a signal coefficient and a noise standard deviation at every level, so one Gaussian draw reaches
any level from a clean image. The thousand-step grid tracks the continuous curve to three decimal
places, and every number here is arithmetic or a sample average against a law written in closed
form.

In Chapter 3 step k = 0 holds a clean image and corruption grows with k, up to pure noise at
k = K.

Chapter 3, Sections 3.11 and 3.12 of the notes. Runs on the offline 28-by-28 images. Prints the
four noise levels of the schedule, the discrete grid against the continuous curve, the chapter's
one pixel at step 207, the worked training batch at abar = 0.8, and the corruption law measured on
a batch.

    python lecture_3/forward_diffusion.py              # 16 levels, 1024 draws at each
    python lecture_3/forward_diffusion.py --steps 20   # a few seconds, for a first look
"""

# %% 01. Imports and configuration
import math

import torch

from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed

NUM_STEPS = 1000  # Diffusion steps K.
BETA_START = 1e-4  # Noise variance added at step 1.
BETA_END = 0.02  # Noise variance added at step K.
BETA_MIN = 0.1  # beta(0) of the continuous schedule these endpoints discretize.
BETA_MAX = 20.0  # beta(1) of the same schedule.

# `--steps` is the number of levels where we measure the corruption law, and `--samples` the
# number of draws we take at each one.


# %% 02. The schedule tensors
#
# alpha_k = 1 - beta_k is the one-step retention and abar_k its running product. We prefix a zero
# to the betas, so Python index k holds beta_k and the cumulative product starts at abar_0 = 1, the
# clean state. The two corruption coefficients are the square roots of abar_k and 1 - abar_k.

betas = torch.cat([
    torch.zeros(1),  # Placeholder so that index k holds beta_k.
    torch.linspace(BETA_START, BETA_END, NUM_STEPS),  # beta_1 ... beta_K.
])
alphas = 1.0 - betas  # One-step signal retention alpha_k.
alphas_cumprod = torch.cumprod(alphas, dim=0)  # Cumulative retention abar_k.
a = alphas_cumprod.sqrt()  # Signal coefficient a_k.
sigma = (1.0 - alphas_cumprod).sqrt()  # Noise standard deviation sigma_k.


# %% 03. Forward corruption
#
# x_k = a_k x_0 + sigma_k eps, the Gaussian transition q(x_k | x_0) = N(a_k x_0, sigma_k^2 I) read
# off at step k. One draw reaches any level, so training never simulates the path.


def extract(schedule, k, shape):
    # One schedule value per item, reshaped to broadcast over C, H, W.
    out = schedule.to(k.device).gather(0, k)
    return out.reshape(k.shape[0], *((1,) * (len(shape) - 1)))


def q_sample(x_0, k, noise=None):
    if noise is None:
        noise = torch.randn_like(x_0)  # Mean 0, covariance I, shape of x_0.
    abar = extract(alphas_cumprod, k, x_0.shape)
    a_k = abar.sqrt()  # Signal coefficient a_k.
    sigma_k = (1.0 - abar).sqrt()  # Noise standard deviation sigma_k.
    return a_k * x_0 + sigma_k * noise, noise  # x_k and the exact noise.


# %% 04. The continuous schedule behind the grid
#
# beta(t) = 0.1 + 19.9 t integrates to B(t) = 0.1 t + 9.95 t^2 and a_t = exp(-B(t)/2). If we divide
# the per-step variances 1e-4 and 0.02 by the step size 1/1000 we recover the two endpoints, so the
# grid and the curve describe one path. We invert a_t = exp(-B(t)/2) to locate a chosen noise level.


def noising_rate(t: float) -> float:
    """beta(t) = beta_min + (beta_max - beta_min) t, the linear noising rate."""
    return BETA_MIN + (BETA_MAX - BETA_MIN) * t


def continuous_signal(t: float) -> float:
    """a_t = exp(-B(t)/2) with B(t) = 0.1 t + 9.95 t^2, the curve the discrete betas track."""
    return math.exp(-(BETA_MIN * t + 0.5 * (BETA_MAX - BETA_MIN) * t ** 2) / 2.0)


def level_time(a_t: float) -> dict:
    """Solve B(t) = -2 log a_t for the time at which the signal coefficient is a_t.

    The quadratic 9.95 t^2 + 0.1 t - B = 0 has one positive root, and the notes tabulates that time
    beside the noise standard deviation and the noising rate there.
    """
    accumulated = -2.0 * math.log(a_t)  # B(t) at the requested level.
    quadratic = 0.5 * (BETA_MAX - BETA_MIN)  # Coefficient of t^2 in B(t).
    t = (-BETA_MIN + math.sqrt(BETA_MIN ** 2 + 4.0 * quadratic * accumulated)) / (2.0 * quadratic)
    return {"t": t, "sigma_t": math.sqrt(1.0 - a_t ** 2), "beta_t": noising_rate(t)}


# %% 05. One training batch at the running level
#
# The notes carries three clean scalars at a level with abar = 0.8 through the denoising loss by
# hand. We print the corrupted values, the squared error against a supposed prediction, the score
# targets the noise regression never forms, and the clean estimates the prediction implies.


def worked_batch(abar: float, x_0, epsilon, predicted) -> dict:
    """One training batch at a level with the given cumulative retention, as the notes works it."""
    a_k, sigma_k = math.sqrt(abar), math.sqrt(1.0 - abar)  # 0.8944 and 0.4472 at abar = 0.8.
    x_0, epsilon, predicted = (torch.tensor(v) for v in (x_0, epsilon, predicted))
    x_k = a_k * x_0 + sigma_k * epsilon  # [3] the corrupted batch.
    return {
        "a_k": a_k, "sigma_k": sigma_k,
        "x_k": [round(float(v), 4) for v in x_k],
        "loss": float((predicted - epsilon).square().mean()),  # L_simple on this batch.
        "score_targets": [round(float(-e / sigma_k), 3) for e in epsilon],
        "x_0_hat": [round(float((x - sigma_k * p) / a_k), 2) for x, p in zip(x_k, predicted)],
    }


# %% 06. The corruption law, measured
#
# q(x_k | x_0) is Gaussian with mean a_k x_0 and variance sigma_k^2 in every coordinate. So if we
# corrupt one clean image many times at a fixed level and average over the draws, we recover
# a_k x_0 pixel by pixel, and the spread around it recovers sigma_k^2. The largest gap over a
# spread of levels falls as one over the square root of the number of draws. A wrong coefficient in
# either of the two lines of `q_sample` would leave a gap that stays put as the draws grow.


def measure_law(x_0, levels, repeats: int, generator=None) -> dict:
    """Corrupt one clean image ``repeats`` times at each level and compare with q(x_k | x_0)."""
    batch = x_0.expand(repeats, *x_0.shape[1:])  # [R, 1, 28, 28] one image, repeated.
    mean_gap, variance_gap = 0.0, 0.0
    for step in levels:
        k = torch.full((repeats,), int(step), dtype=torch.long)  # [R] the same level throughout.
        noise = torch.randn(batch.shape, generator=generator)  # [R, 1, 28, 28] fresh draws.
        x_k, _ = q_sample(batch, k, noise)  # [R, 1, 28, 28] corrupted at this level.
        centre = float(a[step]) * x_0[0]  # [1, 28, 28] the conditional mean a_k x_0.
        mean_gap = max(mean_gap, float((x_k.mean(dim=0) - centre).abs().mean()))
        spread = float(x_k.var(dim=0, unbiased=False).mean())  # Pooled over the pixels.
        variance_gap = max(variance_gap, abs(spread - float(sigma[step]) ** 2))
    return {"mean_gap": mean_gap, "variance_gap": variance_gap}


# %% 07. The report
def main() -> None:
    parser = common_parser(__doc__.split("\n")[0])
    parser.set_defaults(steps=16, samples=1024)
    args = parser.parse_args()
    set_seed(args.seed)
    device = pick_device(args.device)  # Printed for consistency; this file runs on the CPU.
    generator = torch.Generator().manual_seed(args.seed)  # Local generator, so the report repeats.

    from cis6270.data import toy_images

    image = toy_images(1, 28, args.seed)  # [1, 1, 28, 28] one offline image in [-1, 1].
    levels = torch.linspace(1, NUM_STEPS, args.steps).round().long().tolist()  # Spread over K.
    law = measure_law(image, levels, args.samples, generator)

    # The four levels of the notes' table, located by inverting a_t = exp(-B(t)/2).
    located = [level_time(level) for level in (0.98, 0.8, 0.35, 0.04)]
    crossing = level_time(1.0 / math.sqrt(2.0))  # Where a_t and sigma_t meet.
    batch = worked_batch(0.8, (2.0, -1.0, 0.5), (0.5, -0.2, 1.0), (0.4, -0.1, 0.8))

    report = {
        "levels measured, draws each": [len(levels), args.samples],
        "largest gap, sample mean": law["mean_gap"],  # Zero up to the sampling error.
        "largest gap, sample variance": law["variance_gap"],
        "times at a_t = 0.98, 0.8, 0.35, 0.04": [round(v["t"], 4) for v in located],
        "sigma_t at the same four levels": [round(v["sigma_t"], 4) for v in located],
        "beta(t) at the same four levels": [round(v["beta_t"], 4) for v in located],
        # Where the two curves cross, and the value they share there.
        "crossing time and a_t = sigma_t": [round(crossing["t"], 5),
                                            round(crossing["sigma_t"], 4)],  # 0.25896, 0.7071.
        # The discrete coefficient against the continuous curve, at step 207 and at the last step.
        "abar_207": round(float(alphas_cumprod[207]), 4),  # 0.6402.
        "a_207 discrete and continuous": [round(float(a[207]), 4),
                                          round(continuous_signal(0.207), 4)],  # 0.8001, 0.7997.
        "sigma_207 discrete and continuous": [round(float(sigma[207]), 4),
                                              round(math.sqrt(1.0 - continuous_signal(0.207) ** 2),
                                                    4)],  # 0.5999, 0.6004.
        "a_1000 discrete and continuous": [round(float(a[1000]), 4),
                                           round(continuous_signal(1.0), 4)],  # 0.0064, 0.0066.
        "one pixel at k=207, x_0=0.5, eps=-1": float(a[207] * 0.5 - sigma[207]),  # -0.1998.
        "a_500, where noise exceeds signal": round(float(a[500]), 4),  # 0.2803.
        "worked batch at abar=0.8, x_k": batch["x_k"],  # 2.0125, -0.9839, 0.8944.
        "worked batch loss": batch["loss"],  # 0.02.
        "worked batch score targets": batch["score_targets"],  # -1.118, 0.447, -2.236.
        "worked batch x_0 estimates": batch["x_0_hat"],  # 2.05, -1.05, 0.60.
    }
    if not args.quiet:
        print(f"device={device} levels={args.steps} draws={args.samples} seed={args.seed}")
    print_report("The variance-preserving corruption", report)
    save_run(args.out, config=vars(args), report=report)


# %% 08. Run the script
if __name__ == "__main__":
    main()