本文摘取转载至:https://www.bilibili.com/video/BV1NCgVzoEG9

一. 函数

你要相信这个世界上的所有逻辑和知识都可以用一个函数来表示。

我们只需要将现实世界抽象为符号,在设置好一些运算规则,也就是函数,最后算出来结果,反过来解释现实世界就可以了。

比如说输入直角三角形的两个边长,根据勾股定理就可以得到斜边的边长。

再比如输入物体的质量(m)和加速度(a),根据牛顿第二定律就可以得到物体施加的力。这就是人工智能早期的思路符号主义。

但这条路走到头了,很多问题人类实在是想不出怎么写成一个明确的函数,从上帝视角看,就是人类还是太菜了。比如说一个简简单单的识别一张图片是否是狗,对人类来说可能简单到爆炸,但是要让计算机运行一段程序来识别,那一下子就变成了一个史诗级难题。就连有着明确语法规则和词典的翻译函数尚且没有办法做到足够丝滑,那更别说复杂多变的人类智能了。

那既然不知道这个函数长什么样怎么办呢?那就别硬找了,换个思路,我们先从一个简单的例子入手,比如我们知道一些X和Y的值,我们想找到Y和X的函数关系,你有什么办法呢?

X Y
1 2
2 4
3 6
4 8

有人说这不就是 $y=2x$ 吗?啥都能看出来。没错,这就是符号主义的思想,觉得世间万物都能找到背后明确的规律。

但假如我们一开始没有找到这个规律怎么办呢?比如说下面这组数就不能一眼看出来,那就用人类有史以来最具智慧的办法猜。

X Y
2 2.5
3 2.5
4 3
5 3.5

我们先把这个X、Y放到坐标轴上先随便猜一下。比如说函数关系就是Y等于X也就是这里的W和B分别是一和0。然后我们一点点调整这个W和B使得这条直线越来越贴近真实数据,最后发现完全吻合了,行,就它了。

但有的时候可能很难找到完全吻合的函数,比如说这样一组数据。那可怎么办呢?

没事,那就简化一下,问题大差不差,能近似就行了,别要求那么多。我们的做法仍然是一点一点调整W和B看差不多的时候就停下来,这就是现代人工智能的思路。猜和简化问题,说白了实际上就是累,摆烂了,承认自己太菜了,找不到精确的函数了,那就找一个从结果上看大差不差的函数,然后连蒙带猜,逐渐逼近真实答案就好了。

有人说,这连蒙带猜的靠谱吗?一看就不是什么正路子。没错,在连接主义成为主流之前,很多人工智能的专家也是这么想的。但就是这样靠连蒙带猜的办法,我们居然可以用很少的参数轻松实现手写数字识别这样的任务。正是这种方式在很多地方证明了它的有效性,人们才开始重视起来。

回到正题,刚刚我们举的例子都比较简单,只用直线方程就可以表示了。但假如数据稍稍变化一下就会发现,不论怎么调整,这里的W和B好像都无法接近真实的数据,那这个时候就需要让这条直线弯一弯了。换句话说就是我们需要从原来的线性函数进化到非线性函数了。那我们就来研究一下,怎么把原来这个原本线性的函数变成非线性的呢?很简单,在这个函数最外层再套一个非线性的运算就可以了。

  • 比如平方:
  • 比如三角函数:
  • 比如e

这就是激活函数。它的目的就是把原本死气沉沉的线性关系给盘活了,变成了变化能力更强的非线性关系。听到非线性关系的同学千万不要害怕,常用的激活函数都简单到爆炸,但是就是能起到很好的效果。

好了,回到这个新的函数形式,我们之前仅仅有一个输入的变量就是X但实际上可能有很多输入,所以这里的每一个X都要对应一个W像这样:

$f(x_1, x_2) = g(w_1x_1 + w_2x_2 + b)$

再者有的时候只套一层激活函数还是没有办法达到很好的效果。也就是说这个曲线弯的还不够灵活,那这要怎么办呢?很简单,我们把刚刚这一大坨当做一个整体,在此基础之上再进行一次线性的变换,然后再套上一个激活函数,这样就可以无限的套娃下去了:

$f(x_1, x_2) = g\left(w_4 g\left(w_3 g\left(w_1 x_1 + w_2 x_2 + b\right) + b_2\right) + b_3\right)$

那通过这样的方式,我们就可以构造出非常复杂的线性关系,而且理论上可以逼近任意的连续函数。

