Escolhendo o equipamento para um jogo persa usando genética / evolução em Python

Como escolher o melhor equipamento no seu jogo favorito? Claro, você pode classificar banalmente todas as combinações possíveis (por exemplo, para o ladrão de World of Warcraft) e encontrar o melhor. Sem nenhuma mágica ou aprendizado de máquina. Mas é possível alcançar esse resultado não "de frente", mas com a ajuda de algoritmos genéticos, sem experimentar cada combinação? É interessante saber como os ladrões se reproduzem e evoluem? Vai.

imagem

Este artigo é um aprofundamento do tópico anterior , que já descreve os detalhes da implementação do personagem e o impacto do equipamento.

Objetivo: mostrar aos interessados ​​como criar uma população virtual de agentes de jogo por conta própria, reuni-los, implementar um algoritmo genético simples (com mutações) e visualizar tudo isso usando HTML, CSS e Javascript.

Do que você precisa:

1. Python 3 + instala o módulo matplotlib ; IDE (eu tenho PyCharm);
2. Se você deseja editar um modelo de relatório interativo, obtenha um entendimento básico de HTML, CSS, JavaScript (jQuery).

PASSO 1 - estabeleça uma meta, lide com OOP


Portanto, o objetivo final de nossa codificação é tornar possível obter o melhor equipamento com uma simulação de genética / evolução / seleção natural. E tenha a oportunidade de rastrear visualmente esse caminho.

Transformamos nosso objetivo em tarefas específicas:

  1. refletir e implementar o mecanismo genético no nível individual (os “genes” do ladrão devem afetar diretamente a escolha do equipamento)
  2. implementar os mecanismos de nascimento dos descendentes, transferir genes para eles, bem como mutações menores aleatórias, para garantir variabilidade e a possibilidade de encontrar o melhor
  3. realizar a “competição” genética, a seleção de genes (para unir seus portadores, de modo que o resultado da colisão derrube os genes “ruins” e eleve os “bons”)
  4. monte tudo em um sistema coerente no qual os ladrões possam evoluir para lutadores ideais
  5. coletar e visualizar dados para que haja algo para admirar (caso contrário, é interessante!)
  6. avaliar a utilidade dessas atividades

Aqui o OOP nos ajudará, criaremos 4 classes fundamentais:

imagem

PASSO 2 - aprimorando a classe Rogue para criação


concordar com os termos na praia


Neste artigo, partirei com os termos "gene" (no código - gene ) e "genótipo" (no código - genótipo ). Também haverá "mutações" (no código - mutação , mutação ), implicando uma alteração em um ou mais genes de seu significado. A reprodução ocorrerá simplesmente “brotando” o descendente dos pais, para que não haja cruzamento e outras complicações semelhantes. O genótipo do ladrão é uma lista de sete números que são seus genes:

imagem

então, a classe Rogue está crescendo significativamente em comparação com a última vez .

1O ladrão obtém os genes de uma das maneiras possíveis (foi gerado no primeiro dia ou herdado do "pai" com ou sem uma mutação). Os métodos generate_random_genes , mutate_genes , mutate_gene são responsáveis ​​por isso :

