本文是吴恩达老师的深度学习课程[1]笔记部分。
作者:黄海广[2]
主要编写人员:黄海广、林兴木(第四所有底稿,第五课第一二周,第三周前三节)、祝彦森:(第三课所有底稿)、贺志尧(第五课第三周底稿)、王翔、胡瀚文、 余笑、 郑浩、李怀松、 朱越鹏、陈伟贺、 曹越、 路皓翔、邱牧宸、 唐天泽、 张浩、 陈志豪、 游忍、 泽霖、沈伟臣、 贾红顺、 时超、 陈哲、赵一帆、 胡潇杨、段希、于冲、张鑫倩
参与编辑人员:黄海广、陈康凯、石晴路、钟博彦、向伟、严凤龙、刘成 、贺志尧、段希、陈瑶、林家泳、王翔、 谢士晨、蒋鹏
备注:笔记和作业(含数据、原始作业文件)、视频都在 github[3]中下载。
我将陆续将课程笔记发布在公众号“机器学习初学者”,敬请关注。
2.9 逻辑回归中的梯度下降(Logistic Regression Gradient Descent)
本节我们讨论怎样通过计算偏导数来实现逻辑回归的梯度下降算法。它的关键点是几个重要公式,其作用是用来实现逻辑回归中梯度下降算法。但是在本节视频中,我将使用计算图对梯度下降算法进行计算。我必须要承认的是,使用计算图来计算逻辑回归的梯度下降算法有点大材小用了。但是,我认为以这个例子作为开始来讲解,可以使你更好的理解背后的思想。从而在讨论神经网络时,你可以更深刻而全面地理解神经网络。接下来让我们开始学习逻辑回归的梯度下降算法。
假设样本只有两个特征 和 ,为了计算 ,我们需要输入参数 、 和 ,除此之外还有特征值 和 。因此 的计算公式为: 回想一下逻辑回归的公式定义如下: 其中 损失函数: 代价函数: 假设现在只考虑单个样本的情况,单个样本的代价函数定义如下: 其中 是逻辑回归的输出, 是样本的标签值。现在让我们画出表示这个计算的计算图。这里先复习下梯度下降法, 和 的修正量可以表达如下:
,
如图:在这个公式的外侧画上长方形。然后计算: 也就是计算图的下一步。最后计算损失函数 。有了计算图,我就不需要再写出公式了。因此,为了使得逻辑回归中最小化代价函数 ,我们需要做的仅仅是修改参数 和 的值。前面我们已经讲解了如何在单个训练样本上计算代价函数的前向步骤。现在让我们来讨论通过反向计算出导数。因为我们想要计算出的代价函数 的导数,首先我们需要反向计算出代价函数 关于 的导数,在编写代码时,你只需要用 来表示 。通过微积分得到: 如果你不熟悉微积分,也不必太担心,我们会列出本课程涉及的所有求导公式。那么如果你非常熟悉微积分,我们鼓励你主动推导前面介绍的代价函数的求导公式,使用微积分直接求出 关于变量 的导数。如果你不太了解微积分,也不用太担心。现在我们已经计算出 ,也就是最终输出结果的导数。现在可以再反向一步,在编写Python代码时,你只需要用 来表示代价函数 关于 的导数 ,也可以写成 ,这两种写法都是正确的。 。因为, 并且 , 而 ,因此将这两项相乘,得到:
视频中为了简化推导过程,假设 这个推导的过程就是我之前提到过的链式法则。如果你对微积分熟悉,放心地去推导整个求导过程,如果不熟悉微积分,你只需要知道 已经计算好了。
现在进行最后一步反向推导,也就是计算 和 变化对代价函数 的影响,特别地,可以用: 视频中, 表示 , 表示 , 。因此,关于单个样本的梯度下降算法,你所需要做的就是如下的事情:使用公式 计算 , 使用 计算 , 计算 , 来计算 , 然后: 更新 , 更新 , 更新 。这就是关于单个样本实例的梯度下降算法中参数更新一次的步骤。
现在你已经知道了怎样计算导数,并且实现针对单个训练样本的逻辑回归的梯度下降算法。但是,训练逻辑回归模型不仅仅只有一个训练样本,而是有 个训练样本的整个训练集。因此在下一节视频中,我们将这些思想应用到整个训练样本集中,而不仅仅只是单个样本上。
2.10 m 个样本的梯度下降(Gradient Descent on m Examples)
在之前的视频中,你已经看到如何计算导数,以及应用梯度下降在逻辑回归的一个训练样本上。现在我们想要把它应用在 个训练样本上。
首先,让我们时刻记住有关于损失函数 的定义。
当你的算法输出关于样本 的 , 是训练样本的预测值,即: 。所以我们在前面的幻灯中展示的是对于任意单个训练样本,如何计算微分当你只有一个训练样本。因此 , 和 添上上标 表示你求得的相应的值。如果你面对的是我们在之前的幻灯中演示的那种情况,但只使用了一个训练样本 。现在你知道带有求和的全局代价函数,实际上是 1 到 项各个损失的平均。所以它表明全局代价函数对 的微分,对 的微分也同样是各项损失对 微分的平均。
但之前我们已经演示了如何计算这项,即之前幻灯中演示的如何对单个训练样本进行计算。所以你真正需要做的是计算这些微分,如我们在之前的训练样本上做的。并且求平均,这会给你全局梯度值,你能够把它直接应用到梯度下降算法中。
所以这里有很多细节,但让我们把这些装进一个具体的算法。同时你需要一起应用的就是逻辑回归和梯度下降。
我们初始化
代码流程:
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循环是一个小循环遍历 个训练样本,第二个for循环是一个遍历所有特征的for循环。这个例子中我们只有 2 个特征,所以 等于 2 并且 等于 2。但如果你有更多特征,你开始编写你的因此 , ,你有相似的计算从 一直下去到 。所以看来你需要一个for循环遍历所有 个特征。
当你应用深度学习算法,你会发现在代码中显式地使用for循环使你的算法很低效,同时在深度学习领域会有越来越大的数据集。所以能够应用你的算法且没有显式的for循环会是重要的,并且会帮助你适用于更大的数据集。所以这里有一些叫做向量化技术,它可以允许你的代码摆脱这些显式的for循环。
我想在先于深度学习的时代,也就是深度学习兴起之前,向量化是很棒的。可以使你有时候加速你的运算,但有时候也未必能够。但是在深度学习时代向量化,摆脱 for 循环已经变得相当重要。因为我们越来越多地训练非常大的数据集,因此你真的需要你的代码变得非常高效。所以在接下来的几个视频中,我们会谈到向量化,以及如何应用向量化而连一个for循环都不使用。所以学习了这些,我希望你有关于如何应用逻辑回归,或是用于逻辑回归的梯度下降,事情会变得更加清晰。当你进行编程练习,但在真正做编程练习之前让我们先谈谈向量化。然后你可以应用全部这些东西,应用一个梯度下降的迭代而不使用任何for循环。
2.11 向量化(Vectorization)
向量化是非常基础的去除代码中for循环的艺术,在深度学习安全领域、深度学习实践中,你会经常发现自己训练大数据集,因为深度学习算法处理大数据集效果很棒,所以你的代码运行速度非常重要,否则如果在大数据集上,你的代码可能花费很长时间去运行,你将要等待非常长的时间去得到结果。所以在深度学习领域,运行向量化是一个关键的技巧,让我们举个栗子说明什么是向量化。
在逻辑回归中你需要去计算 , 、 都是列向量。如果你有很多的特征那么就会有一个非常大的向量,所以 , ,所以如果你想使用非向量化方法去计算 ,你需要用如下方式(python)
z=0
for i in range(n_x):
z += w[i]*x[i]
z += b
这是一个非向量化的实现,你会发现这真的很慢,作为一个对比,向量化实现将会非常直接计算 ,代码如下:
z=np.dot(w,x)+b
这是向量化计算 的方法,你将会发现这个非常快
让我们用一个小例子说明一下,在我的我将会写一些代码(以下为教授在他的Jupyter notebook上写的Python代码,)
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循环。
以下代码及运行结果截图:
2.12 向量化的更多例子(More Examples of Vectorization)
从上节视频中,你知道了怎样通过numpy内置函数和避开显式的循环(loop)的方式进行向量化,从而有效提高代码速度。
经验提醒我,当我们在写神经网络程序时,或者在写逻辑(logistic)回归,或者其他神经网络模型时,应该避免写循环(loop)语句。虽然有时写循环(loop)是不可避免的,但是我们可以使用比如numpy的内置函数或者其他办法去计算。当你这样使用后,程序效率总是快于循环(loop)。让我们看另外一个例子。如果你想计算向量 ,这时矩阵乘法定义为,矩阵乘法的定义就是: ,这取决于你怎么定义 值。同样使用非向量化实现, , 并且通过两层循环 ,得到 。现在就有了 和 的两层循环,这就是非向量化。向量化方式就可以用 ,右边这种向量化实现方式,消除了两层循环使得代码运行速度更快。
下面通过另一个例子继续了解向量化。如果你已经有一个向量
,并且想要对向量
的每个元素做指数操作,得到向量
等于
的
,
的
,一直到
的
次方。这里是非向量化的实现方式,首先你初始化了向量
,并且通过循环依次计算每个元素。但事实证明可以通过python的numpy内置函数,帮助你计算这样的单个函数。所以我会引入import numpy as np
,执行
命令。注意到,在之前有循环的代码中,这里仅用了一行代码,向量
作为输入,
作为输出。你已经知道为什么需要循环,并且通过右边代码实现,效率会明显的快于循环方式。
事实上,numpy库有很多向量函数。比如 u=np.log
是计算对数函数(
)、 np.abs()
计算数据的绝对值、np.maximum(v, 0)
按元素计算
中每个元素和和 0 相比的最大值,v**2
代表获得元素
每个值的平方、 1/v
获取
中每个元素的倒数等等。所以当你想写循环时候,检查numpy是否存在类似的内置函数,从而避免使用循环(loop)方式。
那么,将刚才所学到的内容,运用在逻辑回归的梯度下降上,看看我们是否能简化两个计算过程中的某一步。这是我们逻辑回归的求导代码,有两层循环。在这例子我们有 个特征值。如果你有超过两个特征时,需要循环 、 、 等等。所以 的实际值是 1、2 和 ,就是你想要更新的值。所以我们想要消除第二循环,在这一行,这样我们就不用初始化 , 都等于 0。去掉这些,而是定义 为一个向量,设置 。定义了一个 行的一维向量,从而替代循环。我们仅仅使用了一个向量操作 。最后,我们得到
希望这个视频给了你一点向量化感觉,减少一层循环使你代码更快,但事实证明我们能做得更好。所以在下个视频,我们将进一步的讲解逻辑回归,你将会看到更好的监督学习结果。在训练中不需要使用任何 for 循环,你也可以写出代码去运行整个训练集。到此为止一切都好,让我们看下一个视频。
2.13 向量化逻辑回归(Vectorizing Logistic Regression)
我们已经讨论过向量化是如何显著加速你的代码,在本次视频中我们将讨论如何实现逻辑回归的向量化计算。这样就能处理整个数据集,甚至不会用一个明确的 for 循环就能实现对于整个数据集梯度下降算法的优化。我对这项技术感到非常激动,并且当我们后面谈到神经网络时同样也不会用到一个明确的 for 循环。
让我们开始吧,首先我们回顾一下逻辑回归的前向传播步骤。所以,如果你有 个训练样本,然后对第一个样本进行预测,你需要这样计算。计算 ,我正在使用这个熟悉的公式 。然后计算激活函数 ,计算第一个样本的预测值
然后对第二个样本进行预测,你需要计算 , 。然后对第三个样本进行预测,你需要计算 , ,依次类推。如果你有 个训练样本,你可能需要这样做 次,可以看出,为了完成前向传播步骤,即对我们的 个样本都计算出预测值。有一个办法可以并且不需要任何一个明确的for循环。让我们来看一下你该怎样做。
首先,回忆一下我们曾经定义了一个矩阵 作为你的训练输入,(如下图中蓝色 )像这样在不同的列中堆积在一起。这是一个 行 列的矩阵。我现在将它写为Python numpy的形式 ,这只是表示 是一个 乘以 的矩阵 。
现在我首先想做的是告诉你该如何在一个步骤中计算 、 、 等等。实际上,只用了一行代码。所以,我打算先构建一个 的矩阵,实际上它是一个行向量,同时我准备计算 , ……一直到 ,所有值都是在同一时间内完成。结果发现它可以表达为 的转置乘以大写矩阵 然后加上向量 , 。 是一个 的向量或者 的矩阵或者是一个 维的行向量。所以希望你熟悉矩阵乘法,你会发现的 转置乘以 , 一直到 。所以 转置可以是一个行向量。所以第一项 将计算 的转置乘以 , 转置乘以 等等。然后我们加上第二项 ,你最终将 加到了每个元素上。所以你最终得到了另一个 的向量,
这是第一个元素, 这是第二个元素, 这是第
如果你参照上面的定义,第一个元素恰好是 的定义,第二个元素恰好是 的定义,等等。所以,因为 是一次获得的,当你得到你的训练样本,一个一个横向堆积起来,这里我将 定义为大写的 ,你用小写 表示并将它们横向排在一起。所以当你将不同训练样本对应的小写 横向堆积在一起时得到大写变量 并且将小写变量也用相同方法处理,将它们横向堆积起来,你就得到大写变量 。结果发现,为了计算 ,numpy命令是 。这里在Python中有一个巧妙的地方,这里 是一个实数,或者你可以说是一个 矩阵,只是一个普通的实数。但是当你将这个向量加上这个实数时,Python自动把这个实数 扩展成一个 的行向量。所以这种情况下的操作似乎有点不可思议,它在Python中被称作广播(brosdcasting),目前你不用对此感到顾虑,我们将在下一个视频中进行进一步的讲解。话说回来它只用一行代码,用这一行代码,你可以计算大写的 ,而大写 是一个包含所有小写 到 的 的矩阵。这就是 的内容,关于变量
我们接下来要做的就是找到一个同时计算 的方法。就像把小写 堆积起来得到大写 和横向堆积小写 得到大写 一样,堆积小写变量 将形成一个新的变量,我们将它定义为大写 。在编程作业中,你将看到怎样用一个向量在sigmoid函数中进行计算。所以sigmoid函数中输入大写 作为变量并且非常高效地输出大写 。你将在编程作业中看到它的细节。
总结一下,在这张幻灯片中我们已经看到,不需要for循环,利用 个训练样本一次性计算出小写 和小写 ,用一行代码即可完成。
Z = np.dot(w.T,X) + b
这一行代码: ,通过恰当地运用 一次性计算所有 。这就是在同一时间内你如何完成一个所有
概括一下,你刚刚看到如何利用向量化在同一时间内高效地计算所有的激活函数的所有 值。接下来,可以证明,你也可以利用向量化高效地计算反向传播并以此来计算梯度。让我们在下一个视频中看该如何实现。
2.14 向量化 logistic 回归的梯度输出(Vectorizing Logistic Regression's Gradient)
注:本节中大写字母代表向量,小写字母代表元素
如何向量化计算的同时,对整个训练集预测结果 ,这是我们之前已经讨论过的内容。在本次视频中我们将学习如何向量化地计算 个训练数据的梯度,本次视频的重点是如何同时计算
之前我们在讲梯度计算的时候,列举过几个例子, , ……等等一系列类似公式。现在,对 个训练数据做同样的运算,我们可以定义一个新的变量 ,所有的 变量横向排列,因此, 是一个 的矩阵,或者说,一个 维行向量。在之前的幻灯片中,我们已经知道如何计算 ,即 ,我们需要找到这样的一个行向量 ,由此,我们可以这样计算 ,不难发现第一个元素就是 ,第二个元素就是
在之前的实现中,我们已经去掉了一个for循环,但我们仍有一个遍历训练集的循环,如下所示:
………….
………….
上述(伪)代码就是我们在之前实现中做的,我们已经去掉了一个for循环,但用上述方法计算
首先我们来看 ,不难发现 , 之前的讲解中,我们知道所有的 已经组成一个行向量 了,所以在Python中,我们很容易地想到 ;接下来看 ,我们先写出它的公式 其中, 是一个行向量。因此展开后
因此我们可以仅用两行代码进行计算: , 。这样,我们就避免了在训练集上使用 for 循环。
现在,让我们回顾一下,看看我们之前怎么实现的逻辑回归,可以发现,没有向量化是非常低效的,如下图所示代码:
我们的目标是不使用for循环,而是向量,我们可以这么做:
现在我们利用前五个公式完成了前向和后向传播,也实现了对所有训练样本进行预测和求导,再利用后两个公式,梯度下降更新参数。我们的目的是不使用for循环,所以我们就通过一次迭代实现一次梯度下降,但如果你希望多次迭代进行梯度下降,那么仍然需要for循环,放在最外层。不过我们还是觉得一次迭代就进行一次梯度下降,避免使用任何循环比较舒服一些。
最后,我们得到了一个高度向量化的、非常高效的逻辑回归的梯度下降算法,我们将在下次视频中讨论Python中的Broadcasting技术。
2.15 Python 中的广播(Broadcasting in Python)
这是一个不同食物(每 100g)中不同营养成分的卡路里含量表格,表格为 3 行 4 列,列表示不同的食物种类,从左至右依次为苹果,牛肉,鸡蛋,土豆。行表示不同的营养成分,从上到下依次为碳水化合物,蛋白质,脂肪。
那么,我们现在想要计算不同食物中不同营养成分中的卡路里百分比。
现在计算苹果中的碳水化合物卡路里百分比含量,首先计算苹果(100g)中三种营养成分卡路里总和 56+1.2+1.8 = 59,然后用 56/59 = 94.9%算出结果。
可以看出苹果中的卡路里大部分来自于碳水化合物,而牛肉则不同。
对于其他食物,计算方法类似。首先,按列求和,计算每种食物中(100g)三种营养成分总和,然后分别用不用营养成分的卡路里数量除以总和,计算百分比。
那么,能否不使用for循环完成这样的一个计算过程呢?
假设上图的表格是一个 4 行 3 列的矩阵 ,记为 ,接下来我们要使用Python的numpy库完成这样的计算。我们打算使用两行代码完成,第一行代码对每一列进行求和,第二行代码分别计算每种食物每种营养成分的百分比。
在jupyter notebook中输入如下代码,按shift+Enter运行,输出如下。
下面使用如下代码计算每列的和,可以看到输出是每种食物(100g)的卡路里总和。
其中sum
的参数axis=0
表示求和运算按列执行,之后会详细解释。
接下来计算百分比,这条指令将 的矩阵 除以一个 的矩阵,得到了一个 的结果矩阵,这个结果矩阵就是我们要求的百分比含量。
下面再来解释一下A.sum(axis = 0)
中的参数axis
。axis 用来指明将要进行的运算是沿着哪个轴执行,在 numpy 中,0 轴是垂直的,也就是列,而 1 轴是水平的,也就是行。
而第二个A/cal.reshape(1,4)
指令则调用了numpy中的广播机制。这里使用
的矩阵
除以
的矩阵
。技术上来讲,其实并不需要再将矩阵
reshape
(重塑)成
,因为矩阵
本身已经是
了。但是当我们写代码时不确定矩阵维度的时候,通常会对矩阵进行重塑来确保得到我们想要的列向量或行向量。重塑操作reshape
是一个常量时间的操作,时间复杂度是
,它的调用代价极低。
那么一个 的矩阵是怎么和 的矩阵做除法的呢?让我们来看一些更多的广播的例子。
在 numpy 中,当一个 的列向量与一个常数做加法时,实际上会将常数扩展为一个 的列向量,然后两者做逐元素加法。结果就是右边的这个向量。这种广播机制对于行向量和列向量均可以使用。
再看下一个例子。
用一个 的矩阵和一个 的矩阵相加,其泛化形式是 的矩阵和 的矩阵相加。在执行加法操作时,其实是将 的矩阵复制成为
下面是最后一个例子
这里相当于是一个 的矩阵加上一个 的矩阵。在进行运算时,会先将 矩阵水平复制 次,变成一个
广播机制的一般原则如下:
这里我先说一下我本人对numpy广播机制的理解,再解释上面这张 PPT。
首先是numpy广播机制
如果两个数组的后缘维度的轴长度相符或其中一方的轴长度为 1,则认为它们是广播兼容的。广播会在缺失维度和轴长度为 1 的维度上进行。
后缘维度的轴长度:A.shape[-1]
即矩阵维度元组中的最后一个位置的值
对于视频中卡路里计算的例子,矩阵
后缘维度的轴长度是 4,而矩阵
的后缘维度也是 4,则他们满足后缘维度轴长度相符,可以进行广播。广播会在轴长度为 1 的维度进行,轴长度为 1 的维度对应axis=0
,即垂直方向,矩阵
沿axis=0
(垂直方向)复制成为
现在解释上图中的例子
矩阵 和矩阵 进行四则运算,后缘维度轴长度相符,可以广播,广播沿着轴长度为 1 的轴进行,即 广播成为
矩阵 和矩阵 进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为 1,可以广播,广播沿着轴长度为 1 的轴进行,即 广播成为
矩阵
和常数
进行四则运算,后缘维度轴长度不相符,但其中一方轴长度为 1,可以广播,广播沿着缺失维度和轴长度为 1 的轴进行,缺失维度就是axis=0
,轴长度为 1 的轴是axis=1
,即
广播成为
最后,对于Matlab/Octave 有类似功能的函数bsxfun
。
总结一下broadcasting
,可以看看下面的图:
2.16 关于 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中构造向量,让我来做一个快速示范。首先设置,这样会生成存储在数组 中的 5 个高斯随机数变量。之后输出 ,从屏幕上可以得知,此时 的shape(形状)是一个 的结构。这在Python中被称作一个一维数组。它既不是一个行向量也不是一个列向量,这也导致它有一些不是很直观的效果。举个例子,如果我输出一个转置阵,最终结果它会和 看起来一样,所以 和 的转置阵最终结果看起来一样。而如果我输出 和 的转置阵的内积,你可能会想: 乘以 的转置返回给你的可能会是一个矩阵。但是如果我这样做,你只会得到一个数。
所以建议你编写神经网络时,不要使用 shape 为 (5,)、(n,) 或者其他一维数组的数据结构。相反,如果你设置 为 ,那么这就将置于 5 行 1 列向量中。在先前的操作里 和 的转置看起来一样,而现在这样的 变成一个新的 的转置,并且它是一个行向量。请注意一个细微的差别,在这种数据结构中,当我们输出
如果你输出 和
就我们刚才看到的,再进一步说明。首先我们刚刚运行的命令是这个 ,它生成了一个数据结构 ,其中 是 。这被称作
相反,如果你每次创建一个数组,你都得让它成为一个列向量,产生一个 向量或者你让它成为一个行向量,那么你的向量的行为可能会更容易被理解。所以在这种情况下, 等同于 。这种表现很像 ,但是实际上却是一个列向量。同时这也是为什么当它是一个列向量的时候,你能认为这是矩阵 ;同时这里 将要变成 ,这就像行向量一样。所以当你需要一个向量时,我会说用这个或那个(column vector or row vector),但绝不会是一维数组。
我写代码时还有一件经常做的事,那就是如果我不完全确定一个向量的维度(dimension),我经常会扔进一个断言语句(assertion statement)。像这样,去确保在这种情况下是一个 向量,或者说是一个列向量。这些断言语句实际上是要去执行的,并且它们也会有助于为你的代码提供信息。所以不论你要做什么,不要犹豫直接插入断言语句。如果你不小心以一维数组来执行,你也能够重新改变数组维数 ,表明一个 数组或者一个 数组,以致于它表现更像列向量或行向量。
我有时候看见学生因为一维数组不直观的影响,难以定位 bug 而告终。通过在原先的代码里清除一维数组,我的代码变得更加简洁。而且实际上就我在代码中表现的事情而言,我从来不使用一维数组。因此,要去简化你的代码,而且不要使用一维数组。总是使用 维矩阵(基本上是列向量),或者 维矩阵(基本上是行向量),这样你可以减少很多assert语句来节省核矩阵和数组的维数的时间。另外,为了确保你的矩阵或向量所需要的维数时,不要羞于 reshape 操作。
总之,我希望这些建议能帮助你解决一个Python中的 bug,从而使你更容易地完成练习。
2.17 Jupyter/iPython Notebooks 快速入门(Quick tour of Jupyter/iPython Notebooks)
学到现在,你即将要开始处理你的第一个编程作业。但在那之前,让我快速地给你介绍一下在Coursera上的iPython Notebooks工具。
这就是Jupyter iPython Notebooks的界面,你可以通过它连接到Coursera。让我快速地讲解下它的一些特性。关于它的说明已经被写入这个Notebook中。
这里有一些空白区域的代码块,你可以在这里编写代码。有时,你也会看到一些函数块。而关于这些的说明都已经在iPython Notebook的文本中。在iPython Notebook中,在这些较长的灰色的区域就是代码块。
有时,你会看到代码块中有像这样的开始代码和结束代码。在进行编程练习时,请确保你的代码写在开始代码和结束代码之间。
比如,编写打印输出Hello World的代码,然后执行这一代码块(你可以按shift +enter来执行这一代码块)。最终,它就会输出我们想要的Hello World。
在运行一个单元格cell时,你也可以选择运行其中的一块代码区域。通过点击Cell菜单的Run Cells执行这部分代码。
也许,在你的计算机上,运行cell的键盘快捷方式可能并非是shift enter。但是,Mac 应该和我的个人电脑一样,可以使用shift + enter来运行cell。
当你正在阅读指南时,如果不小心双击了它,点中的区域就会变成markdown语言形式。如果你不小心使其变成了这样的文本框,只要运行下单元格cell,就可以回到原来的形式。所以,点击cell菜单的Run Cells或者使用shift + enter,就可以使得它变回原样。
这里还有一些其他的小技巧。比如当你执行上面所使用的代码时,它实际上会使用一个内核在服务器上运行这段代码。如果你正在运行超负荷的进程,或者电脑运行了很长一段时间,或者在运行中出了错,又或者网络连接失败,这里依然有机会让Kernel重新工作。你只要点击Kernel,选择Restart,它会重新运行Kernel使程序继续工作。
所以,如果你只是运行相对较小的工作并且才刚刚启动你的ipad或笔记本电脑,这种情况应该是不会发生的。但是,如果你看见错误信息,比如Kernel已经中断或者其他信息,你可以试着重启Kernel。
当我使用iPython Notebook时会有多个代码区域块。尽管我并没有在前面的代码块中添加自己的代码,但还是要确保先执行这块代码。因为在这个例子,它导入了numpy包并另命名为np等,并声明了一些你可能需要的变量。为了能顺利地执行下面的代码,就必须确保先执行上面的代码,即使不要求你去写其他的代码。
最后,当你完成作业后,可以通过点击右上方蓝色的Submit Assignment按钮提交你的作业。
我发现这种交互式的shell命令,在iPython Notebooks是非常有用的,能使你快速地实现代码并且查看输出结果,便于学习。所以我希望这些练习和Jupyter iPython Notebooks会帮助你更快地学习和实践,并且帮助你了解如何去实现这些学习算法。后面一个视频是一个选学视频,它主要是讲解逻辑回归中的代价函数。你可以选择是否观看。不管怎样,都祝愿你能通过这两次编程作业。我会在新一周的课程里等待着你。
2.18 (选修)logistic 损失函数的解释(Explanation of logistic regression cost function)
在前面的视频中,我们已经分析了逻辑回归的损失函数表达式,在这节选修视频中,我将给出一个简洁的证明来说明逻辑回归的损失函数为什么是这种形式。
回想一下,在逻辑回归中,需要预测的结果 ,可以表示为 , 是我们熟悉的 型函数 。我们约定 ,即算法的输出 是给定训练样本 条件下 等于 1 的概率。换句话说,如果 ,在给定训练样本 条件下 ;反过来说,如果 ,在给定训练样本 条件下 等于 1 减去 ,因此,如果 代表 的概率,那么 就是 的概率。接下来,我们就来分析这两个条件概率公式。
这两个条件概率公式定义形式为 并且代表了 或者 这两种情况,我们可以将这两个公式合并成一个公式。需要指出的是我们讨论的是二分类问题的损失函数,因此, 的取值只能是 0 或者 1。上述的两个条件概率公式可以合并成如下公式:
接下来我会解释为什么可以合并成这种形式的表达式: 的 次方这行表达式包含了上面的两个条件概率公式,我来解释一下为什么。
第一种情况,假设 ,由于 ,那么 ,因为 的 1 次方等于 , 的指数项 等于 0,由于任何数的 0 次方都是 1, 乘以 1 等于 。因此当 时 (图中绿色部分)。
第二种情况,当 时 等于多少呢? 假设 , 的 次方就是 的 0 次方,任何数的 0 次方都等于 1,因此 ,前面假设 因此 就等于 1,因此 。因此在这里当 时, 。这就是这个公式(第二个公式,图中紫色字体部分)的结果。
因此,刚才的推导表明 ,就是 的完整定义。由于 log 函数是严格单调递增的函数,最大化 等价于最大化 并且地计算 的 log 对数,就是计算 (其实就是将
而这就是我们前面提到的损失函数的负数 ,前面有一个负号的原因是当你训练学习算法时需要算法输出值的概率是最大的(以最大的概率预测这个值),然而在逻辑回归中我们需要最小化损失函数,因此最小化损失函数与最大化条件概率的对数
在 个训练样本的整个训练集中又该如何表示呢,让我们一起来探讨一下。
让我们一起来探讨一下,整个训练集中标签的概率,更正式地来写一下。假设所有的训练样本服从同一分布且相互独立,也即独立同分布的,所有这些样本的联合概率就是每个样本概率的乘积:
。
如果你想做最大似然估计,需要寻找一组参数,使得给定样本的观测值概率最大,但令这个概率最大化等价于令其对数最大化,在等式两边取对数:
在统计学里面,有一个方法叫做最大似然估计,即求出一组参数,使这个式子取最大值,也就是说,使得这个式子取最大值, ,可以将负号移到求和符号的外面, ,这样我们就推导出了前面给出的logistic回归的成本函数。
由于训练模型时,目标是让成本函数最小化,所以我们不是直接用最大似然概率,要去掉这里的负号,最后为了方便,可以对成本函数进行适当的缩放,我们就在前面加一个额外的常数因子 ,即:。
总结一下,为了最小化成本函数 ,我们从logistic回归模型的最大似然估计的角度出发,假设训练集中的样本都是独立同分布的条件下。尽管这节课是选修性质的,但还是感谢观看本节视频。我希望通过本节课您能更好地明白逻辑回归的损失函数,为什么是那种形式,明白了损失函数的原理,希望您能继续完成课后的练习,前面课程的练习以及本周的测验,在课后的小测验和编程练习中,祝您好运。
参考资料
[1]
深度学习课程: https://mooc.study.163.com/university/deeplearning_ai
[2]
黄海广: https://github.com/fengdu78
[3]
github: https://github.com/fengdu78/deeplearning_ai_books