第6篇 Fast AI深度学习课程——循环神经网络

本节将继续就前面课程中的应用实例介绍Fast.AI的具体实现,并深入介绍相关原理。主要内容包括:

  • 类型变量的内置矩阵含义分析。
  • 随机梯度算法的实现。
  • 循环神经网络(RNN)的原理与实现。
一. 使用PCA对类型变量的内置矩阵进行分析

前面课程讲述了如何将类型变量映射为连续型向量。那么这些连续型向量又都表征了数据的什么特征呢?这可通过可视化技术进行分析。但由于向量维度可能太高,而我们至多只能在三维空间中实现可视化,因此,首先需要利用主成分分析(PCAPrinciple Component Analysis)方法进行降维。

from sklearn.decomposition import PCA
pca = PCA(n_components=3)
movie_pca = pca.fit(movie_emb.T).components_

最终获得各部电影在第一维上的最高得分与最低得分如下图。可以看出,第一维可能表征了电影是严肃性题材,还是娱乐性题材。

图 1. 各部电影在第一维上的最高分与最低分
二. 以线性回归演示自定义的随机梯度算法

首先定义线性函数,均方误差函数,以及损失函数:

# 线性函数,其中a,b为参量
def lin(a,b,x): return a*x+b
# 均方误差函数
def mse(y_hat, y): return ((y_hat - y) ** 2).mean()
# 损失函数
def mse_loss(a, b, x, y): return mse(lin(a,b,x), y)

然后生成相应数据,定义参量,并都转换为PytorchVariable类型(这是针对Pytorch0.3.0版本)。

# 产生10000组线性数据,斜率为3,常数项为8
x, y = gen_fake_data(10000, 3., 8.)
x,y = V(x),V(y)
# 初始化参数,设置梯度求解为真
a = V(np.random.randn(1), requires_grad=True) 
b = V(np.random.randn(1), requires_grad=True)

然后写优化循环:

learning_rate = 1e-3
for t in range(10000):

    loss = mse_loss(a,b,x,y)
    # 这一句会计算requires_grad=True的参数的梯度
    loss.backward()
    # 更新参数值
    a.data -= learning_rate * a.grad.data
    b.data -= learning_rate * b.grad.data
    # 梯度置零,因为可能会有多个损失函数,若再调用其他损失函数的backward()时,需要做此操作。
    a.grad.data.zero_()
    b.grad.data.zero_()
三. 循环神经网络的原理与实现
1. 三层循环的神经网络示例

约定神经网络结构表示如下:

  • 所有的形状代表着激活层(即非线性函数)输出。
  • 所有箭头代表着线性加权操作。
  • 矩形为输入层,圆形为隐藏层,三角形为输出层。
图 2. 神经网络结构图示

考虑如下场景:在一段文本中,通过前三个字母,预测第四个。首先,三个输入字母是同质的,因此对其所做的基础操作(线性变换和非线性输出)应当是一样的;其次,三个字母有先后顺序,越靠后的字母与所需预测的字母关系越紧密,这代表着一种序列关系。因此一种合理的网络结构如下所示:

图 3. 预测第四个字母的网络结构图

其中颜色一致的箭头代表着所做操作的参数是一样的。如三个输入字母所连接的绿线,代表着对三个字母的基础操作是一致的。另外,还约定隐含层之间的系数也一致,这样,不同的字母在不同的层输入,代表着上文所描述的时序性。

基于上述结构,循环网络实现代码如下:

class Char3Model(nn.Module):
    def __init__(self, vocab_size, n_fac):
         super().__init__()
         # 字母表的内置矩阵 
         self.e = nn.Embedding(vocab_size, n_fac)
         # 输入层
         self.l_in = nn.Linear(n_fac, n_hidden)
         # 隐含层
         self.l_hidden = nn.Linear(n_hidden, n_hidden)
         # 输出层
         self.l_out = nn.Linear(n_hidden, vocab_size)              
    def forward(self, c1, c2, c3):
         # 三条绿色输入线所做的操作
         in1 = F.relu(self.l_in(self.e(c1)))
         in2 = F.relu(self.l_in(self.e(c2)))
         in3 = F.relu(self.l_in(self.e(c3)))

         # 为使下面三条语句保持一致的形式,先初始化零向量。
         h = V(torch.zeros(in1.size()).cuda())
         # 三个激活层三角形所做的操作:非线性输出
         h = F.tanh(self.l_hidden(h+in1))
         h = F.tanh(self.l_hidden(h+in2))
         h = F.tanh(self.l_hidden(h+in3))
         # 输出层操作
         return F.log_softmax(self.l_out(h))

