动手学深度学习1--预备知识

学习内容

  • 深度学习基础——线性神经网络,多层感知机
  • 卷积神经网络——LeNet,AlexNet,VGG,Inception,ResNet
  • 循环神经网络——RNN,GRU,LSTM,seq2seq
  • 注意力机制——Attention,Transformer
  • 优化算法——SGD,Momentum,Adam
  • 高性能计算——并行,多GPU,分布式
  • 计算机视觉——目标检测,语义分割
  • 自然语言处理——词嵌入,BERT

数据操作 + 预处理

N 维数组是机器学习和神经网络的主要数据结构。

  • 0-d(标量)——1.0
    • 表示一个类别
  • 1-d(向量)——[1.0, 2.7, 3.4]
    • 一个特征向量
  • 2-d(矩阵)——[[1.0,2.7,3.4], [5.0,0.2,4.6], [4.3,8.5,0.2]]
    • 一个样本的特征矩阵,每一行表示一个样本,每一列表示样本的一个特征
  • 3-d ——[[[0.1,2.7,3.4], [5.0,0.2,4.6], [4.3,8.5,(0.2]], [[3.2,5.7,3.4], [5.4,6.2,3.2], [4.1,3.5,6.2]]]
    • RGB图片(宽 x 高 x 通道)
    • 宽是列的个数(有多少列),高是行的个数(有多少行),通道channel包括RGB三通道
  • 4-d —— [[[[....]]]]
    • 一个RGB图片批量(批量大小 x 宽 x 高 x 通道)
    • 批量大小:batch_size
  • 5-d ——[[[[[....]]]]]
    • 一个视频批量(批量大小 x 时间 x 宽 x 高 x 通道)

数组操作

操作原理

创建数组需要:

  • 形状:例如 3 x 4 矩阵
  • 每个元素的数据类型:例如32位浮点数
  • 每个元素的值,例如全是0,或者随机数

访问元素:image-20250621151410304

image-20250621151422645

创建数组

导入torch

1
import torch

张量表示一个由数值组成的数组,这个数组可能有多个维度

1
2
x = torch.arange(12)
x

tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

可以通过张量的shape属性来访问张量(沿每个轴的长度)的形状和张量中元素的总数

1
x.shape

torch.Size([12]) # 只有一个维度,这个维度的长度是12

1
x.numel()	# x中元素的种类数

12 # 结果为一个标量数据值

要想改变一个张量的形状而不改变元素数量和元素值,可以调用reshape函数

1
2
X = x.reshape(3, 4)	# 改为3行4列赋值给X
X

tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]])

使用全0、全1、其他常量,或者从特定分布中随机采样的数字

1
2
3
4
5
torch.zeros((2, 3, 4))	# 生成全0元素

torch.ones((2, 3, 4)) # 生成全1元素

torch.randn(3, 4) # 生成随机数元素

通过提供包含数值的Python列表(或嵌套列表),来为所需张量中的每个元素赋予确定值

1
torch.tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

