UP | HOME

神经网络基础

Table of Contents

当实现一个神经网络的时候,需要知道一些非常重要的技术和技巧

例如有一个包含m个样本的训练集,很可能习惯于用一个for循环来遍历训练集中的每个样本

但是当实现一个神经网络的时候,通常不直接使用for循环来遍历整个训练集

在神经网络的计算中,通常先有一个叫做 前向暂停 forward pause 或叫做 前向传播 foward propagation 的步骤,接着有一个叫做 反向暂停 backward pause 或叫做 反向传播 backward propagation 的步骤

也会介绍为什么神经网络的训练过程可以分为前向传播和反向传播两个独立的部分 

接下来将使用 逻辑回归 logistic regression 来传达这些想法,以使能够更加容易地理解这些概念

二分类

逻辑回归是一个用于 二分类 binary classification 的算法

首先从一个问题开始说起,这里有一个二分类问题的例子

假如有一张图片作为输入,比如这只猫,如果识别这张图片为猫,则输出标签1作为结果

如果识别出不是猫,那么输出标签0作为结果

现在可以用字母 \(y\) 来表示输出的结果标签,如下图所示:

269118812ea785aee00f6ffc11b5c882.png

接下来来看看一张图片在计算机中是如何表示的,为了保存一张图片,需要保存三个矩阵,它们分别对应图片中的红、绿、蓝三种颜色通道,如果图片大小为64x64像素,那么你就有三个规模为64x64的矩阵,分别对应图片中红、绿、蓝三种像素的强度值。为了便于表示,这里画了三个很小的矩阵,注意它们的规模为5x4 而不是64x64,如下图所示:

1e664a86fa2014d5212bcb88f1c419cf.png

为了把这些像素值放到一个特征向量中,需要把这些像素值提取出来,然后放入一个特征向量 \(x\)

为了把这些像素值转换为特征向量 ,需要像下面这样定义一个特征向量 x 来表示这张图片

把所有的像素都取出来,例如255、231等等,直到取完所有的红色像素

接着最后是255、134、…、255、134等等

直到得到一个特征向量,把图片中所有的红、绿、蓝像素值都列出来

如果图片的大小为64x64像素,那么向量 x 的总维度,将是64 * 64 * 3,这是三个像素矩阵中像素的总量。在这个例子中结果为12,288

现在用 \(n_x = 12288\) 来表示输入特征向量的维度,有时候为了简洁,会直接用小写的n来表示输入特征向量的x维度。所以在二分类问题中,目标就是获得一个分类器,它以图片的特征向量作为输入,然后预测输出结果为1还是0,也就是预测图片中是否有猫:

e173fd42de5f1953deb617623d5087e8.png

符号定义

  • \(x\) : 表示一个 \(n_x\) 维数据,为输入数据,维度为 \((n_x, 1)\)
  • \(y\) : 表示输出结果,取值为 \((0, 1)\)
  • \((x^{(i)}, y^{(i)})\) : 表示第 \(i\) 组数据,可能是训练数据,也可能是测试数据,此处默认为训练数据
  • \(\mathbf{X} = [x^{(1)}, x^{(2)}, \ldots, x^{(m)}]\) : 表示所有的训练数据集的输入值,放在一个 \(n_x \times m\) 的矩阵中,其中 \(m\) 表示样本数目
  • \(\mathbf{Y} = [y^{(1)}, y^{(2)}, \dots, y^{(m)}]\) : 对应表示所有训练数据集的输出值,维度为 \(1 \times m\)

用一对 \((x, y)\) 来表示一个单独的样本,\(x\) 代表 \(n_x\) 维的特征向量, \(y\) 表示标签(输出结果)只能为0或1。 而训练集将由 \(m\) 个训练样本组成:

  • 其中 \((x^{(1)}, y^{(1)})\) 表示第一个样本的输入和输出
  • \((x^{(2)}, y^{(2)})\) 表示第二个样本的输入和输出
  • 直到最后一个样本 \((x^{(m)}, y^{(m)})\)

所有的这些一起表示整个训练集:

  • 有时候为了强调这是训练样本的个数,会写作 \(\mathbf{M}_{train}\)
  • 当涉及到测试集的时候,会使用 \(\mathbf{M}_{test}\) 来表示测试集的样本数,所以这是测试集的样本数

12f602ed40ba90540112ae0fee77fadf.png

最后为了能把训练集表示得更紧凑一点,会定义一个矩阵用大写 \(\mathbf{X}\) 的表示,它由输入向量 \(x^{(1)}\) , \(x^{(2)}\) 等组成,如下图放在矩阵的列中,所以现在 \(x^{(1)}\) 把作为第一列放在矩阵中, \(x^{(2)}\) 作为第二列,\(x^{(m)}\) 放到第 \(m\) 列,然后就得到了 训练集矩阵 。所以这个矩阵有 \(m\) 列,是训练集的 样本数量 ,然后这个矩阵的高度记为 \(n_{x}\) ,注意有时候可能因为其他某些原因,矩阵 \(\mathbf{X}\) 会由训练样本按照行堆叠起来而不是列,\(x^{(1)}\) 的转置直到 \(x^{(m)}\) 的转置,但是在实现神经网络的时候,使用左边的这种形式,会让整个实现的过程变得更加简单:

1661e545ce5fd2c27b15444d5b69ec78.png

python 表示

\(\mathbf{X}\) 是一个规模 \(n_{x}\) 为乘以 \(m\) 的矩阵,当用Python实现的时候,会看到 X.shape()

这是一条Python命令,用于显示矩阵的规模

即 X.shape 等于 \((n_x, m)\) ,\(\mathbf{X}\) 是一个规模为 \(n_x\) 乘以 \(m\) 的矩阵

综上所述,这就是如何将训练样本(输入向量的集合)表示为一个矩阵

那么输出标签呢?同样的道理,为了能更加容易地实现一个神经网络,将标签 \(y\) 放在列中将会使得后续计算非常方便,所以定义大写的 \(\mathbf{Y}\) 等于 \(y^{(1)}, y^{(2)}, \ldots, y^{(m)}\) ,所以在这里是一个规模为1乘以 \(m\) 的矩阵,同样地用Python将表示为 Y.shape 等于,表示这是一个规模为1乘以 \(m\) 的矩阵

55345ba411053da11ff843bbb3406369.png

实现神经网络的时候会发现,一个好的符号约定能够将不同训练样本的数据很好地组织起来

这里所说的数据不仅包括  x 或者 y 还包括之后会看到的其他的量

将不同的训练样本的数据提取出来,然后就像刚刚对 x 或者 y 所做的那样,将他们堆叠在矩阵的列中,形成之后会在逻辑回归和神经网络上要用到的符号表示

逻辑回归

接下来将介绍 逻辑回归Hypothesis Function (假设函数)

