Уровень алгоритма

Участник:Ivanov.kir.m/Быстрое дискретное преобразование Фурье: различия между версиями

Материал из Алговики
Перейти к навигации Перейти к поиску
Строка 28: Строка 28:
Входные данные: вектор действительных чисел <math>x = (x_1,x_2,...,x_N)</math>.
Входные данные: вектор действительных чисел <math>x = (x_1,x_2,...,x_N)</math>.


Выходные данные: вектор комплексных чисел <math>X = (X_1,X_2,...,X_{\lfloor N/2 \rfloor+1})</math>.
Выходные данные: вектор комплексных чисел <math>X = (X_1,X_2,...,X_N)</math>.


Дискретное преобразование Фурье задается следующей формулой:
Дискретное преобразование Фурье задается следующей формулой:

Версия от 20:15, 12 октября 2016


Алгоритм одномерного преобразования Фурье для действительных чисел
Последовательный алгоритм
Последовательная сложность [math]\displaystyle{ O (N \log N) }[/math]
Объём входных данных [math]\displaystyle{ N }[/math] действительных чисел
Объём выходных данных [math]\displaystyle{ \lfloor N/2 \rfloor+1 }[/math] комплексных чисел
Параллельный алгоритм
Высота ярусно-параллельной формы [math]\displaystyle{ O (\log N) }[/math]
Ширина ярусно-параллельной формы [math]\displaystyle{ N }[/math]

Авторы описания К.М.Иванов, А.C.Сапин

Быстрое преобразование Фурье (БПФ, FFT) — алгоритм быстрого  вычисления дискретного преобразования Фурье (ДПФ). То есть, алгоритм вычисления за количество действий, меньшее чем [math]\displaystyle{ O(N^{2}) }[/math], требуемых для прямого (по формуле) вычисления ДПФ. Иногда под БПФ понимается один из быстрых алгоритмов, называемый алгоритмом прореживания по частоте/времени или алгоритмом по основанию 2, имеющий сложность [math]\displaystyle{ O(N\log(N)) }[/math]. Cуществует несколько различных алгоритмов для вычисления ДПФ считающимся быстрым преобразование Фурье:

  • Алгоритм Кули-Тьюки [1]
  • Алгоритм Гуда-Томаса [2]
  • Алгоритм Бруна [3]
  • Алгоритм Блюштейна [4]

ЧАСТЬ. Свойства и структура алгоритмов

Общее описание алгоритма

Быстрое дискретное преобразование Фурье (БПФ) - популярный в современном мире математический метод преобразования вектора/матрицы комплексных/действительных чисел (сигнала) в вектор/матрицу комплексных чисел, т.н. спектр. Область применения данного метода простирается от обработки аудио информации до собственно спектрального анализа. Поскольку в общем случае алгоритм БПФ применим к произвольным векторам чисел, выделяют отдельные подзадачи, для которых можно ввести дополнительную оптимизацию. Одной из таких подзадач является быстрое преобразование Фурье вектора действительных чисел, которому и посвящена данная статья. В качестве тестируемой реализации был выбран алгоритм Кули-Тьюки в библиотеке FFTW, написанная на языке C.


Замечание: Поскольку общие описание алгоритма БПФ весьма сложно в некоторых пунктах данная статья будет ссылаться на простой случай - алгоритм Кули-Тьюки быстрого преобразование Фурье для векторов с размерностью равной степени двойки. Отличительной особенностью данного алгоритма является то, что он обходится без использования специфических приемов, использующихся именно для степеней четверки, восьмерки и т.п. Однако благодаря тому, что на вход данному алгоритму подается вектор чисто вещественных чисел, выходной вектор удовлетворяет эрмитовой избыточности (Hermitian redundancy) , т.е. [math]\displaystyle{ out[i] }[/math] является сопряженным с [math]\displaystyle{ out[n-i] }[/math]. Это обстоятельство позволяет достичь роста скорости и снижения затрат памяти примерно в 2 раза по сравнению с комплексным аналогом алгоритма.

