Implémentation de modèles seq2seq dans Tensorflow

La génération de données à l'aide d'un réseau neuronal récurrent devient une méthode de plus en plus populaire et utilisée dans de nombreux domaines de l'informatique. Depuis le début de la naissance du concept seq2seq en 2014, seulement cinq ans se sont écoulés, mais le monde a vu de nombreuses applications, à commencer par les modèles classiques de traduction et de reconnaissance vocale, et se terminant par la génération de descriptions d'objets dans les photographies.


D'autre part, au fil du temps, la bibliothèque Tensorflow, publiée par Google spécifiquement pour le développement de réseaux de neurones, a gagné en popularité. Naturellement, les développeurs de Google ne pouvaient pas ignorer un paradigme aussi populaire que seq2seq, de sorte que la bibliothèque Tensorflow fournit des classes pour le développement au sein de ce paradigme. Cet article décrit ce système de classes.


Réseaux récurrents


À l'heure actuelle, les réseaux récurrents sont l'un des formalismes les plus connus et les plus pratiques pour construire des réseaux de neurones profonds. Les réseaux récursifs sont conçus pour traiter des données série.Par conséquent, contrairement à une cellule normale (neurone), qui reçoit des données en entrée et génère le résultat des calculs, une cellule récursive contient deux entrées et deux sorties.


L'une des entrées représente les données de l'élément actuel de la séquence, et la deuxième entrée est appelée l' état et est transmise à la suite des calculs de cellule sur l'élément précédent de la séquence.


image

La figure montre la cellule A, pour laquelle les données d'un élément de séquence sont entrées xtainsi que la condition non indiquée ici st1. En sortie, la cellule A donne l'état stet le résultat du calcul ht.


En pratique, la séquence de données est généralement divisée en sous-séquences d'une certaine longueur fixe et transmise au calcul par sous-ensembles entiers (lots). En d'autres termes, les sous-séquences sont des exemples d'apprentissage. Les entrées, sorties et états de cellule d'un réseau récursif sont des séquences de nombres réels. Pour le calcul d'entrée x1il est nécessaire d'utiliser un état qui n'était pas le résultat d'un calcul sur une séquence donnée de données. Ces états sont appelés états initiaux. Si la séquence est suffisamment longue, il est logique de conserver le contexte des calculs sur chaque sous-séquence. Dans ce cas, il est possible de transmettre le dernier état calculé dans la séquence précédente en tant qu'état initial. Si la séquence n'est pas si longue ou si la sous-séquence est le premier segment, vous pouvez initialiser l'état initial avec des zéros.


À l'heure actuelle, pour l'entraînement des réseaux de neurones presque partout, l'algorithme de rétro-propagation des erreurs est utilisé . Le résultat du calcul sur l'ensemble d'exemples transmis (dans notre cas, l'ensemble de sous-séquences) est vérifié par rapport au résultat attendu (données balisées). La différence entre les valeurs réelles et attendues est appelée une erreur et cette erreur est propagée aux poids du réseau dans le sens opposé. Ainsi, le réseau s'adapte aux données étiquetées et, en règle générale, le résultat de cette adaptation fonctionne bien pour les données que le réseau n'a pas rencontrées dans les exemples de formation initiale (hypothèse de généralisation).


Dans le cas d'un réseau récursif, nous avons plusieurs options sur les sorties pour considérer l'erreur. Nous en décrirons ici deux principaux:


  1. Vous pouvez considérer l'erreur en comparant la sortie de la dernière cellule de la sous-séquence avec la sortie attendue. Cela fonctionne bien pour la tâche de classification. Par exemple, nous devons déterminer la coloration émotionnelle d'un tweet. Pour ce faire, nous sélectionnons les tweets et les marquons en trois catégories: négatif, positif et neutre. La sortie de la cellule sera composée de trois nombres - le poids des catégories. Le tweet sera également marqué de trois chiffres - les probabilités de tweet appartenant à la catégorie correspondante. Après avoir calculé l'erreur sur un sous-ensemble des données, vous pouvez la propager à travers la sortie ou l'état comme vous le souhaitez.
  2. Vous pouvez lire l'erreur immédiatement aux sorties du calcul de cellule pour chaque élément de la sous-séquence. Ceci est bien adapté pour la tâche de prédire l'élément suivant d'une séquence à partir des précédents. Une telle approche peut être utilisée, par exemple, dans le problème de la détermination des anomalies dans les séries temporelles de données ou dans la tâche de prédire le caractère suivant dans un texte, afin de le générer plus tard. La propagation des erreurs est également possible via des états ou des sorties.

