web-dev-qa-db-fra.com

Algorithme génétique / w Neural Network jouant au serpent ne s'améliore pas

J'essaie de créer un algorithme génétique pour former un réseau de neurones, dans le but de jouer au serpent de jeu.

Le problème que j'ai, c'est que la forme physique des générations ne s'améliore pas, soit elle reste à la forme physique à laquelle on peut s'attendre en ne donnant aucune contribution au jeu, soit elle ne fait qu'empirer après la première génération. Je soupçonne que c'est un problème avec le réseau neuronal, mais je ne sais pas ce que c'est.

Configuration du réseau neuronal

24 Noeuds d'entrée

2 Calques masqués

8 Noeuds par couche

4 Noeuds de sortie (un pour chaque direction que le serpent peut prendre)

L'entrée est un tableau de toutes les directions que le serpent peut voir. Pour chaque direction, il vérifie la distance par rapport à un mur, un fruit ou lui-même. Le résultat final est un tableau d'une longueur de 3*8 = 24.

Les pondérations et les biais sont des flottants aléatoires compris entre -1 et 1, générés lors de la création du réseau.

Configuration de l'algorithme génétique

Taille de la population: 50000

Parents choisis par génération: 1000

Gardez le sommet par génération: 25000 (nouvelle variable, pour de meilleurs résultats)

Chance de mutation par enfant: 5%

(J'ai essayé de nombreux rapports de taille différents, bien que je ne sois toujours pas sûr de ce qu'est un rapport typique.)

J'utilise un crossover à un seul point. Chaque tableau de poids et de biais est croisé entre les parents et transmis aux enfants (un enfant pour chaque "version" du crossover).

J'utilise ce que je pense être la sélection de roulette pour sélectionner les parents, je posterai la méthode exacte ci-dessous.

