D2L学习笔记-预备知识

  1. 1. 环境安装
  2. 2. 数据操作与数据预处理
  3. 3. 线性代数
  4. 4. 自动微分

李沐动手学深度学习(PyTorch)课程学习笔记第一章:预备知识。

1. 环境安装

首先安装好 PyTorch 环境:Anaconda 与 PyTorch 安装教程

安装需要的包:

1
pip install d2l

2. 数据操作与数据预处理

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

1
2
3
4
import torch

x = torch.arange(12)
print(x) # tensor([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])

我们可以通过张量的 shape 属性来访问张量的形状和张量中元素的总数:

1
2
print(x.shape)  # torch.Size([12])
print(x.numel()) # 12

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

1
2
3
4
5
x = x.reshape(3, 4)
print(x)
# tensor([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
print(torch.zeros((2, 3, 4)))
# tensor([[[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]],
#
# [[0., 0., 0., 0.],
# [0., 0., 0., 0.],
# [0., 0., 0., 0.]]])
print(torch.ones((2, 3, 4)))
# tensor([[[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]],
#
# [[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]]])

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

1
2
x = torch.tensor([2, 3, 4, 5])
print(x) # tensor([2, 3, 4, 5])

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

1
2
3
4
x = torch.tensor([1.0, 2, 3])
y = torch.tensor([2, 2, 2])
print('x + y:', x + y, 'x - y:', x - y) # x + y: tensor([3., 4., 5.]) x - y: tensor([-1., 0., 1.])
print(torch.exp(x)) # tensor([ 2.7183, 7.3891, 20.0855])

我们也可以把多个张量拼接在一起:

1
2
3
4
5
x = torch.arange(6, dtype=torch.float32).reshape(2, 3)
y = torch.tensor([[2.0, 1, 4], [3, 2, 1]])
# dim表示在哪个维度上拼接
print(torch.cat((x, y), dim=0).shape) # torch.Size([4, 3])
print(torch.cat((x, y), dim=1).shape) # torch.Size([2, 6])

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

1
2
3
x = torch.tensor([1, 2, 3])
y = torch.tensor([1, 3, 3])
print(x == y) # tensor([True, False, True])

对张量中的所有元素进行求和会产生一个只有一个元素的张量,或者指定在某一维度上求和:

1
2
3
x = torch.tensor([[1, 2, 3], [1, 1, 1]])
print(x.sum()) # tensor(9)
print(x.sum(0)) # tensor([2, 3, 4])

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

1
2
3
4
5
6
7
x = torch.arange(3).reshape((3, 1))
y = torch.arange(2).reshape((1, 2))
# 必须维度一样才能广播,先将x复制成3行2列,再将y复制成3行2列,然后运算
print(x + y)
# tensor([[0, 1],
# [1, 2],
# [2, 3]])

与 NumPy 张量相互转化:

1
2
3
a = x.numpy()
b = torch.tensor(a)
print(type(a), type(b)) # <class 'numpy.ndarray'> <class 'torch.Tensor'>

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

1
2
x = torch.tensor([3.5])
print(x, x.item(), float(x)) # tensor([3.5000]) 3.5 3.5

创建一个人工数据集,并存储在 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') # 每行表示一个数据样本
f.write('2,NA,106000\n')
f.write('4,NA,178100\n')
f.write('NA,NA,140000\n')

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

1
2
3
4
5
6
7
8
9
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
4
5
6
7
8
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]  # 将data的第0、1列作为input,第2列作为output
inputs = inputs.fillna(inputs.mean(numeric_only=True)) # 将input中为NaN的数据用平均值填充
print(inputs)
# NumRooms Alley
# 0 3.0 Pave
# 1 2.0 NaN
# 2 4.0 NaN
# 3 3.0 NaN

对于 inputs 中的类别值或离散值,我们可以将 NaN 视为一个类别:

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

x, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
print(x, y)
# tensor([[3., 1., 0.],
# [2., 0., 1.],
# [4., 0., 1.],
# [3., 0., 1.]], dtype=torch.float64) tensor([127500, 106000, 178100, 140000])

3. 线性代数

标量由只有一个元素的张量表示,你可以将向量视为标量值组成的列表:

1
2
3
4
5
6
7
8
import torch

x = torch.tensor(3.0)
y = torch.tensor(2.0)
print(x + y, x * y) # tensor(5.) tensor(6.)

x = torch.arange(4)
print(x) # tensor([0, 1, 2, 3])

访问张量的长度和形状:

1
2
print(len(x))  # 4
print(x.shape) # torch.Size([4])

矩阵和矩阵的转置:

1
2
3
4
5
A = torch.arange(9).reshape(3, 3)
print(A.T)
# tensor([[0, 3, 6],
# [1, 4, 7],
# [2, 5, 8]])

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

1
2
3
4
5
6
A = torch.arange(12, dtype=torch.float32).reshape(3, 4)
B = A.clone() # 通过分配新内存,将A的一个副本分配给B
print(A + B)
# tensor([[ 0., 2., 4., 6.],
# [ 8., 10., 12., 14.],
# [16., 18., 20., 22.]])

求和与求平均值:

