Guide simple sur les unités de logique arithmétique neuronale (NALU): explication, intuition et code

Les ingénieurs de recherche de DeepMind, notamment du chercheur en intelligence artificielle bien connu et auteur du livre Grokking Deep Learning, Andrew Trask ont ​​publié un article impressionnant sur un modèle de réseau neuronal pouvant apprendre des fonctions numériques simples à complexes avec une grande capacité d'extrapolation (généralisation).

Dans cet article, je vais expliquer la NALU, son architecture, ses composants et son importance par rapport aux réseaux de neurones traditionnels. L'objectif principal de ce post est de fournir une explication simple et intuitive de NALU (à la fois avec des concepts et du code) pouvant être comprise par les chercheurs, ingénieurs et étudiants ayant une connaissance limitée des réseaux de neurones et de l'apprentissage en profondeur.

Remarque: je recommande fortement aux lecteurs de lire le document original pour une compréhension plus détaillée du sujet. Le document peut être téléchargé ici.

Où les réseaux de neurones échouent-ils?

Cette image est tirée de ce post moyen

Les réseaux de neurones, en théorie, sont de très bons approximateurs de fonction. Ils peuvent presque toujours apprendre toute relation significative entre les entrées (données ou fonctionnalités) et les sorties (étiquettes ou cibles). Par conséquent, ils sont utilisés dans une grande variété d'applications, de la détection et classification d'objets à la conversion parole en texte en agents intelligents de jeu qui peuvent battre les joueurs champions du monde. Il existe de nombreux modèles de réseaux de neurones efficaces qui répondent à divers besoins d'applications telles que les réseaux de neurones convolutionnels (CNN), les réseaux de neurones récurrents (RNN), les autoencodeurs, etc. Le développement des modèles d'apprentissage en profondeur et de réseaux de neurones est un autre sujet en soi.

Cependant, selon les auteurs du document, ils manquent de capacités de base qui semblent anodines pour un humain ou même pour les abeilles! C’est la capacité de compter / manipuler des nombres et aussi d’extrapoler la relation numérique à partir d’un motif numérique observable. Dans le document, il est montré que les réseaux de neurones standard ont même du mal à apprendre même une fonction d'identité (une fonction dont l'entrée et la sortie sont identiques; f (x) = x), qui est la relation numérique la plus simple. L'image ci-dessous montre l'erreur quadratique moyenne (MSE) de divers NN formés pour apprendre une telle fonction d'identité.

L'image montre le MSE d'un réseau de neurones standard ayant exactement la même architecture, formée à l'aide de différentes fonctions d'activation (non-linéarité) dans les couches cachées.

Pourquoi ils échouent?

La raison principale pour laquelle les NN ne parviennent pas à apprendre une telle représentation numérique est l'utilisation de fonctions d'activation non linéaires dans des couches cachées du réseau. Ces fonctions d'activation sont essentielles pour apprendre la relation non linéaire abstraite entre les entrées et les étiquettes, mais elles échouent lamentablement quand il s'agit d'apprendre la représentation numérique en dehors de la plage des nombres vus dans les données d'apprentissage. Par conséquent, de tels réseaux sont très utiles pour mémoriser le modèle numérique observé dans l'ensemble de formation, mais ne parviennent pas à bien extrapoler cette représentation.

C'est comme mémoriser une réponse ou un sujet sans comprendre le concept sous-jacent à l'examen. Ce faisant, on peut très bien fonctionner si des questions similaires sont posées à l’examen, mais cela échouerait si des questions tordues étaient posées dans le but de tester les connaissances et non la capacité de mémorisation d’un candidat.

La gravité de cette défaillance correspond directement au degré de non-linéarité au sein de la fonction d'activation choisie. L’image ci-dessus montre clairement que les fonctions non linéaires à contraintes dures, telles que Tanh et Sigmoid, sont très moins capables de généraliser bien que les fonctions non linéaires à contraintes souples, telles que PReLU et ELU.

Solution: Accumulateur de neurones (NAC)

L'accumulateur neuronal (NAC) constitue la base du modèle NALU. Le NAC est un modèle de réseau neuronal simple mais efficace qui prend en charge la capacité à apprendre l’addition et la soustraction - une propriété souhaitable pour apprendre efficacement les fonctions linéaires.

