pytorch - 基础知识
由于张量是对数据的描述,在神经网络中通常将数据以张量的形式表示,如零维张量表示标量,一维张量表示向量,二维表示矩阵,三维张量表示图像,四维表示视频,这章首先介绍张量,然后介绍它的运算,再是核心包autograd自动微分。
张量
张量的简介
pytorch的torch.Tensor和Numpy的多维数组非常相似,由于tensor提供GPU的自动求梯度和计算,它更适合深度学习。
用dtype指定类型创建tensor,detype有tensor.float/long等
1
torch.tensor(1,dtype=torch.int8)
使用指定类型随机初始化
1
torch.IntTensor(2,3)
tensor和numpy array 互相转化,torch.tensor创建的张量是不共享内存的,但torch.from_numpy()和torch.as_tensor()从numpy array创建得到的张量和原数据是共享内存的,修改numpy array会导致对应tensor的改变
1
2
3
4
5
6
7
8import numpy as np
array = np.array([[1,2,3],[4,5,6]])
tensor = torch.tensor(array)
array2tensor = torch.from_numpy(array)
tensor2array = tensor.numpy()
# 修改array,对应的tensor也会改变
array[0,0] = 100
print(array2tensor)从已存在的tensor创建
1
2
3
4
5
6
7x = torch.tensor([5.5, 3])
x = x.new_ones(4, 3, dtype=torch.double)
# 创建一个新的全1矩阵tensor,返回的tensor默认具有相同的torch.dtype和torch.device
x = torch.randn_like(x, dtype=torch.float)
# 重置数据类型
print(x)
# 结果会有一样的size创建tensor的函数
基础构造 torch.tensor([1,2,3,4])
随机初始化 torch.rand(2,3) [0,1)的均匀分布, torch.randn(2,3) N(0,1)的正态分布,randperm(10) 随机排列
正态分布 torch.normal(2,3) 均值为2标准差为3的正态分布
全1矩阵 torch.ones(2,3)
全0矩阵 torch.zeros(2,3)
对角单位阵 torch.eye(2,3)
有序序列 torch.arange(2,10,2) 从2到10,步长2
均分序列 torch.linspace(2,10,2) 从2到10,均分成2份查看tensor的维度
1
2
3
4import torch
k = torch.tensor(2,3)
print(k.shape)
print(k.size())
张量的操作
加法
1
2
3
4
5
6
7
8k = torch.rand(2, 3)
l = torch.ones(2, 3)
print(k+l)
torch.add(k,l)
k.add(l) # 原值修改索引操作
与原数据内存共享,用clone()不会修改1
2
3
4
5
6
7
8
9
10
11
12
13x = torch.rand(4,3)
# 取第二列
print(x[:, 1])
y = x[0,:]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了了
y -= 1
a = y.clone()
a += 1
print(x[0, :])维度变换
1
2
3
4
5x = torch.randn(4,4)
y = x.view(16)
z = x.view(-1,2)
y += 1
print(x)view()共享内存,仅是更改了对张量的观察角度,而reshape()不共享内存,但原始tensor如果不连续,它会返回原值的copy,所以推荐先用clone创建副本再view,用clone能记录到计算图中,梯度回传到副本时也会传到源tensor
扩展/压缩tensor
1
2
3
4
5
6
7
8
9o = torch.rand(2,3)
print(o)
r = o.unsqueeze(0)
print(r)
print(r.shape)
s = r.squeeze(0)
print(s)
print(s.shape)取值操作
1
2
3
4import torch
x = torch.randn(1)
print(type(x))
print(type(x.item()))其他操作见官方文档
张量的广播机制
两个形状不同的Tensor按元素运算时,可能会触发广播(broadcasting)机制:先适当复制元素使这两个Tensor形状相同后,再按元素运算。
1 | p = torch.arange(1, 3).view(1, 2) |
自动求导
Autograd
它是torch.tensor的核心类,设置它的属性requires_grad=True来追踪张量,完成计算后调用backward()来自动计算所有梯度,导数会自动累积到grad,由链式法则可以计算导数,torch.autograd就是计算雅可比矩阵乘积的。
- requires_grad
如果没有指定的话,默认输入的这个标志是 False。 - grad_fn
每个张量都有一个grad_fn属性,该属性引用了创建Tensor自身的Function(除非这个张量是用户手动创建的,即这个张量的grad_fn是None)。Tensor 和 Function 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。1
2
3
4
5
6
7a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
下面举个例子说明梯度计算过程
- 创建一个张量并设置requires_grad=True用来追踪其计算历史
1
2
3
4
5
6
7
8
9
10x = torch.ones(2, 2, requires_grad=True)
print(x)
y = x**2
print(y)
print(y.grad_fn)
z = y * y * 3
out = z.mean()
print(z, out) - 现在开始进行反向传播,因为out是一个标量,因此out.backward()和 out.backward(torch.tensor(1.)) 等价。由于grad在反向传播是累加的,所以在backward之前要清零.grad.data.zero_()。雅可比向量积,.data.norm()它对张量y每个元素进行平方,然后对它们求和,最后取平方根,这些操作计算就是所谓的L2或欧几里德范数。
1
2
3
4
5
6
7
8
9
10
11
12out.backward()
print(x.grad)
# 再来反向传播⼀一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)当y不再是标量,torch.autograd不能直接计算完整的雅可比矩阵,但是如果我们只想要雅可比向量积,只需将这个向量作为参数传给backward:1
2
3
4
5
6
7
8
9
10x = torch.randn(3, requires_grad=True)
print(x)
y = x * 2
i = 0
while y.data.norm() < 1000:
y = y * 2
i = i + 1
print(y)
print(i)若不需要计算梯度,阻止autograd跟踪设置.requires_grad=True的张量的历史记录1
2
3v = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(v)
print(x.grad)若想修改tensor的数值,又不希望被autograd记录(即不会影响反向传播), 那么我们可以对tensor.data进行操作1
2
3
4
5print(x.requires_grad)
print((x ** 2).requires_grad)
with torch.no_grad():
print((x ** 2).requires_grad)1
2
3
4
5
6
7
8
9
10
11x = torch.ones(1,requires_grad=True)
print(x.data) # 还是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外,返回false
y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y.backward()
print(x) # 更改data的值也会影响tensor的值,x为100
print(x.grad) # 不会受到值改变的影响
并行计算简介
PyTorch可以在编写完模型之后,让多个GPU来参与训练,减少训练时间。CUDA是我们使用GPU的提供商——NVIDIA提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。而pytorch编写深度学习代码使用的CUDA是表示开始要求我们的模型或者数据开始使用GPU了。当我们使用了.cuda()时,其功能是让我们的模型或者数据从CPU迁移到GPU(0)当中,通过GPU开始计算。
注:数据在GPU和CPU之间进行传递时会比较耗时,我们应当尽量避免数据的切换。GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误。我们可以通过以下两种方式继续设置。
1 | #设置在文件最开始部分 |
常见的并行方法:
网络结构分布到不同的设备中(Network partitioning)
将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。同一层的任务分布到不同数据中(Layer-wise partitioning)
同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。不同的数据分布到不同的设备中,执行相同的任务(Data parallelism)
它的逻辑是,不再拆分模型,训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传,这种方式可以解决之前模式遇到的通讯问题,是现在主流方式。参考资料
pytorch - 基础知识

