Delete lecture_1
Browse files- lecture_1/README.md +0 -83
- lecture_1/calculus.py +0 -279
- lecture_1/fields.py +0 -234
- lecture_1/information.py +0 -219
- lecture_1/linear_algebra.py +0 -253
- lecture_1/optimization.py +0 -223
- lecture_1/probability.py +0 -261
lecture_1/README.md
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
# Lecture 1: Introduction and Mathematical Foundations
|
| 2 |
-
|
| 3 |
-
Chapter 1 of the notes is the toolbox the other six chapters draw on, and it works every piece of
|
| 4 |
-
it on a small example by hand. The six scripts here run those examples. Each one prints the
|
| 5 |
-
chapter's hand-worked value, the value the script computes for it, and the gap between the two, so
|
| 6 |
-
a student reading the chapter can watch its arithmetic come out of the code. Nothing here trains a
|
| 7 |
-
network. The scripts are worked examples, and `tests/test_lecture_1.py` asserts every number they
|
| 8 |
-
print against the chapter that states it.
|
| 9 |
-
|
| 10 |
-
Run them from the repository root:
|
| 11 |
-
|
| 12 |
-
```
|
| 13 |
-
cd cis6270
|
| 14 |
-
PYTHONPATH=. python lecture_1/probability.py
|
| 15 |
-
PYTHONPATH=. python -m pytest tests/test_lecture_1.py -q
|
| 16 |
-
```
|
| 17 |
-
|
| 18 |
-
## The files
|
| 19 |
-
|
| 20 |
-
| File | Command | What it works through |
|
| 21 |
-
| --- | --- | --- |
|
| 22 |
-
| `probability.py` | `python lecture_1/probability.py` | Sections 1.3 to 1.11. The two-token DNA distribution, the expected adenine count against a Monte Carlo ladder, the worked variance, the three peptides and their covariance matrix, the noising equation, the motif-and-binding table with its marginals and conditionals, Bayes' rule, and the independence check. |
|
| 23 |
-
| `linear_algebra.py` | `python lecture_1/linear_algebra.py` | Sections 1.12, 1.13, 1.15, 1.16, 1.17 and 1.18. The simplex and a mixture, the softmax of `z = (2, 1, 0)`, one step of the two-state chain, the eigenvalues of `A = [[2, 1], [1, 2]]`, powers along an eigendirection, the telescoping `A^n = V Lambda^n V^{-1}`, the stationary distribution and its rate, and `e^{tA}` by both the series and the eigenvalues. |
|
| 24 |
-
| `calculus.py` | `python lecture_1/calculus.py` | Sections 1.22 to 1.30. The Jacobian by automatic differentiation against the chapter's closed form, the determinant as local volume change, change of variables in one and in two dimensions, the log-determinant of a composition, the two inverse branches of the squaring map, the gradient and directional derivative, the divergence of three fields, and the continuity equation checked numerically on `v(x) = x`. |
|
| 25 |
-
| `fields.py` | `python lecture_1/fields.py` | Sections 1.31 and 1.32. The score of the standard Gaussian and of `N(1, 4)`, the normalizing constant differentiated away, the Laplacian of the bowl `x_1^2 + 3 x_2^2` and of the saddle `x_1^2 - x_2^2`, the heat equation at `D = 1/2` on the widening Gaussian `N(0, t)`, and the Fokker-Planck equation with its two reductions, to the continuity equation at `sigma_t = 0` and to the heat equation at `b_t = 0`. The same drift and noise drive an Euler-Maruyama walk, whose sample variance tracks the elapsed time. Chapter 3 starts from both sections. |
|
| 26 |
-
| `information.py` | `python lecture_1/information.py` | Sections 1.33 to 1.35. Information content, entropy, the cross-entropy of `p = (0.8, 0.2)` scored by `q = (0.6, 0.4)`, the split `H(p, q) = H(p) + KL(p \|\| q)`, nonnegativity and asymmetry, and maximum likelihood on ten DNA bases as a KL minimization. |
|
| 27 |
-
| `optimization.py` | `python lecture_1/optimization.py` | Sections 1.36 to 1.38. Gradient descent on `(theta - 3)^2`, an equality constraint through its Lagrangian, normalization as a constraint, the maximum-entropy derivation that produces the exponential family at a GC target of 0.6, the KKT conditions on a bounded quadratic, and the same likelihood problem solved on logits through the softmax. |
|
| 28 |
-
|
| 29 |
-
## The flags
|
| 30 |
-
|
| 31 |
-
Every script takes `--steps`, `--seed`, `--out` and `--quiet`, so the flags learned once apply
|
| 32 |
-
across the course. `--steps` drives the iteration count of each script, and every flag a script
|
| 33 |
-
takes changes a line of its report.
|
| 34 |
-
|
| 35 |
-
| Flag | Where it changes the report |
|
| 36 |
-
| --- | --- |
|
| 37 |
-
| `--steps` | `probability.py`, repeats of the ten-sample Monte Carlo estimate, default 5000. `linear_algebra.py`, iterations of `p_{t+1} = P^T p_t`, default 10. `calculus.py`, Euler steps along the worked flow, default 200. `fields.py`, Euler-Maruyama steps over `[0, 1]`, default 200. `information.py`, repeats of the ten-sample cross-entropy estimate, default 5000. `optimization.py`, gradient-descent steps, default 600. |
|
| 38 |
-
| `--samples` | `probability.py`, the largest Monte Carlo sample size, default 1000. `fields.py`, paths simulated along the SDE, default 20000. `information.py`, the draws behind the sampled cross-entropy and the random distribution pairs, default 20000. `optimization.py`, the competing distributions drawn at the same GC target, default 4000. |
|
| 39 |
-
| `--grid` | `calculus.py` and `fields.py`, the points used in the numerical checks, default 1000. |
|
| 40 |
-
| `--seed` | The Monte Carlo draws, the simulated paths and the random distributions. The same seed gives the same report. |
|
| 41 |
-
| `--out` | A directory for `config.json` and `report.json`, written through `cis6270.runner.save_run`. |
|
| 42 |
-
| `--quiet` | Print the closing report and nothing else. |
|
| 43 |
-
|
| 44 |
-
## What a default run prints
|
| 45 |
-
|
| 46 |
-
| Script | Headline numbers |
|
| 47 |
-
| --- | --- |
|
| 48 |
-
| `probability.py` | `E[h(X)] = 1.1000`, Monte Carlo estimate `1.1050` at `N = 1000`, `Var(h(X)) = 0.6900`, spread of 5000 ten-sample averages `0.2616` against the predicted `0.2627`, `rho_XY = 0.9449`, `p(X = 1 \| Y = 1) = 0.8182`, association `0.0960`. |
|
| 49 |
-
| `linear_algebra.py` | `softmax(z)_1 = 0.6652`, `lambda_1 = 3.0000`, `x_10` first entry `118099`, `p*_1 = 0.6000`, second eigenvalue `0.5000`, `e^{tA}_11 = 1.2275` at `t = 0.1`, truncation error `0.0002`. |
|
| 50 |
-
| `calculus.py` | `cos(1) = 0.5403`, `det J_phi = 4.0000` at `x_1 = 2`, `p_Y = 0.1667` inside the image of `A`, `log\|det J\| = 3.1781` for the composition, continuity residual `0.0000`, `log p = -1.0000` along the flow at `t = 1`. |
|
| 51 |
-
| `fields.py` | `s(3) = -3.0000`, `s(5) = -1.0000` under `N(1, 4)`, Laplacian `8.0000` on the bowl and `0.0000` on the saddle, `p_1(0) = 0.3989`, `d p_t(0)/dt = -0.1995`, `D` times the Laplacian `-0.1995`, every residual `0.0000`, sample variance `1.0130` at `t = 1` over 20000 paths. |
|
| 52 |
-
| `information.py` | `H(p) = 0.5004`, `H(p, q) = 0.5919`, `KL(p \|\| q) = 0.0915`, `KL(q \|\| p) = 0.1046`, spread of 5000 ten-sample estimates `0.0502` against the predicted `0.0513`, empirical loss `1.2799` nats, uniform loss `1.3863` nats, `KL(p_hat \|\| uniform) = 0.1064`. |
|
| 53 |
-
| `optimization.py` | `theta = 3.0000` after 600 steps, `lambda = -1.0000` on the equality constraint, `lambda = 0.4055` for a GC target of 0.6, `H(p*) = 1.3662` nats, best drawn competitor `1.3661` nats, `lambda = 4.0000` on the active bound, softmax descent loss `1.2799` nats. |
|
| 54 |
-
|
| 55 |
-
Every one of these runs in a few seconds on one core. The sampled rows are the ones that move with
|
| 56 |
-
`--seed`. `probability.py --steps 50000` closes the measured spread to `0.2624` and
|
| 57 |
-
`information.py --steps 50000` closes it to `0.0510`, at the cost of a second each.
|
| 58 |
-
|
| 59 |
-
## What is simplified
|
| 60 |
-
|
| 61 |
-
- Every script runs in double precision. Chapter 1 quotes five decimal places in places, and the
|
| 62 |
-
single precision used by the training lectures would not reach them.
|
| 63 |
-
- The Monte Carlo sections redraw their samples, so the sampled values move with `--seed`. The
|
| 64 |
-
chapter's own ten-sample draw gave 1.2 against the exact 1.1, and that particular draw is not
|
| 65 |
-
reproduced. The exact value and the `1/sqrt(N)` error scale are.
|
| 66 |
-
- The continuity equation is checked by central differences at interior points of the support of
|
| 67 |
-
`p_t`, away from the two ends, where a one-sided difference would be needed. The Fokker-Planck
|
| 68 |
-
residual of `fields.py` uses the same differences, with a second difference for the Laplacian, so
|
| 69 |
-
it sits near `1e-8` and not at zero.
|
| 70 |
-
- `fields.py` takes the Brownian increment of `dX_t = b_t(X_t) dt + sigma_t dW_t` as a scaled
|
| 71 |
-
Gaussian draw per step. Chapter 3 defines Brownian motion properly. With a constant drift and a
|
| 72 |
-
constant noise magnitude the Euler-Maruyama walk has the exact law of the chapter's `N(bt, t)`, so
|
| 73 |
-
the gap the script prints is Monte Carlo error alone.
|
| 74 |
-
- The branch density `1/(2 sqrt(y))` is integrated numerically over `[0.01, 1]`, because the
|
| 75 |
-
integrand grows without bound at the origin. Its value over the full `[0, 1]` comes from the
|
| 76 |
-
antiderivative the chapter gives.
|
| 77 |
-
- The random competitors in the maximum-entropy check and the random pairs in the KL check confirm
|
| 78 |
-
the chapter's inequalities on a finite sample. They are a check, not the proof, which the chapter
|
| 79 |
-
gives in full. The competitors cover the two free splits of the GC-constrained family, one inside
|
| 80 |
-
`{A, T}` and one inside `{C, G}`, drawn independently.
|
| 81 |
-
- Each script has its own three-line `show` helper for the side-by-side printing and its own
|
| 82 |
-
`WORKED` dictionary of computed values. Neither belongs in `cis6270/`, because no other lecture
|
| 83 |
-
prints a chapter number beside a computed one.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/calculus.py
DELETED
|
@@ -1,279 +0,0 @@
|
|
| 1 |
-
"""Jacobians, change of variables, divergence and the continuity equation.
|
| 2 |
-
|
| 3 |
-
A map moves points, and the Jacobian is the linear map it looks like near one of them. Its
|
| 4 |
-
determinant is the local volume factor, so the density at an image point is the density at its
|
| 5 |
-
source divided by that factor, the log-determinants add across a composition, and for a
|
| 6 |
-
many-to-one map the density is a sum of one corrected term per inverse branch. In continuous time
|
| 7 |
-
that factor becomes the divergence of a velocity field, and the continuity equation holds it in
|
| 8 |
-
balance with the change in density at every point.
|
| 9 |
-
|
| 10 |
-
Chapter 1, Sections 1.22 to 1.30 of the notes. Runs on the maps of Sections 1.22.2 and 1.23, the
|
| 11 |
-
squaring map of Section 1.26, and the expanding flow v(x) = x of Section 1.30.2. Every Jacobian
|
| 12 |
-
is taken by automatic differentiation and checked against the chapter's closed form.
|
| 13 |
-
|
| 14 |
-
python lecture_1/calculus.py # every worked example
|
| 15 |
-
python lecture_1/calculus.py --grid 4000 # finer grids in the numerical checks
|
| 16 |
-
python lecture_1/calculus.py --steps 2000 # a finer Euler walk along the worked flow
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import argparse
|
| 23 |
-
import math
|
| 24 |
-
|
| 25 |
-
import torch
|
| 26 |
-
|
| 27 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 28 |
-
|
| 29 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
A_SCALE = torch.tensor([[2.0, 0.0], [0.0, 3.0]]) # The map of Section 1.23, which scales area by 6.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 39 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 40 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 41 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 42 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 43 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 44 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def phi_mixed(x: torch.Tensor) -> torch.Tensor:
|
| 48 |
-
"""The map (x_1^2 + x_2, sin x_1 + 3 x_2) of Section 1.22.2, nonlinear in its first component."""
|
| 49 |
-
return torch.stack([x[0] ** 2 + x[1], torch.sin(x[0]) + 3.0 * x[1]])
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def phi_square(x: torch.Tensor) -> torch.Tensor:
|
| 53 |
-
"""The map (x_1^2, x_2) of Section 1.23, one-to-one once the domain is restricted to x_1 > 0."""
|
| 54 |
-
return torch.stack([x[0] ** 2, x[1]])
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def psi_scale(y: torch.Tensor) -> torch.Tensor:
|
| 58 |
-
"""The linear map y -> A y with A = diag(2, 3), composed after phi_square in Section 1.25."""
|
| 59 |
-
return A_SCALE @ y
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def f_bowl(x: torch.Tensor) -> torch.Tensor:
|
| 63 |
-
"""The scalar field f(x_1, x_2) = x_1^2 + 2 x_2^2 of Section 1.27.1."""
|
| 64 |
-
return x[0] ** 2 + 2.0 * x[1] ** 2
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def divergence(field, x: torch.Tensor) -> float:
|
| 68 |
-
"""Section 1.29: the divergence is the sum of d v_i / d x_i, which is the trace of J_v."""
|
| 69 |
-
return float(torch.autograd.functional.jacobian(field, x).trace())
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
# %% 02. The Jacobian matrix and the local linear approximation
|
| 73 |
-
def jacobian(quiet: bool = False) -> dict:
|
| 74 |
-
"""Sections 1.22.2 and 1.22.3: J_phi at (1, 0), and the displacement it predicts."""
|
| 75 |
-
point = torch.tensor([1.0, 0.0]) # The point at which the chapter evaluates the Jacobian.
|
| 76 |
-
computed = torch.autograd.functional.jacobian(phi_mixed, point) # [2, 2], by backpropagation.
|
| 77 |
-
closed_form = torch.tensor([[2.0 * point[0], 1.0], # The chapter's entries, 2 x_1 and 1.
|
| 78 |
-
[torch.cos(point[0]), 3.0]]) # cos x_1 and 3; [2, 2].
|
| 79 |
-
delta = torch.tensor([0.01, -0.02]) # The small input displacement of Section 1.22.3.
|
| 80 |
-
predicted = computed @ delta # J_phi(x) Delta x, the first-order change in the output; [2].
|
| 81 |
-
exact = phi_mixed(point + delta) - phi_mixed(point) # The change the full map produces; [2].
|
| 82 |
-
show("J_phi(1, 0)_11", 2.0, float(computed[0, 0]), quiet)
|
| 83 |
-
show("J_phi(1, 0)_21 = cos(1)", 0.5403, float(computed[1, 0]), quiet)
|
| 84 |
-
show("largest |autograd - closed form|", 0.0, float((computed - closed_form).abs().max()), quiet)
|
| 85 |
-
show("predicted change, first output", 0.0, float(predicted[0]), quiet)
|
| 86 |
-
show("predicted change, second output", -0.0546, float(predicted[1]), quiet)
|
| 87 |
-
show("exact change, first output", 0.0001, float(exact[0]), quiet)
|
| 88 |
-
show("exact change, second output", -0.05464, float(exact[1]), quiet)
|
| 89 |
-
return {"jacobian": computed, "predicted": predicted, "exact": exact}
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
# %% 03. The Jacobian determinant as local volume change
|
| 93 |
-
def determinant(quiet: bool = False) -> dict:
|
| 94 |
-
"""Section 1.23: the absolute determinant is the factor by which a small region changes area."""
|
| 95 |
-
corners = torch.tensor([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]) # The unit square.
|
| 96 |
-
mapped = corners @ A_SCALE.T # The image rectangle [0, 2] by [0, 3]; [4, 2].
|
| 97 |
-
area = float((mapped[1, 0] - mapped[0, 0]) * (mapped[3, 1] - mapped[0, 1])) # Width times height.
|
| 98 |
-
collapsing = torch.tensor([[1.0, 0.0], [0.0, 0.0]]) # A map that flattens the plane onto a line.
|
| 99 |
-
stretch = torch.autograd.functional.jacobian(phi_square, torch.tensor([2.0, 1.0])) # At x_1 = 2.
|
| 100 |
-
small = torch.tensor([0.25, 1.0]) # A point where the same map contracts, since 2 x_1 is below 1.
|
| 101 |
-
squeeze = torch.autograd.functional.jacobian(phi_square, small) # J_phi at x_1 = 0.25; [2, 2].
|
| 102 |
-
show("det A for diag(2, 3)", 6.0, float(torch.linalg.det(A_SCALE)), quiet)
|
| 103 |
-
show("area of the image rectangle", 6.0, area, quiet)
|
| 104 |
-
show("det of the collapsing map", 0.0, float(torch.linalg.det(collapsing)), quiet)
|
| 105 |
-
show("det J_phi at x_1 = 2", 4.0, float(torch.linalg.det(stretch)), quiet)
|
| 106 |
-
show("det J_phi at x_1 = 0.25", 0.5, float(torch.linalg.det(squeeze)), quiet)
|
| 107 |
-
return {"area": area, "stretch": float(torch.linalg.det(stretch))}
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# %% 04. Change of variables in one and in two dimensions
|
| 111 |
-
def change_of_variables(grid: int, quiet: bool = False) -> dict:
|
| 112 |
-
"""Sections 1.24.1 and 1.24.2: p_Y(phi(x)) = p_X(x) / |det J_phi(x)| on the two worked maps."""
|
| 113 |
-
inverse_slope = 0.5 # d phi^{-1}/dy for phi^{-1}(y) = (y - 1)/2, the reciprocal of phi' = 2.
|
| 114 |
-
p_y = 1.0 * abs(inverse_slope) # p_Y(y) = p_X(phi^{-1}(y)) |d phi^{-1}/dy|, so p_Y = 1/2.
|
| 115 |
-
preimage = ((1.4 - 1.0) / 2.0, (2.0 - 1.0) / 2.0) # The inverse image of B = [1.4, 2.0].
|
| 116 |
-
mass = (preimage[1] - preimage[0]) * 1.0 # P(X in [0.2, 0.5]) under the uniform density.
|
| 117 |
-
line = 1.0 + (torch.arange(grid) + 0.5) * (2.0 / grid) # Midpoints of [1, 3]; [grid].
|
| 118 |
-
normalization = float(torch.full_like(line, p_y).sum() * (2.0 / grid)) # Integral of p_Y there.
|
| 119 |
-
flat = 1.0 / float(torch.linalg.det(A_SCALE)) # p_Y = 1/6 inside the image of the unit square.
|
| 120 |
-
step = 1.0 / grid # Midpoint spacing on the unit interval of the first input coordinate.
|
| 121 |
-
x_one = (torch.arange(grid) + 0.5) * step # Midpoints of (0, 1) in that coordinate; [grid].
|
| 122 |
-
volume_factor = 2.0 * x_one # |det J_phi| = 2 x_1 for phi = (x_1^2, x_2), at each point; [grid].
|
| 123 |
-
output_density = 1.0 / (2.0 * (x_one ** 2).sqrt()) # p_Y(y) = 1/(2 sqrt(y_1)) at y_1 = x_1^2.
|
| 124 |
-
residual = float((output_density * volume_factor - 1.0).abs().max()) # Should recover p_X = 1.
|
| 125 |
-
transported = float((output_density * volume_factor).sum() * step) # Total mass by substitution.
|
| 126 |
-
show("preimage of [1.4, 2.0], lower end", 0.2, preimage[0], quiet)
|
| 127 |
-
show("preimage of [1.4, 2.0], upper end", 0.5, preimage[1], quiet)
|
| 128 |
-
show("P(X in the preimage)", 0.3, mass, quiet)
|
| 129 |
-
show("p_Y on [1, 3] after Y = 2X + 1", 0.5, p_y, quiet)
|
| 130 |
-
show("integral of p_Y over [1, 3]", 1.0, normalization, quiet)
|
| 131 |
-
show("p_Y inside the image of A", 1.0 / 6.0, flat, quiet)
|
| 132 |
-
show("density times area for A", 1.0, flat * 6.0, quiet)
|
| 133 |
-
show("p_Y(y_1 = 1) for the x_1^2 map", 0.5, 1.0 / (2.0 * math.sqrt(1.0)), quiet)
|
| 134 |
-
show("largest |p_Y |det J| - p_X|", 0.0, residual, quiet)
|
| 135 |
-
show("total mass after the x_1^2 map", 1.0, transported, quiet)
|
| 136 |
-
return {"p_y": p_y, "flat": flat, "residual": residual, "transported": transported}
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# %% 05. Composed maps and the log-density form
|
| 140 |
-
def composition(quiet: bool = False) -> dict:
|
| 141 |
-
"""Section 1.25: log|det J| adds across a composition, so two factors become one sum."""
|
| 142 |
-
point = torch.tensor([2.0, 1.0]) # The chapter evaluates the composition at x_1 = 2.
|
| 143 |
-
inner = torch.autograd.functional.jacobian(phi_square, point) # J_phi(x), det 2 x_1 = 4; [2, 2].
|
| 144 |
-
outer = torch.autograd.functional.jacobian(psi_scale, phi_square(point)) # J_psi at phi(x); [2, 2].
|
| 145 |
-
chained = outer @ inner # The chain rule of Proposition 1.1, giving diag(8, 3); [2, 2].
|
| 146 |
-
composed = torch.autograd.functional.jacobian(lambda x: psi_scale(phi_square(x)), point)
|
| 147 |
-
log_inner = math.log(abs(float(torch.linalg.det(inner)))) # log 4 = 1.3863.
|
| 148 |
-
log_outer = math.log(abs(float(torch.linalg.det(outer)))) # log 6 = 1.7918.
|
| 149 |
-
log_density = math.log(1.0) - (log_inner + log_outer) # log p_Y = log p_X - log|det J|.
|
| 150 |
-
show("det J_psi", 6.0, float(torch.linalg.det(outer)), quiet)
|
| 151 |
-
show("chained Jacobian, entry 11", 8.0, float(chained[0, 0]), quiet)
|
| 152 |
-
show("det of the composition", 24.0, float(torch.linalg.det(composed)), quiet)
|
| 153 |
-
show("log 4", 1.3863, log_inner, quiet)
|
| 154 |
-
show("log 6", 1.7918, log_outer, quiet)
|
| 155 |
-
show("log 4 + log 6 = log 24", 3.1781, log_inner + log_outer, quiet)
|
| 156 |
-
show("log p_Y from a unit base density", -3.1781, log_density, quiet)
|
| 157 |
-
return {"log_sum": log_inner + log_outer, "log_density": log_density}
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
# %% 06. Local invertibility and several inverse branches
|
| 161 |
-
def branches(grid: int, quiet: bool = False) -> dict:
|
| 162 |
-
"""Section 1.26: for Y = X^2 on a uniform X, the two preimages each contribute to p_Y(y)."""
|
| 163 |
-
def branch_density(y: float) -> float:
|
| 164 |
-
roots = (math.sqrt(y), -math.sqrt(y)) # The two solutions of x^2 = y inside the support.
|
| 165 |
-
return sum(0.5 / abs(2.0 * root) for root in roots) # Sum of p_X(x) / |phi'(x)| over roots.
|
| 166 |
-
|
| 167 |
-
lower = 0.01 # The integral is cut at 0.01 because the integrand grows without bound at zero.
|
| 168 |
-
step = (1.0 - lower) / grid # Midpoint spacing on [0.01, 1].
|
| 169 |
-
points = lower + (torch.arange(grid) + 0.5) * step # Midpoints of the integration interval.
|
| 170 |
-
partial = float((1.0 / (2.0 * points.sqrt())).sum() * step) # Midpoint integral of 1/(2 sqrt y).
|
| 171 |
-
show("p_Y(0.25), two branches at ±0.5", 1.0, branch_density(0.25), quiet)
|
| 172 |
-
show("p_Y(0.04), two branches at ±0.2", 2.5, branch_density(0.04), quiet)
|
| 173 |
-
show("integral of p_Y over [0.01, 1]", 1.0 - math.sqrt(lower), partial, quiet)
|
| 174 |
-
return {"at_quarter": branch_density(0.25), "partial": partial}
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
# %% 07. Scalar fields, the gradient and the directional derivative
|
| 178 |
-
def gradient(quiet: bool = False) -> dict:
|
| 179 |
-
"""Sections 1.27.1 and 1.27.2: grad f at (1, 2), its steepest direction and a directional rate."""
|
| 180 |
-
point = torch.tensor([1.0, 2.0]) # The point at which the chapter evaluates the gradient.
|
| 181 |
-
grad = torch.autograd.functional.jacobian(f_bowl, point) # (2 x_1, 4 x_2) = (2, 8); [2].
|
| 182 |
-
steepest = grad / grad.norm() # The unit vector of fastest increase; [2].
|
| 183 |
-
along_x1 = float(grad @ torch.tensor([1.0, 0.0])) # D_u f for u the first basis direction.
|
| 184 |
-
along_x2 = float(grad @ torch.tensor([0.0, 1.0])) # D_u f for u the second basis direction.
|
| 185 |
-
show("grad f(1, 2), first entry", 2.0, float(grad[0]), quiet)
|
| 186 |
-
show("grad f(1, 2), second entry", 8.0, float(grad[1]), quiet)
|
| 187 |
-
show("|grad f(1, 2)| = sqrt(68)", math.sqrt(68.0), float(grad.norm()), quiet)
|
| 188 |
-
show("steepest direction, first entry", 0.243, float(steepest[0]), quiet)
|
| 189 |
-
show("steepest direction, second entry", 0.970, float(steepest[1]), quiet)
|
| 190 |
-
show("D_u f along (1, 0)", 2.0, along_x1, quiet)
|
| 191 |
-
show("D_u f along (0, 1)", 8.0, along_x2, quiet)
|
| 192 |
-
return {"gradient": grad, "along_x1": along_x1}
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
# %% 08. Divergence
|
| 196 |
-
def divergences(quiet: bool = False) -> dict:
|
| 197 |
-
"""Section 1.29: the divergence of the chapter's three fields is -3, then 1, then 0."""
|
| 198 |
-
point = torch.tensor([1.0, 1.0]) # The divergences below are the same at every point.
|
| 199 |
-
contract = divergence(lambda x: torch.stack([-x[0], -2.0 * x[1]]), point) # -1 + (-2) = -3.
|
| 200 |
-
expand = divergence(lambda x: torch.stack([2.0 * x[0], -x[1]]), point) # 2 - 1 = 1.
|
| 201 |
-
rotate = divergence(lambda x: torch.stack([-x[1], x[0]]), point) # 0 + 0 = 0.
|
| 202 |
-
show("div of (-x_1, -2 x_2)", -3.0, contract, quiet)
|
| 203 |
-
show("div of (2 x_1, -x_2)", 1.0, expand, quiet)
|
| 204 |
-
show("div of (-x_2, x_1)", 0.0, rotate, quiet)
|
| 205 |
-
show("area factor e^{div t} at t = 0.5", 1.649, math.exp(expand * 0.5), quiet)
|
| 206 |
-
return {"contract": contract, "expand": expand, "rotate": rotate}
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# %% 09. The continuity equation on the worked flow
|
| 210 |
-
def continuity(grid: int, steps: int, quiet: bool = False) -> dict:
|
| 211 |
-
"""Section 1.30.2: v(x) = x stretches a uniform source, with p_t(x) = e^{-t} on [0, e^t].
|
| 212 |
-
|
| 213 |
-
Central differences give the continuity residual at interior points, and one Euler walk
|
| 214 |
-
accumulates the log-density rate -div v of Section 1.30.1 along a trajectory.
|
| 215 |
-
"""
|
| 216 |
-
def flux(time: float, position):
|
| 217 |
-
return math.exp(-time) * position # j_t(x) = p_t(x) v_t(x), with p_t = e^{-t} and v(x) = x.
|
| 218 |
-
|
| 219 |
-
t, h = 0.5, 1e-4 # The time of the residual check, and the central-difference step.
|
| 220 |
-
x = torch.linspace(0.1, 1.4, grid) # Interior points of the support [0, e^{0.5}]; [grid].
|
| 221 |
-
time_rate = (math.exp(-(t + h)) - math.exp(-(t - h))) / (2.0 * h) # d p_t / d t at fixed x.
|
| 222 |
-
flux_rate = (flux(t, x + h) - flux(t, x - h)) / (2.0 * h) # d (p_t v) / d x; [grid].
|
| 223 |
-
residual = float((time_rate + flux_rate).abs().max()) # The continuity equation, point by point.
|
| 224 |
-
x_t, log_p, dt = 0.5, 0.0, 1.0 / max(steps, 1) # One trajectory started inside the source.
|
| 225 |
-
for _ in range(max(steps, 1)):
|
| 226 |
-
log_p -= 1.0 * dt # d/dt log p_t(x_t) = -div v = -1 for the field v(x) = x.
|
| 227 |
-
x_t = x_t + x_t * dt # Euler step on dx_t/dt = v(x_t) = x_t.
|
| 228 |
-
show("largest continuity residual", 0.0, residual, quiet)
|
| 229 |
-
show("log p along the trajectory at t = 1", -1.0, log_p, quiet)
|
| 230 |
-
show("p_t(x_t) at t = 1", 0.3679, math.exp(log_p), quiet)
|
| 231 |
-
show("x_t at t = 1, exact 0.5e", 0.5 * math.e, x_t, quiet)
|
| 232 |
-
show("density at t = log 2", 0.5, math.exp(-math.log(2.0)), quiet)
|
| 233 |
-
show("mass at t = log 2", 1.0, 2.0 * math.exp(-math.log(2.0)), quiet)
|
| 234 |
-
show("mass at t = 1", 1.000, math.e * math.exp(-1.0), quiet)
|
| 235 |
-
return {"residual": residual, "log_p": log_p, "x_t": x_t}
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# %% 10. The report
|
| 239 |
-
def main() -> None:
|
| 240 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 241 |
-
parser.add_argument("--grid", type=int, default=1000, help="points in the numerical checks")
|
| 242 |
-
parser.add_argument("--steps", type=int, default=200, help="Euler steps along the worked flow")
|
| 243 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed, for a repeatable run")
|
| 244 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 245 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 246 |
-
args = parser.parse_args()
|
| 247 |
-
|
| 248 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same run.
|
| 249 |
-
if not args.quiet:
|
| 250 |
-
print("Chapter 1, Sections 1.22 to 1.30, worked example by worked example\n")
|
| 251 |
-
local = jacobian(args.quiet)
|
| 252 |
-
volume = determinant(args.quiet)
|
| 253 |
-
moved = change_of_variables(args.grid, args.quiet)
|
| 254 |
-
stacked = composition(args.quiet)
|
| 255 |
-
folded = branches(args.grid, args.quiet)
|
| 256 |
-
slope = gradient(args.quiet)
|
| 257 |
-
fields = divergences(args.quiet)
|
| 258 |
-
flow = continuity(args.grid, args.steps, args.quiet)
|
| 259 |
-
|
| 260 |
-
report = {
|
| 261 |
-
"J_phi(1, 0)_21 = cos(1)": float(local["jacobian"][1, 0]),
|
| 262 |
-
"det J_phi at x_1 = 2": volume["stretch"],
|
| 263 |
-
"p_Y on [1, 3] after Y = 2X + 1": moved["p_y"],
|
| 264 |
-
"p_Y inside the image of A": moved["flat"],
|
| 265 |
-
"mass after the x_1^2 map": moved["transported"],
|
| 266 |
-
"log|det J| of the composition": stacked["log_sum"],
|
| 267 |
-
"p_Y(0.25) summed over branches": folded["at_quarter"],
|
| 268 |
-
"D_u f along (1, 0)": slope["along_x1"],
|
| 269 |
-
"div of (2 x_1, -x_2)": fields["expand"],
|
| 270 |
-
"largest continuity residual": flow["residual"],
|
| 271 |
-
"log p along the flow at t = 1": flow["log_p"],
|
| 272 |
-
"x_t at t = 1 after Euler steps": flow["x_t"],
|
| 273 |
-
}
|
| 274 |
-
print_report("Calculus, Sections 1.22 to 1.30", report)
|
| 275 |
-
save_run(args.out, config=vars(args), report=report)
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 279 |
-
main() # Work every example of Sections 1.22 to 1.30 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/fields.py
DELETED
|
@@ -1,234 +0,0 @@
|
|
| 1 |
-
"""Score functions, the Laplacian and the Fokker-Planck equation.
|
| 2 |
-
|
| 3 |
-
The score of a density is the gradient of its logarithm, a vector field of the same shape as a
|
| 4 |
-
velocity field, and the normalizing constant differentiates away. The Laplacian is the divergence
|
| 5 |
-
of the gradient, and it measures the curvature that drives diffusion, so the heat equation sets the
|
| 6 |
-
rate of change of a density proportional to it. One statement about the density holds both
|
| 7 |
-
mechanisms at once, the Fokker-Planck equation, and it returns the continuity equation when the
|
| 8 |
-
noise magnitude is zero and the heat equation when the drift is.
|
| 9 |
-
|
| 10 |
-
Chapter 1, Sections 1.31 and 1.32 of the notes. Runs on the Gaussians of Section 1.31, the bowl and
|
| 11 |
-
the saddle of Section 1.32, the widening Gaussian p_t = N(0, t) of Section 1.32.1 and the expanding
|
| 12 |
-
flow v(x) = x of Section 1.30.2. Prints the chapter's number beside the computed one.
|
| 13 |
-
|
| 14 |
-
python lecture_1/fields.py # every worked example
|
| 15 |
-
python lecture_1/fields.py --steps 2000 # a finer Euler-Maruyama walk
|
| 16 |
-
python lecture_1/fields.py --quiet # the closing report only
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import argparse
|
| 23 |
-
import math
|
| 24 |
-
|
| 25 |
-
import torch
|
| 26 |
-
|
| 27 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 28 |
-
|
| 29 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
SCORE_POINTS = torch.tensor([3.0, -2.0, 0.0]) # The three states scored in Section 1.31.
|
| 33 |
-
WIDE_MEAN, WIDE_VARIANCE = 1.0, 4.0 # The general Gaussian N(mu, sigma^2) of Section 1.31.
|
| 34 |
-
DRIFT = 0.5 # The constant drift b_t(x) = 0.5 of the combined process in Section 1.32.2.
|
| 35 |
-
NOISE = 1.0 # The noise magnitude sigma_t, so that D = sigma_t^2 / 2 = 0.5 as in Section 1.32.1.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 42 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 43 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 44 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 45 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 46 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 47 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def log_gaussian(x: torch.Tensor, mu: float = 0.0, variance: float = 1.0) -> torch.Tensor:
|
| 51 |
-
"""log N(x; mu, sigma^2) = -(x - mu)^2 / (2 sigma^2) - (1/2) log(2 pi sigma^2)."""
|
| 52 |
-
normalizer = 0.5 * math.log(2.0 * math.pi * variance) # The term with no x in it.
|
| 53 |
-
return -((x - mu) ** 2) / (2.0 * variance) - normalizer
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def gaussian(t: float, x: torch.Tensor) -> torch.Tensor:
|
| 57 |
-
"""p_t = N(0, t) in one dimension, the widening Gaussian of Section 1.32.1."""
|
| 58 |
-
return (-(x ** 2) / (2.0 * t)).exp() / math.sqrt(2.0 * math.pi * t)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def score(log_density, x: torch.Tensor) -> torch.Tensor:
|
| 62 |
-
"""Section 1.31: s(x) = grad_x log p(x), taken by automatic differentiation."""
|
| 63 |
-
point = x.detach().clone().requires_grad_(True) # A leaf to differentiate against; [N].
|
| 64 |
-
total = log_density(point).sum() # The points do not interact, so one pass gives every score.
|
| 65 |
-
gradient, = torch.autograd.grad(total, point) # d/dx log p at each point; [N].
|
| 66 |
-
return gradient
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def laplacian(field, x: torch.Tensor) -> float:
|
| 70 |
-
"""Section 1.32: the Laplacian is the divergence of the gradient, the trace of the Hessian."""
|
| 71 |
-
return float(torch.autograd.functional.hessian(field, x).trace())
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def fokker_planck_residual(p, b, sigma: float, x: torch.Tensor, t: float) -> float:
|
| 75 |
-
"""Section 1.32.2: d p_t / d t + div(b_t p_t) - (1/2) sigma_t^2 Laplacian p_t, by differences.
|
| 76 |
-
|
| 77 |
-
Setting sigma to zero leaves the continuity equation of Section 1.30, and setting b to zero
|
| 78 |
-
leaves the heat equation of Section 1.32.1, so one residual covers all three statements.
|
| 79 |
-
"""
|
| 80 |
-
h = 1e-4 # The central-difference step, small enough for five digits in double precision.
|
| 81 |
-
time_rate = (p(t + h, x) - p(t - h, x)) / (2.0 * h) # d p_t(x) / d t at fixed x; [grid].
|
| 82 |
-
flux = (b(x + h) * p(t, x + h) - b(x - h) * p(t, x - h)) / (2.0 * h) # div(b_t p_t); [grid].
|
| 83 |
-
curvature = (p(t, x + h) - 2.0 * p(t, x) + p(t, x - h)) / h ** 2 # Laplacian p_t; [grid].
|
| 84 |
-
return float((time_rate + flux - 0.5 * sigma ** 2 * curvature).abs().max())
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# %% 02. The score of a Gaussian
|
| 88 |
-
def scores(quiet: bool = False) -> dict:
|
| 89 |
-
"""Section 1.31: the score of N(0, 1) is -x, and of N(mu, sigma^2) it is -(x - mu)/sigma^2."""
|
| 90 |
-
standard = score(log_gaussian, SCORE_POINTS) # s(x) at x = 3, -2 and 0; [3].
|
| 91 |
-
closed_form = -SCORE_POINTS # The chapter's result, with the log(2 pi) term differentiated away.
|
| 92 |
-
unnormalized = score(lambda z: -(z ** 2) / 2.0, SCORE_POINTS) # The same density without its Z.
|
| 93 |
-
wide = score(lambda z: log_gaussian(z, WIDE_MEAN, WIDE_VARIANCE), torch.tensor([5.0])) # [1].
|
| 94 |
-
show("s(x) at x = 3", -3.0, float(standard[0]), quiet)
|
| 95 |
-
show("s(x) at x = -2", 2.0, float(standard[1]), quiet)
|
| 96 |
-
show("s(x) at the mode x = 0", 0.0, float(standard[2]), quiet)
|
| 97 |
-
show("largest |s(x) + x|", 0.0, float((standard - closed_form).abs().max()), quiet)
|
| 98 |
-
show("largest change from dropping Z", 0.0, float((unnormalized - standard).abs().max()), quiet)
|
| 99 |
-
show("s(5) under N(1, 4)", -1.0, float(wide[0]), quiet)
|
| 100 |
-
return {"standard": standard, "wide": float(wide[0])}
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# %% 03. The Laplacian of a scalar field
|
| 104 |
-
def curvatures(quiet: bool = False) -> dict:
|
| 105 |
-
"""Section 1.32: the bowl x_1^2 + 3 x_2^2 has Laplacian 8, and the saddle x_1^2 - x_2^2 has 0."""
|
| 106 |
-
def bowl(x: torch.Tensor) -> torch.Tensor:
|
| 107 |
-
return x[0] ** 2 + 3.0 * x[1] ** 2 # The field whose two curvatures are 2 and 6.
|
| 108 |
-
|
| 109 |
-
def saddle(x: torch.Tensor) -> torch.Tensor:
|
| 110 |
-
return x[0] ** 2 - x[1] ** 2 # The field whose two curvatures cancel.
|
| 111 |
-
|
| 112 |
-
point = torch.tensor([1.0, 2.0]) # The Laplacians below are the same at every point.
|
| 113 |
-
second = torch.autograd.functional.hessian(bowl, point) # The two pure second derivatives; [2, 2].
|
| 114 |
-
h = 1e-3 # The spacing of the four neighbors used for the average reading of the Laplacian.
|
| 115 |
-
steps = torch.tensor([[h, 0.0], [-h, 0.0], [0.0, h], [0.0, -h]]) # The four offsets; [4, 2].
|
| 116 |
-
neighbors = sum(bowl(point + step) for step in steps) - 4.0 * bowl(point) # Above the center.
|
| 117 |
-
show("d^2 f / d x_1^2", 2.0, float(second[0, 0]), quiet)
|
| 118 |
-
show("d^2 f / d x_2^2", 6.0, float(second[1, 1]), quiet)
|
| 119 |
-
show("Laplacian of x_1^2 + 3 x_2^2", 8.0, laplacian(bowl, point), quiet)
|
| 120 |
-
show("Laplacian from the four neighbors", 8.0, float(neighbors) / h ** 2, quiet)
|
| 121 |
-
show("Laplacian of the saddle", 0.0, laplacian(saddle, point), quiet)
|
| 122 |
-
return {"bowl": laplacian(bowl, point), "saddle": laplacian(saddle, point)}
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# %% 04. The heat equation
|
| 126 |
-
def heat(grid: int, quiet: bool = False) -> dict:
|
| 127 |
-
"""Section 1.32.1: at D = 1/2 the Gaussian N(0, t) solves d p_t / d t = D Laplacian p_t."""
|
| 128 |
-
diffusion = 0.5 * NOISE ** 2 # D = sigma_t^2 / 2, the coefficient the chapter takes as 1/2.
|
| 129 |
-
center = 1.0 / math.sqrt(2.0 * math.pi) # p_1(0), the height at the middle at t = 1.
|
| 130 |
-
time_rate = -0.5 / math.sqrt(2.0 * math.pi) # d p_t(0)/dt = -(1/2)(2 pi)^{-1/2} t^{-3/2} at t = 1.
|
| 131 |
-
curvature = laplacian(lambda z: gaussian(1.0, z), torch.tensor([0.0])) # Laplacian p_1 at 0.
|
| 132 |
-
x = torch.linspace(-3.0, 3.0, grid) # Points covering the bulk of the mass at t = 1; [grid].
|
| 133 |
-
residual = fokker_planck_residual(
|
| 134 |
-
gaussian, torch.zeros_like, NOISE, x, 1.0) # The heat equation, with no drift.
|
| 135 |
-
show("p_1(0) = 1 / sqrt(2 pi)", 0.39894, center, quiet)
|
| 136 |
-
show("d p_t(0) / d t at t = 1", -0.19947, time_rate, quiet)
|
| 137 |
-
show("Laplacian of p_1 at 0", -0.39894, curvature, quiet)
|
| 138 |
-
show("-p_1(0) / sigma^2 at sigma^2 = 1", -0.39894, -center / 1.0, quiet)
|
| 139 |
-
show("D times the Laplacian of p_1 at 0", -0.19947, diffusion * curvature, quiet)
|
| 140 |
-
show("largest heat-equation residual", 0.0, residual, quiet)
|
| 141 |
-
return {"center": center, "time_rate": time_rate, "curvature": curvature,
|
| 142 |
-
"flow": diffusion * curvature, "residual": residual}
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
# %% 05. Drift and diffusion together
|
| 146 |
-
def fokker_planck(grid: int, quiet: bool = False) -> dict:
|
| 147 |
-
"""Section 1.32.2: a drift that translates and a noise that widens, in one equation.
|
| 148 |
-
|
| 149 |
-
With a constant drift b and sigma_t = 1 the density N(bt, t) solves the equation, since the
|
| 150 |
-
drift moves the center and the noise grows the variance at the rate of the heat equation.
|
| 151 |
-
"""
|
| 152 |
-
def translating(t: float, x: torch.Tensor) -> torch.Tensor:
|
| 153 |
-
return gaussian(t, x - DRIFT * t) # N(bt, t), the center carried along by the drift.
|
| 154 |
-
|
| 155 |
-
def expanding(t: float, x: torch.Tensor) -> torch.Tensor:
|
| 156 |
-
return torch.full_like(x, math.exp(-t)) # p_t = e^{-t} on [0, e^t], Section 1.30.2.
|
| 157 |
-
|
| 158 |
-
x = torch.linspace(-3.0, 3.0, grid) # Points across the translating Gaussian; [grid].
|
| 159 |
-
both = fokker_planck_residual(translating, lambda z: torch.full_like(z, DRIFT), NOISE, x, 1.0)
|
| 160 |
-
inside = torch.linspace(0.1, 1.4, grid) # Interior points of the support at t = 0.5; [grid].
|
| 161 |
-
transport = fokker_planck_residual(expanding, lambda z: z, 0.0, inside, 0.5) # sigma_t = 0.
|
| 162 |
-
center = DRIFT * 1.0 # The mean of p_t at t = 1, which the drift alone sets.
|
| 163 |
-
show("center of p_t at t = 1", 0.5, center, quiet)
|
| 164 |
-
show("largest residual, drift and noise", 0.0, both, quiet)
|
| 165 |
-
show("largest residual at sigma_t = 0", 0.0, transport, quiet)
|
| 166 |
-
return {"both": both, "transport": transport, "center": center}
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
# %% 06. The process behind the density
|
| 170 |
-
def euler_maruyama(samples: int, steps: int, quiet: bool = False) -> dict:
|
| 171 |
-
"""Section 1.32.2: dX_t = b_t(X_t) dt + sigma_t dW_t, simulated, against the density above.
|
| 172 |
-
|
| 173 |
-
The variance of p_t = N(bt, t) equals the elapsed time, so the sample mean and the sample
|
| 174 |
-
variance of the walk track bt and t. Both gaps shrink as --samples grows.
|
| 175 |
-
"""
|
| 176 |
-
dt = 1.0 / max(steps, 1) # The time increment of one Euler-Maruyama step.
|
| 177 |
-
x_t = torch.zeros(max(samples, 1)) # Every path starts at the origin, so p_0 is a point mass.
|
| 178 |
-
mean_gap, variance_gap = 0.0, 0.0 # The largest departure from bt and from t along the walk.
|
| 179 |
-
for step in range(max(steps, 1)):
|
| 180 |
-
noise = torch.randn_like(x_t) # The Brownian increment dW_t, scaled by sqrt(dt); [samples].
|
| 181 |
-
x_t = x_t + DRIFT * dt + NOISE * math.sqrt(dt) * noise # One step of the chapter's SDE.
|
| 182 |
-
elapsed = (step + 1) * dt # The time reached after this step.
|
| 183 |
-
mean_gap = max(mean_gap, abs(float(x_t.mean()) - DRIFT * elapsed))
|
| 184 |
-
variance_gap = max(variance_gap, abs(float(x_t.var(unbiased=False)) - elapsed))
|
| 185 |
-
show("sample mean at t = 1, exact 0.5", 0.5, float(x_t.mean()), quiet)
|
| 186 |
-
show("sample variance at t = 1, exact 1", 1.0, float(x_t.var(unbiased=False)), quiet)
|
| 187 |
-
show("largest |sample mean - b t|", 0.0, mean_gap, quiet)
|
| 188 |
-
show("largest |sample variance - t|", 0.0, variance_gap, quiet)
|
| 189 |
-
return {"mean": float(x_t.mean()), "variance": float(x_t.var(unbiased=False)),
|
| 190 |
-
"mean_gap": mean_gap, "variance_gap": variance_gap, "steps": max(steps, 1)}
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
# %% 07. The report
|
| 194 |
-
def main() -> None:
|
| 195 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 196 |
-
parser.add_argument("--grid", type=int, default=1000, help="points in the numerical checks")
|
| 197 |
-
parser.add_argument("--steps", type=int, default=200, help="Euler-Maruyama steps over [0, 1]")
|
| 198 |
-
parser.add_argument("--samples", type=int, default=20000, help="paths simulated along the SDE")
|
| 199 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed for the simulated paths")
|
| 200 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 201 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 202 |
-
args = parser.parse_args()
|
| 203 |
-
|
| 204 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same walk.
|
| 205 |
-
if not args.quiet:
|
| 206 |
-
print("Chapter 1, Sections 1.31 and 1.32, worked example by worked example\n")
|
| 207 |
-
uphill = scores(args.quiet)
|
| 208 |
-
bowl = curvatures(args.quiet)
|
| 209 |
-
spreading = heat(args.grid, args.quiet)
|
| 210 |
-
combined = fokker_planck(args.grid, args.quiet)
|
| 211 |
-
walk = euler_maruyama(args.samples, args.steps, args.quiet)
|
| 212 |
-
|
| 213 |
-
report = {
|
| 214 |
-
"s(x) at x = 3": float(uphill["standard"][0]),
|
| 215 |
-
"s(5) under N(1, 4)": uphill["wide"],
|
| 216 |
-
"Laplacian of x_1^2 + 3 x_2^2": bowl["bowl"],
|
| 217 |
-
"Laplacian of the saddle": bowl["saddle"],
|
| 218 |
-
"p_1(0)": spreading["center"],
|
| 219 |
-
"d p_t(0) / d t at t = 1": spreading["time_rate"],
|
| 220 |
-
"Laplacian of p_1 at 0": spreading["curvature"],
|
| 221 |
-
"D times that Laplacian": spreading["flow"],
|
| 222 |
-
"heat-equation residual": spreading["residual"],
|
| 223 |
-
"Fokker-Planck residual": combined["both"],
|
| 224 |
-
"residual at sigma_t = 0": combined["transport"],
|
| 225 |
-
"Euler-Maruyama steps": walk["steps"],
|
| 226 |
-
"sample variance at t = 1": walk["variance"],
|
| 227 |
-
"largest |sample variance - t|": walk["variance_gap"],
|
| 228 |
-
}
|
| 229 |
-
print_report("Fields, Sections 1.31 and 1.32", report)
|
| 230 |
-
save_run(args.out, config=vars(args), report=report)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 234 |
-
main() # Work every example of Sections 1.31 and 1.32 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/information.py
DELETED
|
@@ -1,219 +0,0 @@
|
|
| 1 |
-
"""Entropy, cross-entropy, KL divergence and maximum likelihood, in nats.
|
| 2 |
-
|
| 3 |
-
Information content measures one observation, entropy averages it under the distribution that
|
| 4 |
-
produced it, cross-entropy averages it under a second distribution supplying the probabilities,
|
| 5 |
-
and the KL divergence is the difference between those two averages. Minimizing a negative
|
| 6 |
-
log-likelihood over a model is therefore minimizing a KL divergence to the data distribution.
|
| 7 |
-
|
| 8 |
-
Chapter 1, Sections 1.33, 1.34 and 1.35 of the notes. Runs on the pair p = (0.8, 0.2) and
|
| 9 |
-
q = (0.6, 0.4) of Section 1.33.3 and on the ten DNA bases of Section 1.35. Prints the chapter's
|
| 10 |
-
number beside the computed one. Logarithms are natural, so every quantity is in nats.
|
| 11 |
-
|
| 12 |
-
python lecture_1/information.py # every worked example
|
| 13 |
-
python lecture_1/information.py --samples 200000 # a tighter Monte Carlo cross-entropy
|
| 14 |
-
python lecture_1/information.py --quiet # the closing report only
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import argparse
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
P_REF = torch.tensor([0.8, 0.2]) # The reference distribution p of Sections 1.33.3 and 1.34.
|
| 31 |
-
Q_MODEL = torch.tensor([0.6, 0.4]) # The comparison distribution q scored against those samples.
|
| 32 |
-
BASE_COUNTS = torch.tensor([4.0, 3.0, 2.0, 1.0]) # n(A), n(C), n(G), n(T) over ten DNA bases.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 39 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 40 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 41 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 42 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 43 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 44 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def entropy(p: torch.Tensor) -> float:
|
| 48 |
-
"""H(p) = -sum_x p(x) log p(x), the expected information in one sample drawn from p."""
|
| 49 |
-
return float(-(p * p.clamp_min(1e-300).log()).sum()) # Clamp keeps 0 log 0 at zero.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def cross_entropy(p: torch.Tensor, q: torch.Tensor) -> float:
|
| 53 |
-
"""H(p, q) = -sum_x p(x) log q(x), the cost of scoring samples from p with the values of q."""
|
| 54 |
-
return float(-(p * q.log()).sum()) # The weights come from p and the probabilities from q.
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def divergence(p: torch.Tensor, q: torch.Tensor) -> float:
|
| 58 |
-
"""KL(p || q) = sum_x p(x) log(p(x) / q(x)), the expected log ratio taken under p."""
|
| 59 |
-
return float((p * (p / q).log()).sum()) # Zero exactly when q matches p at every state.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# %% 02. Information content
|
| 63 |
-
def information(quiet: bool = False) -> dict:
|
| 64 |
-
"""Section 1.33.1: I(x) = -log p(x), so a rarer outcome carries more information."""
|
| 65 |
-
common = -math.log(0.5) # An outcome of probability one half.
|
| 66 |
-
rare = -math.log(0.01) # An outcome fifty times less likely.
|
| 67 |
-
certain = -math.log(1.0) # A certain event, which has zero information.
|
| 68 |
-
show("I(x) at p(x) = 0.5", 0.693, common, quiet)
|
| 69 |
-
show("I(x) at p(x) = 0.01", 4.605, rare, quiet)
|
| 70 |
-
show("I(x) at p(x) = 1", 0.0, certain, quiet)
|
| 71 |
-
show("ratio of the two surprises", 6.6, rare / common, quiet)
|
| 72 |
-
return {"common": common, "rare": rare}
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# %% 03. Entropy
|
| 76 |
-
def entropies(quiet: bool = False) -> dict:
|
| 77 |
-
"""Section 1.33.2: entropy is high when the mass is spread and zero when it is all on one state."""
|
| 78 |
-
skewed = entropy(torch.tensor([0.9, 0.1])) # 0.09482 + 0.23026 nats.
|
| 79 |
-
balanced = entropy(torch.tensor([0.5, 0.5])) # log 2, the largest value on two states.
|
| 80 |
-
deterministic = entropy(torch.tensor([1.0, 0.0])) # All mass on one state, so 1 log 1 = 0.
|
| 81 |
-
uniform_four = entropy(torch.full((4,), 0.25)) # log K at K = 4, proved in Section 1.37.2.
|
| 82 |
-
show("H((0.9, 0.1))", 0.325, skewed, quiet)
|
| 83 |
-
show("H((0.5, 0.5)) = log 2", 0.693, balanced, quiet)
|
| 84 |
-
show("H of a point mass", 0.0, deterministic, quiet)
|
| 85 |
-
show("H of the uniform on 4 states", 1.386, uniform_four, quiet)
|
| 86 |
-
return {"skewed": skewed, "balanced": balanced, "uniform_four": uniform_four}
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
# %% 04. Cross-entropy
|
| 90 |
-
def cross_entropies(quiet: bool = False) -> dict:
|
| 91 |
-
"""Section 1.33.3: scoring samples from p with the probabilities of q costs more than H(p)."""
|
| 92 |
-
cost = cross_entropy(P_REF, Q_MODEL) # 0.40866 + 0.18326 nats.
|
| 93 |
-
own = entropy(P_REF) # 0.17851 + 0.32189 nats, the irreducible part of that cost.
|
| 94 |
-
same = cross_entropy(P_REF, P_REF) # H(p, p) recovers the entropy exactly.
|
| 95 |
-
show("H(p, q)", 0.592, cost, quiet)
|
| 96 |
-
show("H(p)", 0.500, own, quiet)
|
| 97 |
-
show("excess cost H(p, q) - H(p)", 0.092, cost - own, quiet)
|
| 98 |
-
show("H(p, p) - H(p)", 0.0, same - own, quiet)
|
| 99 |
-
return {"cost": cost, "own": own, "excess": cost - own}
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
# %% 05. Kullback-Leibler divergence
|
| 103 |
-
def kl(samples: int, quiet: bool = False) -> dict:
|
| 104 |
-
"""Section 1.34: the split H(p, q) = H(p) + KL(p || q), nonnegativity, and the asymmetry."""
|
| 105 |
-
forward = divergence(P_REF, Q_MODEL) # 0.23015 - 0.13863 nats, the price of using q.
|
| 106 |
-
reverse = divergence(Q_MODEL, P_REF) # -0.17261 + 0.27726 nats, the other direction.
|
| 107 |
-
split = entropy(P_REF) + forward # The boxed identity, which must return H(p, q).
|
| 108 |
-
logits = torch.randn(max(samples, 1), 2, 8) # Random logits for pairs of distributions.
|
| 109 |
-
pairs = torch.softmax(logits, dim=-1) # Two distributions over eight states per row; [N, 2, 8].
|
| 110 |
-
drawn = (pairs[:, 0] * (pairs[:, 0] / pairs[:, 1]).log()).sum(-1) # KL for every pair; [N].
|
| 111 |
-
violation = float((-drawn).clamp_min(0.0).max()) # The largest breach of KL >= 0 among them.
|
| 112 |
-
t = torch.linspace(0.01, 5.0, 500) # The range on which the inequality log t <= t - 1 is checked.
|
| 113 |
-
show("KL(p || q)", 0.0915, forward, quiet)
|
| 114 |
-
show("KL(q || p)", 0.1046, reverse, quiet)
|
| 115 |
-
show("H(p) + KL(p || q) = H(p, q)", 0.592, split, quiet)
|
| 116 |
-
show("KL(p || p)", 0.0, divergence(P_REF, P_REF), quiet)
|
| 117 |
-
show("reverse over forward, percent", 14.0, 100.0 * (reverse / forward - 1.0), quiet)
|
| 118 |
-
show("largest log t - (t - 1)", 0.0, float((t.log() - (t - 1.0)).max()), quiet)
|
| 119 |
-
show(f"largest KL breach over {len(drawn)} pairs", 0.0, violation, quiet)
|
| 120 |
-
return {"forward": forward, "reverse": reverse, "split": split,
|
| 121 |
-
"smallest": float(drawn.min()), "violation": violation}
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
# %% 06. Cross-entropy by Monte Carlo
|
| 125 |
-
def monte_carlo(samples: int, repeats: int, quiet: bool = False) -> dict:
|
| 126 |
-
"""Section 1.5.3 applied to Section 1.33.3: H(p, q) is an expectation, so an average estimates it.
|
| 127 |
-
|
| 128 |
-
We draw states from p and average -log q at the drawn states. The estimate moves with --seed
|
| 129 |
-
and with --samples, and the exact value it approaches does not. Repeating a ten-sample estimate
|
| 130 |
-
``repeats`` times measures the sd that Section 1.5.3 predicts for a ten-sample average.
|
| 131 |
-
"""
|
| 132 |
-
exact = cross_entropy(P_REF, Q_MODEL) # The target of the estimator, 0.592 nats.
|
| 133 |
-
draws = torch.multinomial(P_REF, max(samples, 10), replacement=True) # [N] states drawn from p.
|
| 134 |
-
surprise = -Q_MODEL.log() # The two values -log q(x) the estimator averages over; [2].
|
| 135 |
-
scored = surprise[draws] # The summand -log q(x^(n)) at each drawn state; [N].
|
| 136 |
-
estimate, spread = float(scored.mean()), float(scored.std()) # The average and its per-sample sd.
|
| 137 |
-
error = spread / math.sqrt(len(draws)) # The sd of the average, which falls like 1/sqrt(N).
|
| 138 |
-
show("H(p, q) exactly", 0.592, exact, quiet)
|
| 139 |
-
if not quiet:
|
| 140 |
-
label = f"H(p, q) from {len(draws)} samples"
|
| 141 |
-
print(f" {label:<36s} exact {exact:>11.5f} estimate {estimate:>11.5f}"
|
| 142 |
-
f" sd {error:.1e}")
|
| 143 |
-
rounds = max(repeats, 2) # Two averages is the fewest a standard deviation can be taken over.
|
| 144 |
-
repeated = torch.multinomial(P_REF, 10 * rounds, replacement=True) # [10 * rounds] states.
|
| 145 |
-
averages = surprise[repeated].view(rounds, 10).mean(dim=1) # One ten-sample estimate per round.
|
| 146 |
-
exact_spread = float((P_REF @ (surprise - exact).square()).sqrt()) # sd of one scored draw.
|
| 147 |
-
predicted = exact_spread / math.sqrt(10.0) # The sd of a ten-sample average, Section 1.5.3.
|
| 148 |
-
show(f"sd of {rounds} ten-sample estimates", predicted, float(averages.std()), quiet)
|
| 149 |
-
return {"exact": exact, "estimate": estimate, "error": error, "draws": len(draws),
|
| 150 |
-
"predicted": predicted, "measured": float(averages.std()), "rounds": rounds}
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
# %% 07. Maximum likelihood as KL minimization
|
| 154 |
-
def maximum_likelihood(quiet: bool = False) -> dict:
|
| 155 |
-
"""Section 1.35: the empirical loss at the frequencies is their entropy, and the gap to any
|
| 156 |
-
other model is the KL divergence from the frequencies to it."""
|
| 157 |
-
frequencies = BASE_COUNTS / BASE_COUNTS.sum() # p_hat = (0.4, 0.3, 0.2, 0.1) over A, C, G, T.
|
| 158 |
-
uniform = torch.full((4,), 0.25) # The categorical model that spreads the mass evenly.
|
| 159 |
-
at_frequencies = cross_entropy(frequencies, frequencies) # The empirical loss at p_hat.
|
| 160 |
-
at_uniform = cross_entropy(frequencies, uniform) # The same data scored by the uniform model.
|
| 161 |
-
gap = at_uniform - at_frequencies # The reducible part of the cost, which is a KL divergence.
|
| 162 |
-
terms = -frequencies * frequencies.log() # The four terms the chapter adds up; [4].
|
| 163 |
-
show("term for A, 0.4 log(1/0.4)", 0.36652, float(terms[0]), quiet)
|
| 164 |
-
show("term for C, 0.3 log(1/0.3)", 0.36119, float(terms[1]), quiet)
|
| 165 |
-
show("term for G, 0.2 log(1/0.2)", 0.32189, float(terms[2]), quiet)
|
| 166 |
-
show("term for T, 0.1 log(1/0.1)", 0.23026, float(terms[3]), quiet)
|
| 167 |
-
show("loss at the frequencies", 1.280, at_frequencies, quiet)
|
| 168 |
-
show("loss under the uniform model", 1.386, at_uniform, quiet)
|
| 169 |
-
show("gap between the two losses", 0.1064, gap, quiet)
|
| 170 |
-
show("KL(p_hat || uniform)", 0.1064, divergence(frequencies, uniform), quiet)
|
| 171 |
-
return {"frequencies": frequencies, "at_frequencies": at_frequencies,
|
| 172 |
-
"at_uniform": at_uniform, "gap": gap}
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# %% 08. The report
|
| 176 |
-
def main() -> None:
|
| 177 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 178 |
-
parser.add_argument("--samples", type=int, default=20000, help="draws in the Monte Carlo checks")
|
| 179 |
-
parser.add_argument("--steps", type=int, default=5000, help="repeats of the ten-sample estimate")
|
| 180 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed for the Monte Carlo draws")
|
| 181 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 182 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 183 |
-
args = parser.parse_args()
|
| 184 |
-
|
| 185 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same draws.
|
| 186 |
-
if not args.quiet:
|
| 187 |
-
print("Chapter 1, Sections 1.33 to 1.35, worked example by worked example\n")
|
| 188 |
-
surprise = information(args.quiet)
|
| 189 |
-
spread = entropies(args.quiet)
|
| 190 |
-
cost = cross_entropies(args.quiet)
|
| 191 |
-
gap = kl(args.samples, args.quiet)
|
| 192 |
-
estimate = monte_carlo(args.samples, args.steps, args.quiet)
|
| 193 |
-
fitted = maximum_likelihood(args.quiet)
|
| 194 |
-
|
| 195 |
-
report = {
|
| 196 |
-
"I(x) at p(x) = 0.01, nats": surprise["rare"],
|
| 197 |
-
"H((0.9, 0.1)), nats": spread["skewed"],
|
| 198 |
-
"H((0.5, 0.5)), nats": spread["balanced"],
|
| 199 |
-
"H(p), nats": cost["own"],
|
| 200 |
-
"H(p, q), nats": cost["cost"],
|
| 201 |
-
"KL(p || q), nats": gap["forward"],
|
| 202 |
-
"KL(q || p), nats": gap["reverse"],
|
| 203 |
-
"smallest KL over random pairs": gap["smallest"],
|
| 204 |
-
"largest KL breach, random pairs": gap["violation"],
|
| 205 |
-
"H(p, q) from samples": estimate["estimate"],
|
| 206 |
-
"Monte Carlo samples drawn": estimate["draws"],
|
| 207 |
-
"ten-sample estimates taken": estimate["rounds"],
|
| 208 |
-
"sd of those estimates": estimate["measured"],
|
| 209 |
-
"sd Section 1.5.3 predicts": estimate["predicted"],
|
| 210 |
-
"empirical loss at p_hat, nats": fitted["at_frequencies"],
|
| 211 |
-
"loss under the uniform, nats": fitted["at_uniform"],
|
| 212 |
-
"KL(p_hat || uniform), nats": fitted["gap"],
|
| 213 |
-
}
|
| 214 |
-
print_report("Information, Sections 1.33 to 1.35", report)
|
| 215 |
-
save_run(args.out, config=vars(args), report=report)
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 219 |
-
main() # Work every example of Sections 1.33 to 1.35 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/linear_algebra.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
| 1 |
-
"""The simplex, the softmax, transition matrices and the matrix exponential.
|
| 2 |
-
|
| 3 |
-
A distribution over K states is a point of the simplex, a softmax maps unrestricted logits onto
|
| 4 |
-
that simplex, and a row-stochastic matrix moves one point of it to the next. Along an
|
| 5 |
-
eigendirection a matrix power is a power of a number, and that is where the stationary
|
| 6 |
-
distribution, the rate at which a chain reaches it, and the matrix exponential of continuous time
|
| 7 |
-
all come from.
|
| 8 |
-
|
| 9 |
-
Chapter 1, Sections 1.12, 1.13, 1.15, 1.16, 1.17 and 1.18 of the notes. Runs on the three-base
|
| 10 |
-
simplex of Section 1.12.2, the two-state chain of Section 1.15.1 and the symmetric matrix
|
| 11 |
-
A = [[2, 1], [1, 2]] of Section 1.16. Prints the chapter's number beside the computed one.
|
| 12 |
-
|
| 13 |
-
python lecture_1/linear_algebra.py # every worked example
|
| 14 |
-
python lecture_1/linear_algebra.py --steps 40 # run the chain further toward p*
|
| 15 |
-
python lecture_1/linear_algebra.py --quiet # the closing report only
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import argparse
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
P_BASES = torch.tensor([0.5, 0.3, 0.2]) # The three-state distribution over A, C, G; Section 1.12.2.
|
| 32 |
-
H_BASES = torch.tensor([1.0, 0.0, 4.0]) # Hydrogen-bond contacts per base, the property h.
|
| 33 |
-
Q_BASES = torch.tensor([0.1, 0.1, 0.8]) # The second point of the simplex, mixed with P_BASES.
|
| 34 |
-
LOGITS = torch.tensor([2.0, 1.0, 0.0]) # The logit vector z of Section 1.13.1.
|
| 35 |
-
P_CHAIN = torch.tensor([[0.8, 0.2], # Row 1: stay in state one with 0.8, move to state two with 0.2.
|
| 36 |
-
[0.3, 0.7]]) # Row 2: move to state one with 0.3, stay with 0.7.
|
| 37 |
-
A_MATRIX = torch.tensor([[2.0, 1.0], [1.0, 2.0]]) # The running matrix of Sections 1.16 and 1.18.
|
| 38 |
-
X_START = torch.tensor([3.0, 1.0]) # The initial vector x_0 = 2 v_1 + v_2 of Section 1.16.2.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 45 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 46 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 47 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 48 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 49 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 50 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# %% 02. Probability vectors and the simplex
|
| 54 |
-
def simplex(quiet: bool = False) -> dict:
|
| 55 |
-
"""Sections 1.12.2 and 1.12.3: expectation as a dot product, and a mixture of two points."""
|
| 56 |
-
expectation = float(P_BASES @ H_BASES) # E[h(X)] = p^T h = 0.5 + 0 + 0.8 = 1.3.
|
| 57 |
-
mixture = 0.5 * P_BASES + 0.5 * Q_BASES # r = lambda p + (1 - lambda) q at lambda = 0.5; [3].
|
| 58 |
-
corners = 0.3 * torch.tensor([1.0, 0.0]) + 0.7 * torch.tensor([0.0, 1.0]) # Two-state mixture.
|
| 59 |
-
show("sum_i p_i", 1.0, float(P_BASES.sum()), quiet)
|
| 60 |
-
show("E[h(X)] = p^T h", 1.3, expectation, quiet)
|
| 61 |
-
show("mixture r_1 at lambda = 0.5", 0.30, float(mixture[0]), quiet)
|
| 62 |
-
show("mixture r_2 at lambda = 0.5", 0.20, float(mixture[1]), quiet)
|
| 63 |
-
show("mixture r_3 at lambda = 0.5", 0.50, float(mixture[2]), quiet)
|
| 64 |
-
show("sum_i r_i, still a distribution", 1.0, float(mixture.sum()), quiet)
|
| 65 |
-
show("0.3 e_1 + 0.7 e_2, first entry", 0.3, float(corners[0]), quiet)
|
| 66 |
-
return {"expectation": expectation, "mixture": mixture}
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# %% 03. Logits and the softmax
|
| 70 |
-
def softmax(quiet: bool = False) -> dict:
|
| 71 |
-
"""Section 1.13.1: softmax(z)_i = e^{z_i} / sum_j e^{z_j} on the logits z = (2, 1, 0)."""
|
| 72 |
-
exponentials = LOGITS.exp() # e^2, e^1, e^0; [3].
|
| 73 |
-
partition = float(exponentials.sum()) # The normalizing sum, 7.389 + 2.718 + 1.000.
|
| 74 |
-
probabilities = exponentials / partition # The point of the simplex the logits name; [3].
|
| 75 |
-
shifted = torch.softmax(LOGITS + 5.0, dim=0) # Adding a constant c cancels in the ratio.
|
| 76 |
-
show("e^{z_1}", 7.389, float(exponentials[0]), quiet)
|
| 77 |
-
show("e^{z_2}", 2.718, float(exponentials[1]), quiet)
|
| 78 |
-
show("e^{z_3}", 1.000, float(exponentials[2]), quiet)
|
| 79 |
-
show("partition function sum_j e^{z_j}", 11.107, partition, quiet)
|
| 80 |
-
show("softmax(z)_1", 0.665, float(probabilities[0]), quiet)
|
| 81 |
-
show("softmax(z)_2", 0.245, float(probabilities[1]), quiet)
|
| 82 |
-
show("softmax(z)_3", 0.090, float(probabilities[2]), quiet)
|
| 83 |
-
show("sum_i softmax(z)_i", 1.0, float(probabilities.sum()), quiet)
|
| 84 |
-
show("shift invariance, largest change", 0.0, float((shifted - probabilities).abs().max()), quiet)
|
| 85 |
-
return {"probabilities": probabilities, "partition": partition}
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# %% 04. A probability vector under a transition matrix
|
| 89 |
-
def one_transition(quiet: bool = False) -> dict:
|
| 90 |
-
"""Section 1.15.1: p_{t+1} = P^T p_t moves an even split to (0.55, 0.45) in one step."""
|
| 91 |
-
p_t = torch.tensor([0.5, 0.5]) # The current distribution over the two states; [2].
|
| 92 |
-
p_next = P_CHAIN.T @ p_t # Each new entry collects the mass arriving from every state; [2].
|
| 93 |
-
show("row sums of P, row 1", 1.0, float(P_CHAIN[0].sum()), quiet)
|
| 94 |
-
show("row sums of P, row 2", 1.0, float(P_CHAIN[1].sum()), quiet)
|
| 95 |
-
show("p_{t+1}(1)", 0.55, float(p_next[0]), quiet)
|
| 96 |
-
show("p_{t+1}(2)", 0.45, float(p_next[1]), quiet)
|
| 97 |
-
show("sum_j p_{t+1}(j)", 1.0, float(p_next.sum()), quiet)
|
| 98 |
-
return {"p_next": p_next}
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# %% 05. Eigenvalues and eigenvectors
|
| 102 |
-
def eigen(quiet: bool = False) -> dict:
|
| 103 |
-
"""Section 1.16: the characteristic equation of A, its two eigenvalues and its eigenvectors."""
|
| 104 |
-
trace = float(A_MATRIX.trace()) # The sum of the eigenvalues, 4.
|
| 105 |
-
determinant = float(torch.linalg.det(A_MATRIX)) # The product of the eigenvalues, 3.
|
| 106 |
-
values, vectors = torch.linalg.eigh(A_MATRIX) # A is symmetric, so the eigenvalues are real.
|
| 107 |
-
order = values.argsort(descending=True) # The chapter indexes lambda_1 = 3 before lambda_2 = 1.
|
| 108 |
-
values, vectors = values[order], vectors[:, order] # [2] and [2, 2].
|
| 109 |
-
vectors = vectors / vectors[0] # Rescale each one to first entry one, which here gives (1, ±1).
|
| 110 |
-
residual = (A_MATRIX @ vectors - vectors * values).abs().max() # A v - lambda v at both pairs.
|
| 111 |
-
show("trace A, lambda_1 + lambda_2", 4.0, trace, quiet)
|
| 112 |
-
show("det A, lambda_1 lambda_2", 3.0, determinant, quiet)
|
| 113 |
-
show("lambda_1", 3.0, float(values[0]), quiet)
|
| 114 |
-
show("lambda_2", 1.0, float(values[1]), quiet)
|
| 115 |
-
show("v_1 second entry over first", 1.0, float(vectors[1, 0]), quiet)
|
| 116 |
-
show("v_2 second entry over first", -1.0, float(vectors[1, 1]), quiet)
|
| 117 |
-
show("largest |A v - lambda v|", 0.0, float(residual), quiet)
|
| 118 |
-
return {"values": values, "vectors": vectors}
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# %% 06. Powers along an eigendirection
|
| 122 |
-
def powers(values: torch.Tensor, vectors: torch.Tensor, quiet: bool = False) -> dict:
|
| 123 |
-
"""Section 1.16.2: x_n = c_1 lambda_1^n v_1 + c_2 lambda_2^n v_2, against A^n x_0 taken directly."""
|
| 124 |
-
coefficients = torch.linalg.solve(vectors, X_START) # c_1 = 2 and c_2 = 1 for x_0 = (3, 1).
|
| 125 |
-
eigen_route = vectors @ (coefficients * values ** 2) # The n = 2 state along the two directions.
|
| 126 |
-
direct = torch.linalg.matrix_power(A_MATRIX, 2) @ X_START # A^2 x_0, squaring the matrix first.
|
| 127 |
-
far = vectors @ (coefficients * values ** 10) # The same decomposition carried to n = 10.
|
| 128 |
-
show("c_1", 2.0, float(coefficients[0]), quiet)
|
| 129 |
-
show("c_2", 1.0, float(coefficients[1]), quiet)
|
| 130 |
-
show("lambda_1^10", 59049.0, float(values[0]) ** 10, quiet)
|
| 131 |
-
show("x_2 first entry, eigen route", 19.0, float(eigen_route[0]), quiet)
|
| 132 |
-
show("x_2 second entry, eigen route", 17.0, float(eigen_route[1]), quiet)
|
| 133 |
-
show("x_2 first entry, A^2 x_0", 19.0, float(direct[0]), quiet)
|
| 134 |
-
show("x_10 first entry", 118099.0, float(far[0]), quiet)
|
| 135 |
-
show("x_10 second entry", 118097.0, float(far[1]), quiet)
|
| 136 |
-
return {"x_two": eigen_route, "x_ten": far}
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# %% 07. Diagonalization and the dominant eigenvalue
|
| 140 |
-
def diagonalization(values: torch.Tensor, vectors: torch.Tensor, quiet: bool = False) -> dict:
|
| 141 |
-
"""Sections 1.16.3 and 1.16.4: A = V Lambda V^{-1}, so A^n = V Lambda^n V^{-1} telescopes."""
|
| 142 |
-
inverse = torch.linalg.inv(vectors) # V^{-1} = 0.5 [[1, 1], [1, -1]]; [2, 2].
|
| 143 |
-
rebuilt = vectors @ torch.diag(values) @ inverse # V Lambda V^{-1} returns A itself; [2, 2].
|
| 144 |
-
cube = vectors @ torch.diag(values ** 3) @ inverse # V diag(27, 1) V^{-1}, the n = 3 power.
|
| 145 |
-
direct_cube = torch.linalg.matrix_power(A_MATRIX, 3) # A^3 computed by repeated multiplication.
|
| 146 |
-
dominant = torch.tensor([2.0, 0.5]) ** 10 # The coefficients 2^10 and 0.5^10 of Section 1.16.4.
|
| 147 |
-
show("V^{-1}_11", 0.5, float(inverse[0, 0]), quiet)
|
| 148 |
-
show("largest |V Lambda V^{-1} - A|", 0.0, float((rebuilt - A_MATRIX).abs().max()), quiet)
|
| 149 |
-
show("A^3_11 through the eigenvalues", 14.0, float(cube[0, 0]), quiet)
|
| 150 |
-
show("A^3_12 through the eigenvalues", 13.0, float(cube[0, 1]), quiet)
|
| 151 |
-
show("largest |V Lambda^3 V^{-1} - A^3|", 0.0, float((cube - direct_cube).abs().max()), quiet)
|
| 152 |
-
show("dominant coefficient 2^10", 1024.0, float(dominant[0]), quiet)
|
| 153 |
-
show("decaying coefficient 0.5^10", 0.000977, float(dominant[1]), quiet)
|
| 154 |
-
show("ratio of the two, in millions", 1.05, float(dominant[0] / dominant[1]) / 1e6, quiet)
|
| 155 |
-
return {"inverse": inverse, "cube": cube}
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
# %% 08. Stationary distributions
|
| 159 |
-
def stationary(steps: int, quiet: bool = False) -> dict:
|
| 160 |
-
"""Section 1.17: p* solves (P^T - I) p* = 0 with sum one, and the chain halves its deviation."""
|
| 161 |
-
drift = (P_CHAIN.T - torch.eye(2))[0] # The first row of P^T - I, which reads 0.3 p_2 = 0.2 p_1.
|
| 162 |
-
system = torch.stack([drift, torch.ones(2)]) # That one equation, with normalization; [2, 2].
|
| 163 |
-
p_star = torch.linalg.solve(system, torch.tensor([0.0, 1.0])) # The lambda = 1 eigenvector; [2].
|
| 164 |
-
trace, determinant = float(P_CHAIN.trace()), float(torch.linalg.det(P_CHAIN)) # 1.5 and 0.5.
|
| 165 |
-
second = (trace - math.sqrt(trace ** 2 - 4.0 * determinant)) / 2.0 # The smaller root, 0.5.
|
| 166 |
-
p_t, history = torch.tensor([0.5, 0.5]), [] # Start from the even split of Section 1.15.1.
|
| 167 |
-
for _ in range(max(steps, 1)): # Iterate p_{t+1} = P^T p_t for as many steps as requested.
|
| 168 |
-
p_t = P_CHAIN.T @ p_t # One more application of the transition matrix; [2].
|
| 169 |
-
history.append(p_t.clone())
|
| 170 |
-
show("p*_1", 0.6, float(p_star[0]), quiet)
|
| 171 |
-
show("p*_2", 0.4, float(p_star[1]), quiet)
|
| 172 |
-
show("largest |P^T p* - p*|", 0.0, float((P_CHAIN.T @ p_star - p_star).abs().max()), quiet)
|
| 173 |
-
show("trace P", 1.5, trace, quiet)
|
| 174 |
-
show("det P", 0.5, determinant, quiet)
|
| 175 |
-
show("second eigenvalue, the rate", 0.5, second, quiet)
|
| 176 |
-
for index, value in ((0, 0.55), (1, 0.575), (2, 0.5875), (9, 0.59990)):
|
| 177 |
-
if index < len(history): # The chapter prints p_1, p_2, p_3 and p_10 of this sequence.
|
| 178 |
-
show(f"p_{index + 1}(1)", value, float(history[index][0]), quiet)
|
| 179 |
-
deviation = float((history[-1] - p_star).abs().max()) # |d_n| = 0.5^n |d_0| after n steps.
|
| 180 |
-
show(f"|d_n| after {len(history)} steps", 0.1 * second ** len(history), deviation, quiet)
|
| 181 |
-
return {"p_star": p_star, "second": second, "history": history, "deviation": deviation}
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# %% 09. From discrete steps to continuous time
|
| 185 |
-
def matrix_exponential(values: torch.Tensor, vectors: torch.Tensor, quiet: bool = False) -> dict:
|
| 186 |
-
"""Section 1.18.3: the four-term series and the eigenvalue formula for e^{tA} at t = 0.1."""
|
| 187 |
-
t = 0.1 # The small time at which the chapter truncates the series.
|
| 188 |
-
series, power, factorial = torch.eye(2), torch.eye(2), 1.0 # Accumulate I + tA + t^2A^2/2 + ...
|
| 189 |
-
for k in range(1, 4): # Terms k = 1, 2, 3, which with the identity make four.
|
| 190 |
-
power, factorial = power @ A_MATRIX, factorial * k # A^k and k factorial.
|
| 191 |
-
series = series + t ** k * power / factorial # Add t^k A^k / k!; [2, 2].
|
| 192 |
-
discarded = t ** 4 * (power @ A_MATRIX) / 24.0 # The first omitted term, t^4 A^4 / 4!.
|
| 193 |
-
scaled = torch.diag((t * values).exp()) # e^{t Lambda}, the diagonal of entry exponentials; [2, 2].
|
| 194 |
-
eigen_route = vectors @ scaled @ torch.linalg.inv(vectors) # V e^{t Lambda} V^{-1}; [2, 2].
|
| 195 |
-
gap = float((series - eigen_route).abs().max()) # How far four terms fall short at t = 0.1.
|
| 196 |
-
coefficients = torch.linalg.solve(vectors, X_START) # c_1 = 2, c_2 = 1, as in Section 1.16.2.
|
| 197 |
-
x_one = vectors @ (coefficients * values.exp()) # x_t = sum_i c_i e^{t lambda_i} v_i at t = 1.
|
| 198 |
-
show("four-term series, entry 11", 1.22733, float(series[0, 0]), quiet)
|
| 199 |
-
show("four-term series, entry 12", 0.12217, float(series[0, 1]), quiet)
|
| 200 |
-
show("eigenvalue route, entry 11", 1.22751, float(eigen_route[0, 0]), quiet)
|
| 201 |
-
show("eigenvalue route, entry 12", 0.12234, float(eigen_route[0, 1]), quiet)
|
| 202 |
-
show("largest gap between the routes", 1.8e-4, gap, quiet)
|
| 203 |
-
show("first discarded term, entry 11", 1.71e-4, float(discarded[0, 0]), quiet)
|
| 204 |
-
show("first discarded term, entry 12", 1.67e-4, float(discarded[0, 1]), quiet)
|
| 205 |
-
show("x_1 first entry, 2e^3 + e", 42.889, float(x_one[0]), quiet)
|
| 206 |
-
show("x_1 second entry, 2e^3 - e", 37.453, float(x_one[1]), quiet)
|
| 207 |
-
show("decay e^{-2t} at t = 1", 0.1353, math.exp(-2.0), quiet)
|
| 208 |
-
show("decay e^{-2t} at t = 3", 0.002479, math.exp(-6.0), quiet)
|
| 209 |
-
show("sqrt(lambda_1), Gaussian long axis", 1.732, math.sqrt(float(values[0])), quiet)
|
| 210 |
-
return {"series": series, "eigen_route": eigen_route, "gap": gap, "x_one": x_one}
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
# %% 10. The report
|
| 214 |
-
def main() -> None:
|
| 215 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 216 |
-
parser.add_argument("--steps", type=int, default=10, help="iterations of p_{t+1} = P^T p_t")
|
| 217 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed, for a repeatable run")
|
| 218 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 219 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 220 |
-
args = parser.parse_args()
|
| 221 |
-
|
| 222 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same run.
|
| 223 |
-
if not args.quiet:
|
| 224 |
-
print("Chapter 1, Sections 1.12 to 1.18, worked example by worked example\n")
|
| 225 |
-
triangle = simplex(args.quiet)
|
| 226 |
-
logits = softmax(args.quiet)
|
| 227 |
-
step = one_transition(args.quiet)
|
| 228 |
-
spectrum = eigen(args.quiet)
|
| 229 |
-
grown = powers(spectrum["values"], spectrum["vectors"], args.quiet)
|
| 230 |
-
diagonalization(spectrum["values"], spectrum["vectors"], args.quiet)
|
| 231 |
-
chain = stationary(args.steps, args.quiet)
|
| 232 |
-
flow = matrix_exponential(spectrum["values"], spectrum["vectors"], args.quiet)
|
| 233 |
-
|
| 234 |
-
report = {
|
| 235 |
-
"E[h(X)] on the three bases": triangle["expectation"],
|
| 236 |
-
"softmax(z)_1": float(logits["probabilities"][0]),
|
| 237 |
-
"p_{t+1}(1) after one step": float(step["p_next"][0]),
|
| 238 |
-
"lambda_1 of A": float(spectrum["values"][0]),
|
| 239 |
-
"lambda_2 of A": float(spectrum["values"][1]),
|
| 240 |
-
"x_10 first entry": float(grown["x_ten"][0]),
|
| 241 |
-
"p*_1, stationary": float(chain["p_star"][0]),
|
| 242 |
-
"second eigenvalue of P": chain["second"],
|
| 243 |
-
"chain steps run": len(chain["history"]),
|
| 244 |
-
"deviation from p* at the end": chain["deviation"],
|
| 245 |
-
"e^{tA}_11 at t = 0.1": float(flow["eigen_route"][0, 0]),
|
| 246 |
-
"four-term truncation error": flow["gap"],
|
| 247 |
-
}
|
| 248 |
-
print_report("Linear algebra, Sections 1.12 to 1.18", report)
|
| 249 |
-
save_run(args.out, config=vars(args), report=report)
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 253 |
-
main() # Work every example of Sections 1.12 to 1.18 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/optimization.py
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
"""Unconstrained descent, Lagrange multipliers, maximum entropy and the KKT conditions.
|
| 2 |
-
|
| 3 |
-
A differentiable minimum has a zero gradient, and a descent step moves against that gradient. An
|
| 4 |
-
equality constraint comes with a multiplier, and at the constrained optimum the two gradients are
|
| 5 |
-
parallel. Applied to entropy under an expectation constraint, that condition leads to an
|
| 6 |
-
exponential family. An inequality comes with a sign requirement and complementary slackness, and
|
| 7 |
-
under the softmax the simplex constraints hold by construction.
|
| 8 |
-
|
| 9 |
-
Chapter 1, Sections 1.36, 1.37 and 1.38 of the notes. Runs on the quadratic loss of
|
| 10 |
-
Section 1.36, the constrained distance of Section 1.37.1, the GC-content target of
|
| 11 |
-
Section 1.37.3 and the bounded quadratic of Section 1.38.
|
| 12 |
-
|
| 13 |
-
python lecture_1/optimization.py # every worked example
|
| 14 |
-
python lecture_1/optimization.py --steps 20 # a short descent, far from the minimizer
|
| 15 |
-
python lecture_1/optimization.py --quiet # the closing report only
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import argparse
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
BASE_COUNTS = torch.tensor([4.0, 3.0, 2.0, 1.0]) # n(A), n(C), n(G), n(T) over ten DNA bases.
|
| 32 |
-
GC_INDICATOR = torch.tensor([0.0, 1.0, 1.0, 0.0]) # h(x) = 1 on the strong bases C and G.
|
| 33 |
-
GC_TARGET = 0.6 # The required expected GC content, the constraint value c of Section 1.37.3.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 40 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 41 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 42 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 43 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 44 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 45 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def entropy(p: torch.Tensor) -> float:
|
| 49 |
-
"""H(p) = -sum_i p_i log p_i, the quantity maximized in both constrained problems below."""
|
| 50 |
-
return float(-(p * p.clamp_min(1e-300).log()).sum()) # Clamp keeps 0 log 0 at zero.
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# %% 02. Optimization without constraints
|
| 54 |
-
def unconstrained(steps: int, quiet: bool = False) -> dict:
|
| 55 |
-
"""Section 1.36: on L(theta) = (theta - 3)^2 the error contracts by 0.8 at every step."""
|
| 56 |
-
learning_rate, theta = 0.1, 0.0 # The step size eta and the starting parameter theta_0.
|
| 57 |
-
history = [] # The iterates, so the first three can be compared with the chapter.
|
| 58 |
-
for _ in range(max(steps, 1)):
|
| 59 |
-
theta = theta - learning_rate * 2.0 * (theta - 3.0) # theta - eta L'(theta) = 0.8 theta + 0.6.
|
| 60 |
-
history.append(theta)
|
| 61 |
-
closed_form = 3.0 - 3.0 * 0.8 ** len(history) # theta_k = 3 - 3(0.8)^k, from the contraction.
|
| 62 |
-
for index, value in ((0, 0.600), (1, 1.080), (2, 1.464)):
|
| 63 |
-
if index < len(history): # The chapter prints theta_1, theta_2 and theta_3.
|
| 64 |
-
show(f"theta_{index + 1}", value, history[index], quiet)
|
| 65 |
-
show(f"theta_{len(history)} from the closed form", closed_form, history[-1], quiet)
|
| 66 |
-
show("gradient at the minimizer theta = 3", 0.0, 2.0 * (3.0 - 3.0), quiet)
|
| 67 |
-
return {"history": history, "closed_form": closed_form}
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
# %% 03. An equality constraint and its multiplier
|
| 71 |
-
def equality_constraint(quiet: bool = False) -> dict:
|
| 72 |
-
"""Section 1.37.1: minimize x_1^2 + x_2^2 on the line x_1 + x_2 = 1, through the Lagrangian."""
|
| 73 |
-
system = torch.tensor([[2.0, 0.0, 1.0], # 2 x_1 + lambda = 0, the first stationarity condition.
|
| 74 |
-
[0.0, 2.0, 1.0], # 2 x_2 + lambda = 0, the second.
|
| 75 |
-
[1.0, 1.0, 0.0]]) # x_1 + x_2 = 1, the constraint itself.
|
| 76 |
-
solution = torch.linalg.solve(system, torch.tensor([0.0, 0.0, 1.0])) # (x_1, x_2, lambda); [3].
|
| 77 |
-
optimum = float(solution[:2].square().sum()) # f at the constrained minimizer.
|
| 78 |
-
endpoint = float(torch.tensor([1.0, 0.0]).square().sum()) # f at either end of the line.
|
| 79 |
-
grad_f = 2.0 * solution[:2] # grad f = (2 x_1, 2 x_2) at the solution; [2].
|
| 80 |
-
parallel = float((grad_f + solution[2] * torch.ones(2)).abs().max()) # grad f + lambda grad g.
|
| 81 |
-
show("x_1 at the constrained minimum", 0.5, float(solution[0]), quiet)
|
| 82 |
-
show("x_2 at the constrained minimum", 0.5, float(solution[1]), quiet)
|
| 83 |
-
show("lambda, the equality multiplier", -1.0, float(solution[2]), quiet)
|
| 84 |
-
show("f at the minimum", 0.5, optimum, quiet)
|
| 85 |
-
show("f at the endpoint (1, 0)", 1.0, endpoint, quiet)
|
| 86 |
-
show("largest |grad f + lambda grad g|", 0.0, parallel, quiet)
|
| 87 |
-
return {"solution": solution, "optimum": optimum}
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
# %% 04. Normalization as a constraint
|
| 91 |
-
def normalization(quiet: bool = False) -> dict:
|
| 92 |
-
"""Section 1.37.2: entropy under normalization alone is maximized by the uniform distribution,
|
| 93 |
-
and the empirical negative log-likelihood is minimized by the observed frequencies."""
|
| 94 |
-
states = 4 # K, the size of the DNA alphabet.
|
| 95 |
-
uniform = torch.full((states,), 1.0 / states) # p_i = e^{lambda - 1}, the same at every state.
|
| 96 |
-
frequencies = BASE_COUNTS / BASE_COUNTS.sum() # p(x) = n(x) / N, the solution at lambda = 1.
|
| 97 |
-
loss = float(-(frequencies * frequencies.log()).sum()) # The minimum empirical loss, its entropy.
|
| 98 |
-
nearby = frequencies + torch.tensor([0.05, -0.05, 0.0, 0.0]) # A different categorical model.
|
| 99 |
-
worse = float(-(frequencies * nearby.log()).sum()) # Its loss on the same ten bases.
|
| 100 |
-
show("uniform p_i at K = 4", 0.25, float(uniform[0]), quiet)
|
| 101 |
-
show("max entropy under normalization", 1.386, entropy(uniform), quiet)
|
| 102 |
-
show("p_hat(A) = n(A) / N", 0.4, float(frequencies[0]), quiet)
|
| 103 |
-
show("p_hat(T) = n(T) / N", 0.1, float(frequencies[3]), quiet)
|
| 104 |
-
show("minimum empirical loss", 1.280, loss, quiet)
|
| 105 |
-
show("loss drop from moving off p_hat", 0.0, max(0.0, loss - worse), quiet)
|
| 106 |
-
if not quiet: # The displaced model is not a chapter number, so it is printed on its own.
|
| 107 |
-
print(f" {'loss of a model moved by 0.05':<36s} loss {worse:>11.5f}"
|
| 108 |
-
f" at p_hat {loss:>11.5f}")
|
| 109 |
-
return {"uniform": uniform, "frequencies": frequencies, "loss": loss, "worse": worse}
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
# %% 05. Maximum entropy under a property constraint
|
| 113 |
-
def maximum_entropy(samples: int, quiet: bool = False) -> dict:
|
| 114 |
-
"""Section 1.37.3: with one multiplier per property the entropy maximizer is p* proportional to
|
| 115 |
-
exp(lambda h(x)), and the constraint fixes lambda."""
|
| 116 |
-
strong, weak = float(GC_INDICATOR.sum()), float((1.0 - GC_INDICATOR).sum()) # Two bases each.
|
| 117 |
-
exp_lambda = (GC_TARGET / (1.0 - GC_TARGET)) * (weak / strong) # From 2e^L/(2e^L + 2) = 0.6.
|
| 118 |
-
multiplier = math.log(exp_lambda) # lambda = log 1.5, the tilt toward the strong bases.
|
| 119 |
-
unnormalized = (multiplier * GC_INDICATOR).exp() # exp(lambda h(x)) at the four bases; [4].
|
| 120 |
-
partition = float(unnormalized.sum()) # Z(lambda) = 2 e^lambda + 2 = 5.
|
| 121 |
-
p_star = unnormalized / partition # The maximum-entropy distribution (0.2, 0.3, 0.3, 0.2); [4].
|
| 122 |
-
uniform = torch.full((4,), 0.25) # What lambda = 0 returns, with no property requirement.
|
| 123 |
-
weights = torch.rand(max(samples, 1), 2) # One split per base class, drawn independently; [N, 2].
|
| 124 |
-
competitors = torch.stack([ # Distributions that also meet the GC target, in A, C, G, T order.
|
| 125 |
-
(1.0 - GC_TARGET) * weights[:, 0], GC_TARGET * weights[:, 1],
|
| 126 |
-
GC_TARGET * (1.0 - weights[:, 1]), (1.0 - GC_TARGET) * (1.0 - weights[:, 0])], dim=1)
|
| 127 |
-
drawn = -(competitors * competitors.clamp_min(1e-300).log()).sum(1) # H of every competitor; [N].
|
| 128 |
-
best = float(drawn.max()) # The largest entropy reached by any distribution meeting the target.
|
| 129 |
-
show("e^lambda", 1.5, exp_lambda, quiet)
|
| 130 |
-
show("lambda = log 1.5", 0.4055, multiplier, quiet)
|
| 131 |
-
show("partition function Z", 5.0, partition, quiet)
|
| 132 |
-
show("p*(A)", 0.2, float(p_star[0]), quiet)
|
| 133 |
-
show("p*(C)", 0.3, float(p_star[1]), quiet)
|
| 134 |
-
show("GC content of p*", 0.6, float(p_star @ GC_INDICATOR), quiet)
|
| 135 |
-
show("H(p*)", 1.366, entropy(p_star), quiet)
|
| 136 |
-
show("H of the unconstrained uniform", 1.386, entropy(uniform), quiet)
|
| 137 |
-
show("entropy cost of the constraint", 0.0201, entropy(uniform) - entropy(p_star), quiet)
|
| 138 |
-
show(f"H above H(p*) over {len(drawn)} draws", 0.0, max(0.0, best - entropy(p_star)), quiet)
|
| 139 |
-
return {"multiplier": multiplier, "p_star": p_star, "partition": partition, "best_drawn": best}
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
# %% 06. Inequality constraints and the KKT conditions
|
| 143 |
-
def kkt(quiet: bool = False) -> dict:
|
| 144 |
-
"""Section 1.38: on min (x - 3)^2 with x <= 1 the bound is active, so lambda is positive."""
|
| 145 |
-
x_star = 1.0 # The unconstrained minimizer 3 breaks the bound, so the solution is at it.
|
| 146 |
-
multiplier = -2.0 * (x_star - 3.0) # Stationarity 2(x - 3) + lambda = 0 solved at x = 1.
|
| 147 |
-
slack = x_star - 1.0 # g(x*) = x* - 1, which must be at or below zero.
|
| 148 |
-
show("x* at the bound", 1.0, x_star, quiet)
|
| 149 |
-
show("lambda, the inequality multiplier", 4.0, multiplier, quiet)
|
| 150 |
-
show("g(x*) = x* - 1", 0.0, slack, quiet)
|
| 151 |
-
show("complementary slackness lambda g", 0.0, multiplier * slack, quiet)
|
| 152 |
-
show("stationarity 2(x* - 3) + lambda", 0.0, 2.0 * (x_star - 3.0) + multiplier, quiet)
|
| 153 |
-
show("f at the unconstrained minimum", 0.0, (3.0 - 3.0) ** 2, quiet)
|
| 154 |
-
show("f under the bound", 4.0, (x_star - 3.0) ** 2, quiet)
|
| 155 |
-
show("value drop for epsilon = 0.001", 0.004, -((1.001 - 3.0) ** 2 - 4.0), quiet)
|
| 156 |
-
return {"multiplier": multiplier, "value": (x_star - 3.0) ** 2}
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
# %% 07. Optimization over the simplex through the softmax
|
| 160 |
-
def over_the_simplex(steps: int, quiet: bool = False) -> dict:
|
| 161 |
-
"""Section 1.38.1: under logits the simplex constraints hold at every step, so the constrained
|
| 162 |
-
likelihood problem of Section 1.37.2 becomes an unconstrained descent that reaches p_hat."""
|
| 163 |
-
frequencies = BASE_COUNTS / BASE_COUNTS.sum() # The minimizer the multiplier route produced.
|
| 164 |
-
logits = torch.zeros(4, requires_grad=True) # z in R^4, starting at the uniform distribution.
|
| 165 |
-
learning_rate, losses = 1.0, [] # Plain gradient descent on z, with the loss after each step.
|
| 166 |
-
for _ in range(max(steps, 1)):
|
| 167 |
-
model = torch.softmax(logits, dim=0) # p_i = e^{z_i} / sum_j e^{z_j}, always on the simplex.
|
| 168 |
-
loss = -(frequencies * model.log()).sum() # The empirical negative log-likelihood.
|
| 169 |
-
gradient, = torch.autograd.grad(loss, logits) # d L / d z, which is softmax(z) - p_hat.
|
| 170 |
-
with torch.no_grad(): # The update itself needs no graph.
|
| 171 |
-
logits -= learning_rate * gradient # One descent step of Section 1.36, now on logits.
|
| 172 |
-
losses.append(float(loss.detach()))
|
| 173 |
-
final = torch.softmax(logits.detach(), dim=0) # The distribution the descent arrived at; [4].
|
| 174 |
-
show("sum_i p_i at the end", 1.0, float(final.sum()), quiet)
|
| 175 |
-
show("entries at or below zero", 0.0, float((final <= 0.0).sum()), quiet)
|
| 176 |
-
show("p(A) after the descent", 0.4, float(final[0]), quiet)
|
| 177 |
-
show("p(T) after the descent", 0.1, float(final[3]), quiet)
|
| 178 |
-
show("loss after the descent", 1.280, losses[-1], quiet)
|
| 179 |
-
show("largest |p - p_hat|", 0.0, float((final - frequencies).abs().max()), quiet)
|
| 180 |
-
return {"final": final, "losses": losses, "distance": float((final - frequencies).abs().max())}
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
# %% 08. The report
|
| 184 |
-
def main() -> None:
|
| 185 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 186 |
-
parser.add_argument("--steps", type=int, default=600, help="gradient descent steps")
|
| 187 |
-
parser.add_argument("--samples", type=int, default=4000, help="competing distributions drawn")
|
| 188 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed for the drawn competitors")
|
| 189 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 190 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 191 |
-
args = parser.parse_args()
|
| 192 |
-
|
| 193 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same competitors.
|
| 194 |
-
if not args.quiet:
|
| 195 |
-
print("Chapter 1, Sections 1.36 to 1.38, worked example by worked example\n")
|
| 196 |
-
descent = unconstrained(args.steps, args.quiet)
|
| 197 |
-
constrained = equality_constraint(args.quiet)
|
| 198 |
-
normalized = normalization(args.quiet)
|
| 199 |
-
tilted = maximum_entropy(args.samples, args.quiet)
|
| 200 |
-
bounded = kkt(args.quiet)
|
| 201 |
-
fitted = over_the_simplex(args.steps, args.quiet)
|
| 202 |
-
|
| 203 |
-
report = {
|
| 204 |
-
"descent steps run": len(descent["history"]),
|
| 205 |
-
"theta after the descent": descent["history"][-1],
|
| 206 |
-
"x_1 under x_1 + x_2 = 1": float(constrained["solution"][0]),
|
| 207 |
-
"lambda, equality constraint": float(constrained["solution"][2]),
|
| 208 |
-
"minimum empirical loss, nats": normalized["loss"],
|
| 209 |
-
"lambda for GC content 0.6": tilted["multiplier"],
|
| 210 |
-
"p*(C) under that constraint": float(tilted["p_star"][1]),
|
| 211 |
-
"H(p*), nats": entropy(tilted["p_star"]),
|
| 212 |
-
"best drawn H at the same target": tilted["best_drawn"],
|
| 213 |
-
"lambda, active inequality": bounded["multiplier"],
|
| 214 |
-
"f under the bound": bounded["value"],
|
| 215 |
-
"loss after the softmax descent": fitted["losses"][-1],
|
| 216 |
-
"largest |p - p_hat|": fitted["distance"],
|
| 217 |
-
}
|
| 218 |
-
print_report("Optimization, Sections 1.36 to 1.38", report)
|
| 219 |
-
save_run(args.out, config=vars(args), report=report)
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 223 |
-
main() # Work every example of Sections 1.36 to 1.38 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_1/probability.py
DELETED
|
@@ -1,261 +0,0 @@
|
|
| 1 |
-
"""Probability on discrete and continuous states, in the chapter's own numbers.
|
| 2 |
-
|
| 3 |
-
A distribution allocates one unit of mass over the states, expectation averages a property under
|
| 4 |
-
that allocation, variance and covariance record how the values fluctuate, and conditioning and
|
| 5 |
-
Bayes' rule revise the allocation once something has been observed. The chapter works every
|
| 6 |
-
example below by hand, so each section prints the hand value beside the value this file computes.
|
| 7 |
-
|
| 8 |
-
Chapter 1, Sections 1.3 to 1.11 of the notes. Runs on the two-token DNA distribution of
|
| 9 |
-
Section 1.3.1, the three peptides of Section 1.7.1 and the motif-and-binding table of
|
| 10 |
-
Section 1.9.1. Prints the chapter's number, the computed number and the gap between them.
|
| 11 |
-
|
| 12 |
-
python lecture_1/probability.py # every worked example
|
| 13 |
-
python lecture_1/probability.py --samples 100000 # a longer Monte Carlo ladder
|
| 14 |
-
python lecture_1/probability.py --quiet # the closing report only
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import argparse
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
torch.set_default_dtype(torch.float64) # Chapter 1 is arithmetic, and five printed digits need it.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
STATES = ["AA", "AC", "CA", "CC"] # The state space X of Section 1.3.1, with L = 2 over {A, C}.
|
| 31 |
-
P_X = torch.tensor([0.4, 0.1, 0.2, 0.3]) # p_X over those four states, in the order of STATES.
|
| 32 |
-
A_COUNT = torch.tensor([2.0, 1.0, 1.0, 0.0]) # h(x), the number of A tokens in each state.
|
| 33 |
-
PEPTIDES = torch.tensor([[1.0, 2.0], [2.0, 3.0], [3.0, 7.0]]) # (hydrophobicity, binding score).
|
| 34 |
-
JOINT = torch.tensor([[0.32, 0.08], # Row x = 0, motif absent: columns are y = 0 and y = 1.
|
| 35 |
-
[0.24, 0.36]]) # Row x = 1, motif present: Table 1.1 of Section 1.9.1.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
WORKED: dict[str, float] = {} # Every computed value, by label, for tests/test_lecture_1.py.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def show(label: str, by_hand: float, computed: float, quiet: bool = False) -> None:
|
| 42 |
-
"""Print the chapter's hand-worked value beside the value this file computes, and keep it."""
|
| 43 |
-
WORKED[label] = computed # The test suite asserts these against the chapter's own numbers.
|
| 44 |
-
if not quiet: # Under --quiet only the closing report is printed.
|
| 45 |
-
computed = computed + 0.0 # Adding zero prints a negative zero as a plain zero.
|
| 46 |
-
print(f" {label:<36s} chapter {by_hand:>11.5f} computed {computed:>11.5f}"
|
| 47 |
-
f" gap {abs(by_hand - computed):.1e}")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# %% 02. A distribution over length-two sequences
|
| 51 |
-
def two_token_distribution(quiet: bool = False) -> dict:
|
| 52 |
-
"""Section 1.3.1: the four masses sum to one, and the event {x_1 = A} has probability 0.5."""
|
| 53 |
-
total = float(P_X.sum()) # Normalization, sum over x in X of p_X(x).
|
| 54 |
-
event = [index for index, state in enumerate(STATES) if state[0] == "A"] # A = {x : x_1 = A}.
|
| 55 |
-
first_is_a = float(P_X[event].sum()) # P(X in A), the masses of {AA, AC} added together.
|
| 56 |
-
show("sum_x p_X(x)", 1.0, total, quiet)
|
| 57 |
-
show("P(x_1 = A) = p(AA) + p(AC)", 0.5, first_is_a, quiet)
|
| 58 |
-
return {"normalization": total, "p_first_is_a": first_is_a}
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
# %% 03. Densities for continuous states
|
| 62 |
-
def continuous_densities(quiet: bool = False) -> dict:
|
| 63 |
-
"""Section 1.4: a density is probability per unit length, so its height may exceed one."""
|
| 64 |
-
uniform_height = 1.0 / 0.1 # The uniform density on [0, 0.1] has the value 1/0.1 there.
|
| 65 |
-
narrow_mass = uniform_height * 0.02 # P(0 <= X <= 0.02) is the height times the width.
|
| 66 |
-
gaussian_peak = 1.0 / (math.sqrt(2.0 * math.pi) * 0.1) # N(0, 0.1^2) at its center.
|
| 67 |
-
show("uniform[0, 0.1] density height", 10.0, uniform_height, quiet)
|
| 68 |
-
show("P(0 <= X <= 0.02)", 0.2, narrow_mass, quiet)
|
| 69 |
-
show("peak of N(0, 0.1^2)", 3.989, gaussian_peak, quiet)
|
| 70 |
-
return {"uniform_height": uniform_height, "gaussian_peak": gaussian_peak}
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# %% 04. The expected adenine count
|
| 74 |
-
def expected_adenine(quiet: bool = False) -> dict:
|
| 75 |
-
"""Section 1.5.1: E[h(X)] = sum_x p_X(x) h(x), the probability-weighted average of the count."""
|
| 76 |
-
expectation = float(P_X @ A_COUNT) # 0.4(2) + 0.1(1) + 0.2(1) + 0.3(0).
|
| 77 |
-
across_thousand = 1000.0 * expectation # The adenines expected in 1000 generated sequences.
|
| 78 |
-
show("E[h(X)], expected A count", 1.1, expectation, quiet)
|
| 79 |
-
show("expected A count in 1000 draws", 1100.0, across_thousand, quiet)
|
| 80 |
-
return {"expectation": expectation}
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 05. Monte Carlo estimation
|
| 84 |
-
def monte_carlo(samples: int, repeats: int, quiet: bool = False) -> dict:
|
| 85 |
-
"""Section 1.5.3: the sample average estimates E[h(X)] with an error that shrinks like 1/sqrt(N).
|
| 86 |
-
|
| 87 |
-
The chapter's own ten-sample draw gave 1.2 against the exact 1.1, a gap well inside the
|
| 88 |
-
standard deviation 0.263 of a ten-sample average. We redraw here, so the estimate moves with
|
| 89 |
-
--seed while the exact value and the error scale stay put. Repeating the ten-sample estimate
|
| 90 |
-
``repeats`` times turns that predicted 0.263 into a measured spread.
|
| 91 |
-
"""
|
| 92 |
-
exact = float(P_X @ A_COUNT) # The target of the estimator, E[h(X)] = 1.1.
|
| 93 |
-
second_moment = float(P_X @ A_COUNT.square()) # E[h^2] = 0.4(4) + 0.1 + 0.2 = 1.9.
|
| 94 |
-
variance = second_moment - exact ** 2 # Var(h(X)) = 1.9 - 1.21 = 0.69.
|
| 95 |
-
show("E[h(X)^2]", 1.9, second_moment, quiet)
|
| 96 |
-
show("Var(h(X))", 0.69, variance, quiet)
|
| 97 |
-
ladder, size = [], 10 # The chapter quotes the error at N = 10 and at N = 1000.
|
| 98 |
-
while size <= max(samples, 10):
|
| 99 |
-
draws = torch.multinomial(P_X, size, replacement=True) # [size] state indices from p_X.
|
| 100 |
-
estimate = float(A_COUNT[draws].mean()) # (1/N) sum_n h(x^(n)), the Monte Carlo average.
|
| 101 |
-
ladder.append((size, estimate, math.sqrt(variance / size)))
|
| 102 |
-
size *= 10
|
| 103 |
-
if not quiet:
|
| 104 |
-
for size, estimate, error in ladder:
|
| 105 |
-
print(f" N = {size:<8d} estimate {estimate:.4f} exact {exact:.4f}"
|
| 106 |
-
f" sd of the average {error:.4f}")
|
| 107 |
-
show("sd of a 10-sample average", 0.263, math.sqrt(variance / 10), quiet)
|
| 108 |
-
show("sd of a 1000-sample average", 0.0263, math.sqrt(variance / 1000), quiet)
|
| 109 |
-
rounds = max(repeats, 2) # Two averages is the fewest a standard deviation can be taken over.
|
| 110 |
-
repeated = torch.multinomial(P_X, 10 * rounds, replacement=True) # [10 * rounds] state indices.
|
| 111 |
-
averages = A_COUNT[repeated].view(rounds, 10).mean(dim=1) # One ten-sample average per round.
|
| 112 |
-
measured = float(averages.std()) # The spread of those averages, which 0.263 predicts.
|
| 113 |
-
show(f"sd of {rounds} ten-sample averages", 0.263, measured, quiet)
|
| 114 |
-
return {"exact": exact, "variance": variance, "ladder": ladder, "measured": measured,
|
| 115 |
-
"rounds": rounds}
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
# %% 06. A worked variance
|
| 119 |
-
def worked_variance(quiet: bool = False) -> dict:
|
| 120 |
-
"""Section 1.6.2: Var(X) = E[X^2] - (E[X])^2 on the values 0, 1, 2 with masses 0.2, 0.5, 0.3."""
|
| 121 |
-
values = torch.tensor([0.0, 1.0, 2.0]) # The three values X can take.
|
| 122 |
-
masses = torch.tensor([0.2, 0.5, 0.3]) # p_X at those three values.
|
| 123 |
-
first_moment = float(masses @ values) # E[X] = 0 + 0.5 + 0.6 = 1.1.
|
| 124 |
-
second_moment = float(masses @ values.square()) # E[X^2] = 0 + 0.5 + 1.2 = 1.7.
|
| 125 |
-
from_moments = second_moment - first_moment ** 2 # The boxed identity of Section 1.6.1.
|
| 126 |
-
from_definition = float(masses @ (values - first_moment).square()) # The two-pass route.
|
| 127 |
-
show("E[X]", 1.1, first_moment, quiet)
|
| 128 |
-
show("E[X^2]", 1.7, second_moment, quiet)
|
| 129 |
-
show("Var(X) from the two moments", 0.49, from_moments, quiet)
|
| 130 |
-
show("Var(X) from the definition", 0.49, from_definition, quiet)
|
| 131 |
-
show("sigma_X", 0.7, math.sqrt(from_moments), quiet)
|
| 132 |
-
return {"variance": from_moments, "sigma": math.sqrt(from_moments)}
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
# %% 07. Covariance, correlation and the covariance matrix
|
| 136 |
-
def peptide_covariance(quiet: bool = False) -> dict:
|
| 137 |
-
"""Sections 1.7.1 and 1.7.2: three equally likely peptides, their correlation and their Sigma."""
|
| 138 |
-
mu = PEPTIDES.mean(dim=0) # (mu_X, mu_Y) = (2, 4), each coordinate mean; [2].
|
| 139 |
-
centered = PEPTIDES - mu # The deviations (-1, -2), (0, -1), (1, 3); [3, 2].
|
| 140 |
-
sigma_matrix = centered.T @ centered / PEPTIDES.shape[0] # E[(X-mu)(X-mu)^T]; [2, 2].
|
| 141 |
-
cov_xy = float(sigma_matrix[0, 1]) # Cov(X, Y) = (2 + 0 + 3)/3 = 5/3.
|
| 142 |
-
sd_x = math.sqrt(float(sigma_matrix[0, 0])) # sigma_X = sqrt(2/3).
|
| 143 |
-
sd_y = math.sqrt(float(sigma_matrix[1, 1])) # sigma_Y = sqrt(14/3).
|
| 144 |
-
rho = cov_xy / (sd_x * sd_y) # Correlation, which divides out the units of both variables.
|
| 145 |
-
show("mu_X", 2.0, float(mu[0]), quiet)
|
| 146 |
-
show("mu_Y", 4.0, float(mu[1]), quiet)
|
| 147 |
-
show("Cov(X, Y)", 5.0 / 3.0, cov_xy, quiet)
|
| 148 |
-
show("Var(X) = Sigma_11", 2.0 / 3.0, float(sigma_matrix[0, 0]), quiet)
|
| 149 |
-
show("Var(Y) = Sigma_22", 14.0 / 3.0, float(sigma_matrix[1, 1]), quiet)
|
| 150 |
-
show("sigma_X across the peptides", 0.8165, sd_x, quiet)
|
| 151 |
-
show("sigma_Y across the peptides", 2.160, sd_y, quiet)
|
| 152 |
-
show("rho_XY", 0.9449, rho, quiet)
|
| 153 |
-
show("Sigma symmetry, Sigma_21 - Sigma_12", 0.0, float(sigma_matrix[1, 0] - cov_xy), quiet)
|
| 154 |
-
return {"sigma_matrix": sigma_matrix, "rho": rho}
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
# %% 08. Gaussian noise and the noising equation
|
| 158 |
-
def noising_covariance(quiet: bool = False) -> dict:
|
| 159 |
-
"""Section 1.8: Cov(X_t) = alpha_t^2 Cov(X_0) + sigma_t^2 I at alpha_t = 0.6, sigma_t = 0.8."""
|
| 160 |
-
alpha_t, sigma_t = 0.6, 0.8 # A variance-preserving pair, since 0.36 + 0.64 = 1.
|
| 161 |
-
identity = torch.eye(2) # The isotropic noise covariance; [2, 2].
|
| 162 |
-
isotropic = alpha_t ** 2 * identity + sigma_t ** 2 * identity # Data covariance I stays I.
|
| 163 |
-
wide_data = torch.diag(torch.tensor([4.0, 1.0])) # Cov(X_0) = diag(4, 1); [2, 2].
|
| 164 |
-
anisotropic = alpha_t ** 2 * wide_data + sigma_t ** 2 * identity # diag(2.08, 1.00).
|
| 165 |
-
show("alpha_t^2 + sigma_t^2", 1.0, alpha_t ** 2 + sigma_t ** 2, quiet)
|
| 166 |
-
show("Cov(X_t)_11 with Cov(X_0) = I", 1.0, float(isotropic[0, 0]), quiet)
|
| 167 |
-
show("Cov(X_t)_11 with diag(4, 1)", 2.08, float(anisotropic[0, 0]), quiet)
|
| 168 |
-
show("Cov(X_t)_22 with diag(4, 1)", 1.00, float(anisotropic[1, 1]), quiet)
|
| 169 |
-
return {"anisotropic": anisotropic}
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
# %% 09. Joint, marginal and conditional distributions
|
| 173 |
-
def joint_marginal_conditional(quiet: bool = False) -> dict:
|
| 174 |
-
"""Sections 1.9.1 to 1.9.3: the motif-and-binding table, its marginals and its conditionals."""
|
| 175 |
-
total = float(JOINT.sum()) # 0.36 + 0.24 + 0.08 + 0.32 = 1.
|
| 176 |
-
p_x = JOINT.sum(dim=1) # p(x) = sum_y p(x, y), motif present or absent; [2].
|
| 177 |
-
p_y = JOINT.sum(dim=0) # p(y) = sum_x p(x, y), high or low binding; [2].
|
| 178 |
-
p_y_given_x = JOINT / p_x[:, None] # p(y | x) = p(x, y) / p(x), renormalized row by row; [2, 2].
|
| 179 |
-
show("sum over the four cells", 1.0, total, quiet)
|
| 180 |
-
show("p(X = 1), motif present", 0.60, float(p_x[1]), quiet)
|
| 181 |
-
show("p(X = 0), motif absent", 0.40, float(p_x[0]), quiet)
|
| 182 |
-
show("p(Y = 1), high binding", 0.44, float(p_y[1]), quiet)
|
| 183 |
-
show("p(Y = 0), low binding", 0.56, float(p_y[0]), quiet)
|
| 184 |
-
show("p(Y = 1 | X = 1)", 0.6, float(p_y_given_x[1, 1]), quiet)
|
| 185 |
-
show("p(Y = 0 | X = 1)", 0.4, float(p_y_given_x[1, 0]), quiet)
|
| 186 |
-
show("p(Y = 1 | X = 0)", 0.2, float(p_y_given_x[0, 1]), quiet)
|
| 187 |
-
show("p(Y = 0 | X = 0)", 0.8, float(p_y_given_x[0, 0]), quiet)
|
| 188 |
-
return {"p_x": p_x, "p_y": p_y, "p_y_given_x": p_y_given_x}
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
# %% 10. Bayes' rule and the law of total probability
|
| 192 |
-
def bayes(quiet: bool = False) -> dict:
|
| 193 |
-
"""Section 1.10: the motif probability after observing high binding, with its own denominator."""
|
| 194 |
-
p_x = JOINT.sum(dim=1) # The prior p(x) over motif presence; [2].
|
| 195 |
-
p_y_given_x = JOINT / p_x[:, None] # The likelihood p(y | x); [2, 2].
|
| 196 |
-
evidence = float(p_y_given_x[:, 1] @ p_x) # p(y) = sum_x p(y | x) p(x), summed over the cause.
|
| 197 |
-
posterior = float(p_y_given_x[1, 1] * p_x[1]) / evidence # p(X = 1 | Y = 1) = 0.36 / 0.44.
|
| 198 |
-
show("evidence p(Y = 1)", 0.44, evidence, quiet)
|
| 199 |
-
show("posterior p(X = 1 | Y = 1)", 0.818, posterior, quiet)
|
| 200 |
-
show("prior p(X = 1)", 0.60, float(p_x[1]), quiet)
|
| 201 |
-
show("likelihood ratio 0.6 / 0.2", 3.0, float(p_y_given_x[1, 1] / p_y_given_x[0, 1]), quiet)
|
| 202 |
-
return {"evidence": evidence, "posterior": posterior}
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
# %% 11. Independence
|
| 206 |
-
def independence(quiet: bool = False) -> dict:
|
| 207 |
-
"""Section 1.11: p(x, y) equals p(x) p(y) under independence, and the table fails that test."""
|
| 208 |
-
p_x = JOINT.sum(dim=1) # Marginal over motif presence; [2].
|
| 209 |
-
p_y = JOINT.sum(dim=0) # Marginal over binding activity; [2].
|
| 210 |
-
product = float(p_x[1] * p_y[1]) # 0.60 times 0.44 = 0.264 under a factorized model.
|
| 211 |
-
joint_entry = float(JOINT[1, 1]) # The table's own entry, 0.36.
|
| 212 |
-
show("p(X = 1) p(Y = 1)", 0.264, product, quiet)
|
| 213 |
-
show("p(X = 1, Y = 1)", 0.36, joint_entry, quiet)
|
| 214 |
-
show("association missed by factorizing", 0.096, joint_entry - product, quiet)
|
| 215 |
-
return {"product": product, "joint_entry": joint_entry, "gap": joint_entry - product}
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
# %% 12. The report
|
| 219 |
-
def main() -> None:
|
| 220 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 221 |
-
parser.add_argument("--samples", type=int, default=1000, help="largest Monte Carlo sample size")
|
| 222 |
-
parser.add_argument("--steps", type=int, default=5000, help="repeats of the ten-sample estimate")
|
| 223 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed for the Monte Carlo draws")
|
| 224 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 225 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 226 |
-
args = parser.parse_args()
|
| 227 |
-
|
| 228 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same Monte Carlo ladder.
|
| 229 |
-
if not args.quiet:
|
| 230 |
-
print("Chapter 1, Sections 1.3 to 1.11, worked example by worked example\n")
|
| 231 |
-
two_token_distribution(args.quiet)
|
| 232 |
-
continuous_densities(args.quiet)
|
| 233 |
-
adenine = expected_adenine(args.quiet)
|
| 234 |
-
estimates = monte_carlo(args.samples, args.steps, args.quiet)
|
| 235 |
-
spread = worked_variance(args.quiet)
|
| 236 |
-
peptides = peptide_covariance(args.quiet)
|
| 237 |
-
noising = noising_covariance(args.quiet)
|
| 238 |
-
table = joint_marginal_conditional(args.quiet)
|
| 239 |
-
posterior = bayes(args.quiet)
|
| 240 |
-
association = independence(args.quiet)
|
| 241 |
-
|
| 242 |
-
report = {
|
| 243 |
-
"E[h(X)], expected A count": adenine["expectation"],
|
| 244 |
-
"Monte Carlo estimate at N": estimates["ladder"][-1][0],
|
| 245 |
-
"Monte Carlo estimate": estimates["ladder"][-1][1],
|
| 246 |
-
"Var(h(X))": estimates["variance"],
|
| 247 |
-
"ten-sample averages taken": estimates["rounds"],
|
| 248 |
-
"sd of those averages": estimates["measured"],
|
| 249 |
-
"Var(X) on 0, 1, 2": spread["variance"],
|
| 250 |
-
"rho_XY across three peptides": peptides["rho"],
|
| 251 |
-
"Cov(X_t)_11 from diag(4, 1)": float(noising["anisotropic"][0, 0]),
|
| 252 |
-
"p(Y = 1), high binding": float(table["p_y"][1]),
|
| 253 |
-
"p(X = 1 | Y = 1)": posterior["posterior"],
|
| 254 |
-
"p(X=1,Y=1) - p(X=1)p(Y=1)": association["gap"],
|
| 255 |
-
}
|
| 256 |
-
print_report("Probability, Sections 1.3 to 1.11", report)
|
| 257 |
-
save_run(args.out, config=vars(args), report=report)
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 261 |
-
main() # Work every example of Sections 1.3 to 1.11 and print the report.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|