Русская Википедия:Алгоритм Штрассена

Материал из Онлайн справочника
Перейти к навигацииПерейти к поиску

Алгоритм Штрассена предназначен для быстрого умножения матриц. Он был разработан Фолькером Штрассеном в 1969 году и является обобщением метода умножения Карацубы на матрицы.

В отличие от традиционного алгоритма умножения матриц (по формуле <math>c_{ij}=\sum a_{ik} b_{kj}</math>), работающего за время <math>\Theta(n^{\log_2 8}) = \Theta(n^3)</math>, алгоритм Штрассена умножает матрицы за время <math>\Theta(n^{\log_2 7}) = O(n^{2.81})</math>, что даёт выигрыш на больших плотных матрицах.

Несмотря на то, что алгоритм Штрассена является асимптотически не самым быстрым из существующих алгоритмов быстрого умножения матриц, он проще программируется и эффективнее при умножении матриц относительно малого размера, поэтому именно он чаще используется на практике.

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

Файл:Strassen-scheme.png
Для умножения (2x2)-матриц используются произведения сумм и разностей элементов. На иллюстрации каждое такое произведение взято в отдельную цветную рамку, а способ их компоновки в финальную матрицу обозначен исходящими лучами.

Если добавить к матрицам <math>A</math> и <math>B</math> одинаковые нулевые строки и столбцы, их произведение станет равно матрице <math>AB</math> с теми же добавленными строками и столбцами. Поэтому можно рассматривать только матрицы размера <math>n=2^k,\ k \in {\mathbb N}</math>, а другие случаи сводить к этому добавлением нулей, отчего <math>n</math> может увеличиться лишь вдвое.

Пусть <math>A,B</math> – матрицы размера <math>2^k \times 2^k</math>. Их можно представить как блочные матрицы размера <math>(2 \times 2)</math> из <math>(2^{k-1} \times 2^{k-1})</math>–матриц:

<math>

A = \begin{pmatrix} A_{11} & A_{12} \\ A_{21} & A_{22} \end{pmatrix}, \quad B = \begin{pmatrix} B_{11} & B_{12} \\ B_{21} & B_{22} \end{pmatrix} </math>

По принципу блочного умножения, матрица <math>AB</math> выражается через их произведение

<math>

AB = \begin{pmatrix} A_{11} B_{11} + A_{12} B_{21} & A_{11} B_{12} + A_{12} B_{22} \\ A_{21} B_{11} + A_{22} B_{21} & A_{21} B_{12} + A_{22} B_{22} \end{pmatrix}\ , </math>

где в правой части происходит восемь умножений матриц размера <math>2^{k-1} \times 2^{k-1}</math>. Поскольку матрицы образуют кольцо, то для вычисления правой части годится любой алгоритм умножения <math>(2 \times 2)</math>-матриц, использующий лишь сложения, вычитания и умножения. Штрассен предложил такой алгоритм с семью умножениями:

<math>

\begin{align} D &= (A_{11} + A_{22}) (B_{11} + B_{22}); \\ D_1 &= (A_{12} - A_{22}) (B_{21} + B_{22}); \\ D_2 &= (A_{21} - A_{11}) (B_{11} + B_{12}); \\ H_1 &= (A_{11} + A_{12}) B_{22}; \\ H_2 &= (A_{21} + A_{22}) B_{11}; \\ V_1 &= A_{22} (B_{21} - B_{11}); \\ V_2 &= A_{11} (B_{12} - B_{22}); \\ \end{align} </math>

<math>

\begin{align} AB &= \begin{pmatrix} D & 0 \\ 0 & D \end{pmatrix} + \begin{pmatrix} D_1 & 0 \\ 0 & D_2 \end{pmatrix} + \begin{pmatrix} -H_1 & H_1 \\ H_2 & -H_2 \end{pmatrix} + \begin{pmatrix} V_1 & V_2 \\ V_1 & V_2 \end{pmatrix} \\ &= \begin{pmatrix} D+D_1+V_1-H_1 & V_2+H_1 \\ V_1+H_2 & D+D_2+V_2-H_2 \end{pmatrix}\ . \end{align} </math>