codificar métodos de formação de genes
    #     ():
    def generate_random_genes(self):
        dbg = DBG_rogue_generate_random_genes

        self.my_genes[0] = randrange(0, len(RIGHT_HANDS))    # <--   :
        self.my_genes[1] = randrange(0, len(LEFT_HANDS))     # <--   :
        self.my_genes[2] = randrange(0, len(GLOVES))         # <--  :
        self.my_genes[3] = randrange(0, len(HEADS))          # <--  :
        self.my_genes[4] = randrange(0, len(CHESTS))         # <--  :
        self.my_genes[5] = randrange(0, len(PANTS))          # <--  :
        self.my_genes[6] = randrange(0, len(BOOTS))          # <--  :

        if dbg:  #  :
            print('\nf "generate_random_genes":' + '\n\tgenes generated:\n\t', end='')
            print(self.my_genes)


    #      :
    def mutate_genes(self, parent_genes):
        dbg = DBG_rogue_mutate_genes

        #     :
        self.my_genes = parent_genes.copy()

        #    :
        event_mutation = randint(1, 10)

        #     ,    :
        if event_mutation == 10:
            if dbg:  #  :
                print('\nf "mutate_genes"   :' + '\n\t  \n\told genes: ', end='')
                print(parent_genes)
                print('\tnew genes: ', end='')
                print(self.my_genes)
            return 0

        #    :
        else:
            #  ""  =  ,   :
            mutation_volume = randint(0, 30)
            mutation_iters = 1
            if 22 <= mutation_volume <= 28:
                mutation_iters = 2
            elif 29 <= mutation_volume <= 30:
                mutation_iters = 3

            if dbg:  #  :
                print('\nf "mutate_genes" :' + '\n\t : ' + str(mutation_iters))

            #  ,   :
            genes_available = [0, 1, 2, 3, 4, 5, 6]

            #   :
            genes_mutated = []

            current_iter = 0
            while current_iter < mutation_iters:
                if dbg:  #  :
                    print('\tw1')

                #     :
                gene_with_forced_mutation = choice(genes_available)

                #      :
                if gene_with_forced_mutation not in genes_mutated:
                    self.mutate_gene(gene_with_forced_mutation)
                    genes_mutated.append(gene_with_forced_mutation)
                    current_iter += 1
                    if dbg:  #  :
                        print('\tcurrent_iter =', current_iter)
                else:
                    if dbg:  #  :
                        print('\telse, because ' + str(gene_with_forced_mutation) + ' already in genes_mutated')

        if dbg:  #  :
            genes_mutated_str = ''
            if len(genes_mutated) > 1:
                for x in genes_mutated:
                    genes_mutated_str += str(x) + ', '
            else:
                genes_mutated_str = str(genes_mutated[0])
            print('\nf "mutate_genes" :' + '\n\told genes: ', end='')
            print(parent_genes)
            print('\tgenes_mutated: ' + genes_mutated_str)
            print('\tnew genes: ', end='')
            print(self.my_genes)


    #     ,      :
    def mutate_gene(self, gene_id):
        dbg = DBG_rogue_mutate_gene

        current_value = self.my_genes[gene_id]
        new_value = current_value

        if dbg:  #  :
            print('\nf "mutate_gene":' + '\n\tgene_id: ' + str(gene_id) + '\n\told gene value: ' + str(current_value))

        tries = 0
        while new_value == current_value:
            if dbg and tries > 0:  #  :
                print('\tw2, because ' + str(new_value) + ' = ' + str(current_value) )
            new_value = randrange(0, len(LINKS_TO_EQUIP_DICTS[gene_id]))
            self.my_genes[gene_id] = new_value
            tries += 1

        if dbg:  #  :
            print('\tnew gene value: ' + str(new_value) + '\n\ttries: ' + str(tries))


2. Os genes (genótipo) determinam qual engrenagem usa ladrão (imediatamente no objeto de inicialização do nascimento ). Para fazer isso, o método apply_genes é chamado :

apply_genes
    # ""  ()     :
    def apply_genes(self):
        dbg = DBG_rogue_apply_genes

        pointer = 0
        for item_id in self.my_genes:
            self.wear_item(pointer, item_id, LINKS_TO_EQUIP_DICTS[pointer])
            pointer += 1

        if dbg:  #  :
            print('\nf "apply_genes":' + '\n\t.')
            print(self)


3. O equipamento determinará o valor final da "classificação" do ladrão. Para isso, será chamado o método calcular_rate , que calcula a expectativa matemática de dano:

calcular_rate
    #     :
    def calculate_rate(self):
        dbg = DBG_rogue_calculate_rate

        #   :
        p_hit = self.stat_hit / 100

        #      :
        p_glancing = self.stat_glancing_percent / 100
        not_p_glancing = 1 - self.stat_glancing_percent / 100

        #      :
        p_crit = self.stat_crit / 100
        not_p_crit = 1 - self.stat_crit / 100

        #   :
        expectation_modificator = p_hit * (p_glancing * 0.7 + not_p_glancing * (p_crit * 2 + not_p_crit))

        #      :
        expectation_damage = expectation_modificator * self.stat_attackpower
        expectation_damage = round(expectation_damage, 3)

        if dbg:
            print('\t  =', expectation_modificator)
            print('\t  =', expectation_damage)

        return expectation_damage


4. A classificação do assaltante será o fator determinante para a reprodução ou morte vergonhosa. Para isso, são introduzidos os conceitos de "vitória" (método embody_win ) e "derrota" (método embody_defeat ):

