キーワード: 離散フーリエ変換、高速離散フーリエ変換、多倍長演算、1変数多項式
離散フーリエ変換は、導出の部分やそこに絡む直観をすっ飛ばすと、やっていることはVandermonde行列という特殊行列を用いた行列積です。
一方で、離散フーリエ変換といえば、高速っしょ!!という名言(要出典)もある通り、高速フーリエ変換です。はい。実験のセクションでみる通り、相当速くなります。
ということで、高速化をVandermonde行列を起点に再導出する、という備忘録的な記事というのが、この文章の正体です。同じような説明は探せば出るので、新しい結果などは全く含んでいないです。
高速化の話をしたいものの、離散フーリエ変換が既に存在しているところから話を始めると面白くないのと、一方で連続フーリエ変換経由で離散フーリエ変換の導出をやる一般的な方法だと長くなりすぎます。
そこで本記事では一つの工夫として、多倍長演算の実装という、これまた結局Vandermonde行列積に行き着く別のストーリーを採用します。多倍長演算の実装に離散フーリエ変換を使うのは非常に枯れた方法で、この話の展開の仕方も新しいものではないです。
あくまでよく知られた話をまとめたメモ程度の記事です。
この記事では、 サイズの Vandermonde(ヴァンデルモンド)行列を、2つのパラメータ と を持つ形で表すことにします:
先に書いておいてなんですがVandermonde行列は、次の問題設定のセクションでは使いません。でも問題設定の後のセクションから使います。
ノート: 後で逆行列 を使うことになるので、は何らかの体の値だと思ってください。具体的なイメージを持ちたいので使う体を先に教えてくれという方にお知らせすると、複素数体と有限体 を使います。
疑問: Vandermonde行列を既知の方は、この行列の中身には色んなバリエーションがある(Wikipediaのエントリ)ことを知っているので、何故上の形を選ぶか?という 疑問 があるかと思います。これは解決されなければなりません。この要素の並びにしている理由はセクション3ならびにセクション6でお知らせします。
多倍長整数型というのは、改めてなにかと言われるとやや説明がしづらいのですが、1バイト整数とか4バイト整数とか、ああいう最小値最大値がある数(数値型)ではなく、幾らでも大きい値が書けるような数値型です。
多倍長整数(型)では、例えば以下のような
大きな数字や、その積を考えます。
この記事では、多倍長整数型がない環境に、自力で新たに実装しようというストーリーを考えます。PythonやRubyやHaskell(Integer
型)やJava(BigInteger
型)は言語組み込みの多倍長整数を持ちますが、一方でCやC++やRustだと多倍長整数型は組み込みの型ではないです。とはいえ提供ライブラリがあるのでそれを使えば良いので、別に困りはしないです。
現実的には自分で頑張るよりも、きっちり実装してくれているライブラリ、GNU GMP https://gmplib.org/ などを使うのが筋です。
ということは明記しつつ、それでも何とかやってみましょう。
多桁の数字をどう表現するか考えてすぐ思いつくのは、数を要素とするリストによって表現する方法かと思います。ここではそれと実質的に同じことをする「1変数多項式」による表現を採用します。つまり
のようにして、を代入すると元の数に戻るような1変数上の多項式として記号的に表現する、ということです。
どうしてもデータ構造的に捉えたい方は、リストや辞書型で
のように内部的に表現すると思ってください。
多項式での表現を前提にすると、乗算はどうなるか見てみます。
一例として、
という何の変哲もない計算を、多項式の乗算として処理します:
記号の注意: この記事では、多項式の積を で表します。
結果の多項式を左から順番に読んで が答えかというと、そうではなくて、 によって評価してあげる必要があり、そうすると
として正しい結果を得ることができます。
いや、で評価するって言っても、評価した後の数字を表現する数値型がないという話じゃないの、というのはその通りです。
実際には、下の桁から上に向かって繰り上がり処理をして、多項式のままで「正しい形(係数が9以下の形)」に変形します。つまり:
こういう感じでやれば、積も実行できそうです。
実装の注意:
積を実行したい多項式の次数が巨大になるにつれ、結果の多項式の係数に「相当」巨大な数が出てきます。この係数が実装言語で表現可能な整数の最大値を超えていると、そもそも上のような正規化以前の問題として、正しい多項式が表現できなくなります。この辺をどう解決するかは実用上は重要なのですが、今回は実装はそこまで頑張りたくないのでこのトピックはskipします。
多倍長整数型の積は定義できました。
今の実装は、多項式の積をナイーブに実行するものになっています。
この実装を高速化したいです。
というのは、2つの多項式の積は素朴にやると (Nは次数)の計算になって、が数十万とか数百万になると、しっかり遅くなるんですよね。
一方でこれから見る方法で まで持っていけて、実験のセクションでも見ますが、実行速度に顕著に差が出ます。
このセクションでは、高速化の準備として、2つの1変数上の多項式 について、以下のことを示します:
失礼しました。記号が沢山出てきてしまいました。各記号は
となっています。同じことですが、以下の図式を可換にしたいと言っています:
この定理には Convolution theorem という格好良さげな名前がついているのですが(Link 1 2)、Hadamard積が何者かわからないとなんとも言い難いので、とりあえず各積について見ていきます。
行列積、Hadamard積、Convolution積について一応述べます。
行列積は分かっているものとしますが、上の等式をみると、Vandermonde行列と多項式の積をとっています。いつのまに多項式が行列になったのかというと、これは記号をabuseしていまして、正確には多項式を次のように列ベクトルと同一視しています:
さて問題の行列のHadamard積ですが、これはサイズの一致する2つの行列上の積で、point-wise(各点毎)に積を実行するタイプのものです:
これだけです。この積にHadamard積という格好良い名前がついているんだなあ以上の理解は、少なくとも今回は不要です。
なんだ随分簡単だなあ というとその感想そのものが一つのミソになっていて、Vandermonde行列をかけて行列の世界に移すと、(今まさに高速化しようとしている比較的重たい計算である)多項式の積が、要素別積に化けるというのがConvolution theoremのエッセンスです。
最後に、Convolution積ですが、これは多項式の積の格好良い呼び方です。以上なのですが、それだけだと寂しいので、一応よく見かけるようなformalな書き方をやっておくと:
となり、右辺は、2つの次多項式(左辺)の畳み込み積を実行して得られる多項式での の係数 が
になると言っています。
どのサイズのVandermonde行列でも成り立つのですが、具体的に話をみたいので、以下の2つの二次多項式について考えることにします:
待てい。どこから0が出てきたということについては急いで説明させてください。との畳込み積の結果を格好つけて行列表示すると:
となります。ここのサイズと揃えたい、すなわち2次多項式の積の結果が4次多項式になるので、それに併せてとも行列で見るときには0でpaddingしています。そうしないと我々のconvolution theoremにおける行列の型が左辺と右辺で不一致するので……
あとはConvolution theoremが成立していることを確認したいのですが、実際には次の形の等式を示します:
示すといっても、各行ごと に一致していることを確認するだけです。
です。 と置くと、(Conv)の左辺の 行目は
になります。
これは、一変数上の多項式の計算としてみることができます。
勿論、実際にはは変数ではないです。気持ちの話です。
これは列ベクトルでの表現を、一つの多項式に読み替えただけになります。
上の話は、次のVandermonde行列形でも成立します(確認してみてください):
一方で、
のように、左端が1で揃っていないVandermonde形では、左辺と右辺が一般には一致しません。確認してみてください。
ということは、セクション1のVandermonde行列の形ではなくて、でも良いのではないか?ということになります。Convolution theoremを成立させるところまでで言えばそうです。
しかし、は、以降で見ていく高速化のところで少し問題になります。具体的にはここで説明しますが、結局のところ、セクション1のVandermonde行列の形を選ぶと色々都合が良いということになります。
今回は二次多項式を具体的に使って計算に基づいて確認しましたが、任意次数でも明らかに成立する議論なので、任意サイズの行列でconvolution theoremが成立します。
フーリエ変換では複素数体上の話で同じことが展開されるので、どこまで一般化出来るか分かりづらいのですが、別に複素数体でなくても、どの体(または可換環)上のVandermonde行列を用いても、(Conv)の形 は成立します。
注意点として、(Conv)の形はもともと示したかった convolution theorem:
とは違います。行列の世界に送る時に使う行列の逆行列がとれる必要があるのですが、その保証のためにVandermonde行列を使っていて、これが一つのポイントになります。
また、Vandermonde行列の旨味はそれだけに留まらず、以下で見るように分割統治法を用いた行列積の高速化の鍵ともなっています。
ここまでで、convolution theoremを経由することで、多項式の積(畳み込み積)を実行する別の手段があることをみました。
すなわち、 によって行列の世界に送って、色々やったあとに で戻せばよいということです。
ここから高速フーリエ変換の肝である分割統治法を、Vandermonde行列積の高速実行という視点からみていきます。
まずは行列の世界に送る側のを考えますが、
具体的な計算も実行したいので、例として、のVandermonde行列 を考えます。つまり
勿論これをそのまま計算しても良いのですが、の偶数添字 () と奇数添字 () に分解して計算してみます(天下り的ではありますが)。
偶数添字部分の計算では、について偶数部を取り出して、それに対応する列ベクトルたちを から取り出して、左からかけます:
同様にして奇数添字部分だけとってくると
とした上で、が明らかに成立します。
ここから「2つ」やることがあります。
はかなりに似た構造をしているので、この発想を元に、まずで計算して、が足りない部分をHadamard積で補うことで、行列を消去します:
工夫1を経て、計算のコアが
の形をしていることが分かったので、この計算ステップを減らす工夫をします。
としてこの式を書き直すと:
(横棒は単に視覚的なもので、値として深い意味があったりはしません。)
ここで が成立する を選んでおけば 嬉しいことが起こります。つまり
が成立して、ブロック行列積により
となります。4x4のVandermonde行列積を一度やって、それをコピーすれば良いので、もとの8x8の行列積の計算を半分のサイズの行列積の計算に帰着できました。
まとめると、まず
の計算をして、次に
としたあとで、以下が成立します:
一般的な高速フーリエ変換を知っている方の頭の中には「あのどこからかやってくるマイナス1倍はどこ?」という疑問があるかもしれません。何を言っているか分からない方は、そこは自然にカバーできているので、安心して次のセクションまで読み飛ばしてください。
であり、かつの全てが異なっている場合には(後でみますが、これはVandermonde行列が逆行列を持つために必要な条件です)、でなければならないので、以下が成立します:
一般的な高速フーリエ変換の説明でが出てくるのは、まさにこの部分です。
既に明らかになっていますが、結局のところの計算が、うまいことという縦横半分サイズの行列積に置き換わりました。
ここで話を止める必要はなくて、この4x4の行列積すらも、再帰的に を用いた形に変形できます。
より大きいから話をはじめると、 の形の計算は、を用いた計算に書き換えることができて、この計算が更にを用いた計算に書き換えることができて……という次第です。
高速Vandermonde変換では、行列のサイズを縦横半分にしていきたいので、スタート地点となる行列の形は、2のべき乗の
です。
また、が成立する必要もあります。この条件を (V1) と呼びます。
Vandermonde行列が逆行列を持つためには、2列目の値が全て異なること、すなわち の全てが異なる値を取らなければなりません。
これはVandermonde行列の行列式が差積で与えられるという基本的な結果から明らかです。この条件を (V2) と呼びます。
それでは、両方の条件を満たす を上手いこと作るための2つの例を見てみます。
1つ目の方法は、一般的なフーリエ変換で用いられるものですが、複素数体上で とする方法です。
複素平面上でオイラーの公式を可視化するよくある図を思い出してもらいたいのですが、あの図のおかげでになりつつも、は全て異なる値をとるというのが想起できます。よって簡単に (V1) と (V2) を満足できます。
この方法でVandermonde行列を具体化した場合に、上の方法で高速化したものが、離散高速フーリエ変換と呼ばれるものと一致しています。
じゃあ複素数体を使えば良いね、ということになるのですが、一般的なプログラミング言語の(組み込みの)複素数型は、float型
とかdouble型
とかlong double型
の、精度に限りある数値型が使われているため(参考 1)、桁数に限りのない多倍長演算を実行するには誤差などの心配がつきまといます。
この点を考慮して、別の方法である有限体による実現もみておきます。
別の方法による実現では、素数で整数を割って得られるような体 があります。
の定義は簡単で、
とすれば良いです。
例えば の場合は、 となり、 になります。2が9の逆数なのは から分かります。 が成立するので、除算も出来ていそうです。
とくに のような で表現されている素数については、フェルマーの小定理(参考 1 2) から、任意の について
が成立することが分かるので、 として採用すれば、(V1)を満たします。実際、のときには、が成立します。特にこの場合は
により(V2)も満たすので、高速Vandermonde行列積に使えます。
しかしでとして(V2)が成立するのは「偶然」です。
これを確認するために、別の例として、を考えてみます。
条件(V1) がフェルマーの小定理から成立するのですが、一方で が成立するので、(V2)が成立しません。
強調しますが、フェルマーの小定理は(V1)を保証してくれるだけです。
(V2)も満たす値の探索は、整数論の言葉で言えば、 における原始根を求める問題を解けば十分で、ということになると整数論に関する議論を使いたくなります。
このノートでは整数論の話は端折りますが、代表的な効率の良い手法は確立しているので、物凄く困ったことには一応なりません。
ここまではVandermonde行列によって、多項式を行列の世界に送る側を高速化したのですが、最後に多項式の世界に戻ってくる際にはVandermonde行列の逆行列をかける必要があります。
逆行列についても高速化のための構造が備わっていなければ困るのですが、高速逆Vandermonde行列積が実行できることが、次のことから分かります:
ここで は の積逆元で、複素数体で の場合は複素共役 を取れば良いですし、 で ならば です。
がついているのがちょっと気になりますが、これがないと
となるので仕方がないところです。ですが分割統治法を行う上では問題になりません。
が(V1)と(V2)を満たす時、もこれを満たすことに注意してください。すなわち、全く同じ方法で分割統治法により行列サイズを再帰的に半分にしていくことで、逆Vandermonde行列積も実行出来ます(最後に を全要素にかける必要はありますが)。
もし我々がVandermonde行列の形として
を採用した場合は、既にみたようにconvolution theoremは成立します。
ただし、この行列の逆行列は、各要素を逆元で置き換えた
とはなりません。
例えば の左上の要素を考えてみると、これは
となり、一方で
で、なので、左上の要素は0となります。
よって
となり、明らかに単位行列ではなく、はの逆行列ではないです。
複素数体を使った実装はたくさんあると思うので、 の方法でやってみます。こちらの実装もたくさんあるわけですが。
そこそこ大きな多項式の積を考えたいので、次の設定を使います:
この が素数かどうかは、自分で頑張っても良いですし、Wolframalphaでも確認できます。
このときは、多項式積実行後の次数がまでなら表現できるので、今回は 次数の多項式の積を実行してみます。
これはすなわち、約 35万桁 の整数同士の積を実行することになります。
C++で書いたプログラムはここにおいてあります: https://gist.github.com/yuezato/f965e51aac2ffd66924de9252b9bd897
コンパイルして実行してみると
などが得られ、まずこのノートに書いた手法だと400ms秒足らずで自分の環境だと実行できていることが分かります。
その次の Start naive
ですが、実装の正しさの確認のために、ナイーブな多項式積を実行して、これを高速化手法の結果と比較しようとするものの、数分待った程度では結果が全く返ってきません。
ということで十分高速化できているかなと思います。
念のためにもう少し小さいパラメータで実行してみると……
ということで相当高速化できていることが分かりました。
高速フーリエ変換は、フーリエ変換の式の導出が先立っていて、その出てきた式に対して「上手いこと」分割統治法が働くので、とにかく高速に実行できるという説明になりがちです。
それはそれで良いのですが
といった類のことがどうしても気になるタイプの人達というのは一定居ると思っていて、自分がそれなのですが、その人達が納得するためには、もう少し一般的な立場で展開される構成が知りたいです。
そのような構成として、Vandermonde行列を中心にした説明はある程度うまくいくかなと思います。とはいえ、はじめにでも書いたのですが、このようなVandermonde行列で捉え直す話は新しい話では全くないです、ということは再度強調しておきます。
しかし一般化という観点からはあんまり踏み込めていないので、離散コサイン変換と関連があるChebyshev-Vandermonde変換(リンク 1 pdf)とその高速化まで綺麗に書けるのかなあ、なんかは調べてみたいのですが、結局使ったことがないので手がついていません。
他にやや気になっていることとしては、Convolution theoremまわりの証明を行列の言葉でもっと綺麗に書けないかなあ、というのがあります。
今回は多項式の畳み込み積を、行列の言葉や行列演算ではなく、普通の定義で済ませています。
一方で畳み込み積は、Toeplitz行列を用いた行列積の形で定義できるので、こっちを経由するともう少し綺麗かつ一般的かつ行列の性質ベースの話で済んで、そうするとHadamard行列以外とのinteractionも自然に色々出せないかなあ〜、みたいなやつです。
何かご存知の方は教えてください。