NAC est une couche spéciale de linéarité dont les paramètres de poids ont la restriction d'avoir les seules valeurs 1, 0 ou -1. En contraignant les valeurs de poids de cette manière, la couche ne peut pas changer l'échelle des données d'entrée et reste cohérente sur l'ensemble du réseau, quel que soit le nombre de couches empilées. Par conséquent, le résultat sera la combinaison linéaire du vecteur d’entrée qui peut facilement représenter des opérations d’addition et de soustraction.

Intuition: Pour comprendre ce fait, considérons les exemples suivants de couches NN qui effectuent l’opération arithmétique linéaire sur les entrées.

Illustration expliquant que les couches de réseau neuronal sans biais et ayant une valeur de poids égale à -1, 0 ou 1 peuvent apprendre l’extrapolation linéaire.

Comme indiqué dans les couches NN ci-dessus, le réseau peut apprendre à extrapoler des fonctions arithmétiques simples telles que l'addition et la soustraction (y = x1 + x2 et y = x1 - x2) en limitant les paramètres de pondération à -1, 0 et 1.

Remarque: comme indiqué dans les diagrammes de réseau ci-dessus, NAC ne contient aucun paramètre de biais (b) et aucune non-linéarité appliquée à la sortie des unités de couche masquées.

Comme la contrainte sur les paramètres de poids dans le CNA est difficile à assimiler par un réseau de neurones standard, les auteurs ont décrit une formule très utile pour apprendre de telles valeurs de paramètres restreintes à l’aide des paramètres standard (sans contrainte) W_hat et M_hat. Ces paramètres ressemblent à tous les paramètres de poids NN standard qui peuvent être initialisés de manière aléatoire et peuvent être appris au cours du processus d’entraînement. La formule pour obtenir W en termes de W_hat et de M_hat est donnée ci-dessous:

La formule dénote le produit élément par s entre deux matrices

L'utilisation de l'équation ci-dessus pour calculer les paramètres de pondération dans le réseau garantit que la valeur de ces paramètres sera dans la plage de [-1,1] avec une inclinaison plus grande vers -1, 0 et 1. Cette équation est également une équation différentiable. (dont les dérivées peuvent être calculées en fonction de paramètres de poids). Par conséquent, il sera plus facile pour le CNA d’apprendre le W en utilisant la descente de gradient et la propagation en retour. Vous trouverez ci-dessous le schéma architectural d'une unité NAC.

Architecture NAC pour apprendre des fonctions numériques simples (linéaires)

Implémentation NAC en python avec Tensorflow

Comme on peut l’imaginer, NAC est un modèle simple NN avec quelques petites modifications. Ci-dessous, j'ai montré la mise en œuvre simple d'une seule couche NAC en python à l'aide des bibliothèques Tensorflow et Numpy.

importer numpy en tant que np
importer tensorflow en tant que tf
# Définir un accumulateur de neurones (NAC) pour l'addition / la soustraction -> Utile pour apprendre l'opération d'addition / la soustraction

def nac_simple_single_layer (x_in, out_units):
    '' '
    Les attributs:
        x_in -> Tenseur d'entrée
        out_units -> nombre d'unités de sortie

    Revenir:
       y_out -> Tenseur de sortie de forme mentionnée
       W -> Matrice de pondération du calque
    '' '
# Obtenir le nombre d'entités en entrée (nombres)
    in_features = x_in.shape [1]

    # définir W_hat et M_hat

    W_hat = tf.get_variable (shape = [in_features, out_units],
    initializer = tf.initializers.random_uniform (minval = -2, maxval = 2),
    trainable = True, name = "W_hat")

    M_hat = tf.get_variable (shape = [in_shape, out_units],
    initializer = tf.initializers.random_uniform (minval = -2, maxval = 2),
    trainable = True, name = "M_hat")

    # Obtenez W en utilisant la formule
    W = tf.nn.tanh (W_hat) * tf.nn.sigmoid (M_hat)

    y_out = tf.matmul (x_in, W)

    retourne y_out, W

Dans le code ci-dessus, j'ai utilisé l'initialisation uniforme aléatoire pour les paramètres pouvant être entraînés W_hat et M_hat, mais on peut utiliser n'importe quelle technique d'initialisation de poids recommandée pour ces paramètres. Pour obtenir le code de travail complet, veuillez consulter mon dépôt GitHub mentionné à la fin de cet article.

Au-delà des additions et des soustractions: NAC pour les fonctions numériques complexes

