O que você precisa saber sobre linguagem R para implementar algoritmos evolutivos

Omar Andres Carmona Cortes
Semper Evolutionis
Published in
12 min readDec 1, 2017

Como eu já mencionei no meu artigo “Implementando Algoritmo Genéticos em R”, a escolha pela linguagem R se deve a quatro principais fatores: facilidades de trabalhar com matrizes e vetores; é uma linguagem pronta pra análise estatística; possui mais de 10.000 pacotes em seu repositório; e, não menos importante, é free. Neste artigo trataremos do essencial para podermos implementar algoritmos evolutivos: variáveis, vetores e matrizes, funções, listas e funções de grupo.

Neste artigo estamos usando R 64 bits versão 3.3.3 e RStudio 1.0.136. Bem, vamos começar então a falar logo sobre R!

Variáveis

O primeiro conceito visto em qualquer linguagem são as variáveis. Como R é uma linguagem não fortemente tipada, como o são Linguagens como C, Java, dentre outras, não é necessário definí-las previamente. Muito embora, pré-definir vetores e matrizes ajude o programa a executar mais rapidamente. Outra característica importante é que variáveis em R são case sensitive, ou seja, minúsculas e maiúsculas são consideradas diferentes. Finalmente, uma atribuição é feita através do símbolo ‘<-’. Na verdade pode-se utilizar o símbolo ‘=’ como em outras linguagens, mas eu tentei usá-lo e meu código gerou resultados estranhos. Por esse motivo prefiro focar no tradicional e usar o símbolo ‘<-’. Então são atribuições válidas:

Observe que a terceira atribuição está invertida, porém é valida, pois o símbolo ‘->’ indica que o valor 4 está sendo atribuído à variável c. Outra característica interessante da linguagem R é a atribuição múltipla, na qual duas ou mais variáveis podem ser atribuídas em uma única instrução:

Mas quais identificadores eu posso usar na linguagem R? Já estava chegando lá. Além das regras básicas da maioria das linguagens de não poder começar com um número nem com caracteres especiais, em R permite-se a utilização do underscore ‘_’ e do ponto ‘.’. Por isso, em R o identificador tamanho.pop é válido, ou seja, pop não é o atributo de um objeto chamado tamanho. Muito embora em R seja possível programar de forma orientada a objetos, este não é o objetivo deste artigo.

Vetores

Assim como em outras linguagens, um vetor é um conjunto ou coleção de variáveis do mesmo tipo. Seu acesso é através de colchetes, sendo a primeira posição 1 e não 0. Os vetores em R são inicializados usando a função c() de combinar (combine em inglês). Uma característica interessante, que também esta presente em Matlab e em NumPy (Python), é o acesso consecutivo a elementos. Em R também é possível acessar elementos não consecutivos usando vetores como índices (isso também está disponível em Matlab. Em Numpy eu não sei :P). A seguir mostramos um exemplo de como criar um vetor e os tipos de acessos mencionados.

Observe que no exemplo a[c(1, 4, 2)] estamos acessando a posições 1, 4 e 2 do vetor a. O mesmo se aplica aos vetores de strings.

Um aspecto interessante da função c() é que ela permite a concatenação de dois ou mais vetores, estejam eles criados ou não. Por exemplo, considerando o vetor a, a função c(a, c(10, 9, 8)) concatenaria o vetor a com o vetor [10, 9, 8]. Vale destacar que varias combinações podem ser feitas em uma única função c() como mostrado a seguir:

Três funções que trabalham em cima de vetores são importantes: length(), rep() e which(). A função length() devolve a quantidade de elementos de um vetor. A função rep() repete o elemento definido pela quantidade de vezes especificada na função. Esta função é importante quando o vetor é formado pelo mesmo valor em todas as posições, ou, para alocar espaço de memória antes de um laço. A alocação não é necessária em R, porém, deixa a execução do código mais rápida se efetuada. A função which(condição) devolve os índices dos elementos do vetor que obedecem a uma condição especificada. A figura abaixo mostra um exemplo de uso dessas funções.

