循环神经网络(Recurrent neural network,RNN)是一类以序列(sequence)数据为输入,在序列的演进方向进行递归(recursion)且所有节点(循环单元)按链式连接的递归神经网络。
时间循环神经网络可以描述动态时间行为,RNN将状态在自身网络中循环传递,因此可以接受更广泛的时间序列结构输入。循环神经网络具有记忆性、参数共享并且图灵完备(Turing completeness),因此在对序列的非线性特征进行学习时具有一定优势 。手写识别是最早成功利用RNN的研究结果。
传统神经网络只能单独的取处理一个个的输入,前一个输入和后一个输入是完全没有关系的。但是,某些任务需要能够更好的处理序列的信息,即前面的输入和后面的输入是有关系的。
比如,在理解一句话意思时,孤立的理解这句话的每个词是不够的,而需要处理这些词连接起来的整个序列; 处理视频的时候,也不能只单独的去分析每一帧,而要分析这些帧连接起来的整个序列。
所以为了能够更好的处理序列的信息,RNN就诞生了。
基础的神经网络只在层与层之间建立了权连接,RNN最大的不同之处就是在层之间的神经元之间也建立的权连接。常规的RNN模型结构如下:
右侧为左侧模型按照时间序列展开之后的模型。
用公式表达为如下形式:
{ o ( t ) = g ( V ⋅ h ( t ) ) h ( t ) = f ( U ⋅ x ( t ) + W ⋅ h ( t − 1 ) ) \begin{cases} o^{(t)} = g(V·h^{(t)}) \\ h^{(t)} = f(U·x^{(t)}+W·h^{(t-1)}) \end{cases} {o(t)=g(V⋅h(t))h(t)=f(U⋅x(t)+W⋅h(t−1))
对于任意一个序列索引号 t t t,隐藏状态 h ( t ) h^{(t)} h(t)由 x ( t ) x^{(t)} x(t)和 h ( t − 1 ) h^{(t−1)} h(t−1)得到:
h ( t ) = σ ( z ( t ) ) = σ ( U x ( t ) + W h ( t − 1 ) + b ) h^{(t)}=\sigma(z^{(t)})=\sigma(Ux^{(t)}+Wh^{(t−1)}+b) h(t)=σ(z(t))=σ(Ux(t)+Wh(t−1)+b)
其中 σ \sigma σ为激活函数,一般为 tanh \tanh tanh, b b b为线性偏置。
序列索引号 t t t时模型的输出 o ( t ) o^{(t)} o(t)为:
o ( t ) = V h ( t ) + c o^{(t)}=Vh^{(t)}+c o(t)=Vh(t)+c
在序列索引号 t t t时预测输出为:
y ^ ( t ) = σ ( o ( t ) ) \widehat{y}^{(t)}=\sigma(o^{(t)}) y (t)=σ(o(t))
其中 σ \sigma σ为激活函数,通常RNN用于分类,因此一般用softmax函数 g ( z i ) = e z i ∑ k e z k g(z_i)=\frac{e^{z_i}}{\sum_k e^{z_k}} g(zi)=∑kezkezi。
BPTT算法是针对循环层的训练算法,它的基本原理和BP算法一样的:
在RNN模型中,需要寻优的参数有三个,分别是U、V、W。其中W和U需要追溯之前的历史数据,而V只需关注目前。由于我序列的每个位置都有损失函数,因此最终的损失L为:
L = ∑ t = 1 τ L ( t ) L=\sum_{t=1}^\tau L^{(t)} L=t=1∑τL(t)
先求对 V V V的梯度:
∂ L ∂ V = ∑ t = 1 τ ∂ L ( t ) ∂ V = ∑ t = 1 τ ( y ^ ( t ) − y ( t ) ) ( h ( t ) ) T \frac{\partial L}{\partial V} = \sum_{t=1}^\tau \frac{\partial L^{(t)}}{\partial V} = \sum_{t=1}^\tau (\widehat{y}^{(t)}-y^{(t)})(h^{(t)})^T ∂V∂L=t=1∑τ∂V∂L(t)=t=1∑τ(y (t)−y(t))(h(t))T
再求 L L L对 W , U W,U W,U的梯度:
∂ L ( t ) ∂ W = ∑ t = 0 τ [ ∂ L ( t ) ∂ o ( t ) ∂ o ( t ) ∂ h ( t ) ( ∏ j = t + 1 τ ∂ h ( j ) ∂ h ( j − 1 ) ) ∂ h ( t ) ∂ W ] \frac{\partial L^{(t)}}{\partial W} = \sum_{t=0}^\tau [\frac{\partial L^{(t)}}{\partial o^{(t)}} \frac{\partial o^{(t)}}{\partial h^{(t)}} (\prod_{j=t+1}^\tau \frac{\partial h^{(j)}}{\partial h^{(j-1)}}) \frac{\partial h^{(t)}}{\partial W}] ∂W∂L(t)=t=0∑τ[∂o(t)∂L(t)∂h(t)∂o(t)(j=t+1∏τ∂h(j−1)∂h(j))∂W∂h(t)]
∂ L ( t ) ∂ U = ∑ t = 0 τ [ ∂ L ( t ) ∂ o ( t ) ∂ o ( t ) ∂ h ( t ) ( ∏ j = t + 1 τ ∂ h ( j ) ∂ h ( j − 1 ) ) ∂ h ( t ) ∂ U ] \frac{\partial L^{(t)}}{\partial U} = \sum_{t=0}^\tau [\frac{\partial L^{(t)}}{\partial o^{(t)}} \frac{\partial o^{(t)}}{\partial h^{(t)}} (\prod_{j=t+1}^\tau \frac{\partial h^{(j)}}{\partial h^{(j-1)}}) \frac{\partial h^{(t)}}{\partial U}] ∂U∂L(t)=t=0∑τ[∂o(t)∂L(t)∂h(t)∂o(t)(j=t+1∏τ∂h(j−1)∂h(j))∂U∂h(t)]
查看中间的累乘部分,
∏ j = t + 1 τ ∂ h ( j ) ∂ h ( j − 1 ) = ∏ j = t + 1 τ σ ′ ⋅ W s \prod_{j=t+1}^\tau \frac{\partial h^{(j)}}{\partial h^{(j-1)}} = \prod_{j=t+1}^\tau \sigma' · W_s j=t+1∏τ∂h(j−1)∂h(j)=j=t+1∏τσ′⋅Ws
而 σ ( x ) \sigma(x) σ(x)为 s i g m o i d sigmoid sigmoid或者 t a n h tanh tanh时,容易导致梯度消失,所以此处的激活函数常用ReLU函数, R e L U ( x ) = max ( x , 0 ) ReLU(x) = \max(x,0) ReLU(x)=max(x,0)。
对于RNN来说,根据不同的问题需要不同的输入输出结构(输入 T x T_x Tx,输出 T y T_y Ty)。按照 T x T_x Tx与 T y T_y Ty的关系,RNN模型包含以下几个类型:
不仅在隐藏层有前后序列的关系,网络的其它层次也可以具有序列关系。
在自然语言处理中,上下文关系对模型有着较大的影响,而RNN比传统的神经网络模型能更好的处理这种关系。将每个单词作为一个特征向量,利用nn.RNN模型对语料进行了文本分类的处理。
首先从文件中读取相应训练集和测试集的语料,利用jieba对文本进行分词,然后利用nltk去停用词。这里nltk_data中并没有中文的停用词,我从网上找了一个中文的停用词文件放在对应的stopwords目录下,并命名为chinese。
def tokenizer(text):
sentence = jieba.lcut(text, cut_all=False)
stopwords = stopwords.words('chinese')
sentence = [_ for _ in sentence if _ not in stopwords]
return sentence
利用torchtext包处理预处理好的语料,将所提供的语料集转化为相应的词向量模型。由于每一个词均为一个向量,,作为RNN的输入。
train_set, validation_set = data.TabularDataset.splits(
path='corpus_data/',
skip_header=True,
train='corpus_train.csv',
validation='validation.csv',
format='csv',
fields=[('label', label), ('text', text)],
)
text.build_vocab(train_set, validation_set)
将处理好的词向量输入到RNN模型中进行处理。
self.rnn = nn.RNN(embedding_dim, self.hidden_size, batch_first=True, nonlinearity="relu")
self.linear = nn.Linear(self.hidden_size, label_num)
def forward(self, x):
# 输入x的维度为(batch_size, max_len),
x = self.embedding(x) # 经过embedding,x的维度为(batch_size, time_step, embedding_dim)
# 隐层初始化
# h0维度为(direction_num, batch_size, hidden_size)
h0 = torch.rand(1, x.size(0), self.hidden_size)
# out维度为(batch_size, seq_length, hidden_size*direction_num)
out, hn = self.rnn(x, h0)
# 只需要最后一步的输出状态
out = out[:, -1, :]
out = self.linear(out)
return out
利用训练集对模型进行训练,同时评估训练效果,并利用测试集对模型的准确性进行评估。为了防止偶然性产生的不确定,每一轮迭代会产生100个模型,分别评估其效率,进行调优后再用测试集测试其效率。
优点
不足
RNN 几个典型的应用如下: