pytorch-Tensor

Tensor

从接口的角度来讲,对tensor的操作可分为两类:

  1. torch.function,如torch.save等。

  2. 另一类是tensor.function,如tensor.view等。

而从存储的角度来讲,对tensor的操作又可分为两类:

  1. 不会修改自身的数据,如 a.add(b), 加法的结果会返回一个新的tensor。

  2. 会修改自身的数据,如 a.add_(b), 加法的结果仍存储在a中,a被修改了。

表3-1: 常见新建tensor的方法

|函数|功能|

|:—:|:—:|

|Tensor(*sizes)|基础构造函数|

|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)|随机排列|

其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list的数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor.

  • b.tolist() 把 tensor 转为 list

  • b.numel() b 中元素总数,等价于 b.nelement()

  • torch.Tensor(b.size()) 创建和 b 一样的 tensor

  • 除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 用list的数据创建tensor

b = torch.Tensor([[1,2,3],[4,5,6]])

print(b)

print(b.tolist())

print(b.numel())

# 创建一个和b形状一样的tensor

c = torch.Tensor(b.size())

print(c)

# 创建一个元素为2和3的tensor

d = torch.Tensor((2, 3))

print(d)

tensor([[ 1.,  2.,  3.],

        [ 4.,  5.,  6.]])

[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]

6

tensor(1.00000e-15 *

       [[-3.4942,  0.0000,  0.0000],

        [ 0.0000,  0.0000,  0.0000]])

tensor([ 2.,  3.])

常用Tensor操作

view, squeeze, unsqueeze, resize

  • 通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身的数据,返回的新tensor与源tensor共享内存,也即更改其中的一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时候squeezeunsqueeze两个函数就派上用场了。

tensorflow 里面是 tf.expand_dimtf.squeeze.

  • resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子。
1
2
3
4
5

a = torch.arange(0, 6)

a.view(2, 3)

tensor([[ 0.,  1.,  2.],

        [ 3.,  4.,  5.]])
1
2
3
4
5

b = a.view(-1, 3) # 当某一维为-1的时候,会自动计算它的大小

b

tensor([[ 0.,  1.,  2.],

        [ 3.,  4.,  5.]])
1
2
3

b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)上增加“1”

tensor([[[ 0.,  1.,  2.]],



        [[ 3.,  4.,  5.]]])
1
2
3

b.unsqueeze(-2) # -2表示倒数第二个维度

tensor([[[ 0.,  1.,  2.]],



        [[ 3.,  4.,  5.]]])
1
2
3
4
5

c = b.view(1, 1, 1, 2, 3)

c.squeeze(0) # 压缩第0维的“1”

tensor([[[[ 0.,  1.,  2.],

          [ 3.,  4.,  5.]]]])
1
2
3

c.squeeze() # 把所有维度为“1”的压缩

tensor([[ 0.,  1.,  2.],

        [ 3.,  4.,  5.]])
1
2
3
4
5

a[1] = 100

b # a修改,b作为view之后的,也会跟着修改

tensor([[   0.,  100.,    2.],

        [   3.,    4.,    5.]])
1
2
3
4
5

b.resize_(1, 3)

b

tensor([[   0.,  100.,    2.]])
1
2
3
4
5

b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间

b

tensor([[   0.0000,  100.0000,    2.0000],

        [   3.0000,    4.0000,    5.0000],

        [  -0.0000,    0.0000,    0.0000]])

索引操作

Tensor支持与numpy.ndarray类似的索引操作,语法上也类似,下面通过一些例子,讲解常用的索引操作。如无特殊说明,索引出来的结果与原tensor共享内存,也即修改一个,另一个会跟着修改。

其它常用的选择函数如表3-2所示。

表3-2常用的选择函数

函数|功能|

:—:|:—:|

index_select(input, dim, index)|在指定维度dim上选取,比如选取某些行、某些列

masked_select(input, mask)|例子如上,a[a>0],使用ByteTensor进行选取

non_zero(input)|非0元素的下标

gather(input, dim, index)|根据index,在dim维度上选取数据,输出的size与index一样

gather是一个比较复杂的操作,对一个2维tensor,输出的每个元素如下:

1
2
3
4
5

out[i][j] = input[index[i][j]][j] # dim=0

