Python 深度学习(二)(2)

简介: Python 深度学习(二)

Python 深度学习(二)(1)https://developer.aliyun.com/article/1511967

丢弃

另一个重要的技术是可以在池化层之后应用的,但也通常可以应用于全连接层的技术是随机定期“丢弃”一些神经元及其相应的输入和输出连接。在一个丢弃层中,我们为神经元指定了一个概率p以随机方式“丢弃”。在每个训练周期中,每个神经元都有概率p被从网络中丢弃,概率*(1-p)被保留。这是为了确保没有神经元过多地依赖其他神经元,并且每个神经元都“学到”了对网络有用的东西。这有两个优点:它加快了训练,因为我们每次训练一个较小的网络,还有助于防止过拟合(参见 N. Srivastava, G. Hinton, A. Krizhevsky, I. Sutskever, and R. Salakhutdinov 的Dropout: A Simple Way to Prevent Neural Networks from Overfitting*,刊登于机器学习研究杂志15 (2014), 1929-1958, www.jmlr.org/papers/volume15/srivastava14a.old/source/srivastava14a.pdf)。

然而,重要的是要注意,丢弃层不仅仅限于卷积层;事实上,丢弃层在不同的神经网络架构中都有应用。丢弃层应被视为减少过拟合的正则化技术,我们提到它们是因为它们将在我们的代码示例中被明确使用。

深度学习中的卷积层

当我们介绍深度学习的概念时,我们讨论了“深度”一词不仅指的是我们在神经网络中使用了许多层,还指的是我们有一个“更深入”的学习过程。这种更深入的学习过程的一部分是神经网络自主学习特征的能力。在前一节中,我们定义了特定的滤波器来帮助网络学习特定的特征。这并不一定是我们想要的。正如我们讨论过的,深度学习的重点在于系统能够自主学习,如果我们不得不教会网络哪些特征或特性是重要的,或者如何通过应用边缘层来学习识别数字的形状,我们将会做大部分的工作,并可能限制网络学习可能对我们有用但对网络本身并不重要的特征,从而降低其性能。深度学习的重点在于系统必须自行学习。

在第二章 神经网络中,我们展示了神经网络中的隐藏层如何通过使用反向传播学习权重; 操作员没有设置权重。 同样,操作员设置滤波器中的权重是毫无意义的,我们希望神经网络通过使用反向传播再次学习滤波器中的权重。 操作员唯一需要做的是设置图层的大小、步长和填充,并决定我们要求网络学习多少个特征图。 通过使用监督学习和反向传播,神经网络将自主设置每个滤波器的权重(和偏差)。

还需要提及的是,虽然使用我们提供的卷积层描述可能更简单,但卷积层仍然可以被认为是我们在第三章 深度学习基础中介绍的普通全连接层。 实际上,卷积层的两个主要特征是每个神经元只连接到输入层的一个小区域,并且对应于相同小区域的不同切片共享相同的权重。 这两个属性可以通过创建一个稀疏的权重矩阵来呈现在普通层中,即具有许多零(由于卷积网络的局部连接性)和许多重复权重(由于切片之间的参数共享特性)。 理解这一点清楚地说明了为什么卷积层的参数要比全连接层少得多; 在卷积层中,权重矩阵主要由零条目组成。 然而,在实践中,将卷积层想象成本章节中描述的方式对直觉有所帮助,因为这样可以更好地欣赏卷积层如何突出显示原始图像的特征,正如我们通过模糊图像或突出我们示例中数字的轮廓来图形化展示的那样。

再要明确的一点是,卷积网络的深度通常应该等于可以通过 2 进行迭代除法的数字,例如 32,64,96,128 等。 这在使用池化层时很重要,比如 max-pool 层,因为池化层(如果其大小为(2,2))将使输入层的大小除以 2,类似于我们如何定义“步进”和“填充”,以使输出图像具有整数尺寸。 另外,可以添加填充以确保输出图像大小与输入相同。

Theano 中的卷积层

现在我们已经知道卷积层是如何工作的,我们将使用 Theano 实现一个卷积层的简单示例。

让我们首先导入所需的模块:

import numpy  
import theano  
import matplotlib.pyplot as plt 
import theano.tensor as T
from theano.tensor.nnet import conv
import skimage.data
import matplotlib.cm as cm

Theano 首先创建我们定义的操作的符号表示。我们稍后将通过另一个使用 Keras 的例子,它提供了一个很好的接口来更轻松地创建神经网络,但是使用 Theano(或者 TensorFlow)直接使用时可能缺少一些灵活性。

我们通过定义所需的变量和神经网络操作来定义特征图的数量(卷积层的深度)和滤波器的大小,然后我们使用 Theano 张量类来符号化地定义输入。Theano 把图像通道视为一个单独的维度,所以我们把输入定义为 tensor4。接下来,我们使用-0.2 和 0.2 之间的随机分布来初始化权重。我们现在可以调用 Theano 卷积操作,然后在输出上应用逻辑 sigmoid 函数。最后,我们定义函数f,它接受一个输入,并使用所使用的操作来定义一个输出:

depth = 4
filter_shape = (3, 3) 
input = T.tensor4(name='input')  
w_shape = (depth, 3, filter_shape[0], filter_shape[1]) 
dist = numpy.random.uniform(-0.2, 0.2, size=w_shape)
W = theano.shared(numpy.asarray(dist, dtype=input.dtype), name = 'W')
conv_output = conv.conv2d(input, W)   
output = T.nnet.sigmoid(conv_output)
f = theano.function([input], output)

我们导入的skimage模块可以用来加载一个名为lena的图像,然后在将图像重塑为可传递给我们定义的 Theano 函数后,我们就可以在该图像上调用 Theano 函数:

astronaut = skimage.data.astronaut()
img = numpy.asarray(astronaut, dtype='float32') / 255
filtered_img = f(img.transpose(2, 0, 1).reshape(1, 3, 512, 512))

就是这样。我们现在可以通过这段简单的代码打印出原始图片和经过滤波的图片。