A partir da função which() derivam-se duas importantes funções para quem quer implementar algoritmos evolutivos: which.min() e which.max(). Estas funções devolvem o índice do menor e maior elemento dentro de um vetor respectivamente.

Matrizes

Juntamente com os vetores, as matrizes são a alma na implementação de algoritmos evolutivos, sendo criadas a partir do uso da função matrix() cujos parâmetros mais importantes são: (i) o vetor que será transformado em matriz; (ii) a quantidade de linhas ou colunas; e (iii) se a matriz será gerada por linha ou por coluna. Se o terceiro parâmetro for omitido a matriz será criada por coluna, que é a notação matemática de matrizes. Caso se queira criar uma matriz por linhas, deve-se usar o parâmetro byrow = TRUE, como mostrado no exemplo:

O acesso a um elemento em uma matriz segue uma sintaxe similar a Linguagem C, ou seja, variável[linha,coluna] (variável[linha][coluna] em C). Aqui se estendem a forma de acesso dos vetores. Por exemplo, v <- A[2,] implica que a linha 2 da matriz A esta sendo atribuída ao vetor v. O mesmo pode ser feito em termos de colunas, isto é, v <- A[,2] indica que a coluna 2 esta sendo atribuída ao vetor v. Esta característica é bastante útil quando algortimos evolutivos são implementados. Aqui podemos também brincar com os acessos definindo faixas. Por exemplo, B <- A[1:5,], implica que as linhas de 1 a 5 serão alocadas à matriz B; B <- A[1:2,3:5] indica que B será uma matriz formada pelas linhas 1 e 2, e pelas colunas de 3 a 5 de A.

Todas as operações matriciais em R são element-wise, ou seja, elemento por elemento. Então, se A e B são matrizes, o resultado de A * B será cada elemento de A multiplicado pelo respectivo elemento de B. E assim são feitas as demais operações: soma, subtração e divisão.

As operações matriz operador escalar ou matriz operador vetor também são permitidas. A primeira será aplicada em todos os elemento da matriz. O segundo tipo de operação (matriz op vetor) é um pouco traiçoeira: se o tamanho do vetor for equivalente a quantidade de colunas a operação será feita em termos de colunas, caso contrário em termos de linhas. Porém, se ambos forem iguais (quantidade de linhas = quantidade de colunas), haverá a precedência para que a operação seja feita em termos de colunas. Caso seja impossível evitar que a quantidade de linhas e de colunas seja igual e que você precisa executar a operação em termos de linhas, então você terá que recorrer a função t(), que tem como objetivo fazer a transposta da matriz.

Outra solução possível para o caso linhas x colunas é a utilização da função sweep(), mas deixo esse pequeno trabalho de pesquisa para você leitor.

Agora, para realizar a multiplicação e a divisão tradicional de matrizes, utilizam-se os operadores %*% e %/%, respectivamente. Isso mesmo, porcentagem asterisco porcentagem e porcentagem barra porcentagem.

Agora que sabemos como manipular vetores e matrizes, vamos falar um pouco sobre indexação lógica, que permite acesso a elementos (vetores ou matrizes) através de condições lógicas. Esta é uma característica poderosa que permite que operações sejam feitas em diferentes elementos de uma matriz sem a necessidade da utilização de estruturas de laços. Imagine que temos uma matriz 6 x 6 de números aleatório entre 0 e 1, e que queremos determinar os índices de todos os números menores que 0.2, por exemplo. Com essas informações podemos realizar operações diretamente em todos esses elementos em que a condição é verdadeira. Se, por exemplo, essa matriz representar uma população, seria fácil determinar quais elementos estão fora do domínio permitido. Veja um exemplo logo a seguir, no qual a função runif(n) gera n números aleatórios entre 0 e 1:

No exemplo apresentado, atribuímos à variável idx o resultado da condição que queremos, ou seja, A < 0.2. Como se pode observar, o resultado da condição é uma matriz de elementos lógicos. Essa matriz pode ser utilizada como um gabarito que irá selecionar quais elementos serão substituídos pelo valor -1. Como a matriz resultante da condição é uma matriz lógica, a utilização da cláusula not (!) também é válida. Por exemplo, o código a seguir atribuiria o valor -1 a todos os elementos que não satisfazem a condição.