Математическое описание алгоритма

Входные данные: вектор действительных чисел [math]\displaystyle{ x = (x_1,x_2,...,x_N) }[/math].

Выходные данные: вектор комплексных чисел [math]\displaystyle{ X = (X_1,X_2,...,X_N) }[/math].

Дискретное преобразование Фурье задается следующей формулой:

[math]\displaystyle{ X_k = \sum_{n=0}^{N-1} x_n e^{-\frac{2\pi i}{N} nk}, k = \overline{0,N-1} }[/math]

В простейшем случае(для степеней двойки) алгоритм Кули-Тьюки рассчитывает ДПФ для четных [math]\displaystyle{ (x_{2m}=x_0, x_2, \ldots, x_{N-2}) }[/math] и нечетных элементов отдельно [math]\displaystyle{ (x_{2m+1}=x_1, x_3, \ldots, x_{N-1}) }[/math]. Затем результаты рассчетов объединяются для получения ДПФ всей последовательности:

[math]\displaystyle{ \begin{matrix} X_k & = & \sum \limits_{m=0}^{N/2-1} x_{2m}e^{-\frac{2\pi i}{N} (2m)k} + \sum \limits_{m=0}^{N/2-1} x_{2m+1} e^{-\frac{2\pi i}{N} (2m+1)k} \end{matrix} }[/math]

Таким образом, получается рекурсивный алгоритм, работающий по принципу разделяй и влавствуй.

Рекурсивное описание

Алгоритм:

  1. Входной вектор [math]\displaystyle{ a = (a_0,a_2,...,a_{N-1}) }[/math] преобразуется в матрицу [math]\displaystyle{ A }[/math] размера [math]\displaystyle{ n_1 \times n_2 }[/math], где [math]\displaystyle{ N=n_1 \cdot n_2 }[/math] и [math]\displaystyle{ n_1 \lt n_2 }[/math]

[math]\displaystyle{ A = \begin{pmatrix} a_0 & a_1 & \cdots & a_{n_1-1} \\ a_{n_1} & a_{n_1+1} & \cdots & a_{2n_1-1} \\ \vdots & \vdots & \ddots & \vdots \\ a_{(n_2-1)\cdot n_1} & a_{(n_2-1)\cdot n_1+1} & \cdots & a_{n_2\cdot n_1-1} \end{pmatrix} }[/math]

  1. К каждой строке полученной матрицы применяется быстрое дискретное преобразование Фурье (БПФ) порядка [math]\displaystyle{ n_1 }[/math]
  2. Каждый элемент полученный после применения БПФ умножается на поворотные множители [math]\displaystyle{ exp (2 \pi i(m-1)(j-1)/N) }[/math], где [math]\displaystyle{ m }[/math] - номер строки, а [math]\displaystyle{ j }[/math] - номер столбца
  3. Полученная после шагов 1-2 матрица [math]\displaystyle{ A }[/math] транспонируется
  4. К каждой строке матрицы [math]\displaystyle{ A^T }[/math] применяется БПФ порядка [math]\displaystyle{ n_2 }[/math]

Замечание: Как правило все поворотные множители вычисляются заранее и хранятся в специальном массиве.

Вычислительное ядро алгоритма

В случае размерности входа равной степени двойки, вычислительным ядром алгоритма является, так называемая, "бабочка". В простейшем случае "бабочка" представляет из себя двухточечное преобразование. Рассмотрим этот случай:

На вход алгоритму подается двухэлементный вектор ‒ [math]\displaystyle{ v = (v[0], v[1]) }[/math]. Тогда для вычисления будут происходить по следующим формулам:

[math]\displaystyle{ V[0] = W_2^0 v[0] + W_2^0 v[1] = v[0] + W_2^0 v[1] }[/math]

[math]\displaystyle{ V[1] = W_2^0 v[0] + W_2^1 v[1] = v[0] + W_2^1 v[1] }[/math]

Данный процесс удобно изобразить с помощью следующей схемы:

