零基础入门深度学习(5) - 循环神经网络4

规划数据分析助手2019-04-30 07:09:55

点击上方蓝字

关注我们

规划数据分析助手!

专注规划数据分析与可视化方法

零基础入门深度学习(5) - 循环神经网络4


语言模型的训练

可以使用监督学习的方法对语言模型进行训练,首先,需要准备训练数据集。接下来,我们介绍怎样把语料

我 昨天 上学 迟到 了

转换成语言模型的训练数据集。


首先,我们获取输入-标签对:

然后,使用前面介绍过的向量化方法,对输入x和标签y进行向量化。这里面有意思的是,对标签y进行向量化,其结果也是一个one-hot向量。例如,我们对标签『我』进行向量化,得到的向量中,只有第2019个元素的值是1,其他位置的元素的值都是0。它的含义就是下一个词是『我』的概率是1,是其它词的概率都是0。


最后,我们使用交叉熵误差函数作为优化目标,对模型进行优化。


在实际工程中,我们可以使用大量的语料来对模型进行训练,获取训练数据和训练的方法都是相同的。


交叉熵误差

RNN的实现

为了加深我们对前面介绍的知识的理解,我们来动手实现一个RNN层。我们复用了上一篇文章零基础入门深度学习(4) - 卷积神经网络中的一些代码,所以先把它们导入进来。

import numpy as np

from cnn import ReluActivator, IdentityActivator, element_wise_op

我们用RecurrentLayer类来实现一个循环层。下面的代码是初始化一个循环层,可以在构造函数中设置卷积层的超参数。我们注意到,循环层有两个权重数组,U和W。

class RecurrentLayer(object):

   def __init__(self, input_width, state_width,

                activator, learning_rate):

       self.input_width = input_width

       self.state_width = state_width

       self.activator = activator

       self.learning_rate = learning_rate

       self.times = 0       # 当前时刻初始化为t0

       self.state_list = [] # 保存各个时刻的state

       self.state_list.append(np.zeros(

           (state_width, 1)))           # 初始化s0

       self.U = np.random.uniform(-1e-4, 1e-4,

           (state_width, input_width))  # 初始化U

       self.W = np.random.uniform(-1e-4, 1e-4,

           (state_width, state_width))  # 初始化W


在forward方法中,实现循环层的前向计算,这部分比较简单。


def forward(self, input_array):

       '''

       根据『式2』进行前向计算

       '''

       self.times += 1

       state = (np.dot(self.U, input_array) +

                np.dot(self.W, self.state_list[-1]))

       element_wise_op(state, self.activator.forward)

       self.state_list.append(state)


在backword方法中,实现BPTT算法。


def backward(self, sensitivity_array,

                activator):

       '''

       实现BPTT算法

       '''

       self.calc_delta(sensitivity_array, activator)

       self.calc_gradient()

   def calc_delta(self, sensitivity_array, activator):

       self.delta_list = []  # 用来保存各个时刻的误差项

       for i in range(self.times):

           self.delta_list.append(np.zeros(

               (self.state_width, 1)))

       self.delta_list.append(sensitivity_array)

       # 迭代计算每个时刻的误差项

       for k in range(self.times - 1, 0, -1):

           self.calc_delta_k(k, activator)

   def calc_delta_k(self, k, activator):

       '''

       根据k+1时刻的delta计算k时刻的delta

       '''

       state = self.state_list[k+1].copy()

       element_wise_op(self.state_list[k+1],

                   activator.backward)

       self.delta_list[k] = np.dot(

           np.dot(self.delta_list[k+1].T, self.W),

           np.diag(state[:,0])).T

   def calc_gradient(self):

       self.gradient_list = [] # 保存各个时刻的权重梯度

       for t in range(self.times + 1):

           self.gradient_list.append(np.zeros(

               (self.state_width, self.state_width)))

       for t in range(self.times, 0, -1):

           self.calc_gradient_t(t)

       # 实际的梯度是各个时刻梯度之和

       self.gradient = reduce(

           lambda a, b: a + b, self.gradient_list,

           self.gradient_list[0]) # [0]被初始化为0且没有被修改过

   def calc_gradient_t(self, t):

       '''

       计算每个时刻t权重的梯度

       '''

       gradient = np.dot(self.delta_list[t],

           self.state_list[t-1].T)

       self.gradient_list[t] = gradient


有意思的是,BPTT算法虽然数学推导的过程很麻烦,但是写成代码却并不复杂。


在update方法中,实现梯度下降算法。


def update(self):

       '''

       按照梯度下降,更新权重

       '''

       self.W -= self.learning_rate * self.gradient


上面的代码不包含权重U的更新。这部分实际上和全连接神经网络是一样的,留给感兴趣的读者自己来完成吧。


循环层是一个带状态的层,每次forword都会改变循环层的内部状态,这给梯度检查带来了麻烦。因此,我们需要一个reset_state方法,来重置循环层的内部状态。


def reset_state(self):

       self.times = 0       # 当前时刻初始化为t0

       self.state_list = [] # 保存各个时刻的state

       self.state_list.append(np.zeros(

           (self.state_width, 1)))      # 初始化s0


最后,是梯度检查的代码。


def gradient_check():

   '''

   梯度检查

   '''

   # 设计一个误差函数,取所有节点输出项之和

   error_function = lambda o: o.sum()

   rl = RecurrentLayer(3, 2, IdentityActivator(), 1e-3)

   # 计算forward值

   x, d = data_set()

   rl.forward(x[0])

   rl.forward(x[1])

   # 求取sensitivity map

   sensitivity_array = np.ones(rl.state_list[-1].shape,

                               dtype=np.float64)

   # 计算梯度

   rl.backward(sensitivity_array, IdentityActivator())

   # 检查梯度

   epsilon = 10e-4

   for i in range(rl.W.shape[0]):

       for j in range(rl.W.shape[1]):

           rl.W[i,j] += epsilon

           rl.reset_state()

           rl.forward(x[0])

           rl.forward(x[1])

           err1 = error_function(rl.state_list[-1])

           rl.W[i,j] -= 2*epsilon

           rl.reset_state()

           rl.forward(x[0])

           rl.forward(x[1])

           err2 = error_function(rl.state_list[-1])

           expect_grad = (err1 - err2) / (2 * epsilon)

           rl.W[i,j] += epsilon

           print 'weights(%d,%d): expected - actural %f - %f' % (

               i, j, expect_grad, rl.gradient[i,j])


需要注意,每次计算error之前,都要调用reset_state方法重置循环层的内部状态。下面是梯度检查的结果,没问题!



小节

至此,我们讲完了基本的循环神经网络、它的训练算法:BPTT,以及在语言模型上的应用。RNN比较烧脑,相信拿下前几篇文章的读者们搞定这篇文章也不在话下吧!然而,循环神经网络这个话题并没有完结。我们在前面说到过,基本的循环神经网络存在梯度爆炸和梯度消失问题,并不能真正的处理好长距离的依赖(虽然有一些技巧可以减轻这些问题)。事实上,真正得到广泛的应用的是循环神经网络的一个变体:长短时记忆网络。它内部有一些特殊的结构,可以很好的处理长距离的依赖,我们将在下一篇文章中详细的介绍它。现在,让我们稍事休息,准备挑战更为烧脑的长短时记忆网络吧。

规划数据分析助手

微信公众号

DSJGHZNT

专注规划数据分析与可视化方法

方法|案例|交流

记得扫描上方二维码关注我们哟 

点击

阅读原文

了解更多详情