Contrairement à un réseau neuronal entièrement connecté régulier, un réseau récursif est profond dans le sens où l'erreur se propage non seulement des sorties du réseau à ses poids, mais aussi à gauche, via des connexions entre les états. La profondeur du réseau est ainsi déterminée par la longueur de la sous-séquence. Pour propager l'erreur à travers l'état du réseau récursif, il existe un algorithme spécial. Sa particularité est que les gradients des poids se multiplient entre eux, lorsque l'erreur se propage de droite à gauche. Si l'erreur initiale est supérieure à l'unité, en conséquence, l'erreur peut devenir très importante. Inversement, si l'erreur initiale est inférieure à l'unité, quelque part au début de la séquence, l'erreur peut s'estomper. Cette situation dans la théorie des réseaux de neurones est appelée carrousel d'erreur standard. Afin d'éviter de telles situations pendant l'entraînement, des cellules spéciales ont été inventées qui ne présentent pas de tels inconvénients. La première cellule de ce type était le LSTM , il existe maintenant une large gamme d'alternatives, dont le GRU le plus populaire.


Une bonne introduction aux réseaux de récurrence peut être trouvée dans cet article . Une autre source bien connue est un article du blog d'Andrey Karpaty.


La bibliothèque Tensorflow possède de nombreuses classes et fonctions pour implémenter des réseaux récursifs. Voici un exemple de création d'un réseau récursif dynamique basé sur une cellule de type GRU:


cell = tf.contrib.rnn.GRUCell(dimension) outputs, state = tf.nn.dynamic_rnn(cell, input, sequence_length=input_length, dtype=tf.float32) 

Dans cet exemple, une cellule GRU est créée, qui est ensuite utilisée pour créer un réseau récursif dynamique. Le tenseur de données d'entrée et les longueurs réelles des sous-séquences sont transmis au réseau. Les données d'entrée sont toujours spécifiées par un vecteur de nombres réels. Pour une seule valeur, par exemple, un code de symbole ou un mot, le soi-disant incorporation - mappage de ce code à une séquence de nombres. La fonction de création d'un réseau récursif dynamique renvoie une paire de valeurs: une liste de sorties réseau pour toutes les valeurs de la séquence et le dernier état calculé. En entrée, la fonction prend une cellule, des données d'entrée et un tenseur de longueur de sous-séquence.


Un réseau récursif dynamique diffère d'un réseau statique en ce qu'il ne crée pas à l'avance un réseau de cellules de réseau pour la sous-séquence (au stade de la détermination du graphe de calcul), mais lance dynamiquement les cellules des entrées lors du calcul du graphe sur les données d'entrée. Par conséquent, cette fonction doit connaître les longueurs des sous-séquences des données d'entrée afin de s'arrêter au bon moment.


Génération de modèles basés sur des réseaux de récurrence


Génération de réseaux de récurrence


Plus tôt, nous avons considéré deux méthodes de calcul des erreurs des réseaux récursifs: à la dernière sortie ou à toutes les sorties pour une séquence donnée. Nous considérons ici le problème de la génération de séquences. La formation du réseau de générateurs est basée sur la deuxième méthode de ce qui précède.


Plus en détail, nous essayons de former un réseau récursif pour prédire le prochain élément d'une séquence. Comme mentionné ci-dessus, la sortie d'une cellule dans un réseau récursif est simplement une séquence de nombres. Ce vecteur n'est pas très pratique pour l'apprentissage, par conséquent, ils introduisent un autre niveau, qui reçoit ce vecteur en entrée et en sortie donne le poids des prédictions. Ce niveau est appelé niveau de projection et vous permet de comparer la sortie de la cellule sur un élément donné de la séquence avec la sortie attendue dans les données étiquetées.


Pour illustrer, considérez la tâche de générer du texte qui est représenté comme une séquence de caractères. La longueur du vecteur de sortie du niveau de projection est égale à la taille de l'alphabet du texte source. La taille de l'alphabet ne dépasse généralement pas 150 caractères, si l'on compte les caractères des langues russe et anglaise, ainsi que les signes de ponctuation. La sortie du niveau de projection est un vecteur avec la longueur de l'alphabet, où chaque symbole correspond à une certaine position dans ce vecteur - l'indice de ce symbole. Les données étiquetées sont également un vecteur composé de zéros, où l'on se trouve à la position du caractère suivant la séquence.


