接續上篇文章快速傅立葉轉換-上,我們介紹了離散傅立業轉換的推導還有性質,這篇文章中我們要介紹重頭戲,Cooley–Tukey FFT 演算法。
🔸Cooley–Tukey FFT 演算法推導
現在我們DFT代表離散傅立葉算子,且假設
離散訊號
觀察偶數
觀察偶數的偶數
觀察以上圖案就像一隻蝴蝶,這就是蝴蝶圖(Butterfly diagram)的由來
🔸FFT演算法說明
在使用蝴蝶圖(Butterfly
Diagram)來計算離散傅立葉轉換(DFT)時,我們首先需要對輸入序列
關於如何將一般數列轉換為 Bit-Reversal
排列,我們在此先不展開說明。現在假設序列長度為
令 Bit-Reversal 排列後的初始序列
① 第
層( )
這是最簡單的蝴蝶圖,每組僅有 2 個數值。此時旋轉因子為:
②第
層( )
這一層每組蝴蝶包含 4 個元素,使用旋轉因子:
第一組( ):
第二組( ):
③第
層( )
這是最終一層,將整個
完成第
🔸FFT演算法實作
用Python實作以長度是2的冪的FFT演算法
import numpy as np
def fft_butterfly(x):
# 假設輸入 x 已經先做過 BIT Reverse 排列
= len(x)
N = int(np.log2(N))
Layer = np.array(x, dtype=complex)
x for layer in range(1, Layer + 1):
= 2**layer # 每次蝴蝶圖翅膀展開的長度
m # 如果用 N=8 的範例就是從 m=2 慢慢展開到 m=4,組合出所求 y[k]
= m // 2
half_m = np.exp(-2j * np.pi / m)
omega_m for k in range(0, N, m):
# layer=1 時,取 k=0, 2, 4, 6,也就是每個最小蝴蝶 (展開長 2) 的左邊 index
# layer=2 時,取 k=0, 4,也就是每個中蝴蝶 (展開長 4) 的左邊 index
for j in range(half_m):
= omega_m**j * x[k + j + half_m]
t = x[k + j]
u + j] = u + t
x[k + j + half_m] = u - t
x[k return x
我們來驗證當演算法的正確性
from numpy.fft import fft
import numpy as np
from numpy import linalg as LA
= [complex(i, 0) for i in range(8)]
x = [0, 4, 2, 6, 1, 5, 3, 7]
bit_reverse = [x[i] for i in bit_reverse]
x_bit_reverse = fft_butterfly(x_bit_reverse)
y print(y)
= fft(x)
y2print(y2)
print(LA.norm(y - y2))
執行結果如下:
[28.+0.j -4.+9.65685425j -4.+4.j -4.+1.65685425j
-4.+0.j -4.-1.65685425j -4.-4.j -4.-9.65685425j]
[28.+0.j -4.+9.65685425j -4.+4.j -4.+1.65685425j
-4.+0.j -4.-1.65685425j -4.-4.j -4.-9.65685425j]
2.5121479338940403e-15
結果顯示我們的自己做的FFT 與 numpy 內建的FFT結果一致
Bit-Reversal Permutation
現在,我們可以來探討這種特殊排列是如何產生的。許多網路上文章會直接告訴你:「只要將數字轉成二進位,再把位元順序整個反轉,這樣就得到Bit-Reversal排列了」,接著配個範例說明。雖然這樣的說法快速直觀,很難真正理解為什麼要這麼做。
事實上,這個排列並不是憑空出現,而是伴隨著蝴蝶圖中「偶數在上、奇數在下」的分組方式產生的。在蝴蝶圖演算法中,每一層的運算會將資料分成偶數索引與奇數索引兩組來運算。分組方式隨著層數遞迴深入,最終會將資料重新排序成一個特殊的順序,而這個順序就是所謂的 Bit-Reversal 排列。
📌我們用Top Down 的方式來觀察蝴蝶圖 N = 8 計算的方式
① 第
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 0 | 000 |
偶數 | 2 | 010 |
偶數 | 4 | 100 |
偶數 | 6 | 110 |
奇數 | 1 | 001 |
奇數 | 3 | 011 |
奇數 | 5 | 101 |
奇數 | 7 | 111 |
從上述表格發現,在
②第
將
第一組(
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 0 | 000 |
偶數 | 4 | 100 |
奇數 | 2 | 010 |
奇數 | 6 | 110 |
第二組(
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 1 | 001 |
偶數 | 5 | 101 |
奇數 | 3 | 011 |
奇數 | 7 | 111 |
從上述表格發現,在
③ 第
同理,進一步把
第一組(
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 0 | 000 |
奇數 | 4 | 100 |
第二組(
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 2 | 010 |
奇數 | 6 | 110 |
第三組(
索引類別 | 十進位 | 二進位 |
---|---|---|
偶數 | 1 | 001 |
奇數 | 5 | 101 |
仔細觀察就會發現,每往下一層遞迴,其實就是依據二進位表示中「右邊數來第
- 在
時,看 右邊數來第 1 個 bit - 在
時,看 右邊數來第 2 個 bit - 在
時,看 右邊數來第 3 個 bit
以上觀察可以整理如下圖:
蝴蝶圖的奧祕就在於:每次蝴蝶圖運算進行「偶數在上、奇數在下」分組時,參照的就是該數字二進位表示的「右邊數來第
我們來看上圖中的例子:
- Bit-Reversal 排列中的第 0
個元素,由「偶偶偶」給出,對應二進位
,也就是十進位的 - 第 4
個元素,由「奇偶偶」給出,對應二進位
,也就是十進位的 - 第 6
個元素,由「奇奇偶」給出,對應二進位
,也就是十進位的 - 依此類推
📌 綜合以上說明,我們可以總結: 當要找到Bit-Reversal
排列中的第
而這個特殊排列,其實正是蝴蝶圖層層分組結構自然產生的副產品,而非刻意安排的編號方式
用Python實作以長度是2的冪的Bit-Reversal 排列
import math
def bit_reverse_permutation(arr):
"""
對輸入陣列 arr 做 bit-reversal permutation(位元反轉排列)
"""
= len(arr)
n = []
result
for i in range(n):
# 計算 i 的 bit-reverse 索引
= int('{:0{width}b}'.format(i, width=N)[::-1], 2)
rev
result.append(arr[rev])
return result
再次驗證FFT的實作
from numpy.fft import fft
import numpy as np
from numpy import linalg as LA
= [complex(i, 0) for i in range(16)]
x = bit_reverse_permutation(x)
x_bit_reverse = fft_butterfly(x_bit_reverse)
y print(y)
= fft(x)
y2print(y2)
print(LA.norm(y - y2))
執行結果如下:
[120. +0.j -8.+40.21871594j -8.+19.3137085j -8.+11.9728461j
-8. +8.j -8. +5.3454291j -8. +3.3137085j -8. +1.59129894j
-8. +0.j -8. -1.59129894j -8. -3.3137085j -8. -5.3454291j
-8. -8.j -8.-11.9728461j -8.-19.3137085j -8.-40.21871594j]
[120. +0.j -8.+40.21871594j -8.+19.3137085j -8.+11.9728461j
-8. +8.j -8. +5.3454291j -8. +3.3137085j -8. +1.59129894j
-8. +0.j -8. -1.59129894j -8. -3.3137085j -8. -5.3454291j
-8. -8.j -8.-11.9728461j -8.-19.3137085j -8.-40.21871594j]
1.6687357476335705e-14
🔸時間複雜度分析
📌我們先討論直接計算DFT的時間複雜度
回想DFT計算公式
import numpy as np
def DFT_slow(x):
"""
x : 輸入訊號 (list 或 numpy array),長度 N
return : DFT 結果 (numpy array),長度 N
"""
= np.asarray(x, dtype=complex) # 確保輸入是複數型別
x = x.shape[0]
N = np.zeros(N, dtype=complex) # 輸出 DFT 結果
X
for k in range(N): # 頻率索引
= 0
X[k] for n in range(N): # 時間索引
+= x[n] * np.exp(-2j * np.pi * k * n / N)
X[k] return X
接著我們分析這個演算法,假設每行程式碼的執行時間是 t
透過上圖的討論,直接計算DFT的演算法複雜度為
當
📌FFT的時間複雜度
回想FFT的分解式
假設
首先,我們需要遞迴計算兩個長度
的 DFT,這部分花費時間為接著,當兩個子問題計算完成後,必須將它們合併成最終的長度
的結果。 在這裡,每一對偶數、奇數頻率分量都要經過一次「乘法」與「加減法」,如下圖 1:總共會有
個輸出( 的項數)需要處理,因此合併過程的運算次數是 正比於 的,而且這個 就是tightest bound。
所以,綜合上述兩部分討論,的遞迴關係式可以寫作
,- 「合併」計算量=
與 相同 大師套公式法 Case 2
所以:
用以下程式碼比較一下在
from numpy.fft import fft
import numpy as np
from numpy import linalg as LA
import math
import time
= 2**12
N = [complex(i, 0) for i in range(N)]
x
= fft(x)
ans = time.time()
start_fft = bit_reverse_permutation(x)
x_bit_reverse = fft_butterfly(x_bit_reverse)
y1 = time.time()
end_fft
= time.time()
start_DFT_slow = DFT_slow(x)
y2 = time.time()
end_DFT_slow
print(f"FFT butterfly time: {end_fft - start_fft:.6f} seconds")
print(f"DFT slow time: {end_DFT_slow - start_DFT_slow:.6f} seconds")
print(f"FFT butterfly Norm difference: {LA.norm(ans - y1)}")
print(f"DFT slow Norm difference: {LA.norm(ans - y2)}")
執行結果如下:
FFT butterfly time: 0.012702 seconds
DFT slow time: 10.643429 seconds
FFT butterfly Norm difference: 1.596628055678928e-07
DFT slow Norm difference: 8.190390264959864e-06
看的出來透過FFT演算法來加速,可以提升計算DFT的效率到1000倍以上
🔸FFT的應用:快速離散卷積運算
在上一篇 快速傅立葉轉換-上 中,我們介紹了 離散卷積定理。根據該定理,若要在時域中計算離散卷積,可以改為:先將兩個序列轉換到頻域,在頻域中進行逐點乘積,最後再透過 iDFT 將結果轉換回時域。
直接在時域計算兩個長度為
FFT 轉換:將兩個序列分別做 FFT,計算量為
頻域逐點相乘:僅需
iDFT 還原:透過與 FFT 相同的快速演算法完成,計算量為
事實上,iDFT 也能透過 FFT 演算法加速,其原理與 FFT 推導幾乎相同,因此這裡筆者不再贅述,僅點出最終結論。
綜合上述三個步驟,總時間複雜度為
📌在實作加速離散卷積之前,我們需要先基於蝴蝶圖(Butterfly diagram)來實作iFFT,先前我們已經有FFT的實作,可以利用DFT與iDFT中的共軛性質來直接實作iFFT
回想DFT與iDFT的公式
得到 fft_butterfly(x)
,加上共軛的操作來實作iFFT
from numpy.fft import fft
from numpy.fft import ifft
import numpy as np
from numpy import linalg as LA
import random
def ifft_butterfly(x):
"""
使用 fft_butterfly 實作 IFFT。
"""
= len(x)
N # 共軛
= np.conjugate(x)
x_conj # FFT butterfly
= fft_butterfly(x_conj)
y # 取共軛後乘上1/N
return np.conjugate(y) / N
print("測試 IFFT 蝴蝶算法的正確性:")
print("="*50)
# 生成測試訊號
= 8 # 使用 2^3 長度做測試
test_length = [complex(random.randint(1, 10), random.randint(1, 10)) for _ in range(test_length)]
test_signal
# 使用 numpy 的 FFT 產生頻域數據
= fft(test_signal)
freq_data print(f"Numpy FFT 產生的頻域數據: {freq_data}")
# 使用自製的 IFFT 蝴蝶算法
= bit_reverse_permutation(freq_data)
freq_data_br = ifft_butterfly(freq_data_br)
butterfly_result
# 使用 numpy 的 IFFT 作為基準
= ifft(freq_data)
numpy_result
# 計算誤差
= LA.norm(np.array(butterfly_result) - numpy_result)
error
print(f"\n蝴蝶算法 IFFT 結果: {butterfly_result}")
print(f"Numpy IFFT 結果: {numpy_result}")
print(f"\n誤差分析:")
print(f"蝴蝶算法與 Numpy IFFT 的差異 (Norm): {error}")
print("="*50 + "\n")
執行結果如下:
==================================================
Numpy FFT 產生的頻域數據: [ 43. +45.j -13.94974747 -4.70710678j
-10. -4.j 9.70710678-10.46446609j
-9. -1.j -4.05025253 -3.29289322j
-16. +4.j 8.29289322-17.53553391j]
蝴蝶算法 IFFT 結果: [ 1. +1.j 6. +5.j 5. +1.j 6. +5.j 1.+10.j 9. +8.j 10.+10.j 5. +5.j]
Numpy IFFT 結果: [ 1. +1.j 6. +5.j 5. +1.j 6. +5.j 1.+10.j 9. +8.j 10.+10.j 5. +5.j]
誤差分析:
蝴蝶算法與 Numpy IFFT 的差異 (Norm): 1.2560739669470201e-15
==================================================
📌有幾個程式實作上有個細節需要特別注意:
- 由於我們實作的 FFT 僅支援長度為
的陣列,因此在計算 convolution 之前,必須先將兩個輸入序列zero padding至大於等於 的最小 的冪次長度。 - 因為我們有將兩個陣列都進行zero
padding,也因為我們僅有實作
長度的FFT 與 iFFT,所以再比較結果的時候,我們需要截斷後面多餘的
接著我們用以下程式,來比較一下FFT加速離散卷積運算的效果
from numpy.fft import fft
from numpy.fft import ifft
import numpy as np
from numpy import linalg as LA
import math
import time
import random
def discrete_convolution(f, g):
"""
計算兩個一維向量 f 和 g 的離散卷積。
回傳長度為 len(f) + len(g) - 1 的 numpy array。
"""
= np.asarray(f)
f = np.asarray(g)
g = len(f)
N = len(g)
M = np.zeros(N + M - 1, dtype=complex)
result for n in range(N + M - 1):
for k in range(N):
if 0 <= n - k < M:
+= f[k] * g[n - k]
result[n] return result
# Parameters
= 12 # N > 2 so 2**N > 2**2
N = 2**N
signal_length print(f"Signal length: {signal_length}")
# Generate random input signals
= [complex(random.randint(1, 100), 0) for _ in range(signal_length)]
f = [complex(random.randint(1, 100), 0) for _ in range(signal_length)]
g
# Zero-padding to next power of two (for convolution)
= 2 * signal_length
pad_length = f + [0] * (pad_length - len(f))
f_padded = g + [0] * (pad_length - len(g))
g_padded print(f"Zero-padded length: {pad_length}")
# FFT-based convolution
= time.time()
start_fft # Bit-reverse permutation for custom FFT
= bit_reverse_permutation(f_padded)
f_padded_br = bit_reverse_permutation(g_padded)
g_padded_br # FFT butterfly
= fft_butterfly(f_padded_br)
F = fft_butterfly(g_padded_br)
G = [F[i] * G[i] for i in range(len(F))]
FG # Bit-reverse permutation for inverse FFT
= bit_reverse_permutation(FG)
FG_br = ifft_butterfly(FG_br)
conv_fft = time.time()
end_fft
# Direct convolution (slow)
= time.time()
start_conv = discrete_convolution(f, g)
conv_slow = time.time()
end_conv
# Results and comparison
print(f"FFT butterfly convolution time: {end_fft - start_fft:.6f} seconds")
print(f"Direct convolution time: {end_conv - start_conv:.6f} seconds")
# Compare only the first len(conv_slow) elements
= LA.norm(conv_fft[:len(conv_slow)] - conv_slow)
norm_diff print(f"Norm difference between FFT and direct convolution: {norm_diff}")
執行結果如下:
Signal length: 4096
Zero-padded length: 8192
FFT butterfly convolution time: 0.072530 seconds
Direct convolution time: 1.808187 seconds
Norm difference between FFT and direct convolution: 1.0590499027122327e-05
透過FFT演算法來離散捲積,可以大幅提升計算離散捲積的速度,而且4096維的離散捲積,其結果僅
🔸FFT的應用:數字乘法
回想小時候計算直式的乘法的過程
為了更清楚地看出直式乘法與離散卷積的關係,我們可以用兩個陣列
根據離散卷積的公式:
✨在實作上還有另一個需要注意的地方:我們在紙筆計算直式乘法時,數字的書寫方式是由高位數到低位數(從左到右)。例如,數字 1234 代表的是
然而,若我們直接把它存成陣列 [1, 2, 3, 4]
,就會出現一個問題:我們沒辦法第一時間就確定最前面的1
是代表還是 ,因為陣列的索引是由小到大遞增的,並沒有天然對應到「位數大小」。 為了解決這個對齊問題,我們需要先將數字反轉,把最低位(個位數)對應到陣列索引
。以 1234 為例,我們會把它表示成 這樣一來,陣列的第 個元素對應 ,第 個元素對應 ,以此類推。如此一來,當我們進行卷積運算時,就能保證運算的索引順序與位數的意義一致,確保最終結果正確模擬直式乘法的加總過程。
以下是一個利用 FFT(快速傅立葉轉換) 來進行整數乘法的範例:
from numpy.fft import fft
from numpy.fft import ifft
import numpy as np
from numpy import linalg as LA
import math
import time
import random
def int_to_digits(n):
"""
將整數轉換為數字陣列(最低位在前)。
"""
return [int(d) for d in str(n)][::-1]
def digits_to_int(digits):
"""
將數字陣列(最低位在前)轉回整數。
"""
return int(''.join(str(d) for d in digits[::-1]))
def handle_carries(arr):
"""
處理數字陣列中的進位,產生正確的整數數字。
"""
= np.round(np.real(arr)).astype(int)
arr = 0
carry = []
result for v in arr:
= v + carry
total % 10)
result.append(total = total // 10
carry while carry > 0:
% 10)
result.append(carry //= 10
carry # Remove leading zeros
while len(result) > 1 and result[-1] == 0:
result.pop()return result
# 整數乘法範例
= 1234
a = 5678
b print(f"\n整數乘法範例: {a} * {b}")
= int_to_digits(a)
a_digits = int_to_digits(b)
b_digits = len(a_digits) + len(b_digits) - 1
n = int(np.ceil(np.log2(n)))
N = 2**N
pad_length # 補零到最近的 2 的次方
= a_digits + [0] * (pad_length - len(a_digits))
a_padded = b_digits + [0] * (pad_length - len(b_digits))
b_padded
# 使用 FFT 蝴蝶算法做卷積(乘法)並計算時間
= time.time()
start_fft_mul = bit_reverse_permutation(a_padded)
a_br = bit_reverse_permutation(b_padded)
b_br = fft_butterfly(a_br)
Fa = fft_butterfly(b_br)
Fb = [Fa[i] * Fb[i] for i in range(len(Fa))]
Fc = bit_reverse_permutation(Fc)
Fc_br = ifft_butterfly(Fc_br)
c_fft = handle_carries(c_fft)
c_digits_fft = digits_to_int(c_digits_fft)
product_fft = time.time()
end_fft_mul
# 基準:直接卷積並計算時間
= time.time()
start_slow_mul = discrete_convolution(a_digits, b_digits)
c_slow = handle_carries(c_slow)
c_digits_slow = digits_to_int(c_digits_slow)
product_slow = time.time()
end_slow_mul
print(f"FFT 蝴蝶算法結果: {product_fft}")
print(f"FFT 蝴蝶算法乘法時間: {end_fft_mul - start_fft_mul:.6f} 秒")
print(f"直接卷積結果: {product_slow}")
print(f"直接卷積乘法時間: {end_slow_mul - start_slow_mul:.6f} 秒")
print(f"正確答案: {a * b}")
print(f"FFT 與直接卷積的數字差異 Norm: {LA.norm(np.array(c_digits_fft)[:len(c_digits_slow)] - np.array(c_digits_slow))}")
執行結果如下:
整數乘法範例: 1234 * 5678
FFT 蝴蝶算法結果: 7006652
FFT 蝴蝶算法乘法時間: 0.000105 秒
直接卷積結果: 7006652
直接卷積乘法時間: 0.000020 秒
正確答案: 7006652
FFT 與直接卷積的數字差異 Norm: 0.0
從這個小範例可以發現,FFT 加速的效果反而不如直接的暴力卷積。由於數字規模較小,FFT 本身的額外運算開銷大於加速效果,因此運行時間反而更長。
為了更清楚觀察兩種方法的差異,我們隨機生成兩個長度為
# 隨機生成長度為 2**N 的兩個大整數相乘範例
= 11 # 可自行調整 N
N = 2**N
num_digits # 產生隨機 digit array(每位 0~9)
= [random.randint(0, 9) for _ in range(num_digits)]
rand_digits_a = [random.randint(0, 9) for _ in range(num_digits)]
rand_digits_b # 轉成整數
= digits_to_int(rand_digits_a)
rand_a = digits_to_int(rand_digits_b)
rand_b print(f"\n隨機大整數乘法範例: 長度 {num_digits} 位")
print(f"A = {rand_a}")
print(f"B = {rand_b}")
= time.time()
start_python_built_in_multiplication = rand_a * rand_b
res = time.time()
end_python_built_in_multiplication print(f"Python 內建乘法結果: {res}")
# digit array 準備
= int_to_digits(rand_a)
a_digits = int_to_digits(rand_b)
b_digits = len(a_digits) + len(b_digits) - 1
n = 2**int(np.ceil(np.log2(n)))
pad_length = a_digits + [0] * (pad_length - len(a_digits))
a_padded = b_digits + [0] * (pad_length - len(b_digits))
b_padded
# FFT 乘法
= time.time()
start_fft = bit_reverse_permutation(a_padded)
a_br = bit_reverse_permutation(b_padded)
b_br = fft_butterfly(a_br)
Fa = fft_butterfly(b_br)
Fb = [Fa[i] * Fb[i] for i in range(len(Fa))]
Fc = bit_reverse_permutation(Fc)
Fc_br = ifft_butterfly(Fc_br)
c_fft = handle_carries(c_fft)
c_digits_fft = digits_to_int(c_digits_fft)
product_fft = time.time()
end_fft
# 直接卷積
= time.time()
start_conv = discrete_convolution(a_digits, b_digits)
c_slow = handle_carries(c_slow)
c_digits_slow = digits_to_int(c_digits_slow)
product_slow = time.time()
end_conv
# 比較結果
print(f"\nFFT 蝴蝶算法結果: {product_fft}")
print(f"直接卷積結果: {product_slow}")
= min(len(c_slow), len(c_fft))
min_len print(f"FFT 蝴蝶算法與直接卷積Norm差異 : {LA.norm(np.array(c_slow[:min_len]) - np.array(c_fft[:min_len]))} (直接卷積)")
print(f"FFT 蝴蝶算法時間: {end_fft - start_fft:.6f} 秒")
print(f"直接卷積時間: {end_conv - start_conv:.6f} 秒")
print(f"Python 內建乘法時間: {end_python_built_in_multiplication - start_python_built_in_multiplication:.6f} 秒")
執行結果如下:
隨機大整數乘法範例: 長度 2048 位
A =
#省略
B =
#省略
Python 內建乘法結果:
#省略
FFT 蝴蝶算法結果:
#省略
直接卷積結果:
#省略
FFT 蝴蝶算法與直接卷積Norm差異 : 5.7675031363963895e-08 (直接卷積)
FFT 蝴蝶算法時間: 0.033830 秒
直接卷積時間: 0.909085 秒
Python 內建乘法時間: 0.000016 秒
在這個例子中可以明顯看到:
- FFT 蝴蝶算法 已經大幅快於直接卷積(速度差超過 20 倍)。
- 不過,因為 FFT
過程中涉及浮點數運算,會產生微小的精度誤差,導致最後需要額外處理進位與四捨五入,Norm
差異大約在
等級。這也是為什麼 FFT 並不是專為大數精確運算設計的演算法。 - 事實上,若要進行更精確的大數乘法,有其他更合適的轉換與演算法。
最後值得一提的是,Python 的便利性:
- 在 Python 中,當整數超過一般機器整數範圍時,語言本身會自動切換到「大數運算」(arbitrary-precision integer)。
- 使用者不需要額外處理,直接寫
a * b
就能正確計算任意大整數。 - 這與 Java 或 C++
不同,在那些語言裡,內建整數型別(例如
int
、long
)有固定大小,若要進行大數運算,需要額外引入BigInteger
(Java)或使用第三方大數庫(C++),開發體驗相對麻煩。