segunda-feira, 28 de fevereiro de 2022

Algoritmo EM - Parte I

Hoje vou falar um pouco sobre um algoritmo de estimação bastante conhecido e utilizado quando temos dados faltantes ou variáveis latentes no modelo que estamos trabalhando: o algoritmo EM.

O algoritmo EM foi proposto por Dempster et al. (1977) e é um algoritmo iterativo de estimação de máxima verossimilhança. Este método envolve duas etapas: uma de Esperança (Expectation) e outra de Maximização (Maximization), daí seu nome. A ideia é, no primeiro passo, calcular os valores esperados das variáveis latentes e, no segundo passo, maximizar a função de interesse em relação aos parâmetros do modelo. Essa função é chamada de função Q, como veremos mais adiante.

Como dito, estamos em uma situação em que há dados ausentes, seja por haver missing data ou por modelarmos variáveis latentes para simplificar nosso modelo (o exemplo que darei reflete o segundo caso). Sem perda de generalidade, vamos considerar o caso em que há apenas uma variável latente. Seja $y$ o vetor de valores observados da variável resposta e $u$ a variável latente (ou variável que representa os dados faltantes). Trabalhamos com a função de log-verossimilhança completa dos dados, dada por

$l_c(\theta) = \log f(y,u) = \log f(y|u) + \log(u).$

A função acima seria realmente completa se tivéssemos observado os valores de $u$, mas não temos tais valores. Dessa forma, o primeiro passo do algoritmo EM - passo E - é calcular o valor esperado da função de log-verossimilhança completa dados os valores de $y$ e dos parâmetros de interesse. Para isso, definimos a função $Q$ como

$Q(\theta | \theta^{(k)},y) = E[\log f(y,u)|\theta^{(k)},y].$

Como dito, o algoritmo EM é iterativo, e a função acima calcula a esperança da função de log-verossimilhança completa dados os valores de $y$ e os valores de $\theta$ na $k$-ésima iteração. Ou seja, precisamos dos valores dos parâmetros de interesse para realizar esse passo. Para ficar claro: o algoritmo é iniciado com valores iniciais para os parâmetros.

O segundo passo do algoritmo EM - passo M - é realizado maximizando a função $Q$ em relação aos parâmetros de interesse, isto é,

$\hat{\theta}^{(k+1)} = \text{argmax} Q(\theta | \theta^{(k)},y) .$

Os dois passos são repetidos até que haja convergência na estimação dos parâmetros ou um número máximo de iterações seja atingida.

É importante dizer que os estimadores EM convergem para os estimadores de máxima verossimilhança (EMV). De fato, utilizamos o algoritmo EM quando não há forma fechada para os EMVs do modelo ou quando facilitamos o problema trabalhando com o modelo aumentado (verossimilhança completa).

Isso tudo é bem teórico e acredite quando falo que estou resumindo bastante as coisas aqui. Para tudo que fazemos há fundamentos mas esse blog é voltado para a parte prática, então vou contar que você pesquisará mais sobre o assunto se não é familiarizado com o algoritmo EM. Aliás, não queremos ser apertadores de botão, não é mesmo?

Para entender o método, vamos ver um exemplo simples e didático.

Exemplo: estimação dos parâmetros da distribuição t-Student (com graus de liberdade fixos)

Suponha que temos uma amostra aleatória de tamanho $n$ de uma população com distribuição t-Student com $\nu$ graus de liberdade e suponha que nosso interesse é estimar os parâmetros de média $\mu$ e de escala $\sigma^2$.

Uma opção seria escrever a função de log-verossimilhança da distribuição t e encontrar os estimadores de máxima verossimilhança (EMV). Mas queremos utilizar o algoritmo EM, então vamos escrever a variável $Y$, que segue uma distribuição t, na seguinte forma hierárquica:

$\begin{eqnarray}Y|U &\sim& N(\mu,u^{-1}\sigma^2), \nonumber \\ U &\sim& Gamma(\nu/2,\nu/2). \nonumber \end{eqnarray}$

Note que, marginalmente (em relação a $U$), $Y$ possui uma distribuição t-Student com $\nu$ graus de liberdade.

Bom, agora vamos construir o algoritmo para nosso exemplo. A função de log-verossimilhança completa é dada por

$\begin{eqnarray} l_c(\theta) &\propto& \frac{n}{2} \log(u) - \frac{n}{2} \log(\sigma^2) - \frac{u}{2\sigma^2} \sum_{i=1}^{n} (y_i-\mu)^2 - \frac{\nu}{2} u + \left(\frac{\nu}{2}-1\right) \log(u). \nonumber \end{eqnarray}$

Escrevi "proporcional" por não precisarmos das constantes (em relação aos parâmetros). A função $Q$ será dada por

$\begin{eqnarray} Q(\theta | \theta^{(k)},y) &\propto& \frac{n}{2} E[\log(u)| \theta^{(k)},y] - \frac{n}{2} \log(\sigma^2) - \frac{E[u| \theta^{(k)},y]}{2\sigma^2} \sum_{i=1}^{n} (y_i-\mu)^2 \nonumber\\ && - \frac{\nu}{2} E[u| \theta^{(k)},y] + \left(\frac{\nu}{2}-1\right) E[\log(u)| \theta^{(k)},y]. \nonumber \end{eqnarray}$

O segundo passo é maximizar a função $Q$ em relação aos parâmetros de interesse, no caso, $\mu$ e $\sigma^2$. Para isso, derivamos a função $Q$ em relação ao parâmetro e igualamos a zero para encontrar seu estimador. Note, na função acima, que há termos que são constantes em relação aos parâmetros e, após a derivação, serão anulados. Dessa forma, podemos simplificar ainda mais a função $Q$ mantendo apenas os termos que apresentam $\mu$ e $\sigma^2$, ou seja,

$\begin{eqnarray} Q(\theta | \theta^{(k)},y) &\propto& - \frac{n}{2} \log(\sigma^2) - \frac{E[u| \theta^{(k)},y]}{2\sigma^2} \sum_{i=1}^{n} (y_i-\mu)^2. \nonumber \end{eqnarray}$

Isso nos ajuda (e muito), pois não precisamos calcular a distribuição de $\log(u)| \theta^{(k)},y$ e diminuímos nosso trabalho. Para simplificar a notação, vamos escrever $E[u| \theta^{(k)},y] = E_{U|Y}$.

Agora sim vamos para o segundo passo do algoritmo. Primeiro, vamos encontrar o estimador para a média:

$\begin{eqnarray} \dfrac{d}{d\mu}Q() = \dfrac{E_{U|Y}}{\sigma^2} (\sum_{i=1}^{n} y_i- n\mu).  \nonumber \end{eqnarray}$

Igualando a derivada acima a zero, obtemos

$\begin{eqnarray} \hat{\mu} = \dfrac{\sum_{i=1}^{n} y_i}{n}. \nonumber \end{eqnarray}$

Agora vamos encontrar o estimador para $\sigma^2$:

$\begin{eqnarray} \dfrac{d}{d\sigma^2}Q() = -\dfrac{n}{2\sigma^2} + \dfrac{E_{U|Y}}{2\sigma^4} \sum_{i=1}^{n} (y_i- \mu)^2.  \nonumber \end{eqnarray}$

o que nos leva ao seguinte estimador:

$\begin{eqnarray} \hat{\sigma}^2 = E_{U|Y} \dfrac{\sum_{i=1}^{n} (y_i- \mu)^2}{n}. \nonumber \end{eqnarray}$

Você deve ter notado falta de alguma coisa: como calculamos $E_{U|Y}$? Para isso, vamos descobrir a distribuição de $U|Y$ e, em seguida, calcular seu valor esperado. Note que

$\begin{eqnarray} f(u|y) &\propto& f(y|u)f(u) \nonumber \\ &\propto& (u)^{n/2} \exp \left\{ -\dfrac{u}{2\sigma^2} \sum_{i=1}^{n} (y_i- \mu)^2 \right\} \times \exp\left\{-\dfrac{\nu}{2}u\right\} u^{\nu/2-1} \nonumber \\ &\propto& (u)^{(n+\nu)/2 -1} \exp \left\{ -u\left(\dfrac{\nu}{2} + \dfrac{\sum_{i=1}^{n} (y_i- \mu)^2}{2\sigma^2}\right) \right\}. \nonumber \end{eqnarray}$

