Pytorch学习-Torch与autograd

Tensor

Tensor基础

Tensor,又名张量。 可以简单认为Tensor是一个支持高效科学计算的数组。它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵、黑白图像等)、或者更高维的数组(高阶数据、视频等)。它与Numpy的ndarray类似,但Tensor支持GPU加速。

基本操作

与Numpy的接口设计比较类似。

  • 比如torch.sum(a,b)s.sum(b)是等价的
  • 从修改存储角度,分为两类操作:
  • 不会修改自身存储的数据的操作,如a.add(b),加法结果会返回一个新的tensor
  • 会修改自身存储的数据的操作,如a.ddd_(b),有个下划线,加法的结果会被存储在a中返回

1.创建Tensor

创建方法很多,常用的如下表:

函数 功能
Tensor(*sizes) 基础构造函数
tensor(data,) 类似np.array的构造函数
ones(*sizes) 全1Tensor
zeros(*sizes) 全0Tensor
eye(*sizes) 对角线为1,其他为0
arange(s,e,step) 从s到e,步长为step
linspace(s,e,steps) 从s到e,均匀切分成steps份
rand/randn(*sizes) 均匀/标准分布
normal(mean,std)/uniform(from,to) 正态分布/均匀分布
randperm(m) 随机排列

上表中创建数据类型的同时可以指定数据类型dtype存储设备device

创建未初始化的 5,3 维度的Tensor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 创建未初始化的 5,3 维度的Tensor
x = t.empty(5, 3)
print(x)
# 输出
tensor([[1.5695e-43, 1.5554e-43, 1.5975e-43],
        [1.3593e-43, 1.5975e-43, 1.4714e-43],
        [1.5134e-43, 1.6956e-43, 4.4842e-44],
        [1.6395e-43, 1.5414e-43, 1.3593e-43],
        [1.6535e-43, 1.3593e-43, 1.4714e-43]])
 

创建一个5x3的随机初始化的Tensor:

1
2
x = torch.rand(5, 3)
print(x)

创建一个5x3的long型全0的Tensor:

1
2
x = torch.zeros(5, 3, dtype=torch.long)
print(x)

直接根据数据创建:

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

#输出
tensor([[5.5000, 3.0000],
        [1.0000, 2.0000]])

输入数据是一个Tensor:

1
2
x = torch.Tensor(torch.rand(2,6))
x

指定形状:

1
2
x = torch.Tensor(3,7)
x

通过现有的Tensor来创建: 此方法会默认重用输入Tensor的一些属性,例如数据类型,除非自定义数据类型

1
2
3
4
5
6

x = x.new_ones(5, 3, dtype=torch.float64)  # 返回的tensor默认具有相同的torch.dtype和torch.device
print(x)

x = torch.randn_like(x, dtype=torch.float) # 指定新的数据类型
print(x) 

torch.ones()相关

1
2
3
4
5
6
7
torch.ones(2,3) # 创建形状2,3的全1的tensor

torch.zeros(2,3) # 创建形状2,3的全0的tensor

torch.ones_like(input_t) # 创建一个跟输入tensor形状一样的全1的tensor

torch.eye(2,3, dtype=t.int) # 创建一个对角线值为1,其余值为0的tensor

torch.Tensor()与torch.tensor()区别:

  • torch.Tensor()是类,默认是torch.FloatTensor()
  • torch.tensor()是函数,data支持list/tuple/array/scalar等类型。直接从data进行数据复制,并根据源数据的类型生成对应类型的Tensor。且其接口与numoy更像,所以更推荐这种创建方法
1
2
3
4
5
6
# torch.Tensor()能直接创建空的张量
torch.Tensor()

# torch.tensor()不能创建空的,必须传入一个数据
torch.tensor() # 报错:TypeError: tensor() missing 1 required positional arguments: "data"
torch.tensor(())  # 等效与创建空的

创建一个起始值为1,终止值为19,步长为1的tensor:

1
torch.arange(1, 19, 3)

创建一个区间内3等份是tensor:

1
torch.linspace(1,10,3)

创建一个形状的tensor,取值是标准正态分布中抽取的随机值:

1
torch.randn(2,3)

长度为5,随机排列的:

1
torch.randperm(5)

创建一个大小为(2,3),值全1的tensor,保留原始的数据类型和设备:

1
2
a = tensor.tensor((), dtype=tensor.int32)
a.new_ones((2,3))

通过shape或者size()来获取Tensor的形状:

1
2
3
4

print(x.size()) # 注意:返回的torch.Size其实就是一个tuple, 支持所有tuple的操作。

print(x.shape)

统计元素总数:

