Files
TermNSI/Projets/src/Activité.md

18 KiB

TP d'introduction aux algorithmes génétiques

Durée : 2h00

Objectifs à atteindre :

  • Comprendre le fonctionnement d'un algorithme génétique
  • Parvenir à tester son algorithme
  • Niveau des connaissances : Première
  • Pré requis :
    • Avoir suivi les cours sur les algorithmes

    • Être bien réveillé et suivre la fiche d'activité !

    • Systèmes mis en œuvre : PC connecté au réseau

    • Logiciels utilisés : Thonny.

Type de document

Il s'agit d'un TP de découverte des algorithmes dits "génétiques"

Table des matières

  1. Introduction
  2. Le coffre-fort
  3. Consignes
  4. Au boulot !

Introduction

Dans le dense monde des algorithmes, le génétique est un type particulier. Il ne s'appuie sur aucune forme particulière, et propose de résoudre un problème de manière évolutionniste (comprendre à la manière de Darwin ) et cherche à trouver une solution à l'aide de tests de comparaison.

Ce type d'algorithme necessite un jeu de données, lequel servira de source d'échantillons afin d'effectuer les tests et de stocker les résultats.

Un peu comme lorsque nous avons vu les algorithmes gloutons, il s'agira d'effectuer le meilleur cas optimal mais ici ça ne sera pas localement mais globalement, à l'aide du principe de selection naturelle :

À l'aide d'une classe particulière, nous allons pouvoir procéder à des mesures entre plusieurs individus (on parle d'individus peu importe le type de données comparé, n'oubliez pas qu'on se base sur les travaux de Darwin) et ne garder que les meilleurs éléments selon un score établi.

Nous créerons donc de nouveaux jeux de données avec ces meilleurs éléments qui devraient nous rapprocher du résultat recherché.

En somme, un algorithme génétique compare des éléments pour affiner une recherche et se rapprocher d'un résultat optimal.


Le coffre-fort

En informatique, on connait le principe de bruteforce ou d'attaque par dictionnaire, qui se rapproche du principe d'algorithme générique.

Mais on peut appliquer aussi cet algorithme en réel, par exemple pour trouver une chaîne de caractère la plus proche possible d'un mot donné, - un peu comme si vous jouiez à Motus - à des jeux type mastermind, sobokan, ou à un parcours de labyrinthe, trouver un code pin...etc.

Après tout, tous ces exemples ont une solution et il est possible de tester différentes combinaisons afin de s'en approcher voir même de trouver cette solution.

Songez à un cas pratique qui vous demanderait une combinaison, un code...Et vous pourriez faire quelques essais pour trouver...Une combinaison de coffre fort !

coffre.jpg

Après tout, nous disposons d'un jeu de données : les chiffres [0 - 9], il manque juste une donnée : la longueur du code à trouver.

Notre but sera donc de proposer un code python permettant de tester un nombre à n chiffres, en regardant combien d'essais sont necessaires pour trouver la bonne formule qui ouvrira ce coffre.

Pour simplifier la chose, nous fixerons n nous mêmes afin de réduire le nombre de possibilités : en admettant un code de longueur n avec tous les chiffres possibles, nous aurions 10^n possibilités...

Par exemple, un code de longueur n = 4 qui utiliserait tous les chiffres (0 à 9) aurait 10^4 (10 000) combinaisons possibles ! Imaginez le temps que celà prendra, même avec une bonne machine, si n valait 20, 30, 100 !

Il est donc temps de s'y mettre car cela risque de durer un moment.


Consignes

Avant de commencer à programmer, rappelons les besoins :

  1. Décider de quel type seront les individus que nous allons comparer 2. Un jeu de données 3. Une classe permettant de traiter ces données

  2. Comme indiqué plus haut, nous allons rechercher une suite numérique : il semble logique que le type d'individus que nous allons comparer se trouve être des chiffres. Nous devrons donc indiquer une liste de chiffres possiblement utilisables pour la combinaison.

  3. Quel type de structure utiliser ?

Une liste comprenant les nombres sera plus facile à employer, d'autant qu'il s'agit de stocker des nombres.

  1. Les fichiers individual_interface.py et problem_interface.py vous sont fournis : les classes que vous devez écrire en héritent.

Il s'agira donc de créer un fichier de classe héritant d'individual_interface et un autre héritant de problem_interface.

Il faudra définir les méthodes de ces classes et adapter le code fournit pour notre problème.

N'oubliez pas d'importer les bibliothèques necessaires ainsi que les fichiers python dont vous avez besoin.


Au boulot !

Nous allons travailler sur deux fichiers différents : dans le but d'être clair et de ne pas se perdre, mieux vaut en faire un (1) à la fois.

Commençons par le fichier combinaison.py, qui hérite d'individual_interface : ce fichier nous permettra d'écrire le comportement de notre algorithme.

Avant de créer la classe, déclarons la liste des chiffres susceptibles d'être utilisés :


CHIFFRE = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]