对于二元分类问题来讲,给定一个输入特征向量 \(\mathbf{X}\) ,它可能对应一张图片。如果想知道这张图片是否是一只猫或者不是一只猫的图片,就需要一个算法能够输出预测,这被称之为 \(\hat{y}\) ,也就是对实际值 \(y\) 的估计。更正式地来说,想让 \(\hat{y}\) 表示 \(y\) 等于1的可能性或者是机会,前提条件是给定了输入特征 \(\mathbf{X}\) 。换句话来说,如果 \(\mathbf{X}\) 是一张图片,想让 \(\hat{y}\) 来告诉这是一只猫的图片的机率有多大。\(\mathbf{X}\) 是一个 \(n_{x}\) 维的向量(相当于有 \(n_{x}\) 个特征的特征向量),现在用 \(w\) 来表示 逻辑回归的参数 ,这也是一个 \(n_{x}\) 维向量(因为实际上是 特征权重 ,维度与特征向量相同),参数里面还有 \(b\) ,这是一个实数(表示 偏差 )。在给出了输入 \(x\) 以及参数 \(w\) 和 \(b\) 之后, 可以尝试让 \(\hat{y} = w^{T}x + b\) 来计算 \(\hat{y}\) :

dfb5731c30b81eced917450d31e860a3.png

这时候得到的是一个关于输入 \(x\) 的线性函数,实际上这是在做线性回归时所用到的,但是这对于二元分类问题来讲并不准确,因为想让 \(\hat{y}\) 表示实际值 \(y\) 等于1的机率的话, \(\hat{y}\) 应该在0到1之间。这是一个需要解决的问题,因为 \(w^{T}x + b\) 可能比1要大得多,或者甚至为一个负值。对于想要的在0和1之间的概率来说它是没有意义的,因此在逻辑回归中,输出应该是等于由上面得到的线性函数式作为自变量的 sigmoid函数 中,公式如上图最下面所示,将线性函数转换为非线性函数

Sigmoid函数

下图是 \(\mathbf{sigmoid}\) 函数的图像,如果把水平轴作为 \(z\) 轴,那么关于的sigmoid函数是平滑地从0走向1(纵轴),曲线与纵轴相交的截距是 0.5 ,这就是 \(z\) 关于的 \(\mathbf{sigmoid}\) 函数的图像。通常都使用 \(z\) 来表示 \(w^{T}x + b\) 的值

7e304debcca5945a3443d56bcbdd2964.png

\(\mathbf{sigmoid}\) 函数的公式: \(\sigma(z) = \frac{1}{1 + e^{-z}}\) , \(z\) 在这里是一个实数

这里要说明一些值得注意的事情:

  • 如果 \(z\) 非常大,那么 \(e^{-z}\) 将会接近于0,而 \(\mathbf{sigmoid}\) 函数将会非常接近 1
  • 相反地,如果 \(z\) 非常小或者说是一个绝对值很大的负数,那么关于 \(e^{-z}\) 这项会变成一个很大的数,所以这个就接近于 0
因此当实现逻辑回归时,让机器学习参数应用这个函数使得成为对概率的一个很好的估计

符号惯例

这里介绍一种符号惯例,可以让参数 \(w\) 和参数 \(b\) 分开,而参数 \(b\) 对应的是一种偏置

f5049dc7ce815b495fbbdf71f23fc66c.png

比如在某些例子里,定义一个额外的特征称之为 \(x_0\) ,并且使它等于1,那么现在 \(\mathbf{X}\) 就是一个 \(n_x\) 加1维的变量,然后定义 \(\hat{y} = \sigma(\theta^{T} x)\) 的 \(\mathbf{sigmoid}\) 函数

在这个备选的符号惯例里,有一个参数向量 \(\theta_0, \theta_1, \ldots, \theta_{n_x}\) ,这样 \(\theta_0\) 就充当了 \(b\) ,这是一个实数,而剩下的 \(\theta_1\) 直到 \(\theta_{n_x}\) 充当了 \(w\)

结果就是当实现神经网络时,有一个比较简单的方法是保持 b和 w分开

现在已经知道逻辑回归模型是什么样子了,下一步要做的是训练参数和参数 以及如何 定义一个代价函数

代价函数

为了训练逻辑回归模型的参数 \(w\) 和参数 \(b\) ,需要一个代价函数。先看一下逻辑回归的输出函数:

\begin{aligned} \hat{y} = \sigma(w^{T}x + b), \textrm{where} \quad \sigma(z) = \frac{1}{1 + e^{-z}} \\ \textrm{Given} \quad \{(x^{(1)}, y^{(1)}), \ldots , (x^{(m)}, y^{(m)})\}, \textrm{want} \quad \hat{y}^{(i)} \approx y^{(i)} \end{aligned}

为了让模型通过学习调整参数,需要给予一个样本 \(m\) 的训练集,这会让你在训练集上找到参数 \(w\) 和参数 \(b\),,来得到你的输出函数

对训练集的预测值,将它写成 \(\hat{y}\) ,当然希望它会接近于训练集中的 \(y\) 值。需要说明上面的定义是对 一个训练样本 来说的,这种形式也使用于 每个 训练样本,使用这些 带有圆括号的上标 来区分 索引样本

  • 训练样本 \(i\) 所对应的预测值是 \(\hat{y}^{(i)}\) , 是用训练样本的 \(w^{T}x^{(i)} + b\) 然后通过 \(\mathbf{sigmoid}\) 函数来得到
  • 也可以把 \(z\) 定义为 \(z^{(i)} = w^{T}x^{(i)} + b\)
使用这个符号 (i) 上标来指明数据表示 x 或者 y或者 z 或者 其他数据的第 i 个训练样本,这就是上标的含义

损失函数

损失函数 Loss function 又叫做 误差函数 ,用来衡量算法的运行情况: \(\mathbf{L}(\hat{y}, y)\) 。通过这个称为 \(\mathbf{L}\) 的损失函数,来衡量预测输出值和实际值有多接近

一般损失函数用预测值和实际值的平方差或者它们平方差的一半,但是通常在逻辑回归中不这么做

因为在学习逻辑回归参数的时候,会发现优化目标不是凸优化,只能找到多个局部最优值,梯度下降法很可能找不到全局最优值

虽然平方差是一个不错的损失函数,但是在逻辑回归模型中会定义另外一个损失函数

在逻辑回归中用到的损失函数是:\(\mathbf{L}(\hat{y}, y) = -y\log{\hat{y}} - (1 - y)\log{(1-\hat{y})}\) . 为了更好地理解这个损失函数怎么起作用,举两个例子:

  1. 当 \(y = 1\) 时损失函数 \(\mathbf{L} = -\log{\hat{y}}\) ,如果想要损失函数 \(\mathbf{L}\) 尽可能的小,那么 \(\hat{y}\) 就要尽可能大,因为 \(\mathbf{sigmoid}\) 函数取值是 \([0, 1]\) ,所以 \(\hat{y}\) 会无限接近于1
  2. 当 \(y =0\) 时损失函数 \(\mathbf{L} = -\log{(1-\hat{y})}\) ,如果想要损失函数 \(\mathbf{L}\) 尽可能得小,那么 \(\hat{y}\) 就要尽可能小,因为 \(\mathbf{sigmoid}\) 函数取值是 \([0, 1]\) ,所以 \(\hat{y}\) 会无限接近于0

有很多的函数效果和现在这个类似,就是如果 \(y\) 等于1,就尽可能让 \(\hat{y}\) 变大,如果 \(y\) 等于0,就尽可能让 \(\hat{y}\) 变小

代价函数

损失函数是在单个训练样本中定义的,它衡量的是算法在单个训练样本中表现如何,为了衡量算法在全部训练样本上的表现如何,需要定义一个算法的 代价函数 ,对 \(m\) 个样本的损失函数求和然后除以 \(m\) : \(\mathbf{J}(w, b) = \frac{1}{m} \cdot \sum_{i=1}^{m} \mathbf{L}\Big(\hat{y}^{(i)}, y^{(i)}\Big) = \frac{1}{m} \cdot \sum_{i=1}^{m} \Big(-y\log{\hat{y}} - (1 - y)\log{(1-\hat{y})}\Big)\)