Pour la formation, nous utilisons deux séquences de données:


  1. Une séquence de caractères dans le texte source, au début de laquelle est ajouté un caractère spécial qui ne fait pas partie du texte source. Il est généralement appelé go .
  2. La séquence de caractères du texte source tel quel, sans ajouts.

Exemple pour le texte "maman a lavé le cadre":


 ['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', ''] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', ''] 

Pour la formation, des minibatches sont généralement formés, consistant en un petit nombre d'exemples. Dans notre cas, ce sont des chaînes qui peuvent être de différentes longueurs. Le code décrit ci-dessous utilise la méthode suivante pour résoudre le problème des différentes longueurs. Parmi les nombreuses lignes de ce mini-paquet, la longueur maximale est calculée. Toutes les autres lignes sont remplies d'un caractère spécial (remplissage) afin que tous les exemples du mini-paquet aient la même longueur. Dans l'exemple de code ci-dessous, la chaîne de pavé est utilisée comme un tel caractère. De plus, pour une meilleure génération, à la fin de l'exemple, ajoutez le symbole de fin de phrase - eos . Ainsi, en réalité, les données de l'exemple seront un peu différentes:


 ['<go>', '', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>'] ['', '', ', '', ' ', '', '', '', '', ' ', '', '', '', '', '<eos>', '<pad>', '<pad>', '<pad>', '<pad>'] 

La première séquence est envoyée à l'entrée du réseau et la seconde séquence est utilisée comme données étiquetées. L'entraînement à la prédiction est basé sur le décalage de la séquence d'origine d'un caractère vers la gauche.


Formation et frai


La formation


L'algorithme d'apprentissage est assez simple. Pour chaque élément de la séquence d'entrée, nous calculons le vecteur de sortie de son niveau de projection et le comparons avec celui marqué. La seule question est de savoir comment calculer l'erreur. Vous pouvez utiliser l'erreur quadratique moyenne, mais pour calculer l'erreur dans cette situation, il est préférable d'utiliser l' entropie croisée . La bibliothèque Tensorflow fournit plusieurs fonctions pour son calcul, bien que rien n'arrête l'implémentation de la formule de calcul directement dans le code.


Pour plus de clarté, nous introduisons une notation. Par symbol_id nous désignerons l'identifiant du symbole (son numéro de série dans l'alphabet). Le terme symbole ici est plutôt arbitraire et signifie simplement un élément de l'alphabet. L'alphabet peut ne pas contenir de symboles, mais des mots ou même des ensembles d'attributs plus complexes. Le terme symbol_embedding sera utilisé pour désigner le vecteur de nombres correspondant à un élément donné de l'alphabet. En règle générale, ces ensembles de nombres sont stockés dans une table de taille qui correspond à la taille de l'alphabet.


Tensorflow fournit une fonctionnalité qui vous permet d'accéder à la table d'intégration et de remplacer les indices de caractères par leurs vecteurs d'intégration. Tout d'abord, nous définissons une variable pour stocker la table:


 embedding_table = tf.Variable(tf.random_uniform([alphabet_size, embedding_size])) 

Après cela, vous pouvez convertir les tenseurs d'entrée en tenseurs d'intégration:


 input_embeddings = tf.nn.embedding_lookup(embedding_table, input_ids) 

Le résultat de l'appel de fonction est un tenseur de la même dimension qui a été transféré à l'entrée, mais en conséquence, tous les indices de caractères sont remplacés par les séquences d'intégration correspondantes.


Spawn


Pour calculer, une cellule d'un réseau récursif a besoin d'un état et du caractère courant. Le résultat du calcul est une sortie et un nouvel état. Si nous appliquons le niveau de projection à la sortie, nous pouvons obtenir un vecteur de poids où le poids à la position correspondante peut être considéré (très conditionnellement) comme la probabilité que ce symbole apparaisse à la position suivante dans la séquence.


