RNN
Basic RNN
RNN是传统前馈神经网络的扩展,是一类通过使用带有自反馈功能的神经元,处理任意长度时序数据的神经网络,相比于前馈神经网络,RNN的输出不仅依赖于当前的输入,还与其过去一段时间的输出有关。RNN应用于不同的任务时具有不同的模式,对于短文本分类任务,是多个输入对应一个输出的模式,具体结构如下图所示。
由上图可知,在利用RNN进行分类时,按照短文本序列的顺序,将长度为t的样本序列依次输入到RNN中,并通过反向传播得到不同时刻的隐藏状态,将作为短文本序列最终的特征表示,代入分类函数实现分类。以时刻t为例,不同时刻隐藏状态的更新:。
U是上一个时刻t-1输出到下一个时刻t输入的权重,W是时刻t输入的权重,b为偏置。RNN这种自反馈串联结构特别适合于文本这类具有顺序结构的数据,能有效地获取上下文信息。
在PyTorch中我们是怎么实现RNN的呢?
我们一定要搞清楚out、hidden、inputs、hidden的各个维度。
对于输入的inputs和hidden:
- inputs:[seq_len, batch_size, input_size]
- hidden:[num_layers, batch_size, hidden_size]
对于输出的out和hidden:
- out:[seq_len, batch_size, hidden_size]
- hidden:[num_layers, batch_size, hidden_size]
当num_layers大于1的情况看下图:
LSTM
在实际应用中,RNN逆时间顺序逐步反向传播,当文本序列较长时,容易出现梯度消失或梯度爆炸问题,难以建立文本间的长期依赖关系。基于此,许多研究者进行了改进,其中最为有效的是引入了门控机制。论文Long short-term memory和Learning to forget: continual prediction with LSTM提出长短期记忆网络,其循环单元结构如下图所示。
LSTM的关键是cell状态,即贯穿上图顶部的水平线。cell状态的传输就像一条传送带,向量从整个cell中穿过,只是做了少量的线性操作,这种结构能很轻松地实现信息从整个cell中穿过而不作改变,这样就可以实现长时期的记忆保留。
LSTM也有能力向cell状态中添加或删除信息,这是由称为门的结构控制的。门可以选择性的让信息通过,它们由sigmoid神经网络层和逐点相乘实现。每个LSTM有三个这样的门结构来实现控制信息(分别是遗忘门、输入门、输出门)
LSTM的第一步是决定要从cell中丢弃什么信息: ,它的输入是和,输出是一个数值在0~1之间的向量(向量长度和一样),表示让的各部分信息通过的比重,0表示不让任何信息通过,1表示让所有信息通过。
下一步是决定要让多少新的信息加入到cell状态中。实现这个需要包括两个步骤:
(决定哪些信息需要更新);
(创建一个新的候选向量)
最后,把这两个部分联合起来对cell状态进行更新。(首先,我们把旧的状态和相乘,把一些不想保留的信息忘掉,然后加上。这部分信息就是我们要添加的新内容。)
最后,我们要决定输出什么值了:
Bi-LSTM
在PyTorch中:bi_lstm = nn.LSTM(input_size, hidden_size, num_layers, bidirectional=True)
输入数据格式:
input:[seq_len, batch_size, input_size]
:[num_layers * num_directions, batch_size, hidden_size]
:[num_layers * num_directions, batch_size, hidden_size]
输出数据格式:
output:[seq_len, batch_size, hidden_size * num_directions]
:[num_layers * num_directions, batch_size, hidden_size]
:[num_layers * num_directions, batch_size, hidden_size]
Bi-GRU
The inputs of GRU layer with shape:
input:[seq_len. batch_size, input_size]
hidden:[num_layers * num_directions, batch_size, hidden_size]
The outputs of GRU layer with shape:
output:[seq_len, batch_size, hidden_size * num_directions]
hidden:[num_layers * num_directions, batch_size, hidden_size]
掌握了以上的RNN基础知识后,我们再来看RNN在文本分类的应用以及结合CNN。
先看一下RNN在文本分类的应用吧,直接看下面的Model代码,我相信你一定看的懂得
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
self.embedding = nn.Embedding(config.vocab_size, embedding_dimension, padding_idx = config.vocab_size - 1)
self.lstm = nn.LSTM(config.embedding_dimension, config.hidden_size, config.num_layers, bidirectional=True, batch_first=True, dropout=config.dropout)
self.fc = nn.Linear(config.hidden_size * 2, config.num_classes)
def forward(self, x):
x, _ = x
out = self.embedding(x) # [batch_size, seq_len, embedding_dimension]
out, _ = self.lstm(out) # out: [batch_size, seq_len, hidden_size * 2]
out = self.fc(out[:, -1, :]) # 句子最后时刻的hidden_state
return out
TextRNN
CNN进行短文本分类时,要求其输入具有固定维数,且捕获文本特征时卷积核的窗口大小也是固定的,这无疑限制了文本序列信息表达,难以学到文本序列间的长距离依赖关系。尽管可以通过增加CNN模型的深度来获取文本更长距离的依赖关系,但很大程度上提高了计算成本(参数量会巨大),而RNN以可变长度的文本序列作为输入,能够利用具有自反馈功能神经元来获取序列间长期依赖关系,有效的捕获短文本上下文信息,较好的解决了基于CNN短文本方法存在的问题。
在Recurrent Neural Network for Text Classification with Multi-Task Learning论文中,论文提到已存在的网络都是针对单一任务进行训练,但是这种模型都存在问题,即缺少标注数据,当然这是任何机器学习任务都面临的问题。为了应对数据量少,常用的方法是使用一个无监督的训练模型,比如词向量,实验中也取得了不错的效果,但这样的方法都是间接改善网络效果。作者提出了一种新的方法,将多个任务联合起来训练,用来直接对网络进行改善,基于LSTM设计了三种不通融的信息共享机制进行训练,并在四个基准的文本分类任务中获得了较好的效果。
与论文自己的模型相比较:
与外面的模型相比较:
TextRCNN
我们应该还记得在CNN中提到DPCNN那样增加感受野的方式,RNN也可以缓解长距离依赖的问题。在论文Recurrent Convolutional Neural Networks for Text Classification中使用双向RNN结构捕获上下文信息,然后采用最大池化层,选择在文本分类中起关键作用的特征。
这篇文章的创新,就是将双向输出的Hidden State和词的embedding进行融合,然后进行非线性变换后,得到每个step的输出,然后使用了最大池化再接一个全连接层。
模型的前向过程是:
- 得到单词i的表示
- 通过RNN得到左右双向的表示和
- 将表示拼接得到,再经过变换得到
- 对多个进行max_pooling,得到句子表示y,再做最终的分类
这里的convolutional是指max-pooling。通过加入RNN,比纯CNN提升了1-2个百分点。
class Model(nn.Module):
def __init__(self, config):
super(Model, self).__init__()
if config.embedding_pretrained is not None:
self.embedding = nn.Embedding.from_pretrained(config.embedding_pretrained, freeze=False)
else:
self.embedding = nn.Embedding(config.n_vocab, config.embedding_dim, padding_idx=config.n_vocab - 1)
self.lstm = nn.LSTM(config.embedding_dim, config.hidden_size, config.num_layers, bidirectional=True, batch_first=True, dropout=config.dropout)
self.maxpool = nn.MaxPool1d(config.pad_size)
self.fc = nn.Linear(config.hidden_size * 2 + config.embedding_dim, config.num_classes)
def forward(self, x):
x, _ = x
embed = self.embedding(x) # [batch_size, seq_len, embedding_dim] = [64, 32, 64]
out, _ = self.lstm(embed) # out.shape: [batch_size, 32, 2 * 256]
out = torch.cat((embed, out), 2)
out = F.relu(out)
out = out.permute(0, 2, 1)
out = self.maxpool(out).squeeze()
out = self.fc(out)
return out
1.模型输入:[batch_size, seq_len]
2.经过embedding层:[batch_size, seq_len, embedding_dim]
3.双向LSTM:隐藏层大小为hidden_size,[batch_size, seq_len, hidden_size * 2]
4.将embedding层与LSTM输出拼接,并进行非线性激活:[batch_size, seq_len, hidden_size * 2 + embedding_size]
5.池化层:seq_len个特征中取最大的:[batch_size, hidden_size * 2 + embedding_size]
6.全连接后softmax
思考与总结
对于RNN做文本分类,相当于把每个词作为一个时间节点,把词向量作为每个单元的输入特征,一般会组合前向以及后向来构成双向特征,计算后每个单元有个状态特征以及输出特征,文本分类一般组合每一个单元的输出特征求个平均喂给全连接层来做分类。而在TextRCNN这篇文章中是用max-pooling操作的。但是求平均这个操作可以替换为更通用的注意力机制,复杂度更高点,效果更好。
复杂点的模型会分层来做,句子级别的rnn然后attention,最后文档级别在前一层的基础上再rnn + attention(详情见HAN这篇论文)。
个人感觉,在文本情感分类这种更细粒度的领域内,RNN是要好于CNN的,并且随着句子长度的增长,RNN的这一优势会进一步放大。当句子的情感分类或者说文本分类是由几个局部的key words决定的时候,CNN会更容易分类正确。
具体问题具体对待吧,在实际应用中要考虑多方面的因素。