损失函数只适用于像这样的单个训练样本,而代价函数是参数的总代价

所以在训练逻辑回归模型时候,需要找到合适的 w 和 b ,来让代价函数 J 的总代价降到最低

逻辑回归实际上可以看做是一个非常小的神经网络

梯度下降法

形象化说明

a3c81d2c8629d674141def47dc02f312.jpg

在这个图中,横轴表示空间参数 \(w\) 和 \(b\)

在实践中,可以是更高的维度,但是为了更好地绘图,我们定义 w 和 b 都是单一实数

代价函数(成本函数)\(\mathbf{J}(w, b)\) 是在水平轴 \(w\) 和 \(b\) 上的曲面,因此曲面的高度就是在某一点的函数值。所做的就是找到使得代价函数(成本函数) \(\mathbf{J}(w, b)\) 函数值是最小值时所对应的参数 \(w\) 和 \(b\)

236774be30d12524a2002c3c484d22d5.jpg

上图的代价函数(成本函数) \(\mathbf{J}(w, b)\) 是一个 凸函数 convex function ,像一个大碗一样

af11ecd5d72c85f777592f8660678ce6.jpg

这与刚才的图有些相反,因为它是非凸的并且有很多不同的局部最小值

由于逻辑回归的代价函数(成本函数)特性,必须定义代价函数(成本函数)J(w, b) 为凸函数

过程

1. 初始化 \(w\) 和 \(b\)

1b79cca8e1902f0ee24b4eb966755ddd.jpg

可以用如图那个小红点来初始化参数 \(w\) 和 \(b\) ,也可以采用随机初始化的方法

对于逻辑回归几乎所有的初始化方法都有效

因为函数是凸函数,无论在哪里初始化,应该达到同一点或大致相同的点

0ad6c298d0ac25ca9b26546bb06d462c.jpg

现在以第二个图的小红点的坐标来初始化参数 \(w\) 和 \(b\)

2. 朝最陡的下坡方向走一步,不断地迭代

朝最陡的下坡方向走一步,走到了如图中第二个小红点处:

bb909b874b2865e66eaf9a5d18cc00e5.jpg

可能停在这里也有可能继续朝最陡的下坡方向再走一步,如图,经过两次迭代走到第三个小红点处

c5eda5608fd2f4d846559ed8e89ed33c.jpg

3. 直到走到全局最优解或者接近全局最优解的地方

通过以上的三个步骤可以找到全局最优解,也就是代价函数(成本函数) \(\mathbf{J}(w, b)\) 这个凸函数的最小值点

说明

仅有一个参数

5300d40870ec58cb0b8162747b9559b9.jpg

假定代价函数(成本函数) {J}(w) 只有一个参数 w,即用一维曲线代替多维曲线,这样可以更好画出图像

所谓 迭代 就是不断重复公式 \(w := w - \alpha \frac{\mathrm{d} \mathbf{J}(w)}{\mathrm{d} w}\) :

  • \(:=\) 表示更新参数
  • \(\alpha\) 表示 学习率 learning rate ,用来控制 步长 step
    • 向下走一步的长度: \(\frac{\mathrm{d} \mathbf{J}(w)}{\mathrm{d} w}\) 是函数 \(\mathbf{J}(w)\) 对 \(w\) 求导 derivative

      在代码中会使用 dw 表示这个结果
      

对于导数更加形象化的理解就是 斜率 slope ,如图该点的导数就是这个点相切于 \(\mathbf{J}(w)\) 的小三角形的高除宽。假设以如图点为初始化点,该点处的斜率的符号是正的,即 \(\frac{\mathrm{d} \mathbf{J}(w)}{\mathrm{d} w} > 0\) ,所以接下来会向左走一步:

4fb3b91114ecb2cd81ec9f3662434d81.jpg

整个梯度下降法的迭代过程就是不断地向左走,直至逼近最小值点:

579fb3957063480420c6a7d294503e97.jpg

假设以如图点为初始化点,该点处的斜率的符号是负的,即 \(\frac{\mathrm{d} \mathbf{J}(w)}{\mathrm{d} w} < 0\) ,所以接下来会向右走一步:

21541fc771ad8895c18d292dd4734fe7.jpg

整个梯度下降法的迭代过程就是不断地向右走,即朝着最小值点方向走

两个参数

逻辑回归的代价函数(成本函数) \(\mathbf{J}(w, b)\) 是含有两个参数的:

\begin{aligned} w := w - \alpha \frac{\partial \mathbf{J}(w, b)}{\partial w} \\ b := b - \alpha \frac{\partial \mathbf{J}(w, b)}{\partial b} \end{aligned}

\(\frac{\partial \mathbf{J}(w, b)}{\partial w}\) 就是函数 \(\mathbf{J}(w, b)\) 对 \(w\) 求偏导

在代码中会使用 dw 表示这个结果

\(\frac{\partial \mathbf{J}(w, b)}{\partial b}\) 就是函数 \(\mathbf{J}(w, b)\) 对 \(b\) 求偏导

在代码中会使用 db 表示这个结果

计算图

一个神经网络的计算,都是按照前向或反向传播过程组织的:

  1. 计算 出一个新的网络的 输出 前向过程
  2. 进行一个 反向传输 操作,用来计算出对应的 梯度导数

计算图 解释了为什么用这种方式组织这些计算过程

举一个例子说明计算图是什么

这里用到了一个比逻辑回归更加简单的,或者说不那么正式的神经网络的例子

尝试计算函数 \(\mathbf{J}\) ,是由三个变量 \(a\), \(b\), \(c\) 组成的函数,这个函数是 \(3(a + bc)\) 。计算这个函数实际上有三个不同的步骤:

  1. 首先是计算 \(b\) 乘以 \(c\) ,把它储存在变量 \(u\) 中 ,因此 \(u = bc\)
  2. 计算 \(v = a + u\)
  3. 输出 \(J = 3a\)

这就是要计算的函数。可以把这三步画成如下的计算图

5216254e20325aad2dd51975bbc70068.png

先在这画三个变量 \(a\), \(b\), \(c\) :

  1. 计算 \(u = bc\) ,在这周围放个矩形框,它的输入是 \(b\), \(c\)
  2. 接着 \(v = a + u\)
  3. 最后 \(J = 3a\)
举个例子:  a = 5, b = 3, c = 2

u = bc 就是6, ,v = a + u 就是5+6=11, J 是3倍的 u ,因此即 3 * (5 + 3*2),如果把它算出来,实际上得到33就是的值

当有不同的或者一些特殊的输出变量时,例如本例中的 J 和逻辑回归中想优化的代价函数 J,用计算图用来处理这些计算会很方便

从这个小例子中可以看出,通过一个从左向右的过程,可以计算出的输出值

为了计算导数,从右到左(红色箭头,和蓝色箭头的过程相反)的过程是用于计算导数最自然的方式

概括一下:计算图组织计算的形式是用蓝色箭头从左到右的计算

接下来看下如何进行反向红色箭头(也就是从右到左)的导数计算

使用计算图求导数

上面看了一个例子使用流程计算图来计算函数J

现在清理一下流程图的描述,看看如何利用它计算出函数的导数

