Ir para o conteúdo

Algoritmo de Strassen

Origem: Wikipédia, a enciclopédia livre.
Para multiplicar matrizes (2x2), são utilizados os produtos das somas e diferenças dos elementos. Na imagem, cada produto é colocado em uma moldura colorida separada, e a forma como eles são combinados na matriz final é indicada por raios que saem deles.

Em álgebra linear, o algoritmo de Strassen, em homenagem a Volker Strassen, é um algoritmo de multiplicação de matrizes. É mais rápido que o algoritmo de multiplicação de matrizes padrão para matrizes grandes, com melhor complexidade assintótica ( versus ), embora o algoritmo ingênuo seja frequentemente melhor para matrizes menores. O algoritmo de Strassen é mais lento do que os algoritmos conhecidos mais rápidos para matrizes extremamente grandes, mas tais algoritmos galácticos não são úteis na prática, pois são muito mais lentos para matrizes de tamanho prático. Para matrizes pequenas, existem algoritmos ainda mais rápidos.

O algoritmo de Strassen funciona para qualquer anel, como soma/multiplicação, mas não para todos os semianéis, como min-plus ou álgebra booliana, onde o algoritmo ingênuo ainda funciona, sendo a chamada multiplicação de matrizes combinatória.

História

[editar | editar código]

Volker Strassen publicou este algoritmo pela primeira vez em 1969 e, assim, provou que o algoritmo de multiplicação de matrizes geral não era ótimo.[1] A publicação do algoritmo de Strassen resultou em mais pesquisas sobre multiplicação de matrizes que levaram a limites inferiores assintóticos e a limites superiores computacionais melhorados.

Algoritmo

[editar | editar código]
A coluna esquerda visualiza os cálculos necessários para determinar o resultado de uma multiplicação de matrizes 2x2. A multiplicação de matrizes ingênua requer uma multiplicação para cada "1" da coluna esquerda. Cada uma das outras colunas (M1-M7) representa uma única das 7 multiplicações no algoritmo de Strassen. A soma das colunas M1-M7 dá o mesmo resultado que a multiplicação de matrizes completa à esquerda.

Sejam , duas matrizes quadradas sobre um anel , por exemplo, matrizes cujas entradas são números inteiros ou números reais. O objetivo da multiplicação de matrizes é calcular o produto matricial . A seguinte exposição do algoritmo assume que todas essas matrizes têm tamanhos que são potências de dois (isto é, ), mas isso é apenas conceitualmente necessário — se as matrizes , não são do tipo , as linhas e colunas "faltantes" podem ser preenchidas com zeros para obter matrizes com tamanhos de potências de dois — embora implementações reais do algoritmo não façam isso na prática.

O algoritmo de Strassen particiona , e em matrizes blocos de tamanhos iguais

com . O algoritmo ingênuo seria:

Esta construção não reduz o número de multiplicações: ainda são necessárias 8 multiplicações de blocos de matrizes para calcular as matrizes , o mesmo número de multiplicações necessário ao usar a multiplicação de matrizes padrão.

O algoritmo de Strassen define, em vez disso, novos valores:

usando apenas 7 multiplicações (uma para cada ) em vez de 8. Podemos agora expressar os em termos de :

Iteramos recursivamente este processo de divisão até que as submatrizes degenerem em números (elementos do anel ). Se, como mencionado acima, a matriz original tivesse um tamanho que não é potência de 2, então o produto resultante terá linhas e colunas zero assim como e , e estas serão então removidas neste ponto para obter a matriz (menor) que realmente queríamos.

Implementações práticas do algoritmo de Strassen alternam para métodos padrão de multiplicação de matrizes para submatrizes suficientemente pequenas, para as quais esses algoritmos são mais eficientes. O ponto de transição específico para o qual o algoritmo de Strassen é mais eficiente depende da implementação e do hardware. Autores anteriores estimaram que o algoritmo de Strassen é mais rápido para matrizes com larguras de 32 a 128 para implementações otimizadas.[2] No entanto, observou-se que este ponto de transição tem aumentado nos últimos anos, e um estudo de 2010 descobriu que mesmo uma única etapa do algoritmo de Strassen muitas vezes não é benéfica em arquiteturas atuais, em comparação com uma multiplicação tradicional altamente otimizada, até que os tamanhos das matrizes excedam 1000 ou mais, e mesmo para tamanhos de matriz de vários milhares, o benefício é tipicamente marginal na melhor das hipóteses (cerca de 10% ou menos).[3] Um estudo mais recente (2016) observou benefícios para matrizes tão pequenas quanto 512 e um benefício em torno de 20%.[4]

