共计 8964 个字符,预计需要花费 23 分钟才能阅读完成。
前两篇文章咱们曾经介绍了自回归模型 PixelCNNs,以及如何解决多维输出数据,本篇文章咱们将关注 PixelCNNs 的最大限度之一(即盲点)以及如何改良以修复它。
在前两篇文章中,咱们介绍了生成模型 PixelCNN 概念并钻研了黑白 PixelCNN 的工作原理。PixelCNN 是一种学习像素概率分布的生成模型,将来像素的强度将由之前的像素决定。在以前的文章中,咱们实现了两个 PixelCNN,并留神到性能并不杰出。咱们也提到进步模型性能的办法之一是修复盲点问题。在这篇文章中咱们将介绍盲点的概念,探讨 PixelCNN 是如何受到影响的,并实现一种解决方案——Gated PixelCNN。
盲点
PixelCNN 学习图像中所有像素的条件散布并应用此信息进行预测。PixelCNN 将学习像素从左到右和从上到下的散布,通常应用掩码来确保“将来”像素(即正在预测的像素右侧或下方的像素)不能用于给定像素的预测。如下图 A 所示,掩码将以后被预测的像素(这对应于掩码核心的像素)“之后”的像素清零。然而这种操作导致并不是所有“过来”的像素都会被用来计算新点,失落的信息会产生盲点。
要理解盲点问题,让咱们看上图 B。在图 B 中,深粉色点 (m) 是咱们要预测的像素,因为它位于过滤器的核心。如果咱们应用的是 3×3 掩码 (上图 A.),像素 m 取决于 l、g、h、i。另一方面,这些像素取决于之前的像素。例如,像素 g 依赖于 f、a、b、c,像素 i 依赖于 h、c、d、e。从上图 B 中,咱们还能够看到,只管呈现在像素 m 之前,但从未思考像素 j 来计算 m 的预测。同样,如果咱们想对 q、j、n、o 进行预测,则永远不会思考(上图 C 橙色局部)。所以并非所有先前的像素都会影响预测,这种状况就被称为盲点问题。
咱们将首先看看 pixelcnn 的实现,以及盲点将如何影响后果。上面的代码片段展现了应用 Tensorflow 2.0 的 PixelCNN 实现掩码。
class MaskedConv2D(keras.layers.Layer):
"""Convolutional layers with masks.
Convolutional layers with simple implementation of masks type A and B for
autoregressive models.
Arguments:
mask_type: one of `"A"` or `"B".`
filters: Integer, the dimensionality of the output space
(i.e. the number of output filters in the convolution).
kernel_size: An integer or tuple/list of 2 integers, specifying the
height and width of the 2D convolution window.
Can be a single integer to specify the same value for
all spatial dimensions.
strides: An integer or tuple/list of 2 integers,
specifying the strides of the convolution along the height and width.
Can be a single integer to specify the same value for
all spatial dimensions.
Specifying any stride value != 1 is incompatible with specifying
any `dilation_rate` value != 1.
padding: one of `"valid"` or `"same"` (case-insensitive).
kernel_initializer: Initializer for the `kernel` weights matrix.
bias_initializer: Initializer for the bias vector.
"""
def __init__(self,
mask_type,
filters,
kernel_size,
strides=1,
padding='same',
kernel_initializer='glorot_uniform',
bias_initializer='zeros'):
super(MaskedConv2D, self).__init__()
assert mask_type in {'A', 'B'}
self.mask_type = mask_type
self.filters = filters
self.kernel_size = kernel_size
self.strides = strides
self.padding = padding.upper()
self.kernel_initializer = initializers.get(kernel_initializer)
self.bias_initializer = initializers.get(bias_initializer)
def build(self, input_shape):
self.kernel = self.add_weight('kernel',
shape=(self.kernel_size,
self.kernel_size,
int(input_shape[-1]),
self.filters),
initializer=self.kernel_initializer,
trainable=True)
self.bias = self.add_weight('bias',
shape=(self.filters,),
initializer=self.bias_initializer,
trainable=True)
center = self.kernel_size // 2
mask = np.ones(self.kernel.shape, dtype=np.float64)
mask[center, center + (self.mask_type == 'B'):, :, :] = 0.
mask[center + 1:, :, :, :] = 0.
self.mask = tf.constant(mask, dtype=tf.float64, name='mask')
def call(self, input):
masked_kernel = tf.math.multiply(self.mask, self.kernel)
x = nn.conv2d(input,
masked_kernel,
strides=[1, self.strides, self.strides, 1],
padding=self.padding)
x = nn.bias_add(x, self.bias)
return x
察看原始 PixelCNN 的接管域(下图 2 中用黄色标记),咱们能够看到盲点以及它是如何在不同层上流传的。在这本篇文章的第二局部,咱们将应用改进版的 PixelCNN,门控 PixelCNN,它引入了一种新的机制来防止盲点的产生。
图 2:PixelCNN 上盲点区域的可视化
Gated PixelCNN
为了解决这些问题,van den Oord 等人 (2016) 引入了门控 PixelCNN。门控 PixelCNN 不同于 PixelCNN 在两个次要方面:
- 它解决了盲点问题
- 应用门控卷积层进步了模型的性能
Gated PixelCNN 如何解决盲点问题
这个新模型通过将卷积分成两局部来解决盲点问题:垂直和程度堆栈。让咱们看看垂直和程度堆栈是如何工作的。
图 3:垂直(绿色)和程度堆栈(蓝色)
在垂直堆栈中,指标是解决以后行之前所有行的上下文信息。用于确保应用所有先前信息并放弃因果关系(以后预测的像素不应该晓得其右侧的信息)的办法是别离将掩码的核心向上挪动到被预测的像素上一行。如图 3 所示,核心是浅绿色的像素(m),但垂直堆栈收集的信息不会用于预测它,而是用于预测它上面一行的像素(r)。
独自应用垂直堆栈会在彩色预测像素 (m) 的左侧产生盲点。为防止这种状况,垂直堆栈收集的信息与来自程度堆栈的信息(图 3 中以蓝色示意的 p-q)相结合,从而预测须要预测的像素 (m) 左侧的所有像素。程度和垂直堆栈的联合解决了两个问题:
(1)不会应用预测像素右侧的信息,
(2)因为咱们作为一个块来思考,不再有盲点。
原始论文通过感触野具备 2×3 的卷积实现了垂直堆栈。在本篇文章中咱们则通过应用 3×3 卷积并屏蔽掉最初一行来实现这一点。在程度堆栈中,卷积层将预测值与来自以后剖析像素行的数据相关联。这能够应用 1×3 卷积来实现,这样就能够屏蔽将来的像素以保障自回归模型的因果关系条件。与 PixelCNN 相似,咱们实现了 A 型掩码(用于第一层)和 B 型掩码(用于后续层)。
上面的代码片段展现了应用 Tensorflow 2.0 框架实现的掩码。
class MaskedConv2D(keras.layers.Layer):
"""Convolutional layers with masks.
Convolutional layers with simple implementation of masks type A and B for
autoregressive models.
Arguments:
mask_type: one of `"A"` or `"B".`
filters: Integer, the dimensionality of the output space
(i.e. the number of output filters in the convolution).
kernel_size: An integer or tuple/list of 2 integers, specifying the
height and width of the 2D convolution window.
Can be a single integer to specify the same value for
all spatial dimensions.
strides: An integer or tuple/list of 2 integers,
specifying the strides of the convolution along the height and width.
Can be a single integer to specify the same value for
all spatial dimensions.
Specifying any stride value != 1 is incompatible with specifying
any `dilation_rate` value != 1.
padding: one of `"valid"` or `"same"` (case-insensitive).
kernel_initializer: Initializer for the `kernel` weights matrix.
bias_initializer: Initializer for the bias vector.
"""
def __init__(self,
mask_type,
filters,
kernel_size,
strides=1,
padding='same',
kernel_initializer='glorot_uniform',
bias_initializer='zeros'):
super(MaskedConv2D, self).__init__()
assert mask_type in {'A', 'B', 'V'}
self.mask_type = mask_type
self.filters = filters
self.kernel_size = kernel_size
self.strides = strides
self.padding = padding.upper()
self.kernel_initializer = initializers.get(kernel_initializer)
self.bias_initializer = initializers.get(bias_initializer)
def build(self, input_shape):
kernel_h = self.kernel_size
kernel_w = self.kernel_size
self.kernel = self.add_weight('kernel',
shape=(self.kernel_size,
self.kernel_size,
int(input_shape[-1]),
self.filters),
initializer=self.kernel_initializer,
trainable=True)
self.bias = self.add_weight('bias',
shape=(self.filters,),
initializer=self.bias_initializer,
trainable=True)
mask = np.ones(self.kernel.shape, dtype=np.float64)
# Get centre of the filter for even or odd dimensions
if kernel_h % 2 != 0:
center_h = kernel_h // 2
else:
center_h = (kernel_h - 1) // 2
if kernel_w % 2 != 0:
center_w = kernel_w // 2
else:
center_w = (kernel_w - 1) // 2
if self.mask_type == 'V':
mask[center_h + 1:, :, :, :] = 0.
else:
mask[:center_h, :, :] = 0.
mask[center_h, center_w + (self.mask_type == 'B'):, :, :] = 0.
mask[center_h + 1:, :, :] = 0.
self.mask = tf.constant(mask, dtype=tf.float64, name='mask')
def call(self, input):
masked_kernel = tf.math.multiply(self.mask, self.kernel)
x = nn.conv2d(input,
masked_kernel,
strides=[1, self.strides, self.strides, 1],
padding=self.padding)
x = nn.bias_add(x, self.bias)
return x
通过在整个网络中增加这两个堆栈的特色图,咱们失去了一个具备统一感触野且不会产生盲点的自回归模型(图 4)。
图 4:Gated PixelCNN 的感触野可视化。咱们留神到,应用垂直和程度堆栈的组合,咱们可能防止在 PixelCNN 的初始版本中察看到的盲点问题(比照图 2)。
门控激活单元(或门控块)
从原始的 PixelCNNs 到 Gated CNNs 的第二个次要改良是引入了门控块和乘法单元(以 LSTM 门的模式)。因而,而不是像原始 PixelCNNs 那样在掩码卷积之间应用 ReLU;Gated PixelCNN 应用门控激活单元来模仿特色之间更简单的交互。这个门控激活单元应用 sigmoid(作为忘记门)和 tanh(作为真正的激活)。在原始论文中,作者认为这可能是 PixelRNN(应用 LSTM 的)优于 PixelCNN 的起因之一,因为它们可能通过循环更好地捕捉过来的像素——它们能够记住过来的信息。因而,Gated PixelCNN 增加并应用了以下内容:
σ 是 sigmoid,k 是层数,⊙ 是元素乘积,* 是卷积算子,W 是来自前一层的权重。有了这个公式咱们就能够更具体地介绍 PixelCNN 中的单个层。
Gated PixelCNN 中的单层块
堆栈和门是 Gated PixelCNN 的基本块(下图 5)。然而它们是如何连贯的,信息将如何解决?咱们将把它分解成 4 个解决步骤,咱们将在上面的会话中探讨。
图 5:Gated PixelCNN 架构概述(来自原始论文的图像)。色彩代表不同的操作(即,绿色:卷积;红色:元素乘法和加法;蓝色:具备权重的卷积
1、计算垂直堆栈特色图
作为第一步,来自垂直堆栈的输出由 3 ×3 卷积层和垂直掩码解决。而后生成的特色图通过门控激活单元并输出到下一个块的垂直堆栈中。
2、将垂直地图送入程度堆栈
对于自回归模型,须要联合垂直和程度堆栈的信息。为此在每个块中垂直堆栈也用作程度层的输出之一。因为垂直堆栈的每个卷积步骤的核心对应于剖析的像素,所以咱们不能只增加垂直信息,这将突破自回归模型的因果关系条件,因为它将容许应用将来像素的信息来预测程度堆栈中的值。下图 8A 中的第二幅图就是这种状况,其中彩色像素右侧(或将来)的像素用于预测它。因为这个起因在将垂直信息提供给程度堆栈之前,须要应用填充和裁剪将其向下挪动(图 8B)。通过对图像进行零填充并裁剪图像底部,能够确保垂直和程度堆栈之间的因果关系。咱们将在当前的文章中深入研究裁剪如何工作的更多细节,所以如果它的细节不齐全分明,请不要放心
图 8:如何保障像素之间的因果关系
3、计算程度特色图
在这一步中,解决程度卷积层的特色图。事实上,首先从垂直卷积层到程度卷积层输入的特色映射求和。这种组合的输入具备现实的接管格局,它思考了所有以前像素的信息,而后就是通过门控激活单元。
4、计算程度叠加上的残差连贯
在这最初一步中,如果该块不是网络的第一个块,则应用残差连贯合并上一步的输入(通过 1 ×1 卷积解决),而后送入下一个块的程度堆栈。如果是网络的第一个块,则跳过这一步。
应用 Tensorflow 2 的代码如下:
class GatedBlock(tf.keras.Model):
"""Gated block that compose Gated PixelCNN."""
def __init__(self, mask_type, filters, kernel_size):
super(GatedBlock, self).__init__(name='')
self.mask_type = mask_type
self.vertical_conv = MaskedConv2D(mask_type='V',
filters=2 * filters,
kernel_size=kernel_size)
self.horizontal_conv = MaskedConv2D(mask_type=mask_type,
filters=2 * filters,
kernel_size=kernel_size)
self.padding = keras.layers.ZeroPadding2D(padding=((1, 0), 0))
self.cropping = keras.layers.Cropping2D(cropping=((0, 1), 0))
self.v_to_h_conv = keras.layers.Conv2D(filters=2 * filters, kernel_size=1)
self.horizontal_output = keras.layers.Conv2D(filters=filters, kernel_size=1)
def _gate(self, x):
tanh_preactivation, sigmoid_preactivation = tf.split(x, 2, axis=-1)
return tf.nn.tanh(tanh_preactivation) * tf.nn.sigmoid(sigmoid_preactivation)
def call(self, input_tensor):
v = input_tensor[0]
h = input_tensor[1]
vertical_preactivation = self.vertical_conv(v)
# Shifting vertical stack feature map down before feed into horizontal stack to
# ensure causality
v_to_h = self.padding(vertical_preactivation)
v_to_h = self.cropping(v_to_h)
v_to_h = self.v_to_h_conv(v_to_h)
horizontal_preactivation = self.horizontal_conv(h)
v_out = self._gate(vertical_preactivation)
horizontal_preactivation = horizontal_preactivation + v_to_h
h_activated = self._gate(horizontal_preactivation)
h_activated = self.horizontal_output(h_activated)
if self.mask_type == 'A':
h_out = h_activated
elif self.mask_type == 'B':
h_out = h + h_activated
return v_out, h_out
综上所述,利用门控块解决了接管域盲点问题,进步了模型性能。
后果比照
原始论文中,PixelCNN 应用以下架构: 第一层是一个带有 7 ×7 过滤器的掩码卷积(a 型)。而后应用 15 个残差块。每个块采纳掩码 B 类的 3 ×3 层卷积层和规范 1 ×1 卷积层的组合解决数据。在每个卷积层之间,应用 ReLU 进行激活。最初还包含一些残差链接。
所以咱们训练了一个 PixelCNN 和一个 Gated PixelCNN,并在上面比拟后果。
当比拟 PixelCNN 和 Gated PixelCNN 的 MNIST 预测时(上图),咱们并没有发现到在 MNIST 上有很大的改良。一些先前被修改的预测数字当初被谬误地预测了。这并不意味着 pixelcnn 体现不好,在下一篇博文中,咱们将探讨带门控的 PixelCNN++,所以请持续关注!
Gated PixelCNN 论文地址:https://arxiv.org/abs/1606.05328
作者:Walter Hugo Lopez Pinaya, Pedro F. da Costa, and Jessica Dafflon