⽂本分类中CNN-LSTM融合原理
CNN-LSTM融合原理
阅读这篇⽂章需要较扎实的CNN和RNN基础。
怎么把CNN结合LSTM做⽂本分类,宏观的看,我以为有三种⽅法:东莞读音
1. CNN-> LSTM:将⽂本先经过CNN提取局部特征,再⽤LSTM提取这些局部特征的长距离特征,再经变换输⼊全连接层。
2. LSTM-> CNN:先对⽂本⽤LSTM提取长距离特征,得到融合了上下⽂的新⽂本,再⽤CNN提取新⽂本的局部特征(有点像先
Word2Vec再CNN),再经变换输⼊全连接层。
3. CNN、LSTM同步:将CNN提取的局部特征和LSTM提取的长距离特征同时连接起来作为全连接层(DNN)的输⼊。
⽂章⽬录
0. ⽤到的参数2021四级考试时间
依次对这三种⽅法做详解:
1. CNN-> LSTM
核⼼思想:将CNN的输出作为LSTM的输⼊,由于CNN设置了多个卷积核尺⼨,因此LSTM也要做多次,最后做维度变换后作为全连接层的输⼊
recycle是什么意思
解决⽅法:
我们都知道,TextCNN只卷积不池化,因为池化会丢失⽂本的位置信息,再使⽤LSTM就没意义了。
(ps:纯CNN是要池化的,但是我们要结合LSTM,必须保留⽂本的位置信息,因此不做池化操作),所以TextCNN的输出格式为:
out_cnn:[batch_size, [Doc_size-Filter_size], Filter_Num, 1]
不同深度学习平台的输⼊输出形式可能有差异,能理解我的意思就⾏,这⾥以pytorch为例。
参数解释:
三胎配套支持措施
batch_size:每批样本的个数;
Filter_size:卷积核的⼤⼩,⼀个列表,如[1, 2, 4, 6];
[Doc_size-Filter_size]: 可以理解为经卷积过后的⽂本长度/词数;
Filter_Num: 每个尺⼨卷积核的数量,int;
1:词向量维度,因为TextCNN的卷积核的宽度和词向量维度⼀致,因此卷积后只剩1
由于最后⼀维维度为1,所以先把out最后⼀维去除,因此
out:[batch_size, [Doc_size-Filter_size], Filter_Num]
假设batch_size=64,Doc_size=80,Filter_size =[1, 2, 4, 6],Filter_Num=200
⾸先搞清楚:
我们卷积的时候分别以[1, 2, 4, 6]为卷积核长度,对单个⽂本进⾏特征提取,于是会⽣成80/79/77/75长度的特征图,每种卷积核有200个,于是每个词组(词组长度为1, 2, 4, 6)都会得到长度为200的特征向量,于是每个样本都会得到长为80/79/77/75,宽度为200的特征图,尺⼨:[80/79/77/75, 200],为此,做了⼀个草图帮助理解:
以卷积核长度为2,步长为1为例:
那么我们每个样本都会有尺⼨分别为80×200、79×200、77×200、75×200的4个特征图。看形状是不是特别像我们的⽂本矩阵?没错,我们将⽂本的词组合成了词组的形式,每个词组都有200个特征,这就相等于⽂本有80/79/77/75个词,每个词都有⼀个200维的词向量。因此,我们可以将80/79/77/75视为⽂本长度,将200视为词向量维度,共有4个特征图。
由于我们有4个特征图,那么我们需要进⾏4次LSTM,每次LSTM的输⼊为:
input=[64, 80/79/77/75, 200]
每次LSTM的输出为:
out: [64, 80/79/77/75, num_directions*hidden_dim]
h_n: [num_layers * num_directions, 64, hidden_dim]
num_directions: LSTM的⽅向数,若为双向LSTM,该值则为2,单向为1
num_layers:LSTM中神经⽹络隐含层层数
hidden_dim:LSTM中神经⽹络隐含层节点数/隐藏状态数
out是所有时间步的输出,可以⽤来做⽂本预测或者视为⼀个含有上下⽂语义的全新⽂本矩阵。
h_n是最后⼀个时间步的输出,⼀般⽤来做⽂本分类。
由于我们进⾏了4次LSTM,因此我们会得到4个out和4个h_n,由于我们是做⽂本分类(先LSTM再CNN要提取out),因此只需要h_n,先把h_n的第⼀维和第三维连接,即:
h_n=[64, num_layers * num_directions * hidden_dim]
再把4个h_n的第⼆维合并,得到全连接层的input:
input=[64, num_layers * num_directions * hidden_dim *4]
此时input只剩两维,可以作为全连接层的输⼊层进⾏学习。
附上forward的源码:
def forward(lf, batch_x):
# batch_x:[64, 1, 80, 300]
out =[conv(batch_x)for conv vs]
# 经卷积后 [64,1,80,300] -> [64,Filter_Num,80,1],[64,Filter_Num,79,1],[64,Filter_Num,77,1]
input=[]
for i in range(len(out)):
# print('out[{}]:'.format(i),out[i].size())
temp = out[i].squeeze(dim=-1)# 将最后⼀维去掉 [64,Filter_Num,80/79/...,1]->[64,Filter_Num,80/79/...] # print('降维后:', temp.size())
temp = temp.permute(0,2,1)# 2,3维转换
# print('转化维度后:', temp.size())
重庆化妆学校
jar of love什么意思# 现在输出格式为[64, 80/79/..., Filter_Num],若把80/79/...视为⽂本长度,那么Filter_Num个卷积值视为词特征
temp, h_n = lf.lstm(temp)# 把[64, 80/78/..., Filter_Num]依次放⼊lstm进⾏学习
# print('h_n:', h_n.size()) # h_n:[num_layers * num_directions, batch_size, hidden_dim]
# 合并h_n的⼀三维:先把h_n转化为列表,在合并h_n的最后⼀维
x =[]
for i in range(h_n.size(0)):
x.append(h_n[i,:,:])
x = torch.cat(x, dim=-1)
# print('x:', x.size())
# x:[batch_size, num_layers * num_directions * hidden_dim]
input.append(x)
# input:[batch_size, num_layers * num_directions, hidden_dim] * Filter_size
input= torch.cat(input, dim=-1)# ⽤cat连接第⼆维 input-> [batch_size, num_layers * num_directions * hidden_dim* Filter_size] output = lf.den(input)# 全连接层
output = lf.output(output)# 输出[batch_size,6],这个输出矩阵,⾏表⽰样本,列表⽰各分类概率
return output
实际上,维度变换有两种⽅法:
1. 先合并h_n的第⼀三维再合并4个h_n的hidden_dim
2. 先合并4个h_n的hidden_dim再合并h_n的第⼀三维
第⼀种⽅法就是上⽂所说⽅法,第⼆种:
[num_layers * num_directions, batch_size, hidden_dim] * Filter_size ->
[num_layers * num_directions, batch_size, hidden_dim* Filter_size] ->
[batch_size, num_layers * num_directions * hidden_dim* Filter_size]
经实验证明,第⼀种和第⼆种⽅法效果差不多,但是从逻辑上讲,第⼀种⽅法较为合理。
2. LSTM-> CNN
核⼼思想:将LSTM的输出经维度变换后作为CNN的输⼊
解决⽅法:
⽐起CNN-> LSTM简单很多。
我们知道LSTM的输⼊形式:
input_lstm : [batch_size, Doc_size, WordVec_size]
最好的翻译软件
参数解释:
batch_size:批样本个数
Doc_size:⽂本长度
WordVec_size:词向量/特征维度
假设batch_size=64, Doc_size=80, WordVec_size=300
输出形式:
out: [64, 80, num_directions*hidden_dim]
h_n: [num_layers * num_directions, 64, hidden_dim]
前⽂说了,out是所有时间步的输出,因此可以视为⼀个包含了上下⽂语义的新⽂本矩阵,因此out可以作为CNN的输⼊,但是CNN的输⼊形式:
input_cnn : [batch_size, channels, Doc_size, WordVec_size]
out少了第⼆维,于是⽤unsqueeze()把out加⼀维即可:
out: [64, 1, 80, num_directions*hidden_dim]
带⼊CNN计算,卷积+最⼤池化,得到输出:
output_cnn : [64, 200, 1, 1]
200为每种卷积核个数,和前⽂是⼀个意思。curity是什么意思
等等,前⽂不是说TextCNN不能池化吗,当然纯TextCNN是不能的,但是我们已经⽤了LSTM提取了长距离特征,所以CNN的输⼊已经有了长距离特征,我们只需要继续提取短距离特征即可,全连接层会帮我们决定哪些操作是对的哪些操作不对,因此我们只需要把所有可能的特征提取出来即可。
继续做维度变换使其作为全连接层的输⼊,即把最后俩维去除即可:
output_cnn : [64, 200]
只剩两维,这个是符合全连接层的输⼊形式的。
扩展:实际上我们的池化⽅法还有别的选择,如K-MaxPooling、Chunk-MaxPooling、meanPooling等,如果选择K-MaxPooling、Chunk-MaxPooling那么第三维可能就不是1了,⽐如我⽤3-MaxPooling,他的输出形式是这样的:
output_cnn : [64, 200, 3, 1]
这个时候就需要把⼆三维连接再作为全连接层的输⼊。
附上forward代码:
def forward(lf, batch_x):
# batch_x:[64, 80, 300]
out, h_n = lf.lstm(batch_x)# out: [64, 80, num_directions*hidden_dim]
out = out.unsqueeze(dim=1)# out: [64, 1, 80, num_directions*hidden_dim] 为了进⾏CNN,在第⼆维加⼀维# print('out: ', out.size()) # out:[64, 80, num_directions*hidden_dim]
out =[conv(out)for conv vs]
# 经卷积后 [64,1,80,num_directions*hidden_dim] -> [64,Filter_Num,80,1],[64,Filter_Num,77,1],[64,Filter_Num,71,1] # 池化-> [64,Filter_Num,KMax,1]*3(out是list对象,⾥⾯的元素为tensor[64,Filter_Num,KMax,1])
# print('卷积池化后out: ', out[0].size()) # [64,Filter_Num,KMax,1] * n
out = torch.cat(out, dim=1)# 对应第⼆个维度(⾏)拼接起来,[64,Filter_Num,KMax,1]*3->[64,Filter_Num*3,KMax,1] out = out.squeeze(dim=-1)# 将最后⼀维去掉 [64,Filter_Num*n,KMax]
# print('拼接后out: ', out.size())
# 先获取第⼆维的out,再对第⼀维进⾏concat ,即[64,Filter_Num*n*KMax]
x =[]
for i in range(out.size(1)):
smartfilemanx.append(out[:, i,:])
out = torch.cat(x, dim=1)
# print('降维后out: ', out.size())
output = lf.den(out)# 全连接层
output = lf.output(output)# 输出[batch_size,6],这个输出矩阵,⾏表⽰样本,列表⽰各分类概率return output
3. LSTM+CNN
主要思想:CNN和LSTM的输出共同作为DNN的输⼊
实现⽅法:
LSTM输⼊:
input_lstm = [64,80,300]
带⼊LSTM得到h_n和out,取h_n:
h_n: [num_directions * n_layers, 64, hidden_dim]
经维度变换后,得到⼆维的全连接层第⼀个输⼊,设为x1:
x1: [64, num_directions * n_layers * hidden_dim]
CNN输⼊:
input_cnn = [64,1,80,300]
带⼊CNN得到out:
医疗保险英文
out: [64, Filter_Num, KMax, 1]* 卷积核种数
经维度变换后,得到⼆维的全连接层第⼆个输⼊,设为x2:
x2: [64, Filter_Num * KMax * 卷积核种数]
将x1、x2的第⼆维合并,得到全连接层的输⼊:
input:[64, (num_directions * n_layers * hidden_dim)+( Filter_Num * KMax* 卷积核种数)]
附forward的代码: