分享

Theano深度学习入门

 htxu91 2015-07-27

入门指南

这些教材不会试图去弥补研究生或者本科生的机器学习课程,而是概述一些重要的概念(和记法),以确保我们拥有相同的进度。为了运行接下来的教程例子代码,你还需要下载这节提到的数据集。

下载

在每个学习算法的页面,你可以下载相应的文件。如果希望一次性下载所有文件,你可以复制教程的Git仓库:

git clone git://github.com/lisa-lab/DeepLearningTutorials.git

数据集

MNIST数据集

mnist.pkl.gz

MNIST数据集由手写数字图像组成,分成60000个训练样本和10000测试样本。在许多论文,与这个教程一样,官方的60000训练样本分成50000个训练样本和10000个验证样本(为了选择像学习率和模型大小的超参数)。所有数字图像大小都归一化在固定的2828大小的图像中。在原始的数据集,图像的每个像素表示为一个[0,255]的值,0表示黑色,255表示白色,(0,255)之间的是不同的灰度。
这里有一些MNIST数字的例子:

为了方便,我处理了一下数据集,使得更容易在Python使用。可以在这里下载处理好的样本。处理过的文件表示为3个列表的元组:训练集、验证集和测试集。每个都是一对一系列图像和一系列类标识组成。一个图像表示为numpy的一维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()

当我们使用数据集时,我们经常把它分成多批(查看随机梯度下降)。你应该把数据集存储到共享变量里,基于批的索引号访问数据集,批的大小是固定已知的。使用共享变量的理由与使用GPU有关。当拷贝数据到GPU内存时,从CPU拷贝数据到GPU是个大的瓶颈。如果代码要做拷贝数据,如果没有使用共享变量,因为这个瓶颈,GPU代码不会比CPU代码快(可能更慢)。虽然你把数据放到Theano共享变量,当共享变量创建时,单次调用时,GPU也会把整个数据拷贝到GPU内存里。然后GPU可以使用切片共享变量访问任何一批数据。不需要从CPU内存里拷贝任何信息,因此绕过了瓶颈。因为样本和标识通常与自然的有区别(标识一般为整形而样本一般为实数),建议为标识和数据使用不同的变量。我们也推荐为训练集、验证集合测试集使用不同的变量,使得代码更具可读性(从而使用6个不同的变量)。
由于现在数据在一个变量里,一批定义为变量的切片,这样可以更自然的使用索引和大小来定义一批。在我们的设置中,整个代码中批的大小保持不变,因此一个函数实际上只需要仅仅表示哪个样本的的索引。下面的代码显示如何存储数据和访问每批样本:

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 * 500: 3 * 500]
label = train_set_y[2 * 500: 3 * 500]

在GPU,数据被保存成浮点(正确的类型dtypetheano.config.floatX给定)。为了回避标识的短处,我们把它们保存为浮点,然后转换为整形。

注意:如果你在GPU上运行代码,使用的数据集超出内存的大小,代码就会崩溃。这种情况下,需要把数据保存到共享变量。在训练时使用,你可以保存为足够小的数据块到共享变量。一旦你通过数据块获取数据,可以更新保存的值。这个方法可以最小化CPU和GPU内存之间的数据传输。

记法

数据集记法

我们标记数据集为D。区别是很重要的,我表示训练、验证和测试数据集为DtrainDvalidDtest。验证集用于执行模型选择和超参数选择,而测试集用于评估最终的泛化错误,并公平地与其他不同算法比较。
教材大多数处理分类问题,每个数据集D是一组索引对(x(i)y(i))。我们使用上标区分训练集的样本:x(i)RD是因为第i个训练样本是D维的。同样地,y(i)0,...,L是第i个与输入样本x(i)的标识。可以很简单地扩展这些样本,比如y(i)可以有其它的类型(比如,高斯回归或者预测多个符号的多项式组)。

数学约定

  • W:除非特定说明,引用矩阵的大写符号。
  • Wij:矩阵W的第i行第j列的元素。
  • Wi:矩阵的第i行向量。
  • Wj:矩阵的第j列向量。
  • b:除非特定说明,引用向量的小写符号。
  • bi:向量b的第i个元素。

