web-dev-qa-db-fra.com

Keras utilisant le backend Tensorflow - fonction de masquage sur perte

J'essaie d'implémenter une tâche séquence à séquence à l'aide de LSTM par Keras avec le backend Tensorflow. Les entrées sont des phrases anglaises de longueurs variables. Pour construire un ensemble de données avec une forme 2D [batch_number, max_sentence_length], j'ajoute EOF à la fin de la ligne et complète chaque phrase avec suffisamment d'espaces réservés, par ex. "#". Et puis chaque caractère de la phrase est transformé en un vecteur à chaud, le jeu de données a maintenant une forme 3D [batch_number, max_sentence_length, character_number]. Après les couches de codeur et de décodeur LSTM, l’entropie croisée de softmax entre la sortie et la cible est calculée. 

Pour éliminer l'effet de remplissage dans la formation de modèle, le masquage peut être utilisé pour les fonctions d'entrée et de perte. La saisie de masque dans Keras peut être effectuée à l’aide de "layers.core.Masking". Dans Tensorflow, la fonction de masquage sur perte peut être réalisée comme suit: fonction de perte masquée personnalisée dans Tensorflow

Cependant, je ne trouve aucun moyen de le réaliser dans Keras, car une fonction de perte définie par l'utilisateur utilisée dans keras n'accepte que les paramètres y_true et y_pred. Alors, comment entrer de vraies séquences_lengths à la fonction de perte et au masque?

En outre, je trouve une fonction "_weighted_masked_objective (fn)" dans\keras\engine\training.py. Sa définition est "Ajoute la prise en charge du masquage et de la pondération de l'échantillon à une fonction objectif." Mais il semble que la fonction ne peut accepter que fn (y_true, y_pred). Y a-t-il un moyen d'utiliser cette fonction pour résoudre mon problème?

Pour être précis, je modifie l'exemple de Yu-Yang.

from keras.models import Model
from keras.layers import Input, Masking, LSTM, Dense, RepeatVector, TimeDistributed, Activation
import numpy as np
from numpy.random import seed as random_seed
random_seed(123)

max_sentence_length = 5
character_number = 3 # valid character 'a, b' and placeholder '#'

input_tensor = Input(shape=(max_sentence_length, character_number))
masked_input = Masking(mask_value=0)(input_tensor)
encoder_output = LSTM(10, return_sequences=False)(masked_input)
repeat_output = RepeatVector(max_sentence_length)(encoder_output)
decoder_output = LSTM(10, return_sequences=True)(repeat_output)
output = Dense(3, activation='softmax')(decoder_output)

model = Model(input_tensor, output)
model.compile(loss='categorical_crossentropy', optimizer='adam')
model.summary()

X = np.array([[[0, 0, 0], [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]],
          [[0, 0, 0], [0, 1, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]]])
y_true = np.array([[[0, 0, 1], [0, 0, 1], [1, 0, 0], [0, 1, 0], [0, 1, 0]], # the batch is ['##abb','#babb'], padding '#'
          [[0, 0, 1], [0, 1, 0], [1, 0, 0], [0, 1, 0], [0, 1, 0]]])

y_pred = model.predict(X)
print('y_pred:', y_pred)
print('y_true:', y_true)
print('model.evaluate:', model.evaluate(X, y_true))
# See if the loss computed by model.evaluate() is equal to the masked loss
import tensorflow as tf
logits=tf.constant(y_pred, dtype=tf.float32)
target=tf.constant(y_true, dtype=tf.float32)
cross_entropy = tf.reduce_mean(-tf.reduce_sum(target * tf.log(logits),axis=2))
losses = -tf.reduce_sum(target * tf.log(logits),axis=2)
sequence_lengths=tf.constant([3,4])
mask = tf.reverse(tf.sequence_mask(sequence_lengths,maxlen=max_sentence_length),[0,1])
losses = tf.boolean_mask(losses, mask)
masked_loss = tf.reduce_mean(losses)
with tf.Session() as sess:
    c_e = sess.run(cross_entropy)
    m_c_e=sess.run(masked_loss)
    print("tf unmasked_loss:", c_e)
    print("tf masked_loss:", m_c_e)

