在文本处理中,Conv1D 可以处理序列数据。假设我们有一段文本序列,首先会通过词嵌入将每个单词转换成固定维度的向量,然后使用 Conv1D 对这些向量进行卷积操作,以捕获不同大小的上下文窗口中的特征。
1. 计算过程
nn.Conv1d
的输入形状通常为 (batch_size, in_channels,
,对应于输入的文本数据形状 seq_len
)(batch_size, token_dim, seq_len)
,所以,in_channels 对应与 token 的 embbeding 维度,out_channel 对应了输出的维度。
import torch import torch.nn as nn def demo(): batch_size, token_dim, seq_len = 2, 4, 3 inputs = torch.randn(batch_size, token_dim, seq_len) # 输入形状 (batch_size, in_channels, seq_len) # in_channels 表示输入 token 的向量维度 print(inputs.shape) conv = nn.Conv1d(in_channels=token_dim, out_channels=8, kernel_size=2) # 卷积核参数形状: (out_channel, in_channels, kernel_size) # kernel_size: 卷积核在 seq_len 方向上按照 kernel_size 滑动窗口计算 print(conv.weight.shape) outputs = conv(inputs) # 输出形状: (batch_size, out_channel, seq_len) print(outputs.shape) if __name__ == '__main__': demo()
torch.Size([2, 4, 3]) torch.Size([8, 4, 2]) torch.Size([2, 8, 2])
上面的代码计算,我们发现一个小问题:输入的是 3 个 token,卷积计算之后得到了 2 个 token。我们可以设置 padding=1,此时会在输入矩阵的两侧添加两列 0。例如:
[[1, 3, 2], [8, 2, 2], [4, 6, 7], [3, 9, 1]] [[0, 1, 3, 2, 0], [0, 8, 2, 2, 0], [0, 4, 6, 7, 0], [0, 3, 9, 1, 0]]
但是,我们发现卷积计算之后得到了 4 个 token,而不是输入的 3 个 token。这个比较简单,我们使用切片获得输出的 output[:, :, :padding]
就可以了。注意:这里的 padding 等于几,取决于我们使用 kernel_size 的大小。
文本卷积计算时,还有另外一个参数 dilation,它的默认值是 1。假设,输入形状为: (1, 4, 3):
[[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=1 ,则可以使用下面的公式来动态计算 padding 的值:

2. 文本分类
TextCNN(Convolutional Neural Networks for Sentence Classification)是一种基于 1D 卷积(Conv1D) 处理文本的深度学习模型,由 Kim (2014) 提出。它适用于 文本分类任务,如情感分析、新闻分类、垃圾邮件检测等。网络结构:
- 词嵌入层(Embedding):将输入文本转换为词向量矩阵。
- 多个 1D 卷积层(Conv1D):使用不同的卷积核(例如 3, 4, 5)提取 n-gram 语义特征。
- 最大池化层(Max Pooling):从每个卷积核的输出中选取最重要的特征值。
- 拼接层(Concatenation):将不同卷积核的池化输出拼接在一起。
- 全连接层(Fully Connected):用于最终的分类任务。
- Dropout:防止过拟合。
import torch import torch.nn as nn import torch.nn.functional as F class TextCNN(nn.Module): def __init__(self, vocab_size, embed_dim, num_classes, kernel_sizes=[3, 4, 5], num_filters=100, dropout=0.5): super(TextCNN, self).__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) self.convs = nn.ModuleList([ nn.Conv1d(in_channels=embed_dim, out_channels=num_filters, kernel_size=k) for k in kernel_sizes ]) self.dropout = nn.Dropout(dropout) self.fc = nn.Linear(num_filters * len(kernel_sizes), num_classes) def forward(self, inputs): # 输入形状:(batch_size, seq_len, embed_dim) inputs = self.embedding(inputs) # 装换形状:(batch_size, embed_dim, seq_len) inputs = inputs.permute(0, 2, 1) # 应用不同尺度文本卷积 inputs = [ conv(inputs) for conv in self.convs ] # 应用 ReLU 激活函数 inputs = [ F.relu(i) for i in inputs ] # [torch.Size([1, 100, 6]), torch.Size([1, 100, 5]), torch.Size([1, 100, 4])] # print([i.shape for i in inputs]) # 应用最大池化 inputs = [ i.max(dim=2)[0] for i in inputs ] # [torch.Size([1, 100]), torch.Size([1, 100]), torch.Size([1, 100])] # print([i.shape for i in inputs]) inputs = torch.cat(inputs, dim=1) # 拼接不同卷积核的输出 print(inputs.shape) inputs = self.dropout(inputs) return self.fc(inputs) if __name__ == '__main__': estimator = TextCNN(vocab_size=100, embed_dim=8, num_classes=2) inputs = torch.randint(0, 100, size=(1, 8)) logits = estimator(inputs) print(logits)
torch.Size([1, 300]) tensor([[0.1518, 0.4898]], grad_fn=<AddmmBackward0>)