【问题标题】:How do I mask a loss function in Keras with the TensorFlow backend?如何使用 TensorFlow 后端屏蔽 Keras 中的损失函数?
【发布时间】:2018-04-13 22:02:29
【问题描述】:

我正在尝试使用 Keras 的 LSTM 和 TensorFlow 后端来实现序列到序列的任务。输入是长度可变的英语句子。为了构建具有二维形状[batch_number, max_sentence_length] 的数据集,我在行尾添加EOF 并用足够的占位符填充每个句子,例如#。然后将句子中的每个字符转换为 one-hot 向量,从而使数据集具有 3-D 形状[batch_number, max_sentence_length, character_number]。在 LSTM 编码器和解码器层之后,计算输出和目标之间的 softmax 交叉熵。

为了消除模型训练中的填充效应,可以对输入和损失函数使用掩码。 Keras 中的掩码输入可以通过使用layers.core.Masking 来完成。在 TensorFlow 中,可以对损失函数进行掩码,如下所示:custom masked loss function in TensorFlow

但是,我没有找到在 Keras 中实现它的方法,因为 Keras 中用户定义的损失函数只接受参数y_truey_pred。那么如何在损失函数和掩码中输入真sequence_lengths呢?

此外,我在\keras\engine\training.py 中找到了一个函数_weighted_masked_objective(fn)。它的定义是

为目标函数添加对掩蔽和样本加权的支持。

但似乎该函数只能接受fn(y_true, y_pred)。有没有办法使用这个功能来解决我的问题?

具体来说,我修改了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)

Keras 和 TensorFlow 中的输出对比如下:

如上所示,在某些图层之后会禁用遮罩。那么当这些层被添加时,如何在 Keras 中掩盖损失函数呢?

【问题讨论】:

  • 您想要动态屏蔽吗?
  • @MarcinMożejko 如果“动态掩码”是指根据模型的不同输入数据对损失函数进行掩码,是的,这就是我想要的。

标签: keras lstm masking loss-function


【解决方案1】:

我采用了两种方法,并为多个时间步、单个缺失目标值、LSTM(或其他 RecurrentNN)的损失以及 return_sequences=True 提供了一种方法。

由于isMask = K.all(isMask, axis=-1),Daniels Answer 无法满足多个目标。删除此聚合可能会使函数不可微。我不知道舒尔,因为我从不运行纯函数,也无法判断它是否适合模型。

我将 Yu-Yangs 和 Daniels 的答案融合在一起,效果很好。


from tensorflow.keras.layers import Layer, Input, LSTM, Dense, TimeDistributed
from tensorflow.keras import Model, Sequential
import tensorflow.keras.backend as K
import numpy as np


mask_Value = -2
def get_loss(mask_value):
    mask_value = K.variable(mask_value)
    def masked_loss(yTrue,yPred):
        
        #find which values in yTrue (target) are the mask value
        isMask = K.equal(yTrue, mask_Value) #true for all mask values
    
        #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
        isMask
        
        #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
        
        # perform a root mean square error, whereas the mean is in respect to the mask
        mean_loss = K.sum(K.square(yPred - yTrue))/K.sum(isMask)
        loss = K.sqrt(mean_loss)
    
        return loss
        #RootMeanSquaredError()(yTrue,yPred)
        
    return masked_loss

# define timeseries data
n_sample = 10
timesteps = 5
feat_inp = 2
feat_out = 2

X = np.random.uniform(0,1, (n_sample, timesteps, feat_inp))
y = np.random.uniform(0,1, (n_sample,timesteps, feat_out))

# define model
model = Sequential()
model.add(LSTM(50, activation='relu',return_sequences=True, input_shape=(timesteps, feat_inp)))
model.add(Dense(feat_out))
model.compile(optimizer='adam', loss=get_loss(mask_Value))
model.summary()

# %%
model.fit(X, y, epochs=50, verbose=0)

