1. 假设我们有两个卷积核,大小分别为k1和k2(中间没有非线性激活函数)。
- 证明运算可以用单次卷积来表示。
- 这个等效的单个卷积核的维数是多少呢?
- 反之亦然吗?
假设我们有两个卷积核,大小分别为 k1 和 k2,在中间没有非线性激活函数。我们将证明连续的两次卷积操作可以用单次卷积操作来表示,并确定等效的单个卷积核的维数。
1. 证明运算可以用单次卷积来表示
考虑一个输入信号 X,它首先通过卷积核 k1 进行卷积得到中间结果 M,然后 M 再通过卷积核 k2 进行卷积得到最终输出 Y。数学上,这可以表示为:
M=X∗k1
Y=M∗k2=(X∗k1)∗k2
由于卷积运算具有结合律,即:
(X∗k1)∗k2=X∗(k1∗k2)
我们可以将两次卷积操作合并为一次卷积操作,其中新的卷积核 k 是原来的两个卷积核 k1 和 k2 的卷积结果:
k=k1∗k2
因此,运算可以用单次卷积来表示:
Y=X∗k
2. 等效的单个卷积核的维数
假设 k1 和 k2 的大小分别为 (m1×n1) 和 (m2×n2),则等效的单个卷积核 k 的大小为 (m1+m2−1)×(n1+n2−1)。
这是因为卷积核的卷积(即两个核的交互)会导致核的尺寸增大,其大小由原来两个核的大小决定:
Size(k)=Size(k1)+Size(k2)−1
3. 反之亦然吗?
反之,即将一个卷积核分解为两个较小的卷积核,通常是不可行的。这是因为:
- 将一个卷积操作分解为两个连续的卷积操作实际上是一种分解操作,这相当于在信号处理或矩阵运算中的因式分解。
- 一般情况下,卷积核没有唯一的分解形式,尤其是对高维信号或大尺寸核的分解可能不存在或不唯一。
虽然对于特定的卷积核,可能存在一些特定的分解方法(例如在某些特定情况下,利用SVD分解核矩阵),但这些方法依赖于卷积核的特性,无法作为通用的方法来适用所有卷积核。
总结起来:
- 证明运算可以用单次卷积来表示:两个卷积核的连续卷积可以合并为一个卷积核。
- 等效的单个卷积核的维数:新卷积核的维数为 (m1+m2−1)×(n1+n2−1)。
- 反之亦然吗:一般情况下不可行,因为卷积核的分解在数学上并没有唯一的解。
2. 假设输入为ci×h×w,卷积核大小为co×ci×kh×kw,填充为(ph,pw),步幅为(sh,sw)。
- 前向传播的计算成本(乘法和加法)是多少?
- 内存占用是多少?
- 反向传播的内存占用是多少?
- 反向传播的计算成本是多少?
1. 前向传播的计算成本(乘法和加法)
假设输入张量 X 的形状为 (ci,h,w),卷积核 K 的形状为 (co,ci,kh,kw),填充为 (ph,pw),步幅为 (sh,sw)。卷积运算后,输出张量 Y 的形状为 (co,ho,wo),其中:
ho=⌊shh+2ph−kh⌋+1
wo=⌊sww+2pw−kw⌋+1
对于每个输出元素 Yco,ho,wo ,需要 ci×kh×kw 次乘法和 ci×kh×kw−1 次加法。因此,总的乘法和加法次数为:
-
乘法次数:
乘法次数=co×ho×wo×(ci×kh×kw)
-
加法次数:
加法次数=co×ho×wo×(ci×kh×kw−1)
2. 内存占用
内存占用主要包括输入张量、卷积核和输出张量的内存。
-
输入张量 X 的内存占用:
输入张量=ci×h×w
-
卷积核 K 的内存占用:
卷积核=co×ci×kh×kw
-
输出张量 Y 的内存占用:
输出张量=co×ho×wo
总的内存占用为:
总内存=ci×h×w+co×ci×kh×kw+co×ho×wo
3. 反向传播的内存占用
反向传播时,我们需要存储以下内容:
- 输入张量 X : ci×h×w
- 卷积核 K : co×ci×kh×kw
- 输出张量 Y : co×ho×wo
- 梯度张量 ∂Y∂L : co×ho×wo
- 输入梯度 ∂X∂L : ci×h×w
- 卷积核梯度 ∂K∂L : co×ci×kh×kw
因此,反向传播的总内存占用为:
反向传播总内存=2×(ci×h×w)+2×(co×ci×kh×kw)+2×(co×ho×wo)
4. 反向传播的计算成本
反向传播时,计算成本主要包括计算梯度的乘法和加法操作:
计算 ∂X∂L
每个输入元素 ∂Xci,h,w∂L 需要 co×kh×kw 次乘法和 co×kh×kw−1 次加法。
-
乘法次数:
乘法次数∂X∂L=ci×h×w×(co×kh×kw)
-
加法次数:
加法次数∂X∂L=ci×h×w×(co×kh×kw−1)
计算 ∂K∂L
每个卷积核元素 ∂Kco,ci,kh,kw∂L 需要 ho×wo 次乘法和 ho×wo−1 次加法。
-
乘法次数:
乘法次数∂K∂L=co×ci×kh×kw×(ho×wo)
-
加法次数:
加法次数∂K∂L=co×ci×kh×kw×(ho×wo−1)
计算 ∂Y∂L
这个是已知的损失梯度传递,无需额外计算。
总计算成本
-
乘法次数:
总乘法次数=ci×h×w×(co×kh×kw)+co×ci×kh×kw×(ho×wo)
-
加法次数:
总加法次数=ci×h×w×(co×kh×kw−1)+co×ci×kh×kw×(ho×wo−1)
总结如下:
-
前向传播的计算成本(乘法和加法次数):
- 乘法次数:co×ho×wo×(ci×kh×kw)
- 加法次数:co×ho×wo×(ci×kh×kw−1)
-
内存占用:
ci×h×w+co×ci×kh×kw+co×ho×wo
-
反向传播的内存占用:
2×(ci×h×w)+2×(co×ci×kh×kw)+2×(co×ho×wo)
-
反向传播的计算成本(乘法和加法次数):
- 乘法次数:ci×h×w×(co×kh×kw)+co×ci×kh×kw×(ho×wo)
- 加法次数:ci×h×w×(co×kh×kw−1)+co×ci×kh×kw×(ho×wo−1)
3. 如果我们将输入通道ci和输出通道co的数量加倍,计算数量会增加多少?如果我们把填充数量翻一番会怎么样?
让我们详细分析一下输入通道 ci 和输出通道 co 数量加倍以及填充数量翻一番对计算数量的影响。
1. 输入通道和输出通道数量加倍
前向传播计算成本
假设原始输入通道数量为 ci,输出通道数量为 co。若将输入通道和输出通道数量加倍,新的通道数量分别为 2ci 和 2co。
前向传播的计算成本(乘法和加法)原始为:
- 乘法次数:co×ho×wo×(ci×kh×kw)
- 加法次数:co×ho×wo×(ci×kh×kw−1)
新情况下:
- 乘法次数:2co×ho×wo×(2ci×kh×kw)=4×(co×ho×wo×(ci×kh×kw))
- 加法次数:2co×ho×wo×(2ci×kh×kw−1)=4×(co×ho×wo×(ci×kh×kw))−2×(co×ho×wo)
可以看到,当输入通道和输出通道加倍时,前向传播的乘法和加法次数大约增加了四倍。
内存占用
内存占用原始为:
ci×h×w+co×ci×kh×kw+co×ho×wo
新情况下:
2ci×h×w+2co×2ci×kh×kw+2co×ho×wo=2×(ci×h×w)+4×(co×ci×kh×kw)+2×(co×ho×wo)
当输入通道和输出通道加倍时,内存占用约增加两倍至四倍。
反向传播计算成本
原始反向传播计算成本为:
- 乘法次数:ci×h×w×(co×kh×kw)+co×ci×kh×kw×(ho×wo)
- 加法次数:ci×h×w×(co×kh×kw−1)+co×ci×kh×kw×(ho×wo−1)
新情况下:
- 乘法次数:2ci×h×w×(2co×kh×kw)+2co×2ci×kh×kw×(ho×wo)=4×(ci×h×w×(co×kh×kw))+4×(co×ci×kh×kw×(ho×wo))
- 加法次数:2ci×h×w×(2co×kh×kw−1)+2co×2ci×kh×kw×(ho×wo−1)=4×(ci×h×w×(co×kh×kw−1))+4×(co×ci×kh×kw×(ho×wo−1))
当输入通道和输出通道加倍时,反向传播的乘法和加法次数大约增加了四倍。
2. 填充数量翻一番
填充数量翻一番对计算成本的影响如下:
前向传播计算成本
填充数量 (ph,pw) 翻一番后,新填充数量为 (2ph,2pw)。
填充只会影响输出特征图的大小 ho 和 wo,具体公式如下:
ho=⌊shh+2ph−kh⌋+1
wo=⌊sww+2pw−kw⌋+1
假设原始输出大小为 ho 和 wo,新情况下填充翻倍后输出大小变为 ho′ 和 wo′,如下:
ho′=⌊shh+4ph−kh⌋+1
wo′=⌊sww+4pw−kw⌋+1
这意味着输出特征图的大小可能增大,具体增大多少取决于输入大小、卷积核大小和步幅,但不会改变计算的乘法和加法次数的规模性。
内存占用
由于填充增加会导致输出特征图变大,影响如下:
- 输出张量 Y 的内存占用变大:co×ho′×wo′
但输入张量和卷积核的内存占用保持不变。因此,填充翻倍后内存占用增加量主要取决于输出特征图增大的程度。
反向传播计算成本和内存占用
反向传播计算成本和内存占用同样受输出特征图大小变化影响,具体变化与前向传播类似:
- 乘法次数:增加比例取决于 ho′ 和 wo′
- 加法次数:增加比例取决于 ho′ 和 wo′
总结:
-
输入通道和输出通道数量加倍:
- 计算数量增加约四倍。
- 内存占用增加约两倍至四倍。
- 反向传播计算成本增加约四倍。
-
填充数量翻一番:
- 计算数量增加比例取决于输出特征图的增加量。
- 内存占用增加比例取决于输出特征图的增加量。
- 反向传播计算成本增加比例取决于输出特征图的增加量。
总体来看,输入通道和输出通道数量加倍对计算成本和内存占用的影响较大,而填充数量翻一番主要影响输出特征图的大小,进而影响计算成本和内存占用。
4. 如果卷积核的高度和宽度是kh=kw=1,前向传播的计算复杂度是多少?
当卷积核的高度和宽度都是 kh=kw=1 时,我们可以简化卷积操作的计算复杂度分析。对于多输入多输出通道的卷积操作,计算复杂度取决于输入张量的大小、卷积核的大小、输出通道数等参数。
符号说明
- X 是输入张量,形状为 (ci,h,w)。
- K 是卷积核张量,形状为 (co,ci,1,1)。
- Y 是输出张量,形状为 (co,h,w)。
前向传播计算复杂度
计算复杂度推导
对于每个输出通道 co 和每个输出位置 (i,j) 处的计算,前向传播涉及的乘法和加法如下:
-
每个输出位置的计算:
- 每个输出位置 Yco,i,j 的计算公式:
Yco,i,j=∑ciXci,i,j×Kco,ci,1,1
- 由于卷积核的大小是 1×1,每个输出位置的计算涉及 ci 次乘法和 ci−1 次加法。
-
每个输出通道的计算:
- 每个输出通道有 h×w 个位置,每个位置需要进行 ci 次乘法和 ci−1 次加法。
- 因此,每个输出通道总共需要进行 ci×h×w 次乘法和 (ci−1)×h×w 次加法。
-
所有输出通道的计算:
- 总共有 co 个输出通道,因此总的计算量为:
总乘法次数=co×ci×h×w
总加法次数=co×(ci−1)×h×w
计算复杂度总结
当卷积核大小为 1×1 时,前向传播的总计算复杂度(包括乘法和加法)可以总结如下:
总计算复杂度=乘法次数+加法次数=co×ci×h×w+co×(ci−1)×h×w
因为加法次数和乘法次数是同阶的,常见情况下可以简化为主要考虑乘法次数:
总计算复杂度≈co×ci×h×w
示例
假设输入张量 X 的形状为 (3,32,32),卷积核张量 K 的形状为 (64,3,1,1),那么:
- ci=3(输入通道数)
- h=32(输入高度)
- w=32(输入宽度)
- co=64(输出通道数)
前向传播的总计算复杂度为:
总计算复杂度≈64×3×32×32=196,608
这表明在 1×1 卷积核情况下,计算复杂度主要依赖于输入输出通道数和输入特征图的尺寸。在这种情况下,计算复杂度相对较低,因此 1×1 卷积在深度学习模型中广泛用于降低计算成本和参数量。
5. 本节最后一个示例中的变量Y1
和Y2
是否完全相同?为什么?
def corr2d_multi_in_out(X, K):
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
tensor([[[-1.5243, 1.0310, -1.1528],
[-0.7533, 0.6093, 0.1063],
[ 0.4095, 1.3068, -0.2419]],
[[-1.5065, 1.9989, -2.2026],
[-1.9817, 1.5140, -0.8929],
[ 0.3886, 0.2262, -0.2948]]])
tensor([[[-1.5243, 1.0310, -1.1528],
[-0.7533, 0.6093, 0.1063],
[ 0.4095, 1.3068, -0.2419]],
[[-1.5065, 1.9989, -2.2026],
[-1.9817, 1.5140, -0.8929],
[ 0.3886, 0.2262, -0.2948]]])
tensor([[[True, True, True],
[True, True, True],
[True, True, True]],
[[True, True, True],
[True, True, True],
[True, True, True]]])
示例中的变量 Y1
和 Y2
应该是完全相同的。以下是详细解释为什么它们会相同。
代码实现回顾
函数 corr2d_multi_in_out
这个函数实现了标准的二维卷积操作。对于多输入通道和多输出通道的卷积核,它逐通道地计算输入张量 X
和每个卷积核的卷积,并将结果堆叠在一起。
def corr2d_multi_in_out(X, K):
return torch.stack([corr2d_multi_in(X, k) for k in K], 0)
X
是形状为 (ci,h,w) 的输入张量。
K
是形状为 (co,ci,kh,kw) 的卷积核张量。
corr2d_multi_in(X, k)
对输入 X
和每个卷积核 k
进行多通道二维卷积。
- 最终结果
Y2
是所有输出通道的卷积结果堆叠在一起,形状为 (co,ho,wo)。
函数 corr2d_multi_in_out_1x1
这个函数专门处理卷积核大小为 1×1 的情况。它将卷积操作简化为矩阵乘法。
def corr2d_multi_in_out_1x1(X, K):
c_i, h, w = X.shape
c_o = K.shape[0]
X = X.reshape((c_i, h * w))
K = K.reshape((c_o, c_i))
Y = torch.matmul(K, X)
return Y.reshape((c_o, h, w))
X
的形状是 (ci,h,w),卷积核 K
的形状是 (co,ci,1,1)。
X
被重塑为 (ci,h×w)。
K
被重塑为 (co,ci)。
- 使用矩阵乘法计算结果
Y
,形状为 (co,h×w),然后重塑为 (co,h,w)。
数学解释
对于 1×1 卷积核,卷积操作可以看作对输入张量的每个位置应用一个线性变换(矩阵乘法),这等价于:
Yco,i,j=∑ciXci,i,j×Kco,ci,1,1
为什么 Y1
和 Y2
应该相同?
-
操作本质一致:
- 在
corr2d_multi_in_out_1x1
中,将 1×1 卷积核的卷积运算转化为矩阵乘法,计算每个位置的线性组合。
- 在
corr2d_multi_in_out
中,使用标准卷积操作计算每个位置的线性组合,卷积核也是 1×1。
- 这两种方法本质上都在对输入张量的每个位置应用相同的线性变换。
-
精度:
- 数值计算中的浮点运算精度问题可能会引起微小的误差,但这些误差通常在合理范围内,不影响结果的总体一致性。
- 在这个例子中,
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
验证了 Y1
和 Y2
在数值上几乎完全一致,误差小于 10−6,这表示它们可以被认为是相同的。
验证结果
X = torch.normal(0, 1, (3, 3, 3))
K = torch.normal(0, 1, (2, 3, 1, 1))
Y1 = corr2d_multi_in_out_1x1(X, K)
Y2 = corr2d_multi_in_out(X, K)
assert float(torch.abs(Y1 - Y2).sum()) < 1e-6
Y1
是通过矩阵乘法得到的结果。
Y2
是通过标准卷积操作得到的结果。
assert
语句验证了 Y1
和 Y2
在数值上几乎完全一致。
因此,Y1
和 Y2
在理论上和实现上应该是完全相同的。即使在浮点数运算中可能存在微小的误差,这些误差在实际应用中通常可以忽略不计。
6. 当卷积窗口不是1×1时,如何使用矩阵乘法实现卷积?
当卷积窗口不是 1×1 时,卷积操作依然可以使用矩阵乘法来实现。这种方法称为 im2col 技术(即将图像数据重排列为列),然后将其与卷积核进行矩阵乘法。下面是实现步骤和原理:
基本思想
- 将输入张量
X
转换为列矩阵:通过将每个卷积窗口的元素重排列成列,我们可以将卷积操作转换为矩阵乘法。
- 将卷积核
K
展开成行矩阵:将卷积核重排列成一个二维矩阵,以便与重排后的输入进行矩阵乘法。
- 进行矩阵乘法:通过矩阵乘法计算卷积结果。
- 将结果重排回卷积后的形状:将矩阵乘法的结果重排为卷积后的输出形状。
详细步骤
输入张量 X
和卷积核 K
的定义
假设输入张量 X
的形状为 (ci,h,w),卷积核 K
的形状为 (co,ci,kh,kw)。
1. 将输入张量 X
转换为列矩阵
使用 im2col 技术:
- 对于输入张量中的每一个卷积窗口(大小为 kh×kw),将其展平成一列。
- 结果是一个新的矩阵,形状为 (ci×kh×kw,h′×w′),其中 h′ 和 w′ 是输出张量的高度和宽度。
import torch
import torch.nn.functional as F
def im2col(X, k_h, k_w, stride, padding):
X_unfold = F.unfold(X, kernel_size=(k_h, k_w), stride=stride, padding=padding)
return X_unfold
X = torch.randn(1, 3, 5, 5)
k_h, k_w = 3, 3
stride, padding = 1, 1
X_col = im2col(X, k_h, k_w, stride, padding)
2. 将卷积核 K
展开成行矩阵
- 将卷积核
K
展开为一个二维矩阵,形状为 (co,ci×kh×kw)。
K = torch.randn(2, 3, 3, 3)
K_reshaped = K.view(2, -1)
3. 进行矩阵乘法
- 矩阵乘法:将重排后的输入矩阵与卷积核矩阵进行矩阵乘法。
Y_col = K_reshaped @ X_col
4. 将结果重排回卷积后的形状
batch_size, _, h_out, w_out = X.shape[0], K.shape[0], (X.shape[2] - k_h + 2 * padding) // stride + 1, (X.shape[3] - k_w + 2 * padding) // stride + 1
Y = Y_col.view(batch_size, -1, h_out, w_out)
完整代码示例
import torch
import torch.nn.functional as F
def im2col(X, k_h, k_w, stride, padding):
X_unfold = F.unfold(X, kernel_size=(k_h, k_w), stride=stride, padding=padding)
return X_unfold
X = torch.randn(1, 3, 5, 5)
K = torch.randn(2, 3, 3, 3)
k_h, k_w = 3, 3
stride, padding = 1, 1
X_col = im2col(X, k_h, k_w, stride, padding)
K_reshaped = K.view(2, -1)
Y_col = K_reshaped @ X_col
batch_size, _, h_out, w_out = X.shape[0], K.shape[0], (X.shape[2] - k_h + 2 * padding) // stride + 1, (X.shape[3] - k_w + 2 * padding) // stride + 1
Y = Y_col.view(batch_size, -1, h_out, w_out)
print(Y.shape)
计算复杂度分析
通过这种方法实现卷积,计算复杂度主要由矩阵乘法的复杂度决定:
- 将输入转换为列矩阵的复杂度:O(ci×kh×kw×h′×w′)。
- 卷积核重排后的矩阵乘法复杂度:O(co×(ci×kh×kw)×(h′×w′))。
- 重排回输出形状的复杂度:O(co×h′×w′)。
总体来说,通过矩阵乘法实现卷积在实际应用中能够利用矩阵运算的高度优化和并行计算特性,从而提升计算效率,尤其是在GPU上。