embody_win e embody_defeat
    #    :
    def embody_win(self):
        dbg = DBG_rogue_embody_win

        self.my_wins += 1
        stats.genes_add_win(self.my_genes)

        #    :
        if self.my_wins % population.wins_to_reproduce == 0:

            #    :
            total_borns = choice(population.possible_birth_quantities)
            if dbg:
                print('  ' + str(total_borns))

            for x in range(0, total_borns):
                if dbg:
                    print(self.name + '  ...')

                #  -:
                new_rogue = Rogue(self.my_genes, self.my_generation, from_parent=True)
                ROGUES_LIST.append(new_rogue)

            Population.day_of_last_changes = current_day

        #         :
        if self.my_wins > Population.record_max_wins:
            Population.record_max_wins = self.my_wins
            Population.max_winner_name = self.name
            Population.max_winner_genes = self.my_genes


    #    :
    def embody_defeat(self):
        dbg = DBG_rogue_embody_defeat

        self.my_defeats += 1

        #    :
        if self.my_defeats == population.defeats_to_die:
            self.alive = False
            Population.how_many_rogues_alive -= 1
            Population.day_of_last_changes = current_day

            if dbg:
                print(self.name + ' ...')


Bem, o construtor da classe foi redesenhado de acordo para fazer com que essa cadeia “genes - equipamento - classificação” funcione:

Construtor de classes não autorizadas
    def __init__(self, genes_list_inherited, parent_generation, from_parent=True, genes_can_mutate=True):

        #    ,    id  :
        # 0 -  , 1 -  , 2 - , 3 - , 4 - , 5 - , 6 - 
        self.equipment_slots = [0] * 7

        #    ,      :
        self.equipment_names = [''] * 7

        #    ( -     ):
        self.basic_stat_agility = 50
        self.basic_stat_power = 40
        self.basic_stat_hit = 80
        self.basic_stat_crit = 20
        self.basic_stat_mastery = 0

        #     :
        self.set_stats_without_equip()

        #  :
        Population.how_many_rogues += 1
        Population.how_many_rogues_alive += 1

        #  :
        self.my_generation = parent_generation + 1
        if self.my_generation > Population.generations:
            Population.generations = self.my_generation

        # "" :
        self.name = '"' + str(Population.how_many_rogues) + '-,   ' + str(parent_generation + 1) + '"'

        #  :
        self.alive = True

        #    :
        self.my_wins = 0
        self.my_defeats = 0

        #   :
        self.my_genes = [0] * 7

        if genes_can_mutate:
            #      ,     :
            if from_parent:
                self.mutate_genes(genes_list_inherited)
            else:
                self.generate_random_genes()
        else:
            self.my_genes = genes_list_inherited

        #     :
        stats.genes_add_presence(self.my_genes, self.my_generation)

        #     :
        self.apply_genes()


Para resumir essa lógica de classe com uma figura:

imagem

ETAPA 3 - População, estatísticas e classes de desafio


A classe Population é necessária apenas para três coisas:

  1. criar uma população a partir do número especificado de ladrões com genes aleatórios e parâmetros biológicos que sempre serão inalterados ( wins_to_reproduce - quantas vitórias um ladrão precisa obter para reprodução, derrotas_para_duas - quantas derrotas farão um indivíduo morrer);
  2. "Redefinir" da população, ou seja, quando o último dia do estágio atual termina, todos os ladrões são destruídos e os ladrões com os melhores genótipos são criados (consulte o método de recarga );
  3. armazenando alguns dados da população para estatísticas.

