项目Github地址
本篇博客主要介绍基于TextCNN的文本分类算法的原理及实现细节。
目录
1. 分类原理
2. 实现细节
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.Moduledef __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))大小的tensorencoding = 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