反向传播算法的工作原理(1)

反向传播算法是神经网络中的重要算法,通过它能够快速计算梯度,进而通过梯度下降实现权重和偏置参数的更新

反向传播算法最初是在20世纪70年代被引入的,但直到1986年大卫·鲁梅尔哈特、杰弗里·辛顿和罗纳德·威廉姆斯合作的一篇著名论文问世后,人们才充分认识到它的重要性。这篇论文描述了几种神经网络,其中反向传播比以前的方法快得多,使人们有可能利用神经网络来解决以前无法解决的问题。如今,反向传播算法是神经网络中所要学习的主要内容。

本文的内容中涉及到更多的数学问题。如果你对数学不感兴趣,可以把反向传播当作一个黑匣子,忽略其中的细节。但是,如果想深入理解神经网络,还是有必要花时间研究这些细节的。

反向传播的核心思想是代价函数 C C C 相对于网络中任何权重 w w w(或偏置 b b b)的偏导数 ∂ C / ∂ w \partial C/\partial w C/w ,此式说明,更新权重和偏差时,代价函数的变化程度。虽然表达式有点复杂,但它的每一个元素都有一个自然的、直观的解释。所以反向传播不仅仅是一种可供学习的快速算法,它也为我们详细解释了权重和偏置的改变,从而提升网络的整体预测能力。这很值得详细研究。

基于矩阵的计算

在讨论反向传播之前,让我们先用一个基于矩阵的快速算法来计算神经网络的输出。

首先要明确一些符号的意义,文中会用 w j k l w^l_{jk} wjkl 表示从 ( l − 1 ) t h (l-1)^{th} (l1)th 层的第 k t h k^{th} kth 个神经元到 l t h l^{th} lth 层的第 j t h j^{th} jth 个神经元的连接权重参数。下图显示的是从网络第二层的第四个神经元到第三层的第二个神经元的连接的权重:

反向传播算法的工作原理(1)_第1张图片

这种记法一开始很麻烦,要掌握它确实需要费些功夫。但只要稍加努力,你就会发现这种记法变得简单而自然。它的一个奇怪之处是 j j j k k k 的顺序。你可能认为更合理的操作是:使用 j j j 表示输入神经元、 k k k 表示输出神经元,反之就不成立了,是这样。下面将解释这个奇怪现象的原因。

我们使用类似的符号来表示网络的偏置和激活结果。 b j l b^l_j bjl 表示 l t h l^{th} lth 层中的 j t h j^{th} jth 神经元的偏差;用 a j l a^l_j ajl 来表示激活 l t h l^{th} lth 层中的 j t h j^{th} jth 神经元。下面的图表展示了这些符号的应用:

反向传播算法的工作原理(1)_第2张图片

有了这些符号, l t h l^{th} lth 层中的 j t h j^{th} jth 神经元的激活 a j l a^l_j ajl ( l − 1 ) t h (l−1)^{th} (l1)th 层中的激活产生了关联,公式如下:

a j l = σ ( ∑ k w j k l a k l − 1 + b j l ) (23) a^l_j=\sigma(\sum_k w^l_{jk}a^{l-1}_k+b_j^l) \tag{23} ajl=σ(kwjklakl1+bjl)(23)

其中,求和表示的是 ( l − 1 ) t h (l−1)^{th} (l1)th 层中所有神经元共计 k k k 个。为了以矩阵形式重写这个表达式,我们为每个层 l l l 定义一个权重矩阵 w l w^l wl,权重矩阵 w l w^l wl 的各项是连接到神经元的 l t h l^{th} lth 层的权重,也就是说, j t h j^{th} jth 行和 k t h k^{th} kth 列中的项是 w j k l w^l_{jk} wjkl。类似地,对于每个层 l l l,我们定义一个偏差向量, b l b^l bl。你也许可以猜出这样操作的原理 —— 偏差向量的元素 b j l b^l_j bjl ,是 l t h l^{th} lth 层中每个神经元的一个分量。最后,我们定义了一个激活函数的输出向量 a l a^l al,它的分量是 a j l a^l_j ajl