Les résultats dans Keras et Tensorflow sont comparés comme suit:  enter image description here

Comme indiqué ci-dessus, le masquage est désactivé après certains types de couches. Alors, comment masquer la fonction de perte dans les keras lorsque ces couches sont ajoutées? 

16
Shuaaai

S'il existe un masque dans votre modèle, il sera propagé couche par couche et finalement appliqué à la perte. Donc, si vous remplissez et masquez les séquences de manière correcte, la perte sur les espaces réservés de remplissage sera ignorée.

Quelques détails:

C'est un peu compliqué d'expliquer tout le processus, alors je vais le décomposer en plusieurs étapes:

  1. Dans compile(), le masque est collecté en appelant compute_mask() et appliqué à la ou aux pertes (les lignes non pertinentes sont ignorées pour plus de clarté). 
weighted_losses = [_weighted_masked_objective(fn) for fn in loss_functions]

# Prepare output masks.
masks = self.compute_mask(self.inputs, mask=None)
if masks is None:
    masks = [None for _ in self.outputs]
if not isinstance(masks, list):
    masks = [masks]

# Compute total loss.
total_loss = None
with K.name_scope('loss'):
    for i in range(len(self.outputs)):
        y_true = self.targets[i]
        y_pred = self.outputs[i]
        weighted_loss = weighted_losses[i]
        sample_weight = sample_weights[i]
        mask = masks[i]
        with K.name_scope(self.output_names[i] + '_loss'):
            output_loss = weighted_loss(y_true, y_pred,
                                        sample_weight, mask)
  1. Dans Model.compute_mask(), run_internal_graph() est appelé.
  2. À l'intérieur de run_internal_graph(), les masques du modèle sont propagés couche par couche à partir des entrées du modèle vers les sorties en appelant Layer.compute_mask() pour chaque couche de manière itérative.

Ainsi, si vous utilisez une couche Masking dans votre modèle, vous ne devez pas vous inquiéter de la perte sur les espaces réservés de remplissage. La perte sur ces entrées sera masquée comme vous l'avez probablement déjà vu dans _weighted_masked_objective().

Un petit exemple:

max_sentence_length = 5
character_number = 2

input_tensor = Input(shape=(max_sentence_length, character_number))
masked_input = Masking(mask_value=0)(input_tensor)
output = LSTM(3, return_sequences=True)(masked_input)
model = Model(input_tensor, output)
model.compile(loss='mae', optimizer='adam')

X = np.array([[[0, 0], [0, 0], [1, 0], [0, 1], [0, 1]],
              [[0, 0], [0, 1], [1, 0], [0, 1], [0, 1]]])
y_true = np.ones((2, max_sentence_length, 3))
y_pred = model.predict(X)
print(y_pred)
[[[ 0.          0.          0.        ]
  [ 0.          0.          0.        ]
  [-0.11980877  0.05803877  0.07880752]
  [-0.00429189  0.13382857  0.19167568]
  [ 0.06817091  0.19093043  0.26219055]]

 [[ 0.          0.          0.        ]
  [ 0.0651961   0.10283815  0.12413475]
  [-0.04420842  0.137494    0.13727818]
  [ 0.04479844  0.17440712  0.24715884]
  [ 0.11117355  0.21645413  0.30220413]]]

# See if the loss computed by model.evaluate() is equal to the masked loss
unmasked_loss = np.abs(1 - y_pred).mean()
masked_loss = np.abs(1 - y_pred[y_pred != 0]).mean()

print(model.evaluate(X, y_true))
0.881977558136

print(masked_loss)
0.881978

print(unmasked_loss)
0.917384

Comme le montre cet exemple, la perte sur la partie masquée (les zéros dans y_pred) est ignorée et la sortie de model.evaluate() est égale à masked_loss.


MODIFIER:

