详解keras中的Mask机制

一. Mask背景

在NLP中,mask使用最为常见。在NLP中,许多句子都有着不同的长度,我们往往需要按照一定的长度, 对句子进行填补和截取操作。 一般使用keras.preprocessing.sequence包中的pad_sequences方法, 在句子前面或者后面补0. 但是这些零是我们不需要的, 只是为了组成可以计算的结构才填补的. 因此计算过程中, 我们希望用mask的思想, 既然所有样本现在都具有了统一长度,那就必须告知模型,数据的某些部分实际上是填充,应该忽略。这种机制就是遮盖(Mask)

1.2 例子1

在这里插入图片描述
如图中所示,Hello world只有两个字符,而I am here有三个字符。因此在转化成list的时候,就需要在第一个list中加入0变成与第二个list相同长度。

1.2 例子2

这里用简单的向量来描述padding的原理。假设有一个长度为5的向量:
x=[1,0,3,4,5]

经过padding变成长度为8: x=[1,0,3,4,5,0,0,0]

当你将这个长度为8的向量输入到模型中时,模型并不知道你这个向量究竟是“长度为8的向量”还是“长度为5的向量,填充了3个无意义的0”。为了表示出哪些是有意义的,哪些是padding的,我们还需要一个mask向量(矩阵):m=[1,1,1,1,1,0,0,0]

这是一个0/1向量(矩阵),用1表示有意义的部分(True),用0表示无意义的padding部分(False)。
所谓mask,就是xm的运算,来排除padding带来的效应。比如我们要求x的均值,本来期望的结果是:
a v g ( x ) = 1 + 0 + 3 + 4 + 55 5 = 12.6 avg(x)=\frac{1+0+3+4+55}{5}=12.6 avg(x)=51+0+3+4+55=12.6
但是由于向量已经经过padding,直接算的话就得到:
a v g ( x ) = 1 + 0 + 3 + 4 + 55 + 0 + 0 + 0 8 = 7.875 avg(x)=\frac{1+0+3+4+55+0+0+0}{8}=7.875 avg(x)=81+0+3+4+55+0+0+0=7.875
会带来偏差。更严重的是,对于同一个输入,每次padding的零的数目可能是不固定的,因此同一个样本每次可能得到不同的均值,这是很不合理的。有了mask向量m之后,我们可以重写求均值的运算:
a v g ( x ) = s u m ( x ⊗ m ) s u m ( m ) avg(x)=\frac{sum(x⊗m)}{sum(m)} avg(x)=sum(m)sum(xm)
这里的⊗是逐位对应相乘的意思。这样一来,分子只对非padding部分求和,分母则是对非padding部分计数,不管你padding多少个零,最终算出来的结果都是一样的。

二. 原理

在keras中, Tensor在各层之间传递, Layer对象接受的上层Layer得到的Tensor, 输出经过处理后的Tensor。Keras是用一个mask矩阵来参与到计算当中, 决定在计算中屏蔽哪些位置的值。 因此mask矩阵其中的值就是True/False, 其形状一般与对应的Tensor相同. 同样与Tensor相同的是, mask矩阵也会在每层Layer被处理, 得到传入到下一层的mask情况。

mask矩阵如上文例2中,m=[1,1,1,1,1,0,0,0]所示,其中1为True,0为False。

三. 方式

在 Keras 模型中引入输入掩码(Mask)有三种方式:

  • 使用 mask_zero=True 配置一个keras.layers.Embedding 层。
  • 添加一个 keras.layers.Masking 层
  • 在调用支持 mask 参数的层(如 RNN 层)时,手动传递此参数。

3.1 配置keras.layers.Embedding 层


首先,先了解一下Embedding层
假如现在有这么两句话

Hope to see you soon
Nice to see you again

在神经网络中,我们将这个作为输入,一般就会将每个单词用一个正整数代替,这样,上面的两句话在输入中是这样的

[0, 1, 2, 3, 4]

[5, 1, 2, 3, 6]

