数据操作

  • N维数组是机器学习和神经网络的主要数据结构;
  • 创建数组需要:形状,元素类型,元素的值;
  • 访问的时候可以从一个元素开始、一行一列、子区域,甚至是带步长的分离区域。

连结,Concatenate

我们也可以把多个张量连结(concatenate)在一起, 把它们端对端地叠起来形成一个更大的张量。 我们只需要提供张量列表,并给出沿哪个轴连结。 下面的例子分别演示了当我们沿行(轴-0,形状的第一个元素) 和按列(轴-1,形状的第二个元素)连结两个矩阵时,会发生什么情况。 我们可以看到,第一个输出张量的轴-0长度( 6 )是两个输入张量轴-0长度的总和( 3+3 ); 第二个输出张量的轴-1长度( 8 )是两个输入张量轴-1长度的总和( 4+4 )。

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

(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.]]))

广播机制

在上面的部分中,我们看到了如何在相同形状的两个张量上执行按元素操作。 在某些情况下,即使形状不同,我们仍然可以通过调用 广播机制(broadcasting mechanism)来执行按元素操作。 这种机制的工作方式如下:首先,通过适当复制元素来扩展一个或两个数组, 以便在转换之后,两个张量具有相同的形状。 其次,对生成的数组执行按元素操作。

1
2
3
4
5
6
7
8
9
10
11
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
a, b, a+b

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

索引和切片

就像在任何其他Python数组中一样,张量中的元素可以通过索引访问。 与任何Python数组一样:第一个元素的索引是0,最后一个元素索引是-1; 可以指定范围以包含第一个元素和最后一个之前的元素。
我们可以用[-1]选择最后一个元素,可以用[1:3]选择第二个和第三个元素。

节省内存

运行一些操作可能会导致为新结果分配内存。例如,如果我们用Y = X + Y,我们将取消引用Y指向的张量,而是指向新分配的内存处的张量。
在下面的例子中,我们用Python的id()函数演示了这一点, 它给我们提供了内存中引用对象的确切地址。 运行Y = Y + X后,我们会发现id(Y)指向另一个位置。 这是因为Python首先计算Y + X,为结果分配新的内存,然后使Y指向内存中的这个新位置。

1
2
3
4
5
before = id(Y)
Y = Y + X
id(Y) == before, before, id(Y)

(False, 2153248149728, 2153248204096)

这可能是不可取的,原因有两个:首先,我们不想总是不必要地分配内存。 在机器学习中,我们可能有数百兆的参数,并且在一秒内多次更新所有参数。 通常情况下,我们希望原地执行这些更新。 其次,如果我们不原地更新,其他引用仍然会指向旧的内存位置, 这样我们的某些代码可能会无意中引用旧的参数。
幸运的是,(执行原地操作)非常简单。 我们可以使用切片表示法将操作的结果分配给先前分配的数组,例如Y[:] = 。 为了说明这一点,我们首先创建一个新的矩阵Z,其形状与另一个Y相同, 使用zeros_like来分配一个全 0 的块。

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

id(Z): 2153570368624
id(Z): 2153570368624

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

1
2
3
4
5
6
before = id(X)
X += Y
X[:] = X + Y
id(X) == before

True

转换为其他python对象

将深度学习框架定义的张量转换为NumPy张量(ndarray)很容易,反之也同样容易。 torch张量和numpy数组将共享它们的底层内存,就地操作更改一个张量也会同时更改另一个张量。

1
2
3
4
5
6
7
X = torch.arange(12, dtype=torch.float32).reshape((3,4))
A = X.numpy()
B = torch.tensor(A)
type(X),type(A), type(B), id(X), id(A), id(B)

(torch.Tensor, numpy.ndarray, torch.Tensor,
2153570697440, 2153570749968, 2153570698640)
1
2
3
4
5
6
7
8
9
10
11
12
X[2,:] = 36
X, A, B

(tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[36., 36., 36., 36.]]),
array([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[36., 36., 36., 36.]], dtype=float32),
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
1
2
3
4
5
6
7
8
9
10
11
12
A[1,:] = 1
X, A, B

