2.4 使用 Python 制作神经网络
现在,我们将开始旅程,使用我们刚刚学习的Python知识制作神经网络。
我们会沿着这个旅程,从简单开始,小步前进,逐步建立Python程序。
从小处入手,然后让程序慢慢长大,这是构建中等复杂度计算机代码的一种明智的方式。
在完成了刚才的工作之后,我们非常自然地从建立神经网络类的框架开始,让我们直接前进吧!
2.4.1 框架代码
让我们勾勒神经网络类的大概样子。我们知道,它应该至少有3个函数:
· 初始化函数——设定输入层节点、隐藏层节点和输出层节点的数量。
· 训练——学习给定训练集样本后,优化权重。
· 查询——给定输入,从输出节点给出答案。
目前,这些函数还未完全定义,也许还需要更多的函数,但是,就目前而言,让我们从这些函数起步。
所编写的代码框架如下所示:
# neural network class definition class neuralNetwork : # initialise the neural network def __init__() : pass # train the neural network def train() : pass # query the neural network def query() : pass
这个开局不错。事实上,这是一个坚实的框架,在这个框架上,你可以充实神经网络工作的详细细节了。
2.4.2 初始化网络
从初始化网络开始。我们需要设置输入层节点、隐藏层节点和输出层节点的数量。这些节点数量定义了神经网络的形状和尺寸。我们不会将这些数量固定,而是当我们使用参数创建一个新的神经网络对象时,才会确定这些数量。通过这种方式,我们保留了选择的余地,轻松地创建不同大小的新神经网络。
在我们刚刚所做出决定中,其底层蕴含着一个重要意义。优秀的程序员、计算机科学家和数学家,只要可能,都尽力创建一般代码,而不是具体的代码。这是一种好习惯,它迫使我们以一种更深更广泛的适用方式思考求解问题。如果能做到这点,就意味着我们的解决方案可以适用于不同的场景。在此处,这意味着,我们将尽可能地为神经网络开发代码,使神经网络保持尽可能多地开放有用的选项,并将假设降低到最低限度,从而使代码很容易根据不同需要得到使用。我们希望同一个类可以创建一个小型的神经网络,也可创建一个大型的神经网络——只需传递所需的大小给参数即可。
同时也请不要忘了学习率。当创建新的神经网络时,这也是待设置的有用参数。让我们看看__init __()函数是什么样子的:
# initialise the neural network def __init__( self , inputnodes, hiddennodes, outputnodes, learningrate ) : # set number of nodes in each input, hidden, output layer self . inodes = inputnodes self . hnodes = hiddennodes self . onodes = outputnodes # learning rate self . lr = learningrate pass
让我们使用所定义的神经网络类,尝试创建每层3个节点、学习率为0.5的小型神经网络对象。
# number of input, hidden and output nodes input_nodes = 3 hidden_nodes = 3 output_nodes = 3 # learning rate is 0.5 learning_rate = 0.5 # create instance of neural network n = neuralNetwork(input_nodes,hidden_nodes,output_nodes, learning_rate)
当然,这段代码创建了一个对象,但是由于我们还没有编码任何函数执行实际的工作,因此这个对象还没有任何用途。没关系,从小处着眼,让代码逐步成长,在通往目标的途中,查找并解决问题,这是一种很好的技术。
为了确保读者没有迷失方向,下图显示了在这个阶段的IPython Notebook,其中包含了神经网络类的定义以及创建对象的代码。
下一步该做些什么呢?我们已经告诉神经网络的对象,希望有多少个输入层节点、隐藏层节点和输出层节点,但是实际上,有关这个方面的任何工作都还没有开始进行呢。
2.4.3 权重——网络的核心
下一步是创建网络的节点和链接。网络中最重要的部分是链接权重,我们使用这些权重来计算前馈信号、反向传播误差,并且在试图改进网络时优化链接权重本身。
前面我们看到,可以使用矩阵简明地表示权重。因此,我们可以创建:
· 在输入层与隐藏层之间的链接权重矩阵Winput_hidden ,大小为hidden_ nodes 乘以 input_nodes。
· 在隐藏层和输出层之间的链接权重矩阵Whidden_output ,大小为hidden_nodes乘以 output_nodes。
请谨记先前的规则,来看看为什么第一个矩阵的大小是input_nodes乘以 hidden_nodes,而不是hidden_node 乘以input_nodes。
请记住,在本书的第1章中,链接权重的初始值应该较小,并且是随机的。下面的numpy函数生成一个数组,数组中元素为0~1的随机值,数组的大小为rows乘以columns。
numpy. random. rand( rows , columns )
所有优秀的程序员都使用互联网搜索引擎来查找关于使用酷炫的Python函数的在线文档,甚至找到了他们不知道的、已存在的非常实用的函数。Google在查找关于编程的信息方面特别有用,例如此处描述的numpy.random.rand()函数。
如果要使用numpy的扩展包,那么需要在代码顶端导入库。
试试这个函数,并自己确认函数能够工作。下图显示了这个函数可以生成3×3的numpy数组。数组中的每个值都是0~1的随机值。
我们其实可以做得更好。我们忽略了权重可以为正数也可以为负数。权重的范围可以在-1.0到+1.0之间。为了简单起见,我们可以将上面数组中的每个值减去0.5,这样,在效果上,数组中的每个值都成为了-0.5到0.5之间的随机值。下图显示这个小诀窍成功了,你可以看到一些小于0的随机值。
我们已经准备好了,在Python程序中创建初始权重矩阵。权重是神经网络的固有部分,与神经网络共存亡,它不是一个临时数据集,不会随着函数调用结束而消失。这意味着,权重必须也是初始化的一部分,并且可以使用其他函数(如训练函数和查询函数)来访问。
下面的代码包括了注释,创建了两个链接权重矩阵,并使用self. inodes、self. hnodes和 self. onodes为两个链接权重矩阵设置了合适的大小。
# link weight matrices, wih and who # weights inside the arrays are w_i_j, where link is from node i to node j in the next layer # w11 w21 # w12 w22 etc self.wih = (numpy.random.rand(self.hnodes, self.inodes) - 0.5) self.who = (numpy.random.rand(self.onodes, self.hnodes) - 0.5)
做得好!我们已经实现了神经网络的心脏——链接权重矩阵!
2.4.4 可选项:较复杂的权重
我们可以选择这种简单却是流行的优化初始权重的方式。
如果阅读本书第1章关于准备数据以及初始化权重的讨论,你将会发现,有些人更喜欢稍微复杂的方法来创建初始随机权重。他们使用正态概率分布采样权重,其中平均值为0,标准方差为节点传入链接数目的开方,即1/ 。
在numpy程序库的帮助下,这是很容易实现的。同样,Google可以帮助我们找到合适的文档。numpy.random.normal()函数可以帮助我们以正态分布的方式采样。由于我们所需要的是随机矩阵,而不是单个数字,因此采用分布中心值、标准方差和numpy数组的大小作为参数。
初始化权重的更新代码看起来如下所示:
self.wih = numpy.random.normal ( 0.0 , pow(self.hnodes, -0.5) , (self.hnodes, self.inodes) ) self.who = numpy.random.normal ( 0.0 , pow(self.onodes, -0.5) , (self.onodes, self.hnodes) )
我们将正态分布的中心设定为0.0。与下一层中节点相关的标准方差的表达式,按照Python的形式,就是pow(self.hnodes, -0.5),简单说来,这个表达式就是表示节点数目的-0.5次方。最后一个参数,就是我们希望的numpy数组的形状大小。
2.4.5 查询网络
接下来,顺理成章,我们现在应该编写训练神经网络的代码,填写当前空的train()函数。但是,还是等一下再写train()函数,让我们先编写简单的query()函数吧。这将会给我们更多的时间来逐步建立信心,获得使用Python和神经网络对象内部权重矩阵的实践经验。
query()函数接受神经网络的输入,返回网络的输出。这个功能非常简单,但是,为了做到这一点,你要记住,我们需要传递来自输入层节点的输入信号,通过隐藏层,最后从输出层输出。你还要记住,当信号馈送至给定的隐藏层节点或输出层节点时,我们使用链接权重调节信号,还应用S激活函数抑制来自这些节点的信号。
如果有很多节点,那么我们就面临着一个很可怕的任务,即为这些节点中的每一个写出Python代码,进行权重调节,加和信号,应用激活函数。节点越多,代码越多。这简直是一场噩梦!好在我们知道了如何使用简单简洁的矩阵形式写出这些指令,因此无需这样做。下式显示了输入层和隐藏层之间的链接权重矩阵如何与输入矩阵相乘,给出隐藏层节点的输入信号。
X hidden = W input_hidden •I
这样做的好处,不仅是更容易书写,而且如Python这样的编程语言也可以理解矩阵,由于这些编程语言认识到所有这些基础计算之间的相似之处,它们可以非常有效率地完成所有实际工作。
你会惊讶于Python代码实际上是多么简单!以下代码应用了numpy代码库,将链接权重矩阵W input_hidden 点乘输入矩阵I。
hidden_inputs = numpy.dot(self.wih, inputs)
计算结束!
这一段简单的Python完成了所有的工作,将所有的输入与所有正确的链接权重组合,生成了组合调节后的信号矩阵,传输给每个隐藏层节点。如果下一次选择使用不同数量的输入层节点或隐藏层节点,不必重写这段代码就可以进行工作。这种力量与优雅就是我们先前将精力投入到理解如何使用矩阵乘法的原因。
为了获得从隐藏层节点处出现的信号,我们简单地将S抑制函数应用到每一个出现的信号上:
O hidden = sigmoid( X hidden )
如果在某个现成的Python库中,已经定义了这个S函数,那么这种操作就变得非常容易。果不其然!SciPy Python库有一组特殊的函数,在这组函数中,S函数称为expit()。不要问我为什么S函数有这样一个愚蠢的名字。可以像导入numpy程序库一样,导入scipy函数库:
# scipy.special for the sigmoid function expit() import scipy.special
由于我们可能希望进行实验和调整,甚至完全改变激活函数,因此当神经网络对象初始化时,在神经网络对象内部只定义一次S函数,这么做是有道理的。在此之后,我们也多次引用了S函数,例如在query()函数中。这样的安排意味着只需要改变S函数的定义一次,而无需找到使用激活函数的每个位置以改变其代码。
在神经网络初始化部分的代码内部,下列代码定义了希望使用的激活函数。
# activation function is the sigmoid function self.activation_function = lambda x: scipy.special.expit(x)
这是什么代码?它看起来不像我们以前见过的任何代码。lambda是什么?这看起来可能有点令人生畏,但是实际上,这并不可怕。这里所做的一切就是创建一个函数,就像创建其他函数一样,不过我们使用了较短的方式将这个函数写出来了。我们不使用通常的def()来定义函数,在此,我们使用神奇的lambda来创建函数,方便又快捷。这个函数接受了x,返回scipy.special.expit(x),这就是S函数。使用lambda创建的函数是没有名字的,经验丰富的程序员喜欢称它们为匿名函数,但是这里分配给它一个名字self.activation_function()。所有这些事情意味着,无论何时任何人需要使用激活函数,那么他所需做的就是调用self.activation_function()。
回到手上的任务,我们要将激活函数应用到组合调整后,准备进入隐藏层节点的信号。其代码与下面的代码一样简单:
# calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs)
也就是说,隐藏层节点的输出信号在名为hidden_outputs的矩阵中。
这让信号到达了中间隐藏层,那么信号如何到达最终输出层呢?其实,在隐藏层节点和最终输出层节点之间没有什么本质的区别,因此过程也是一样的。这意味着代码也非常相似。
看看下面的代码,这些代码总结了我们如何计算隐藏层信号和输出层信号。
# calculate signals into hidden layer hidden_inputs = numpy.dot(self.wih, inputs) # calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs) # calculate signals into final output layer final_inputs = numpy.dot(self.who, hidden_outputs) # calculate the signals emerging from final output layer final_outputs = self.activation_function(final_inputs)
如果删除注释,那么只有四行粗体显示的代码进行了所需的计算,两行为隐藏层,两行为最终输出层。
2.4.6 迄今为止的代码
让我们喘一口气,停下来,检查一下正在构建的神经网络类的代码看起来怎么样了。这看起来应该如下所示。
# neural network class definition class neuralNetwork : # initialise the neural network def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate) : # set number of nodes in each input, hidden, output layer self.inodes = inputnodes self.hnodes = hiddennodes self.onodes = outputnodes # link weight matrices, wih and who # weights inside the arrays are w_i_j, where link is from node i to node j in the next layer # w11 w21 # w12 w22 etc self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes)) self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes)) # learning rate self.lr = learningrate # activation function is the sigmoid function self.activation_function = lambda x: scipy.special.expit(x) pass # train the neural network def train() : pass # query the neural network def query(self, inputs_list) : # convert inputs list to 2d array inputs = numpy.array(inputs_list, ndmin=2).T # calculate signals into hidden layer hidden_inputs = numpy.dot(self.wih, inputs) # calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs) # calculate signals into final output layer final_inputs = numpy.dot(self.who, hidden_outputs) # calculate the signals emerging from final output layer final_outputs = self.activation_function(final_inputs) return final_outputs
这就是类的代码。除此之外,应该在Notebook的第一个单元格中,在代码顶部导入numpy和scipy.special:
import numpy # scipy.special for the sigmoid function expit() import scipy.special
值得一提的是,query()函数只需要input_list。它不需要任何其他输入。
我们已经取得了很好的进展,现在,我们来看看缺少的train()函数。请记住,在训练神经网络的过程中有两个阶段,第一个阶段就是计算输出,如同query()所做的事情,第二个阶段就是反向传播误差,告知如何优化链接权重。
在继续编写train()函数并使用样本训练网络之前,让我们测试目前得到的所有的代码。我们创建一个小网络,使用一些随机输入查询网络,看看网络如何工作。显而易见,这样做不会有任何实际意义,只是为了使用刚刚创建的函数。
下图显示的是所创建的小型网络,其中,在输入层、隐藏层和输出层中,每层有3个节点,并且使用随机选择的输入(1.0,0.5,-1.5)查询网络。
你可以发现,神经网络对象的创建确实需要设置学习率,即使现在还不使用这个学习率。神经网络类的定义有一个初始化函数__init __(),这个函数需要指定一个学习率。如果不设置学习率,Python代码将不会成功运行,并且会抛出错误信息。
输入是一个列表,Python语言将列表写在方括号内。输出也是数字列表。由于还没有训练网络,这个输出没有实际意义,但是我们还是很高兴代码没有出错。
2.4.7 训练网络
现在来解决这个稍微复杂的训练任务。训练任务分为两个部分:
· 第一部分,针对给定的训练样本计算输出。这与我们刚刚在query()函数上所做的没什么区别。
· 第二部分,将计算得到的输出与所需输出对比,使用差值来指导网络权重的更新。
我们已经完成了第一部分,现在,先把这部分写出来:
# train the neural network def train(self, inputs_list, targets_list): # convert inputs list to 2d array inputs = numpy.array(inputs_list, ndmin=2).T targets = numpy.array(targets_list, ndmin=2).T # calculate signals into hidden layer hidden_inputs = numpy.dot(self.wih, inputs) # calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs) # calculate signals into final output layer final_inputs = numpy.dot(self.who, hidden_outputs) # calculate the signals emerging from final output layer final_outputs = self.activation_function(final_inputs) pass
我们使用完全相同的方式从输入层前馈信号到最终输出层,所以这段代码与在query()函数中的几乎完全一样。
由于需要使用包含期望值或目标答案的训练样本来训练网络——因此唯一的区别是,这部分代码中有一个额外的参数,即在函数的名称中定义的targets_list。
def train (self, inputs_list, targets_list)
这段代码还把targets_list变成了numpy数组,就像inputs_list变成numpy数组一样。
targets = numpy.array(targets_list, ndmin=2).T
现在,我们越来越接近神经网络工作的核心,即基于所计算输出与目标输出之间的误差,改进权重。
让我们按照轻柔可控的步骤,进行这种操作。
首先需要计算误差,这个值等于训练样本所提供的预期目标输出值与实际计算得到的输出值之差。这个差也就是将矩阵targets和矩阵final_outputs中每个对应元素相减得到的。Python代码非常简单,这再次优雅地显示了矩阵的力量。
# error is the (target - actual) output_errors = targets - final_outputs
我们可以计算出隐含层节点反向传播的误差。请回忆一下如何根据所连接的权重分割误差,为每个隐藏层节点重组这些误差。对于这个计算过程,我们得到了其矩阵形式:
errors hidden = weights T hidden_output • errors output
由于Python有能力使用numpy进行点乘,因此,这段代码再简单不过了:
# hidden layer error is the output_errors, split by weights, recombined at hidden nodes hidden_errors = numpy.dot(self.who.T, output_errors)
这样,我们就拥有了所需要的一切,可以优化各个层之间的权重了。对于在隐蔽层和最终层之间的权重,我们使用output_errors进行优化。对于输入层和隐藏层之间的权重,我们使用刚才计算得到的hidden_errors进行优化。
先前,我们得到了用于更新节点j与其下一层节点k之间链接权重的矩阵形式的表达式:
α是学习率,sigmoid是先前看到的激活函数。请记住,*乘法是正常的对应元素的乘法,•点乘是矩阵点积。最后一点要注意,来自上一层的输出矩阵被转置了。实际上,这意味着输出矩阵的列变成了行。
在Python代码中,这种转换很容易。我们首先为隐藏层和最终层之间的权重进行编码。
# update the weights for the links between the hidden and output layers self.who += self.lr * numpy.dot( ( output_errors * final_outputs * (1.0 - final_outputs) ) , numpy.transpose(hidden_outputs) )
虽然这是一段很长的代码,但是,彩色标记应该有助于发现代码与数学表达式的联系。学习率是self.lr,它就是很简单地与表达式的其余部分进行相乘。我们使用numpy.dot进行矩阵乘法,红色的元素显示了与来自下一层的误差和S函数相关的部分,绿色的元素显示了与来自前一层转置输出矩阵的相关部分。
简单说来,+ = 意思是将先前变量增加一个量。因此,x + = 3,意思就是x增加3。这是x = x + 3的简短写法。也可以使用这种方法表示其他运算,如x / = 3表示x除以3。
用于输入层和隐藏层之间权重的代码也是类似的。我们只是利用对称性,重写代码,更换名字,这样它们指的就是神经网络的前一层了。下面是这两个权重集的代码,代码着色了,这样你可以发现它们之间的异同点:
# update the weights for the links between the hidden and output layers self. who += self.lr * numpy.dot(( output_errors * final_outputs * (1.0 - final_outputs) ), numpy.transpose(hidden_outputs) ) # update the weights for the links between the input and hidden layers self. wih += self.lr * numpy.dot(( hidden_errors * hidden_outputs * (1.0 - hidden_outputs) ), numpy.transpose(inputs) )
计算结束!
我们先前进行的所有工作、海量的计算、努力得到的矩阵方法、通过梯度下降法最小化网络误差……所有的这些都变成了以上简短而简洁的代码,这真是难以置信!在某种意义上,这是Python力量的表现,但是,实际上,这是我们努力工作并简化那些很容易变得复杂而可怕事情的结果。
2.4.8 完整的神经网络代码
我们已经完成了神经网络类。以下代码仅供参考,你可以通过以下链接访问GitHub,这是一个共享代码的在线网站:
# neural network class definition class neuralNetwork : # initialise the neural network def __init__(self, inputnodes, hiddennodes, outputnodes, learningrate) : # set number of nodes in each input, hidden, output layer self.inodes = inputnodes self.hnodes = hiddennodes self.onodes = outputnodes # link weight matrices, wih and who # weights inside the arrays are w_i_j, where link is from node i to node j in the next layer # w11 w21 # w12 w22 etc self.wih = numpy.random.normal(0.0, pow(self.hnodes, -0.5), (self.hnodes, self.inodes)) self.who = numpy.random.normal(0.0, pow(self.onodes, -0.5), (self.onodes, self.hnodes)) # learning rate self.lr = learningrate # activation function is the sigmoid function self.activation_function = lambda x: scipy.special.expit(x) pass # train the neural network def train(self, inputs_list, targets_list) : # convert inputs list to 2d array inputs = numpy.array(inputs_list, ndmin=2).T targets = numpy.array(targets_list, ndmin=2).T # calculate signals into hidden layer hidden_inputs = numpy.dot(self.wih, inputs) # calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs) # calculate signals into final output layer final_inputs = numpy.dot(self.who, hidden_outputs) # calculate the signals emerging from final output layer final_outputs = self.activation_function(final_inputs) # output layer error is the (target - actual) output_errors = targets - final_outputs # hidden layer error is the output_errors, split by weights, recombined at hidden nodes hidden_errors = numpy.dot(self.who.T, output_errors) # update the weights for the links between the hidden and output layers self.who += self.lr * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs)) # update the weights for the links between the input and hidden layers self.wih += self.lr * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose (inputs)) pass # query the neural network def query(self, inputs_list) : # convert inputs list to 2d array inputs = numpy.array(inputs_list, ndmin=2).T # calculate signals into hidden layer hidden_inputs = numpy.dot(self.wih, inputs) # calculate the signals emerging from hidden layer hidden_outputs = self.activation_function(hidden_inputs) # calculate signals into final output layer final_inputs = numpy.dot(self.who, hidden_outputs) # calculate the signals emerging from final output layer final_outputs = self.activation_function(final_inputs) return final_outputs
这些代码可用于创建、训练和查询3层神经网络,进行几乎任何任务,这么看来,代码不算太多。
下一步,我们将进行特定任务,学习识别手写数字。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论