Рис. 1. "Бабочка" для двумерного входа

Для 4-х элеметного вектора [math]\displaystyle{ v=(v[0],v[1],v[2],v[3]) }[/math], алгоритм строится похожим образом. Сначала создаются простейшие "бабочки", а потом их результаты соединяются с противоположеной "бабочкой":

[math]\displaystyle{ V[0]=v[0]+W_2^0 v[2]+W_4^0(v[1]+W_2^0 v[3]) }[/math]

[math]\displaystyle{ V[1]=v[0]-W_2^0 v[2]+W_4^1(v[1]-W_2^0 v[3]) }[/math]

[math]\displaystyle{ V[2]=v[0]+W_2^0 v[2]-W_4^0(v[1]+W_2^0 v[3]) }[/math]

[math]\displaystyle{ V[3]=v[0]-W_2^0 v[2]-W_4^1(v[1]-W_2^0 v[3]) }[/math]

Схема в таком случае будет выглядеть следующим образом:

Рис. 2. "Бабочка" для четырёхмерного входа

Для случая, когда вход не является степенью двойки, "бабочки" будут "несимментричными", но в остальном вычисления будут проходить схожим образом.

Макроструктура алгоритма

Для исходного вектора [math]\displaystyle{ a = (a_1,a_2,...,a_N) }[/math] размерности [math]\displaystyle{ N = n_1 \cdot n_2 }[/math], [math]\displaystyle{ n_1 \lt n_2 }[/math] БПФ представляется как:

  1. [math]\displaystyle{ n_2 }[/math] БПФ порядка [math]\displaystyle{ n_1 }[/math]
  2. [math]\displaystyle{ n_1 \cdot n_2 }[/math] умножение комплексных чисел
  3. [math]\displaystyle{ n_1 }[/math] БПФ порядка [math]\displaystyle{ n_2 }[/math]

Схема реализации последовательного алгоритма

Схема организации состоит в том, что на каждом шаге [math]\displaystyle{ i }[/math] для выполнения "бабочки" все элементы разбиваются на [math]\displaystyle{ n_{i_1} }[/math] векторов по [math]\displaystyle{ n_{i_2} }[/math] элементов, причем [math]\displaystyle{ n_{i_1} \cdot n_{i_2} = n_i }[/math], где [math]\displaystyle{ n_i }[/math] длина входного вектора на текущем шаге [math]\displaystyle{ i }[/math]. В случае если такое разбиение невозможно, по причине того что [math]\displaystyle{ n_i }[/math] простое число, исходный вектор на текущем шаге дополняется элементом. В зависимости от номера шага, разница координат для каждой пары элементов увеличивается соразмерно разбиению [math]\displaystyle{ (n_{i_1},n_{i_2}) }[/math]. При этом результат суммы записывается в элемент с меньшим номером, а результат вычитания с последующим умножением - в элемент с большим.

Последовательная сложность алгоритма

Быстрое дискретное преобразование Фурье выполнимо за [math]\displaystyle{ O(N(n_1+\cdots+n_m)) }[/math] действий при [math]\displaystyle{ N=n_1n_2\cdots n_m }[/math] (в простом случае, при [math]\displaystyle{ N=2^m }[/math] необходимо [math]\displaystyle{ O(N\log_2(N)) }[/math] действий). Дискретное преобразование Фурье преобразует вектор [math]\displaystyle{ a = (a_0, \dots, a_{N-1}) }[/math] в вектор комплексных чисел [math]\displaystyle{ b = (b_0, \dots, b_{N-1}) }[/math], такой, что [math]\displaystyle{ b_i=\sum_{j=0}^{N-1}a_j\varepsilon^{ij} }[/math], где [math]\displaystyle{ \varepsilon^n=1 }[/math] и [math]\displaystyle{ \varepsilon^k\neq 1 }[/math] при [math]\displaystyle{ 0\lt k\lt N }[/math].