符号和缩略词列表

  • D:输入维度大小
  • D(i)h:第i层的隐层单元数
  • fθ(x)f(x):模型P(Y|x,θ)对应的分类函数,定义为argmaxkP(Y=k|x,θ)。注意我们一般会丢掉下标θ
  • L:标识数目
  • L(θ,D):在样本集D上的以参数θ定义的模型的对数似然函数。
  • (θ,D):在数据集D上的以θ为参数的预测函数f的经验代价函数。
  • NLL:负对数似然函数。
  • θ:给定模型的所有参数集。

Python命名空间

教程代码经常使用下面的命名空间:

import theano
import theano.tensor as T
import numpy

一个深度学习的监督优化的初级教程

关于深度学习的令人兴奋的是主要使用无监督的深度学习网络。但是,监督学习也扮演着重要的角色。无监督预训练的作用经常估计在监督微调(fine-tuning)之后可以达到的性能。这节复习用于分类模型的监督学习,包括用于微调深度学习入门的多个模型的批量随机梯度下降。看看基于梯度学习的基础入门课程笔记中更多使用梯度优化训练准则的基本概念。

学习分类器

0-1代价

在这些深度学习教程提出的模型主要用于分类。训练分类器的目标是最小化测试样本集的错误率(0-1代价)。如果f:RD0,...,L是预测函数,则代价可以写为成:
0,1=|D|i=0If(x(i))y(i)
其中D为训练集或者DDtrain=(避免估计验证集或者测试集的错误率的偏差)。I是一个指示函数,定义为:
I_x=\left{\begin{array}{ccc}1& \mbox{ if x is True} \ 0& \mbox{ otherwise}\end{array}\right
在这个教程,f定义为:
f(x)=argmaxkP(Y=k|x,θ)
python里,可以使用Theano表示为:

# 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代价不可微,优化大的模型(数千或者数百万的参数的)的代价非常高。因此最大化训练集给定所有标识的分类器的对数似然函数。
L(θ,D)=|D|i=0logP(Y=y(i)|x(i),θ)
预测正确的类的概率不是正确预测的数目,但是从随机初始化的分类器看,它们相当相似的。记住似然函数和0-1代价函数的目的是不同的。你应该看到在验证集它们是相关的,但是有时一个会上升另一个会下降。
由于我们通常会说最小化代价函数,因此学习将尝试最小化负对数似然函数(NLL),定义为:

NLL(θ,D)=|D|i=0logP(Y=y(i)|x(i),θ)

我们分类器的NLL是一个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
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

深度学习中推荐是一个随机梯度下降更加优化的变种,叫批量随机梯度下降。批量随机梯度下降跟随机梯度下降一样地工作,除了每次使用更多的训练样本估计梯度。这个技术减少估计的梯度的方差,常更好的利用计算机的分层内存组织。

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

批量的大小B是一个权衡的选择。B从1增加到2时,减少方差(估计的梯度更准)和SIMD指令的使用,但是边界的改善就迅速消失了。越大的B,就越成倍减少估计的梯度的方差,在额外的梯度估计步骤就会花费更多的时间。最佳的B是模型、数据集和硬件独立的,可以为1到几百。在这教程,设置为20,但是这个选择几乎是任意的(即使无害的)。
注意:如果训练固定次数,批量的大小就变得更加重要,因为这个参数控制更新的数量。训练同样的模型10次,批量大小为1和训练10,批量大小为20的会产生完全不同的结果。记住当转换批量大小,准备好使用这个大小作用于所有的参数。
下面的所有代码块显示算法的伪代码是怎么样的。实现这样的算法,在Theano可以像下面这样的:

# 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

正规化

因为随机梯度下降是在线更新的算法,所以可以使用新的样本训练已有的模型。不过这个容易过拟合。一个防止过拟合的方法是正规化。还有一个是训练时一定条件下提早结束。

L1和L2正规化

L1和L2正规化包括添加额外的项到代价函数里,用于惩罚某些参数配置。形式上,如果代价函数是:
NLL(θ,D)=|D|i=0logP(Y=y(i)|x(i),θ)
则正规化的代价函数为:
E(θ,D)=NLL(θ,D)+λR(θ)$E(\theta, \mathcal{D}) = NLL(\theta, \mathcal{D}) + \lambda||\theta||p^p||\theta||p = \left(\sum_{j=0}^{|\theta|}{|\theta_j|^p}\right)^{\frac{1}{p}}\thetaL_p\lambdap12L1L2p2使线NLLR(\theta)NLLR(\theta)$)一致。因此,最小化两个的和,理论上,与找到匹配训练数据和解决方案的一般性的平衡。遵循奥卡姆剃刀原则,最小化可以让我们找到匹配训练数据的最简单的方法。
注意一个事实是简单的方案不一定意味着好的泛化能力。但是一般在神经网络的背景里,正规化可以提高泛化能力,尤其在小的集合上。下面的代码显示怎样在Python里计算带权值L1和L2的代价函数。