在神经网络中,第一层是

Embedding(input_dim=7, output_dim=2, input_length=5)

其中,第一个参数是input_dim,值为7,代表的是单词表的长度;第二个参数是output_dim,值为2,代表输出后向量长度为2;第三个参数是input_length,值为5,代表输入序列的长度。

一旦神经网络被训练了,Embedding层就会被赋予一个权重,计算出来的结果如下:

+------------+------------+
|   index    |  Embedding |
+------------+------------+
|     0      | [1.2, 3.1] |
|     1      | [0.1, 4.2] |
|     2      | [1.0, 3.1] |
|     3      | [0.3, 2.1] |
|     4      | [2.2, 1.4] |
|     5      | [0.7, 1.7] |
|     6      | [4.1, 2.0] |
+------------+------------+

根据这个权重,第二个输入计算出来的embedding vector就是下面这个,shape为(input_length,output_dim)=(5,2):

[[0.7, 1.7], [0.1, 4.2], [1.0, 3.1], [0.3, 2.1], [4.1, 2.0]]

来自Keras Embedding层详解的例子,解释其中迷惑的点。
若令mask_zero=True,则注意input_dim应该加一,例如原本ID类取值为0到5,为了实现padding,ID取值范围变成1到6,则此时input_dim=6+1=7。
下面给出一个例子,我们准备了四个ID列表型样本,经过了padding补零到长度4,为了方便展示,我们让所有非零ID相同。在Embedding层中设置mask_zero=True,然后调用自定义的MeanPooling层(见这里)。如果masking起作用,则输出的result应该是一个4x3矩阵,且所有行向量相同。
MyMeanPool()层就是把每个样本按行求平均值,具体例子看上述的例子2。如果应用Masking机制(mask_zero=True),对四个样本直接按行求平均值: M e a n s e q 0 = 1 1 = 1 Mean_{seq0}=\frac{1}{1}=1 Meanseq0=11=1 M e a n s e q 1 = 1 + 1 2 = 1 Mean_{seq1}=\frac{1+1}{2}=1 Meanseq1=21+1=1 M e a n s e q 2 = 1 + 1 + 1 3 = 1 Mean_{seq2}=\frac{1+1+1}{3}=1 Meanseq2=31+1+1=1 M e a n s e q 3 = 1 + 1 + 1 + 1 4 = 1 Mean_{seq3}=\frac{1+1+1+1}{4}=1 Meanseq3=41+1+1+1=1 则结果相同,都为1。先经过Embedding生成一个(4,3)的Embedding矩阵,维度为(4,3)是因为(input_length,output_dim)=(4,3),再经过MyMeanPool(axis=1)后按行求平均值, 因此所有的行向量应该相同。

#四个ID列表型样本,经过padding
ID_seq0 = [1, 0, 0, 0]
ID_seq1 = [1, 1, 0, 0]
ID_seq2 = [1, 1, 1, 0]
ID_seq3 = [1, 1, 1, 1]
data = np.array([ID_seq0, ID_seq1, ID_seq2, ID_seq3])

model =  tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(input_dim=5, output_dim=3, input_length=4, mask_zero=True))
model.add(MyMeanPool(axis=1))

result = model.predict(data) #为输入样本生成输出预测
print(result)

输出结果如下,这些数值都是Embedding层随机生成的权值而已。

[[-0.00347356 -0.03284906 -0.03016986]
 [-0.00347356 -0.03284906 -0.03016986]
 [-0.00347356 -0.03284906 -0.03016986]
 [-0.00347356 -0.03284906 -0.03016986]]

3.2 添加keras.layers.Masking层

from keras.layers import Input, Masking
from keras.models import Model
from MyMeanPooling import MyMeanPool

data = [[[10,10],[0, 0 ],[0, 0 ],[0, 0 ]],
        [[10,10],[20,20],[0, 0 ],[0, 0 ]],
        [[10,10],[20,20],[30,30],[0, 0 ]],
        [[10,10],[20,20],[30,30],[40,40]]]