plt.axis('off') 
plt.imshow(img) 
plt.show()  
for img in range(depth):
    fig = plt.figure()   
    plt.axis( 'off')   
    plt.imshow(filtered_img[0, img, :, :, ], cmap = cm.gray)
    plt.show()
    filename = "astro" + str(img)
    fig.savefig(filename, bbox_inches='tight')

如果读者对可视化所使用的权重感兴趣,在 Theano 中,可以使用print W.get_value()来打印值。

这段代码的输出如下:(由于我们还没有固定随机种子,并且权重是随机初始化的,读者可能会得到略有不同的图像):


原始图片和滤波后的图片。

一个使用 Keras 识别数字的卷积层示例

在第三章中,我们介绍了使用 Keras 对数字进行分类的简单神经网络,我们得到了 94%的准确率。在本章中,我们将努力使用卷积网络将该准确率提高到 99%以上。由于初始化的变化,实际值可能会略有不同。

首先,我们可以通过使用 400 个隐藏神经元来改进我们之前定义的神经网络,并将其运行 30 个周期;这样就应该已经将准确率提高到了大约 96.5%:

hidden_neurons = 400
    epochs = 30

接下来,我们可以尝试对输入进行缩放。图像由像素组成,每个像素的整数值在 0 到 255 之间。我们可以使该值成为浮点数,并将其在 0 到 1 之间缩放,只需在定义输入后添加这四行代码即可:

X_train = X_train.astype('float32')     
X_test = X_test.astype('float32')     
X_train /= 255     
X_test /= 255

如果我们现在运行我们的网络,我们得到的准确率较低,略高于 92%,但我们不需要担心。通过重新缩放,我们实际上改变了我们函数的梯度值,因此它将收敛得更慢,但有一个简单的解决方法。在我们的代码中,在model.compile函数内,我们定义了一个优化器等于"sgd"。这是标准的随机梯度下降,它使用梯度收敛到最小值。然而,Keras 允许其他选择,特别是"adadelta",它自动使用动量,并根据梯度调整学习率,使其与梯度成反比地变大或变小,以便网络不会学习得太慢,也不会通过采取太大的步骤跳过最小值。通过使用 adadelta,我们动态调整参数随时间改变(也见:Matthew D. Zeiler,Adadelta:一种自适应学习率方法,arXiv:1212.5701v1 (arxiv.org/pdf/1212.5701v1.pdf))。

在主函数内部,我们现在将改变我们的编译函数并使用:

model.compile(loss='categorical_crossentropy', 
              metrics=['accuracy'], optimizer='adadelta')

如果我们再次运行我们的算法,现在我们的准确率约为 98.25%。最后,让我们修改我们的第一个密集(全连接)层,使用relu激活函数而不是sigmoid

model.add(Activation('relu'))

这将带来大约 98.4%的准确率。问题在于,现在使用传统的前馈架构变得越来越难以改善我们的结果,由于过拟合,增加迭代次数或修改隐藏神经元的数量将带来任何额外的好处,因为网络将简单地学会对数据进行过度拟合,而不是学会更好地泛化。因此,我们现在将在示例中引入卷积网络。

为了做到这一点,我们保持我们的输入值在 0 和 1 之间。然而,为了被卷积层使用,我们将数据重塑成大小为(28,28,1)的体积=(图像宽度,图像高度,通道数),并将隐藏神经元的数量减少到 200 个,但现在我们在开始处添加了一个简单的卷积层,使用 3 x 3 的滤波器,不填充,步长为 1,然后是一个步幅为 2 且大小为 2 的最大池化层。为了将输出传递给密集层,我们需要将体积(卷积层是体积)拉直以传递给具有 100 个隐藏神经元的常规密集层,使用以下代码:

from keras.layers import Convolution2D, MaxPooling2D, Flatten
hidden_neurons = 200
X_train = X_train.reshape(60000, 28, 28, 1)     
X_test = X_test.reshape(10000, 28, 28, 1)
model.add(Convolution2D(32, (3, 3), input_shape=(28, 28, 1)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())

我们还可以将迭代次数减少到 8 次,然后我们将得到大约 98.55%的准确率。通常情况下,常用成对的卷积层,所以我们添加了一个类似第一个卷积层的第二个卷积层(在池化层之前):

model.add(Convolution2D(32, (3, 3))) 
model.add(Activation('relu'))

现在我们的准确率已经达到了 98.9%。

为了达到 99%,我们按照我们所讨论的方法添加一个辍学层。这不会增加任何新的参数,但能帮助防止过拟合,并且我们将其添加在拉直层之前:

from keras.layers import Dropout
model.add(Dropout(0.25))

在这个例子中,我们使用了约 25%的辍学率,因此每个神经元每四次就会被随机抛弃一次。

这将使我们的准确度达到 99%以上。如果我们想进一步提高(准确度可能因初始化的差异而有所不同),我们还可以添加更多的 dropout 层,例如在隐藏层之后,并增加时期的数量。这将迫使最终密集层中容易过拟的神经元被随机丢弃。我们的最终代码如下:

import numpy as np      
np.random.seed(0)  #for reproducibility
from keras.datasets import mnist 
from keras.models import Sequential  
from keras.layers import Dense, Activation, Convolution2D, MaxPooling2D, Flatten, Dropout  
from keras.utils import np_utils
input_size = 784
batch_size = 100     
hidden_neurons = 200     
classes = 10     
epochs = 8          
(X_train, Y_train), (X_test, Y_test) = mnist.load_data()          
X_train = X_train.reshape(60000, 28, 28, 1)     
X_test = X_test.reshape(10000, 28, 28, 1)          
X_train = X_train.astype('float32')     
X_test = X_test.astype('float32')     
X_train /= 255     
X_test /= 255               
Y_train = np_utils.to_categorical(Y_train, classes)     
Y_test = np_utils.to_categorical(Y_test, classes)              
model = Sequential()
model.add(Convolution2D(32, (3, 3), input_shape=(28, 28, 1)))             
model.add(Activation('relu'))                     
model.add(Convolution2D(32, (3, 3)))
model.add(Activation('relu'))                  
model.add(MaxPooling2D(pool_size=(2, 2)))             
model.add(Dropout(0.25))                  
model.add(Flatten())         
model.add(Dense(hidden_neurons))
model.add(Activation('relu'))       
model.add(Dense(classes))       
model.add(Activation('softmax'))
model.compile(loss='categorical_crossentropy',             
              metrics=['accuracy'], optimizer='adadelta')    
model.fit(X_train, Y_train, batch_size=batch_size,        
        epochs=epochs, validation_split = 0.1, verbose=1) 
score = model.evaluate(X_train, Y_train, verbose=1)
print('Train accuracy:', score[1])                           
score = model.evaluate(X_test, Y_test, verbose=1) 
print('Test accuracy:', score[1])

这个网络可以进一步优化,但这里的重点不是获得一个获奖得分,而是理解过程,并了解我们采取的每一步是如何提高性能的。还要注意,通过使用卷积层,我们实际上也避免了网络的过拟合问题,因为利用了更少的参数。

使用 Keras 进行 cifar10 的卷积层示例

现在我们可以尝试在cifar10数据集上使用相同的网络。在第三章中的深度学习基础知识中,我们在测试数据上得到了 50%的低准确度,为了测试刚刚在mnist数据集上使用的新网络,我们只需要对代码进行一些小的修改:我们需要加载cifar10数据集(不进行任何重新调整,那些行将被删除):

(X_train, Y_train), (X_test, Y_test) = cifar10.load_data()

改变第一个卷积层的输入值:

model.add(Convolution2D(32, (3, 3), input_shape=(32, 32, 3)))

运行这个网络 5 个时期将给我们约 60%的准确度(从约 50%提高)和 10 个时期后的 66%的准确度,但接着网络开始过拟合并停止改善性能。

当然,cifar10的图像有 32 x 32 x 3 = 3072 个像素,而不是 28 x 28 = 784 个像素,所以在前两层之后,我们可能需要添加几个额外的卷积层:

model.add(Convolution2D(64, (3, 3))) 
model.add(Activation('relu'))     
model.add(Convolution2D(64, (3, 3)))     
model.add(Activation('relu'))     
model.add(MaxPooling2D(pool_size=(2, 2)))     
model.add(Dropout(0.25))

一般来说,最好将大型卷积层划分为较小尺寸的卷积层。例如,如果我们有两个连续的(3 x 3)卷积层,第一层将具有对输入图像的(3 x 3)视图,第二层将为每个像素提供对输入图像的(5 x 5)视图。然而,每层都会具有非线性特征,这些特征将堆叠起来,创建出比仅仅创建单个(5 x 5)滤波器时更复杂和有趣的输入特征。

如果我们将这个网络运行 3 个时期,我们的准确度也在 60%左右,但是经过 20 个时期后,通过使用简单的网络,我们的准确度达到了 75%。先进的卷积网络可以达到 90%的准确度,但需要更长的训练时间,并且更加复杂。我们将以图形方式展示一个重要的卷积神经网络的架构,称为 VGG-16,在下一段中,用户可以尝试使用 Keras 或其他他们熟悉的语言来实现它,例如 Theano 或 TensorFlow(该网络最初是使用 Caffe 创建的,Caffe 是在伯克利开发的一个重要的深度学习框架,详情请见:caffe.berkeleyvision.org)。

在使用神经网络时,能够“看到”网络学习的权重是很重要的。这使用户能够了解网络正在学习什么特征,并且能够进行更好的调整。这个简单的代码将输出每个层的所有权重:

index = 0
numpy.set_printoptions(threshold='nan')     
for layer in model.layers:       
    filename = "conv_layer_" + str(index)       
    f1 = open(filename, 'w+')       
    f1.write(repr(layer.get_weights()))       
    f1.close()       
    print (filename + " has been opened and closed")     
    index = index+1

例如,如果我们对第 0 层,即第一个卷积层的权重感兴趣,我们可以将它们应用于图像,以查看网络正在突出显示的特征。如果我们将这些滤波器应用于图像lena,我们会得到:


我们可以看到每个滤波器如何突出显示不同的特征。

预训练

正如我们所见,神经网络,特别是卷积网络,通过调整网络的权重,就像它们是一个大型方程的系数一样来获得给定特定输入的正确输出。调整通过反向传播来移动权重,以使它们朝着给定选择的神经网络架构的最佳解决方案移动。因此,其中一个问题是找到神经网络中权重的最佳初始化值。诸如 Keras 的库可以自动处理这个问题。然而,这个话题足够重要,值得讨论这一点。

通过使用输入作为期望输出来预先训练网络来使用受限玻尔兹曼机,使网络自动学习输入的表示并相应地调整其权重,这个话题已经在第四章中讨论过,无监督特征学习

此外,存在许多预训练网络提供良好的结果。正如我们所提到的,许多人一直在研究卷积神经网络,并取得了令人印象深刻的结果,通过重新利用这些网络学到的权重并将它们应用于其他项目,通常可以节省时间。

K. Simonyan, A. Zisserman 在Very Deep Convolutional Networks for Large-Scale Image Recognition中使用的 VGG-16 模型,arxiv.org/pdf/1409.1556v6.pdf,是图像识别中的重要模型。在这个模型中,输入是一个固定的 224 x 224 的 RGB 图像,唯一的预处理是减去在训练集上计算的平均 RGB 值。我们在附图中概述了这个网络的架构,用户可以尝试自己实现这样一个网络,但也要注意运行这样一个网络的计算密集性。在这个网络中,架构如下:


Simonyan 和 Zisserman 的 VGG-16 卷积神经网络架构。

我们还将感兴趣的读者引荐到另一个值得注意的例子,即 AlexNet 网络,包含在 Alex Krizhevsky, Ilya Sutskeve, Geoffrey Hinton 的使用深度卷积神经网络进行 ImageNet 分类中,出自于 Advances in Neural Information Processing Systems 25 (NIPS 2012),papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf,但我们出于简洁起见,在此不讨论它,但我们邀请感兴趣的读者去查看。我们还邀请感兴趣的读者查看 github.com/fchollet/deep-learning-models 获取 VGG-16 和其他网络的代码示例。

总结

值得注意的是,正如可能已经清楚的,卷积神经网络没有通用的架构。但是,有一些一般性的指导原则。通常,池化层跟在卷积层后面,并且经常习惯于堆叠两个或更多连续的卷积层来检测更复杂的特征,就像在前面展示的 VGG-16 神经网络示例中所做的那样。卷积网络非常强大。然而,它们可能非常耗费资源(例如上面的 VGG-16 示例相对复杂),通常需要长时间的训练,这就是为什么使用 GPU 可以帮助加速性能的原因。它们的优势在于它们不专注于整个图像,而是专注于较小的子区域,以找到组成图像的有趣特征,从而能够找到不同输入之间的区别元素。由于卷积层非常耗费资源,我们引入了池化层来帮助减少参数数量而不增加复杂性,而使用丢弃层有助于确保没有神经元过于依赖其他神经元,因此神经网络中的每个元素都会有助于学习。

在本章中,我们从类比我们的视觉皮层如何工作开始,介绍了卷积层,并随后描述了它们为何有效的直观理解。我们介绍了滤波器,还涵盖了滤波器可以有不同的大小和不同的填充方式,我们还看到了如何设置零填充可以确保结果图像与原始图像具有相同的大小。如上所述,池化层可以帮助减少复杂性,而丢弃层可以使神经网络在识别模式和特征方面更加有效,并且特别适用于减少过拟合的风险。

一般来说,在给定的例子中,特别是在mnist的例子中,我们已经展示了神经网络中的卷积层在处理图像时,可以比普通的深度神经网络取得更好的准确性,在数字识别方面达到了超过 99%的准确度,而且通过限制参数的使用,避免了模型过拟合的问题。在接下来的章节中,我们将会研究语音识别,然后开始研究使用强化学习而不是监督或非监督学习的模型的例子,介绍在棋盘游戏和视频游戏中使用深度学习的例子。

第六章:循环神经网络和语言模型

我们在前几章讨论的神经网络架构接受固定大小的输入并提供固定大小的输出。即使在图像识别中使用的卷积网络(第五章,“图像识别”)也被展平成一个固定输出向量。本章将通过引入循环神经网络RNNs)来摆脱这一限制。RNN 通过在这些序列上定义递推关系来帮助我们处理可变长度的序列,因此得名。

