File size: 5,431 Bytes
59e1897
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""Reconstruction of the two ingredients Theorem 2.10's bound is built from.

Theorem 2.10 is a *sufficiency* statement quantified over every model and every
``(eps, delta)``, so measuring one algorithm's sample requirement can corroborate
it but never establish it.  What can be established is the derivation, and it
factors into two independently measurable pieces:

  (S)  **Sensitivity.**  ``comp(Sigma, A)`` is an explicit quadratic polynomial
       in the entries of ``Sigma``, so its exact gradient is available in closed
       form.  Write ``L(Sigma, A)`` for the ``l1`` norm of that gradient, i.e.
       the largest first-order change in ``comp`` caused by an entrywise
       perturbation of ``Sigma`` of size 1.  To get ``|comp(Sigmahat) -
       comp(Sigma)| <= eps`` it suffices to estimate every entry of ``Sigma`` to
       accuracy ``t = eps / L``.

  (C)  **Concentration.**  For a centered Gaussian, ``Sigmahat_ab`` is an average
       of ``N`` iid sub-exponential variables with variance
       ``(Sigma_aa Sigma_bb + Sigma_ab^2)/N <= 2V^2/N``.  A union bound over the
       ``n(n+1)/2`` entries gives ``max_ab |Sigmahat_ab - Sigma_ab| <= t`` with
       probability ``1 - delta`` once ``N >= C (V/t)^2 log(n^2/delta)``.

Composing them, ``N >= C V^2 L^2 / eps^2 * log(n^2/delta)``.  This has the
theorem's shape exactly when ``L <= K n^2 (1+a+b)^2 V``, since then

    V^2 L^2 = K^2 n^4 (1+a+b)^4 V^4.

So the theorem's four exponents are *predictions* about two measurable
quantities, and neither measurement uses the theorem's formula:

    exponent of L in n           = 2      (=> exponent 4 of N* in n)
    exponent of L in (1+a+b)     = 2      (=> exponent 4 in (1+a+b))
    exponent of L in V           = 1      (=> exponent 4 in V, with the V^2 above)
    exponent of t in 1/sqrt(N)   = 1      (=> exponent 2 in 1/eps)
    exponent of t in sqrt(log(n^2/delta)) = 1  (=> exponent 1 in log(n/delta))

``L`` is deterministic -- no Monte-Carlo noise at all -- so its exponents come
out sharply, which is exactly where the end-to-end ``N*`` measurement is weakest.
"""

from __future__ import annotations

import numpy as np

from .linear import disjoint_path_pair_sums, gamma_from_A


def comp_gradient(Sigma: np.ndarray, A: np.ndarray) -> np.ndarray:
    """Exact gradient of ``comp`` w.r.t. the free parameters of a symmetric Sigma.

    Returns an upper-triangular array ``g`` with ``g[a, b] = d comp / d theta_ab``
    where ``theta_ab`` is the shared value of ``Sigma_ab`` and ``Sigma_ba``.

    ``comp = sum_{i<j} u_ij^2 - w_ij^2`` with ``u_ij = Sigma_ij - A_ji Sigma_ii``
    and ``w_ij = Sigma_ij - sum_{k<=i} Sigma_kk F[k,i,j]``, both *linear* in
    Sigma, so the derivative is elementary and needs no finite differences.
    """
    n = Sigma.shape[0]
    F = disjoint_path_pair_sums(gamma_from_A(A))
    diag = np.diag(Sigma)
    g = np.zeros((n, n))
    u = np.zeros((n, n))
    w = np.zeros((n, n))
    for i in range(n):
        for j in range(i + 1, n):
            u[i, j] = Sigma[i, j] - A[j, i] * Sigma[i, i]
            w[i, j] = Sigma[i, j] - float(np.dot(diag[: i + 1], F[: i + 1, i, j]))
    for i in range(n):
        for j in range(i + 1, n):
            g[i, j] += 2.0 * u[i, j] - 2.0 * w[i, j]      # via Sigma_ij itself
            g[i, i] += -2.0 * u[i, j] * A[j, i]           # via Sigma_ii in u
            for k in range(i + 1):                        # via Sigma_kk in w
                g[k, k] += 2.0 * w[i, j] * F[k, i, j]
    return g


def sensitivity(Sigma: np.ndarray, A: np.ndarray) -> float:
    """``L = ||grad comp||_1`` -- worst first-order change per unit sup-perturbation."""
    return float(np.abs(comp_gradient(Sigma, A)).sum())


def curvature(Sigma: np.ndarray, A: np.ndarray) -> float:
    """``Q``: the second-order coefficient, so that for every perturbation D

        ``|comp(Sigma+D) - comp(Sigma)| <= L ||D||_inf + Q ||D||_inf^2``.

    ``comp`` is exactly quadratic in Sigma, so this two-term bound is exact --
    there is no higher-order remainder to neglect.  ``Q`` is the ``l1`` norm of
    the quadratic form ``sum_{i<j} (du_ij)^2 - (dw_ij)^2``, bounded by summing
    the absolute coefficients.
    """
    n = Sigma.shape[0]
    F = disjoint_path_pair_sums(gamma_from_A(A))
    Q = 0.0
    for i in range(n):
        for j in range(i + 1, n):
            du = 1.0 + abs(A[j, i])                       # |dSigma_ij| + |A_ji||dSigma_ii|
            dw = 1.0 + float(np.abs(F[: i + 1, i, j]).sum())
            Q += du * du + dw * dw
    return float(Q)


def covariance_quantiles(n: int, Sigma: np.ndarray, N: int, reps: int,
                         probs, rng, block: int = 4000) -> np.ndarray:
    """Upper quantiles of ``max_{a<=b} |Sigmahat_ab - Sigma_ab|`` over ``reps`` draws.

    ``probs`` are *exceedance* probabilities (i.e. ``delta``), so the returned
    value at ``delta`` is the smallest ``t`` with an empirical
    ``P(max dev > t) <= delta``.  Computed in blocks to bound memory.
    """
    L = np.linalg.cholesky(Sigma)
    iu = np.triu_indices(n)
    devs = np.empty(reps)
    done = 0
    while done < reps:
        r = min(block, reps - done)
        z = rng.standard_normal((r, N, n))
        x = z @ L.T
        S = np.einsum("rna,rnb->rab", x, x) / N
        devs[done:done + r] = np.abs(S - Sigma)[:, iu[0], iu[1]].max(axis=1)
        done += r
    return np.quantile(devs, 1.0 - np.asarray(probs, dtype=float))