Melhorias no algoritmo de Strassen

[editar | editar código]

É possível reduzir o número de adições de matrizes usando a seguinte forma descoberta por Winograd em 1971:[5]

onde .

Isso reduz o número de adições e subtrações de matrizes de 18 para 15. O número de multiplicações de matrizes continua sendo 7, e a complexidade assintótica é a mesma.[6]

O algoritmo foi posteriormente otimizado em 2017 usando uma base alternativa,[7] reduzindo o número de adições de matrizes por etapa bilinear para 12, mantendo o número de multiplicações de matrizes, e novamente em 2023:[8]

Complexidade assintótica

[editar | editar código]

O esboço do algoritmo acima mostrou que podemos nos livrar de apenas 7, em vez das tradicionais 8, multiplicações matriz-matriz para os sub-blocos da matriz. Por outro lado, é necessário fazer adições e subtrações de blocos, embora isso não seja preocupante para a complexidade geral: Adicionar matrizes de tamanho requer apenas operações, enquanto a multiplicação é substancialmente mais cara (tradicionalmente operações de adição ou multiplicação).

A questão é quantas operações exatamente são necessárias para o algoritmo de Strassen e como isso se compara com a multiplicação de matrizes padrão que leva aproximadamente (onde ) operações aritméticas, ou seja, uma complexidade assintótica .

O número de adições e multiplicações necessárias no algoritmo de Strassen pode ser calculado como segue: seja o número de operações para uma matriz . Então, pela aplicação recursiva do algoritmo de Strassen, vemos que , para alguma constante que depende do número de adições realizadas em cada aplicação do algoritmo. Portanto, , ou seja, a complexidade assintótica para multiplicar matrizes de tamanho usando o algoritmo de Strassen é . A redução no número de operações aritméticas, no entanto, tem o preço de uma estabilidade numérica um pouco reduzida,[9] e o algoritmo também requer significativamente mais memória em comparação com o algoritmo ingênuo. Ambas as matrizes iniciais devem ter suas dimensões expandidas para a próxima potência de 2, o que resulta no armazenamento de até quatro vezes mais elementos, e as sete matrizes auxiliares contêm cada uma um quarto dos elementos das matrizes expandidas.

O algoritmo de Strassen precisa ser comparado com a maneira "ingênua" de fazer a multiplicação de matrizes que exigiria 8 em vez de 7 multiplicações de sub-blocos. Isso daria então origem à complexidade que se espera da abordagem padrão: . A comparação desses dois algoritmos mostra que, assintoticamente, o algoritmo de Strassen é mais rápido: existe um tamanho tal que matrizes maiores são multiplicadas de forma mais eficiente com o algoritmo de Strassen do que da maneira "tradicional". No entanto, a afirmação assintótica não implica que o algoritmo de Strassen seja sempre mais rápido, mesmo para matrizes pequenas, e na prática este não é o caso: Para matrizes pequenas, o custo das adições adicionais de blocos de matrizes supera as economias no número de multiplicações. Há também outros fatores não capturados pela análise acima, como a diferença no custo em hardware atual entre carregar dados da memória para os processadores versus o custo de realmente realizar operações sobre esses dados. Como consequência desses tipos de considerações, o algoritmo de Strassen é tipicamente usado apenas em matrizes "grandes". Esse tipo de efeito é ainda mais pronunciado com algoritmos alternativos como o de Coppersmith e Winograd: Embora assintoticamente ainda mais rápidos, o ponto de transição é tão grande que o algoritmo geralmente não é usado em matrizes que encontramos na prática.

Posto ou complexidade bilinear

[editar | editar código]

A complexidade bilinear ou posto de um mapa bilinear é um conceito importante na complexidade assintótica da multiplicação de matrizes. O posto de um mapa bilinear sobre um corpo F é definido como (um tanto quanto um abuso de notação)

