当前位置:首页阅读

(Pytorch笔记)Tensor

(Pytorch笔记)Tensor

torch

(Pytorch笔记)Tensor

# torch-Tensor

本文章旨在说明torch的张量和张量运算。主要包括torch文档中torch.Tensor,Tensor Attribute这两章,以及torch这章中Tensors和Math operations两节。

## 一种数据结构

torch中的Tensor(张量)是一切运算的基础。这是一种由torch提供的数据结构。它和Python内置的几种数据结构都有所不同。比起嵌套的Python List,它更像C语言中的多维数组。虽然你能够方便地索引到Tensor中任意位置的成员并读写它,但你不能给一个已经生成的Tensor追加成员或者维度。所有的修改Tensor对象本身结构的操作都是通过新建Tensor然后把对象数据复制进去的方式完成的(不过由于torch对这一过程做了封装,用起来还是很方便的)。

## 数学上的张量

数学上的张量,简单来说就是向量,向量的向量,向量的向量的向量……

由于抽象代数中定义“向量”这一概念的时候,并不限制向量的成员是什么(只要所有成员的类型一致就可以)。我们理所当然地,可以把同类型向量(指长度相同,且成员类型相同)作为另一个向量的成员,从而构造出更“高阶”的向量。由于“向量的向量的向量”这类说法太拗口,就干脆起了一个新名字称呼所有向量和嵌套的向量——张量。

向量的类型

在类型系统中。向量是一类类型。而且向量是必须依托原子类型或者其他类型而存在的。比如,我们不会定义一个成员没有类型的向量(可以接受任意类型作为成员的向量,被认为是一种泛型,而不是无类型)。

如果一个向量的成员类型是整型数,那么我们就称这个向量是整形向量。如果一个向量的成员类型是字符,那么就称为字符向量。如果一个向量的成员是字符向量(字符串),那么就成为字符向量的向量,二阶字符向量或者字符串向量。

我们不说向量依托的类型时,一般是泛指结构相同的所有向量。比如“二阶向量”,可以指整型数二阶向量,字符二阶向量,字符串二阶向量,整型数二阶向量的二阶向量,整型数向量的二阶向量等等。

向量的长度

阶数相同,依托的原子类型相同,但是长度不同的两个向量算不算同一种向量类型呢?

这个问题具有一定的混淆性。因为很多时候我们会把“向量”,“列表”,“线性表”等术语混用。这里我们稍微统一一下。对于Python的列表List,Haskell的List,C的链表(用指针和结构体实现的那种)来说,长度不同,不影响它们作为同一种类型。比如Haskell中,`[[1],[2,3]]`这个二阶List中两个成员长度不同但是其类型为`Num a = [[a]]`。

但是对于向量,我们认为长度不同的向量是不同类型。比如表示平面点的二维向量和表示三维空间点的三维向量不输于同一种类型。例如Haskell中的`(Num a, Num b) = (a,b)`和`(Num a, Num b, Num c) = (a,b,c)`不是同一种类型。

如果你实在记不住这么多。那么只要知道torch的Tensor不允许参差不齐的矩阵就行了。这种写法是会报错的~~`torch.Tensor([[1,2],[3]])`~~。

### 张量的数据类型

这里所说的“张量的数据类型”是指张量中归根到底的成员是什么类型的。虽然张量就是向量的嵌套,但是不可能永远是向量的向量。最内层的结构必然是某种原子类型的向量,或者某种非张量(非向量)类型的向量。作为张量最基本成员的类型,就是这里我们所说的张量的类型。(为了和防止和其他概念混淆,我们不妨称之为“张量基于的数据类型”。)

比如三维空间向量,向量中所有成员都是实数。所以其数据类型是实数。实数矩阵中所有成员也都是实数,所以其数据类型也是实数。复数矩阵,复数张量中所有成员都是复数,所以其数据类型都是复数。

### 张量的阶数

张量的阶数用来表示从原子类型算起(或者从其他非张量的类型算起),嵌套了几层向量结构。我们称原子类型为0阶张量。那么原子类型的向量就是1阶张量。以1阶张量为成员的张量就是2阶张量。以n阶张量为成员的张量就是n+1阶张量。

强调其他非张量的类型是因为。理论上函数也能弄个张量,结构体,各种类对象都能弄个张量出来。

不过torch中的张量只能基于几种原子类型。

### 张量各阶的维度

仅仅长度不同的向量,也属于不同的类型。因此阶数相同,但是某一阶上向量长度不同的张量严格来说也不是相同的类型。比如$2\times 3$的2阶张量和$3\times3$的2阶张量就是不同的。

如果用笛卡儿积来解释的话,全体实数$2\times 3$的2阶张量(实数$2\times 3$矩阵)是$(R \times R\times R)\times (R \times R\times R)$,用指数记法是$(R^3)^2$。而$3\times3$的2阶张量(实数$3\times 3$矩阵)则是$(R \times R\times R)\times (R \times R\times R)\times (R \times R \times R)$,也就是$(R^3)^3$。

