终于到了现代一点的模型了,为了整明白这些折腾了很久。很多大佬都是基于图像领域讲解的,但是对于我这种没整过图像的就很难受了。接下来就谈一谈自己对两个模型的理解
TextCNN
这是很著名的cs231n给出的模型图,可以看到CNN有四个层组成
conv:卷积层
就是用一个称之为卷积核的矩阵(滤波器(filter))做内积,就是逐个元素相乘再求和。每一步求出来的是一个值!具体如图
具体看算式。这样就把输入抽象化了
这个矩阵有几个参数,常用的就是深度,步长和填充值。
深度:上图中的conv层是深度=1的特殊情况,深度=2就两个二维矩阵,就两个滤波器,就两个filter。
步长:一次卷积滑动几步
填充值:为了总长能被步长整除,所以可能要加几圈0.
这里depth(深度)=2,stride(步长)=2,padding(填充值)=1
这里有几个特殊的创新点
局部感知:每次滤波器只对一个局部进行卷积,就像人一次只能看到固定长度的文字
权重共享:滤波器的权重不变
RELU,激励函数
Relu的全称是线性整流函数,作用是:1、克服梯度消失问题2、加快训练速度
Pooled,池化层
取某个区域内的平均,极值等,主要是压缩数据,减少过拟合。
平铺层
这里将特征最终输出为最终结果,采用一个全连接。具体可以参考全连接网络
下面来聊一聊代码,代码来自https://github.com/graykode/nlp-tutorial/blob/master/2-1.TextCNN/TextCNN.ipynb
里面代码写的真的很好,强推强推。
哦对,代码之前先把论文内的模型图放出来
这里输入采用Ont-hot编码,输入层的尺寸是[word_len,dic_len] ,其中word_len是一次输入单词的个数,dic_len是词典的单词量。因为是用Ont-hot嘛。但是这样作者认为这样不够,太稀疏了怎么训练?因此你看到的输入其实是经过Embedding过来的结果!经过一层Embedding层后我们才得到了[word_len,em_len]的输入,其中em_len是经过Embedding层后输出的维度。
红色的框框是一个卷积核一次卷积的大小,也就是卷积核的大小。可以看到这里的卷积核是不规则的。虽然论文里叫他卷积核,但我认为也有一些N-grams的意思。图片中有两个这样的图所以batch size=2。这样一个三维的输入就做成了!
接下来得到卷积后的结果再池化,全连接就结束了,还是比较简单的。下面再看一下代码
embedding_size = 2 # embedding size
sequence_length = 3 # sequence length
num_classes = 2 # number of classes
filter_sizes = [2, 2, 2] # n-gram windows
num_filters = 3 # number of filters
class TextCNN(nn.Module):
def __init__(self):
super(TextCNN, self).__init__()
self.num_filters_total = num_filters * len(filter_sizes)
self.W = nn.Embedding(vocab_size, embedding_size)
self.Weight = nn.Linear(self.num_filters_total, num_classes, bias=False)
self.Bias = nn.Parameter(torch.ones([num_classes]))
self.filter_list = nn.ModuleList([nn.Conv2d(1, num_filters, (size, embedding_size)) for size in filter_sizes])
def forward(self, X):
embedded_chars = self.W(X) # [batch_size, sequence_length, sequence_length]
embedded_chars = embedded_chars.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
pooled_outputs = []
for i, conv in enumerate(self.filter_list):
# conv : [input_channel(=1), output_channel(=3), (filter_height, filter_width), bias_option]
h = F.relu(conv(embedded_chars))
# mp : ((filter_height, filter_width))
mp = nn.MaxPool2d((sequence_length - filter_sizes[i] + 1, 1))
# pooled : [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3)]
pooled = mp(h).permute(0, 3, 2, 1)
pooled_outputs.append(pooled)
h_pool = torch.cat(pooled_outputs, len(filter_sizes)) # [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3) * 3]
h_pool_flat = torch.reshape(h_pool, [-1, self.num_filters_total]) # [batch_size(=6), output_height * output_width * (output_channel * 3)]
model = self.Weight(h_pool_flat) + self.Bias # [batch_size, num_classes]
return model
分开来看一下
self.num_filters_total = num_filters * len(filter_sizes)
self.W = nn.Embedding(vocab_size, embedding_size)
self.Weight = nn.Linear(self.num_filters_total, num_classes, bias=False)
self.Bias = nn.Parameter(torch.ones([num_classes]))
self.filter_list = nn.ModuleList([nn.Conv2d(1, num_filters, (size, embedding_size)) for size in filter_sizes])
num_filters_total定义了卷积核的数量,我的理解是作者想尝试不同尺寸的卷积核的效果。其实一个的话也可以实现。
self.W定义了Embedding层,大小是[词典长度,想变成的长度]。作者把One-hot编码变成了一个2维的embedding变量
Weight和Bias为线性层的参数
filter_list就是卷积层的集合啦
embedded_chars = self.W(X) # [batch_size, sequence_length, sequence_length]
embedded_chars = embedded_chars.unsqueeze(1) # add channel(=1) [batch, channel(=1), sequence_length, embedding_size]
首先将模型放入embedding层生成词向量,此时的词向量是三维的,但是torch的CNN输入要是四维,缺一个通道。所以第二行使用unsqueeze加了一个通道,这样就能愉快的训练了
pooled_outputs = []
for i, conv in enumerate(self.filter_list):
# conv : [input_channel(=1), output_channel(=3), (filter_height, filter_width), bias_option]
h = F.relu(conv(embedded_chars))
# mp : ((filter_height, filter_width))
mp = nn.MaxPool2d((sequence_length - filter_sizes[i] + 1, 1))
# pooled : [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3)]
pooled = mp(h).permute(0, 3, 2, 1)
pooled_outputs.append(pooled)
这里是使用不同的卷积层进行操作,这里使用了一个小小的技巧
F.relu(conv(embedded_chars))因为conv层不需要更改输出就可以进入激活层,所以可以嵌套使用
但是进入池化层的话需要更改一下输入的量,并使用permute函数对输出的值交换一下次序,方便下一次继续操作。
h_pool = torch.cat(pooled_outputs, len(filter_sizes)) # [batch_size(=6), output_height(=1), output_width(=1), output_channel(=3) * 3]
h_pool_flat = torch.reshape(h_pool, [-1, self.num_filters_total]) # [batch_size(=6), output_height * output_width * (output_channel * 3)]
model = self.Weight(h_pool_flat) + self.Bias # [batch_size, num_classes]
return model
这里将所有结果拼接之后输入到线性层,线性层得到最后的分类结果并输出
具体的训练过程大家可以看上面的链接,这里就不加赘述了