一、前置知识神经网络主要包含两大核心步骤前向传播获得预测的结果反向传播根据预测值与真实值的损失计算每个权重和偏置的梯度然后根据梯度更新参数以此往复便是训练的过程。前向传播其中前向传播比较简单在这个过程中信息从输入层流向输出层每一层的输出都作为下一层的输入每一层都包括权重w和 偏置b非必须然后使用σ(wxb)方式计算每一层的结果其中σ表示非线性激活函数例如sigmod、tanh、relu等经过层层计算最终输出便是预测值。其中单层的计算公式表示这里以sigmod为例整体的流转过程就如下图所示反向传播反向传播过程相对复杂是训练神经网络的核心过程通过计算损失函数关于网络中各个参数的梯度来更新网络的权重和偏置。更新的主要依据便是梯度下降法如图所示梯度下降法是一种用于寻找函数最小值的优化算法主要用于衡量目标函数在我们这里就是损失函数衡量预测值与真实值之间的差异然后根据目标函数计算出关于其不同参数的偏导数也就是损失关于每个w和b的偏导数。在高中数学中我们就知道导数表示着斜率或者叫梯度通过导数能知道往哪个方向移动可以最小化目标函数梯度下降也是基于这一理念。但是神经网络的组成必然是一个复杂的函数针对这一复杂函数的求导就要用到我们大学数学微积分中的偏导数和链式法则了如果忘记了没有关系因为我也忘记了但是一看概念就记起来了。其中导数一般是针对单变量函数的。如果一个函数依赖一个变量比如y f(x)那么这个函数相对于变量x的导数描述的就是当x发生变化时函数y变化的速率可以理解为函数图形在某一点的切线斜率。而偏导数一般是针对多变量函数的。如果一个函数依赖多个变量比如z f(x, y)那么这个函数相对于其中一个变量的偏导数描述的是当这个变量发生变化而其他变量保持不变时函数变化的速率。例如z x**2 y**2的关于x和y时的偏导数关于x的偏导数关于y的偏导数但如果一个函数包含多个函数的输出则计算其导数时还要使用链式法则例如有一个复合函数f(g(x))其中f是外层函数g是内层函数那么其导数使用链式法则可以表述为通过偏导数链式法则就可以计算出loss关于每个权重w和b的梯度有了梯度那怎么优化参数呢就是向梯度的反方向更新参数具体更新多少呢就要看我们指定的学习率是多少了计算公式如下其中α表示学习率L表示损失函数。损失函数上面反向传播过程中多次用到损失函数关于损失函数例如有经常见的MSE均方差损失函数以及经常使用的Cross Entropy交叉熵损失函数。MSE比较简单但MSE对异常值比较敏感后续我们使用交叉熵损失作为演示其中 交叉熵损失函数的公式为对于二分类问题多分类问题时有了这些前置知识下面就容易实现神经网络了关于多维向量的计算这里直接采用Numpy作为基础计算框架。二、实现前向传播过程前向传播比较容易理解这里我们以两层神经网络为例其中激活函数使用sigmod实现过程如下importnumpyasnpclassNeuralNetwork:def__init__(self,input_size,hidden_size,output_size):# 输入大小self.input_sizeinput_size# 隐藏层的大小self.hidden_sizehidden_size# 输出层的大小self.output_sizeoutput_size# 初始化第一层的权重参数self.w1np.random.randn(self.input_size,self.hidden_size)# 初始化第一层的偏置参数self.b1np.zeros((1,self.hidden_size))# 初始化第二层的权重参数self.w2np.random.randn(self.hidden_size,self.output_size)# 初始化第二层的偏置参数self.b2np.zeros((1,self.output_size))# 激活函数defsigmoid(self,x,differentiateFalse):ifdifferentiate:# 计算sigmoid激活函数的导数returnx*(1-x)else:# 计算sigmoid激活函数return1/(1np.exp(-x))# 前向传播defforward(self,x):# 第一层隐藏层的计算self.hidden_outputself.sigmoid(np.dot(x,self.w1)self.b1)# 第二层输出层的计算self.outputself.sigmoid(np.dot(self.hidden_output,self.w2)self.b2)returnself.output下面构建输入数据执行一遍前向传播的过程# 构造输入数据xnp.array([[0,0],[0,1],[1,0],[1,1]])# 初始化网络netNeuralNetwork(input_size2,hidden_size4,output_size1)# 前向传播print(net.forward(x))运行后可以看到前向输出结果由于现在没有目标值以及反向传播更新参数、这里的输出还是比较随机化的三、实现反向传播过程反向传播我们首先的目的就是先求出损失函数关于每个w和b的梯度。上面前向传播有了输入数据这里我们再设定目标值# 构造输入数据xnp.array([[0,0],[0,1],[1,0],[1,1]])# 目标标签ynp.array([[0],[0.85],[1],[0]])这里的目标值不是固定的你可以随意修改但由于使用的是sigmod激活函数所以目标值只能设置0-1之间的随意数字。有了目标就可以通过损失函数计算损失这里以交叉商损失为例实现过程如下defcross_entropy_loss(output,y,differentiateFalse):# 为了防止计算log(0)对 output 做一个很小的正则化epsilon1e-15outputnp.clip(output,epsilon,1-epsilon)ifdifferentiate:# 计算交叉熵的导数return-(y/output-(1-y)/(1-output))else:# 计算交叉熵损失return-np.mean(y*np.log(output)(1-y)*np.log(1-output))计算损失其中output为前向传播的结果losscross_entropy_loss(output, y)有了损失就可以一步一步的根据链式法则求取loss关于每个w和b的梯度这里演示下w2和b2的梯度如何计算的首先计算loss的梯度d_lossself.cross_entropy_loss(output,y,True)依据链式法则可以计算出loss关于输出output的梯度d_outputd_loss*sigmoid(output,True)再依据链式法则可以计算出loss关于w2的梯度d_w2np.dot(hidden_output.T,d_output)再依据链式法则可以计算出loss关于b2的梯度d_b2d_output*1同理方式计算loss关于w1和b1的梯度整体实现过程如下classNeuralNetwork:...衔接上面前向传播过程# 交叉熵损失函数defcross_entropy_loss(self,output,y,differentiateFalse):# 为了防止计算log(0)对 output 做一个很小的正则化epsilon1e-15outputnp.clip(output,epsilon,1-epsilon)ifdifferentiate:# 计算交叉熵的导数return-(y/output-(1-y)/(1-output))else:# 计算交叉熵损失return-np.mean(y*np.log(output)(1-y)*np.log(1-output))# 反向传播defbackward(self,x,y,output):# 计算损失的梯度d_lossself.cross_entropy_loss(output,y,True)# 依据链式法则计算损失关于输出的梯度d_outputd_loss*self.sigmoid(output,True)# 依据链式法则计算损失关于w2的梯度d_w2np.dot(self.hidden_output.T,d_output)# 依据链式法则计算损失关于b2的梯度d_b2d_output*1# 同理依据链式法则计算隐藏层输出的梯度d_hiddennp.dot(d_output,self.w2.T)*self.sigmoid(self.hidden_output,True)# 同理w1 的梯度d_w1np.dot(x.T,d_hidden)# 同理b1 的梯度d_b1d_hidden*1有了loss关于每个w和b的梯度就可以根据梯度下降公式更新每个w和b的值了这里需要有一个学习率的参数计算公式依据上面前置知识中的公式实现过程如下self.w2-learning_rate*d_w2 self.b2-learning_rate*np.sum(d_b2,axis0,keepdimsTrue)self.w1-learning_rate*d_w1 self.b1-learning_rate*np.sum(d_b1,axis0,keepdimsTrue)最后完整的过程为importnumpyasnpclassNeuralNetwork:def__init__(self,input_size,hidden_size,output_size):# 输入大小self.input_sizeinput_size# 隐藏层的大小self.hidden_sizehidden_size# 输出层的大小self.output_sizeoutput_size# 初始化第一层的权重参数self.w1np.random.randn(self.input_size,self.hidden_size)# 初始化第一层的偏置参数self.b1np.zeros((1,self.hidden_size))# 初始化第二层的权重参数self.w2np.random.randn(self.hidden_size,self.output_size)# 初始化第二层的偏执参数self.b2np.zeros((1,self.output_size))# 激活函数defsigmoid(self,x,differentiateFalse):ifdifferentiate:# 计算sigmoid激活函数的导数returnx*(1-x)else:# 计算sigmoid激活函数return1/(1np.exp(-x))# 前向传播defforward(self,x):# 第一层隐藏层的计算self.hidden_outputself.sigmoid(np.dot(x,self.w1)self.b1)# 第二层输出层的计算self.outputself.sigmoid(np.dot(self.hidden_output,self.w2)self.b2)returnself.output# 交叉熵损失函数defcross_entropy_loss(self,output,y,differentiateFalse):# 为了防止计算log(0)对 output 做一个很小的正则化epsilon1e-15outputnp.clip(output,epsilon,1-epsilon)ifdifferentiate:# 计算交叉熵的导数return-(y/output-(1-y)/(1-output))else:# 计算交叉熵损失return-np.mean(y*np.log(output)(1-y)*np.log(1-output))# 反向传播defbackward(self,x,y,output,learning_rate):# 计算损失的梯度d_lossself.cross_entropy_loss(output,y,True)# 依据链式法则计算损失关于输出的梯度d_outputd_loss*self.sigmoid(output,True)# 依据链式法则计算损失关于w2的梯度d_w2np.dot(self.hidden_output.T,d_output)# 依据链式法则计算损失关于b2的梯度d_b2d_output*1# 同理依据链式法则计算隐藏层输出的梯度d_hiddennp.dot(d_output,self.w2.T)*self.sigmoid(self.hidden_output,True)# 同理w1 的梯度d_w1np.dot(x.T,d_hidden)# 同理b1 的梯度d_b1d_hidden*1# 梯度下降更新参数self.w2-learning_rate*d_w2 self.b2-learning_rate*np.sum(d_b2,axis0,keepdimsTrue)self.w1-learning_rate*d_w1 self.b1-learning_rate*np.sum(d_b1,axis0,keepdimsTrue)四、迭代训练上面前向传播、反向传播实现后核心的内容就已经基本结束了训练过程就比较简单了通过循环往复的计算梯度更新参数使loss值最小化达到预测值逐渐逼近真实值的过程。实现过程importmatplotlib.pyplotasplt# 构造输入数据xnp.array([[0,0],[0,1],[1,0],[1,1]])# 标签ynp.array([[0],[0.85],[1],[0]])# 初始化网络netNeuralNetwork(input_size2,hidden_size4,output_size1)# 训练epochs5000## 迭代周期learning_rate0.1## 学习率# 绘图数据plot_x,plot_y[],[]forepochinrange(epochs):# 前向传播outputnet.forward(x)# 反向传播更新参数net.backward(x,y,output,learning_rate)# 计算损失lossnet.cross_entropy_loss(output,y)plot_x.append(epoch)plot_y.append(loss)# 每1000轮打印一次ifepoch%10000orepochepochs-1:print(fepoch:{epoch}, loss:{loss})# 测试模型的输出print(最终输出)np.set_printoptions(suppressTrue)outputnet.forward(x)print(output)# 绘制loss曲线图plt.plot(plot_x,plot_y)# 添加标题和标签plt.xlabel(epoch)plt.ylabel(loss)# 显示图表plt.show()运行后可以看到loss曲线图以及最后测试的结果从日志中可以看出在5000个epoch下损失最终降到0.108左右并且最后的预测结果已经非常趋于真实值了。如果我们将真实值随意修改为但注意要在0-1范围内ynp.array([[0.96],[0.63],[0.75],[0.45]])这里你可以任意修改然后再次训练查看日志结果仍然非常趋于真实值因为整个过程是以结果为导向的。五、总结到此整个从零实现神经网络前向传播、反向传播、迭代训练过程就结束了。通过上述内容你应该了解了神经网络的基本训练过程包括前向传播、反向传播以及梯度下降。虽然梯度下降是一种有效的优化算法但它也存在一些局限性。其中一个主要问题是局部最优解。由于梯度下降算法是沿着损失函数的梯度下降如果损失函数是非凸的存在多个山峰和山谷算法可能会陷入局部最优解而不是全局最优解。不过不用过于担心实际我们训练网络模型时使用的Adam优化器已经结合了动量和自适应学习率或者RMSprop优化器为每个参数调整学习率这些优化方式都有助于优化局部最优。