1
2
3
a.numel()

a.nelement()

2.Tensor的类型

设备类型:

  • cuda
  • cpu t.dtype

据类型: 每个数据类型都有CPU和GPU版本 t.device

不同数据类型Tensor之间相互转换的方法:

  • 如:tensor.type(new_type),快捷方法tensor.float(),tensor.half()
  • 设备类型转换:t.cuda(),t.cpu(),或t.to(divice)
  • t.*_like(t1)生成和t1相同属性的新tensor,t.new_*(new_shape)生成和相同属性但是相撞不同的新tensor,t1.as_type(t2)修改tensor的类型
1
2
3
4
5
6
7
8
# 以下代码只有在PyTorch GPU版本上才会执行
if torch.cuda.is_available():
    device = torch.device("cuda")          # GPU
    y = torch.ones_like(x, device=device)  # 直接创建一个在GPU上的Tensor
    x = x.to(device)                       # 等价于 .to("cuda")
    z = x + y
    print(z)
    print(z.to("cpu", torch.double))       # to()还可以同时更改数据类型

3.索引

Note:索引出来的结果与原数据共享内存,也即修改一个,另一个会跟着修改。 比如:

1
2
3
4
5
6
7
8
y = x[0, :]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了

# 输出
tensor([1.4963, 1.1113, 1.8689, 1.4671, 1.9555, 1.4945])
tensor([1.4963, 1.1113, 1.8689, 1.4671, 1.9555, 1.4945])

常见索引:

1
2
3
4
5
6
7
8
9
"""
对于x
tensor([[1.4963, 1.1113, 1.8689, 1.4671, 1.9555, 1.4945],
        [0.7284, 0.2248, 0.1715, 0.6737, 0.0110, 0.6075]])
"""

x[0] # 第一行
x[:, 1] # 第二列
x[1, -2:]  # 第二行最后两个元素
1
2
3
4
5
6
7
# 返回布尔值
print(x>1)
tensor([[ True,  True,  True,  True,  True,  True],
        [False, False, False, False, False, False]])


print((x>1).int())

返回满足条件的结果:

1
2
3
4
5
print(x[x>1]) # 返回的结果不共享内存
print(x.masked_select(x>1))

# where保留原始索引,不满足条件位置置0
print(torch.where(x>1), x, torch.zeros_like(x))

除了常用索引来选择数据,还有一些高级的选择函数:

级的选择函数:
函数 功能
index_select(input, dim, index) 在指定维度dim上选取,比如选取某些行、某些列
masked_select(input, mask) 例子如上,a[a>0],使用ByteTensor进行选取,选取结果不共享内存
nonzero(input) 非0元素的下标
gather(input, dim, index) 根据index,在dim维度上选取数据,输出的size与index一样

gather:

1
2
3
4
5
6
7
8
9
a = torch.arange(0,16).view(4,4)

# 选择对角线上元素
index = t.tensor([0,1,2,3])
a.gather(0, index)

# 选择反对角线上元素
index = t.tensor([3,2,1,0]).t()
a.gather(1, index)

gather的逆操作:scatter_。gather将数据从input中按index取出;scatter_将按照index数据写入

scatter_是inplace操作,会直接对当前数据进行修改

item():将一个标量Tensor转换成一个Python number:

1
2
3
4
# 只针对包含一个元素的tensor有效
x = torch.randn(1)
print(x)
print(x.item())

4.拼接

  • cat(tensor,dim):将多个tensor在指定维度上进行拼接
  • stack(tensor,dim):将多个tensor沿一个新的维度进行拼接
 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
a = torch.arange(0,16).view(2,8)

# cat执行在dim=0上拼接
torch.cat((a,a), 0)
#结果
tensor([[ 0,  1,  2,  3,  4,  5,  6,  7],
        [ 8,  9, 10, 11, 12, 13, 14, 15],
        [ 0,  1,  2,  3,  4,  5,  6,  7],
        [ 8,  9, 10, 11, 12, 13, 14, 15]])

# cat执行在dim=1上拼接
torch.cat((a,a), 0)
#结果
tensor([[ 0,  1,  2,  3,  4,  5,  6,  7,  0,  1,  2,  3,  4,  5,  6,  7],
        [ 8,  9, 10, 11, 12, 13, 14, 15,  8,  9, 10, 11, 12, 13, 14, 15]])



# stack在dim=0上拼接
b = torch.stack((a,a), 0)
#结果
tensor([[[ 0,  1,  2,  3,  4,  5,  6,  7],
         [ 8,  9, 10, 11, 12, 13, 14, 15]],

        [[ 0,  1,  2,  3,  4,  5,  6,  7],
         [ 8,  9, 10, 11, 12, 13, 14, 15]]])