# 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 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

如果先于容忍跑完所有的批次的训练数据,则只是简单的跳到数据集的开头,然后重复。
注意:

  • validation_frequency总是小于patience。在跑出容忍的次数之前,代码至少应该两次检测模型的性能。这是我们使用validation_frequency = min( value, patience/2.)公式的理由。
  • 在决定是否增加容忍次数时,算法可以使用一个统计意义上的测试而不是简单的比较,这能提高性能。

测试

所有循环结束之后,最好的参数指得是在验证集上的最好性能的模型。如果我们对另外的模型重复这样的过程,或者甚至另一个随机的初始化,应该使用相同划分的训练、验证和测试集,得到其它更好的模型。如果我们不得不选择哪个是最好的模型或者最好的初始化,可以比较每个模型最好的验证性能。如果我们最终选择一个我们认为最好的模型(在验证集上的),可以报告模型的测试集的性能。这个我们期望的性能可以认为是未见的样本的性能。

摘要

这就是优化的部分。提早结束的技术要求划分样本为三个集合(训练集、验证集和测试集)。训练集用于批量梯度下降算法近似估计目标函数。在训练时,可以定期获取在验证集的性能以查看模型的性能(或者依靠经验估计)。当看到一个在验证集性能好的模型,可以保存它,当长时间看到一个好的模型,可以停止搜索,返回在测试集上的估计最好的参数。

Theano/Python窍门

加载和保存模型

当实验时,使用梯度下降可能会花费几个小时(有时几天)寻找好的参数。一旦你找到它们,可以保存那些权值。在搜索过程中,你也可能想要保存当前最好的估计。

pickle保存共享变量中的numpy的n维数组

保存或者归档模型的参数的最好的方法是使用pickle或者deepcopy保存n维数组。比如,如果你的参数在共享变量w、v、u中,则保存的命令应该看起来像这样:

>>> import cPickle
>>> save_file = open('path', 'wb')  # this will overwrite current contents
>>> cPickle.dump(w.get_value(borrow=True), save_file, -1)  # the -1 is for HIGHEST_PROTOCOL
>>> cPickle.dump(v.get_value(borrow=True), save_file, -1)  # .. and it triggers much more efficient
>>> cPickle.dump(u.get_value(borrow=True), save_file, -1)  # .. storage than numpy's default
>>> save_file.close()

然后,可以像这样子加载数据:

>>> save_file = open('path')
>>> w.set_value(cPickle.load(save_file), borrow=True)
>>> v.set_value(cPickle.load(save_file), borrow=True)
>>> u.set_value(cPickle.load(save_file), borrow=True)

这个技术有点繁琐,但是可靠并且正确的。你能加载之前保存的数据,并不用折腾就可以在matplotlib中显示。

不要使用pickle长期存储训练和测试函数

Theano兼容Python的deepcopy和pickle机理,但是不应该使用pickle保存一个Theano函数。如果你更新了Theano目录,其中一个改变了,则你可能不能un-pickle你的模型了。Theano仍然在积极的开发中,内部的APIs可能会被修改。所以为了安全起见,比如一个临时文件,或者在分布式工作中的额外的机器的拷贝。

显示中间结果

可视化是一个理解模型怎么样的或者判断训练的算法的好坏的有用的工具。你可能会插入matplotlib画图指令,或者PIL图像显示指令到你的模型训练脚本中。之后你能观察到一些在预先显示的图像中感兴趣的地方,检查与图像不对的地方。你可能想要你已经保存原始模型。

如果有足够的硬盘空间,训练脚本应该保存中间模型,可视化的脚本应该处理这些保存的模型。

你应该有一个模型保存函数,对吧?仅仅再次使用并保存中间模型。你可能想要知道的库:Python图像库(PIL),matplotlib

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多