这个教程涵盖了深度学习(Deep Learning)的一些重要概念,是一个快速入门的大纲教程,包含了三个部分:
这个数据集(mnist.pkl.gz)可以在CSDN下载中免费下载:http://download.csdn.net/detail/ws_20100/9224993
MNIST数据集包含手写字符图像,其中有60000个是训练样本,而10000个是测试样本。对于很多论文,包含这篇教程,60000个训练样本被分为50000个样本作训练集,10000个样本作验证集(用于选择超参数,例如学习率和模型大小)。所有的数字图像在大小上都已经规范化并且处于28 × 28像素的中心位置。所有的图像像素点的值都处于0到255之间,其中,0代表黑色,255代表白色,中间的值为不同等级的灰色。
为了方便我们在python中使用MNIST数据集,我们对MNIST数据集进行处理。处理后的数据集有3个列表:训练集,验证集和测试集。每个列表包含多个二元组,每个二元组包含一个图像和对应的标签。图像被表示为一个1 × 784(28 × 28)维数组,数组中每个元素在0到1之间,0代表黑,1代表白。标签是一个0到9的数值。加载数据集的代码如下:
import cPickle, gzip, numpy
# Load the dataset
f = gzip.open('mnist.pkl.gz', 'rb')
train_set, valid_set, test_set = cPickle.load(f)
f.close()
当使用数据集的时候,我们通常将其分成多个minibatch(详细的请看下面的随机梯度下降法)。在实际使用时,最好使用共享变量,因为共享变量和GPU相关。如果不使用共享变量,使用GPU运算可能不比CPU快多少,可能更慢。存取数据,读取minibatch的代码如下:
def shared_dataset(data_xy):
""" Function that loads the dataset into shared variables The reason we store our dataset in shared variables is to allow Theano to copy it into the GPU memory (when code is run on GPU). Since copying data into the GPU is slow, copying a minibatch everytime is needed (the default behaviour if the data is not in a shared variable) would lead to a large decrease in performance. """
data_x, data_y = data_xy
shared_x = theano.shared(numpy.asarray(data_x, dtype=theano.config.floatX))
shared_y = theano.shared(numpy.asarray(data_y, dtype=theano.config.floatX))
# When storing data on the GPU it has to be stored as floats
# therefore we will store the labels as ``floatX`` as well
# (``shared_y`` does exactly that). But during our computations
# we need them as ints (we use labels as index, and if they are
# floats it doesn't make sense) therefore instead of returning
# ``shared_y`` we will have to cast it to int. This little hack
# lets us get around this issue
return shared_x, T.cast(shared_y, 'int32')
test_set_x, test_set_y = shared_dataset(test_set)
valid_set_x, valid_set_y = shared_dataset(valid_set)
train_set_x, train_set_y = shared_dataset(train_set)
batch_size = 500 # size of the minibatch
# accessing the third minibatch of the training set
data = train_set_x[2 * batch_size: 3 * batch_size]
label = train_set_y[2 * batch_size: 3 * batch_size]
If you are running your code on the GPU and the dataset you are using is too large to fit in memory the code will crash. In such a case you should store the data in a shared variable. You can however store a sufficiently small chunk of your data (several minibatches) in a shared variable and use that during training. Once you got through the chunk, update the values it stores. This way you minimize the number of data transfers between CPU memory and GPU memory.
我们将数据集标记为 D ,如果需要标注不同的数据集,我们将训练集,验证集和测试集分别标注为 Dtrain , Dvalid , Dtest 。验证集用于完成模型选择和超参数选择,而测试集用于评估最终的泛化误差,并公平的比较不同算法的性能。
这个教程多数算法都是用于处理分类问题,所以数据集 D 是一个二元组序列 (x(i),y(i)) ,我们使用上标来区别不同的训练集样本: x(i)∈RD ,其中第 i 个训练样本是 D 维向量。相似的, yi∈{0,...,L} ,其中第 i 个标签对应于 x(i) 的输入。显然,我们可以将样本对应的标签 yi 扩展到其他类型(例如,用于回归的高斯过程,或者用于预测多个符号的多项式组)。
教程的代码通常使用以下的命名空间:
import theano
import theano.tensor as T
import numpy
深度学习(Deep Learning)中最大的特点,就是大量使用深度网络的无监督学习(unsupervised learning)。但是监督学习仍然扮演着非常重要的角色。非监督预学习(pre-training)的作用在于,评估(在监督精细迭代(fine-tuning)之后)网络可以达到的性能。这节回顾了分类模型中监督学习的理论基础,并且包含了多数模型中精细迭代所需要的小批量数据的随机梯度下降算法(minibatch stochastic gradient descent algorithm)。
在这个深度学习教程中出现的模型大多是用于分类器设计。训练分类器的目的在于,对于未见过的样本,最小化错误分类(0-1损失)的数量。如果 f:RD→{0,...,L} 是预测函数,那么它的损失值可以写为:
# zero_one_loss is a Theano variable representing a symbolic
# expression of the zero one loss ; to get the actual value this
# symbolic expression has to be compiled into a Theano function (see
# the Theano tutorial for more details)
zero_one_loss = T.sum(T.neq(T.argmax(p_y_given_x), y))
由于0-1损失是不可微的,所以在大模型中使用它,会存在成千上万的系数,会不可避免地增加繁重的计算量。所以,对于训练集对应的标签,我们最大化分类器的对数似然。
# NLL is a symbolic variable ; to get the actual value of NLL, this symbolic
# expression has to be compiled into a Theano function (see the Theano
# tutorial for more details)
NLL = -T.sum(T.log(p_y_given_x)[T.arange(y.shape[0]), y])
# note on syntax: T.arange(y.shape[0]) is a vector of integers [0,1,2,...,len(y)].
# Indexing a matrix M by the two vectors [0,1,...,K], [a,b,...,k] returns the
# elements M[0,a], M[1,b], ..., M[K,k] as a vector. Here, we use this
# syntax to retrieve the log-probability of the correct labels, y.
什么是普通的梯度下降法呢?它是一个简单的算法,在含参损失函数的误差曲面上,向着误差更小的方向一步一步的调整。为了达到这个目标,我们需要将训练数据引入损失函数。该算法的伪代码如下所示:
# GRADIENT DESCENT
while True:
loss = f(params)
d_loss_wrt_params = ... # compute gradient
params -= learning_rate * d_loss_wrt_params
if <stopping condition is met>:
return params
随机梯度下降法(Stochastic gradient descent, SGD)遵循着和一般梯度下降法一样的准则。但是随机梯度下降法具有更快的速度,它一次只对一小部分样本估计梯度,而不是全部的样本集。为了更加纯粹的形式,我们每次只对一个样本进行梯度估计。
# STOCHASTIC GRADIENT DESCENT
for (x_i,y_i) in training_set:
# imagine an infinite generator
# that may repeat examples (if there is only a finite training set)
loss = f(params, x_i, y_i)
d_loss_wrt_params = ... # compute gradient
params -= learning_rate * d_loss_wrt_params
if <stopping condition is met>:
return params
用于深度学习的梯度下降法,是一种随机梯度下降法的变体,我们叫它”minibatches”。称为Minibatch SGD(MSGD),它和SGD工作原理相同,只是在一次估计中使用多个样本,而不仅仅是一个。这种方法减少了估计梯度的方差,并且能够在现代计算机中更好的组织内存。
for (x_batch,y_batch) in train_batches:
# imagine an infinite generator
# that may repeat examples
loss = f(params, x_batch, y_batch)
d_loss_wrt_params = ... # compute gradient using theano
params -= learning_rate * d_loss_wrt_params
if <stopping condition is met>:
return params
在选择minibatch大小(记为 B )时,需要一个折衷。在 B 从1增加到2时,方差的减少和SIMD指令的使用起了很大的作用,但是在 B 很大时,提升就不是那么明显了。对于更大的 B 值,时间应该被更好地用在梯度的步进上,而不是减少梯度的方差上。一个最优的 B 值应该是在模型上,数据集上,和硬件上都是独立的,并且可以取1到几百之间的任意数值,我们在这个教程中定义 B=20 ,但是要记住,它的取值是任意的。
如果你的训练的迭代次数是固定的,minibatch的大小就会变得至关重要,因为它控制着参数更新的次数。迭代次数为10,minibatch为1的迭代结果显然和迭代次数为10,但是minibatch为20的迭代结果不同。在调整minibatch大小时,需要谨记这点。
以上的代码都是伪代码格式的,真实的实用代码如下:
# Minibatch Stochastic Gradient Descent
# assume loss is a symbolic description of the loss function given
# the symbolic variables params (shared variable), x_batch, y_batch;
# compute gradient of loss with respect to params
d_loss_wrt_params = T.grad(loss, params)
# compile the MSGD step into a theano function
updates = [(params, params - learning_rate * d_loss_wrt_params)]
MSGD = theano.function([x_batch,y_batch], loss, updates=updates)
for (x_batch, y_batch) in train_batches:
# here x_batch and y_batch are elements of train_batches and
# therefore numpy arrays; function MSGD also updates the params
print('Current loss is ', MSGD(x_batch, y_batch))
if stopping_condition_is_met:
return params
最优化并不是机器学习的全部内容。在训练中,除了我们给定的完美样本以外,模型还会遇到它从来没有见过的样本。MSGD的训练过程不会考虑到这些,因此可能会对训练样本过拟合。一个用于抵抗过拟合的方法就是:正则化。正则化的方法有很多,在这里我们仅仅介绍 ℓ1 和 ℓ2 正则化,以及提前退出。
ℓ1 和 ℓ2 正则化,是在损失函数之后增加了一个额外项,用于惩罚相应的参数配置。在格式上,如果我们的损失函数定义为:
# symbolic Theano variable that represents the L1 regularization term
L1 = T.sum(abs(param))
# symbolic Theano variable that represents the squared L2 term
L2_sqr = T.sum(param ** 2)
# the loss
loss = NLL + lambda_1 * L1 + lambda_2 * L2
提前退出(Early-stopping),通过在一个验证集(validate set)上监控模型性能,来防止过拟合的发生。验证集的样本,没有用于梯度下降法(训练阶段),但也不是测试集的一部分。验证集样本被视为未来测试样本的一个典型代表。我们可以将它用于训练,因为它并不是测试集的一部分。如果在验证集上,模型的性能已经没有提高了,甚至在某些情况下,模型的性能已经随着训练有所下降时,那么训练程序会提前退出。
提前退出的条件有很多种,我们在此使用基于patience的增加数量的退出策略。
# early-stopping parameters
patience = 5000 # look as this many examples regardless
patience_increase = 2 # wait this much longer when a new best is
# found
improvement_threshold = 0.995 # a relative improvement of this much is
# considered significant
validation_frequency = min(n_train_batches, patience/2)
# go through this many
# minibatches before checking the network
# on the validation set; in this case we
# check every epoch
best_params = None
best_validation_loss = numpy.inf
test_score = 0.
start_time = time.clock()
done_looping = False
epoch = 0
while (epoch < n_epochs) and (not done_looping):
# Report "1" for first epoch, "n_epochs" for last epoch
epoch = epoch + 1
for minibatch_index in xrange(n_train_batches):
d_loss_wrt_params = ... # compute gradient
params -= learning_rate * d_loss_wrt_params # gradient descent
# iteration number. We want it to start at 0.
iter = (epoch - 1) * n_train_batches + minibatch_index
# note that if we do `iter % validation_frequency` it will be
# true for iter = 0 which we do not want. We want it true for
# iter = validation_frequency - 1.
if (iter + 1) % validation_frequency == 0:
this_validation_loss = ... # compute zero-one loss on validation set
if this_validation_loss < best_validation_loss:
# improve patience if loss improvement is good enough
if this_validation_loss < best_validation_loss * improvement_threshold:
patience = max(patience, iter * patience_increase)
best_params = copy.deepcopy(params)
best_validation_loss = this_validation_loss
if patience <= iter:
done_looping = True
break
# POSTCONDITION:
# best_params refers to the best out-of-sample parameters observed during the optimization
如果我们在达到终止条件(耗尽patience)之前,耗尽了所有的训练样本minibatch,那么我们就从最初的训练样本开始,重复训练。
注意:
validation_frequency
永远都要小于patience
。在耗尽patience之前,代码需要至少两次检查模型性能。这就是为什么我们使用公式:validation_frequency = min( value, patience/2.)
注意: 在决定是否增加patience时,不使用简单的比较,而使用统计显著性的一个测试,有可能可以增强算法的性能。