out[i][j] = input[i][index[i][j]] # dim=1

三维tensor的gather操作同理,下面举几个例子。

index_select(input, dim, index) 指定维度上选取某些行和列, 返回的是某行和某列

1
2
3
4
5
6
7

a = torch.randn(3, 4)

print(a)

print(a[0,1]) # 第 0 行, 第 1 列

tensor([[ 0.5948, -0.5760,  1.3726, -0.9664],

        [ 0.5705,  1.0374, -1.1780,  0.0635],

        [-0.1195,  0.6657,  0.9583, -1.8952]])

tensor(-0.5760)
返回行的四种方式
1
2
3

print(a[torch.LongTensor([1,2])]) # 第 0 行 和 第 1 行

tensor([[ 0.5705,  1.0374, -1.1780,  0.0635],

        [-0.1195,  0.6657,  0.9583, -1.8952]])
1
2
3
4
5

index = torch.LongTensor([1,2])

a.index_select(dim=0, index=index)

tensor([[ 0.5705,  1.0374, -1.1780,  0.0635],

        [-0.1195,  0.6657,  0.9583, -1.8952]])
1
2
3

a[1:3] # 只能是连续的行

tensor([[ 0.5705, 1.0374, -1.1780, 0.0635],

        [-0.1195,  0.6657,  0.9583, -1.8952]])
1
2
3

print(a[torch.LongTensor([[1],[2]])]) # 还是第 0 行 和 第 1 行

tensor([[[ 0.5705,  1.0374, -1.1780,  0.0635]],



        [[-0.1195,  0.6657,  0.9583, -1.8952]]])
返回列的两种方式
1
2
3

a.index_select(dim=1, index=index)

tensor([[-0.5760,  1.3726],

        [ 1.0374, -1.1780],

        [ 0.6657,  0.9583]])
1
2
3

a[:, 1:3] # 连续的列

tensor([[-0.5760,  1.3726],

        [ 1.0374, -1.1780],

        [ 0.6657,  0.9583]])

masked_selected(input, mask) 使用 ByteTensor 进行选取

mask is ByteTensor, 类似于 a[a>1]

1
2
3
4
5
6
7
8
9

a = torch.randn(3, 4)

print(a)

print(a[a>0])

a.masked_select(a>0)

tensor([[ 0.3464,  1.4499,  0.7417, -1.9551],

        [-0.0042, -0.0141,  1.2861,  0.0691],

        [ 0.5843,  1.6635, -1.2771, -1.4623]])

tensor([ 0.3464,  1.4499,  0.7417,  1.2861,  0.0691,  0.5843,  1.6635])



tensor([ 0.3464,  1.4499,  0.7417,  1.2861,  0.0691,  0.5843,  1.6635])
1
2
3

a>0 # 返回一个 ByteTensor

tensor([[ 1,  1,  1,  0],

        [ 0,  0,  1,  1],

        [ 1,  1,  0,  0]], dtype=torch.uint8)
1
2
3
4
5

b = torch.ByteTensor(3,4)

b

tensor([[  80,  235,  127,  167],

        [ 199,   85,    0,    0],

        [   0,    0,    0,    0]], dtype=torch.uint8)
1
2
3

a[b]

tensor([ 0.3464,  1.4499,  0.7417, -1.9551, -0.0042, -0.0141])

gather(input, dim, index) 根据 index 在 dim 维度上选取数据,输出 size 与 index 一样.

1
2
3
4
5
6
7
8
9

a = torch.arange(0, 20).view(4,5)

print(a)

index = torch.LongTensor([[0,1,2,1,3]])

print(index, index.shape)

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,  1,  3]]) torch.Size([1, 5])
1
2
3

a.gather(dim=0, index=index)

tensor([[  0.,   6.,  12.,   8.,  19.]])

所以 gather 就是 index 与 input 中某一个维度一致,比如这里 input.size()=[4,5].

那么 dim=0, index.size()=[1,5]. 然后在每列对应的 index 选取对应的数据。最后输出 size 与 index 一致。

1
2
3
4
5

index2 = torch.LongTensor([[1],[2],[3],[4]])

print(index2.shape)

torch.Size([4, 1])
1
2
3

a.gather(dim=1, index=index2)

tensor([[  1.],

        [  7.],

        [ 13.],

        [ 19.]])