1
2
3
4
5
6
7
8
9
print(A.sum())  # tensor(66.),所有元素求和
print(A.sum(axis=0)) # tensor([12., 15., 18., 21.]),在第0维上求和
print(A.sum(axis=1)) # tensor([ 6., 22., 38.]),在第1维上求和
print(A.mean()) # tensor(5.5000),所有元素均值
print(A.mean(axis=1)) # tensor([1.5000, 5.5000, 9.5000]),在第1维上求均值
print(A.sum(axis=1, keepdims=True)) # 计算总和或均值时保持维度不变
# tensor([[ 6.],
# [22.],
# [38.]])

点积是相同位置的按元素乘积的和,torch.dot 只能对一维向量做点积。注意 NumPy 中的 np.dot 函数计算的是两个矩阵的矩阵乘法,而非对应元素相乘求和:

1
2
3
x = torch.arange(4, dtype=torch.float32)  # tensor([0., 1., 2., 3.])
y = torch.ones(4, dtype=torch.float32) # tensor([1., 1., 1., 1.])
print(torch.dot(x, y)) # tensor(6.)

矩阵向量积:

1
2
3
4
5
x = torch.arange(6, dtype=torch.float32).reshape(2, 3)
# tensor([[0., 1., 2.],
# [3., 4., 5.]])
y = torch.tensor([1, 2, 3], dtype=torch.float32) # tensor([1., 2., 3.])
print(torch.mv(x, y)) # tensor([ 8., 26.])

矩阵乘法,torch.mmnp.dot 类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
x = torch.arange(6, dtype=torch.float32).reshape(2, 3)
# tensor([[0., 1., 2.],
# [3., 4., 5.]])
y = torch.ones(6, dtype=torch.float32).reshape(3, 2)
# tensor([[1., 1.],
# [1., 1.],
# [1., 1.]])
print(torch.mm(x, y))
# tensor([[ 3., 3.],
# [12., 12.]])
import numpy as np
print(np.dot(x, y))
# [[ 3. 3.]
# [12. 12.]]

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

1
2
3
x = torch.tensor([3, -4], dtype=torch.float32)
print(torch.norm(x)) # tensor(5.)
print(torch.norm(x, 2)) # tensor(5.)

L1 范数为向量所有元素的绝对值之和:

1
2
print(torch.abs(x).sum())  # tensor(7.)
print(torch.norm(x, 1)) # tensor(7.)

F 范数(弗罗贝尼乌斯范数)是矩阵所有元素的平方和的平方根:

1
print(torch.norm(torch.ones((4, 9))))  # tensor(6.)

4. 自动微分

先举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import torch

x = torch.arange(4.0)
print(x) # tensor([0., 1., 2., 3.])

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

y = 2 * torch.dot(x, x) # 计算y,注意一个标量函数关于向量x的梯度是向量,并且与x具有相同的形状
print(y) # tensor(28., grad_fn=<MulBackward0>),隐式构造了计算图,所以有一个求梯度的函数

y.backward() # 通过调用反向传播函数来自动计算y关于x每个分量的梯度
print(x.grad) # tensor([ 0., 4., 8., 12.]),y=2*x*x的导数为4x
print(x.grad == 4 * x) # tensor([True, True, True, True])

# 现在计算x的另一个函数
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
print(x.grad) # tensor([1., 1., 1., 1.])

y 不是标量时,向量 y 关于向量 x 的导数的最自然解释是一个矩阵。对于高阶和高维的 yx,求导的结果可以是一个高阶张量。

然而,虽然这些更奇特的对象确实出现在高级机器学习中(包括深度学习中),但当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。这里,我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和:

1
2
3
4
5
6
7
8
# 对非标量调用backward()需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x
print(y) # tensor([0., 1., 4., 9.], grad_fn=<MulBackward0>)
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
print(x.grad) # tensor([0., 2., 4., 6.])

有时,我们希望将某些计算移动到记录的计算图之外。例如,假设 y 是作为 x 的函数计算的,而 z 则是作为 yx 的函数计算的。想象一下,我们想计算 z 关于 x 的梯度,但由于某种原因,希望将 y 视为一个常数,并且只考虑到 xy 被计算后发挥的作用。

这里可以分离 y 来返回一个新变量 u,该变量与 y 具有相同的值,但丢弃计算图中如何计算 y 的任何信息。换句话说,梯度不会向后流经 ux。因此,下面的反向传播函数计算 z = u * x 关于 x 的偏导数,同时将 u 作为常数处理,而不是计算 z = x * x * x 关于 x 的偏导数。

1
2
3
4
5
6
7
8
9
10
11
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x

z.sum().backward()
print(x.grad == u) # tensor([True, True, True, True])

x.grad.zero_()
y.sum().backward()
print(x.grad == 2 * x) # tensor([True, True, True, True])

使用自动微分的一个好处是:即使构建函数的计算图需要通过 Python 控制流(例如,条件、循环或任意函数调用),我们仍然可以计算得到变量的梯度。在下面的代码中,while 循环的迭代次数和 if 语句的结果都取决于输入 a 的值。对于任何 a,存在某个常量标量 k,使得 d = f(a) = k * a,其中 k 的值取决于输入 a,因此可以用 d / a 验证梯度是否正确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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) # tensor(True)

下一章:线性神经网络