pytorch-Tensor
Tensor
从接口的角度来讲,对tensor的操作可分为两类:
torch.function
,如torch.save
等。另一类是
tensor.function
,如tensor.view
等。
而从存储的角度来讲,对tensor的操作又可分为两类:
不会修改自身的数据,如
a.add(b)
, 加法的结果会返回一个新的tensor。会修改自身的数据,如
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 |
|
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共享内存,也即更改其中的一个,另外一个也会跟着改变。在实际应用中可能经常需要添加或减少某一维度,这时候squeeze
和unsqueeze
两个函数就派上用场了。
tensorflow
里面是 tf.expand_dim
和 tf.squeeze
.
resize
是另一种可用来调整size
的方法,但与view
不同,它可以修改tensor的大小。如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子。
1 |
|
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
1 |
|
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
1 |
|
tensor([[[ 0., 1., 2.]],
[[ 3., 4., 5.]]])
1 |
|
tensor([[[ 0., 1., 2.]],
[[ 3., 4., 5.]]])
1 |
|
tensor([[[[ 0., 1., 2.],
[ 3., 4., 5.]]]])
1 |
|
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
1 |
|
tensor([[ 0., 100., 2.],
[ 3., 4., 5.]])
1 |
|
tensor([[ 0., 100., 2.]])
1 |
|
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 |
|
三维tensor的gather
操作同理,下面举几个例子。
index_select(input, dim, index) 指定维度上选取某些行和列, 返回的是某行和某列
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 |
|
tensor([[ 0.5705, 1.0374, -1.1780, 0.0635],
[-0.1195, 0.6657, 0.9583, -1.8952]])
1 |
|
tensor([[ 0.5705, 1.0374, -1.1780, 0.0635],
[-0.1195, 0.6657, 0.9583, -1.8952]])
1 |
|
tensor([[ 0.5705, 1.0374, -1.1780, 0.0635],
[-0.1195, 0.6657, 0.9583, -1.8952]])
1 |
|
tensor([[[ 0.5705, 1.0374, -1.1780, 0.0635]],
[[-0.1195, 0.6657, 0.9583, -1.8952]]])
返回列的两种方式
1 |
|
tensor([[-0.5760, 1.3726],
[ 1.0374, -1.1780],
[ 0.6657, 0.9583]])
1 |
|
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 |
|
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 |
|
tensor([[ 1, 1, 1, 0],
[ 0, 0, 1, 1],
[ 1, 1, 0, 0]], dtype=torch.uint8)
1 |
|
tensor([[ 80, 235, 127, 167],
[ 199, 85, 0, 0],
[ 0, 0, 0, 0]], dtype=torch.uint8)
1 |
|
tensor([ 0.3464, 1.4499, 0.7417, -1.9551, -0.0042, -0.0141])
gather(input, dim, index) 根据 index 在 dim 维度上选取数据,输出 size 与 index 一样.
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, 1, 3]]) torch.Size([1, 5])
1 |
|
tensor([[ 0., 6., 12., 8., 19.]])
所以 gather 就是 index 与 input 中某一个维度一致,比如这里 input.size()=[4,5].
那么 dim=0, index.size()=[1,5]. 然后在每列对应的 index 选取对应的数据。最后输出 size 与 index 一致。
1 |
|
torch.Size([4, 1])
1 |
|
tensor([[ 1.],
[ 7.],
[ 13.],
[ 19.]])
list 转换成 one-hot 向量
1 |
|
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],所以可能出现溢出等问题。
表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)
是通用的做法,同时还有float
、long
、half
等快捷方法。CPU tensor与GPU tensor之间的互相转换通过tensor.cuda
和tensor.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 |
|
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”,取决于参数keepdim
,keepdim=True
会保留维度1
。注意,以上只是经验总结,并非所有函数都符合这种形状变化方式,如cumsum
。
1 |
|
tensor([[ 0., 1., 2.],
[ 3., 4., 5.]])
1 |
|
(tensor([ 3., 5., 7.]),
tensor([ 3.0000, 4.1231, 5.3852]),
tensor([ 3.0000, 4.0207, 5.1045]))
1 |
|
$||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 |
|
tensor(2.)
1 |
|
tensor([ 0.7617, 1.0060, 1.6778])
比较
比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作。常用比较函数如表3-6所示。
表3-6: 常用比较函数
|函数|功能|
|:–:|:–:|
|gt/lt/ge/le/eq/ne|大于/小于/大于等于/小于等于/等于/不等|
|topk|最大的k个数|
|sort|排序|
|max/min|比较两个tensor最大最小值|
表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=b
、a>b
、a!=b
、a==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 |
|
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 |
|
(tensor([ 0.7326, 0.6784, 0.9791, 0.9011]), tensor([ 1, 2, 1, 1]))
1 |
|
(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 |
|
(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 |
|
tensor([[ 0.8260, 1.3392, 0.5944],
[ 1.3392, 2.7192, 1.0062],
[ 0.5944, 1.0062, 0.6130]])
1 |
|
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 |
|
(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 |
|
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 |
|
float64
array([[1., 1., 1.],
[1., 1., 1.]])
1 |
|
torch.FloatTensor
tensor([[ 1., 100., 1.],
[ 1., 1., 1.]])
1 |
|
1 |
|
torch.DoubleTensor
tensor([[ 1., 1., 1.],
[ 1., 1., 1.]], dtype=torch.float64)
1 |
|
tensor([[ 1., 1., 1.],
[ 1., 1., 1.]])
1 |
|
tensor([[ 1., 100., 1.],
[ 1., 1., 1.]], dtype=torch.float64)
BroadCast
广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。
Numpy的广播法则定义如下:
让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐
两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状
PyTorch当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:
unsqueeze
或者view
:为数据某一维的形状补1,实现法则1expand
或者expand_as
,重复数组,实现法则3;该操作不会复制数组,所以不会占用额外的空间。
注意,repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。
1 |
|
1 |
|
tensor([[[ 1., 1.],
[ 1., 1.],
[ 1., 1.]],
[[ 1., 1.],
[ 1., 1.],
[ 1., 1.]]])
1 |
|
tensor([[[ 1., 1.],
[ 1., 1.],
[ 1., 1.]],
[[ 1., 1.],
[ 1., 1.],
[ 1., 1.]]])
1 |
|
内部结构
tensor的数据结构如图3-1所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用则取决于tensor中元素的数目,也即存储区的大小。
一般来说一个tensor有着与之相对应的storage, storage是在data之上封装的接口,便于使用,而不同tensor的头信息一般不同,但却可能使用相同的数据。下面看两个例子。
1 |
|
tensor([ 0., 1., 2., 3., 4., 5.])
1 |
|
0.0
1.0
2.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
1 |
|
0.0
1.0
2.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
1 |
|
True
1 |
|
0.0
1.0
2.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
1 |
|
True
1 |
|
(tensor([[ 0., 100., 2.],
[ 3., 4., 5.]]), tensor([ 0., 1., 2., 3., 4., 5.]))
1 |
|
(139719200619016, 139719200619016)
1 |
|
tensor([ 2., 3., 4., 5.])
0.0
100.0
2.0
3.0
4.0
5.0
[torch.FloatStorage of size 6]
1 |
|
(94551854283064, 94551854283056)
1 |
|
tensor([ 0., 100., -100., 3., 4., 5.])
1 |
|
tensor([[ 6666., 100., -100.],
[ 3., 4., 5.]])
1 |
|
True
1 |
|
(0, 2, 0)
1 |
|
True
1 |
|
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 |
|
1 |
|
tensor([ 6666., 100., -100., 3., 4., 5.], device='cuda:0')
向量化
向量化计算是一种特殊的并行计算方式,相对于一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大提高科学运算的效率,Python本身是一门高级语言,使用很方便,但这也意味着很多操作很低效,尤其是for
循环。在科学计算程序中应当极力避免使用Python原生的for循环
。
1 |
|
1 |
|
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 |
|
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 |
|
1 |
|
1 |
|
tensor(2.0264241695) tensor(2.9323694706)