【讨论】:

    【解决方案2】:

    如果您的模型中有掩码,它将逐层传播并最终应用于损失。因此,如果您以正确的方式填充和屏蔽序列,则填充占位符上的损失将被忽略。

    一些细节:

    解释整个过程有点牵强,所以我将它分解为几个步骤:

    1. compile() 中,通过调用compute_mask() 收集掩码并将其应用于损失(为清楚起见,忽略不相关的行)。
    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. Model.compute_mask()内部,run_internal_graph()被调用。
    2. run_internal_graph() 内部,模型中的掩码通过对每一层迭代调用Layer.compute_mask(),从模型的输入逐层传播到输出。

    因此,如果您在模型中使用Masking 层,则不必担心填充占位符的丢失。正如您可能已经在 _weighted_masked_objective() 中看到的那样,这些条目的损失将被掩盖。

    一个小例子:

    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
    

    从这个例子可以看出,被屏蔽部分的损失(y_pred 中的零)被忽略了,model.evaluate() 的输出等于masked_loss


    编辑:

    如果存在带有return_sequences=False 的循环层,则掩码停止传播(即,返回的掩码为None)。在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
    

    在您的情况下,如果我理解正确,您需要一个基于 y_true 的掩码,并且每当 y_true 的值是 [0, 0, 1](“#”的单热编码)时,您想要丢失被掩盖。如果是这样,您需要以与 Daniel 的回答有些相似的方式掩盖损失值。

    主要区别在于最终平均值。平均值应超过未屏蔽值的数量,即K.sum(mask)。而且,y_true 可以直接与 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')
    

    然后上面代码的输出表明损失只在未屏蔽的值上计算:

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

    该值与您的不同,因为我已将 tf.reverse 中的 axis 参数从 [0,1] 更改为 [1]

    【讨论】:

    • 感谢您的回复。是的,这可以在 LSTM 中的 return_sequences=True 时工作。但是在encoder-decoder模型中,encoder中的LSTM一般设置return_sequences=False,使用RepeatVector重复最后一个单元的输出,然后decoder中的LSTM接受。具体来说,我修改了您的小示例以显示问题。我将通过下面的“回答我的问题”来展示它,因为评论不能太长。
    • @Shuaaai 啊,通过 seq2seq,我以为你的意思是像 example 中的模型。我已经更新了答案。请看看这是不是你想要的。
    • 首先,非常感谢您。是的,我想要一个基于y_true 的面具。我运行您更新的代码,它会引发错误“ValueError:尺寸必须相等,但对于输入形状为 [2,5,3], [3, 1]。”这是由于版本不同还是其他原因造成的?
    • 我的错。我粘贴了错误的代码。它现在应该可以工作了。
    • 仍有错误“ValueError: initial_value must have a shape specified: Tensor("dense_1_target:0", shape=(?, ?, ?), dtype=float32)"。也许我犯了一些错误?
    【解决方案3】:

    如果你没有像 Yu-Yang 的回答那样使用面具,你可以试试这个。

    如果您的目标数据Y 带有长度并用掩码值填充,您可以:

    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)
    

    如果您仅对输入数据进行填充,或者如果 Y 没有长度,则可以在函数外部使用自己的掩码:

    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)
    

    由于掩码取决于您的输入数据,因此您可以使用掩码值来知道在哪里放置零,例如:

    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.
    

    并使您的函数从外部获取掩码(如果您更改输入数据,则必须重新创建损失函数):

    def customLoss(yTrue,yPred):
    
        yTrue = masks*yTrue
        yPred = masks*yPred
    
        return someLossFunction(yTrue,yPred)
    

    有谁知道 keras 是否会自动屏蔽损失函数? 由于它提供了一个遮罩层并且没有说明输出,所以它可能会自动执行它?

    【讨论】:

    • 丹尼尔 - 这是一个非常糟糕的答案。长度上的掩码被动态分配给y_truey_pred,因此您无法在外部定义它——因为这样的掩码正在改变。如果您以您提供的方式执行此操作 - 这将最终以一个不变的掩码结束 - 这不是 OP 所期望的。
    • @MarcinMożejko,非常感谢。我的回答确实不好。
    • 相比于Yu-Yang的还是不好,但如果他们不使用遮罩层,它可能适用。
    • 如果您在模型函数中定义自定义损失,您仍然可以访问掩码张量。所以这个答案是有效的。
    • @DanielMöller 在您的 customLoss 片段中:如果掩码将某些 yTrue 和 yPred 值设置为零,这是否意味着 yTrue=yPred 并且损失会人为地上升?
    猜你喜欢
    • 2021-11-21
    • 1970-01-01
    • 2018-01-26
    • 2019-12-10
    • 2019-01-20
    • 2019-10-13
    • 2017-12-01
    • 2017-04-21
    • 2018-12-26
    相关资源
    最近更新 更多