当然了,这样写下去实在是太让人头大了,普通人看个两层估计脑子就炸了。所以我们得换一种更傻瓜的更直观的形式。回到最初的形式,我们把这样一个线性变换套一个激活函竖画成下面这样:

这样左边是输入层,只有一个输入X;右边是输出层,只有一个输出Y。

我们把这里的每一个小圈圈叫做一个神经元。当然这里我不建议你把它跟生物的神经元相类比,因为他们两个其实一毛钱关系都没有,看似很形象,但实际上反而会影响理解,总之就是这样。两个圈圈一点就表示上面一个函数关系。刚刚我们说输入可能有多个,所以对应的变化就是输入层变成了多个:

像这样我们还说可以继续在外层不断的套线性变换,再套激活函数。那么每套一层就相当于神经元水平方向又扩展了一个:

当然扩展之后,中间这一层就不再是最终的输出了,而是包裹在了一个很复杂的函数变换之中。看不到,我们管它叫做隐藏层。而整个这一大坨神经元互相连接形成的网络结构就叫做神经网络。

好,接下来我们看一下函数和神经网络的对应关系。

首先有两个输入变量,一个是X1,另一个是X2,它们构成了输入层,然后X1、X2进行一次线性变换,再进行一次激活函数,就得到了隐藏层A。这个A对应的就是上面这一大坨灰色的表达式。那我们把它当做一个整体,继续进行一次线性变换和一次激活函数,这就计算出了最终的输出层Y。

重新再看一下这个过程,从神经网络的这个图来看的话,似乎就像是一个信号从左到右传播了过去。那这个过程就叫做神经网络的前向传播。但是实际上,就是一点点分步骤把一个函数的值计算出来了而已,神经网络的每一层神经元都可以无限增加,同时隐藏层的层数也可以无限增加,进而就可以构成一个非常复杂的非线性函数了。

虽然这个函数可能非常复杂,但是我们的目标却非常简单和明确。就是根据已知的一组X和Y的值猜出所有这里的W和B都是多少。当然了,我们一开始举的例子非常简单,光靠肉眼法就能慢慢猜出答案了,但是现在有这么多参数,可能就无法凭感觉猜了。

二. 梯度下降

下面我们来看看如何计算这个w(权重)和b(偏置)我们先别搞那么复杂的非线性函数,先从最简单的一个线性函数入手。首先第一个问题就是什么样的w、b是好的呢?答案其实很简单,我们的目标是让这个函数的输出结果尽可能的接近真实数据。因此好的w和b就是能够使得函数的输出尽可能拟合真实数据的那一组参数。接下来第二个问题,什么叫拟合的好?先别想那么多,从直觉上理解这条线儿就拟合的挺好,那这个就拟合的不好好,第三个问题就自然浮出水面了。

怎么用数学语言表达刚刚我说的这个直觉上的理解呢?很简单,我们可以在每个数据点上画一条竖直的线,使其与拟合的直线相交。由于这里的每个点的纵坐标表示的就是真实数据,我们用 $y$ 来表示。落在直线上的点表示预测数据,我们用 $\hat{y}$ 来表示,那么这条线段的长度就是真实值与预测值的误差。为了评估整体的拟合效果,我们可以将所有这些线段的长度加起来。

$\sum_{i=1}^{N} |y_i - \hat{y}_i|$

这样就得到了预测数据与真实数据之间的总的差异,也就可以反映当前这个线性函数与真实数据的拟合度了。而这个表示预测数据与真实数据误差的函数,我们叫它损失函数

我们着重看一下这个公式。这个绝对值有些讨厌,数学优化时不太友好。我们做数学题时往往最讨厌碰到这种带绝对值的问题了,要各种分类讨论,想想就头疼。所以我们改造下用平方来代替。一来解决了绝对值不平滑的问题,二来也放大了误差较大的值的影响。然后我们再根据样本的数量平均一下,消除样本数量大小的影响。最终得到这个公式就叫做均方误差(MSE)。

$MSE = \frac{1}{N}\sum_{i=1}^{N} (y_i - \hat{y}_i)^2$

而均方误差就是用来表示损失函数的一种。我们把损失函数记作L从参数的视角来看,它就是一个关于W和B的一个函数。

$L(w, b) = \frac{1}{N}\sum_{i=1}^{N} (y_i - \hat{y}_i)^2$

好,先不要过于陷入这个公式的细节,还记得我们要干啥不?损失函数表示的是真实值与预测值的误差,而我们的目的就是让这个误差最小,也就是找到可以让这个损失函数L最小化的那个W和B那怎么求解呢?自然就是用我们初中就学过的让其导数等于零求极值点的过程。

