Keras嵌入层中的mask_zero是如何工作的?

37

我以为 mask_zero=True 会在输入值为0时输出0,这样以下的层可以跳过计算或者其他操作。

mask_zero 是如何工作的?

例子:

data_in = np.array([
  [1, 2, 0, 0]
])
data_in.shape
>>> (1, 4)

# model
x = Input(shape=(4,))
e = Embedding(5, 5, mask_zero=True)(x)

m = Model(inputs=x, outputs=e)
p = m.predict(data_in)
print(p.shape)
print(p)
实际输出为:(数字是随机的)
(1, 4, 5)
[[[ 0.02499047  0.04617121  0.01586803  0.0338897   0.009652  ]
  [ 0.04782704 -0.04035913 -0.0341589   0.03020919 -0.01157228]
  [ 0.00451764 -0.01433611  0.02606953  0.00328832  0.02650392]
  [ 0.00451764 -0.01433611  0.02606953  0.00328832  0.02650392]]]

然而,我认为输出将会是:

[[[ 0.02499047  0.04617121  0.01586803  0.0338897   0.009652  ]
  [ 0.04782704 -0.04035913 -0.0341589   0.03020919 -0.01157228]
  [ 0 0 0 0 0]
  [ 0 0 0 0 0]]]

1
它们正在重复上一步骤的输出。文档向你保证它不再“计算”它们了。由于在所有剩余的步骤中它们都是相同的,很可能只是一个虚拟的重复,以填充一个numpy数组的形状。 - Daniel Möller
想知道为什么它们不是零。它们是如何计算的? - GRS
在tf.keras文档https://www.tensorflow.org/guide/keras/masking_and_padding中有一个关于mask_zero如何工作以及传播效果的优秀说明。 - khuang834
除非“掩蔽支持”(在CNN层中不支持)参见,否则似乎只有这种方法可行... - profPlum
2个回答

48
实际上,为嵌入层设置mask_zero=True不会返回零向量。相反,嵌入层的行为不会改变,它将返回索引为零的嵌入向量。您可以通过检查嵌入层权重(例如,在您提到的示例中,它将是m.layers[0].get_weights())来确认这一点。相反,它会影响后续层的行为,如RNN层。
如果您检查嵌入层的源代码,您会看到一个名为compute_mask的方法:
def compute_mask(self, inputs, mask=None):
    if not self.mask_zero:
        return None
    output_mask = K.not_equal(inputs, 0)
    return output_mask

该输出掩码将被传递作为mask参数,用于支持掩码的以下层。这已经在基础层Layer__call__方法中实现:

# Handle mask propagation.
previous_mask = _collect_previous_mask(inputs)
user_kwargs = copy.copy(kwargs)
if not is_all_none(previous_mask):
    # The previous layer generated a mask.
    if has_arg(self.call, 'mask'):
        if 'mask' not in kwargs:
            # If mask is explicitly passed to __call__,
            # we should override the default mask.
            kwargs['mask'] = previous_mask

这使得以下层忽略(即在计算中不考虑)这些输入步骤。下面是一个最简示例:

data_in = np.array([
  [1, 0, 2, 0]
])

x = Input(shape=(4,))
e = Embedding(5, 5, mask_zero=True)(x)
rnn = LSTM(3, return_sequences=True)(e)

m = Model(inputs=x, outputs=rnn)
m.predict(data_in)

array([[[-0.00084503, -0.00413611,  0.00049972],
        [-0.00084503, -0.00413611,  0.00049972],
        [-0.00144554, -0.00115775, -0.00293898],
        [-0.00144554, -0.00115775, -0.00293898]]], dtype=float32)

正如您所看到的,LSTM层在第二和第四个时间步的输出与第一和第三个时间步的输出相同。这意味着这些时间步已被掩盖。

更新:考虑到掩码,损失函数也将被考虑在内,因为内部使用weighted_masked_objective来支持掩码。

