離散フーリエ変換の高速アルゴリズムであるFFTはなぜ速いのか? について、テンソル積の意味でローカルな演算子に分解しているから、という直観的理解をする。実質的に量子フーリエ変換の回路をエミュレーションしていることになる。
DFT
N次元の離散フーリエを以下とする(規格化を一旦考えない)。
F[f](m)=0≤n<N∑f(n)exp(−2πiNnm)
逆変換は複素共役で
F−1[g](n)=0≤m<N∑g(m)exp(2πiNnm)
規格化を除いてユニタリとなる。
F−1F[f](n)=0≤m,n′<N∑f(n′)exp(−2πiNm(n′−n))=0≤n′<N∑f(n′)δnn′N=Nf(n)
FFT
DFTの係数行列は密なので、そのままだとO(N2)の計算コストがかかる。FFTはN=NkNk−1…N1へ因数分解できるとき、O((Nk+Nk−1+…N1+k)(NkNk−1…N1))へ高速化する。
この計算がいまいちピンと来なかったが、これは分割統治を テンソル積の意味で やっているとみなすと直観に適う。これを説明してみる。
N=NkNk−1…N1の因数分解を前提とするということは、信号空間もまたCN=CNk⊗CNk−1⊗…CN1のようなテンソル積分解ができることを意味する。ここで重要なのが、基底のインデックスを工夫することだ。
DFTの積分核には基底の番号が現れるので、CNk⊗CNk−1⊗…CN1の基底ベクトルのインデックスを一つの整数インデックスに対応付ける必要がある。テンソル積分解された信号空間の基底ベクトルは、0≤ni<Niからなるk個の添字で指定できる。そこでこの添字から単一整数添字への単射を作ればよい。
ここで二通りのインデックスを考える。
IL(k∣nk,nk−1…n1)==IR(k∣mk,mk−1…m1)==nk(Nk−1…N1)+nk−1(Nk−2…N1)+⋯+n2N1+n1i=1∑knij=1∏i−1Njmk+Nkmk−1+⋯+(Nk…N3)m2+(Nk…N2)m1i=1∑kmij=i+1∏kNj
直感的にはILは、各桁がNk…N1である位取りにしたがって、左をMSB、つまり最大桁としたものとなる。IRは逆に右が最大桁となる。
このインデックスによるDFTを実行する。ここで、入力側の基底インデックスと出力側の基底インデックスを一旦別の物にする。 これが計算で効いてくる。テンソル積分解された基底添字で表されたインデックスをDFTに代入すると
=F[f](IR(k∣mk,…m1)){ni∣0≤ni<Ni}i∑f(IL(k∣nk,…n1))exp(−2πiNk…N1IL(k∣nk,…n1)IR(k∣mk,…m1))
となる。これを計算していく。
積分核の分解
積分核の指数にある
Nk…N1IL(k∣nk,…n1)IR(k∣mk,…m1)
の振る舞いが重要になる。これは以下のように再帰的に分解できる。ただし、ここに2πiがかかって指数関数に入れられるので、mod1で計算する。
===Nk…N1IL(k∣nk,…n1)IR(k∣mk,…m1)Nk…N11(nk(Nk−1…N1)+IL(k−1∣nk−1,…n1))(mk+NkIR(k−1∣mk−1,…m1))Nk−1…N1IL(k−1∣nk−1,…n1)IR(k−1∣mk−1,…m1)+Nk…N1mkIL(k−1∣nk−1,…n1)+Nkmknk+nkIR(k−1∣mk−1,…m1)Nk−1…N1IL(k−1∣nk−1,…n1)IR(k−1∣mk−1,…m1)+Nk…N1mkIL(k−1∣nk−1,…n1)+Nkmknk
最後の初項にkが1ズレたものが出てきた。したがってこれを繰り返すと次のようになる。
=++++Nk…N1IL(k∣nk,…n1)IR(k∣mk,…m1)N1m1n1N2N1m2IL(1∣n1)+N2m2n2N3N2N1m3IL(2∣n2,n1)+N3m3n3⋯Nk−1…N1mk−1IL(k−2∣nk−2,…n1)+Nk−1mk−1nk−1Nk…N1mkIL(k−1∣nk−1,…n1)+Nkmknk
この式は複雑だが、Niminiの項の手前ではniはもはや出現せず、この項の後にはmiは出現しない、という良い性質がある。これによってniによる足し上げを逐次的に行ってよくなる。
=×××××F[f](IR(k∣mk,…m1))0≤n1<N1∑exp(−2πiN1m1n1)exp(−2πiN2N1m2IL(1∣n1))0≤n2<N2∑exp(−2πiN2m2n2)exp(−2πiN3N2N1m3IL(2∣n2,n1))0≤n3<N3∑exp(−2πiN3m3n3)…exp(−2πiNk−1…N1mk−1IL(k−2∣nk−2,…n1))0≤nk−1<Nk−1∑exp(−2πiNk−1mk−1nk−1)exp(−2πiNk…N1mkIL(k−1∣nk−1,…n1))0≤nk<Nk∑exp(−2πiNkmknk)f(IL(k∣nk,…n1))
ここで、総和記号による足し上げは、それ以降のすべての項に掛かっていることに注意。
説明としてはNiminiはNi次元のDFTであるから、N次元DFTが、その因数サイズのDFT+αに分解できたことになるが、しかしまだ複雑で一体どうやって加速されているのかピンとこない。
StringDiagramでのバタフライ演算
ここで、String Diagramの出番だ。この式をみると、演算の対象となっているのはf(IL(k∣nk,…n1))で、その成分添字は、∑niの前後でmiに入れ替わっている。つまり、Ni次元DFTが実行されて添字が入れ替わった。それ以外の
Tw(j):=exp(−2πiNj…N1mjIL(j−1∣nj−1,…n1))
は複雑ではあるが、この項の前後で添字は置き換わってない。これはこの項はこの瞬間採用している基底ベクトルに対しては掛け算作用素のようになっていることを意味する。掛け算作用素は疎行列だから演算は高速に実施できる。それはたかだかO(N)しかかからない。
ni↔miのNi次元DFTが実施されているときは、別にNi次元の演算が起きているわけではない。この瞬間でもni,mi以外の添字はずっと存在しているから、演算そのものはずっとN次元だ。Ni次元DFTがここに出現しているのは、その演算がni,mi以外の添字には依存しないということで、つまりni,mi以外の添字についても均一に作用しているということだ。これはテンソル積空間において、id⊗Fのような演算になっていることになる。
そういうことで、Fは、String Diagramで書くなら次のようになっている。