将(23)式用矩阵形式重写,不过,这里还需要将激活函数( σ \sigma σ )向量化。基本想法是函数( σ \sigma σ)应用于向量 v \mathbf v v 中的每个元素,于是用符号 σ ( v ) \sigma (\mathbf v) σ(v) 来表示函数的这种应用,也就是说, σ ( v ) \sigma (\mathbf v) σ(v) 的分量只是 σ ( v ) j = σ ( v j ) \sigma (\mathbf v)_j = \sigma (\mathbf v_j) σ(v)j=σ(vj)。举个例子,对于函数 f ( x ) = x 2 f(x)=x^2 f(x)=x2 f f f 矢量化形式的效果如下:

f ( [ 2 3 ] ) = [ f ( 2 ) f ( 3 ) ] = [ 4 9 ] f(\begin{bmatrix}2\\3\end{bmatrix})=\begin{bmatrix}f(2)\\f(3)\end{bmatrix}=\begin{bmatrix}4\\9\end{bmatrix} f([23])=[f(2)f(3)]=[49]

也就是说,矢量化的 f f f 只是对矢量的每个元素求平方。

有了这些符号,我们就可以把式(23)改写成漂亮而紧凑的矢量化形式:

a l = σ ( w l a l − 1 + b l ) (25) a^l = \sigma (w^la^{l-1} + b^l) \tag{25} al=σ(wlal1+bl)(25)