Em outras palavras, o posto de um mapa bilinear é o comprimento de seu cálculo bilinear mais curto.[10] A existência do algoritmo de Strassen mostra que o posto da multiplicação de matrizes não é maior que sete. Para ver isso, vamos expressar este algoritmo (juntamente com o algoritmo padrão) como tal cálculo bilinear. No caso de matrizes, os espaços duais A* e B* consistem em mapas para o corpo F induzidos por um produto duplo escalar (ou seja, neste caso, a soma de todas as entradas de um produto de Hadamard.)

Algoritmo padrãoAlgoritmo de Strassen
1
2
3
4
5
6
7
8

Pode-se mostrar que o número total de multiplicações elementares necessárias para a multiplicação de matrizes está assintoticamente fortemente ligado ao posto , isto é, , ou mais especificamente, como as constantes são conhecidas, . Uma propriedade útil do posto é que ele é submultiplicativo para produtos tensoriais, e isso permite mostrar que a multiplicação de matrizes pode ser realizada com não mais que multiplicações elementares para qualquer . (Este produto tensorial -vezes do mapa de multiplicação de matrizes consigo mesmo — uma -ésima potência tensorial — é realizado pela etapa recursiva no algoritmo mostrado.)

Comportamento de cache

[editar | editar código]

O algoritmo de Strassen é cache-oblivious. A análise de seu comportamento de cache mostrou que ele incorre em

falhas de cache durante sua execução, assumindo um cache idealizado de tamanho (isto é, com linhas de comprimento ).[11]:13

Considerações de implementação

[editar | editar código]

A descrição acima afirma que as matrizes são quadradas e o tamanho é uma potência de dois, e que o preenchimento com zeros deve ser usado se necessário. Essa restrição permite que as matrizes sejam divididas ao meio, recursivamente, até o limite da multiplicação escalar. A restrição simplifica a explicação e a análise de complexidade, mas não é realmente necessária;[12] e, de fato, preencher a matriz como descrito aumentará o tempo de computação e pode facilmente eliminar as pequenas economias de tempo obtidas pelo uso do método em primeiro lugar.

Uma boa implementação observará o seguinte:

  • Não é necessário nem desejável usar o algoritmo de Strassen até o limite dos escalares. Em comparação com a multiplicação de matrizes convencional, o algoritmo adiciona uma carga de trabalho considerável em adições/subtrações; portanto, abaixo de um certo tamanho, será melhor usar a multiplicação convencional. Assim, por exemplo, uma matriz não precisa ser preenchida para , pois poderia ser subdividida até matrizes e a multiplicação convencional pode então ser usada nesse nível.
  • O método pode de fato ser aplicado a matrizes quadradas de qualquer dimensão.[3] Se a dimensão for par, elas são divididas ao meio como descrito. Se a dimensão for ímpar, o preenchimento com zero por uma linha e uma coluna é aplicado primeiro. Esse preenchimento pode ser aplicado sob demanda e de forma preguiçosa, e as linhas e colunas extras são descartadas à medida que o resultado é formado. Por exemplo, suponha que as matrizes sejam . Elas podem ser divididas de modo que a parte superior esquerda seja e a inferior direita seja . Onde as operações exigirem, as dimensões de são preenchidas com zero para primeiro. Observe, por exemplo, que o produto é usado apenas na linha inferior da saída, portanto, é necessário apenas ter linhas de altura; e assim o fator esquerdo usado para gerá-lo precisa ter apenas linhas de altura; consequentemente, não há necessidade de preencher essa soma para linhas; só é necessário preencher para colunas para corresponder a .

Além disso, não há necessidade de as matrizes serem quadradas. Matrizes não quadradas podem ser divididas ao meio usando os mesmos métodos, resultando em matrizes não quadradas menores. Se as matrizes forem suficientemente não quadradas, valerá a pena reduzir a operação inicial a produtos mais quadrados, usando métodos simples que são essencialmente , por exemplo:

  • Um produto de tamanho pode ser feito como 20 operações separadas , arranjadas para formar o resultado;
  • Um produto de tamanho pode ser feito como 10 operações separadas , somadas para formar o resultado.

