close

來源:DeepHub IMBA

本文約4500字,建議閱讀10分鐘

本篇文章我們將關注 PixelCNNs 的最大限制之一(即盲點)以及如何改進以修復它。


在這篇文章中我們將介紹盲點的概念,討論 PixelCNN 是如何受到影響的,並實現一種解決方案——Gated PixelCNN。

盲點

PixelCNN 學習圖像中所有像素的條件分布並使用此信息進行預測。PixelCNN 將學習像素從左到右和從上到下的分布,通常使用掩碼來確保「未來」像素(即正在預測的像素右側或下方的像素)不能用於給定像素的預測。如下圖A所示,掩碼將當前被預測的像素(這對應於掩碼中心的像素)「之後」的像素清零。但是這種操作導致並不是所有「過去」的像素都會被用來計算新點,丟失的信息會產生盲點。


要了解盲點問題,讓我們看上圖B。在圖B 中,深粉色點 (m) 是我們要預測的像素,因為它位於過濾器的中心。如果我們使用的是 3x3 掩碼 (上圖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)因為我們作為一個塊來考慮,不再有盲點。

原始論文通過感受野具有 2x3的卷積實現了垂直堆棧 。在本篇文章中我們則通過使用 3x3 卷積並屏蔽掉最後一行來實現這一點。在水平堆棧中,卷積層將預測值與來自當前分析像素行的數據相關聯。這可以使用 1x3 卷積來實現,這樣就可以屏蔽未來的像素以保證自回歸模型的因果關係條件。與 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、計算垂直堆棧特徵圖

作為第一步,來自垂直堆棧的輸入由3x3 卷積層和垂直掩碼處理。然後生成的特徵圖通過門控激活單元並輸入到下一個塊的垂直堆棧中。

2、將垂直地圖送入水平堆棧

對於自回歸模型,需要結合垂直和水平堆棧的信息。為此在每個塊中垂直堆棧也用作水平層的輸入之一。由於垂直堆棧的每個卷積步驟的中心對應於分析的像素,所以我們不能只添加垂直信息,這將打破自回歸模型的因果關係條件,因為它將允許使用未來像素的信息來預測水平堆棧中的值。下圖 8A 中的第二幅圖就是這種情況,其中黑色像素右側(或未來)的像素用於預測它。因為這個原因在將垂直信息提供給水平堆棧之前,需要使用填充和裁剪將其向下移動(圖 8B)。通過對圖像進行零填充並裁剪圖像底部,可以確保垂直和水平堆棧之間的因果關係。我們將在以後的文章中深入研究裁剪如何工作的更多細節,所以如果它的細節不完全清楚,請不要擔心。

圖8:如何保證像素之間的因果關係

3、計算水平特徵圖


在這一步中,處理水平卷積層的特徵圖。事實上,首先從垂直卷積層到水平卷積層輸出的特徵映射求和。這種組合的輸出具有理想的接收格式,它考慮了所有以前像素的信息,然後就是通過門控激活單元。

4、計算水平疊加上的殘差連接


在這最後一步中,如果該塊不是網絡的第一個塊,則使用殘差連接合併上一步的輸出(經過1x1卷積處理),然後送入下一個塊的水平堆棧。如果是網絡的第一個塊,則跳過這一步。

使用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使用以下架構:第一層是一個帶有7x7過濾器的掩碼卷積(a型)。然後使用15個殘差塊。每個塊採用掩碼B類的3x3層卷積層和標準1x1卷積層的組合處理數據。在每個卷積層之間,使用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

編輯:王菁
校對:林亦霖
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()