#形状
print(b.shape)
torch.Size([2, 2, 8])

高级索引

不与原tensor共享内存

1
2
3
4
a = torch.arange(0,16).view(2,2,4)

a[[1,0], [1,1], [2,0]] # a[1,1,2] 和a[0,1,0]
# tensor([14,  4])

5.逐元素&归并操作

常用逐元素操作: 20220905104714

举例:

1
clamp(torch.clamp(a,min=2,max=4)) # 上下截断

常用归并操作: 20220905105000 大多数执行归并函数都有一个维度参数dim,指定在哪个维度上进行 关于dim的解释有点乱,下面是经验总结(并非所有函数都符号如下变化): 假设输入形状(m,n,k):

  • 如果指定dim=0,那么输出形状为(1,n,k)或(n,k)
  • 如果指定dim=1,那么输出形状为(m,1,k)或(m,k)
  • 如果指定dim=2,那么输出形状为(m,n,1)或(m,n)

维度中是否有1,取决于参数keepdim是否为True(保留维度)

1
2
3
4
5
6
7
8
9
(b.sum(dim=0, keepdim=False)).shape #torch.Size([3])
(b.sum(dim=0, keepdim=True)).shape # torch.Size([1, 3])



#
b.sum(dim=0) # tensor([2., 2., 2.])

b.sum(dim=1) # tensor([3., 3.])

6.比较

20220905105929

以max为例,有三种情况:

  • max(input)
  • max(input, dim) 指定维度返回最大值
  • max(tensor1, tensor2) 返回两个tensor上对应位置较大的元素

比较两个整形tensor可以用==比较,对于有精度限制的浮点数需要使用allclose比较

7.算术操作

  • 加法: add()
  • 减法: sub
  • 乘法: multiply
  • 除法: div
  • 倒数: reciprocal

同一个操作有很多种形式,以加法为例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
## 形式1
y = torch.rand(5, 3)
print(x + y)


## 形式2
print(torch.add(x, y))

# 还可以指定输出
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print(result)


# 形式3:inplace
# adds x to y
y.add_(x)
print(y)

注:PyTorch操作inplace版本都有后缀_, 例如x.copy_(y), x.t_()

改变形状

查看Tensor的维度

  • tensor.shape
  • tensor.size() 与上面等价
  • tensor.dim() 查看维度,等价于 len(tensor.shape),对应Numpy的array.ndim
  • tensor.numel() 查看元素数量,等价于tensor.shape[0]*tensor.shape[1]…或者np.prod(tensor.shape),对应Numpy中的array.size

改变维度:

reshape()

会自动先把内存中不连续的Tensor变成连续的,然后进行形状变化等价于tensor.contiguous().view(new_shape)

view()

仅能处理空间中连续的Tensor,经过view之后的Tensor仍共享存储区 如果原Tensor在空间上不连续,会报错

view()来改变Tensor的形状:

1
2
3
4
5
y = x.view(15)
z = x.view(-1, 5)  # -1所指的维度可以根据其他维度的值推出来
print(x.size(), y.size(), z.size())

# 输出:torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])

注意view()返回的新Tensor与源Tensor虽然可能有不同的size,但是是共享data的,也即更改其中的一个,另外一个也会跟着改变。(顾名思义,view仅仅是改变了对这个张量的观察角度,内部数据并未改变)

reshape()此函数并不能保证返回的是其拷贝。推荐先用clone创造一个副本然后再使用view

1
2
3
4
5
x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)
# 使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor。

常用快捷变形方法:

  • tensor.view(dim1, -1, dim2) 指定其中一个维度为-1,pytorchhi自动计算对应形状
  • tensor.view_as(other) 把形状变得跟另一个一样,等价于tensor.view(other.shape)
  • tensor.squeeze() 将tensor中尺寸为1的维度减掉。例如:形状(1,3,1,4)会变为(3,4)
  • tensor.flatten(start_dim=0, end_dim=-1) 将tensor形状中的某些连续的维度合并为一个维度。例如,(2,3,4,5)会变为(2,12,5)
  • tensor[None]和tensor.unsqueeze(dim):为tensor新建一个维度,该维度尺寸为1

Tensor的转置

  • transpose: 只能用于两个维度的转置
  • permute:可以对任意高纬度矩阵进行转置
  • tensor.t()
  • tensor.T

转置会使得tensor在空间上不连续,此时最好通过tensor.contiguous()将其变成连续的

1
2
3
4
# C * H * W
img1 = torch.randn(3,128,256)