它们都是二阶张量,因此表示成集合的自然数指数运算的时候,指数运算的次数都是2次。但是在具体每次指数运算的时候,它们的指数不完全相等。我们称每次指数运算时的指数为它们(某一阶)上的维度。

一般来说,指数表达式中,越内层的指数的阶越低,越外层的阶越高。我们在命令名或者调用函数表示的时候,则一般把高阶指数写在前边,低阶指数写在后边。比如$((R^4)^3)^2$表示全体实数$2 \times 3 \times 4$张量。

换句话说一个“实数$2\times 3\times 4$张量”是$2$个$3\times 4$张量组成的。而每个$3\times 4$张量是$3$个$4$维向量组成的。

## torch中的Tensor类

torch中Tensor类作为张量。这一部分对Tensor相关代码做一些说明。以后的代码假设已经执行过包引用。

```python

import torch

```

### 判断是否是Tensor

判断一个对象是否是Tensor,可以用Python中的算符。但是torch提供了一个更简单的方法,用`torch.is_tensor`函数判断。如果传入的参数是Tensor对象,则返回`True`,否则返回`False`。

### 构造Tensor

构造Tensor最主要的方法就是调用Tensor的构造函数。例如根据嵌套的列表构造函数构造一个Tensor(使用的列表中各级列表长度必须一致,不能参差不齐):

```python

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

```

torch中还提供了一些比较简单的函数能够构造指定大小的Tensor然后以某些值填充。比如构造全0张量:

```python

torch.zeros([2,3,5])

```

作为参数的List表示的是各阶的维度。这里实际上构造了一个$((R^5)^3)^2$中的全0张量。(省掉列表,直接把维度写进去也行,比如`torch.zeros(2,3,5)`。)

还有用随机数填充的张量和用指定数填充的张量。

```python

torch.rand(2,3,5) # [0,1)区间上平均分布的随机数填充

torch.randn(2,3,5) # 正态随机数填充

torch.ones(2,3,5) # 用1填充

torch.full([2,3,5],3.15) # 指定用3.15填充。由于这个函数需要传入填充张量用的数字,必须用列表传入维度,防止混乱。

```

还有一类构造函数,需要传入一个Tensor对象。虽然不会复制传入对象的数据,但是会复制其维度,数据类型,存储设备(显存还是内存)等信息。这类函数都以“某某like”命名。例如`torch.randn_like(x)`会构造和`x`同形的Tensor,但是内容是用正态随机数填充的。

```python

x = torch.zeros(2,3)

torch.randn_like(x)

```

### Tensor对象的维度

通过Tensor对象的的`size`方法查看其各阶维度。

```python

x = torch.randn(2,3,5)

print(x.size())

```

Tensor对象的阶数和维度一旦确定,就几乎无法修改。要得到不同阶数和维度的Tensor对象,只能新建Tensor。

### Tensor对象的类型

通过张量的`dtype`属性可以查看张量的数据类型。默认Tensor的数据类型是32位浮点型。

在构造Tensor的时候可以通过给`dtype`参数赋值来指定数据类型。(所有数据类型都是`torch`的成员。)

```python

torch.zeros([2,3,4],dtype=torch.int32)

```

可以通过调用Tensor对象的`to`方法改变此对象的类型。

```pytho

x = torch.zeros([2,3,4],dtype=torch.int32)

x.to(torch.float64)

```

也有一些简略的写法比如:

```python

x.int() # 转换为int32

x.double() # 转换为float64

```

### Tensor对象复制

对象直接赋值都是引用传递。要想赋值Tensor对象,可以调用对象的`clone`方法。例如:

```python

x = randn(2,3)

y = x.clone()

y[0,0]=12345

print(x)

print(y)

```

## 索引

Tensor可以像List那样索引。多重索引可以写在一对方括号内(用逗号隔开)。可以直接把索引结果当作对象进行赋值和读取。

```python

x = torch.randn(2,3,4)

print(x[1,0,2]) # x[1,0,2]和x[1][0][2]作用一样

x[1,0,3]=1234

```

### 切片操作

在索引中使用`:`可以完成切片操作。所谓的切片就是指通过索引选中张量中的一部分(而非单个对象)。

首先,在索引的时候如果用`:`代替某个阶的索引值,表示选中所有。假设有`x=torch.zeros(2,3)`。`x[0]`选中的是行索引为0的一整行。而`x[0,:]`选中行索引为0,列索引为`:`的所有成员。实际效果与`x[0]`等价。

不过用切片操作还能做到一些`x[0]`这种写法做不到的事情。比如选中各行中所有列索引为1的成员,组成一个新行。用切片操作表示是`x[:,1]`。

也可以指定要选区的区间。方法是在`:`前后加数字。例如`i:j`,索引为`i`的,索引大于`i`而小于`j`的都会被选中。(可以省略`i`或者`j`不写。省略`i`会从索引为0开始取,省略`j`会一直取到最后一个。)

