2-浅层神经网络

浅层神经网络

神经网络概述

之前讨论了逻辑回归,模型如下图。

image-20220907151411464

公式为:

xwb}z=wTx+b\left.\begin{array}{l} x \\ w \\ b \end{array} \right \} \Longrightarrow z=w^T x+b

如图,首先输入特征x,参数wbw,b,然后计算出zz,公式为:

xwb}z=wTx+ba=σ(z)L(a,y)\begin{aligned} \left.\begin{array}{l} x \\ w \\ b \end{array}\right\} \Longrightarrow z = & w^T x+b \Longrightarrow a = \sigma(z) \\ & \Longrightarrow L(a, y) \end{aligned}

神经网络看起来是下面这个样子:

image-20220907152343000

注:字母的上标[i]代表神经网络中的第i层(layer)

使用括号(i)作为上标,代表第i个训练样本,不要弄混。

可以把许多sigmoid单元堆叠起来,形成一个神经网路。

在这个神经网络对应的三个节点中,首先计算第一层网络中各个节点的z[1]z^{[1]},接着计算a[1]a^{[1]},计算下一层网络同理。

在第一层中,计算公式为:

xW[1]b[1]}z[1]=W[1]x+b[1]a[1]=σ(z[1])\left.\begin{array}{l} x \\ W^{[1]} \\ b^{[1]} \\ \end{array}\right\} \Longrightarrow z^{[1]} = W^{[1]}x + b^{[1]} \Longrightarrow a^{[1]} = \sigma(z^{[1]})

在第二层中,计算公式为:

使用第一层中的输出a[1]a^{[1]}作为第二层的输入。

a[1]W[2]b[2]}z[2]=W[2]a[1]+b[2]a[2]=σ(z[2])L(a[2],y)\left.\begin{array}{l} a^{[1]} \\ W^{[2]} \\ b^{[2]} \\ \end{array}\right\} \Longrightarrow z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} \Longrightarrow a^{[2]} = \sigma(z^{[2]}) \\ \Longrightarrow L(a^{[2]}, y)

此时a[2]a^{[2]}就是整个神经网络最终的输出。

神经网络的表示

image-20220907154158240

上图是一个神经网络的例子。

有输入特征x1,x2,x3x_1, x_2, x_3,竖直地堆叠起来,叫做神经网络的输入层(input layer)。输入层右边有另外一层,称为隐藏层(hidden layer),最后只有一个结点的一层称为输出层(output layer)。

其中,隐藏层的含义为:在一个神经网络中,当使用监督学习训练它的时候,训练集包含了输入x和目标输出y,所以隐藏层表示在训练集中,这些中间节点的准确值是不知道的。可以看到输入的值,也能看到输出的值,但是隐藏层中的数据,在训练集中是无法看到的。


再引入几个符号。

就像之前使用向量x表示输入特征,这里有个可代替的记号a[0]a^{[0]}可表示输入特征,a表示激活(activate)的意思。它意味着网络中不同层的值会传递到它们后面的层中,下一层隐藏同样会产生一些激活值,记为a[1]a^{[1]},以此类推。

所以隐藏层的激活值可以写为一个四维向量或维度为4x1的矩阵。

a[1]=[a1[1]a2[1]a3[1]a4[1]]a^{[1]} = \begin{bmatrix} a^{[1]}_1 \\ a^{[1]}_2 \\ a^{[1]}_3 \\ a^{[1]}_4 \end{bmatrix}

最后输出层产生某个数值a[2]a^{[2]},是一个单独的实数。


上图的是一个二层的神经网络,原因是当计算网络层数时,**输入层不算入总层数。**所以隐藏层是第一次层,输出层是第二层。

(无论在阅读研究论文还是在这门课中,都遵循此惯例。)


最后,隐藏层和最后的输出层是带有参数的,这里的隐藏层拥有两个参数W[1]W^{[1]}b[1]b^{[1]},表示这些参数和第一层输入层有关系。

这里的W[1]W^{[1]}是一个4x3的矩阵(隐藏层有四个神经元,每一个神经元都对应了输入x1,x2,x3x_1,x_2,x_3,因此每个神经元有三个参数),而b[1]b^{[1]}是一个4x1的向量。

类似的,输入层也有相关联的参数W[2],b[2]W^{[2]}, b^{[2]}。它们的维数分别为:1x41x1

