我们到目前为止,我们讨论了神经网络中应用最广的BP算法,虽然想深入浅出的讲解出来,但是回头来看,还是非常复杂,晦涩难懂。这说明神经网络理论上是非常高深的,想要搞明白,是需要一定的IQ和努力的,这点上无法取巧。
但是我们不要被数学的复杂性的蒙蔽,其实多层前馈网络,从原理上来说还是很简单的。在深度学习理论出现之前,神经网络尤其是BP网络,一般都只有三层,既输入层、隐藏层和输出层,可以证明,通过引入隐藏层,BP网络可以拟合任意函数。因为受限于输入信号,输入层神经元数是固定的,而根据要处理的问题域,输出层神经元的数量也是固定的,所以传统BP网络主要是确定隐藏层神经元数量,由于没有合适的理论指导,这个决策更多的取决于研究人员的经验。但是从其本质上来说,如果我们隐藏层的数目小于输入样本的维数,我们就希望在隐藏层做自动聚类,使问题得以减化。而如果隐藏层节点数大于输入样本维数,那么我们是致力于增加样本的维数,只样本在高维空间中可分,例如如果两个点的x,y坐标相同,只有z坐标不同,那么其在二维空间中,这两个点是不可分的。但是如果将这两个点放到三维空间中,我们就很容易通过z坐标来对它们进行区分了。
说了这么多的废话,我们回到用Theano来实现多层前馈网络这个话题上来。我们在这里仅讨论只有一个隐藏层的情况。因为输入层只是简单的将输入信号传递给隐藏层,我们重点需要定义和实现隐藏层和输出层。我们隐藏层将采用非线性神经元,激活函数为双曲正切函数,因为该函数比Sigmal函数收敛性上更佳。隐藏层之上,我们的输出层将采用逻辑回归算法,激活函数为softmax函数。虽然我们采用的技术与上一篇博文中理论推导中所用的不同,但基本原理是相同的。
我们首先来定义隐藏层:
class HiddenLayer(object): def __init__(self, rng, input, n_in, n_out, W=None, b=None, activation=T.tanh): self.input = input if W is None: W_values = numpy.asarray( rng.uniform( low=-numpy.sqrt(6. / (n_in + n_out)), high=numpy.sqrt(6. / (n_in + n_out)), size=(n_in, n_out) ), dtype=theano.config.floatX ) if activation == theano.tensor.nnet.sigmoid: W_values *= 4 W = theano.shared(value=W_values, name='W', borrow=True) if b is None: b_values = numpy.zeros((n_out,), dtype=theano.config.floatX) b = theano.shared(value=b_values, name='b', borrow=True) self.W = W self.b = b lin_output = T.dot(input, self.W) + self.b self.output = ( lin_output if activation is None else activation(lin_output) ) # parameters of the model self.params = [self.W, self.b]我们首先定义权值矩阵,由于只有一个隐藏层,所以权值向量的维数是:输入信号维数 * 输出结果维数,然后检测激活函数类型,如果是Sigmod函数,则将权值数值乘以一个4,之后将权值矩阵定义为一个共享变量。然后初始化bias向量。最后定义本层输出值,如果未定义激活函数,则直接取输入信号与权值矩阵的点积加上bias,如果有则将该值代入激活函数,求出最终的输出值。
有了隐藏层的定义,接下来我们需要将隐藏层与逻辑回归的输出层进行联连接,为此我们定义了MLP类,代码如下所示:
class MLP(object): def __init__(self, rng, input, n_in, n_hidden, n_out): self.hiddenLayer = HiddenLayer( rng=rng, input=input, n_in=n_in, n_out=n_hidden, activation=T.tanh ) self.logRegressionLayer = LogisticRegression( input=self.hiddenLayer.output, n_in=n_hidden, n_out=n_out ) self.L1 = ( abs(self.hiddenLayer.W).sum() + abs(self.logRegressionLayer.W).sum() ) self.L2_sqr = ( (self.hiddenLayer.W ** 2).sum() + (self.logRegressionLayer.W ** 2).sum() ) self.negative_log_likelihood = ( self.logRegressionLayer.negative_log_likelihood ) self.errors = self.logRegressionLayer.errors self.params = self.hiddenLayer.params + self.logRegressionLayer.params self.input = input我们首先初始化隐藏层,然后初始化作为输出层的逻辑回归层,为了避免过度优化(即过份拟合于当前数据,使得对未知数据的性能下降),我们定义了l1和l2,将整个网络的最大似然函数和误差函数,定义为逻辑回归类中所定义的最大似然函数和误差函数。并将输入信号直接作为网络的输入。这样我们就完整地定义了一个三层前馈网络。
定义好网络结构之后,我们需要装入训练样本,对网络进行训练。在这里我们还使用MNIST的手写数字识别库,因此我们需要首先定义一个MNIST库的装入类,代码如下所示:
class MnistLoader(object): def load_data(self, dataset): data_dir, data_file = os.path.split(dataset) if data_dir == "" and not os.path.isfile(dataset): new_path = os.path.join( os.path.split(__file__)[0], "..", "data", dataset ) if os.path.isfile(new_path) or data_file == 'mnist.pkl.gz': dataset = new_path if (not os.path.isfile(dataset)) and data_file == 'mnist.pkl.gz': from six.moves import urllib origin = ( 'http://www.iro.umontreal.ca/~lisa/deep/data/mnist/mnist.pkl.gz' ) print('Downloading data from %s' % origin) urllib.request.urlretrieve(origin, dataset) print('... loading data') # Load the dataset with gzip.open(dataset, 'rb') as f: try: train_set, valid_set, test_set = pickle.load(f, encoding='latin1') except: train_set, valid_set, test_set = pickle.load(f) def shared_dataset(data_xy, borrow=True): data_x, data_y = data_xy shared_x = theano.shared(numpy.asarray(data_x, dtype=theano.config.floatX), borrow=borrow) shared_y = theano.shared(numpy.asarray(data_y, dtype=theano.config.floatX), borrow=borrow) 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) rval = [(train_set_x, train_set_y), (valid_set_x, valid_set_y), (test_set_x, test_set_y)] return rval上面的代码功能比较简单,就是从MNIST文件中,读出训练样本集,并分为训练、测试、验证样本集。
下面就是具体的网络训练,我们定义mlp_mnist_engine类,来完成训练过程。首先是构造函数:
class MlpMnistEngine(object): def __init__(self): print("create MlpMnistEngine") self.learning_rate=0.01 self.L1_reg=0.00 self.L2_reg=0.0001 self.n_epochs=1000 self.dataset = 'mnist.pkl.gz' self.batch_size=600 # 20 self.n_hidden=500在这里面对参数进行初始设置,其中L1_reg和L2_reg是为避免模型陷入过拟合而设置的调整参数。
下面就是建立训练、测试、验证模型,代码如下所示:
def build_model(self): loader = MnistLoader() datasets = loader.load_data(self.dataset) train_set_x, train_set_y = datasets[0] valid_set_x, valid_set_y = datasets[1] test_set_x, test_set_y = datasets[2] n_train_batches = train_set_x.get_value(borrow=True).shape[0] // self.batch_size n_valid_batches = valid_set_x.get_value(borrow=True).shape[0] // self.batch_size n_test_batches = test_set_x.get_value(borrow=True).shape[0] // self.batch_size print('... building the model') index = T.lscalar() # index to a [mini]batch x = T.matrix('x') # the data is presented as rasterized images y = T.ivector('y') # the labels are presented as 1D vector of rng = numpy.random.RandomState(1234) # 全新运行时 classifier = MLP( rng=rng, input=x, n_in=28 * 28, n_hidden=self.n_hidden, n_out=10 ) cost = ( classifier.negative_log_likelihood(y) + self.L1_reg * classifier.L1 + self.L2_reg * classifier.L2_sqr ) test_model = theano.function( inputs=[index], outputs=classifier.errors(y), givens={ x: test_set_x[index * self.batch_size:(index + 1) * self.batch_size], y: test_set_y[index * self.batch_size:(index + 1) * self.batch_size] } ) validate_model = theano.function( inputs=[index], outputs=classifier.errors(y), givens={ x: valid_set_x[index * self.batch_size:(index + 1) * self.batch_size], y: valid_set_y[index * self.batch_size:(index + 1) * self.batch_size] } ) gparams = [T.grad(cost, param) for param in classifier.params] updates = [ (param, param - self.learning_rate * gparam) for param, gparam in zip(classifier.params, gparams) ] train_model = theano.function( inputs=[index], outputs=cost, updates=updates, givens={ x: train_set_x[index * self.batch_size: (index + 1) * self.batch_size], y: train_set_y[index * self.batch_size: (index + 1) * self.batch_size] } ) return (classifier, n_train_batches, n_valid_batches, n_test_batches, train_model, validate_model, test_model)在这里,我们首先载入MNIST手写数字识别的数据文件,并将其划分为训练、测试、验证样本集。然后建立刚才定义的MLP实例为分类器,定义代价函数为输出层负的最大似然函数加上我们为防止过拟合定义的修正量。接着定义测试和验证模型,其输入为网络输入信号,输出为网络输出层的,并且每次训练一个批量的样本。最后是定义培训模型,首先我们通过Theano内置函数将代价函数求网络参数的偏导数,然定定义网络参数的更新规则,最后是培训模型的建立,其与测试和验证模型的区别主要有两种:其一是输出量为上文所定义的代价函数,其二是定义了网络参数的更新规则。
定义完模型之后,就是网络的训练过程,训练过程与逻辑回归的训练过程非常相似,代码如下所示:
def train(self): classifier, n_train_batches, n_valid_batches, n_test_batches, train_model, validate_model, test_model = self.build_model() print('... training') patience = 5000 # 10000 # look as this many examples regardless patience_increase = 2 # wait this much longer when a new best is improvement_threshold = 0.995 # a relative improvement of this much is validation_frequency = min(n_train_batches, patience // 2) best_validation_loss = numpy.inf best_iter = 0 test_score = 0. start_time = timeit.default_timer() epoch = 0 done_looping = False while (epoch < self.n_epochs) and (not done_looping): epoch = epoch + 1 for minibatch_index in range(n_train_batches): minibatch_avg_cost = train_model(minibatch_index) iter = (epoch - 1) * n_train_batches + minibatch_index if (iter + 1) % validation_frequency == 0: validation_losses = [validate_model(i) for i in range(n_valid_batches)] this_validation_loss = numpy.mean(validation_losses) print( 'epoch %i, minibatch %i/%i, validation error %f %%' % ( epoch, minibatch_index + 1, n_train_batches, this_validation_loss * 100. ) ) if this_validation_loss < best_validation_loss: if ( this_validation_loss < best_validation_loss * improvement_threshold ): patience = max(patience, iter * patience_increase) best_validation_loss = this_validation_loss best_iter = iter test_losses = [test_model(i) for i in range(n_test_batches)] test_score = numpy.mean(test_losses) with open('best_model.pkl', 'wb') as f: pickle.dump(classifier, f) print((' epoch %i, minibatch %i/%i, test error of ' 'best model %f %%') % (epoch, minibatch_index + 1, n_train_batches, test_score * 100.)) if patience <= iter: done_looping = True break end_time = timeit.default_timer() print(('Optimization complete. Best validation score of %f %% ' 'obtained at iteration %i, with test performance %f %%') % (best_validation_loss * 100., best_iter + 1, test_score * 100.)) print(('The code for file ' + os.path.split(__file__)[1] + ' ran for %.2fm' % ((end_time - start_time) / 60.)), file=sys.stderr)我们编写一个入口类,建立并培训这个MLP网络,代码如下所示:
from mlp_mnist_engine import MlpMnistEngine if __name__ == '__main__': print("Train MLP") engine = MlpMnistEngine() engine.train()大概运行十几个小时之后,系统可以达到错误率1.X%,所以请耐心等待系统运行结束。
在网络训练完成之后,我们就要将网络用于解决实际问题,我们需要在MlpMnistEngine类中添加运行模式的代码:
def run(self): print("run mlp") classifier = pickle.load(open('best_model.pkl', 'rb')) predict_model = theano.function( inputs=[classifier.input], outputs=classifier.logRegressionLayer.y_pred ) dataset='mnist.pkl.gz' loader = MnistLoader() datasets = loader.load_data(dataset) test_set_x, test_set_y = datasets[2] test_set_x = test_set_x.get_value() predicted_values = predict_model(test_set_x[:10]) print("Predicted values for the first 10 examples in test set:") print(predicted_values)在这里,我是从MNIST数据集中取出几条样本,传给训练好的网络进行预测。实际上也可以按照指定的数据格式,提供样本,来测试网络。
调用上面方法的代码如下所示:
from mlp_mnist_engine import MlpMnistEngine if __name__ == '__main__': print("Run MLP") engine = MlpMnistEngine() engine.run()至此,一个简单的三层前馈网络就构建完成了。由上面的实现过程我们可以看出,虽然BP算法非常复杂,但是在Theano下,只需要通过将隐藏层和逻辑回归层进行联连,指定一下更新规则,然后就可以Theano自动完成前向生成输出信息,自动计算误差,并根据梯度下降法更新网络参数,而我们则不需要关注于这些细节。
既然Theano已经帮我们实现了人工神经网络中最难的算法实现部分,那么我们还有什么可做的吗?这个问题是问得很好,其实,对于Theano库的使用者而言,重点不是理解各种神经网络的算法原理,而是对各种网络结构参数、权值、Bias、调节参数、提前结束条件等的定义,同时,还有更重要的一点,就是对实际问题进行建模,使其适合于神经网络处理。因为并不是所有问题都像图像识别这样,很容易确定输入信号形式,有些问题,比如下棋问题,怎么将其变为一组数字向量来表示,就是一个很复杂的问题。一般来讲,如果对一个实际问题,可以找到很好的特性集来描述,那么采用流行的神经网络算法,基本都可以得到很好的解决。但是问题的关键是,我们很难找到最佳的描述实际问题的特性,这才是制约传统神经网络发展的瓶颈。对于这个问题,深度学习网络给出了自己的解决方案,即通过无监督学习,自动找出描述实际问题最佳的特性,然后我们再利用监督学习,找到问题的最终解决方案。深度学习由于解决了制约之前神经网络发展的瓶颈问题,所以在近几年得到了长足发展。因此,我们在学习深度学习时,也要抓住其本质,从这一点上来看待深度学习。
其实,到目前为止,我们所讲的逻辑回归和多层前馈网络,都是深度学习崛起前的传统神经网络的概念,从严格的意义上来讲,并不是深度学习的组成部分。所以从下一篇博文开始,我们将真正进入深度学习领域,我们首先研究一下多层卷积神经网络(LeNet 5),这个模型在几年前MNIST的识别竞赛中取得了当年比赛的第一名,我们会实现一个这个网络的简单版本,达到误差率小于1%的结果。