这个表达式使我们可以从全局的角度来思考问题:一个层里的激活函数是如何与前一层里的激活输出相关联的。我们只需将权重矩阵应用于激活函数,然后添加偏置向量,最后应用 σ \sigma σ 函数(顺便说一下,正是这个表达式激活了前面提到的 w j k l w^l_{jk} wjkl 符号中的怪现象。如果我们用 j j j 来表示输入的神经元,用 k k k 来表示输出的神经元,那么,我们需要用权重矩阵的转置来代替方程(25)中的权重矩阵。这是一个很小的改变,但是很烦人,我们将失去简单易懂的说法(和想法):“将权重矩阵应用于激活函数”。与我们现在所采用的逐神经元观点相比,这种全局观点通常更简单、更简洁(涉及的指数更少!)。这种方法可以使我们逃离“角标地狱”,同时仍然精确地表述所发生的情况。该表达式在实践中也很有用,因为大多数矩阵库提供了实现矩阵乘法、矢量加法和矢量化的快速方法。

当使用方程(25)计算 a l a^l al 时,我们计算中间量 z l ≡ w l a l − 1 + b l z^l≡ w^l a^{l−1}+b^l zlwlal1+bl。把 z l z^l zl 称为层 l l l 中的神经元的加权输入。我们将在后半部分大量使用加权输入 z l z^l zl。方程(25) 有时用加权输入来表示,如: a l = σ ( z l ) a^l=\sigma (z^l) al=σ(zl)。同样值得注意的是, z l z^l zl 包含 z j l = ∑ k w j k l a k l − 1 + b j l z^l_j=\sum_k w^l_{jk}a^{l−1}_k+b^l_j zjl=kwjklakl1+bjl,也就是说, z j l z^l_j zjl 只是层 l l l 中神经元 j j j 的激活函数的加权输入。

代价函数的两个假设

反向传播的目标是计算代价函数 C C C 相对于网络中任何权重 w w w 或偏置 b b b 的偏导数 ∂ C / ∂ w \partial C/ \partial w C/w ∂ C / ∂ b \partial C/ \partial b C/b 。为了使反向传播有效,我们需要对代价函数的形式做两个主要的假设。不过,在陈述这些假设之前,先考虑以示例说明代价函数。

以下是二次代价函数的形式:

C = 1 2 n ∑ x ∥ y ( x ) − a L ( x ) ∥ 2 C=\frac{1}{2n}\sum_x \begin{Vmatrix}y(x)-a^L(x)\end{Vmatrix}^2 C=2n1xy(x)aL(x)2

其中, n n n 是训练示例的总数; ∑ x \sum_x x 是对所有单个训练示例求和; y = y ( x ) y=y(x) y=y(x) 是相应的真实输出; L L L 表示网络中的层数; a L = a L ( x ) a^L=a^L(x) aL=aL(x) 是输入 x x x 时从网络输出的激活向量(即网络的预测值)。

那么,为了应用反向传播,我们需要对代价函数 C C C 做什么样的假设呢?

第一个假设是,对于单个训练示例 x x x,可以把代价函数写成一个平均值 C = 1 n ∑ x C x C=\frac{1}{n}\sum_xC_x C=n1xCx,而不是 C x C_x Cx 。对于二次代价函数来说,情况就是如此。其中单个训练示例的代价为 C x = 1 2 ∥ y − a L ∥ 2 C_x=\frac{1}{2}\begin{Vmatrix}y−a^L\end{Vmatrix}2 Cx=21yaL2。这个假设也适用于所有其他的代价函数。

之所以需要这个假设,是因为反向传播实际上允许我们计算一个训练集的偏导数 ∂ C x / ∂ w \partial C_x/ \partial w Cx/w ∂ C x / ∂ b \partial C_x/ \partial b Cx/b。然后,我们通过对训练模型求平均值来恢复 ∂ C / ∂ w \partial C/ \partial w C/w ∂ C / ∂ b \partial C/ \partial b C/b。事实上,基于这个假设,我们认为训练示例 x x x 已修复,并删除 x x x 下标,将代价 C x C_x Cx 写成 C C C。我们最终会让 x x x 回到原处,但目前它是一个令人讨厌的符号,最好还是隐式的。

我们对代价的第二个假设是,它可以写成神经网络输出的函数:

反向传播算法的工作原理(1)_第3张图片

例如,二次代价函数满足这一要求,因为单个训练示例 x x x 的二次代价可以写成:

C = 1 2 ∥ y − a L ∥ 2 = 1 2 ∑ j ( y j − a j L ) 2 (27) C=\frac{1}{2}\begin{Vmatrix}y-a^L\end{Vmatrix}^2=\frac{1}{2}\sum_j(y_j-a_j^L)^2 \tag{27} C=21yaL2=21j(yjajL)2(27)

代价函数也取决于真实值 y y y。你可能感到疑惑:为什么我们不把代价也看作是 y y y 的函数?不过,请记住,输入训练示例 x x x 是固定的,因此输出 y y y 也是一个固定参数。特别是, y y y 不是我们可以通过改变权重和偏差来修改的。也就是说,它不是神经网络学习的东西。因此,将 C C C 看作是 a L a^L aL 的函数,而 y y y 只是一个帮助定义该函数的参数。

矩阵的Hadamard积 s ⨀ t s \bigodot t st

反向传播算法基于常见的线性代数运算,如矢量加法、矢量乘矩阵等。但其中一种运算不太常用。

假设 s s s t t t 是同一维的两个矢量。然后我们使用 s ⨀ t s\bigodot t st 来表示这两个矢量的对应元素的积。因此, s ⨀ t s\bigodot t st 的分量仅为 ( s ⨀ t ) j = s j t j (s \bigodot t)_j=s_jt_j (st)j=sjtj。例如,

[ 1 2 ] ⨀ [ 3 4 ] = [ 1 × 3 2 × 4 ] = [ 3 8 ] \begin{bmatrix}1\\2\end{bmatrix} \bigodot \begin{bmatrix}3\\4\end{bmatrix} = \begin{bmatrix}1\times 3\\2\times 4\end{bmatrix}=\begin{bmatrix}3\\8\end{bmatrix} [12][34]=[1×32×4]=[38]

这种矩阵的对应元素相乘被称为矩阵的Hadamard积。很多支持矩阵计算的库通常提供了Hadamard积的函数或算式,这在实现反向传播时非常有用。

反向传播幕后的四个基本方程

反向传播能够让网络中的权重和偏置参数更新,从而最小化代价函数,这意味着计算偏导数 ∂ C / ∂ w j k l \partial C/ \partial w^l_{jk} C/wjkl ∂ C / ∂ b j l \partial C/ \partial b^l_j C/bjl。但是为了便于计算,我们首先引入一个中间量 δ j l \delta^l_j δjl,它表示 l t h l^{th} lth 层的 j t h j^{th} jth 神经元误差。反向传播将为我们提供一个计算误差 δ j l \delta^l_j δjl 的过程,然后将 δ j l \delta^l_j δjl ∂ C / ∂ w j k l \partial C/ \partial w^l_{jk} C/wjkl ∂ C / ∂ b j l \partial C/ \partial b^l_j C/bjl关联起来。

为了理解这个误差是如何定义的,我们想象在神经网络中有一个精灵:

反向传播算法的工作原理(1)_第4张图片

精灵坐在 l l l 层的 j t h j^{th} jth 神经元上。当输入结果传给神经元时,精灵就会扰乱神经元的运作。它在神经元的加权输入中增加了一点变化 Δ z j l \Delta z^l_j Δzjl,因此神经元输出的不是 δ ( z j l ) \delta (z^l_j) δ(zjl),而是输出 δ ( z j l + Δ z j l ) \delta (z^l_j+\Delta z^l_j) δ(zjl+Δzjl)。这种变化会在网络中的后续层传播,最终导致总代价发生变化,变化的幅度为: ∂ C ∂ z j l Δ z j l \frac{\partial C}{\partial z^l_j}\Delta z^l_j zjlCΔzjl

现在,这个精灵表现很好,它试图帮助你减少代价。也就是说,他试图找到一个使代价更小的 Δ z j l \Delta z^l_j Δzjl。假设 ∂ C ∂ z j l \frac{\partial C}{\partial z^l_j} zjlC 有一个很大的值(正或负),精灵可以选择 Δ z j l \Delta z^l_j Δzjl,获取与 ∂ C ∂ z j l \frac{\partial C}{\partial z^l_j} zjlC 相反的符号,从而实现梯度下降。相比之下,如果 ∂ C ∂ z j l \frac{\partial C}{\partial z^l_j} zjlC 接近于零,那么精灵无法通过扰动加权输入 z j l z^l_j zjl 来减少代价。这是精灵会认为,神经元已经接近最佳状态(当然,这种情况只适合于小的改变 Δ z j l \Delta z^l_j Δzjl。我们假设精灵会被迫做出这么小的改变)。所以, ∂ C ∂ z j l \frac{\partial C}{\partial z^l_j} zjlC 是对神经元误差的度量。

受此启发,我们在 l l l 层中定义神经元 j j j 的误差 δ j l \delta^l_j δjl,其表达式为:

δ j l = ∂ C ∂ z j l (29) \delta _j^l = \frac{\partial C}{\partial z_j^l} \tag{29} δjl=zjlC(29)

按照通常的约定,使用 δ l \delta^l δl 来表示与层 l l l 相关的误差向量。反向传播将为我们提供一种计算每层的 δ l \delta^l δl 的方法,然后将这些误差与实际感兴趣的量 ∂ C / ∂ w j k l \partial C/ \partial w^l_{jk} C/wjkl ∂ C / ∂ b j l \partial C/ \partial b^l_j C/bjl相关联。

你可能感到疑惑:为什么精灵要更改加权输入 z j l z^l_j zjl?或许,想象它更改输出激活 a j l a^l_j ajl 会更自然,因为这样的更改使我们能够利用 ∂ C ∂ a j l \frac{\partial C}{\partial a^l_j} ajlC 作为误差的度量标准。事实上,如果你这样做,结果会和下面的讨论非常相似。但这种做法使反向传播在代数表达上更为复杂。因此,我们将坚持使用 δ j l = ∂ C ∂ z j l \delta^l_j=\frac{\partial C}{\partial z^l_j} δjl=zjlC 作为误差度量标准。

制胜计划:反向传播基于四个基本方程。这些方程为我们提供了一种计算误差 δ l \delta^l δl 和代价函数梯度的方法。我讲到的是以下四个方程式。不过,需要注意的是:你不应该期望在瞬间就掌握这些方程式,期望越高失望越大。事实上,要理解反向传播,需要相当多的时间和耐心,需要逐渐深入研究这些方程。

输出层 δ L \delta^L δL 中的误差方程式:
δ j L = ∂ C ∂ a j L σ ′ ( z j L ) (BP1) \delta_j^L = \frac{\partial C}{\partial a_j^L}\sigma'(z_j^L) \tag{BP1} δjL=ajLCσ(zjL)(BP1)
(BP1)给出了 δ L \delta^L δL 的分量。

这是一个非常自然的表达。右边的第一项 ∂ C / ∂ a j L \partial C/ \partial a^L_j C/ajL 是对 j t h j^{th} jth 输出激活的函数,代价的变化。例如,如果 C C C 不太依赖于某个特定的输出神经元 j j j,那么 δ j L \delta^L_j δjL 的值将很小,这是我们所期望的。右边的第二项 σ ′ ( z j L ) \sigma'(z^L_j) σ(zjL) 是激活函数 σ \sigma σ z j L z^L_j zjL 的导数。

注意,(BP1)中的所有内容都很容易计算。特别是,我们计算 z j L z^L_j zjL,计算 σ ′ ( z j L ) \sigma'(z^L_j) σ(zjL) 只是简单的求导。当然, ∂ C / ∂ a j L \partial C/ \partial a^L_j C/ajL 的确切形式取决于代价函数的形式。但是,如果代价函数已知,则计算 ∂ C / ∂ a j L \partial C/ \partial a^L_j C/ajL 应该不会有什么问题。例如,如果我们使用二次代价函数 C = 1 2 ∑ j ( y j − a j L ) 2 C=\frac{1}{2}\sum_j(y_j−a^L_j)^2 C=21j(yjajL)2,容易得出 ∂ C / ∂ a L j = ( a L j − y j ) \partial C/ \partial aL_j=(aL_j−y_j) C/aLj=(aLjyj),这显然是很容易计算的。

式(BP1)是 δ L \delta^L δL 的分量式表达式。这是一个非常好的表达式,但不是反向传播所需要的基于矩阵的形式。然而,我们很容易将这个方程改写为基于矩阵的形式,如:

δ L = ∇ a C ⨀ σ ′ ( z L ) (BP1a) \delta^L=\nabla_a C \bigodot \sigma'(z^L) \tag{BP1a} δL=aCσ(zL)(BP1a)

在这里, ∇ a C \nabla_a C aC 是一个矢量,其分量是偏导数 ∂ C / ∂ a j L \partial C/ \partial a^L_j C/ajL。你可以将 ∇ a C \nabla_a C aC 看作是 C C C 相对于输出激活的变化率。显然(BP1a)和(BP1)是等价的,因此从现在起,我们将交替使用(BP1)。例如,在二次代价的情况下,得到 ∇ a C = ( a L − y ) \nabla_a C=(a^L−y) aC=(aLy)。因此,完全基于矩阵的(BP1)就变成这种形式:

δ L = ( a L − y ) ⨀ σ ′ ( z L ) (30) \delta^L = (a^L-y)\bigodot \sigma'(z^L) \tag{30} δL=(aLy)σ(zL)(30)

如你所见,此表达式中的所有内容都有一个很好的矢量形式,并且可以使用诸如Numpy之类的库轻松地进行计算。

关于下一层 δ l + 1 \delta^{l+1} δl+1 的误差 δ l \delta^l δl 的方程是

δ l = ( ( w l + 1 ) T δ l + 1 ) ⨀ σ ′ ( z l ) (BP2) \delta^l = ((w^{l+1})^T \delta^{l+1})\bigodot \sigma'(z^l) \tag{BP2} δl=((wl+1)Tδl+1)σ(zl)(BP2)

其中, ( w l + 1 ) T (w^{l+1})T (wl+1)T ( l + 1 ) t h (l+1)^{th} (l+1)th 层的权重矩阵 w l + 1 w^{l+1} wl+1 的转置。这个方程看起来很复杂,但每个元素都很好解释。假设我们知道 ( l + 1 ) t h (l+1)^{th} (l+1)th 层的误差 δ l + 1 \delta^{l+1} δl+1 。当我们应用转置权重矩阵 ( w l + 1 ) T (w^{l+1})T (wl+1)T 时,可以直观地认为:这是在网络中反向移动误差,使我们可以对第 l l l 层输出处的误差进行某种衡量。 然后,我们取Hadamard积 ⨀ σ ′ ( z l ) \bigodot \sigma'(z^l) σ(zl)。这就通过层 l l l 中的激活函数反向移动误差,从而使我们得出层 l l l 的加权输入中的误差 δ l \delta^l δl

将(BP2)与(BP1)相结合,我们可以计算网络中任何层的误差 δ l \delta^l δl。首先使用(BP1)来计算 δ L \delta^L δL,再应用方程(BP2)来计算 δ L − 1 \delta^{L−1} δL1,然后再次使用方程(BP2)来计算 δ L − 2 \delta^{L−2} δL2,在网络中依此类推。

与网络中任何偏置相关的、代价变化率的方程是:

∂ C ∂ b j l = δ j l (BP3) \frac{\partial C}{\partial b_j^l}=\delta_j^l \tag{BP3} bjlC=δjl(BP3)

也就是说,误差 δ j l \delta^l_j δjl 正好等于变化率 ∂ C / ∂ b j l \partial C/ \partial b^l_j C/bjl。这是一个好消息,因为(BP1)和(BP2)已经告诉我们如何计算 δ j l \delta^l_j δjl。我们可以将(BP3)简写为:

∂ C ∂ b = δ (31) \frac{\partial C}{\partial b} = \delta \tag{31} bC=δ(31)

我们知道 δ \delta δ 和偏差 b b b 是在同一个神经元上评估的。

与网络中任何权重相关的、代价变化率的方程是:

∂ C ∂ w j k l = a k l − 1 δ j l (BP4) \frac{\partial C}{\partial w^l_{jk}}=a^{l−1}_k \delta^l_j \tag{BP4} wjklC=akl1δjl(BP4)

这告诉我们如何计算与 δ l \delta^l δl a l − 1 a^{l−1} al1 相关的偏导数 ∂ C / ∂ w j k l \partial C/ \partial w^l_{jk} C/wjkl,而我们已经知道如何计算 δ l \delta^l δl a l − 1 a^{l−1} al1了。可以用角标较少的符号改写方程,如下所示:

∂ C ∂ w = a i n δ o u t (32) \frac{\partial C}{\partial w}=a_{in}\delta_{out} \tag{32} wC=ainδout(32)

其中, a i n a_{in} ain 是对权重 w w w 的神经元输入的激活, δ o u t \delta_{out} δout 是权重 w w w 的神经元输出的误差。放大查看权重 w w w,以及由该权重连接的两个神经元,我们可以将其描述为:

方程(32)的一个很好的结果是:当激活 a i n a_{in} ain 很小的时候,也就是 a i n ≈ 0 a_{in} \approx 0 ain0,梯度项 ∂ C / ∂ w \partial C/ \partial w C/w 也将趋于很小。在这种情况下,权重学习缓慢,这意味着它在梯度下降过程中变化不大。换句话说,(BP4)的一个结果是低激活神经元输出的权重学习缓慢。

从(BP1)到(BP4)中可以得到其他关于这些方面的理解。我们首先着眼于输出层。考虑一下(BP1)中的 σ ′ ( z j L ) \sigma'(z^L_j) σ(zjL),可以使用S型函数。当 σ ( z j L ) \sigma(z^L_j) σ(zjL) 大约为0或1时, σ \sigma σ 函数变得非常平坦。当这种情况发生时,我们就得到 σ ′ ( z j L ) ≈ 0 \sigma'(z^L_j) \approx 0 σ(zjL)0。因此,要吸取的教训是:如果输出神经元是低激活度( ≈ 0 \approx 0 0)或高激活度( ≈ 1 \approx 1 1),最后一层中的权重将缓慢学习。在这种情况下,通常会说:输出神经元已经饱和,因此权重停止学习(或学习缓慢)。类似的情况也适用于输出神经元的偏置。

我们可以对早期的层获得类似的结果。特别要注意(BP2)中的 σ ′ ( z l ) \sigma'(z^l) σ(zl) 项。这意味着:如果神经元接近饱和, δ j l \delta^l_j δjl 可能会变小。这也意味着:输入到饱和神经元的任何权重都将学习缓慢(如果 ( w l + 1 ) T δ l + 1 (w^{l+1})^T \delta^{l+1} (wl+1)Tδl+1 有足够大的项来补偿 δ ′ ( z j l ) \delta'(z^l_j) δ(zjl) 的小值,这个推理就不成立了。但我说的是总体趋势)。

综上所述,我们已经了解到:如果输入神经元的激活度很低,或者输出神经元已经饱和,也就是说,无论是高激活还是低激活,权重学习都会很慢。

这些观察结果都不太令人惊讶。尽管如此,它们仍有助于改进我们的心理模型。这些模型反应了神经网络学习过程中所发生的情况。此外,我们可以优化这种推理方式。这四个基本方程对任何激活函数都适用,而不仅仅是标准的S型函数。记住这四个方程(BP1)-(BP4) 可以帮助解释:为什么要尝试这样的修改?以及,它们会产生什么样的影响?

反向传播算法的工作原理(1)_第5张图片

参考链接:http://www.math.hkbu.edu.hk/~mhyipa/nndl/chap2.pdf

你可能感兴趣的:(机器学习,神经网络,python,机器学习,人工智能)