Pytorch是一个基于Python的实现深度学习的计算图框架。
官网对其简介如下:
An open source machine learning framework that accelerates the path from research prototyping to production deployment.
即Pytorch是一个开源的机器学习框架,它可以加速从科研原型到实际生产部署这一过程。
目前神经网络框架分为静态图框架和动态图框架,PyTorch 和 TensorFlow、Caffe 等框架最大的区别就是他们拥有不同的计算图表现形式。 TensorFlow 使用静态图,这意味着我们先定义计算图,然后不断使用它,而在 PyTorch 中,每次都会重新构建一个新的计算图。
静态图和动态图的各有优缺点,比如动态图比较方便debug,使用者能够用任何他们喜欢的方式进行debug,同时非常直观,而静态图是通过先定义后运行的方式,之后再次运行的时候就不再需要重新构建计算图,所以速度会比动态图更快。
Pytorch官网上提供了下载安装的命令,无论我们使用Conda/Pip还是其他方式,都可以直接找到命令。不过在此之前,需要我们首先确认机器的基本信息。
CUDA(Compute Unified Device Architecture),是显卡厂商NVIDIA推出的运算平台。 CUDA™是一种由NVIDIA推出的通用并行计算架构,该架构使GPU能够解决复杂的计算问题。对于Ubuntu系统,查看CUDA版本的命令为
nvcc -V
再查看好CUDA版本后,我们就可以选择安装方式来获取相应的命令。
pip install torch==1.7.1+cu101 torchvision==0.8.2+cu101 torchaudio==0.7.2 -f https://download.pytorch.org/whl/torch_stable.html
安装完成后可以在Python中检查Pytorch版本
import torch
print(torch.__version__)
Pytorch库的核心是张量,这是一种多维数据的数学对象。零阶张量即一个数字或叫标量,一阶张量是数字数组或者向量,类似的,二阶张量是向量数组或矩阵。因而,我们可以把张量概括为标量的N维数组。
接下来介绍Pytorch的张量基本操作。
首先,我们定义一个辅助函数:describe(x),主要将张量x的多种属性打印出来,例如张量的类型、维度以及内容。
def describe(x):
print("Type: {}".format(x.type()))
print("Shape/size: {}".format(x.shape))
print("Values: \n{}".format(x))
Pytorch允许我们使用torch包来以许多方式创建张量。以下主要对几种常见方法进行记录。
# 使用torch.Tensor创造一个张量
describe(torch.Tensor(2, 3))
得到如下输出:
Type: torch.FloatTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[ 3.1654e+09, 4.5635e-41, -5.4825e-21],
[ 3.0718e-41, 4.4842e-44, 0.0000e+00]])
上面是随机创造一个张量,我们也可以给定初始分布让Pytorch随机创建张量
describe(torch.rand(2, 3)) # 均匀分布
describe(torch.randn(2, 3)) # 正态分布
将分别得到以下输出
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[0.0290, 0.4019, 0.2598],
[0.3666, 0.0583, 0.7006]])
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[-0.8545, 0.5098, -0.0821],
[ 0.6607, 0.0785, 0.7884]])
# 填充0
describe(torch.zeros(2, 3))
# 填充1
x = torch.ones(2, 3)
describe(x)
# 填充指定数值
x.fill_(5)
describe(x)
将会得到以下输出
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[0., 0., 0.],
[0., 0., 0.]])
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1., 1., 1.],
[1., 1., 1.]])
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[5., 5., 5.],
[5., 5., 5.]])
这里需要注意的是,以_为结尾的操作是在原数字基础上的代替操作。
我们还可以从列表或者numpy数组中的值来直接创建与初始化张量。
# 使用列表创建和初始化张量
x = torch.Tensor([[1, 2],
[3, 4]])
describe(x)
# 使用numpy创建和初始化张量
npy = np.random.rand(2, 3)
describe(torch.from_numpy(npy))
得到输出结果为
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 2])
Values:
tensor([[1., 2.],
[3., 4.]])
Type: torch.DoubleTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[0.6574, 0.1528, 0.5589],
[0.8965, 0.2688, 0.0198]], dtype=torch.float64)
这里讨论不同方法从一个numpy数组创建一个张量的异同。
data = np.array([[1,2,3],[4,5,6]])
o1 = torch.Tensor(data)
o2 = torch.tensor(data)
o3 = torch.as_tensor(data)
o4 = torch.from_numpy(data)
以上四种操作都可以从一个numpy数组生成张量,那么它们有什么区别呢。这里先打印出它们分别的数值和类型等信息查看。
describe(o1)
describe(o2)
describe(o3)
describe(o4)
得到以下输出
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1., 2., 3.],
[4., 5., 6.]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1, 2, 3],
[4, 5, 6]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1, 2, 3],
[4, 5, 6]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1, 2, 3],
[4, 5, 6]])
首先注意到第一个操作torch.Tensor()和第二个操作torch.tensor()所返回结果的类型不同。
第一个选项(即包含大写T的)是torch.Tensor 类的构造函数。第二个选项是我们所谓的工厂函数( factory function),该函数构造torch.Tensor对象并将其返回给调用者。可以将torch.tensor()函数看作是在给定一些参数输入的情况下构建张量的工厂。工厂函数是用于创建对象的软件设计模式。这两种方法中,工厂函数torch.tensor() 具有更好的文档和更多的配置选项,因而应优先选取。
那么这里的类型差异是从何而来的呢?经查资料发现,这是由于在构建张量时,torch.Tensor() 构造函数使用的默认的dtype不同。我们可以使用torch.get_default_dtype() 方法验证默认的dtype:
torch.get_default_dtype()
得到如下输出
torch.float32
其他调用根据传入的数据来选择 dtype。这称为类型推断(type inference)。dtype 根据传入的数据来推断。请注意,也可以通过给 dtype 指定参数来为这些调用显示设置 dtype。
torch.tensor(data, dtype=torch.float32)
torch.as_tensor(data, dtype=torch.float32)
使用torch.Tensor(),我们无法将 dtype 传递给构造函数。这是torch.Tensor() 构造函数缺少配置选项的示例。这也是使用 torch.tensor() 工厂函数创建张量的原因之一。
还有一个重要的区别是这些方法对变量的复制与共享机制。这里我们在使用ndarray创建张量之后,对numpy.ndarray中的原始输入数据进行更改。
让我们这样做,看看会得到什么:
print("old: ", data)
data[0][0] = -1
print("new: ", data)
输出为:
old: [[1 2 3]
[4 5 6]]
new: [[-1 2 3]
[ 4 5 6]]
接着我们查看之前通过不同方式创建过的张量值
describe(o1)
describe(o2)
describe(o3)
describe(o4)
输出结果为
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1., 2., 3.],
[4., 5., 6.]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[1, 2, 3],
[4, 5, 6]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[-1, 2, 3],
[ 4, 5, 6]])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[-1, 2, 3],
[ 4, 5, 6]])
可以看到一开始data [0][0] = 1,并且还注意到我们只更改了原始numpy.ndarray中的数据。注意,我们没有明确地对张量(o1,o2,o3,o4)进行任何更改。
但是,在设置data [0][0] = 0后,我们可以看到一些张量发生了变化。对于索引0,前两个o1和o2仍具有原始值1,而对于索引0,后两个 o3 和 o4 具有新值0。
发生这种情况是因为torch.Tensor() 和torch.tensor() 复制了它们的输入数据,而torch.as_tensor() 和torch.from_numpy() 与原始输入对象共享了它们在内存中的输入数据。
Share Data | Copy data |
---|---|
torch.as_tensor() | torch.tensor() |
torch.from_numpy | torch.Tensor() |
这种共享仅仅意味着内存中的实际数据存在于一个地方。因此,基础数据中发生的任何更改都将反映在两个对象中,即torch.Tensor和numpy.ndarray。
与复制数据相比,共享数据更高效,占用的内存更少,因为数据不是写在内存中的两个位置。
如果我们有 torch.Tensor 的话,我们要把它转换成一个numpy.ndarray,我们是这样做的
print(o3.numpy())
print(o4.numpy())
得到输出
[[-1 2 3]
[ 4 5 6]]
[[-1 2 3]
[ 4 5 6]]
torch.from_numpy() 函数仅接受 numpy.ndarrays,而torch.as_tensor() 函数则接受包括其他PyTorch张量在内的各种数组对象。因此,torch.as_tensor() 是内存共享比赛中的获胜选择。
考虑到所有这些细节,这两个是最佳选择:
torch.tensor()
torch.as_tensor()
torch.tensor() 调用是一种 go-to 调用,而在调整代码性能时应使用torch.as_tensor()。
关于内存共享,要记住一些注意事项(它可以在某些地方起作用):
在创建好张量后,我们可以像处理基本数据类型那样对其进行操作。
使用张量做线性代数的运算是现代深度学习的基础,这里首先介绍基础的数学加减运算。
对于加法运算,可以直接使用运算符 + 来计算,也可以使用torch.add函数,例如:
describe(torch.add(x, x))
describe(x + x)
输出结果为
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[-0.3098, -2.7412, -0.2638],
[ 1.7697, -0.5222, 1.2208]])
Type: torch.FloatTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[-0.3098, -2.7412, -0.2638],
[ 1.7697, -0.5222, 1.2208]])
查阅文档知其函数原型为:
torch.add(input, other, *, alpha=1, out=None)
其计算结果为
out=input+alpha×other
同样,对于减法操作也是类似的,
torch.sub(input, other, *, alpha=1, out=None)
out =input −alpha×other
torch.Tensor的4种乘法
torch.Tensor有4种常见的乘法:*, torch.mul, torch.mm, torch.matmul.
其中*和torch.mul是相同的功能的,是element-wise的乘法,也是支持broadcast。
一个张量乘以一个标量即所有的元素都乘以这个标量,一个张量乘以一个向量时有以下两种情况:
a = torch.ones(3,4)
b = torch.Tensor([1,2,3,4])
describe(torch.mul(a, b))
得到以下结果
Type: torch.FloatTensor
Shape/Size: torch.Size([3, 4])
Values:
tensor([[1., 2., 3., 4.],
[1., 2., 3., 4.],
[1., 2., 3., 4.]])
相当于行向量在列的维度上进行了Boardcast,然后再进行Element-wise的乘法
a = torch.ones(3,4)
b = torch.Tensor([[1.],
[2.],
[3.]])
describe(torch.mul(a, b))
得到以下结果
Type: torch.FloatTensor
Shape/Size: torch.Size([3, 4])
Values:
tensor([[1., 1., 1., 1.],
[2., 2., 2., 2.],
[3., 3., 3., 3.]])
即在行的维度上进行了Boardcast,然后再进行Element-wise的乘法
a = torch.randn(4, 1)
b = torch.randn(1, 4)
describe(a)
describe(b)
describe(torch.mul(a, b))
describe(torch.mul(b, a))
得到以下输出
Type: torch.FloatTensor
Shape/Size: torch.Size([4, 1])
Values:
tensor([[-0.8625],
[ 0.1596],
[ 0.8184],
[-0.7649]])
Type: torch.FloatTensor
Shape/Size: torch.Size([1, 4])
Values:
tensor([[ 0.8042, -0.1383, 0.3196, -1.0187]])
Type: torch.FloatTensor
Shape/Size: torch.Size([4, 4])
Values:
tensor([[-0.6936, 0.1193, -0.2756, 0.8786],
[ 0.1283, -0.0221, 0.0510, -0.1626],
[ 0.6581, -0.1132, 0.2615, -0.8337],
[-0.6151, 0.1058, -0.2444, 0.7792]])
Type: torch.FloatTensor
Shape/Size: torch.Size([4, 4])
Values:
tensor([[-0.6936, 0.1193, -0.2756, 0.8786],
[ 0.1283, -0.0221, 0.0510, -0.1626],
[ 0.6581, -0.1132, 0.2615, -0.8337],
[-0.6151, 0.1058, -0.2444, 0.7792]])
即向两个维度分别Boardcast,由此也可以看出其并不是矩阵乘法的计算形式,因为这里无论是行向量乘以列向量还是列向量乘以行向量都是向两个维度分别Boardcast得到矩阵。
a = torch.tensor([[1, 2], [2, 3]])
describe(torch.mul(a,a))
得到结果
Type: torch.LongTensor
Shape/Size: torch.Size([2, 2])
Values:
tensor([[1, 4],
[4, 9]])
在这里也可以验证其是element-wise的乘法,而非常规的矩阵乘法。
torch.mm即数学里的矩阵乘法,要求两个Tensor的维度满足矩阵乘法的要求.
a = torch.ones(3,4)
b = torch.randn(4, 2)
describe(torch.mm(a, b))
输出如下
Type: torch.FloatTensor
Shape/Size: torch.Size([3, 2])
Values:
tensor([[-2.7147, 2.8422],
[-2.7147, 2.8422],
[-2.7147, 2.8422]])
torch.matmul 是torch.mm的broadcast版本.
a = torch.ones(3,4)
b = torch.randn(5, 4, 2)
describe(torch.matmul(a, b))
输出如下:
Type: torch.FloatTensor
Shape/Size: torch.Size([5, 3, 2])
Values:
tensor([[[-1.2275, 0.8948],
[-1.2275, 0.8948],
[-1.2275, 0.8948]],
[[-2.1218, 0.7451],
[-2.1218, 0.7451],
[-2.1218, 0.7451]],
[[ 3.2141, 0.6984],
[ 3.2141, 0.6984],
[ 3.2141, 0.6984]],
[[-0.5601, 1.6092],
[-0.5601, 1.6092],
[-0.5601, 1.6092]],
[[ 0.3134, 0.7653],
[ 0.3134, 0.7653],
[ 0.3134, 0.7653]]])
(关于Boardcast暂时没细看,以后补上)
一些运算可以应用到张量的特定维度上。
对于二维张量,我们把行表示为维度0,列表示为维度1
维度张量操作举例如下:
首先是view操作,如下所示代码
x = torch.arange(6)
describe(x)
x = x.view(2, 3)
describe(x)
得到以下输出:
Type: torch.LongTensor
Shape/Size: torch.Size([6])
Values:
tensor([0, 1, 2, 3, 4, 5])
Type: torch.LongTensor
Shape/Size: torch.Size([2, 3])
Values:
tensor([[0, 1, 2],
[3, 4, 5]])
在这里,我们将一个一维的张量(向量)变成了一个二维的张量,但要注意的是,这只是更改了原数据的显示(?读取?)方式,即这个张量还是原来的那个张量,只是共享了这段存储。这里有一个官网的示例,可以很方便理解:
>>> b = t.view(2, 8)
>>> t.storage().data_ptr() == b.storage().data_ptr() # `t` and `b` share the same underlying data.
True
# Modifying view tensor changes base tensor as well.
>>> b[0][0] = 3.14
>>> t[0][0]
tensor(3.14)
t.storage().data_ptr() == b.storage().data_ptr()的结果为True,即可表明view前后的结果使用同一段存储。又如下面这个例子,我们对一个元素进行更改,那么view之后的结果也会更改,尽管我们并没有直接去view后的结果进行修改。
>>> base = torch.tensor([[0, 1],[2, 3]])
>>> base.is_contiguous()
True
>>> t = base.transpose(0, 1) # `t` is a view of `base`. No data movement happened here.
# View tensors might be non-contiguous.
>>> t.is_contiguous()
False
# To get a contiguous tensor, call `.contiguous()` to enforce
# copying data when `t` is not contiguous.
>>> c = t.contiguous()
我们也可以在指定维度上进行求和运算,如下所示例代码
describe(torch.sum(x, dim=0)) # 在列的维度上进行操作,保留一行的信息
describe(torch.sum(x, dim=1)) # 在行的维度上进行操作,保留一列的信息
得到以下输出,
Type: torch.LongTensor
Shape/Size: torch.Size([3])
Values:
tensor([3, 5, 7])
Type: torch.LongTensor
Shape/Size: torch.Size([2])
Values:
tensor([ 3, 12])
这里注意的是,对一个二维张量进行操作时,行表示为维度0,列表示为维度1
对原张量进行转置操作
describe(torch.transpose(x, 0, 1))
得到如下输出
Type: torch.LongTensor
Shape/Size: torch.Size([3, 2])
Values:
tensor([[0, 3],
[1, 4],
[2, 5]])
pytorch的索引和切片方式与numpy很像。以下给出几个例子:
x = torch.arange(6).view(2, 3)
print("x: \n", x)
print("---")
print("x[:2, :2]: \n", x[:2, :2])
print("---")
print("x[0][1]: \n", x[0][1])
print("---")
print("Setting [0][1] to be 8")
x[0][1] = 8
print(x)
输出结果为:
x:
tensor([[0, 1, 2],
[3, 4, 5]])
---
x[:2, :2]:
tensor([[0, 1],
[3, 4]])
---
x[0][1]:
tensor(1)
---
Setting [0][1] to be 8
tensor([[0, 8, 2],
[3, 4, 5]])
我们可以使用index_select函数来选取一个subset
x = torch.arange(9).view(3,3)
print(x)
print("---")
indices = torch.LongTensor([0, 2])
print(torch.index_select(x, dim=0, index=indices)) # 同样也是对列操作,保留结果为一行的维度
print("---")
indices = torch.LongTensor([0, 2])
print(torch.index_select(x, dim=1, index=indices)) # 同样也是对行操作,保留结果为一列的维度
结果输出如下:
tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
---
tensor([[0, 1, 2],
[6, 7, 8]])
---
tensor([[0, 2],
[3, 5],
[6, 8]])
也可以使用numpy风格的高级索引
x = torch.arange(9).view(3,3)
indices = torch.LongTensor([0, 2])
print(x[indices])
print("---")
print(x[indices, :])
print("---")
print(x[:, indices])
输出结果为:
tensor([[0, 1, 2],
[6, 7, 8]])
---
tensor([[0, 1, 2],
[6, 7, 8]])
---
tensor([[0, 2],
[3, 5],
[6, 8]])
我们可以通过连接它们来组合张量。
首先,对行进行连接
x = torch.arange(6).view(2,3)
describe(x)
describe(torch.cat([x, x], dim=0))
describe(torch.cat([x, x], dim=1))
describe(torch.stack([x, x]))
得到输出如下:
Type: torch.LongTensor
Shape/size: torch.Size([2, 3])
Values:
tensor([[0, 1, 2],
[3, 4, 5]])
Type: torch.LongTensor
Shape/size: torch.Size([4, 3])
Values:
tensor([[0, 1, 2],
[3, 4, 5],
[0, 1, 2],
[3, 4, 5]])
Type: torch.LongTensor
Shape/size: torch.Size([2, 6])
Values:
tensor([[0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5]])
Type: torch.LongTensor
Shape/size: torch.Size([2, 2, 3])
Values:
tensor([[[0, 1, 2],
[3, 4, 5]],
[[0, 1, 2],
[3, 4, 5]]])
我们可以在列维度上进行连接
x = torch.arange(9).view(3,3)
print(x)
print("---")
new_x = torch.cat([x, x, x], dim=1)
print(new_x.shape)
print(new_x)
得到输出如下:
tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
---
torch.Size([3, 9])
tensor([[0, 1, 2, 0, 1, 2, 0, 1, 2],
[3, 4, 5, 3, 4, 5, 3, 4, 5],
[6, 7, 8, 6, 7, 8, 6, 7, 8]])
我们还可以在一个新的张量的第0维度上堆叠这个张量
x = torch.arange(9).view(3,3)
print(x)
print("---")
new_x = torch.stack([x, x, x])
print(new_x.shape)
print(new_x)
得到输出为:
tensor([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
---
torch.Size([3, 3, 3])
tensor([[[0, 1, 2],
[3, 4, 5],
[6, 7, 8]],
[[0, 1, 2],
[3, 4, 5],
[6, 7, 8]],
[[0, 1, 2],
[3, 4, 5],
[6, 7, 8]]])
在Pytorch中,变形的操作叫做view。如下示例代码:
x = torch.arange(0, 20)
print(x.view(1, 20))
print(x.view(2, 10))
print(x.view(4, 5))
print(x.view(5, 4))
print(x.view(10, 2))
print(x.view(20, 1))
即可得到如下输出
tensor([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
18, 19]])
tensor([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]])
tensor([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14],
[15, 16, 17, 18, 19]])
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
tensor([[ 0, 1],
[ 2, 3],
[ 4, 5],
[ 6, 7],
[ 8, 9],
[10, 11],
[12, 13],
[14, 15],
[16, 17],
[18, 19]])
tensor([[ 0],
[ 1],
[ 2],
[ 3],
[ 4],
[ 5],
[ 6],
[ 7],
[ 8],
[ 9],
[10],
[11],
[12],
[13],
[14],
[15],
[16],
[17],
[18],
[19]])
从输出结果也可以看出,无论形状怎么变换,数据的顺序是被保留的。
我们经常使用view来增加一个长度为1的维度,这在与其他一些张量进行一些运算或者操作时是非常有用的。这里即实现了广播机制。
x = torch.arange(12).view(3, 4)
y = torch.arange(4).view(1, 4)
z = torch.arange(3).view(3, 1)
print(x)
print(y)
print(z)
print(x + y)
print(x + z)
得到输出如下
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]])
tensor([[0, 1, 2, 3]])
tensor([[0],
[1],
[2]])
tensor([[ 0, 2, 4, 6],
[ 4, 6, 8, 10],
[ 8, 10, 12, 14]])
tensor([[ 0, 1, 2, 3],
[ 5, 6, 7, 8],
[10, 11, 12, 13]])
unsqueeze 和squeeze 用于增加或删除一个维度。
torch.squeeze() 这个函数主要对数据的维度进行压缩,去掉维数为1的的维度。
torch.unsqueeze()这个函数主要是对数据维度进行扩充。给指定位置加上维数为一的维度。
x = torch.arange(12).view(3, 4)
print(x.shape)
x = x.unsqueeze(dim=1)
print(x.shape)
x = x.squeeze()
print(x.shape)
得到输出为:
torch.Size([3, 4])
torch.Size([3, 1, 4])
torch.Size([3, 4])