PyTorch 的重要组成部分是张量类(Tensor),即NN 维数组。

张量是数学中的概念,可以理解为:list是张量在计算机中实现的一种数据结构

引入方式:

1
import torch

它与 Numpy 的 ndarray 类似,然而却能够得到GPU的支持加速计算,并且能支持自动微分。

它包含 datagrad(属于 Tensor

Tensor 中的 data,就避免建立计算图(不再计算梯度);同样,取 Tensorgrad.item(),也能够避免建立计算图

入门

创建

创建行向量,除非额外指定,否则新张量存储在内存中,并采用基于CPU的计算

在深度学习中,浮点数尽量使用32

1
2
3
4
5
6
# 包含从0开始的前15个整数,默认创建为浮点数
x = torch.arange(15)
print(x)
# 传入Python嵌套列表,最外层列表对应于轴0,内层列表对应轴1
y = torch.tensor([[2, 1, 4, 3], [0, -2, 3, 4]])
print(y)
1
2
3
tensor([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
tensor([[ 2, 1, 4, 3],
[ 0, -2, 3, 4]])

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

1
2
3
print(torch.zeros((2, 3, 4))) # 三维度的全零矩阵
print(torch.ones((1, 5))) # 两维度的全1矩阵
print(torch.zeros_like(x)) # 形状与x相同,元素全为0的矩阵
1
2
3
4
5
6
7
8
9
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.]]])

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

创建张量,其每个元素满足都从均值为0、标准差为1的标准高斯分布中随机采样:

1
torch.randn(2, 3)
1
2
tensor([[ 0.1094,  0.1251, -0.0131],
[ 0.9402, -1.3864, 0.8064]])

张量的拷贝,能够使用 .clone(),该方法能够分配新内存,将下面的A\mathbf{A} 分配给B\mathbf{B}

1
2
3
4
A = torch.arange(20, dtype=torch.float32).reshape(5, 4)
B = A.clone()
A[3][3] = 666666
A, B
1
2
3
4
5
6
7
8
9
10
(tensor([[0.0000e+00, 1.0000e+00, 2.0000e+00, 3.0000e+00],
[4.0000e+00, 5.0000e+00, 6.0000e+00, 7.0000e+00],
[8.0000e+00, 9.0000e+00, 1.0000e+01, 1.1000e+01],
[1.2000e+01, 1.3000e+01, 1.4000e+01, 6.6667e+05],
[1.6000e+01, 1.7000e+01, 1.8000e+01, 1.9000e+01]]),
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.],
[16., 17., 18., 19.]]))

形状

向量的长度,又称向量的维度,可通过Python内置 len() 来访问:

1
2
a = torch.arange(3)
a, len(a)
1
(tensor([0, 1, 2]), 3)

改变张量的形状而不改变元素数量和元素值:reshape()

1
2
3
ttt = torch.arange(24).reshape(2, 4, 3)
ttt.shape, ttt.size(), ttt.numel()
# 张量的形状,返回元组 张量元素总数(number of element),返回的是标量
1
torch.Size([2, 4, 3]), torch.Size([2, 4, 3]), 24

.numel() 十分常用!

⭐张量在给出其他部分维度数据后能够传入参数 -1 ,自动计算出另一个维度:

1
2
X = x.reshape(-1, 4)
Y = x.reshape(3, -1)

索引与切片

通过指定数组下标索引,能够对元素进行读写操作

1
2
3
4
5
6
x = torch.arange(15).reshape(3, 5)
x[0:2, :] = 23333 # 通过指定索引来修改某一元素
x[0, 2] = 66666
print(x)
print(x[-1]) # 取最后一行,即第三行
print(x[1:3]) # 取第一行及第二行的元素
tensor([[23333, 23333, 23333, 23333, 23333],
        [23333, 23333, 23333, 23333, 23333],
        [   10,    11,    12,    13,    14]])
tensor([10, 11, 12, 13, 14])
tensor([[23333, 23333, 23333, 23333, 23333],
        [   10,    11,    12,    13,    14]])

基本运算

