Escolar Documentos
Profissional Documentos
Cultura Documentos
Uma enfermeira não pode trabalhar mais de cinco turnos por semana.
O número de enfermeiras por turno em seu departamento deve estar dentro dos seguintes limites:
Além disso, cada enfermeira pode ter preferências de turno. Por exemplo, uma enfermeira prefere trabalhar apenas no turno
da manhã, outra enfermeira prefere não trabalhar no turno da tarde e assim por diante.
Esta tarefa é um exemplo do problema de agendamento de enfermagem (NSP), que pode ter muitas variantes. Possíveis
variações podem incluir diferentes especialidades para diferentes enfermeiras, a capacidade de trabalhar em turnos de
cobertura (horas extras) ou até mesmo diferentes tipos de turnos – como turnos de 8 horas e turnos de 12 horas.
Até agora, provavelmente parece uma boa ideia escrever um programa que fará o agendamento para você. Por que não
aplicar nosso conhecimento de algoritmos genéticos para implementar tal programa? Como de costume,
começaremos representando a solução do problema.
Representação da solução
Para resolver o problema de agendamento de enfermagem, decidimos usar uma lista binária (ou array) para representar o
agendamento, pois será intuitivo para nós interpretarmos, e vimos que os algoritmos genéticos podem lidar naturalmente com
essa representação.
Para cada enfermeiro, podemos ter uma string binária representando os 21 turnos da semana. Um valor de 1 representa um
turno em que a enfermeira está agendada para trabalhar. Por exemplo, dê uma olhada na seguinte lista binária:
(0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0)
[ 137 ]
Machine Translated by Google
Isso pode ser dividido nos seguintes grupos de três valores, representando os turnos que esta
enfermeira trabalhará em cada dia da semana:
As agendas de todas as enfermeiras podem ser então concatenadas para criar uma longa lista binária
que representa toda a solução.
Ao avaliar uma solução, essa longa lista pode ser dividida nos horários de cada enfermeira e as
violações das restrições podem ser verificadas. O exemplo de horário de enfermeira anterior, por
exemplo, contém duas ocorrências de valores 1 consecutivos que representam turnos consecutivos sendo
trabalhados (tarde seguida de noite e noite seguida de manhã). O número de turnos semanais dessa
mesma enfermeira pode ser calculado somando os valores binários da lista, o que resulta em 8 turnos.
Também podemos verificar facilmente a adesão às preferências de turno comparando os turnos de cada
dia com os turnos preferidos da enfermeira.
Finalmente, para verificar as restrições do número de enfermeiros por turno, podemos somar os
horários semanais de todos os enfermeiros e procurar entradas maiores que o máximo permitido
ou menores que o mínimo permitido.
Mas antes de continuarmos com nossa implementação, precisamos discutir a diferença entre
restrições rígidas e restrições flexíveis.
As preferências dos enfermeiros, por outro lado, podem ser consideradas restrições suaves.
Gostaríamos de aderir a eles tanto quanto possível, e uma solução que não contém nenhuma
violação ou menos violações dessas restrições é considerada melhor do que uma que contém mais
violações. Mas uma violação dessas restrições não invalida a solução.
[ 138 ]
Machine Translated by Google
No caso do problema N-Queens, todas as restrições – linha, coluna e diagonal – eram restrições rígidas. Se não
tivéssemos encontrado uma solução onde o número de violações fosse zero, não teríamos uma solução válida
para o problema. Aqui, por outro lado, enquanto as regras do hospital são consideradas restrições
rígidas, as preferências das enfermeiras são restrições suaves.
Portanto, estamos realmente procurando uma solução que não viole nenhuma das regras do hospital e, ao
mesmo tempo, minimize o número de violações às preferências dos enfermeiros.
Embora lidar com restrições flexíveis seja semelhante ao que fazemos em qualquer problema de otimização,
ou seja, nos esforçamos para minimizá-las, como lidamos com as restrições rígidas que as
acompanham? Existem várias estratégias possíveis:
No nosso caso, optamos por aplicar a quarta abordagem e penalizar as violações das restrições hard em
maior grau do que as das soft constraints. Isso foi feito criando uma função de custo, em que o custo de uma
violação de restrição rígida é maior do que o de uma violação de restrição flexível. O custo total é então
usado como a função de aptidão a ser minimizada. Isso é implementado na representação do problema que
será discutida na próxima subseção.
[ 139 ]
Machine Translated by Google
# lista de enfermeiros:
self.enfermeiras = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
# preferências de turno dos respectivos enfermeiros - manhã, tarde, noite: self.shiftPreference = [[1, 0, 0], [1,
1, 0], [0, 1, 1], [0, 1, 0], [0, 0, 1], [1, 1, 1], [0, 1, 1], [1, 1, 1]]
# número mínimo e máximo de enfermeiros permitidos para cada turno - manhã, tarde, noite: self.shiftMin = [2, 2, 1]
self.shiftMax = [3, 4, 2]
A classe usa o seguinte método para converter o cronograma fornecido em um dicionário com um
cronograma separado para cada enfermeira:
getNurseShifts(horário)
countConsecutiveShiftViolations(nurseShiftsDict)
countShiftsPerWeekViolations(nurseShiftsDict)
countNursesPerShiftViolations(nurseShiftsDict)
countShiftPreferenceViolations(nurseShiftsDict)
[ 140 ]
Machine Translated by Google
O método principal da classe exercita os métodos da classe criando uma instância do problema de
agendamento de enfermagem e testando uma solução gerada aleatoriamente para ele. A saída
resultante pode ter a seguinte aparência, com o valor de hardConstraintPenalty definido como 10:
Solução Aleatória =
[1 0 0 0 0 1 1 1 0 0 0 1 1 0 0 0 1 0 1 0 1 1 0 1 0 1 1 1 0 1 0 1 0 1 1 1 1 0 0 1 0 1 0 0 1 0 1 1 0 1 1 0 1 1 0 1 1 1 1 0 1 0 1 0 1 0 1 1 0 1 0 1 1 1
1010001101111011011111010011011100000
0101000011000000000110011110000110110
0 1 0 1 1 1 0 0 0 0 0 0 0 1 1 1 0 0 1 1]
R: [1 0 0 0 0 1 1 1 0 0 0 1 1 0 0 0 1 0 1 0 1]
B: [1 0 1 0 1 1 1 0 1 0 1 0 1 1 1 1 0 0 1 0 1]
C: [0 0 1 0 1 1 0 1 1 0 1 1 0 1 1 1 1 0 1 0 1]
D: [0 1 0 1 1 0 1 0 1 1 1 1 0 1 0 0 0 1 1 0 1]
E : [1 1 1 0 1 1 0 1 1 1 1 1 0 1 0 0 1 1 0 1 1]
F: [1 0 0 0 0 0 0 1 0 1 0 0 0 0 1 1 0 0 0 0 0]
G: [0 0 0 0 1 1 0 0 1 1 1 1 0 0 0 0 1 1 0 1 1]
H: [0 0 1 0 1 1 1 0 0 0 0 0 0 0 1 1 1 0 0 1 1] violações de turno consecutivas = 40
[ 141 ]
Machine Translated by Google
Como fica evidente a partir desses resultados, uma solução gerada aleatoriamente provavelmente
produzirá um grande número de violações e, consequentemente, um grande valor de custo. Na
próxima subseção, tentamos minimizar o custo e eliminar todas as violações de restrição severa usando
uma solução baseada em algoritmo genético.
Como a representação da solução que escolhemos para este problema é uma lista (ou uma matriz) de
valores binários, pudemos usar a mesma abordagem genética que usamos para vários problemas que já
resolvemos, como o problema da mochila 0-1 que descrevemos no Capítulo 4, Otimização
Combinatória.
nsp = enfermeiras.NurseSchedulingProblem(HARD_CONSTRAINT_PENALTY)
3. Como a solução é representada por uma lista de valores 0 ou 1 , usamos as seguintes definições
de caixa de ferramentas para criar a população inicial:
[ 142 ]
Machine Translated by Google
4. A função de adequação real é definida para calcular o custo das várias violações em
o cronograma, representado por cada solução individual:
toolbox.register("avaliar", getCost)
5. Quanto aos operadores genéticos, usamos seleção de torneio com um tamanho de torneio de 2,
juntamente com cruzamento de dois pontos e mutação flip-bit, pois isso é adequado para listas
binárias:
nsp.printScheduleInfo(melhor)
POPULATION_SIZE = 300
P_CROSSOVER = 0,9
P_MUTATION = 0,1
MAX_GENERATIONS = 200
HALL_OF_FAME_SIZE = 30
Além disso, vamos começar definindo a penalidade por violação de restrições rígidas para um valor de 1, o que
torna o custo de violação de uma restrição rígida semelhante ao de violação de uma restrição flexível:
HARD_CONSTRAINT_PENALTY = 1
[ 143 ]
Machine Translated by Google
-- Horário =
Agenda para cada enfermeira:
A: [0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0]
B: [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0]
C: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
D: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
E : [0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0]
F: [0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0]
G: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1]
H: [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0] violações de turno consecutivas = 0
Isso pode parecer um bom resultado, pois acabamos com apenas três violações de restrição.
No entanto, uma delas é a violação do plantão semanal – a enfermeira B estava agendada com
seis plantões semanais – ultrapassando o máximo permitido de cinco. Isso é suficiente para tornar
toda a solução inaceitável.
Tentando eliminar esse tipo de violação, procedemos para aumentar o valor da penalidade de hard
constraint para 10:
HARD_CONSTRAINT_PENALTY = 10
-- Horário =
Agenda para cada enfermeira:
A: [0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
B: [1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0]
C: [0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1]
D: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
E : [0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
F: [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0]
[ 144 ]
Machine Translated by Google
G: [0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0]
H: [1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0] violações de turno consecutivas = 0
Mais uma vez, obtivemos três violações, mas, desta vez, foram todas violações de restrição suave, o
que torna esta solução válida.
O gráfico a seguir, que mostra o fitness mínimo e médio ao longo das gerações, indica que ao longo
das primeiras 40-50 gerações, o algoritmo foi capaz de eliminar todas as violações de restrição rígida
e, a partir daí, houve apenas pequenas melhorias incrementais, que ocorreram sempre que outra
restrição suave foi eliminada:
[ 145 ]
Machine Translated by Google
Parece que, no nosso caso, foi o suficiente para definir uma penalidade de dez vezes para violações
de restrição severa. Em outros problemas, valores mais altos podem ser necessários. Você é
encorajado a experimentar alterando as definições do problema, bem como as configurações do algoritmo genético.
O mesmo trade-off que acabamos de ver entre restrições flexíveis e rígidas desempenhará um papel na
próxima tarefa que assumimos – o problema de coloração de grafos.
Gráfico de
Petersen Fonte: https://commons.wikimedia.org/wiki/
File:Petersen1_tiny.svg Imagem de Leshabirukov. Licenciado sob Creative Commons 3.0: https://creativecommons.org/licenses/by-sa/3.0/deed.en
Os gráficos são objetos notavelmente úteis, pois podem representar e nos ajudar a pesquisar uma
enorme variedade de estruturas, padrões e relacionamentos da vida real, como redes sociais, layouts
de rede elétrica, estruturas de sites, composições linguísticas, redes de computadores, estruturas
atômicas, padrões de migração , e mais.
A tarefa de coloração do gráfico é usada para atribuir uma cor para cada nó no gráfico de forma que nenhum
par de nós conectados (adjacentes) compartilhe a mesma cor. Isso também é conhecido como a coloração
adequada do gráfico.
[ 146 ]
Machine Translated by Google
O diagrama a seguir mostra o mesmo gráfico de Petersen, mas desta vez colorido corretamente:
A atribuição de cores geralmente é acompanhada por um requisito de otimização – use o número mínimo
possível de cores. Por exemplo, o gráfico de Peterson pode ser colorido adequadamente usando três cores,
conforme demonstrado no diagrama anterior. Mas seria impossível colori-lo corretamente usando apenas duas
cores. Em termos de teoria dos grafos, isso significa que o número cromático desse grafo é três.
Por que nos importamos em colorir os nós do grafo? Aparentemente, muitos problemas da vida real podem ser
traduzidos em uma representação gráfica de forma que a coloração do gráfico represente uma solução; por
exemplo, agendar aulas para um aluno ou turnos para um funcionário pode ser traduzido em um gráfico, onde nós
adjacentes representam aulas ou turnos que causam um conflito. Tal conflito pode ser classes que caem ao mesmo tempo
ou turnos que são consecutivos (soa familiar?). Devido a este conflito, designar a mesma pessoa para ambas as aulas
(ou ambos os turnos) invalidará o horário. Se cada cor representa uma pessoa diferente, atribuir cores diferentes
a nós adjacentes resolverá os conflitos. O problema da N-Rainha que encontramos no início deste capítulo pode
ser representado como um problema de coloração de grafos, em que cada nó no grafo representa um quadrado no
tabuleiro de xadrez e cada par de nós que compartilham uma linha, uma coluna ou um diagonal é conectada por
uma aresta.
Outros exemplos relevantes incluem atribuições de frequência para estações de rádio, planejamento de
redundância da rede elétrica, temporização de semáforos e até mesmo resolução de quebra-cabeças Sudoku.
Felizmente, isso o convenceu de que a coloração de gráficos é um problema que vale a pena resolver. Como
sempre, começaremos formulando uma representação apropriada de uma possível solução para este problema.
[ 147 ]
Machine Translated by Google
Representação da solução
Expandindo a representação de lista binária (ou array) comumente usada, podemos empregar uma lista
de inteiros, onde cada inteiro representa uma cor única, enquanto cada elemento da lista
corresponde a um dos nós do grafo.
Por exemplo, como o grafo de Petersen tem 10 nós, podemos atribuir a cada nó um índice entre 0
e 9. Então, podemos representar a coloração dos nós desse gráfico usando uma lista de 10 elementos.
Por exemplo, vamos dar uma olhada no que temos nesta representação particular:
(0, 2, 1, 3, 1, 2, 0, 3, 3, 0)
Para avaliar a solução, precisamos iterar sobre cada par de nós adjacentes e verificar se eles
compartilham a mesma cor. Se o fizerem, isso é uma violação de coloração e procuramos minimizar o
número de violações a zero para obter a coloração adequada do gráfico.
No entanto, você deve se lembrar que também procuramos minimizar o número de cores usadas.
Se já conhecermos esse número, podemos usar tantos valores inteiros quanto o número conhecido
de cores. Mas e se não o fizermos? Uma maneira de fazer isso é começar com uma estimativa (ou
apenas um palpite) para o número de cores usadas. Se encontrarmos uma solução adequada usando
esse número, podemos reduzir o número e tentar novamente. Se nenhuma solução for encontrada,
podemos aumentar o número e tentar novamente até obter o menor número com o qual poderíamos
encontrar uma solução. No entanto, podemos chegar a esse número mais rapidamente usando restrições
flexíveis e rígidas, conforme descrito na próxima subseção.
[ 148 ]
Machine Translated by Google
Minimizar o número de cores usadas, no entanto, pode ser introduzido como uma restrição flexível.
Gostaríamos de minimizar esse número, mas não à custa de violar a restrição rígida.
Isso nos permitirá lançar o algoritmo com um número de cores maior do que nossa estimativa e deixar
o algoritmo minimizá-lo até – idealmente – atingir a contagem mínima real de cores.
Para implementar esta classe, utilizamos o pacote Python de código aberto NetworkX (https://
networkx.github.io), que permite, entre outras coisas, a criação, manipulação e desenho de
gráficos. O grafo que usamos como objeto do problema de coloração é uma instância da classe de
grafos NetworkX. Em vez de criar esse grafo do zero, podemos aproveitar os inúmeros grafos
preexistentes contidos nessa biblioteca, como o grafo de Petersen que vimos anteriormente.
O construtor da classe GraphColoringProblem aceita como parâmetro o gráfico a ser colorido. Além
disso, aceita o parâmetro hardConstraintPenalty , que representa o fator de penalidade para
uma violação de restrição rígida.
[ 149 ]
Machine Translated by Google
O construtor então cria uma lista dos nós do gráfico, bem como uma matriz de adjacência, que nos permite
descobrir rapidamente se quaisquer dois nós no gráfico são adjacentes:
A classe usa o seguinte método para calcular o número de violações de cores no arranjo de cores
fornecido:
getViolationsCount(colorArrangement)
O método a seguir é usado para calcular o número de cores usadas pelo arranjo de cores fornecido:
getNumberOfColors(colorArrangement)
O método principal da classe exercita os métodos da classe criando uma instância do grafo de
Petersen e testando um arranjo de cores gerado aleatoriamente para ela, contendo até cinco cores.
Além disso, define o valor de hardConstraintPenalty como 10:
Número de violações = 1
Custo = 15
Como essa solução aleatória específica usa cinco cores e causa uma violação de cor, o custo calculado é
15.
[150]
Machine Translated by Google
O gráfico para esta solução é o seguinte – você consegue identificar a única violação de coloração?
Na próxima subseção, aplicamos uma solução baseada em algoritmo genético na tentativa de eliminar
quaisquer violações de coloração e, ao mesmo tempo, minimizar o número de cores usadas.
Como a representação da solução que escolhemos para este problema é uma lista de números inteiros,
precisamos expandir um pouco a abordagem genética de usar uma lista binária.
1. O programa começa criando uma instância da classe GraphColoringProblem com o grafo NetworkX
desejado a ser resolvido – o familiar grafo de Petersen neste caso, e o valor desejado para
hardConstraintPenalty, que é definido pela constante HARD_CONSTRAINT_PENALTY :
gcp = graphs.GraphColoringProblem(nx.petersen_graph(),
HARD_CONSTRAINT_PENALTY)
[ 151 ]
Machine Translated by Google
3. Como a solução é representada por uma lista de valores inteiros que representam o
cores participantes, precisamos definir um gerador aleatório que crie um inteiro entre 0 e o
número de cores menos 1. Esse inteiro aleatório representa uma das cores participantes. Em
seguida, definimos um criador de solução (individual) que gera uma lista desses números
inteiros aleatórios que correspondem ao comprimento do gráfico fornecido – é assim que
atribuímos aleatoriamente uma cor para cada nó no gráfico. Por fim, definimos o operador
que cria toda uma população de indivíduos:
toolbox.register("avaliar", getCost)
5. Quanto aos operadores genéticos, ainda podemos usar as mesmas operações de seleção
e cruzamento que usamos para listas binárias; no entanto, a operação de mutação precisa
mudar. A mutação flip bit usada para listas binárias alterna entre os valores de 0 e 1,
enquanto aqui precisamos alterar um determinado inteiro para outro inteiro gerado
aleatoriamente no intervalo permitido. O operador mutUniformInt faz exatamente isso – só
precisamos definir o intervalo semelhante ao que fizemos com o operador inteiro anterior:
[ 152 ]
Machine Translated by Google
6. Continuamos usando a abordagem elitista, onde os membros do HOF – os melhores indivíduos atuais
– são sempre passados intocados para a próxima geração:
7. Quando o algoritmo é concluído, imprimimos os detalhes da melhor solução encontrada antes de plotar
os gráficos.
Além disso, precisamos definir a penalidade por violar restrições rígidas para um valor de 10 e o
número de cores para 10:
HARD_CONSTRAINT_PENALTY = 10
MAX_CORES = 10
número de cores = 3
Número de violações = 0
Custo = 3
Isso significa que o algoritmo foi capaz de encontrar uma coloração adequada para o gráfico usando três cores,
denotadas pelos inteiros 0, 5 e 6. Como mencionamos anteriormente, os valores inteiros reais não importam – é a
distinção entre eles que faz. Três é de fato o número cromático conhecido do grafo de Petersen.
[ 153 ]
Machine Translated by Google
Um gráfico do gráfico de Petersen devidamente colorido pelo programa usando três cores
O gráfico a seguir, que mostra a aptidão mínima e média ao longo das gerações, indica
que o algoritmo alcançou a solução rapidamente, pois o gráfico de Petersen é
relativamente pequeno:
[ 154 ]
Machine Translated by Google
Para tentar um grafo maior, vamos substituir o grafo de Petersen por um grafo de Mycielski de ordem 5.
Este gráfico contém 23 nós e 71 arestas e é conhecido por ter um número cromático de 5:
número de cores = 6
Número de violações = 0
Custo = 6
Como sabemos que o número cromático desse grafo é 5, essa não é a solução ótima, embora próxima. Como
podemos chegar lá? E se não soubéssemos de antemão o número cromático? Uma maneira de fazer isso é mudar
os parâmetros do algoritmo genético; por exemplo, aumentar o tamanho da população (e possivelmente o tamanho do
HOF) e/ou aumentar o número de gerações. Outra abordagem seria iniciar a mesma pesquisa novamente, mas com
um número reduzido de cores. Como o algoritmo encontrou uma solução com seis cores, vamos reduzir o número
máximo de cores para cinco e ver se o algoritmo ainda consegue encontrar uma solução válida:
MAX_CORES = 5
Por que o algoritmo encontraria uma solução de cinco cores agora se não encontrou uma em primeiro lugar? À
medida que diminuímos o número de cores de 10 para 5, o espaço de busca é consideravelmente reduzido –
neste caso, de 1023 para 523 (já que temos 23 nós no grafo) – e o algoritmo tem mais chances de encontrar
a solução ótima (s), mesmo com um curto prazo e um tamanho populacional limitado. Portanto, embora a primeira
execução do algoritmo possa nos aproximar da solução, pode ser uma boa prática continuar diminuindo o
número de cores até que o algoritmo não encontre uma solução melhor.
No nosso caso, quando iniciado com cinco cores, o algoritmo foi capaz de encontrar uma solução de cinco
cores com bastante facilidade:
número de cores = 5
Número de violações = 0
Custo = 5
[ 155 ]
Machine Translated by Google
Um gráfico do grafo de Mycielski devidamente colorido pelo programa usando cinco cores
Agora, se tentarmos diminuir o número máximo de cores para quatro, sempre teremos pelo menos
uma violação.
Resumo
Neste capítulo, você foi apresentado aos problemas de satisfação de restrições, um parente próximo
dos problemas de otimização combinatória estudados anteriormente. Em seguida, exploramos
três casos clássicos de satisfação de restrições – o problema N-Queen, o problema de agendamento
de enfermeiras e o problema de coloração de grafos. Para cada um desses problemas, seguimos o
processo agora conhecido de encontrar uma representação apropriada para uma solução,
criando uma classe que encapsula o problema e avalia uma determinada solução e criando uma
solução de algoritmo genético que utiliza essa classe. Acabamos com soluções válidas para os
problemas enquanto nos familiarizamos com o conceito de restrições rígidas versus restrições flexíveis.
[ 156 ]
Machine Translated by Google
Até agora, examinamos problemas de busca discreta consistindo de estados e transições de estado.
No próximo capítulo, estudaremos problemas de busca em um espaço contínuo para demonstrar a
versatilidade da abordagem dos algoritmos genéticos.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes
recursos:
Constraint Satisfaction Problems, do livro Artificial Intelligence with Python de Prateek Joshi,
janeiro de 2017
[ 157 ]
Machine Translated by Google
Otimização Contínua
6
Funções
Este capítulo descreve como problemas de otimização contínua do espaço de busca podem ser resolvidos
por algoritmos genéticos. Começaremos descrevendo os cromossomos e os operadores genéticos
comumente usados para algoritmos genéticos com populações baseadas em números reais e
examinaremos as ferramentas oferecidas pela estrutura DEAP para esse domínio. Em seguida,
cobriremos vários exemplos práticos de problemas de otimização de funções contínuas e suas soluções
baseadas em Python usando a estrutura DEAP. Isso inclui a otimização da função Eggholder, a
função de Himmelblau, bem como a otimização restrita da função de Simionescu. Ao longo do caminho,
aprenderemos como encontrar várias soluções usando nichos, compartilhamento e manipulação de
restrições.
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
Machine Translated by Google
matplotlib
nascido no mar
Os programas usados neste capítulo podem ser encontrados no repositório GitHub do livro em: https://
github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/tree/ master/Chapter06.
Além disso, mencionamos que, embora os vários métodos de seleção funcionem da mesma forma
para cromossomos inteiros ou reais, métodos especializados de cruzamento e mutação são
necessários para os cromossomos codificados reais. Esses operadores são geralmente aplicados em
uma base dimensão por dimensão, conforme ilustrado a seguir.
[ 159 ]
Machine Translated by Google
Suponha que temos dois cromossomos: [x1 , x2 , x3 ] e [y1 , y2 , 33 ]. Como a operação de cruzamento é
aplicada separadamente para cada dimensão, um descendente [o1 , o2 , o3 ] será criado da seguinte forma:
Da mesma forma, o operador de mutação será aplicado individualmente a cada dimensão, para que cada um
dos componentes o1 , o2 e o3 possam estar sujeitos a mutação.
O valor de ÿ é comumente definido como 0,5, resultando em um intervalo de seleção duas vezes
maior que o intervalo entre os pais.
Simulated Binary Crossover (SBX), onde os dois filhos são criados a partir dos dois pais usando
a seguinte fórmula, garantindo que a média dos valores dos filhos seja igual à dos valores
dos pais:
O valor de ÿ, também conhecido como fator de dispersão, é calculado usando uma combinação
de um valor escolhido aleatoriamente e um parâmetro pré-determinado conhecido como ÿ
(eta), índice de distribuição ou fator de aglomeração. Com valores maiores de ÿ, a prole
tenderá a ser mais parecida com seus pais. Valores comuns de ÿ estão entre 10 e 20.
Na próxima seção, veremos como cromossomos codificados reais e operadores genéticos são suportados
pela estrutura DEAP.
[ 160 ]
Machine Translated by Google
Para a codificação do cromossomo, podemos usar uma lista (ou array) de números de ponto flutuante.
Uma coisa a ter em mente, porém, é que os operadores genéticos existentes do DEAP não funcionarão
bem com objetos individuais que estendem a classe numpy.ndarray devido à maneira como esses
objetos estão sendo fatiados, bem como à maneira como estão sendo comparados a uns aos outros.
O uso de indivíduos baseados em numpy.ndarray exigirá a redefinição dos operadores genéticos de
acordo. Isso é abordado na documentação do DEAP, em Herdando do NumPy. Por esse motivo,
bem como por motivos de desempenho, as listas comuns do Python ou as matrizes de números de ponto
flutuante são geralmente preferidas ao usar o DEAP.
Quanto aos operadores genéticos codificados reais, a estrutura DEAP oferece várias implementações
prontas para uso, contidas nos módulos crossover e mutação:
Além disso, uma vez que a otimização de funções contínuas é normalmente feita em uma determinada
região delimitada em vez de em todo o espaço, o DEAP fornece alguns operadores que aceitam
parâmetros de contorno e garantem que os indivíduos resultantes residirão dentro desses contornos:
[ 161 ]
Machine Translated by Google
Na próxima seção, demonstraremos o uso de operadores limitados ao otimizar uma função clássica
de benchmark.
A função Eggholder
Fonte: https://en.wikipedia.org/wiki/File:Eggholder_function.pdf. Imagem de Gaortizg.
Licenciado sob Creative Commons CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/deed.en.
[ 162 ]
Machine Translated by Google
Geralmente é avaliado no espaço de busca limitado por [-512, 512] em cada dimensão.
x=512, y = 404,2319
https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/ blob/master/Chapter06/01-optimize-
eggholder.py
[ 163 ]
Machine Translated by Google
2. Como estamos lidando com números de ponto flutuante confinados por certos
limites, a seguir definimos uma função auxiliar que cria números de ponto flutuante
aleatórios, distribuídos uniformemente dentro do intervalo especificado:
Esta função assume que os limites superior e inferior são os mesmos para
todas as dimensões.
toolbox.register("avaliar", porta-ovos)
[ 164 ]
Machine Translated by Google
# Operadores genéticos:
toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUND_LOW, up=BOUND_UP,
eta=CROWDING_FACTOR) toolbox.register("mutate ",
tools.mutPolynomialBounded, baixo=BOUND_LOW, up=BOUND_UP,
eta=CROWDING_FACTOR, indpb=1.0/DIMENSIONS)
6. Como fizemos várias vezes, usamos nossa versão modificada do fluxo de algoritmo genético
simples do DEAP, onde adicionamos elitismo - mantendo os melhores indivíduos (membros
do hall da fama) e movendo-os para a próxima geração, intocados pela genética
operadores:
8. Além das constantes anteriores do algoritmo genético comum, agora precisamos de uma nova,
o fator de aglomeração (eta) que é usado pelas operações de cruzamento e mutação:
CROWDING_FACTOR = 20,0
[ 165 ]
Machine Translated by Google
Finalmente estamos prontos para executar o programa. Os resultados obtidos com essas configurações são
mostrados a seguir:
Se examinarmos o gráfico estatístico gerado pelo programa, mostrado a seguir, podemos dizer que o algoritmo
encontrou alguns valores de mínimos locais imediatamente e depois fez pequenas melhorias incrementais
até que finalmente encontrou os mínimos globais:
Uma área interessante está por volta da geração 180 - vamos explorá-la mais na próxima subseção.
[ 166 ]
Machine Translated by Google
Uma maneira de interpretar essa observação é que talvez a introdução de mais ruído possa levar a
melhores resultados mais rapidamente. Essa pode ser outra manifestação do conhecido princípio
de exploração versus exploração que discutimos várias vezes antes — aumentar a exploração
(que se manifesta como ruído no diagrama) pode nos ajudar a localizar o mínimo global mais
rapidamente. Uma maneira fácil de aumentar a medida de exploração é aumentar a probabilidade
de mutações. Esperançosamente, o uso do elitismo – mantendo os melhores resultados
intocados – nos impedirá de explorar demais, o que leva a um comportamento aleatório de pesquisa.
[ 167 ]
Machine Translated by Google
Para testar essa ideia, vamos aumentar a probabilidade de mutação de 0,1 para 0,5:
P_MUTAÇÃO = 0,5
Executando o programa modificado, encontramos novamente o mínimo global, mas muito mais rápido,
como fica evidente na saída, bem como no gráfico estatístico mostrado a seguir, onde a linha vermelha
(o melhor resultado) atinge o ótimo rapidamente, enquanto o pontuação média (verde) é mais ruidosa do
que antes e está mais distante do melhor resultado:
Estatísticas do programa otimizando a função Eggholder com uma probabilidade de mutação aumentada
Manteremos essa ideia em mente ao lidar com nossa próxima função de referência, conhecida como
função de Himmelblau.
[ 168 ]
Machine Translated by Google
Função de Himmelblau
Fonte: https://commons.wikimedia.org/wiki/File:Himmelblau_function.svg.
Imagem de Morn, o Gorn. Lançado ao domínio público.
Embora esta função pareça mais simples em comparação com a função Eggholder, ela chama a
atenção por ser multimodal, ou seja, possui mais de um mínimo global. Para ser exato, a função
tem quatro mínimos globais avaliados em 0, que podem ser encontrados nos seguintes locais:
x=3,0, y=2,0
x=ÿ2,805118, y=3,131312
x=ÿ3,779310, y=ÿ3,283186
x=3,584458, y=ÿ1,848126
[ 169 ]
Machine Translated by Google
Ao otimizar funções multimodais, muitas vezes estamos interessados em encontrar todas (ou a
maioria) as localizações mínimas. No entanto, vamos começar encontrando um, o que faremos na
próxima subseção.
https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/blob/master/Chapter06/02-optimize-
himmelblau.py _
O programa é semelhante ao que usamos para otimizar a função Eggholder, com algumas
diferenças destacadas a seguir:
[ 170 ]
Machine Translated by Google
3. Como a função que otimizamos possui vários mínimos, pode ser interessante observar a
distribuição das soluções encontradas ao final da execução. Portanto, adicionamos
um gráfico de dispersão contendo as localizações dos quatro mínimos globais e a população
final no mesmo plano xy :
plt.figure(1)
globalMinima = [[3.0, 2.0], [-2.805118, 3.131312], [-3.779310, -3.283186], [3.584458, -1.848126]]
plt.scatter(*zip(*globalMinima), marcador= 'X',
color='vermelho', zorder=1) plt.scatter(*zip(*população), marcador='.', color='azul', zorder=0)
Executando o programa, os resultados indicam que encontramos um dos quatro mínimos (x=3,0,
y=2,0):
A impressão dos membros do hall da fama sugere que todos representam a mesma solução:
...
[ 171 ]
Machine Translated by Google
O diagrama a seguir, ilustrando a distribuição de toda a população, confirma ainda mais que
os algoritmos genéticos convergiram para um dos mínimos das quatro funções - aquele
que reside em (x=3,0, y=2,0):
Gráfico de dispersão da população no final da primeira execução, ao lado dos mínimos das quatro funções
Além disso, é evidente que muitos dos indivíduos da população têm o componente x ou y do mínimo
que encontramos.
RANDOM_SEED = 42
random.seed(RANDOM_SEED)
Isso é feito para permitir a repetibilidade dos resultados; no entanto, na vida real, normalmente
usaremos diferentes valores de semente aleatórios para diferentes execuções, comentando essas
linhas ou definindo explicitamente a constante para valores diferentes.
[ 172 ]
Machine Translated by Google
Gráfico de dispersão da população ao final da segunda execução, ao lado dos mínimos das quatro funções
Se passarmos a alterar o valor da semente para 17, a execução do programa fornecerá a solução
(x=3,584458, y=ÿ1,848126), conforme ilustrado no diagrama a seguir:
Gráfico de dispersão da população ao final da terceira execução, ao lado dos mínimos das quatro funções
[ 173 ]
Machine Translated by Google
No entanto, e se quiséssemos encontrar todos os mínimos globais em uma única execução? Como veremos na próxima
subseção, os algoritmos genéticos nos oferecem uma maneira de perseguir esse objetivo.
Vamos tentar aplicar essa ideia ao processo de otimização de função de Himmelblau e ver se ele pode ajudar
a localizar todos os quatro mínimos em uma única execução. Essa tentativa é implementada no programa 03-
optimize-himmelblau-sharing.py , localizado em: https://github.com/PacktPublishing/
Hands-On-Genetic-Algorithms-with-Python/blob/master/Chapter06/03- optimize-himmelblau-sharing.py
O programa é baseado no anterior, mas tivemos que fazer algumas modificações importantes, descritas
a seguir:
def himmelblauInvertido(individual):
x = indivíduo[0] y =
indivíduo[1]
[ 174 ]
Machine Translated by Google
toolbox.register("avaliar", himmelblauInverted)
DISTANCE_THRESHOLD = 0,1
SHARING_EXTENT = 5,0
Essa função começa definindo os valores adaptativos originais de lado para que possam ser
recuperados posteriormente. Em seguida, itera sobre cada indivíduo e calcula um número,
sharingSum, pelo qual seu valor de aptidão será dividido. Esse valor de soma é acumulado
calculando a distância entre a localização do indivíduo atual e a localização de cada um dos
outros indivíduos da população. Se a distância for menor que o limite definido pela
constante DISTANCE_THRESHOLD , o seguinte valor é adicionado à soma acumulada:
[ 175 ]
Machine Translated by Google
Depois de recalcular o valor de aptidão para cada indivíduo, a seleção do torneio é realizada
usando os novos valores de aptidão:
plt.figure(2)
plt.scatter(*zip(*globalMaxima), marcador='x', color='vermelho', zorder=1)
Quando executamos este programa, os resultados não decepcionam. Examinando os membros do hall da
fama, parece que localizamos todos os quatro locais ótimos:
...
[ 176 ]
Machine Translated by Google
O diagrama a seguir, que ilustra a distribuição dos membros do hall da fama, confirma
ainda mais que:
Gráfico de dispersão das melhores soluções no final da corrida, ao lado dos mínimos das quatro funções, ao usar niching
Gráfico de dispersão da população no final da execução, ao lado dos mínimos das quatro funções, ao usar niching
[ 177 ]
Machine Translated by Google
Por mais impressionante que pareça, precisamos lembrar que o que fizemos aqui pode ser mais difícil de
implementar em situações da vida real. Por um lado, as modificações que adicionamos ao processo de
seleção aumentam a complexidade do cálculo e o tempo consumido pelo algoritmo. Além disso, o
tamanho da população geralmente precisa ser aumentado para cobrir suficientemente todas as áreas
de interesse. Os valores das constantes de compartilhamento podem ser difíceis de determinar em alguns casos,
por exemplo, se não soubermos com antecedência quão próximos os vários picos podem estar. No entanto,
sempre podemos usar essa técnica para localizar aproximadamente áreas de interesse e depois explorar cada
uma delas usando a versão padrão do algoritmo.
Uma abordagem alternativa para encontrar vários pontos ótimos se enquadra no domínio da otimização
restrita, que é o assunto da próxima seção.
A função é geralmente avaliada no espaço de busca limitado por [-1,25, 1,25] em cada dimensão e pode ser
expressa matematicamente da seguinte forma:
[ 178 ]
Machine Translated by Google
Essa restrição efetivamente limita os valores de x e y que são considerados válidos para essa
função. O resultado é representado no seguinte diagrama de contorno:
A borda em forma de flor é criada pela restrição, enquanto as cores dos contornos denotam o valor
real — vermelho para os valores mais altos e roxo para os mais baixos. Se não fosse pela restrição, os
pontos mínimos estariam em (1,25, -1,25) e (-1,25, 1,25).
No entanto, depois de aplicar a restrição, os mínimos globais da função estão localizados nos seguintes
locais:
x=0,84852813, y=–0,84852813
x=ÿ0,84852813, y=0,84852813
Estes representam as pontas das duas pétalas opostas contendo os contornos roxos. Ambos os
mínimos avaliam o valor de -0,072.
Na próxima subseção, tentaremos encontrar esses mínimos usando nossa abordagem de algoritmos
genéticos codificados reais.
[ 179 ]
Machine Translated by Google
A próxima abordagem é reparar qualquer solução candidata que viole uma restrição,
modificando-a para que não viole mais a(s) restrição(ões). Isso pode ser difícil de
implementar e, ao mesmo tempo, pode levar a uma perda significativa de informações.
Por fim, a abordagem que funcionou para nós no Capítulo 5, Satisfação de restrições, foi
penalizar soluções candidatas que violassem uma restrição degradando a pontuação
da solução e tornando-a menos desejável. Para problemas de busca, implementamos
essa abordagem criando uma função de custo que adiciona um custo fixo a cada violação
de restrição. Aqui, no caso do espaço contínuo, podemos usar uma penalidade fixa ou
aumentar a penalidade com base no grau em que a restrição foi violada.
[ 180 ]
Machine Translated by Google
https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/ blob/master/Chapter06/04-optimize-simionescu.py
O programa é muito semelhante ao primeiro que utilizamos neste capítulo, criado originalmente para
a função Eggholder, com as seguintes diferenças destacadas:
2. Além disso, uma nova constante determina uma penalidade fixa (ou custo) por violar o
limitação:
PENALTY_VALUE = 10,0
toolbox.register("avaliado",simionescu)
def viável(individual):
x = individual[0] y =
individual[1] retornar x**2 +
y**2 <= (1 + 0,2 * math.cos(8,0 * math.atan2(x, y)))**2
[ 181 ]
Machine Translated by Google
Agora o programa está pronto para usar! Os resultados indicam que realmente encontramos um dos
dois locais mínimos conhecidos:
A técnica de nicho que usamos para a função de Himmelblau às vezes é chamada de nicho
paralelo, pois tenta localizar várias soluções ao mesmo tempo. Como já mencionamos,
é propenso a várias desvantagens práticas. Niching serial (ou niching sequencial), por outro lado, é
um método usado para encontrar uma solução de cada vez. Para implementar o niching serial,
usamos o algoritmo genético como de costume e encontramos a melhor solução. Em seguida,
atualizamos a função de aptidão para que a área da(s) solução(ões) já encontrada(s) seja
penalizada, incentivando assim o algoritmo a explorar outras áreas do espaço do problema.
Isso pode ser repetido várias vezes até que nenhuma solução viável adicional seja encontrada.
[ 182 ]
Machine Translated by Google
Para encontrar o segundo mínimo para a função de Simionescu, criamos o programa Python 05-
optimize-simionescu-second.py , localizado em:
https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/ blob/master/Chapter06/05-optimize-
simionescu-second.py
1. Primeiro adicionamos uma constante que define o limite de distância das soluções encontradas
anteriormente - novas soluções mais próximas do que esse valor limite para qualquer uma das
antigas serão penalizadas:
DISTANCE_THRESHOLD = 0,1
Ao executar este programa, os resultados indicam que realmente encontramos o segundo mínimo:
Você é incentivado a adicionar esse ponto mínimo como outra restrição à função viável() e verificar se a
execução do programa novamente não encontra nenhum outro local de valor mínimo igual no espaço
de entrada.
[ 183 ]
Machine Translated by Google
Resumo
Neste capítulo, você foi apresentado aos problemas de otimização contínua do espaço de busca e como
eles podem ser representados e resolvidos usando algoritmos genéticos e, especificamente,
utilizando a estrutura DEAP. Em seguida, exploramos vários exemplos práticos de problemas de
otimização de funções contínuas — a função Eggholder, a função de Himmelblau e a função de
Simionescu — junto com suas soluções baseadas em Python. Além disso, abordamos abordagens
para encontrar várias soluções e lidar com restrições.
Nos próximos quatro capítulos do livro, demonstraremos como as várias técnicas que aprendemos
até agora neste livro podem ser aplicadas na solução de problemas relacionados ao
aprendizado de máquina e à inteligência artificial. O primeiro desses capítulos fornecerá uma visão
geral rápida do aprendizado supervisionado e demonstrará como os algoritmos genéticos
podem melhorar o resultado dos modelos de aprendizado selecionando as partes mais relevantes
do conjunto de dados fornecido.
Leitura adicional
Para obter mais informações, consulte os seguintes recursos:
[ 184 ]
Machine Translated by Google
3
Seção 3: Inteligência Artificial
Aplicações da Genética
Algoritmos
Esta seção se concentra no uso de algoritmos genéticos para melhorar os resultados obtidos com vários
algoritmos de aprendizado de máquina.
Começaremos este capítulo com uma rápida revisão do aprendizado de máquina supervisionado. Se você for um
cientista de dados experiente, sinta-se à vontade para pular as seções introdutórias.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
pandas
matplotlib
nascido no mar
Ao longo dos anos, muitos tipos de modelos de aprendizado de máquina foram desenvolvidos. Cada
modelo possui parâmetros internos particulares que podem afetar o mapeamento entre a entrada e a
saída, e os valores desses parâmetros podem ser ajustados, conforme ilustrado na imagem a
seguir:
[ 187 ]
Machine Translated by Google
Por exemplo, se o modelo implementasse uma árvore de decisão, poderia conter vários IF
THEN declarações, que podem ser formuladas da seguinte forma:
SE <valor de entrada> FOR MENOR ENTÃO <algum valor limite> ENTÃO <ir para algum ramo de destino>
Nesse caso, tanto o valor limite quanto a identidade do ramo de destino são parâmetros que podem ser ajustados
ou ajustados durante o processo de aprendizado.
Para ajustar os parâmetros internos, cada tipo de modelo possui um algoritmo de aprendizado que itera
sobre os valores de entrada e saída fornecidos e procura corresponder a saída fornecida para cada uma das
entradas fornecidas. Para atingir esse objetivo, um algoritmo de aprendizado típico medirá a diferença (ou erro)
entre a saída real e a saída desejada; o algoritmo então tentará minimizar esse erro ajustando os parâmetros
internos do modelo.
Os dois principais tipos de aprendizado de máquina supervisionado são classificação e regressão e serão
descritos nas subseções a seguir.
[ 188 ]
Machine Translated by Google
Classificação
Ao realizar uma tarefa de classificação, o modelo precisa decidir a qual categoria uma determinada entrada
pertence. Cada categoria é representada por uma única saída (chamada de rótulo), enquanto as entradas
são chamadas de recursos:
Em termos de saída, existem três rótulos: Iris setosa, Iris virginica e Iris versicolor.
Estes representam os três tipos diferentes de íris.
Quando os valores de entrada estão presentes, que representam as medidas que foram tiradas de
uma determinada flor de íris, esperamos que a saída do rótulo correto seja alta e as outras duas sejam
baixas:
[ 189 ]
Machine Translated by Google
Tarefas de classificação têm uma infinidade de aplicações da vida real, como aprovação de empréstimos
bancários e cartões de crédito, detecção de spam por e-mail, reconhecimento de dígitos manuscritos e
reconhecimento facial. Mais adiante neste capítulo, demonstraremos a classificação dos tipos de animais
usando o conjunto de dados Zoo.
O segundo tipo principal de aprendizado de máquina supervisionado, a regressão, será descrito na próxima
subseção.
Regressão
Ao contrário das tarefas de classificação, o modelo para tarefas de regressão mapeia os valores de entrada
em uma única saída para fornecer um valor contínuo, conforme ilustrado na imagem a seguir:
[ 190 ]
Machine Translated by Google
Dados os valores de entrada, espera-se que o modelo preveja o valor correto da saída.
Exemplos de regressão da vida real incluem prever o valor das ações, a qualidade do vinho ou o preço de
mercado de uma casa, conforme ilustrado na imagem a seguir:
Na imagem anterior, as entradas são recursos que fornecem informações que descrevem uma
determinada casa, enquanto a saída é o valor previsto da casa.
Existem muitos tipos de modelos para realizar tarefas de classificação e regressão – alguns deles são
descritos na subseção a seguir.
Árvores de Decisão: Uma família de algoritmos que utiliza um gráfico em forma de árvore,
onde os pontos de ramificação representam decisões e os ramos representam
suas consequências.
Random Forests: Algoritmos que criam um grande número de árvores de decisão durante a
fase de treinamento e usam uma combinação de suas saídas.
[ 191 ]
Machine Translated by Google
Support Vector Machines: Algoritmos que mapeiam as entradas fornecidas como pontos no
espaço, de modo que as entradas pertencentes a categorias separadas sejam divididas pelo
maior intervalo possível.
Redes Neurais Artificiais: Modelos que consistem em múltiplos nós simples, ou neurônios,
que podem ser interconectados de diversas formas. Cada conexão pode ter um peso que controla
o nível do sinal que é transportado de um neurônio para o
próximo.
Existem certas técnicas que podem ser usadas para melhorar e aumentar o desempenho de tais modelos.
Uma técnica interessante – seleção de recursos – será discutida na próxima seção.
A seleção de recursos é o processo de selecionar o conjunto de recursos mais benéfico e essencial de todo
o conjunto de recursos fornecido. Além de aumentar a precisão do modelo, uma seleção de recursos
bem-sucedida pode fornecer as seguintes vantagens:
Ao procurar métodos para realizar a seleção de recursos, os algoritmos genéticos são candidatos naturais.
Demonstraremos como eles podem ser aplicados para encontrar os melhores recursos de um conjunto de
dados gerado artificialmente na próxima seção.
[ 192 ]
Machine Translated by Google
As variáveis de entrada, x0..x4 , são independentes e uniformemente distribuídas no intervalo [0, 1]. O
último componente da fórmula é o ruído gerado aleatoriamente. O ruído é normalmente
distribuído e multiplicado pelo ruído constante, que determina seu nível.
Podemos aproveitar essa função para testar a eficácia dos algoritmos genéticos como um mecanismo
de seleção de recursos. Em nosso teste, usaremos a função make_friedman1() para criar um conjunto
de dados com 15 recursos e usaremos o algoritmo genético para procurar o subconjunto de recursos
que fornece o melhor desempenho. Como resultado, esperamos que o algoritmo genético escolha os
cinco primeiros recursos e descarte o restante, assumindo que a precisão do modelo é melhor quando
apenas os recursos relevantes são usados como entrada. A função de aptidão do algoritmo
genético utilizará um modelo de regressão que, para cada solução potencial – um subconjunto do
recurso a ser usado – será treinado usando o conjunto de dados contendo apenas os recursos selecionados.
[ 193 ]
Machine Translated by Google
Representação da solução
O objetivo do nosso algoritmo é encontrar um subconjunto de recursos que produza o melhor desempenho.
Portanto, uma solução precisa indicar quais recursos são escolhidos e quais são descartados.
Uma maneira óbvia de fazer isso é representar cada indivíduo usando uma lista de valores binários.
Cada entrada nessa lista corresponde a um dos recursos no conjunto de dados. Um valor de 1 representa a
seleção do recurso correspondente, enquanto um valor de 0 significa que o recurso não foi selecionado. Isso é
muito semelhante à abordagem que usamos no problema da mochila 0-1 que descrevemos no Capítulo 4,
Otimização Combinatória.
A presença de cada 0 na solução será traduzida na remoção da coluna de dados do recurso correspondente
do conjunto de dados, como veremos na próxima subseção.
[ 194 ]
Machine Translated by Google
self.regressor =
GradientBoostingRegressor(random_state=self.randomSeed)
Observe que estamos passando a semente aleatória para que ela possa ser usada
internamente pelo regressor. Dessa forma, podemos garantir que os resultados obtidos sejam
repetíveis.
4. O método getMSE() da classe é usado para determinar o desempenho de nosso modelo de regressão
de aumento de gradiente para um conjunto de recursos selecionados. Ele aceita uma lista de valores
binários correspondentes aos recursos no conjunto de dados - um valor de 1 representa a
seleção do recurso correspondente, enquanto um valor de 0 significa que o recurso foi descartado. O
método exclui as colunas nos conjuntos de treinamento e validação que correspondem aos
recursos não selecionados:
[ 195 ]
Machine Translated by Google
A métrica usada aqui para avaliar o regressor é chamada de erro quadrático médio
(MSE), que encontra a diferença média quadrada entre os valores previstos do modelo
e os valores reais. Um valor menor dessa medida indica melhor desempenho do
regressor.
6. O método main() da classe cria uma instância da classe Friedman1Test com 15 recursos.
Em seguida, ele usa repetidamente o método getMSE() para avaliar o desempenho do
regressor com os primeiros n recursos, enquanto n é incrementado de 1 a 15:
Ao executar o método main, os resultados mostram que, à medida que adicionamos os cinco
primeiros recursos um a um, o desempenho melhora. No entanto, posteriormente, cada recurso
adicional degrada o desempenho do regressor:
1 primeiros recursos: pontuação = 47,553993
2 primeiros recursos: pontuação = 26,121143
3 primeiros recursos: pontuação = 18,509415
4 primeiros recursos: pontuação = 7,322589
5 primeiros recursos: pontuação = 6,702669
6 primeiros recursos: pontuação = 7,677197
7 primeiros recursos: pontuação = 11,614536
8 primeiros recursos: pontuação = 11,294010
9 primeiros recursos: pontuação = 10,858028
10 primeiros recursos: pontuação = 11,602919
11 primeiros recursos: pontuação = 15,017591
12 primeiros recursos: pontuação = 14,258221
13 primeiros recursos: pontuação = 15,274851
14 primeiros recursos: pontuação = 15,726690
15 primeiros recursos: pontuação = 17,187479
[ 196 ]
Machine Translated by Google
Isso é ilustrado ainda mais pelo gráfico gerado, mostrando o valor MSE mínimo onde os cinco primeiros
recursos são usados:
Na próxima subseção, descobriremos se um algoritmo genético pode identificar com sucesso esses cinco
primeiros recursos.
Como lembrete, a representação do cromossomo que está sendo usada aqui é uma lista de números
inteiros com os valores 0 ou 1, denotando se um recurso deve ser usado ou descartado. Isso torna
nosso problema, do ponto de vista do algoritmo genético, semelhante ao problema OneMax ou ao
problema da mochila 0-1 que resolvemos anteriormente. A diferença está na função de aptidão que
retorna o MSE do modelo de regressão, que é calculado dentro da classe
Friedman1Test .
[ 197 ]
Machine Translated by Google
toolbox.register("avaliar", fritomanTestScore)
5. Quanto aos operadores genéticos, usamos seleção por torneio com um tamanho de torneio de
2 e operadores de cruzamento e mutação especializados para cromossomos de lista
binária:
[ 198 ]
Machine Translated by Google
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats,
halloffame=hof,
verbose=True)
Ao executar o algoritmo por 30 gerações com um tamanho de população de 30, obtemos o seguinte
resultado:
Isso indica que os cinco primeiros recursos foram selecionados para fornecer o melhor MSE (cerca de 6,7)
para nosso teste. Observe que o algoritmo genético não faz suposições sobre o conjunto de recursos
que estava procurando, o que significa que não sabia que estávamos procurando um subconjunto dos
primeiros n recursos. Ele simplesmente procurou o melhor subconjunto possível de recursos.
Na próxima seção, avançaremos do uso de dados gerados artificialmente para um conjunto de dados
real e utilizaremos o algoritmo genético para selecionar os melhores recursos para um problema de
classificação.
[ 199 ]
Machine Translated by Google
A maioria dos recursos são booleanos (valor de 1 ou 0), indicando a presença ou ausência de um determinado
atributo, como cabelo, barbatanas e assim por diante. A primeira característica, nome do animal, serve apenas
para nos fornecer algumas informações e não participa do processo de aprendizagem.
Este conjunto de dados é usado para testar tarefas de classificação, onde os recursos de entrada precisam
ser mapeados em duas ou mais categorias/rótulos. Neste conjunto de dados, o último
recurso – chamado tipo – representa a categoria e é usado como valor de saída. Neste conjunto de dados,
existem sete categorias no total. Um valor de tipo de 5, por exemplo, representa uma categoria animal que inclui
sapo, salamandra e sapo. Para resumir, um modelo de classificação treinado com este conjunto de dados usará
os recursos 2-17 (cabelos, penas, barbatanas e assim por diante) para prever o valor do recurso 18 (tipo de animal).
Mais uma vez, queremos usar um algoritmo genético para selecionar os recursos que nos darão as melhores
previsões. Vamos começar criando uma classe Python que representa um classificador que foi treinado com esse
conjunto de dados.
[ 200 ]
Machine Translated by Google
3. Em vez de apenas separar os dados em um conjunto de treinamento e um conjunto de teste, como fizemos na
seção anterior, estamos usando a validação cruzada k-fold. Isso significa que os dados são divididos em k
partes iguais e o modelo é avaliado k vezes, cada vez usando (k-1) partes para treinamento e a parte
restante para teste (ou validação). Isso é fácil de fazer em Python usando o método model_selection.KFold()
da biblioteca scikit-learn :
4. Em seguida, criamos um modelo de classificação baseado em uma árvore de decisão. Esse tipo de
classificador cria uma estrutura de árvore durante a fase de treinamento que divide o conjunto de dados em
subconjuntos menores, resultando em uma previsão:
auto.classificador =
DecisionTreeClassifier(random_state=self.randomSeed)
Observe que estamos passando a semente aleatória para que ela possa ser usada internamente
pelo classificador. Dessa forma, podemos garantir que os resultados obtidos sejam repetíveis.
[ 201 ]
Machine Translated by Google
A métrica que está sendo usada aqui para avaliar o classificador é a precisão – a parcela
dos casos que foram classificados corretamente. Uma precisão de 0,85, por exemplo,
significa que 85% dos casos foram classificados corretamente. Como, em nosso caso, treinamos e
avaliamos o classificador k vezes, utilizamos o valor médio (mean) da acurácia que foi obtido ao
longo dessas avaliações.
7. O método main() da classe cria uma instância da classe Zoo e avalia o classificador com
todos os 16 recursos presentes usando a representação de uma única solução:
Ao executar o método principal da classe, a impressão mostra que, ao testar nosso classificador com
validação cruzada de 5 vezes usando todos os 16 recursos, a precisão de classificação alcançada é de cerca
de 91%:
[ 202 ]
Machine Translated by Google
Como na seção anterior, a representação do cromossomo que está sendo usada aqui é uma lista de
inteiros com os valores de 0 ou 1, indicando se um recurso deve ser usado ou descartado.
1. Primeiro, precisamos criar uma instância da classe Zoo e passar nossa semente aleatória
para produzir resultados repetíveis:
zoo = zoo.Zoo(RANDOM_SEED)
[ 203 ]
Machine Translated by Google
retornar 0,0,
outro:
precisão = zoo.getMeanAccuracy(individual) precisão de retorno -
*
FEATURE_PENALTY_FACTOR numFeaturesUsed, # retorna
uma tupla
toolbox.register("avaliar",
zooClassificationAccuracy)
5. Para os operadores genéticos, usamos novamente a seleção por torneio com um tamanho
de torneio de 2 e operadores de cruzamento e mutação especializados para cromossomos
de lista binária:
6. E mais uma vez, continuamos a usar a abordagem elitista, onde os membros do HOF –
os melhores indivíduos atuais – são sempre passados intocados para a próxima
geração:
[ 204 ]
Machine Translated by Google
7. No final da execução, imprimimos todos os membros do HOF para que possamos ver os
principais resultados encontrados pelo algoritmo. Imprimimos o valor de aptidão, que
inclui a penalidade pelo número de recursos, e o valor de precisão real:
Esses resultados indicam que todas as cinco principais soluções atingiram um valor de precisão
de 97%, usando seis ou sete recursos dos 16 disponíveis. Graças ao fator de penalidade em vários
recursos, a solução superior é o conjunto de seis recursos, que são como segue:
leite de
penas
no ar
espinha dorsal
barbatanas
cauda
[ 205 ]
Machine Translated by Google
Resumo
Neste capítulo, você foi apresentado ao aprendizado de máquina e aos dois principais tipos de
tarefas de aprendizado de máquina supervisionado – regressão e classificação. Em seguida,
você foi apresentado aos benefícios potenciais da seleção de recursos no desempenho dos modelos
que executam essas tarefas. No centro deste capítulo estavam duas demonstrações de como os
algoritmos genéticos podem ser utilizados para melhorar o desempenho de tais modelos por meio da
seleção de recursos. No primeiro caso, identificamos as características genuínas que foram geradas
pelo problema de regressão do teste Friedman-1, enquanto, no outro caso, selecionamos as
características mais benéficas do conjunto de dados de classificação Zoo.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes recursos:
Applied Supervised Learning with Python, Benjamin Johnston e Ishita Mathur, 26 de abril de
2019
Feature Engineering Made Easy, Sinan Ozdemir e Divya Susarla, 22 de janeiro de 2018
Seleção de recursos para classificação, M.Dash e H.Liu, 1997: https:// doi.org/ 10.1016/
S1088-467X(97)00008-5
Repositório de aprendizado de máquina da UCI: https://archive.ics.uci.edu/ml/index.
php
[ 206 ]
Machine Translated by Google
Ajuste de hiperparâmetros de
8
Modelos de aprendizado de máquina
Este capítulo descreve como os algoritmos genéticos podem ser usados para melhorar o desempenho de modelos
de aprendizado de máquina supervisionado ajustando os hiperparâmetros dos modelos. O capítulo começará
com uma breve introdução ao ajuste de hiperparâmetros no aprendizado de máquina antes de descrever o
conceito de pesquisa em grade. Depois de apresentar o conjunto de dados Wine e o classificador de reforço
adaptativo, ambos os quais serão usados ao longo deste capítulo, demonstraremos o ajuste de hiperparâmetros
usando uma pesquisa de grade convencional e uma pesquisa de grade orientada por algoritmo genético. Por fim,
tentaremos aprimorar os resultados obtidos usando uma abordagem de algoritmo genético direto para ajuste
de hiperparâmetros.
Começaremos este capítulo com uma visão geral rápida dos hiperparâmetros no aprendizado de máquina. Se você
for um cientista de dados experiente, sinta-se à vontade para pular a seção introdutória.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
pandas
matplotlib
nascido no mar
aprendido
No entanto, a maioria dos modelos possui outro conjunto de parâmetros que são definidos antes do
aprendizado. Estes são chamados de hiperparâmetros e afetam a maneira como o aprendizado é
feito. A imagem a seguir ilustra os dois tipos de parâmetros:
[ 208 ]
Machine Translated by Google
Normalmente, os hiperparâmetros têm valores padrão que terão efeito se não os definirmos
especificamente. Por exemplo, se olharmos para a implementação da biblioteca sklearn do
classificador de árvore de decisão (https://scikit-learn.org/stable/modules/generated/
sklearn.tree.DecisionTreeClassifier.html ), veremos vários hiperparâmetros e seus valores padrão.
Padrão
Nome Tipo Descrição Valor
profundidade máxima int A profundidade máxima da árvore Nenhum
Cada um desses parâmetros afeta a forma como a árvore de decisão é construída durante o
processo de aprendizagem, e seus efeitos combinados nos resultados do processo de aprendizagem
– e, consequentemente, no desempenho do modelo – podem ser significativos.
[ 209 ]
Machine Translated by Google
Ajuste de hiperparâmetros
Uma maneira comum de procurar boas combinações de hiperparâmetros é usar uma pesquisa em
grade. Usando esse método, escolhemos um subconjunto de valores para cada hiperparâmetro que
queremos ajustar. Como exemplo, dado o classificador da Árvore de Decisão, podemos escolher o
subconjunto de valores {2, 5, 10} para o parâmetro max_depth , enquanto, para o parâmetro splitter ,
escolhemos os dois valores possíveis – {"best", "random" }. Em seguida, tentamos todas as seis
combinações possíveis desses valores. Para cada combinação, o classificador é treinado e avaliado para
um determinado critério de desempenho, por exemplo, precisão. Ao final do processo, escolhemos a
combinação de valores de hiperparâmetros que rendeu o melhor desempenho.
A principal desvantagem da busca em grade é a busca exaustiva que ela realiza sobre todas as
combinações possíveis, o que pode ser muito demorado. Uma forma comum de produzir boas combinações
em menos tempo é a busca aleatória, onde combinações aleatórias de hiperparâmetros são escolhidas e
testadas.
Uma opção melhor – de particular interesse para nós – quando se trata de realizar a busca da grade, é
aproveitar um algoritmo genético para buscar a(s) melhor(es) combinação(s) de hiperparâmetros dentro
da grade pré-definida. Este método oferece o potencial para encontrar as melhores combinações
de grade em um período de tempo menor do que a pesquisa de grade exaustiva original.
Embora a pesquisa em grade e a pesquisa aleatória sejam suportadas pela biblioteca sklearn , a opção de pesquisa em grade
orientada por algoritmo genético é oferecida pela biblioteca sklearn-deap e se baseia nos recursos do algoritmo genético
baseado em DEAP. esta biblioteca pode ser instalado da seguinte forma:
Nas seções a seguir, vamos experimentar e comparar ambas as versões – exaustiva e baseada em
algoritmos genéticos – da busca em grade. Mas primeiro, vamos dar uma olhada rápida no conjunto de
dados que usaremos para nosso experimento – o conjunto de dados UCI Wine.
[ 210 ]
Machine Translated by Google
A análise química consiste em 13 medições diferentes, representando as quantidades dos seguintes constituintes
que se encontram em cada vinho:
Álcool
ácido málico
Cinzas
As colunas 2-14 do conjunto de dados contêm os valores das medições anteriores, enquanto o resultado da
classificação – o próprio tipo de vinho (1, 2 ou 3) – é encontrado na primeira coluna.
Em seguida, vamos ver o classificador que escolhemos para classificar este conjunto de dados.
[ 211 ]
Machine Translated by Google
Padrão
Nome Tipo Descrição Valor
n_estimadores int O número máximo de estimadores 50
Pode ser usado para diminuir a contribuição de cada
flutuação da taxa_de_aprendizagem 1
classificador
{'SAME', 'SAMME.R' – usa um algoritmo de aumento real,
algoritmo 2
'SAME.R'} 'SAMME' – usa um algoritmo de aumento discreto
self.initWineDataset()
self.initClassifier() self.initKfold()
self.initGridParams()
[ 212 ]
Machine Translated by Google
2. O método initGridParams() inicializa a busca da grade definindo os valores testados dos três
hiperparâmetros mencionados na seção anterior:
O parâmetro n_estimators é testado em 10 valores, espaçados linearmente
entre 10 e 100.
O parâmetro learning_rate é testado em 10 valores, logaritmicamente
espaçados entre 0,1 (10-2) e 1 (100 ).
Ambos os valores possíveis do parâmetro do algoritmo , 'SAMME' e 'SAMME.R',
são testados.
self.gridParams =
{ 'n_estimators': [10, 20, 30, 40, 50, 60, 70, 80, 90, 100], 'learning_rate': np.logspace(-2, 0,
num=10, base= 10), 'algoritmo': ['SAMME', 'SAMME.R'],
pontuação='precisão') return
cv_results.mean()
4. O método gridTest() executa uma pesquisa de grade convencional sobre o conjunto de valores
de hiperparâmetros testados que definimos anteriormente. A melhor combinação de
parâmetros é determinada, com base em sua métrica de 'precisão' média de validação
cruzada k-fold :
gridSearch.fit(self.X, self.y)
[ 213 ]
Machine Translated by Google
gridSearch =
EvolutionaryAlgorithmSearchCV(estimator=self.classifier,
params=self.gridParams,
cv=self.kfold,
scoring='precisão',
verbose=Verdadeiro,population_size=20,
gene_mutation_prob=0,30,
torneio_size=2, gerações_número=5)
gridSearch.fit(self.X, self.y)
6. Finalmente, o método main() da classe começa avaliando o desempenho do classificador com seus
valores padrão de hiperparâmetros. Em seguida, ele executa a pesquisa de grade
convencional e exaustiva, seguida pela pesquisa de grade orientada por algoritmo genético, enquanto
cronometra cada pesquisa.
Os resultados da execução do método principal desta classe são descritos na próxima subseção.
Esta não é uma precisão particularmente boa. Esperançosamente, a pesquisa de grade pode melhorar isso
encontrando uma melhor combinação de valores de hiperparâmetros.
[ 214 ]
Machine Translated by Google
A precisão da classificação que alcançamos com esses valores é de cerca de 93%, o que é uma grande melhoria em relação
aos 65% originais. O tempo de execução da pesquisa foi de cerca de 32 segundos:
Em seguida, vem a busca de grade de energia genética. Será que vai corresponder a esses resultados? Vamos descobrir.
Esta impressão refere-se à grade em que estamos pesquisando - uma lista de 10 números inteiros ( valores
n_estimators ), um ndarray de 10 elementos ( valores learning_rate) e uma lista de duas strings ( valores de algoritmo), como
segue:
'Types [1, 2, 1]' refere-se aos tipos de grade de [list, ndarray, list]. 'maxint [9, 9, 1]' corresponde
A próxima linha impressa refere-se à quantidade total de combinações de grade possíveis (10×10×2):
O restante da impressão parece muito familiar, pois utiliza as mesmas ferramentas de algoritmo genético baseadas em
DEAP que usamos o tempo todo, detalhando o processo de evolução das gerações e imprimindo uma linha de estatísticas
para cada geração:
[ 215 ]
Machine Translated by Google
Esses resultados indicam que a busca em grade orientada por algoritmo genético foi capaz de encontrar o
mesmo melhor resultado que foi encontrado usando a busca exaustiva, mas em um período de tempo menor –
cerca de 11 segundos.
Observe que este é um exemplo simples que é executado muito rapidamente. Em situações da vida real,
geralmente encontramos grandes conjuntos de dados, bem como modelos complexos e extensas grades de
hiperparâmetros. Nessas circunstâncias, a execução de uma pesquisa de grade exaustiva pode ser
proibitivamente demorada, enquanto a pesquisa de grade orientada por algoritmo genético tem o potencial
de produzir bons resultados dentro de um período de tempo razoável.
Mas ainda assim, todas as buscas de grade, baseadas em genética ou não, são limitadas ao subconjunto de
valores de hiperparâmetros que são definidos pela grade. E se quisermos pesquisar fora da grade, sem
ficarmos limitados a um subconjunto de valores predefinidos? Uma solução possível é descrita na próxima seção.
[ 216 ]
Machine Translated by Google
Como os hiperparâmetros podem ser de tipos variados, por exemplo, float, int e enumerated, que temos em nosso
classificador AdaBoost, podemos querer codificar cada um deles de maneira diferente e, em seguida, definir
as operações genéticas como uma combinação de operadores separados que estão adaptados a cada um
dos tipos. No entanto, também podemos usar uma abordagem preguiçosa e codificar todos eles como
parâmetros float para simplificar a implementação do algoritmo, como veremos
próximo.
Representação de hiperparâmetros
No Capítulo 6, Otimizando funções contínuas, usamos algoritmos genéticos para otimizar as funções de
parâmetros de valores reais. Esses parâmetros foram representados como uma lista de números flutuantes,
assim:
Conseqüentemente, os operadores genéticos que usamos eram especializados para lidar com listas de números
de ponto flutuante.
Para adaptar essa abordagem para que ela possa ajustar os hiperparâmetros, representaremos cada
hiperparâmetro como um número de ponto flutuante, independentemente de seu tipo real. Para fazer isso
funcionar, precisamos encontrar uma maneira de transformar cada parâmetro em um número de ponto flutuante e
voltar de um número de ponto flutuante à sua representação original. Vamos implementar essas transformações
da seguinte forma:
Essas conversões serão realizadas por dois arquivos Python, ambos descritos nas subseções a seguir.
[ 217 ]
Machine Translated by Google
return cv_results.mean()
Essa classe é utilizada pelo programa que implementa o algoritmo genético de ajuste de
hiperparâmetros. Isso será descrito na próxima seção.
[ 218 ]
Machine Translated by Google
1. Começamos definindo o limite inferior e superior para cada um dos valores flutuantes
representando um hiperparâmetro, conforme descrito na subseção anterior – [1, 100] para
n_estimators, [0,01, 1] para learning_rate e [0, 1] para algoritmo:
teste =
hyperparameter_tuning_genetic.HyperparameterTuningGenetic(RANDO M_SEED)
4. Agora vem uma parte particularmente interessante: como a solução é representada por uma
lista de valores flutuantes, cada um de um intervalo diferente, usamos o loop a seguir para
iterar sobre todos os pares de valores de limite inferior e superior. Para cada hiperparâmetro,
criamos um operador de caixa de ferramentas separado, que será usado para gerar valores
flutuantes aleatórios no intervalo apropriado:
for i in range(NUM_OF_PARAMS): #
"hyperparameter_0", "hyperparameter_1", ... toolbox.register("hyperparameter_"
+ str(i),
random.uniform,
BOUNDS_LOW[i],
BOUNDS_HIGH[i])
[ 219 ]
Machine Translated by Google
hyperparameters = () for i in
range(NUM_OF_PARAMS): hyperparameters
= hyperparameters + \ (toolbox.__getattribute__("hyperparameter_"
+ str(i)),)
6. Agora, podemos usar esta tupla de hiperparâmetros , em conjunto com o operador initCycle()
embutido no DEAP , para criar um novo operador individualCreator que preenche uma instância
individual com uma combinação de valores de hiperparâmetros gerados aleatoriamente:
toolbox.register("avaliar", classificaçãoPrecisão)
toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR)
toolbox.register("mutar",
[ 220 ]
Machine Translated by Google
tools.mutPolynomialBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR,
indpb=1.0 / NUM_OF_PARAMS)
Ao executar o algoritmo por cinco gerações com um tamanho de população de 20, obtemos
o seguinte resultado:
Esses resultados indicam que a melhor combinação encontrada foi n_estimators = 69,
learning_rate = 0,628 e algoritmo = 'SAMME.R'.
A precisão de classificação que alcançamos com esses valores é de cerca de 94,9% – uma
melhoria digna em relação à precisão que alcançamos com a pesquisa em grade. Curiosamente,
os melhores valores encontrados para n_estimators e learning_rate estão fora dos valores da
grade em que pesquisamos.
[ 221 ]
Machine Translated by Google
Resumo
Neste capítulo, você foi apresentado ao conceito de ajuste de hiperparâmetros no aprendizado de
máquina. Depois de se familiarizar com o conjunto de dados Wine e o classificador Adaptive
Boosting, ambos os quais usamos para testar ao longo deste capítulo, você foi apresentado aos
métodos de ajuste de hiperparâmetros de uma pesquisa de grade exaustiva e sua contraparte
orientada por algoritmo genético. Esses dois métodos foram então comparados usando nosso
cenário de teste. Finalmente, tentamos uma abordagem de algoritmo genético direto, onde
todos os hiperparâmetros foram representados como valores flutuantes. Essa abordagem nos permitiu
melhorar os resultados da pesquisa em grade.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes
recursos:
[ 222 ]
Machine Translated by Google
Otimização da Arquitetura de
9
Redes de aprendizado profundo
Este capítulo descreve como algoritmos genéticos podem ser usados para melhorar o desempenho de
modelos baseados em redes neurais artificiais, otimizando a arquitetura de rede desses modelos.
Começaremos com uma breve introdução às redes neurais e ao aprendizado profundo. Depois de apresentar
o conjunto de dados Iris e o classificador Multilayer Perceptron, demonstraremos a otimização da
arquitetura de rede usando uma solução baseada em algoritmo genético. Em seguida, estenderemos essa
abordagem para combinar a otimização da arquitetura de rede com o ajuste de hiperparâmetros
do modelo, que será realizado em conjunto por uma solução baseada em algoritmos genéticos.
Começaremos este capítulo com uma visão geral das redes neurais artificiais. Se você for um
cientista de dados experiente, sinta-se à vontade para pular as seções introdutórias.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
aprendido
Modelo de neurônio
biológico Fonte: https://pixabay.com/vectors/neuron-nerve-cell-axon-dendrite-296581/
[ 224 ]
Machine Translated by Google
Os dendritos da célula neuronal, que circundam o corpo celular no lado esquerdo do diagrama
anterior, são usados como entradas de várias células semelhantes, enquanto o longo axônio, saindo
do corpo celular, serve como saída e pode ser conectado para várias outras células.
Essa estrutura é imitada pelo modelo artificial chamado perceptron, ilustrado a seguir:
O perceptron calcula a saída multiplicando cada um dos valores de entrada por um determinado
peso; os resultados são acumulados e um valor de viés é adicionado à soma. Uma função de
ativação não linear então mapeia o resultado para a saída. Essa funcionalidade emula a operação
do neurônio biológico, que dispara (envia uma série de pulsos de sua saída) quando a
soma ponderada das entradas está acima de um determinado limite.
O modelo perceptron pode ser usado para tarefas simples de classificação e regressão se ajustarmos
seus valores de peso e viés para que mapeiem certas entradas para os níveis de saída desejados.
No entanto, um modelo muito mais capaz pode ser construído ao conectar várias unidades
perceptron em uma estrutura chamada Multilayer Perceptron, que será descrita na próxima subseção.
[ 225 ]
Machine Translated by Google
Perceptron Multicamadas
O Multilayer Perceptron (MLP) estende a ideia do perceptron usando vários nós, cada um
implementando um perceptron. Os nós no MLP são organizados em camadas e cada camada é conectada
à seguinte. A estrutura básica do MLP é ilustrada no seguinte diagrama:
Camada de entrada: recebe os valores de entrada e conecta cada um deles a cada neurônio
da próxima camada.
Camada de Saída: Entrega os resultados calculados pelo MLP. Quando o MLP é usado como
classificador, cada uma das saídas representa uma das classes. Quando o MLP for usado
para regressão, haverá um único nó de saída, produzindo um valor contínuo.
O treinamento desse modelo envolve o ajuste dos valores de ponderação e viés para cada um dos nós.
Isso geralmente é feito usando uma família de algoritmos chamada retropropagação. O princípio
básico da retropropagação é minimizar o erro entre as saídas reais e as desejadas, propagando o erro
de saída pelas camadas do modelo, da camada de saída para dentro. Os pesos dos vários nós são
ajustados de forma que os pesos que mais contribuíram para o erro sejam mais ajustados.
[ 226 ]
Machine Translated by Google
Por muitos anos, as limitações computacionais dos algoritmos de retropropagação restringiram os MLPs
a não mais do que duas ou três camadas ocultas, até que novos desenvolvimentos mudaram
drasticamente as coisas. Estes serão explicados na próxima seção.
Na próxima seção, descobriremos como a arquitetura do MLP pode ser otimizada usando um algoritmo
genético.
[ 227 ]
Machine Translated by Google
O conjunto de dados contém 50 amostras de cada uma das três espécies e consiste nos quatro recursos
a seguir:
sepal_length (cm)
sepal_width (cm)
petal_length (cm)
petal_width (cm)
Este conjunto de dados está disponível diretamente na biblioteca sklearn e pode ser inicializado da seguinte maneira:
[ 228 ]
Machine Translated by Google
Em nossos experimentos, usaremos um classificador MLP em conjunto com este conjunto de dados e
aproveitaremos o poder dos algoritmos genéticos para encontrar a arquitetura de rede – o número de camadas
ocultas e o número de nós em cada camada – que produzirá a melhor precisão de classificação .
Como estamos usando a abordagem de algoritmos genéticos, a primeira coisa que precisamos fazer é encontrar
uma maneira de representar essa arquitetura usando um cromossomo, conforme descrito na próxima subseção.
Para conseguir isso, precisamos criar um cromossomo que possa expressar o número de camadas e o
número de nós em cada camada. Um cromossomo de comprimento variável que pode ser traduzido
diretamente na tupla de comprimento variável que é usado como o parâmetro hidden_layer_sizes
do modelo é uma opção; no entanto, essa abordagem exigiria operadores genéticos personalizados,
possivelmente complicados. Para poder usar nossos operadores genéticos padrão, usaremos uma
representação de comprimento fixo. Ao usar essa abordagem, o número máximo de camadas é decidido
antecipadamente e todas as camadas são sempre representadas, mas não necessariamente
expressas na solução. Por exemplo, se decidirmos limitar a rede a quatro camadas ocultas, o cromossomo terá a
seguinte aparência:
[ 229 ]
Machine Translated by Google
No entanto, para controlar o número real de camadas ocultas na rede, alguns desses valores
podem ser zero ou negativos. Tal valor significa que não serão adicionadas mais camadas à rede.
Os exemplos a seguir ilustram esse método:
O cromossomo [10, 20, -5, 15] é traduzido na tupla (10, 20), pois o -5 encerra a
contagem de camadas.
O cromossomo [10, 0, -5, 15] é traduzido na tupla (10, ) pois o 0 encerra a
contagem de camadas.
O cromossomo [10, 20, 5, -15] é traduzido na tupla (10, 20, 5), pois o -15 encerra a
contagem de camadas.
O cromossomo [10, 20, 5, 15] é traduzido na tupla (10, 20, 5, 15).
Para garantir que haja pelo menos uma camada oculta, podemos garantir que o primeiro
parâmetro seja sempre maior que zero. Os outros parâmetros da camada podem ter
distribuições variadas em torno de zero para que possamos controlar suas chances de serem
os parâmetros finais.
Além disso, embora esse cromossomo seja formado por números inteiros, optamos por utilizar
números flutuantes, assim como fizemos no capítulo anterior para vários tipos de variáveis.
O uso de uma lista de números flutuantes é conveniente, pois permite usar operadores genéticos
existentes e, ao mesmo tempo, estender facilmente o cromossomo para incluir outros parâmetros
de tipos diferentes. Faremos isso mais adiante. Os números float podem ser traduzidos de
volta para inteiros usando a função round() . Alguns exemplos dessa abordagem generalizada são
os seguintes:
[ 230 ]
Machine Translated by Google
1. O método convertParam() da classe usa uma lista chamada params. Na verdade, esse é o
cromossomo que descrevemos na subseção anterior e contém os valores flutuantes que
representam até quatro camadas ocultas. O método transforma esta lista de floats na tupla
hidden_layer_sizes :
if round(params[1]) <= 0:
hiddenLayerSizes = round(params[0]), elif round(params[2])
<= 0:
hiddenLayerSizes = (round(params[0]), round(params[1]))
elif round(params[3]) <= 0:
hiddenLayerSizes = (round(params[0]), round(params[1]), round(params[2])) else:
hiddenLayerSizes = self.convertParams(params)
auto.classificador =
MLPClassifier(hidden_layer_sizes=hiddenLayerSizes)
Em seguida, ele encontra a precisão do classificador usando a validação cruzada k-fold que
criamos para o conjunto de dados do Wine:
pontuação='precisão') return
cv_results.mean()
A classe MlpLayersTest é utilizada pelo otimizador baseado em algoritmo genético. Explicaremos isso na
próxima seção.
[ 231 ]
Machine Translated by Google
1. Começamos definindo o limite inferior e superior para cada um dos valores flutuantes que
representam uma camada oculta. A primeira camada oculta recebe o intervalo [5, 15],
enquanto o restante das camadas começa com valores negativos cada vez maiores, o que
aumenta suas chances de encerrar a contagem de camadas:
teste = mlp_layers_test.MlpLayersTest(RANDOM_SEED)
4. Agora, empregamos a mesma abordagem que usamos no capítulo anterior: como a solução
é representada por uma lista de valores flutuantes, cada um de um intervalo diferente,
usamos o seguinte loop para iterar sobre todos os pares de limite inferior, superior -bound,
e para cada intervalo, criamos um operador de caixa de
ferramentas separado (layer_size_attribute) que será usado para gerar valores flutuantes
aleatórios no intervalo apropriado posteriormente:
[ 232 ]
Machine Translated by Google
random.uniform,
BOUNDS_LOW[i],
BOUNDS_HIGH[i])
toolbox.register("avaliar", classificaçãoPrecisão)
toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,
[ 233 ]
Machine Translated by Google
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR)
toolbox.register("mutar",
tools.mutPolynomialBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR,
indpb=1.0 / NUM_OF_PARAMS)
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats, halloffame=hof, verbose=True)
[ 234 ]
Machine Translated by Google
Os resultados anteriores indicam que, dentro dos intervalos que definimos, a melhor combinação
encontrada foi de três camadas ocultas de tamanho 15, 5 e 8, respectivamente. A precisão de
classificação que alcançamos com esses valores é de cerca de 86,7%.
Esta precisão parece ser um resultado razoável para o problema em questão. No entanto, há mais
que podemos fazer para melhorá-lo ainda mais.
{'constante',
Programação da taxa de aprendizado
learning_rate 'invscaling', 'adaptável'} 'constante'
para atualizações de peso
[ 235 ]
Machine Translated by Google
Como vimos no capítulo anterior, uma representação cromossômica baseada em ponto flutuante
nos permite combinar vários tipos de hiperparâmetros no processo de otimização baseado em
algoritmos genéticos. Como usamos um cromossomo baseado em ponto flutuante para representar
a configuração das camadas ocultas, agora podemos incorporar outros hiperparâmetros no processo
de otimização aumentando o cromossomo de acordo. Vamos descobrir como podemos fazer isso.
Representação da solução
Em relação à nossa configuração de arquitetura de rede existente de quatro floats da forma [n1, n2,
n3, n4], podemos adicionar os seguintes quatro hiperparâmetros:
[ 236 ]
Machine Translated by Google
Esta classe é baseada na que usamos para otimizar a configuração das camadas ocultas, MlpLayersTest, mas
com algumas modificações. Vamos a estes:
1. O método convertParam() agora lida com uma lista de parâmetros onde os quatro primeiros
entradas (params[0] a params[3]) representam os tamanhos das camadas ocultas, assim como antes,
mas, além disso, params[4] a params[7] representam os quatro hiperparâmetros que adicionamos à
avaliação. Consequentemente, o método foi aumentado com as seguintes linhas de código,
permitindo que ele transforme o restante dos parâmetros fornecidos (params[4] a params[7]) em
seus valores correspondentes, que podem então ser alimentados ao classificador MLP:
ativação=ativação,
solucionador=resolvedor,
alpha=alfa,
learning_rate=learning_rate)
[ 237 ]
Machine Translated by Google
Graças à representação de número flutuante unificada que é usada para todos os parâmetros,
este programa é quase idêntico ao que usamos na seção anterior para otimizar a arquitetura
de rede. A principal diferença está na definição das listas BOUNDS_LOW e BOUNDS_HIGH ,
que contém as faixas dos parâmetros. Em relação aos quatro intervalos que definimos
anteriormente – um para cada camada oculta – adicionaremos mais quatro, representando os
quatro hiperparâmetros adicionais que discutimos anteriormente nesta seção:
# 'alpha': # 0.0001..2.0
E isso é tudo – o programa é capaz de lidar com os parâmetros adicionados sem nenhuma
alteração adicional.
'hidden_layer_sizes'=(8, 8) 'ativação'='relu'
'solver'='lbfgs'
'alpha'=0.572971105096338
'learning_rate'='invscaling' => precisão =
0.9466666666666667
Os resultados anteriores indicam que, dentro dos intervalos que definimos, a melhor combinação
que encontramos para a configuração da camada oculta e hiperparâmetros foi a seguinte:
[ 238 ]
Machine Translated by Google
Um valor alfa de cerca de 0,572 – consideravelmente maior que o valor padrão de 0,0001
Essa otimização combinada resultou em uma precisão de classificação de cerca de 94,7% – uma
melhoria significativa em relação aos resultados anteriores, usando menos camadas ocultas e menos nós
do que antes.
Resumo
Neste capítulo, você foi apresentado aos conceitos básicos de redes neurais artificiais e aprendizado
profundo. Depois de se familiarizar com o conjunto de dados Iris e o classificador Multilayer Perceptron
(MLP) , você foi apresentado à noção de otimização de arquitetura de rede.
Em seguida, demonstramos uma otimização baseada em algoritmo genético da arquitetura de rede para o
classificador MLP. Por fim, conseguimos combinar a otimização da arquitetura de rede com o ajuste de
hiperparâmetros do modelo com o processo de algoritmos genéticos e aprimorar ainda mais o desempenho
do classificador.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes
recursos:
Python Deep Learning - Segunda Edição, Gianmario Spacagna, Daniel Slater et al.
16 de janeiro de
2019 Projetos de rede neural com Python, James Loy, 28 de fevereiro de
2019 scikit-learn Multilayer Perceptron Classifier: https://scikit-learn.org/ stable/modules/
neural_networks_supervised.html
Repositório de aprendizado de máquina da UCI: https://archive.ics.uci.edu/ml/index.
php
[ 239 ]
Machine Translated by Google
Começaremos este capítulo descrevendo os conceitos básicos do aprendizado por reforço. Se você for um
cientista de dados experiente, sinta-se à vontade para pular esta seção introdutória.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
aprendido
Os ambientes de Ginásio que serão usados neste capítulo são MountainCar-v0 (https://gym. openai.com/
envs/MountainCar-v0/) e CartPole-V1 (https://gym.openai.com/envs/CartPole-v1/ ).
Os programas que serão usados neste capítulo podem ser encontrados no repositório GitHub
deste livro em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms with-Python/tree/
master/Chapter10.
[ 241 ]
Machine Translated by Google
Os dois principais componentes da tarefa de aprendizado por reforço são o ambiente e o agente, conforme
ilustrado no diagrama a seguir:
Aprendizagem por reforço representada como uma interação entre o agente e o ambiente
O agente representa um algoritmo que interage com o ambiente e tenta resolver um determinado
problema maximizando a recompensa cumulativa.
A troca que ocorre entre o agente e o ambiente pode ser expressa como uma série de etapas. Em cada
etapa, o ambiente apresenta ao agente determinado (s) estado(s), também chamado de observação.
O agente, por sua vez, realiza uma ação (a). O ambiente responde com um novo estado (s'), bem
como com um valor intermediário de recompensa (R). Essa troca se repete até que uma
determinada condição de parada seja atendida. O objetivo do agente é maximizar a soma dos valores de
recompensa que são coletados ao longo do caminho.
Apesar da simplicidade dessa formulação, ela pode ser usada para descrever tarefas e situações
extremamente complexas, o que torna o aprendizado por reforço aplicável a uma ampla gama de
aplicações, como teoria dos jogos, assistência médica, sistemas de controle, automação da cadeia de
suprimentos e pesquisa operacional.
A versatilidade dos algoritmos genéticos será demonstrada mais uma vez neste capítulo, pois os
usaremos para auxiliar nas tarefas de aprendizado por reforço.
[ 242 ]
Machine Translated by Google
Antes de começarmos a aplicar algoritmos genéticos para tarefas de aprendizado por reforço, vamos
nos familiarizar com o kit de ferramentas que será usado para conduzir essas tarefas — OpenAI Gym.
Ginásio OpenAI
Ginásio OpenAI (https://github.com/openai/gym) é uma biblioteca de código aberto que foi escrita para permitir
o acesso a um conjunto padronizado de tarefas de aprendizado por reforço. Ele fornece um kit de
ferramentas que pode ser usado para comparar e desenvolver algoritmos de aprendizado por reforço.
O OpenAI Gym consiste em uma coleção de ambientes, todos apresentando uma interface comum chamada
env. Essa interface separa os vários ambientes dos agentes, que podem ser implementados da maneira que
quisermos — o único requisito do agente é que ele possa interagir com o(s) ambiente(s) por meio da
interface env . Isso será descrito na próxima subseção.
O pacote básico, academia, dá acesso a diversos ambientes e pode ser instalado da seguinte forma:
Vários outros pacotes estão disponíveis, como 'Atari', 'Box2D' e 'MuJoCo', que fornecem acesso a vários
e diversos ambientes adicionais. Alguns desses pacotes possuem dependências do sistema e podem estar
disponíveis apenas para determinados sistemas operacionais. Mais informações estão disponíveis em https://
github.com/openai/gym#installation.
[ 243 ]
Machine Translated by Google
A interface do ambiente
Para criar um ambiente, precisamos utilizar o método make() e o nome do ambiente desejado,
conforme abaixo:
env = gym.make('MountainCar-v0')
Informações detalhadas sobre os ambientes disponíveis estão disponíveis nos links a seguir:
https://github.com/openai/gym/blob/master/docs/environments.md
https://gym.openai.com/envs/
Depois de criado o ambiente, ele pode ser inicializado usando o método reset() , conforme o trecho de
código a seguir:
observação = env.reset()
Conforme o ciclo de aprendizado por reforço que descrevemos na subseção anterior, a interação
contínua com o ambiente consiste em enviar a ele uma ação e, em troca, receber uma recompensa
intermediária e um novo estado. Isso é implementado pelo método step() , da seguinte forma:
Além do objeto de observação , que descreve o novo estado e o valor da recompensa flutuante que
representa a recompensa temporária, esse método retorna os seguintes valores:
done: Este é um Booleano que se torna verdadeiro quando a execução atual (também
chamada de episódio) termina, por exemplo, o agente perdeu uma vida ou concluiu a
tarefa com sucesso. info: Este é um dicionário contendo informações adicionais opcionais
que podem ser úteis para depuração. No entanto, não deve ser usado pelo agente
para aprendizado.
A qualquer momento, o ambiente pode ser renderizado para apresentação visual, como segue:
env.render()
[ 244 ]
Machine Translated by Google
Finalmente, um ambiente pode ser fechado para invocar qualquer limpeza necessária, como segue:
env.close()
Se esse método não for chamado, o ambiente se fechará automaticamente na próxima vez que o Python
executar seu processo de coleta de lixo (o processo de identificação e liberação de memória que não está
mais em uso pelo programa) ou quando o programa for encerrado.
O ciclo completo de interação com o ambiente será demonstrado na próxima seção, onde encontraremos
nosso primeiro desafio de academia - o ambiente MountainCar .
[ 245 ]
Machine Translated by Google
O objetivo é fazer com que o carro suba a colina mais alta - a da direita - e, por fim, bata na bandeira:
A simulação é configurada com o motor do carro muito fraco para subir diretamente a colina mais alta.
A única maneira de atingir o objetivo é dirigir o carro para frente e para trás até que haja impulso
suficiente para subir. Subir a colina à esquerda pode ajudar a atingir esse objetivo, pois atingir o pico
esquerdo fará o carro saltar de volta para a direita, conforme mostrado na captura de tela a seguir:
Esta simulação é um ótimo exemplo em que a perda intermediária (mover para a esquerda)
pode ajudar a atingir o objetivo final (ir totalmente para a direita).
[ 246 ]
Machine Translated by Google
O valor da ação esperada nesta simulação é um número inteiro com um dos três valores a seguir:
1: Sem empurrão
O objeto de observação contém dois carros alegóricos que descrevem a posição e a velocidade do carro. Um
exemplo disso pode ser visto no seguinte trecho de código:
[-1,0260268, -0,03201975]
Por fim, o valor da recompensa é -1 para cada etapa de tempo, até que a meta (localizada na posição 0,5)
seja atingida. A simulação será interrompida após 200 passos se a meta não for atingida antecipadamente.
Mais informações sobre o ambiente MountainCar-v0 podem ser encontradas nos seguintes links:
Representação da solução
Como o MountainCar é controlado por uma sequência de ações, cada uma com um valor de 0 (pressionar
para a esquerda), 1 (sem pressionar) ou 2 (pressionar para a direita), e pode haver até 200 ações em um
único episódio, uma maneira óbvia para representar uma solução candidata é usar uma lista de
comprimento 200, contendo valores de 0, 1 ou 2. Um exemplo disso é o seguinte:
[0, 1, 2, 0, 0, 1, 2, 2, 1, ... , 0, 2, 1, 1]
Os valores na lista serão usados como ações para controlar o carro e levá-lo até a bandeira. Se o carro chegar
à bandeira em menos de 200 passos, os últimos itens da lista não serão usados.
Em seguida, precisamos determinar como avaliar uma dada solução desta forma.
[ 247 ]
Machine Translated by Google
Avaliando a solução
Ao avaliar uma determinada solução, ou ao comparar duas soluções, é evidente que o valor da
recompensa por si só pode não nos fornecer informações suficientes. Pela forma como a
recompensa é definida, seu valor sempre será -200 se não acertarmos a bandeira. Quando
comparamos duas soluções candidatas que não atingem a bandeira, ainda assim gostaríamos
de saber qual delas se aproximou dela e considerá-la uma solução melhor. Portanto, vamos utilizar
a posição final do carro, além do valor da recompensa, para determinar a pontuação da solução.
Se o carro não acertou a bandeira, a pontuação será a distância da bandeira. Portanto,
estaremos procurando uma solução que minimize a pontuação. Se o carro bater na bandeira, a
pontuação base será zero, e disso, deduzimos um valor adicional baseado em quantos
passos faltaram, tornando a pontuação negativa. Como estamos procurando a menor
pontuação possível, esse arranjo incentivará soluções para acertar a bandeira usando a menor
quantidade possível de ações.
[ 248 ]
Machine Translated by Google
O método main da classe pode ser usado depois que uma solução foi serializada e salva usando o método
saveActions() . O método main inicializará a classe e chamará replaySavedActions() para renderizar e
animar a última solução salva.
Normalmente, usamos o método principal para animar a melhor solução encontrada pelo programa baseado
em algoritmo genético. Isso será descrito na próxima subseção.
Como a representação da solução que escolhemos para este problema é uma lista contendo os valores
inteiros 0, 1 ou 2, este programa é semelhante ao que usamos para resolver o problema da mochila 0-1 no
Capítulo 4, Otimização Combinatória, onde as soluções foram representadas como listas com os valores 0 e 1.
1. Começamos criando uma instância da classe MountainCar , que nos permitirá pontuar as várias
soluções para o desafio MountainCar :
carro = mountain_car.MountainCar(RANDOM_SEED)
2. Como nosso objetivo é minimizar o placar - em outras palavras, acertar a bandeira com o
contagem mínima de passos; caso contrário, aproxime-se o máximo possível da bandeira –
definimos um único objetivo, minimizando a estratégia de aptidão:
3. Agora, precisamos criar um operador de caixa de ferramentas que possa produzir um dos três
valores de ação permitidos — 0, 1 ou 2:
toolbox.register("zeroOneOrTwo", random.randint, 0, 2)
[ 249 ]
Machine Translated by Google
4. Isso é seguido por um operador que preenche uma instância individual com esses
valores:
toolbox.register("avaliar", carScore)
6. Quanto aos operadores genéticos, começamos com a seleção de torneio usual com um
tamanho de torneio de 2. Como nossa representação de solução é uma lista de
valores inteiros 0, 1 ou 2, podemos usar o operador de cruzamento de dois pontos,
exatamente como fizemos quando a solução foi representada por uma lista de
valores 0 e 1. Para mutação, no entanto, em vez do operador FlipBit, que normalmente
é usado para o caso binário, precisamos usar o operador UniformInt , que é usado
para um intervalo de valores inteiros e configurá-lo para o intervalo de 0..2:
toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate",
tools.cxTwoPoint) toolbox.register("mutate", tools.mutUniformInt,
low=0, up=2, indpb= 1.0/len(carro))
[ 250 ]
Machine Translated by Google
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats,
halloffame=hof,
verbose=True)
8. Após a execução, imprimimos a melhor solução e a salvamos para que possamos animá-la
usando o recurso de repetição que construímos na classe MountainCar posteriormente:
Executando o algoritmo para 80 gerações e com um tamanho de população de 100, obtemos o seguinte
resultado:
[ 251 ]
Machine Translated by Google
A partir da saída anterior, podemos ver que, após cerca de 60 gerações, a(s) melhor(es) solução(ões) começaram
a atingir o sinalizador, produzindo valores de pontuação de zero ou menos. A partir daqui, as melhores
soluções atingem a bandeira em menos etapas, produzindo, portanto, valores de pontuação cada vez mais
negativos.
Como já mencionamos, a melhor solução foi salva no final da execução e agora podemos reproduzi-la
executando o programa mountain_car . A captura de tela a seguir ilustra como as ações de nossa solução
conduzem o carro para frente e para trás entre os dois picos, subindo cada vez mais alto, até que o
carro seja capaz de subir a colina inferior à esquerda. Em seguida, ele se recupera, o que significa que temos
impulso suficiente para continuar subindo a colina mais alta à direita, atingindo a bandeira:
Embora resolvê-lo tenha sido muito divertido, a maneira como esse ambiente é configurado não exige que
interajamos dinamicamente com ele. Conseguimos subir o morro usando uma sequência de ações que nosso
algoritmo montou, com base na localização inicial do carro. Em contraste com isso, o próximo ambiente que
estamos prestes a enfrentar - chamado CartPole - exige que calculemos dinamicamente nossa ação a
qualquer passo de tempo, com base na última observação produzida. Continue lendo para descobrir como
podemos fazer isso funcionar.
[ 252 ]
Machine Translated by Google
O poste, agindo como um pêndulo neste ambiente, começa na vertical dentro de um pequeno ângulo aleatório,
conforme mostrado na seguinte saída renderizada:
Nosso objetivo é evitar que o pêndulo caia para qualquer lado pelo maior tempo possível, ou seja, até 500 passos
de tempo. Para cada passo de tempo que o mastro permanece na posição vertical, recebemos uma recompensa
de +1, então a recompensa total máxima é 500. O episódio terminará prematuramente se uma das seguintes situações
ocorrer durante a corrida:
O valor da ação esperada nesta simulação é um número inteiro de um dos dois valores a seguir:
Em nossa solução proposta, usaremos esses valores como entradas em cada passo de tempo para determinar
qual ação tomar. Faremos isso com a ajuda de um controlador baseado em rede neural. Isso é descrito com mais
detalhes na próxima subseção.
[ 253 ]
Machine Translated by Google
Mais informações sobre o ambiente CartPole-v1 estão disponíveis nos seguintes links:
https://gym.openai.com/envs/CartPole-v1/
https://github.com/openai/gym/wiki/CartPole-v0
Uma boa maneira de implementar esse mapeamento é usando uma rede neural. Como vimos no Capítulo 9,
Otimização da Arquitetura de Redes de Aprendizagem Profunda, uma rede neural, como um Multilayer Perceptron
(MLP), pode implementar mapeamentos complexos entre suas entradas e saídas. Este mapeamento é feito com o
auxílio dos parâmetros da rede, ou seja, os pesos e bias dos nós ativos na rede, bem como as funções de transferência
que são implementadas por esses nós. No nosso caso, usaremos uma rede com uma única camada oculta de
quatro nós. Além disso, a camada de entrada consiste em quatro nós, um para cada um dos valores de entrada
fornecidos pelo ambiente, enquanto a camada de saída tem um único nó, pois temos apenas um valor de saída – a
ação a ser executada. Esta estrutura de rede pode ser ilustrada com o seguinte diagrama:
[ 254 ]
Machine Translated by Google
Como já vimos, os valores dos pesos e bias de uma rede neural são normalmente definidos durante
um processo no qual a rede é treinada. A parte interessante é que, até agora, vimos apenas esse tipo de
rede neural sendo treinada usando o algoritmo de retropropagação durante a implementação
do aprendizado supervisionado. Ou seja, em cada um dos casos anteriores, tivemos um conjunto de
treinamento de entradas e saídas correspondentes, e a rede foi treinada para mapear cada entrada fornecida
para sua saída correspondente. Aqui, no entanto, enquanto praticamos o aprendizado por reforço,
não temos essas informações de treinamento disponíveis. Em vez disso, sabemos apenas o quão bem a rede
se saiu no final do episódio. Isso significa que, em vez de usar os algoritmos de treinamento convencionais,
precisamos de um método que nos permita encontrar os melhores parâmetros de rede – pesos e vieses –
com base nos resultados obtidos ao executar os episódios do ambiente. Esse é exatamente o tipo de
otimização em que os algoritmos genéticos são bons: encontrar um conjunto de parâmetros que nos dará os
melhores resultados, desde que você tenha uma maneira de avaliar e comparar esses resultados. Para fazer
isso, precisamos descobrir como representar os parâmetros da rede, bem como avaliar um determinado
conjunto desses parâmetros. Ambos os tópicos serão abordados na próxima subseção.
Camada de entrada: Esta camada não participa do mapeamento da rede; em vez disso, ele
recebe os valores de entrada e os encaminha para cada neurônio na próxima camada.
Portanto, nenhum parâmetro é necessário para esta rede.
Camada oculta: Cada nó nesta camada está totalmente conectado a cada uma das entradas e,
portanto, requer quatro pesos além de um único valor de viés.
Camada de saída: O único nó nesta camada está conectado a cada um dos nós na camada
oculta e, portanto, requer quatro pesos além de um único valor de viés.
No total, temos 20 valores de peso e cinco valores de bias que precisamos encontrar, todos do tipo float.
Portanto, cada solução potencial pode ser representada como uma lista de 25 valores flutuantes, assim:
[ 255 ]
Machine Translated by Google
Avaliar uma determinada solução significa criar nosso MLP com as dimensões corretas - quatro
entradas, uma camada oculta de quatro nós e uma única saída - e atribuir os valores de peso e
viés de nossa lista de flutuações aos vários nós. Então, precisamos usar este MLP como o
controlador para o bastão do carrinho durante um episódio. A recompensa total resultante do
episódio é usada como valor de pontuação para esta solução. Ao contrário da tarefa anterior, aqui
pretendemos maximizar a pontuação alcançada. Este procedimento de avaliação de pontuação
é implementado pela classe CartPole , que será descrita na próxima subseção.
A classe é inicializada com uma semente aleatória opcional e fornece os seguintes métodos:
O método principal da classe deve ser usado depois que uma solução foi serializada e salva
usando o método saveParams() . O método main inicializará a classe e chamará
replayWithSavedParams() para renderizar e animar a solução salva.
Normalmente, usaremos o método principal para animar a melhor solução encontrada por
nossa solução orientada por algoritmo genético. Isso será descrito na próxima subseção.
[ 256 ]
Machine Translated by Google
Como estamos usando uma lista de floats para representar uma solução - os pesos e bias da rede - este
programa é muito semelhante aos programas de otimização de função que vimos no Capítulo 6, Otimizando funções
contínuas, como o que usamos para a função Eggholder otimização.
1. Começamos por criar uma instância da classe CartPole , que nos permitirá testar as várias soluções
para o desafio CartPole:
cartPole = cart_pole.CartPole(RANDOM_SEED)
2. Em seguida, definimos os limites superior e inferior para os valores flutuantes que seremos
procurando. Como todos os nossos valores representam pesos e vieses dentro de uma rede neural,
precisamos definir o intervalo para que fique entre -1,0 e 1,0 em todas as dimensões:
3. Como você deve se lembrar, nosso objetivo neste desafio é maximizar a pontuação - o tempo
que conseguimos manter o mastro equilibrado. Para isso, definimos um único objetivo,
maximizando a estratégia de aptidão:
4. Agora, precisamos criar uma função auxiliar para criar números reais aleatórios que são distribuídos
uniformemente dentro de um determinado intervalo. Esta função assume que o intervalo é o mesmo
para todas as dimensões, como é o caso da nossa solução:
5. Agora, usamos esta função para criar um operador que retorna aleatoriamente uma lista de
flutua no intervalo desejado que definimos anteriormente:
[ 257 ]
Machine Translated by Google
6. Isso é seguido por um operador que preenche uma instância individual usando o
operador precedente:
toolbox.register("avaliar", pontuar)
8. É hora de escolher nossos operadores genéticos. Mais uma vez, usaremos o torneio
seleção com um tamanho de torneio de 2 como nosso operador de seleção. Como nossa
representação de solução é uma lista de flutuações em um determinado intervalo,
usaremos os operadores especializados de cruzamento e mutação limitados contínuos
fornecidos pela estrutura DEAP - cxSimulatedBinaryBounded e mutPolynomialBounded,
respectivamente:
toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR)
toolbox.register("mutar",
tools.mutPolynomialBounded,
low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR,
indpb=1.0/NUM_OF_PARAMS)
[ 258 ]
Machine Translated by Google
9. E, como sempre, usamos a abordagem elitista, onde os membros do HOF - os melhores indivíduos
atuais - são sempre passados intocados para a próxima geração:
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats,
halloffame=hof,
verbose=True)
10. Após a execução, imprimimos a melhor solução e a salvamos para que possamos animá-la
usando o recurso de repetição que construímos na classe MountainCar posteriormente:
best = hof.items[0]
print("Melhor solução = ", best) print("Melhor
pontuação = ", best.fitness.values[0]) cartPole.saveParams(best)
11. Além disso, uma vez que o desafio CartPole fornece os requisitos resolvidos
definição, vamos verificar se nossa solução atende a esses requisitos.
No momento da redação deste artigo, o desafio é considerado resolvido quando a recompensa
média for maior ou igual a 195,0 em 100 tentativas consecutivas: https:// github.com/openai/gym/
wiki/CartPole-v0#solved-requirements .
Essa definição provavelmente foi criada quando o máximo de intervalos de tempo em um único
episódio era de 200. No entanto, parece que, desde então, a duração do episódio aumentou
para 500 intervalos de tempo. Então, em vez disso, buscaremos atingir ou exceder uma média de
490,0 em 100 tentativas consecutivas. Verificaremos se o requisito foi cumprido realizando 100
testes consecutivos usando nosso melhor indivíduo e calculando a média dos resultados de todos
os testes, conforme segue:
escores = [] para
teste no intervalo(100):
escores.append(cart_pole.CartPole().getScore(best))
print("pontuações = ", pontuações)
print("Pontuação média = ", soma(pontuações) / len(pontuações))
Observe que, durante essas execuções de teste, o problema CartPole é iniciado aleatoriamente a cada vez,
portanto, cada episódio começa a partir de uma condição inicial ligeiramente diferente e pode gerar um
resultado diferente.
[ 259 ]
Machine Translated by Google
É hora de descobrir o quão bem nos saímos neste desafio. Ao executar o algoritmo por 10 gerações com um
tamanho de população de 20, obtemos o seguinte resultado:
1 15 54 17,3
...
5 16 157 63,9
6 17 500 87,2
...
9 15 500 270,9
10 13 500 420,3
A partir da saída anterior, podemos ver que, após apenas seis gerações, a(s) melhor(es) solução(ões) atingiram
a pontuação máxima de 500, equilibrando o pólo durante todo o tempo do episódio.
Olhando para os resultados do teste a seguir, parece que todos os 100 testes terminaram com uma pontuação
perfeita de 500:
...
500,0, 500,0, 500,0, 500,0]
média pontuação = 500,0
Como mencionamos anteriormente, cada uma dessas 100 execuções é feita com um ponto de partida
aleatório ligeiramente diferente. No entanto, o controlador é poderoso o suficiente para equilibrar o bastão
durante toda a corrida, todas as vezes. Para assistir ao controlador em ação, podemos reproduzir um
episódio CartPole - ou vários episódios - com os resultados que salvamos anteriormente ao iniciar o programa
cart_pole . A animação ilustra como o controlador responde dinamicamente ao movimento do bastão
aplicando ações que mantêm o bastão equilibrado no carrinho durante todo o episódio.
Se você quiser comparar esses resultados com resultados menos que perfeitos, é recomendável repetir esse
experimento com três (ou até dois) nós na camada oculta em vez de quatro - basta alterar o valor da
constante HIDDEN_LAYER de acordo na classe CartPole .
[ 260 ]
Machine Translated by Google
Resumo
Neste capítulo, você foi apresentado aos conceitos básicos do aprendizado por reforço. Depois de se
familiarizar com o kit de ferramentas OpenAI Gym, você foi apresentado ao desafio
MountainCar , em que um carro precisa ser controlado de forma a permitir que ele suba a mais alta das
duas montanhas. Depois de resolver este desafio usando algoritmos genéticos, você foi apresentado ao
próximo desafio, CartPole, onde um carrinho deve ser controlado com precisão para manter um poste
vertical equilibrado. Conseguimos resolver esse desafio combinando o poder de um controlador
baseado em rede neural com treinamento guiado por algoritmo genético.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes recursos:
[ 261 ]
Machine Translated by Google
Seção 4: Relacionado
4
tecnologias
Esta seção descreve várias técnicas de otimização relacionadas a algoritmos genéticos, bem como outros
algoritmos computacionais de inspiração biológica.
Começaremos com uma visão geral do processamento de imagens em Python e nos familiarizaremos com
três bibliotecas úteis – Pillow, scikit-image e opencv-python. Em seguida, descobriremos como uma imagem
pode ser desenhada do zero usando polígonos e como a diferença entre duas imagens pode ser calculada.
Em seguida, desenvolveremos um programa baseado em algoritmo genético para reconstruir um segmento
de uma pintura famosa usando polígonos e examinar os resultados.
Começaremos este capítulo fornecendo uma visão geral da tarefa de reconstrução de imagens.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
matplotlib
nascido no mar
Os programas que serão usados neste capítulo podem ser encontrados no repositório GitHub deste
livro em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms
with-Python/tree/master/Chapter11.
Nesses experimentos de reconstrução de imagens – cujas variações podem ser encontradas na internet –
uma imagem familiar, muitas vezes uma pintura famosa ou um trecho dela, é usada como referência. O
objetivo é construir uma imagem semelhante reunindo uma coleção de formas sobrepostas, geralmente
polígonos, de cores e transparências variadas.
Aqui, abordaremos esse desafio utilizando a abordagem de algoritmos genéticos e a biblioteca deap , assim
como fizemos para vários tipos de problemas ao longo deste livro.
No entanto, como precisaremos desenhar imagens e compará-las com uma imagem de referência, vamos nos
familiarizar com os fundamentos do processamento de imagens em Python.
[ 264 ]
Machine Translated by Google
A biblioteca Pillow usa o namespace PIL. Se você já tiver a biblioteca PIL original instalada, será necessário
desinstalá-la primeiro. Mais informações podem ser encontradas na documentação, localizada
em https://pillow.readthedocs.io/en/stable/index.
html.
[ 265 ]
Machine Translated by Google
O scikit-image vem pré-instalado com várias distribuições Python, como Anaconda e winPython. Se
precisar instalá-lo, use o utilitário pip , da seguinte forma:
A biblioteca consiste em quatro pacotes diferentes, todos usando o mesmo namespace (cv2).
Apenas um desses pacotes deve ser selecionado para ser instalado em um único ambiente. Para
nossos propósitos, podemos usar o seguinte comando, que instala apenas os módulos principais:
pip instalar opencv-python
[ 266 ]
Machine Translated by Google
'RGB' e 'RGBA' são os valores para o argumento de modo . O valor 'RGB' indica três valores de 8 bits por
pixel – um para cada uma das cores Vermelho ('R'), Verde ('G') e Azul ('B'). O valor 'RGBA' adiciona um
quarto valor de 8 bits, "A", representando o nível alfa (opacidade) dos desenhos a serem adicionados. A
combinação de uma imagem de base RGB e um desenho RGBA nos permitirá desenhar polígonos de vários
graus de transparência sobre um fundo preto.
desenhar.polígono([(x1, y1), (x2, y2), (x3, y3)], (vermelho, verde, azul, alfa))
A lista a seguir explica os termos que foram usados na declaração anterior com mais detalhes:
As tuplas (x1, y1), (x2, y2) e (x3, y3) representam os três vértices do triângulo. Cada tupla contém as
coordenadas x, y do vértice correspondente dentro da imagem. vermelho, verde e azul são valores
inteiros no intervalo de
[0, 255], cada um representando a intensidade da cor correspondente do polígono. alfa é um
valor inteiro no intervalo de [0, 255], representando o valor de opacidade do polígono (um
valor menor significa mais transparência).
Para desenhar um polígono com mais vértices, precisaríamos adicionar mais (xi , yi )
tuplas à lista.
[ 267 ]
Machine Translated by Google
Podemos adicionar mais e mais polígonos desta forma, todos desenhados na mesma imagem, possivelmente
sobrepostos uns aos outros, como mostra a imagem a seguir:
Depois de desenhar uma imagem usando polígonos, precisamos compará-la com a imagem de referência, conforme
descrito na próxima subseção.
[ 268 ]
Machine Translated by Google
Quando ambas as imagens são representadas usando a biblioteca OpenCV (cv2), esse cálculo pode ser
feito de forma bem direta, da seguinte forma:
Quando as duas imagens forem idênticas, o valor MSE será zero. Consequentemente, minimizar essa
métrica pode ser usado como objetivo do nosso algoritmo.
O módulo de métricas da biblioteca scikit-image nos fornece uma função que calcula o índice de
similaridade estrutural entre duas imagens. Quando ambas as imagens são representadas usando
a biblioteca OpenCV (cv2), esta função pode ser usada diretamente, como segue:
O valor retornado é um float no intervalo de [-1, 1], representando o índice SSIM entre as duas imagens
fornecidas. Um valor de um indica imagens idênticas.
Por padrão, esta função compara imagens em tons de cinza. Para comparar imagens coloridas, o argumento
multicanal opcional deve ser definido como verdadeiro.
[ 269 ]
Machine Translated by Google
Uma lista de tuplas, [(x1 , y1 ), (x2 , y2 ), ..., (xn , yn )], representando os vértices
do polígono. Cada tupla contém as coordenadas x, y do vértice correspondente dentro
da imagem. Portanto, os valores das coordenadas x estão no intervalo [0, largura da
imagem – 1], enquanto os valores das coordenadas y estão no intervalo [0, altura da
imagem – 1].
Três valores inteiros no intervalo de [0, 255], representando os componentes vermelho,
verde e azul da cor do polígono.
Um valor inteiro adicional no intervalo de [0, 255], representando o valor alfa – ou
opacidade – do polígono.
Isso significa que, para cada polígono em nossa coleção, precisaremos de parâmetros
[2×(tamanho do polígono) + 4]. Um triângulo, por exemplo, exigirá 10 parâmetros, enquanto
um hexágono exigirá 16 parâmetros. Consequentemente, uma coleção de triângulos será
representada usando uma lista no seguinte formato, onde cada 10 parâmetros representa um único triângulo:
[x11, y11, x12, y12 , x13, y13, r1 , g1, b1, alpha1 , x21, y21, x22, y22, x23 , y23 , r2, g2, b2, alpha2, ...]
Para simplificar essa representação, usaremos números flutuantes no intervalo de [0, 1] para cada
um dos parâmetros. Antes de desenhar os polígonos, vamos expandir cada parâmetro de acordo
para que caiba dentro do intervalo necessário – largura e altura da imagem para as coordenadas
dos vértices e [0, 255] para os valores de cores e opacidade.
[ 270 ]
Machine Translated by Google
Usando esta representação, uma coleção de 50 triângulos será representada como uma lista de
500 valores float entre 0 e 1, assim:
...
0.4551462922205506, 0.9912529573649455, 0.7882252614083617, 0.01684396868069853,
0.2353587486989349, 0.003221988752732261, 0.9998952203500615, 0.48148512088979356,
0.11555604920908047, 0.08328550982740457]
Avaliar uma determinada solução significa dividir essa longa lista em pedaços que
representam polígonos individuais – no caso de triângulos, o pedaço terá um comprimento de
10. Em seguida, precisamos criar uma nova imagem em branco e desenhar os vários polígonos
da lista em cima dela, um por um. Finalmente, a diferença entre a imagem resultante e a imagem
original (referência) precisa ser calculada. Como vimos na seção anterior, existem dois
métodos diferentes que podemos empregar para calcular a diferença entre as imagens – MSE
baseado em pixels e o índice SSIM. Este procedimento de avaliação de pontuação (um tanto
elaborado) é implementado por uma classe Python, que será descrita na próxima subseção.
A classe é inicializada com dois parâmetros: o caminho do arquivo que contém a imagem de
referência e o número de vértices dos polígonos que estão sendo usados para construir a
imagem. A classe fornece os seguintes métodos públicos:
[ 271 ]
Machine Translated by Google
plotImages(): Cria um gráfico lado a lado da imagem fornecida ao lado da imagem de referência
para fins de comparação visual. saveImage(): Aceita dados de
polígonos, cria uma imagem contendo esses polígonos, cria um gráfico lado a lado desta imagem
ao lado da imagem de referência e salva o gráfico em um arquivo.
Durante a execução do algoritmo genético, o método saveImage() será chamado a cada 100 gerações
para salvar uma comparação de imagem lado a lado representando um instantâneo do processo
de reconstrução. A chamada deste método será realizada por uma função callback, conforme descrito
na próxima subseção.
Como estamos usando uma lista de flutuadores para representar uma solução – os vértices, cores e
valores de transparência dos polígonos – este programa é muito semelhante aos programas de
otimização de funções que vimos no Capítulo 6, Otimizando funções contínuas, como o que
usamos para otimização da função Eggholder .
POLYGON_SIZE = 3
NUM_OF_POLYGONS = 100
2. Continuamos criando uma instância da classe ImageTest , que nos permitirá criar imagens
de polígonos e comparar essas imagens com a imagem de referência, bem como
salvar instantâneos de nosso progresso:
[ 272 ]
Machine Translated by Google
3. Em seguida, definimos os limites superior e inferior para os valores flutuantes que seremos
Procurando por. Como mencionamos anteriormente, usaremos valores flutuantes para todos os
nossos parâmetros e os definiremos no mesmo intervalo, entre 0,0 e 1,0, por
conveniência. Ao avaliar uma solução, os valores serão expandidos para seu intervalo real e
convertidos em números inteiros quando necessário:
5. Agora, precisamos criar uma função auxiliar que criará números reais aleatórios distribuídos
uniformemente dentro de um determinado intervalo. Esta função assume que o intervalo é o
mesmo para todas as dimensões, como é o caso da nossa solução:
6. Em seguida, usamos a função anterior para criar um operador que retorna aleatoriamente
uma lista de floats, todos no intervalo desejado de [0, 1]:
7. Isso é seguido pela definição de um operador que preenche uma instância individual usando o
operador anterior:
[ 273 ]
Machine Translated by Google
def getDiff(individual):
return imageTest.getDifference(individual, "MSE"),
toolbox.register("avaliar", getDiff)
toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR)
toolbox.register("mutar",
tools.mutPolynomialBounded,
baixo=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR,
indpb=1.0/NUM_OF_PARAMS)
[ 274 ]
Machine Translated by Google
11. Como já fizemos várias vezes antes, usaremos a abordagem elitista, onde os membros do
hall da fama (HOF) – os melhores indivíduos atuais – são sempre passados intocados
para a próxima geração. No entanto, desta vez, adicionaremos um novo recurso a essa
implementação – uma função de retorno de chamada que será usada para salvar a
imagem a cada 100 gerações. Discutiremos esse retorno de chamada com mais detalhes na
próxima subseção:
população, logbook =
elitism_callback.eaSimpleWithElitismAndCallback(population, toolbox, cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
callback=saveImage, stats=stats,
halloffame=hof, verbose=True)
12. Ao final da execução, imprimimos a melhor solução e plotamos a imagem que ela cria ao lado
da imagem de referência:
best = hof.items[0]
print("Melhor solução = ", best) print("Melhor
pontuação = ", best.fitness.values[0])
imageTest.plotImages(imageTest.polygonDataToImage(melhor))
[ 275 ]
Machine Translated by Google
Este argumento representa uma função externa que será chamada após cada
iteração.
2. A outra parte está dentro do loop principal. Aqui, a função callback é chamada depois
que a nova geração foi criada e avaliada. O número da geração atual e o melhor
indivíduo atual são passados para o callback como argumentos:
se retorno de chamada:
callback(gen, halloffame.items[0])
Ser capaz de definir uma função de retorno de chamada que será chamada após cada geração
pode ser útil em várias situações. Para aproveitá-la aqui, definiremos a função saveImage() em
nosso programa 01-reconstruct-with-polygons.py . Vamos usá-lo para salvar uma imagem lado a
lado da melhor imagem atual e da imagem de referência, a cada 100 gerações, da seguinte
forma:
1. Usamos o operador módulo (%) para ativar o método apenas uma vez a cada 100
gerações:
se gen% 100 == 0:
2. Se for uma dessas gerações, criamos uma pasta para as imagens caso não exista. As
imagens são salvas em uma pasta nomeada de acordo com o tamanho do polígono e
o número de polígonos – por exemplo, run-3-100 ou run-6-50, sob o diretório
images/results/ path:
pasta = "imagens/resultados/run-{}-
{}".format(POLYGON_SIZE, NUM_OF_POLYGONS) se não
os.path.exists(pasta): os.makedirs(pasta)
[ 276 ]
Machine Translated by Google
3. Em seguida, salvamos a imagem do melhor indivíduo atual naquela pasta. O nome da imagem contém o
número de gerações passadas; por exemplo, after-300-generations.png:
imageTest.saveImage(polygonData,
"{}/after-{}-gen.png".format(pasta, gen), "Após {}
Gerações".format(gen))
Finalmente estamos prontos para executar este algoritmo com imagens de referência e conferir os resultados.
Os polígonos que serão usados para criar a imagem serão 100 triângulos:
POLYGON_SIZE = 3
NUM_OF_POLYGONS = 100
Executaremos o algoritmo para 5.000 gerações com um tamanho de população de 200. Conforme
discutimos anteriormente, uma comparação de imagens lado a lado é salva a cada 100 gerações. Ao final da
corrida, podemos voltar e examinar as imagens salvas para acompanhar a evolução da imagem reconstruída.
[ 277 ]
Machine Translated by Google
Antes de continuarmos executando o programa, informamos que, devido ao tamanho dos dados do
polígono e à complexidade das operações de processamento de imagem, os tempos de execução de
nossos experimentos de reconstrução de imagens genéticas são muito mais longos do que os
outros programas que testamos até agora em este livro, e são normalmente várias horas por
experimento. Os resultados desses experimentos serão descritos nas subseções seguintes.
Resultados marcantes da reconstrução da Mona Lisa usando erro quadrático médio baseado em pixels - parte 1
[ 278 ]
Machine Translated by Google
Resultados marcantes da reconstrução da Mona Lisa usando erro quadrático médio baseado em pixels - parte 2
[ 279 ]
Machine Translated by Google
O resultado final tem uma grande semelhança com a imagem original, embora contenha cantos
nítidos e linhas retas, como seria de esperar de uma imagem baseada em polígonos. Apertar os
olhos enquanto olha para as imagens ajuda a desfocar essas características da imagem
reconstruída. Um efeito semelhante pode ser alcançado programaticamente usando o filtro
GaussianBlur oferecido pela biblioteca OpenCV, como segue:
Versões borradas da imagem original e reconstruída com base em pixels, lado a lado
def getDiff(individual):
return imageTest.getDifference(individual, "SSIM"),
[ 280 ]
Machine Translated by Google
Este experimento produziu os seguintes marcos lado a lado das imagens salvas:
[ 281 ]
Machine Translated by Google
[ 282 ]
Machine Translated by Google
O resultado parece interessante – ele capturou a estrutura da imagem, mas de uma forma mais
grosseira do que os resultados baseados em MSE. As cores também parecem um pouco estranhas,
já que o SSIM é mais focado na estrutura e na textura. A seguir, uma versão desfocada das imagens
finais lado a lado:
Pode ser interessante combinar os métodos de medição de diferença MSE e SSIM - você é
encorajado a experimentar por conta própria. Outras sugestões para experimentação
são descritas na próxima subseção.
POLYGON_SIZE = 6
[ 283 ]
Machine Translated by Google
Repetir esse experimento, desta vez com 10.000 gerações, produz a seguinte comparação final de
imagens lado a lado:
Resultados da reconstrução da imagem usando o erro quadrático médio baseado em pixels e um tamanho de polígono de seis
Como seria de esperar, esta imagem reconstruída parece um pouco mais refinada do que a
baseada em triângulo. O que se segue é uma versão desfocada desta imagem:
Versões borradas da imagem original e reconstruída com base em pixels, lado a lado, usando um tamanho de polígono de seis
[ 284 ]
Machine Translated by Google
Resumo
Neste capítulo, você foi apresentado ao conceito popular de reconstrução de imagens existentes usando
um conjunto de polígonos semitransparentes sobrepostos. Em seguida, você conheceu várias bibliotecas de
processamento de imagens em Python e descobriu como uma imagem pode ser desenhada do zero de
forma programática usando polígonos, além de como calcular a diferença entre duas imagens. Em
seguida, desenvolvemos um programa baseado em algoritmo genético que reconstruiu um segmento de
uma pintura famosa usando polígonos. Inúmeras possibilidades para novas experiências também foram
mencionadas.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes recursos:
[ 285 ]
Machine Translated by Google
Começaremos este capítulo desvendando a extensa família da computação evolutiva e discutindo as principais
características compartilhadas por seus membros.
Machine Translated by Google
Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:
profundo
entorpecido
rede x
Os programas que serão usados neste capítulo podem ser encontrados no repositório GitHub
deste livro em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms with-Python/tree/
master/Chapter12.
[ 287 ]
Machine Translated by Google
De forma mais ampla, como as técnicas de computação evolutiva são baseadas em vários sistemas ou
comportamentos biológicos, algumas delas se sobrepõem à família de algoritmos conhecida como computação
bioinspirada.
Nas seções a seguir, abordaremos alguns dos membros mais usados da computação evolutiva e da
computação bioinspirada – alguns deles com mais detalhes, enquanto outros serão mencionados
apenas brevemente. Começaremos com um relato detalhado de uma técnica fascinante que nos permite
desenvolver programas de computador reais – a programação genética.
programação genética
A programação genética (GP) é uma forma especial de algoritmo genético – a técnica que aplicamos ao
longo de todo este livro. Nesse caso especial, as soluções candidatas – ou indivíduos – que estamos
evoluindo com o objetivo de encontrar a melhor para o nosso propósito são programas de computador
reais, daí o nome. Em outras palavras, quando aplicamos GP, desenvolvemos programas de computador
com o objetivo de encontrar um programa que se sobressaia na execução de uma determinada tarefa.
Como você deve se lembrar, os algoritmos genéticos usam uma representação das soluções candidatas,
geralmente chamadas de cromossomos. Essa representação é o assunto dos operadores genéticos, ou
seja, seleção, cruzamento e mutação. A aplicação desses operadores à geração atual resulta em uma
nova geração de soluções que deverá produzir melhores resultados do que sua antecessora. Na maioria
dos problemas que examinamos até agora, essa representação era uma lista (ou uma matriz) de valores de
um determinado tipo, como inteiros, booleanos ou flutuantes. Para representar um programa, no entanto,
normalmente usamos uma estrutura de árvore, conforme mostrado no diagrama a seguir:
[ 288 ]
Machine Translated by Google
Terminais, ou as folhas da árvore. Estes são os argumentos e os valores constantes que podem
ser usados na árvore. Em nosso exemplo, X e Y são argumentos, enquanto 2.2, 11 e 7 são
constantes. As constantes também podem ser geradas aleatoriamente, dentro de um
determinado intervalo, quando uma árvore é criada.
Primitivos, ou os nós internos da árvore. São funções (ou operadores) que aceitam um ou
mais argumentos e geram um único valor de saída. Em nosso exemplo, +, -, * e ÷ são
primitivas que aceitam dois argumentos, enquanto cos é uma primitiva que aceita um único
argumento.
Neste exemplo, os dois pais na linha superior têm subárvores que foram trocadas entre eles para criar
os dois descendentes na segunda linha. As subárvores trocadas são marcadas pelos retângulos ao seu
redor.
[ 289 ]
Machine Translated by Google
Na mesma linha, o operador de mutação, que pretende introduzir mudanças aleatórias em um único indivíduo,
pode ser implementado escolhendo uma subárvore dentro da solução candidata e substituindo-a por outra
gerada aleatoriamente.
A biblioteca deap , que usamos ao longo deste livro, oferece suporte inerente à programação genética.
Na próxima seção, implementaremos um exemplo simples de programação genética usando esta biblioteca.
Esse tipo de tabela costuma ser chamado de tabela-verdade da operação em questão. Como fica evidente
nesta tabela de verdade, uma das razões pelas quais a verificação de paridade costuma ser usada como
referência é que qualquer alteração nos valores de entrada resultará em uma alteração no valor de saída.
A verificação de paridade também pode ser representada usando portas lógicas, como AND, OR, NOT e
XOR (OU Exclusivo ). Enquanto a porta NOT aceita uma única entrada e a inverte, cada um dos outros três
tipos de porta aceita duas entradas. Para que a respectiva saída seja 1, a porta AND exige que ambas as
entradas sejam 1, a porta OR exige que pelo menos uma delas seja 1 e a porta XOR exige que exatamente
uma delas seja 1, conforme a tabela a seguir :
in_0 em 1 E OU LIVRE
0 0 0 0 0
0 1 0 1 1
[ 290 ]
Machine Translated by Google
1 0 0 1 1
1 1 1 1 0
Existem muitas maneiras possíveis de implementar a verificação de paridade de três entradas usando portas lógicas.
A maneira mais simples de fazer isso é usando duas portas XOR, conforme mostrado no diagrama a seguir:
Verificação de paridade par de três entradas implementada usando duas portas XOR
Na próxima subseção, usaremos a programação genética para criar um pequeno programa que implementa a
verificação de paridade par usando as operações lógicas de AND, OR, NOT e XOR.
Como a programação genética é um caso especial de algoritmos genéticos, grande parte deste programa lhe
parecerá familiar se você tiver examinado os programas que apresentamos nos capítulos anteriores deste
livro.
NUM_INPUTS = 3
NUM_COMBINATIONS = 2 ** NUM_INPUTS
[ 291 ]
Machine Translated by Google
2. Isso é seguido pelas conhecidas constantes do algoritmo genético que vimos várias vezes
antes:
POPULATION_SIZE = 60
P_CROSSOVER = 0,9
P_MUTATION = 0,5
MAX_GENERATIONS = 20
HALL_OF_FAME_SIZE = 10
MIN_TREE_HEIGHT = 3
MAX_TREE_HEIGHT = 5
MUT_MIN_TREE_HEIGHT = 0
MUT_MAX_TREE_HEIGHT = 2
LIMIT_TREE_HEIGHT = 17
4. Em seguida, calculamos a tabela verdade da verificação de paridade par para que possamos usá-la
como referência quando precisarmos verificar a precisão de uma determinada solução candidata.
A matriz parityIn representa as colunas de entrada da tabela verdade, enquanto o vetor parityOut
representa a coluna de saída. A função itertools.product() do Python é uma
substituição elegante de loops for aninhados que, de outra forma, seriam necessários para iterar
todas as combinações de valores de entrada:
parityOut.append(sum(row) % 2)
5. É hora de criar o conjunto de primitivas, ou seja, os operadores que serão utilizados em nossos
programas evoluídos. A primeira declaração cria um conjunto usando os três argumentos a
seguir:
O nome do programa a ser gerado usando as primitivas do conjunto (aqui,
chamamos de principal)
O número de entradas para o programa
O prefixo a ser usado ao nomear as entradas (opcional)
Esses três argumentos são usados para criar o seguinte conjunto primitivo:
[ 292 ]
Machine Translated by Google
6. Agora, preenchemos o conjunto primitivo com as várias funções (ou operadores) que serão
usadas como blocos de construção do programa. Para cada operador, usamos uma
referência à função que queremos usar e o número de argumentos que ela espera.
Embora pudéssemos definir nossas próprias funções para esse fim, neste caso, estamos
utilizando o módulo operador Python existente , que contém inúmeras funções úteis, incluindo os
operadores lógicos de que precisamos:
primitivoSet.addPrimitive(operator.and_, 2)
primitivoSet.addPrimitive(operator.or_, 2)
primitivoSet.addPrimitive(operator.xor, 2)
primitivoSet.addPrimitive(operator.not_, 1)
primitivoSet.addTerminal(1)
primitivoSet.addTerminal(0)
8. Como nosso objetivo é criar um programa que implemente a tabela verdade da verificação de
paridade par, tentaremos minimizar a diferença entre a saída do programa e os valores de
saída conhecidos. Para isso, definiremos um único objetivo, minimizando a estratégia de
aptidão:
10. Para nos ajudar a construir um indivíduo na população, criaremos uma função auxiliar que
gerará árvores aleatórias usando o conjunto primitivo que definimos anteriormente. Aqui,
estamos utilizando a função genFull() oferecida por deap e fornecendo a ela o conjunto primitivo,
bem como os valores para definir a altura mínima e máxima das árvores geradas:
[ 293 ]
Machine Translated by Google
11. Isso é seguido pela definição de dois operadores, o primeiro dos quais cria um
instância individual usando o operador auxiliar anterior. O outro gera uma lista de tais
indivíduos:
12. Em seguida, criamos um operador para compilar a árvore primitiva em código Python usando
a função compile() oferecida por deap. Consequentemente, usamos este operador de
compilação em uma função que iremos criar, chamada parityError(). Esta função
conta o número de linhas na tabela verdade para as quais o resultado do cálculo
difere do esperado:
toolbox.register("avaliar", getCost)
14. É hora de escolher nossos operadores genéticos, começando pelo operador de seleção
(alias com select). Para a programação genética, esse operador normalmente é a mesma
seleção de torneio que usamos ao longo deste livro. Aqui, estamos usando com um
tamanho de torneio de 2:
15. Quanto ao operador de cruzamento (alias com mate), usamos o operador especializado de
programação genética cxOnePoint() fornecido por deap. Como os programas em
evolução são representados por árvores, esse operador pega duas árvores-pai e troca
seções delas para criar duas árvores descendentes válidas:
toolbox.register("mate", gp.cxOnePoint)
[ 294 ]
Machine Translated by Google
16. Em seguida, vem o operador de mutação, que introduz alterações aleatórias em uma árvore
existente. A mutação é definida em dois estágios. Primeiro, especificamos um operador
auxiliar que utiliza a função genGrow() de programação genética especializada , fornecida por
deap. Este operador cria uma subárvore dentro dos limites definidos pelas duas constantes.
Em seguida, definimos o próprio operador de mutação (alias com mutate).
Este operador utiliza a função mutUniform() do DEAP , que substitui aleatoriamente uma
subárvore em uma determinada árvore por uma aleatória que foi gerada usando o
operador auxiliar:
toolbox.register("expr_mut", gp.genGrow,
min_=MUT_MIN_TREE_HEIGHT, max_=MUT_MAX_TREE_HEIGHT)
toolbox.register("mutate", gp.mutUniform, expr=toolbox.expr_mut,
pset=primitiveSet)
17. Para evitar que indivíduos na população cresçam em árvores excessivamente grandes,
potencialmente contendo um número excessivo de primitivos, precisamos introduzir
medidas de controle de inchaço . Isso é feito usando a função staticLimit() do DEAP ,
que impõe uma restrição de altura da árvore nos resultados das operações de
cruzamento e mutação:
toolbox.decorate("mate",
gp.staticLimit(key=operator.attrgetter("height"), max_value=LIMIT_TREE_HEIGHT))
toolbox.decorate("mutate",
gp.staticLimit(key=operator.attrgetter("height") ,
max_value=LIMIT_TREE_HEIGHT))
18. O loop principal do programa é muito semelhante aos que vimos nos capítulos anteriores.
Depois de criar a população inicial, definir as medidas estatísticas e criar o objeto HOF,
chamamos de algoritmo evolutivo. Como já fizemos várias vezes antes, aplicamos a
abordagem elitista, onde os membros do HOF – os melhores indivíduos atuais – são
sempre passados intocados para a próxima geração:
cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats,
halloffame=hof,
verbose=True)
[ 295 ]
Machine Translated by Google
19. Ao final da corrida, imprimimos a melhor solução, bem como a altura da árvore que está
sendo utilizada para representá-la e seu comprimento; ou seja, o número total de
operadores contidos na árvore:
20. A última coisa que precisamos fazer é plotar uma ilustração gráfica da árvore que
representa a melhor solução. Para isso, utilizamos a biblioteca de grafos e
redes networkx (nx), que apresentamos no Capítulo 5, Satisfação de restrições.
Começamos chamando a função graph() fornecida por deap, que divide a árvore
individual em nós, arestas e rótulos necessários para o gráfico e, em seguida, criamos
o gráfico usando as funções networkx apropriadas:
21. Em seguida, desenhamos os nós, arestas e rótulos. Como o layout deste gráfico não é
uma árvore hierárquica clássica, distinguimos o nó superior colorindo-o de
vermelho e ampliando-o:
nx.draw_networkx_edges(g, pos)
nx.draw_networkx_labels(g, pos, **{"labels": labels, "font_size": 8})
-- Melhor Indivíduo = xor(and_(not_(and_(in_1, in_2)), not_(and_(1, in_2))), xor(or_(xor(in_1, in_0), and_(0, 0)), 1 ))
[ 296 ]
Machine Translated by Google
-- comprimento=19, altura=4 --
Melhor condicionamento físico = 0,0
Gráfico representando a solução de verificação de paridade que foi encontrada pelo programa inicial
A razão para o gráfico relativamente complexo é que não há vantagem em usar expressões mais
simples. Contanto que estejam dentro da limitação imposta de altura da árvore, as
expressões que são avaliadas não incorrem em penalidade por complexidade. Na próxima
subseção, tentaremos mudar essa situação introduzindo uma pequena modificação no programa na
esperança de alcançar o mesmo resultado – a implementação da verificação de paridade par –
mas com uma solução mais simples.
[ 297 ]
Machine Translated by Google
Este programa é quase idêntico ao anterior, exceto por algumas pequenas alterações:
1. A principal alteração foi introduzida na função custo, que o algoritmo procura minimizar.
Ao erro calculado originalmente, foi adicionada uma pequena medida de penalidade que
depende da altura da árvore:
2. A única outra alteração foi no final da corrida, após imprimir a melhor solução encontrada.
Aqui, adicionamos uma impressão do erro de paridade real obtido, sem a penalidade
presente no fitness:
[ 298 ]
Machine Translated by Google
A partir da saída anterior, podemos dizer que, após cinco gerações, o algoritmo foi capaz de encontrar
uma solução que reproduz corretamente a tabela verdade de verificação de paridade uniforme,
pois o valor de aptidão naquele ponto era quase 0. No entanto, à medida que o algoritmo continuava
em execução, a altura das árvores foi reduzida de quatro (penalidade de 0,04) para duas (penalidade de
0,02). Como resultado, a melhor solução é muito simples e consiste em apenas cinco elementos – as
três entradas e dois operadores XOR. Na verdade, a solução que encontramos representa a solução
conhecida mais simples que vimos anteriormente, que consiste em duas portas XOR. Isso é
ilustrado pela seguinte trama, produzida pelo programa:
Gráfico representando a solução de verificação de paridade que foi encontrada pelo programa modificado
[ 299 ]
Machine Translated by Google
O algoritmo PSO é iterativo e, a cada iteração, a posição de cada partícula é avaliada e sua melhor
localização até o momento, bem como a melhor localização dentro de todo o grupo de partículas, são atualizadas,
se necessário. Então, a velocidade de cada partícula é atualizada de acordo com as seguintes informações:
Isso é seguido por uma atualização da posição da partícula, com base na velocidade recém-calculada.
O processo iterativo continua até que alguma condição de parada, como o limite de iterações, seja atendida.
Neste ponto, a melhor posição atual do grupo é tomada como solução pelo algoritmo.
Esse processo simples, mas eficiente, será ilustrado em detalhes na próxima seção, onde veremos um
programa que otimiza uma função usando o algoritmo PSO.
[300]
Machine Translated by Google
Função de Himmelblau
Fonte: https://commons.wikimedia.org/wiki/File:Himmelblau_function.svg
Imagem de Morn the Gorn. Liberado para o domínio público
Tem quatro mínimos globais, avaliando a 0, indicados pelas áreas azuis no gráfico. Situam-se nas
seguintes coordenadas:
x=3,0, y=2,0
x=ÿ2,805118, y=3,131312
[ 301 ]
Machine Translated by Google
x=ÿ3,779310, y=ÿ3,283186
x=3,584458, y=ÿ1,848126
DIMENSÕES = 2
POPULATION_SIZE = 20
MAX_GENERATIONS = 500
2. Estas são seguidas por várias constantes adicionais que afetam como as partículas
são criados e atualizados. Veremos como eles desempenham seus papéis enquanto examinamos
o restante do programa:
[ 302 ]
Machine Translated by Google
4. Agora, precisamos criar a classe Particle . Como essa classe representa um local no
espaço contínuo, podemos baseá-la em uma lista comum de floats. No entanto, aqui,
decidimos usar a matriz N-dimensional da biblioteca numpy (ndarray) , pois ela se
presta a operações algébricas elementares, como adição e multiplicação,
que serão necessárias quando atualizarmos a localização da partícula.
Além da localização atual, a classe Particle recebe vários atributos adicionais:
aptidão,
usando a aptidão minimizadora que definimos anteriormente.
velocidade, que é usada para manter a velocidade atual da partícula em
cada dimensão. Embora seu valor inicial seja None, a velocidade
será preenchida com outro ndarray
posteriormente. best, que representa a melhor localização registrada até
agora para essa partícula específica (melhor local).
5. Para nos ajudar a construir uma partícula individual na população, precisamos definir
uma função auxiliar que criará e inicializará uma partícula aleatória. Usaremos a
função random.uniform() da biblioteca numpy para gerar aleatoriamente as matrizes
de localização e velocidade da nova partícula, dentro dos limites fornecidos:
def createParticle(): partícula =
criador.Particle(np.random.uniform(
MIN_START_POSITION,
MAX_START_POSITION,
DIMENSIONS))
partícula.velocidade = np.random.uniform(MIN_SPEED, MAX_SPEED, DIMENSIONS)
partícula de retorno
[ 303 ]
Machine Translated by Google
O método começa criando dois fatores aleatórios – um para a atualização local e outro para a atualização
global – dentro do intervalo predefinido. Em seguida, ele calcula duas atualizações de velocidade
correspondentes (local e global) e as adiciona à velocidade atual da partícula.
Observe que todos os valores envolvidos são do tipo ndarray , são bidimensionais em nosso
caso e os cálculos são executados elemento a elemento, um por dimensão.
localUpdateFactor = np.random.uniform(0,
MAX_LOCAL_UPDATE_FACTOR,
partícula.tamanho)
globalUpdateFactor = np.random.uniform(0,
MAX_GLOBAL_UPDATE_FACTOR,
partícula.tamanho)
8. O método updateParticle() continua certificando-se de que a nova velocidade não exceda os limites
predefinidos e atualiza a localização das partículas usando a velocidade atualizada. Como mencionamos
anteriormente, tanto a localização quanto a velocidade são do tipo ndarray e possuem componentes
separados para cada dimensão:
9. Em seguida, registramos o método updateParticle() como um operador de caixa de ferramentas que estará
no loop principal posteriormente:
toolbox.register("atualizar", atualizarParticle)
[ 304 ]
Machine Translated by Google
11. Agora que finalmente chegamos ao método main() , podemos iniciá-lo criando a
população de partículas:
população = toolbox.populationCreator(n=POPULATION_SIZE)
12. Antes de iniciar o loop principal do algoritmo , precisamos criar o objeto stats , em
para calcular as estatísticas da população, e o objeto logbook , para registrar as
estatísticas a cada iteração:
stats = tools.Statistics(lambda ind: ind.fitness.values) stats.register("min", np.min)
stats.register("avg", np.mean)
13. O loop principal do programa contém um loop externo que itera sobre o
gerações/ciclos de atualização. Dentro de cada iteração, existem dois loops
secundários, cada um iterando sobre todas as partículas da população. O primeiro
loop, que pode ser visto no código a seguir, avalia cada partícula em relação à
função a ser otimizada e atualiza o melhor local e o melhor global, se necessário:
partícula.fitness.values = toolbox.evaluate(partícula)
# melhor local:
se partícula.best for Nenhum ou partícula.best.size == 0 ou partícula.best.fitness <
partícula.fitness:
partícula.best = criador.Particle(partícula) partícula.best.fitness.values
= partícula.fitness.values
# melhor global: se o
melhor for None ou best.size == 0 ou best.fitness <
partícula.fitness: melhor =
criador.Particle(particle) best.fitness.values =
partícula.fitness.values
[ 305 ]
Machine Translated by Google
14. O segundo loop interno chama o operador de atualização . Como vimos anteriormente, este
operador atualiza a velocidade e a localização da partícula usando uma combinação de
inércia, força cognitiva e força social:
toolbox.update(partícula, melhor)
16. Uma vez concluído o loop externo, imprimimos as informações para o melhor local que foi
registrado durante a execução. Esta é considerada a solução que o algoritmo encontrou para
o problema em questão:
Esses resultados indicam que o algoritmo foi capaz de localizar um dos mínimos, em torno de x=ÿ3,77
e y=ÿ3,28. Olhando para as estatísticas que registramos ao longo do caminho, podemos ver que o melhor
resultado foi alcançado na geração 480. Também é evidente que as partículas se movimentam bastante e,
durante a corrida, oscilam mais perto do melhor resultado e se afastando de isto.
Para encontrar os outros locais mínimos, você pode reexecutar o algoritmo com uma semente aleatória
diferente. Você também pode penalizar as soluções nas áreas em torno dos mínimos encontrados
anteriormente, assim como fizemos com a função de Simionescu no Capítulo 6, Otimizando funções contínuas.
Outra abordagem poderia ser o uso de vários enxames simultâneos para localizar vários mínimos na
mesma corrida – você é encorajado a tentar isso por conta própria (consulte a seção Leitura adicional
para obter mais informações).
[ 306 ]
Machine Translated by Google
Estratégias de evolução
As estratégias de evolução (ES) são um tipo de algoritmo genético que enfatiza a mutação em
vez do cruzamento como o facilitador evolutivo. A mutação é adaptativa e sua força é aprendida ao
longo das gerações. O operador de seleção na estratégia de evolução é sempre baseado em
classificação, em vez de ser feito usando valores de aptidão reais. Uma versão simples desta
técnica é chamada (1 + 1). Inclui apenas dois indivíduos - um pai e sua prole mutante. O melhor deles
continua a ser o pai do próximo descendente mutante. No caso mais geral, chamado (1 + ÿ), há um
progenitor e descendentes ÿ mutantes, e o melhor dos descendentes continua a ser o progenitor
do próximo descendente ÿ. Algumas variações mais recentes do algoritmo incluem mais de um pai,
bem como um operador de cruzamento.
evolução diferencial
A evolução diferencial (DE) é uma variante especializada de algoritmos genéticos usada para a
otimização de funções de valor real. DE difere de algoritmos genéticos nos seguintes aspectos:
[ 307 ]
Machine Translated by Google
Os algoritmos ACO usam formigas artificiais que se movem no espaço de busca procurando a localização
das melhores soluções. As formigas acompanham suas localizações e as soluções candidatas que
encontraram ao longo do caminho. Esta informação é utilizada pelas formigas das iterações
subsequentes para que possam encontrar melhores soluções. Esses algoritmos são frequentemente
combinados com o método de busca local, que é ativado após a localização de uma área de interesse.
O AIS recente pode ser usado em várias tarefas de aprendizado de máquina e otimização e geralmente
pertence a um dos três subcampos a seguir:
Seleção clonal: imitando o processo pelo qual o sistema imunológico seleciona a melhor célula
para reconhecer e eliminar um antígeno que entra no corpo. A célula é escolhida de um pool
de células preexistentes com especificidades variadas e, uma vez escolhida, é clonada
para criar uma população de células que elimina o antígeno invasor. Esse paradigma é
normalmente aplicado a tarefas de otimização e reconhecimento de padrões.
Seleção negativa: segue um processo que identifica e exclui células que podem atacar os
tecidos próprios. Esses algoritmos são normalmente usados em tarefas de detecção de anomalias,
onde padrões normais são usados para treinar "negativamente" filtros que serão capazes de
detectar padrões anômalos.
[ 308 ]
Machine Translated by Google
Algoritmos de rede imune: é inspirado na teoria que sugere que o sistema imunológico é
regulado usando tipos especiais de anticorpos que se ligam a outros anticorpos. Nesse
tipo de algoritmo, os anticorpos representam nós em uma rede e o processo de
aprendizado envolve a criação ou remoção de arestas entre os nós, resultando em uma
estrutura de grafo de rede em evolução. Esses algoritmos são normalmente usados em
tarefas de aprendizado de máquina não supervisionadas, bem como nas áreas de controle
e otimização.
vida artificial
Em vez de ser um ramo da computação evolutiva, a vida artificial (ALife) é um campo mais amplo que
envolve sistemas e processos que imitam a vida natural de diferentes maneiras, como simulações de
computador e sistemas robóticos.
A computação evolutiva pode, na verdade, ser vista como uma aplicação do ALife, onde a população
que busca otimizar uma determinada função de aptidão é uma metáfora para organismos em
busca de comida. Os mecanismos de nicho e compartilhamento, por exemplo, que descrevemos no
Capítulo 2, Compreendendo os principais componentes dos algoritmos genéticos, são extraídos
diretamente da metáfora da comida.
O ALife também pode ser visto como a contraparte ascendente da inteligência artificial, já que o ALife
normalmente se baseia no ambiente biológico, nos mecanismos e nas estruturas, em vez da cognição
de alto nível.
Resumo
Neste capítulo, você foi apresentado à extensa família da computação evolutiva e a algumas das
características comuns de seus membros. Em seguida, usamos programação genética –
um caso especial de algoritmos genéticos – para implementar a tarefa de verificação de paridade par.
Isso foi seguido pela criação de um programa que utilizava a técnica de otimização de enxame de partículas
para otimizar a função de Himmelblau. Concluímos este capítulo com uma breve visão geral de
várias outras técnicas de solução de problemas relacionadas.
[ 309 ]
Machine Translated by Google
Agora que este livro chegou ao fim, gostaria de agradecê-lo por fazer esta jornada comigo e
examinar os vários aspectos e casos de uso de algoritmos genéticos e computação
evolutiva. Espero que você tenha achado este livro interessante, bem como instigante. Como
este livro demonstrou, os algoritmos genéticos e suas técnicas relacionadas podem ser aplicados a
uma infinidade de tarefas em praticamente qualquer área de computação e engenharia,
incluindo – muito provavelmente – aquelas com as quais você está envolvido atualmente. Lembre-
se, tudo o que é necessário para o algoritmo genético começar a processar um problema é
uma forma de representar uma solução e uma forma de avaliar uma solução – ou comparar duas
soluções. Como esta é a era da inteligência artificial e da computação em nuvem, você descobrirá
que os algoritmos genéticos se prestam bem a ambos e podem ser uma ferramenta poderosa em
seu arsenal ao abordar um novo desafio.
Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes
recursos:
[ 310 ]
Machine Translated by Google
ISBN: 978-1-83882-491-4
Descubra os algoritmos de neuroevolução mais populares – NEAT, HyperNEAT e ES-
HyperNEAT
Explore como implementar algoritmos baseados em neuroevolução em Python
Familiarize-se com ferramentas avançadas de visualização para examinar gráficos de redes
neurais evoluídas
Compreender como examinar os resultados de experimentos e analisar o desempenho do
algoritmo
Mergulhe nas técnicas de neuroevolução para melhorar o desempenho dos métodos
existentes
Aplicar neuroevolução profunda para desenvolver agentes para jogar jogos de Atari
Machine Translated by Google
ISBN: 9-781-78995-617-7
Abrange arquiteturas de rede neural avançadas e de última geração
[ 312 ]
Machine Translated by Google
[ 313 ]
Machine Translated by Google
Índice
[ 315 ]
Machine Translated by Google
operadores genéticos
G
criando 60, 61 para
analogia de algoritmos genéticos números reais 159
sobre 10
programação genética (GP) sobre
cruzamento 12
288, 289 exemplo
função de condicionamento físico 11
290, 291 implementando
genótipo 10 291, 292, 293, 294, 295, 296,
mutação 12, 13 297
população 11 solução, simplificando 298, 299
seleção 11 genótipo 10, 98
algoritmos genéticos, limitações sobre Gradient Boosting Regressor (GBR) 195 gradiente
20 aplicação descendente 16 problema
21 computação de coloração de gráfico 146, 147, 148 solução de
intensiva 21 ajuste de hiperparâmetros algoritmos genéticos 151, 152, 153, 154,
21 sem solução garantida 22 155, 156
convergência prematura 21 restrições rígidas, usando 149
algoritmos genéticos sobre 9 Representação de problemas em Python 149, 150, 151
restrições suaves, usando 149
resolvendo 146
vantagens 17 e Unidades de processamento gráfico (GPUs) 227
aprendizado por reforço 243 retorno de pesquisa em grade 210
chamada, adicionando 275
problemas complexos, lidando com 19 H
aprendizado contínuo 20
hall da fama (HOF) 77, 134, 199, 234 restrições
evolução darwiniana 9, 10 rígidas 138
Função eggholder, otimizando 163, 165, 166 otimização função do céu azul
global 18
niching, para encontrar várias soluções 174, 175, 176,
Função de Himmelblau, otimizando 170, 172, 174 implementando 178
272, 274, 275 falta de representação otimizando 169, 170
matemática, manipulando 19 otimizando, com algoritmos genéticos 170, 172,
Arquitetura MLP, otimizando com 232, 233, 235 174
Configuração combinada de MLP, otimizando com 237, compartilhamento, para encontrar várias soluções 174, 176,
239 178 ajuste de hiperparâmetros, combinando com otimização de
paralelismo 20 arquitetura
resiliência, ao ruído 19 cerca de 235
teorema do esquema 14, 15 precisão do classificador, avaliando 236
teoria 13, 14 representação de solução 236
casos de uso 22 hiperparâmetros, ajuste com abordagem genética direta
usado, para reconstrução de imagens 270 cerca de
usado, para representação de problemas em Python 271 216, 219, 220, 221 precisão do
usado, para representação e avaliação de soluções classificador, avaliando 218 representação
270, 271 de hiperparâmetro 217 hiperparâmetros,
usado, para resolver problemas 50, 51 ajustando com pesquisa de grade genética sobre 212, 213,
pesquisa de grade 214 desempenho padrão
genética usada, para ajustar hiperparâmetros 212, 213, 214 do classificador, testando 214
[ 316 ]
Machine Translated by Google
algoritmos genéticos de
Link de referência do ambiente
reconstrução de imagem, usando 270
MountainCar-v0 247
resultados 277
Multilayer Perceptron (MLP) 226, 254 mutação 27
k mutação de troca 40
[ 317 ]
Machine Translated by Google
[ 318 ]
Machine Translated by Google
[ 319 ]
Machine Translated by Google