A última expressão corresponde ao núcleo de uma distribuição Gamma. Logo,

$$U|Y \sim Gamma \left( \dfrac{1}{2}(n+\nu), \dfrac{1}{2} (\nu + \sum_{i=1}^{n} (y_i- \mu)^2/\sigma^2) \right). $$

O valor esperado de uma distribuição Gamma($\alpha,\beta$) é $\alpha/\beta$. Então,

$$E_{U|Y} = \dfrac{(n-\nu)\sigma^2}{\nu\sigma^2 + \sum_{i=1}^{n} (y_i- \mu)^2}.$$

Agora temos o valor de $E_{U|Y}$ e os estimadores dos parâmetros de interesse. Estamos prontos para executar o algoritmo EM:

  1. Assuma valores iniciais $\hat{\mu}^{(0)}$ e $(\hat{\sigma}^2)^{(0)}$ e um número de iterações máxima.
  2. E: Dados os valores de $y$, $\hat{\mu}^{(k)}$ e $(\hat{\sigma}^2)^{(k)}$, calcule $E_{U|Y}$.
  3. M: Dados os valores de $y$ e $E_{U|Y}$, calcule $\hat{\mu}^{(k+1)}$ e $(\hat{\sigma}^2)^{(k+1)}$.
  4. Se houve convergência ou o número máximo de iterações foi atingida, retorne as estimativas dos parâmetros. Caso contrário, repita os passos 2-3.

Com esse exemplo é fácil ver como o algoritmo EM funciona. Gastamos um tempo para explicar e exemplificar o método para que fique claro seu funcionamento, pois iremos implementá-lo no próximo post. E para programar precisamos entender o que estamos fazendo, não é verdade?

Então espero você na próxima aula!

Referência citada

Dempster, A.P., Laird, N.M., and Rubin, D.B. (1977). Maximum Likelihood from Incomplete Data via the EM Algorithm. Journal of the Royal Statistical Society. Series B, 39(1):1-38.


sexta-feira, 25 de fevereiro de 2022

Calculando determinante de uma matriz pelo Teorema de Laplace - Parte III

Nos posts anteriores, implementamos o teorema de Laplace para calcular o determinante de uma matriz quadrada. Na Parte I construímos a função recursiva e, na Parte II, a função iterativa. Neste post, vamos comparar a eficiência das duas funções que criamos e com a função det embutida no R.

Para isso, vamos utilizar o pacote rbenchmark. Vamos testar nossas funções com uma matriz de ordem 5:

set.seed(7)
M = matrix(sample(1:100,size = 25,replace = F),ncol=5,byrow = T)

> M
     [,1] [,2] [,3] [,4] [,5]
[1,]   42   83   31   92   66
[2,]   15   90    8   67   88
[3,]   40   22   47   93   59
[4,]   12   51   20   94   98
[5,]   79    6   16   89   86

Vamos utilizar a função benchmark e fazer 1000 replicações. Vejamos:

> benchmark(det(M),calc_det(M),calc_det_laplace(M),replications = 1000,
+           columns = c("test", "replications", "elapsed","relative"))
                 test replications elapsed relative
2         calc_det(M)         1000    0.19    6.333
3 calc_det_laplace(M)         1000    0.25    8.333
1              det(M)         1000    0.03    1.000

Veja que a função det é muito mais eficiente que as nossas, o que era esperado, pois as funções do R já são otimizadas.

Agora perceba algo interessante: a função recursiva foi executada mais rápida que a iterativa. Isso ocorreu, provavelmente, pelo fato de construirmos e armazenamos a árvore de matrizes. Se fizermos a conta, o tempo de execução da função iterativa é 1,32 vezes o tempo da função recursiva.

Quanto maior a dimensão da matriz, mais custoso será para calcularmos seu determinante e será mais pesado para nossas funções. Por exemplo, considere a matriz de ordem 10 abaixo:

set.seed(7)
M = matrix(sample(1:100,size = 100,replace = F),ncol=10,byrow = T)

