"""
Tensor又名张量,读者可能对这个名词似曾相识,因它不仅在PyTorch中出现过,它也是Theano、TensorFlow、 Torch和MxNet中重要的数据结构。
关于张量的本质不乏深度的剖析,但从工程角度来讲,可简单地认为它就是一个数组,且支持高效的科学计算。
它可以是一个数(标量)、一维数组(向量)、二维数组(矩阵)和更高维的数组(高阶数据)。
Tensor和Numpy的ndarrays类似,但PyTorch的tensor支持GPU加速。
"""
from __future__ import print_function
import torch as t
#torch的版本
print(t.__version__)
"""
3.1.1基础操作
从接口的角度来讲,对tensor的操作可分为两类:
1.torch.function,如torch.save等。
2.另一类是tensor.function,如tensor.view等。
为方便使用,对tensor的大部分操作同时支持这两类接口,在本书中不做具体区分,如torch.sum (torch.sum(a, b))与tensor.sum (a.sum(b))功能等价。
而从存储的角度来讲,对tensor的操作又可分为两类:
不会修改自身的数据,如 a.add(b), 加法的结果会返回一个新的tensor。
会修改自身的数据,如 a.add_(b), 加法的结果仍存储在a中,a被修改了。
函数名以_结尾的都是inplace方式, 即会修改调用者自己的数据,在实际应用中需加以区分
"""
"创建Tensor"
a=t.Tensor(2,3)#指定tensor的形状
print(a)#数值取决于内存空间的状态,print时候可能overflow
b=t.Tensor([[1,2,3],[4,5,6]])#用list的数据创建tensor
print(b)
print(b.tolist())#把tensor转为list
b_size=b.size()#tensor.size()返回torch.Size对象
print(b_size)
print(b.numel())#b中的元素总个数,2*3,等价于b.nelement()
c=t.Tensor(b_size)#创建一个与b形状一样的tensor
d=t.Tensor((2,3))#创建一个元素W为2和3的tensor
print(c,d)
#除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size()
print(c.shape)
"""
需要注意的是,t.Tensor(*sizes)创建tensor时,系统不会马上分配空间,
只是会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其它操作都是在创建完tensor之后马上进行空间分配。
"""
"其他常用的创建tensor的方法"
print(t.ones(2,3))
print(t.zeros(2,3))
print(t.arange(1,6,2).shape)
print(t.linspace(1,10,3))
print(t.randn(2,3,device=t.device('cpu')))
print(t.randperm(5))#长度为5的随机排列
print(t.eye(2,4,dtype=t.int))#对角线为1,不要求行列数一致
scalar=t.tensor(3.14159)
print('scalar: %s, shape of sclar: %s' %(scalar, scalar.shape))
vector=t.tensor([1,2])
print('vector: %s, shape of vector: %s' %(vector, vector.shape))
tensor = t.Tensor(1,2) # 注意和t.tensor([1, 2])的区别
print(tensor.shape)
matrix = t.tensor([[0.1, 1.2], [2.2, 3.1], [4.9, 5.2]])
print(matrix,matrix.shape)
empty_tensor = t.tensor([])
print(empty_tensor.shape)
"""
常用Tensor操作
通过tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。
view不会修改自身的数据,返回的新tensor与源tensor共享内存,也即更改其中的一个,另外一个也会跟着改变。
在实际应用中可能经常需要添加或减少某一维度,这时候squeeze和unsqueeze两个函数就派上用场了。
"""
a=t.arange(0,6)
print(a.view(2,3))
b=a.view(-1,3)#当某一维为-1的时候,会自动计算它的大小
print(b.size())
b.unsqueeze(1)#注意,在第一维上增加'1'(下标从0开始)
print(b[:,None].shape)
b.unsqueeze(-2)#-2表示倒数第二个维度
print(b[None,:].shape)
c=b.view(1,1,1,2,3)
print(c.squeeze(0))#压缩第0维的'1'
print(c.squeeze())#把所有维度为‘1’的压缩
a[1]=100
print(b)# a修改,b作为view之后的,也会跟着修改
"""
resize是另一种可用来调整size的方法,但与view不同,它可以修改tensor的大小。
如果新大小超过了原大小,会自动分配新的内存空间,而如果新大小小于原大小,则之前的数据依旧会被保存,看一个例子
"""
print(b.resize_(1,3))
print(b.resize_(3,3))#旧的数据依旧保存,多出的大小会分配空间
"""
索引操作
"""
a=t.randn(3,4)
print(a)
print(a[0])#第0行(下标从0开始)
print(a[:,0])#第0列
print(a[0][2])# 第0行第2个元素,等价于a[0, 2]
print(a[:2])#前两行
print(a[:2,0:2])# 前两行,第0,1列
print(a[0:1, :2]) # 第0行,前两列
print(a[0, :2]) # 注意两者的区别:形状不同
# None类似于np.newaxis, 为a新增了一个轴
# 等价于a.view(1, a.shape[0], a.shape[1])
print(a[None].shape)
print(a[:,None,:].shape)
print(a[:,None,None,:,None].shape)
"""
高级索引
"""
x=t.arange(0,27).view(3,3,3)
print(x)
print(x[[1,2],[1,2],[2,0]])# x[1,1,2]和x[2,2,0]
print(x[[2,1,0],[0],[1]])# x[2,0,1],x[1,0,1],x[0,0,1]
print(x[[0, 2], ...]) # x[0] 和 x[2]
"""
逐元素操作
这部分操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,
此类操作的输入与输出形状一致
"""
a = t.arange(0,6).view(2,3)
print("a:",a)
#print("t.cos(a):",t.cos(a))
print("a % 3:",a % 3) # t.fmod(a, 3)
print("a ** 2:",a ** 2) # t.pow(a, 2)
print("t.clamp(a, min=2, max=4)",t.clamp(a,min=2,max=4))
# 取a中的每一个元素与3相比较大的一个 (小于3的截断成3)
print(a)
print(t.clamp(a, min=3))
#b=a.sin_()# 效果同 a = a.sin();b=a ,但是更高效节省显存
#print(b)
"""
归并操作
此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。
如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和
以上大多数函数都有一个参数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。
"""
b = t.ones(2,3)
print("b.sum():",b.sum(dim=0,keepdim=True))
print("b.sum():",b.sum(dim=0,keepdim=False))# keepdim=False,不保留维度"1",注意形状
a = t.arange(0, 6).view(2, 3)
print(a)
print(a.cumsum(dim=1)) # 沿着行累加
"""
比较
比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有一些则类似于归并操作
t.max(tensor):返回tensor中最大的一个数
t.max(tensor,dim):指定维上最大的数,返回tensor和下标
t.max(tensor1, tensor2): 比较两个tensor相比较大的元素
"""
a = t.linspace(0, 15, 6).view(2, 3)
b = t.linspace(15, 0, 6).view(2, 3)
print(a>b)
print(a[a>b])
print(t.max(a))
print(t.max(b,dim=1))
# 第一个返回值的15和6分别表示第0行和第1行最大的元素
# 第二个返回值的0和0表示上述最大的数是该行第0个元素
print(t.max(a,b))
print(t.clamp(a,min=10))# 比较a和10较大的元素
"""
线性代数
trace:对角线元素之和(矩阵的迹)
diag:对角线元素
triu/tril:矩阵的上三角/下三角
mm/bmm:矩阵乘法,batch的矩阵乘法
t:转置
dot/cross:内积/外积
inverse:求逆矩阵
svd:奇异值分解
"""
b=a.t()
print(b.is_contiguous())#矩阵的转置为导致存储空间不连续,要调用它的.contiguous方法将其转为连续
print(b.contiguous())
"""
Tensor和Numpy
Tensor和Numpy数组之间具有很高的相似性,彼此之间的互相操作也非常简单高效。
需要注意的是,Numpy和Tensor共享内存。
由于Numpy历史悠久,支持丰富的操作,
所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小
"""
import numpy as np
a=np.ones([2,3],dtype=np.float32)
print(a)
b=t.from_numpy(a)
print(b)
b=t.Tensor(a)#也可以直接将numpy对象传入Tensor
print(b)
a[0][1]=100
print(b)
c=b.numpy()#a,b,c三个对象共享内存
print(c)#当numpy的数据类型和Tensor的类型不一样的时候,数据会被复制,不会共享内存
a=np.ones([2,3])
print(a.dtype)#注意和上面的a的区别(dtype不是float32)
b=t.Tensor(a)#此处进行拷贝,不共享内存
print(b.dtype)
c=t.from_numpy(a)#注意c的类型(DoubleTensor)
print(c)
a[0,1]=100
print(b,c)#b与a不共享内存,c与a共享内存
#不论输入的类型是什么,t.tensor都会进行数据拷贝,不会共享内存
tensor=t.tensor(a)
tensor[0,0]=2
print(a)
"""
广播法则(broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。 Numpy的广播法则定义如下:
1.让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐
2.两个数组要么在某一个维度的长度一致,要么其中一个为1,否则不能计算
3.当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状
PyTorch当前已经支持了自动广播法则,但是笔者还是建议读者通过以下两个函数的组合手动实现广播法则,这样更直观,更不易出错:
1.unsqueeze或者view,或者tensor[None],:为数据某一维的形状补1,实现法则1
2.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)
print(a+b)
#手动广播法则
print(a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2))
e=a.unsqueeze(0).expand(1000000,3,2)
print(e)
"""
内部结构
tensor分为头信息区(Tensor)和存储区(Storage),
信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,
而真正的数据则保存成连续数组。
由于数据动辄成千上万,因此信息区元素占用内存较少,
主要内存占用则取决于tensor中元素的数目,也即存储区的大小。
"""
a=t.arange(0,6)
print(a.storage())
b=a.view(2,3)
print(b.storage())
# 一个对象的id值可以看作它在内存中的地址
# storage的内存地址一样,即是同一个storage
print(id(b.storage()) == id(a.storage()))
a[1]=100
print(b)#a改变,b也随之改变,因为他们共享storage
c=a[2:]
print(c.storage())
#data_ptr返回tensor首元素的内存地址
print(c.data_ptr(),a.data_ptr())
"""
其他有关Tensor的话题
"""
"GPU/CPU"
#tensor可以很随意的在gpu/cpu上传输。
# 使用tensor.cuda(device_id)或者tensor.cpu()。
# 另外一个更通用的方法是tensor.to(device)"
a=t.randn(3,4)
print(a.device)
"""
线性回归:
线性回归是机器学习入门知识,应用十分广泛。
线性回归利用数理统计中回归分析,来确定两种或两种以上变量间相互依赖的定量关系的,
其表达形式为y = wx+b+e,e为误差服从均值为0的正态分布。
利用随机梯度下降法更新参数w和b来最小化损失函数,最终学得w和b的数值。
"""
import torch as t
from matplotlib import pyplot as plt
from IPython import display
device = t.device('cpu')
# 设置随机数种子,保证在不同电脑上运行时下面的输出一致
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分布
x, y = get_fake_data(batch_size=16)
plt.scatter(x.squeeze().cpu().numpy(), y.squeeze().cpu().numpy())
# 随机初始化参数
w = t.rand(1, 1).to(device)
b = t.zeros(1, 1).to(device)
lr = 0.001 # 学习率
for ii in range(2000): # 2000次迭代
x, y = get_fake_data() # 生成数据
# 前向传播forward,计算loss
y_pred = x.mm(w) + b.expand_as(y)
loss = 0.5 * (y_pred - y) ** 2 # 均方误差就loss
loss = loss.sum()
# 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 % 1000 == 0: # 没1000次迭代,画一次图
# 画图
display.clear_output(wait=True)
x = t.arange(0, 20).view(-1, 1).float()
y = x.mm(w) + b.expand_as(x)
plt.plot(x.numpy(), y.numpy()) # 预测
x2, y2 = get_fake_data(batch_size=20)
plt.scatter(x2.numpy(), y2.numpy())
plt.xlim(0, 20)
plt.ylim(0, 41)
plt.show()
plt.pause(0.5)
print(w.squeeze(), b.squeeze())
from __future__ import print_function
import torch as t
#创建tensor的时候指定requires_grad
a=t.randn(3,4,requires_grad=True)
print(a)
b=t.zeros(3,4).requires_grad_()
print(b)
c=a.add(b)
print(c)
d=c.sum()
d.backward()# 反向传播
print(d)
print(d.requires_grad)
print(a.grad)
# 此处虽然没有指定c需要求导,但c依赖于a,而a需要求导,
# 因此c的requires_grad属性会自动设为True
print(a.requires_grad, b.requires_grad, c.requires_grad)
print(a.is_leaf,b.is_leaf,c.is_leaf)
"""
计算y=x^2*e^x的导数
autograd的计算结果与自动求导计算结果的误差
"""
def f(x):
'计算y'
y=x**2*t.exp(x)
return y
def gradf(x):
'手动求导数'
dx=2*x*t.exp(x)+x**2*t.exp(x)
return dx
x=t.randn(3,4,requires_grad=True)
y=f(x)
print(y)
y.backward(t.ones(y.size()))#gradient形状与y一致
print(x.grad)
# autograd的计算结果与利用公式手动计算的结果一致
print(gradf(x))
"""
计算图
"""
x=t.ones(1)
b=t.rand(1,requires_grad=True)
w=t.rand(1,requires_grad=True)
y=w*x#等价于y=w.mul(x)
z=y+b#等价于z=y.add(b)
print(x.requires_grad,b.requires_grad,w.requires_grad)
# 虽然未指定y.requires_grad为True,但由于y依赖于需要求导的w
# 故而y.requires_grad为True
print(y.requires_grad)
# grad_fn可以查看这个variable的反向传播函数,
# z是add函数的输出,所以它的反向传播函数是AddBackward
print(z.grad_fn)
# next_functions保存grad_fn的输入,是一个tuple,tuple的元素也是Function
# 第一个是y,它是乘法(mul)的输出,所以对应的反向传播函数y.grad_fn是MulBackward
# 第二个是b,它是叶子节点,由用户创建,grad_fn为None,但是有
print(z.grad_fn.next_functions)
# variable的grad_fn对应着和图中的function相对应
print(z.grad_fn.next_functions[0][0] == y.grad_fn)
# 第一个是w,叶子节点,需要求导,梯度是累加的
# 第二个是x,叶子节点,不需要求导,所以为None
print(y.grad_fn.next_functions)
# 叶子节点的grad_fn是None
print(w.grad_fn,x.grad_fn)
"""
计算w的梯度的时候,需要用到x的数值,这些数值在前向过程中会保存成buffer,在计算完梯度之后会自动清空。
为了能够多次反向传播需要指定retain_graph来保留这些buffer。
"""
#使用retain_graph来保存buffer
print(z.backward(retain_graph=True))
print(w.grad)
#多次反向传播,梯度累加,这也就是w中AccumulateGrad标识的含义
z.backward()
print(w.grad)
"""
PyTorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建,所以它能够使用Python控制语句(如for、if等)根据需求创建计算图。
这点在自然语言处理领域中很有用,它意味着你不需要事先构建所有可能用到的图的路径,图在运行时才构建
"""
def abs(x):
if x.data[0]>0:
return x
else:
return -x
x=t.ones(1,requires_grad=True)
y=abs(x)
y.backward()
print(x.grad)
x=-1*t.ones(1)
x=x.requires_grad_()
y=abs(x)
y.backward()
print(x.grad)
def f2(x):
result=1
for li in x:
if li.item()>0:
result=li*result
return result
x=t.arange(-2.0,4.0,requires_grad=True)
y=f2(x)#y=x[3]*x[4]*x[5]
y.backward()
print(x.grad)
"""
变量的requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,
那么所有依赖它的节点requires_grad都是True。
有些时候我们可能不希望autograd对tensor求导。
认为求导需要缓存许多中间结构,增加额外的内存/显存开销,那么我们可以关闭自动求导。
对于不需要反向传播的情景(如inference,即测试推理时),关闭自动求导可实现一定程度的速度提升,并节省约一半显存,因其不需要分配空间计算梯度。
"""
x=t.ones(1,requires_grad=True)
w=t.rand(1,requires_grad=True)
y=x*w
#y依赖于w,而w.requires_grad = True
print(x.requires_grad,w.requires_grad,y.requires_grad)
with t.no_grad():
x=t.ones(1)
w=t.rand(1,requires_grad=True)
y=x*w
# y依赖于w和x,虽然w.requires_grad = True,但是y的requires_grad依旧为False
print(x.requires_grad, w.requires_grad, y.requires_grad)
t.set_grad_enabled(False)
X=t.ones(1)
w=t.rand(1,requires_grad=True)
y=x*w
# y依赖于w和x,虽然w.requires_grad = True,但是y的requires_grad依旧为False
print(x.requires_grad, w.requires_grad, y.requires_grad)
"""
如果我们想要修改tensor的数值,但是又不希望被autogard记录,
那么我们可以会tensor.data进行操作或者tensor.detach()
"""
a = t.ones(3,4,requires_grad=True)
b = t.ones(3,4,requires_grad=True)
c = a * b
print(a.data)# 还是一个tensor
print(a.data.requires_grad) # 但是已经是独立于计算图之外
# 近似于 tensor=a.data, 但是如果tensor被修改,backward可能会报错
tensor = a.detach()
print(tensor.requires_grad)
另外值得注意的是,只有对variable的操作才能使用autograd,如果对variable的data直接进行操作,将无法使用反向传播。除了对参数初始化,一般我们不会修改variable.data的值。
在PyTorch中计算图的特点可总结如下:
autograd根据用户对variable的操作构建其计算图。对变量的操作抽象为Function。
对于那些不是任何函数(Function)的输出,由用户创建的节点称为叶子节点,叶子节点的grad_fn为None。叶子节点中需要求导的variable,具有AccumulateGrad标识,因其梯度是累加的。
variable默认是不需要求导的,即requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都为True。
variable的volatile属性默认为False,如果某一个variable的volatile属性被设为True,那么所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导,volatile的优先级比requires_grad高。
多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需指定retain_graph=True来保存这些缓存。
非叶子节点的梯度计算完之后即被清空,可以使用autograd.grad或hook技术获取非叶子节点的值。
variable的grad与data形状一致,应避免直接修改variable.data,因为对data的直接操作无法利用autograd进行反向传播
反向传播函数backward的参数grad_variables可以看成链式求导的中间结果,如果是标量,可以省略,默认为1
PyTorch采用动态图设计,可以很方便地查看中间层的输出,动态的设计计算图结构。
目前绝大多数函数都可以使用autograd
实现反向求导,但如果需要自己写一个复杂的函数,不支持自动反向求导怎么办? 写一个Function
,实现它的前向传播和反向传播代码,Function
对应于计算图中的矩形, 它接收参数,计算并返回结果。下面给出一个例子
class Mul(Function):
@staticmethod
def forward(ctx, w, x, b, x_requires_grad = True):
ctx.x_requires_grad = x_requires_grad
ctx.save_for_backward(w,x)
output = w * x + b
return output
@staticmethod
def backward(ctx, grad_output):
w,x = ctx.saved_variables
grad_w = grad_output * x
if ctx.x_requires_grad:
grad_x = grad_output * w
else:
grad_x = None
grad_b = grad_output * 1
return grad_w, grad_x, grad_b, None
分析如下:
自定义的Function需要继承autograd.Function,没有构造函数__init__,forward和backward函数都是静态方法
forward函数的输入和输出都是Tensor,backward函数的输入和输出都是Variable
backward函数的输出和forward函数的输入一一对应,backward函数的输入和forward函数的输出一一对应
backward函数的grad_output参数即t.autograd.backward中的grad_variables
如果某一个输入不需要求导,直接返回None,如forward中的输入参数x_requires_grad显然无法对它求导,直接返回None即可
反向传播可能需要利用前向传播的某些中间结果,需要进行保存,否则前向传播结束后这些对象即被释放
Function的使用利用Function.apply(variable)
from torch.autograd import Function
class MultiplyAdd(Function):
@staticmethod
def forward(ctx, w, x, b):
print('type in forward',type(x))
ctx.save_for_backward(w,x)
output = w * x + b
return output
@staticmethod
def backward(ctx, grad_output):
w,x = ctx.saved_variables
print('type in backward',type(x))
grad_w = grad_output * x
grad_x = grad_output * w
grad_b = grad_output * 1
return grad_w, grad_x, grad_b
x = V(t.ones(1))
w = V(t.rand(1), requires_grad = True)
b = V(t.rand(1), requires_grad = True)
print('开始前向传播')
z=MultiplyAdd.apply(w, x, b)
print('开始反向传播')
z.backward() # 等效
# x不需要求导,中间过程还是会计算它的导数,但随后被清空
x.grad, w.grad, b.grad
开始前向传播
type in forward
开始反向传播
type in backward
(None, tensor([1.]), tensor([1.]))
x = V(t.ones(1))
w = V(t.rand(1), requires_grad = True)
b = V(t.rand(1), requires_grad = True)
print('开始前向传播')
z=MultiplyAdd.apply(w,x,b)
print('开始反向传播')
# 调用MultiplyAdd.backward
# 输出grad_w, grad_x, grad_b
z.grad_fn.apply(V(t.ones(1)))
开始前向传播
type in forward
开始反向传播
type in backward
(tensor([1.]), tensor([0.0806], grad_fn=), tensor([1.]))
之所以forward函数的输入是tensor,而backward函数的输入是variable,是为了实现高阶求导。backward函数的输入输出虽然是variable,但在实际使用时autograd.Function会将输入variable提取为tensor,并将计算结果的tensor封装成variable返回。在backward函数中,之所以也要对variable进行操作,是为了能够计算梯度的梯度(backward of backward)。下面举例说明,有关torch.autograd.grad的更详细使用请参照文档
x = V(t.Tensor([5]), requires_grad=True)
y = x ** 2
grad_x = t.autograd.grad(y, x, create_graph=True)
grad_x # dy/dx = 2 * x
(tensor([10.], grad_fn=),)
grad_grad_x = t.autograd.grad(grad_x[0],x)
grad_grad_x # 二阶导数 d(2x)/dx = 2
(tensor([2.]),)
这种设计虽然能让autograd具有高阶求导功能,但其也限制了Tensor的使用,因autograd中反向传播的函数只能利用当前已经有的Variable操作。这个设计是在0.2版本新加入的,为了更好的灵活性,也为了兼容旧版本的代码,PyTorch还提供了另外一种扩展autograd的方法。PyTorch提供了一个装饰器@once_differentiable,能够在backward函数中自动将输入的variable提取成tensor,把计算结果的tensor自动封装成variable。有了这个特性我们就能够很方便的使用numpy/scipy中的函数,操作不再局限于variable所支持的操作。但是这种做法正如名字中所暗示的那样只能求导一次,它打断了反向传播图,不再支持高阶求导。
上面所描述的都是新式Function,还有个legacy Function,可以带有__init__方法,forward和backwad函数也不需要声明为@staticmethod,但随着版本更迭,此类Function将越来越少遇到,在此不做更多介绍。
此外在实现了自己的Function之后,还可以使用gradcheck函数来检测实现是否正确。gradcheck通过数值逼近来计算梯度,可能具有一定的误差,通过控制eps的大小可以控制容忍的误差。
关于这部份的内容可以参考github上开发者们的讨论[2]。
下面举例说明如何利用Function实现sigmoid Function
class Sigmoid(Function):
@staticmethod
def forward(ctx, x):
output = 1 / (1 + t.exp(-x))
ctx.save_for_backward(output)
return output
@staticmethod
def backward(ctx, grad_output):
output, = ctx.saved_variables
grad_x = output * (1 - output) * grad_output
return grad_x
# 采用数值逼近方式检验计算梯度的公式对不对
test_input = V(t.randn(3,4), requires_grad=True)
t.autograd.gradcheck(Sigmoid.apply, (test_input,), eps=1e-3)
True
def f_sigmoid(x):
y = Sigmoid.apply(x)
y.backward(t.ones(x.size()))
def f_naive(x):
y = 1/(1 + t.exp(-x))
y.backward(t.ones(x.size()))
def f_th(x):
y = t.sigmoid(x)
y.backward(t.ones(x.size()))
x=V(t.randn(100, 100), requires_grad=True)
%timeit -n 100 f_sigmoid(x)
%timeit -n 100 f_naive(x)
%timeit -n 100 f_th(x)
E:\Anaconda\lib\site-packages\ipykernel_launcher.py:11: DeprecationWarning: 'saved_variables' is deprecated; use 'saved_tensors'
# This is added back by InteractiveShellApp.init_path()
The slowest run took 4.77 times longer than the fastest. This could mean that an intermediate result is being cached.
501 µs ± 412 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
357 µs ± 123 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
The slowest run took 4.84 times longer than the fastest. This could mean that an intermediate result is being cached.
353 µs ± 286 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
显然f_sigmoid
要比单纯利用autograd
加减和乘方操作实现的函数快不少,因为f_sigmoid的backward优化了反向传播的过程。另外可以看出系统实现的buildin接口(t.sigmoid)更快
"""
在反向传播过程中非叶子节点的导数计算完之后即被清空。若想查看这些变量的梯度,有两种方法:
使用autograd.grad函数
使用hook
autograd.grad和hook方法都是很强大的工具,更详细的用法参考官方api文档,这里举例说明基础的使用。
推荐使用hook方法,但是在实际使用中应尽量避免修改grad的值。
"""
x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
# y依赖于w,而w.requires_grad = True
z = y.sum()
print(x.requires_grad, w.requires_grad, y.requires_grad)
z.backward()
print(x.grad, w.grad, y.grad)
# 第一种方法:使用grad获取中间变量的梯度
x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
z = y.sum()
# z对y的梯度,隐式调用backward()
print(t.autograd.grad(z, y))
# 第二种方法:使用hook
# hook是一个函数,输入是梯度,不应该有返回值
def variable_hook(grad):
print('y的梯度:',grad)
x = t.ones(3, requires_grad=True)
w = t.rand(3, requires_grad=True)
y = x * w
# 注册hook
hook_handle = y.register_hook(variable_hook)
z = y.sum()
z.backward()
# 除非你每次都要用hook,否则用完之后记得移除hook
hook_handle.remove()
"""
用Variable实现线性回归
"""
import torch as t
from matplotlib import pyplot as plt
from IPython import display
import numpy as np
# 设置随机数种子,为了在不同人电脑上运行时下面的输出一致
t.manual_seed(1000)
def get_fake_data(batch_size=8):
''' 产生随机数据:y = x*2 + 3,加上了一些噪声'''
x = t.rand(batch_size,1) * 5
y = x * 2 + 3 + t.randn(batch_size, 1)
return x, y
# 来看看产生x-y分布是什么样的
x, y = get_fake_data()
plt.scatter(x.squeeze().numpy(), y.squeeze().numpy())
# 随机初始化参数
w = t.rand(1, 1, requires_grad=True)
b = t.zeros(1, 1, requires_grad=True)
losses = np.zeros(500)
lr = 0.005 # 学习率
for ii in range(500):
x, y = get_fake_data(batch_size=32)
# forward:计算loss
y_pred = x.mm(w) + b.expand_as(y)
loss = 0.5 * (y_pred - y) ** 2
loss = loss.sum()
losses[ii] = loss.item()
# backward:手动计算梯度
loss.backward()
# 更新参数
w.data.sub_(lr * w.grad.data)
b.data.sub_(lr * b.grad.data)
# 梯度清零
w.grad.data.zero_()
b.grad.data.zero_()
if ii % 50 == 0:
# 画图
display.clear_output(wait=True)
x = t.arange(0, 6).view(-1, 1).float()
y = x.mm(w.data) + b.data.expand_as(x)
plt.plot(x.numpy(), y.numpy()) # predicted
x2, y2 = get_fake_data(batch_size=20)
plt.scatter(x2.numpy(), y2.numpy()) # true data
plt.xlim(0, 5)
plt.ylim(0, 13)
plt.show()
plt.pause(0.5)
print(w.item(), b.item())
plt.plot(losses)
plt.ylim(5,50)
https://github.com/chenyuntc/pytorch-book/blob/master/chapter3-Tensor%E5%92%8Cautograd/Autograd.ipynb
https://blog.csdn.net/V_lq6h/article/details/88320308