Scolariser Flappy Bird : un didacticiel d'apprentissage par renforcement
Publié: 2022-03-11Dans la programmation classique, les instructions logicielles sont explicitement créées par les programmeurs et rien n'est appris des données. En revanche, l'apprentissage automatique est un domaine de l'informatique qui utilise des méthodes statistiques pour permettre aux ordinateurs d'apprendre et d'extraire des connaissances des données sans être explicitement programmés.
Dans ce didacticiel d'apprentissage par renforcement, je vais montrer comment nous pouvons utiliser PyTorch pour enseigner à un réseau neuronal d'apprentissage par renforcement comment jouer à Flappy Bird. Mais d'abord, nous devrons couvrir un certain nombre de blocs de construction.
Les algorithmes d'apprentissage automatique peuvent être divisés en deux parties : les algorithmes d'apprentissage traditionnels et les algorithmes d'apprentissage en profondeur. Les algorithmes d'apprentissage traditionnels ont généralement beaucoup moins de paramètres apprenables que les algorithmes d'apprentissage en profondeur et ont beaucoup moins de capacité d'apprentissage.
De plus, les algorithmes d'apprentissage traditionnels ne sont pas capables d' extraire des caractéristiques : les spécialistes de l'intelligence artificielle doivent trouver une bonne représentation des données qui est ensuite envoyée à l'algorithme d'apprentissage. Les exemples de techniques traditionnelles d'apprentissage automatique incluent SVM, la forêt aléatoire, l'arbre de décision et les $k$-means, tandis que l'algorithme central de l'apprentissage en profondeur est le réseau de neurones profond .
L'entrée d'un réseau de neurones profonds peut être des images brutes, et un spécialiste de l'intelligence artificielle n'a pas besoin de trouver de représentation de données : le réseau de neurones trouve la meilleure représentation pendant le processus de formation.
De nombreuses techniques d'apprentissage en profondeur sont connues depuis très longtemps, mais les progrès récents du matériel ont rapidement stimulé la recherche et le développement en matière d'apprentissage en profondeur. Nvidia est responsable de l'expansion du domaine car ses GPU ont permis des expériences rapides d'apprentissage en profondeur.
Paramètres apprenables et hyperparamètres
Les algorithmes d'apprentissage automatique se composent de paramètres apprenables qui sont réglés dans le processus de formation et de paramètres non apprenables qui sont définis avant le processus de formation. Les paramètres définis avant l'apprentissage sont appelés hyperparamètres .
La recherche par grille est une méthode courante pour trouver les hyperparamètres optimaux. C'est une méthode de force brute : cela signifie essayer toutes les combinaisons possibles d'hyperparamètres sur une plage définie et choisir la combinaison qui maximise une métrique prédéfinie.
Algorithmes d'apprentissage supervisé, non supervisé et par renforcement
Une façon de classer les algorithmes d'apprentissage consiste à tracer une ligne entre les algorithmes supervisés et non supervisés. (Mais ce n'est pas nécessairement aussi simple : l'apprentissage par renforcement se situe quelque part entre ces deux types.)
Lorsque nous parlons d'apprentissage supervisé, nous regardons les paires $ (x_i, y_i) $. $ x_i $ est l'entrée de l'algorithme et $ y_i $ est la sortie. Notre tâche est de trouver une fonction qui fera le mappage correct de $ x_i $ à $ y_i $.
Afin d'ajuster les paramètres apprenables afin qu'ils définissent une fonction qui mappe $ x_i $ à $ y_i $, une fonction de perte et un optimiseur doivent être définis. Un optimiseur minimise la fonction de perte. Un exemple de fonction de perte est l'erreur quadratique moyenne (MSE) :
\[MSE = \sum_{i=1}^{n} (y_i - \widehat{y_i} )^2\]Ici, $ y_i $ est une étiquette de vérité terrain et $ \widehat{y_i} $ est une étiquette prédite. Un optimiseur très populaire dans l'apprentissage en profondeur est la descente de gradient stochastique . Il existe de nombreuses variantes qui tentent d'améliorer la méthode de descente de gradient stochastique : Adam, Adadelta, Adagrad, etc.
Les algorithmes non supervisés essaient de trouver une structure dans les données sans être explicitement pourvus d'étiquettes. $k$-means est l'un des exemples d'algorithmes non supervisés qui tentent de trouver des clusters optimaux dans les données. Ci-dessous, une image avec 300 points de données. Les algorithmes $k$-means ont trouvé la structure dans les données et ont attribué une étiquette de cluster à chaque point de données. Chaque grappe a sa propre couleur.
L'apprentissage par renforcement utilise des récompenses : des étiquettes clairsemées et différées dans le temps. Un agent agit, ce qui modifie l'environnement, à partir duquel il peut obtenir une nouvelle observation et une nouvelle récompense. Une observation est le stimulus qu'un agent perçoit de l'environnement. Il peut s'agir de ce que l'agent voit, entend, sent, etc.
Une récompense est donnée à l'agent lorsqu'il entreprend une action. Il indique à l'agent à quel point l'action est bonne. En percevant des observations et des récompenses, un agent apprend à se comporter de manière optimale dans l'environnement. J'aborderai cela plus en détail ci-dessous.
Apprentissage par renforcement actif, passif et inverse
Il existe différentes approches de cette technique. Tout d'abord, il y a l'apprentissage par renforcement actif, que nous utilisons ici. En revanche, il y a l'apprentissage par renforcement passif, où les récompenses ne sont qu'un autre type d'observation, et les décisions sont plutôt prises selon une politique fixe.
Enfin, l'apprentissage par renforcement inverse tente de reconstruire une fonction de récompense compte tenu de l'historique des actions et de leurs récompenses dans divers états.
Généralisation, sur-ajustement et sous-ajustement
Toute instance fixe de paramètres et d'hyperparamètres est appelée un modèle. Les expériences d'apprentissage automatique se composent généralement de deux parties : la formation et les tests.
Pendant le processus de formation, les paramètres apprenables sont réglés à l'aide des données de formation. Dans le processus de test, les paramètres apprenables sont gelés et la tâche consiste à vérifier dans quelle mesure le modèle fait des prédictions sur des données inédites. La généralisation est la capacité d'une machine d'apprentissage à exécuter avec précision un nouvel exemple ou une tâche inédite après avoir expérimenté un ensemble de données d'apprentissage.
Si un modèle est trop simple par rapport aux données, il ne pourra pas s'adapter aux données d'apprentissage et il fonctionnera mal à la fois sur l'ensemble de données d'apprentissage et sur l'ensemble de données de test. Dans ce cas, on dit que le modèle est sous- ajusté .
Si un modèle d'apprentissage automatique fonctionne bien sur un ensemble de données d'entraînement, mais mal sur un ensemble de données de test, nous disons qu'il est surajusté . Le surajustement est la situation où un modèle est trop complexe par rapport aux données. Il peut parfaitement s'adapter aux données de formation, mais il est tellement adapté à l'ensemble de données de formation qu'il fonctionne mal sur les données de test, c'est-à-dire qu'il ne généralise tout simplement pas.
Ci-dessous, une image montrant le sous-ajustement et le surajustement par rapport à une situation équilibrée entre les données globales et la fonction de prédiction.
Évolutivité
Les données sont cruciales dans la construction de modèles d'apprentissage automatique. Habituellement, les algorithmes d'apprentissage traditionnels ne nécessitent pas trop de données. Mais en raison de leur capacité limitée, les performances sont également limitées. Vous trouverez ci-dessous un graphique montrant comment les méthodes d'apprentissage en profondeur évoluent bien par rapport aux algorithmes d'apprentissage automatique traditionnels.
Les réseaux de neurones
Les réseaux de neurones sont constitués de plusieurs couches. L'image ci-dessous montre un réseau de neurones simple à quatre couches. La première couche est la couche d'entrée et la dernière couche est la couche de sortie. Les deux couches entre les couches d'entrée et de sortie sont des couches masquées.
Si un réseau neuronal a plus d'une couche cachée, nous l'appelons un réseau neuronal profond. L'ensemble d'entrée $ X $ est donné au réseau de neurones et la sortie $ y $ est obtenue. L'apprentissage se fait à l'aide d'un algorithme de rétropropagation qui combine une fonction de perte et un optimiseur.
La rétropropagation se compose de deux parties : une passe avant et une passe arrière. Dans le passage vers l'avant, les données d'entrée sont placées sur l'entrée du réseau neuronal et la sortie est obtenue. La perte entre la vérité terrain et la prédiction est calculée, puis dans la passe arrière, les paramètres des réseaux de neurones sont réglés par rapport à la perte.
Réseau de neurones convolutifs
Une variante du réseau neuronal est le réseau neuronal convolutif . Il est principalement utilisé pour les tâches de vision par ordinateur.
La couche la plus importante dans les réseaux de neurones convolutifs est la couche convolutive (d'où son nom). Ses paramètres sont constitués de filtres apprenants, également appelés noyaux. Les couches convolutives appliquent une opération de convolution à l'entrée, transmettant le résultat à la couche suivante. L'opération de convolution réduit le nombre de paramètres apprenables, fonctionnant comme une sorte d'heuristique et facilitant l'apprentissage du réseau de neurones.
Voici comment fonctionne un noyau convolutif dans une couche convolutive. Le noyau est appliqué à l'image et une caractéristique convoluée est obtenue.
Les couches ReLU sont utilisées pour introduire des non-linéarités dans le réseau de neurones. Les non-linéarités sont importantes car avec elles nous pouvons modéliser toutes sortes de fonctions, pas seulement linéaires, faisant du réseau de neurones un approximateur de fonction universel. Cela fait qu'une fonction ReLU est définie comme ceci :
\[ReLU = \max(0, x)\]ReLU est l'un des exemples de fonctions dites d'activation utilisées pour introduire des non-linéarités dans les réseaux de neurones. Des exemples d'autres fonctions d'activation comprennent les fonctions sigmoïdes et hyper-tangentes. ReLU est la fonction d'activation la plus populaire car il a été démontré qu'elle rend l'entraînement du réseau de neurones plus efficace par rapport aux autres fonctions d'activation.
Vous trouverez ci-dessous un tracé d'une fonction ReLU.
Comme vous pouvez le voir, cette fonction ReLU change simplement les valeurs négatives en zéros. Cela permet d'éviter le problème de gradient de fuite. Si un gradient disparaît, il n'aura pas un grand impact sur le réglage du poids du réseau neuronal.
Un réseau neuronal convolutif se compose de plusieurs couches : couches convolutives, couches ReLU et couches entièrement connectées. Des couches entièrement connectées connectent chaque neurone d'une couche à chaque neurone d'une autre couche, comme on le voit avec les deux couches cachées dans l'image au début de cette section. La dernière couche entièrement connectée mappe les sorties de la couche précédente vers, dans ce cas, les valeurs number_of_actions
.
Applications
L'apprentissage en profondeur est un succès et surpasse les algorithmes d'apprentissage automatique classiques dans plusieurs sous-domaines de l'apprentissage automatique, notamment la vision par ordinateur, la reconnaissance vocale et l'apprentissage par renforcement. Ces domaines d'apprentissage profond sont appliqués dans divers domaines du monde réel : finance, médecine, divertissement, etc.
Apprentissage par renforcement
L'apprentissage par renforcement est basé sur un agent . Un agent entreprend des actions dans un environnement et en tire des observations et des récompenses. Un agent doit être formé pour maximiser la récompense cumulée. Comme indiqué dans l'introduction, avec les algorithmes d'apprentissage automatique classiques, les ingénieurs en apprentissage automatique doivent effectuer une extraction de caractéristiques, c'est-à-dire créer de bonnes caractéristiques qui représentent bien l'environnement et qui sont introduites dans un algorithme d'apprentissage automatique.
À l'aide de l'apprentissage en profondeur, il est possible de créer un système de bout en bout qui prend des entrées de grande dimension, par exemple une vidéo, et à partir de là, il apprend la stratégie optimale pour qu'un agent entreprenne de bonnes actions.
En 2013, la startup londonienne d'IA DeepMind a créé une grande percée dans l'apprentissage du contrôle des agents directement à partir d'entrées sensorielles de haute dimension. Ils ont publié un article, Playing Atari with Deep Reinforcement Learning , dans lequel ils ont montré comment ils ont appris à un réseau de neurones artificiels à jouer à des jeux Atari simplement en regardant l'écran. Ils ont été rachetés par Google, puis ont publié un nouvel article dans Nature avec quelques améliorations : Contrôle au niveau humain grâce à l'apprentissage par renforcement profond .
Contrairement à d'autres paradigmes d'apprentissage automatique, l'apprentissage par renforcement n'a pas de superviseur, seulement un signal de récompense. La rétroaction est retardée : elle n'est pas instantanée comme dans les algorithmes d'apprentissage supervisé. Les données sont séquentielles et les actions d'un agent affectent les données ultérieures qu'il reçoit.
Or, un agent est situé dans son environnement, qui est dans un certain état. Pour décrire cela plus en détail, nous utilisons un processus de décision de Markov, qui est une manière formelle de modéliser cet environnement d'apprentissage par renforcement. Il est composé d'un ensemble d'états, d'un ensemble d'actions possibles et de règles (par exemple, des probabilités) pour passer d'un état à un autre.
L'agent est capable d'effectuer des actions, transformant l'environnement. Nous appelons la récompense $ R_t $. Il s'agit d'un signal de rétroaction scalaire indiquant la performance de l'agent à l'étape $t$.
Pour une bonne performance à long terme, non seulement les récompenses immédiates mais aussi les récompenses futures doivent être prises en compte. La récompense totale pour un épisode à partir du pas de temps $t$ est de $ R_t = r_t + r_{t+1} + r_{t+2} + \ldots + r_n $. L'avenir est incertain et plus nous allons loin dans le futur, plus les prédictions futures peuvent diverger. Pour cette raison, une récompense future actualisée est utilisée : $ R_t = r_t +\gamma r_{t+1} + \gamma^2r_{t+2} + \ldots + \gamma^{nt}r_n = r_t + \gamma R_{t+1} $. Un agent doit choisir l'action qui maximise la récompense future escomptée.
Apprentissage Q approfondi
La fonction $ Q(s, a) $ représente la récompense future actualisée maximale lorsque l'action $ a $ est effectuée
L'estimation d'une récompense future est donnée par l'équation de Bellman : $ Q(s, a) = r + \gamma \max_{a'}Q(s', a') $ . En d'autres termes, la récompense future maximale compte tenu d'un état $ s $ et d'une action $ a $ est la récompense immédiate plus la récompense future maximale pour l'état suivant.