list 转换成 one-hot 向量
1
2
3
4
5
6
7
8
9
10
11

### list 转换成 one-hot 向量

label = [1, 2, 3, 4, 5]

label = torch.LongTensor(label).view(-1, 1)

one_hot = torch.zeros(5, 10).scatter_(dim=1, index=label, value=1)

one_hot

tensor([[0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],

        [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],

        [0., 0., 0., 1., 0., 0., 0., 0., 0., 0.],

        [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.],

        [0., 0., 0., 0., 0., 1., 0., 0., 0., 0.]])

Tensor 类型

Tensor有不同的数据类型,如表3-3所示,每种类型分别对应有CPU和GPU版本(HalfTensor除外)。默认的tensor是FloatTensor,可通过t.set_default_tensor_type 来修改默认tensor类型(如果默认类型为GPU tensor,则所有操作都将在GPU上进行)。Tensor的类型对分析内存占用很有帮助。例如对于一个size为(1000, 1000, 1000)的FloatTensor,它有1000*1000*1000=10^9个元素,每个元素占32bit/8 = 4Byte内存,所以共占大约4GB内存/显存。HalfTensor是专门为GPU版本设计的,同样的元素个数,显存占用只有FloatTensor的一半,所以可以极大缓解GPU显存不足的问题,但由于HalfTensor所能表示的数值大小和精度有限[^2],所以可能出现溢出等问题。

^2: https://stackoverflow.com/questions/872544/what-range-of-numbers-can-be-represented-in-a-16-32-and-64-bit-ieee-754-syste

表3-3: tensor数据类型

数据类型| CPU tensor |GPU tensor|

:—:|:—:|:–:|

32-bit 浮点| torch.FloatTensor |torch.cuda.FloatTensor

64-bit 浮点| torch.DoubleTensor| torch.cuda.DoubleTensor

16-bit 半精度浮点| N/A |torch.cuda.HalfTensor

8-bit 无符号整形(0~255)| torch.ByteTensor| torch.cuda.ByteTensor

8-bit 有符号整形(-128~127)| torch.CharTensor |torch.cuda.CharTensor

16-bit 有符号整形 | torch.ShortTensor| torch.cuda.ShortTensor

32-bit 有符号整形 |torch.IntTensor |torch.cuda.IntTensor

64-bit 有符号整形 |torch.LongTensor |torch.cuda.LongTensor

各数据类型之间可以互相转换,type(new_type)是通用的做法,同时还有floatlonghalf等快捷方法。CPU tensor与GPU tensor之间的互相转换通过tensor.cudatensor.cpu方法实现。Tensor还有一个new方法,用法与t.Tensor一样,会调用该tensor对应类型的构造函数,生成与当前tensor类型一致的tensor。

  • torch.set_sefault_tensor_type(‘torch.IntTensor)

逐元素操作

这部分操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,此类操作的输入与输出形状一致。常用的操作如表3-4所示。

表3-4: 常见的逐元素操作

|函数|功能|

|:–:|:–:|

|abs/sqrt/div/exp/fmod/log/pow..|绝对值/平方根/除法/指数/求余/求幂..|

|cos/sin/asin/atan2/cosh..|相关三角函数|

|ceil/round/floor/trunc| 上取整/四舍五入/下取整/只保留整数部分|

|clamp(input, min, max)|超过min和max部分截断|

|sigmod/tanh..|激活函数

对于很多操作,例如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。如a ** 2 等价于torch.pow(a,2), a * 2等价于torch.mul(a,2)

其中clamp(x, min, max)的输出满足以下公式:

$$

y_i =

\begin{cases}

min, & \text{if } x_i \lt min \

x_i, & \text{if } min \le x_i \le max \

max, & \text{if } x_i \gt max\

\end{cases}

$$

clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值。

1
2
3
4
5
6
7
8
9

import torch

a = torch.arange(0,6).view(2,3)

print(a)

torch.clamp(a, min=3, max=5)

tensor([[ 0.,  1.,  2.],

        [ 3.,  4.,  5.]])



tensor([[ 3.,  3.,  3.],

        [ 3.,  4.,  5.]])

归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和。常用的归并操作如表3-5所示。

表3-5: 常用归并操作

|函数|功能|