Essas técnicas tornarão a implementação mais complicada, em comparação com simplesmente preencher para um quadrado de potência de dois; no entanto, é uma suposição razoável que qualquer pessoa que realize uma implementação de Strassen, em vez da multiplicação convencional, colocará uma prioridade maior na eficiência computacional do que na simplicidade da implementação.

Na prática, o algoritmo de Strassen pode ser implementado para alcançar melhor desempenho do que a multiplicação convencional mesmo para matrizes tão pequenas quanto , para matrizes que não são nada quadradas e sem exigir espaço de trabalho além dos buffers já necessários para uma multiplicação convencional de alto desempenho.[4]

Implementação em C

[editar | editar código]
/*------------------------------------------------------------------------------*/
void strassen(double **a, double **b, double **c, int tam){

	// caso base:
	if(tam == 1){
		c[0][0] = a[0][0] * b[0][0];
		return;
	}


	else{
		int novoTam = tam/2;
		double **a11, **a12, **a21, **a22;
		double **b11, **b12, **b21, **b22;
		double **c11, **c12, **c21, **c22;
		double **p1, **p2, **p3, **p4, **p5, **p6, **p7;

		// alocação de memória:
		a11 = alocar_matriz_real(novoTam, -1);
		a12 = alocar_matriz_real(novoTam, -1);
		a21 = alocar_matriz_real(novoTam, -1);
		a22 = alocar_matriz_real(novoTam, -1);

		b11 = alocar_matriz_real(novoTam, -1);
		b12 = alocar_matriz_real(novoTam, -1);
		b21 = alocar_matriz_real(novoTam, -1);
		b22 = alocar_matriz_real(novoTam, -1);

		c11 = alocar_matriz_real(novoTam, -1);
		c12 = alocar_matriz_real(novoTam, -1);
		c21 = alocar_matriz_real(novoTam, -1);
		c22 = alocar_matriz_real(novoTam, -1);

		p1 = alocar_matriz_real(novoTam, -1);
		p2 = alocar_matriz_real(novoTam, -1);
		p3 = alocar_matriz_real(novoTam, -1);
		p4 = alocar_matriz_real(novoTam, -1);
		p5 = alocar_matriz_real(novoTam, -1);
		p6 = alocar_matriz_real(novoTam, -1);
		p7 = alocar_matriz_real(novoTam, -1);
		
		double **aResult = alocar_matriz_real(novoTam, -1);
		double **bResult = alocar_matriz_real(novoTam, -1);

		int i, j;


		//dividindo as matrizes de entrada nas 4 submatrizes:
            for (i = 0; i < novoTam; i++)
            {
                for (j = 0; j < novoTam; j++)
                {
                    a11[i][j] = a[i][j];
                    a12[i][j] = a[i][j + novoTam];
                    a21[i][j] = a[i + novoTam][j];
                    a22[i][j] = a[i + novoTam][j + novoTam];

                    b11[i][j] = b[i][j];
                    b12[i][j] = b[i][j + novoTam];
                    b21[i][j] = b[i + novoTam][j];
                    b22[i][j] = b[i + novoTam][j + novoTam];
                }
            }

		// Cálculos de p1 até p7:

		soma(a11, a22, aResult, novoTam); // a11 + a22
		soma(b11, b22, bResult, novoTam); // b11 + b22
		strassen(aResult, bResult, p1, novoTam); // p1 = (a11+a22) * (b11+b22)

		soma(a21, a22, aResult, novoTam); // a21 + a22
		strassen(aResult, b11, p2, novoTam); // p2 = (a21+a22) * (b11)

		subtrai(b12, b22, bResult, novoTam); // b12 - b22
		strassen(a11, bResult, p3, novoTam); // p3 = (a11) * (b12 - b22)

		subtrai(b21, b11, bResult, novoTam); // b21 - b11
		strassen(a22, bResult, p4, novoTam); // p4 = (a22) * (b21 - b11)

		soma(a11, a12, aResult, novoTam); // a11 + a12
		strassen(aResult, b22, p5, novoTam); // p5 = (a11+a12) * (b22)	

		subtrai(a21, a11, aResult, novoTam); // a21 - a11
		soma(b11, b12, bResult, novoTam); // b11 + b12
		strassen(aResult, bResult, p6, novoTam); // p6 = (a21-a11) * (b11+b12)
		
		subtrai(a12, a22, aResult, novoTam); // a12 - a22
		soma(b21, b22, bResult, novoTam); // b21 + b22
		strassen(aResult, bResult, p7, novoTam); // p6 = (a21-a11) * (b11+b12)

		soma(p3, p5, c12, novoTam); // c12 = p3 + p5
		soma(p2, p4, c21, novoTam); // c21 = p2 + p4

		soma(p1, p4, aResult, novoTam); // p1 + p4
		soma(aResult, p7, bResult, novoTam); // p1 + p4 + p7
		subtrai(bResult, p5, c11, novoTam); // c11 = p1 + p4 - p5 + p7

		soma(p1, p3, aResult, novoTam); // p1 + p3
		soma(aResult, p6, bResult, novoTam); // p1 + p3 + p6
		subtrai(bResult, p2, c22, novoTam); // c22 = p1 + p3 - p2 + p6


		// agrupando os resultados obtidos em uma única matriz (conquista):
		 for (i = 0; i < novoTam ; i++)
            {
                for (j = 0 ; j < novoTam ; j++)
                {
                    c[i][j] = c11[i][j];
                    c[i][j + novoTam] = c12[i][j];
                    c[i + novoTam][j] = c21[i][j];
                    c[i + novoTam][j + novoTam] = c22[i][j];
                }
            }



		// desalocação de memória:
		a11 = liberar_matriz_real(a11, novoTam);
		a12 = liberar_matriz_real(a12, novoTam);
		a21 = liberar_matriz_real(a21, novoTam);
		a22 = liberar_matriz_real(a22, novoTam);

		b11 = liberar_matriz_real(b11, novoTam);
		b12 = liberar_matriz_real(b12, novoTam);
		b21 = liberar_matriz_real(b21, novoTam);
		b22 = liberar_matriz_real(b22, novoTam);

		c11 = liberar_matriz_real(c11, novoTam);
		c12 = liberar_matriz_real(c12, novoTam);
		c21 = liberar_matriz_real(c21, novoTam);
		c22 = liberar_matriz_real(c22, novoTam);

		p1 = liberar_matriz_real(p1, novoTam);
		p2 = liberar_matriz_real(p2, novoTam);
		p3 = liberar_matriz_real(p3, novoTam);
		p4 = liberar_matriz_real(p4, novoTam);
		p5 = liberar_matriz_real(p5, novoTam);
		p6 = liberar_matriz_real(p6, novoTam);
		p7 = liberar_matriz_real(p7, novoTam);
		aResult = liberar_matriz_real(aResult, novoTam);
		bResult = liberar_matriz_real(bResult, novoTam);
	} // fim do else

} // fim da função strassen