Maintenant, il va s'agir de remplir ces espaces vides

  1. Copier notre individu

class Combinaison(Individual_Interface):
   
   
   def copy(self):
       '''
       créé une copie d'un individu (appelé génome) 

       :return: un individu nouveau, qui est son propre clone
       :rtype: un type d'individu
       '''
       copy = ....(self.get_size())
       copy.set_value(self.get_value())
       return ....

Ici, il est question de copier l'individu sur lequel nous allons travailler : rappelez vous, notre jeu de données sera modifié et remplacé par un nouveau, après les tests. Donc nous avons besoin de cloner nos données.


2. Initialiser un nouvel individu

Pour remplir notre jeu de données avec de nouveaux individus, il nous faut bien une initialisation.


def init_value(self):
        '''
        randomly initialize the genome value of self
        '''
        
        return [choice(....) for .... in .... (self.get_size())]



Rappel : choice est une méthode permettant de retourner un nombre aléatoire dans une séquence spécifique. Il faut donc importer une bibliothèque et choisir la séquence indiqué plus tôt.


  1. Croiser deux individus afin de générer un tuple de deux nouveaux individus.

Les deux individus passés en paramètres de la fonction seront appelés self et other.

Pour les besoins de cette fonction, il sera necessaire de créer un "pivot" qui sera un nombre aléatoire compris entre 0 et le nombre d'élements dans la séquence CHIFFRE (donc sa longueur...)

Ce pivot permettra de créer des individus aléatoire grâce au croisement.

On appellera test_1 et test_2 nos deux nouveaux individus obtenus depuis self et other ainsi qu'à l'aide de pivot.

Finalement, nous aurons besoin de définir la valeur de nos deux nouveaux amis

    test_1.set_value(param1[:...] + param2[...:])
    test_2.set_value(param2[:...] + param1[...:])

def cross_with(self, other):
        '''
        Croiser self et other, grâce à un point pivot, puis retourner deux nouveaux individus créés.

        :param other: le second individu
        :type other: un individu
        :return: Les deux individus crées par le croisement, test_1 et test_2
        :rtype: tuple de test_1 et test_2
        '''
        value = self.
        other_value = other.
        
        #générer le nombre pivot ici
        
        #initialiser test_1 et test_2 (comme vous avez créé les individus de base)
        
        #modifier la valeur de test_1
        #modifier la valeur de test_2
        
        return (...)


  1. Enfin, puisqu'on parle de génétique, de croisement : lorsque l'on va tester notre combinaison de coffre fort, et bien si le test n'est pas concluant du premier coup, il faudra changer les chiffres utilisés et pour cela, on parlera de mutation

Chaque individu est susceptible d'être transformé par un autre individu, qui fera partie de la séquence génétique que nous avons déclaré au tout début...


def mutate(self, probability):
        '''
        Appliquer la mutation à l'individu self passé en paramètre : chaque élément de la séquence (combinaison ici) est changé aléatoirement selon une probabilité donnée

        Effet de bord : l'individu va être modifié

        :param probability: la probabilité de mutation de chaque génome
        :type probability: float
        :UC: probability entre [0,1[
        '''
        for .... in (len(self.get_value())):
            #variable = uniform (nombre_min, nombre_max) 
            #si variable < param probabilité :
                self.mutate_gene(gene)