|:—:|:—:|

|mean/sum/median/mode|均值/和/中位数/众数|

|norm/dist|范数/距离|

|std/var|标准差/方差|

|cumsum/cumprod|累加/累乘|

以上大多数函数都有一个参数 **dim**,用来指定这些操作是在哪个维度上执行的。关于dim(对应于Numpy中的axis)的解释众说纷纭,这里提供一个简单的记忆方式:

假设输入的形状是(m, n, k)

  • 如果指定dim=0,输出的形状就是(1, n, k)或者(n, k)

  • 如果指定dim=1,输出的形状就是(m, 1, k)或者(m, k)

  • 如果指定dim=2,输出的形状就是(m, n, 1)或者(m, n)

size中是否有”1”,取决于参数keepdimkeepdim=True会保留维度1。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如cumsum

1
2
3
4
5

a = torch.arange(0, 6).view(2,3)

a

tensor([[ 0.,  1.,  2.],

        [ 3.,  4.,  5.]])
1
2
3

a.norm(dim=0, p=1), a.norm(dim=0, p=2), a.norm(dim=0, p=3)

(tensor([ 3.,  5.,  7.]),

 tensor([ 3.0000,  4.1231,  5.3852]),

 tensor([ 3.0000,  4.0207,  5.1045]))
1
2
3

torch.norm??

$||x||{p} = \sqrt[p]{x{1}^{p} + x_{2}^{p} + \ldots + x_{N}^{p}}$

torch.dist??

dist(input, other, p=2) -> Tensor

Returns the p-norm of (:attr:input - :attr:other)

1
2
3

torch.dist(torch.ones(4), torch.zeros(4), 2)

tensor(2.)
1
2
3

torch.var(torch.randn(10,3), dim=0)

tensor([ 0.7617,  1.0060,  1.6778])

比较

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作。常用比较函数如表3-6所示。

表3-6: 常用比较函数

|函数|功能|

|:–:|:–:|

|gt/lt/ge/le/eq/ne|大于/小于/大于等于/小于等于/等于/不等|

|topk|最大的k个数|

|sort|排序|

|max/min|比较两个tensor最大最小值|

表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=ba>ba!=ba==b,其返回结果是一个ByteTensor,可用来选取元素。max/min这两个操作比较特殊,以max来说,它有以下三种使用情况:

  • t.max(tensor):返回tensor中最大的一个数

  • t.max(tensor,dim):指定维上最大的数,返回tensor和下标

  • t.max(tensor1, tensor2): 比较两个tensor相比较大的元素

至于比较一个tensor和一个数,可以使用clamp函数。下面举例说明。

  • max/min

  • sort

  • topk

1
2
3
4
5

a = torch.rand(3,4)

a

tensor([[ 0.1845,  0.4101,  0.1470,  0.0083],

        [ 0.7520,  0.8871,  0.9494,  0.2504],

        [ 0.3879,  0.4554,  0.4080,  0.1703]])
1
2
3

torch.max(a, dim=0)

(tensor([ 0.7326,  0.6784,  0.9791,  0.9011]), tensor([ 1,  2,  1,  1]))
1
2
3

a.sort(dim=0)

(tensor([[ 0.1424,  0.5681,  0.1833,  0.1654],

         [ 0.4556,  0.6418,  0.3242,  0.5120],

         [ 0.7326,  0.6784,  0.9791,  0.9011]]), tensor([[ 2,  0,  0,  2],

         [ 0,  1,  2,  0],

         [ 1,  2,  1,  1]]))
1
2
3

a.topk(k=2, dim=0)

(tensor([[ 0.7326,  0.6784,  0.9791,  0.9011],

         [ 0.4556,  0.6418,  0.3242,  0.5120]]), tensor([[ 1,  2,  1,  1],

         [ 0,  1,  2,  0]]))

线性代数

PyTorch的线性函数主要封装了Blas和Lapack,其用法和接口都与之类似。常用的线性代数函数如表3-7所示。

表3-7: 常用的线性代数函数

|函数|功能|

|:—:|:—:|

|trace|对角线元素之和(矩阵的迹)|

|diag|对角线元素|

|triu/tril|矩阵的上三角/下三角,可指定偏移量|

|mm/bmm|矩阵乘法,batch的矩阵乘法|