我们先不上公式,通过一个具体的例子来说,假设我们就四个样本数据,就是简简单单的(1,1)、(2,2)、(3,3)、(4,4)这样。然后我们的线性模型也简单点,把B去掉,只保留一个W其实就是一个简单的经过原点的一条直线 $y = wx$ 。

这个时候我们展开一下损失函数,把 $\hat{y}$ 的值代入进来:

$L(w) = \frac{1}{N}\sum_{i=1}^{N} (y_i - wx_i)^2$

把求和符号展开:

$L(w) = \frac{1}{4}\left[(y_1 - wx_1)^2 + (y_2 - wx_2)^2 + (y_3 - wx_3)^2 + (y_4 - wx_4)^2\right]$

然后再把上面这组(X,Y)的数据带进来:

$L(w) = \frac{1}{4}\left[(1 - 1w)^2 + (2 - 2w)^2 + (3 - 3w)^2 + (3 - 4w)^2\right]$

平方展开后:

$L(w) = \frac{1}{4}\left(30 - 60w + 30w^2\right)$

化简:

$L(w) = 7.5 - 15w + 7.5w^2$

好,化简之后我们就可以清晰的看到,这就是一个简简单单的关于W的一个二次函数。接下来对W求导,再让其导数等于0,就可以求出W等于一了。

$L’(w) = -15 + 15w$

函数求导法则:

函数类型 具体函数 导数公式 示例
常数函数 $y = C$( $C$ 为常数) $(C)’ = 0$ $(3)’ = 0$
幂函数 $y = x^\alpha$( $\alpha$ 为实数) $(x^\alpha)’ = \alpha x^{\alpha - 1}$ $(x^3)’ = 3x^2$

代入回原直线函数,此时Y等于X就是让损失函数最小,也就是最拟合真实数据的那条直线。

回过头再来看一下这个损失函数,它实际上是一条开口向上的抛物线。刚刚其实就在寻找这个最低点,采用的办法就是让导数等于0。

不过我们此时的模型是简单的线性函数,而且只保留了w(权重)而忽略了b(偏置)如果此时把b算进来,那就需要求关于w和b两个变量的损失函数的最小值了。这个时候损失函数图像就是一个三维图像,是一个开口向上的这个碗状形状。

我们的目标同样也是找到这个二元函数最小值所对应的那个w和b而多元函数求最小值的问题就不再是导数了,而是要让每个参数的偏导数等于零来求解。

$\frac{\partial L(w, b)}{\partial w} = 0$ :对损失函数 L 关于权重 w 求偏导,并令偏导数等于 0。这一步是为了找到使损失最小的 w。

$\frac{\partial L(w, b)}{\partial b} = 0$ :对损失函数 L 关于偏置 b 求偏导,并令偏导数等于 0。这一步是为了找到使损失最小的 b。

在数学中, $ \partial $ 是偏导数的符号,读作 “偏”。

它用于多元函数的求导场景:当一个函数依赖多个变量(比如 $ f(x,y,z) $ 同时依赖 $ x $、$ y $、$ z $)时,若只对其中一个变量求导(其余变量视为 “固定不变”),就需要用偏导数符号 $ \partial $ 。

例如,对于函数 $ f(x,y) = x^2 + 3xy + y^3 $ :

  • 对 $ x $ 求偏导: $ \frac{\partial f}{\partial x} = 2x + 3y $ (此时把 $ y $ 当作常数);

  • 对 $ y $ 求偏导: $ \frac{\partial f}{\partial y} = 3x + 3y^2 $ (此时把 $ x $ 当作常数)。

偏导数在这里就不展开了,不过可能不少人学偏导数的时候都觉得很头疼。其实很简单,对w求偏导就是把b当做常数,和一元函数求导就一样了。

回到之前讲的神经网络。神经网络是一个通过线性函数和非线性激活函数不断组合形成的一个非常复杂的非线性函数。它对应的损失函数则是更复杂的非线性函数。往往不能像刚刚那样,通过让导数等于零直接求出最小值,那怎么办呢?

人们的解决办法也非常的简单粗暴,就是一点点是具体怎么试呢?假如此时w和b的值均为五,损失函数计算结果是10。我们来看一下,尝试把w增加一个单位变成6,再次计算损失函数,发现结果是9,那就说明w的这次调整是对的,让损失函数变小了。再尝试把b增加一变成6,发现损失函数增加了二变成了11,说明b的增大会让误差变大。那我们就反过来把b往小了调整,让损失函数继续变小,如此循环往复,不断调整就可以了。