处理任意输入序列的能力使得 RNN 可用于诸如语言建模(参见语言建模部分)或语音识别(参见语音识别部分)等任务。事实上,理论上,RNN 可以应用于任何问题,因为已经证明它们是图灵完备的 [1]。这意味着在理论上,它们可以模拟任何常规计算机无法计算的程序。作为这一点的例证,Google DeepMind 提出了一个名为“神经图灵机”的模型,该模型可以学习执行简单的算法,比如排序 [2]。

在本章中,我们将涵盖以下主题:

  • 如何构建和训练一个简单的 RNN,基于一个简单的问题
  • RNN 训练中消失和爆炸梯度的问题以及解决方法
  • 用于长期记忆学习的 LSTM 模型
  • 语言建模以及 RNN 如何应用于这个问题
  • 应用深度学习于语音识别的简要介绍

循环神经网络

RNN 之所以得名,是因为它们在序列上重复应用相同的函数。可以通过下面的函数将 RNN 写成递推关系:


这里的 S[t] —第 t 步的状态—是由函数 f 从前一步的状态,即 t-1,和当前步骤的输入 X[t] 计算得出。这种递推关系通过在先前状态上的反馈循环来定义状态如何逐步在序列中演变,如下图所示:


图来自[3]

左:RNN 递推关系的可视化示例:S [t] = S [t-1] ** W + X* [t] ** U*。最终输出将是 o [t] = VS* [t]

右:RNN 状态在序列 t-1, t, t+1 上递归展开。注意参数 U、V 和 W 在所有步骤之间共享。

这里 f 可以是任何可微分函数。例如,一个基本的 RNN 定义如下递推关系:


这里W定义了从状态到状态的线性变换,U是从输入到状态的线性变换。tanh函数可以被其他变换替代,比如 logit,tanh 或者 ReLU。这个关系可以在下图中进行解释,O[t]是网络生成的输出。

例如,在词级语言建模中,输入X将是一个序列的单词编码的输入向量*(X*[1]…X[t]…)。状态S将会是一个状态向量的序列*(S*[1]…S[t]…)。输出O将是下一个序列中的单词的概率向量的序列*(O*[1]…O[t]…)

需要注意的是,在 RNN 中,每个状态都依赖于其之前的所有计算,通过这种循环关系。这个关系的一个重要含义是,RNN 在时间上有记忆,因为状态S包含了基于先前步骤的信息。理论上,RNN 可以记住任意长时间的信息,但在实践中,它们只能回顾几个步骤。我们将在消失爆炸梯度部分更详细地讨论这个问题。

因为 RNN 不限于处理固定大小的输入,它们确实扩展了我们可以使用神经网络进行计算的可能性,比如不同长度的序列或不同大小的图像。下图直观地说明了我们可以制作的一些序列的组合。以下是这些组合的简要说明:

  • 一对一:这是非顺序处理,比如前馈神经网络和卷积神经网络。请注意,一个前馈网络和 RNN 应用在一个时间步骤上没有太大的区别。一个一对一处理的例子是来自章节的图像分类(参见第五章,图像识别)。
  • 一对多:这基于单一的输入生成一个序列,例如,来自图像的标题生成[4]。
  • 多对一:这基于一个序列输出一个单一的结果,例如,文本的情感分类。
  • 多对多间接:一个序列被编码成一个状态向量,之后这个状态向量被解码成一个新的序列,例如,语言翻译[5],[6]。
  • 多对多直接:这对每个输入步骤输出一个结果,例如,语音识别中的帧语素标记(见语音识别部分)。
    来自[7]的图片
    RNN 扩展了我们可以使用神经网络进行计算的可能性—红色:输入 X,绿色:状态 S,蓝色:输出 O。

RNN — 如何实现和训练

在前面的部分,我们简要讨论了 RNN 是什么以及它们可以解决的问题。让我们深入了解 RNN 的细节,以及如何通过一个非常简单的玩具例子来训练它:在一个序列中计算“1”的个数。

在这个问题中,我们要教会最基本的循环神经网络如何计算输入中 1 的个数,并且在序列结束时输出结果。我们将在 Python 和 NumPy 中展示这个网络的实现。输入和输出的一个示例如下:

In:  (0, 0, 0, 0, 1, 0, 1, 0, 1, 0)
Out:  3

我们要训练的网络是一个非常基本的网络,如下图所示:


基本的循环神经网络用于计算输入中的 1 的个数

网络只有两个参数:一个输入权重 U 和一个循环权重 W。输出权重 V 设为 1,所以我们只需读取最后一个状态作为输出 y。这个网络定义的循环关系是 S [t] = S [t-1] ** W + X* [t] ** U*。请注意,这是一个线性模型,因为在这个公式中我们没有应用非线性函数。这个函数的代码定义如下:

def step(s, x, U, W):
    return x * U + s * W

因为 3 是我们想要输出的数字,而且有三个 1,这个问题的一个很好的解决方案就是简单地对整个序列进行求和。如果我们设置 U=1,那么每当接收到一个输入,我们将得到它的完整值。如果我们设置 W=1,那么我们累积的值将永远不会衰减。所以,对于这个例子,我们会得到期望的输出:3。

尽管,这个神经网络的训练和实现将会很有趣,正如我们将在本节的其余部分中看到的。所以让我们看看我们如何通过反向传播来得到这个结果。

通过时间的反向传播

通过时间的反向传播算法是我们用来训练循环网络的典型算法[8]。这个名字已经暗示了它是基于我们在 第二章 讨论的反向传播算法,神经网络

如果你了解常规的反向传播,那么通过时间的反向传播就不难理解。主要区别在于,循环网络需要在一定数量的时间步长内进行展开。这个展开如前图所示(基本的循环神经网络用于计算输入中的 1 的个数)。展开完成后,我们得到一个与常规的多层前馈网络非常相似的模型。唯一的区别在于,每层都有多个输入(上一个状态,即 S [t-1]),和当前输入(X [t]),以及参数(这里的 UW)在每层之间是共享的。

前向传播将 RNN 沿着序列展开,并为每个步骤构建一个活动堆栈。批处理输入序列 X 的前向步骤可实现如下:

def forward(X, U, W):
    # Initialize the state activation for each sample along the sequence
    S = np.zeros((number_of_samples, sequence_length+1))
    # Update the states over the sequence
    for t in range(0, sequence_length):
        S[:,t+1] = step(S[:,t], X[:,t], U, W)  # step function
    return S

在进行这个前向步骤之后,我们有了每一步和每个样本在批处理中的激活,由 S 表示。因为我们想输出更多或更少连续的输出(全部为 1 的总和),我们使用均方误差代价函数来定义我们的输出成本与目标和输出 y,如下:

cost = np.sum((targets – y)**2)

现在我们有了前向步骤和成本函数,我们可以定义梯度如何向后传播。首先,我们需要得到输出y相对于成本函数的梯度(??/?y)。

一旦我们有了这个梯度,我们可以通过向后传播堆栈中构建的活动来将其传播到每个时间步长处的误差导数。传播该梯度通过网络的循环关系可以写成以下形式:


参数的梯度通过以下方式累积:


在接下来的实现中,在反向步骤中,分别通过gUgW累积UW的梯度:

def backward(X, S, targets, W):
    # Compute gradient of output
    y = S[:,-1]  # Output `y` is last activation of sequence
    # Gradient w.r.t. cost function at final state
    gS = 2.0 * (y - targets)
    # Accumulate gradients backwards
    gU, gW = 0, 0  # Set the gradient accumulations to 0    
    for k in range(sequence_len, 0, -1):
        # Compute the parameter gradients and accumulate the results.
        gU += np.sum(gS * X[:,k-1])
        gW += np.sum(gS * S[:,k-1])
        # Compute the gradient at the output of the previous layer
        gS = gS * W
    return gU, gW
• 1

现在我们可以尝试使用梯度下降优化我们的网络:

learning_rate = 0.0005
# Set initial parameters
parameters = (-2, 0)  # (U, W)
# Perform iterative gradient descent
for i in range(number_iterations):
    # Perform forward and backward pass to get the gradients
    S = forward(X, parameters(0), parameters(1))
    gradients = backward(X, S, targets, parameters(1))
    # Update each parameter `p` by p = p - (gradient * learning_rate).
    # `gp` is the gradient of parameter `p`
    parameters = ((p - gp * learning_rate) 
                  for p, gp in zip(parameters, gradients))

不过存在一个问题。注意,如果尝试运行此代码,最终参数UW往往会变为不是一个数字NaN)。让我们尝试通过在错误曲面上绘制参数更新来调查发生了什么,如下图所示。注意,参数慢慢朝着最佳值(U=W=1)移动,直到超过并达到大约(U=W=1.5)。此时,梯度值突然爆炸,使参数值跳出图表。这个问题被称为梯度爆炸。下一节将详细解释为什么会发生这种情况以及如何预防。


参数更新通过梯度下降在错误曲面上绘制。错误曲面以对数颜色比例尺绘制。

梯度消失和梯度爆炸

RNN 相对于前馈或卷积网络更难训练。一些困难源于 RNN 的循环性质,其中同一权重矩阵用于计算所有状态更新[9],[10]。

上一节的结尾,前面的图示了梯度爆炸,由于长期组件的膨胀导致 RNN 训练进入不稳定状态。除了梯度爆炸问题,还存在梯度消失问题,即相反的情况发生。长期组件以指数速度趋于零,模型无法从时间上遥远的事件中学习。在本节中,我们将详细解释这两个问题以及如何应对它们。

爆炸和消失梯度都源于通过时间向后传播梯度的循环关系形成了一个几何序列:


在我们简单的线性 RNN 中,如果*|W| > 1*,梯度会呈指数增长。这就是所谓的梯度爆炸(例如,50 个时间步长下的W=1.5W**[50]**=1.5˜≈6 * 10⁸*)。如果*|W| < 1*,梯度会呈指数衰减;这就是所谓的梯度消失(例如,20 个时间步长下的W=0.6W**[20]**=0.6˜≈310**^(-5))。如果权重参数W是矩阵而不是标量,则这种爆炸或消失的梯度与W的最大特征值(ρ)有关(也称为谱半径)。对于梯度消失,ρ < 1足够,使得梯度消失,对于梯度爆炸,ρ > 1是必要的。

