Apprentissage par renforcement

Théorie : Exercices :

Processus décisionnel Markovien (MDP)

En machine learning, on considère qu'il existe trois catégories d'apprentissages :

L'apprentissage par renforcement introduit trois problématiques :

L'apprentissage par renforcement peut être utilisé dans des simulations ou des jeux (robot dans un labyrinthe, échecs, Go, ...) mais aussi dans des situations humaines très concrètes (exploration spatiale, secours dans les décombres, comportement en milieu hostile, ...).

Pacman est dans un environnement hostile, il y a des fantômes, des récompenses intermédiaires
(manger les monstres, les points) et une récompense ultime (finir le niveau)


Pour modéliser un problème dans une configuration de renforcement, on utilise les processus décisionnels markoviens (Markov Decision Process, MDP). Un MDP est un quadruplet $\{ S, A, T, R \}$ définissant :
  • $P$ l'espace des transitions
  • Le but de l'agent est de maximiser sa récompense, nous pouvons définir la récompense cumulée comme suit : $$R_t = r_{t+1} + r_{t+2} + ... + r_T = \sum_{i = t + 1}^{T} r_i$$ Le gain est le début de la vision à long terme : supposez que vous soyez dans un labyrinthe à un instant $t$ et que 5 actions plus tard vous trouviez la sortie et recevez alors la récompense de $1$. Il faut récompenser l'état $t$ qui a fait partie de la séquence de la victoire. Il serait donc injuste de lui donner $0$ comme récompense, mais aussi sur-évalué de lui donner 1 (comme le dernier coup qui a donné la victoire). On peut alors définir un gain cumulé pondéré : $$R_t = r_{t+1} + \gamma \; r_{t+2} + \gamma^2 \; r_{t+3} + ... + \gamma^{T-t+1} \; r_T = \sum_{k = 0}^{\inf} \gamma^k \; r_{t+k+1}$$ Pour maximiser nos gains cumulés pondérés, on doit trouver la stratégie (ou politique) $\pi$.
    Définition : La politique ou stratégie $\pi_t$ de l'agent à l'instant $t$ est une application de $S$ dans A qui définit le comportement de l'apprenant (mapping entre situation et action). La politique ou stratégie optimale $\pi^*$ pour un MDP donné est une politique qui maximise le gain.
    Pour trouver une bonne politique, il faut disposer d’une fonction d'évaluation locale du gain à long terme espéré : $$V^\pi(s) = \mathbb{E}^\pi [\sum_{k=0}^\infty \gamma^k r_{t+k+1} | s_t = s]$$ où $V^\pi(s)$ peut être défini comme le gain moyen espéré en partant de $s$ et en suivant la politique $\pi$. Elle est appelée fonction de valeur. Le but est donc de maximiser cette fonction de valeur.
    Principe : Principe d'optimalité de Bellman (1955) : une sous-trajectoire d'une trajectoire optimale est elle-même optimale pour la fonction d'objectif restreinte aux trajectoires ayant pour origine celle de cette sous-trajectoire. Ce principe permet une méthode de résolution ascendante, qui détermine une solution optimale pour un problème à partir des solutions de tous les sous-problèmes.
    Selon le principe d'optimalité ci-dessus, nous pouvons réécrire la fonction de valeur optimale : $$V^*(s)=\max _{a \in A}\left(R(s, a) + \gamma \; V^*\left(s^{\prime}\right)\right)\tag{1}$$ c'est-à-dire que dans l'état $s$, on trouve l'action $a$ qui maximise le gain courant et les futurs gains dans $s^{\prime}$ (qui eux aussi sont optimaux). On en extrait la politique optimale $\pi^*$ : $$\pi^*(s)=\text{arg max} _{a}\left(R(s, a) + \gamma \; V^*\left(s^{\prime}\right)\right)\tag{2}$$ Seulement, la réalité n'est pas un monde parfait : elle est chaotique et complexe. Lorsqu'un robot prend une action $a$ dans l'état $s$, qui peut garantir qu'il arrivera réellement dans un état $s^\prime$ bien déterminé ? Cependant, on peut donner un degré de confiance avec lequel on pense que le robot arrivera dans $s^\prime$. Nous définissons une matrice de transition $P\left(s, a, s^{\prime}\right)$ qui dicte la probabilité d'atterrir dans l'état $s^\prime$ après avoir pris l'action $a$ dans l'état $s$. La fonction de valeur optimale devient : $$ V^*(s)=\max _{a \in A}\left(R(s, a) + \gamma \sum_{s^{\prime}} P\left(s, a, s^{\prime}\right) V^*\left(s^{\prime}\right)\right) \tag{3} $$
    Propriété : Propriété de Markov : la distribution conditionnelle de probabilité des états futurs, étant donnés les états passés et l'état présent, ne dépend en fait que de l'état présent et non pas des états passés (absence de « mémoire »). Un processus qui possède cette propriété est appelé processus de Markov. Nous voyons effectivement qu'aucune information passée n'intervient dans l'équation 3.

    Programmation dynamique : Itération de la valeur

    Pour trouver cette fonction de valeur optimale, et donc la politique optimale, nous allons utiliser l'algorithme d'itération de la valeur :
    Algorithme d'itération de la valeur
    
    Initialiser $V_0$ $n \leftarrow 0$ Tant que $||V_{n+1} - V|| > \epsilon $ faire Pour $s \in S$ faire $V_{n+1}(s)=\max _{a}\left(R(s, a) + \gamma \sum_{s^{\prime}} P\left(s, a, s^{\prime}\right) V_{n}\left(s^{\prime}\right)\right)$ Fin pour $n \leftarrow n+1$ Fin tant que Pour $s \in S$ faire $\pi^*(s)=\text{arg max} _{a}\left(R(s, a) + \gamma \; V^*\left(s^{\prime}\right)\right)$ Fin pour Retourner $V_n, \pi$
    L'algorithme itère l'équation (3) jusqu'à convergence, c'est-à-dire quand plus aucune mise à jour de fonction de la valeur n'est supérieure au seuil $\epsilon$.
    Exercice : Nous allons résoudre l'exercice classique du Gridworld. L'environnement se repésente comme suit :

    Un déplacement engendre une reward de -1 et arriver dans un état terminal, en gris, donne une reward de 0 et termine l'épisode. Pour maximiser sa récompense, il faut donc arriver au plus vite dans un état terminal.
    In [1]:
    #!!!credit:
    # >> gridworld taken from https://github.com/dennybritz <<
    !wget https://jbdel.github.io/teaching/nn/ressources/rl/lib/envs/gridworld.py
    
    --2019-08-14 07:54:32--  https://jbdel.github.io/teaching  /nn/ressources/rl/lib/envs/gridworld.py
    Resolving jbdel.github.io (jbdel.github.io)... 185.199.111.153, 185.199.109.153, 185.199.110.153, ...
    Connecting to jbdel.github.io (jbdel.github.io)|185.199.111.153|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 3850 (3.8K) [application/octet-stream]
    Saving to: ‘gridworld.py’
    
    gridworld.py        100%[===================>]   3.76K  --.-KB/s    in 0s
    
    2019-08-14 07:54:32 (97.2 MB/s) - ‘gridworld.py’ saved [3850/3850]
    
    
    In [2]:
    import numpy as np
    import pprint
    import sys
    from gridworld import GridworldEnv
    
    
    pp = pprint.PrettyPrinter(indent=2)
    env = GridworldEnv()
    env._render()
    
    T  o  o  o
    o  o  o  o
    o  o  o  o
    x  o  o  T
    
    Le grid contient deux états terminaux : un à la case 0, et un à la case 15. Le "x" indique la position de l'agent, mais ne sera pas utilisé pour cet exercice.
    Le grid est codé de telle manière que l'on reçoit une récompense de 0 si on arrive dans un terminal et qu'on y réalise n'importe quelle action. L'épisode est alors terminé. Chaque autre action donne une récompense de -1. Le gridworld dispose d'une matrice $P$ qui prend en entrée un état $s$ (une case du grid) et une action $a$ et retourne les différentes valeurs associées à cet état-action.
    In [7]:
    # P[s][a] = (prob, next_state, reward, is_done)
    # a = 0 -> up
    # a = 1 -> right
    # a = 2 -> down
    # a = 3 -> left
    
    print(env.P[0][2])
    print(env.P[13][0])
    
    [(1.0, 0, 0.0, True)]
    [(1.0, 9, -1.0, False)]
    
    Ici, env.P[13][0] comprend les valeurs de l'état-action suivant : "être à la case 13 et aller vers le haut". La reward est de -1.0, la probabilité de passer dans la case 9 est de 1 et l'épisode n'est pas terminé (is_done vaut "False"). De même pour env.P[0][2], l'état-action est le suivant : "être dans l'état terminal et aller vers le bas". La reward est de 0.0 et l'épisode est terminé (is_done vaut "True"). En vous basant sur l'algorithme d'itération de valeur, complétez le code ci-dessous. Vous devriez obtenir exactement les mêmes valeurs que celles imprimées.
    In [0]:
    def value_iteration(env, theta=0.0001, discount_factor=0.99):
        """
          env.P[s][a] la liste des transition (prob, next_state, reward, done)
          env.nS nombre d'états dans l'environnement
          env.nA nombre d'actions dans l'environnement
          theta: facteur d'arrêt
          discount_factor: le facteur de pondération de la récompense
            
        Retourne:
            Un tuple (policy, V) la politique optimale pi* et la fonction de 
            valeur optimale v*
        """
    
        def chercher_s_prime(state, V):
            A = np.zeros(env.nA)
            #appliquer ici r(s,a) + sum (discount_factor * prob * V(s'))
            for a in range(env.nA):
                for prob, next_state, reward, done in env.P[state][a]:
                    A[a] += ######## A COMPLETER ###########
            return A
    
        V = np.zeros(env.nS)
        while True:
            # Condition d'arrêt
            delta = 0
            # MAJ de la fonction de valeur pour chaque etat s
            for s in range(env.nS):
                # on va chercher la valeur de s_prime pour chaque action 
                A = chercher_s_prime(s, V)
                best_action_value = np.max(A)
                # est-ce que une MAJ d'état est au dessus du delta
                delta = max(delta, np.abs(best_action_value - V[s]))
                # MAJ de la fonction de valeur de l'état courant 
                V[s] = ######## A COMPLETER ###########        
            # condition d'arrêt 
            if delta < theta:
                break
    
        # Extraction de la police optimale
        policy = np.zeros([env.nS, env.nA])
        for s in range(env.nS):
            # on regarde les s_prime pour trouver quelle action est la meilleure        
            A = chercher_s_prime(s, V)
            best_action = np.argmax(A)
            policy[s, best_action] = 1.0
    
        return policy, V
    
    In [9]:
    policy, v = value_iteration(env)
    
    print("Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):")
    print(np.reshape(np.argmax(policy, axis=1), env.shape))
    print("")
    
    print("Value Function:")
    print(v)
    print("")
    
    print("Reshaped Grid Value Function:")
    print(v.reshape(env.shape))
    print("")
    
    
    # Test the value function
    expected_v = np.array([ 0, -1, -2, -3, -1, -2, -3, -2, -2, -3, -2, -1, -3, -2, -1,  0])
    np.testing.assert_array_almost_equal(v, expected_v, decimal=1)
    
    Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):
    [[0 3 3 2]
     [0 0 0 2]
     [0 0 1 2]
     [0 1 1 0]]
    
    Value Function:
    [ 0.     -1.     -1.99   -2.9701 -1.     -1.99   -2.9701 -1.99   -1.99
     -2.9701 -1.99   -1.     -2.9701 -1.99   -1.      0.    ]
    
    Reshaped Grid Value Function:
    [[ 0.     -1.     -1.99   -2.9701]
     [-1.     -1.99   -2.9701 -1.99  ]
     [-1.99   -2.9701 -1.99   -1.    ]
     [-2.9701 -1.99   -1.      0.    ]]
    
    
    Cet algorithme itératif n'est possible que lorsque l'environnement est connu. Ici, la fonction de transition et de récompense vous était directement accessible. Nous ne pouvons pas encore proprement parler d'apprentissage par renforcement où l'agent apprend par essai-erreur. D'ailleurs, vous n'avez pas utilisé l'agent dans l'exercice du labyrinthe pour trouver la politique optimale : vous avez simplement itéré sur les états.

    Algorithme model-free : Q-learning

    Nous comprenons que la fonction $V^*$ donne la valeur optimale d'un état selon la politique $\pi^*$. Cette valeur découle d'une agrégation des retours de récompenses sur chaque possibilité d'actions dans l'état $s$. Un des sous-problèmes est donc de se préoccuper de la valeur d'une action particulière dans un état. Pour ce faire, on introduit la fonction action-état $Q$ : $$Q^\pi(s, a) = \mathbb{E}^\pi \{\sum_{k=0}^\infty \gamma^k r_{t+k+1} | s_t = s, a_t=a\}$$ qui correspond au retour espéré pour l'état $s$ en prenant l'action $a$, puis en suivant la politique $\pi$. Selon le principe d'optimalité, on peut réécrire $Q$ de cette manière : $$Q(s, a)= R(s, a) + \gamma \sum_{s^{\prime}}\left(P\left(s, a, s^{\prime}\right) V\left(s^{\prime}\right)\right) \tag{4} $$ L'équation (4) est simplement l'équation (3) pour un $a$ explicite (on enlève donc juste le $\max$). Avec l'équation (3) et (4), on peut réécrire $V^{*}$ : $$V^{*}(s)= \max_{a \in A} Q^*(s,a) \tag{5}$$ Et remplacer directement $V^*$ dans $Q^*(s, a)$: $$Q^*(s, a)=R(s, a) + \gamma \sum_{s^{\prime}}\left(P\left(s, a, s^{\prime}\right) \max _{a^{\prime}} Q^*\left(s^{\prime}, a^{\prime}\right)\right)\tag{6}$$ Grâce à cette dernière equation (6), l'agent dispose désormais d'une fonction où l'action est explicite. Il peut désormais expérimenter un environnement en interagissant avec celui-ci.

    Il est possible d'apprendre $Q$ par échantillonnage (traditionnellement appelée Méthode de Monte-Carlo): Problème : il faut attendre la fin de l’interaction pour améliorer la politique. En fonction de la complexité du modèle que l'on essaie d'apprendre, trouver une bonne politique peut prendre trop de temps.

    En 1988, Richard Sutton (considéré comme le père de l'apprentissage par renforcement) propose une nouvelle classe de méthodes appliquées à l'exploration "model-free" appelée "apprentissage par différence temporelle" (ou temporal difference learning). Il définit son concept comme suit :
    Suppose you wish to predict the weather for Saturday, and you have some model that predicts Saturday's weather, given the weather of each day in the week. In the standard case, you would wait until Saturday and then adjust all your models. However, when it is, for example, Friday, you should have a pretty good idea of what the weather would be on Saturday – and thus be able to change, say, Saturday's model before Saturday arrives.
    (from https://web.archive.org/web/20170808062001/http://www.incompleteideas.net/sutton/papers/sutton-88-with-erratum.pdf)

    L'intuition est la suivante : il est possible de mettre à jour un modèle prédictif en cours de route (car les prédictions proches de la dernière prédiction sont plus précises et donnent déjà un retour sur les prédictions précédentes), plutôt que d'attendre le retour ou la récompense finale avant pour mettre à jour l'entièreté du modèle. Prenez un autre exemple : un coach sportif. En début de saison, il affine la stratégie de son équipe en fonction des résultats de la saison précédente, des transferts, des conditions météorologiques, de la santé de ses joueurs, etc. Ses trois premiers matchs sont une catastrophe : son équipe essuie trois défaites. Attendra-t-il la fin de la saison pour effectuer des révisions dans sa stratégie ?

    La Q-learning est un algorithme de différence temporelle et peut se décrire comme suit : $$Q^{new}(s_{t},a_{t}) \leftarrow \underbrace{Q(s_{t},a_{t})}_{\text{old value}} + \underbrace{\alpha}_{\text{learning rate}} \cdot \overbrace{ \bigg( \underbrace{r_{t}}_{\text{récompense}} + \underbrace{\gamma}_{\text{discount factor}} \cdot \underbrace{\max_{a}Q(s_{t+1}, a)}_{\text{estimation valeur optimale}} - Q(s_{t},a_{t}) \bigg)}^{\text{difference temporelle}} $$ Voici le pseudo-code associé :
    Algorithme Q-learning
    
    Pour $n \leftarrow 0$ jusqu'à $N_{\text{tot}}$ faire $s \leftarrow $ ChoixEtat Jusqu'à FinEpisode $a \leftarrow$ ChoixAction$(s)$ $s^{\prime}, r \leftarrow$ Simuler$(s, a)$ $\delta \leftarrow r + \gamma \cdot \max_b Q(s^{\prime}, b)-Q(s,a)$ $Q(s,a) \leftarrow Q(s,a) + \alpha \cdot \delta$ $s \leftarrow s^{\prime}$ Fin Jusqu'à $n \leftarrow n+1$ Fin pour Retourner $Q_{N_{\text{tot}}}$
    Exercice : Nous allons résoudre l'exercice classique de la promenade le long de la falaise. L'environnement se représente comme suit :

    Un déplacement engendre une reward de -1 et tomber de la falaise une reward de -100. Il y a donc un chemin optimal le long de la falaise qui donne une reward de -13 et un chemin plus "safe" qui donne une reward de -17. Nous allons réaliser en Q-learning deux versions de l'exercice. Pour la première, les conditions météorologiques sont parfaites : on doit converger vers le chemin optimal. Pour la seconde, des rafales de vent sont présentes : pour chaque action, il y a 20% de chances que celles-ci soit ignorées et nous somme déplacés vers le bas (vers la falaise).
    Nous commençons par la version sans le vent :
    In [0]:
    !wget https://jbdel.github.io/teaching/nn/ressources/rl/lib/envs/cliff_walking.py
    !wget https://jbdel.github.io/teaching/nn/ressources/rl/lib/plotting.py
    
    In [0]:
    %matplotlib inline
    
    import gym
    import itertools
    import matplotlib
    import numpy as np
    import sys
    
    from collections import defaultdict
    from cliff_walking import CliffWalkingEnv
    import plotting
    
    matplotlib.style.use('ggplot')
    
    In [0]:
    env = CliffWalkingEnv()
    env.render()
    
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    x  C  C  C  C  C  C  C  C  C  C  T
    
    
    La position de l'agent est représentée par "x", la falaise par "C" et l'état terminal par "T". Vous pouvez désormais utiliser la méthode step() pour expérimenter l'environnement :
    In [0]:
    #0=up, 1=right, 2=down, 3=left
    print(env.step(0))
    env.render()
    
    print(env.step(1))
    env.render()
    
    print(env.step(1))
    env.render()
    
    print(env.step(2))
    env.render()
    
    (24, -1.0, False, {'prob': 1.0})
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    x  o  o  o  o  o  o  o  o  o  o  o
    o  C  C  C  C  C  C  C  C  C  C  T
    
    (25, -1.0, False, {'prob': 1.0})
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  x  o  o  o  o  o  o  o  o  o  o
    o  C  C  C  C  C  C  C  C  C  C  T
    
    (26, -1.0, False, {'prob': 1.0})
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  x  o  o  o  o  o  o  o  o  o
    o  C  C  C  C  C  C  C  C  C  C  T
    
    (38, -100.0, True, {'prob': 1.0})
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  C  x  C  C  C  C  C  C  C  C  T
    
    
    La fonction ci-dessous définit une politique d'exploration-exploitation pour l'agent. A chaque décision, on choisit la meilleure action avec une probabilité de $1-\epsilon$, sinon on en choisit une autre aléatoirement. Cette méthode est déjà complétée.
    In [0]:
    def make_epsilon_greedy_policy(Q, epsilon, nA):
        """
        Creates an epsilon-greedy policy based on a given Q-function and epsilon.
        
        Args:
            Q: A dictionary that maps from state -> action-values.
                Each value is a numpy array of length nA (see below)
            epsilon: The probability to select a random action. Float between 0 and 1.
            nA: Number of actions in the environment.
        
        Returns:
            A function that takes the observation as an argument and returns
            the probabilities for each action in the form of a numpy array of length nA.
        
        """
        def policy_fn(observation):
            A = np.ones(nA, dtype=float) * epsilon / nA
            best_action = np.argmax(Q[observation])
            A[best_action] += (1.0 - epsilon)
            return A
        return policy_fn
    
    Voici la classe à remplir selon l'algorithme décrit ci-dessus. Vous devriez avoir des résultats équivalent à ceux imprimés.
    In [0]:
    def q_learning(env, num_episodes, discount_factor=1.0, alpha=0.5, epsilon=0.1):
        """
        Q-Learning algorithm: Off-policy TD control. Finds the optimal greedy policy
        while following an epsilon-greedy policy
        
        Args:
            env: OpenAI environment.
            num_episodes: Number of episodes to run for.
            discount_factor: Gamma discount factor.
            alpha: TD learning rate.
            epsilon: Chance to sample a random action. Float between 0 and 1.
        
        Returns:
            A tuple (Q, episode_lengths).
            Q is the optimal action-value function, a dictionary mapping state -> action values.
            stats is an EpisodeStats object with two numpy arrays for episode_lengths and episode_rewards.
        """
    
        # The final action-value function.
        # A nested dictionary that maps state -> (action -> action-value).
        Q = defaultdict(lambda: np.zeros(env.action_space.n))
    
        # Keeps track of useful statistics
        stats = plotting.EpisodeStats(
            episode_lengths=np.zeros(num_episodes),
            episode_rewards=np.zeros(num_episodes))
    
        # The policy we're following
        policy = make_epsilon_greedy_policy(Q, epsilon, env.action_space.n)
    
        for i_episode in range(num_episodes):
            # Print out which episode we're on, useful for debugging.
            if (i_episode + 1) % 100 == 0:
                print("\rEpisode {}/{}.".format(i_episode + 1, num_episodes), end="")
                sys.stdout.flush()
    
            # Reset the environment and pick the first action
            state = env.reset()
    
            # One step in the environment
            # total_reward = 0.0
            for t in itertools.count():
    
                # Take a step
                action_probs = policy(state)
                action = np.random.choice(np.arange(len(action_probs)), p=action_probs)
                next_state, reward, done, _ = env.step(action)
    
                # Update statistics
                stats.episode_rewards[i_episode] += reward
                stats.episode_lengths[i_episode] = t
    
                # TD Update
                best_next_action = # à compléter
                td_target = # à compléter
                td_delta = td_target - Q[state][action]
                Q[state][action] += # à compléter
    
                if done:
                    break
    
                state = next_state
    
        return Q, stats
    
    In [0]:
    Q, stats = q_learning(env, 500)
    
    policy = [np.argmax(Q[s]) for s in range(env.nS)]
    print("Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):")
    print(np.reshape(policy, env.shape))
    print("")
    
    Episode 500/500.Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):
    [[3 1 1 1 1 1 1 1 1 1 2 2]
     [0 1 2 1 1 1 2 2 2 2 2 2]
     [1 1 1 1 1 1 1 1 1 1 1 2]
     [0 0 0 0 0 0 0 0 0 0 0 0]]
    
    
    Nous remarquons que l'algorithme Q-learning a convergé vers le chemin "optimal". Nous pouvons vérifier la convergence avec les graphiques suivants.
    In [0]:
    plotting.plot_episode_stats(stats)
    
    Out[0]:
    (<Figure size 360x216 with 1 Axes>, <Figure size 360x216 with 1 Axes>)
    Vous pouvez désormais ajouter du vent en utilisant l'attribut "wind=True". Nous remarquons qu'aller vers la droite, en bord de falaise, est risqué. Aller vers le haut nous garantit une sécurité.
    In [0]:
    env = CliffWalkingEnv(wind=True)
    env.reset()
    
    env.step(0)
    env.step(1)
    env.render()
    #aller en haut
    print("UP",env.P[env.s][0])
    #aller a droite
    print("RIGHT",env.P[env.s][1])
    
    o  o  o  o  o  o  o  o  o  o  o  o
    o  o  o  o  o  o  o  o  o  o  o  o
    o  x  o  o  o  o  o  o  o  o  o  o
    o  C  C  C  C  C  C  C  C  C  C  T
    
    UP [(1.0, 13, -1.0, False)]
    RIGHT [(0.8, 26, -1.0, False), (0.2, 37, -100.0, True)]
    
    Si votre algorithme est correct, vous devriez converger pour cet exercice vers le "safer path".
    In [0]:
    Q, stats = q_learning(env, 2000)
    
    policy = [np.argmax(Q[s]) for s in range(env.nS)]
    print("Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):")
    print(np.reshape(policy, env.shape))
    print("")
    
    Episode 2000/2000.Reshaped Grid Policy (0=up, 1=right, 2=down, 3=left):
    [[1 1 1 1 1 1 1 1 1 1 1 2]
     [0 1 0 1 1 0 0 0 1 1 1 2]
     [0 0 0 0 0 0 0 0 0 0 0 2]
     [0 0 0 0 0 0 0 0 0 0 0 0]]
    
    
    Propriété : Fléau de la dimension : Lorsque le nombre de dimensions augmente, le volume de l'espace croît rapidement si bien que les données se retrouvent « isolées » et deviennent éparses. Cela est problématique pour les méthodes nécessitant un nombre significatif de données pour être valides, les rendant alors peu efficaces voire inopérantes. Notre algorithme de Q-learning est concerné : échantilloner $Q(s,a)$ peut devenir une tâche insurmontable pour des environnements de grandes dimensions. Par exemple, aux échecs, $Q(s,a)$ est de taille $7\times10^{13}$ après seulement dix coups.
    Nous comprenons que si il y a un nombre restreint d'interactions, la propagation de l'information peut ne pas avoir atteint tous les états. Comment généraliser ce qui a déjà été appris dans des situations semblables lorsqu'on rencontre de nouveaux états ? Il est possible d'approximer des fonctions de valeurs avec des réseaux de neurones.