Параллельный алгоритм поиска собственных чисел для

advertisement
Параллельный алгоритм поиска собственных чисел для симметричной
тридиагональной матрицы
Введение
Во многих практических задачах, таких как имитационное моделирование,
генетические алгоритмы обучения и т. д. часто требуется получить набор случайных
векторов с нормальным распределением, заданным ковариационной матрицей. Чтобы
сгенерировать случайный вектор, требуется найти собственные числа и собственные
векторы матрицы ковариации.
Обобщенная задача поиска собственных чисел и векторов для матрицы может быть
решена с помощью SVD (singular value decomposition) разложения. Однако
вычислительная сложность SVD - O(n3), что может быть серьезной проблемой при
решении больших задач.
Специализированные алгоритмы поиска собственных чисел и векторов имеют
сложность O(n2). Эти алгоритмы принимают на вход симметричную тридиагональную
матрицу. Тридиагональная матрица – матрица в которой заданны главная диагональ и по
одной диагонали выше и ниже главной, остальные элементы равны нулю. Симметричная
трилиагональная матрица размера nxn может быть описана двумя векторами: вектором
главной диагонали – d, длинной n, и вектором диагонали под/над главной – e, длинной
n-1.
Алгоритм
Пусть A – симметричная тридиагональная матрица размера n×n c диагональю d0,
…,dn-1 и наддиагональю e0,…,en-2, а z0,…,zm-1 – собственные числа матрицы. Известно, что
есть функция count(x), определенная ниже, которая возвращает количество собственных
чисел, меньше x. Текст функции приведен в алгоритме 1.
count = 0;
diff = d0 – x;
for i = 1 to n – 1
2
⁄𝑑𝑖𝑓𝑓 ;
diff = di-x-𝑒𝑖−1
if diff < 0 then count = count + 1;
end for
Алгоритм 1 - функция count(x), которая возвращает количество собственных чисел,
меньше x.
Таким образом, количество собственных чисел в интервале [x1; x2) можно легко
вычислить: count(x2) – count(x1). Это свойство позволяет использовать любой алгоритм
поиска, например метод дихотомии, для поиска собственных чисел на заданном отрезке.
Такой алгоритм поиска будет полагаться на тот факт, что функция count(x) монотонна. Но
реализация count(x) для конкретной машины с арифметикой с плавающей точкой
floatCount(x) не обязательно будет монотонной [1]. Так можно будет найти такие x1 < x2,
что floatCount(x1) > floatCount(x2), что, очевидно, неверно. Ниже будут приведены
приемы, позволяющие избежать получения денормализованных чисел, при операциях с
плавающей точкой.
Если требуется найти все собственные числа матрицы, то начальный отрезок для
поиска [x1; x2) можно рассчитать по теореме Гершгорина. Пусть Ri – сумма абсолютных
значений недиагональных элементов i строки матрицы A, то D(aii, Ri) будет диском
Гершгорина с центром aii и радиусом Ri. Теорема Гершгорина гласит, что каждое
собственное число матрицы A находится внутри хотя бы одного из дисков Гершгорина.
Доказательство может быть найдено в [2]. Для вычисления x1 и x2 можно использовать
алгоритм 2.
x1 = d0;
x2 = d0;
prev = 0;
for j = 0 to n - 2
x1 = min(x1, dj - prev - |ej|);
x2 = max(x2, dj + prev + |ej|);
prev = |ej|;
endfor
x1 = min(x1, dn-1 - prev);
x2 = max(x2, dn-1 + prev);
Алгоритм 2 – Вычисление интервала Гершгорина [x1; x2), содержащего все
собственные числа матрицы
Ниже приведен псевдокод алгоритма поиска m собственных чисел. Алгоритм 3
оптимизирован для вычислений на GPU.
Алгоритм 3 использует служебную функцию getNumEigensIn для вычисления
количества собственных чисел, находящихся внутри интервала [low, up). Из-за
особенностей арифметики с плавающей точкой, можно найти такие low и up, что
floatCount(up) - floatCount(low) будет меньше 0, что некорректно в данном контексте.
Чтобы избежать такой ситуации, добавлено дополнительное действие max(0, count).
Процедура refineIntervals выполняет одну итерацию деления интервалов.
Предполагается, что refineIntervals выполняется параллельно на m потоках, где m –
количество собственных чисел, которые требуется найти. Общее количество собственных
чисел для тридиагональной матрицы можно найти, если найти разность count(gu) –
count(gl), где gu и gl – границы интервала Гершгорина (алгоритм 2).
На вход функция refineIntervals получает параметра: inIL0..m-1, inIU0..m-1, outIL0..m-1,
outIU0..m-1.Где inILi (input interval lower) – нижняя граница i входного интервала, inIUi
(input interval upper) – верхняя граница i входного интервала, outILi (output interval lower) –
нижняя граница i выходного интервала, outIUi (output interval upper) – верхняя граница i
выходного интервала.
Предполагается, что getThreadNumber() – системная функция, возвращающая
порядковый номер вычислительного потока.
Первым шагом вычислительный поток отыскивает индекс i интервала,
содержащего id-ое собственное число. Стоит отметить, что цикл «перешагивает» через
интервалы, не содержащие собственных чисел.
Вторым шагом refineIntervals выполняет деление интервала. Функция isNarrow
возвращает истину, если интервал достаточно мал и не подлежит дальнейшему делению.
Ниже будет обсуждаться численная стабильность реализации isNarrow.
Если интервал содержит одно собственное число, то он делится пополам.
Вычисляется количество собственных чисел лежащих в левом подинтервале
[inILi, middle). Если оно больше 0, то берется нижняя половина, иначе – верхняя.
Если интервал содержит более одного собственного числа, то он делится на n
равных частей. Причем, каждый вычислительный поток берет только splitN-ую часть из n
частей. Для улучшения численной устойчивости реализации алгоритма, важно выполнять
именно такие действия при разбиении интервала.
function getNumEigensIn(low, up)
count = floatCount(up) - floatCount(low);
getNumEigensIn = max(0, count);
end function
procedure refineIntervals(inIL, inIU, outIL, outIU)
id = getThreadNumber();
// шаг 1: найти индекс входного интервала
i = 0;
splitN = id;
while splitN >= getNumEigensIn(inILi, inIUi)
splitN = splitN - getNumEigensIn(inILi, inIUi);
i = i + 1;
end while
// 2) обработать входной интервал
if isNarrow(inILi, inIUi) then
// если интервал достаточно мал,
// оставить его без изменений
outILid = inILi;
outIUid = inIUi;
else
n = getNumEigensIn(inILi, inIUi);
// разделить интервал
if n = 1 then
// деление интервала пополам
middle = 0.5 * (inILi + inIUi);
if getNumEigensIn(inILi, middle) > 0 then
// взять нижнюю половину интервала
outILid = inILi;
outIUid = middle;
else
// взять верхнюю половину интервала
outILid = middle;
outIUid = inIUi;
end if
else
// поледить интервал на n частей и взять splitN часть
width = abs(inIUi - inILi) / n;
outILid = inILi + splitN * width;
outIUid = outILid + width;
end if
end if
end procedure
Алгоритм 3 – Параллельный алгоритм разделения интервалов
Если проанализировать шаг 1 алгоритма 3, то станет ясно, что на первой итерации
refineIntervals достаточно чтобы был известен один только начальный интервал, при
условии, что он содержит все искомые собственные числа. При поиске всех собственных
чисел достаточно инициализировать inIL0 и inIU0 верхней и нижней границами интервала
Гершгорина.
Полностью алгоритм поиска всех собственных чисел поиска всех собственных
чисел приведен в алгоритме 4.
(inIL0, inIU0) = getGersgorinInterval();
for i = 1 to maxIterations
parallel for j = 1 to m
refineIntervals(inIL, inIU, outIL, outIU);
end parallel for
if isAllNarrow(outIL, outIU) then break;
swap(inIL, outIL);
swap(inIU, outIU);
end for
Алгоритм 4 – Алгоритм итеративного приближения интервалов к собственным
числам матрицы.
Каждая итерация цикла parallel for выполняется отдельным вычислительным
потоком. Функция isAllNarrow возвращает истину, если isNarrow истина для всех inIUi,
outIUi, i=0..m-1. Если на каждом шаге ширина интервала уменьшается в 2 раза, то
максимальное число итераций maxIterations, за которое любой интервал станет достаточно
малым будет равно log 2
𝑤0
𝑤𝑚𝑖𝑛
. Где w0 – начальная ширина интервала (inIU0 - inIL0 в
алгоритме 4), а wmin – минимальная ширина интервала.
Численная устойчивость
Реализация floatCount отличается от алгоритма count (алгоритм 1), тем, что может
терять точность и/или выдавать неверные результаты из-за погрешностей или особых
случаев арифметики с плавающей точкой. Все это может привести к тому, что floatCount
может не быть монотонной. Проблемное место – вычисление следующего значения diff.
diff = d0 – x;
2
diff = di-x-𝑒𝑖−1
⁄ 𝑑𝑖𝑓𝑓
При малых значениях diif может возникнуть переполнение при делении, которое
приведет к тому, что реализация floatCount перестанет быть монотонной. Введем число
pivmin – для данной арифметики с плавающей точкой, минимальное положительное
число, такое что 𝑒𝑖2 /𝑝𝑖𝑣𝑚𝑖𝑛 не равно бесконечности (не вызывает переполнения) для всех
i=0,…,n-2. Тогда численно стабильная версия floatCount1 может быть реализована с
помощью алгоритма 5.
function floatCount1(x, pivmin)
diff = d0 - x;
if |diff| < pivmin then diff = -pivmin;
if diff <= 0 then floatCount1 = 1 else floatCount1 = 0;
for i = 1 to n - 1
2
diff = di - x - 𝑒𝑖−1
/ diff;
if |diff| < pivmin then diff = -pivmin;
if diff <= 0 then floatCount1 = floatCount1 + 1;
end for
end function
Алгоритм 5 – численно устойчивая реализация функции floatCount.
Если в предыдущей реализации floatCount при делении e2i−1 на diff меньше pivmin в
результате получалась бесконечность (специальное состояние вещественного числа – INF
из стандарта IEEE 754 [3]), то следующая операция e2i−1/diff даст 0. При делении на
бесконечность всегда получается 0. Это приведет к неточности.
Реализация floatCount1 лишена такого недостатка, так как в ней diff не может быть
2
по модулю меньше pivmin. Следовательно, операция 𝑒𝑖−1
/diff никогда не приведет к
переполнению.
Отдельного упоминания заслуживает функция isNarrow, которая должна
возвращать истину, если интервал достаточно узок. В теории, алгоритм бисекции
интервалов должен давать сколько угодно узкий интервал. А поскольку значение
собственного числа является средним арифметическим от верней и нижней границ
интервала, это должно позволить находить собственные числа о сколько угодно высокой
точностью. Но на практике в арифметике с плавающей точкой может быть представлено
только несколько цифр. Для чисел одинарной точности количество цифр – 6-7. Это
значит, что например числа в окрестности единицы могут быть представлены с точностью
до 6 знака после запятой, а в окрестности 10000 – только с точностью до 3 знака. Это
необходимо учитывать при сравнении чисел с плавающей точкой, а также при
определении практической минимальной возможной ширины интервала, после которой
продолжать разбиение интервала бессмысленно.
Введем минимальное число eps, такое что 1 + eps <> 1. Для вещественных чисел с
одинарной точностью стандарта IEEE 754 это значение равно 1.192092896e-07, для чисел
с двойной точностью - 2.2204460492503131e-016. В арифметике с плавающей точкой если
модуль разности между числом и единицей меньше eps, считается что число равно
единице. Помня, что точность представления с плавающей точкой относительна, можно
обобщить это утверждение: если |a-b|<max(|a|, |b|)∙eps, то числа a и b равны. Это свойство
вещественных чисел можно использовать для проверки сошелся интервал или нет.
Однако пользователь может желать указать также минимальную ширину
интервала, после которой нет смысла производить деление интервала. Назовем такую
ширину абсолютной погрешностью. Тогда функцию isNarrow можно реализовать с
помощью алгоритма 6, где a и b соответственно нижняя и верхняя границы интервала,
absTol – абсолютная минимальная ширина интервала, заданная пользователем, а relTol =
eps для машин с IEEE 754 подобной арифметикой.
function isNarrow(a, b, absTol, relTol)
width = |a - b|;
norm = max(|a|, |b|);
minWidth = max(absTol, relTol * norm);
isNarrow = (width < minWidth);
end function
Алгоритм 6 – реализация функции isNarrow, возвращающей истину, если
дальнейшее деление интервала [a, b) не оправдано.
Реализация поиска собственных чисел в LAPACK
В библиотеке алгоритмов LAPACK (Linear Algebra PACKage – пакет линейной
алгебры) [4] также используется реализация алгоритма поиска собственных чисел
методом последовательного разбиения интервала Гершгорина. Для этого в LAPACK
существует функция *STEBZ.
*STEBZ вычисляет собственные числа симметричной тридиагональной матрицы T.
Пользователь может вычислить с ее помощью все собственные числа, все собственные
числа в полуоткрытом интервале (VL, VU], или собственные числа с номерами с IL по IU.
Чтобы избежать переполнения, матрица должна быть отмасштабирована так, чтобы
наибольший по модулю элемент не превышал √𝑜𝑣𝑒𝑟𝑓𝑙𝑜𝑤 ∙ 4√𝑢𝑛𝑑𝑒𝑟𝑓𝑙𝑜𝑤 , и для большей
точности, он не должен быть значительно меньше этого значения.
В LAPACK присутствуют различные реализации *STEBZ, различающиеся первой
буквой названия, которая определяет тип используемых для вычислений переменных:
вещественных одинарной (SSTEBZ) или двойной (DSTEBZ) точностью.
Реализация из AMD APP SDK (статья?)
Моя реализация
Сравнение
Выводы
Источники
1. J. Demmel, I. Dhillon, and H. Ren, On The Correctness Of Some Bisection-Like
Parallel Eigenvalue Algorithms In Floating Point Arithmetic, Trans. Num. Anal. (ETNA), 3,
1996.
2. http://en.wikipedia.org/wiki/Gershgorin_circle_theorem
3. IEEE Standard for Floating-Point Arithmetic
http://ieeexplore.ieee.org/xpl/mostRecentIssue.jsp?punumber=4610933
4. LAPACK http://www.netlib.org/lapack/
Download