使用到的公式:

\(\frac{\mathrm{d} J}{\mathrm{d} u} = \frac{\mathrm{d} J}{\mathrm{d} v} \frac{\mathrm{d} v}{\mathrm{d} u}\) , \(\frac{\mathrm{d} J}{\mathrm{d} b} = \frac{\mathrm{d} J}{\mathrm{d} u} \frac{\mathrm{d} u}{\mathrm{d} b}\), \(\frac{\mathrm{d} J}{\mathrm{d} a} = \frac{\mathrm{d} J}{\mathrm{d} u} \frac{\mathrm{d} u}{\mathrm{d} a}\)

这是一个流程图:

b1c9294420787ec6d7724d64ed9b4a43.png

假设要计算 \(\frac{\mathrm{d} J}{\mathrm{d} v}\) ,那要怎么算呢?比如说,把这个 \(v\) 值拿过来,改变一下,那么 \(J\) 的值会怎么变呢?定义上 \(J = 3v\) ,现在 \(v = 11\) ,如果让 \(v\) 增加一点点,比如到11.001,那么 \(J = 3v = 33.003\) ,最终结果是 \(J\) 上升到原来的3倍,所以 \(\frac{\mathrm{d} J}{\mathrm{d} v} = 3\) ,因为对于任何 \(v\) 的增量 \(J\) 都会有3倍增量

44c62688d05844b26599653545d24dd4.png

在反向传播算法中的术语:

如果想计算最后输出变量的导数,使用最关心的变量对的导数,那么就做完了一步反向传播

在这个流程图中是一个反向步骤

44c62688d05844b26599653545d24dd4.png

来看另一个例子, \(\frac{\mathrm{d} J}{\mathrm{d} a}\) 是多少呢?换句话说,如果提高 \(a\) 的数值,对 \(J\) 的数值有什么影响?变量 \(a = 5\) ,让它增加到5.001,那么对v的影响就是 \(a + v\) ,之前 \(v = 11\) ,现在变成11.001,\(J\) 就变成33.003了,所以如果让 \(a\) 增加0.001,\(J\) 增加0.003。这意味传播到流程图的最右 \(J\) 的增量是3乘以 \(a\) 的增量,也就说导数是3

d4f37c1db52a999dd68b89564449669f.png

一种解释这个计算过程的方式是:如果你改变了 \(a\) ,那么也会改变 \(v\) ,通过改变 \(v\) ,也会改变 \(J\) 。当提升这个值 \(a\) 一点点(0.001) , \(J\) 变化量是0.003

7e9ea7f52cab1a428aa1fb670fbe54e9.png

首先 \(a\) 增加了, \(v\) 也会增加, \(v\) 增加多少呢?这取决于 \(\frac{\mathrm{d} v}{\mathrm{d} a}\) ,然后 \(v\) 的变化导致 \(J\) 也在增加。如果 \(a\) 影响到 \(v\) ,\(v\) 影响到 \(J\) ,那么让 \(a\) 变大时, \(J\) 的变化量就是当改变 \(a\) 时, \(v\) 的变化量乘以 改变 \(v\) 时 \(J\) 的变化量,在微积分里这叫 链式法则

eccab9443ace5d97b50ec283a8f85ba8.png

从这个计算中看到,如果让 \(a\) 增加0.001, \(v\) 也会变化相同的大小,所以 \(\frac{\mathrm{d} v}{\mathrm{d} a} = 1\) 。事实上,如果代入进去之前算过 \(\frac{\mathrm{d} J}{\mathrm{d} v} = 3\) ,所以两者乘积 \(3 \times 1\) ,实际上就给出了正确答案 \(\frac{\mathrm{d} J}{\mathrm{d} a} = 3\)

960127d6727a198511fe59459bf4a724.png

这图表示了如何计算 \(\frac{\mathrm{d} J}{\mathrm{d} v}\) ,就是 \(J\) 对变量 \(v\) 的导数,它可以帮助计算 \(\frac{\mathrm{d} J}{\mathrm{d} a}\) ,这是另一步反向传播计算

符号约定

当编程实现反向传播时,通常会有一个 最终输出值 是要关心的,最终的输出变量,真正想要关心或者说优化的。在这种情况下最终的输出变量是 \(J\) ,就是流程图里最后一个符号,所以有很多计算尝试计算输出变量的导数,所以输出变量对某个变量的导数,就用 \(d_{var}\) 命名,还有各种中间变量比如 \(a, b, c, u, v\) ,当在软件里实现的时候,导数的变量名叫什么?在 python 中,可以写一个很长的变量名,比如 \(d_{FinalOutputVarDvar}\) ,但这个变量名有点长,就用 \(dJ_{var}\) ,但因为是一直对 \(J\) 求导,在代码里,就使用变量名 \(dvar\) ,来表示那个 \(J\) 对 \(var\) 的导数

88f92a16ca8cd36f8252c5c6db9c980b.png

这是使用新符号后的计算图,其中 \(dv = 3\) , \(da = 3\)

22e98c374e2aaa999f46d27339ce6720.png

目前为止,一直在往回传播,并计算出 \(dv = 3\) , \(dv\) 是代码里的变量名,其真正的定义是 \(\frac{\mathrm{d} J}{\mathrm{d} v}\) 。同样的 \(da =3\) ,\(da\) 是代码里的变量名,其实代表 \(\frac{\mathrm{d} J}{\mathrm{d} a}\) 的值

74a169313e532a2c953ea01b05d57385.png

现在看变量 \(u\) ,那么 \(\frac{\mathrm{d} J}{\mathrm{d} u}\) 是多少呢?通过和之前类似的计算,现在从 \(u = 6\) 出发,如果令 \(u\) 增加到6.001,之前 \(v\) 是11,现在变成11.001了, \(J\) 就从33变成33.003,所以 \(J\) 增量是3倍,所以 \(\frac{\mathrm{d} J}{\mathrm{d} u} =3\) 。对 \(u\) 的分析很类似对 \(a\) 的分析,实际上这计算起来就是 \(\frac{\mathrm{d} J}{\mathrm{d} v} \cdot \frac{\mathrm{d} v}{\mathrm{d} u}\) ,有了这个,可以算出, \(\frac{\mathrm{d} J}{\mathrm{d} v} = 3 , \frac{\mathrm{d} v}{\mathrm{d} u} = 1\) ,最终算出结果是 \(3 \times 1 = 3\)

596ea4f3492f8e96ecd560e3899e2700.png

接下来一个例子是 \(\frac{\mathrm{d} J}{\mathrm{d} b}\) ,使用微积分链式法则,这可以写成两者的乘积,就是 \(\frac{\mathrm{d} J}{\mathrm{d} u} \cdot \frac{\mathrm{d} u}{\mathrm{d} b}\) 。 \(u\) 的定义是 \(b \cdot c\) ,所以 \(b = 3\) 时这是6,现在就变成6.002了,因为在例子中 \(c = 2\) ,所以这表明 \(\frac{\mathrm{d} u}{\mathrm{d} b} = 2\) 当让 \(b\) 增加0.001时, \(u\) 就增加两倍。前面已经计算过: \(\frac{\mathrm{d} J}{\mathrm{d} u} = 3\) ,因此 \(\frac{\mathrm{d} J}{\mathrm{d} b} = \frac{\mathrm{d} J}{\mathrm{d} u} \cdot \frac{\mathrm{d} u}{\mathrm{d} b} = 3 \times 2 = 6\)

