首页 FFT-快速傅里叶变换的推导、推广与优化
文章
取消

FFT-快速傅里叶变换的推导、推广与优化

Part 3 - Radix-N FFT Generalization

Radix 组合间的映射函数

在上面的推导中,映射函数只考虑了使用一个 Radix 的情况,现在考虑多个 Radix 时数据的映射

我们最希望看到的情况当然是不同的Radix间在等价步骤的数据的等价的,这样我们不需要做任何额外的处理,让我们来推导验证一下

对于 $R_1$ $R_2$ 两个Radix 的总步骤数为 $L_1 = \log_{R_1}N$ $L_2 = \log_{R_2}N$,假设 $L_1 = 2L_2$ 即 $R_1 = R_2^2$,则

它们的等价步骤为 $l_1 = {1,2,\cdots,L_1}$ $l_2 = {{1,2},{3,4},\cdots, {L_2-1, L_2 }}$

我们只需要验证 $\text{Radix }R_1$ 中的第 $l_1$ 个步骤与 $\text{Radix }R_2$ 中的第 ${2l_1-1,2l_1}$ 步骤的映射等价

由于手推复合函数的工作量实在太大,这样验证实在有些困难

这里从另一个角度来解释这个问题

回顾一下 $\text{Radix }4$ 的递推公式

\[\begin{align*} X_N[k] &= X_{N/4}[4k] + W_N^kX_{N/4}[4k+1] + W_N^{2k}X_{N/4}[4k+2] + W_N^{3k}X_{N/4}[4k+3]\\ &= f^4_{N}(X_{N/4}[4k], X_{N/4}[4k+1], X_{N/4}[4k+2], X_{N/4}[4k+3]) \end{align*}\]

在推导 $\text{Radix }2$ 过程中我们得到

\[\begin{align*} X_N[k]&= X_{N/2}[2k] + W_N^kX_{N/2}[2k+1]\\ &= f^2_{N}(X_{N/2}[2k], X_{N/2}[2k+1])\\ &= f^2_{N}(f^2_{N/2}(X_{N/4}[4k], X_{N/4}[4k+2]), f^2_{N/2}(X_{N/4}[4k+1], X_{N/4}[4k+3])) \end{align*}\]

两次$\text{Radix }2$ 的访问序列 ${X_{N/4}[4k], X_{N/4}[4k+2], X_{N/4}[4k+1], X_{N/4}[4k+3]}$ 与 $\text{Radix }4$ 的访问序列 ${X_{N/4}[4k], X_{N/4}[4k+1], X_{N/4}[4k+2], X_{N/4}[4k+3]}$ 是等价的

同理可以推广到 $\log_2R$ 次 $\text{Radix }2$ 的访问序列与 $\text{Radix }R$ 的访问序列是等价的 $\text{Radix }R_1$ 与 $\log_2R_1$ 次$\text{Radix }2$ 的映射等价,$\text{Radix }R_2$ 与 $\log_2R_2$次$\text{Radix }2$ 的映射等价 所以 $\text{Radix }R_1$ 与 $log_{R_2}R_1$ 次 $\text{Radix }R_2$ 的映射等价

其实 Radix-4 Radix-8 …… 都可以看作是 Radix-2 的的递归展开,所以它们自上而下是等价的

为什么说是自上而下,这里涉及到一个对齐问题或等价替换问题

简单来说是 Radix-N 的替换必须与 Radix-N 计算步骤对齐,这样才能保证等价

image-20230623201646239

这意味着在现有的公式下我们在前的贪心策略是不可行的,因为它会导致不对齐的情况

我们需要将未对齐的 Radix-N 细分为 Radix-N/2 直到对齐为止

或者我们可以推导Radix-N到第 $l$ 次 Radix-2的映射,来解决任意Radix间的映射问题, 更宽泛一点地说,我们可以推导 $\text{Radix } R_1$ 的第 $\overline{l_1}$ 次映射到 $\text{Radix } R_2$ 的映射

在前推导的映射函数如下

\[X_{N/R^{\overline{l}}}[k] =\ _{R}f^{k-k \text{ mod } R^{\overline{l}}}_{N} \begin{pmatrix} X_{N/R^{\overline{l}+1}}[Rk+r\cdot R^{\overline{l}} - (R-1)\cdot(k \text{ mod } R^{\overline{l}})] \end{pmatrix}\]