Основной шаг алгоритма состоит в сведении задачи для [math]\displaystyle{ N }[/math] чисел к задаче для [math]\displaystyle{ n_1=N/n_2 }[/math] числам, где [math]\displaystyle{ n_2 }[/math] — делитель [math]\displaystyle{ N }[/math].

Пусть мы уже умеем решать задачу для [math]\displaystyle{ N/n_2 }[/math] чисел. Применим преобразование Фурье к векторам [math]\displaystyle{ a_i,a_{n_2+i}, \dots, a_{n_2(n_1-1)+i} }[/math] для [math]\displaystyle{ i=0,1,\dots,n_2-1 }[/math]. Покажем теперь, что за [math]\displaystyle{ O(Np) }[/math] действий можно решить исходную задачу. Заметим, что [math]\displaystyle{ b_i=\sum_{j=0}^{n_2-1} \varepsilon^{ij} \left(\sum_{k=0}^{n_1-1}a_{kn_2+j}\varepsilon^{kin_2}\right) }[/math]. Выражения в скобках нам уже известны — это [math]\displaystyle{ (i\mod p) }[/math]-тое число после преобразования Фурье [math]\displaystyle{ j }[/math]-го вектора. Таким образом, для вычисления каждого [math]\displaystyle{ b_i }[/math] нужно [math]\displaystyle{ O(n_2) }[/math] действий, а для вычисления всех [math]\displaystyle{ b_i }[/math] всего [math]\displaystyle{ O(Nn_2) }[/math] действий, что и требовалось.

В общем случае :

Пусть [math]\displaystyle{ 4N\gt 2^k\ge2N }[/math]. Заметим, что тогда [math]\displaystyle{ b_i=\varepsilon^{-i^2/2}\sum_{j=0}^{N-1}\varepsilon^{(i+j)^2/2}\varepsilon^{-j^2/2}a_j }[/math]. Обозначим [math]\displaystyle{ \bar{a}_i=\varepsilon^{-i^2/2}a_i }[/math], [math]\displaystyle{ \bar{b}_i=\varepsilon^{i^2/2}b_i }[/math], [math]\displaystyle{ c_i=\varepsilon^{(2N-2-i)^2/2} }[/math]. Тогда [math]\displaystyle{ \bar{b}_i=\sum_{j=0}^{2N-2-i}\bar{a}_jc_{2N-2-i-j} }[/math], если положить [math]\displaystyle{ \bar{a}_i=0 }[/math] при [math]\displaystyle{ i\ge N }[/math].

Таким образом задача сведена к вычислению свёртки, а это можно сделать с помощью трёх преобразований Фурье для [math]\displaystyle{ 2^k }[/math] элементов. Выполняем прямое преобразование Фурье для [math]\displaystyle{ (\bar{a_0}, \cdots, \bar{a}_{2^k-1}) }[/math] и [math]\displaystyle{ (c_1,\cdots,c_{2^k-1}) }[/math], перемножаем поэлементно результаты и выполняем обратное преобразование Фурье.

Вычисления всех [math]\displaystyle{ \bar{a}_i }[/math] и [math]\displaystyle{ c_i }[/math] требуют [math]\displaystyle{ O(N) }[/math] действий, три преобразования Фурье требуют [math]\displaystyle{ O(N\log(N)) }[/math] действий, перемножение результатов преобразований Фурье требует [math]\displaystyle{ O(N) }[/math] действий, вычисление всех [math]\displaystyle{ b_i }[/math] зная значения свертки требует [math]\displaystyle{ O(N) }[/math] действий. Итого для дискретного преобразования Фурье требуется [math]\displaystyle{ O(N\log(N)) }[/math] действий для любого [math]\displaystyle{ N }[/math].

Информационный граф

Информационный граф алгоритма Кули-Тьюки для [math]\displaystyle{ N = 21 }[/math]. Исходные данные обозначены зеленым, результат — синим. Преобразования Фурье для [math]\displaystyle{ n_1 = 7 }[/math] и [math]\displaystyle{ n_2 = 3 \text{ } (N = n_1 \cdot n_2) }[/math] представлены как «чёрные ящики». Умножение на поворотные коэффициенты (множители) представлено коричневыми ромбами.