S'il existe un calque récurrent avec return_sequences=False, l'arrêt du masque se propage (c'est-à-dire que le masque renvoyé est None). Dans RNN.compute_mask():

def compute_mask(self, inputs, mask):
    if isinstance(mask, list):
        mask = mask[0]
    output_mask = mask if self.return_sequences else None
    if self.return_state:
        state_mask = [None for _ in self.states]
        return [output_mask] + state_mask
    else:
        return output_mask

Dans votre cas, si je comprends bien, vous voulez un masque basé sur y_true, et chaque fois que la valeur de y_true est [0, 0, 1] (le codage à chaud de "#"), vous souhaitez masquer la perte. Si tel est le cas, vous devez masquer les valeurs de perte d'une manière assez similaire à celle de Daniel.

La principale différence est la moyenne finale. La moyenne doit correspondre au nombre de valeurs non masquées, qui est simplement K.sum(mask). De plus, y_true peut être comparé directement au vecteur codé one-hot [0, 0, 1].

def get_loss(mask_value):
    mask_value = K.variable(mask_value)
    def masked_categorical_crossentropy(y_true, y_pred):
        # find out which timesteps in `y_true` are not the padding character '#'
        mask = K.all(K.equal(y_true, mask_value), axis=-1)
        mask = 1 - K.cast(mask, K.floatx())

        # multiply categorical_crossentropy with the mask
        loss = K.categorical_crossentropy(y_true, y_pred) * mask

        # take average w.r.t. the number of unmasked entries
        return K.sum(loss) / K.sum(mask)
    return masked_categorical_crossentropy

masked_categorical_crossentropy = get_loss(np.array([0, 0, 1]))
model = Model(input_tensor, output)
model.compile(loss=masked_categorical_crossentropy, optimizer='adam')

La sortie du code ci-dessus montre alors que la perte est calculée uniquement sur les valeurs non masquées:

model.evaluate: 1.08339476585
tf unmasked_loss: 1.08989
tf masked_loss: 1.08339

La valeur est différente de la vôtre car j'ai modifié l'argument axis dans tf.reverse de [0,1] à [1].

10
Yu-Yang

Si vous n'utilisez pas de masques comme dans la réponse de Yu-Yang, vous pouvez essayer ceci.

Si vous avez vos données cibles Y avec longueur et complétées avec la valeur de masque, vous pouvez:

import keras.backend as K
def custom_loss(yTrue,yPred):

    #find which values in yTrue (target) are the mask value
    isMask = K.equal(yTrue, maskValue) #true for all mask values

    #since y is shaped as (batch, length, features), we need all features to be mask values
    isMask = K.all(isMask, axis=-1) #the entire output vector must be true
        #this second line is only necessary if the output features are more than 1

    #transform to float (0 or 1) and invert
    isMask = K.cast(isMask, dtype=K.floatx())
    isMask = 1 - isMask #now mask values are zero, and others are 1

    #multiply this by the inputs:
       #maybe you might need K.expand_dims(isMask) to add the extra dimension removed by K.all
     yTrue = yTrue * isMask   
     yPred = yPred * isMask

     return someLossFunction(yTrue,yPred)

Si vous avez un remplissage uniquement pour les données d'entrée ou si Y n'a pas de longueur, vous pouvez avoir votre propre masque en dehors de la fonction:

masks = [
   [1,1,1,1,1,1,0,0,0],
   [1,1,1,1,0,0,0,0,0],
   [1,1,1,1,1,1,1,1,0]
]
 #shape (samples, length). If it fails, make it (samples, length, 1). 

import keras.backend as K

masks = K.constant(masks)

Étant donné que les masques dépendent de vos données d'entrée, vous pouvez utiliser la valeur de votre masque pour savoir où mettre des zéros, tels que:

masks = np.array((X_train == maskValue).all(), dtype='float64')    
masks = 1 - masks

#here too, if you have a problem with dimensions in the multiplications below
#expand masks dimensions by adding a last dimension = 1.

Et faites que votre fonction prenne des masques de l’extérieur (vous devez recréer la fonction de perte si vous modifiez les données d’entrée):

def customLoss(yTrue,yPred):

    yTrue = masks*yTrue
    yPred = masks*yPred

    return someLossFunction(yTrue,yPred)

Est-ce que quelqu'un sait si keras masque automatiquement la fonction de perte ?? Puisqu'elle fournit une couche de masquage et ne dit rien sur les sorties, peut-être qu'il le fait automatiquement

0
Daniel Möller