On vous donnera la fonction mutate_gene que l'on utilise ci dessus afin de muter un individu



def mutate_gene(self, gene):
        temp = self.get_value()
        self.set_value(temp[:gene] + [choice(COLOR)] + temp [gene+1:])





Ici, notre fichier combinaison.py est terminé. Il nous reste à travailler sur le fichier de la combinaison : celui qui va créer le problème auquel notre algorithme devra répondre.


Maintenant, attaquons nous à problem.py, qui hérite de problem_interface.py


Ne pas oublier d'importer le fichier dont on hérite, ainsi que le fichier que l'on vient de créer.

C'est parti :

Pour pouvoir résoudre un problème de combinaison à trouver, il faudra tout d'abord....créer le problème à résoudre !

Ainsi, il va falloir initialiser le calcul que nous souhaitons régler par notre algorithme et se faisant, générer une longueur de combinaison à trouver.


class Problem(Problem_Interface) :
    """
    classe probleme : Combinaison Coffre-Fort
    """
    
    def __init__(self, calcul):
        self.__size = #longueur du paramètre calcul
        self.__combinaison = #paramètre calcul
        

Dans le fichier combinaison.py nous clonions des individus. Mais ici, nous allons implémenter la fonction qui permettra de générer cet individu pour un problème donné.



def create_individual(self):
        '''
        create a randomly generated indidivual for this problem

        :return: a randomly generated individual gfor this problem
        :rtype: an Individual object
        '''
     
     #retourne un individu crée par la classe que nous avons définie dans combinaison.py


Un point important non abordé jusqu'ici : on parle de comparer les individus entre eux pour en selectionner les meilleurs. Mais comment les compare t-on ? Sur quel(s) critère(s) ?

Et bien tout simplement en leur attribuant un score, selon leur positionnement par rapport à la bonne réponse :

