使用python实现深度神经网络 2(转)

时间:2023-01-23 23:38:40

https://blog.csdn.net/oxuzhenyi/article/details/73026796

导数与梯度、矩阵运算性质、科学计算库numpy

一、实验介绍

1.1 实验内容

虽然在实验一中我想尽量少的引入(会让人放弃继续学习的)数学概念,但我似乎还是失败了。不过这几乎是没有办法的事,要想真正学会深度学习,没有一定的数学基础(高等数学、线性代数、概率论、信息论等),(几乎)是不可能的。学深度学习不学其中的原理你可能能够学会搭建模型,但当模型出了问题或者无法训练出好的结果时,不懂原理是很难调试的。

不过话说回来,要想理解深度学习中的基本概念(而不是想要在深度学习领域做研究),要学的数学知识也不是很难。你应该很快就能掌握这些知识。

所以本次实验课,我们介绍本课程会涉及到的数学知识以及在之后“ 图片英文字母识别”的项目中要用到的python numpy模块。

警告:本次实验介绍的数学知识只是为了让你更好地理解本课程中的相关概念,有些地方不够严谨,请勿等同于数学教科书参考

1.2 实验知识点

  • 导数、偏导、梯度、链式法则
  • 矩阵运算基本法则
  • numpy基本运算介绍

1.3 实验环境

  • python 2.7
  • numpy 1.12.1

二、实验步骤

2.1 导数、偏导、梯度、复合函数求导链式法则

2.1.1 函数值随自变量的变化速率--导数

高中数学里面我们已经学过,函数值随自变量的变化速率是导数。导数衡量的,其实是一个变量对函数值影响能力的大小。导数值越大,则该变量每改变一点对最终函数值的影响越大。且导数值为正时,代表自变量增大时函数值增大,反之若导数值为负,则自变量增大时函数值减小。
常见函数的导函数:

原函数f 导函数f'
任何常数 0
x 1
e^x e^x
x^2 2*x
1/x -1/x^2
ln(x) 1/x

2.1.2 从单变量到多变量--偏导

上面我们列举的都是只有一个自变量的函数,如果自变量有多个,如何求导数呢?比如对于函数f=x+y,怎样衡量x和y分别对函数值f的影响快慢呢?
数学上引入了偏导的概念,对一个多变量函数f,求f对其中一个自变量x的偏导很简单,就是将与x无关的其他自变量视为常亮,再使用单变量求导的方法去求导。得到的即为f对x的偏导。比如:

令f=x+2y, 则f对x求偏导的结果为1,对y求偏导的结果为2。
令f=x*y, 则f对x求偏导结果为y,对y求偏导结果为x。

2.1.3 多变量函数变化最快的方向--梯度

2.1.1 中我们提到了,对单变量函数来说,导数值的正负代表自变量对函数值影响的“方向”:变大或变小。那对于多变量函数来说,如何表达这个方向呢?这就引入了梯度的概念:

梯度是一个向量,向量长度与自变量的个数相等,且其中的每一个元素为函数对于对应变量求偏导的值。

比如对于函数f=x*y, 其梯度向量为(y,x),
对于具体的自变量的值,比如x=1,y=1的点,其梯度向量就为(1,1),
又比如x=10,y=-20点,其梯度向量就为(-20,10)

梯度作为一个向量,指向的是使函数值增大最快的方向(回想第一次实验中的损失函数图,梯度所指的方向是向上的)。

2.1.4 复合函数求导链式法则

上面我们讲的求导数和求偏导,都是对于“简单函数”,对于“复合函数”,比如下面这样的函数:

  1.  f1(x)=1/x
     
  2.  f2(x)=e^x
     
  3.  
     
  4.  f=f1(f2(x))
     

f函数是一个复合函数,它由f1f2函数“串联”而来。其中f1的输入是f2的输出。

对于复合函数求导,一种方法是将复合函数展开,比如对于上面的函数, 得到f=1/(e^x),然后再根据简单函数求导法则对自变量求导。过程如下:
f' = -1/((e^x)^2)*((e^x)') = -(e^x)/((e^x)^2) = -1/(e^x)
即 f' = -1/(e^x)

