Theano3.2-练习之数据集及目标函数介绍
来自http://deeplearning.net/tutorial/gettingstarted.html#gettingstarted
一、下载
在后续的每个学习算法上,都需要下载对应的文档,如果想要一次全部下好,那么可以复制git上面的这个教程的资料:
MNIST 数据集(mnist.pkl.gz)(现在这个数据集除了教学,好像已经没什么人关注了)
这个MNIST 数据集包含的是手写数字图像,其中有6w张训练样本和1w张测试样本,不过在几乎许多论文和本教程中,都是将这6w张训练样本划分成5w张训练样本和1w张验证集样本,所有的图片都已经中心化而且是固定的大小28×28,其中是灰度图,白色为255,黑色为0.为了方面本教程,是需要在python下使用的,可以下载 here.也就是已经划分好了三个list:训练集、验证集和测试集。每一个list都是由图像和相应的标签组成的。其中图像是numpy的784(28*28)的一维数组(就是把2维的图像拉成一条向量),标签则是一个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()当使用这个数据集的时候,通常是将它划分成minibatches( Stochastic Gradient Descent)(还有http://blog.csdn.net/shouhuxianjian/article/details/41040245,这是hinton的视频的第6课)。我们建议你可以将这个数据集放入到共享变量(shared variables)中,并通过基于minibatch索引、给定一个固定和已知的batch size来访问它。这样做的好处就是共享变量之后可以用在gpu上,因为当将数据复制到GPU内存上的时候会有较大的开销,如果按照代码的执行(每个minibatch都是独立的)来进行传输数据的时候,如果不是用共享变量的方法,结果反而比只使用CPU的速度还慢。如果使用Theano 共享变量,那么就是让Theano将整个数据在共享变量构造的时候通过一个单一的调用都复制到GPU上。之后,GPU可以通过在这个共享变量上进行切片slice来访问任何minibatch,而不需要从cpu的内存上复制到GPU上,所以避免了很多的数据传输的开销。因为这些数据点和他们的标签通常都是不同的(标签通常是整数,而数据点通常是实数),我们建议使用不同的变量来表示数据和标签。同样我们推荐使用对这三个不同的集合也采用不同的变量来使得代码更具有可读性(会生成6个不同的共享变量)。因为当前的数据是一个变量,而且一个minibatch可以被定为这个变量的一个切片,所以很自然的可以通过指定索引和尺寸来定义一个minibatch。在我们的步骤中,batch size在代码执行过程中 一直是一个常量,所以一个函数实际上需要的只是索引来指定哪个数据点被使用了。下面的代码来表示如何存储数据并且如何访问一个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))#因为GPU只接受float类型 shared_y = theano.shared(numpy.asarray(data_y, dtype=theano.config.floatX))#因为GPU只接受float类型 # 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') #返回的时候强制标签为int类型 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 访问训练集的第三个minibatch data = train_set_x[2 * 500: 3 * 500] label = train_set_y[2 * 500: 3 * 500]在GPU上存储的数据只能是floats类型的,(存储在GPu上时,右边的dtype被赋值为theano.config.floatX).为了绕过这个标签上的问题,通过将其存储为float,然后返回的时候强制为int类型。
note:如果你想要在GPU上运行代码,而你使用的数据集太大而无法放入GPU内存中,这种情况下,你可能会将数据存储到一个共享变量中。然而你可以存储一个足够小的数据块(几个minibatches)放到一个共享变量中,然后使用这个来进行训练,当这个数据块训练完成之后,更新存储的数据块换下一部分。这个方法是为了最小化CPU和GPU之间数据传输的次数的折衷方法。
三、符号
数据集符号
我们将数据集表示成,当需要区别对待的时候,将训练集,验证集和测试集表示成:: , 和。验证集是用来执行模型选取和超参数选择的,而且测试集是用来验证最后的 泛化误差和以无偏的方式来对比不同的算法。该教程基本上处理的是分类问题,这里每个数据集是有关 对的索引集合,使用上标来区分不同的训练集样本: 是第 i 个维度为的训练样本。相似的,是第 i 个指派给输入的标签。这样就可以简单的扩展这些例子,使得能够有其他类型(例如,高斯回归,或者能够预测多个符号的多项式组(也就是多分类))。
数学约定
- : 大写符号用来表示一个矩阵,除非有其他的特指
- : 矩阵的第i 行第j 列的元素
- : 向量, 表示矩阵第i 行
- : 向量, 表示矩阵第j 列
- : 小写符号用来表示一个向量,除非有其他的特指
- : 向量的第i 个元素
符号和函数的列表
- : 输入的维度数.
- : 第i 层隐藏单元的个数.
- , : 和模型 相关的分类函数, 定义成 . 注意到这里通常将下标 丢弃掉.
- L:标签的个数.
- : 由参数定义模型 的的log似然 .
- 在数据集上由参数决定的预测函数 f 的期望损失.
- NLL: 负似然log(negative log-likelihood,NLL)
- : 对于一个给定模型的所有参数集合
python的命名空间
本教程的代码通常使用下面的命名空间:
import theano import theano.tensor as T import numpy
四、在DL上的有监督优化的入门
学习一个分类器
zero-one 损失
本教程中呈现的模型大多是用来做分类的。训练一个分类器的目标是为了最小化在不可见样本上的误差(0-1损失)的数量。如果 是预测函数,那么损失函数可以写成:
这里是 表示的是训练集合(在训练的时候)或者 (为了避免验证集或者测试误差的有偏估计)。 是指示函数,可以被定义成:
在这个教程中,f 被定义成:
在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))
负log似然损失
因为0-1损失不能微分,对于大型模型(成千上百万的参数)的优化来说,代价是非常高昂的(计算量)。所以我们在训练集给定所有的标签的基础上让我们的分类器的log似然最大化:
正确类别的似然和正确预测的数量是不相同的,不过从一个随机初始化的分类器的观点上看,它们相当接近。不过提醒下,0-1损失和似然是不同的目标;你需要看见它们是在验证集上是正相关的,不过有时候却是负相关的。因为我们通常说要最小化一个损失函数,所以学习其实就是为了最小化这个负log似然函数,定义为:
我们分类器的NLL是可微分的,所以可以用来代替0-1损失,而且我们在基于训练数据上使用这个函数的梯度作为 一个分类器的dl 有监督学习信号(其实就是用梯度来训练分类器的意思,我直译的而已)。这可以通过使用下面的代码来计算得到:
# 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
随机梯度下降法是和用普通梯度下降法一样的原则来work的,不过可以通过每次一点样本来计算梯度从而更快速的进行处理,所以不需要一次放入整个训练样本了。对应的伪代码如下:
# 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在dl 上我们推荐使用在随机梯度上的进一步变体,叫做“minibatches”。minibatch sgd的工作规则是和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 size 的选择上的权衡考虑。方差的减小和SIMD指令的使用在当从1增加到2的时候通常是很有帮助的,不过这个很小的提升却会很快的回归虚无。使用更大的,时间会消耗在减少梯度估计器的方差减少上,本来这些时间是应该更好的用在额外的梯度步长上的。一个最优的是基于模型、数据集、和硬件考虑的,同时可以在任何地方从1上升到甚至好几百。在这个教程中,我们将它设置成20,不过这个选择通常是任意的。
note:如果你训练的时候使用的是固定数量的epochs,那么这个minibatch size就变得很重要了,因为它控制着你的参数的更新次数。使用batch size 为1 的10次epochs来训练相同的模型得到的结果完全不同于训练batch size 为20的而且也是10个epochs的结果。记得,在不同的batch sizes之间转换的时候,记得按照使用过的这个batch size 来调整所有的其他参数。
上面所有的演示该算法的伪代码块,在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
正则化
除了优化,在机器学习中还有更重要的部分。当我们从数据中训练我们的模型的时候,我们是将它准备用在新样本上的,而不是那些我们已经见过的样本。上面的MSGD的训练循环如果没有考虑到这一点,也许就会过拟合训练样本。一个对应过你的方法就是正则化。这里有好几种正则化的方法,这里会介绍L1/L2正则化和早期停止。
L1/L2 正则化
L2和L2正则化涉及到在损失函数上增加额外的项,用来惩罚某一个参数组合。形式上,如果我们的损失函数是:
那么正则化损失就该是:
或者,在我们的情况中:
这里
这是 的 范数。是一个超参数,用来控制正则化参数的相关重要性。通常 p的值是1和2,所以命名为L1/L2,如果p=2,那么这个正则化叫做“权重衰减”。原则上说,对损失函数增加一个正则化项将会使得在NN中网络更加的平滑(通过惩罚值较大的参数,这些值较大的参数会降低网络模型的非线性程度,所以需要惩罚)。更直观的说,这两项(NLL和)对应于很好的对数据进行建模(NLL)和有着“简单”或“平滑”的解决方法。因此,最小化这两个项的和,从理论上来说,就是为了在拟合训练数据和解决方法的“泛化”之间找到正确的平衡点。为了遵循Occam的razor原则,这个最小化应该让我们找到最简单的解决方法(通过我们简单的标准来测量的)来拟合训练数据。注意到这样一个事实,一个所谓的“简单”的解决方法不是意味着能够很好的泛化。经验上来说,通常是在NN的背景下这样的正则化的添加有助于泛化,特别是对于小的数据集来说。下面的代码块用来表示当包含由来权重化的L1正则化项和由来权重化的L2正则化项的时候如何在python中计算损失的:
# 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
早期停止
用早期停止来解决过拟合是通过在验证集合上监测模型的执行结果来完成的。验证集就是我们在梯度下降的时候未使用的样本集,不过这同样也不是测试集的一部分。验证集样本是被认为作为未来测试集样本的代表的。我们可以在训练的时候使用时因为它们不是测试集的一部分。如果模型的效果在验证集上已经停止了提升,或者甚至在后面的优化上还有下降,那么这里需要做的就是停止使用更多的优化。选择什么时候停止是一个主观判断而且是存在启发式的,不过这些教程将会在基于会具有几何增长的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之前跑完了所有的训练数据,那么我们只需要回到训练数据的开始部分,然后再来一次。
note:validation_frequency应该总是要小于patience的。在跑完patience之前代码需要检查至少两次。这是因为我们使用的公式validation_frequency = min( value,patience/2)。
note:当决定什么时候需要增大patience的时候,算法可以通过使用统计测试的方法来明显的提升,而不是简单的使用对比。
测试
在现有的循环之后,best_params变量表示在验证集上best-performing的模型。如果我们给另一个模型类别重复这个过程,或者甚至使用另一个随机初始化,我们应该也要对数据使用相同的train/valid/test划分,然后得到其他best-performing模型。如果我们不得不需要选择最好的模型类别或者最好的初始化,我们需要对每个模型进行对比best_validation_loss。当我们选择我们认为的最好的模型(基于验证集)的时候,我们会将这个模型用在测试集上,并报告结果。
回顾
这是为了优化部分准备的。早期停止的技术需要我们将样本集合划分成三个不同的集合(训练集、验证集、测试集)。训练集用来作为目标函数的可微分的近似函数的minibatch sgd上。当我们执行梯度下降的时候,我们定期的使用验证集来观察我们在真正的目标函数上模型的结果(或者至少从经验上分析)。当我们在验证集上看到一个好的模型的时候,我们需要保存下来,当我们发现从看到一个好模型已经过去了很久,那么我们就放弃我们的研究,回头去找到那些最好的参数,然后在测试集上进行评估。
五、theano/python的提示
装载和保存模型
当你做实验的时候,会花费好几个小时(或者几天)来做梯度下降然后找到最好的参数。一旦你找到了它们,你将会需要保存这些权重。随着研究的开展,你也许同样会想要保存你当前最好的结果。
从共享变量中pickle这个numpy ndarrays
最好的保存/存档你的模型的参数的方法是使用pickle或者深度复制ndarray对象。例如,如果你的参数都放在共享变量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的深度复制和pickle机制的,不过你没必要一定pickle一个theano函数。如果你更新你的theano文件夹或者说其内部有一些改变,那么你也许没法unpickle你的模型。theano仍然是一个动态的开发项目,内部的APIs可能会改变。所以从安全角度上来说,不要为了长期的存储而pickle你的整个训练或者测试函数(也就是如果save几天或者几个礼拜估计还ok,就是怕几个月之后更新了theano,之前的保存的就没法读取了)。pickle机智是为了短期存储而准备的,例如一个临时文件,或者在一个分布式工作中从一个机器上复制到另一个机器上。
了解更多可以看看 serialization in Theano, 或者 Python的 pickling.
显示中间的结果
可视化对于理解你的模型或者训练算法在干什么是很有帮助的工具。你可能需要视图键入matplotlib画图命令,或者PIL 图像呈现命令到你的模型训练脚本中。然而,之后你可能想要从这些预呈现的图形中显示一些你感兴趣的或者想要看看从图形中得到的是否清晰,那么你需要保存原始的模型。
如果你有足够的磁盘空间,你的训练脚本应该保存中间模型并且一个可视化脚本应该用来处理这些保存的模型。
你已经有了一个模型保存函数了吗?可以再次使用它来保存这些中间模型
你可能会想要了解的库:python图像库 (PIL), matplotlib.
参考资料:
[1] 官网:http://deeplearning.net/tutorial/gettingstarted.html#gettingstarted
[2] theano学习指南1:http://www.cnblogs.com/xueliangliu/archive/2013/04/03/2997437.html