Genéticos: Problema da Mochila em Python

De Aulas

Afluentes: Inteligência Artificial, Modelos Evolutivos e Tratamento de Incertezas

Definição do Problema

Para mostrar uma implementação de um Algoritmo Genético, iremos trabalhar com o Problema da Mochila.

  • Problema de otimização combinatória (NP-Completo)
  • Estudado por mais de um século (desde ~1897)
  • Resolvido por vários algoritmos
Ag mochila.png

Podemos representar a aptidão de uma solução como a soma dos valores dos objetos e com uma penalidade para soluções que passam do limite de volume.

  • Queremos o maior valor possível sem ultrapassar o limite da mochila.
  • Vários itens que gostaria de levar em uma mochila
  • Cada item tem um peso e um benefício, ou valor
  • Há uma capacidade limite de peso
  • Deve-se carregar itens com o máximo valor total sem superar o limite de peso

Programa em Python

Tplnote Bulbgraph.png

Eu estava fazendo procedural e estava tendo que fazer uns remendos, como encher de variáveis globais e outras coisas mais, e acho que isso estava permitindo um bug difícil de encontrar. Então decidi ir pra orientação a objetos que pra mim é mais fácil de entender e organizar. A ideia do algoritmo é a mesma que expliquei, mesmas funções, mesmaa funcionalidade, só está organizado como objetos. E, no final, era um bug bobo mesmo por causa de variáveis globais.

Arquivo de Configuração config.json

 1{
 2  "capacity": "25",
 3  "items": [
 4    { "name": "notebook", "value": "5", "weight": "7" },
 5    { "name": "tv", "value": "8", "weight": "8" },
 6    { "name": "tablet", "value": "3", "weight": "4" },
 7    { "name": "anel", "value": "2", "weight": "10" },
 8    { "name": "colar", "value": "7", "weight": "4" },
 9    { "name": "videogame", "value": "9", "weight": "6" },
10    { "name": "smarthone", "value": "4", "weight": "4" }
11  ]
12}

Programa mochila.py

  1import json
  2from random import *
  3
  4
  5# ITEM CLASS ---------------------------------------------
  6# representa os itens do arquivo config.json
  7class Item:
  8    def __init__(self, name, value, weight):
  9        self.name = name        # nome do produto
 10        self.value = value      # valor
 11        self.weight = weight    # peso
 12
 13
 14# DATA STATIC CLASS ------------------------------------
 15# Lê as configurações do arquivo config.json e carrega
 16# nessa classe estática para ser usada em todo o programa
 17class Data:
 18    size = 0        # quantidade de itens
 19    capacity = 0    # capacidade da mochila
 20    items = []      # vetor contendo os itens
 21
 22    @staticmethod
 23    def load():
 24        # abre o arquivo e converte para um objeto json
 25        file = open("config.json")
 26        data = json.load(file)
 27        file.close()
 28        # coloca as informações do objeto json na classe
 29        # Data pra facilitar o acesso aos dados
 30        Data.capacity = int(data["capacity"])
 31        for i in range(len(data["items"])):
 32            name = data["items"][i]["name"]
 33            value = int(data["items"][i]["value"])
 34            weight = int(data["items"][i]["weight"])
 35            Data.items.append(Item(name, value, weight))
 36            Data.size += 1
 37
 38
 39# INDVIDUAL CLASS -------------------------------------------
 40# Cada instância dessa classe representa cada indivíduo da
 41# população do ecossistema do algoritmo
 42class Individual:
 43    def __init__(self):
 44        self.fitness = 0                # aptidão do indivíduo
 45        self.accumulated_fitness = 0    # aptidão acumulada (usada na seleção)
 46        self.selected = False           # se indivíduo foi selecionado
 47        self.gene = []                  # gens, representando os ítens na mochiila
 48        for i in range(Data.size):
 49            # coloca ítens aleatórios na mochila
 50            self.gene.append(randint(0, 1))
 51
 52    # Calcula a aptidão do indivíduo
 53    def evaluate(self):
 54        self.accumulated_fitness = 0
 55        self.fitness = 0
 56        weight = 0
 57        # primeiro soma todos os valores dos ítens que estão inclusos
 58        # nessa solução de mochila
 59        for i in range(Data.size):
 60            if self.gene[i] == 1:
 61                self.fitness += Data.items[i].value
 62                weight += Data.items[i].weight
 63        # caso o peso total de ítens incluso seja maior que a capacidade
 64        # da mochila, dá uma penalidade, diminuindo em 100 a aptidão
 65        if weight > Data.capacity:
 66            self.fitness -= 100
 67
 68    # apresenta informações do indivíduo de forma simplificada para debug
 69    def show(self):
 70        print(self.gene, self.fitness, self.selected, self.accumulated_fitness)
 71
 72    # apresenta informações completas da solução, representada por um indivíduo
 73    def show_info(self):
 74        print("SOLUÇÃO ------------------------------")
 75        print("Capacidade da mochila:", Data.capacity)
 76        weight = 0
 77        value = 0
 78        for i in range(Data.size):
 79            if self.gene[i] == 1:
 80                weight += Data.items[i].weight
 81                value += Data.items[i].value
 82                print("-", Data.items[i].name, "valor:",
 83                      Data.items[i].value, "gold - peso:", Data.items[i].weight)
 84        print("Valor total:", value, "gold")
 85        print("Peso total :", weight)
 86        print("--------------------------------------")
 87
 88    # clona o indivíduo e retorna uma instância dessa cópia
 89    def clone(self):
 90        other = Individual()
 91        for i in range(Data.size):
 92            other.gene[i] = self.gene[i]
 93        other.fitness = self.fitness
 94        return other
 95
 96
 97# GENETIC CLASS --------------------------------------------------
 98class Genetic:
 99    # Configuro um padrão aqui, mas faço ajustes no main
