10 行 Python 代碼實現令陶哲軒驚歎的數學公式

最近,陶神的一篇小論文引起了廣泛關注,我們也來跟風吃個瓜。

爲了代碼實現,有必要先來解讀一下數學公式。陶神的研究領域需要比較深的數學背景知識,一般人是很難看懂的。而這篇裏的公式涉及的數學知識大家在本科時都已經學過了,所以只要稍作展開就能看懂哦。

論文裏的證明比較簡練,爲了讓更多的吃瓜羣衆看明白,本文按照陶神論文裏的思路將證明作了詳細展開。如果不想啃公式,那就直接跳到後面看代碼吧。

理論證明

我們先看一下公式,AA 是一個 HermitianHermitian 矩陣(簡單點,可以只考慮元素是實數的情況,就是實對稱矩陣),λi(A)\lambda_i(A) 表示它的第 ii 個特徵值(實數),viv_{i} 是它第 ii 個特徵向量,vi,jv_{i,j} 就是 viv_{i} 的第 jj 個分量,以及由 AA 刪掉第 jj 行和第 jj 列得到的子矩陣 MjM_jn ⁣ ⁣1n\!-\!\small1 階主子式),那麼有如下公式成立,

vi,j2k=1;kin(λi(A)λk(A))=k=1n1(λi(A)λk(Mj)). \begin{aligned} \left|v_{i, j}\right|^{2} &\prod_{k=1 ; k \neq i}^{n}\left(\lambda_{i}(A)-\lambda_{k}(A)\right)\\ = &\prod_{k=1}^{n-1}\left(\lambda_{i}(A)-\lambda_{k}\left(M_{j}\right)\right). \end{aligned}

具體一點,舉個例子看看。例如我們將 HermitianHermitian 矩陣 AA 分解爲,

A=(aXXM), \displaystyle A = \begin{pmatrix} a & X^* \\ X & M \end{pmatrix},

其中,aa 是實數,XXn ⁣ ⁣1{n\!-\!\small 1} 維向量,以及 MMn ⁣ ⁣1  ⁣×n ⁣ ⁣1{n\!-\!\small{1}\: \! \times \normalsize{n}\!-\!\small{1}}HermitianHermitian 矩陣,則有,

vi,12=k=1n1(λi(A)λk(M))k=1;kin(λi(A)λk(A)), \displaystyle |v_{i,1}|^2 = \frac{\prod_{k=1}^{n-1} (\lambda_i(A) - \lambda_k(M))}{\prod_{k=1; k \neq i}^n (\lambda_i(A) - \lambda_k(A))},

當然這裏假設分母不爲零。

簡單地說,就是由原矩陣的特徵值以及它所有 n ⁣ ⁣1n\!-\!\small 1 階主子式的特徵值,可以計算出原矩陣的所有特徵向量分量的模平方。

下面我們根據論文 https://arxiv.org/pdf/1908.03795.pdf 來理一下陶神的證明思路。

引理  \; 假設 HermitianHermitian 矩陣 AA 的一個特徵值爲 00,不失一般性,令 λn(A)=0\lambda_n(A)=0,那麼對於任意 nnn ⁣ ⁣1n\!-\!\small1 列的矩陣 BB,下式均成立,

i=1n1λi(A)det([B,vn])2=det(BAB),(1) \begin{aligned} & \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([B,v_{n}]\right)\right|^{2} \\ &=\operatorname{det}\left(B^{*} A B\right), \end{aligned} \tag{1}

其中 vnv_n 表示 AA 的第 nn 個特徵向量,[B,vn][B , v_{n}] 表示由一個 nnn ⁣ ⁣1n\!-\!\small1 列的矩陣 BB 和一個有 nn 個元素的列向量 vnv_n 組成的 nnnn 列的新矩陣。

證明: 對 HermitianHermitian 矩陣 AA 作特徵分解(對角化 AA),得 A=VDVA=V D V^{*},其中 DD 爲對角矩陣

Ddiag(λ1(A),,λn1(A),0). D \equiv \operatorname{diag}\left(\lambda_{1}(A), \ldots, \lambda_{n-1}(A), 0\right).

