[关闭]
@cleardusk 2015-11-27T19:39:08.000000Z 字数 4649 阅读 1626

DL 在线书籍源码阅读(二)

GjzCVCode


IO 部分

仍然是 IO 部分,上一篇针对的是只有一个隐藏层的网络,这本在线书后两章介绍了 DNN,以及其中一种 model: ConvNet,本书中的 ConvNet 的 Python 实现与之前的有一个不同之处:Thenao 的使用。

1.1 Theano

Theano 可以借助 GPU 加速,在读取数据时多了一个将数据 copy 到 GPU 的过程,就是下面的 shared() 函数,这里有一些细节:

  1. return shared_x, T.cast(shared_y, "int32")

以上这行代码的返回值是 tuple 形式,T.cast 应该是将 shared_y,就是表示 digit的 10 维向量转换为 int32 datatype,但 GPU 运算的时候还是使用 float32 格式。Theano 调用 GPU,只需调用 Theano.shared() 函数,很方便,当然,底层是通过 cuda 来运作的(具体的机制不清楚,只能靠大概的猜测,Theano 调用 cuda 库的函数)。自己的 mint 系统装不了 cuda,另一个 ubuntu 系统上还没花时间配置 theano 等库。目前只用 CPU 跑。

  1. def load_data_shared(filename="../data/mnist.pkl.gz"):
  2. f = gzip.open(filename, 'rb')
  3. training_data, validation_data, test_data = cPickle.load(f)
  4. f.close()
  5. def shared(data):
  6. """Place the data into shared variables. This allows Theano to copy
  7. the data to the GPU, if one is available.
  8. """
  9. shared_x = theano.shared(
  10. np.asarray(data[0], dtype=theano.config.floatX), borrow=True)
  11. shared_y = theano.shared(
  12. np.asarray(data[1], dtype=theano.config.floatX), borrow=True)
  13. return shared_x, T.cast(shared_y, "int32")
  14. return [shared(training_data), shared(validation_data), shared(test_data)]

仔细看了下代码,要开启 GPU mode,配置如下

  1. theano.config.device = 'gpu'

1.2 小结

IO 部分应该就是这么多内容,关于 Theano 写的很少,因为还没花时间研究。接下来会写网络具体实现的部分。

NN 部分

关于单个隐藏层的 NN (不知道用什么 term 来描述)的实现,我先花一点时间笼统地总结一下这个,然后总结下 CNN ,CNN 是以 NN 为基础的,如果我把这些全忘了(十年内应该不会全忘),我可能会先从基础部分的 NN 看起。

2.1 Earlier NN

我想带着问题,对源码进行通读是个不错的选择。假设已经对 NN 有了一个大概的概念,比如知道 [input|hideen|output]layer, neuron(unit), weights, biases, activation function 这些词的意思。

2.1.1 如何初始化一个网络?

Python 是 OO (面向对象)的,初始化网络的部分当然要放在 contruction (构造函数)部分中。

网络的规模

首先是网络的规模,由一个 list 中的变量表示,784×100×10 就用

  1. sizes = [784, 100, 10]

表示,网络的规模是可调的,隐藏层的 unit
个数一般多多益善,但会增长训练的时间。

初始值

  1. self.biases = [np.random.randn(y, 1) for y in self.sizes[1:]]
  2. self.weights = [np.random.randn(y, x)/np.sqrt(x)
  3. for x, y in zip(self.sizes[:-1], self.sizes[1:])]

接下来是 weights, biases 的初始值问题,上面这段代码用的是高斯分布 N(0,1),但这种方法可能会导致网络过早 saturate,训练不动(即 weights、biases 没啥变化),就我知道的有两种改进方法,一是用 N(0,1)/nin,这是从 σ(z)z=jwjxj+b 两个式子搞出来的,大概原因是大量 z 分布在远大于 1 和远小于 1 的位置,使得 hidden layer 的输出 σ(z) 接近 01,网络训练不动,改进后,分布在远大于 1 和远小于 1 的位置变少了,二是用 dropout 的方法,就是在训练的时候,每次只选择,随机地去掉一半的 hidden layer 中的 unit 数目,其实 dropout 这个 trick 应该属于 train 过程中的,不过既然想到了就按这个顺序写吧。

2.1.2 网络的 feedforward 过程?

