TextCNN(CNN for Sentence Classification)

在文本处理中,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>)
未经允许不得转载:一亩三分地 » TextCNN(CNN for Sentence Classification)
评论 (0)

6 + 1 =