总之我们每次都看下当前状况调整w或b对损失函数的影响,然后每次把参数向着让损失函数变小那个方向调整一点点,直到让损失函数足够小。那具体怎么调呢?回到最初始的状态,w变化一点点,使得损失函数会变化多少。这其实就是损失函数对w的偏导数,对b来说同样也是如此。

我们要做的就是让w和b不断往偏导数的反方向去变化

$w = w - \frac{\partial L(w, b)}{\partial w}$

$b = b - \frac{\partial L(w, b)}{\partial b}$

具体变化的快慢再增加一个系数来控制,我们叫它学习率

$w = w - \eta \frac{\partial L(w, b)}{\partial w}$

$b = b - \eta \frac{\partial L(w, b)}{\partial b}$

这些偏导数所构成的向量( $\left( \frac{\partial L}{\partial w}, \frac{\partial L}{\partial b} \right)$ )就叫做梯度。而不断变化w和b让损失函数逐渐减小的一个过程,进而求出最终的W和B这个过程就叫做梯度下降。

梯度核心意义是:沿着梯度的方向,函数的增长速率最快;而沿着梯度的反方向,函数的下降速率最快

假设我们有一组真实数据: $(x_1, y_1) = (1, 2)$ 、 $(x_2, y_2) = (2, 4)$ 、 $(x_3, y_3) = (3, 6)$ ,我们要拟合线性模型 $ \hat{y} = w x + b $,损失函数采用均方误差 $ L(w, b) = \frac{1}{3}\sum_{i=1}^{3} (y_i - (w x_i + b))^2 $ 。

步骤1:假设初始时 $ w = 0 $,$ b = 0 $,学习率 $ \eta = 0.1 $

步骤2:先对 $ w $ 求偏导

$
\frac{\partial L}{\partial w} = \frac{2}{3}\sum_{i=1}^{3} -x_i (y_i - (w x_i + b))
$

代入初始值 $ w=0 $、$ b=0 $:

$
\frac{\partial L}{\partial w} = \frac{2}{3}\left[ -1 \times (2 - 0) + -2 \times (4 - 0) + -3 \times (6 - 0) \right] = \frac{2}{3}(-2 - 8 - 18) = \frac{2}{3} \times (-28) \approx -18.67
$

再对 $ b $ 求偏导:

$
\frac{\partial L}{\partial b} = \frac{2}{3}\sum_{i=1}^{3} - (y_i - (w x_i + b))
$

代入初始值:

$
\frac{\partial L}{\partial b} = \frac{2}{3}\left[ -(2 - 0) + -(4 - 0) + -(6 - 0) \right] = \frac{2}{3}(-12) = -8
$

步骤 3:梯度下降更新参数

更新 $ w $:

$
w = 0 - 0.1 \times (-18.67) \approx 1.867
$

更新 $ b $:

$
b = 0 - 0.1 \times (-8) = 0.8
$

步骤 4:迭代优化

重复计算梯度、更新参数的过程。比如经过第 2 次迭代后,$ w \approx 1.95 $,$ b \approx 0.3 $;经过数十次迭代后,$ w $ 会逐渐趋近于 2,$ b $ 逐渐趋近于 0,此时损失函数 $ L(w, b) $ 几乎为 0,模型 $ \hat{y} = 2x + 0 $ 完美拟合真实数据 $ y = 2x $。

这个例子清晰展现了梯度下降如何通过 “计算梯度→反向调整参数→迭代优化” 的流程,从初始的 “欠拟合” 状态逐步逼近最优参数,最终让模型拟合真实数据。

在之前在线性回归问题中,偏导数就是一个1元2次函数,求导非常简单。但是在神经网络中,函数本身就是一个复杂到变态的非线性函数,那更别说损失函数了,直接求导可能就不太好求了

那怎么办呢?其实也很简单,虽然神经网络整体所代表的函数很复杂,但是层与层之间的关系却是非常简单的。我们就用上面这个麻雀虽小,但五脏俱全的一个简单的神经网络结构来举例,只有一个输入和一个输出,而且中间的隐藏层也只有一个神经元。

首先我们根据输入x的值计算出隐藏层a的值,这里的g就是随便一个激活函数。再根据a的值计算出输出层 $\hat{y}$ 的值,然后再根据 $\hat{y}$ 的值以及真实值 y 计算出损失函数。那这里损失函数就用均方误差了。而且由于只有一个输出数据,所以说我把求和符号也省去了。