其实,在上面的求导过程中,我们已经使用了求导链式法则(chain
rule)
,只是你没有察觉而已。求导链式法则让我们可以一部分一部分地,对复合函数求导,而不用放在一起求。这对于编程来说十分重要,它使得对复合函数求导变得十分简单。
但是这里描述起来可能稍显复杂。以f为例,当我们需要对自变量x求导时,我们可以先将f2(x)看做一个自变量f2,先让f1f2求导,得到第一部分导函数-1/(f2^2),再让f2x求导,得到第二部分导函数e^x。求好之后,直接将两部分导数乘起来,即得到最终复合函数整体的导数。不过要先使用实际的表达式替换掉第一部分导数中的f2,
即第一部分导数为-1/((e^x)^2), 第二部分导数为e^x。两部分乘起来就得到了最终正确的-1/(e^x)

现在你可能觉得这个链式法则是复杂乏味的,但是下一次实验你会发现链式法则真是太强大了。实际上,我们最后实现的深度神经网络,就是不断在运用求导链式法则。

2.2 矩阵及其基本运算性质

如果你上过本科线性代数课程,你十有八九会对矩阵没有什么感觉,甚至对这么一个运算法则十分奇怪的东西感到厌恶。但我希望你今后能改变对矩阵、对线性代数的看法,不要让糟糕的教材和老师糟糕的ppt毁掉线性代数可能带给你的巨大的提升自己(是的,这并不夸张)的机会。矩阵其实非常非常非常有用,在现代科学的每一个角落,几乎都能看到矩阵的身影,深度学习中更是如此。

限于篇幅,本节只会介绍必要的矩阵相关知识,线性代数中的更多东西,请你通过其他途径学习(推荐使用英文教材学习)。

2.2.1 矩阵的表达形式

一个m*n的矩阵为一个m行n列的数组,比如:

使用python实现深度神经网络 2(转)

a是一个3*2的矩阵,b是一个2*3的矩阵,c是一个3*1的矩阵,d是一个1*2的矩阵。

其中,c只有一列,我们也可以称c列向量d只有一行,我们也可以称d行向量。本课程中,对于向量,默认都是指列向量

2.2.2 矩阵的运算法则

  1. 矩阵的数乘运算
    一个标量(你可以直接理解为一个数字)乘以矩阵,得到的结果为矩阵中的每个元素和该标量相乘,如下图:
    使用python实现深度神经网络 2(转)
  1. 矩阵的转置运算
    转置运算通过在矩阵右上角添加一“撇”表示。
    使用python实现深度神经网络 2(转)
    转置就是矩阵翻转一下,转置会改变矩阵的形状。注意观察转置是绕着哪个轴翻转的。

  2. 矩阵之间的加减法
    矩阵之间的加减法要求参与运算的两个矩阵尺寸相同,运算的结果等于两个矩阵对应元素相加减。
    使用python实现深度神经网络 2(转)

  3. 矩阵魔力的来源--矩阵之间的乘法
    矩阵的乘法有些复杂,但在第一讲实验中你已经见过它了。矩阵的乘法其实就是代表了一个线性方程组参数和自变量如何结合的过程(矩阵乘法还有更多丰富的含义,如有兴趣,请你自己去探索)。
    使用python实现深度神经网络 2(转)
    矩阵乘法的具体规则就是,第一个矩阵中的第i行的所有元素,与第二个矩阵中的第j列的所有元素,分别相乘之后再求和,得到结果矩阵中第i行第j列的元素。
    上面的描述只看一遍很难弄懂,请你结合图片中的例子仔细揣摩。
    矩阵乘法首先要求参与乘法运算的两个矩阵的尺寸能够“兼容”,具体的要求就是,第一个矩阵的列数与第二个矩阵的行数必须相同。你可以观察图片中的示例,第一个矩阵的列数都是2,第二个矩阵的行数也都是2,这样才能保证“第一个矩阵中的第i行所有元素”与“第二个矩阵中第j列的所有元素”能够一一对应。
    矩阵乘法运算得到的结果矩阵,其行数等于第一个矩阵的行数,其列数等于第二个矩阵的列数。
    矩阵乘法不满足交换律!!首先,交换两个矩阵的位置之后它们的尺寸不一定能够兼容,然后即使兼容,运算得到的结果也不一定与原来相同。你可以自己随便举几个例子试一下。

2.3 科学计算库 numpy