以下图形直观地说明了梯度爆炸的概念。发生的情况是我们正在训练的成本表面非常不稳定。使用小步,我们可能会移动到成本函数的稳定部分,梯度很低,并突然遇到成本的跳跃和相应的巨大梯度。因为这个梯度非常巨大,它将对我们的参数产生很大影响。它们最终会落在成本表面上离它们最初的位置很远的地方。这使得梯度下降学习不稳定,甚至在某些情况下是不可能的。


梯度爆炸的插图[11]

我们可以通过控制梯度的大小来对抗梯度爆炸的效果。一些解决方案的例子有:

  • 梯度截断,我们将梯度能获得的最大值设定为阈值[11]。
  • 二阶优化(牛顿法),我们模拟成本函数的曲率。模拟曲率使我们能够在低曲率情况下迈出大步,在高曲率情况下迈出小步。出于计算原因,通常只使用二阶梯度的近似值[12]。
  • 依赖于局部梯度较少的优化方法,例如动量[13]或 RmsProp [14]。

例如,我们可以利用 Rprop [15]重新训练我们无法收敛的网络(参见梯度爆炸的插图)。 Rprop 是一种类似于动量法的方法,仅使用梯度的符号来更新动量参数,因此不受梯度爆炸的影响。如果我们运行 Rprop 优化,可以看到训练收敛于下图。请注意,尽管训练开始于一个高梯度区域(U=-1.5,W=2),但它很快收敛直到找到最佳点(U=W=1)。


通过 Rprop 在误差表面绘制的参数更新。误差表面是以对数刻度绘制的。

消失梯度问题是爆炸梯度问题的逆问题。梯度在步数上呈指数衰减。这意味着早期状态的梯度变得非常小,保留这些状态历史的能力消失了。较早时间步的小梯度被较近时间步的较大梯度所淘汰。Hochreiter 和 Schmidhuber [16] 将其描述如下:通过时间的反向传播对最近的干扰过于敏感

这个问题更难以检测,因为网络仍然会学习和输出一些东西(不像爆炸梯度的情况)。它只是无法学习长期依赖性。人们已经尝试用类似于我们用于爆炸梯度的解决方案来解决这个问题,例如二阶优化或动量。这些解决方案远非完美,使用简单 RNN 学习长期依赖性仍然非常困难。幸运的是,有一种聪明的解决方案可以解决消失梯度问题,它使用由记忆单元组成的特殊架构。我们将在下一节详细讨论这个架构。

长短期记忆

在理论上,简单的 RNN 能够学习长期依赖性,但在实践中,由于消失梯度问题,它们似乎只限于学习短期依赖性。Hochreiter 和 Schmidhuber 对这个问题进行了广泛的研究,并提出了一种解决方案,称为长短期记忆LSTM)[16]。由于特别设计的记忆单元,LSTM 可以处理长期依赖性。它们工作得非常好,以至于目前在各种问题上训练 RNN 的大部分成就都归功于使用 LSTM。在本节中,我们将探讨这个记忆单元的工作原理以及它是如何解决消失梯度问题的。

LSTM 的关键思想是单元状态,其中的信息只能明确地写入或删除,以使单元状态在没有外部干扰的情况下保持恒定。下图中时间 t 的单元状态表示为 c [t]

LSTM 单元状态只能通过特定的门来改变,这些门是让信息通过的一种方式。这些门由 logistic sigmoid 函数和逐元素乘法组成。因为 logistic 函数只输出介于 0 和 1 之间的值,所以乘法只能减小通过门的值。典型的 LSTM 由三个门组成:遗忘门、输入门和输出门。这些在下图中都表示为 fio。请注意,单元状态、输入和输出都是向量,因此 LSTM 可以在每个时间步骤保存不同信息块的组合。接下来,我们将更详细地描述每个门的工作原理。


LSTM 单元

x*[t]、c[t]、h[t]* 分别是时间 t 的输入、细胞状态和 LSTM 输出。

LSTM 中的第一个门是遗忘门;因为它决定我们是否要擦除细胞状态,所以被称为遗忘门。这个门不在 Hochreiter 最初提出的 LSTM 中;而是由 Gers 等人提出[17]的。遗忘门基于先前的输出 h [t-1] 和当前的输入 x*[t]* . 它将这些信息组合在一起,并通过逻辑函数压缩它们,以便为细胞的矢量块输出介于 0 和 1 之间的数字。由于与细胞的逐元素乘法,一个输出为 0 的完全擦除特定的细胞块,而输出为 1 会保留该细胞块中的所有信息。这意味着 LSTM 可以清除其细胞状态向量中的不相关信息。


接下来的门决定要添加到内存单元的新信息。这分为两部分进行。第一部分决定是否添加信息。与输入门类似,它基于 h*[t-1]* 和 x*[t]* 进行决策,并通过每个细胞块的矢量的逻辑函数输出 0 或 1。输出为 0 意味着不向该细胞块的内存中添加任何信息。因此,LSTM 可以在其细胞状态向量中存储特定的信息片段:


要添加的输入 a [t] 是由先前的输出 (h [t-1]) 和当前的输入 (x*[t]*) 派生,并通过 tanh 函数变换:


遗忘门和输入门完全决定了通过将旧的细胞状态与新的信息相加来确定新细胞:


最后一个门决定输出结果。输出门将 h [t-1]x*[t]* 作为输入,并通过逻辑函数输出 0 或 1,在每个单元块的内存中均可用。输出为 0 表示该单元块不输出任何信息,而输出为 1 表示整个单元块的内存传递到细胞的输出。因此,LSTM 可以从其细胞状态向量中输出特定的信息块:


最终输出的值是通过 tanh 函数传递的细胞内存:


因为所有这些公式是可导的,我们可以像连接简单的 RNN 状态一样连接 LSTM 单元,并通过时间反向传播来训练网络。

