Je suis intéressé par le réseau neuronal récurrent, mais j'ai du mal à écrire du code. N'y a-t-il pas beaucoup de cas comme celui-ci? Il y a plusieurs raisons, mais dans mon cas, je peux penser aux suivantes.
À propos, le Deep Learning de Theano et le tutoriel de TensorFlow traitent des modèles de langage. Ceux qui sont familiers avec les modèles de langage peuvent commencer rapidement, mais les débutants doivent d'abord comprendre "ce que l'exemple tente de résoudre".
Cette fois, j'ai pris un exemple traitant d'une séquence de nombres plus simple qui n'est pas un modèle de langage, et j'ai décidé de mettre en œuvre un simple réseau neuronal récurrent (RNN).
(L'environnement de programmation utilisé est python 2.7.11, Theano 0.7.0.)
En examinant RNN, j'ai d'abord essayé d'exécuter le tutoriel (ptb_word_lm.py) de "TensorFlow". On peut voir que la variable «perplexité» diminue à mesure que la valeur «époque» augmente. Cependant, je ne pouvais pas comprendre les détails de ce qu'il résolvait. Puisque LSTM (Long Short-term Memory) est également utilisé comme modèle de RNN, j'ai senti que l'introduction de RNN était un seuil élevé.
Elman Net est présenté comme un simple RNN dans le document "Deep Learning". Aussi, quand j'ai cherché "Elman RNN" comme mot-clé, j'ai trouvé un blog appelé "Peter's note" (http://peterroelants.github.io/) qui présente des RNN simples comme référence, j'ai donc créé un programme basé sur cela. enquêté.
Le chiffre de RNN est cité sur le site ci-dessus.
Fig. Simple RNN structure
Les données entrent à partir de l'unité d'entrée x, et après avoir multiplié par le poids W_in, elles entrent dans l'unité de couche cachée s. Il y a un flux récursif pour la sortie de l'unité S, et le résultat de l'application du poids W_rec revient à l'unité s au moment suivant. De plus, il est généralement nécessaire de considérer le poids W_out pour la sortie, mais pour simplifier la structure, si W_out = 1.0 est fixe, l'état de l'unité S sera sorti tel quel.
Considérez l'état "étendu" sur le côté droit afin d'appliquer la méthode BPTT (propagation arrière dans le temps) à l'état indiqué sur la gauche. L'état de la valeur initiale s_0 de l'unité cachée change vers la droite en multipliant le poids W_rec au fur et à mesure que le temps avance. De plus, [x_1, x_2, ... x_n] est entré à chaque fois. L'état de s_n est émis vers l'unité y au moment final.
Le modèle illustré ci-dessus peut être converti en code Python comme suit. (Extrait de "Peter's note".)
def update_state(xk, sk, wx, wRec):
return xk * wx + sk * wRec
def forward_states(X, wx, wRec):
# Initialise the matrix that holds all states for all input sequences.
# The initial state s0 is set to 0.
S = np.zeros((X.shape[0], X.shape[1]+1))
# Use the recurrence relation defined by update_state to update the
# states trough time.
for k in range(0, X.shape[1]):
# S[k] = S[k-1] * wRec + X[k] * wx
S[:,k+1] = update_state(X[:,k], S[:,k], wx, wRec)
return S
En outre, en ce qui concerne "quel type de problème est traité par le modèle RNN ci-dessus", entrez une valeur numérique binaire de X_k = 0. ou 1. comme entrée. La sortie est un modèle de réseau qui génère la valeur totale de ces binaires. Par exemple Pour X = [0.1.0.0.0.0.0.0.0.0.0.0. 1.](car la valeur totale de cette liste X est 2.) Réglez la sortie de Y = 2. sur la valeur correcte. Bien entendu, le contenu de l'exemple est d'estimer par RNN (comprenant deux coefficients de pondération) sans utiliser «l'algorithme de comptage de valeurs numériques».
Puisque la valeur de sortie est une valeur numérique qui prend des valeurs continues, elle peut être considérée comme une sorte de problème de «retour», pas comme un problème de «classification». Par conséquent, MSE (erreur quadratique moyenne) est utilisée comme fonction de coût et les données unitaires sont transmises telles quelles sans passer la fonction d'activation.
Tout d'abord, la formation est effectuée à l'aide des données Train (créées à l'avance), et le coefficient de pondération de 2 [W_in, W_rec] est obtenu. Il peut être facilement estimé en regardant la figure ci-dessus, mais la réponse correcte est «[W_in, W_rec] = [1.0, 1.0] ».
Dans l'article "Peter's note" auquel j'ai fait référence, j'ai utilisé python (avec numpy) pour créer un notebook IPython sans utiliser la bibliothèque Deep Learning. Si cela est copié tel quel, le résultat comme dans l'article du blog peut être obtenu, mais compte tenu du développement, nous avons essayé de l'implémenter en utilisant la bibliothèque Deep Learning. Nous avons considéré les éléments suivants comme des options.
Au début, j'ai essayé de faire du code python original "un par un" dans la version TensorFlow,
for k in range(0, X.shape[1]):
# S[k] = S[k-1] * wRec + X[k] * wx
S[:,k+1] = update_state(X[:,k], S[:,k], wx, wRec)
return S
Il s'est avéré que le traitement en boucle de la partie de n'était pas bien corrigé (vers la version TensorFlow). Si vous vous référez au code du tutoriel de TensorFlow (ptb_word_lm.py etc.), vous devriez être en mesure d'implémenter ce modèle RNN simple comme une évidence, mais comme la bibliothèque de classes associée était compliquée et difficile à comprendre, j'ai décidé d'utiliser TensorFlow cette fois. passé.
De plus, les options 3, telles que «Keras» et «Pylearn2», n'ont pas été retenues cette fois parce qu'elles s'écartent de l'objectif de «comprendre l'implémentation de RNN».
En fin de compte, j'ai décidé d'écrire la version "Theano" du code pour l'option 2.
Ce qui est commun au code RNN de Theano trouvé sur le net est que la plupart du code utilise "Theano scan". Theano scan est une fonction permettant d'effectuer des traitements de boucle (traitement d'itération) et d'itération (calcul de convergence) dans le cadre de Theano. Les spécifications sont compliquées, et il est difficile à comprendre immédiatement même si vous regardez la documentation originale (documentation Theano). Bien que les informations japonaises soient assez limitées, j'ai procédé à l'enquête sur le comportement de Theano scan en essayant un petit code avec Jupyter Notebook en me référant à l'article de blog de M. sinhrks.
n = T.iscalar('n')
result, updates = theano.scan(fn=lambda prior, nonseq: prior * 2,
sequences=None,
outputs_info=a, #Voir la valeur dans la boucle précédente--> prior
non_sequences=a, #Valeur non séquentielle--> nonseq
n_steps=n)
myfun1 = theano.function(inputs=[a, n], outputs=result, updates=updates)
myfun1(5, 3)
# array([10, 20, 40])
# return-1 = 5 * 2
# return-2 = return-1 * 2
# return-3 = return-2 * 2
Résultat de l'exécution:
>>> array([10, 20, 40], dtype=int32)
Je ne peux pas l'expliquer en détail, je vais donc couvrir quelques exemples d'utilisation. Theano.scan () prend 5 types d'arguments comme décrit ci-dessus.
Key Word | Contenu | Exemple d'utilisation |
---|---|---|
fn | Fonctions pour le traitement itératif | fn=lambda prior, nonseq: prior * 2 |
sequences | Répertoriez ces entrées tout en faisant avancer les éléments pendant le traitement séquentiel,Variable de type de matrice | sequences=T.arange(x) |
outputs_info | Donne la valeur initiale du traitement séquentiel | outputs_info=a |
non_sequences | Valeur fixe qui n'est pas une séquence (invariante avec le traitement itératif) | non_sequences=a |
n_steps | Fonction itérative | n_steps=n |
Dans le code ci-dessus, theano.scan () reçoit une valeur initiale de 5 (pas une séquence) et un nombre de fois 3, et chaque itération est multipliée par 2 au résultat du processus précédent. Il y a. Première itération: 5 x 2 = 10 Deuxième itération: 10 x 2 = 20 Troisième itération: 20 x 2 = 40 En conséquence, result = [10, 20, 40] est calculé.
Ce qui suit est un test qui est un peu plus conscient RNN.
v = T.matrix('v')
s0 = T.vector('s0')
result, updates = theano.scan(fn=lambda seq, prior: seq + prior * 2,
sequences=v,
outputs_info=s0,
non_sequences=None)
myfun2 = theano.function(inputs=[v, s0], outputs=result, updates=updates)
myfun2([[1., 0.], [0., 1.], [1., 1.]], [0.5, 0.5])
Résultat de l'exécution:
>>> array([[ 2., 1.],
[ 4., 3.],
[ 9., 7.]], dtype=float32)
La valeur initiale [0,5, 0,5] est entrée dans la fonction.
"theano.scan ()" est une fonction qui prend en charge le contrôle de flux du traitement requis par RNN. Une fonctionnalité similaire pour TensorFlow n'est actuellement pas prise en charge,
Our white paper mentions a number of control flow operations that we've experimented with -- I think once we're happy with its API and confident in its implementation we will try to make it available through the public API -- we're just not quite there yet. It's still early days for us :)
(Extrait de la discussion dans le numéro 208 de GitHub TensorFlow.)
J'aimerais donc attendre un soutien futur.
(Je ne comprends pas quel type d'implémentation est fait pour le modèle RNN de TensorFlow, mais le fait que le calcul RNN ait déjà été réalisé signifie qu'une telle fonction semblable à "theano.scan ()" l'est " Cela signifie que ce n'est pas "essentiel". Je pense qu'il est nécessaire d'étudier un peu plus l'exemple de code de TenforFlow dans ce cas.)
Maintenant que nous connaissons Theano Scan (), regardons le code RNN simple. Tout d'abord, définissez une classe simpleRNN.
class simpleRNN(object):
# members: slen : state length
# w_x : weight of input-->hidden layer
# w_rec : weight of recurrnce
def __init__(self, slen, nx, nrec):
self.len = slen
self.w_x = theano.shared(
np.asarray(np.random.uniform(-.1, .1, (nx)),
dtype=theano.config.floatX)
)
self.w_rec = theano.shared(
np.asarray(np.random.uniform(-.1, .1, (nrec)),
dtype=theano.config.floatX)
)
def state_update(self, x_t, s0):
# this is the network updater for simpleRNN
def inner_fn(xv, s_tm1, wx, wr):
s_t = xv * wx + s_tm1 * wr
y_t = s_t
return [s_t, y_t]
w_x_vec = T.cast(self.w_x[0], 'float32')
w_rec_vec = T.cast(self.w_rec[0], 'float32')
[s_t, y_t], updates = theano.scan(fn=inner_fn,
sequences=x_t,
outputs_info=[s0, None],
non_sequences=[w_x_vec, w_rec_vec]
)
return y_t
En tant que membre de classe, une classe est définie en donnant la longueur et le poids (w_x, w_rec) de l'état. La méthode de classe state_update () met à jour l'état du réseau en fonction de la valeur initiale s0 de state et de la séquence d'entrée x_t, et calcule y_t (séquence de sortie). y_t est un vecteur, mais dans le traitement principal, seule la valeur finale est extraite et utilisée pour calculer la fonction de coût, telle que y = y_t [-1]
.
Dans le processus principal, tout d'abord, les données utilisées pour l'apprentissage sont créées. (Presque comme dans la "note de Peter" originale.)
np.random.seed(seed=1)
# Create Dataset by program
num_samples = 20
seq_len = 10
trX = np.zeros((num_samples, seq_len))
for row_idx in range(num_samples):
trX[row_idx,:] = np.around(np.random.rand(seq_len)).astype(int)
trY = np.sum(trX, axis=1)
trX = trX.astype(np.float32)
trX = trX.T # need 'List of vector' shape dataset
trY = trY.astype(np.float32)
# s0 is time-zero state
s0np = np.zeros((num_samples), dtype=np.float32)
trX est une série de données de longueur 10 et 20 échantillons. Le point ici est que la matrice est transposée comme trX = trX.T
. En tant qu'ensemble de données d'apprentissage automatique général, il semble que les quantités de caractéristiques d'une donnée soient disposées dans la direction horizontale (colonne) et disposées dans la direction verticale (ligne) pour le nombre d'échantillons.
Data Set Shape
feature1 feature2 feature3 ...
sample1: - - -
sample2: - - -
sample3: - - -
.
.
Cependant, cette fois, lors de la mise à jour des données de séries chronologiques avec theano.scan (), il était nécessaire de regrouper les données verticalement et de transmettre les données.
(En regroupant comme suit, theano.scan()Il est cohérent avec le fonctionnement de. )
Data Set Shape (updated)
[ time1[sample1, time2[sample1, time3[sample1 ... ]
sample2, sample2, sample2,
sample3, sample3, sample3,
... ] ... ] ... ]
Afin de réaliser cela facilement, la matrice est transposée et traitée comme une entrée dans theano.scan ().
Après cela, le coût «perte» est calculé à partir du graphique de Theano, de la valeur de calcul du modèle «y_hypo» et de l'étiquette de données du train «y_».
# Tensor Declaration
x_t = T.matrix('x_t')
x = T.matrix('x')
y_ = T.vector('y_')
s0 = T.vector('s0')
y_hypo = T.vector('y_hypo')
net = simpleRNN(seq_len, 1, 1)
y_t = net.state_update(x_t, s0)
y_hypo = y_t[-1]
loss = ((y_ - y_hypo) ** 2).sum()
Une fois que vous atteignez ce point, vous pouvez procéder à l'apprentissage d'une manière familière.
# Train Net Model
params = [net.w_x, net.w_rec]
optimizer = GradientDescentOptimizer(params, learning_rate=1.e-5)
train_op = optimizer.minimize(loss)
# Compile ... define theano.function
train_model = theano.function(
inputs=[],
outputs=[loss],
updates=train_op,
givens=[(x_t, trX), (y_, trY), (s0, s0np)],
allow_input_downcast=True
)
n_epochs = 2001
epoch = 0
w_x_ini = (net.w_x).get_value()
w_rec_ini = (net.w_rec).get_value()
print('Initial weights: wx = %8.4f, wRec = %8.4f' \
% (w_x_ini, w_rec_ini))
while (epoch < n_epochs):
epoch += 1
loss = train_model()
if epoch % 100 == 0:
print('epoch[%5d] : cost =%8.4f' % (epoch, loss[0]))
w_x_final = (net.w_x).get_value()
w_rec_final = (net.w_rec).get_value()
print('Final weights : wx = %8.4f, wRec = %8.4f' \
% (w_x_final, w_rec_final))
Cette fois, nous avons préparé et utilisé deux optimiseurs, GradientDecent (méthode de descente de gradient) et RMSPropOptimizer (méthode RMSProp). (Le code de la partie optimiseur est omis cette fois. Pour la méthode RMSProp, reportez-vous au site Web affiché plus loin.)
La description selon laquelle "RNN est généralement difficile à faire progresser dans l'apprentissage" peut être trouvée à divers endroits, mais le résultat m'a fait comprendre.
Initial weights: wx = 0.0900, wRec = 0.0113
epoch[ 100] : cost =529.6915
epoch[ 200] : cost =504.5684
epoch[ 300] : cost =475.3019
epoch[ 400] : cost =435.9507
epoch[ 500] : cost =362.6525
epoch[ 600] : cost = 0.2677
epoch[ 700] : cost = 0.1585
epoch[ 800] : cost = 0.1484
epoch[ 900] : cost = 0.1389
epoch[ 1000] : cost = 0.1300
epoch[ 1100] : cost = 0.1216
epoch[ 1200] : cost = 0.1138
epoch[ 1300] : cost = 0.1064
epoch[ 1400] : cost = 0.0995
epoch[ 1500] : cost = 0.0930
epoch[ 1600] : cost = 0.0870
epoch[ 1700] : cost = 0.0813
epoch[ 1800] : cost = 0.0760
epoch[ 1900] : cost = 0.0710
epoch[ 2000] : cost = 0.0663
Final weights : wx = 1.0597, wRec = 0.9863
Grâce à l'apprentissage, nous avons pu obtenir une valeur approximative de la bonne réponse [w_x, w_rec] = [1.0, 1.0]. La figure ci-dessous montre comment la fonction de coût est réduite.
Fig. Loss curve (GradientDescent)
Initial weights: wx = 0.0900, wRec = 0.0113
epoch[ 100] : cost = 5.7880
epoch[ 200] : cost = 0.3313
epoch[ 300] : cost = 0.0181
epoch[ 400] : cost = 0.0072
epoch[ 500] : cost = 0.0068
epoch[ 600] : cost = 0.0068
epoch[ 700] : cost = 0.0068
epoch[ 800] : cost = 0.0068
epoch[ 900] : cost = 0.0068
epoch[ 1000] : cost = 0.0068
epoch[ 1100] : cost = 0.0068
epoch[ 1200] : cost = 0.0068
epoch[ 1300] : cost = 0.0068
epoch[ 1400] : cost = 0.0068
epoch[ 1500] : cost = 0.0068
epoch[ 1600] : cost = 0.0068
epoch[ 1700] : cost = 0.0068
epoch[ 1800] : cost = 0.0068
epoch[ 1900] : cost = 0.0068
epoch[ 2000] : cost = 0.0068
Final weights : wx = 0.9995, wRec = 0.9993
Fig. Loss curve (RMSProp)
Dans ce modèle, la non-linéarité de la fonction de coût par rapport aux paramètres est très forte. Comme la valeur numérique diverge dès que le taux d'apprentissage est augmenté, il était nécessaire de fixer le taux d'apprentissage à 1,0e-5, ce qui est assez petit, dans la méthode Gradient Descent. En revanche, avec la méthode RMSProp, qui est dite adaptée pour RNN, l'apprentissage peut se dérouler sans problème même avec un taux d'apprentissage de 0,001.
(Supplément) Le blog de référence "Peter's note" a une explication détaillée de l'état de la fonction de coût et de RMSProp (nommé "Rprop" dans le blog source). La non-linéarité de la fonction de coût est visualisée avec des nuances de couleur, veuillez donc vous y référer si vous êtes intéressé. (Ce sera le lien ci-dessous.)
Recommended Posts