TextCNN
TextCNN模型结构
TextCNN的详细过程原理图如下:
plot_model()画出的TextCNN模型结构图如下:
TextCNN的第一层为嵌入层。
获得单词嵌入向量的方式目前可以分为:预训练和“新训练”。预训练的词嵌入可以利用其它语料库的单词分布得到更多的先验知识,而通过当前网络训练的词嵌入可以更好地捕获与当前任务相关的单词分布特征。
嵌入层的输入是 [公式] 的矩阵,其中 [公式] 表示句子长度, [公式] 表示单词初始向量的维度。嵌入层的输出是 [公式] 的矩阵, [公式] 表示单词嵌入向量的维度。为了方便批处理,通常对长度不等的句子进行padding操作。
图3中的嵌入层采用了双通道(static与non-static)的形式,一个表示预训练的词嵌入,在训练过程中不再发生变化,另一个表示“参与网络训练”的词嵌入,其作为参数在训练过程中发生改变。
TextCNN的第二层为卷积层。
与CV领域不同的是,NLP中的卷积核只在一个方向上进行滑动。
可以试着这么理解这个区别。在图像中,每个像素都是一个特征,而图像具有长和宽。因此,应用于CV中的卷积核,为了捕获长和宽两个维度的局部特征,卷积核通常在长宽两个方向上按步长进行滑动(进行卷积操作)。而在句子中,一个单词是一个特征,也就是说,句子只具有长度一个维度。因此,应用于NLP中的卷积核,宽度与单词的嵌入维度相同,且卷积核只会在句子长度这一个方向上进行滑动(进行卷积操作)。
图3中,共有四个卷积核,两个大小为2的卷积核和两个大小为3的卷积核。
卷积层的输入是 [公式] 的句子矩阵,输出是 [公式] 的向量,其中k [公式] 表示卷积核的大小(长度)。
TextCNN的第三层是池化层。
与卷积层类似,NLP中的池化层也只在一个方向上进行Pooling操作。
图3中,池化层的输入是 [公式] 的向量,输出是一个 [公式] (标量)。
一个卷积操作+池化操作会获得一个 [公式] ,将具有相同卷积核(但是是不同的卷积核,即参数不同)大小( [公式] )的卷积操作结果再进行池化的 [公式] 拼在一起,即构成这个卷积核大小下的特征向量(feature vector)。
将不同卷积核大小的feature vector拼接在一起(final feature vector),作为输出层的输入。
TextCNN的第四层是输出层。
输出层是全连接层,使用Dropout防止发生过拟合。
输出层的输入是final feature vector,输出是类别的概率分布。
以上内容就是对TextCNN模型结构的详细解读了,接下来使用Pytorch搭建一个简易版的TextCNN来更加直观地理解其网络结构。
以下代码仅供参考:
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.modules.activation import ReLU
class Config(object):
max_seq_len = 16
embed_size = 128
kernel_num = 64
kernel_size = [2,3,4]
output_size = 10
dropout_p = 0.5
class textCNN(nn.Module):
def __init__(self, vocab_size: int, config: Config, embedding_pretrained=None):
super(textCNN, self).__init__()
#Embedding layer
if not embedding_pretrained:
self.embedding = nn.Embedding(vocab_size, config.embed_size)
else:#使用预训练词向量
self.embedding = nn.Embedding.from_pretrained(embedding_pretrained, freeze=False)
self.embedding.weight.requires_grad = True
#Conv layer + pooling layer
#conv_block_*: *表示卷积核的大小
self.conv_block_2 = nn.Sequential(
nn.Conv1d(config.embed_size, config.kernel_num, config.kernel_size[0]),
nn.ReLU(),#激活函数
nn.MaxPool1d(config.max_seq_len - config.kernel_size[0] + 1)#输入n*l,卷积核为k,卷积操作之后,输出为(n-k+1)*1.
)
self.conv_block_3 = nn.Sequential(
nn.Conv1d(config.embed_size, config.kernel_num, config.kernel_size[1]),
nn.ReLU(),#激活函数
nn.MaxPool1d(config.max_seq_len - config.kernel_size[1] + 1)#输入n*l,卷积核为k,卷积操作之后,输出为(n-k+1)*1.
)
self.conv_block_4 = nn.Sequential(
nn.Conv1d(config.embed_size, config.kernel_num, config.kernel_size[2]),
nn.ReLU(),#激活函数
nn.MaxPool1d(config.max_seq_len - config.kernel_size[2] + 1)#输入n*l,卷积核为k,卷积操作之后,输出为(n-k+1)*1.
)
self.dropout = nn.Dropout(p=config.dropout_p)
#Output layer: FC layer
#卷积核大小有2,3,4;分别有64个.
#每个卷积核+池化操作-->一个Scalar;将所有经过卷积+池化操作获得Scalar拼接,形成final feature vector
self.fc = nn.Linear(config.kernel_num * len(config.kernel_size), config.output_size)
# #多类别分类任务
# self.output = nn.Softmax()
def forward(self, x):
#x.shape: (batch_size, max_seq_len)
e = self.embedding(x)#e.shape: (batch_size, max_seq_len, embed_size)
e = e.permute(0, 2, 1)#e.shape: (batch_size, embed_size, max_seq_len)
conv_block_2 = self.conv_block_2(e)#conv_block_2.shape: (batch_size, kernel_num, 1)
conv_block_3 = self.conv_block_3(e)#conv_block_2.shape: (batch_size, kernel_num, 1)
conv_block_4 = self.conv_block_4(e)#conv_block_2.shape: (batch_size, kernel_num, 1)
#torch.squeeze(tensor)#删除tensor中所有为1的维度
#tensor.squeeze(dim=i)#若tensor的第i维度为1,则将其删除
#conv_block_i.squeeze(2).shape: (batch_size, kernel_num)
#feature_vector.shape: (batch_size, kernel_num * 3)
feature_vector = torch.cat((conv_block_2.squeeze(2), conv_block_3.squeeze(2), conv_block_4.squeeze(2)), 1)
feature_vector_after_dropout = self.dropout(feature_vector)
output = self.fc(feature_vector_after_dropout)#output.shape: (batch_size, output_size)
output_prabability = F.softmax(output, dim=1)
return output_prabability, {
"e": e,
"conv_block_2": conv_block_2,
"conv_block_3": conv_block_3,
"conv_block_4": conv_block_4,
"feature_vector": feature_vector,
"output": output
}
上述模型几乎是遵循《Convolutional Neural Networks for Sentence Classification》论文的描述构建的,唯一的不同在于:论文中词向量采用了static和non-static两种方式,而上述模型仅采用non-static方式。由于textCNN模型的构建中明确声明了self.embedding.weight.requires_grad = True,所以不管词向量是基于“预训练”模式还是“新训练”模式,其在训练过程中都会发生改变。
基于模型结构,仿造合适大小的数据送至textCNN模型,我们就可以观察模型的详细结构描述和模型每一层的输入输出大小。
#查看模型结构
config = Config()
vocab_size = 100
input = torch.LongTensor([[i+1 for i in range(16)], [(i+1)*2 for i in range(16)]])#batch_size: 2, max_seq_len: 16
textcnn = textCNN(vocab_size=vocab_size, config=config)
print(textcnn)
output_p, tmp = textcnn(input)
print(tmp["e"].size())
print(tmp["conv_block_2"].size())
print(tmp["conv_block_3"].size())
print(tmp["conv_block_4"].size())
print(tmp["feature_vector"].size())
print(tmp["output"].size())
print(output_p)
# textCNN(
# (embedding): Embedding(100, 128)
# (conv_block_2): Sequential(
# (0): Conv1d(128, 64, kernel_size=(2,), stride=(1,))
# (1): ReLU()
# (2): MaxPool1d(kernel_size=15, stride=15, padding=0, dilation=1, ceil_mode=False)
# )
# (conv_block_3): Sequential(
# (0): Conv1d(128, 64, kernel_size=(3,), stride=(1,))
# (1): ReLU()
# (2): MaxPool1d(kernel_size=14, stride=14, padding=0, dilation=1, ceil_mode=False)
# )
# (conv_block_4): Sequential(
# (0): Conv1d(128, 64, kernel_size=(4,), stride=(1,))
# (1): ReLU()
# (2): MaxPool1d(kernel_size=13, stride=13, padding=0, dilation=1, ceil_mode=False)
# )
# (dropout): Dropout(p=0.5, inplace=False)
# (fc): Linear(in_features=192, out_features=10, bias=True)
# )
# torch.Size([2, 128, 16])
# 卷积操作之后输出大小为 (2, 64, max_seq_len - k + 1)# 池化操作之后输出大小为 (2, 64, 1)
# torch.Size([2, 64, 1])
# torch.Size([2, 64, 1])
# torch.Size([2, 64, 1])
# torch.Size([2, 192])
# torch.Size([2, 10])
# tensor([[0.1259, 0.0255, 0.2410, 0.0230, 0.1196, 0.0401, 0.0580, 0.0461, 0.1468,
# 0.1739],
# [0.0644, 0.0564, 0.1970, 0.0447, 0.0918, 0.0630, 0.0518, 0.0409, 0.1474,
# 0.2426]], grad_fn=)