Algorítimos Coevolucionários
Inteligencia Computacional tem sido usada como ferramenta de apoio para outras áreas da computação. O objetivo desse Post é oferecer uma VISÃO GERAL a respeito de Computação evolucionária. Vou tentar compartilhar um pouco do que estudei para usarmos como meio no desenvolvimento de melhores ferramentas de segurança da informação.
Uma das áreas mais fascinantes de Inteligencia Computacional certamente é a Computação Natural. A Computação Natural representa uma série de técnicas computacionais inspiradas em mecanismos encontrados na natureza. Esses mecanismos naturais vão desde o funcionamento de neurônios ao comportamento coletivo de organismos. As principais técnicas estudadas em computação natural são: Redes Neurais Artificiais, Algoritmos Evolucionários, Inteligência por Enxames e Sistemas Imunológicos Artificiais.
Nesse artigo vamos falar sobre Algoritmos Evolucionários, mais especificamente sobre algorítimos que usam mecanismos de Coevolução. Os algorítimos evolucionários são meta-heurísticas que imitam o mecanismo natural de evolução biológica proposto por Charles Darwin nas décadas de 60. Tal classe de algoritmos é geralmente usada para resolver problemas que não se conhece uma solução em tempo polinomial (NP-completos). Os Algoritmos Evolucionários são baseados em populações de indivíduos que evoluem ao decorrer do tempo. Cada indivíduo representa uma solução para o problema tratado.
Assim como na natureza, os Algorítimos Evolucionários possuem três operadores que trabalham sistematicamente:
- Seleção de indivíduos mais aptos para passar as suas características para as próximas gerações;
- Cruzamento de indivíduos aptos para formação de um novo indivíduo;
- Mutação dos novos indivíduos que garante diversidade na nova população.
Esses operadores são executados, repetidamente na população resultando em uma nova população que melhor se adapta ao ambiente. E assim o problema é resolvido iterativamente. Uma população de um algoritmo evolucionário qualquer e seus operadores podem ser observados na Figura 1.
Na Figura 1 mostramos três etapas de evolução numa população. Na primeira etapa os indivíduos mais e menos aptos selecionados, na segunda etapa o pior indivíduo é eliminado e os indivíduos mais aptos realizam um cruzamento retornando um novo indivíduo que corresponde a composição das duas melhores soluções e na terceira etapa o novo indivíduo sofre uma mutação gerando uma solução completamente diferente e garantindo uma diversidade na população. Nesse exemplo mostramos, de uma forma simplista, o funcionamento básico de uma técnica específica de Algoritmos Evolucionários que são os Algoritmos Genéticos.
Um exemplo de aplicação para Algoritmos Genéticos está em achar soluções para problemas de otimização de funções contínuas. Exemplo é a função Rosenbrock exibida na Figura 2.
Figura 2. Função Rosenbrock
É possível aplicar Algorítimos genéticos para achar o ponto mínimo dessa função que se encontra na coordenada (0,0). Cada indivíduo do algoritmo seria representado por um par ordenado (x,y) e a sua função de aptidão seria a função Rosenbrock. No inicio do problema os indivíduos seriam espalhados aleatoriamente pelo espaço de busca. Após o fim de uma determinada quantidade de gerações, perceberíamos que os indivíduos dessa nova geração estariam juntos próximo ao ponto (0,0), indicando qual a melhor solução para o problema de minimização da função Rosenbrock. Um exemplo da evolução do algoritmo é mostrado na Figura 3, onde os pontos pretos são os indivíduos do problema.
Algoritmo Genético é a técnica de Computação Evolucionária mais popular e possui diversas variações. Bom, o que foi mostrado é o BÁSICO de apenas uma técnica do mundo que é algorítimos evolucionários. Para mais detalhes sobre algorítimos genéticos ler os livros [1][2]. Esses fundamentos são essenciais para compreensão dos mecanismos de coevolução mostrados mais adiante.
Ao algorítimos evolucionários podemos adicionar características cooperativas e ai chegamos aos algoritmos co-evolucionários. Mas … o que é Coevolução ? É o processo que envolve a relação entre a evolução de duas ou mais entidades. Essa entidade pode ser uma população, um organismo vivo qualquer ou diferentes algoritmos computacionais inteligentes. Um exemplo de coevolução, encontrado na natureza da flor e do inseto, as flores precisam dos insetos para sua reprodução e os insetos precisam das flores para sua alimentação. Com isso surge um ciclo simbiótico que ambas espécimes são beneficiadas e evoluem em paralelo. No caso de uma coevolução cooperativa, ao decorrer dos anos essas especies se adaptam e evoluem para que o processo simbiótico seja fortalecido. Um exemplo disso são as orquídeas abelhas (Figura 4) que possuem o seu gineceu em forma de abelha fêmea, atraindo os machos e facilitando o processo de polinização.
Figura 4. Orquídea Abelha e seu Gineceu em forma de abelha fêmea
Esse mecanismo de coevolução entre entidades pode ser de duas naturezas: competitiva e cooperativa. Para a coevolução competitiva, temos duas sub-classes:
- Competição: onde ambas as espécies são inibidas e a evolução emerge da competição;
- Amensalismo: onde apenas uma espécie é inibida e existe uma relação de espécie ameaçadora e espécie ameaçada;
Na evolução cooperativa (ou simbiótica) temos três sub-classes:
- Mutualismo: onde ambas as espécies são beneficiadas. A melhoria em uma das espécies serve como feedback positivo para a melhoria da outra espécie;
- Comensalismo: onde apenas uma das espécies é beneficiada e a outra não é afetada;
- Parasitismo: onde apenas uma das espécies é beneficiada e a outra é penalizada;
Nesse artigo vamos focar em coevolução por competição (CCE – Competitive Coevolution) mostrando o funcionamento de um algoritmo CCE genérico.
Diferentemente de abordagens evolucionárias como o algorítimo genético, o CCE avalia a aptidão através de uma função de aptidão relativa. Isso significa que a aptidão de cada indivíduo de uma população será avaliada em relação a população concorrente.
A avaliação dessa função de aptidão relativa significa uma complexidade a mais em relação a um algoritmo evolucionário convencional. Além da avaliação da função objetivo que está sendo otimizada é necessário uma comparação direta de cada indivíduo com os indivíduos da população concorrente. Para amenizar o impacto computacional de tal comparação, uma seleção dos indivíduos da população concorrente é realizada. Existem algumas estratégias de seleção que ajuda no calculo da aptidão relativa:
- Seleção todos versus todos: cada indivíduo em uma população é comparado com todos os indivíduos da população concorrente;
- Seleção aleatória: cada indivíduo de uma população é testado com um grupo selecionado aleatoriamente da população concorrente;
- Seleção por torneio: cada indivíduo de uma população é comparado com os indivíduos mais aptos relativamente de outra população;
- Seleção de todos versus os melhores: cada indivíduo de uma população é testado com os melhores indivíduos (função objetivo) na população concorrente;
- Seleção compartilhada: cada indivíduo de uma população é testado com os melhores indivíduos em relação a aptidão compartilhada máxima (será comentado mais na frente).
Existem também abordagens diferentes para o cálculo da aptidão relativa de cada indivíduo em uma população. As principais estratégias são:
- Aptidão Simples: para cada indivíduo da população, é realizado um somatório simples do número de indivíduos na população concorrente que são menos aptos;
- Aptidão Compartilhada: Essa aptidão é calculada levando em consideração similaridades entre os indivíduos da população. A aptidão simples de um indivíduo em uma população é dividido pela soma da aptidão dos indivíduos similares a ele. Isso causa um efeito de reforço para os indivíduos “diferentes” na população;
- Aptidão por Torneio: faz uso de uma estrutura de dados em forma de árvore para organizar um torneio entre os indivíduos, onde o indivíduo mais apto é colocado na raiz. Para cada nível da arvore dois indivíduos (escolhidos aleatoriamente) são posicionados e apenas o mais apto irá passar para o próximo nível. Após a formação da árvore um método de seleção qualquer é utilizado e uma aptidão simples é calculada.
Assim como a maioria dos algorítimos evolucionários o CCE possui problemas com elitismo. Uma das soluções mais eficazes é a chamada Hall da Fama, que consiste em criar uma área de tamanho limitado para proteger os indivíduos mais aptos da população (melhores soluções). Para cada geração o melhor indivíduo da iteração é armazenado no hall da fama da população. Caso o hall da fama esteja lotado o pior individuo é substituído.
Um pseudo-código do CCE com duas populações pode ser observado na Figura 5. Em cada iteração um algoritmo um algoritmo evolucionário qualquer como PSO ou algoritmo genético é usado para a fase de evolução de cada população.
Mas para que essa teoria toda ?! Tudo culpa dos meus amigos Julios (Fort e Auto) : ]. Eu havia lido na página do Júlio Auto sobre um de seus projetos chamado “On Code Normalization” [3]. Esse projeto consiste em levantar hipóteses para um dos desafios propostos em 2006 pelo Halvar Flake na Black Hat Vegas. O desafio consiste em basicamente conseguir produzir uma forma normal, de forma automática, para dois códigos sintaticamente diferentes mas com a mesma semântica. Fui catar o vídeo da apresentação e achei uma série de desafios interessantes [4], alguns parcialmente resolvidos e outros ainda sem solução. Um desses desafios me chamou muita atenção por o Halvar propor uma solução usando inteligencia computacional e eu achar que essa proposição realmente faz sentido e vale a pena experimentar. O desafio foi o seguinte: “Automating input craft for a path through the executable“. Esse desafio consiste em dado dois pontos “A” e “B” em um determinado programa determinar de forma automática uma configuração de entrada no ponto “A” que garanta a execução do código localizado no ponto “B”.
Vamos modelar esse problema em forma de um algorítimo evolucionário. Como seria modelado o indivíduo da nossa população? Esse indivíduo seria composto pela coleção de porções de memória modificáveis (variáveis, registradores, estruturas) pertencentes ao ambiente “A” e a combinação de todos os possíveis valores que essas podem assumir. Como seria medido a qualidade de uma solução? O fluxo do software pode ser representado por um grafo (ou múltiplos grafos), podemos medir a distancia do ponto B para um ponto C (alcançado por uma entrada qualquer) através de uma busca pelo menor caminho entre esses dois pontos. Com isso transformamos nosso problema de encontrar entradas que nos levam de um ponto a outro no software em um problema de otimização (minimização) em espaço discreto e que algorítimos evolucionários resolvem MUITO BEM.
Ai que entra o outro culpado, Júlio Fort, que me chamou para colaborar para uCon Security Conference III [5]. Apesar da necessidade de uma investigação rigorosa, estou otimista quanto aos possíveis resultados. Por isso, pensei em propor falar algo nessa linha e apresentar resultados que conseguir produzir daqui para o dia da conferência. A ideia é mostrar algo bem prático, com exemplos didáticos e provas de conceito. Em breve escreverei em mais detalhes a respeito dessa investigação por aqui.
- Mutação:










