第三周 超参数调试、Batch正则化和程序框架(Hyperparameter tuning)(下)
文章目录
3.6 Batch Norm 为什么奏效?(Why does Batch Norm work?)
为什么Batch归一化会起作用呢?
一个原因是,你已经看到如何归一化输入特征值 x x x,使其均值为0,方差1,它又是怎样加速学习的,有一些从0到1而不是从1到1000的特征值,通过归一化所有的输入特征值 x x x,以获得类似范围的值,可以加速学习。所以Batch归一化起的作用的原因,直观的一点就是,它在做类似的工作,但不仅仅对于这里的输入值,还有隐藏单元的值,这只是Batch归一化作用的冰山一角,还有些深层的原理,它会有助于你对Batch归一化的作用有更深的理解,让我们一起来看看吧。
Batch归一化有效的第二个原因是,它可以使权重比你的网络更滞后或更深层,比如,第10层的权重更能经受得住变化,相比于神经网络中前层的权重,比如第1层,为了解释我的意思,让我们来看看这个最生动形象的例子。
这是一个网络的训练,也许是个浅层网络,比如logistic回归或是一个神经网络,也许是个浅层网络,像这个回归函数。或一个深层网络,建立在我们著名的猫脸识别检测上,但假设你已经在所有黑猫的图像上训练了数据集,如果现在你要把此网络应用于有色猫,这种情况下,正面的例子不只是左边的黑猫,还有右边其它颜色的猫,那么你的cosfa可能适用的不会很好。
如果图像中,你的训练集是这个样子的,你的正面例子在这儿,反面例子在那儿(左图),但你试图把它们都统一于一个数据集,也许正面例子在这,反面例子在那儿(右图)。你也许无法期待,在左边训练得很好的模块,同样在右边也运行得很好,即使存在运行都很好的同一个函数,但你不会希望你的学习算法去发现绿色的决策边界,如果只看左边数据的话。
所以使你数据改变分布的这个想法,有个有点怪的名字“Covariate shift”,想法是这样的,如果你已经学习了 x x x到 y y y 的映射,如果 x x x 的分布改变了,那么你可能需要重新训练你的学习算法。这种做法同样适用于,如果真实函数由 x x x 到 y y y 映射保持不变,正如此例中,因为真实函数是此图片是否是一只猫,训练你的函数的需要变得更加迫切,如果真实函数也改变,情况就更糟了。
“Covariate shift”的问题怎么应用于神经网络呢?试想一个像这样的深度网络,让我们从这层(第三层)来看看学习过程。此网络已经学习了参数 w [ 3 ] w^{[3]} w[3]和 b [ 3 ] b^{[3]} b[3],从第三隐藏层的角度来看,它从前层中取得一些值,接着它需要做些什么,使希望输出值 y ^ \hat y y^接近真实值 y y y。
让我先遮住左边的部分,从第三隐藏层的角度来看,它得到一些值,称为 a 1 [ 2 ] a_{1}^{[2]} a1[2], a 2 [ 2 ] a_{2}^{[2]} a2[2], a 3 [ 2 ] a_{3}^{[2]} a3[2], a 4 [ 2 ] a_{4}^{[2]} a4[2],但这些值也可以是特征值 x 1 x_{1} x1, x 2 x_{2} x2, x 3 x_{3} x3, x 4 x_{4} x4,第三层隐藏层的工作是找到一种方式,使这些值映射到 y ^ \hat y y^,你可以想象做一些截断,所以这些参数 w [ 3 ] w^{[3]} w[3]和 b [ 3 ] b^{[3]} b[3]或 w [ 4 ] w^{[4]} w[4]和 b [ 4 ] b^{[4]} b[4]或 w [ 5 ] w^{[5]} w[5]和 b [ 5 ] b^{[5]} b[5],也许是学习这些参数,所以网络做的不错,从左边我用黑色笔写的映射到输出值 y ^ \hat y y^。
现在我们把网络的左边揭开,这个网络还有参数 w [ 2 ] w^{[2]} w[2], b [ 2 ] b^{[2]} b[2]和 w [ 1 ] w^{[1]} w[1], b [ 1 ] b^{[1]} b[1],如果这些参数改变,这些 a [ 2 ] a^{[2]} a[2]的值也会改变。所以从第三层隐藏层的角度来看,这些隐藏单元的值在不断地改变,所以它就有了“Covariate shift”的问题,上张幻灯片中我们讲过的。
Batch归一化做的,是它减少了这些隐藏值分布变化的数量。如果是绘制这些隐藏的单元值的分布,也许这是重整值 z z z,这其实是 z 1 [ 2 ] z_{1}^{[2]} z1[2], z 2 [ 2 ] z_{2}^{[2]} z2[2],我要绘制两个值而不是四个值,以便我们设想为2D,Batch归一化讲的是 z 1 [ 2 ] z_{1}^{[2]} z1[2], z 2 [ 2 ] z_{2}^{[2]} z2[2]的值可以改变,它们的确会改变,当神经网络在之前层中更新参数,Batch归一化可以确保无论其怎样变化 z 1 [ 2 ] z_{1}^{[2]} z1[2], z 2 [ 2 ] z_{2}^{[2]} z2[2]的均值和方差保持不变,所以即使 z 1 [ 2 ] z_{1}^{[2]} z1[2], z 2 [ 2 ] z_{2}^{[2]} z2[2]的值改变,至少他们的均值和方差也会是均值0,方差1,或不一定必须是均值0,方差1,而是由 β [ 2 ] {\beta}^{[2]} β[2]和 γ [ 2 ] \gamma^{[2]} γ[2]决定的值。如果神经网络选择的话,可强制其为均值0,方差1,或其他任何均值和方差。但它做的是,它限制了在前层的参数更新,会影响数值分布的程度,第三层看到的这种情况,因此得到学习。
Batch归一化减少了输入值改变的问题,它的确使这些值变得更稳定,神经网络的之后层就会有更坚实的基础。即使使输入分布改变了一些,它会改变得更少。它做的是当前层保持学习,当改变时,迫使后层适应的程度减小了,你可以这样想,它减弱了前层参数的作用与后层参数的作用之间的联系,它使得网络每层都可以自己学习,稍稍独立于其它层,这有助于加速整个网络的学习。
所以,希望这能带给你更好的直觉,重点是Batch归一化的意思是,尤其从神经网络后层之一的角度而言,前层不会左右移动的那么多,因为它们被同样的均值和方差所限制,所以,这会使得后层的学习工作变得更容易些。
Batch归一化还有一个作用,它有轻微的正则化效果,Batch归一化中非直观的一件事是,每个mini-batch,我会说mini-batch X { t } X^{\{ t \}} X{t}的值为 z [ t ] z^{\lbrack t\rbrack} z[t], z [ l ] z^{[l]} z[l],在mini-batch计算中,由均值和方差缩放的,因为在mini-batch上计算的均值和方差,而不是在整个数据集上,均值和方差有一些小的噪声,因为它只在你的mini-batch上计算,比如64或128或256或更大的训练例子。因为均值和方差有一点小噪音,因为它只是由一小部分数据估计得出的。缩放过程从 z [ l ] z^{[l]} z[l]到 z ~ [ l ] {\tilde{z}}^{[l]} z~[l],过程也有一些噪音,因为它是用有些噪音的均值和方差计算得出的。
所以和dropout相似,它往每个隐藏层的激活值上增加了噪音,dropout有增加噪音的方式,它使一个隐藏的单元,以一定的概率乘以0,以一定的概率乘以1,所以你的dropout含几重噪音,因为它乘以0或1。
对比而言,Batch归一化含几重噪音,因为标准偏差的缩放和减去均值带来的额外噪音。这里的均值和标准差的估计值也是有噪音的,所以类似于dropout,Batch归一化有轻微的正则化效果,因为给隐藏单元添加了噪音,这迫使后部单元不过分依赖任何一个隐藏单元,类似于dropout,它给隐藏层增加了噪音,因此有轻微的正则化效果。因为添加的噪音很微小,所以并不是巨大的正则化效果,你可以将Batch归一化和dropout一起使用,如果你想得到dropout更强大的正则化效果。
也许另一个轻微非直观的效果是,如果你应用了较大的mini-batch,对,比如说,你用了512而不是64,通过应用较大的min-batch,你减少了噪音,因此减少了正则化效果,这是dropout的一个奇怪的性质,就是应用较大的mini-batch可以减少正则化效果。
说到这儿,我会把Batch归一化当成一种正则化,这确实不是其目的,但有时它会对你的算法有额外的期望效应或非期望效应。但是不要把Batch归一化当作正则化,把它当作将你归一化隐藏单元激活值并加速学习的方式,我认为正则化几乎是一个意想不到的副作用。
所以希望这能让你更理解Batch归一化的工作,在我们结束Batch归一化的讨论之前,我想确保你还知道一个细节。Batch归一化一次只能处理一个mini-batch数据,它在mini-batch上计算均值和方差。所以测试时,你试图做出预测,试着评估神经网络,你也许没有mini-batch的例子,你也许一次只能进行一个简单的例子,所以测试时,你需要做一些不同的东西以确保你的预测有意义。
在下一个也就是最后一个Batch归一化视频中,让我们详细谈谈你需要注意的一些细节,来让你的神经网络应用Batch归一化来做出预测。
3.7 测试时的 Batch Norm(Batch Norm at test time)
Batch归一化将你的数据以mini-batch的形式逐一处理,但在测试时,你可能需要对每个样本逐一处理,我们来看一下怎样调整你的网络来做到这一点。
回想一下,在训练时,这些就是用来执行Batch归一化的等式。在一个mini-batch中,你将mini-batch的 z ( i ) z^{(i)} z(i)值求和,计算均值,所以这里你只把一个mini-batch中的样本都加起来,我用m来表示这个mini-batch中的样本数量,而不是整个训练集。然后计算方差,再算 z norm ( i ) z_{\text{norm}}^{(i)} znorm(i),即用均值和标准差来调整,加上 ε \varepsilon ε是为了数值稳定性。 z ~ \tilde{z} z~是用 γ \gamma γ和 β \beta β再次调整 z norm z_{\text{norm}} znorm得到的。
请注意用于调节计算的 μ \mu μ和 σ 2 \sigma^{2} σ2是在整个mini-batch上进行计算,但是在测试时,你可能不能将一个mini-batch中的6428或2056个样本同时处理,因此你需要用其它方式来得到 μ \mu μ和 σ 2 \sigma^{2} σ2,而且如果你只有一个样本,一个样本的均值和方差没有意义。那么实际上,为了将你的神经网络运用于测试,就需要单独估算 μ \mu μ和 σ 2 \sigma^{2} σ2,在典型的Batch归一化运用中,你需要用一个指数加权平均来估算,这个平均数涵盖了所有mini-batch,接下来我会具体解释。
我们选择 l l l层,假设我们有mini-batch, X [ 1 ] X^{[1]} X[1], X [ 2 ] X^{[2]} X[2], X [ 3 ] X^{[3]} X[3]……以及对应的 y y y值等等,那么在为 l l l层训练 X { 1 } X^{\{ 1\}} X{1}时,你就得到了 μ [ l ] \mu^{[l]} μ[l],我还是把它写做第一个mini-batch和这一层的 μ \mu μ吧,( μ [ l ] → μ { 1 } [ l ] \mu^{[l]} \rightarrow \mu^{\left\{1 \right\}[l]} μ[l]→μ{1}[l])。当你训练第二个mini-batch,在这一层和这个mini-batch中,你就会得到第二个 μ \mu μ( μ { 2 } [ l ] \mu^{\{2\}[l]} μ{2}[l])值。然后在这一隐藏层的第三个mini-batch,你得到了第三个 μ \mu μ( μ { 3 } [ l ] \mu^{\left\{3 \right\}[l]} μ{3}[l])值。正如我们之前用的指数加权平均来计算 θ 1 \theta_{1} θ1, θ 2 \theta_{2} θ2, θ 3 \theta_{3} θ3的均值,当时是试着计算当前气温的指数加权平均,你会这样来追踪你看到的这个均值向量的最新平均值,于是这个指数加权平均就成了你对这一隐藏层的 z z z均值的估值。同样的,你可以用指数加权平均来追踪你在这一层的第一个mini-batch中所见的 σ 2 \sigma^{2} σ2的值,以及第二个mini-batch中所见的 σ 2 \sigma^{2} σ2的值等等。因此在用不同的mini-batch训练神经网络的同时,能够得到你所查看的每一层的 μ \mu μ和 σ 2 \sigma^{2} σ2的平均数的实时数值。
最后在测试时,对应这个等式( z norm ( i ) = z ( i ) − μ σ 2 + ε z_{\text{norm}}^{(i)} = \frac{z^{(i)} -\mu}{\sqrt{\sigma^{2} +\varepsilon}} znorm(i)=σ2+εz(i)−μ),你只需要用你的 z z z值来计算 z norm ( i ) z_{\text{norm}}^{(i)} znorm(i),用 μ \mu μ和 σ 2 \sigma^{2} σ2的指数加权平均,用你手头的最新数值来做调整,然后你可以用左边我们刚算出来的 z norm z_{\text{norm}} znorm和你在神经网络训练过程中得到的 β \beta β和 γ \gamma γ参数来计算你那个测试样本的 z ~ \tilde{z} z~值。
总结一下就是,在训练时, μ \mu μ和 σ 2 \sigma^{2} σ2是在整个mini-batch上计算出来的包含了像是64或28或其它一定数量的样本,但在测试时,你可能需要逐一处理样本,方法是根据你的训练集估算 μ \mu μ和 σ 2 \sigma^{2} σ2,估算的方式有很多种,理论上你可以在最终的网络中运行整个训练集来得到 μ \mu μ和 σ 2 \sigma^{2} σ2,但在实际操作中,我们通常运用指数加权平均来追踪在训练过程中你看到的 μ \mu μ和 σ 2 \sigma^{2} σ2的值。还可以用指数加权平均,有时也叫做流动平均来粗略估算 μ \mu μ和 σ 2 \sigma^{2} σ2,然后在测试中使用 μ \mu μ和 σ 2 \sigma^{2} σ2的值来进行你所需要的隐藏单元 z z z值的调整。在实践中,不管你用什么方式估算 μ \mu μ和 σ 2 \sigma^{2} σ2,这套过程都是比较稳健的,因此我不太会担心你具体的操作方式,而且如果你使用的是某种深度学习框架,通常会有默认的估算 μ \mu μ和 σ 2 \sigma^{2} σ2的方式,应该一样会起到比较好的效果。但在实践中,任何合理的估算你的隐藏单元 z z z值的均值和方差的方式,在测试中应该都会有效。
Batch归一化就讲到这里,使用Batch归一化,你能够训练更深的网络,让你的学习算法运行速度更快,在结束这周的课程之前,我还想和你们分享一些关于深度学习框架的想法,让我们在下一段视频中一起讨论这个话题。
3.8 Softmax 回归(Softmax regression)
到目前为止,我们讲到过的分类的例子都使用了二分分类,这种分类只有两种可能的标记0或1,这是一只猫或者不是一只猫,如果我们有多种可能的类型的话呢?有一种logistic回归的一般形式,叫做Softmax回归,能让你在试图识别某一分类时做出预测,或者说是多种分类中的一个,不只是识别两个分类,我们来一起看一下。
假设你不单需要识别猫,而是想识别猫,狗和小鸡,我把猫加做类1,狗为类2,小鸡是类3,如果不属于以上任何一类,就分到“其它”或者说“以上均不符合”这一类,我把它叫做类0。这里显示的图片及其对应的分类就是一个例子,这幅图片上是一只小鸡,所以是类3,猫是类1,狗是类2,我猜这是一只考拉,所以以上均不符合,那就是类0,下一个类3,以此类推。我们将会用符号表示,我会用大写的 C C C来表示你的输入会被分入的类别总个数,在这个例子中,我们有4种可能的类别,包括“其它”或“以上均不符合”这一类。当有4个分类时,指示类别的数字,就是从0到 C − 1 C-1 C−1,换句话说就是0、1、2、3。
在这个例子中,我们将建立一个神经网络,其输出层有4个,或者说 C C C个输出单元,因此 n n n,即输出层也就是 L L L层的单元数量,等于4,或者一般而言等于 C C C。我们想要输出层单元的数字告诉我们这4种类型中每个的概率有多大,所以这里的第一个节点(最后输出的第1个方格+圆圈)输出的应该是或者说我们希望它输出“其它”类的概率。在输入 X X X的情况下,这个(最后输出的第2个方格+圆圈)会输出猫的概率。在输入 X X X的情况下,这个会输出狗的概率(最后输出的第3个方格+圆圈)。在输入 X X X的情况下,输出小鸡的概率(最后输出的第4个方格+圆圈),我把小鸡缩写为bc(baby chick)。因此这里的 y ^ \hat y y^将是一个 4 × 1 4×1 4×1维向量,因为它必须输出四个数字,给你这四种概率,因为它们加起来应该等于1,输出中的四个数字加起来应该等于1。
让你的网络做到这一点的标准模型要用到Softmax层,以及输出层来生成输出,让我把式子写下来,然后回过头来,就会对Softmax的作用有一点感觉了。
在神经网络的最后一层,你将会像往常一样计算各层的线性部分, z [ l ] z^{[l]} z[l]这是最后一层的 z z z变量,记住这是大写 L L L层,和往常一样,计算方法是 z [ l ] = W [ l ] a [ L − 1 ] + b [ l ] z^{[l]} = W^{[l]}a^{[L-1]} + b^{[l]} z[l]=W[l]a[L−1]+b[l],算出了 z z z之后,你需要应用Softmax激活函数,这个激活函数对于Softmax层而言有些不同,它的作用是这样的。首先,我们要计算一个临时变量,我们把它叫做t,它等于 e z [ l ] e^{z^{[l]}} ez[l],这适用于每个元素,而这里的 z [ l ] z^{[l]} z[l],在我们的例子中, z [ l ] z^{[l]} z[l]是4×1的,四维向量 t = e z [ l ] t=e^{z^{[l]}} t=ez[l],这是对所有元素求幂, t t t也是一个4×1维向量,然后输出的 a [ l ] a^{[l]} a[l],基本上就是向量 t t t,但是会归一化,使和为1。因此 a [ l ] = e z [ l ] ∑ j = 1 4 t i a^{[l]} = \frac{e^{z^{[l]}}}{\sum_{j =1}^{4}t_{i}} a[l]=∑j=14tiez[l],换句话说, a [ l ] a^{[l]} a[l]也是一个4×1维向量,而这个四维向量的第 i i i个元素,我把它写下来, a i [ l ] = t i ∑ j = 1 4 t i a_{i}^{[l]} = \frac{t_{i}}{\sum_{j =1}^{4}t_{i}} ai[l]=∑j=14titi,以防这里的计算不够清晰易懂,我们马上会举个例子来详细解释。
我们来看一个例子,详细解释,假设你算出了 z [ l ] z^{[l]} z[l], z [ l ] z^{[l]} z[l]是一个四维向量,假设为 z [ l ] = [ 5 2 − 1 3 ] z^{[l]} = \begin{bmatrix} 5 \\ 2 \\ - 1 \\ 3 \\ \end{bmatrix} z[l]= 52−13 ,我们要做的就是用这个元素取幂方法来计算 t t t,所以 t = [ e 5 e 2 e − 1 e 3 ] t =\begin{bmatrix} e^{5} \\ e^{2} \\ e^{- 1} \\ e^{3} \\ \end{bmatrix} t= e5e2e−1e3 ,如果你按一下计算器就会得到以下值 t = [ 148.4 7.4 0.4 20.1 ] t = \begin{bmatrix} 148.4 \\ 7.4 \\ 0.4 \\ 20.1 \\ \end{bmatrix} t= 148.47.40.420.1 ,我们从向量 t t t得到向量 a [ l ] a^{[l]} a[l]就只需要将这些项目归一化,使总和为1。如果你把 t t t的元素都加起来,把这四个数字加起来,得到176.3,最终 a [ l ] = t 176.3 a^{[l]} = \frac{t} {176.3} a[l]=176.3t。
例如这里的第一个节点,它会输出 e 5 176.3 = 0.842 \frac{e^{5}}{176.3} =0.842 176.3e5=0.842,这样说来,对于这张图片,如果这是你得到的 z z z值( [ 5 2 − 1 3 ] \begin{bmatrix} 5 \\ 2 \\ - 1 \\ 3 \\ \end{bmatrix} 52−13 ),它是类0的概率就是84.2%。下一个节点输出 e 2 176.3 = 0.042 \frac{e^{2}}{176.3} =0.042 176.3e2=0.042,也就是4.2%的几率。下一个是 e − 1 176.3 = 0.002 \frac{e^{- 1}}{176.3} =0.002 176.3e−1=0.002。最后一个是 e 3 176.3 = 0.114 \frac{e^{3}}{176.3} =0.114 176.3e3=0.114,也就是11.4%的概率属于类3,也就是小鸡组,对吧?这就是它属于类0,类1,类2,类3的可能性。
神经网络的输出 a [ l ] a^{[l]} a[l],也就是 y ^ \hat y y^,是一个4×1维向量,这个4×1向量的元素就是我们算出来的这四个数字( [ 0.842 0.042 0.002 0.114 ] \begin{bmatrix} 0.842 \\ 0.042 \\ 0.002 \\ 0.114 \\ \end{bmatrix} 0.8420.0420.0020.114 ),所以这种算法通过向量 z [ l ] z^{[l]} z[l]计算出总和为1的四个概率。
如果我们总结一下从 z [ l ] z^{[l]} z[l]到 a [ l ] a^{[l]} a[l]的计算步骤,整个计算过程,从计算幂到得出临时变量 t t t,再归一化,我们可以将此概括为一个Softmax激活函数。设 a [ l ] = g [ l ] ( z [ l ] ) a^{[l]} = g^{[l]}(z^{[l]}) a[l]=g[l](z[l]),这一激活函数的与众不同之处在于,这个激活函数 g g g 需要输入一个4×1维向量,然后输出一个4×1维向量。之前,我们的激活函数都是接受单行数值输入,例如Sigmoid和ReLu激活函数,输入一个实数,输出一个实数。Softmax激活函数的特殊之处在于,因为需要将所有可能的输出归一化,就需要输入一个向量,最后输出一个向量。
那么Softmax分类器还可以代表其它的什么东西么?我来举几个例子,你有两个输入 x 1 x_{1} x1, x 2 x_{2} x2,它们直接输入到Softmax层,它有三四个或者更多的输出节点,输出 y ^ \hat y y^,我将向你展示一个没有隐藏层的神经网络,它所做的就是计算 z [ 1 ] = W [ 1 ] x + b [ 1 ] z^{[1]} = W^{[1]}x + b^{[1]} z[1]=W[1]x+b[1],而输出的出 a [ l ] a^{[l]} a[l],或者说 y ^ \hat y y^, a [ l ] = y = g ( z [ 1 ] ) a^{[l]} = y = g(z^{[1]}) a[l]=y=g(z[1]),就是 z [ 1 ] z^{[1]} z[1]的Softmax激活函数,这个没有隐藏层的神经网络应该能让你对Softmax函数能够代表的东西有所了解。
这个例子中(左边图),原始输入只有 x 1 x_{1} x1和 x 2 x_{2} x2,一个 C = 3 C=3 C=3个输出分类的Softmax层能够代表这种类型的决策边界,请注意这是几条线性决策边界,但这使得它能够将数据分到3个类别中,在这张图表中,我们所做的是选择这张图中显示的训练集,用数据的3种输出标签来训练Softmax分类器,图中的颜色显示了Softmax分类器的输出的阈值,输入的着色是基于三种输出中概率最高的那种。因此我们可以看到这是logistic回归的一般形式,有类似线性的决策边界,但有超过两个分类,分类不只有0和1,而是可以是0,1或2。
这是(中间图)另一个Softmax分类器可以代表的决策边界的例子,用有三个分类的数据集来训练,这里(右边图)还有一个。对吧,但是直觉告诉我们,任何两个分类之间的决策边界都是线性的,这就是为什么你看到,比如这里黄色和红色分类之间的决策边界是线性边界,紫色和红色之间的也是线性边界,紫色和黄色之间的也是线性决策边界,但它能用这些不同的线性函数来把空间分成三类。
我们来看一下更多分类的例子,这个例子中(左边图) C = 4 C=4 C=4,因此这个绿色分类和Softmax仍旧可以代表多种分类之间的这些类型的线性决策边界。另一个例子(中间图)是 C = 5 C=5 C=5类,最后一个例子(右边图)是 C = 6 C=6 C=6,这显示了Softmax分类器在没有隐藏层的情况下能够做到的事情,当然更深的神经网络会有 x x x,然后是一些隐藏单元,以及更多隐藏单元等等,你就可以学习更复杂的非线性决策边界,来区分多种不同分类。
我希望你了解了神经网络中的Softmax层或者Softmax激活函数有什么作用,下一个视频中,我们来看一下你该怎样训练一个使用Softmax层的神经网络。
3.9 训练一个 Softmax 分类器(Training a Softmax classifier)
上一个视频中我们学习了Softmax层和Softmax激活函数,在这个视频中,你将更深入地了解Softmax分类,并学习如何训练一个使用了Softmax层的模型。
回忆一下我们之前举的的例子,输出层计算出的 z [ l ] z^{[l]} z[l]如下, z [ l ] = [ 5 2 − 1 3 ] z^{[l]} = \begin{bmatrix} 5 \\ 2 \\ - 1 \\ 3 \\ \end{bmatrix} z[l]= 52−13 我们有四个分类 C = 4 C=4 C=4, z [ l ] z^{[l]} z[l]可以是4×1维向量,我们计算了临时变量 t t t, t = [ e 5 e 2 e − 1 e 3 ] t = \begin{bmatrix} e^{5} \\ e^{2} \\ e^{- 1} \\ e^{3} \\ \end{bmatrix} t= e5e2e−1e3 ,对元素进行幂运算,最后,如果你的输出层的激活函数 g [ L ] ( ) g^{[L]}() g[L]()是Softmax激活函数,那么输出就会是这样的:
简单来说就是用临时变量 t t t将它归一化,使总和为1,于是这就变成了 a [ L ] a^{[L]} a[L],你注意到向量 z z z中,最大的元素是5,而最大的概率也就是第一种概率。
Softmax这个名称的来源是与所谓hardmax对比,hardmax会把向量 z z z变成这个向量 [ 1 0 0 0 ] \begin{bmatrix} 1 \\ 0 \\ 0 \\ 0 \\ \end{bmatrix} 1000 ,hardmax函数会观察 z z z的元素,然后在 z z z中最大元素的位置放上1,其它位置放上0,所这是一个hard max,也就是最大的元素的输出为1,其它的输出都为0。与之相反,Softmax所做的从 z z z到这些概率的映射更为温和,我不知道这是不是一个好名字,但至少这就是softmax这一名称背后所包含的想法,与hardmax正好相反。
有一点我没有细讲,但之前已经提到过的,就是Softmax回归或Softmax激活函数将logistic激活函数推广到 C C C类,而不仅仅是两类,结果就是如果 C = 2 C=2 C=2,那么 C = 2 C=2 C=2的Softmax实际上变回了logistic回归,我不会在这个视频中给出证明,但是大致的证明思路是这样的,如果 C = 2 C=2 C=2,并且你应用了Softmax,那么输出层 a [ L ] a^{[L]} a[L]将会输出两个数字,如果 C = 2 C=2 C=2的话,也许输出0.842和0.158,对吧?这两个数字加起来要等于1,因为它们的和必须为1,其实它们是冗余的,也许你不需要计算两个,而只需要计算其中一个,结果就是你最终计算那个数字的方式又回到了logistic回归计算单个输出的方式。这算不上是一个证明,但我们可以从中得出结论,Softmax回归将logistic回归推广到了两种分类以上。
接下来我们来看怎样训练带有Softmax输出层的神经网络,具体而言,我们先定义训练神经网络使会用到的损失函数。举个例子,我们来看看训练集中某个样本的目标输出,真实标签是 [ 0 1 0 0 ] \begin{bmatrix} 0 \\ 1 \\ 0 \\ 0 \\ \end{bmatrix} 0100 ,用上一个视频中讲到过的例子,这表示这是一张猫的图片,因为它属于类1,现在我们假设你的神经网络输出的是 y ^ \hat y y^, y ^ \hat y y^是一个包括总和为1的概率的向量, y = [ 0.3 0.2 0.1 0.4 ] y = \begin{bmatrix} 0.3 \\ 0.2 \\ 0.1 \\ 0.4 \\ \end{bmatrix} y= 0.30.20.10.4 ,你可以看到总和为1,这就是 a [ l ] a^{[l]} a[l], a [ l ] = y = [ 0.3 0.2 0.1 0.4 ] a^{[l]} = y = \begin{bmatrix} 0.3 \\ 0.2 \\ 0.1 \\ 0.4 \\ \end{bmatrix} a[l]=y= 0.30.20.10.4 。对于这个样本神经网络的表现不佳,这实际上是一只猫,但却只分配到20%是猫的概率,所以在本例中表现不佳。
那么你想用什么损失函数来训练这个神经网络?在Softmax分类中,我们一般用到的损失函数是
L
(
y
^
,
y
)
=
−
∑
j
=
1
4
y
j
l
o
g
y
^
j
L(\hat y,y ) = - \sum_{j = 1}^{4}{y_{j}log\hat y_{j}}
L(y^,y)=−∑j=14yjlogy^j,我们来看上面的单个样本来更好地理解整个过程。注意在这个样本中
y
1
=
y
3
=
y
4
=
0
y_{1} =y_{3} = y_{4} = 0
y1=y3=y4=0,因为这些都是0,只有
y
2
=
1
y_{2} =1
y2=1,如果你看这个求和,所有含有值为0的
y
j
y_{j}
yj的项都等于0,最后只剩下
−
y
2
t
l
o
g
y
^
2
-y_{2}t{log}\hat y_{2}
−y2tlogy^2,因为当你按照下标
j
j
j全部加起来,所有的项都为0,除了
j
=
2
j=2
j=2时,又因为
y
2
=
1
y_{2}=1
y2=1,所以它就等于
−
l
o
g
y
^
2
- \ log\hat y_{2}
− logy^2。
L
(
y
^
,
y
)
=
−
∑
j
=
1
4
y
j
log
y
^
j
=
−
y
2
l
o
g
y
^
2
=
−
l
o
g
y
^
2
L\left( \hat y,y \right) = - \sum_{j = 1}^{4}{y_{j}\log \hat y_{j}} = - y_{2}{\ log} \hat y_{2} = - {\ log} \hat y_{2}
L(y^,y)=−∑j=14yjlogy^j=−y2 logy^2=− logy^2
这就意味着,如果你的学习算法试图将它变小,因为梯度下降法是用来减少训练集的损失的,要使它变小的唯一方式就是使 − log y ^ 2 -{\log}\hat y_{2} −logy^2变小,要想做到这一点,就需要使 y ^ 2 \hat y_{2} y^2尽可能大,因为这些是概率,所以不可能比1大,但这的确也讲得通,因为在这个例子中 x x x是猫的图片,你就需要这项输出的概率尽可能地大( y = [ 0.3 0.2 0.1 0.4 ] y= \begin{bmatrix} 0.3 \\ 0.2 \\ 0.1 \\ 0.4 \\ \end{bmatrix} y= 0.30.20.10.4 中第二个元素)。
概括来讲,损失函数所做的就是它找到你的训练集中的真实类别,然后试图使该类别相应的概率尽可能地高,如果你熟悉统计学中最大似然估计,这其实就是最大似然估计的一种形式。但如果你不知道那是什么意思,也不用担心,用我们刚刚讲过的算法思维也足够了。
这是单个训练样本的损失,整个训练集的损失 J J J又如何呢?也就是设定参数的代价之类的,还有各种形式的偏差的代价,它的定义你大致也能猜到,就是整个训练集损失的总和,把你的训练算法对所有训练样本的预测都加起来,
J ( w [ 1 ] , b [ 1 ] , … … ) = 1 m ∑ i = 1 m L ( y ^ ( i ) , y ( i ) ) J( w^{[1]},b^{[1]},\ldots\ldots) = \frac{1}{m}\sum_{i = 1}^{m}{L( \hat y^{(i)},y^{(i)})} J(w[1],b[1],……)=m1∑i=1mL(y^(i),y(i))
因此你要做的就是用梯度下降法,使这里的损失最小化。
最后还有一个实现细节,注意因为 C = 4 C=4 C=4, y y y是一个4×1向量, y y y也是一个4×1向量,如果你实现向量化,矩阵大写 Y Y Y就是 [ y ( 1 ) y ( 2 ) … … y ( m ) ] \lbrack y^{(1)}\text{}y^{(2)}\ldots\ldots\ y^{\left( m \right)}\rbrack [y(1)y(2)…… y(m)],例如如果上面这个样本是你的第一个训练样本,那么矩阵 Y = [ 0 0 1 … 1 0 0 … 0 1 0 … 0 0 0 … ] Y =\begin{bmatrix} 0 & 0 & 1 & \ldots \\ 1 & 0 & 0 & \ldots \\ 0 & 1 & 0 & \ldots \\ 0 & 0 & 0 & \ldots \\ \end{bmatrix} Y= 010000101000………… ,那么这个矩阵 Y Y Y最终就是一个 4 × m 4×m 4×m维矩阵。类似的, Y ^ = [ y ^ ( 1 ) y ^ ( 2 ) … … y ^ ( m ) ] \hat{Y} = \lbrack{\hat{y}}^{(1)}{\hat{y}}^{(2)} \ldots \ldots\ {\hat{y}}^{(m)}\rbrack Y^=[y^(1)y^(2)…… y^(m)],这个其实就是 y ^ ( 1 ) {\hat{y}}^{(1)} y^(1)( a [ l ] ( 1 ) = y ( 1 ) = [ 0.3 0.2 0.1 0.4 ] a^{[l](1)} = y^{(1)} = \begin{bmatrix} 0.3 \\ 0.2 \\ 0.1 \\ 0.4 \\ \end{bmatrix} a[l](1)=y(1)= 0.30.20.10.4 ),或是第一个训练样本的输出,那么 Y ^ = [ 0.3 … 0.2 … 0.1 … 0.4 … ] \hat{Y} = \begin{bmatrix} 0.3 & \ldots \\ 0.2 & \ldots \\ 0.1 & \ldots \\ 0.4 & \ldots \\ \end{bmatrix} Y^= 0.30.20.10.4………… , Y ^ \hat{Y} Y^本身也是一个 4 × m 4×m 4×m维矩阵。
最后我们来看一下,在有Softmax输出层时如何实现梯度下降法,这个输出层会计算 z [ l ] z^{[l]} z[l],它是 C × 1 C×1 C×1维的,在这个例子中是4×1,然后你用Softmax激活函数来得到 a [ l ] a^{[l]} a[l]或者说 y y y,然后又能由此计算出损失。我们已经讲了如何实现神经网络前向传播的步骤,来得到这些输出,并计算损失,那么反向传播步骤或者梯度下降法又如何呢?其实初始化反向传播所需要的关键步骤或者说关键方程是这个表达式 d z [ l ] = y ^ − y dz^{[l]} = \hat{y} -y dz[l]=y^−y,你可以用 y ^ \hat{y} y^这个4×1向量减去 y y y这个4×1向量,你可以看到这些都会是4×1向量,当你有4个分类时,在一般情况下就是 C × 1 C×1 C×1,这符合我们对 d z dz dz的一般定义,这是对 z [ l ] z^{[l]} z[l]损失函数的偏导数( d z [ l ] = ∂ J ∂ z [ l ] dz^{[l]} = \frac{\partial J}{\partial z^{[l]}} dz[l]=∂z[l]∂J),如果你精通微积分就可以自己推导,或者说如果你精通微积分,可以试着自己推导,但如果你需要从零开始使用这个公式,它也一样有用。
有了这个,你就可以计算 d z [ l ] dz^{[l]} dz[l],然后开始反向传播的过程,计算整个神经网络中所需要的所有导数。
但在这周的初级练习中,我们将开始使用一种深度学习编程框架,对于这些编程框架,通常你只需要专注于把前向传播做对,只要你将它指明为编程框架,前向传播,它自己会弄明白怎样反向传播,会帮你实现反向传播,所以这个表达式值得牢记( d z [ l ] = y ^ − y dz^{[l]} = \hat{y} -y dz[l]=y^−y),如果你需要从头开始,实现Softmax回归或者Softmax分类,但其实在这周的初级练习中你不会用到它,因为编程框架会帮你搞定导数计算。
Softmax分类就讲到这里,有了它,你就可以运用学习算法将输入分成不止两类,而是 C C C个不同类别。接下来我想向你展示一些深度学习编程框架,可以让你在实现深度学习算法时更加高效,让我们在下一个视频中一起讨论。
3.10 深度学习框架(Deep Learning frameworks)
你已经差不多从零开始学习了使用Python和NumPy实现深度学习算法,很高兴你这样做了,因为我希望你理解这些深度学习算法实际上在做什么。但你会发现,除非应用更复杂的模型,例如卷积神经网络,或者循环神经网络,或者当你开始应用很大的模型,否则它就越来越不实用了,至少对大多数人而言,从零开始全部靠自己实现并不现实。
幸运的是,现在有很多好的深度学习软件框架,可以帮助你实现这些模型。类比一下,我猜你知道如何做矩阵乘法,你还应该知道如何编程实现两个矩阵相乘,但是当你在建很大的应用时,你很可能不想用自己的矩阵乘法函数,而是想要访问一个数值线性代数库,它会更高效,但如果你明白两个矩阵相乘是怎么回事还是挺有用的。我认为现在深度学习已经很成熟了,利用一些深度学习框架会更加实用,会使你的工作更加有效,那就让我们来看下有哪些框架。
现在有许多深度学习框架,能让实现神经网络变得更简单,我们来讲主要的几个。每个框架都针对某一用户或开发群体的,我觉得这里的每一个框架都是某类应用的可靠选择,有很多人写文章比较这些深度学习框架,以及这些深度学习框架发展得有多好,而且因为这些框架往往不断进化,每个月都在进步,如果你想看看关于这些框架的优劣之处的讨论,我留给你自己去网上搜索,但我认为很多框架都在很快进步,越来越好,因此我就不做强烈推荐了,而是与你分享推荐一下选择框架的标准。
一个重要的标准就是便于编程,这既包括神经网络的开发和迭代,还包括为产品进行配置,为了成千上百万,甚至上亿用户的实际使用,取决于你想要做什么。
第二个重要的标准是运行速度,特别是训练大数据集时,一些框架能让你更高效地运行和训练神经网络。
还有一个标准人们不常提到,但我觉得很重要,那就是这个框架是否真的开放,要是一个框架真的开放,它不仅需要开源,而且需要良好的管理。不幸的是,在软件行业中,一些公司有开源软件的历史,但是公司保持着对软件的全权控制,当几年时间过去,人们开始使用他们的软件时,一些公司开始逐渐关闭曾经开放的资源,或将功能转移到他们专营的云服务中。因此我会注意的一件事就是你能否相信这个框架能长时间保持开源,而不是在一家公司的控制之下,它未来有可能出于某种原因选择停止开源,即便现在这个软件是以开源的形式发布的。但至少在短期内,取决于你对语言的偏好,看你更喜欢Python,Java还是**C++**或者其它什么,也取决于你在开发的应用,是计算机视觉,还是自然语言处理或者线上广告,等等,我认为这里的多个框架都是很好的选择。
程序框架就讲到这里,通过提供比数值线性代数库更高程度的抽象化,这里的每一个程序框架都能让你在开发深度机器学习应用时更加高效。
3.11 TensorFlow
欢迎来到这周的最后一个视频,有很多很棒的深度学习编程框架,其中一个是TensorFlow,我很期待帮助你开始学习使用TensorFlow,我想在这个视频中向你展示TensorFlow程序的基本结构,然后让你自己练习,学习更多细节,并运用到本周的编程练习中,这周的编程练习需要花些时间来做,所以请务必留出一些空余时间。
先提一个启发性的问题,假设你有一个损失函数 J J J需要最小化,在本例中,我将使用这个高度简化的损失函数, J w = w 2 − 10 w + 25 Jw= w^{2}-10w+25 Jw=w2−10w+25,这就是损失函数,也许你已经注意到该函数其实就是 ( w − 5 ) 2 {(w -5)}^{2} (w−5)2,如果你把这个二次方式子展开就得到了上面的表达式,所以使它最小的 w w w值是5,但假设我们不知道这点,你只有这个函数,我们来看一下怎样用TensorFlow将其最小化,因为一个非常类似的程序结构可以用来训练神经网络。其中可以有一些复杂的损失函数 J ( w , b ) J(w,b) J(w,b)取决于你的神经网络的所有参数,然后类似的,你就能用TensorFlow自动找到使损失函数最小的 w w w和 b b b的值。但让我们先从左边这个更简单的例子入手。
我在我的Jupyter notebook中运行Python,
import numpy as np
import tensorflow as tf
#导入TensorFlow
w = tf.Variable(0,dtype = tf.float32)
#接下来,让我们定义参数w,在TensorFlow中,你要用tf.Variable()来定义参数
#然后我们定义损失函数:
cost = tf.add(tf.add(w**2,tf.multiply(- 10.,w)),25)
#然后我们定义损失函数J
然后我们再写:
train = tf.train.GradientDescentOptimizer(0.01).minimize(cost)
#(让我们用0.01的学习率,目标是最小化损失)。
#最后下面的几行是惯用表达式:
init = tf.global_variables_initializer()
session = tf.Session()#这样就开启了一个TensorFlow session。
session.run(init)#来初始化全局变量。
#然后让TensorFlow评估一个变量,我们要用到:
session.run(w)
#上面的这一行将w初始化为0,并定义损失函数,我们定义train为学习算法,它用梯度下降法优化器使损失函数最小化,但实际上我们还没有运行学习算法,所以#上面的这一行将w初始化为0,并定义损失函数,我们定义train为学习算法,它用梯度下降法优化器使损失函数最小化,但实际上我们还没有运行学习算法,所以session.run(w)评估了w,让我::
print(session.run(w))
所以如果我们运行这个,它评估 w w w等于0,因为我们什么都还没运行。
#现在让我们输入:
$session.run(train),它所做的就是运行一步梯度下降法。
#接下来在运行了一步梯度下降法后,让我们评估一下w的值,再print:
print(session.run(w))
#在一步梯度下降法之后,w现在是0.1。
现在我们运行梯度下降1000次迭代:
这是运行了梯度下降的1000次迭代,最后 w w w变成了4.99999,记不记得我们说 ( w − 5 ) 2 {(w -5)}^{2} (w−5)2最小化,因此 w w w的最优值是5,这个结果已经很接近了。
希望这个让你对TensorFlow程序的大致结构有了了解,当你做编程练习,使用更多TensorFlow代码时,我这里用到的一些函数你会熟悉起来,这里有个地方要注意,
w
w
w是我们想要优化的参数,因此将它称为变量,注意我们需要做的就是定义一个损失函数,使用这些add
和multiply
之类的函数。TensorFlow知道如何对add
和mutiply
,还有其它函数求导,这就是为什么你只需基本实现前向传播,它能弄明白如何做反向传播和梯度计算,因为它已经内置在add
,multiply
和平方函数中。
对了,要是觉得这种写法不好看的话,TensorFlow其实还重载了一般的加减运算等等,因此你也可以把
c
o
s
t
cost
cost写成更好看的形式,把之前的cost
标成注释,重新运行,得到了同样的结果。
一旦 w w w被称为TensorFlow变量,平方,乘法和加减运算都重载了,因此你不必使用上面这种不好看的句法。
TensorFlow还有一个特点,我想告诉你,那就是这个例子将 w w w的一个固定函数最小化了。如果你想要最小化的函数是训练集函数又如何呢?不管你有什么训练数据 x x x,当你训练神经网络时,训练数据 x x x会改变,那么如何把训练数据加入TensorFlow程序呢?
我会定义 x x x,把它想做扮演训练数据的角色,事实上训练数据有 x x x和 y y y,但这个例子中只有 x x x,把 x x x定义为:
x = tf.placeholder(tf.float32,[3,1])
,让它成为
[
3
,
1
]
[3,1]
[3,1]数组,我要做的就是,因为
c
o
s
t
cost
cost这个二次方程的三项前有固定的系数,它是
w
2
+
10
w
+
25
w^{2}+10w + 25
w2+10w+25,我们可以把这些数字1,-10和25变成数据,我要做的就是把
c
o
s
t
cost
cost替换成:
cost = x[0][0]*w**2 +x[1][0]*w + x[2][0]
,现在
x
x
x变成了控制这个二次函数系数的数据,这个placeholder函数告诉TensorFlow,你稍后会为
x
x
x提供数值。
让我们再定义一个数组,coefficient = np.array([[1.],[-10.],[25.]])
,这就是我们要接入
x
x
x的数据。最后我们需要用某种方式把这个系数数组接入变量
x
x
x,做到这一点的句法是,在训练这一步中,要提供给
x
x
x的数值,我在这里设置:
feed_dict = {x:coefficients}
好了,希望没有语法错误,我们重新运行它,希望得到和之前一样的结果。
现在如果你想改变这个二次函数的系数,假设你把:
coefficient = np.array([[1.],[-10.],[25.]])
改为:coefficient = np.array([[1.],[-20.],[100.]])
现在这个函数就变成了 ( w − 10 ) 2 {(w -10)}^{2} (w−10)2,如果我重新运行,希望我得到的使 ( w − 10 ) 2 {(w -10)}^{2} (w−10)2最小化的 w w w值为10,让我们看一下,很好,在梯度下降1000次迭代之后,我们得到接近10的 w w w。
在你做编程练习时,见到更多的是,TensorFlow中的placeholder是一个你之后会赋值的变量,这种方式便于把训练数据加入损失方程,把数据加入损失方程用的是这个句法,当你运行训练迭代,用feed_dict
来让x=coefficients
。如果你在做mini-batch梯度下降,在每次迭代时,你需要插入不同的mini-batch,那么每次迭代,你就用feed_dict
来喂入训练集的不同子集,把不同的mini-batch喂入损失函数需要数据的地方。
希望这让你了解了TensorFlow能做什么,让它如此强大的是,你只需说明如何计算损失函数,它就能求导,而且用一两行代码就能运用梯度优化器,Adam优化器或者其他优化器。
这还是刚才的代码,我稍微整理了一下,尽管这些函数或变量看上去有点神秘,但你在做编程练习时多练习几次就会熟悉起来了。
还有最后一点我想提一下,这三行(蓝色大括号部分)在TensorFlow里是符合表达习惯的,有些程序员会用这种形式来替代,作用基本上是一样的。
但这个with结构也会在很多TensorFlow程序中用到,它的意思基本上和左边的相同,但是Python中的with命令更方便清理,以防在执行这个内循环时出现错误或例外。所以你也会在编程练习中看到这种写法。那么这个代码到底做了什么呢?让我们看这个等式:
cost =x[0][0]*w**2 +x[1][0]*w + x[2][0]#(w-5)**2
TensorFlow程序的核心是计算损失函数,然后TensorFlow自动计算出导数,以及如何最小化损失,因此这个等式或者这行代码所做的就是让TensorFlow建立计算图,计算图所做的就是取 x [ 0 ] [ 0 ] x[0][0] x[0][0],取 w w w,然后将它平方,然后 x [ 0 ] [ 0 ] x[0][0] x[0][0]和 w 2 w^{2} w2相乘,你就得到了 x [ 0 ] [ 0 ] ∗ w 2 x[0][0]*w^{2} x[0][0]∗w2,以此类推,最终整个建立起来计算 c o s t = [ 0 ] [ 0 ] ∗ w ∗ ∗ 2 + x [ 1 ] [ 0 ] ∗ w + x [ 2 ] [ 0 ] cost = [0][0]*w**2 + x[1][0]*w + x[2][0] cost=[0][0]∗w∗∗2+x[1][0]∗w+x[2][0],最后你得到了损失函数。
TensorFlow的优点在于,通过用这个计算损失,计算图基本实现前向传播,TensorFlow已经内置了所有必要的反向函数,回忆一下训练深度神经网络时的一组前向函数和一组反向函数,而像TensorFlow之类的编程框架已经内置了必要的反向函数,这也是为什么通过内置函数来计算前向函数,它也能自动用反向函数来实现反向传播,即便函数非常复杂,再帮你计算导数,这就是为什么你不需要明确实现反向传播,这是编程框架能帮你变得高效的原因之一。
如果你看TensorFlow的使用说明,我只是指出TensorFlow的说明用了一套和我不太一样的符号来画计算图,它用了 x [ 0 ] [ 0 ] x[0][0] x[0][0], w w w,然后它不是写出值,想这里的 w 2 w^{2} w2,TensorFlow使用说明倾向于只写运算符,所以这里就是平方运算,而这两者一起指向乘法运算,以此类推,然后在最后的节点,我猜应该是一个将 x [ 2 ] [ 0 ] x[2][0] x[2][0]加上去得到最终值的加法运算。
为本课程起见,我认为计算图用第一种方式会更容易理解,但是如果你去看TensorFlow的使用说明,如果你看到说明里的计算图,你会看到另一种表示方式,节点都用运算来标记而不是值,但这两种呈现方式表达的是同样的计算图。
在编程框架中你可以用一行代码做很多事情,例如,你不想用梯度下降法,而是想用Adam优化器,你只要改变这行代码,就能很快换掉它,换成更好的优化算法。所有现代深度学习编程框架都支持这样的功能,让你很容易就能编写复杂的神经网络。
我希望我帮助你了解了TensorFlow程序典型的结构,概括一下这周的内容,你学习了如何系统化地组织超参数搜索过程,我们还讲了Batch归一化,以及如何用它来加速神经网络的训练,最后我们讲了深度学习的编程框架,有很多很棒的编程框架,这最后一个视频我们重点讲了TensorFlow。有了它,我希望你享受这周的编程练习,帮助你更熟悉这些概念。