了解卷积和自动微分引擎

$$\gdef \sam #1 {\mathrm{softargmax}(#1)}$$ $$\gdef \vect #1 {\boldsymbol{#1}} $$ $$\gdef \matr #1 {\boldsymbol{#1}} $$ $$\gdef \E {\mathbb{E}} $$ $$\gdef \V {\mathbb{V}} $$ $$\gdef \R {\mathbb{R}} $$ $$\gdef \N {\mathbb{N}} $$ $$\gdef \relu #1 {\texttt{ReLU}(#1)} $$ $$\gdef \D {\,\mathrm{d}} $$ $$\gdef \deriv #1 #2 {\frac{\D #1}{\D #2}}$$ $$\gdef \pd #1 #2 {\frac{\partial #1}{\partial #2}}$$ $$\gdef \set #1 {\left\lbrace #1 \right\rbrace} $$ % My colours $$\gdef \aqua #1 {\textcolor{8dd3c7}{#1}} $$ $$\gdef \yellow #1 {\textcolor{ffffb3}{#1}} $$ $$\gdef \lavender #1 {\textcolor{bebada}{#1}} $$ $$\gdef \red #1 {\textcolor{fb8072}{#1}} $$ $$\gdef \blue #1 {\textcolor{80b1d3}{#1}} $$ $$\gdef \orange #1 {\textcolor{fdb462}{#1}} $$ $$\gdef \green #1 {\textcolor{b3de69}{#1}} $$ $$\gdef \pink #1 {\textcolor{fccde5}{#1}} $$ $$\gdef \vgrey #1 {\textcolor{d9d9d9}{#1}} $$ $$\gdef \violet #1 {\textcolor{bc80bd}{#1}} $$ $$\gdef \unka #1 {\textcolor{ccebc5}{#1}} $$ $$\gdef \unkb #1 {\textcolor{ffed6f}{#1}} $$ % Vectors $$\gdef \vx {\pink{\vect{x }}} $$ $$\gdef \vy {\blue{\vect{y }}} $$ $$\gdef \vb {\vect{b}} $$ $$\gdef \vz {\orange{\vect{z }}} $$ $$\gdef \vtheta {\vect{\theta }} $$ $$\gdef \vh {\green{\vect{h }}} $$ $$\gdef \vq {\aqua{\vect{q }}} $$ $$\gdef \vk {\yellow{\vect{k }}} $$ $$\gdef \vv {\green{\vect{v }}} $$ $$\gdef \vytilde {\violet{\tilde{\vect{y}}}} $$ $$\gdef \vyhat {\red{\hat{\vect{y}}}} $$ $$\gdef \vycheck {\blue{\check{\vect{y}}}} $$ $$\gdef \vzcheck {\blue{\check{\vect{z}}}} $$ $$\gdef \vztilde {\green{\tilde{\vect{z}}}} $$ $$\gdef \vmu {\green{\vect{\mu}}} $$ $$\gdef \vu {\orange{\vect{u}}} $$ % Matrices $$\gdef \mW {\matr{W}} $$ $$\gdef \mA {\matr{A}} $$ $$\gdef \mX {\pink{\matr{X}}} $$ $$\gdef \mY {\blue{\matr{Y}}} $$ $$\gdef \mQ {\aqua{\matr{Q }}} $$ $$\gdef \mK {\yellow{\matr{K }}} $$ $$\gdef \mV {\lavender{\matr{V }}} $$ $$\gdef \mH {\green{\matr{H }}} $$ % Coloured math $$\gdef \cx {\pink{x}} $$ $$\gdef \ctheta {\orange{\theta}} $$ $$\gdef \cz {\orange{z}} $$ $$\gdef \Enc {\lavender{\text{Enc}}} $$ $$\gdef \Dec {\aqua{\text{Dec}}}$$
🎙️ Alfredo Canziani

了解 1 维的卷积

这里我们要讨论卷积,因为我们想探索资料的稀疏性、平稳性与复合性。

我们使用的不是上周的 $A$ 矩阵,而是将其宽度改变为核的大小 $k$。因此,矩阵的每一列都是一个核。我们可以将核堆叠并滑动来使用(见图 1)。 如此我们会得到 $m$ 个高度为 $n-k+1$ 的层。

1
图 1: 一维卷积的插图

输出是 $m$(厚度) 个尺寸为 $n-k+1$ 的向量。

2
图 2: 一维卷积的结果

一个单独的输入向量可以视作一个单音讯号。

3
图 3: 单音讯号

现在,输入 $x$ 是一个映射:

\[x:\Omega\rightarrow\mathbb{R}^{c}\]