假设构造了一个Char3Model实例m,接下来定义m的优化函数:

opt = optim.Adam(m.parameters(), 1e-2)

然后进行训练:

fit(m, md, 1, opt, F.nll_loss)

其中md是由ColumnarModelData生成的数据集。

2. 通用RNN的实现

观察Char3Model()forward()方法,其中三条绿线和三个三角所代表的操作形式一致,因此可以用循环来表示为更紧凑的形式。事实上,这样做之后,循环的次数就可以由参数设定,这样就演变出了一个更为通用的RNN的实现,相应的结构图示也可简化为下图:

图 4. RNN结构图示

相应代码如下:

class CharLoopModel(nn.Module):
    # This is an RNN!
    def __init__(self, vocab_size, n_fac):
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.l_in = nn.Linear(n_fac, n_hidden)
        self.l_hidden = nn.Linear(n_hidden, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)

    def forward(self, *cs):
        # cs stands for character set
        bs = cs[0].size(0)
        h = V(torch.zeros(bs, n_hidden).cuda())
        for c in cs:
            inp = F.relu(self.l_in(self.e(c)))
            h = F.tanh(self.l_hidden(h+inp))

        return F.log_softmax(self.l_out(h), dim=-1)

在隐含层处,我们将新输入的字符特征和已经处理过的若干字符的特征进行相加,这可能会导致信息丢失。因此一个可以改进的地方是将相加操作改为连接操作。

3. 使用Pytorch实现RNN

使用Pytorch中的nn.RNN()实现RNNPytorch会自动创建输出层,另外不需要自己写循环语句,Pytorch已经做了相应操作。

class CharRnn(nn.Module):
    def __init__(self, vocab_size, n_fac):
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.RNN(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)

    def forward(self, *cs):
        bs = cs[0].size(0)
        h = V(torch.zeros(1, bs, n_hidden))
        inp = self.e(torch.stack(cs))
        outp,h = self.rnn(inp, h)

        return F.log_softmax(self.l_out(outp[-1]), dim=-1)

其中PytorchRNN对象,不仅会返回输出outp,还会返回各个隐含层输入状态h。而且,在每个隐含层处都会有输出,因此outp是多维输出,而我们仅需最后一个,所以在进行log_softmax()时,指定outp的索引为-1

4. 更为紧凑的RNN结构

上述结构有个问题,比如我们使用三个字母预测第四个字母,若采用上述结构,则本次输入和上次输入之间重叠了两个字母,这就意味着这两个字母的相关操作是重复的。

图 5. 前后两次被重复计算的部分


如果我们记录各个隐含层的输入状态,那么这部分就可被复用。更进一步,我们没有必要一个字符一个字符地移动输入,而是以三个字符为步长进行移动。比如一个字符串:c1c2c3c4c5c6c7c8c9,按照之前的结构,计算过程是:输入c1c2c3,预测c4,然后输入c2c3c4,预测c5 ……而按照新结构,我们可以第一次输入c1c2c3,预测c2c3c4,第二次输入c4c5c6,预测
c5c6c7 ……即每输入一个字符,就预测一个字符。这样在输入c4预测c5时,由于已经记录c2c3的隐含层输入状态,这样其实也就是利用c2c3c4预测c5

最终所得的网络结构如下图所示,即将输出纳入到循环中。

图 6. 紧凑型RNN网络

备注

  • Fast.AI的学习器中获取Pytorch模型:learer.model。其中model是用@property标记的属性。
  • vim技巧
    • tag ColumnarModelData: 跳转到ColumnarModelData定义处。
    • ctrl + ]: 跳转到光标所在的变量的定义处。
    • ctr + t: 回到上次浏览位置。
  • RNN中,隐含层之间的连接系数初始化:这些系数构成了结构图中黄色线条所代表的操作中的线性变换,这一变换会被循环执行,如利用3个字母预测第4个时,则对第一个字母,这一操作会被执行3次。若这些系数组成的矩阵,有明显放大或缩小输入向量的作用的话,执行多次后会导致其所作用的向量要么过大要么过小。出于这个考虑,可将这些系数所组成的矩阵初始化为单位矩阵。

一些有用的链接

猜你喜欢

转载自blog.csdn.net/suredied/article/details/81749991
今日推荐