例如从大矩阵中分隔小矩阵:

```python

x = torch.randn(10,8)

y = x[2:5,1:4]

```

### 胜似filter

方括号中还可以直接写逻辑表达式。用Tensor对象名直接当作表示Tensor对象中成员的符号参与逻辑运算最后就能选中所有符合条件的对象。例如:

```python

x = torch.randn(10,10)

x[x0]

```

这种用法的原理是torch重载了Tensor的比较算符。Tensor参与比较运算的时候会返回一个只包含0和1的同形Tensor。其中为1的位置表示满足条件,为0的位置表示不满足条件(文档中称这个比较运算返回的结果为mask)。

然后torch重载了Tensor的索引算符(方括号),当传入一个mask到索引算符中时,会按照mask把所有符合条件的成员选出来。

实际上可以自己构造一个同形Tensor用于筛选元素(不过数据类型需要是`uint8`)。通过调用`torch.masked_select`来完成筛选。例如:

```python

m = torch.zeros_like(x,dtype=torch.uint8)

m[0,1] = 1

torch.masked_select(x,m)

```

### 索引是引用传递

切片和索引都是引用传递而非值传递。尝试这个例程:

```python

x = torch.randn(2,3)

y = x[:,1]

y[0] = 1000

print(y)

print(x)

```

## Tensor操作和运算

### 拼凑高阶张量

切片索引可能让张量的阶越来越低。而`torch.stack`则能把几个同形,同数据类型的低阶张量拼凑成高阶张量。

```python

x = torch.randn(2,3)

y = torch.stack((x,x,x)) # 要拼接的几个张量以元组形式传入。

print(y)

print(y.size())

```

### 张量拼接

拼接操作不会提高张量的阶数。例如把两个向量拼接成一个更长的向量。

```python

x = torch.randn(5)

y = torch.randn(3)

z = torch.cat((x,y)) # 要拼接的张量也是以元组形式传入。

```

拼接操作还能指定维度。这个功能的作用在拼接2阶张量(或者2阶以上张量)才能体现出来。在2阶张量中分按行拼接和按列拼接。

不论是之前的拼凑还是现在的拼接,还是后续的操作。大都不会改变当作参数传入的张量本身,而是会返回一个新建的作为结果的张量。

### 重排张量

改变一个Tensor对象的维度是不行的。但是可以新建一个对象从中复制数据。使用`torch.reshape`可以快速完成这一操作。例如把一个3阶张量展开为一个1阶张量:

```python

x = torch.randn(2,3,2)

y = torch.reshape(x,(2*3*2,)) # 第二个参数指定变形后的阶数和维度。由于必须是一个元组,所以我加了一个逗号。

```

把某一个维度的维度设为`-1`表示让torch根据其他维度以及原来张量的成员个数推断这个维度。例如:

```python

z = torch.reshape(x,(4,-1))

```

### 转置及其拓展

对于2阶张量,让所有成员的行列索引互换所得的张量为其转置。我们可以用`torch.t`求一个2阶张量的转置。

进入张量领域后,转置运算有了拓展。使用`torch.transpose`交换任意两个阶的维度(对于所有成员来说是交换对应位置的索引)。例如:

```python

x = torch.randn(3,5)

torch.t(x) # 求转置

x.t() # 每个对象Tensor也有t方法。

y = torch.randn(3,4,5)

torch.transpose(y,1,2) # 交换第二维度4和第三维度5。

```

所有用到指定维度的地方。0都是指最高阶维度。1是次高阶维度。依此类推。

如果一个Tensor的`size()`是`(5,2,7,9)`那么dim=0就是指第一数个5,dim=1指第二个数2,dim=3指第三数7,dim=4指第四个数9。

## 张量运算

### 成员运算

我们可以仅仅把张量看作一种批量运算工具。把张量之间的运算看作同形张量的对应元素之间的运算。我们一般可以把这类运算称作元素向运算。

例如:

```python

x = torch.randn(3,5,2)

y = torch.randn(3,5,2)

x+y

x-y

x*y

x/y

xy # 返回一个0和1的矩阵

```

torch中还提供了一些常用的数学函数比如三角函数,反三角函数,指数函数等。

### 张量代数

我们也可以把张量本身当作代数系统。规定一些只有张量才能参与的运算。比如向量内积,外积,矩阵乘法。这类运算一般对张量的阶和各阶维度有所要求。

```python

x = torch.randn(3,5)

y = torch.randn(5,2)

torch.mm(x,y) # 矩阵乘法

```

再演示一个用矩阵乘法算内积的操作:

```python

x = torch.Tensor([1,2,3])

y = torch.Tensor([4,5,6])

torch.mm(x.reshape(1,3),y.reshape(3,1)).reshape(-1)

```

具体的运算,函数有很多,我这里简单提一下。要用的时候看看官方手册。

## 主要参考

-

-

-

以上就是((Pytorch笔记)Tensor)全部内容,收藏起来下次访问不迷路!