Un article soumis par DeepMind à Nature Hybrid computing using a neural network with dynamic external memory Décrit le «calcul neuronal différentiel (DNC)» utilisé dans. L'explication de la logique est la principale, mais l'exemple d'implémentation par Python --Chainer est également présenté.
Le désir de traiter des données séquentielles avec Neural Net semble exister depuis longtemps, et le plus standard est le Recurrent Neural Net (RNN). Cependant, RNN a un "problème de disparition de gradient", et le modèle qui le surmonte est appelé mémoire à long court terme (LSTM). Ce qui est court et long, c'est que les données d'entrée restent à l'intérieur du Neural Net jusqu'à ce qu'elles soient sorties. Vous pouvez penser à cela comme quelque chose comme "Mémoire" d'une certaine manière. Cependant, il ne peut être mémorisé que pendant un "court laps de temps" de l'entrée à la sortie. Par conséquent, il est appelé "momory à court terme". Il semble qu'elle s'appelle Long Short Term Memory dans le sens où "cette mémoire à court terme peut être stockée pendant longtemps". Ensuite, je sens que le très long Meomory peut être à l'extérieur de la machine d'apprentissage plutôt qu'à l'intérieur. Je ne sais pas s'il est né d'une telle pensée, mais le calcul neuronal différentiel (DNC) consiste à préparer une mémoire en dehors de la machine d'apprentissage et à la laisser apprendre, y compris comment utiliser la mémoire. Cela signifie «Différentiable», mais si vous voulez vous entraîner avec une rétro-propagation, vous devez être capable de calculer le différentiel. Par conséquent, cela signifie que "le calcul différentiel est possible", y compris les opérations sur la mémoire externe.
Les composants du DNC sont le contrôleur principal du corps et la mémoire externe. Selon l'article, le flux de données global peut être représenté comme le montre la figure ci-dessous. DNC traite des données séquentielles, donc le pas de temps actuel est représenté par $ t $. L'étape précédente est $ t-1 $, l'étape suivante est $ t + 1 $, et l'indice $ t $ dans la figure indique à quel pas de temps chaque variable a été générée.
Examinons pas à pas le flux de données. Les nombres (1) à (4) sont attribués dans la figure, alors suivez-les dans cet ordre. (1): Les données d'entrée $ x_t $ sont entrées à partir de "Data Set". $ \ Boldsymbol {\ chi} \ _ t = [\ boldsymbol {x} \ _ t, \ boldsymbol, combiné avec la sortie de Memory à l'étape précédente $ \ boldsymbol {r} \ _ {t-1} $ Soit {r} \ _ {t-1}] $ l'entrée du contrôleur. (2): La sortie $ \ boldsymbol {h} \ _t $ de Controller est obtenue, donc divisez-la en deux routes. L'un est $ \ boldsymbol {v} \ _t $ pour "Out Put" et l'autre est "interface vector" $ \ boldsymbol {\ xi} \ _t $ pour contrôler la mémoire. Dans l'article, ce sont les transformations linéaires de $ \ boldsymbol {h} \ _t $ $ \ boldsymbol {v} \ _t = W \ _y \ boldsymbol {h} \ _t $ et $ \ boldsymbol {\ xi} \ _ t = Il s'écrit W \ _ {\ xi} \ boldsymbol {h} \ _ t $. (3): Les données sont écrites dans la mémoire en fonction du "vecteur d'interface" $ \ boldsymbol {\ xi} \ _t $, et l'état de la mémoire est mis à jour. Il lit également à partir de la mémoire, vous donnant un "vecteur de lecture" $ \ boldsymbol {r} _t $. (4): "read vector" $ \ boldsymbol {r} \ _ t $ est ajouté à la sortie vers "Out Put", tout en étant envoyé à l'entrée du contrôleur à l'étape suivante. (5): Combinez la sortie du contrôleur et la sortie de la mémoire et allez dans "Out Put" $ \ boldsymbol {y} \ _ t = \ boldsymbol {v} \ _ t + W \ _r \ boldsymbol {r} \ _ t $ Est sortie. Comme mentionné ci-dessus, 1 étape est complétée par (1) à (5).
Le contrôleur peut être n'importe quelle machine d'apprentissage qui reçoit des entrées multidimensionnelles et renvoie des sorties multidimensionnelles, mais il semble que Neural Net (récurrent) et LSTM soient souvent utilisés. (Récurrent) Je laisserai l'explication de Neural Net et LSTM à d'autres, mais dans cet article, j'expliquerai "la lecture et l'écriture de mémoire externe" qui est la plus grande caractéristique de DNC.
Parlons maintenant de la lecture et de l'écriture de mémoire externe. C'est un problème, alors je l'appellerai juste Memory ci-dessous.
Tout d'abord, comprenons la structure de la mémoire. En tant que mémoire, une matrice de $ N $ × $ W $ est utilisée. Autrement dit, considérons que les adresses sont attribuées de $ 1 $ à $ N $, et chaque adresse a un emplacement qui peut stocker un vecteur numérique de dimension $ W $. Puisque l'état de la mémoire est mis à jour à chaque instant, nous écrirons la matrice représentant la mémoire au pas de temps $ t $ comme $ M \ _t $. Cependant, on suppose que les dimensions de la matrice $ N $ × $ W $ sont fixes, où $ N $ est toujours le nombre total d'emplacements mémoire (nombre total d'adresses) et $ W $ est la longueur des emplacements (du vecteur numérique à stocker). Dimension).
Comme mentionné dans "Flux de données global", le fonctionnement de la mémoire par le contrôleur est Cela se fait via le "vecteur d'interface" $ \ boldsymbol {\ xi} \ _t $. Puisqu'il y a plusieurs tâches qui peuvent être envisagées, comme la lecture / l'écriture / l'adressage, même si vous dites Opération mémoire en une seule bouchée, $ \ boldsymbol {\ xi} \ _t $ est décomposé en chaque composant par fonction.
\begin{align}
\boldsymbol{\xi}_t = \Big[ \boldsymbol{k}_t^{r, 1},...,\boldsymbol{k}_t^{r, R}, \; \hat{\beta}_t^{r, 1},...,\hat{\beta}_t^{r, R}, \; \boldsymbol{k}_t^w, \; \hat{\beta_t}^w, \hat{\boldsymbol{e}}_t, \; \boldsymbol{\nu}_t, \; \hat{f}_t^1,...,\hat{f}_t^R, \; \hat{g}_t^a, \; \hat{g}_t^w, \; \hat{\boldsymbol{\pi}}_t^1,...,\hat{\boldsymbol{\pi}}_t^R \Big]
\end{align}
Ceux avec le signe '$ \ hat {} $' sont sujets à une conversion d'échelle supplémentaire. Il existe de nombreux types et c'est compliqué, je vais donc le résumer une fois. La valeur entre (,) est (dimension, nombre). ・ "Clé de lecture" $ (W, R) \ ;: ; \ boldsymbol {k} \ _ t ^ {r, 1}, ..., \ boldsymbol {k} \ _ t ^ {r, R} $ ・ "Force de lecture" $ (1, R) \ ;: ; \ beta \ _ t ^ {r, 1}, ..., \ beta \ _ t ^ {r, R} ; ; \ big (\ beta) \ _ t ^ {r, i} = \ text {oneplus} (\ hat {\ beta} \ _ t ^ {r, i}) \ big) $ ・ "Clé d'écriture" $ (W, 1) \ ;: ; \ boldsymbol {k} \ _ t ^ w $ ・ "Force d'écriture" $ (1,1) \ ;: ; \ beta \ _ t ^ w = \ text {oneplus} (\ hat {\ beta} \ _ t ^ w) $ ・ "Effacer le vecteur" $ (W, 1) \ ;: ; \ boldsymbol {e} \ _ t = \ sigma (\ hat {\ boldsymbol {e}} _t) $ ・ "Écrire le vecteur" $ (W, 1) \ ;: ; \ boldsymbol {\ nu} \ _t $ ・ "Porte libre" $ (1, R) \ ;: ; f \ _ t ^ 1, ..., f \ _ t ^ R ; ; \ big (f \ _ t ^ i = \ sigma (\ hat { f} \ _ t ^ i) \ big) $ ・ "Porte d'allocation" $ (1, 1) \ ;: ; g ^ a \ _ t = \ sigma (\ hat {g} ^ a \ _ t) $ ・ "Porte d'écriture" $ (1, 1) \ ;: ; g ^ w \ _ t = \ sigma (\ hat {g} ^ w \ _t) $ ・ "Mode lecture" $ (3, R) \ ;: ; \ boldsymbol {\ pi} \ _ t ^ 1, ..., \ boldsymbol {\ pi} \ _ t ^ R ; ; \ big (\ boldsymbol {\ pi} \ _ t ^ i = \ text {softmax} (\ hat {\ boldsymbol {\ pi}} \ _ t ^ i) \ big) $ La fonction utilisée pour la conversion d'échelle est
\begin{align}
& \text{oneplus}(x) = 1+\text{log}(1+e^x) \in [1, \infty) \\
& \sigma(x) = \frac{1}{1+e^{-x}} \in [0, 1] \\
& \text{softmax}(\boldsymbol{x}) = \frac{e^\boldsymbol{x}}{\sum_ie^{x_i}} \in [0, 1]
\end{align}
Est défini comme. Notez que si vous prenez une valeur vectorielle comme argument de $ \ text {oneplus} (x) $, $ \ sigma (x) $, $ \ text {exp} (x) $, cela signifie l'action de chaque composant. S'il vous plaît.
Je ne fais qu'énumérer les définitions ici, donc je ne pense pas que vous sachiez quoi que ce soit. Ci-dessous, nous expliquerons dans l'ordre comment utiliser ces composants pour contrôler la mémoire.
Avant de poursuivre, permettez-moi d'ajouter un point. $ R $ représente le nombre de lectures depuis la mémoire. Le DNC dans le papier est configuré pour lire plusieurs fois dans la mémoire en une seule étape, et le nombre de fois est $ R $. En revanche, l'écriture n'est définie qu'une seule fois par étape. Vous pouvez également voir que les dimensions du vecteur d'interface sont déterminées par $ W $ et $ R $, soit $ WR + 3W + 5R + 3 $.
Pour lire ou écrire dans la mémoire, vous devez spécifier l'adresse de l'emplacement mémoire à lire ou à écrire. Cependant, pour pouvoir apprendre par rétro-propagation, la différentiabilité, au moins la continuité des opérations, est nécessaire. Par conséquent, au lieu de spécifier une seule adresse spécifique, nous donnons une largeur et un poids dont l'adresse est lue / écrite de manière intensive. Par la suite, cette pondération sera appelée «pondération lecture / écriture», mais il s'agit de savoir comment les trouver.
Une fois que vous avez demandé la "pondération lecture / écriture", l'opération de lecture / écriture elle-même est simple. Je laisserai les détails plus tard, mais voyons un aperçu. Prenons d'abord le cas de la lecture. Supposons maintenant que vous obteniez une "pondération en lecture" $ \ boldsymbol {w} ^ r \ _t $. La dimension de ce vecteur est $ N $, ce qui équivaut au nombre total d'emplacements mémoire. Chaque composant représente la "force" de lecture des informations dans la fente mémoire de l'adresse correspondante. Autrement dit, les informations lues sont exprimées sous la forme $ M \ _t ^ T \ boldsymbol {w} ^ r \ _t $ comme le produit de la matrice mémoire et du vecteur de poids. Comme mentionné précédemment, dans l'article, la lecture se fait $ R $ fois en une seule étape, donc $ R $ de «pondération de lecture» est également $ \ {\ boldsymbol {w} \ _ t ^ {r, i} \} \ _ {i = 1, ..., R} $ Consiste. Aussi, comme une demande naturelle pour empêcher l'amplification due à la lecture.
\begin{align}
& 0 \leqq \boldsymbol{w}_t^{r,i}[n] \leqq 1 \;\; (n=1,2,...,N)\\
& \sum_{n=1}^N \boldsymbol{w}_t^{r,i}[n] \leqq 1 \;\; (i=1,2,...,R)
\end{align}
Doit imposer. De même, pensez à écrire. Supposons que vous obteniez une "pondération en écriture" $ \ boldsymbol {w_t ^ w} $. Nous avons également un vecteur $ \ boldsymbol {\ nu} \ _t $ qui représente les informations que nous voulons écrire. La dimension de $ \ boldsymbol {w_t ^ w} $ est $ N $, et chaque composant signifie à quel point $ \ boldsymbol {\ nu} \ _t $ est écrit dans l'emplacement mémoire correspondant. En d'autres termes, l'écriture est effectuée et la mémoire est mise à jour en ajoutant la matrice $ \ boldsymbol {w_t ^ w} \ boldsymbol {\ nu} \ _t ^ T $ à la matrice mémoire $ M \ _ {t-1} $. Pour chaque ingrédient
\begin{align}
& 0 \leqq \boldsymbol{w}_t^w[n] \leqq 1 \;\; (n=1,2,...,N)\\
& \sum_{n=1}^N \boldsymbol{w}_t^w[n] \leqq 1
\end{align}
Le point d'imposer est le même.
Dans l'article, les quatre étapes suivantes sont suivies, y compris la lecture et l'écriture dans la mémoire elle-même. ① Mettre à jour la pondération d'écriture ② Ecrire en mémoire ③ Mettre à jour la pondération de lecture ④ Lire depuis la mémoire
Ci-dessous, nous expliquerons tout au long de cette étape, mais même dans l'exemple de mise en œuvre, le traitement est résumé dans cet ordre, veuillez donc vous y référer. De plus, pondération en lecture / écriture $ \ {\ boldsymbol {w} \ _ {t-1} ^ {r, i} \} \ _ {i = 1, ..., R} au pas de temps précédent On suppose que $ / $ \ boldsymbol {w} \ _ {t-1} ^ {w} $ a été obtenu.
Il existe deux façons de choisir l'adresse à laquelle écrire et la pondération d'écriture se compose de deux éléments. En d'autres termes, (1) sélectionnez le créneau de destination d'écriture sur la base de l'entrée "clé" via le vecteur d'interface, et (2) sélectionnez le créneau dans lequel les informations utilisées restent sur la base de l'état de lecture jusqu'au pas de temps précédent. ,,.
Commençons par (1). La similitude est calculée en faisant correspondre la "clé d'écriture" $ \ boldsymbol {k} \ _t ^ w $ dans le vecteur d'interface $ \ boldsymbol {\ xi} \ _t $ avec les informations contenues dans chaque emplacement mémoire. Il utilise également la "force d'écriture" $ \ beta_t ^ w $ comme paramètre pour ajuster la netteté du pic de pondération. Ce calcul est également courant pour trouver la pondération de lecture, donc pour la matrice $ N $ × $ W $ $ M $, le vecteur d'ordre $ W $ $ \ boldsymbol {k} $ et la valeur scalaire $ \ beta $, Définissez l'opération.
\begin{align}
\mathcal{C}(M, \boldsymbol{k}, \beta)[n] = \frac{\text{exp}\big(\mathcal{D}(\boldsymbol{k}, M[n,:])\beta \big)}{\sum_m\text{exp}\big(\mathcal{D}(\boldsymbol{k}, M[m,:])\beta \big)}
\end{align}
Ici, $ \ mathcal {D} $ est la distance entre deux vecteurs, et il y a plusieurs façons de la prendre, mais la similitude cosinus est selon l'article.
\begin{align}
\mathcal{D}(\boldsymbol{u}, \boldsymbol{v}) = \frac{\boldsymbol{u} \cdot \boldsymbol{v}}{||\boldsymbol{u}|| \; ||\boldsymbol{v}||}
\end{align}
Je vais le laisser tel quel. A la limite de $ \ beta \ rightarrow \ infty $, $ \ mathcal {C} $ n'a qu'un seul pic net. À propos, en utilisant l'opération définie ici, la pondération d'écriture par la méthode de (1) est
\begin{align}
\boldsymbol{c}_t^w=\mathcal{C}(M_{t-1}, \boldsymbol{k}_t^w, \beta_t^w)
\end{align}
Est calculé.
Vient ensuite le calcul par la méthode (2). C'est un peu compliqué, mais nous le demanderons dans l'ordre suivant.
(2) -1. Configuration du "vecteur de rétention" $ \ boldsymbol {\ psi} \ _t $ Il est naturel de vouloir utiliser l'emplacement de mémoire dont les informations ont été lues au pas de temps précédent $ t-1 $ comme emplacement utilisé pour l'écriture. Cependant, si vous disposez toujours des informations dont vous avez besoin, ne les écrasez pas. De cette façon, le drapeau (poids car il s'agit en fait d'une valeur continue de 0 à 1) est "free gate" $ f \ _ t ^ i \ si le slot de mémoire lu dans le pas de temps précédent peut être réellement libéré. ; (i = 1, ..., R) $. Autrement dit, considérez $ f \ _t ^ i \ boldsymbol {w} \ _ {t-1} ^ {r, i} $ en unités de composant, et considérez "$ f \ _t ^ iw \ _ {t-1} ^ { r, i} $ $ \ simeq 1 $ $ \ Leftrightarrow f \ _t ^ i \ simeq 1 $ et $ w \ _ {t-1} ^ {r, i} \ simeq 1 $ "est lu et ouvert OK Alors écrasez-le. Aussi, "$ f \ _t ^ iw \ _ {t-1} ^ {r, i} $ $ \ simeq 0 $ $ \ Leftrightarrow f \ _t ^ i \ simeq 0 $ ou $ w \ _ {t-1} S'il s'agit de "^ {r, i} \ simeq 0 $", il ne peut pas être écrasé car il n'a pas été lu dans le pas de temps précédent, ou même s'il a été lu, l'indicateur de version n'a pas été défini. Maintenant, je réfléchissais à chaque lecture, mais j'ai défini le "vecteur de rétention" $ \ boldsymbol {\ psi} \ _t $, qui est le drapeau en cours d'utilisation (non écrasable) pour toutes les fois $ R $.
\begin{align}
\boldsymbol{\psi}_t = \prod_{i=1}^R\big(1-f_t^i\boldsymbol{w}_{t-1}^{r, i}\big)
\end{align}
Défini dans. Le produit est le produit de chaque composant. Etant donné que le produit est calculé après avoir calculé la différence à partir de 1, l'emplacement de mémoire qui est jugé écrasé même une fois dans $ R $ est jugé écrasé. D'autre part, il est jugé qu'il est en cours d'utilisation (non écrasé) $ \ psi \ _t \ simeq 1 $ uniquement lorsqu'il est jugé qu'il ne peut pas être écrasé pendant toutes les $ R $ fois.
(2) -2. Configuration du "vecteur d'utilisation" $ \ boldsymbol {u} \ _t $ Dans (2) -1, j'ai pensé à $ \ boldsymbol {\ psi} \ _t $ comme un drapeau en cours d'utilisation (correctement pondéré), mais je ne pensais qu'à lire dans le pas de temps précédent. En réalité, si une écriture est faite, elle devrait être plus utilisée, et vous devriez considérer la situation au pas de temps avant le dernier pas de temps. Sur cette base, le poids "vecteur d'utilisation de la mémoire" $ \ boldsymbol {u} _t $, qui représente le degré d'utilisation de chaque emplacement mémoire, est défini par la formule de mise à jour suivante.
\begin{align}
\boldsymbol{u}_t &= \big(\boldsymbol{u}_{t-1} + \boldsymbol{w}_{t-1}^w - \boldsymbol{u}_{t-1} \circ \boldsymbol{w}_{t-1}^w \big) \circ \boldsymbol{\psi}_t \\
\boldsymbol{u}_0 &= 0, \;\; \boldsymbol{w}_0^w = 0
\end{align}
$ \ Circ $ représente le produit de chaque composant. Le troisième élément de (...) est une correction pour ajuster chaque composant de $ \ boldsymbol {u} \ _t $ afin qu'il ne dépasse pas $ 1 $. Lorsque $ u \ _t = 1 $ est atteint, la mise à jour du poids par écriture s'arrêtera, et même si $ u \ _t> 1 $, la valeur sera réduite par la mise à jour.
(2) -3. Configuration de la "pondération d'allocation" Détermine l'adresse de l'emplacement mémoire sur lequel écrire en fonction du degré d'utilisation $ \ boldsymbol {u} \ _t $. Fondamentalement, il est écrit à l'endroit où le degré d'utilisation est faible, mais il est incliné. Tout d'abord, soit $ \ boldsymbol {\ phi} _t $ le vecteur constitué des index lorsque les composantes de $ \ boldsymbol {u} \ _t $ sont disposées par ordre croissant de valeur. En d'autres termes
\begin{align}
\boldsymbol{u}_t[\boldsymbol{\phi}_t[1]] \leqq \boldsymbol{u}_t[\boldsymbol{\phi}_t[2]] \leqq
\boldsymbol{u}_t[\boldsymbol{\phi}_t[3]] \leqq \cdots \leqq
\boldsymbol{u}_t[\boldsymbol{\phi}_t[N]]
\end{align}
est. Utilisez ceci pour "pondérer l'allocation" $ \ boldsymbol {a} \ _t $
\begin{align}
\boldsymbol{a}_t[\boldsymbol{\phi}_t[n]] = \big(1-\boldsymbol{u}_t[\boldsymbol{\phi}_t[n]]\big) \prod_{m=1}^{n-1}\boldsymbol{u}_t[\boldsymbol{\phi}_t[m]]
\end{align}
Défini dans. Le produit est le produit de chaque composant. En gros, c'est $ 1- \ boldsymbol {u} \ _t $, donc c'est un poids qui indique le degré d'utilisation (inscriptible).
Comme mentionné ci-dessus, les résultats de (1) et (2) sont intégrés et écrivent la pondération $ \ boldsymbol {w} \ _t ^ w $
\begin{align}
\boldsymbol{w}_t^w = g_t^w\big(g_t^a\boldsymbol{a}_t+(1-g_t^a)\boldsymbol{c}_t^w\big)
\end{align}
Mettre à jour en tant que. Où $ g_t ^ a $ et $ g_t ^ w $ ont été saisis via le vecteur d'interface $ \ boldsymbol {\ xi} _t $. Ils agissent comme des portes qui contrôlent «s'il faut écrire en fonction du drapeau ou de la« clé »utilisé» et «s'il faut écrire en premier lieu», respectivement.
Maintenant que la mise à jour de la pondération d'écriture est terminée, écrivez dans la mémoire. Cependant, les anciennes informations sont supprimées en même temps que l'écriture. La pondération de l'emplacement mémoire ciblé se fait avec la "pondération en écriture" $ \ boldsymbol {w} \ _t ^ w $. Les données à écrire sont données par "write vector" $ \ boldsymbol {\ nu} \ _t $, et le modèle pour effacer les données dans le slot est donné par effacer le vecteur $ \ boldsymbol {e} \ t $. Les deux sont saisis via le vecteur d'interface. La matrice mémoire $ M {t-1} $ est mise à jour par effacement / écriture selon la formule suivante.
\begin{align}
& M_t[n,s] = M_{t-1}[n,s] \big(1-\boldsymbol{w}_t^w[n] \boldsymbol{e}_t[s] \big) + \boldsymbol{w}_t^w[n] \boldsymbol{\nu}_t[s] \\
& \Leftrightarrow M_t = M_{t-1} \circ \big(1-\boldsymbol{w}_t^w \boldsymbol{e}_t^T \big) + \boldsymbol{w}_t^w \boldsymbol{\nu}_t^T
\end{align}
Il existe deux façons de sélectionner l'adresse de destination à lire. En d'autres termes (1) Sélectionnez l'emplacement de destination de lecture en fonction de la "clé" entrée via le vecteur d'interface, et (2) Sélectionnez l'emplacement en fonction de l'ordre d'écriture.
Concernant (1), "read key" $ \ {\ boldsymbol {k} \ _ t ^ {r, i} \} \ _ {i = 1,2, .., comme dans le cas de la pondération en écriture Utilisation de R} $ et de "force de lecture" $ \ {\ beta \ _t ^ {r, i} \} \ _ {i = 1,2, .., R} $,
\begin{align}
\boldsymbol{c}_t^{r, i} = \mathcal{C}\big(M_t, \boldsymbol{k}_t^{r,i}, \beta_t^{r,i}\big) \;\; (i=1,2,...,R)
\end{align}
Calculer.
(2) est un peu long, mais je vais l'expliquer dans l'ordre. Pour faciliter l'explication, nous expliquerons dans l'ordre de (2) -2 → (2) -1, mais lors de la mise en œuvre, le flux est (2) -1 → (2) -2.
(2) -2. Composition de la "pondération de priorité" Définissez la pondération de priorité $ \ boldsymbol {p} _t $ avec l'expression de mise à jour suivante.
\begin{align}
& \boldsymbol{p}_0 = 0 \\
& \boldsymbol{p}_t = \Big(1-\sum_{n=1}^N \boldsymbol{w}_t^w[n]\Big)\boldsymbol{p}_{t-1} + \boldsymbol{w}_t^w
\end{align}
Veuillez noter que $ \ boldsymbol {w} \ _ t ^ w $ a déjà été mis à jour dans ①. Si une écriture "forte" est faite en ②, ce sera $ \ sum \ boldsymbol {w} \ _ t ^ w \ simeq 1 $, donc les informations du pas de temps précédent $ \ boldsymbol {p} \ _ {t- 1} $ est effacé et enregistre l'endroit où l'écriture a été effectuée dans le pas de temps actuel. Si aucune écriture n'a été effectuée, le poids de la dernière écriture est conservé. Vous pouvez aussi facilement voir qu'il s'agit de $ \ boldsymbol {p} \ _ t [n] \ leq 1 $, $ \ sum \ _n \ boldsymbol {p} _t [n] \ leq 1 $.
(2) -1. Configuration de la "matrice de liens temporels" Construit une matrice $ N $ × $ N $ $ L_t $ qui représente l'ordre d'écriture dans l'emplacement mémoire. Lorsqu'il est visualisé en unités de composants, $ L_t [n, m] \ simeq 1 $ signifie que "dans la mémoire $ M_t $, les informations de l'emplacement 'n' sont écrites après les informations de l'emplacement 'm'. La formule de mise à jour est donnée ci-dessous afin que la relation «est» puisse être exprimée.
\begin{align}
& L_0[n,m]=0 \\
& L_t[n,n]=0 \\
& L_t[n,m]=\big(1-\boldsymbol{w}_t^w[n]-\boldsymbol{w}_t^w[m]\big)L_{t-1}[n,m] + \boldsymbol{w}_t^w[n]\boldsymbol{p}_{t-1}[m]
\end{align}
La composante diagonale n'a pas de sens, alors fixez-la à 0 $. $ 0 \ leqq L \ _t [n, m] \ leqq 1 $ et $ \ sum_m L \ _t [n, m] \ leqq 1 $ peuvent être confirmés immédiatement. J'ai essayé de vérifier $ \ sum_n L \ _t [n, m] \ leqq 1 $, mais je n'ai pas pu le montrer. Si quelqu'un peut le montrer, je vous serais reconnaissant si vous pouviez commenter.
(2) -3. Configuration de la "pondération avant / arrière" Pondération avant / arrière $ \ {\ boldsymbol {f} \ _t ^ i \} \ _ {i = 1, .., R} $ / $ \ {\ boldsymbol {b } \ _t ^ i \} \ _ {i = 1, .., R} $ est un peu difficile
\begin{align}
&\boldsymbol{f}_t^i[n]=\sum_{m=1}^NL_t[n,m]\boldsymbol{w}_{t-1}^{r,i}[m] \; \Leftrightarrow \; \boldsymbol{f}_t^i=L_t\boldsymbol{w}_{t-1}^{r,i} \\
&\boldsymbol{b}_t^i[m]=\sum_{n=1}^N\boldsymbol{w}_{t-1}^{r,i}[n]L_t[n,m] \; \Leftrightarrow \; \boldsymbol{b}_t^i=L_t^T\boldsymbol{w}_{t-1}^{r,i} \\
\end{align}
Cela consiste en. Si $ \ sum_n L \ _t [n, m] \ leqq 1 $ tient, alors $ 0 \ leqq \ boldsymbol {f} \ _t ^ i [n] \ leqq 1 $ et $ \ sum_n \ boldsymbol {f} \ _t ^ i [n] \ leqq1 $ tient. La même chose est vraie pour $ \ boldsymbol {b} \ _ t ^ i $.
Comme mentionné ci-dessus, en intégrant (1) et (2), lire pondération $ \ {\ boldsymbol {w} \ _ t ^ {i, r} \} \ _ {i = 1, ..., R} $ Mettre à jour.
\begin{align}
\boldsymbol{w}_t^{r,i}=\boldsymbol{\pi}_t^i[1]\boldsymbol{b}_t^i + \boldsymbol{\pi}_t^i[2]\boldsymbol{c}_t^i + \boldsymbol{\pi}_t^i[3]\boldsymbol{f}_t^i \;\; (i=1,2,...,R)
\end{align}
$ \ {\ pi_t ^ i \} \ _ {i = 1, ..., R} $ est entré via le vecteur d'interface.
La lecture à partir de la mémoire est facile. Le résultat de lecture "vecteur de lecture" $ \ {\ boldsymbol {r} \ _t ^ i \} \ _ {i = 1, .., R} $ est
\begin{align}
\boldsymbol{r}_t^i[s] = \sum_{n=1}^N \boldsymbol{w}_t^{r,i}[n]M_t[n,s] \; \Leftrightarrow \; \boldsymbol{r}_t^i = M_t^T\boldsymbol{w}_t^{r,i} \;\; (i=1,2,...,R)
\end{align}
Est calculé.
Ceci termine la lecture et l'écriture de la mémoire pour le pas de temps $ t $.
Voici un exemple d'implémentation python de la logique décrite ci-dessus. Le code d'origine est Présentation de DNC (Differentiable Neural Computers) + Implémentation par Chainer C'est dedans. Le traitement est regroupé en fonctions et les noms des variables sont modifiés pour faciliter la comparaison avec l'explication ci-dessus, mais le contenu est presque le même.
Une chose à noter est que la sortie globale du contrôleur, $ \ boldsymbol {h} \ _t $, intègre non seulement la sortie finale du contrôleur, mais toute la sortie de la couche cachée dans le papier. On dirait que c'est le cas. Cependant, par souci de simplicité, le contrôleur utilisé ici est limité à un contrôleur simple avec environ deux couches, et la sortie de la couche cachée n'est pas retirée du contrôleur.
dnc.py
import numpy as np
import chainer
from chainer import functions as F
from chainer import links as L
from chainer import optimizers, Chain, Link, Variable
# controller of DNC
class SimpleLSTM(Chain):
def __init__(self, d_in, d_hidden, d_out):
super(SimpleLSTM, self).__init__(
l1 = L.LSTM(d_in, d_hidden),
l2 = L.Linear(d_hidden, d_out),
)
def __call__(self, x):
return self.l2(self.l1(x))
def reset_state(self):
self.l1.reset_state()
class DNC(Chain):
def __init__(self, X, Y, N, W, R):
self.X = X # input dimension
self.Y = Y # output dimension
self.N = N # number of memory slot
self.W = W # dimension of one memory slot
self.R = R # number of read heads
self.d_ctr_in = W*R+X # input dimension into the controller
self.d_ctr_out = Y+W*R+3*W+5*R+3 # output dimension from the controller
self.d_ctr_hidden = self.d_ctr_out # dimension of hidden unit of the controller
self.d_interface = W*R+3*W+5*R+3 # dimension of interface vector
self.controller = SimpleLSTM(self.d_ctr_in, self.d_ctr_hidden, self.d_ctr_out)
super(DNC, self).__init__(
l_ctr = self.controller,
l_Wy = L.Linear(self.d_ctr_out, self.Y),
l_Wxi = L.Linear(self.d_ctr_out, self.d_interface),
l_Wr = L.Linear(self.R * self.W, self.Y),
)
self.reset_state()
def reset_state(self):
# initialize all the recurrent state
self.l_ctr.reset_state() # controller
self.u = Variable(np.zeros((self.N, 1)).astype(np.float32)) # usage vector (N, 1)
self.p = Variable(np.zeros((self.N, 1)).astype(np.float32)) # precedence weighting (N, 1)
self.L = Variable(np.zeros((self.N, self.N)).astype(np.float32)) # temporal memory linkage (N, N)
self.Mem = Variable(np.zeros((self.N, self.W)).astype(np.float32)) # memory (N, W)
self.r = Variable(np.zeros((1, self.R*self.W)).astype(np.float32)) # read vector (1, R * W)
self.wr = Variable(np.zeros((self.N, self.R)).astype(np.float32)) # read weighting (N, R)
self.ww = Variable(np.zeros((self.N, 1)).astype(np.float32)) # write weighting (N, 1)
# utility functions
def _cosine_similarity(self, u, v):
# cosine similarity as a distance of two vectors
# u, v: (1, -) Variable -> (1, 1) Variable
denominator = F.sqrt(F.batch_l2_norm_squared(u) * F.batch_l2_norm_squared(v))
if (np.array_equal(denominator.data, np.array([0]))):
return F.matmul(u, F.transpose(v))
return F.matmul(u, F.transpose(v)) / F.reshape(denominator, (1, 1))
def _C(self, Mem, k, beta):
# similarity between rows of matrix Mem and vector k
# Mem:(N, W) Variable, k:(1, W) Variable, beta:(1, 1) Variable -> (N, 1) Variable
N, W = Mem.shape
ret_list = [0] * N
for i in range(N):
# calculate distance between i-th row of Mem and k
ret_list[i] = self._cosine_similarity(F.reshape(Mem[i,:], (1, W)), k) * beta
# concat horizontally because softmax operates along the direction of axis=1
return F.transpose(F.softmax(F.concat(ret_list, 1)))
def _u2a(self, u):
# convert usage vector u to allocation weighting a
# u, a: (N, 1) Variable
N = u.shape[0]
phi = np.argsort(u.data.flatten()) # u.data[phi]: ascending
a_list = [0] * N
cumprod = Variable(np.array([[1.0]]).astype(np.float32))
for i in range(N):
a_list[phi[i]] = cumprod * (1.0 - F.reshape(u[phi[i]], (1, 1)))
cumprod *= F.reshape(u[phi[i]], (1, 1))
return F.concat(a_list, 0)
# operations of the DNC system
def _controller_io(self, x):
# input data from the Data Set : x (1, X) Variable
# out-put from the controller h is split into two ways : v (1, Y), xi(1, W*R+3*W+5*R+3) Variable
chi = F.concat([x, self.r], 1) # total input to the controller
h = self.l_ctr(chi) # total out-put from the controller
self.v = self.l_Wy(h)
self.xi = self.l_Wxi(h)
# interface vector xi is split into several components
(self.kr, self.beta_r, self.kw, self.beta_w,
self.e, self.nu, self.f, self.ga, self.gw, self.pi
) = F.split_axis(self.xi, np.cumsum(
[self.W*self.R, self.R, self.W, 1, self.W, self.W, self.R, 1, 1]), 1) # details of the interface vector
# rescale components
self.kr = F.reshape(self.kr, (self.R, self.W)) # read key (R, W)
self.beta_r = 1 + F.softplus(self.beta_r) # read strength (1, R)
# self.kw : write key (1, W)
self.beta_w = 1 + F.softplus(self.beta_w) # write strength (1, 1)
self.e = F.sigmoid(self.e) # erase vector (1, W)
# self.nu : write vector (1, W)
self.f = F.sigmoid(self.f) # free gate (1, R)
self.ga = F.sigmoid(self.ga) # allcation gate (1, 1)
self.gw = F.sigmoid(self.gw) # write gate (1, 1)
self.pi = F.softmax(F.reshape(self.pi, (self.R, 3))) # read mode (R, 3)
def _up_date_write_weighting(self):
# calculate retention vector : psi (N, 1)
# here, read weighting : wr (N, R) must retain state one step former
psi_mat = 1 - F.matmul(Variable(np.ones((self.N, 1)).astype(np.float32)), self.f) * self.wr # (N, R)
self.psi = Variable(np.ones((self.N, 1)).astype(np.float32))
for i in range(self.R):
self.psi = self.psi * F.reshape(psi_mat[:,i],(self.N,1)) # (N, 1)
# up date usage vector : u (N, 1)
# here, write weighting : ww (N, 1) must retain state one step former
self.u = (self.u + self.ww - (self.u * self.ww)) * self.psi
# calculate allocation weighting : a (N, 1)
self.a = self._u2a(self.u)
# calculate write content weighting : cw (N, 1)
self.cw = self._C(self.Mem, self.kw, self.beta_w)
# up date write weighting : ww (N, 1)
self.ww = F.matmul(F.matmul(self.a, self.ga) + F.matmul(self.cw, 1.0 - self.ga), self.gw)
def _write_to_memory(self):
# erase vector : e (1, W) deletes information on the Memory : Mem (N, W)
# and write vector : nu (1, W) is written there
# write weighting : ww (N, 1) must be up-dated before this step
self.Mem = self.Mem * (np.ones((self.N, self.W)).astype(np.float32) - F.matmul(self.ww, self.e)) + F.matmul(self.ww, self.nu)
def _up_date_read_weighting(self):
# up date temporal memory linkage : L (N, N)
ww_mat = F.matmul(self.ww, Variable(np.ones((1, self.N)).astype(np.float32))) # (N, N)
# here, precedence wighting : p (N, 1) must retain state one step former
self.L = (1.0 - ww_mat - F.transpose(ww_mat)) * self.L + F.matmul(self.ww, F.transpose(self.p)) # (N, N)
self.L = self.L * (np.ones((self.N, self.N)) - np.eye(self.N)) # constrain L[i,i] == 0
# up date prcedence weighting : p (N, 1)
self.p = (1.0 - F.matmul(Variable(np.ones((self.N, 1)).astype(np.float32)), F.reshape(F.sum(self.ww),(1, 1)))) * self.p + self.ww
# calculate forward weighting : fw (N, R)
# here, read wighting : wr (N, R) must retain state one step former
self.fw = F.matmul(self.L, self.wr)
# calculate backward weighting : bw (N, R)
self.bw = F.matmul(F.transpose(self.L), self.wr)
# calculate read content weighting : cr (N, R)
self.cr_list = [0] * self.R
for i in range(self.R):
self.cr_list[i] = self._C(self.Mem, F.reshape(self.kr[i,:], (1, self.W)), F.reshape(self.beta_r[0,i],(1, 1))) # (N, 1)
self.cr = F.concat(self.cr_list, 1) # (1, N * R)
# compose up-dated read weighting : wr (N, R)
bcf_tensor = F.concat([
F.reshape(F.transpose(self.bw), (self.R, self.N, 1)),
F.reshape(F.transpose(self.cr), (self.R, self.N, 1)),
F.reshape(F.transpose(self.fw), (self.R, self.N, 1))
], 2) # (R, N, 3)
self.pi = F.reshape(self.pi, (self.R, 3, 1)) # (R, 3, 1)
self.wr = F.transpose(F.reshape(F.batch_matmul(bcf_tensor, self.pi), (self.R, self.N))) # (N, R)
def _read_from_memory(self):
# read information from the memory : Mem (N, W) and compose read vector : r (W, R) to reshape (1, W * R)
# read weighting : wr (N, R) must be up-dated before this step
self.r = F.reshape(F.matmul(F.transpose(self.Mem), self.wr), (1, self.R * self.W))
def __call__(self, x):
self._controller_io(x) # input data is processed through the controller
self._up_date_write_weighting()
self._write_to_memory() # memory up-date
self._up_date_read_weighting()
self._read_from_memory() # extract information from the memory
self.y = self.l_Wr(self.r) + self.v # compose total out put y : (1, Y)
return self.y
Un exemple d'utilisation est également fourni, mais le contenu est le même que celui de Overview of DNC (Differentiable Neural Computers) + Implementation by Chainer. Un nombre aléatoire de vecteurs one-hot de longueur fixe est entré, et une fois l'entrée terminée, les données d'entrée sont renvoyées en tant que sortie. L'apprentissage est effectué pour chaque ensemble d'entrées, avec l'erreur quadratique entre les données écho et les données d'entrée comme une perte. Lorsque vous avez terminé l'entraînement, entrez le prochain ensemble de vecteurs one-hot.
dnc_echo_test.py
import dnc
import numpy as np
import chainer
from chainer import functions as F
from chainer import links as L
from chainer import optimizers, Chain, Link, Variable
def onehot(x, n):
ret = np.zeros(n).astype(np.float32)
ret[x] = 1.0
return ret
X = 5
Y = 5
N = 10
W = 10
R = 2
model = dnc.DNC(X, Y, N, W, R)
optimizer = optimizers.Adam()
optimizer.setup(model)
n_data = 10000 # number of input data
loss = 0.0
acc = 0.0
acc_bool = []
for data_cnt in range(n_data):
loss_frac = np.zeros((1, 2))
# prepare one pair of input and target data
# length of input data is randomly set
len_content = np.random.randint(3, 6)
# generate one input data as a sequence of randam integers
content = np.random.randint(0, X-1, len_content)
len_seq = len_content + len_content # the former is for input, the latter for the target
x_seq_list = [float('nan')] * len_seq # input sequence
t_seq_list = [float('nan')] * len_seq # target sequence
for i in range(len_seq):
# convert a format of input data
if (i < len_content):
x_seq_list[i] = onehot(content[i], X)
elif (i == len_content):
x_seq_list[i] = onehot(X-1, X)
else:
x_seq_list[i] = np.zeros(X).astype(np.float32)
# convert a format of output data
if (i >= len_content):
t_seq_list[i] = onehot(content[i - len_content], X)
model.reset_state() # reset reccurent state per input data
# input data is fed as a sequence
for cnt in range(len_seq):
x = Variable(x_seq_list[cnt].reshape(1, X))
if (isinstance(t_seq_list[cnt], np.ndarray)):
t = Variable(t_seq_list[cnt].reshape(1, Y))
else:
t = []
y = model(x)
if (isinstance(t, chainer.Variable)):
loss += (y - t)**2
acc_bool.append(np.argmax(y.data)==np.argmax(t.data))
if (np.argmax(y.data)==np.argmax(t.data)): acc += 1
if (cnt+1==len_seq):
# training by back propagation
model.cleargrads()
loss.grad = np.ones(loss.shape, dtype=np.float32)
loss.backward()
optimizer.update()
loss.unchain_backward()
# print loss and accuracy
if data_cnt < 50 or data_cnt >= 9950:
print('(', data_cnt, ')', acc_bool, ' :: ', loss.data.sum()/loss.data.size/len_content, ' :: ', acc/len_content)
loss_frac += [loss.data.sum()/loss.data.size/len_seq, 1.]
loss = 0.0
acc = 0.0
acc_bool = []
C'est le résultat quand il est répété 10000 fois. La valeur booléenne dans [] est la valeur booléenne qui indique si la valeur maximale du vecteur de sortie est définie sur 1 et les autres sont modifiées sur 0, ce qui correspond à la valeur d'entrée.
・ Résultats des 20 premières fois
( 0 ) [True, False, False] :: 0.197543557485 :: 0.3333333333333333
( 1 ) [False, False, False, False] :: 0.209656882286 :: 0.0
( 2 ) [True, False, False] :: 0.172263367971 :: 0.3333333333333333
( 3 ) [False, True, True] :: 0.185363880793 :: 0.6666666666666666
( 4 ) [True, True, True, True] :: 0.157090616226 :: 1.0
( 5 ) [False, False, False, False, False] :: 0.191528530121 :: 0.0
( 6 ) [True, False, False, False, False] :: 0.175649337769 :: 0.2
( 7 ) [False, False, False, True, True] :: 0.173387451172 :: 0.4
( 8 ) [True, False, True, True] :: 0.150813746452 :: 0.75
( 9 ) [False, True, False] :: 0.163899072011 :: 0.3333333333333333
( 10 ) [False, False, False, False, False] :: 0.183468780518 :: 0.0
( 11 ) [True, False, True, False] :: 0.152743542194 :: 0.5
( 12 ) [False, False, True, False] :: 0.170574557781 :: 0.25
( 13 ) [False, True, False, True, False] :: 0.161617393494 :: 0.4
( 14 ) [False, False, False, False] :: 0.168220555782 :: 0.0
( 15 ) [False, False, False] :: 0.167814588547 :: 0.0
( 16 ) [False, True, False, False] :: 0.158575570583 :: 0.25
( 17 ) [False, False, False, False] :: 0.165678012371 :: 0.0
( 18 ) [False, False, False] :: 0.165241924922 :: 0.0
( 19 ) [False, True, False] :: 0.143808253606 :: 0.3333333333333333
・ Résultats des 20 dernières fois
( 9980 ) [True, True, True, True] :: 0.000208107382059 :: 1.0
( 9981 ) [True, True, True, True, True] :: 0.000164349582046 :: 1.0
( 9982 ) [True, True, True, True, True] :: 0.000122650777921 :: 1.0
( 9983 ) [True, True, True] :: 0.000181751077374 :: 1.0
( 9984 ) [True, True, True, True, True] :: 0.000318505689502 :: 1.0
( 9985 ) [True, True, True, True, True] :: 0.00023639023304 :: 1.0
( 9986 ) [True, True, True, True, True] :: 0.000988183766603 :: 1.0
( 9987 ) [True, True, True, True, True] :: 0.000226851813495 :: 1.0
( 9988 ) [True, True, True] :: 0.000401457709571 :: 1.0
( 9989 ) [True, True, True, True] :: 0.000256504747085 :: 1.0
( 9990 ) [True, True, True, True, True] :: 0.000165695995092 :: 1.0
( 9991 ) [True, True, True, True] :: 0.000123940082267 :: 1.0
( 9992 ) [True, True, True, True, True] :: 0.000351718552411 :: 1.0
( 9993 ) [True, True, True, True] :: 0.000147357559763 :: 1.0
( 9994 ) [True, True, True, True] :: 0.000173216045368 :: 1.0
( 9995 ) [True, True, True, True] :: 0.000108330522198 :: 1.0
( 9996 ) [True, True, True, True] :: 0.00016659933608 :: 1.0
( 9997 ) [True, True, True] :: 0.000255667418242 :: 1.0
( 9998 ) [True, True, True] :: 0.000280433737983 :: 1.0
( 9999 ) [True, True, True, True, True] :: 0.000443447269499 :: 1.0
La réponse est parfaitement correcte. Je n'ai posté que 20 fois, mais False était de 0 fois au cours des 100 dernières fois. Cependant, je ne l'ai pas comparé à d'autres méthodes, donc on ne sait pas si c'est à cause de DNC.
Cette fois, le but était d'introduire la logique, alors c'est tout.
・ J'ai utilisé l'exemple de Chainer sur cette page. Présentation de DNC (Differentiable Neural Computers) + Implémentation par Chainer
・ Je n'ai pas pensé au GPU, au traitement par lots, etc. cette fois, mais je pense que la page ci-dessous sera utile. Apprentissage et génération de chaînes avec (Chainer) DNC (Differentiable Neural Computers)
-Il semble y avoir une implémentation dans Tensor Flow https://github.com/Mostafa-Samir/DNC-tensorflow
・ L'explication de LSTM est facile à comprendre ici. Comprendre le LSTM-avec les tendances récentes