Bien que le modèle de réseau neuronal simple mentionné ci-dessus puisse apprendre des fonctions arithmétiques de base telles que l'addition et la soustraction, il est souhaitable de pouvoir apprendre des opérations arithmétiques plus complexes telles que les fonctions de multiplication, de division et de pouvoir.

Vous trouverez ci-dessous l'architecture modifiée du NAC, capable d'apprendre des fonctions numériques plus complexes en utilisant l'espace de journalisation (valeurs logarithmiques et exposants) pour ses paramètres de pondération. Notez que ce CNA est différent de celui mentionné en premier dans le post.

Architecture du CNA pour apprendre une fonction numérique plus complexe

Comme indiqué dans le diagramme ci-dessus, cette cellule applique la fonction logarrière aux données d'entrée avant la multiplication matricielle avec la matrice de pondération W, puis applique une fonction exponentielle à la matrice résultante. La formule pour obtenir un résultat est donnée dans l'équation ci-dessous.

Équation en sortie du NAC complexe montré ci-dessus. Epsilon est une très petite valeur pour éviter la situation de log (0) pendant l’entraînement

Par conséquent, tout est identique en ce qui concerne le mécanisme sous-jacent du NAC simple et du NAC complexe, y compris la formule des poids restreints W en termes de W_hat et de M_hat. La seule différence est que le NAC complexe applique un espace de journalisation sur les entrées et les sorties de la couche.

Implémentation en Python du NAC complexe:

Comme pour les architectures des deux NAC, l'implémentation python de NAC complexe est presque identique à l'exception du changement mentionné dans la formule du tenseur en sortie. Vous trouverez ci-dessous le code de ce type de CNA.

# définir un NAC complexe dans l'espace journal -> pour des fonctions arithmétiques plus complexes telles que la multiplication, la division et la puissance

def nac_complex_single_layer (x_in, out_units, epsilon = 0.000001):

    '' '
    : param x_in: vecteur d'entité en entrée
    : param out_units: nombre d'unités de sortie de la cellule
    : param epsilon: petite valeur pour éviter log (0) dans le résultat en sortie
    : retour m: tenseur de sortie
    : return W: matrice de poids associée
    
    '' '

    in_features = x_in.shape [1]

    W_hat = tf.get_variable (shape = [in_shape, out_units],
    initializer = tf.initializers.random_uniform (minval = -2, maxval = 2),
    trainable = True, name = "W_hat")

    M_hat = tf.get_variable (shape = [in_features, out_units],
    initializer = tf.initializers.random_uniform (minval = -2, maxval = 2),
    trainable = True, name = "M_hat")
    #Obtenir la matrice de paramètres W sans restriction
    W = tf.nn.tanh (W_hat) * tf.nn.sigmoid (M_hat)

    # Fonction de saisie express dans l'espace journal pour apprendre des fonctions complexes
    x_modified = tf.log (tf.abs (x_in) + epsilon)

    m = tf.exp (tf.matmul (x_modified, W))

    retour m, W

Encore une fois, pour le code de travail complet, veuillez consulter mon dépôt GitHub mentionné à la fin de cet article.

Rassembler tout cela: unités de logique arithmétique neuronale (NALU)

A présent, on peut imaginer qu'au-dessus de deux modèles NAC combinés, on peut apprendre à peu près toutes les opérations arithmétiques. C’est l’idée maîtresse de NALU, qui consiste en la combinaison pondérée d’un NAC simple et d’un NAC complexe susmentionné, commandés par un signal de porte appris. Ainsi, la NAC constitue la pierre angulaire des NALU. Donc, si vous avez bien compris le CNA, NALU est très facile à comprendre. Si ce n’est pas le cas, prenez votre temps et relisez les explications du CNA. L'image ci-dessous décrit l'architecture de NALU.

Diagramme annoté de NALU

Comme indiqué dans l’image ci-dessus, en NALU, les deux NAC (cellules violettes) sont interpolées (combinées) par une commande de grille sigmoïde apprise (cellule orange), de sorte que la sortie de l’une ou l’autre NAC puisse être activée ou désactivée par la grille en fonction de la fonction numérique tentent de former le réseau pour.

Comme mentionné ci-dessus, le CNA simple calcule la fonction d’accumulation, il est donc chargé de stocker les opérations linéaires de NALU (addition et soustraction). Alors que le CNA complexe est responsable de l'exécution de ses fonctions numériques plus complexes telles que les fonctions de multiplication, de division et de puissance. La sortie des cellules sous-jacentes dans une NALU peut être représentée mathématiquement comme suit:

