[PYTHON] Expliquer Chat Bot annoncé à PyCon 2016 à partir de la base de code (réponse de chat à l'aide de Chainer)

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)

Screen Shot 2016-12-26 at 8.04.52 AM.png

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

Réponse de chat

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

image.png image.png

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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.

Il se compose des cinq ci-dessus.

HOW

src_embed.py

Screen Shot 2016-12-26 at 9.38.00 AM.png

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 decupy.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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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

    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 untransb`, 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]]])
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

Screen Shot 2016-12-26 at 9.38.00 AM.png

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'utiliseself.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.')

Résumé

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.

référence

Attention and Memory in Deep Learning and NLP

Recommended Posts

Expliquer Chat Bot annoncé à PyCon 2016 à partir de la base de code (réponse de chat à l'aide de Chainer)
Essayez d'écrire du code à partir de 1 en utilisant le chainer du cadre d'apprentissage automatique (édition mnist)
Explication du concept d'analyse de régression à l'aide de python Partie 2
[Question] À propos de la conversion API du chat bot à l'aide de Python
Récupérer le code retour d'un script Python depuis bat
Explication du concept d'analyse de régression à l'aide de Python Partie 1
Analysons les émotions de Tweet en utilisant Chainer (2ème)
Explication du concept d'analyse de régression à l'aide de Python Extra 1
Étude de Python Hour8: Utilisation de packages
Analysons les émotions de Tweet en utilisant Chainer (1er)