转置卷积操作的具体合成

1. 简介

转置卷积是用于生成图像的,只管它们曾经存在了一段时间,并且失去了很好的解释——我依然很难了解它们到底是如何实现工作的。我分享的文章形容了一个简略的试验来阐明这个过程。我还介绍了一些有助于进步网络性能的技巧。

本文次要内容:

  • 反卷积操作的整体可视化
  • 通过拆散更重要的组件来优化网络
  • 解决合成数据集问题

插图的工作是我能想到的最简略的工作:为合成数据构建一个主动编码器。它是合成的这一事实可能会引发一些问题。确实,在此类数据上训练的模型在实在数据上可能体现不佳。然而咱们稍后会看到为什么会这样以及如何解决它。该模型由一个用于编码器的卷积层和一个用于解码器的反卷积(也称为卷积转置)组成。

2. 数据集

数据是一组一维贝塞尔曲线,如下所示:

  • 这是生成数据的代码:
import numpy as npfrom tqdm import tqdmimport matplotlib.pyplot as pltDATASIZE = 1024 * 16WINSIZE = 15np.random.seed(22)# A Wikipedia page on Bezier curves contains a thorough explanation on# the topic, as well as mathematical formulas.# A 3-rd order curve is used here, so it is built on 4 pointsdef get_curve_points(param1, param2, n):    # The four characteristic points depend on two parameters    p1 = {'x': 0, 'y': 0}    p2 = {'x': 0.5, 'y': param1}    p4 = {'x': 1, 'y': 0}    p3 = {'x': 0.5, 'y': param2}    xs = np.zeros(n)    ys = np.zeros(n)    # Calculating the curve points by the formula from Wikipedia    for i, t in enumerate(np.linspace(0, 1, n)):        xs[i] = (1-t)**3 * p1['x'] + 3*(1-t)**2*t * p2['x']\                + 3*(1-t)*t**2 * p3['x'] + t**3 * p4['x']        ys[i] = (1-t)**3 * p1['y'] + 3*(1-t)**2*t * p2['y']\                + 3*(1-t)*t**2 * p3['y'] + t**3 * p4['y']    xs = np.array(xs)    ys = np.array(ys)    return xs, ysdef generate_input(p1, p2):    xs, ys = get_curve_points(p1, p2, WINSIZE * 10)    # The function above generates the curve as a set    # of X and Y coordinates. The code below resamples them    # into a set of Y coordinates only.    # The function above does generate 10 times more points    # than we would need, to make it dense enough for the resampling.    result = []    for x in np.linspace(0, 1, WINSIZE):        result.append(ys[np.argmin((xs - x)**2)])    return np.array(result)if __name__ == '__main__':    data = []    for i in tqdm(range(DATASIZE)):        # Generate two random parameters for each data item        p1 = np.random.uniform() * 2 - 1        p2 = np.random.uniform() * 2 - 1        ys = generate_input(p1, p2)        data.append(ys)        # Plot the first 9 samples in 3x3 window        if i < 3*3:            plt.subplot(3, 3, i+1)            plt.plot(ys)    plt.gcf().set_size_inches(8, 5)    plt.show()    data = np.array(data)    np.save('data.npy', data)

这些曲线每条蕴含 15 个点。每条曲线将作为一维数组馈入网络(而不是为每个点传递 [x, y] 坐标,仅传递 [y])。

每条曲线都能够用两个参数来表征,这意味着咱们的网络应该可能将任何曲线编码/解码为大小为 2 的向量。

3. 办法

这是一个可用于对曲线进行编码的示例网络:

在上图中,输出信号(底部)被分成 3 个大小为 7 的块(通过利用窗口大小为 7 和步幅为 4 的卷积层)。每个补丁被编码成一个大小为 3 的向量,给出一个 3x3 矩阵。而后将该矩阵编码为大小为 2 的向量;而后在解码器中反向反复该操作。

网络可能被分成两局部。第一个将可能编码/解码曲线的一部分(7 像素补丁);而第二个只会解决 3x3 矩阵。这样,我就能够别离训练每个局部。我能够制作一个仅实用于 7 像素补丁的更小的主动编码器:

所以我要将每个示例拆分成补丁,并训练网络对补丁进行编码/解码。而后,我将组装这个网络来生成整个曲线。

我不会进一步编码这个 3x3 矩阵,因为这个过程不会携带任何新信息。

4. 网络

4.1. 模型

我将为编码器和解码器应用不同的模型。它们非常简单:

# The content of this file is later referenced as 'models.py'# it will be imported by 'from models import *'import torchclass Encoder(torch.nn.Module):    def __init__(self, ch, win):        super(Encoder, self).__init__()        self.conv = torch.nn.Conv1d(1, ch, win, 4)    def forward(self, x):        x = self.conv(x)        return torch.tanh(x)class Decoder(torch.nn.Module):    def __init__(self, ch, win):        super(Decoder, self).__init__()        self.deconv = torch.nn.ConvTranspose1d(ch, 1, win, 4)    def forward(self, x):        x = self.deconv(x)        return x

步幅为 4,因为正如“办法”局部中的图像所指,过滤器一次挪动 4 个像素。因为咱们在这里只施行一个阶段,因而这一步齐全是可选的;它只会在咱们组装更大的网络时产生影响。

4.2. 训练

首先,我将设置一组不同种子的训练。训练代码尽可能简略:

import osimport torchimport numpy as npfrom models import *from torch.utils.data import TensorDataset, DataLoaderN_CHANNELS = 3WINSIZE = 7N_EPOCHS = 5000device = torch.device('cuda:0')inputs = np.load('data.npy')# Cut the 15px curves into 7px items, at random pointinputs_cut = []np.random.seed(22)start_idxs = np.random.randint(0, inputs.shape[1] - WINSIZE, inputs.shape[0])for i in range(len(start_idxs)):    item_upd = inputs[i, start_idxs[i]:start_idxs[i] + WINSIZE]    inputs_cut.append(item_upd)inputs = torch.tensor(inputs_cut).float().unsqueeze(1).to(device)dataset = TensorDataset(inputs)def train_model(seed):    torch.manual_seed(seed)    # Create the dataloader after the random seed was set,     # because its 'shuffle' depends on the seed    dataloader = DataLoader(dataset, batch_size=128, shuffle=True)    enc = Encoder(N_CHANNELS, WINSIZE).to(device)    dec = Decoder(N_CHANNELS, WINSIZE).to(device)    opt = torch.optim.Adam(list(enc.parameters()) + list(dec.parameters()), lr=1e-4)    mse = torch.nn.MSELoss()    for epoch in range(N_EPOCHS):        train_loss = 0        for item, in dataloader:            opt.zero_grad()            code = enc(item)            out = dec(code)            loss = mse(out, item)            loss.backward()            opt.step()            train_loss += loss.item()        if epoch % 10 == 0:            print(epoch, train_loss / len(dataloader))        os.makedirs('models', exist_ok=True)        torch.save(enc.state_dict(), open('models/encoder_{}.pt'.format(seed), 'wb'))        torch.save(dec.state_dict(), open('models/decoder_{}.pt'.format(seed), 'wb'))        for seed in [11, 22, 33, 44, 55]:    train_model(seed)

以下是这些试验的损失体现:

该图显示了应用不同种子进行的 10 次试验的损失均值和标准差。

如果我当初将标签与网络输入进行视觉比拟,它看起来如下所示:

这看起来还不错,看起来网络在工作,1e-4 下的损失值就足够了。

接下来,我将阐明编码器和解码器在这个例子中是如何工作的。

4.3. Decoder

解码器旨在将代码(3 维向量)转换为曲线补丁。作为反卷积操作,它有一组过滤器,每个过滤器按某个值缩放,而后相加;换句话说,滤波器的加权和应该与所需的输入相匹配。

下图蕴含两个复原曲线的示例。右边的例子是从向量 [0.0, 0.1, 0.2] 解码而来的,左边的例子是 [0.1, 0.1, 0.0]。每个示例在顶部蕴含缩放的过滤器,在底部蕴含不缩放的过滤器。

果然,咱们能够一次扭转一个矢量重量并实时渲染网络输入,造成一个看起来很酷的动画。所以这里是:

下面的每个动画都蕴含多个情节。左下角的点显示输出向量。它的 X 和 Y 坐标和大小代表输出的第一、第二和第三重量。右下角的图显示了原始过滤器,并且所有动画都放弃不变。右上图显示缩放过滤器和输入。因为只有一个参数在变动,因而只有一个滤波器被缩放并且输入与该滤波器匹配。

左上图可能是最乏味的,因为它旨在显示输入如何同时依赖于两个组件。它下面的每条曲线代表第三个组件的不同值的输入。人们可能会看到,在第三个动画中,图形没有挪动,只是不同的曲线变粗了。在前两个动画中,只有两头曲线放弃粗体,因为第三个重量放弃为零,但总的来说,它给出了如果第三个重量发生变化时输入会是什么的想法。