(tensor([[ 0., 1., 2., 3.],
[ 1., 1., 1., 1.],
[36., 36., 36., 36.]]),
array([[ 0., 1., 2., 3.],
[ 1., 1., 1., 1.],
[36., 36., 36., 36.]], dtype=float32),
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))
1
2
3
4
5
6
7
8
9
10
11
12
B[0,3] = 128
X, A, B

(tensor([[ 0., 1., 2., 3.],
[ 1., 1., 1., 1.],
[36., 36., 36., 36.]]),
array([[ 0., 1., 2., 3.],
[ 1., 1., 1., 1.],
[36., 36., 36., 36.]], dtype=float32),
tensor([[ 0., 1., 2., 128.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]]))

要(将大小为1的张量转换为Python标量),我们可以调用item函数或Python的内置函数。

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

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

数据预处理

为了能用深度学习来解决现实世界的问题,我们经常从预处理原始数据开始, 而不是从那些准备好的张量格式数据开始。 在Python中常用的数据分析工具中,我们通常使用pandas软件包。 像庞大的Python生态系统中的许多其他扩展包一样,pandas可以与张量兼容。 本节我们将简要介绍使用pandas预处理原始数据,并将原始数据转换为张量格式的步骤。 我们将在后面的章节中介绍更多的数据预处理技术。

读取数据集

举一个例子,我们首先(创建一个人工数据集,并存储在CSV(逗号分隔值)文件) …/data/house_tiny.csv中。 以其他格式存储的数据也可以通过类似的方式进行处理。 下面我们将数据集按行写入CSV文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import pandas as pd
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')
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
9
inputs, outputs = data.iloc[:, 0:2], data.iloc[:, 2]
inputs = inputs.fillna(inputs.mean())
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”。 巷子类型为“Pave”的行会将“Alley_Pave”的值设置为1,“Alley_nan”的值设置为0。 缺少巷子类型的行会将“Alley_Pave”和“Alley_nan”分别设置为0和1。

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

转换为张量格式

1
2
3
4
5
6
7
8
9
import torch
X, y = torch.tensor(inputs.values), torch.tensor(outputs.values)
X, y

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

线性代数

在数学表示法中,将向量记为粗体的小写的符号。而向量的任一元素是标量,不用加粗。
大量文献认为列向量是向量的默认方向。

长度,维度和形状

向量只是一个数字数组,就像每个数组都有一个长度一样,每个向量也是如此。在数学表示法中,如果我们想说一个向量x\mathbf{x}nn个实值标量组成,我们可以将其表示为xRn\mathbf{x}\in\mathbb{R}^n。向量的长度通常称为向量的维度(dimension)。
与普通的Python数组一样,我们可以通过调用Python的内置len()函数来访问张量的长度。
当用张量表示一个向量(只有一个轴)时,我们也可以通过.shape属性访问向量的长度。
形状(shape)是一个元素组,列出了张量沿每个轴的长度(维数)。对于(只有一个轴的张量,形状只有一个元素。)

1
2
3
4
5
import torch
x=torch.arange(4)
len(x), x.shape

4, torch.Size([4])

请注意,维度(dimension)这个词在不同上下文时往往会有不同的含义,这经常会使人感到困惑。
为了清楚起见,我们在此明确一下:
向量的维度被用来表示向量的长度,即向量或轴的元素数量。
然而,张量的维度用来表示张量具有的轴数。
在这个意义上,张量的某个轴的维数就是这个轴的长度。

矩阵与张量

正如向量将标量从零阶推广到一阶,矩阵将向量从一阶推广到二阶。矩阵,我们通常用粗体、大写字母来表示(例如,X\mathbf{X}Y\mathbf{Y}Z\mathbf{Z}),在代码中表示为具有两个轴的张量。而张量,为我们提供了描述具有任意数量轴的n维数组的通用方法。

范数

:label:subsec_lin-algebra-norms

线性代数中最有用的一些运算符是范数(norm)。非正式地说,一个向量的范数告诉我们一个向量有多大。这里考虑的大小(size)概念不涉及维度,而是分量的大小。

