2.1 数据操作
机器学习通常需要处理大型数据集,我们可以将某些数据集视为一个表,表的行对应样本,列对应属性。
n维数组,也称为张量tensor,深度学习框架比Numpy中的ndarray多一些重要的功能:首先GPU很好地支持加速计算,而Numpy仅支持cpu计算;其次张量支持自动微分。
2.1.1 入门
张量表示一个由数值组成的数组,这个数组可能有多个维度。具有一个轴的张量对应数学上的向量;具有两个轴的张量对应矩阵;具有两个轴以上的张量没有特殊名称。
使用arange创建一个行向量x如下,默认创建整数,张量中的每个值称为张量的元素。除非额外指定,新的张量将存储在内存中,并采用基于GPU的计算。
x = torch.arange(12)
"""
tensor([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
"""
y = torch.arange(1, 5)
"""
tensor([1, 2, 3, 4])
"""
可以通过张量的shape属性来访问张量(沿每个轴的长度)的形状。
x.shape
"""
torch.Size([12])
"""
如果只想知道张量中元素的总数,即形状的所有元素乘积,可以检查它的大小size,由于此处是向量,所以它的shape和size相同。
x.numel() # 12
通过改变张量的形状,张量的大小不会改变。也可以通过-1来自动计算出维度。
X = x.reshape(3, 4)
X = x.reshape(-1, 4)
可以设置张量全0、全1、其他常量。或者从特定分布中随机采样的数字来初始化矩阵。
x = 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.]]])
"""
或者从特定分布中随机采样的数字来初始化矩阵,以下代码创建的张量每个元素从均值0、标准差1的标准高斯分布(正态分布)中随机采样。
torch.randn(3, 4)
"""
tensor([[ 0.7407, 0.7261, -1.6166, -1.5261],
[ 0.4725, -0.4785, 0.1242, -1.7671],
[-1.3982, 2.0153, -0.9779, 1.5072]])
"""
2.1.1 运算符
将多个张量连接在一起,只需要提供张量列表,并给出沿哪个轴连结。
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) # dim = 0即按行,dim = 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.]]))
"""
对张量中所有元素求和,会产生一个单元素张量。
X.sum() # tensor(66.)
2.1.3 广播机制
在某些情况下,即使形状不同,我们仍然可以通过调用广播机制,来执行按元素操作。
2.1.4 索引和切片
第一个元素的索引是0,最后一个元素的索引是-1。如下所示,可以用-1来选择最后一个元素,用[1:3]选择第二和第三个元素。
X[-1], X[1:3]
"""
(tensor([ 8., 9., 10., 11.]),
tensor([[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
"""
也可以通过指定索引来将元素写入矩阵。
X[1, 2] = 9
如果想为多个元素赋相同的值,可以索引所有元素,然后为它们赋值。例如,[0:2, :]访问第1行和第2行,:代表沿轴1(列)的所有元素。
X[0:2, 1] = 12
"""
tensor([[12., 12., 12., 12.],
[12., 12., 12., 12.],
[8., 9., 10., 11.]])
"""
也可以访问指定行和列,[0, 2]表示第1列和第三列。
X[:, [0, 2]] = 12
"""
tensor([[12., 1., 12., 3.],
[12., 5., 12., 7.],
[12., 9., 12., 11.]])
"""
[:, 0:2]表示选择一个连续的列区间(第1列和第2列),而[:, [0,2]]表示选择不连续的特定列(第1列和第3列。
2.1.5 节省内存
运行一些操作可能会导致为新结果分配内存,比如使用Y = X + Y,会首先计算Y + X,然后为结果分配新的内存,使Y指向内存中新的位置。
但是我们不想总是不必要地分配内存,在机器学习中,更新参数时希望原地执行这些更新。
可以使用切片表示法将结果分配给先前分配的数组。如果在后续的计算没有重复使用X,可以用X[:] = X + Y或X += Y来减少操作的内存开销。
2.1.6 转换为其他Python对象
将深度学习框架定义的张量转换为NumPy张量(ndarray
)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。
A = X.numpy()
B = torch.tensor(A)
type(A), type(B)
"""
(numpy.ndarray, torch.Tensor)
"""
要将大小为1的张量转换为Python标量,可以调用item
函数或Python的内置函数。
a = torch.tensor([3.5])
a, a.item(), float(a), int(a)
"""
(tensor([3.5000]), 3.5, 3.5, 3)
"""
2.2 数据预处理
2.2.1 读取数据集
首先创建一个人工数据集,并存储在CSV(逗号分隔值)文件../data/house_tiny.csv中,下面将数据集按行写入CSV文件中。
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文件加载原始数据集,通过导入pandas包并调用read_csv函数。
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
"""
2.2.2 处理缺失值
上面数据集中,NaN项代表缺失值,为了处理缺失值,典型的方法包括插值法和删除法。插值法用一个替代值弥补缺失值,删除法直接忽略缺失值,此处考虑插值法。
通过位置索引iloc,将data分成inputs(前两项)和outputs(最后一项),对于缺失值,使用同一列的均值替换NaN项。
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean()) # mean表示使用同一列均值替换NaN项
print(inputs)
"""
NumRooms Alley
0 3.0 Pave
1 2.0 NaN
2 4.0 NaN
3 3.0 NaN
"""
对于inputs中的类别值或离散值,我们将NaN视为一个类别。由于Alley类型列只接受两种类型的类别值Pave和NaN,pandas可以自动将此列转换成两列Alley_Pave和Alley_nan。
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
"""
2.2.3 转换为张量格式
现在inputs和outputs中所有条目都是数值类型,可以转换为张量格式。数据采用张量格式后,就可以引入那些张量函数来进一步操作。
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))
"""
2.3 线性代数
2.3.1 标量
表达式𝑐=5/9(𝑓−32)中,每一项(5,9,32)都是标量值。符号c和f称为变量,它们表示未知的标量值。标量由只有一个元素的张量表示。
2.3.2 向量
向量被视为标量值组成的列表,这些标量值被称为向量的元素或分量。通过一维张量表示向量。
x = torch.arange(4) # tensor([0, 1, 2, 3])
可以通过下标来引用向量的任一元素。向量的长度通常称为向量的维度。可以通过Python内置函数len()访问张量的长度。
x[3] # tensor(3)
len(x) # 4
对于只有一个轴的张量,形状只有一个元素。
x.shape # torch.Size([4])
向量或轴的维度被用来表示其长度,而张量的维度用来表示张量具有的轴数。
2.3.3 矩阵
调用函数来实例化张量时,可以通过指定两个分量m和n来创建一个形状为m×n的矩阵。
A = torch.arange(20).reshape(5, 4)
"""
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
"""
现在在代码中访问矩阵的转置。
A.T
对称矩阵等于其转置。
2.3.4 张量
我们开始处理图像时,张量将变得更加重要,图像以n维数组形式出现,其中3个轴对应高度,宽度以及通道轴,用于表示颜色通道(红、绿和蓝色)。
X = torch.arange(24).reshape(2, 3, 4)
2.3.5 张量算法的基本性质
两个矩阵按元素乘法称为Hadamard积(非矩阵乘法,要求两矩阵形状相同),A和B的Hadamard积为
2.3.6 降维
默认情况下,调用求和函数会沿所有的轴降低张量的维度,使它变为一个标量,可以指定张量沿哪一个轴来通过求和降低维度,以矩阵为例,为了通过求和所有行来降维,可以在调用函数时指定axis = 0。由于输入矩阵沿轴0降维以生成输出向量,因此输入轴0的维数在输出形状中消失。
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
"""
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]])
"""
A_sum_axis0 = A.sum(axis=0)
A_sum_axis0, A_sum_axis0.shape
"""
(tensor([40., 45., 50., 55.]), torch.Size([4]))
"""
轴0对应行,沿着行求和即对每一列元素求和。
沿着行和列对矩阵求和,等于对矩阵所有元素求和。
A.sum(axis=[0, 1]) # 结果和A.sum()相同
可以调用函数来计算任意形状张量的平均值:A.mean(),或A.sum() / A.numel()
计算平均值的函数也可以按指定轴降低张量的维度。
A.mean(axis=0), A.sum(axis=0) / A.shape[0]
"""
(tensor([ 8., 9., 10., 11.]), tensor([ 8., 9., 10., 11.]))
"""
有时在调用函数来计算总和或均值时保持轴数不变会很有用。
sum_A = A.sum(axis=1, keepdims=True)
"""
tensor([[ 6.],
[22.],
[38.],
[54.],
[70.]])
"""
如果想沿着某个轴计算A元素的累计总和,比如按axis=0,可以调用cumsum函数,此函数不会沿任何轴降低输入张量的维度。
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.]])
"""
2.3.7 点积
给定两个向量x,y∈,它们的点积是xTy或<x,y>,点积是相同位置的按元素乘积的和。
2.3.8 矩阵-向量积
将矩阵A(m×n)用它的行向量a(1×n)表示。
x是n×1的列向量,Ax是一个长度为m的列向量。
在代码中使用张量表示矩阵-向量积,使用mv函数,torch.mv(A, x)会执行矩阵-向量积。
2.3.9 矩阵-矩阵乘法
a,b均为k维向量。
则有
可以将矩阵-矩阵乘法看作简单执行了m次矩阵-向量积。并将结果拼在一起,形成一个n×m的矩阵。
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.]])
"""
2.3.10 范数
向量的范数表示一个向量有多大。这里的大小不涉及维度,而是分量的大小。在线性代数中,向量范数是将向量映射到标量的函数f。给定任意向量x,向量范数要满足一些属性。第一个性质是:如果按常数因子α缩放向量的所有元素,其范数也会按照相同常数因子的绝对值缩放:
第二个性质是三角不等式:
第三个性质即范数必须是非负的:
这是因为大多数情况下,任何东西的最小的大小是0,最后一个性质要求范数最小为0,当且仅当向量全由0组成。
欧几里得距离是一个范数:假设n维向量x中的元素是x1,x2....xn,其
范数是向量元素的平方和的平方根:
其中,在范数中经常省略下标2,即
等同于
。在代码中,可以按如下方式计算
范数。
u = torch.tensor([3.0, -4.0])
torch.norm(u)
深度学习也经常遇到范数,它表示为向量元素的绝对值之和:
与范数相比,
范数受异常值的影响较小。
范数计算方式如下:
torch.abs(u).sum()
范数和
范数都是更一般
范数的特例:
类似于向量的范数,矩阵X(m×n)的Frobenius范数是矩阵元素平方和的平方根:
Frobenius范数也满足向量范数的所有性质,它就像是矩阵形向量的范数,计算方式如下:
torch.norm(torch.ones((4, 9))) # 6
2.3.11 小结
一个张量可以通过sum和mean沿指定的轴降低维度。
两个矩阵的按元素乘法称为它们的Hadamard积,与矩阵乘法不同。
标量、向量、矩阵和张量分别具有0、1、2和任意数量的轴。
2.4 微积分
训练模型只能将模型与我们实际能看到的数据相拟合。因此,我们可以将拟合模型的任务分解为两个关键问题:
优化:用模型拟合观测数据的过程;
泛化:指导我们生成出有效性超出用于训练的数据集本身的模型。
2.4.1 导数和微分
在深度学习中,通常选择对于模型参数可微的损失函数。即如果把这个参数增加或减少一个无穷小的量,可以知道损失会以多快的速度增加或减少。
假设有一个函数f,输入输出都是标量,如果f的导数存在,这个极限被定义为
如果f'(a)存在,则称f在a处可微。如果f在一个区间的每个数都是可微的,则此函数在此区间是可微的。将上式的导数解释为f(x)相对于x的瞬时变化率。
先给定y=f(x),以下表达式是等价的:
其中D和是微分运算符。
注释#@save是一个特殊的标记,会将对应的函数、类或语句保存在d2l包中。 因此,以后无须重新定义就可以直接调用它们(如d2l.use_svg_display())。
def use_svg_display(): #@save
"""使用svg格式在Jupyter中显示绘图"""
backend_inline.set_matplotlib_formats('svg')
定义set_figsize函数来设置图表大小。此处可以直接使用d2l.plt,因为导入语句from matplotlib import pyplot as plt
已标记为保存到d2l
包中。
def set_figsize(figsize=(3.5, 2.5)): #@save
"""设置matplotlib的图表大小"""
use_svg_display()
d2l.plt.rcParams['figure.figsize'] = figsize
下面的set_axes函数用来设置由matplotlib生成图标的轴的属性。
#@save
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()
通过上述三个用于图形配置的函数,定义一个plot函数来简洁地绘制多条曲线。
#@save
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()
# 如果X有一个轴,输出True
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,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)'])
2.4.2 偏导数
对于偏导数的表示,以下是等价的:
2.4.3 梯度
可以连结一个多元函数对其所有变量的偏导数,以得到该函数的梯度向量。设函数f输入一个n维向量,输出是一个标量。函数f(x)相对于x的梯度是一个包含n个偏导数的向量:
假设x为n维向量,在微分多元函数经常使用以下规则:
2.4.4 链式法则
假设可微分函数y有变量u1,u2...um,其中每个可微分函数ui都有变量x1,x2...xn。则链式法则给出:
2.5 自动微分
深度学习框架通过自动计算导数,即自动微分来加快求导。实际中,根据设计好的模型,系统会构建一个计算图,来跟踪计算是哪些数据通过操作组合起来产生输出。自动微分使系统能够随后反向传播梯度。反向传播意味着跟踪整个计算图,填充关于每个参数的偏导数。
2.5.1 一个简单的例子
假设想对y=2xTx关于列向量x求导。在计算y关于x的梯度之前,需要一个地方来存储梯度,我们不会在每次对一个参数求导时都分配新的内存。一个标量函数关于向量x的梯度时向量,且与x有相同的形状。
import torch
x=torch.arange(4.0,requires_grad=True)
x.grad # 默认值是None
现在计算y。
y = 2 * torch.dot(x, x)
接下来通过反向传播函数来自动计算y关于x每个分量的梯度。
y.backward()
x.grad
"""
tensor([ 0., 4., 8., 12.])
"""
现在计算x的另一个函数。
# 在默认情况下,PyTorch会累积梯度,我们需要清除之前的值
x.grad.zero_()
y = x.sum()
y.backward()
x.grad
"""
tensor([1., 1., 1., 1.])
"""
2.5.2 非标量变量的反向传播
y不是标量时,向量y关于向量x的导数是一个矩阵,对于高阶的y和x,求导的结果可以是一个高阶张量。
当调用向量的反向计算时,我们通常会试图计算一批训练样本中每个组成部分的损失函数的导数。 这里我们的目的不是计算微分矩阵,而是单独计算批量中每个样本的偏导数之和。
# 对非标量调用backward需要传入一个gradient参数,该参数指定微分函数关于self的梯度。
# 本例只想求偏导数的和,所以传递一个1的梯度是合适的
x.grad.zero_()
y = x * x # y=x1**2 + x2**2 +....
# 等价于y.backward(torch.ones(len(x)))
y.sum().backward()
x.grad
"""
tensor([0., 2., 4., 6.])
"""
2.5.3 分离计算
有时,我们希望将某些计算移动到记录的计算图之外。 例如,假设y是作为x的函数计算的,而z
则是作为y
和x
的函数计算的。 想象一下,我们想计算z关于x的梯度,但由于某种原因,希望将y视为一个常数, 并且只考虑到x在y被计算后发挥的作用。
这里可以分离y来返回一个新变量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
"""
tensor([True, True, True, True])
"""
由于记录了y的计算结果,我们可以随后在y上调用反向传播, 得到y=x*x关于x的导数,即2*x
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
"""
tensor([True, True, True, True])
"""
2.5.4 Python控制流的梯度计算
2.5.5 小结
深度学习框架可以自动计算导数,首先将梯度附加到想要对其计算偏导数的变量上,然后记录目标值的计算,执行它的反向传播函数,并访问得到的梯度。
2.6 概率
2.6.1 基本概率论
以投掷骰子为例。
%matplotlib inline
import torch
from torch.distributions import multinomial
from d2l import torch as d2l
可以把分布看作对事件的概率分配。将概率分配给一些离散选择的分布称为多项分布。
为了抽取一个样本,即掷骰子,只需传入一个概率向量,输出另一个相同长度的向量。在索引i处的值是采样结果中i出现的次数。
fair_probs = torch.ones([6]) / 6 # tensor([1/6,1/6,1/6....])
multinomial.Multinomial(1, fair_probs).sample()
# 1表示进行一次实验,.sample方法表示从该多项分布中采样。
"""
tensor([0., 0., 0., 1., 0., 0.])
"""
在估计一个骰子的公平性时,我们希望从同一分布中生成多个样本。
multinomial.Multinomial(10, fair_probs).sample()
"""
tensor([1., 1., 2., 1., 3., 2.])
"""
现在可以模拟1000次投掷,计算相对频率,以作为真实概率的估计。
# 将结果存储为32位浮点数以进行除法
counts = multinomial.Multinomial(1000, fair_probs).sample()
counts / 1000 # 相对频率作为估计值
"""
tensor([0.1570, 0.1690, 0.1600, 0.1810, 0.1620, 0.1710])
"""
下面进行500组实验,每组抽取10个样本。
counts = multinomial.Multinomial(10, fair_probs).sample((500,))
cum_counts = counts.cumsum(dim=0)
estimates = cum_counts / cum_counts.sum(dim=1, keepdims=True)
print(estimates)
d2l.set_figsize((6, 4.5))
for i in range(6):
d2l.plt.plot(estimates[:, i].numpy(),
label=("P(die=" + str(i + 1) + ")"))
d2l.plt.axhline(y=0.167, color='black', linestyle='dashed')
d2l.plt.gca().set_xlabel('Groups of experiments')
d2l.plt.gca().set_ylabel('Estimated probability')
d2l.plt.legend();
其中,.sample()接收一个可选参数'sample_shape',通过传递该参数,可以生成多次采样的结果。.sample((10,))会从分布中采样10次返回大小为[10,6]的张量。
2.6.2 处理多个随机变量
P(A丨B) = P(AB)/P(B)
2.6.3 期望和方差
为例概括概率分布的关键特征,需要一些测量方法。一个随机变量X的期望表示为:
当函数𝑓(𝑥)𝑓(𝑥)的输入是从分布𝑃𝑃中抽取的随机变量时,𝑓(𝑥)𝑓(𝑥)的期望值为