DNN 以及 CNN 的模型和前向反向传播算法,这些算法都是前向反馈的,模型的输出和模型本身没有关联关系。

循环神经网络(Recurrent Neural Network)指一个随着时间的推移,重复发生的结构。它能够实现某种“记忆功能”,是进行时间序列分析时最好的选择。

RNN 模型如下:

       

循环神经网络进行分类 循环神经网络的模型_DNN

这个网络在 $t$ 时刻接收到输入 $x_t$ 之后,隐藏层的值是 $s_t$,输出值是 $o_t$。关键一点是 $s_t$ 的值不仅仅取决于 $x_t$,还取决于 $s_{t-1}$。

$U,W,V$ 这三个矩阵是我们的模型的线性关系参数,它在整个 RNN 网络中是共享的,这点和 DNN 很不相同。 也正因为是共享了,它体现

了 RNN 的模型的“循环反馈”的思想。

RNN 神经网络是定义在全连接神经网络上的,上图右侧每个圆代表 RNN Cell,里面的结构就是全连接神经网络,虽然画出了三个圆,但是其

实都是同一个网络,即同一个 Cell,这体现了参数共享。假设输入层有 $i$ 个神经元,隐藏层有 $h$ 个神经元,则 RNN Cell 的内部进行的运算为:

$$h^{(t)} = \sigma(z^{(t)}) = \sigma(U_{h \times i} \; x^{(t)} + W_{h \times h} \; h^{(t-1)} + b)$$

$h^{(t)}$ 就是图中的 $s^{(t)}$,$\sigma$ 为 RNN 的激活函数,一般为 tanh, b 为线性关系的偏置。

隐藏层到输出层的表达式比较简单:

$$o^{(t)} = Vh^{(t)} + c$$

$$\hat{y}^{(t)} = \sigma(o^{(t)})$$

通常由于 RNN 是识别类的分类模型,所以输出层的激活函数一般是 softmax。

将输入层到隐藏层之间的线性运算做个变形:

$$z^{(t)} = U_{h \times i} \; x^{(t)} + W_{h \times h} \; h^{(t-1)} + b \\
= \begin{bmatrix}
U_{h \times i} & W_{h \times h} 
\end{bmatrix}\begin{bmatrix}
x^{(t)} \\ 
h^{(t-1)}
\end{bmatrix} + b$$

可以发现,RNN Cell 用一个线性层就可以实现了,它的输入神经元有 $h + i$ 个,输出神经元有 $h$ 个。如下图所示:

     

循环神经网络进行分类 循环神经网络的模型_反向传播_02

上图是 RNN 最简单的一个 Cell,它只有一个隐藏层,其中输入由 $x_t$ 和 $h_{t-1}$ 组成,网络结构上仍属于全连接神经网络。

那如何定义多隐藏层的循环神经网络呢?

        

循环神经网络进行分类 循环神经网络的模型_反向传播_03

上面两个图的最后一个隐藏层都需要再接一个输出层,这里没有画出来。下面推导一下 RNN 的反向传播算法。

BTPP 算法将第 $l$ 层 $t$ 时刻的误差项 $\delta_t^l$ 值沿两个方向传播,一个方向是其传递到上一层网络,得到 $\delta_t^{l-1}$,这部分只和权重矩阵 $U$ 有关;

另一个是方向是将其沿时间线 $t-1, t-2,...,1$ 传递到初始时刻,得到 $\delta_{t-1}^l\;,\delta_{t-2}^l \;,...,\delta_{1}^l$,这部分只和权重矩阵 $W$ 有关。

对于 RNN,由于我们在序列的每个时刻都有损失函数,因此最终的损失 $L$ 为:

$$L = \sum\limits_{t=1}^{T}L_t$$

这个反向传播过程其实很简单,重点是观察反向传播路径,发现有两条,以下图为例:

      

循环神经网络进行分类 循环神经网络的模型_Machine-Learning_04

我们要求误差函数 $L$ 对 $h_1^2$ 的偏导数,可以发现反向传播由两条路径,一条来自 $L_1$,另一条来自 $L_2$,所以将连个路径的偏导数分别求出来,

然后相加就可以了,每个路径的偏导数求法就和 DNN 完全一样。设

$$\frac{\partial L}{\partial h_2^3} = \delta_2^3 \\
\frac{\partial L}{\partial h_1^3} = \delta_1^3$$

可得

$$\delta_1^2 = W^{T}diag(\sigma^{'}(z_2^3))\delta_2^3 + U^{T}diag(\sigma^{'}(z_1^3))\delta_1^3$$

如何对矩阵 $U$ 求导呢?可以发现有很多路径可以到达 $U$,只需要一条一条的求,然后求和就可以了。最后求导结果为

$$\frac{\partial L}{\partial U} = \delta_1^3 \left (h_1^2 \right )^{T} + \delta_2^3 \left (h_2^2 \right )^{T} + \cdots + \delta_N^3 \left (h_N^2 \right )^{T} \\
= \sum_{t=1}^{N}\delta_t^3 \left (h_t^2 \right )^{T}$$

下面举个例子,训练如下模型:

       

循环神经网络进行分类 循环神经网络的模型_神经网络_05

import torch

input_size = 4
hidden_size = 4
batch_size = 1

idx2char = ['e', 'h', 'l', 'o']
one_hot_lookup = [[1, 0, 0, 0],
                  [0, 1, 0, 0],
                  [0, 0, 1, 0],
                  [0, 0, 0, 1]]
x_data = [1, 2, 2, 2, 3]
y_data = [3, 1, 2, 3, 2]

x_one_hot = [one_hot_lookup[x] for x in x_data] # 查询字典生成独热向量
inputs = torch.Tensor(x_one_hot).view(-1, batch_size, input_size)
labels = torch.LongTensor(y_data).view(-1, 1)

class Model(torch.nn.Module):
    def __init__(self, input_size, hidden_size, batch_size):
        super(Model,self).__init__()
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.rnncell = torch.nn.RNNCell(input_size=self.input_size, hidden_size=self.hidden_size)

    def forward(self, input, hidden):
        hidden=self.rnncell(input, hidden)
        return hidden

    def init_hidden(self):
        return torch.zeros(self.batch_size, self.hidden_size)

net = Model(input_size, hidden_size, batch_size)
criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(net.parameters(),lr=0.1)

for epoch in range(15):
    loss = 0
    optimizer.zero_grad()    # 每一轮训练先把优化器的梯度清零
    hidden = net.init_hidden()
    print('Predicted string:',end='')

    for input, label in zip(inputs, labels):
        hidde n= net(input,hidden)
        loss+ = criterion(hidden,label)
        _, idx = hidden.max(dim=1)
        print(idx2char[idx.item()], end='')
        loss.backward()
        # loss.backward(retain_graph=True)
        optimizer.step()
        print(',Epoch [%d/15] loss=%.4f' % (epoch + 1, loss.item())