img2 = img1.permute(1,2,0) # 代表 H,W,C的索引来进行转置

9.其他操作

常用的线性代数基本操作 20220905110224

此外,在torch.distributions中还提供了可自定义参数的概率分布函数和采样函数。

Tensor与Numpy相互转换

我们很容易用numpy()from_numpy()将Tensor和NumPy中的数组相互转换。但是需要注意的一点是: 这两个函数所产生的的Tensor和NumPy中的数组共享相同的内存(所以他们之间的转换很快),改变其中一个时另一个也会改变!! torch.tensor()会进行数据拷贝,但会消耗更多时间和空间

Tensor转Numpy:numpy

1
2
3
4
5
6
7
8
a = torch.ones(5)
b = a.numpy()
print(a, b)

a += 1
print(a, b)
b += 1
print(a, b)

Numpy转Tensor:from_numpy

1
2
3
4
5
6
7
8
9
import numpy as np
a = np.ones(5, dtype=np.float32)
b = torch.from_numpy(a) # dtype为float32,所以共享内存(修改b的值a也会变)
print(a, b)

a += 1
print(a, b)
b += 1
print(a, b)

注意1: 使用torch.Tensor()创建的张量默认是float32,如果numpy的数据类型与默认的不一致,那么数据仅仅会被复制,不会共享内存

注意2: 无论输入什么类型,torch.tensor()都只进行数据复制,不会共享内存,要注意

1
2
3
a_tensor = torch.tensor(a)
a_tensor[0,1] = 111
a_tensor   # a和s_tendor不共享内存

PyTorch还提供了torch.utils.dlpack模块,仍是共享内存的

所有在CPU上的Tensor(除了CharTensor)都支持与NumPy数组相互转换。

命名张量

允许用户将显式名称与Tensor的维度关联起来,便于其他操作,推荐使用维度代名词进行维度操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import warning
warning.filter("ignore")

imgs = torch.randn(1,2,2,3, names=('N', 'C', 'H', 'W'))
imgs.names

# 输出 ('N', 'C', 'H', 'W')

# 旋转
imgs_rotate = imgs.transpose(2,3)
imgs_rotate.names

# 修改部分维度的名称 rename(H='Height')

# 对未命名的张量命名,不需要的用None表示 refine_names('N','W',None,'C')

# 通过维度的名词进行维度变换
align_to()

命名张量可以提高安全性,如果两个张量维度相同但是Tensor的维度名称没有对齐,那么也无法进行计算

1
2
3
imgs = torch.randn(1,2,2,3, names=('N', 'C', 'H', 'W'))
imgs2 = torch.randn(1,2,2,3, names=('C', 'N', 'W', 'H'))
# img2 + imgs2 会报错

Tensor的基本结构

分为:

  • 信息区(Tensor): 头信息区主要保存size、Stride、数据类型等
  • 存储区(Storeage):真正的数据被保存册亨连续数组

绝大多数操作不是修改Tensor的存储区,而是修改Tensor的头信息(更节省内存,提升了速度)。此外,有些操作会导致Tensor不联系,这需要调用tensor.contiguous()变成连续的数据,该方法会复制数据到新内存,不再与原来的数据共享存储区。

使用Torch从零实现线性回归

 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
62
63
64
65
"""
不使用深度学习框架的技术,只使用Torch从零实现线性回归
"""
import torch
import torch as t
import matplotlib.pyplot as plt


device = t.device('cpu')  # 如果使用GPU就修改

# 设置随机种子,保证在不同机器上运行时输出一致
t.manual_seed(2022)


def get_fake_dta(batch_size=8):
    '''
    产生随机数据:y=2x+3,加上一些噪声
    :param batch_size:
    :return: x, y
    '''
    # 均值为batch_size,方差为1
    x = t.rand(batch_size, 1, device=device) * 5
    y = x*2+3 + t.rand(batch_size, 1, device=device)
    return x, y


# 随机初始化参数
w = t.rand(1, 1).to(device)
b = t.rand(1, 1).to(device)
lr = 0.02  # 学习率

for ii in range(500):
    x, y = get_fake_dta(batch_size=4)

    # 前向传播
    y_pred = x.mm(w) + b.expand_as(y)  # expand_as用到了广播机制
    loss = 0.5 * (y_pred -y) ** 2  # 均方误差
    loss = loss.mean()

    # 反向传播: 手动计算梯度
    dloss = 1
    dy_pred = dloss * (y_pred-y)  #

    dw = x.t().mm(dy_pred)  # 梯度
    db = dy_pred.sum()

    # 更新参数
    w.sub_(lr * dw)
    b.sub_(lr * db)

    if ii % 50 == 0:
        # 画图
        x = t.arange(0,6).float().view(-1,1)
        y = x.mm(w)+b.expand_as(x)
        plt.plot(x.numpy(), y.numpy())

        x2, y2 = get_fake_dta(batch_size=32)
        plt.scatter(x2.numpy(), y2.numpy())

        plt.xlim(0, 5)
        plt.ylim(0, 13)
        plt.show()
        plt.pause(0.5)