这是所有组件同时变动的状况:

4.4. Encoder

这个例子中的编码器看起来有点不直观,但无论如何它以某种形式实现了工作。 (其品质将在以下局部进行审查)

左侧显示了原始过滤器以及示例输出。右图显示了利用于示例输出的这些雷同过滤器(即输出的每个点乘以过滤器的每个点)。标签还蕴含每个过滤器的输入(即过滤器 1 的每个点乘以输出的每个点的总和失去 0.02)。

上图中的示例将被编码为一个向量 (0.02, 0.08, -0.08)。解码器将以如下所示的形式复原输出:

右边的图显示理解卷积滤波器。左边的那个——每个滤波器乘以其代码向量值及其总和(这也是解码器的输入)。

4.5. 问题

该解决方案仿佛无效——网络将其 7 值输出编码为 3 值代码;而后以低谬误将其解码回来。但我看到了两种改良的可能性:

  1. 当应用不同的种子训练网络时,过滤器会有很大差别。这意味着网络具备太多的灵活性,咱们能够对其进行束缚。
  2. 编码器鲁棒性。咱们如何确保编码器实用于各种实在数据?

为了阐明第一种状况,我将简略地绘制通过应用不同种子训练取得的过滤器:

显然,咱们须要思考过滤器的符号和程序:第一个图像上的过滤器 3 和第二个图像上的过滤器 1 是雷同的。具备符号和程序弥补的过滤器集如下所示:

这里的过滤器显然有很大差别。

5. 改良

5.1. 噪声

不同的过滤器集给出雷同后果的事实表明过滤器可能受到束缚。限度过滤器的一种办法是确定它们的优先级。与 PCA 技术相似,将编码大部分信息的过滤器与编码大量细节的过滤器离开。这能够通过向编码图像增加噪声来实现:

在上图中,右边的图像展现了一个传统的主动编码器:输出被编码到一个二维立体上;而后解码回来。网络解码两头图像中 2D 立体略微偏移的点。这迫使网络将类似的图像编码为在立体上放弃靠近的点。右图显示了解码点在 Y 轴上比在 X 轴上漂移更远的状况。这迫使网络将最重要的特色编码到 X 组件中,因为它的噪声较小。

这是在代码中实现的雷同概念:

for epoch in range(N_EPOCHS):    train_loss = 0    for item, in dataloader:        opt.zero_grad()        code = enc(item)        # Generate noise to be added to the code        coderand = torch.randn(code.shape).to(device)        # Vary the noise magnitude for different channels        coderand[:, 0 :] *= 0.1        coderand[:, 1 :] *= 0.01        coderand[:, 2 :] *= 0.001        # Apply the noise        out = dec(code + coderand)                loss = mse(out, item)        loss.backward()        opt.step()

再次运行试验后,我会收到一组更统一的过滤器:

如果我像以前一样弥补签名和订单,在“有什么问题”局部:

能够分明地看到一侧蜿蜒而另一侧尖利的过滤器,并且一个组件在两头蜿蜒。过滤器 2 和 3 仿佛等同重要,因为它们有时在第一个组件中编码,有时在第二个组件中编码。然而,第三个滤波器的幅度较小,并且总是编码在最嘈杂的重量中,这表明它不太重要。

能够通过比拟模型的性能同时将编码向量的不同重量归零来查看这种噪声的影响:

上图显示了通过将第一、第二或第三重量归零后的损失变动,左侧为噪声模型,右侧为原始模型。对于原始模型,禁用每个组件会导致绝对雷同的损耗降落。

这是视觉上的比拟。在下图中找到模型输出和输入。从左到右:残缺模型;禁用过滤器 1 的模型;禁用过滤器 2 的模型;禁用过滤器 3 的模型。

用噪声训练的模型:

对于原始模型:

当仅禁用噪声组件时,应用噪声训练的模型的误差显著较小。

5.2. 输出乐音

当初让咱们再看一下编码器过滤器。它们一点也不润滑,有些看起来很类似。这很奇怪,因为编码器的目标是发现输出中的差别,而您无奈应用一组类似的过滤器来发现差别。网络如何做到这一点?好吧,答案是咱们的合成数据集太完满了,无奈训练正确的过滤器。网络能够很容易地通过一个点对输出进行分类:

在下面的每个图上,较大的图上都有一个曲线示例(称为“原始”),以及 3 个与该原始示例具备类似点的示例。示例自身是不同的,但给出该点的准确值(即 -0.247 或 -0.243),网络可能对整个示例做出决定。