idx <- A < 0.2
A[!idx] <- -1

Essa mesma funcionalidade pode ser aplicada com vetores, ou seja, matriz condição vetor, ou até mesmo matriz condição matriz, tendo o cuidado de sempre assegurar que a quantidade de elementos a serem substituídos coincide com a quantidade de elementos sendo atribuídos. Isso é importante porque caso isso não seja feito, o R não irá mostrar uma mensagem de erro e sim apenas um warning após o término do programa, tornando a depuração mais complexa.

Aqui cabe uma informação importante. Vocês se lembram que diferentes elementos podem ser indexados através da função c()? Isso se estende às matrizes também. Vamos supor que queremos indexar um conjunto de linhas aleatoriamente selecionadas através da função sample() como a seguir:

idx <- sample(1:100, 5)
A[idx,] <- novos.individuos()

O trecho de código apresentado gera 5 números inteiros aleatórios na faixa de 1 a 100. Em seguida esses números são usados como índices para geração de novos indivíduos, por exemplo. Mas, e se quisermos fazer o contrário, isto é, criar novos indivíduos nos outros índices? Seria A[!idx]? Não, isso não funcionaria porque idx não é um vetor ou uma matriz lógica. Para solucionar esse problema seria necessário indexar as linhas através de A[-idx].

Listas

Listas podem armazenar objetos de diferentes tipos. Assim, uma função pode devolver mais de um objeto ao mesmo tempo. Isso é particularmente importante na implementação de evolutivos porque é comum uma função ter que devolver uma população (matriz) e as respectivas aptidões (vetor). Claro, se o programador não quiser devolver tudo como uma matriz só, combinando a população com os seus respectivos fitnesses usando a função cbind() (combinação de colunas). Eu particularmente prefiro retornar essas estruturas de forma separada usando listas.

O acesso aos elementos de uma lista é parecido ao que se faz em um vetor. Porém, deve-se usar colchetes duplos. Vejamos no exemplo a seguir, no qual são criados dois vetores e uma matriz, sendo todos atribuídos à lista MyList.

Uma característica interessante das listas é que você pode atribuir nomes aos objetos que compõem a lista. Essa característica tende a tornar os programas em R mais legíveis e fáceis de entender. Os nomes são dados durante a chamada a função list e muda a sintaxe de acesso aos objetos, exigindo a utilização do caractere ‘$’ seguido pelo nome do objeto como mostrado no exemplo a seguir.

Muito embora a atribuição de nomes aos objetos que compõem uma lista torne o programa mais legível, o padrão de acesso usando colchetes duplos continua válido.

Funções

A criação de funções é um elemento crucial em qualquer linguagem de programação, sejam procedimentos em linguagens procedurais ou métodos em linguagens orientadas a objetos. Em R, a sintaxe para criação de funções é nome_da_função <- function(parâmetros), isto é, o nome da função que recebe a palavra reservada function seguido dos parâmetros entre parênteses.

Se a função precisar de mais de uma instrução então ela deve ser limitada por chaves, da mesma forma como ocorre com funções em C ou métodos em Java, sendo que a última instrução deve ser uma função return() garantindo que a função retorne algum valor.

Um detalhe que deve ser lembrado é que as funções em R devem ser carregadas na memória antes de serem usadas. Isso pode ser feito através do botão source que fica no canto superior esquerdo do código ou através da função source(‘nome_do_arquivo.R’) no código. Outro detalhe é que um único arquivo pode contar várias funções. Usar um arquivo por função ou várias funções em um arquivo é uma questão de gosto do programador. Eu gosto de usar uma função por arquivo, pois é mais fácil de debugar. No caso de ter que implementar várias funções, costumo agrupá-las no mesmo arquivo de acordo com seus propósitos, por exemplo, mutacao.R contém todas as funções de mutação que podem ser utilizadas.

Vamos voltar um pouco para as listas, quando queremos que uma função retorne diferentes estruturas é necessário a utilização da função list(). A sintaxe desse tipo de retorno é:

return(list(nome1 = estrutura1, nome2 = estrutura2, ... ))