print(f"w:{w.item():.3f}, b:{b.item():.3f}")

可以看到还是稍微复杂的…

autograd

autograd自动求导,能够根据输入和前向传播过程自动构建计算图,执行反向传播。 只需要对Tensor增加requires_grad=True属性

几个简单例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
a = torch.randn(3,4, requires_grad=True)
a.requires_grad # True


b = torch.randn(3,4, requires_grad=True)
c = (a + b).sum() # c的requires_grad也被设置成了True
c.backward()
c  # tensor(4.4589, grad_fn=<SumBackward0>)
a.grad  # 梯度

c.grad_fn # 查看这个Tensor的反向传播函数:<SumBackward0 at 0x142081550>  这里是sum的,所以是这个函数名

c.grad_fn.next_functions  # 保存了frad_fn的输入(tuple)


## 叶子节点(is_leaf=True)
#requires_grad为False的时候就是叶子Tensor
#requires_grad为True,且是由用户创建的时候也是叶子Tensor,她的梯度信息会被保留下来
a.is_leaf  # True

autograd沿着计算图从根几点溯源,利用链式法则计算所有叶子节点的梯度。每个前向传播操作的函数都有阈值对应的反向传播函数来计算输入Tensor的梯度,这些函数的名字通常以Backward结尾

反向传播过程中,非叶子节点的梯度不会被保存,如果想查看这些变量的梯度,有两种办法:

  • 使用autograd.grad函数
  • 使用hook方法(推荐使用)

其他:

  • 由用户创建的节点叫做叶子节点,叶子节点的grad_fn为None。对于在叶子节点中需要求导的tensor,因为其梯度是累加的,所以剧透AccumulateGrad标识
  • Tensor默认不需要求导,如果一个节点的requires_grad被设置为True,那么所有依赖他的节点也是True
  • 多次反向传播过程中,梯度是不断累加的。多次反向传播,需要指定retain_graph=True来保存中间缓存
  • 反向传播中,非叶子节点的梯度不会被保存,可以使用autograd.grad或hook来获取
  • Tensor的grad与data形状一致,应该避免直接修改tensor.data

使用autograd从零实现线性回归

 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
62
63
64
65
66
67
"""
加上autograd从零实现线性回归
"""
import torch
import torch as t
import matplotlib.pyplot as plt
import numpy as np

device = t.device('cpu')  # 如果使用GPU就修改

# 设置随机种子,保证在不同机器上运行时输出一致
t.manual_seed(2022)


def get_fake_dta(batch_size=8):
    '''
    产生随机数据:y=2x+3,加上一些噪声
    :param batch_size:
    :return: x, y
    '''
    # 均值为batch_size,方差为1
    x = t.rand(batch_size, 1, device=device) * 5
    y = x*2+3 + t.rand(batch_size, 1, device=device)
    return x, y


# 随机初始化参数
w = t.rand(1, 1, requires_grad=True)
b = t.rand(1, 1, requires_grad=True)
lr = 0.03  # 学习率
losses = np.zeros(500)

for ii in range(500):
    x, y = get_fake_dta(batch_size=4)

    # 前向传播:计算loss
    y_pred = x.mm(w) + b.expand_as(y)  # expand_as用到了广播机制
    loss = 0.5 * (y_pred -y) ** 2  # 均方误差
    loss = loss.sum()
    losses[ii] = loss.item()

    # 反向传播: 自动计算梯度
    loss.backward()

    # 更新参数
    w.sub_(lr * w.grad.data)
    b.sub_(lr * b.grad.data)

    # 梯度清零
    w.grad.data.zero_()
    b.grad.data.zero_()

    if ii % 50 == 0:
        # 画图
        x = t.arange(0,6).float().view(-1,1)
        y = x.mm(w)+b.expand_as(x)
        plt.plot(x.numpy(), y.numpy())

        x2, y2 = get_fake_dta(batch_size=32)
        plt.scatter(x2.numpy(), y2.numpy())

        plt.xlim(0, 5)
        plt.ylim(0, 13)
        plt.show()
        plt.pause(0.5)

print(f"w:{w.item():.3f}, b:{b.item():.3f}")

稍微简单一些了……

0%