这能够通过向原始输出增加噪声来轻松解决。

for epoch in range(N_EPOCHS):    train_loss = 0    for item, in dataloader:        opt.zero_grad()        # Generate the noise to be added to the input        itemrand = torch.randn(item.shape).to(device)        itemrand *= 0.01        # Apply the input noise        code = enc(item + itemrand)                # Generate noise to be added to the code        coderand = torch.randn(code.shape).to(device)        # Vary the noise magnitude for different channels        coderand[:, 0 :] *= 0.1        coderand[:, 1 :] *= 0.01        coderand[:, 2 :] *= 0.001        # Apply the noise        out = dec(code + coderand)                loss = mse(out, item)        loss.backward()        opt.step()

再次训练模型后,我取得了很好的平滑过滤器:

人们可能会看到噪声最大的过滤器 1 比其余过滤器变得更大。我的猜想是,它试图使其输入大于乐音,试图升高其影响。然而因为我有一个 tanh 激活,它的输入不能大于 1,所以噪声依然无效。

5. 残缺模型

当初咱们有了模型的工作组件,咱们能够屡次利用它,这样它就能够对整个 15 点曲线进行编码/解码。这没什么特地的,我须要做的就是不要把我的例子切成碎片:

import numpy as npimport torchfrom torch.utils.data import TensorDataset, DataLoaderimport matplotlib.pyplot as pltfrom models import *WINSIZE = 7N_CHANNELS = 3device = torch.device('cuda')enc = Encoder(N_CHANNELS, WINSIZE).to(device)dec = Decoder(N_CHANNELS, WINSIZE).to(device)def eval_model(seed):    enc.load_state_dict(torch.load(open('models/encoder_{}.pt'.format(seed), 'rb')))    dec.load_state_dict(torch.load(open('models/decoder_{}.pt'.format(seed), 'rb')))    mse = torch.nn.MSELoss()    eval_loss = 0    cnt = 0    for item, in dataloader:        cnt += 1        # Plot first two examples in a 2x2 grid        if cnt <= 4:            code = enc(item)            out = dec(code)            loss = mse(out, item)            eval_loss += loss.item()            plt.subplot(2, 2, cnt)            plt.plot(out[0, 0].cpu().detach().numpy(), label="Output")            plt.plot(item[0, 0].cpu().detach().numpy(), label="Target")            plt.legend()            plt.grid()    print(eval_loss / len(dataloader))    plt.show()inputs = np.load('data.npy')inputs = torch.tensor(inputs).float().unsqueeze(1).to(device)dataset = TensorDataset(inputs)dataloader = DataLoader(dataset, batch_size=128, shuffle=False)eval_model(22)view raw

这将给我以下输入:

好吧,兴许我须要扭转一些货色。问题是,反卷积将总结图像的重叠局部。所以上面图中的这些局部加倍了:

有一个简略的解决办法:我能够将输入重叠的过滤器的权重缩小一半:

def eval_model(seed):    enc.load_state_dict(torch.load(open('models/encoder_{}.pt'.format(seed), 'rb')))    dec.load_state_dict(torch.load(open('models/decoder_{}.pt'.format(seed), 'rb')))    # With the deconvolution stride 4 and     # window size 7, which is our case, the    # only weight that does not overlap is     # the middle one, indexed as 4.    # All the other weights are reduced by a half.    dec.deconv.weight.data[:, :, 0] *= 0.5    dec.deconv.weight.data[:, :, -1] *= 0.5    dec.deconv.weight.data[:, :, 1] *= 0.5    dec.deconv.weight.data[:, :, -2] *= 0.5    dec.deconv.weight.data[:, :, 2] *= 0.5    dec.deconv.weight.data[:, :, -3] *= 0.5...

进行此操作后,我将失去以下输入:

这导致了另一个问题——边界点没有任何重叠,解码后的图像与应有的不同。对此有几种可能的解决方案。例如,在编码图像中蕴含填充,以便边界也被编码,或者疏忽这些边界(也可能将它们排除在损失计算之外)。我将在这里进行,因为进一步的改良超出了本文的范畴。

总结

这个简略的例子阐明了反卷积是如何工作的,以及如何应用噪声(有时大小不同)来训练神经网络。

所形容的办法的确实用于较大的网络,并且噪声幅度成为试验的超参数。该办法可能不适用于 ReLu 激活:它们的输入可能很容易变得比噪声幅度大得多,并且噪声会失去作用。

本文由mdnice多平台公布