为书写方便 记 $ d =\overline{l}$ 为 $\text{Radix } R$ 的递归深度 $ d \in {0,1,2,\cdots,\log_RN-1}$

\[X_{N/R^{d}}[k] =\ _{R}f^{k-k \text{ mod } R^{d}}_{N} \begin{pmatrix} X_{N/R^{d+1}}[Rk+r\cdot R^{d} - (R-1)\cdot(k \text{ mod } R^{d})] \end{pmatrix}\]

$\text{Radix } R_1$ 的递归深度为 $d_1$

假定需要在 $\text{Radix } R_1$ $d_1$展开后 进行 $\text{Radix } R_2$ 的展开

\[X_{N/R^{d_1}}[k] =\ _{R_1}f^{k-k \text{ mod } R_1^{d_1}}_{N} \begin{pmatrix} X_{N/R^{d_1+1}}[R_1k+r_1\cdot R_1^{d_1} - (R_1-1)\cdot(k \text{ mod } R_1^{d_1})] \end{pmatrix}\]

将 $R_1k$ 换元为 $R_1(R_2 k’ + r_2)$

得到

\[\begin{align*} &X_{N/R^{d_1+1}}[R_1k'+r_1\cdot R_1^{d_1} - (R_1-1)\cdot(k' \text{ mod } R_1^{d_1})]\\ &=\ _{R_2}f^{k'}_{N/R^{d_1+1}} \begin{pmatrix} X_{N/(R_1^{d_1+1}\cdot R_2)} [R_1(R_2 k' + r_2)+r_1\cdot R_1^{d_1} - (R_1-1)\cdot(k' \text{ mod } R_1^{d_1})]\\ \end{pmatrix} \end{align*}\]

首先从 $R_1k’+r_1\cdot R_1^{d_1} - (R_1-1)\cdot(k’ \text{ mod } R_1^{d_1}) = k$ 中求解 $k’$ 但是这个函数按照函数的在数学上定义是没有反函数的,需要保留 $k’$

\[k' = \frac{k - r_1\cdot R_1^{d_1} + (R_1-1)\cdot(k' \text{ mod } R_1^{d_1})}{R_1}\]

我在推导时被卡在这里了,这种推导方法就留给读者钻研了

在这里通过另一个思路来推导

在 Radix-2 Radix-4 的推导中,因为带模的函数过于复杂,在推导过程中并没有使用带模的函数作为中继,而是从原始的递推式中不断进行递推,并在每一步中将其转换为带模的形式

令 $s \in {0,1,2,\cdots,R_1^{d_1}-1}$ $\text{Radix } R_1$ 的递归深度为 $d_1$ 的原始递推式为

\[X_{N/R_1^{d_1}}[R_1^{d_1} k + s] =\ _{R_1}f^{k}_{N/R_1^{d_1}}\begin{pmatrix} X_{N/R_1^{d_1+1}}[R^{d_1+1}k + R^{d_1}r_1 + s] \end{pmatrix}\]

在这一步不再进行$\text{Radix } R_1$,而是进行 $\text{Radix } R_2$ 的展开

\[X_{N/R_1^{d_1}}[R_1^{d_1} k + s] =\ _{R_2}f^{k}_{N/R_1^{d_1}}\begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[R_1^{d_1}R_2k+R_1^{d_1}r_2 + s] \end{pmatrix}\]

这样求逆函数要容易地多,最终得到

\[\begin{align*} X_{N/R_1^{d_1}}[k] &=\ _{R_2}f^{(k-s)/R_1^{d_1}}_{N/R_1^{d_1}} \begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[R_1^{d_1}r_2+R_2(k-s)+s] \end{pmatrix}\\ &=\ _{R_2}f^{k-s}_{N} \begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[R_1^{d_1}r_2+R_2k-(R_2-1)s] \end{pmatrix}\\ &=\ _{R_2}f^{k-k\text{ mod }R^{d_1}}_{N} \begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[R_2k+R_1^{d_1}r_2-(R_2-1) (k\text{ mod }R^{d_1})] \end{pmatrix}\\ X_{N/R_1^{d_1}}[k] &=\ _{R_2}f^{k-k\text{ mod }R^{d_1}}_{N} \begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[R_2k+R_1^{d_1}r_2-(R_2-1) (k\text{ mod }R^{d_1})] \end{pmatrix}\\ \end{align*}\]