Différentes stratégies peuvent être utilisées pour sélectionner le symbole suivant en fonction du vecteur de poids généré par le niveau de projection:


  • Stratégie de recherche gourmande. Chaque fois que nous sélectionnons le symbole avec le poids le plus élevé, c'est-à-dire très probablement dans cette situation, mais pas nécessairement le plus approprié dans le contexte de la séquence entière.
  • Stratégie pour choisir la meilleure séquence (recherche de faisceau). Nous ne sélectionnons pas un symbole à la fois, mais nous nous souvenons de plusieurs variantes des symboles les plus probables. Une fois toutes ces options calculées pour tous les éléments de la séquence générée, nous sélectionnons la séquence de caractères la plus probable, en tenant compte du contexte de la séquence entière. Habituellement, cela est mis en œuvre au moyen d'une matrice dont la largeur est égale à la longueur de la séquence et la hauteur au nombre de largeurs de génération de faisceau. Une fois la génération des variantes de séquence terminée, l'une des variantes de l'algorithme de Viterbi est utilisée pour sélectionner la séquence la plus probable.

Bibliothèque Tensorflow système de type seq2seq


Compte tenu de ce qui précède, il est clair que la mise en œuvre de modèles génératifs basés sur des réseaux de récurrence est une tâche assez difficile à coder. Par conséquent, naturellement, des systèmes de classes ont été proposés pour faciliter la solution de ce problème. L'un de ces systèmes est appelé seq2seq, puis nous décrivons la fonctionnalité de ses principaux types.


Mais, tout d'abord, quelques mots sur le nom de la bibliothèque. Le nom seq2seq est l'abréviation de séquence à séquence (de séquence à séquence). L'idée originale de générer une séquence a été proposée pour la mise en œuvre d'un système de traduction. La séquence d'entrée de mots a été transmise à l'entrée d'un réseau récursif, appelé codeur dans ce système. La sortie de ce réseau récursif était l'état du calcul de cellule sur le dernier caractère de la séquence. Cet état a été présenté comme l'état initial du deuxième réseau récursif, le décodeur, qui a été formé pour générer le mot suivant. Les mots ont été utilisés comme symboles dans les deux réseaux. Des erreurs sur le décorateur ont été propagées à l'encodeur via l'état transmis. Le vecteur d'état lui-même dans cette terminologie était appelé le vecteur de pensée. La présentation intermédiaire a été utilisée dans les modèles de traduction traditionnels et, en règle générale, était un graphique représentant la structure du texte d'entrée pour la traduction. Le système de traduction a généré un texte de sortie basé sur cette structure intermédiaire.


En fait, l'implémentation de seq2seq dans Tensorflow appartient à la partie décodeur, sans affecter l'encodeur. Par conséquent, il serait juste d'appeler la bibliothèque 2seq, mais la force de la tradition et l'inertie de la pensée l'emportent évidemment sur le bon sens.


Les deux principaux métatypes de la bibliothèque seq2seq sont:


  1. Classe d' aide .
  2. Décodeur de classe.

Les développeurs de bibliothèque ont identifié ces types sur la base des considérations suivantes. Considérons le processus d'apprentissage et le processus de génération, que nous avons décrits ci-dessus, sous un angle légèrement différent.


Pour la formation dont vous avez besoin:


  1. Pour chaque caractère, passez le calcul de l'état courant et l'incorporation du caractère courant.
  2. N'oubliez pas l'état de sortie et la projection calculés pour la sortie.
  3. Obtenez le caractère suivant dans la séquence et passez à l'étape 1.

Après cela, vous pouvez commencer à compter les erreurs en comparant les résultats des calculs avec les caractères suivants de la séquence.


Pour le générer il faut:


  1. Pour chaque caractère, passez le calcul de l'état courant et l'incorporation du caractère courant.
  2. N'oubliez pas l'état de sortie et la projection calculés pour la sortie.
  3. Calculez le caractère suivant comme le maximum des indices de niveau de projection et passez à l'étape 1.

Comme le montre la description, les algorithmes sont très similaires. Par conséquent, les développeurs de la bibliothèque ont décidé d'encapsuler la procédure d'obtention du caractère suivant dans la classe Helper. Pour l'entraînement, il s'agit simplement de lire le caractère suivant de la séquence, et pour le générer, sélectionner le caractère avec le poids maximum (bien sûr, pour une recherche gourmande).