Plus un individu ressemblera au résultat recherché (donc à l'individu parfait dans notre résultat) plus son score sera élevé.

Au contraire, un individu éloigné de la bonne réponse aura un score plus faible.



def evaluate_fitness(self, individual):
        '''
        Calcule le score d'un individu pour un problème donné.

        :param individual: l'individu à évaluer
        :type individual: un individu
        :return: le score de l'individu évalué
        :rtype: un individu
        '''

        #retourner la somme de  ([1 si calcul[element] est équivalent à self.__combinaison[element] sinon 0 pour chaque element dans la fourchette (longueur(calcul))])


Une fois que chaque individu aura obtenu un score, nous allons pouvoir ranger la population d'individu qui compose notre génétique.

Pour cela, il suffit de trier par un ordre de score.


 def sort_population(self, population):
        '''
        :param population : la liste d'individus à trier
        effet souhaité : on obtient une liste triée d'individus en sortie
        
              
        '''
        #utiliser la méthode sort de la liste en paramètre afin de trier par score.





En supposant que notre liste soit bien triée, desormais, on va souhaiter garder le meilleur de la liste afin de ne conserver que les meilleurs gènes, comme la selection naturelle le fait.



def best_individual(self, population):
        '''
        renvoie le meilleur individu d'une liste.
        à vous de voir si c'est celui avec le plus grand score ou le plus petit 
        '''

        #trier la population 
        #renvoie l'élement en tête de liste ou en bout

Maintenant que l'on a nos meilleurs individus, il est temps d'organiser un Mortal Kombat pour décider qui gagnera !


def tournament(self, first, second):
        '''
        On organise un match entre deux individus, celui qui a le plus gros score remporte la victoire


        :param first: un individu
        
        :param second: un individu
        
        :return: le vainqueur
    
        '''
        #si tel individu est plus grand qu'un autre, alors le resultat = individu_plusfort

        #sinon, resultat = l'autre individu
        #renvoie le resultat

Et maintenant j'ai terminé ? Et bien non. Si vous avez bien suivi il manquait en fait une chose importante : le fichier algo_gen.py, qui vous est fournit, n'est pas complet.

Ce fichier va nous permettre d'initialiser le probleme, de créer une population d'individus, et de tester notre algorithme solution.

Allez, on y est presque !


Comme d'habitude, on n'oublie pas d'importer nos fichiers précédemment crées.


BEST_KEPT = 5

class AlgoGen(object):

    def __init__(self, problem, population_size, crossover_rate, mutation_probability):
        '''
        build an genetic algorithm to solve problem using a population of size population_size and a probability of muation of mutation_probability

        :param problem:  Le probleme que l'on souhaite ré
        :type problem: a Problem object
        :param population_size: the size of the population (must be even)
        :type population_size: int 
        :param mutation_probability: the mutation probability 
        :type mutation_probability: float 
        :UC: population_size must be even and mutation_probability must be  0<= and <1
        '''
        self.__problem = problem
        self.__population_size = population_size
        self.__population = []        
        self.__nb_crossover = crossover_rate
        self.__mutation_probability = mutation_probability
    

    def get_population(self):
        return self.__population

Cette partie vous est donnée complète, elle permet de voir comment on initialise un problème à résoudre, et obtenir la population d'un problème - qui doit être paire, afin de pouvoir la diviser par deux.

Nous allons commencer à modifier les fonctions qui sont incomplètes dans votre fichier algo_gen.py

Tout d'abord, la fonction qui va se charger de créer la population, la genèse. Sachant qu'au dessus, nous avons noté

self.__population = [] 

Que peut-on en conclure de population ?

Du coup, pour générer la population, il suffira de compter chaque élément dans cette structure et d'utiliser la méthode permettant d'ajouter un élément à cette une structure de même type, que vous trouverez dans votre fichier problem.py



 def genesis(self):
        """
        générer la population
        """
        


Pour la fonction suivante, il s'agit d'évaluer non pas un individu spécifique, mais toute une population. C'est à dire chaque individu qui la compose.


def evaluate_population(self, population):
        """
        evalue la  population passée en paramètre
        :param population: (list) la population
        """
        
        #Pour chaque individu, on evalue.


Une fois évalué, que fait - on ? On relance un Mortal Kombat !

C'est ici que l'on exploitera la parité de votre population : On compare toujours deux individus entre eux. Sur une population complète, on peut rapporter cela à un test d'une moitié de la population envers une autre. Il nous restera donc...une moitié.


def next_generation_tournament(self, population):
        '''
        organise un tournoi entre les individu d'une population .
        chaque individu choisi une et une seule fois, dans un tournoi ,
        vainqueur conservé taille résultat = 1/2 taille population

        :param population: (list) la population
        :return: (list) les vainqueurs du tournoi
        
        '''
       

Comme fait précédemment, une fois le tournoi effectué, il faut croiser les individus pour, encore une fois,ne garder que les meilleurs des meilleurs.


def crossover(self, population):
        '''Croise les individus d'une population.

        Chaque individu est sélectionné une et une seule fois, on
        garde le meilleur descendant à chaque fois.

        taille résultat = 1/2 taille population

        :param population: (list) a population
        :return: (list) new individuals
        '''

        #Une liste
        #pour chaque individu dans une fourchette de la moitié de population
            #individu_1 et individu_2 = croisement d'un individu dans la #liste population avec l'individu qui le précède
            
            #Puis on évalue les deux individus
            #On les ajoute à la liste initialisée plus haut
        #et on suit les consignes jusqu'au bout 


Enfin, il ne vous reste plus qu'à intégrer une fonction qui fera muter chaque individu dans une liste donnée en paramètre, qu'on appellera "individuals".

Rappel : Au début de votre fichier se trouve "self.__mutation_probability" qui se révélera utile ici.

Et c'est terminé.

Une dernière chose : ça serait bien de tester. Alors allons y :


if __name__ =="__main__":

    probleme = problem('1','3','3','7')
    #On initialise le probleme avec une combinaison

    solution = AlgoGen(p,100,0,0.1)

    solution.solve(100)