No caso de programação de algoritmos evolutivos costumo utilizar algo do tipo return(list(population = pop, fitness = fit)), na qual pop é uma matriz e fit é um vetor. Para acessar essas estruturas utiliza-se a sintaxe variável$nome, por exemplo, new.pop$population para acessar a população e new.pop$fitness para acessar o vetor de fitness retornados pela minha função.

No console do RStudio é possível também criar uma função, basta seguir a sintaxe. Quando o programador abre as chaves o console espera até que a mesma seja fechada. Quando isso ocorre a função é carregada automaticamente para a memória.

Funções de Grupo

Os códigos em R que usam laços (for, while) são mais lentos do que os códigos que usam funções de grupo, pois esse tipo de função normalmente é otimizada, sendo muitas vezes implementada em linguagem C. Então é altamente recomendável tentar achar uma solução para seu problema que não use laços.

De maneira geral, as funções de grupo atuam tanto em matrizes quanto em vetores. Se o parâmetro de entrada é um vetor, o retorno normalmente é um escalar. Quando o parâmetro de entrada é uma matriz, normalmente o retorno é um vetor, mas pode ser também um escalar, depende da função. Alguns exemplos de funções de grupo são: sum(), mean() e sd(). Essas funções de exemplo devolvem escalares mesmo quando matrizes são utilizadas, pois aplicam a operação soma (sum), média (mean) e desvio padrão (sd) em todos os seus elementos. Caso o programador queira, por exemplo, somar em termos de colunas ou linhas deve usar as funções colSums() ou rowSums() como mostrado a seguir. Sim, o ‘S’ no meio das funções é maiúsculo.

Como pudemos observar, as funções colSums() e rowSums() devolvem a soma de colunas e de linhas, respectivamente. Isso evita a utilização de laços para essa operação. Ai você pode se perguntar, e se eu tiver que aplicar uma função que eu desenvolvi em linhas ou colunas de uma matriz? Bom, para isso existe a função apply cuja sintaxe é apply(object, margin, function, parameters), na qual object é a matriz que será trabalhada, margin indica se a operação será em termos de linhas (1) ou colunas (2), function é a função que será usada na matriz e parameters são os parâmetros que sua função precisa.

Mas por que você esta passando um object e não uma matrix?

Porque a função apply() pode ser utilizada também por uma estrutura de dados em R chamada de data.frame, mas isso é outra história. :)

Vamos a utilização da função apply. Vamos supor que nós temos uma população de 10 indivíduos com uma dimensão 5, ou seja, uma matriz 10 x 5, e desejamos calcular a aptidão dessa população usando a função Schwefel, por exemplo. O código para essa operação é mostrado a seguir.

Observe como nesse pequeno exemplo utilizamos vários conceitos vistos neste artigo: matrizes, vetores (sim runif(50) gera um vetor de 50 posições de números aleatórios), funções e funções de grupo.

Com este último exemplo chegamos ao fim. Com este artigo eu espero duas coisas: que você se sinta incentivado a programar em R e que seja um primeiro passo no mundo dos algoritmos evolutivos. Se quiser mais detalhes de como implementar um Algoritmo Genético em R sugiro o artigo: Implementando Algoritmos Genéticos em R de minha autoria. Também é possível ver um tutorial de aplicações de Algoritmos Genéticos em Otimização Numérica na Revista Brasileira de Computação Aplicada.

Neste artigo também não introduzimos a utilização de estruturas de controle, pois assumimos que o leitor já tem pelo menos um conhecimento básico em programação. Além disso, essas estruturas são muito parecidas em outras linguagens como C, Matlab ou Java, com pequenas diferenças em suas sintaxes.

Para saber mais sobre R…

Este livro é bem geral fornecendo um overview bem amplo dos recursos que a linguagem R possui, partindo de sua instalação, passando pelas suas estruturas de dados, estruturas de controle, criação de gráficos, manipulação de dados e estatística.

Embora este livro tenha uma introdução ao R mostrando seus conceitos básicos e estruturas de dados, este livro é mais direcionado aos que pretendem utilizar a linguagem como ferramenta estatística.

--

--