Par conséquent, la classe de base Helper implémente la méthode next_inputs pour obtenir le caractère suivant du courant et de l'état, ainsi que la méthode d'exemple pour obtenir les indices de caractère du niveau de projection. La classe TrainingHelper est fournie pour la formation et la classe GreedyEmbeddingHelper est disponible pour la génération gourmande. Malheureusement, le modèle de recherche de faisceau ne rentre pas dans ce système de type, par conséquent, une classe spéciale BeamSearchDecoder est implémentée dans la bibliothèque pour cela. n'utilise pas Helper.


La classe Decoder fournit une interface pour implémenter un décodeur. En fait, la classe propose deux méthodes:


  1. initialiser pour initialiser au début du travail.
  2. étape pour mettre en œuvre une étape d'apprentissage ou une génération. Le contenu de cette étape est déterminé par l'assistant correspondant.

La bibliothèque implémente la classe BasicDecoder , qui peut être utilisée à la fois pour la formation et pour la reproduction avec les assistants TrainingHelper et GreedyEmbeddingHelper. Ces trois classes sont généralement suffisantes pour implémenter des modèles de génération basés sur des réseaux de récurrence.


Enfin, les fonctions dynamic_decode sont utilisées pour organiser le passage à travers une entrée ou une séquence générée.


Ensuite, nous considérerons un exemple illustratif, qui montre des méthodes pour construire des modèles de génération pour différents types de bibliothèque seq2seq.


Exemple illustratif


Tout d'abord, il faut dire que tous les exemples sont implémentés en Python 2.7. Une liste de bibliothèques supplémentaires se trouve dans le fichier requirements.txt.


À titre d'exemple illustratif, considérons une partie des données du concours Défi de normalisation de texte - langue russe organisé par Kaggle by Google en 2017. Le but de ce concours était de convertir le texte russe en une forme adaptée à la lecture. Le texte du concours a été décomposé en expressions dactylographiées. Les données d'entraînement ont été spécifiées dans un fichier CSV de la forme suivante:


 "sentence_id","token_id","class","before","after" 0,0,"PLAIN","","" 0,1,"PLAIN","","" 0,2,"PLAIN","","" 0,3,"DATE","1862 ","    " 0,4,"PUNCT",".","." 1,0,"PLAIN","","" 1,1,"PLAIN","","" 1,2,"PLAIN","","" 1,3,"PLAIN","","" 1,4,"PLAIN","","" 1,5,"PLAIN","","" 1,6,"PLAIN","","" 1,7,"PLAIN","","" 1,8,"PLAIN","","" 1,9,"PUNCT",".","." ... 

Dans l'exemple ci-dessus, une expression de type DATE est intéressante: "1862" se traduit par "mille huit cent soixante-deuxième année". Pour illustrer, nous considérons les données de type DATE uniquement comme des paires de la forme (expression avant, expression après). Début du fichier de données:


 before,after 1862 ,     1811 ,    12  2013,      15  2013,      1905 ,    17  2014,      7  2010 ,      1 ,  1843 ,     30  2007 ,      1846 ,     1996 ,     9 ,  ... 

Nous allons construire le modèle générateur à l'aide de la bibliothèque seq2seq, dans laquelle l'encodeur sera implémenté au niveau du symbole (c'est-à-dire que les éléments de l'alphabet sont des symboles), et le décodeur utilisera les mots comme alphabet. Un exemple de code, comme les données, est disponible dans le référentiel sur Github .


Les données de formation sont divisées en trois sous-ensembles: train.csv, test.csv et dev.csv, pour la vérification de la formation, des tests et du recyclage, respectivement. Les données se trouvent dans le répertoire de données. Trois modèles sont implémentés dans le référentiel: seq2seq_greedy.py, seq2seq_attention.py et seq2seq_beamsearch.py. Ici, nous regardons le code du modèle de recherche gourmand de base.


Tous les modèles utilisent la classe Estimator pour implémenter. L'utilisation de cette classe vous permet de simplifier le codage sans être distrait par des pièces non modèles. Par exemple, il n'est pas nécessaire d'implémenter un cycle de transfert de données pour la formation, de créer des sessions pour travailler avec Tensorflow, de penser à transférer des données vers Tensorboard, etc. Estimator ne nécessite que deux fonctions pour sa mise en œuvre: pour le transfert de données et pour la construction d'un modèle. Les exemples utilisent également la classe Dataset pour transmettre des données pour le traitement. Cette implémentation moderne est beaucoup plus rapide que les dictionnaires traditionnels pour le transfert de données feed_dict.