def weighted_masked_objective(fn):
    """Adds support for masking and sample-weighting to an objective function.
    It transforms an objective function `fn(y_true, y_pred)`
    into a sample-weighted, cost-masked objective function
    `fn(y_true, y_pred, weights, mask)`.
    # Arguments
        fn: The objective function to wrap,
            with signature `fn(y_true, y_pred)`.
    # Returns
        A function with signature `fn(y_true, y_pred, weights, mask)`.
    """

在编译模型时

weighted_losses = [weighted_masked_objective(fn) for fn in loss_functions]
您可以使用以下示例进行验证:
data_in = np.array([[1, 2, 0, 0]])
data_out = np.arange(12).reshape(1,4,3)

x = Input(shape=(4,))
e = Embedding(5, 5, mask_zero=True)(x)
d = Dense(3)(e)

m = Model(inputs=x, outputs=d)
m.compile(loss='mse', optimizer='adam')
preds = m.predict(data_in)
loss = m.evaluate(data_in, data_out, verbose=0)
print(preds)
print('Computed Loss:', loss)

[[[ 0.009682    0.02505393 -0.00632722]
  [ 0.01756451  0.05928303  0.0153951 ]
  [-0.00146054 -0.02064196 -0.04356086]
  [-0.00146054 -0.02064196 -0.04356086]]]
Computed Loss: 9.041069030761719

# verify that only the first two outputs 
# have been considered in the computation of loss
print(np.square(preds[0,0:2] - data_out[0,0:2]).mean())

9.041070036475277

谢谢,那么在模型评估时会发生什么呢?这是否意味着我们需要将输出向量向右移动1个单位?二元分类即当 y 在 0 和 1 之间。或者假设损失是通过掩码计算的,我们如何在最后实际评估这样的生成器?当我们运行预测时,我们仍然会得到一个输出 y,我们需要手动适配掩码吗?例如,如果我们将序列填充到长度为100,那么 y 总是100,但实际序列的长度是可变的。我们如何让 model.predict() 返回这些可变长度的序列? - GRS
所以我的意思是,当我将这个输出传递给不支持掩码的 Dense 层时,它仍然会计算你示例中第一个和第三个索引的一些损失。并将其与 y 的 0 进行比较。如何实现这样的 Dense 层? - GRS
嗨@today,感谢您的出色回答。您对忽略具有多个特征的自编码器中解码器的填充/缺失时间步骤有什么建议吗? - A.B
你说:“第二和第四个时间步与第一个和第三个时间步的输出相同”,但我看到的是第一和第二个时间步相同,第三和第四个时间步也相同。这不是真的吗? - Amin Shn
@AminShn 这正是我所说的 :) 注意到我句子末尾的“分别”;即第二个输出 == 第一个输出,第四个输出 == 第三个输出(我并没有说第二个 == 第四个或第一个 == 第三个)... 我不确定,但也许我可以写得更清楚些。 - today
显示剩余2条评论

8
通知模型要忽略某些数据中的填充部分的过程称为掩码(Masking)
在Keras模型中引入输入掩码(input masks)有三种方法:
  1. 添加keras.layers.Masking层。
  2. mask_zero=True配置到keras.layers.Embedding层中。
  3. 在调用支持此参数的层时手动传递掩码参数(例如,RNN层)。
下面是使用keras.layers.Embedding引入输入掩码(Input Masks)的代码。
import numpy as np

import tensorflow as tf

from tensorflow.keras import layers

raw_inputs = [[83, 91, 1, 645, 1253, 927],[73, 8, 3215, 55, 927],[711, 632, 71]]
padded_inputs = tf.keras.preprocessing.sequence.pad_sequences(raw_inputs,
                                                              padding='post')

print(padded_inputs)

embedding = layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)
masked_output = embedding(padded_inputs)

print(masked_output._keras_mask)

以上代码的输出如下所示:

[[  83   91    1  645 1253  927]
 [  73    8 3215   55  927    0]
 [ 711  632   71    0    0    0]]

tf.Tensor(
[[ True  True  True  True  True  True]
 [ True  True  True  True  True False]
 [ True  True  True False False False]], shape=(3, 6), dtype=bool)

更多信息,请参考这个Tensorflow教程


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接