本篇博客主要介绍基于TextCNN的文本分类算法的原理及实现细节。
目录
1. 分类原理
TextCNN可以从两个角度来解读,既可以把它看作但输入通道的2维卷积也可以把它看作多输入通道的1维卷积(其中词嵌入维度为通道维),二者其实是等价的。
如果把它看作一个单输入通道的2维卷积的话,它的分类流程就如上图所示。
1)把输入文本中的词转换为其对应的词向量,那么每个输入文本就可以表示为一个n*d的矩阵(n是输入文本包含的词数,d为词向量的维数)。
2)对输入矩阵进行卷积操作。可以使用不同大小的卷积核,每种类型的卷积核可以有多个。假设卷积核的大小是(f,d),f可以是不同的取值(如f=2,3,4),而d是固定的,是词向量的维度,并且假设总共使用了k个卷积核,步长为1。经过卷积操作后我们会得到k个向量,每个向量的长度是n-f+1. 我们使用不同大小的卷积核,从输入文本中提取丰富的特征,这和n-gram特征有点相似(f=2,3,4分别对应于2-gram,3-gram-4-gram)。
3)对卷积操作的输出进行全局max-pooling操作。作用于k个长度为n-f+1的向量上,每个向量整体取最大值,得到k个标量。
4)把k个标量拼接起来,组成一个向量表示最后提取的特征。他的长度是固定的,取决于我们所使用的不同大小的卷积核的总数(k)。
5)最后在接一个全联接层作为输出层,如果是2分类的话使用sigmoid激活函数,多分类则使用softmax激活函数,得到模型的输出。
2. 实现细节
#自定义时序(全局)最大池化层
class GlobalMaxPool1d(nn.Module):
def __init__(self):
super(GlobalMaxPool1d, self).__init__()
def forward(self, x):
# x (batch_size, channel, seq_len)
return F.max_pool1d(x, kernel_size=x.shape[2]) # (batch_size, channel, 1)
# 多输入通道的一维卷积和单输入通道的2维卷积等价
# 这里按多输入通道的一维卷积来做 也可以用单输入通道的2维卷积来做
class TextCNN(BasicModule): #继承自BasicModule 其中封装了保存加载模型的接口,BasicModule继承自nn.Module
def __init__(self, vocab_size, opt):#opt是config类的实例 里面包括所有模型超参数的配置
super(TextCNN, self).__init__()
# 嵌入层
self.embedding = nn.Embedding(vocab_size,opt.embed_size)#词嵌入矩阵 每一行代表词典中一个词对应的词向量;
# 词嵌入矩阵可以随机初始化连同分类任务一起训练,也可以用预训练词向量初始化(冻结或微调)
# 创建多个一维卷积层
self.convs = nn.ModuleList()
for c, k in zip(opt.num_channels, opt.kernel_sizes): #num_channels定义了每种卷积核的个数 kernel_sizes定义了每种卷积核的大小
self.convs.append(nn.Conv1d(in_channels=opt.embed_size,
out_channels=c,
kernel_size=k))
#定义dropout层
self.dropout = nn.Dropout(opt.drop_prop)
#定义输出层
self.fc = nn.Linear(sum(opt.num_channels), opt.classes)
# 时序最大池化层没有权重,所以可以共用一个实例
self.pool = GlobalMaxPool1d()
def forward(self, inputs):
# inputs(batch_size,seq_len)
embeddings = self.embedding(inputs) # (batch_size, seq_len, embed_size)
# 根据conv1d的输入要求 把通道维提前(这里的通道维是词向量维度)
# (batch_size,channel/embed_size,seq_len)
embeddings = embeddings.permute(0, 2, 1)
# 对于每个一维卷积层,会得到一个(batch_size,num_channel(卷积核的个数),seq_len-kernel_size+1)大小的tensor
# 在时序最大池化后会得到一个形状为(batch_size, num_channel, 1)的 tensor
# 使用squeeze去掉最后一维 并在通道维上连结 得到(batch_size,sum(num_channels))大小的tensor
encoding = torch.cat([self.pool(F.relu(conv(embeddings))).squeeze(-1) for conv in self.convs], dim=1)
# 应用丢弃法后使用全连接层得到输出 (batch_size,classes)
outputs = self.fc(self.dropout(encoding))
return outputs