前一部分使用了带有膨胀系数的卷积。在这种情况下,节点 t 与节点 t-n 计算卷积,节点 t+1 与节点 t+1-n 计算卷积。这意味着一个层中的隐藏状态的数量等于输入的数量,使得能够直接利用缓存。然而,使用跨度卷积使问题更加困难,因为隐藏层中的状态数量与输入数量不同。 跨度卷积是下采样层(downsampling layer)。这意味着隐藏状态少于输入。典型的卷积在局部邻域(local neighborhood)上进行卷积计算,然后移动 1 个位置并重复该过程计算。例如,节点 t-1 和 t 将计算卷积,然后节点 t 和 t+1 将计算卷积。跨度会影响卷积移动经过的位置的数量。在前面的例子中,跨度为 1。然而,当跨度大于 1 时,输入将被下采样。例如,当跨度为 2 时,因为卷积移动 2 个位置,节点 t-1 和 t 将计算卷积,然后节点 t+1 和 t+2 将计算卷积。这意味着该层的每对输入仅产生一个输出,因此隐藏状态的数量小于输入的数量。 类似地,还有上采样层(upsampling layer),它是跨度转置卷积(strided transposed convolutions)。跨度记为 s,即上采样层将为该层的每个输入产生 s 个输出。与传入该层的输入数量相比,这增加了隐藏状态的数量。PixelCNN++ 先使用 2 个下采样层,然后使用 2 个上采样层,每个上采样层的跨度为 2,这意味着生成的像素的数量与输入像素的数量相同(即 D/2/2*2*2 = D)。关于跨度和转置卷积的详细解释可以在这两个链接中找到:https://github.com/vdumoulin/conv_arithmetic 和 https://arxiv.org/abs/1603.07285 由于隐藏状态的数量不同,因此无法在每个时间步骤(timestep)中更新缓存。因此,每个缓存都有一个附加属性 cache every,缓存每次只在 cache every 的步骤更新。每个下采样层通过增加跨度来增加该层的 cache every 属性。相反,每个上采样层通过减少跨度来减少该层的 cache every 属性。
上图显示了一个具有 2 个上采样层和 2 个下采样层的模型示例,每个采样层的跨度为 2。橙色节点在当前的时间步骤计算,蓝色节点是先前的缓存状态,灰色节点不参与当前的时间步骤。 在第 1 个时间步骤 t=0,第 1 个输入用于计算和缓存所有节点,这里有足够的信息生成节点,包括前 4 个输出。 在 t=1 时,节点没有足够的信息用来计算,但是 t=1 的输出已经在 t=0 时算出。 在 t=2 时,有 1 个新节点有足够的信息来计算,虽然 t=2 的输出也在 t=0 时算出。 t=3 的情形类似于 t=1。 在 t=4 时,有足够的信息来计算多个隐藏状态并生成接下来的 4 个输出。这类似于 t=0 的情形。 t=5 类似于 t=1,并且该循环过程适用于所有未来的时间步骤。 在我们的代码中,我们还使用一个属性 run every,等同于下一层的 cache every 属性。这在如果下一层忽略输入时避免了计算。 加速 PixelCNN++ 在理解了前面的部分之后,我们将 1 维的例子直接推广到 2 维。事实上,我们的推广算法只有很少的变化。现在,每个层的缓存是 2 维的,缓存的高度(height)等于过滤器(filter)的高度,缓存的宽度(width)等于图像宽度。在生成整个行之后,将弹出缓存中最早的一行,并推送入新的一行。因为使用了大量的卷积,我们使用上一部分中详述的 cache every 的想法。 PixelCNN 有两个计算流:垂直流(vertical stream)和水平流(horizontal stream)。将细节稍微简化一下,垂直流查看当前像素上方的所有像素,而水平流查看当前像素的左边相邻的所有像素,它满足 2 维情况下的自回归属性(参见 PixelCNN 论文以获得更精确的解释)。水平流也能把垂直流作为输入。在我们的代码中,我们一次计算一行的垂直流,缓存并使用它来一次计算一个像素的水平流(和生成的输出)。 有了这一点,我们能够实现 PixelCNN++ 生成的数量级加速!下图中增加的批处理大小(batch size)表明我们方法的可扩展性。虽然初始方法的实现结果随批处理大小呈线性扩展(由于 100%的 GPU 利用率),因为最小计算要求,我们的方法具有优越的扩展(scaling/时间复杂度)性能。
PixelCNN++ 延伸 (责任编辑:本港台直播) |