Você está na página 1de 184

Machine Translated by Google

Satisfação de restrição capítulo 5

Resolvendo o problema de agendamento de enfermagem


Imagine que você é responsável por agendar os turnos das enfermeiras do departamento do hospital para esta semana.
Há três turnos por dia – manhã, tarde e noite – e para cada turno, você precisa designar uma ou mais das oito enfermeiras que
trabalham em seu departamento. Se isso parece uma tarefa simples, dê uma olhada na lista de regras hospitalares
relevantes:

Uma enfermeira não pode trabalhar dois turnos consecutivos.

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:

Turno da manhã: 2–3 enfermeiros


Turno da tarde: 2 a 4 enfermeiras

Turno da noite: 1–2 enfermeiras

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

Satisfação de restrição capítulo 5

Isso pode ser dividido nos seguintes grupos de três valores, representando os turnos que esta
enfermeira trabalhará em cada dia da semana:

Domingo Segunda- Terça- Quarta-feira Quinta-feira Sexta-feira Sábado


feira (0, 1, 0) (1, 0, 1) tarde feira (0, 0, 0) (0, 0, 1) (1, 0, 0) (0, 1, 0) (nenhum)
manhã e noite (0, 1, 1) tarde e noite noite manhã tarde

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.

Restrições rígidas versus restrições suaves


Ao resolver o problema de agendamento de enfermagem, devemos ter em mente que algumas das
restrições representam regras hospitalares que não podem ser quebradas. Uma programação que
contenha uma ou mais violações dessas regras será considerada inválida. De forma mais geral, são
conhecidas como restrições rígidas.

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

Satisfação de restrição capítulo 5

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:

Encontre uma representação específica (codificação) da solução que elimine a possibilidade


de uma violação de restrição rígida. Ao resolver o problema N-Queens, conseguimos representar
uma solução de forma que eliminou a possibilidade de duas das três restrições – linha e coluna,
o que simplificou consideravelmente nossa solução. Mas, geralmente, essa codificação pode
ser difícil de encontrar.
Ao avaliar as soluções, descarte soluções candidatas que violem qualquer restrição rígida. A
desvantagem dessa abordagem é a perda de informações contidas nessas soluções, que podem
ser valiosas para o problema. Isso pode retardar consideravelmente o processo de
otimização.
Ao avaliar as soluções, repare as soluções candidatas que violam qualquer restrição rígida. Em
outras palavras, encontre uma maneira de manipular a solução e modificá-la para que ela não
viole mais a(s) restrição(ões). A criação desse procedimento de reparo pode ser difícil ou impossível
para a maioria dos problemas e, ao mesmo tempo, o processo de reparo pode resultar em perda
significativa de informações.
Ao avaliar as soluções, penalize as soluções candidatas que violam qualquer restrição rígida.
Isso degradará a pontuação da solução e a tornará menos desejável, mas não a eliminará
completamente, portanto, as informações contidas nela não serão perdidas.
Efetivamente, isso leva a que uma restrição hard seja considerada semelhante a uma soft
constraint, mas com uma penalidade mais pesada. Ao usar esse método, o desafio pode ser
encontrar a extensão apropriada da penalidade. Uma penalidade muito severa pode levar à
eliminação de fato de tais soluções, enquanto uma penalidade muito pequena pode fazer com
que essas soluções pareçam ótimas.

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

Satisfação de restrição capítulo 5

Representação do problema Python


Para encapsular o problema de agendamento de enfermagem que descrevemos no início desta seção,
criamos uma classe Python chamada NurseSchedulingProblem. Esta classe está contida no arquivo
Nurses.py , que pode ser encontrado em https://github.com/PacktPublishing/Hands-On Genetic-
Algorithms-with-Python/blob/master/Chapter05/nurses.py.

O construtor da classe aceita o parâmetro hardConstraintPenalty , que representa o fator de penalidade


para uma violação de restrição rígida. Em seguida, continua a inicializar os vários parâmetros,
descrevendo o problema de escalonamento:

# 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]

# turnos máximos por semana permitidos para cada enfermeira


self.maxShiftsPerWeek = 5

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)

Os seguintes métodos são usados para contar os vários tipos de violações:

countConsecutiveShiftViolations(nurseShiftsDict)

countShiftsPerWeekViolations(nurseShiftsDict)

countNursesPerShiftViolations(nurseShiftsDict)

countShiftPreferenceViolations(nurseShiftsDict)

[ 140 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

Além disso, a classe fornece os seguintes métodos públicos:

getCost(schedule): Calcula o custo total das várias violações no cronograma fornecido.


Este método usa o valor da variável hardConstraintPenalty .

printScheduleInfo(schedule): Imprime os detalhes do cronograma e das violações.

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]

Agenda para cada enfermeira:

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

Turnos semanais = [9, 13, 13, 12, 15, 5, 10, 9]


Violações de turnos por semana = 46

Enfermeiros por turno = [4, 2, 4, 1, 6, 6, 4, 4, 5, 4, 5, 5, 2, 4, 4, 4, 5, 3, 4, 3, 7]

Enfermeiras por Violações de Turno = 30

Violações de preferência de turno = 30

Custo Total = 1190

[ 141 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

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.

Solução de algoritmos genéticos


Para resolver o problema de agendamento de enfermagem usando um algoritmo genético, criamos o programa Python
chamado 02-solve-nurses.py, que está localizado em https://github.com/PacktPublishing/Hands-On-Genetic-
Algorithms-with-Python /blob/master/Capítulo05/
02-solve-nurses.py.

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.

As principais partes da nossa solução são descritas nas seguintes etapas:

1. Nosso programa começa criando uma instância da classe NurseSchedulingProblem com o


valor desejado para hardConstraintPenalty, que é definido pela constante
HARD_CONSTRAINT_PENALTY :

nsp = enfermeiras.NurseSchedulingProblem(HARD_CONSTRAINT_PENALTY)

2. Como nosso objetivo é minimizar o custo, definimos um único objetivo, minimizar


estratégia de condicionamento físico:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

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:

toolbox.register("zeroOrOne", random.randint, 0, 1) toolbox.register("individualCreator",


tools.initRepeat, criador.Individual, toolbox.zeroOrOne, len(nsp))
toolbox.register("populationCreator", ferramentas .initRepeat, lista, caixa de
ferramentas.individualCreator)

[ 142 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

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:

def getCost(individual): return


nsp.getCost(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:

toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate",


tools.cxTwoPoint) toolbox.register("mutate", tools.mutFlipBit,
indpb=1.0/len(nsp))

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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,


cxpb=P_CROSSOVER, mutpb=P_MUTATION,
ngen=MAX_GENERATIONS, stats=stats, halloffame=hof, verbose=True)

7. Quando o algoritmo termina, imprimimos os detalhes da melhor solução que foi


encontrado:

nsp.printScheduleInfo(melhor)

Antes de executarmos o programa, vamos definir as constantes do algoritmo, como segue:

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

Satisfação de restrição capítulo 5

A execução do programa com essas configurações produz a seguinte saída:


-- Melhor condicionamento físico = 3,0

-- 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

Turnos semanais = [5, 6, 2, 5, 4, 5, 5, 5]


Violações de turnos por semana = 1

Enfermeiros por turno = [2, 2, 1, 2, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 1]

Enfermeiras por Violações de Turno = 0

Violações de preferência de turno = 2

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

Agora, o resultado é o seguinte:

-- Melhor condicionamento físico = 3,0

-- 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

Satisfação de restrição capítulo 5

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

Turnos semanais = [4, 5, 5, 5, 3, 5, 5, 5]


Violações de turnos por semana = 0

Enfermeiros por turno = [2, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 1, 2, 2, 1, 2, 2, 2, 2, 2, 1]

Enfermeiras por Violações de Turno = 0

Violações de preferência de turno = 3

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:

Estatísticas do programa resolvendo o problema de agendamento de enfermagem

[ 145 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

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.

Resolvendo o problema de coloração de grafos


No ramo matemático da teoria dos grafos, um grafo é uma coleção estruturada de objetos que representa as
relações entre pares desses objetos. Os objetos aparecem como vértices (ou nós) no grafo, enquanto a
relação entre um par de objetos é representada por uma aresta. Uma maneira comum de ilustrar um gráfico
é desenhando os vértices como círculos e as arestas como linhas de conexão, conforme representado no
diagrama a seguir do gráfico de Petersen, em homenagem ao matemático dinamarquês Julius Petersen:

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

Satisfação de restrição capítulo 5

O diagrama a seguir mostra o mesmo gráfico de Petersen, mas desta vez colorido corretamente:

Coloração adequada do gráfico de Petersen. Fonte: https://en.wikipedia.org/wiki/File:Petersen_graph_3-coloring.svg


Liberado para domínio público

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

Satisfação de restrição capítulo 5

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)

Vamos falar sobre o que temos aqui em detalhes:

Quatro cores são usadas, representadas pelos inteiros 0, 1, 2, 3.


O primeiro, sétimo e décimo nós do gráfico são coloridos com a primeira cor (0).

O terceiro e quinto nós são coloridos com a segunda cor (1).


O segundo e sexto nós são coloridos com a terceira cor (2).
O quarto, oitavo e nono nós são coloridos com a quarta cor (3).

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

Satisfação de restrição capítulo 5

Usando restrições rígidas e flexíveis para o


problema de coloração de grafos
Ao resolver o problema de agendamento de enfermagem no início deste capítulo, observamos a
diferença entre restrições rígidas – aquelas às quais temos de aderir para que a solução seja
considerada válida – e restrições flexíveis – aquelas que nos esforçamos para minimizar para
obter a melhor solução. No problema de coloração de grafos, o requisito de atribuição de cores –
onde dois nós adjacentes não podem ter a mesma cor – é uma restrição difícil. Temos que
minimizar o número de violações dessa restrição a zero para obter uma solução válida.

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.

Como fizemos no problema de agendamento de enfermagem, implementaremos essa abordagem


criando uma função de custo, na qual o custo de uma violação de restrição rígida é maior do que o
custo causado pelo uso de mais cores. O custo total é então usado como a função de aptidão a ser
minimizada. Essa funcionalidade pode ser incorporada à classe Python, que será descrita na próxima
subseção.

Representação do problema Python


Para encapsular o problema de coloração de grafos, criamos uma classe Python
chamada GraphColoringProblem. Essa classe pode ser encontrada no arquivo graphs.py , que pode
ser encontrado em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with Python/
blob/master/Chapter05/graphs.py.

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

Satisfação de restrição capítulo 5

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:

self.nodeList = list(self.graph.nodes) self.adjMatrix =


nx.adjacency_matrix(graph).todense()

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)

Além disso, a classe fornece os seguintes métodos públicos:

getCost(colorArrangement): Calcula o custo total do arranjo de cores dado

plotGraph(colorArrangement): Traça o gráfico com os nós coloridos de acordo com o arranjo


de cores dado

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:

gcp = GraphColoringProblem(nx.petersen_graph(), 10) solução = np.random.randint(5,


tamanho=len(gcp))

A saída resultante pode ter a seguinte aparência:

solução = [2 4 1 3 0 0 2 2 0 3] número de cores = 5

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

Satisfação de restrição capítulo 5

O gráfico para esta solução é o seguinte – você consegue identificar a única violação de coloração?

Gráfico de Petersen incorretamente colorido com cinco cores

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.

Solução de algoritmos genéticos


Para resolver o problema de coloração de grafos usando um algoritmo genético, criamos o programa Python 03-solve-
graphs.py, localizado em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with- Python/
blob/master/Capítulo05/
03-resolva-gráficos.py.

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.

As etapas a seguir descrevem os principais pontos da nossa solução:

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

Satisfação de restrição capítulo 5

2. Como nosso objetivo é minimizar o custo, definimos um único objetivo, minimizar


estratégia de condicionamento físico:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

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("Integers", random.randint, 0, MAX_COLORS - 1) toolbox.register("individualCreator",


tools.initRepeat, criador.Individual, toolbox.Integers, len(gcp))
toolbox.register("populationCreator" , ferramentas.initRepeat, lista, caixa
de ferramentas.individualCreator)

4. A função de avaliação de adequação é definida para calcular o custo combinado de


as violações de coloração e o número de cores usadas, que está associado a cada solução
individual, chamando o método getCost() da classe GraphColoringProblem :

def getCost(individual): return


gcp.getCost(individual),

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:

toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate",


tools.cxTwoPoint) toolbox.register("mutate", tools.mutUniformInt,
low=0, up=MAX_COLORS - 1, indpb=1.0/len(gcp))

[ 152 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas, cxpb=P_CROSSOVER,


mutpb=P_MUTATION, ngen=MAX_GENERATIONS, stats=stats, halloffame=hof, verbose=True)

7. Quando o algoritmo é concluído, imprimimos os detalhes da melhor solução encontrada antes de plotar
os gráficos.

Antes de executarmos o programa, vamos definir as constantes do algoritmo, como segue:

POPULATION_SIZE = 100 P_CROSSOVER


= 0,9 P_MUTATION = 0,1
MAX_GENERATIONS = 100
HALL_OF_FAME_SIZE = 5

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

A execução do programa com essas configurações produz a seguinte saída:

-- Melhor Individual = [5, 0, 6, 5, 0, 6, 5, 0, 0, 6]


-- Melhor condicionamento físico = 3,0

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

Satisfação de restrição capítulo 5

O código anterior cria o seguinte gráfico, que ilustra a validade da solução:

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:

Estatísticas do programa resolvendo o problema de coloração de grafos para o grafo de Petersen