c56e483eaebb9425af973bb9849ad2e4.png

下图是计算 \(du = 6\) 的反向计算图:

c933c8d935a95e66bbf6a61adecbb816.png

同样可以计算出 \(\frac{\mathrm{d} J}{\mathrm{d} c} = \frac{\mathrm{d} J}{\mathrm{d} u} \cdot \frac{\mathrm{d} u}{\mathrm{d} c} = 3 \times 3 = 9\) , 可以推出 \(dc = 9\)

c933c8d935a95e66bbf6a61adecbb816.png

当计算所有这些导数时,最有效率的办法是从右到左计算,跟着这个红色箭头走。特别是当第一次计算对 \(v\) 的导数时,之后在计算对 \(a\) 导数就可以用到。然后对 \(u\) 的导数,可以帮助计算对 \(b\) 和 \(c\) 的导数:

90fd887adacd062cc60be1f553797fab.png

总结:一个计算流程图,就是 正向 从左到右 的计算来计算 代价函数 \(\mathbf{J}\) ,然后 反向 从右到左 计算 导数

逻辑回归中的梯度下降

怎样通过计算偏导数来实现逻辑回归的梯度下降算法?它的关键点是几个重要公式,其作用是用来实现逻辑回归中梯度下降算法

但接下来先使用计算图对梯度下降算法进行计算

必须要承认的是,使用计算图来计算逻辑回归的梯度下降算法有点大材小用了

但是,以这个例子作为开始来讲解,可以更好的理解背后的思想,更深刻而全面地理解神经网络

假设样本只有两个特征 \(x_1\) 和 \(x_2\) ,为了计算 \(z\) ,需要输入参数 \(w_1\) , \(w_2\) 和 \(b\) 。因此 \(z\) 的计算公式为: \(z = w_{1}x_{1} + w_{2}x_{2} + b\)

回想一下逻辑回归的公式定义如下:

  • \(\hat{y} = \sigma(z)\) 其中 \(z = w^{T}x + b\) , \(\sigma(z) = \frac{1}{1 + e^{-z}}\)
  • 损失函数: \(\mathbf{L}(\hat{y}, y) = -y\log{\hat{y}} - (1 - y)\log{(1-\hat{y})}\)
  • 代价函数: \(\mathbf{J}(w, b) = \frac{1}{m} \cdot \sum_{i=1}^{m} \mathbf{L}\Big(\hat{y}^{(i)}, y^{(i)}\Big)\)

假设现在只考虑单个样本的情况: 单个样本的代价函数 定义如下:\(\mathbf{L}(a, y) = - \Big(y\log{a} + (1-y)\log{(1-a)}\Big)\) 其中 \(a\) 是 逻辑回归的输出 , \(y\) 是 样本的标签值

现在画出表示这个计算的计算图。 这里先复习下梯度下降法,\(w\) 和 \(b\) 的修正量可以表达如下:

\begin{aligned} w := w - \alpha \frac{\partial \mathbf{J}(w, b)}{\partial w} \\ b := b - \alpha \frac{\partial \mathbf{J}(w, b)}{\partial b} \end{aligned}

03f5f96177ab15d5ead8298ba50300ac.jpg

先在这个公式的外侧画上长方形。然后计算 \(\hat{y} = \sigma(z)\) : 也就是计算图的下一步。最后计算损失函数 \(\mathbf{L}(a, y)\)

为了使得逻辑回归中最小化代价函数 L(a,y) ,需要做的仅仅是修改参数 w 和 b的值

接下来来讨论通过反向计算出导数

因为想要计算出的代价函数 \(\mathbf{L}\) 的导数,首先需要反向计算出代价函数 \(\mathbf{L}\) 关于 \(a\) 的导数,在编写代码时,只需要用 \(da\) 来表示 \(\frac{\mathrm{d} L(a, y)}{\mathrm{d} a}\) 。 通过微积分得到: \(\frac{\mathrm{d} L(a, y)}{\mathrm{d} a} = -\frac{y}{a} + \frac{(1-y)}{(1-a)}\)

如果不熟悉微积分,也不必太担心,会列出本课程涉及的所有求导公式

那么如果非常熟悉微积分,鼓励你主动推导前面介绍的代价函数的求导公式

在编写 Python 代码时,只需要用 \(dz\) 来表示代价函数 \(\mathbf{L}\) 关于 \(z\) 的导数 \(\frac{\mathrm{d} L}{\mathrm{d} z}\) ,也可以写成 \(\frac{\mathrm{d} L(a, y)}{\mathrm{d} z}\) ,这两种写法都是正确的。因为 \(\frac{\mathrm{d} L(a, y)}{\mathrm{d} z} = \frac{\mathrm{d} L}{\mathrm{d} z} = \frac{\mathrm{d} L}{\mathrm{d} a} \cdot \frac{\mathrm{d} a}{\mathrm{d} z}\) , 并且 \(\frac{\mathrm{d} a}{\mathrm{d} z} = a \cdot (1 - a)\) , 而 \(\frac{\mathrm{d} L}{\mathrm{d} a} = -\frac{y}{a} + \frac{(1-y)}{(1-a)}\) ,将这两项相乘,得到:\(\frac{\mathrm{d} L}{\mathrm{d} z} = (-\frac{y}{a} + \frac{(1-y)}{(1-a)}) \cdot (a \cdot (1 - a)) = a -y\)

现在进行最后一步反向推导,也就是计算 \(w\) 和 \(b\) 变化对代价函数 \(\mathbf{J}\) 的影响:

\begin{aligned} \mathrm{d}w_{1} = \frac{1}{m}\sum_{i}^{m}x_{1}^{(i)}(a^{(i)} - y^{(i)}) \\ \mathrm{d}w_{2} = \frac{1}{m}\sum_{i}^{m}x_{2}^{(i)}(a^{(i)} - y^{(i)}) \\ \mathrm{d}b = \frac{1}{m}\sum_{i}^{m}(a^{(i)} - y^{(i)}) \end{aligned}

特别地 \(\mathrm{d} w_1\) 表示 \(\frac{\partial \mathbf{L}}{\partial w_1} = x_1 \cdot \mathrm{d} z\) , \(\mathrm{d} w_2\) 表示 \(\frac{\partial \mathbf{L}}{\partial w_2} = x_2 \cdot \mathrm{d} z\) , \(\mathrm{d} b = \mathrm{d} z\)

6403f00e5844c3100f4aa9ff043e2319.jpg

因此,关于单个样本的梯度下降算法,所需要做的就是如下的事情:

  1. 使用公式计算 \(\mathrm{d} z = a - y\) 计算 \(\mathrm{d} z\)
  2. 使用 \(\mathrm{d} w_1 = x_1 \cdot \mathrm{d} z\) 计算 \(\mathrm{d} w_1\)
  3. 使用 \(\mathrm{d} w_2 = x_2 \cdot \mathrm{d} z\) 计算 \(\mathrm{d} w_2\)
  4. 使用 \(\mathrm{d} b = \mathrm{d} z\) 计算 \(\mathrm{d} b\)
  5. 更新 \(w_1 = w_1 - \alpha\mathrm{d} w_1\)
  6. 更新 \(w_2 = w_2 - \alpha\mathrm{d} w_2\)
  7. 更新 \(b = b - \alpha\mathrm{d} b\)
这就是关于单个样本实例的梯度下降算法中参数更新一次的步骤 