população de classe
class Population():
    """     ."""

    how_many_rogues = 0 # <--   
    how_many_rogues_alive = 0 # <--   
    how_many_battles = 0 # <--  
    how_many_ties = 0 # <--  
    generations = 0 # <--  
    day_of_last_changes = 0  # <--       
    max_days_without_changes = 0  # <--    

    #        ,      :
    record_max_wins = 0
    max_winner_name = 'none'
    max_winner_genes = 'none'


    #       :
    def __init__(self, total, possible_birth_quantities, wins_to_reproduce, defeats_to_die):

        #   :
        self.initial_size = total

        #   ,        :
        self.wins_to_reproduce = wins_to_reproduce
        self.defeats_to_die = defeats_to_die
        self.possible_birth_quantities = possible_birth_quantities

        while total > 0:

            #   ,  " "    ,     :
            new_rogue = Rogue('', 0, from_parent=False)

            #   :
            ROGUES_LIST.append(new_rogue)

            total -= 1


    #     :
    def __str__(self):
        text = ':\n'
        text += ': ' + str(Population.generations) + '\n'
        text += ' : ' + str(Population.how_many_rogues) + '\n'
        text += ' : ' + str(Population.how_many_rogues_alive) + '\n'
        text += ' : ' + str(Population.record_max_wins) + '\n'
        text += ' : ' + str(Population.max_winner_name) + '\n'
        text += ' : ' + str(Population.max_winner_genes) + '\n'
        return text


    #  ,    how_many_to_save     :
    def reload(self, how_many_to_save):

        #  - :
        Population.how_many_rogues_alive = 0

        # ""   :
        for x in ROGUES_LIST:
            x.alive = False

        #        :
        list_genotypes_top = stats.get_ordered_list_from_dict(LIST_FOR_DICTS_GENOTYPES[current_stage], inner_index=2)

        #      :
        for x in range(0, how_many_to_save):

            #      :
            genotype_in_str = list_genotypes_top[x][0]
            genotype_in_list = []
            for char in genotype_in_str:
                if char != '-':
                    genotype_in_list.append( int(char) )

            #   ,      ,     :
            new_rogue = Rogue(genotype_in_list, 0, from_parent=True, genes_can_mutate=False)

            #   :
            ROGUES_LIST.append(new_rogue)


A classe Stats executa duas tarefas principais:

  1. coleta de dados durante a simulação (por exemplo, indicadores de cada dia: o número de ladrões vivos, o número de genótipos exclusivos, etc.)
  2. ( matplotlib, HTML-, ).

No futuro, mostro como será o relatório:

imagem

O código do modelo HTML do relatório está disponível no github .

A classe Stats também é melhor visualizada no github (caso contrário, haverá muitas linhas aqui).

A classe Challenger é responsável por simular colisões entre ladrões selecionados aleatoriamente. Todos os dias, o método é chamado perform_battles , que se forma a partir de ladrões vivos duelando e os confronta com um método perform_battle , e eventualmente para cada ladrão é causado um método embody_win , qualquer método embody_defeat. A propósito, se ocorrer um empate (genótipos e, portanto, a classificação dos assaltantes são os mesmos), eles divergem sem consequências:

desafiante de classe
class Challenger():
    """      ."""

    #   :
    def perform_battles(self):
        dbg = DBG_challenger_perform_battles

        #    :
        rogues_alive = []
        for x in ROGUES_LIST:
            if x.alive:
                rogues_alive.append(x)

        #  :
        shuffle(rogues_alive)

        #       :
        pairs_total = int(len(rogues_alive) // 2)

        if dbg:
            print('pairs_total =', pairs_total)

        #       :
        counter = 0
        pointer = 0
        while counter < pairs_total:
            a_1 = rogues_alive[pointer]
            a_2 = rogues_alive[pointer + 1]
            self.perform_battle(a_1, a_2)
            counter += 1
            pointer += 2


    #     :
    def perform_battle(self, rogue_1, rogue_2):
        dbg = DBG_challenger_perform_battle

        if dbg:
            print('\n  :', rogue_1.name, '', rogue_2.name)

        #     (        ):
        rating_1 = rogue_1.calculate_rate()
        rating_2 = rogue_2.calculate_rate()

        #  :
        Population.how_many_battles += 1

        if dbg:
            print('\t :', rating_1, '', rating_2, '.')

        #      :
        if rating_1 > rating_2:
            rogue_1.embody_win()
            rogue_2.embody_defeat()
        elif rating_1 < rating_2:
            rogue_1.embody_defeat()
            rogue_2.embody_win()
        else:
            Population.how_many_ties += 1
            if dbg:
                print('\t !  !')


Bem, agora nos voltamos para o corpo do programa, que liga todas as partes mencionadas do código. Primeiro, vêm as constantes que determinam os principais parâmetros da simulação: o número de estágios, dias em um estágio, o número inicial de assaltantes em uma população, etc. Em seguida, vem as constantes auxiliares para depuração. E os próprios desafios:

código para executar a simulação
# :
MAX_STAGES = 8 # <--      
MAX_DAYS_AT_STAGE = 20 # <--        
SLIDING_FREQUENCY = 10 # <--     HTML-    (1 =   , 10 =   10 )
ROGUES_AT_BEGIN = 10 # <--    (  )
WINS_TO_REPRODUCE = 2 # <--     ,  
DEFEATS_TO_DIE = 2 # <--      
POSSIBLE_BIRTH_QUANTITIES = [1] # <--   ,       , :
# [1, 2] ,   50%-    1,  1 
# [1, 1, 2] ,   66%-   1 

HTML_GSQUARE_SIDE = 10 # <--   ,     
HTML_GSQUARE_MARGIN = 3 # <--   ,     

#     :
LINKS_TO_EQUIP_DICTS = [RIGHT_HANDS, LEFT_HANDS, GLOVES, HEADS, CHESTS, PANTS, BOOTS]

#         (    ):
LIST_FOR_DICTS_GENOTYPES = [{}] * MAX_STAGES

#        :
DICT_UNIQUE_GENOTYPES = {}

#      :
DICT_DAYS = {}

# ,       -  :
ROGUES_LIST = list()

#       (     )
current_stage = 0

#     "" (    /):
DBG_rogue_mutate_genes = False
DBG_rogue_generate_random_genes = False
DBG_rogue_apply_genes = False
DBG_rogue_calculate_rate = False
DBG_rogue_mutate_gene = False
DBG_rogue_embody_win = False
DBG_rogue_embody_defeat = False
DBG_rogue_wear_item = False
DBG_challenger_perform_battles = False
DBG_challenger_perform_battle = False
DBG_days_report = False  # <--     



# :
if __name__ == '__main__':

    #  :
    time_begin = time()

    #        :
    max_days_for_current_stage = 0
    current_day = 1
    while current_stage < MAX_STAGES:

        #    :
        if current_stage == 0:

            #   :
            stats = Stats()

            #          ,      :
            population = Population(ROGUES_AT_BEGIN, POSSIBLE_BIRTH_QUANTITIES, wins_to_reproduce=WINS_TO_REPRODUCE, defeats_to_die=DEFEATS_TO_DIE)

            #     :
            challenger = Challenger()

            # "" :
            print(population)

        #        :
        max_days_for_current_stage += MAX_DAYS_AT_STAGE

        # /      (1  = 1    (*) )
        # * -     -    , -           
        while current_day <= max_days_for_current_stage:
            print('st ' + str(current_stage) + ', day ' + str(current_day))
            if DBG_days_report:
                print('\n\n/DAY', current_day)

            #   :
            challenger.perform_battles()

            if DBG_days_report:
                print('\n', current_day, '.')
                print(population)

            #    :
            stats.add_new_day(current_day)

            #        :
            if current_day - Population.day_of_last_changes > Population.max_days_without_changes:
                Population.max_days_without_changes = current_day - Population.day_of_last_changes

            #    SLIDING_FREQUENCY  (     )     :
            if current_day % SLIDING_FREQUENCY == 0 or current_day == 1 or current_day == MAX_DAYS_AT_STAGE * MAX_STAGES:
                stats.draw_genes_distribution(current_day)

            #    SLIDING_FREQUENCY  (     )     :
            if current_day % SLIDING_FREQUENCY == 0 or current_day == 1 or current_day == MAX_DAYS_AT_STAGE * MAX_STAGES:
                stats.draw_genes_wins(current_day)

            current_day += 1

        #      ,  ,     -  :
        if current_stage < MAX_STAGES - 1:
            population.reload(ROGUES_AT_BEGIN)

        current_stage += 1


    #      ,      LIST_FOR_DICTS_GENOTYPES:
    current_stage -= 1

    #   :
    print('\n:')
    print(DICT_DAYS)

    #       :
    stats.draw_and_put_line_chart_to_file(DICT_DAYS, 1, ' ', '', '', 'charts/chart_population_demography.png')

    #       -  :
    stats.draw_and_put_line_chart_to_file(DICT_DAYS, 0, ' ', '', '', 'charts/chart_population_total.png')

    #      :
    stats.draw_and_put_line_chart_to_file(DICT_DAYS, 2, ' ', '', ' ', 'charts/chart_genotypes_variety.png')

    #      (=   ):
    stats.draw_and_put_line_chart_to_file(DICT_DAYS, 3, ' ', '', '', 'charts/chart_genotypes_ties.png')

    #   HTML   :
    stats.create_index_html()

    #   :
    time_session = time() - time_begin
    duration_info = '  : ' + str(round(time_session, 2)) + ' .'
    print('\n' + duration_info)

else:
    print('__name__ is not "__main__".')


No final da simulação, um arquivo com o nome do tipo "report 2020-04-25_10-33-54.html" é gerado na pasta do programa. Mais sobre isso na próxima parte do artigo.

PASSO 4 - visualize os dados


Para mais explicações, usarei este conjunto de equipamentos:

evolution_equipment_obvious_strong.py
#       .
#    ,     :
# 0 - , 1 - , 2 - , 3 - , 4 - , 5 - , 6 - 

EQUIPMENT_COLLECTION = 'obvious_strong'

RIGHT_HANDS = dict()
RIGHT_HANDS[1] = (' ', 5000, 0, 0, 0, 0, 0)
RIGHT_HANDS[2] = (' ', 800, 0, 0, 0, 0, 0)
RIGHT_HANDS[3] = (' ', 20, 0, 0, 0, 0, 0)

LEFT_HANDS = dict()
LEFT_HANDS[1] = (' ', 4000, 0, 0, 0, 0, 0)
LEFT_HANDS[2] = (' ', 10, 0, 0, 0, 0, 0)

GLOVES = dict()
GLOVES[1] = (' ', 300, 0, 0, 0, 0, 0)
GLOVES[2] = (' ', 1, 0, 0, 0, 0, 0)

HEADS = dict()
HEADS[1] = (' ', 300, 0, 0, 0, 0, 0)
HEADS[2] = (' ', 1, 0, 0, 0, 0, 0)

CHESTS = dict()
CHESTS[1] = (' ', 300, 0, 0, 0, 0, 0)
CHESTS[2] = (' ', 1, 0, 0, 0, 0, 0)

PANTS = dict()
PANTS[1] = (' ', 300, 0, 0, 0, 0, 0)
PANTS[2] = (' ', 1, 0, 0, 0, 0, 0)

BOOTS = dict()
BOOTS[1] = (' ', 300, 0, 0, 0, 0, 0)
BOOTS[2] = (' ', 1, 0, 0, 0, 0, 0)


Aqui estão as combinações possíveis de 192 conjuntos (3 * 2 * 2 * 2 * 2 * 2 * 2). Nesse sentido, haverá 192 possíveis genótipos .

A principal idéia da visualização : representar todo o espaço de possíveis genótipos como um campo retangular, onde cada quadrado representa um genótipo separado. Um algoritmo simples encontra dois divisores intermediários do número 192, são 16 e 12:

o código para esse algoritmo do construtor da classe Stats
        #      - :
        list_divisors = list()
        current_number = int(self.genotypes_total // 2)
        while current_number >= 2:
            if self.genotypes_total % current_number == 0:
                list_divisors.append(current_number)
            current_number -= 1
        print('list_divisors:', list_divisors)

        #        :
        somewhere_in_the_middle = len(list_divisors) // 2

        #     :
        side_1 = list_divisors[somewhere_in_the_middle]
        side_2 = self.genotypes_total / side_1

        # ,   ,     ""  :
        self.side_x = int(side_1 if side_1 >= side_2 else side_2)
        self.side_y = int(self.genotypes_total / self.side_x)
        print('side_x =', self.side_x, 'side_y =', self.side_y)


Temos uma área 16x12:

imagem

Uma lista de possíveis genótipos é gerada por este código:

        #    :
        self.list_of_possible_genotypes = list()

        #      :
        for g1 in RIGHT_HANDS:
            #      :
            for g2 in LEFT_HANDS:
                #   :
                for g3 in GLOVES:
                    #   :
                    for g4 in HEADS:
                        #   :
                        for g5 in CHESTS:
                            #   :
                            for g6 in PANTS:
                                #   :
                                for g7 in BOOTS:
                                    current_genotype = str(g1-1)+'-'+str(g2-1)+'-'+str(g3-1)+'-'+str(g4-1)+'-'+str(g5-1)+'-'+str(g6-1)+'-'+str(g7-1)
                                    self.list_of_possible_genotypes.append(current_genotype)

Portanto, os quadrados no campo representam os genótipos nesta ordem:

imagem

Esta é uma característica importante, porque, por exemplo, os genótipos que contêm o "gene Strong Sword" ( canto azul ) estão na parte superior do campo e os que também contêm o "gene Strong Dagger" ( canto azul ) - ocupam uma região ainda mais alta:

imagem

assim, o genótipo mais forte ( 0-0-0-0-0-0-0-0-0 ) fica no canto superior esquerdo e o mais fraco ( 2-1-1-1- 1-1-1 ) - pelo contrário. Isso nos ajudará a observar a dinâmica da situação, porque saberemos que durante a simulação o "pool genético dominante" deve mudar para o canto superior esquerdo do campo:

imagem

Agora execute a simulação com os seguintes parâmetros:

MAX_STAGES = 5
MAX_DAYS_AT_STAGE = 40
SLIDING_FREQUENCY = 1
ROGUES_AT_BEGIN = 8
WINS_TO_REPRODUCE = 2
DEFEATS_TO_DIE = 2
POSSIBLE_BIRTH_QUANTITIES = [1]

HTML_GSQUARE_SIDE = 16
HTML_GSQUARE_MARGIN = 3

Ou seja, 8 ladrões no início, <div> -contêineres com campos (a seguir denominados slides ) são criados para cada dia, 5 estágios de 40 dias cada. Como resultado, obtemos dois conjuntos de slides de 200 peças cada: " prevalência de genótipos " (cores azul esverdeado) e " vitória de genótipos " (cores vermelho e verde). A saturação da cor dos quadrados depende dos valores.

Abrimos o relatório HTML gerado e vemos esta foto pelo primeiro e pelo décimo dia:

imagem

Portanto, no primeiro dia, foram gerados 8 ladrões, seus quadrados projetarão uma sombra até o final da simulação. Além disso, vemos que eles começaram a lutar, o que significa que as primeiras vitórias aparecem, levando à reprodução. A reprodução com alta probabilidade está associada a mutações, portanto, há mais quadrados coloridos.

Dê uma olhada nos dias seguintes. No 44º dia, o genótipo " 0-0-0-0-0-0-0-0 " apareceu (veja atrás da seta azul). No 59º dia, ele já venceu as vitórias (veja atrás da seta vermelha).

imagem

No 137º dia, pode-se ver que " 0-0-0-0-0-0-0-0 " foi vencido pelo número de aparições na população (veja a seta azul). E no slide do último dia ele tinha uma sombra dourada, pois ocupava o primeiro lugar em termos de número de vitórias.

imagem

Como você pode ver, de fato, a seleção natural muda o “pool genético” da população para a esquerda e para cima, ou seja, em direção a equipamentos mais fortes. Os genótipos " inferior direito " são mais fracos que os " superior esquerdo ", portanto desaparecem assim que aparecem (vermelho indica que não há vitórias).Isso é mais claramente visto em b sobreem uma escala maior:

imagem

Bem, o veredicto no relatório é verdadeiro: o genótipo vencedor é 0-0-0-0-0-0-0-0 : Os

imagem

gráficos obtidos usando o matplotlib ajudarão a avaliar melhor o que está acontecendo:

1. o gráfico " população viva " mostra a dinâmica mudanças no número de ladrões vivos simultaneamente;
2. o gráfico de " nascido todos " é quase sempre uma linha reta, se você não começar a brincar com parâmetros demográficos:

imagem

3. o gráfico de " diversidade de genótipos " - geralmente crescimento rápido no início, depois diminui a velocidade, aproximando-se gradualmente do máximo;
4. agendar " desenhar dinâmica"- mostra como o número de empates muda ao longo do tempo (são brigas quando dois genótipos idênticos se chocam em uma luta sem sentido):

imagem

acontece que uma população" estagna "quando ladrões com os mesmos genótipos permanecem nela (os gráficos mostram: um aumento acentuado no número de empates, imutabilidade . em termos de população e número de genótipos únicos)

estagnação da população significa que os slides de cálculo e desenho vão em vão para reduzir os períodos de estagnação, é necessário reduzir o valor MAX_DAYS_AT_STAGE constante e será visto que o congestionamento diminuiu e, em alguns lugares, desapareceu completamente .:

imagem

PASSO 5 - experimente grandes espaços genotípicos


Agora vamos tentar executar a simulação para o conjunto de equipamentos evolution_equipment_custom discutido no último artigo com parâmetros ligeiramente diferentes. Aqui, o valor POSSIBLE_BIRTH_QUANTITIES = [1, 2] significa que, com o ato de reprodução com uma probabilidade de 50%, nascerão 1 ou 2 descendentes. Isso aumentará a dinâmica do que está acontecendo:

MAX_STAGES = 20
MAX_DAYS_AT_STAGE = 50
SLIDING_FREQUENCY = 10
ROGUES_AT_BEGIN = 8 
WINS_TO_REPRODUCE = 2
DEFEATS_TO_DIE = 2
POSSIBLE_BIRTH_QUANTITIES = [1, 2]

HTML_GSQUARE_SIDE = 10
HTML_GSQUARE_MARGIN = 3

Aqui o padrão de distribuição dos genótipos nas lâminas será diferente, pois o equipamento mais significativo para o resultado geral ("Espada do Mestre") está agora em "outros lugares". A visualização deste conjunto de equipamentos já é caracterizada pelo aparecimento de vários “focos de energia” em diferentes locais onde a energia do equipamento é maior do que nas “áreas” vizinhas.

imagem

A propósito, esse algoritmo determinou inconfundivelmente o conjunto superior de equipamentos ( 3-3-0-0-0-0-0-1 ), que corresponde ao determinado pelo algoritmo combinatório do artigo anterior :

imagem

E, finalmente, executei o teste para o conjunto estendido , o que fornece 18432 combinações:

com tais parâmetros
MAX_STAGES = 30
MAX_DAYS_AT_STAGE = 50
SLIDING_FREQUENCY = 100
ROGUES_AT_BEGIN = 20
WINS_TO_REPRODUCE = 2
DEFEATS_TO_DIE = 2
POSSIBLE_BIRTH_QUANTITIES = [1, 2]

HTML_GSQUARE_SIDE = 4
HTML_GSQUARE_MARGIN = 1


imagem

Em geral, você pode continuar se divertindo com isso, mas a tendência permanece inalterada: durante a simulação, os genótipos rapidamente começam a se acumular nesses mesmos "centros de poder". E o genótipo dominante está em um dos "focos" mais impressionantes.

PASSO 6 - Tudo faz sentido?


Se nos voltarmos agora para o lado prático da questão, precisamos entender se esse algoritmo genético é capaz de encontrar a resposta certa ao custo de menos lutas do que uma simples busca combinatória de todos os genótipos. Resposta: sim, capaz . Prova sob o corte:

prova da eficácia do algoritmo genético
1728 .

, .. 1728 .

, 1728 . «» — 2 ( ). , , 1728 / 2 = 864. .

:

MAX_STAGES = 8
MAX_DAYS_AT_STAGE = 20
SLIDING_FREQUENCY = 10
ROGUES_AT_BEGIN = 10
WINS_TO_REPRODUCE = 2
DEFEATS_TO_DIE = 2
POSSIBLE_BIRTH_QUANTITIES = [1]

HTML_GSQUARE_SIDE = 10
HTML_GSQUARE_MARGIN = 3

, 3-3-0-0-0-0-1:

image

:

image

, 3-3-0-0-0-0-1 547 . 87 (), 16% .

.

PS É possível obter a resposta correta mesmo com ROGUES_AT_BEGIN = 2 e POSSIBLE_BIRTH_QUANTITIES = [1] . Isso parece surpreendente, porque a população nunca ultrapassa os 2 assaltantes. Como um perde, o outro vence, o primeiro morre e o segundo dá à luz um descendente. E os pais começam a competir com esse descendente. O descendente é mais forte ou mais fraco. E assim a roda de seleção cruel se move entre os pais e o descendente até que ele chegue a um ponto melhor (o que ele pode alcançar no tempo previsto, portanto nem sempre é o melhor).

Sumário


  1. O problema é resolvido usando algoritmos genéticos.
  2. «» .
  3. , , ( calculate_rate Rogue, ).
  4. Obviamente, neste programa você ainda pode experimentar e experimentar, melhorando a eficiência. Por exemplo, em algum momento, comece a "banir" obviamente perder genótipos, não permitindo que seus proprietários briguem ou até apareçam. Assim, o funil dos "líderes" diminuirá, onde eles devem determinar exatamente quem é "mais forte" entre si.

Publiquei todo o código do projeto no github .

Caro comunidade, terei prazer em receber feedback sobre este tópico.

All Articles