这个神经网络结构中一共有四个参数( $w_1$ 、 $b_1$ 、 $w_2$ 、 $b_2$ )要通过梯度下降的方式逐渐求解。

之前也说了,关键问题就是求出L对它们的偏导数。那我们直接拿最难的 $w_1$ 来举例,如何求出L对 $w_1$ 的偏导数呢?

其实很简单,从偏导数要表达的意思出发,一下就能想明白了。其实我们就想看 $w_1$ 变化一点点会使得L变化多少呢?那我们就先看 $w_1$ 变化一个单位会使得 $a$ 变化多少,再看看 $a$ 变化一个单位会使得 $\hat{y}$ 变化多少,然后再看 $\hat{y}$ 的变化一个单位会使L变化多少。每一个都是一个简单的偏导数,那把这三者乘起来就知道 $w_1$ 变化一个单位会使得L变化多少了。

$\frac{\partial L}{\partial w_1} = \frac{\partial L}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial a} \frac{\partial a}{\partial w_1}$

如果实在想不明白的话,可以联想一下齿轮怎么计算,第一个转一圈会使得最后一个齿轮转多少圈呢?其实就是乘起来。那这种偏导数的计算方式就叫做链式法则,其实就是微积分中的复合函数求导。

由于我们可以从右向左依次求导,然后逐步更新每一层的参数,直到把所有的神经网络的参数都更新一遍,在计算前一层时用到的一些偏导数的值,后面也会用到。所以说不用计算那么多,而是让这些值从右向左一点点传播过来就好了。我们把这样一个过程形象的称之为反向传播

结合之前的知识,我们通过前向传播,根据输入 $x$ 计算出输出 $y$ 然后再通过反向传播计算出损失函数关于每个参数的梯度。然后每个参数都向着梯度的反方向向变化一点点,这就构成了神经网络的一次训练。而神经网络经过多轮这样的训练,里面的参数都一点一点的变化,直到让损失函数足够小,也就是找到了我们想要的那个函数。

三. 调教神经网络的方法

我们知道神经网络的本质就是线性变换套上一个激活函数不断组合而成的一个非常复杂的非线性函数,并且巧妙的通过梯度下降一点一点计算出神经网络中的一组合适的参数。那这样看起来,只要神经网络足够大,岂不是什么问题都能解决了?

理想很丰满,现实很骨感,令人头疼的问题很快就接踵而至。前文中我们提到,我们的目标是让数据拟合的好。比如左边这个就拟合的挺好,右面这个就不太好:

那再来一组图,这回你认为是左边好还是右边好呢?纯从预测值与真实值误差来看,也就是损失值最小化这个目标来看的话,显然右边这个更好。但直觉似乎告诉我们,右边这个好像有点好的太过了。结果可能是只适合训练数据,对新数据的预测反而不如左边的准。

这种在训练数据上表现的很完美,但是在没见过的数据上表现的很糟糕的现象就叫做过拟合。而在没见过的数据上的表现能力,我们叫它泛化能力。那为什么会过拟合呢?看看刚刚这个图,其实就是训练数据本身是个很简单的规律,但模型太复杂了,把那些噪声和随机波动也给学会了,那这该怎么办呢?自然就是简化一下模型的复杂度。比如说这个案例中,你用一个非常复杂的神经网络模型来训练,效果甚至不如一个线性模型好。这就告诉我们神经网络不是越大越好,那简化模型复杂度有效果,与之相对的就是增加训练数据的量。数据量足够充足,那原本复杂的模型也就相对变得简单了。

但有的时候我们确实无法收集或者懒得收集更多的数据,怎么办呢?那就在原有的数据中创造更多的数据。比如说在图像处理中,我们可以对图像进行旋转、翻转、裁剪、加噪声等操作,创造出更多新的训练样本,这就叫做数据增强。这样不仅仅能够产生更多的数据,还刚好训练了一个让模型不因输入的一点点小的变化而对结果产生很大的波动,这就是增强了模型的鲁棒性。

好,刚刚是从数据和模型本身入手来防止过拟合。那有没有可能从训练过程入手阻止过拟合的发生呢?

其实训练过程就是不断调整参数的过程,其实只要不让参数继续过分的向着过拟合的方向发展就可以了。所以有个简单到你都不敢相信的办法,就是提前终止训练过程。意思就是差不多就行了,不用追求那么完美。不过这种办法还是太粗糙了,像咱们这种精致的人,肯定还得追求一些更精细的办法。

