文本分类(一) | (8) DPCNN

项目Github地址

本篇博客主要介绍基于DPCNN的文本分类算法的原理及实现细节。

目录

1. 分类原理

2. 实现细节


1. 分类原理

ACL2017年中,腾讯AI-lab提出了Deep Pyramid Convolutional Neural Networks for Text Categorization(DPCNN)。论文中提出了一种基于word-level级别的网络-DPCNN,由于之前介绍的TextCNN 不能通过卷积获得文本的长距离依赖关系,而论文中DPCNN通过不断加深网络,可以抽取长距离的文本依赖关系。实验证明在不增加太多计算成本的情况下,增加网络深度就可以获得最佳的准确率。‍

  • 等长卷积

首先交代一下卷积的的一个基本概念。一般常用的卷积有以下三类:

假设输入的序列长度为n,卷积核大小为m,步长(stride)为s,输入序列两端各填补p个零(zero padding),那么该卷积层的输出序列为(n-m+2p)/s+1。

1)窄卷积:步长s=1,两端不补零,即p=0,卷积后输出长度为n-m+1。

2)宽卷积:步长s=1,两端补零p=m-1,卷积后输出长度 n+m-1。

3)等长卷积: 步长s=1,两端补零p=(m-1)/2,卷积后输出长度为n。(不改变序列长度)

  • 池化

那么DPCNN是如何捕捉长距离依赖的呢?——Downsampling with the number of feature maps fixed。

作者选择了适当的两层等长卷积来提高词位embedding的表示的丰富性。然后接下来就开始 Downsampling (池化)。再每一个卷积块(两层的等长卷积)后,使用一个size=3和stride=2进行maxpooling进行池化。序列的长度就被压缩成了原来的一半。其能够感知到的文本片段就比之前长了一倍。

DPCNN中重复使用多个这种模块,每个模块包含两层等长卷积(不改变序列长度)和一个池化下采样(序列长度减半)。

例如之前是只能感知3个词位长度的信息,经过1/2池化层后就能感知6个词位长度的信息啦,这时把1/2池化层和size=3的卷积层组合起来如图所示。

  • 固定feature map(filters/卷积核)的数量

为什么要固定feature maps的数量呢?许多模型每当执行池化操作时,增加feature maps的数量(即序列减半,输出通道数加倍),导致总计算复杂度是深度的函数。与此相反,作者对feature map的数量进行了修正,他们实验发现增加feature map的数量只会大大增加计算时间,而没有提高精度。

固定了feature map的数量,每当使用一个size=3和stride=2进行max pooling进行池化时,每个卷积层的计算时间减半(数据大小减半/序列长度减半),从而形成一个金字塔。

剩下的我们就只需要重复的进行等长卷积+等长卷积+使用一个size=3和stride=2进行maxpooling进行池化就可以啦,DPCNN就可以捕捉文本的长距离依赖啦!还有一个问题,网络加深时,需要解决梯度消失问题。

  • shortcut connections with pre-activation

(1) 初始化CNN的时,往往各层权重都初始化为很小的值,这导致了最开始的网络中,后续几乎每层的输入都是接近0,这时的网络输出没有意义;

(2) 小权重阻碍了梯度的传播,使得网络的初始训练阶段往往要迭代好久才能启动;

(3) 就算网络启动完成,由于深度网络中仿射矩阵(每两层间的连接边)近似连乘,训练过程中网络也非常容易发生梯度爆炸或弥散问题。

当然,上述这几点问题本质就是梯度弥散问题。那么如何解决深度CNN网络的梯度弥散问题呢?当然是膜一下何恺明大神,然后把ResNet的精华拿来用啦! ResNet中提出的shortcut-connection/ skip-connection/ residual-connection(残差连接)就是一种非常简单、合理、有效的解决方案。

类似地,为了使深度网络的训练成为可能,作者为了恒等映射,所以使用加法进行shortcut connections,即z+f(z),其中 f 用的是两层的等长卷积(z和f(z)的形状相同,可以直接相加)。这样就可以极大的缓解了梯度消失问题。

另外,作者也使用了 pre-activation,这个最初在何凯明的“Identity Mappings in Deep Residual Networks上提及,有兴趣的大家可以看看这个的原理。直观上,这种“线性”简化了深度网络的训练,类似于LSTM中constant error carousels的作用。而且实验证明  pre-activation优于post-activation(即把激活函数放在卷积操作前面,卷积层的顺序为 BatchNorm-ReLU-Conv)。

  • Region Embedding