|addmm/addbmm/addmv/addr/badbmm..|矩阵运算

|t|转置|

|dot/cross|内积/外积

|inverse|求逆矩阵

|svd|奇异值分解

具体使用说明请参见官方文档^3,需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

1
2
3
4
5
6
7

b.contiguous(), b.size()

b.contiguous().is_contiguous()

print(a.matmul(b.contiguous()))

tensor([[ 0.8260,  1.3392,  0.5944],

        [ 1.3392,  2.7192,  1.0062],

        [ 0.5944,  1.0062,  0.6130]])
1
2
3
4
5
6
7
8
9

b = a.t()

print(a.size(), b.shape)

print(a.mm(b))

b.is_contiguous()

torch.Size([3, 4]) torch.Size([4, 3])

tensor([[ 0.8260,  1.3392,  0.5944],

        [ 1.3392,  2.7192,  1.0062],

        [ 0.5944,  1.0062,  0.6130]])

False
1
2
3

b, b.diag()

(tensor([[ 0.4556,  0.7326,  0.1424],

         [ 0.5681,  0.6418,  0.6784],

         [ 0.1833,  0.9791,  0.3242],

         [ 0.5120,  0.9011,  0.1654]]), tensor([ 0.4556,  0.6418,  0.3242]))
1
2
3
4
5

a = torch.randn(5,5)

a.triu(1)

tensor([[ 0.0000,  1.5959, -0.2253,  0.2349, -0.5151],

        [ 0.0000,  0.0000, -0.0366, -0.0867,  0.2737],

        [ 0.0000,  0.0000,  0.0000,  0.9904, -1.4889],

        [ 0.0000,  0.0000,  0.0000,  0.0000, -1.1053],

        [ 0.0000,  0.0000,  0.0000,  0.0000,  0.0000]])

Tensor和Numpy

Tensor和Numpy数组之间具有很高的相似性,彼此之间的互操作也非常简单高效。需要注意的是,Numpy和Tensor共享内存。由于Numpy历史悠久,支持丰富的操作,所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小。

注意: 当numpy的数据类型和Tensor的类型不一样的时候,数据会被复制,不会共享内存。

1
2
3
4
5
6
7
8
9

import numpy as np

a = np.ones([2,3])

print(a.dtype)

a

float64

array([[1., 1., 1.],

       [1., 1., 1.]])
1
2
3
4
5
6
7

b = torch.Tensor(a)

print(b.type())

b

torch.FloatTensor

tensor([[   1.,  100.,    1.],

        [   1.,    1.,    1.]])
1
2
3

torch.from_numpy??

1
2
3
4
5
6
7

c = torch.from_numpy(a)

print(c.type())

c

torch.DoubleTensor

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

        [ 1.,  1.,  1.]], dtype=torch.float64)
1
2
3
4
5

a[0,1] = 100

b # b与a不通向内存,所以即使a改变了,b也不变

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

        [ 1.,  1.,  1.]])
1
2
3

c # c 与 a 共享内存

tensor([[   1.,  100.,    1.],

        [   1.,    1.,    1.]], dtype=torch.float64)

BroadCast

广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。

Numpy的广播法则定义如下:

  • 让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐

  • 两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算

  • 当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状

PyTorch当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:

  • unsqueeze或者view:为数据某一维的形状补1,实现法则1

  • expand或者expand_as,重复数组,实现法则3;该操作不会复制数组,所以不会占用额外的空间。

注意,repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。

1
2
3
4
5

a = torch.ones(3, 2)

b = torch.zeros(2, 3, 1)

1
2
3
4
5
6
7
8
9
10
11
12
13

# 自动广播法则

# 第一步:a是2维,b是3维,所以先在较小的a前面补1 ,

# 即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1),

# 第二步: a和b在第一维和第三维形状不一样,其中一个为1 ,

# 可以利用广播法则扩展,两个形状都变成了(2,3,2)

a+b

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

         [ 1.,  1.],

         [ 1.,  1.]],



        [[ 1.,  1.],

         [ 1.,  1.],

         [ 1.,  1.]]])
1
2
3

a.unsqueeze(0).expand(2,3,2) + b.expand(2,3,2)

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

         [ 1.,  1.],

         [ 1.,  1.]],



        [[ 1.,  1.],

         [ 1.,  1.],

         [ 1.,  1.]]])
