Un tutoriel d'apprentissage en profondeur : des perceptrons aux réseaux profonds

Publié: 2022-03-11

Ces dernières années, il y a eu une recrudescence dans le domaine de l'Intelligence Artificielle. Il s'est propagé au-delà du monde académique avec des acteurs majeurs comme Google, Microsoft et Facebook créant leurs propres équipes de recherche et réalisant des acquisitions impressionnantes.

Cela peut être attribué en partie à l'abondance de données brutes générées par les utilisateurs de réseaux sociaux, dont une grande partie doit être analysée, à l'essor des solutions avancées de science des données, ainsi qu'à la puissance de calcul bon marché disponible via les GPGPU.

Mais au-delà de ces phénomènes, cette résurgence a été alimentée en grande partie par une nouvelle tendance de l'IA, en particulier de l'apprentissage automatique, connue sous le nom de "Deep Learning". Dans ce didacticiel, je vais vous présenter les concepts et algorithmes clés de l'apprentissage en profondeur, en commençant par l'unité de composition la plus simple et en passant par les concepts d'apprentissage automatique en Java.

(Pour une divulgation complète : je suis également l'auteur d'une bibliothèque d'apprentissage en profondeur Java, disponible ici, et les exemples de cet article sont implémentés à l'aide de la bibliothèque ci-dessus. Si vous l'aimez, vous pouvez la soutenir en lui attribuant une étoile sur GitHub . , ce dont je vous serais reconnaissant. Les instructions d'utilisation sont disponibles sur la page d'accueil.)

Un tutoriel de trente secondes sur l'apprentissage automatique

Si vous n'êtes pas familier, consultez cette introduction à l'apprentissage automatique :

La procédure générale est la suivante :

  1. Nous avons un algorithme qui donne une poignée d'exemples étiquetés, disons 10 images de chiens avec l'étiquette 1 ("Chien") et 10 images d'autres choses avec l'étiquette 0 ("Pas de chien") - notez que nous nous en tenons principalement à une classification binaire supervisée pour ce poste.
  2. L'algorithme "apprend" à identifier les images de chiens et, lorsqu'il reçoit une nouvelle image, espère produire la bonne étiquette (1 s'il s'agit d'une image de chien, et 0 sinon).

Ce paramètre est incroyablement général : vos données peuvent être des symptômes et vos étiquettes des maladies ; ou vos données pourraient être des images de caractères manuscrits et vos étiquettes les caractères réels qu'elles représentent.

Perceptrons : premiers algorithmes d'apprentissage en profondeur

L'un des premiers algorithmes d'entraînement supervisé est celui du perceptron, un élément de base du réseau neuronal.

Disons que nous avons n points dans le plan, étiquetés '0' et '1'. On nous donne un nouveau point et nous voulons deviner son étiquette (cela s'apparente au scénario « Chien » et « Pas de chien » ci-dessus). Comment faisons-nous ça?

Une approche pourrait consister à regarder le voisin le plus proche et à renvoyer l'étiquette de ce point. Mais une façon un peu plus intelligente de procéder serait de choisir une ligne qui sépare le mieux les données étiquetées et de l'utiliser comme classificateur.

Une représentation des données d'entrée par rapport à un classificateur linéaire est une approche de base de l'apprentissage en profondeur.

Dans ce cas, chaque élément de données d'entrée serait représenté par un vecteur x = ( x_1, x_2 ) et notre fonction serait quelque chose comme "'0' si en dessous de la ligne, '1' si au-dessus".

Pour représenter cela mathématiquement, définissons notre séparateur par un vecteur de poids w et un décalage vertical (ou biais) b . Ensuite, notre fonction combinerait les entrées et les poids avec une fonction de transfert de somme pondérée :

fonction de transfert de somme pondérée

Le résultat de cette fonction de transfert serait ensuite introduit dans une fonction d'activation pour produire un étiquetage. Dans l'exemple ci-dessus, notre fonction d'activation était un seuil de coupure (par exemple, 1 si supérieur à une certaine valeur) :

résultat de cette fonction de transfert

Entraînement du Perceptron

L'entraînement du perceptron consiste à l'alimenter en plusieurs échantillons d'entraînement et à calculer la sortie pour chacun d'eux. Après chaque échantillon, les poids w sont ajustés de manière à minimiser l' erreur de sortie , définie comme la différence entre la sortie souhaitée (cible) et la sortie réelle . Il existe d'autres fonctions d'erreur, comme l'erreur quadratique moyenne, mais le principe de base de l'apprentissage reste le même.

Inconvénients du Perceptron unique

L'approche perceptron unique de l'apprentissage en profondeur présente un inconvénient majeur : elle ne peut apprendre que des fonctions linéairement séparables. Quelle est la gravité de cet inconvénient ? Prenez XOR, une fonction relativement simple, et notez qu'elle ne peut pas être classée par un séparateur linéaire (notez la tentative infructueuse, ci-dessous) :

L'inconvénient de cette approche d'apprentissage en profondeur est que certaines fonctions ne peuvent pas être classées par un séparateur linéaire.

Pour résoudre ce problème, nous devrons utiliser un perceptron multicouche, également connu sous le nom de réseau de neurones à anticipation : en effet, nous allons composer un ensemble de ces perceptrons pour créer un mécanisme d'apprentissage plus puissant.

Réseaux de neurones feedforward pour l'apprentissage en profondeur

Un réseau de neurones n'est en réalité qu'une composition de perceptrons, connectés de différentes manières et fonctionnant sur différentes fonctions d'activation.

L'apprentissage en profondeur de réseau neutre par anticipation est une approche plus complexe que les perceptrons uniques.

Pour commencer, nous examinerons le réseau de neurones feedforward, qui a les propriétés suivantes :

  • Une entrée, une sortie et une ou plusieurs couches masquées . La figure ci-dessus montre un réseau avec une couche d'entrée à 3 unités, une couche cachée à 4 unités et une couche de sortie à 2 unités (les termes unités et neurones sont interchangeables).
  • Chaque unité est un perceptron unique comme celui décrit ci-dessus.
  • Les unités de la couche d'entrée servent d'entrées pour les unités de la couche cachée, tandis que les unités de la couche cachée sont des entrées de la couche de sortie.
  • Chaque connexion entre deux neurones a un poids w (similaire aux poids du perceptron).
  • Chaque unité de la couche t est généralement connectée à chaque unité de la couche précédente t - 1 (bien que vous puissiez les déconnecter en définissant leur poids sur 0).
  • Pour traiter les données d'entrée, vous "fixez" le vecteur d'entrée à la couche d'entrée, en définissant les valeurs du vecteur comme "sorties" pour chacune des unités d'entrée. Dans ce cas particulier, le réseau peut traiter un vecteur d'entrée à 3 dimensions (du fait des 3 unités d'entrée). Par exemple, si votre vecteur d'entrée est [7, 1, 2], vous définirez la sortie de l'unité d'entrée supérieure sur 7, celle du milieu sur 1, etc. Ces valeurs sont ensuite propagées vers les unités cachées à l'aide de la fonction de transfert de somme pondérée pour chaque unité cachée (d'où le terme propagation vers l'avant), qui à leur tour calculent leurs sorties (fonction d'activation).
  • La couche de sortie calcule ses sorties de la même manière que la couche cachée. Le résultat de la couche de sortie est la sortie du réseau.

Au-delà de la linéarité

Et si chacun de nos perceptrons n'était autorisé à utiliser qu'une fonction d'activation linéaire ? Ensuite, la sortie finale de notre réseau sera toujours une fonction linéaire des entrées, juste ajustée avec une tonne de poids différents collectés à travers le réseau. En d'autres termes, une composition linéaire d'un ensemble de fonctions linéaires n'est toujours qu'une fonction linéaire. Si nous sommes limités aux fonctions d'activation linéaires, alors le réseau de neurones à anticipation n'est pas plus puissant que le perceptron, quel que soit le nombre de couches dont il dispose.

Une composition linéaire d'un ensemble de fonctions linéaires n'est encore qu'une fonction linéaire, de sorte que la plupart des réseaux de neurones utilisent des fonctions d'activation non linéaires.

Pour cette raison, la plupart des réseaux de neurones utilisent des fonctions d'activation non linéaires telles que la logistique, le tanh, le binaire ou le redresseur. Sans eux, le réseau ne peut apprendre que des fonctions qui sont des combinaisons linéaires de ses entrées.

Perceptrons d'entraînement

L'algorithme d'apprentissage en profondeur le plus courant pour l'entraînement supervisé des perceptrons multicouches est connu sous le nom de rétropropagation. La procédure de base :

  1. Un échantillon d'apprentissage est présenté et propagé vers l'avant à travers le réseau.
  2. L'erreur de sortie est calculée, généralement l'erreur quadratique moyenne :

    erreur quadratique moyenne

    t est la valeur cible et y est la sortie réelle du réseau. D'autres calculs d'erreur sont également acceptables, mais le MSE est un bon choix.

  3. L'erreur de réseau est minimisée à l'aide d'une méthode appelée descente de gradient stochastique.

    Descente graduelle

    La descente de gradient est universelle, mais dans le cas des réseaux de neurones, il s'agirait d'un graphique de l'erreur d'apprentissage en fonction des paramètres d'entrée. La valeur optimale pour chaque poids est celle à laquelle l'erreur atteint un minimum global . Pendant la phase d'apprentissage, les poids sont mis à jour par petites étapes (après chaque échantillon d'apprentissage ou un mini-lot de plusieurs échantillons) de telle sorte qu'ils essaient toujours d'atteindre le minimum global - mais ce n'est pas une tâche facile, car vous aboutissent souvent à des minima locaux, comme celui de droite. Par exemple, si le poids a une valeur de 0,6, il faut le modifier vers 0,4.

    Cette figure représente le cas le plus simple, celui où l'erreur dépend d'un seul paramètre. Cependant, l'erreur de réseau dépend de chaque poids de réseau et la fonction d'erreur est beaucoup, beaucoup plus complexe.

    Heureusement, la rétropropagation fournit une méthode pour mettre à jour chaque poids entre deux neurones par rapport à l'erreur de sortie. La dérivation elle-même est assez compliquée, mais la mise à jour du poids pour un nœud donné a la forme (simple) suivante :

    exemple de formulaire

    E est l'erreur de sortie et w_i est le poids de l'entrée i du neurone.

    Essentiellement, le but est de se déplacer dans la direction du gradient par rapport au poids i . Le terme clé est bien sûr la dérivée de l'erreur, qui n'est pas toujours facile à calculer : comment trouverait-on cette dérivée pour un poids aléatoire d'un nœud caché aléatoire au milieu d'un grand réseau ?

    La réponse : par rétropropagation. Les erreurs sont d'abord calculées au niveau des unités de sortie où la formule est assez simple (basée sur la différence entre les valeurs cibles et prédites), puis propagées à travers le réseau de manière intelligente, ce qui nous permet de mettre à jour efficacement nos poids pendant l'entraînement et (espérons-le) atteindre un minimum.

Couche masquée

La couche cachée est particulièrement intéressante. Par le théorème d'approximation universel, un seul réseau de couches cachées avec un nombre fini de neurones peut être formé pour approximer une fonction arbitrairement aléatoire. En d'autres termes, une seule couche cachée est suffisamment puissante pour apprendre n'importe quelle fonction. Cela dit, nous apprenons souvent mieux dans la pratique avec plusieurs couches cachées (c'est-à-dire des filets plus profonds).

La couche cachée est l'endroit où le réseau stocke sa représentation abstraite interne des données d'apprentissage.

La couche cachée est l'endroit où le réseau stocke sa représentation abstraite interne des données d'entraînement, de la même manière qu'un cerveau humain (analogie très simplifiée) a une représentation interne du monde réel. À l'avenir dans le didacticiel, nous examinerons différentes façons de jouer avec le calque caché.

Un exemple de réseau

Vous pouvez voir un réseau de neurones à anticipation simple (couche 4-2-3) qui classe l'ensemble de données IRIS implémenté en Java ici via la méthode testMLPSigmoidBP . L'ensemble de données contient trois classes de plantes d'iris avec des caractéristiques telles que la longueur des sépales, la longueur des pétales, etc. Le réseau est fourni à 50 échantillons par classe. Les caractéristiques sont fixées aux unités d'entrée, tandis que chaque unité de sortie correspond à une seule classe du jeu de données : "1/0/0" indique que la plante est de classe Setosa, "0/1/0" indique Versicolor et " 0/0/1 » indique Virginie). L'erreur de classification est de 2/150 (c'est-à-dire qu'elle classe mal 2 échantillons sur 150).

Le problème des grands réseaux

Un réseau de neurones peut avoir plusieurs couches cachées : dans ce cas, les couches supérieures "construisent" de nouvelles abstractions au-dessus des couches précédentes. Et comme nous l'avons mentionné précédemment, vous pouvez souvent mieux apprendre dans la pratique avec des réseaux plus importants.

Cependant, l'augmentation du nombre de couches masquées entraîne deux problèmes connus :

  1. Gradients de disparition : à mesure que nous ajoutons de plus en plus de couches cachées, la rétropropagation devient de moins en moins utile pour transmettre des informations aux couches inférieures. En effet, au fur et à mesure que l'information est renvoyée, les gradients commencent à disparaître et deviennent petits par rapport aux poids des réseaux.
  2. Le surajustement : peut-être le problème central de l'apprentissage automatique. En bref, le surajustement décrit le phénomène d'ajustement trop étroit des données d'entraînement, peut-être avec des hypothèses trop complexes. Dans un tel cas, votre apprenant finira par très bien s'adapter aux données de formation, mais ses performances seront beaucoup, beaucoup plus médiocres sur des exemples réels.

Examinons quelques algorithmes d'apprentissage en profondeur pour résoudre ces problèmes.

Auto-encodeurs

La plupart des cours d'introduction à l'apprentissage automatique ont tendance à s'arrêter aux réseaux de neurones à anticipation. Mais l'espace des réseaux possibles est beaucoup plus riche, alors continuons.

Un auto-encodeur est généralement un réseau de neurones à anticipation qui vise à apprendre une représentation compressée et distribuée (encodage) d'un ensemble de données.

Un auto-encodeur est un réseau neuronal d'apprentissage en profondeur qui vise à apprendre une certaine représentation d'un ensemble de données.

Conceptuellement, le réseau est formé pour « recréer » l'entrée, c'est-à-dire que l'entrée et les données cibles sont les mêmes. En d'autres termes : vous essayez de produire la même chose que vous avez entrée, mais compressée d'une manière ou d'une autre. C'est une approche déroutante, alors regardons un exemple.

Compression de l'entrée : images en niveaux de gris

Supposons que les données d'apprentissage consistent en des images en niveaux de gris 28x28 et que la valeur de chaque pixel est fixée à un neurone de la couche d'entrée (c'est-à-dire que la couche d'entrée aura 784 neurones). Ensuite, la couche de sortie aurait le même nombre d'unités (784) que la couche d'entrée et la valeur cible pour chaque unité de sortie serait la valeur d'échelle de gris d'un pixel de l'image.

L'intuition derrière cette architecture est que le réseau n'apprendra pas un "mappage" entre les données d'apprentissage et ses étiquettes, mais apprendra plutôt la structure interne et les caractéristiques des données elles-mêmes. (Pour cette raison, la couche cachée est également appelée détecteur de caractéristiques .) Habituellement, le nombre d'unités cachées est inférieur à celui des couches d'entrée/sortie, ce qui oblige le réseau à apprendre uniquement les caractéristiques les plus importantes et permet une réduction de la dimensionnalité.

Nous voulons quelques petits nœuds au milieu pour apprendre les données à un niveau conceptuel, produisant une représentation compacte.

En effet, nous voulons que quelques petits nœuds au milieu apprennent vraiment les données à un niveau conceptuel, produisant une représentation compacte qui capture d'une certaine manière les principales caractéristiques de notre entrée.

Maladie grippale

Pour démontrer davantage les encodeurs automatiques, examinons une autre application.

Dans ce cas, nous utiliserons un ensemble de données simple composé de symptômes de la grippe (crédit à cet article de blog pour l'idée). Si cela vous intéresse, le code de cet exemple se trouve dans la méthode testAEBackpropagation .

Voici comment l'ensemble de données se décompose :

  • Il existe six fonctionnalités d'entrée binaire.
  • Les trois premiers sont des symptômes de la maladie. Par exemple, 1 0 0 0 0 0 indique que ce patient a une température élevée, tandis que 0 1 0 0 0 0 indique une toux, 1 1 0 0 0 0 indique une toux et une température élevée, etc.
  • Les trois dernières caractéristiques sont les « contre » symptômes ; lorsqu'un patient en a un, il est moins probable qu'il soit malade. Par exemple, 0 0 0 1 0 0 indique que ce patient est vacciné contre la grippe. Il est possible d'avoir des combinaisons des deux ensembles de caractéristiques : 0 1 0 1 0 0 indique un patient vacciné qui tousse, et ainsi de suite.

Nous considérerons qu'un patient est malade lorsqu'il présente au moins deux des trois premières caractéristiques et en bonne santé s'il présente au moins deux des trois secondes (les liens se brisant en faveur des patients sains), par exemple :

  • 111000, 101000, 110000, 011000, 011100 = malade
  • 000111, 001110, 000101, 000011, 000110 = sain

Nous allons former un auto-encodeur (utilisant la rétropropagation) avec six unités d'entrée et six unités de sortie, mais seulement deux unités cachées .

Après plusieurs centaines d'itérations, on observe que lorsque chacun des échantillons « malades » est présenté au réseau d'apprentissage automatique, l'une des deux unités cachées (la même unité pour chaque échantillon « malade ») présente toujours une valeur d'activation supérieure à la autre. Au contraire, lorsqu'un échantillon "sain" est présenté, l'autre unité cachée a une activation plus élevée.

Revenir à l'apprentissage automatique

Essentiellement, nos deux unités cachées ont appris une représentation compacte de l'ensemble de données sur les symptômes de la grippe. Pour voir comment cela se rapporte à l'apprentissage, nous revenons au problème du surapprentissage. En entraînant notre réseau à apprendre une représentation compacte des données, nous privilégions une représentation plus simple plutôt qu'une hypothèse très complexe qui dépasse les données d'entraînement.

D'une certaine manière, en privilégiant ces représentations plus simples, nous essayons d'apprendre les données dans un sens plus vrai.

Machines Boltzmann restreintes

La prochaine étape logique consiste à examiner une machine de Boltzmann restreinte (RBM), un réseau neuronal stochastique génératif qui peut apprendre une distribution de probabilité sur son ensemble d'entrées .

En apprentissage automatique, les machines Botlzmann restreintes sont composées d'unités visibles et cachées.

Les RBM sont composés d'une couche cachée, visible et biaisée. Contrairement aux réseaux feedforward, les connexions entre les couches visibles et cachées sont non dirigées (les valeurs peuvent être propagées dans les sens visible vers caché et caché vers visible) et entièrement connectées (chaque unité d'une couche donnée est connectée à chaque unité dans la suivante - si nous permettions à n'importe quelle unité de n'importe quelle couche de se connecter à n'importe quelle autre couche, alors nous aurions une machine Boltzmann (plutôt qu'une machine Boltzmann restreinte ).

Le RBM standard a des unités binaires cachées et visibles : c'est-à-dire que l'activation de l'unité est de 0 ou 1 sous une distribution de Bernoulli, mais il existe des variantes avec d'autres non-linéarités.

Alors que les chercheurs connaissent les RBM depuis un certain temps maintenant, l'introduction récente de l'algorithme d'entraînement non supervisé à divergence contrastive a renouvelé l'intérêt.

Divergence contrastive

L'algorithme de divergence contrastive en une seule étape (CD-1) fonctionne comme ceci :

  1. Phase positive :
    • Un échantillon d'entrée v est fixé à la couche d'entrée.
    • v est propagé à la couche cachée de la même manière que les réseaux à anticipation. Le résultat des activations de la couche cachée est h .
  2. Phase négative :
    • Propager h vers la couche visible avec le résultat v' (les connexions entre les couches visible et cachée ne sont pas dirigées et permettent donc un mouvement dans les deux sens).
    • Propager le nouveau v' vers la couche cachée avec le résultat des activations h' .
  3. Mise à jour du poids :

    mise à jour du poids

    a est le taux d'apprentissage et v , v' , h , h' et w sont des vecteurs.

L'intuition derrière l'algorithme est que la phase positive ( h étant donné v ) reflète la représentation interne du réseau des données du monde réel. Pendant ce temps, la phase négative représente une tentative de recréer les données à partir de cette représentation interne ( v' étant donné h ). L'objectif principal est que les données générées soient aussi proches que possible du monde réel et cela se reflète dans la formule de mise à jour du poids.

En d'autres termes, le réseau a une certaine perception de la façon dont les données d'entrée peuvent être représentées, il essaie donc de reproduire les données en fonction de cette perception. Si sa reproduction n'est pas assez proche de la réalité, il fait un ajustement et réessaye.

Retour à la grippe

Pour démontrer la divergence contrastive, nous utiliserons le même ensemble de données de symptômes qu'auparavant. Le réseau de test est un RBM avec six unités visibles et deux unités cachées. Nous entraînerons le réseau en utilisant la divergence contrastive avec les symptômes v fixés à la couche visible. Lors des tests, les symptômes sont à nouveau présentés à la couche visible ; ensuite, les données sont propagées à la couche cachée. Les unités cachées représentent l'état malade/sain, une architecture très similaire à l'auto-encodeur (propagation des données de la couche visible à la couche cachée).

Après plusieurs centaines d'itérations, on observe le même résultat qu'avec les auto-encodeurs : l'une des unités cachées a une valeur d'activation plus élevée lorsqu'un des échantillons « malades » est présenté, alors que l'autre est toujours plus active pour les échantillons « sains ».

Vous pouvez voir cet exemple en action dans la méthode testContrastiveDivergence .

Réseaux profonds

Nous avons maintenant démontré que les couches cachées des auto-encodeurs et des RBM agissent comme des détecteurs de caractéristiques efficaces ; mais il est rare que nous puissions utiliser ces fonctionnalités directement. En fait, l'ensemble de données ci-dessus est plus une exception qu'une règle. Au lieu de cela, nous devons trouver un moyen d'utiliser indirectement ces fonctionnalités détectées.

Heureusement, il a été découvert que ces structures peuvent être empilées pour former des réseaux profonds . Ces réseaux peuvent être formés avidement, une couche à la fois, pour aider à surmonter les problèmes de gradient de fuite et de surajustement associés à la rétropropagation classique.

Les structures qui en résultent sont souvent assez puissantes, produisant des résultats impressionnants. Prenez, par exemple, le célèbre article "chat" de Google dans lequel ils utilisent un type spécial d'auto-encodeurs profonds pour "apprendre" la détection des visages humains et des chats sur la base de données non étiquetées .

Regardons de plus près.

Auto-encodeurs empilés

Comme son nom l'indique, ce réseau se compose de plusieurs auto-encodeurs empilés.

Les encodeurs automatiques empilés ont une série d'entrées, de sorties et de couches cachées qui contribuent aux résultats de l'apprentissage automatique.

La couche cachée de l'auto-encodeur t agit comme une couche d'entrée de l'auto-encodeur t + 1 . La couche d'entrée du premier auto-encodeur est la couche d'entrée pour l'ensemble du réseau. La procédure de formation gourmande en couches fonctionne comme ceci :

  1. Entraînez le premier auto-encodeur ( t=1 , ou les connexions rouges dans la figure ci-dessus, mais avec une couche de sortie supplémentaire) individuellement en utilisant la méthode de rétropropagation avec toutes les données d'apprentissage disponibles.
  2. Entraînez le deuxième autoencodeur t=2 (connexions vertes). Puisque la couche d'entrée pour t=2 est la couche cachée de t=1 nous ne sommes plus intéressés par la couche de sortie de t=1 et nous la supprimons du réseau. L'apprentissage commence par fixer un échantillon d'entrée à la couche d'entrée de t=1 , qui est propagé vers la couche de sortie de t=2 . Ensuite, les poids (input-hidden et hidden-output) de t=2 sont mis à jour à l'aide de la rétropropagation. t=2 utilise tous les échantillons d'apprentissage, similaire à t=1 .
  3. Répétez la procédure précédente pour toutes les couches (c'est-à-dire, supprimez la couche de sortie de l'auto-encodeur précédent, remplacez-la par un autre auto-encodeur et entraînez-vous avec la rétro-propagation).
  4. Les étapes 1 à 3 sont appelées pré-entraînement et laissent les poids correctement initialisés. Cependant, il n'y a pas de mappage entre les données d'entrée et les étiquettes de sortie. Par exemple, si le réseau est formé pour reconnaître des images de chiffres manuscrits, il n'est toujours pas possible de mapper les unités du dernier détecteur de caractéristiques (c'est-à-dire la couche cachée du dernier auto-encodeur) au type de chiffre de l'image. Dans ce cas, la solution la plus courante consiste à ajouter une ou plusieurs couches entièrement connectées à la dernière couche (connexions bleues). L'ensemble du réseau peut maintenant être considéré comme un perceptron multicouche et est formé à l'aide de la rétropropagation (cette étape est également appelée réglage fin ).

Les encodeurs automatiques empilés visent donc à fournir une méthode de pré-formation efficace pour initialiser les poids d'un réseau, vous laissant avec un perceptron multicouche complexe prêt à être formé (ou à affiner ).

Réseaux de croyance profonde

Comme pour les auto-encodeurs, nous pouvons également empiler des machines Boltzmann pour créer une classe connue sous le nom de réseaux de croyances profondes (DBN) .

Les réseaux de croyances profondes sont constitués d'un empilement de machines Boltzmann.

Dans ce cas, la couche cachée de RBM t agit comme une couche visible pour RBM t+1 . La couche d'entrée du premier RBM est la couche d'entrée pour l'ensemble du réseau, et la pré-formation gourmande en couches fonctionne comme ceci :

  1. Entraînez le premier RBM t = 1 en utilisant la divergence contrastive avec tous les échantillons d'entraînement.
  2. Entraînez le deuxième RBM t=2 . Étant donné que la couche visible pour t=2 est la couche cachée de t=1 , l'apprentissage commence par fixer l'échantillon d'entrée à la couche visible de t=1 , qui se propage vers la couche cachée de t=1 . Ces données servent ensuite à initier l'entraînement à la divergence contrastive pour t=2 .
  3. Répétez la procédure précédente pour toutes les couches.
  4. Semblable aux encodeurs automatiques empilés, après la pré-formation, le réseau peut être étendu en connectant une ou plusieurs couches entièrement connectées à la couche cachée RBM finale. Cela forme un perceptron multicouche qui peut ensuite être affiné en utilisant la rétropropagation.

Cette procédure s'apparente à celle des auto-encodeurs empilés, mais avec les auto-encodeurs remplacés par des RBM et la rétropropagation remplacée par l'algorithme de divergence contrastive.

(Remarque : pour en savoir plus sur la construction et la formation d'auto-encodeurs empilés ou de réseaux de croyances profondes, consultez l'exemple de code ici.)

Réseaux convolutionnels

En guise d'architecture finale d'apprentissage en profondeur, intéressons-nous aux réseaux convolutifs, une classe particulièrement intéressante et spéciale de réseaux feedforward très bien adaptés à la reconnaissance d'images.

Les réseaux convolutifs sont une classe spéciale de réseaux d'apprentissage en profondeur.
Image via DeepLearning.net

Avant d'examiner la structure réelle des réseaux convolutifs, nous définissons d'abord un filtre d'image, ou une région carrée avec des poids associés. Un filtre est appliqué sur toute une image d'entrée et vous appliquerez souvent plusieurs filtres. Par exemple, vous pouvez appliquer quatre filtres 6x6 à une image d'entrée donnée. Ensuite, le pixel de sortie avec les coordonnées 1,1 est la somme pondérée d'un carré 6x6 de pixels d'entrée avec le coin supérieur gauche 1,1 et les poids du filtre (qui est également un carré 6x6). Le pixel de sortie 2,1 est le résultat du carré d'entrée avec le coin supérieur gauche 2,1 et ainsi de suite.

Cela dit, ces réseaux sont définis par les propriétés suivantes :

  • Les couches convolutives appliquent un certain nombre de filtres à l'entrée. Par exemple, la première couche convolutive de l'image pourrait avoir quatre filtres 6x6. Le résultat d'un filtre appliqué sur l'image est appelé carte de caractéristiques (FM) et le nombre de cartes de caractéristiques est égal au nombre de filtres. Si la couche précédente est également convolutive, les filtres sont appliqués sur tous ses FM avec des poids différents, de sorte que chaque FM d'entrée est connectée à chaque FM de sortie. L'intuition derrière les poids partagés sur l'image est que les caractéristiques seront détectées quel que soit leur emplacement, tandis que la multiplicité des filtres permet à chacun d'eux de détecter un ensemble différent de caractéristiques.
  • Les couches de sous-échantillonnage réduisent la taille de l'entrée. Par exemple, si l'entrée consiste en une image 32x32 et que le calque a une région de sous-échantillonnage de 2x2, la valeur de sortie serait une image 16x16, ce qui signifie que 4 pixels (chaque carré de 2x2) de l'image d'entrée sont combinés en une seule sortie pixels. Il existe plusieurs façons de sous-échantillonner, mais les plus populaires sont la mise en commun maximale, la mise en commun moyenne et la mise en commun stochastique.
  • La dernière couche de sous-échantillonnage (ou de convolution) est généralement connectée à une ou plusieurs couches entièrement connectées, dont la dernière représente les données cibles.
  • La formation est effectuée à l'aide d'une rétropropagation modifiée qui prend en compte les couches de sous-échantillonnage et met à jour les poids du filtre convolutif en fonction de toutes les valeurs auxquelles ce filtre est appliqué.

Vous pouvez voir plusieurs exemples de réseaux convolutifs entraînés (avec rétropropagation) sur l'ensemble de données MNIST (images en niveaux de gris de lettres manuscrites) ici, en particulier dans les méthodes testLeNet * (je recommanderais testLeNetTiny2 car il atteint un faible taux d'erreur d'environ 2% dans un laps de temps relativement court). Il y a aussi une belle visualisation JavaScript d'un réseau similaire ici.

Mise en œuvre

Maintenant que nous avons couvert les variantes de réseau de neurones les plus courantes, j'ai pensé écrire un peu sur les défis posés lors de la mise en œuvre de ces structures d'apprentissage en profondeur.

D'une manière générale, mon objectif en créant une bibliothèque Deep Learning était (et est toujours) de construire un cadre basé sur un réseau de neurones qui satisfasse aux critères suivants :

  • Une architecture commune capable de représenter divers modèles (toutes les variantes sur les réseaux de neurones que nous avons vues plus haut, par exemple).
  • La capacité d'utiliser divers algorithmes d'apprentissage (rétropropagation, divergence contrastive, etc.).
  • Performances décentes.

Pour satisfaire ces exigences, j'ai adopté une approche à plusieurs niveaux (ou modulaire) pour la conception du logiciel.

Structure

Commençons par les bases:

  • NeuralNetworkImpl est la classe de base pour tous les modèles de réseaux de neurones.
  • Chaque réseau contient un ensemble de couches.
  • Chaque couche a une liste de connexions, où une connexion est un lien entre deux couches de sorte que le réseau est un graphe orienté acyclique.

Cette structure est suffisamment agile pour être utilisée pour les réseaux feedforward classiques, ainsi que pour les RBM et les architectures plus complexes comme ImageNet.

Il permet également à une couche de faire partie de plusieurs réseaux. Par exemple, les couches d'un Deep Belief Network sont également des couches dans leurs RBM correspondants.

De plus, cette architecture permet à un DBN d'être considéré comme une liste de RBM empilés pendant la phase de pré-formation et un réseau d'anticipation pendant la phase de réglage fin, ce qui est à la fois intuitivement agréable et pratique du point de vue de la programmation.

Propagation des données

Le module suivant s'occupe de propager les données à travers le réseau, un processus en deux étapes :

  1. Déterminez l'ordre des calques. Par exemple, pour obtenir les résultats d'un perceptron multicouche, les données sont "fixées" à la couche d'entrée (c'est donc la première couche à calculer) et propagées jusqu'à la couche de sortie. Afin de mettre à jour les poids lors de la rétropropagation, l'erreur de sortie doit être propagée à travers chaque couche dans l'ordre de la largeur, en commençant par la couche de sortie. Ceci est réalisé en utilisant diverses implémentations de LayerOrderStrategy , qui tire parti de la structure de graphe du réseau, en utilisant différentes méthodes de parcours de graphe. Quelques exemples incluent la stratégie de largeur d'abord et le ciblage d'une couche spécifique. L'ordre est en fait déterminé par les connexions entre les couches, de sorte que les stratégies renvoient une liste ordonnée de connexions.
  2. Calculez la valeur d'activation. Chaque couche a un ConnectionCalculator associé qui prend sa liste de connexions (de l'étape précédente) et les valeurs d'entrée (d'autres couches) et calcule l'activation résultante. Par exemple, dans un réseau à anticipation sigmoïde simple, le ConnectionCalculator de la couche cachée prend les valeurs des couches d'entrée et de polarisation (qui sont, respectivement, les données d'entrée et un tableau de 1s ) et les poids entre les unités (en cas de connexion complète couches, les poids sont en fait stockés dans une connexion FullyConnected en tant que Matrix ), calcule la somme pondérée et alimente le résultat dans la fonction sigmoïde. Les calculateurs de connexion implémentent une variété de fonctions de transfert (par exemple, somme pondérée, convolutive) et d'activation (par exemple, logistique et tanh pour le perceptron multicouche, binaire pour RBM). La plupart d'entre eux peuvent être exécutés sur un GPU à l'aide d'Aparapi et utilisables avec une formation en mini-batch.

Calcul GPU avec Aparapi

Comme je l'ai mentionné plus tôt, l'une des raisons pour lesquelles les réseaux de neurones ont fait une résurgence ces dernières années est que leurs méthodes de formation sont très propices au parallélisme, ce qui vous permet d'accélérer considérablement la formation avec l'utilisation d'un GPGPU. Dans ce cas, j'ai choisi de travailler avec la bibliothèque Aparapi pour ajouter le support GPU.

Aparapi impose quelques restrictions importantes sur les calculateurs de connexion :

  • Seuls les tableaux (et variables) unidimensionnels de types de données primitifs sont autorisés.
  • Seules les méthodes membres de la classe Aparapi Kernel elle-même peuvent être appelées à partir du code exécutable GPU.

Ainsi, la plupart des données (pondérations, tableaux d'entrée et de sortie) sont stockées dans des instances Matrix , qui utilisent en interne des tableaux flottants unidimensionnels. Tous les calculateurs de connexion Aparapi utilisent soit AparapiWeightedSum (pour les couches entièrement connectées et les fonctions d'entrée de somme pondérée), AparapiSubsampling2D (pour les couches de sous-échantillonnage) ou AparapiConv2D (pour les couches convolutionnelles). Certaines de ces limitations peuvent être surmontées avec l'introduction de l'architecture système hétérogène. Aparapi also allows to run the same code on both CPU and GPU.

Formation

The training module implements various training algorithms. It relies on the previous two modules. For example, BackPropagationTrainer (all the trainers are using the Trainer base class) uses feedforward layer calculator for the feedforward phase and a special breadth-first layer calculator for propagating the error and updating the weights.

My latest work is on Java 8 support and some other improvements, will soon be merged into master.

Conclusion

The aim of this Java deep learning tutorial was to give you a brief introduction to the field of deep learning algorithms, beginning with the most basic unit of composition (the perceptron) and progressing through various effective and popular architectures, like that of the restricted Boltzmann machine.

The ideas behind neural networks have been around for a long time; but today, you can't step foot in the machine learning community without hearing about deep networks or some other take on deep learning. Hype shouldn't be mistaken for justification, but with the advances of GPGPU computing and the impressive progress made by researchers like Geoffrey Hinton, Yoshua Bengio, Yann LeCun and Andrew Ng, the field certainly shows a lot of promise. There's no better time to get familiar and get involved like the present.

Appendix: Resources

If you're interested in learning more, I found the following resources quite helpful during my work:

  • DeepLearning.net: a portal for all things deep learning. It has some nice tutorials, software library and a great reading list.
  • An active Google+ community.
  • Two very good courses: Machine Learning and Neural Networks for Machine Learning, both offered on Coursera.
  • The Stanford neural networks tutorial.
Related: Schooling Flappy Bird: A Reinforcement Learning Tutorial