Каждое умножение можно совершать рекурсивно по той же процедуре, а сложение – тривиально, складывая <math>(2^{k-1})^2</math> элементов. Тогда время работы алгоритма <math>T(n)</math> оценивается через рекуррентное соотношение:

<math>T(n) = 7 T(n/2) + O(n^2) = O(n^{\log_2 7})\ .</math>

Пример реализации

Ниже приведён пример реализации алгоритма на языке Python с использованием библиотеки NumPy для быстрого взятия подматриц. Основная функция – strassen_mul. Предполагается, что все матрицы квадратны, представлены типом numpy.array и их размер является степенью 2.

При небольших размерах матрицы прямое умножение оказывается быстрее алгоритма Штрассена из-за большого числа сложений в последнем. Граница таких размеров зависит от соотношения времени сложения и умножения элементов и поэтому может варьироваться в зависимости от аппаратной среды. В коде за её назначение отвечает константа TRIVIAL_MULTIPLICATION_BOUND.

from itertools import product
import numpy as np

def split_to_2x2_blocks(matrix):
	return list(map(
		lambda row: np.hsplit(row, 2),
		np.vsplit(matrix, 2)
	))

def strassen_mul_2x2(lb, rb):
	d = strassen_mul(lb[0][0] + lb[1][1], rb[0][0] + rb[1][1])
	d_1 = strassen_mul(lb[0][1] - lb[1][1], rb[1][0] + rb[1][1])
	d_2 = strassen_mul(lb[1][0] - lb[0][0], rb[0][0] + rb[0][1])

	left = strassen_mul(lb[1][1], rb[1][0] - rb[0][0])
	right = strassen_mul(lb[0][0], rb[0][1] - rb[1][1])
	top = strassen_mul(lb[0][0] + lb[0][1], rb[1][1])
	bottom = strassen_mul(lb[1][0] + lb[1][1], rb[0][0])

	return [[d + d_1 + left - top, right + top],
			[left + bottom, d + d_2 + right - bottom]]

def trivial_mul(left, right):
	height, mid_size = left.shape
	mid_size, width = right.shape

	result = np.zeros((height, width))
	for row, col, mid in product(*map(range, [height, width, mid_size])):
		result[row][col] += left[row][mid] * right[mid][col]

	return result

TRIVIAL_MULTIPLICATION_BOUND = 8

def strassen_mul(left, right):
	assert(left.shape == right.shape)
	assert(left.shape[0] == left.shape[1])

	if left.shape[0] <= TRIVIAL_MULTIPLICATION_BOUND:
		return trivial_mul(left, right)

	assert(left.shape[0] % 2 == 0)
	return np.block(
		strassen_mul_2x2(*map(split_to_2x2_blocks, [left, right]))
	)

Дальнейшее развитие

Штрассен был первым, кто показал возможность умножения матриц более эффективным способом, чем стандартный. После публикации его работы в 1969 начались активные поиски более быстрого алгоритма. Самым асимптотически быстрым алгоритмом на сегодняшний день является алгоритм Копперсмита — Винограда, позволяющий перемножать матрицы за <math>{\rm O}(n^{2.376})</math> операций[1], предложенный в 1987 году и усовершенствованный в 2011 году до уровня <math>{\rm O}(n^{2.3727})</math>[1]. Этот алгоритм не представляет практического интереса в силу астрономически большой константы в оценке арифметической сложности. Вопрос о предельной в асимптотике скорости перемножения матриц не решен. Существует гипотеза Штрассена о том, что для достаточно больших <math>n</math> существует алгоритм перемножения двух матриц размера <math>n \times n</math> за <math>{\rm O}(n^{2+\varepsilon})</math> операций, где <math>\varepsilon</math> сколь угодно малое наперед заданное положительное число. Эта гипотеза имеет сугубо теоретический интерес, так как размер матриц, для которых <math>\varepsilon</math> действительно мало, по всей видимости очень велик.

Вопрос о построении наиболее быстрого и устойчивого практического алгоритма умножения больших матриц также остается нерешённым.

Алгоритм Винограда — Штрассена