计算一个神经网络的输出

关于神经网络是如何计算的,首先从之前学到的逻辑回归开始。

image-20220907160515918

如图。

逻辑回归计算有两个步骤,首先根据特征输入向量和参数计算zz,然后以sigmoid函数为激活函数计算aa

一个神经网络只是做了很多次这种重复计算。


回到刚才提到的两层神经网络。

image-20220907160659657

从隐藏层的一个神经元开始计算。

和逻辑回归运算一样分为两步。

  1. 计算z1[1]z^{[1]}_1z1[1]=w1[1]Tx+b1[1]z^{[1]}_1 = w^{[1]T}_1x + b^{[1]}_1
  2. 通过激活函数sigmoid,计算a1[1]=σ(z1[1])a^{[1]}_1 = \sigma(z^{[1]}_1)

隐藏层的其他几个神经元也是这样计算,只是表示符号不同。

详细结果为:

z1[1]=w1[1]Tx+b1[1]a1[1]=σ(z1[1])z2[1]=w2[1]Tx+b2[1]a2[1]=σ(z2[1])z3[1]=w3[1]Tx+b3[1]a3[1]=σ(z3[1])z4[1]=w4[1]Tx+b4[1]a4[1]=σ(z4[1])z^{[1]}_1 = w^{[1]T}_1x + b^{[1]}_1,a^{[1]}_1 = \sigma(z^{[1]}_1) \\ \\ z^{[1]}_2 = w^{[1]T}_2x + b^{[1]}_2,a^{[1]}_2 = \sigma(z^{[1]}_2) \\ \\ z^{[1]}_3 = w^{[1]T}_3x + b^{[1]}_3,a^{[1]}_3 = \sigma(z^{[1]}_3) \\ \\ z^{[1]}_4 = w^{[1]T}_4x + b^{[1]}_4,a^{[1]}_4 = \sigma(z^{[1]}_4) \\ \\

向量化计算

将上述公式向量化。

说先将隐藏层中的参数ww堆积起来,变成一个(4,3)的矩阵,使用符号W[1]W^{[1]}表示,偏置bb变成一个(4,1)的矩阵,用符号b[1]b^{[1]}表示。

那么公式为:z[n]=w[n]x+b[n]z^{[n]} = w^{[n]}x + b^{[n]}a[n]=σ(z[n])a^{[n]} = \sigma(z^{[n]})

详细公式为:

z[1]=[z1[1]z2[1]z3[1]z4[1]]=[W1[1]TW2[1]TW3[1]TW4[1]T]W[1]4×3的矩阵[x1x2x3]+[b1[1]b2[1]b3[1]b4[1]]b[1]z^{[1]} = \begin{bmatrix} z^{[1]}_1 \\ z^{[1]}_2 \\ z^{[1]}_3 \\ z^{[1]}_4 \\ \end{bmatrix} = \overbrace{ \begin{bmatrix} \cdots & W^{[1]T}_1 & \cdots \\ \cdots & W^{[1]T}_2 & \cdots \\ \cdots & W^{[1]T}_3 & \cdots \\ \cdots & W^{[1]T}_4 & \cdots \\ \end{bmatrix} }^{W^{[1]}\quad 4\times3的矩阵} \begin{bmatrix} x_1 \\ x_2 \\ x_3 \\ \end{bmatrix} + \overbrace{ \begin{bmatrix} b^{[1]}_1 \\ b^{[1]}_2 \\ b^{[1]}_3 \\ b^{[1]}_4 \\ \end{bmatrix} }^{b^{[1]}}

a[1]=[a1[1]a2[1]a3[1]a4[1]]=σ(z[1])a^{[1]} = \begin{bmatrix} a^{[1]}_1 \\ a^{[1]}_2 \\ a^{[1]}_3 \\ a^{[1]}_4 \\ \end{bmatrix} = \sigma(z^{[1]})

多样本向量化(Vectorizing across multiple examples)

在上个视频中,了解了针对单一的训练样本,在神经网络上计算出预测值。

接下来,将说明如何向量化多个训练样本,并计算结果。该过程与逻辑回归中类似。

同样以上面给出的两层神经网络的隐藏层为例。

首先将m个训练样本的特征输入向量写成一个矩阵。

