RNN à partir de zéro | Construire un modèle RNN en Python

Contenu

introduction

Les humains ne réinitialisent pas leur compréhension du langage chaque fois que nous entendons une phrase. Étant donné un poste, nous saisissons le contexte en fonction de notre compréhension préalable de ces mots. L'une des caractéristiques qui nous définissent est notre mémoire (ou détenir le pouvoir).

Un algorithme peut-il reproduire cela? La première technique qui me vient à l'esprit est un réseau de neurones (NN). Mais, Malheureusement, les NN traditionnels ne peuvent pas faire cela. Prenons l'exemple de vouloir prédire ce qui va suivre dans une vidéo. Un réseau de neurones traditionnel aura du mal à générer des résultats précis.

C'est là qu'intervient le concept de réseaux de neurones récurrents. (RNN). Les RNN sont devenus extrêmement populaires dans l'espace d'apprentissage en profondeur, ce qui rend leur apprentissage encore plus impératif. Certaines applications réelles de RNN incluent:

  • Accréditation vocale
  • Machine à traduire
  • Composition musicale
  • Accréditation d'écriture manuscrite
  • Apprentissage de la grammaire

principes de base des réseaux de neuronesDans ce billet, nous allons d'abord passer en revue rapidement les composants de base d'un modèle RNN typique. Plus tard, nous configurerons la déclaration du problème que nous résoudrons en conclusion en implémentant un modèle RNN à partir de zéro en Python.

Nous pouvons toujours profiter des bibliothèques Python de haut niveau pour encoder un RNN. Ensuite, Pourquoi le coder à partir de zéro? Je crois fermement que la meilleure façon d'apprendre et de vraiment enraciner un concept est de l'apprendre à partir de zéro.. Et c'est ce que je vais montrer dans ce tutoriel.

Cet article suppose une compréhension de base des réseaux de neurones récurrents. Si vous avez besoin d'un rappel rapide ou si vous cherchez à apprendre les bases de RNN, Je vous recommande de lire les messages ci-dessous en premier:

Table des matières

  • Retour en arrière: un résumé des concepts de réseaux de neurones récurrents
  • Prédiction de séquence à l'aide de RNN
  • Construire un modèle RNN à l'aide de Python

Retour en arrière: un résumé des concepts de réseaux de neurones récurrents

Récapitulons rapidement les bases des réseaux de neurones récurrents.

Nous allons le faire en utilisant un exemple de données de séquence, disons les actions d'une entreprise particulière. Un modèle d'apprentissage automatique simple, ou un réseau de neurones artificiels, vous pouvez apprendre à prédire le prix des actions en fonction d'un certain nombre de caractéristiques, que le volume des actions, la valeur d'ouverture, etc. En dehors de ces, le prix dépend également de la performance de l'action au cours des semaines et des mois précédents. Pour un commerçant, ces données historiques sont en fait un facteur décisif majeur pour faire des prédictions.

Dans les réseaux de neurones à réaction conventionnelle, tous les cas de test sont considérés comme indépendants. Pouvez-vous voir que cela ne correspond pas bien à la prévision des cours des actions ?? Le modèle NN ne tiendrait pas compte des valeurs boursières ci-dessus, Pas une bonne idée!

Il existe un autre concept sur lequel nous pouvons compter lorsque nous traitons des données urgentes: réseaux de neurones récurrents (RNN).

Un RNN typique ressemble à ceci:

Cela peut sembler intimidant au premier abord. Mais une fois que nous l'avons développé, les choses commencent à sembler beaucoup plus simples:

Il nous est désormais plus facile de visualiser comment ces réseaux envisagent l'évolution des cours des actions. Cela nous aide à prévoir les prix du jour. Ici, chaque prédiction à l'instant t (h_t) dépend de toutes les prédictions précédentes et des informations obtenues à partir de celles-ci. Assez simple, vérité?

Les RNN peuvent résoudre dans une large mesure notre objectif de gestion des séquences, mais pas tout à fait.

Le texte est un autre bon exemple de données de séquence. Être capable de prédire quel mot ou quelle phrase vient après un certain texte pourrait être un avantage très utile.. Nous voulons nos modèles écrire des sonnets shakespeariens!