L'approximation des valeurs Q à l'aide de fonctions non linéaires (réseaux de neurones) n'est pas très stable. Pour cette raison, la relecture de l'expérience est utilisée pour la stabilité. L'expérience pendant les épisodes d'une séance d'entraînement est stockée dans une mémoire de répétition. Des mini-lots aléatoires de la mémoire de relecture sont utilisés au lieu d'utiliser la transition la plus récente. Cela rompt la similitude des échantillons d'apprentissage ultérieurs qui, autrement, conduiraient le réseau de neurones à un minimum local.
Il y a deux autres aspects importants à mentionner à propos du deep Q-learning : l'exploration et l'exploitation. Avec l'exploitation, la meilleure décision compte tenu des informations actuelles est prise. L'exploration recueille plus d'informations.
Lorsque l'algorithme effectue une action proposée par le réseau de neurones, il fait de l'exploitation : Il exploite les connaissances apprises du réseau de neurones. En revanche, un algorithme peut prendre une action aléatoire, explorer de nouvelles possibilités et introduire de nouvelles connaissances potentielles dans le réseau neuronal.
L'algorithme "Deep Q-learning with Experience Replay" de l'article de DeepMind Playing Atari with Deep Reinforcement Learning est présenté ci-dessous.
DeepMind fait référence aux réseaux convolutifs formés avec leur approche en tant que réseaux Q profonds (DQN).
Exemple de Q-learning approfondi avec Flappy Bird
Flappy Bird était un jeu mobile populaire développé à l'origine par l'artiste et programmeur de jeux vidéo vietnamien Dong Nguyen. Dans celui-ci, le joueur contrôle un oiseau et essaie de voler entre des tuyaux verts sans les toucher.
Ci-dessous, une capture d'écran d'un clone de Flappy Bird codé avec PyGame :
Le clone a depuis été bifurqué et modifié : l'arrière-plan, les sons et les différents styles d'oiseaux et de tuyaux ont été supprimés et le code a été ajusté afin qu'il puisse être facilement utilisé avec des cadres d'apprentissage par renforcement simples. Le moteur de jeu modifié est tiré de ce projet TensorFlow :
Mais au lieu d'utiliser TensorFlow, j'ai construit un cadre d'apprentissage par renforcement profond à l'aide de PyTorch. PyTorch est un cadre d'apprentissage en profondeur pour une expérimentation rapide et flexible. Il fournit des tenseurs et des réseaux de neurones dynamiques en Python avec une forte accélération GPU.
L'architecture du réseau neuronal est la même que celle de DeepMind utilisée dans l'article Contrôle au niveau humain grâce à l'apprentissage par renforcement profond .
Couche | Contribution | Taille du filtre | Foulée | Nombre de filtres | Activation | Sortir |
---|---|---|---|---|---|---|
conv1 | 84x84x4 | 8x8 | 4 | 32 | ReLU | 20x20x32 |
conv2 | 20x20x32 | 4x4 | 2 | 64 | ReLU | 9x9x64 |
conv3 | 9x9x64 | 3x3 | 1 | 64 | ReLU | 7x7x64 |
fc4 | 7x7x64 | 512 | ReLU | 512 | ||
fc5 | 512 | 2 | Linéaire | 2 |
Il y a trois couches convolutionnelles et deux couches entièrement connectées. Chaque couche utilise l'activation ReLU, sauf la dernière, qui utilise l'activation linéaire. Le réseau de neurones génère deux valeurs représentant les seules actions possibles du joueur : "Voler vers le haut" et "Ne rien faire".
L'entrée consiste en quatre images consécutives en noir et blanc 84x84. Vous trouverez ci-dessous un exemple de quatre images qui sont transmises au réseau de neurones.
Vous remarquerez que les images sont pivotées. C'est parce que la sortie du moteur de jeu du clone est tournée. Mais si le réseau de neurones est enseigné puis testé à l'aide de telles images, cela n'affectera pas ses performances.
Vous pouvez également remarquer que l'image est recadrée de sorte que le sol est omis, car il n'est pas pertinent pour cette tâche. Tous les pixels qui représentent les tuyaux et l'oiseau sont blancs et tous les pixels qui représentent l'arrière-plan sont noirs.
Cela fait partie du code qui définit le réseau de neurones. Les poids des réseaux de neurones sont initialisés pour suivre la distribution uniforme $\mathcal{U}(-0.01, 0.01)$. La partie biais des paramètres des réseaux de neurones est fixée à 0,01. Plusieurs initialisations différentes ont été essayées (Xavier uniforme, Xavier normal, Kaiming uniforme, Kaiming normal, uniforme et normal), mais l'initialisation ci-dessus a fait converger le réseau neuronal et s'entraîner le plus rapidement. La taille du réseau neuronal est de 6,8 Mo.
class NeuralNetwork(nn.Module): def __init__(self): super(NeuralNetwork, self).__init__() self.number_of_actions = 2 self.gamma = 0.99 self.final_epsilon = 0.0001 self.initial_epsilon = 0.1 self.number_of_iterations = 2000000 self.replay_memory_size = 10000 self.minibatch_size = 32 self.conv1 = nn.Conv2d(4, 32, 8, 4) self.relu1 = nn.ReLU(inplace=True) self.conv2 = nn.Conv2d(32, 64, 4, 2) self.relu2 = nn.ReLU(inplace=True) self.conv3 = nn.Conv2d(64, 64, 3, 1) self.relu3 = nn.ReLU(inplace=True) self.fc4 = nn.Linear(3136, 512) self.relu4 = nn.ReLU(inplace=True) self.fc5 = nn.Linear(512, self.number_of_actions) def forward(self, x): out = self.conv1(x) out = self.relu1(out) out = self.conv2(out) out = self.relu2(out) out = self.conv3(out) out = self.relu3(out) out = out.view(out.size()[0], -1) out = self.fc4(out) out = self.relu4(out) out = self.fc5(out) return out
Dans le constructeur, vous remarquerez qu'il y a des hyperparamètres définis. L'optimisation des hyperparamètres n'est pas effectuée dans le cadre de cet article de blog. Au lieu de cela, les hyperparamètres sont principalement utilisés à partir des articles de DeepMind. Ici, certains des hyperparamètres sont mis à l'échelle pour être plus bas que dans l'article de DeepMind, car Flappy Bird est moins complexe que les jeux Atari qu'ils ont utilisés pour le réglage.
De plus, epsilon est modifié pour être beaucoup plus raisonnable pour ce jeu. DeepMind utilise un epsilon de un, mais ici nous utilisons 0,1. En effet, des epsilons plus élevés forcent l'oiseau à battre beaucoup, ce qui pousse l'oiseau vers le bord supérieur de l'écran, ce qui finit toujours par l'écraser dans un tuyau.
Le code d'apprentissage par renforcement a deux modes : Entraînement et test. Pendant la phase de test, nous pouvons voir à quel point l'algorithme d'apprentissage par renforcement a appris à jouer le jeu. Mais d'abord, le réseau de neurones doit être formé. Nous devons définir la fonction de perte à minimiser et les optimiseurs qui minimiseront la fonction de perte. Nous utiliserons la méthode d'optimisation d' Adam et l'erreur quadratique moyenne pour la fonction de perte :
optimizer = optim.Adam(model.parameters(), lr=1e-6) criterion = nn.MSELoss()
Le jeu doit être instancié :
game_state = GameState()
La mémoire de relecture est définie comme une liste Python :
replay_memory = []
Maintenant, nous devons initialiser le premier état. Une action est un tenseur bidimensionnel :
- [1, 0] représente "ne rien faire"
- [0, 1] représente "voler vers le haut"
La méthode frame_step
nous donne l'écran suivant, la récompense et des informations indiquant si l'état suivant est terminal. La récompense est de 0.1
pour le mouvement de chaque oiseau sans mourir lorsqu'il ne passe pas par un tuyau, 1
si l'oiseau passe avec succès à travers un tuyau et -1
si l'oiseau s'écrase.
La fonction resize_and_bgr2gray
recadre le sol, redimensionne l'écran en une image 84x84 et modifie l'espace colorimétrique de BGR en noir et blanc. La fonction image_to_tensor
convertit l'image en un tenseur PyTorch et la place dans la mémoire GPU si CUDA est disponible. Enfin, les quatre derniers écrans séquentiels sont concaténés et sont prêts à être envoyés au réseau de neurones.
action = torch.zeros([model.number_of_actions], dtype=torch.float32) action[0] = 1 image_data, reward, terminal = game_state.frame_step(action) image_data = resize_and_bgr2gray(image_data) image_data = image_to_tensor(image_data) state = torch.cat((image_data, image_data, image_data, image_data)).unsqueeze(0)
L'epsilon initial est défini à l'aide de cette ligne de code :
epsilon = model.initial_epsilon
La boucle infinie principale suit. Les commentaires sont écrits dans le code et vous pouvez comparer le code avec l'algorithme Deep Q-learning with Experience Replay écrit ci-dessus.
L'algorithme échantillonne des mini-lots à partir de la mémoire de relecture et met à jour les paramètres du réseau neuronal. Les actions sont exécutées à l'aide de l'exploration gourmande epsilon . Epsilon est recuit au fil du temps. La fonction de perte minimisée est $ L = \frac{1}{2}\left[\max_{a'}Q(s', a') - Q(s, a)\right]^2 $ . $ Q(s, a) $ est la valeur de vérité terrain calculée à l'aide de l'équation de Bellman et $ \max_{a'}Q(s', a') $ est obtenue à partir du réseau de neurones. Le réseau neuronal donne deux valeurs Q pour les deux actions possibles et l'algorithme prend l'action avec la valeur Q la plus élevée.
while iteration < model.number_of_iterations: # get output from the neural network output = model(state)[0] # initialize action action = torch.zeros([model.number_of_actions], dtype=torch.float32) if torch.cuda.is_available(): # put on GPU if CUDA is available action = action.cuda() # epsilon greedy exploration random_action = random.random() <= epsilon if random_action: print("Performed random action!") action_index = [torch.randint(model.number_of_actions, torch.Size([]), dtype=torch.int) if random_action else torch.argmax(output)][0] if torch.cuda.is_available(): # put on GPU if CUDA is available action_index = action_index.cuda() action[action_index] = 1 # get next state and reward image_data_1, reward, terminal = game_state.frame_step(action) image_data_1 = resize_and_bgr2gray(image_data_1) image_data_1 = image_to_tensor(image_data_1) state_1 = torch.cat((state.squeeze(0)[1:, :, :], image_data_1)).unsqueeze(0) action = action.unsqueeze(0) reward = torch.from_numpy(np.array([reward], dtype=np.float32)).unsqueeze(0) # save transition to replay memory replay_memory.append((state, action, reward, state_1, terminal)) # if replay memory is full, remove the oldest transition if len(replay_memory) > model.replay_memory_size: replay_memory.pop(0) # epsilon annealing epsilon = epsilon_decrements[iteration] # sample random minibatch minibatch = random.sample(replay_memory, min(len(replay_memory), model.minibatch_size)) # unpack minibatch state_batch = torch.cat(tuple(d[0] for d in minibatch)) action_batch = torch.cat(tuple(d[1] for d in minibatch)) reward_batch = torch.cat(tuple(d[2] for d in minibatch)) state_1_batch = torch.cat(tuple(d[3] for d in minibatch)) if torch.cuda.is_available(): # put on GPU if CUDA is available state_batch = state_batch.cuda() action_batch = action_batch.cuda() reward_batch = reward_batch.cuda() state_1_batch = state_1_batch.cuda() # get output for the next state output_1_batch = model(state_1_batch) # set y_j to r_j for terminal state, otherwise to r_j + gamma*max(Q) y_batch = torch.cat(tuple(reward_batch[i] if minibatch[i][4] else reward_batch[i] + model.gamma * torch.max(output_1_batch[i]) for i in range(len(minibatch)))) # extract Q-value q_value = torch.sum(model(state_batch) * action_batch, dim=1) # PyTorch accumulates gradients by default, so they need to be reset in each pass optimizer.zero_grad() # returns a new Tensor, detached from the current graph, the result will never require gradient y_batch = y_batch.detach() # calculate loss loss = criterion(q_value, y_batch) # do backward pass loss.backward() optimizer.step() # set state to be state_1 state = state_1
Maintenant que tous les éléments sont en place, voici un aperçu de haut niveau du flux de données utilisant notre réseau de neurones :
Voici une courte séquence avec un réseau neuronal formé.
Le réseau de neurones illustré ci-dessus a été formé à l'aide d'un GPU Nvidia GTX 1080 haut de gamme pendant quelques heures ; en utilisant plutôt une solution basée sur le processeur, cette tâche particulière prendrait plusieurs jours. Les FPS du moteur de jeu ont été réglés sur un très grand nombre pendant l'entraînement : 999… 999, en d'autres termes, autant d'images par seconde que possible. En phase de test, le FPS était fixé à 30.
Vous trouverez ci-dessous un graphique montrant comment la valeur Q maximale a changé au cours des itérations. Chaque 10 000e itération est affichée. Un pic vers le bas signifie que pour une image particulière (une itération étant une image), le réseau de neurones prédit que l'oiseau obtiendra une très faible récompense à l'avenir, c'est-à-dire qu'il s'écrasera très bientôt.
Le code complet et le modèle pré-entraîné sont disponibles ici.
Apprentissage par renforcement profond : 2D, 3D et même dans la vraie vie
Dans ce didacticiel d'apprentissage par renforcement PyTorch, j'ai montré comment un ordinateur peut apprendre à jouer à Flappy Bird sans aucune connaissance préalable du jeu, en utilisant uniquement une approche par essais et erreurs comme le ferait un humain lorsqu'il rencontre le jeu pour la première fois.
Il est intéressant de noter que l'algorithme peut être implémenté en quelques lignes de code en utilisant le framework PyTorch. L'article sur lequel la méthode de ce blog est basée est relativement ancien, et de nombreux articles plus récents avec diverses modifications permettant une convergence plus rapide sont disponibles. Depuis lors, l'apprentissage par renforcement profond a été utilisé pour jouer à des jeux 3D et dans des systèmes robotiques du monde réel.
Des entreprises comme DeepMind, Maluuba et Vicarious travaillent intensivement sur l'apprentissage par renforcement profond. Une telle technologie a été utilisée dans AlphaGo, qui a battu Lee Sedol, l'un des meilleurs joueurs de Go au monde. A l'époque, on considérait qu'il faudrait au moins dix ans pour que les machines puissent vaincre les meilleurs joueurs de Go.
Le flot d'intérêt et d'investissements dans l'apprentissage par renforcement profond (et dans l'intelligence artificielle en général) pourrait même conduire à une intelligence générale artificielle (IAG) potentielle - une intelligence de niveau humain (ou même au-delà) qui peut être exprimée sous la forme d'un algorithme et simulé sur ordinateur. Mais AGI devra faire l'objet d'un autre article.
Les références:
- Démystifier l'apprentissage par renforcement profond
- Apprentissage par renforcement profond pour Flappy Bird
- "Réseau de neurones convolutifs" sur Wikipedia
- "Apprentissage par renforcement" sur Wikipedia
- "Processus de décision de Markov" sur Wikipedia
- Cours de l'University College London sur le RL