其中 $\Omega = \lbrace 1, 2, 3, \cdots \rbrace \subset \mathbb{N}^1$(因为这是个一维讯号;它的定义域是一维的)且此情况中通道数 $c$ 是 $1$。2; 当 $c=2$ 这就成为了立体声的讯号。

对于一维的卷积,我们可以逐个核计算标量乘积(如图 4)。

4
图 4: 在一维卷积逐层进行标量乘积

Pytorch 中核的维度与输出的宽度

技巧:我们可以在 Ipython 中使用 问号 来取得函数的文件。例如

Init signature:
nn.Conv1d(
    in_channels,           # number of channels in the input image
    out_channels,          # number of channels produced by the convolution
    kernel_size,           # size of the convolving kernel
    stride=1,              # stride of the convolution
    padding=0,             # zero-padding added to both sides of the input
    dilation=1,            # spacing between kernel elements
    groups=1,              # nb of blocked connections from input to output
    bias=True,             # if `True`, adds a learnable bias to the output
    padding_mode='zeros',  # accepted values `zeros` and `circular`
)

一维卷积

我们用大小为 $3$,步幅 $1$ 的一维卷积使通道从 $2$ 个(立体声讯号)变为 $16$ 个($16$ 个核)。于是我们有 $16$ 个厚度为 $2$ 长度为 $3$ 的核。假设输入信号是一个大小 $1$ 通道 $2$ 个且有 $64$ 个样本的批次。生成的输出层会有 $1$ 个信号,具有 $16$ 个通道与长度 $62$ ($=64-3+1$)。并且,如果我们输出偏置的大小,会是 $16$ 因为我们每个权重都有一个偏置。

conv = nn.Conv1d(2, 16, 3)  # 2 channels (stereo signal), 16 kernels of size 3
conv.weight.size()          # output: torch.Size([16, 2, 3])
conv.bias.size()            # output: torch.Size([16])

x = torch.rand(1, 2, 64)    # batch of size 1, 2 channels, 64 samples
conv(x).size()              # output: torch.Size([1, 16, 62])

conv = nn.Conv1d(2, 16, 5)  # 2 channels, 16 kernels of size 5
conv(x).size()              # output: torch.Size([1, 16, 60])

二维卷积

首先我们定义输入资料有一个样本,$20$ 个通道(假设我们使用的是高光谱影像)高 $64$ 而 宽 $128$。二维的卷积有 $20$ 个来自输入的通道,与 $16$ 个 $3\times 3$ 大小的卷积核。卷积之后,输出的资料有一个样本,$16$ 个通道,高 $62$ $(=64-3+1)$ 且宽 $124$ $(=128-5+1)$。

x = torch.rand(1, 20, 64, 128)    # 1 sample, 20 channels, height 64, and width 128
conv = nn.Conv2d(20, 16, (3, 5))  # 20 channels, 16 kernels, kernel size is 3 x 5
conv.weight.size()                # output: torch.Size([16, 20, 3, 5])
conv(x).size()                    # output: torch.Size([1, 16, 62, 124])

如果我们想输出相同的维度,可以采用填充。以上方的代码为基础,我们可以于卷积函数添加新的参数:stride=1padding=(1, 2),表示 $y$ 方向填充 $1$(一上一下),$x$ 方向填充 $2$。如此,输出的信号就与输入信号有相同尺寸。储存二维卷积使用的卷积核共需要 $4$ 个维度。

# 20 channels, 16 kernels of size 3 x 5, stride is 1, padding of 1 and 2
conv = nn.Conv2d(20, 16, (3, 5), 1, (1, 2))
conv(x).size()  # output: torch.Size([1, 16, 64, 128])

自动梯度如何运作?

在这个部份,我们将使用 torch 来记录所有在张量上进行的运算,以计算偏导数。

  • 建立一个 $2\times2$ 且能累积梯度的张量 $\boldsymbol{x}$;
  • 从 $\boldsymbol{x}$ 的所有元素减去 $2$,取得 $\boldsymbol{y}$;(若我们印出 y.grad_fn 会得到 <SubBackward0 object at 0x12904b290> 代表 y 是从减法的模组 $\boldsymbol{x}-2$ 产生。我们也可以用 y.grad_fn.next_functions[0][0].variable 导出原始的张量。)
  • 再度进行运算:$\boldsymbol{z} = 3\boldsymbol{y}^2$;
  • 计算 $\boldsymbol{z}$ 的均值。
5
图 5:自动梯度范例的流程图

