python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数


目录

  • 问题引入
  • 数值微分方法
    • 损失函数与权重参数的函数关系(数学表达式)
      • 神经网络中权重参数的符号
      • 函数关系的推导
    • 数学基础知识及代码实现的复习
      • 求函数梯度的函数及改进
      • 神经网络中求损失函数梯度的例子
    • 实现一个的3层神经网络(一个隐藏层)
      • 神经网络类代码
      • 训练过程代码
      • 训练过程代码中的一些问题
        • 关于训练速度
        • 关于大量数据对神经网络的意义

本博客参考书籍:深度学习入门(斋藤康毅著)
问题引入 在从零到一实现神经网络(python):二中,我们完成了神经网络由输入值经过隐藏层得到最终输出值的完整过程,这一过程也被称为神经网络的信号前馈过程,同时我们也提到了神经网络相对于单层感知机的优势:能够自动从数据中学习得到权重参数,而不需要人为设置。那么这个过程是怎么实现的呢?
数值微分方法 损失函数与权重参数的函数关系(数学表达式) 神经网络中权重参数的符号
我们在从零到一实现神经网络(python):二中介绍过权重参数的表示方法,现在再来复习一遍
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

函数关系的推导
【python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数】我们举出一个含1个隐藏层的3层神经网络作为例子
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

我们把一个神经元的拟输出用y表示,实际输出用z表示,理想输出用t表示
输入层的第i个神经元
输出: x i x_i xi?
隐藏层的第j个神经元
拟输出 y j = ∑ i = 1 3 W j , i ? x i + b j y_j=\sum_{i=1}^3W_{j,i}*x_i+b_j yj?=i=1∑3?Wj,i??xi?+bj?
实际输出: z j = 1 1 + e ? ( y ( j ) ) ( s i g m o i d 函 数 ) z_j=\frac{1}{1+e^{-(y(j))}}(sigmoid函数) zj?=1+e?(y(j))1?(sigmoid函数)
输出层的第k个神经元
拟输出: y k = ∑ j = 1 3 W k , j ? z j + b k y_k=\sum_{j=1}^3W_{k,j}*z_j+b_k yk?=j=1∑3?Wk,j??zj?+bk?
实际输出: z k = e y k ∑ n = 1 3 ( e y n ) ( s o f t m a x 函 数 ) z_k=\frac{e^{y_k}}{\sum_{n=1}^3(e^{y_n})}(softmax函数) zk?=∑n=13?(eyn?)eyk??(softmax函数)
理想输出: t k t_k tk?
误差: o k = ? t k ? l o g ( z k ) ( 交 叉 熵 损 失 函 数 ) o_k=-t_k*log(z_k)(交叉熵损失函数) ok?=?tk??log(zk?)(交叉熵损失函数)
于是我们得到下面一个奇怪的式子:
o k = ? t k ? l o g ( e ∑ j = 1 3 W k , j ? ( 1 1 + e ? ( ∑ i = 1 3 W j , i ? x i + b j ) ) + b k ∑ n = 1 3 ( e ∑ j = 1 3 W n , j ? ( 1 1 + e ? ( ∑ i = 1 3 W j , i ? x i + b j ) ) + b n ) ) o_k=-t_k*log(\frac{e^{\sum_{j=1}^3W_{k,j}*(\frac{1}{1+e^{-(\sum_{i=1}^3W_{j,i}*x_i+b_j)}})+b_k}}{\sum_{n=1}^3(e^{\sum_{j=1}^3W_{n,j}*(\frac{1}{1+e^{-(\sum_{i=1}^3W_{j,i}*x_i+b_j)}})+b_n})}) ok?=?tk??log(∑n=13?(e∑j=13?Wn,j??(1+e?(∑i=13?Wj,i??xi?+bj?)1?)+bn?)e∑j=13?Wk,j??(1+e?(∑i=13?Wj,i??xi?+bj?)1?)+bk??)
这个式子就是我们一直寻找的损失函数与权重参数的关系,惊不惊喜?但是呢,我们并不需要去对它求解,我们之所以在这里一步步将这个式子列出来,目的有两点
  1. 我们要时刻记住,损失函数是关于权重参数的函数,训练神经网络的过程,就是寻找一组合适的权重参数使得损失函数的值最小
  2. 在实现比较简单的神经网络时,我们往往将神经网络定义为一个类时,将权重定义为它的一个属性,而损失函数定义为它的一个方法,然后将该神经网络实例化出来
  3. 在这个3层神经网络的例子中,我们会发现当神经网络的输入层x和理想输出t确定时,误差函数与下面四个自变量有关:输入层与隐藏层之间的权重参数 w j , i w_{j,i} wj,i?,隐藏层神经元对应的偏置 b k b_k bk?,隐藏层与输出层之间的权重参数 w k , j w_{k,j} wk,j?,输出层神经元对应的偏置 b k b_k bk?
数学基础知识及代码实现的复习 求函数梯度的函数及改进
def numerical_grad(f,x): '''f:关于x的函数 x:某一点,x=[x1,x2,...] ------- returns:函数在某一点处的梯度 ''' h=1e-4 grad=np.zeros_like(x) for i in range(x.size): tmp=x[i] x[i]=tmp-h f1=f(x)x[i]=tmp+h f2=f(x)grad[i]=(f2-f1)/(2*h) x[i]=tmp return grad

上面的定义的函数中的自变量x大多数都是一个一维数组,但是神经网络中往往牵扯到多维数组,例如上面举的例子中,输入层和隐藏层之间的权重参数是一个3x3的多维数组
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