[ 154 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

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:

gcp = graphs.GraphColoringProblem(nx.mycielski_graph(5), HARD_CONSTRAINT_PENALTY)

Usando os mesmos parâmetros de antes, incluindo a configuração de 10 cores, obtemos os seguintes


resultados:

-- Melhor Individual = [9, 6, 9, 4, 0, 0, 6, 5, 4, 5, 1, 5, 1, 1, 6, 6, 9, 5, 9, 6, 5, 1, 4]

-- Melhor condicionamento físico = 6,0

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:

-- Melhor Indivíduo = [0, 3, 0, 2, 4, 4, 2, 2, 2, 4, 1, 4, 3, 1, 3, 3, 4, 4, 2, 2, 4, 3, 0]

-- Melhor condicionamento físico = 5,0

número de cores = 5
Número de violações = 0
Custo = 5

[ 155 ]
Machine Translated by Google

Satisfação de restrição capítulo 5

O gráfico do gráfico colorido é o seguinte:

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.

Você é encorajado a experimentar outros gráficos e experimentar as várias configurações do algoritmo.

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

Satisfação de restrição capítulo 5

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

Introdução à teoria dos grafos, do livro Python Data Science Essentials –


Segunda edição por Alberto Boschetti, Luca Massaron, outubro de 2016

Tutorial NetworkX: https://networkx.github.io/documentation/stable/


tutorial.html

[ 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.

Ao final deste capítulo, você será capaz de fazer o seguinte:

Compreender os cromossomos e operadores genéticos usados para números reais


Use DEAP para otimizar funções contínuas
Otimize a função Eggholder
Otimize a função do Sky Blue
Realize otimização restrita com a função de Simionescu
Utilize nichos paralelos e seriais para localizar vários ótimos para funções multimodais

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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.

Confira o vídeo a seguir para ver o Código em ação: http://bit.ly/2REfGuT

Cromossomos e operadores genéticos


para números reais
Nos capítulos anteriores, focamos em problemas de busca que lidam inerentemente com a
avaliação metódica de estados e transições entre estados. Conseqüentemente, as soluções para
esses problemas eram melhor representadas por listas (ou arrays) de parâmetros binários
ou inteiros. Em contraste com isso, este capítulo aborda problemas em que o espaço de soluções
é contínuo, o que significa que as soluções são compostas de números reais (ponto flutuante).
Como mencionamos no Capítulo 2, Compreendendo os principais componentes dos
algoritmos genéticos, a representação de números reais usando listas binárias ou inteiras estava longe
do ideal e, em vez disso, listas (ou matrizes) de números de valor real são agora consideradas uma
abordagem mais simples e melhor.

Reiterando o exemplo do Capítulo 2, Compreendendo os principais componentes da genética


Algoritmos, se tivermos um problema envolvendo três parâmetros de valor real, o cromossomo ficará
assim:

[x1, x2, x3]

Onde x1 , x2 , x3 representam números reais, como:

[1,23, 7,2134, -25,309] ou [-30,10, 100,2, 42,424]

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

Otimizando funções contínuas Capítulo 6

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:

o1 é o resultado de um operador de cruzamento entre x1 e y1 . o2


é o resultado de um operador de cruzamento entre x2 e y2 . o3 é o
resultado de um operador de cruzamento entre x3 e y3 .

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.

Alguns dos operadores de código real comumente usados são:

Blend Crossover (também conhecido como BLX), onde cada descendente é


selecionado aleatoriamente a partir do seguinte intervalo criado por seus pais:

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.

Mutação normalmente distribuída (ou Gaussiana), onde o valor original é substituído


por um número aleatório que é gerado usando uma distribuição normal, com valores
predeterminados para média e desvio padrão.

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

Otimizando funções contínuas Capítulo 6

Usando DEAP com funções contínuas


Ao resolver problemas de busca discreta, a estrutura DEAP pode ser usada para otimizar funções
contínuas de maneira muito semelhante ao que vimos até agora. Tudo o que é necessário são
algumas modificações sutis.

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:

cxBlend() é a implementação do DEAP de Blend Crossover, usando o argumento alpha


como o valor ÿ.

cxSimulatedBinary() implementa Simulated Binary Crossover, usando o argumento eta


como o valor ÿ (fator de aglomeração).

mutGaussian() implementa mutação normalmente distribuída, usando os


argumentos mu e sigma como os valores para a média e desvio padrão, respectivamente.

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:

cxSimulatedBinaryBounded() é uma versão limitada do operador


cxSimulatedBinary() , aceitando os argumentos low e up como os limites inferior e superior do
espaço de pesquisa, respectivamente.

[ 161 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

mutPolynomialBounded() é um operador de mutação limitada que usa uma função


polinomial (em vez de gaussiana) para a distribuição de probabilidade. Esse operador também
aceita os argumentos baixo e alto como os limites inferior e superior do espaço de
pesquisa. Além disso, utiliza o parâmetro eta como fator de crowding, onde um valor alto
produzirá um mutante próximo ao seu valor original, enquanto um valor baixo produzirá
um mutante muito diferente de seu valor original.

Na próxima seção, demonstraremos o uso de operadores limitados ao otimizar uma função clássica
de benchmark.

Otimizando a função Eggholder


A função Eggholder, representada no diagrama a seguir, é frequentemente usada como
referência para algoritmos de otimização de funções. Encontrar o único mínimo global desta função
é considerado uma tarefa difícil devido ao grande número de mínimos locais, que lhe
conferem a forma de eggholder:

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

Otimizando funções contínuas Capítulo 6

A função pode ser expressa matematicamente da seguinte forma:

Geralmente é avaliado no espaço de busca limitado por [-512, 512] em cada dimensão.

O mínimo global da função é conhecido como:

x=512, y = 404,2319

Onde o valor da função é -959.6407.

Na próxima subseção, tentaremos encontrar o mínimo global usando o método de algoritmos


genéticos.

Otimizando a função Eggholder com algoritmos


genéticos
O programa baseado em algoritmo genético que criamos para otimizar a função Eggholder reside no
programa Python 01-optimize-eggholder.py localizado em:

https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/ blob/master/Chapter06/01-optimize-
eggholder.py

As etapas a seguir destacam as principais partes deste programa:

1. O programa começa definindo as constantes da função, ou seja, o número de dimensões de


entrada—2, já que esta função é definida sobre o plano xy , e os limites que foram
mencionados anteriormente:

DIMENSIONS = 2 # número de dimensões BOUND_LOW,


BOUND_UP = -512.0, 512.0 # limites, iguais para todas as dimensões

[ 163 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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.

def randomFloat(baixo, cima):


return [random.uniform(l, u) for l, u in zip([low] *
DIMENSÕES, [acima] * DIMENSÕES)]

3. Em seguida, definimos o operador attrFloat . Este operador utiliza o anterior


função auxiliar para criar um único número de ponto flutuante aleatório dentro dos limites
fornecidos. O operador attrFloat é então usado pelo operador individualCreator para criar
indivíduos aleatórios. Isso é seguido porpopulationCreator, que pode gerar o número
desejado de indivíduos:

toolbox.register("attrFloat", randomFloat, BOUND_LOW, BOUND_UP) toolbox.register("individualCreator", tools.initIterate,


criador.Individual, toolbox.attrFloat) toolbox.register("populationCreator", tools.initRepeat, lista, caixa de
ferramentas. criador individual)

4. Dado que o objeto a ser minimizado é a função Eggholder, nós a utilizamos


diretamente como avaliador de fitness. Como o indivíduo é uma lista de números de
ponto flutuante com uma dimensão (ou comprimento) de 2, extraímos os valores x e y do
indivíduo de acordo e calculamos a função:

def eggholder(individual): x = individual[0] y =


individual[1] f = (-(y + 47.0) *
np.sin(np.sqrt(abs(x/2.0 + (y +
47.0)))) * np.sin(np.sqrt(abs(x - (y + 47,0))))) return f, # retorna uma tupla
-x

toolbox.register("avaliar", porta-ovos)

[ 164 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

5. A seguir estão os operadores genéticos. Dado que o operador de seleção é independente do


tipo individual e tivemos uma boa experiência até agora usando a seleção de torneio
com tamanho de torneio 2, juntamente com a abordagem elitista, continuaremos a
usá-lo aqui. Os operadores de cruzamento e mutação, por outro lado, precisam ser
especializados para números de ponto flutuante dentro de determinados limites e, portanto,
usamos o operador cxSimulatedBinaryBounded fornecido pelo
DEAP para cruzamento e o operador mutPolynomialBounded para
mutação:

# 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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,


cxpb=P_CROSSOVER, mutpb=P_MUTATION,
ngen=MAX_GENERATIONS, stats=stats, halloffame=hof, verbose=True)

7. Começaremos com os seguintes parâmetros para as configurações do algoritmo genético.


Como a função Eggholder pode ser um pouco difícil de otimizar, usamos um
tamanho de população relativamente grande considerando a baixa contagem de dimensões:

# Constantes do Algoritmo Genético:


POPULATION_SIZE = 300
P_CROSSOVER = 0,9
P_MUTATION = 0,1
MAX_GENERATIONS = 300
HALL_OF_FAME_SIZE = 30

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

Otimizando funções contínuas Capítulo 6

Também é possível definir fatores de aglomeração separados para cruzamento e mutação.

Finalmente estamos prontos para executar o programa. Os resultados obtidos com essas configurações são
mostrados a seguir:

-- Melhor Individual = [512.0, 404.23180541839946]


-- Melhor condicionamento físico = -959.6406627208509

Isso significa que encontramos o mínimo global.

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:

Estatísticas do primeiro programa otimizando a função Eggholder

Uma área interessante está por volta da geração 180 - vamos explorá-la mais na próxima subseção.

[ 166 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

Melhorando a velocidade com uma taxa de mutação


aumentada
Se ampliarmos a parte inferior do eixo de aptidão, perceberemos uma melhora relativamente
grande do melhor resultado encontrado (linha vermelha) por volta da geração 180, acompanhada de
uma grande oscilação dos resultados médios (linha verde):

Seção ampliada do gráfico de estatísticas do primeiro programa

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

Otimizando funções contínuas Capítulo 6

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

Otimizando funções contínuas Capítulo 6

Otimizando a função do Sky Blue


Outra função frequentemente usada para algoritmos de otimização de benchmarking é
A função de Himmelblau, representada no diagrama a seguir:

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.

A função pode ser expressa matematicamente da seguinte forma:

Geralmente é avaliado no espaço de busca limitado por [-5, 5] em cada dimensão.

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

Otimizando funções contínuas Capítulo 6

Esses locais são representados no diagrama de contorno da função, como segue:

Diagrama de contorno da função de


Himmelblau Fonte: https://commons.wikimedia.org/wiki/File:Himmelblau_contour.svg. Imagem por: Nicoguaro.
Licenciado sob Creative Commons CC BY 4.0: https://creativecommons.org/licenses/by/4.0/deed.en.

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.

Otimizando a função de Himmelblau com


algoritmos genéticos
O programa baseado em algoritmo genético que criamos para encontrar um único mínimo
da função de Himmelblau reside no programa Python 02-optimize-himmelblau.py , localizado em:

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:

1. Definimos os limites dessa função para [-5,0, 5,0]:

BOUND_LOW, BOUND_UP = -5.0, 5.0 # limites para todas as dimensões

[ 170 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

2. Agora usamos a função de Himmelblau como avaliador de aptidão:

def céu azul (individual):


x = individual[0] y =
individual[1] f = (x ** 2 + y -
11) ** 2 + (x + y retorna f, # retorna uma tupla ** 2 - 7) ** 2

toolbox.register("avaliar", céu azul)

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)

4. Também imprimimos os membros do hall da fama - os melhores indivíduos encontrados


durante a corrida:

print("- As melhores soluções são:") para i no


intervalo(HALL_OF_FAME_SIZE):
print(i, ": ", hof.items[i].fitness.values[0], " -> ",
hof.itens[i])

Executando o programa, os resultados indicam que encontramos um dos quatro mínimos (x=3,0,
y=2,0):

-- Melhor Individual = [2,9999999999987943, 2,0000000000007114]


-- Melhor Fitness = 4.523490304795033e-23

A impressão dos membros do hall da fama sugere que todos representam a mesma solução:

- As melhores soluções são:


0 : 4.523490304795033e-23 -> [2.9999999999987943, 2.0000000000007114] 1 : 4.523732642865117e-23 ->
[2.9999999999987943, 2.000000000000697] 2 : 4.523900512465748e-23 -> [2.9999999999987943,
2.0000000000006937] 3 : 4.5240633333565856e-23 -> [2.9999999999987943, 2,00000000000071]

...

[ 171 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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.

Os resultados anteriores representam o que geralmente esperamos do algoritmo genético -


identificar um ótimo global e convergir para ele. Como neste caso temos vários mínimos, espera-se
que converja para um deles. Qual será é amplamente baseado na inicialização aleatória do algoritmo.
Como você deve se lembrar, em todos os nossos programas até agora, usamos uma semente
aleatória fixa (de valor 42):

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

Otimizando funções contínuas Capítulo 6

Por exemplo, se definirmos o valor da semente para 13, teremos a solução


(x=ÿ2,805118, y=3,131312), conforme ilustrado no diagrama a seguir:

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

Otimizando funções contínuas Capítulo 6

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.

Usando nichos e compartilhamento para encontrar


várias soluções
No Capítulo 2, Compreendendo os principais componentes dos algoritmos genéticos, mencionamos que o nicho e
o compartilhamento em algoritmos genéticos imitam a maneira como um ambiente natural é dividido em vários
subambientes ou nichos. Esses nichos são povoados por diferentes espécies, ou subpopulações, aproveitando os
recursos únicos disponíveis em cada nicho, enquanto espécimes que coexistem no mesmo nicho têm que competir
pelos mesmos recursos.
A implementação de um mecanismo de compartilhamento dentro do algoritmo genético incentivará os
indivíduos a explorar novos nichos e poderá ser usado para encontrar várias soluções ótimas, cada uma
considerada um nicho. Uma maneira comum de realizar o compartilhamento é dividir o valor bruto de aptidão de
cada indivíduo com (alguma função de) as distâncias combinadas de todos os outros indivíduos, penalizando
efetivamente uma população populosa ao compartilhar a recompensa local entre seus indivíduos.

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:

1. Para começar, a implementação de um mecanismo de compartilhamento geralmente exige que


otimizar uma função que produz valores positivos de aptidão e procurar valores máximos em vez de
mínimos. Isso nos permite dividir os valores brutos de aptidão como forma de diminuir a aptidão e
praticamente compartilhar os recursos entre os indivíduos vizinhos. Como a função de Himmelblau
produz valores entre 0 e (aproximadamente) 2.000, podemos usar uma função modificada que retorna
2.000 menos o valor original, o que garantirá que todos os valores da função sejam positivos,
enquanto transforma os pontos mínimos em pontos máximos que retornam o valor de 2.000. Observe
que as localizações desses pontos não serão alteradas, portanto, encontrá-los ainda servirá ao
nosso propósito original:

def himmelblauInvertido(individual):
x = indivíduo[0] y =
indivíduo[1]

[ 174 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

f = (x ** 2 + y - 11) ** 2 + (x + y ** 2 - 7) ** 2 return 2000.0 - f, # retorna uma


tupla

toolbox.register("avaliar", himmelblauInverted)

2. Para completar a conversão, redefinimos a estratégia de condicionamento para ser maximizadora


um:

criador.create("FitnessMax", base.Fitness, pesos=(1.0,))

3. Para habilitar a implementação do compartilhamento, primeiro criamos dois


constantes:

DISTANCE_THRESHOLD = 0,1
SHARING_EXTENT = 5,0

4. Em seguida, precisamos implementar o mecanismo de compartilhamento. Um local conveniente


para essa implementação é dentro do operador genético de seleção. O operador de
seleção é onde os valores de aptidão de todos os indivíduos são examinados e usados para
selecionar os pais para a próxima geração. Isso nos permite injetar algum código que recalcula
esses valores de aptidão logo antes da seleção ocorrer e, em seguida, recuperar os valores de
aptidão originais antes de continuar, para fins de rastreamento. Para que isso aconteça,
implementamos uma nova função
selTournamentWithSharing() , que tem a mesma assinatura da função tools.selTournament()
original que usamos até agora:

def selTournamentWithSharing(individuals, k, tournsize, fit_attr="fitness"):

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

Otimizando funções contínuas Capítulo 6

Isso significa que a redução no valor de aptidão será maior quando:

A distância (normalizada) entre os indivíduos é menor


A constante de extensão de compartilhamento é maior

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:

selecionado = tools.selTournament(individuals, k, tournsize, fit_attr)

Por fim, os valores de fitness originais são recuperados:

for i, ind in enumerate(individuals):


ind.fitness.values = origFitnesses[i],

5. Como toque final, adicionamos um gráfico mostrando as localizações dos melhores


indivíduos - os membros do hall da fama - no plano xy , ao lado da localização ótima conhecida,
semelhante ao que já fazemos para toda a população:

plt.figure(2)
plt.scatter(*zip(*globalMaxima), marcador='x', color='vermelho', zorder=1)

plt.scatter(*zip(*hof.items), marcador='. ', color='azul', zorder=0)

Quando executamos este programa, os resultados não decepcionam. Examinando os membros do hall da
fama, parece que localizamos todos os quatro locais ótimos:

- As melhores soluções são:


0 : 1999.9997428476076 -> [3.00161237138945, 1.9958270919300878] 1 : 1999.9995532774788 ->
[3.585506608049694, -1.8432407550446581] 2 : 1999.9988186889173 -> [3.585506608049694,
-1.8396197402430106] 3 : 1999.9987642838498 -> [-3.7758887140006174, -3.285804345540637] 4 :
1999.9986563457114 -> [ -2,8072634380293766, 3,125893564009283]

...

[ 176 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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

Enquanto isso, o diagrama que representa a distribuição de toda a população demonstra


como a população está espalhada pelas quatro soluções:

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

Otimizando funções contínuas Capítulo 6

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.

Função de Simionescu e otimização


restrita
À primeira vista, a função de Simionescu pode não parecer particularmente interessante. No entanto, tem uma
restrição que o torna intrigante para trabalhar, bem como agradável de se olhar.
no.

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:

Onde os valores de x, y estão sujeitos à seguinte condição:

[ 178 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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:

Diagrama de contorno da função de Simionescu restrita


Fonte: https://commons.wikimedia.org/wiki/File:Simionescu%27s_function.PNG.
Imagem por Simiprof. Licenciado sob Creative Commons CC BY-SA 3.0: https://creativecommons.org/licenses/by-sa/3.0/deed.en.

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

Otimizando funções contínuas Capítulo 6

Otimização restrita com algoritmos genéticos


Já lidamos com restrições no Capítulo 5, Satisfação de restrições, quando tratamos de restrições
dentro do domínio dos problemas de busca. No entanto, embora os problemas de pesquisa nos
apresentem estados ou combinações inválidos, aqui precisamos abordar restrições no espaço
contínuo, definidas como desigualdades matemáticas.

As abordagens para ambos os casos, no entanto, são semelhantes e as diferenças estão


na implementação. Vamos revisitar essas abordagens:

A melhor abordagem, quando disponível, é eliminar a possibilidade de uma violação de


restrição. Na verdade, temos feito isso o tempo todo neste capítulo, pois usamos
regiões limitadas para nossas funções. Na verdade, essas são restrições simples em cada
variável de entrada. Conseguimos contorná-los gerando populações iniciais dentro
dos limites fornecidos e utilizando operadores genéticos limitados, como
cxSimulatedBinaryBounded(), que produziu resultados dentro dos limites fornecidos.
Infelizmente, essa abordagem pode ser difícil de implementar quando as restrições são
mais complexas do que apenas limites superiores e inferiores para uma variável de
entrada.
Outra abordagem é descartar as soluções candidatas que violam qualquer restrição.
Como mencionamos anteriormente, essa abordagem leva à perda de informações
contidas nessas soluções e pode retardar consideravelmente o processo de otimização.

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.

Ao adotar a última abordagem – penalizar a pontuação por violações de restrição – podemos


utilizar um recurso oferecido pela estrutura DEAP, ou seja, a função de penalidade, como
demonstraremos na próxima subseção.

[ 180 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

Otimizando a função de Simionescu usando


algoritmos genéticos
O programa baseado em algoritmo genético que criamos para otimizar a função de Simionescu
reside no programa Python 04-optimize-simionescu.py , localizado em:

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:

1. As constantes que definem os limites são ajustadas para corresponder ao domínio


da função de Simionescu:

BOUND_LOW, BOUND_UP = -1,25, 1,25

2. Além disso, uma nova constante determina uma penalidade fixa (ou custo) por violar o
limitação:

PENALTY_VALUE = 10,0

3. A aptidão agora é determinada pela definição da função de Simionescu:

def simionescu (individual):


x = individual[0] y =
individual[1] f = 0.1 * retorna
f, # retorna x * e
uma tupla

toolbox.register("avaliado",simionescu)

4. Aqui é onde começa a parte interessante: agora definimos um novo


Practical() , que especifica o domínio de entrada válido usando as restrições. Esta
função retorna um valor de True para x, valores de y que atendem às restrições e um valor
de False caso contrário:

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

Otimizando funções contínuas Capítulo 6

5. Em seguida, usamos o operador toolbox.decorate() do DEAP em combinação com


a função tools.DeltaPenalty() para modificar (decorar) a função fitness original, de
forma que os valores fitness sejam penalizados sempre que as restrições não forem
satisfeitas. DeltaPenalty aceita a função viável() e o valor da penalidade fixa como
parâmetros:

toolbox.decorate("avaliar", tools.DeltaPenalty(viável, PENALTY_VALUE))

A função DeltaPenalty também pode aceitar um terceiro parâmetro que


representa a distância da região factível, fazendo com que a penalidade
aumente com a distância.

Agora o programa está pronto para usar! Os resultados indicam que realmente encontramos um dos
dois locais mínimos conhecidos:

-- Melhor Individual = [0,8487712463169383, -0,8482833185888866]


-- Melhor condicionamento físico = -0,07199984895485578

E o segundo local? Continue a ler — iremos procurá-lo na próxima subseção.

Usando restrições para encontrar várias soluções


Anteriormente neste capítulo, ao otimizar a função de Himmelblau, estávamos procurando por mais
de um local mínimo e observamos duas maneiras possíveis de fazer isso - uma era alterar
a semente aleatória e a outra era usar niching e compartilhamento. Aqui, demonstraremos uma
terceira opção, alimentada por restrições!

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.

Curiosamente, penalizar as áreas em torno das soluções encontradas anteriormente


pode ser implementado impondo restrições ao espaço de busca e, como acabamos de aprender
a aplicar restrições à função em questão, podemos usar esse conhecimento para implementar
niching serial, demonstrado a seguir.

[ 182 ]
Machine Translated by Google

Otimizando funções contínuas Capítulo 6

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

O programa é quase idêntico ao anterior, com algumas alterações, como segue:

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

2. Em seguida, adicionamos uma segunda restrição à definição da função viável()


usando uma instrução condicional com várias cláusulas. A nova restrição se aplica a valores de
entrada mais próximos do limite para a solução já encontrada (x=0,848, y = -0,848):

def viável(individual): x = individual[0] y


= individual[1]

se x**2 + y**2 > (1 + 0,2 * math.cos(8,0 * math.atan2(x, y)))**2: return False

elif (x - 0,848)**2 + (y + 0,848)**2 < DISTANCE_THRESHOLD**2:


retorna falso
outro:
retornar Verdadeiro

Ao executar este programa, os resultados indicam que realmente encontramos o segundo mínimo:

-- Melhor Individual = [-0,8473430282562487, 0,8496942440090975]


-- Melhor condicionamento físico = -0,07199824938105727

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

Otimizando funções contínuas Capítulo 6

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:

Otimização matemática: encontrando mínimos de funções: http://scipy talks.org/


advanced/mathematical_optimization/

Funções de teste de otimização e conjuntos de dados: https://www.sfu.ca/~ssurjano/


otimização.html

Introdução à otimização restrita: https://web.stanford.edu/group/sisl/k12/optimization/MO-


unit3-pdfs/3.1introandgraphical.pdf

Tratamento de restrições no DEAP: https://deap.readthedocs.io/en/master/


tutoriais/advanced/constraints.html

[ 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.

Esta seção compreende os seguintes capítulos:

Capítulo 7, Aprimorando modelos de aprendizado de máquina usando seleção de recursos


Capítulo 8, Ajuste de hiperparâmetros de modelos de aprendizado de máquina
Capítulo 9, Otimização de arquitetura de redes de aprendizado profundo
Capítulo 10, Aprendizagem por Reforço com Algoritmos Genéticos
Machine Translated by Google

Aprimorando o aprendizado de máquina


7
Modelos que usam seleção de recursos
Este capítulo descreve como os algoritmos genéticos podem ser usados para melhorar o desempenho de modelos
de aprendizado de máquina supervisionados, selecionando o melhor subconjunto de recursos dos dados
de entrada fornecidos. Este capítulo começará com uma breve introdução ao aprendizado de máquina e, em
seguida, descreverá os dois principais tipos de tarefas de aprendizado de máquina supervisionado – regressão
e classificação. Em seguida, discutiremos os benefícios potenciais da seleção de recursos quando se trata do
desempenho desses modelos. A seguir, demonstraremos como os algoritmos genéticos podem ser
utilizados para identificar as características genuínas que são geradas pelo problema de regressão do teste
Friedman-1 . Em seguida, usaremos o conjunto de dados do Zoo da vida real para criar um modelo de
classificação e melhorar sua precisão – novamente aplicando algoritmos genéticos para isolar os melhores
recursos para a tarefa.

Neste capítulo, abordaremos os seguintes tópicos:

Entenda os conceitos básicos de aprendizado de máquina supervisionado, bem como tarefas


de regressão e classificação
Compreender os benefícios da seleção de recursos no desempenho de modelos de aprendizado
supervisionado
Melhorar o desempenho de um modelo de regressão para o problema de regressão
Friedman-1 Test, usando a seleção de recursos realizada por um algoritmo genético codificado
com a estrutura DEAP

Melhorar o desempenho de um modelo de classificação para o problema de classificação


do conjunto de dados Zoo, usando a seleção de recursos realizada por um algoritmo genético
codificado com o framework DEAP

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido

pandas

matplotlib
nascido no mar

sklearn – introduzido neste capítulo

Além disso, usaremos o UCI Zoo Dataset (https://archive.ics.uci.edu/ml/datasets/


zoo ).
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/Chapter07.

Confira o vídeo a seguir para ver o Código em ação: http://


bit.ly/37HCKyr

Aprendizado de máquina supervisionado


O termo aprendizado de máquina geralmente se refere a um programa de computador que recebe
entradas e produz saídas. Nosso objetivo é treinar esse programa, também conhecido como modelo,
para produzir as saídas corretas para as entradas fornecidas, sem programá-las explicitamente.

Durante esse processo de treinamento, o modelo aprende o mapeamento entre as entradas e as


saídas ajustando seus parâmetros internos. Uma maneira comum de treinar o modelo é fornecer a
ele um conjunto de entradas, para as quais a saída correta é conhecida. Para cada uma dessas
entradas, informamos ao modelo qual é a saída correta para que ele possa ajustar, ou sintonizar-
se, visando eventualmente produzir a saída desejada para cada uma das entradas fornecidas. Essa
sintonia está no centro do processo de aprendizagem.

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Ajuste de parâmetros de um modelo de aprendizado de máquina

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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:

Modelo de classificação de aprendizado de máquina

Por exemplo, no conhecido conjunto de dados Iris Flower (https://archive.ics.uci.edu/ml/datasets/Iris ),


existem quatro recursos: comprimento da pétala, largura da pétala, comprimento da sépala e largura
da sépala. Estes representam as medições que foram feitas manualmente de flores de íris reais.

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Classificador Iris Flower ilustrado

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:

Modelo de regressão de aprendizado de máquina

[ 190 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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:

Regressor de preços de casas

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.

Algoritmos de aprendizado supervisionado


Como mencionamos anteriormente, cada modelo de aprendizado supervisionado consiste em um conjunto
de parâmetros internos ajustáveis e um algoritmo que ajusta esses parâmetros na tentativa de alcançar o
resultado desejado.

Alguns modelos/algoritmos comuns de aprendizado supervisionado incluem o seguinte:

Á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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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.

Seleção de recursos no aprendizado supervisionado


Como vimos na seção anterior, um modelo de aprendizado supervisionado recebe um conjunto de
entradas, chamadas de recursos, e as mapeia para um conjunto de saídas. A suposição é que as informações
descritas pelos recursos são úteis para determinar o valor das saídas correspondentes.
À primeira vista, pode parecer que quanto mais informações pudermos usar como entrada, melhores serão
nossas chances de prever a(s) saída(s) corretamente. No entanto, em muitos casos, o oposto é verdadeiro;
se alguns dos recursos que usamos forem irrelevantes ou redundantes, a consequência pode ser uma
diminuição (às vezes significativa) na precisão dos modelos.

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:

Os tempos de treinamento dos modelos são menores.


Os modelos treinados resultantes são mais simples e fáceis de interpretar.
É provável que os modelos resultantes forneçam melhor generalização, ou seja, eles
funcionam melhor com novos dados de entrada diferentes dos dados usados para treinamento.

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Selecionando os recursos para o problema


de regressão Friedman-1
O problema de regressão Friedman-1, criado por Friedman e Breiman, descreve um único valor
de saída, y, que é uma função de cinco valores de entrada, x0..x4 , e ruído gerado aleatoriamente,
de acordo com a seguinte fórmula:

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.

Em Python, a biblioteca scikit-learn (sklearn) nos fornece a função


make_friedman1() , que pode ser usada para gerar um conjunto de dados contendo o número
desejado de amostras. Cada uma das amostras consiste em valores x0..x4
gerados aleatoriamente e seu valor y calculado correspondente . A parte interessante, no entanto, é
que podemos dizer à função para adicionar um número arbitrário de variáveis de entrada irrelevantes
às cinco originais, definindo o parâmetro n_features com um valor maior que cinco. Se, por exemplo,
definirmos o valor de n_features como 15, obteremos um conjunto de dados contendo as cinco
variáveis de entrada originais (ou recursos) que foram usados para gerar os valores y de acordo com
a fórmula anterior e 10 recursos adicionais que são completamente irrelevante para a saída. Isso pode
ser usado, por exemplo, para testar a resiliência de vários modelos de regressão no ruído e na
presença de recursos irrelevantes no conjunto de dados.

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.

Como de costume, começaremos escolhendo uma representação apropriada para a solução,


conforme descrito na próxima subseção.

[ 193 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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.

Representação do problema Python


Para encapsular o problema de seleção de recursos de Friedman-1, criamos uma classe Python
chamada Friedman1Test. Essa classe pode ser encontrada no arquivo fritoman.py , localizado em https://
github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/blob/master/Chapter07/friedman.py .

As principais partes desta classe são as seguintes:

1. O método __init__() da classe cria o conjunto de dados, conforme a seguir:

self.X, self.y = datasets.make_friedman1(n_samples=self.numSamples,


n_features=self.numFeatures,
noise=self.NOISE,
random_state=self.randomSeed)

2. Em seguida, ele divide os dados em dois subconjuntos – um conjunto de treinamento e um


conjunto de validação – usando o método scikit-learn model_selection.train_test_split() :

self.X_train, self.X_validation, self.y_train, self.y_validation = \


model_selection.train_test_split(self.X,
self.y,
test_size=self.VALIDATION_SIZE, random_state=self.randomSeed)

[ 194 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Dividir os dados em um conjunto de treinamento e um conjunto de validação nos permite


treinar o modelo de regressão no conjunto de treinamento, onde a previsão correta é fornecida
ao modelo para fins de treinamento e, em seguida, testá-lo com o conjunto de validação
separado, onde as previsões corretas não são dados ao modelo e são, em vez disso,
comparados com as previsões que ele produz. Dessa forma, podemos testar o quão bem o
modelo é capaz de generalizar, em vez de memorizar os dados de treinamento.

3. Em seguida, criamos o modelo de regressão, que é do Gradient Boosting


Tipo de regressor (GBR) . Este modelo cria um conjunto (ou agregação) de árvores de
decisão durante a fase de treinamento:

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:

zeroIndices = [i para i, n em enumerate(zeroOneList) if n == 0] currentX_train = np.delete(self.X_train,


zeroIndices, 1) currentX_validation = np.delete(self.X_validation, zeroIndices, 1)

5. O conjunto de treinamento modificado – contendo apenas os recursos selecionados – é então


usado para treinar o regressor, enquanto o conjunto de validação modificado é usado para
avaliar suas previsões:

self.regressor.fit(currentX_train, self.y_train) predição =


self.regressor.predict(currentX_validation) return mean_squared_error(self.y_validation,
predição)

[ 195 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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:

para n no intervalo(1, len(teste) + 1):


nFirstFeatures = [1] * pontuação = n + [0] * (len(teste) - n)
test.getMSE(nFirstFeatures)

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Isso é ilustrado ainda mais pelo gráfico gerado, mostrando o valor MSE mínimo onde os cinco primeiros
recursos são usados:

Gráfico de valores de erro para o problema de regressão Friedman-1

Na próxima subseção, descobriremos se um algoritmo genético pode identificar com sucesso esses cinco
primeiros recursos.

Solução de algoritmos genéticos


Para identificar o melhor conjunto de recursos a ser usado para nosso teste de regressão
usando um algoritmo genético, criamos o programa Python 01-solve-friedman.py, que está localizado
em https://github.com/PacktPublishing/Hands- Algoritmos-genéticos-com-Python/
blob/master/Chapter07/01-solve-friedman.py.

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

As etapas a seguir descrevem as principais partes da nossa solução:

1. Primeiro, precisamos criar uma instância da classe Friedman1Test com o desejado


Parâmetros:

fritada = fritada.Friedman1Test(NUM_OF_FEATURES, NUM_OF_SAMPLES,


RANDOM_SEED)

2. Como nosso objetivo é minimizar o MSE do modelo de regressão, definimos um único


objetivo, minimizando a estratégia de aptidão:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

3. Como a solução é representada por uma lista de valores inteiros 0 ou 1, usamos as


seguintes definições de caixa de ferramentas para criar a população inicial:

toolbox.register("zeroOrOne", random.randint, 0, 1) toolbox.register("individualCreator",


tools.initRepeat, criador.Individual, toolbox.zeroOrOne, len(friedman))
toolbox.register("populationCreator", ferramentas .initRepeat, lista, caixa de
ferramentas.individualCreator)

4. Em seguida, instruímos o algoritmo genético a usar o método getMSE() de


a instância Friedman1Test para avaliação de aptidão:

def friesedmanTestScore(individual): return


friesman.getMSE(individual), # retorna uma tupla

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:

toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate", tools.cxTwoPoint)


toolbox.register("mutate", tools.mutFlipBit, indpb=1.0/len(friedman))

[ 198 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

6. Além disso, continuamos a usar a abordagem elitista, onde o hall da fama


(HOF) membros - os melhores indivíduos atuais - são sempre passados intactos para a próxima
geração:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,

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:

-- Melhor Individual de Sempre = [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]


-- Melhor condicionamento físico de todos os tempos = 6,702668910463287

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.

Selecionando as feições para a classificação


Conjunto de dados do zoológico

O repositório de aprendizado de máquina da UCI (https://archive.ics.uci.edu/ml/index. php)


mantém mais de 350 conjuntos de dados como um serviço para a comunidade de aprendizado de
máquina. Esses conjuntos de dados podem ser usados para experimentação com vários modelos e
algoritmos. Um conjunto de dados típico contém uma série de recursos (entradas) e a saída desejada, na
forma de colunas, com uma descrição de seu significado.

[ 199 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Nesta seção, usaremos o conjunto de dados UCI Zoo (https://archive.ics.uci.edu/ml/datasets/zoo ). Este


conjunto de dados descreve 101 animais diferentes usando os 18 recursos a seguir:

Nº Nome do recurso Tipo de dados


1 nome de animal Único para cada instância
2 cabelo boleano
3 penas boleano
4 ovos boleano
5 leite boleano
6 no ar boleano
7 predador boleano
8 aquático boleano
9 dentado boleano
10 espinha dorsal boleano
11 respira boleano
12 venenoso boleano
13 para boleano
14 pernas Numérico (conjunto de valores {0,2,4,5,6,8})
15 cauda boleano
16 doméstico boleano
17 tamanho de gato boleano
18 tipo Numérico (valores inteiros no intervalo [1,7])

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Representação do problema Python


Para encapsular o processo de seleção de recursos para a tarefa de classificação do conjunto de dados Zoo, criamos
uma classe Python chamada Zoo. Essa classe está contida no arquivo zoo.py , localizado em https://github.com/
PacktPublishing/Hands-On-Genetic-Algorithms-with Python/blob/master/Chapter07/zoo.py.

As principais partes desta classe são destacadas a seguir:

1. O método __init__() da classe carrega o conjunto de dados Zoo da web enquanto


pulando a primeira característica – nome do animal – da seguinte forma:

self.data = read_csv(self.DATASET_URL, header=None, usecols=range(1, 18))

2. Em seguida, separa os dados para os recursos de entrada (primeiras 16 colunas restantes) e o


categoria resultante (última coluna):

self.X = self.data.iloc[:, 0:16] self.y = self.data.iloc[:,


16]

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 :

self.kfold = model_selection.KFold(n_splits=self.NUM_FOLDS, random_state=self.randomSeed)

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

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

5. O método getMeanAccuracy() da classe é usado para avaliar o


desempenho do classificador para um conjunto de características selecionadas.
Semelhante ao método getMSE() na classe Friedman1Test , esse método 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 descarta as colunas no conjunto de dados que correspondem aos
recursos não selecionados:

zeroIndices = [i for i, n in enumerate(zeroOneList) if n == 0] currentX =


self.X.drop(self.X.columns[zeroIndices], axis=1)

6. Este conjunto de dados modificado – contendo apenas as características selecionadas – é então


usado para executar o processo de validação cruzada k-fold e determinar o desempenho
do classificador sobre as partições de dados. O valor de k em nossa classe é definido como 5,
portanto, cinco avaliações ocorrem a cada vez:

cv_results = model_selection.cross_val_score(self.classifier, currentX, self.y, cv=self.kfold,


scoring='precisão') return cv_results.mean()

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:

allOnes = [1] * len(zoo) print("-- Todos


os recursos selecionados: ", allOnes, ", exatidão = ", zoo.getMeanAccuracy(allOnes))

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%:

-- Todos os recursos selecionados: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], precisão = 0,9099999999999999

[ 202 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

Na próxima subseção, tentaremos melhorar a precisão do classificador selecionando um subconjunto de


recursos do conjunto de dados, em vez de usar todos os recursos. Usaremos – você adivinhou – um
algoritmo genético para selecionar esses recursos para nós.

Solução de algoritmos genéticos


Para identificar o melhor conjunto de recursos a ser usado para nossa tarefa de classificação do zoológico
usando um algoritmo genético, criamos o programa Python 02-solve-zoo.py, localizado em
https://github.com/PacktPublishing/Hands -On-Genetic-Algorithms-with-Python/ blob/master/Chapter07/02-
solve-zoo.py.

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.

As etapas a seguir destacam as partes principais do programa:

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)

2. Como nosso objetivo é maximizar a precisão do modelo do classificador, definimos


um único objetivo, maximizando a estratégia de condicionamento físico:

criador.create("FitnessMax", base.Fitness, pesos=(1.0,))

3. Assim como na seção anterior, usamos as seguintes definições de caixa de ferramentas


para criar a população inicial de indivíduos, cada um construído como uma lista de 0 ou
1 valores inteiros:

toolbox.register("zeroOrOne", random.randint, 0, 1) toolbox.register("individualCreator",


tools.initRepeat, criador.Individual, toolbox.zeroOrOne, len(zoo)) toolbox.register("populationCreator",
ferramentas .initRepeat, lista, caixa de ferramentas.individualCreator)

[ 203 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

4. Em seguida, instruímos o algoritmo genético a usar o método getMeanAccuracy() da


instância Zoo para avaliação de fitness. Para fazer isso, tivemos que fazer duas
modificações:
Eliminamos a possibilidade de nenhum recurso ser selecionado (todos os
zeros individuais), pois nosso classificador lançará uma exceção nesse caso.
Adicionamos uma pequena penalidade para cada recurso usado para incentivar
a seleção de menos recursos. O valor da penalidade é muito pequeno (0,001),
por isso só entra em jogo como desempate entre dois classificadores de
desempenho igual, levando o algoritmo a preferir aquele que usa menos
recursos:

def zooClassificationAccuracy(individual): numFeaturesUsed =


soma(individual) if numFeaturesUsed == 0:

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:

toolbox.register("select", tools.selTournament, tournsize=2) toolbox.register("mate",


tools.cxTwoPoint) toolbox.register("mutate", tools.mutFlipBit,
indpb=1.0/len(zoo))

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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,


cxpb=P_CROSSOVER, mutpb=P_MUTATION, ngen=MAX_GENERATIONS, stats=stats,
halloffame=hof, verbose=True)

[ 204 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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:

print("- As melhores soluções são:") para i in


range(HALL_OF_FAME_SIZE): print(i, ": ", hof.items[i],
", fitness = ", hof.items[i].fitness.values[ 0], ", precisão = ",
zoo.getMeanAccuracy(hof.items[i]),

", features = ", sum(hof.items[i]))

Ao executar o algoritmo por 50 gerações com um tamanho de população de 50 e tamanho de HOF


de 5, obtemos o seguinte resultado:
- As melhores soluções são:
0: [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0] precisão = 0,97 1: [0, 1, 0, 1, 1 , 0, 0, 0, , aptidão = 0,964 ,
1, 0, 0, 1, 0, 1, 0, 1] , características = 6
precisão = 0,97 2: [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1 , 1, 0, 1, 0, 0] precisão = 0,97 3 : [1, 1, , aptidão = 0,963 ,
0, 1, 1, 0, 0, 0, 1, 0, 0, , características = 7
1, 0, 1, 0, 0] precisão = 0,97 4: [0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0] precisão = , aptidão = 0,963 ,
0,97 , características = 7
, aptidão = 0,963 ,
, características = 7
, aptidão = 0,963 ,
, características = 7

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

Em conclusão, ao selecionar esses recursos específicos dentre os 16 fornecidos no conjunto de


dados, não apenas reduzimos a dimensionalidade do problema, mas também melhoramos a
precisão do modelo de 91% para 97%. Se isso não parecer um grande aprimoramento à primeira
vista, pense nisso como uma redução da taxa de erro de 9% para 3% – uma melhoria
muito significativa em termos de desempenho de classificação.

[ 205 ]
Machine Translated by Google

Aprimorando modelos de aprendizado de máquina usando seleção de recursos Capítulo 7

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.

No próximo capítulo, veremos outra maneira possível de melhorar o desempenho de modelos de


aprendizado de máquina supervisionado, ou seja, o ajuste de hiperparâmetros.

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.

Ao final deste capítulo, você:

Entenda o conceito de ajuste de hiperparâmetros no aprendizado de máquina


Familiarize-se com o conjunto de dados do Wine e o classificador de reforço adaptável
Melhore o desempenho de um classificador usando uma pesquisa de grade de hiperparâmetros
Melhore o desempenho de um classificador usando uma pesquisa de grade de
hiperparâmetros orientada por algoritmo genético
Melhore o desempenho de um classificador 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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido

pandas

matplotlib
nascido no mar

aprendido

sklearn-deap - introduzido neste capítulo

Além disso, usaremos o UCI Wine Dataset (https://archive.ics.uci.edu/ml/datasets/


Wine ).
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/
Chapter08.

Confira o vídeo a seguir para ver o Código em ação:


http://bit.ly/37Q45id

Hiperparâmetros em aprendizado de máquina


No Capítulo 7, Aprimorando modelos de aprendizado de máquina usando seleção de recursos,
descrevemos o aprendizado supervisionado como o processo programático de ajuste (ou ajuste) dos
parâmetros internos de um modelo para produzir as saídas desejadas em resposta a entradas fornecidas. Para
que isso aconteça, cada tipo de modelo de aprendizado supervisionado é acompanhado por um
algoritmo de aprendizado que ajusta iterativamente seus parâmetros internos durante a fase de aprendizado
(ou treinamento).

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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

Ajuste de hiperparâmetros de um modelo de aprendizado de máquina

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.

Alguns desses hiperparâmetros são descritos na tabela a seguir:

Padrão
Nome Tipo Descrição Valor
profundidade máxima int A profundidade máxima da árvore Nenhum

divisor A estratégia usada para escolher a divisão em cada melhor


corda
nó (melhor ou aleatória)
O número mínimo de amostras necessárias para dividir 2
um int min_samples_split ou um nó
interno flutuante

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.

Como a escolha de hiperparâmetros tem um impacto considerável no desempenho dos modelos


de aprendizado de máquina, os cientistas de dados geralmente gastam uma quantidade significativa
de tempo procurando as melhores combinações de hiperparâmetros, um processo chamado ajuste de
hiperparâmetros. Alguns dos métodos usados para ajuste de hiperparâmetros serão descritos na
próxima subseção.

[ 209 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

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:

pip install sklearn-deap

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.

O conjunto de dados Wine


Um conjunto de dados comumente usado do UCI Machine Learning Repository (https://archive.ics.uci.edu/
ml/index.php ), o conjunto de dados Wine (https://archive.ics.uci.edu/ml/datasets/Wine ), contém
os resultados de uma análise química realizada para 178 vinhos diferentes cultivados na mesma
região da Itália e classifica esses vinhos em um dos três tipos.

[ 210 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

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

Alcalinidade das cinzas


Magnésio
fenóis totais
Flavonóides

Fenóis não flavonoides


Proantocianinas
Intensidade da cor
Matiz
OD280/OD315 de vinhos diluídos
Prolina

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.

O classificador de reforço adaptativo


O algoritmo de reforço adaptativo, ou AdaBoost, para abreviar, é um poderoso modelo de aprendizado de
máquina que combina as saídas de várias instâncias de um algoritmo de aprendizado simples (aprendiz fraco)
usando uma soma ponderada. O AdaBoost adiciona instâncias do aluno fraco durante o processo de
aprendizagem, cada uma das quais é ajustada para melhorar as entradas anteriormente classificadas incorretamente.

[ 211 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

A implementação da biblioteca sklearn deste modelo, AdaboostClassifier (https://scikit learn.org/


stable/modules/generated/sklearn.ensemble.AdaBoostClassifier.html), usa vários
hiperparâmetros, alguns dos quais são os seguintes:

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

Curiosamente, cada um desses três hiperparâmetros é de um tipo diferente – um int, um float e


um tipo enumerado (ou categórico). Mais adiante, descobriremos como cada método de
afinação lida com esses diferentes tipos. Começaremos com duas formas de pesquisa em grade,
ambas descritas na próxima seção.

Ajustando os hiperparâmetros usando uma pesquisa


de grade genética
Para encapsular o ajuste de hiperparâmetros do classificador AdaBoost para o conjunto
de dados do vinho usando uma pesquisa em grade – tanto a versão convencional quanto a
versão orientada por algoritmo genético – criamos uma classe Python chamada
HyperparameterTuningGrid . Essa classe pode ser encontrada no arquivo 01-
hyperparameter-tuning-grid.py , localizado em https://github.com/PacktPublishing/Hands-On-
Genetic-Algorithms-with-Python/blob/master/Chapter08/ 01-hyperparameter-tuning-grid.py.

As principais partes desta classe são destacadas a seguir:

1. O método __init__() da classe inicializa o conjunto de dados do vinho, o classificador


AdaBoost, a métrica de validação cruzada k-fold e os parâmetros da grade:

self.initWineDataset()
self.initClassifier() self.initKfold()
self.initGridParams()

[ 212 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

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.

Esta configuração abrange um total de 200 (10 × 10 × 2) combinações diferentes dos


parâmetros da grade:

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'],

3. O método getDefaultAccuracy() avalia a precisão do classificador


com seus valores de hiperparâmetro padrão usando o valor médio da 'precisão'
métrica:

cv_results = model_selection.cross_val_score(self.classifier, self.X, self.y, cv=self.kfold,

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 = GridSearchCV(estimator=self.classifier, param_grid=self.gridParams,


cv=self.kfold, scoring='precisão')

gridSearch.fit(self.X, self.y)

[ 213 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

5. O método geneticGridTest() executa uma grade orientada por algoritmo genético


procurar. Ele utiliza o método EvolutionaryAlgorithmSearchCV()
da biblioteca sklearn-deap , que foi projetado para ser chamado de maneira muito semelhante à
da pesquisa de grade convencional. Tudo o que precisamos fazer é adicionar alguns parâmetros
do algoritmo genético – tamanho da população, probabilidade de mutação, tamanho do torneio e o
número de gerações:

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.

Testando o desempenho padrão do classificador


Os resultados da execução indicam que, com os valores de parâmetro padrão de n_estimators = 50,
learning_rate = 1,0 e algoritmo = 'SAMME.R', a precisão de classificação do classificador é de cerca de 65%:

Valores padrão do hiperparâmetro do classificador: {'algorithm':


'SAMME.R', 'base_estimator': nenhum, 'learning_rate': 1,0, 'n_estimators': 50, 'random_state': 42} pontuação
com valores padrão = 0,6457142857142857

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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

Executando a pesquisa de grade convencional


A pesquisa de grade convencional e exaustiva, cobrindo todas as 200 combinações possíveis, precisa ser executada em seguida.
Os resultados da pesquisa indicam que a melhor combinação foi n_estimators = 70, learning_rate
ÿ 0,359 e algoritmo = 'SAMME.R'.

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:

realizando pesquisa de grade... melhores


parâmetros: {'algoritmo': 'SAMME.R', 'learning_rate': 0,3593813663804626, 'n_estimators': 70}

melhor pontuação: 0,9325842696629213

Tempo decorrido = 32,180874824523926

Em seguida, vem a busca de grade de energia genética. Será que vai corresponder a esses resultados? Vamos descobrir.

Executando a pesquisa de grade orientada por algoritmo genético


A última parte da execução descreve a busca de grade orientada por algoritmo genético, que é realizada com os mesmos
parâmetros de grade. A saída detalhada da pesquisa começa com uma impressão um tanto enigmática:

realizando pesquisa de grade genética...


Tipos [1, 2, 1] e maxint [9, 9, 1] detectados

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

aos tamanhos de lista/array de [10, 10, 2].

A próxima linha impressa refere-se à quantidade total de combinações de grade possíveis (10×10×2):

--- Evolua em 200 combinações possíveis ---

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:

gen nevals avg min max std 0 20 0,642135 0,117978


0,904494 0,304928
1 14 0,807865 0,123596 0,91573 0,20498

[ 215 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

2 15 0,829775 0,123596 0,921348 0,172647


3 12 0,885393 0,679775 0,921348 0,0506055
4 13 0,903652 0,865169 0,926966 0,0176117
5 11 0,905618 0,797753 0,932584 0,027728

Ao final do processo, é impressa a melhor combinação, o valor da pontuação e o tempo decorrido:

O melhor indivíduo é: {'n_estimators': 70, 'learning_rate': 0,3593813663804626, 'algorithm': 'SAMME.R'}


com aptidão: 0,9325842696629213

Tempo decorrido = 10,997037649154663

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.

Ajustando os hiperparâmetros usando uma


abordagem genética direta
Além de oferecer uma opção de pesquisa de grade eficiente, os algoritmos genéticos podem ser utilizados
para pesquisar diretamente todo o espaço de parâmetros, assim como os usamos para pesquisar o espaço de
entrada para muitos tipos de problemas ao longo deste livro. Cada hiperparâmetro pode ser representado como
uma variável participante da busca, e o cromossomo pode ser uma combinação de todas essas variáveis.

[ 216 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

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:

[1.23, 7.2134, -25.309]

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:

n_estimators, originalmente um número inteiro, será representado por um valor flutuante em um


determinado intervalo; por exemplo, [1, 100]. Para transformar o valor float de volta em um
inteiro, usaremos a função round() do Python , que o arredondará para o inteiro mais próximo.
learning_rate já é
float, então nenhuma conversão é necessária. Ele será limitado ao intervalo de [0,01, 1,0]. algoritmo
pode ter um de dois valores,
'SAMME' ou 'SAMME.R', e será representado por um número flutuante no intervalo de [0,
1]. Para transformar o valor float, iremos arredondá-lo para o inteiro mais próximo – 0 ou 1. Em
seguida, substituiremos um valor de 0 por 'SAMME' e um valor de 1 por 'SAMME.R'.

Essas conversões serão realizadas por dois arquivos Python, ambos descritos nas subseções a seguir.

[ 217 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

Avaliando a precisão do classificador


Começamos com uma classe Python que encapsula a avaliação de precisão do
classificador, chamada HyperparameterTuningGenetic. Essa classe pode
ser encontrada no arquivo hyperparameter_tuning_genetic_test.py , localizado em https://
github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/blob/master/ Chapter08/
hyperparameter_tuning_genetic_test.py.

A principal funcionalidade desta classe é destacada a seguir:

1. O método convertParam() da classe pega uma lista chamada params, contendo os


valores flutuantes que representam os hiperparâmetros, e os transforma em seus
valores reais, conforme discutido na subseção anterior:

n_estimators = round(params[0]) learning_rate =


params[1] algoritmo = ['SAMME',
'SAMME.R'][round(params[2])]

2. O método getAccuracy() obtém uma lista de números flutuantes que representam o


valores de hiperparâmetro, usa o método convertParam() para transformá-los em
valores reais e inicializa o classificador ADABooost com estes valores:

n_estimators, learning_rate, algoritmo = self.convertParams(params)


self.classifier =
AdaBoostClassifier(n_estimators=n_estimators, learning_rate=learning_rate, algoritmo=algoritmo)

3. Em seguida, encontra a precisão do classificador usando a validação cruzada k-fold que


criamos para o conjunto de dados do vinho:

cv_results = model_selection.cross_val_score(self.classifier, self.X, self.y, cv=self.kfold, scoring='precisão')

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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

Ajustando os hiperparâmetros usando


algoritmos genéticos
A busca baseada em algoritmo genético para os melhores valores de hiperparâmetros é implementada pelo programa Python,
02-hyperparameter-tuning-genetic.py, que está localizado em https://github.com/PacktPublishing/Hands-On-Genetic-
Algorithms- com-Python/
blob/master/Chapter08/02-hyperparameter-tuning-genetic.py.

As etapas a seguir descrevem as partes principais deste programa:

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:

# [n_estimators, learning_rate, algoritmo]: BOUNDS_LOW = [ 1, 0.01,


0]
BOUNDS_HIGH = [100, 1,00, 1]

2. Em seguida, criamos uma instância da classe HyperparameterTuningGenetic que nos


permitirá testar as várias combinações dos hiperparâmetros:

teste =
hyperparameter_tuning_genetic.HyperparameterTuningGenetic(RANDO M_SEED)

3. Como nosso objetivo é maximizar a precisão do classificador, definimos um único objetivo,


maximizando a estratégia de aptidão:

criador.create("FitnessMax", base.Fitness, pesos=(1.0,))

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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

5. Em seguida, criamos a tupla de hiperparâmetros , que contém o float separado


geradores de números que acabamos de criar para cada hiperparâmetro:

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:

caixa de ferramentas.register("criador individual",


ferramentas.initCycle,
criador.Individual,
hiperparâmetros, n=1)

7. Em seguida, instruímos o algoritmo genético a usar o método getAccuracy() da instância


HyperparameterTuningGenetic para avaliação de aptidão. Como lembrete, o método
getAccuracy() , que descrevemos na subseção anterior, converte o indivíduo fornecido –
uma lista de três floats – de volta aos valores de hiperparâmetros do classificador que eles
representam, treina o classificador com esses valores e avalia sua precisão usando validação
cruzada k-fold:

classificação def Precisão(individual):


return test.getAccuracy(individual),

toolbox.register("avaliar", classificaçãoPrecisão)

8. Agora, precisamos definir os operadores genéticos. Enquanto, para o operador de seleção,


usamos a seleção de torneio usual com um tamanho de torneio de 2, escolhemos operadores
de cruzamento e mutação que são especializados para cromossomos de lista flutuante
limitada e fornecemos a eles os limites que definimos para cada hiperparâmetro:

toolbox.register("select", tools.selTournament, tournsize=2)

toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR)

toolbox.register("mutar",

[ 220 ]
Machine Translated by Google

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

tools.mutPolynomialBounded, low=BOUNDS_LOW,
up=BOUNDS_HIGH,
eta=CROWDING_FACTOR,
indpb=1.0 / NUM_OF_PARAMS)

9. Além disso, 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:

população, diário de bordo = elitism.eaSimpleWithElitism(população,


caixa de
ferramentas, cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS, stats=stats,
halloffame=hof,
verbose=True)

Ao executar o algoritmo por cinco gerações com um tamanho de população de 20, obtemos
o seguinte resultado:

gen nevals max avg 0 20 0,92127


0,841024
1 14 0,943651 0,900603
2 13 0,943651 0,912841
3 14 0,943651 0,922476
4 15 0,949206 0,929754
5 13 0,949206 0,938563
- A melhor solução é:

params = 'n_estimators'= 69, 'learning_rate'=0,628, 'algorithm'=SAMME.R Precisão = 0,94921

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

Ajuste de hiperparâmetros de modelos de aprendizado de máquina Capítulo 8

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.

No próximo capítulo, examinaremos os fascinantes modelos de aprendizado de máquina de redes


neurais e aprendizado profundo e aplicaremos algoritmos genéticos para melhorar seu desempenho.

Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes
recursos:

Introdução ao ajuste de hiperparâmetros, do livro Mastering Predictive Analytics with


scikit-learn and TensorFlow, Alan Fontaine, setembro de 2018: https://subscription.packtpub.com/
book/big_data_and_business_intelligence/ 9781789617740/2/ch02lvl1sec16/introduction-
to-hyperparameter- afinação
sklearn-deap no GitHub: https://github.com/rsteca/sklearn-deap
Comparando EvolutionaryAlgorithmSearchCV com GridSearchCV e
RandomizedSearchCV: https://github.com/rsteca/sklearn-deap/blob/
master/test.ipynb
Classificador ADABoost sklearn: https://scikit-learn.org/stable/modules/
generated/sklearn.ensemble.AdaBoostClassifier.html
Repositório de aprendizado de máquina da UCI: https://archive.ics.uci.edu/
ml/index. php

[ 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.

Neste capítulo, abordaremos os seguintes tópicos:

Compreender os conceitos básicos de redes neurais artificiais e aprendizagem profunda


O conjunto de dados Iris e o classificador Multilayer Perceptron (MLP)
Melhorando o desempenho de um classificador de aprendizado profundo usando
otimização de arquitetura de rede
Melhorando ainda mais o desempenho do classificador de aprendizado profundo, combinando
a otimização da arquitetura de rede com o ajuste de hiperparâmetros

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

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido

aprendido

Além disso, usaremos o conjunto de dados de flores UCI Iris (https://archive.ics.uci.edu/


ml/datasets/Iris ).

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/Chapter09.

Confira o vídeo a seguir para ver o Código em ação: http://


bit.ly/317KCXA

Redes neurais artificiais e aprendizado profundo


As redes neurais estão entre os modelos mais usados em aprendizado de máquina e foram
inspiradas na estrutura do cérebro humano. Os blocos de construção básicos dessas
redes são nós, ou neurônios, que são baseados na célula de neurônio biológico, conforme
representado no diagrama a seguir:

Modelo de neurônio
biológico Fonte: https://pixabay.com/vectors/neuron-nerve-cell-axon-dendrite-296581/

[ 224 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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:

Modelo de neurônio artificial – o perceptron

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

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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:

A estrutura básica de um Perceptron multicamada

O Multilayer Perceptron consiste em três partes principais:

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.

Camada(s) Oculta(s): Fornece o verdadeiro poder e complexidade deste modelo. Embora o


diagrama anterior mostre apenas duas camadas ocultas, pode haver várias camadas
ocultas, cada uma com um tamanho arbitrário, que são colocadas entre as camadas de
entrada e saída. À medida que o número de camadas ocultas cresce, a rede se torna
mais profunda e é capaz de realizar um mapeamento cada vez mais complexo e não
linear entre as entradas e as saídas.

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

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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.

Aprendizado profundo e redes neurais convolucionais


Nos últimos anos, os algoritmos de retropropagação deram um salto à frente, permitindo o uso de um grande
número de camadas ocultas em uma única rede. Nessas redes neurais profundas, cada camada pode
interpretar uma combinação de vários conceitos abstratos mais simples que foram aprendidos pelos nós da
camada anterior e produzir conceitos de nível superior. Por exemplo, ao implementar uma tarefa de
reconhecimento facial, a primeira camada processará os pixels de uma imagem e aprenderá a detectar
bordas em diferentes orientações. A próxima camada pode reuni-los em linhas, cantos e assim por diante,
até uma camada que detecte características faciais como nariz e lábios e, finalmente, uma que os
combine no conceito completo de um rosto.

Outros avanços trouxeram a ideia de redes neurais convolucionais.


Essas estruturas podem reduzir a contagem de nós em redes neurais profundas que processam
informações bidimensionais (como imagens), tratando entradas próximas de maneira diferente em
comparação com entradas distantes. Como resultado, esses modelos provaram ser especialmente
bem-sucedidos quando se trata de tarefas de processamento de imagem e vídeo. Além das camadas
totalmente conectadas, semelhantes às camadas ocultas do Multilayer Perceptron, essas redes
utilizam camadas de agrupamento (amostragem descendente), que agregam saídas de
neurônios das camadas anteriores, e camadas convolucionais, que podem ser usadas como filtros para
detectar certas características (como como uma borda em uma orientação particular).

O treinamento de modelos de aprendizado profundo pode ser computacionalmente intensivo e geralmente é


feito com o auxílio de unidades de processamento gráfico (GPUs), que são mais eficientes do que as
CPUs comuns na implementação do algoritmo de retropropagação. Bibliotecas de aprendizado profundo
especializadas, como TensorFlow, são capazes de utilizar plataformas de computação baseadas em
GPU. Neste capítulo, no entanto, para simplificar, usaremos a implementação MLP oferecida pela
biblioteca sklearn e um conjunto de dados simples. Os princípios que serão usados, no entanto, ainda se
aplicam a redes e conjuntos de dados mais complexos.

Na próxima seção, descobriremos como a arquitetura do MLP pode ser otimizada usando um algoritmo
genético.

[ 227 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

Otimizando a arquitetura de um classificador


de aprendizado profundo
Ao criar um modelo de rede neural para que possamos realizar uma determinada tarefa de aprendizado de máquina,
uma decisão de design crucial que precisa ser tomada é a configuração da arquitetura de rede. No caso do Perceptron
Multilayer, o número de nós nas camadas de entrada e saída é determinado pelas características do problema em
questão. Portanto, as escolhas a serem feitas são sobre as camadas ocultas – quantas camadas e quantos nós
em cada camada. Algumas regras práticas podem ser empregadas para tomar essas decisões, mas, em muitos casos,
identificar as melhores escolhas pode se transformar em um complicado processo de tentativa e erro.

Uma maneira de lidar com os parâmetros da arquitetura de rede é considerá-los como


hiperparâmetros do modelo, pois eles precisam ser determinados antes do treinamento e afetam os resultados do
treinamento. Nesta seção, vamos aplicar essa abordagem e usar a abordagem de algoritmos genéticos para encontrar
a melhor combinação de camadas ocultas, de maneira semelhante à maneira como escolhemos os melhores
valores de hiperparâmetros no capítulo anterior. Vamos começar com a tarefa que queremos enfrentar – a classificação
da flor de íris.

O conjunto de dados da flor Iris


Talvez o conjunto de dados mais bem estudado, o conjunto de dados da flor Iris (https://archive.ics.uci.edu/ml/
datasets/Iris ) contém medições das partes sépalas e pétalas de três espécies de Iris (Iris setosa, Iris virginica e
Iris versicolor), tomadas por biólogos em 1936.

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:

de conjuntos de dados de importação do sklearn

dados = conjuntos de dados.load_iris()


X = dados['dados']
y = dados['alvo']

[ 228 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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.

Representando a configuração da camada oculta


Como a arquitetura do MLP é determinada pela configuração da camada oculta, vamos explorar como essa
configuração pode ser representada em nossa solução. A configuração da camada oculta do sklearn
Multilayer Perceptron (https://scikit-learn.org/stable/modules/neural_networks_supervised.html ) model é
transmitido por meio da tupla hidden_layer_sizes , que é enviada como um parâmetro
para o construtor do modelo.
Por padrão, o valor dessa tupla é (100,), o que significa uma única camada oculta de 100 nós.
Se quiséssemos, por exemplo, configurar o MLP com três camadas ocultas de 20 nós cada, o valor deste
parâmetro seria (20, 20, 20). Antes de implementarmos nosso otimizador baseado em algoritmo genético para
a configuração da camada oculta, precisamos definir um cromossomo que possa ser traduzido nesse padrã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:

[n1, n2, n3, n4]

Aqui, ni denota o número de nós na camada i.

[ 229 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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:

O cromossomo [9.35, 10.71, -2.51, 17.99] é traduzido na tupla (9, 11)


O cromossomo [9.35, 10.71, 2.51, -17.99] é traduzido na tupla (9, 11, 3)

Para avaliar um determinado cromossomo que representa a arquitetura, precisaremos traduzi-lo de


volta na tupla de camadas, criar o classificador MLP implementando essas camadas, treiná-lo e
avaliá-lo. Aprenderemos como fazer isso na próxima subseção.

Avaliando a precisão do classificador


Vamos começar com uma classe Python que encapsula a avaliação de precisão do classificador
MLP para o conjunto de dados Iris. A classe é chamada MlpLayersTest e
pode ser encontrada no arquivo mlp_layers_test.py , localizado em https://github.com/PacktPublishing/
Hands-On-Genetic-Algorithms-with-Python/blob/master/Chapter09/mlp_layers_test .
py.

[ 230 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

A principal funcionalidade desta classe é destacada a seguir:

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 = (round(params[0]), round(params[1]), round(params[2]),


round(params[3]))

2. O método getAccuracy() pega a lista de parâmetros que representa o


configuração das camadas ocultas, usa o método convertParam() para transformá-lo na
tupla hidden_layer_sizes e inicializa o classificador MLP com esta tupla:

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:

cv_results = model_selection.cross_val_score(self.classifier, self.X, self.y, cv=self.kfold,

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

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

Otimizando a arquitetura MLP usando


algoritmos genéticos
Agora que temos uma maneira de representar a configuração da arquitetura do MLP que é usado para
classificar o conjunto de dados da flor Iris e uma maneira de determinar a precisão do MLP para cada
configuração, podemos seguir em frente e criar um otimizador baseado em algoritmo genético para
pesquisar para a configuração – o número de camadas ocultas (até 4, no nosso caso) e o número de
nós em cada camada – que produzirá a melhor precisão. Esta solução é implementada pelo programa
Python 01-optimize-mlp-layers.py, localizado em https://github.com/PacktPublishing/Hands-On-Genetic-
Algorithms-with-Python/blob/master/Chapter09/ 01-optimize-mlp-layers.py.

As etapas a seguir descrevem as partes principais deste programa:

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:

# [layer_layer_1_size, hidden_layer_2_size, hidden_layer_3_size,


hidden_layer_4_size]
BOUNDS_LOW = [ 5, -5, -10, -20]
BOUNDS_HIGH = [15, 10, 10, 10]

2. Em seguida, criamos uma instância da classe MlpLayersTest , que nos permitirá


teste as várias combinações da arquitetura das camadas ocultas:

teste = mlp_layers_test.MlpLayersTest(RANDOM_SEED)

3. Como nosso objetivo é maximizar a precisão do classificador, definimos um único objetivo,


maximizando a estratégia de aptidão:

criador.create("FitnessMax", base.Fitness, pesos=(1.0,))

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:

para i no intervalo (NUM_OF_PARAMS):


# "layer_size_attribute_0", "layer_size_attribute_1", ... toolbox.register("layer_size_attribute_"
+ str(i),

[ 232 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

random.uniform,
BOUNDS_LOW[i],
BOUNDS_HIGH[i])

5. Em seguida, criamos a tupla layer_size_attributes , que contém os geradores de


números flutuantes separados que acabamos de criar para cada camada oculta:

layer_size_attributes = () para i no intervalo


(NUM_OF_PARAMS):
layer_size_attributes = layer_size_attributes + \
(toolbox.__getattribute__("layer_size_attribute_" + str(i)),)

6. Agora, podemos usar esta tupla layer_size_attributes em conjunto com o operador


initCycle() integrado do DEAP para criar um novo
operador individualCreator que preenche uma instância individual com uma combinação
de valores de tamanho de camada oculta gerados aleatoriamente:

caixa de ferramentas.register("criador individual",


tools.initCycle,
criador.Individual,
layer_size_attributes, n=1)

7. Em seguida, instruímos o algoritmo genético a usar o método getAccuracy() do


Instância MlpLayersTest para avaliação de adequação. Como lembrete,
o método getAccuracy() , que descrevemos na subseção anterior, converte o dado
individual – uma lista de quatro floats – em uma tupla de tamanhos de camadas ocultas.
Estes são usados para configurar o classificador MLP. Em seguida, treinamos o
classificador e avaliamos sua precisão usando validação cruzada k-fold:

classificação def Precisão(individual):


return test.getAccuracy(individual),

toolbox.register("avaliar", classificaçãoPrecisão)

8. Quanto aos operadores genéticos, repetimos a configuração do anterior


capítulo. Enquanto para o operador de seleção, usamos a seleção de torneio usual com
um tamanho de torneio de 2, escolhemos operadores de cruzamento e mutação que são
especializados para cromossomos de lista flutuante limitada e fornecemos a eles os
limites que definimos para cada camada oculta:

toolbox.register("select", tools.selTournament, tournsize=2)

toolbox.register("mate",
tools.cxSimulatedBinaryBounded, low=BOUNDS_LOW,

[ 233 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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)

9. Além disso, continuamos a usar a abordagem elitista, onde o hall da fama


(HOF) membros - os melhores indivíduos atuais - são sempre passados intactos para
a próxima geração:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas, cxpb=P_CROSSOVER,

mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats, halloffame=hof, verbose=True)

Ao executar o algoritmo por 10 gerações com um tamanho de população de 20, obtemos


o seguinte resultado:

gen nevals max avg 0 20 0,666667


0,416333
1 17 0,693333 0,487
2 15 0,76 0,537333
3 14 0,76 0,550667
4 17 0,76 0,568333
5 17 0,76 0,653667
6 14 0,76 0,589333
7 15 0,76 0,618
8 16 0,866667 0,616667
9 16 0,866667 0,666333
10 16 0,866667 0,722667

- A melhor solução é: 'hidden_layer_sizes'=(15, 5, 8) 0,8666666666666666 , precisão =

[ 234 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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.

Combinando otimização de arquitetura


com ajuste de hiperparâmetros
Ao otimizar a configuração da arquitetura de rede - os parâmetros da camada oculta
- usamos os parâmetros padrão do classificador MLP. No entanto, como vimos no capítulo anterior,
ajustar os vários hiperparâmetros tem o potencial de aumentar o desempenho do classificador.
Podemos incorporar o ajuste de hiperparâmetros em nossa otimização? Como você deve ter
adivinhado, a resposta é sim. Mas primeiro, vamos dar uma olhada nos hiperparâmetros que
gostaríamos de otimizar.

A implementação sklearn do classificador MLP contém vários hiperparâmetros ajustáveis.


Para nossa demonstração, vamos nos concentrar nos seguintes hiperparâmetros:

Nome Digite Descrição Valor padrão

{'tanh', 'relu', 'logística'} Função de ativação para as


ativação 'replay'
camadas ocultas
O solucionador para
solucionador otimização de peso {'sgd', 'adam', 'lbfgs'} 'Adão'

alfa flutuador Parâmetro do termo de regularização 0,0001

{'constante',
Programação da taxa de aprendizado
learning_rate 'invscaling', 'adaptável'} 'constante'
para atualizações de peso

[ 235 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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:

a ativação pode ter um dos três valores: 'tanh', 'relu' ou 'logistic'.


Isso pode ser obtido representando-o como um número flutuante no intervalo de [0, 2,99].
Para transformar o valor float em um dos valores mencionados acima, precisamos aplicar a
função floor() a ele, que resultará em 0, 1 ou 2. Em seguida, substituímos um valor de 0 por
tanh, um valor de 1 por relu e um valor de 2 com logística. solver pode ter um dos três valores:
'sgd', 'adam'
ou 'lbfgs'. Assim como o parâmetro de ativação , ele pode ser representado usando um número
flutuante no intervalo de [0, 2,99]. alpha já é float, então nenhuma conversão é necessária. Ele
será limitado ao
intervalo de [0,0001, 2,0]. learning_rate pode ter um dos três valores: 'constant', 'invscaling' ou
'adaptive'. Mais uma
vez, podemos usar um número flutuante no intervalo de [0, 2,99] para representar seu valor.

Avaliando a precisão do classificador


A classe que será utilizada para avaliar a precisão do classificador MLP para determinada
combinação de camadas ocultas e hiperparâmetros é denominada MlpHyperparametersTest e está
contida no arquivo mlp_hyperparameters_test.py, localizado em https:// github.com/PacktPublishing/
Hands -On-Genetic-Algorithms-with-Python/blob/master/ Chapter09/mlp_hyperparameters_test.py.

[ 236 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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 = ['tanh', 'relu', 'logistic'][floor(params[4])] solver = ['sgd', 'adam', 'lbfgs'][floor(params[5])]


alpha = params[6] learning_rate = ['constant', 'invscaling', 'adaptive']
[floor(params[7])]

2. Da mesma forma, o método getAccuracy() agora manipula a lista de parâmetros aumentada .


Ele configura o classificador MLP com os valores convertidos de todos esses parâmetros, em vez de
apenas a configuração da camada oculta:

hiddenLayerSizes, ativação, solucionador, alfa, learning_rate = self.convertParams(params)

self.classifier = MLPClassifier(random_state=self.randomSeed, hidden_layer_sizes=hiddenLayerSizes,

ativação=ativação,
solucionador=resolvedor,
alpha=alfa,
learning_rate=learning_rate)

Essa classe MlpHyperparametersTest é utilizada pelo otimizador baseado em algoritmo genético.


Veremos isso na próxima seção.

Otimizando a configuração combinada do


MLP usando algoritmos genéticos
A busca baseada em algoritmo genético para a melhor combinação de camadas ocultas e
hiperparâmetros é implementada pelo programa Python 02-optimize-mlp hyperparameters.py,
localizado em https://github.com/PacktPublishing/Hands On-Genetic-Algorithms -with-Python/blob/master/
Chapter09/02-optimize-mlp
hyperparameters.py.

[ 237 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

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:

# 'hidden_layer_sizes': primeiros quatro valores # 'activation': # 'solver':


0..2.99
0..2.99

# 'alpha': # 0.0001..2.0

'learning_rate': 0..2.99 BOUNDS_LOW = [ 5, -5, -10,


-20, 0, BOUNDS_HIGH = [15, 10, 10, 10, 2,999, 2,999, 2,0, 0, 0,0001, 0 ]
2.999]

E isso é tudo – o programa é capaz de lidar com os parâmetros adicionados sem nenhuma
alteração adicional.

A execução do algoritmo por cinco gerações com um tamanho de população de 20 produz


o seguinte resultado:

gen nevals max avg 0 20 0,933333


0,447333
1 16 0,933333 0,631667
2 15 0,94 0,736667
3 16 0,94 0,849
4 15 0,94 0,889667
5 17 0,946667 0,937
- A melhor solução é:

'hidden_layer_sizes'=(8, 8) 'ativação'='relu'

'solver'='lbfgs'
'alpha'=0.572971105096338
'learning_rate'='invscaling' => precisão =
0.9466666666666667

Observe que, devido a variações entre sistemas operacionais, os resultados


que serão produzidos ao executar este programa em seu sistema podem ser
um pouco diferentes do que está sendo mostrado aqui.

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:

Duas camadas ocultas, de 8 nós cada


O parâmetro de ativação do tipo 'relu' - o mesmo que o valor padrão

[ 238 ]
Machine Translated by Google

Otimização de Arquitetura de Redes de Deep Learning Capítulo 9

O parâmetro do solucionador do tipo 'lbfgs' - em vez do padrão 'adam'


O parâmetro learning_rate do tipo 'invscaling' – em vez do padrão 'constant'

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.

Até agora, nos concentramos no aprendizado supervisionado. No próximo capítulo, examinaremos a


aplicação de algoritmos genéticos ao aprendizado por reforço, um ramo empolgante e em rápido
desenvolvimento do aprendizado de máquina.

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

Aprendizagem por Reforço com


10
Algorítmos genéticos
Neste capítulo, demonstraremos como os algoritmos genéticos podem ser aplicados ao
aprendizado por reforço – um ramo do aprendizado de máquina em rápido desenvolvimento capaz de
lidar com tarefas complexas. Faremos isso resolvendo dois ambientes de referência do kit de ferramentas
OpenAI Gym. Começaremos fornecendo uma visão geral do aprendizado por reforço, seguido por uma
breve introdução ao OpenAI Gym, um kit de ferramentas que pode ser usado para comparar e desenvolver
algoritmos de aprendizado por reforço, bem como uma descrição de sua interface baseada em Python.
Em seguida, realizaremos dois ambientes de Ginásio, MountainCar e CartPole, e desenvolveremos
programas baseados em algoritmos genéticos para resolver os desafios que eles apresentam.

Neste capítulo, abordaremos os seguintes tópicos:

Compreendendo os conceitos básicos do aprendizado por reforço


Familiarizar-se com o projeto OpenAI Gym e sua interface compartilhada
Usando algoritmos genéticos para resolver o ambiente OpenAI Gym MountainCar
Usando algoritmos genéticos, em combinação com uma rede neural, para resolver o
Ambiente OpenAI Gym CartPole

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido

aprendido

ginásio—apresentado neste capítulo

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.

Confira o vídeo a seguir para ver o Código em ação:


http://bit.ly/2Oey7V0

Aprendizagem por reforço


Nos capítulos anteriores, cobrimos vários tópicos relacionados ao aprendizado de máquina e focamos
em tarefas de aprendizado supervisionado. Embora o aprendizado supervisionado seja imensamente
importante e tenha muitas aplicações na vida real, o aprendizado por reforço atualmente parece ser o
ramo mais empolgante e promissor do aprendizado de máquina. As razões para essa empolgação
incluem as tarefas complexas e cotidianas com as quais o aprendizado por reforço tem o potencial de
lidar. Em março de 2016, o AlphaGo, um sistema baseado em aprendizado por reforço especializado
no jogo altamente complexo de Go, conseguiu derrotar o considerado o maior jogador de Go da última
década em uma competição amplamente divulgada pela mídia.

Embora o aprendizado supervisionado exija dados rotulados para treinamento - em outras


palavras, pares de entradas e saídas correspondentes - o aprendizado por reforço não
apresenta feedback imediato de certo/errado ; em vez disso, fornece um ambiente em que
uma recompensa cumulativa de longo prazo é buscada. Isso significa que, às vezes, um algoritmo
precisará dar um passo momentâneo para trás para eventualmente atingir um objetivo de
longo prazo, como será demonstrado em nosso primeiro exemplo deste capítulo.

[ 241 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

Algoritmos Genéticos e Aprendizado por Reforço


Vários algoritmos dedicados foram desenvolvidos para realizar tarefas de aprendizado por reforço - Q-Learning,
SARSA e DQN, para citar alguns. No entanto, como as tarefas de aprendizado por reforço envolvem a
maximização de uma recompensa de longo prazo, podemos considerá-las como problemas de
otimização. Como vimos ao longo deste livro, os algoritmos genéticos podem ser usados para resolver
problemas de otimização de vários tipos. Portanto, os algoritmos genéticos também podem ser utilizados para
aprendizado por reforço, e de várias maneiras diferentes — duas delas serão demonstradas neste capítulo.
No primeiro caso, nossa solução baseada em algoritmo genético fornecerá diretamente a série ótima de ações
do agente. No segundo caso, fornecerá os parâmetros ótimos para o controlador neural que fornece essas
ações.

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:

pip instalar academia

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.

A próxima subseção descreve a interação com a interface env .

[ 243 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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()

Este método retorna um objeto de observação , descrevendo o estado inicial do ambiente.


O conteúdo da observação depende do ambiente.

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:

observação, recompensa, feito, info = env.step(action)

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

A apresentação renderizada é específica do ambiente.

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.

Informações detalhadas sobre a interface env estão disponíveis em http://gym.


openai.com/docs/.

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 .

Resolvendo o ambiente MountainCar


O ambiente MountainCar-v0 simula um carro em uma pista unidimensional, situada entre duas colinas.
A simulação começa com o carro colocado entre as colinas, conforme mostrado na seguinte saída renderizada:

Simulação MountainCar - ponto de partida

[ 245 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

O objetivo é fazer com que o carro suba a colina mais alta - a da direita - e, por fim, bata na bandeira:

Simulação MountainCar - carro subindo a colina à direita

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:

Simulação de MountainCar - carro quicando na colina à esquerda

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

O valor da ação esperada nesta simulação é um número inteiro com um dos três valores a seguir:

0: Empurre para a esquerda

1: Sem empurrão

2: Empurre para a direita

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.

No momento da redação deste artigo, os requisitos resolvidos do ambiente da academia para o


desafio MountainCar ainda não foram definidos, portanto, simplesmente tentaremos acertar a bandeira em 200
passos ou menos quando dada uma posição inicial fixa e usando uma sequência de ações. Para encontrar uma
sequência de ações que farão com que o carro suba a colina alta e bata na bandeira, criaremos uma
solução baseada em algoritmo genético. Como sempre, começaremos definindo como será uma solução
candidata para esse desafio.

Mais informações sobre o ambiente MountainCar-v0 podem ser encontradas nos seguintes links:

MountainCar-v0: https://gym.openai.com/envs/MountainCar-v0/ MountainCar-v0:


https://github.com/openai/gym/wiki/MountainCar-v0

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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.

Este procedimento de avaliação de pontuação é implementado pela classe MountainCar , que


será descrita na próxima subseção.

Representação do problema Python


Para encapsular o desafio MountainCar , criamos uma classe Python chamada
MountainCar. Essa classe está contida no arquivo mountain_car.py , localizado em https://
github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/blob/master/Chapter10/
mountain_car.py .

A classe é inicializada com uma semente aleatória e fornece os seguintes métodos:

getScore(actions): Calcula a pontuação de uma determinada solução, representada pela lista de


ações. A pontuação é calculada iniciando um episódio do ambiente MountainCar e
executando-o com as ações fornecidas. Quanto menor a pontuação, melhor. saveActions(actions):
Isso salva uma lista de
ações em um arquivo usando pickle (módulo de serialização e desserialização de objetos
do Python). replaySavedActions(): desserializa a última lista de ações
salvas e a repete usando o método replay . replay(actions): Isso renderiza o ambiente e repete
a lista de ações fornecida para visualizar
uma determinada solução.

[ 248 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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.

Solução de algoritmos genéticos


Para enfrentar o desafio MountainCar usando a abordagem de algoritmos genéticos, criamos o programa Python,
01-solve-mountain-car.py, localizado em https://github. com/PacktPublishing/Hands-On-Genetic-Algorithms-
with-Python/blob/master/ Chapter10/01-solve-mountain-car.py.

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.

As etapas a seguir descrevem as partes principais deste programa:

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:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

4. Isso é seguido por um operador que preenche uma instância individual com esses
valores:

caixa de ferramentas.register("criador individual",


tools.initRepeat,
criador.Individual,
toolbox.zeroOneOrTwo, len(carro))

5. Em seguida, instruímos o algoritmo genético a usar o método getScore() do


Instância MountainCar para avaliação de condicionamento físico.

Como lembrete, o método getScore() , que descrevemos na subseção anterior, inicia


um episódio do ambiente MountainCar e usa o indivíduo fornecido — uma lista de
ações — como entradas para o ambiente até que o episódio termine. Em seguida,
avalia a pontuação – quanto menor, melhor – de acordo com a localização final do carro.
Se o carro bater na bandeira, a pontuação pode ficar ainda menor, de acordo com o
número de passos que deu para chegar lá:
def carScore(individual): return
car.getScore(individual),

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

7. Além disso, continuamos a usar a abordagem elitista, onde o hall da fama


(HOF) membros - os melhores indivíduos atuais - são sempre passados intactos para a próxima
geração:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,

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:

best = hof.items[0] print("Best


Solution = ", best) print("Best Fitness = ",
best.fitness.values[0]) car.saveActions(best)

Executando o algoritmo para 80 gerações e com um tamanho de população de 100, obtemos o seguinte
resultado:

gen nevals min avg 0 100


0,659205 1,02616
1 78 0,659205 0,970209
...
60 75 0,00367593 0,100664
61 73 0,00367593 0,0997352
62 77 -0,005 0,100359
63 73 -0,005 0,103559
...
67 78 -0,015 0,0679005
68 80 -0,015 0,0793169
...
79 76 -0,02 0,020927
80 76 -0,02 0,0175934

Melhor solução = [0, 1, 2, 0, 0, 1, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 0, ..., 1, 0, 2, 2, 0, 2, 1]

Melhor condicionamento físico = -0,02

[ 251 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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:

Simulação MountainCar - carro atingindo a meta

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.

Resolvendo o ambiente CartPole


O ambiente CartPole-v1 simula um ato de equilíbrio de um poste, articulado em sua parte inferior a um carrinho,
que se move para a esquerda e para a direita ao longo de uma pista. O equilíbrio da vara na vertical é feito
aplicando-se ao carrinho uma unidade de força - para a direita ou para a esquerda - de cada vez.

[ 252 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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:

Simulação CartPole - ponto de partida

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 ângulo do pólo da posição vertical excede 15 graus.


A distância do carrinho do centro excede 2,4 unidades.

Consequentemente, a recompensa total nesses casos será menor que 500.

O valor da ação esperada nesta simulação é um número inteiro de um dos dois valores a seguir:

0: Empurre o carrinho para a esquerda

1: Empurre o carrinho para a direita

O objeto de observação contém quatro flutuadores que descrevem as seguintes informações:

Posição do carrinho (entre -2,4 e 2,4)


Velocidade do carrinho (entre -Inf e Inf)
Ângulo polar (entre -41,8° e 41,8°)
Velocidade do pólo na ponta do pólo (entre -Inf e Inf)

Por exemplo, poderíamos ter [ 0,33676587, 0,3786464, -0,00170739, -0,36586074].

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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

Controlando o CartPole com uma rede neural


Para realizar com sucesso o desafio CartPole, gostaríamos de responder dinamicamente às mudanças no ambiente.
Por exemplo, quando o mastro começa a se inclinar em uma direção, provavelmente deveríamos mover o carrinho
naquela direção, mas possivelmente parar de empurrar quando ele começar a se estabilizar. Portanto, a tarefa
de aprendizado por reforço aqui pode ser considerada como ensinar um controlador a equilibrar o bastão mapeando
as quatro entradas disponíveis - posição do carrinho, velocidade do carrinho, ângulo do bastão e velocidade do
bastão - na ação apropriada em cada passo de tempo. Como podemos implementar esse mapeamento?

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:

Estrutura da rede neural usada para controlar o carrinho

[ 254 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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.

Representação e avaliação da solução


Como decidimos controlar o carrinho no desafio CartPole usando uma rede neural do tipo Multilayer
Perceptron, o conjunto de parâmetros que precisaremos otimizar são os pesos e bias da rede, conforme
segue:

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:

[0.9505049282421143, -0.8068797228337171, -0.45488246459260073, -0.7598208314027836, ... ,


0.4039043861825575,
-0.874433212682847, 0.9461075409693256, 0.6720551701599038]

[ 255 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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.

Representação do problema Python


Para encapsular o desafio CartPole, criamos uma classe Python chamada CartPole. Esta classe está contida no
arquivo cart_pole.py , localizado em https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-
Python/blob/master/Chapter10/
cart_pole.py.

A classe é inicializada com uma semente aleatória opcional e fornece os seguintes métodos:

initMlp(): Inicializa um regressor Multilayer Perceptron com a arquitetura de rede desejada


(camadas) e parâmetros de rede (pesos e vieses), que são derivados da lista de flutuadores
que representam uma solução candidata. getScore(): Calcula a pontuação de uma
determinada solução, representada pela lista de parâmetros de rede com valor flutuante. Isso é feito
criando um regressor MLP correspondente, iniciando um episódio do ambiente CartPole e
executando-o com o MLP controlando as ações, enquanto usa as observações como entradas.

Quanto maior a pontuação, melhor.


saveParams(): Serializa e salva uma lista de parâmetros de rede usando pickle.

replayWithSavedParams(): desserializa a última lista salva de parâmetros de rede e


a usa para reproduzir um episódio usando o método de repetição . replay():
Renderiza o ambiente e usa os parâmetros de rede fornecidos para reproduzir um
episódio, visualizando uma determinada solução.

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

Solução de algoritmos genéticos


Para interagir com o ambiente CartPole e resolvê-lo usando um algoritmo genético, criamos o programa Python, 02-solve-
cart-pole.py, que está localizado em https:// github.com/PacktPublishing/Hands-On- Genetic-Algorithms-with-Python/
blob/master/ Chapter10/02-solve-cart-pole.py.

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.

As etapas a seguir descrevem as partes principais deste programa:

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:

BOUNDS_LOW, BOUNDS_HIGH = -1,0, 1,0

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:

criador.create("FitnessMax", base.Fitness, pesos=(1.0,))

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:

def randomFloat(baixo, cima):


return [random.uniform(l, u) for l, u in zip([low] *
NUM_OF_PARAMS, [acima] * NUM_OF_PARAMS)]

5. Agora, usamos esta função para criar um operador que retorna aleatoriamente uma lista de
flutua no intervalo desejado que definimos anteriormente:

toolbox.register("attrFloat", randomFloat, BOUNDS_LOW, BOUNDS_HIGH)

[ 257 ]
Machine Translated by Google

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

6. Isso é seguido por um operador que preenche uma instância individual usando o
operador precedente:

caixa de ferramentas.register("criador individual",


tools.initIterate,
criador.Individual, toolbox.attrFloat)

7. Em seguida, instruímos o algoritmo genético a usar o método getScore() do


Instância CartPole para avaliação de aptidão.

Como lembrete, o método getScore() , que descrevemos na subseção anterior, inicia


um episódio do ambiente CartPole. Durante este episódio, o carrinho é controlado por um
MLP de camada única oculta. Os valores de peso e viés deste MLP são preenchidos pela
lista de floats que representam a solução atual.
Ao longo do episódio, o MLP mapeia dinamicamente os valores de observação do
ambiente para uma ação de direita ou esquerda. Terminado o episódio, a pontuação é a
recompensa total, que equivale à quantidade de passos de tempo que o MLP conseguiu
manter o bastão equilibrado — quanto maior, melhor:

pontuação def (individual):


retornar carrinhoPole.getScore(individual),

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("select", tools.selTournament, tournsize=2)

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,

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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

É 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:

gen nevals max avg 0 20 41 13,7

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

Melhor solução = [0,733351790484474, -0,8068797228337171, -0,45488246459260073, ...

Melhor pontuação = 500,0

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:

Executando 100 episódios usando a melhor solução... pontuações = [500.0, 500.0,


500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0, 500.0,

...
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

Aprendizagem por Reforço com Algoritmos Genéticos Capítulo 10

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.

No próximo capítulo, faremos a transição para o mundo da arte e descobriremos como os


algoritmos genéticos podem ser usados para reconstruir imagens de pinturas famosas com um
conjunto de formas sobrepostas semitransparentes.

Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes recursos:

Python Reinforcement Learning, Rajalingappaa Shanmugamani e Sudharsan Ravichandiran,


et al., 17 de abril de 2019 Deep
Reinforcement Learning Hands-On, Maksim Lapan, 21 de junho de 2018
Documentação do OpenAI Gym: http://gym.openai.com/docs/ OpenAI
Gym (White Paper), Greg Brockman, Vicki Cheung, Ludwig Pettersson, Jonas Schneider, John
Schulman, Jie Tang, Wojciech Zaremba: https://arxiv. org/abs/1606.01540

[ 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.

Esta seção compreende os seguintes capítulos:

Capítulo 11, Reconstrução da Imagem Genética


Capítulo 12, Outras técnicas de computação evolutivas e bioinspiradas
Machine Translated by Google

Reconstrução de Imagem Genética


11
Neste capítulo, vamos experimentar uma das formas mais populares de aplicação de algoritmos genéticos
ao processamento de imagens – a reconstrução de uma imagem com um conjunto de polígonos semitransparentes.
Ao longo do caminho, ganharemos uma experiência útil no processamento de imagens, juntamente com uma
visão visual do processo evolutivo.

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.

Neste capítulo, abordaremos os seguintes tópicos:

Familiarizando-se com várias bibliotecas de processamento de imagem para Python


Entendendo como desenhar programaticamente uma imagem usando polígonos
Descobrir como comparar programaticamente duas imagens fornecidas
Usando algoritmos genéticos, em combinação com bibliotecas de processamento de imagem,
para reconstruir uma imagem usando polígonos

Começaremos este capítulo fornecendo uma visão geral da tarefa de reconstrução de imagens.
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Requerimentos técnicos
Neste capítulo, usaremos o Python 3 com as seguintes bibliotecas de suporte:

profundo

entorpecido

matplotlib
nascido no mar

Travesseiro (garfo PIL) – apresentado neste capítulo scikit-

image (skimage) – apresentado neste capítulo

OpenCV-Python (cv2) – apresentado neste capítulo

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.

Confira o vídeo a seguir para ver o código em ação:


http://bit.ly/2u1ytHz

Reconstruindo imagens com polígonos


Um dos exemplos mais populares do uso de algoritmos genéticos no processamento de imagens é a
reconstrução de uma determinada imagem com um conjunto de formas semitransparentes e sobrepostas. Além
do aspecto divertido e da oportunidade de ganhar experiência no processamento de imagens,
esses experimentos fornecem uma excelente visão visual do processo evolutivo e podem levar a uma
melhor compreensão das artes visuais, bem como ao desenvolvimento da análise e compressão de imagens.

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

Reconstrução de Imagem Genética Capítulo 11

Processamento de imagens em Python


Para atingir nosso objetivo, precisaremos realizar várias operações de processamento de imagem; por
exemplo, precisaremos criar uma imagem do zero, desenhar formas em uma imagem, plotar uma imagem,
abrir um arquivo de imagem, salvar uma imagem em um arquivo, comparar duas imagens e possivelmente
redimensionar uma imagem. Nas seções a seguir, exploraremos algumas das maneiras pelas quais essas
operações podem ser executadas ao usar o Python.

Bibliotecas de processamento de imagem Python


Da riqueza de bibliotecas de processamento de imagens disponíveis para programadores Python,
escolhemos usar três das mais proeminentes. Essas bibliotecas serão brevemente discutidas nas subseções
a seguir.

A biblioteca Pillow Pillow é uma


bifurcação atualmente mantida da Python Imaging Library (PIL) original. Ele oferece suporte para
abrir, manipular e salvar arquivos de imagem de vários formatos. Como nos permite manipular arquivos de
imagem, desenhar formas, controlar sua transparência e manipular pixels, vamos usá-lo como nossa
ferramenta principal na criação da imagem reconstruída.

A página inicial desta biblioteca pode ser encontrada aqui: https://python-pillow.org/.

Uma instalação típica do Pillow usa o comando pip , como segue:

pip instalar travesseiro

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.

A biblioteca scikit-image A biblioteca scikit-


image , desenvolvida pela comunidade SciPy, estende o scipy.image e fornece uma coleção de
algoritmos para processamento de imagem, incluindo E/S de imagem, filtragem, manipulação de cores
e detecção de objetos. Aqui, utilizaremos apenas seu módulo de métricas , que é usado para comparar duas
imagens.

[ 265 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

A página inicial desta biblioteca pode ser encontrada aqui: https://scikit-image.org/.

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:

pip instalar scikit-image

Se você estiver executando o Anaconda ou miniconda, use o seguinte comando:


conda install -c conda-forge scikit-image

Mais informações podem ser encontradas na documentação do scikit-image , localizada em


https://scikit-image.org/docs/stable/index.html.

A biblioteca opencv-python OpenCV é uma


biblioteca elaborada que fornece vários algoritmos relacionados à visão computacional e
aprendizado de máquina. Ele suporta uma ampla variedade de linguagens de programação e está
disponível em diferentes plataformas. opencv-python é a API Python para esta biblioteca. Ele
combina a velocidade da API C++ com a facilidade de uso da linguagem Python. Aqui, usaremos
esta biblioteca principalmente para calcular a diferença entre duas imagens, pois ela nos permite
representar uma imagem como uma matriz numérica.

A página inicial do opencv-python pode ser encontrada aqui: https://pypi.org/project/opencv-


python/ .

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

Mais informações podem ser encontradas na documentação do OpenCV, localizada em https://


docs.opencv.org/master/ .

[ 266 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Desenhar imagens com polígonos


Para desenhar uma imagem do zero, podemos usar as classes Image e ImageDraw de Pillow , conforme
a seguir:

image = Image.new('RGB', (largura, altura)) draw =


ImageDraw.Draw(image, 'RGBA')

'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.

Agora, podemos adicionar um polígono à imagem base usando a função de polígono


da classe ImageDraw , conforme mostrado no exemplo a seguir. A seguinte declaração irá desenhar um
triângulo na imagem:

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

Reconstrução de Imagem Genética Capítulo 11

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:

Um gráfico de polígonos sobrepostos com cores e valores de opacidade variados

Depois de desenhar uma imagem usando polígonos, precisamos compará-la com a imagem de referência, conforme
descrito na próxima subseção.

Medindo a diferença entre as imagens


Como queremos construir uma imagem o mais semelhante possível à original, precisamos de uma forma de avaliar a
semelhança ou a diferença entre as duas imagens dadas.
Dois métodos possíveis são os seguintes:

Erro quadrático médio baseado em pixel (MSE)


Semelhança Estrutural (SSIM)

Vamos discutir ambos em detalhes.

[ 268 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Erro quadrático médio baseado em pixel A


maneira mais comum de avaliar a similaridade entre imagens é realizando uma
comparação pixel a pixel. Isso requer, é claro, que ambas as imagens tenham as mesmas dimensõe
A métrica MSE pode ser calculada da seguinte forma:

1. Calcule o quadrado da diferença entre cada par de pixels correspondentes de ambas as


imagens. Como cada pixel no desenho é representado usando três valores separados –
vermelho, verde e azul – a diferença para cada pixel é calculada nessas três dimensões.

2. Calcule a soma de todos esses quadrados.


3. Divida a soma pelo número total de pixels.

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:

MSE = np.sum((cv2Image1.astype("float") - cv2Image2.astype("float")) ** 2)/float(numPixels)

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.

Semelhança Estrutural (SSIM)


O índice SSIM foi criado para ser usado para prever a qualidade da imagem produzida por um
determinado algoritmo de compactação, comparando a imagem compactada com a original.
Ao invés de calcular um valor de erro absoluto, que é feito pelo método MSE, por exemplo, o SSIM é
baseado na percepção e considera mudanças nas informações estruturais, além de efeitos como brilho e
textura nas imagens.

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:

SSIM = similaridade_estrutural(cv2Image1, cv2Image2)

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

Reconstrução de Imagem Genética Capítulo 11

Usando algoritmos genéticos para


reconstruir imagens
Como discutimos anteriormente, nosso objetivo neste experimento é usar uma imagem familiar
como referência e criar uma segunda imagem, o mais semelhante possível à referência,
usando uma coleção de polígonos sobrepostos de cores e transparências variadas. Usando a
abordagem de algoritmos genéticos, cada solução candidata é um conjunto desses polígonos, e a
avaliação da solução é realizada criando uma imagem usando esses polígonos e comparando-a
com a imagem de referência. Como sempre, a primeira decisão que precisamos tomar é como
essas soluções são representadas. Discutiremos isso na próxima subseção.

Representação e avaliação da solução


Como mencionamos anteriormente, nossa solução consiste em um conjunto de polígonos dentro
dos limites da imagem. Cada polígono tem sua própria cor e transparência. Desenhar tal polígono
usando a biblioteca Pillow requer os seguintes argumentos:

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

Reconstrução de Imagem Genética Capítulo 11

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.1499488467959301, 0.3812631075049196, 0.0004394580562993053, 0.9988170920722447,


0.9975357316889601, 0.9997461395379549, 0.6338072268312986, 0.379170095245514,
0.29280945382368373, 0.20126488596803083,

...
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.

Representação do problema Python


Para encapsular o desafio de reconstrução de imagem, criamos uma classe Python
chamada ImageTest. Esta classe está contida no arquivo image_test.py , localizado em
https://github.com/PacktPublishing/Hands-On-Genetic-Algorithms-with-Python/
blob/master/Chapter11/image_test.py.

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:

polygonDataToImage(): aceita a lista contendo os dados do polígono que discutimos na subseção


anterior, divide essa lista em partes que representam polígonos individuais e cria uma imagem contendo
esses polígonos desenhando os polígonos um a um em uma imagem em branco. getDifference(): Aceita
dados de polígonos, cria uma imagem contendo esses
polígonos e calcula a diferença entre esta imagem e a imagem de referência usando um dos dois
métodos – MSE ou SSIM.

[ 271 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

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.

Implementação de algoritmo genético


Para reconstruir uma determinada imagem com um conjunto de polígonos sobrepostos semitransparentes usando um
algoritmo genético, criamos um programa Python chamado 01-reconstruct-with polygons.py, localizado em
https://github.com/PacktPublishing/ Hands-On Genetic-Algorithms-with-Python/blob/master/Chapter11/reconstruct-
with-polygons.
py.

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 .

As etapas a seguir descrevem as partes principais deste programa:

1. Começamos definindo os valores constantes relacionados ao problema. POLYGON_SIZE


determina o número de vértices para cada polígono, enquanto NUM_OF_POLYGONS
determina o número total de polígonos que serão usados para criar a imagem
reconstruída:

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:

imageTest = image_test.ImageTest("images/Mona_Lisa_Head.png", POLYGON_SIZE)

[ 272 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

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:

BOUNDS_LOW, BOUNDS_HIGH = 0,0, 1,0

4. Como nosso objetivo é minimizar a diferença entre as imagens - a referência


imagem e a que estamos criando usando polígonos – definimos um único objetivo, minimizando a
estratégia de fitness:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

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:

def randomFloat(baixo, cima):


return [random.uniform(l, u) for l, u in zip([low] *
NUM_OF_PARAMS, [acima] * NUM_OF_PARAMS)]

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]:

toolbox.register("attrFloat", randomFloat, BOUNDS_LOW, BOUNDS_HIGH)

7. Isso é seguido pela definição de um operador que preenche uma instância individual usando o
operador anterior:

caixa de ferramentas.register("criador individual",


tools.initIterate,
criador.Individual,
toolbox.attrFloat)

8. Em seguida, instruímos o algoritmo genético a usar o método getDifference() de


a instância ImageTest para avaliação de adequação.

[ 273 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Como lembrete, o método getDifference() , que descrevemos na subseção anterior,


aceita o indivíduo que representa uma lista de polígonos, cria uma imagem contendo esses
polígonos e calcula a diferença entre essa imagem e a imagem de referência usando um
dos dois métodos – MSE ou SSIM. Para começar, usaremos o método MSE para
calcular a diferença:

def getDiff(individual):
return imageTest.getDifference(individual, "MSE"),

toolbox.register("avaliar", getDiff)

9. É hora de escolher nossos operadores genéticos. Para o operador de seleção, vamos


use a seleção de torneio com um tamanho de torneio de 2. Como vimos no Capítulo 4,
Otimização Combinatória, esse esquema de seleção funciona bem em conjunto com a
abordagem elitista que planejamos utilizar aqui também:

toolbox.register("select", tools.selTournament, tournsize=2)

10. Quanto ao operador de cruzamento (alias com mate) e o operador de mutação


(mutate), uma vez que nossa representação de solução é uma lista de floats
limitados a um intervalo, usaremos os operadores limitados contínuos especializados
fornecidos pela estrutura DEAP - cxSimulatedBinaryBounded e
mutPolynomialBounded, respectivamente – que vimos pela primeira vez no Capítulo
6, Otimizando funções contínuas:

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

Reconstrução de Imagem Genética Capítulo 11

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))

Antes de vermos os resultados, vamos discutir a implementação da função callback.

Adicionando um retorno de chamada à execução genética Para


poder salvar a melhor imagem atual a cada 100 gerações, precisamos introduzir uma modificação
no loop genético principal. Como você deve se lembrar, no final do Capítulo 4, Otimização
Combinatória, já fizemos uma modificação no loop principal que nos permitiu introduzir a
abordagem elitista. Para poder introduzir essa mudança, criamos o método eaSimpleWithElitism() , que
está contido em um arquivo chamado elitism.py.
Este método era uma versão modificada do método eaSimple() do framework deap , que está contido no arquivo
algoritmos.py . Modificamos o método original adicionando a funcionalidade de elitismo, que pega os membros do HOF
– os melhores indivíduos atuais – e os passa intocados para a próxima geração a cada iteração do loop. Agora, com o
objetivo de implementar um callback, introduziremos outra pequena modificação e mudaremos o nome do método para
eaSimpleWithElitismAndCallback(). Também renomearemos o arquivo que o contém para elitism_callback.py.

[ 275 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Há duas partes para esta modificação, como segue:

1. A primeira parte da modificação consiste em adicionar um argumento chamado


retorno de chamada para o método:

def eaSimpleWithElitismAndCallback(população, caixa de ferramentas, cxpb,


mutpb, ngen, callback=Nenhum,
stats=Nenhum, halloffame=Nenhum,
detalhado=__debug__):

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

Reconstrução de Imagem Genética Capítulo 11

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.

Resultados da reconstrução da imagem


Para testar nosso programa, usaremos a seguinte imagem, que faz parte da famosa Mona
Retrato de Lisa de Leonardo da Vinci, considerado a pintura mais conhecida do mundo:

Corte da cabeça da pintura de Mona


Lisa Fonte: https://commons.wikimedia.org/wiki/File:Mona_Lisa_headcrop.jpg. Artista: Leonardo da Vinci.
Licenciado sob Creative Commons CC0 1.0: https://creativecommons.org/publicdomain/zero/1.0/

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

Reconstrução de Imagem Genética Capítulo 11

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.

Usando erro quadrático médio baseado em pixel


Começaremos usando a métrica MSE baseada em pixel para medir a diferença entre a imagem de
referência e a imagem reconstruída. A seguir estão vários marcos das imagens salvas lado a lado
resultantes:

Resultados marcantes da reconstrução da Mona Lisa usando erro quadrático médio baseado em pixels - parte 1

[ 278 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

A imagem a seguir mostra os resultados para as seguintes gerações:

Resultados marcantes da reconstrução da Mona Lisa usando erro quadrático médio baseado em pixels - parte 2

[ 279 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

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:

origImage = cv2.imread('caminho/para/imagem') blurredImage


= cv2.GaussianBlur(origImage, (45, 45), cv2.BORDER_DEFAULT)

Uma versão borrada das últimas imagens lado a lado é a seguinte:

Versões borradas da imagem original e reconstruída com base em pixels, lado a lado

A seguir, tentaremos o outro método de medir a diferença entre a imagem de referência e a


imagem reconstruída – o índice SSIM.

Usando o índice SSIM Agora, vamos


repetir o experimento, mas desta vez usando a métrica SSIM para medir a diferença entre a
imagem de referência e a imagem reconstruída. Para que isso aconteça, modificaremos a definição
de getDiff(), conforme a seguir:

def getDiff(individual):
return imageTest.getDifference(individual, "SSIM"),

[ 280 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Este experimento produziu os seguintes marcos lado a lado das imagens salvas:

Resultados marcantes da reconstrução da Mona Lisa usando o índice SSIM – parte 1

[ 281 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

A imagem a seguir mostra os resultados para as seguintes gerações:

Resultados marcantes da reconstrução da Mona Lisa usando o índice SSIM – parte 2

[ 282 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

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:

Versões borradas da imagem reconstruída original e baseada em SSIM, 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.

Outras experiências Existem


muitas variações que você pode experimentar. Uma variação simples e óbvia é aumentar o
número de vértices que o polígono possui. Com isso, esperamos obter um resultado mais preciso,
pois as formas que estão sendo utilizadas são mais versáteis. Vamos alterar o número de vértices
para seis, da seguinte forma:

POLYGON_SIZE = 6

[ 283 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

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

Além de alterar o número de vértices, existem muitas outras possibilidades e combinações


para experimentar, como as seguintes:

Alterando o número total de formas


Mudando o tamanho da população e o número de gerações
Usando formas não poligonais (círculos, elipse) ou formas regulares (quadrados,
retângulos)

[ 284 ]
Machine Translated by Google

Reconstrução de Imagem Genética Capítulo 11

Usando diferentes tipos de imagens de referência (pinturas, desenhos, fotos, logotipos)


Usando imagens em tons de cinza em vez de coloridas

Divirta-se criando e trabalhando em seus próprios experimentos!

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.

No próximo capítulo, descreveremos e demonstraremos várias técnicas de resolução de problemas


relacionadas a algoritmos genéticos, bem como outros algoritmos computacionais de inspiração
biológica.

Leitura adicional
Para obter mais informações sobre os tópicos abordados neste capítulo, consulte os seguintes recursos:

Crie sua própria imagem: https://chriscummins.cc/s/genetics/


Programação Genética: Evolução da Mona Lisa: https://rogerjohansson.blog/
2008/12/07/genetic-programming-evolution-of-mona-lisa/
Hands-On Image Processing with Python, Sandipan Dey, 30 de novembro de 2018 Wang,
Z., Bovik, AC, Sheikh, HR e Simoncelli, EP (2004), Avaliação da qualidade da imagem: da
visibilidade do erro à similaridade estrutural. IEEE Transactions on Image Processing, 13, 600-612 :
https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf

[ 285 ]
Machine Translated by Google

Outros evolutivos e biológicos


12
Computação inspirada
Técnicas
Neste capítulo, você ampliará seus horizontes e descobrirá várias novas técnicas de solução de problemas e
otimização relacionadas a algoritmos genéticos. Duas técnicas diferentes dessa família estendida – programação
genética e otimização de enxame de partículas – serão então demonstradas pela implementação de programas
Python de solução de problemas. Por fim, forneceremos uma breve visão geral de vários outros paradigmas
de computação relacionados.

Este capítulo abordará os seguintes tópicos:

A família de algoritmos de computação evolutiva


Compreender os conceitos de programação genética e como eles diferem dos algoritmos genéticos

Usando programação genética para resolver o problema de verificação de paridade par


Usando otimização de enxame de partículas para otimizar a função de Himmelblau
Compreender os princípios por trás de várias outras técnicas evolutivas e biologicamente inspiradas

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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.

Confira o vídeo a seguir para ver o Código em ação: http://bit.ly/


2Gx4KsL

Computação evolutiva e computação


bioinspirada
Ao longo deste livro, abordamos a técnica de solução de problemas conhecida como algoritmos
genéticos e a aplicamos a vários tipos de problemas, incluindo otimização combinatória,
satisfação de restrições e otimização de função contínua, bem como aprendizado de máquina e
inteligência artificial. No entanto, como mencionamos no Capítulo 1, Uma Introdução aos Algoritmos
Genéticos, os algoritmos genéticos são apenas um ramo dentro de uma família maior de algoritmos
chamada computação evolutiva. Esta família consiste em várias técnicas relacionadas de solução de
problemas e otimização, todas inspiradas na teoria da evolução natural de Charles Darwin.

As principais características compartilhadas por essas técnicas são as seguintes:

O ponto de partida é um conjunto inicial (população) de soluções candidatas.


As soluções candidatas (indivíduos) são atualizadas iterativamente para criar novas
gerações.
A criação de uma nova geração envolve a remoção de indivíduos menos bem-
sucedidos (seleção), bem como a introdução de pequenas mudanças aleatórias
(mutações) em alguns indivíduos. Outros operadores, como interação com outros indivíduos
(crossover), também podem ser aplicados.
Como resultado, conforme as gerações passam, a aptidão da população aumenta; em outras
palavras, as soluções candidatas crescem melhor na solução do problema em questão.

[ 287 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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:

Representação da estrutura em árvore de um


programa simples Fonte: https://commons.wikimedia.org/wiki/
File:Genetic_Program_Tree.png Imagem de Baxelrod. Liberado para o domínio público

[ 288 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

A estrutura de árvore representada no diagrama anterior representa o cálculo mostrado abaixo da


árvore. Esse cálculo é equivalente a um programa curto (ou uma função) que aceita dois argumentos, X e
Y, e retorna uma determinada saída com base em seus valores. Para criar e evoluir tais estruturas de
árvore, precisamos definir dois conjuntos diferentes:

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.

No Capítulo 2, Compreendendo os principais componentes dos algoritmos genéticos, demonstramos


como o operador genético de cruzamento de ponto único opera em listas de valores binários. A
operação de cruzamento criou dois descendentes de dois pais cortando uma parte de cada pai e trocando
as partes separadas entre os pais. Da mesma forma, um operador de cruzamento para a
representação da árvore pode separar uma subárvore (uma ramificação ou um grupo de ramificações) de
cada pai e trocar os ramos separados entre os pais para criar árvores descendentes, conforme demonstrado
no diagrama a seguir:

Operação de cruzamento entre duas estruturas de árvore que


representam programas /sa/1.0/

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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.

Exemplo de programação genética – verificação de


paridade uniforme
Para nosso exemplo, usaremos programação genética para criar um programa que implemente uma verificação
de paridade par. Nesta tarefa, os possíveis valores das entradas são 0 ou 1. O valor da saída deve ser 1 se o
número de entradas com o valor 1 for ímpar, produzindo assim um número total par de 1 valores; caso
contrário, o valor de saída deve ser 0. A tabela a seguir lista as várias combinações possíveis de valores de
entrada para o caso de três entradas, juntamente com os valores de saída de paridade par correspondente:

in_0 in_1 in_2 paridade


0 0 0 par 0
0 0 1 1
0 1 0 1
0 1 1 0
1 0 0 1
1 0 1 0
1 1 0 0
1 1 1 1

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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.

Implementação de programação genética


Para desenvolver um programa que implementa a lógica de verificação de paridade par, criamos um programa
Python baseado em programação genética chamado 01-gp-even-parity.py, localizado em https://github.com/
PacktPublishing/Hands- Algoritmos-genéticos-com-Python/
blob/master/Chapter12/01-gp-even-parity.py.

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.

As etapas a seguir descrevem as partes principais deste programa:

1. Começamos definindo os valores constantes relacionados ao problema. NUM_INPUTS determina o


número de entradas para o verificador de paridade par. Usaremos o valor de 3 para simplificar; no
entanto, valores maiores também podem ser definidos. A constante NUM_COMBINATIONS
representa o número de combinações possíveis de valores para as entradas, que é análogo ao número
de linhas na tabela verdade que vimos anteriormente:

NUM_INPUTS = 3
NUM_COMBINATIONS = 2 ** NUM_INPUTS

[ 291 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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

3. No entanto, a programação genética requer várias constantes adicionais que se referem à


representação em árvore das soluções candidatas. Eles são definidos no código a seguir.
Veremos como eles são usados enquanto examinamos o restante do programa:

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:

parityIn = list(itertools.product([0, 1], repeat=NUM_INPUTS)) parityOut = [] para linha em parityIn:

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:

primitivoSet = gp.PrimitiveSet("main", NUM_INPUTS, "in_")

[ 292 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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)

7. As definições a seguir definem os valores do terminal a serem usados. Como mencionamos


anteriormente, essas são constantes que podem ser usadas como valores de entrada para
a árvore. No nosso caso, faz sentido usar os valores 0 e 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:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

9. Agora, criaremos a classe Individual , baseada na classe PrimitiveTree


fornecido pela biblioteca deap :

criador.create("Individual", gp.PrimitiveTree, fitness=creator.FitnessMin)

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:

toolbox.register("expr", gp.genFull, pset=primitiveSet,


min_=MIN_TREE_HEIGHT, max_=MAX_TREE_HEIGHT)

[ 293 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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:

toolbox.register("individualCreator", tools.initIterate, criador.Individual, toolbox.expr)


toolbox.register("populationCreator", tools.initRepeat,
list, toolbox.individualCreator)

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("compile", gp.compile, pset=primitiveSet)

def parityError(individual): func =


toolbox.compile(expr=individual) return sum(func(*pIn) !=
pOut para pIn, pOut in
zip(parityIn, parityOut))

13. Em seguida, instruímos o algoritmo de programação genética a usar o método getCost()


função para avaliação de aptidão. Essa função retorna o erro de paridade que acabamos
de ver na forma de tupla exigida pelo algoritmo evolutivo subjacente:

def getCost(individual): return


parityError(individual),

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:

toolbox.register("select", tools.selTournament, tournsize=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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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:

população, diário de bordo = elitism.eaSimpleWithElitism(população, caixa de ferramentas,

cxpb=P_CROSSOVER,
mutpb=P_MUTATION,
ngen=MAX_GENERATIONS,
stats=stats,
halloffame=hof,
verbose=True)

[ 295 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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:

best = hof.items[0] print("-- Best


Individual = ", best) print("-- length={}, height={}".format(len(best),
best.height)) print( "-- Melhor condicionamento físico = ", best.fitness.values[0])

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:

nós, arestas, rótulos = gp.graph(best) g = nx.Graph()


g.add_nodes_from(nodes)
g.add_edges_from(edges) pos =
nx.spring_layout(g)

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_nodes(g, pos, node_color='ciano') nx.draw_networkx_nodes(g, pos,


nodelist=[0], node_color='vermelho', node_size=400)

nx.draw_networkx_edges(g, pos)
nx.draw_networkx_labels(g, pos, **{"labels": labels, "font_size": 8})

Ao executar este programa, obtemos a seguinte saída:

gen nevals min avg 0 60 2


3.91667
1 50 1 3,75
2 47 1 3,45
...
5 47 0 3,15
...
20 48 0 1.68333

-- 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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

-- comprimento=19, altura=4 --
Melhor condicionamento físico = 0,0

Como se trata de um problema simples, o fitness atingiu rapidamente o valor mínimo de 0, o


que significa que conseguimos encontrar uma solução que reproduz corretamente a tabela
verdade de verificação de paridade par. No entanto, a expressão resultante, que consiste em 19
elementos e quatro níveis na hierarquia, parece excessivamente complexa. Isso é ilustrado pela
seguinte trama que foi produzida pelo programa:

Gráfico representando a solução de verificação de paridade que foi encontrada pelo programa inicial

Como mencionamos anteriormente, o nó vermelho no gráfico representa o topo da árvore do


programa, que mapeia para a primeira operação XOR na expressão.

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

Simplificando a solução Na implementação


que acabamos de ver, foram implementadas medidas para restringir o tamanho das árvores que
representam as soluções candidatas. No entanto, a melhor solução que encontramos parece
excessivamente complexa. Uma maneira de pressionar o algoritmo a produzir resultados mais
simples é impor uma pequena penalidade de custo pela complexidade. A penalidade deve ser
pequena o suficiente, no entanto, para não favorecer soluções mais simples que não resolvem o
problema. Ele deve servir como um desempate entre duas boas soluções, portanto, a mais simples das duas será p
Essa abordagem foi implementada no programa Python 02-gp-even-parity Reduced.py, localizado em https://
github.com/PacktPublishing/Hands-On Genetic-Algorithms-with-Python/blob/master/Chapter12 /02-gp-paridade-
mesmo-reduzida.
py.

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:

def getCost(individual): return


parityError(individual) + individual.height / 100,

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:

print("-- Erro de melhor paridade = ", parityError(melhor))

Ao executar esta versão modificada, obtemos a seguinte saída:

gen nevals min avg 0 60


2,03 3,9565
1 50 2,03 3,7885
...
5 47 0,04 3,45233
...
10 48 0,03 3,0145
...
15 49 0,02 2,57983
...
20 45 0,02 2,88533
-- Melhor indivíduo = xor(xor(in_0, in_1), in_2) -- comprimento=5, altura=2
-- Melhor condicionamento físico
= 0,02
-- Melhor erro de paridade = 0

[ 298 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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

Na próxima seção, examinaremos outro algoritmo baseado em população de inspiração biológica.


No entanto, esse algoritmo se desvia do uso dos familiares operadores genéticos de seleção,
cruzamento e mutação e, em vez disso, utiliza um conjunto diferente de regras para modificar a população
a cada geração – bem-vindo ao mundo do comportamento de enxames.

[ 299 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

Otimização de enxame de particulas


A otimização de enxame de partículas (PSO) se inspira em agrupamentos naturais de organismos
individuais, como bandos de pássaros ou cardumes de peixes, geralmente chamados de enxames. Os
organismos interagem dentro do enxame sem supervisão central, trabalhando juntos em direção a um
objetivo comum. Esse comportamento observado deu origem a um método computacional que pode
resolver ou otimizar um determinado problema usando um grupo de soluções candidatas representadas por
partículas análogas a organismos em um enxame. As partículas se movem no espaço de busca, procurando
a melhor solução, e seu movimento é regido por regras simples que envolvem sua posição e velocidade
(velocidade direcional).

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:

A velocidade atual da partícula e a direção do movimento - representando a inércia


A melhor posição da partícula encontrada até agora (melhor local) – representando a força cognitiva
A melhor posição de todo o grupo encontrada até agora (melhor global) – representando a força
social

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

Exemplo de PSO - otimização de função


Para fins de demonstração, usaremos o algoritmo PSO para encontrar a(s) localização(ões)
mínima(s) da função de Himmelblau, uma referência comumente usada que otimizamos
anteriormente usando algoritmos genéticos no Capítulo 6, Otimizando funções contínuas . Esta
função pode ser representada pela seguinte imagem:

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

Como lembrete, a função pode ser expressa matematicamente da seguinte forma:

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

x=ÿ3,779310, y=ÿ3,283186
x=3,584458, y=ÿ1,848126

Para o nosso exemplo, tentaremos encontrar qualquer um desses mínimos.

Implementação de otimização de enxame de partículas


Para localizar um mínimo da função de Himmelblau usando otimização de enxame de partículas, criamos um programa
Python chamado 03-pso-himmelblau.py, localizado em https:// github.com/PacktPublishing/Hands-On-Genetic-
Algorithms- with-Python/blob/master/ Chapter12/03-pso-himmelblau.py.

As etapas a seguir descrevem as partes principais deste programa:

1. Começamos definindo várias constantes que serão usadas ao longo do programa.


A primeira é a dimensionalidade do problema em questão – dois, no nosso caso – que, por sua
vez, determina a dimensionalidade da localização e velocidade de cada partícula.
Em seguida, vem o tamanho da população – o número total de partículas no enxame e o número
de gerações, ou iterações, de execução do algoritmo:

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:

MIN_START_POSITION, MAX_START_POSITION = -5, 5 MIN_SPEED,


MAX_SPEED = -3, 3
MAX_LOCAL_UPDATE_FACTOR = MAX_GLOBAL_UPDATE_FACTOR = 2,0

3. Como nosso objetivo é localizar um mínimo na função de Himmelblau, precisamos


definir um único objetivo, minimizando a estratégia de aptidão:

criador.create("FitnessMin", base.Fitness, pesos=(-1.0,))

[ 302 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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).

A definição resultante para o criador da classe Particle é a seguinte:

criador.create("Particle", np.ndarray, fitness=creator.FitnessMin,


speed=None, best=None)

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

6. Esta função é utilizada na definição do operador que cria uma instância de


partícula. Isso, por sua vez, é usado pelo operador de criação de população:

toolbox.register("particleCreator", createParticle) toolbox.register("populationCreator",


tools.initRepeat, list, toolbox.particleCreator)

7. Em seguida vem o método que serve como o coração do algoritmo –


updateParticle(). Este método é responsável por atualizar a localização e a velocidade de cada
partícula da população. Os argumentos desta função são uma única partícula na população
e a melhor posição atualmente registrada.

[ 303 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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.

A velocidade atualizada da partícula é efetivamente uma combinação da velocidade original da partícula


(representando a inércia), a localização mais conhecida da partícula (força cognitiva) e a localização
mais conhecida de toda a população (força social):

def updateParticle(partícula, melhor):

localUpdateFactor = np.random.uniform(0,
MAX_LOCAL_UPDATE_FACTOR,
partícula.tamanho)
globalUpdateFactor = np.random.uniform(0,
MAX_GLOBAL_UPDATE_FACTOR,
partícula.tamanho)

localSpeedUpdate = localUpdateFactor * (particle.best - partícula) globalSpeedUpdate


=
globalUpdateFactor * (melhor - partícula)

partícula.velocidade = partícula.velocidade + (localSpeedUpdate + lobalSpeedUpdate)

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:

partícula.velocidade = np.clip(partícula.velocidade, MIN_SPEED, MAX_SPEED) partícula[:] =


partícula + partícula.velocidade

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

10. Ainda precisamos definir a função a ser otimizada – a função de Himmelblau, no


nosso caso – e registrá-la como o operador de avaliação de fitness:

def himmelblau ( partícula ) : x = partícula


[ 0 ] y = partícula [ 1 ] f =
( x ** 2 + y - 11 ) ** 2 +
( x + y retorna f , # retorna uma tupla ** 2 - 7) ** 2

toolbox.register("avaliar", céu azul)

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)

logbook = tools.Logbook() logbook.header


= ["gen", "avaliações"] + estatísticas.campos

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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)

15. Ao final do loop externo, registramos as estatísticas da geração atual e as imprimimos:

logbook.record(gen=geração, evals=len(população), **stats.compile(população))


print(logbook.stream)

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:

# imprimir informações para a melhor solução encontrada:


print("-- Best Particle = ", best) print("-- Best Fitness = ",
best.fitness.values[0])

Ao executar este programa, obtemos a seguinte saída:

gen evals min avg 0 20


8.74399 167.468
1 20 19.0871 357.577
2 20 32.4961 219.132
...
497 20 7.2162 412.189
498 20 6.87945 273.712
499 20 16.1034 272.385

-- Melhor partícula = [-3,77695478 -3,28649153]


-- Melhor condicionamento físico = 0,0010248367255068806

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

Na próxima seção, revisaremos brevemente vários outros membros da família de


computação evolutiva estendida.

Outras técnicas relacionadas


Além das técnicas que abordamos até agora, existem inúmeras outras técnicas de solução de
problemas e otimização que se inspiram na teoria da evolução darwiniana, bem como em vários
sistemas e comportamentos biológicos. As subseções a seguir descrevem brevemente várias outras
dessas técnicas.

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:

A população DE é sempre representada como uma coleção de vetores de valores reais.


Em vez de substituir toda a geração atual por uma nova geração, o DE continua
iterando sobre a população, modificando um indivíduo por vez ou mantendo o
indivíduo original se for melhor que sua versão modificada.
Os tradicionais operadores de crossover e mutação são substituídos por operadores
especializados, modificando assim o valor do indivíduo atual usando os valores de
outros três indivíduos escolhidos aleatoriamente.

[ 307 ]
Machine Translated by Google

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

Otimização de colônia de formigas


Os algoritmos de otimização de colônia de formigas (ACO) são inspirados na maneira como certas espécies
de formigas localizam alimentos. As formigas começam vagando aleatoriamente, e quando alguma delas
localiza comida, elas voltam para sua colônia enquanto depositam feromônios pelo caminho, marcando o
caminho para outras formigas. Outras formigas que encontrarem comida no mesmo local irão reforçar
a trilha depositando seus próprios feromônios. As marcas de feromônio desaparecem com o tempo, dando
vantagem aos caminhos mais curtos e aos caminhos que são percorridos com mais frequência.

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.

Sistemas imunológicos artificiais


Os sistemas imunológicos artificiais (AIS) inspiram-se nas características dos sistemas imunológicos
adaptativos encontrados em mamíferos. Esses sistemas são capazes de identificar e aprender novas ameaças,
bem como aplicar o conhecimento adquirido e responder mais rapidamente na próxima vez que uma
ameaça semelhante for detectada.

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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.

Os principais ramos da ALife são os seguintes:

Suave: Representa simulação baseada em software (digital)


Difícil: Representa a robótica (física) baseada em hardware
Molhado: representa manipulação de base bioquímica ou biologia sintética

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

Outras técnicas de computação evolutivas e bioinspiradas Capítulo 12

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:

Programação genética: aprendizado de máquina bioinspirado http://


geneticprogramming.com/tutorial/
Machine Learning for Finance, Jannes Klaas, 30 de maio de
2019 Inteligência Artificial para Big Data, Manish Kumar e Anand Deshpande, 21 de maio
de 2018
Otimização multimodal usando algoritmos de otimização de enxame de partículas:
competição CEC 2015 em otimização multi-nicho de objetivo único: https://ieeexplore.
ieee.org/document/7257009

[ 310 ]
Machine Translated by Google

Outros livros que você pode gostar


Se você gostou deste livro, pode estar interessado nestes outros livros de Packt:

Neuroevolução prática com Python


Iaroslav Omelianenko

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

Outros livros que você pode gostar

Aprendizado Profundo Avançado com Python


Ivan Vasilev

ISBN: 9-781-78995-617-7
Abrange arquiteturas de rede neural avançadas e de última geração

Entenda a teoria e a matemática por trás das redes neurais


Treine DNNs e aplique-os a problemas modernos de aprendizado profundo
Use CNNs para detecção de objetos e segmentação de imagens
Implemente redes adversárias generativas (GANs) e autoencoders variacionais
para gerar novas imagens
Resolva tarefas de processamento de linguagem natural (NLP), como tradução automática,
usando modelos de sequência a sequência
Compreender as técnicas de DL, como meta-aprendizagem e redes neurais gráficas

[ 312 ]
Machine Translated by Google

Outros livros que você pode gostar

Deixe um comentário - deixe outros leitores saberem


o que você pensa
Por favor, compartilhe seus pensamentos sobre este livro com outras pessoas, deixando um
comentário no site onde você o comprou. Se você comprou o livro na Amazon, deixe-nos uma crítica
honesta na página deste livro na Amazon. Isso é vital para que outros leitores em potencial
possam ver e usar sua opinião imparcial para tomar decisões de compra, podemos entender
o que nossos clientes pensam sobre nossos produtos e nossos autores podem ver seus comentários
sobre o título que eles trabalharam com a Packt para criar. Levará apenas alguns minutos do seu
tempo, mas é valioso para outros clientes em potencial, nossos autores e a Packt. Obrigado!

[ 313 ]
Machine Translated by Google

Índice

hall da fama, adicionando 77, 79


A logbook 75
AdaboostClassifier
programa, executando 76, 77
link de referência 212
configurações, experimentando com 79
algoritmo de reforço adaptável (AdaBoost) 211 agente Objeto de estatísticas 74
242 otimização
de colônia de formigas (ACO) 308 C
combinação de otimização de
link de referência
arquitetura, com ajuste de hiperparâmetros 235 sistemas do arquivo cart_pole.py
imunológicos artificiais (AIS) cerca de 256 Controle do ambiente
308
CartPole, com rede neural 254, 255 solução de
seleção clonal 308
algoritmo genético 257, 258, 259, 260 camada oculta 255
algoritmos de rede imune 309 seleção
camada de entrada
negativa 308 vida artificial
255 camada de
(ALife) cerca de 309
saída 255 Problema
difícil 309
Python, representando 256 avaliação da
solução 255 representação da
suave 309
solução 255, 256 solução 252, 253 Link
molhado
de referência do
309 redes neurais artificiais (ANN) 224, 225 ambiente CartPole-v1 254
codificação de
B cromossomos 50 cromossomos
backpropagation 226 fluxo cerca de 16 para
básico, de algoritmo genético números
cerca de 25 reais 159 classificação 189,
cruzamento, aplicando 26 190 classificação Conjunto
fitness, calculando 26 de dados Zoo, seleção de recursos cerca de 199, 200
população inicial, criando 26 solução de algoritmos
mutações, aplicando 26 genéticos 203, 205 Representação de
seleções, aplicando 26 problemas em Python 201, 202, 203 otimização combinatória
condições de parada, verificando 27 95 componentes, camadas ocultas
computação bioinspirada 287 Multilayer Perceptron (MLP) 226 camada de entrada
Blend Crossover (BLX) 42, 43, 160 medidas 226 camada de saída
de controle de inchaço 295
226 otimização
hipótese do bloco de construção 13 restrita cerca de 178,
algoritmos evolutivos embutidos sobre 179
74, 75
Machine Translated by Google

usando, para encontrar várias soluções 182, 183 com implementando 65


algoritmos genéticos 180 satisfação programa, executando 73, 74 link
de restrição de referência 56
em problemas de busca 127 configurando 65, 68
funções contínuas solução, evoluindo 69, 71, 72 usado,
Estrutura DEAP, usando 161 para resolver o problema OneMax (One-Max) 63 índice de
redes neurais convolucionais com distribuição 46, 160
módulo criador 227 de
aprendizagem profunda E
usado, para criar a classe Fitness 57 usado, Função porta-ovos
para definir a classe Individual 59 usando 56, 57 otimizando 162, 163
métodos de otimizando, com algoritmos genéticos 163, 165,
cruzamento 166
cerca de 34 velocidade, melhorando com taxa de mutação 167, 168
para listas ordenadas 37 quebra-cabeça de oito rainhas
cruzamento de k pontos 35, 36 128 mecanismo de elitismo 47, 113
cruzamento de ponto único 35 interface env
cruzamento de dois pontos 35 cerca de 243, 244
cruzamento uniforme 36, 37 link de referência 245
operação de cruzamento 27 estratégias de evolução (ES) 307
fator de aglomeração 160 computação evolutiva 287 exploração
111 exploração 111
D princípio de
evolução darwiniana exploração versus exploração 167
cerca de 9, 10
princípios 9 F
estrutura DEAP características
usado, com funções contínuas 161 189 seleção de características, problema de regressão Friedman-1
Link de referência do classificador sobre 193
de árvore de decisão 209 algoritmos genéticos solução 197, 199
classificador de aprendizado profundo, precisão Python representação do problema 194, 195, 197
do classificador de arquitetura, avaliando a representação da solução 194
configuração de 230 camadas ocultas, representando 229, Aula de ginástica
230
criando 57
Conjunto de dados de flor de íris 228
estratégias de aptidão, definindo 57
otimizando 228 valores de aptidão, armazenando
aprendizado 58 funções de
profundo sobre 224, aptidão usando 11, 50
225 com redes neurais convolucionais 227
seleção proporcional de aptidão (FPS) 28 escala de
evolução diferencial (DE) 307 abordagem aptidão 32, 33 mutação flip
genética direta usada, para bit 40 teorema
ajustar hiperparâmetros 216 fundamental de algoritmos genéticos 14
Algoritmos Evolucionários Distribuídos em Python
(DEAP)
cerca de 55

[ 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

pesquisa de grade convencional, executando 215


eu
pesquisa de grade orientada por algoritmo genético, executando
dados rotulados 241
215, 216
hiperparâmetros sobre Licença Pública Geral Menor (LGPL) 56
208
algoritmo de reforço adaptável (AdaBoost) 211 ajuste 210 M
usado, no aprendizado de máquina
aprendizado de máquina 208, 209 hiperparâmetros, usando 208, 209
Conjunto de dados de vinho 210 erro quadrático médio (MSE) 196
Otimização da
arquitetura MLP, com algoritmos genéticos 232, 233,
235
Bibliotecas de processamento de imagens, Python
cerca de 265 Otimização de configuração
combinada MLP, com algoritmos genéticos 237, 239
biblioteca opencv-python 266
link de referência do
Biblioteca Pillow 265
arquivo mountain_car.py 248
biblioteca scikit-image 265
Solução de algoritmos genéticos
processamento de imagem,
Python cerca do ambiente MountainCar 249, 250, 252
Problema Python, representando 248 solução
de 265 diferenças de imagem, medindo 268
representação 247 solução, avaliando
imagens, desenhando com polígonos 267, 268
bibliotecas 265 248 resolvendo 245, 246, 247

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

reconstrução de imagens, com polígonos 264


métodos de mutação
mutação de inversão 40
cerca de 39
Conjunto de dados de flor de íris

cerca de 228 mutação de flip bit 40


mutação de inversão 40
link de referência 228
mutação embaralhada 41

k mutação de troca 40

cruzamento de ponto k 35, 36


componentes do
N
Solução de algoritmos
problema da mochila
genéticos do problema N-Queens 132, 133, 134, 135,
96 solução de algoritmos genéticos 100, 101, 102
136
problema Python, representando 98 Rosetta
Code 97 representação Python representação do problema 131, 132
representação da solução 129, 130, 131 resolução
da solução 98 resolvendo 96
128 networkx
arquivo
(nx) 296 rede neural
knapsack.py
link de referência 98
Ambiente CartPole, controlando 254, 255 niching 47, 48,
49 mutação normalmente
distribuída (ou gaussiana) 46,

[ 317 ]
Machine Translated by Google

160 implementando 302, 303, 304, 305, 306 fenótipo 98


Classe NQueensProblem, métodos
getViolationsCount(posições) 131 Link de referência
plotBoard(posições) 131 problema da biblioteca de
de agendamento de enfermagem (NSP) travesseiros 265 erro quadrático médio (MSE)
cerca de 137 baseado em
solução de algoritmos genéticos 142, 143, 145, 146 restrições pixels cerca de 269
rígidas, versus restrições flexíveis 138, usando
139
278, 280 polígonos usados, para reconstruir imagens 264
Python representação do problema 140, 141, 142 convergência prematura 18, 88 técnicas
representação da solução 137, 138 resolução de solução de problemas e otimização
137 cerca de 307
otimização de colônia de formigas (ACO) 308
O sistemas imunológicos artificiais (AIS) 308 vida
observação 242 artificial (ALife) 309 evolução
Problema OneMax (One-Max) sobre diferencial (DE) 307 estratégias de
63 evolução (ES) 307 métodos de classe
link de referência 63 Python 98 Python
resolvendo, com DEAP 63 Imaging Library
Problema OneMax, resolvendo com o (PIL) 265 programa Python, 02-
cromossomo DEAP, selecionando hyperparameter- link de referência 219 do tuning genetic.py
63 fitness, calculando 64
operadores genéticos, selecionando 64
condições de parada, configurando 64
Ginásio OpenAI Processamento de imagem Python 265
cerca de 243
interface ambiente 244 R
URL 243 pesquisa aleatória 210
Link de referência da seleção baseada em classificação 30,
documentação OpenCV 31 mutação real 46
266 link de referência da algoritmos genéticos codificados reais
biblioteca opencv-python 266
sobre 41, 42
soluções ótimas, para TSPs simétricos Blend Crossover (BLX) 42, 43 mutação
link de referência 104 real 46
cruzamento ordenado (OX1) método 37, 38, 39, 61 Crossover Binário Simulado (SBX) 44, 45, 46
regressão 190, 191
P
aprendizagem por reforço 241, 242, 243
Niching Paralelo Código Roseta
Cerca de 182 URL 97
Versos Niching Serial seleção da roda da roleta 28, 29, 91, 92
ajuste de parâmetros 209
crossover parcialmente combinado (PMX) 61 S
otimização de enxame de partículas (PSO) esquema teorema 14, 15
cerca de 300
biblioteca scikit-image
otimização de função 301 link de referência 266

[ 318 ]
Machine Translated by Google

mutação embaralhada 41 mutação de troca 40


problemas de busca 95
métodos de seleção T
cerca de 28, 50 função alvo 11
escala de aptidão 32, 33 Aptidão da classe
seleção baseada em classificação 30, caixa de ferramentas,
31 seleção de roleta 28, 29 calculando 62 operadores genéticos,
Amostragem Estocástica Universal (SUS) 29, 30 seleção de criando 60 populações, criando
torneio 34
61 usando
operador de seleção 59 seleções de torneios 34
sobre 26, 86 algoritmos tradicionais, versus algoritmos genéticos cerca de 15
seleção de roleta 91, 92 relação de torneio,
para probabilidade de mutação 87, função de condicionamento físico 16

89, 91 representação genética 16 baseada


tamanho do torneio 87, 89, 91 niching na população 16
serial versus comportamento probabilístico 17
niching paralelo 49 configurações, problema do caixeiro viajante (TSP), métodos públicos de classe
operador de cruzamento de algoritmo evolucionário
integrado 82, 83 operador de getTotalDistance(índices) 106
mutação 84, 85 número de plotData(índices) 106 problema
gerações 80, 82 tamanho da população do caixeiro viajante (TSP)
80, 82 operador de seleção 86 cerca de 132
tamanho do torneio 89 soluções de algoritmos genéticos 107, 108, 110
Problema Python, representando 105, 106 resultados,
mecanismo de compartilhamento 47, 48, 49 melhorando com elitismo 111, 112, 113,
Função de Simionescu sobre 115
178, 179 otimizando, resultados, melhorando com exploração aprimorada 111, 112,
com algoritmos genéticos 181, 182 113, 115 solução,
Crossover Binário Simulado (SBX) 44, 45, 46, representando 105 resolvendo 102,
160
104
crossover de ponto único 35 fator Arquivos de referência TSPLIB 104
de propagação 160 estruturas de árvore
Amostragem Estocástica Universal (SUS) 29, 30, 61 primitivos 289
Similaridade Estrutural (SSIM) 269 algoritmos terminais 289
de aprendizado supervisionado cerca de crossover de dois pontos 35
191
Redes Neurais Artificiais 192 EM
Árvores de Decisão 191
Repositório de aprendizado de máquina UCI
Florestas Aleatórias 191
link de referência 210
Máquinas de Vetores de Suporte 192
cruzamento uniforme 36, 37
aprendizado de máquina supervisionado
sobre 187, 188
V
classificação 189, 190 seleção
de recursos 192 regressão problema de roteamento de veículos (VRP), métodos
públicos de
190, 191
classe getMaxDistance(índices) 118

[ 319 ]
Machine Translated by Google

getRouteDistance(índices) 118 solução, representando 117


getRoutes(índices) 118 resolvendo 115
getTotalDistance(índices) 118
plotData(índices) 118 EM
problema de roteamento de Link de referência do
veículos (VRP) conjunto de dados do vinho 210
sobre 116, 132
x
componentes 115 algoritmos genéticos, solução 120, 122, 123, 125
Problema Python, representando 118, 119
XOR (OU Exclusivo) 290

Você também pode gostar