http://www.ai-start.com/dl2017/html/lesson1-week2.html
神经网络的编程基础(Basics of Neural Network programming)
二分类(Binary Classification)
我们来看看一张图片在计算机中是如何表示的,为了保存一张图片,需要保存三个矩阵,它们分别对应图片中的红、绿、蓝三种颜色通道,如果你的图片大小为64x64像素,那么你就有三个规模为64x64的矩阵,分别对应图片中红、绿、蓝三种像素的强度值。为了便于表示,这里我画了三个很小的矩阵,注意它们的规模为5x4 而不是64x64,如下图所示:
为了把这些像素值放到一个特征向量中,我们需要把这些像素值提取出来,然后放入一个特征向量
x
x
x。为了把这些像素值转换为特征向量
x
x
x,我们需要像下面这样定义一个特征向量
x
x
x 来表示这张图片,我们把所有的像素都取出来,例如255、231等等,直到取完所有的红色像素,接着最后是255、134、…、255、134等等,直到得到一个特征向量,把图片中所有的红、绿、蓝像素值都列出来。如果图片的大小为64x64像素,那么向量
x
x
x 的总维度,将是64乘以64乘以3,这是三个像素矩阵中像素的总量。在这个例子中结果为12,288。现在我们用
n
x
=
12
,
288
n_x=12,288
nx=12,288,来表示输入特征向量的维度,有时候为了简洁,我会直接用小写的
n
n
n来表示输入特征向量
x
x
x的维度。所以在二分类问题中,我们的目标就是习得一个分类器,它以图片的特征向量作为输入,然后预测输出结果
y
y
y为1还是0,也就是预测图片中是否有猫:
接下来我们说明一些在余下课程中,需要用到的一些符号。
符号定义 :
x x x:表示一个 n x n_x nx维数据,为输入数据,维度为 ( n x , 1 ) (n_x,1) (nx,1);
y y y:表示输出结果,取值为 ( 0 , 1 ) (0,1) (0,1);
( x ( i ) , y ( i ) ) (x^{(i)},y^{(i)}) (x(i),y(i)):表示第 i i i组数据,可能是训练数据,也可能是测试数据,此处默认为训练数据;
X = [ x ( 1 ) , x ( 2 ) , . . . , x ( m ) ] X=[x^{(1)},x^{(2)},...,x^{(m)}] X=[x(1),x(2),...,x(m)]:表示所有的训练数据集的输入值,放在一个 n x × m n_x×m nx×m的矩阵中,其中 m m m表示样本数目;
Y = [ y ( 1 ) , y ( 2 ) , . . . , y ( m ) ] Y=[y^{(1)},y^{(2)},...,y^{(m)}] Y=[y(1),y(2),...,y(m)]:对应表示所有训练数据集的输出值,维度为 1 × m 1×m 1×m。
用一对
(
x
,
y
)
(x,y)
(x,y)来表示一个单独的样本,
x
x
x代表
n
x
n_x
nx维的特征向量,
y
y
y 表示标签(输出结果)只能为0或1。 而训练集将由
m
m
m个训练样本组成,其中
(
x
(
1
)
,
y
(
1
)
)
(x^{(1)},y^{(1)})
(x(1),y(1))表示第一个样本的输入和输出,
(
x
(
2
)
,
y
(
2
)
)
(x^{(2)},y^{(2)})
(x(2),y(2))表示第二个样本的输入和输出,直到最后一个样本
(
x
(
m
)
,
y
(
m
)
)
(x^{(m)},y^{(m)})
(x(m),y(m)),然后所有的这些一起表示整个训练集。有时候为了强调这是训练样本的个数,会写作
M
t
r
a
i
n
M_{train}
Mtrain,当涉及到测试集的时候,我们会使用
M
t
e
s
t
M_{test}
Mtest来表示测试集的样本数,所以这是测试集的样本数:
最后为了能把训练集表示得更紧凑一点,我们会定义一个矩阵用大写
X
X
X的表示,它由输入向量
x
(
1
)
x^{(1)}
x(1)、
x
(
2
)
x^{(2)}
x(2)等组成,如下图放在矩阵的列中,所以现在我们把
x
(
1
)
x^{(1)}
x(1)作为第一列放在矩阵中,
x
(
2
)
x^{(2)}
x(2)作为第二列,
x
(
m
)
x^{(m)}
x(m)放到第
m
m
m列,然后我们就得到了训练集矩阵
X
X
X。所以这个矩阵有
m
m
m列,
m
m
m是训练集的样本数量,然后这个矩阵的高度记为
n
x
n_x
nx,注意有时候可能因为其他某些原因,矩阵
X
X
X会由训练样本按照行堆叠起来而不是列,如下图所示:
x
(
1
)
x^{(1)}
x(1)的转置直到
x
(
m
)
x^{(m)}
x(m)的转置,但是在实现神经网络的时候,使用左边的这种形式,会让整个实现的过程变得更加简单:
现在来简单温习一下: X X X是一个规模为 n x n_x nx乘以 m m m的矩阵,当你用Python实现的时候,你会看到X.shape,这是一条Python命令,用于显示矩阵的规模,即X.shape等于 ( n x , m ) (n_x,m) (nx,m), X X X是一个规模为 n x n_x nx乘以 m m m的矩阵。所以综上所述,这就是如何将训练样本(输入向量 X X X的集合)表示为一个矩阵。
那么输出标签 y y y呢?同样的道理,为了能更加容易地实现一个神经网络,将标签 y y y放在列中将会使得后续计算非常方便,所以我们定义大写的 Y Y Y等于 y ( 1 ) , y ( m ) , . . . , y ( m ) {{y}^{\left( 1 \right)}},{{y}^{\left( m \right)}},...,{{y}^{\left( m \right)}} y(1),y(m),...,y(m),所以在这里是一个规模为1乘以 m m m的矩阵,同样地使用Python将表示为Y.shape等于 ( 1 , m ) (1,m) (1,m),表示这是一个规模为1乘以 m m m的矩阵。
逻辑回归(Logistic Regression)
对于二元分类问题来讲,给定一个输入特征向量
X
X
X,它可能对应一张图片,你想识别这张图片识别看它是否是一只猫或者不是一只猫的图片,你想要一个算法能够输出预测,你只能称之为
y
^
\hat{y}
y^,也就是你对实际值
y
y
y 的估计。更正式地来说,你想让
y
^
\hat{y}
y^ 表示
y
y
y 等于1的一种可能性或者是机会,前提条件是给定了输入特征
X
X
X。换句话来说,如果
X
X
X是我们在上个视频看到的图片,你想让
y
^
\hat{y}
y^ 来告诉你这是一只猫的图片的机率有多大。在之前的视频中所说的,
X
X
X是一个
n
x
n_x
nx维的向量(相当于有
n
x
n_x
nx个特征的特征向量)。我们用
w
w
w来表示逻辑回归的参数,这也是一个
n
x
n_x
nx维向量(因为
w
w
w实际上是特征权重,维度与特征向量相同),参数里面还有
b
b
b,这是一个实数(表示偏差)。所以给出输入
x
x
x以及参数
w
w
w和
b
b
b之后,我们怎样产生输出预测值
y
^
\hat{y}
y^,一件你可以尝试却不可行的事是让
y
^
=
w
T
x
+
b
\hat{y}={{w}^{T}}x+b
y^=wTx+b。
这时候我们得到的是一个关于输入
x
x
x的线性函数,实际上这是你在做线性回归时所用到的,但是这对于二元分类问题来讲不是一个非常好的算法,因为你想让
y
^
\hat{y}
y^表示实际值
y
y
y等于1的机率的话,
y
^
\hat{y}
y^ 应该在0到1之间。这是一个需要解决的问题,因为
w
T
x
+
b
{{w}^{T}}x+b
wTx+b可能比1要大得多,或者甚至为一个负值。对于你想要的在0和1之间的概率来说它是没有意义的,因此在逻辑回归中,我们的输出应该是
y
^
\hat{y}
y^等于由上面得到的线性函数式子作为自变量的sigmoid函数中,公式如上图最下面所示,将线性函数转换为非线性函数。
下图是sigmoid函数的图像,如果我把水平轴作为
z
z
z轴,那么关于
z
z
z的sigmoid函数是这样的,它是平滑地从0走向1,让我在这里标记纵轴,这是0,曲线与纵轴相交的截距是0.5,这就是关于
z
z
z的sigmoid函数的图像。我们通常都使用
z
z
z来表示
w
T
x
+
b
{{w}^{T}}x+b
wTx+b的值。
关于sigmoid函数的公式是这样的,
σ
(
z
)
=
1
1
+
e
−
z
\sigma \left( z \right)=\frac{1}{1+{{e}^{-z}}}
σ(z)=1+e−z1,在这里
z
z
z是一个实数,这里要说明一些要注意的事情,如果
z
z
z非常大那么
e
−
z
{{e}^{-z}}
e−z将会接近于0,关于
z
z
z的sigmoid函数将会近似等于1除以1加上某个非常接近于0的项,因为
e
e
e 的指数如果是个绝对值很大的负数的话,这项将会接近于0,所以如果
z
z
z很大的话那么关于
z
z
z的sigmoid函数会非常接近1。相反地,如果
z
z
z非常小或者说是一个绝对值很大的负数,那么关于
e
−
z
{{e}^{-z}}
e−z这项会变成一个很大的数,你可以认为这是1除以1加上一个非常非常大的数,所以这个就接近于0。实际上你看到当
z
z
z变成一个绝对值很大的负数,关于
z
z
z的sigmoid函数就会非常接近于0,因此当你实现逻辑回归时,你的工作就是去让机器学习参数
w
w
w以及
b
b
b这样才使得
y
^
\hat{y}
y^成为对
y
=
1
y=1
y=1这一情况的概率的一个很好的估计。
在继续进行下一步之前,介绍一种符号惯例,可以让参数
w
w
w和参数
b
b
b分开。在符号上要注意的一点是当我们对神经网络进行编程时经常会让参数
w
w
w和参数
b
b
b分开,在这里参数
b
b
b对应的是一种偏置。在之前的机器学习课程里,你可能已经见过处理这个问题时的其他符号表示。比如在某些例子里,你定义一个额外的特征称之为
x
0
{{x}{0}}
x0,并且使它等于1,那么现在
X
X
X就是一个
n
x
n_x
nx加1维的变量,然后你定义
y
^
=
σ
(
θ
T
x
)
\hat{y}=\sigma \left( {{\theta }^{T}}x \right)
y^=σ(θTx)的sigmoid函数。在这个备选的符号惯例里,你有一个参数向量
θ
0
,
θ
1
,
θ
2
,
.
.
.
,
θ
n
x
{{\theta }{0}},{{\theta }{1}},{{\theta }{2}},...,{{\theta }{{{n}{x}}}}
θ0,θ1,θ2,...,θnx,这样
θ
0
{{\theta }{0}}
θ0就充当了
b
b
b,这是一个实数,而剩下的
θ
1
{{\theta }{1}}
θ1 直到
θ
n
x
{{\theta }{{{n}{x}}}}
θnx充当了
w
w
w,结果就是当你实现你的神经网络时,有一个比较简单的方法是保持
b
b
b和
w
w
w分开。但是在这节课里我们不会使用任何这类符号惯例,所以不用去担心。 现在你已经知道逻辑回归模型是什么样子了,下一步要做的是训练参数
w
w
w和参数
b
b
b,你需要定义一个代价函数,让我们在下节课里对其进行解释。
逻辑回归的代价函数(Logistic Regression Cost Function)
为什么需要代价函数:
为了训练逻辑回归模型的参数参数
w
w
w和参数
b
b
b我们,需要一个代价函数,通过训练代价函数来得到参数
w
w
w和参数
b
b
b。先看一下逻辑回归的输出函数:
为了让模型通过学习调整参数,你需要给予一个
m
m
m样本的训练集,这会让你在训练集上找到参数
w
w
w和参数
b
b
b,,来得到你的输出。
对训练集的预测值,我们将它写成 y ^ \hat{y} y^,我们更希望它会接近于训练集中的 y y y值,为了对上面的公式更详细的介绍,我们需要说明上面的定义是对一个训练样本来说的,这种形式也使用于每个训练样本,我们使用这些带有圆括号的上标来区分索引和样本,训练样本 i i i所对应的预测值是 y ( i ) {{y}^{(i)}} y(i),是用训练样本的 w T x ( i ) + b {{w}^{T}}{{x}^{(i)}}+b wTx(i)+b然后通过sigmoid函数来得到,也可以把 z z z定义为 z ( i ) = w T x ( i ) + b {{z}^{(i)}}={{w}^{T}}{{x}^{(i)}}+b z(i)=wTx(i)+b,我们将使用这个符号 ( i ) (i) (i)注解,上标 ( i ) (i) (i)来指明数据表示 x x x或者 y y y或者 z z z或者其他数据的第 i i i个训练样本,这就是上标 ( i ) (i) (i)的含义。
损失函数:
损失函数又叫做误差函数,用来衡量算法的运行情况,Loss function:
L
(
y
^
,
y
)
L\left( \hat{y},y \right)
L(y^,y).
我们通过这个 L L L称为的损失函数,来衡量预测输出值和实际值有多接近。一般我们用预测值和实际值的平方差或者它们平方差的一半,但是通常在逻辑回归中我们不这么做,因为当我们在学习逻辑回归参数的时候,会发现我们的优化目标不是凸优化,只能找到多个局部最优值,梯度下降法很可能找不到全局最优值,虽然平方差是一个不错的损失函数,但是我们在逻辑回归模型中会定义另外一个损失函数。
我们在逻辑回归中用到的损失函数是: L ( y ^ , y ) = − y log ( y ^ ) − ( 1 − y ) log ( 1 − y ^ ) L\left( \hat{y},y \right)=-y\log(\hat{y})-(1-y)\log (1-\hat{y}) L(y^,y)=−ylog(y^)−(1−y)log(1−y^)
为什么要用这个函数作为逻辑损失函数?当我们使用平方误差作为损失函数的时候,你会想要让这个误差尽可能地小,对于这个逻辑回归损失函数,我们也想让它尽可能地小,为了更好地理解这个损失函数怎么起作用,我们举两个例子:
当 y = 1 y=1 y=1时损失函数 L = − log ( y ^ ) L=-\log (\hat{y}) L=−log(y^),如果想要损失函数 L L L尽可能得小,那么 y ^ \hat{y} y^就要尽可能大,因为sigmoid函数取值 [ 0 , 1 ] [0,1] [0,1],所以 y ^ \hat{y} y^会无限接近于1。
当 y = 0 y=0 y=0时损失函数 L = − log ( 1 − y ^ ) L=-\log (1-\hat{y}) L=−log(1−y^),如果想要损失函数 L L L尽可能得小,那么 y ^ \hat{y} y^就要尽可能小,因为sigmoid函数取值 [ 0 , 1 ] [0,1] [0,1],所以 y ^ \hat{y} y^会无限接近于0。
在这门课中有很多的函数效果和现在这个类似,就是如果 y y y等于1,我们就尽可能让 y ^ \hat{y} y^变大,如果 y y y等于0,我们就尽可能让 y ^ \hat{y} y^ 变小。 损失函数是在单个训练样本中定义的,它衡量的是算法在单个训练样本中表现如何,为了衡量算法在全部训练样本上的表现如何,我们需要定义一个算法的代价函数,算法的代价函数是对 m m m个样本的损失函数求和然后除以 m m m: J ( w , b ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) = 1 m ∑ i = 1 m ( − y ( i ) log y ^ ( i ) − ( 1 − y ( i ) ) log ( 1 − y ^ ( i ) ) ) J\left( w,b \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{L\left( {{{\hat{y}}}^{(i)}},{{y}^{(i)}} \right)}=\frac{1}{m}\sum\limits_{i=1}^{m}{\left( -{{y}^{(i)}}\log {{{\hat{y}}}^{(i)}}-(1-{{y}^{(i)}})\log (1-{{{\hat{y}}}^{(i)}}) \right)} J(w,b)=m1i=1∑mL(y^(i),y(i))=m1i=1∑m(−y(i)logy^(i)−(1−y(i))log(1−y^(i))) 损失函数只适用于像这样的单个训练样本,而代价函数是参数的总代价,所以在训练逻辑回归模型时候,我们需要找到合适的 w w w和 b b b,来让代价函数 J J J 的总代价降到最低。 根据我们对逻辑回归算法的推导及对单个样本的损失函数的推导和针对算法所选用参数的总代价函数的推导,结果表明逻辑回归可以看做是一个非常小的神经网络,在下一个视频中,我们会看到神经网络会做什么。
梯度下降法(Gradient Descent)
梯度下降法可以做什么?
在你测试集上,通过最小化代价函数(成本函数) J ( w , b ) J(w,b) J(w,b)来训练的参数 w w w和 b b b,
如图,在第二行给出和之前一样的逻辑回归算法的代价函数(成本函数)
梯度下降法的形象化说明
在这个图中,横轴表示你的空间参数
w
w
w和
b
b
b,在实践中,
w
w
w可以是更高的维度,但是为了更好地绘图,我们定义
w
w
w和
b
b
b,都是单一实数,代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)是在水平轴
w
w
w和
b
b
b上的曲面,因此曲面的高度就是
J
(
w
,
b
)
J(w,b)
J(w,b)在某一点的函数值。我们所做的就是找到使得代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)函数值是最小值,对应的参数
w
w
w和
b
b
b。
如图,代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)是一个凸函数(convex function),像一个大碗一样。
如图,这就与刚才的图有些相反,因为它是非凸的并且有很多不同的局部最小值。由于逻辑回归的代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)特性,我们必须定义代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)为凸函数。 初始化
w
w
w和
b
b
b,
可以用如图那个小红点来初始化参数
w
w
w和
b
b
b,也可以采用随机初始化的方法,对于逻辑回归几乎所有的初始化方法都有效,因为函数是凸函数,无论在哪里初始化,应该达到同一点或大致相同的点。
我们以如图的小红点的坐标来初始化参数
w
w
w和
b
b
b。
- 朝最陡的下坡方向走一步,不断地迭代
我们朝最陡的下坡方向走一步,如图,走到了如图中第二个小红点处。
我们可能停在这里也有可能继续朝最陡的下坡方向再走一步,如图,经过两次迭代走到第三个小红点处。
3.直到走到全局最优解或者接近全局最优解的地方
通过以上的三个步骤我们可以找到全局最优解,也就是代价函数(成本函数) J ( w , b ) J(w,b) J(w,b)这个凸函数的最小值点。
梯度下降法的细节化说明(仅有一个参数)
假定代价函数(成本函数)
J
(
w
)
J(w)
J(w) 只有一个参数
w
w
w,即用一维曲线代替多维曲线,这样可以更好画出图像。
迭代就是不断重复做如图的公式:
: = := :=表示更新参数,
$a $ 表示学习率(learning rate),用来控制步长(step),即向下走一步的长度
d
J
(
w
)
d
w
\frac{dJ(w)}{dw}
dwdJ(w) 就是函数
J
(
w
)
J(w)
J(w)对
w
w
w 求导(derivative),在代码中我们会使用
d
w
dw
dw表示这个结果
对于导数更加形象化的理解就是斜率(slope),如图该点的导数就是这个点相切于
J
(
w
)
J(w)
J(w)的小三角形的高除宽。假设我们以如图点为初始化点,该点处的斜率的符号是正的,即
d
J
(
w
)
d
w
>
0
\frac{dJ(w)}{dw}>0
dwdJ(w)>0,所以接下来会向左走一步。
整个梯度下降法的迭代过程就是不断地向左走,直至逼近最小值点。
假设我们以如图点为初始化点,该点处的斜率的符号是负的,即
d
J
(
w
)
d
w
<
0
\frac{dJ(w)}{dw}<0
dwdJ(w)<0,所以接下来会向右走一步。
整个梯度下降法的迭代过程就是不断地向右走,即朝着最小值点方向走。
梯度下降法的细节化说明(两个参数)
逻辑回归的代价函数(成本函数)
J
(
w
,
b
)
J(w,b)
J(w,b)是含有两个参数的。
$\partial $ 表示求偏导符号,可以读作round,
∂
J
(
w
,
b
)
∂
w
\frac{\partial J(w,b)}{\partial w}
∂w∂J(w,b) 就是函数
J
(
w
,
b
)
J(w,b)
J(w,b) 对
w
w
w 求偏导,在代码中我们会使用
d
w
dw
dw 表示这个结果,
∂
J
(
w
,
b
)
∂
b
\frac{\partial J(w,b)}{\partial b}
∂b∂J(w,b) 就是函数
J
(
w
,
b
)
J(w,b)
J(w,b)对
b
b
b 求偏导,在代码中我们会使用
d
b
db
db 表示这个结果, 小写字母
d
d
d 用在求导数(derivative),即函数只有一个参数, 偏导数符号$\partial $ 用在求偏导(partial derivative),即函数含有两个以上的参数。
计算图(Computation Graph)
计算图组织计算的形式是用蓝色箭头从左到右的计算,让我们看看下一个视频中如何进行反向红色箭头(也就是从右到左)的导数计算,让我们继续下一个视频的学习。
使用计算图求导(Derivatives with a Computation Graph)
在上一个视频中,我们看了一个例子使用流程计算图来计算函数J。现在我们清理一下流程图的描述,看看你如何利用它计算出函数 J J J的导数。
下面用到的公式:
d J d u = d J d v d v d u \frac{dJ}{du}=\frac{dJ}{dv}\frac{dv}{du} dudJ=dvdJdudv , d J d b = d J d u d u d b \frac{dJ}{db}=\frac{dJ}{du}\frac{du}{db} dbdJ=dudJdbdu , d J d a = d J d u d u d a \frac{dJ}{da}=\frac{dJ}{du}\frac{du}{da} dadJ=dudJdadu
这是一个流程图:
假设你要计算 d J d v \frac{{dJ}}{{dv}} dvdJ,那要怎么算呢?好,比如说,我们要把这个 v v v值拿过来,改变一下,那么 J J J的值会怎么变呢?
所以定义上 J = 3 v J = 3v J=3v,现在 v = 11 v=11 v=11,所以如果你让 v v v增加一点点,比如到11.001,那么 J = 3 v = 33.003 J =3v =33.003 J=3v=33.003,所以我这里 v v v增加了0.001,然后最终结果是 J J J上升到原来的3倍,所以 d J d v = 3 \frac{{dJ}}{{dv}}=3 dvdJ=3,因为对于任何 v v v 的增量 J J J都会有3倍增量,而且这类似于我们在上一个视频中的例子,我们有 f ( a ) = 3 a f(a)=3a f(a)=3a,然后我们推导出 d f ( a ) d a = 3 \frac{{df}(a)}{{da}}= 3 dadf(a)=3,所以这里我们有 J = 3 v J=3v J=3v,所以 d J d v = 3 \frac{{dJ}}{{dv}} =3 dvdJ=3,这里 J J J扮演了 f f f的角色,在之前的视频里的例子。
在反向传播算法中的术语,我们看到,如果你想计算最后输出变量的导数,使用你最关心的变量对 v v v的导数,那么我们就做完了一步反向传播,在这个流程图中是一个反向步骤。
我们来看另一个例子, d J d a \frac{{dJ}}{da} dadJ是多少呢?换句话说,如果我们提高 a a a的数值,对 J J J的数值有什么影响?
好,我们看看这个例子。变量 a = 5 a=5 a=5,我们让它增加到5.001,那么对v的影响就是 a + u a+u a+u,之前 v = 11 v=11 v=11,现在变成11.001,我们从上面看到现在 J J J 就变成33.003了,所以我们看到的是,如果你让 a a a增加0.001, J J J增加0.003。那么增加 a a a,我是说如果你把这个5换成某个新值,那么 a a a的改变量就会传播到流程图的最右,所以 J J J最后是33.003。所以J的增量是3乘以 a a a的增量,意味着这个导数是3。
要解释这个计算过程,其中一种方式是:如果你改变了 a a a,那么也会改变 v v v,通过改变 v v v,也会改变 J J J,所以 J J J值的净变化量,当你提升这个值(0.001),当你把 a a a值提高一点点,这就是 J J J的变化量(0.003)。
首先a增加了, v v v也会增加, v v v增加多少呢?这取决于 d v d a \frac{{dv}}{da} dadv,然后 v v v的变化导致 J J J也在增加,所以这在微积分里实际上叫链式法则,如果 a a a影响到 v v v, v v v影响到 J J J,那么当你让 a a a变大时, J J J的变化量就是当你改变 a a a时, v v v的变化量乘以改变 v v v时 J J J的变化量,在微积分里这叫链式法则。
我们从这个计算中看到,如果你让 a a a增加0.001, v v v也会变化相同的大小,所以 d v d a = 1 \frac{{dv}}{da}= 1 dadv=1。事实上,如果你代入进去,我们之前算过 d J d v = 3 \frac{{dJ}}{{dv}} =3 dvdJ=3, d v d a = 1 \frac{{dv}}{da} =1 dadv=1,所以这个乘积3×1,实际上就给出了正确答案, d J d a = 3 \frac{{dJ}}{da} = 3 dadJ=3。
这张小图表示了如何计算, d J d v \frac{{dJ}}{{dv}} dvdJ就是 J J J对变量 v v v的导数,它可以帮助你计算 d J d a \frac{{dJ}}{da} dadJ,所以这是另一步反向传播计算。
现在我想介绍一个新的符号约定,当你编程实现反向传播时,通常会有一个最终输出值是你要关心的,最终的输出变量,你真正想要关心或者说优化的。在这种情况下最终的输出变量是J,就是流程图里最后一个符号,所以有很多计算尝试计算输出变量的导数,所以输出变量对某个变量的导数,我们就用
d
v
a
r
dvar
dvar命名,所以在很多计算中你需要计算最终输出结果的导数,在这个例子里是
J
J
J,还有各种中间变量,比如
a
、
b
、
c
、
u
、
v
a、b、c、u、v
a、b、c、u、v,当你在软件里实现的时候,变量名叫什么?你可以做的一件事是,在python中,你可以写一个很长的变量名,比如
d
F
i
n
a
l
O
u
t
p
u
t
v
a
r
d
v
a
r
{dFinalOutputvar}_{dvar}
dFinalOutputvardvar,但这个变量名有点长,我们就用
d
J
d
v
a
r
dJ_dvar
dJdvar,但因为你一直对
d
J
dJ
dJ求导,对这个最终输出变量求导。我这里要介绍一个新符号,在程序里,当你编程的时候,在代码里,我们就使用变量名
d
v
a
r
dvar
dvar,来表示那个量。
好,所以在程序里是
d
v
a
r
dvar
dvar表示导数,你关心的最终变量
J
J
J的导数,有时最后是
L
L
L,对代码中各种中间量的导数,所以代码里这个东西,你用
d
v
dv
dv表示这个值,所以
d
v
=
3
dv=3
dv=3,你的代码表示就是
d
a
=
3
da=3
da=3。
好,所以我们通过这个流程图完成部分的后向传播算法。我们在下一张幻灯片看看这个例子剩下的部分。
我们清理出一张新的流程图,我们回顾一下,到目前为止,我们一直在往回传播,并计算 d v = 3 dv=3 dv=3,再次, d v dv dv是代码里的变量名,其真正的定义是 d J d v \frac{{dJ}}{{dv}} dvdJ。我发现 d a = 3 da=3 da=3,再次, d a da da是代码里的变量名,其实代表 d J d a \frac{{dJ}}{da} dadJ的值。
大概手算了一下,两条直线怎么计算反向传播。
好,我们继续计算导数,我们看看这个值 u u u,那么 d J d u \frac{dJ}{du} dudJ是多少呢?通过和之前类似的计算,现在我们从 u = 6 u=6 u=6出发,如果你令 u u u增加到6.001,那么 v v v之前是11,现在变成11.001了, J J J 就从33变成33.003,所以 J J J 增量是3倍,所以 d J d u = 3 \frac{{dJ}}{du}= 3 dudJ=3。对 u u u的分析很类似对a的分析,实际上这计算起来就是 d J d v ⋅ d v d u \frac{{dJ}}{dv}\cdot \frac{{dv}}{du} dvdJ⋅dudv,有了这个,我们可以算出 d J d v = 3 \frac{{dJ}}{dv} =3 dvdJ=3, d v d u = 1 \frac{{dv}}{du} = 1 dudv=1,最终算出结果是 3 × 1 = 3 3×1=3 3×1=3。
所以我们还有一步反向传播,我们最终计算出 d u = 3 du=3 du=3,这里的 d u du du当然了,就是 d J d u \frac{{dJ}}{du} dudJ。
现在,我们仔细看看最后一个例子,那么 d J d b \frac{{dJ}}{db} dbdJ呢?想象一下,如果你改变了 b b b的值,你想要然后变化一点,让 J J J 值到达最大或最小,那么导数是什么呢?这个 J J J函数的斜率,当你稍微改变 b b b值之后。事实上,使用微积分链式法则,这可以写成两者的乘积,就是 d J d u ⋅ d u d b \frac{{dJ}}{du}\cdot \frac{{du}}{db} dudJ⋅dbdu,理由是,如果你改变 b b b一点点,所以 b b b变化比如说3.001,它影响J的方式是,首先会影响 u u u,它对 u u u的影响有多大?好, u u u的定义是 b ⋅ c b\cdot c b⋅c,所以 b = 3 b=3 b=3时这是6,现在就变成6.002了,对吧,因为在我们的例子中 c = 2 c=2 c=2,所以这告诉我们 d u d b = 2 \frac{{du}}{db}= 2 dbdu=2当你让 b b b增加0.001时, u u u就增加两倍。所以 d u d b = 2 \frac{{du}}{db} =2 dbdu=2,现在我想 u u u的增加量已经是 b b b的两倍,那么 d J d u \frac{{dJ}}{du} dudJ是多少呢?我们已经弄清楚了,这等于3,所以让这两部分相乘,我们发现 d J d b = 6 \frac{{dJ}}{db}= 6 dbdJ=6。
好,这就是第二部分的推导,其中我们想知道 u u u 增加0.002,会对 J J J 有什么影响。实际上 d J d u = 3 \frac{{dJ}}{du}=3 dudJ=3,这告诉我们u增加0.002之后, J J J上升了3倍,那么 J J J 应该上升0.006,对吧。这可以从 d J d u = 3 \frac{{dJ}}{du}= 3 dudJ=3推导出来。
如果你仔细看看这些数学内容,你会发现,如果 b b b变成3.001,那么 u u u就变成6.002, v v v变成11.002,然后 J = 3 v = 33.006 J=3v=33.006 J=3v=33.006,对吧?这就是如何得到 d J d b = 6 \frac{{dJ}}{db}= 6 dbdJ=6。
为了填进去,如果我们反向走的话,
d
b
=
6
db=6
db=6,而
d
b
db
db其实是Python代码中的变量名,表示
d
J
d
b
\frac{{dJ}}{db}
dbdJ。
我不会很详细地介绍最后一个例子,但事实上,如果你计算 d J d c = d J d u ⋅ d u d c = 3 × 3 \frac{{dJ}}{dc} =\frac{{dJ}}{du}\cdot \frac{{du}}{dc} = 3 \times 3 dcdJ=dudJ⋅dcdu=3×3,这个结果是9。
我不会详细说明这个例子,在最后一步,我们可以推出 d c = 9 dc=9 dc=9。
所以这个视频的要点是,对于那个例子,当计算所有这些导数时,最有效率的办法是从右到左计算,跟着这个红色箭头走。特别是当我们第一次计算对 v v v的导数时,之后在计算对 a a a导数就可以用到。然后对 u u u的导数,比如说这个项和这里这个项:
可以帮助计算对
b
b
b的导数,然后对
c
c
c的导数。
所以这是一个计算流程图,就是正向或者说从左到右的计算来计算成本函数J,你可能需要优化的函数,然后反向从右到左计算导数。如果你不熟悉微积分或链式法则,我知道这里有些细节讲的很快,但如果你没有跟上所有细节,也不用怕。在下一个视频中,我会再过一遍。在逻辑回归的背景下过一遍,并给你介绍需要做什么才能编写代码,实现逻辑回归模型中的导数计算。
逻辑回归中的梯度下降Logistic Regression Gradient Descent)
本节我们讨论怎样通过计算偏导数来实现逻辑回归的梯度下降算法。它的关键点是几个重要公式,其作用是用来实现逻辑回归中梯度下降算法。但是在本节视频中,我将使用计算图对梯度下降算法进行计算。我必须要承认的是,使用计算图来计算逻辑回归的梯度下降算法有点大材小用了。但是,我认为以这个例子作为开始来讲解,可以使你更好的理解背后的思想。从而在讨论神经网络时,你可以更深刻而全面地理解神经网络。接下来让我们开始学习逻辑回归的梯度下降算法。
假设样本只有两个特征 x 1 {{x}{1}} x1和 x 2 {{x}{2}} x2,为了计算 z z z,我们需要输入参数 w 1 {{w}{1}} w1、 w 2 {{w}{2}} w2 和 b b b,除此之外还有特征值 x 1 {{x}{1}} x1和 x 2 {{x}{2}} x2。因此 z z z的计算公式为: z = w 1 x 1 + w 2 x 2 + b z={{w}{1}}{{x}{1}}+{{w}{2}}{{x}{2}}+b z=w1x1+w2x2+b 回想一下逻辑回归的公式定义如下: y ^ = a = σ ( z ) \hat{y}=a=\sigma (z) y^=a=σ(z) 其中 z = w T x + b z={{w}^{T}}x+b z=wTx+b σ ( z ) = 1 1 + e − z \sigma \left( z \right)=\frac{1}{1+{{e}^{-z}}} σ(z)=1+e−z1 损失函数: L ( y ^ ( i ) , y ( i ) ) = − y ( i ) log y ^ ( i ) − ( 1 − y ( i ) ) log ( 1 − y ^ ( i ) ) L( {{{\hat{y}}}^{(i)}},{{y}^{(i)}})=-{{y}^{(i)}}\log {{\hat{y}}^{(i)}}-(1-{{y}^{(i)}})\log (1-{{\hat{y}}^{(i)}}) L(y^(i),y(i))=−y(i)logy^(i)−(1−y(i))log(1−y^(i)) 代价函数: J ( w , b ) = 1 m ∑ i m L ( y ^ ( i ) , y ( i ) ) J\left( w,b \right)=\frac{1}{m}\sum\nolimits_{i}^{m}{L( {{{\hat{y}}}^{(i)}},{{y}^{(i)}})} J(w,b)=m1∑imL(y^(i),y(i)) 假设现在只考虑单个样本的情况,单个样本的代价函数定义如下: L ( a , y ) = − ( y log ( a ) + ( 1 − y ) log ( 1 − a ) ) L(a,y)=-(y\log (a)+(1-y)\log (1-a)) L(a,y)=−(ylog(a)+(1−y)log(1−a)) 其中 a a a是逻辑回归的输出, y y y是样本的标签值。现在让我们画出表示这个计算的计算图。 这里先复习下梯度下降法, w w w和 b b b的修正量可以表达如下:
w
:
=
w
−
a
∂
J
(
w
,
b
)
∂
w
w:=w-a \frac{\partial J(w,b)}{\partial w}
w:=w−a∂w∂J(w,b),
b
:
=
b
−
a
∂
J
(
w
,
b
)
∂
b
b:=b-a\frac{\partial J(w,b)}{\partial b}
b:=b−a∂b∂J(w,b)
如图:在这个公式的外侧画上长方形。然后计算:
y
^
=
a
=
σ
(
z
)
\hat{y}=a=\sigma(z)
y^=a=σ(z) 也就是计算图的下一步。最后计算损失函数
L
(
a
,
y
)
L(a,y)
L(a,y)。 有了计算图,我就不需要再写出公式了。因此,为了使得逻辑回归中最小化代价函数
L
(
a
,
y
)
L(a,y)
L(a,y),我们需要做的仅仅是修改参数
w
w
w和
b
b
b的值。前面我们已经讲解了如何在单个训练样本上计算代价函数的前向步骤。现在让我们来讨论通过反向计算出导数。 因为我们想要计算出的代价函数
L
(
a
,
y
)
L(a,y)
L(a,y)的导数,首先我们需要反向计算出代价函数
L
(
a
,
y
)
L(a,y)
L(a,y)关于
a
a
a的导数,在编写代码时,你只需要用
d
a
da
da 来表示
d
L
(
a
,
y
)
d
a
\frac{dL(a,y)}{da}
dadL(a,y) 。 通过微积分得到:
d
L
(
a
,
y
)
d
a
=
−
y
/
a
+
(
1
−
y
)
/
(
1
−
a
)
\frac{dL(a,y)}{da}=-y/a+(1-y)/(1-a)
dadL(a,y)=−y/a+(1−y)/(1−a) 如果你不熟悉微积分,也不必太担心,我们会列出本课程涉及的所有求导公式。那么如果你非常熟悉微积分,我们鼓励你主动推导前面介绍的代价函数的求导公式,使用微积分直接求出
L
(
a
,
y
)
L(a,y)
L(a,y)关于变量
a
a
a的导数。如果你不太了解微积分,也不用太担心。现在我们已经计算出
d
a
da
da,也就是最终输出结果的导数。 现在可以再反向一步,在编写Python代码时,你只需要用
d
z
dz
dz来表示代价函数
L
L
L关于
z
z
z 的导数
d
L
d
z
\frac{dL}{dz}
dzdL,也可以写成
d
L
(
a
,
y
)
d
z
\frac{dL(a,y)}{dz}
dzdL(a,y),这两种写法都是正确的。
d
L
d
z
=
a
−
y
\frac{dL}{dz}=a-y
dzdL=a−y 。 因为
d
L
(
a
,
y
)
d
z
=
d
L
d
z
=
(
d
L
d
a
)
⋅
(
d
a
d
z
)
\frac{dL(a,y)}{dz}=\frac{dL}{dz}=(\frac{dL}{da})\cdot (\frac{da}{dz})
dzdL(a,y)=dzdL=(dadL)⋅(dzda), 并且
d
a
d
z
=
a
⋅
(
1
−
a
)
\frac{da}{dz}=a\cdot (1-a)
dzda=a⋅(1−a), 而
d
L
d
a
=
(
−
y
a
+
(
1
−
y
)
(
1
−
a
)
)
\frac{dL}{da}=(-\frac{y}{a}+\frac{(1-y)}{(1-a)})
dadL=(−ay+(1−a)(1−y)),因此将这两项相乘,得到:
d z = d L ( a , y ) d z = d L d z = ( d L d a ) ⋅ ( d a d z ) = ( − y a + ( 1 − y ) ( 1 − a ) ) ⋅ a ( 1 − a ) = a − y {dz} = \frac{{dL}(a,y)}{{dz}} = \frac{{dL}}{{dz}} = \left( \frac{{dL}}{{da}} \right) \cdot \left(\frac{{da}}{{dz}} \right) = ( - \frac{y}{a} + \frac{(1 - y)}{(1 - a)})\cdot a(1 - a) = a - y dz=dzdL(a,y)=dzdL=(dadL)⋅(dzda)=(−ay+(1−a)(1−y))⋅a(1−a)=a−y
视频中为了简化推导过程,假设 n x {{n}_{x}} nx 这个推导的过程就是我之前提到过的链式法则。如果你对微积分熟悉,放心地去推导整个求导过程,如果不熟悉微积分,你只需要知道 d z = ( a − y ) dz=(a-y) dz=(a−y)已经计算好了。
现在进行最后一步反向推导,也就是计算
w
w
w和
b
b
b变化对代价函数
L
L
L的影响,特别地,可以用:
d
w
1
=
1
m
∑
i
m
x
1
(
i
)
(
a
(
i
)
−
y
(
i
)
)
d{{w}{1}}=\frac{1}{m}\sum\limits{i}^{m}{x_{1}^{(i)}}({{a}^{(i)}}-{{y}^{(i)}})
dw1=m1∑imx1(i)(a(i)−y(i))
d
w
2
=
1
m
∑
i
m
x
2
(
i
)
(
a
(
i
)
−
y
(
i
)
)
d{{w}{2}}=\frac{1}{m}\sum\limits{i}^{m}{x_{2}^{(i)}}({{a}^{(i)}}-{{y}^{(i)}})
dw2=m1∑imx2(i)(a(i)−y(i))
d
b
=
1
m
∑
i
m
(
a
(
i
)
−
y
(
i
)
)
db=\frac{1}{m}\sum\limits_{i}^{m}{({{a}^{(i)}}-{{y}^{(i)}})}
db=m1i∑m(a(i)−y(i)) 视频中,
d
w
1
d{{w}{1}}
dw1 表示
∂
L
∂
w
1
=
x
1
⋅
d
z
\frac{\partial L}{\partial {{w}{1}}}={{x}{1}}\cdot dz
∂w1∂L=x1⋅dz,
d
w
2
d{{w}{\text{2}}}
dw2 表示
∂
L
∂
w
2
=
x
2
⋅
d
z
\frac{\partial L}{\partial {{w}{2}}}={{x}{2}}\cdot dz
∂w2∂L=x2⋅dz,
d
b
=
d
z
db=dz
db=dz。 因此,关于单个样本的梯度下降算法,你所需要做的就是如下的事情: 使用公式
d
z
=
(
a
−
y
)
dz=(a-y)
dz=(a−y)计算
d
z
dz
dz, 使用
d
w
1
=
x
1
⋅
d
z
d{{w}{1}}={{x}{1}}\cdot dz
dw1=x1⋅dz 计算
d
w
1
d{{w}{1}}
dw1,
d
w
2
=
x
2
⋅
d
z
d{{w}{2}}={{x}{2}}\cdot dz
dw2=x2⋅dz计算
d
w
2
d{{w}{2}}
dw2,
d
b
=
d
z
db=dz
db=dz 来计算
d
b
db
db, 然后: 更新
w
1
=
w
1
−
a
d
w
1
{{w}{1}}={{w}{1}}-a d{{w}{1}}
w1=w1−adw1, 更新
w
2
=
w
2
−
a
d
w
2
{{w}{2}}={{w}{2}}-a d{{w}{2}}
w2=w2−adw2, 更新
b
=
b
−
α
d
b
b=b-\alpha db
b=b−αdb。 这就是关于单个样本实例的梯度下降算法中参数更新一次的步骤。
现在你已经知道了怎样计算导数,并且实现针对单个训练样本的逻辑回归的梯度下降算法。但是,训练逻辑回归模型不仅仅只有一个训练样本,而是有
m
m
m个训练样本的整个训练集。因此在下一节视频中,我们将这些思想应用到整个训练样本集中,而不仅仅只是单个样本上。
m个样本的梯度下降(Gradient Descent on m Examples)
在之前的视频中,你已经看到如何计算导数,以及应用梯度下降在逻辑回归的一个训练样本上。现在我们想要把它应用在
m
m
m个训练样本上。
首先,让我们时刻记住有关于损失函数
J
(
w
,
b
)
J(w,b)
J(w,b)的定义。
J ( w , b ) = 1 m ∑ i = 1 m L ( a ( i ) , y ( i ) ) J(w,b)=\frac{1}{m}\sum\limits_{i=1}^{m}{L({{a}^{(i)}},{{y}^{(i)}})} J(w,b)=m1i=1∑mL(a(i),y(i))
当你的算法输出关于样本
y
y
y的
a
(
i
)
{{a}^{(i)}}
a(i),
a
(
i
)
{{a}^{(i)}}
a(i)是训练样本的预测值,即:
σ
(
z
(
i
)
)
=
σ
(
w
T
x
(
i
)
+
b
)
\sigma ( {{z}^{(i)}})=\sigma( {{w}^{T}}{{x}^{\left( i \right)}}+b)
σ(z(i))=σ(wTx(i)+b)。 所以我们在前面的幻灯中展示的是对于任意单个训练样本,如何计算微分当你只有一个训练样本。因此
d
w
1
d{{w}{1}}
dw1,
d
w
2
d{{w}{\text{2}}}
dw2和
d
b
db
db 添上上标
i
i
i表示你求得的相应的值。如果你面对的是我们在之前的幻灯中演示的那种情况,但只使用了一个训练样本
(
x
(
i
)
,
y
(
i
)
)
({{x}^{(i)}},{{y}^{(i)}})
(x(i),y(i))。 现在你知道带有求和的全局代价函数,实际上是1到
m
m
m项各个损失的平均。 所以它表明全局代价函数对
w
1
{{w}{1}}
w1的微分,对
w
1
{{w}{1}}
w1的微分也同样是各项损失对
w
1
{{w}_{1}}
w1微分的平均。
但之前我们已经演示了如何计算这项,即之前幻灯中演示的如何对单个训练样本进行计算。所以你真正需要做的是计算这些微分,如我们在之前的训练样本上做的。并且求平均,这会给你全局梯度值,你能够把它直接应用到梯度下降算法中。
所以这里有很多细节,但让我们把这些装进一个具体的算法。同时你需要一起应用的就是逻辑回归和梯度下降。
我们初始化 J = 0 , d w 1 = 0 , d w 2 = 0 , d b = 0 J=0,d{{w}{1}}=0,d{{w}{2}}=0,db=0 J=0,dw1=0,dw2=0,db=0
代码流程:
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循环。第一个for循环是一个小循环遍历 m m m个训练样本,第二个for循环是一个遍历所有特征的for循环。这个例子中我们只有2个特征,所以 n n n等于2并且 n x {{n}{x}} nx 等于2。 但如果你有更多特征,你开始编写你的因此 d w 1 d{{w}{1}} dw1, d w 2 d{{w}{2}} dw2,你有相似的计算从 d w 3 d{{w}{3}} dw3一直下去到 d w n d{{w}_{n}} dwn。所以看来你需要一个for循环遍历所有 n n n个特征。
当你应用深度学习算法,你会发现在代码中显式地使用for循环使你的算法很低效,同时在深度学习领域会有越来越大的数据集。所以能够应用你的算法且没有显式的for循环会是重要的,并且会帮助你适用于更大的数据集。所以这里有一些叫做向量化技术,它可以允许你的代码摆脱这些显式的for循环。
我想在先于深度学习的时代,也就是深度学习兴起之前,向量化是很棒的。可以使你有时候加速你的运算,但有时候也未必能够。但是在深度学习时代向量化,摆脱for循环已经变得相当重要。因为我们越来越多地训练非常大的数据集,因此你真的需要你的代码变得非常高效。所以在接下来的几个视频中,我们会谈到向量化,以及如何应用向量化而连一个for循环都不使用。所以学习了这些,我希望你有关于如何应用逻辑回归,或是用于逻辑回归的梯度下降,事情会变得更加清晰。当你进行编程练习,但在真正做编程练习之前让我们先谈谈向量化。然后你可以应用全部这些东西,应用一个梯度下降的迭代而不使用任何for循环。
向量化(Vectorization)
代码运行速度非常重要,否则如果在大数据集上,你的代码可能花费很长时间去运行,你将要等待非常长的时间去得到结果。所以在深度学习领域,运行向量化是一个关键的技巧,让我们举个栗子说明什么是向量化。
在逻辑回归中你需要去计算 z = w T x + b z={{w}^{T}}x+b z=wTx+b, w w w、 x x x都是列向量。如果你有很多的特征那么就会有一个非常大的向量,所以 w ∈ R n x w\in {{\mathbb{R}}^{{{n}{x}}}} w∈Rnx , x ∈ R n x x\in{{\mathbb{R}}^{{{n}{x}}}} x∈Rnx,所以如果你想使用非向量化方法去计算 w T x {{w}^{T}}x wTx,你需要用如下方式(python)
z=0
for i in range(n_x):
z += w[i]*x[i]
z += b
这是一个非向量化的实现,你会发现这真的很慢,作为一个对比,向量化实现将会非常直接计算 w T x {{w}^{T}}x wTx,代码如下:
z=np.dot(w,x)+b
这是向量化计算
w
T
x
{{w}^{T}}x
wTx的方法,你将会发现这个非常快。
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个小时去运行。
一句话总结,以上都是再说和for循环相比,向量化可以快速得到结果。
你可能听过很多类似如下的话,“大规模的深度学习使用了GPU或者图像处理单元实现”,但是我做的所有的案例都是在jupyter notebook上面实现,这里只有CPU,CPU和GPU都有并行化的指令,他们有时候会叫做SIMD指令,这个代表了一个单独指令多维数据,这个的基础意义是,如果你使用了built-in函数,像np.function或者并不要求你实现循环的函数,它可以让python的充分利用并行化计算,这是事实在GPU和CPU上面计算,GPU更加擅长SIMD计算,但是CPU事实上也不是太差,可能没有GPU那么擅长吧。接下来的视频中,你将看到向量化怎么能够加速你的代码,经验法则是,无论什么时候,避免使用明确的for循环。
向量化的更多例子(More Examples of Vectorization)
当我们在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(loop)语句。虽然有时写循环(loop)是不可避免的,但是我们可以使用比如numpy的内置函数或者其他办法去计算。当你这样使用后,程序效率总是快于循环(loop)。
让我们看另外一个例子。如果你想计算向量 u = A v u=Av u=Av,这时矩阵乘法定义为,矩阵乘法的定义就是: u i = ∑ j A ij v i u_{i} =\sum_{j}^{}{A_{\text{ij}}v_{i}} ui=∑jAijvi,这取决于你怎么定义 u i u_{i} ui值。同样使用非向量化实现, u = n p . z e r o s ( n , 1 ) u=np.zeros(n,1) u=np.zeros(n,1), 并且通过两层循环 f o r ( i ) : f o r ( j ) : for(i):for(j): for(i):for(j):,得到 u [ i ] = u [ i ] + A [ i ] [ j ] ∗ v [ j ] u[i]=u[i]+A[i][j]*v[j] u[i]=u[i]+A[i][j]∗v[j] 。现在就有了 i i i 和 j j j 的两层循环,这就是非向量化。向量化方式就可以用 u = n p . d o t ( A , v ) u=np.dot(A,v) u=np.dot(A,v),右边这种向量化实现方式,消除了两层循环使得代码运行速度更快。
下面通过另一个例子继续了解向量化。如果你已经有一个向量
v
v
v,并且想要对向量
v
v
v的每个元素做指数操作,得到向量
u
u
u等于
e
e
e的
v
1
v_1
v1,
e
e
e的
v
2
v_2
v2,一直到
e
e
e的
v
n
v_n
vn次方。这里是非向量化的实现方式,首先你初始化了向量
u
=
n
p
.
z
e
r
o
s
(
n
,
1
)
u=np.zeros(n,1)
u=np.zeros(n,1),并且通过循环依次计算每个元素。但事实证明可以通过python的numpy内置函数,帮助你计算这样的单个函数。所以我会引入import numpy as np,执行
u
=
n
p
.
e
x
p
(
v
)
u=np.exp(v)
u=np.exp(v) 命令。注意到,在之前有循环的代码中,这里仅用了一行代码,向量
v
v
v作为输入,
u
u
u作为输出。你已经知道为什么需要循环,并且通过右边代码实现,效率会明显的快于循环方式。
事实上,numpy库有很多向量函数。比如 u=np.log是计算对数函数( l o g log log)、 np.abs() 计算数据的绝对值、np.maximum(v, 0) 按元素计算 v v v中每个元素和和0相比的最大值,v**2 代表获得元素 v v v 每个值的平方、 1/v 获取 v v v 中每个元素的倒数等等。所以当你想写循环时候,检查numpy是否存在类似的内置函数,从而避免使用循环(loop)方式。
那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看我们是否能简化两个计算过程中的某一步。这是我们逻辑回归的求导代码,有两层循环。在这例子我们有
n
n
n个特征值。如果你有超过两个特征时,需要循环
d
w
1
dw_1
dw1 、
d
w
2
dw_2
dw2 、
d
w
3
dw_3
dw3 等等。所以
j
j
j 的实际值是1、2 和
n
x
n_x
nx,就是你想要更新的值。所以我们想要消除第二循环,在这一行,这样我们就不用初始化
d
w
1
dw_1
dw1 ,
d
w
2
dw_2
dw2 都等于0。去掉这些,而是定义
d
w
dw
dw 为一个向量,设置
u
=
n
p
.
z
e
r
o
s
(
n
(
x
)
,
1
)
u=np.zeros(n(x),1)
u=np.zeros(n(x),1)。定义了一个
x
x
x行的一维向量,从而替代循环。我们仅仅使用了一个向量操作
d
w
=
d
w
+
x
(
i
)
d
z
(
i
)
dw=dw+x^{(i)}dz^{(i)}
dw=dw+x(i)dz(i) 。最后,我们得到
d
w
=
d
w
/
m
dw=dw/m
dw=dw/m 。现在我们通过将两层循环转成一层循环,我们仍然还有这个循环训练样本。
希望这个视频给了你一点向量化感觉,减少一层循环使你代码更快,但事实证明我们能做得更好。
向量化逻辑回归(Vectorizing Logistic Regression)
我们已经讨论过向量化是如何显著加速你的代码,在本次视频中我们将讨论如何实现逻辑回归的向量化计算。这样就能处理整个数据集,甚至不会用一个明确的for循环就能实现对于整个数据集梯度下降算法的优化。我对这项技术感到非常激动,并且当我们后面谈到神经网络时同样也不会用到一个明确的 for 循环。
让我们开始吧,首先我们回顾一下逻辑回归的前向传播步骤。所以,如果你有 m m m 个训练样本,然后对第一个样本进行预测,你需要这样计算。计算 z z z,我正在使用这个熟悉的公式 z ( 1 ) = w T x ( 1 ) + b z^{(1)}=w^{T}x^{(1)}+b z(1)=wTx(1)+b 。然后计算激活函数 a ( 1 ) = σ ( z ( 1 ) ) a^{(1)}=\sigma (z^{(1)}) a(1)=σ(z(1)) ,计算第一个样本的预测值 y y y 。
然后对第二个样本进行预测,你需要计算 z ( 2 ) = w T x ( 2 ) + b z^{(2)}=w^{T}x^{(2)}+b z(2)=wTx(2)+b , a ( 2 ) = σ ( z ( 2 ) ) a^{(2)}=\sigma (z^{(2)}) a(2)=σ(z(2)) 。然后对第三个样本进行预测,你需要计算 z ( 3 ) = w T x ( 3 ) + b z^{(3)}=w^{T}x^{(3)}+b z(3)=wTx(3)+b , a ( 3 ) = σ ( z ( 3 ) ) a^{(3)}=\sigma (z^{(3)}) a(3)=σ(z(3)) ,依次类推。如果你有 m m m 个训练样本,你可能需要这样做 m m m 次,可以看出,为了完成前向传播步骤,即对我们的 m m m 个样本都计算出预测值。有一个办法可以并且不需要任何一个明确的for循环。让我们来看一下你该怎样做。
首先,回忆一下我们曾经定义了一个矩阵 X X X 作为你的训练输入,(如下图中蓝色 X X X )像这样在不同的列中堆积在一起。这是一个 n x n_x nx 行 m m m 列的矩阵。我现在将它写为Python numpy的形式 ( n x , m ) (n_{x},m) (nx,m) ,这只是表示 X X X 是一个 n x n_x nx 乘以 m m m 的矩阵 R n x × m R^{n_x \times m} Rnx×m。
现在我首先想做的是告诉你该如何在一个步骤中计算
z
1
z_1
z1、
z
2
z_2
z2 、
z
3
z_3
z3 等等。实际上,只用了一行代码。所以,我打算先构建一个
1
×
m
1\times m
1×m 的矩阵,实际上它是一个行向量,同时我准备计算
z
(
1
)
z^{(1)}
z(1),
z
(
2
)
z^{(2)}
z(2) ……一直到
z
(
m
)
z^{(m)}
z(m) ,所有值都是在同一时间内完成。结果发现它可以表达为
w
w
w 的转置乘以大写矩阵
x
x
x 然后加上向量
[
b
b
.
.
.
b
]
[b b...b]
[bb...b] ,
(
[
z
(
1
)
z
(
2
)
.
.
.
z
(
m
)
]
=
w
T
+
[
b
b
.
.
.
b
]
)
([z^{(1)} z^{(2)}...z^{(m)}]=w^{T}+[bb...b])
([z(1)z(2)...z(m)]=wT+[bb...b]) 。
[
b
b
.
.
.
b
]
[b b...b]
[bb...b] 是一个
1
×
m
1\times m
1×m 的向量或者
1
×
m
1\times m
1×m 的矩阵或者是一个
m
m
m 维的行向量。所以希望你熟悉矩阵乘法,你会发现的
w
w
w 转置乘以
x
(
1
)
x^{(1)}
x(1) ,
x
(
2
)
x^{(2)}
x(2) 一直到
x
(
m
)
x^{(m)}
x(m) 。所以
w
w
w 转置可以是一个行向量。所以第一项
w
T
X
w^{T}X
wTX 将计算
w
w
w 的转置乘以
x
(
1
)
x^{(1)}
x(1),
w
w
w 转置乘以
x
(
2
)
x^{(2)}
x(2) 等等。然后我们加上第二项
[
b
b
.
.
.
b
]
[b b...b]
[bb...b] ,你最终将
b
b
b 加到了每个元素上。所以你最终得到了另一个
1
×
m
1\times m
1×m 的向量,
[
z
(
1
)
z
(
2
)
.
.
.
z
(
m
)
]
=
w
T
X
+
[
b
b
.
.
.
b
]
=
[
w
T
x
(
1
)
+
b
,
w
T
x
(
2
)
+
b
.
.
.
w
T
x
(
m
)
+
b
]
[z^{(1)} z^{(2)}...z^{(m)}]=w^{T}X+[b b...b]=[w^{T}x^{(1)}+b,w^{T}x^{(2)}+b...w^{T}x^{(m)}+b]
[z(1)z(2)...z(m)]=wTX+[bb...b]=[wTx(1)+b,wTx(2)+b...wTx(m)+b] 。
w T x ( 1 ) + b w^{T}x^{(1)}+b wTx(1)+b 这是第一个元素, w T x ( 2 ) + b w^{T}x^{(2)}+b wTx(2)+b 这是第二个元素, w T x ( m ) + b w^{T}x^{(m)}+b wTx(m)+b 这是第 m m m 个元素。
如果你参照上面的定义,第一个元素恰好是 z ( 1 ) z^{(1)} z(1) 的定义,第二个元素恰好是 z ( 2 ) z^{(2)} z(2) 的定义,等等。所以,因为 X X X是一次获得的,当你得到你的训练样本,一个一个横向堆积起来,这里我将 [ z ( 1 ) z ( 2 ) . . . z ( m ) ] [z^{(1)} z^{(2)} ... z^{(m)}] [z(1)z(2)...z(m)] 定义为大写的 Z Z Z ,你用小写 z z z 表示并将它们横向排在一起。所以当你将不同训练样本对应的小写 x x x 横向堆积在一起时得到大写变量 X X X 并且将小写变量也用相同方法处理,将它们横向堆积起来,你就得到大写变量 Z Z Z 。结果发现,为了计算 W T X + [ b b . . . b ] W^{T}X+[b b ... b] WTX+[bb...b] ,numpy命令是 Z = n p . d o t ( w . T , X ) + b Z=np.dot(w.T,X)+b Z=np.dot(w.T,X)+b。这里在Python中有一个巧妙的地方,这里 b b b 是一个实数,或者你可以说是一个 1 × 1 1\times 1 1×1 矩阵,只是一个普通的实数。但是当你将这个向量加上这个实数时,Python自动把这个实数 b b b 扩展成一个 1 × m 1\times m 1×m 的行向量。所以这种情况下的操作似乎有点不可思议,它在Python中被称作广播(brosdcasting),目前你不用对此感到顾虑,我们将在下一个视频中进行进一步的讲解。话说回来它只用一行代码,用这一行代码,你可以计算大写的 Z Z Z,而大写 Z Z Z 是一个包含所有小写 z ( 1 ) z^{(1)} z(1) 到 $ z^{(m)}$ 的 1 × m 1\times m 1×m 的矩阵。这就是 Z Z Z 的内容,关于变量 a a a 又是如何呢?
我们接下来要做的就是找到一个同时计算 [ a ( 1 ) a ( 2 ) . . . a ( m ) ] [a^{(1)} a^{(2)} ... a^{(m)}] [a(1)a(2)...a(m)] 的方法。就像把小写 x x x 堆积起来得到大写 X X X 和横向堆积小写 z z z 得到大写 Z Z Z 一样,堆积小写变量 a a a 将形成一个新的变量,我们将它定义为大写 A A A。在编程作业中,你将看到怎样用一个向量在sigmoid函数中进行计算。所以sigmoid函数中输入大写 Z Z Z 作为变量并且非常高效地输出大写 A A A。你将在编程作业中看到它的细节。
总结一下,在这张幻灯片中我们已经看到,不需要for循环,利用 m m m 个训练样本一次性计算出小写 z z z 和小写 a a a,用一行代码即可完成。
Z = np.dot(w.T,X) + b
这一行代码: A = [ a ( 1 ) a ( 2 ) . . . a ( m ) ] = σ ( Z ) A=[a^{(1)} a^{(2)} ... a^{(m)}]=\sigma (Z) A=[a(1)a(2)...a(m)]=σ(Z) ,通过恰当地运用 σ \sigma σ一次性计算所有 a a a。这就是在同一时间内你如何完成一个所有 m m m 个训练样本的前向传播向量化计算。
概括一下,你刚刚看到如何利用向量化在同一时间内高效地计算所有的激活函数的所有 a a a值。接下来,可以证明,你也可以利用向量化高效地计算反向传播并以此来计算梯度。让我们在下一个视频中看该如何实现。
向量化logistic回归的梯度输出(Vectorizing Logistic Regression’s Gradient)
注:本节中大写字母代表向量,小写字母代表元素
如何向量化计算的同时,对整个训练集预测结果 a a a,这是我们之前已经讨论过的内容。在本次视频中我们将学习如何向量化地计算 m m m个训练数据的梯度,本次视频的重点是如何同时计算 m m m 个数据的梯度,并且实现一个非常高效的逻辑回归算法**(Logistic Regression**)。
之前我们在讲梯度计算的时候,列举过几个例子, d z ( 1 ) = a ( 1 ) − y ( 1 ) dz^{(1)}=a^{(1)}-y^{(1)} dz(1)=a(1)−y(1), d z ( 2 ) = a ( 2 ) − y ( 2 ) dz^{(2)}=a^{(2)}-y^{(2)} dz(2)=a(2)−y(2) ……等等一系列类似公式。现在,对 m m m个训练数据做同样的运算,我们可以定义一个新的变量 d Z = [ d z ( 1 ) , d z ( 2 ) . . . d z ( m ) ] dZ=[dz^{(1)} ,dz^{(2)} ... dz^{(m)}] dZ=[dz(1),dz(2)...dz(m)] ,所有的 d z dz dz 变量横向排列,因此, d Z dZ dZ 是一个 1 × m 1\times m 1×m 的矩阵,或者说,一个 m m m 维行向量。在之前的幻灯片中,我们已经知道如何计算 A A A,即 [ a ( 1 ) , a ( 2 ) . . . a ( m ) ] [a^{(1)},a^{(2)} ... a^{(m)}] [a(1),a(2)...a(m)],我们需要找到这样的一个行向量 Y = [ y ( 1 ) y ( 2 ) . . . y ( m ) ] Y=[y^{(1)} y^{(2)} ... y^{(m)}] Y=[y(1)y(2)...y(m)] ,由此,我们可以这样计算 d Z = A − Y = [ a ( 1 ) − y ( 1 ) a ( 2 ) − y ( 2 ) . . . a ( m ) − y ( m ) ] dZ=A-Y=[a^{(1)}-y^{(1)} a^{(2)}-y^{(2)} ... a^{(m)}-y^{(m)}] dZ=A−Y=[a(1)−y(1)a(2)−y(2)...a(m)−y(m)],不难发现第一个元素就是 d z ( 1 ) dz^{(1)} dz(1),第二个元素就是 d z ( 2 ) dz^{(2)} dz(2) ……所以我们现在仅需一行代码,就可以同时完成这所有的计算。
在之前的实现中,我们已经去掉了一个for循环,但我们仍有一个遍历训练集的循环,如下所示:
d w = 0 dw=0 dw=0
d w + = x ( 1 ) ∗ d z ( 1 ) dw + = x^{(1)}*{dz}^{(1)} dw+=x(1)∗dz(1)
d w + = x ( 2 ) ∗ d z ( 2 ) dw + = x^{(2)}\ *dz^{(2)} dw+=x(2) ∗dz(2)
………….
d w + = x ( m ) ∗ d z ( m ) dw + = x^{(m)}*{dz}^{(m)} dw+=x(m)∗dz(m)
d w = d w m dw = \frac{{dw}}{m} dw=mdw
d b = 0 db = 0 db=0
d b + = d z ( 1 ) db + = {dz}^{(1)} db+=dz(1)
d b + = d z ( 2 ) db + = {dz}^{(2)} db+=dz(2)
………….
d b + = d z ( m ) db + = dz^{(m)} db+=dz(m)
d b = d b m db = \frac{{db}}{m} db=mdb
上述(伪)代码就是我们在之前实现中做的,我们已经去掉了一个for循环,但用上述方法计算 d w dw dw 仍然需要一个循环遍历训练集,我们现在要做的就是将其向量化!
首先我们来看 d b db db,不难发现 d b = 1 m ∑ i = 1 m d z ( i ) db=\frac{1}{m}\sum_{i=1}^{m}dz^{(i)} db=m1i=1∑mdz(i) , 之前的讲解中,我们知道所有的 d z i ) dz^{i)} dzi)已经组成一个行向量 d Z dZ dZ了,所以在Python中,我们很容易地想到 d b = 1 m n p . s u m ( d Z ) db=\frac{1}{m}np.sum(dZ) db=m1np.sum(dZ);接下来看 d w dw dw,我们先写出它的公式 d w = 1 m X d z T dw=\frac{1}{m}Xdz^{T} dw=m1XdzT 其中, X X X 是一个行向量。因此展开后 d w = 1 m ( x ( 1 ) d z ( 1 ) + x ( 2 ) d z ( 2 ) + . . . + x m d z m ) dw=\frac{1}{m}(x^{(1)}dz^{(1)}+x^{(2)}dz^{(2)}+...+x^{m}dz^{m}) dw=m1(x(1)dz(1)+x(2)dz(2)+...+xmdzm) 。因此我们可以仅用两行代码进行计算: d b = 1 m ∗ n p . s u m ( d Z ) db=\frac{1}{m}*np.sum(dZ) db=m1∗np.sum(dZ), d w = 1 m X d z T dw=\frac{1}{m}Xdz^{T} dw=m1XdzT。这样,我们就避免了在训练集上使用for循环。
现在,让我们回顾一下,看看我们之前怎么实现的逻辑回归,可以发现,没有向量化是非常低效的,如下图所示代码:
我们的目标是不使用for循环,而是向量,我们可以这么做:
Z = w T X + b = n p . d o t ( w . T , X ) + b Z = w^{T}X + b = np.dot( w.T,X)+b Z=wTX+b=np.dot(w.T,X)+b
A = σ ( Z ) A = \sigma( Z ) A=σ(Z)
d Z = A − Y dZ = A - Y dZ=A−Y
d w = 1 m X d z T {{dw} = \frac{1}{m}Xdz^{T}\ } dw=m1XdzT
d b = 1 m ∗ n p . s u m ( d Z ) db= \frac{1}{m}*np.sum( dZ) db=m1∗np.sum(dZ)
w : = w − a ∗ d w w: = w - a*dw w:=w−a∗dw
b : = b − a ∗ d b b: = b - a*db b:=b−a∗db
现在我们利用前五个公式完成了前向和后向传播,也实现了对所有训练样本进行预测和求导,再利用后两个公式,梯度下降更新参数。我们的目的是不使用for循环,所以我们就通过一次迭代实现一次梯度下降,但如果你希望多次迭代进行梯度下降,那么仍然需要for循环,放在最外层。不过我们还是觉得一次迭代就进行一次梯度下降,避免使用任何循环比较舒服一些。
最后,我们得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法,我们将在下次视频中讨论Python中的Broadcasting技术。
Python 中的广播(Broadcasting in Python)
首先是numpy广播机制
如果两个数组的后缘维度的轴长度相符或其中一方的轴长度为1,则认为它们是广播兼容的。广播会在缺失维度和轴长度为1的维度上进行。
后缘维度的轴长度:A.shape[-1] 即矩阵维度元组中的最后一个位置的值
对于视频中卡路里计算的例子,矩阵 A 3 , 4 A_{3,4} A3,4 后缘维度的轴长度是4,而矩阵 c a l 1 , 4 cal_{1,4} cal1,4 的后缘维度也是4,则他们满足后缘维度轴长度相符,可以进行广播。广播会在轴长度为1的维度进行,轴长度为1的维度对应axis=0,即垂直方向,矩阵 cal 1 , 4 \text{cal}{1,4} cal1,4 沿axis=0(垂直方向)复制成为 KaTeX parse error: Expected '}', got '_' at position 10: \text{cal_̲temp}{3,4} ,之后两者进行逐元素除法运算。
现在解释上图中的例子
矩阵 A m , n A_{m,n} Am,n 和矩阵 B 1 , n B_{1,n} B1,n 进行四则运算,后缘维度轴长度相符,可以广播,广播沿着轴长度为1的轴进行,即 B 1 , n B_{1,n} B1,n 广播成为 B m , n ′ {B_{m,n}}' Bm,n′ ,之后做逐元素四则运算。
矩阵 A m , n A_{m,n} Am,n 和矩阵 B m , 1 B_{m,1} Bm,1 进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为1,可以广播,广播沿着轴长度为1的轴进行,即 B m , 1 B_{m,1} Bm,1 广播成为 B m , n ′ {B_{m,n}}' Bm,n′ ,之后做逐元素四则运算。
矩阵 A m , 1 A_{m,1} Am,1 和常数$ R$ 进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为1,可以广播,广播沿着缺失维度和轴长度为1的轴进行,缺失维度就是axis=0,轴长度为1的轴是axis=1,即 R R R广播成为 B m , 1 ′ {B_{m,1}}' Bm,1′ ,之后做逐元素四则运算。
最后,对于Matlab/Octave 有类似功能的函数bsxfun。
总结一下broadcasting,可以看看下面的图:
关于 python _ numpy 向量的说明(A note on python or numpy vectors)
本节主要讲Python中的numpy一维数组的特性,以及与行向量或列向量的区别。并介绍了老师在实际应用中的一些小技巧,去避免在coding中由于这些特性而导致的bug。
Python的特性允许你使用广播(broadcasting)功能,这是Python的numpy程序语言库中最灵活的地方。而我认为这是程序语言的优点,也是缺点。优点的原因在于它们创造出语言的表达性,Python语言巨大的灵活性使得你仅仅通过一行代码就能做很多事情。但是这也是缺点,由于广播巨大的灵活性,有时候你对于广播的特点以及广播的工作原理这些细节不熟悉的话,你可能会产生很细微或者看起来很奇怪的bug。例如,如果你将一个列向量添加到一个行向量中,你会以为它报出维度不匹配或类型错误之类的错误,但是实际上你会得到一个行向量和列向量的求和。
在Python的这些奇怪的影响之中,其实是有一个内在的逻辑关系的。但是如果对Python不熟悉的话,我就曾经见过的一些学生非常生硬、非常艰难地去寻找bug。所以我在这里想做的就是分享给你们一些技巧,这些技巧对我非常有用,它们能消除或者简化我的代码中所有看起来很奇怪的bug。同时我也希望通过这些技巧,你也能更容易地写没有bug的Python和numpy代码。
为了演示Python-numpy的一个容易被忽略的效果,特别是怎样在Python-numpy中构造向量,让我来做一个快速示范。首先设置 a = n p . r a n d o m . r a n d n ( 5 ) a=np.random.randn(5) a=np.random.randn(5),这样会生成存储在数组 a a a 中的5个高斯随机数变量。之后输出 a a a,从屏幕上可以得知,此时 a a a 的shape(形状)是一个 ( 5 , ) (5,) (5,)的结构。这在Python中被称作一个一维数组。它既不是一个行向量也不是一个列向量,这也导致它有一些不是很直观的效果。举个例子,如果我输出一个转置阵,最终结果它会和 a a a看起来一样,所以 a a a和 a a a的转置阵最终结果看起来一样。而如果我输出 a a a和 a a a的转置阵的内积,你可能会想: a a a乘以 a a a的转置返回给你的可能会是一个矩阵。但是如果我这样做,你只会得到一个数。
所以建议你编写神经网络时,不要使用shape为 (5,)、(n,) 或者其他一维数组的数据结构。相反,如果你设置
a
a
a 为
(
5
,
1
)
(5,1)
(5,1),那么这就将置于5行1列向量中。在先前的操作里
a
a
a 和
a
a
a 的转置看起来一样,而现在这样的
a
a
a 变成一个新的
a
a
a 的转置,并且它是一个行向量。请注意一个细微的差别,在这种数据结构中,当我们输出
a
a
a 的转置时有两对方括号,而之前只有一对方括号,所以这就是1行5列的矩阵和一维数组的差别。
如果你输出
a
a
a 和
a
a
a 的转置的乘积,然后会返回给你一个向量的外积,是吧?所以这两个向量的外积返回给你的是一个矩阵。
就我们刚才看到的,再进一步说明。首先我们刚刚运行的命令是这个
(
a
=
n
p
.
r
a
n
d
o
m
.
r
a
n
d
n
(
5
)
)
(a=np.random.randn(5))
(a=np.random.randn(5)),它生成了一个数据结构
a
a
a,其中
a
.
s
h
a
p
e
a.shape
a.shape是
(
5
,
)
(5,)
(5,)。这被称作
a
a
a 的一维数组,同时这也是一个非常有趣的数据结构。它不像行向量和列向量那样表现的很一致,这使得它带来一些不直观的影响。所以我建议,当你在编程练习或者在执行逻辑回归和神经网络时,你不需要使用这些一维数组。
相反,如果你每次创建一个数组,你都得让它成为一个列向量,产生一个 ( 5 , 1 ) (5,1) (5,1)向量或者你让它成为一个行向量,那么你的向量的行为可能会更容易被理解。所以在这种情况下, a . s h a p e a.shape a.shape等同于 ( 5 , 1 ) (5,1) (5,1)。这种表现很像 a a a,但是实际上却是一个列向量。同时这也是为什么当它是一个列向量的时候,你能认为这是矩阵 ( 5 , 1 ) (5,1) (5,1);同时这里 a . s h a p e a.shape a.shape 将要变成 ( 1 , 5 ) (1,5) (1,5),这就像行向量一样。所以当你需要一个向量时,我会说用这个或那个(column vector or row vector),但绝不会是一维数组。
我写代码时还有一件经常做的事,那就是如果我不完全确定一个向量的维度(dimension),我经常会扔进一个断言语句(assertion statement)。像这样,去确保在这种情况下是一个 ( 5 , 1 ) (5,1) (5,1)向量,或者说是一个列向量。这些断言语句实际上是要去执行的,并且它们也会有助于为你的代码提供信息。所以不论你要做什么,不要犹豫直接插入断言语句。如果你不小心以一维数组来执行,你也能够重新改变数组维数 a = r e s h a p e a=reshape a=reshape,表明一个 ( 5 , 1 ) (5,1) (5,1)数组或者一个 ( 1 , 5 ) (1,5) (1,5)数组,以致于它表现更像列向量或行向量。
我有时候看见学生因为一维数组不直观的影响,难以定位bug而告终。通过在原先的代码里清除一维数组,我的代码变得更加简洁。而且实际上就我在代码中表现的事情而言,我从来不使用一维数组。因此,要去简化你的代码,而且不要使用一维数组。总是使用
n
×
1
n \times 1
n×1 维矩阵(基本上是列向量),或者
1
×
n
1 \times n
1×n 维矩阵(基本上是行向量),这样你可以减少很多assert语句来节省核矩阵和数组的维数的时间。另外,为了确保你的矩阵或向量所需要的维数时,不要羞于 reshape 操作。
Jupyter/iPython Notebooks快速入门(Quick tour of Jupyter/iPython Notebooks)
logistic 损失函数的解释(Explanation of logistic regression cost function)
回想一下,在逻辑回归中,需要预测的结果
y
^
\hat{y}
y^,可以表示为
y
^
=
σ
(
w
T
x
+
b
)
\hat{y}=\sigma(w^{T}x+b)
y^=σ(wTx+b),
σ
\sigma
σ是我们熟悉的
S
S
S型函数
σ
(
z
)
=
σ
(
w
T
x
+
b
)
=
1
1
+
e
−
z
\sigma(z)=\sigma(w^{T}x+b)=\frac{1}{1+e^{-z}}
σ(z)=σ(wTx+b)=1+e−z1 。我们约定
y
^
=
p
(
y
=
1
∣
x
)
\hat{y}=p(y=1|x)
y^=p(y=1∣x) ,即算法的输出
y
^
\hat{y}
y^ 是给定训练样本
x
x
x 条件下
y
y
y 等于1的概率。换句话说,如果
y
=
1
y=1
y=1,在给定训练样本
x
x
x 条件下
y
=
y
^
y=\hat{y}
y=y^;反过来说,如果
y
=
0
y=0
y=0,在给定训练样本
x
x
x条件下
y
y
y 等于1减去
y
^
(
y
=
1
−
y
^
)
\hat{y}(y=1-\hat{y})
y^(y=1−y^),因此,如果
y
^
\hat{y}
y^ 代表
y
=
1
y=1
y=1 的概率,那么
1
−
y
^
1-\hat{y}
1−y^就是
y
=
0
y=0
y=0的概率。接下来,我们就来分析这两个条件概率公式。
这两个条件概率公式定义形式为
p
(
y
∣
x
)
p(y|x)
p(y∣x)并且代表了
y
=
0
y=0
y=0 或者
y
=
1
y=1
y=1 这两种情况,我们可以将这两个公式合并成一个公式。需要指出的是我们讨论的是二分类问题的损失函数,因此,
y
y
y的取值只能是0或者1。上述的两个条件概率公式可以合并成如下公式:
p ( y ∣ x ) = y ^ y ( 1 − y ^ ) ( 1 − y ) p(y|x)={\hat{y}}^{y}{(1-\hat{y})}^{(1-y)} p(y∣x)=y^y(1−y^)(1−y)
接下来我会解释为什么可以合并成这种形式的表达式:
(
1
−
y
^
)
(1-\hat{y})
(1−y^)的
(
1
−
y
)
(1-y)
(1−y)次方这行表达式包含了上面的两个条件概率公式,我来解释一下为什么。
第一种情况,假设
y
=
1
y=1
y=1,由于
y
=
1
y=1
y=1,那么
(
y
^
)
y
=
y
^
{(\hat{y})}^{y}=\hat{y}
(y^)y=y^,因为
y
^
\hat{y}
y^的1次方等于
y
^
\hat{y}
y^,
1
−
(
1
−
y
^
)
(
1
−
y
)
1-{(1-\hat{y})}^{(1-y)}
1−(1−y^)(1−y)的指数项
(
1
−
y
)
(1-y)
(1−y)等于0,由于任何数的0次方都是1,
y
^
\hat{y}
y^乘以1等于
y
^
\hat{y}
y^。因此当
y
=
1
y=1
y=1时
p
(
y
∣
x
)
=
y
^
p(y|x)=\hat{y}
p(y∣x)=y^(图中绿色部分)。
第二种情况,当 y = 0 y=0 y=0 时 p ( y ∣ x ) p(y|x) p(y∣x) 等于多少呢? 假设 y = 0 y=0 y=0, y ^ \hat{y} y^的 y y y次方就是 y ^ \hat{y} y^ 的0次方,任何数的0次方都等于1,因此 p ( y ∣ x ) = 1 × ( 1 − y ^ ) 1 − y p(y|x)=1×{(1-\hat{y})}^{1-y} p(y∣x)=1×(1−y^)1−y ,前面假设 y = 0 y=0 y=0 因此 ( 1 − y ) (1-y) (1−y)就等于1,因此 p ( y ∣ x ) = 1 × ( 1 − y ^ ) p(y|x)=1×(1-\hat{y}) p(y∣x)=1×(1−y^)。因此在这里当 y = 0 y=0 y=0时, p ( y ∣ x ) = 1 − y ^ p(y|x)=1-\hat{y} p(y∣x)=1−y^。这就是这个公式(第二个公式,图中紫色字体部分)的结果。
因此,刚才的推导表明 p ( y ∣ x ) = y ^ ( y ) ( 1 − y ^ ) ( 1 − y ) p(y|x)={\hat{y}}^{(y)}{(1-\hat{y})}^{(1-y)} p(y∣x)=y^(y)(1−y^)(1−y),就是 p ( y ∣ x ) p(y|x) p(y∣x) 的完整定义。由于 log 函数是严格单调递增的函数,最大化 l o g ( p ( y ∣ x ) ) log(p(y|x)) log(p(y∣x)) 等价于最大化 p ( y ∣ x ) p(y|x) p(y∣x) 并且地计算 p ( y ∣ x ) p(y|x) p(y∣x) 的 log对数,就是计算 l o g ( y ^ ( y ) ( 1 − y ^ ) ( 1 − y ) ) log({\hat{y}}^{(y)}{(1-\hat{y})}^{(1-y)}) log(y^(y)(1−y^)(1−y)) (其实就是将 p ( y ∣ x ) p(y|x) p(y∣x) 代入),通过对数函数化简为:
y l o g y ^ + ( 1 − y ) l o g ( 1 − y ^ ) ylog\hat{y}+(1-y)log(1-\hat{y}) ylogy^+(1−y)log(1−y^)
而这就是我们前面提到的损失函数的负数 ( − L ( y ^ , y ) ) (-L(\hat{y},y)) (−L(y^,y)) ,前面有一个负号的原因是当你训练学习算法时需要算法输出值的概率是最大的(以最大的概率预测这个值),然而在逻辑回归中我们需要最小化损失函数,因此最小化损失函数与最大化条件概率的对数 l o g ( p ( y ∣ x ) ) log(p(y|x)) log(p(y∣x)) 关联起来了,因此这就是单个训练样本的损失函数表达式。
在
m
m
m个训练样本的整个训练集中又该如何表示呢,让我们一起来探讨一下。
让我们一起来探讨一下,整个训练集中标签的概率,更正式地来写一下。假设所有的训练样本服从同一分布且相互独立,也即独立同分布的,所有这些样本的联合概率就是每个样本概率的乘积:
P ( labels in training set ) = ∏ i = 1 m P ( y ( i ) ∣ x ( i ) ) P\left(\text{labels in training set} \right) = \prod_{i =1}^{m}{P(y^{(i)}|x^{(i)})} P(labels in training set)=∏i=1mP(y(i)∣x(i))。
如果你想做最大似然估计,需要寻找一组参数,使得给定样本的观测值概率最大,但令这个概率最大化等价于令其对数最大化,在等式两边取对数:
l o g p ( labels in training set ) = l o g ∏ i = 1 m P ( y ( i ) ∣ x ( i ) ) = ∑ i = 1 m l o g P ( y ( i ) ∣ x ( i ) ) = ∑ i = 1 m − L ( y ^ ( i ) , y ( i ) ) logp\left( \text{labels in training set} \right) = log\prod_{i =1}^{m}{P(y^{(i)}|x^{(i)})} = \sum_{i = 1}^{m}{logP(y^{(i)}|x^{(i)})} = \sum_{i =1}^{m}{- L(\hat y^{(i)},y^{(i)})} logp(labels in training set)=log∏i=1mP(y(i)∣x(i))=∑i=1mlogP(y(i)∣x(i))=∑i=1m−L(y^(i),y(i))
在统计学里面,有一个方法叫做最大似然估计,即求出一组参数,使这个式子取最大值,也就是说,使得这个式子取最大值, ∑ i = 1 m − L ( y ^ ( i ) , y ( i ) ) \sum_{i= 1}^{m}{- L(\hat y^{(i)},y^{(i)})} ∑i=1m−L(y^(i),y(i)),可以将负号移到求和符号的外面, − ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) - \sum_{i =1}^{m}{L(\hat y^{(i)},y^{(i)})} −∑i=1mL(y^(i),y(i)),这样我们就推导出了前面给出的logistic回归的成本函数 J ( w , b ) = ∑ i = 1 m L ( y ^ ( i ) , y ( ^ i ) ) J(w,b)= \sum_{i = 1}^{m}{L(\hat y^{(i)},y^{\hat( i)})} J(w,b)=∑i=1mL(y^(i),y(^i))。
由于训练模型时,目标是让成本函数最小化,所以我们不是直接用最大似然概率,要去掉这里的负号,最后为了方便,可以对成本函数进行适当的缩放,我们就在前面加一个额外的常数因子
1
m
\frac{1}{m}
m1,即:
J
(
w
,
b
)
=
1
m
∑
i
=
1
m
L
(
y
^
(
i
)
,
y
(
i
)
)
J(w,b)= \frac{1}{m}\sum_{i = 1}^{m}{L(\hat y^{(i)},y^{(i)})}
J(w,b)=m1∑i=1mL(y^(i),y(i))。
总结一下,为了最小化成本函数 J ( w , b ) J(w,b) J(w,b),我们从logistic回归模型的最大似然估计的角度出发,假设训练集中的样本都是独立同分布的条件下。尽管这节课是选修性质的,但还是感谢观看本节视频。我希望通过本节课您能更好地明白逻辑回归的损失函数,为什么是那种形式,明白了损失函数的原理,希望您能继续完成课后的练习,前面课程的练习以及本周的测验,在课后的小测验和编程练习中,祝您好运。