在线性代数中,向量范数是将向量映射到标量的函数ff。给定任意向量x\mathbf{x},向量范数要满足一些属性。
第一个性质是:如果我们按常数因子α\alpha缩放向量的所有元素,其范数也会按相同常数因子的绝对值缩放:

f(αx)=αf(x).f(\alpha \mathbf{x}) = |\alpha| f(\mathbf{x}).

第二个性质是我们熟悉的三角不等式:

f(x+y)f(x)+f(y).f(\mathbf{x} + \mathbf{y}) \leq f(\mathbf{x}) + f(\mathbf{y}).

第三个性质简单地说范数必须是非负的:

f(x)0.f(\mathbf{x}) \geq 0.

这是有道理的。因为在大多数情况下,任何东西的最小的大小是0。
最后一个性质要求范数最小为0,当且仅当向量全由0组成。

i,[x]i=0f(x)=0.\forall i, [\mathbf{x}]_i = 0 \Leftrightarrow f(\mathbf{x})=0.

你可能会注意到,范数听起来很像距离的度量。如果你还记得欧几里得距离和毕达哥拉斯定理,那么非负性的概念和三角不等式可能会给你一些启发。事实上,欧几里得距离是一个L2L_2范数:
假设nn维向量x\mathbf{x}中的元素是x1,,xnx_1,\ldots,x_n,其[L2L_2范数是向量元素平方和的平方根:]

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

其中,在L2L_2范数中常常省略下标22,也就是说x\|\mathbf{x}\|等同于x2\|\mathbf{x}\|_2
在代码中,我们可以按如下方式计算向量的L2L_2范数。

在深度学习中,我们更经常地使用L2L_2范数的平方。你还会经常遇到L1L_1范数,它表示为向量元素的绝对值之和:

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

L2L_2范数相比,L1L_1范数受异常值的影响较小。为了计算L1L_1范数,我们将绝对值函数和按元素求和组合起来。

L2L_2范数和L1L_1范数都是更一般的LpL_p范数的特例:

xp=(i=1nxip)1/p.\|\mathbf{x}\|_p = \left(\sum_{i=1}^n \left|x_i \right|^p \right)^{1/p}.

类似于向量的L2L_2范数,[矩阵]XRm×n\mathbf{X} \in \mathbb{R}^{m \times n}(Frobenius范数(Frobenius norm)是矩阵元素平方和的平方根:)

XF=i=1mj=1nxij2.\|\mathbf{X}\|_F = \sqrt{\sum_{i=1}^m \sum_{j=1}^n x_{ij}^2}.

Frobenius范数满足向量范数的所有性质,它就像是矩阵形向量的L2L_2范数。

例题

微积分题

绘制函数y=f(x)=x31xy = f(x) = x^3 - \frac{1}{x}和其在x=1x = 1处切线的图像。

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
51
52
53
54
55
56
57
58
59
60
61
%matplotlib inline
import numpy as np
from matplotlib_inline import backend_inline
from d2l import torch as d2l

def numerical_lim(f, x, h):
return (f(x + h) - f(x)) / h

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

#@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()

#@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)

微积分例题1

自动求导例题

使f(x)=sin(x)f(x)=\sin(x),绘制f(x)f(x)df(x)dx\frac{df(x)}{dx}的图像,其中后者不使用f(x)=cos(x)f'(x)=\cos(x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
import torch
import numpy as np

t=torch.arange(-2*np.pi, 2*np.pi, 0.02*np.pi, requires_grad=True)
y0=torch.zeros(len(t))
dy=torch.zeros(len(t))
for i in range(len(t)):
m=t[i]
n=torch.tensor([m])
n.requires_grad_(True)
y=torch.sin(n)
y.backward()
y0[i]=y
dy[i]=n.grad
n.grad.zero_()

plt.figure(figsize=(16, 9))
plt.plot(t.detach().numpy(),y0.detach().numpy(),label='f(x)=sin(x)')
plt.plot(t.detach().numpy(),dy.detach().numpy(),label="f'(x)")
plt.legend(loc=0) #保证图例正常显示,loc=0表示在左下角

自动求导例题2