第二十三周周报
摘要
本文深入探讨了深度学习中的数学基础,特别是微积分在优化和泛化中的作用。文章首先回顾了微积分的历史和基本概念,包括逼近法、导数、微分和积分。接着,文章详细讨论了深度学习中的优化问题,包括损失函数的最小化和模型的泛化能力。文章还介绍了自动微分技术,这是深度学习框架中用于加速求导过程的关键技术。最后,文章通过一系列示例,展示了如何使用自动微分技术来计算梯度,包括非标量变量的反向传播、分离计算以及处理Python控制流的梯度计算。本文为读者提供了一个全面的深度学习数学基础概览,以及如何在实际应用中应用这些概念。
Abstract
This paper delves into the mathematical foundations of deep learning, with a particular focus on the role of calculus in optimization and generalization. We begin by revisiting the history and fundamental concepts of calculus, including the method of exhaustion, derivatives, and differential and integral calculus. The paper then discusses optimization problems in deep learning, such as minimizing loss functions and enhancing model generalization. It introduces automatic differentiation, a key technique used in deep learning frameworks to expedite the process of computing derivatives. Finally, through a series of examples, the paper demonstrates how to calculate gradients using automatic differentiation, including backpropagation for non-scalar variables, detached computation, and gradient computation involving Python control flow. The paper provides a comprehensive overview of the mathematical underpinnings of deep learning and their practical applications.
动手深度学习
1. 微积分
为了计算曲线形状(例如圆形)的面积,古希腊人采用了一种巧妙的方法:在这些形状内嵌入内接多边形。
如图所示:
随着内接多边形边数的增加,其形状越来越接近圆形。这种通过不断增加边数来逼近真实形状的方法,被称为逼近法(method of exhaustion)。
逼近法实际上奠定了积分学(integral calculus)的基础。两千多年后,微积分的另一分支——微分学(differential calculus)被发明出来。在微分学中,最重要的应用之一是解决优化问题,即如何将某项任务执行得尽可能完美。
在深度学习领域,我们通过“训练”模型来不断更新它们,使模型在处理越来越多的数据时表现得更加出色。通常情况下,所谓的“变得更好”意味着最小化一个损失函数(loss function),这个函数衡量了“我们的模型表现有多差”。最终,我们的目标是创建一个模型,它能够在未见过的数据上也能表现良好。然而,“训练”模型只能使模型适应我们实际可见的数据。因此,我们可以将模型拟合任务分解为两个关键问题:
- 优化(Optimization):使用模型拟合观测数据的过程;
- 泛化(Generalization):数学原理和实践者的智慧,它们指导我们构建出有效性超出用于训练的数据集本身的模型。
这两个问题共同构成了深度学习中模型训练和评估的核心。优化确保我们的模型能够从训练数据中学习,而泛化则确保我们的模型能够将这种学习应用到新的、未见过的数据上。
1.1 导数和微分
我们⾸先讨论导数的计算,这是⼏乎所有深度学习优化算法的关键步骤。
在深度学习中,我们通常选择对于模型参数可微的损失函数。
简⽽⾔之,对于每个参数,如果我们把这个参数增加或减少⼀个⽆穷⼩的量,我们可以知道损失会以多快的速度增加或减少。
假设我们有⼀个函数f : R → R,其输⼊和输出都是标量,如果f的导数存在,这个极限被定义为:
f ′ ( x ) = lim h → 0 f ( x + h ) − f ( x ) h f^{\prime}(x)=\lim _{h \rightarrow 0} \frac{f(x+h)-f(x)}{h} f′(x)=h→0limhf(x+h)−f(x)
如果f′(a)存在,则称f在a处是可微(differentiable)的。
如果函数 f 在某个区间内的每一点都是可微的,那么我们可以认为这个函数在整个区间上都是可微的。导数( f’(x) )可以被理解为函数 f(x) 相对于变量 x 的瞬时变化率。这个瞬时变化率是基于 x 的微小变化 h来定义的,其中h趋近于0。
定义u = f(x) = 3x2 − 4x如下:
import numpy as np
from matplotlib_inline import backend_inline
from d2l import torch as d2l
def f(x):
return 3 * x ** 2 - 4 * x
def numerical_lim(f, x, h):
return (f(x + h) - f(x)) / h
h = 0.1
for i in range(5):
print(f'h={h:.5f}, numerical limit={numerical_lim(f, 1, h):.5f}')
h *= 0.1
def use_svg_display(): #@save
"""使⽤svg格式在Jupyter中显⽰绘图"""
backend_inline.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)): #@save
"""设置matplotlib的图表⼤⼩"""
use_svg_display()
d2l.plt.rcParams['figure.figsize'] = figsize
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
"""设置matplotlib的轴"""
axes.set_xlabel(xlabel)
axes.set_ylabel(ylabel)
axes.set_xscale(xscale)
axes.set_yscale(yscale)
axes.set_xlim(xlim)
axes.set_ylim(ylim)
if legend:
axes.legend(legend)
axes.grid()
给定y = f(x),其中x和y分别是函数f的⾃变量和因变量。
以下表达式是等价的:
f
′
(
x
)
=
y
′
=
d
y
d
x
=
d
f
d
x
=
d
d
x
f
(
x
)
=
D
f
(
x
)
=
D
x
f
(
x
)
f^{\prime}(x)=y^{\prime}=\frac{d y}{d x}=\frac{d f}{d x}=\frac{d}{d x} f(x)=D f(x)=D_{x} f(x)
f′(x)=y′=dxdy=dxdf=dxdf(x)=Df(x)=Dxf(x)
其中符号
d
d
x
和
D
是微分运算符
,
表⽰微分操作
其中符号\frac{d}{d x}和D是微分运算符,表⽰微分操作
其中符号dxd和D是微分运算符,表⽰微分操作
我们可以使⽤以下规则来对常⻅函数求微分:
• DC = 0(C是⼀个常数)
• Dxn = nxn−1(幂律,n是任意实数)
• Dex = ex
• D ln(x) = 1/x
为了微分⼀个由⼀些常⻅函数组成的函数,下⾯的⼀些法则⽅便使⽤。
假设函数f和g都是可微的,C是⼀个常数则:
现在,我们可以应用上述几个法则来计算导数 u’ = f’(x) 。
具体来说,我们有:
f
′
(
x
)
=
3
d
x
−
4
d
x
2
=
6
x
−
4
f'(x) = \frac{3}{dx} - \frac{4}{dx^2} = 6x - 4
f′(x)=dx3−dx24=6x−4
当 x = 1 时,我们得到 u’ = 2 。
这个数值结果接近于2,这与我们在本节前面实验中得到的结果相吻合。
在 x = 1 时,这个导数值也代表了曲线 u = f(x) 的切线斜率。
为了对导数的这种解释进行可视化,我们将使用 matplotlib,这是一个在 Python 中广泛使用的绘图库。
为了配置 matplotlib 生成图形的属性,我们需要定义几个函数。
下面,use_svg_display
函数指定 matplotlib 软件包输出 SVG 图表,以获得更清晰的图像。
请注意,注释 #@save
是一个特殊的标记,它会将对应的函数、类或语句保存在 d2l
包中。
因此,以后无需重新定义就可以直接调用它们(例如,d2l.use_svg_display()
)。
# 导入numpy库用于数学运算,导入matplotlib.pyplot用于绘图
import numpy as np
import matplotlib.pyplot as plt
# 定义 f(x) 函数,根据给定的数学表达式 3x^2 - 4x
def f(x):
return 3 * x ** 2 - 4 * x
# 设置matplotlib的图表大小
def set_figsize(figsize=(3.5, 2.5)):
"""设置matplotlib的图表大小"""
plt.rcParams['figure.figsize'] = figsize
# 设置matplotlib的轴属性
def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
"""设置matplotlib的轴属性"""
axes.set_xlabel(xlabel) # 设置x轴标签
axes.set_ylabel(ylabel) # 设置y轴标签
axes.set_xscale(xscale) # 设置x轴的刻度类型
axes.set_yscale(yscale) # 设置y轴的刻度类型
axes.set_xlim(xlim) # 设置x轴的范围
axes.set_ylim(ylim) # 设置y轴的范围
if legend: # 如果有图例
axes.legend(legend) # 显示图例
axes.grid() # 显示网格
# 绘图函数,用于绘制多个数据系列
def plot(X, Y=None, xlabel=None, ylabel=None, legend=None, xlim=None,
ylim=None, xscale='linear', yscale='linear',
fmts=('-', 'm--', 'g-.', 'r:'), figsize=(3.5, 2.5), axes=None):
"""绘制图形,并设置图表和轴的属性"""
if legend is None: # 如果没有图例,则设置为空列表
legend = []
set_figsize(figsize) # 设置图表大小
axes = axes if axes else plt.gca() # 如果没有指定axes,则获取当前的axes
if Y is None: # 如果Y没有被指定,则设置Y为X的列表
Y = [X]
elif isinstance(Y, (list, tuple)) and len(Y) == 1: # 如果Y是单元素列表或元组
Y = Y * len(X) # 复制Y以匹配X的长度
elif not isinstance(Y, (list, tuple)): # 如果Y不是列表或元组
Y = [Y] # 将Y包装成列表
if len(X) != len(Y[0]): # 如果X和Y的长度不匹配
X = [X] * len(Y[0]) # 复制X以匹配Y的长度
axes.cla() # 清除当前的axes
for x, y, fmt in zip(X, Y, fmts): # 遍历X, Y和fmts
if isinstance(x, np.ndarray) and len(x): # 如果x是numpy数组且非空
axes.plot(x, y, fmt) # 绘制x和y
else: # 否则
axes.plot(y, fmt) # 绘制y
set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend) # 设置轴的属性
# 主程序入口
if __name__ == "__main__":
# 定义 x 的范围,从 0 到 3,步长为 0.1
x = np.arange(0, 3, 0.1)
# 计算 f(x) 和切线对应的 y 值
f_x = f(x)
tangent_line = 2 * x - 3 # 切线方程为 y = 2x - 3
# 绘制 f(x) 和切线,设置图例和轴标签
plot(x, [f_x, tangent_line], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])
# 显示图形
plt.show()
1.2 偏导数
到目前为止,我们仅讨论了只涉及单一变量的函数的微分。
然而,在深度学习领域,函数往往依赖于多个变量。
因此,我们需要将微分的概念扩展到多元函数上。
设
y = f ( x 1 , x 2 , … , x n ) 是一个具有 n 个变量的函数。 y = f(x_1, x_2, \ldots, x_n)是一个具有 n 个变量的函数。 y=f(x1,x2,…,xn)是一个具有n个变量的函数。
y 关于第 i 个变量 xi的偏导数定义为:
∂
y
∂
x
i
=
lim
h
→
0
f
(
x
1
,
…
,
x
i
−
1
,
x
i
+
h
,
x
i
+
1
,
…
,
x
n
)
−
f
(
x
1
,
…
,
x
i
,
…
,
x
n
)
h
\frac{\partial y}{\partial x_i} = \lim_{h \to 0} \frac{f(x_1, \ldots, x_{i-1}, x_i + h, x_{i+1}, \ldots, x_n) - f(x_1, \ldots, x_i, \ldots, x_n)}{h}
∂xi∂y=h→0limhf(x1,…,xi−1,xi+h,xi+1,…,xn)−f(x1,…,xi,…,xn)
为了计算 ∂ x i ∂ y \frac{\partial x_i}{\partial y} ∂y∂xi我们可以将 x 1 , … , x i − 1 , x i + 1 , … , x n x_1, \ldots, x_{i-1}, x_{i+1}, \ldots, x_n x1,…,xi−1,xi+1,…,xn视为常数,并计算 y 关于 xi的导数。
对于偏导数的表示,以下是等价的:
∂ y ∂ x i = ∂ f ∂ x i = f x i = f i = D i f = D x i f \frac{\partial y}{\partial x_i} = \frac{\partial f}{\partial x_i} = f_{x_i} = f^i = D_i f = D_{x_i} f ∂xi∂y=∂xi∂f=fxi=fi=Dif=Dxif
1.3 梯度
我们可以将多元函数对所有变量的偏导数组合起来,形成一个梯度向量。
具体来说,设函数
f
:
R
n
→
R
的输入是一个
n
维向量
具体来说,设函数 f: \mathbb{R}^n \to \mathbb{R} 的输入是一个 n 维向量
具体来说,设函数f:Rn→R的输入是一个n维向量
x
=
[
x
1
,
x
2
,
…
,
x
n
]
T
,并且输出是一个标量。
\mathbf{x} = [x_1, x_2, \ldots, x_n]^T ,并且输出是一个标量。
x=[x1,x2,…,xn]T,并且输出是一个标量。
函数
x
相对于
x
的梯度是一个包含
n
个偏导数的向量:
函数 \mathbf{x} 相对于\mathbf{x} 的梯度是一个包含 n 个偏导数的向量:
函数x相对于x的梯度是一个包含n个偏导数的向量:
∇ x f ( x ) = [ ∂ f ( x ) ∂ x 1 , ∂ f ( x ) ∂ x 2 , … , ∂ f ( x ) ∂ x n ] T \nabla_{\mathbf{x}} f(\mathbf{x}) = \left[ \frac{\partial f(\mathbf{x})}{\partial x_1}, \frac{\partial f(\mathbf{x})}{\partial x_2}, \ldots, \frac{\partial f(\mathbf{x})}{\partial x_n} \right]^T ∇xf(x)=[∂x1∂f(x),∂x2∂f(x),…,∂xn∂f(x)]T
在没有歧义的情况下,
∇
x
f
(
x
)
通常被简化为
∇
f
(
x
)
\nabla_{\mathbf{x}} f(\mathbf{x}) 通常被简化为 \nabla f(\mathbf{x})
∇xf(x)通常被简化为∇f(x)
假设 x是一个 n 维向量,在微分多元函数时常使用以下规则:
对于所有
A
∈
R
m
×
n
,都有
∇
x
A
x
=
A
T
。
对于所有 A \in \mathbb{R}^{m \times n} ,都有 \nabla_{\mathbf{x}} A\mathbf{x} = A^T 。
对于所有A∈Rm×n,都有∇xAx=AT。
对于所有
A
∈
R
n
×
m
,都有
∇
x
x
T
A
=
A
。
对于所有 A \in \mathbb{R}^{n \times m} ,都有 \nabla_{\mathbf{x}} \mathbf{x}^T A = A 。
对于所有A∈Rn×m,都有∇xxTA=A。
对于所有
A
∈
R
n
×
n
,都有
∇
x
x
T
A
x
=
(
A
+
A
T
)
x
。
对于所有 A \in \mathbb{R}^{n \times n} ,都有 \nabla_{\mathbf{x}} \mathbf{x}^T A \mathbf{x} = (A + A^T)\mathbf{x} 。
对于所有A∈Rn×n,都有∇xxTAx=(A+AT)x。
∇
x
∥
x
∥
2
=
∇
x
x
T
x
=
2
x
。同样,对于任何矩阵
X
,都有
∇
X
∥
X
∥
F
2
=
2
X
。
\nabla_{\mathbf{x}} \|\mathbf{x}\|^2 = \nabla_{\mathbf{x}} \mathbf{x}^T \mathbf{x} = 2\mathbf{x} 。同样,对于任何矩阵 X ,都有 \nabla_X \|X\|_F^2 = 2X 。
∇x∥x∥2=∇xxTx=2x。同样,对于任何矩阵X,都有∇X∥X∥F2=2X。
1.4 链式法则
1.5 小结
-
微分和积分构成了微积分的两大支柱,其中微分在深度学习中的优化问题有着重要的应用。
-
导数可以被理解为函数相对于其变量的瞬时变化率,它同样表示函数曲线在特定点处切线的斜率。
-
梯度是一个向量,其分量包含了多变量函数对其所有变量的偏导数。
-
链式法则使我们能够对复合函数进行微分。
2. 自动微分
深度学习框架通过自动计算导数,即自动微分技术,来加速求导过程。在实际应用中,根据我们设计的模型,系统会构建一个计算图,用以追踪数据通过哪些操作组合产生输出的路径。自动微分使得系统能够随后进行梯度的反向传播。在这里,反向传播指的是在整个计算图中追踪,并计算每个参数的偏导数,从而填充这些参数的梯度信息。
作为⼀个演⽰例⼦,假设我们想对函数y = 2x⊤x关于列向量x求导。⾸先,我们创建变量x并为其分配⼀个初始值
import torch
x = torch.arange(4.0)
x
x.requires_grad_(True) # 等价于x=torch.arange(4.0,requires_grad=True)
x.grad # 默认值是None
y = 2 * torch.dot(x, x)
y
y.backward()
x.grad
x.grad == 4 * x
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
2.1 非标量变量的反向传播
当变量
y不是单个数值(即非标量)时,向量 y 关于向量 x 的导数最直观的解释是一个矩阵。对于更高阶和更高维度的 y 和 x,求导的结果可能是一个高阶张量。
尽管这些更复杂的数学对象确实在高级机器学习领域(包括深度学习)中出现,但在我们执行反向传播计算时,我们通常的目标是计算一批训练样本中每个组成部分的损失函数的导数。在这种情况下,我们的目的并不是计算整个微分矩阵,而是分别计算批量中每个样本的偏导数,并求和。
# 对⾮标量调⽤backward需要传⼊⼀个gradient参数,该参数指定微分函数关于self的梯度。
# 在我们的例⼦中,我们只想求偏导数的和,所以传递⼀个1的梯度是合适的
x.grad.zero_()
y = x * x
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
2.2 分离计算
有时,我们可能需要将特定的计算步骤从记录的计算图中分离出来。
例如,假设 y 是根据 x 的函数计算得到的,而 z 则是基于 y 和 x 的函数计算的。设想一下,我们想要计算 z 关于 x 的梯度,但由于某些原因,我们希望在计算过程中将 y视为一个常数,仅考虑 x 在 y 计算完成后的影响。
在这种情况下,我们可以将 y 分离出来,创建一个新的变量 u,这个变量 u 与 y 拥有相同的值,但是不保留计算图中关于 y 是如何计算的任何信息。换句话说,梯度不会通过 u 反向传播到 x。因此,下面的反向传播函数计算 z=u⋅x 关于 x的偏导数时,将 u 视为常数,而不是计算 z=x⋅x⋅x 关于 x 的偏导数。
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
由于记录了y的计算结果,我们可以随后在y上调⽤反向传播,得到y=xx关于的x的导数,即2x。
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
2.3 Python控制流的梯度计算
使⽤⾃动微分的⼀个好处是:即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调⽤),我们仍然可以计算得到的变量的梯度。在下⾯的代码中,while循环的迭代次数和if语句的结果都取决于输⼊a的值。
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
计算梯度
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
可以分析上⾯定义的f函数。请注意,它在其输⼊a中是分段线性的。换⾔之,对于任何a,存在某个常量标量k,使得f(a)=k*a,其中k的值取决于输⼊a。因此,我们可以⽤d/a验证梯度是否正确。
a.grad == d / a
import torch
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
print(a.grad == d / a)
2.4 小结
深度学习框架可以⾃动计算导数:我们⾸先将梯度附加到想要对其计算偏导数的变量上。
然后我们记
录⽬标值的计算,执⾏它的反向传播函数,并访问得到的梯度。
总结
本次周报,我们全面探讨了深度学习中的数学基础,特别是微积分在优化和泛化中的核心作用。从微积分的历史和基本概念入手,解释了如何使用逼近法来计算复杂形状的面积,以及导数和微分在优化问题中的应用。并强调了损失函数最小化和模型泛化的重要性,并介绍了自动微分技术,这是深度学习框架中用于高效计算梯度的关键工具。通过一系列实例,我们展示了如何计算非标量变量的梯度、如何从计算图中分离特定步骤,以及如何处理涉及Python控制流的梯度计算。这些技术不仅加深了我们对深度学习数学基础的理解,也为实际应用中的模型训练和评估提供了实用的工具。通过本文的讨论,读者应该能够更好地把握深度学习中数学概念的应用,并在实际问题中有效地利用这些工具。
下一周计划学概论知识