O objetivo deste projeto é otimizar uma função criada para randomizar elementos de uma matriz exparsa, simétrica e com zeros na diagonal prinicipal. Além disso, as matrizes de interesse só possuem valores 1 ou 0. Para entender melhor, suponha que você possua uma matriz desta forma:
\[ \begin{bmatrix} 0 & 1 & 0 & 1 & 0\\ 1 & 0 & 1 & 0 & 1\\ 0 & 1 & 0 & 0 & 0\\ 1 & 0 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 & 0 \end{bmatrix} \]
Imagine agora que por algum motivo específico você queira trocar os elementos desta matriz de lugar, mas que ela continue com as propriedades citadas. Este procedimento é muito útil por exemplo, quando a matriz citada é uma matriz de vizinhança e queremos saber se a dependência entre os vizinhos continua caso façamos uma randomização na mesma.
Com esta motivação um código em R foi escrito, contudo, ele deixa a desejar quanto ao desempenho. Daí, através do pacote Rcpp algumas funções que compoem o código em R foram reescritas em C++.
O código possui 2 funções “chave”, que são:
sorteador: Esta função sorteia os elementos a serem trocados de lugar. Possui como entrada 2 vetores, que representam as posições da linha (i) e coluna (j) a serem trocadas.
Funciona da seguinte maneira: Enquanto o valor de \(i \leq j\), o sorteio é refeito. Isto é necessário já que a matriz é simétrica.
taboo: Esta função guarda um número “n” das posições que foram trocadas durante o processo dentro de uma matriz cujo os 2 primeiros elementos são a posição (i,j) de um elemento e os outros dois são as posições (i,j) do outro elemento, ambos sorteados pela função sorteador. A matriz desta função é normalmente chamada na literatura de “Matriz taboo”.
Note que a quantidade de linhas da matriz taboo dada pelo usúario nunca pode ser maior que o número de elementos a serem trocados,caso isso aconteça, o algoritmo entrará em “loop infinito”.
A seguir um exemplo para fixação do modo de uso da função usando a matriz citada anteriormente e seu respectivo código em R.
#Funções e Variaveis Globais
#OBS: Compilar na ordem que está disposto neste script
##########################################################
o.tab = function(e1,e2,tab,n.taboo)
{
flag = -1
for(i in 1:n.taboo)
{
if(anyNA(tab[i,1])==TRUE)
{
break
}
if(((e1[1]==tab[i,1])&&(e1[2]==tab[i,2]))||((e1[1]==tab[i,3])&&(e1[2]==tab[i,4]))||
((e2[1]==tab[i,1])&&(e2[2]==tab[i,2]))||((e2[1]==tab[i,3])&&(e2[2]==tab[i,4])))
{
flag = 0
break
}
else
{
flag = 1
}
}
return (flag)
}
###########################################################
#Sorteador das posicoes da matriz
sorteador = function(In,Jn)
{
j = i = NULL;
aux = numeric(2)
#ij eh a posicao sorteada para a iteração
i = sample(In, 1, replace = T)
j = sample(Jn, 1, replace = T)
while(i<=j)
{
i = sample(In, 1, replace = T)
j = sample(Jn, 1, replace = T)
}
aux = c(i,j)
return(aux)
}
############################################################
smr = function(matriz.original, n = 1000, n.taboo = 5)
{
tab = matrix(nrow = n.taboo ,ncol = 4)
m = matriz.original
m.or = matriz.original
aux1 = aux2 = e1 = e2 = flag = NULL # e1 e e2 sao as posições dos elementos de troca
o = t = ta = 0 #Contador de iterações do while
In = c(1:length(m.or[,1])) #range para o nº de linhas
Jn = c(1:length(m.or[1,])) #range para o nº de colunas
for(i in 1:n)
{
e1 = sorteador(In,Jn)
e2 = sorteador(In,Jn)
while(m.or[e1[1],e1[2]]==m.or[e2[1],e2[2]])
{
e1 = sorteador(In,Jn)
e2 = sorteador(In,Jn)
o = o + 1
}
if(t<n.taboo)
{t = t +1}
else
{t = 0}
tab[t,1] = e1[1]
tab[t,2] = e1[2]
tab[t,3] = e2[1]
tab[t,4] = e2[2]
if(n.taboo>0){
flag = o.tab(e1,e2,tab,n.taboo) #Sinalizador se o indice sorteado pertence a matriz taboo
while(flag < 1||m.or[e1[1],e1[2]]==m.or[e2[1],e2[2]])
{
e1 = sorteador(In,Jn)
e2 = sorteador(In,Jn)
flag = o.tab(e1,e2,tab,n.taboo)
ta = ta + 1
}
}
tab[t,1] = e1[1]
tab[t,2] = e1[2]
tab[t,3] = e2[1]
tab[t,4] = e2[2]
aux1 = m.or[e1[1],e1[2]]
aux2 = m.or[e1[2],e1[1]]
m.or[e1[1],e1[2]] = m.or[e2[1],e2[2]]
m.or[e1[2],e1[1]] = m.or[e2[2],e2[1]]
m.or[e2[1],e2[2]] = aux1
m.or[e2[2],e2[1]] = aux2
}
resu = list(m,m.or,tab,o,ta)
names(resu) = c("Matriz.Original","Matriz.Randomizada","Matriz.Taboo.n.5",
"N.de.vezes.elementos.iguais","Taboo.vezes.entrada")
return(resu)
}
mt <- mt <- cbind(c(0,1,0,1,0),c(1,0,1,0,1),c(0,1,0,0,0),c(1,0,0,0,0),c(0,1,0,0,0))
# matriz teste
smr(# matriz a ser randomizada
matriz.original = mt,
# Número de trocas
n = 1000,
# Número de elementos que irão entrar na matriz taboo
n.taboo = 3)
## $Matriz.Original
## [,1] [,2] [,3] [,4] [,5]
## [1,] 0 1 0 1 0
## [2,] 1 0 1 0 1
## [3,] 0 1 0 0 0
## [4,] 1 0 0 0 0
## [5,] 0 1 0 0 0
##
## $Matriz.Randomizada
## [,1] [,2] [,3] [,4] [,5]
## [1,] 0 0 0 1 0
## [2,] 0 0 0 1 1
## [3,] 0 0 0 0 1
## [4,] 1 1 0 0 0
## [5,] 0 1 1 0 0
##
## $Matriz.Taboo.n.5
## [,1] [,2] [,3] [,4]
## [1,] 4 2 2 1
## [2,] 3 1 5 3
## [3,] 4 1 5 4
##
## $N.de.vezes.elementos.iguais
## [1] 1002
##
## $Taboo.vezes.entrada
## [1] 12718
Agora a implementação das funções “chave” em C++ utilizando Rcpp e RcppArmadillo.
// [[Rcpp::depends(RcppArmadillo)]]
#include <RcppArmadillo.h>
#include <RcppArmadilloExtensions/sample.h>
using namespace Rcpp;
// Matriz taboo
// [[Rcpp::export]]
int tabooC(IntegerVector e1,
IntegerVector e2,
IntegerVector tab1,
IntegerVector tab2,
IntegerVector tab3,
IntegerVector tab4,
int ntaboo)
{
int flag = -1;
int i = 0;
for(i; i <ntaboo; i++)
{
//Rcout << tab[i] << "\n";
if(IntegerMatrix::is_na(tab1[i])==1)
{
break;
}
if(((e1[0]==tab1[i])&&(e1[1]==tab2[i]))||((e1[0]==tab3[i])&&(e1[1]==tab4[i]))||
((e2[0]==tab1[i])&&(e2[1]==tab2[i]))||((e2[0]==tab3[i])&&(e2[1]==tab4[i])))
{
flag = 0;
break;
}
else
{
flag = 1;
}
}
return flag;
}
// Sorteador
// [[Rcpp::export]]
IntegerVector sorteadorC(IntegerVector In,
IntegerVector Jn)
{
int i;
int j;
//ij eh a posicao sorteada para a iteração
i = Rcpp::RcppArmadillo::sample(In,1,1)[0];
j = Rcpp::RcppArmadillo::sample(Jn,1,1)[0];
while(i<=j)
{
i = Rcpp::RcppArmadillo::sample(In,1,1)[0];
j = Rcpp::RcppArmadillo::sample(Jn,1,1)[0];
}
IntegerVector aux;
aux = {i,j};
return aux;
}
Agora vamos fazer uma comparação entre as duas funções utilizando o pacote `microbenchmark´.
Primeiro vamos medir o tempo de uma única amostra.
# Funcoes em C
tabooCpre <- function(e1,e2,mt,n){
t1 <- mt[c(1:n),1]
t2 <- mt[c(1:n),2]
t3 <- mt[c(1:n),3]
t4 <- mt[c(1:n),4]
flag <- tabooC(e1,e2,t1,t2,t3,t4,n)
return(flag)
}
smrC = function(matriz.original, n = 1000, n.taboo = 5)
{
tab = matrix(nrow = n.taboo ,ncol = 4)
m = matriz.original
m.or = matriz.original
aux1 = aux2 = e1 = e2 = flag = NULL # e1 e e2 sao as posições dos elementos de troca
o = t = ta = 0 #Contador de iterações do while
In = c(1:length(m.or[,1])) #range para o nº de linhas
Jn = c(1:length(m.or[1,])) #range para o nº de colunas
for(i in 1:n)
{
e1 = sorteadorC(In,Jn)
e2 = sorteadorC(In,Jn)
while(m.or[e1[1],e1[2]]==m.or[e2[1],e2[2]])
{
e1 = sorteadorC(In,Jn)
e2 = sorteadorC(In,Jn)
o = o + 1
}
if(t<n.taboo)
{t = t +1}
else
{t = 0}
tab[t,1] = e1[1]
tab[t,2] = e1[2]
tab[t,3] = e2[1]
tab[t,4] = e2[2]
if(n.taboo>0){
flag = tabooCpre(e1,e2,tab,n.taboo) #Sinalizador se o indice sorteado pertence a matriz taboo
while(flag < 1||m.or[e1[1],e1[2]]==m.or[e2[1],e2[2]])
{
e1 = sorteadorC(In,Jn)
e2 = sorteadorC(In,Jn)
flag = tabooCpre(e1,e2,tab,n.taboo)
ta = ta + 1
}
}
tab[t,1] = e1[1]
tab[t,2] = e1[2]
tab[t,3] = e2[1]
tab[t,4] = e2[2]
aux1 = m.or[e1[1],e1[2]]
aux2 = m.or[e1[2],e1[1]]
m.or[e1[1],e1[2]] = m.or[e2[1],e2[2]]
m.or[e1[2],e1[1]] = m.or[e2[2],e2[1]]
m.or[e2[1],e2[2]] = aux1
m.or[e2[2],e2[1]] = aux2
}
resu = list(m,m.or,tab,o,ta)
names(resu) = c("Matriz.Original","Matriz.Randomizada","Matriz.Taboo.n.5",
"N.de.vezes.elementos.iguais","Taboo.vezes.entrada")
return(resu)
}
library(microbenchmark)
desempenho.mt <- microbenchmark(list = list(smrR = smr(matriz.original = mt,
n.taboo = 3),
smrC = smrC(matriz.original = mt,
n.taboo = 3)),
times = 1)
desempenho.mt
## Unit: nanoseconds
## expr min lq mean median uq max neval
## smrR 2161 2161 2161 2161 2161 2161 1
## smrC 675 675 675 675 675 675 1
Note que a função do R mostra-se bem inferior a que foi otimizada por C++.
Vamos agora fazer uma amostra de tamanho 10000 e comparar o desempenho das duas funções.
desempenho.mt <- microbenchmark(list = list(smrR = smr(matriz.original = mt,
n.taboo = 3),
smrC = smrC(matriz.original = mt,
n.taboo = 3)),
times = 10000)
desempenho.mt
## Unit: nanoseconds
## expr min lq mean median uq max neval
## smrR 9 11 18.3024 13 14 8710 10000
## smrC 9 11 18.6775 13 14 6990 10000
autoplot.microbenchmark(desempenho.mt)
## Loading required namespace: ggplot2
wilcox.test(desempenho.mt$time~desempenho.mt$expr)
##
## Wilcoxon rank sum test with continuity correction
##
## data: desempenho.mt$time by desempenho.mt$expr
## W = 49897000, p-value = 0.7983
## alternative hypothesis: true location shift is not equal to 0
Veja que as duas se comportam de forma muito parecidade e que o teste de Wilcox(Distribuição assimétrica) não rejeitou \(H_0\), sendo assim, elas mostram-se com um desempenho igual (estatísticamente falando).
As performances foram muito parecidas, porém, como o processo de cada uma é totalmente aleátorio, essa ainda não é uma comparação tão confiável. Mesmo assim, a função em C++ mostra-se bem superior a em R quando utilizada em outras máquinas.
Sendo assim, a implementação de parte da função em C++ valeu a pena.