万字长文:深入理解各类型神经网络(简单神经网络,CNN,LSTM)的输入和输出
简单神经网络当输入为标量对于一个最简单的神经网络而言,它的一个神经元通常长这个样子:假设我们有a1, a2, ... , an共n个输入,对于每个输入都给定一个权重w1, w2, ... , wn,再给定一个偏置b,将权重和输入相乘,加上偏置,就得到了一个神经元SUM,SUM的表达式为:SUM = w1 * x1 + w2 * x2 + ... + wn * xn + b,在这里由于各个项都是标量
目录
简单神经网络
当输入为标量
对于一个最简单的神经网络而言,它的一个神经元通常长这个样子:
假设我们有a1, a2, ... , an共n个输入,对于每个输入都给定一个权重w1, w2, ... , wn,再给定一个偏置b,将权重和输入相乘,加上偏置,就得到了一个神经元SUM,SUM的表达式为:SUM = w1 * x1 + w2 * x2 + ... + wn * xn + b,在这里由于各个项都是标量,得到的结果SUM也是一个标量。得到了SUM这个输出神经元后,我们通常会在后面加一个激活函数(上图中的f),增强其拟合数据的能力。通过相乘和相加的运算,并通过激活函数后,我们得到了最终的结果t(上图中最后的小圆圈)。
上图仅仅是一个神经元,在神经网络的不同层中,往往有不止一个神经元,所以在学习神经网络的过程中,我们往往会看到如下图所示的神经网络的示意图:
在这张图所表示的神经网络中,有一个输入层(input layer),三个隐层(hidden layer 1-3),最后是一个输出层(output layer),下面一一进行分析(在这步及其以后的分析中,我都会忽略偏置b,因为偏置对于形状的变化没有影响):
在input layer层,也就是输入层,我们先还是假设输入是简单的标量,即:a1, a2, ... , an,这时候我们看到input layer后的hidden layer 1层,就远远不止一个神经元了。第一步的分析中我们知道,每个输入都对应一组权重:w1, w2, ... , wn,有了这组权重之后,我们才能计算得到一个神经元的值,那么对于hidden layer 1中9个神经元的情况,我们自然也需要9组不同的权重,每组权重分别和输入计算得到一个神经元,每组权重都是独立的。分析完了hidden layer 1 之后,其余的两个隐层和最后的输出层也是同样的处理,比如要计算得到hidden layer 2,我们可以把hidden layer 1的每个神经元看成输入;要计算得到output layer,把hidden layer 3中的每个神经元看成输入。
补充一些小细节:
1.一般神经网络的示意图中,都是跟上图一样,只画一个圆圈表示神经元,但是不画激活函数,但其实激活函数已经包含在这个小圆圈里面了,便于理解,可以将神经元的这个小圆圈从中间切一刀,前半部分的状态是输入的各个量乘以权重,加上偏置得到的未经过激活函数之前的状态,然后把中间的这条竖线看成是激活函数,那么右半边的这个半圆就是代表通过激活函数之后的神经元了。
2.若神经网络要完成一个分类任务,输出层output layer通常使用softmax作为激活函数,想实现n分类,把输出层的神经元数量也设计为n即可,各个层的神经元的数量都是可以自己设计的。
当输入为向量
但是我们在用一些深度学习的框架完成一些任务的时候,往往会发现输入的形式往往不是一个数(标量),通常情况下都是一个多维的向量,这时候的情况要略微复杂一些,看下面这张神经网络的图:
在这张图中,只画了两层的网络:N-1层,N层,N-1层可以是输入层,也可以是任意的一个隐层,在前面中我们提到,隐层的神经元在计算得到下一层的网络时,可以看成输入。所以无论N-1层是输入层还是隐层,都可以看成输入层,图中N-1层有n个神经元(图中只标了8个,但是讨论中我们认为这一层的神经元数量是n,而第N层的神经元数量是k),那么我们可以认为对应n个标量的输入,观察这个式子,有了前面的讲解,这个式子应该非常容易理解:
这个式子的结果就是如图中箭头所示的神经元,下标的0是代表这个神经元在当前层中,它是第一个神经元,上标的(1)代表层的标号(N-1层中神经元的标号是0,那么N层就是1,以此类推),继续观察式子中的权重参数,w有两个下标,第一个下标都是0,代表它们都是第一组权重(也就是得到下一层第一个神经元的一组w),后面的下标是0-n,就是对应n个神经元的输入。那么对应的,要是我们想要计算得到第N层第二个神经元,那么这组w就应该为...,这样的w共有n组,n取决于第N层到底有多少个神经元。
那么此时思考两个问题,1.怎么表示这个n组w?2.怎么表示权重w和输入的相乘呢?答案是矩阵相乘的形式:
首先为了避免混淆,再次说明下图中符号的含义:N-1层有n个输入神经元,第N层有k个神经元,也就是说:要通过运算,做一个形状变化,由N个神经元的层,变为k个神经元的层。
图中可以看到,我们把输入变为列向量的形式,形状为[n,1];把k组权重w变成了矩阵的形式,形状为[k,n]。由矩阵乘法的定义可知,前面矩阵的列,后面矩阵(列向量也是矩阵)的行相同,可以进行乘法运算,可以得到一个[k,1]的矩阵,恰好代表了第N层的k个神经元。最后仔细思考一下矩阵乘法的运算过程,这跟我们前面讨论的标量的情况是一模一样的。
考虑batch_size
以上,我们理解了标量的输入形式和向量的输入形式。但往往我们用数据的时候,会有一个维度叫做batch_size。换而言之,我们的输入往往不止一个数据,在把数据喂给神经网络时,通常情况下我们都有一个batch_size,也就是同时把一批数据作为输入。我们还是在上图矩阵相乘的图中考虑此时跟之前一个样本时候的差异。
1.之前一个样本的时候,输入是列向量的形式,此时样本数量变多,那么这个时候在列这个维度上,自然不然是1了,此时的输入应该是一个矩阵,其形状是[n,batch_size],即行不变,每一列代表一个样本的所有特征输入。
2.权重矩阵w不变,形状仍然是[k,n],样本量的变化并不会影响权重矩阵的形状。此时进行矩阵乘法发现,[k,n]*[n,batch_size]得到的结果为[k,batch_size],k代表计算得到结果的这一层的神经元数量。在过程中我们可以发现,无论通过几次矩阵运算,batch_size这个维度始终不会变化,因为权重矩阵都是针对一个样本的,样本间是“互相独立”的,但是权重矩阵是公用的。用深度学习框架设计网络结构时,也不会考虑batch_size这一维。
总结以及易混淆的要点梳理
以下是之前我在学习deep learning的一些困惑,可能看一遍会有所帮助:
1.上面几张神经网络的示意图,也包括我们在网上看到的所有神经网络的结构,都仅仅是“针对一个样本”的,也就是说,我们设计网络结构的时候,不会去考虑样本的数量,也就是说,不会去考虑batch_size这个维度,这一点相信在使用深度学习框架设计网络结构的时候,都会深有体会。
2.为什么要一次向网络中输入多个样本呢?因为这样才能发挥GPU并行计算的优势,GPU能够加速神经网络的收敛,其实现原理的底层就在于它能够加速张量(向量)的运算。GPU跟CPU相比,运算能力远远没有CPU那么强,但是GPU有很多小的运算核心,对于一些简单的运算任务,它能挥发“人多力量大”的优势,而CPU虽然运算能力很强,但是没有那么多的核心。就好比做极为简单的加减乘除数学题,100个小学生和一个博士都能做,但是两者数量存在差异,使得小学生做相同的题反而比博士快得多。恰巧神经网络能够充分发挥并行计算的优势(神经网络本质就是矩阵向量间的运算),所以GPU在深度学习中至关重要。
3.既然GPU能够对多个样本进行并行计算,为什么不一次性将所有样本输入呢?当数据量过大的时候,输入矩阵的维度也跟着膨胀,一次喂给神经网络的样本过多,会超过GPU的负载能力。因此batch_size的设计要合理,不能过大也不能过小。
4.考虑下batch_size这个维度,理一下神经网络的优化过程:对于一个样本,输入层的n个神经元就是其特征的维度,比如我们要进行房价的预测任务,房价高低有多个影响因素,比如地理位置,平方数等。也就是有多个特征,这些特征都需要转化成可以量化的数据进行表示。一个房价样本通过隐层到输出层,输入通过层与层之间一系列的权重得到了最终的输出,跟这个样本的目标值(比如该预测任务中,目标值就是房价)计算损失(分类和回归有不同的损失函数),最后反向传播,更新权重参数w,这就是一个完整的过程。当然一个样本肯定不能把w更新的很完美,那么所有数据中的样本都通过神经网络一遍,自然神经网络就能找出输入数据共性的规律,然后将这些规律反映在权重参数w中。还是那房价预测的例子直观感受下:房价高低必然是受地理位置,平方数等因素影响的,其中每个因素影响有大有小,将房价样本通过神经网络,假设就学习到了:房价=平方数*0.8+地理位置*0.2,0.8和0.2就是在大量样本中学习到的,也就是模型的参数,有了这些参数后,后续就能根据特征进行最终房价的预测。
5.各种优化算法(梯度下降BGD,随机梯度下降SGD等)是怎么体现的呢?在4中,我们说的是一个样本,但是实际中一次有batch_size个样本,事实上在进行反向传播更新参数的时候,我们通常也不是每次就根据一个样本来更新参数的,而是有着更多样化的选择。比如梯度下降算法(BGD):每次迭代都需要把所有样本都送入,这样的好处是每次迭代都顾及了全部的样本,做的是全局最优化,但是有可能达到局部最优。随机梯度下降(SGD):从样本中随机抽出一个,训练后按梯度更新一次,然后再抽取一个,再更新一次(看到一个样本就更新一次,类似4中的方法),优点在于在样本量极其大的情况下,可能不用训练完所有的样本就可以获得一个损失值在可接受范围之内的模型。缺点在于单个样本的训练可能会带来很多噪声,使得SGD并不是每次迭代都向着整体最优化方向,因此在刚开始训练时可能收敛得很快,但是训练一段时间后就会变得很慢。当然还有小批量梯度下降等:选取一个小批次的样本计算损失,然后进行参数更新,是大多数情况下一个较为常用,也是较为优秀的优化算法的选择。
6.怎么理解将数据按照batch_size分成好几个批次,还要设计各种SGD/BGD等优化算法,这两者不是矛盾了吗?首先,梯度下降的各种算法是在batch_size的基础上的,也就是说。即使你使用的是BGD,顾及所有样本进行梯度更新,这个所有样本也只是当前的batch_size的大小,并不是所有数据。当然如果batch_size比较小,直接兼顾所有样本用BGD也可以,但实际上我们用其他的优化算法用的比较多,很少用BGD。所以batch_size和各种优化算法并不矛盾。
卷积神经网络
图片的在计算机中的表示
我们花了很大的篇幅理清了普通神经网络的输入和输出,接下来考虑卷积神经网络CNN,有了上面的基础,卷积神经网络的理解是水到渠成的。
CNN的卷积、池化等一些概念就不再这里讨论了,网上相关的资料十分详细,这里主要是从输入输出和形状变化的角度,从宏观上理解CNN。
CNN通常用于CV领域,那么我们也以图片为例,分析CNN中输入和输出的形式,首先我们要先明白图片在计算机中是怎样表示的:图片一般用三维向量来表示(假定通道数在最后一维),即(H,W,C),
意思为(高,宽,通道数),
黑白图片的通道数只有1,其中每个像素点的取值为[0,255],彩色图片的通道数为(R,G,B),每个通道的每个像素点的取值为[0,255],三个通道的颜色相互叠加,形成了各种颜色。关于图片的像素表示,可以想象一下有一个矩阵,它的高和宽分别为H和W,这个矩阵中每个点的取值范围是[0,255],一个点代表一个像素,对于黑白图片,就只有一个这样的矩阵;而对于彩色图片,有三个这样的矩阵,分别表示三个通道(R,G,B),三层像素点叠加后形成彩色。
图片特征维度的转变
我们先不考虑CNN,如果我们把图片放到普通神经的网络中,该怎样进行分类呢?从上文的讨论中我们可以知道,除去样本数量batch_size这个维度,迄今为止样本的特征都是一维的列向量,比如一个样本有n个特征,那么输入为[n,batch_size],表示特征的只有n这个维度,无论特征数量有多少,它始终只是一个维度上的数量的拓展。而一张图片,即使不考虑样本数量这个维度,它也需要三个维度,即(H,W,C),
事实上,全连接层也能实现图片的分类,只不过效果肯定不如CNN好,具体的做法是,我们需要将图片的三维特征“展平”,变成一维的特征,比如十分有名的Mnist数据集,每张图片的形状是[28,28,1],表示高和宽都为28,由于是黑白图片,通道数为1,我们将[28,28,1]展开,变成[28*28*1],这样暴力的将像素点铺平,就是其转变的方式。要是我们只用全连接层来实现Mnist数据集的分类,代码如下,会发现和普通神经网络是一模一样的,只是多了一步从三维变成一维的转变:
# 由于Mnist数据集本身比较简单,全连接层的效果也十分好,正确率在95%左右
class MnistNet(nn.Module):
def __init__(self):
super(MnistNet, self).__init__()
self.fc1 = nn.Linear(28 * 28 * 1, 28)
self.fc2 = nn.Linear(28, 10)
def forward(self, x):
x = x.view(-1, 28 * 28 * 1)
x = self.fc1(x)
x = F.relu(x)
x = self.fc2(x)
return F.log_softmax(x, dim=-1)
事实上,即使是CNN,在网络的最后几层,也一定是全连接层,也一定会进行“将多维的像素点映射到一维上”这个操作,比如在下面这个图中,在倒数第四层池化之后,最后三层都是全连接层,倒数第四层到倒数第三层的变化就是铺平像素点的操作。
CNN的拓扑结构图:图来自博客:https://www.zhihu.com/question/41949741
这下上图CNN的结果就更为清晰了,CNN的核心操作,是卷积和池化,而卷积和池化无非就是对输入的三维[H,W,C]进行变化,比如原始输入是[28,28,1],经过卷积后变成[28,28,6],池化后变成[14,14,6]。卷积和池化不改变原始的维度,保持图片原始三个维度的基础上,从图片中抽取特征,当然最后还是要变成一维,进行最后的工作,如图片分类。
循环神经网络
循环神经网络这一部分拿最常用的LSTM举例,其余的如传统RNN,GRU等都类似,只是内部机制不太相同,在输入输出的宏观层面是一样的。
过程中会拿pytorch的LSTM api结合起来分析,tensorflow等框架应该大体是类似的,理解了原理后应该也能很快入门。
本节内容有一部分参考知乎:https://zhuanlan.zhihu.com/p/79064602
使用LSTM的困惑
相比起传统神经网络和CNN,RNNs(LSTM、GRU)等网络的输入输出比较难理解,尤其是即使理解了LSTM的内部机制(输入门,遗忘门,输出门),到实际使用时还是会疑惑,比如pytorch中LSTM的官方API如下:
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
torch.nn.LSTM(input_size,hidden_size,num_layers,batch_first,dropout,bidirectional)
-
input_size
:输入数据的形状,即embedding_dim,句子中一个词用embedding_dim长度的向量表示 -
hidden_size
:隐藏层神经元的数量,即每一层有多少个LSTM单元 -
num_layer
:即RNN的中LSTM单元的层数 -
batch_first
:默认值为False,输入的数据需要[seq_len,batch,feature]
,如果为True,则为[batch,seq_len,feature]
-
dropout:
dropout的比例,默认值为0。dropout是一种训练过程中让部分参数随机失活的一种方式,能够提高训练速度,同时能够解决过拟合的问题。这里是在LSTM的最后一层,对每个输出进行dropout -
bidirectional
:是否使用双向LSTM,默认是False
实例化LSTM对象之后,不仅需要传入数据,还需要前一次的h_0(前一次的隐藏状态)和c_0(前一次memory)
即:lstm(input,(h_0,c_0))
LSTM的默认输入为input, (h_0, c_0)
-
input
:(seq_len, batch, input_size)
--->batch_first=False -
h_0
:(num_layers * num_directions, batch, hidden_size)
-
c_0
:(num_layers * num_directions, batch, hidden_size)
LSTM的默认输出为output, (h_n, c_n)
-
output
:(seq_len, batch, num_directions * hidden_size)
--->batch_first=False -
h_n
:(num_layers * num_directions, batch, hidden_size)
-
c_n
:(num_layers * num_directions, batch, hidden_size)
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
即使把输入输出的参数描述的很清楚,也经常会有以下困惑:
1.输入和输出为什么是这样的形状?
2.该怎么设计LSTM的输入?
3.该怎么用LSTM的输出,是用output、h_n,还是c_n,或者怎么拼接输出?
为了理清这些疑惑,先从文本的输入表示开始讲起。
文本的表示
对于一段文本,对它进行分词处理后,分成一个个词,在深度学习中,要想表示一个词,有两种形式:onehot encoding 和 word embedding,两者都是词的向量表示,不同的是前者是稀疏的向量,后者是稠密的向量。由于word embedding能够在一定程度上反映词和词之间的语义相似度,所以用途更为广泛。在这里定义一个分类任务,贯穿下面的分析过程:假设有一个IMDB情感分类数据集,每个样本都是一段评论文本,标签是文本的情感极性,共有10个分类,我们要做的是把这些文本通过LSTM进行情感分类,首先对该数据集进行如下的预处理:
1.在这个任务中,每段文本对应一条数据,数据的单位是一整段的文本,而不是单个句子或者句子中的每个词,因为分类的标签是建立在整段文本上的,这一点必须明确。
2.对每段文本进行分词处理,同时由于数据集中每段文本的长度不统一,需要指定一个固定长度max_len,使得所有的样本中分词得到的数量是相同的(比如每个样本的文本中都有50个词语),比max_len长的可以截去,比max_len短的可以进行补零操作。
3.对每个词进行word embedding处理,给定一个embedding dim,即词向量的维度,假定其值为300。假设在2中设定max_len的值是50,那么对于数据集的每个样本而言,它的形状被处理成[max_len, embedding_dim],即[50,300]。含义:每段文本有50个词,词用一个300维的向量表示。
LSTM的输入和输出
对数据进行了预处理之后,来观察LSTM的输入:
其中seq_len代表词的数量,这这里也就是50,batch是一次训练的样本数量,input_size就是词向量的维度,这里是300.
接下来分析其他的维度,在学习LSTM时,我们经常会看到这么一张图:
这张图很好的反映了LSTM单元的内部结构,但观察这张图,似乎很难和之前上文讲解的神经网络联系起来,这张图也一度让我懵圈了好久,实际上,从宏观上把握LSTM,它的结构长如下这样:
首先观察其中一个神经网络的“切片”,也就是下图所示的这个:
会发现这个单独的神经网络几乎和传统的神经网络一模一样,最下面绿色的是输入层,上面黄色的是隐层,最上面蓝色的是输出层,接下来就是重点:(为了方便叙述,我将样本中那一段文本简单的认为是一个句子,实际中一个样本可能不止一句话,同时讲解中会忽略细胞状态c,细胞状态跟隐藏状态h非常的像,而且一般常用的就是隐藏状态h和输出output)
在大图中的左侧可以看到,其中有一个时序:t,时序这个概念是LSTM的核心,在图中画出的4个神经网络的下方,也注明了T=1,2,3,4...这个时序实际上就是输入的seq_len,也就是表示句子中词的个数,LTSM的核心思想在于要“理解”前文的信息,句子中词的存在不是孤立的,那么该如何运用句子中上文的信息呢,答案是隐藏状态h_n是跟随着时间步来更新的,图中用红线连接4个神经网络来表示。也就是说,一开始输入的是h_0,随着时间步的推移,h_0这个参数会随着时间步更新。如果有n个时间步,那么最后的隐藏状态就是h_n,h_n蕴含了整句话的编码信息。
在LSTM中,有几点需要注意,关于隐层,图中只画了黄色的一层,但是实际上可能不止一层,层数由参数num_layers指定,num_layers中每层的神经元的数量由参数hidden_size指定,且因为这个参数只指定了一次,所以每个隐藏层,包括输出层的LSTM神经元都是相同的,都为hidden_size(这一点是我按照api的参数自己理解的,可能不太对,但是暂时可以这么理解)
我们继续来分析output和h_n的形状,先是output,其形状是:
output
:(seq_len, batch, num_directions * hidden_size)
从图中可以看出,每个时间步都有一个输出,有seq_len个时间步,第一维自然是seq_len,batch是样本的数量,第三个维度较难理解,从图中我们可以看到,无论隐层有多少个,一个时间步上,输出都只有一个,因此输出和LSTM纵向的深度无关,稍微补充一点,LSTM横向的深度指的是seq_len,纵向的深度是隐层数量num_layers,一般num_layers不会设置的很大,seq_len一般还是挺大的。所以经常说LSTM是一个“长但是不深”的网络,它的深度的概念和CNN有点不同,一个是横向的深度,一个是纵向的深度。言归正传,第三维的hidden_size很好理解,是该层神经元的数量,而num_directions的含义是指它是不是双向的网络,双向的话,num_directions=2,单向的则为1。单向的情况下,output第三维就是hidden_size,双向的话就是2*hidden_size。双向情况下有一个较难理解的点,就是:双向LSTM中,output:按照正反计算的结果顺序在第2个维度(维度从0开始计数,其实也就是我们讨论的第三维)进行拼接,正向第一个拼接反向的最后一个输出。也就是说,比如seq_len=0,也就是第一个时间步,在正向的LSTM中,它对应了正向的第一个output,但是在反向的LSTM中,句子中的第一个词变成了最后一个词,自然就对应了反向的最后一个输出。理解了这个之后,output的形状就显而易见了。
分析h_n的形状,其形状是:
h_n
:(num_layers * num_directions, batch, hidden_size)
明确一点,h_n是所有隐层(包括输出层!!!)的权重的集合,所以与output不同,它和纵向的深度有关,但是与横向的深度(时间步)无关,时间步越长,h_n随着时间步不断累积序列的信息,但是本身的形状不会变(有点绕,不理解的可以继续结合着图理解一下)。如果是双向的LSTM,相当于把句子倒过来,再喂给一个新的LSTM,h_n自然也和是否双向有关,所以第一维是num_layers * num_directions,二三维就很好理解了,分别是样本数量和每层LSTM的神经元。在双向LSTM的情况下,h_n的拼接和output不同,是按照得到的结果在第0个维度(维度从0开始计数,其实也就是我们讨论的第一维)进行拼接,正向第一个之后接着是反向第一个。这一点可以较为感性的去理解,输出output蕴含的是包括上文的编码信息,考虑第一个时间步的编码,正向的LSTM它能最先得到编码,而反向的LSTM中,这个词就是最后一个,得在最后一步才能得到全句的编码,而且output第一维是seq_len,正向第一个拼接反向的最后一个输出就合情合理了。而对于隐藏状态h_n,正向和反向的LSTM是同时进行编码的,一个从前往后,一个从后往前,而且它还没有seq_len的限制,不存在说需要拿到全局的编码才能拼接,这时候正向第一个之后接着是反向第一个就好了(感觉有点难讲清楚。。)
以上,输入输出总算是讲完了,细胞状态c_n就跟h_n基本是一样的,不过多赘述了。然后我们看下面这个画的十分好的图:
仔细看下,会发现有了知识储备后,这图十分好理解,蓝色方块是每个LSTM单元,横轴代表时间步,纵轴代表LSTM共有多少层,h0,c0经过时间步更新成h_n,c_n,每个时间步都对应一个output。
即:output就是最后一个layer上,序列中每个时刻(横向)状态h的集合(若为双向则按位置拼接,输出维度2*hidden_size),而hn实际上是每个layer最后一个状态(纵向)输出的拼接。(知乎:ymmy的回答)
输出的获取以及Pytorch实例
1.一个简单的使用实例(单向LSTM):
batch_size =10 # 数据量大小
seq_len = 20 # 每个句子有20个词
embedding_dim = 30 # 每个词用长度为30的向量表示
word_vocab = 100 # 词表的大小
hidden_size = 18 # 隐藏层神经元的数量
num_layer = 2 # LSTM单元的层数
#准备输入数据
input = torch.randint(low=0,high=100,size=(batch_size,seq_len))
#准备embedding
embedding = torch.nn.Embedding(word_vocab,embedding_dim)
lstm = torch.nn.LSTM(embedding_dim,hidden_size,num_layer)
#进行embed操作
embed = embedding(input) #input:[10, 20] embed:[10,20,30]
#转化数据为batch_first=False
embed = embed.permute(1,0,2) #[20,10,30]
#初始化状态, 如果不初始化,torch默认初始值为全0
h_0 = torch.rand(num_layer,batch_size,hidden_size)
c_0 = torch.rand(num_layer,batch_size,hidden_size)
output,(h_n,c_n) = lstm(embed,(h_0,c_0))
# 输出如下:
# ==========================================================================
In [122]: output.size()
Out[122]: torch.Size([20, 10, 18])
In [123]: h_n.size()
Out[123]: torch.Size([2, 10, 18])
In [124]: c_n.size()
Out[124]: torch.Size([2, 10, 18])
2.考虑一个问题,我们怎么在这么多的输出中获得我们想要的?如果我们是为了做文本分类任务,此时我们只要获得整个句子的编码即可,考虑最后一个时间步,它累积了上文所有的信息,所以我们取最后一个seq_len的output,再通过全连接层进行处理即可,对应到实际,只需要取output[-1,:,:],-1表示最后一个时间步。前面我们说过,h_n是所有隐层(包括输出层!!!)的权重的集合,最后一个隐层就是输出层,那么获取的方式也可以是:h_n[-1,:,:],即:最后一次的h_1应该和output的最后一个time step的输出是一样,output[-1,:,:]==h_n[-1,:,:]。
代码验证如下:
In [179]: a = output[-1,:,:]
In [180]: a.size()
Out[180]: torch.Size([10, 18])
In [183]: b = h_n[-1,:,:]
In [183]: b.size()
Out[183]: torch.Size([10, 18])
In [184]: a == b
Out[184]:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
dtype=torch.uint8)
3.双向的LSTM的输出的获取,跟1稍稍不同。首先还是举个例子:
batch_size =10 #句子的数量
seq_len = 20 #每个句子的长度
embedding_dim = 30 #每个词语使用多长的向量表示
word_vocab = 100 #词典中词语的总数
hidden_size = 18 #隐层中lstm的个数
num_layer = 2 #多少个隐藏层
input = torch.randint(low=0,high=100,size=(batch_size,seq_len))
embedding = torch.nn.Embedding(word_vocab,embedding_dim)
lstm = torch.nn.LSTM(embedding_dim,hidden_size,num_layer,bidirectional=True)
embed = embedding(input) #[10,20,30]
#转化数据为batch_first=False
embed = embed.permute(1,0,2) #[20,10,30]
h_0 = torch.rand(num_layer*2,batch_size,hidden_size)
c_0 = torch.rand(num_layer*2,batch_size,hidden_size)
output,(h_n,c_n) = lstm(embed,(h_0,c_0))
In [135]: output.size()
Out[135]: torch.Size([20, 10, 36])
In [136]: h_n.size()
Out[136]: torch.Size([4, 10, 18])
In [137]: c_n.size()
Out[137]: torch.Size([4, 10, 18])
前向输出的获取:前向的LSTM中,最后一个time step的输出的前hidden_size个,和最后一层向前传播h_n的输出相同:
#-1是前向LSTM的最后一个,前18是前hidden_size个
In [188]: a = output[-1,:,:18] #前项LSTM中最后一个time step的output
In [189]: b = h_1[-2,:,:] #拼接方式:[前向第一,后向第一,...,前向最后一个,后向最后一个].倒数第二个为前向最后一个
In [190]: a.size()
Out[190]: torch.Size([10, 18])
In [191]: b.size()
Out[191]: torch.Size([10, 18])
In [192]: a == b
Out[192]:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
dtype=torch.uint8)
后向的获取:后向LSTM中,最后一个time step的输出的后hidden_size个,和最后一层后向传播的h_1的输出相同:
#0 是反向LSTM的最后一个,后18是后hidden_size个
In [196]: c = output[0,:,18:] #后向LSTM中的最后一个输出
In [197]: d = h_1[-1,:,:] #后向LSTM中的最后一个隐藏层状态
In [198]: c == d
Out[198]:
tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]],
dtype=torch.uint8)
4.使用注意点:
-
第一次调用之前LSTM api,需要初始化隐藏状态(一般规定形状,用rand函数即可),如果不初始化,默认创建全为0的隐藏状态
-
往往会使用LSTM or GRU 的输出的最后一维的结果,来代表LSTM、GRU对文本处理的结果,其形状为
[batch, num_directions*hidden_size]
。-
并不是所有模型都会使用最后一维的结果
-
如果实例化LSTM的过程中,batch_first=False,则
output[-1] or output[-1,:,:]
可以获取最后一维 -
如果实例化LSTM的过程中,batch_first=True,则
output[:,-1,:]
可以获取最后一维
-
-
如果结果是
(seq_len, batch_size, num_directions * hidden_size)
,需要把它转化为(batch_size,seq_len, num_directions * hidden_size)
的形状,不能使用view等变形的方法,需要使用output.permute(1,0,2)
,即交换0和1轴,实现上述效果 -
使用双向LSTM的时候,往往会分别使用每个方向最后一次的output,进行拼接,作为当前数据经过双向LSTM的结果(!!!!!!)
-
即:
torch.cat([h_1[-2,:,:],h_1[-1,:,:]],dim=-1)
-
最后的表示的size是
[batch_size,hidden_size*2]
-
-
上述内容在GRU中同理,GRU的API和LSTM十分接近,有兴趣可以了解一下。
更多推荐
所有评论(0)