也就是说有没有什么办法能够直接抑制参数的野蛮增长呢?非常简单,你想想看参数是怎么被训练出来的。是不是通过让参数往让损失函数变小的方向不断调整,也就是梯度下降。那我们可以在损失函数中把参数本身的值加上去:

这样在参数往大了调整时,如果让损失函数减小的没有那么多,导致新的损失函数反而是变大的,那么此时调整就是不合适的,因此一定程度上就抑制了参数的野蛮增长。除了可以用参数的绝对值之和之外,我们还可以用参数的平方和。这样参数大的时候抑制的效果就更强了。我们把这一项叫做惩罚项,把通过这种向损失函数中添加权重惩罚项抑制其野蛮增长的方法叫做正则化

上面这个参数绝对值相加的叫 L1 正则化,下面这个平方项相加的叫 L2 正则化。然后和之前梯度下降时增加学习率控制下降力度一样,我们也增加一个参数来控制惩罚项的力度,我们叫它正则化系数。而这些控制参数的参数,我们以后统称为超参数:

那为什么简简单单公式叫什么 L1 正则化 和 L2 正则化呢?因为绝对值之和叫做L1范数,而平方和的平方根叫做L2范数,这是向量空间中范数的概念。光是把这些名词术语念上一遍,都比讲这个原理本身还要长,真是苦了各位学习者了。总之这个破玩意儿就只是为了抑制参数的野蛮增长罢了。

除了这种方式外,还有一种令人简单到发指,但是效果显著的办法,你猜是什么?想想看,我们的目的是为了防止让模型过于依赖某几个参数。举个形象的例子,假如神经网络中的参数是一支军队里面有好多普通士兵,但是其中混入了一支战斗力极强的闪客。然后你研究各种战术,让士兵和别人打仗。如果每次训练都有闪客主导战局,那么你会误认为整体的战斗力很强,一旦遇到特殊情况那就会败北,这就是过度依赖少量参数的典型表现。

那怎么办呢?我们可以在训练过程中每次都随机丢弃一部分参数,让闪客偶尔缺席。这样模型就必须学会依赖更多的普通士兵,从而避免了在某些关键参数上过度依赖的风险。虽然听起来有点玄学,但确实十分有效。这种方法叫dropout,其翻译过来就是丢弃。可能你此时会皱紧眉头,这都什么玩意儿,感觉怎么这么儿戏呢?对,很无厘头,但没办法就是有效。而且这个方法是大名鼎鼎的深度学习之父辛顿提出来的。

好了,上面我们了解了在对抗过拟合这条路上,我们绞尽脑汁想了各种办法,包括增加数据量,减少模型复杂度,提前终止训练,L1正则化,L2正则化、dropout等等。

那除此之外,模型还会遇到其他问题,比如说梯度消失,也就是网络越深,梯度反向传播时会越来越小,导致参数更新困难,梯度爆炸,梯度数值越来越大参的调整幅度失去了控制,收敛速度过慢,比如可能陷入局部最优或者来回震荡。计算开销过大,数据规模量太庞大了,每次完整的前向传播和反向传播都非常耗时。那每个问题人们都想了各种办法来解决,比如用梯度裁剪来防止梯度的更新过大用合理的网络结构,比如残差网络来防止深层网络的梯度衰减。用合理的权重初始化和将输入数据归一化让梯度分布更平滑。用动量法、RMSProp、Adam等自适应优化器来加速收敛,减少震荡。用mini-batch把巨量的训练数据分割成几个小批次来降低单次的计算开销。

这里的每个概念展开都是一个全新的世界,但他们和我们今天着重讲的内容一样,都是为了让训练过程更好罢了。鉴于我们这个系列是抓大放小解决主要思想的特点,这里就不一一展开。对哪个概念如果特别感兴趣的朋友,可以弹幕或者评论区告诉我,我可以单独出几期拓展视频来讲解。所以深度学习的世界其实不是我们想象的那种只要神经网络足够大,就能包打天下的局面。正是由于出现了各种各样的困境,人们才想出各种五花八门的应对策略,也让人们不禁感叹,深度学习的确像是一门玄学。

当然,随着研究的不断深入,还有更多令人惊艳的技巧层出不穷。比如卷积网络CNN如何利用卷积层、池化层处理图片数据。循环网络RNN如何利用上下文处理序列数据,以及后来的注意力机制。Attention的引入催生了transformer,并衍生出了现在的众多大语言模型,让人工智能不仅仅是识别和判断,还可以创造和决策。好,下个视频就跟我一起正式进入玄学的世界。