标准算术运算符(+-*/**)均升级为按元素运算 ,即运算符应用于数组中每一个元素。

对于将两个数组作为输⼊的函数,按元素运算将⼆元运算符应⽤于两个数组中的每对位置对应的元素

1
2
3
x = torch.tensor([1, 9.3232, -3, 0])
y = torch.tensor([8, -2, -3, -3.4])
x + y, x / y, x ** y, x == y
(tensor([ 9.0000,  7.3232, -6.0000, -3.4000]),
 tensor([ 0.1250, -4.6616,  1.0000, -0.0000]),
 tensor([ 1.0000,  0.0115, -0.0370,     inf]),
 tensor([False, False,  True, False]))

当两个张量的形状不同,会调用广播机制(broadcasting mechansim)来执行按元素操作:

1
2
3
4
5
a = torch.arange(3).reshape((3, 1))
b = torch.arange(2).reshape((1, 2))
print(a)
print(b)
print(a + b)
tensor([[0],
        [1],
        [2]])
tensor([[0, 1]])
tensor([[0, 1],
        [1, 2],
        [2, 3]])

此外,还能够调用其他方法进行初等运算

1
2
torch.exp(x), x.sum() 
#求幂 求和,并且返回元素只有一个的张量
tensor([2.7183e+00, 1.1195e+04, 4.9787e-02, 1.0000e+00]), tensor(7.3232)

矩阵运算

通过两个分量mmnn 来创建一个形状为m×nm \times n 的矩阵,通过 .T 即可获得矩阵的转置

B=AT\mathbf{B} = \mathbf{A}^T,则对于任意的iijj ,都有bij=ajib_{ij} = a_{ji}

1
2
tmp = torch.arange(16).reshape(4, 4)
tmp, tmp.T, tmp.T == tmp
1
2
3
4
5
6
7
8
9
10
11
12
tensor([[ 0,  1,  2,  3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
tensor([[ 0, 4, 8, 12],
[ 1, 5, 9, 13],
[ 2, 6, 10, 14],
[ 3, 7, 11, 15]])
tensor([[ True, False, False, False],
[False, True, False, False],
[False, False, True, False],
[False, False, False, True]])

两个矩阵的按元素乘法,称为 哈达玛积(数学符号为\odot

1
2
3
4
5
a = torch.arange(6).reshape(2, 3)
b = torch.tensor([[3, 5, 3],
[9, -3, 0]])
x = -1 # 标量
a * b, a + x, (a * x).shape
1
2
3
4
5
tensor([[  0,   5,   6],
[ 27, -12, 0]]),
tensor([[-1, 0, 1],
[ 2, 3, 4]]),
torch.Size([2, 3])

点积(Dot Product),对于向量而言,即在相同位置上按元素乘积并在最后进行求和,即:

两个向量x,yRd\mathbf{x},\mathbf{y} \in \mathbb{R} ^ dxTy=idxiyi\mathbf{x}^T\mathbf{y} = \sum^d_i{x_iy_i}

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

两个矩阵进行矩阵-矩阵乘法(matrix-matrix multiplication),简称为矩阵乘法,可使用 .mm()

1
2
3
A = torch.arange(16, dtype=torch.float32).reshape(4, 4)
B = torch.ones(4, 3)
A, B, torch.mm(A, B)
1
2
3
4
5
6
7
8
9
10
11
12
tensor([[ 0.,  1.,  2.,  3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[12., 13., 14., 15.]]),
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]]),
tensor([[ 6., 6., 6.],
[22., 22., 22.],
[38., 38., 38.],
[54., 54., 54.]])

降维

默认情况下,调用求和函数 .sum() 会沿所有的轴降低张量的维度,使它变为一个标量。

此外,我们可以指定张量沿哪一个轴来通过求和降低维度,指定 axis 等于多少,则相当于使该张量在该轴的维数在输出形状中消失。

1
2
3
4
5
6
7
8
9
10
# 每一个求和的结果,是按第0维进行遍历求和得到的。
b_sum_0 = b.sum(axis = 0)

# 每一个求和结果,是按1轴的方向(遍历第1维)进行求和
b_sum_1 = b.sum(axis = 1)

# 求和的同时不丢掉原来的维度(keepdims = True),常用作广播运算
b_sum_kpt = b.sum(axis = 1, keepdims = True)

b, b_sum_0, b_sum_1, b_sum_kpt, b / b_sum_kpt
1
2
3
4
5
6
7
8
tensor([[ 3,  5,  3],
[ 9, -3, 0]]),
tensor([12, 2, 3]),
tensor([11, 6]),
tensor([[11],
[ 6]]),
tensor([[ 0.2727, 0.4545, 0.2727],
[ 1.5000, -0.5000, 0.0000]])

范数

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

x2=i=1nxi2\|x\|_{2}=\sqrt{\sum_{i=1}^{n} x_{i}^{2}}

L1L_1范数是向量元素的绝对值之和:

x1=i=1nxi\|x\|_{1}=\sum_{i=1}^{n}\left|x_{i}\right|

对于矩阵而言,弗罗贝尼乌斯范数,是矩阵元素的平方和的平方根(类似于L2L_2范数):

XF=i=1mj=1nxij2\|X\|_{F}=\sqrt{\sum_{i=1}^{m} \sum_{j=1}^{n} x_{i j}^{2}}

1
2
3
4
u = torch.tensor([3.0, -4.0])
print(torch.norm(u)) # L2范数
print(torch.abs(u).sum()) # L1范数
print(torch.norm(torch.ones(2, 3))) # 弗罗贝尼乌斯范数
1
2
3
tensor(5.)
tensor(7.)
tensor(2.4495)

连接

将多个张量连接在一起,即将其端对端地叠起来形成一个更大的张量

1
2
3
4
5
6
7
8
x = torch.arange(12, dtype = torch.float32).reshape((3, 4)) 
print(x)
y = torch.tensor([[2, 0, 4, 3],
[1, 2, 3, 4],
[2, -3, 2, 0]])
print(y)
torch.cat((x, y), dim = 0), torch.cat((x, y), dim = 1)
# 按第0维的方向进行连接,和按第1维的方向进行连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tensor([[ 0.,  1.,  2.,  3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.]])
tensor([[ 2, 0, 4, 3],
[ 1, 2, 3, 4],
[ 2, -3, 2, 0]])
tensor([[ 0., 1., 2., 3.],
[ 4., 5., 6., 7.],
[ 8., 9., 10., 11.],
[ 2., 0., 4., 3.],
[ 1., 2., 3., 4.],
[ 2., -3., 2., 0.]]),
tensor([[ 0., 1., 2., 3., 2., 0., 4., 3.],
[ 4., 5., 6., 7., 1., 2., 3., 4.],
[ 8., 9., 10., 11., 2., -3., 2., 0.]])

转换为其他Python对象

Tensor与Numpy张量的转换:

1
2
3
q = x.numpy()
p = torch.tensor(q)
type(q), type(p)
(numpy.ndarray, torch.Tensor)

将大小为1的张量转化Python的标量,可以调用 item() 或者 Python的内置函数

1
2
tmp = torch.tensor([2.33333])
tmp, tmp.item(), float(tmp)
(tensor([2.3333]), 2.333329916000366, 2.333329916000366)

.item() 十分常用

数据预处理

创建数据集

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

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

1
2
import pandas as pd
data = pd.read_csv(data_file)

为处理缺失(NaN项)的数据,方法有:插值(即用替代值来代替缺失值)、删除(忽略缺失值)

1
# 代码待补充

批量加载数据集⭐

在机器学习中,若要使用批量随机梯度下降法(SGD),使用数据加载器是必不可少的,它能够按照需要批量地加载数据至内存,而不是直接加载数据集所有数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import torch
from torch.utils.data import Dataset # 抽象类,需要有一个继承类
from torch.utils.data import DataLoader # 用于批量加载数据

# 自定义数据集
class DiabetesDataset(Dataset): # 继承自Dataset,需要覆写以下方法
def __init__(self, filepath):
xy = np.loadtxt(filepath, delimiter=',', dtype=np.float32)
self.len = xy.shape[0]
self.x_data = torch.from_numpy(xy[:, :-1])
self.y_data = torch.from_numpy(xy[:, [-1]])

def __getitem__(self, index): # 能够实现中括号下标访问
return self.x_data[index], self.y_data[index]

def __len__(self): # 返回数据集的长度
return self.len

dataset = DiabetesDataset('...') #创建实例
train_loader = DataLoader(dataset = dataset, # 传入数据集
batch_size=32, # 批量大小
shuffle=True, # 是否打乱
num_workers=2) # 批量读入时的进程数

如何通过 DataLoader 遍历数据集所有数据 ?

1
2
3
4
5
6
# DataLoader是一个Iterable,但可以通过iter()来获得一个Iterator对象
next(iter(train_loader)) # 获取小批量数据

# for循环本质上通过不断调用next()函数实现
for i, data in enumerate (train_loader, 0): # 第二个参数为下标起始位置
#....

enumerate() 能将可遍历的数据对象组合为一个带索引的序列,即同时返回数据和数据下标。此外,通过其下标索引,还能够批量修改列表内的元素

若要使用一些标准数据集,如MNIST、Fashion-MNIST等,可通过PyTorch框架中的内置函数将其下载并读取到内存中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
batch_size = 64

# 无需再自定义Dataset的子类
train_dataset = datasets.MNIST(root = '../dataset/mnist/', # 数据集的本地存放路径
train= True, # 训练集还是测试集
download = True, # 若未下载到本地,则需要选择True
transform = transform) # transform暂不在此处谈
test_dataset = datasets.MNIST(root = '../dataset/mnist',
train = False,
download = True,
transform = transform)
train_loader = DataLoader(train_dataset,
shuffle = True,
batch_size = batch_size)
test_loader = DataLoader(test_dataset,
shuffle = False,
batch_size = batch_size)