> M
      [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
 [1,]   42   83   31   92   66   15   90    8   67    88
 [2,]   40   22   47   93   59   12   51   20   94    98
 [3,]   79    6   16   89   86   21    4   11   26     2
 [4,]   38   43   36    3   53   41   96   77   87    45
 [5,]   91   65   81   34   32   85   75   62   24    10
 [6,]   46   82   48   23    5   56   70   97   76    78
 [7,]   64   28  100   18   29   84   30   63   55    80
 [8,]   74   44   17   73   13   61   60   52   37     1
 [9,]   39   54   25   27   68   71    9   35   19    49
[10,]   14   72   58   57   99   69    7   33   95    50

Para essa matriz, temos os seguintes tempos de execução:

> t1 = Sys.time()
> det(M)
[1] 1.197026e+18
> t2 = Sys.time()
> calc_det_laplace(M)
[1] 1.197026e+18
> t3 = Sys.time()
> calc_det(M)
[1] 1.197026e+18
> t4 = Sys.time()

> t2-t1
Time difference of 0.002995014 secs
> t3-t2
Time difference of 10.98335 secs
> t4-t3
Time difference of 6.374352 secs

Perceba que todas as funções apresentaram o mesmo resultado, como era de se esperar. Mas observe o tempo de execução. A função det não chegou a 0,1 segundo de execução, ao passo que nossas funções apresentam tempos bem maiores.

Isso nos mostra que é preferível utilizar as funções já otimizadas do R ao invés de implementá-las. É claro, nossa estratégia de pegar sempre a primeira linha da matriz é ingênua e o código não está otimizado, mas é fato que vale a pena implementar esse tipo de função se for melhor do que o R já disponibiliza.

O intuito com o exercício dessa série foi praticar a programação e utilizamos vários conceitos para isso. Logo, nossa missão foi cumprida.

Espero que tenha gostado da aula.

Até a próxima aula!

quarta-feira, 23 de fevereiro de 2022

Calculando determinante de uma matriz pelo Teorema de Laplace - Parte II

No post anterior (Parte I), implementamos o algoritmo para calcular o determinante de uma matriz via teorema de Laplace (ou método do cofator). No caso, fizemos o método recursivo. Nesse post, faremos o método iterativo.

Para isso, temos que pensar em algumas coisas:

  1. Para calcular o determinante, precisamos calcular cofatores.
  2. Para calcular cofatores, precisamos calcular o determinante das matrizes reduzidas.
  3. Para calcular o determinante das matrizes reduzidas, precisamos calcular os cofatores dessas matrizes...
  4. Até chegarmos nas matrizes reduzidas de ordem 3, que sabemos calcular o determinante de forma direta.

Perceba que, para cada matriz de ordem n, temos n submatrizes de ordem n-1. Para cada matriz de ordem n-1, temos n-1 submatrizes de ordem n-2, e assim por diante. É como se tivéssemos uma árvore de matrizes, cuja base é composta pelas matrizes de ordem 3. A Figura abaixo apresenta um exemplo para uma matriz de ordem 5.


Após calcular o determinante das matrizes de ordem 3, calculamos os determinantes das matrizes de ordem 4, depois das de ordem 5... ou seja, subimos os níveis da árvore de matrizes.

Aqui eu preciso deixar claro que vamos trabalhar com a estrutura de dados de lista e não a de árvore propriamente dita. Porém, continuarei chamando a estrutura de árvore por ser mais fácil de entender a lógica do problema.

Logo, precisamos saber quais são todas essas matrizes. Para isso, vamos implementar uma função que, dada uma matriz de ordem n, calcula as submatrizes de ordem n-1. Assim como no post anterior, para facilitar o problema, vamos escolher sempre a primeira linha da matriz para computar o método do cofator. Vejamos o código:

calc_submatriz = function(A){
  auxsub = A[-1,];
  
  subm = list();
  for(i in 1:ncol(A)) subm[[i]] = auxsub[,-i]
  
  return(subm)
}

O que estamos fazendo no código acima?

  1. Retiramos a primeira linha da matriz;
  2. Iniciamos subm como uma lista para receber as submatrizes.
  3. Para cada coluna da matriz, retiramos a i-ésima coluna e armazenamos em subm.
  4. Retornamos o resultado.

Perceba que calculamos as submatrizes apenas para o próximo nível da árvore. Precisamos calcular todas as submatrizes de ordem inferiores dada uma matriz original. Vamos ao código:

calc_all_submatriz = function(A){
  n = ncol(A);
  if(n<=3){
    cat("Não é necessário calcular as submatrizes.\n")
    return()
  }else{
    res = list()
    res[[1]] = list(ori = A);
    for(i in 2:(n-2)){
      auxres = list();
      auxj = 1;
      for(j in 1:length(res[[i-1]])){
        res_ = calc_submatriz(res[[i-1]][[j]])
        for(w in 1:length(res_)){
          auxres[[auxj]] = res_[[w]]  
          auxj = auxj +1;
        }
      }
      res[[i]] = auxres;
    }
  }
  
  return(res)
}

Vejamos novamente o que estamos fazendo:

  1. Se a dimensão da matriz é igual ou inferior a 3, não é necessário calcular submatrizes.
  2. Caso contrário, iniciamos nossa lista de resultado res.
  3. O primeiro nível da árvore conterá a matriz original.
  4. Para o próximo nível, calculamos as submatrizes da seguinte forma: 
  5. Para cada submatriz do nível anterior, calcule as submatrizes correspondentes.
  6. Armazene essas submatrizes na lista auxres.
  7. Repita os passos 5 e 6 até acabarem as submatrizes do nível anterior.
  8. Armazene auxres na lista de resultado res.
Dessa forma, res será uma lista em que cada elemento dessa lista conterá todas as submatrizes de um nível da árvore. Por exemplo, res[[1]] armazena a matriz original, res[[2]] armazena todas as submatrizes de ordem n-1, res[[3]] armazena todas as submatrizes de ordem n-2 e assim por diante, até res[[n-2]] que armazena todas as matrizes de ordem 3.

Vamos testar nosso código? Vamos listar a árvore de matrizes de uma matriz de ordem 4.

D = matrix(c(1,2,3,4,2,1,3,4,2,1,9,7,4,5,1,4),ncol=4,byrow=T)

calc_all_submatriz(D)

[[1]]
[[1]]$ori
     [,1] [,2] [,3] [,4]
[1,]    1    2    3    4
[2,]    2    1    3    4
[3,]    2    1    9    7
[4,]    4    5    1    4


[[2]]
[[2]][[1]]
     [,1] [,2] [,3]
[1,]    1    3    4
[2,]    1    9    7
[3,]    5    1    4

[[2]][[2]]
     [,1] [,2] [,3]
[1,]    2    3    4
[2,]    2    9    7
[3,]    4    1    4

[[2]][[3]]
     [,1] [,2] [,3]
[1,]    2    1    4
[2,]    2    1    7
[3,]    4    5    4

[[2]][[4]]
     [,1] [,2] [,3]
[1,]    2    1    3
[2,]    2    1    9
[3,]    4    5    1

Você pode verificar que todas as submatrizes de ordem 3 estão especificadas corretamente. Também pode testar com uma matriz de ordem maior, de ordem 5 por exemplo.

Logo, já temos nossa função para calcular todas as submatrizes de uma matriz dada. Agora, precisamos calcular o determinante das matrizes de ordem 3 e depois voltar aos níveis da árvore para calcular os determinantes das matrizes de ordem superior.

Vamos começar definindo a função:

calc_det_laplace = function(A){
  n = ncol(A);
  if(n==2){
    return(calc_det2(A));
  }else if(n==3){
    return(calc_det3(A));
  }
  
  (...)
  
}

Definimos a função e, novamente, se a matriz dada possui ordem 2 ou 3, chamamos a respectiva função para calcular seu determinante de forma direta. Caso a matriz seja de ordem superior a 3, aplicamos o método do cofator.

Vamos continuar com o código da função:

  submat = calc_all_submatriz(A);
  n_levels = length(submat);

No código acima:

  1. Calculamos todas as submatrizes da matriz dada e armazenamos em submat,
  2. Verificamos quantos níveis essa árvore possui.

O próximo passo é calcular o determinante de todas as submatrizes de ordem 3:

  for(i in 1:length(submat[[n_levels]])) auxdet1[i] = calc_det3(submat[[n_levels]][[i]])

Ou seja, estamos acessando o último nível da árvore (lembre que estamos utilizando lista e não a estrutura de dados árvore) e, para cada matriz desse nível, calculando seu determinante.

O próximo passo é, dados os determinantes das matrizes de ordem 3, calcular os determinantes das matrizes de ordem 4, depois das de ordem 5 até chegar na matriz original. Vejamos o código:

  for(i in (n_levels-1):1){
    cofator = NULL;
    dim_mat = ncol(submat[[i]][[1]])
    
    auxdet2 = NULL;
    for(j in 1:length(submat[[i]])){
      aux_sub = submat[[i]][[j]][1,]
      cofator = rep(-1,dim_mat)^(1+1:dim_mat) * auxdet1[((j-1)*dim_mat+1):(j*dim_mat)];
      auxdet2[j] = sum(aux_sub*cofator);
    }
    auxdet1 = auxdet2;
  }

Analisemos o código acima: 

  • O primeiro for (da variável i) irá caminhar pelos níveis da árvore, do penúltimo ao primeiro. 
  • Iniciamos a variável que armazenará os cofatores dos elementos da primeira linha da submatriz.
  • Verificamos a dimensão da submatriz desejada.
  • Iniciamos a variável auxdet2 para armazenar os determinantes do nível superior da matriz.
  • O segundo for (da variável j) irá caminhar pelas matrizes do nível i da árvore.
  • Para cada matriz desse nível, pegamos sua primeira linha, calculamos seus respectivos cofatores e, em seguida, calculamos o determinante dessa matriz e armazenamos em auxdet2.
  • Repetimos esse processo para todas as matrizes de cada nível da árvore, sempre subindo o nível (que, no caso, significa diminuir o índice i) até chegar à matriz original.
  • Quando i=1, calculamos o determinante da matriz desejada.

Após a execução acima, retornamos o resultado.

  return(auxdet1);

O código completo da função está abaixo:

calc_det_laplace = function(A){
  n = ncol(A);
  if(n==2){
    return(calc_det2(A));
  }else if(n==3){
    return(calc_det3(A));
  }
  
  submat = calc_all_submatriz(A);
  n_levels = length(submat);
  auxdet1 = NULL;
  
  for(i in 1:length(submat[[n_levels]])) auxdet1[i] = calc_det3(submat[[n_levels]][[i]])
    
  for(i in (n_levels-1):1){
    cofator = NULL;
    dim_mat = ncol(submat[[i]][[1]])
    
    auxdet2 = NULL;
    for(j in 1:length(submat[[i]])){
      aux_sub = submat[[i]][[j]][1,]
      cofator = rep(-1,dim_mat)^(1+1:dim_mat) * auxdet1[((j-1)*dim_mat+1):(j*dim_mat)];
      auxdet2[j] = sum(aux_sub*cofator);
    }
    auxdet1 = auxdet2;
  }
  
  return(auxdet1);
}

Note que realizamos todo o processo sem o uso da recursão. Porém, pagamos o preço de armazenar toda a árvore de submatrizes.

Vamos testar nosso código com o mesmo exemplo do post anterior:

D = matrix(c(1,2,3,4,2,1,3,4,2,1,9,7,4,5,1,4),ncol=4,byrow=T)
print(D)

     [,1] [,2] [,3] [,4]
[1,]    1    2    3    4
[2,]    2    1    3    4
[3,]    2    1    9    7
[4,]    4    5    1    4

det(D)
[1] 72

calc_det_laplace(D)
[1] 72

Agora temos uma função para calcular o determinante de uma matriz utilizando o teorema de Laplace e de forma iterativa! No próximo post, vamos comparar a eficiência das funções que criamos com a função embutida no R.

Espero que tenha gostado da aula.

Até a próxima aula!

segunda-feira, 21 de fevereiro de 2022

Calculando determinante de uma matriz pelo Teorema de Laplace - Parte I

O determinante de uma matriz é uma função que, dada a matriz, retorna um número real. Na estatística, a própria fórmula da distribução normal multivariada apresenta o determinante da matriz de covariâncias. 

O computador realiza a operação para nós, sem nos preocuparmos como está sendo calculado. Mas você já parou para pensar em, além de como é calculado, como é implementado o algoritmo? Vamos fazer isso nesse post, vamos implementar o cálculo de determinante pelo Teorema de Laplace que utiliza o método do cofator. Para isso, apresentarei duas formas de implementá-lo: o método recursivo e o método iterativo.

Se você já viu algo sobre recursividade em programação, sabe que ela pode ser um vilão para a memória do computador. Dessa forma, transformar o algoritmo recursivo em um algoritmo iterativo pode ser uma boa ideia. Nesse post faremos o algoritmo recursivo e, no próximo, o algoritmo iterativo.

A primeira coisa que vamos fazer é implementar a função que calcula o determinante de matrizes de ordem 2 e 3, que pode ser feito de forma simples e direta. É o que fazemos no seguinte código: 

#  Determinante de matrizes de ordem 2 e 3 ---------------

calc_det2 = function(A){
  res = A[1,1]*A[2,2] - A[1,2]*A[2,1]
  return(res)
}

calc_det3 = function(A){
  res = A[1,1]*A[2,2]*A[3,3] + A[1,2]*A[2,3]*A[3,1] + A[1,3]*A[2,1]*A[3,2]
  res = res - (A[1,3]*A[2,2]*A[3,1] + A[1,1]*A[2,3]*A[3,2] + A[1,2]*A[2,1]*A[3,3])
  return(res)
}

Para matrizes de ordem maior que 3 o cálculo não é direto. Utilizamos, por exemplo, o teorema de Laplace, que diz que o determinante de uma matriz será dado pela soma do produto dos elementos de uma linha ou coluna (a sua escolha) pelos seus respectivos cofatores. Obviamente, esse método serve para as matrizes de ordem 3 e 2, porém o cálculo é mais complicado e o método direto é preferível.

Para calcular o cofator de um elemento, precisamos do determinante da matriz reduzida. De fato, $C(a_{ij}) = (-1)^{(i+j)} \times |D_{ij}|$, em que $a_{ij}$ é o elemento da linha i e coluna j; e  $D_{ij}$ é a matriz obtida retirando-se a linha i e a coluna j (que estou chamando de matriz reduzida). Logo, necessitamos do determinante de uma outra matriz para calcular o determinante da matriz original. Por exemplo, se queremos calcular o determinante de uma matriz 4x4 e escolhemos a primeira linha para aplicar o método, precisamos calcular o determinante de quatro matrizes reduzidas 3x3 primeiro (uma para o cofator de cada elemento da linha escolhida).

O que isso está nos dizendo? Que se temos uma matriz n x n e queremos calcular seu determinante pelo método do cofator, precisamos calcular o determinante de todas as matrizes reduzidas de ordem 3 primeiro para depois calcular as de ordem maior, até chegar na matriz original.

Na verdade, poderíamos reduzir as matrizes até a ordem 1 no método, mas já sabemos calcular o determinante de matrizes de ordem 3 diretamente, então poupamos cálculo e também memória do computador.

Vamos começar nosso algoritmo. Declaramos nossa função:

calc_det = function(A){
 ...
}

Agora basta implementar. Pense comigo, se a matriz passada pelo parâmetro é de ordem 2 ou 3, já sabemos calcular, correto? Então, basta verificar a ordem da matriz e chamar uma das duas funções.

if(ncol(A)==3){
    return(calc_det3(A));
  }else if(ncol(A)==2){
    return(calc_det2(A));
}

Agora, se a ordem da matriz é maior que 3, aplicamos o método do cofator. Podemos escolher qualquer linha ou coluna para aplicar o método e normalmente utilizamos a que contiver o maior número de zeros para facilitar os cálculos. Não faremos essa verificação aqui. Para simplificar o problema, vamos escolher sempre a primeira linha da matriz (inclusive das matrizes reduzidas, caso necessário) para realizar o método.

Além disso, precisamos de uma variável para armazenar o resultado do determinante e da matriz atual (matriz reduzida). Vejamos:

  L1 = A[1,]
  det_ = 0
  auxM = A[-1,]

Até aqui estamos apenas preparando o terreno. Note que auxM é a matriz passada na função sem a primeira linha, como queríamos. Agora, temos que retirar as colunas, o que será feito de forma iterativa, isto é, para cada elemento da linha, retiramos sua respectiva coluna. Vejamos:

  for(i in 1:length(L1)){
    auxM2 = auxM[,-i]
    auxdet = calc_det(auxM2)
    cofat = (-1)^(1+i) * auxdet
    det_ = det_ + L1[i]*cofat
  }

Vamos por partes, das linhas internas ao for: 

  • Linha 1: calcula a matriz reduzida retirando a i-ésima coluna da matriz auxM.
  • Linha 2: calcula o determinante da matriz reduzida.
  • Linha 3: calcula o cofator associado ao elemento atual da linha escolhida da matriz.
  • Linha 4: soma o cálculo do cofator ao resultado final.
Agora é a parte interessante. Quando o código vai da linha 2 para a linha 3? Apenas quando calculamos o determinante da matriz reduzida. Se você reparou, estamos chamando a função calc_det dentro dela. Chamamos isso de recursão em programação. Antes de avançar para a linha 3, a linha 2 deve ser finalizada, mas isso acontecerá apenas quando a função retornar o determinante da matriz reduzida. 

Por exemplo, se estamos trabalhando com uma matriz 4x4, as matrizes reduzidas serão de ordem 3 e a função retornará o determinante dessas matrizes em uma etapa (pois calculamos diretamente). Mas se estamos trabalhando com uma matriz 5x5, as matrizes reduzidas serão de ordem 4 e, para calcular seu determinante, necessitamos das matrizes reduzidas de ordem 3. Logo, para avançar da linha 2 para a linha 3, vários códigos são executados recursivamente. Quando obtemos os determinantes das matrizes reduzidas de ordem 3, conseguimos calcular os determinantes das matrizes reduzidas de ordem 4 para, então, conseguir calcular o determinante da matriz original de ordem 5. Parece confuso, mas não é. Caso necessite, uma boa estudada sobre recursão ajuda.

Ao final da execução, devemos retornar o resultado:

  return(aux)

O código completo da função está abaixo:

calc_det = function(A){
  if(ncol(A)==3){
    return(calc_det3(A));
  }else if(ncol(A)==2){
    return(calc_det2(A));
  }
  
  L1 = A[1,]
  det_ = 0
  auxM = A[-1,]
  
  for(i in 1:length(L1)){
    auxM2 = auxM[,-i]
    auxdet = calc_det(auxM2)
    cofat = (-1)^(1+i) * auxdet
    det_ = det_ + L1[i]*cofat
  }
  return(det_)
}
Vamos testar nosso código. Primeiro com uma matriz de ordem 4:
D = matrix(c(1,2,3,4,2,1,3,4,2,1,9,7,4,5,1,4),ncol=4,byrow=T)
print(D)

     [,1] [,2] [,3] [,4]
[1,]    1    2    3    4
[2,]    2    1    3    4
[3,]    2    1    9    7
[4,]    4    5    1    4

det(D)
[1] 72

calc_det(D)
[1] 72

A função det é a função implementada no R e estamos utilizando para verificar se nossa função calc_det está retornando o resultado correto. Podemos ver que nossa função retornou o resultado esperado.

Vamos testar com uma matriz de ordem 5:

E = matrix(c(3,9,1,8,1,7,4,9,2,1,2,3,
             4,2,1,3,4,2,1,9,7,4,5,1,4),ncol=5,byrow=T)
print(E)

     [,1] [,2] [,3] [,4] [,5]
[1,]    3    9    1    8    1
[2,]    7    4    9    2    1
[3,]    2    3    4    2    1
[4,]    3    4    2    1    9
[5,]    7    4    5    1    4

det(E)
[1] -629

calc_det(E)
[1] -629

Agora, temos nossa própria função para calcular o determinante de uma matriz via método do cofator (ou teorema de Laplace)!

No próximo post, faremos a versão iterativa do nosso algoritmo e vamos comparar a eficiência das função.

Espero que tenha gostado da aula.

Até a próxima aula!