但是,训练逻辑回归模型不仅仅只有一个训练样本,而是有 m 个训练样本的整个训练集

m 个样本的梯度下降

首先,时刻记住有关于损失函数 \(\mathbf{J}(w, b)\) 的定义:\(\mathbf{J}(w, b) = \frac{1}{m}\sum_{i=1}^{m}\mathbf{L}(a^{(i)}, y^{(i)})\)

bf930b1f68d8e0726dda5393afc83672.png

  1. 关于样本 \(y\) 的 \(a^{(i)}\) 是单个训练样本的预测值
  2. \(\mathrm{d} w_{1}^{(i)}\) , \(\mathrm{d} w_{2}^{(i)}\) , \(\mathrm{d} b^{(i)}\) 也是对单个样本的梯度估算

现在对于求和的全局代价函数,实际上是1到 \(m\) 项各个损失的平均。 所以它表明全局代价函数对 \(w_1\) 的微分 也同样是各项损失对 \(w_1\) 微分的平均

8b725e51dcffc53a5def49438b70d925.png

真正需要做的是计算这些微分,如同之前的单个训练样本上做的。然后求平均,这会给出 *全局梯度值*,并能够把它直接应用到 梯度下降算法

尽管这里有很多细节,但可以把这些装进一个具体的算法

同时需要一起应用的就是逻辑回归和梯度下降

下面给出如何进行一步梯度下降。因此需要重复以上步骤很多次,以应用多次梯度下降

J=0;dw1=0;dw2=0;db=0;