Génération de données


Considérez un code de génération de données pour la formation et la génération.


 def parse_fn(line_before, line_after): # Encode in Bytes for TF source = [c.encode('utf8') for c in line_before.decode('utf8').rstrip('\n')] t = [w.encode('utf8') for w in nltk.word_tokenize(line_after.decode('utf8').strip())] learn_target = t + ['<eos>'] + ['<pad>'] target = ['<go>'] + t + ['<eos>'] return (source, len(source)), (target, learn_target, len(target)) def generator_fn(data_file): with open(data_file, 'rb') as f: reader = csv.DictReader(f, delimiter=',', quotechar='"') for row in reader: yield parse_fn(row['before'], row['after']) def input_fn(data_file, params=None): params = params if params is not None else {} shapes = (([None], ()), ([None], [None], ())) types = ((tf.string, tf.int32), (tf.string, tf.string, tf.int32)) defaults = (('<pad>', 0), ('<pad>', '<pad>', 0)) dataset = tf.data.Dataset.from_generator(functools.partial(generator_fn, data_file), output_shapes=shapes, output_types=types) dataset = dataset.repeat(params['epochs']) return (dataset.padded_batch(params.get('batch_size', 50), shapes, defaults).prefetch(1)) 

La fonction input_fn est utilisée pour créer une collection de données que Estimator transmet ensuite à la formation et à la génération. Le type de données est défini en premier. Il s'agit d'une paire de la forme ((séquence codeur, longueur), (séquence décodeur, séquence décodeur avec un préfixe, longueur)). La chaîne "" est utilisée comme préfixe, chaque séquence d'encodeur se termine par un mot spécial "". De plus, étant donné que les séquences (à la fois en entrée et en sortie) ont une longueur inégale, le symbole de remplissage avec la valeur "" est utilisé.

Le code de préparation des données lit le fichier de données, divise la chaîne d'encodeur en caractères et la chaîne de décodeur en mots, en utilisant la bibliothèque nltk pour cela. Une ligne ainsi traitée est un exemple de données d'apprentissage. La collection générée est divisée en mini-packages et la quantité de données est clonée en fonction du nombre d'époques de formation (chaque époque correspond à une passe de données).


Travailler avec des dictionnaires


Les dictionnaires sont stockés sous forme de liste dans des fichiers, une ligne pour un mot ou un caractère. Pour créer des dictionnaires, utilisez le script build_vocabs.py. Les dictionnaires générés se trouvent dans le répertoire de données sous forme de fichiers de la forme vocab. *. Txt.


Code de lecture des dictionnaires:


 # Read vocabs and inputs dropout = params['dropout'] source, source_length = features training = (mode == tf.estimator.ModeKeys.TRAIN) vocab_source = tf.contrib.lookup.index_table_from_file(vocabulary_file=params['source_vocab_file'], num_oov_buckets=params['num_oov_buckets']) with open(params['source_vocab_file']) as f: num_sources = sum(1 for _ in f) + params['num_oov_buckets'] vocab_target = tf.contrib.lookup.index_table_from_file(vocabulary_file=params['target_vocab_file'], num_oov_buckets=params['num_oov_buckets']) with open(params['target_vocab_file']) as f: num_targets = sum(1 for _ in f) + params['num_oov_buckets'] 

Ici, probablement, la fonction index_table_from_file est intéressante, qui lit les éléments du dictionnaire d'un fichier, et son paramètre num_oov_buckets est le nombre de paniers de vocabulaire. Par défaut, ce nombre est égal à un, c'est-à-dire tous les mots qui ne sont pas dans le dictionnaire ont le même indice égal à la taille du dictionnaire + 1. Nous avons trois mots inconnus: "", "" et "", pour lesquels nous voulons avoir des index différents. Par conséquent, définissez ce paramètre sur le numéro trois. Malheureusement, vous devez relire le fichier d'entrée pour obtenir le nombre de mots dans le dictionnaire comme constante de temps pour définir le graphique du modèle.

Nous devons encore créer une table pour implémenter l'incorporation - _source_embedding, ainsi que la traduction des chaînes de mots en chaînes d'identification:


 # source embeddings matrix _source_embedding = tf.Variable(tf.random_uniform([num_sources, params['embedding_size']])) source_ids = vocab_source.lookup(source) source_embedding = tf.nn.embedding_lookup(_source_embedding, source_ids) 

