最近,陶神的一篇小論文引起了廣泛關注,我們也來跟風吃個瓜。
爲了代碼實現,有必要先來解讀一下數學公式。陶神的研究領域需要比較深的數學背景知識,一般人是很難看懂的。而這篇裏的公式涉及的數學知識大家在本科時都已經學過了,所以只要稍作展開就能看懂哦。
論文裏的證明比較簡練,爲了讓更多的吃瓜羣衆看明白,本文按照陶神論文裏的思路將證明作了詳細展開。如果不想啃公式,那就直接跳到後面看代碼吧。
理論證明
我們先看一下公式, 是一個 矩陣(簡單點,可以只考慮元素是實數的情況,就是實對稱矩陣), 表示它的第 個特徵值(實數), 是它第 個特徵向量, 就是 的第 個分量,以及由 刪掉第 行和第 列得到的子矩陣 ( 階主子式),那麼有如下公式成立,
具體一點,舉個例子看看。例如我們將 矩陣 分解爲,
其中, 是實數, 是 維向量,以及 是 的 矩陣,則有,
當然這裏假設分母不爲零。
簡單地說,就是由原矩陣的特徵值以及它所有 階主子式的特徵值,可以計算出原矩陣的所有特徵向量分量的模平方。
下面我們根據論文 https://arxiv.org/pdf/1908.03795.pdf
來理一下陶神的證明思路。
引理 假設 矩陣 的一個特徵值爲 ,不失一般性,令 ,那麼對於任意 行 列的矩陣 ,下式均成立,
其中 表示 的第 個特徵向量, 表示由一個 行 列的矩陣 和一個有 個元素的列向量 組成的 行 列的新矩陣。
證明: 對 矩陣 作特徵分解(對角化 ),得 ,其中 爲對角矩陣
將分解代入等式右端,並令 ,得
其中 表示矩陣 的前 行和前 列構成的子矩陣, 和 亦同理。
由 得 ,將之代入等式左端得,
可見,左右兩端相等,證畢 。
其實上面證明過程中很多中間步驟可以省略掉,但爲了儘可能讓更多人看明白,這裏儘量保留了詳細步驟。
定理 矩陣 的特徵向量各元素的平方與 的特徵值以及各個子矩陣 的特徵值之間有如下關係,
不失一般性,我們設 和 ,即第 個特徵向量的第 個元素。爲了使用引理,讓矩陣第 個特徵值 ,因此讓 減去 ,得 ,爲了簡化符號,變換後的新 仍然記作 ,此時 新 的特徵值與原來 的特徵值之間的關係剛好是 。因此現在有 ;當然 的其他特徵值以及 的特徵值也都作了相同偏差。關於 的特徵值,我們可以來驗證一下。將變換後的新 記爲 , 即有
它的特徵多項式爲,
可見 。同樣的,將變換後的新 仍然記爲 。
回到前面,此時矩陣 滿足引理的條件,且式 可簡化爲,
式 的右端就是 。
接下來使用引理來證明上面式 成立。我們先構造一個矩陣,
將 代入式
得左端爲,
易知右端爲 ,所以式 的左端 也等於 ,因此式 得證 。
思維導圖
這裏聯想一下上面定理的證明思路,畫了一個簡單的導圖。
最後構造了一個類似柯西–比內公式(Cauchy–Binet formula)的引理,而柯西–比內公式將行列式的可乘性推廣到非方塊矩陣,即非方塊矩陣乘積的行列式轉化爲方塊矩陣行列式的乘積之和。
另外,論文裏還提供了使用伴隨矩陣的另一種證明方法,
有興趣的話可以前往 arxiv
下載論文學習。
經過網絡傳播,各路網友紛紛加入吃瓜,後來大家知道這個公式其實早在 1968 年的線性代數書籍上就出現了,它並不是什麼新公式。那麼這次重新提出是不是完全就沒有意義呢?
可以這麼認爲,公式早就誕生了,但是可能並沒有很好地應用開。這次幾個物理學家從他們從事的中微子研究中重新發現了它,讓這個公式出現在了具體的應用領域,也具有了相應的物理意義。雖然目前應用不是很廣泛,但對於該公式來說,意義並不小,因爲後面必將會受到更多的關注。
至於該公式在機器學習或更廣的應用數學中如何應用,值得大家思考。
下面我們來用 Python 實現一下,看看它的性能如何。
代碼實現
再回顧下公式,
或者
因此,要計算矩陣 的第 個特徵向量 的第 個元素 的模,需要求出矩陣 和 子矩陣 的特徵值。
本文實現了 NumPy 和 PyTorch 兩個版本的代碼。我們只處理實對稱矩陣的情況, 矩陣只需要稍微修改一下代碼即可。由於直接根據公式來實現,所以寫法比較直接,代碼也很容易懂。
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 版本也方便。
最後留個作業
,用該公式計算 矩陣的特徵向量的時間複雜度爲多少呢?
下載本文代碼請前往倉庫。
文章首發於下面的公衆號。