tensor([[2, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])

数组运算

常见的标准算术运算符(+-*/**)都可以被升级为按元素运算

1
2
3
x = torch.tensor([1.0, 2, 4, 8])
y = torch.tensor([2, 2, 2, 2])
x + y, x - y, x * y, x / y, x ** y

tensor([ 3., 4., 6., 10.]),

tensor([-1., 0., 2., 6.]),

tensor([ 2., 4., 8., 16.]),

tensor([0.5000, 1.0000, 2.0000, 4.0000]),

tensor([ 1., 4., 16., 64.]))

​ “按元素”方式可以应用更多的计算

1
torch.exp(x)

tensor([2.7183e+00, 7.3891e+00, 5.4598e+01, 2.9810e+03])

我们也可以把多个张量连结(concatenate)在一起

1
2
3
4
5
6
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
Y = torch.tensor([[2.0, 1, 4, 3], [1, 2, 3, 4], [4, 3, 2, 1]])
torch.cat((X, Y), dim=0), torch.cat((X, Y), dim=1)
# cat把X、Y合并在一起,dim等于几,就会改变哪个维度,相当于numpy里的axis参数
# dim=0表示在第0维(行)
# dim=1表示在第1维(列)合并

(tensor([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [ 2., 1., 4., 3.], [ 1., 2., 3., 4.], [ 4., 3., 2., 1.]]), tensor([[ 0., 1., 2., 3., 2., 1., 4., 3.], [ 4., 5., 6., 7., 1., 2., 3., 4.], [ 8., 9., 10., 11., 4., 3., 2., 1.]]))

通过逻辑运算符构建二元张量

1
X == Y

tensor([[False, True, False, True], [False, False, False, False], [False, False, False, False]])

对张量中的所有元素进行求和,会产生一个单元素张量

1
X.sum()

tensor(66.)

数组广播

即使形状不同,我们仍然可以通过调用广播机制(broadcasting mechanism)来执行按元素操作

1
2
3
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b

(tensor([[0], [1], [2]]),

tensor([[0, 1]]))

1
a + b	# 广播机制自动复制自身

[[0, 0], [[0, 1], [[0, 1], [1, 1], + [0, 1], = [1, 2], [2, 2]] [0, 1]] [2, 3],

可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素

1
X[-1], X[1:3]

(tensor([ 8., 9., 10., 11.]), tensor([[ 4., 5., 6., 7.], [ 8., 9., 10., 11.]]))

除读取外,我们还可以通过指定索引来将元素写入矩阵

1
2
X[1, 2] = 9
X

tensor([[ 0., 1., 2., 3.], [ 4., 5., 9., 7.], [ 8., 9., 10., 11.]])

为多个元素赋值相同的值,我们只需要索引所有元素,然后为它们赋值

1
2
X[0:2, :] = 12
X

tensor([[12., 12., 12., 12.], [12., 12., 12., 12.], [ 8., 9., 10., 11.]])

内存分配

运行一些操作可能会导致为新结果分配内存

1
2
3
before = id(Y)
Y = Y + X
id(Y) == before

False

执行原地操作

1
2
3
4
Z = torch.zeros_like(Y)
print('id(Z):', id(Z))
Z[:] = X + Y
print('id(Z):', id(Z))

id(Z): 140327634811696 id(Z): 140327634811696

如果在后续计算中没有重复使用X,我们也可以使用X[:] = X + YX += Y来减少操作的内存开销

1
2
3
before = id(X)
X += Y
id(X) == before

True

转换为NumPy张量(ndarray

1
2
3
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)

(numpy.ndarray, torch.Tensor)

将大小为1的张量转换为Python标量

1
2
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)

(tensor([3.5000]), 3.5, 3.5, 3)

数据预处理

是指如果我有一个原始数据,我怎么把它读取进来,使得我们通过机器学习的方法能够处理。

创建数据集

创建一个人工数据集,并存储在CSV(逗号分隔值)文件

1
2
3
4
5
6
7
8
9
10
import os

os.makedirs(os.path.join('..', 'data'), exist_ok=True)
data_file = os.path.join('..', 'data', 'house_tiny.csv')
with open(data_file, 'w') as f:
f.write('NumRooms,Alley,Price\n') # 表头:房间数量,房间路线名称,房间价格
f.write('NA,Pave,127500\n') # NA表示未知的数
f.write('2,NA,106000\n')
f.write('4,NA,178100\n')
f.write('NA,NA,140000\n')

读取数据

从创建的CSV文件中加载原始数据集

一般读取CSV文件使用pandas库

1
2
3
4
import pandas as pd

data = pd.read_csv(data_file)
print(data)

NumRooms Alley Price 0 NaN Pave 127500 1 2.0 NaN 106000 2 4.0 NaN 178100 3 NaN NaN 140000

处理数据

为了处理缺失的数据,典型的方法包括插值法删除法

这里,我们将考虑插值法

1
2
3
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean()) # mean()指input的均值
print(inputs)

NumRooms Alley 0 3.0 Pave 1 2.0 NaN 2 4.0 NaN 3 3.0 NaN

对于inputs中的类别值或离散值,我们将“NaN”视为一个类别(Not a Number)

对于字符型数值,最好转变为数值类型

1
2
3
# 把列里面所有出现的不同种类的值都变成一个特征
inputs = pd.get_dummies(inputs, dummy_na=True) # 给不同的类分别赋值为一个单独的特征
print(inputs)

NumRooms Alley_Pave Alley_nan 0 3.0 1 0 1 2.0 0 1 2 4.0 0 1 3 3.0 0 1

现在inputsoutputs中的所有条目都是数值类型,它们可以转换为张量格式

1
2
3
4
5
import torch

X = torch.tensor(inputs.to_numpy(dtype=float))
y = torch.tensor(outputs.to_numpy(dtype=float))
X, y

(tensor([[3., 1., 0.], [2., 0., 1.], [4., 0., 1.], [3., 0., 1.]], dtype=torch.float64), tensor([127500., 106000., 178100., 140000.], dtype=torch.float64))

传统python计算会用32位或64位浮点数,但是64位浮点数对深度学习来讲计算比较慢,一般指定数据类型为32位浮点数。

线性代数

理论基础

标量—-一个值

  • 简单操作

    • c = a + b
    • c = a ⋅ b
    • c = sin a
  • 长度

    • $$ |a| = \begin{cases} a & \text{if } a > 0 \\ -a & \text{otherwise} \end{cases} $$

    • |a + b| ≤ |a|+|b|

    • |a ⋅ b| = |a|⋅|b|

向量 —-一行值

  • 向量的简单操作

    • $ c = a + b c_i = a_i + b_i $
    • $ c = b c_i = b_i $
    • $ c = a c_i = a_i $
  • 长度(范数)—-向量的长度就是向量的每个元素的平方求和再开根号

    • 范数(以 L2 范数为例) $$ \lVert a \rVert_2 = \left[ \sum_{i=1}^m a_i^2 \right]^{\frac{1}{2}} $$

    • $ a   a $

    • $ a + b a + b $

    • $ a b = |a| b $

  • 点乘

    • ab = ∑iaibi
  • 正交

    • ab = ∑iaibi = 0

矩阵

  • 简单操作
    • 矩阵加法:$ C = A + B C_{ij} = A_{ij} + B_{ij} $
    • 矩阵数乘:$ C = B C_{ij} = B_{ij} $
    • 矩阵逐元素正弦运算:$ C = A C_{ij} = A_{ij} $
  • 特征向量和特征值
    • 不被矩阵改变方向的向量
    • 对称矩阵总是可以找到特征向量
  • 乘法(矩阵乘以向量)
    • c = Ab where ci = ∑jAijbj
  • 乘法(矩阵乘以矩阵)
    • C = AB where Cik = ∑jAijBjk
  • 范数
    • c = A ⋅ b hence ‖c‖ ≤ ‖A‖ ⋅ ‖b
    • (b)(c)
  • 常见范数
    • 矩阵范数:最小的满足上面公式的值
    • Frobenius 范数
      $$ \lVert A \rVert_{\text{Frob}} = \left[ \sum_{ij} A_{ij}^2 \right]^{\frac{1}{2}} $$

特殊矩阵

  • 对称和反对称
    • $ A_{ij} = A_{ji} $(对称矩阵,元素关于主对角线对称)
    • $ A_{ij} = -A_{ji} $(反对称矩阵,主对角线元素为 0,( (i,j) ) 与 ( (j,i) ) 元素互反 )
  • 正定—-如果一个矩阵是正定的,那么它乘以任何一个列向量或行向量都大于等于0
    • 向量范数平方: x2 = xx ≥ 0
      (欧几里得范数平方,内积定义,结果非负 )
    • 二次型推广: xAx ≥ 0
      (刻画半正定/正定矩阵,( A ) 对称时,非零 ( x ) 代入结果非负则 ( A ) 半正定 )
  • 正交矩阵
    • 所有行都相互正交
    • 所有行都有单位长度 U with ∑jUijUkj = δik
    • 可以写成 UU = 1
  • 置换矩阵
    • P where Pij = 1 if and only if j = π(i)
    • 置换矩阵是正交矩阵

代码实现

标量计算

标量由只有一个元素的张量表示

1
2
3
4
5
6
import torch

x = torch.tensor(3.0)
y = torch.tensor(2.0)

x + y, x * y, x / y, x**y

(tensor(5.), tensor(6.), tensor(1.5000), tensor(9.))

向量基础

向量可以被视为标量值组成的列表

1
2
x = torch.arange(4)
x

tensor([0, 1, 2, 3])

通过张量的索引来访问任一元素

1
x[3]

tensor(3)

访问张量的长度

1
len(x)

4

只有一个轴的张量,形状只有一个元素

1
x.shape

torch.Size([4])

矩阵基础

通过指定两个分量mn来创建一个形状为m × n的矩阵

1
2
A = torch.arange(20).reshape(5, 4)
A

tensor([[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11], [12, 13, 14, 15], [16, 17, 18, 19]])

矩阵的转置

1
A.T

tensor([[ 0, 4, 8, 12, 16], [ 1, 5, 9, 13, 17], [ 2, 6, 10, 14, 18], [ 3, 7, 11, 15, 19]])

对称矩阵(symmetric matrix)A等于其转置:A = A

1
2
B = torch.tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])
B

tensor([[1, 2, 3], [2, 0, 4], [3, 4, 5]])

1
B == B.T

tensor([[True, True, True], [True, True, True], [True, True, True]])

矩阵形状改变

就像向量是标量的推广,矩阵是向量的推广一样,我们可以构建具有更多轴的数据结构

只要元素个数不发生变化,就可以根据个数改变矩阵的形状

行是最后一维(有几列)列是倒数第二维(有几行)

1
2
X = torch.arange(24).reshape(2, 3, 4)
X

tensor([[[ 0, 1, 2, 3], [ 4, 5, 6, 7], [ 8, 9, 10, 11]],

​ [[12, 13, 14, 15], ​ [16, 17, 18, 19], ​ [20, 21, 22, 23]]])

复制矩阵

给定具有相同形状的任意两个张量,任何按元素二元运算的结果都将是相同形状的张量

1
2
3
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone()
A, A + B

(tensor([[ 0., 1., 2., 3.], [ 4., 5., 6., 7.], [ 8., 9., 10., 11.], [12., 13., 14., 15.], [16., 17., 18., 19.]]),

tensor([[ 0., 2., 4., 6.], [ 8., 10., 12., 14.], [16., 18., 20., 22.], [24., 26., 28., 30.], [32., 34., 36., 38.]]))

矩阵元素积

两个矩阵的按元素乘法称为Hadamard积(Hadamard product)(数学符号

1
A * B

tensor([[ 0., 1., 4., 9.], [ 16., 25., 36., 49.], [ 64., 81., 100., 121.], [144., 169., 196., 225.], [256., 289., 324., 361.]])

1
2
3
a = 2
X = torch.arange(24).reshape(2, 3, 4)
a + X, (a * X).shape

(tensor([[[ 2, 3, 4, 5], [ 6, 7, 8, 9], [10, 11, 12, 13]],

​ [[14, 15, 16, 17], ​ [18, 19, 20, 21], ​ [22, 23, 24, 25]]]),

torch.Size([2, 3, 4]))

求和运算

计算向量元素的和

1
2
x = torch.arange(4, dtype=torch.float32)
x, x.sum()

(tensor([0., 1., 2., 3.]), tensor(6.))

表示任意形状张量的元素和

1
A.shape, A.sum()

(torch.Size([5, 4]), tensor(190.))

指定轴求和

指定张量沿哪一个轴来通过求和降低维度

1
2
A_sum_axis0 = A.sum(axis=0)		# 对A.shape的第0维进行求和运算,剩下的矩阵是A.shape去掉第0维后的形状
A_sum_axis0, A_sum_axis0.shape

(tensor([40., 45., 50., 55.]), torch.Size([4]))

1
2
A_sum_axis1 = A.sum(axis=1)		# 对A.shape的第1维进行求和运算,剩下的矩阵是A.shape去掉第1维后的形状
A_sum_axis1, A_sum_axis1.shape

(tensor([ 6., 22., 38., 54., 70.]), torch.Size([5]))

1
A.sum(axis=[0, 1])		# 对A.shape的第0维和第1维都进行求和运算,剩下的矩阵是A.shape去掉第0维和第1维后的形状

tensor(190.)

平均值计算

一个与求和相关的量是平均值(mean或average)

1
A.mean(), A.sum() / A.numel()

(tensor(9.5000), tensor(9.5000))

1
A.mean(axis=0), A.sum(axis=0) / A.shape[0]	# 按某维度来计算

(tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.]))

沿轴求和时维度不变

计算总和或均值时保持轴数不变

keepdims=True时,指不会把要求和的维度去掉而是把shape中对应的那个维度变成1

1
2
sum_A = A.sum(axis=1, keepdims=True)
sum_A

tensor([[ 6.], [22.], [38.], [54.], [70.]])

通过广播将A除以sum_A

1
A / sum_A

tensor([[0.0000, 0.1667, 0.3333, 0.5000], [0.1818, 0.2273, 0.2727, 0.3182], [0.2105, 0.2368, 0.2632, 0.2895], [0.2222, 0.2407, 0.2593, 0.2778], [0.2286, 0.2429, 0.2571, 0.2714]])

沿轴累加求和

某个轴计算A元素的累积总和

1
A.cumsum(axis=0)

tensor([[ 0., 1., 2., 3.], [ 4., 6., 8., 10.], [12., 15., 18., 21.], [24., 28., 32., 36.], [40., 45., 50., 55.]])

点积是相同位置的按元素乘积的和

1
2
y = torch.ones(4, dtype = torch.float32)
x, y, torch.dot(x, y)

(tensor([0., 1., 2., 3.]), tensor([1., 1., 1., 1.]), tensor(6.))

向量点积计算

我们可以通过执行按元素乘法,然后进行求和来表示两个向量的点积

1
torch.sum(x * y)

tensor(6.)

矩阵与向量积计算

矩阵向量积Ax是一个长度为m的列向量,其第i个元素是点积aix

1
A.shape, x.shape, torch.mv(A, x)

(torch.Size([5, 4]), torch.Size([4]), tensor([ 14., 38., 62., 86., 110.]))

矩阵x矩阵计算

我们可以将矩阵-矩阵乘法AB看作简单地执行m次矩阵-向量积,并将结果拼接在一起,形成一个n × m矩阵

1
2
B = torch.ones(4, 3)
torch.mm(A, B)

tensor([[ 6., 6., 6.], [22., 22., 22.], [38., 38., 38.], [54., 54., 54.], [70., 70., 70.]])

向量范数

L2范数是向量元素平方和的平方根:

$$\|\mathbf{x}\|_2 = \sqrt{\sum_{i=1}^n x_i^2}$$

1
2
u = torch.tensor([3.0, -4.0])
torch.norm(u)

tensor(5.)

L1范数,它表示为向量元素的绝对值之和:

$$\|\mathbf{x}\|_1 = \sum_{i=1}^n \left|x_i \right|$$

1
torch.abs(u).sum()

tensor(7.)

矩阵范数

矩阵的Frobenius范数(Frobenius norm)是矩阵元素平方和的平方根:

$$\|\mathbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}$$

1
torch.norm(torch.ones((4, 9)))

tensor(6.)

微积分

矩阵的计算部分要清楚如何求导数!

理论基础

标量导数

  • 导数是切线的斜率
y a xn exp (x) log (x) sin (x)
$\dfrac{dy}{dx}$ 0 nxn − 1 exp (x) $\dfrac{1}{x}$ cos (x)

注:a 不是 x 的函数

y u + v uv y = f(u), u = g(x)
$\dfrac{dy}{dx}$ $\dfrac{du}{dx} + \dfrac{dv}{dx}$ $\dfrac{du}{dx} v + \dfrac{dv}{dx} u$ $\dfrac{dy}{du} \dfrac{du}{dx}$

亚导数

当不存在导数时怎么办?

  • 将导数拓展到不可微的函数

    • $ y = |x| $

    • 绝对值函数的导数

    • $$ \frac{\partial |x|}{\partial x} = \begin{cases} 1 & \text{if } x > 0 \\ -1 & \text{if } x < 0 \\ a & \text{if } x = 0, \ a \in [-1,1] \end{cases} $$

    • ReLU 函数的导数(分段形式):
      $$ \frac{\partial}{\partial x} \text{max}(x, 0) = \begin{cases} 1 & \text{if } x > 0 \\ 0 & \text{if } x < 0 \\ a & \text{if } x = 0, \ a \in [0,1] \end{cases} $$

向量梯度

  • 将导数拓展到向量,需要注意向量或矩阵的形状!!
    • image-20250622164809141
  • 求导后梯度的形状,按照分子布局符号,如上图
    • y是列向量,x是标量,求导后的结果是y的形状(列向量)
    • x是列向量,y是标量,求导后的结果行向量
    • x是向量,y是向量,求导后的结果是矩阵
    • 如果是反过来的情况,则称为分母布局符号

单向量梯度样例

x是列向量y是标量

y 表达式 $\dfrac{\partial y}{\partial \boldsymbol{x}}$ 补充说明
y = a 标量(与 x 无关) 0 a 不是 x 的函数
y = au 标量(ux 的函数) $a \dfrac{\partial u}{\partial \boldsymbol{x}}$ 数乘求导法则
y = sum(x) 向量元素和 1 对向量各元素求和后求导
y = ‖x2 向量 L2 范数平方 2x x2 = xx 求导

符号说明

  • 0:零向量(元素全 0 ),0 是其转置(行向量);
  • 1:全 1 向量(元素全 1 ),1 是其转置(行向量);
  • 用途:向量微积分、机器学习梯度推导基础规则。
y 表达式 $\dfrac{\partial y}{\partial \boldsymbol{x}}$ 说明
y = u + v 和(u, v 是标量/向量函数) $\dfrac{\partial u}{\partial \boldsymbol{x}} + \dfrac{\partial v}{\partial \boldsymbol{x}}$ 加法求导法则
y = uv 积(u, v 是标量/向量函数) $\dfrac{\partial u}{\partial \boldsymbol{x}} v + \dfrac{\partial v}{\partial \boldsymbol{x}} u$ 乘积求导法则
y = ⟨u, v 内积(u, v 是向量函数) $\boldsymbol{u}^\top \dfrac{\partial \boldsymbol{v}}{\partial \boldsymbol{x}} + \boldsymbol{v}^\top \dfrac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}}$ 内积求导法则

用途说明:向量微积分基础规则,用于机器学习(如反向传播)、优化理论中推导梯度,计算复杂函数对参数向量的导数。

向量对向量的导数

(雅可比矩阵,Jacobian Matrix)

给定: $$ \boldsymbol{x} = \begin{bmatrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{bmatrix}, \quad \boldsymbol{y} = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_m \end{bmatrix} $$

雅可比矩阵定义:
$$ \frac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}} = \begin{bmatrix} \frac{\partial y_1}{\partial \boldsymbol{x}} \\ \frac{\partial y_2}{\partial \boldsymbol{x}} \\ \vdots \\ \frac{\partial y_m}{\partial \boldsymbol{x}} \end{bmatrix} = \begin{bmatrix} \frac{\partial y_1}{\partial x_1}, \frac{\partial y_1}{\partial x_2}, \dots, \frac{\partial y_1}{\partial x_n} \\ \frac{\partial y_2}{\partial x_1}, \frac{\partial y_2}{\partial x_2}, \dots, \frac{\partial y_2}{\partial x_n} \\ \vdots \\ \frac{\partial y_m}{\partial x_1}, \frac{\partial y_m}{\partial x_2}, \dots, \frac{\partial y_m}{\partial x_n} \end{bmatrix} $$

首先把y拆分后对向量x求导,再计算单独求导后的结果。

雅可比矩阵

y $\dfrac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}}$ 维度与说明
y = a(常向量,与 x 无关) 0(零矩阵) a 不随 x 变化
y = x(恒等映射) I(单位矩阵) 自身对自身求导为单位矩阵
y = Ax(矩阵-向量乘法) A(原矩阵) 线性变换导数是变换矩阵本身
y = xA(向量-矩阵乘法) A(矩阵转置) 转置保证导数维度 $^{m n} $

补充条件

  • 维度: x ∈ ℝn , $ ^m $,故 $ ^{m n} $;
  • 常量: $a, , $均与 $ $无关;
  • 符号:0 (零矩阵)、 I (单位矩阵) 。

用途:向量微积分基础,机器学习(如反向传播)、控制理论中线性变换梯度推导的核心规则。

y $\dfrac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}}$ 说明
y = au(标量-向量数乘) $a \dfrac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}}$ 数乘法则(标量提至导数外)
y = Au(矩阵-向量乘法) $\boldsymbol{A} \dfrac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}}$ 线性变换法则(矩阵提至导数外)
y = u + v(向量和) $\dfrac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}} + \dfrac{\partial \boldsymbol{v}}{\partial \boldsymbol{x}}$ 加法法则(和的导数拆为导数和)

用途说明

向量微积分基础规则,用于机器学习(如神经网络反向传播)、优化理论中,推导线性/仿射变换的梯度,计算损失函数对参数的导数。

向量链式法制

  • 标量链式法则

若 $y = f(u) u = g(x) $(复合函数, y 经 u 依赖 x ),则:

$$ \frac{dy}{dx} = \frac{dy}{du} \cdot \frac{du}{dx} $$

说明:微积分核心求导法则,是神经网络反向传播的数学基础(分解复杂梯度为多层简单梯度乘积)。

  • 标量中间变量 u

    • y = f(u)(y),u = g(x)(ux ∈ ℝn$ = $

    • 维度:$$ \frac{dy}{\partial \boldsymbol{x}} (1,n) = \frac{dy}{du} (1,) \cdot \frac{\partial u}{\partial \boldsymbol{x}} (1,n) $$

  • 拓展到向量,向量中间变量 u

    • y = f(u)(y),u = g(x)(u ∈ ℝkx ∈ ℝn$$ \frac{dy}{\partial \boldsymbol{x}} = \frac{dy}{\partial \boldsymbol{u}} \cdot \frac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}} $$
    • 维度:$$ \frac{dy}{\partial \boldsymbol{x}} (1,n) = \frac{dy}{\partial \boldsymbol{u}} (1,k) \cdot \frac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}} (k,n) $$
  • 向量对向量的链式求导法则

    • 若 $ 经向量 依赖于 $,则: $$ \frac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}} = \frac{\partial \boldsymbol{y}}{\partial \boldsymbol{u}} \cdot \frac{\partial \boldsymbol{u}}{\partial \boldsymbol{x}} $$
  • 维度说明

    • $\frac{\partial \boldsymbol{y}}{\partial \boldsymbol{x}} :(m, n) 矩阵( \boldsymbol{y} \in \mathbb{R}^m , \boldsymbol{x} \in \mathbb{R}^n )$

    • $$ \frac{\partial \boldsymbol{y}}{\partial \boldsymbol{u}} : (m, k) 矩阵( \boldsymbol{u} \in \mathbb{R}^k ) $$

    • $ : (k, n) 矩阵 $

  • 合法性:需满足 ( k ) 维度匹配(前导矩阵列数 = 后续矩阵行数)。

代码实现

如果f导数存在,这个极限被定义为:

$$f'(x) = \lim_{h \rightarrow 0} \frac{f(x+h) - f(x)}{h}$$

定义u = f(x) = 3x2 − 4x

1
2
3
4
5
6
7
8
# %matplotlib inline
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

通过令x = 1并让h接近0$\frac{f(x+h)-f(x)}{h}$的数值结果接近2

1
2
3
4
5
6
7
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

h=0.10000, numerical limit=2.30000 h=0.01000, numerical limit=2.03000 h=0.00100, numerical limit=2.00300 h=0.00010, numerical limit=2.00030 h=0.00001, numerical limit=2.00003

为了对导数的这种解释进行可视化,我们将使用matplotlib定义几个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def use_svg_display():  
"""使用svg格式在Jupyter中显示绘图"""
backend_inline.set_matplotlib_formats('svg')

def set_figsize(figsize=(3.5, 2.5)):
"""设置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()

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 d2l.plt.gca()

def has_one_axis(X):
return (hasattr(X, "ndim") and X.ndim == 1 or isinstance(X, list)
and not hasattr(X[0], "__len__"))

if has_one_axis(X):
X = [X]
if Y is None:
X, Y = [[]] * len(X), X
elif has_one_axis(Y):
Y = [Y]
if len(X) != len(Y):
X = X * len(Y)
axes.cla()
for x, y, fmt in zip(X, Y, fmts):
if len(x):
axes.plot(x, y, fmt)
else:
axes.plot(y, fmt)
set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend)

绘制函数u = f(x)及其在x = 1处的切线y = 2x − 3

1
2
x = np.arange(0, 3, 0.1)
plot(x, [f(x), 2 * x - 3], 'x', 'f(x)', legend=['f(x)', 'Tangent line (x=1)'])

自动求导

自动求导计算一个函数在指定值上的导数

它有别于符号求导数值求导

理论基础

计算图

将代码分解成操作子

将计算表示成一个无环图

image-20250626171942683

显示构造,可实现框架:Tensorflow、Theano、MXNet

1
2
3
4
5
6
from mxnet import sym

a = sym.var()
b = sym.var()
c = 2 * a + b
# bind data into a and b later

隐式构造,可实现框架:PyTorch、MXNet

1
2
3
4
5
6
from mxnet import autograd, nd

with autograd.record():
a = nd.ones((2,1))
b = nd.ones((2,1))
c = 2 * a + b

两种模式

核心原理链式法则$$\frac{\partial y}{\partial x} = \frac{\partial y}{\partial u_n} \frac{\partial u_n}{\partial u_{n - 1}} \dots \frac{\partial u_2}{\partial u_1} \frac{\partial u_1}{\partial x}$$

  • 正向积累:$$\frac{\partial y}{\partial x} = \frac{\partial y}{\partial u_n} \left( \frac{\partial u_n}{\partial u_{n - 1}} \left( \dots \left( \frac{\partial u_2}{\partial u_1} \frac{\partial u_1}{\partial x} \right) \right) \right)$$

  • 反向累积、又称反向传递 $$\frac{\partial y}{\partial x} = \left( \left( \left( \frac{\partial y}{\partial u_n} \frac{\partial u_n}{\partial u_{n - 1}} \right) \dots \right) \frac{\partial u_2}{\partial u_1} \right) \frac{\partial u_1}{\partial x}$$

反向累积总结

  • 构造计算图
  • 前向:执行图,存储中间结果
  • 反向:从相反方向执行图
    • 去除不需要的枝
image-20250626173435627

复杂度

  • 计算复杂度:O(n), n 是操作子个数
    • 通常正向和反向的代价类似
  • 内存复杂度: O(n),因为需要存储正向的所有中间结果 。(耗费计算资源的主要原因)
  • 跟正向累积对比:
    • O(n) 计算复杂度用来计算一个变量的梯度
    • O(1) 内存复杂度

代码实现

假设我们想对函数y = 2xx关于列向量x求导

1
2
3
import torch

x = torch.arange(4.0)

在我们计算y关于x的梯度之前,需要一个地方来存储梯度

1
2
x.requires_grad_(True)	# 等价于x = torch.arange(4.0, requires_grad=True),默认值是None
x.grad

现在计算y

1
y = 2 * torch.dot(x, x)

tensor(28., grad_fn=)

通过调用反向传播函数来自动计算y关于x每个分量的梯度

1
2
y.backward()	# 求导
x.grad # 访问导数

tensor([ 0., 4., 8., 12.])

1
x.grad == 4 * x

tensor([True, True, True, True])

现在计算x的另一个函数

1
2
3
4
x.grad.zero_()
y = x.sum()
y.backward()
x.grad

tensor([1., 1., 1., 1.])

深度学习中,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和

1
2
3
4
x.grad.zero_()
y = x * x
y.sum().backward()
x.grad

tensor([0., 2., 4., 6.])

将某些计算移动到记录的计算图之外

1
2
3
4
5
6
7
x.grad.zero_()
y = x * x
u = y.detach() # 把y当作一个常数,而不是关于x的函数
z = u * x

z.sum().backward() # z求导
x.grad == u # u*x的导数为u

tensor([True, True, True, True])

1
2
3
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x

tensor([True, True, True, True])

即使构建函数的计算图需要通过Python控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到的变量的梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def f(a):
b = a * 2
while b.norm() < 1000:
# norm()是求欧几里得范数,欧几里得范数指得就是通常意义上的距离范数。
# 例如在欧式空间里,它表示两点间的距离(向量x的模长)。
# 即使函数经过了流程控制会产生不同的函数公式,每次计算的时候,pytorch都会把计算图存起来。
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c

a = torch.randn(size=(), requires_grad=True) # size=空,表示是一个标量
d = f(a)
d.backward()

a.grad == d / a

tensor(True)