文本卷积计算(Conv1D)

Conv2D 主要用在图像特征提取,而对于文本数据我们一般用 Conv1D。怎么去理解 1D 和 2D?

首先,我们可以把 Conv2D 中的 channel 理解为 Conv1D 中的 length。

Conv2D 输入的数据形状为:(batch, channel, heght, width)
Conv1D 输入的数据形状为:(batch, length, dim)

除了 batch 和 channel 之外,Conv2D 还有 2 维,Conv1D 就剩下 1 维

接下来,我们了解下 Conv1D 参数的作用:

  1. Conv1D channels
  2. Conv1D kernel_size
  3. Conv1D padding
  4. Conv1D stride
  5. 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])

从结果可知:

  1. 输入和输出的 batch_size 不会发生变化
  2. out_channels 影响了 length 的值, 由 3 变成了 5
  3. 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. [1, 8, 4, 3] 和 [3, 2, 6, 9] 卷积计算
  2. [3, 2, 6, 9] 和 [2, 2, 7, 1] 卷积计算

但是默认的计算方式却是:

  1. [1, 3, 2] 和 [8, 2, 2]
  2. [8, 2, 2] 和 [4, 6, 7]
  3. [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] 的卷积结果,即:跳跃了一列元素进行计算。

未经允许不得转载:一亩三分地 » 文本卷积计算(Conv1D)
评论 (0)

5 + 9 =