现在问题是 LSTM 如何保护我们免受梯度消失的影响?请注意,如果遗忘门为 1 且输入门为 0,则细胞状态会被逐步地从步骤复制。只有遗忘门才能完全清除细胞的记忆。因此,记忆可以长时间保持不变。还要注意,输入是添加到当前细胞记忆的 tanh 激活;这意味着细胞记忆不会爆炸,并且非常稳定。

实际上,以下图示了 LSTM 如何展开。

初始时,网络的输入被赋值为 4.2;输入门被设置为 1,所以完整的值被存储。接下来的两个时间步骤中,遗忘门被设置为 1。所以在这些步骤中保留了全部信息,并且没有添加新的信息,因为输入门被设置为 0。最后,输出门被设置为 1,4.2 被输出并保持不变。


LSTM 通过时间展开[18]

尽管在前面的图示中描述的 LSTM 网络是大多数应用中使用的典型 LSTM 版本,但有许多变体的 LSTM 网络,它们以不同的顺序组合不同的门[19]。深入了解所有这些不同的架构超出了本书的范围。

语言建模

语言模型的目标是计算单词序列的概率。它们对于许多不同的应用非常关键,例如语音识别、光学字符识别、机器翻译和拼写校正。例如,在美式英语中,短语"wreck a nice beach"和"recognize speech"在发音上几乎相同,但它们的含义完全不同。一个好的语言模型可以根据对话的上下文区分哪个短语最有可能是正确的。本节将概述基于单词和字符的语言模型以及如何使用循环神经网络来构建它们。

基于单词的模型

基于单词的语言模型定义了一个对单词序列的概率分布。给定长度为m的单词序列,它为完整的单词序列赋予了概率P(w [1] , … , w [m] )。这些概率的应用有两个方面。我们可以用它们来估计自然语言处理应用中不同短语的可能性。或者,我们可以用它们来生成新的文本。

N-gram 模型

推断一个长序列(如w [1] , …, w [m])的概率通常是不可行的。通过应用以下链式法则可以计算出P(w [1] , … , w [m] *)*的联合概率:


特别是基于前面的单词给出后面单词的概率将很难从数据中估计出来。这就是为什么这个联合概率通常被一个独立假设近似,即第i个单词只依赖于前n-1个单词。我们只建模n个连续单词称为 n-grams 的联合概率。注意,n-grams 可以用来指代其他长度为n的序列,例如n个字符。

联合分布的推断通过 n-gram 模型进行近似,将联合分布拆分为多个独立部分。注意,n-grams 是多个连续单词的组合,其中n是连续单词的数量。例如,在短语the quick brown fox中,我们有以下 n-grams:

  • 1-gram:“The,” “quick,” “brown,” 和 “fox”(也称为 unigram)
  • 2-grams:“The quick,” “quick brown,” 和 “brown fox”(也称为 bigram)
  • 3-grams:“The quick brown” 和 “quick brown fox”(也称为 trigram)
  • 4-grams:“The quick brown fox”

现在,如果我们有一个庞大的文本语料库,我们可以找到直到某个n(通常为 2 到 4)的所有 n-grams,并计算该语料库中每个 n-gram 的出现次数。从这些计数中,我们可以估计每个 n-gram 的最后一个单词在给定前n-1个单词的情况下的概率:

  • 1-gram
  • 2-gram
  • n-gram

现在可以使用第i个单词仅依赖于前n**-1个单词的独立假设来近似联合分布。

例如,对于一个 unigram,我们可以通过以下方式近似联合分布:


对于 trigram,我们可以通过以下方式近似联合分布:


我们可以看到,基于词汇量,随着n的增加,n-grams 的数量呈指数增长。例如,如果一个小词汇表包含 100 个单词,那么可能的 5-grams 的数量将是100**⁵ = 10,000,000,000个不同的 5-grams。相比之下,莎士比亚的整个作品包含大约30,000个不同的单词,说明使用具有大n值的 n-grams 是不可行的。不仅需要存储所有概率,我们还需要一个非常庞大的文本语料库来为较大的n值创建良好的 n-gram 概率估计。这个问题就是所谓的维度灾难。当可能的输入变量(单词)数量增加时,这些输入值的不同组合数量呈指数增长。当学习算法需要至少一个与相关值组合的示例时,就会出现这种维度灾难,这在 n-gram 建模中是这样的情况。我们的n越大,我们就越能近似原始分布,并且我们需要更多的数据来对 n-gram 概率进行良好的估计。

神经语言模型

在前面的部分中,我们用 n-grams 建模文本时展示了维度灾难。我们需要计算的 n-grams 数量随着n和词汇表中的单词数量呈指数增长。克服这个问题的一种方法是通过学习单词的较低维度、分布式表示来学习一个嵌入函数[20]。这个分布式表示是通过学习一个嵌入函数将单词空间转换为较低维度的单词嵌入空间而创建的,具体如下:


从词汇表中取出的单词被转换为大小为 V 的独热编码向量(V 中的每个单词都被唯一编码)。然后,嵌入函数将这个 V 维空间转换为大小为 D 的分布式表示(这里 D=4)。

这个想法是,学习的嵌入函数会学习关于单词的语义信息。它将词汇表中的每个单词与一个连续值向量表示相关联,即单词嵌入。在这个嵌入空间中,每个单词对应一个点,其中不同的维度对应于这些单词的语法或语义属性。目标是确保在这个嵌入空间中彼此接近的单词应具有相似的含义。这样,一些单词语义上相似的信息可以被语言模型利用。例如,它可能会学习到“fox”和“cat”在语义上相关,并且“the quick brown fox”和“the quick brown cat”都是有效的短语。然后,一系列单词可以转换为一系列捕捉到这些单词特征的嵌入向量。

可以通过神经网络对语言模型进行建模,并隐式地学习这个嵌入函数。我们可以学习一个神经网络,给定一个n-1个单词的序列(w*[t-n+1],…,w*[t-1]),试图输出下一个单词的概率分布,即w**[t]。网络由不同部分组成。

