6.3 卷积网络的代码
好了,现在来看看我们的卷积网络代码,network3.py
。整体看来,程序结构类似于 network2.py
,尽管细节有差异,因为我们使用了 Theano。首先我们来看 FullyConnectedLayer
类,这类似于我们之前讨论的那些神经网络层。下面是代码
class FullyConnectedLayer(object):
def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):
self.n_in = n_in
self.n_out = n_out
self.activation_fn = activation_fn
self.p_dropout = p_dropout
# Initialize weights and biases
self.w = theano.shared(
np.asarray(
np.random.normal(
loc=0.0, scale=np.sqrt(1.0/n_out), size=(n_in, n_out)),
dtype=theano.config.floatX),
name='w', borrow=True)
self.b = theano.shared(
np.asarray(np.random.normal(loc=0.0, scale=1.0, size=(n_out,)),
dtype=theano.config.floatX),
name='b', borrow=True)
self.params = [self.w, self.b]
def set_inpt(self, inpt, inpt_dropout, mini_batch_size):
self.inpt = inpt.reshape((mini_batch_size, self.n_in))
self.output = self.activation_fn(
(1-self.p_dropout)*T.dot(self.inpt, self.w) + self.b)
self.y_out = T.argmax(self.output, axis=1)
self.inpt_dropout = dropout_layer(
inpt_dropout.reshape((mini_batch_size, self.n_in)), self.p_dropout)
self.output_dropout = self.activation_fn(
T.dot(self.inpt_dropout, self.w) + self.b)
def accuracy(self, y):
"Return the accuracy for the mini-batch."
return T.mean(T.eq(y, self.y_out))
__init__
方法中的大部分都是可以自解释的,这里再给出一些解释。我们根据正态分布随机初始化了权重和偏差。代码中对应这个操作的一行看起来可能很吓人,但其实只在进行载入权重和偏差到 Theano 中所谓的共享变量中。这样可以确保这些变量可在 GPU中进行处理。对此不做过深的解释。如果感兴趣,可以查看 Theano 的文档。而这种初始化的方式也是专门为 sigmoid 激活函数设计的(参见\hyperref[sec:weightinitialization]{这里})。理想的情况是,我们初始化权重和偏差时会根据不同的激活函数(如 tanh 和 Rectified Linear Function)进行调整。这个在下面的问题中会进行讨论。初始方法 `_init以
self.params = [self.W, self.b]结束。这样将该层所有需要学习的参数都归在一起。后面,
Network.SGD方法会使用
params` 属性来确定网络实例中什么变量可以学习。
set_inpt
方法用来设置该层的输入,并计算相应的输出。我使用 inpt
而非 input
因为在python 中 input
是一个内置函数。如果将两者混淆,必然会导致不可预测的行为,对出现的问题也难以定位。注意我们实际上用两种方式设置输入的:self.input
和 self.inpt_dropout
。因为训练时我们可能要使用 dropout。如果使用 dropout,就需要设置对应丢弃的概率 self.p_dropout
。这就是在 set_inpt
方法的倒数第二行 dropout_layer
做的事。所以 self.inpt_dropout
和 self.output_dropout
在训练过程中使用,而 self.inpt
和 self.output
用作其他任务,比如衡量验证集和测试集模型的准确度。
ConvPoolLayer
和 SoftmaxLayer
类定义和FullyConnectedLayer
定义差不多。所以我这儿不会给出代码。如果你感兴趣,可以参考本节后面的 network3.py
的代码。
尽管这样,我们还是指出一些重要的微弱的细节差别。明显一点的是,在 ConvPoolLayer
和 SoftmaxLayer
中,我们采用了相应的合适的计算输出激活值方式。幸运的是,Theano 提供了内置的操作让我们计算卷积、max-pooling和 softmax 函数。
不大明显的,在我们引入\hyperref[sec:softmax]{softmax layer} 时,我们没有讨论如何初始化权重和偏差。其他地方我们已经讨论过对 sigmoid 层,我们应当使用合适参数的正态分布来初始化权重。但是这个启发式的论断是针对 sigmoid 神经元的(做一些调整可以用于 tanh 神经元上)。但是,并没有特殊的原因说这个论断可以用在 softmax 层上。所以没有一个先验的理由应用这样的初始化。与其使用之前的方法初始化,我这里会将所有权值和偏差设置为 。这是一个 ad hoc 的过程,但在实践使用过程中效果倒是很不错。
好了,我们已经看过了所有关于层的类。那么 Network 类是怎样的呢?让我们看看 __init__
方法:
class Network(object):
def __init__(self, layers, mini_batch_size):
"""Takes a list of `layers`, describing the network architecture, and
a value for the `mini_batch_size` to be used during training
by stochastic gradient descent.
"""
self.layers = layers
self.mini_batch_size = mini_batch_size
self.params = [param for layer in self.layers for param in layer.params]
self.x = T.matrix("x")
self.y = T.ivector("y")
init_layer = self.layers[0]
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
for j in xrange(1, len(self.layers)):
prev_layer, layer = self.layers[j-1], self.layers[j]
layer.set_inpt(
prev_layer.output, prev_layer.output_dropout, self.mini_batch_size)
self.output = self.layers[-1].output
self.output_dropout = self.layers[-1].output_dropout
这段代码大部分是可以自解释的。self.params = [param for layer in ...]
此行代码对每层的参数捆绑到一个列表中。Network.SGD
方法会使用 self.params
来确定 Network
中哪些变量需要学习。而 self.x = T.matrix("x")
和 self.y = T.ivector("y")
则定义了 Theano 符号变量 x 和 y。这些会用来表示输入和网络得到的输出。
这里不是 Theano 的教程,所以不会深度讨论这些变量指代什么东西。但是粗略的想法就是这些代表了数学变量,而非显式的值。我们可以对这些变量做通常需要的操作:加减乘除,作用函数等等。实际上,Theano 提供了很多对符号变量进行操作方法,如卷积、最大值混合等等。但是最重要的是能够进行快速符号微分运算,使用反向传播算法一种通用的形式。这对于应用随机梯度下降在若干种网络结构的变体上特别有效。特别低,接下来几行代码定义了网络的符号输出。我们通过下面这行
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)
设置初始层的输入。
请注意输入是以每次一个 mini-batch 的方式进行的,这就是小批量数据大小为何要指定的原因。还需要注意的是,我们将输入 self.x
传了两次:这是因为我们我们可能会以两种方式(有dropout和无dropout)使用网络。for
循环将符号变量 self.x
通过 Network
的层进行前向传播。这样我们可以定义最终的输出 output
和 output_dropout
属性,这些都是 Network
符号式输出。
现在我们理解了 Network
是如何初始化了,让我们看看它如何使用 SGD
方法进行训练的。代码看起来很长,但是它的结构实际上相当简单。代码后面也有一些注解。
def SGD(self, training_data, epochs, mini_batch_size, eta,
validation_data, test_data, lmbda=0.0):
"""Train the network using mini-batch stochastic gradient descent."""
training_x, training_y = training_data
validation_x, validation_y = validation_data
test_x, test_y = test_data
# compute number of minibatches for training, validation and testing
num_training_batches = size(training_data)/mini_batch_size
num_validation_batches = size(validation_data)/mini_batch_size
num_test_batches = size(test_data)/mini_batch_size
# define the (regularized) cost function, symbolic gradients, and updates
l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
cost = self.layers[-1].cost(self)+\
0.5*lmbda*l2_norm_squared/num_training_batches
grads = T.grad(cost, self.params)
updates = [(param, param-eta*grad)
for param, grad in zip(self.params, grads)]
# define functions to train a mini-batch, and to compute the
# accuracy in validation and test mini-batches.
i = T.lscalar() # mini-batch index
train_mb = theano.function(
[i], cost, updates=updates,
givens={
self.x:
training_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
training_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
validate_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
validation_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
validation_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
test_mb_accuracy = theano.function(
[i], self.layers[-1].accuracy(self.y),
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size],
self.y:
test_y[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
self.test_mb_predictions = theano.function(
[i], self.layers[-1].y_out,
givens={
self.x:
test_x[i*self.mini_batch_size: (i+1)*self.mini_batch_size]
})
# Do the actual training
best_validation_accuracy = 0.0
for epoch in xrange(epochs):
for minibatch_index in xrange(num_training_batches):
iteration = num_training_batches*epoch+minibatch_index
if iteration
print("Training mini-batch number {0}".format(iteration))
cost_ij = train_mb(minibatch_index)
if (iteration+1)
validation_accuracy = np.mean(
[validate_mb_accuracy(j) for j in xrange(num_validation_batches)])
print("Epoch {0}: validation accuracy {1:.2
epoch, validation_accuracy))
if validation_accuracy >= best_validation_accuracy:
print("This is the best validation accuracy to date.")
best_validation_accuracy = validation_accuracy
best_iteration = iteration
if test_data:
test_accuracy = np.mean(
[test_mb_accuracy(j) for j in xrange(num_test_batches)])
print('The corresponding test accuracy is {0:.2
test_accuracy))
print("Finished training network.")
print("Best validation accuracy of {0:.2
best_validation_accuracy, best_iteration))
print("Corresponding test accuracy of {0:.2
前面几行很直接,将数据集分解成 x 和 y 两部分,并计算在每个数据集中小批量数据的数量。接下来的几行更加有意思,这也体现了 Theano 有趣的特性。那么我们就摘录详解一下:
# define the (regularized) cost function, symbolic gradients, and updates
l2_norm_squared = sum([(layer.w**2).sum() for layer in self.layers])
cost = self.layers[-1].cost(self)+\
0.5*lmbda*l2_norm_squared/num_training_batches
grads = T.grad(cost, self.params)
updates = [(param, param-eta*grad)
for param, grad in zip(self.params, grads)]
这几行,我们符号化地给出了规范化的对数似然代价函数,在梯度函数中计算了对应的导数,以及对应参数的更新方式。Theano 让我们通过这短短几行就能够获得这些效果。唯一隐藏的是计算 cost
包含一个对输出层 cost
方法的调用;该代码在 network3.py
中其他地方。但是,总之代码很短而且简单。有了所有这些定义好的东西,下面就是定义 train_mini_batch
函数,该 Theano 符号函数在给定 minibatch 索引的情况下使用 updates
来更新 Network
的参数。类似地,validate_mb_accuracy
和 test_mb_accuracy
计算在任意给定的 minibatch 的验证集和测试集合上 Network
的准确度。通过对这些函数进行平均,我们可以计算整个验证集和测试数据集上的准确度。
SGD
方法剩下的就是可以自解释的了 —— 我们对次数进行迭代,重复使用训练数据的小批量数据来训练网络,计算验证集和测试集上的准确度。
好了,我们已经理解了 network3.py
代码中大多数的重要部分。让我们看看整个程序,你不需过分仔细地读下这些代码,但是应该享受粗看的过程,并随时深入研究那些激发出你好奇地代码段。理解代码的最好的方法就是通过修改代码,增加额外的特征或者重新组织那些你认为能够更加简洁地完成的代码。代码后面,我们给出了一些对初学者的建议。这儿是代码\footnote{在 GPU 上使用 Theano 可能会有点难度。特别地,很容易在从GPU 中
拉取数据时出现错误,这可能会让运行变得相当慢。我已经试着避免出现这样的情况,但
是也不能肯定在代码扩充后出现一些问题。对于你们遇到的问题或者给出的意见我洗耳恭
听([email protected])。}:
\lstinputlisting[language=Python]{code_samples/src/network3.py}
问题
- 目前,
SGD
方法要求用户手动选择用于训练迭代期的数量。前文中,我们讨论了一种自动选择训练次数的方法,也就是\hyperref[early_stopping]{提前停止}。修改network3.py
以实现提前停止。 - 增加一个
Network
方法来返回在任意数据集上的准确度。 - 修改
SGD
方法来允许学习率 可以是训练次数的函数。提示:在思考这个问题一段时间后,你可能会在\href{https://groups.google.com/forum/\#!topic/theano-users/NQ9NYLvleGc{这个链接}找到有用的信息。} - 在本章前面我曾经描述过一种通过应用微小的旋转、扭曲和变化来扩展训练数据的方法。改变
network3.py
来加入这些技术。注意:除非你有充分多的内存,否则显式地产生整个扩展数据集是不大现实的。所以要考虑一些变通的方法。 - 在
network3.py
中增加load
和save
方法。 - 当前的代码缺点就是只有很少的用来诊断的工具。你能想出一些诊断方法告诉我们网络过匹配到什么程度么?加上这些方法。
- 我们已经对修正线性单元及 S 型和 tanh 函数神经元使用了同样的初始方法。正如\hyperref[sec:weight_initialization]{这里所说},这种初始化方法只是适用于sigmoid 函数。假设我们使用一个全部使用 ReLU 的网络。试说明以常数 倍调整网络的权重最终只会对输出有常数 倍的影响。如果最后一层是 softmax,则会发生什么样的变化?对 ReLU 使用 sigmoid 函数的初始化方法会怎么样?有没有更好的初始化方法?注意:这是一个开放的问题,并不是说有一个简单的自包含答案。还有,思考这个问题本身能够帮助你更好地理解包含 ReLU 的神经网络。
- 我们对于不稳定梯度问题的\hyperref[sec:what_is_causing_the_vanishing_gradient_problem_unstable_gradients_in_deep_neural_nets]{分析}实际上是针对 sigmoid 神经元的。如果是 ReLU,那分析又会有什么差异?你能够想出一种使得网络不太会受到不稳定梯度问题影响的好方法么?注意:“好”这个词实际上就是一个研究性问题。实际上有很多容易想到的修改方法。但我现在还没有研究足够深入,能告诉你们什么是真正的好技术。