实现我们的深度神经网络,需要进行很多数学运算,尤其是矩阵运算。而你也看到了,矩阵的(乘法)运算很复杂,自己编程实现比较困难而且容易出错。为了解决这些问题,我们将会使用python中的科学计算库numpy。有了numpy,
我们的代码将大大简化,同时速度也会有很大提升。

2.3.1 使用numpy

实验楼环境已经安装了numpy,使用import语句导入即可,为了简化代码,导入后我们将numpy命名为np。

  1.  import numpy as np
     
  2.  print numpy.__version__ # 查看numpy版本
     

当你使用numpy进行计算时,在terminal 里输入top命令,你会发现有多个"一样"的python进程在运行,这是因为numpy会自动进行多进程运算,提高计算速度。

>> top

以下的实例请你自己在python shell 中一起实验一遍。

2.3.2 numpy基本数据类型

numpy中的数据类型被称为ndarray(即 N-dimensional array,多维数组),创建一个ndarray很简单:

  1.  import numpy as np
  2.   
  3.  array=np.array([1,2,3],dtype=np.uint8)
  4.  print array

即向np.array()函数传入一个python列表即可。注意dtype参数是可选的,它指定了生成的数组的数据长度和类型,这里是长度为8bit的无符号整数。

2.3.3 快速创建矩阵

mat1=np.zeros((2,3))

np.zeros()快速创建一个指定维度的全0矩阵,注意传进去的参数是一个tuple

2.3.4 numpy中的高维矩阵

"矩阵"一般指有行和列的“二维”矩阵,但numpy还支持高维矩阵,比如下面:

  1.  nd=np.zeros((1,2,3,4))
  2.  print nd.shape
  3.  print nd.size

nd就可以看作是一个1x2x3x4尺寸的高维矩阵。ndarray.shape保存的是数组的“形状”,也就是高维矩阵每一维的长度。ndarray.size保存的是数组每一维长度相乘的结果,即数组元素的个数。

2.3.5 标准矩阵运算

首先你要注意的是,numpy中的运算和数学中的运算不是完全一样的,实际上,numpy不仅为我们提供了标准运算,还提供了更多方便我们编程的运算类型和特性。

我们先来看标准的矩阵运算:

  1. 标量与矩阵相乘
    1.  scalar=2
    2.  mat=np.zeros((2,3))
    3.  mat1=scalar*mat
  2. 矩阵转置
    1.  mat=np.zeros((2,3))
    2.  tmat=mat.T
    3.  print mat.shape, tmat.shape
    4.  mat3=np.array((1,2,3))
    5.  tmat3=mat3.T
    6.  print mat3.shape, tmat3.shape

    对于二维矩阵,ndarray.T即可得到其转置。对于高维矩阵,ndarray.T会将维度的顺序完全翻转(顺序逆过来)。

  3. 矩阵相加
    1.  mat1=np.array([[1,2],[3,4]])
    2.  mat2=np.array([[1,0],[0,1]])
    3.  mat3=mat1+mat2
  4. 矩阵乘法
    1.  mat1=np.array([[1,2],[3,4]])
    2.  mat2=np.array([[5,6],[7,8]])
    3.  mat3=mat1.dot(mat2)

    注意这里有一些变化,矩阵相乘不能直接使用*号,而是通过.dot()函数。

2.3.6 扩展运算