A = Input(shape=[4,2]) # None * 4 * 2
mA = Masking()(A)
out = MyMeanPool(axis=1)(mA)

model = Model(inputs=[A], outputs=[out])

print model.summary()
print model.predict(data)

结果如下,每一行对应一个样本的结果,例如第一个样本只有第一个时刻有值,输出结果是[10. 10. ],是正确的。

[[10. 10.]
 [15. 15.]
 [20. 20.]
 [25. 25.]]

例2:

import tensorflow as tf
import numpy as np

raw_inputs = [[83, 91, 1, 645, 1253, 927],
              [73, 8, 3215, 55, 927],
              [711, 632, 71]]
padded_input = tf.keras.preprocessing.sequence.pad_sequences(raw_inputs, maxlen=None,
                                                             dtype='int32', padding='post',
                                                             truncating='post', value=0.0)
print(padded_input)

embedding_layer = tf.keras.layers.Embedding(input_dim=5000, output_dim=16, mask_zero=True)

masked_output = embedding_layer(padded_input)
print('Shape of masked input passed through embedding: {}'.format(padded_input.shape))
print('Shape of Embeding output: {}'.format(masked_output.shape))
print('Shape of mask attached to this input: {}'.format(masked_output._keras_mask.shape))
print('Original input: \n{}'.format(padded_input))
print('attached Mask: \n{}'.format(masked_output._keras_mask))

# Using Masking layer:
masking_layer = tf.keras.layers.Masking()
# simulate the embedding look up by expanding the 2D input to 3D
# with embedding dimension of 10
mask_value = 0.
x = np.random.rand(10, 6, 2)
x[0, 3:, :] = mask_value

print('\nUsing masking layer...')
print("Unmasked embedding: \n {}".format(x.shape))

masked_embedding = masking_layer(x)
print(masked_embedding._keras_mask.shape)
print(masked_embedding._keras_mask)

输出:
在这里插入图片描述

3.3 自定义

更多的,我们还是在自定义的层中,需要支持mask的操作,因此需要对应的逻辑。
如果我们希望自定义的这个层支持mask操作,就需要在__init__方法中指定

self.supports_masking = True

如果在本层计算中需要使用到mask,则call方法需要多传入一个mask参数,即

def call(self,inputs,mask=None):
	pass

然后,如果还要继续输出mask,供之后的层使用,如果不对mask矩阵进行变换,这不用进行任何操作,否则就需要实现compute_mask函数

def compute_mask(self,inputs,mask=None):
	pass
	

这里的inputs就是输入的Tensor,与call方法中接收到的一样,mask就是上层传入的mask矩阵。
如果希望mask到此为止,之后的层不再使用,则该函数直接返回None即可。

def compute_mask(self,inputs,mask=None):
	return  None

例子:在上文中的MyMeanpooling层,就是自定义的Masking:

from keras import backend as K
from keras.engine.topology import Layer
import tensorflow as tf

class MyMeanPool(Layer):
    def __init__(self, axis, **kwargs):
        self.supports_masking = True
        self.axis = axis
        super(MyMeanPool, self).__init__(**kwargs)

    def compute_mask(self, input, input_mask=None):
        # need not to pass the mask to next layers
        return None

    def call(self, x, mask=None):
        if mask is not None:
            mask = K.repeat(mask, x.shape[-1])
            mask = tf.transpose(mask, [0,2,1])
            mask = K.cast(mask, K.floatx())
            x = x * mask
            return K.sum(x, axis=self.axis) / K.sum(mask, axis=self.axis)
        else:
            return K.mean(x, axis=self.axis)

    def compute_output_shape(self, input_shape):
        output_shape = []
        for i in range(len(input_shape)):
            if i!=self.axis:
                output_shape.append(input_shape[i])
        return tuple(output_shape)

参考资料

  1. keras中的compute_mask函数讲解
  2. Keras自定义实现带masking的meanpooling层
  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值