前言
使用pytorch搭建TextCNN时,需要使用卷积层与池化层,以下对pytorch中的卷积及池化层的特点进行记录。pytorch中卷积与池化需要重点关注input_channels、output_channles、kernel_size,即“输入张量通道数量、输出张量通道数量、核大小。”
卷积层
Conv1d
一维卷积是指卷积核张量是一维,其调用形式为:
torch.nn.Conv1d(in_channels: int, out_channels: int,
kernel_size: Union[T, Tuple[T]], stride: Union[T, Tuple[T]] = 1,
NLP问题中,上述四个参数比较常用,kernel_size与stride参数可以是单个整数,也可以是元组包装的单个整数。假设输入张量的形状为(N, Cin, Lin),则输出张量的形状为(N, Cout, Lout),卷积计算在最后一个维度上进行。
卷积层中权重的形状为output_channels * (in_channels * kernel_size)
, 偏差的形状为output_channels
。
Conv2d
二维卷积是指卷积核张量的尺寸是二维,其调用为:
torch.nn.Conv2d(in_channels: int, out_channels: int,
kernel_size: Union[T, Tuple[T, T]], stride: Union[T, Tuple[T, T]] = 1
kernel_size与stride可以为整数或者元组,当为整数时,表示卷积核的长宽相当,为指定值;当为元组时,可以指定不同的长宽。对于stride也同理,表示向右向下移动的步长。卷积层中权重的形状为output_channels * (in_channels * (kernel_size_height * kernel_size_weight))
, 偏差的形状为output_channels
。
2D卷积运算中还有一种特殊尺寸的卷积核——1*1卷积核,1*1卷积可以实现“对不同通道的特征加权求和与非线性转换,以及压缩通道数”。比如2D卷积核的shape为(输出通道,输入通道, 1, 1),则权重参数有“输入通道“数量的参数,可以理解为对输入通道加权。当“输出通道”数量小于“输入通道”数量时,则可以实现压缩通道数量的效果。
最大池化层
MaxPool1d
一维最大池化在一个一维窗口中取最大值,窗口大小有kerner_size指定,是单个值,窗口的步长由stride指定,默认为kernel_size的值。注意池化层是没有可学习参数的,调用如下:
torch.nn.MaxPool1d(kernel_size: Union[T, Tuple[T, ...]],
stride: Optional[Union[T, Tuple[T, ...]]] = None
假设输入张量的形状为(N, C, Lin), 则输出的形状为(N, C, Lout)。注意卷积运算与池化运算的一个不同点,不论是一维还是二维运算,卷积运算都需要同时对输入的所有通道数据进行运算, 而池化运算是分别对通道数据进行运算。因此卷积核权重参数中的通道的数量,必须与输入数据的通道数量保持一致,而池化核则没有此要求。
MaxPool2d
对比一维最大池化,二维最大池化是在一个二维窗口中取最大值。
补充材料
pytorch搭建TextCNN,需要使用二维卷积与一维最大池化,因为TextCNN本质是抽取N-gram特征。
import torch
from torch import nn
import torch.nn.functional as F
from transformers import BertConfig, BertModel
class TextCNN(nn.Module):
def __init__(self):
""" 使用预训练模型训练的词嵌入向量 """
super(TextCNN, self).__init__()
self.embedding = nn.Embedding(21128, 768)
bare_bert = BertModel.from_pretrained('hfl/chinese-bert-wwm')
self.embedding.load_state_dict(bare_bert.embeddings.word_embeddings.state_dict(), strict=True)
self.fc1 = nn.Linear(768, 100) # 减低bert中的维度
self.kernel_sizes = [2, 3, 4]
# 对文本数据做二维卷积
self.conv2s = nn.ModuleList([nn.Conv2d(1, 32, kernel_size=(kernel_size, 100))
for kernel_size in self.kernel_sizes])
self.dropout = nn.Dropout(0.1)
self.fc2 = nn.Linear(32 * len(self.kernel_sizes), 33)
@staticmethod
def conv_and_maxpool(x, conv_layer):
"""
initial x.shape: (batch_size, seq_len, 100)
-> x.unsqueeze(1) -> shape: (batch_size, 1, seq_len, 100)
-> nn.Conv2d(x) -> shape: (batch_size, output_channels, seq_len - kernel_size + 1, 1)
-> squeeze(3) -> shape: (batch_size, output_channels, seq_len - kernel_size + 1)
-> max_pool1d -> shape: (batch_size, output_channels, 1)
-> squeeze(2) -> shape: (batch_size, output_channels)
"""
x = conv_layer(x.unsqueeze(1)).squeeze(3)
x = F.max_pool1d(x, x.shape[2], 1).squeeze(2)
return x
def forward(self, x, labels=None):
x = self.embedding(x)
x = self.fc1(x)
x = torch.cat([self.conv_and_maxpool(x, conv_layer=conv) for conv in self.conv2s], dim=1)
x = self.dropout(x)
logits = self.fc2(x)
if labels is None:
return logits
else:
loss = F.cross_entropy(logits, labels)
return loss, logits