文本分类模型(二)——DPCNN
文章目录
一、概述
DPCNN(Deep Pyramid Convolutional Neural Networksfor Text Categorization),是RieJohnson和腾讯AI-Lab等提出的一种深度卷积神经网络,可以称之为"深度金字塔卷积神经网络"。相关论文链接Deep Pyramid Convolutional Neural Networks for Text Categorization。
文献中提出了一种基于word-level级别的网络——DPCNN。由于TextCNN不能通过卷积获得文本的长距离依赖关系,而论文中DPCNN通过不断加深网络,可以抽取长距离的文本依赖关系。实验证明在不增加太多计算成本的情况下,增加网络深度就可以获得最佳的准确率。
DPCNN的深度与padding后的最大句子长度有关系,例如 m a x _ l e n = 64 = 2 5 max\_len=64=2^5 max_len=64=25,即长度为64的句子只够5次pooling/2,因此DPCNN更加适合长文本。
二、背景
Deep Pyramid Convolutional设计的目的,一是提高准确率;另一个则是在提高准确率的前提下,尽量的提升计算效率。
文本分类是一个十分重要的任务,其应用场景包括垃圾邮件检测,情感和主题分类等。研究显示:深度为32层字符级CNN显示出优于深度为9层字符级CNN;非常浅的1层单词级CNN被证明比非常深的字符级CNN更加准确且快得多。结合这两点,对深度的单词级CNN进行探究,文献中提出了一种深度但低复杂度的网络架构,名为DPCNN(深度金字塔卷积神经网络)。
DPCNN主要的特色是引入了残差结构,增加了多尺度信息,并且增加了用于文本分类CNN的网络深度,以提取文本中远程关系特征,并且并没有带来较高的复杂度。
三、DPCNN特点
- 1.减采样的设计。每层设计步长为2的卷积层,保持filter数目不变(默认250),像一个金字塔的形状(pyramid)。研究发现下采样时增加feature map的数量除了白白增加了工作量以外,对提升准确率没有很大的帮助。DPCNN中将每层的计算量减半,保持filter(feature mapping)的数目不变,省去维度匹配的计算时间,而且准确率不会受影响,
- 2.残差连接的设计。该结构仿照了resnet的shortcut的设计思路,并且将原有的卷积计算 σ ( W x + b ) \sigma(Wx+b) σ(Wx+b),修改为先将x进行激活 W σ ( x ) + b W\sigma(x)+b Wσ(x)+b,这样可以带来更好的效果;此外filter数目不变,可以省略维度匹配的时间。这种设计使得网络可以扩展为深层网络进行参数的训练。
- 3.Region Embeeding层。该结构接在embedding层后面。DPCNN的embedding层是one-hot的形式。在region=k的范围内,对word进行线性映射( W x + b Wx+b Wx+b),实质上相当于CNN+one-hot的实现。
- 4.Block结构:在每个卷积块之后都用大小为3的feature map进行stride为2的max-pooling。这样可以得到相邻3个文本区域的之间特征表示,而且stride为2的pooling操作将每个文本的中间表示直接减少了一半,从而使得每个卷积层的计算复杂度直接减少一半。此外pooling操作使得卷积操作的有效覆盖面积加倍。
四、DPCNN原理
4.1 模型结构
DPCNN主要的组成结构包括:
- Redion embedding层(文本区域嵌入层)
- 两个convolution block,每层block由两个固定卷积核为3的conv卷积函数构成,两个block构建的层可以pre-activation直接连接
- Repeat结构,与上一层相似,只不过在conv之前、pre-activate之后增加了Max-polling层
4.2 结构细节
1.模型输入:输入词维度为[batch_size, seq_len]。
2.Region Embeeding层
将TextCNN的包含多尺寸卷积滤波器的卷积层的卷积结果称之为Region embedding,意思就是对一个文本区域/片段(比如3-gram)进行一组卷积操作后生成的embedding。
此外为了进一步提高性能,还使用了tv-embedding (two-views embedding)进一步提高DPCNN的accuracy,也就是引入预训练的词向量.
经过embedding层,词向量维度为:[batch_size, seq_len, embedded_size]
3.等长卷积层
等长卷积(equal-width convolution):步长
s
=
1
s=1
s=1,两端补零
p
=
(
m
−
1
)
/
2
p=(m-1)/2
p=(m−1)/2,卷积后输出长度为
n
n
n。DPCNN模型中采用两层(+relu)每层均为250个尺寸为3的卷积核。
该层采用了pre-activation的做法,卷积运算是
W
σ
(
x
)
+
b
W\sigma(x)+b
Wσ(x)+b,而不是通常用的
σ
(
W
x
+
b
)
\sigma(Wx+b)
σ(Wx+b),这种“线性”简化了深度网络的训练。
经过两层的卷积之后,输出序列维度为:[batch_size, 250, seq_len - 3 + 1]
4.Repeat结构
等长卷积后,固定feature maps的数量(减少计算量等),再进行池化。模型中的池化是在每个卷积块后结束之后,对特征合集做一个池化,池化用的是最大池化(
p
o
o
l
_
s
i
z
e
=
3
,
s
t
r
i
d
e
=
2
pool\_size=3, stride=2
pool_size=3,stride=2),使每个卷积核的维度减半,形成一个金字塔结构。这种压缩式的Downsampling,可以拼接等实现文本远距离信息匹配于对应。
残差连接:为了使深度网络的训练成为可能,使用加法进行shortcut connections,接近等值连接,不需要做任何的维度匹配工作。 z + f ( z ) z+f(z) z+f(z),其中 f f f用的是两层的等长卷积,缓解了梯度消失问题。
该层的具体操作如下:
- 进行 p o o l _ s i z e = 3 , s t r i d e = 2 pool\_size=3, stride=2 pool_size=3,stride=2的Max-Pooling,将序列长度压缩为原来的二分之一。
- 两层等长卷积(+relu),每层都是250个尺寸为3的卷积核。
- 残差连接:将上述二者的结果相加
重复以上操作,直至序列长度等于1。
最终该结构的输出维度为:[batch_size, 250, 1]
5.全连接+softmax归一化:
将张量的维度从[batch_size, num_block]转化到[batch_size, 1]。
五、DPCNN模型的使用
5.1 文本二分类
首先从文件中读取相应训练集和测试集的语料,利用jieba对文本进行分词,然后利用nltk去停用词。
def tokenizer(text):
sentence = jieba.lcut(text, cut_all=False)
stopwords = stopwords.words('chinese')
sentence = [_ for _ in sentence if _ not in stopwords]
return sentence
利用torchtext包处理预处理好的语料,将所提供的语料集转化为相应的词向量模型。由于每一个词均为一个向量,作为模型的输入。
train_set, validation_set = data.TabularDataset.splits(
path='corpus_data/',
skip_header=True,
train='corpus_train.csv',
validation='corpus_validation.csv',
format='csv',
fields=[('label', label), ('text', text)],
)
text.build_vocab(train_set, validation_set)
将处理好的词向量输入到DPCNN模型中进行处理。
self.conv_region = nn.Conv2d(1, args.filter_num, (3, embedding_dim), stride=1)
self.conv = nn.Conv2d(args.filter_num, args.filter_num, (3, 1), stride=1)
self.max_pool = nn.MaxPool2d(kernel_size=(3, 1), stride=2)
self.padding1 = nn.ZeroPad2d((0, 0, 1, 1)) # top bottom
self.padding2 = nn.ZeroPad2d((0, 0, 0, 1)) # bottom
self.relu = nn.ReLU()
self.fc = nn.Linear(args.filter_num, label_num)
def forward(self, x):
# region embedding 层
# 输入x的维度为(batch_size, max_len)
x = self.embedding(x) # [batch_size, seq_length, embedding_dim]
x = x.view(x.size(0), 1, x.size(1), self.args.embedding_dim) # [(]batch_size, 1, seq_length, embedding_dim]
x = self.conv_region(x) # x = [batch_size, num_filters, seq_len-3+1, 1]
# 等长卷积层
x = self.padding1(x) # [batch_size, num_filters, seq_len, 1]
x = self.relu(x)
x = self.conv(x) # [batch_size, num_filters, seq_len-3+1, 1]
x = self.padding1(x) # [batch_size, num_filters, seq_len, 1]
x = self.relu(x)
x = self.conv(x) # [batch_size, num_filters, seq_len-3+1, 1]
# block结构
while x.size()[2] >= 2:
x = self._block(x) # [batch_size, num_filters,1,1]
x = x.squeeze() # [batch_size, num_filters]
# 全连接+输出
loggits = self.fc(x) # [batch_size, 1]
return loggits
def _block(self, x):
x = self.padding2(x)
px = self.max_pool(x)
x = self.padding1(px)
x = F.relu(x)
x = self.conv(x)
x = self.padding1(x)
x = F.relu(x)
x = self.conv(x)
x = x + px # Short Cut
return x
利用训练集对模型进行训练,同时评估训练效果,并利用测试集对模型的准确性进行评估。为了防止偶然性产生的不确定,每一轮迭代会产生100个模型,分别评估其效率,进行调优后再用测试集测试其效率。
在训练开始时,测试集的准确率在75%左右,然后逐步提高到90%。
多轮迭代之后,模型的准确率大概稳定在91%~92%之间。
5.2 文本多分类与可视化
从.csv文件中导入相关的训练集、评估集和测试集数据,并进行一定的处理,转化为DataSet。词向量采用globe.6B dim=300的已训练好的词向量。
ID = Field(sequential=False, use_vocab=False)
LABEL = LabelField(sequential=False, use_vocab=True, is_target=True)
INFO = Field(sequential=True, tokenize=jieba.lcut, include_lengths=True)
fields = [
('id', ID),
(None, None),
('label', LABEL),
('info', INFO),
]
# 加载数据
train_data = TabularDataset(
os.path.join('data', 'train.csv'),
format='csv',
fields=fields,
csv_reader_params={'delimiter': '\t'}
)
··· # 评估集和测试集数据加载省略
# 创建字典
INFO.build_vocab(train_data, vectors='glove.6B.300d')
LABEL.build_vocab(train_data)
return LABEL, INFO, train_data, valid_data, test_data
构建模型:通过nn所提供的函数,构造DPCNN的模型,以及其中的block结构。
class DPCNN(nn.Module):
def __init__(self, vocab_size, label_size, embedding_dim, hidden_dim,
layer_num, filter_num, dropout=float(0.1)):
super().__init__()
# embedding层
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.conv_region = nn.Conv2d(1, filter_num, (3, embedding_dim), stride=1)
self.conv = nn.Conv2d(filter_num, filter_num, (3, 1), stride=1)
self.max_pool = nn.MaxPool2d(kernel_size=(3, 1), stride=2)
self.padding1 = nn.ZeroPad2d((0, 0, 1, 1)) # top bottom
self.padding2 = nn.ZeroPad2d((0, 0, 0, 1)) # bottom
self.relu = nn.ReLU()
self.fc = nn.Linear(filter_num, label_size)
# 丢弃概率
self.dropout = nn.Dropout(p=dropout)
def forward(self, text):
embedded = self.embedding(text)
embedded = embedded.unsqueeze(1) # [batch_size, 1, seq_len, embedding_dim]
conv_r = self.conv_region(embedded) # [batch_size, num_filters, seq_len-3+1, 1]
conv_r_padding = self.padding1(conv_r) # [batch_size, num_filters, seq_len, 1]
conv_r_padding = self.relu(conv_r_padding)
conv = self.conv(conv_r_padding) # [batch_size, num_filters, seq_len-3+1, 1]
conv_padding = self.padding1(conv) # [batch_size, num_filters, seq_len, 1]
conv_padding = self.relu(conv_padding)
conv_padding = self.conv(conv_padding) # [batch_size, num_filters, seq_len-3+1, 1]
while conv_padding.size()[2] >= 2:
conv_padding = self._block(conv_padding) # [batch_size, num_filters,1,1]
conv_padding = conv_padding.squeeze() # [batch_size, num_filters]
loggits = self.fc(conv_padding) # [batch_size, 1]
return loggits
def _block(self, x):
x = self.padding2(x)
px = self.max_pool(x)
x = self.padding1(px)
x = F.relu(x)
x = self.conv(x)
x = self.padding1(x)
x = F.relu(x)
x = self.conv(x)
# Short Cut
x = x + px
return x
数据分批:将dataset中的数据进行分批,依次传入模型中运行。
train_iterator, valid_iterator, test_iterator = BucketIterator.splits(
(train_data, valid_data, test_data),
batch_size=args.batch_size,
sort_within_batch=True,
sort_key = lambda x: len(x.info),
device=torch.device('cpu')
)
使用TensorBoard可视化:
writer = SummaryWriter()
writer.add_scalar('训练集/损失率', loss.item(), step)
writer.add_scalar('训练集/学习率', scheduler.get_last_lr()[0], step)
···
writer.close()
tensorboard --logdir ./runs
TensorFlow installation not found - running with reduced feature set.
Serving TensorBoard on localhost; to expose to the network, use a proxy or pass --bind_all
TensorBoard 2.3.0 at http://localhost:6006/ (Press CTRL+C to quit)
多轮迭代之后,模型的准确率在82%左右。
查看模型在训练过程中的参数曲线,相关参数如下图所示:
六、总结
- DPCNN模型的设计思想:设计一个词级别的深度CNN
- 词序信息在文本分类中很重要,RNN和CNN都能获得词序信息,但由于CNN更简单并且容易并行,所以DPCNN中使用CNN
- CNN研究中,浅层词级别的CNN效果好于深度字级别的CNN,DPCNN中使用词级别作为输入
- CNN的词序信息有限(感受野),通过加深网络可以获得更大感受野,但是被证明加深网络词级别CNN性能反而下降。
- DPCNN模型的创新点:
- 预训练的Region Embedding
- Pre-activation Shortcut Connections
- 下采样不增加feature maps