反向传播用于计算梯度。在这个例子里,反向传播的过程可视为计算梯度 $\frac{d\boldsymbol{a}}{d\boldsymbol{x}}$。如果手算 $\frac{d\boldsymbol{a}}{d\boldsymbol{x}}$ 作为验证,我们会发现执行 a.backward() 给我们的值等同于我们自己计算的 x.grad

这里是手动进行反向传播的过程:

\[\begin{aligned} a &= \frac{1}{4} (z_1 + z_2 + z_3 + z_4) \\ z_i &= 3y_i^2 = 3(x_i-2)^2 \\ \frac{da}{dx_i} &= \frac{1}{4}\times3\times2(x_i-2) = \frac{3}{2}x_i-3 \\ x &= \begin{pmatrix} 1&2\\3&4\end{pmatrix} \\ \left(\frac{da}{dx_i}\right)^\top &= \begin{pmatrix} 1.5-3&3-3\\[2mm]4.5-3&6-3\end{pmatrix}=\begin{pmatrix} -1.5&0\\[2mm]1.5&3\end{pmatrix} \end{aligned}\]

在 PyTorch 中,偏导数总是有与原资料相同的形状。不过雅各比矩阵其实应该是转置的。

从基础到更刺激的

考虑我们有 $1\times3$ 的向量 $x$,指定 $y$ 为 $x$ 的两倍并持续加倍直到范数不小于 $1000$。由于我们给 $x$ 的随机性,我们不能直接知道这个过程终止前经过多少次迭代。

x = torch.randn(3, requires_grad=True)

y = x * 2
i = 0
while y.data.norm() < 1000:
    y = y * 2
    i += 1

然而,靠着梯度就能简单的推测出来

gradients = torch.FloatTensor([0.1, 1.0, 0.0001])
y.backward(gradients)

print(x.grad)
tensor([1.0240e+02, 1.0240e+03, 1.0240e-01])
print(i)
9

在推断的时后,我们可以如下地使用 requires_grad=True 来标记想要记录梯度的张量。如果我们省去 $x$ 或 $w$ 的 requires_grad=True,将造成执行时错误,由于没有梯度积累于 $x$ 或 $w$。

# Both x and w that allows gradient accumulation
x = torch.arange(1., n + 1, requires_grad=True)
w = torch.ones(n, requires_grad=True)
z = w @ x
z.backward()
print(x.grad, w.grad, sep='\n')

我们也可利用 with torch.no_grad() 来避免梯度积累:

x = torch.arange(1., n + 1)
w = torch.ones(n, requires_grad=True)

# All torch tensors will not have gradient accumulation
with torch.no_grad():
    z = w @ x

try:
    z.backward()  # PyTorch will throw an error here, since z has no grad accum.
except RuntimeError as e:
    print('RuntimeError!!! >:[')
    print(e)

更多东西--自定梯度

此外,除了基础的数值运算,我们可以自定义模组、函数,并将其加入神经网络的图中。Jupyter Notebook 在这里

为此,我们要继承 torch.autograd.Function 并覆盖 forward()backward() 函数。举例而言,假如我们想训练网络,我们就必须取得前向传播,了解输入对输出的偏导数,如此我们才能在代码中的任何部份使用这个模组。接者,藉由反向传播(链式法则),我们可以在一连串的运算中的任何部份插入这个模组,因为我们知道了输入对输出的偏导数。

在这个案例,有三个 自定模组notebook 中,分别是 addsplitmax。例如自定的加法模组:

# Custom addition module
class MyAdd(torch.autograd.Function):

    @staticmethod
    def forward(ctx, x1, x2):
        # ctx is a context where we can save
        # computations for backward.
        ctx.save_for_backward(x1, x2)
        return x1 + x2

    @staticmethod
    def backward(ctx, grad_output):
        x1, x2 = ctx.saved_tensors
        grad_x1 = grad_output * torch.ones_like(x1)
        grad_x2 = grad_output * torch.ones_like(x2)
        # need to return grads in order
        # of inputs to forward (excluding ctx)
        return grad_x1, grad_x2

如果要将两数相加并输出,我们必须像这样覆盖 forward 函数。当我们进行反向传播,梯度会复制到两边,所以我们以复制来重写 backward 函数。

至于 splitmax,参阅 notebook,看看我们如何覆盖 forwardbackward 函数。当我们从一个东西 分割,计算梯度时要相加。对 argmax,因为它选择了最大值的索引,那个有着最大值的索引的梯度就是 $1$ 而其他是 $0$。记住,根据不同的自定模组,我们必须覆盖其前向传播与反向传播计算。


📝 Leyi Zhu, Siqi Wang, Tao Wang, Anqi Zhang
Titus Tzeng
25 Feb 2020