for i = 1 to m
    z(i) = wx(i)+b;
    a(i) = sigmoid(z(i));
    J += -[y(i)log(a(i))+(1-y(i))log(1-a(i));
    dz(i) = a(i)-y(i);
    dw1 += x1(i)dz(i);
    dw2 += x2(i)dz(i);
    db += dz(i);

J /= m;
dw1 /= m;
dw2 /= m;
db /= m;
w = w - alpha*dw
b = b - alpha*db

这种计算中有缺点,也就是说应用此方法在逻辑回归上需要编写两个 for 循环:

  1. 第一个 for 循环是一个小循环遍历 \(m\) 个训练样本
  2. 第二个 for 循环是一个遍历所有特征的循环。这个例子中我们只有2个特征,所以 \(n\) 等于2并且 \(n_x\) 等于2。 但如果有更多特征,会有相似的计算从 \(\mathrm{d} w_1\) 一直下去到 \(\mathrm{d} w_{n_{x}}\)
当应用深度学习算法,会发现在代码中显式地使用for循环使你的算法很低效

同时在深度学习领域会有非常大的数据集

所以能够使算法没有显式的for循环会是重要的

这里有一个叫做 向量化 技术,它可以允许代码摆脱这些显式的for循环

在先于深度学习的时代,也就是深度学习兴起之前,向量化是很棒的。可以加速你的运算,但有时候也未必能够

但是在深度学习时代向量化,向量化已经变得相当重要

向量化

向量化是非常基础的去除代码中for循环的艺术,下面举例说明什么是向量化

在逻辑回归中需要计算 \(z=w^Tx + b\) ,\(w\) 和 \(b\) 都是 列向量 。因为 \(w \in \mathbb{R}^{n_x}\) , \(x \in \mathbb{R}^{n_x}\) 如果有很多的特征那么就会有一个非常大的向量 ,所以如果想使用非向量化方法 \(w^Tx\) 去计算,需要用如下方式:

​x
z=0
​
for i in range(n_x)
​    z += w[i]*x[i]
​
z += b
这是一个非向量化的实现,会发现这真的很慢

作为一个对比,向量化实现将会非常直接计算,代码如下:

import numpy as np

z = np.dot(w, x) + b 

e9c7f9f3694453c07fe6b9fe7cf0c4c8.png

有人说:“大规模的深度学习使用了GPU或者图像处理单元实现”,但是实际上 CPU和GPU都有并行化的指令

CPU也有会叫做SIMD指令,这个代表了一个单独指令多维数据

如果使用了像np.function或者并不要求实现循环的函数,它可以充分利用并行化计算

事实是相对而言 GPU更加擅长SIMD计算,因为现代GPU 内置了数以千计的小的浮点运算单元

重要的是:无论什么时候在可能的情况下避免使用显示的for循环 

一个更具体的例子:

import numpy as np #导入numpy库

a = np.array([1,2,3,4]) #创建一个数据a
print(a)
# [1 2 3 4]

import time #导入时间库
a = np.random.rand(1000000)
b = np.random.rand(1000000) #通过round随机得到两个一百万维度的数组
tic = time.time() #现在测量一下当前时间
#向量化的版本
c = np.dot(a,b)
toc = time.time()
print(“Vectorized version:” + str(1000*(toc-tic)) +”ms”) #打印一下向量化的版本的时间#继续增加非向量化的版本
c = 0
tic = time.time()
for i in range(1000000):
    c += a[i]*b[i]
    toc = time.time()
    print(c)
    print(“For loop:” + str(1000*(toc-tic)) + “ms”)#打印for循环的版本的时间
在两个方法中,向量化和非向量化计算了相同的值

向量化版本花费了1.5毫秒,非向量化版本的for循环花费了大约几乎500毫秒,非向量化版本多花费了300倍时间

所以在这个例子中,仅仅是向量化代码,就会运行300倍快

这意味着如果向量化方法需要花费一分钟去运行的数据,for循环将会花费5个小时去运行

更多例子

如果想计算向量 \(u = Av\) ,矩阵乘法的定义就是:\(u_i = \sum_jA_{ij}v_i\) 使用非向量化实现

  1. 初始化 \(u = np.zeros(n, 1)\)
  2. 通过两层循环 \(for(i) : for(j) :\) ,得到 \(u[i] = u[i] + A[i][j] * v[j]\)
现在就有了 i 和 j 的两层循环,这就是非向量化

向量化方式就可以用 \(u = np.dot(A, v)\) ,使得代码运行速度更快

a22eb5a73d2d45b342810eee70a4620c.png

事实上,numpy库有很多向量化函数,比如:

  • \(u=np.log(v)\) : 计算对数函数(log)
  • \(u = np.exp(v)\) : 计算指数操作
  • \(np.abs()\) : 计算数据的绝对值
  • \(np.maximum()\) : 计算元素中的最大值
    • 也可以 \(np.maximum(v,0)\)
  • \(v ** 2\) 代表获得元素 每个值得平方
  • \(\frac{1}{v}\) 获取每个元素的倒数
  • …….
所以当想写循环时候,检查numpy是否存在类似的内置函数,从而避免使用循环(loop)方式

6a7d76b4a66ef5af71d55b8f980a5ab2.png

那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看是否能简化两个计算过程中的某一步

逻辑回归的求导代码,有两层循环:

  1. 如果有超过两个特征时,需要循环 \(\mathrm{d} w_1\) , \(\mathrm{d} w_2\) , \(\mathrm{d} w_3\) 等。所以 \(j\) 的实际值是1, 2 和 \(n_x\) 。如果想要消除第二循环,首先不用初始化 \(\mathrm{d} w_1\) , \(\mathrm{d} w_2\) 都等于0,而是定义 \(\mathrm{d} w\) 为一个向量,设置 \(dw = np.zeros(nx, 1)\) ,定义了一个 \(n_x\) 行的一维向量,从而替代循环
  2. 使用了一个向量操作 \(\mathrm{d} w = \mathrm{d} w + x^{(i)}\mathrm{d} z^{(i)}\)
  3. 最后 \(\mathrm{d} w = \frac{\mathrm{d} w}{m}\)

e28b7cda6504e2a8ff0f0b2f1e258a96.png

虽然通过将两层循环转成一层循环,但仍然还有一个for循环来训练样本

接下来需要解决这个问题

向量化逻辑回归

前面讨论过向量化是如何显著加速代码,接下来将讨论如何实现逻辑回归的向量化计算

这样就能处理整个数据集,甚至不会用一个明确的for循环就能实现对于整个数据集梯度下降算法的优化

如果你有 \(m\) 个训练样本:

  1. 对第一个样本进行预测:
    • 使用这个熟悉的公式 \(z^{(1)} = w^Tx^{(1)} + b\) 计算 \(z\)
    • 计算激活函数 \(a^{(1)} = \sigma(z^{(1)})\)
    • 计算第一个样本的预测值 \(y^{(1)}\)
  2. 对第二个样本进行预测,需要计算 \(z^{(2)} = w^Tx^{(2)} + b\) , \(a^{(2)} = \sigma(z^{(2)})\) , \(y^{(2)}\)
  3. 对第三个样本进行预测,需要计算 \(z^{(3)} = w^Tx^{(3)} + b\) , \(a^{(3)} = \sigma(z^{(3)})\) , \(y^{(3)}\)
  4. 依次类推
如果有 m 个训练样本,可能需要这样做  m 次

有没有办法可以并且不需要任何一个明确的for循环完成向前传播? 

首先,曾经定义了一个矩阵 \(\mathbf{X}\) 作为 训练输入 (如下图中蓝色) 像这样在不同的列中堆积在一起。这是一个 \(n_x\) 行 \(m\) 列的矩阵。如果把它写为 numpy 的形式 \((n_x, m)\) ,这表示 \(\mathbf{X}\) 是一个 \(n_{x}\) 乘以 \({m}\) 的矩阵 \(\mathbb{R}^{n_x \times m}\)

3a8a0c9ed33cd6c033103e35c26eeeb7.png

首先在一个步骤中计算 \(z_1, z_2, \ldots z_m\) :

  1. 先构建一个 \(1 \times m\) 的矩阵,实际上它是一个 行向量
  2. 同时准备计算 \(z^{(1)}\), \(z^{(2)}\) 一直到 \(z^{(m)}\) ,所有值都是在同一时间内完成

实际上计算结果可以表达为 \(w\) 的转置乘以大写矩阵 \(\mathbf{X}\) 然后加上向量 \([b, b, \ldots, b]\) , 也就是说 \([z^{(1)}, z^{(2)}, \ldots, z^{(m)}] = w^T \cdot \mathbf{X} + [b, b, \ldots, b]\) , 其中 \([b, b, \ldots, b]\) 是一个 \(1\times m\) 的向量或者 \(1\times m\) 的矩阵或者是一个 \(m\) 维的行向量。如果熟悉矩阵乘法,会发现的 \(w\) 转置乘以 \(x^{(1)}\) , \(x^{(2)}\) 一直到 \(x^{(m)}\) 。然后加上 \([b, b, \ldots, b]\) ,实际上将 \(b\) 加到了每个元素上。最终得到了另一个 \(1 \times m\) 的向量:

\([z^{(1)}, z^{(2)}, \ldots, z^{(m)}] = w^T \cdot \mathbf{X} + [b, b, \ldots, b] = [w^Tx{(1)} + b, w^Tx{(2)} + b, \ldots , w^Tx{(m)} + b]\) , 而 \(w^Tx{(1)} + b\) 第一个元素, \(w^Tx{(2)} + b\) 是第二个元素, \(w^Tx{(m)} + b\) 是第 \(m\) 个元素

第一个元素恰好是 \(z^{(1)}\) 的定义,第二个元素恰好是 \(z_{(2)}\) 的定义 …. 另外 \(\mathbf{X}\) 是一次获得的,在得到的训练样本,一个一个横向堆积起来。类似地也将 \([z^{(1)}, z^{(2)}, \ldots, z^{(m)}]\) 定义为大写的 \(\mathbf{Z}\) 。为了计算 \(w^T \cdot \mathbf{X} + [b, b, \ldots, b]\) , numpy 代码只需要 \(Z = np.dot(w.T, X) + b\)

这里在Python中有一个巧妙的地方,这里 b 是一个实数,或者你可以说是一个 1 X 1 矩阵,只是一个普通的实数

但是将这个向量加上这个实数时,Python自动把这个实数 b 扩展成一个 1 X m 的行向量,这被称作广播 (brosdcasting)

它只用一行代码,用这一行代码,你可以计算Z,包含所有小写 z1 到 zm 的 1 X m 的矩阵

接下下来要做的就是找到一个同时计算 \([a^{(1)}, a^{(2)}, \ldots, a^{(m)}]\) 的方法。就像横向堆积小写 \(z\) 得到大写 \(\mathbf{Z}\) 一样,堆积小写变量 \(a\) 将形成一个新的变量,将它定义为大写 \(\mathbf{A}\) 。剩余地只需要编写一个支持向量化计算的 \(\mathbf{sigmoid}\) 函数:

\(\mathbf{A} = [a^{(1)}, a^{(2)}, \ldots, a^{(m)}] = \sigma(\mathbf{Z})\)

概括一下:已经学习了如何利用向量化在同一时间内高效地计算所有的激活函数的所有 a 值

接下来,也可以利用向量化高效地计算反向传播并以此来计算梯度

梯度输出

接下来将学习如何向量化地计算个 m 训练数据的梯度,并且实现一个非常高效的逻辑回归算法

讲梯度计算的时候,列举过几个例子 \(\mathrm{d} z^{(1)} = a^{(1)} - y^{(1)}, \mathrm{d} z^{(2)} = a^{(2)} - y^{(2)} \ldots\) , 等等一系列类似公式。现在,对 \(m\) 个训练数据做同样的运算,可以定义一个新的变量 \(\mathrm{d} \mathbf{Z} = [\mathrm{d} z^{(1)}, \mathrm{d} z^{(2)}, \dots, \mathrm{d} z^{(m)}]\) ,所有的 \(\mathrm{d} z\) 变量横向排列,因此,\(\mathrm{d} \mathbf{Z}\) 是一个 \(1 \times m\) 的矩阵,或者说,一个 \(m\) 维行向量:

  • 已经知道如何计算 \(\mathbf{A}\) ,即 \([a^{(1)}, a^{(2)}, \ldots, a^{(m)}]\)
  • 而行向量 \(\mathbf{Y} = [y^{(1)}, y^{(2)}, \ldots, y^{(m)}]\) 实际上就是标本里的实际结果横向堆积起来

由此,可以这样计算 \(\mathrm{d} \mathbf{Z} = \mathbf{A} - \mathbf{Y}\) ,第一个元素就是 \(\mathrm{d} z^{(1)}\) ,第二个元素就是 \(\mathrm{d} z^{(2)}\) ……

所以现在仅需一行代码,就可以同时完成对m个样本的 dz 计算

循环剩余部分:

\begin{aligned} & \mathrm{d} w = 0 \\ & \mathrm{d} w += x^{(1)} \cdot \mathrm{d} z^{(1)} \\ & \mathrm{d} w += x^{(2)} \cdot \mathrm{d} z^{(2)} \\ & \ldots\ldots \\ & \mathrm{d} w += x^{(m)} \cdot \mathrm{d} z^{(m)} \\ &\mathrm{d} w = \frac{\mathrm{d} w}{m} \\ & \\ & \mathrm{d} b = 0 \\ & \mathrm{d} b += \mathrm{d} z^{(1)} \\ & \mathrm{d} b += \mathrm{d} z^{(2)} \\ & \ldots\ldots \\ & \mathrm{d} b += \mathrm{d} z^{(m)} \\ & \mathrm{d} b = \frac{\mathrm{d} b}{m} \end{aligned}
  • 首先来看 \(\mathrm{d} b\) ,不难发现 \(\mathrm{d} b = \frac{1}{m} \sum_{i=1}^{m} \mathrm{d} z^{(i)}\) ,所以在 numpy 中很容易地想到 \(\mathrm{d}b = \frac{1}{m} \cdot np.sum(\mathrm{d} \mathbf{Z})\)
  • 接下来看 \(\mathrm{d} w\) 先写出它的公式 \(\mathrm{d} w = \frac{1}{m} \cdot (x^{(1)}\mathrm{d} z^{(1)} + x^{(2)}\mathrm{d} z^{(2)} + \ldots + x^{(m)}\mathrm{d} z^{(m)})\) 这等价于 \(\mathrm{d} w = \frac{1}{m} \cdot \mathbf{X} \cdot \mathrm{d} \mathbf{Z}^T\) , 其中 \(\mathbf{X}\) 是一个行向量
仅用两行代码进行计算就避免了在训练集上使用for循环

现在回顾下前面介绍过的逻辑回归算法:

505663d02e8120e30c3d8405f31a8497.jpg

向量化后:

\begin{aligned} & \mathbf{Z} = w^T\mathbf{X} + b = np.dot(w.T, X) + b\\ & \mathbf{A} = \sigma(\mathbf{Z}) \\ & \mathrm{d} \mathbf{Z} = \mathbf{A} - \mathbf{Y} \\ & \mathrm{d} w = \frac{1}{m} \cdot \mathbf{X} \cdot \mathrm{d} \mathbf{Z}^T \\ & \mathrm{d} b = \frac{1}{m} \cdot np.sum(\mathrm{d} \mathbf{Z}) \\ & w := w - \alpha \cdot \mathrm{d} w \\ & b := b - \alpha \cdot \mathrm{d} b \end{aligned}

前五个公式完成了 前向后向 传播,也实现了对所有训练样本进行 预测求导 ,再利用后两个公式,梯度下降 更新 参数

通过一次迭代实现一次梯度下降,但如果希望多次迭代进行梯度下降,那么仍然需要for循环,放在最外层

最后,得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法

损失函数的解释

前面已经分析了逻辑回归的损失函数表达式

接下来将给出一个简洁的证明来说明逻辑回归的损失函数为什么是这种形式 

5b658991f4c61d088aa962310cee99eb.png

在逻辑回归中,需要预测的结果,可以表示为 \(\hat{y} = \sigma(w^Tx + b)\) ,其中 \(\sigma(z) = \frac{1}{1 + e^{-z}}\) :

  • 根据约定 \(\hat{y} = p(y=1|x)\) ,即算法的输出 \(\hat{y}\) 是给定训练样本 \(x\) 条件下 \(y\) 等于1的概率。换句话说,如果 \(y = 1\) ,在给定训练样本 \(x\) 条件下 \(y = \hat{y}\)
  • 反过来说,如果 \(y = 0\) ,在给定训练样本条件下 \(y = 1 - \hat{y}\)

因此,如果 \(\hat{y}\) 代表 \(y = 1\) 的概率,那么 \(1 - \hat{y}\) 就是 \(y = 0\) 的概率。接下来,就来分析这两个条件概率公式:

\begin{aligned} & if \quad y = 1: \quad p(y|x) = \hat{y} \\ & if \quad y = 0: \quad p(y|x) = 1 - \hat{y} \end{aligned}
需要注意:这里讨论的是二分类问题的损失函数,因此 y 的取值只能是0或者1

把上诉概率公式合并成一个: \(p(y|x) = \hat{y}^{y} \cdot (1 - \hat{y})^{(1 - y)}\)

由于 \(\mathbf{log}\) 函数是 严格单调递增 的函数,最大化 \(\log(p(y|x))\) 等价于最大化 \(p(y|x)\) 再计算对数,通过对数函数化简为: \(\log(p(y|x)) = \log( \hat{y}^{y} \cdot (1 - \hat{y})^{(1 - y)}) = ylog\hat{y} + (1 - y)\log(1 - \hat{y})\)

而这就是前面提到的损失函数的负数 \(-\mathbf{L}(\hat{y}, y)\) ,负数的原因是训练学习算法时需要算法输出值的概率是最大的(以最大的概率预测这个值),然而在逻辑回归中需要最小化损失函数,因此最小化损失函数与最大化条件概率的对数正好相反,所以这就是单个训练样本的损失函数表达式

5a3c2208dfbc9dbcfac57fde07dfac0e.png

m 个训练样本的整个训练集中又该如何表示呢?

整个训练集中标签的概率,假设所有的训练样本服从同一分布且相互独立,也即 独立同分布 的,所有这些样本的联合概率就是每个样本概率的乘积:

\(\mathbf{P}(\text{labels in training set}) = \prod_{i = 1}^m\mathbf{P}(y^{(i)} | x^{(i)})\)

6ce9e585fab07fe9fc124dd2dc14bdbf.png

如果想做最大似然估计,需要寻找一组参数,使得给定样本的观测值概率最大,因为令这个概率最大化等价于令其对数最大化,在等式两边取对数:

\(\log{\big(\mathbf{P}(\text{labels in training set})\big)} = \log{\big(\prod_{i = 1}^m\mathbf{P}(y^{(i)} | x^{(i)})\big)} = \sum_{i = 1}^m\log{\mathbf{P}(y^{(i)} | x^{(i)})} = \sum_{i = 1}^m-\mathbf{L}(\hat{y}^{(i)}, y^{(i)})\)

在统计学里面,有一个方法叫做最大似然估计,即求出一组参数,使这个式子 取最大值,也就是说,使得这个式子 \(\sum_{i = 1}^m-\mathbf{L}(\hat{y}^{(i)}, y^{(i)})\) 取最大值,,把将负号移到求和符号的外面 \(-\sum_{i = 1}^m\mathbf{L}(\hat{y}^{(i)}, y^{(i)})\) ,这就推导出了前面给出的 逻辑回归的成本函数 \(\mathbf{J}(w, b) = \sum_{i = 1}^m\mathbf{L}(\hat{y}^{(i)}, y^{(i)})\)

6ce9e585fab07fe9fc124dd2dc14bdbf.png

由于训练模型时,目标是让成本函数最小化,所以不是直接用最大似然概率,而是去掉这里的负号,最后为了方便,可以对成本函数进行适当的缩放,就在前面加一个额外的常数因子 \(\frac{1}{m}\) ,即: \(\mathbf{J}(w, b) = \frac{1}{m} \cdot \sum_{i = 1}^m\mathbf{L}(\hat{y}^{(i)}, y^{(i)})\)

Next:浅层神经网络 Previous:神经网络引言 Home:神经网络