À présent, les infirmières immatriculées sont excellentes lorsqu'il s'agit d'un contexte de courte ou petite taille. Mais être capable de construire une histoire et de s'en souvenir, nos modèles doivent être capables de comprendre le contexte derrière les séquences, comme un cerveau humain.

Prédiction de séquence à l'aide de RNN

Dans ce billet, nous allons travailler sur un obstacle de prédiction de séquence en utilisant RNN. L'une des tâches les plus simples pour cela est la prévision des ondes sinusoïdales. La séquence contient une tendance visible et est facile à corriger à l'aide d'heuristiques. Voici à quoi ressemble une onde sinusoïdale:

Nous allons d'abord concevoir un réseau de neurones récurrent à partir de zéro pour résoudre ce problème.. Notre modèle RNN devrait également être bien généralisable afin que nous puissions l'appliquer à d'autres problèmes de séquence..

Nous allons formuler notre problème de cette façon: étant donné une séquence de 50 nombres appartenant à une onde sinusoïdale, prédit le nombre 51 de la série. Il est temps de mettre votre ordinateur portable Jupyter sous tension ! (ou votre IDE de choix)!

Encodage RNN à l'aide de Python

Paso 0: préparation des données

Ah, la première étape inévitable de tout projet de science des données: préparer les données avant de faire autre chose.

Comment notre modèle de réseau s'attend-il à ce que les données soient? J'accepterais une séquence d'une seule longueur 50 en entrée. Ensuite, la forme des données d'entrée sera:

(number_of_records x length_of_sequence x types_of_sequences)

Ici, types_of_sequences es 1, car nous n'avons qu'un seul type de séquence: la puis sinusoïdale.

D'autre part, la sortie n'aurait qu'une valeur pour chaque enregistrement. Depuis lors, ce sera la valeur 51 dans la séquence d'entrée. Alors sa forme serait:

(number_of_records x types_of_sequences) #où types_of_sequences est 1

Plongeons dans le code. Premier, importer des bibliothèques indispensables:

%pylône en ligne

importer math

Pour créer une onde sinusoïdale en tant que données, nous utiliserons la fonction sinusoïdale de Python Matematiques Une bibliothèque:

sin_wave = par exemple.déployer([math.sans pour autant(X) pour X dans par exemple.ranger(200)])

Visualiser l'onde sinusoïdale que nous venons de générer:

plt.terrain(sin_wave[:50])

Nous allons créer les données maintenant dans le bloc de code suivant:

X = []
Oui = []

seq_len = 50
nombre_enregistrements = longueur(sin_wave) - seq_len

pour je dans gamme(nombre_enregistrements - 50):
    X.ajouter(sin_wave[je:je+seq_len])
    Oui.ajouter(sin_wave[je+seq_len])
    
X = par exemple.déployer(X)
X = par exemple.expand_dims(X, axe=2)

Oui = par exemple.déployer(Oui)
Oui = par exemple.expand_dims(Oui, axe=1)

Imprimer le formulaire de données:

Notez que nous avons fait une boucle pour (nombre_enregistrements – 50) parce que nous voulons réserver 50 des enregistrements tels que nos données de validation. Nous pouvons créer ces données de validation maintenant:

X_val = []
Y_val = []

pour je dans gamme(nombre_enregistrements - 50, nombre_enregistrements):
    X_val.ajouter(sin_wave[je:je+seq_len])
    Y_val.ajouter(sin_wave[je+seq_len])
    
X_val = par exemple.déployer(X_val)
X_val = par exemple.expand_dims(X_val, axe=2)

Y_val = par exemple.déployer(Y_val)
Y_val = par exemple.expand_dims(Y_val, axe=1)

Paso 1: Créer l'architecture de notre modèle RNN

Notre prochaine tâche consiste à établir toutes les variables et fonctions indispensables que nous utiliserons dans le modèle RNN.. Notre modèle prendra la séquence d'entrée, le traitera au moyen d'une couche cachée de 100 unités et produira une sortie de valeur unique:

taux d'apprentissage = 0.0001    
népoch = 25               
T = 50                   # longueur de la séquence
caché_dim = 100         
output_dim = 1

bptt_truncate = 5
valeur_min_clip = -10
max_clip_value = 10

Plus tard nous définirons les poids du réseau:

U = par exemple.Aléatoire.uniforme(0, 1, (caché_dim, T))
W = par exemple.Aléatoire.uniforme(0, 1, (caché_dim, caché_dim))
V = par exemple.Aléatoire.uniforme(0, 1, (output_dim, caché_dim))

Ici,

  • U est la matrice de poids pour les poids entre les couches d'entrée et cachées
  • V est la matrice de pondération pour les pondérations entre les couches cachées et en sortie
  • W est la matrice de poids pour les poids partagés dans la couche RNN (couche cachée)

En résumé, on va définir la fonction d'activation, sigmoïde, à utiliser dans la couche cachée:

déf sigmoïde(X):
    revenir 1 / (1 + par exemple.exp(-X))

Paso 2: entraîner le modèle

Maintenant que nous avons défini notre modèle, en conclusion, nous pouvons continuer avec l'entraînement sur nos données de séquence. Nous pouvons subdiviser la procédure de formation en étapes plus petites, a savoir:

Paso 2.1: Vérifier la perte de données d'entraînement
Paso 2.1.1: Passer en avant
Paso 2.1.2: Calculer l'erreur
Paso 2.2: Vérifier la perte de données de validation
Paso 2.2.1: Passer en avant
Paso 2.2.2: Calculer l'erreur
Paso 2.3: Commencer la formation proprement dite
Paso 2.3.1: Passer en avant
Paso 2.3.2: Erreur de rétropropagation
Paso 2.3.3: Mettre à jour les poids

Nous devons répéter ces étapes jusqu'à la convergence. Si le modèle commence à surdimensionner, Arrêter! Ou simplement prédéfinir le nombre d'époques.

Paso 2.1: Vérifier la perte de données d'entraînement

Nous allons faire un passage en avant dans notre modèle RNN et calculer l'erreur quadratique des prédictions pour tous les enregistrements afin d'obtenir la valeur de perte.

pour époque dans gamme(népoch):
    # vérifier la perte dans le train
    perte = 0.0
    
    # faire une passe avant pour obtenir une prédiction
    pour je dans gamme(Oui.forme[0]):
        X, Oui = X[je], Oui[je]                    # obtenir une entrée, valeurs de sortie de chaque enregistrement
        précédent_s = par exemple.zéros((caché_dim, 1))   # ici, prev-s est la valeur de l'activation précédente de la couche cachée; qui est initialisé comme tous les zéros
        pour t dans gamme(T):
            nouvelle_entrée = par exemple.zéros(X.forme)    # nous effectuons ensuite une passe en avant pour chaque pas de temps de la séquence
            nouvelle_entrée[t] = X[t]              # pour ça, nous établissons une seule entrée pour ce pas de temps
            mulu = par exemple.point(U, nouvelle_entrée)
            mulw = par exemple.point(W, précédent_s)
            ajouter = mulw + mulu
            s = sigmoïde(ajouter)
            mulv = par exemple.point(V, s)
            précédent_s = s

    # calculer l'erreur 
        perte_par_enregistrement = (Oui - mulv)**2 / 2
        perte += perte_par_enregistrement
    perte = perte / flotter(Oui.forme[0])

Paso 2.2: Vérifier la perte de données de validation

Nous ferons de même pour calculer la perte dans les données de validation (dans le même cycle):

    # vérifier la perte sur val
    perte_val = 0.0
    pour je dans gamme(Y_val.forme[0]):
        X, Oui = X_val[je], Y_val[je]
        précédent_s = par exemple.zéros((caché_dim, 1))
        pour t dans gamme(T):
            nouvelle_entrée = par exemple.zéros(X.forme)
            nouvelle_entrée[t] = X[t]
            mulu = par exemple.point(U, nouvelle_entrée)
            mulw = par exemple.point(W, précédent_s)
            ajouter = mulw + mulu
            s = sigmoïde(ajouter)
            mulv = par exemple.point(V, s)
            précédent_s = s

        perte_par_enregistrement = (Oui - mulv)**2 / 2
        perte_val += perte_par_enregistrement
    perte_val = perte_val / flotter(Oui.forme[0])

    imprimer('Époque: ', époque + 1, ', Perte: ', perte, ', Perte de Val: ', perte_val)