1
2
3
4
5

# expand不会占用额外空间,只会在需要的时候才扩充,可极大节省内存

e = a.unsqueeze(0).expand(10000000000000, 3,2)

内部结构

tensor的数据结构如图3-1所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。

一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。下面看两个例子。

图3-1: Tensor的数据结构

1
2
3
4
5

a = torch.arange(0,6)

a

tensor([ 0.,  1.,  2.,  3.,  4.,  5.])
1
2
3

a.storage()

 0.0

 1.0

 2.0

 3.0

 4.0

 5.0

[torch.FloatStorage of size 6]
1
2
3
4
5

b = a.view(2, 3)

b.storage()

 0.0

 1.0

 2.0

 3.0

 4.0

 5.0

[torch.FloatStorage of size 6]
1
2
3
4
5
6
7

# 一个对象的id值可以看作它在内存中的地址

# storage的内存地址一样,即是同一个storage

id(b.storage()) == id(a.storage())

True
1
2
3
4
5

c = torch.arange(0, 6)

c.storage()

 0.0

 1.0

 2.0

 3.0

 4.0

 5.0

[torch.FloatStorage of size 6]
1
2
3
4
5
6
7

# 一个对象的id值可以看作它在内存中的地址

# storage的内存地址一样,即是同一个storage

id(c.storage()) == id(a.storage())

True
1
2
3
4
5
6
7

# a改变,b也随之改变,因为他们共享storage, 但是 c 没有改变啊,很神奇

a[1] = 100

b, c

(tensor([[   0.,  100.,    2.],

         [   3.,    4.,    5.]]), tensor([ 0.,  1.,  2.,  3.,  4.,  5.]))
1
2
3
4
5
6
7

# 一个对象的id值可以看作它在内存中的地址

# storage的内存地址一样,即是同一个storage

id(c[1].storage()), id(c.storage())

(139719200619016, 139719200619016)
1
2
3
4
5
6
7

c = a[2:]

print(c)

c.storage()

tensor([ 2.,  3.,  4.,  5.])

 0.0

 100.0

 2.0

 3.0

 4.0

 5.0

[torch.FloatStorage of size 6]
1
2
3
4
5

c.data_ptr(), a.data_ptr() # data_ptr返回tensor首元素的内存地址

# 可以看出相差8,这是因为2*4=8--相差两个元素,每个元素占4个字节(float)

(94551854283064, 94551854283056)
1
2
3
4
5

c[0]=-100 # c[0]的内存地址对应 a[2] 的内存地址

a

tensor([   0.,  100., -100.,    3.,    4.,    5.])
1
2
3
4
5
6
7

d = torch.Tensor(c.storage())

d[0] = 6666

b

tensor([[ 6666.,   100.,  -100.],

        [    3.,     4.,     5.]])
1
2
3
4
5

# 下面4个tensor共享storage

id(a.storage()) == id(b.storage()) == id(c.storage()) == id(d.storage())

True
1
2
3

a.storage_offset(), c.storage_offset(), d.storage_offset()

(0, 2, 0)
1
2
3
4
5

e = b[::2, ::2] # 隔2行/列取一个元素

id(e.storage()) == id(a.storage())

True
1
2
3

e.is_contiguous()

False

可见绝大多数操作并不修改tensor的数据,而只是修改了tensor的头信息。这种做法更节省内存,同时提升了处理速度。在使用中需要注意。

此外有些操作会导致tensor不连续,这时需调用tensor.contiguous方法将它们变成连续的数据,该方法会使数据复制一份,不再与原来的数据共享storage。

另外读者可以思考一下,之前说过的高级索引一般不共享stroage,而普通索引共享storage,这是为什么?(提示:普通索引可以通过只修改tensor的offset,stride和size,而不修改storage来实现)。

持久化

Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其它GPU上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

if torch.cuda.is_available():

a = a.cuda() # 把a转为GPU1上的tensor,

torch.save(a,'a.pth')



# 加载为b, 存储于GPU1上(因为保存时tensor就在GPU1上)

b = torch.load('a.pth')

# 加载为c, 存储于CPU

c = torch.load('a.pth', map_location=lambda storage, loc: storage)

# 加载为d, 存储于GPU0上

