张量操作
张量的初始化:
import torchx = torch.empty(5, 3) # 创建一个5*3未初始化的Tensorx = torch.rand(5, 3) # 创建一个5*3随机初始化的Tensorx = torch.zeros(5, 3, dtype=torch.long) # dtype可以指定张量中数据类型x = torch.Tensor([5.5, 3]) # 直接根据数据创建
重置张量:
x = x.new_ones(5, 3) # 可以重新分配张量数据,数据类型默认还是与原来相同x = torch.randn_like(x, dtype=torch.float) # 指定新的数据类型
获取张量的形状与大小:
x = torch.rand(5, 3)print(x.size())print(x.shape) # 可直接获取大小与形状
张量的运算
x = torch.rand(5, 3)y = torch.rand(5, 3)print(x + y)print(torch.add(x, y)) # 张量的相加操作,两种方式皆可result = torch.empty(5, 3)torch.add(x, y, out=result)print(result) # 还可指定输出print(y.add_(x)) # 还可进行inplace操作 一般inplace操作末尾都有下划线,如copy_()等
索引机制:
x = torch.rand(5, 3)y = x[0, :] # 可以通过索引访问张量的一部分y += 1 # 要注意的是,修改索引的值会直接导致原变量的值发生改变print(y)print(x) # 源tensor也被改了
改变张量的形状:
x = torch.rand(5, 3)y = x.view(15) # 通过view函数可以改变张量的形状,只传入一个参数则代表维数z = x.view(-1, 5) # -1表示维度未知,但所指的维度可以由计算机根据其他维度的值推出来x += 1 # 要注意的是,view同样是一个索引操作,它只改变了观察张量的角度,原变量的值也会随之改变print(x)print(y)print(x.size(), y.size(), z.size())
非索引方法改变张量的形状:
x = torch.rand(5, 3)x_cp = x.clone().view(15) # 要进行深拷贝并改变形状的操作可以调用clone函数,不推荐使用reshape函数,因为它不能保证返回的是其拷贝x -= 1 # 使用clone还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源Tensor。print(x)print(x_cp)
标量转为数字:
x = torch.randn(1)print(x)print(x.item()) # 使用item函数可以将标量张量转换为数字
广播机制:
x = torch.arange(1, 3).view(1, 2) # arrange函数可以使值连续分布在张量中print(x)y = torch.arange(1, 4).view(3, 1)print(y)print(x + y) # 维数不同时,会进行广播
内存机制:
x = torch.tensor([1, 2])y = torch.tensor([3, 4])id_before = id(y)y = y + xprint(id(y) == id_before) # False 这样的操作会开辟新的内存,再让y指向新的内存,耗费资源id_before = id(y)y[:] = y + xprint(id(y) == id_before) # True 索引操作不需开辟内存torch.add(x, y, out=y)print(id(y) == id_before) # True 指定自己为输出也不需要y += x # y += x, y.add_(x)print(id(y) == id_before) # True 自增也不需要
GPU与CPU
if torch.cuda.is_available():device = torch.device("cuda") # GPUy = torch.ones_like(x, device=device) # 直接创建一个在GPU上的Tensorx = x.to(device) # 等价于 .to("cuda")z = x + yprint(z)print(z.to("cpu", torch.double)) # to()还可以同时更改数据类型
Autograd
在初始化张量时,使require_grad为true后,就可利用链式法则进行梯度追踪了:
import torchx = torch.ones(2, 2, requires_grad=True) # 如果将其属性.requires_grad设置为True,它将开始追踪在其上的所有操作(这样就可以利用链式法则进行梯度传播了)。print(x)
grad_fn属性:
print(x.grad_fn) # 每个Tensor都有一个.grad_fn属性,该属性即创建该Tensor的Function, 就是说该Tensor是不是通过某些运算得到的,若是,则grad_fn返回一个与这些运算相关的对象,否则是None。y = x + 2print(y.grad_fn) # 非noneprint(x.is_leaf, y.is_leaf) # 像x这种直接创建的叫叶子节点,否则不是
转为标量:
z = y * y * 3out = z.mean() # mean表示求平均,out为标量print(z, out)
可以通过requiresgrad函数中途改变张量的require_grad属性
a = torch.randn(2, 2) # 缺失情况下默认 requires_grad = Falsea = ((a * 3) / (a - 1))print(a.requires_grad) # Falsea.requires_grad_(True) # 可以采用inplace方式改变require_grad属性print(a.requires_grad) # Trueb = (a * a).sum()print(b.grad_fn) # 这样,b就不是none了
求导计算(x对out求导)具有累加特性,因此每次计算梯度时一般来说会先把梯度清零:
out.backward() # 对哪个标量求导,就调用谁的backward函数print(x.grad) # 每个对生成生成out变量的梯度就会更新# 再来反向传播一次,注意grad是累加的out2 = x.sum()out2.backward()print(x.grad) # 得到的是累加后的梯度out3 = x.sum()x.grad.data.zero_() # 将梯度清零out3.backward()print(x.grad) # 发现与之前的值不一样
在y.backward()时,如果y是标量,则不需要为backward()传入任何参数;否则,需要传入一个与y同形的Tensor。简单来说就是为了避免向量(甚至更高维张量)对张量求导,而转换成标量对张量求导。本质是因为不允许张量对张量求导,只允许标量对张量求导,求导结果是和自变量同形的张量。所以必要时我们要把张量通过将所有张量的元素加权求和的方式转换为标量,举个例子,假设y由自变量x计算而来,w是和y同形的张量,y.backward(w)的含义是:先计算l = torch.sum(y * w),则l是个标量,然后求l对自变量x的导数。
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)y = 2 * xz = y.view(2, 2) # 此时z不是一个标量print(z)v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)z.backward(v) # 传入了一个与z同形的权重向量,此时求和得到标量后可进行反向传播计算print(x.grad)
中断梯度追踪:
x = torch.tensor(1.0, requires_grad=True)y1 = x ** 2with torch.no_grad(): # 可在中途中断梯度追踪y2 = x ** 3y3 = y1 + y2print(x.requires_grad) # Trueprint(y1, y1.requires_grad) # Trueprint(y2, y2.requires_grad) # Falseprint(y3, y3.requires_grad) # Truey3.backward() # 此时若使y3对x求梯度print(x.grad) # 输出的是x方对x求导的值,因为y2中断了梯度追踪,不会回传梯度,也不能调用backward()
直接中断梯度追踪过于暴力,对tensor.data进行操作则既可以修改数值,也不会被计算图记录:
x = torch.ones(1, requires_grad=True)print(x.data) # x.data还是一个tensorprint(x.data.requires_grad) # 但是x.data已经是独立于计算图之外y = 2 * xx.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播y.backward()print(x) # 更改data的值也会影响tensor的值print(x.grad)