X=[x(1)x(2)x(m)]X = \begin{bmatrix} \vdots & \vdots & \vdots & \vdots \\ x^{(1)} & x^{(2)} & \cdots & x^{(m)} \\ \vdots & \vdots & \vdots & \vdots \\ \end{bmatrix}

隐藏层的两个参数wbw,b为:

W[1]=[W1[1]TW2[1]TW3[1]TW4[1]T]b[1]=[b1[1]b2[1]b3[1]b4[1]]W^{[1]} = \begin{bmatrix} \cdots & W^{[1]T}_1 & \cdots \\ \cdots & W^{[1]T}_2 & \cdots \\ \cdots & W^{[1]T}_3 & \cdots \\ \cdots & W^{[1]T}_4 & \cdots \\ \end{bmatrix} \\ \\ b^{[1]} = \begin{bmatrix} b^{[1]}_1 & \cdots \\ b^{[1]}_2 & \cdots \\ b^{[1]}_3 & \cdots \\ b^{[1]}_4 & \cdots \\ \end{bmatrix}

第一个式子:每一行代表一个神经元,每行各分量对应一个输入特征。

第二个式子:首列各分量代表四个神经元的偏置b,后面所有列与第一列相同。

矩阵b[1]b^{[1]}每一列分别对应一个训练样本,不同训练样本对于同一个神经元的偏置是相同的,因此矩阵后面各列与首列相同。

函数输出zz写成矩阵:

Z[1]=[z[1](1)z[1](2)z[1](m)]Z^{[1]} = \begin{bmatrix} \vdots & \vdots & \vdots & \vdots \\ z^{[1](1)} & z^{[1](2)} & \cdots & z^{[1](m)}\\ \vdots & \vdots & \vdots & \vdots \\ \end{bmatrix}

注意区分[]()

预测值aa写成矩阵:

A[1]=[a[1](1)a[1](2)a[1](m)]A^{[1]} =\begin{bmatrix} \vdots & \vdots & \vdots & \vdots \\ a^{[1](1)} & a^{[1](2)} & \cdots & a^{[1](m)}\\ \vdots & \vdots & \vdots & \vdots \\ \end{bmatrix}

分析矩阵A[1]A^{[1]}每一个元素的含义。

例如矩阵AA中第一行第一列的元素,代表隐藏层中,第一个神经元对第一个训练样本的预测输出值。

例如矩阵AA中第二行第一列的元素,代表隐藏层中,第二个神经元对第一个训练样本的预测数值。

总的来说,从水平方向(或横向)看,矩阵A代表了各个训练样本,从竖直方向看,矩阵A的不同索引对应不同的神经元。

至此,完成了一个神经网络的前向传播。

激活函数

使用神经网络时,需要决定哪种激活函数用在隐藏层上,哪种用在输出层。到目前为止,只是用过sigmoid激活函数。但是有时其他的激活函数效果会更好。

在神经网络的前向传播中,在a[1]=σ(z[1]),a[2]=σ(z[2])a^{[1]} = \sigma(z^{[1]}),a^{[2]} = \sigma(z^{[2]})这两步使用到了sigmoid函数,在这里被称为激活函数。

更通常的情况下,使用不同的激活函数g(z[1])g(z^{[1]})gg可以是除了sigmoid函数以外的其他非线性函数。例如双曲正切函数:

tanh(z)=ezezez+eztanh(z) = \frac{e^{z} - e^{-z}}{e^{z} + e^{-z}}

该函数的值域是(-1, 1)

tanh函数在总体上要优于sigmoid函数。

事实上,tanh函数是sigmoid函数向下平移和伸缩的结果。

对它变形后,穿过了(0,0)点。

函数图像为:

神经网络基础-反向传播-激活函数| PLM's Notes | 好好学习,天天笔记

结果表明,如果在隐藏层中使用tanh双曲正切函数,效果总是优于sigmoid函数。并且因为值域为(-1, 1),因此均值更接近0。

有一点要说明:现在几乎不会使用到sigmoid激活函数,tanh函数在所有场合都优于sigmoid函数。但是一个例外是在二分类问题中,因为输出层yy的值为0或1,可以对输出层使用sigmoid函数。

所以,在不同的神经网络层中,激活函数可以不同。