d = torch.load('a.pth', map_location={'cuda:1':'cuda:0'})

1
2
3
4
5

a = torch.load("a.pth")

print(a)

tensor([ 6666.,   100.,  -100.,     3.,     4.,     5.], device='cuda:0')

向量化

向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是for循环。在科学计算程序中应当极力避免使用Python原生的for循环

1
2
3
4
5
6
7
8
9
10
11

def for_loop_add(x, y):

result = []

for i,j in zip(x, y):

result.append(i + j)

return torch.Tensor(result)

1
2
3
4
5
6
7
8
9

x = torch.zeros(100)

y = torch.ones(100)

%timeit -n 10 for_loop_add(x, y)

%timeit -n 10 x + y

351 µs ± 9.1 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

The slowest run took 16.46 times longer than the fastest. This could mean that an intermediate result is being cached.

4.24 µs ± 7.12 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

可见二者有超过40倍的速度差距,因此在实际使用中应尽量调用内建函数(buildin-function),这些函数底层由C/C++实现,能通过执行底层优化实现高效计算。因此在平时写代码时,就应养成向量化的思维习惯。

此外还有以下几点需要注意:

  • 大多数torch.function都有一个参数out,这时候产生的结果将保存在out指定tensor之中。

  • torch.set_num_threads可以设置PyTorch进行CPU多线程并行计算时候所占用的线程数,这个可以用来限制PyTorch所占用的CPU数目。

  • torch.set_printoptions可以用来设置打印tensor时的数值精度和格式。

下面举例说明。

1
2
3
4
5
6
7

a = torch.randn(2,3)

torch.set_printoptions(precision=10)

a

tensor([[-0.3306640089, -0.0507176071, -0.4223535955],

        [-0.8678948879, -0.0437202156, 0.0183448847]])

线性回归

线性回归是机器学习入门知识,应用十分广泛。线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,其表达形式为$y = wx+b+e$,$e$为误差服从均值为0的正态分布。首先让我们来确认线性回归的损失函数:

$$

loss = \sum_i^N \frac 1 2 ({y_i-(wx_i+b)})^2

$$

然后利用随机梯度下降法更新参数$\textbf{w}$和$\textbf{b}$来最小化损失函数,最终学得$\textbf{w}$和$\textbf{b}$的数值。

1
2
3
4
5
6
7
8
9

import torch as t

%matplotlib inline

from matplotlib import pyplot as plt

from IPython import display

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

# 设置随机数种子,保证在不同电脑上运行时下面的输出一致

t.manual_seed(1000)



def get_fake_data(batch_size=8):

''' 产生随机数据:y=x*2+3,加上了一些噪声'''

x = t.rand(batch_size, 1) * 20

y = x * 2 + (1 + t.randn(batch_size, 1))*3

return x, y



# 来看看产生的x-y分布

x, y = get_fake_data()

plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

# 随机初始化参数

w = torch.randn(1,1)

b = torch.zeros(1,1)



lr = 0.001 # 学习率



for epoch in range(20000):

x, y = get_fake_data(batch_size=8)



# forward

y_pred = x.mm(w) + b.expand_as(y)

loss = 0.5 * (y_pred - y) ** 2

loss = loss.sum()



# backward: 手动计算梯度

dloss = 1

dy_pred = dloss * (y_pred - y)



dw = x.t().contiguous().mm(dy_pred)

db = dy_pred.sum()



# 更新参数

w.sub_(lr * dw)

b.sub_(lr * db)



if epoch % 1000 == 0:

print("epoch:{}, loss:{}".format(epoch, loss))

# 画图

display.clear_output(wait=True)

x = torch.arange(0, 20).view(-1, 1) # [20, 1]

y = x.mm(w) + b.expand_as(x) # predicted data

plt.plot(x.numpy(), y.numpy())



x2, y2 = get_fake_data(batch_size=20) # true data

plt.scatter(x2.numpy(), y2.numpy())



plt.xlim(0,20)

plt.ylim(0,41)

plt.show()

plt.pause(0.5)



print(w.squeeze()[0], b.squeeze()[0])

png

tensor(2.0264241695) tensor(2.9323694706)
作者

Xie Pan

发布于

2018-12-01

更新于

2021-06-29

许可协议

评论