Conv2D 主要用在图像特征提取,而对于文本数据我们一般用 Conv1D。怎么去理解 1D 和 2D?
首先,我们可以把 Conv2D 中的 channel 理解为 Conv1D 中的 length。
Conv2D 输入的数据形状为:(batch, channel, heght, width)
Conv1D 输入的数据形状为:(batch, length, dim)
除了 batch 和 channel 之外,Conv2D 还有 2 维,Conv1D 就剩下 1 维
接下来,我们了解下 Conv1D 参数的作用:
- Conv1D channels
- Conv1D kernel_size
- Conv1D padding
- Conv1D stride
- Conv1D delation
1. channels
channels 参数主要有:in_channels、out_channels,前者表示输入通道数量,后者表示输出通道数量。结合输入的文本数据 in_channels 表示输入序列的长度,out_channels 表示输出序列的长度。下面是一个例子,我们通过结果来分析下两个参数的作用:
import torch.nn as nn import torch # 固定随机数 torch.manual_seed(0) # channels def test01(): # 输入数据为: batch_size=2, length=3, dim=4 batch, length, dim = 2, 3, 4 inputs = torch.randint(0, 10, size=(batch, length, dim)).float() print('输入形状:', inputs.shape) conv1d = nn.Conv1d(in_channels=length, out_channels=5, kernel_size=2) output = conv1d(inputs) print('输出形状:', output.shape) if __name__ == '__main__': test01()
程序输出结果:
输入形状: torch.Size([2, 3, 4]) 输出形状: torch.Size([2, 5, 3])
从结果可知:
- 输入和输出的 batch_size 不会发生变化
- out_channels 影响了 length 的值, 由 3 变成了 5
- kernel_size 影响了 dim 维度,由 4 变成 3
2. kernel_size
在 channels 例子中,kernel_size 是如何影响计算结果的呢?接下来,我们就针对该问题来理解了 kernel_size 的计算规则和过程。
kernel_size (int or tuple) 表示卷积核的大小。我们在使用 Conv2D 时,卷积核大小一般都是 (m, n),即:卷积核可以在行和列方向移动。
对于文本数据,只能在列方向移动。卷积核大小一般设置为 (m,) 你可能会疑惑,怎么会只有一个维度?我们刚说了,对于文本数据只在列方向移动,另一个维度由 in_channels 来指定。比如我们输入的内容为:我是人
,其词向量表示为:
[[1, 8, 4, 3], [3, 2, 6, 9], [2, 2, 7, 1]]
in_channels 对应的值为 3,kernel_size 如何设置为 2 的话,那我们的卷积核大小为:(3, 2),行就自动设置为序列的长度。
这时,有个问题:我们希望对 我是人
在卷积时应该是:我是、是人,对应的应该是:
- [1, 8, 4, 3] 和 [3, 2, 6, 9] 卷积计算
- [3, 2, 6, 9] 和 [2, 2, 7, 1] 卷积计算
但是默认的计算方式却是:
- [1, 3, 2] 和 [8, 2, 2]
- [8, 2, 2] 和 [4, 6, 7]
- [4, 6, 7] 和 [3, 9, 1]
所以,我们在使用 Conv1D 对输入文本数据进行卷积计算时,会先将输入向量的维度转置,即: (length, dim) 转置为 (dim, length),转置之后的结果为:
[[1, 3, 2], [8, 2, 2], [4, 6, 7], [3, 9, 1]]
然后,再按照前面介绍的卷积计算规则就达到我们的预期。再结合 channels 小节的内容,我们实际计算的是转置后的矩阵,所以 in_channels 实际上并不是真正的 length, 而是 dim。同理,out_channels 就不再是影响文本数据的长度了,而是影响文本数据的维度。
示例代码:
import torch.nn as nn import torch # 固定随机数 torch.manual_seed(0) # kernel_size def test02(): batch, length, dim = 2, 3, 4 inputs = torch.randint(0, 10, size=(batch, length, dim)).float() print('输入形状:', inputs.shape) # 对输入数据的后两维转置 inputs = inputs.permute(0, 2, 1) print('转置形状:', inputs.shape) # 注意: in_channels 应该是 dim 值 conv1d = nn.Conv1d(in_channels=dim, out_channels=4, kernel_size=2) output = conv1d(inputs) print('输出形状:', output.shape) # 对输入数据的后两维转置 output = output.permute(0, 2, 1) print('恢复形状:', output.shape) if __name__ == '__main__': test02()
程序执行结果:
输入形状: torch.Size([2, 3, 4]) 转置形状: torch.Size([2, 4, 3]) 输出形状: torch.Size([2, 4, 2]) # 还原回原始的数据形状 恢复形状: torch.Size([2, 2, 4])
3. padding
我们再看下上面的卷积计算过程,我们转置之后的矩阵为:
[[1, 3, 2], [8, 2, 2], [4, 6, 7], [3, 9, 1]]
输入的是 3 列,结果卷积之后得到了 2 列,相当于输入了 3 个 token,结果得到了 2 个 token 的表征。注意:我们这里使用的是 kernel_size=2, 我们可以设置 padding=1,此时会在输入矩阵的两侧添加两列 0,如下所示:
[[0, 1, 3, 2, 0], [0, 8, 2, 2, 0], [0, 4, 6, 7, 0], [0, 3, 9, 1, 0]]
但是,我们发现卷积计算之后得到了 4 列,而不是希望的 3 列。这个比较简单,我们使用切片获得输出的 output[:, :, :padding] 就可以了。注意:这里的 padding 等于几,取决于我们使用 kernel_size 的大小。
我们通过一个代码例子,来看下计算过程:
import torch.nn as nn import torch # 固定随机数 torch.manual_seed(0) # padding def test03(): batch, length, dim = 2, 3, 4 inputs = torch.randint(0, 10, size=(batch, length, dim)).float() print('输入形状:', inputs.shape) # 对输入数据的后两维转置 inputs = inputs.permute(0, 2, 1) print('转置形状:', inputs.shape) # 注意: 设置 padding=1 padding = 1 conv1d = nn.Conv1d(in_channels=dim, out_channels=4, kernel_size=2, padding=padding) output = conv1d(inputs) print('输出形状:', output.shape) # 使用切片截取前面的 token output = output[:, :, :-padding] # 对输入数据的后两维转置 output = output.permute(0, 2, 1) print('恢复形状:', output.shape) if __name__ == '__main__': test03()
程序执行结果:
输入形状: torch.Size([2, 3, 4]) 转置形状: torch.Size([2, 4, 3]) 输出形状: torch.Size([2, 4, 4]) # 第二维的长度和输入形状的第二维相同了,达到了我们的目标 恢复形状: torch.Size([2, 3, 4])
4. stride
stride 默认步长。这个和 Conv2D 中的 stride 含义是相同的,只不过它指的是列的步长。例如,我们的输入转置之后的结果为:
[[1, 3, 2], [8, 2, 2], [4, 6, 7], [3, 9, 1]]
当 stride=2 时,只会计算前两列,即:[1, 8, 4, 3] 和 [3, 2, 6, 9],向右侧移动 2 个步长时没有内容了,故而不再计算,得到的结果是 1 列数据,而不是原来的 2 列数据。
示例代码:
import torch.nn as nn import torch # 固定随机数 torch.manual_seed(0) # stride def test04(): batch, length, dim = 2, 3, 4 inputs = torch.randint(0, 10, size=(batch, length, dim)).float() print('输入形状:', inputs.shape) # 对输入数据的后两维转置 inputs = inputs.permute(0, 2, 1) print('转置形状:', inputs.shape) # 注意: 设置 stride=2 stride = 2 conv1d = nn.Conv1d(in_channels=dim, out_channels=4, kernel_size=2, stride=stride) output = conv1d(inputs) print('输出形状:', output.shape) # 对输入数据的后两维转置 output = output.permute(0, 2, 1) print('恢复形状:', output.shape) if __name__ == '__main__': test04()
程序输出结果:
输入形状: torch.Size([2, 3, 4]) 转置形状: torch.Size([2, 4, 3]) 输出形状: torch.Size([2, 4, 1]) 恢复形状: torch.Size([2, 1, 4])
5. dilation
dilation 默认值是 1,表示元素的跨度,例如,我们的输入转置之后的结果为:
[[1, 3, 2], [8, 2, 2], [4, 6, 7], [3, 9, 1]]
- 如果 dilation=1, 则计算卷积时: [1, 8, 4, 3] 和 [3, 2, 6 ,9]、[3, 2, 6 ,9] 和 [2, 2, 7, 1]
- 如果 dilation=2, 则计算卷积时: [1, 8, 4, 3] 和 [2, 2, 7, 1],即:跳过了中间的 [3, 2, 6, 9]
通过这里我们可以看到,dilation 能够帮我们跳跃选取更大范围的 token 进行表征,并且不增加额外的计算量。这里需要注意,由于文本处理时,要求输入的 token 数量和输出的 token 数量一样,dilation 会导致输出 token 数量不同,这里需要添加 padding 来解决该问题。
也就是说,padding 值的设定是由 kernel_size 和 dilation 来决定的。当然还有 stride,我们这里先不考虑 stride 的问题。可以使用下面的公式来动态计算 padding 的值:
示例代码:
import torch.nn as nn import torch # 固定随机数 torch.manual_seed(0) # delation def test05(): batch, length, dim = 2, 3, 4 inputs = torch.randint(0, 10, size=(batch, length, dim)).float() # 对输入数据的后两维转置 inputs = inputs.permute(0, 2, 1) print('转置数据:\n', inputs) print('-' * 30) # 注意: 设置 dilation=2 dilation = 2 conv1d = nn.Conv1d(in_channels=dim, out_channels=4, kernel_size=2, dilation=dilation) # 固定权重和偏置 nn.init.constant_(conv1d.weight, 1) nn.init.constant_(conv1d.bias, 0) output = conv1d(inputs) print('输出数据:\n', output) if __name__ == '__main__': test05()
程序输出结果:
转置数据: tensor([[[4., 3., 7.], [9., 9., 3.], [3., 7., 1.], [0., 3., 6.]], [[6., 6., 6.], [9., 8., 9.], [8., 4., 1.], [6., 3., 4.]]]) ------------------------------ 输出数据: tensor([[[33.], [33.], [33.], [33.]], [[49.], [49.], [49.], [49.]]], grad_fn=<ConvolutionBackward0>)
从结果来看,当 dilation 设置为 2 时,第一个样本计算的是 [4, 9, 3, 0] 和 [7, 3, 1, 6] 的卷积结果。第二个样本计算的是 [6, 9, 8, 6] 和 [6, 9, 1, 4] 的卷积结果,即:跳跃了一列元素进行计算。