/*------------------------------------------------------------------------------*/
void soma(double **a, double **b, double **resultado, int tam){

	int i, j;

	for(i=0; i< tam; i++){
		for(j=0; j<tam; j++){
			resultado[i][j] = a[i][j] + b[i][j];
		}
	}
}

/*------------------------------------------------------------------------------*/
void subtrai(double **a, double **b, double **resultado, int tam){

	int i, j;

	for(i=0; i< tam; i++){
		for(j=0; j<tam; j++){
			resultado[i][j] = a[i][j] - b[i][j];
		}
	}	
}

/*------------------------------------------------------------------------------*/
// passar valor 0 para a variável randomico se quiser que matriz seja inicializada com 0, passar valor 1 se quiser que seja
// inicializada com valores aleatórios, e passar qualquer outro valor (-1 por exemplo) se quiser que a matriz
// não seja inicializada com valor nenhum.
double **alocar_matriz_real (int tam, int randomico)
{
   int i, j, n = tam, m = tam;
   double **v, a;         // ponteiro para o vetor

  // aloca o vetor
   v = (double**)malloc(n*sizeof(double*));

   if (v == NULL) {
       printf ("** Erro na alocacao da matriz: Memoria Insuficiente **");
       return (NULL);
       }

    
   for(i=0;i<n;i++)
   {
      v[i] = (double*)malloc(m*sizeof(double));
		
		if (v[i] == NULL) {
	       printf ("** Erro: Memoria Insuficiente **");
			   liberar_matriz_real(v, n);
	       return (NULL);
       }

		// inicializa a matriz com zeros
		if(randomico == 0){
			for(j=0; j<m; j++)
				v[i][j] = 0.0;
		}

		// inicializa a matriz com valores aleatórios entre 0 e 10
		else{
			if(randomico == 1){
				for(j=0; j<m; j++){
					a = rand();
					v[i][j] = (a - (int)a) * 10;
				}
			}
		}
   }

   return (v);     // retorna o ponteiro para o vetor
}