在不同层中,使用不同的上标来标明激活函数。如g[1]g^{[1]}表示第一层使用的激活函数。


sigmoid函数和tanh函数共同的缺点是:在zz特别大或特别小的情况下,激活函数导数梯度会变得非常小,最后接近于0,这会降低梯度下降的速度。


另外一个很流行的函数是:ReLu函数,图像如下图。

Why is the ReLU function not differentiable at x=0?

公式为:f(x)=max(0,x)f(x) = max(0, x)


有一些选择激活函数的经验法则:

如果是二分类问题(输出是0,1),则输出层选择sigmoid函数,其他所有层都选择Relu函数。

这是很多激活函数的默认选择,即 如果在隐藏层中不确定使用哪个激活函数,那么通常会使用Relu函数。

该函数的一个优点是:当zz是负值是,导数为0。


也有另外一个版本的Relu被称为Leaky Relu

zz是负值时,函数值不是0,而是微微向y轴负方向倾斜。

image-20220907230218182

公式可以为:f(x)=max(0.1x,x)f(x) = max(0.1x, x)。这里的0.1可以为其他值。

这两个Relu函数的优点是:

  1. z的区间变动很大的情况下,激活函数的导数或斜率会远大于0。并且在程序中可以很容易使用if-else语句实现。而sigmoid需要浮点四则运算。在实践中,使用Relu的神经网络通常会比使用sigmoidtanh学习的更快。
  2. sigmoidtanh函数的导数在正负饱和区梯度都接近于0,这会导致梯度弥散。而ReluLeaky Relu函数大于0的部分都为常数,不会产生梯度弥散的现象。(当Relu进入负半区时,梯度为0,神经元不会训练,产生所谓稀疏性,Leaky Relu不会有这个问题)。

概括一下不同激活函数。

  • sigmoid函数:除了输入层是一个二分类问题,基本不会使用。
  • tanh函数:tanh很优秀,几乎适合所有场合。
  • Relu函数:最常用的默认函数。如果不确定用哪个激活函数,就是用ReluLeaky Relu。其中Leaky Relu函数f(x)=max(ax,x)f(x) = max(ax, x),可以为不同算法选择不同的参数a(0 < a < 1)

在深度学习中经常遇到的问题时,在编写神经网络时,会有很多选择:隐藏层单元的个数,激活函数的选择,初始化权重。这些选择想得到一个比较好的指导原则比较困难。

因此通常的建议是,如果不确定哪一个激活函数效果更好,可以都试试,然后在验证集上评价。

为什么要使用非线性激活函数

假如使用线性激活函数,例如令a[1]=z[1]a^{[1]} = z^{[1]},也就是令激活函数g(z)=zg(z) = z,也被称为线性激活函数(更学术的名字是恒等激活函数,因为只是把输入值输出)。同样,为了说明问题把a[2]=z[2]a^{[2]} = z^{[2]},那么这个模型的输出yy仅仅只是输入特征的一个线性组合,可以由z=wx+bz = wx+b来计算。这里不再给出计算过程(很简单,把第一层的变线性函数带入第二层的a[2]=z[2]a^{[2]} = z^{[2]})。

事实证明,如果使用线性激活函数或者没有使用激活函数,那么无论神经网络有多少层,一直在做的只是计算线性函数,所以不如直接去掉所有隐藏层。

激活函数的导数