Существует модификация алгоритма Штрассена, для которой требуется 7 умножений и 15 сложений (вместо 18 для обычного алгоритма Штрассена).

Матрицы <math> A, \, B, \, C</math> делятся на блочные подматрицы как показано выше.

Вычисляются промежуточные элементы <math>S_1, \, \ldots, \, S_8, \, P_1, \, \ldots, \, P_7, \, T_1, \, T_2</math>

<math>

\begin{align} S_1 &= (A_{21} + A_{22}); \\ S_2 &= (S_1 - A_{11}); \\ S_3 &= (A_{11} - A_{21}); \\ S_4 &= (A_{12} - S_2); \\ S_5 &= (B_{12} - B_{11}); \\ S_6 &= (B_{22} - S_5); \\ S_7 &= (B_{22} - B_{12}); \\ S_8 &= (S_6 - B_{21}); \\ P_1 &= S_2 S_6; \\ P_2 &= A_{11} B_{11}; \\ P_3 &= A_{12} B_{21}; \\ P_4 &= S_3 S_7; \\ P_5 &= S_1 S_5; \\ P_6 &= S_4 B_{22}; \\ P_7 &= A_{22} S_8; \\ T_1 &= P_1 + P_2; \\ T_2 &= T_1 + P_4. \end{align} </math>

Элементы матрицы <math>C</math> вычисляются следующим образом:

<math>

\begin{pmatrix} C_{11} & C_{12} \\ C_{21} & C_{22} \end{pmatrix} = \begin{pmatrix} P_2 + P_3 & T_1 + P_5 + P_6 \\ T_2 - P_7 & T_2 + P_5 \end{pmatrix}. </math>

Современное состояние вопроса

Алгоритм Штрассена является билинейным алгоритмом, его коэффициенты являются корнями кубической системы уравнений Брента.[2] Для класса точных алгоритмов <2x2x2> это минимальная задача, решение которой позволяет уменьшить число умножений в кольце элементов матриц.[3][4] Проблема поиска новых алгоритмов заключается в том, что система Брента нелинейная, числа неизвестных и уравнений (эти числа не совпадают) быстро растет с увеличением размера матриц и требуются решения только с большим количеством нулей.

B 2013 году после частичного преодоления этих проблем удалось найти первый практически реализуемый билинейный алгоритм умножения матриц, который асимптотически быстрее алгоритма Штрассена.[5] Алгоритм Смирнова <3x3x6; 40> умножает матрицу 3X3 на матрицу 3x6, используя 40 умножений вместо 54. Его асимптотическая сложность <math> O(n^{\log_{54} 64000}) = O(n^{2.78})</math>. (Тензорное умножение алгоритма на себя с циклическим сдвигом аргументов приводит к алгоритму для квадратных матриц <54x54x54; 64000> с той же сложностью). Для реального ускорения умножения требуется существенная оптимизация — удаление множества дублирующих вычислений в линейных формах.

По состоянию на 2022 год это асимптотически наиболее быстрый практически реализуемый билинейный алгоритм для произвольного поля элементов матриц.

В 2022 году компанией DeepMind при помощи нейросетевого алгоритма AlphaTensor были найдены несколько новых алгоритмов перемножения матриц различных размерностей. Однако их скорость для произвольного поля далека от скорости известных лучших алгоритмов. Так для матриц 4X4 алгоритм Штрассена требует 49 умножений, а AlphaTensor нашёл алгоритм, требующий 47 умножений, но работает он только для поля Шаблон:Нп5.[6][7]

Примечания

Шаблон:Примечания

Литература

  1. 1,0 1,1 Шаблон:Cite web
  2. R.P.Brent. Algorithms for matrix multiplications// Computer Science Dept. Report CS 157, Stanford University, 1970.
  3. Сложность умножения матриц. Обзор//Кибернетич. сборник. 1988. Вып. 25. С. 139—236.
  4. Landsberg J. M. Geometry and the complexity of matrix multiplication// Bull. Amer. Math. Soc. 2008. V.45. P. 247—284.
  5. Шаблон:Cite web
  6. Шаблон:Cite web
  7. Шаблон:Статья