L'aptitude d'un serpent est calculée avec: age * 2**score (plus, plus d'informations dans la mise à jour), où l'âge est le nombre de tours que le serpent a survécu et le score est la quantité de fruits qu'il a récoltés.

Détails

Voici un pseudo-code pour essayer de résumer comment mon algorithme génétique (devrait) fonctionner:

pop = Population(size=1000)

while True:  # Have yet to implement a 'converged' check
    pop.calc_fitness()

    new_pop = []

    for i in range(n_parents):

        parent1 = pop.fitness_based_selection()
        parent2 = pop.fitness_based_selection()

        child_snake1, child_snake2 = parent1.crossover(parent2)

        if Rand() <= mutate_chance:
            child_snake.mutate()

        new_pop.append(child_snake1, child_snake2)

    pop.population = new_pop

    print(generation_statistics)
    gen += 1

Voici la méthode que j'utilise pour sélectionner un parent:

def fitness_based_selection(self):
    """
    A slection process that chooses a snake, where a snake with a higher fitness has a higher chance of being
    selected
    :return: The chosen snake's brain
    """
    sum_fitnesses = sum(list([snake[1] for snake in self.population]))

    # A random cutoff digit.
    r = randint(0, sum_fitnesses)

    current_sum = 0

    for snake in self.population:
        current_sum += snake[1]
        if current_sum > r:
            # Return brain of chosen snake
            return snake[0]

Il est à noter que self.population est une liste de serpents, où chaque serpent est une liste contenant le NeuralNet qui le contrôle et la forme physique atteinte par le réseau.

Et voici la méthode pour obtenir une sortie d'un réseau à partir d'une sortie de jeu, car je soupçonne qu'il y a peut-être quelque chose que je fais mal ici:

def get_output(self, input_array: np.ndarray):
    """
    Get output from input by feed forwarding it through the network

    :param input_array: The input to get an output from, should be an array of the inputs
    :return: an output array with 4 values of the shape 1x4
    """

    # Add biases then multiply by weights, input => h_layer_1, this is done opposite because the input can be zero
    h_layer_1_b = input_array  + self.biases_input_hidden1
    h_layer_1_w = np.dot(h_layer_1_b, self.weights_input_hidden1)
    h_layer_1 = self.sigmoid(h_layer_1_w)  # Run the output through a sigmoid function

    # Multiply by weights then add biases, h_layer_1 => h_layer_2
    h_layer_2_w = np.dot(h_layer_1, self.weights_hidden1_hidden2)
    h_layer_2_b = h_layer_2_w + self.biases_hidden1_hidden2
    h_layer_2 = self.sigmoid(h_layer_2_b)

    # Multiply by weights then add biases, h_layer_2 => output
    output_w = np.dot(h_layer_2, self.weights_hidden2_output)
    output_b = output_w + self.biases_hidden2_output

    output = self.sigmoid(output_b)
    return output

Lorsque vous exécutez le réseau neuronal manuellement, avec une version graphique du jeu activée, il est clair que le réseau ne change presque jamais de direction plus d'une fois. Cela m'embrouille, car j'avais l'impression que si tous les poids et biais étaient générés de manière aléatoire, l'entrée serait traitée de manière aléatoire et donnerait une sortie aléatoire, mais la sortie semble changer une fois au premier tour du jeu, puis jamais changer à nouveau de manière significative.

Lors de l'exécution de l'algorithme génétique, la forme physique la plus élevée de chaque génération dépasse à peine la forme physique que l'on pourrait attendre d'un serpent sans entrée (dans ce cas 16), ce qui, je suppose, est corrélé au problème avec le réseau neuronal. Lorsqu'il dépassera, les générations futures reviendront à 16.

Toute aide avec son problème serait grandement appréciée, je suis encore nouveau dans ce domaine et je le trouve vraiment intéressant. Je répondrai avec plaisir à plus de détails si besoin est. Mon code complet peut être trouvé ici , si quelqu'un ose s'y plonger.

Mise à jour:

J'ai changé deux ou trois choses:

  • Correction de la génération de poids/biais, auparavant, ils ne généraient qu'entre 0 et 1.
  • Modification de ma méthode de croisement pour renvoyer deux enfants par ensemble de parents au lieu d'un seul.
  • Modification de la fonction de fitness pour qu'elle ne soit égale qu'à l'âge du serpent (à des fins de test)
  • Modification des variables de population

Maintenant, l'algorithme fonctionne mieux, la première génération trouve généralement un serpent avec une forme physique de 14-16, ce qui signifie que le serpent fait des tours pour éviter la mort, mais il ne descend presque toujours qu'à partir de là. Le premier serpent aura en fait réalisé une tactique de virage à proximité des bords est et nord/sud, mais jamais du bord ouest. Après la première génération, la forme physique ne fait qu'empirer, pour finalement revenir à la forme physique la plus basse possible. Je ne sais pas ce qui ne va pas, mais j'ai l'impression que c'est peut-être quelque chose de grand que j'ai négligé.

Mise à jour # 2:

J'ai pensé que je pourrais aussi bien mentionner certaines choses que j'ai essayées qui n'ont pas fonctionnent:

  • Les nœuds par couche cachée ont été modifiés de 8 à 16, ce qui a rendu les serpents beaucoup moins performants.
  • Le serpent a pu redevenir lui-même, ce qui a également rendu les serpents moins performants.
  • Grand (je pense qu'ils sont grands, je ne sais pas ce qu'est une taille pop standard.) Taille de la population d'environ 1 000 000, avec environ 1 000 parents, pas de changement positif.
  • 16 ou 32 nœuds par couche cachée, avaient apparemment peu ou pas d'impact.
  • Correction de la fonction de mutation pour attribuer correctement des valeurs comprises entre -1 et 1, sans impact notable.

Mise à jour # 3:

J'ai changé quelques choses et j'ai commencé à voir de meilleurs résultats. Tout d'abord, j'ai empêché les fruits de se reproduire pour simplifier le processus d'apprentissage, et j'ai plutôt donné aux serpents une forme physique égale à leur âge (combien de tours/images ils ont survécu), et après avoir désactivé la normalisation du tableau d'entrée, j'ai eu un serpent avec un fitness de 300! 300 est l'âge maximum qu'un serpent peut avoir avant de mourir de vieillesse.

Cependant, le problème persiste, car après les deux premières générations, la forme physique va chuter, les 1 à 5 premières générations peuvent avoir une forme physique de 300 (parfois non, et ont une faible forme physique à la place, mais je suppose que c'est en baisse) à la taille de la population.), mais après cela, la forme physique des générations chutera à environ 20-30 et y restera.

De plus, si je rallume les fruits, les serpents retrouvent une forme physique abyssale.Parfois, la première génération atteindra un serpent capable de se déplacer en boucles et donc d'obtenir une forme physique de 300 sans ramasser de fruits, mais cela n'est presque jamais transféré à la suivante génération.

24
Ben Wo

J'ai remarqué que dans votre pseudocode, lors de la création de chaque nouvelle génération, la génération parent est complètement effacée et seule la génération enfant est conservée. Cela peut naturellement conduire à une baisse des niveaux de fitness, car rien ne garantit que la progéniture aura des niveaux de fitness comparables à ceux de la génération parentale. Afin de garantir que les niveaux de forme physique ne diminuent pas, vous devez soit fusionner les générations parent et enfant et élaguer les membres les plus faibles (ce que je recommanderais), ou vous pouvez exiger que la fonction génératrice de progéniture produise une progéniture au moins aussi adaptée comme les parents (par de nombreux essais et erreurs).


Si vous décidez de vous concentrer sur le générateur de progéniture, une façon de garantir (quelque peu) une progéniture améliorée consiste à implémenter la reproduction asexuée en ajoutant simplement une petite quantité de bruit à chaque vecteur de poids. Si le niveau de bruit est suffisamment faible, vous pouvez générer une progéniture améliorée avec un taux de réussite allant jusqu'à 50%. Des niveaux de bruit plus élevés, cependant, permettent une amélioration plus rapide et ils aident à sortir des optima locaux, même s'ils ont des taux de réussite inférieurs à 50%.

4
Default picture

Vous mutez seulement 5% de la population, pas 5% du "génome". Cela signifie que votre population sera fixée incroyablement rapidement - https://en.wikipedia.org/wiki/Fixation_ (population_genetics) .

Cela explique pourquoi la population ne va pas très bien, car vous explorez uniquement une très petite zone du paysage de la forme physique ( https://en.wikipedia.org/wiki/Fitness_landscape ).

Vous devez changer la fonction de mutation pour muter 5% du génome (c'est-à-dire les poids entre les nœuds). N'hésitez pas à jouer avec le taux de mutation également - différents problèmes fonctionnent mieux à différents taux de mutation.

Si vous craignez de perdre le "meilleur génome" actuel, une approche typique du calcul évolutif consiste à copier l'individu ayant la meilleure forme physique sur la prochaine génération sans mutation.

(Désolé, cela aurait probablement dû être un commentaire mais je n'ai pas assez de réputation).

2
dzs757