这篇文章确实需要一定的数学基础,第一次接触的小伙伴可以先看一下我示例演绎这个主题的前两篇文章:
示例演绎机器学习中(深度学习)神经网络的数学基础——快速理解核心概念(一):
政安晨:示例演绎机器学习中(深度学习)神经网络的数学基础——快速理解核心概念(一){两篇文章讲清楚}https://blog.csdn.net/snowdenkeke/article/details/136089968示例演绎机器学习中(深度学习)神经网络的数学基础——快速理解核心概念(二):
政安晨:示例演绎机器学习中(深度学习)神经网络的数学基础——快速理解核心概念(二){两篇文章讲清楚}https://blog.csdn.net/snowdenkeke/article/details/136090846
简述概念
在机器学习中,神经网络是一种常用的模型,用于解决各种问题,如图像识别、自然语言处理等。神经网络通过一系列的神经元(也称为节点)组成的层次结构来模拟人脑的神经系统。
神经网络的训练过程可以看作是一个优化问题,目标是找到最优的参数来最小化损失函数。为了实现这个目标,梯度和导数是非常重要的概念。
梯度是损失函数对于参数的导数。在神经网络中,我们使用梯度来更新参数,以使损失函数最小化。通过计算损失函数对于每个参数的梯度,我们可以确定应该如何调整参数的值,以逐渐减少损失。
导数是函数在某一点的斜率或变化率。在神经网络中,我们将损失函数视为参数的函数,并计算其在参数点处的导数。这告诉我们在该点上损失函数是如何随参数的变化而变化的。
通过梯度和导数,我们可以了解损失函数如何随参数的变化而变化,并根据这些信息来调整参数的值,以改善模型的性能。梯度下降是一种常用的优化算法,它使用梯度的信息来逐步调整参数的值,以最小化损失函数。
总结起来,神经网络中的梯度和导数是用于优化模型的重要概念。它们提供了关于损失函数随参数变化的信息,帮助我们调整参数的值以改善模型性能。
基于梯度的优化
如同咱们前面文章提到的,对于咱们前面提到的深度学习中的模型而言,每个神经网络层都对输入数据进行如下变换:
output = relu(dot(input, W) + b)
这里,W和b是张量,均为该层的属性。它们被称为该层的权重(weight)或可训练参数(trainable parameter),分别对应属性kernel和bias。这些权重包含模型从训练数据中学到的信息。
咱们先大概了解一下:
在机器学习中,神经网络的kernel(或称为kernel function)指的是一种用于计算两个数据样本之间相似度的函数。这个函数会将输入的数据样本映射到一个高维特征空间,并在该特征空间中计算两个样本之间的相似度或距离。通过定义适当的kernel函数,可以处理非线性问题,并且可以提高神经网络的表达能力和预测性能。常见的kernel函数有线性核函数、多项式核函数、高斯核函数等。这些kernel函数可以根据具体的问题和数据特征选择和定义。在神经网络中,kernel函数通常用于支持向量机(SVM)和卷积神经网络(CNN)等模型中,用于计算样本之间的相似度。
同时,神经网络的bias(偏差)指的是模型在学习过程中对目标函数的预测的偏离程度。神经网络的bias项是模型的一个可学习参数,用于调整模型的输出值。它通常被添加到神经网络的每一层的神经元上,以调整模型的预测结果与真实值之间的偏差。bias可以理解为模型的预测的基准线,它的值决定了模型对于不同类型数据的初始偏好。通过调整bias的值,可以使模型更加适应不同类型的数据分布。
一开始,这些权重矩阵取较小的随机值,这一步叫作随机初始化(random initialization)。
当然,W和b都是随机的,relu(dot(input, W) + b)不会得到任何有用的表示。虽然得到的表示没有意义,但这是一个起点。下一步则是根据反馈信号逐步调节这些权重。这个逐步调节的过程叫作训练(training),也就是机器学习中的“学习”过程。
上述过程发生在一个训练循环(training loop)内,其具体流程如下:(在一个循环中重复下列步骤,直到损失值变得足够小)
1) 抽取训练样本x和对应目标y_true组成的一个数据批量。
2) 在x上运行模型(这一步叫作前向传播(forward pass)),得到预测值y_pred。
3) 计算模型在这批数据上的损失值,用于衡量y_pred和y_true之间的差距。
4) 更新模型的所有权重,以略微减小模型在这批数据上的损失值。
最终得到的模型在训练数据上的损失值非常小,即预测值y_pred与预期目标y_true之间的差距非常小,模型已“学会”将输入映射到正确的目标,虽然看起来很复杂,但如果您将其简化为基本步骤,那么会发现它变得非常简单。
第1步看起来很简单,只是输入/输出(I/O)的代码。
第2步和第3步仅仅是应用了一些张量运算,所以你完全可以利用咱们在前两篇文章里演绎的知识来实现这两步。
难点在于第4步:更新模型的权重。对于模型的某个权重系数,你怎么知道这个系数应该增大还是减小,以及变化多少呢?
一种简单的解决方案是:
保持模型的其他权重不变,只考虑某一个标量系数,让其尝试不同的取值。
假设这个系数的初始值为0.3。
对一批数据做完前向传播后,模型在这批数据上的损失值是0.5。
如果将这个系数改为0.35并重新运行前向传播,那么损失值增大为0.6。
但如果将这个系数减小到0.25,那么损失值减小为0.4。
在这个例子中,将系数减小0.05似乎有助于让损失值最小化。
对于模型的所有系数都要重复这一过程。
但上述这种方法非常低效,因为系数有很多(通常有上千个,有时甚至多达上百万个),对每个系数都要计算两次前向传播,计算代价很大。其实,有一种更好的方法:梯度下降法(gradient descent)。
梯度下降是驱动现代神经网络的优化方法,其要点如下:
我们的模型用到的所有函数(比如dot或+),都以一种平滑、连续的方式对输入进行变换。
举个例子,对于z = x + y,y的微小变化只会导致z的微小变化,如果你知道y的变化方向,就可以推断出z的变化方向。
用数学语言来讲,这些函数是可微(differentiable)的,将这样的函数组合在一起,得到的函数仍然是可微的,尤其是,将模型系数映射到模型在数据批量上损失值的函数,也是可微的:模型系数的微小变化,将导致损失值发生可预测的微小变化。
我们可以用一个叫作梯度(gradient)的数学运算符来描述:模型系数向不同方向移动时,损失值如何变化,计算出梯度,就可以利用它来更新系数,使损失值减小(在一次更新中全部完成,而不是一次更新一个系数)。
咱们接下来,更进一步来解释这个概念:
导数
假设有一个光滑的连续函数f(x) = y,将一个数字x映射到另一个数字y。我们以下图所示的函数为例:
由于函数是连续的,因此x的微小变化只会导致y的微小变化——这就是函数连续性(continuity)的直观解释。假设x增加了一个很小的因子epsilon_x,这导致y发生了很小的变化epsilon_y,如下图所示:
此外,由于函数是光滑的(意思是,函数曲线没有任何突变的角度),因此在某个点p附近:
如果epsilon_x足够小,就可以将f近似地看作斜率为a的线性函数:
这样epsilon_y就等于a * epsilon_x。
f(x + epsilon_x) = y + a * epsilon_x
显然,只有在x足够接近p时,这个线性近似才有效。
斜率a被称为f在p点的导数(derivative)。
·如果a为负,那么说明x在p点附近的微增将导致f(x)减小(如下图所示);
·如果a为正,那么x的微增将导致f(x)增大。
·此外,a的绝对值(导数大小)表示这种增大或减小的速度。
对于每个可微函数f(x)(可微的意思是“可以被求导”,比如光滑的连续函数可以被求导),都存在一个导数函数f'(x),将x的值映射为f在该点局部线性近似的斜率。
例如,cos(x)的导数是-sin(x),f(x) = a * x的导数是f'(x) =a。
优化的目的就是找到使f(x)最小化的x值,就此而言,函数求导是一个非常强大的工具。如果你想将x改变一个很小的因子epsilon_x,目的是将f(x)最小化,并且你知道f的导数,那么问题已经解决了:导数描述的就是,改变x后f(x)会如何变化。如果你想减小f(x)的值,那么只需将x沿着导数的反方向移动一小步。
张量运算的导数:梯度
上文提到的函数f是将一个标量值x映射为另一个标量值y,你可以将函数绘制为二维平面上的一条曲线。
现在想象有一个函数,将标量元组(x, y)映射为一个标量值z,那么这是一个向量运算。你可以将它绘制为三维空间(以x、y、z为坐标轴)中的二维表面(surface)。同样,你还可以想象以矩阵为输入的函数、以3阶张量为输入的函数,等等。
导数这一概念可以应用于任意函数,只要函数所对应的表面是连续且光滑的。张量运算(或张量函数)的导数叫作梯度(gradient)。梯度就是将导数这一概念推广到以张量为输入的函数。还记不记得,对于一个标量函数来说,导数是如何表示函数曲线的局部斜率(local slope)的?同样,张量函数的梯度表示该函数所对应多维表面的曲率(curvature)。它表示的是,当输入参数发生变化时,函数输出如何变化。
咱们再举例。
假设我们有:
一个输入向量x(数据集中的一个样本);
一个矩阵W(模型权重);
一个目标值y_true(模型应该学到的与x相关的结果);
一个损失函数loss(用于衡量模型当前预测值与y_true之间的差距)。
你可以用W来计算预测值y_pred,然后计算损失值,即预测值y_pred与目标值y_true之间的差距。
# 利用模型权重W对x进行预测
y_pred = dot(W, x)
# 估算预测值的偏差有多大
loss_value = loss(y_pred, y_true)
现在我们想利用梯度来更新W,以使loss_value变小。
如何做到这一点呢?
如果输入数据x和y_true保持不变,那么可以将前面的运算看作一个将模型权重W的值映射到损失值的函数。
# f描述的是:当W变化时,损失值所形成的曲线(或高维表面)
loss_value = f(W)
假设W的当前值为W0,f在W0点的导数是一个张量grad(loss_value, W0),其形状与W相同,每个元素grad(loss_value, W0)[i, j]表示当W0[i, j]发生变化时loss_value变化的方向和大小。
张量grad(loss_value, W0)是函数f(W) = loss_value在W0处的梯度,也叫作“loss_value相对于W在W0附近的梯度”。
偏导数
张量运算grad(f(W), W)以矩阵W为输入,它可以表示为标量函数grad_ij(f(W), w_ij)的组合,每个标量函数返回的是,loss_value =f(W)相对于W[i, j]的导数(假设W的其他所有元素都不变)。grad_ij叫作f相对于W[i, j]的偏导数(partial derivative)。
grad(loss_value, W0)具体代表什么呢?
我们在前面看到,单变量函数f(x)的导数可以看作函数f曲线的斜率。同样,grad(loss_value, W0)可以看作表示loss_value = f(W)在W0附近最陡上升方向的张量,也表示这一上升方向的斜率。每个偏导数表示f在某个方向上的斜率。
对于一个函数f(x),你可以通过将x沿着导数的反方向移动一小步来减小f(x)的值。同样,对于一个张量函数f(W),你也可以通过将W沿着梯度的反方向移动来减小loss_value = f(W)。
比如W1 = W0 - step * grad(f(W0), W0),其中step是一个很小的比例因子。也就是说,沿着f最陡上升的反方向移动,直观上看可以移动到曲线上更低的位置。注意,比例因子step是必需的,因为grad(loss_value, W0)只是W0附近曲率的近似值,所以不能离W0太远。
随机梯度下降
给定一个可微函数,理论上可以用解析法找到它的最小值:函数的最小值就是导数为0的点,因此只需找到所有导数为0的点,然后比较函数在其中哪个点的取值最小。
将这一方法应用于神经网络,就是用解析法求出损失函数最小值对应的所有权重值。可以通过对方程grad(f(W), W) = 0求解W来实现这一方法。这是一个包含N个变量的多项式方程,其中N是模型的系数个数。当N = 2或N = 3时,可以对这样的方程进行求解,但对于实际的神经网络是无法求解的,因为参数的个数不会少于几千个,而且经常有上千万个。
不过你可以使用刚才开头总结的算法:基于当前在随机数据批量上的损失值,一点一点地对参数进行调节。
我们要处理的是一个可微函数,所以可以计算出它的梯度,从而有效地实现第4步,沿着梯度的反方向更新权重,每次损失值都会减小一点:
(刚才咱们提到的步骤)
(1)抽取训练样本x和对应目标y_true组成的一个数据批量。
(2)在x上运行模型,得到预测值y_pred。这一步叫作前向传播。
(3)计算模型在这批数据上的损失值,用于衡量y_pred和y_true之间的差距。
(4)计算损失相对于模型参数的梯度。这一步叫作反向传播(backward pass)。
(5)将参数沿着梯度的反方向移动一小步,比如W -= learning_rate *gradient,从而使这批数据上的损失值减小一些。学习率(learning_rate)是一个调节梯度下降“速度”的标量因子。
我们刚刚介绍的方法叫作小批量随机梯度下降(mini-batch stochastic gradient descent,简称小批量SGD)。
接下里,咱们用下图给出了一维的示例,模型只有一个参数,并且只有一个训练样本:
如你所见,直观上来看,learning_rate因子的取值很重要:
如果取值太小,那么沿着曲线下降需要很多次迭代,而且可能会陷入局部极小点。
如果取值过大,那么更新权重值之后可能会出现在曲线上完全随机的位置。
注意,小批量SGD算法的一个变体是每次迭代只抽取一个样本和目标,而不是抽取一批数据。这叫作真SGD(true SGD,有别于小批量SGD)。
还可以走向另一个极端:每次迭代都在所有数据上运行,这叫作批量梯度下降(batch gradient descent)。
这样做的话,每次更新权重都会更加准确,但计算成本也高得多。这两个极端之间有效的折中方法则是选择合理的小批量大小。
下图展示的是一维参数空间中的梯度下降,但在实践中需要在高维空间中使用梯度下降。
神经网络的每一个权重系数都是空间中的一个自由维度,神经网络则可能包含数万个甚至上百万个参数。为了对损失表面有更直观的认识,你还可以将沿着二维损失表面的梯度下降可视化,如下图所示:
(但你不可能将神经网络的真实训练过程可视化,因为无法用人类可以理解的方式来可视化1 000 000维空间。因此最好记住,在这些低维表示中建立的直觉,实践中不一定总是准确的。这一直是深度学习研究的问题来源。)
此外,SGD还有多种变体,比如带动量的SGD、Adagrad、RMSprop等。
它们的不同之处在于,计算下一次权重更新时还要考虑上一次权重更新,而不是仅考虑当前的梯度值,这些变体被称为优化方法(optimization method)或优化器(optimizer)。动量的概念尤其值得关注,它被用于许多变体,动量解决了SGD的两个问题:收敛速度和局部极小值。
下图给出了损失作为模型参数的函数的曲线:
在某个参数值附近,有一个局部极小点(local minimum):在这个点附近,向左和向右移动都会导致损失值增大。
如果使用学习率较小的SGD对参数进行优化,那么优化过程可能会陷入局部极小点,而无法找到全局极小点。
使用动量方法可以避免这样的问题,这一方法的灵感来源于物理学:一个有用的思维模型是将优化过程想象成小球从损失函数曲线上滚下来,如果小球的动量足够大,那么它不会卡在峡谷里,最终会到达全局极小点。动量方法的实现过程是,每一步移动小球,不仅要考虑当前的斜率值(当前的加速度),还要考虑当前的速度(由之前的加速度产生)。
这在实践中的含义是:更新参数w不仅要考虑当前梯度值,还要考虑上一次参数更新,其简单实现如下:
past_velocity = 0.
# 不变的动量因子
momentum = 0.1
# 优化循环
while loss > 0.01:
w, loss, gradient = get_current_parameters()
velocity = past_velocity * momentum - learning_rate * gradient
w = w + momentum * velocity - learning_rate * gradient
past_velocity = velocity
update_parameter(w)
链式求导:反向传播算法
在前面的算法中,我们假设函数是可微的,所以很容易计算其梯度。
但这种假设是合理的吗?
我们在实践中如何计算复杂表达式的梯度?
对于咱们开头说到的双层模型,我们如何计算出损失相对于权重的梯度?这时就需要用到反向传播算法(backpropagation algorithm)。
链式法则
反向传播是这样一种方法:利用简单运算(如加法、relu或张量积)的导数,可以轻松计算出这些基本运算的任意复杂组合的梯度。重要的是,神经网络由许多链接在一起的张量运算组成,每个张量运算的导数都是已知的,且都很简单。
例如:
咱们上文给的代码,定义的模型可以表示为,一个由变量W1、b1、W2和b2(分别属于第1个和第2个Dense层)参数化的函数,其中用到的基本运算是dot、relu、softmax和+,以及损失函数loss,这些运算都是很容易求导的。
loss_value = loss(y_true, softmax(dot(relu(dot(inputs, W1) + b1), W2) + b2))
根据微积分的知识,这种函数链可以利用下面这个恒等式进行求导,它叫作链式法则(chain rule)。
考虑两个函数f和g,以及它们的复合函数fg:fg(x) == f(g(x))。
def fg(x):
x1 = g(x)
y = f(x1)
return y
链式法则规定:grad(y, x) == grad(y, x1) * grad(x1, x)。因此,只要知道f和g的导数,就可以求出fg的导数,如果添加更多的中间函数,看起来就像是一条链,因此得名链式法则。
def fghj(x):
x1 = j(x)
x2 = h(x1)
x3 = g(x2)
y = f(x3)
return y
grad(y, x) == (grad(y, x3) * grad(x3, x2) * grad(x2, x1) * grad(x1, x))
将链式法则应用于神经网络梯度值的计算,就得到了一种叫作反向传播的算法。
咱们来具体看一下它的工作原理:
用计算图进行自动微分
思考反向传播的一种有用方法是利用计算图(compu- tation graph)。计算图是TensorFlow和深度学习革命的核心数据结构。它是一种由运算(比如我们用到的张量运算)构成的有向无环图。
计算图是计算机科学中一个非常成功的抽象概念。
有了计算图,我们可以将计算看作数据:将可计算的表达式编码为机器可读的数据结构,然后用于另一个程序的输入或输出。
例如,你可以想象这样一个程序:接收一个计算图作为输入,并返回一个新的计算图,新计算图可实现相同计算的大规模分布式版本。这意味着你可以对任意计算实现分布式,而无须自己编写分布式逻辑。或者想象这样一个程序:接收一个计算图作为输入,然后自动计算它所对应表达式的导数。如果将计算表示为一个明确的图数据结构,而不是.py文件中的几行ASCII字符,那么做这些事情就容易多了。
如今,人们利用能够自动微分的现代框架来实现神经网络,比如TensorFlow。自动微分是利用刚才咱们所述的计算图来实现的。自动微分可以计算任意可微张量运算组合的梯度,只需要写出前向传播,而无须做任何额外工作。
ensorFlow的梯度带GradientTape是一个API,让你可以充分利用TensorFlow强大的自动微分能力。它是一个Python作用域(scope),能够以计算图[有时也被称为“条带”(tape)]的形式“记录”在其中运行的张量运算。计算图可用来获取任意输出相对于任意变量或变量集的梯度,这些变量或变量集都是tf.Variable类的实例,tf.Variable是一类用于保存可变状态的张量,比如神经网络的权重就是tf.Variable的实例。
import tensorflow as tf
# 将标量Variable的值初始化为0
x = tf.Variable(0.)
#创建一个GradientTape作用域
with tf.GradientTape() as tape:
# 在作用域内,对变量做一些张量运算
y = 2 * x + 3
# 利用梯度带获取输出y相对于变量x的梯度
grad_of_y_wrt_x = tape.gradient(y, x)
GradientTape也可用于张量运算:
# 将Variable初始化为形状为(2, 2)的零张量
x = tf.Variable(tf.zeros((2, 2)))
with tf.GradientTape() as tape:
y = 2 * x + 3
# grad_of_y_wrt_x是一个形状为(2, 2)的张量(形状与x相同),表示y = 2 * a + 3在x = [[0, 0], [0, 0]]附近的曲率
grad_of_y_wrt_x = tape.gradient(y, x)
它还适用于变量列表:
W = tf.Variable(tf.random.uniform((2, 2)))
b = tf.Variable(tf.zeros((2,)))
x = tf.random.uniform((2, 2))
with tf.GradientTape() as tape:
# 在TensorFlow中,matmul是指点积
y = tf.matmul(x, W) + b
# grad_of_y_wrt_W_and_b是由两个张量组成的列表,这两个张量的形状分别与W和b相同
grad_of_y_wrt_W_and_b = tape.gradient(y, [W, b])
小结论
模型由许多层链接在一起组成,并将输入数据映射为预测值。随后,损失函数将这些预测值与目标值进行比较,得到一个损失值,用于衡量模型预测值与预期结果之间的匹配程度。优化器将利用这个损失值来更新模型权重。
这是那个比较清晰的神经网络、层、损失函数与优化器之间的关系。