在神经网络中使用反向传播时,需要计算激活函数的导数或斜率。针对上面讲到的四种激活函数,分别计算导数(就是个求导的事儿)。

  1. sigmoid函数

    g(z)=11+ezg(z)=dg(z)dz=ez(1+ez)2=g(z)(1g(z))g(z) = \frac{1}{1+e^{-z}} \\ \begin{aligned} {g(z)}^{'} = \frac{d g(z)}{dz} & = \frac{e^{-z}}{(1+e^{-z})^2} \\ & = g(z)(1-g(z)) \end{aligned}

  2. tanh函数

    g(z)=tanh(z)=ezezez+ezg(z)=dg(z)dz=4(ez+ez)2=1(tanh(z))2g(z) = tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} \\ \begin{aligned} {g(z)}^{'} = \frac{dg(z)}{dz} & = \frac{4}{(e^z + e^{-z})^2} \\ & = 1 - (tanh(z))^2 \end{aligned}

  3. ReLU

    g(z)=max(0,x)g(z)={0z<01z>0undefinedz=0g(z) = max(0, x) \\ \\ {g(z)}^{'} = \begin{cases} 0 \quad z \lt 0 \\ 1 \quad z\gt 0 \\ undefined \quad z = 0 \end{cases}

    通常该函数在z=0z=0处不可导,但是通常在z=0z=0时给定其导数10,当然z=0z = 0的情况很少。

  4. Leaky ReLU

    g(z)=max(0.01z,z)g(z)={0.01z<01z>0undefinedz=0g(z) = max(0.01z, z) \\ \\ {g(z)}^{'} = \begin{cases} 0.01 \quad z \lt 0 \\ 1 \quad z\gt 0 \\ undefined \quad z = 0 \end{cases}

    同样,可在z=0z=0处给定导数0.011

神经网络的梯度下降

在这部分,说明神经网络的反向传播。

首先,nxn_xn[0]n^{[0]}表示输入特征个数,即输入层特征变量个数;n[1]n^{[1]}表示隐藏单元个数(隐藏层神经元个数),n[2]n^{[2]}表示输出单元个数(输出层神经元个数)。

有参数:W[1]W^{[1]}(维度(n[1],n[0])(n^{[1]}, n^{[0]})),b[1]b^{[1]}(维度(n[1],1)(n^{[1]}, 1)),W[2]W^{[2]}(维度(n[2],n[1])(n^{[2]}, n^{[1]})),b[2]b^{[2]}(维度(n[2],1)(n^{[2]}, 1))


神经网络成本函数,假设在做二分类问题,那么代价函数为:

J(W[1],b[1],W[2],b[2])=1mi=1mL(a,y)J(W^{[1]},b^{[1]},W^{[2]}, b^{[2]}) = \frac{1}{m}\sum_{i=1}^mL(a, y)

损失函数LL和逻辑回归中完全一样。

L(a,y)=ylog(a)(1y)log(1a)L(a, y) = -ylog(a) - (1-y)log(1-a)

每次梯度下降,需要循环计算下列值:

dW[1]=dJdW[1]db[1]=dJdb[1]dW[2]=dJdW[2]db[2]=dJdb[2]dW^{[1]} = \frac{dJ}{dW^{[1]}} \\ db^{[1]} = \frac{dJ}{db^{[1]}} \\ dW^{[2]} = \frac{dJ}{dW^{[2]}} \\ db^{[2]} = \frac{dJ}{db^{[2]}}

然后更新参数:

W[1]=W[1]αdW[1]b[1]=b[1]αdb[1]W[2]=W[2]αdW[2]b[2]=b[2]αdb[2]W^{[1]} = W^{[1]} - \alpha dW^{[1]} \\ b^{[1]} = b^{[1]} - \alpha db^{[1]} \\ W^{[2]} = W^{[2]} - \alpha dW^{[2]} \\ b^{[2]} = b^{[2]} - \alpha db^{[2]}


总结

正向传播相关公式如下:

z[1]=W[1]x+b[1]a[1]=σ(z[1])z[2]=W[2]a[1]+b[2]a[2]=σ(z[2])z^{[1]} = W^{[1]}x + b^{[1]} \\ a^{[1]} = \sigma(z^{[1]}) \\ z^{[2]} = W^{[2]}a^{[1]} + b^{[2]} \\ a^{[2]} = \sigma(z^{[2]}) \\

反向传播相关公式如下:

dz[2]=A[2]YdW[2]=1mdz[2]A[1]Tdb[2]=1mnp.sum(dz[2],axis=1,keepdims=True)dz[1]=W[2]Tdz[2](n[1],m)g[1](z[1])(n[1],m)dW[1]=1mdz[1]xTdb[1]=1mnp.sum(dz[1],axis=1,keepdism=True)\begin{aligned} & dz^{[2]} = A^{[2]} - Y \\ & dW^{[2]} = \frac{1}{m}dz^{[2]}{A^{[1]}}^T \\ & db^{[2]} = \frac{1}{m}np.sum(dz^{[2]}, axis=1,keepdims = True) \\ \\ & dz^{[1]} = \underbrace{ {W^{[2]} }^Tdz^{[2]} }_{(n^{[1]}, m)} * \underbrace{ {g^{[1]} }^{'}(z^{ [1] }) }_{(n^{[1]}, m)} \\ & dW^{[1]} = \frac{1}{m}dz^{[1]}x^T \\ & db^{[1]} = \frac{1}{m}np.sum(dz^{[1]}, axis=1,keepdism=True) \\ \end{aligned}

(axis=1参数表示水平相加,keepdims参数防止python输出奇怪秩数)。

*是矩阵点乘,即维数相同矩阵对应位置相乘,满足交换律。

其中dz[1]dz^{[1]}比较难算。

因为z[1]z^{[1]}是隐藏层的输出,而输出层的输出z[2]z^{[2]}与代价函数有直接联系,因此需要链式求导才可算出dz[1]dz^{[1]}

首先有:

dz[1]=dJdz[1]=dJdz[2]dz[2]dz[1]z[2]=W[2]a[1]+b[2]=W[2]g[1](z[1])+b[2]dz^{[1]} = \frac{dJ}{dz^{[1]}} = \frac{dJ}{dz^{[2]}}\frac{dz^{[2]}}{dz^{[1]}} \\ \\ \begin{aligned} z^{[2]} & = W^{[2]}a^{[1]} + b^{[2]} \\ \\ & = W^{[2]}g^{[1]}(z^{[1]}) + b^{[2]} \\ \end{aligned}

其中g[1]g^{[1]}表示隐藏层使用的激活函数。

因此可得到z[2]z^{[2]}z[1]z^{[1]}的导数。

注意:这里的乘法是数值意义的乘法,不是矩阵乘法,因此写成点乘。

dz[2]dz[1]=W[2]g[1](z[1])\frac{dz^{[2]}}{dz^{[1]}} = W^{[2]} * {g^{[1]}}^{'}(z^{[1]})

代入上面链式求导公式得到:

dz[1]=W[2]Tdz[2]g[1](z[1])dz^{[1]} = {W^{[2]}}^{T}dz^{[2]} * {g^{[1]}}^{'}(z^{[1]})

也可根据下图,来更好理解上面公式中的链式求导。

image-20220909092916976

随机初始化

当训练神经网络时,权重参数的初始化是很重要的。对于逻辑回归,把权重全初始化为0是可以的。但是对于神经网络,如果把权重参数全部初始化为0,则梯度下降不会起作用。


举一个例子,有两个输入特征,则n[0]=2n^{[0]} = 2,2两个隐藏层单元,n[1]=2n^{[1]}=2。那么隐藏层的权重参数矩阵W[1]W^{[1]}是一个2x2的矩阵,如果把该矩阵所有参数初始化为0,偏置b[1]b^{[1]}也初始化为0,[0,0]T{[0,0]}^T

把偏置项置为0是可以的,但是把权重参数ww置为0就有问题了

那么这样计算的话,会发现隐藏层的两个单元的输出a1[1],a2[1]a^{[1]}_1,a^{[1]}_2相等。因为这两个隐含单元计算同样的函数,参数也想通。并且做反向传播时,dz1[1],dz2[1]dz^{[1]}_1,dz^{[1]}_2也会相等。

因此如果把权重都初始化为0,那么所有隐含单元的计算都是相同的。无论运行梯度下降多少次,都会一直计算相同的函数。


那么如何初始化呢。

解决方法就是随机初始参数。可以使用np.random.randn(2,2)(生成高斯分布),通常会再乘上一个较小的数,例如0.01

例如上面例子:

W[1]=np.random.randn(2,2)0.01,b[1]=np.zeros((2,1))W^{[1]} = np.random.randn(2,2) * 0.01, b^{[1]} =np.zeros((2,1))

那么为什么要乘上这个常数0.01

这是因为如果使用激活函数tanhsigmoid,当参数值很大时,求出的z值就会很大或很小,这种情况下,z就会落在sigmoidtanh很平坦的地方,这些位置梯度很小,从而造成梯度消失。

而梯度很小,梯度下降就会很慢,从而造成参数收敛很慢。


事实上,有比0.01更好的参数。当训练只有一个隐藏层的网络时,可以使用0.01

但当训练一个非常非常深的网络时,可能要试试其他常数。


2-浅层神经网络
https://zhaoquaner.github.io/2022/09/10/DeepLearning/CourseNote/2-浅层神经网络/
更新于
2022年9月19日
许可协议