网络的前向计算过程容易理解,但是在 Python 处理的时候,要特别注意整个符号体系以及下标。为了简化代码,乘积运算用的都是矩阵或向量的形式。代码很短。

  1. def feedforward(self, a):
  2. """Return the output of the network if ``a`` is input."""
  3. for b, w in zip(self.biases, self.weights):
  4. a = sigmoid(np.dot(w, a)+b)
  5. return a

调参似乎不涉及到 feedfoward 过程,在这个过程能下功夫的就是矩阵运算的优化了,Python 的 numpy 有一些优化,不过有专门处理 Linear Algebra 的库,比如 BLAS 等库,我曾在 C++ 中简单地用过 Armadillo 库。不过在训练过程中,运算的优化起到明显效果的,针对的应该不是 feedfoward,而是 backpropagate 过程。当然,拿一个训练好的模型来用的话,矩阵运算优化对 feedfoward 就很重要了,因为直接决定了效率。

2.1.3 网络是如何训练的?

网络的训练其实是 weights 和 biases 的调整过程,BP 算法提供梯度计算的方法,Stochastic Gradient Descent 算法(有变种,如 Hessian,Momemtum-based,我还没研究)其实是对求目标函数最小值算法的复杂度的一个优化(有点绕)。具体说一下,C 是最终的优化目标函数(这里的 C 用的是 quadratic cost function,还有 cross-entropy,其余的我还没了解,但推导过程应该类似),x 是 input,即 784 维的向量。

C=12nxy(x)aL(x)2,(1)

但是 BP 算法是计算这个目标函数的梯度。

Cx=12yaL2(2)

理论上,计算 Cwljk, Cblj 是要 (2) 式所有的 x 的和,这个规模是训练数据集的规模,很大,比如 50000 张 images,就是 50000 的规模,现在一般计算机 cpu 频率在 108 ~ 109 上下,粗略估计了下,一个 784 × 100 × 10 规模的简单网络,只训练一个 epoch,大概在小时左右的级别。SGD 算法就是来拯救这种局面的,SGD 可以使用小规模的 sample 来近似目标函数 C 的梯度,这个小规模就是 mini_batch_size,然后规模变成了 mini_batch_size × mini_batch_size,mini_batch_size 若为 10,计算规模(局部,不是整体)就下降到 100,训练速度一下子提升了 500 倍,如果训练数据规模更大的话,SGD(以及变种) 的效果就更明显。我测试了一下,一个 epoch,mini_batch_size 设置为 10,784 × 100 × 10 的网络,Python 版本(cpu mode,只用了 numpy,theano 等库没用)需要 30s。

具体的训练过程,可以参考这个:BP 算法以及 BP+SGD 算法描述。这是核心的算法。关于 BP、SGD 的算法的推导证明的在此忽略(SGD 我还具体去看)。

至于代码部分,先定位到 SGD() 函数,我删除了部分代码,只留下核心算法。

  1. def SGD(self, training_data, epochs, mini_batch_size, etalmbda = 0.0):
  2. n = len(training_data)
  3. for j in xrange(epochs):
  4. random.shuffle(training_data)
  5. mini_batches = [
  6. training_data[k:k+mini_batch_size]
  7. for k in xrange(0, n, mini_batch_size)]
  8. for mini_batch in mini_batches:
  9. self.update_mini_batch(
  10. mini_batch, eta, lmbda, len(training_data))
  11. print "Epoch %s training complete" % j

random.shuffle 随机排列 list 数据,即 training_data(是一个list),mini_batches list 存储了 mini_batch_size × mini_batch_size 张 images,,即 inputs。每一个 mini_batch 作为 update_mini_batch() 函数的参数,update_mini_batch() 会调用 backprop() 函数计算每一个 input x 时,Cx 的梯度,然后用这个梯度来更新 weights 和 biases 的值。其它的看看 doc string 就能明白。update_mini_batch() 和 backprop() 都有几十行,就不贴了。这是完整代码

训练过程中涉及到的参数比较多,可调的参数,可能涉及的 trick,加上之前的,把我能想到的都列在这。

其中,数 regularization 和 dropout 比较黑科技,expand training
data 比较有趣。另外,书中有相当长的篇幅是介绍如何调参的,hyper parameter。


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注