NAC simple: a = W X
Complexe NAC: m = exp (W log (| X | + e))
Où, W = tanh (W_hat) * sigmoïde (M_hat)
Cellule de porte: g = sigmoïde (GX) # où G est la matrice de paramètres pouvant être entraînée standard
# Et enfin la sortie de la NALU
NALU: y = g * a + (1-g) * m # Où * est le produit par rapport aux éléments

Dans la formule ci-dessus de NALU, nous pouvons dire que si la sortie de la porte g = 0, le réseau n'apprendra que des fonctions complexes, mais pas des fonctions simples. En revanche, si g = 1, le réseau n’apprenera que les fonctions additives et non les fonctions complexes. Par conséquent, le NALU peut apprendre toutes les fonctions numériques consistant en des fonctions de multiplication, d’addition, de soustraction, de division et de pouvoir de manière à bien extrapoler les nombres extérieurs à la plage observée lors de la formation.

Implémentation de NALU en Python:

Lors de la mise en œuvre de NALU, nous utiliserons les NAC simples et complexes définis dans les extraits de code précédents.

def nalu (x_in, out_units, epsilon = 0.000001, get_weights = False):
    '' '
    : param x_in: vecteur d'entité en entrée
    : param out_units: nombre d'unités de sortie de la cellule
    : param epsilon: petite valeur pour éviter log (0) dans le résultat en sortie
    : param get_weights: Vrai si vous voulez obtenir les poids du modèle
                        en retour
    : retour: tenseur de sortie
    : return: matrice de poids de la porte
    : return: NAC1 (matrice de poids simple)
    : return: NAC2 (matrice de poids complexe)
    '' '

    in_features = x_in.shape [1]

    # Obtenir le tenseur de sortie du simple NAC
    a, W_simple = nac_simple_single_layer (x_in, out_units)

    # Obtenir le tenseur de sortie du NAC complexe
    m, W_complex = nac_complex_single_layer (x_in, out_units, epsilon = epsilon)

    # Couche de signal de porte
    G = tf.get_variable (shape = [in_features, out_units],
    initializer = tf.random_normal_initializer (stddev = 1.0),
    trainable = True, name = "Gate_weights")

    g = tf.nn.sigmoid (tf.matmul (x_in, G))

    y_out = g * a + (1 - g) * m

    si (get_weights):
        retourne y_out, G, W_simple, W_complex
    autre:
        retourne y_out

Encore une fois, dans le code ci-dessus, j'ai utilisé l'initialisation normale aléatoire pour les paramètres de porte G mais on peut utiliser n'importe quelle technique d'initialisation de poids recommandée.

Ibelieve NALU est une avancée moderne dans le domaine de l'IA et des réseaux de neurones qui semble très prometteuse. Ils peuvent ouvrir la porte à de nombreuses applications qui semblent difficiles à gérer pour les NN standard.

Les auteurs ont présenté diverses expériences et résultats mettant en œuvre NALU dans différents domaines d’application de réseaux de neurones, allant de tâches d’apprentissage de fonctions arithmétiques simples à la comptabilisation du nombre de chiffres manuscrits dans une série d’images MNIST fournies pour permettre au réseau d’apprendre à évaluer des programmes informatiques!

Les résultats sont étonnants et prouvent que la NALU excelle à bien généraliser dans presque toutes les tâches impliquant une représentation numérique par rapport aux modèles NN standard. Je recommande aux lecteurs d’examiner ces expériences et leurs résultats afin de mieux comprendre comment NALU peut être utile dans certaines tâches numériques intéressantes.

Cependant, il est peu probable que NAC ou NALU soit la solution idéale pour chaque tâche. Ils illustrent plutôt une stratégie de conception générale pour la création de modèles destinés à une classe cible de fonctions numériques.

Ci-dessous, le lien vers mon référentiel GitHub qui montre la mise en œuvre complète des extraits de code présentés dans ce message.

https://github.com/faizan2786/nalu_implementation

Vous êtes invités à essayer diverses fonctions pour tester mon modèle en utilisant différents hyperpramètres pour régler le réseau.

Faites-moi savoir si vous avez des questions ou des réflexions sur ce post dans les commentaires ci-dessous et je ferai de mon mieux pour les aborder.

PS: Ceci est mon premier article de blog sur n'importe quel sujet. Donc, toutes les recommandations, suggestions et futurs conseils techniques et non techniques concernant mon écriture sont les bienvenus.