化简

\[\begin{align*} R_2k+R_1^{d_1}r_2-(R_2-1) (k\text{ mod }R^{d_1}) &= R_2k+R_1^{d_1}r_2-(R_2-1) (k - R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor)\\ &= R_2k+R_1^{d_1}r_2-R_2k+k + (R_2-1)R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor\\ &= R_1^{d_1}r_2+k + (R_2-1)R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor\\ k-k\text{ mod }R^{d_1}& = R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor \end{align*}\] \[X_{N/R_1^{d_1}}[k] =\ _{R_2}f^{R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor}_{N} \begin{pmatrix} X_{N/(R_1^{d_1}\cdot R_2)}[(R_2-1)R_1^{d_1} \lfloor \frac{k}{R_1^{d_1}} \rfloor +k+ R_1^{d_1}r_2] \end{pmatrix}\]

这样就可以愉快地使用贪心策略了

非二的整数幂的 Radix-N FFT

在上的公式可以直接推广到非二的整数幂的 Radix 因为在推导过程中并没有使用与整数幂相关的性质

在前推导的公式是多次使用一个 Radix 对 FFT 进行分解,然后使用第二个 Radix 进行一次分解

考虑自上向下使用 Raidx 序列 ${R_1,R_2\cdots}$ 对 FFT 进行分解

则有

\[\begin{align*} X_{N}[k] &=\ _{R_1}f^{ \lfloor \frac{k}{1} \rfloor}_{N} \begin{pmatrix} X_{N/\cdot R_1}[(R_1-1)\cdot 1 \cdot\lfloor \frac{k}{1} \rfloor +k+ 1r_1] \end{pmatrix}\\ X_{N/R_1}[k] &=\ _{R_2}f^{R_1 \lfloor \frac{k}{R_1} \rfloor}_{N} \begin{pmatrix} X_{N/(R_1\cdot R_2)}[(R_2-1)R_1 \lfloor \frac{k}{R_1} \rfloor +k+ R_1r_2] \end{pmatrix}\\ X_{N/(R_1R_2)}[k] &=\ _{R_3}f^{R_1R_2 \lfloor \frac{k}{R_1R_2} \rfloor}_{N} \begin{pmatrix} X_{N/(R_1R_2\cdot R_3)}[(R_3-1)R_1R_2 \lfloor \frac{k}{R_1R_2} \rfloor +k+ R_1R_2r_3] \end{pmatrix}\\ &\vdots\\ X_{N/(\prod_{i=1}^{n-1}R_i)}[k] &=\ _{R_n}f^{\prod_{i=1}^{n-1}R_i \lfloor \frac{k}{\prod_{i=1}^{n-1}R_i} \rfloor}_{N} \begin{pmatrix} X_{N/(\prod_{i=1}^{n-1}R_i\cdot R_n)}[(R_n-1)\prod_{i=1}^{n-1}R_i \lfloor \frac{k}{\prod_{i=1}^{n-1}R_i} \rfloor +k+ \prod_{i=1}^{n-1}R_ir_n] \end{pmatrix}\\ \end{align*}\]

取模后就得到最为一般的 Radix-N FFT 的映射函数

\[X_{N/(\prod_{i=1}^{n-1}R_i)}[k] =\ _{R_n}f^{\prod_{i=1}^{n-1}R_i \lfloor \frac{k}{\prod_{i=1}^{n-1}R_i} \rfloor}_{N} \begin{pmatrix} \begin{pmatrix} X_{N/(\prod_{i=1}^{n-1}R_i\cdot R_n)}[(R_n-1)\prod_{i=1}^{n-1}R_i \lfloor \frac{k}{\prod_{i=1}^{n-1}R_i} \rfloor +k+ \prod_{i=1}^{n-1}R_ir_n] \end{pmatrix} \text{ mod } N \end{pmatrix}\]

整理之后