numpy内置的扩展运算用起来非常方便。

  1. 两个矩阵的对应元素相乘(内积?)

    1.  mat1=np.array([[1,2],[3,4]])
    2.  mat2=np.array([[5,6],[7,8]])
    3.  mat=mat1*mat2

    注意相乘的两个矩阵尺寸必须相同。

  2. 标量与矩阵相加

    1.  scalar=2
    2.  mat=np.array([[1,2],[3,4]])
    3.  mat1=scalar+mat

    标量与矩阵相加就相当于对矩阵的每个元素都加上该标量。

  3. 操纵高维矩阵的维度

    1.  mat3=np.zeros((1,2,3))
    2.  tmat3=mat3.transpose(0,2,1)
    3.  print mat3.shape,tmat3.shape

    有时候,我们想要改变高维矩阵维度的顺序,但ndarray.T只能完全翻转,无法满足我们的需求,这个时候就可以调用ndarray.transpose(),其参数代表原本矩阵的维度重新排列的顺序。所以这里的例子实际上相当于第0维不变,第1第2维交换。

  4. broad cast--拓宽操作
    国内有些人将numpy的broadcast按照字面意思翻译为“广播”,这样显然是容易误导人的。根据broadcast在numpy中的实际作用,我个人更倾向于将braodcast 拆开并翻译为“拓宽”(向更宽的矩阵拓展)。其具体作用为:
    当两个矩阵进行加/减法运算时,比如我们需要将一个列向量加到一个矩阵的每一列上,由于尺寸不同,无法直接进行运算,一种直接粗暴的做法就是循环遍历矩阵的每一列,再把列向量加到每一列上,这样代码会显得很复杂。而 numpy会自动执行的broadcast操作则会先将列向量“拓宽”成一个相同尺寸的矩阵,且其每一列都是对原列向量的复制,然后再进行运算。如下:
    1.  
      mat1=np.zeros((3,2))
    2.  vec=np.array([[1],[2],[3]])
       
    3.  print mat1+vec
       

    对于行向量和高维矩阵也是如此。
    更详细的描述,请参考numpy文档:broadcasting

2.3.7 杂项操作

本节介绍一些后面的项目会用到的其他杂项操作

  1. 生成随机数据

  2. rannum=np.random.randn(5,10)

  3.  这里的np.random.randn()函数生成一个指定尺寸的矩阵,且矩阵中的所有数字符合正态分布(normal distribution)
    1.  l=[1,2,3]
    2.  np.random.shuffle(l)
    3.  print l

    np.random.shuffle()函数可以接收python list或者numpy ndarray,并将数组中的元素随机打乱。

  4. 对矩阵求和

    1.  a=np.random.randn(3,2)
    2.  print np.sum(a)

    np.sum()函数会对矩阵中的所有元素求和。

  5. numpy中的“轴(axis)”
    我们之前使用“维度”描述矩阵的形状,这样容易和之前提到的向量的维度(长度)混淆,numpy中有另一个概念叫做“轴(axis)”与这里所说的“维度”很类似,指的是对一个矩阵进行操作时,所执行的“方向”。文字不太好描述,我们结合实例来理解:

    1.  a=np.zeros((3,2))
       
    2.  a=a+1
       
    3.  print np.sum(a,axis=0)
       
    4.  print np.sum(a,axis=1)
       

    np.sum(a,axis=0)就是对矩阵a,在第一个“轴”上求和,具体效果就是对矩阵的每一列求和。np.sum(a,axis=1)就是对矩阵a,在第二个“轴”上求和,具体效果就是对矩阵的每一行求和。
    这里可能不太好理解,请自己多举几个例子实验一下。

  6. e的指数

    1.  a=np.random.randn(3,2)
       
    2.  print np.exp(a)
       

    np.exp()返回输入中的每个元素x都对e求指数的结果。

  7. 求一个数组中最大元素的下标

    1.  
      a=[1,2,3,4,3,2,1]
    2.  print np.argmax(a)
       

    np.argmax()返回一个python列表或numpy ndarray中的最大元素的下标。

三、实验总结

本次实验的内容已经被我尽量精简了,只保留了后面的项目当中会用到的内容。我希望你能尽量理解上面的知识,虽然对于一些人来说这可能有些难,但数学最能体现人类的智慧不是吗,数学是深度学习,乃至人工智能得以发展的重要基础。
如果你觉得本次实验内容太简单或者写的不够好,请自行查阅其他资料学习相关内容。

本次实验,我们学习了:

  1. 导数衡量一个自变量对函数值影响的能力大小。
  2. 偏导用来衡量多变量函数中的一个自变量对函数值影响的能力大小。
  3. 梯度是一个向量,指向函数值增大最快的方向。
  4. 链式法则是指,对于复合函数,其求导过程可以一部分一部分地进行,再“链接”起来。
  5. 可以认为向量是矩阵的一种特殊形式。
  6. 矩阵乘法与线性方程组关系密切。
  7. numpy库中的ndarray可以很方便的用来进行矩阵运算。

四、课后作业

    1. 请你回想本次实验所讲的每一个知识点,确保你对它们都理解的很清楚。
    2. numpy是一个非常著名的,经常被使用的库,值得你进一步学习,请你自己继续学习numpy中的其他东西:numpy官网