第六章 深度学习(中下)
卷積網絡的代碼
好了,現在來看看我們的卷積網絡代碼,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_inself.n_out = n_outself.activation_fn = activation_fnself.p_dropout = p_dropout# Initialize weights and biasesself.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 documentation。而這種初始化的方式也是專門為 sigmoid 激活函數設計的(參見這里)。理想的情況是,我們初始化權重和偏差時會根據不同的激活函數(如 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 函數。
不大明顯的,在我們引入softmax layer 時,我們沒有討論如何初始化權重和偏差。其他地方我們已經討論過對 sigmoid 層,我們應當使用合適參數的正態分布來初始化權重。但是這個啟發式的論斷是針對 sigmoid 神經元的(做一些調整可以用于 tanh 神經元上)。但是,并沒有特殊的原因說這個論斷可以用在 softmax 層上。所以沒有一個先驗的理由應用這樣的初始化。與其使用之前的方法初始化,我這里會將所有權值和偏差設置為 0。這是一個 ad hoc 的過程,但在實踐使用過程中效果倒是很不錯。
好了,我們已經看過了所有關于層的類。那么 Network 類是怎樣的呢?讓我們看看 __init__ 方法:
class Network(object):def __init__(self, layers, mini_batch_size):"""Takes a list of `layers`, describing the network architecture, anda value for the `mini_batch_size` to be used during trainingby stochastic gradient descent."""self.layers = layersself.mini_batch_size = mini_batch_sizeself.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].outputself.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 提供了很多對符號變量進行操作方法,如卷積、max-pooling等等。但是最重要的是能夠進行快速符號微分運算,使用 BP 算法一種通用的形式。這對于應用隨機梯度下降在若干種網絡結構的變體上特別有效。特別低,接下來幾行代碼定義了網絡的符號輸出。我們通過下面這行
init_layer.set_inpt(self.x, self.x, self.mini_batch_size)設置初始層的輸入。
請注意輸入是以每次一個 mini-batch 的方式進行的,這就是 mini-batch size 為何要指定的原因。還需要注意的是,我們將輸入 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_datavalidation_x, validation_y = validation_datatest_x, test_y = test_data# compute number of minibatches for training, validation and testingnum_training_batches = size(training_data)/mini_batch_sizenum_validation_batches = size(validation_data)/mini_batch_sizenum_test_batches = size(test_data)/mini_batch_size# define the (regularized) cost function, symbolic gradients, and updatesl2_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_batchesgrads = 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 indextrain_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 trainingbest_validation_accuracy = 0.0for epoch in xrange(epochs):for minibatch_index in xrange(num_training_batches):iteration = num_training_batches*epoch+minibatch_indexif iteration % 1000 == 0:print("Training mini-batch number {0}".format(iteration))cost_ij = train_mb(minibatch_index)if (iteration+1) % num_training_batches == 0:validation_accuracy = np.mean([validate_mb_accuracy(j) for j in xrange(num_validation_batches)])print("Epoch {0}: validation accuracy {1:.2%}".format(epoch, validation_accuracy))if validation_accuracy >= best_validation_accuracy:print("This is the best validation accuracy to date.")best_validation_accuracy = validation_accuracybest_iteration = iterationif 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%}'.format(test_accuracy))print("Finished training network.")print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(best_validation_accuracy, best_iteration))print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))前面幾行很直接,將數據集分解成 x 和 y 兩部分,并計算在每個數據集中 mini-batch 的數量。接下來的幾行更加有意思,這也體現了 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*lambda*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)]這幾行,我們符號化地給出了規范化的 log-likelihood 代價函數,在梯度函數中計算了對應的導數,以及對應參數的更新方式。Theano 讓我們通過這短短幾行就能夠獲得這些效果。唯一隱藏的是計算 cost 包含一個對輸出層 cost 方法的調用;該代碼在 network3.py 中其他地方。但是,總之代碼很短而且簡單。有了所有這些定義好的東西,下面就是定義 train_mini_batch 函數,該 Theano 符號函數在給定 minibatch 索引的情況下使用 updates 來更新 Network 的參數。類似地,validate_mb_accuracy 和 test_mb_accuracy 計算在任意給定的 minibatch 的驗證集和測試集合上 Network 的準確度。通過對這些函數進行平均,我們可以計算整個驗證集和測試數據集上的準確度。
SGD 方法剩下的就是可以自解釋的了——我們對次數進行迭代,重復使用 訓練數據的 minibatch 來訓練網絡,計算驗證集和測試集上的準確度。
好了,我們已經理解了 network3.py 代碼中大多數的重要部分。讓我們看看整個程序,你不需過分仔細地讀下這些代碼,但是應該享受粗看的過程,并隨時深入研究那些激發出你好奇地代碼段。理解代碼的最好的方法就是通過修改代碼,增加額外的特征或者重新組織那些你認為能夠更加簡潔地完成的代碼。代碼后面,我們給出了一些對初學者的建議。這兒是代碼:
在 GPU 上使用 Theano 可能會有點難度。特別地,很容在從 GPU 中拉取數據時出現錯誤,這可能會讓運行變得相當慢。我已經試著避免出現這樣的情況,但是也不能肯定在代碼擴充后出現一些問題。對于你們遇到的問題或者給出的意見我洗耳恭聽(mn@michaelnielsen.org)。
"""network3.py ~~~~~~~~~~~~~~ A Theano-based program for training and running simple neural networks. Supports several layer types (fully connected, convolutional, max pooling, softmax), and activation functions (sigmoid, tanh, and rectified linear units, with more easily added). When run on a CPU, this program is much faster than network.py and network2.py. However, unlike network.py and network2.py it can also be run on a GPU, which makes it faster still. Because the code is based on Theano, the code is different in many ways from network.py and network2.py. However, where possible I have tried to maintain consistency with the earlier programs. In particular, the API is similar to network2.py. Note that I have focused on making the code simple, easily readable, and easily modifiable. It is not optimized, and omits many desirable features. This program incorporates ideas from the Theano documentation on convolutional neural nets (notably, http://deeplearning.net/tutorial/lenet.html ), from Misha Denil's implementation of dropout (https://github.com/mdenil/dropout ), and from Chris Olah (http://colah.github.io ). """#### Libraries # Standard library import cPickle import gzip# Third-party libraries import numpy as np import theano import theano.tensor as T from theano.tensor.nnet import conv from theano.tensor.nnet import softmax from theano.tensor import shared_randomstreams from theano.tensor.signal import downsample# Activation functions for neurons def linear(z): return z def ReLU(z): return T.maximum(0.0, z) from theano.tensor.nnet import sigmoid from theano.tensor import tanh#### Constants GPU = True if GPU:print "Trying to run under a GPU. If this is not desired, then modify "+\"network3.py\nto set the GPU flag to False."try: theano.config.device = 'gpu'except: pass # it's already settheano.config.floatX = 'float32' else:print "Running with a CPU. If this is not desired, then the modify "+\"network3.py to set\nthe GPU flag to True."#### Load the MNIST data def load_data_shared(filename="../data/mnist.pkl.gz"):f = gzip.open(filename, 'rb')training_data, validation_data, test_data = cPickle.load(f)f.close()def shared(data):"""Place the data into shared variables. This allows Theano to copythe data to the GPU, if one is available."""shared_x = theano.shared(np.asarray(data[0], dtype=theano.config.floatX), borrow=True)shared_y = theano.shared(np.asarray(data[1], dtype=theano.config.floatX), borrow=True)return shared_x, T.cast(shared_y, "int32")return [shared(training_data), shared(validation_data), shared(test_data)]#### Main class used to construct and train networks class Network(object):def __init__(self, layers, mini_batch_size):"""Takes a list of `layers`, describing the network architecture, anda value for the `mini_batch_size` to be used during trainingby stochastic gradient descent."""self.layers = layersself.mini_batch_size = mini_batch_sizeself.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].outputself.output_dropout = self.layers[-1].output_dropoutdef 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_datavalidation_x, validation_y = validation_datatest_x, test_y = test_data# compute number of minibatches for training, validation and testingnum_training_batches = size(training_data)/mini_batch_sizenum_validation_batches = size(validation_data)/mini_batch_sizenum_test_batches = size(test_data)/mini_batch_size# define the (regularized) cost function, symbolic gradients, and updatesl2_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_batchesgrads = 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 indextrain_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 trainingbest_validation_accuracy = 0.0for epoch in xrange(epochs):for minibatch_index in xrange(num_training_batches):iteration = num_training_batches*epoch+minibatch_indexif iteration % 1000 == 0:print("Training mini-batch number {0}".format(iteration))cost_ij = train_mb(minibatch_index)if (iteration+1) % num_training_batches == 0:validation_accuracy = np.mean([validate_mb_accuracy(j) for j in xrange(num_validation_batches)])print("Epoch {0}: validation accuracy {1:.2%}".format(epoch, validation_accuracy))if validation_accuracy >= best_validation_accuracy:print("This is the best validation accuracy to date.")best_validation_accuracy = validation_accuracybest_iteration = iterationif 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%}'.format(test_accuracy))print("Finished training network.")print("Best validation accuracy of {0:.2%} obtained at iteration {1}".format(best_validation_accuracy, best_iteration))print("Corresponding test accuracy of {0:.2%}".format(test_accuracy))#### Define layer typesclass ConvPoolLayer(object):"""Used to create a combination of a convolutional and a max-poolinglayer. A more sophisticated implementation would separate thetwo, but for our purposes we'll always use them together, and itsimplifies the code, so it makes sense to combine them."""def __init__(self, filter_shape, image_shape, poolsize=(2, 2),activation_fn=sigmoid):"""`filter_shape` is a tuple of length 4, whose entries are the numberof filters, the number of input feature maps, the filter height, and thefilter width.`image_shape` is a tuple of length 4, whose entries are themini-batch size, the number of input feature maps, the imageheight, and the image width.`poolsize` is a tuple of length 2, whose entries are the y andx pooling sizes."""self.filter_shape = filter_shapeself.image_shape = image_shapeself.poolsize = poolsizeself.activation_fn=activation_fn# initialize weights and biasesn_out = (filter_shape[0]*np.prod(filter_shape[2:])/np.prod(poolsize))self.w = theano.shared(np.asarray(np.random.normal(loc=0, scale=np.sqrt(1.0/n_out), size=filter_shape),dtype=theano.config.floatX),borrow=True)self.b = theano.shared(np.asarray(np.random.normal(loc=0, scale=1.0, size=(filter_shape[0],)),dtype=theano.config.floatX),borrow=True)self.params = [self.w, self.b]def set_inpt(self, inpt, inpt_dropout, mini_batch_size):self.inpt = inpt.reshape(self.image_shape)conv_out = conv.conv2d(input=self.inpt, filters=self.w, filter_shape=self.filter_shape,image_shape=self.image_shape)pooled_out = downsample.max_pool_2d(input=conv_out, ds=self.poolsize, ignore_border=True)self.output = self.activation_fn(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))self.output_dropout = self.output # no dropout in the convolutional layersclass FullyConnectedLayer(object):def __init__(self, n_in, n_out, activation_fn=sigmoid, p_dropout=0.0):self.n_in = n_inself.n_out = n_outself.activation_fn = activation_fnself.p_dropout = p_dropout# Initialize weights and biasesself.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))class SoftmaxLayer(object):def __init__(self, n_in, n_out, p_dropout=0.0):self.n_in = n_inself.n_out = n_outself.p_dropout = p_dropout# Initialize weights and biasesself.w = theano.shared(np.zeros((n_in, n_out), dtype=theano.config.floatX),name='w', borrow=True)self.b = theano.shared(np.zeros((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 = softmax((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 = softmax(T.dot(self.inpt_dropout, self.w) + self.b)def cost(self, net):"Return the log-likelihood cost."return -T.mean(T.log(self.output_dropout)[T.arange(net.y.shape[0]), net.y])def accuracy(self, y):"Return the accuracy for the mini-batch."return T.mean(T.eq(y, self.y_out))#### Miscellanea def size(data):"Return the size of the dataset `data`."return data[0].get_value(borrow=True).shape[0]def dropout_layer(layer, p_dropout):srng = shared_randomstreams.RandomStreams(np.random.RandomState(0).randint(999999))mask = srng.binomial(n=1, p=1-p_dropout, size=layer.shape)return layer*T.cast(mask, theano.config.floatX)問題
-
目前,SGD 方法需要用戶手動確定訓練的次數(epoch)。早先在本書中,我們討論了一種自動選擇訓練次數的方法,也就是early stopping。修改 network3.py 以實現 Early stopping。
-
增加一個 Network 方法來返回在任意數據集上的準確度。
-
修改 SGD 方法來允許學習率 $$\eta$$ 可以是訓練次數的函數。提示:在思考這個問題一段時間后,你可能會在this link 找到有用的信息。
-
在本章前面我曾經描述過一種通過應用微小的旋轉、扭曲和變化來擴展訓練數據的方法。改變 network3.py 來加入這些技術。注意:除非你有充分多的內存,否則顯式地產生整個擴展數據集是不大現實的。所以要考慮一些變通的方法。
-
在 network3.py 中增加 load 和 save 方法。
-
當前的代碼缺點就是只有很少的用來診斷的工具。你能想出一些診斷方法告訴我們網絡過匹配到什么程度么?加上這些方法。
-
我們已經對rectified linear unit 及 sigmoid 和 tanh 函數神經元使用了同樣的初始方法。正如這里所說,這種初始化方法只是適用于 sigmoid 函數。假設我們使用一個全部使用 RLU 的網絡。試說明以常數 $$c$$ 倍調整網絡的權重最終只會對輸出有常數 $$c$$ 倍的影響。如果最后一層是 softmax,則會發生什么樣的變化?對 RLU 使用 sigmoid 函數的初始化方法會怎么樣?有沒有更好的初始化方法?注意:這是一個開放的問題,并不是說有一個簡單的自包含答案。還有,思考這個問題本身能夠幫助你更好地理解包含 RLU 的神經網絡。
-
我們對于不穩定梯度問題的分析實際上是針對 sigmoid 神經元的。如果是 RLU,那分析又會有什么差異?你能夠想出一種使得網絡不太會受到不穩定梯度問題影響的好方法么?注意:好實際上就是一個研究性問題。實際上有很多容易想到的修改方法。但我現在還沒有研究足夠深入,能告訴你們什么是真正的好技術。
圖像識別領域中的近期進展
在 1998 年,MNIST 數據集被提出來,那時候需要花費數周能夠獲得一個最優的模型,和我們現在使用 GPU 在少于 1 小時內訓練的模型性能差很多。所以,MNIST 已經不是一個能夠推動技術邊界前進的問題了;不過,現在的訓練速度讓 MNIST 能夠成為教學和學習的樣例。同時,研究重心也已經發生了轉變,現代的研究工作包含更具挑戰性的圖像識別問題。在本節,我們簡短介紹一些近期使用神經網絡進行圖像識別上的研究進展。
本節內容和本書其他大部分都不一樣。整本書,我都專注在那些可能會成為持久性的方法上——諸如 BP、規范化、和卷積網絡。我已經盡量避免提及那些在我寫書時很熱門但長期價值未知的研究內容了。在科學領域,這樣太過熱門容易消逝的研究太多了,最終對科學發展的價值卻是很微小的。所以,可能會有人懷疑:“好吧,在圖像識別中近期的發展就是這種情況么?兩到三年后,事情將發生變化。所以,肯定這些結果僅僅是一些想在研究前沿陣地領先的專家的專屬興趣而已?為何又費力來討論這個呢?”
這種懷疑是正確的,近期研究論文中一些改良的細節最終會失去其自身的重要性。過去幾年里,我們已經看到了使用深度學習解決特別困難的圖像識別任務上巨大進步。假想一個科學史學者在 2100 年寫起計算機視覺。他們肯定會將 2011 到 2015(可能再加上幾年)這幾年作為使用深度卷積網絡獲得重大突破的時段。但這并不意味著深度卷積網絡,還有dropout RLU等等,在 2100 年仍在使用。但這確實告訴我們在思想的歷史上,現在,正發生著重要的轉變。這有點像原子的發現,抗生素的發明:在歷史的尺度上的發明和發現。所以,盡管我們不會深入這些細節,但仍值得從目前正在發生的研究成果中獲得一些令人興奮的研究發現。
The 2012 LRMD paper:讓我們從一篇來自 Stanford 和 Google 的研究者的論文開始。后面將這篇論文簡記為 LRMD,前四位作者的姓的首字母命名。LRMD 使用神經網絡對 ImageNet 的圖片進行分類,這是一個具有非常挑戰性的圖像識別問題。2011 年 ImageNet 數據包含了 16,000,000 的全色圖像,有 20,000 個類別。圖像從開放的網絡上爬去,由 Amazon Mechanical Turk 服務的工人分類。下面是幾幅 ImageNet 的圖像:
Paste_Image.png
上面這些分別屬于 圓線刨,棕色爛根須,加熱的牛奶,及 通常的蚯蚓。如果你想挑戰一下,你可以訪問hand tools,里面包含了一系列的區分的任務,比如區分 圓線刨、短刨、倒角刨以及其他十幾種類型的刨子和其他的類別。我不知道讀者你是怎么樣一個人,但是我不能將所有這些工具類型都確定地區分開。這顯然是比 MNIST 任務更具挑戰性的任務。LRMD 網絡獲得了不錯的 15.8% 的準確度。這看起很不給力,但是在先前最優的 9.3% 準確度上卻是一個大的突破。這個飛躍告訴人們,神經網絡可能會成為一個對非常困難的圖像識別任務的強大武器。
The 2012 KSH paper:在 2012 年,出現了一篇 LRMD 后續研究 Krizhevsky, Sutskever and Hinton (KSH)。KSH 使用一個受限 ImageNet 的子集數據訓練和測試了一個深度卷積神經網絡。這個數據集是機器學習競賽常用的一個數據集——ImageNet Large-Scale Visual Recognition Challenge(ILSVRC)。使用一個競賽數據集可以方便比較神經網絡和其他方法之間的差異。ILSVRC-2012 訓練集包含 120,000 幅 ImageNet 的圖像,共有 1,000 類。驗證集和測試集分別包含 50,000 和 150,000 幅,也都是同樣的 1,000 類。
ILSVRC 競賽中一個難點是許多圖像中包含多個對象。假設一個圖像展示出一只拉布拉多犬追逐一只足球。所謂“正確的”分類可能是拉布拉多犬。但是算法將圖像歸類為足球就應該被懲罰么?由于這樣的模糊性,我們做出下面設定:如果實際的ImageNet分類是出于算法給出的最可能的 5 類,那么算法最終被認為是正確的。KSH 深度卷積網絡達到了 84.7% 的準確度,比第二名的 73.8% 高出很多。使用更加嚴格度量,KSH 網絡業達到了 63.3% 的準確度。
我們這里會簡要說明一下 KSH 網絡,因為這是后續很多工作的源頭。而且它也和我們之前給出的卷積網絡相關,只是更加復雜精細。KSH 使用深度卷積網絡,在兩個 GPU 上訓練。使用兩個 GPU 因為 GPU 的型號使然(NVIDIA GeForce GTX 580 沒有足夠大的內存來存放整個網絡)所以用這樣的方式進行內存的分解。
KSH 網絡有 7 個隱藏層。前 5 個隱藏層是卷積層(可能會包含 max-pooling),而后兩個隱藏層則是全連接層。輸出層則是 1,000 的 softmax,對應于 1,000 種分類。下面給出了網絡的架構圖,來自 KSH 的論文。我們會給出詳細的解釋。注意很多層被分解為 2 個部分,對應于 2 個 GPU。
Paste_Image.png
輸出層包含 3 224 224 神經元,表示一幅 224 224 的圖像的RGB 值。回想一下,ImageNet 包含不同分辨率的圖像。這里也會有問題,因為神經網絡輸入層通常是固定的大小。KSH 通過將每幅圖進行的重設定,使得短的邊長度為 256。然后在重設后的圖像上裁剪出 256 256 的區域。最終 KSH 從 256 256 的圖像中抽取出隨機的 224 224 的子圖(和水平反射)。他們使用隨機的方式,是為了擴展訓練數據,這樣能夠緩解過匹配的情況。在大型網絡中這樣的方法是很有效的。這些 224 * 224 的圖像就成為了網絡的輸入。在大多數情形下,裁剪的圖像仍會包含原圖中主要的對象。
現在看看 KSH 的隱藏層,第一隱藏層是一個卷積層,還有 max-pooling。使用了大小為 11 11 的局部感應區,和大小為 4 的步長。總共有 96 個特征映射。特征映射被分成兩份,分別存放在兩塊 GPU 上。max-pooling 在這層和下層都是 3 3 區域進行,由于允許使用重疊的 pooling 區域,pooling 層其實會產生兩個像素值。
Pooling layers in CNNs summarize the outputs of neighboring groups of neurons in the same kernel map. Traditionally, the neighborhoods summarized by adjacent pooling units do not overlap (e.g., [17, 11, 4]). To be more precise, a pooling layer can be thought of as consisting of a grid of pooling units spaced s pixels apart, each summarizing a neighborhood of size z × z centered at the location of the pooling unit. If we set s = z, we obtain traditional local pooling as commonly employed in CNNs. If we set s < z, we obtain overlapping pooling. This is what we use throughout our network, with s = 2 and z = 3. This scheme reduces the top-1 and top-5 error rates by 0.4% and 0.3%, respectively, as compared with the non-overlapping scheme s = 2, z = 2, which produces output of equivalent dimensions. We generally observe during training that models with overlapping pooling find it slightly more difficult to overfit.
第二隱藏層同樣是卷積層,并附加一個 max-pooling 步驟。使用了 5 * 5 的局部感知區,總共有 256 個特征映射,在每個 GPU 上各分了 128 個。注意到,特征映射只使用 48 個輸入信道,而不是前一層整個 96 個輸出。這是因為任何單一的特征映射僅僅使用來自同一個 GPU 的輸入。從這個角度看,這個網絡和此前我們使用的卷積網絡結構還是不同的,盡管本質上仍一致。
第三、四和五隱藏層也是卷積層,但和前兩層不同的是:他們不包含 max-pooling 步。各層參數分別是:(3)384 個特征映射,3 3 的局部感知區,256 個輸入信道;(4)384 個特征映射,其中 3 3 的局部感知區 和 192 個輸入信道;(5)256 個特征映射,3 * 3 的局部感知區,和 192 個輸入信道。注意第三層包含一些 GPU 間的通信(如圖所示)這樣可使得特征映射能夠用上所有 256 個輸入信道。
第六、七隱藏層是全連接層,其中每層有 4,096 個神經元。
輸出層是一個 1,000 個單元的 softmax 層。
KSH 網絡使用了很多的技術。放棄了 sigmoid 或者 tanh 激活函數的使用, KSH 全部采用 RLU,顯著地加速了訓練過程。KSH 網絡用有將近 60,000,000 的參數,所以,就算有大規模的數據訓練,也可能出現過匹配情況。為了克服這個缺點,作者使用了隨機剪裁策略擴展了訓練數據集。另外還通過使用 l2 regularization的變體和 dropuout 來克服過匹配。網絡本身使用 基于momentum 的 mini-batch 隨機梯度下降進行訓練。
這就是 KSH 論文中諸多核心想法的概述。細節我們不去深究,你可以通過仔細閱讀論文獲得。或者你也可以參考 Alexandrite Krizhevsky 的cuda-convnet 及后續版本,這里包含了這些想法的實現。還有基于 Theano 的實現也可以在這兒找到。盡管使用多 GPU 會讓情況變得復雜,但代碼本身還是類似于之前我們寫出來的那些。Caffe 神經網絡框架也有一個 KSH 網絡的實現,看Model Zoo。
The 2014 ILSVRC 競賽:2012年后,研究一直在快速推進。看看 2014年的 ILSVRC 競賽。和 2012 一樣,這次也包括了一個 120, 000 張圖像,1,000 種類別,而最終評價也就是看網絡輸出前五是不是包含正確的分類。勝利的團隊,基于 Google 之前給出的結果,使用了包含 22 層的深度卷積網絡。他們稱此為 GoogLeNet,向 LeNet-5 致敬。GoogLeNet 達到了93.33% 的準確率遠超2013年的 88.3% Clarifai 和 2012 年的KSH 84.7%。
那么 GoogLeNet 93.33% 的準確率又是多好呢?在2014年,一個研究團隊寫了一篇關于 ILSVRC 競賽的綜述文章。其中有個問題是人類在這個競賽中能表現得如何。為了做這件事,他們構建了一個系統讓人類對 ILSVRC 圖像進行分類。其作者之一 Andrej Karpathy 在一篇博文中解釋道,讓人類達到 GoogLeNet 的性能確實很困難:
...the task of labeling images with 5 out of 1000 categories quickly turned out to be extremely challenging, even for some friends in the lab who have been working on ILSVRC and its classes for a while. First we thought we would put it up on [Amazon Mechanical Turk]. Then we thought we could recruit paid undergrads. Then I organized a labeling party of intense labeling effort only among the (expert labelers) in our lab. Then I developed a modified interface that used GoogLeNet predictions to prune the number of categories from 1000 to only about 100. It was still too hard - people kept missing categories and getting up to ranges of 13-15% error rates. In the end I realized that to get anywhere competitively close to GoogLeNet, it was most efficient if I sat down and went through the painfully long training process and the subsequent careful annotation process myself... The labeling happened at a rate of about 1 per minute, but this decreased over time... Some images are easily recognized, while some images (such as those of fine-grained breeds of dogs, birds, or monkeys) can require multiple minutes of concentrated effort. I became very good at identifying breeds of dogs... Based on the sample of images I worked on, the GoogLeNet classification error turned out to be 6.8%... My own error in the end turned out to be 5.1%, approximately 1.7% better.
換言之,一個專家級別的人類,非常艱難地檢查圖像,付出很大的精力才能夠微弱勝過深度神經網絡。實際上,Karpathy 指出第二個人類專家,用小點的圖像樣本訓練后,只能達到 12.0% 的 top-5 錯誤率,明顯弱于 GoogLeNet。大概有一半的錯誤都是專家“難以發現和認定正確的類別究竟是什么”。
這些都是驚奇的結果。根據這項工作,很多團隊也報告 top-5 錯誤率實際上好過 5.1%。有時候,在媒體上被報道成系統有超過人類的視覺。盡管這項發現是很振奮人心的,但是這樣的報道只能算是一種誤解,認為系統在視覺上超過了人類,事實上并非這樣。ILSVRC 競賽問題在很多方面都是受限的——在公開的網絡上獲得圖像并不具備在實際應用中的代表性!而且 top-5 標準也是非常人工設定的。我們在圖像識別,或者更寬泛地說,計算機視覺方面的研究,還有很長的路要走。當然看到近些年的這些進展,還是很鼓舞人心的。
其他研究活動:上面關注于 ImageNet,但是也還有一些其他的使用神經網絡進行圖像識別的工作。我們也介紹一些進展。
一個鼓舞人心的應用上的結果就是 Google 的一個團隊做出來的,他們應用深度卷積網絡在識別 Google 的街景圖像庫中街景數字上。在他們的論文中,對接近 100,000,000 街景數字的自動檢測和自動轉述已經能打到與人類不相上下的程度。系統速度很快:在一個小時內將法國所有的街景數字都轉述了。他們說道:“擁有這種新數據集能夠顯著提高 Google Maps 在一些國家的地理精準度,尤其是那些缺少地理編碼的地區。”他們還做了一個更一般的論斷:“我們堅信這個模型,已經解決了很多應用中字符短序列的 OCR 問題。 ”
我可能已經留下了印象——所有的結果都是令人興奮的正面結果。當然,目前一些有趣的研究工作也報道了一些我們還沒能夠真的理解的根本性的問題。例如,2013 年一篇論文指出,深度網絡可能會受到有效忙點的影響。看看下面的圖示。左側是被網絡正確分類的 ImageNet 圖像。右邊是一幅稍受干擾的圖像(使用中間的噪聲進行干擾)結果就沒有能夠正確分類。作者發現對每幅圖片都存在這樣的“對手”圖像,而非少量的特例。
Paste_Image.png
這是一個令人不安的結果。論文使用了基于同樣的被廣泛研究使用的 KSH 代碼。盡管這樣的神經網絡計算的函數在理論上都是連續的,結果表明在實際應用中,可能會碰到很多非常不連續的函數。更糟糕的是,他們將會以背離我們直覺的方式變得不連續。真是煩心啊。另外,現在對這種不連續性出現的原因還沒有搞清楚:是跟損失函數有關么?或者激活函數?又或是網絡的架構?還是其他?我們一無所知。
現在,這些問題也沒有聽起來這么嚇人。盡管對手圖像會出現,但是在實際場景中也不常見。正如論文指出的那樣:
對手反例的存在看起來和網絡能獲得良好的泛化性能相違背。實際上,如果網絡可以很好地泛化,會受到這些難以區分出來的對手反例怎么樣的影響?解釋是,對手反例集以特別低的概率出現,因此在測試集中幾乎難以發現,然而對手反例又是密集的(有點像有理數那樣),所以會在每個測試樣本附近上出現。
我們對神經網絡的理解還是太少了,這讓人覺得很沮喪,上面的結果僅僅是近期的研究成果。當然了,這樣結果帶來一個主要好處就是,催生出一系列的研究工作。例如,最近一篇文章說明,給定一個訓練好的神經網絡,可以產生對人類來說是白噪聲的圖像,但是網絡能夠將其確信地分類為某一類。這也是我們需要追尋的理解神經網絡和圖像識別應用上的研究方向。
雖然遇到這么多的困難,前途倒還是光明的。我們看到了在眾多相當困難的基準任務上快速的研究進展。同樣還有實際問題的研究進展,例如前面提到的街景數字的識別。但是需要注意的是,僅僅看到在那些基準任務,乃至實際應用的進展,是不夠的。因為還有很多根本性的現象,我們對其了解甚少,就像對手圖像的存在問題。當這樣根本性的問題還亟待發現(或者解決)時,盲目地說我們已經接近最終圖像識別問題的答案就很不合適了。這樣的根本問題當然也會催生出不斷的后續研究。
文/Not_GOD(簡書作者)
原文鏈接:http://www.jianshu.com/p/93a349538627
總結
以上是生活随笔為你收集整理的第六章 深度学习(中下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第六章 深度学习(上中)
- 下一篇: 理解LSTM 网络Understandi