3.1 Tensor
3.1.1. 基础操作:
1. 创建Tensor:
(1)Tensor是最复杂多变的方式,既可以接收一个list,并据其数据新建tensor,也可以根据制定的形状新建tensor,还能传入其他tensor;
(2)b.tolist可以吧tensor b 转化为list;
(3)一些命令:
a = t.tensor(2, 3)
a.size #输出torch.size([2,3])
a.numel() #输出 6
c = t.Tensor(b_size)
c.shape
t.ones(2, 3)
t.zeros(2, 3)
t.arrange(1, 6, 2)
t.linspace(1, 10, 3)
t.randn(2, 3, device=t.device('cpu'))]
t.randperm(5) # 长度为5的随机排列
t.eye(2, 3, dtype=t.int) # 对角线为1, 不要求行列数一致
2. 常用tensor操作:
(1)view、unsqueeze、squeeze:
b = a.view(-1, 3) # 当某一维为-1的时候,会自动计算它的大小
b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)上增加“1”
b.unsqueeze(-2) # -2表示倒数第二个维度
c = b.view(1, 1, 1, 2, 3)
c.squeeze(0) # 压缩第0维的“1”
c.squeeze() # 把所有维度为“1”的压缩
a[1] = 100
b # a修改,b作为view之后的,也会跟着修改
(2)resize:与view不同,它可以修改tensor的大小,如果新大小超过了原大小,则自动分配新的内存空间;如果新大小小于原大小,则之前的数据依旧会被保存:
b.resize_(1, 3)
b
b.resize_(3, 3) # 旧的数据依旧保存着,多出的大小会分配新空间
b
3. 索引操作:
***如无特殊说明,索引出来的结果与原tensor共享内存,修改一个另一个也会跟着改变。***
(1)普通索引:
a = t.randn(3, 4)
a[0] # 第0行(下标从0开始)
a[:, 0] # 第0列
a[0][2] # 第0行第2个元素,等价于a[0, 2]
a[0, -1] # 第0行最后一个元素
a[:2] # 前两行
a[:2, 0:2] # 前两行,第0,1列
print(a[0:1, :2]) # 第0行,前两列
print(a[0, :2]) # 注意两者的区别:形状不同
#tensor([[-1.1001, 0.7252]])
#tensor([-1.1001, 0.7252])
# None类似于np.newaxis, 为a新增了一个轴
# 等价于a.view(1, a.shape[0], a.shape[1])
a[None].shape # torch.Size([1, 3, 4])
a[None].shape # 等价于a[None,:,:]
a[:,None,:].shape # torch.Size([3, 1, 4])
a[:,None,:,None,None].shape # torch.Size([3, 1, 4, 1, 1])
a > 1 # 返回一个ByteTensor
a[a>1] # 等价于a.masked_select(a>1),且选择结果与原tensor不共享内存空间
a[t.LongTensor([0,1])] # 第0行和第1行
(2)选择函数:
其中,gather函数比较复杂:gather(input, dim, index),在dim的维度上,选取与index一样的值,且index的维度要与input的相符。
(2-1)当dim=0时,需要从做往右看,有“列数”个样本,再选取每个样本由上往下的第index个数据:
a = torch.tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
index = torch.tensor([0,1,2,2])
a.gather(dim=0,index) #输出为:tensor([[0, 5, 10, 11]])
a.gather(dim=0,index.t()) #输出为:tensor([[0], [1], [2], [2]])
(2-2)当dim=1时,需要从上往下看,有“行数”个样本,再选取每个样本由左往右2的第index个数据:
a = torch.tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
index = torch.tensor([0,1,2,2])
a.gather(dim=1,index) #输出为:tensor([[0, 1, 2, 2]])
a.gather(dim=1,index.t()) #输出为:tensor([[0], [5], [10], [14]])
(2-3) scatter_函数则与gather相反,需要把取出来的数据按照index的位置放回去。
c = t.zeros(4,4)
c.scatter_(1, index, b.float())
#tensor([[ 0., 0., 0., 3.],
# [ 0., 5., 6., 0.],
# [ 0., 9., 10., 0.],
# [12., 0., 0., 14.]])
(2-4)对tensor的索引操作结果仍然是一个tensor,需要调用tensor.item( ),可以将只包含一个元素的tensor的数据提取出来,且不用管tensor的形状(只要只有一个元素就可以用)。
(3)高级索引:
x = t.arange(0,27).view(3,3,3)
x[[1, 2], [1, 2], [2, 0]] # x[1,1,2]和x[2,2,0]
x[[2, 1, 0], [0], [1]] # x[2,0,1],x[1,0,1],x[0,0,1]
x[[0, 2], ...] # x[0] 和 x[2]
(4) 生成与其他tensor类似属性(类型,形状等)的tensor:tensor 默认是32bit的,占用4Byte
c = a.type_as(b) #如果类型相同,则不作改变;否则将其变为和b一样类型的tensor
a.new(2,3) # 等价于torch.DoubleTensor(2,3),建议使用a.new_tensor
t.zeros_like(a) #等价于t.zeros(a.shape,dtype=a.dtype,device=a.device)
t.rand_like(a)
a.new_ones(4,5, dtype=t.int)
a.new_tensor([3,4])
4. 逐元素操作
5. 归并操作
注:dim=0表示“跨行”进行操作;dim=1表示“跨列”进行操作。
6. 比较
7. 线性代数
注: 矩阵的转置会导致存储空间不连续,需要调用它的.contiguous方法使其连续。
3.1.2 Tensor和Numpy
Tensor和Numpy数组之间具有很高的相似性,彼此之间的互操作也非常简单高效。需要注意的是,Numpy和Tensor共享内存。由于Numpy历史悠久,支持丰富的操作,所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小。
1. 互相转换:
import numpy as np
a = np.ones([2, 3],dtype=np.float32)
b = t.from_numpy(a)
b = t.Tensor(a) # 也可以直接将numpy对象传入Tensor
a[0, 1]=100
b # b的值会跟着a的值变化而变化
c = b.numpy() # a, b, c三个对象共享内存
注:当Numpy和Tensor的类型不一样的时候,数据会被复制,但不会共享内存:
a = np.ones([2, 3]) # 注意此时a是64bit
a.dtype
b = t.Tensor(a) # 此处进行拷贝,不共享内存,b是32bit的
b.dtype
c = t.from_numpy(a) # c也是64bit
a[0, 1] = 100
b # b与a不共享内存,所以b不会变
c # c与a共享内存,所以c会变
注:t.tensor与t.Tensor不同,t.tensor不论输入的类型是什么,都只会进行拷贝,而不会共享内存。
2. 广播法则:
(1)三个法则:
- 让所有数组都向其中最长的数组看齐,shape中不足的部分通过在前面加1补齐
- 两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
- 当输入数组某个维度的长度为1时,计算时沿此维度复制扩充为一样的形状
(2)手动广播法则的方法:
- unsqueeze或者view,或者tensor[None]:为数据某一维的形状补1,实现法则1
- expand或者expand_as,重复数组,实现法则3;该操作不会复制数组,故不会占用额外的空间
a = t.ones(3, 2)
b = t.zeros(2, 3,1)
# 自动广播法则
# 第一步: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
# 手动广播法则
# 或者 a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
a[None].expand(2, 3, 2) + b.expand(2,3,2)
3.1.3 内部结构
(1)tensor的数据结构包括信息区(Tensor)和存储区(Storage),信息区保存着形状(size)、步长(stride)、数据类型(type) 等信息,真正的数据则保存成连续数组。由于数据可能很大,故主要内存占用取决于tensor中元素的数目,也即存储区的大小。
(2)另外,一般来说一个tensor有着与之对应的storage,所以不同的tensor头信息可能不同,但是可能使用相同的数据。
(3)storage_offset命令是指tensor的第一个数据,是从连续存储的数据区中的第几个位置开始;tensor.data_ptr( )可以返回tensor首元素的内存地址。
3.1.4 其他
1. GPU/CPU转换:
***使用tensor.cuda(device_id)或者tensor.cpu()可随意在gpu/cpu上传输,另一个通用的方法是tensor.to(device)
a = t.randn(3, 4)
a.device # device(type='cpu')
if t.cuda.is_available():
a = t.randn(3,4, device=t.device('cuda:0'))
# 等价于
# a.t.randn(3,4).cuda(0)
# 但是前者更快
a.device
device = t.device('cpu')
a.to(device)
注:
- 尽量使用`tensor.to(device)`, 将`device`设为一个可配置的参数,这样可以很轻松的使程序同时兼容GPU和CPU
- 数据在GPU之中传输的速度要远快于内存(CPU)到显存(GPU), 所以尽量避免频繁的在内存和显存中传输数据。
2. 持久化
Tensor的保存和加载十分的简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的`pickle`模块,在load时还可将GPU tensor映射到CPU或其它GPU上。
if t.cuda.is_available():
a = a.cuda(0) # 把a转为GPU0上的tensor,
t.save(a,'a.pth')
# 加载为b, 存储于GPU1上(因为保存时tensor就在GPU0上)
b = t.load('a.pth')
# 加载为c, 存储于CPU
c = t.load('a.pth', map_location=lambda storage, loc: storage)
# 加载为d, 存储于GPU1上
d = t.load('a.pth', map_location={'cuda:0':'cuda:1'})
3. 向量化
向量化可在同一时间执行多个操作,通常是对不同的数据执行同一个或一批命令,提高运算效率,避免使用for循环。
(1)时间节省实示例:
def for_loop_add(x, y):
result = []
for i,j in zip(x, y):
result.append(i + j)
return t.Tensor(result)
x = t.zeros(100)
y = t.ones(100)
%timeit -n 10 for_loop_add(x, y)
%timeit -n 10 x + y
# 运算结果为:
# 497 µs ± 31.2 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# The slowest run took 13.83 times longer than the fastest. This could mean that an intermediate result is being cached.
# 5.75 µs ± 9.06 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
(2)其他注意点:
- 大多数't.function'函数有一个参数"out",用于存储产生的结果;
- t.set_num_threads 可以设置Pytorch进行CPU多线程并行计算时所占用的线程数,可以限制占用CPU的数目
- t.set_printoptions 可以用来设置打印tensor时的数值精度和格式
a = t.arange(0, 20000000)
print(a[-1], a[-2]) # 32bit的IntTensor精度有限导致溢: tensor(19999999) tensor(19999998)
b = t.LongTensor()
t.arange(0, 20000000, out=b) # 64bit的LongTensor不会溢出
b[-1],b[-2] # (tensor(19999999), tensor(19999998))
a = t.randn(2,3)
a
t.set_printoptions(precision=10)
a
3.1.5 线性回归示例
线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,其表达形式为y = wx+b+e,e为误差服从均值为0的正态分布,其损失函数为:
采用随机梯度下降法更新参数w和b来最小化损失函数,最终学习得到w和b的数值
import torch as t
%matplotlib inline
from matplotlib import pyplot as plt
import matplotlib
from IPython import display
device = t.device('cpu') #如果你想用gpu,改成t.device('cuda:0')
t.manual_seed(1000) # 设置随机数种子,保证在不同电脑上运行时下面的输出一致
def get_fake_data(batch_size=8): # 产生随机数据:y=x*2+3,加上了一些噪声
x = t.rand(batch_size, 1, device=device) * 5
y = x * 2 + 3 + t.randn(batch_size, 1, device=device)
return x, y
x, y = get_fake_data(batch_size=16)
plt.scatter(x.squeeze().cpu().numpy(), y.squeeze().cpu().numpy()) # 查看产生的x-y分布
# 随机初始化参数
w = t.rand(1, 1).to(device)
b = t.zeros(1, 1).to(device)
lr =0.02 # 学习率
for ii in range(500):
x, y = get_fake_data(batch_size=4)
# forward:计算loss
y_pred = x.mm(w) + b.expand_as(y) # x@W等价于x.mm(w);for python3 only
loss = 0.5 * (y_pred - y) ** 2 # 均方误差
loss = loss.mean()
# backward:手动计算梯度
dloss = 1
dy_pred = dloss * (y_pred - y)
dw = x.t().mm(dy_pred)
db = dy_pred.sum()
# 更新参数
w.sub_(lr * dw)
b.sub_(lr * db)
if ii%50 ==0:
# 画图
display.clear_output(wait=True)
x = t.arange(0, 6).view(-1, 1)
y = x.float().mm(w) + b.expand_as(x)
plt.plot(x.cpu().numpy(), y.cpu().numpy()) # predicted
x2, y2 = get_fake_data(batch_size=32)
plt.scatter(x2.numpy(), y2.numpy()) # true data
plt.xlim(0, 5)
plt.ylim(0, 13)
plt.show()
plt.pause(0.5)
print('w: ', w.item(), 'b: ', b.item())