/*------------------------------------------------------------------------------*/
double **liberar_matriz_real (double **v, int tam)
{ // inicio funçao
   int i;
   if (v == NULL) return (NULL);

	for(i=0;i<tam;i++){ // inicio for
		if(v[i]){ // inicio if
   	   free(v[i]);
			 v[i] = NULL;
		} // fim if
	} // fim for

   free(v);         // libera o vetor /
	v = NULL;

   return (NULL);   //retorna um ponteiro nulo /
} // fim funcao

/*------------------------------------------------------------------------------*/


Ver também

[editar | editar código]

Referências

[editar | editar código]
  1. Strassen, Volker (1969). «Gaussian Elimination is not Optimal». Numer. Math. 13 (4): 354–356. doi:10.1007/BF02165411
  2. Skiena, Steven S. (1998). «§8.2.3 Matrix multiplication». The Algorithm Design Manual. Berlim, Nova Iorque: Springer-Verlag. ISBN 978-0-387-94860-7
  3. 1 2 D'Alberto, Paolo; Nicolau, Alexandru (2005). Using Recursion to Boost ATLAS's Performance (PDF). Sixth Int'l Symp. on High Performance Computing
  4. 1 2 Huang, Jianyu; Smith, Tyler M.; Henry, Greg M.; van de Geijn, Robert A. (13 de novembro de 2016). Strassen's Algorithm Reloaded. SC16: The International Conference for High Performance Computing, Networking, Storage and Analysis. IEEE Press. pp. 690–701. ISBN 9781467388153. doi:10.1109/SC.2016.58. Consultado em 1 de novembro de 2022
  5. Winograd, S. (Outubro de 1971). «On multiplication of 2 × 2 matrices». Linear Algebra and Its Applications (em inglês). 4 (4): 381–388. doi:10.1016/0024-3795(71)90009-7
  6. Knuth (1997), p. 500.
  7. Karstadt, Elaye; Schwartz, Oded (24 de julho de 2017). Matrix Multiplication, a Little Faster. Proceedings of the 29th ACM Symposium on Parallelism in Algorithms and Architectures. pp. 101–110. ISBN 978-1-4503-4593-4. doi:10.1145/3087556.3087579
  8. Schwartz, Oded; Vaknin, Noa (31 de dezembro de 2023). «Pebbling Game and Alternative Basis for High Performance Matrix Multiplication». SIAM Journal on Scientific Computing (em inglês). 45 (6): C277–C303. Bibcode:2023SJSC...45C.277S. ISSN 1064-8275. doi:10.1137/22M1502719
  9. Webb, Miller (1975). «Computational complexity and numerical stability». SIAM J. Comput. 4 (2): 97–107. doi:10.1137/0204009
  10. Burgisser; Clausen; Shokrollahi (1997). Algebraic Complexity Theory. [S.l.]: Springer-Verlag. ISBN 3-540-60582-7
  11. Frigo, M.; Leiserson, C. E.; Prokop, H.; Ramachandran, S. (1999). Cache-oblivious algorithms (PDF). Proc. IEEE Symp. on Foundations of Computer Science (FOCS). pp. 285–297
  12. Higham, Nicholas J. (1990). «Exploiting fast matrix multiplication within the level 3 BLAS» (PDF). ACM Transactions on Mathematical Software. 16 (4): 352–368. doi:10.1145/98267.98290. hdl:1813/6900Acessível livremente

Bibliografia

[editar | editar código]

Ligações externas

[editar | editar código]