WHY
Je pense que beaucoup de gens sont intéressés par le deep learning, je décrirai donc la mise en œuvre du deep learning dans le dialogue.
Étant donné que la réponse au chat utilise Chainer, je me concentrerai sur cette partie. Cependant, veuillez noter que la version est ancienne.
La version dont le fonctionnement a été confirmé est la 1.5.1.
Il peut y avoir des erreurs. Il y avait une partie que je voulais comprendre profondément, donc je suis un code Chainer. Nous vous prions de nous excuser pour la gêne occasionnée, mais nous vous serions reconnaissants de bien vouloir signaler toute erreur.
―― Le contenu annoncé à PyCon 2016 est plutôt un concept et une base de contour, il n'y a donc pas d'explication du code réellement implémenté, il vaut donc mieux signifier un reflet de vous-même.
«J'ai donc écrit cet article parce que je veux que plus de gens le comprennent et l'utilisent en ajoutant une explication de code. (J'espère qu'il y aura plus d'étoiles sur github si possible)
Docker Hub
https://hub.docker.com/r/masayaresearch/dialogue/
github
https://github.com/SnowMasaya/Chainer-Slack-Twitter-Dialogue
Il existe de nombreux autres domaines tels que les questions et réponses, la classification des sujets et la parallélisation de l'acquisition de données, donc j'écrirai cette partie si demandé.
WHAT
Nous nous entraînons sur les données classifiées. Le modèle d'attention est utilisé même dans le deep learning. Qu'est-ce qu'un modèle d'attention?
Dans la tâche de traduction automatique des réseaux de neurones, le modèle séquence à séquence avait le problème que l'importance du premier mot était diminuée par l'accumulation de différenciation lorsqu'il était agrégé en un vecteur dans l'entrée d'une longue phrase. Surtout en anglais, le premier mot devient plus important.
Afin de résoudre ce problème, dans le passé, la précision de la traduction était améliorée en entrant dans la direction opposée. Cependant, dans le cas du japonais et du chinois, au contraire, le dernier mot est important, donc ce n'est pas une solution essentielle.
Par conséquent, le modèle d'attention a été proposé comme modèle qui prédit la sortie de chaque décodage en pondérant la couche cachée et l'entrée du codage qui correspondent au décodage sans coder et décoder séparément l'entrée. À l'origine, il a réussi dans le domaine des images, mais maintenant il produit des résultats dans les tâches de traduction automatique et de synthèse de phrases.
image
Pour prédire «mo», il s'agit de la post-probabilité lorsque «je» est entré («je suis ingénieur»). La probabilité postérieure est le score du mot précédent (I), l'état de la couche cachée et le vecteur de contexte ("Je suis ingénieur"). Ignorez le vecteur de contexte pour le moment. Je l'expliquerai plus tard. La fonction g est généralement une fonction softmax
Comme le montre la figure ci-dessus, la formule utilisée lors de la prédiction à partir de l'état et du contexte actuels en tenant compte de la sortie précédente est la suivante.
p(y_i|y_1,...y_{i_1}, \vec{x}) = g(y_{i-1}, s_i, c_i)
Ici, l'état de la couche cachée au temps t peut être le suivant. (État pour prédire "mo") Ceci est déterminé par le vecteur de contexte du mot précédent "I", de l'état précédent et du précédent "Je suis ingénieur". La fonction f est généralement sigmoïde
s_i=f(s_{i-1}, y_{i-1},c_i)
Le vecteur de contexte est déterminé par la somme de la couche cachée de la partie encodeur ("Je suis ingénieur") et du poids $ a $.
c_{i} = \sum^{T_x}_{j=1}\alpha_{ij}h_{j}
Ensuite, comment trouver le poids défini précédemment, le poids obtenu à partir de la couche cachée h appelée e et l'état précédent s du côté sortie ("I" dans le cas de "") est utilisé comme score. Cette forme est due au fait que h dans la partie codeur a une forme spéciale. Ce point sera décrit plus loin. Le score e est une petite valeur en raison de la probabilité. Il est rendu une grande valeur par la fonction exp et divisé par toutes les parties d'entrée pour calculer le poids qui correspond à la paire d'entrée et de sortie.
\alpha_{ij} = \frac{exp(e_{ij})}{\sum_{k=1}^{T_x}exp(e_{ik})} \\
e_{ij} = a(s_{i-1}, h_j)
Alors, quelle est la particularité de la couche cachée h? En fait, il diffère d'une séquence normale à une séquence en ce qu'il combine en avant et en arrière. Définissez vers l'avant et vers l'arrière comme indiqué ci-dessous et exprimez-les de manière concaténée. Il s'agit de la couche cachée de l'entrée d'encodage "Je suis ingénieur".
(\vec{h_1},...\vec{h_{T_x}})\\
(\overleftarrow{h_1},...\overleftarrow{h_{T_x}})\\
h_j = [\vec{h_j^T};\overleftarrow{h_j^T}]^T
Suivons comment cette formule est réellement réalisée dans la base de code.
src_embed.py
C'est la partie qui déplace les données de langage dans l'espace du réseau neuronal.
attention_encoder.py --C'est la partie qui propage les informations transférées dans l'espace du réseau neuronal du langage côté entrée. (Cela correspond à la partie vocale de l'utilisateur dans le dialogue)
attention.py
La partie qui crée des informations de contexte
attention_decoder.py
Il s'agit de la partie réseau neuronal du langage de sortie. Il génère même des informations de contexte, la langue cible et propage les couches cachées.
attention_dialogue.py --Charger le modèle
Enregistrer le modèle
Initialisation du poids
Poids d'encastrement
Processus d'encodage
Processus de décodage
Il se compose des cinq ci-dessus.
HOW
src_embed.py
Je vais expliquer à partir de la partie qui intègre les informations de la langue d'entrée. Définit le vocabulaire de la langue d'entrée et le nombre de couches intégrées dans le réseau neuronal. Le vocabulaire de la langue d'entrée est le discours de l'utilisateur dans le cas d'un dialogue.
def __init__(self, vocab_size, embed_size):
super(SrcEmbed, self).__init__(
weight_xi=links.EmbedID(vocab_size, embed_size),
)
Ce sera le contenu d'un traitement spécifique.
W (~ chainer.Variable)
est une matrice intégrée de chainer.Variable.
Utilise les poids initiaux générés à partir d'une distribution normale avec une moyenne de 0 et une variance de 1,0.
def __init__(self, in_size, out_size, initialW=None, ignore_label=None):
super(EmbedID, self).__init__(W=(in_size, out_size))
if initialW is None:
initialW = initializers.Normal(1.0)
initializers.init_weight(self.W.data, initialW)
self.ignore_label = ignore_label
Plus précisément, c'est la partie qui génère des données à partir de la distribution normale.
Puisque la partie xp
est lorsque vous utilisez gpu
, nous utilisons xp.random.normal
au lieu de numpy.random.normal
.
référence https://docs.scipy.org/doc/numpy/reference/generated/numpy.random.normal.html
class Normal(initializer.Initializer):
def __init__(self, scale=0.05, dtype=None):
self.scale = scale
super(Normal, self).__init__(dtype)
def __call__(self, array):
xp = cuda.get_array_module(array)
array[...] = xp.random.normal(
loc=0.0, scale=self.scale, size=array.shape)
Le poids initial à renvoyer ici est défini ci-dessous.
Les données de ʻinitializer sont définies par les données ou la classe de
numpy.ndarrayou la classe de
cupy.ndarray`.
def init_weight(weights, initializer, scale=1.0):
if initializer is None:
initializer = HeNormal(1 / numpy.sqrt(2))
elif numpy.isscalar(initializer):
initializer = Constant(initializer)
elif isinstance(initializer, numpy.ndarray):
initializer = Constant(initializer)
assert callable(initializer)
initializer(weights)
weights *= scale
Lorsque ʻinitializer est différent de
None`, il retourne s'il s'agit d'un tableau au format gpu ou d'un tableau normal.
class Constant(initializer.Initializer):
def __init__(self, fill_value, dtype=None):
self.fill_value = fill_value
super(Constant, self).__init__(dtype)
def __call__(self, array):
if self.dtype is not None:
assert array.dtype == self.dtype
xp = cuda.get_array_module(array)
array[...] = xp.asarray(self.fill_value)
Les pièces spécifiquement évaluées et renvoyées sont les suivantes.
def get_array_module(*args):
if available:
return cupy.get_array_module(*args)
else:
return numpy
La fonction __call__
appelle src_embed pour incorporer la langue d'entrée dans l'espace du réseau neuronal.
Il est mappé à un espace divisible en utilisant une fonction bipolaire dans functions.tanh
. S'il s'agit d'un espace divisible, l'apprentissage est possible par propagation en retour d'erreur.
def __call__(self, source):
return functions.tanh(self.weight_xi(source))
attention_encoder.py
La couche d'entrée mappée dans l'espace du réseau neuronal est transmise à la couche cachée. Pourquoi 4 fois
Porte d'entrée Porte de l'oubli Porte de sortie Porte qui prend en compte l'entrée précédente
C'est parce que les quatre ci-dessus sont pris en considération. Je n'entrerai pas dans les détails sur les raisons pour lesquelles cela est nécessaire car il est mentionné dans d'autres documents, mais cet appareil empêche le sur-apprentissage.
def __init__(self, embed_size, hidden_size):
super(AttentionEncoder, self).__init__(
source_to_hidden=links.Linear(embed_size, 4 * hidden_size),
hidden_to_hidden=links.Linear(hidden_size, 4 * hidden_size),
)
Il s'agit d'un processus de lien spécifique.
def __init__(self, in_size, out_size, wscale=1, bias=0, nobias=False,
initialW=None, initial_bias=None):
super(Linear, self).__init__()
self.initialW = initialW
self.wscale = wscale
self.out_size = out_size
self._W_initializer = initializers._get_initializer(initialW, math.sqrt(wscale))
if in_size is None:
self.add_uninitialized_param('W')
else:
self._initialize_params(in_size)
if nobias:
self.b = None
else:
if initial_bias is None:
initial_bias = bias
bias_initializer = initializers._get_initializer(initial_bias)
self.add_param('b', out_size, initializer=bias_initializer)
def _initialize_params(self, in_size):
self.add_param('W', (self.out_size, in_size), initializer=self._W_initializer)
Il s'agit d'un processus d'initialisation spécifique.
Puisque l'échelle est 1 par défaut, multipliez-la pour créer un tableau.
Avec la Constant
sortie plus tôt, la valeur initiale est initialisée avec une valeur fixe et mise à l'échelle.
class _ScaledInitializer(initializer.Initializer):
def __init__(self, initializer, scale=1.0):
self.initializer = initializer
self.scale = scale
dtype = getattr(initializer, 'dtype', None)
super(Identity, self).__init__(dtype)
def __call__(self, array):
self.initializer(array)
array *= self.scale
def _get_initializer(initializer, scale=1.0):
if initializer is None:
return HeNormal(scale / numpy.sqrt(2))
if numpy.isscalar(initializer):
return Constant(initializer * scale)
if isinstance(initializer, numpy.ndarray):
return Constant(initializer * scale)
assert callable(initializer)
if scale == 1.0:
return initializer
return _ScaledInitializer(initializer, scale)
Nous transmettons l'état actuel, la valeur de la couche cachée précédente et la valeur de la couche d'entrée.
def __call__(self, source, current, hidden):
return functions.lstm(current, self.source_to_hidden(source) + self.hidden_to_hidden(hidden))
Le traitement appelé lors du traitement avant dans le lstm ci-dessus est le suivant.
Le fichier sera chainer / functions / activation / lstm.py
.
L'entrée est divisée en quatre portes de lstm.
len (x)
: Obtenir la longueur de la ligne
x.shape [1]
: Obtenir la longueur de la colonne
x.shape [2:]
: Utilisé pour les données de 3 dimensions ou plus
def _extract_gates(x):
r = x.reshape((len(x), x.shape[1] // 4, 4) + x.shape[2:])
return [r[:, :, i] for i in six.moves.range(4)]
traitement cpu
--Obtenir l'entrée et l'état
Le traitement de gpu est le même, mais comme C ++ est utilisé, il est lu en utilisant la définition suivante. C'est le même que lstm défini sur python, mais il est écrit pour être traité en C ++.
_preamble = '''
template <typename T> __device__ T sigmoid(T x) {
const T half = 0.5;
return tanh(x * half) * half + half;
}
template <typename T> __device__ T grad_sigmoid(T y) { return y * (1 - y); }
template <typename T> __device__ T grad_tanh(T y) { return 1 - y * y; }
#define COMMON_ROUTINE \
T aa = tanh(a); \
T ai = sigmoid(i_); \
T af = sigmoid(f); \
T ao = sigmoid(o);
'''
def forward(self, inputs):
c_prev, x = inputs
a, i, f, o = _extract_gates(x)
batch = len(x)
if isinstance(x, numpy.ndarray):
self.a = numpy.tanh(a)
self.i = _sigmoid(i)
self.f = _sigmoid(f)
self.o = _sigmoid(o)
c_next = numpy.empty_like(c_prev)
c_next[:batch] = self.a * self.i + self.f * c_prev[:batch]
h = self.o * numpy.tanh(c_next[:batch])
else:
c_next = cuda.cupy.empty_like(c_prev)
h = cuda.cupy.empty_like(c_next[:batch])
cuda.elementwise(
'T c_prev, T a, T i_, T f, T o', 'T c, T h',
'''
COMMON_ROUTINE;
c = aa * ai + af * c_prev;
h = ao * tanh(c);
''',
'lstm_fwd', preamble=_preamble)(
c_prev[:batch], a, i, f, o, c_next[:batch], h)
c_next[batch:] = c_prev[batch:]
self.c = c_next[:batch]
return c_next, h
Le traitement de gpu est le suivant. Le processus pour appeler le contenu de cuda est le suivant. J'utilise Cupy. À propos de Cupy
http://docs.chainer.org/en/stable/cupy-reference/overview.html
Créez une fonction du noyau ci-dessous, mettez-la en cache dans la mémoire de cuda et liez le résultat avec le périphérique de cuda. Voir ci-dessous la raison pour laquelle les valeurs calculées dans l'espace mémoire gpu doivent être liées.
http://www.nvidia.com/docs/io/116711/sc11-cuda-c-basics.pdf
@memoize(for_each_device=True)
def elementwise(in_params, out_params, operation, name, **kwargs):
check_cuda_available()
return cupy.ElementwiseKernel(
in_params, out_params, operation, name, **kwargs)
En cas de rétrogradation, le traitement est le suivant.
C'est utile car le chainer cache le traitement ici.
Identique au traitement direct, mais la différence est qu'il utilise non seulement l'entrée mais également la sortie de gradient.
Avec gc_prev [: batch]
, le produit du calque caché et du calque de sortie est mis à jour en ajoutant le dégradé à la taille du lot.
Le gradient est calculé et mis à jour avec _grad_tanh
et _grad_sigmoid
.
co = numpy.tanh(self.c)
gc_prev = numpy.empty_like(c_prev)
# multiply f later
gc_prev[:batch] = gh * self.o * _grad_tanh(co) + gc_update
gc = gc_prev[:batch]
ga[:] = gc * self.i * _grad_tanh(self.a)
gi[:] = gc * self.a * _grad_sigmoid(self.i)
gf[:] = gc * c_prev[:batch] * _grad_sigmoid(self.f)
go[:] = gh * co * _grad_sigmoid(self.o)
gc_prev[:batch] *= self.f # multiply f here
gc_prev[batch:] = gc_rest
C'est la partie traitement de gpu.
Identique au traitement de cpu, mais calculé en utilisant cuda.elementwise
pour passer C ++.
a, i, f, o = _extract_gates(x)
gc_prev = xp.empty_like(c_prev)
cuda.elementwise(
'T c_prev, T c, T gc, T gh, T a, T i_, T f, T o',
'T gc_prev, T ga, T gi, T gf, T go',
'''
COMMON_ROUTINE;
T co = tanh(c);
T temp = gh * ao * grad_tanh(co) + gc;
ga = temp * ai * grad_tanh(aa);
gi = temp * aa * grad_sigmoid(ai);
gf = temp * c_prev * grad_sigmoid(af);
go = gh * co * grad_sigmoid(ao);
gc_prev = temp * af;
''',
'lstm_bwd', preamble=_preamble)(
c_prev[:batch], self.c, gc_update, gh, a, i, f, o,
gc_prev[:batch], ga, gi, gf, go)
gc_prev[batch:] = gc_rest
attention.py
C'est la partie qui contient les informations de contexte.
--ʻAnnotion_weight est le poids de la partie avant --
back_weight est le poids de la partie arrière, --
pw` est le poids du calque actuel
weight_exponential
de traiter la fonction exp dans le réseau neuronal def __init__(self, hidden_size):
super(Attention, self).__init__(
annotion_weight=links.Linear(hidden_size, hidden_size),
back_weight=links.Linear(hidden_size, hidden_size),
pw=links.Linear(hidden_size, hidden_size),
weight_exponential=links.Linear(hidden_size, 1),
)
self.hidden_size = hidden_size
ʻAnnotion_listest une liste de mots en avant
back_word_listest une liste de mots en arrière
p` est le poids du calque actuel
def __call__(self, annotion_list, back_word_list, p):
Initialisation pour le traitement par lots
batch_size = p.data.shape[0]
exponential_list = []
sum_exponential = XP.fzeros((batch_size, 1))
Créez un poids qui combine la liste de mots avant, la liste de mots back_word et l'état actuel du calque Équivalent à ce qui suit
e_{ij} = a(s_{i-1}, h_j)
Répertoriez chaque valeur en autorisant la fonction exp à traiter les valeurs qui y sont obtenues. Calculez également la valeur totale
\alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T_x}\exp(e_{ik})} \\
Etant donné que le traitement est effectué dans les deux sens, la liste d'annotations depuis la direction avant et la liste arrière depuis la direction arrière sont acquises et le poids est calculé en incluant le poids actuel. Créez une liste de poids pour la fonction exp. Calculer la somme des fonctions exp
for annotion, back_word in zip(annotion_list, back_word_list):
weight = functions.tanh(self.annotion_weight(annotion) + self.back_weight(back_word) + self.pw(p))
exponential = functions.exp(self.weight_exponential(weight))
exponential_list.append(exponential)
sum_exponential += exponential
L'initialisation est effectuée, les poids avant et arrière sont calculés et les valeurs calculées par la matrice avant et arrière sont préparées et renvoyées pour la taille du lot. Le calcul de la matrice se fait avec functions.batch_matmul
.
ʻAest la matrice de gauche
best la bonne matrice S'il y a un "transa", la matrice de gauche est transposée. S'il y a un
transb`, transposez la bonne matrice
def batch_matmul(a, b, transa=False, transb=False):
return BatchMatMul(transa=transa, transb=transb)(a, b)
Contenu du calcul matriciel réel --Convertir la matrice en une forme calculable Transforme la matrice pour qu'elle puisse être calculée élément par élément
a = a.reshape(a.shape[:2] + (-1,))
Quand il y a une ligne comme celle ci-dessous
array([[1, 2, 3],
[4, 5, 6],
[3, 4, 5]])
Il est converti comme suit.
array([[[1],
[2],
[3]],
[[4],
[5],
[6]],
[[3],
[4],
[5]]])
matmul
. matmul
n'autorise pas les calculs scalaires et les processus en empilant des matrices sur une pile.def _batch_matmul(a, b, transa=False, transb=False, transout=False):
a = a.reshape(a.shape[:2] + (-1,))
b = b.reshape(b.shape[:2] + (-1,))
trans_axis = (0, 2, 1)
if transout:
transa, transb = not transb, not transa
a, b = b, a
if transa:
a = a.transpose(trans_axis)
if transb:
b = b.transpose(trans_axis)
xp = cuda.get_array_module(a)
if xp is numpy:
ret = numpy.empty(a.shape[:2] + b.shape[2:], dtype=a.dtype)
for i in six.moves.range(len(a)):
ret[i] = numpy.dot(a[i], b[i])
return ret
return xp.matmul(a, b)
Initialise avec une matrice nulle pour la taille du lot et la taille de la matrice, et retourne la somme calculée par ʻannotion et
back_word`.
ZEROS = XP.fzeros((batch_size, self.hidden_size))
annotion_value = ZEROS
back_word_value = ZEROS
# Calculate the Convolution Value each annotion and back word
for annotion, back_word, exponential in zip(annotion_list, back_word_list, exponential_list):
exponential /= sum_exponential
annotion_value += functions.reshape(functions.batch_matmul(annotion, exponential), (batch_size, self.hidden_size))
back_word_value += functions.reshape(functions.batch_matmul(back_word, exponential), (batch_size, self.hidden_size))
return annotion_value, back_word_value
attention_decoder.py
Ce sera la partie sortie. Dans le cas du dialogue, c'est la réponse du système.
C'est plus compliqué que de taper.
ʻEmbed_vocab: La partie qui mappe le langage de sortie à l'espace du réseau neuronal ʻEmbed_hidden
: La partie qui propage la valeur du réseau neuronal vers LSTM
hidden_hidden
: Propagation d'une partie du calque caché
ʻAnnotation_hidden: Vecteur de contexte de type avant
back_word_hidden: vecteur de contexte de type backword
hidden_embed: Propagation de la couche cachée à la couche de sortie (correspondant à la réponse du système) ʻEmbded_target
: Propagation de la couche de sortie vers la sortie du système (correspondant à la réponse du système)
super(AttentionDecoder, self).__init__(
embed_vocab=links.EmbedID(vocab_size, embed_size),
embed_hidden=links.Linear(embed_size, 4 * hidden_size),
hidden_hidden=links.Linear(hidden_size, 4 * hidden_size),
annotation_hidden=links.Linear(embed_size, 4 * hidden_size),
back_word_hidden=links.Linear(hidden_size, 4 * hidden_size),
hidden_embed=links.Linear(hidden_size, embed_size),
embded_target=links.Linear(embed_size, vocab_size),
)
Utilisez une fonction bipolaire divisible qui mappe le mot de sortie à un calque masqué Prédisez l'état et la couche cachée en donnant à lsm la somme de la couche cachée, de la couche cachée, du vecteur de contexte en avant et du vecteur de contexte en arrière du mot de sortie. Prédire la couche cachée pour la sortie avec une fonction bipolaire divisible à l'aide de la couche cachée prédite précédemment Prédire les mots de sortie en utilisant la couche cachée pour la sortie, retourner l'état actuel, la couche cachée
embed = functions.tanh(self.embed_vocab(target))
current, hidden = functions.lstm(current, self.embed_hidden(embed) + self.hidden_hidden(hidden) +
self.annotation_hidden(annotation) + self.back_word_hidden(back_word))
embed_hidden = functions.tanh(self.hidden_embed(hidden))
return self.embded_target(embed_hidden), current, hidden
attention_dialogue.py
C'est la partie qui effectue un traitement de dialogue spécifique.
Nous utiliserons les quatre modèles décrits précédemment.
ʻEmbmappe la langue d'entrée dans l'espace du réseau neuronal.
forward_encode: encode en avant et prépare le vecteur de contexte pour la création.
back_encdode: encodé en arrière pour préparer le vecteur de contexte pour la création. ʻAttention
: Préparé pour l'attention
dec
: Préparé pour les mots pour la sortie
Il détermine la taille du vocabulaire, la taille à mapper sur l'espace du réseau neuronal, la taille de la couche cachée et s'il faut utiliser gpu dans XP.
super(AttentionDialogue, self).__init__(
emb=SrcEmbed(vocab_size, embed_size),
forward_encode=AttentionEncoder(embed_size, hidden_size),
back_encdode=AttentionEncoder(embed_size, hidden_size),
attention=Attention(hidden_size),
dec=AttentionDecoder(vocab_size, embed_size, hidden_size),
)
self.vocab_size = vocab_size
self.embed_size = embed_size
self.hidden_size = hidden_size
self.XP = XP
Il est initialisé à zéro gradient.
def reset(self):
self.zerograds()
self.source_list = []
La langue d'entrée (discours de l'utilisateur) est conservée sous forme de liste de mots.
def embed(self, source):
self.source_list.append(self.emb(source))
ʻEncodeC'est la partie pour le traitement. Seule la partie unidimensionnelle du langage d'entrée est utilisée pour obtenir la taille du lot. Figure J'initialise, mais comme la valeur d'initialisation est différente entre gpu et cpu, j'utilise
self.XP.fzeros`.
Je reçois une liste de renvois pour créer un vecteur de contexte avant.
Backward fait de même.
def encode(self):
batch_size = self.source_list[0].data.shape[0]
ZEROS = self.XP.fzeros((batch_size, self.hidden_size))
context = ZEROS
annotion = ZEROS
annotion_list = []
# Get the annotion list
for source in self.source_list:
context, annotion = self.forward_encode(source, context, annotion)
annotion_list.append(annotion)
context = ZEROS
back_word = ZEROS
back_word_list = []
# Get the back word list
for source in reversed(self.source_list):
context, back_word = self.back_encdode(source, context, back_word)
back_word_list.insert(0, back_word)
self.annotion_list = annotion_list
self.back_word_list = back_word_list
self.context = ZEROS
self.hidden = ZEROS
Obtenez le vecteur de contexte pour chacune des couches d'attention avant, arrière et cachée. Renvoie le mot de sortie avec le mot cible, le contexte (obtenu par dec), la valeur de la couche masquée, la valeur avant et la valeur arrière.
def decode(self, target_word):
annotion_value, back_word_value = self.attention(self.annotion_list, self.back_word_list, self.hidden)
target_word, self.context, self.hidden = self.dec(target_word, self.context, self.hidden, annotion_value, back_word_value)
return target_word
Enregistrer le modèle Il stocke la taille du vocabulaire, la taille de mappage de la couche latente et la taille de la couche cachée.
def save_spec(self, filename):
with open(filename, 'w') as fp:
print(self.vocab_size, file=fp)
print(self.embed_size, file=fp)
print(self.hidden_size, file=fp)
La partie de chargement du modèle. La valeur lue ici est acquise et transmise au modèle.
def load_spec(filename, XP):
with open(filename) as fp:
vocab_size = int(next(fp))
embed_size = int(next(fp))
hidden_size = int(next(fp))
return AttentionDialogue(vocab_size, embed_size, hidden_size, XP)
EncoderDecoderModelAttention.py
En fait, utilisez le module expliqué plus haut dans cette partie Différents paramètres sont définis.
def __init__(self, parameter_dict):
self.parameter_dict = parameter_dict
self.source = parameter_dict["source"]
self.target = parameter_dict["target"]
self.test_source = parameter_dict["test_source"]
self.test_target = parameter_dict["test_target"]
self.vocab = parameter_dict["vocab"]
self.embed = parameter_dict["embed"]
self.hidden = parameter_dict["hidden"]
self.epoch = parameter_dict["epoch"]
self.minibatch = parameter_dict["minibatch"]
self.generation_limit = parameter_dict["generation_limit"]
self.word2vec = parameter_dict["word2vec"]
self.word2vecFlag = parameter_dict["word2vecFlag"]
self.model = parameter_dict["model"]
self.attention_dialogue = parameter_dict["attention_dialogue"]
XP.set_library(False, 0)
self.XP = XP
Mise en œuvre du traitement aval. Il obtient la taille de la cible et de la source et obtient l'index de chacun.
def forward_implement(self, src_batch, trg_batch, src_vocab, trg_vocab, attention, is_training, generation_limit):
batch_size = len(src_batch)
src_len = len(src_batch[0])
trg_len = len(trg_batch[0]) if trg_batch else 0
src_stoi = src_vocab.stoi
trg_stoi = trg_vocab.stoi
trg_itos = trg_vocab.itos
attention.reset()
La langue d'entrée est saisie dans la direction opposée. Si vous entrez dans la direction opposée, le résultat de la traduction automatique sera amélioré, donc le dialogue est sous la même forme, mais je pense que cela n'a aucun effet.
x = self.XP.iarray([src_stoi('</s>') for _ in range(batch_size)])
attention.embed(x)
for l in reversed(range(src_len)):
x = self.XP.iarray([src_stoi(src_batch[k][l]) for k in range(batch_size)])
attention.embed(x)
attention.encode()
Initialisez la chaîne de langue cible que vous souhaitez obtenir avec <s>
.
t = self.XP.iarray([trg_stoi('<s>') for _ in range(batch_size)])
hyp_batch = [[] for _ in range(batch_size)]
C'est la partie d'apprentissage.
Les informations de langue ne peuvent pas être apprises à moins qu'il ne s'agisse d'informations d'index, alors utilisez stoi
pour changer la langue pour indexer les informations.
Obtenez la cible (dans ce cas, la sortie du dialogue) et comparez-la aux données correctes pour calculer l'entropie croisée.
Puisque l'entropie croisée donne la distance entre les distributions de probabilité, on peut voir que plus la perte de ce calcul est faible, plus le résultat de sortie est proche de la cible.
Il renvoie des candidats hypothétiques et des pertes calculées.
if is_training:
loss = self.XP.fzeros(())
for l in range(trg_len):
y = attention.decode(t)
t = self.XP.iarray([trg_stoi(trg_batch[k][l]) for k in range(batch_size)])
loss += functions.softmax_cross_entropy(y, t)
output = cuda.to_cpu(y.data.argmax(1))
for k in range(batch_size):
hyp_batch[k].append(trg_itos(output[k]))
return hyp_batch, loss
Ceci est la partie test.
Le réseau neuronal peut générer des candidats infinis, et surtout dans le cas du modèle lstm, puisqu'il utilise l'état passé, il peut entrer dans une boucle infinie, il est donc limité.
Sortie à l'aide de la chaîne de mots cible initialisée.
La valeur maximale des données de sortie est sortie et «t» est mis à jour.
La sortie des candidats pour la taille du lot est convertie des informations d'index en informations de langue.
interrompre
le processus si tous les candidats se terminent par un symbole de terminaison </ s>
.
else:
while len(hyp_batch[0]) < generation_limit:
y = attention.decode(t)
output = cuda.to_cpu(y.data.argmax(1))
t = self.XP.iarray(output)
for k in range(batch_size):
hyp_batch[k].append(trg_itos(output[k]))
if all(hyp_batch[k][-1] == '</s>' for k in range(batch_size)):
break
return hyp_batch
C'est le traitement de tout l'apprentissage.
Initialise les énoncés d'entrée et de sortie.
self.vocab
génère un générateur avec gens.word_list
dans tout le vocabulaire.
src_vocab = Vocabulary.new(gens.word_list(self.source), self.vocab)
trg_vocab = Vocabulary.new(gens.word_list(self.target), self.vocab)
Je crée des informations de vocabulaire pour les énoncés d'entrée et de sortie avec Vocabulary.new ()
.
Créez le générateur
suivant avec gens.word_list (self.source)
. Le nom du fichier d'entrée est donné dans self.source
.
def word_list(filename):
with open(filename) as fp:
for l in fp:
yield l.split()
Le processus de conversion des informations de vocabulaire en informations d'index est effectué dans la partie suivante.
<Unk>
il est 0 dans le mot inconnu, <s>
1 avec le préfixe, </ s>
a mis 2 à la fin de la phrase.
Puisque les valeurs sont définies dans ces derniers à l'avance, +3 est ajouté de sorte que l'index se trouve après le mot réservé.
@staticmethod
def new(list_generator, size):
self = Vocabulary()
self.__size = size
word_freq = defaultdict(lambda: 0)
for words in list_generator:
for word in words:
word_freq[word] += 1
self.__stoi = defaultdict(lambda: 0)
self.__stoi['<unk>'] = 0
self.__stoi['<s>'] = 1
self.__stoi['</s>'] = 2
self.__itos = [''] * self.__size
self.__itos[0] = '<unk>'
self.__itos[1] = '<s>'
self.__itos[2] = '</s>'
for i, (k, v) in zip(range(self.__size - 3), sorted(word_freq.items(), key=lambda x: -x[1])):
self.__stoi[k] = i + 3
self.__itos[i + 3] = k
return self
Créer un modèle d'attention. Donne le vocabulaire, la couche intégrée, la couche cachée et «XP». «XP» est la partie qui effectue les calculs CPU et GPU.
trace('making model ...')
self.attention_dialogue = AttentionDialogue(self.vocab, self.embed, self.hidden, self.XP)
Cela fera partie de l'apprentissage par transfert. Ici, le poids créé par word2vec est transféré.
Puisque le nom du poids du modèle créé avec word2vec est weight_xi
, l'énoncé d'entrée est unifié, mais la partie d'énoncé de sortie est différente pour ʻembded_target`, donc le traitement suivant est inclus.
La partie [0] est le nom du poids
La partie [1] est la valeur.
if dst["embded_target"] and child.name == "weight_xi" and self.word2vecFlag:
for a, b in zip(child.namedparams(), dst["embded_target"].namedparams()):
b[1].data = a[1].data
Ceci est une copie du poids.
Tournez l'itération de la pièce d'origine et copiez les poids si les conditions sont remplies.
Condition 1: il y a quelque chose qui correspond au nom donné au poids
Condition 2: les types de poids sont les mêmes
Condition 3: La partie de link.Link
, c'est-à-dire que la partie de modèle a été atteinte.
Condition 4: La longueur de la matrice de poids du modèle est la même
def copy_model(self, src, dst, dec_flag=False):
print("start copy")
for child in src.children():
if dec_flag:
if dst["embded_target"] and child.name == "weight_xi" and self.word2vecFlag:
for a, b in zip(child.namedparams(), dst["embded_target"].namedparams()):
b[1].data = a[1].data
print('Copy weight_jy')
if child.name not in dst.__dict__: continue
dst_child = dst[child.name]
if type(child) != type(dst_child): continue
if isinstance(child, link.Chain):
self.copy_model(child, dst_child)
if isinstance(child, link.Link):
match = True
for a, b in zip(child.namedparams(), dst_child.namedparams()):
if a[0] != b[0]:
match = False
break
if a[1].data.shape != b[1].data.shape:
match = False
break
if not match:
print('Ignore %s because of parameter mismatch' % child.name)
continue
for a, b in zip(child.namedparams(), dst_child.namedparams()):
b[1].data = a[1].data
print('Copy %s' % child.name)
if self.word2vecFlag:
self.copy_model(self.word2vec, self.attention_dialogue.emb)
self.copy_model(self.word2vec, self.attention_dialogue.dec, dec_flag=True)
Créez un générateur d'énoncés d'entrée et de sortie.
gen1 = gens.word_list(self.source)
gen2 = gens.word_list(self.target)
gen3 = gens.batch(gens.sorted_parallel(gen1, gen2, 100 * self.minibatch), self.minibatch)
Créez les deux pour la taille du lot. Créez la taille du lot au format tapple ci-dessous.
def batch(generator, batch_size):
batch = []
is_tuple = False
for l in generator:
is_tuple = isinstance(l, tuple)
batch.append(l)
if len(batch) == batch_size:
yield tuple(list(x) for x in zip(*batch)) if is_tuple else batch
batch = []
if batch:
yield tuple(list(x) for x in zip(*batch)) if is_tuple else batch
Les énoncés d'entrée et les énoncés de sortie sont créés et triés en fonction de la taille des lots.
def sorted_parallel(generator1, generator2, pooling, order=1):
gen1 = batch(generator1, pooling)
gen2 = batch(generator2, pooling)
for batch1, batch2 in zip(gen1, gen2):
#yield from sorted(zip(batch1, batch2), key=lambda x: len(x[1]))
for x in sorted(zip(batch1, batch2), key=lambda x: len(x[order])):
yield x
Adagrad est utilisé pour l'optimisation. C'est une méthode qui réduit la largeur de mise à jour à mesure que le nombre de mises à jour s'accumule.
r ← r + g^2_{\vec{w}}\\
w ← w - \frac{\alpha}{r + }g^2_{\vec{w}}
ʻOptimizer.GradientClipping (5) `utilise la régularisation L2 pour maintenir le gradient dans une certaine plage.
opt = optimizers.AdaGrad(lr = 0.01)
opt.setup(self.attention_dialogue)
opt.add_hook(optimizer.GradientClipping(5))
Ci-dessous, les énoncés de l'utilisateur d'entrée et les énoncés de l'utilisateur correspondants sont remplis avec *
par fill_batch
pour rendre l'apprentissage en profondeur possible.
def fill_batch(batch, token='</s>'):
max_len = max(len(x) for x in batch)
return [x + [token] * (max_len - len(x) + 1) for x in batch]
Le traitement en amont est effectué en utilisant la perte obtenue lors du traitement en aval et le poids est mis à jour.
Le traitement en amont dépend de la fonction d'activation.
La partie mise à jour est la suivante.
Changer si les données sont traitées par GPU ou CPU
L'optimisation par la fonction de perte est modifiée en changeant la manière de donner les données dans tuple
, dict
et autres.
def update_core(self):
batch = self._iterators['main'].next()
in_arrays = self.converter(batch, self.device)
optimizer = self._optimizers['main']
loss_func = self.loss_func or optimizer.target
if isinstance(in_arrays, tuple):
in_vars = tuple(variable.Variable(x) for x in in_arrays)
optimizer.update(loss_func, *in_vars)
elif isinstance(in_arrays, dict):
in_vars = {key: variable.Variable(x)
for key, x in six.iteritems(in_arrays)}
optimizer.update(loss_func, **in_vars)
else:
in_var = variable.Variable(in_arrays)
optimizer.update(loss_func, in_var)
for src_batch, trg_batch in gen3:
src_batch = fill_batch(src_batch)
trg_batch = fill_batch(trg_batch)
K = len(src_batch)
hyp_batch, loss = self.forward_implement(src_batch, trg_batch, src_vocab, trg_vocab, self.attention_dialogue, True, 0)
loss.backward()
opt.update()
Enregistrer le modèle entraîné save et save_spec n'existent pas dans le standard chainer, mais sont créés séparément pour enregistrer les informations sur la langue.
save
enregistre les informations de données vocales
save_spec
enregistre la taille du vocabulaire, la taille du calque intégré, la taille du calque caché
save_hdf5
enregistre le modèle au format hdf5
trace('saving model ...')
prefix = self.model
model_path = APP_ROOT + "/model/" + prefix
src_vocab.save(model_path + '.srcvocab')
trg_vocab.save(model_path + '.trgvocab')
self.attention_dialogue.save_spec(model_path + '.spec')
serializers.save_hdf5(model_path + '.weights', self.attention_dialogue)
Ceci est la partie test. La sortie du modèle pendant l'apprentissage est lue et le contenu de l'énoncé de l'utilisateur pour l'énoncé d'entrée est sorti.
def test(self):
trace('loading model ...')
prefix = self.model
model_path = APP_ROOT + "/model/" + prefix
src_vocab = Vocabulary.load(model_path + '.srcvocab')
trg_vocab = Vocabulary.load(model_path + '.trgvocab')
self.attention_dialogue = AttentionDialogue.load_spec(model_path + '.spec', self.XP)
serializers.load_hdf5(model_path + '.weights', self.attention_dialogue)
trace('generating translation ...')
generated = 0
with open(self.test_target, 'w') as fp:
for src_batch in gens.batch(gens.word_list(self.source), self.minibatch):
src_batch = fill_batch(src_batch)
K = len(src_batch)
trace('sample %8d - %8d ...' % (generated + 1, generated + K))
hyp_batch = self.forward_implement(src_batch, None, src_vocab, trg_vocab, self.attention_dialogue, False, self.generation_limit)
source_cuont = 0
for hyp in hyp_batch:
hyp.append('</s>')
hyp = hyp[:hyp.index('</s>')]
print("src : " + "".join(src_batch[source_cuont]).replace("</s>", ""))
print('hyp : ' +''.join(hyp))
print(' '.join(hyp), file=fp)
source_cuont = source_cuont + 1
generated += K
trace('finished.')
Le contenu a été annoncé à PyCon 2016, mais si vous pensez qu'il en fait toujours partie, il semble qu'il y ait un long chemin à parcourir si vous incluez l'explication des autres parties. À l'heure actuelle, la gamme qui peut être gérée par un simple apprentissage en profondeur est limitée, nous utilisons donc plusieurs technologies. Puisqu'il existe de nombreux modèles de dialogue en apprentissage profond, je pense que cela conduira à une amélioration des performances en déterminant l'indice d'évaluation et en changeant le modèle d'apprentissage profond.
Attention and Memory in Deep Learning and NLP
Recommended Posts