Как видно из рисунка, размер этого графа линейно-логарифмический, а формулы дуг имеют экспоненциальные компоненты.

Ресурс параллелизма алгоритма

Если считать только главные члены выражений, то простой алгоритм Кули-Тьюки имеет критический путь, состоящий из [math]\displaystyle{ log(N) }[/math] операций комплексного сложения/вычитания и [math]\displaystyle{ log(N) }[/math] операций комплексного умножения (основание [math]\displaystyle{ log }[/math] целиком зависит от выбранного на каждом шаге алгоритма разбиения). Таким образом, простой алгоритм БПФ в общем случае может быть отнесён к логарифмическому классу по параллельной сложности.

Входные и выходные данные алгоритма

Входные данные: вектор действительных чисел [math]\displaystyle{ a = (a_1,a_2,...,a_N) }[/math].

Выходные данные: вектор комплексных чисел [math]\displaystyle{ b = (b_1,b_2,...,b_{\lfloor N/2 \rfloor+1}) }[/math].

Свойства алгоритма

Таким образом в общем у алгоритма БПФ в случае неограниченных ресурсов:

  • последовательная сложность - линейно-логарифмическая
  • параллельная сложность - логарифмическая.

При этом алгоритм полностью детерминирован.

ЧАСТЬ. Программная реализация алгоритма

В качестве тестируемой реализации была выбрана библиотека FFTW, а именно функция, выполняющая БПФ одномерного вектора действительных чисел.

Особенности последовательной реализации алгоритма

Простейший вариант алгоритма для степеней двойки на языке C:

#include <stdio.h>
#include <math.h>
#include <complex.h>

typedef double complex cplx;
 
void _fft(cplx *buf, cplx *out, int n, int step)
{
    if (step < n) {
	_fft(out, buf, n, step * 2); //рекурсивный рассчет
	_fft(out + step, buf + step, n, step * 2);
 
	for (int i = 0; i < n; i += 2 * step) {
	    cplx t = cexp(-I * PI * i / n) * out[i + step];
	    buf[i / 2]     = out[i] + t;
	    buf[(i + n)/2] = out[i] - t;
        }
    }
}
 
void fft(cplx *buf, int n)
{
    cplx out[n];
    for (int i = 0; i < n; i++) out[i] = buf[i];
 
    _fft(buf, out, n, 1);
}

Масштабируемость алгоритма и его реализации

Масштабируемость алгоритма проверялась с помощью тестовой программы расположенной по [ ссылке]. Проводились замеры времени работы алгоритма, а также эффективности.

Существующие реализации алгоритма

Наиболее известной библиотекой для выполнения различных вариантов быстрого преобразования Фурье является FFTW, разработанная Матео Фриго и Стивеном Джонсоном в Массачусетском технологическом институте. Для преобразования небольших объемов данных используется алгоритм Кули - Тьюки, в случае входа больших размеров используется алгоритм Радера или Блюштейна. Библиотека считается самой быстрой реализацией БПФ и на входах любых размеров и размерностей сохраняет ассимптотическую сложность [math]\displaystyle{ O(N\cdot log(N)) }[/math]. Также, существует версия FFTW с поддержкой MPI, которая позволяет с объемами данных не умещающимися в память.

FFTW изначально написана на языке C, однако имеет API для нескольких языков, в том числе Fortran и Ada. Открытая версия библиотеки распространяется под лицензией GNU General Public License. Также существует реализация для коммерческого использования распространяемая под лицензией MIT.

Существует несколько реализаций алгоритма, для рассчета на GPU:

  • cuFFT для рассчета на графических ускорителях от NVIDIA.
  • clFFT для рассчета на GPU и CPU.

Литература

[1] Википедия [Электронный ресурс]. Тема: Быстрое преобразование Фурье – Электрон. дан. – URL Быстрое преобразование Фурье (дата обращения 17.09.2016)