四. 从矩阵到CNN

一个最简单的神经网络就是 $y=wx+b$ 套上一个激活函数变成 $y=g(wx+b)$ 。

如果输入变成了两个,那么就是两个 $w$ 和两个 $x$:

$y_1 = g\left(w_1x_1 + w_2x_2 + b_1\right)$

如果输入变成了三个,那么就是三个 $w$ 和 $x$ :

$y_1 = g\left(w_1x_1 + w_2x_2 + w_3x_3 + b_1\right)$

以此类推,我就不写了。如果输出变成两个,再来一行公式就可以了。这里的 $w$ 的标号保证不一样,能区分开就行。比如说这个 $w_{12}$ 就表示第一个神经元的第二个参数:

好,你发现一个问题没有?就是这样写下去的话太麻烦了。要是神经元多了的话,公式密密麻麻的,没有数学的简洁之美。那这怎么办呢?别急,现在我们的注意力放在这个公式上,注意看啊,我要变形了。

其实就是把加减乘除替换成了矩阵运算的写法。这里先忽略一下激活函数,重点看中间这个矩阵的乘法。矩阵乘法很简单,就是这一行 $w$ 的元素分别和 $x$ 这一列的元素相乘并求和。得到的结果放到这里。同样对于第二行也是如此。

回到刚刚,我们现在把这些矩阵都替换成新的字母。分别替换为大写Y、大写W、大写X、小写b:

那么整个公式就化简成了这个样子:

$Y = g(WX + b)$

不过现在还有个问题,就是神经元的层并没有体现在公式中。那假如神经元再多几层怎么办呢?那我们此时抽象一下,也别分什么X、Y和隐藏层了,就通通用字母A来表示。

输入层就当做第零层,用 $\text{A}^{[0]}$ 来表示,以此类推。第一层的公式就是这样:

第二层的公式就是这样: $\text{A}^{[2]} = g\left( \text{W}^{[2]} \text{A}^{[1]} + \text{b}^{[2]} \right)$

第三层的公式就是这样: $\text{A}^{[3]} = g\left( \text{W}^{[3]} \text{A}^{[2]} + \text{b}^{[3]} \right)$

我们用L表示在第几层,最终的通用公式就是这个样子,每一层的神经元的值都是上一层的函数:

$\text{A}^{[L]} = g\left( \text{W}^{[L]} \text{A}^{[L-1]} + \text{b}^{[L]} \right)$

符号定义如下:

  • $\text{A}^{[L]}$ :表示第L层的激活值矩阵。矩阵中每个元素( $a_i^{[L]}$ )是第L层第i个神经元的输出(“激活值”)。矩阵维度为 “第L层神经元数 × 样本数”(若同时处理多个样本)
  • $\text{W}^{[L]}$ :表示第L层的权重矩阵。用来连接第(L-1)层和第L层的神经元,维度为 “第L层神经元数 × 第(L-1)层神经元数”。
  • $\text{b}^{[L]}$ :表示第L层的偏置向量,维度为 “第L层神经元数 × 1”,用于调整激活函数的偏移。
  • $g(\cdot)$ :表示激活函数(如 Sigmoid、ReLU 等),为神经元引入非线性,让网络能学习复杂关系。

那我们费了这么大劲儿简化这个公式有啥用呢?一方面是公式简单了也更抽象了,有利于我们进一步讨论更深的问题。另一方面是麻烦的加减乘除替换成了矩阵运算,可以充分利用GPU的并行计算的特性,加速神经网络的训练和推理过程。这就不仅仅是秀写法上的一个操作了。

回到这个公式和神经网络结构,可以看到这里的每个神经元都与前一层的所有神经元相连。当然我们一直认为这是理所应当的,但它其实只是神经网络结构中的一种,叫做全连接层。也就是说还有其他不是全连接的结构吗?

别急,我们先来看一下全连接层的问题。假如我们现在要做个图像识别的模型,假如输入是个30乘30的灰度图像,那么平铺展开后未给输入层的就是900个神经元。假如下一层的神经元的数量是1000个,那么这个全连接层的总参数量就达到了90万,这太大了。

另外这里仅仅是把输入的图片平铺展开,无法保留像素之间的空间关系。图片稍稍动一下,可能所有神经元都和原来完全不同。但从图片整体上看,可能仅仅是平移或者变暗,这就是不能很好的理解图像的局部模式。