这个时候上面的代码已经不能满足我们的需求(强行使用上述代码求自变量为非一维的函数的梯度程序会报错:‘index 2 is out of bounds for axis 0 with size 2’)。为了解决这一问题,我们将上面的代码稍微修改一下,使它能够遍历多维数组
np.nditer()函数返回一个有效的多维迭代器对象,可以遍历数组。
具体信息请访问:
1、numpy官方
2、流年里不舍的执着的博客
def numerical_gradient(f, x): h = 1e-4 # 0.0001 grad = np.zeros_like(x)it = np.nditer(x, flags=['multi_index'], op_flags=['readwrite']) while not it.finished: idx = it.multi_index tmp_val = x[idx] x[idx] = float(tmp_val) + h fxh1 = f(x) # f(x+h)x[idx] = tmp_val - h fxh2 = f(x) # f(x-h) grad[idx] = (fxh1 - fxh2) / (2*h)x[idx] = tmp_val # 还原值 it.iternext()return grad

神经网络中求损失函数梯度的例子
我们举出一个不含隐藏层的2层神经网络作为例子1
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

代码实现
class Twolayer_net: def __init__(self): self.W=np.random.randn(2,3) def predict(self,x): return np.dot(x,self.W) def loss(self,x,t): z=self.predict(x) y=softmax(z) loss=cross_entropy_loss(y,t) return loss

我们实例化这个神经网络,利用numerical_grad()函数求在某一点处它的梯度
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

关于上面代码中的那个没有实际作用的函数,它的定义是为了兼容numerical_grad()的函数。这里大家可能会有疑问:numerical_grad(f,w)函数中两个形式参数,前者要将后者作为参数带入运算,但是这里定义的函数f(w),无论形式参数w如何改变,得到的值都是神经网络损失函数的值?
解答:我们曾反复说过:损失函数是权重参数的函数,求一个神经网络的损失函数值一定要用到权重参数,在函数numerical_grad()函数中,曾将net.w的值改变,那么,对应的net.loss()的值调用时使用的net.w值也一定会改变,net.loss()的值就随着改变,于是,我们便达到了目的
实现一个的3层神经网络(一个隐藏层) 神经网络类代码
class Threelayer_net: def __init__(self,input_size,hidden_size,output_size,lr=0.01): self.params={} self.params['w1']=lr*np.random.randn(input_size,hidden_size)# 输入层与隐藏层之间的权重参数 self.params['b1']=np.zeros(hidden_size)# 隐藏层各神经元对应的偏置 self.params['w2']=lr*np.random.randn(hidden_size,output_size) # 隐藏层与输出层之间的权重参数 self.params['b2']=np.zeros(output_size)# 输出层各神经元对应的偏置 def predict(self,x): w1,w2=self.params['w1'],self.params['w2'] b1,b2=self.params['b1'],self.params['b2'] a1=np.dot(x,w1)+b1 z1=sigmoid(a1) a2=np.dot(z1,w2)+b2 z2=softmax(a2) returnz2 def loss(self,x,t): z=self.predict(x) return cross_entropy_error(z,t) def grad(self,x,t):# 用于保存损失函数对于每个 def f(w): return self.loss(x,t) grads={} grads['w1']=numerical_gradient(f,self.params['w1']) grads['b1']=numerical_gradient(f,self.params['b1']) grads['w2']=numerical_gradient(f,self.params['w2']) grads['b2']=numerical_gradient(f,self.params['b2']) return grads

训练过程代码
import time iter_num=100 lr=0.1 loss=[] start_time=time.time() for j in range(iter_num): grad=new_net.grad(a[j],b[j]) for i in ['w1','b1','w2','b2']: new_net.params[i]-=lr*grad[i] loss.append(new_net.loss(a[j],b[j])) if j%5==0: print(len(loss)) end_time=time.time()print(end_time-start_time)

训练过程代码中的一些问题
训练网络笔者推荐大家使用jupyter notebook
在这个训练代码中,笔者使用了从零到一实现神经网络(三)中提到了mnist_train_100.csv,隐藏层设置了100个神经元,每次迭代从文件中获得一个mnist手写图片以及对应的标签,代入计算梯度,然后使用梯度下降法更新权重参数
笔者使用上面代码训练时得到的输出如下
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

居然耗费了将近30分钟,而且在笔者训练过程中观察到电脑cpu的利用率在33%左右,可见上面的训练代码质量并不高,我们来看看训练出来的网络表现如何
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

仔细看我们发现正确的个数仅有3个,太过于差劲了!
笔者只将训练迭代了100次,也就是说权重参数仅仅更新了100次,而现实中的问题往往不可能这么简单(比如我们这次使用到的Mnist手写数字),所以这也是我们得到神经网络表现很差劲的原因
后续笔者想要尝试使用多线程加速,不知道能不能行得通
关于训练速度 使用数值微分方法计算梯度很大的一个缺点就是速度很慢,这也是大型网络中不使用数值微分的重要原因,下一节将通过误差反向传播解决这一问题
关于大量数据对神经网络的意义 在上一节中我们求一个多元函数最小值时,仅仅输入了一个x的坐标值,经过200次迭代,得到最小值,现在我们在神经网络中是不是也能通过一组数据得到有效的权重参数呢?那这么来看用数据喂神经网络是不是就没有意义了呢?
并不是这样,笔者这样尝试过,但是得到了下面的结果
python机器学习|从零到一实现神经网络(五):数学微分法更新权重参数
文章图片

仅仅使用一组数据(即一个mnist手写数字图片),训练200次,我们看到,训练得到的网络拿来做测试的时候,对于所有的测试数据,一律输出7;现在我们得到了答案:训练得到的神经网络要对未知数据有很好的预测效果,才能说这个神经网络性能好,这便是我们使用大量数据喂给神经网络的意义
  1. 在定义f(w)函数有什么意义的问题上,感谢博主嘻嘻作者哈哈的博客对于该问题的解释 ??

    推荐阅读