\[\begin{align*} X_{N/P}[k] &=\ _{R_n}f^{b}_{N} \begin{pmatrix} X_{N/(P\cdot R_n)}[(k + b\cdot(R_n-1) + P \cdot r_n)\text{ mod } N] \end{pmatrix}\\ b &= P \lfloor \frac{k}{P} \rfloor\\ P &= \prod_{i=1}^{n-1}R_i\\ &_Rf^b_N(x_0, x_1, \cdots, x_{R-1}) = \sum_{r=0}^{R-1}x_rW_N^{br}\\ \end{align*}\]

Radix-N FFT 的分解策略

当使用整数幂 Radix 时,可以对乘除法进行优化,但是当使用非整数幂 Radix 时,乘除法会来带额外的开销,所以非整数幂应该尽可能少

其次2的整数幂数在进行乘、除、取模时都可以使用位运算进行优化,而非整数幂则不能,在 Radix-N FFT 中应该尽可能让运算的数是2的整数幂,而与分解策略直接相关的是 $P = \prod_{i=1}^{n-1}R_i$

因此在进行分解时,应该先将因子 2 分解出来,然后再分解其他的因子,这样可以保证在最后的几层运算中 $P$ 是2的整数幂

Raidx 序列实际上是 FFT 大小 $N$ 的因子序列

因为分解策略的静态编译的,我们不太需要担心分解策略的性能问题

先把 $N$ 的 $2^a$ 因子分解出来

$N$ 的 $2$ 因子数在二进制中是数的低位的连续的 $0$ 的个数,所以可以使用位运算进行分解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def factor2(n):
    a = 0
    if n & 0x0000ffff == 0:
        a += 16
        n >>= 16
    if n & 0x000000ff == 0:
        a += 8
        n >>= 8
    if n & 0x0000000f == 0:
        a += 4
        n >>= 4
    if n & 0x00000003 == 0:
        a += 2
        n >>= 2
    if n & 0x00000001 == 0:
        a += 1
        n >>= 1
    return a

对于剩下的数的分解,要求分解的因子尽可能大,但小于 Radix-Max,直接使用试除法

最后完整的分解策略如下

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
def factorize(n, max_factor):
    def factor2(n):
        log2_factor = 0
        if n & 0x0000ffff == 0:
            log2_factor += 16
            n >>= 16
        if n & 0x000000ff == 0:
            log2_factor += 8
            n >>= 8
        if n & 0x0000000f == 0:
            log2_factor += 4
            n >>= 4
        if n & 0x00000003 == 0:
            log2_factor += 2
            n >>= 2
        if n & 0x00000001 == 0:
            log2_factor += 1
            n >>= 1
        return log2_factor

    log2_factor = factor2(n)
    n >>= log2_factor
    factors = []
    f = max_factor - 1
    sqrt_n = int(np.sqrt(n))
    while f >= sqrt_n:
        f -= 2
    while f > 2 and n > max_factor:
        if n % f == 0:
            factors.append(f)
            n //= f
        else:
            f -= 2
    if n != 1:
        if n > max_factor:
            f = 3
            while f*f < n:
                if n % f == 0:
                    factors.append(f)
                    n //= f
                else:
                    f += 2
            if n != 1:
                factors.append(n)
        else:
            factors.append(n)
    return log2_factor, factors


def radix_seqence(n, log2_max_radix):
    radix = []
    log2_radix = []
    max_radix = 1 << log2_max_radix
    log2_factor, factors = factorize(n, max_radix)

    for f in factors:
        radix.append(f)
        log2_radix.append(0)
    non_pow2_pass = len(radix)

    max_radix_pass = log2_factor//log2_max_radix
    for _ in range(max_radix_pass):
        radix.append(max_radix)
        log2_radix.append(log2_max_radix)
    pow2_pass = max_radix_pass
    log2_min_radix = log2_factor % log2_max_radix
    min_radix = 1 << log2_min_radix
    if log2_min_radix != 0:
        radix.append(min_radix)
        log2_radix.append(log2_min_radix)
        pow2_pass += 1

    radix_prod = [1 for i in radix]
    for i in range(len(radix)-2, -1, -1):
        radix_prod[i] = radix_prod[i+1] * radix[i+1]
    log_2_radix_prod = [int(np.log2(r)) for r in radix_prod]

    return (radix, log2_radix,
            radix_prod, log_2_radix_prod,
            non_pow2_pass, pow2_pass)
本文由作者按照 CC BY 4.0 进行授权