ただし、先のやり方で計算すると計算結果の添字はmiつまり、IRによるインデックスとして得られるので、インデックスを揃える写像を追加した。これは成分の並び替えだけなのでO(N)で実施できる。
これは量子フーリエ変換の回路と似ているが、そもそも数学的に同じものである。
よくFFTの演算はバタフライ演算と呼ばれるが、テンソル積分解とString Diagramの観点からは、テンソル積の意味でセパラブル/ローカルな演算子(と、掛け算作用素)への帰着が本質的だとわかる。一般の線形作用素はN2の計算量がかかるが、セパラブルであればそれぞれのテンソル成分に関してだけ二乗であるような作用素に分解できるからだ。
例えば
F⊗G=(id⊗G)∘(F⊗id)
は作用素テンソルの恒等式だが、計算量の観点では
(dimF×dimG)2=dimF×(dimG)2+(dimF)2×dimG=(dimF+dimG)(dimF×dimG)
と等しくない。この差を利用できるためには、演算子がテンソル積の意味でローカルな演算子に分解できればよい。
最後に計算量見積もりを確認しよう。Ni次元DFTはO(Ni2)だが、自明な作用が他の成分に作用しているから、これに他のNjの1次がかかる。つまりそれぞれについてNNiだ。Tw(j)は掛け算なので∏iNi=Nである。これがk−1層あるので、オーダーで見ればO((Nk+Nk−1+…N1+k)(NkNk−1…N1))となる。
これは分かりづらいが、よくあるNi=2,N=2kと単純化する場合は
O((Nk+Nk−1+…N1+k)(NkNk−1…N1))=O((2k+k)(2k))=O(NlogN)
となり、O(N2)よりも高速になる。
(追記)実装してみる
Ni=2で揃え、入力信号のl番目の成分を、まずl=IL(k∣nk,…,n1)だとする。そうするとniを参照するには、lを二進表記してビットにアクセスすればいい。
あとはString Diagramの通りに、jビット目の2d-DFTと、Tw(j)を交互に実施すればよい。再帰は不要で、ループで処理する。jビット目の2d-DFTと言っても、テンソル積になっているから、すべての添字を舐める間に、添字のjビット目の値だけを置き換えてアクセスすることで実視する。
処理が完了すると、ni添字がmi添字に入れ替わるが、mi添字はIR(k∣mk,…,m1)のそれなので、そのままアクセスすると順番が崩れている。添字をビット列反転して差し替えることでこれを修正する。
chatgpt氏に適当にテストコードを書いてもらおう。


Discussion