那怎么办呢?我们随便在这个图像中取一个3乘3的矩阵,这里面的数值就是颜色的灰度值。然后我们再来一个固定的矩阵,比如这样把这两个矩阵:

我们将图像当前 3*3 的灰度值和卷积核(也是 3*3)每个对应位置的值相乘并求和,最终得到一个值是250。

$
\begin{align*}
&\text{250} = (46 \times 0) + (75 \times -1) + (82 \times 0) + \
&\quad\quad\quad (140 \times -1) + (162 \times 5) + (173 \times -1) + \
&\quad\quad\quad (169 \times 0) + (172 \times -1) + (178 \times 0) \
&= 0 - 75 + 0 - 140 + 810 - 173 + 0 - 172 + 0 \
&= (810) - (75 + 140 + 173 + 172) \
&= 810 - 560 \
&= 250
\end{align*}
$

然后我们再选取一个地方再次进行这样的运算。最终我们把这种运算方式遍历,划过原图像的每个地方,得出的数值,形成一个新的图像,那这种方式叫做卷积运算,而刚刚我们这个固定的矩阵叫做卷积核,卷积核不是一个新的概念,在传统的图像处理领域,卷积核是已知的,可以达到一定的图像处理效果。比如模糊效果、浮雕效果、轮廓效果以及刚刚的锐化效果等等,就是PS的常规操作。

那在深度学习领域,卷积核的值就是未知的,和神经网络中的其他参数一样,是被训练出来的一组值。

回到刚刚的经典神经网络结构,其实就是把其中一个全连接层替换成了卷积层:

这就大大的减少了权重参数的数量,同时还能更有效地捕捉到图片中的一些局部特征,可谓是一举两得。而从公式上看,其实就是把原来的矩阵的标准乘法及差乘替换成了卷积运算。好,那接下来我们的神经网络就不用再画成一个一个的小圈了,而用更抽象更简洁的图来表示。

像这样在图像识别的神经网络结构中,除了卷积层外,通常还有池化层,其作用是对卷积层后的特征图像进行降维,减少计算量,同时保留主要特征。池化层的工作过程就像用一个滑动窗口在特征图上扫描,并对窗口内的数值进行一个简单的聚合操作,操作步骤如下:

  • 设置窗口和步长
    • 池化窗口:通常是一个 2x2 的正方形小窗口。
    • 步长:通常设置为2,意味着窗口每次向右或向下移动2个像素。
  • 滑动并计算
    • 将这个 2x2 的窗口放在特征图的左上角。
    • 执行聚合操作(最大池化就是取这个窗口内的最大值,平均池化就是取这个窗口内的平均值)。
    • 将这个最大值作为输出特征图对应位置的像素值。

假设我们有一个 4x4 的输入特征图,使用 2x2 的窗口步长2 进行最大池化。

输入特征图:

1
2
3
4
[ [1, 3, 2, 5],
[4, 2, 7, 1],
[1, 1, 6, 3],
[2, 4, 3, 2] ]

池化过程:

  1. 第一个窗口覆盖 [ [1,3], [4,2] ]最大值是 4
  2. 向右移动,第二个窗口覆盖 [ [2,5], [7,1] ]最大值是 7
  3. 下移一行,第三个窗口覆盖 [ [1,1], [2,4] ]最大值是 4
  4. 最后一个窗口覆盖 [ [6,3], [3,2] ]最大值是 6

得到的输出特征图(2x2):

1
2
[ [4, 7],
[4, 6] ]

可以看到,4x4 的输入被压缩成了 2x2 的输出,数据量减少了75%。

通常情况下卷积层、池化层、全连接层都可以有多个。而这种适用于图像识别领域的神经网络结构就叫做卷积神经网络CNN

总结一下,CCN通常由卷积层、池化层、全连接层组成:

  • 卷积层:卷积层是CNN的核心和灵魂,它的主要作用是自动从输入数据中提取特征。使用多个可学习的过滤器(卷积核) 在输入数据上滑动,检测局部区域内的特定模式(如边缘、角点、颜色、纹理等)。每个过滤器会生成一张特征图,记录了该过滤器所代表的特征在整个输入空间中的分布和强度。
  • 池化层:池化层的主要作用是对特征图进行下采样,保留重要信息的同时减少数据量。减少特征图的空间尺寸,显著降低计算复杂度和内存消耗。通过减少参数数量,提高模型的泛化能力。
  • 全连接层:全连接层通常位于CNN的末端,负责将提取的高级特征映射到最终的输出类别