將分解代入等式右端,並令 U=VBU = V^{*}B,得

     ⁣ ⁣ ⁣ ⁣  det(BAB)=det(BVDVB)=det(UDU)=det(Un1×n1Dn1×n1Un1×n1)=det(Dn1×n1)det(Un1×n1)2, \begin{aligned} &\quad\;\;\!\!\!\!\;\operatorname{det}\left(B^{*} A B\right) \\[1.5mm] &= \operatorname{det}\left(B^{*} V D V^{*} B\right) \\[1.5mm] &= {\operatorname{det}\left(U^{*} D U\right)} \\[1.5mm] &= {\operatorname{det}\left(U^{*}_{\large n\small{-1} \times {\large n\small{-1}}} D_{\large n\small{-1} \times {\large n\small{-1}}} U_{\large n\small{-1} \times {\large n\small{-1}}}\right)} \\[1.5mm] &= \color{#a3a}{\operatorname{det}\left(D_{\large n\small{-1} \times {\large n\small{-1}}} \right) \left|\operatorname{det}\left(U_{\large n\small{-1} \times {\large n\small{-1}}} \right)\right|^2}, \end{aligned}

其中 Dn1×n1D_{\large n\small{-1} \times {\large n\small{-1}}} 表示矩陣 DD 的前 n1\large n\small{-1} 行和前 n1\large n\small{-1} 列構成的子矩陣,Un1×n1U^{*}_{\large n\small{-1} \times {\large n\small{-1}}}Un1×n1U_{\large n\small{-1} \times {\large n\small{-1}}} 亦同理。

U=VBU = V^{*}BB=VUB = VU,將之代入等式左端得,

     ⁣ ⁣ ⁣ ⁣  i=1n1λi(A)det([B,vn])2=i=1n1λi(A)det([VU,vn])2=i=1n1λi(A)det([VU,VVvn])2=i=1n1λi(A)det(V[U,Vvn])2=i=1n1λi(A)det([U,Vvn])2=i=1n1λi(A)det([U,en])2=det(Dn1×n1)det([U,en])2=det(Dn1×n1)det(Un1×n1)2. \begin{aligned} &\quad\;\;\!\!\!\!\; \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([B, v_{n}]\right)\right|^{2} \\ &= \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([VU, v_{n}]\right)\right|^{2} \\ &= \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([VU, VV^{*}v_{n}]\right)\right|^{2} \\ &= \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left(V[U, V^{*}v_{n}]\right)\right|^{2} \\ &= \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([U, V^{*}v_{n}]\right)\right|^{2} \\ &= \color{#a3a}{\prod_{i=1}^{n-1} \lambda_{i}(A)}\color{#a33}{\left|\operatorname{det}\left([U, e_{n}]\right)\right|^{2}} \\ &= \color{#a3a}{\operatorname{det}\left(D_{\large n\small{-1} \times {\large n\small{-1}}} \right)}\color{#a33}{\left|\operatorname{det}\left([U, e_{n}]\right)\right|^{2}} \\[2.15mm] &= \color{#a3a}{\operatorname{det}\left(D_{\large n\small{-1} \times {\large n\small{-1}}} \right)}\color{#a33}{ \left|\operatorname{det}\left(U_{\large n\small{-1} \times {\large n\small{-1}}} \right)\right|^2}. \end{aligned}

可見,左右兩端相等,證畢 \boxdot

其實上面證明過程中很多中間步驟可以省略掉,但爲了儘可能讓更多人看明白,這裏儘量保留了詳細步驟。

定理  \; 矩陣 AA 的特徵向量各元素的平方與 AA 的特徵值以及各個子矩陣 {Mj}\{M_j\} 的特徵值之間有如下關係,

vi,j2k=1;kin(λi(A)λk(A))=k=1n1(λi(A)λk(Mj)).(2) \begin{aligned} \left|v_{i, j}\right|^{2} & \prod_{k=1 ; k \neq i}^{n}\left(\lambda_{i}(A)-\lambda_{k}(A)\right) \\ = & \prod_{k=1}^{n-1}\left(\lambda_{i}(A)-\lambda_{k}\left(M_{j}\right)\right). \end{aligned} \tag{2}

不失一般性,我們設 j=1j=1i=ni=n,即第 nn 個特徵向量的第 11 個元素。爲了使用引理,讓矩陣第 nn 個特徵值 λn(A)=0\lambda_{n}(A) = 0,因此讓 AA 減去 λn(A)In\lambda_n (A)I_n,得 A=Aλn(A)InA = A - \lambda_n (A)I_n,爲了簡化符號,變換後的新 AA 仍然記作 AA,此時 新 AA 的特徵值與原來 AA 的特徵值之間的關係剛好是 λnew=λoldλn\lambda_{new} = \lambda_{old} - \lambda_{n}。因此現在有 λn(A)=0\lambda_n(A) = 0;當然 AA 的其他特徵值以及 MjM_j 的特徵值也都作了相同偏差。關於 MjM_j 的特徵值,我們可以來驗證一下。將變換後的新 MjM_j 記爲 M^j\hat{M}_j, 即有

M^j=MjλnIn1×n1, \hat{M}_j = M_j - \lambda_n I_{\large n\small{-1} \times {\large n\small{-1}}},

它的特徵多項式爲,

   ⁣M^jλnewIn1×n1=MjλnIn1×n1λnewIn1×n1=Mj(λn+λnew)In1×n1. \begin{aligned} &\quad\; \! \left| \hat{M}_j - \lambda_{new} I_{\large n\small{-1} \times {\large n\small{-1}}}\right| \\[1.98mm] &= \left| M_j - \lambda_n I_{\large n\small{-1} \times {\large n\small{-1}}} - \lambda_{new} I_{\large n\small{-1} \times {\large n\small{-1}}}\right| \\[1.98mm] &= \left| M_j - (\lambda_n + \lambda_{new}) I_{\large n\small{-1} \times {\large n\small{-1}}}\right|. \end{aligned}

可見 λnew=λoldλn\lambda_{new} = \lambda_{old} - \lambda_{n}。同樣的,將變換後的新 MjM_j 仍然記爲 MjM_j

回到前面,此時矩陣 AA 滿足引理的條件,且式 (2)(2) 可簡化爲,

vn,12k=1n1λk(A)=k=1n1λk(M1).(3) \left|v_{n, 1}\right|^{2} \prod_{k=1}^{n-1} \lambda_{k}(A)=\prod_{k=1}^{n-1} \lambda_{k}\left(M_{1}\right). \tag{3}

(3)(3) 的右端就是 det(M1)\operatorname{det}\left(M_{1}\right)

接下來使用引理來證明上面式 (3)(3) 成立。我們先構造一個矩陣,

B=(0In1)=(001001)n×n1. B=\left(\begin{array}{c}{0} \\ {I_{n\small{-1}}}\end{array}\right) = \begin{pmatrix} 0 & \cdots & 0 \\ 1 & \cdots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \cdots & 1 \end{pmatrix}_{\large n \times {\large n\small{-1}}}.

BB 代入式 (1)(1)

i=1n1λi(A)det([B,vn])2=det(BAB) \prod_{i=1}^{n-1} \lambda_{i}(A)\left|\operatorname{det}\left([B,v_{n}]\right)\right|^{2}=\operatorname{det}\left(B^{*} A B\right)

得左端爲,

i=1n1λi(A)vn,12. \prod_{i=1}^{n-1} \lambda_{i}(A)\left|v_{n, 1}\right|^{2}.

易知右端爲 det(M1)\operatorname{det}\left(M_{1}\right),所以式 (3)(3) 的左端 vn,12k=1n1λk(A)\left|v_{n, 1}\right|^{2} \prod_{k=1}^{n-1} \lambda_{k}(A) 也等於 det(M1)\operatorname{det}\left(M_{1}\right),因此式 (3)(3) 得證 \boxdot

思維導圖

這裏聯想一下上面定理的證明思路,畫了一個簡單的導圖。

最後構造了一個類似柯西–比內公式(Cauchy–Binet formula)的引理,而柯西–比內公式將行列式的可乘性推廣到非方塊矩陣,即非方塊矩陣乘積的行列式轉化爲方塊矩陣行列式的乘積之和。

另外,論文裏還提供了使用伴隨矩陣的另一種證明方法,

adj(λInA)=det(λInA)(λInA)1. \operatorname{adj}\left(\lambda I_{n}-A\right)=\operatorname{det}\left(\lambda I_{n}-A\right)\left(\lambda I_{n}-A\right)^{-1}.

有興趣的話可以前往 arxiv 下載論文學習。

經過網絡傳播,各路網友紛紛加入吃瓜,後來大家知道這個公式其實早在 1968 年的線性代數書籍上就出現了,它並不是什麼新公式。那麼這次重新提出是不是完全就沒有意義呢?

可以這麼認爲,公式早就誕生了,但是可能並沒有很好地應用開。這次幾個物理學家從他們從事的中微子研究中重新發現了它,讓這個公式出現在了具體的應用領域,也具有了相應的物理意義。雖然目前應用不是很廣泛,但對於該公式來說,意義並不小,因爲後面必將會受到更多的關注。

至於該公式在機器學習或更廣的應用數學中如何應用,值得大家思考。

下面我們來用 Python 實現一下,看看它的性能如何。

代碼實現

再回顧下公式,

vi,j2k=1;kin(λi(A)λk(A))=k=1n1(λi(A)λk(Mj)), \begin{aligned} \left|v_{i, j}\right|^{2} \prod_{k=1 ; k \neq i}^{n}\left(\lambda_{i}(A)-\lambda_{k}(A)\right) = \prod_{k=1}^{n-1}\left(\lambda_{i}(A)-\lambda_{k}\left(M_{j}\right)\right), \end{aligned}

或者

vi,j2=k=1n1(λi(A)λk(Mj))k=1;kin(λi(A)λk(A)). \displaystyle |v_{i,j}|^2 = \frac{\prod_{k=1}^{n-1} (\lambda_i(A) - \lambda_k(M_j))}{\prod_{k=1; k \neq i}^n (\lambda_i(A) - \lambda_k(A))}.

因此,要計算矩陣 AA 的第 ii 個特徵向量 viv_{i} 的第 jj 個元素 vi,jv_{i,j} 的模,需要求出矩陣 AA 和 子矩陣 MjM_j 的特徵值。

本文實現了 NumPy 和 PyTorch 兩個版本的代碼。我們只處理實對稱矩陣的情況,HermitianHermitian 矩陣只需要稍微修改一下代碼即可。由於直接根據公式來實現,所以寫法比較直接,代碼也很容易懂。

NumPy 版本

import numpy as np
from numpy import linalg as LA
np.__version__
'1.17.2'
# 取 A 的子矩陣 M_j,多個版本,可以比較性能
def sub_matrix_np0(A, n, j):
    row, row[j] = np.ones(n, dtype=bool), False
    return A[row][:,row]

def sub_matrix_np1(A, n, j):
    row, row[j:] = np.arange(n-1), np.arange(n-1)[j:]+1
    return A[row][:,row]

def sub_matrix_np2(A, n, j):
    row = np.arange(n)
    row = np.concatenate([row[:j], row[j+1:]])
    return A[row][:,row]
# 此處用到 NumPy 只計算特徵值,不計算特徵向量
def eigv_ij_tao_np(A, i, j):
    eigvals = LA.eigvals(A)
    M_j = sub_matrix_np0(A, eigvals.shape[0], j)
    eigvals_M_j = LA.eigvals(M_j)
    eigvals_M_j = eigvals_M_j - eigvals[i]
    eigvals, eigvals[i] = eigvals - eigvals[i], 1.0
    return np.prod(eigvals_M_j) / np.prod(eigvals)
# 我們使用如下實對稱矩陣作測試
A = np.array([[3.20233843, 2.06764157, 2.88108853, 3.04719701, 1.35948144,
        2.56403503, 1.45954379, 2.13631733, 1.53986513, 3.25995899],
       [2.06764157, 2.29127686, 2.1014315 , 2.70700189, 1.39188518,
        2.29620216, 1.40820018, 1.53013976, 1.63189934, 2.82578892],
       [2.88108853, 2.1014315 , 3.50400477, 3.47397131, 1.76088485,
        3.23409601, 1.32428533, 2.77130533, 2.23036096, 3.42124947],
       [3.04719701, 2.70700189, 3.47397131, 4.73687997, 2.20972905,
        3.77378375, 2.34901207, 2.48563082, 2.19559837, 4.13322167],
       [1.35948144, 1.39188518, 1.76088485, 2.20972905, 1.59570009,
        2.14854992, 1.1252818 , 1.94769495, 1.34582313, 2.1726746 ],
       [2.56403503, 2.29620216, 3.23409601, 3.77378375, 2.14854992,
        3.90925026, 2.11631829, 2.99858233, 2.44964915, 3.61299442],
       [1.45954379, 1.40820018, 1.32428533, 2.34901207, 1.1252818 ,
        2.11631829, 2.14092743, 1.13700682, 1.22321095, 2.05145548],
       [2.13631733, 1.53013976, 2.77130533, 2.48563082, 1.94769495,
        2.99858233, 1.13700682, 3.41056761, 2.08224044, 3.02608265],
       [1.53986513, 1.63189934, 2.23036096, 2.19559837, 1.34582313,
        2.44964915, 1.22321095, 2.08224044, 2.12302856, 2.41838461],
       [3.25995899, 2.82578892, 3.42124947, 4.13322167, 2.1726746 ,
        3.61299442, 2.05145548, 3.02608265, 2.41838461, 4.55396155]])
n = A.shape[0]
# 測試,計算第 n 個特徵向量分量的模平方
[eigv_ij_tao_np(A, i=n-1, j=j) for j in range(n)]
[0.010078898355312161,
 0.008526097435704025,
 0.03552221239921944,
 0.021411824331722774,
 0.17690013381018135,
 0.5189551148981736,
 0.06059635621137234,
 0.00246512246354466,
 0.10612109694238948,
 0.059423143152378614]
%%timeit
[eigv_ij_tao_np(A, i=n-1, j=j) for j in range(n)]
766 µs ± 3.02 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
# 設置 numpy 輸出的精度,用於展示精度對比
np.set_printoptions(precision=18)
# 使用 numpy.linalg 直接計算結果,比較用
n = A.shape[0]
(LA.eig(A)[1]**2)[:,n-1].reshape(-1,1)
array([[0.010078898355311885],
       [0.008526097435703766],
       [0.035522212399219155],
       [0.021411824331722753],
       [0.17690013381018374 ],
       [0.5189551148981745  ],
       [0.0605963562113716  ],
       [0.002465122463544841],
       [0.10612109694238844 ],
       [0.059423143152379225]])
%%timeit
(LA.eig(A)[1]**2)[:,n-1].reshape(-1,1)
39.8 µs ± 227 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

看到沒有,NumPy 直接計算特徵向量的效率明顯比上面 Tao 算法的實現要高效得多。

PyTorch 版本

import torch
torch.__version__
'1.1.0'
# 設置 torch 輸出的精度,用於展示精度對比
torch.set_printoptions(precision=18)
# 取 A 的子矩陣 M_j,多個版本,可以比較性能
def sub_matrix_torch0(A, n, j):
    row, row[j] = torch.ones(n, dtype=torch.bool), False
    return A[row][:,row]

def sub_matrix_torch1(A, n, j):
    row, row[j:] = torch.arange(n-1), torch.arange(n-1)[j:]+1
    return A[row][:,row]

def sub_matrix_torch2(A, n, j):
    row = torch.arange(n)
    row = torch.cat([row[:j], row[j+1:]])
    return A[row][:,row]
def eigv_ij_tao_torch(A, i, j):
    eigvals, _ = torch.symeig(A)
    M = sub_matrix_torch2(A, eigvals.shape[0], j)
    eigvals_M_j, _ = torch.symeig(M)
    eigvals_M_j = eigvals_M_j - eigvals[i]
    eigvals, eigvals[i] = eigvals - eigvals[i], 1.0
    return torch.prod(eigvals_M_j) / torch.prod(eigvals)
A_torch = torch.tensor(A)
n = A_torch.shape[0]
# 由於特徵值次序不同,所以這裏 i 跟上面 numpy 版本取值不同
[eigv_ij_tao_torch(A_torch, i=n-8, j=j) for j in range(n)]
[tensor(0.010078898355311965, dtype=torch.float64),
 tensor(0.008526097435700507, dtype=torch.float64),
 tensor(0.035522212399216449, dtype=torch.float64),
 tensor(0.021411824331723325, dtype=torch.float64),
 tensor(0.176900133810184573, dtype=torch.float64),
 tensor(0.518955114898170700, dtype=torch.float64),
 tensor(0.060596356211371390, dtype=torch.float64),
 tensor(0.002465122463545969, dtype=torch.float64),
 tensor(0.106121096942389470, dtype=torch.float64),
 tensor(0.059423143152376595, dtype=torch.float64)]

可以跟上面 NumPy 版本的結果對比一下看是否一致。

%%timeit
[eigv_ij_tao_torch(A_torch, i=n-8, j=j) for j in range(n)]
828 µs ± 2.27 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

從這個小矩陣來看,PyTorch 實現的效率還不如 NumPy 版本。
在實現之際,發現網絡上已經有一些版本,本文的實現跟這個版本比較接近,但比它的更加簡短且性能更好一些。

性能對比

上面使用一個小矩陣測試代碼,下面用比較大的矩陣測試對比下 NumPy 和 PyTorch 實現的兩個版本的性能。

A = np.random.rand(200, 200)
A = A.dot(A.T)

NumPy 版本 Tao 算法:

3.08 s ± 19.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

PyTorch 版本 Tao 算法:

539 ms ± 1.79 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

NumPy 直接求解特徵向量:

10.8 ms ± 33.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

PyTorch 直接求解特徵向量:

10.5 ms ± 53.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

從陶等人的公式本身可想而知,計算效率並不高。不過這個公式並不是爲了計算來的,而是揭示了特徵值與特徵向量分量的模之間的關係。

另外,本人其實更喜歡 Julia 語言,但考慮到目前瞭解 Python 的人更多。上面代碼要改寫成 Julia 版本也方便。

最後留個作業,用該公式計算 HermitianHermitian 矩陣的特徵向量的時間複雜度爲多少呢?

下載本文代碼請前往倉庫
文章首發於下面的公衆號。

公衆號

發佈了4 篇原創文章 · 獲贊 2 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章