TextCNN原理解析与代码实现

2,559 阅读4分钟

TextCNN原理

  • Yoon Kim在论文(2014 EMNLP) Convolutional Neural Networks for Sentence Classification提出TextCNN。将卷积神经网络CNN应用到文本分类任务,利用多个不同size的kernel来提取句子中的关键信息(类似于多窗口大小的ngram),从而能够更好地捕捉局部相关性。
  • 每一个单词的embedding固定,所以kernel size的宽度不变,只能改变高度。kernel的通道可以理解为用不同的词向量表示。
  • 输入句子的长度不一样,但卷积核的个数一样。每个卷积核抽取单词的个数不一样,高度低的形成feature maps长度就较长,高度高的形成feature maps的长度就较短,但feature maps的长度不影响后面的输入,因为通过Max-over-time pooling层后,每一个feature maps都只取一个最大值,所以最终形成的向量与卷积核的个数一致。

  • TextCNN模型结构
    • TextCNN只能输入文本上纵向滑动,因为每个单词的embedding长度固定,不能截断。Filter的宽度要与输入向量一致,不同的Filter高度不一样。

  • TextCnn与image-CNN的差别:

    • 最大的不同便是在输入数据的不同;

    • 图像是二维数据, 图像的卷积核是从左到右, 从上到下进行滑动来进行特征抽取;

    • 自然语言是一维数据, 虽然经过word-embedding 生成了二维向量,但是对词向量只能做从上到下,做从左到右滑动来进行卷积没有意义;

    • 文本卷积宽度的固定的,宽度的就embedding的维度。文本中卷集核的设计和图像中的不同。

TextCNN训练详细过程

到顶

  Embedding:第一层是图中最左边的7乘5的句子矩阵,每行是词向量,维度=5,这个可以类比为图像中的原始像素点。
  Convolution:然后经过 kernel_sizes=(2,3,4) 的一维卷积层,每个kernel_size 有两个输出 channel。
  MaxPolling:第三层是一个1-max pooling层,这样不同长度句子经过pooling层之后都能变成定长的表示。
  FullConnection and Softmax:最后接一层全连接的 softmax 层,输出每个类别的概率。
  • 通道:
    • 图像中可以利用 (R, G, B) 作为不同channel;
    • 文本的输入的channel通常是不同方式的embedding方式(比如 word2vec或Glove),实践中也有利用静态词向量和fine-tunning词向量作为不同channel的做法。
  • 一维卷积(conv-1d):
    • 图像是二维数据;
    • 文本是一维数据,因此在TextCNN卷积用的是一维卷积(在word-level上是一维卷积;虽然文本经过词向量表达后是二维数据,但是在embedding-level上的二维卷积没有意义)。一维卷积带来的问题是需要通过 设计不同 kernel_size 的 filter 获取不同宽度的视野。

基于pytorch实现TextCNN模型

class Config(object):

    """配置参数"""
    def __init__(self, dataset):
    
        # Bert的输出词向量的维度
        self.hidden_size = 768
        # 卷积核尺寸
        self.filter_sizes = (2,3,4)
        # 卷积核数量
        self.num_filters = 256
        # droptout
        self.dropout = 0.5
class Model(nn.Module):
    def __init__(self, config):
        super(Model, self).__init__()
        self.bert = BertModel.from_pretrained(config.bert_path)
        for param in self.bert.parameters():
            param.requires_grad = True

        self.convs = nn.ModuleList(
             [nn.Conv2d(in_channels=1, out_channels=config.num_filters, kernel_size=(k, config.hidden_size)) for k in config.filter_sizes]
        )

        self.droptout = nn.Dropout(config.dropout)

        self.fc = nn.Linear(config.num_filters * len(config.filter_sizes), config.num_classes)

    def conv_and_pool(self, x, conv):
        x = conv(x)#  最后一个维度为1   :(input_height-kenl_size+padding*2)/stride[0]
        x = F.relu(x)
        x = x.squeeze(3)#去掉最后一个维度
        size = x.size(2)
        x = F.max_pool1d(x, size)
        x = x.squeeze(2)
        return x

    def forward(self, x):
        # x [ids, seq_len, mask]
        context = x[0] #对应输入的句子 shape[128,32]
        mask = x[2] #对padding部分进行mask shape[128,32]
        encoder_out, pooled = self.bert(context, attention_mask = mask, output_all_encoded_layers = False) 
        out = encoder_out.unsqueeze(1) #输入卷积需要四维的数据
        out = torch.cat([self.conv_and_pool(out, conv)for conv in self.convs], 1)
        out = self.droptout(out)
        out = self.fc(out)
        return out
  • 代码解析(Textcnn中维度的转化)

    上述代码采用bert模型进行预训练,输出的向量out的shape为[batch_size, sequence_length, hidden_size_size]。而nn.Conv2d需要输入4维度的向量,所以通过encoder_out.unsqueeze(1)在第一个位置上增加一个维度,表示通道的含义,shape为[batch_size,1, sequence_length, hidden_size_size]。conv_and_pool函数是池化层,经过conv(x)处理后,输出的维度为[batch_size, num_filters,seq_length,1]。最后两个维度有下图公式计算得出。在文本处理领域,经过conv(x)处理后的shape最后一个维度为1,所以通过x = x.squeeze(3)去掉最后一个维度。然后经过F.max_pool1d处理,在最后一个维度上取一个最大值并通过x = x.squeeze(2)消除最后一个维度,最终输出维度为[batch_size, num_filters]。最后将3个不同filter_sizes输出的结果在最后一个维度上进行拼接,输入到线性分类层。