同时DPCNN的底层保持了跟TextCNN一样的结构,这里作者将TextCNN的包含多尺寸卷积滤波器的卷积层的卷积结果称之为Region embedding,意思就是对一个文本区域/片段(比如3gram,卷积核大小为3)进行一组卷积操作后生成的embedding。

另外,作者为了进一步提高性能,还使用了tv-embedding (two-views embedding)进一步提高DPCNN的accuracy。

2. 实现细节

class ResnetBlock(nn.Module):
    def __init__(self, channel_size):
        super(ResnetBlock, self).__init__()

        self.channel_size = channel_size
        #将序列长度减半
        self.maxpool = nn.Sequential(
            nn.ConstantPad1d(padding=(0, 1), value=0), #在每个通道上(一维) 一边填充0个0(不填充) 另一边填1个0
            nn.MaxPool1d(kernel_size=3, stride=2) #序列长度减半 height = (height-kernel_size+padding+stride)//stride=height // 2
        )
        self.conv = nn.Sequential(  #等长卷积 不改变height
            nn.BatchNorm1d(num_features=self.channel_size),
            nn.ReLU(),
            nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1),

            nn.BatchNorm1d(num_features=self.channel_size),
            nn.ReLU(),
            nn.Conv1d(self.channel_size, self.channel_size, kernel_size=3, padding=1),
        )

    def forward(self, x): #(batch_size,channel_size,seq_len)
        x_shortcut = self.maxpool(x) # (batch_size,channel_size,seq_len//2)
        x = self.conv(x_shortcut)#(batch_size,channel_size,seq_len//2)
        x = x + x_shortcut#(batch_size,channel_size,seq_len//2) shortcut 残差连结

        return x

class DPCNN(BasicModule):#继承自BasicModule 其中封装了保存加载模型的接口,BasicModule继承自nn.Module

    def __init__(self,vocab_size,opt): #opt是config类的实例 里面包括所有模型超参数的配置

        super(DPCNN, self).__init__()
        # 嵌入层
        self.embedding = nn.Embedding(vocab_size, opt.embed_size)#词嵌入矩阵 每一行代表词典中一个词对应的词向量;
        # 词嵌入矩阵可以随机初始化连同分类任务一起训练,也可以用预训练词向量初始化(冻结或微调)

        # region embedding
        self.region_embedding = nn.Sequential(
            nn.Conv1d(opt.embed_size, opt.channel_size, kernel_size=3, padding=1), #same卷积 不改变height/序列长度
            nn.BatchNorm1d(num_features=opt.channel_size),
            nn.ReLU(),
            nn.Dropout(opt.drop_prop_dpcnn)
        )

        #卷积块 same卷积 不改变height
        self.conv_block = nn.Sequential(
            nn.BatchNorm1d(num_features=opt.channel_size),
            nn.ReLU(),
            nn.Conv1d(opt.channel_size, opt.channel_size, kernel_size=3, padding=1),
            nn.BatchNorm1d(num_features=opt.channel_size),
            nn.ReLU(),
            nn.Conv1d(opt.channel_size, opt.channel_size, kernel_size=3, padding=1),
        )

        self.seq_len = opt.max_len #序列最大长度
        resnet_block_list = []  #存储多个残差块

        while (self.seq_len > 2): #每经过一个残差块 序列长度减半 只要长度>2 就不停地加残差块
            resnet_block_list.append(ResnetBlock(opt.channel_size))
            self.seq_len = self.seq_len // 2

        #将残差块 构成残差层 作为一个子模块
        self.resnet_layer = nn.Sequential(*resnet_block_list)

        #输出层 分类
        self.linear_out = nn.Linear(self.seq_len * opt.channel_size, opt.classes)

    def forward(self, inputs):

        embeddings = self.embedding(inputs) #(batch_size,max_len,embed_size)

        x = embeddings.permute(0, 2, 1) #(batch_size,embed_size,max_len) 交换维度 作为1维卷积的输入 embed_size 作为通道维
        x = self.region_embedding(x) #(batch_size,channel_size,max_len)

        x = self.conv_block(x) #(batch_size,channel_size,max_len)

        x = self.resnet_layer(x) #经过多个残差块 每次长度减半 (batch_size,channel_size,self.seq_len)

        x = x.permute(0, 2, 1)  #(batch_size,self.seq_len,channel_size)

        x = x.contiguous().view(x.size(0), -1) #(batch_size,self.seq_len*channel_size) 拉伸为向量
        out = self.linear_out(x)      #(batch_size,classes)
        return out
发布了365 篇原创文章 · 获赞 714 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/sdu_hao/article/details/103598710