27 décembre 2022 — FFT, Python
Dans cet article, nous allons introduire la transformée de Fourier discrète, qui associe à une séquence de nombres complexes une autre séquence de complexes de même longueur .
L’algorithme de transformation de Fourier rapide (FFT) permet de calculer cette transformée en . En particulier, il peut être utilisé pour multiplier rapidement des polynômes, ou des entiers.
Le code source des fonctions présentées est disponible ici.
La transformation de Fourier discrète associe à une séquence de nombres complexes sa transformée .
Pour ce faire, on considère le polynôme associé à la séquence initiale et on évalue ce polynôme en points bien choisis pour obtenir la transformée.
Plus précisément, on choisit les racines -èmes de l’unité .
On définit ici comme une racine -ème primitive de l’unité, par exemple .
On a donc :
Cette relation s’écrit aussi matriciellement :
La transformation de Fourier inverse consiste à inverser cette matrice, je vous laisse vérifier que la matrice ci-dessous convient :
Par exemple, dans le cas la transformée de Fourier discrète de est constituée de la somme et de la différence. La transformée inverse s’obtient par demi-somme et demi-différence : .
Si et sont deux polynômes de degrés inférieurs à et , leur produit est un polynôme de degré inférieur à , dont au plus les premiers coefficients sont non nuls.
La transformée de Fourier discrète (de taille ) de s’obtient en évaluant les pour .
En particulier, on remarque que : la transformée de est égale à la multiplication point par point des transformées de et de .
Notons que toutes les transformées considérées ici sont de taille , une séquence de taille doit donc être complétée par zéros pour calculer sa transformée.
On suppose ici que est une puissance de : pour un certain .
On peut écrire
où et .
En particulier, on a pour :
Dans l’expression précédente, où est une racine -ème primitive de l’unité.
On a donc :
Cette expression fait intervenir l’évaluation des polynômes et sur racines -èmes de l’unité.
C’est précisément ce que calculent les transformées de taille de et . On en déduit donc un algorithme de calcul de la transformée qui réduit le problème en deux sous-problèmes de taille :
from math import e, pidef fft(L, sign=-1):n = len(L)if n == 1:return Lw_n = e**(sign * 2j * pi / n)w = 1F_even = fft(L[::2], sign)F_odd = fft(L[1::2], sign)F = [0] * nfor k in range(n // 2):F[k] = F_even[k] + w * F_odd[k]F[k + n // 2] = F_even[k] - w * F_odd[k]w = w * w_nreturn F
On désignera maintenant par FFT la transformée de Fourier discrète d’une séquence obtenue par cette méthode.
Note : l’algorithme implémenté ici est celui décrit dans le chapitre 30 de Introduction to Algorithms, que je vous recommande pour une présentation plus détaillée.
La fonction fft
effectue un nombre proportionnel à d’opérations et exécute deux appels récursifs à elle-même
sur des séquences de taille .
Le nombre d’opération effectuée est donc de l’ordre de
On a vu plus haut que la matrice d’inversion s’obtenait en considérant la matrice de la transformée, en remplaçant par son conjugué et en divisant par .
On en déduit la fonction inverse de la transformée ci-dessus :
def ifft(L):return [c / len(L) for c in fft(L, sign=1)]
En particulier, on en déduit un algorithme qui calcule le produit de deux polynômes en procédant ainsi :
Cet algorithme a une complexité en alors que la multiplication naïve (en appliquant la formule) amènerait à une multiplication en opérations.
En voici une implémentation en Python :
def multiply(P, Q):n = 1while n < len(P) + len(Q) - 1:n <<= 1P = P + [0] * (n - len(P))Q = Q + [0] * (n - len(Q))F = [a * b for a, b in zip(fft(P), fft(Q))]return ifft(F)
>>> multiply([1, 2, 3], [4, 5])[(4-6.661338147750939e-16j),(13+6.123233995736765e-17j),(22+6.661338147750939e-16j),(15-6.123233995736765e-17j)]
Notons que cet algorithme a le défaut de propager des erreurs d’approximation des flottants comme ci-dessus. On peut nettoyer les coefficients obtenus à l’aide de Numpy :
>>> import numpy as np>>> P = multiply([1, 2, 3], [4, 5])>>> np.trim_zeros(np.round(np.real(P)).astype(np.int32), "b")array([ 4, 13, 22, 15], dtype=int32)
Une variante de l’algorithme décrit précédemment est utilisée dans un algorithme de multiplication d’entiers en , l’algorithme de Schönhage-Strassen.
Nous avons vu précédemment que le calcul dans de la transformée introduit inévitablement des erreurs d’approximations des flottants. Ceci n’est pas acceptable pour un algorithme de multiplication d’entiers qui doit obtenir un résultat exact.
La transformée que nous avons décrite repose sur des opérations d’additions et multiplications et sur l’existence de racines primitives -èmes de l’unité.
Les résultats que nous avons obtenus, notamment la formule d’inversion et l’algorithme de calcul du produit de polynômes restent vrais en utilisant des séquences à valeurs dans d’autres anneaux que .
Il est notamment possible de considérer des séquences dans des anneaux finis, où les calculs peuvent être effectués de façon exacte par un ordinateur.
C’est ce type d’anneau qui est utilisé dans l’algorithme de Schönhage-Strassen (voir ici).
Notons notamment l’utilisation de cet algorithme dans V8 (le moteur Javascript utilisé notamment par Google Chrome et Node.js) pour multiplier des entiers de plus de chiffres (voir le code source).
Maths et applications, avec les mains et avec du code 💻
Suivre sur Twitter