Vous devriez obtenir le résultat suivant:

Époque:  1 , Perte:  [[101185.61756671]] , Perte de Val:  [[50591.0340148]]
...
...

Paso 2.3: Commencer la formation proprement dite

Nous allons maintenant commencer par la formation proprement dite du réseau. dans cette, nous allons d'abord faire une passe en avant pour calculer les erreurs et une passe en arrière pour calculer les gradients et les mettre à jour. Laissez-moi vous montrer ces étapes pour que vous puissiez visualiser comment cela fonctionne dans votre esprit.

Paso 2.3.1: Passer en avant

Dans la passe d'avance:

  • D'abord, nous multiplions l'entrée avec les poids entre l'entrée et les couches cachées.
  • Ajoutez ceci en multipliant les poids dans la couche RNN. C'est parce que nous voulons capturer la connaissance du pas de temps précédent.
  • Faites-le passer par une fonction d'activation sigmoïde.
  • Multipliez cela avec les poids entre les couches cachées et de sortie.
  • Sur la couche de sortie, on a une activation linéaire des valeurs, donc nous ne passons pas explicitement la valeur à travers une couche de déclenchement.
  • Enregistrer l'état dans la couche actuelle et aussi l'état dans le pas de temps précédent dans un dictionnaire

Voici le code pour effectuer une passe avant (notez qu'il s'agit d'une continuation du cycle précédent):

    # maquette de train
    pour je dans gamme(Oui.forme[0]):
        X, Oui = X[je], Oui[je]
    
        couches = []
        précédent_s = par exemple.zéros((caché_dim, 1))
        dU = par exemple.zéros(U.forme)
        dV = par exemple.zéros(V.forme)
        dW = par exemple.zéros(W.forme)
        
        dU_t = par exemple.zéros(U.forme)
        dV_t = par exemple.zéros(V.forme)
        dW_t = par exemple.zéros(W.forme)
        
        dU_i = par exemple.zéros(U.forme)
        dW_i = par exemple.zéros(W.forme)
        
        # Passe avant
        pour t dans gamme(T):
            nouvelle_entrée = par exemple.zéros(X.forme)
            nouvelle_entrée[t] = X[t]
            mulu = par exemple.point(U, nouvelle_entrée)
            mulw = par exemple.point(W, précédent_s)
            ajouter = mulw + mulu
            s = sigmoïde(ajouter)
            mulv = par exemple.point(V, s)
            couches.ajouter({'s':s, 'préc_s':précédent_s})
            précédent_s = s

Paso 2.3.2: Erreur de rétropropagation

Après l'étape de propagation vers l'avant, nous calculons les gradients dans chaque couche et propageons les erreurs. Nous utiliserons la propagation arrière tronquée dans le temps (TBPTT), au lieu de rétro-propager la vanille. Cela peut sembler complexe, mais c'est en fait assez simple.

La différence centrale entre BPTT et backprop est que l'étape de rétropropagation est effectuée pour tous les pas de temps dans la couche RNN. Ensuite, si la longueur de notre séquence est 50, nous allons rétropropager tous les pas de temps avant le pas de temps actuel.

Si tu as bien deviné, BPTT semble très coûteux en calcul. Ensuite, au lieu de se propager en arrière à travers tous les pas de temps précédents, nous propageons jusqu'à x pas de temps pour économiser la puissance de calcul. Considérez ceci idéologiquement similaire à la descente de gradient stochastique, où nous incluons un lot de points de données au lieu de tous les points de données.

Voici le code pour propager les erreurs à l'envers:

        # dérivé de pred
        dmulv = (mulv - Oui)
        
        # passe en arrière
        pour t dans gamme(T):
            dV_t = par exemple.point(dmulv, par exemple.transposer(couches[t]['s']))
            dsv = par exemple.point(par exemple.transposer(V), dmulv)
            
            ds = dsv
            papa = ajouter * (1 - ajouter) * ds
            
            dmulw = papa * par exemple.ones_like(mulw)

            dprev_s = par exemple.point(par exemple.transposer(W), dmulw)


            pour je dans gamme(t-1, max(-1, t-bptt_truncate-1), -1):
                ds = dsv + dprev_s
                papa = ajouter * (1 - ajouter) * ds

                dmulw = papa * par exemple.ones_like(mulw)
                dmulou = papa * par exemple.ones_like(mulu)

                dW_i = par exemple.point(W, couches[t]['préc_s'])
                dprev_s = par exemple.point(par exemple.transposer(W), dmulw)

                nouvelle_entrée = par exemple.zéros(X.forme)
                nouvelle_entrée[t] = X[t]
                dU_i = par exemple.point(U, nouvelle_entrée)
                dx = par exemple.point(par exemple.transposer(U), dmulou)

                dU_t += dU_i
                dW_t += dW_i
                
            dV += dV_t
            dU += dU_t
            dW += dW_t

Paso 2.3.3: Mettre à jour les poids

Finalement, on met à jour les poids avec les gradients de poids calculés. Une chose à laquelle nous devons faire attention est que les dégradés ont tendance à exploser si vous ne les gardez pas sous contrôle.. Il s'agit d'un sujet fondamental dans la formation des réseaux de neurones., appelé le problème du gradient explosif. Il faut donc les tenir dans une fourchette pour qu'ils n'explosent pas. On peut faire comme ça

            si dU.max() > max_clip_value:
                dU[dU > max_clip_value] = max_clip_value
            si dV.max() > max_clip_value:
                dV[dV > max_clip_value] = max_clip_value
            si dW.max() > max_clip_value:
                dW[dW > max_clip_value] = max_clip_value
                
            
            si dU.min() < valeur_min_clip:
                dU[dU < valeur_min_clip] = valeur_min_clip
            si dV.min() < valeur_min_clip:
                dV[dV < valeur_min_clip] = valeur_min_clip
            si dW.min() < valeur_min_clip:
                dW[dW < valeur_min_clip] = valeur_min_clip
        
        # mettre à jour
        U -= taux d'apprentissage * dU
        V -= taux d'apprentissage * dV
        W -= taux d'apprentissage * dW

Lors de l'entraînement du modèle précédent, on obtient ce résultat:

Époque:  1 , Perte:  [[101185.61756671]] , Perte de Val:  [[50591.0340148]]
Époque:  2 , Perte:  [[61205.46869629]] , Perte de Val:  [[30601.34535365]]
Époque:  3 , Perte:  [[31225.3198258]] , Perte de Val:  [[15611.65669247]]
Époque:  4 , Perte:  [[11245.17049551]] , Perte de Val:  [[5621.96780111]]
Époque:  5 , Perte:  [[1264.5157739]] , Perte de Val:  [[632.02563908]]
Époque:  6 , Perte:  [[20.15654115]] , Perte de Val:  [[10.05477285]]
Époque:  7 , Perte:  [[17.13622839]] , Perte de Val:  [[8.55190426]]
Époque:  8 , Perte:  [[17.38870495]] , Perte de Val:  [[8.68196484]]
Époque:  9 , Perte:  [[17.181681]] , Perte de Val:  [[8.57837827]]
Époque:  10 , Perte:  [[17.31275313]] , Perte de Val:  [[8.64199652]]
Époque:  11 , Perte:  [[17.12960034]] , Perte de Val:  [[8.54768294]]
Époque:  12 , Perte:  [[17.09020065]] , Perte de Val:  [[8.52993502]]
Époque:  13 , Perte:  [[17.17370113]] , Perte de Val:  [[8.57517454]]
Époque:  14 , Perte:  [[17.04906914]] , Perte de Val:  [[8.50658127]]
Époque:  15 , Perte:  [[16.96420184]] , Perte de Val:  [[8.46794248]]
Époque:  16 , Perte:  [[17.017519]] , Perte de Val:  [[8.49241316]]
Époque:  17 , Perte:  [[16.94199493]] , Perte de Val:  [[8.45748739]]
Époque:  18 , Perte:  [[16.99796892]] , Perte de Val:  [[8.48242177]]
Époque:  19 , Perte:  [[17.24817035]] , Perte de Val:  [[8.6126231]]
Époque:  20 , Perte:  [[17.00844599]] , Perte de Val:  [[8.48682234]]
Époque:  21 , Perte:  [[17.03943262]] , Perte de Val:  [[8.50437328]]
Époque:  22 , Perte:  [[17.01417255]] , Perte de Val:  [[8.49409597]]
Époque:  23 , Perte:  [[17.20918888]] , Perte de Val:  [[8.5854792]]
Époque:  24 , Perte:  [[16.92068017]] , Perte de Val:  [[8.44794633]]
Époque:  25 , Perte:  [[16.76856238]] , Perte de Val:  [[8.37295808]]

Bien paraître! Il est temps d'obtenir les prédictions et de les tracer pour avoir une idée visuelle de ce que nous avons conçu.

Paso 3: obtenir des prédictions

Nous allons faire une passe en avant à travers les poids entraînés pour obtenir nos prédictions:

preds = []
pour je dans gamme(Oui.forme[0]):
    X, Oui = X[je], Oui[je]
    précédent_s = par exemple.zéros((caché_dim, 1))
    # Passe avant
    pour t dans gamme(T):
        mulu = par exemple.point(U, X)
        mulw = par exemple.point(W, précédent_s)
        ajouter = mulw + mulu
        s = sigmoïde(ajouter)
        mulv = par exemple.point(V, s)
        précédent_s = s

    preds.ajouter(mulv)
    
preds = par exemple.déployer(preds)

Tracer ces prédictions avec les valeurs réelles:

plt.terrain(preds[:, 0, 0], 'g')
plt.terrain(Oui[:, 0], 'r')
plt.spectacle()

C'était dans les données d'entraînement. Comment savoir si notre modèle n'était pas trop serré? C'est là qu'intervient l'ensemble de validation., que nous avons précédemment créé:

preds = []
pour je dans gamme(Y_val.forme[0]):
    X, Oui = X_val[je], Y_val[je]
    précédent_s = par exemple.zéros((caché_dim, 1))
    # Pour chaque pas de temps...
    pour t dans gamme(T):
        mulu = par exemple.point(U, X)
        mulw = par exemple.point(W, précédent_s)
        ajouter = mulw + mulu
        s = sigmoïde(ajouter)
        mulv = par exemple.point(V, s)
        précédent_s = s

    preds.ajouter(mulv)
    
preds = par exemple.déployer(preds)

plt.terrain(preds[:, 0, 0], 'g')
plt.terrain(Y_val[:, 0], 'r')
plt.spectacle()

Rien de mal. Les prédictions semblent impressionnantes. Le score RMSE dans les données de validation est également respectable:

de sklearn.metrics importer Mean_squared_error

math.carré(Mean_squared_error(Y_val[:, 0] * max_val, preds[:, 0, 0] * max_val))
0.127191931509431

Remarques finales

Je ne saurais trop insister sur l'utilité des RNN lorsque vous travaillez avec des données de séquence. J'implore tout le monde de prendre cet apprentissage et de l'appliquer à un ensemble de données. Faites un obstacle à la PNL et voyez si vous pouvez trouver une solution. Vous pouvez toujours me joindre dans la section commentaire ci-dessous si vous avez des questions.

Dans ce billet, nous avons appris à créer un modèle de réseau de neurones récurrent à partir de zéro en utilisant uniquement la bibliothèque numpy. Depuis lors, vous pouvez utiliser une bibliothèque de haut niveau comme Keras ou Caffe, mais il est essentiel de connaître le concept que vous mettez en œuvre.

Partage tes pensées, questions et commentaires sur ce post ci-dessous. Bon apprentissage!

Abonnez-vous à notre newsletter

Nous ne vous enverrons pas de courrier SPAM. Nous le détestons autant que vous.