100    population_size = 50            # tamanho da população
101    selection_size = 20             # quantos vou selecionar
102    crossover_gene_size = 2         # quantos gens vou usar no cruzamento
103    mutation_individual_size = 10   # quantos indivíduos vou mutar cada geração
104    mutation_gene_size = 2          # quantos gens de cada indivíduo vou mutar
105    generations = 100               # por quantas gerações o algoritmo vai executar
106
107    # constructor
108    def __init__(self):
109        Data.load()             # carrega as configurações
110        self.population = []    # inicia com população vazia
111
112    # mostra todos os indivíduos da população
113    def show(self):
114        for i in self.population:
115            i.show()
116
117    # cria a população inicial com indivíduos contendo ítens aleatórios
118    def create_population(self):
119        print("CREIANDO POPULAÇÃO INICIAL...")
120        for i in range(Genetic.population_size):
121            self.population.append(Individual())
122
123    # calcula a aptidão de todos os indivíduos da população
124    def evaluate(self):
125        for i in self.population:
126            i.evaluate()
127        # ordena a população com base na aptidão
128        self.population.sort(key=lambda x: x.fitness)
129
130    # seleciona uma quantidade de indivíduos. Se best for verdadeiro,
131    # então seleciona os melhores, se for falso, seleciona os piores.
132    def selection(self, best):
133        who = "MELHORES"
134        if not best:
135            who = "PIORES"
136        print("SELECIONANDO OS", who, "INDIVÍDUOS ...")
137        # como para fins de cálculo não pode haver aptidão negativa,
138        # procura a menor aptidão negativa para subtrair de todos,
139        # deixando todos positivos.
140        lower = 0
141        for i in self.population:
142            if i.fitness < lower:
143                lower = i.fitness
144
145        # Se houve aptidão negativa, subtrai o menor valor de
146        # todos os elementos para não termos aptidão negativa
147        if lower < 0:
148            for i in self.population:
149                i.fitness = i.fitness - lower
150
151        # ordena a população com base na aptidão
152        self.population.sort(key=lambda x: x.fitness)
153
154        # calcula a aptidão acumulada e aptidão total
155        fitness_total = 0
156        for i in self.population:
157            fitness_total += i.fitness
158            i.accumulated_fitness = fitness_total
159
160        # se é pra selecionar os piores, inverte o aptidão
161        # acumulada e ordena novamente pela aptidão acumulada
162        # assim, os piores tem mais chances de serem escolhidos
163        if not best:
164            # Inverte as probabilidades. Os piores vão ganhar uma
165            # porção maior na roleta e os melhores menor
166            size = len(self.population)
167            for i in range(int(size / 2)):
168                a = self.population[i]
169                b = self.population[size - 1 - i]
170                aux = a.accumulated_fitness
171                a.accumulated_fitness = b.accumulated_fitness
172                b.accumulated_fitness = aux
173            self.population.sort(key=lambda x: x.accumulated_fitness)
174
175        # e roda a roleta para selecionar a quantidade de indivúdios
176        # definida em Genetic.selection_size
177        for j in range(Genetic.selection_size):
178            # pega um número aleatório com base na aptidão total
179            num = randint(0, fitness_total)
180            last = 0
181            i = 0
182            found = False
183            # enquanto não encontrou um indivíduo que tem a ficha
184            # desse número, procura
185            while not found:
186                current = self.population[i].accumulated_fitness
187                # se encontrou o felizardo (ou não)
188                if last <= num <= current:
189                    # se já está selecionado, pega o próximo não
190                    # selecionado da roleta
191                    while self.population[i].selected:
192                        # se chegou ao final, volta para o início
193                        # veja que está girando a roleta e é circular
194                        i += 1
195                        if i >= len(self.population):
196                            i = 0
197                    # achou um indivíduo seleciona ele
198                    self.population[i].selected = True
199                    found = True
200                last = current
201                i += 1
202
203    # faz o cruzamento dos indivíduos selecioados
204    def crossover(self):
205        print("CRUZAMENTO...")
206        # cria uma lista para os selecionados. Fica mais fácil.
207        selected = []
208        for i in self.population:
209            if i.selected:
210                selected.append(i)
211                # já deseleciona os pais
212                i.selected = False
213        # randomiza para fazer pares aleatórios
214        shuffle(selected)
215        i = 0
216        # vai seguindo a lista dos selecionados pegando os dois
217        # primeiros, depois os dois seguintes e assim por diante
218        while i < len(selected):
219            child_a = selected[i].clone()
220            child_b = selected[i+1].clone()
221            for j in range(Genetic.crossover_gene_size):
222                # escolhe um gen aleatório e troca nos filhos
223                k = randint(0, Data.size - 1)
224                child_a.gene[k] = selected[i+1].gene[k]
225                child_b.gene[k] = selected[i].gene[k]
226            # coloca os filhos na lista da população
227            self.population.append(child_a)
228            self.population.append(child_b)
229            i += 2
230
231    # Faz a mutação de alguns indivíduos escolhidos aleatoriamente.
232    def mutation(self):
233        print("MUTAÇÃO...")
234        # pega uma quantidade aleatória de indivíduos para mutar
235        # a cada geração, entre 0 e Genetic.mutation_individual_size
236        size = randint(0, Genetic.mutation_individual_size)
237        for i in range(size):
238            # pega um indivíduo aleatório da população
239            k = randint(0, len(self.population) - 1)
240            # pega alguns gens aleatórios e inverte o valor
241            for j in range(Genetic.mutation_gene_size):
242                l = randint(0, Data.size - 1)
243                if self.population[k].gene[l] == 1:
244                    self.population[k].gene[l] = 0
245                else:
246                    self.population[k].gene[l] = 1
247
248    # Depois de selecionar os piores indivíduos, mas com
249    # possibilidades de alguns bons serem selecionados na
250    # roleta, mata esses selecionados, deixando apenas os
251    # "melhores" para a próxima geração
252    def survival(self):
253        print("SOBREVIVENTES...")
254        aux = []
255        for i in self.population:
256            if not i.selected:
257                aux.append(i)
258        self.population = aux
259
260    # Algoritmo genético propriamente dito.
261    def run(self):
262        self.create_population()
263        for g in range(Genetic.generations):
264            print("GERAÇÃO", g)
265            self.evaluate()
266            self.selection(True)
267            self.show()
268            self.crossover()
269            self.mutation()
270            self.evaluate()
271            self.selection(False)
272            self.show()
273            self.survival()
274        self.evaluate()
275        self.show()
276        self.population[-1].show_info()
277
278
279# MAIN PROGRAM -----------------------------------------
280def main():
281    # Faço ajustes para ser mais fácil (atributos globais)
282    Genetic.population_size = 100
283    Genetic.selection_size = 40
284    Genetic.crossover_gene_size = 2
285    Genetic.mutation_individual_size = 20
286    Genetic.mutation_gene_size = 2
287    Genetic.generations = 500
288
289    # inicio o algoritmo
290    genetic = Genetic()
291    genetic.run()
292
293
294# -------------------------------------------------------
295if __name__ == "__main__":
296    main()

Solução

A solução encontrada para o algoritmo com os parâmetros e configuração acima fica:

SOLUÇÃO ------------------------------
Capacidade da mochila: 25
- notebook valor: 5 gold - peso: 7
- tv valor: 8 gold - peso: 8
- colar valor: 7 gold - peso: 4
- videogame valor: 9 gold - peso: 6
Valor total: 29 gold
Peso total : 25
--------------------------------------

Alterações para novo teste

Adicionei um item (impressora) no arquivo de configuração e aumentei a capacidade para 25

 1{
 2  "capacity": "25",
 3  "items": [
 4    { "name": "notebook", "value": "5", "weight": "7" },
 5    { "name": "tv", "value": "8", "weight": "8" },
 6    { "name": "tablet", "value": "3", "weight": "4" },
 7    { "name": "anel", "value": "2", "weight": "10" },
 8    { "name": "colar", "value": "7", "weight": "4" },
 9    { "name": "videogame", "value": "9", "weight": "6" },
10    { "name": "smarthone", "value": "4", "weight": "4" },
11    { "name": "impressora", "value": "2", "weight": "5" }
12  ]
13}