嵌入层接受单词w [i]的独热表示,并通过与嵌入矩阵C相乘将其转换为其嵌入。这种计算可以通过表查找有效地实现。嵌入矩阵C在所有单词上共享,因此所有单词使用相同的嵌入函数。C由一个V * D矩阵表示,其中V是词汇量的大小,D是嵌入的大小。得到的嵌入被连接成一个隐藏层;之后,可以应用一个偏置b和一个非线性函数,比如tanh。隐藏层的输出因此由函数z = tanh(concat(w [t-n+1] , …, w [t-1] ) + b)表示。从隐藏层,我们现在可以通过将隐藏层与U相乘来输出下一个单词w [t]的概率分布。这将隐藏层映射到单词空间,添加一个偏置b并应用 softmax 函数以获得概率分布。最终层计算softmax(zU +b)*。这个网络如下图所示:


给定单词w**[t-1] … w*[t-n+1],输出单词w[t]*的概率分布的神经网络语言模型。C是嵌入矩阵。

这个模型同时学习词汇表中所有单词的嵌入以及单词序列的概率函数模型。由于这些分布式表示,它能够将这个概率函数推广到在训练过程中没有看到的单词序列。测试集中特定的单词组合在训练集中可能没有出现,但是具有类似嵌入特征的序列更有可能在训练过程中出现。

下图展示了一些词嵌入的二维投影。可以看到,在嵌入空间中,语义上接近的单词也彼此接近。


在这个空间中,二维嵌入空间中相关的单词彼此接近 [21]。

单词嵌入可以在大型文本数据语料库上无监督地训练。这样,它们能够捕捉单词之间的一般语义信息。得到的嵌入现在可以用于改进其他任务的性能,其中可能没有大量标记的数据可用。例如,试图对文章的情感进行分类的分类器可能是在使用先前学习的单词嵌入而不是独热编码向量进行训练的。这样,单词的语义信息就变得对情感分类器可用了。因此,许多研究致力于创建更好的单词嵌入,而不是专注于学习单词序列上的概率函数。例如,一种流行的单词嵌入模型是 word2vec [22],[23]。

令人惊讶的是,这些词嵌入可以捕捉单词之间的类比作为差异。例如,它可能捕捉到"女人"和"男人"的嵌入之间的差异编码了性别,并且这个差异在其他与性别相关的单词,如"皇后"和"国王"中是相同的。


词嵌入可以捕捉单词之间的语义差异 [24]。

embed(女人) - embed(男人) ? embed(姑妈) - embed(叔叔)

embed(女人) - embed(男人) ? embed(皇后) - embed(国王)

虽然之前的前馈网络语言模型可以克服模拟大词汇输入的维度诅咒,但仍然仅限于建模固定长度的单词序列。为了克服这个问题,我们可以使用 RNN 来构建一个不受固定长度单词序列限制的 RNN 语言模型 [25]。这些基于 RNN 的模型不仅可以在输入嵌入中聚类相似的单词,还可以在循环状态向量中聚类相似的历史。

这些基于单词的模型的一个问题是计算每个单词在词汇表中的输出概率 P(w [i] | context)。我们通过对所有单词激活进行 softmax 来获得这些输出概率。对于一个包含50,000个单词的小词汇表 V,这将需要一个*|S| * |V|的输出矩阵,其中|V|是词汇表的大小,|S|*是状态向量的大小。这个矩阵非常庞大,在增加词汇量时会变得更大。由于 softmax 通过所有其他激活的组合来归一化单个单词的激活,我们需要计算每个激活以获得单个单词的概率。这两者都说明了在大词汇表上计算 softmax 的困难性;在 softmax 之前需要大量参数来建模线性转换,并且 softmax 本身计算量很大。

有一些方法可以克服这个问题,例如,通过将 softmax 函数建模为一个二叉树,从而只需要 log(|V|) 计算来计算单个单词的最终输出概率 [26]。

不详细介绍这些解决方法,让我们看看另一种语言建模的变体,它不受这些大词汇量问题的影响。

Python 深度学习(二)(3)https://developer.aliyun.com/article/1511970

相关文章
|
3天前
|
机器学习/深度学习 人工智能 算法
中草药识别系统Python+深度学习人工智能+TensorFlow+卷积神经网络算法模型
中草药识别系统Python+深度学习人工智能+TensorFlow+卷积神经网络算法模型
18 0
|
10天前
|
机器学习/深度学习 自然语言处理 TensorFlow
使用Python实现深度学习模型:注意力机制(Attention)
使用Python实现深度学习模型:注意力机制(Attention)
24 0
使用Python实现深度学习模型:注意力机制(Attention)
|
12天前
|
机器学习/深度学习 数据可视化 PyTorch
使用Python实现深度学习模型:迁移学习与预训练模型
使用Python实现深度学习模型:迁移学习与预训练模型
34 0
|
13天前
|
机器学习/深度学习 人工智能 算法
食物识别系统Python+深度学习人工智能+TensorFlow+卷积神经网络算法模型
食物识别系统采用TensorFlow的ResNet50模型,训练了包含11类食物的数据集,生成高精度H5模型。系统整合Django框架,提供网页平台,用户可上传图片进行食物识别。效果图片展示成功识别各类食物。[查看演示视频、代码及安装指南](https://www.yuque.com/ziwu/yygu3z/yhd6a7vai4o9iuys?singleDoc#)。项目利用深度学习的卷积神经网络(CNN),其局部感受野和权重共享机制适于图像识别,广泛应用于医疗图像分析等领域。示例代码展示了一个使用TensorFlow训练的简单CNN模型,用于MNIST手写数字识别。
38 3
|
16天前
|
机器学习/深度学习 存储 TensorFlow
Python 深度学习(一)(4)
Python 深度学习(一)
29 2
|
16天前
|
机器学习/深度学习 算法 算法框架/工具
Python 深度学习(一)(3)
Python 深度学习(一)
34 2
|
16天前
|
机器学习/深度学习 算法 自动驾驶
Python 深度学习(一)(2)
Python 深度学习(一)
31 1
|
16天前
|
机器学习/深度学习 人工智能 算法
Python 深度学习(一)(1)
Python 深度学习(一)
16 1
|
16天前
|
机器学习/深度学习 分布式计算 算法
Python 深度学习(三)(4)
Python 深度学习(三)
19 0
|
16天前
|
机器学习/深度学习 运维 算法
Python 深度学习(三)(3)
Python 深度学习(三)
15 0
Python 深度学习(三)(3)