Implémentation de l'encodeur


Pour l'encodeur, nous utiliserons un réseau récursif bidirectionnel à plusieurs niveaux. , , .


 # add multilayer bidirectional RNN cell_fw = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) cell_bw = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) outputs, states = tf.nn.bidirectional_dynamic_rnn(cell_fw, cell_bw, source_embedding, sequence_length=source_length, dtype=tf.float32) # prepare output output = tf.concat(outputs, axis=-1) encoder_output = tf.layers.dense(output, params['dim']) # prepare state state_fw, state_bw = states cells = [] for fw, bw in zip(state_fw, state_bw): state = tf.concat([fw, bw], axis=-1) cells += [tf.layers.dense(state, params['dim'])] encoder_state = tuple(cells) 

GRU, MultiRNNCell, , rnn.Cell. ,
sequence_length — , , .


, , , . , 128, 256. , , 128. .


. Parce que , , bidirectional_dynamic_rnn, , . , . , .. . , , . , , .



, . .


  # decoder RNN cell decoder_cell = tf.contrib.rnn.MultiRNNCell([tf.contrib.rnn.GRUCell(params['dim']) for _ in range(params['layers'])]) decoder_initial_state = encoder_state # projection layer projection_layer = tf.layers.Dense(num_targets, use_bias=False) # embedding table for targets target_embedding = tf.Variable(tf.random_uniform([num_targets, params['embedding_size']])) 

La formation


TrainingHelper + BasicDecoder.


  # target embeddings matrix target, learn_target, target_length = labels target_ids = vocab_target.lookup(target) target_learn_ids = vocab_target.lookup(learn_target) # train encoder _target_embedding = tf.nn.embedding_lookup(target_embedding, target_ids) train_helper = tf.contrib.seq2seq.TrainingHelper(_target_embedding, target_length) train_decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, train_helper, decoder_initial_state, output_layer=projection_layer) train_outputs, _, _ = tf.contrib.seq2seq.dynamic_decode(train_decoder) train_output = train_outputs.rnn_output train_sample_id = train_outputs.sample_id 


.


 # prediction decoder prediction_helper = tf.contrib.seq2seq.GreedyEmbeddingHelper( embedding=target_embedding, start_tokens=tf.fill([batch_size], tf.to_int32(vocab_target.lookup(tf.fill([], '<go>')))), end_token=tf.to_int32(vocab_target.lookup(tf.fill([], '<eos>')))) prediction_decoder = tf.contrib.seq2seq.BasicDecoder(decoder_cell, prediction_helper, decoder_initial_state, output_layer=projection_layer) prediction_output, _, _ = tf.contrib.seq2seq.dynamic_decode(prediction_decoder, maximum_iterations=params['max_iters']) # prepare prediction reverse_vocab_target = tf.contrib.lookup.index_to_string_table_from_file(params['target_vocab_file']) pred_strings = reverse_vocab_target.lookup(tf.to_int64(prediction_output.sample_id)) predictions = { 'ids': prediction_output.sample_id, 'text': pred_strings } 

GreedyEmbeddingHelper "", "". . , , dynamic_decode . , , . , , .


, seq2seq.


 # loss masks = tf.sequence_mask(lengths=target_length, dtype=tf.float32) loss = tf.contrib.seq2seq.sequence_loss(logits=train_output, targets=target_learn_ids, weights=masks) 

, , sequence_mask.


Adam , .


 optimizer = tf.train.AdamOptimizer(learning_rate=params.get('lr', .001)) grads, vs = zip(*optimizer.compute_gradients(loss)) grads, gnorm = tf.clip_by_global_norm(grads, params.get('clip', .5)) train_op = optimizer.apply_gradients(zip(grads, vs), global_step=tf.train.get_or_create_global_step()) 


. 0.9 . , , , . , .


 24  1944                 1  2003              1992 .           11  1927               1969            1  2016             1047          1863            17      22  2014               

. — , — , — .


, — . . , ( ), . . , .


Conclusion


seq2seq. , , . , .


. Tensorflow , , . , , . , . , , padding , embedding ? , , . — . , , . , , , . , . , , , , .

Source: https://habr.com/ru/post/fr440472/


All Articles